React library now has a hook for link query

main
Jackson Morgan 4 months ago
parent d1eb89ea91
commit 8dd6540c63
  1. 17
      packages/connected/src/index.ts
  2. 5
      packages/connected/src/linkTraversal/ResourceLinkQuery.ts
  3. 3
      packages/connected/src/types/ILinkQuery.ts
  4. 3
      packages/react/src/createLdoReactMethods.tsx
  5. 4
      packages/react/src/index.ts
  6. 51
      packages/react/src/methods/useLinkQuery.ts
  7. 61
      packages/react/src/util/TrackingProxyContext.ts
  8. 62
      packages/react/src/util/TrackingSetProxy.ts
  9. 49
      packages/react/src/util/TrackingSubjectProxy.ts
  10. 26
      packages/react/src/util/useTrackingProxy.ts

@ -1,14 +1,17 @@
export * from "./types/IConnectedLdoDataset";
export * from "./ConnectedLdoBuilder";
export * from "./ConnectedLdoDataset";
export * from "./ConnectedLdoTransactionDataset";
export * from "./types/ConnectedPlugin";
export * from "./Resource";
export * from "./InvalidIdentifierResource";
export * from "./types/ConnectedContext";
export * from "./methods";
export * from "./createConntectedLdoDataset";
export * from "./notifications/SubscriptionCallbacks";
export * from "./types/ConnectedContext";
export * from "./types/ConnectedPlugin";
export * from "./types/IConnectedLdoDataset";
export * from "./types/IConnectedLdoBuilder";
export * from "./types/ILinkQuery";
export * from "./util/splitChangesByGraph";
@ -22,6 +25,12 @@ export * from "./results/success/ReadSuccess";
export * from "./results/success/UpdateSuccess";
export * from "./notifications/NotificationSubscription";
export * from "./notifications/SubscriptionCallbacks";
export * from "./trackingProxy/TrackingProxyContext";
export * from "./trackingProxy/TrackingSetProxy";
export * from "./trackingProxy/TrackingSubjectProxy";
export * from "./trackingProxy/createTrackingProxy";
export * from "./linkTraversal/ResourceLinkQuery";
export * from "./linkTraversal/exploreLinks";

