Before slight refactor to have dataset provided by context

main
Jackson Morgan 6 months ago
parent 39388bfe83
commit 108b1d677b
  1. 7
      packages/react/src/createLdoReactMethods.ts
  2. 5
      packages/solid-react/package.json
  3. 34
      packages/solid-react/src/BrowserSolidLdoProvider.tsx
  4. 55
      packages/solid-react/src/SolidLdoProvider.tsx
  5. 3
      packages/solid-react/src/UnauthenticatedSolidLdoProvider.tsx
  6. 15
      packages/solid-react/src/defaultIntance.ts
  7. 7
      packages/solid-react/src/index.ts
  8. 84
      packages/solid-react/src/useLdoMethods.ts
  9. 21
      packages/solid-react/src/useMatchObject.ts
  10. 21
      packages/solid-react/src/useMatchSubject.ts
  11. 114
      packages/solid-react/src/useResource.ts
  12. 30
      packages/solid-react/src/useSubject.ts
  13. 52
      packages/solid-react/src/useSubscribeToResource.ts
  14. 61
      packages/solid-react/src/util/TrackingProxyContext.ts
  15. 56
      packages/solid-react/src/util/TrackingSetProxy.ts
  16. 43
      packages/solid-react/src/util/TrackingSubjectProxy.ts
  17. 55
      packages/solid-react/src/util/useTrackingProxy.ts

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createUseLdo } from "./methods/useLdo";
import {
createConnectedLdoDataset,
@ -9,9 +10,9 @@ import { createUseResource } from "./methods/useResource";
import { createUseSubject } from "./methods/useSubject";
import { createUseSubscribeToResource } from "./methods/useSubscribeToResource";
export function createLdoReactMethods<Plugins extends ConnectedPlugin[]>(
plugins: Plugins,
) {
export function createLdoReactMethods<
Plugins extends ConnectedPlugin<any, any, any, any>[],
>(plugins: Plugins) {
const dataset = createConnectedLdoDataset(plugins);
dataset.setMaxListeners(1000);

@ -37,11 +37,8 @@
},
"dependencies": {
"@inrupt/solid-client-authn-browser": "^2.0.0",
"@ldo/dataset": "^1.0.0-alpha.1",
"@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.1",
"@ldo/ldo": "^1.0.0-alpha.1",
"@ldo/connected": "^1.0.0-alpha.1",
"@ldo/subscribable-dataset": "^1.0.0-alpha.1",
"@ldo/connected-solid": "^1.0.0-alpha.1",
"@rdfjs/data-model": "^1.2.0",
"cross-fetch": "^3.1.6"
},

@ -9,14 +9,22 @@ import {
logout as libraryLogout,
fetch as libraryFetch,
} from "@inrupt/solid-client-authn-browser";
import { SolidLdoProvider } from "./SolidLdoProvider";
import type { ConnectedLdoDataset } from "@ldo/connected";
import type { SolidConnectedPlugin } from "@ldo/connected-solid";
const PRE_REDIRECT_URI = "PRE_REDIRECT_URI";
export const BrowserSolidLdoProvider: FunctionComponent<PropsWithChildren> = ({
export function createBrowserSolidLdoProvider(
dataset: ConnectedLdoDataset<(SolidConnectedPlugin | SolidConnectedPlugin)[]>,
) {
dataset.setContext("solid", { fetch: libraryFetch });
const BrowserSolidLdoProvider: FunctionComponent<PropsWithChildren> = ({
children,
}) => {
const [session, setSession] = useState<SessionInfo>(getDefaultSession().info);
const [session, setSession] = useState<SessionInfo>(
getDefaultSession().info,
);
const [ranInitialAuthCheck, setRanInitialAuthCheck] = useState(false);
const runInitialAuthCheck = useCallback(async () => {
@ -41,7 +49,8 @@ export const BrowserSolidLdoProvider: FunctionComponent<PropsWithChildren> = ({
}, 0);
}, []);
const login = useCallback(async (issuer: string, options?: LoginOptions) => {
const login = useCallback(
async (issuer: string, options?: LoginOptions) => {
const cleanUrl = new URL(window.location.href);
cleanUrl.hash = "";
cleanUrl.search = "";
@ -54,7 +63,9 @@ export const BrowserSolidLdoProvider: FunctionComponent<PropsWithChildren> = ({
window.localStorage.setItem(PRE_REDIRECT_URI, window.location.href);
await libraryLogin(fullOptions);
setSession({ ...getDefaultSession().info });
}, []);
},
[],
);
const logout = useCallback(async () => {
await libraryLogout();
@ -86,12 +97,21 @@ export const BrowserSolidLdoProvider: FunctionComponent<PropsWithChildren> = ({
ranInitialAuthCheck,
fetch: libraryFetch,
}),
[login, logout, ranInitialAuthCheck, runInitialAuthCheck, session, signUp],
[
login,
logout,
ranInitialAuthCheck,
runInitialAuthCheck,
session,
signUp,
],
);
return (
<SolidAuthContext.Provider value={solidAuthFunctions}>
<SolidLdoProvider>{children}</SolidLdoProvider>
{children}
</SolidAuthContext.Provider>
);
};
return BrowserSolidLdoProvider;
}

@ -1,55 +0,0 @@
import React, { createContext, useContext } from "react";
import {
useMemo,
type FunctionComponent,
type PropsWithChildren,
useEffect,
} from "react";
import { useSolidAuth } from "./SolidAuthContext";
import type { SolidLdoDataset } from "@ldo/solid";
import { createSolidLdoDataset } from "@ldo/solid";
import type { UseLdoMethods } from "./useLdoMethods";
import { createUseLdoMethods } from "./useLdoMethods";
export const SolidLdoReactContext =
// This will be set in the provider
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
createContext<UseLdoMethods>(undefined);
export function useLdo(): UseLdoMethods {
return useContext(SolidLdoReactContext);
}
export interface SolidLdoProviderProps extends PropsWithChildren {}
export const SolidLdoProvider: FunctionComponent<SolidLdoProviderProps> = ({
children,
}) => {
const { fetch } = useSolidAuth();
// Initialize storeDependencies before render
const solidLdoDataset: SolidLdoDataset = useMemo(() => {
const ldoDataset = createSolidLdoDataset({
fetch,
});
ldoDataset.setMaxListeners(1000);
return ldoDataset;
}, []);
// Keep context in sync with props
useEffect(() => {
solidLdoDataset.context.fetch = fetch;
}, [fetch]);
const value: UseLdoMethods = useMemo(
() => createUseLdoMethods(solidLdoDataset),
[solidLdoDataset],
);
return (
<SolidLdoReactContext.Provider value={value}>
{children}
</SolidLdoReactContext.Provider>
);
};

@ -4,7 +4,6 @@ import type { FunctionComponent, PropsWithChildren } from "react";
import type { LoginOptions, SessionInfo } from "./SolidAuthContext";
import { SolidAuthContext } from "./SolidAuthContext";
import libraryFetch from "cross-fetch";
import { SolidLdoProvider } from "./SolidLdoProvider";
const DUMMY_SESSION: SessionInfo = {
isLoggedIn: false,
@ -56,7 +55,7 @@ export const UnauthenticatedSolidLdoProvider: FunctionComponent<
return (
<SolidAuthContext.Provider value={solidAuthFunctions}>
<SolidLdoProvider>{children}</SolidLdoProvider>
{children}
</SolidAuthContext.Provider>
);
};

@ -0,0 +1,15 @@
import { solidConnectedPlugin } from "@ldo/connected-solid";
import { createLdoReactMethods } from "@ldo/react";
import { createBrowserSolidLdoProvider } from "./BrowserSolidLdoProvider";
export const {
dataset,
useLdo,
useMatchObject,
useMatchSubject,
useResource,
useSubject,
useSubscribeToResource,
} = createLdoReactMethods([solidConnectedPlugin]);
export const BrowserSolidLdoProvider = createBrowserSolidLdoProvider(dataset);

@ -1,12 +1,7 @@
export * from "./BrowserSolidLdoProvider";
export * from "./UnauthenticatedSolidLdoProvider";
export * from "./SolidAuthContext";
export { useLdo } from "./SolidLdoProvider";
export * from "./defaultIntance";
// hooks
export * from "./useResource";
export * from "./useSubject";
export * from "./useMatchSubject";
export * from "./useMatchObject";
export * from "./useRootContainer";

@ -1,84 +0,0 @@
import type { LdoBase, ShapeType } from "@ldo/ldo";
import type { SubjectNode } from "@ldo/rdf-utils";
import type {
Resource,
SolidLdoDataset,
SolidLdoTransactionDataset,
} from "@ldo/solid";
import { changeData, commitData } from "@ldo/solid";
export interface UseLdoMethods {
dataset: SolidLdoDataset;
getResource: SolidLdoDataset["getResource"];
getSubject<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
): Type;
createData<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
resource: Resource,
...additionalResources: Resource[]
): Type;
changeData<Type extends LdoBase>(
input: Type,
resource: Resource,
...additionalResources: Resource[]
): Type;
commitData(
input: LdoBase,
): ReturnType<SolidLdoTransactionDataset["commitToPod"]>;
}
export function createUseLdoMethods(dataset: SolidLdoDataset): UseLdoMethods {
return {
dataset: dataset,
/**
* Gets a resource
*/
getResource: dataset.getResource.bind(dataset),
/**
* Returns a Linked Data Object for a subject
* @param shapeType The shape type for the data
* @param subject Subject Node
* @returns A Linked Data Object
*/
getSubject<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
): Type {
return dataset.usingType(shapeType).fromSubject(subject);
},
/**
* Begins tracking changes to eventually commit for a new subject
* @param shapeType The shape type that defines the created data
* @param subject The RDF subject for a Linked Data Object
* @param resources Any number of resources to which this data should be written
* @returns A Linked Data Object to modify and commit
*/
createData<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
resource: Resource,
...additionalResources: Resource[]
): Type {
return dataset.createData(
shapeType,
subject,
resource,
...additionalResources,
);
},
/**
* Begins tracking changes to eventually commit
* @param input A linked data object to track changes on
* @param resources
*/
changeData: changeData,
/**
* Commits the transaction to the global dataset, syncing all subscribing
* components and Solid Pods
*/
commitData: commitData,
};
}

@ -1,21 +0,0 @@
import type { LdoBase, LdSet, ShapeType } from "@ldo/ldo";
import type { QuadMatch } from "@ldo/rdf-utils";
import type { LdoBuilder } from "@ldo/ldo";
import { useCallback } from "react";
import { useTrackingProxy } from "./util/useTrackingProxy";
export function useMatchObject<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject?: QuadMatch[0] | string,
predicate?: QuadMatch[1] | string,
graph?: QuadMatch[3] | string,
): LdSet<Type> {
const matchObject = useCallback(
(builder: LdoBuilder<Type>) => {
return builder.matchObject(subject, predicate, graph);
},
[subject, predicate, graph],
);
return useTrackingProxy(shapeType, matchObject);
}

@ -1,21 +0,0 @@
import type { LdoBase, LdSet, ShapeType } from "@ldo/ldo";
import type { QuadMatch } from "@ldo/rdf-utils";
import type { LdoBuilder } from "@ldo/ldo";
import { useCallback } from "react";
import { useTrackingProxy } from "./util/useTrackingProxy";
export function useMatchSubject<Type extends LdoBase>(
shapeType: ShapeType<Type>,
predicate?: QuadMatch[1] | string,
object?: QuadMatch[2] | string,
graph?: QuadMatch[3] | string,
): LdSet<Type> {
const matchSubject = useCallback(
(builder: LdoBuilder<Type>) => {
return builder.matchSubject(predicate, object, graph);
},
[predicate, object, graph],
);
return useTrackingProxy(shapeType, matchSubject);
}

@ -1,114 +0,0 @@
import { useMemo, useEffect, useRef, useState, useCallback } from "react";
import type {
Container,
ContainerUri,
LeafUri,
Resource,
Leaf,
} from "@ldo/solid";
import { useLdo } from "./SolidLdoProvider";
export interface UseResourceOptions {
suppressInitialRead?: boolean;
reloadOnMount?: boolean;
subscribe?: boolean;
}
export function useResource(
uri: ContainerUri,
options?: UseResourceOptions,
): Container;
export function useResource(uri: LeafUri, options?: UseResourceOptions): Leaf;
export function useResource(
uri: string,
options?: UseResourceOptions,
): Leaf | Container;
export function useResource(
uri?: ContainerUri,
options?: UseResourceOptions,
): Container | undefined;
export function useResource(
uri?: LeafUri,
options?: UseResourceOptions,
): Leaf | undefined;
export function useResource(
uri?: string,
options?: UseResourceOptions,
): Leaf | Container | undefined;
export function useResource(
uri?: string,
options?: UseResourceOptions,
): Leaf | Container | undefined {
const { getResource } = useLdo();
const subscriptionIdRef = useRef<string | undefined>();
// Get the resource
const resource = useMemo(() => {
if (uri) {
const resource = getResource(uri);
// Run read operations if necissary
if (!options?.suppressInitialRead) {
if (options?.reloadOnMount) {
resource.read();
} else {
resource.readIfUnfetched();
}
}
return resource;
}
return undefined;
}, [getResource, uri]);
const [resourceRepresentation, setResourceRepresentation] =
useState(resource);
const pastResource = useRef<
{ resource?: Resource; callback: () => void } | undefined
>();
useEffect(() => {
if (options?.subscribe) {
resource
?.subscribeToNotifications()
.then((subscriptionId) => (subscriptionIdRef.current = subscriptionId));
} else if (subscriptionIdRef.current) {
resource?.unsubscribeFromNotifications(subscriptionIdRef.current);
}
return () => {
if (subscriptionIdRef.current)
resource?.unsubscribeFromNotifications(subscriptionIdRef.current);
};
}, [resource, options?.subscribe]);
// Callback function to force the react dom to reload.
const forceReload = useCallback(
// Wrap the resource in a proxy so it's techically a different object
() => {
if (resource) setResourceRepresentation(new Proxy(resource, {}));
},
[resource],
);
useEffect(() => {
// Remove listeners for the previous resource
if (pastResource.current?.resource) {
pastResource.current.resource.off(
"update",
pastResource.current.callback,
);
}
// Set a new past resource to the current resource
pastResource.current = { resource, callback: forceReload };
if (resource) {
// Add listener
resource.on("update", forceReload);
setResourceRepresentation(new Proxy(resource, {}));
// Unsubscribe on unmount
return () => {
resource.off("update", forceReload);
};
} else {
setResourceRepresentation(undefined);
}
}, [resource]);
return resourceRepresentation;
}

@ -1,30 +0,0 @@
import type { SubjectNode } from "@ldo/rdf-utils";
import type { ShapeType } from "@ldo/ldo";
import type { LdoBuilder } from "@ldo/ldo";
import type { LdoBase } from "@ldo/ldo";
import { useCallback } from "react";
import { useTrackingProxy } from "./util/useTrackingProxy";
export function useSubject<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject: string | SubjectNode,
): Type;
export function useSubject<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject?: string | SubjectNode,
): Type | undefined;
export function useSubject<Type extends LdoBase>(
shapeType: ShapeType<Type>,
subject?: string | SubjectNode,
): Type | undefined {
const fromSubject = useCallback(
(builder: LdoBuilder<Type>) => {
if (!subject) return;
return builder.fromSubject(subject);
},
[subject],
);
return useTrackingProxy(shapeType, fromSubject);
}

