Skip to content
Draft
Show file tree
Hide file tree
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
12 changes: 10 additions & 2 deletions lib/loading/ComponentRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Readable } from 'stream';
import type { Readable } from 'node:stream';
import type * as RDF from '@rdfjs/types';
import type { ContextParser } from 'jsonld-context-parser';
import type { Resource, RdfObjectLoader } from 'rdf-object';
import type { Logger } from 'winston';
import { RdfParser } from '../rdf/RdfParser';
Expand All @@ -20,6 +21,7 @@ export class ComponentRegistry {
private readonly componentResources: Record<string, Resource>;
private readonly skipContextValidation: boolean;
private readonly remoteContextLookups: boolean;
private readonly contextParser?: ContextParser;

public constructor(options: IComponentLoaderRegistryOptions) {
this.moduleState = options.moduleState;
Expand All @@ -28,6 +30,7 @@ export class ComponentRegistry {
this.componentResources = options.componentResources;
this.skipContextValidation = options.skipContextValidation;
this.remoteContextLookups = options.remoteContextLookups;
this.contextParser = options.contextParser;
}

/**
Expand All @@ -37,7 +40,7 @@ export class ComponentRegistry {
*/
public async registerAvailableModules(): Promise<void> {
await Promise.all(Object.values(this.moduleState.componentModules)
.flatMap(Object.values)
.flatMap(x => Object.values(x))
.map((moduleResourceUrl: string) => this.registerModule(moduleResourceUrl)));
}

Expand All @@ -54,6 +57,7 @@ export class ComponentRegistry {
logger: this.logger,
skipContextValidation: this.skipContextValidation,
remoteContextLookups: this.remoteContextLookups,
contextParser: this.contextParser,
}));
}

Expand Down Expand Up @@ -123,4 +127,8 @@ export interface IComponentLoaderRegistryOptions {
componentResources: Record<string, Resource>;
skipContextValidation: boolean;
remoteContextLookups: boolean;
/**
* An optional shared, cache-bearing JSON-LD context parser reused across all module files.
*/
contextParser?: ContextParser;
}
40 changes: 28 additions & 12 deletions lib/loading/ComponentsManagerBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import type { Resource } from 'rdf-object';
import { RdfObjectLoader } from 'rdf-object';
import type { Logger } from 'winston';
import { createLogger, format, transports } from 'winston';

