clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

main
lofi / package / preact / live-data.ts
live-data.ts 5.4K
1import type { QueryBuilder, TableProxy } from "jazz-tools";2import { useCallback, useEffect, useRef, useState } from "preact/hooks";3import {4 acquireLiveQuery,5 type LiveQuerySnapshot,6 type LiveQueryStore,7} from "../runtime/live-query-store.ts";8import {9 acquireTableMutations,10 type TableMutationSnapshot,11 type TableMutationStore,12} from "../runtime/table-mutations.ts";13import type { TableRow } from "../runtime/table-store.ts";14import type { WriteHandle } from "../runtime/write-handle.ts";1516const loading = <T extends TableRow>(): LiveQuerySnapshot<T> => ({17 status: "loading",18 rows: [],19 error: null,20});2122/**23 * Subscribes a Preact component to any typed Jazz query.24 *25 * Equivalent mounted queries share one Jazz subscription. The snapshot preserves26 * the exact row type produced by the builder, including `select` and `include`27 * projections. An empty `rows` array with `status: "ready"` is an empty result,28 * not a loading signal.29 *30 * @example31 * ```ts32 * import { useLiveQuery } from "@nzip/lofi/preact";33 * import { app } from "../app.ts";34 *35 * const records = useLiveQuery(36 * () => app.schema.records.where({ workspaceId, archived: false }),37 * [workspaceId],38 * );39 * // records.status is "loading" | "ready" | "error"; records.rows is typed.40 * ```41 *42 * @param createQuery Builds the typed Jazz query; re-invoked when dependencies change.43 * @param dependencies Values that, when changed, release the query and open a replacement.44 * @returns The live snapshot: `status`, exact typed `rows`, and `error`.45 */46export function useLiveQuery<T extends TableRow>(47 createQuery: () => QueryBuilder<T>,48 dependencies: readonly unknown[],49): LiveQuerySnapshot<T> {50 const [snapshot, setSnapshot] = useState<LiveQuerySnapshot<T>>(loading);51 useEffect(() => {52 setSnapshot(loading());53 try {54 const lease = acquireLiveQuery(createQuery());55 const update = () => setSnapshot(lease.store.getSnapshot());56 const unsubscribe = lease.store.subscribe(update);57 update();58 return () => {59 unsubscribe();60 lease.release();61 };62 } catch (error) {63 setSnapshot({64 status: "error",65 rows: [],66 error: error instanceof Error ? error.message : String(error),67 });68 }69 }, dependencies);70 return snapshot;71}7273/** Typed mutation methods plus their shared table-scoped observable state. */74export type TableMutations<T extends TableRow, Init> = TableMutationSnapshot & {75 insert(values: Init): WriteHandle<T>;76 update(id: string, patch: Partial<Init>): WriteHandle<void>;77 remove(id: string): WriteHandle<void>;78};7980/**81 * Binds one typed Jazz table to stable insert, update, and remove methods.82 *83 * Each method returns a thenable `WriteHandle`: awaiting it resolves after84 * local durability, and the handle's `saved` and `synced` promises track the85 * write's sync fate. When managed sync is active, global confirmation86 * continues in the background and updates the shared table-scoped `pending`,87 * `durability`, and `error` state.88 *89 * @example90 * ```tsx91 * import { settleUiMutation } from "@nzip/lofi";92 * import { useTableMutations } from "@nzip/lofi/preact";93 * import { app } from "../app.ts";94 *95 * function ArchiveButton({ id }: { id: string }) {96 * const records = useTableMutations(app.schema.records);97 * // The handle is thenable: settled at saved, while the sync fate and the98 * // shared pending/durability state continue in the background.99 * return (100 * <button onClick={() => void settleUiMutation(records.update(id, { archived: true }))}>101 * Archive {records.pending > 0 && "…"}102 * </button>103 * );104 * }105 * ```106 *107 * @param table The typed schema table to mutate, e.g. `app.schema.records`.108 * @returns Stable `insert`, `update`, and `remove` methods plus the shared mutation state.109 * To attach declared effects to a write, wrap the same table in a verb110 * (`s.mutation(name, s.insert(table), { … })`) and observe its111 * {@link WriteHandle} with `useWrite`.112 */113export function useTableMutations<T extends TableRow, Init>(114 table: TableProxy<T, Init>,115): TableMutations<T, Init> {116 const [snapshot, setSnapshot] = useState<TableMutationSnapshot>({117 pending: 0,118 durability: "none",119 error: null,120 });121 const store = useRef<TableMutationStore<T, Init> | null>(null);122 useEffect(() => {123 const lease = acquireTableMutations(table);124 store.current = lease.store;125 const update = () => setSnapshot(lease.store.getSnapshot());126 const unsubscribe = lease.store.subscribe(update);127 update();128 return () => {129 if (store.current === lease.store) store.current = null;130 unsubscribe();131 lease.release();132 };133 }, [table]);134135 const requireStore = useCallback((): TableMutationStore<T, Init> => {136 if (!store.current) throw new Error("table mutations are not mounted yet");137 return store.current;138 }, []);139 const insert = useCallback((values: Init) => requireStore().insert(values), [requireStore]);140 const update = useCallback(141 (id: string, patch: Partial<Init>) => requireStore().update(id, patch),142 [requireStore],143 );144 const remove = useCallback((id: string) => requireStore().remove(id), [requireStore]);145 return { ...snapshot, insert, update, remove };146}147148// Keep the exact store type reachable in generated declaration output without149// asking application authors to import implementation modules.150export type { LiveQuerySnapshot, LiveQueryStore, TableMutationSnapshot, TableMutationStore };