- commit
- cde09a776a7266ff6c91582e3e909dd12deaf1a8
- parent
- 94e79272d1af58cea34ffcb52b0b50534c275438
- author
- Dami <dami.etoile@gmail.com>
| M | apps/ | 10 | ++---- |
| M | apps/ | 49 | +++++++----------------------- |
| M | package/ | 10 | ++---- |
| M | package/ | 49 | +++++++----------------------- |
| M | package/ | 4 | +- |
@@ -2,11 +2,12 @@ import { useState } from "preact/hooks";
22 import { settleUiMutation } from "@nzip/lofi";33 import {44 type BootProgress,5+ Notices,56 useBootProgress,67 usePendingWrites,78 useSyncStatus,89 } from "@nzip/lofi/preact";9-import { type Task, useTaskNotice, useTasks } from "./use-tasks.ts";10+import { type Task, useTasks } from "./use-tasks.ts";1011 1112 // A cold first visit waits on the engine download, not on storage; name the1213 // wait it is actually in, with byte progress while the download runs.@@ -26,7 +27,6 @@ function loadingLabel(boot: BootProgress): string {
2627 */2728 export default function TaskList() {2829 const { status, error, durability, tasks, failureKind, create, setCompleted } = useTasks();29- const notice = useTaskNotice();3030 const pending = usePendingWrites();3131 const boot = useBootProgress();3232 const [text, setText] = useState("");@@ -78,11 +78,7 @@ export default function TaskList() {
7878 {pending.count} change{pending.count === 1 ? "" : "s"} waiting to sync7979 </p>8080 )}81- {notice && (82- <p class="state" role="status" data-notice={notice.kind}>83- {notice.text}84- </p>85- )}81+ <Notices />8682 <ul aria-label="Tasks">8783 {tasks.map((task) => <TaskItem key={task.id} task={task} setCompleted={setCompleted} />)}8884 </ul>@@ -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);@@ -2,11 +2,12 @@ import { useState } from "preact/hooks";
22 import { settleUiMutation } from "@nzip/lofi";33 import {44 type BootProgress,5+ Notices,56 useBootProgress,67 usePendingWrites,78 useSyncStatus,89 } from "@nzip/lofi/preact";9-import { type Task, useTaskNotice, useTasks } from "./use-tasks.ts";10+import { type Task, useTasks } from "./use-tasks.ts";1011 1112 // A cold first visit waits on the engine download, not on storage; name the1213 // wait it is actually in, with byte progress while the download runs.@@ -26,7 +27,6 @@ function loadingLabel(boot: BootProgress): string {
2627 */2728 export default function TaskList() {2829 const { status, error, durability, tasks, failureKind, create, setCompleted } = useTasks();29- const notice = useTaskNotice();3030 const pending = usePendingWrites();3131 const boot = useBootProgress();3232 const [text, setText] = useState("");@@ -78,11 +78,7 @@ export default function TaskList() {
7878 {pending.count} change{pending.count === 1 ? "" : "s"} waiting to sync7979 </p>8080 )}81- {notice && (82- <p class="state" role="status" data-notice={notice.kind}>83- {notice.text}84- </p>85- )}81+ <Notices />8682 <ul aria-label="Tasks">8783 {tasks.map((task) => <TaskItem key={task.id} task={task} setCompleted={setCompleted} />)}8884 </ul>@@ -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);@@ -15,8 +15,8 @@
1515 "src/app.ts": "2021889ec895b7c758e9c541eb968a480f63610073fe2c2d316faa7b66e85113",1616 "src/env.d.ts": "b44daed05ec5cdfacfd8d8acf7866974b5f6b8db923ab53bd244419093c719da",1717 "src/islands/AccountGate.tsx": "0faa82edb05ea9c6da1f575ebb75cc2f60d8e95e88524c0720e3c885a1eee659",18- "src/islands/TaskList.tsx": "7b0da8830e5fab9c59948eff1cbaa357805ecea9c5d070249dd46157e5e36bdf",19- "src/islands/use-tasks.ts": "6ae8e1130c76604dba16e4bb7680b2c66163568db8b250fba065996db3fb2146",18+ "src/islands/TaskList.tsx": "5b76707bf953d12a42d2be26c0a4f3eab77e972e228685a6cd945a65891e18f0",19+ "src/islands/use-tasks.ts": "277a54f6f77afebc303d869f0aac9db8423a2b5c707333046ce4d95993577a17",2020 "src/layouts/Shell.astro": "57f5c144813fad7d6c7223b0e68498665f41dcd8b0855f2e8bf020d3bb072711",2121 "src/pages/index.astro": "8a696a8154b77c34deba3919f6383dc2ed0a6af15612fd9b16f04416e3331e93",2222 "src/permissions.ts": "7a2dad8ce48816c1611c85049209f4fddef08ab332dcbd94bac4fa162fc6c1b0",