@prisma/composer-prisma-cloud 0.1.0-dev.18 → 0.1.0-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/control.d.mts +29 -6
- package/dist/control.mjs +67 -454
- package/dist/control.mjs.map +1 -1
- package/dist/cron/index.mjs +6 -14
- package/dist/cron/index.mjs.map +1 -1
- package/dist/cron/scheduler-entrypoint.mjs +21 -70
- package/dist/cron/scheduler-entrypoint.mjs.map +1 -1
- package/dist/cron/scheduler-service.mjs +6 -14
- package/dist/cron/scheduler-service.mjs.map +1 -1
- package/dist/index.d.mts +1 -40
- package/dist/index.mjs +2 -42
- package/dist/index.mjs.map +1 -1
- package/dist/{prisma-next-DlU01kXJ-2sP5mQb6.mjs → prisma-next-qPB8_Az6.mjs} +3 -3
- package/dist/prisma-next-qPB8_Az6.mjs.map +1 -0
- package/dist/prisma-next.d.mts +1 -1
- package/dist/prisma-next.mjs +1 -1
- package/dist/{provisioned-edges-DIQAR4q4-Bn9op-JG.mjs → provisioned-edges-Bb7WiV49-DeSPi3Ax.mjs} +4 -4
- package/dist/provisioned-edges-Bb7WiV49-DeSPi3Ax.mjs.map +1 -0
- package/dist/storage/index.d.mts +10 -11
- package/dist/storage/index.mjs +6 -14
- package/dist/storage/index.mjs.map +1 -1
- package/dist/storage/storage-entrypoint.mjs +1 -11
- package/dist/storage/storage-entrypoint.mjs.map +1 -1
- package/dist/storage/storage-service.mjs +6 -14
- package/dist/storage/storage-service.mjs.map +1 -1
- package/dist/storage/testing.mjs.map +1 -1
- package/dist/streams/index.d.mts +12 -13
- package/dist/streams/index.mjs +205 -489
- package/dist/streams/index.mjs.map +1 -1
- package/dist/streams/streams-entrypoint.mjs +1 -11
- package/dist/streams/streams-entrypoint.mjs.map +1 -1
- package/dist/streams/streams-service.mjs +6 -14
- package/dist/streams/streams-service.mjs.map +1 -1
- package/dist/streams/testing.d.mts +1 -1
- package/dist/streams/testing.mjs.map +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs.map +1 -1
- package/package.json +15 -15
- package/dist/prisma-next-DlU01kXJ-2sP5mQb6.mjs.map +0 -1
- package/dist/provisioned-edges-DIQAR4q4-Bn9op-JG.mjs.map +0 -1
package/dist/streams/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dependency, hydrateSecrets, hydrateSync, module, number, provisionNeed, resource, service, string } from "@prisma/composer";
|
|
2
2
|
import { blindCast } from "@prisma/composer/casts";
|
|
3
|
-
import { RPC_PEER_KEY } from "@prisma/composer/
|
|
3
|
+
import { RPC_PEER_KEY } from "@prisma/composer/rpc";
|
|
4
4
|
import { type } from "arktype";
|
|
5
5
|
import node from "@prisma/composer/node";
|
|
6
6
|
blindCast(Symbol.for("prisma:prisma-cloud-secret-source"));
|
|
@@ -199,7 +199,7 @@ function standardValidateSync(schema, value) {
|
|
|
199
199
|
return result.value;
|
|
200
200
|
}
|
|
201
201
|
//#endregion
|
|
202
|
-
//#region ../../1-prisma-cloud/1-extensions/target/dist/provisioned-edges-
|
|
202
|
+
//#region ../../1-prisma-cloud/1-extensions/target/dist/provisioned-edges-Bb7WiV49.mjs
|
|
203
203
|
/**
|
|
204
204
|
* RPC's reserved provider param (ADR-0030/ADR-0031): the declaration —
|
|
205
205
|
* name + schema + brand — for the accepted-keys set a provider stores, shared
|
|
@@ -218,7 +218,7 @@ function standardValidateSync(schema, value) {
|
|
|
218
218
|
* The reserved provider param for RPC's accepted-keys set: the var name is
|
|
219
219
|
* `RPC_ACCEPTED_KEYS`, derived through `configKey` at both ends
|
|
220
220
|
* (`configKey(address, …)` at deploy, `configKey('', …)` at boot — the
|
|
221
|
-
* address-free form is `@internal/
|
|
221
|
+
* address-free form is `@internal/rpc`'s `RPC_ACCEPTED_KEYS_ENV`). `brand` is
|
|
222
222
|
* `RPC_PEER_KEY`, the same brand `perBindingToken()`'s need carries — control.ts
|
|
223
223
|
* looks its `value(refs)` up by this field.
|
|
224
224
|
*/
|
|
@@ -254,16 +254,8 @@ configKey("", {
|
|
|
254
254
|
});
|
|
255
255
|
const RESERVED_PROVIDER_PARAMS = [RPC_ACCEPTED_KEYS_PARAM, STREAMS_API_KEY_PARAM];
|
|
256
256
|
blindCast(Symbol.for("prisma:prisma-cloud-param-source"));
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
__cmp: {
|
|
260
|
-
url: "",
|
|
261
|
-
bucket: "",
|
|
262
|
-
accessKeyId: "",
|
|
263
|
-
secretAccessKey: ""
|
|
264
|
-
},
|
|
265
|
-
satisfies: (required) => required.kind === "s3"
|
|
266
|
-
});
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region ../../1-prisma-cloud/1-extensions/target/dist/index.mjs
|
|
267
259
|
const reservedParams = { port: number({ default: 3e3 }) };
|
|
268
260
|
/**
|
|
269
261
|
* A Prisma Compute service — declarations only (deps + params + build + the
|
|
@@ -416,7 +408,7 @@ function s3StoreService(def) {
|
|
|
416
408
|
}));
|
|
417
409
|
}
|
|
418
410
|
//#endregion
|
|
419
|
-
//#region ../../1-prisma-cloud/2-shared-modules/storage/dist/storage-service-
|
|
411
|
+
//#region ../../1-prisma-cloud/2-shared-modules/storage/dist/storage-service-BQdOiMsW.mjs
|
|
420
412
|
const s3Contract = Object.freeze({
|
|
421
413
|
kind: "s3",
|
|
422
414
|
__cmp: {
|
|
@@ -473,7 +465,7 @@ function storageService(opts) {
|
|
|
473
465
|
}
|
|
474
466
|
storageService({ bucket: "storage" });
|
|
475
467
|
//#endregion
|
|
476
|
-
//#region ../../1-prisma-cloud/2-shared-modules/streams/dist/streams-service-
|
|
468
|
+
//#region ../../1-prisma-cloud/2-shared-modules/streams/dist/streams-service-Mz48Yi0V.mjs
|
|
477
469
|
var __create = Object.create;
|
|
478
470
|
var __defProp = Object.defineProperty;
|
|
479
471
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -872,7 +864,7 @@ var FetchError = class FetchError extends Error {
|
|
|
872
864
|
let text = void 0;
|
|
873
865
|
let json = void 0;
|
|
874
866
|
const contentType = response.headers.get(`content-type`);
|
|
875
|
-
if (!response.bodyUsed
|
|
867
|
+
if (!response.bodyUsed) if (contentType && contentType.includes(`application/json`)) try {
|
|
876
868
|
json = await response.json();
|
|
877
869
|
} catch {
|
|
878
870
|
text = await response.text();
|
|
@@ -921,7 +913,7 @@ var DurableStreamError = class DurableStreamError extends Error {
|
|
|
921
913
|
const status = response.status;
|
|
922
914
|
let details;
|
|
923
915
|
const contentType = response.headers.get(`content-type`);
|
|
924
|
-
if (!response.bodyUsed
|
|
916
|
+
if (!response.bodyUsed) if (contentType && contentType.includes(`application/json`)) try {
|
|
925
917
|
details = await response.json();
|
|
926
918
|
} catch {
|
|
927
919
|
details = await response.text();
|
|
@@ -1290,204 +1282,6 @@ async function* parseSSEStream(stream$1, signal) {
|
|
|
1290
1282
|
}
|
|
1291
1283
|
}
|
|
1292
1284
|
/**
|
|
1293
|
-
* Abstract base class for stream response state.
|
|
1294
|
-
* All state transitions return new immutable state objects.
|
|
1295
|
-
*/
|
|
1296
|
-
var StreamResponseState = class {
|
|
1297
|
-
shouldContinueLive(stopAfterUpToDate, liveMode) {
|
|
1298
|
-
if (stopAfterUpToDate && this.upToDate) return false;
|
|
1299
|
-
if (liveMode === false) return false;
|
|
1300
|
-
if (this.streamClosed) return false;
|
|
1301
|
-
return true;
|
|
1302
|
-
}
|
|
1303
|
-
};
|
|
1304
|
-
/**
|
|
1305
|
-
* State for long-poll mode. shouldUseSse() returns false.
|
|
1306
|
-
*/
|
|
1307
|
-
var LongPollState = class LongPollState extends StreamResponseState {
|
|
1308
|
-
offset;
|
|
1309
|
-
cursor;
|
|
1310
|
-
upToDate;
|
|
1311
|
-
streamClosed;
|
|
1312
|
-
constructor(fields) {
|
|
1313
|
-
super();
|
|
1314
|
-
this.offset = fields.offset;
|
|
1315
|
-
this.cursor = fields.cursor;
|
|
1316
|
-
this.upToDate = fields.upToDate;
|
|
1317
|
-
this.streamClosed = fields.streamClosed;
|
|
1318
|
-
}
|
|
1319
|
-
shouldUseSse() {
|
|
1320
|
-
return false;
|
|
1321
|
-
}
|
|
1322
|
-
withResponseMetadata(update) {
|
|
1323
|
-
return new LongPollState({
|
|
1324
|
-
offset: update.offset ?? this.offset,
|
|
1325
|
-
cursor: update.cursor ?? this.cursor,
|
|
1326
|
-
upToDate: update.upToDate,
|
|
1327
|
-
streamClosed: this.streamClosed || update.streamClosed
|
|
1328
|
-
});
|
|
1329
|
-
}
|
|
1330
|
-
withSSEControl(event) {
|
|
1331
|
-
const streamClosed = this.streamClosed || (event.streamClosed ?? false);
|
|
1332
|
-
return new LongPollState({
|
|
1333
|
-
offset: event.streamNextOffset,
|
|
1334
|
-
cursor: event.streamCursor || this.cursor,
|
|
1335
|
-
upToDate: event.streamClosed ?? false ? true : event.upToDate ?? this.upToDate,
|
|
1336
|
-
streamClosed
|
|
1337
|
-
});
|
|
1338
|
-
}
|
|
1339
|
-
pause() {
|
|
1340
|
-
return new PausedState(this);
|
|
1341
|
-
}
|
|
1342
|
-
};
|
|
1343
|
-
/**
|
|
1344
|
-
* State for SSE mode. shouldUseSse() returns true.
|
|
1345
|
-
* Tracks SSE connection resilience (short connection detection).
|
|
1346
|
-
*/
|
|
1347
|
-
var SSEState = class SSEState extends StreamResponseState {
|
|
1348
|
-
offset;
|
|
1349
|
-
cursor;
|
|
1350
|
-
upToDate;
|
|
1351
|
-
streamClosed;
|
|
1352
|
-
consecutiveShortConnections;
|
|
1353
|
-
connectionStartTime;
|
|
1354
|
-
constructor(fields) {
|
|
1355
|
-
super();
|
|
1356
|
-
this.offset = fields.offset;
|
|
1357
|
-
this.cursor = fields.cursor;
|
|
1358
|
-
this.upToDate = fields.upToDate;
|
|
1359
|
-
this.streamClosed = fields.streamClosed;
|
|
1360
|
-
this.consecutiveShortConnections = fields.consecutiveShortConnections ?? 0;
|
|
1361
|
-
this.connectionStartTime = fields.connectionStartTime;
|
|
1362
|
-
}
|
|
1363
|
-
shouldUseSse() {
|
|
1364
|
-
return true;
|
|
1365
|
-
}
|
|
1366
|
-
withResponseMetadata(update) {
|
|
1367
|
-
return new SSEState({
|
|
1368
|
-
offset: update.offset ?? this.offset,
|
|
1369
|
-
cursor: update.cursor ?? this.cursor,
|
|
1370
|
-
upToDate: update.upToDate,
|
|
1371
|
-
streamClosed: this.streamClosed || update.streamClosed,
|
|
1372
|
-
consecutiveShortConnections: this.consecutiveShortConnections,
|
|
1373
|
-
connectionStartTime: this.connectionStartTime
|
|
1374
|
-
});
|
|
1375
|
-
}
|
|
1376
|
-
withSSEControl(event) {
|
|
1377
|
-
const streamClosed = this.streamClosed || (event.streamClosed ?? false);
|
|
1378
|
-
return new SSEState({
|
|
1379
|
-
offset: event.streamNextOffset,
|
|
1380
|
-
cursor: event.streamCursor || this.cursor,
|
|
1381
|
-
upToDate: event.streamClosed ?? false ? true : event.upToDate ?? this.upToDate,
|
|
1382
|
-
streamClosed,
|
|
1383
|
-
consecutiveShortConnections: this.consecutiveShortConnections,
|
|
1384
|
-
connectionStartTime: this.connectionStartTime
|
|
1385
|
-
});
|
|
1386
|
-
}
|
|
1387
|
-
startConnection(now) {
|
|
1388
|
-
return new SSEState({
|
|
1389
|
-
offset: this.offset,
|
|
1390
|
-
cursor: this.cursor,
|
|
1391
|
-
upToDate: this.upToDate,
|
|
1392
|
-
streamClosed: this.streamClosed,
|
|
1393
|
-
consecutiveShortConnections: this.consecutiveShortConnections,
|
|
1394
|
-
connectionStartTime: now
|
|
1395
|
-
});
|
|
1396
|
-
}
|
|
1397
|
-
handleConnectionEnd(now, wasAborted, config) {
|
|
1398
|
-
if (this.connectionStartTime === void 0) return {
|
|
1399
|
-
action: `healthy`,
|
|
1400
|
-
state: this
|
|
1401
|
-
};
|
|
1402
|
-
const duration = now - this.connectionStartTime;
|
|
1403
|
-
if (duration < config.minConnectionDuration && !wasAborted) {
|
|
1404
|
-
const newCount = this.consecutiveShortConnections + 1;
|
|
1405
|
-
if (newCount >= config.maxShortConnections) return {
|
|
1406
|
-
action: `fallback`,
|
|
1407
|
-
state: new LongPollState({
|
|
1408
|
-
offset: this.offset,
|
|
1409
|
-
cursor: this.cursor,
|
|
1410
|
-
upToDate: this.upToDate,
|
|
1411
|
-
streamClosed: this.streamClosed
|
|
1412
|
-
})
|
|
1413
|
-
};
|
|
1414
|
-
return {
|
|
1415
|
-
action: `reconnect`,
|
|
1416
|
-
state: new SSEState({
|
|
1417
|
-
offset: this.offset,
|
|
1418
|
-
cursor: this.cursor,
|
|
1419
|
-
upToDate: this.upToDate,
|
|
1420
|
-
streamClosed: this.streamClosed,
|
|
1421
|
-
consecutiveShortConnections: newCount,
|
|
1422
|
-
connectionStartTime: this.connectionStartTime
|
|
1423
|
-
}),
|
|
1424
|
-
backoffAttempt: newCount
|
|
1425
|
-
};
|
|
1426
|
-
}
|
|
1427
|
-
if (duration >= config.minConnectionDuration) return {
|
|
1428
|
-
action: `healthy`,
|
|
1429
|
-
state: new SSEState({
|
|
1430
|
-
offset: this.offset,
|
|
1431
|
-
cursor: this.cursor,
|
|
1432
|
-
upToDate: this.upToDate,
|
|
1433
|
-
streamClosed: this.streamClosed,
|
|
1434
|
-
consecutiveShortConnections: 0,
|
|
1435
|
-
connectionStartTime: this.connectionStartTime
|
|
1436
|
-
})
|
|
1437
|
-
};
|
|
1438
|
-
return {
|
|
1439
|
-
action: `healthy`,
|
|
1440
|
-
state: this
|
|
1441
|
-
};
|
|
1442
|
-
}
|
|
1443
|
-
pause() {
|
|
1444
|
-
return new PausedState(this);
|
|
1445
|
-
}
|
|
1446
|
-
};
|
|
1447
|
-
/**
|
|
1448
|
-
* Paused state wrapper. Delegates all sync field access to the inner state.
|
|
1449
|
-
* resume() returns the wrapped state unchanged (identity preserved).
|
|
1450
|
-
*/
|
|
1451
|
-
var PausedState = class PausedState extends StreamResponseState {
|
|
1452
|
-
#inner;
|
|
1453
|
-
constructor(inner) {
|
|
1454
|
-
super();
|
|
1455
|
-
this.#inner = inner;
|
|
1456
|
-
}
|
|
1457
|
-
get offset() {
|
|
1458
|
-
return this.#inner.offset;
|
|
1459
|
-
}
|
|
1460
|
-
get cursor() {
|
|
1461
|
-
return this.#inner.cursor;
|
|
1462
|
-
}
|
|
1463
|
-
get upToDate() {
|
|
1464
|
-
return this.#inner.upToDate;
|
|
1465
|
-
}
|
|
1466
|
-
get streamClosed() {
|
|
1467
|
-
return this.#inner.streamClosed;
|
|
1468
|
-
}
|
|
1469
|
-
shouldUseSse() {
|
|
1470
|
-
return this.#inner.shouldUseSse();
|
|
1471
|
-
}
|
|
1472
|
-
withResponseMetadata(update) {
|
|
1473
|
-
const newInner = this.#inner.withResponseMetadata(update);
|
|
1474
|
-
return new PausedState(newInner);
|
|
1475
|
-
}
|
|
1476
|
-
withSSEControl(event) {
|
|
1477
|
-
const newInner = this.#inner.withSSEControl(event);
|
|
1478
|
-
return new PausedState(newInner);
|
|
1479
|
-
}
|
|
1480
|
-
pause() {
|
|
1481
|
-
return this;
|
|
1482
|
-
}
|
|
1483
|
-
resume() {
|
|
1484
|
-
return {
|
|
1485
|
-
state: this.#inner,
|
|
1486
|
-
justResumed: true
|
|
1487
|
-
};
|
|
1488
|
-
}
|
|
1489
|
-
};
|
|
1490
|
-
/**
|
|
1491
1285
|
* Constant used as abort reason when pausing the stream due to visibility change.
|
|
1492
1286
|
*/
|
|
1493
1287
|
const PAUSE_STREAM = `PAUSE_STREAM`;
|
|
@@ -1504,7 +1298,10 @@ var StreamResponseImpl = class {
|
|
|
1504
1298
|
#statusText;
|
|
1505
1299
|
#ok;
|
|
1506
1300
|
#isLoading;
|
|
1507
|
-
#
|
|
1301
|
+
#offset;
|
|
1302
|
+
#cursor;
|
|
1303
|
+
#upToDate;
|
|
1304
|
+
#streamClosed;
|
|
1508
1305
|
#isJsonMode;
|
|
1509
1306
|
#abortController;
|
|
1510
1307
|
#fetchNext;
|
|
@@ -1519,7 +1316,11 @@ var StreamResponseImpl = class {
|
|
|
1519
1316
|
#unsubscribeFromVisibilityChanges;
|
|
1520
1317
|
#pausePromise;
|
|
1521
1318
|
#pauseResolve;
|
|
1319
|
+
#justResumedFromPause = false;
|
|
1522
1320
|
#sseResilience;
|
|
1321
|
+
#lastSSEConnectionStartTime;
|
|
1322
|
+
#consecutiveShortSSEConnections = 0;
|
|
1323
|
+
#sseFallbackToLongPoll = false;
|
|
1523
1324
|
#encoding;
|
|
1524
1325
|
#responseStream;
|
|
1525
1326
|
constructor(config) {
|
|
@@ -1527,13 +1328,10 @@ var StreamResponseImpl = class {
|
|
|
1527
1328
|
this.contentType = config.contentType;
|
|
1528
1329
|
this.live = config.live;
|
|
1529
1330
|
this.startOffset = config.startOffset;
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
streamClosed: config.initialStreamClosed
|
|
1535
|
-
};
|
|
1536
|
-
this.#syncState = config.startSSE ? new SSEState(syncFields) : new LongPollState(syncFields);
|
|
1331
|
+
this.#offset = config.initialOffset;
|
|
1332
|
+
this.#cursor = config.initialCursor;
|
|
1333
|
+
this.#upToDate = config.initialUpToDate;
|
|
1334
|
+
this.#streamClosed = config.initialStreamClosed;
|
|
1537
1335
|
this.#headers = config.firstResponse.headers;
|
|
1538
1336
|
this.#status = config.firstResponse.status;
|
|
1539
1337
|
this.#statusText = config.firstResponse.statusText;
|
|
@@ -1590,7 +1388,6 @@ var StreamResponseImpl = class {
|
|
|
1590
1388
|
#pause() {
|
|
1591
1389
|
if (this.#state === `active`) {
|
|
1592
1390
|
this.#state = `pause-requested`;
|
|
1593
|
-
this.#syncState = this.#syncState.pause();
|
|
1594
1391
|
this.#pausePromise = new Promise((resolve) => {
|
|
1595
1392
|
this.#pauseResolve = resolve;
|
|
1596
1393
|
});
|
|
@@ -1604,8 +1401,8 @@ var StreamResponseImpl = class {
|
|
|
1604
1401
|
#resume() {
|
|
1605
1402
|
if (this.#state === `paused` || this.#state === `pause-requested`) {
|
|
1606
1403
|
if (this.#abortController.signal.aborted) return;
|
|
1607
|
-
if (this.#syncState instanceof PausedState) this.#syncState = this.#syncState.resume().state;
|
|
1608
1404
|
this.#state = `active`;
|
|
1405
|
+
this.#justResumedFromPause = true;
|
|
1609
1406
|
this.#pauseResolve?.();
|
|
1610
1407
|
this.#pausePromise = void 0;
|
|
1611
1408
|
this.#pauseResolve = void 0;
|
|
@@ -1627,16 +1424,16 @@ var StreamResponseImpl = class {
|
|
|
1627
1424
|
return this.#isLoading;
|
|
1628
1425
|
}
|
|
1629
1426
|
get offset() {
|
|
1630
|
-
return this.#
|
|
1427
|
+
return this.#offset;
|
|
1631
1428
|
}
|
|
1632
1429
|
get cursor() {
|
|
1633
|
-
return this.#
|
|
1430
|
+
return this.#cursor;
|
|
1634
1431
|
}
|
|
1635
1432
|
get upToDate() {
|
|
1636
|
-
return this.#
|
|
1433
|
+
return this.#upToDate;
|
|
1637
1434
|
}
|
|
1638
1435
|
get streamClosed() {
|
|
1639
|
-
return this.#
|
|
1436
|
+
return this.#streamClosed;
|
|
1640
1437
|
}
|
|
1641
1438
|
#ensureJsonMode() {
|
|
1642
1439
|
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`);
|
|
@@ -1662,68 +1459,160 @@ var StreamResponseImpl = class {
|
|
|
1662
1459
|
* and whether we've received upToDate or streamClosed.
|
|
1663
1460
|
*/
|
|
1664
1461
|
#shouldContinueLive() {
|
|
1665
|
-
|
|
1462
|
+
if (this.#stopAfterUpToDate && this.upToDate) return false;
|
|
1463
|
+
if (this.live === false) return false;
|
|
1464
|
+
if (this.#streamClosed) return false;
|
|
1465
|
+
return true;
|
|
1666
1466
|
}
|
|
1667
1467
|
/**
|
|
1668
1468
|
* Update state from response headers.
|
|
1669
1469
|
*/
|
|
1670
1470
|
#updateStateFromResponse(response) {
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1471
|
+
const offset = response.headers.get(STREAM_OFFSET_HEADER);
|
|
1472
|
+
if (offset) this.#offset = offset;
|
|
1473
|
+
const cursor = response.headers.get(STREAM_CURSOR_HEADER);
|
|
1474
|
+
if (cursor) this.#cursor = cursor;
|
|
1475
|
+
this.#upToDate = response.headers.has(STREAM_UP_TO_DATE_HEADER);
|
|
1476
|
+
if (response.headers.get(STREAM_CLOSED_HEADER)?.toLowerCase() === `true`) this.#streamClosed = true;
|
|
1677
1477
|
this.#headers = response.headers;
|
|
1678
1478
|
this.#status = response.status;
|
|
1679
1479
|
this.#statusText = response.statusText;
|
|
1680
1480
|
this.#ok = response.ok;
|
|
1681
1481
|
}
|
|
1682
1482
|
/**
|
|
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
|
+
/**
|
|
1683
1565
|
* Update instance state from an SSE control event.
|
|
1684
1566
|
*/
|
|
1685
1567
|
#updateStateFromSSEControl(controlEvent) {
|
|
1686
|
-
this.#
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1568
|
+
this.#offset = controlEvent.streamNextOffset;
|
|
1569
|
+
if (controlEvent.streamCursor) this.#cursor = controlEvent.streamCursor;
|
|
1570
|
+
if (controlEvent.upToDate !== void 0) this.#upToDate = controlEvent.upToDate;
|
|
1571
|
+
if (controlEvent.streamClosed) {
|
|
1572
|
+
this.#streamClosed = true;
|
|
1573
|
+
this.#upToDate = true;
|
|
1574
|
+
}
|
|
1690
1575
|
}
|
|
1691
1576
|
/**
|
|
1692
1577
|
* Mark the start of an SSE connection for duration tracking.
|
|
1693
|
-
* If the state is not SSEState (e.g., auto-detected SSE from content-type),
|
|
1694
|
-
* transitions to SSEState first.
|
|
1695
1578
|
*/
|
|
1696
1579
|
#markSSEConnectionStart() {
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1580
|
+
this.#lastSSEConnectionStartTime = Date.now();
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Handle SSE connection end - check duration and manage fallback state.
|
|
1584
|
+
* Returns a delay to wait before reconnecting, or null if should not reconnect.
|
|
1585
|
+
*/
|
|
1586
|
+
async #handleSSEConnectionEnd() {
|
|
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;
|
|
1704
1604
|
}
|
|
1705
1605
|
/**
|
|
1706
1606
|
* Try to reconnect SSE and return the new iterator, or null if reconnection
|
|
1707
1607
|
* is not possible or fails.
|
|
1708
1608
|
*/
|
|
1709
1609
|
async #trySSEReconnect() {
|
|
1710
|
-
if (
|
|
1610
|
+
if (this.#sseFallbackToLongPoll) return null;
|
|
1711
1611
|
if (!this.#shouldContinueLive() || !this.#startSSE) return null;
|
|
1712
|
-
|
|
1713
|
-
this.#syncState = result.state;
|
|
1714
|
-
if (result.action === `fallback`) {
|
|
1715
|
-
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.");
|
|
1716
|
-
return null;
|
|
1717
|
-
}
|
|
1718
|
-
if (result.action === `reconnect`) {
|
|
1719
|
-
const maxDelay = Math.min(this.#sseResilience.backoffMaxDelay, this.#sseResilience.backoffBaseDelay * Math.pow(2, result.backoffAttempt));
|
|
1720
|
-
const delayMs = Math.floor(Math.random() * maxDelay);
|
|
1721
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1722
|
-
}
|
|
1612
|
+
if (await this.#handleSSEConnectionEnd() === null) return null;
|
|
1723
1613
|
this.#markSSEConnectionStart();
|
|
1724
1614
|
this.#requestAbortController = new AbortController();
|
|
1725
1615
|
const newSSEResponse = await this.#startSSE(this.offset, this.cursor, this.#requestAbortController.signal);
|
|
1726
|
-
this.#updateEncodingFromSSEResponse(newSSEResponse);
|
|
1727
1616
|
if (newSSEResponse.body) return parseSSEStream(newSSEResponse.body, this.#requestAbortController.signal);
|
|
1728
1617
|
return null;
|
|
1729
1618
|
}
|
|
@@ -1756,7 +1645,7 @@ var StreamResponseImpl = class {
|
|
|
1756
1645
|
this.#updateStateFromSSEControl(event);
|
|
1757
1646
|
if (event.upToDate) return {
|
|
1758
1647
|
type: `response`,
|
|
1759
|
-
response: createSSESyntheticResponse(``, event.streamNextOffset, event.streamCursor, true, event.streamClosed ?? false
|
|
1648
|
+
response: this.#createSSESyntheticResponse(``, event.streamNextOffset, event.streamCursor, true, event.streamClosed ?? false)
|
|
1760
1649
|
};
|
|
1761
1650
|
return { type: `continue` };
|
|
1762
1651
|
}
|
|
@@ -1773,7 +1662,7 @@ var StreamResponseImpl = class {
|
|
|
1773
1662
|
while (true) {
|
|
1774
1663
|
const { done: controlDone, value: controlEvent } = await sseEventIterator.next();
|
|
1775
1664
|
if (controlDone) {
|
|
1776
|
-
const response = createSSESyntheticResponseFromParts(bufferedDataParts, this.offset, this.cursor, this.upToDate, this.streamClosed
|
|
1665
|
+
const response = this.#createSSESyntheticResponseFromParts(bufferedDataParts, this.offset, this.cursor, this.upToDate, this.streamClosed);
|
|
1777
1666
|
try {
|
|
1778
1667
|
return {
|
|
1779
1668
|
type: `response`,
|
|
@@ -1791,7 +1680,7 @@ var StreamResponseImpl = class {
|
|
|
1791
1680
|
this.#updateStateFromSSEControl(controlEvent);
|
|
1792
1681
|
return {
|
|
1793
1682
|
type: `response`,
|
|
1794
|
-
response: createSSESyntheticResponseFromParts(bufferedDataParts, controlEvent.streamNextOffset, controlEvent.streamCursor, controlEvent.upToDate ?? false, controlEvent.streamClosed ?? false
|
|
1683
|
+
response: this.#createSSESyntheticResponseFromParts(bufferedDataParts, controlEvent.streamNextOffset, controlEvent.streamCursor, controlEvent.upToDate ?? false, controlEvent.streamClosed ?? false)
|
|
1795
1684
|
};
|
|
1796
1685
|
}
|
|
1797
1686
|
bufferedDataParts.push(controlEvent.data);
|
|
@@ -1814,7 +1703,6 @@ var StreamResponseImpl = class {
|
|
|
1814
1703
|
firstResponseYielded = true;
|
|
1815
1704
|
if ((firstResponse.headers.get(`content-type`)?.includes(`text/event-stream`) ?? false) && firstResponse.body) {
|
|
1816
1705
|
this.#markSSEConnectionStart();
|
|
1817
|
-
this.#updateEncodingFromSSEResponse(firstResponse);
|
|
1818
1706
|
this.#requestAbortController = new AbortController();
|
|
1819
1707
|
sseEventIterator = parseSSEStream(firstResponse.body, this.#requestAbortController.signal);
|
|
1820
1708
|
} else {
|
|
@@ -1827,22 +1715,6 @@ var StreamResponseImpl = class {
|
|
|
1827
1715
|
return;
|
|
1828
1716
|
}
|
|
1829
1717
|
}
|
|
1830
|
-
if (!sseEventIterator && this.upToDate && this.#startSSE && this.#shouldContinueLive()) {
|
|
1831
|
-
if (this.#state === `pause-requested` || this.#state === `paused`) {
|
|
1832
|
-
this.#state = `paused`;
|
|
1833
|
-
if (this.#pausePromise) await this.#pausePromise;
|
|
1834
|
-
if (this.#abortController.signal.aborted) {
|
|
1835
|
-
this.#markClosed();
|
|
1836
|
-
controller.close();
|
|
1837
|
-
return;
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
this.#markSSEConnectionStart();
|
|
1841
|
-
this.#requestAbortController = new AbortController();
|
|
1842
|
-
const sseResponse = await this.#startSSE(this.offset, this.cursor, this.#requestAbortController.signal);
|
|
1843
|
-
this.#updateEncodingFromSSEResponse(sseResponse);
|
|
1844
|
-
if (sseResponse.body) sseEventIterator = parseSSEStream(sseResponse.body, this.#requestAbortController.signal);
|
|
1845
|
-
}
|
|
1846
1718
|
if (sseEventIterator) {
|
|
1847
1719
|
if (this.#state === `pause-requested` || this.#state === `paused`) {
|
|
1848
1720
|
this.#state = `paused`;
|
|
@@ -1882,7 +1754,6 @@ var StreamResponseImpl = class {
|
|
|
1882
1754
|
}
|
|
1883
1755
|
}
|
|
1884
1756
|
if (this.#shouldContinueLive()) {
|
|
1885
|
-
let resumingFromPause = false;
|
|
1886
1757
|
if (this.#state === `pause-requested` || this.#state === `paused`) {
|
|
1887
1758
|
this.#state = `paused`;
|
|
1888
1759
|
if (this.#pausePromise) await this.#pausePromise;
|
|
@@ -1891,15 +1762,16 @@ var StreamResponseImpl = class {
|
|
|
1891
1762
|
controller.close();
|
|
1892
1763
|
return;
|
|
1893
1764
|
}
|
|
1894
|
-
resumingFromPause = true;
|
|
1895
1765
|
}
|
|
1896
1766
|
if (this.#abortController.signal.aborted) {
|
|
1897
1767
|
this.#markClosed();
|
|
1898
1768
|
controller.close();
|
|
1899
1769
|
return;
|
|
1900
1770
|
}
|
|
1771
|
+
const resumingFromPause = this.#justResumedFromPause;
|
|
1772
|
+
this.#justResumedFromPause = false;
|
|
1901
1773
|
this.#requestAbortController = new AbortController();
|
|
1902
|
-
const response = await this.#fetchNext(this.offset, this.cursor, this.#requestAbortController.signal,
|
|
1774
|
+
const response = await this.#fetchNext(this.offset, this.cursor, this.#requestAbortController.signal, resumingFromPause);
|
|
1903
1775
|
this.#updateStateFromResponse(response);
|
|
1904
1776
|
controller.enqueue(response);
|
|
1905
1777
|
return;
|
|
@@ -2062,25 +1934,22 @@ var StreamResponseImpl = class {
|
|
|
2062
1934
|
controller.enqueue(pendingItems.shift());
|
|
2063
1935
|
return;
|
|
2064
1936
|
}
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
parsed = JSON.parse(content);
|
|
2071
|
-
} catch (err) {
|
|
2072
|
-
const preview = content.length > 100 ? content.slice(0, 100) + `...` : content;
|
|
2073
|
-
throw new DurableStreamError(`Failed to parse JSON response: ${err instanceof Error ? err.message : String(err)}. Data: ${preview}`, `PARSE_ERROR`);
|
|
2074
|
-
}
|
|
2075
|
-
pendingItems = Array.isArray(parsed) ? parsed : [parsed];
|
|
2076
|
-
if (pendingItems.length > 0) {
|
|
2077
|
-
controller.enqueue(pendingItems.shift());
|
|
2078
|
-
return;
|
|
2079
|
-
}
|
|
2080
|
-
result = await reader.read();
|
|
1937
|
+
const { done, value: response } = await reader.read();
|
|
1938
|
+
if (done) {
|
|
1939
|
+
this.#markClosed();
|
|
1940
|
+
controller.close();
|
|
1941
|
+
return;
|
|
2081
1942
|
}
|
|
2082
|
-
|
|
2083
|
-
|
|
1943
|
+
const content = (await response.text()).trim() || `[]`;
|
|
1944
|
+
let parsed;
|
|
1945
|
+
try {
|
|
1946
|
+
parsed = JSON.parse(content);
|
|
1947
|
+
} catch (err) {
|
|
1948
|
+
const preview = content.length > 100 ? content.slice(0, 100) + `...` : content;
|
|
1949
|
+
throw new DurableStreamError(`Failed to parse JSON response: ${err instanceof Error ? err.message : String(err)}. Data: ${preview}`, `PARSE_ERROR`);
|
|
1950
|
+
}
|
|
1951
|
+
pendingItems = Array.isArray(parsed) ? parsed : [parsed];
|
|
1952
|
+
if (pendingItems.length > 0) controller.enqueue(pendingItems.shift());
|
|
2084
1953
|
},
|
|
2085
1954
|
cancel: () => {
|
|
2086
1955
|
reader.releaseLock();
|
|
@@ -2112,7 +1981,7 @@ var StreamResponseImpl = class {
|
|
|
2112
1981
|
while (!result.done) {
|
|
2113
1982
|
if (abortController.signal.aborted) break;
|
|
2114
1983
|
const response = result.value;
|
|
2115
|
-
const { offset, cursor, upToDate, streamClosed } = getMetadataFromResponse(response
|
|
1984
|
+
const { offset, cursor, upToDate, streamClosed } = this.#getMetadataFromResponse(response);
|
|
2116
1985
|
const content = (await response.text()).trim() || `[]`;
|
|
2117
1986
|
let parsed;
|
|
2118
1987
|
try {
|
|
@@ -2156,7 +2025,7 @@ var StreamResponseImpl = class {
|
|
|
2156
2025
|
while (!result.done) {
|
|
2157
2026
|
if (abortController.signal.aborted) break;
|
|
2158
2027
|
const response = result.value;
|
|
2159
|
-
const { offset, cursor, upToDate, streamClosed } = getMetadataFromResponse(response
|
|
2028
|
+
const { offset, cursor, upToDate, streamClosed } = this.#getMetadataFromResponse(response);
|
|
2160
2029
|
const buffer = await response.arrayBuffer();
|
|
2161
2030
|
await subscriber({
|
|
2162
2031
|
data: new Uint8Array(buffer),
|
|
@@ -2193,7 +2062,7 @@ var StreamResponseImpl = class {
|
|
|
2193
2062
|
while (!result.done) {
|
|
2194
2063
|
if (abortController.signal.aborted) break;
|
|
2195
2064
|
const response = result.value;
|
|
2196
|
-
const { offset, cursor, upToDate, streamClosed } = getMetadataFromResponse(response
|
|
2065
|
+
const { offset, cursor, upToDate, streamClosed } = this.#getMetadataFromResponse(response);
|
|
2197
2066
|
await subscriber({
|
|
2198
2067
|
text: await response.text(),
|
|
2199
2068
|
offset,
|
|
@@ -2229,97 +2098,6 @@ var StreamResponseImpl = class {
|
|
|
2229
2098
|
}
|
|
2230
2099
|
};
|
|
2231
2100
|
/**
|
|
2232
|
-
* Extract stream metadata from Response headers.
|
|
2233
|
-
* Falls back to the provided defaults when headers are absent.
|
|
2234
|
-
*/
|
|
2235
|
-
function getMetadataFromResponse(response, fallbackOffset, fallbackCursor, fallbackStreamClosed) {
|
|
2236
|
-
const offset = response.headers.get(STREAM_OFFSET_HEADER);
|
|
2237
|
-
const cursor = response.headers.get(STREAM_CURSOR_HEADER);
|
|
2238
|
-
const upToDate = response.headers.has(STREAM_UP_TO_DATE_HEADER);
|
|
2239
|
-
const streamClosed = response.headers.get(STREAM_CLOSED_HEADER)?.toLowerCase() === `true`;
|
|
2240
|
-
return {
|
|
2241
|
-
offset: offset ?? fallbackOffset,
|
|
2242
|
-
cursor: cursor ?? fallbackCursor,
|
|
2243
|
-
upToDate,
|
|
2244
|
-
streamClosed: streamClosed || fallbackStreamClosed
|
|
2245
|
-
};
|
|
2246
|
-
}
|
|
2247
|
-
/**
|
|
2248
|
-
* Decode base64 string to Uint8Array.
|
|
2249
|
-
* Per protocol: concatenate data lines, remove \n and \r, then decode.
|
|
2250
|
-
*/
|
|
2251
|
-
function decodeBase64(base64Str) {
|
|
2252
|
-
const cleaned = base64Str.replace(/[\n\r]/g, ``);
|
|
2253
|
-
if (cleaned.length === 0) return /* @__PURE__ */ new Uint8Array(0);
|
|
2254
|
-
if (cleaned.length % 4 !== 0) throw new DurableStreamError(`Invalid base64 data: length ${cleaned.length} is not a multiple of 4`, `PARSE_ERROR`);
|
|
2255
|
-
try {
|
|
2256
|
-
if (typeof Buffer !== `undefined`) return new Uint8Array(Buffer.from(cleaned, `base64`));
|
|
2257
|
-
else {
|
|
2258
|
-
const binaryStr = atob(cleaned);
|
|
2259
|
-
const bytes = new Uint8Array(binaryStr.length);
|
|
2260
|
-
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
|
|
2261
|
-
return bytes;
|
|
2262
|
-
}
|
|
2263
|
-
} catch (err) {
|
|
2264
|
-
throw new DurableStreamError(`Failed to decode base64 data: ${err instanceof Error ? err.message : String(err)}`, `PARSE_ERROR`);
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
/**
|
|
2268
|
-
* Create a synthetic Response from SSE data with proper headers.
|
|
2269
|
-
* Includes offset/cursor/upToDate/streamClosed in headers so subscribers can read them.
|
|
2270
|
-
*/
|
|
2271
|
-
function createSSESyntheticResponse(data, offset, cursor, upToDate, streamClosed, contentType, encoding) {
|
|
2272
|
-
return createSSESyntheticResponseFromParts([data], offset, cursor, upToDate, streamClosed, contentType, encoding);
|
|
2273
|
-
}
|
|
2274
|
-
/**
|
|
2275
|
-
* Create a synthetic Response from multiple SSE data parts.
|
|
2276
|
-
* For base64 mode, each part is independently encoded, so we decode each
|
|
2277
|
-
* separately and concatenate the binary results.
|
|
2278
|
-
* For text mode, parts are simply concatenated as strings.
|
|
2279
|
-
*/
|
|
2280
|
-
function createSSESyntheticResponseFromParts(dataParts, offset, cursor, upToDate, streamClosed, contentType, encoding, isJsonMode) {
|
|
2281
|
-
const headers = {
|
|
2282
|
-
"content-type": contentType ?? `application/json`,
|
|
2283
|
-
[STREAM_OFFSET_HEADER]: String(offset)
|
|
2284
|
-
};
|
|
2285
|
-
if (cursor) headers[STREAM_CURSOR_HEADER] = cursor;
|
|
2286
|
-
if (upToDate) headers[STREAM_UP_TO_DATE_HEADER] = `true`;
|
|
2287
|
-
if (streamClosed) headers[STREAM_CLOSED_HEADER] = `true`;
|
|
2288
|
-
let body;
|
|
2289
|
-
if (encoding === `base64`) {
|
|
2290
|
-
const decodedParts = dataParts.filter((part) => part.length > 0).map((part) => decodeBase64(part));
|
|
2291
|
-
if (decodedParts.length === 0) body = /* @__PURE__ */ new ArrayBuffer(0);
|
|
2292
|
-
else if (decodedParts.length === 1) {
|
|
2293
|
-
const decoded = decodedParts[0];
|
|
2294
|
-
body = decoded.buffer.slice(decoded.byteOffset, decoded.byteOffset + decoded.byteLength);
|
|
2295
|
-
} else {
|
|
2296
|
-
const totalLength = decodedParts.reduce((sum, part) => sum + part.length, 0);
|
|
2297
|
-
const combined = new Uint8Array(totalLength);
|
|
2298
|
-
let offset$1 = 0;
|
|
2299
|
-
for (const part of decodedParts) {
|
|
2300
|
-
combined.set(part, offset$1);
|
|
2301
|
-
offset$1 += part.length;
|
|
2302
|
-
}
|
|
2303
|
-
body = combined.buffer;
|
|
2304
|
-
}
|
|
2305
|
-
} else if (isJsonMode) {
|
|
2306
|
-
const mergedParts = [];
|
|
2307
|
-
for (const part of dataParts) {
|
|
2308
|
-
const trimmed = part.trim();
|
|
2309
|
-
if (trimmed.length === 0) continue;
|
|
2310
|
-
if (trimmed.startsWith(`[`) && trimmed.endsWith(`]`)) {
|
|
2311
|
-
const inner = trimmed.slice(1, -1).trim();
|
|
2312
|
-
if (inner.length > 0) mergedParts.push(inner);
|
|
2313
|
-
} else mergedParts.push(trimmed);
|
|
2314
|
-
}
|
|
2315
|
-
body = `[${mergedParts.join(`,`)}]`;
|
|
2316
|
-
} else body = dataParts.join(``);
|
|
2317
|
-
return new Response(body, {
|
|
2318
|
-
status: 200,
|
|
2319
|
-
headers
|
|
2320
|
-
});
|
|
2321
|
-
}
|
|
2322
|
-
/**
|
|
2323
2101
|
* Resolve headers from HeadersRecord (supports async functions).
|
|
2324
2102
|
* Unified implementation used by both stream() and DurableStream.
|
|
2325
2103
|
*/
|
|
@@ -2486,6 +2264,7 @@ async function streamInternal(options) {
|
|
|
2486
2264
|
const startOffset = options.offset ?? `-1`;
|
|
2487
2265
|
fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, startOffset);
|
|
2488
2266
|
const live = options.live ?? true;
|
|
2267
|
+
if (live === `long-poll` || live === `sse`) fetchUrl.searchParams.set(LIVE_QUERY_PARAM, live);
|
|
2489
2268
|
const params = await resolveParams(options.params);
|
|
2490
2269
|
for (const [key, value] of Object.entries(params)) fetchUrl.searchParams.set(key, value);
|
|
2491
2270
|
const headers = await resolveHeaders(options.headers);
|
|
@@ -2510,11 +2289,12 @@ async function streamInternal(options) {
|
|
|
2510
2289
|
const initialStreamClosed = firstResponse.headers.get(STREAM_CLOSED_HEADER)?.toLowerCase() === `true`;
|
|
2511
2290
|
const isJsonMode = options.json === true || (contentType?.includes(`application/json`) ?? false);
|
|
2512
2291
|
const encoding = firstResponse.headers.get(STREAM_SSE_DATA_ENCODING_HEADER) === `base64` ? `base64` : void 0;
|
|
2513
|
-
const fetchNext = async (offset, cursor, signal,
|
|
2292
|
+
const fetchNext = async (offset, cursor, signal, resumingFromPause) => {
|
|
2514
2293
|
const nextUrl = new URL(url);
|
|
2515
2294
|
nextUrl.searchParams.set(OFFSET_QUERY_PARAM, offset);
|
|
2516
|
-
if (
|
|
2517
|
-
if (live ===
|
|
2295
|
+
if (!resumingFromPause) {
|
|
2296
|
+
if (live === `sse`) nextUrl.searchParams.set(LIVE_QUERY_PARAM, `sse`);
|
|
2297
|
+
else if (live === true || live === `long-poll`) nextUrl.searchParams.set(LIVE_QUERY_PARAM, `long-poll`);
|
|
2518
2298
|
}
|
|
2519
2299
|
if (cursor) nextUrl.searchParams.set(`cursor`, cursor);
|
|
2520
2300
|
const nextParams = await resolveParams(options.params);
|
|
@@ -2638,7 +2418,6 @@ var IdempotentProducer = class {
|
|
|
2638
2418
|
#maxBatchBytes;
|
|
2639
2419
|
#lingerMs;
|
|
2640
2420
|
#fetchClient;
|
|
2641
|
-
#headers;
|
|
2642
2421
|
#signal;
|
|
2643
2422
|
#onError;
|
|
2644
2423
|
#pendingBatch = [];
|
|
@@ -2646,11 +2425,9 @@ var IdempotentProducer = class {
|
|
|
2646
2425
|
#lingerTimeout = null;
|
|
2647
2426
|
#queue;
|
|
2648
2427
|
#maxInFlight;
|
|
2649
|
-
#deferredEnqueues = /* @__PURE__ */ new Set();
|
|
2650
2428
|
#closed = false;
|
|
2651
2429
|
#closeResult = null;
|
|
2652
2430
|
#pendingFinalMessage;
|
|
2653
|
-
#lastSuccessfulOffset;
|
|
2654
2431
|
#epochClaimed;
|
|
2655
2432
|
#seqState = /* @__PURE__ */ new Map();
|
|
2656
2433
|
/**
|
|
@@ -2676,7 +2453,6 @@ var IdempotentProducer = class {
|
|
|
2676
2453
|
this.#maxBatchBytes = maxBatchBytes;
|
|
2677
2454
|
this.#lingerMs = lingerMs;
|
|
2678
2455
|
this.#signal = opts?.signal;
|
|
2679
|
-
this.#headers = opts?.headers;
|
|
2680
2456
|
this.#onError = opts?.onError;
|
|
2681
2457
|
this.#fetchClient = opts?.fetch ?? ((...args) => fetch(...args));
|
|
2682
2458
|
this.#maxInFlight = maxInFlight;
|
|
@@ -2738,10 +2514,7 @@ var IdempotentProducer = class {
|
|
|
2738
2514
|
this.#lingerTimeout = null;
|
|
2739
2515
|
}
|
|
2740
2516
|
if (this.#pendingBatch.length > 0) this.#enqueuePendingBatch();
|
|
2741
|
-
|
|
2742
|
-
await this.#queue.drained();
|
|
2743
|
-
await Promise.all(this.#deferredEnqueues);
|
|
2744
|
-
} while (this.#deferredEnqueues.size > 0 || this.inFlightCount > 0);
|
|
2517
|
+
await this.#queue.drained();
|
|
2745
2518
|
}
|
|
2746
2519
|
/**
|
|
2747
2520
|
* Stop the producer without closing the underlying stream.
|
|
@@ -2805,13 +2578,13 @@ var IdempotentProducer = class {
|
|
|
2805
2578
|
else body = bodyBytes;
|
|
2806
2579
|
}
|
|
2807
2580
|
const seqForThisRequest = this.#nextSeq;
|
|
2808
|
-
const headers =
|
|
2581
|
+
const headers = {
|
|
2809
2582
|
"content-type": contentType,
|
|
2810
2583
|
[PRODUCER_ID_HEADER]: this.#producerId,
|
|
2811
2584
|
[PRODUCER_EPOCH_HEADER]: this.#epoch.toString(),
|
|
2812
2585
|
[PRODUCER_SEQ_HEADER]: seqForThisRequest.toString(),
|
|
2813
2586
|
[STREAM_CLOSED_HEADER]: `true`
|
|
2814
|
-
}
|
|
2587
|
+
};
|
|
2815
2588
|
const response = await this.#fetchClient(this.#stream.url, {
|
|
2816
2589
|
method: `POST`,
|
|
2817
2590
|
headers,
|
|
@@ -2820,15 +2593,11 @@ var IdempotentProducer = class {
|
|
|
2820
2593
|
});
|
|
2821
2594
|
if (response.status === 204) {
|
|
2822
2595
|
this.#nextSeq = seqForThisRequest + 1;
|
|
2823
|
-
|
|
2824
|
-
this.#recordSuccessfulOffset(finalOffset);
|
|
2825
|
-
return { finalOffset };
|
|
2596
|
+
return { finalOffset: response.headers.get(STREAM_OFFSET_HEADER) ?? `` };
|
|
2826
2597
|
}
|
|
2827
2598
|
if (response.status === 200) {
|
|
2828
2599
|
this.#nextSeq = seqForThisRequest + 1;
|
|
2829
|
-
|
|
2830
|
-
this.#recordSuccessfulOffset(finalOffset);
|
|
2831
|
-
return { finalOffset };
|
|
2600
|
+
return { finalOffset: response.headers.get(STREAM_OFFSET_HEADER) ?? `` };
|
|
2832
2601
|
}
|
|
2833
2602
|
if (response.status === 403) {
|
|
2834
2603
|
const currentEpochStr = response.headers.get(PRODUCER_EPOCH_HEADER);
|
|
@@ -2876,14 +2645,7 @@ var IdempotentProducer = class {
|
|
|
2876
2645
|
* Number of batches currently in flight.
|
|
2877
2646
|
*/
|
|
2878
2647
|
get inFlightCount() {
|
|
2879
|
-
return this.#queue.length()
|
|
2880
|
-
}
|
|
2881
|
-
/**
|
|
2882
|
-
* The greatest non-empty stream offset returned by a successful producer
|
|
2883
|
-
* append or close request.
|
|
2884
|
-
*/
|
|
2885
|
-
get lastSuccessfulOffset() {
|
|
2886
|
-
return this.#lastSuccessfulOffset;
|
|
2648
|
+
return this.#queue.length();
|
|
2887
2649
|
}
|
|
2888
2650
|
/**
|
|
2889
2651
|
* Enqueue the current pending batch for processing.
|
|
@@ -2891,22 +2653,17 @@ var IdempotentProducer = class {
|
|
|
2891
2653
|
#enqueuePendingBatch() {
|
|
2892
2654
|
if (this.#pendingBatch.length === 0) return;
|
|
2893
2655
|
const batch = this.#pendingBatch;
|
|
2656
|
+
const seq = this.#nextSeq;
|
|
2894
2657
|
this.#pendingBatch = [];
|
|
2895
2658
|
this.#batchBytes = 0;
|
|
2896
|
-
if (this.#autoClaim && !this.#epochClaimed && this.inFlightCount > 0) {
|
|
2897
|
-
const deferred = this.#queue.drained().then(() => {
|
|
2898
|
-
this.#pushBatch(batch);
|
|
2899
|
-
}).finally(() => {
|
|
2900
|
-
this.#deferredEnqueues.delete(deferred);
|
|
2901
|
-
});
|
|
2902
|
-
this.#deferredEnqueues.add(deferred);
|
|
2903
|
-
deferred.catch(() => {});
|
|
2904
|
-
} else this.#pushBatch(batch);
|
|
2905
|
-
}
|
|
2906
|
-
#pushBatch(batch) {
|
|
2907
|
-
const seq = this.#nextSeq;
|
|
2908
2659
|
this.#nextSeq++;
|
|
2909
|
-
this.#queue.
|
|
2660
|
+
if (this.#autoClaim && !this.#epochClaimed && this.#queue.length() > 0) this.#queue.drained().then(() => {
|
|
2661
|
+
this.#queue.push({
|
|
2662
|
+
batch,
|
|
2663
|
+
seq
|
|
2664
|
+
}).catch(() => {});
|
|
2665
|
+
});
|
|
2666
|
+
else this.#queue.push({
|
|
2910
2667
|
batch,
|
|
2911
2668
|
seq
|
|
2912
2669
|
}).catch(() => {});
|
|
@@ -2918,8 +2675,7 @@ var IdempotentProducer = class {
|
|
|
2918
2675
|
const { batch, seq } = task;
|
|
2919
2676
|
const epoch = this.#epoch;
|
|
2920
2677
|
try {
|
|
2921
|
-
|
|
2922
|
-
this.#recordSuccessfulOffset(result.offset);
|
|
2678
|
+
await this.#doSendBatch(batch, seq, epoch);
|
|
2923
2679
|
if (!this.#epochClaimed) this.#epochClaimed = true;
|
|
2924
2680
|
this.#signalSeqComplete(epoch, seq, void 0);
|
|
2925
2681
|
} catch (error) {
|
|
@@ -2928,9 +2684,6 @@ var IdempotentProducer = class {
|
|
|
2928
2684
|
throw error;
|
|
2929
2685
|
}
|
|
2930
2686
|
}
|
|
2931
|
-
#recordSuccessfulOffset(offset) {
|
|
2932
|
-
if (offset && (!this.#lastSuccessfulOffset || offset > this.#lastSuccessfulOffset)) this.#lastSuccessfulOffset = offset;
|
|
2933
|
-
}
|
|
2934
2687
|
/**
|
|
2935
2688
|
* Signal that a sequence has completed (success or failure).
|
|
2936
2689
|
*/
|
|
@@ -3005,12 +2758,12 @@ var IdempotentProducer = class {
|
|
|
3005
2758
|
batchedBody = concatenated;
|
|
3006
2759
|
}
|
|
3007
2760
|
const url = this.#stream.url;
|
|
3008
|
-
const headers =
|
|
2761
|
+
const headers = {
|
|
3009
2762
|
"content-type": contentType,
|
|
3010
2763
|
[PRODUCER_ID_HEADER]: this.#producerId,
|
|
3011
2764
|
[PRODUCER_EPOCH_HEADER]: epoch.toString(),
|
|
3012
2765
|
[PRODUCER_SEQ_HEADER]: seq.toString()
|
|
3013
|
-
}
|
|
2766
|
+
};
|
|
3014
2767
|
const response = await this.#fetchClient(url, {
|
|
3015
2768
|
method: `POST`,
|
|
3016
2769
|
headers,
|
|
@@ -3051,15 +2804,6 @@ var IdempotentProducer = class {
|
|
|
3051
2804
|
if (response.status === 400) throw await DurableStreamError.fromResponse(response, url);
|
|
3052
2805
|
throw await FetchError.fromResponse(response, url);
|
|
3053
2806
|
}
|
|
3054
|
-
async #buildHeaders(protocolHeaders) {
|
|
3055
|
-
const streamHeaders = await this.#stream.resolveHeaders();
|
|
3056
|
-
const producerHeaders = await resolveHeaders(this.#headers);
|
|
3057
|
-
return {
|
|
3058
|
-
...streamHeaders,
|
|
3059
|
-
...producerHeaders,
|
|
3060
|
-
...protocolHeaders
|
|
3061
|
-
};
|
|
3062
|
-
}
|
|
3063
2807
|
/**
|
|
3064
2808
|
* Clear pending batch and report error.
|
|
3065
2809
|
*/
|
|
@@ -3103,7 +2847,7 @@ function isPromiseLike(value) {
|
|
|
3103
2847
|
* contentType: "application/json"
|
|
3104
2848
|
* });
|
|
3105
2849
|
*
|
|
3106
|
-
* //
|
|
2850
|
+
* // Write data
|
|
3107
2851
|
* await stream.append(JSON.stringify({ message: "hello" }));
|
|
3108
2852
|
*
|
|
3109
2853
|
* // Read with the new API
|
|
@@ -3126,7 +2870,6 @@ var DurableStream = class DurableStream {
|
|
|
3126
2870
|
contentType;
|
|
3127
2871
|
#options;
|
|
3128
2872
|
#fetchClient;
|
|
3129
|
-
#baseFetchClient;
|
|
3130
2873
|
#onError;
|
|
3131
2874
|
#batchingEnabled;
|
|
3132
2875
|
#queue;
|
|
@@ -3147,9 +2890,7 @@ var DurableStream = class DurableStream {
|
|
|
3147
2890
|
if (opts.contentType) this.contentType = opts.contentType;
|
|
3148
2891
|
this.#batchingEnabled = opts.batching !== false;
|
|
3149
2892
|
if (this.#batchingEnabled) this.#queue = import_queue.default.promise(this.#batchWorker.bind(this), 1);
|
|
3150
|
-
|
|
3151
|
-
const backOffOpts = { ...opts.backoffOptions ?? BackoffDefaults };
|
|
3152
|
-
const fetchWithBackoffClient = createFetchWithBackoff(this.#baseFetchClient, backOffOpts);
|
|
2893
|
+
const fetchWithBackoffClient = createFetchWithBackoff(opts.fetch ?? ((...args) => fetch(...args)), { ...opts.backoffOptions ?? BackoffDefaults });
|
|
3153
2894
|
this.#fetchClient = createFetchWithConsumedBody(fetchWithBackoffClient);
|
|
3154
2895
|
}
|
|
3155
2896
|
/**
|
|
@@ -3204,15 +2945,12 @@ var DurableStream = class DurableStream {
|
|
|
3204
2945
|
*/
|
|
3205
2946
|
async head(opts) {
|
|
3206
2947
|
const { requestHeaders, fetchUrl } = await this.#buildRequest();
|
|
3207
|
-
const response = await this.#
|
|
2948
|
+
const response = await this.#fetchClient(fetchUrl.toString(), {
|
|
3208
2949
|
method: `HEAD`,
|
|
3209
2950
|
headers: requestHeaders,
|
|
3210
2951
|
signal: opts?.signal ?? this.#options.signal
|
|
3211
2952
|
});
|
|
3212
|
-
if (!response.ok)
|
|
3213
|
-
if (response.status === 404) return { exists: false };
|
|
3214
|
-
await handleErrorResponse(response, this.url);
|
|
3215
|
-
}
|
|
2953
|
+
if (!response.ok) await handleErrorResponse(response, this.url);
|
|
3216
2954
|
const contentType = response.headers.get(`content-type`) ?? void 0;
|
|
3217
2955
|
const offset = response.headers.get(STREAM_OFFSET_HEADER) ?? void 0;
|
|
3218
2956
|
const etag = response.headers.get(`etag`) ?? void 0;
|
|
@@ -3310,14 +3048,9 @@ var DurableStream = class DurableStream {
|
|
|
3310
3048
|
/**
|
|
3311
3049
|
* Append a single payload to the stream.
|
|
3312
3050
|
*
|
|
3313
|
-
*
|
|
3314
|
-
*
|
|
3315
|
-
*
|
|
3316
|
-
* before the next is issued, no batching happens — each call becomes its
|
|
3317
|
-
* own roundtrip. For tight loops driving an async iterable (e.g. LLM
|
|
3318
|
-
* token streams), prefer `appendStream()` / `writable()` which pipe the
|
|
3319
|
-
* source over a single POST, or fire `append()` calls without awaiting
|
|
3320
|
-
* each one and await the last promise (and `close()`) at the end.
|
|
3051
|
+
* When batching is enabled (default), multiple append() calls made while
|
|
3052
|
+
* a POST is in-flight will be batched together into a single request.
|
|
3053
|
+
* This significantly improves throughput for high-frequency writes.
|
|
3321
3054
|
*
|
|
3322
3055
|
* - `body` must be string or Uint8Array.
|
|
3323
3056
|
* - For JSON streams, pass pre-serialized JSON strings.
|
|
@@ -3327,7 +3060,7 @@ var DurableStream = class DurableStream {
|
|
|
3327
3060
|
*
|
|
3328
3061
|
* @example
|
|
3329
3062
|
* ```typescript
|
|
3330
|
-
* // JSON stream - pass pre-serialized JSON
|
|
3063
|
+
* // JSON stream - pass pre-serialized JSON
|
|
3331
3064
|
* await stream.append(JSON.stringify({ message: "hello" }));
|
|
3332
3065
|
*
|
|
3333
3066
|
* // Byte stream
|
|
@@ -3336,14 +3069,6 @@ var DurableStream = class DurableStream {
|
|
|
3336
3069
|
*
|
|
3337
3070
|
* // Promise value - awaited before buffering
|
|
3338
3071
|
* await stream.append(fetchData());
|
|
3339
|
-
*
|
|
3340
|
-
* // High-frequency writes from an async iterable - fire-and-track-last
|
|
3341
|
-
* let last: Promise<void> = Promise.resolve();
|
|
3342
|
-
* for await (const chunk of source) {
|
|
3343
|
-
* last = stream.append(JSON.stringify(chunk));
|
|
3344
|
-
* }
|
|
3345
|
-
* await last;
|
|
3346
|
-
* await stream.close();
|
|
3347
3072
|
* ```
|
|
3348
3073
|
*/
|
|
3349
3074
|
async append(body, opts) {
|
|
@@ -3550,7 +3275,6 @@ var DurableStream = class DurableStream {
|
|
|
3550
3275
|
let writeError = null;
|
|
3551
3276
|
const producer = new IdempotentProducer(this, producerId, {
|
|
3552
3277
|
autoClaim: true,
|
|
3553
|
-
headers: opts?.headers,
|
|
3554
3278
|
lingerMs: opts?.lingerMs,
|
|
3555
3279
|
maxBatchBytes: opts?.maxBatchBytes,
|
|
3556
3280
|
onError: (error) => {
|
|
@@ -3633,14 +3357,6 @@ var DurableStream = class DurableStream {
|
|
|
3633
3357
|
});
|
|
3634
3358
|
}
|
|
3635
3359
|
/**
|
|
3636
|
-
* Resolve the stream's configured headers.
|
|
3637
|
-
* Used by IdempotentProducer to merge auth headers into its requests.
|
|
3638
|
-
* @internal
|
|
3639
|
-
*/
|
|
3640
|
-
async resolveHeaders() {
|
|
3641
|
-
return resolveHeaders(this.#options.headers);
|
|
3642
|
-
}
|
|
3643
|
-
/**
|
|
3644
3360
|
* Build request headers and URL.
|
|
3645
3361
|
*/
|
|
3646
3362
|
async #buildRequest() {
|