diff --git a/docs/reference/architecture/ARCHITECTURE.md b/docs/reference/architecture/ARCHITECTURE.md index cd6844e..4962a3f 100644 --- a/docs/reference/architecture/ARCHITECTURE.md +++ b/docs/reference/architecture/ARCHITECTURE.md @@ -43,11 +43,11 @@ ever uses the level(s) below it — so anything you build is reusable by everyth ```mermaid graph TD - P["Pages
dashboard.page · login.page · intake.page"] - T["Templates
page-shell · shell"] - O["Organisms
login-form · registration-table · intake-wizard"] - M["Molecules
form-field · data-row · async"] - A["Atoms
button · text-input · radio-group · alert · heading"] + P["Pages
dashboard.page · login.page · intake.page"] + T["Templates
page-shell · shell"] + O["Organisms
login-form · registration-table · intake-wizard"] + M["Molecules
form-field · data-row · async"] + A["Atoms
button · text-input · radio-group · alert · heading"] P --> T --> O --> M --> A classDef l fill:#e5f1fb,stroke:#007bc7,color:#00567d; class P,T,O,M,A l; @@ -110,12 +110,12 @@ the 4 states that are real** — the illegal ones can't be expressed at all. graph LR subgraph bad["3 booleans = 8 states (most illegal)"] direction TB - b1["loading ✓ · error ✗ · data ✗ ✅"] - b2["loading ✗ · error ✓ · data ✗ ✅"] - b3["loading ✗ · error ✗ · data ✓ ✅"] - b4["loading ✓ · error ✓ · data ✓ ❌ nonsense"] - b5["loading ✓ · error ✗ · data ✓ ❌ nonsense"] - b6["… 3 more illegal combos ❌"] + b1["loading ✓ · error ✗ · data ✗ — legal"] + b2["loading ✗ · error ✓ · data ✗ — legal"] + b3["loading ✗ · error ✗ · data ✓ — legal"] + b4["loading ✓ · error ✓ · data ✓ — nonsense"] + b5["loading ✓ · error ✗ · data ✓ — nonsense"] + b6["… 3 more illegal combos"] end subgraph good["1 union = 4 legal states"] direction TB @@ -125,8 +125,10 @@ graph LR g4["Success (carries value)"] end bad -->|"choose a better type"| good - classDef ok fill:#e8f5e9,stroke:#39870c; classDef no fill:#fdecea,stroke:#d52b1e; - class b1,b2,b3,g1,g2,g3,g4 ok; class b4,b5,b6 no; + classDef ok fill:#e8f5e9,stroke:#39870c; + classDef no fill:#fdecea,stroke:#d52b1e; + class b1,b2,b3,g1,g2,g3,g4 ok; + class b4,b5,b6 no; ``` The same argument applies to forms (a `submitting` boolean that can be true _with_ @@ -175,7 +177,7 @@ stateDiagram-v2 Loading --> Success: data arrived Loading --> Empty: arrived, but no rows Loading --> Failure: request failed - Failure --> Loading: reload() + Failure --> Loading: reload note right of Success value lives ONLY here end note @@ -230,15 +232,15 @@ _one_ function. No state is mutated anywhere else. ```mermaid sequenceDiagram actor User - participant View as View (template) - participant Store as createStore (signal) - participant Reduce as reduce() — PURE + participant View + participant Store + participant Reduce User->>View: clicks / types View->>Store: dispatch(msg) - Store->>Reduce: reduce(model, msg) + Store->>Reduce: reduce(model, msg) — PURE Reduce-->>Store: next model - Store-->>View: signal updates → re-render - Note over Reduce: the ONLY place state changes;
no HTTP, no timers, no mutation + Store-->>View: signal updates, re-render + Note over Reduce: the ONLY place state changes
no HTTP, no timers, no mutation ``` Side effects (HTTP) sit _outside_ this loop: a command does the I/O, then `dispatch`es a @@ -366,6 +368,22 @@ So it _feels_ like save-on-blur only because you usually stop typing when you le field, and the debounce fires ~600 ms later. The trigger is **"stopped changing," not "lost focus."** Submit is a separate, explicit action (§2d). +**The last-mile guard (leaving mid-debounce).** A debounce means an edit made in the final +<600 ms before you leave hasn't been written yet. Two seams close that window +([`pending-saves.ts`](../../../src/app/shared/application/pending-saves.ts)): every autosave +owner (the brief/org-template root stores and each wizard's `draft-sync`) registers in a +`PendingSaves` registry, and + +- **in-app navigation** — a `CanDeactivate` guard (`flushPendingGuard`, on the autosave + routes) flushes the pending write and _awaits_ it before the route changes, so the page + can't tear down with an unsaved keystroke; +- **hard close / reload** — a `beforeunload` handler fires the flush best-effort and triggers + the browser's native "unsaved changes" prompt. It is deliberately _not_ a guaranteed sync + save: the HTTP seam is Angular `HttpClient` (no `keepalive`/`sendBeacon`), so an async write + can't be promised to finish as the page unloads — the prompt lets the debounce land if the + user stays. The authoritative _submit_ path already force-flushes first, so only unsent + draft keystrokes are ever at risk. + --- ## 3. "Parse, don't validate" — value objects @@ -443,11 +461,14 @@ where the user left off. ```mermaid stateDiagram-v2 [*] --> Answering - Answering --> Answering: SetAnswer / Next / Back
(steps re-derived each time) - Answering --> Submitting: Submit (all answers valid) + Answering --> Answering: SetAnswer / Next / Back + Answering --> Submitting: Submit when all answers valid Submitting --> Submitted: ok Submitting --> Failed: error Failed --> Submitting: Retry + note right of Answering + steps re-derived each time + end note ``` See it live on `/concepts` (section 5) — the step list and the "stap N van M" counter