Skip to content

Commit 2ab13f5

Browse files
author
Fletcher91
committed
[IMP] Restructure store creation
This allows all layers of the library to execute actions via middleware. The first implemented use case here consists of executing the actions described in the `Exec-Action` header.
1 parent f9f8bb9 commit 2ab13f5

11 files changed

+144
-47
lines changed

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@
6161
"coverageThreshold": {
6262
"global": {
6363
"branches": 64,
64-
"functions": 74,
65-
"lines": 74,
66-
"statements": 74
64+
"functions": 76,
65+
"lines": 76,
66+
"statements": 75
6767
}
6868
},
6969
"setupTestFrameworkScriptFile": "./jest-plugins",

src/LinkedDataAPI.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66

77
import {
88
DataProcessorOpts,
9+
Dispatcher,
910
} from "./types";
1011
import {
1112
DataTuple,
@@ -16,7 +17,7 @@ import {
1617
SomeNode,
1718
} from "./types";
1819

19-
export interface LinkedDataAPI {
20+
export interface LinkedDataAPI extends Dispatcher {
2021
execActionByIRI(subject: NamedNode, dataTuple: DataTuple): Promise<LinkedActionResponse>;
2122

2223
/**

src/LinkedRenderStore.ts

+29-27
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ import { Schema } from "./Schema";
1717
import {
1818
ComponentRegistration,
1919
DataObject,
20+
Dispatcher,
2021
EmptyRequestStatus,
2122
FetchOpts,
2223
FulfilledRequestStatus,
2324
LazyNNArgument,
2425
LinkedActionResponse,
2526
LinkedRenderStoreOptions,
2627
MiddlewareActionHandler,
27-
MiddlewareFn,
2828
NamespaceMap,
2929
SomeNode,
3030
SubscriptionRegistration,
@@ -39,7 +39,7 @@ declare global {
3939
}
4040
}
4141

42-
export class LinkedRenderStore<T> {
42+
export class LinkedRenderStore<T> implements Dispatcher {
4343
public static registerRenderer<T>(
4444
component: T,
4545
type: LazyNNArgument,
@@ -61,7 +61,7 @@ export class LinkedRenderStore<T> {
6161

6262
private api: LinkedDataAPI;
6363
private mapping: ComponentStore<T>;
64-
private middleware: MiddlewareActionHandler;
64+
private _dispatch?: MiddlewareActionHandler;
6565
private schema: Schema;
6666
private store: RDFStore = new RDFStore();
6767
private subscriptions: SubscriptionRegistration[] = [];
@@ -74,16 +74,29 @@ export class LinkedRenderStore<T> {
7474
}
7575

7676
this.api = opts.api || new DataProcessor({
77-
requestNotifier: this.touch.bind(this),
77+
dispatch: opts.dispatch,
7878
store: this.store,
7979
});
80+
if (opts.dispatch) {
81+
this.dispatch = opts.dispatch;
82+
}
8083
this.defaultType = opts.defaultType || defaultNS.schema("Thing");
8184
this.namespaces = opts.namespaces || {...defaultNS};
8285
this.schema = opts.schema || new Schema(this.store);
8386
this.mapping = opts.mapping || new ComponentStore(this.schema);
84-
// tslint:disable-next-line typedef
85-
const actionMiddleware: MiddlewareFn<T> = () => () => this.execActionByIRI.bind(this);
86-
this.middleware = this.applyMiddleware(...(opts.middleware || []), actionMiddleware);
87+
}
88+
89+
public get dispatch(): MiddlewareActionHandler {
90+
if (typeof this._dispatch === "undefined") {
91+
throw new Error("Invariant: cannot call `dispatch` before initialization is complete");
92+
}
93+
94+
return this._dispatch;
95+
}
96+
97+
public set dispatch(value: MiddlewareActionHandler) {
98+
this._dispatch = value;
99+
this.api.dispatch = value;
87100
}
88101

89102
/**
@@ -128,7 +141,7 @@ export class LinkedRenderStore<T> {
128141
* @param {Object} args The arguments to the function defined by the subject.
129142
*/
130143
public async exec(subject: NamedNode, args?: DataObject): Promise<any> {
131-
return this.middleware(subject, args);
144+
return this.dispatch(subject, args);
132145
}
133146

134147
/**
@@ -317,6 +330,14 @@ export class LinkedRenderStore<T> {
317330
this.subscriptions.push(registration);
318331
}
319332

333+
/** @internal */
334+
public touch(iri: string | NamedNode, _err?: Error): boolean {
335+
const resource = typeof iri === "string" ? namedNodeByIRI(iri) : iri;
336+
this.store.addStatements([new Statement(resource, defaultNS.ll("nop"), Literal.fromValue(0))]);
337+
this.broadcast();
338+
return true;
339+
}
340+
320341
/**
321342
* Returns an entity from the cache directly.
322343
* This won't cause any network requests even if the entity can't be found.
@@ -329,18 +350,6 @@ export class LinkedRenderStore<T> {
329350
return this.store.statementsFor(iri);
330351
}
331352

332-
/**
333-
* Binds and reduces an array of middleware function down to a handler.
334-
* @param layers
335-
*/
336-
private applyMiddleware(...layers: Array<MiddlewareFn<T>>): MiddlewareActionHandler {
337-
const storeBound = layers.map((middleware) => middleware(this));
338-
339-
const dispatch: MiddlewareActionHandler = (a: NamedNode, _o: any): Promise<any> => Promise.resolve(a);
340-
341-
return storeBound.reduceRight((composed, f) => f(composed), dispatch);
342-
}
343-
344353
/**
345354
* Broadcasts buffered to all subscribers.
346355
* The actual broadcast might be executed asynchronously to prevent lag.
@@ -389,11 +398,4 @@ export class LinkedRenderStore<T> {
389398
}
390399
});
391400
}
392-
393-
private touch(iri: string | NamedNode, _err?: Error): boolean {
394-
const resource = typeof iri === "string" ? namedNodeByIRI(iri) : iri;
395-
this.store.addStatements([new Statement(resource, defaultNS.ll("nop"), Literal.fromValue(0))]);
396-
this.broadcast();
397-
return true;
398-
}
399401
}

src/__tests__/utilities.spec.ts

-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import {
1010
allRDFPropertyStatements,
1111
allRDFValues,
1212
anyRDFValue,
13-
fetchWithExtension,
14-
getExtention,
1513
getPropBestLang,
1614
getPropBestLangRaw,
1715
isDifferentOrigin,

src/createStore.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { NamedNode } from "rdflib";
2+
3+
import { LinkedRenderStore } from "./LinkedRenderStore";
4+
import { linkMiddleware } from "./linkMiddleware";
5+
import { LinkedRenderStoreOptions, MiddlewareActionHandler, MiddlewareFn } from "./types";
6+
7+
function applyMiddleware<T>(lrs: LinkedRenderStore<T>, ...layers: Array<MiddlewareFn<T>>): MiddlewareActionHandler {
8+
const storeBound = layers.map((middleware) => middleware(lrs));
9+
10+
const finish: MiddlewareActionHandler = (a: NamedNode, _o: any): Promise<any> => Promise.resolve(a);
11+
12+
return storeBound.reduceRight((composed, f) => f(composed), finish);
13+
}
14+
15+
export function createStore<T>(storeOpts: LinkedRenderStoreOptions<T>,
16+
middleware = [],
17+
trailingMiddleware = []): LinkedRenderStore<T> {
18+
19+
const LRS = new LinkedRenderStore<T>(storeOpts);
20+
21+
LRS.dispatch = applyMiddleware<T>(
22+
LRS,
23+
...middleware,
24+
linkMiddleware(trailingMiddleware.length > 0),
25+
...trailingMiddleware,
26+
);
27+
28+
return LRS;
29+
}

src/link-lib.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { LinkedRenderStore } from "./LinkedRenderStore";
22

3+
export { createStore } from "./createStore";
4+
export { linkMiddleware } from "./linkMiddleware";
35
export { RDFStore } from "./RDFStore";
46
export { Schema } from "./Schema";
57
export * from "./testUtilities";

src/linkMiddleware.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { NamedNode } from "rdflib";
2+
3+
import { LinkedRenderStore } from "./LinkedRenderStore";
4+
import { MiddlewareActionHandler, MiddlewareFn, MiddlewareWithBoundLRS } from "./types";
5+
import { defaultNS } from "./utilities/constants";
6+
7+
/**
8+
* Binds various uris to link actions.
9+
*
10+
* @param catchActions {boolean} Set to true to catch and pass left-over actions to execActionByIRI.
11+
*/
12+
export const linkMiddleware = <T>(catchActions = true): MiddlewareFn<T> =>
13+
(lrs: LinkedRenderStore<T>): MiddlewareWithBoundLRS =>
14+
(next: MiddlewareActionHandler): MiddlewareActionHandler =>
15+
(action: NamedNode, args: any): Promise<any> => {
16+
17+
if (action.value.startsWith(defaultNS.ll("data/rdflib/").value)) {
18+
return Promise.resolve(lrs.touch(args[0], args[1]));
19+
}
20+
21+
if (catchActions) {
22+
return lrs.execActionByIRI(action, args);
23+
}
24+
25+
return next(action, args);
26+
};

src/processor/DataProcessor.ts

+32-7
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
IndexedFormula,
1111
Literal,
1212
NamedNode,
13-
RequestCallbackHandler,
1413
Serializer,
1514
Statement,
1615
uri as Uri,
@@ -25,6 +24,7 @@ import {
2524
FailedResponse,
2625
FulfilledRequestStatus,
2726
LinkedActionResponse,
27+
MiddlewareActionHandler,
2828
ResponseAndFallbacks,
2929
ResponseTransformer,
3030
} from "../types";
@@ -145,11 +145,11 @@ export class DataProcessor implements LinkedDataAPI {
145145
public timeout: number = 30000;
146146

147147
private _fetcher: Fetcher | undefined;
148+
private _dispatch?: MiddlewareActionHandler;
148149
private readonly requestInitGenerator: RequestInitGenerator;
149150
private readonly mapping: { [k: string]: ResponseTransformer[] };
150151
private readonly requestMap: Map<NamedNode, Promise<Statement[]> | undefined>;
151152
private readonly statusMap: Map<NamedNode, EmptyRequestStatus | FulfilledRequestStatus>;
152-
private readonly requestNotifier?: RequestCallbackHandler;
153153
private readonly store: RDFStore;
154154

155155
private get fetcher(): Fetcher {
@@ -159,10 +159,13 @@ export class DataProcessor implements LinkedDataAPI {
159159
timeout: this.timeout,
160160
});
161161
FETCHER_CALLBACKS.forEach((hook) => {
162+
const hookIRI = defaultNS.ll(`data/rdflib/${hook}`);
162163
this._fetcher!.addCallback(hook, this.invalidateCache.bind(this));
163-
if (typeof this.requestNotifier === "function") {
164-
this._fetcher!.addCallback(hook, this.requestNotifier);
165-
}
164+
this._fetcher!.addCallback(hook, (iri: string | NamedNode, _err?: Error) => {
165+
this.dispatch(hookIRI, [typeof iri === "string" ? namedNodeByIRI(iri) : iri, _err]);
166+
167+
return true;
168+
});
166169
});
167170
}
168171
return this._fetcher;
@@ -176,17 +179,29 @@ export class DataProcessor implements LinkedDataAPI {
176179
this.accept = opts.accept || {
177180
default: "",
178181
};
182+
this._dispatch = opts.dispatch;
179183
this.requestInitGenerator = opts.requestInitGenerator || new RequestInitGenerator();
180184
this.mapping = opts.mapping || {};
181185
this.requestMap = new Map();
182186
this.statusMap = new Map();
183187
this.store = opts.store;
184-
this.requestNotifier = opts.requestNotifier;
185188
if (opts.fetcher) {
186189
this.fetcher = opts.fetcher;
187190
}
188191
}
189192

193+
public get dispatch(): MiddlewareActionHandler {
194+
if (typeof this._dispatch === "undefined") {
195+
throw new Error("Invariant: cannot call `dispatch` before initialization is complete");
196+
}
197+
198+
return this._dispatch;
199+
}
200+
201+
public set dispatch(value: MiddlewareActionHandler) {
202+
this._dispatch = value;
203+
}
204+
190205
public async execActionByIRI(subject: NamedNode, dataTuple: DataTuple): Promise<LinkedActionResponse> {
191206

192207
const [graph, blobs = []] = dataTuple;
@@ -273,7 +288,17 @@ export class DataProcessor implements LinkedDataAPI {
273288
opts,
274289
);
275290

276-
return this.fetcher.load(iri, options);
291+
return this.fetcher.load(iri, options).then((res) => {
292+
const actionsHeader = getHeader(res, "Exec-Action");
293+
if (actionsHeader) {
294+
const actions = actionsHeader.split(", ");
295+
for (let i = 0; i < actions.length; i++) {
296+
this.dispatch(namedNodeByIRI(actions[i]), undefined);
297+
}
298+
}
299+
300+
return res;
301+
});
277302
}
278303

279304
/**

src/testUtilities.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { NamedNode } from "rdflib";
22

33
import { ComponentStore } from "./ComponentStore";
4+
import { createStore } from "./createStore";
45
import { LinkedDataAPI } from "./LinkedDataAPI";
56
import { LinkedRenderStore } from "./LinkedRenderStore";
67
import { DataProcessor } from "./processor/DataProcessor";
78
import { RDFStore } from "./RDFStore";
89
import { Schema } from "./Schema";
9-
import { LinkedRenderStoreOptions } from "./types";
10+
import { LinkedRenderStoreOptions, MiddlewareActionHandler } from "./types";
1011

1112
export type BasicComponent = () => string | undefined;
1213

@@ -20,6 +21,7 @@ export class ComponentStoreTestProxy<T> extends ComponentStore<T> {
2021

2122
export interface ExplodedLRS<T> {
2223
api: LinkedDataAPI;
24+
dispatch: MiddlewareActionHandler;
2325
processor: DataProcessor;
2426
lrs: LinkedRenderStore<T>;
2527
mapping: ComponentStoreTestProxy<T>;
@@ -42,7 +44,14 @@ export const getBasicStore = (opts: GetBasicStoreOpts = {}): ExplodedLRS<BasicC
4244
schema,
4345
store,
4446
} as LinkedRenderStoreOptions<BasicComponent>;
45-
const lrs = new LinkedRenderStore(conf);
47+
const lrs = createStore(conf);
4648

47-
return {lrs, mapping, processor, schema, store} as ExplodedLRS<BasicComponent>;
49+
return {
50+
dispatch: lrs.dispatch,
51+
lrs,
52+
mapping,
53+
processor,
54+
schema,
55+
store,
56+
} as ExplodedLRS<BasicComponent>;
4857
};

0 commit comments

Comments
 (0)