refactor: rename ws adapter to server across codebase

'ws' conflated storage mode with transport protocol (WebSocket). Renamed for
clarity: the adapter talks the self-hosted server (src/storage/server.js), which
happens to use WebSocket for realtime — but the name should describe what it
connects to, not how.

Renames:
- src/storage/ws.js -> server.js
- createWsStorage() -> createServerStorage()
- storage mode 'ws' -> 'server'
- REACT_APP_BACKEND_WS -> REACT_APP_BACKEND_REALTIME_URL
- factory param wsUrl -> realtimeUrl
- server/tests/ws-*.test.js -> server-*.test.js

Kept literal: 'ws' npm package, ws:// protocol URLs, /ws WebSocket endpoint.
Transport is accurate there; only storage-naming changed.

Updated all docs (DEVELOPMENT, TESTING, REWORK_PLAN, GLOSSARY, TODO), scripts
(dev-start.sh, replay-combat.js), factory + ESM tests. 204 tests green.
This commit is contained in:
david raistrick
2026-07-04 15:47:13 -04:00
parent cb41d9ec8f
commit e94e1959ff
16 changed files with 64 additions and 65 deletions
+6 -6
View File
@@ -65,12 +65,12 @@ curl http://127.0.0.1:4001/health # -> {"ok":true}
Never put db in `/tmp` (wipe risk). Use `./data/` (gitignored) or docker volume.
### Frontend (dev server, ws mode)
### Frontend (dev server, server mode)
```bash
REACT_APP_STORAGE=ws \
REACT_APP_STORAGE=server \
REACT_APP_BACKEND_URL=http://127.0.0.1:4001 \
REACT_APP_BACKEND_WS=ws://127.0.0.1:4001/ws \
REACT_APP_BACKEND_REALTIME_URL=ws://127.0.0.1:4001/ws \
BROWSER=none PORT=3999 \
npm start
```
@@ -225,12 +225,12 @@ App passes firebase-prefixed paths (`artifacts/{APP_ID}/public/data/campaigns/..
`getStorageMode()` reads `REACT_APP_STORAGE` env (default `firebase`).
- `firebase` → real SDK init
- `ws`/`memory` → stub auth + db sentinel, route via `storage.*` adapter
- `server` → stub auth + db sentinel, route via `storage.*` adapter
## Test layers
- **Layer 1**: App vs firebase mock. Proves adapter call shape. Never exercises ws adapter.
- **Layer 2**: ws adapter vs live backend. Proves translation + path identity.
- **Layer 1**: App vs firebase mock. Proves adapter call shape. Never exercises server adapter.
- **Layer 2**: server adapter vs live backend. Proves translation + path identity.
Both required — Layer 1 alone misses adapter bugs (path mismatch, no-op players, ws.on EventEmitter vs browser handlers).
+2 -2
View File
@@ -52,8 +52,8 @@ Round 2: turn 1 (Fighter) → ... → turn 8 (Merchant) [round counter +=1 at
| Term | Meaning |
|------|---------|
| **Adapter** | `src/storage/{firebase,ws,memory}.js`. Contract boundary between App and backend. App only calls `storage.*`, never raw SDK/fetch. |
| **Adapter** | `src/storage/{firebase,server}.js`. Contract boundary between App and backend. App only calls `storage.*`, never raw SDK/fetch. |
| **Path normalization** (`norm()`) | Strip firebase prefix (`artifacts/{APP_ID}/public/data/`) → bare canonical path (`campaigns/X`). Runs inside every adapter method. |
| **Generic KV doc store** | Backend stores opaque JSON at arbitrary path strings. No shape-specific endpoints. Backend = firebase mirror, not REST API for app entities. |
| **Layer 1 test** | App vs firebase mock. Proves adapter call shape. |
| **Layer 2 test** | ws adapter vs live backend. Proves translation + path identity. |
| **Layer 2 test** | server adapter vs live backend. Proves translation + path identity. |
+10 -10
View File
@@ -61,7 +61,7 @@ The storage interface is the test seam and the upstream-compat layer.
| Impl | When used | Automated-tested? |
|---|---|---|
| `firebase.js` | default (`STORAGE=firebase`) — upstream path | No — requires live Firebase project |
| `ws.js` | `STORAGE=ws` — our fork, talks to backend | Yes — against running backend |
| `server.js` | `STORAGE=server` — our fork, talks to backend | Yes — against running backend |
| `memory.js` | test-only, in-process | Yes — fast, deterministic |
**Frontend interface contract** (all three implement):
@@ -82,7 +82,7 @@ Memory impl: in-memory Map + EventEmitter, for tests (M3).
storage/
index.js # factory: pick impl from STORAGE env
firebase.js # extracted from current App.js (verbatim)
ws.js # NEW — talks to backend
server.js # talks to backend (was ws.js)
memory.js # NEW — test only
contract.js # interface spec (runStorageContract)
tests/ # frontend tests
@@ -116,7 +116,7 @@ Each milestone = independently mergeable PR upstream (unless marked ❌).
| M | Does | Tests? |
|---|---|---|
| 0 | repo, branch, remotes | no |
| 1 | build backend (Node+Express+ws+better-sqlite3) | unit tests as built |
| 1 | build backend (Node+Express+WebSocket+better-sqlite3) | unit tests as built |
| 2 | frontend WS adapter — app runs vs backend, cross-device works | yes |
| 3 | characterization tests lock current behavior | yes |
| 4 | resolve initiative rotation corruption (BUG-5) | yes |
@@ -135,7 +135,7 @@ Each milestone = independently mergeable PR upstream (unless marked ❌).
- **Upstream-PRable:** n/a (fork infra)
### Milestone 1 — Build backend ✅
- `server/`: Express + ws + better-sqlite3.
- `server/`: Express + WebSocket + better-sqlite3.
- Generic KV doc store (firebase mirror): `docs` table (path PK, parent, data JSON, updated_at). REST: GET/PUT/PATCH/DELETE `/api/doc?path=`, GET `/api/collection?path=`, POST `/api/collection`, POST `/api/batch`. WS: subscribe by path.
- Server holds authoritative state. No turn logic server-side (logic stays client-side in `shared/turn.js`).
- **Exit criteria:** backend boots, serves state over WS, persists to SQLite, unit tests green. ✅ DONE.
@@ -144,10 +144,10 @@ Each milestone = independently mergeable PR upstream (unless marked ❌).
### Milestone 2 — Frontend WS adapter ✅
- Define `storage/contract.js` interface spec.
- Move all Firestore call sites from `App.js` into `storage/firebase.js` behind interface (verbatim).
- Implement `storage/ws.js` per interface, talking to backend. Generic KV ops, subscribes to WS.
- Implement `storage/server.js` per interface, talking to backend. Generic KV ops, subscribes via WebSocket.
- Implement `storage/memory.js` for frontend unit tests.
- `storage/index.js` factory: `STORAGE` env → pick impl. Default `firebase` (upstream unchanged).
- App runs against backend with `STORAGE=ws`.
- App runs against backend with `STORAGE=server`.
- Cross-device verified manually: DM view + player display + tablet.
- **Exit criteria:** app runs fully against local backend, no Firebase. Multi-device sync works. ✅ DONE.
- **Upstream-PRable:** ⚠️ partial. Storage interface + firebase extract = ✅. WS impl = ❌.
@@ -155,7 +155,7 @@ Each milestone = independently mergeable PR upstream (unless marked ❌).
### Milestone 3 — Characterization tests lock current behavior ✅
- Lock current behavior via tests.
- Cover: START, NEXT_TURN, PAUSE, RESUME, ADD_PARTICIPANT, REMOVE_PARTICIPANT, TOGGLE_ACTIVE, REORDER, APPLY_DAMAGE/HEAL, DEATH_SAVE, END.
- Two layers: Layer 1 (App + firebase mock, proves call shape), Layer 2 (ws adapter vs live backend, proves translation).
- Two layers: Layer 1 (App + firebase mock, proves call shape), Layer 2 (server adapter vs live backend, proves translation).
- Iterate until confident: baseline solid, regressions impossible to silently slip.
- **Exit criteria:** characterization suite green. Baseline locked. ✅ DONE.
- **Upstream-PRable:** ✅ if kept storage-agnostic (tests target turn logic shape).
@@ -173,7 +173,7 @@ Each milestone = independently mergeable PR upstream (unless marked ❌).
- Single container: caddy (front, static + proxy) + node backend (internal :4001).
- Files in `docker/` tree (kept separate from upstream root Dockerfile):
- `docker/Dockerfile` — build FE + BE, runtime caddy+node
- `docker/Caddyfile` — proxy /api + /ws to node, static SPA fallback
- `docker/Caddyfile` — proxy /api + /ws (WebSocket path) to node, static SPA fallback
- `docker/entrypoint.sh` — node bg + caddy fg
- `docker/docker-compose.yml` — one `app` service, volume for sqlite
- Run: `docker compose -f docker/docker-compose.yml up --build` (or `cd docker && docker compose up --build`). Port 8080.
@@ -214,7 +214,7 @@ Each milestone = independently mergeable PR upstream (unless marked ❌).
### Manual smoke via config flags
- `STORAGE=firebase` → current behavior (friend's path, upstream default).
- `STORAGE=ws` → our path, local backend.
- `STORAGE=server` → our path, local backend.
- docker-compose profiles mirror the above.
### Accepted test gap
@@ -261,6 +261,6 @@ Default `STORAGE=firebase` + `AUTH_MODE=none` (unset) = upstream sees literally
- M0 ✅, M1 ✅, M2 ✅, M3 ✅, M4 ✅, M5 ✅
- Backend live: port 4001, db `./data/tracker.sqlite`
- Frontend: port 3999 with `REACT_APP_STORAGE=ws`
- Frontend: port 3999 with `REACT_APP_STORAGE=server`
- Test suite: ~160 tests (shared + server + FE). Bugs tracked in `TODO.md`.
- Next milestones: M5 docker-compose. Undo moved to TODO backlog.
+5 -6
View File
@@ -194,11 +194,11 @@ curl http://127.0.0.1:4001/health # → {"ok":true}
Never db in `/tmp` (wipe risk). Use `./data/` (gitignored) or docker volume.
### Frontend (ws mode)
### Frontend (server mode)
```bash
REACT_APP_STORAGE=ws \
REACT_APP_STORAGE=server \
REACT_APP_BACKEND_URL=http://127.0.0.1:4001 \
REACT_APP_BACKEND_WS=ws://127.0.0.1:4001/ws \
REACT_APP_BACKEND_REALTIME_URL=ws://127.0.0.1:4001/ws \
BROWSER=none PORT=3999 \
npm start
```
@@ -210,10 +210,9 @@ Firebase mode (default): set `REACT_APP_FIREBASE_*` in `.env.local` (copy `env.e
`STORAGE_MODE = getStorageMode()` reads `REACT_APP_STORAGE`:
- `firebase` (default) → real SDK
- `ws` → backend (docker/prod)
- `memory` → in-process (test seed)
- `server` → backend (docker/prod)
All adapters ESM. Adapter contract: `src/storage/contract.js` — same spec vs memory/ws/firebase.
All adapters ESM. Adapter contract: `src/storage/contract.js` — same spec vs server/firebase.
## Known RED / backlog