@ -180,6 +180,11 @@ export class ResourceLinkQuery<
}
}
async unsubscribeAll() {
this.thisUnsubscribeIds.clear();
await this.fullUnsubscribe();
}
fromSubject(): ExpandDeep<LQReturn<Type, QueryInput>> {
return this.ldoBuilder.fromSubject(
this.startingSubject,

@ -91,7 +91,8 @@ export interface ILinkQuery<Type extends LdoBase, Input extends LQInput<Type>> {
options?: LinkQueryRunOptions,
): Promise<ExpandDeep<LQReturn<Type, Input>>>;
subscribe(): Promise<string>;
unsubscribe(subscriptionId: string): void;
unsubscribe(subscriptionId: string): Promise<void>;
unsubscribeAll(): Promise<void>;
fromSubject(): ExpandDeep<LQReturn<Type, Input>>;
}

@ -9,6 +9,7 @@ import { createUseMatchSubject } from "./methods/useMatchSubject";
import { createUseResource } from "./methods/useResource";
import { createUseSubject } from "./methods/useSubject";
import { createUseSubscribeToResource } from "./methods/useSubscribeToResource";
import { createUseLinkQuery } from "./methods/useLinkQuery";
/**
* A function that creates all common react functions given specific plugin.
@ -29,6 +30,7 @@ import { createUseSubscribeToResource } from "./methods/useSubscribeToResource";
* useResource,
* useSubject,
* useSubscribeToResource,
* useLinkQuery,
* } = createLdoReactMethods([
* solidConnectedPlugin,
* nextGraphConnectedPlugin
@ -70,5 +72,6 @@ export function createLdoReactMethods<
useResource: createUseResource(dataset),
useSubject: createUseSubject(dataset),
useSubscribeToResource: createUseSubscribeToResource(dataset),
useLinkQuery: createUseLinkQuery(dataset),
};
}

@ -6,8 +6,6 @@ export * from "./methods/useMatchSubject";
export * from "./methods/useResource";
export * from "./methods/useSubject";
export * from "./methods/useSubscribeToResource";
export * from "./methods/useLinkQuery";
export * from "./util/TrackingProxyContext";
export * from "./util/TrackingSetProxy";
export * from "./util/TrackingSubjectProxy";
export * from "./util/useTrackingProxy";

@ -1,14 +1,22 @@
import type { ConnectedLdoDataset, ConnectedPlugin } from "@ldo/connected";
import type { LdoBase } from "@ldo/ldo";
import type {
ConnectedLdoDataset,
ConnectedPlugin,
ExpandDeep,
LQInput,
LQReturn,
ResourceLinkQuery,
} from "@ldo/connected";
import type { LdoBase, LdoBuilder, ShapeType } from "@ldo/ldo";
import type { SubjectNode } from "@ldo/rdf-utils";
import type { LQInput } from "packages/connected/dist/types/ILinkQuery";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTrackingProxy } from "../util/useTrackingProxy";
/**
* @internal
*
* Creates a useMatchSubject function.
*/
export function createUseQueryLink<Plugins extends ConnectedPlugin[]>(
export function createUseLinkQuery<Plugins extends ConnectedPlugin[]>(
dataset: ConnectedLdoDataset<Plugins>,
) {
/**
@ -23,5 +31,38 @@ export function createUseQueryLink<Plugins extends ConnectedPlugin[]>(
startingResource: string,
startingSubject: SubjectNode | string,
linkQuery: QueryInput,
): ExpandDeep<LQReturn<Type, QueryInput>> | undefined {};
): ExpandDeep<LQReturn<Type, QueryInput>> | undefined {
const linkQueryRef = useRef<
ResourceLinkQuery<Type, QueryInput, Plugins> | undefined
>();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (linkQueryRef.current) {
linkQueryRef.current.unsubscribeAll();
}
const resource = dataset.getResource(startingResource);
setIsLoading(true);
linkQueryRef.current = dataset
.usingType(shapeType)
.startLinkQuery(resource, startingSubject, linkQuery);
linkQueryRef.current.subscribe().then(() => setIsLoading(false));
}, [shapeType, startingResource, startingSubject, linkQuery]);
const fromSubject = useCallback(
(builder: LdoBuilder<Type>) => {
if (!startingSubject) return;
return builder.fromSubject(startingSubject);
},
[startingSubject],
);
const linkedDataObject = useTrackingProxy(shapeType, fromSubject, dataset);
return isLoading
? undefined
: (linkedDataObject as unknown as ExpandDeep<LQReturn<Type, QueryInput>>);
};
}

@ -1,61 +0,0 @@
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 { 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
* 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: () => void;
private subscribableDataset: SubscribableDataset<Quad>;
constructor(options: TrackingProxyContextOptions, listener: () => void) {
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,
);
}
}

@ -1,62 +0,0 @@
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"]);

@ -1,49 +0,0 @@
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;
}

@ -1,12 +1,7 @@
import {
ContextUtil,
JsonldDatasetProxyBuilder,
} from "@ldo/jsonld-dataset-proxy";
import { LdoBuilder } from "@ldo/ldo";
import type { LdoBuilder } from "@ldo/ldo";
import type { LdoBase, LdoDataset, ShapeType } from "@ldo/ldo";
import { useCallback, useEffect, useMemo, useState } from "react";
import { TrackingProxyContext } from "./TrackingProxyContext";
import { defaultGraph } from "@rdfjs/data-model";
import { createTrackingProxyBuilder } from "@ldo/connected";
/**
* @internal
@ -28,22 +23,7 @@ export function useTrackingProxy<Type extends LdoBase, ReturnType>(
const linkedDataObject = useMemo(() => {
// Remove all current subscriptions
dataset.removeListenerFromAllEvents(forceUpdate);
// 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"],
},
forceUpdate,
);
const builder = new LdoBuilder(
new JsonldDatasetProxyBuilder(proxyContext),
shapeType,
);
const builder = createTrackingProxyBuilder(dataset, shapeType, forceUpdate);
return createLdo(builder);
}, [shapeType, dataset, forceUpdateCounter, forceUpdate, createLdo]);

Loading…
Cancel
Save