Files
boocode/apps/web/src/hooks/useUserEvents.ts
indifferentketchup 2f6be39efd chore: surface swallowed errors + remove dead session_renamed paths
Swallowed-error logging (audit Feature 3):
- file_index.ts:36-37 (git mtime probes): comment — best-effort, project
  may not be a git repo.
- useUserEvents.ts:44 / 53 (ws.close on error / unmount): comments —
  best-effort, socket may already be closing.
- RightRail.tsx:38 (localStorage write): comment — best-effort, quota or
  private mode.
- App.tsx:21 (api.sessions.get for RightRail projectId): replaced silent
  catch with console.warn.
- Session.tsx:38, 41 (session fetch + project list for breadcrumb):
  replaced silent catches with console.warn.

H1: ProjectSidebar.tsx:189 — dropped the local sessionEvents.emit
({type:'session_renamed'}) after PATCH. Server publishes via
broker.publishUser since v1.4; useUserEvents forwards.

H2: useSessionStream.ts session_renamed case removed (dead — no
server code path publishes session_renamed on the per-session WS
channel; only user channel via broker.publishUser). Also dropped the
session_renamed variant from WsFrame (in apps/web/src/api/types.ts)
to keep the discriminated-union switch exhaustive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 04:35:49 +00:00

59 lines
1.8 KiB
TypeScript

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 parsed: unknown = JSON.parse(ev.data);
if (parsed && typeof (parsed as { type?: unknown }).type === 'string') {
sessionEvents.emit(parsed as import('./sessionEvents').SessionEvent);
}
} catch (err) {
console.warn('useUserEvents: failed to parse frame', err);
}
};
ws.onclose = () => {
if (unmounted) return;
const delay = reconnectDelay;
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
reconnectTimer = setTimeout(connect, delay);
};
ws.onerror = () => {
// close handler will trigger reconnect; best-effort, ignore failure
// because the socket may already be closing
try { ws?.close(); } catch {}
};
};
connect();
return () => {
unmounted = true;
if (reconnectTimer) clearTimeout(reconnectTimer);
// best-effort cleanup; ignore failure because the socket may already be closed
if (ws) try { ws.close(); } catch {}
};
}, []);
}