vault-os/dashboard/src/api/ws.ts
Harald Hoyer f820a72b04 Initial implementation of vault-os
Complete implementation across all 13 phases:

- vault-core: types, YAML frontmatter parsing, entity classification,
  filesystem ops, config, prompt composition, validation, search
- vault-watch: filesystem watcher with daemon write filtering, event
  classification
- vault-scheduler: cron engine, process executor, task runner with
  retry logic and concurrency limiting
- vault-api: Axum REST API (15 route modules), WebSocket with broadcast,
  AI assistant proxy, validation, templates
- Dashboard: React + TypeScript + Tailwind v4 with kanban, CodeMirror
  editor, dynamic view system, AI chat sidebar
- Nix flake with dev shell and NixOS module
- Graceful shutdown, inotify overflow recovery, tracing instrumentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:21:17 +01:00

80 lines
2 KiB
TypeScript

import type { WsEvent } from './types';
type Listener = (event: WsEvent) => void;
export class VaultWebSocket {
private ws: WebSocket | null = null;
private listeners: Map<string, Set<Listener>> = new Map();
private globalListeners: Set<Listener> = new Set();
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private url: string;
constructor(url?: string) {
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
this.url = url || `${proto}//${window.location.host}/ws`;
}
connect() {
if (this.ws?.readyState === WebSocket.OPEN) return;
this.ws = new WebSocket(this.url);
this.ws.onmessage = (msg) => {
try {
const event: WsEvent = JSON.parse(msg.data);
this.globalListeners.forEach((fn) => fn(event));
this.listeners.get(event.type)?.forEach((fn) => fn(event));
} catch {
// ignore malformed messages
}
};
this.ws.onclose = () => {
this.scheduleReconnect();
};
this.ws.onerror = () => {
this.ws?.close();
};
}
disconnect() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
this.ws?.close();
this.ws = null;
}
/** Listen to a specific event type */
on(type: string, fn: Listener): () => void {
if (!this.listeners.has(type)) {
this.listeners.set(type, new Set());
}
this.listeners.get(type)!.add(fn);
return () => this.listeners.get(type)?.delete(fn);
}
/** Listen to all events */
onAny(fn: Listener): () => void {
this.globalListeners.add(fn);
return () => this.globalListeners.delete(fn);
}
send(action: Record<string, unknown>) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(action));
}
}
private scheduleReconnect() {
if (this.reconnectTimer) return;
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connect();
}, 3000);
}
}
export const vaultWs = new VaultWebSocket();