Skip to content

Commit

Permalink
Use a per-documentLoader resolved context cache.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
davidlehn committed May 31, 2024
1 parent 5367858 commit 0664018
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 21 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
69 changes: 48 additions & 21 deletions lib/jsonld.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,28 @@ 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
// TODO: add controls for cache resource usage
// cache size per document loader
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 for the documentLoaders
const _resolvedContextCaches = new WeakMap();
// default key to use when no documentLoader used
const _defaultDocumentLoaderKey = {};

// make a ContextResolver using a per-documentLoader shared cache
function _makeContextResolver({documentLoader = _defaultDocumentLoaderKey}) {
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 */

Expand Down Expand Up @@ -152,8 +170,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 ? options.documentLoader : undefined
})
});
if(options.link) {
// force skip expansion when linking, "link" is not part of the public
Expand Down Expand Up @@ -269,8 +288,9 @@ jsonld.expand = async function(input, options) {
// set default options
options = _setDefaults(options, {
keepFreeFloatingNodes: false,
contextResolver: new ContextResolver(
{sharedCache: _resolvedContextCache})
contextResolver: _makeContextResolver({
documentLoader: options ? options.documentLoader : undefined
})
});

// build set of objects that may have @contexts to resolve
Expand Down Expand Up @@ -368,8 +388,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 ? options.documentLoader : undefined
})
});

// expand input
Expand Down Expand Up @@ -423,8 +444,9 @@ jsonld.frame = async function(input, frame, options) {
requireAll: false,
omitDefault: false,
bnodesToClear: [],
contextResolver: new ContextResolver(
{sharedCache: _resolvedContextCache})
contextResolver: _makeContextResolver({
documentLoader: options ? options.documentLoader : undefined
})
});

// if frame is a string, attempt to dereference remote document
Expand Down Expand Up @@ -565,8 +587,9 @@ jsonld.normalize = jsonld.canonize = async function(input, options) {
algorithm: 'URDNA2015',
skipExpansion: false,
safe: true,
contextResolver: new ContextResolver(
{sharedCache: _resolvedContextCache})
contextResolver: _makeContextResolver({
documentLoader: options ? options.documentLoader : undefined
})
});
if('inputFormat' in options) {
if(options.inputFormat !== 'application/n-quads' &&
Expand Down Expand Up @@ -674,8 +697,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 ? options.documentLoader : undefined
})
});

// TODO: support toRDF custom map?
Expand Down Expand Up @@ -726,8 +750,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 ? options.documentLoader : undefined
})
});

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

// set default options
options = _setDefaults(options, {
contextResolver: new ContextResolver(
{sharedCache: _resolvedContextCache})
contextResolver: _makeContextResolver({
documentLoader: options ? options.documentLoader : undefined
})
});

// expand all documents
Expand Down Expand Up @@ -926,8 +952,9 @@ jsonld.processContext = async function(
// set default options
options = _setDefaults(options, {
base: '',
contextResolver: new ContextResolver(
{sharedCache: _resolvedContextCache})
contextResolver: _makeContextResolver({
documentLoader: options ? options.documentLoader : undefined
})
});

// return initial context early for null context
Expand Down
57 changes: 57 additions & 0 deletions tests/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,63 @@ describe('loading multiple levels of contexts', () => {
});
});

// check that internal caching is unique for each document loader
describe('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
Expand Down

0 comments on commit 0664018

Please sign in to comment.