From 3dd8187ee6fd7aaf1d060e19f3842f8ea60b3649 Mon Sep 17 00:00:00 2001 From: Oleh Omelchenko Date: Fri, 5 Jun 2026 13:07:55 +0300 Subject: [PATCH] Document persistence failure-surfacing and overlay-store conventions --- docs/architecture/01-state-and-stores.md | 14 ++++++++++++++ docs/architecture/02-persistence.md | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/docs/architecture/01-state-and-stores.md b/docs/architecture/01-state-and-stores.md index 813147d..5afa193 100644 --- a/docs/architecture/01-state-and-stores.md +++ b/docs/architecture/01-state-and-stores.md @@ -162,6 +162,20 @@ Each cohesive feature owns a store holding its durable domain state. - **`useSettingsStore`** — user preferences (editor options, render debounce, date format, theme); mirrors what gets persisted to `localStorage`. +### Global overlay stores (imperative trigger) + +Some surfaces are summoned from anywhere — including non-React code — and own only +ephemeral request state, not durable data: + +- **`useConfirmStore`** — the blocking confirm dialog (the `window.confirm` replacement). +- **`useNotificationStore`** — non-blocking toasts (failed saves, etc.). + +Each pairs its store with a thin **imperative trigger** exported alongside the hook — +`confirm(opts): Promise` and `notify(opts): string` — so orchestration/services can +raise one without a hook: `export const notify = (o) => useNotificationStore.getState().notify(o)`. +Components subscribe to the store to _render_ it; everyone else calls the function. (Why a +toast at all, and which channel for which message: [10 · Interaction & Feedback](10-interaction-and-feedback.md) §1.) + ### The central `useAppStore` `useAppStore` holds only _cross-cutting, ephemeral UI state_ that no single diff --git a/docs/architecture/02-persistence.md b/docs/architecture/02-persistence.md index c20b26f..e93cc01 100644 --- a/docs/architecture/02-persistence.md +++ b/docs/architecture/02-persistence.md @@ -410,6 +410,20 @@ export async function saveSnippet(s: Snippet): Promise { > **Do:** surface quota warnings _before_ the budget is hit (the 80% threshold) and hard errors loudly when a write fails. > **Don't:** wrap a save in a bare `try/catch {}` that logs and returns — that turns "your work wasn't saved" into a silent data-loss bug. The only thing the adapter may safely swallow is a _read_ failure, where falling back to defaults/empty is the correct behavior. +**The adapter propagating is only half — a consumer must catch and surface it.** A +fire-and-forget `void saveSnippet(n)` re-buries the very error the adapter took care to +throw. Persistence write-backs are wired as store subscribers, so the surfacing path is: + +`orchestration/persistence.ts` (write-through `.catch`) / `orchestration/startup.ts` (load +`.catch`, then run in memory) → `services/storage-errors.ts` (pure error→message mapper) → +`notify()` (`stores/NotificationStore`) → `Toaster`. + +Rules this encodes (spec §10 "told when a save fails"): never `void`-fire a persist without +a `.catch` that calls `notify(storageErrorNotification(op, err))`; the mapper splits +user-fixable (storage full → next step, no diagnostic) from not (blocked storage → plain +explanation **+** a reportable `detail`); and a blocked store at startup **warns and runs +in memory** rather than rejecting into the void. + --- ## 7. Checklist for Adding a New Persisted Entity