Added tracking proxy to the connected library rather than the react library.

main
Jackson Morgan 4 months ago
parent e4f434eeb3
commit 0c4b5e0770
  1. 72
      packages/connected/src/trackingProxy/TrackingProxyContext.ts
  2. 62
      packages/connected/src/trackingProxy/TrackingSetProxy.ts
  3. 49
      packages/connected/src/trackingProxy/TrackingSubjectProxy.ts
  4. 42
      packages/connected/src/trackingProxy/createTrackingProxy.ts
  5. 2
      packages/connected/test/ErrorResult.test.ts
  6. 8
      packages/connected/test/mocks/MockResource.ts

@ -0,0 +1,72 @@
import type {
ProxyContextOptions,
SubjectProxy,
SetProxy,
} from "@ldo/jsonld-dataset-proxy";
import { ProxyContext } from "@ldo/jsonld-dataset-proxy";
import type { QuadMatch } from "@ldo/rdf-utils";
import type {
nodeEventListener,
SubscribableDataset,
} from "@ldo/subscribable-dataset";
import type { BlankNode, NamedNode, Quad } from "@rdfjs/types";
import { createTrackingSubjectProxy } from "./TrackingSubjectProxy";
import { createTrackingSetProxy } from "./TrackingSetProxy";
/**
* @internal
* Options to be passed to the tracking proxy
*/
export interface TrackingProxyContextOptions extends ProxyContextOptions {
dataset: SubscribableDataset<Quad>;
}
/**
* @internal
* A listener that gets triggered whenever there's an update
*/
/**
* @internal
* This proxy exists to ensure react components rerender at the right time. It
* keeps track of every key accessed in a Linked Data Object and only when the
* dataset is updated with that key does it rerender the react component.
*/
export class TrackingProxyContext extends ProxyContext {
private listener: nodeEventListener<Quad>;
private subscribableDataset: SubscribableDataset<Quad>;
constructor(
options: TrackingProxyContextOptions,
listener: nodeEventListener<Quad>,
) {
super(options);
this.subscribableDataset = options.dataset;
this.listener = listener;
}
// Adds the listener to the subscribable dataset while ensuring deduping of the listener
public addListener(eventName: QuadMatch) {
const listeners = this.subscribableDataset.listeners(eventName);
if (!listeners.includes(this.listener)) {
this.subscribableDataset.on(eventName, this.listener);
}
}
protected createNewSubjectProxy(node: NamedNode | BlankNode): SubjectProxy {
return createTrackingSubjectProxy(this, node);
}
protected createNewSetProxy(
quadMatch: QuadMatch,
isSubjectOriented?: boolean,
isLangStringSet?: boolean,
): SetProxy {
return createTrackingSetProxy(
this,
quadMatch,
isSubjectOriented,
isLangStringSet,
);
}
}

@ -0,0 +1,62 @@
import { createNewSetProxy, type SetProxy } from "@ldo/jsonld-dataset-proxy";
import type { TrackingProxyContext } from "./TrackingProxyContext";
import type { QuadMatch } from "@ldo/rdf-utils";
/**
* @internal
*
* Creates a tracking proxy for a set, a proxy that tracks the fields that have
* been accessed.
*/
export function createTrackingSetProxy(
proxyContext: TrackingProxyContext,
quadMatch: QuadMatch,
isSubjectOriented?: boolean,
isLangStringSet?: boolean,
): SetProxy {
const baseSetProxy = createNewSetProxy(
quadMatch,
isSubjectOriented ?? false,
proxyContext,
isLangStringSet,
);
return new Proxy(baseSetProxy, {
get: (target: SetProxy, key: string | symbol, receiver) => {
if (trackingMethods.has(key)) {
proxyContext.addListener(quadMatch);
} else if (disallowedMethods.has(key)) {
console.warn(
"You've attempted to modify a value on a Linked Data Object from the useSubject, useMatchingSubject, or useMatchingObject hooks. These linked data objects should only be used to render data, not modify it. To modify data, use the `changeData` function.",
);
}
return Reflect.get(target, key, receiver);
},
});
}
const trackingMethods = new Set([
"has",
"size",
"entries",
"keys",
"values",
Symbol.iterator,
"every",
"every",
"some",
"forEach",
"map",
"reduce",
"toArray",
"toJSON",
"difference",
"intersection",
"isDisjointFrom",
"isSubsetOf",
"isSupersetOf",
"symmetricDifference",
"union",
]);
const disallowedMethods = new Set<string | symbol>(["add", "clear", "delete"]);

