State Management (Zustand)
Client-side stores for the MetaForge Digital Twin Dashboard. Each store is a self-contained Zustand slice with typed state, actions, and optional localStorage persistence.
src/store/ui-store.ts
Controls sidebar visibility, theme preference, and the command palette. Persisted to localStorage so preferences survive page reloads.
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export type Theme = "light" | "dark" | "system";
export interface UIState {
/** Whether the sidebar drawer is open (mobile) or visible (desktop). */
sidebarOpen: boolean;
/** When true the sidebar renders in narrow icon-only mode. */
sidebarCollapsed: boolean;
/** Active colour-scheme preference. */
theme: Theme;
/** Whether the Cmd+K command palette is showing. */
commandPaletteOpen: boolean;
}
export interface UIActions {
toggleSidebar: () => void;
setSidebarCollapsed: (collapsed: boolean) => void;
setTheme: (theme: Theme) => void;
toggleCommandPalette: () => void;
}
export type UIStore = UIState & UIActions;
// ---------------------------------------------------------------------------
// Resolved theme helper
// ---------------------------------------------------------------------------
/**
* Resolve the effective theme, respecting the `system` option by reading
* the user's OS preference via `prefers-color-scheme`.
*/
export function resolveTheme(theme: Theme): "light" | "dark" {
if (theme !== "system") return theme;
if (typeof window === "undefined") return "light";
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
// ---------------------------------------------------------------------------
// Store
// ---------------------------------------------------------------------------
export const useUIStore = create<UIStore>()(
persist(
(set) => ({
// -- state defaults ---------------------------------------------------
sidebarOpen: true,
sidebarCollapsed: false,
theme: "system",
commandPaletteOpen: false,
// -- actions ----------------------------------------------------------
toggleSidebar: () =>
set((s) => ({ sidebarOpen: !s.sidebarOpen })),
setSidebarCollapsed: (collapsed) =>
set({ sidebarCollapsed: collapsed }),
setTheme: (theme) => {
// Apply the resolved class to <html> immediately so Tailwind picks it up.
const resolved = resolveTheme(theme);
document.documentElement.classList.remove("light", "dark");
document.documentElement.classList.add(resolved);
set({ theme });
},
toggleCommandPalette: () =>
set((s) => ({ commandPaletteOpen: !s.commandPaletteOpen })),
}),
{
name: "metaforge-ui",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
sidebarCollapsed: state.sidebarCollapsed,
theme: state.theme,
}),
},
),
);
src/store/websocket-store.ts
Manages a single WebSocket connection to the Gateway, including automatic reconnection with exponential back-off.
import { create } from "zustand";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export type WSStatus = "connecting" | "connected" | "disconnected" | "error";
export interface WebSocketMessage<T = unknown> {
event: string;
data: T;
timestamp?: string;
}
export interface WebSocketState {
status: WSStatus;
socket: WebSocket | null;
reconnectAttempts: number;
lastMessage: WebSocketMessage | null;
}
export interface WebSocketActions {
connect: (url: string) => void;
disconnect: () => void;
send: <T>(data: T) => void;
setStatus: (status: WSStatus) => void;
}
export type WebSocketStore = WebSocketState & WebSocketActions;
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const INITIAL_BACKOFF_MS = 1_000;
const MAX_BACKOFF_MS = 30_000;
const BACKOFF_MULTIPLIER = 2;
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
/** Calculate exponential back-off delay capped at MAX_BACKOFF_MS. */
function backoffDelay(attempt: number): number {
return Math.min(INITIAL_BACKOFF_MS * BACKOFF_MULTIPLIER ** attempt, MAX_BACKOFF_MS);
}
// ---------------------------------------------------------------------------
// Store
// ---------------------------------------------------------------------------
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
export const useWebSocketStore = create<WebSocketStore>()((set, get) => ({
// -- state defaults -------------------------------------------------------
status: "disconnected",
socket: null,
reconnectAttempts: 0,
lastMessage: null,
// -- actions --------------------------------------------------------------
connect: (url: string) => {
const { socket: existing } = get();
// Prevent duplicate connections
if (
existing &&
(existing.readyState === WebSocket.OPEN ||
existing.readyState === WebSocket.CONNECTING)
) {
return;
}
set({ status: "connecting" });
const ws = new WebSocket(url);
ws.addEventListener("open", () => {
set({ status: "connected", socket: ws, reconnectAttempts: 0 });
});
ws.addEventListener("message", (event: MessageEvent) => {
try {
const parsed: WebSocketMessage = JSON.parse(event.data as string);
set({ lastMessage: parsed });
} catch {
// Non-JSON frames are silently ignored.
}
});
ws.addEventListener("close", () => {
set({ status: "disconnected", socket: null });
// --- auto-reconnect with exponential back-off -----------------------
const attempts = get().reconnectAttempts;
const delay = backoffDelay(attempts);
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(() => {
set({ reconnectAttempts: attempts + 1 });
get().connect(url);
}, delay);
});
ws.addEventListener("error", () => {
set({ status: "error" });
// The subsequent "close" event will trigger reconnection.
});
set({ socket: ws });
},
disconnect: () => {
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
const { socket } = get();
if (socket) {
// Remove all listeners before closing to prevent auto-reconnect.
socket.onclose = null;
socket.onerror = null;
socket.onmessage = null;
socket.close();
}
set({
status: "disconnected",
socket: null,
reconnectAttempts: 0,
lastMessage: null,
});
},
send: <T>(data: T) => {
const { socket, status } = get();
if (status !== "connected" || !socket) {
console.warn("[ws] Cannot send — socket is not connected.");
return;
}
socket.send(JSON.stringify(data));
},
setStatus: (status: WSStatus) => set({ status }),
}));
chat-store(sidebar visibility, streaming content, typing indicators) is specified in Agent Chat Channel.