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:
@@ -20,11 +20,11 @@ TODO: custom condition field
|
||||
|
||||
|
||||
|
||||
ws = YOUR SQLite server. server/db.js = better-sqlite3. src/storage/ws.js = client that talks to it (REST +
|
||||
server = YOUR SQLite server. server/db.js = better-sqlite3. src/storage/server.js = client that talks to it (REST +
|
||||
websocket). Name bad. Should be "sqlite-adapter" or "server". You right.
|
||||
|
||||
memory = in-process JS Map. Useless? Mostly yes for prod. Value: fast contract tests, no server spinup. If
|
||||
you skip memory, contract test still runs vs firebase-mock + live ws-server. Memory = optional convenience.
|
||||
contract test runs vs firebase-mock + live server backend.
|
||||
|
||||
Memory = optional convenience. === extra complication.
|
||||
|
||||
@@ -136,7 +136,7 @@ Memory = optional convenience. === extra complication.
|
||||
`src/__mocks__/firebase/firestore.js` setDoc records call, IGNORES opts
|
||||
(no actual merge). Mock must simulate firebase merge semantics for test
|
||||
to pass. Fix = mock setDoc: if opts.merge, MOCK_DB.merge(path,data) else
|
||||
replace. OR change App.js setDoc(merge) → updateDoc (cleaner, ws adapter
|
||||
replace. OR change App.js setDoc(merge) → updateDoc (cleaner, server adapter
|
||||
uses PATCH). Decide which.
|
||||
- Test: render App + DisplayView, toggle hide-HP, assert display still shows
|
||||
encounter (not paused).
|
||||
@@ -166,8 +166,8 @@ Memory = optional convenience. === extra complication.
|
||||
- `reorderParticipants` returns `log: null`. Other ops return `log.undo`.
|
||||
- Cannot undo drag-drop. Candidate for undo system (M6).
|
||||
|
||||
### BUG-8: ws adapter has no reconnect
|
||||
- Test: `server/tests/ws-reconnect.test.js` (RED).
|
||||
### BUG-8: server adapter has no reconnect
|
||||
- Test: `server/tests/server-reconnect.test.js` (RED).
|
||||
- WS dies (idle/error/close) → `wsReady=null`, subscribers dead forever.
|
||||
- Display frozen until full reload.
|
||||
- Fix: `onclose` → reconnect + re-subscribe existing paths.
|
||||
@@ -245,7 +245,7 @@ Memory = optional convenience. === extra complication.
|
||||
- [x] BUG-6: fixed structurally (1-list model)
|
||||
- [x] BUG-12: fixed --- campaign selection follows activeDisplay
|
||||
- [x] BUG-15: fixed --- DisplayView no longer re-sorts (drag order preserved)
|
||||
- [x] BUG-8: ws adapter reconnect (implemented + GREEN)
|
||||
- [x] BUG-8: server adapter reconnect (implemented + GREEN)
|
||||
- [ ] BUG-10: deact+reactivate double-act
|
||||
- [ ] BUG-11: FE Combat.scenario crash
|
||||
- [ ] BUG-13: reorder cross-pointer semantics (RED + decide block/allow)
|
||||
|
||||
+6
-6
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ Manual demo tool. NOT test.
|
||||
|
||||
## replay-combat.js
|
||||
|
||||
Live backend demo. Drives full combat via ws adapter (same contract as App).
|
||||
Live backend demo. Drives full combat via server adapter (same contract as App).
|
||||
Player display live-updates. Watch UI react to state changes.
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start local dev stack: node backend (sqlite) + react frontend, ws storage mode.
|
||||
# Start local dev stack: node backend (sqlite) + react frontend, server storage mode.
|
||||
# Usage: ./scripts/dev-start.sh
|
||||
# Stop: ./scripts/dev-stop.sh
|
||||
set -euo pipefail
|
||||
@@ -25,12 +25,12 @@ else
|
||||
echo "backend already on :4001"
|
||||
fi
|
||||
|
||||
# frontend: ws storage, :3999
|
||||
# frontend: server storage, :3999
|
||||
if ! lsof -ti :3999 >/dev/null 2>&1; then
|
||||
echo "starting frontend :3999..."
|
||||
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 \
|
||||
nohup npm start > tmp/fe.log 2>&1 &
|
||||
echo $! > tmp/fe.pid
|
||||
|
||||
@@ -30,10 +30,10 @@ const {
|
||||
toggleParticipantActive, applyHpChange, deathSave,
|
||||
toggleCondition, reorderParticipants, endEncounter,
|
||||
} = shared;
|
||||
const { createWsStorage } = require('../src/storage/ws');
|
||||
const { createServerStorage } = require('../src/storage/server');
|
||||
|
||||
const BACKEND = process.env.BACKEND_URL || 'http://127.0.0.1:4001';
|
||||
const WS_URL = process.env.BACKEND_WS || BACKEND.replace(/^http/, 'ws') + '/ws';
|
||||
const WS_URL = process.env.BACKEND_REALTIME_URL || BACKEND.replace(/^http/, 'ws') + '/ws';
|
||||
const ROUNDS = parseInt(process.argv[2], 10) || 100;
|
||||
const DELAY = parseInt(process.argv[3], 10) || 200;
|
||||
|
||||
@@ -51,7 +51,7 @@ const getPath = {
|
||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
// Use the ADAPTER as the contract boundary (same as App). No raw REST.
|
||||
const storage = createWsStorage({ baseUrl: BACKEND, wsUrl: WS_URL });
|
||||
const storage = createServerStorage({ baseUrl: BACKEND, realtimeUrl: WS_URL });
|
||||
|
||||
// Mirror App.js CONDITIONS so we exercise all of them.
|
||||
const CONDITIONS = [
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
// logs/{id} doc
|
||||
//
|
||||
// No shape-specific tables. Data is opaque JSON. This is the firebase mirror:
|
||||
// the adapter (src/storage/ws.js) is a thin passthrough, app logic unchanged.
|
||||
// the adapter (src/storage/server.js) is a thin passthrough, app logic unchanged.
|
||||
|
||||
'use strict';
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// server/index.js — generic KV document store over HTTP + WebSocket.
|
||||
// firebase mirror: doc-tree model. Thin REST, path-based WS push.
|
||||
// Adapter (src/storage/ws.js) = passthrough, no shape translation.
|
||||
// Adapter (src/storage/server.js) = passthrough, no shape translation.
|
||||
|
||||
'use strict';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Layer 2 test: exercise ws.js storage adapter against a LIVE backend.
|
||||
// Layer 2 test: exercise server.js storage adapter against a LIVE backend.
|
||||
// Complements Layer 1 (App + firebase mock) which proves App call shape but
|
||||
// never touches ws.js. This catches translation bugs in the adapter.
|
||||
//
|
||||
// Runs the shared storage contract (same spec memory/firebase satisfy) against
|
||||
// createWsStorage pointed at an ephemeral backend instance. A FRESH backend is
|
||||
// createServerStorage pointed at an ephemeral backend instance. A FRESH backend is
|
||||
// spun up per test to guarantee isolation (backend has no reset endpoint yet).
|
||||
|
||||
'use strict';
|
||||
@@ -11,7 +11,7 @@
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { createServer } = require('../index');
|
||||
const { createWsStorage } = require('../../src/storage/ws');
|
||||
const { createServerStorage } = require('../../src/storage/server');
|
||||
const { runStorageContract } = require('../../src/storage/contract');
|
||||
|
||||
// Factory: fresh backend (unique sqlite file) + storage pointed at it.
|
||||
@@ -26,9 +26,9 @@ async function makeStorage() {
|
||||
const port = handle.server.address().port;
|
||||
const baseUrl = `http://127.0.0.1:${port}`;
|
||||
const wsUrl = `ws://127.0.0.1:${port}/ws`;
|
||||
const storage = createWsStorage({ baseUrl, wsUrl });
|
||||
const storage = createServerStorage({ baseUrl, realtimeUrl: wsUrl });
|
||||
storage.dispose = (done) => handle.close(done);
|
||||
return storage;
|
||||
}
|
||||
|
||||
runStorageContract('ws (live backend)', makeStorage);
|
||||
runStorageContract('server (live backend)', makeStorage);
|
||||
@@ -8,7 +8,7 @@
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { createServer } = require('../index');
|
||||
const { createWsStorage } = require('../../src/storage/ws');
|
||||
const { createServerStorage } = require('../../src/storage/server');
|
||||
|
||||
const flush = (ms = 150) => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
@@ -22,7 +22,7 @@ async function makeStorage() {
|
||||
const port = handle.server.address().port;
|
||||
const baseUrl = `http://127.0.0.1:${port}`;
|
||||
const wsUrl = `ws://127.0.0.1:${port}/ws`;
|
||||
const storage = createWsStorage({ baseUrl, wsUrl });
|
||||
const storage = createServerStorage({ baseUrl, realtimeUrl: wsUrl });
|
||||
storage.dispose = (done) => handle.close(done);
|
||||
return storage;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/storage/index.js — storage factory + SDK re-exports.
|
||||
// STORAGE=firebase (default): adapter wraps SDK. STORAGE=ws: backend.
|
||||
// STORAGE=firebase (default): adapter wraps SDK. STORAGE=server: backend.
|
||||
// App.js imports getStorage() for subscribe; still imports SDK pieces for writes (per-group refactor pending).
|
||||
|
||||
import { initializeApp } from 'firebase/app';
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
onSnapshot, updateDoc, deleteDoc, query, orderBy, limit, writeBatch,
|
||||
} from 'firebase/firestore';
|
||||
import { initFirebase, createFirebaseStorage } from './firebase';
|
||||
import { createWsStorage } from './ws';
|
||||
import { createServerStorage } from './server';
|
||||
|
||||
let storageInstance = null;
|
||||
|
||||
@@ -23,13 +23,13 @@ export function getStorage() {
|
||||
const ok = initFirebase();
|
||||
if (!ok) throw new Error('Firebase config missing. Check REACT_APP_FIREBASE_* env.');
|
||||
storageInstance = createFirebaseStorage();
|
||||
} else if (mode === 'ws') {
|
||||
storageInstance = createWsStorage({
|
||||
} else if (mode === 'server') {
|
||||
storageInstance = createServerStorage({
|
||||
baseUrl: process.env.REACT_APP_BACKEND_URL || '',
|
||||
wsUrl: process.env.REACT_APP_BACKEND_WS || '',
|
||||
realtimeUrl: process.env.REACT_APP_BACKEND_REALTIME_URL || '',
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Unknown REACT_APP_STORAGE mode: ${mode}. Use 'firebase' or 'ws'.`);
|
||||
throw new Error(`Unknown REACT_APP_STORAGE mode: ${mode}. Use 'firebase' or 'server'.`);
|
||||
}
|
||||
return storageInstance;
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@ if (typeof WebSocket !== 'undefined') {
|
||||
WebSocketImpl = WebSocket;
|
||||
}
|
||||
|
||||
function createWsStorage({ baseUrl, wsUrl } = {}) {
|
||||
function createServerStorage({ baseUrl, realtimeUrl } = {}) {
|
||||
// Same-origin by default: empty baseUrl = relative fetch (Caddy/proxy).
|
||||
// Fallback to localhost for bare `npm start` dev without proxy.
|
||||
const API = (baseUrl || (typeof window !== 'undefined' && window.location ? '' : 'http://127.0.0.1:4001')).replace(/\/$/, '');
|
||||
let WS;
|
||||
if (wsUrl) {
|
||||
WS = wsUrl;
|
||||
if (realtimeUrl) {
|
||||
WS = realtimeUrl;
|
||||
} else if (typeof window !== 'undefined' && window.location) {
|
||||
// derive from current origin (http→ws, https→wss), same host/port.
|
||||
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
@@ -259,4 +259,4 @@ function createWsStorage({ baseUrl, wsUrl } = {}) {
|
||||
return storage;
|
||||
}
|
||||
|
||||
export { createWsStorage };
|
||||
export { createServerStorage };
|
||||
@@ -1,6 +1,6 @@
|
||||
// Lock: storage adapters must use ESM exports (no module.exports).
|
||||
// Regression guard: CJS in src/ crashes CRA prod build (ESM strict).
|
||||
// Bug history: ws.js + firebase.js used module.exports. Dev lenient (masked),
|
||||
// Bug history: server.js + firebase.js used module.exports. Dev lenient (masked),
|
||||
// prod bundle crashed blank page. firebase.js always ESM.
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
@@ -8,7 +8,7 @@ import path from 'path';
|
||||
const ADAPTER_DIR = path.join(__dirname, '..', 'storage');
|
||||
|
||||
describe('storage adapters use ESM (no CJS)', () => {
|
||||
const adapters = ['ws.js', 'firebase.js', 'index.js'];
|
||||
const adapters = ['server.js', 'firebase.js', 'index.js'];
|
||||
test.each(adapters)('%s has no module.exports', (file) => {
|
||||
const full = fs.readFileSync(path.join(ADAPTER_DIR, file), 'utf8');
|
||||
// strip line comments so words like 'require' in explanatory comments don't trip the guard
|
||||
|
||||
@@ -27,10 +27,10 @@ describe('getStorageMode', () => {
|
||||
expect(getStorageMode()).toBe('firebase');
|
||||
});
|
||||
|
||||
test('returns ws when REACT_APP_STORAGE=ws', () => {
|
||||
process.env.REACT_APP_STORAGE = 'ws';
|
||||
test('returns server when REACT_APP_STORAGE=server', () => {
|
||||
process.env.REACT_APP_STORAGE = 'server';
|
||||
const { getStorageMode } = require('../storage/index');
|
||||
expect(getStorageMode()).toBe('ws');
|
||||
expect(getStorageMode()).toBe('server');
|
||||
});
|
||||
|
||||
test('throws on unknown mode', () => {
|
||||
@@ -41,8 +41,8 @@ describe('getStorageMode', () => {
|
||||
});
|
||||
|
||||
describe('getStorage factory routing', () => {
|
||||
test('ws mode returns ws adapter (init may fail without backend — catch)', () => {
|
||||
process.env.REACT_APP_STORAGE = 'ws';
|
||||
test('server mode returns server adapter (init may fail without backend — catch)', () => {
|
||||
process.env.REACT_APP_STORAGE = 'server';
|
||||
const { getStorage } = require('../storage/index');
|
||||
try {
|
||||
const s = getStorage();
|
||||
@@ -56,7 +56,7 @@ describe('getStorage factory routing', () => {
|
||||
});
|
||||
|
||||
test('returns singleton on repeat call (same instance)', () => {
|
||||
process.env.REACT_APP_STORAGE = 'ws';
|
||||
process.env.REACT_APP_STORAGE = 'server';
|
||||
const { getStorage } = require('../storage/index');
|
||||
let a;
|
||||
try { a = getStorage(); } catch (e) { return; } // no backend ok
|
||||
|
||||
Reference in New Issue
Block a user