@bagelink/sdk 1.12.53 → 1.12.57
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/dist/index.cjs +61 -21
- package/dist/index.d.cts +14 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.mjs +61 -21
- package/package.json +1 -1
- package/src/openAPITools/streamClient.ts +118 -72
package/dist/index.cjs
CHANGED
|
@@ -1265,29 +1265,50 @@ class StreamController {
|
|
|
1265
1265
|
}
|
|
1266
1266
|
|
|
1267
1267
|
function createSSEStream(url, options = {}) {
|
|
1268
|
-
const {
|
|
1268
|
+
const {
|
|
1269
|
+
headers = {},
|
|
1270
|
+
withCredentials = true,
|
|
1271
|
+
lastEventId: initialLastEventId,
|
|
1272
|
+
maxReconnects = 5,
|
|
1273
|
+
reconnectDelayMs = 1e3
|
|
1274
|
+
} = options;
|
|
1269
1275
|
const controller = new AbortController();
|
|
1270
1276
|
const stream = new StreamController(() => {
|
|
1271
1277
|
controller.abort();
|
|
1272
1278
|
});
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1279
|
+
let lastEventId = initialLastEventId;
|
|
1280
|
+
let reconnectAttempts = 0;
|
|
1281
|
+
async function connect() {
|
|
1282
|
+
const requestHeaders = {
|
|
1276
1283
|
"Accept": "text/event-stream",
|
|
1277
1284
|
"Cache-Control": "no-cache",
|
|
1278
1285
|
...headers
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1286
|
+
};
|
|
1287
|
+
if (lastEventId !== void 0) {
|
|
1288
|
+
requestHeaders["Last-Event-ID"] = lastEventId;
|
|
1289
|
+
}
|
|
1290
|
+
let response;
|
|
1291
|
+
try {
|
|
1292
|
+
response = await fetch(url, {
|
|
1293
|
+
method: "GET",
|
|
1294
|
+
headers: requestHeaders,
|
|
1295
|
+
credentials: withCredentials ? "include" : "same-origin",
|
|
1296
|
+
signal: controller.signal,
|
|
1297
|
+
keepalive: false
|
|
1298
|
+
});
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
if (error?.name === "AbortError") return;
|
|
1301
|
+
return handleDisconnect(error);
|
|
1302
|
+
}
|
|
1285
1303
|
if (!response.ok) {
|
|
1286
|
-
|
|
1304
|
+
stream.emit("error", new Error(`HTTP ${response.status}: ${response.statusText}`));
|
|
1305
|
+
return;
|
|
1287
1306
|
}
|
|
1288
1307
|
if (!response.body) {
|
|
1289
|
-
|
|
1308
|
+
stream.emit("error", new Error("Response body is null"));
|
|
1309
|
+
return;
|
|
1290
1310
|
}
|
|
1311
|
+
reconnectAttempts = 0;
|
|
1291
1312
|
const reader = response.body.getReader();
|
|
1292
1313
|
const decoder = new TextDecoder();
|
|
1293
1314
|
let buffer = "";
|
|
@@ -1295,8 +1316,10 @@ function createSSEStream(url, options = {}) {
|
|
|
1295
1316
|
while (true) {
|
|
1296
1317
|
const { done, value } = await reader.read();
|
|
1297
1318
|
if (done) {
|
|
1298
|
-
|
|
1299
|
-
|
|
1319
|
+
if (!controller.signal.aborted) {
|
|
1320
|
+
stream.emit("done", {});
|
|
1321
|
+
stream.emit("complete", {});
|
|
1322
|
+
}
|
|
1300
1323
|
break;
|
|
1301
1324
|
}
|
|
1302
1325
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -1304,27 +1327,35 @@ function createSSEStream(url, options = {}) {
|
|
|
1304
1327
|
buffer = lines.pop() || "";
|
|
1305
1328
|
let currentEvent = "message";
|
|
1306
1329
|
let currentData = "";
|
|
1330
|
+
let currentId;
|
|
1307
1331
|
for (const line of lines) {
|
|
1308
|
-
if (line.startsWith("
|
|
1332
|
+
if (line.startsWith("id:")) {
|
|
1333
|
+
currentId = line.slice(3).trim();
|
|
1334
|
+
} else if (line.startsWith("event:")) {
|
|
1309
1335
|
currentEvent = line.slice(6).trim();
|
|
1310
1336
|
} else if (line.startsWith("data:")) {
|
|
1311
1337
|
currentData = line.slice(5).trim();
|
|
1312
1338
|
} else if (line === "" && currentData) {
|
|
1339
|
+
if (currentId !== void 0) lastEventId = currentId;
|
|
1313
1340
|
try {
|
|
1314
1341
|
const parsed = JSON.parse(currentData);
|
|
1315
1342
|
stream.emit(currentEvent, parsed);
|
|
1343
|
+
if (currentEvent === "done" || currentEvent === "error") {
|
|
1344
|
+
stream.emit("complete", {});
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1316
1347
|
} catch {
|
|
1317
1348
|
stream.emit(currentEvent, currentData);
|
|
1318
1349
|
}
|
|
1319
1350
|
currentEvent = "message";
|
|
1320
1351
|
currentData = "";
|
|
1352
|
+
currentId = void 0;
|
|
1321
1353
|
}
|
|
1322
1354
|
}
|
|
1323
1355
|
}
|
|
1324
1356
|
} catch (error) {
|
|
1325
1357
|
if (error?.name !== "AbortError") {
|
|
1326
|
-
|
|
1327
|
-
stream.emit("error", new Error(errorMessage));
|
|
1358
|
+
return handleDisconnect(error);
|
|
1328
1359
|
}
|
|
1329
1360
|
} finally {
|
|
1330
1361
|
try {
|
|
@@ -1332,11 +1363,20 @@ function createSSEStream(url, options = {}) {
|
|
|
1332
1363
|
} catch {
|
|
1333
1364
|
}
|
|
1334
1365
|
}
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1366
|
+
}
|
|
1367
|
+
async function handleDisconnect(error) {
|
|
1368
|
+
if (controller.signal.aborted) return;
|
|
1369
|
+
if (reconnectAttempts >= maxReconnects) {
|
|
1370
|
+
const msg = error?.message || "Stream connection lost";
|
|
1371
|
+
stream.emit("error", new Error(`${msg} (gave up after ${maxReconnects} reconnects)`));
|
|
1372
|
+
return;
|
|
1339
1373
|
}
|
|
1374
|
+
reconnectAttempts++;
|
|
1375
|
+
const delay = reconnectDelayMs * Math.pow(2, reconnectAttempts - 1);
|
|
1376
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1377
|
+
if (!controller.signal.aborted) await connect();
|
|
1378
|
+
}
|
|
1379
|
+
connect().catch(() => {
|
|
1340
1380
|
});
|
|
1341
1381
|
return stream;
|
|
1342
1382
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -55,6 +55,20 @@ interface SSEStreamOptions {
|
|
|
55
55
|
headers?: Record<string, string>;
|
|
56
56
|
/** Whether to include credentials (cookies) with the request */
|
|
57
57
|
withCredentials?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Last-Event-ID to resume from on reconnect.
|
|
60
|
+
* The server uses this to replay buffered events from that sequence number.
|
|
61
|
+
*/
|
|
62
|
+
lastEventId?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Maximum number of automatic reconnect attempts (GET streams only).
|
|
65
|
+
* Set to 0 to disable auto-reconnect. Defaults to 5.
|
|
66
|
+
*/
|
|
67
|
+
maxReconnects?: number;
|
|
68
|
+
/**
|
|
69
|
+
* Base delay in ms between reconnect attempts (exponential backoff). Defaults to 1000.
|
|
70
|
+
*/
|
|
71
|
+
reconnectDelayMs?: number;
|
|
58
72
|
}
|
|
59
73
|
/**
|
|
60
74
|
* Creates an SSE stream consumer with elegant event-based API
|
package/dist/index.d.mts
CHANGED
|
@@ -55,6 +55,20 @@ interface SSEStreamOptions {
|
|
|
55
55
|
headers?: Record<string, string>;
|
|
56
56
|
/** Whether to include credentials (cookies) with the request */
|
|
57
57
|
withCredentials?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Last-Event-ID to resume from on reconnect.
|
|
60
|
+
* The server uses this to replay buffered events from that sequence number.
|
|
61
|
+
*/
|
|
62
|
+
lastEventId?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Maximum number of automatic reconnect attempts (GET streams only).
|
|
65
|
+
* Set to 0 to disable auto-reconnect. Defaults to 5.
|
|
66
|
+
*/
|
|
67
|
+
maxReconnects?: number;
|
|
68
|
+
/**
|
|
69
|
+
* Base delay in ms between reconnect attempts (exponential backoff). Defaults to 1000.
|
|
70
|
+
*/
|
|
71
|
+
reconnectDelayMs?: number;
|
|
58
72
|
}
|
|
59
73
|
/**
|
|
60
74
|
* Creates an SSE stream consumer with elegant event-based API
|
package/dist/index.d.ts
CHANGED
|
@@ -55,6 +55,20 @@ interface SSEStreamOptions {
|
|
|
55
55
|
headers?: Record<string, string>;
|
|
56
56
|
/** Whether to include credentials (cookies) with the request */
|
|
57
57
|
withCredentials?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Last-Event-ID to resume from on reconnect.
|
|
60
|
+
* The server uses this to replay buffered events from that sequence number.
|
|
61
|
+
*/
|
|
62
|
+
lastEventId?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Maximum number of automatic reconnect attempts (GET streams only).
|
|
65
|
+
* Set to 0 to disable auto-reconnect. Defaults to 5.
|
|
66
|
+
*/
|
|
67
|
+
maxReconnects?: number;
|
|
68
|
+
/**
|
|
69
|
+
* Base delay in ms between reconnect attempts (exponential backoff). Defaults to 1000.
|
|
70
|
+
*/
|
|
71
|
+
reconnectDelayMs?: number;
|
|
58
72
|
}
|
|
59
73
|
/**
|
|
60
74
|
* Creates an SSE stream consumer with elegant event-based API
|
package/dist/index.mjs
CHANGED
|
@@ -1259,29 +1259,50 @@ class StreamController {
|
|
|
1259
1259
|
}
|
|
1260
1260
|
|
|
1261
1261
|
function createSSEStream(url, options = {}) {
|
|
1262
|
-
const {
|
|
1262
|
+
const {
|
|
1263
|
+
headers = {},
|
|
1264
|
+
withCredentials = true,
|
|
1265
|
+
lastEventId: initialLastEventId,
|
|
1266
|
+
maxReconnects = 5,
|
|
1267
|
+
reconnectDelayMs = 1e3
|
|
1268
|
+
} = options;
|
|
1263
1269
|
const controller = new AbortController();
|
|
1264
1270
|
const stream = new StreamController(() => {
|
|
1265
1271
|
controller.abort();
|
|
1266
1272
|
});
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1273
|
+
let lastEventId = initialLastEventId;
|
|
1274
|
+
let reconnectAttempts = 0;
|
|
1275
|
+
async function connect() {
|
|
1276
|
+
const requestHeaders = {
|
|
1270
1277
|
"Accept": "text/event-stream",
|
|
1271
1278
|
"Cache-Control": "no-cache",
|
|
1272
1279
|
...headers
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1280
|
+
};
|
|
1281
|
+
if (lastEventId !== void 0) {
|
|
1282
|
+
requestHeaders["Last-Event-ID"] = lastEventId;
|
|
1283
|
+
}
|
|
1284
|
+
let response;
|
|
1285
|
+
try {
|
|
1286
|
+
response = await fetch(url, {
|
|
1287
|
+
method: "GET",
|
|
1288
|
+
headers: requestHeaders,
|
|
1289
|
+
credentials: withCredentials ? "include" : "same-origin",
|
|
1290
|
+
signal: controller.signal,
|
|
1291
|
+
keepalive: false
|
|
1292
|
+
});
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
if (error?.name === "AbortError") return;
|
|
1295
|
+
return handleDisconnect(error);
|
|
1296
|
+
}
|
|
1279
1297
|
if (!response.ok) {
|
|
1280
|
-
|
|
1298
|
+
stream.emit("error", new Error(`HTTP ${response.status}: ${response.statusText}`));
|
|
1299
|
+
return;
|
|
1281
1300
|
}
|
|
1282
1301
|
if (!response.body) {
|
|
1283
|
-
|
|
1302
|
+
stream.emit("error", new Error("Response body is null"));
|
|
1303
|
+
return;
|
|
1284
1304
|
}
|
|
1305
|
+
reconnectAttempts = 0;
|
|
1285
1306
|
const reader = response.body.getReader();
|
|
1286
1307
|
const decoder = new TextDecoder();
|
|
1287
1308
|
let buffer = "";
|
|
@@ -1289,8 +1310,10 @@ function createSSEStream(url, options = {}) {
|
|
|
1289
1310
|
while (true) {
|
|
1290
1311
|
const { done, value } = await reader.read();
|
|
1291
1312
|
if (done) {
|
|
1292
|
-
|
|
1293
|
-
|
|
1313
|
+
if (!controller.signal.aborted) {
|
|
1314
|
+
stream.emit("done", {});
|
|
1315
|
+
stream.emit("complete", {});
|
|
1316
|
+
}
|
|
1294
1317
|
break;
|
|
1295
1318
|
}
|
|
1296
1319
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -1298,27 +1321,35 @@ function createSSEStream(url, options = {}) {
|
|
|
1298
1321
|
buffer = lines.pop() || "";
|
|
1299
1322
|
let currentEvent = "message";
|
|
1300
1323
|
let currentData = "";
|
|
1324
|
+
let currentId;
|
|
1301
1325
|
for (const line of lines) {
|
|
1302
|
-
if (line.startsWith("
|
|
1326
|
+
if (line.startsWith("id:")) {
|
|
1327
|
+
currentId = line.slice(3).trim();
|
|
1328
|
+
} else if (line.startsWith("event:")) {
|
|
1303
1329
|
currentEvent = line.slice(6).trim();
|
|
1304
1330
|
} else if (line.startsWith("data:")) {
|
|
1305
1331
|
currentData = line.slice(5).trim();
|
|
1306
1332
|
} else if (line === "" && currentData) {
|
|
1333
|
+
if (currentId !== void 0) lastEventId = currentId;
|
|
1307
1334
|
try {
|
|
1308
1335
|
const parsed = JSON.parse(currentData);
|
|
1309
1336
|
stream.emit(currentEvent, parsed);
|
|
1337
|
+
if (currentEvent === "done" || currentEvent === "error") {
|
|
1338
|
+
stream.emit("complete", {});
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1310
1341
|
} catch {
|
|
1311
1342
|
stream.emit(currentEvent, currentData);
|
|
1312
1343
|
}
|
|
1313
1344
|
currentEvent = "message";
|
|
1314
1345
|
currentData = "";
|
|
1346
|
+
currentId = void 0;
|
|
1315
1347
|
}
|
|
1316
1348
|
}
|
|
1317
1349
|
}
|
|
1318
1350
|
} catch (error) {
|
|
1319
1351
|
if (error?.name !== "AbortError") {
|
|
1320
|
-
|
|
1321
|
-
stream.emit("error", new Error(errorMessage));
|
|
1352
|
+
return handleDisconnect(error);
|
|
1322
1353
|
}
|
|
1323
1354
|
} finally {
|
|
1324
1355
|
try {
|
|
@@ -1326,11 +1357,20 @@ function createSSEStream(url, options = {}) {
|
|
|
1326
1357
|
} catch {
|
|
1327
1358
|
}
|
|
1328
1359
|
}
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1360
|
+
}
|
|
1361
|
+
async function handleDisconnect(error) {
|
|
1362
|
+
if (controller.signal.aborted) return;
|
|
1363
|
+
if (reconnectAttempts >= maxReconnects) {
|
|
1364
|
+
const msg = error?.message || "Stream connection lost";
|
|
1365
|
+
stream.emit("error", new Error(`${msg} (gave up after ${maxReconnects} reconnects)`));
|
|
1366
|
+
return;
|
|
1333
1367
|
}
|
|
1368
|
+
reconnectAttempts++;
|
|
1369
|
+
const delay = reconnectDelayMs * Math.pow(2, reconnectAttempts - 1);
|
|
1370
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1371
|
+
if (!controller.signal.aborted) await connect();
|
|
1372
|
+
}
|
|
1373
|
+
connect().catch(() => {
|
|
1334
1374
|
});
|
|
1335
1375
|
return stream;
|
|
1336
1376
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
/* eslint-disable no-await-in-loop */
|
|
3
|
-
/* eslint-disable ts/strict-boolean-expressions */
|
|
4
|
-
/* eslint-disable ts/use-unknown-in-catch-callback-variable */
|
|
1
|
+
|
|
5
2
|
/**
|
|
6
3
|
* SSE (Server-Sent Events) Client Utilities
|
|
7
4
|
* Simple, reliable SSE streaming without event loss
|
|
@@ -17,6 +14,20 @@ export interface SSEStreamOptions {
|
|
|
17
14
|
headers?: Record<string, string>
|
|
18
15
|
/** Whether to include credentials (cookies) with the request */
|
|
19
16
|
withCredentials?: boolean
|
|
17
|
+
/**
|
|
18
|
+
* Last-Event-ID to resume from on reconnect.
|
|
19
|
+
* The server uses this to replay buffered events from that sequence number.
|
|
20
|
+
*/
|
|
21
|
+
lastEventId?: string
|
|
22
|
+
/**
|
|
23
|
+
* Maximum number of automatic reconnect attempts (GET streams only).
|
|
24
|
+
* Set to 0 to disable auto-reconnect. Defaults to 5.
|
|
25
|
+
*/
|
|
26
|
+
maxReconnects?: number
|
|
27
|
+
/**
|
|
28
|
+
* Base delay in ms between reconnect attempts (exponential backoff). Defaults to 1000.
|
|
29
|
+
*/
|
|
30
|
+
reconnectDelayMs?: number
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
/**
|
|
@@ -40,98 +51,133 @@ export function createSSEStream<TEventMap extends StreamEventMap = StreamEventMa
|
|
|
40
51
|
url: string,
|
|
41
52
|
options: SSEStreamOptions = {}
|
|
42
53
|
): StreamController<TEventMap> {
|
|
43
|
-
const {
|
|
44
|
-
|
|
54
|
+
const {
|
|
55
|
+
headers = {},
|
|
56
|
+
withCredentials = true,
|
|
57
|
+
lastEventId: initialLastEventId,
|
|
58
|
+
maxReconnects = 5,
|
|
59
|
+
reconnectDelayMs = 1000,
|
|
60
|
+
} = options
|
|
45
61
|
|
|
62
|
+
const controller = new AbortController()
|
|
46
63
|
const stream = new StreamController<TEventMap>(() => { controller.abort() })
|
|
47
64
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
let lastEventId = initialLastEventId
|
|
66
|
+
let reconnectAttempts = 0
|
|
67
|
+
|
|
68
|
+
async function connect(): Promise<void> {
|
|
69
|
+
const requestHeaders: Record<string, string> = {
|
|
52
70
|
'Accept': 'text/event-stream',
|
|
53
71
|
'Cache-Control': 'no-cache',
|
|
54
72
|
...headers,
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
keepalive: false,
|
|
60
|
-
})
|
|
61
|
-
.then(async (response) => {
|
|
62
|
-
if (!response.ok) {
|
|
63
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
64
|
-
}
|
|
73
|
+
}
|
|
74
|
+
if (lastEventId !== undefined) {
|
|
75
|
+
requestHeaders['Last-Event-ID'] = lastEventId
|
|
76
|
+
}
|
|
65
77
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
let response: Response
|
|
79
|
+
try {
|
|
80
|
+
response = await fetch(url, {
|
|
81
|
+
method: 'GET',
|
|
82
|
+
headers: requestHeaders,
|
|
83
|
+
credentials: withCredentials ? 'include' : 'same-origin',
|
|
84
|
+
signal: controller.signal,
|
|
85
|
+
keepalive: false,
|
|
86
|
+
})
|
|
87
|
+
} catch (error: any) {
|
|
88
|
+
if (error?.name === 'AbortError') return
|
|
89
|
+
return handleDisconnect(error)
|
|
90
|
+
}
|
|
69
91
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
stream.emit('error', new Error(`HTTP ${response.status}: ${response.statusText}`))
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (!response.body) {
|
|
97
|
+
stream.emit('error', new Error('Response body is null'))
|
|
98
|
+
return
|
|
99
|
+
}
|
|
73
100
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const { done, value } = await reader.read()
|
|
101
|
+
// Successful connection — reset reconnect counter
|
|
102
|
+
reconnectAttempts = 0
|
|
77
103
|
|
|
78
|
-
|
|
104
|
+
const reader = response.body.getReader()
|
|
105
|
+
const decoder = new TextDecoder()
|
|
106
|
+
let buffer = ''
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
while (true) {
|
|
110
|
+
const { done, value } = await reader.read()
|
|
111
|
+
|
|
112
|
+
if (done) {
|
|
113
|
+
// Server closed the connection — emit done only if not aborted
|
|
114
|
+
if (!controller.signal.aborted) {
|
|
79
115
|
stream.emit('done', {})
|
|
80
116
|
stream.emit('complete', {})
|
|
81
|
-
break
|
|
82
117
|
}
|
|
118
|
+
break
|
|
119
|
+
}
|
|
83
120
|
|
|
84
|
-
|
|
85
|
-
|
|
121
|
+
buffer += decoder.decode(value, { stream: true })
|
|
122
|
+
const lines = buffer.split('\n')
|
|
123
|
+
buffer = lines.pop() || ''
|
|
86
124
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
125
|
+
let currentEvent = 'message'
|
|
126
|
+
let currentData = ''
|
|
127
|
+
let currentId: string | undefined
|
|
90
128
|
|
|
91
|
-
|
|
92
|
-
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
if (line.startsWith('id:')) {
|
|
131
|
+
currentId = line.slice(3).trim()
|
|
132
|
+
} else if (line.startsWith('event:')) {
|
|
133
|
+
currentEvent = line.slice(6).trim()
|
|
134
|
+
} else if (line.startsWith('data:')) {
|
|
135
|
+
currentData = line.slice(5).trim()
|
|
136
|
+
} else if (line === '' && currentData) {
|
|
137
|
+
// Track last seen ID for reconnect
|
|
138
|
+
if (currentId !== undefined) lastEventId = currentId
|
|
93
139
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
currentEvent
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const parsed = JSON.parse(currentData)
|
|
103
|
-
stream.emit(currentEvent, parsed)
|
|
104
|
-
} catch {
|
|
105
|
-
// If not valid JSON, emit as string
|
|
106
|
-
stream.emit(currentEvent, currentData)
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(currentData)
|
|
142
|
+
stream.emit(currentEvent, parsed)
|
|
143
|
+
|
|
144
|
+
// Auto-close stream on terminal events
|
|
145
|
+
if (currentEvent === 'done' || currentEvent === 'error') {
|
|
146
|
+
stream.emit('complete', {})
|
|
147
|
+
return
|
|
107
148
|
}
|
|
108
|
-
|
|
109
|
-
currentEvent
|
|
110
|
-
currentData = ''
|
|
149
|
+
} catch {
|
|
150
|
+
stream.emit(currentEvent, currentData)
|
|
111
151
|
}
|
|
152
|
+
currentEvent = 'message'
|
|
153
|
+
currentData = ''
|
|
154
|
+
currentId = undefined
|
|
112
155
|
}
|
|
113
156
|
}
|
|
114
|
-
} catch (error: any) {
|
|
115
|
-
// Handle all stream errors (network errors, protocol errors, etc.)
|
|
116
|
-
if (error?.name !== 'AbortError') {
|
|
117
|
-
const errorMessage = error?.message || 'Stream connection error'
|
|
118
|
-
stream.emit('error', new Error(errorMessage))
|
|
119
|
-
}
|
|
120
|
-
} finally {
|
|
121
|
-
// Clean up reader
|
|
122
|
-
try {
|
|
123
|
-
reader.releaseLock()
|
|
124
|
-
} catch {
|
|
125
|
-
// Ignore cleanup errors
|
|
126
|
-
}
|
|
127
157
|
}
|
|
128
|
-
})
|
|
129
|
-
.catch((error: any) => {
|
|
158
|
+
} catch (error: any) {
|
|
130
159
|
if (error?.name !== 'AbortError') {
|
|
131
|
-
|
|
132
|
-
stream.emit('error', new Error(errorMessage))
|
|
160
|
+
return handleDisconnect(error)
|
|
133
161
|
}
|
|
134
|
-
}
|
|
162
|
+
} finally {
|
|
163
|
+
try { reader.releaseLock() } catch { /* ignore */ }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function handleDisconnect(error?: any): Promise<void> {
|
|
168
|
+
if (controller.signal.aborted) return
|
|
169
|
+
if (reconnectAttempts >= maxReconnects) {
|
|
170
|
+
const msg = error?.message || 'Stream connection lost'
|
|
171
|
+
stream.emit('error', new Error(`${msg} (gave up after ${maxReconnects} reconnects)`))
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
reconnectAttempts++
|
|
175
|
+
const delay = reconnectDelayMs * Math.pow(2, reconnectAttempts - 1) // exponential backoff
|
|
176
|
+
await new Promise(resolve => setTimeout(resolve, delay))
|
|
177
|
+
if (!controller.signal.aborted) await connect()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
connect().catch(() => { /* errors handled inside connect() */ })
|
|
135
181
|
|
|
136
182
|
return stream
|
|
137
183
|
}
|