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";1415const loading = <T extends TableRow>(): LiveQuerySnapshot<T> => ({16 status: "loading",17 rows: [],18 error: null,19});2021/**22 * Subscribes a Preact component to any typed Jazz query.23 *24 * Equivalent mounted queries share one Jazz subscription. The snapshot preserves25 * the exact row type produced by the builder, including `select` and `include`26 * projections. An empty `rows` array with `status: "ready"` is an empty result,27 * not a loading signal.28 *29 * @example30 * ```ts31 * import { useLiveQuery } from "@nzip/lofi/preact";32 * import { app } from "../app.ts";33 *34 * const records = useLiveQuery(35 * () => app.schema.records.where({ workspaceId, archived: false }),36 * [workspaceId],37 * );38 * // records.status is "loading" | "ready" | "error"; records.rows is typed.39 * ```40 *41 * @param createQuery Builds the typed Jazz query; re-invoked when dependencies change.42 * @param dependencies Values that, when changed, release the query and open a replacement.43 * @returns The live snapshot: `status`, exact typed `rows`, and `error`.44 */45export function useLiveQuery<T extends TableRow>(46 createQuery: () => QueryBuilder<T>,47 dependencies: readonly unknown[],48): LiveQuerySnapshot<T> {49 const [snapshot, setSnapshot] = useState<LiveQuerySnapshot<T>>(loading);50 useEffect(() => {51 setSnapshot(loading());52 try {53 const lease = acquireLiveQuery(createQuery());54 const update = () => setSnapshot(lease.store.getSnapshot());55 const unsubscribe = lease.store.subscribe(update);56 update();57 return () => {58 unsubscribe();59 lease.release();60 };61 } catch (error) {62 setSnapshot({63 status: "error",64 rows: [],65 error: error instanceof Error ? error.message : String(error),66 });67 }68 }, dependencies);69 return snapshot;70}7172/** Typed mutation methods plus their shared table-scoped observable state. */73export type TableMutations<T extends TableRow, Init> = TableMutationSnapshot & {74 insert(values: Init): Promise<T>;75 update(id: string, patch: Partial<Init>): Promise<void>;76 remove(id: string): Promise<void>;77};7879/**80 * Binds one typed Jazz table to stable insert, update, and remove methods.81 *82 * Mutation promises resolve after local durability; when managed sync is active,83 * global confirmation continues in the background and updates the shared84 * table-scoped `pending`, `durability`, and `error` state.85 *86 * @example87 * ```ts88 * import { useTableMutations } from "@nzip/lofi/preact";89 * import { app } from "../app.ts";90 *91 * const records = useTableMutations(app.schema.records);92 * const created = await records.insert({ title: "Release notes", archived: false });93 * await records.update(created.id, { archived: true });94 * await records.remove(created.id);95 * ```96 *97 * @param table The typed schema table to mutate, e.g. `app.schema.records`.98 * @returns Stable `insert`, `update`, and `remove` methods plus the shared mutation state.99 */100export function useTableMutations<T extends TableRow, Init>(101 table: TableProxy<T, Init>,102): TableMutations<T, Init> {103 const [snapshot, setSnapshot] = useState<TableMutationSnapshot>({104 pending: 0,105 durability: "none",106 error: null,107 });108 const store = useRef<TableMutationStore<T, Init> | null>(null);109 useEffect(() => {110 const lease = acquireTableMutations(table);111 store.current = lease.store;112 const update = () => setSnapshot(lease.store.getSnapshot());113 const unsubscribe = lease.store.subscribe(update);114 update();115 return () => {116 if (store.current === lease.store) store.current = null;117 unsubscribe();118 lease.release();119 };120 }, [table]);121122 const requireStore = useCallback((): TableMutationStore<T, Init> => {123 if (!store.current) throw new Error("table mutations are not mounted yet");124 return store.current;125 }, []);126 const insert = useCallback((values: Init) => requireStore().insert(values), [requireStore]);127 const update = useCallback(128 (id: string, patch: Partial<Init>) => requireStore().update(id, patch),129 [requireStore],130 );131 const remove = useCallback((id: string) => requireStore().remove(id), [requireStore]);132 return { ...snapshot, insert, update, remove };133}134135// Keep the exact store type reachable in generated declaration output without136// asking application authors to import implementation modules.137export type { LiveQuerySnapshot, LiveQueryStore, TableMutationSnapshot, TableMutationStore };