cleanup testing utilities, documentation, and AI commentary

This commit is contained in:
seavor 2026-04-18 15:32:50 -05:00
parent bd2382c94e
commit ef6cea6f6c
150 changed files with 891 additions and 1233 deletions

View file

@ -0,0 +1,70 @@
# Webatrice architecture diagrams
Three views of the same architecture at different zoom levels. The `.mmd` files are the source of truth — edit them and re-render when the architecture changes. The `.png` files are committed so the README and GitHub file view render everywhere, including offline.
For the prose counterpart and conventions, see [../.github/instructions/webclient.instructions.md](../../.github/instructions/webclient.instructions.md) (the canonical AI-tool instruction surface for this package).
## When to look at which
| Diagram | Use it when |
|---|---|
| **[simple](simple.mmd)** | You need a mental model of the request/response loop in ten seconds. Good for onboarding. |
| **[detailed](detailed.mmd)** | You're making a structural change and want to see every module, which layer it belongs to, and how data moves between them. |
| **[flow](flow.mmd)** | You're debugging a specific round-trip and need to see the runtime order. Shows the `cmdId`-correlated response path vs. the extension-dispatched event path, plus the "no timeout, no retry" caveat. |
## Simple — high-level flow
![Simple architecture](simple.png)
Application on the left, Servatrice on the right, two-lane racetrack in between. The top lane is outbound (`client.request.*``Commands`), the bottom lane is inbound (`Events · Responses``client.response.*`), and both lanes ride the same WebSocket. Redux hangs off Application as its in-memory store; IndexedDB sits under Servatrice as the browser-side persistent store reached from hooks via Dexie. Both are stores, both sit outside the racetrack.
**Color = role:**
- Blue — application code (UI, hooks, API seams, WebClient)
- Purple — transport (WebSocket layer, services)
- Amber — state / data stores (Redux, protocol types)
- Gray — external systems (Servatrice, IndexedDB)
## Detailed — layers & dependencies
![Detailed architecture](detailed.png)
Every meaningful module in the webclient, arranged as a three-lane racetrack: outbound (`src/api/request/``commands/`) on top, transport (`WebClientProvider``WebClient``services/`) in the middle, inbound (`events/``src/api/response/`) on the bottom. Application bookend on the left holds UI, hooks, Redux store, and the Dexie persistence pair; Servatrice sits on the right. The protocol satellite (`src/types/` + `src/generated/proto/`) is drawn below with dashed edges up to the modules it types — it's cross-cutting, not on the flow path. Same four-role palette as the simple diagram.
Load-bearing invariants (enforced on `webclient-websocket-layer`; keep it that way):
- **UI never imports `@app/websocket` or `@app/api`** — always go through `useWebClient()`.
- **Only `src/types/` imports from `@app/generated`** — everywhere else uses `Data` / `Enriched` / `App`.
- **Only `*.dispatch.ts` helpers and `*ResponseImpl` classes call `store.dispatch`** — the API response layer is the single inbound seam into Redux.
## Flow — command → response → event round-trip
![Sequence: join room](flow.png)
Scenario: user joins a room. The sequence shows the outbound command path (steps 16), the correlated response path matched by `cmdId` in `ProtobufService`'s pending map (steps 710), and an unsolicited server event dispatched by proto-extension match against the event registry in `processRoomEvent` / `processSessionEvent` / `processGameEvent` (steps 1115).
Read the footnote: `ProtobufService` has no timeout and no retry, and `resetCommands()` on reconnect silently drops in-flight callbacks. Code that needs reconnection resilience has to handle it at a higher layer.
## Rendering
npm scripts are defined in [../package.json](../package.json) — no separate build step, no added runtime dependency (everything runs via `npx`).
```bash
# from the webclient/ directory:
npm run diagram # render all three (simple + detailed + flow)
npm run diagram:simple # render just simple.png
npm run diagram:detailed # render just detailed.png
npm run diagram:flow # render just flow.png
```
Under the hood each command is:
```bash
npx -y -p @mermaid-js/mermaid-cli -p puppeteer mmdc \
-i architecture/<name>.mmd -o architecture/<name>.png -b white -s 2
```
`-s 2` renders at 2× scale so the PNG stays crisp on high-DPI displays; `-b white` gives the diagrams a light-mode background that looks right in both GitHub's light and dark themes.
If `mmdc` fails locally (it spawns headless Chromium — some sandboxed environments block that), paste the `.mmd` contents into [mermaid.live](https://mermaid.live) and export to PNG. The `.mmd` sources remain canonical either way.

View file

@ -0,0 +1,123 @@
---
config:
layout: elk
theme: base
themeVariables:
background: "#ffffff"
primaryColor: "#ffffff"
primaryBorderColor: "#1f2937"
primaryTextColor: "#0b1220"
lineColor: "#1f2937"
textColor: "#0b1220"
edgeLabelBackground: "#ffffff"
fontSize: "20px"
clusterBkg: "#fafafa"
clusterBorder: "#9ca3af"
flowchart:
htmlLabels: true
curve: basis
nodeSpacing: 60
rankSpacing: 90
---
flowchart LR
%% =========================================================
%% Left bookend — browser-side Application
%% =========================================================
subgraph LBE["<b>Application</b>"]
direction TB
UI["<b>UI</b><br/><span style='font-size:15px'>containers · components<br/>forms · dialogs</span>"]
Hooks["<b>hooks/</b><br/><span style='font-size:15px'>useWebClient · useAutoLogin<br/>useSettings · useKnownHosts</span>"]
Store[("<b>@app/store</b><br/><span style='font-size:15px'>server · rooms · game<br/>actions · common</span>")]
DTOs["<b>dexie DTOs</b><br/><span style='font-size:15px'>Card · Host · Set<br/>Setting · Token</span>"]
IDB[("<b>IndexedDB</b>")]
end
%% =========================================================
%% Racetrack — three lanes: outbound / transport / inbound
%% =========================================================
subgraph RACE[" "]
direction TB
subgraph TOP["<b>Outbound lane</b>"]
direction LR
Req["<b>src/api/request/</b><br/><span style='font-size:15px'>Authentication · Session · Rooms<br/>Game · Admin · Moderator</span>"]
Cmds["<b>commands/</b><br/><span style='font-size:15px'>session · room · game<br/>admin · moderator</span>"]
end
subgraph MID["<b>Transport</b>"]
direction LR
Provider["<b>WebClientProvider</b>"]
WC[["<b>WebClient</b><br/><span style='font-size:15px'>singleton · request · response</span>"]]
Svc["<b>services/</b><br/><span style='font-size:15px'>ProtobufService · WebSocketService<br/>KeepAliveService · command-options</span>"]
end
subgraph BOT["<b>Inbound lane</b>"]
direction LR
Evts["<b>events/</b><br/><span style='font-size:15px'>session · room · game</span>"]
Res["<b>src/api/response/</b><br/><span style='font-size:15px'>Session · Room · Game<br/>Admin · Moderator</span>"]
end
end
%% =========================================================
%% Right bookend — Servatrice
%% =========================================================
Srv[("<b>Servatrice</b>")]
%% =========================================================
%% Protocol satellite — cross-cutting types
%% =========================================================
subgraph PROTO["<b>Protocol (cross-cutting)</b>"]
direction LR
Types["<b>src/types/</b><br/><span style='font-size:15px'>Data · Enriched · App</span>"]
Gen["<b>src/generated/proto/</b><br/><span style='font-size:15px'>@bufbuild/protobuf</span>"]
end
%% =========================================================
%% UI-side wiring
%% =========================================================
UI --> Hooks
Hooks -- "useWebClient()" --> Provider
Provider --> WC
UI -- "selectors (read)" --> Store
Hooks --> DTOs
DTOs <--> IDB
%% =========================================================
%% Outbound — request goes up through the top lane to Srv
%% =========================================================
WC --> Req
Req --> Cmds
Cmds --> Svc
Svc -- "frames" --> Srv
%% =========================================================
%% Inbound — Srv comes back through services, splits to
%% cmdId response (direct) and event-registry dispatch
%% =========================================================
Srv -- "frames" --> Svc
Svc --> Evts
Svc -- "response by cmdId" --> Res
Evts --> Res
Res -- "dispatch" --> Store
%% =========================================================
%% Protocol edges — dashed, cross-cutting
%% =========================================================
Req -.-> Types
Res -.-> Types
Cmds -.-> Types
Evts -.-> Types
Types --> Gen
%% =========================================================
%% Palette — four roles
%% =========================================================
classDef app fill:#dbeafe,stroke:#1f2937,stroke-width:1.5px,color:#0b1220
classDef transport fill:#ede9fe,stroke:#1f2937,stroke-width:1.5px,color:#0b1220
classDef store fill:#fde68a,stroke:#1f2937,stroke-width:1.5px,color:#0b1220
classDef external fill:#e5e7eb,stroke:#1f2937,stroke-width:1.5px,color:#0b1220
class UI,Hooks,DTOs,Provider,WC app
class Req,Cmds,Svc,Evts,Res transport
class Store,Types,Gen store
class Srv,IDB external

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View file

@ -0,0 +1,41 @@
%%{init: {"theme": "base", "themeVariables": {"background": "#ffffff", "primaryColor": "#ffffff", "primaryBorderColor": "#1f2937", "primaryTextColor": "#0b1220", "lineColor": "#1f2937", "textColor": "#0b1220", "signalColor": "#1f2937", "signalTextColor": "#0b1220", "actorBkg": "#ffedd5", "actorBorder": "#1f2937", "actorTextColor": "#0b1220", "actorLineColor": "#1f2937", "sequenceNumberColor": "#ffffff", "noteBkgColor": "#fef3c7", "noteBorderColor": "#1f2937", "noteTextColor": "#0b1220", "labelBoxBkgColor": "#ffffff", "labelTextColor": "#0b1220"}, "sequence": {"showSequenceNumbers": true, "mirrorActors": false, "messageFontSize": "13px", "actorFontSize": "13px", "noteFontSize": "12px", "actorMargin": 14, "boxMargin": 6, "boxTextMargin": 3, "noteMargin": 8, "messageMargin": 48, "diagramMarginX": 8, "diagramMarginY": 8, "width": 120}}}%%
sequenceDiagram
autonumber
participant C as Application
participant RQ as API Request
participant CMD as Command
participant PB as Protobuf
participant WS as WebSocket
participant S as Servatrice
participant EV as Event
participant RS as API Response
rect rgb(219, 234, 254)
Note over C,S: Request — user action → command
C->>RQ: joinRoom(roomId)
RQ->>CMD: build RoomCommand
CMD->>PB: sendRoomCommand(cmd, onSuccess/onError)
PB->>PB: assign cmdId,<br/>register callback
PB->>WS: send(bytes)
WS->>S: Command
end
rect rgb(254, 243, 199)
Note over S,RS: Response — correlated by cmdId
S-->>WS: Response (cmdId)
WS-->>PB: processServerResponse
PB->>RS: onSuccess(response)
RS->>RS: dispatch → Redux
end
rect rgb(220, 252, 231)
Note over S,RS: Event — no cmdId — dispatched by extension
S-->>WS: Event
WS-->>PB: processRoomEvent
PB->>EV: pick handler by extension
EV->>RS: roomEvent(...)
RS->>RS: dispatch → Redux
end
Note over PB,WS: No timeout, no retry.<br/>resetCommands() on reconnect<br/>silently drops pending callbacks.

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View file

@ -0,0 +1,58 @@
---
config:
theme: base
themeVariables:
background: "#ffffff"
primaryColor: "#ffffff"
primaryBorderColor: "#1f2937"
primaryTextColor: "#0b1220"
lineColor: "#1f2937"
textColor: "#0b1220"
edgeLabelBackground: "#ffffff"
fontSize: "18px"
clusterBkg: "#ffffff"
clusterBorder: "#9ca3af"
flowchart:
htmlLabels: true
curve: basis
nodeSpacing: 55
rankSpacing: 90
---
flowchart LR
subgraph APP_COL[" "]
direction TB
App["<b>Application</b><br/><span style='font-size:13px'>containers · components · hooks</span>"]
Rdx[("<b>Redux</b><br/><span style='font-size:12px'>in-memory state</span>")]
end
subgraph SRV_COL[" "]
direction TB
Srv[("<b>Servatrice</b>")]
IDB[("<b>IndexedDB</b><br/><span style='font-size:12px'>local persistent store</span>")]
end
Req["client.request"]
Res["client.response"]
%% Outbound lane (top)
App -- "useWebClient()" --> Req
Req -- "Commands" --> Srv
%% Inbound lane (bottom)
Srv -- "Events · Responses" --> Res
Res -- "dispatch · rerender" --> App
%% Local stores — Application owns both; edges only to IndexedDB
%% (Redux state is implicit — reducers sit under dispatch, selectors under rerender)
App -. "Dexie: settings · hosts · cards" .-> IDB
%% Palette — four roles
classDef app fill:#dbeafe,stroke:#1f2937,stroke-width:1.5px,color:#0b1220
classDef seam fill:#dbeafe,stroke:#1f2937,stroke-width:1.5px,color:#0b1220
classDef store fill:#fde68a,stroke:#1f2937,stroke-width:1.5px,color:#0b1220
classDef external fill:#e5e7eb,stroke:#1f2937,stroke-width:1.5px,color:#0b1220
class App app
class Req,Res seam
class Rdx store
class Srv,IDB external

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB