UNPKG

@orbitinghail/sqlsync-react

Version:

SQLSync is a collaborative offline-first wrapper around SQLite. It is designed to synchronize web application state between users, devices, and the edge.

129 lines (109 loc) 3.69 kB
import { ConnectionStatus, DocId, DocType, ParameterizedQuery, QuerySubscription, Row, SQLSync, normalizeQuery, pendingPromise, } from "@orbitinghail/sqlsync-worker"; import { deepEqual } from "fast-equals"; import { useCallback, useContext, useEffect, useRef, useState } from "react"; import { SQLSyncContext } from "./context"; export function useSQLSync(): SQLSync { const value = useContext(SQLSyncContext); if (!value) { throw new Error( "could not find sqlsync context value; please ensure the component is wrapped in a <SqlSyncProvider>", ); } return value; } type MutateFn<M> = (mutation: M) => Promise<void>; type UseMutateFn<M> = (docId: DocId) => MutateFn<M>; type UseQueryFn = <R = Row>(docId: DocId, query: ParameterizedQuery | string) => QueryState<R>; type SetConnectionEnabledFn = (enabled: boolean) => Promise<void>; type UseSetConnectionEnabledFn = (docId: DocId) => SetConnectionEnabledFn; export interface DocHooks<M> { useMutate: UseMutateFn<M>; useQuery: UseQueryFn; useSetConnectionEnabled: UseSetConnectionEnabledFn; } export function createDocHooks<M>(docType: DocType<M>): DocHooks<M> { const useMutate = (docId: DocId): MutateFn<M> => { const sqlsync = useSQLSync(); return useCallback( (mutation: M) => sqlsync.mutate(docId, docType, mutation), [sqlsync, docId, docType], ); }; const useQueryWrapper = <R = Row>(docId: DocId, query: ParameterizedQuery | string) => { return useQuery<M, R>(docType, docId, query); }; const useSetConnectionEnabledWrapper = (docId: DocId) => { const sqlsync = useSQLSync(); return useCallback( (enabled: boolean) => sqlsync.setConnectionEnabled(docId, docType, enabled), [sqlsync, docId, docType], ); }; return { useMutate, useQuery: useQueryWrapper, useSetConnectionEnabled: useSetConnectionEnabledWrapper, }; } export type QueryState<R> = | { state: "pending"; rows?: R[] } | { state: "success"; rows: R[] } | { state: "error"; error: Error; rows?: R[] }; export function useQuery<M, R = Row>( docType: DocType<M>, docId: DocId, rawQuery: ParameterizedQuery | string, ): QueryState<R> { const sqlsync = useSQLSync(); const [state, setState] = useState<QueryState<R>>({ state: "pending" }); // memoize query based on deep equality let query = normalizeQuery(rawQuery); const queryRef = useRef<ParameterizedQuery>(query); if (!deepEqual(queryRef.current, query)) { queryRef.current = query; } query = queryRef.current; useEffect(() => { const [unsubPromise, unsubResolve] = pendingPromise<() => void>(); const subscription: QuerySubscription = { handleRows: (rows: Row[]) => setState({ state: "success", rows: rows as R[] }), handleErr: (err: string) => setState((s) => ({ state: "error", error: new Error(err), rows: s.rows, })), }; sqlsync .subscribe(docId, docType, query, subscription) .then(unsubResolve) .catch((err: Error) => { console.error("sqlsync: error subscribing", err); setState({ state: "error", error: err }); }); return () => { unsubPromise .then((unsub) => unsub()) .catch((err) => { console.error("sqlsync: error unsubscribing", err); }); }; }, [sqlsync, docId, docType, query]); return state; } export const useConnectionStatus = (): ConnectionStatus => { const sqlsync = useSQLSync(); const [status, setStatus] = useState<ConnectionStatus>(sqlsync.connectionStatus); useEffect(() => sqlsync.addConnectionStatusListener(setStatus), [sqlsync]); return status; };