Skip to content

Commit 0664018

Browse files
committed
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.
1 parent 5367858 commit 0664018

File tree

3 files changed

+112
-21
lines changed

3 files changed

+112
-21
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# jsonld ChangeLog
22

3+
## 8.4.0 - 2024-xx-xx
4+
5+
### Fixed
6+
- Use a per-`documentLoader` resolved context cache. Fixes an issue where
7+
multiple document loaders are used which each have different values for
8+
static contexts.
9+
310
## 8.3.2 - 2023-12-06
411

512
### Fixed

lib/jsonld.js

+48-21
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,28 @@ const wrapper = function(jsonld) {
9797
/** Registered RDF dataset parsers hashed by content-type. */
9898
const _rdfParsers = {};
9999

100-
// resolved context cache
101-
// TODO: consider basing max on context size rather than number
100+
// resolved context caches
101+
// TODO: add controls for cache resource usage
102+
// cache size per document loader
102103
const RESOLVED_CONTEXT_CACHE_MAX_SIZE = 100;
103-
const _resolvedContextCache = new LRU({max: RESOLVED_CONTEXT_CACHE_MAX_SIZE});
104+
// caches are created and indexed per documentLoader
105+
// resources are cleaned up with WeakMap semantics for the documentLoaders
106+
const _resolvedContextCaches = new WeakMap();
107+
// default key to use when no documentLoader used
108+
const _defaultDocumentLoaderKey = {};
109+
110+
// make a ContextResolver using a per-documentLoader shared cache
111+
function _makeContextResolver({documentLoader = _defaultDocumentLoaderKey}) {
112+
let cache = _resolvedContextCaches.get(documentLoader);
113+
if(!cache) {
114+
// TODO: consider basing max on context size rather than number
115+
cache = new LRU({max: RESOLVED_CONTEXT_CACHE_MAX_SIZE});
116+
_resolvedContextCaches.set(documentLoader, cache);
117+
}
118+
return new ContextResolver({
119+
sharedCache: cache
120+
});
121+
}
104122

105123
/* Core API */
106124

@@ -152,8 +170,9 @@ jsonld.compact = async function(input, ctx, options) {
152170
skipExpansion: false,
153171
link: false,
154172
issuer: new IdentifierIssuer('_:b'),
155-
contextResolver: new ContextResolver(
156-
{sharedCache: _resolvedContextCache})
173+
contextResolver: _makeContextResolver({
174+
documentLoader: options ? options.documentLoader : undefined
175+
})
157176
});
158177
if(options.link) {
159178
// force skip expansion when linking, "link" is not part of the public
@@ -269,8 +288,9 @@ jsonld.expand = async function(input, options) {
269288
// set default options
270289
options = _setDefaults(options, {
271290
keepFreeFloatingNodes: false,
272-
contextResolver: new ContextResolver(
273-
{sharedCache: _resolvedContextCache})
291+
contextResolver: _makeContextResolver({
292+
documentLoader: options ? options.documentLoader : undefined
293+
})
274294
});
275295

276296
// build set of objects that may have @contexts to resolve
@@ -368,8 +388,9 @@ jsonld.flatten = async function(input, ctx, options) {
368388
// set default options
369389
options = _setDefaults(options, {
370390
base: _isString(input) ? input : '',
371-
contextResolver: new ContextResolver(
372-
{sharedCache: _resolvedContextCache})
391+
contextResolver: _makeContextResolver({
392+
documentLoader: options ? options.documentLoader : undefined
393+
})
373394
});
374395

375396
// expand input
@@ -423,8 +444,9 @@ jsonld.frame = async function(input, frame, options) {
423444
requireAll: false,
424445
omitDefault: false,
425446
bnodesToClear: [],
426-
contextResolver: new ContextResolver(
427-
{sharedCache: _resolvedContextCache})
447+
contextResolver: _makeContextResolver({
448+
documentLoader: options ? options.documentLoader : undefined
449+
})
428450
});
429451

430452
// if frame is a string, attempt to dereference remote document
@@ -565,8 +587,9 @@ jsonld.normalize = jsonld.canonize = async function(input, options) {
565587
algorithm: 'URDNA2015',
566588
skipExpansion: false,
567589
safe: true,
568-
contextResolver: new ContextResolver(
569-
{sharedCache: _resolvedContextCache})
590+
contextResolver: _makeContextResolver({
591+
documentLoader: options ? options.documentLoader : undefined
592+
})
570593
});
571594
if('inputFormat' in options) {
572595
if(options.inputFormat !== 'application/n-quads' &&
@@ -674,8 +697,9 @@ jsonld.toRDF = async function(input, options) {
674697
options = _setDefaults(options, {
675698
base: _isString(input) ? input : '',
676699
skipExpansion: false,
677-
contextResolver: new ContextResolver(
678-
{sharedCache: _resolvedContextCache})
700+
contextResolver: _makeContextResolver({
701+
documentLoader: options ? options.documentLoader : undefined
702+
})
679703
});
680704

681705
// TODO: support toRDF custom map?
@@ -726,8 +750,9 @@ jsonld.createNodeMap = async function(input, options) {
726750
// set default options
727751
options = _setDefaults(options, {
728752
base: _isString(input) ? input : '',
729-
contextResolver: new ContextResolver(
730-
{sharedCache: _resolvedContextCache})
753+
contextResolver: _makeContextResolver({
754+
documentLoader: options ? options.documentLoader : undefined
755+
})
731756
});
732757

733758
// expand input
@@ -774,8 +799,9 @@ jsonld.merge = async function(docs, ctx, options) {
774799

775800
// set default options
776801
options = _setDefaults(options, {
777-
contextResolver: new ContextResolver(
778-
{sharedCache: _resolvedContextCache})
802+
contextResolver: _makeContextResolver({
803+
documentLoader: options ? options.documentLoader : undefined
804+
})
779805
});
780806

781807
// expand all documents
@@ -926,8 +952,9 @@ jsonld.processContext = async function(
926952
// set default options
927953
options = _setDefaults(options, {
928954
base: '',
929-
contextResolver: new ContextResolver(
930-
{sharedCache: _resolvedContextCache})
955+
contextResolver: _makeContextResolver({
956+
documentLoader: options ? options.documentLoader : undefined
957+
})
931958
});
932959

933960
// return initial context early for null context

tests/misc.js

+57
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,63 @@ describe('loading multiple levels of contexts', () => {
299299
});
300300
});
301301

302+
// check that internal caching is unique for each document loader
303+
describe('unique document loaders caching', () => {
304+
const documentLoader0 = url => {
305+
if(url === 'https://example.com/context') {
306+
return {
307+
document: {
308+
"@context": {
309+
"ex": "https://example.com/0#"
310+
}
311+
},
312+
// must be marked static to get into the shared cache
313+
tag: 'static',
314+
contextUrl: null,
315+
documentUrl: url
316+
};
317+
}
318+
};
319+
const documentLoader1 = url => {
320+
if(url === 'https://example.com/context') {
321+
return {
322+
document: {
323+
"@context": {
324+
"ex": "https://example.com/1#"
325+
}
326+
},
327+
contextUrl: null,
328+
documentUrl: url
329+
};
330+
}
331+
};
332+
const doc = {
333+
"@context": "https://example.com/context",
334+
"ex:test": "test"
335+
};
336+
const expected0 = [{
337+
"https://example.com/0#test": [{
338+
"@value": "test"
339+
}]
340+
}];
341+
const expected1 = [{
342+
"https://example.com/1#test": [{
343+
"@value": "test"
344+
}]
345+
}];
346+
347+
it('unique document loader caches', async () => {
348+
const expanded0 = await jsonld.expand(doc, {
349+
documentLoader: documentLoader0
350+
});
351+
assert.deepEqual(expanded0, expected0);
352+
const expanded1 = await jsonld.expand(doc, {
353+
documentLoader: documentLoader1
354+
});
355+
assert.deepEqual(expanded1, expected1);
356+
});
357+
});
358+
302359
describe('url tests', () => {
303360
it('should detect absolute IRIs', done => {
304361
// absolute IRIs

0 commit comments

Comments
 (0)