clone

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

main
lofi / apps / reference / src / islands / use-tasks.ts
use-tasks.ts 3.2K
1import { useCallback, useState } from "preact/hooks";2import type { RowOf, WriteHandle } from "@nzip/lofi";3import { useLiveQuery, useWrite } from "@nzip/lofi/preact";4import { s } from "@nzip/lofi/schema";5import { app } from "../app.ts";67/**8 * Author-owned example. Tables are the nouns of your app; verbs are what9 * happens to them. This module declares two verbs over the `tasks` table —10 * `addTask` carries effects that run when the write's sync fate settles, and11 * `setTaskCompleted` is a plain verb — then wraps them in a domain hook.12 * Replace `tasks` and the verbs below with your own model; framework runtime13 * code remains in `@nzip/lofi`.14 */15const tasksTable = app.schema.tasks;1617/** The row type comes straight from the declared schema. */18export type Task = RowOf<typeof tasksTable>;1920/**21 * The verb call sites use. Its effect units are declared once, here: the22 * consequence runs when the store confirms the task, the compensation runs if23 * a stale-policy write is denied — even if the app restarted in between.24 */25export const addTask = s.mutation("addTask", s.insert(tasksTable), {26 effects: [27 s.log("task-added"),28 s.trace("task-added"),29 s.notice<Task>({30 synced: (task) => `"${task.text ?? "Task"}" synced to your account`,31 // The engine already rolled a denied insert out of local reads; this32 // durable notice compensates what the user was told, even after reload.33 rejected: (task) => `"${task.text ?? "Task"}" was declined by the store and has been removed`,34 }),35 ],36});3738/** Toggling completion is a plain verb: no consequences, same lifecycle. */39export const setTaskCompleted = s.mutation("setTaskCompleted", s.update(tasksTable));4041export function useTasks() {42 const query = useLiveQuery(() => tasksTable.orderBy("createdAt", "desc"), []);43 const [lastWrite, setLastWrite] = useState<WriteHandle<Task> | null>(null);44 const [writeError, setWriteError] = useState<string | null>(null);45 const write = useWrite(lastWrite);4647 const create = useCallback((text: string) => {48 // The verb returns a WriteHandle: awaiting it means saved on this device;49 // `.synced` and the declared effects follow the store's confirmation.50 const handle = addTask({ text, completed: false, createdAt: new Date() });51 setLastWrite(handle);52 setWriteError(null);53 handle.saved.then(undefined, (error) => {54 setWriteError(error instanceof Error ? error.message : String(error));55 });56 return handle;57 }, []);58 const setCompleted = useCallback(59 (id: string, completed: boolean) => setTaskCompleted(id, { completed }),60 [],61 );6263 const durability = write.stage === "synced"64 ? "global" as const65 : write.stage === "saved" || write.stage === "syncing"66 ? "local" as const67 : write.stage === "rejected"68 ? "failed" as const69 : "none" as const;7071 return {72 status: query.status === "error" || writeError !== null ? "error" as const : query.status,73 error: query.error ?? writeError,74 durability,75 lastWriteStage: write.stage,76 lastWriteReason: write.reason,77 tasks: query.rows,78 failureKind: query.status === "error" ? "read" as const : "write" as const,79 create,80 setCompleted,81 };82}