feat: implement websocket-stream for journal server
Integrate `websocket-stream` to handle MQTT broker upgrades in the CLI and add granular connection status tracking to the UI. - Add `websocket-stream` dependency - Update CLI to use `websocket-stream` for socket upgrades - Enhance `journalStream` store with `connectionStatus` and `connectionError` states - Add visual connection status indicators to `App` and `JournalPanel`
This commit is contained in:
+14
-2
@@ -1,5 +1,6 @@
|
||||
import { Component, createEffect, createMemo, createSignal } from "solid-js";
|
||||
import { useLocation } from "@solidjs/router";
|
||||
import { useJournalStream } from "./components/stores/journalStream";
|
||||
|
||||
// 导入组件以注册自定义元素
|
||||
import "./components";
|
||||
@@ -15,6 +16,7 @@ import { JournalPanel } from "./components/journal";
|
||||
|
||||
const App: Component = () => {
|
||||
const location = useLocation();
|
||||
const stream = useJournalStream();
|
||||
const [isSidebarOpen, setIsSidebarOpen] = createSignal(false);
|
||||
const [isDocOpen, setIsDocOpen] = createSignal(false);
|
||||
const [isDataSourceOpen, setIsDataSourceOpen] = createSignal(false);
|
||||
@@ -84,13 +86,23 @@ const App: Component = () => {
|
||||
<div class="flex-1" />
|
||||
<button
|
||||
onClick={() => setIsJournalOpen((v) => !v)}
|
||||
class={`w-8 h-8 rounded flex items-center justify-center text-sm ${
|
||||
class={`relative w-8 h-8 rounded flex items-center justify-center text-sm ${
|
||||
isJournalOpen()
|
||||
? "bg-blue-100 text-blue-600"
|
||||
: "text-gray-500 hover:bg-gray-100"
|
||||
}`}
|
||||
title="Journal"
|
||||
title={`Journal (${stream.connectionStatus})`}
|
||||
>
|
||||
<span
|
||||
class="absolute top-0.5 right-0.5 w-2 h-2 rounded-full border border-white"
|
||||
classList={{
|
||||
"bg-gray-400": stream.connectionStatus === "disconnected",
|
||||
"bg-yellow-400 animate-pulse":
|
||||
stream.connectionStatus === "connecting",
|
||||
"bg-green-500": stream.connectionStatus === "connected",
|
||||
"bg-red-500": stream.connectionStatus === "error",
|
||||
}}
|
||||
/>
|
||||
📋
|
||||
</button>
|
||||
<button
|
||||
|
||||
+6
-1
@@ -20,6 +20,7 @@ import {
|
||||
} from "fs";
|
||||
import type { Server as HttpServer } from "http";
|
||||
import type { AedesPublishPacket } from "aedes";
|
||||
import websocketStream from "websocket-stream";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -106,7 +107,11 @@ export async function createJournalServer(
|
||||
|
||||
httpServer.on("upgrade", (req, socket, head) => {
|
||||
wsServer.handleUpgrade(req, socket, head, (ws) => {
|
||||
broker.handle(ws as unknown as import("net").Socket);
|
||||
// websocket-stream's types reference an older ws version; cast works.
|
||||
const stream = websocketStream(
|
||||
ws as unknown as Parameters<typeof websocketStream>[0],
|
||||
);
|
||||
broker.handle(stream);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -38,12 +38,17 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
|
||||
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="text-sm font-semibold text-gray-700">Journal</h2>
|
||||
<Show when={stream.connected}>
|
||||
<span
|
||||
class="w-2 h-2 rounded-full bg-green-500"
|
||||
title="Connected"
|
||||
/>
|
||||
</Show>
|
||||
<span
|
||||
class="w-2 h-2 rounded-full"
|
||||
classList={{
|
||||
"bg-gray-400": stream.connectionStatus === "disconnected",
|
||||
"bg-yellow-400 animate-pulse":
|
||||
stream.connectionStatus === "connecting",
|
||||
"bg-green-500": stream.connectionStatus === "connected",
|
||||
"bg-red-500": stream.connectionStatus === "error",
|
||||
}}
|
||||
title={stream.connectionStatus}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={props.onClose}
|
||||
@@ -104,7 +109,10 @@ const ConnectDialog: Component = () => {
|
||||
await hydrateFromServer(sessionId);
|
||||
await connectStream(sessionId, brokerUrl);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Connection failed");
|
||||
const msg = e instanceof Error ? e.message : "Connection failed";
|
||||
setError(msg);
|
||||
// If connectStream threw, the error handler also sets connectionStatus to error.
|
||||
// Make sure the error message is captured.
|
||||
} finally {
|
||||
setConnecting(false);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ export interface JournalStreamState {
|
||||
revealedPaths: Set<string>;
|
||||
/** MQTT connection status */
|
||||
connected: boolean;
|
||||
/** Granular connection state for UI indicators */
|
||||
connectionStatus: "disconnected" | "connecting" | "connected" | "error";
|
||||
/** Last connection error message, if any */
|
||||
connectionError: string | null;
|
||||
/** This client's identity */
|
||||
myName: string;
|
||||
/** Broker URL, set after connect */
|
||||
@@ -84,6 +88,8 @@ const [state, setState] = createStore<JournalStreamState>({
|
||||
senderSeq: {},
|
||||
revealedPaths: new Set(),
|
||||
connected: false,
|
||||
connectionStatus: "disconnected",
|
||||
connectionError: null,
|
||||
myName: persisted.myName,
|
||||
brokerUrl: persisted.brokerUrl,
|
||||
});
|
||||
@@ -189,6 +195,9 @@ export async function connectStream(
|
||||
): Promise<void> {
|
||||
const { default: mqtt } = await import("mqtt");
|
||||
|
||||
setState("connectionStatus", "connecting");
|
||||
setState("connectionError", null);
|
||||
|
||||
const client = mqtt.connect(brokerUrl, {
|
||||
clientId: `${state.myName}-${Date.now()}`,
|
||||
protocol: brokerUrl.startsWith("wss") ? "wss" : "ws",
|
||||
@@ -200,6 +209,8 @@ export async function connectStream(
|
||||
client.on("connect", () => {
|
||||
_mqttConnected = true;
|
||||
setState("connected", true);
|
||||
setState("connectionStatus", "connected");
|
||||
setState("connectionError", null);
|
||||
setState("brokerUrl", brokerUrl);
|
||||
|
||||
// Persist connection info for next time
|
||||
@@ -251,10 +262,13 @@ export async function connectStream(
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -416,6 +430,7 @@ export function disconnectStream(): void {
|
||||
_mqttConnected = false;
|
||||
}
|
||||
setState("connected", false);
|
||||
setState("connectionStatus", "disconnected");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user