mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
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:
@@ -21,8 +21,15 @@ export const BATCH_FLUSH_THRESHOLD = 32 * 1024;
|
|||||||
/** Task event batching interval (ms) */
|
/** Task event batching interval (ms) */
|
||||||
export const TASK_UPDATE_BATCH_INTERVAL = 100;
|
export const TASK_UPDATE_BATCH_INTERVAL = 100;
|
||||||
|
|
||||||
/** SSE client health check interval (ms) */
|
/** SSE heartbeat interval — sends padded keepalive to flush proxy buffers (ms).
|
||||||
export const SSE_HEALTH_CHECK_INTERVAL = 30 * 1000;
|
* 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
|
// State Persistence
|
||||||
|
|||||||
+20
-7
@@ -96,7 +96,8 @@ import {
|
|||||||
SESSIONS_LIST_CACHE_TTL,
|
SESSIONS_LIST_CACHE_TTL,
|
||||||
SCHEDULED_CLEANUP_INTERVAL,
|
SCHEDULED_CLEANUP_INTERVAL,
|
||||||
SCHEDULED_RUN_MAX_AGE,
|
SCHEDULED_RUN_MAX_AGE,
|
||||||
SSE_HEALTH_CHECK_INTERVAL,
|
SSE_HEARTBEAT_INTERVAL,
|
||||||
|
SSE_PADDING_SIZE,
|
||||||
SESSION_LIMIT_WAIT_MS,
|
SESSION_LIMIT_WAIT_MS,
|
||||||
ITERATION_PAUSE_MS,
|
ITERATION_PAUSE_MS,
|
||||||
BATCH_FLUSH_THRESHOLD,
|
BATCH_FLUSH_THRESHOLD,
|
||||||
@@ -110,6 +111,12 @@ import {
|
|||||||
const DEC_SYNC_START = '\x1b[?2026h'; // Begin synchronized update
|
const DEC_SYNC_START = '\x1b[?2026h'; // Begin synchronized update
|
||||||
const DEC_SYNC_END = '\x1b[?2026l'; // End synchronized update (flush to screen)
|
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.
|
* Get or generate a self-signed TLS certificate for HTTPS.
|
||||||
* Certs are stored in ~/.codeman/certs/ and reused across restarts.
|
* 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
|
// Use light state for SSE init to avoid sending 2MB+ terminal buffers
|
||||||
// Buffers are fetched on-demand when switching tabs
|
// Buffers are fetched on-demand when switching tabs
|
||||||
this.sendSSE(reply, 'init', this.getLightState());
|
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', () => {
|
req.raw.on('close', () => {
|
||||||
this.sseClients.delete(reply);
|
this.sseClients.delete(reply);
|
||||||
@@ -1855,10 +1865,12 @@ export class WebServer extends EventEmitter {
|
|||||||
this.cachedLightState = null;
|
this.cachedLightState = null;
|
||||||
this.cachedSessionsList = 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;
|
let message: string;
|
||||||
try {
|
try {
|
||||||
message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n` + SSE_PADDING;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Handle circular references or non-serializable values
|
// Handle circular references or non-serializable values
|
||||||
console.error(`[Server] Failed to serialize SSE event "${event}":`, err);
|
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) {
|
if (!socket || socket.destroyed || !socket.writable) {
|
||||||
deadClients.push(client);
|
deadClients.push(client);
|
||||||
} else {
|
} else {
|
||||||
// Send SSE comment as keep-alive (comments start with ':')
|
// Send padded SSE comment as keep-alive — the padding flushes
|
||||||
client.raw.write(':keepalive\n\n');
|
// Cloudflare tunnel buffers so subsequent events arrive promptly.
|
||||||
|
client.raw.write(':keepalive\n' + SSE_PADDING);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Error accessing socket means client is dead
|
// Error accessing socket means client is dead
|
||||||
@@ -2213,8 +2226,8 @@ export class WebServer extends EventEmitter {
|
|||||||
() => {
|
() => {
|
||||||
this.cleanupDeadSSEClients();
|
this.cleanupDeadSSEClients();
|
||||||
},
|
},
|
||||||
SSE_HEALTH_CHECK_INTERVAL,
|
SSE_HEARTBEAT_INTERVAL,
|
||||||
{ description: 'SSE client health check' }
|
{ description: 'SSE heartbeat + dead client cleanup' }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Start token recording timer (every 5 minutes for long-running sessions)
|
// Start token recording timer (every 5 minutes for long-running sessions)
|
||||||
|
|||||||
Reference in New Issue
Block a user