clone

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

refactor(reference): adopt durable task notices (#185)

FelineStateMachine committed 24ac0d0
commit
24ac0d080d2033c4700034487ca301be58021164
parent
94e79272d1af58cea34ffcb52b0b50534c275438
author
FelineStateMachine <dami.etoile@gmail.com>
5 files changed, +30 -92
Mapps/reference/src/islands/TaskList.tsx10++----
Mapps/reference/src/islands/use-tasks.ts49+++++++-----------------------
Mpackage/starter/src/islands/TaskList.tsx.txt10++----
Mpackage/starter/src/islands/use-tasks.ts.txt49+++++++-----------------------
Mpackage/testdata/starter.snapshot.json4+-
M apps/reference/src/islands/TaskList.tsx +3 -7
M apps/reference/src/islands/use-tasks.ts +11 -38
@@ -1,4 +1,4 @@
1-import { useCallback, useEffect, useState } from "preact/hooks";1+import { useCallback, useState } from "preact/hooks";22 import type { RowOf, WriteHandle } from "@nzip/lofi";33 import { useLiveQuery, useWrite } from "@nzip/lofi/preact";44 import { s } from "@nzip/lofi/schema";
@@ -17,54 +17,27 @@ const tasksTable = app.schema.tasks;
1717 /** The row type comes straight from the declared schema. */1818 export type Task = RowOf<typeof tasksTable>;1919 20-/** A one-line consequence or compensation surfaced to the UI. */21-export type TaskNotice = { kind: "synced" | "rejected"; text: string };22-23-// A tiny author-owned notice channel: effect handlers run outside any24-// component, so they publish through module state and hooks subscribe.25-let notice: TaskNotice | null = null;26-const noticeListeners = new Set<() => void>();27-28-function publishNotice(next: TaskNotice | null): void {29- notice = next;30- for (const listener of [...noticeListeners]) listener();31-}32-3320 /**3421 * The verb call sites use. Its effect units are declared once, here: the3522 * consequence runs when the store confirms the task, the compensation runs if3623 * a stale-policy write is denied — even if the app restarted in between.3724 */3825 export const addTask = s.mutation("addTask", s.insert(tasksTable), {39- effects: [s.log("task-added"), s.trace("task-added")],40- onSynced: (task) => {41- publishNotice({ kind: "synced", text: `"${task.text ?? "Task"}" synced to your account` });42- },43- onRejected: (task) => {44- // The engine already rolled the denied row back out of local reads; this45- // compensates what the user was told.46- publishNotice({47- kind: "rejected",48- text: `"${task.text ?? "Task"}" was declined by the store and has been removed`,49- });50- },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+ ],5136 });5237 5338 /** Toggling completion is a plain verb: no consequences, same lifecycle. */5439 export const setTaskCompleted = s.mutation("setTaskCompleted", s.update(tasksTable));5540 56-/** Subscribes to the latest effect notice; `null` until one is published. */57-export function useTaskNotice(): TaskNotice | null {58- const [current, setCurrent] = useState<TaskNotice | null>(notice);59- useEffect(() => {60- const listener = () => setCurrent(notice);61- noticeListeners.add(listener);62- listener();63- return () => void noticeListeners.delete(listener);64- }, []);65- return current;66-}67-6841 export function useTasks() {6942 const query = useLiveQuery(() => tasksTable.orderBy("createdAt", "desc"), []);7043 const [lastWrite, setLastWrite] = useState<WriteHandle<Task> | null>(null);
M package/starter/src/islands/TaskList.tsx.txt +3 -7
M package/starter/src/islands/use-tasks.ts.txt +11 -38
@@ -1,4 +1,4 @@
1-import { useCallback, useEffect, useState } from "preact/hooks";1+import { useCallback, useState } from "preact/hooks";22 import type { RowOf, WriteHandle } from "@nzip/lofi";33 import { useLiveQuery, useWrite } from "@nzip/lofi/preact";44 import { s } from "@nzip/lofi/schema";
@@ -17,54 +17,27 @@ const tasksTable = app.schema.tasks;
1717 /** The row type comes straight from the declared schema. */1818 export type Task = RowOf<typeof tasksTable>;1919 20-/** A one-line consequence or compensation surfaced to the UI. */21-export type TaskNotice = { kind: "synced" | "rejected"; text: string };22-23-// A tiny author-owned notice channel: effect handlers run outside any24-// component, so they publish through module state and hooks subscribe.25-let notice: TaskNotice | null = null;26-const noticeListeners = new Set<() => void>();27-28-function publishNotice(next: TaskNotice | null): void {29- notice = next;30- for (const listener of [...noticeListeners]) listener();31-}32-3320 /**3421 * The verb call sites use. Its effect units are declared once, here: the3522 * consequence runs when the store confirms the task, the compensation runs if3623 * a stale-policy write is denied — even if the app restarted in between.3724 */3825 export const addTask = s.mutation("addTask", s.insert(tasksTable), {39- effects: [s.log("task-added"), s.trace("task-added")],40- onSynced: (task) => {41- publishNotice({ kind: "synced", text: `"${task.text ?? "Task"}" synced to your account` });42- },43- onRejected: (task) => {44- // The engine already rolled the denied row back out of local reads; this45- // compensates what the user was told.46- publishNotice({47- kind: "rejected",48- text: `"${task.text ?? "Task"}" was declined by the store and has been removed`,49- });50- },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+ ],5136 });5237 5338 /** Toggling completion is a plain verb: no consequences, same lifecycle. */5439 export const setTaskCompleted = s.mutation("setTaskCompleted", s.update(tasksTable));5540 56-/** Subscribes to the latest effect notice; `null` until one is published. */57-export function useTaskNotice(): TaskNotice | null {58- const [current, setCurrent] = useState<TaskNotice | null>(notice);59- useEffect(() => {60- const listener = () => setCurrent(notice);61- noticeListeners.add(listener);62- listener();63- return () => void noticeListeners.delete(listener);64- }, []);65- return current;66-}67-6841 export function useTasks() {6942 const query = useLiveQuery(() => tasksTable.orderBy("createdAt", "desc"), []);7043 const [lastWrite, setLastWrite] = useState<WriteHandle<Task> | null>(null);
M package/testdata/starter.snapshot.json +2 -2