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
Prev Previous commit
Next Next commit
feat: context caching
  • Loading branch information
jeswr committed Oct 27, 2023
commit 92a4d238df3f883f4d28e2da673e154a1a87a9e2
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,18 @@ This library exposes many operations that are useful to parse and handle a JSON-
For this, the static functions on [`Util`](https://github.com/rubensworks/jsonld-context-parser.js/blob/master/lib/Util.ts)
and [`ContextParser`](https://github.com/rubensworks/jsonld-context-parser.js/blob/master/lib/ContextParser.ts) can be used.

##### Context Caching

This library supports the ability to cache context entry calculations and share them between multiple context parsers. This can be done as follows:

```ts
import { ContextCache, ContextParser } from 'jsonld-context-parser';

const contextCache = new ContextCache();
const contextParser1 = new ContextParser({ contextCache });
const contextParser2 = new ContextParser({ contextCache });
```

## Command-line

A command-line tool is provided to quickly normalize any context by URL, file or string.
Expand Down
41 changes: 11 additions & 30 deletions lib/ContextCache.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { JsonLdContextNormalized } from "./JsonLdContextNormalized";
import { IJsonLdContext, JsonLdContext } from "./JsonLdContext";
import { JsonLdContext } from "./JsonLdContext";
import md5 = require("md5");
import { IParseOptions } from "./ContextParser";
import { IContextCache } from "./IContextCache";
import { LRUCache } from "lru-cache";

function hashOptions(options: IParseOptions | undefined) {
const opts = { ...options, parentContext: undefined };
Expand All @@ -17,29 +18,20 @@ function hashOptions(options: IParseOptions | undefined) {

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)))),
JSON.stringify(context),
);
}
return typeof context === "string" ? md5(context) : cmap(context).toString();
return typeof context === "string" ? md5(context) : md5(JSON.stringify(context));
}


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

private cachedParsing: Record<string, Promise<JsonLdContextNormalized>> = {};

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

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

private mapIndex = 1;

constructor() {
// Empty
constructor(options?: LRUCache.Options<string, Promise<JsonLdContextNormalized>, unknown>) {
this.cachedParsing = new LRUCache(options ?? { max: 512 })
}

public hash(
Expand All @@ -49,28 +41,17 @@ export class ContextCache implements IContextCache {
let hash = hashOptions(options);

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

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

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

set(context: string, normalized: Promise<JsonLdContextNormalized>): void {
this.cachedParsing[context] = normalized;
this.cachedParsing.set(context, normalized);
}

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)!;
};
}
26 changes: 12 additions & 14 deletions lib/ContextParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {IJsonLdContext, IJsonLdContextNormalizedRaw, IPrefixValue, JsonLdContext
import {JsonLdContextNormalized, defaultExpandOptions, IExpandOptions} from "./JsonLdContextNormalized";
import {Util} from "./Util";
import { IContextCache } from './IContextCache';
import { ContextCache } from './ContextCache';

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

constructor(options?: IContextParserOptions) {
options = options || {};
this.documentLoader = options.documentLoader || new FetchDocumentLoader();
this.contextCache = options.contextCache;
this.contextCache = options.contextCache || new ContextCache();
this.documentCache = {};
this.validateContext = !options.skipValidation;
this.expandContentTypeToBase = !!options.expandContentTypeToBase;
Expand Down Expand Up @@ -636,18 +637,15 @@ 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);
options: IParseOptions = {}): Promise<JsonLdContextNormalized> {
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;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions lib/IContextCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ export interface IContextCache {
*/
get(context: string): Promise<JsonLdContextNormalized> | undefined;
/**
* Returns a cached version of the normalized version of a JSON-LD context.
* Stores 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 a context/options pair into the key to be used for caching the context.
*/
hash(context: JsonLdContext, options: IParseOptions | undefined): string;
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"canonicalize": "^1.0.1",
"cross-fetch": "^3.0.6",
"http-link-header": "^1.0.2",
"lru-cache": "^10.0.1",
"md5": "^2.3.0",
"relative-to-absolute-iri": "^1.0.5"
}
Expand Down
16 changes: 12 additions & 4 deletions perf/bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ async function main() {

const contextCache = new ContextCache();
const context = Object.keys(contexts);
const contextObject = contexts[context[0] as keyof typeof contexts];
const contextParser = new ContextParser({ documentLoader: new CachedDocumentLoader(), contextCache: new ContextCache() });
await contextParser.parse(context);
await contextParser.parse(contextObject);

// add tests
suite
Expand All @@ -36,16 +38,22 @@ async function main() {
await contextParser.parse(context);
}),
).add(
'Parse a context that has not been cached; and with caching in place',
'Parse a list of iri contexts that have been cached',
deferred(async () => {
const contextParser = new ContextParser({ documentLoader: new CachedDocumentLoader(), contextCache: new ContextCache() });
const contextParser = new ContextParser({ documentLoader: new CachedDocumentLoader(), contextCache });
await contextParser.parse(context);
}),
).add(
'Parse a context that has been cached',
'Parse a context object that has not been cached',
deferred(async () => {
const contextParser = new ContextParser({ documentLoader: new CachedDocumentLoader() });
await contextParser.parse(contextObject);
}),
).add(
'Parse a context object that has been cached',
deferred(async () => {
const contextParser = new ContextParser({ documentLoader: new CachedDocumentLoader(), contextCache });
await contextParser.parse(context);
await contextParser.parse(contextObject);
}),
).on('cycle', (event: Event) => {
console.log(event.target.toString());
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2527,6 +2527,11 @@ loud-rejection@^1.0.0:
currently-unhandled "^0.4.1"
signal-exit "^3.0.0"

lru-cache@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a"
integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==

lru-cache@^4.0.1:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
Expand Down