@opengeni/api-router 0.5.6 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/http/sse.ts CHANGED
@@ -1,6 +1,246 @@
1
1
  import type { SessionEvent, WorkspaceControlEvent } from "@opengeni/contracts";
2
2
  import { listSessionEvents, listWorkspaceControlEvents, type Database } from "@opengeni/db";
3
- import { formatSse, type EventBus } from "@opengeni/events";
3
+ import {
4
+ formatSessionEventSse,
5
+ formatWorkspaceControlEventSse,
6
+ SESSION_EVENT_SSE_FRAME_MAX_BYTES,
7
+ type EventBus,
8
+ } from "@opengeni/events";
9
+ import type { Observability } from "@opengeni/observability";
10
+
11
+ const SESSION_REPLAY_PAGE_SIZE = 100;
12
+ const WORKSPACE_CONTROL_REPLAY_PAGE_SIZE = 100;
13
+ export const SSE_QUEUED_FRAME_MAX_COUNT = 1;
14
+ export const SSE_WRITE_STALL_TIMEOUT_MS = 30_000;
15
+
16
+ export type SseDeliveryBoundObservation = {
17
+ reason: "desired_size_non_positive" | "stall_timeout" | "frame_too_large";
18
+ desiredSize: number | null;
19
+ queuedFrames: number;
20
+ queuedBytes: number;
21
+ };
22
+
23
+ export type ByteBoundedSseStreamOptions = {
24
+ maxQueuedBytes?: number;
25
+ stallTimeoutMs?: number;
26
+ onStop?: () => void;
27
+ onObservation?: (observation: SseDeliveryBoundObservation) => void;
28
+ };
29
+
30
+ export type ByteBoundedSseStream = {
31
+ stream: ReadableStream<Uint8Array>;
32
+ write: (frame: string) => Promise<boolean>;
33
+ close: () => void;
34
+ fail: (error: unknown) => void;
35
+ stopped: () => boolean;
36
+ };
37
+
38
+ /**
39
+ * A byte-counted SSE body. `ReadableStreamDefaultController.enqueue()` does not
40
+ * itself wait for a slow HTTP consumer, so replaying bounded frames without
41
+ * checking `desiredSize` can still accumulate an unbounded server-side queue.
42
+ *
43
+ * One writer is expected per stream. The Web Streams queue holds at most one
44
+ * complete frame, and that frame must fit inside the byte cap. A second write
45
+ * waits for consumer pull only for a bounded interval; cancellation or a stalled
46
+ * reader wakes it and terminates upstream delivery before another durable page is
47
+ * read. One frame is deliberate: it makes both queued-frame count and queued
48
+ * bytes independently bounded instead of relying on byte accounting alone.
49
+ */
50
+ export function createByteBoundedSseStream(
51
+ options: ByteBoundedSseStreamOptions = {},
52
+ ): ByteBoundedSseStream {
53
+ const maxQueuedBytes = options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES;
54
+ const stallTimeoutMs = options.stallTimeoutMs ?? SSE_WRITE_STALL_TIMEOUT_MS;
55
+ if (!Number.isSafeInteger(maxQueuedBytes) || maxQueuedBytes <= 0) {
56
+ throw new RangeError("SSE byte high-water mark must be a positive safe integer");
57
+ }
58
+ if (!Number.isSafeInteger(stallTimeoutMs) || stallTimeoutMs <= 0) {
59
+ throw new RangeError("SSE write stall timeout must be a positive safe integer");
60
+ }
61
+ const encoder = new TextEncoder();
62
+ let controller!: ReadableStreamDefaultController<Uint8Array>;
63
+ let stopped = false;
64
+ let capacityWake: (() => void) | null = null;
65
+ let queuedFrames = 0;
66
+ let queuedBytes = 0;
67
+
68
+ const wakeWriter = () => {
69
+ const wake = capacityWake;
70
+ capacityWake = null;
71
+ wake?.();
72
+ };
73
+ const stop = (settle: () => void) => {
74
+ if (stopped) return;
75
+ stopped = true;
76
+ wakeWriter();
77
+ options.onStop?.();
78
+ try {
79
+ settle();
80
+ } catch {
81
+ // A concurrent consumer cancellation may already have settled the body.
82
+ }
83
+ };
84
+
85
+ const stream = new ReadableStream<Uint8Array>(
86
+ {
87
+ start: (rawController) => {
88
+ controller = rawController;
89
+ },
90
+ pull: () => {
91
+ // With a one-frame high-water mark, pull after an enqueue means that
92
+ // frame has left the controller queue (either delivered to a pending
93
+ // read or consumed by the HTTP adapter). There is no hidden second frame.
94
+ queuedFrames = 0;
95
+ queuedBytes = 0;
96
+ wakeWriter();
97
+ },
98
+ cancel: () => {
99
+ if (stopped) return;
100
+ stopped = true;
101
+ wakeWriter();
102
+ options.onStop?.();
103
+ },
104
+ },
105
+ {
106
+ highWaterMark: SSE_QUEUED_FRAME_MAX_COUNT,
107
+ size: () => 1,
108
+ },
109
+ );
110
+
111
+ return {
112
+ stream,
113
+ write: async (frame) => {
114
+ const chunk = encoder.encode(frame);
115
+ if (chunk.byteLength > maxQueuedBytes) {
116
+ const error = new RangeError(
117
+ `SSE frame cannot fit in the configured queue (${chunk.byteLength} > ${maxQueuedBytes} bytes)`,
118
+ );
119
+ options.onObservation?.({
120
+ reason: "frame_too_large",
121
+ desiredSize: controller.desiredSize,
122
+ queuedFrames,
123
+ queuedBytes,
124
+ });
125
+ stop(() => controller.error(error));
126
+ throw error;
127
+ }
128
+ for (;;) {
129
+ if (stopped) return false;
130
+ const desired = controller.desiredSize;
131
+ if (desired === null) return false;
132
+ if (desired >= 1 && queuedFrames === 0) {
133
+ controller.enqueue(chunk);
134
+ queuedFrames = 1;
135
+ queuedBytes = chunk.byteLength;
136
+ return true;
137
+ }
138
+ options.onObservation?.({
139
+ reason: "desired_size_non_positive",
140
+ desiredSize: desired,
141
+ queuedFrames,
142
+ queuedBytes,
143
+ });
144
+ const outcome = await new Promise<"capacity" | "timeout">((resolve) => {
145
+ let settled = false;
146
+ const finish = (result: "capacity" | "timeout") => {
147
+ if (settled) return;
148
+ settled = true;
149
+ clearTimeout(timer);
150
+ if (capacityWake === wake) capacityWake = null;
151
+ resolve(result);
152
+ };
153
+ const wake = () => finish("capacity");
154
+ const timer = setTimeout(() => finish("timeout"), stallTimeoutMs);
155
+ capacityWake = wake;
156
+ });
157
+ if (outcome === "timeout" && !stopped) {
158
+ const error = new TypeError(
159
+ `SSE consumer did not drain the single-frame queue within ${stallTimeoutMs}ms`,
160
+ );
161
+ options.onObservation?.({
162
+ reason: "stall_timeout",
163
+ desiredSize: controller.desiredSize,
164
+ queuedFrames,
165
+ queuedBytes,
166
+ });
167
+ stop(() => controller.error(error));
168
+ throw error;
169
+ }
170
+ }
171
+ },
172
+ close: () => stop(() => controller.close()),
173
+ fail: (error) => stop(() => controller.error(error)),
174
+ stopped: () => stopped,
175
+ };
176
+ }
177
+
178
+ export type LatestWinsDelivery<T extends { sequence: number }> = {
179
+ publish: (events: readonly T[]) => void;
180
+ stop: () => void;
181
+ whenIdle: () => Promise<void>;
182
+ pendingSequence: () => number | null;
183
+ };
184
+
185
+ /**
186
+ * Keep at most one live notification while an earlier notification is being
187
+ * delivered. The notification is only a cursor target: `send` gap-fills every
188
+ * missing durable event from Postgres, so replacing N intermediate notices with
189
+ * their newest sequence loses no event and prevents backpressure from migrating
190
+ * into the NATS subscription queue.
191
+ */
192
+ export function createLatestWinsDelivery<T extends { sequence: number }>(
193
+ send: (event: T) => Promise<void>,
194
+ onError: (error: unknown) => void,
195
+ ): LatestWinsDelivery<T> {
196
+ let newest: T | null = null;
197
+ let running: Promise<void> | null = null;
198
+ let stopped = false;
199
+
200
+ const start = () => {
201
+ if (stopped || running || !newest) return;
202
+ const run = async () => {
203
+ for (;;) {
204
+ if (stopped || !newest) return;
205
+ const target = newest;
206
+ newest = null;
207
+ await send(target);
208
+ }
209
+ };
210
+ running = run()
211
+ .catch((error) => {
212
+ stopped = true;
213
+ newest = null;
214
+ onError(error);
215
+ })
216
+ .finally(() => {
217
+ running = null;
218
+ start();
219
+ });
220
+ };
221
+
222
+ return {
223
+ publish: (events) => {
224
+ if (stopped) return;
225
+ for (const event of events) {
226
+ if (!newest || event.sequence > newest.sequence) newest = event;
227
+ }
228
+ start();
229
+ },
230
+ stop: () => {
231
+ stopped = true;
232
+ newest = null;
233
+ },
234
+ whenIdle: async () => {
235
+ for (;;) {
236
+ const pending = running;
237
+ if (!pending) return;
238
+ await pending;
239
+ }
240
+ },
241
+ pendingSequence: () => newest?.sequence ?? null,
242
+ };
243
+ }
4
244
 