@ -1,52 +0,0 @@
import { useLdo } from "./SolidLdoProvider";
import { useEffect, useRef } from "react";
export function useSubscribeToResource(...uris: string[]): void {
const { dataset } = useLdo();
const currentlySubscribed = useRef<Record<string, string>>({});
useEffect(() => {
const resources = uris.map((uri) => dataset.getResource(uri));
const previousSubscriptions = { ...currentlySubscribed.current };
Promise.all<void>(
resources.map(async (resource) => {
if (!previousSubscriptions[resource.uri]) {
// Prevent multiple triggers from created subscriptions while waiting
// for connection
currentlySubscribed.current[resource.uri] = "AWAITING";
// Read and subscribe
await resource.readIfUnfetched();
currentlySubscribed.current[resource.uri] =
await resource.subscribeToNotifications();
} else {
delete previousSubscriptions[resource.uri];
}
}),
).then(async () => {
// Unsubscribe from all remaining previous subscriptions
await Promise.all(
Object.entries(previousSubscriptions).map(
async ([resourceUri, subscriptionId]) => {
// Unsubscribe
delete currentlySubscribed.current[resourceUri];
const resource = dataset.getResource(resourceUri);
await resource.unsubscribeFromNotifications(subscriptionId);
},
),
);
});
}, [uris]);
// Cleanup Subscriptions
useEffect(() => {
return () => {
Promise.all(
Object.entries(currentlySubscribed.current).map(
async ([resourceUri, subscriptionId]) => {
const resource = dataset.getResource(resourceUri);
await resource.unsubscribeFromNotifications(subscriptionId);
},
),
);
};
}, []);
}