@ -0,0 +1,49 @@
import type { SubjectProxyTarget } from "@ldo/jsonld-dataset-proxy";
import {
createSubjectHandler,
type SubjectProxy,
} from "@ldo/jsonld-dataset-proxy";
import type { BlankNode, NamedNode } from "@rdfjs/types";
import type { TrackingProxyContext } from "./TrackingProxyContext";
import { namedNode } from "@rdfjs/data-model";
/**
* @internal
*
* Creates a tracking proxy for a single value, a proxy that tracks the fields
* that have been accessed.
*/
export function createTrackingSubjectProxy(
proxyContext: TrackingProxyContext,
node: NamedNode | BlankNode,
): SubjectProxy {
const baseHandler = createSubjectHandler(proxyContext);
const oldGetFunction = baseHandler.get;
const newGetFunction: ProxyHandler<SubjectProxyTarget>["get"] = (
target: SubjectProxyTarget,
key: string | symbol,
receiver,
) => {
const subject = target["@id"];
const rdfTypes = proxyContext.getRdfType(subject);
if (typeof key === "symbol") {
// Do Nothing
} else if (key === "@id") {
proxyContext.addListener([subject, null, null, null]);
} else if (!proxyContext.contextUtil.isSet(key, rdfTypes)) {
const predicate = namedNode(
proxyContext.contextUtil.keyToIri(key, rdfTypes),
);
proxyContext.addListener([subject, predicate, null, null]);
}
return oldGetFunction && oldGetFunction(target, key, receiver);
};
baseHandler.get = newGetFunction;
baseHandler.set = () => {
console.warn(
"You've attempted to set a value on a Linked Data Object from the useSubject, useMatchingSubject, or useMatchingObject hooks. These linked data objects should only be used to render data, not modify it. To modify data, use the `changeData` function.",
);
return true;
};
return new Proxy({ "@id": node }, baseHandler) as unknown as SubjectProxy;
}

@ -0,0 +1,42 @@
import {
ContextUtil,
JsonldDatasetProxyBuilder,
} from "@ldo/jsonld-dataset-proxy";
import { LdoBuilder } from "@ldo/ldo";
import type { LdoBase, LdoDataset, ShapeType } from "@ldo/ldo";
import { TrackingProxyContext } from "./TrackingProxyContext";
import { defaultGraph } from "@rdfjs/data-model";
import type { nodeEventListener } from "@ldo/subscribable-dataset";
import type { Quad } from "@rdfjs/types";
/**
* @internal
* Creates a Linked Data Object builder that when creating linked data objects
* it tracks when something that was read from it is updated and triggers some
* action based on that.
*/
export function createTrackingProxyBuilder<Type extends LdoBase>(
dataset: LdoDataset,
shapeType: ShapeType<Type>,
onUpdate: nodeEventListener<Quad>,
): LdoBuilder<Type> {
// Remove all current subscriptions
// dataset.removeListenerFromAllEvents(onUpdate);
// Rebuild the LdoBuilder from scratch to inject TrackingProxyContext
const contextUtil = new ContextUtil(shapeType.context);
const proxyContext = new TrackingProxyContext(
{
dataset,
contextUtil,
writeGraphs: [defaultGraph()],
languageOrdering: ["none", "en", "other"],
},
onUpdate,
);
const builder = new LdoBuilder(
new JsonldDatasetProxyBuilder(proxyContext),
shapeType,
);
return builder;
}

@ -5,7 +5,7 @@ import {
UnexpectedResourceError, UnexpectedResourceError,
} from "../src/results/error/ErrorResult"; } from "../src/results/error/ErrorResult";
import { InvalidUriError } from "../src/results/error/InvalidUriError"; import { InvalidUriError } from "../src/results/error/InvalidUriError";
import { MockResouce } from "./MockResource"; import { MockResouce } from "./mocks/MockResource";
const mockResource = new MockResouce("https://example.com/"); const mockResource = new MockResouce("https://example.com/");

@ -1,15 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import EventEmitter from "events"; import EventEmitter from "events";
import type { ResourceError } from "../src"; import type { ResourceError } from "../../src";
import { import {
Unfetched, Unfetched,
type ConnectedResult, type ConnectedResult,
type Resource, type Resource,
type ResourceEventEmitter, type ResourceEventEmitter,
} from "../src"; } from "../../src";
import type { DatasetChanges } from "@ldo/rdf-utils"; import type { DatasetChanges } from "@ldo/rdf-utils";
import type { ReadSuccess } from "../src/results/success/ReadSuccess"; import type { ReadSuccess } from "../../src/results/success/ReadSuccess";
import type { UpdateSuccess } from "../src/results/success/UpdateSuccess"; import type { UpdateSuccess } from "../../src/results/success/UpdateSuccess";
export class MockResouce export class MockResouce
extends (EventEmitter as new () => ResourceEventEmitter) extends (EventEmitter as new () => ResourceEventEmitter)
Loading…
Cancel
Save