diff --git a/src/storage/ws.js b/src/storage/ws.js index 92b8b35..070bec0 100644 --- a/src/storage/ws.js +++ b/src/storage/ws.js @@ -3,26 +3,12 @@ 'use strict'; -// Use native browser WebSocket when available (production). Fallback to the -// `ws` npm package in Node/jest where global WebSocket is absent. -let WebSocketImpl; -if (typeof WebSocket !== 'undefined') { - WebSocketImpl = WebSocket; -} else { - // require inside else so webpack ignores it in browser bundle - WebSocketImpl = require('ws').WebSocket; -} +const { WebSocket } = require('ws'); function createWsStorage({ baseUrl, wsUrl } = {}) { const API = (baseUrl || 'http://127.0.0.1:4001').replace(/\/$/, ''); const WS = wsUrl || (API.replace(/^http/, 'ws') + '/ws'); - // App.js passes firebase-prefixed paths: artifacts/{APP_ID}/public/data/campaigns/... - // Backend uses canonical: campaigns/... Strip the prefix so all matchers work. - function norm(p) { - return p.replace(/^[\s\S]*\/public\/data\//, ''); - } - const docSubs = new Map(); // path -> Set const collSubs = new Map(); // collPath -> Set let ws = null; @@ -32,28 +18,14 @@ function createWsStorage({ baseUrl, wsUrl } = {}) { function ensureWs() { if (wsReady) return wsReady; wsReady = new Promise((resolve, reject) => { - ws = new WebSocketImpl(WS); - // addEventListener works on both browser WebSocket and Node ws pkg. - const onOpen = () => resolve(ws); - const onError = (err) => { wsReady = null; reject(err instanceof Event ? new Error('ws error') : err); }; - const onClose = () => { wsReady = null; }; - 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; } + ws = new WebSocket(WS); + ws.on('open', () => resolve(ws)); + ws.on('error', (err) => { wsReady = null; reject(err); }); + ws.on('close', () => { wsReady = null; }); + ws.on('message', (raw) => { + let msg; try { msg = JSON.parse(raw.toString()); } catch { return; } handleMessage(msg); - }; - // browser-style property handlers - ws.onopen = onOpen; - ws.onerror = onError; - ws.onclose = onClose; - ws.onmessage = onMessage; - // Node ws-style addEventListener fallback (noop in browser if absent) - if (typeof ws.addEventListener === 'function') { - ws.addEventListener('open', onOpen); - ws.addEventListener('error', onError); - ws.addEventListener('close', onClose); - ws.addEventListener('message', onMessage); - } + }); }); return wsReady; } @@ -62,16 +34,14 @@ function createWsStorage({ baseUrl, wsUrl } = {}) { async function handleMessage(msg) { if (msg.type !== 'change' || !msg.change) return; const c = msg.change; - // Notify doc subscribers whose normalized path we cached. - for (const [rawPath, cbs] of docSubs) { - const path = norm(rawPath); + // Notify doc subscribers whose path we cached. + for (const [path, cbs] of docSubs) { if (pathMatchesChange(path, c)) { const doc = await storage.getDoc(path); cbs.forEach(cb => cb(doc)); } } - for (const [rawCollPath, cbs] of collSubs) { - const collPath = norm(rawCollPath); + for (const [collPath, cbs] of collSubs) { if (collMatchesChange(collPath, c)) { const docs = await storage.getCollection(collPath); cbs.forEach(cb => cb(docs)); @@ -113,8 +83,8 @@ function createWsStorage({ baseUrl, wsUrl } = {}) { const storage = { // --- reads --- - async getDoc(rawPath) { - const path = norm(rawPath); + async getDoc(path) { + // canonical paths used by App.js if (path === 'activeDisplay/status') { const ad = await api('GET', '/api/activeDisplay'); return ad; @@ -132,8 +102,7 @@ function createWsStorage({ baseUrl, wsUrl } = {}) { return null; }, - async getCollection(rawCollPath) { - const collPath = norm(rawCollPath); + async getCollection(collPath) { if (collPath === 'campaigns') return await api('GET', '/api/campaigns'); const m = collPath.match(/^campaigns\/([^/]+)\/encounters$/); if (m) return await api('GET', `/api/campaigns/${m[1]}/encounters`); @@ -142,8 +111,7 @@ function createWsStorage({ baseUrl, wsUrl } = {}) { }, // --- writes (translated to backend action endpoints) --- - async setDoc(rawPath, data) { - const path = norm(rawPath); + async setDoc(path, data) { // activeDisplay merges if (path === 'activeDisplay/status') { if ('activeCampaignId' in data || 'activeEncounterId' in data) { @@ -167,8 +135,7 @@ function createWsStorage({ baseUrl, wsUrl } = {}) { } }, - async updateDoc(rawPath, patch) { - const path = norm(rawPath); + async updateDoc(path, patch) { const cm = path.match(/^campaigns\/([^/]+)$/); if (cm) { if (Array.isArray(patch.players)) { @@ -192,16 +159,14 @@ function createWsStorage({ baseUrl, wsUrl } = {}) { } }, - async deleteDoc(rawPath) { - const path = norm(rawPath); + async deleteDoc(path) { const cm = path.match(/^campaigns\/([^/]+)$/); if (cm) { await api('DELETE', `/api/campaigns/${cm[1]}`); return; } const em = path.match(/^campaigns\/([^/]+)\/encounters\/([^/]+)$/); if (em) { await api('DELETE', `/api/campaigns/${em[1]}/encounters/${em[2]}`); return; } }, - async addDoc(rawCollPath, data) { - const collPath = norm(rawCollPath); + async addDoc(collPath, data) { if (collPath === 'logs') { // backend auto-logs; direct insert not needed return { id: 'auto', path: 'logs/auto' }; @@ -216,8 +181,7 @@ function createWsStorage({ baseUrl, wsUrl } = {}) { } }, - subscribeDoc(rawPath, cb) { - const path = norm(rawPath); + subscribeDoc(path, cb) { ensureWs().then(() => { // subscribe to coarse change types that could affect this path const types = changeTypesForDocPath(path); @@ -230,8 +194,7 @@ function createWsStorage({ baseUrl, wsUrl } = {}) { return () => { docSubs.get(path)?.delete(cb); }; }, - subscribeCollection(rawCollPath, cb) { - const collPath = norm(rawCollPath); + subscribeCollection(collPath, cb) { ensureWs().then(() => { const types = changeTypesForCollPath(collPath); types.forEach(t => ws.send(JSON.stringify({ type: 'subscribe', key: t }))); @@ -250,15 +213,13 @@ function createWsStorage({ baseUrl, wsUrl } = {}) { return storage; } -function changeTypesForDocPath(rawPath) { - const path = rawPath.replace(/^[\s\S]*\/public\/data\//, ''); +function changeTypesForDocPath(path) { if (path === 'activeDisplay/status') return ['activeDisplay']; if (path.match(/^campaigns\/[^/]+\/encounters\//)) return ['encounter', 'activeDisplay']; if (path.match(/^campaigns\//)) return ['campaign', 'campaigns']; return []; } -function changeTypesForCollPath(rawCollPath) { - const collPath = rawCollPath.replace(/^[\s\S]*\/public\/data\//, ''); +function changeTypesForCollPath(collPath) { if (collPath === 'campaigns') return ['campaigns']; if (collPath.match(/^campaigns\/[^/]+\/encounters$/)) return ['encounters']; if (collPath === 'logs') return ['logs'];