Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
chore: create cache for normalized jsonld contexts
  • Loading branch information
jeswr committed Oct 26, 2023
commit 7ba6b6b8a7bc48230931c468222aa2b5ce952e00
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ yarn-error.log
**/index.d.ts
coverage
documentation
perf/output.txt
2 changes: 1 addition & 1 deletion bin/jsonld-context-parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ default:
break;
}

new ContextParser().parse(input, { external, baseIRI })
new ContextParser()._parse(input, { external, baseIRI })
.then((context) => {
process.stdout.write(JSON.stringify(context.getContextRaw(), null, ' '));
process.stdout.write('\n');
Expand Down
71 changes: 71 additions & 0 deletions lib/ContextCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { JsonLdContextNormalized } from "./JsonLdContextNormalized";
import { IJsonLdContext, JsonLdContext } from "./JsonLdContext";
import md5 from "md5";
import { IParseOptions } from "./ContextParser";
import { IContextCache } from "./IContextCache";

function hashOptions(options: IParseOptions | undefined) {
const opts = { ...options, parentContext: undefined };
for (const key of Object.keys(opts)) {
if (typeof opts[key as keyof typeof opts] === "undefined") {
delete opts[key as keyof typeof opts];
}
}

return md5(JSON.stringify(opts, Object.keys(opts).sort()));
}

function hashContext(
context: JsonLdContext,
cmap: (c: IJsonLdContext) => number,
): string {
if (Array.isArray(context)) {
return md5(
JSON.stringify(context.map((c) => (typeof c === "string" ? c : cmap(c)))),
);
}
return typeof context === "string" ? md5(context) : cmap(context).toString();
}


export class ContextCache implements IContextCache {
private cachedParsing: Record<string, Promise<JsonLdContextNormalized>> = {};

private contextMap: Map<IJsonLdContext, number> = new Map();

private contextHashMap: Map<string, number> = new Map();

private mapIndex = 1;

private cmap = (context: IJsonLdContext) => {
if (!this.contextMap.has(context)) {
const hash = md5(JSON.stringify(context));
if (!this.contextHashMap.has(hash)) {
this.contextHashMap.set(hash, (this.mapIndex += 1));
}
this.contextMap.set(context, this.contextHashMap.get(hash)!);
}
return this.contextMap.get(context)!;
};

public hash(
context: JsonLdContext,
options?: IParseOptions
): string {
let hash = hashOptions(options);

if (options?.parentContext && Object.keys(options.parentContext).length !== 0) {
hash = md5(hash + this.cmap(options.parentContext));
}

return md5(hash + hashContext(context, this.cmap));
}

get(context: string): Promise<JsonLdContextNormalized> | undefined {
return this.cachedParsing[context];
}

set(context: string, normalized: Promise<JsonLdContextNormalized>): void {
this.cachedParsing[context] = normalized;
}
}
38 changes: 33 additions & 5 deletions lib/ContextParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {IDocumentLoader} from "./IDocumentLoader";
import {IJsonLdContext, IJsonLdContextNormalizedRaw, IPrefixValue, JsonLdContext} from "./JsonLdContext";
import {JsonLdContextNormalized, defaultExpandOptions, IExpandOptions} from "./JsonLdContextNormalized";
import {Util} from "./Util";
import { IContextCache } from './IContextCache';

// tslint:disable-next-line:no-var-requires
const canonicalizeJson = require('canonicalize');
Expand All @@ -23,10 +24,12 @@ export class ContextParser {
private readonly expandContentTypeToBase: boolean;
private readonly remoteContextsDepthLimit: number;
private readonly redirectSchemaOrgHttps: boolean;
private readonly contextCache?: IContextCache;

constructor(options?: IContextParserOptions) {
options = options || {};
this.documentLoader = options.documentLoader || new FetchDocumentLoader();
this.contextCache = options.contextCache;
this.documentCache = {};
this.validateContext = !options.skipValidation;
this.expandContentTypeToBase = !!options.expandContentTypeToBase;
Expand Down Expand Up @@ -610,14 +613,14 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
const parentContext = {...context};
parentContext[key] = {...parentContext[key]};
delete parentContext[key]['@context'];
await this.parse(value['@context'],
await this._parse(value['@context'],
{ ...options, external: false, parentContext, ignoreProtection: true, ignoreRemoteScopedContexts: true, ignoreScopedContexts: true });
} catch (e) {
throw new ErrorCoded(e.message, ERROR_CODES.INVALID_SCOPED_CONTEXT);
}
}

value['@context'] = (await this.parse(value['@context'],
value['@context'] = (await this._parse(value['@context'],
{ ...options, external: false, minimalProcessing: true, ignoreRemoteScopedContexts: true, parentContext: context }))
.getContextRaw();
}
Expand All @@ -633,6 +636,27 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
* @return {Promise<JsonLdContextNormalized>} A promise resolving to the context.
*/
public async parse(context: JsonLdContext,
options: IParseOptions = {}): Promise<JsonLdContextNormalized> {
if (this.contextCache) {
const hash = this.contextCache.hash(context, options);
const cached = this.contextCache.get(hash);
if (cached)
return cached;

const parsed = this._parse(context, options);
this.contextCache.set(hash, parsed);
return parsed;
}
return this._parse(context, options);
}

/**
* Parse a JSON-LD context in any form.
* @param {JsonLdContext} context A context, URL to a context, or an array of contexts/URLs.
* @param {IParseOptions} options Optional parsing options.
* @return {Promise<JsonLdContextNormalized>} A promise resolving to the context.
*/
private async _parse(context: JsonLdContext,
options: IParseOptions = {}): Promise<JsonLdContextNormalized> {
const {
baseIRI,
Expand Down Expand Up @@ -667,7 +691,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
if (overriddenLoad) {
return new JsonLdContextNormalized(overriddenLoad);
}
const parsedStringContext = await this.parse(await this.load(contextIri),
const parsedStringContext = await this._parse(await this.load(contextIri),
{
...options,
baseIRI: contextIri,
Expand Down Expand Up @@ -699,7 +723,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
}

const reducedContexts = await contexts.reduce((accContextPromise, contextEntry, i) => accContextPromise
.then((accContext) => this.parse(contextEntry, {
.then((accContext) => this._parse(contextEntry, {
...options,
baseIRI: contextIris[i] || options.baseIRI,
external: !!contextIris[i] || options.external,
Expand All @@ -714,7 +738,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
return reducedContexts;
} else if (typeof context === 'object') {
if ('@context' in context) {
return await this.parse(context['@context'], options);
return await this._parse(context['@context'], options);
}

// Make a deep clone of the given context, to avoid modifying it.
Expand Down Expand Up @@ -891,6 +915,10 @@ export interface IContextParserOptions {
* An optional loader that should be used for fetching external JSON-LD contexts.
*/
documentLoader?: IDocumentLoader;
/**
* An optional cache for parsed contexts.
*/
contextCache?: IContextCache;
/**
* By default, JSON-LD contexts will be validated.
* This can be disabled by setting this option to true.
Expand Down
25 changes: 25 additions & 0 deletions lib/IContextCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { JsonLdContextNormalized } from "./JsonLdContextNormalized";
import { JsonLdContext } from "./JsonLdContext";
import { IParseOptions } from "./ContextParser";

/**
* Caches the normalized version of a JSON-LD context.
*/
export interface IContextCache {
/**
* Returns a cached version of the normalized version of a JSON-LD context.
* @param {string} context A hashed JSON-LD Context.
* @return {Promise<JsonLdContextNormalized> | undefined} A promise resolving to a normalized JSON-LD context.
*/
get(context: string): Promise<JsonLdContextNormalized> | undefined;
/**
* Returns a cached version of the normalized version of a JSON-LD context.
* @param {string} context A hashed JSON-LD Context.
* @return {Promise<JsonLdContextNormalized>} A promise resolving to a normalized JSON-LD context.
*/
set(context: string, normalized: Promise<JsonLdContextNormalized>): void;
/**
*
*/
hash(context: JsonLdContext, options: IParseOptions | undefined): string;
}
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,17 @@
},
"sideEffects": false,
"devDependencies": {
"@types/benchmark": "^2.1.4",
"@types/jest": "^28.0.0",
"@types/md5": "^2.3.4",
"benchmark": "^2.1.4",
"coveralls": "^3.0.0",
"jest": "^28.0.1",
"manual-git-changelog": "^1.0.0",
"pre-commit": "^1.2.2",
"ts-jest": "^28.0.1",
"ts-loader": "^9.3.1",
"ts-node": "^10.9.1",
"tslint": "^6.0.0",
"tslint-eslint-rules": "^5.3.1",
"typescript": "^5.0.0",
Expand Down Expand Up @@ -81,14 +85,16 @@
"build-watch": "tsc --watch",
"validate": "npm ls",
"prepare": "npm run build",
"version": "manual-git-changelog onversion"
"version": "manual-git-changelog onversion",
"perf": "ts-node perf/bench.ts 2>&1 | tee perf/output.txt"
},
"dependencies": {
"@types/http-link-header": "^1.0.1",
"@types/node": "^18.0.0",
"canonicalize": "^1.0.1",
"cross-fetch": "^3.0.6",
"http-link-header": "^1.0.2",
"md5": "^2.3.0",
"relative-to-absolute-iri": "^1.0.5"
}
}
33 changes: 33 additions & 0 deletions perf/bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { type Options, type Event, Suite } from 'benchmark';
import contexts from './contexts';
import { IDocumentLoader, IJsonLdContext, ContextParser } from '..'

class CachedDocumentLoader implements IDocumentLoader {
load(url: string): Promise<IJsonLdContext> {
if (!contexts[url as keyof typeof contexts])
return Promise.reject(new Error(`No context for ${url}`));

return Promise.resolve(contexts[url as keyof typeof contexts]);
}
}

function deferred(fn: () => Promise<any>): Options {
return {
defer: true,
fn: (deferred: { resolve: () => void }) => fn().then(() => deferred.resolve())
}
}

const suite = new Suite();

// add tests
suite
.add(
'Parse a context that has not been cached; and without caching in place',
deferred(async () => {
const contextParser = new ContextParser({ documentLoader: new CachedDocumentLoader() });
await contextParser._parse(Object.keys(contexts));
}),
).on('cycle', (event: Event) => {
console.log(event.target.toString());
}).run();
94 changes: 94 additions & 0 deletions perf/contexts/data-integrity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//
// Copyright Inrupt Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
// Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
export default {
"@context": {
id: "@id",
type: "@type",
"@protected": true,
proof: {
"@id": "https://w3id.org/security#proof",
"@type": "@id",
"@container": "@graph",
},
DataIntegrityProof: {
"@id": "https://w3id.org/security#DataIntegrityProof",
"@context": {
"@protected": true,
id: "@id",
type: "@type",
challenge: "https://w3id.org/security#challenge",
created: {
"@id": "http://purl.org/dc/terms/created",
"@type": "http://www.w3.org/2001/XMLSchema#dateTime",
},
domain: "https://w3id.org/security#domain",
expires: {
"@id": "https://w3id.org/security#expiration",
"@type": "http://www.w3.org/2001/XMLSchema#dateTime",
},
nonce: "https://w3id.org/security#nonce",
proofPurpose: {
"@id": "https://w3id.org/security#proofPurpose",
"@type": "@vocab",
"@context": {
"@protected": true,
id: "@id",
type: "@type",
assertionMethod: {
"@id": "https://w3id.org/security#assertionMethod",
"@type": "@id",
"@container": "@set",
},
authentication: {
"@id": "https://w3id.org/security#authenticationMethod",
"@type": "@id",
"@container": "@set",
},
capabilityInvocation: {
"@id": "https://w3id.org/security#capabilityInvocationMethod",
"@type": "@id",
"@container": "@set",
},
capabilityDelegation: {
"@id": "https://w3id.org/security#capabilityDelegationMethod",
"@type": "@id",
"@container": "@set",
},
keyAgreement: {
"@id": "https://w3id.org/security#keyAgreementMethod",
"@type": "@id",
"@container": "@set",
},
},
},
cryptosuite: "https://w3id.org/security#cryptosuite",
proofValue: {
"@id": "https://w3id.org/security#proofValue",
"@type": "https://w3id.org/security#multibase",
},
verificationMethod: {
"@id": "https://w3id.org/security#verificationMethod",
"@type": "@id",
},
},
},
},
};
Loading