/** * Adapts iframe bridge resources to the React instance owned by a guest app. * The adapter takes hooks structurally, so React stays a guest dependency. */ import { type FabricValue, hashStringOf } from "@commonfabric/data-model"; import { type FabricClient, type ResourceSnapshot, type SqliteQueryInput, } from "./guest.ts"; type DependencyList = readonly unknown[]; /** React hooks required by the bridge adapter. */ export type ReactHooks = { useCallback unknown>( callback: T, dependencies: DependencyList, ): T; useEffect( effect: () => void | (() => void), dependencies?: DependencyList, ): void; useMemo(factory: () => T, dependencies: DependencyList): T; useRef(initial: T): { current: T }; useState( initial: T | (() => T), ): [T, (value: T | ((current: T) => T)) => void]; useSyncExternalStore( subscribe: (listener: () => void) => () => void, getSnapshot: () => T, getServerSnapshot?: () => T, ): T; }; /** Reactive state and mutations returned for a bridged cell. */ export type CellHookResult = ResourceSnapshot & { initialize(value: T): Promise; set(value: T | ((current: T) => T)): Promise; refresh(): Promise; }; /** Reactive state returned for a bridged SQLite query. */ export type QuerySnapshot = | { status: "loading"; rows: undefined; error: undefined } | { status: "ready"; rows: Row[]; error: undefined } | { status: "error"; rows: undefined; error: Error }; function loadingQuery(): QuerySnapshot { return { status: "loading", rows: undefined, error: undefined }; } type KeyedQuerySnapshot = { key: string; snapshot: QuerySnapshot; }; /** Builds hooks against the React instance already used by the guest app. */ export function createFabricReact(react: ReactHooks, client: FabricClient) { function useCell( name: string, ): CellHookResult { const cell = react.useMemo(() => client.cell(name), [name]); const snapshot = react.useSyncExternalStore( (notify) => cell.subscribeSnapshot(() => notify()), cell.getSnapshot, cell.getSnapshot, ); react.useEffect(() => { if (cell.getSnapshot().status === "loading") { void cell.pull().catch(() => {}); } }, [cell]); const set = react.useCallback( (next: T | ((current: T) => T)) => typeof next === "function" ? cell.update(next as (current: T) => T) : cell.set(next), [cell], ); const initialize = react.useCallback( (value: T) => cell.initialize(value), [cell], ); const refresh = react.useCallback(() => cell.pull(), [cell]); return { ...snapshot, initialize, set, refresh, }; } function useSqliteQuery>( name: string, sql: string, params?: SqliteQueryInput["params"], ): QuerySnapshot & { refresh(): Promise } { const database = react.useMemo(() => client.sqlite(name), [name]); const paramsKey = hashStringOf(params ?? null); const queryKey = hashStringOf({ name, sql, params: params ?? null }); const activeQuery = react.useRef(undefined); const generation = react.useRef(0); const [state, setState] = react.useState>(() => ({ key: queryKey, snapshot: loadingQuery(), })); const snapshot = state.key === queryKey ? state.snapshot : loadingQuery(); const refresh = react.useCallback(async () => { if (activeQuery.current !== queryKey) return; const request = ++generation.current; setState({ key: queryKey, snapshot: loadingQuery(), }); try { const result = await database.query(sql, params); if ( activeQuery.current !== queryKey || request !== generation.current ) return; setState({ key: queryKey, snapshot: { status: "ready", rows: result.rows, error: undefined, }, }); } catch (cause) { if ( activeQuery.current !== queryKey || request !== generation.current ) return; setState({ key: queryKey, snapshot: { status: "error", rows: undefined, error: cause instanceof Error ? cause : new Error(String(cause)), }, }); } }, [database, sql, paramsKey, queryKey]); react.useEffect(() => { activeQuery.current = queryKey; const unsubscribe = database.sink(() => void refresh()); void refresh(); return () => { if (activeQuery.current === queryKey) { activeQuery.current = undefined; generation.current++; } unsubscribe(); }; }, [database, queryKey, refresh]); return { ...snapshot, refresh }; } return { useCell, useSqliteQuery }; }