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:
2026-07-06 15:53:52 +08:00
parent 34d647d611
commit 98c4a195e8
5 changed files with 108 additions and 153 deletions
+17 -9
View File
@@ -25,17 +25,24 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
const stream = useJournalStream();
return (
<Show when={props.open}>
<>
{/* Backdrop — hidden on desktop since panel doesn't overlay content */}
<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}
/>
{/* Panel — always mounted for exit animation, visibility toggled */}
<aside
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"
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
transition-transform duration-300 ease-in-out ${
props.open ? "translate-x-0" : "translate-x-full"
}`}
aria-hidden={!props.open}
>
{/* 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">
<h2 class="text-sm font-semibold text-gray-700">Journal</h2>
<span
@@ -52,16 +59,17 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
</div>
<button
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>
</div>
<Show
when={stream.connected}
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 />
</div>
}
@@ -73,7 +81,7 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
<ComposePanel />
</Show>
</aside>
</Show>
</>
);
};
+68 -63
View File
@@ -198,77 +198,82 @@ export async function connectStream(
setState("connectionStatus", "connecting");
setState("connectionError", null);
const client = mqtt.connect(brokerUrl, {
clientId: `${state.myName}-${Date.now()}`,
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);
return new Promise<void>((resolve, reject) => {
const client = mqtt.connect(brokerUrl, {
clientId: `${state.myName}-${Date.now()}`,
protocol: brokerUrl.startsWith("wss") ? "wss" : "ws",
reconnectPeriod: 2000,
});
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) => {
const raw = payload.toString();
const parts = topic.split("/");
client.on("error", (err) => {
console.error("[stream] mqtt error:", err);
setState("connectionStatus", "error");
setState("connectionError", err.message);
reject(err);
});
if (topic === $SESSIONS) {
// Session manifest update
try {
const manifest: SessionManifest = JSON.parse(raw);
setSessionList(manifest);
} catch (e) {
console.error("[stream] manifest parse err:", e);
client.on("close", () => {
_mqttConnected = false;
setState("connected", false);
setState("connectionStatus", "disconnected");
});
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") {
// Session meta update — manifest will be republished by server,
// handled via $SESSIONS subscription above.
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);
if (parts.length >= 3 && parts[0] === "ttrpg" && parts[2] === "meta") {
// Session meta update — manifest will be republished by server,
// handled via $SESSIONS subscription above.
return;
}
}
});
client.on("close", () => {
_mqttConnected = false;
setState("connected", false);
setState("connectionStatus", "disconnected");
});
client.on("error", (err) => {
console.error("[stream] mqtt error:", err);
setState("connectionStatus", "error");
setState("connectionError", err.message);
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);
}
}
});
});
}