Rework backend #1
@@ -183,7 +183,7 @@ REWORK_PLAN.md.
|
|||||||
- [x] BUG-6: fixed structurally (1-list model)
|
- [x] BUG-6: fixed structurally (1-list model)
|
||||||
- [x] BUG-12: fixed — campaign selection follows activeDisplay
|
- [x] BUG-12: fixed — campaign selection follows activeDisplay
|
||||||
- [x] BUG-15: fixed — DisplayView no longer re-sorts (drag order preserved)
|
- [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-10: deact+reactivate double-act
|
||||||
- [ ] BUG-11: FE Combat.scenario crash
|
- [ ] BUG-11: FE Combat.scenario crash
|
||||||
- [ ] BUG-13: reorder cross-pointer semantics (RED + decide block/allow)
|
- [ ] BUG-13: reorder cross-pointer semantics (RED + decide block/allow)
|
||||||
|
|||||||
+5
-1
@@ -131,7 +131,11 @@ function createServer({ dbPath, port, corsOrigin } = {}) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
app, server, wss, store, db,
|
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 { createWsStorage } = require('../../src/storage/ws');
|
||||||
const { runStorageContract } = require('../../src/storage/contract');
|
const { runStorageContract } = require('../../src/storage/contract');
|
||||||
|
|
||||||
let nextPort = 4000 + Math.floor(Math.random() * 999);
|
|
||||||
|
|
||||||
// Factory: fresh backend (unique sqlite file) + storage pointed at it.
|
// Factory: fresh backend (unique sqlite file) + storage pointed at it.
|
||||||
// Disposing the storage closes the backend so each test is fully isolated.
|
// Disposing the storage closes the backend so each test is fully isolated.
|
||||||
async function makeStorage() {
|
async function makeStorage() {
|
||||||
const port = nextPort++;
|
const dbPath = path.join(os.tmpdir(), `ws-contract-${Date.now()}-${Math.random().toString(36).slice(2)}.sqlite`);
|
||||||
const dbPath = path.join(os.tmpdir(), `ws-contract-${port}-${Date.now()}.sqlite`);
|
const handle = createServer({ dbPath, port: 0 });
|
||||||
const handle = createServer({ dbPath, port });
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
handle.server.on('error', 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 baseUrl = `http://127.0.0.1:${port}`;
|
||||||
const wsUrl = `ws://127.0.0.1:${port}/ws`;
|
const wsUrl = `ws://127.0.0.1:${port}/ws`;
|
||||||
const storage = createWsStorage({ baseUrl, wsUrl });
|
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));
|
const flush = (ms = 150) => new Promise(r => setTimeout(r, ms));
|
||||||
|
|
||||||
let nextPort = 5000 + Math.floor(Math.random() * 999);
|
|
||||||
|
|
||||||
async function makeStorage() {
|
async function makeStorage() {
|
||||||
const port = nextPort++;
|
const dbPath = path.join(os.tmpdir(), `ws-recon-${Date.now()}-${Math.random().toString(36).slice(2)}.sqlite`);
|
||||||
const dbPath = path.join(os.tmpdir(), `ws-recon-${port}-${Date.now()}.sqlite`);
|
const handle = createServer({ dbPath, port: 0 });
|
||||||
const handle = createServer({ dbPath, port });
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
handle.server.on('error', 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 baseUrl = `http://127.0.0.1:${port}`;
|
||||||
const wsUrl = `ws://127.0.0.1:${port}/ws`;
|
const wsUrl = `ws://127.0.0.1:${port}/ws`;
|
||||||
const storage = createWsStorage({ baseUrl, wsUrl });
|
const storage = createWsStorage({ baseUrl, wsUrl });
|
||||||
|
|||||||
+47
-3
@@ -39,13 +39,51 @@ function createWsStorage({ baseUrl, wsUrl } = {}) {
|
|||||||
let ws = null;
|
let ws = null;
|
||||||
let wsReady = null;
|
let wsReady = null;
|
||||||
|
|
||||||
|
let disposed = false;
|
||||||
|
let reconnectTimer = null;
|
||||||
|
let everConnected = false;
|
||||||
|
const RECONNECT_DELAY = 500;
|
||||||
|
|
||||||
function ensureWs() {
|
function ensureWs() {
|
||||||
if (wsReady) return wsReady;
|
if (wsReady) return wsReady;
|
||||||
wsReady = new Promise((resolve, reject) => {
|
wsReady = new Promise((resolve, reject) => {
|
||||||
ws = new WebSocketImpl(WS);
|
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 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 onMessage = (ev) => {
|
||||||
const raw = typeof ev === 'string' ? ev : (ev.data !== undefined ? ev.data : 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; }
|
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); };
|
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,
|
_api: api,
|
||||||
_test: {
|
_test: {
|
||||||
|
|||||||
Reference in New Issue
Block a user