Rework backend #1
@@ -183,7 +183,7 @@ REWORK_PLAN.md.
|
||||
- [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)
|
||||
- [ ] BUG-8: ws adapter reconnect
|
||||
- [x] BUG-8: ws 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)
|
||||
|
||||
+5
-1
@@ -131,7 +131,11 @@ function createServer({ dbPath, port, corsOrigin } = {}) {
|
||||
|
||||
return {
|
||||
app, server, wss, store, db,
|
||||
close(done) { wss.close(); server.close(() => { db.close(); if (done) done(); }); },
|
||||
close(done) {
|
||||
wss.clients.forEach(c => { try { c.terminate(); } catch {} });
|
||||
wss.close();
|
||||
server.close(() => { db.close(); if (done) done(); });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,18 +14,16 @@ const { createServer } = require('../index');
|
||||
const { createWsStorage } = require('../../src/storage/ws');
|
||||
const { runStorageContract } = require('../../src/storage/contract');
|
||||
|
||||
let nextPort = 4000 + Math.floor(Math.random() * 999);
|
||||
|
||||
// Factory: fresh backend (unique sqlite file) + storage pointed at it.
|
||||
// Disposing the storage closes the backend so each test is fully isolated.
|
||||
async function makeStorage() {
|
||||
const port = nextPort++;
|
||||
const dbPath = path.join(os.tmpdir(), `ws-contract-${port}-${Date.now()}.sqlite`);
|
||||
const handle = createServer({ dbPath, port });
|
||||
const dbPath = path.join(os.tmpdir(), `ws-contract-${Date.now()}-${Math.random().toString(36).slice(2)}.sqlite`);
|
||||
const handle = createServer({ dbPath, port: 0 });
|
||||
await new Promise((resolve, reject) => {
|
||||
handle.server.on('error', reject);
|
||||
handle.server.listen(port, resolve);
|
||||
handle.server.listen(0, resolve);
|
||||
});
|
||||
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 });
|
||||
|
||||
@@ -12,16 +12,14 @@ const { createWsStorage } = require('../../src/storage/ws');
|
||||
|
||||
const flush = (ms = 150) => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
let nextPort = 5000 + Math.floor(Math.random() * 999);
|
||||
|
||||
async function makeStorage() {
|
||||
const port = nextPort++;
|
||||
const dbPath = path.join(os.tmpdir(), `ws-recon-${port}-${Date.now()}.sqlite`);
|
||||
const handle = createServer({ dbPath, port });
|
||||
const dbPath = path.join(os.tmpdir(), `ws-recon-${Date.now()}-${Math.random().toString(36).slice(2)}.sqlite`);
|
||||
const handle = createServer({ dbPath, port: 0 });
|
||||
await new Promise((resolve, reject) => {
|
||||
handle.server.on('error', reject);
|
||||
handle.server.listen(port, resolve);
|
||||
handle.server.listen(0, resolve);
|
||||
});
|
||||
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 });
|
||||
|
||||
+47
-3
@@ -39,13 +39,51 @@ function createWsStorage({ baseUrl, wsUrl } = {}) {
|
||||
let ws = null;
|
||||
let wsReady = null;
|
||||
|
||||
let disposed = false;
|
||||
let reconnectTimer = null;
|
||||
let everConnected = false;
|
||||
const RECONNECT_DELAY = 500;
|
||||
|
||||
function ensureWs() {
|
||||
if (wsReady) return wsReady;
|
||||
wsReady = new Promise((resolve, reject) => {
|
||||
ws = new WebSocketImpl(WS);
|
||||
const onOpen = () => resolve(ws);
|
||||
const onOpen = () => {
|
||||
const isReconnect = everConnected;
|
||||
everConnected = true;
|
||||
// resubscribe all existing subscribers after (re)connect
|
||||
for (const p of docSubs.keys()) {
|
||||
ws.send(JSON.stringify({ type: 'subscribe', kind: 'doc', path: p }));
|
||||
}
|
||||
for (const p of collSubs.keys()) {
|
||||
ws.send(JSON.stringify({ type: 'subscribe', kind: 'collection', path: p }));
|
||||
}
|
||||
// On RECONNECT only: re-fetch current values — catches writes that
|
||||
// happened while disconnected (broadcast missed). Skip on first connect
|
||||
// (initial REST fetch in subscribeDoc/subscribeCollection already did).
|
||||
if (isReconnect) {
|
||||
for (const [p, cbs] of docSubs) {
|
||||
storage.getDoc(p).then(doc => { cbs.forEach(cb => cb(doc)); }).catch(() => {});
|
||||
}
|
||||
for (const [p, cbs] of collSubs) {
|
||||
storage.getCollection(p).then(docs => { cbs.forEach(cb => cb(docs)); }).catch(() => {});
|
||||
}
|
||||
}
|
||||
resolve(ws);
|
||||
};
|
||||
const onError = (err) => { wsReady = null; reject(err instanceof Event ? new Error('ws error') : err); };
|
||||
const onClose = () => { wsReady = null; };
|
||||
const onClose = () => {
|
||||
wsReady = null;
|
||||
ws = null;
|
||||
if (disposed) return;
|
||||
// auto-reconnect (BUG-8): try again after delay. ensureWs() re-arms.
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null;
|
||||
if (!disposed) ensureWs().catch(() => {});
|
||||
}, RECONNECT_DELAY);
|
||||
if (reconnectTimer && typeof reconnectTimer.unref === 'function') reconnectTimer.unref();
|
||||
};
|
||||
const onMessage = (ev) => {
|
||||
const raw = typeof ev === 'string' ? ev : (ev.data !== undefined ? ev.data : ev);
|
||||
let msg; try { msg = JSON.parse(typeof raw === 'string' ? raw : raw.toString()); } catch { return; }
|
||||
@@ -168,7 +206,13 @@ function createWsStorage({ baseUrl, wsUrl } = {}) {
|
||||
return () => { collSubs.get(p)?.delete(cb); };
|
||||
},
|
||||
|
||||
dispose() { if (ws) ws.close(); docSubs.clear(); collSubs.clear(); },
|
||||
dispose(cb) {
|
||||
disposed = true;
|
||||
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
||||
if (ws) ws.close();
|
||||
docSubs.clear(); collSubs.clear();
|
||||
if (typeof cb === 'function') cb();
|
||||
},
|
||||
|
||||
_api: api,
|
||||
_test: {
|
||||
|
||||
Reference in New Issue
Block a user