parent
c69fef3070
commit
39388bfe83
@ -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 extends ConnectedPlugin[]>( |
||||||
|
plugins: Plugins, |
||||||
|
): ConnectedLdoDataset<Plugins> { |
||||||
|
return new ConnectedLdoDataset( |
||||||
|
plugins, |
||||||
|
createDatasetFactory(), |
||||||
|
createTransactionDatasetFactory(), |
||||||
|
); |
||||||
|
} |
@ -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<PropsWithChildren> = ({ |
|
||||||
children, |
|
||||||
}) => { |
|
||||||
const [session, setSession] = useState<SessionInfo>(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 ( |
|
||||||
<SolidAuthContext.Provider value={solidAuthFunctions}> |
|
||||||
<SolidLdoProvider>{children}</SolidLdoProvider> |
|
||||||
</SolidAuthContext.Provider> |
|
||||||
); |
|
||||||
}; |
|
@ -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<void>; |
|
||||||
logout: () => Promise<void>; |
|
||||||
signUp: (issuer: string, loginOptions?: LoginOptions) => Promise<void>; |
|
||||||
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<SolidAuthFunctions>(undefined); |
|
||||||
|
|
||||||
export function useSolidAuth(): SolidAuthFunctions { |
|
||||||
return useContext(SolidAuthContext); |
|
||||||
} |
|
@ -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> |
|
||||||
); |
|
||||||
}; |
|
@ -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 ( |
|
||||||
<SolidAuthContext.Provider value={solidAuthFunctions}> |
|
||||||
<SolidLdoProvider>{children}</SolidLdoProvider> |
|
||||||
</SolidAuthContext.Provider> |
|
||||||
); |
|
||||||
}; |
|
@ -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 extends ConnectedPlugin[]>( |
||||||
|
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), |
||||||
|
}; |
||||||
|
} |
@ -1,12 +1 @@ |
|||||||
export * from "./BrowserSolidLdoProvider"; |
export * from "./createLdoReactMethods"; |
||||||
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"; |
|
||||||
|
@ -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<Plugins extends ConnectedPlugin[]>( |
||||||
|
dataset: ConnectedLdoDataset<Plugins>, |
||||||
|
) { |
||||||
|
return 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, dataset); |
||||||
|
}; |
||||||
|
} |
@ -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<Plugins extends ConnectedPlugin[]>( |
||||||
|
dataset: ConnectedLdoDataset<Plugins>, |
||||||
|
) { |
||||||
|
return 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, dataset); |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,122 @@ |
|||||||
|
import { useMemo, useEffect, useRef, useState, useCallback } from "react"; |
||||||
|
import type { |
||||||
|
ConnectedLdoDataset, |
||||||
|
ConnectedPlugin, |
||||||
|
GetResourceReturnType, |
||||||
|
Resource, |
||||||
|
} from "@ldo/connected"; |
||||||
|
|
||||||
|
export interface UseResourceOptions<Name> { |
||||||
|
pluginName?: Name; |
||||||
|
suppressInitialRead?: boolean; |
||||||
|
reloadOnMount?: boolean; |
||||||
|
subscribe?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export type useResourceType<Plugins extends ConnectedPlugin[]> = { |
||||||
|
< |
||||||
|
Name extends Plugins[number]["name"], |
||||||
|
Plugin extends Extract<Plugins[number], { name: Name }>, |
||||||
|
UriType extends string, |
||||||
|
>( |
||||||
|
uri: UriType, |
||||||
|
options?: UseResourceOptions<Name>, |
||||||
|
): GetResourceReturnType<Plugin, UriType>; |
||||||
|
< |
||||||
|
Name extends Plugins[number]["name"], |
||||||
|
Plugin extends Extract<Plugins[number], { name: Name }>, |
||||||
|
UriType extends string, |
||||||
|
>( |
||||||
|
uri?: UriType, |
||||||
|
options?: UseResourceOptions<Name>, |
||||||
|
): GetResourceReturnType<Plugin, UriType> | undefined; |
||||||
|
}; |
||||||
|
|
||||||
|
export function createUseResource<Plugins extends ConnectedPlugin[]>( |
||||||
|
dataset: ConnectedLdoDataset<Plugins>, |
||||||
|
): useResourceType<Plugins> { |
||||||
|
return function useResource< |
||||||
|
Name extends Plugins[number]["name"], |
||||||
|
Plugin extends Extract<Plugins[number], { name: Name }>, |
||||||
|
UriType extends string, |
||||||
|
>( |
||||||
|
uri?: UriType, |
||||||
|
options?: UseResourceOptions<Name>, |
||||||
|
): GetResourceReturnType<Plugin, UriType> | undefined { |
||||||
|
const subscriptionIdRef = useRef<string | undefined>(); |
||||||
|
|
||||||
|
// 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<Plugin, UriType> |
||||||
|
| undefined; |
||||||
|
}; |
||||||
|
} |
@ -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 = { |
||||||
|
<Type extends LdoBase>( |
||||||
|
shapeType: ShapeType<Type>, |
||||||
|
subject: string | SubjectNode, |
||||||
|
): Type; |
||||||
|
<Type extends LdoBase>( |
||||||
|
shapeType: ShapeType<Type>, |
||||||
|
subject?: string | SubjectNode, |
||||||
|
): Type | undefined; |
||||||
|
<Type extends LdoBase>( |
||||||
|
shapeType: ShapeType<Type>, |
||||||
|
subject?: string | SubjectNode, |
||||||
|
): Type | undefined; |
||||||
|
}; |
||||||
|
|
||||||
|
export function createUseSubject<Plugins extends ConnectedPlugin[]>( |
||||||
|
dataset: ConnectedLdoDataset<Plugins>, |
||||||
|
): useSubjectType { |
||||||
|
return 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, dataset); |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
import { useEffect, useRef } from "react"; |
||||||
|
import type { ConnectedLdoDataset, ConnectedPlugin } from "@ldo/connected"; |
||||||
|
|
||||||
|
export function createUseSubscribeToResource<Plugins extends ConnectedPlugin[]>( |
||||||
|
dataset: ConnectedLdoDataset<Plugins>, |
||||||
|
) { |
||||||
|
return function useSubscribeToResource(...uris: string[]): void { |
||||||
|
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,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,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); |
|
||||||
} |
|
@ -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); |
|
||||||
}, |
|
||||||
), |
|
||||||
); |
|
||||||
}; |
|
||||||
}, []); |
|
||||||
} |
|
Loading…
Reference in new issue