From 67f38b9aed05ac08cc0a401051a05013ac65207f Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 31 May 2024 00:26:56 -0400 Subject: [PATCH] Use a per-`documentLoader` resolved context cache. Fixes an issue where multiple document loaders are used which each have different values for static contexts. A WeakMap is used for caches and is cleaned up using WeakMap semantics based on the lifetime of the documentLoader keys. --- CHANGELOG.md | 7 ++++++ lib/jsonld.js | 66 +++++++++++++++++++++++++++++++++++---------------- tests/misc.js | 57 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c0926a8..6bb2ee17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # jsonld ChangeLog +## 8.4.0 - 2024-xx-xx + +### Fixed +- Use a per-`documentLoader` resolved context cache. Fixes an issue where + multiple document loaders are used which each have different values for + static contexts. + ## 8.3.2 - 2023-12-06 ### Fixed diff --git a/lib/jsonld.js b/lib/jsonld.js index c6931aeb..aacd0c66 100644 --- a/lib/jsonld.js +++ b/lib/jsonld.js @@ -97,10 +97,25 @@ const wrapper = function(jsonld) { /** Registered RDF dataset parsers hashed by content-type. */ const _rdfParsers = {}; -// resolved context cache -// TODO: consider basing max on context size rather than number +// resolved context caches const RESOLVED_CONTEXT_CACHE_MAX_SIZE = 100; -const _resolvedContextCache = new LRU({max: RESOLVED_CONTEXT_CACHE_MAX_SIZE}); +// caches are created and indexed per documentLoader +// resources are cleaned up with WeakMap semantics +// TODO: add controls for cache resource usage +const _resolvedContextCaches = new WeakMap(); + +// make a ContextResolver using a per-documentLoader shared cache +function _makeContextResolver({documentLoader = 'default'}) { + let cache = _resolvedContextCaches.get(documentLoader); + if(!cache) { + // TODO: consider basing max on context size rather than number + cache = new LRU({max: RESOLVED_CONTEXT_CACHE_MAX_SIZE}); + _resolvedContextCaches.set(documentLoader, cache); + } + return new ContextResolver({ + sharedCache: cache + }); +} /* Core API */ @@ -152,8 +167,9 @@ jsonld.compact = async function(input, ctx, options) { skipExpansion: false, link: false, issuer: new IdentifierIssuer('_:b'), - contextResolver: new ContextResolver( - {sharedCache: _resolvedContextCache}) + contextResolver: _makeContextResolver({ + documentLoader: options.documentLoader + }) }); if(options.link) { // force skip expansion when linking, "link" is not part of the public @@ -269,8 +285,9 @@ jsonld.expand = async function(input, options) { // set default options options = _setDefaults(options, { keepFreeFloatingNodes: false, - contextResolver: new ContextResolver( - {sharedCache: _resolvedContextCache}) + contextResolver: _makeContextResolver({ + documentLoader: options.documentLoader + }) }); // build set of objects that may have @contexts to resolve @@ -368,8 +385,9 @@ jsonld.flatten = async function(input, ctx, options) { // set default options options = _setDefaults(options, { base: _isString(input) ? input : '', - contextResolver: new ContextResolver( - {sharedCache: _resolvedContextCache}) + contextResolver: _makeContextResolver({ + documentLoader: options.documentLoader + }) }); // expand input @@ -423,8 +441,9 @@ jsonld.frame = async function(input, frame, options) { requireAll: false, omitDefault: false, bnodesToClear: [], - contextResolver: new ContextResolver( - {sharedCache: _resolvedContextCache}) + contextResolver: _makeContextResolver({ + documentLoader: options.documentLoader + }) }); // if frame is a string, attempt to dereference remote document @@ -565,8 +584,9 @@ jsonld.normalize = jsonld.canonize = async function(input, options) { algorithm: 'URDNA2015', skipExpansion: false, safe: true, - contextResolver: new ContextResolver( - {sharedCache: _resolvedContextCache}) + contextResolver: _makeContextResolver({ + documentLoader: options.documentLoader + }) }); if('inputFormat' in options) { if(options.inputFormat !== 'application/n-quads' && @@ -674,8 +694,9 @@ jsonld.toRDF = async function(input, options) { options = _setDefaults(options, { base: _isString(input) ? input : '', skipExpansion: false, - contextResolver: new ContextResolver( - {sharedCache: _resolvedContextCache}) + contextResolver: _makeContextResolver({ + documentLoader: options.documentLoader + }) }); // TODO: support toRDF custom map? @@ -726,8 +747,9 @@ jsonld.createNodeMap = async function(input, options) { // set default options options = _setDefaults(options, { base: _isString(input) ? input : '', - contextResolver: new ContextResolver( - {sharedCache: _resolvedContextCache}) + contextResolver: _makeContextResolver({ + documentLoader: options.documentLoader + }) }); // expand input @@ -774,8 +796,9 @@ jsonld.merge = async function(docs, ctx, options) { // set default options options = _setDefaults(options, { - contextResolver: new ContextResolver( - {sharedCache: _resolvedContextCache}) + contextResolver: _makeContextResolver({ + documentLoader: options.documentLoader + }) }); // expand all documents @@ -926,8 +949,9 @@ jsonld.processContext = async function( // set default options options = _setDefaults(options, { base: '', - contextResolver: new ContextResolver( - {sharedCache: _resolvedContextCache}) + contextResolver: _makeContextResolver({ + documentLoader: options.documentLoader + }) }); // return initial context early for null context diff --git a/tests/misc.js b/tests/misc.js index 908052cd..106e8689 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -299,6 +299,63 @@ describe('loading multiple levels of contexts', () => { }); }); +// check that internal caching is unique for each document loader +describe.only('unique document loaders caching', () => { + const documentLoader0 = url => { + if(url === 'https://example.com/context') { + return { + document: { + "@context": { + "ex": "https://example.com/0#" + } + }, + // must be marked static to get into the shared cache + tag: 'static', + contextUrl: null, + documentUrl: url + }; + } + }; + const documentLoader1 = url => { + if(url === 'https://example.com/context') { + return { + document: { + "@context": { + "ex": "https://example.com/1#" + } + }, + contextUrl: null, + documentUrl: url + }; + } + }; + const doc = { + "@context": "https://example.com/context", + "ex:test": "test" + }; + const expected0 = [{ + "https://example.com/0#test": [{ + "@value": "test" + }] + }]; + const expected1 = [{ + "https://example.com/1#test": [{ + "@value": "test" + }] + }]; + + it('unique document loader caches', async () => { + const expanded0 = await jsonld.expand(doc, { + documentLoader: documentLoader0 + }); + assert.deepEqual(expanded0, expected0); + const expanded1 = await jsonld.expand(doc, { + documentLoader: documentLoader1 + }); + assert.deepEqual(expanded1, expected1); + }); +}); + describe('url tests', () => { it('should detect absolute IRIs', done => { // absolute IRIs