@ -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,56 +0,0 @@
import { createNewSetProxy, type SetProxy } from "@ldo/jsonld-dataset-proxy";
import type { TrackingProxyContext } from "./TrackingProxyContext";
import type { QuadMatch } from "@ldo/rdf-utils";
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,43 +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";
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,55 +0,0 @@
import {
ContextUtil,
JsonldDatasetProxyBuilder,
} from "@ldo/jsonld-dataset-proxy";
import { LdoBuilder } from "@ldo/ldo";
import type { LdoBase, ShapeType } from "@ldo/ldo";
import { useCallback, useEffect, useMemo, useState } from "react";
import { TrackingProxyContext } from "./TrackingProxyContext";
import { defaultGraph } from "@rdfjs/data-model";
import { useLdo } from "../SolidLdoProvider";
export function useTrackingProxy<Type extends LdoBase, ReturnType>(
shapeType: ShapeType<Type>,
createLdo: (builder: LdoBuilder<Type>) => ReturnType,
): ReturnType {
const { dataset } = useLdo();
const [forceUpdateCounter, setForceUpdateCounter] = useState(0);
const forceUpdate = useCallback(
() => setForceUpdateCounter((val) => val + 1),
[],
);
// The main linked data object
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,
);
return createLdo(builder);
}, [shapeType, dataset, forceUpdateCounter, forceUpdate, createLdo]);
useEffect(() => {
// Unregister force update listener upon unmount
return () => {
dataset.removeListenerFromAllEvents(forceUpdate);
};
}, [shapeType]);
return linkedDataObject;
}
Loading…
Cancel
Save