Skip to content

Commit 67f38b9

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 67f38b9

File tree

3 files changed

+109
-21
lines changed

3 files changed

+109
-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

+45-21
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,25 @@ 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
102101
const RESOLVED_CONTEXT_CACHE_MAX_SIZE = 100;
103-
const _resolvedContextCache = new LRU({max: RESOLVED_CONTEXT_CACHE_MAX_SIZE});
102+
// caches are created and indexed per documentLoader
103+
// resources are cleaned up with WeakMap semantics
104+
// TODO: add controls for cache resource usage
105+
const _resolvedContextCaches = new WeakMap();
106+
107+
// make a ContextResolver using a per-documentLoader shared cache
108+
function _makeContextResolver({documentLoader = 'default'}) {
109+
let cache = _resolvedContextCaches.get(documentLoader);
110+
if(!cache) {
111+
// TODO: consider basing max on context size rather than number
112+
cache = new LRU({max: RESOLVED_CONTEXT_CACHE_MAX_SIZE});
113+
_resolvedContextCaches.set(documentLoader, cache);
114+
}
115+
return new ContextResolver({
116+
sharedCache: cache
117+
});
118+
}
104119

105120
/* Core API */
106121

@@ -152,8 +167,9 @@ jsonld.compact = async function(input, ctx, options) {
152167
skipExpansion: false,
153168
link: false,
154169
issuer: new IdentifierIssuer('_:b'),
155-
contextResolver: new ContextResolver(
156-
{sharedCache: _resolvedContextCache})
170+
contextResolver: _makeContextResolver({
171+
documentLoader: options.documentLoader
172+
})
157173
});
158174
if(options.link) {
159175
// force skip expansion when linking, "link" is not part of the public
@@ -269,8 +285,9 @@ jsonld.expand = async function(input, options) {
269285
// set default options
270286
options = _setDefaults(options, {
271287
keepFreeFloatingNodes: false,
272-
contextResolver: new ContextResolver(
273-
{sharedCache: _resolvedContextCache})
288+
contextResolver: _makeContextResolver({
289+
documentLoader: options.documentLoader
290+
})
274291
});
275292

276293
// build set of objects that may have @contexts to resolve
@@ -368,8 +385,9 @@ jsonld.flatten = async function(input, ctx, options) {
368385
// set default options
369386
options = _setDefaults(options, {
370387
base: _isString(input) ? input : '',
371-
contextResolver: new ContextResolver(
372-
{sharedCache: _resolvedContextCache})
388+
contextResolver: _makeContextResolver({
389+
documentLoader: options.documentLoader
390+
})
373391
});
374392

375393
// expand input
@@ -423,8 +441,9 @@ jsonld.frame = async function(input, frame, options) {
423441
requireAll: false,
424442
omitDefault: false,
425443
bnodesToClear: [],
426-
contextResolver: new ContextResolver(
427-
{sharedCache: _resolvedContextCache})
444+
contextResolver: _makeContextResolver({
445+
documentLoader: options.documentLoader
446+
})
428447
});
429448

430449
// if frame is a string, attempt to dereference remote document
@@ -565,8 +584,9 @@ jsonld.normalize = jsonld.canonize = async function(input, options) {
565584
algorithm: 'URDNA2015',
566585
skipExpansion: false,
567586
safe: true,
568-
contextResolver: new ContextResolver(
569-
{sharedCache: _resolvedContextCache})
587+
contextResolver: _makeContextResolver({
588+
documentLoader: options.documentLoader
589+
})
570590
});
571591
if('inputFormat' in options) {
572592
if(options.inputFormat !== 'application/n-quads' &&
@@ -674,8 +694,9 @@ jsonld.toRDF = async function(input, options) {
674694
options = _setDefaults(options, {
675695
base: _isString(input) ? input : '',
676696
skipExpansion: false,
677-
contextResolver: new ContextResolver(
678-
{sharedCache: _resolvedContextCache})
697+
contextResolver: _makeContextResolver({
698+
documentLoader: options.documentLoader
699+
})
679700
});
680701

681702
// TODO: support toRDF custom map?
@@ -726,8 +747,9 @@ jsonld.createNodeMap = async function(input, options) {
726747
// set default options
727748
options = _setDefaults(options, {
728749
base: _isString(input) ? input : '',
729-
contextResolver: new ContextResolver(
730-
{sharedCache: _resolvedContextCache})
750+
contextResolver: _makeContextResolver({
751+
documentLoader: options.documentLoader
752+
})
731753
});
732754

733755
// expand input
@@ -774,8 +796,9 @@ jsonld.merge = async function(docs, ctx, options) {
774796

775797
// set default options
776798
options = _setDefaults(options, {
777-
contextResolver: new ContextResolver(
778-
{sharedCache: _resolvedContextCache})
799+
contextResolver: _makeContextResolver({
800+
documentLoader: options.documentLoader
801+
})
779802
});
780803

781804
// expand all documents
@@ -926,8 +949,9 @@ jsonld.processContext = async function(
926949
// set default options
927950
options = _setDefaults(options, {
928951
base: '',
929-
contextResolver: new ContextResolver(
930-
{sharedCache: _resolvedContextCache})
952+
contextResolver: _makeContextResolver({
953+
documentLoader: options.documentLoader
954+
})
931955
});
932956

933957
// 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.only('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)