batch3 T5: frontend foundation — Pane types, panes API, user-events WS
- Mirror Pane/PaneState/UserStream types - api.panes.* CRUD methods - sessionEvents adds session_updated, session_loaded, open_file_in_browser - useUserEvents hook: single app-level WS to /api/ws/user with reconnect - useSidebar handles session_updated (in-place patch + re-sort) and session_loaded (active-project highlight gap fix); open_file_in_browser is a deliberate no-op here, consumed by Workspace later - App.tsx mounts useUserEvents once Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
58
apps/web/src/hooks/useUserEvents.ts
Normal file
58
apps/web/src/hooks/useUserEvents.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useEffect } from 'react';
|
||||
import { sessionEvents } from './sessionEvents';
|
||||
|
||||
const RECONNECT_INITIAL_MS = 1000;
|
||||
const RECONNECT_MAX_MS = 30000;
|
||||
|
||||
export function useUserEvents(): void {
|
||||
useEffect(() => {
|
||||
let ws: WebSocket | null = null;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let reconnectDelay = RECONNECT_INITIAL_MS;
|
||||
let unmounted = false;
|
||||
|
||||
const connect = () => {
|
||||
if (unmounted) return;
|
||||
const url = new URL('/api/ws/user', window.location.href);
|
||||
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
ws = new WebSocket(url.toString());
|
||||
|
||||
ws.onopen = () => {
|
||||
reconnectDelay = RECONNECT_INITIAL_MS;
|
||||
};
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
try {
|
||||
const frame = JSON.parse(ev.data);
|
||||
// The server emits frames whose `type` matches SessionEvent union members
|
||||
// (project_created, project_deleted, session_created, session_deleted, session_updated).
|
||||
// Pass through onto the bus.
|
||||
sessionEvents.emit(frame);
|
||||
} catch (err) {
|
||||
console.warn('useUserEvents: failed to parse frame', err);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (unmounted) return;
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
|
||||
connect();
|
||||
}, reconnectDelay);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
// close handler will trigger reconnect
|
||||
try { ws?.close(); } catch {}
|
||||
};
|
||||
};
|
||||
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
unmounted = true;
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
if (ws) try { ws.close(); } catch {}
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
Reference in New Issue
Block a user