Relay-metered traffic drops on every channel that carried avoidable
bytes; binary payloads (attachments, voice audio) are unchanged.
Hub:
- gzip /api/* JSON responses, gated by q-aware Accept-Encoding
negotiation (explicit gzip;q=0 beats a wildcard in either order;
hono's compress() alone matches by substring)
- enable WebSocket permessage-deflate and default flagless ws.send()
to compressed frames - Bun negotiates the extension but compresses
nothing unless each send opts in, and @socket.io/bun-engine never
passes the flag (measured 96 KB terminal payload -> 608 B on wire)
- replay missed SSE events on reconnect: 256-event/2MB ring buffer,
per-process epoch ids bound to the authenticated namespace so a
token swap can never resume from a foreign cursor, standard
Last-Event-ID header preferred over the ?lastEventId fallback,
live broadcasts queued until the replay flushes to preserve order
Web:
- skip the full sessions/details/messages resync when the hub answers
resume:ok - a phone unlock now costs a handshake plus the gap delta
instead of refetching everything
- own every EventSource retry path: take over browser-native
CONNECTING retries, defer reconnects while the tab is hidden, and
raise the backoff ceiling to 5 min after repeated failures
- drop the 30s skills/slash-commands polling; refresh on demand
without blocking the suggestion menu behind a stalled CLI RPC
- remember the websocket upgrade across terminal socket reconnects
* perf(hub): gzip the SSE stream without delaying delivery
SSE payloads are plain JSON that repeat the same field names on every
event, so they compress well - measured 72-77% on real captured traffic
from a hub with 15 active sessions.
Compression could not simply be turned on, though. Hono's compress()
middleware bails out whenever Transfer-Encoding is set, which streamSSE
always sets, so mounting it is a no-op. Wrapping the body in a
CompressionStream does compress, but it buffers until the stream ends -
measured on a 10-event stream, every event arrived at once when the
stream closed. On a connection that stays open for hours that means
events never arrive at all.
So drive zlib directly and issue a Z_SYNC_FLUSH after each chunk. That
costs about one percentage point of ratio and keeps delivery immediate:
verified in a real Chromium EventSource, first event at 13ms and each
subsequent event at its own 500ms tick, with no error events.
Clients that do not send Accept-Encoding: gzip keep the uncompressed
stream. No event payload or timing changes.
* fix(hub): cancel through the reader, gate reads on demand, honour q=0
Three defects in the first version of the SSE gzip wrapper:
Cancelling the source directly threw. The wrapper holds a reader for the
whole life of the connection, and cancelling a locked stream is invalid -
in Bun it throws TypeError: Invalid state: ReadableStream is locked
synchronously out of the cancel callback. Since SSE clients disconnect
mid-stream as a matter of course, this fired on essentially every
disconnect, and the upstream cancel never ran. Cancel through the reader
instead, which is allowed to.
Reads were not gated on downstream demand. Only zlib's own buffer was
consulted, and SSE compresses well enough that a slow client can be
megabytes behind while the compressed queue still looks nearly empty: a
test with a non-reading consumer pulled 1752 chunks before stalling.
Reading now waits for desiredSize to go positive, resumed from pull().
Accept-Encoding was matched with a substring test, so "gzip;q=0" - which
means the client refuses gzip - was read as acceptance. Parse the q-value.
Re-verified that none of this costs the property the change exists for:
in a real Chromium EventSource the first event still arrives at 13ms and
each one after it on its own 500ms tick, with no error events.