From 226560089abe893cb37a6a7b791274739ae38db9 Mon Sep 17 00:00:00 2001 From: Derek Scruggs Date: Fri, 15 May 2026 15:50:51 -0500 Subject: [PATCH 1/3] upgrade lru-memoize to v4 with backwards-compatible maxAge support - Bump @digitalbazaar/lru-memoize dependency from ^3.0.0 to ^4.0.0 - Translate v3-style `maxAge` option to v4-style `ttl` in CachedResolver constructor; if neither is provided the prior default of 5000ms is kept - `ttl` takes precedence when both `ttl` and `maxAge` are supplied - Update JSDoc to document `ttl` as the preferred option and `maxAge` as a deprecated v3 compatibility alias - Add test/CachedResolver.spec.js with full coverage of the constructor option-translation logic and all public methods (get, use, generate) - Update README Cache management section to reflect the renamed option --- .gitignore | 1 + CHANGELOG.md | 10 +++ README.md | 8 +- lib/CachedResolver.js | 30 ++++++-- package.json | 2 +- test/CachedResolver.spec.js | 150 ++++++++++++++++++++++++++++++++++++ 6 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 test/CachedResolver.spec.js diff --git a/.gitignore b/.gitignore index 4098ed4..16785d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.sw[op] *~ +.zed/ .cproject .project .c9 diff --git a/CHANGELOG.md b/CHANGELOG.md index cce0245..18cd86d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # did-io ChangeLog +## 2.1.1 - 2026-TBD + +### Changed +- Upgrade `@digitalbazaar/lru-memoize` dependency from `^3.0.0` to `^4.0.0`. + The `CachedResolver` constructor now accepts the v4-style `ttl` option + (preferred) in place of the v3-style `maxAge` option. For backwards + compatibility, `maxAge` is still accepted and automatically translated to + `ttl`; if both are supplied, `ttl` takes precedence. The prior default of + 5000ms is preserved when neither option is given. + ## 2.1.0 - 2026-01-21 ### Added diff --git a/README.md b/README.md index 1c18d0f..1959330 100644 --- a/README.md +++ b/README.md @@ -211,13 +211,15 @@ which helps in high-concurrency use cases. (And that library in turn uses [`lru-cache`](https://www.npmjs.com/package/lru-cache) under the hood.) The `CachedResolver` constructor passes any options given to it through to -the `lru-cache` constructor, so see that repo for the full list of cache +the `lru-cache` constructor, so see that repo for the full list of cache management options. Commonly used ones include: * `max` (default: 100) - maximum size of the cache. -* `maxAge` (default: 5 sec/5000 ms) - maximum age of an item in ms. +* `ttl` (default: 5 sec/5000 ms) - maximum age (time-to-live) of an item in ms. +* `maxAge` - deprecated alias for `ttl`, retained for backwards compatibility + with v3. If both `ttl` and `maxAge` are provided, `ttl` takes precedence. * `updateAgeOnGet` (default: `false`) - When using time-expiring entries with - `maxAge`, setting this to true will make each entry's effective time update to + `ttl`, setting this to true will make each entry's effective time update to the current time whenever it is retrieved from cache, thereby extending the expiration date of the entry. diff --git a/lib/CachedResolver.js b/lib/CachedResolver.js index c1bcf65..08e17cf 100644 --- a/lib/CachedResolver.js +++ b/lib/CachedResolver.js @@ -12,20 +12,36 @@ export class CachedResolver { * object, minimally implementing `memoize()`; if this option is used, * then all other options are ignored. * @param {number} [options.max=100] - Max number of items in the cache. - * @param {number} [options.maxAge=5000] - Max age of a cache item, in ms. + * @param {number} [options.ttl=5000] - Max age (time-to-live) of a cache + * item, in ms. Preferred over `maxAge`. + * @param {number} [options.maxAge] - Deprecated alias for `ttl` (v3 + * compatibility). If both `ttl` and `maxAge` are provided, `ttl` takes + * precedence. * @param {boolean} [options.updateAgeOnGet=false] - When using time-expiring - * entries with `maxAge`, setting this to true will make each entry's + * entries with `ttl`, setting this to true will make each entry's * effective time update to the current time whenever it is retrieved from * cache, thereby extending the expiration date of the entry. * @param {object} [options.cacheOptions] - Additional `lru-cache` options. */ constructor({ - cache, max = 100, maxAge = 5000, updateAgeOnGet = false, + cache, + max = 100, + maxAge, + ttl, + updateAgeOnGet = false, ...cacheOptions } = {}) { - this._cache = cache ?? new LruCache({ - max, maxAge, updateAgeOnGet, ...cacheOptions - }); + // Translate v3-style `maxAge` to v4-style `ttl` for backwards + // compatibility. If neither is provided, default to 5000ms. + const resolvedTtl = ttl ?? maxAge ?? 5000; + this._cache = + cache ?? + new LruCache({ + max, + ttl: resolvedTtl, + updateAgeOnGet, + ...cacheOptions, + }); this._methods = new Map(); } @@ -58,7 +74,7 @@ export class CachedResolver { return this._cache.memoize({ key: did, - fn: () => method.get({did, ...args}) + fn: () => method.get({did, ...args}), }); } diff --git a/package.json b/package.json index 6522aea..f70db03 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "lib/**/*.js" ], "dependencies": { - "@digitalbazaar/lru-memoize": "^3.0.0" + "@digitalbazaar/lru-memoize": "^4.0.0" }, "devDependencies": { "c8": "^7.11.3", diff --git a/test/CachedResolver.spec.js b/test/CachedResolver.spec.js new file mode 100644 index 0000000..cc312fc --- /dev/null +++ b/test/CachedResolver.spec.js @@ -0,0 +1,150 @@ +/*! + * Copyright (c) 2021-2026 Digital Bazaar, Inc. All rights reserved. + */ +import chai from 'chai'; +chai.should(); +const {expect} = chai; + +import {CachedResolver} from '../lib/CachedResolver.js'; + +// Minimal mock DID driver +function mockDriver({method = 'ex', doc = {id: 'did:ex:123'}} = {}) { + return { + method, + get: async ({did} = {}) => ({...doc, id: did ?? doc.id}), + generate: async () => ({doc, keys: {}}) + }; +} + +describe('CachedResolver', () => { + describe('constructor cache options', () => { + it('should use a default ttl of 5000ms when no options are given', + async () => { + const resolver = new CachedResolver(); + // The underlying LruCache ttl option should be 5000 + expect(resolver._cache.options.ttl).to.equal(5000); + }); + + it('should accept the v4-style `ttl` option', async () => { + const resolver = new CachedResolver({ttl: 3000}); + expect(resolver._cache.options.ttl).to.equal(3000); + }); + + it('should accept the v3-style `maxAge` option and translate it to `ttl`', + async () => { + const resolver = new CachedResolver({maxAge: 3000}); + expect(resolver._cache.options.ttl).to.equal(3000); + }); + + it('should prefer `ttl` over `maxAge` when both are provided', async () => { + const resolver = new CachedResolver({ttl: 4000, maxAge: 1000}); + expect(resolver._cache.options.ttl).to.equal(4000); + }); + + it('should accept the `max` option', async () => { + const resolver = new CachedResolver({max: 50}); + expect(resolver._cache.options.max).to.equal(50); + }); + + it('should use a custom cache instance when `cache` option is provided', + async () => { + const customCache = { + memoize: async ({fn}) => fn() + }; + const resolver = new CachedResolver({cache: customCache}); + expect(resolver._cache).to.equal(customCache); + }); + }); + + describe('use()', () => { + it('should register a driver by its method name', async () => { + const resolver = new CachedResolver(); + const driver = mockDriver({method: 'ex'}); + resolver.use(driver); + expect(resolver._methods.get('ex')).to.equal(driver); + }); + }); + + describe('get()', () => { + it('should resolve a DID document using a registered driver', async () => { + const resolver = new CachedResolver(); + resolver.use(mockDriver({method: 'ex'})); + + const doc = await resolver.get({did: 'did:ex:123'}); + expect(doc).to.have.property('id', 'did:ex:123'); + }); + + it('should accept `url` as an alias for `did`', async () => { + const resolver = new CachedResolver(); + resolver.use(mockDriver({method: 'ex'})); + + const doc = await resolver.get({url: 'did:ex:123'}); + expect(doc).to.have.property('id', 'did:ex:123'); + }); + + it('should return a cached result on the second call', async () => { + let callCount = 0; + const driver = { + method: 'ex', + get: async ({did}) => { + callCount++; + return {id: did}; + } + }; + + const resolver = new CachedResolver({ttl: 60000}); + resolver.use(driver); + + await resolver.get({did: 'did:ex:123'}); + await resolver.get({did: 'did:ex:123'}); + expect(callCount).to.equal(1); + }); + + it('should throw if neither `did` nor `url` is given', async () => { + const resolver = new CachedResolver(); + let err; + try { + await resolver.get({}); + } catch(e) { + err = e; + } + expect(err).to.be.instanceof(TypeError); + expect(err.message).to.include('"did" or "url"'); + }); + + it('should throw if no driver is registered for the DID method', + async () => { + const resolver = new CachedResolver(); + let err; + try { + await resolver.get({did: 'did:unknown:123'}); + } catch(e) { + err = e; + } + expect(err).to.be.instanceof(Error); + expect(err.message).to.include('unknown'); + }); + }); + + describe('generate()', () => { + it('should delegate to the registered driver', async () => { + const resolver = new CachedResolver(); + resolver.use(mockDriver({method: 'ex'})); + + const result = await resolver.generate({method: 'ex'}); + expect(result).to.have.property('doc'); + }); + + it('should throw if no driver is registered for the method', async () => { + const resolver = new CachedResolver(); + let err; + try { + await resolver.generate({method: 'unknown'}); + } catch(e) { + err = e; + } + expect(err).to.be.instanceof(Error); + expect(err.message).to.include('unknown'); + }); + }); +}); From e8b850adae30ce03c94ae7a0c7687a31426e2ccc Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sat, 16 May 2026 18:39:30 -0400 Subject: [PATCH 2/3] Remove trailing comma. --- lib/CachedResolver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/CachedResolver.js b/lib/CachedResolver.js index 08e17cf..01352a2 100644 --- a/lib/CachedResolver.js +++ b/lib/CachedResolver.js @@ -74,7 +74,7 @@ export class CachedResolver { return this._cache.memoize({ key: did, - fn: () => method.get({did, ...args}), + fn: () => method.get({did, ...args}) }); } From 556b7007e5dc1810a6d2b478dadb421cc15764fe Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sat, 16 May 2026 18:39:51 -0400 Subject: [PATCH 3/3] Simplify file header. --- test/CachedResolver.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/CachedResolver.spec.js b/test/CachedResolver.spec.js index cc312fc..569f961 100644 --- a/test/CachedResolver.spec.js +++ b/test/CachedResolver.spec.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2021-2026 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2021-2026 Digital Bazaar, Inc. */ import chai from 'chai'; chai.should();