14. Domain categories — src/lib/client/store/store.tsx
Source path: knowledge-base/knowledge/architecture/persistence/client-and-edge-cases.md
# 14. Domain categories — `src/lib/client/store/store.tsx` Three sets, exported for UI differentiation: - `DOMAIN_PLAYGROUND_IDS` — bundled `public/playgrounds` corpus. - `DOMAIN_WORK_IDS` — bundled `public/works` corpus. - `DOMAIN_USER_GENERATED_IDS` — created at runtime (analysis, wizard, extensions) = the complement of the two bundled sets. Both bundled categories prepopulate from `/public` on hydration. `DELETE_DOMAIN` treats both bundled categories as reset-not-deleted (they re-seed); only USER_GENERATED is truly removable. # 15. Game guards — `src/lib/core/game/guards.ts` While a Scenario game is live, structural edits that would corrupt the board are hard-blocked **in the reducer** and surfaced with a toast (`GAME_LOCK_MESSAGE`): - **Branch-scoped** (`isBranchGameLocked`): `DELETE_SCENE`, branch-entry removal, `CREATE_MERGE`/`REVERT_MERGE`, `REMOVE_STREAM` — locked on a branch with a live game. - **Domain-scoped** (`hasActiveGame`): `UPSERT_PERSPECTIVE` — perspectives are shared across branches and a live board renders them to players, so they're locked on **any** active game. The game's own RESOLVE generation runs through `useScenario` (not these guards). Abandoning the game (`clearGame` → `REMOVE_GAME_ROOM`) is **not** guarded — it's the escape hatch and unlocks everything. **Admins get equal control:** `isGM` is `actAsSeatId === null` (not sync-role), so a client in the domain interface gets the full Director view and can abandon the game; control actions flow to the master like any edit. Game *hosting* (the Scenario broker) stays master-only (`useScenarioLiveHost`). # 16. Edge cases handled | Edge case | Handling | |---|---| | Concurrent edits to different domains/branches | Master applies each delta onto canonical state, serialized — all land | | Concurrent edits to the *same scene* | Last applied wins; the loser adopts canonical (toast "Updated by host") | | Action no longer applicable (edits a deleted scene) | Master catches the throw → `reject` → client refreshes from canonical | | Stale document write to the daemon | `writeDocument` forward-time guard → **409**; the SSE echo converges the cache (Part A §3) | | Stale-base write with a *fresh* clock (replica missed a headless commit, then edited) | `writeDocument` collection-regression guard → **409** when it would drop a populated collection; the catch-up re-pulls the fuller record copy (Part A §2) | | No master connected | Pushes get 503 → "Host offline" toast; reads still work from the local replica | | Master restart (broker globalThis reset) | Broker backfills its shape; the client's SSE drops → lock releases event-driven, then re-`pull`s on reconnect | | Client loses connection mid-push | SSE `onerror` unlocks immediately and `requestReconcile`s the pending domain — no waiting on a timer | | Two localhost master tabs | Broker `displaced` frame puts the old tab inert (read-only) so it can't double-publish | | Tunnel stopped | `disconnectAllClients` tells clients to disconnect and clears the roster | | Far-out-of-date client | Every `entries` frame re-pulls exactly the stamped-stale domains (version ledger, §13) — reconnects clear the per-connection request guard so lost requests retry | | Client boots before its replica decrypts | Storm guard defers the reconcile; the warm-ledger replay re-runs it against the buffered latest entries (§13) | | Aggregate view opens before its member Domains / sync handshake exist | Replica read-through queues until connection + identity, coalesces each Domain pull, and resolves from the authorized canonical frame | | Headless commit while every window was CLOSED | The version-gated sweep (`catchUpEntries`, Part A §5) re-pulls held docs the record moved past, on the next boot/reconnect | | Headless commit to a background world | `saveDomainProjection` for every adopted domain (Part A §5) + `relayHeadlessDomain` (§11) land it in IndexedDB and on clients | | Client-created assets | Uploaded to the master before the referencing domain is pushed (no dangling ref) | | Large embedding sets over the tunnel | Binary encoding + one bulk request + yielded master encoding | | Asset bytes overwritten in place | Avoided by content-addressing; `meridians:asset-changed` re-resolves consumers | # 17. Toasts — feedback discipline Toasts are the user-facing signal for every sync/lock action. The store fires `meridians:toast` window events (it sits above `ToastProvider`); the provider renders them bottom-right with solid backgrounds. They use the shared communications vocabulary in `src/lib/core/comms/signal.ts`, so the toast, console line, system log, and persisted API record describe an event in the same language. Its axes are orthogonal: **tone** controls urgency/colour; **register** says what kind of communication this is; **failure tier** names ownership; actor + operation code + HTTP status supply provenance. - **Confirmed** (normally `success`) — an action landed. - **Intel** (normally `info`) — neutral state or reporting; no response is required. - **Advisory** (normally `warning`) — a user-correctable condition, guard, or fallback. - **Failure tier** (`error`, or a persistent warning such as Billing) — `Our bug`, `Provider`, `Billing`, or `Unclassified`. Every red failure must carry a tier and a greppable operation `code`; use `Unclassified` when the boundary cannot decide blame rather than omitting the tag. Add a detail line that says what remains safe and what the operator should do next. Input validation is Advisory, not a bug. Social relays carry the actor and suppress the machine register. Semantically identical on-screen messages dedupe; rich reporting and classified issues stay until dismissed. Rich cards expose a **Copy report** action containing the visible register or tier, operation code, HTTP status, headline, and next step. Register may be overridden independently of tone when meaning and urgency differ. Dedupe includes ownership, source, status, and actor, so equal headlines from distinct incidents remain distinct. - **Client:** Syncing… / Saved ✓ / Hold on — syncing your last change / Host offline — change not saved / Host didn't confirm / Couldn't apply — refreshing / Updated by host / Host stopped hosting / Host offline — start Meridians on the host machine. - **Master:** Client connected — N online / Client left — N online / All clients disconnected / Hosting moved to another tab. - **Both:** `GAME_LOCK_MESSAGE` whenever a guarded structural edit is blocked. # 18. Known limitations / future - **Permissions:** clients are full admins today. Tightening is a small set of gates (`isGM`, the generate/host controls, the reducer guards). - **Same-domain same-field concurrency** resolves last-writer-wins (rare for admins). - **Scene generation into an existing domain** is master-only (bursts don't fit the one-in-flight lock); analysis/wizard/extension (whole-domain creates) work from clients. - **Timing constants** live in one place, `src/lib/server/sync/constants.ts`, with a header that states the rule: time-based handling is kept only where it's *inherent* — debounces coalesce publish bursts, network timeouts bound a wait when no event can report the answer. Everything else (lock release, reconnect catch-up, echo suppression, the projection echo) is event- or version-driven. - **Asset relay timeouts** are bounded (`ASSET_PULL_TIMEOUT_MS` / `ASSET_UPLOAD_TIMEOUT_MS`); a stalled master degrades to partial data + self-heal, never a hang. - **Production build recommended for tunnel testing** — `next dev`'s on-demand route compilation is slow over a tunnel (`allowedDevOrigins` is set for ngrok to keep HMR from blocking, but pre-compiled `next start` is the real fix).Open on GitHub
Raw Markdown source