diff --git a/package-lock.json b/package-lock.json index c7913d0..e1427fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4632,6 +4632,10 @@ "resolved": "packages/rdf-utils", "link": true }, + "node_modules/@ldo/react": { + "resolved": "packages/react", + "link": true + }, "node_modules/@ldo/schema-converter-shex": { "resolved": "packages/schema-converter-shex", "link": true @@ -21375,6 +21379,88 @@ "node": ">=4.2.0" } }, + "packages/react": { + "name": "@ldo/react", + "version": "1.0.0-alpha.1", + "license": "MIT", + "dependencies": { + "@ldo/connected": "^1.0.0-alpha.1", + "@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.1", + "@ldo/ldo": "^1.0.0-alpha.1", + "@ldo/rdf-utils": "^1.0.0-alpha.1", + "@ldo/subscribable-dataset": "^1.0.0-alpha.1", + "@rdfjs/data-model": "^1.2.0", + "cross-fetch": "^3.1.6" + }, + "devDependencies": { + "@rdfjs/types": "^1.0.1", + "@testing-library/react": "^14.1.2", + "@types/jest": "^27.0.3", + "jest-environment-jsdom": "^27.0.0", + "start-server-and-test": "^2.0.3", + "ts-jest": "^27.1.2", + "ts-node": "^10.9.2" + } + }, + "packages/react/node_modules/ts-jest": { + "version": "27.1.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.5.tgz", + "integrity": "sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^27.0.0", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "7.x", + "yargs-parser": "20.x" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@types/jest": "^27.0.0", + "babel-jest": ">=27.0.0 <28", + "jest": "^27.0.0", + "typescript": ">=3.8 <5.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "packages/react/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "packages/schema-converter-shex": { "name": "@ldo/schema-converter-shex", "version": "1.0.0-alpha.1", @@ -21525,10 +21611,10 @@ "license": "MIT", "dependencies": { "@inrupt/solid-client-authn-browser": "^2.0.0", + "@ldo/connected": "^1.0.0-alpha.1", "@ldo/dataset": "^1.0.0-alpha.1", "@ldo/jsonld-dataset-proxy": "^1.0.0-alpha.1", "@ldo/ldo": "^1.0.0-alpha.1", - "@ldo/solid": "^1.0.0-alpha.1", "@ldo/subscribable-dataset": "^1.0.0-alpha.1", "@rdfjs/data-model": "^1.2.0", "cross-fetch": "^3.1.6" diff --git a/packages/connected/src/ConnectedLdoDataset.ts b/packages/connected/src/ConnectedLdoDataset.ts index f6d5760..5cfae20 100644 --- a/packages/connected/src/ConnectedLdoDataset.ts +++ b/packages/connected/src/ConnectedLdoDataset.ts @@ -7,12 +7,11 @@ import type { ITransactionDatasetFactory } from "@ldo/subscribable-dataset"; import { InvalidIdentifierResource } from "./InvalidIdentifierResource"; import type { ConnectedContext } from "./ConnectedContext"; import type { + GetResourceReturnType, IConnectedLdoDataset, - ReturnTypeFromArgs, } from "./IConnectedLdoDataset"; import { ConnectedLdoTransactionDataset } from "./ConnectedLdoTransactionDataset"; import type { SubjectNode } from "@ldo/rdf-utils"; -import type { Resource } from "./Resource"; export class ConnectedLdoDataset< Plugins extends ConnectedPlugin[], @@ -69,12 +68,7 @@ export class ConnectedLdoDataset< Name extends Plugins[number]["name"], Plugin extends Extract, UriType extends string, - >( - uri: UriType, - pluginName?: Name, - ): UriType extends Plugin["types"]["uri"] - ? ReturnTypeFromArgs - : ReturnType | InvalidIdentifierResource { + >(uri: UriType, pluginName?: Name): GetResourceReturnType { // Check for which plugins this uri is valid const validPlugins = this.plugins .filter((plugin) => plugin.isUriValid(uri)) @@ -129,8 +123,8 @@ export class ConnectedLdoDataset< createData( shapeType: ShapeType, subject: string | SubjectNode, - resource: Resource, - ...additionalResources: Resource[] + resource: Plugins[number]["types"]["resource"], + ...additionalResources: Plugins[number]["types"]["resource"][] ): Type { const resources = [resource, ...additionalResources]; const linkedDataObject = this.usingType(shapeType) diff --git a/packages/connected/src/ConnectedLdoTransactionDataset.ts b/packages/connected/src/ConnectedLdoTransactionDataset.ts index 7b7292d..b1cf8bb 100644 --- a/packages/connected/src/ConnectedLdoTransactionDataset.ts +++ b/packages/connected/src/ConnectedLdoTransactionDataset.ts @@ -8,10 +8,9 @@ import { import type { DatasetChanges, GraphNode } from "@ldo/rdf-utils"; import type { ConnectedPlugin } from "./ConnectedPlugin"; import type { ConnectedContext } from "./ConnectedContext"; -import type { InvalidIdentifierResource } from "./InvalidIdentifierResource"; import type { + GetResourceReturnType, IConnectedLdoDataset, - ReturnTypeFromArgs, } from "./IConnectedLdoDataset"; import { splitChangesByGraph } from "./util/splitChangesByGraph"; import type { IgnoredInvalidUpdateSuccess } from "./results/success/UpdateSuccess"; @@ -88,12 +87,7 @@ export class ConnectedLdoTransactionDataset Name extends Plugins[number]["name"], Plugin extends Extract, UriType extends string, - >( - uri: UriType, - pluginName?: Name, - ): UriType extends Plugin["types"]["uri"] - ? ReturnTypeFromArgs - : ReturnType | InvalidIdentifierResource { + >(uri: UriType, pluginName?: Name): GetResourceReturnType { return this.context.dataset.getResource(uri, pluginName); } diff --git a/packages/connected/src/IConnectedLdoDataset.ts b/packages/connected/src/IConnectedLdoDataset.ts index 77a68bf..81dcb9b 100644 --- a/packages/connected/src/IConnectedLdoDataset.ts +++ b/packages/connected/src/IConnectedLdoDataset.ts @@ -10,6 +10,13 @@ export type ReturnTypeFromArgs = Func extends ( ? R : never; +export type GetResourceReturnType< + Plugin extends ConnectedPlugin, + UriType extends string, +> = UriType extends Plugin["types"]["uri"] + ? ReturnTypeFromArgs + : ReturnType | InvalidIdentifierResource; + export interface IConnectedLdoDataset extends LdoDataset { /** @@ -36,9 +43,7 @@ export interface IConnectedLdoDataset >( uri: UriType, pluginName?: Name, - ): UriType extends Plugin["types"]["uri"] - ? ReturnTypeFromArgs - : ReturnType | InvalidIdentifierResource; + ): GetResourceReturnType; createResource< Name extends Plugins[number]["name"], diff --git a/packages/connected/src/InvalidIdentifierResource.ts b/packages/connected/src/InvalidIdentifierResource.ts index fca53eb..dd63bc5 100644 --- a/packages/connected/src/InvalidIdentifierResource.ts +++ b/packages/connected/src/InvalidIdentifierResource.ts @@ -47,7 +47,10 @@ export class InvalidIdentifierResource async update(): Promise> { return this.status; } - async subscribeToNotifications(_callbacks): Promise { + async subscribeToNotifications(_callbacks?: { + onNotification: (message: unknown) => void; + onNotificationError: (err: Error) => void; + }): Promise { throw new Error("Cannot subscribe to an invalid resource."); } async unsubscribeFromNotifications(_subscriptionId: string): Promise { diff --git a/packages/connected/src/createConntectedLdoDataset.ts b/packages/connected/src/createConntectedLdoDataset.ts new file mode 100644 index 0000000..c7440d3 --- /dev/null +++ b/packages/connected/src/createConntectedLdoDataset.ts @@ -0,0 +1,14 @@ +import { createDatasetFactory } from "@ldo/dataset"; +import { ConnectedLdoDataset } from "./ConnectedLdoDataset"; +import type { ConnectedPlugin } from "./ConnectedPlugin"; +import { createTransactionDatasetFactory } from "@ldo/subscribable-dataset"; + +export function createConnectedLdoDataset( + plugins: Plugins, +): ConnectedLdoDataset { + return new ConnectedLdoDataset( + plugins, + createDatasetFactory(), + createTransactionDatasetFactory(), + ); +} diff --git a/packages/connected/src/index.ts b/packages/connected/src/index.ts index b327d3b..0ba6d2c 100644 --- a/packages/connected/src/index.ts +++ b/packages/connected/src/index.ts @@ -6,6 +6,7 @@ export * from "./Resource"; export * from "./InvalidIdentifierResource"; export * from "./ConnectedContext"; export * from "./methods"; +export * from "./createConntectedLdoDataset"; export * from "./util/splitChangesByGraph"; diff --git a/packages/react/package.json b/packages/react/package.json index 51dddff..139ecfa 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,7 +1,7 @@ { "name": "@ldo/react", "version": "1.0.0-alpha.1", - "description": "A React library for LDO and Solid", + "description": "A React library for LDO.", "main": "dist/index.js", "scripts": { "build": "tsc --project tsconfig.build.json", @@ -26,7 +26,6 @@ }, "homepage": "https://github.com/o-development/ldobjects/tree/main/packages/solid-react#readme", "devDependencies": { - "@ldo/rdf-utils": "^1.0.0-alpha.1", "@rdfjs/types": "^1.0.1", "@testing-library/react": "^14.1.2", "@types/jest": "^27.0.3", @@ -36,11 +35,10 @@ "ts-node": "^10.9.2" }, "dependencies": { - "@inrupt/solid-client-authn-browser": "^2.0.0", - "@ldo/dataset": "^1.0.0-alpha.1", + "@ldo/connected": "^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/rdf-utils": "^1.0.0-alpha.1", "@ldo/subscribable-dataset": "^1.0.0-alpha.1", "@rdfjs/data-model": "^1.2.0", "cross-fetch": "^3.1.6" diff --git a/packages/react/src/BrowserSolidLdoProvider.tsx b/packages/react/src/BrowserSolidLdoProvider.tsx deleted file mode 100644 index 6213890..0000000 --- a/packages/react/src/BrowserSolidLdoProvider.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import type { FunctionComponent, PropsWithChildren } from "react"; -import type { LoginOptions, SessionInfo } from "./SolidAuthContext"; -import { SolidAuthContext } from "./SolidAuthContext"; -import { - getDefaultSession, - handleIncomingRedirect, - login as libraryLogin, - logout as libraryLogout, - fetch as libraryFetch, -} from "@inrupt/solid-client-authn-browser"; -import { SolidLdoProvider } from "./SolidLdoProvider"; - -const PRE_REDIRECT_URI = "PRE_REDIRECT_URI"; - -export const BrowserSolidLdoProvider: FunctionComponent = ({ - children, -}) => { - const [session, setSession] = useState(getDefaultSession().info); - const [ranInitialAuthCheck, setRanInitialAuthCheck] = useState(false); - - const runInitialAuthCheck = useCallback(async () => { - if (!window.localStorage.getItem(PRE_REDIRECT_URI)) { - window.localStorage.setItem(PRE_REDIRECT_URI, window.location.href); - } - - await handleIncomingRedirect({ - restorePreviousSession: true, - }); - // Set timout to ensure this happens after the redirect - setTimeout(() => { - setSession({ ...getDefaultSession().info }); - window.history.replaceState( - {}, - "", - window.localStorage.getItem(PRE_REDIRECT_URI), - ); - window.localStorage.removeItem(PRE_REDIRECT_URI); - - setRanInitialAuthCheck(true); - }, 0); - }, []); - - const login = useCallback(async (issuer: string, options?: LoginOptions) => { - const cleanUrl = new URL(window.location.href); - cleanUrl.hash = ""; - cleanUrl.search = ""; - const fullOptions = { - redirectUrl: cleanUrl.toString(), - clientName: "Solid App", - oidcIssuer: issuer, - ...options, - }; - window.localStorage.setItem(PRE_REDIRECT_URI, window.location.href); - await libraryLogin(fullOptions); - setSession({ ...getDefaultSession().info }); - }, []); - - const logout = useCallback(async () => { - await libraryLogout(); - setSession({ ...getDefaultSession().info }); - }, []); - - const signUp = useCallback( - async (issuer: string, options?: LoginOptions) => { - // The typings on @inrupt/solid-client-authn-core have not yet been updated - // TODO: remove this ts-ignore when they are updated. - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return login(issuer, { ...options, prompt: "create" }); - }, - [login], - ); - - useEffect(() => { - runInitialAuthCheck(); - }, []); - - const solidAuthFunctions = useMemo( - () => ({ - runInitialAuthCheck, - login, - logout, - signUp, - session, - ranInitialAuthCheck, - fetch: libraryFetch, - }), - [login, logout, ranInitialAuthCheck, runInitialAuthCheck, session, signUp], - ); - - return ( - - {children} - - ); -}; diff --git a/packages/react/src/SolidAuthContext.ts b/packages/react/src/SolidAuthContext.ts deleted file mode 100644 index 4620b21..0000000 --- a/packages/react/src/SolidAuthContext.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { - ISessionInfo, - ILoginInputOptions, -} from "@inrupt/solid-client-authn-core"; -import { createContext, useContext } from "react"; - -export type SessionInfo = ISessionInfo; -export type LoginOptions = ILoginInputOptions; - -export interface SolidAuthFunctions { - login: (issuer: string, loginOptions?: LoginOptions) => Promise; - logout: () => Promise; - signUp: (issuer: string, loginOptions?: LoginOptions) => Promise; - fetch: typeof fetch; - session: SessionInfo; - ranInitialAuthCheck: boolean; -} - -// There is no initial value for this context. It will be given in the provider -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -export const SolidAuthContext = createContext(undefined); - -export function useSolidAuth(): SolidAuthFunctions { - return useContext(SolidAuthContext); -} diff --git a/packages/react/src/SolidLdoProvider.tsx b/packages/react/src/SolidLdoProvider.tsx deleted file mode 100644 index a0c9664..0000000 --- a/packages/react/src/SolidLdoProvider.tsx +++ /dev/null @@ -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(undefined); - -export function useLdo(): UseLdoMethods { - return useContext(SolidLdoReactContext); -} - -export interface SolidLdoProviderProps extends PropsWithChildren {} - -export const SolidLdoProvider: FunctionComponent = ({ - 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 ( - - {children} - - ); -}; diff --git a/packages/react/src/UnauthenticatedSolidLdoProvider.tsx b/packages/react/src/UnauthenticatedSolidLdoProvider.tsx deleted file mode 100644 index 8f25de1..0000000 --- a/packages/react/src/UnauthenticatedSolidLdoProvider.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* istanbul ignore file */ -import React, { useCallback, useMemo } from "react"; -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, - webId: undefined, - clientAppId: undefined, - sessionId: "no_session", - expirationDate: undefined, -}; - -export const UnauthenticatedSolidLdoProvider: FunctionComponent< - PropsWithChildren -> = ({ children }) => { - const login = useCallback( - async (_issuer: string, _options?: LoginOptions) => { - throw new Error( - "login is not available for a UnauthenticatedSolidLdoProvider", - ); - }, - [], - ); - - const logout = useCallback(async () => { - throw new Error( - "logout is not available for a UnauthenticatedSolidLdoProvider", - ); - }, []); - - const signUp = useCallback( - async (_issuer: string, _options?: LoginOptions) => { - throw new Error( - "signUp is not available for a UnauthenticatedSolidLdoProvider", - ); - }, - [], - ); - - const solidAuthFunctions = useMemo( - () => ({ - runInitialAuthCheck: () => {}, - login, - logout, - signUp, - session: DUMMY_SESSION, - ranInitialAuthCheck: true, - fetch: libraryFetch, - }), - [login, logout, signUp], - ); - - return ( - - {children} - - ); -}; diff --git a/packages/react/src/createLdoReactMethods.ts b/packages/react/src/createLdoReactMethods.ts new file mode 100644 index 0000000..e84fbdc --- /dev/null +++ b/packages/react/src/createLdoReactMethods.ts @@ -0,0 +1,27 @@ +import { createUseLdo } from "./methods/useLdo"; +import { + createConnectedLdoDataset, + type ConnectedPlugin, +} from "@ldo/connected"; +import { createUseMatchObject } from "./methods/useMatchObject"; +import { createUseMatchSubject } from "./methods/useMatchSubject"; +import { createUseResource } from "./methods/useResource"; +import { createUseSubject } from "./methods/useSubject"; +import { createUseSubscribeToResource } from "./methods/useSubscribeToResource"; + +export function createLdoReactMethods( + plugins: Plugins, +) { + const dataset = createConnectedLdoDataset(plugins); + dataset.setMaxListeners(1000); + + return { + dataset, + useLdo: createUseLdo(dataset), + useMatchObject: createUseMatchObject(dataset), + useMatchSubject: createUseMatchSubject(dataset), + useResource: createUseResource(dataset), + useSubject: createUseSubject(dataset), + useSubscribeToResource: createUseSubscribeToResource(dataset), + }; +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index edbcc74..e2c56af 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,12 +1 @@ -export * from "./BrowserSolidLdoProvider"; -export * from "./UnauthenticatedSolidLdoProvider"; -export * from "./SolidAuthContext"; - -export { useLdo } from "./SolidLdoProvider"; - -// hooks -export * from "./useResource"; -export * from "./useSubject"; -export * from "./useMatchSubject"; -export * from "./useMatchObject"; -export * from "./useRootContainer"; +export * from "./createLdoReactMethods"; diff --git a/packages/react/src/useLdoMethods.ts b/packages/react/src/methods/useLdo.ts similarity index 55% rename from packages/react/src/useLdoMethods.ts rename to packages/react/src/methods/useLdo.ts index fc95869..78a6c79 100644 --- a/packages/react/src/useLdoMethods.ts +++ b/packages/react/src/methods/useLdo.ts @@ -1,15 +1,16 @@ -import type { LdoBase, ShapeType } from "@ldo/ldo"; +import { + changeData, + type ConnectedLdoDataset, + type ConnectedLdoTransactionDataset, + type ConnectedPlugin, +} from "@ldo/connected"; +import { getDataset, type LdoBase, type 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"]; +export interface UseLdoMethods { + dataset: ConnectedLdoDataset; + getResource: ConnectedLdoDataset["getResource"]; + setContext: ConnectedLdoDataset["setContext"]; getSubject( shapeType: ShapeType, subject: string | SubjectNode, @@ -17,26 +18,32 @@ export interface UseLdoMethods { createData( shapeType: ShapeType, subject: string | SubjectNode, - resource: Resource, - ...additionalResources: Resource[] + resource: Plugins[number]["types"]["resource"], + ...additionalResources: Plugins[number]["types"]["resource"][] ): Type; changeData( input: Type, - resource: Resource, - ...additionalResources: Resource[] + resource: Plugins[number]["types"]["resource"], + ...additionalResources: Plugins[number]["types"]["resource"][] ): Type; commitData( input: LdoBase, - ): ReturnType; + ): ReturnType["commitToRemote"]>; } -export function createUseLdoMethods(dataset: SolidLdoDataset): UseLdoMethods { - return { - dataset: dataset, +export function createUseLdo( + dataset: ConnectedLdoDataset, +) { + return (): UseLdoMethods => ({ + dataset, /** * Gets a resource */ getResource: dataset.getResource.bind(dataset), + /** + * Set the context + */ + setContext: dataset.setContext.bind(dataset), /** * Returns a Linked Data Object for a subject * @param shapeType The shape type for the data @@ -59,8 +66,8 @@ export function createUseLdoMethods(dataset: SolidLdoDataset): UseLdoMethods { createData( shapeType: ShapeType, subject: string | SubjectNode, - resource: Resource, - ...additionalResources: Resource[] + resource: Plugins[number]["types"]["resource"], + ...additionalResources: Plugins[number]["types"]["resource"][] ): Type { return dataset.createData( shapeType, @@ -79,6 +86,13 @@ export function createUseLdoMethods(dataset: SolidLdoDataset): UseLdoMethods { * Commits the transaction to the global dataset, syncing all subscribing * components and Solid Pods */ - commitData: commitData, - }; + commitData( + input: LdoBase, + ): ReturnType["commitToRemote"]> { + const inputDataset = getDataset( + input, + ) as ConnectedLdoTransactionDataset; + return inputDataset.commitToRemote(); + }, + }); } diff --git a/packages/react/src/methods/useMatchObject.ts b/packages/react/src/methods/useMatchObject.ts new file mode 100644 index 0000000..a77ff8a --- /dev/null +++ b/packages/react/src/methods/useMatchObject.ts @@ -0,0 +1,26 @@ +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"; +import type { ConnectedLdoDataset, ConnectedPlugin } from "@ldo/connected"; + +export function createUseMatchObject( + dataset: ConnectedLdoDataset, +) { + return function useMatchObject( + shapeType: ShapeType, + subject?: QuadMatch[0] | string, + predicate?: QuadMatch[1] | string, + graph?: QuadMatch[3] | string, + ): LdSet { + const matchObject = useCallback( + (builder: LdoBuilder) => { + return builder.matchObject(subject, predicate, graph); + }, + [subject, predicate, graph], + ); + + return useTrackingProxy(shapeType, matchObject, dataset); + }; +} diff --git a/packages/react/src/methods/useMatchSubject.ts b/packages/react/src/methods/useMatchSubject.ts new file mode 100644 index 0000000..0adac52 --- /dev/null +++ b/packages/react/src/methods/useMatchSubject.ts @@ -0,0 +1,26 @@ +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"; +import type { ConnectedLdoDataset, ConnectedPlugin } from "@ldo/connected"; + +export function createUseMatchSubject( + dataset: ConnectedLdoDataset, +) { + return function useMatchSubject( + shapeType: ShapeType, + predicate?: QuadMatch[1] | string, + object?: QuadMatch[2] | string, + graph?: QuadMatch[3] | string, + ): LdSet { + const matchSubject = useCallback( + (builder: LdoBuilder) => { + return builder.matchSubject(predicate, object, graph); + }, + [predicate, object, graph], + ); + + return useTrackingProxy(shapeType, matchSubject, dataset); + }; +} diff --git a/packages/react/src/methods/useResource.ts b/packages/react/src/methods/useResource.ts new file mode 100644 index 0000000..f6d0cf3 --- /dev/null +++ b/packages/react/src/methods/useResource.ts @@ -0,0 +1,122 @@ +import { useMemo, useEffect, useRef, useState, useCallback } from "react"; +import type { + ConnectedLdoDataset, + ConnectedPlugin, + GetResourceReturnType, + Resource, +} from "@ldo/connected"; + +export interface UseResourceOptions { + pluginName?: Name; + suppressInitialRead?: boolean; + reloadOnMount?: boolean; + subscribe?: boolean; +} + +export type useResourceType = { + < + Name extends Plugins[number]["name"], + Plugin extends Extract, + UriType extends string, + >( + uri: UriType, + options?: UseResourceOptions, + ): GetResourceReturnType; + < + Name extends Plugins[number]["name"], + Plugin extends Extract, + UriType extends string, + >( + uri?: UriType, + options?: UseResourceOptions, + ): GetResourceReturnType | undefined; +}; + +export function createUseResource( + dataset: ConnectedLdoDataset, +): useResourceType { + return function useResource< + Name extends Plugins[number]["name"], + Plugin extends Extract, + UriType extends string, + >( + uri?: UriType, + options?: UseResourceOptions, + ): GetResourceReturnType | undefined { + const subscriptionIdRef = useRef(); + + // Get the resource + const resource = useMemo(() => { + if (uri) { + const resource = dataset.getResource(uri); + // Run read operations if necissary + if (!options?.suppressInitialRead) { + if (options?.reloadOnMount) { + resource.read(); + } else { + resource.readIfUnfetched(); + } + } + return resource; + } + return undefined; + }, [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 as + | GetResourceReturnType + | undefined; + }; +} diff --git a/packages/react/src/methods/useSubject.ts b/packages/react/src/methods/useSubject.ts new file mode 100644 index 0000000..2969983 --- /dev/null +++ b/packages/react/src/methods/useSubject.ts @@ -0,0 +1,42 @@ +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"; +import type { ConnectedLdoDataset, ConnectedPlugin } from "@ldo/connected"; + +export type useSubjectType = { + ( + shapeType: ShapeType, + subject: string | SubjectNode, + ): Type; + ( + shapeType: ShapeType, + subject?: string | SubjectNode, + ): Type | undefined; + ( + shapeType: ShapeType, + subject?: string | SubjectNode, + ): Type | undefined; +}; + +export function createUseSubject( + dataset: ConnectedLdoDataset, +): useSubjectType { + return function useSubject( + shapeType: ShapeType, + subject?: string | SubjectNode, + ): Type | undefined { + const fromSubject = useCallback( + (builder: LdoBuilder) => { + if (!subject) return; + return builder.fromSubject(subject); + }, + [subject], + ); + + return useTrackingProxy(shapeType, fromSubject, dataset); + }; +} diff --git a/packages/react/src/methods/useSubscribeToResource.ts b/packages/react/src/methods/useSubscribeToResource.ts new file mode 100644 index 0000000..c9b589e --- /dev/null +++ b/packages/react/src/methods/useSubscribeToResource.ts @@ -0,0 +1,55 @@ +import { useEffect, useRef } from "react"; +import type { ConnectedLdoDataset, ConnectedPlugin } from "@ldo/connected"; + +export function createUseSubscribeToResource( + dataset: ConnectedLdoDataset, +) { + return function useSubscribeToResource(...uris: string[]): void { + const currentlySubscribed = useRef>({}); + useEffect(() => { + const resources = uris.map((uri) => dataset.getResource(uri)); + const previousSubscriptions = { ...currentlySubscribed.current }; + Promise.all( + 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); + }, + ), + ); + }; + }, []); + }; +} diff --git a/packages/react/src/useMatchObject.ts b/packages/react/src/useMatchObject.ts deleted file mode 100644 index c338646..0000000 --- a/packages/react/src/useMatchObject.ts +++ /dev/null @@ -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( - shapeType: ShapeType, - subject?: QuadMatch[0] | string, - predicate?: QuadMatch[1] | string, - graph?: QuadMatch[3] | string, -): LdSet { - const matchObject = useCallback( - (builder: LdoBuilder) => { - return builder.matchObject(subject, predicate, graph); - }, - [subject, predicate, graph], - ); - - return useTrackingProxy(shapeType, matchObject); -} diff --git a/packages/react/src/useMatchSubject.ts b/packages/react/src/useMatchSubject.ts deleted file mode 100644 index 494afc8..0000000 --- a/packages/react/src/useMatchSubject.ts +++ /dev/null @@ -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( - shapeType: ShapeType, - predicate?: QuadMatch[1] | string, - object?: QuadMatch[2] | string, - graph?: QuadMatch[3] | string, -): LdSet { - const matchSubject = useCallback( - (builder: LdoBuilder) => { - return builder.matchSubject(predicate, object, graph); - }, - [predicate, object, graph], - ); - - return useTrackingProxy(shapeType, matchSubject); -} diff --git a/packages/react/src/useResource.ts b/packages/react/src/useResource.ts deleted file mode 100644 index fb3fffb..0000000 --- a/packages/react/src/useResource.ts +++ /dev/null @@ -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(); - - // 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; -} diff --git a/packages/react/src/useRootContainer.ts b/packages/react/src/useRootContainer.ts deleted file mode 100644 index e32369a..0000000 --- a/packages/react/src/useRootContainer.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { Container, ContainerUri } from "@ldo/solid"; -import { useEffect, useState } from "react"; -import type { UseResourceOptions } from "./useResource"; -import { useResource } from "./useResource"; -import { useLdo } from "./SolidLdoProvider"; - -export function useRootContainerFor( - uri?: string, - options?: UseResourceOptions, -): Container | undefined { - const { getResource } = useLdo(); - - const [rootContainerUri, setRootContainerUri] = useState< - ContainerUri | undefined - >(undefined); - - useEffect(() => { - if (uri) { - const givenResource = getResource(uri); - givenResource.getRootContainer().then((result) => { - if (!result.isError) { - setRootContainerUri(result.uri); - } - }); - } else { - setRootContainerUri(undefined); - } - }, [uri]); - - return useResource(rootContainerUri, options); -} diff --git a/packages/react/src/useSubject.ts b/packages/react/src/useSubject.ts deleted file mode 100644 index c728c6f..0000000 --- a/packages/react/src/useSubject.ts +++ /dev/null @@ -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( - shapeType: ShapeType, - subject: string | SubjectNode, -): Type; -export function useSubject( - shapeType: ShapeType, - subject?: string | SubjectNode, -): Type | undefined; -export function useSubject( - shapeType: ShapeType, - subject?: string | SubjectNode, -): Type | undefined { - const fromSubject = useCallback( - (builder: LdoBuilder) => { - if (!subject) return; - return builder.fromSubject(subject); - }, - [subject], - ); - - return useTrackingProxy(shapeType, fromSubject); -} diff --git a/packages/react/src/useSubscribeToResource.ts b/packages/react/src/useSubscribeToResource.ts deleted file mode 100644 index d6dc8b0..0000000 --- a/packages/react/src/useSubscribeToResource.ts +++ /dev/null @@ -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>({}); - useEffect(() => { - const resources = uris.map((uri) => dataset.getResource(uri)); - const previousSubscriptions = { ...currentlySubscribed.current }; - Promise.all( - 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); - }, - ), - ); - }; - }, []); -} diff --git a/packages/react/src/util/useTrackingProxy.ts b/packages/react/src/util/useTrackingProxy.ts index 05fa452..64793e0 100644 --- a/packages/react/src/util/useTrackingProxy.ts +++ b/packages/react/src/util/useTrackingProxy.ts @@ -3,18 +3,16 @@ import { JsonldDatasetProxyBuilder, } from "@ldo/jsonld-dataset-proxy"; import { LdoBuilder } from "@ldo/ldo"; -import type { LdoBase, ShapeType } 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 { useLdo } from "../SolidLdoProvider"; export function useTrackingProxy( shapeType: ShapeType, createLdo: (builder: LdoBuilder) => ReturnType, + dataset: LdoDataset, ): ReturnType { - const { dataset } = useLdo(); - const [forceUpdateCounter, setForceUpdateCounter] = useState(0); const forceUpdate = useCallback( () => setForceUpdateCounter((val) => val + 1), diff --git a/packages/react/test/Solid-Integration.test.tsx b/packages/react/test/Solid-Integration.test.tsx index 5dd3f1c..88f4de5 100644 --- a/packages/react/test/Solid-Integration.test.tsx +++ b/packages/react/test/Solid-Integration.test.tsx @@ -8,15 +8,15 @@ import { setUpServer, } from "./setUpServer"; import { UnauthenticatedSolidLdoProvider } from "../src/UnauthenticatedSolidLdoProvider"; -import { useResource } from "../src/useResource"; -import { useRootContainerFor } from "../src/useRootContainer"; -import { useLdo } from "../src/SolidLdoProvider"; +import { useResource } from "../src/methods/useResource"; +import { useRootContainerFor } from "../src/methods/useRootContainer"; +import { useLdo } from "../src/createLdoReactMethods"; import { PostShShapeType } from "./.ldo/post.shapeTypes"; import type { PostSh } from "./.ldo/post.typings"; -import { useSubject } from "../src/useSubject"; -import { useMatchSubject } from "../src/useMatchSubject"; -import { useMatchObject } from "../src/useMatchObject"; -import { useSubscribeToResource } from "../src/useSubscribeToResource"; +import { useSubject } from "../src/methods/useSubject"; +import { useMatchSubject } from "../src/methods/useMatchSubject"; +import { useMatchObject } from "../src/methods/useMatchObject"; +import { useSubscribeToResource } from "../src/methods/useSubscribeToResource"; // Use an increased timeout, since the CSS server takes too much setup time. jest.setTimeout(40_000);