@prisma/composer-prisma-cloud 0.1.0-dev.4 → 0.1.0-dev.5
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/control.mjs.map +1 -1
- package/dist/cron/scheduler-entrypoint.mjs.map +1 -1
- package/dist/storage/storage-entrypoint.mjs.map +1 -1
- package/dist/streams/index.mjs +475 -199
- package/dist/streams/index.mjs.map +1 -1
- package/dist/streams/streams-entrypoint.mjs.map +1 -1
- package/dist/streams/streams-service.mjs.map +1 -1
- package/dist/streams/testing.mjs.map +1 -1
- package/package.json +14 -14
package/dist/streams/index.mjs
CHANGED
|
@@ -465,7 +465,7 @@ function storageService(opts) {
|
|
|
465
465
|
}
|
|
466
466
|
storageService({ bucket: "storage" });
|
|
467
467
|
//#endregion
|
|
468
|
-
//#region ../../1-prisma-cloud/2-shared-modules/streams/dist/streams-service-
|
|
468
|
+
//#region ../../1-prisma-cloud/2-shared-modules/streams/dist/streams-service-Br5Tj3AY.mjs
|
|
469
469
|
var __create = Object.create;
|
|
470
470
|
var __defProp = Object.defineProperty;
|
|
471
471
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -864,7 +864,7 @@ var FetchError = class FetchError extends Error {
|
|
|
864
864
|
let text = void 0;
|
|
865
865
|
let json = void 0;
|
|
866
866
|
const contentType = response.headers.get(`content-type`);
|
|
867
|
-
if (!response.bodyUsed) if (contentType && contentType.includes(`application/json`)) try {
|
|
867
|
+
if (!response.bodyUsed && response.body !== null) if (contentType && contentType.includes(`application/json`)) try {
|
|
868
868
|
json = await response.json();
|
|
869
869
|
} catch {
|
|
870
870
|
text = await response.text();
|
|
@@ -913,7 +913,7 @@ var DurableStreamError = class DurableStreamError extends Error {
|
|
|
913
913
|
const status = response.status;
|
|
914
914
|
let details;
|
|
915
915
|
const contentType = response.headers.get(`content-type`);
|
|
916
|
-
if (!response.bodyUsed) if (contentType && contentType.includes(`application/json`)) try {
|
|
916
|
+
if (!response.bodyUsed && response.body !== null) if (contentType && contentType.includes(`application/json`)) try {
|
|
917
917
|
details = await response.json();
|
|
918
918
|
} catch {
|
|
919
919
|
details = await response.text();
|
|
@@ -1282,6 +1282,204 @@ async function* parseSSEStream(stream$1, signal) {
|
|
|
1282
1282
|
}
|
|
1283
1283
|
}
|
|
1284
1284
|
/**
|
|
1285
|
+
* Abstract base class for stream response state.
|
|
1286
|
+
* All state transitions return new immutable state objects.
|
|
1287
|
+
*/
|
|
1288
|
+
var StreamResponseState = class {
|
|
1289
|
+
shouldContinueLive(stopAfterUpToDate, liveMode) {
|
|
1290
|
+
if (stopAfterUpToDate && this.upToDate) return false;
|
|
1291
|
+
if (liveMode === false) return false;
|
|
1292
|
+
if (this.streamClosed) return false;
|
|
1293
|
+
return true;
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
/**
|
|
1297
|
+
* State for long-poll mode. shouldUseSse() returns false.
|
|
1298
|
+
*/
|
|
1299
|
+
var LongPollState = class LongPollState extends StreamResponseState {
|
|
1300
|
+
offset;
|
|
1301
|
+
cursor;
|
|
1302
|
+
upToDate;
|
|
1303
|
+
streamClosed;
|
|
1304
|
+
constructor(fields) {
|
|
1305
|
+
super();
|
|
1306
|
+
this.offset = fields.offset;
|
|
1307
|
+
this.cursor = fields.cursor;
|
|
1308
|
+
this.upToDate = fields.upToDate;
|
|
1309
|
+
this.streamClosed = fields.streamClosed;
|
|
1310
|
+
}
|
|
1311
|
+
shouldUseSse() {
|
|
1312
|
+
return false;
|
|
1313
|
+
}
|
|
1314
|
+
withResponseMetadata(update) {
|
|
1315
|
+
return new LongPollState({
|
|
1316
|
+
offset: update.offset ?? this.offset,
|
|
1317
|
+
cursor: update.cursor ?? this.cursor,
|
|
1318
|
+
upToDate: update.upToDate,
|
|
1319
|
+
streamClosed: this.streamClosed || update.streamClosed
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
withSSEControl(event) {
|
|
1323
|
+
const streamClosed = this.streamClosed || (event.streamClosed ?? false);
|
|
1324
|
+
return new LongPollState({
|
|
1325
|
+
offset: event.streamNextOffset,
|
|
1326
|
+
cursor: event.streamCursor || this.cursor,
|
|
1327
|
+
upToDate: event.streamClosed ?? false ? true : event.upToDate ?? this.upToDate,
|
|
1328
|
+
streamClosed
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
pause() {
|
|
1332
|
+
return new PausedState(this);
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
/**
|
|
1336
|
+
* State for SSE mode. shouldUseSse() returns true.
|
|
1337
|
+
* Tracks SSE connection resilience (short connection detection).
|
|
1338
|
+
*/
|
|
1339
|
+
var SSEState = class SSEState extends StreamResponseState {
|
|
1340
|
+
offset;
|
|
1341
|
+
cursor;
|
|
1342
|
+
upToDate;
|
|
1343
|
+
streamClosed;
|
|
1344
|
+
consecutiveShortConnections;
|
|
1345
|
+
connectionStartTime;
|
|
1346
|
+
constructor(fields) {
|
|
1347
|
+
super();
|
|
1348
|
+
this.offset = fields.offset;
|
|
1349
|
+
this.cursor = fields.cursor;
|
|
1350
|
+
this.upToDate = fields.upToDate;
|
|
1351
|
+
this.streamClosed = fields.streamClosed;
|
|
1352
|
+
this.consecutiveShortConnections = fields.consecutiveShortConnections ?? 0;
|
|
1353
|
+
this.connectionStartTime = fields.connectionStartTime;
|
|
1354
|
+
}
|
|
1355
|
+
shouldUseSse() {
|
|
1356
|
+
return true;
|
|
1357
|
+
}
|
|
1358
|
+
withResponseMetadata(update) {
|
|
1359
|
+
return new SSEState({
|
|
1360
|
+
offset: update.offset ?? this.offset,
|
|
1361
|
+
cursor: update.cursor ?? this.cursor,
|
|
1362
|
+
upToDate: update.upToDate,
|
|
1363
|
+
streamClosed: this.streamClosed || update.streamClosed,
|
|
1364
|
+
consecutiveShortConnections: this.consecutiveShortConnections,
|
|
1365
|
+
connectionStartTime: this.connectionStartTime
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
withSSEControl(event) {
|
|
1369
|
+
const streamClosed = this.streamClosed || (event.streamClosed ?? false);
|
|
1370
|
+
return new SSEState({
|
|
1371
|
+
offset: event.streamNextOffset,
|
|
1372
|
+
cursor: event.streamCursor || this.cursor,
|
|
1373
|
+
upToDate: event.streamClosed ?? false ? true : event.upToDate ?? this.upToDate,
|
|
1374
|
+
streamClosed,
|
|
1375
|
+
consecutiveShortConnections: this.consecutiveShortConnections,
|
|
1376
|
+
connectionStartTime: this.connectionStartTime
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
startConnection(now) {
|
|
1380
|
+
return new SSEState({
|
|
1381
|
+
offset: this.offset,
|
|
1382
|
+
cursor: this.cursor,
|
|
1383
|
+
upToDate: this.upToDate,
|
|
1384
|
+
streamClosed: this.streamClosed,
|
|
1385
|
+
consecutiveShortConnections: this.consecutiveShortConnections,
|
|
1386
|
+
connectionStartTime: now
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
handleConnectionEnd(now, wasAborted, config) {
|
|
1390
|
+
if (this.connectionStartTime === void 0) return {
|
|
1391
|
+
action: `healthy`,
|
|
1392
|
+
state: this
|
|
1393
|
+
};
|
|
1394
|
+
const duration = now - this.connectionStartTime;
|
|
1395
|
+
if (duration < config.minConnectionDuration && !wasAborted) {
|
|
1396
|
+
const newCount = this.consecutiveShortConnections + 1;
|
|
1397
|
+
if (newCount >= config.maxShortConnections) return {
|
|
1398
|
+
action: `fallback`,
|
|
1399
|
+
state: new LongPollState({
|
|
1400
|
+
offset: this.offset,
|
|
1401
|
+
cursor: this.cursor,
|
|
1402
|
+
upToDate: this.upToDate,
|
|
1403
|
+
streamClosed: this.streamClosed
|
|
1404
|
+
})
|
|
1405
|
+
};
|
|
1406
|
+
return {
|
|
1407
|
+
action: `reconnect`,
|
|
1408
|
+
state: new SSEState({
|
|
1409
|
+
offset: this.offset,
|
|
1410
|
+
cursor: this.cursor,
|
|
1411
|
+
upToDate: this.upToDate,
|
|
1412
|
+
streamClosed: this.streamClosed,
|
|
1413
|
+
consecutiveShortConnections: newCount,
|
|
1414
|
+
connectionStartTime: this.connectionStartTime
|
|
1415
|
+
}),
|
|
1416
|
+
backoffAttempt: newCount
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
if (duration >= config.minConnectionDuration) return {
|
|
1420
|
+
action: `healthy`,
|
|
1421
|
+
state: new SSEState({
|
|
1422
|
+
offset: this.offset,
|
|
1423
|
+
cursor: this.cursor,
|
|
1424
|
+
upToDate: this.upToDate,
|
|
1425
|
+
streamClosed: this.streamClosed,
|
|
1426
|
+
consecutiveShortConnections: 0,
|
|
1427
|
+
connectionStartTime: this.connectionStartTime
|
|
1428
|
+
})
|
|
1429
|
+
};
|
|
1430
|
+
return {
|
|
1431
|
+
action: `healthy`,
|
|
1432
|
+
state: this
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
pause() {
|
|
1436
|
+
return new PausedState(this);
|
|
1437
|
+
}
|
|
1438
|
+
};
|
|
1439
|
+
/**
|
|
1440
|
+
* Paused state wrapper. Delegates all sync field access to the inner state.
|
|
1441
|
+
* resume() returns the wrapped state unchanged (identity preserved).
|
|
1442
|
+
*/
|
|
1443
|
+
var PausedState = class PausedState extends StreamResponseState {
|
|
1444
|
+
#inner;
|
|
1445
|
+
constructor(inner) {
|
|
1446
|
+
super();
|
|
1447
|
+
this.#inner = inner;
|
|
1448
|
+
}
|
|
1449
|
+
get offset() {
|
|
1450
|
+
return this.#inner.offset;
|
|
1451
|
+
}
|
|
1452
|
+
get cursor() {
|
|
1453
|
+
return this.#inner.cursor;
|
|
1454
|
+
}
|
|
1455
|
+
get upToDate() {
|
|
1456
|
+
return this.#inner.upToDate;
|
|
1457
|
+
}
|
|
1458
|
+
get streamClosed() {
|
|
1459
|
+
return this.#inner.streamClosed;
|
|
1460
|
+
}
|
|
1461
|
+
shouldUseSse() {
|
|
1462
|
+
return this.#inner.shouldUseSse();
|
|
1463
|
+
}
|
|
1464
|
+
withResponseMetadata(update) {
|
|
1465
|
+
const newInner = this.#inner.withResponseMetadata(update);
|
|
1466
|
+
return new PausedState(newInner);
|
|
1467
|
+
}
|
|
1468
|
+
withSSEControl(event) {
|
|
1469
|
+
const newInner = this.#inner.withSSEControl(event);
|
|
1470
|
+
return new PausedState(newInner);
|
|
1471
|
+
}
|
|
1472
|
+
pause() {
|
|
1473
|
+
return this;
|
|
1474
|
+
}
|
|
1475
|
+
resume() {
|
|
1476
|
+
return {
|
|
1477
|
+
state: this.#inner,
|
|
1478
|
+
justResumed: true
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
/**
|
|
1285
1483
|
* Constant used as abort reason when pausing the stream due to visibility change.
|
|
1286
1484
|
*/
|
|
1287
1485
|
const PAUSE_STREAM = `PAUSE_STREAM`;
|
|
@@ -1298,10 +1496,7 @@ var StreamResponseImpl = class {
|
|
|
1298
1496
|
#statusText;
|
|
1299
1497
|
#ok;
|
|
1300
1498
|
#isLoading;
|
|
1301
|
-
#
|
|
1302
|
-
#cursor;
|
|
1303
|
-
#upToDate;
|
|
1304
|
-
#streamClosed;
|
|
1499
|
+
#syncState;
|
|
1305
1500
|
#isJsonMode;
|
|
1306
1501
|
#abortController;
|
|
1307
1502
|
#fetchNext;
|
|
@@ -1316,11 +1511,7 @@ var StreamResponseImpl = class {
|
|
|
1316
1511
|
#unsubscribeFromVisibilityChanges;
|
|
1317
1512
|
#pausePromise;
|
|
1318
1513
|
#pauseResolve;
|
|
1319
|
-
#justResumedFromPause = false;
|
|
1320
1514
|
#sseResilience;
|
|
1321
|
-
#lastSSEConnectionStartTime;
|
|
1322
|
-
#consecutiveShortSSEConnections = 0;
|
|
1323
|
-
#sseFallbackToLongPoll = false;
|
|
1324
1515
|
#encoding;
|
|
1325
1516
|
#responseStream;
|
|
1326
1517
|
constructor(config) {
|
|
@@ -1328,10 +1519,13 @@ var StreamResponseImpl = class {
|
|
|
1328
1519
|
this.contentType = config.contentType;
|
|
1329
1520
|
this.live = config.live;
|
|
1330
1521
|
this.startOffset = config.startOffset;
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1522
|
+
const syncFields = {
|
|
1523
|
+
offset: config.initialOffset,
|
|
1524
|
+
cursor: config.initialCursor,
|
|
1525
|
+
upToDate: config.initialUpToDate,
|
|
1526
|
+
streamClosed: config.initialStreamClosed
|
|
1527
|
+
};
|
|
1528
|
+
this.#syncState = config.startSSE ? new SSEState(syncFields) : new LongPollState(syncFields);
|
|
1335
1529
|
this.#headers = config.firstResponse.headers;
|
|
1336
1530
|
this.#status = config.firstResponse.status;
|
|
1337
1531
|
this.#statusText = config.firstResponse.statusText;
|
|
@@ -1388,6 +1582,7 @@ var StreamResponseImpl = class {
|
|
|
1388
1582
|
#pause() {
|
|
1389
1583
|
if (this.#state === `active`) {
|
|
1390
1584
|
this.#state = `pause-requested`;
|
|
1585
|
+
this.#syncState = this.#syncState.pause();
|
|
1391
1586
|
this.#pausePromise = new Promise((resolve) => {
|
|
1392
1587
|
this.#pauseResolve = resolve;
|
|
1393
1588
|
});
|
|
@@ -1401,8 +1596,8 @@ var StreamResponseImpl = class {
|
|
|
1401
1596
|
#resume() {
|
|
1402
1597
|
if (this.#state === `paused` || this.#state === `pause-requested`) {
|
|
1403
1598
|
if (this.#abortController.signal.aborted) return;
|
|
1599
|
+
if (this.#syncState instanceof PausedState) this.#syncState = this.#syncState.resume().state;
|
|
1404
1600
|
this.#state = `active`;
|
|
1405
|
-
this.#justResumedFromPause = true;
|
|
1406
1601
|
this.#pauseResolve?.();
|
|
1407
1602
|
this.#pausePromise = void 0;
|
|
1408
1603
|
this.#pauseResolve = void 0;
|
|
@@ -1424,16 +1619,16 @@ var StreamResponseImpl = class {
|
|
|
1424
1619
|
return this.#isLoading;
|
|
1425
1620
|
}
|
|
1426
1621
|
get offset() {
|
|
1427
|
-
return this.#offset;
|
|
1622
|
+
return this.#syncState.offset;
|
|
1428
1623
|
}
|
|
1429
1624
|
get cursor() {
|
|
1430
|
-
return this.#cursor;
|
|
1625
|
+
return this.#syncState.cursor;
|
|
1431
1626
|
}
|
|
1432
1627
|
get upToDate() {
|
|
1433
|
-
return this.#upToDate;
|
|
1628
|
+
return this.#syncState.upToDate;
|
|
1434
1629
|
}
|
|
1435
1630
|
get streamClosed() {
|
|
1436
|
-
return this.#streamClosed;
|
|
1631
|
+
return this.#syncState.streamClosed;
|
|
1437
1632
|
}
|
|
1438
1633
|
#ensureJsonMode() {
|
|
1439
1634
|
if (!this.#isJsonMode) throw new DurableStreamError(`JSON methods are only valid for JSON-mode streams. Content-Type is "${this.contentType}" and json hint was not set.`, `BAD_REQUEST`);
|
|
@@ -1459,160 +1654,68 @@ var StreamResponseImpl = class {
|
|
|
1459
1654
|
* and whether we've received upToDate or streamClosed.
|
|
1460
1655
|
*/
|
|
1461
1656
|
#shouldContinueLive() {
|
|
1462
|
-
|
|
1463
|
-
if (this.live === false) return false;
|
|
1464
|
-
if (this.#streamClosed) return false;
|
|
1465
|
-
return true;
|
|
1657
|
+
return this.#syncState.shouldContinueLive(this.#stopAfterUpToDate, this.live);
|
|
1466
1658
|
}
|
|
1467
1659
|
/**
|
|
1468
1660
|
* Update state from response headers.
|
|
1469
1661
|
*/
|
|
1470
1662
|
#updateStateFromResponse(response) {
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1663
|
+
this.#syncState = this.#syncState.withResponseMetadata({
|
|
1664
|
+
offset: response.headers.get(STREAM_OFFSET_HEADER) || void 0,
|
|
1665
|
+
cursor: response.headers.get(STREAM_CURSOR_HEADER) || void 0,
|
|
1666
|
+
upToDate: response.headers.has(STREAM_UP_TO_DATE_HEADER),
|
|
1667
|
+
streamClosed: response.headers.get(STREAM_CLOSED_HEADER)?.toLowerCase() === `true`
|
|
1668
|
+
});
|
|
1477
1669
|
this.#headers = response.headers;
|
|
1478
1670
|
this.#status = response.status;
|
|
1479
1671
|
this.#statusText = response.statusText;
|
|
1480
1672
|
this.#ok = response.ok;
|
|
1481
1673
|
}
|
|
1482
1674
|
/**
|
|
1483
|
-
* Extract stream metadata from Response headers.
|
|
1484
|
-
* Used by subscriber APIs to get the correct offset/cursor/upToDate/streamClosed for each
|
|
1485
|
-
* specific Response, rather than reading from `this` which may be stale due to
|
|
1486
|
-
* ReadableStream prefetching or timing issues.
|
|
1487
|
-
*/
|
|
1488
|
-
#getMetadataFromResponse(response) {
|
|
1489
|
-
const offset = response.headers.get(STREAM_OFFSET_HEADER);
|
|
1490
|
-
const cursor = response.headers.get(STREAM_CURSOR_HEADER);
|
|
1491
|
-
const upToDate = response.headers.has(STREAM_UP_TO_DATE_HEADER);
|
|
1492
|
-
const streamClosed = response.headers.get(STREAM_CLOSED_HEADER)?.toLowerCase() === `true`;
|
|
1493
|
-
return {
|
|
1494
|
-
offset: offset ?? this.offset,
|
|
1495
|
-
cursor: cursor ?? this.cursor,
|
|
1496
|
-
upToDate,
|
|
1497
|
-
streamClosed: streamClosed || this.streamClosed
|
|
1498
|
-
};
|
|
1499
|
-
}
|
|
1500
|
-
/**
|
|
1501
|
-
* Decode base64 string to Uint8Array.
|
|
1502
|
-
* Per protocol: concatenate data lines, remove \n and \r, then decode.
|
|
1503
|
-
*/
|
|
1504
|
-
#decodeBase64(base64Str) {
|
|
1505
|
-
const cleaned = base64Str.replace(/[\n\r]/g, ``);
|
|
1506
|
-
if (cleaned.length === 0) return /* @__PURE__ */ new Uint8Array(0);
|
|
1507
|
-
if (cleaned.length % 4 !== 0) throw new DurableStreamError(`Invalid base64 data: length ${cleaned.length} is not a multiple of 4`, `PARSE_ERROR`);
|
|
1508
|
-
try {
|
|
1509
|
-
if (typeof Buffer !== `undefined`) return new Uint8Array(Buffer.from(cleaned, `base64`));
|
|
1510
|
-
else {
|
|
1511
|
-
const binaryStr = atob(cleaned);
|
|
1512
|
-
const bytes = new Uint8Array(binaryStr.length);
|
|
1513
|
-
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
|
|
1514
|
-
return bytes;
|
|
1515
|
-
}
|
|
1516
|
-
} catch (err) {
|
|
1517
|
-
throw new DurableStreamError(`Failed to decode base64 data: ${err instanceof Error ? err.message : String(err)}`, `PARSE_ERROR`);
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
/**
|
|
1521
|
-
* Create a synthetic Response from SSE data with proper headers.
|
|
1522
|
-
* Includes offset/cursor/upToDate/streamClosed in headers so subscribers can read them.
|
|
1523
|
-
*/
|
|
1524
|
-
#createSSESyntheticResponse(data, offset, cursor, upToDate, streamClosed) {
|
|
1525
|
-
return this.#createSSESyntheticResponseFromParts([data], offset, cursor, upToDate, streamClosed);
|
|
1526
|
-
}
|
|
1527
|
-
/**
|
|
1528
|
-
* Create a synthetic Response from multiple SSE data parts.
|
|
1529
|
-
* For base64 mode, each part is independently encoded, so we decode each
|
|
1530
|
-
* separately and concatenate the binary results.
|
|
1531
|
-
* For text mode, parts are simply concatenated as strings.
|
|
1532
|
-
*/
|
|
1533
|
-
#createSSESyntheticResponseFromParts(dataParts, offset, cursor, upToDate, streamClosed) {
|
|
1534
|
-
const headers = {
|
|
1535
|
-
"content-type": this.contentType ?? `application/json`,
|
|
1536
|
-
[STREAM_OFFSET_HEADER]: String(offset)
|
|
1537
|
-
};
|
|
1538
|
-
if (cursor) headers[STREAM_CURSOR_HEADER] = cursor;
|
|
1539
|
-
if (upToDate) headers[STREAM_UP_TO_DATE_HEADER] = `true`;
|
|
1540
|
-
if (streamClosed) headers[STREAM_CLOSED_HEADER] = `true`;
|
|
1541
|
-
let body;
|
|
1542
|
-
if (this.#encoding === `base64`) {
|
|
1543
|
-
const decodedParts = dataParts.filter((part) => part.length > 0).map((part) => this.#decodeBase64(part));
|
|
1544
|
-
if (decodedParts.length === 0) body = /* @__PURE__ */ new ArrayBuffer(0);
|
|
1545
|
-
else if (decodedParts.length === 1) {
|
|
1546
|
-
const decoded = decodedParts[0];
|
|
1547
|
-
body = decoded.buffer.slice(decoded.byteOffset, decoded.byteOffset + decoded.byteLength);
|
|
1548
|
-
} else {
|
|
1549
|
-
const totalLength = decodedParts.reduce((sum, part) => sum + part.length, 0);
|
|
1550
|
-
const combined = new Uint8Array(totalLength);
|
|
1551
|
-
let offset$1 = 0;
|
|
1552
|
-
for (const part of decodedParts) {
|
|
1553
|
-
combined.set(part, offset$1);
|
|
1554
|
-
offset$1 += part.length;
|
|
1555
|
-
}
|
|
1556
|
-
body = combined.buffer;
|
|
1557
|
-
}
|
|
1558
|
-
} else body = dataParts.join(``);
|
|
1559
|
-
return new Response(body, {
|
|
1560
|
-
status: 200,
|
|
1561
|
-
headers
|
|
1562
|
-
});
|
|
1563
|
-
}
|
|
1564
|
-
/**
|
|
1565
1675
|
* Update instance state from an SSE control event.
|
|
1566
1676
|
*/
|
|
1567
1677
|
#updateStateFromSSEControl(controlEvent) {
|
|
1568
|
-
this.#
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
this.#streamClosed = true;
|
|
1573
|
-
this.#upToDate = true;
|
|
1574
|
-
}
|
|
1678
|
+
this.#syncState = this.#syncState.withSSEControl(controlEvent);
|
|
1679
|
+
}
|
|
1680
|
+
#updateEncodingFromSSEResponse(response) {
|
|
1681
|
+
this.#encoding = response.headers.get(STREAM_SSE_DATA_ENCODING_HEADER) === `base64` ? `base64` : void 0;
|
|
1575
1682
|
}
|
|
1576
1683
|
/**
|
|
1577
1684
|
* Mark the start of an SSE connection for duration tracking.
|
|
1685
|
+
* If the state is not SSEState (e.g., auto-detected SSE from content-type),
|
|
1686
|
+
* transitions to SSEState first.
|
|
1578
1687
|
*/
|
|
1579
1688
|
#markSSEConnectionStart() {
|
|
1580
|
-
this.#
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
if (this.#lastSSEConnectionStartTime === void 0) return 0;
|
|
1588
|
-
const connectionDuration = Date.now() - this.#lastSSEConnectionStartTime;
|
|
1589
|
-
const wasAborted = this.#abortController.signal.aborted;
|
|
1590
|
-
if (connectionDuration < this.#sseResilience.minConnectionDuration && !wasAborted) {
|
|
1591
|
-
this.#consecutiveShortSSEConnections++;
|
|
1592
|
-
if (this.#consecutiveShortSSEConnections >= this.#sseResilience.maxShortConnections) {
|
|
1593
|
-
this.#sseFallbackToLongPoll = true;
|
|
1594
|
-
if (this.#sseResilience.logWarnings) console.warn("[Durable Streams] SSE connections are closing immediately (possibly due to proxy buffering or misconfiguration). Falling back to long polling. Your proxy must support streaming SSE responses (not buffer the complete response). Configuration: Nginx add 'X-Accel-Buffering: no', Caddy add 'flush_interval -1' to reverse_proxy.");
|
|
1595
|
-
return null;
|
|
1596
|
-
} else {
|
|
1597
|
-
const maxDelay = Math.min(this.#sseResilience.backoffMaxDelay, this.#sseResilience.backoffBaseDelay * Math.pow(2, this.#consecutiveShortSSEConnections));
|
|
1598
|
-
const delayMs = Math.floor(Math.random() * maxDelay);
|
|
1599
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1600
|
-
return delayMs;
|
|
1601
|
-
}
|
|
1602
|
-
} else if (connectionDuration >= this.#sseResilience.minConnectionDuration) this.#consecutiveShortSSEConnections = 0;
|
|
1603
|
-
return 0;
|
|
1689
|
+
if (!(this.#syncState instanceof SSEState)) this.#syncState = new SSEState({
|
|
1690
|
+
offset: this.#syncState.offset,
|
|
1691
|
+
cursor: this.#syncState.cursor,
|
|
1692
|
+
upToDate: this.#syncState.upToDate,
|
|
1693
|
+
streamClosed: this.#syncState.streamClosed
|
|
1694
|
+
});
|
|
1695
|
+
this.#syncState = this.#syncState.startConnection(Date.now());
|
|
1604
1696
|
}
|
|
1605
1697
|
/**
|
|
1606
1698
|
* Try to reconnect SSE and return the new iterator, or null if reconnection
|
|
1607
1699
|
* is not possible or fails.
|
|
1608
1700
|
*/
|
|
1609
1701
|
async #trySSEReconnect() {
|
|
1610
|
-
if (this.#
|
|
1702
|
+
if (!this.#syncState.shouldUseSse()) return null;
|
|
1611
1703
|
if (!this.#shouldContinueLive() || !this.#startSSE) return null;
|
|
1612
|
-
|
|
1704
|
+
const result = this.#syncState.handleConnectionEnd(Date.now(), this.#abortController.signal.aborted, this.#sseResilience);
|
|
1705
|
+
this.#syncState = result.state;
|
|
1706
|
+
if (result.action === `fallback`) {
|
|
1707
|
+
if (this.#sseResilience.logWarnings) console.warn("[Durable Streams] SSE connections are closing immediately (possibly due to proxy buffering or misconfiguration). Falling back to long polling. Your proxy must support streaming SSE responses (not buffer the complete response). Configuration: Nginx add 'X-Accel-Buffering: no', Caddy add 'flush_interval -1' to reverse_proxy.");
|
|
1708
|
+
return null;
|
|
1709
|
+
}
|
|
1710
|
+
if (result.action === `reconnect`) {
|
|
1711
|
+
const maxDelay = Math.min(this.#sseResilience.backoffMaxDelay, this.#sseResilience.backoffBaseDelay * Math.pow(2, result.backoffAttempt));
|
|
1712
|
+
const delayMs = Math.floor(Math.random() * maxDelay);
|
|
1713
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1714
|
+
}
|
|
1613
1715
|
this.#markSSEConnectionStart();
|
|
1614
1716
|
this.#requestAbortController = new AbortController();
|
|
1615
1717
|
const newSSEResponse = await this.#startSSE(this.offset, this.cursor, this.#requestAbortController.signal);
|
|
1718
|
+
this.#updateEncodingFromSSEResponse(newSSEResponse);
|
|
1616
1719
|
if (newSSEResponse.body) return parseSSEStream(newSSEResponse.body, this.#requestAbortController.signal);
|
|
1617
1720
|
return null;
|
|
1618
1721
|
}
|
|
@@ -1645,7 +1748,7 @@ var StreamResponseImpl = class {
|
|
|
1645
1748
|
this.#updateStateFromSSEControl(event);
|
|
1646
1749
|
if (event.upToDate) return {
|
|
1647
1750
|
type: `response`,
|
|
1648
|
-
response:
|
|
1751
|
+
response: createSSESyntheticResponse(``, event.streamNextOffset, event.streamCursor, true, event.streamClosed ?? false, this.contentType, this.#encoding)
|
|
1649
1752
|
};
|
|
1650
1753
|
return { type: `continue` };
|
|
1651
1754
|
}
|
|
@@ -1662,7 +1765,7 @@ var StreamResponseImpl = class {
|
|
|
1662
1765
|
while (true) {
|
|
1663
1766
|
const { done: controlDone, value: controlEvent } = await sseEventIterator.next();
|
|
1664
1767
|
if (controlDone) {
|
|
1665
|
-
const response =
|
|
1768
|
+
const response = createSSESyntheticResponseFromParts(bufferedDataParts, this.offset, this.cursor, this.upToDate, this.streamClosed, this.contentType, this.#encoding, this.#isJsonMode);
|
|
1666
1769
|
try {
|
|
1667
1770
|
return {
|
|
1668
1771
|
type: `response`,
|
|
@@ -1680,7 +1783,7 @@ var StreamResponseImpl = class {
|
|
|
1680
1783
|
this.#updateStateFromSSEControl(controlEvent);
|
|
1681
1784
|
return {
|
|
1682
1785
|
type: `response`,
|
|
1683
|
-
response:
|
|
1786
|
+
response: createSSESyntheticResponseFromParts(bufferedDataParts, controlEvent.streamNextOffset, controlEvent.streamCursor, controlEvent.upToDate ?? false, controlEvent.streamClosed ?? false, this.contentType, this.#encoding, this.#isJsonMode)
|
|
1684
1787
|
};
|
|
1685
1788
|
}
|
|
1686
1789
|
bufferedDataParts.push(controlEvent.data);
|
|
@@ -1703,6 +1806,7 @@ var StreamResponseImpl = class {
|
|
|
1703
1806
|
firstResponseYielded = true;
|
|
1704
1807
|
if ((firstResponse.headers.get(`content-type`)?.includes(`text/event-stream`) ?? false) && firstResponse.body) {
|
|
1705
1808
|
this.#markSSEConnectionStart();
|
|
1809
|
+
this.#updateEncodingFromSSEResponse(firstResponse);
|
|
1706
1810
|
this.#requestAbortController = new AbortController();
|
|
1707
1811
|
sseEventIterator = parseSSEStream(firstResponse.body, this.#requestAbortController.signal);
|
|
1708
1812
|
} else {
|
|
@@ -1715,6 +1819,22 @@ var StreamResponseImpl = class {
|
|
|
1715
1819
|
return;
|
|
1716
1820
|
}
|
|
1717
1821
|
}
|
|
1822
|
+
if (!sseEventIterator && this.upToDate && this.#startSSE && this.#shouldContinueLive()) {
|
|
1823
|
+
if (this.#state === `pause-requested` || this.#state === `paused`) {
|
|
1824
|
+
this.#state = `paused`;
|
|
1825
|
+
if (this.#pausePromise) await this.#pausePromise;
|
|
1826
|
+
if (this.#abortController.signal.aborted) {
|
|
1827
|
+
this.#markClosed();
|
|
1828
|
+
controller.close();
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
this.#markSSEConnectionStart();
|
|
1833
|
+
this.#requestAbortController = new AbortController();
|
|
1834
|
+
const sseResponse = await this.#startSSE(this.offset, this.cursor, this.#requestAbortController.signal);
|
|
1835
|
+
this.#updateEncodingFromSSEResponse(sseResponse);
|
|
1836
|
+
if (sseResponse.body) sseEventIterator = parseSSEStream(sseResponse.body, this.#requestAbortController.signal);
|
|
1837
|
+
}
|
|
1718
1838
|
if (sseEventIterator) {
|
|
1719
1839
|
if (this.#state === `pause-requested` || this.#state === `paused`) {
|
|
1720
1840
|
this.#state = `paused`;
|
|
@@ -1754,6 +1874,7 @@ var StreamResponseImpl = class {
|
|
|
1754
1874
|
}
|
|
1755
1875
|
}
|
|
1756
1876
|
if (this.#shouldContinueLive()) {
|
|
1877
|
+
let resumingFromPause = false;
|
|
1757
1878
|
if (this.#state === `pause-requested` || this.#state === `paused`) {
|
|
1758
1879
|
this.#state = `paused`;
|
|
1759
1880
|
if (this.#pausePromise) await this.#pausePromise;
|
|
@@ -1762,16 +1883,15 @@ var StreamResponseImpl = class {
|
|
|
1762
1883
|
controller.close();
|
|
1763
1884
|
return;
|
|
1764
1885
|
}
|
|
1886
|
+
resumingFromPause = true;
|
|
1765
1887
|
}
|
|
1766
1888
|
if (this.#abortController.signal.aborted) {
|
|
1767
1889
|
this.#markClosed();
|
|
1768
1890
|
controller.close();
|
|
1769
1891
|
return;
|
|
1770
1892
|
}
|
|
1771
|
-
const resumingFromPause = this.#justResumedFromPause;
|
|
1772
|
-
this.#justResumedFromPause = false;
|
|
1773
1893
|
this.#requestAbortController = new AbortController();
|
|
1774
|
-
const response = await this.#fetchNext(this.offset, this.cursor, this.#requestAbortController.signal, resumingFromPause);
|
|
1894
|
+
const response = await this.#fetchNext(this.offset, this.cursor, this.#requestAbortController.signal, this.upToDate, resumingFromPause);
|
|
1775
1895
|
this.#updateStateFromResponse(response);
|
|
1776
1896
|
controller.enqueue(response);
|
|
1777
1897
|
return;
|
|
@@ -1934,22 +2054,25 @@ var StreamResponseImpl = class {
|
|
|
1934
2054
|
controller.enqueue(pendingItems.shift());
|
|
1935
2055
|
return;
|
|
1936
2056
|
}
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2057
|
+
let result = await reader.read();
|
|
2058
|
+
while (!result.done) {
|
|
2059
|
+
const content = (await result.value.text()).trim() || `[]`;
|
|
2060
|
+
let parsed;
|
|
2061
|
+
try {
|
|
2062
|
+
parsed = JSON.parse(content);
|
|
2063
|
+
} catch (err) {
|
|
2064
|
+
const preview = content.length > 100 ? content.slice(0, 100) + `...` : content;
|
|
2065
|
+
throw new DurableStreamError(`Failed to parse JSON response: ${err instanceof Error ? err.message : String(err)}. Data: ${preview}`, `PARSE_ERROR`);
|
|
2066
|
+
}
|
|
2067
|
+
pendingItems = Array.isArray(parsed) ? parsed : [parsed];
|
|
2068
|
+
if (pendingItems.length > 0) {
|
|
2069
|
+
controller.enqueue(pendingItems.shift());
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
result = await reader.read();
|
|
1950
2073
|
}
|
|
1951
|
-
|
|
1952
|
-
|
|
2074
|
+
this.#markClosed();
|
|
2075
|
+
controller.close();
|
|
1953
2076
|
},
|
|
1954
2077
|
cancel: () => {
|
|
1955
2078
|
reader.releaseLock();
|
|
@@ -1981,7 +2104,7 @@ var StreamResponseImpl = class {
|
|
|
1981
2104
|
while (!result.done) {
|
|
1982
2105
|
if (abortController.signal.aborted) break;
|
|
1983
2106
|
const response = result.value;
|
|
1984
|
-
const { offset, cursor, upToDate, streamClosed } =
|
|
2107
|
+
const { offset, cursor, upToDate, streamClosed } = getMetadataFromResponse(response, this.offset, this.cursor, this.streamClosed);
|
|
1985
2108
|
const content = (await response.text()).trim() || `[]`;
|
|
1986
2109
|
let parsed;
|
|
1987
2110
|
try {
|
|
@@ -2025,7 +2148,7 @@ var StreamResponseImpl = class {
|
|
|
2025
2148
|
while (!result.done) {
|
|
2026
2149
|
if (abortController.signal.aborted) break;
|
|
2027
2150
|
const response = result.value;
|
|
2028
|
-
const { offset, cursor, upToDate, streamClosed } =
|
|
2151
|
+
const { offset, cursor, upToDate, streamClosed } = getMetadataFromResponse(response, this.offset, this.cursor, this.streamClosed);
|
|
2029
2152
|
const buffer = await response.arrayBuffer();
|
|
2030
2153
|
await subscriber({
|
|
2031
2154
|
data: new Uint8Array(buffer),
|
|
@@ -2062,7 +2185,7 @@ var StreamResponseImpl = class {
|
|
|
2062
2185
|
while (!result.done) {
|
|
2063
2186
|
if (abortController.signal.aborted) break;
|
|
2064
2187
|
const response = result.value;
|
|
2065
|
-
const { offset, cursor, upToDate, streamClosed } =
|
|
2188
|
+
const { offset, cursor, upToDate, streamClosed } = getMetadataFromResponse(response, this.offset, this.cursor, this.streamClosed);
|
|
2066
2189
|
await subscriber({
|
|
2067
2190
|
text: await response.text(),
|
|
2068
2191
|
offset,
|
|
@@ -2098,6 +2221,97 @@ var StreamResponseImpl = class {
|
|
|
2098
2221
|
}
|
|
2099
2222
|
};
|
|
2100
2223
|
/**
|
|
2224
|
+
* Extract stream metadata from Response headers.
|
|
2225
|
+
* Falls back to the provided defaults when headers are absent.
|
|
2226
|
+
*/
|
|
2227
|
+
function getMetadataFromResponse(response, fallbackOffset, fallbackCursor, fallbackStreamClosed) {
|
|
2228
|
+
const offset = response.headers.get(STREAM_OFFSET_HEADER);
|
|
2229
|
+
const cursor = response.headers.get(STREAM_CURSOR_HEADER);
|
|
2230
|
+
const upToDate = response.headers.has(STREAM_UP_TO_DATE_HEADER);
|
|
2231
|
+
const streamClosed = response.headers.get(STREAM_CLOSED_HEADER)?.toLowerCase() === `true`;
|
|
2232
|
+
return {
|
|
2233
|
+
offset: offset ?? fallbackOffset,
|
|
2234
|
+
cursor: cursor ?? fallbackCursor,
|
|
2235
|
+
upToDate,
|
|
2236
|
+
streamClosed: streamClosed || fallbackStreamClosed
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* Decode base64 string to Uint8Array.
|
|
2241
|
+
* Per protocol: concatenate data lines, remove \n and \r, then decode.
|
|
2242
|
+
*/
|
|
2243
|
+
function decodeBase64(base64Str) {
|
|
2244
|
+
const cleaned = base64Str.replace(/[\n\r]/g, ``);
|
|
2245
|
+
if (cleaned.length === 0) return /* @__PURE__ */ new Uint8Array(0);
|
|
2246
|
+
if (cleaned.length % 4 !== 0) throw new DurableStreamError(`Invalid base64 data: length ${cleaned.length} is not a multiple of 4`, `PARSE_ERROR`);
|
|
2247
|
+
try {
|
|
2248
|
+
if (typeof Buffer !== `undefined`) return new Uint8Array(Buffer.from(cleaned, `base64`));
|
|
2249
|
+
else {
|
|
2250
|
+
const binaryStr = atob(cleaned);
|
|
2251
|
+
const bytes = new Uint8Array(binaryStr.length);
|
|
2252
|
+
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
|
|
2253
|
+
return bytes;
|
|
2254
|
+
}
|
|
2255
|
+
} catch (err) {
|
|
2256
|
+
throw new DurableStreamError(`Failed to decode base64 data: ${err instanceof Error ? err.message : String(err)}`, `PARSE_ERROR`);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Create a synthetic Response from SSE data with proper headers.
|
|
2261
|
+
* Includes offset/cursor/upToDate/streamClosed in headers so subscribers can read them.
|
|
2262
|
+
*/
|
|
2263
|
+
function createSSESyntheticResponse(data, offset, cursor, upToDate, streamClosed, contentType, encoding) {
|
|
2264
|
+
return createSSESyntheticResponseFromParts([data], offset, cursor, upToDate, streamClosed, contentType, encoding);
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Create a synthetic Response from multiple SSE data parts.
|
|
2268
|
+
* For base64 mode, each part is independently encoded, so we decode each
|
|
2269
|
+
* separately and concatenate the binary results.
|
|
2270
|
+
* For text mode, parts are simply concatenated as strings.
|
|
2271
|
+
*/
|
|
2272
|
+
function createSSESyntheticResponseFromParts(dataParts, offset, cursor, upToDate, streamClosed, contentType, encoding, isJsonMode) {
|
|
2273
|
+
const headers = {
|
|
2274
|
+
"content-type": contentType ?? `application/json`,
|
|
2275
|
+
[STREAM_OFFSET_HEADER]: String(offset)
|
|
2276
|
+
};
|
|
2277
|
+
if (cursor) headers[STREAM_CURSOR_HEADER] = cursor;
|
|
2278
|
+
if (upToDate) headers[STREAM_UP_TO_DATE_HEADER] = `true`;
|
|
2279
|
+
if (streamClosed) headers[STREAM_CLOSED_HEADER] = `true`;
|
|
2280
|
+
let body;
|
|
2281
|
+
if (encoding === `base64`) {
|
|
2282
|
+
const decodedParts = dataParts.filter((part) => part.length > 0).map((part) => decodeBase64(part));
|
|
2283
|
+
if (decodedParts.length === 0) body = /* @__PURE__ */ new ArrayBuffer(0);
|
|
2284
|
+
else if (decodedParts.length === 1) {
|
|
2285
|
+
const decoded = decodedParts[0];
|
|
2286
|
+
body = decoded.buffer.slice(decoded.byteOffset, decoded.byteOffset + decoded.byteLength);
|
|
2287
|
+
} else {
|
|
2288
|
+
const totalLength = decodedParts.reduce((sum, part) => sum + part.length, 0);
|
|
2289
|
+
const combined = new Uint8Array(totalLength);
|
|
2290
|
+
let offset$1 = 0;
|
|
2291
|
+
for (const part of decodedParts) {
|
|
2292
|
+
combined.set(part, offset$1);
|
|
2293
|
+
offset$1 += part.length;
|
|
2294
|
+
}
|
|
2295
|
+
body = combined.buffer;
|
|
2296
|
+
}
|
|
2297
|
+
} else if (isJsonMode) {
|
|
2298
|
+
const mergedParts = [];
|
|
2299
|
+
for (const part of dataParts) {
|
|
2300
|
+
const trimmed = part.trim();
|
|
2301
|
+
if (trimmed.length === 0) continue;
|
|
2302
|
+
if (trimmed.startsWith(`[`) && trimmed.endsWith(`]`)) {
|
|
2303
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
2304
|
+
if (inner.length > 0) mergedParts.push(inner);
|
|
2305
|
+
} else mergedParts.push(trimmed);
|
|
2306
|
+
}
|
|
2307
|
+
body = `[${mergedParts.join(`,`)}]`;
|
|
2308
|
+
} else body = dataParts.join(``);
|
|
2309
|
+
return new Response(body, {
|
|
2310
|
+
status: 200,
|
|
2311
|
+
headers
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
/**
|
|
2101
2315
|
* Resolve headers from HeadersRecord (supports async functions).
|
|
2102
2316
|
* Unified implementation used by both stream() and DurableStream.
|
|
2103
2317
|
*/
|
|
@@ -2264,7 +2478,6 @@ async function streamInternal(options) {
|
|
|
2264
2478
|
const startOffset = options.offset ?? `-1`;
|
|
2265
2479
|
fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, startOffset);
|
|
2266
2480
|
const live = options.live ?? true;
|
|
2267
|
-
if (live === `long-poll` || live === `sse`) fetchUrl.searchParams.set(LIVE_QUERY_PARAM, live);
|
|
2268
2481
|
const params = await resolveParams(options.params);
|
|
2269
2482
|
for (const [key, value] of Object.entries(params)) fetchUrl.searchParams.set(key, value);
|
|
2270
2483
|
const headers = await resolveHeaders(options.headers);
|
|
@@ -2289,12 +2502,11 @@ async function streamInternal(options) {
|
|
|
2289
2502
|
const initialStreamClosed = firstResponse.headers.get(STREAM_CLOSED_HEADER)?.toLowerCase() === `true`;
|
|
2290
2503
|
const isJsonMode = options.json === true || (contentType?.includes(`application/json`) ?? false);
|
|
2291
2504
|
const encoding = firstResponse.headers.get(STREAM_SSE_DATA_ENCODING_HEADER) === `base64` ? `base64` : void 0;
|
|
2292
|
-
const fetchNext = async (offset, cursor, signal, resumingFromPause) => {
|
|
2505
|
+
const fetchNext = async (offset, cursor, signal, upToDate, resumingFromPause) => {
|
|
2293
2506
|
const nextUrl = new URL(url);
|
|
2294
2507
|
nextUrl.searchParams.set(OFFSET_QUERY_PARAM, offset);
|
|
2295
|
-
if (!resumingFromPause) {
|
|
2296
|
-
if (live === `
|
|
2297
|
-
else if (live === true || live === `long-poll`) nextUrl.searchParams.set(LIVE_QUERY_PARAM, `long-poll`);
|
|
2508
|
+
if (upToDate && !resumingFromPause) {
|
|
2509
|
+
if (live === true || live === `long-poll`) nextUrl.searchParams.set(LIVE_QUERY_PARAM, `long-poll`);
|
|
2298
2510
|
}
|
|
2299
2511
|
if (cursor) nextUrl.searchParams.set(`cursor`, cursor);
|
|
2300
2512
|
const nextParams = await resolveParams(options.params);
|
|
@@ -2418,6 +2630,7 @@ var IdempotentProducer = class {
|
|
|
2418
2630
|
#maxBatchBytes;
|
|
2419
2631
|
#lingerMs;
|
|
2420
2632
|
#fetchClient;
|
|
2633
|
+
#headers;
|
|
2421
2634
|
#signal;
|
|
2422
2635
|
#onError;
|
|
2423
2636
|
#pendingBatch = [];
|
|
@@ -2425,9 +2638,11 @@ var IdempotentProducer = class {
|
|
|
2425
2638
|
#lingerTimeout = null;
|
|
2426
2639
|
#queue;
|
|
2427
2640
|
#maxInFlight;
|
|
2641
|
+
#deferredEnqueues = /* @__PURE__ */ new Set();
|
|
2428
2642
|
#closed = false;
|
|
2429
2643
|
#closeResult = null;
|
|
2430
2644
|
#pendingFinalMessage;
|
|
2645
|
+
#lastSuccessfulOffset;
|
|
2431
2646
|
#epochClaimed;
|
|
2432
2647
|
#seqState = /* @__PURE__ */ new Map();
|
|
2433
2648
|
/**
|
|
@@ -2453,6 +2668,7 @@ var IdempotentProducer = class {
|
|
|
2453
2668
|
this.#maxBatchBytes = maxBatchBytes;
|
|
2454
2669
|
this.#lingerMs = lingerMs;
|
|
2455
2670
|
this.#signal = opts?.signal;
|
|
2671
|
+
this.#headers = opts?.headers;
|
|
2456
2672
|
this.#onError = opts?.onError;
|
|
2457
2673
|
this.#fetchClient = opts?.fetch ?? ((...args) => fetch(...args));
|
|
2458
2674
|
this.#maxInFlight = maxInFlight;
|
|
@@ -2514,7 +2730,10 @@ var IdempotentProducer = class {
|
|
|
2514
2730
|
this.#lingerTimeout = null;
|
|
2515
2731
|
}
|
|
2516
2732
|
if (this.#pendingBatch.length > 0) this.#enqueuePendingBatch();
|
|
2517
|
-
|
|
2733
|
+
do {
|
|
2734
|
+
await this.#queue.drained();
|
|
2735
|
+
await Promise.all(this.#deferredEnqueues);
|
|
2736
|
+
} while (this.#deferredEnqueues.size > 0 || this.inFlightCount > 0);
|
|
2518
2737
|
}
|
|
2519
2738
|
/**
|
|
2520
2739
|
* Stop the producer without closing the underlying stream.
|
|
@@ -2578,13 +2797,13 @@ var IdempotentProducer = class {
|
|
|
2578
2797
|
else body = bodyBytes;
|
|
2579
2798
|
}
|
|
2580
2799
|
const seqForThisRequest = this.#nextSeq;
|
|
2581
|
-
const headers = {
|
|
2800
|
+
const headers = await this.#buildHeaders({
|
|
2582
2801
|
"content-type": contentType,
|
|
2583
2802
|
[PRODUCER_ID_HEADER]: this.#producerId,
|
|
2584
2803
|
[PRODUCER_EPOCH_HEADER]: this.#epoch.toString(),
|
|
2585
2804
|
[PRODUCER_SEQ_HEADER]: seqForThisRequest.toString(),
|
|
2586
2805
|
[STREAM_CLOSED_HEADER]: `true`
|
|
2587
|
-
};
|
|
2806
|
+
});
|
|
2588
2807
|
const response = await this.#fetchClient(this.#stream.url, {
|
|
2589
2808
|
method: `POST`,
|
|
2590
2809
|
headers,
|
|
@@ -2593,11 +2812,15 @@ var IdempotentProducer = class {
|
|
|
2593
2812
|
});
|
|
2594
2813
|
if (response.status === 204) {
|
|
2595
2814
|
this.#nextSeq = seqForThisRequest + 1;
|
|
2596
|
-
|
|
2815
|
+
const finalOffset = response.headers.get(STREAM_OFFSET_HEADER) ?? ``;
|
|
2816
|
+
this.#recordSuccessfulOffset(finalOffset);
|
|
2817
|
+
return { finalOffset };
|
|
2597
2818
|
}
|
|
2598
2819
|
if (response.status === 200) {
|
|
2599
2820
|
this.#nextSeq = seqForThisRequest + 1;
|
|
2600
|
-
|
|
2821
|
+
const finalOffset = response.headers.get(STREAM_OFFSET_HEADER) ?? ``;
|
|
2822
|
+
this.#recordSuccessfulOffset(finalOffset);
|
|
2823
|
+
return { finalOffset };
|
|
2601
2824
|
}
|
|
2602
2825
|
if (response.status === 403) {
|
|
2603
2826
|
const currentEpochStr = response.headers.get(PRODUCER_EPOCH_HEADER);
|
|
@@ -2645,7 +2868,14 @@ var IdempotentProducer = class {
|
|
|
2645
2868
|
* Number of batches currently in flight.
|
|
2646
2869
|
*/
|
|
2647
2870
|
get inFlightCount() {
|
|
2648
|
-
return this.#queue.length();
|
|
2871
|
+
return this.#queue.length() + this.#queue.running();
|
|
2872
|
+
}
|
|
2873
|
+
/**
|
|
2874
|
+
* The greatest non-empty stream offset returned by a successful producer
|
|
2875
|
+
* append or close request.
|
|
2876
|
+
*/
|
|
2877
|
+
get lastSuccessfulOffset() {
|
|
2878
|
+
return this.#lastSuccessfulOffset;
|
|
2649
2879
|
}
|
|
2650
2880
|
/**
|
|
2651
2881
|
* Enqueue the current pending batch for processing.
|
|
@@ -2653,17 +2883,22 @@ var IdempotentProducer = class {
|
|
|
2653
2883
|
#enqueuePendingBatch() {
|
|
2654
2884
|
if (this.#pendingBatch.length === 0) return;
|
|
2655
2885
|
const batch = this.#pendingBatch;
|
|
2656
|
-
const seq = this.#nextSeq;
|
|
2657
2886
|
this.#pendingBatch = [];
|
|
2658
2887
|
this.#batchBytes = 0;
|
|
2888
|
+
if (this.#autoClaim && !this.#epochClaimed && this.inFlightCount > 0) {
|
|
2889
|
+
const deferred = this.#queue.drained().then(() => {
|
|
2890
|
+
this.#pushBatch(batch);
|
|
2891
|
+
}).finally(() => {
|
|
2892
|
+
this.#deferredEnqueues.delete(deferred);
|
|
2893
|
+
});
|
|
2894
|
+
this.#deferredEnqueues.add(deferred);
|
|
2895
|
+
deferred.catch(() => {});
|
|
2896
|
+
} else this.#pushBatch(batch);
|
|
2897
|
+
}
|
|
2898
|
+
#pushBatch(batch) {
|
|
2899
|
+
const seq = this.#nextSeq;
|
|
2659
2900
|
this.#nextSeq++;
|
|
2660
|
-
|
|
2661
|
-
this.#queue.push({
|
|
2662
|
-
batch,
|
|
2663
|
-
seq
|
|
2664
|
-
}).catch(() => {});
|
|
2665
|
-
});
|
|
2666
|
-
else this.#queue.push({
|
|
2901
|
+
this.#queue.push({
|
|
2667
2902
|
batch,
|
|
2668
2903
|
seq
|
|
2669
2904
|
}).catch(() => {});
|
|
@@ -2675,7 +2910,8 @@ var IdempotentProducer = class {
|
|
|
2675
2910
|
const { batch, seq } = task;
|
|
2676
2911
|
const epoch = this.#epoch;
|
|
2677
2912
|
try {
|
|
2678
|
-
await this.#doSendBatch(batch, seq, epoch);
|
|
2913
|
+
const result = await this.#doSendBatch(batch, seq, epoch);
|
|
2914
|
+
this.#recordSuccessfulOffset(result.offset);
|
|
2679
2915
|
if (!this.#epochClaimed) this.#epochClaimed = true;
|
|
2680
2916
|
this.#signalSeqComplete(epoch, seq, void 0);
|
|
2681
2917
|
} catch (error) {
|
|
@@ -2684,6 +2920,9 @@ var IdempotentProducer = class {
|
|
|
2684
2920
|
throw error;
|
|
2685
2921
|
}
|
|
2686
2922
|
}
|
|
2923
|
+
#recordSuccessfulOffset(offset) {
|
|
2924
|
+
if (offset && (!this.#lastSuccessfulOffset || offset > this.#lastSuccessfulOffset)) this.#lastSuccessfulOffset = offset;
|
|
2925
|
+
}
|
|
2687
2926
|
/**
|
|
2688
2927
|
* Signal that a sequence has completed (success or failure).
|
|
2689
2928
|
*/
|
|
@@ -2758,12 +2997,12 @@ var IdempotentProducer = class {
|
|
|
2758
2997
|
batchedBody = concatenated;
|
|
2759
2998
|
}
|
|
2760
2999
|
const url = this.#stream.url;
|
|
2761
|
-
const headers = {
|
|
3000
|
+
const headers = await this.#buildHeaders({
|
|
2762
3001
|
"content-type": contentType,
|
|
2763
3002
|
[PRODUCER_ID_HEADER]: this.#producerId,
|
|
2764
3003
|
[PRODUCER_EPOCH_HEADER]: epoch.toString(),
|
|
2765
3004
|
[PRODUCER_SEQ_HEADER]: seq.toString()
|
|
2766
|
-
};
|
|
3005
|
+
});
|
|
2767
3006
|
const response = await this.#fetchClient(url, {
|
|
2768
3007
|
method: `POST`,
|
|
2769
3008
|
headers,
|
|
@@ -2804,6 +3043,15 @@ var IdempotentProducer = class {
|
|
|
2804
3043
|
if (response.status === 400) throw await DurableStreamError.fromResponse(response, url);
|
|
2805
3044
|
throw await FetchError.fromResponse(response, url);
|
|
2806
3045
|
}
|
|
3046
|
+
async #buildHeaders(protocolHeaders) {
|
|
3047
|
+
const streamHeaders = await this.#stream.resolveHeaders();
|
|
3048
|
+
const producerHeaders = await resolveHeaders(this.#headers);
|
|
3049
|
+
return {
|
|
3050
|
+
...streamHeaders,
|
|
3051
|
+
...producerHeaders,
|
|
3052
|
+
...protocolHeaders
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
2807
3055
|
/**
|
|
2808
3056
|
* Clear pending batch and report error.
|
|
2809
3057
|
*/
|
|
@@ -2847,7 +3095,7 @@ function isPromiseLike(value) {
|
|
|
2847
3095
|
* contentType: "application/json"
|
|
2848
3096
|
* });
|
|
2849
3097
|
*
|
|
2850
|
-
* //
|
|
3098
|
+
* // Single write
|
|
2851
3099
|
* await stream.append(JSON.stringify({ message: "hello" }));
|
|
2852
3100
|
*
|
|
2853
3101
|
* // Read with the new API
|
|
@@ -2870,6 +3118,7 @@ var DurableStream = class DurableStream {
|
|
|
2870
3118
|
contentType;
|
|
2871
3119
|
#options;
|
|
2872
3120
|
#fetchClient;
|
|
3121
|
+
#baseFetchClient;
|
|
2873
3122
|
#onError;
|
|
2874
3123
|
#batchingEnabled;
|
|
2875
3124
|
#queue;
|
|
@@ -2890,7 +3139,9 @@ var DurableStream = class DurableStream {
|
|
|
2890
3139
|
if (opts.contentType) this.contentType = opts.contentType;
|
|
2891
3140
|
this.#batchingEnabled = opts.batching !== false;
|
|
2892
3141
|
if (this.#batchingEnabled) this.#queue = import_queue.default.promise(this.#batchWorker.bind(this), 1);
|
|
2893
|
-
|
|
3142
|
+
this.#baseFetchClient = opts.fetch ?? ((...args) => fetch(...args));
|
|
3143
|
+
const backOffOpts = { ...opts.backoffOptions ?? BackoffDefaults };
|
|
3144
|
+
const fetchWithBackoffClient = createFetchWithBackoff(this.#baseFetchClient, backOffOpts);
|
|
2894
3145
|
this.#fetchClient = createFetchWithConsumedBody(fetchWithBackoffClient);
|
|
2895
3146
|
}
|
|
2896
3147
|
/**
|
|
@@ -2945,12 +3196,15 @@ var DurableStream = class DurableStream {
|
|
|
2945
3196
|
*/
|
|
2946
3197
|
async head(opts) {
|
|
2947
3198
|
const { requestHeaders, fetchUrl } = await this.#buildRequest();
|
|
2948
|
-
const response = await this.#
|
|
3199
|
+
const response = await this.#baseFetchClient(fetchUrl.toString(), {
|
|
2949
3200
|
method: `HEAD`,
|
|
2950
3201
|
headers: requestHeaders,
|
|
2951
3202
|
signal: opts?.signal ?? this.#options.signal
|
|
2952
3203
|
});
|
|
2953
|
-
if (!response.ok)
|
|
3204
|
+
if (!response.ok) {
|
|
3205
|
+
if (response.status === 404) return { exists: false };
|
|
3206
|
+
await handleErrorResponse(response, this.url);
|
|
3207
|
+
}
|
|
2954
3208
|
const contentType = response.headers.get(`content-type`) ?? void 0;
|
|
2955
3209
|
const offset = response.headers.get(STREAM_OFFSET_HEADER) ?? void 0;
|
|
2956
3210
|
const etag = response.headers.get(`etag`) ?? void 0;
|
|
@@ -3048,9 +3302,14 @@ var DurableStream = class DurableStream {
|
|
|
3048
3302
|
/**
|
|
3049
3303
|
* Append a single payload to the stream.
|
|
3050
3304
|
*
|
|
3051
|
-
*
|
|
3052
|
-
*
|
|
3053
|
-
*
|
|
3305
|
+
* Batching: when batching is enabled (default), append() calls that overlap
|
|
3306
|
+
* in time (e.g. fired without awaiting each one) are coalesced into a
|
|
3307
|
+
* single POST while a prior POST is in flight. If every call is awaited
|
|
3308
|
+
* before the next is issued, no batching happens — each call becomes its
|
|
3309
|
+
* own roundtrip. For tight loops driving an async iterable (e.g. LLM
|
|
3310
|
+
* token streams), prefer `appendStream()` / `writable()` which pipe the
|
|
3311
|
+
* source over a single POST, or fire `append()` calls without awaiting
|
|
3312
|
+
* each one and await the last promise (and `close()`) at the end.
|
|
3054
3313
|
*
|
|
3055
3314
|
* - `body` must be string or Uint8Array.
|
|
3056
3315
|
* - For JSON streams, pass pre-serialized JSON strings.
|
|
@@ -3060,7 +3319,7 @@ var DurableStream = class DurableStream {
|
|
|
3060
3319
|
*
|
|
3061
3320
|
* @example
|
|
3062
3321
|
* ```typescript
|
|
3063
|
-
* // JSON stream - pass pre-serialized JSON
|
|
3322
|
+
* // JSON stream - pass pre-serialized JSON (single write)
|
|
3064
3323
|
* await stream.append(JSON.stringify({ message: "hello" }));
|
|
3065
3324
|
*
|
|
3066
3325
|
* // Byte stream
|
|
@@ -3069,6 +3328,14 @@ var DurableStream = class DurableStream {
|
|
|
3069
3328
|
*
|
|
3070
3329
|
* // Promise value - awaited before buffering
|
|
3071
3330
|
* await stream.append(fetchData());
|
|
3331
|
+
*
|
|
3332
|
+
* // High-frequency writes from an async iterable - fire-and-track-last
|
|
3333
|
+
* let last: Promise<void> = Promise.resolve();
|
|
3334
|
+
* for await (const chunk of source) {
|
|
3335
|
+
* last = stream.append(JSON.stringify(chunk));
|
|
3336
|
+
* }
|
|
3337
|
+
* await last;
|
|
3338
|
+
* await stream.close();
|
|
3072
3339
|
* ```
|
|
3073
3340
|
*/
|
|
3074
3341
|
async append(body, opts) {
|
|
@@ -3275,6 +3542,7 @@ var DurableStream = class DurableStream {
|
|
|
3275
3542
|
let writeError = null;
|
|
3276
3543
|
const producer = new IdempotentProducer(this, producerId, {
|
|
3277
3544
|
autoClaim: true,
|
|
3545
|
+
headers: opts?.headers,
|
|
3278
3546
|
lingerMs: opts?.lingerMs,
|
|
3279
3547
|
maxBatchBytes: opts?.maxBatchBytes,
|
|
3280
3548
|
onError: (error) => {
|
|
@@ -3357,6 +3625,14 @@ var DurableStream = class DurableStream {
|
|
|
3357
3625
|
});
|
|
3358
3626
|
}
|
|
3359
3627
|
/**
|
|
3628
|
+
* Resolve the stream's configured headers.
|
|
3629
|
+
* Used by IdempotentProducer to merge auth headers into its requests.
|
|
3630
|
+
* @internal
|
|
3631
|
+
*/
|
|
3632
|
+
async resolveHeaders() {
|
|
3633
|
+
return resolveHeaders(this.#options.headers);
|
|
3634
|
+
}
|
|
3635
|
+
/**
|
|
3360
3636
|
* Build request headers and URL.
|
|
3361
3637
|
*/
|
|
3362
3638
|
async #buildRequest() {
|