From 8dd6540c63b8210d689c9664933c86284fca1ebb Mon Sep 17 00:00:00 2001 From: Jackson Morgan Date: Sun, 4 May 2025 21:37:34 -0400 Subject: [PATCH] React library now has a hook for link query --- packages/connected/src/index.ts | 17 +++-- .../src/linkTraversal/ResourceLinkQuery.ts | 5 ++ packages/connected/src/types/ILinkQuery.ts | 3 +- packages/react/src/createLdoReactMethods.tsx | 3 + packages/react/src/index.ts | 4 +- packages/react/src/methods/useLinkQuery.ts | 51 +++++++++++++-- .../react/src/util/TrackingProxyContext.ts | 61 ------------------ packages/react/src/util/TrackingSetProxy.ts | 62 ------------------- .../react/src/util/TrackingSubjectProxy.ts | 49 --------------- packages/react/src/util/useTrackingProxy.ts | 26 +------- 10 files changed, 73 insertions(+), 208 deletions(-) delete mode 100644 packages/react/src/util/TrackingProxyContext.ts delete mode 100644 packages/react/src/util/TrackingSetProxy.ts delete mode 100644 packages/react/src/util/TrackingSubjectProxy.ts diff --git a/packages/connected/src/index.ts b/packages/connected/src/index.ts index b8cec59..d50f77e 100644 --- a/packages/connected/src/index.ts +++ b/packages/connected/src/index.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"; diff --git a/packages/connected/src/linkTraversal/ResourceLinkQuery.ts b/packages/connected/src/linkTraversal/ResourceLinkQuery.ts index 3e96b23..9d349a2 100644 --- a/packages/connected/src/linkTraversal/ResourceLinkQuery.ts +++ b/packages/connected/src/linkTraversal/ResourceLinkQuery.ts @@ -180,6 +180,11 @@ export class ResourceLinkQuery< } } + async unsubscribeAll() { + this.thisUnsubscribeIds.clear(); + await this.fullUnsubscribe(); + } + fromSubject(): ExpandDeep> { return this.ldoBuilder.fromSubject( this.startingSubject, diff --git a/packages/connected/src/types/ILinkQuery.ts b/packages/connected/src/types/ILinkQuery.ts index ec4970e..1cfc189 100644 --- a/packages/connected/src/types/ILinkQuery.ts +++ b/packages/connected/src/types/ILinkQuery.ts @@ -91,7 +91,8 @@ export interface ILinkQuery> { options?: LinkQueryRunOptions, ): Promise>>; subscribe(): Promise; - unsubscribe(subscriptionId: string): void; + unsubscribe(subscriptionId: string): Promise; + unsubscribeAll(): Promise; fromSubject(): ExpandDeep>; } diff --git a/packages/react/src/createLdoReactMethods.tsx b/packages/react/src/createLdoReactMethods.tsx index eee6e21..6747a82 100644 --- a/packages/react/src/createLdoReactMethods.tsx +++ b/packages/react/src/createLdoReactMethods.tsx @@ -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), }; } diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 05d1868..ca9d962 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -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"; diff --git a/packages/react/src/methods/useLinkQuery.ts b/packages/react/src/methods/useLinkQuery.ts index fa92ee1..a3d9dc4 100644 --- a/packages/react/src/methods/useLinkQuery.ts +++ b/packages/react/src/methods/useLinkQuery.ts @@ -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( +export function createUseLinkQuery( dataset: ConnectedLdoDataset, ) { /** @@ -23,5 +31,38 @@ export function createUseQueryLink( startingResource: string, startingSubject: SubjectNode | string, linkQuery: QueryInput, - ): ExpandDeep> | undefined {}; + ): ExpandDeep> | undefined { + const linkQueryRef = useRef< + ResourceLinkQuery | 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) => { + if (!startingSubject) return; + return builder.fromSubject(startingSubject); + }, + [startingSubject], + ); + + const linkedDataObject = useTrackingProxy(shapeType, fromSubject, dataset); + + return isLoading + ? undefined + : (linkedDataObject as unknown as ExpandDeep>); + }; } diff --git a/packages/react/src/util/TrackingProxyContext.ts b/packages/react/src/util/TrackingProxyContext.ts deleted file mode 100644 index 3bbd84a..0000000 --- a/packages/react/src/util/TrackingProxyContext.ts +++ /dev/null @@ -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; -} - -/** - * @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; - - 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, - ); - } -} diff --git a/packages/react/src/util/TrackingSetProxy.ts b/packages/react/src/util/TrackingSetProxy.ts deleted file mode 100644 index 5f141f5..0000000 --- a/packages/react/src/util/TrackingSetProxy.ts +++ /dev/null @@ -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(["add", "clear", "delete"]); diff --git a/packages/react/src/util/TrackingSubjectProxy.ts b/packages/react/src/util/TrackingSubjectProxy.ts deleted file mode 100644 index 74154bf..0000000 --- a/packages/react/src/util/TrackingSubjectProxy.ts +++ /dev/null @@ -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["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; -} diff --git a/packages/react/src/util/useTrackingProxy.ts b/packages/react/src/util/useTrackingProxy.ts index 33d39e1..d0c993a 100644 --- a/packages/react/src/util/useTrackingProxy.ts +++ b/packages/react/src/util/useTrackingProxy.ts @@ -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( 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]);