mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
perf(tiles): no SSE terminal stream for the focused tile while tiles own the terminal
With the grid open the page's SSE filter still named the focused tile's session, so the server streamed that session's output over SSE as well. The main terminal is parked (its socket closed, _wsReady false), so every frame was JSON.parsed and then dropped by the park guard; the tile has the same output over its own socket. The filter now names TILE_GRID_SSE_FILTER (constants.js, a fixed id no session takes) while tiles own the terminal, at both places that set it: the live re-subscribe every tile focus runs (_updateSseSubscription) and the connect URL an SSE reconnect rebuilds (connectSSE), through one helper, _sseFilterSessionId(). Leaving the grid gives the filter back to the session shown (selectSession re-subscribes). Why it is safe, server side: the filter is read in exactly one place, SseStreamManager.flushSessionTerminalBatch. The connect route parses it, POST /api/events/subscribe replaces it (updateClientFilter); broadcast(), the multi-user ownership check (canDeliver), the heartbeat, the order and tab-layout frames and the shutdown notice never read it, and no push, viewing or acknowledgement logic does. The only page consumer of session:terminal is _onSSETerminal -> _onSessionTerminal, a no-op while tiles own the terminal. Measured at checkpoint 1 (6 tiles, focused tile a printing shell): 16 to 18 frames/s, 2.2 to 2.4 KB/s parsed and dropped -> 0. Live, this code (6 tiles, shells printing), SSE terminal frames per 5 s: 0 with the grid open; 0 after an SSE reconnect with the grid open (connect URL sessions=tile-grid); 86 in the single view after closing the grid and 86 after a reload into it (that connect URL names no session, as before; selectSession's re-subscribe names the shown one). Just before this commit: 18 frames/s, 2.4 KB/s. A tile focus runs no connectSSE and no handleInit; it posts the grid id. Tests: the page subscribes with the grid id on open and on every tile focus, gives the session back on close and on a reload into the single view, and connectSSE asks the same helper. Server, live, multi-user: the id is taken on the connect query and on a re-subscribe, and then withholds terminal output while session:updated and hook events still reach their owner (and only their owner). Mutation-checked five ways (helper ignoring the grid, connectSSE on the raw id, the server dropping non-UUID ids on subscribe and on connect, the filter gating every event). Scope: PR 2 (constants.js, the app.js SSE seam). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+16
-2
@@ -1483,11 +1483,24 @@ class CodemanApp {
|
||||
* (just at higher cost), and the next reconnect carries the filter via
|
||||
* the SSE query string.
|
||||
*/
|
||||
/**
|
||||
* The session id the SSE filter names for `sessionId`: itself, or while the
|
||||
* tile grid owns the terminal the grid's fixed filter (TILE_GRID_SSE_FILTER,
|
||||
* constants.js), which no session matches. Both places that set the filter
|
||||
* ask here: the live re-subscribe below and the connect URL (connectSSE),
|
||||
* which an SSE reconnect rebuilds with the grid still open.
|
||||
*/
|
||||
_sseFilterSessionId(sessionId) {
|
||||
if (this._tilesOwnTerminal?.()) return window.CodemanTileGrid?.TILE_GRID_SSE_FILTER || sessionId;
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
_updateSseSubscription(sessionId) {
|
||||
try {
|
||||
const filterId = this._sseFilterSessionId(sessionId);
|
||||
const body = JSON.stringify({
|
||||
clientId: this._clientId,
|
||||
sessions: sessionId ? [sessionId] : null,
|
||||
sessions: filterId ? [filterId] : null,
|
||||
});
|
||||
fetch('/api/events/subscribe', {
|
||||
method: 'POST',
|
||||
@@ -1883,7 +1896,8 @@ class CodemanApp {
|
||||
// session we're rendering. Lifecycle/metadata events are sent globally
|
||||
// regardless of filter (server side).
|
||||
const _sseParams = new URLSearchParams({ clientId: this._clientId });
|
||||
if (this.activeSessionId) _sseParams.set('sessions', this.activeSessionId);
|
||||
const _sseFilterId = this._sseFilterSessionId(this.activeSessionId);
|
||||
if (_sseFilterId) _sseParams.set('sessions', _sseFilterId);
|
||||
this.eventSource = new EventSource(CodemanBase.url(`/api/events?${_sseParams.toString()}`));
|
||||
|
||||
// Store all event listeners for cleanup on reconnect.
|
||||
|
||||
@@ -1662,6 +1662,13 @@ const TILE_GRID_WIDE_3X1 = 1800;
|
||||
const TILE_SCROLLBACK = 10000;
|
||||
// Tiles have their own per-device font size (a tile is a fraction of the screen).
|
||||
const TILE_FONT_SIZE_DEFAULT = 13;
|
||||
// What the page's SSE filter names while tiles own the terminal: a value no
|
||||
// session id takes (ids are UUIDs), so the server, whose filter gates only
|
||||
// session:terminal batches, sends none. The tiles carry their own output over
|
||||
// their own sockets, and the parked main terminal only parsed those frames to
|
||||
// drop them (16 to 18 a second for one busy shell). Every other event still
|
||||
// arrives (test/sse-tile-grid-filter.test.ts pins the server's side of this).
|
||||
const TILE_GRID_SSE_FILTER = 'tile-grid';
|
||||
|
||||
/**
|
||||
* Columns and rows for `count` tiles, by count (the spec's table), and whether
|
||||
@@ -2156,6 +2163,7 @@ if (typeof window !== 'undefined') {
|
||||
TILE_MIN_H,
|
||||
TILE_SCROLLBACK,
|
||||
TILE_FONT_SIZE_DEFAULT,
|
||||
TILE_GRID_SSE_FILTER,
|
||||
};
|
||||
window.CodemanRenderLiveness = { shouldKickRenderer, RENDER_STALL_MS, RENDER_LIVENESS_POLL_MS };
|
||||
window.CodemanFetchDeadline = {
|
||||
|
||||
Reference in New Issue
Block a user