Skip to content

Use a per-documentLoader resolved context cache. #552

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
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
69 changes: 48 additions & 21 deletions lib/jsonld.js
Original file line number Diff line number Diff line change
@@ -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 = Symbol();

// 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 */

@@ -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
@@ -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
@@ -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
@@ -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
@@ -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' &&
@@ -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?
@@ -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
@@ -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
@@ -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
57 changes: 57 additions & 0 deletions tests/misc.js
Original file line number Diff line number Diff line change
@@ -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
Loading