diff --git a/package-lock.json b/package-lock.json index d5b75d4301..0762532576 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,7 @@ "jwks-rsa": "3.2.0", "ldapjs": "3.0.7", "lodash": "4.18.1", - "lru-cache": "11.2.7", + "lru-cache": "11.5.2", "mime": "4.1.0", "mongodb": "7.1.0", "mustache": "4.2.0", @@ -16191,9 +16191,9 @@ } }, "node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -38174,9 +38174,9 @@ "dev": true }, "lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==" + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==" }, "lru-memoizer": { "version": "2.2.0", diff --git a/package.json b/package.json index f26487dc4d..ce6c8d9361 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "jwks-rsa": "3.2.0", "ldapjs": "3.0.7", "lodash": "4.18.1", - "lru-cache": "11.2.7", + "lru-cache": "11.5.2", "mime": "4.1.0", "mongodb": "7.1.0", "mustache": "4.2.0", diff --git a/spec/InMemoryCacheAdapter.spec.js b/spec/InMemoryCacheAdapter.spec.js index add976fbc9..1215c3a7f9 100644 --- a/spec/InMemoryCacheAdapter.spec.js +++ b/spec/InMemoryCacheAdapter.spec.js @@ -50,4 +50,52 @@ describe('InMemoryCacheAdapter', function () { .then(value => expect(value).toEqual(null)) .then(done); }); + + it('should keep an entry whose ttl outlives the cache ttl', async () => { + const cache = new InMemoryCacheAdapter({ ttl: 10 }); + + await cache.put(KEY, VALUE, 5000); + await wait(50); + + expect(await cache.get(KEY)).toEqual(VALUE); + }); + + it('should expire an entry whose ttl is shorter than the cache ttl', async () => { + const cache = new InMemoryCacheAdapter({ ttl: 5000 }); + + await cache.put(KEY, VALUE, 10); + expect(await cache.get(KEY)).toEqual(VALUE); + await wait(50); + + expect(await cache.get(KEY)).toEqual(null); + }); + + it('should not expire an entry with an infinite ttl', async () => { + const cache = new InMemoryCacheAdapter({ ttl: 10 }); + + await cache.put(KEY, VALUE, Infinity); + await wait(50); + + expect(await cache.get(KEY)).toEqual(VALUE); + }); + + it('should fall back to the cache ttl when the entry ttl is not a positive number', async () => { + const cache = new InMemoryCacheAdapter({ ttl: 10 }); + + await cache.put(KEY, VALUE, 'not a ttl'); + expect(await cache.get(KEY)).toEqual(VALUE); + await wait(50); + + expect(await cache.get(KEY)).toEqual(null); + }); + + it('should fall back to the cache ttl when no entry ttl is given', async () => { + const cache = new InMemoryCacheAdapter({ ttl: 10 }); + + await cache.put(KEY, VALUE); + expect(await cache.get(KEY)).toEqual(VALUE); + await wait(50); + + expect(await cache.get(KEY)).toEqual(null); + }); }); diff --git a/src/Adapters/Cache/LRUCache.js b/src/Adapters/Cache/LRUCache.js index 129a006376..38c0df1736 100644 --- a/src/Adapters/Cache/LRUCache.js +++ b/src/Adapters/Cache/LRUCache.js @@ -1,8 +1,14 @@ import { LRUCache as LRU } from 'lru-cache'; import defaults from '../../defaults'; +const isValidTTL = ttl => typeof ttl === 'number' && Number.isFinite(ttl) && ttl > 0; + +// `lru-cache` expresses "never expires" as a TTL of zero. +const NO_EXPIRY = 0; + export class LRUCache { constructor({ ttl = defaults.cacheTTL, maxSize = defaults.cacheMaxSize }) { + this.ttl = ttl; this.cache = new LRU({ max: maxSize, ttl, @@ -14,7 +20,24 @@ export class LRUCache { } put(key, value, ttl = this.ttl) { - this.cache.set(key, value, ttl); + // `lru-cache` takes the per-entry TTL as a property of an options object. + // Passed positionally it is silently discarded, leaving the entry on the + // cache-wide TTL. + // + // A TTL of `Infinity` means "never expires", matching `RedisCacheAdapter`, + // and is translated to the zero that `lru-cache` uses for that. `Infinity` + // is not forwarded as-is because Node cannot express it as a timer + // duration: under `ttlAutopurge` it emits a `TimeoutOverflowWarning` and + // clamps the timer to 1ms, so the purge timer re-fires every millisecond. + // Any other TTL that is not a positive, finite number is forwarded as + // `undefined`, which is how the library expresses "use the cache-wide TTL". + let entryTTL; + if (ttl === Infinity) { + entryTTL = NO_EXPIRY; + } else if (isValidTTL(ttl)) { + entryTTL = ttl; + } + this.cache.set(key, value, { ttl: entryTTL }); } del(key) { diff --git a/src/LiveQuery/ParseLiveQueryServer.ts b/src/LiveQuery/ParseLiveQueryServer.ts index a56880e677..d5aa7447eb 100644 --- a/src/LiveQuery/ParseLiveQueryServer.ts +++ b/src/LiveQuery/ParseLiveQueryServer.ts @@ -681,7 +681,9 @@ class ParseLiveQueryServer { const result: any = {}; if (error && error.code === Parse.Error.INVALID_SESSION_TOKEN) { result.error = error; - this.authCache.set(sessionToken, Promise.resolve(result), this.config.cacheTimeout); + this.authCache.set(sessionToken, Promise.resolve(result), { + ttl: this.config.cacheTimeout, + }); } else { this.authCache.delete(sessionToken); }