UNPKG

@orbitinghail/sqlsync-solid-js

Version:

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

127 lines (108 loc) 3.6 kB
import { ConnectionStatus, DocId, DocType, ParameterizedQuery, QuerySubscription, Row, SQLSync, normalizeQuery, pendingPromise, } from "@orbitinghail/sqlsync-worker"; import { Accessor, createEffect, createSignal, onCleanup, useContext } from "solid-js"; import { SQLSyncContext } from "./context"; export function useSQLSync(): Accessor<SQLSync> { const [sqlSync] = useContext(SQLSyncContext); return () => { const value = sqlSync(); 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: Accessor<DocId>, query: Accessor<ParameterizedQuery | string>, ) => Accessor<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: Accessor<DocType<M>>): DocHooks<M> { const useMutate = (docId: DocId): MutateFn<M> => { const sqlsync = useSQLSync(); return (mutation: M) => sqlsync().mutate(docId, docType(), mutation); }; const useQueryWrapper = <R = Row>( docId: Accessor<DocId>, query: Accessor<ParameterizedQuery | string>, ) => { return useQuery<M, R>(docType, docId, query); }; const useSetConnectionEnabledWrapper = (docId: DocId) => { const sqlsync = useSQLSync(); return (enabled: boolean) => sqlsync().setConnectionEnabled(docId, docType(), enabled); }; 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: Accessor<DocType<M>>, docId: Accessor<DocId>, rawQuery: Accessor<ParameterizedQuery | string>, ): Accessor<QueryState<R>> { const sqlsync = useSQLSync(); const [state, setState] = createSignal<QueryState<R>>({ state: "pending" }); createEffect(() => { const query = normalizeQuery(rawQuery()); 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 }); }); onCleanup(() => { unsubPromise .then((unsub) => unsub()) .catch((err) => { console.error("sqlsync: error unsubscribing", err); }); }); }); return state; } export const useConnectionStatus = (): Accessor<ConnectionStatus> => { const sqlsync = useSQLSync(); const [status, setStatus] = createSignal<ConnectionStatus>(sqlsync().connectionStatus); createEffect(() => { const cleanup = sqlsync().addConnectionStatusListener(setStatus); onCleanup(cleanup); }); return status; };