Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
*.sw[op]
*~
.zed/
.cproject
.project
.c9
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
28 changes: 22 additions & 6 deletions lib/CachedResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"lib/**/*.js"
],
"dependencies": {
"@digitalbazaar/lru-memoize": "^3.0.0"
"@digitalbazaar/lru-memoize": "^4.0.0"
},
"devDependencies": {
"c8": "^7.11.3",
Expand Down
150 changes: 150 additions & 0 deletions test/CachedResolver.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*!
* Copyright (c) 2021-2026 Digital Bazaar, Inc.
*/
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');
});
});
});
Loading