### Two unrelated but small UI fixes surfaced while testing the Canvas
**1. Legend hidden under the open TemplatePalette.**
Legend is `fixed bottom-6 left-4 z-30`. TemplatePalette's drawer (when
open) is `fixed top-0 left-0 w-[280px] z-30` — same z-index, same
left-edge column. The Legend overlapped the palette's bottom 180 px.
Published the palette-open state to the canvas store so the Legend
can shift right (to `left-[296px]` — 280 px palette + 16 px gap) while
the palette is open, animated via a 200 ms `transition-[left]` to
match the palette's slide. Closes cleanly back to `left-4` when the
palette is dismissed.
Files:
- `store/canvas.ts` — added `templatePaletteOpen` + `setTemplatePaletteOpen`.
- `TemplatePalette.tsx` — calls `setTemplatePaletteOpen(open)` on
every open/close transition via a new useEffect.
- `Legend.tsx` — reads the flag and swaps `left-4` <-> `left-[296px]`.
**2. "WebSocket is closed before the connection is established" spam.**
Two components (`ChatTab`, `AgentCommsPanel`) open their own short-
lived WebSocket to tail the ACTIVITY_LOGGED stream. Their cleanup
path called `ws.close()` unconditionally, which trips a browser
console warning when React StrictMode re-runs the effect in dev and
the handshake hasn't completed yet. Confirmed via DevTools console
on the running canvas.
Added a `closeWebSocketGracefully(ws)` helper in `lib/ws-close.ts`:
- OPEN / CLOSING → close immediately (normal path).
- CONNECTING → defer close to the 'open' listener so the
browser sees a full handshake. Also wires an
'error' listener that cancels the queued close
if the handshake fails (no double-close).
- CLOSED → no-op.
Both consumers now call the helper in their useEffect cleanup.
Silences the warning without changing observable behaviour.
### Tests
`canvas/src/lib/__tests__/ws-close.test.ts` — 5 cases with a fake
WebSocket covering each readyState branch plus the error-before-open
cancellation path. Full vitest suite: 927/927 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
39 lines
1.4 KiB
TypeScript
39 lines
1.4 KiB
TypeScript
/**
|
|
* closeWebSocketGracefully closes a WebSocket without tripping the
|
|
* browser console warning "WebSocket is closed before the connection is
|
|
* established". That warning fires when `ws.close()` runs while
|
|
* readyState is still CONNECTING (0) — most often triggered by React
|
|
* StrictMode's double-invoked useEffect in dev, or any rapid
|
|
* mount/unmount (tab switch, route change) during the WS handshake.
|
|
*
|
|
* Behaviour by state:
|
|
* - OPEN / CLOSING: close immediately (the normal path).
|
|
* - CONNECTING: defer the close until 'open' fires, so the
|
|
* browser sees a full handshake before the shutdown.
|
|
* - CLOSED: no-op.
|
|
*
|
|
* Returns the ws unchanged for chaining.
|
|
*/
|
|
export function closeWebSocketGracefully(ws: WebSocket): WebSocket {
|
|
const state = ws.readyState;
|
|
if (state === WebSocket.OPEN || state === WebSocket.CLOSING) {
|
|
ws.close();
|
|
return ws;
|
|
}
|
|
if (state === WebSocket.CONNECTING) {
|
|
const onOpen = () => {
|
|
ws.close();
|
|
};
|
|
ws.addEventListener("open", onOpen, { once: true });
|
|
// Also wire an error listener — if the handshake fails we don't
|
|
// need to close (the browser already tore it down) and we should
|
|
// clear the queued onOpen handler.
|
|
ws.addEventListener(
|
|
"error",
|
|
() => ws.removeEventListener("open", onOpen),
|
|
{ once: true },
|
|
);
|
|
}
|
|
return ws;
|
|
}
|