Document persistence failure-surfacing and overlay-store conventions

This commit is contained in:
2026-06-05 13:07:55 +03:00
parent 8e80b82cc0
commit 3dd8187ee6
2 changed files with 28 additions and 0 deletions
+14
View File
@@ -162,6 +162,20 @@ Each cohesive feature owns a store holding its durable domain state.
- **`useSettingsStore`** — user preferences (editor options, render debounce, - **`useSettingsStore`** — user preferences (editor options, render debounce,
date format, theme); mirrors what gets persisted to `localStorage`. 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<boolean>` 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` ### The central `useAppStore`
`useAppStore` holds only _cross-cutting, ephemeral UI state_ that no single `useAppStore` holds only _cross-cutting, ephemeral UI state_ that no single
+14
View File
@@ -410,6 +410,20 @@ export async function saveSnippet(s: Snippet): Promise<void> {
> **Do:** surface quota warnings _before_ the budget is hit (the 80% threshold) and hard errors loudly when a write fails. > **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. > **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 ## 7. Checklist for Adding a New Persisted Entity