@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 CHANGED
@@ -1265,29 +1265,50 @@ class StreamController {
1265
1265
  }
1266
1266
 
1267
1267
  function createSSEStream(url, options = {}) {
1268
- const { headers = {}, withCredentials = true } = options;
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
- fetch(url, {
1274
- method: "GET",
1275
- headers: {
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
- credentials: withCredentials ? "include" : "same-origin",
1281
- signal: controller.signal,
1282
- // Disable keepalive for streaming
1283
- keepalive: false
1284
- }).then(async (response) => {
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
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1304
+ stream.emit("error", new Error(`HTTP ${response.status}: ${response.statusText}`));
1305
+ return;
1287
1306
  }
1288
1307
  if (!response.body) {
1289
- throw new Error("Response body is null");
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
- stream.emit("done", {});
1299
- stream.emit("complete", {});
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("event:")) {
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
- const errorMessage = error?.message || "Stream connection error";
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
- }).catch((error) => {
1336
- if (error?.name !== "AbortError") {
1337
- const errorMessage = error?.message || "Failed to establish stream connection";
1338
- stream.emit("error", new Error(errorMessage));
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 { headers = {}, withCredentials = true } = options;
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
- fetch(url, {
1268
- method: "GET",
1269
- headers: {
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
- credentials: withCredentials ? "include" : "same-origin",
1275
- signal: controller.signal,
1276
- // Disable keepalive for streaming
1277
- keepalive: false
1278
- }).then(async (response) => {
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
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1298
+ stream.emit("error", new Error(`HTTP ${response.status}: ${response.statusText}`));
1299
+ return;
1281
1300
  }
1282
1301
  if (!response.body) {
1283
- throw new Error("Response body is null");
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
- stream.emit("done", {});
1293
- stream.emit("complete", {});
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("event:")) {
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
- const errorMessage = error?.message || "Stream connection error";
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
- }).catch((error) => {
1330
- if (error?.name !== "AbortError") {
1331
- const errorMessage = error?.message || "Failed to establish stream connection";
1332
- stream.emit("error", new Error(errorMessage));
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,7 @@
1
1
  {
2
2
  "name": "@bagelink/sdk",
3
3
  "type": "module",
4
- "version": "1.12.53",
4
+ "version": "1.12.57",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -1,7 +1,4 @@
1
- /* eslint-disable ts/no-unnecessary-condition */
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 { headers = {}, withCredentials = true } = options
44
- const controller = new AbortController()
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
- // Start the fetch request
49
- fetch(url, {
50
- method: 'GET',
51
- headers: {
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
- credentials: withCredentials ? 'include' : 'same-origin',
57
- signal: controller.signal,
58
- // Disable keepalive for streaming
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
- if (!response.body) {
67
- throw new Error('Response body is null')
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
- const reader = response.body.getReader()
71
- const decoder = new TextDecoder()
72
- let buffer = ''
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
- try {
75
- while (true) {
76
- const { done, value } = await reader.read()
101
+ // Successful connection — reset reconnect counter
102
+ reconnectAttempts = 0
77
103
 
78
- if (done) {
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
- // Decode the chunk and add to buffer
85
- buffer += decoder.decode(value, { stream: true })
121
+ buffer += decoder.decode(value, { stream: true })
122
+ const lines = buffer.split('\n')
123
+ buffer = lines.pop() || ''
86
124
 
87
- // Process complete events in the buffer
88
- const lines = buffer.split('\n')
89
- buffer = lines.pop() || '' // Keep incomplete line in buffer
125
+ let currentEvent = 'message'
126
+ let currentData = ''
127
+ let currentId: string | undefined
90
128
 
91
- let currentEvent = 'message'
92
- let currentData = ''
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
- for (const line of lines) {
95
- if (line.startsWith('event:')) {
96
- currentEvent = line.slice(6).trim()
97
- } else if (line.startsWith('data:')) {
98
- currentData = line.slice(5).trim()
99
- } else if (line === '' && currentData) {
100
- // Empty line indicates end of event - emit immediately
101
- try {
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
- // Reset for next event
109
- currentEvent = 'message'
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
- const errorMessage = error?.message || 'Failed to establish stream connection'
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
  }