fix: add SSE padding to flush Cloudflare tunnel buffers for real-time events

Cloudflare quick tunnels buffer small SSE responses, causing tab creation
and other UI events to arrive late on mobile. Adds ~8KB SSE comment padding
(ignored by EventSource) to force the proxy to flush immediately.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-03-01 18:24:14 +01:00
co-authored by Claude Opus 4.6
parent 8764a684ac
commit 2b61a8db1b
2 changed files with 29 additions and 9 deletions
+9 -2
View File
@@ -21,8 +21,15 @@ export const BATCH_FLUSH_THRESHOLD = 32 * 1024;
/** Task event batching interval (ms) */
export const TASK_UPDATE_BATCH_INTERVAL = 100;
/** SSE client health check interval (ms) */
export const SSE_HEALTH_CHECK_INTERVAL = 30 * 1000;
/** SSE heartbeat interval — sends padded keepalive to flush proxy buffers (ms).
* 15s is fast enough to keep Cloudflare tunnel buffers flushed while avoiding
* excessive bandwidth. Also serves as dead-client detection. */
export const SSE_HEARTBEAT_INTERVAL = 15 * 1000;
/** SSE padding size (bytes). Cloudflare quick tunnels buffer small SSE events;
* appending ~8KB of SSE comment padding forces the proxy to flush immediately.
* SSE comments (lines starting with ':') are silently ignored by EventSource. */
export const SSE_PADDING_SIZE = 8 * 1024;
// ============================================================================
// State Persistence
+20 -7
View File
@@ -96,7 +96,8 @@ import {
SESSIONS_LIST_CACHE_TTL,
SCHEDULED_CLEANUP_INTERVAL,
SCHEDULED_RUN_MAX_AGE,
SSE_HEALTH_CHECK_INTERVAL,
SSE_HEARTBEAT_INTERVAL,
SSE_PADDING_SIZE,
SESSION_LIMIT_WAIT_MS,
ITERATION_PAUSE_MS,
BATCH_FLUSH_THRESHOLD,
@@ -110,6 +111,12 @@ import {
const DEC_SYNC_START = '\x1b[?2026h'; // Begin synchronized update
const DEC_SYNC_END = '\x1b[?2026l'; // End synchronized update (flush to screen)
// SSE padding for Cloudflare tunnel buffer flushing.
// Cloudflare quick tunnels buffer small SSE responses, causing lag for real-time events.
// Appending SSE comment padding (ignored by EventSource) forces the proxy to flush.
// Pre-computed once at startup to avoid repeated string allocation.
const SSE_PADDING = ':' + 'p'.repeat(SSE_PADDING_SIZE) + '\n';
/**
* Get or generate a self-signed TLS certificate for HTTPS.
* Certs are stored in ~/.codeman/certs/ and reused across restarts.
@@ -574,6 +581,9 @@ export class WebServer extends EventEmitter {
// Use light state for SSE init to avoid sending 2MB+ terminal buffers
// Buffers are fetched on-demand when switching tabs
this.sendSSE(reply, 'init', this.getLightState());
// Flush Cloudflare tunnel buffer with padding — ensures the init event
// (and any immediately following events) are delivered without proxy delay.
try { reply.raw.write(SSE_PADDING); } catch { /* client gone */ }
req.raw.on('close', () => {
this.sseClients.delete(reply);
@@ -1855,10 +1865,12 @@ export class WebServer extends EventEmitter {
this.cachedLightState = null;
this.cachedSessionsList = null;
}
// Performance optimization: serialize JSON once for all clients
// Performance optimization: serialize JSON once for all clients.
// Append padding to flush Cloudflare tunnel buffers for all non-terminal events.
// Terminal data is high-volume and already exceeds buffer thresholds naturally.
let message: string;
try {
message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n` + SSE_PADDING;
} catch (err) {
// Handle circular references or non-serializable values
console.error(`[Server] Failed to serialize SSE event "${event}":`, err);
@@ -2128,8 +2140,9 @@ export class WebServer extends EventEmitter {
if (!socket || socket.destroyed || !socket.writable) {
deadClients.push(client);
} else {
// Send SSE comment as keep-alive (comments start with ':')
client.raw.write(':keepalive\n\n');
// Send padded SSE comment as keep-alive — the padding flushes
// Cloudflare tunnel buffers so subsequent events arrive promptly.
client.raw.write(':keepalive\n' + SSE_PADDING);
}
} catch {
// Error accessing socket means client is dead
@@ -2213,8 +2226,8 @@ export class WebServer extends EventEmitter {
() => {
this.cleanupDeadSSEClients();
},
SSE_HEALTH_CHECK_INTERVAL,
{ description: 'SSE client health check' }
SSE_HEARTBEAT_INTERVAL,
{ description: 'SSE heartbeat + dead client cleanup' }
);
// Start token recording timer (every 5 minutes for long-running sessions)