@agentuity/core 3.0.0-alpha.5 → 3.0.0-alpha.7
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/AGENTS.md +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +1 -1
- package/dist/services/coder/api-reference.d.ts.map +1 -1
- package/dist/services/coder/api-reference.js +30 -1
- package/dist/services/coder/api-reference.js.map +1 -1
- package/dist/services/coder/client.d.ts +5 -1
- package/dist/services/coder/client.d.ts.map +1 -1
- package/dist/services/coder/client.js +8 -1
- package/dist/services/coder/client.js.map +1 -1
- package/dist/services/coder/index.d.ts +2 -2
- package/dist/services/coder/index.d.ts.map +1 -1
- package/dist/services/coder/index.js +1 -1
- package/dist/services/coder/index.js.map +1 -1
- package/dist/services/coder/protocol.d.ts +65 -0
- package/dist/services/coder/protocol.d.ts.map +1 -1
- package/dist/services/coder/protocol.js +8 -0
- package/dist/services/coder/protocol.js.map +1 -1
- package/dist/services/coder/sessions.d.ts +22 -0
- package/dist/services/coder/sessions.d.ts.map +1 -1
- package/dist/services/coder/sessions.js +10 -1
- package/dist/services/coder/sessions.js.map +1 -1
- package/dist/services/coder/sse.d.ts +2 -2
- package/dist/services/coder/sse.d.ts.map +1 -1
- package/dist/services/coder/sse.js +290 -178
- package/dist/services/coder/sse.js.map +1 -1
- package/dist/services/coder/types.d.ts +554 -0
- package/dist/services/coder/types.d.ts.map +1 -1
- package/dist/services/coder/types.js +138 -0
- package/dist/services/coder/types.js.map +1 -1
- package/dist/services/index.d.ts +0 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +0 -2
- package/dist/services/index.js.map +1 -1
- package/dist/services/sandbox/run.d.ts.map +1 -1
- package/dist/services/sandbox/run.js +15 -2
- package/dist/services/sandbox/run.js.map +1 -1
- package/package.json +2 -16
- package/src/index.ts +0 -15
- package/src/services/coder/api-reference.ts +31 -0
- package/src/services/coder/client.ts +12 -0
- package/src/services/coder/index.ts +3 -0
- package/src/services/coder/protocol.ts +12 -0
- package/src/services/coder/sessions.ts +26 -0
- package/src/services/coder/sse.ts +343 -184
- package/src/services/coder/types.ts +179 -0
- package/src/services/index.ts +0 -2
- package/src/services/sandbox/run.ts +13 -2
- package/dist/services/auth/index.d.ts +0 -7
- package/dist/services/auth/index.d.ts.map +0 -1
- package/dist/services/auth/index.js +0 -7
- package/dist/services/auth/index.js.map +0 -1
- package/dist/services/auth/types.d.ts +0 -192
- package/dist/services/auth/types.d.ts.map +0 -1
- package/dist/services/auth/types.js +0 -11
- package/dist/services/auth/types.js.map +0 -1
- package/dist/services/eval/api-reference.d.ts +0 -4
- package/dist/services/eval/api-reference.d.ts.map +0 -1
- package/dist/services/eval/api-reference.js +0 -121
- package/dist/services/eval/api-reference.js.map +0 -1
- package/dist/services/eval/events.d.ts +0 -93
- package/dist/services/eval/events.d.ts.map +0 -1
- package/dist/services/eval/events.js +0 -24
- package/dist/services/eval/events.js.map +0 -1
- package/dist/services/eval/get.d.ts +0 -36
- package/dist/services/eval/get.d.ts.map +0 -1
- package/dist/services/eval/get.js +0 -23
- package/dist/services/eval/get.js.map +0 -1
- package/dist/services/eval/index.d.ts +0 -6
- package/dist/services/eval/index.d.ts.map +0 -1
- package/dist/services/eval/index.js +0 -6
- package/dist/services/eval/index.js.map +0 -1
- package/dist/services/eval/list.d.ts +0 -50
- package/dist/services/eval/list.d.ts.map +0 -1
- package/dist/services/eval/list.js +0 -32
- package/dist/services/eval/list.js.map +0 -1
- package/dist/services/eval/run-get.d.ts +0 -48
- package/dist/services/eval/run-get.d.ts.map +0 -1
- package/dist/services/eval/run-get.js +0 -29
- package/dist/services/eval/run-get.js.map +0 -1
- package/dist/services/eval/run-list.d.ts +0 -70
- package/dist/services/eval/run-list.d.ts.map +0 -1
- package/dist/services/eval/run-list.js +0 -42
- package/dist/services/eval/run-list.js.map +0 -1
- package/dist/webrtc.d.ts +0 -243
- package/dist/webrtc.d.ts.map +0 -1
- package/dist/webrtc.js +0 -5
- package/dist/webrtc.js.map +0 -1
- package/dist/workbench-config.d.ts +0 -62
- package/dist/workbench-config.d.ts.map +0 -1
- package/dist/workbench-config.js +0 -58
- package/dist/workbench-config.js.map +0 -1
- package/dist/workbench.d.ts +0 -2
- package/dist/workbench.d.ts.map +0 -1
- package/dist/workbench.js +0 -2
- package/dist/workbench.js.map +0 -1
- package/src/services/auth/index.ts +0 -19
- package/src/services/auth/types.ts +0 -223
- package/src/services/eval/api-reference.ts +0 -124
- package/src/services/eval/events.ts +0 -92
- package/src/services/eval/get.ts +0 -33
- package/src/services/eval/index.ts +0 -29
- package/src/services/eval/list.ts +0 -49
- package/src/services/eval/run-get.ts +0 -39
- package/src/services/eval/run-list.ts +0 -59
- package/src/webrtc.ts +0 -259
- package/src/workbench-config.ts +0 -79
- package/src/workbench.ts +0 -1
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* })) {
|
|
46
46
|
* console.log('Event:', event.event, event.data);
|
|
47
47
|
*
|
|
48
|
-
* if (event.
|
|
48
|
+
* if (event.data.type === 'broadcast' && event.data.event === 'session_complete') {
|
|
49
49
|
* controller.abort(); // Stop the stream
|
|
50
50
|
* }
|
|
51
51
|
* }
|
|
@@ -189,12 +189,124 @@ async function buildSSEUrl(sessionId, options) {
|
|
|
189
189
|
const queryString = params.toString();
|
|
190
190
|
return queryString ? `${baseUrl}${path}?${queryString}` : `${baseUrl}${path}`;
|
|
191
191
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
const TYPED_TRANSPORT_EVENTS = new Set(['snapshot', 'hydration', 'presence', 'broadcast']);
|
|
193
|
+
function isAbortError(err) {
|
|
194
|
+
return err instanceof Error && err.name === 'AbortError';
|
|
195
|
+
}
|
|
196
|
+
function parseSSEFrame(block) {
|
|
197
|
+
let event = 'message';
|
|
198
|
+
const dataLines = [];
|
|
199
|
+
for (const line of block.split('\n')) {
|
|
200
|
+
if (!line || line.startsWith(':'))
|
|
201
|
+
continue;
|
|
202
|
+
const separatorIndex = line.indexOf(':');
|
|
203
|
+
const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
|
|
204
|
+
let value = separatorIndex === -1 ? '' : line.slice(separatorIndex + 1);
|
|
205
|
+
if (value.startsWith(' ')) {
|
|
206
|
+
value = value.slice(1);
|
|
207
|
+
}
|
|
208
|
+
if (field === 'event') {
|
|
209
|
+
event = value || 'message';
|
|
210
|
+
}
|
|
211
|
+
else if (field === 'data') {
|
|
212
|
+
dataLines.push(value);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (dataLines.length === 0) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
event,
|
|
220
|
+
data: dataLines.join('\n'),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function consumeSSEBuffer(rawBuffer, onFrame) {
|
|
224
|
+
const normalized = rawBuffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
225
|
+
let cursor = 0;
|
|
226
|
+
while (true) {
|
|
227
|
+
const boundary = normalized.indexOf('\n\n', cursor);
|
|
228
|
+
if (boundary === -1)
|
|
229
|
+
break;
|
|
230
|
+
const block = normalized.slice(cursor, boundary);
|
|
231
|
+
cursor = boundary + 2;
|
|
232
|
+
if (!block.trim())
|
|
233
|
+
continue;
|
|
234
|
+
const frame = parseSSEFrame(block);
|
|
235
|
+
if (frame) {
|
|
236
|
+
onFrame(frame);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return normalized.slice(cursor);
|
|
240
|
+
}
|
|
241
|
+
function decodeCoderSSEEvent(frame, sessionId) {
|
|
242
|
+
let parsed;
|
|
243
|
+
try {
|
|
244
|
+
parsed = JSON.parse(frame.data);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
throw new CoderSSEError({
|
|
248
|
+
message: `Failed to parse SSE ${frame.event} event: ${err instanceof Error ? err.message : String(err)}`,
|
|
249
|
+
code: 'parse_error',
|
|
250
|
+
sessionId,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const payload = TYPED_TRANSPORT_EVENTS.has(frame.event) && parsed && typeof parsed === 'object'
|
|
254
|
+
? { type: frame.event, ...parsed }
|
|
255
|
+
: parsed;
|
|
256
|
+
const result = ObserverSseMessageSchema.safeParse(payload);
|
|
257
|
+
if (!result.success) {
|
|
258
|
+
throw new CoderSSEError({
|
|
259
|
+
message: `Invalid SSE ${frame.event} event format`,
|
|
260
|
+
code: 'parse_error',
|
|
261
|
+
sessionId,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
const event = frame.event === 'message' ? result.data.type : frame.event;
|
|
265
|
+
return { event, data: result.data };
|
|
266
|
+
}
|
|
267
|
+
async function readSSEStream(response, signal, onEvent, sessionId) {
|
|
268
|
+
if (!response.body) {
|
|
269
|
+
throw new CoderSSEError({
|
|
270
|
+
message: 'SSE response did not include a readable body',
|
|
271
|
+
code: 'connection_failed',
|
|
272
|
+
sessionId,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const reader = response.body.getReader();
|
|
276
|
+
const decoder = new TextDecoder();
|
|
277
|
+
let buffer = '';
|
|
278
|
+
try {
|
|
279
|
+
while (!signal.aborted) {
|
|
280
|
+
const { done, value } = await reader.read();
|
|
281
|
+
if (done)
|
|
282
|
+
break;
|
|
283
|
+
buffer += decoder.decode(value, { stream: true });
|
|
284
|
+
buffer = consumeSSEBuffer(buffer, (frame) => {
|
|
285
|
+
onEvent(decodeCoderSSEEvent(frame, sessionId));
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
try {
|
|
291
|
+
reader.releaseLock();
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
// Some runtimes throw if the reader is already released after abort.
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function buildConnectionError(response, sessionId) {
|
|
299
|
+
return new CoderSSEError({
|
|
300
|
+
message: `SSE connection failed: ${response.status} ${response.statusText || 'HTTP error'}`,
|
|
301
|
+
code: response.status === 401 || response.status === 403 ? 'auth_failed' : 'connection_failed',
|
|
302
|
+
sessionId,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
function isRetryableStreamError(error) {
|
|
306
|
+
if (error instanceof CoderSSEError) {
|
|
307
|
+
return error.code === 'connection_failed';
|
|
196
308
|
}
|
|
197
|
-
return
|
|
309
|
+
return true;
|
|
198
310
|
}
|
|
199
311
|
/**
|
|
200
312
|
* Class-based SSE client for observing Coder Hub sessions.
|
|
@@ -225,7 +337,7 @@ function getSSEData(event) {
|
|
|
225
337
|
export class CoderSSEClient {
|
|
226
338
|
#options;
|
|
227
339
|
#state = 'closed';
|
|
228
|
-
#
|
|
340
|
+
#abortController = null;
|
|
229
341
|
#reconnectAttempts = 0;
|
|
230
342
|
#reconnectTimer = null;
|
|
231
343
|
#intentionallyClosed = false;
|
|
@@ -267,7 +379,7 @@ export class CoderSSEClient {
|
|
|
267
379
|
* Whether the client is currently connected and receiving events.
|
|
268
380
|
*/
|
|
269
381
|
get isConnected() {
|
|
270
|
-
return this.#state === 'connected' && this.#
|
|
382
|
+
return this.#state === 'connected' && this.#abortController !== null;
|
|
271
383
|
}
|
|
272
384
|
/**
|
|
273
385
|
* Establish the SSE connection and start receiving events.
|
|
@@ -298,9 +410,9 @@ export class CoderSSEClient {
|
|
|
298
410
|
clearTimeout(this.#reconnectTimer);
|
|
299
411
|
this.#reconnectTimer = null;
|
|
300
412
|
}
|
|
301
|
-
if (this.#
|
|
302
|
-
this.#
|
|
303
|
-
this.#
|
|
413
|
+
if (this.#abortController) {
|
|
414
|
+
this.#abortController.abort();
|
|
415
|
+
this.#abortController = null;
|
|
304
416
|
}
|
|
305
417
|
this.#state = 'closed';
|
|
306
418
|
this.#options.onClose?.();
|
|
@@ -308,50 +420,20 @@ export class CoderSSEClient {
|
|
|
308
420
|
#setState(state) {
|
|
309
421
|
this.#state = state;
|
|
310
422
|
}
|
|
311
|
-
#
|
|
312
|
-
this.#
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
this.#options.onSnapshot?.(result.data);
|
|
326
|
-
}
|
|
327
|
-
else if (result.data.type === 'hydration') {
|
|
328
|
-
this.#options.onHydration?.(result.data);
|
|
329
|
-
}
|
|
330
|
-
else if (result.data.type === 'presence') {
|
|
331
|
-
this.#options.onPresence?.(result.data);
|
|
332
|
-
}
|
|
333
|
-
else if (result.data.type === 'broadcast') {
|
|
334
|
-
this.#options.onBroadcast?.(result.data);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
const parseError = new CoderSSEError({
|
|
339
|
-
message: `Invalid SSE ${eventName} event format`,
|
|
340
|
-
code: 'parse_error',
|
|
341
|
-
sessionId: this.#options.sessionId,
|
|
342
|
-
});
|
|
343
|
-
this.#options.onError?.(parseError);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
catch (err) {
|
|
347
|
-
const parseError = new CoderSSEError({
|
|
348
|
-
message: `Failed to parse SSE ${eventName} event: ${err instanceof Error ? err.message : String(err)}`,
|
|
349
|
-
code: 'parse_error',
|
|
350
|
-
sessionId: this.#options.sessionId,
|
|
351
|
-
});
|
|
352
|
-
this.#options.onError?.(parseError);
|
|
353
|
-
}
|
|
354
|
-
});
|
|
423
|
+
#dispatchEvent(event) {
|
|
424
|
+
this.#options.onEvent?.(event);
|
|
425
|
+
if (event.data.type === 'snapshot') {
|
|
426
|
+
this.#options.onSnapshot?.(event.data);
|
|
427
|
+
}
|
|
428
|
+
else if (event.data.type === 'hydration') {
|
|
429
|
+
this.#options.onHydration?.(event.data);
|
|
430
|
+
}
|
|
431
|
+
else if (event.data.type === 'presence') {
|
|
432
|
+
this.#options.onPresence?.(event.data);
|
|
433
|
+
}
|
|
434
|
+
else if (event.data.type === 'broadcast') {
|
|
435
|
+
this.#options.onBroadcast?.(event.data);
|
|
436
|
+
}
|
|
355
437
|
}
|
|
356
438
|
async #connectInternal() {
|
|
357
439
|
if (this.#intentionallyClosed) {
|
|
@@ -370,46 +452,60 @@ export class CoderSSEClient {
|
|
|
370
452
|
if (this.#intentionallyClosed || this.#state === 'closed') {
|
|
371
453
|
return;
|
|
372
454
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
455
|
+
const controller = new AbortController();
|
|
456
|
+
this.#abortController = controller;
|
|
457
|
+
let response;
|
|
376
458
|
try {
|
|
377
|
-
|
|
378
|
-
|
|
459
|
+
response = await fetch(url, {
|
|
460
|
+
headers: {
|
|
461
|
+
accept: 'text/event-stream',
|
|
462
|
+
},
|
|
463
|
+
signal: controller.signal,
|
|
464
|
+
});
|
|
379
465
|
}
|
|
380
466
|
catch (err) {
|
|
467
|
+
this.#abortController = null;
|
|
468
|
+
if (this.#intentionallyClosed || isAbortError(err)) {
|
|
469
|
+
this.#setState('closed');
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
381
472
|
this.#setState('closed');
|
|
382
473
|
this.#options.onError?.(new CoderSSEError({
|
|
383
|
-
message: `Failed to
|
|
474
|
+
message: `Failed to connect SSE stream: ${err instanceof Error ? err.message : String(err)}`,
|
|
384
475
|
code: 'connection_failed',
|
|
385
476
|
sessionId: this.#options.sessionId,
|
|
386
477
|
}));
|
|
387
478
|
this.#scheduleReconnect();
|
|
388
479
|
return;
|
|
389
480
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
this.#
|
|
393
|
-
|
|
394
|
-
this.#eventSource.close();
|
|
395
|
-
this.#eventSource = null;
|
|
396
|
-
}
|
|
397
|
-
if (this.#intentionallyClosed) {
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
481
|
+
if (!response.ok) {
|
|
482
|
+
this.#abortController = null;
|
|
483
|
+
this.#setState('closed');
|
|
484
|
+
this.#options.onError?.(buildConnectionError(response, this.#options.sessionId));
|
|
400
485
|
this.#scheduleReconnect();
|
|
401
|
-
|
|
402
|
-
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
403
489
|
this.#reconnectAttempts = 0;
|
|
404
490
|
this.#setState('connected');
|
|
405
491
|
this.#options.logger.debug('SSE connection established for session %s', this.#options.sessionId);
|
|
406
492
|
this.#options.onOpen?.();
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
493
|
+
await readSSEStream(response, controller.signal, (event) => this.#dispatchEvent(event), this.#options.sessionId);
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
if (this.#intentionallyClosed || isAbortError(err)) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
this.#options.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
if (this.#abortController === controller) {
|
|
503
|
+
this.#abortController = null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (!this.#intentionallyClosed) {
|
|
507
|
+
this.#scheduleReconnect();
|
|
508
|
+
}
|
|
413
509
|
}
|
|
414
510
|
#scheduleReconnect() {
|
|
415
511
|
if (this.#intentionallyClosed || !this.#options.reconnect) {
|
|
@@ -498,13 +594,14 @@ export async function* streamCoderSessionSSE(options) {
|
|
|
498
594
|
if (signal?.aborted) {
|
|
499
595
|
return;
|
|
500
596
|
}
|
|
501
|
-
let
|
|
597
|
+
let activeController = null;
|
|
502
598
|
let reconnectAttempts = 0;
|
|
503
599
|
const buffer = [];
|
|
504
600
|
const MAX_BUFFER = 1000;
|
|
505
601
|
let resolve = null;
|
|
506
602
|
let done = false;
|
|
507
603
|
let terminalError = null;
|
|
604
|
+
let reconnectTimer = null;
|
|
508
605
|
const wake = () => {
|
|
509
606
|
if (resolve) {
|
|
510
607
|
resolve();
|
|
@@ -512,51 +609,55 @@ export async function* streamCoderSessionSSE(options) {
|
|
|
512
609
|
}
|
|
513
610
|
};
|
|
514
611
|
const cleanup = () => {
|
|
515
|
-
if (
|
|
516
|
-
|
|
517
|
-
|
|
612
|
+
if (reconnectTimer !== null) {
|
|
613
|
+
clearTimeout(reconnectTimer);
|
|
614
|
+
reconnectTimer = null;
|
|
615
|
+
}
|
|
616
|
+
if (activeController) {
|
|
617
|
+
activeController.abort();
|
|
618
|
+
activeController = null;
|
|
518
619
|
}
|
|
519
620
|
};
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
});
|
|
621
|
+
const finish = (error) => {
|
|
622
|
+
if (error) {
|
|
623
|
+
terminalError = error;
|
|
624
|
+
}
|
|
625
|
+
done = true;
|
|
626
|
+
wake();
|
|
627
|
+
};
|
|
628
|
+
const scheduleReconnect = (error) => {
|
|
629
|
+
if (done) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (signal?.aborted || (error && isAbortError(error))) {
|
|
633
|
+
finish();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (error && !isRetryableStreamError(error)) {
|
|
637
|
+
finish(error);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (!reconnect) {
|
|
641
|
+
finish(error);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (reconnectAttempts >= maxReconnectAttempts) {
|
|
645
|
+
finish(new CoderSSEError({
|
|
646
|
+
message: `Exceeded maximum reconnection attempts (${maxReconnectAttempts})`,
|
|
647
|
+
code: 'max_reconnects_exceeded',
|
|
648
|
+
sessionId: options.sessionId,
|
|
649
|
+
}));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const baseDelay = reconnectDelayMs * 2 ** reconnectAttempts;
|
|
653
|
+
const jitter = 0.5 + Math.random() * 0.5;
|
|
654
|
+
const delay = Math.min(Math.floor(baseDelay * jitter), maxReconnectDelayMs);
|
|
655
|
+
reconnectAttempts++;
|
|
656
|
+
logger.debug('SSE connection lost, reconnecting in %dms (attempt %d)', delay, reconnectAttempts);
|
|
657
|
+
reconnectTimer = setTimeout(() => {
|
|
658
|
+
reconnectTimer = null;
|
|
659
|
+
void connect();
|
|
660
|
+
}, delay);
|
|
560
661
|
};
|
|
561
662
|
const connect = async () => {
|
|
562
663
|
if (done || signal?.aborted) {
|
|
@@ -570,82 +671,92 @@ export async function* streamCoderSessionSSE(options) {
|
|
|
570
671
|
});
|
|
571
672
|
}
|
|
572
673
|
catch (err) {
|
|
573
|
-
|
|
574
|
-
done = true;
|
|
575
|
-
wake();
|
|
674
|
+
finish(err instanceof Error ? err : new Error(String(err)));
|
|
576
675
|
return;
|
|
577
676
|
}
|
|
578
677
|
if (signal?.aborted) {
|
|
579
|
-
|
|
580
|
-
wake();
|
|
678
|
+
finish();
|
|
581
679
|
return;
|
|
582
680
|
}
|
|
583
|
-
|
|
681
|
+
const controller = new AbortController();
|
|
682
|
+
activeController = controller;
|
|
683
|
+
const abortFromCaller = () => controller.abort();
|
|
684
|
+
signal?.addEventListener('abort', abortFromCaller, { once: true });
|
|
685
|
+
let response;
|
|
584
686
|
try {
|
|
585
|
-
|
|
586
|
-
|
|
687
|
+
response = await fetch(url, {
|
|
688
|
+
headers: {
|
|
689
|
+
accept: 'text/event-stream',
|
|
690
|
+
},
|
|
691
|
+
signal: controller.signal,
|
|
692
|
+
});
|
|
587
693
|
}
|
|
588
694
|
catch (err) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
695
|
+
signal?.removeEventListener('abort', abortFromCaller);
|
|
696
|
+
if (activeController === controller) {
|
|
697
|
+
activeController = null;
|
|
698
|
+
}
|
|
699
|
+
if (signal?.aborted || isAbortError(err)) {
|
|
700
|
+
finish();
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const error = err instanceof CoderSSEError
|
|
704
|
+
? err
|
|
705
|
+
: new CoderSSEError({
|
|
706
|
+
message: `Failed to connect SSE stream: ${err instanceof Error ? err.message : String(err)}`,
|
|
707
|
+
code: 'connection_failed',
|
|
708
|
+
sessionId: options.sessionId,
|
|
709
|
+
});
|
|
710
|
+
scheduleReconnect(error);
|
|
596
711
|
return;
|
|
597
712
|
}
|
|
598
713
|
if (signal?.aborted) {
|
|
714
|
+
signal?.removeEventListener('abort', abortFromCaller);
|
|
599
715
|
cleanup();
|
|
600
|
-
|
|
601
|
-
wake();
|
|
716
|
+
finish();
|
|
602
717
|
return;
|
|
603
718
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
if (
|
|
607
|
-
|
|
608
|
-
wake();
|
|
609
|
-
return;
|
|
719
|
+
if (!response.ok) {
|
|
720
|
+
signal?.removeEventListener('abort', abortFromCaller);
|
|
721
|
+
if (activeController === controller) {
|
|
722
|
+
activeController = null;
|
|
610
723
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
724
|
+
scheduleReconnect(buildConnectionError(response, options.sessionId));
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
reconnectAttempts = 0;
|
|
728
|
+
logger.debug('SSE connection established for session %s', options.sessionId);
|
|
729
|
+
let readFailed = false;
|
|
730
|
+
void readSSEStream(response, controller.signal, (event) => {
|
|
731
|
+
if (buffer.length >= MAX_BUFFER) {
|
|
732
|
+
buffer.shift();
|
|
733
|
+
logger.debug('SSE buffer full, dropped oldest event');
|
|
620
734
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
735
|
+
buffer.push(event);
|
|
736
|
+
wake();
|
|
737
|
+
}, options.sessionId)
|
|
738
|
+
.catch((err) => {
|
|
739
|
+
readFailed = true;
|
|
740
|
+
scheduleReconnect(err instanceof Error ? err : new Error(String(err)));
|
|
741
|
+
})
|
|
742
|
+
.finally(() => {
|
|
743
|
+
signal?.removeEventListener('abort', abortFromCaller);
|
|
744
|
+
if (activeController === controller) {
|
|
745
|
+
activeController = null;
|
|
629
746
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
747
|
+
if (done || signal?.aborted) {
|
|
748
|
+
finish();
|
|
749
|
+
return;
|
|
633
750
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
};
|
|
639
|
-
handleSSEEvent('snapshot', 'snapshot');
|
|
640
|
-
handleSSEEvent('hydration', 'hydration');
|
|
641
|
-
handleSSEEvent('presence', 'presence');
|
|
642
|
-
handleSSEEvent('broadcast', 'broadcast');
|
|
643
|
-
handleSSEEvent('message');
|
|
751
|
+
if (readFailed) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
scheduleReconnect();
|
|
755
|
+
});
|
|
644
756
|
};
|
|
645
757
|
const onAbort = () => {
|
|
646
|
-
done = true;
|
|
647
758
|
cleanup();
|
|
648
|
-
|
|
759
|
+
finish();
|
|
649
760
|
};
|
|
650
761
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
651
762
|
await connect();
|
|
@@ -670,6 +781,7 @@ export async function* streamCoderSessionSSE(options) {
|
|
|
670
781
|
}
|
|
671
782
|
finally {
|
|
672
783
|
signal?.removeEventListener('abort', onAbort);
|
|
784
|
+
done = true;
|
|
673
785
|
cleanup();
|
|
674
786
|
}
|
|
675
787
|
}
|