clone

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

main
lofi / apps / demo / overlay / src / islands / use-incidents.ts
use-incidents.ts 3.3K
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 domain module. Tables are the nouns of the app; verbs are what9 * happens to them. This module declares two verbs over the `incidents` table,10 * `reportIncident` with effects that run when the write's sync fate settles11 * and `setIncidentStatus` as a plain verb, then wraps them in a domain hook.12 */13const incidentsTable = app.schema.incidents;1415/** The row type comes straight from the declared schema. */16export type Incident = RowOf<typeof incidentsTable>;1718export type Severity = Incident["severity"];19export type IncidentStatus = Incident["status"];2021/**22 * The reporting verb. Its effect units are declared once, here: the23 * consequence runs when the store confirms the row, the compensation runs if24 * a stale-policy write is denied, even if the app restarted in between.25 */26export const reportIncident = s.mutation("reportIncident", s.insert(incidentsTable), {27 effects: [28 s.log("incident-reported"),29 s.notice<Incident>({30 synced: (incident) => `"${incident.title ?? "Incident"}" confirmed by the store`,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: (incident) =>34 `"${incident.title ?? "Incident"}" was declined by the store and has been removed`,35 }),36 ],37});3839/** Moving an incident between states is a plain verb: same lifecycle. */40export const setIncidentStatus = s.mutation("setIncidentStatus", s.update(incidentsTable));4142export function useIncidents() {43 const query = useLiveQuery(() => incidentsTable.orderBy("openedAt", "desc"), []);44 const [lastWrite, setLastWrite] = useState<WriteHandle<Incident> | null>(null);45 const [writeError, setWriteError] = useState<string | null>(null);46 const write = useWrite(lastWrite);4748 const report = useCallback((title: string, severity: Severity) => {49 // The verb returns a WriteHandle: awaiting it means saved on this device;50 // `.synced` and the declared effects follow the store's confirmation.51 const handle = reportIncident({52 title,53 severity,54 status: "burning",55 openedAt: new Date(),56 });57 setLastWrite(handle);58 setWriteError(null);59 handle.saved.then(undefined, (error) => {60 setWriteError(error instanceof Error ? error.message : String(error));61 });62 return handle;63 }, []);64 const setStatus = useCallback(65 (id: string, status: IncidentStatus) => setIncidentStatus(id, { status }),66 [],67 );6869 const durability = write.stage === "synced"70 ? "global" as const71 : write.stage === "saved" || write.stage === "syncing"72 ? "local" as const73 : write.stage === "rejected"74 ? "failed" as const75 : "none" as const;7677 return {78 status: query.status === "error" || writeError !== null ? "error" as const : query.status,79 error: query.error ?? writeError,80 durability,81 lastWriteStage: write.stage,82 lastWriteReason: write.reason,83 incidents: query.rows,84 failureKind: query.status === "error" ? "read" as const : "write" as const,85 report,86 setStatus,87 };88}