refactor: replace websocket-stream with native ws utility
Remove the `websocket-stream` dependency in favor of `createWebSocketStream` from the `ws` package. This simplifies the MQTT broker setup in the journal server and removes unnecessary transitive dependencies. Also improve the JournalPanel UI with smoother transitions and better visibility handling.
This commit is contained in:
parent
34d647d611
commit
98c4a195e8
|
|
@ -29,7 +29,6 @@
|
||||||
"solid-js": "^1.9.3",
|
"solid-js": "^1.9.3",
|
||||||
"three": "^0.183.2",
|
"three": "^0.183.2",
|
||||||
"three-3mf-exporter": "^45.1.0",
|
"three-3mf-exporter": "^45.1.0",
|
||||||
"websocket-stream": "^5.5.2",
|
|
||||||
"ws": "^8.21.0",
|
"ws": "^8.21.0",
|
||||||
"yarn-spinner-loader": "^0.1.0",
|
"yarn-spinner-loader": "^0.1.0",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
|
|
@ -3724,12 +3723,6 @@
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/async-limiter": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
|
@ -5267,18 +5260,6 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/duplexify": {
|
|
||||||
"version": "3.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
|
|
||||||
"integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"end-of-stream": "^1.0.0",
|
|
||||||
"inherits": "^2.0.1",
|
|
||||||
"readable-stream": "^2.0.0",
|
|
||||||
"stream-shift": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.331",
|
"version": "1.5.331",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz",
|
||||||
|
|
@ -5306,15 +5287,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/end-of-stream": {
|
|
||||||
"version": "1.4.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
|
||||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"once": "^1.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.20.1",
|
"version": "5.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
|
||||||
|
|
@ -8838,6 +8810,7 @@
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
|
|
@ -9783,12 +9756,6 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stream-shift": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
|
@ -10297,12 +10264,6 @@
|
||||||
"node": ">=0.8.0"
|
"node": ">=0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ultron": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
|
@ -10586,31 +10547,6 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/websocket-stream": {
|
|
||||||
"version": "5.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/websocket-stream/-/websocket-stream-5.5.2.tgz",
|
|
||||||
"integrity": "sha512-8z49MKIHbGk3C4HtuHWDtYX8mYej1wWabjthC/RupM9ngeukU4IWoM46dgth1UOS/T4/IqgEdCDJuMe2039OQQ==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"duplexify": "^3.5.1",
|
|
||||||
"inherits": "^2.0.1",
|
|
||||||
"readable-stream": "^2.3.3",
|
|
||||||
"safe-buffer": "^5.1.2",
|
|
||||||
"ws": "^3.2.0",
|
|
||||||
"xtend": "^4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/websocket-stream/node_modules/ws": {
|
|
||||||
"version": "3.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
|
|
||||||
"integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"async-limiter": "~1.0.0",
|
|
||||||
"safe-buffer": "~5.1.0",
|
|
||||||
"ultron": "~1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/whatwg-encoding": {
|
"node_modules/whatwg-encoding": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
||||||
|
|
@ -10741,6 +10677,7 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/write-file-atomic": {
|
"node_modules/write-file-atomic": {
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@
|
||||||
"solid-js": "^1.9.3",
|
"solid-js": "^1.9.3",
|
||||||
"three": "^0.183.2",
|
"three": "^0.183.2",
|
||||||
"three-3mf-exporter": "^45.1.0",
|
"three-3mf-exporter": "^45.1.0",
|
||||||
"websocket-stream": "^5.5.2",
|
|
||||||
"ws": "^8.21.0",
|
"ws": "^8.21.0",
|
||||||
"yarn-spinner-loader": "^0.1.0",
|
"yarn-spinner-loader": "^0.1.0",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@
|
||||||
* Journal stream persistence — MQTT broker + JSONL append + session manifest
|
* Journal stream persistence — MQTT broker + JSONL append + session manifest
|
||||||
*
|
*
|
||||||
* Runs an aedes MQTT broker over WebSocket, attached to the existing
|
* Runs an aedes MQTT broker over WebSocket, attached to the existing
|
||||||
* HTTP server via the upgrade event. No separate TCP port.
|
* HTTP server. Uses ws v8's built-in createWebSocketStream.
|
||||||
|
*
|
||||||
|
* Pattern from aedes docs:
|
||||||
|
* https://github.com/moscajs/aedes#mqtt-server-over-websocket
|
||||||
*
|
*
|
||||||
* Persistence uses aedes's internal subscribe/publish API, so the
|
* Persistence uses aedes's internal subscribe/publish API, so the
|
||||||
* server process doesn't need to connect to itself as an MQTT client.
|
* server process doesn't need to connect to itself as an MQTT client.
|
||||||
|
|
@ -20,7 +23,7 @@ import {
|
||||||
} from "fs";
|
} from "fs";
|
||||||
import type { Server as HttpServer } from "http";
|
import type { Server as HttpServer } from "http";
|
||||||
import type { AedesPublishPacket } from "aedes";
|
import type { AedesPublishPacket } from "aedes";
|
||||||
import websocketStream from "websocket-stream";
|
import { WebSocketServer, createWebSocketStream } from "ws";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Types
|
// Types
|
||||||
|
|
@ -101,18 +104,21 @@ export async function createJournalServer(
|
||||||
const { Aedes: AedesFactory } = await import("aedes");
|
const { Aedes: AedesFactory } = await import("aedes");
|
||||||
const broker = new AedesFactory();
|
const broker = new AedesFactory();
|
||||||
|
|
||||||
// ---- WebSocket server (attached to HTTP server) ----
|
// ---- WebSocket server (attached to the existing HTTP server) ----
|
||||||
const { WebSocketServer } = await import("ws");
|
// Pattern from aedes docs: pass { server } to WSS, use 'connection' event
|
||||||
const wsServer = new WebSocketServer({ noServer: true });
|
const wss = new WebSocketServer({ server: httpServer });
|
||||||
|
|
||||||
httpServer.on("upgrade", (req, socket, head) => {
|
wss.on("connection", (ws, req) => {
|
||||||
wsServer.handleUpgrade(req, socket, head, (ws) => {
|
console.log(`[journal] ws connected from ${req.socket.remoteAddress}`);
|
||||||
// websocket-stream's types reference an older ws version; cast works.
|
const stream = createWebSocketStream(ws);
|
||||||
const stream = websocketStream(
|
broker.handle(stream, req);
|
||||||
ws as unknown as Parameters<typeof websocketStream>[0],
|
});
|
||||||
);
|
|
||||||
broker.handle(stream);
|
broker.on("client", (client: import("aedes").Client) => {
|
||||||
});
|
console.log(`[journal] mqtt client ready: ${client.id}`);
|
||||||
|
});
|
||||||
|
broker.on("clientDisconnect", (client: import("aedes").Client) => {
|
||||||
|
console.log(`[journal] mqtt client disconnected: ${client.id}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- Persistence (internal subscribe, no separate MQTT client) ----
|
// ---- Persistence (internal subscribe, no separate MQTT client) ----
|
||||||
|
|
@ -149,10 +155,10 @@ export async function createJournalServer(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
broker,
|
broker,
|
||||||
wsServer,
|
wsServer: wss,
|
||||||
async close() {
|
async close() {
|
||||||
console.log("[journal] shutting down...");
|
console.log("[journal] shutting down...");
|
||||||
await new Promise<void>((r) => wsServer.close(() => r()));
|
await new Promise<void>((r) => wss.close(() => r()));
|
||||||
await new Promise<void>((r) => broker.close(() => r()));
|
await new Promise<void>((r) => broker.close(() => r()));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -25,17 +25,24 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
|
||||||
const stream = useJournalStream();
|
const stream = useJournalStream();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={props.open}>
|
<>
|
||||||
|
{/* Backdrop — hidden on desktop since panel doesn't overlay content */}
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 bg-black/30 z-40 md:bg-transparent"
|
class="fixed inset-0 bg-black/30 z-40 md:hidden"
|
||||||
|
classList={{ hidden: !props.open }}
|
||||||
onClick={props.onClose}
|
onClick={props.onClose}
|
||||||
/>
|
/>
|
||||||
|
{/* Panel — always mounted for exit animation, visibility toggled */}
|
||||||
<aside
|
<aside
|
||||||
class="fixed top-16 right-0 bottom-0 z-50 bg-white border-l border-gray-200
|
class={`fixed top-16 right-0 bottom-0 z-50 bg-white border-l border-gray-200
|
||||||
flex flex-col w-full max-w-md md:w-[420px] shadow-lg"
|
flex flex-col w-full max-w-md md:w-[420px] shadow-lg
|
||||||
|
transition-transform duration-300 ease-in-out ${
|
||||||
|
props.open ? "translate-x-0" : "translate-x-full"
|
||||||
|
}`}
|
||||||
|
aria-hidden={!props.open}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200">
|
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 shrink-0">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h2 class="text-sm font-semibold text-gray-700">Journal</h2>
|
<h2 class="text-sm font-semibold text-gray-700">Journal</h2>
|
||||||
<span
|
<span
|
||||||
|
|
@ -52,16 +59,17 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={props.onClose}
|
onClick={props.onClose}
|
||||||
class="text-gray-400 hover:text-gray-600 text-sm"
|
class="text-gray-400 hover:text-gray-600 text-sm px-1"
|
||||||
|
title="Hide panel"
|
||||||
>
|
>
|
||||||
✕
|
▸
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
when={stream.connected}
|
when={stream.connected}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="flex-1 flex items-center justify-center p-4">
|
<div class="flex-1 flex items-center justify-center p-4 overflow-y-auto">
|
||||||
<ConnectDialog />
|
<ConnectDialog />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -73,7 +81,7 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
|
||||||
<ComposePanel />
|
<ComposePanel />
|
||||||
</Show>
|
</Show>
|
||||||
</aside>
|
</aside>
|
||||||
</Show>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -198,77 +198,82 @@ export async function connectStream(
|
||||||
setState("connectionStatus", "connecting");
|
setState("connectionStatus", "connecting");
|
||||||
setState("connectionError", null);
|
setState("connectionError", null);
|
||||||
|
|
||||||
const client = mqtt.connect(brokerUrl, {
|
return new Promise<void>((resolve, reject) => {
|
||||||
clientId: `${state.myName}-${Date.now()}`,
|
const client = mqtt.connect(brokerUrl, {
|
||||||
protocol: brokerUrl.startsWith("wss") ? "wss" : "ws",
|
clientId: `${state.myName}-${Date.now()}`,
|
||||||
reconnectPeriod: 2000,
|
protocol: brokerUrl.startsWith("wss") ? "wss" : "ws",
|
||||||
});
|
reconnectPeriod: 2000,
|
||||||
|
|
||||||
_mqttClient = client;
|
|
||||||
|
|
||||||
client.on("connect", () => {
|
|
||||||
_mqttConnected = true;
|
|
||||||
setState("connected", true);
|
|
||||||
setState("connectionStatus", "connected");
|
|
||||||
setState("connectionError", null);
|
|
||||||
setState("brokerUrl", brokerUrl);
|
|
||||||
|
|
||||||
// Persist connection info for next time
|
|
||||||
if (typeof localStorage !== "undefined") {
|
|
||||||
localStorage.setItem(LS_BROKER_URL, brokerUrl);
|
|
||||||
localStorage.setItem(LS_LAST_SESSION, sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
client.subscribe(`ttrpg/${sessionId}/stream`, { qos: 1 }, (err) => {
|
|
||||||
if (err) console.error("[stream] stream sub err:", err);
|
|
||||||
});
|
});
|
||||||
client.subscribe($SESSIONS, { qos: 1 }, (err) => {
|
|
||||||
if (err) console.error("[stream] sessions sub err:", err);
|
_mqttClient = client;
|
||||||
|
|
||||||
|
client.on("connect", () => {
|
||||||
|
_mqttConnected = true;
|
||||||
|
setState("connected", true);
|
||||||
|
setState("connectionStatus", "connected");
|
||||||
|
setState("connectionError", null);
|
||||||
|
setState("brokerUrl", brokerUrl);
|
||||||
|
|
||||||
|
// Persist connection info for next time
|
||||||
|
if (typeof localStorage !== "undefined") {
|
||||||
|
localStorage.setItem(LS_BROKER_URL, brokerUrl);
|
||||||
|
localStorage.setItem(LS_LAST_SESSION, sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
client.subscribe(`ttrpg/${sessionId}/stream`, { qos: 1 }, (err) => {
|
||||||
|
if (err) console.error("[stream] stream sub err:", err);
|
||||||
|
});
|
||||||
|
client.subscribe($SESSIONS, { qos: 1 }, (err) => {
|
||||||
|
if (err) console.error("[stream] sessions sub err:", err);
|
||||||
|
});
|
||||||
|
client.subscribe(`ttrpg/${sessionId}/meta`, { qos: 1 });
|
||||||
|
|
||||||
|
resolve();
|
||||||
});
|
});
|
||||||
client.subscribe(`ttrpg/${sessionId}/meta`, { qos: 1 });
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("message", (topic, payload) => {
|
client.on("error", (err) => {
|
||||||
const raw = payload.toString();
|
console.error("[stream] mqtt error:", err);
|
||||||
const parts = topic.split("/");
|
setState("connectionStatus", "error");
|
||||||
|
setState("connectionError", err.message);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
if (topic === $SESSIONS) {
|
client.on("close", () => {
|
||||||
// Session manifest update
|
_mqttConnected = false;
|
||||||
try {
|
setState("connected", false);
|
||||||
const manifest: SessionManifest = JSON.parse(raw);
|
setState("connectionStatus", "disconnected");
|
||||||
setSessionList(manifest);
|
});
|
||||||
} catch (e) {
|
|
||||||
console.error("[stream] manifest parse err:", e);
|
client.on("message", (topic, payload) => {
|
||||||
|
const raw = payload.toString();
|
||||||
|
const parts = topic.split("/");
|
||||||
|
|
||||||
|
if (topic === $SESSIONS) {
|
||||||
|
// Session manifest update
|
||||||
|
try {
|
||||||
|
const manifest: SessionManifest = JSON.parse(raw);
|
||||||
|
setSessionList(manifest);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[stream] manifest parse err:", e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts.length >= 3 && parts[0] === "ttrpg" && parts[2] === "meta") {
|
if (parts.length >= 3 && parts[0] === "ttrpg" && parts[2] === "meta") {
|
||||||
// Session meta update — manifest will be republished by server,
|
// Session meta update — manifest will be republished by server,
|
||||||
// handled via $SESSIONS subscription above.
|
// handled via $SESSIONS subscription above.
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (parts.length >= 3 && parts[0] === "ttrpg" && parts[2] === "stream") {
|
|
||||||
try {
|
|
||||||
const msg: StreamMessage = JSON.parse(raw);
|
|
||||||
receiveMessage(msg);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[stream] malformed message:", e);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("close", () => {
|
if (parts.length >= 3 && parts[0] === "ttrpg" && parts[2] === "stream") {
|
||||||
_mqttConnected = false;
|
try {
|
||||||
setState("connected", false);
|
const msg: StreamMessage = JSON.parse(raw);
|
||||||
setState("connectionStatus", "disconnected");
|
receiveMessage(msg);
|
||||||
});
|
} catch (e) {
|
||||||
|
console.error("[stream] malformed message:", e);
|
||||||
client.on("error", (err) => {
|
}
|
||||||
console.error("[stream] mqtt error:", err);
|
}
|
||||||
setState("connectionStatus", "error");
|
});
|
||||||
setState("connectionError", err.message);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue