From dafc66a91cefdb2a3cd1e9c90ae6e1d89f7cd3d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicu=20Micleu=C8=99anu?= Date: Sun, 14 Nov 2021 14:18:20 +0200 Subject: [PATCH] Add .start() and .stop() methods --- .github/workflows/main.yml | 2 +- README.md | 8 ++- index.d.ts | 33 +++------ index.js | 6 +- lib/cache.js | 138 ++++++++++++++++++++++++------------- package.json | 10 +-- test/cache.test.js | 71 ++++++++++++++----- 7 files changed, 168 insertions(+), 100 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5758407..c49e771 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: - node-version: [10.x, 12.x, 14.x, 16.x] + node-version: [12.x, 14.x, 16.x] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index d6b4256..8639df8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ -# 0.5.3 +[![npm version](https://img.shields.io/npm/v/recache.svg?logo=npm&style=flat-square)](https://www.npmjs.com/package/recache) +[![npm downloads](https://img.shields.io/npm/dm/recache.svg?style=flat-square)](https://www.npmjs.com/package/recache) +[![npm types](https://img.shields.io/npm/types/recache.svg?style=flat-square)](https://www.npmjs.com/package/recache) +[![node version](https://img.shields.io/node/v/recache.svg?style=flat-square)](https://www.npmjs.com/package/recache) +[![license](https://img.shields.io/npm/l/recache.svg?style=flat-square)](https://www.npmjs.com/package/recache) `recache` is a file system cache, it watches recursively a directory tree or a file content and updates the data on changes, optionally it may provide the @@ -21,7 +25,7 @@ reading its paths, stats and content directly using only its API. `recache` is a pure JS solution and does not require any code compilation on installation. #### Any feedback is welcome! -#### Works with node.js 8.0+! +#### Works with node.js 10.0+! ## Installation diff --git a/index.d.ts b/index.d.ts index 5ba07e1..b417f73 100644 --- a/index.d.ts +++ b/index.d.ts @@ -480,30 +480,15 @@ declare namespace recache { * Create and start a file system cache */ declare function recache( - path: string, - options: recache.CacheOptions, - callback: recache.CacheCallback + ...args: + | [path: string] + | [path: string, options: recache.CacheOptions] + | [path: string, callback: recache.CacheCallback] + | [ + path: string, + options: recache.CacheOptions, + callback: recache.CacheCallback + ] ): recache.Cache; -/** - * Create and start a file system cache - */ -declare function recache( - path: string, - options: recache.CacheOptions -): recache.Cache; - -/** - * Create and start a file system cache - */ -declare function recache( - path: string, - callback: recache.CacheCallback -): recache.Cache; - -/** - * Create and start a file system cache - */ -declare function recache(path: string): recache.Cache; - export = recache; \ No newline at end of file diff --git a/index.js b/index.js index bc5e01b..1d17b0e 100644 --- a/index.js +++ b/index.js @@ -3,12 +3,10 @@ const Cache = require('recache/lib/cache'); /** - * @typedef {import('recache').CacheCallback} CacheCallback - * @typedef {import('recache').CacheOptions} CacheOptions - * @typedef {import('recache/lib/cache').CacheArgs} CacheArgs + * @typedef {import('recache')} recache */ /** - * @param {CacheArgs} args + * @type {recache} */ module.exports = (...args) => new Cache(...args); \ No newline at end of file diff --git a/lib/cache.js b/lib/cache.js index 2cb0d2b..81f41f0 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -16,15 +16,15 @@ const { isBoolean, isFunction, isInstanceOf, - isNull, + isNullable, isObjectOf, - isString, - isUnionOf + isString } = require('r-assign/lib'); /** * @typedef {import('fs').FSWatcher} FSWatcher * @typedef {import('fs').BigIntStats} BigIntStats + * @typedef {import('recache')} recache * @typedef {import('recache').Cache} CacheInterface * @typedef {import('recache').CacheElement} CacheElement * @typedef {import('recache').CacheElementType} CacheElementType @@ -34,15 +34,6 @@ const { * @typedef {import('recache').UserData} UserData */ -/** - * @typedef {| - * [string] | - * [string, CacheOptions] | - * [string, CacheCallback] | - * [string, CacheOptions, CacheCallback] - * } CacheArgs - */ - /** * @typedef {(cache: Cache) => void} CacheCallback */ @@ -116,7 +107,7 @@ const getOptions = (options) => { const isCacheFilter = isFunction([isString, isBigIntStats], isBoolean); return rAssign({ - filter: getType(isUnionOf([isCacheFilter, isNull]), null), + filter: getType(isNullable(isCacheFilter), null), persistent: getType(isBoolean, false), store: getType(isBoolean, false) }, options); @@ -150,7 +141,7 @@ class Cache extends EventEmitter { /** * Cache constructor - * @param {CacheArgs} args + * @param {Parameters} args */ constructor(...args) { @@ -180,38 +171,47 @@ class Cache extends EventEmitter { this._updating = false; /** @type {Map} */ this._watchers = new Map(); + this._watching = false; // Allow multiple elements updates in the same time this.setMaxListeners(0); - // Start preparing the cache - Cache.prepareCache(this, getCallback(options, callback)); + // Start watching process + this.start(getCallback(options, callback)); } /** * Destroy the cache data + * @param {() => void} [callback] */ - destroy() { + destroy(callback) { // Check if the cache is not already destroyed if (!this._destroyed) { - // Stop watching the root path for changes - if (this._listener) { - unwatchFile(this.path, this._listener); - } - - // Close the fs watchers - this._watchers.forEach((watcher) => { - watcher.close(); - }); + // Stop watching process + this.stop(); // Mark as destroyed this._destroyed = true; + // Reset cache properties + this._changed = false; + this._listener = null; + this._ready = false; + this._updating = false; + + // Clear the elements container + this._container.clear(); + // Emit the destroy event a bit later setImmediate(() => { this.emit(destroyEvent); + + // Call the provided callback + if (typeof callback === 'function') { + callback(); + } }); } } @@ -267,6 +267,45 @@ class Cache extends EventEmitter { return list; } + /** + * Start the watching process + * @param {CacheCallback | null} [callback] + */ + start(callback) { + if (!this._destroyed && !this._watching) { + this._watching = true; + + // Start preparing the cache + Cache.prepareCache(this, callback); + } + + return this; + } + + /** + * Stop the watching process + */ + stop() { + if (!this._destroyed && this._watching) { + this._watching = false; + + // Stop watching the root path for changes + if (this._listener) { + unwatchFile(this.path, this._listener); + } + + // Close the fs watchers + this._watchers.forEach((watcher) => { + watcher.close(); + }); + + // Clear the fs watchers container + this._watchers.clear(); + } + + return this; + } + /** * Add a file system watcher for the provided element path and location * @param {Cache} cache @@ -275,32 +314,35 @@ class Cache extends EventEmitter { */ static addWatcher(cache, path, location) { - const watcher = watch(path, cache._options); + if (cache._watching) { - // Add the fs watcher to the list of watchers - cache._watchers.set(location, watcher); + const watcher = watch(path, cache._options); - // Set the fs watcher event listeners - watcher.on(errorEvent, (error) => { - Cache.emitElementError(cache, location, error); - }).on(changeEvent, () => { - Cache.listenChange(cache, location); - }); + // Add the fs watcher to the list of watchers + cache._watchers.set(location, watcher); - // Watch root location for rename, move or remove - if (location === rootSymbol) { + // Set the fs watcher event listeners + watcher.on(errorEvent, (error) => { + Cache.emitElementError(cache, location, error); + }).on(changeEvent, () => { + Cache.listenChange(cache, location); + }); - // Create and save root change listener - cache._listener = () => { - Cache.listenChange(cache, rootSymbol); - }; + // Watch root location for rename, move or remove + if (location === rootSymbol) { - // Watch provided path for changes - watchFile(path, cache._options, cache._listener); - } + // Create and save root change listener + cache._listener = () => { + Cache.listenChange(cache, rootSymbol); + }; - // Emit the watched path - cache.emit(watchEvent, path); + // Watch provided path for changes + watchFile(path, cache._options, cache._listener); + } + + // Emit the watched path + cache.emit(watchEvent, path); + } } /** @@ -475,7 +517,7 @@ class Cache extends EventEmitter { * @param {Cache} cache * @param {string} location * @param {string[]} content - * @param {number} [index=0] + * @param {number} [index] * @returns {Promise} */ static async iterateDirectory(cache, location, content, index = 0) { @@ -561,7 +603,7 @@ class Cache extends EventEmitter { /** * Initiate cache * @param {Cache} cache - * @param {CacheCallback | null} callback + * @param {CacheCallback | null} [callback] * @returns {Promise} */ static async prepareCache(cache, callback) { diff --git a/package.json b/package.json index 2893c27..dc7b93b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "recache", - "version": "0.5.3", + "version": "0.6.0", "description": "File system cache that listens to file changes", "keywords": [ "cache", @@ -30,7 +30,7 @@ "eslint-check": "eslint . --ext .js,.ts", "eslint-fix": "npm run eslint-check -- --fix", "prepublishOnly": "npm test && npm run check", - "test": "tap -b test/*.test.js", + "test": "tap -b --no-check-coverage --timeout=60 test/*.test.js", "test-cov-html": "npm test -- --coverage-report=html", "test-lcov": "npm test -- --coverage-report=lcov", "ts-check": "tsc" @@ -40,9 +40,9 @@ }, "devDependencies": { "@types/node": "12.x", - "@typescript-eslint/eslint-plugin": "4.x", - "@typescript-eslint/parser": "4.x", - "eslint": "7.x", + "@typescript-eslint/eslint-plugin": "5.x", + "@typescript-eslint/parser": "5.x", + "eslint": "8.x", "rimraf": "3.x", "tap": "15.x", "typescript": "4.x" diff --git a/test/cache.test.js b/test/cache.test.js index 37a29c7..bcdfb11 100644 --- a/test/cache.test.js +++ b/test/cache.test.js @@ -11,11 +11,14 @@ const { } = require('fs'); const { join, relative } = require('path'); const rimraf = require('rimraf'); -const { equal, fail, match, ok, teardown, test } = require('tap'); +const { equal, fail, match, ok, teardown, test, throws } = require('tap'); const { promisify } = require('util'); const Cache = require('recache/lib/cache'); +// Timeout for fs operations to work fine in CI +const timeout = 1000; + const nonExistent = 'non-existent'; const testDir = join(__dirname, 'test-dir'); const slink = join(testDir, 'slink'); @@ -52,6 +55,18 @@ test('Cache.normalizeLocation()', ({ end }) => { end(); }); +test('new Cache(): Invalid parameters', ({ end }) => { + + throws(() => { + const cache = new Cache(); + + cache.on('ready', () => { + fail('Cache should never be ready for invalid parameters'); + }); + }, TypeError('"path" argument must be a non-empty string')); + + end(); +}); test('new Cache(): Non-existent element', ({ end }) => { @@ -99,9 +114,7 @@ test('new Cache(): Cache file with error', async ({ end }) => { await promisify(writeFile)(path, ''); - const cache = new Cache(path, { - store: true - }); + const cache = new Cache(path, { store: true }); return new Promise((resolve) => { cache.on('destroy', () => { @@ -172,9 +185,7 @@ test('new Cache(): Non-empty directory with store', ({ end }) => { const path = join(testDir, 'dir'); - const cache = new Cache(path, { - store: true - }); + const cache = new Cache(path, { store: true }); cache.on('destroy', () => { end(); @@ -207,9 +218,7 @@ test('new Cache(): Root file', ({ end }) => { test('new Cache(): Remove root directory on ready', ({ end }) => { const path = join(testDir, 'empty-dir'); - const cache = new Cache(path, { - persistent: true - }); + const cache = new Cache(path, { persistent: true }); cache.on('destroy', () => { end(); @@ -226,9 +235,7 @@ test('new Cache(): Modify root file before ready', ({ end }) => { const path = join(testDir, 'dir', 'file'); - const cache = new Cache(path, { - persistent: true - }); + const cache = new Cache(path, { persistent: true }); cache.on('change', (element) => { equal(element.location, ''); @@ -250,12 +257,16 @@ test('new Cache(): Modify root file on ready', ({ end }) => { const path = join(testDir, 'dir', 'file'); - const cache = new Cache(path); + const cache = new Cache(path, { persistent: true }, (c) => { + equal(cache, c); + }); cache.on('destroy', () => { end(); - }).on('ready', async () => { - await promisify(writeFile)(path, 'data'); + }).on('ready', () => { + setTimeout(async () => { + await promisify(writeFile)(path, 'data'); + }, timeout); }).on('update', (c) => { equal(cache, c); cache.destroy(); @@ -293,6 +304,19 @@ test('Cache.prototype.destroy()', ({ end }) => { }); }); +test('Cache.prototype.destroy() with callback', ({ end }) => { + + const cache = new Cache(testDir); + + cache.on('error', (error) => { + fail(error.message); + }).on('ready', () => { + cache.destroy(() => { + end(); + }); + }); +}); + test('Cache.prototype.get()', ({ end }) => { const path = join(testDir); @@ -349,4 +373,19 @@ test('Cache.prototype.list()', ({ end }) => { }); }); +test('Cache.prototype.stop() + .start()', ({ end }) => { + + const path = join(testDir, 'dir'); + + const cache = new Cache(path); + + cache.stop().start(() => { + cache.destroy(); + }).on('destroy', () => { + end(); + }).on('error', (error) => { + fail(error.message); + }); +}); + teardown(cleanUp); \ No newline at end of file