5
245
  export async function sseSessionStream(
6
246
  db: Database,
@@ -9,76 +249,137 @@ export async function sseSessionStream(
9
249
  sessionId: string,
10
250
  after: number,
11
251
  signal: AbortSignal,
252
+ options: SessionSseDeliveryOptions = {},
12
253
  ): Promise<Response> {
13
- const encoder = new TextEncoder();
14
- let controller: ReadableStreamDefaultController<Uint8Array>;
254
+ if (
255
+ options.reauthorize &&
256
+ options.reauthorizeAfterMs !== undefined &&
257
+ (!Number.isSafeInteger(options.reauthorizeAfterMs) ||
258
+ options.reauthorizeAfterMs < 1_000 ||
259
+ options.reauthorizeAfterMs > 60_000)
260
+ ) {
261
+ throw new RangeError("session stream reauthorization must be between 1000 and 60000ms");
262
+ }
15
263
  let lastSent = after;
16
- let replaying = true;
17
- const buffered: SessionEvent[] = [];
264
+ let bootstrapping = true;
265
+ let newestBuffered: SessionEvent | null = null;
18
266
  let unsubscribe: (() => void) | null = null;
267
+ let delivery: LatestWinsDelivery<SessionEvent> | null = null;
268
+ let reauthorizationTimer: ReturnType<typeof setTimeout> | null = null;
269
+ let detachAbortListener = () => {};
270
+ const stopUpstream = () => {
271
+ detachAbortListener();
272
+ if (reauthorizationTimer) {
273
+ clearTimeout(reauthorizationTimer);
274
+ reauthorizationTimer = null;
275
+ }
276
+ delivery?.stop();
277
+ const release = unsubscribe;
278
+ unsubscribe = null;
279
+ release?.();
280
+ };
281
+ const channel = createByteBoundedSseStream({
282
+ maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
283
+ ...(options.stallTimeoutMs === undefined ? {} : { stallTimeoutMs: options.stallTimeoutMs }),
284
+ onObservation: sseObservationReporter("session", options),
285
+ onStop: stopUpstream,
286
+ });
19
287
 
20
- const stream = new ReadableStream<Uint8Array>({
21
- start: async (rawController) => {
22
- controller = rawController;
23
- const send = async (event: SessionEvent) => {
24
- if (event.sequence <= lastSent) {
25
- return;
26
- }
27
- if (event.sequence > lastSent + 1) {
28
- const missing = await listSessionEvents(
29
- db,
30
- workspaceId,
31
- sessionId,
32
- lastSent,
33
- event.sequence - lastSent - 1,
288
+ const fail = (error: unknown) => {
289
+ channel.fail(retryableSseFailure("session event stream delivery failed", error));
290
+ };
291
+ const scheduleReauthorization = () => {
292
+ if (!options.reauthorize || channel.stopped()) return;
293
+ const interval = options.reauthorizeAfterMs ?? 15_000;
294
+ reauthorizationTimer = setTimeout(() => {
295
+ reauthorizationTimer = null;
296
+ void options.reauthorize!()
297
+ .then(scheduleReauthorization)
298
+ .catch((error) => fail(error));
299
+ }, interval);
300
+ };
301
+ scheduleReauthorization();
302
+ const writeFrame = async (frame: string) => {
303
+ if (!(await channel.write(frame))) {
304
+ throw new SseStreamStoppedError();
305
+ }
306
+ };
307
+ const send = async (event: SessionEvent) => {
308
+ if (event.sequence <= lastSent) return;
309
+ if (event.sequence > lastSent + 1) {
310
+ while (lastSent + 1 < event.sequence) {
311
+ const previousLastSent = lastSent;
312
+ const missing = await listSessionEvents(db, workspaceId, sessionId, {
313
+ after: lastSent,
314
+ limit: Math.min(SESSION_REPLAY_PAGE_SIZE, event.sequence - lastSent - 1),
315
+ });
316
+ if (missing.length === 0) {
317
+ throw new Error(
318
+ `Session event replay stalled before sequence ${event.sequence}; last sent ${lastSent}`,
34
319
  );
35
- for (const missed of missing) {
36
- if (missed.sequence > lastSent) {
37
- controller.enqueue(encoder.encode(formatSse(missed)));
38
- lastSent = missed.sequence;
39
- }
40
- }
41
320
  }
42
- controller.enqueue(encoder.encode(formatSse(event)));
43
- lastSent = event.sequence;
44
- };
45
-
46
- unsubscribe = await bus.subscribe(workspaceId, sessionId, async (events) => {
47
- if (replaying) {
48
- buffered.push(...events);
49
- return;
321
+ for (const missed of missing) {
322
+ if (missed.sequence >= event.sequence) break;
323
+ if (missed.sequence > lastSent) {
324
+ await writeFrame(formatSessionEventSse(missed));
325
+ lastSent = missed.sequence;
326
+ }
50
327
  }
51
- for (const event of events.sort((a, b) => a.sequence - b.sequence)) {
52
- await send(event);
328
+ if (lastSent === previousLastSent) {
329
+ throw new Error(
330
+ `Session event replay made no progress before sequence ${event.sequence}; last sent ${lastSent}`,
331
+ );
53
332
  }
54
- });
333
+ }
334
+ }
335
+ await writeFrame(formatSessionEventSse(event));
336
+ lastSent = event.sequence;
337
+ };
338
+ delivery = createLatestWinsDelivery(send, fail);
55
339
 
56
- await replaySessionEvents(
57
- (cursor, limit) => listSessionEvents(db, workspaceId, sessionId, cursor, limit),
58
- send,
59
- after,
60
- );
61
- replaying = false;
62
- for (const event of buffered.sort((a, b) => a.sequence - b.sequence)) {
63
- await send(event);
340
+ void (async () => {
341
+ const release = await bus.subscribe(workspaceId, sessionId, (events) => {
342
+ if (bootstrapping) {
343
+ for (const event of events) {
344
+ if (!newestBuffered || event.sequence > newestBuffered.sequence) {
345
+ newestBuffered = event;
346
+ }
347
+ }
348
+ } else {
349
+ delivery?.publish(events);
64
350
  }
65
- buffered.length = 0;
66
- controller.enqueue(encoder.encode(": connected\n\n"));
67
- },
68
- cancel: () => {
69
- unsubscribe?.();
70
- },
351
+ });
352
+ if (channel.stopped()) {
353
+ release();
354
+ return;
355
+ }
356
+ unsubscribe = release;
357
+
358
+ await replaySessionEvents(
359
+ (cursor, limit) => listSessionEvents(db, workspaceId, sessionId, cursor, limit),
360
+ send,
361
+ after,
362
+ SESSION_REPLAY_PAGE_SIZE,
363
+ );
364
+ await writeFrame(": connected\n\n");
365
+ bootstrapping = false;
366
+ const buffered = newestBuffered;
367
+ newestBuffered = null;
368
+ if (buffered) delivery.publish([buffered]);
369
+ })().catch((error) => {
370
+ if (!(error instanceof SseStreamStoppedError)) fail(error);
71
371
  });
72
372
 
73
- signal.addEventListener(
74
- "abort",
75
- () => {
76
- unsubscribe?.();
77
- },
78
- { once: true },
79
- );
373
+ const abort = () => {
374
+ channel.close();
375
+ };
376
+ if (signal.aborted) abort();
377
+ else {
378
+ signal.addEventListener("abort", abort, { once: true });
379
+ detachAbortListener = () => signal.removeEventListener("abort", abort);
380
+ }
80
381
 
81
- return new Response(stream, {
382
+ return new Response(channel.stream, {
82
383
  headers: {
83
384
  "Content-Type": "text/event-stream; charset=utf-8",
84
385
  "Cache-Control": "no-cache, no-transform",
@@ -91,21 +392,28 @@ export async function replaySessionEvents(
91
392
  loadPage: (after: number, limit: number) => Promise<SessionEvent[]>,
92
393
  send: (event: SessionEvent) => Promise<void>,
93
394
  after: number,
94
- pageSize = 1000,
395
+ pageSize = SESSION_REPLAY_PAGE_SIZE,
95
396
  ): Promise<void> {
96
397
  let cursor = after;
97
398
  while (true) {
399
+ const previousCursor = cursor;
98
400
  const page = await loadPage(cursor, pageSize);
99
401
  if (page.length === 0) {
100
402
  return;
101
403
  }
102
404
  for (const event of page.sort((a, b) => a.sequence - b.sequence)) {
405
+ if (event.sequence <= cursor) continue;
103
406
  await send(event);
104
- cursor = Math.max(cursor, event.sequence);
407
+ cursor = event.sequence;
105
408
  }
106
409
  if (page.length < pageSize) {
107
410
  return;
108
411
  }
412
+ if (cursor === previousCursor) {
413
+ throw new Error(
414
+ `Session event replay made no progress after sequence ${cursor}; refusing to repeat a full stale page`,
415
+ );
416
+ }
109
417
  }
110
418
  }
111
419
 
@@ -115,47 +423,106 @@ export async function sseWorkspaceControlStream(
115
423
  workspaceId: string,
116
424
  after: number,
117
425
  signal: AbortSignal,
426
+ options: SseDeliveryOptions = {},
118
427
  ): Promise<Response> {
119
- const encoder = new TextEncoder();
120
428
  let lastSent = after;
121
- let replaying = true;
122
- const buffered: WorkspaceControlEvent[] = [];
429
+ let bootstrapping = true;
430
+ let newestBuffered: WorkspaceControlEvent | null = null;
123
431
  let unsubscribe: (() => void) | null = null;
432
+ let delivery: LatestWinsDelivery<WorkspaceControlEvent> | null = null;
433
+ let detachAbortListener = () => {};
434
+ const stopUpstream = () => {
435
+ detachAbortListener();
436
+ delivery?.stop();
437
+ const release = unsubscribe;
438
+ unsubscribe = null;
439
+ release?.();
440
+ };
441
+ const channel = createByteBoundedSseStream({
442
+ maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
443
+ ...(options.stallTimeoutMs === undefined ? {} : { stallTimeoutMs: options.stallTimeoutMs }),
444
+ onObservation: sseObservationReporter("workspace_control", options),
445
+ onStop: stopUpstream,
446
+ });
124
447
 
125
- const stream = new ReadableStream<Uint8Array>({
126
- start: async (controller) => {
127
- const send = (event: WorkspaceControlEvent) => {
128
- if (event.sequence <= lastSent) return;
129
- controller.enqueue(encoder.encode(formatSse(event)));
130
- lastSent = event.sequence;
131
- };
132
- unsubscribe = await bus.subscribeWorkspaceControl(workspaceId, async (event) => {
133
- if (replaying) {
134
- buffered.push(event);
135
- } else {
136
- send(event);
448
+ const fail = (error: unknown) => {
449
+ channel.fail(retryableSseFailure("workspace control stream delivery failed", error));
450
+ };
451
+ const writeFrame = async (frame: string) => {
452
+ if (!(await channel.write(frame))) throw new SseStreamStoppedError();
453
+ };
454
+ const send = async (event: WorkspaceControlEvent) => {
455
+ if (event.sequence <= lastSent) return;
456
+ if (event.sequence > lastSent + 1) {
457
+ while (lastSent < event.sequence) {
458
+ const previousLastSent = lastSent;
459
+ const limit = Math.min(
460
+ WORKSPACE_CONTROL_REPLAY_PAGE_SIZE,
461
+ Math.max(1, event.sequence - lastSent),
462
+ );
463
+ const missing = await listWorkspaceControlEvents(db, workspaceId, lastSent, limit);
464
+ let reachedIncoming = false;
465
+ for (const missed of missing.sort((a, b) => a.sequence - b.sequence)) {
466
+ if (missed.sequence >= event.sequence) {
467
+ reachedIncoming = true;
468
+ break;
469
+ }
470
+ if (missed.sequence > lastSent) {
471
+ await writeFrame(formatWorkspaceControlEventSse(missed));
472
+ lastSent = missed.sequence;
473
+ }
137
474
  }
138
- });
139
- let cursor = after;
140
- while (true) {
141
- const page = await listWorkspaceControlEvents(db, workspaceId, cursor, 1000);
142
- for (const event of page) {
143
- send(event);
144
- cursor = Math.max(cursor, event.sequence);
475
+ if (reachedIncoming || missing.length < limit) break;
476
+ if (lastSent === previousLastSent) {
477
+ throw new Error(
478
+ `Workspace control gap fill returned a full stale page before sequence ${event.sequence}; last sent ${lastSent}`,
479
+ );
145
480
  }
146
- if (page.length < 1000) break;
147
481
  }
148
- replaying = false;
149
- for (const event of buffered.sort((left, right) => left.sequence - right.sequence)) {
150
- send(event);
482
+ }
483
+ await writeFrame(formatWorkspaceControlEventSse(event));
484
+ lastSent = event.sequence;
485
+ };
486
+ delivery = createLatestWinsDelivery(send, fail);
487
+
488
+ void (async () => {
489
+ const release = await bus.subscribeWorkspaceControl(workspaceId, (event) => {
490
+ if (bootstrapping) {
491
+ if (!newestBuffered || event.sequence > newestBuffered.sequence) newestBuffered = event;
492
+ } else {
493
+ delivery?.publish([event]);
151
494
  }
152
- buffered.length = 0;
153
- controller.enqueue(encoder.encode(": connected\n\n"));
154
- },
155
- cancel: () => unsubscribe?.(),
495
+ });
496
+ if (channel.stopped()) {
497
+ release();
498
+ return;
499
+ }
500
+ unsubscribe = release;
501
+ await replayWorkspaceControlEvents(
502
+ (cursor, limit) => listWorkspaceControlEvents(db, workspaceId, cursor, limit),
503
+ send,
504
+ after,
505
+ WORKSPACE_CONTROL_REPLAY_PAGE_SIZE,
506
+ );
507
+ await writeFrame(": connected\n\n");
508
+ bootstrapping = false;
509
+ const buffered = newestBuffered;
510
+ newestBuffered = null;
511
+ if (buffered) delivery.publish([buffered]);
512
+ })().catch((error) => {
513
+ if (!(error instanceof SseStreamStoppedError)) fail(error);
156
514
  });
157
- signal.addEventListener("abort", () => unsubscribe?.(), { once: true });
158
- return new Response(stream, {
515
+
516
+ const abort = () => {
517
+ channel.close();
518
+ };
519
+ if (signal.aborted) abort();
520
+ else {
521
+ signal.addEventListener("abort", abort, { once: true });
522
+ detachAbortListener = () => signal.removeEventListener("abort", abort);
523
+ }
524
+
525
+ return new Response(channel.stream, {
159
526
  headers: {
160
527
  "Content-Type": "text/event-stream; charset=utf-8",
161
528
  "Cache-Control": "no-cache, no-transform",
@@ -163,3 +530,70 @@ export async function sseWorkspaceControlStream(
163
530
  },
164
531
  });
165
532
  }
533
+
534
+ async function replayWorkspaceControlEvents(
535
+ loadPage: (after: number, limit: number) => Promise<WorkspaceControlEvent[]>,
536
+ send: (event: WorkspaceControlEvent) => Promise<void>,
537
+ after: number,
538
+ pageSize: number,
539
+ ): Promise<void> {
540
+ let cursor = after;
541
+ while (true) {
542
+ const previousCursor = cursor;
543
+ const page = await loadPage(cursor, pageSize);
544
+ if (page.length === 0) return;
545
+ for (const event of page.sort((a, b) => a.sequence - b.sequence)) {
546
+ if (event.sequence <= cursor) continue;
547
+ await send(event);
548
+ cursor = event.sequence;
549
+ }
550
+ if (page.length < pageSize) return;
551
+ if (cursor === previousCursor) {
552
+ throw new Error(
553
+ `Workspace control replay made no progress after sequence ${cursor}; refusing to repeat a full stale page`,
554
+ );
555
+ }
556
+ }
557
+ }
558
+
559
+ class SseStreamStoppedError extends Error {}
560
+
561
+ export type SseDeliveryOptions = {
562
+ maxQueuedBytes?: number;
563
+ stallTimeoutMs?: number;
564
+ observability?: Observability | undefined;
565
+ onObservation?: ((observation: SseDeliveryBoundObservation) => void) | undefined;
566
+ };
567
+
568
+ export type SessionSseDeliveryOptions = SseDeliveryOptions & {
569
+ /** Host ACL re-check, run even while the event stream is otherwise idle. */
570
+ reauthorize?: (() => Promise<void>) | undefined;
571
+ reauthorizeAfterMs?: number | undefined;
572
+ };
573
+
574
+ function sseObservationReporter(
575
+ stream: "session" | "workspace_control",
576
+ options: SseDeliveryOptions,
577
+ ): (observation: SseDeliveryBoundObservation) => void {
578
+ return (observation) => {
579
+ options.onObservation?.(observation);
580
+ options.observability?.incrementCounter({
581
+ name: "opengeni_sse_delivery_bound_events_total",
582
+ help: "SSE writes that encountered a configured queue, frame, or stall bound.",
583
+ labels: { stream, reason: observation.reason },
584
+ });
585
+ if (observation.reason !== "desired_size_non_positive") {
586
+ options.observability?.warn("SSE delivery terminated at a bounded stream seam", {
587
+ stream,
588
+ reason: observation.reason,
589
+ desiredSize: observation.desiredSize,
590
+ queuedFrames: observation.queuedFrames,
591
+ queuedBytes: observation.queuedBytes,
592
+ });
593
+ }
594
+ };
595
+ }
596
+
597
+ function retryableSseFailure(message: string, error: unknown): TypeError {
598
+ return error instanceof TypeError ? error : new TypeError(message, { cause: error });
599
+ }