14. Domain categories — src/lib/client/store/store.tsx
From the Meridians Wiki · Public · Maintained · joint
Three sets, exported for UI differentiation:
DOMAIN_PLAYGROUND_IDS— bundledpublic/playgroundscorpus.DOMAIN_WORK_IDS— bundledpublic/workscorpus.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-pulls on reconnect |
| Client loses connection mid-push | SSE onerror unlocks immediately and requestReconciles 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, orUnclassified. Every red failure must carry a tier and a greppable operationcode; useUnclassifiedwhen 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_MESSAGEwhenever 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 (allowedDevOriginsis set for ngrok to keep HMR from blocking, but pre-compilednext startis the real fix).