diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d9fa2a..cff5497 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +### 4.0.0 (2024-06-20) +* __BREAKING CHANGE__: The `maxAge` option now ensures that the cache becomes invalid after the specified cache lifetime is reached based on `stats.mtimeMs` (last modification time of the cache file) instead of relying on an in memory timeout that invalidates the cache. This ensures that cache life times are evaluated correctly between multiple processes. + +#### How to upgrade +If you are using the `maxAge` option, ensure that you add the option to every `memoizer.fn` call (if you have multiple) to reliably check cache validity. Most probably this is already the case in your application, and you don't need to change anything. + ### 3.0.4 (2024-03-06) * style: improve typing by using Awaited instead of our custom EnsurePromise type diff --git a/README.md b/README.md index 26b2b1d..73e8d5a 100755 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ memoizer.fn(fnToMemoize, { salt: 'foobar' }) ### maxAge -With `maxAge` option you can ensure that cache for given call is cleared after a predefined period of time (in milliseconds). +You can ensure that cache becomes invalid after a cache lifetime defined by the `maxAge` option is reached. memoize-fs uses [stats.mtimeMs](https://nodejs.org/api/fs.html#statsmtimems) (last modification time) when checking the age of the cache. ```js memoizer.fn(fnToMemoize, { maxAge: 10000 }) diff --git a/package-lock.json b/package-lock.json index cb3fe52..f68f9c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "memoize-fs", - "version": "3.0.4", + "version": "4.0.0-rc-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "memoize-fs", - "version": "3.0.4", + "version": "4.0.0-rc-1", "license": "MIT", "dependencies": { "meriyah": "^4.3.4" diff --git a/package.json b/package.json index 9093155..c1d4c27 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "memoize-fs", - "version": "3.0.4", + "version": "4.0.0-rc-1", "private": false, "type": "module", "author": "Boris Diakur (https://borisdiakur.de)", diff --git a/src/index.test.ts b/src/index.test.ts index aa70cd2..f197169 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1245,7 +1245,7 @@ describe('memoize-fs', () => { ) }) - it('invalidates cache after timeout with maxAge option set', async () => { + it('checks cache age if maxAge option is set', async () => { const cachePath = FIXTURE_CACHE const memoize = memoizeFs({ cachePath }) let c = 3 @@ -1262,7 +1262,7 @@ describe('memoize-fs', () => { function (a: number, b: number) { return a + b + c }, - { cacheId: 'foobar' } + { cacheId: 'foobar', maxAge: 10 } ) result = await memFn(1, 2) assert.strictEqual(result, 6, 'expected result to strictly equal 7') @@ -1274,7 +1274,7 @@ describe('memoize-fs', () => { function (a: number, b: number) { return a + b + c }, - { cacheId: 'foobar' } + { cacheId: 'foobar', maxAge: 10 } ) result = await memFn(1, 2) assert.strictEqual(result, 7, 'expected result to strictly equal 7') @@ -1307,7 +1307,7 @@ describe('memoize-fs', () => { function (a: number, b: number) { return a + b + c }, - { cacheId: 'foobar' } + { cacheId: 'foobar', maxAge: 10 } ) result = await memFn(1, 2) assert.strictEqual(result, 6, 'expected result to strictly equal 7') @@ -1319,7 +1319,7 @@ describe('memoize-fs', () => { function (a: number, b: number) { return a + b + c }, - { cacheId: 'foobar' } + { cacheId: 'foobar', maxAge: 10 } ) result = await memFn(1, 2) assert.strictEqual(result, 7, 'expected result to strictly equal 7') diff --git a/src/index.ts b/src/index.ts index e278572..a3b92a3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,7 +24,7 @@ export interface MemoizerOptions { function serialize(val: unknown) { const circRefColl: unknown[] = [] - return JSON.stringify(val, function (name, value) { + return JSON.stringify(val, function (_name, value) { if (typeof value === 'function') { return // ignore arguments and attributes of type function silently } @@ -131,15 +131,6 @@ async function writeResult( } else { resultString = '{"data":' + r + '}' } - if (optExt.maxAge) { - setTimeout(function () { - fs.rm(filePath, { recursive: true }).catch(function (err) { - if (err && err.code !== 'ENOENT') { - throw err - } - }) - }, optExt.maxAge) - } try { await fs.writeFile(filePath, resultString) cb() @@ -245,6 +236,33 @@ async function processFnAsync( ;(fn as (...args: unknown[]) => unknown).apply(null, args) } +async function checkFileAgeAndRead(filePath: string, maxAge?: number): Promise { + let fileHandle; + try { + fileHandle = await fs.open(filePath, 'r'); + + if (maxAge !== undefined) { + const stats = await fileHandle.stat(); + + const now = new Date().getTime(); + const fileAge = now - stats.mtimeMs; + + if (fileAge > maxAge) { + return null; + } + } + + const content = await fs.readFile(filePath, { encoding: 'utf8' }); + return content; + } catch (err) { + return null; + } finally { + if (fileHandle) { + await fileHandle.close(); + } + } +} + export default function buildMemoizer( memoizerOptions: Partial ) { @@ -321,8 +339,12 @@ export default function buildMemoizer( await processFn(fn, args, allOptions, filePath, resolve, reject) } - fs.readFile(filePath, { encoding: 'utf8' }) + checkFileAgeAndRead(filePath, allOptions.maxAge) .then(function (data) { + if (data === null) { + return cacheAndReturn() + } + const parsedData = parseResult(data, allOptions.deserialize) if (allOptions.retryOnInvalidCache && parsedData === undefined) { return cacheAndReturn()