batch3 T5 review fixes: backoff off-by-one + activeSession shape + headers
- useUserEvents: double delay before scheduling, producing 1/2/4/8/16/30s
- useSidebar: activeSessionProjectId -> activeSession {session_id,project_id}
so consumers can verify URL match and ignore stale values
- api.panes.create/update: drop redundant Content-Type (request helper sets)
- useUserEvents: minimal type guard on incoming WS frame before emit
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -123,13 +123,11 @@ export const api = {
|
|||||||
create: (sessionId: string, body: PaneCreateRequest) =>
|
create: (sessionId: string, body: PaneCreateRequest) =>
|
||||||
request<Pane>(`/api/sessions/${sessionId}/panes`, {
|
request<Pane>(`/api/sessions/${sessionId}/panes`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
}),
|
}),
|
||||||
update: (id: string, body: PaneUpdateRequest) =>
|
update: (id: string, body: PaneUpdateRequest) =>
|
||||||
request<Pane>(`/api/panes/${id}`, {
|
request<Pane>(`/api/panes/${id}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
}),
|
}),
|
||||||
remove: (id: string) =>
|
remove: (id: string) =>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ let sharedError: string | null = null;
|
|||||||
let sharedLoading: boolean = true;
|
let sharedLoading: boolean = true;
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
let fetchInFlight: Promise<void> | null = null;
|
let fetchInFlight: Promise<void> | null = null;
|
||||||
let activeSessionProjectId: string | null = null;
|
let activeSession: { session_id: string; project_id: string } | null = null;
|
||||||
const subscribers = new Set<() => void>();
|
const subscribers = new Set<() => void>();
|
||||||
|
|
||||||
function notify(): void {
|
function notify(): void {
|
||||||
@@ -150,7 +150,7 @@ sessionEvents.subscribe((event) => {
|
|||||||
// session_loaded updates activeSessionProjectId regardless of whether
|
// session_loaded updates activeSessionProjectId regardless of whether
|
||||||
// sharedData is populated yet — notify so subscribers can re-read.
|
// sharedData is populated yet — notify so subscribers can re-read.
|
||||||
if (event.type === 'session_loaded') {
|
if (event.type === 'session_loaded') {
|
||||||
activeSessionProjectId = event.project_id;
|
activeSession = { session_id: event.session_id, project_id: event.project_id };
|
||||||
notify();
|
notify();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -165,11 +165,11 @@ interface Snapshot {
|
|||||||
data: SidebarResponse | null;
|
data: SidebarResponse | null;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
activeSessionProjectId: string | null;
|
activeSession: { session_id: string; project_id: string } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function snapshot(): Snapshot {
|
function snapshot(): Snapshot {
|
||||||
return { data: sharedData, error: sharedError, loading: sharedLoading, activeSessionProjectId };
|
return { data: sharedData, error: sharedError, loading: sharedLoading, activeSession };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSidebar(): {
|
export function useSidebar(): {
|
||||||
@@ -177,7 +177,7 @@ export function useSidebar(): {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
retry: () => void;
|
retry: () => void;
|
||||||
activeSessionProjectId: string | null;
|
activeSession: { session_id: string; project_id: string } | null;
|
||||||
} {
|
} {
|
||||||
const [state, setState] = useState<Snapshot>(snapshot);
|
const [state, setState] = useState<Snapshot>(snapshot);
|
||||||
|
|
||||||
@@ -199,5 +199,5 @@ export function useSidebar(): {
|
|||||||
void load();
|
void load();
|
||||||
};
|
};
|
||||||
|
|
||||||
return { data: state.data, error: state.error, loading: state.loading, retry, activeSessionProjectId: state.activeSessionProjectId };
|
return { data: state.data, error: state.error, loading: state.loading, retry, activeSession: state.activeSession };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,10 @@ export function useUserEvents(): void {
|
|||||||
|
|
||||||
ws.onmessage = (ev) => {
|
ws.onmessage = (ev) => {
|
||||||
try {
|
try {
|
||||||
const frame = JSON.parse(ev.data);
|
const parsed: unknown = JSON.parse(ev.data);
|
||||||
// The server emits frames whose `type` matches SessionEvent union members
|
if (parsed && typeof (parsed as { type?: unknown }).type === 'string') {
|
||||||
// (project_created, project_deleted, session_created, session_deleted, session_updated).
|
sessionEvents.emit(parsed as import('./sessionEvents').SessionEvent);
|
||||||
// Pass through onto the bus.
|
}
|
||||||
sessionEvents.emit(frame);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('useUserEvents: failed to parse frame', err);
|
console.warn('useUserEvents: failed to parse frame', err);
|
||||||
}
|
}
|
||||||
@@ -35,10 +34,9 @@ export function useUserEvents(): void {
|
|||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
if (unmounted) return;
|
if (unmounted) return;
|
||||||
reconnectTimer = setTimeout(() => {
|
const delay = reconnectDelay;
|
||||||
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
|
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
|
||||||
connect();
|
reconnectTimer = setTimeout(connect, delay);
|
||||||
}, reconnectDelay);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user