// eslint-disable-next-line import/extensions
import contextJson from '../../components/context.json';
import { ComponentsManager } from '../ComponentsManager';
import { ConfigConstructorPool } from '../construction/ConfigConstructorPool';
import type { IConfigConstructorPool } from '../construction/IConfigConstructorPool';
Expand All @@ -11,6 +14,7 @@ import { ConfigPreprocessorComponent } from '../preprocess/ConfigPreprocessorCom
import { ConfigPreprocessorComponentMapped } from '../preprocess/ConfigPreprocessorComponentMapped';
import { ConfigPreprocessorOverride } from '../preprocess/ConfigPreprocessorOverride';
import { ParameterHandler } from '../preprocess/ParameterHandler';
import { RdfParser } from '../rdf/RdfParser';
import type { LogLevel } from '../util/LogLevel';
import { ComponentRegistry } from './ComponentRegistry';
import { ComponentRegistryFinalizer } from './ComponentRegistryFinalizer';
Expand All @@ -21,25 +25,25 @@ import type { IModuleState } from './ModuleStateBuilder';
/**
* Builds {@link ComponentsManager}'s based on given options.
*/
export class ComponentsManagerBuilder<Instance = any> {
export class ComponentsManagerBuilder<TInstance = any> {
private readonly mainModulePath: string;
private readonly componentLoader: (registry: ComponentRegistry) => Promise<void>;
private readonly configLoader: (registry: ConfigRegistry) => Promise<void>;
private readonly constructionStrategy: IConstructionStrategy<Instance>;
private readonly constructionStrategy: IConstructionStrategy<TInstance>;
private readonly dumpErrorState: boolean;
private readonly logger: Logger;
private readonly moduleState?: IModuleState;
private readonly skipContextValidation: boolean;
private readonly typeChecking: boolean;
private readonly remoteContextLookups: boolean;

public constructor(options: IComponentsManagerBuilderOptions<Instance>) {
public constructor(options: IComponentsManagerBuilderOptions<TInstance>) {
this.mainModulePath = options.mainModulePath;
this.componentLoader = options.moduleLoader || (async registry => registry.registerAvailableModules());
this.configLoader = options.configLoader || (async() => {
this.componentLoader = options.moduleLoader ?? (async registry => registry.registerAvailableModules());
this.configLoader = options.configLoader ?? (async() => {
// Do nothing
});
this.constructionStrategy = options.constructionStrategy || new ConstructionStrategyCommonJs({ req: require });
this.constructionStrategy = options.constructionStrategy ?? new ConstructionStrategyCommonJs({ req: require });
this.dumpErrorState = options.dumpErrorState === undefined ? true : Boolean(options.dumpErrorState);
this.logger = ComponentsManagerBuilder.createLogger(options.logLevel);
this.moduleState = options.moduleState;
Expand Down Expand Up @@ -73,14 +77,14 @@ export class ComponentsManagerBuilder<Instance = any> {
public static createObjectLoader(): RdfObjectLoader {
return new RdfObjectLoader({
uniqueLiterals: true,
context: require('../../components/context.json'),
context: contextJson,
});
}

/**
* @return A new instance of {@link ComponentsManager}.
*/
public async build(): Promise<ComponentsManager<Instance>> {
public async build(): Promise<ComponentsManager<TInstance>> {
// Initialize module state
let moduleState: IModuleState;
if (this.moduleState) {
Expand All @@ -95,6 +99,16 @@ export class ComponentsManagerBuilder<Instance = any> {
// Initialize object loader with built-in context
const objectLoader: RdfObjectLoader = ComponentsManagerBuilder.createObjectLoader();

// Create a single, cache-bearing JSON-LD context parser (with one shared prefetched document
// loader) that is reused across every component and config file. This avoids re-loading and
// re-normalizing the shared well-known @contexts once per file.
const contextParser = RdfParser.createSharedContextParser({
contexts: moduleState.contexts,
logger: this.logger,
remoteContextLookups: this.remoteContextLookups,
skipContextValidation: this.skipContextValidation,
});

// Load modules
this.logger.info(`Initiating component loading`);
const componentResources: Record<string, Resource> = {};
Expand All @@ -105,6 +119,7 @@ export class ComponentsManagerBuilder<Instance = any> {
componentResources,
skipContextValidation: this.skipContextValidation,
remoteContextLookups: this.remoteContextLookups,
contextParser,
});
await this.componentLoader(componentRegistry);
const componentFinalizer = new ComponentRegistryFinalizer({
Expand All @@ -122,14 +137,15 @@ export class ComponentsManagerBuilder<Instance = any> {
logger: this.logger,
skipContextValidation: this.skipContextValidation,
remoteContextLookups: this.remoteContextLookups,
contextParser,
});
await this.configLoader(configRegistry);
this.logger.info(`Loaded configs`);

// Build constructor pool
const runTypeConfigs = {};
const parameterHandler = new ParameterHandler({ objectLoader, typeChecking: this.typeChecking });
const configConstructorPool: IConfigConstructorPool<Instance> = new ConfigConstructorPool({
const configConstructorPool: IConfigConstructorPool<TInstance> = new ConfigConstructorPool({
objectLoader,
configPreprocessors: [
new ConfigPreprocessorOverride({
Expand All @@ -156,7 +172,7 @@ export class ComponentsManagerBuilder<Instance = any> {
moduleState,
});

return new ComponentsManager<Instance>({
return new ComponentsManager<TInstance>({
moduleState,
objectLoader,
componentResources,
Expand All @@ -168,7 +184,7 @@ export class ComponentsManagerBuilder<Instance = any> {
}
}

export interface IComponentsManagerBuilderOptions<Instance> {
export interface IComponentsManagerBuilderOptions<TInstance> {
/* ----- REQUIRED FIELDS ----- */
/**
* Absolute path to the package root from which module resolution should start.
Expand All @@ -192,7 +208,7 @@ export interface IComponentsManagerBuilderOptions<Instance> {
* A strategy for constructing instances.
* Defaults to {@link ConstructionStrategyCommonJs}.
*/
constructionStrategy?: IConstructionStrategy<Instance>;
constructionStrategy?: IConstructionStrategy<TInstance>;
/**
* If the error state should be dumped into `componentsjs-error-state.json`
* after failed instantiations.
Expand Down
11 changes: 10 additions & 1 deletion lib/loading/ConfigRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Readable } from 'stream';
import type { Readable } from 'node:stream';
import type * as RDF from '@rdfjs/types';
import type { ContextParser } from 'jsonld-context-parser';
import type { RdfObjectLoader, Resource } from 'rdf-object';
import { termToString } from 'rdf-string';
import type { Logger } from 'winston';
Expand All @@ -15,13 +16,15 @@ export class ConfigRegistry {
private readonly logger: Logger;
private readonly skipContextValidation: boolean;
private readonly remoteContextLookups: boolean;
private readonly contextParser?: ContextParser;

public constructor(options: IConfigLoaderRegistryOptions) {
this.moduleState = options.moduleState;
this.objectLoader = options.objectLoader;
this.logger = options.logger;
this.skipContextValidation = options.skipContextValidation;
this.remoteContextLookups = options.remoteContextLookups;
this.contextParser = options.contextParser;
}

/**
Expand All @@ -38,6 +41,7 @@ export class ConfigRegistry {
logger: this.logger,
skipContextValidation: this.skipContextValidation,
remoteContextLookups: this.remoteContextLookups,
contextParser: this.contextParser,
}));
}

Expand All @@ -62,6 +66,7 @@ export class ConfigRegistry {
): Promise<void> {
// Create ad-hoc resource
const configResource = this.objectLoader.createCompactedResource({
// eslint-disable-next-line ts/naming-convention
'@id': configId,
types: componentTypeIri,
});
Expand All @@ -85,4 +90,8 @@ export interface IConfigLoaderRegistryOptions {
logger: Logger;
skipContextValidation: boolean;
remoteContextLookups: boolean;
/**
* An optional shared, cache-bearing JSON-LD context parser reused across all config files.
*/
contextParser?: ContextParser;
}
90 changes: 81 additions & 9 deletions lib/rdf/RdfParser.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,61 @@
import { createReadStream } from 'fs';
import type { Readable } from 'stream';
import { createReadStream, promises as fs } from 'node:fs';
import type { Readable } from 'node:stream';
import type * as RDF from '@rdfjs/types';
import * as JsonLdContextParser from 'jsonld-context-parser';
import { ContextParser } from 'jsonld-context-parser';
import type { ParseOptions } from 'rdf-parse';
import rdfParser from 'rdf-parse';
import { rdfParser } from 'rdf-parse';
import type { Logger } from 'winston';
import { PrefetchedDocumentLoader } from './PrefetchedDocumentLoader';
import { RdfStreamIncluder } from './RdfStreamIncluder';
// Import syntax only works in Node > 12
const fs = require('fs').promises;

/**
* Parses a data stream to a triple stream.
*/
export class RdfParser {
/**
* Create a single, cache-bearing JSON-LD context parser that can be reused across all files
* that are parsed during a component/config load.
*
* By default, Components.js constructs a fresh {@link PrefetchedDocumentLoader} and (indirectly,
* via jsonld-streaming-parser) a fresh `ContextParser` for every `.jsonld` file. As a result the
* well-known `@context`s (e.g. the shared `componentsjs` and `@solid/community-server` contexts)
* are re-loaded and re-normalized once per file - during a Community Solid Server boot the
* `componentsjs` context alone is normalized ~979 times.
*
* Passing the returned parser to every parse (see {@link RdfParserOptions#contextParser}) shares a
* single {@link PrefetchedDocumentLoader}, a single raw-document cache, and a single normalized
* {@link ContextCache} across all files, so the well-known contexts are loaded once and (once the
* shared cache can be hit) normalized once instead of once per file.
*
* @param options Options describing the prefetched contexts and validation behaviour.
*/
public static createSharedContextParser(options: ISharedContextParserOptions): ContextParser {
const documentLoader = new PrefetchedDocumentLoader({
contexts: options.contexts ?? {},
logger: options.logger,
remoteContextLookups: options.remoteContextLookups,
});
const parserOptions: Record<string, any> = {
documentLoader,
skipValidation: options.skipContextValidation,
};
// Attach a shared normalized-context cache when the installed jsonld-context-parser exposes one
// (feature-detected so this remains compatible with versions that predate the cache).
if ((<any> JsonLdContextParser).ContextCache) {
parserOptions.contextCache = new (<any> JsonLdContextParser).ContextCache();
}
return new ContextParser(parserOptions);
}

/**
* Parses the given stream into RDF quads.
* @param textStream A text stream.
* @param options Parsing options.
*/
public parse(textStream: NodeJS.ReadableStream, options: RdfParserOptions): RDF.Stream & Readable {
// Parsing libraries don't work as expected if path contains backslashes
options.path = options.path.replace(/\\+/gu, '/');
options.path = options.path.replaceAll(/\\+/gu, '/');

if (!options.baseIRI) {
// Try converting path to URL using defined import paths
Expand All @@ -44,10 +79,12 @@ export class RdfParser {
}

// Set JSON-LD parser options
(<any> options)['@comunica/actor-rdf-parse-jsonld:parserOptions'] = {
// Override the JSON-LD document loader
const jsonLdParserOptions: Record<string, any> = {
// Override the JSON-LD document loader.
// This is always provided so that a jsonld-streaming-parser that does not (yet) support an
// injected context parser still uses the prefetched contexts and remote-lookup protection.
documentLoader: new PrefetchedDocumentLoader({
contexts: options.contexts || {},
contexts: options.contexts ?? {},
logger: options.logger,
path: options.path,
remoteContextLookups: options.remoteContextLookups,
Expand All @@ -57,6 +94,15 @@ export class RdfParser {
// If JSON-LD context validation should be skipped
skipContextValidation: options.skipContextValidation,
};
if (options.contextParser) {
// Reuse one shared, cache-bearing context parser (which carries its own single prefetched
// document loader) across all files, so the shared @contexts are only loaded/normalized once
// instead of once per file. This is honoured by a jsonld-streaming-parser that supports the
// `contextParser` option; on older versions the option is ignored and the per-file
// `documentLoader` above is used, preserving the previous behaviour.
jsonLdParserOptions.contextParser = options.contextParser;
}
(<any> options)['@comunica/actor-rdf-parse-jsonld:parserOptions'] = jsonLdParserOptions;

// Execute parsing
const quadStream = rdfParser.parse(textStream, options);
Expand Down Expand Up @@ -131,4 +177,30 @@ export type RdfParserOptions = ParseOptions & {
* If allowed, only a warning is emitted.
*/
remoteContextLookups?: boolean;
/**
* An optional shared JSON-LD context parser to reuse across all files.
* When provided, this (cache-bearing) parser and its prefetched document loader are used for all
* context resolution instead of constructing a new document loader (and context parser) per file.
* Create one with {@link RdfParser#createSharedContextParser}.
*/
contextParser?: ContextParser;
};

export interface ISharedContextParserOptions {
/**
* The cached JSON-LD contexts (id -> context document).
*/
contexts?: Record<string, any>;
/**
* An optional logger, used to warn on remote context lookups.
*/
logger?: Logger;
/**
* If remote context lookups are allowed.
*/
remoteContextLookups?: boolean;
/**
* If JSON-LD context validation should be skipped.
*/
skipContextValidation?: boolean;
}
Loading
Loading