@cntryl/fitz 0.0.2 → 0.0.4
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/README.md +24 -2
- package/dist/index.cjs +998 -174
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +998 -175
- package/dist/index.mjs.map +1 -1
- package/dist/types/client/client.d.ts +1 -1
- package/dist/types/client/client.d.ts.map +1 -1
- package/dist/types/client/connection.d.ts +10 -3
- package/dist/types/client/connection.d.ts.map +1 -1
- package/dist/types/client/resilience.d.ts +21 -0
- package/dist/types/client/resilience.d.ts.map +1 -0
- package/dist/types/core/errors.d.ts.map +1 -1
- package/dist/types/core/types.d.ts +13 -0
- package/dist/types/core/types.d.ts.map +1 -1
- package/dist/types/core/wake-gate.d.ts +11 -0
- package/dist/types/core/wake-gate.d.ts.map +1 -0
- package/dist/types/domains/base.d.ts +9 -2
- package/dist/types/domains/base.d.ts.map +1 -1
- package/dist/types/domains/kv/transaction.d.ts.map +1 -1
- package/dist/types/domains/lease/client.d.ts.map +1 -1
- package/dist/types/domains/lease/types.d.ts.map +1 -1
- package/dist/types/domains/queue/client.d.ts +5 -0
- package/dist/types/domains/queue/client.d.ts.map +1 -1
- package/dist/types/domains/queue/types.d.ts.map +1 -1
- package/dist/types/domains/rpc/client.d.ts.map +1 -1
- package/dist/types/domains/schedule/client.d.ts +4 -1
- package/dist/types/domains/schedule/client.d.ts.map +1 -1
- package/dist/types/domains/stream/client.d.ts +7 -0
- package/dist/types/domains/stream/client.d.ts.map +1 -1
- package/dist/types/domains/stream/session.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/transport/tcp.d.ts.map +1 -1
- package/dist/types/transport/types.d.ts +7 -0
- package/dist/types/transport/types.d.ts.map +1 -1
- package/dist/types/transport/websocket.d.ts.map +1 -1
- package/package.json +7 -3
package/dist/index.cjs
CHANGED
|
@@ -528,11 +528,18 @@ function retryableKey(error) {
|
|
|
528
528
|
if (error.domainCode === void 0) return null;
|
|
529
529
|
return `${prefix}_${error.domainCode}`;
|
|
530
530
|
}
|
|
531
|
+
function isTransientQueueCommitFailure(error) {
|
|
532
|
+
if (!error.code.startsWith("QUEUE_")) return false;
|
|
533
|
+
const message = error.message.toLowerCase();
|
|
534
|
+
if (!message.includes("failed to commit transaction:")) return false;
|
|
535
|
+
return message.includes("writestall(") || message.includes("memory budget exceeded") || message.includes("lease heartbeat reports unhealthy") || message.includes("refusing writes");
|
|
536
|
+
}
|
|
531
537
|
function isRetryable(error) {
|
|
532
538
|
if (!(error instanceof FitzError)) return false;
|
|
533
539
|
if (error instanceof TimeoutError || error instanceof TransportError) return true;
|
|
534
540
|
const key = retryableKey(error);
|
|
535
|
-
|
|
541
|
+
if (key !== null && retryableErrorCodes.has(key)) return true;
|
|
542
|
+
return isTransientQueueCommitFailure(error);
|
|
536
543
|
}
|
|
537
544
|
/**
|
|
538
545
|
* Error types for Fitz client
|
|
@@ -971,7 +978,7 @@ function createMultiplexer(observability = {}) {
|
|
|
971
978
|
const span = tracer?.startSpan("fitz.request", attributes);
|
|
972
979
|
let spanEnded = false;
|
|
973
980
|
if (signal?.aborted) {
|
|
974
|
-
const error = abortError$
|
|
981
|
+
const error = abortError$3();
|
|
975
982
|
span?.recordException(error);
|
|
976
983
|
span?.end();
|
|
977
984
|
meter?.counter("fitz.request.failed", 1, {
|
|
@@ -1045,7 +1052,7 @@ function createMultiplexer(observability = {}) {
|
|
|
1045
1052
|
heapifyUp(requestEntry.timeoutIndex);
|
|
1046
1053
|
if (signal) {
|
|
1047
1054
|
onAbort = () => {
|
|
1048
|
-
failRequest(abortError$
|
|
1055
|
+
failRequest(abortError$3());
|
|
1049
1056
|
};
|
|
1050
1057
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1051
1058
|
}
|
|
@@ -1065,7 +1072,7 @@ function createMultiplexer(observability = {}) {
|
|
|
1065
1072
|
return deferred.promise;
|
|
1066
1073
|
}, (err) => {
|
|
1067
1074
|
if (!finalize()) {
|
|
1068
|
-
if (signal?.aborted) throw abortError$
|
|
1075
|
+
if (signal?.aborted) throw abortError$3();
|
|
1069
1076
|
throw err;
|
|
1070
1077
|
}
|
|
1071
1078
|
unregisterRequest(messageType, requestEntry);
|
|
@@ -1078,7 +1085,7 @@ function createMultiplexer(observability = {}) {
|
|
|
1078
1085
|
span?.end();
|
|
1079
1086
|
spanEnded = true;
|
|
1080
1087
|
}
|
|
1081
|
-
if (signal?.aborted) throw abortError$
|
|
1088
|
+
if (signal?.aborted) throw abortError$3();
|
|
1082
1089
|
throw err;
|
|
1083
1090
|
});
|
|
1084
1091
|
};
|
|
@@ -1190,7 +1197,7 @@ function createMultiplexer(observability = {}) {
|
|
|
1190
1197
|
const Multiplexer = function(observability = {}) {
|
|
1191
1198
|
return createMultiplexer(observability);
|
|
1192
1199
|
};
|
|
1193
|
-
function abortError$
|
|
1200
|
+
function abortError$3() {
|
|
1194
1201
|
const error = /* @__PURE__ */ new Error("The operation was aborted");
|
|
1195
1202
|
error.name = "AbortError";
|
|
1196
1203
|
return error;
|
|
@@ -1234,8 +1241,64 @@ function readU32BE(payload, offset) {
|
|
|
1234
1241
|
return (payload[offset] << 24 | payload[offset + 1] << 16 | payload[offset + 2] << 8 | payload[offset + 3]) >>> 0;
|
|
1235
1242
|
}
|
|
1236
1243
|
//#endregion
|
|
1244
|
+
//#region src/client/resilience.ts
|
|
1245
|
+
const resilienceMetaSymbol = Symbol("fitz.resilience.meta");
|
|
1246
|
+
function attachResilienceMeta(error, meta) {
|
|
1247
|
+
if (error && (typeof error === "object" || typeof error === "function")) Object.defineProperty(error, resilienceMetaSymbol, {
|
|
1248
|
+
value: meta,
|
|
1249
|
+
configurable: true,
|
|
1250
|
+
enumerable: false,
|
|
1251
|
+
writable: true
|
|
1252
|
+
});
|
|
1253
|
+
return error;
|
|
1254
|
+
}
|
|
1255
|
+
function getResilienceMeta(error) {
|
|
1256
|
+
if (!error || typeof error !== "object" && typeof error !== "function") return;
|
|
1257
|
+
return error[resilienceMetaSymbol];
|
|
1258
|
+
}
|
|
1259
|
+
function classifyFailureKind(error) {
|
|
1260
|
+
if (error instanceof TimeoutError) return "timeout";
|
|
1261
|
+
if (error instanceof TransportError) return "transport";
|
|
1262
|
+
if (error instanceof ConnectionError) return "connection";
|
|
1263
|
+
if (error instanceof FitzError) return "domain";
|
|
1264
|
+
return "other";
|
|
1265
|
+
}
|
|
1266
|
+
function isTransientRetryError(error) {
|
|
1267
|
+
return error instanceof TimeoutError || error instanceof TransportError || error instanceof ConnectionError || isRetryable(error);
|
|
1268
|
+
}
|
|
1269
|
+
function shouldRetryOperation(retryClass, error) {
|
|
1270
|
+
switch (retryClass) {
|
|
1271
|
+
case "wait_only": return false;
|
|
1272
|
+
case "replayable_read": return isTransientRetryError(error);
|
|
1273
|
+
case "confirmed_negative_retry": {
|
|
1274
|
+
const meta = getResilienceMeta(error);
|
|
1275
|
+
return meta?.explicitNegative === true && meta.boundary === "post-send" && isRetryable(error);
|
|
1276
|
+
}
|
|
1277
|
+
default: return false;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
//#endregion
|
|
1237
1281
|
//#region src/client/connection.ts
|
|
1238
1282
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1283
|
+
const sleepWithAbort = async (ms, signal) => {
|
|
1284
|
+
if (signal?.aborted) throw abortError$2();
|
|
1285
|
+
if (ms <= 0) return;
|
|
1286
|
+
await new Promise((resolve, reject) => {
|
|
1287
|
+
const timer = setTimeout(() => {
|
|
1288
|
+
cleanup();
|
|
1289
|
+
resolve();
|
|
1290
|
+
}, ms);
|
|
1291
|
+
const onAbort = () => {
|
|
1292
|
+
cleanup();
|
|
1293
|
+
reject(abortError$2());
|
|
1294
|
+
};
|
|
1295
|
+
const cleanup = () => {
|
|
1296
|
+
clearTimeout(timer);
|
|
1297
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1298
|
+
};
|
|
1299
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1300
|
+
});
|
|
1301
|
+
};
|
|
1239
1302
|
function createAsyncHandlerDispatcher(maxConcurrency, timeoutMs, onError) {
|
|
1240
1303
|
let activeCount = 0;
|
|
1241
1304
|
let closed = false;
|
|
@@ -1303,7 +1366,7 @@ function createRequestGate(maxConcurrency, maxQueueSize) {
|
|
|
1303
1366
|
let closed = false;
|
|
1304
1367
|
const queue = [];
|
|
1305
1368
|
const acquire = async (signal) => {
|
|
1306
|
-
if (signal?.aborted) throw abortError();
|
|
1369
|
+
if (signal?.aborted) throw abortError$2();
|
|
1307
1370
|
if (closed) throw connectionClosedError();
|
|
1308
1371
|
return await new Promise((resolve, reject) => {
|
|
1309
1372
|
const grant = () => {
|
|
@@ -1326,7 +1389,7 @@ function createRequestGate(maxConcurrency, maxQueueSize) {
|
|
|
1326
1389
|
waiter.onAbort = () => {
|
|
1327
1390
|
removeWaiter(waiter);
|
|
1328
1391
|
cleanup();
|
|
1329
|
-
reject(abortError());
|
|
1392
|
+
reject(abortError$2());
|
|
1330
1393
|
};
|
|
1331
1394
|
if (signal) signal.addEventListener("abort", waiter.onAbort, { once: true });
|
|
1332
1395
|
if (activeCount < maxConcurrency) {
|
|
@@ -1375,7 +1438,7 @@ function createRequestGate(maxConcurrency, maxQueueSize) {
|
|
|
1375
1438
|
close
|
|
1376
1439
|
};
|
|
1377
1440
|
}
|
|
1378
|
-
function abortError() {
|
|
1441
|
+
function abortError$2() {
|
|
1379
1442
|
const error = /* @__PURE__ */ new Error("The operation was aborted");
|
|
1380
1443
|
error.name = "AbortError";
|
|
1381
1444
|
return error;
|
|
@@ -1383,12 +1446,37 @@ function abortError() {
|
|
|
1383
1446
|
function connectionClosedError() {
|
|
1384
1447
|
return new ConnectionError("Connection closed", { state: "CLOSED" });
|
|
1385
1448
|
}
|
|
1386
|
-
function throwIfAborted(signal) {
|
|
1387
|
-
if (signal?.aborted) throw abortError();
|
|
1449
|
+
function throwIfAborted$1(signal) {
|
|
1450
|
+
if (signal?.aborted) throw abortError$2();
|
|
1388
1451
|
}
|
|
1389
1452
|
function isAbortError$1(error) {
|
|
1390
1453
|
return error instanceof Error && error.name === "AbortError";
|
|
1391
1454
|
}
|
|
1455
|
+
const waitForSharedPromise$1 = async (promise, signal) => {
|
|
1456
|
+
if (!signal) return promise;
|
|
1457
|
+
if (signal.aborted) throw abortError$2();
|
|
1458
|
+
return await new Promise((resolve, reject) => {
|
|
1459
|
+
let settled = false;
|
|
1460
|
+
const cleanup = () => {
|
|
1461
|
+
signal.removeEventListener("abort", onAbort);
|
|
1462
|
+
};
|
|
1463
|
+
const settle = (callback) => {
|
|
1464
|
+
if (settled) return;
|
|
1465
|
+
settled = true;
|
|
1466
|
+
cleanup();
|
|
1467
|
+
callback();
|
|
1468
|
+
};
|
|
1469
|
+
const onAbort = () => {
|
|
1470
|
+
settle(() => reject(abortError$2()));
|
|
1471
|
+
};
|
|
1472
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1473
|
+
promise.then((value) => {
|
|
1474
|
+
settle(() => resolve(value));
|
|
1475
|
+
}, (error) => {
|
|
1476
|
+
settle(() => reject(error));
|
|
1477
|
+
});
|
|
1478
|
+
});
|
|
1479
|
+
};
|
|
1392
1480
|
function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
1393
1481
|
const timeout = options.timeout ?? 3e4;
|
|
1394
1482
|
const authSettleDelayMs = options.authSettleDelayMs ?? 100;
|
|
@@ -1396,6 +1484,13 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1396
1484
|
const reconnectMaxAttempts = options.reconnect?.maxAttempts ?? Infinity;
|
|
1397
1485
|
const reconnectBackoffMs = options.reconnect?.backoffMs ?? 250;
|
|
1398
1486
|
const reconnectMaxBackoffMs = options.reconnect?.maxBackoffMs ?? 5e3;
|
|
1487
|
+
const retryEnabled = options.retry?.enabled ?? true;
|
|
1488
|
+
const retryMaxAttempts = options.retry?.maxAttempts ?? 3;
|
|
1489
|
+
const retryBackoffMs = options.retry?.backoffMs ?? 100;
|
|
1490
|
+
const retryMaxBackoffMs = options.retry?.maxBackoffMs ?? 1e3;
|
|
1491
|
+
const heartbeatEnabled = options.heartbeat?.enabled ?? true;
|
|
1492
|
+
const heartbeatIntervalMs = options.heartbeat?.intervalMs ?? 1e4;
|
|
1493
|
+
const heartbeatTimeoutMs = options.heartbeat?.timeoutMs ?? 3e4;
|
|
1399
1494
|
const maxInFlightRequests = options.maxInFlightRequests ?? 256;
|
|
1400
1495
|
const maxRequestQueueSize = options.maxRequestQueueSize ?? 1024;
|
|
1401
1496
|
const observability = options.observability;
|
|
@@ -1409,10 +1504,23 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1409
1504
|
let receiveLoop = null;
|
|
1410
1505
|
let receiveLoopAbort = false;
|
|
1411
1506
|
let closeRequested = false;
|
|
1507
|
+
let permanentlyClosed = false;
|
|
1508
|
+
let connectPromise = null;
|
|
1412
1509
|
let reconnectPromise = null;
|
|
1510
|
+
let connectionLossPromise = null;
|
|
1511
|
+
let reconnectRestoreActive = false;
|
|
1413
1512
|
let authOutcome = null;
|
|
1414
1513
|
let authRejected = false;
|
|
1514
|
+
let hasEstablishedSession = false;
|
|
1515
|
+
let reconnectExhausted = false;
|
|
1516
|
+
let readyWaiterCount = 0;
|
|
1517
|
+
const closeAbortController = new AbortController();
|
|
1415
1518
|
const connectionScope = createScope("connection");
|
|
1519
|
+
const readyListeners = /* @__PURE__ */ new Set();
|
|
1520
|
+
let heartbeatTimer = null;
|
|
1521
|
+
let heartbeatTransport = null;
|
|
1522
|
+
let heartbeatPending = false;
|
|
1523
|
+
let lastActivityAt = Date.now();
|
|
1416
1524
|
const log = (level, event, fields) => {
|
|
1417
1525
|
observability?.logger?.log(level, event, fields);
|
|
1418
1526
|
};
|
|
@@ -1443,6 +1551,8 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1443
1551
|
};
|
|
1444
1552
|
const handlePossibleTransportFailure = (error) => {
|
|
1445
1553
|
if (closeRequested) return;
|
|
1554
|
+
if (state !== "AUTHENTICATED") return;
|
|
1555
|
+
if (getResilienceMeta(error)?.boundary === "pre-send") return;
|
|
1446
1556
|
if (error instanceof TransportError || error instanceof ConnectionError || error instanceof AuthenticationError) handleConnectionLoss(error);
|
|
1447
1557
|
};
|
|
1448
1558
|
const multiplexer = new Multiplexer({
|
|
@@ -1453,17 +1563,35 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1453
1563
|
log("warn", "fitz.connection.handler_failed", { error: describeError(error) });
|
|
1454
1564
|
});
|
|
1455
1565
|
const connect = async (options = {}) => {
|
|
1456
|
-
closeRequested
|
|
1566
|
+
if (permanentlyClosed || closeRequested) throw connectionClosedError();
|
|
1567
|
+
throwIfAborted$1(options.signal);
|
|
1568
|
+
if (state === "AUTHENTICATED") return;
|
|
1569
|
+
if (connectPromise) {
|
|
1570
|
+
await waitForSharedPromise$1(connectPromise, options.signal);
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
if (reconnectPromise || state === "CONNECTING" || state === "CONNECTED" || state === "AUTHENTICATING" || state === "RECONNECTING" || state === "DISCONNECTED" && canWaitForReconnect()) {
|
|
1574
|
+
await waitForReady(options.signal, timeout);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1457
1577
|
authRejected = false;
|
|
1458
|
-
|
|
1578
|
+
reconnectExhausted = false;
|
|
1579
|
+
const sharedConnectPromise = openAndAuthenticate(false, options.signal).finally(() => {
|
|
1580
|
+
if (connectPromise === sharedConnectPromise) connectPromise = null;
|
|
1581
|
+
});
|
|
1582
|
+
connectPromise = sharedConnectPromise;
|
|
1583
|
+
await sharedConnectPromise;
|
|
1459
1584
|
};
|
|
1460
1585
|
const close = async () => {
|
|
1461
1586
|
if (state === "CLOSED" && !transport) {
|
|
1462
1587
|
await connectionScope.dispose();
|
|
1463
1588
|
return;
|
|
1464
1589
|
}
|
|
1590
|
+
permanentlyClosed = true;
|
|
1465
1591
|
closeRequested = true;
|
|
1466
1592
|
receiveLoopAbort = true;
|
|
1593
|
+
stopHeartbeat();
|
|
1594
|
+
closeAbortController.abort();
|
|
1467
1595
|
asyncHandlerDispatcher.close();
|
|
1468
1596
|
const scopeDisposePromise = connectionScope.dispose();
|
|
1469
1597
|
setState("CLOSED");
|
|
@@ -1484,15 +1612,34 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1484
1612
|
await asyncHandlerDispatcher.drain();
|
|
1485
1613
|
await scopeDisposePromise;
|
|
1486
1614
|
};
|
|
1487
|
-
const
|
|
1615
|
+
const waitForRequestReady = async (signal, allowReconnectRestore = false) => {
|
|
1616
|
+
if (allowReconnectRestore && reconnectRestoreActive && state === "AUTHENTICATING" && !closeRequested && !authRejected && transport) return;
|
|
1617
|
+
const releaseReadyWaitSlot = acquireReadyWaitSlot();
|
|
1618
|
+
try {
|
|
1619
|
+
await waitForReady(signal, timeout);
|
|
1620
|
+
} finally {
|
|
1621
|
+
releaseReadyWaitSlot?.();
|
|
1622
|
+
}
|
|
1488
1623
|
ensureAuthenticated();
|
|
1624
|
+
};
|
|
1625
|
+
const requestInternal = async (messageType, requestPayload, signal, allowReconnectRestore = false) => {
|
|
1626
|
+
let sendStarted = false;
|
|
1627
|
+
await waitForRequestReady(signal, allowReconnectRestore);
|
|
1489
1628
|
const releaseRequestSlot = await requestGate.acquire(signal);
|
|
1490
1629
|
const startedAt = Date.now();
|
|
1491
1630
|
try {
|
|
1492
1631
|
const activeTransport = ensureTransport();
|
|
1493
1632
|
const frame = FrameCodec.encodeFrame(messageType, requestPayload);
|
|
1494
|
-
return await multiplexer.request(messageType, frame, (data) =>
|
|
1633
|
+
return await multiplexer.request(messageType, frame, (data) => {
|
|
1634
|
+
sendStarted = true;
|
|
1635
|
+
return sendSerialized(activeTransport, data);
|
|
1636
|
+
}, timeout, signal);
|
|
1495
1637
|
} catch (error) {
|
|
1638
|
+
attachResilienceMeta(error, {
|
|
1639
|
+
boundary: sendStarted ? "post-send" : "pre-send",
|
|
1640
|
+
failureKind: classifyFailureKind(error),
|
|
1641
|
+
explicitNegative: false
|
|
1642
|
+
});
|
|
1496
1643
|
log("error", "fitz.connection.request_failed", {
|
|
1497
1644
|
operation: "request",
|
|
1498
1645
|
state,
|
|
@@ -1507,13 +1654,34 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1507
1654
|
releaseRequestSlot();
|
|
1508
1655
|
}
|
|
1509
1656
|
};
|
|
1510
|
-
const
|
|
1657
|
+
const request = async (messageType, requestPayload, signal) => {
|
|
1658
|
+
return await requestInternal(messageType, requestPayload, signal);
|
|
1659
|
+
};
|
|
1660
|
+
const requestDuringReconnectRestore = async (messageType, requestPayload, signal) => {
|
|
1661
|
+
return await requestInternal(messageType, requestPayload, signal, true);
|
|
1662
|
+
};
|
|
1663
|
+
const send = async (messageType, requestPayload, signal) => {
|
|
1664
|
+
let sendStarted = false;
|
|
1665
|
+
const releaseReadyWaitSlot = acquireReadyWaitSlot();
|
|
1666
|
+
try {
|
|
1667
|
+
await waitForReady(signal, timeout);
|
|
1668
|
+
} finally {
|
|
1669
|
+
releaseReadyWaitSlot?.();
|
|
1670
|
+
}
|
|
1511
1671
|
ensureAuthenticated();
|
|
1512
|
-
const releaseRequestSlot = await requestGate.acquire();
|
|
1672
|
+
const releaseRequestSlot = await requestGate.acquire(signal);
|
|
1513
1673
|
const startedAt = Date.now();
|
|
1514
1674
|
try {
|
|
1515
|
-
|
|
1675
|
+
const activeTransport = ensureTransport();
|
|
1676
|
+
const frame = FrameCodec.encodeFrame(messageType, requestPayload);
|
|
1677
|
+
sendStarted = true;
|
|
1678
|
+
await sendSerialized(activeTransport, frame);
|
|
1516
1679
|
} catch (error) {
|
|
1680
|
+
attachResilienceMeta(error, {
|
|
1681
|
+
boundary: sendStarted ? "post-send" : "pre-send",
|
|
1682
|
+
failureKind: classifyFailureKind(error),
|
|
1683
|
+
explicitNegative: false
|
|
1684
|
+
});
|
|
1517
1685
|
log("error", "fitz.connection.send_failed", {
|
|
1518
1686
|
operation: "send",
|
|
1519
1687
|
state,
|
|
@@ -1528,8 +1696,8 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1528
1696
|
releaseRequestSlot();
|
|
1529
1697
|
}
|
|
1530
1698
|
};
|
|
1531
|
-
const sendFireAndForget = async (messageType, requestPayload) => {
|
|
1532
|
-
await send(messageType, requestPayload);
|
|
1699
|
+
const sendFireAndForget = async (messageType, requestPayload, signal) => {
|
|
1700
|
+
await send(messageType, requestPayload, signal);
|
|
1533
1701
|
};
|
|
1534
1702
|
const registerNotificationHandler = (messageType, handler) => {
|
|
1535
1703
|
multiplexer.registerNotificationHandler(messageType, handler);
|
|
@@ -1557,22 +1725,224 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1557
1725
|
const getState = () => state;
|
|
1558
1726
|
const isConnected = () => state === "AUTHENTICATED";
|
|
1559
1727
|
const getUrl = () => ensureTransport().getUrl();
|
|
1728
|
+
const canWaitForReconnect = () => {
|
|
1729
|
+
return reconnectEnabled && hasEstablishedSession && !reconnectExhausted && !authRejected;
|
|
1730
|
+
};
|
|
1731
|
+
const readyFailure = () => {
|
|
1732
|
+
if (state === "AUTHENTICATED") return null;
|
|
1733
|
+
if (closeRequested || state === "CLOSED") return connectionClosedError();
|
|
1734
|
+
if (authRejected) return new AuthenticationError("Authentication rejected", { state });
|
|
1735
|
+
if (state === "CONNECTING" || state === "CONNECTED" || state === "AUTHENTICATING" || state === "RECONNECTING") return null;
|
|
1736
|
+
if (state === "DISCONNECTED" && canWaitForReconnect()) return null;
|
|
1737
|
+
return new ConnectionError(`Cannot use connection while state is ${state}`, { state });
|
|
1738
|
+
};
|
|
1739
|
+
const notifyReadyListeners = () => {
|
|
1740
|
+
for (const listener of readyListeners) listener();
|
|
1741
|
+
};
|
|
1742
|
+
const acquireReadyWaitSlot = () => {
|
|
1743
|
+
const failure = readyFailure();
|
|
1744
|
+
if (state === "AUTHENTICATED" || failure) return null;
|
|
1745
|
+
if (readyWaiterCount >= maxRequestQueueSize) throw new RequestQueueFullError();
|
|
1746
|
+
readyWaiterCount += 1;
|
|
1747
|
+
let released = false;
|
|
1748
|
+
return () => {
|
|
1749
|
+
if (released) return;
|
|
1750
|
+
released = true;
|
|
1751
|
+
readyWaiterCount = Math.max(readyWaiterCount - 1, 0);
|
|
1752
|
+
};
|
|
1753
|
+
};
|
|
1754
|
+
const waitForReady = async (signal, waitTimeoutMs = timeout) => {
|
|
1755
|
+
throwIfAborted$1(signal);
|
|
1756
|
+
const immediateFailure = readyFailure();
|
|
1757
|
+
if (!immediateFailure) {
|
|
1758
|
+
if (state === "AUTHENTICATED") return;
|
|
1759
|
+
} else throw immediateFailure;
|
|
1760
|
+
await new Promise((resolve, reject) => {
|
|
1761
|
+
let settled = false;
|
|
1762
|
+
let timeoutId = null;
|
|
1763
|
+
const cleanup = () => {
|
|
1764
|
+
readyListeners.delete(onStateChange);
|
|
1765
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
1766
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1767
|
+
};
|
|
1768
|
+
const settle = (cb) => {
|
|
1769
|
+
if (settled) return;
|
|
1770
|
+
settled = true;
|
|
1771
|
+
cleanup();
|
|
1772
|
+
cb();
|
|
1773
|
+
};
|
|
1774
|
+
const onAbort = () => {
|
|
1775
|
+
settle(() => reject(abortError$2()));
|
|
1776
|
+
};
|
|
1777
|
+
const onStateChange = () => {
|
|
1778
|
+
const failure = readyFailure();
|
|
1779
|
+
if (state === "AUTHENTICATED") {
|
|
1780
|
+
settle(resolve);
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
if (failure) settle(() => reject(failure));
|
|
1784
|
+
};
|
|
1785
|
+
readyListeners.add(onStateChange);
|
|
1786
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1787
|
+
timeoutId = setTimeout(() => {
|
|
1788
|
+
settle(() => reject(new ConnectionError("Timed out waiting for connection to become ready", { state })));
|
|
1789
|
+
}, waitTimeoutMs);
|
|
1790
|
+
onStateChange();
|
|
1791
|
+
});
|
|
1792
|
+
};
|
|
1793
|
+
const waitUntilReady = async (signal, waitTimeoutMs = timeout) => {
|
|
1794
|
+
await waitForReady(signal, waitTimeoutMs);
|
|
1795
|
+
};
|
|
1796
|
+
const shouldWaitForReconnect = () => {
|
|
1797
|
+
return reconnectPromise !== null || state === "RECONNECTING" || state === "DISCONNECTED" && canWaitForReconnect();
|
|
1798
|
+
};
|
|
1799
|
+
const getRetryDelayMs = (baseDelayMs) => {
|
|
1800
|
+
const jitter = Math.floor(Math.random() * baseDelayMs * .5);
|
|
1801
|
+
return Math.min(Math.max(baseDelayMs + jitter, 1), retryMaxBackoffMs);
|
|
1802
|
+
};
|
|
1803
|
+
const recordRetry = (operation, attempt, delayMs, error) => {
|
|
1804
|
+
const meta = getResilienceMeta(error);
|
|
1805
|
+
log("warn", "fitz.request.retry", {
|
|
1806
|
+
domain: operation.domain,
|
|
1807
|
+
operation: operation.operation,
|
|
1808
|
+
attempt,
|
|
1809
|
+
delayMs,
|
|
1810
|
+
boundary: meta?.boundary ?? "unknown",
|
|
1811
|
+
error: describeError(error),
|
|
1812
|
+
...describeErrorFields(error)
|
|
1813
|
+
});
|
|
1814
|
+
observability?.meter?.counter("fitz.request.retry", 1, {
|
|
1815
|
+
domain: operation.domain,
|
|
1816
|
+
operation: operation.operation,
|
|
1817
|
+
boundary: meta?.boundary ?? "unknown"
|
|
1818
|
+
});
|
|
1819
|
+
};
|
|
1820
|
+
const recordRetryExhausted = (operation, attempt, error) => {
|
|
1821
|
+
const meta = getResilienceMeta(error);
|
|
1822
|
+
log("warn", "fitz.request.retry_exhausted", {
|
|
1823
|
+
domain: operation.domain,
|
|
1824
|
+
operation: operation.operation,
|
|
1825
|
+
attempt,
|
|
1826
|
+
boundary: meta?.boundary ?? "unknown",
|
|
1827
|
+
error: describeError(error),
|
|
1828
|
+
...describeErrorFields(error)
|
|
1829
|
+
});
|
|
1830
|
+
observability?.meter?.counter("fitz.request.retry_exhausted", 1, {
|
|
1831
|
+
domain: operation.domain,
|
|
1832
|
+
operation: operation.operation,
|
|
1833
|
+
boundary: meta?.boundary ?? "unknown"
|
|
1834
|
+
});
|
|
1835
|
+
};
|
|
1836
|
+
const executeWithRetry = async (operation, task) => {
|
|
1837
|
+
if (!retryEnabled || operation.retryClass === "wait_only") return task();
|
|
1838
|
+
let attempt = 0;
|
|
1839
|
+
let delayMs = retryBackoffMs;
|
|
1840
|
+
while (true) {
|
|
1841
|
+
attempt += 1;
|
|
1842
|
+
try {
|
|
1843
|
+
return await task();
|
|
1844
|
+
} catch (error) {
|
|
1845
|
+
if (isAbortError$1(error)) throw error;
|
|
1846
|
+
if (!shouldRetryOperation(operation.retryClass, error)) throw error;
|
|
1847
|
+
if (attempt >= retryMaxAttempts) {
|
|
1848
|
+
recordRetryExhausted(operation, attempt, error);
|
|
1849
|
+
throw error;
|
|
1850
|
+
}
|
|
1851
|
+
const actualDelayMs = getRetryDelayMs(delayMs);
|
|
1852
|
+
recordRetry(operation, attempt, actualDelayMs, error);
|
|
1853
|
+
await sleepWithAbort(actualDelayMs, operation.signal);
|
|
1854
|
+
delayMs = Math.min(delayMs * 2, retryMaxBackoffMs);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
const withWriteLock = async (operation) => {
|
|
1859
|
+
const prior = writeChain;
|
|
1860
|
+
let release;
|
|
1861
|
+
writeChain = new Promise((resolve) => {
|
|
1862
|
+
release = resolve;
|
|
1863
|
+
});
|
|
1864
|
+
await prior;
|
|
1865
|
+
try {
|
|
1866
|
+
return await operation();
|
|
1867
|
+
} finally {
|
|
1868
|
+
release();
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
const markOutboundActivity = () => {
|
|
1872
|
+
lastActivityAt = Date.now();
|
|
1873
|
+
};
|
|
1874
|
+
const markRemoteActivity = () => {
|
|
1875
|
+
lastActivityAt = Date.now();
|
|
1876
|
+
};
|
|
1877
|
+
const stopHeartbeat = () => {
|
|
1878
|
+
if (heartbeatTimer) {
|
|
1879
|
+
clearTimeout(heartbeatTimer);
|
|
1880
|
+
heartbeatTimer = null;
|
|
1881
|
+
}
|
|
1882
|
+
heartbeatTransport = null;
|
|
1883
|
+
heartbeatPending = false;
|
|
1884
|
+
};
|
|
1885
|
+
const startHeartbeat = (activeTransport) => {
|
|
1886
|
+
if (!heartbeatEnabled) return;
|
|
1887
|
+
stopHeartbeat();
|
|
1888
|
+
activeTransport.enableKeepAlive?.(heartbeatIntervalMs);
|
|
1889
|
+
heartbeatTransport = activeTransport;
|
|
1890
|
+
markRemoteActivity();
|
|
1891
|
+
const scheduleNext = () => {
|
|
1892
|
+
if (closeRequested || receiveLoopAbort || heartbeatTransport !== activeTransport) return;
|
|
1893
|
+
heartbeatTimer = setTimeout(tick, heartbeatIntervalMs);
|
|
1894
|
+
};
|
|
1895
|
+
const tick = () => {
|
|
1896
|
+
heartbeatTimer = null;
|
|
1897
|
+
if (closeRequested || receiveLoopAbort || heartbeatTransport !== activeTransport) return;
|
|
1898
|
+
const now = Date.now();
|
|
1899
|
+
if (now - lastActivityAt < heartbeatIntervalMs) {
|
|
1900
|
+
scheduleNext();
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
const supportsHeartbeat = activeTransport.supportsHeartbeat?.() ?? true;
|
|
1904
|
+
if (!heartbeatPending && supportsHeartbeat && activeTransport.sendHeartbeat) {
|
|
1905
|
+
heartbeatPending = true;
|
|
1906
|
+
const heartbeatSentAt = now;
|
|
1907
|
+
const dispatchHeartbeat = (heartbeat) => activeTransport.sendHeartbeat(heartbeat);
|
|
1908
|
+
withWriteLock(async () => {
|
|
1909
|
+
await dispatchHeartbeat({ timeoutMs: heartbeatTimeoutMs });
|
|
1910
|
+
}).then(() => {
|
|
1911
|
+
if (heartbeatTransport !== activeTransport) return;
|
|
1912
|
+
heartbeatPending = false;
|
|
1913
|
+
markRemoteActivity();
|
|
1914
|
+
}).catch((error) => {
|
|
1915
|
+
if (heartbeatTransport !== activeTransport) return;
|
|
1916
|
+
heartbeatPending = false;
|
|
1917
|
+
if (lastActivityAt > heartbeatSentAt) return;
|
|
1918
|
+
const heartbeatError = new TransportError(`Heartbeat failed: ${describeError(error)}`);
|
|
1919
|
+
activeTransport.close().catch(() => void 0);
|
|
1920
|
+
handleConnectionLoss(heartbeatError);
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
scheduleNext();
|
|
1924
|
+
};
|
|
1925
|
+
scheduleNext();
|
|
1926
|
+
};
|
|
1560
1927
|
const openAndAuthenticate = async (isReconnect, signal) => {
|
|
1561
|
-
throwIfAborted(signal);
|
|
1928
|
+
throwIfAborted$1(signal);
|
|
1562
1929
|
receiveLoopAbort = false;
|
|
1563
1930
|
frameParser.parseFrames(new Uint8Array(0));
|
|
1564
1931
|
requestGate = createRequestGate(maxInFlightRequests, maxRequestQueueSize);
|
|
1565
1932
|
const activeTransport = transportFactory();
|
|
1566
1933
|
transport = activeTransport;
|
|
1934
|
+
stopHeartbeat();
|
|
1567
1935
|
setState(isReconnect ? "RECONNECTING" : "CONNECTING");
|
|
1568
1936
|
emitLifecycleEvent(isReconnect ? "reconnect_start" : "connect_start");
|
|
1569
1937
|
await activeTransport.connect();
|
|
1938
|
+
markRemoteActivity();
|
|
1570
1939
|
if (closeRequested) {
|
|
1940
|
+
stopHeartbeat();
|
|
1571
1941
|
await activeTransport.close().catch(() => void 0);
|
|
1572
1942
|
if (transport === activeTransport) transport = null;
|
|
1573
1943
|
throw connectionClosedError();
|
|
1574
1944
|
}
|
|
1575
|
-
throwIfAborted(signal);
|
|
1945
|
+
throwIfAborted$1(signal);
|
|
1576
1946
|
receiveLoop = startReceiveLoop();
|
|
1577
1947
|
setState("CONNECTED");
|
|
1578
1948
|
setState("AUTHENTICATING");
|
|
@@ -1581,23 +1951,34 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1581
1951
|
try {
|
|
1582
1952
|
await sendConnect();
|
|
1583
1953
|
if (closeRequested) throw connectionClosedError();
|
|
1584
|
-
throwIfAborted(signal);
|
|
1954
|
+
throwIfAborted$1(signal);
|
|
1585
1955
|
await Promise.race([authOutcome.promise, sleep(authSettleDelayMs)]);
|
|
1586
1956
|
if (closeRequested) throw connectionClosedError();
|
|
1587
|
-
throwIfAborted(signal);
|
|
1957
|
+
throwIfAborted$1(signal);
|
|
1588
1958
|
authOutcome?.resolve();
|
|
1589
1959
|
authOutcome = null;
|
|
1590
1960
|
if (isReconnect) {
|
|
1591
|
-
|
|
1592
|
-
|
|
1961
|
+
multiplexer.setConnected();
|
|
1962
|
+
reconnectRestoreActive = true;
|
|
1963
|
+
try {
|
|
1964
|
+
await restoreReconnectState();
|
|
1965
|
+
if (closeRequested) throw connectionClosedError();
|
|
1966
|
+
} finally {
|
|
1967
|
+
reconnectRestoreActive = false;
|
|
1968
|
+
}
|
|
1593
1969
|
}
|
|
1970
|
+
hasEstablishedSession = true;
|
|
1971
|
+
reconnectExhausted = false;
|
|
1594
1972
|
setState("AUTHENTICATED");
|
|
1595
|
-
|
|
1973
|
+
startHeartbeat(activeTransport);
|
|
1974
|
+
if (!isReconnect) multiplexer.setConnected();
|
|
1596
1975
|
emitLifecycleEvent(isReconnect ? "reconnect_succeeded" : "connect_succeeded");
|
|
1597
1976
|
} catch (error) {
|
|
1598
1977
|
authOutcome = null;
|
|
1978
|
+
reconnectRestoreActive = false;
|
|
1599
1979
|
multiplexer.setDisconnected();
|
|
1600
1980
|
emitDisconnect();
|
|
1981
|
+
stopHeartbeat();
|
|
1601
1982
|
if (activeTransport) await activeTransport.close().catch(() => void 0);
|
|
1602
1983
|
if (transport === activeTransport) transport = null;
|
|
1603
1984
|
const rejectedAuth = error instanceof AuthenticationError;
|
|
@@ -1605,7 +1986,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1605
1986
|
if (closeRequested) setState("CLOSED");
|
|
1606
1987
|
else setState(rejectedAuth ? "CLOSED" : "DISCONNECTED");
|
|
1607
1988
|
emitLifecycleEvent(isReconnect ? "reconnect_failed" : "connect_failed", error);
|
|
1608
|
-
if (isAbortError$1(error)) throw abortError();
|
|
1989
|
+
if (isAbortError$1(error)) throw abortError$2();
|
|
1609
1990
|
throw error;
|
|
1610
1991
|
}
|
|
1611
1992
|
};
|
|
@@ -1613,10 +1994,12 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1613
1994
|
const token = await tokenProvider();
|
|
1614
1995
|
const frame = FrameCodec.encodeFrame(1, utf8Encoder.encode(token));
|
|
1615
1996
|
await ensureTransport().send(frame);
|
|
1997
|
+
markOutboundActivity();
|
|
1616
1998
|
};
|
|
1617
1999
|
const startReceiveLoop = async () => {
|
|
1618
2000
|
while (!receiveLoopAbort && !closeRequested) try {
|
|
1619
2001
|
const data = await ensureTransport().receive();
|
|
2002
|
+
markRemoteActivity();
|
|
1620
2003
|
const frames = frameParser.parseFrames(data);
|
|
1621
2004
|
for (const frame of frames) multiplexer.dispatch(frame.messageType, frame.payload);
|
|
1622
2005
|
} catch (error) {
|
|
@@ -1626,6 +2009,17 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1626
2009
|
}
|
|
1627
2010
|
};
|
|
1628
2011
|
const handleConnectionLoss = async (error) => {
|
|
2012
|
+
if (connectionLossPromise) {
|
|
2013
|
+
await connectionLossPromise;
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
connectionLossPromise = handleConnectionLossOnce(error).finally(() => {
|
|
2017
|
+
connectionLossPromise = null;
|
|
2018
|
+
});
|
|
2019
|
+
await connectionLossPromise;
|
|
2020
|
+
};
|
|
2021
|
+
const handleConnectionLossOnce = async (error) => {
|
|
2022
|
+
stopHeartbeat();
|
|
1629
2023
|
multiplexer.setDisconnected();
|
|
1630
2024
|
requestGate.close();
|
|
1631
2025
|
emitDisconnect();
|
|
@@ -1646,6 +2040,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1646
2040
|
emitLifecycleEvent("auth_rejected", error);
|
|
1647
2041
|
return;
|
|
1648
2042
|
}
|
|
2043
|
+
reconnectExhausted = false;
|
|
1649
2044
|
setState("DISCONNECTED");
|
|
1650
2045
|
emitLifecycleEvent("connection_lost", error);
|
|
1651
2046
|
if (!reconnectEnabled) return;
|
|
@@ -1667,12 +2062,13 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1667
2062
|
emitLifecycleEvent("reconnect_scheduled", void 0, attempts);
|
|
1668
2063
|
const actualDelayMs = getReconnectDelayMs(delayMs);
|
|
1669
2064
|
try {
|
|
1670
|
-
await
|
|
2065
|
+
await sleepWithAbort(actualDelayMs, closeAbortController.signal);
|
|
1671
2066
|
if (closeRequested) return;
|
|
1672
2067
|
await openAndAuthenticate(true);
|
|
1673
2068
|
return;
|
|
1674
2069
|
} catch (error) {
|
|
1675
2070
|
if (closeRequested) return;
|
|
2071
|
+
if (isAbortError$1(error)) return;
|
|
1676
2072
|
log("warn", "fitz.connection.reconnect_retry", {
|
|
1677
2073
|
attempts,
|
|
1678
2074
|
delayMs: actualDelayMs,
|
|
@@ -1686,6 +2082,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1686
2082
|
setState("CLOSED");
|
|
1687
2083
|
return;
|
|
1688
2084
|
}
|
|
2085
|
+
reconnectExhausted = true;
|
|
1689
2086
|
setState("DISCONNECTED");
|
|
1690
2087
|
emitLifecycleEvent("reconnect_exhausted", void 0, attempts);
|
|
1691
2088
|
};
|
|
@@ -1706,19 +2103,13 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1706
2103
|
};
|
|
1707
2104
|
const setState = (newState) => {
|
|
1708
2105
|
state = newState;
|
|
2106
|
+
notifyReadyListeners();
|
|
1709
2107
|
};
|
|
1710
2108
|
const sendSerialized = async (transport, data) => {
|
|
1711
|
-
|
|
1712
|
-
let release;
|
|
1713
|
-
writeChain = new Promise((resolve) => {
|
|
1714
|
-
release = resolve;
|
|
1715
|
-
});
|
|
1716
|
-
await prior;
|
|
1717
|
-
try {
|
|
2109
|
+
await withWriteLock(async () => {
|
|
1718
2110
|
await transport.send(data);
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
}
|
|
2111
|
+
markOutboundActivity();
|
|
2112
|
+
});
|
|
1722
2113
|
};
|
|
1723
2114
|
const emitLifecycleEvent = (event, error, attempt) => {
|
|
1724
2115
|
const payload = {
|
|
@@ -1745,6 +2136,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1745
2136
|
connect,
|
|
1746
2137
|
close,
|
|
1747
2138
|
request,
|
|
2139
|
+
requestDuringReconnectRestore,
|
|
1748
2140
|
send,
|
|
1749
2141
|
sendFireAndForget,
|
|
1750
2142
|
registerNotificationHandler,
|
|
@@ -1753,6 +2145,9 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1753
2145
|
onDisconnect,
|
|
1754
2146
|
getMultiplexer,
|
|
1755
2147
|
dispatchAsyncHandler,
|
|
2148
|
+
executeWithRetry,
|
|
2149
|
+
waitUntilReady,
|
|
2150
|
+
shouldWaitForReconnect,
|
|
1756
2151
|
getScope,
|
|
1757
2152
|
getState,
|
|
1758
2153
|
isConnected,
|
|
@@ -1790,6 +2185,7 @@ function createWebSocketTransport(url, options = {}) {
|
|
|
1790
2185
|
let receiverResolve = null;
|
|
1791
2186
|
const timeout = options.timeout ?? 3e4;
|
|
1792
2187
|
const maxFrameSize = options.maxFrameSize ?? 65535;
|
|
2188
|
+
const receiveTimeoutEnabled = options.receiveTimeout ?? true;
|
|
1793
2189
|
const enqueueMessage = (data) => {
|
|
1794
2190
|
if (receiverResolve) {
|
|
1795
2191
|
receiverResolve(data);
|
|
@@ -1863,6 +2259,56 @@ function createWebSocketTransport(url, options = {}) {
|
|
|
1863
2259
|
}
|
|
1864
2260
|
});
|
|
1865
2261
|
};
|
|
2262
|
+
const sendHeartbeat = async (heartbeatOptions) => {
|
|
2263
|
+
if (!connected || !ws) throw new TransportError("WebSocket is not connected");
|
|
2264
|
+
const activeWs = ws;
|
|
2265
|
+
if (typeof activeWs.ping !== "function" || typeof activeWs.once !== "function" || typeof activeWs.removeListener !== "function") throw new TransportError("WebSocket heartbeat is not supported");
|
|
2266
|
+
const socket = activeWs;
|
|
2267
|
+
return new Promise((resolve, reject) => {
|
|
2268
|
+
let settled = false;
|
|
2269
|
+
let timeoutId = null;
|
|
2270
|
+
const cleanup = () => {
|
|
2271
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
2272
|
+
socket.removeListener("pong", onPong);
|
|
2273
|
+
socket.removeListener("close", onClose);
|
|
2274
|
+
socket.removeListener("error", onError);
|
|
2275
|
+
};
|
|
2276
|
+
const settle = (callback) => {
|
|
2277
|
+
if (settled) return;
|
|
2278
|
+
settled = true;
|
|
2279
|
+
cleanup();
|
|
2280
|
+
callback();
|
|
2281
|
+
};
|
|
2282
|
+
const onPong = () => {
|
|
2283
|
+
settle(resolve);
|
|
2284
|
+
};
|
|
2285
|
+
const onClose = () => {
|
|
2286
|
+
settle(() => reject(new TransportError("WebSocket closed during heartbeat")));
|
|
2287
|
+
};
|
|
2288
|
+
const onError = (...args) => {
|
|
2289
|
+
const event = args[0];
|
|
2290
|
+
const message = event instanceof Error ? event.message : event?.message || "unknown error";
|
|
2291
|
+
settle(() => reject(new TransportError(`WebSocket heartbeat failed: ${message}`)));
|
|
2292
|
+
};
|
|
2293
|
+
timeoutId = setTimeout(() => {
|
|
2294
|
+
settle(() => reject(new TimeoutError(`WebSocket heartbeat timeout after ${heartbeatOptions.timeoutMs}ms`)));
|
|
2295
|
+
}, heartbeatOptions.timeoutMs);
|
|
2296
|
+
socket.once("pong", onPong);
|
|
2297
|
+
socket.once("close", onClose);
|
|
2298
|
+
socket.once("error", onError);
|
|
2299
|
+
try {
|
|
2300
|
+
socket.ping(new Uint8Array(), void 0, (err) => {
|
|
2301
|
+
if (err) settle(() => reject(new TransportError(`WebSocket ping failed: ${err.message}`)));
|
|
2302
|
+
});
|
|
2303
|
+
} catch (err) {
|
|
2304
|
+
settle(() => reject(new TransportError(`WebSocket heartbeat error: ${err instanceof Error ? err.message : String(err)}`)));
|
|
2305
|
+
}
|
|
2306
|
+
});
|
|
2307
|
+
};
|
|
2308
|
+
const supportsHeartbeat = () => {
|
|
2309
|
+
return typeof ws?.ping === "function" && typeof ws?.once === "function" && typeof ws?.removeListener === "function";
|
|
2310
|
+
};
|
|
2311
|
+
const enableKeepAlive = () => void 0;
|
|
1866
2312
|
const receive = async () => {
|
|
1867
2313
|
if (receiveQueue.length > 0) {
|
|
1868
2314
|
const message = receiveQueue.shift();
|
|
@@ -1871,12 +2317,12 @@ function createWebSocketTransport(url, options = {}) {
|
|
|
1871
2317
|
}
|
|
1872
2318
|
if (!connected) throw new TransportError("Connection closed");
|
|
1873
2319
|
return new Promise((resolve, reject) => {
|
|
1874
|
-
const timeoutId = setTimeout(() => {
|
|
2320
|
+
const timeoutId = receiveTimeoutEnabled ? setTimeout(() => {
|
|
1875
2321
|
receiverResolve = null;
|
|
1876
2322
|
reject(new TimeoutError(`WebSocket receive timeout after ${timeout}ms`));
|
|
1877
|
-
}, timeout);
|
|
2323
|
+
}, timeout) : null;
|
|
1878
2324
|
receiverResolve = (data) => {
|
|
1879
|
-
clearTimeout(timeoutId);
|
|
2325
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
1880
2326
|
receiverResolve = null;
|
|
1881
2327
|
if (data === null) {
|
|
1882
2328
|
reject(new TransportError("Connection closed"));
|
|
@@ -1911,6 +2357,9 @@ function createWebSocketTransport(url, options = {}) {
|
|
|
1911
2357
|
connect,
|
|
1912
2358
|
send,
|
|
1913
2359
|
receive,
|
|
2360
|
+
sendHeartbeat,
|
|
2361
|
+
supportsHeartbeat,
|
|
2362
|
+
enableKeepAlive,
|
|
1914
2363
|
close,
|
|
1915
2364
|
getUrl,
|
|
1916
2365
|
isConnected
|
|
@@ -1936,6 +2385,7 @@ function createTcpTransport(url, options = {}) {
|
|
|
1936
2385
|
let receiverResolve = null;
|
|
1937
2386
|
const timeout = options.timeout ?? 3e4;
|
|
1938
2387
|
const maxFrameSize = options.maxFrameSize ?? 65535;
|
|
2388
|
+
const receiveTimeoutEnabled = options.receiveTimeout ?? true;
|
|
1939
2389
|
let lengthBuffer = new Uint8Array(4);
|
|
1940
2390
|
let lengthOffset = 0;
|
|
1941
2391
|
let currentMessageLength = null;
|
|
@@ -2010,7 +2460,7 @@ function createTcpTransport(url, options = {}) {
|
|
|
2010
2460
|
clearTimeout(connectTimeout);
|
|
2011
2461
|
connected = true;
|
|
2012
2462
|
activeSocket.setNoDelay(true);
|
|
2013
|
-
activeSocket.setTimeout(timeout);
|
|
2463
|
+
activeSocket.setTimeout(receiveTimeoutEnabled ? timeout : 0);
|
|
2014
2464
|
resolve();
|
|
2015
2465
|
});
|
|
2016
2466
|
activeSocket.on("data", (chunk) => {
|
|
@@ -2028,8 +2478,10 @@ function createTcpTransport(url, options = {}) {
|
|
|
2028
2478
|
if (receiverResolve) receiverResolve(new Uint8Array(0));
|
|
2029
2479
|
});
|
|
2030
2480
|
activeSocket.on("timeout", () => {
|
|
2031
|
-
|
|
2032
|
-
|
|
2481
|
+
if (receiveTimeoutEnabled) {
|
|
2482
|
+
activeSocket.destroy();
|
|
2483
|
+
connected = false;
|
|
2484
|
+
}
|
|
2033
2485
|
});
|
|
2034
2486
|
} catch (err) {
|
|
2035
2487
|
reject(new TransportError(`Failed to create TCP socket: ${err instanceof Error ? err.message : String(err)}`));
|
|
@@ -2054,6 +2506,10 @@ function createTcpTransport(url, options = {}) {
|
|
|
2054
2506
|
});
|
|
2055
2507
|
});
|
|
2056
2508
|
};
|
|
2509
|
+
const enableKeepAlive = (intervalMs) => {
|
|
2510
|
+
socket?.setKeepAlive(true, intervalMs);
|
|
2511
|
+
};
|
|
2512
|
+
const supportsHeartbeat = () => false;
|
|
2057
2513
|
const receive = async () => {
|
|
2058
2514
|
if (receiveQueue.length > 0) {
|
|
2059
2515
|
const message = receiveQueue.shift();
|
|
@@ -2061,12 +2517,12 @@ function createTcpTransport(url, options = {}) {
|
|
|
2061
2517
|
return message;
|
|
2062
2518
|
}
|
|
2063
2519
|
return new Promise((resolve, reject) => {
|
|
2064
|
-
const timeoutId = setTimeout(() => {
|
|
2520
|
+
const timeoutId = receiveTimeoutEnabled ? setTimeout(() => {
|
|
2065
2521
|
receiverResolve = null;
|
|
2066
2522
|
reject(new TimeoutError(`TCP receive timeout after ${timeout}ms`));
|
|
2067
|
-
}, timeout);
|
|
2523
|
+
}, timeout) : null;
|
|
2068
2524
|
receiverResolve = (data) => {
|
|
2069
|
-
clearTimeout(timeoutId);
|
|
2525
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
2070
2526
|
receiverResolve = null;
|
|
2071
2527
|
if (data.length === 0) reject(new TransportError("Connection closed"));
|
|
2072
2528
|
else resolve(data);
|
|
@@ -2098,6 +2554,8 @@ function createTcpTransport(url, options = {}) {
|
|
|
2098
2554
|
connect,
|
|
2099
2555
|
send,
|
|
2100
2556
|
receive,
|
|
2557
|
+
supportsHeartbeat,
|
|
2558
|
+
enableKeepAlive,
|
|
2101
2559
|
close,
|
|
2102
2560
|
getUrl,
|
|
2103
2561
|
isConnected
|
|
@@ -2136,11 +2594,23 @@ function createTransport(url, transportType = "auto", options = {}) {
|
|
|
2136
2594
|
//#region src/domains/base.ts
|
|
2137
2595
|
function createDomainClient(connection) {
|
|
2138
2596
|
const requestFrame = async (messageType, payload, signal) => connection.request(messageType, payload, signal);
|
|
2597
|
+
const requestReconnectFrame = async (messageType, payload, signal) => {
|
|
2598
|
+
const resilientConnection = connection;
|
|
2599
|
+
if (typeof resilientConnection.requestDuringReconnectRestore === "function") return await resilientConnection.requestDuringReconnectRestore(messageType, payload, signal);
|
|
2600
|
+
return await connection.request(messageType, payload, signal);
|
|
2601
|
+
};
|
|
2139
2602
|
const sendFrame = async (messageType, payload) => connection.send(messageType, payload);
|
|
2603
|
+
const runWithRetry = async (operation, task) => {
|
|
2604
|
+
const resilientConnection = connection;
|
|
2605
|
+
if (typeof resilientConnection.executeWithRetry === "function") return resilientConnection.executeWithRetry(operation, task);
|
|
2606
|
+
return task();
|
|
2607
|
+
};
|
|
2140
2608
|
return {
|
|
2141
2609
|
connection,
|
|
2142
2610
|
requestFrame,
|
|
2143
|
-
|
|
2611
|
+
requestReconnectFrame,
|
|
2612
|
+
sendFrame,
|
|
2613
|
+
runWithRetry
|
|
2144
2614
|
};
|
|
2145
2615
|
}
|
|
2146
2616
|
//#endregion
|
|
@@ -2318,8 +2788,11 @@ function createAsyncIterableIterator(iterator) {
|
|
|
2318
2788
|
//#region src/domains/kv/transaction.ts
|
|
2319
2789
|
function createKvTransaction(connection, route, txId) {
|
|
2320
2790
|
let closed = false;
|
|
2321
|
-
const
|
|
2791
|
+
const resilientConnection = connection;
|
|
2792
|
+
let unsubscribeDisconnect = () => void 0;
|
|
2793
|
+
unsubscribeDisconnect = connection.onDisconnect(() => {
|
|
2322
2794
|
closed = true;
|
|
2795
|
+
unsubscribeDisconnect();
|
|
2323
2796
|
});
|
|
2324
2797
|
const ensureOpen = () => {
|
|
2325
2798
|
if (closed) throw new KvError("Transaction already closed", "TX_CLOSED");
|
|
@@ -2334,6 +2807,10 @@ function createKvTransaction(connection, route, txId) {
|
|
|
2334
2807
|
[5]: "OperationNotAllowed"
|
|
2335
2808
|
}[status] ?? `Unknown(${status})`}`, operation, status);
|
|
2336
2809
|
};
|
|
2810
|
+
const runWithRetry = async (operation, task) => {
|
|
2811
|
+
if (typeof resilientConnection.executeWithRetry === "function") return resilientConnection.executeWithRetry(operation, task);
|
|
2812
|
+
return task();
|
|
2813
|
+
};
|
|
2337
2814
|
const put = async (key, value, signal) => {
|
|
2338
2815
|
ensureOpen();
|
|
2339
2816
|
const payload = KvCodec.encodePut(txId, route, key, value);
|
|
@@ -2348,15 +2825,22 @@ function createKvTransaction(connection, route, txId) {
|
|
|
2348
2825
|
};
|
|
2349
2826
|
const get = async (key, signal) => {
|
|
2350
2827
|
ensureOpen();
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2828
|
+
return runWithRetry({
|
|
2829
|
+
domain: "kv",
|
|
2830
|
+
operation: "get",
|
|
2831
|
+
retryClass: "replayable_read",
|
|
2832
|
+
signal
|
|
2833
|
+
}, async () => {
|
|
2834
|
+
const payload = KvCodec.encodeGet(txId, route, key);
|
|
2835
|
+
const response = await connection.request(103, payload, signal);
|
|
2836
|
+
const decoded = KvCodec.decodeGetResponse(response);
|
|
2837
|
+
checkStatus(decoded.status, "GET");
|
|
2838
|
+
if (!decoded.found || !decoded.value) return { type: "not-found" };
|
|
2839
|
+
return {
|
|
2840
|
+
type: "found",
|
|
2841
|
+
value: decoded.value
|
|
2842
|
+
};
|
|
2843
|
+
});
|
|
2360
2844
|
};
|
|
2361
2845
|
const deleteItem = async (key, signal) => {
|
|
2362
2846
|
ensureOpen();
|
|
@@ -2372,11 +2856,18 @@ function createKvTransaction(connection, route, txId) {
|
|
|
2372
2856
|
};
|
|
2373
2857
|
const scan = async (options = {}, signal) => {
|
|
2374
2858
|
ensureOpen();
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2859
|
+
return runWithRetry({
|
|
2860
|
+
domain: "kv",
|
|
2861
|
+
operation: "scan",
|
|
2862
|
+
retryClass: "replayable_read",
|
|
2863
|
+
signal
|
|
2864
|
+
}, async () => {
|
|
2865
|
+
const payload = KvCodec.encodeScan(txId, route, options);
|
|
2866
|
+
const response = await connection.request(108, payload, signal);
|
|
2867
|
+
const decoded = KvCodec.decodeScanResponse(response);
|
|
2868
|
+
checkStatus(decoded.status, "SCAN");
|
|
2869
|
+
return createAsyncIterableIterator(createSliceIterator(decoded.keys));
|
|
2870
|
+
});
|
|
2380
2871
|
};
|
|
2381
2872
|
const commit = async (signal) => {
|
|
2382
2873
|
ensureOpen();
|
|
@@ -2443,6 +2934,64 @@ const KvClient = function(connection) {
|
|
|
2443
2934
|
return createKvClient(connection);
|
|
2444
2935
|
};
|
|
2445
2936
|
//#endregion
|
|
2937
|
+
//#region src/core/wake-gate.ts
|
|
2938
|
+
function abortError$1() {
|
|
2939
|
+
const error = /* @__PURE__ */ new Error("The operation was aborted");
|
|
2940
|
+
error.name = "AbortError";
|
|
2941
|
+
return error;
|
|
2942
|
+
}
|
|
2943
|
+
function createWakeGate() {
|
|
2944
|
+
let currentVersion = 0;
|
|
2945
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
2946
|
+
const cleanup = (waiter) => {
|
|
2947
|
+
waiters.delete(waiter);
|
|
2948
|
+
if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
2949
|
+
};
|
|
2950
|
+
const wake = () => {
|
|
2951
|
+
currentVersion += 1;
|
|
2952
|
+
const version = currentVersion;
|
|
2953
|
+
for (const waiter of Array.from(waiters)) {
|
|
2954
|
+
cleanup(waiter);
|
|
2955
|
+
waiter.resolve(version);
|
|
2956
|
+
}
|
|
2957
|
+
return version;
|
|
2958
|
+
};
|
|
2959
|
+
const waitAfter = async (observedVersion, options = {}) => {
|
|
2960
|
+
if (currentVersion > observedVersion) return currentVersion;
|
|
2961
|
+
if (options.signal?.aborted) throw abortError$1();
|
|
2962
|
+
return await new Promise((resolve, reject) => {
|
|
2963
|
+
const waiter = {
|
|
2964
|
+
observedVersion,
|
|
2965
|
+
resolve,
|
|
2966
|
+
reject,
|
|
2967
|
+
signal: options.signal
|
|
2968
|
+
};
|
|
2969
|
+
waiter.onAbort = () => {
|
|
2970
|
+
cleanup(waiter);
|
|
2971
|
+
reject(abortError$1());
|
|
2972
|
+
};
|
|
2973
|
+
if (options.signal) options.signal.addEventListener("abort", waiter.onAbort, { once: true });
|
|
2974
|
+
if (currentVersion > observedVersion) {
|
|
2975
|
+
cleanup(waiter);
|
|
2976
|
+
resolve(currentVersion);
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
waiters.add(waiter);
|
|
2980
|
+
});
|
|
2981
|
+
};
|
|
2982
|
+
const wait = async (options) => {
|
|
2983
|
+
return await waitAfter(currentVersion, options);
|
|
2984
|
+
};
|
|
2985
|
+
return {
|
|
2986
|
+
get version() {
|
|
2987
|
+
return currentVersion;
|
|
2988
|
+
},
|
|
2989
|
+
wake,
|
|
2990
|
+
waitAfter,
|
|
2991
|
+
wait
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2994
|
+
//#endregion
|
|
2446
2995
|
//#region src/domains/queue/codec.ts
|
|
2447
2996
|
/**
|
|
2448
2997
|
* Queue domain codec for encoding and decoding protocol messages.
|
|
@@ -2656,7 +3205,17 @@ const QueueCodec = {
|
|
|
2656
3205
|
//#endregion
|
|
2657
3206
|
//#region src/domains/queue/types.ts
|
|
2658
3207
|
function createQueueItem(id, token, body, route, connection) {
|
|
3208
|
+
let closed = false;
|
|
3209
|
+
let unsubscribeDisconnect = () => void 0;
|
|
3210
|
+
unsubscribeDisconnect = connection.onDisconnect(() => {
|
|
3211
|
+
closed = true;
|
|
3212
|
+
unsubscribeDisconnect();
|
|
3213
|
+
});
|
|
3214
|
+
const ensureOpen = () => {
|
|
3215
|
+
if (closed) throw new QueueError("Queue item is no longer valid after disconnect", "ITEM_CLOSED");
|
|
3216
|
+
};
|
|
2659
3217
|
const extend = async (leaseSecs, signal) => {
|
|
3218
|
+
ensureOpen();
|
|
2660
3219
|
const payload = QueueCodec.encodeExtend(route, id, token, leaseSecs);
|
|
2661
3220
|
const response = await connection.request(203, payload, signal);
|
|
2662
3221
|
const decoded = QueueCodec.decodeExtendResponse(response);
|
|
@@ -2667,6 +3226,7 @@ function createQueueItem(id, token, body, route, connection) {
|
|
|
2667
3226
|
}
|
|
2668
3227
|
};
|
|
2669
3228
|
const complete = async (signal) => {
|
|
3229
|
+
ensureOpen();
|
|
2670
3230
|
const requestPayload = QueueCodec.encodeComplete(route, id, token);
|
|
2671
3231
|
const response = await connection.request(204, requestPayload, signal);
|
|
2672
3232
|
const decoded = QueueCodec.decodeCompleteResponse(response);
|
|
@@ -2675,9 +3235,12 @@ function createQueueItem(id, token, body, route, connection) {
|
|
|
2675
3235
|
const statusName = QueueStatus[errorCode] || `Unknown(${errorCode})`;
|
|
2676
3236
|
throw new QueueError(`COMPLETE failed: ${decoded.errorMessage ?? statusName}`, statusName, errorCode);
|
|
2677
3237
|
}
|
|
3238
|
+
closed = true;
|
|
3239
|
+
unsubscribeDisconnect();
|
|
2678
3240
|
};
|
|
2679
3241
|
const testOnlyInvalidToken = () => id + 1n;
|
|
2680
3242
|
const testOnlyCompleteWithToken = async (tokenToUse, signal) => {
|
|
3243
|
+
ensureOpen();
|
|
2681
3244
|
const requestPayload = QueueCodec.encodeComplete(route, id, tokenToUse);
|
|
2682
3245
|
const response = await connection.request(204, requestPayload, signal);
|
|
2683
3246
|
const decoded = QueueCodec.decodeCompleteResponse(response);
|
|
@@ -2686,6 +3249,8 @@ function createQueueItem(id, token, body, route, connection) {
|
|
|
2686
3249
|
const statusName = QueueStatus[errorCode] || `Unknown(${errorCode})`;
|
|
2687
3250
|
throw new QueueError(`COMPLETE failed: ${decoded.errorMessage ?? statusName}`, statusName, errorCode);
|
|
2688
3251
|
}
|
|
3252
|
+
closed = true;
|
|
3253
|
+
unsubscribeDisconnect();
|
|
2689
3254
|
};
|
|
2690
3255
|
return {
|
|
2691
3256
|
body,
|
|
@@ -2723,7 +3288,7 @@ let QueueStatus = /* @__PURE__ */ function(QueueStatus) {
|
|
|
2723
3288
|
* Queue domain client.
|
|
2724
3289
|
*/
|
|
2725
3290
|
function createQueueClient(connection) {
|
|
2726
|
-
const { requestFrame } = createDomainClient(connection);
|
|
3291
|
+
const { requestFrame, requestReconnectFrame, runWithRetry } = createDomainClient(connection);
|
|
2727
3292
|
const subscriptionsByPattern = /* @__PURE__ */ new Map();
|
|
2728
3293
|
const patternsBySubId = /* @__PURE__ */ new Map();
|
|
2729
3294
|
let notificationHandlerRegistered = false;
|
|
@@ -2737,7 +3302,7 @@ function createQueueClient(connection) {
|
|
|
2737
3302
|
subscriptionsByPattern.clear();
|
|
2738
3303
|
patternsBySubId.clear();
|
|
2739
3304
|
for (const subscription of subscriptions) {
|
|
2740
|
-
const subId = await subscribeWire(subscription.pattern);
|
|
3305
|
+
const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
|
|
2741
3306
|
subscriptionsByPattern.set(subscription.pattern, {
|
|
2742
3307
|
subId,
|
|
2743
3308
|
handlers: new Map(subscription.handlers)
|
|
@@ -2747,11 +3312,17 @@ function createQueueClient(connection) {
|
|
|
2747
3312
|
});
|
|
2748
3313
|
const enqueue = async (route, body, options) => {
|
|
2749
3314
|
assertQueueRoute(route);
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
3315
|
+
return runWithRetry({
|
|
3316
|
+
domain: "queue",
|
|
3317
|
+
operation: "enqueue",
|
|
3318
|
+
retryClass: "confirmed_negative_retry"
|
|
3319
|
+
}, async () => {
|
|
3320
|
+
const response = await requestFrame(200, QueueCodec.encodeEnqueue(route, body, options));
|
|
3321
|
+
const decoded = QueueCodec.decodeEnqueueResponse(response);
|
|
3322
|
+
checkStatus(decoded, "ENQUEUE");
|
|
3323
|
+
if (decoded.messageId === void 0) throw new QueueError("ENQUEUE response missing messageId", "MISSING_MESSAGE_ID");
|
|
3324
|
+
return decoded.messageId;
|
|
3325
|
+
});
|
|
2755
3326
|
};
|
|
2756
3327
|
const reserve = async (route, leaseSeconds, batchSize = 1, waitSeconds = 0) => {
|
|
2757
3328
|
assertQueueReserveRoute(route);
|
|
@@ -2759,43 +3330,55 @@ function createQueueClient(connection) {
|
|
|
2759
3330
|
let items = await reserveOnce(route, leaseSeconds, batchSize);
|
|
2760
3331
|
if (items.length > 0) return items;
|
|
2761
3332
|
const deadline = Date.now() + waitSeconds * 1e3;
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
pendingNotifications += 1;
|
|
2766
|
-
if (!waiter) return;
|
|
2767
|
-
const resolve = waiter;
|
|
2768
|
-
waiter = void 0;
|
|
2769
|
-
pendingNotifications = 0;
|
|
2770
|
-
resolve();
|
|
3333
|
+
const wakeGate = createWakeGate();
|
|
3334
|
+
const subscription = await subscribe(route, () => {
|
|
3335
|
+
wakeGate.wake();
|
|
2771
3336
|
});
|
|
2772
3337
|
try {
|
|
2773
3338
|
while (true) {
|
|
3339
|
+
const observed = wakeGate.version;
|
|
2774
3340
|
items = await reserveOnce(route, leaseSeconds, batchSize);
|
|
2775
3341
|
if (items.length > 0) return items;
|
|
2776
3342
|
const remainingMs = deadline - Date.now();
|
|
2777
3343
|
if (remainingMs <= 0) return items;
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
}
|
|
2784
|
-
const release = () => {
|
|
2785
|
-
clearTimeout(timeoutId);
|
|
2786
|
-
if (waiter === release) waiter = void 0;
|
|
2787
|
-
resolve();
|
|
2788
|
-
};
|
|
2789
|
-
const timeoutId = setTimeout(release, remainingMs);
|
|
2790
|
-
waiter = release;
|
|
3344
|
+
const waitPromise = wakeGate.waitAfter(observed);
|
|
3345
|
+
let timeoutId = null;
|
|
3346
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
3347
|
+
timeoutId = setTimeout(() => {
|
|
3348
|
+
resolve("timeout");
|
|
3349
|
+
}, remainingMs);
|
|
2791
3350
|
});
|
|
3351
|
+
if (await Promise.race([waitPromise.then(() => {
|
|
3352
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
3353
|
+
return "wake";
|
|
3354
|
+
}), timeoutPromise]) === "timeout") return items;
|
|
2792
3355
|
}
|
|
2793
3356
|
} finally {
|
|
2794
|
-
await subscription.unsubscribe();
|
|
3357
|
+
await subscription.unsubscribe().catch(() => void 0);
|
|
2795
3358
|
}
|
|
2796
3359
|
};
|
|
2797
|
-
const
|
|
2798
|
-
|
|
3360
|
+
const reserveWhenAvailable = async function* (route, options) {
|
|
3361
|
+
assertQueueReserveRoute(route);
|
|
3362
|
+
const wakeGate = createWakeGate();
|
|
3363
|
+
const subscription = await subscribe(route, () => {
|
|
3364
|
+
wakeGate.wake();
|
|
3365
|
+
});
|
|
3366
|
+
try {
|
|
3367
|
+
while (true) {
|
|
3368
|
+
const observed = wakeGate.version;
|
|
3369
|
+
const items = await reserveOnce(route, options.leaseSeconds, options.batchSize ?? 1, options.signal);
|
|
3370
|
+
if (items.length > 0) {
|
|
3371
|
+
yield items;
|
|
3372
|
+
continue;
|
|
3373
|
+
}
|
|
3374
|
+
await wakeGate.waitAfter(observed, { signal: options.signal });
|
|
3375
|
+
}
|
|
3376
|
+
} finally {
|
|
3377
|
+
await subscription.unsubscribe().catch(() => void 0);
|
|
3378
|
+
}
|
|
3379
|
+
};
|
|
3380
|
+
const reserveOnce = async (route, leaseSeconds, batchSize, signal) => {
|
|
3381
|
+
const response = await requestFrame(202, QueueCodec.encodeReserve(route, leaseSeconds, batchSize), signal);
|
|
2799
3382
|
const decoded = QueueCodec.decodeReserveResponse(response);
|
|
2800
3383
|
checkStatus(decoded, "RESERVE");
|
|
2801
3384
|
return (decoded.items ?? []).map((item) => createQueueItem(item.id, item.token, item.body, route, connection));
|
|
@@ -2807,8 +3390,8 @@ function createQueueClient(connection) {
|
|
|
2807
3390
|
if (existing) return addLocalSubscription(pattern, existing.subId, handler);
|
|
2808
3391
|
return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
|
|
2809
3392
|
};
|
|
2810
|
-
const subscribeWire = async (pattern) => {
|
|
2811
|
-
const response = await
|
|
3393
|
+
const subscribeWire = async (pattern, request = requestFrame) => {
|
|
3394
|
+
const response = await request(207, QueueCodec.encodeSubscribe(wireWatchPattern(pattern)));
|
|
2812
3395
|
const decoded = QueueCodec.decodeSubscribeResponse(response);
|
|
2813
3396
|
checkStatus(decoded, "SUBSCRIBE");
|
|
2814
3397
|
if (decoded.subId === void 0) throw new QueueError("SUBSCRIBE response missing subId", "MISSING_SUB_ID");
|
|
@@ -2875,11 +3458,16 @@ function createQueueClient(connection) {
|
|
|
2875
3458
|
[4]: "QueueFull",
|
|
2876
3459
|
[5]: "InvalidDelay"
|
|
2877
3460
|
}[errorCode] ?? `Unknown(${errorCode})`;
|
|
2878
|
-
throw new QueueError(`${operation} failed: ${response.errorMessage ?? statusName}`, statusName, errorCode)
|
|
3461
|
+
throw attachResilienceMeta(new QueueError(`${operation} failed: ${response.errorMessage ?? statusName}`, statusName, errorCode), {
|
|
3462
|
+
boundary: "post-send",
|
|
3463
|
+
failureKind: "domain",
|
|
3464
|
+
explicitNegative: true
|
|
3465
|
+
});
|
|
2879
3466
|
};
|
|
2880
3467
|
return {
|
|
2881
3468
|
enqueue,
|
|
2882
3469
|
reserve,
|
|
3470
|
+
reserveWhenAvailable,
|
|
2883
3471
|
subscribe
|
|
2884
3472
|
};
|
|
2885
3473
|
}
|
|
@@ -3190,16 +3778,35 @@ function createRpcSubscription(route, unsubscribeFn) {
|
|
|
3190
3778
|
*/
|
|
3191
3779
|
function createRpcResponseWriter(connection, correlationId) {
|
|
3192
3780
|
let sequence = 0n;
|
|
3781
|
+
let stale = false;
|
|
3782
|
+
let unsubscribeDisconnect = () => void 0;
|
|
3783
|
+
const dispose = () => {
|
|
3784
|
+
if (stale) return;
|
|
3785
|
+
stale = true;
|
|
3786
|
+
unsubscribeDisconnect();
|
|
3787
|
+
unsubscribeDisconnect = () => void 0;
|
|
3788
|
+
};
|
|
3789
|
+
unsubscribeDisconnect = connection.onDisconnect(() => {
|
|
3790
|
+
dispose();
|
|
3791
|
+
});
|
|
3193
3792
|
const send = async (body, isEnd) => {
|
|
3793
|
+
if (stale) throw new ConnectionError("RPC response writer is no longer valid");
|
|
3194
3794
|
const payload = RpcCodec.encodeResponse(correlationId, sequence++, body, isEnd);
|
|
3195
3795
|
try {
|
|
3196
3796
|
await connection.send(303, payload);
|
|
3797
|
+
if (isEnd) dispose();
|
|
3197
3798
|
} catch (error) {
|
|
3198
|
-
if (isBenignShutdownError(error, connection))
|
|
3799
|
+
if (isBenignShutdownError(error, connection)) {
|
|
3800
|
+
dispose();
|
|
3801
|
+
return;
|
|
3802
|
+
}
|
|
3199
3803
|
throw error;
|
|
3200
3804
|
}
|
|
3201
3805
|
};
|
|
3202
|
-
return {
|
|
3806
|
+
return {
|
|
3807
|
+
send,
|
|
3808
|
+
dispose
|
|
3809
|
+
};
|
|
3203
3810
|
}
|
|
3204
3811
|
function isBenignShutdownError(error, connection) {
|
|
3205
3812
|
if (connection.getState() !== "AUTHENTICATED") return true;
|
|
@@ -3347,7 +3954,7 @@ const RpcClient = function(connection) {
|
|
|
3347
3954
|
return createRpcClient(connection);
|
|
3348
3955
|
};
|
|
3349
3956
|
function createRpcClient(connection) {
|
|
3350
|
-
const { requestFrame } = createDomainClient(connection);
|
|
3957
|
+
const { requestFrame, requestReconnectFrame } = createDomainClient(connection);
|
|
3351
3958
|
const pendingRpcs = /* @__PURE__ */ new Map();
|
|
3352
3959
|
const workers = /* @__PURE__ */ new Map();
|
|
3353
3960
|
let initialized = false;
|
|
@@ -3365,7 +3972,7 @@ function createRpcClient(connection) {
|
|
|
3365
3972
|
if (workers.size === 0) return;
|
|
3366
3973
|
const registeredWorkers = Array.from(workers.entries());
|
|
3367
3974
|
workers.clear();
|
|
3368
|
-
for (const [route, handler] of registeredWorkers) await
|
|
3975
|
+
for (const [route, handler] of registeredWorkers) await registerWorkerInternal(route, handler, requestReconnectFrame);
|
|
3369
3976
|
});
|
|
3370
3977
|
const call = async (route, body, options) => {
|
|
3371
3978
|
assertRpcRoute(route);
|
|
@@ -3391,13 +3998,16 @@ function createRpcClient(connection) {
|
|
|
3391
3998
|
throw error;
|
|
3392
3999
|
}
|
|
3393
4000
|
};
|
|
3394
|
-
const
|
|
3395
|
-
|
|
3396
|
-
initRpcHandler();
|
|
3397
|
-
const response = await requestFrame(300, RpcCodec.encodeSubscribeWorker(route));
|
|
4001
|
+
const registerWorkerInternal = async (route, handler, request = requestFrame) => {
|
|
4002
|
+
const response = await request(300, RpcCodec.encodeSubscribeWorker(route));
|
|
3398
4003
|
const decoded = RpcCodec.decodeSubscribeWorkerResponse(response);
|
|
3399
4004
|
if (decoded.status !== 0) throw new RpcError(`RPC SUBSCRIBE_WORKER failed: status ${decoded.status}`, "SUBSCRIBE_FAILED", decoded.status);
|
|
3400
4005
|
workers.set(route, handler);
|
|
4006
|
+
};
|
|
4007
|
+
const registerWorker = async (route, handler) => {
|
|
4008
|
+
assertRpcRoute(route);
|
|
4009
|
+
initRpcHandler();
|
|
4010
|
+
await registerWorkerInternal(route, handler);
|
|
3401
4011
|
const unsubscribeFn = async (registeredRoute) => {
|
|
3402
4012
|
await unregisterWorker(registeredRoute);
|
|
3403
4013
|
};
|
|
@@ -3466,6 +4076,8 @@ function createRpcClient(connection) {
|
|
|
3466
4076
|
try {
|
|
3467
4077
|
await writer.send(utf8Encoder.encode(`Handler error: ${message}`), true);
|
|
3468
4078
|
} catch {}
|
|
4079
|
+
} finally {
|
|
4080
|
+
writer.dispose();
|
|
3469
4081
|
}
|
|
3470
4082
|
});
|
|
3471
4083
|
};
|
|
@@ -3701,7 +4313,17 @@ function createLeaseSubscription(subId, pattern, unsubscribeFn) {
|
|
|
3701
4313
|
function createLease(token, expiresAt, route, connection) {
|
|
3702
4314
|
let currentToken = token;
|
|
3703
4315
|
let currentExpiry = expiresAt;
|
|
4316
|
+
let closed = false;
|
|
4317
|
+
let unsubscribeDisconnect = () => void 0;
|
|
4318
|
+
unsubscribeDisconnect = connection.onDisconnect(() => {
|
|
4319
|
+
closed = true;
|
|
4320
|
+
unsubscribeDisconnect();
|
|
4321
|
+
});
|
|
4322
|
+
const ensureOpen = () => {
|
|
4323
|
+
if (closed) throw new LeaseError("Lease handle is no longer valid after disconnect", "CLOSED");
|
|
4324
|
+
};
|
|
3704
4325
|
const extend = async (ttlSecs, signal) => {
|
|
4326
|
+
ensureOpen();
|
|
3705
4327
|
const requestPayload = LeaseCodec.encodeExtend(route, currentToken, ttlSecs);
|
|
3706
4328
|
const data = assertSuccess(await connection.request(401, requestPayload, signal), "EXTEND");
|
|
3707
4329
|
if (data && data.length >= 8) currentToken = new BufferReader(data).readU64BE();
|
|
@@ -3709,12 +4331,16 @@ function createLease(token, expiresAt, route, connection) {
|
|
|
3709
4331
|
return currentExpiry;
|
|
3710
4332
|
};
|
|
3711
4333
|
const release = async (signal) => {
|
|
4334
|
+
ensureOpen();
|
|
3712
4335
|
const payload = LeaseCodec.encodeRelease(route, currentToken);
|
|
3713
4336
|
assertSuccess(await connection.request(402, payload, signal), "RELEASE");
|
|
4337
|
+
closed = true;
|
|
4338
|
+
unsubscribeDisconnect();
|
|
3714
4339
|
};
|
|
3715
4340
|
const getExpiry = () => currentExpiry;
|
|
3716
4341
|
const testOnlyInvalidToken = () => currentToken + 1n;
|
|
3717
4342
|
const testOnlyExtendWithToken = async (tokenToUse, ttlSecs, signal) => {
|
|
4343
|
+
ensureOpen();
|
|
3718
4344
|
const requestPayload = LeaseCodec.encodeExtend(route, tokenToUse, ttlSecs);
|
|
3719
4345
|
const data = assertSuccess(await connection.request(401, requestPayload, signal), "EXTEND");
|
|
3720
4346
|
if (data && data.length >= 8) currentToken = new BufferReader(data).readU64BE();
|
|
@@ -3722,8 +4348,11 @@ function createLease(token, expiresAt, route, connection) {
|
|
|
3722
4348
|
return currentExpiry;
|
|
3723
4349
|
};
|
|
3724
4350
|
const testOnlyReleaseWithToken = async (tokenToUse, signal) => {
|
|
4351
|
+
ensureOpen();
|
|
3725
4352
|
const payload = LeaseCodec.encodeRelease(route, tokenToUse);
|
|
3726
4353
|
assertSuccess(await connection.request(402, payload, signal), "RELEASE");
|
|
4354
|
+
closed = true;
|
|
4355
|
+
unsubscribeDisconnect();
|
|
3727
4356
|
};
|
|
3728
4357
|
return {
|
|
3729
4358
|
extend,
|
|
@@ -3740,7 +4369,7 @@ function createLease(token, expiresAt, route, connection) {
|
|
|
3740
4369
|
* Lease domain client.
|
|
3741
4370
|
*/
|
|
3742
4371
|
function createLeaseClient(connection) {
|
|
3743
|
-
const { requestFrame } = createDomainClient(connection);
|
|
4372
|
+
const { requestFrame, requestReconnectFrame, runWithRetry } = createDomainClient(connection);
|
|
3744
4373
|
const subscriptionsByPattern = /* @__PURE__ */ new Map();
|
|
3745
4374
|
let initialized = false;
|
|
3746
4375
|
let nextHandlerId = 1;
|
|
@@ -3752,7 +4381,7 @@ function createLeaseClient(connection) {
|
|
|
3752
4381
|
}));
|
|
3753
4382
|
subscriptionsByPattern.clear();
|
|
3754
4383
|
for (const subscription of subscriptions) {
|
|
3755
|
-
const subId = await subscribeWire(subscription.pattern);
|
|
4384
|
+
const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
|
|
3756
4385
|
subscriptionsByPattern.set(subscription.pattern, {
|
|
3757
4386
|
subId,
|
|
3758
4387
|
handlers: new Map(subscription.handlers)
|
|
@@ -3769,16 +4398,22 @@ function createLeaseClient(connection) {
|
|
|
3769
4398
|
};
|
|
3770
4399
|
const query = async (route) => {
|
|
3771
4400
|
assertExactLeaseRoute(route);
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
4401
|
+
return runWithRetry({
|
|
4402
|
+
domain: "lease",
|
|
4403
|
+
operation: "query",
|
|
4404
|
+
retryClass: "replayable_read"
|
|
4405
|
+
}, async () => {
|
|
4406
|
+
const response = await requestFrame(403, LeaseCodec.encodeQuery(route));
|
|
4407
|
+
const decoded = LeaseCodec.decodeQueryResponse(response);
|
|
4408
|
+
if (decoded.status !== 0) throw new LeaseError("QUERY failed", "QUERY_FAILED", decoded.status);
|
|
4409
|
+
return {
|
|
4410
|
+
isHeld: decoded.isHeld ?? false,
|
|
4411
|
+
owner: decoded.owner,
|
|
4412
|
+
token: decoded.token,
|
|
4413
|
+
ttlRemainingSecs: decoded.ttlRemainingSecs,
|
|
4414
|
+
expiresAt: decoded.expiresAt
|
|
4415
|
+
};
|
|
4416
|
+
});
|
|
3782
4417
|
};
|
|
3783
4418
|
const subscribe = async (pattern, handler) => {
|
|
3784
4419
|
assertExactLeaseRoute(pattern);
|
|
@@ -3787,8 +4422,8 @@ function createLeaseClient(connection) {
|
|
|
3787
4422
|
if (existing) return addLocalSubscription(pattern, existing.subId, handler);
|
|
3788
4423
|
return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
|
|
3789
4424
|
};
|
|
3790
|
-
const subscribeWire = async (pattern) => {
|
|
3791
|
-
const response = await
|
|
4425
|
+
const subscribeWire = async (pattern, request = requestFrame) => {
|
|
4426
|
+
const response = await request(407, LeaseCodec.encodeSubscribe(pattern));
|
|
3792
4427
|
const decoded = LeaseCodec.decodeSubscribeResponse(response);
|
|
3793
4428
|
if (decoded.subId === void 0) throw new LeaseError("SUBSCRIBE failed", "SUBSCRIBE_FAILED");
|
|
3794
4429
|
return decoded.subId;
|
|
@@ -3935,7 +4570,7 @@ function createNoticeSubscription(subId, pattern, unsubscribeFn) {
|
|
|
3935
4570
|
* Notice domain client.
|
|
3936
4571
|
*/
|
|
3937
4572
|
function createNoticeClient(connection) {
|
|
3938
|
-
const { requestFrame } = createDomainClient(connection);
|
|
4573
|
+
const { requestFrame, requestReconnectFrame } = createDomainClient(connection);
|
|
3939
4574
|
const subscriptionsByPattern = /* @__PURE__ */ new Map();
|
|
3940
4575
|
const patternsBySubId = /* @__PURE__ */ new Map();
|
|
3941
4576
|
let initialized = false;
|
|
@@ -3949,7 +4584,7 @@ function createNoticeClient(connection) {
|
|
|
3949
4584
|
subscriptionsByPattern.clear();
|
|
3950
4585
|
patternsBySubId.clear();
|
|
3951
4586
|
for (const subscription of subscriptions) {
|
|
3952
|
-
const subId = await subscribeWire(subscription.pattern);
|
|
4587
|
+
const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
|
|
3953
4588
|
subscriptionsByPattern.set(subscription.pattern, {
|
|
3954
4589
|
subId,
|
|
3955
4590
|
handlers: new Map(subscription.handlers)
|
|
@@ -3975,8 +4610,8 @@ function createNoticeClient(connection) {
|
|
|
3975
4610
|
if (existing) return addLocalSubscription(pattern, existing.subId, handler);
|
|
3976
4611
|
return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
|
|
3977
4612
|
};
|
|
3978
|
-
const subscribeWire = async (pattern) => {
|
|
3979
|
-
const response = await
|
|
4613
|
+
const subscribeWire = async (pattern, request = requestFrame) => {
|
|
4614
|
+
const response = await request(501, NoticeCodec.encodeSubscribe(pattern));
|
|
3980
4615
|
const decoded = NoticeCodec.decodeSubscribeResponse(response);
|
|
3981
4616
|
if (decoded.subId === void 0) throw new NoticeError("SUBSCRIBE response missing subId", "MISSING_SUB_ID");
|
|
3982
4617
|
return decoded.subId;
|
|
@@ -4418,8 +5053,10 @@ function createStreamSubscription(subId, pattern, unsubscribeFn) {
|
|
|
4418
5053
|
//#region src/domains/stream/session.ts
|
|
4419
5054
|
function createStreamSession(connection, _route, sessionId) {
|
|
4420
5055
|
let closed = false;
|
|
4421
|
-
|
|
5056
|
+
let unsubscribeDisconnect = () => void 0;
|
|
5057
|
+
unsubscribeDisconnect = connection.onDisconnect(() => {
|
|
4422
5058
|
closed = true;
|
|
5059
|
+
unsubscribeDisconnect();
|
|
4423
5060
|
});
|
|
4424
5061
|
const ensureOpen = () => {
|
|
4425
5062
|
if (closed) throw new StreamError("Stream session already closed", "SESSION_CLOSED");
|
|
@@ -4493,7 +5130,7 @@ function isAbortSignal(value) {
|
|
|
4493
5130
|
* 3. `commit()` or `rollback()` finalizes the session
|
|
4494
5131
|
*/
|
|
4495
5132
|
function createStreamClient(connection) {
|
|
4496
|
-
const { requestFrame } = createDomainClient(connection);
|
|
5133
|
+
const { requestFrame, requestReconnectFrame, runWithRetry } = createDomainClient(connection);
|
|
4497
5134
|
const subscriptionsByPattern = /* @__PURE__ */ new Map();
|
|
4498
5135
|
const patternsBySubId = /* @__PURE__ */ new Map();
|
|
4499
5136
|
let initialized = false;
|
|
@@ -4507,7 +5144,7 @@ function createStreamClient(connection) {
|
|
|
4507
5144
|
subscriptionsByPattern.clear();
|
|
4508
5145
|
patternsBySubId.clear();
|
|
4509
5146
|
for (const subscription of snapshot) {
|
|
4510
|
-
const subId = await subscribeWire(subscription.pattern);
|
|
5147
|
+
const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
|
|
4511
5148
|
subscriptionsByPattern.set(subscription.pattern, {
|
|
4512
5149
|
subId,
|
|
4513
5150
|
handlers: new Map(subscription.handlers)
|
|
@@ -4525,43 +5162,89 @@ function createStreamClient(connection) {
|
|
|
4525
5162
|
};
|
|
4526
5163
|
const readPage = async (route, startOffset, limit = 100, options) => {
|
|
4527
5164
|
assertStreamPattern(route);
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
5165
|
+
return runWithRetry({
|
|
5166
|
+
domain: "stream",
|
|
5167
|
+
operation: "read",
|
|
5168
|
+
retryClass: "replayable_read",
|
|
5169
|
+
signal: options?.signal
|
|
5170
|
+
}, async () => {
|
|
5171
|
+
const response = await requestFrame(604, StreamCodec.encodeRead(route, startOffset, limit, options), options?.signal);
|
|
5172
|
+
const decoded = StreamCodec.decodeReadResponse(response);
|
|
5173
|
+
checkStatus(decoded.status, "READ");
|
|
5174
|
+
return {
|
|
5175
|
+
items: decoded.items,
|
|
5176
|
+
cursor: decoded.cursor ?? {
|
|
5177
|
+
lastResourceOffset: startOffset,
|
|
5178
|
+
lastAreaOffset: void 0,
|
|
5179
|
+
lastRealmOffset: void 0,
|
|
5180
|
+
hasMore: false
|
|
5181
|
+
}
|
|
5182
|
+
};
|
|
5183
|
+
});
|
|
4540
5184
|
};
|
|
4541
5185
|
const read = async (route, startOffset, limit = 100, options) => {
|
|
4542
5186
|
const page = await readPage(route, startOffset, limit, options);
|
|
4543
5187
|
return StreamCodec.flattenStreamReadItems(page.items);
|
|
4544
5188
|
};
|
|
5189
|
+
const readWhenCommitted = async function* (route, options) {
|
|
5190
|
+
assertStreamPattern(route);
|
|
5191
|
+
const wakeGate = createWakeGate();
|
|
5192
|
+
const subscription = await subscribe(route, () => {
|
|
5193
|
+
wakeGate.wake();
|
|
5194
|
+
});
|
|
5195
|
+
try {
|
|
5196
|
+
let offset = options.offset;
|
|
5197
|
+
while (true) {
|
|
5198
|
+
const observed = wakeGate.version;
|
|
5199
|
+
const page = await readPage(route, offset, options.batchSize ?? 100, {
|
|
5200
|
+
maxBytes: options.maxBytes,
|
|
5201
|
+
filter: options.filter,
|
|
5202
|
+
signal: options.signal
|
|
5203
|
+
});
|
|
5204
|
+
if (page.items.length > 0) {
|
|
5205
|
+
offset = page.cursor.lastResourceOffset + 1n;
|
|
5206
|
+
const records = StreamCodec.flattenStreamReadItems(page.items);
|
|
5207
|
+
if (records.length > 0) yield records;
|
|
5208
|
+
}
|
|
5209
|
+
if (page.cursor.hasMore) continue;
|
|
5210
|
+
await wakeGate.waitAfter(observed, { signal: options.signal });
|
|
5211
|
+
}
|
|
5212
|
+
} finally {
|
|
5213
|
+
await subscription.unsubscribe().catch(() => void 0);
|
|
5214
|
+
}
|
|
5215
|
+
};
|
|
4545
5216
|
const consume = async (route, startOffset, limit = 100, options) => {
|
|
4546
5217
|
return createAsyncIterableIterator(createSliceIterator(await read(route, startOffset, limit, options)));
|
|
4547
5218
|
};
|
|
4548
5219
|
const peek = async (route) => {
|
|
4549
5220
|
assertStreamRoute(route);
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
5221
|
+
return runWithRetry({
|
|
5222
|
+
domain: "stream",
|
|
5223
|
+
operation: "last",
|
|
5224
|
+
retryClass: "replayable_read"
|
|
5225
|
+
}, async () => {
|
|
5226
|
+
const response = await requestFrame(605, StreamCodec.encodeLast(route));
|
|
5227
|
+
const decoded = StreamCodec.decodeLastResponse(response);
|
|
5228
|
+
checkStatus(decoded.status, "LAST");
|
|
5229
|
+
return decoded.record ?? null;
|
|
5230
|
+
});
|
|
4554
5231
|
};
|
|
4555
5232
|
const metadata = async (route) => {
|
|
4556
5233
|
assertStreamRoute(route);
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
5234
|
+
return runWithRetry({
|
|
5235
|
+
domain: "stream",
|
|
5236
|
+
operation: "metadata",
|
|
5237
|
+
retryClass: "replayable_read"
|
|
5238
|
+
}, async () => {
|
|
5239
|
+
const response = await requestFrame(606, StreamCodec.encodeMetadata(route));
|
|
5240
|
+
const decoded = StreamCodec.decodeMetadataResponse(response);
|
|
5241
|
+
checkStatus(decoded.status, "GET_METADATA");
|
|
5242
|
+
return decoded.metadata ?? {
|
|
5243
|
+
firstOffset: 0n,
|
|
5244
|
+
lastOffset: 0n,
|
|
5245
|
+
recordCount: 0n
|
|
5246
|
+
};
|
|
5247
|
+
});
|
|
4565
5248
|
};
|
|
4566
5249
|
const subscribe = async (pattern, handler) => {
|
|
4567
5250
|
assertStreamPattern(pattern);
|
|
@@ -4570,8 +5253,8 @@ function createStreamClient(connection) {
|
|
|
4570
5253
|
if (existing) return addLocalSubscription(pattern, existing.subId, handler);
|
|
4571
5254
|
return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
|
|
4572
5255
|
};
|
|
4573
|
-
const subscribeWire = async (pattern) => {
|
|
4574
|
-
const response = await
|
|
5256
|
+
const subscribeWire = async (pattern, request = requestFrame) => {
|
|
5257
|
+
const response = await request(607, StreamCodec.encodeSubscribe(pattern));
|
|
4575
5258
|
const decoded = StreamCodec.decodeSubscribeResponse(response);
|
|
4576
5259
|
checkStatus(decoded.status, "SUBSCRIBE");
|
|
4577
5260
|
if (decoded.subId === void 0) throw new StreamError("SUBSCRIBE response missing subId", "MISSING_SESSION_ID");
|
|
@@ -4649,6 +5332,7 @@ function createStreamClient(connection) {
|
|
|
4649
5332
|
begin,
|
|
4650
5333
|
readPage,
|
|
4651
5334
|
read,
|
|
5335
|
+
readWhenCommitted,
|
|
4652
5336
|
consume,
|
|
4653
5337
|
peek,
|
|
4654
5338
|
metadata,
|
|
@@ -4826,9 +5510,10 @@ var ScheduleError$1 = class extends Error {
|
|
|
4826
5510
|
* Schedule domain client.
|
|
4827
5511
|
*/
|
|
4828
5512
|
function createScheduleClient(connection) {
|
|
4829
|
-
const { requestFrame } = createDomainClient(connection);
|
|
5513
|
+
const { requestFrame, requestReconnectFrame } = createDomainClient(connection);
|
|
4830
5514
|
const subscriptionsByPattern = /* @__PURE__ */ new Map();
|
|
4831
5515
|
const patternsBySubId = /* @__PURE__ */ new Map();
|
|
5516
|
+
const pendingNotificationsBySubId = /* @__PURE__ */ new Map();
|
|
4832
5517
|
let notifyHandlerInitialized = false;
|
|
4833
5518
|
let nextHandlerId = 1;
|
|
4834
5519
|
connection.onReconnect(async () => {
|
|
@@ -4839,8 +5524,9 @@ function createScheduleClient(connection) {
|
|
|
4839
5524
|
}));
|
|
4840
5525
|
subscriptionsByPattern.clear();
|
|
4841
5526
|
patternsBySubId.clear();
|
|
5527
|
+
pendingNotificationsBySubId.clear();
|
|
4842
5528
|
for (const subscription of subscriptions) {
|
|
4843
|
-
const subId = await subscribeWire(subscription.pattern);
|
|
5529
|
+
const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
|
|
4844
5530
|
subscriptionsByPattern.set(subscription.pattern, {
|
|
4845
5531
|
subId,
|
|
4846
5532
|
handlers: new Map(subscription.handlers)
|
|
@@ -4863,6 +5549,29 @@ function createScheduleClient(connection) {
|
|
|
4863
5549
|
const decoded = ScheduleCodec.decodeListResponse(assertSuccess(response, "LIST"));
|
|
4864
5550
|
return [decoded.entries, decoded.totalCount];
|
|
4865
5551
|
};
|
|
5552
|
+
const waitForNotifications = async function* (route, options = {}) {
|
|
5553
|
+
assertConcreteScheduleRoute(route);
|
|
5554
|
+
const wakeGate = createWakeGate();
|
|
5555
|
+
const pendingNotifications = [];
|
|
5556
|
+
const subscription = await subscribe(route, (notification) => {
|
|
5557
|
+
pendingNotifications.push(notification);
|
|
5558
|
+
wakeGate.wake();
|
|
5559
|
+
});
|
|
5560
|
+
try {
|
|
5561
|
+
while (true) {
|
|
5562
|
+
const notification = pendingNotifications.shift();
|
|
5563
|
+
if (notification) {
|
|
5564
|
+
yield notification;
|
|
5565
|
+
continue;
|
|
5566
|
+
}
|
|
5567
|
+
const observed = wakeGate.version;
|
|
5568
|
+
if (pendingNotifications.length > 0) continue;
|
|
5569
|
+
await wakeGate.waitAfter(observed, { signal: options.signal });
|
|
5570
|
+
}
|
|
5571
|
+
} finally {
|
|
5572
|
+
await subscription.unsubscribe().catch(() => void 0);
|
|
5573
|
+
}
|
|
5574
|
+
};
|
|
4866
5575
|
const subscribe = async (pattern, handler) => {
|
|
4867
5576
|
assertConcreteScheduleRoute(pattern);
|
|
4868
5577
|
initNotifyHandler();
|
|
@@ -4870,8 +5579,8 @@ function createScheduleClient(connection) {
|
|
|
4870
5579
|
if (existing) return addLocalSubscription(pattern, existing.subId, handler);
|
|
4871
5580
|
return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
|
|
4872
5581
|
};
|
|
4873
|
-
const subscribeWire = async (pattern) => {
|
|
4874
|
-
const response = await
|
|
5582
|
+
const subscribeWire = async (pattern, request = requestFrame) => {
|
|
5583
|
+
const response = await request(703, ScheduleCodec.encodeSubscribe(pattern));
|
|
4875
5584
|
return ScheduleCodec.decodeSubscribeResponse(assertSuccess(response, "SUBSCRIBE")).subId;
|
|
4876
5585
|
};
|
|
4877
5586
|
const addLocalSubscription = (pattern, subId, handler) => {
|
|
@@ -4886,6 +5595,7 @@ function createScheduleClient(connection) {
|
|
|
4886
5595
|
patternsBySubId.set(subId, pattern);
|
|
4887
5596
|
}
|
|
4888
5597
|
subscription.handlers.set(handlerId, handler);
|
|
5598
|
+
flushPendingNotifications(subId);
|
|
4889
5599
|
return createScheduleSubscription(subId, pattern, async () => {
|
|
4890
5600
|
await unsubscribe(pattern, handlerId);
|
|
4891
5601
|
});
|
|
@@ -4897,6 +5607,7 @@ function createScheduleClient(connection) {
|
|
|
4897
5607
|
if (subscription.handlers.size > 0) return;
|
|
4898
5608
|
subscriptionsByPattern.delete(pattern);
|
|
4899
5609
|
patternsBySubId.delete(subscription.subId);
|
|
5610
|
+
pendingNotificationsBySubId.delete(subscription.subId);
|
|
4900
5611
|
const response = await requestFrame(704, ScheduleCodec.encodeUnsubscribe(pattern));
|
|
4901
5612
|
ScheduleCodec.decodeUnsubscribeResponse(assertSuccess(response, "UNSUBSCRIBE"));
|
|
4902
5613
|
};
|
|
@@ -4907,16 +5618,40 @@ function createScheduleClient(connection) {
|
|
|
4907
5618
|
try {
|
|
4908
5619
|
const decoded = ScheduleCodec.decodeNotification(payload);
|
|
4909
5620
|
const pattern = patternsBySubId.get(decoded.subId);
|
|
4910
|
-
if (!pattern)
|
|
5621
|
+
if (!pattern) {
|
|
5622
|
+
queuePendingNotification(decoded.subId, { payload: decoded.payload });
|
|
5623
|
+
return;
|
|
5624
|
+
}
|
|
4911
5625
|
const subscription = subscriptionsByPattern.get(pattern);
|
|
4912
|
-
if (!subscription)
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
});
|
|
5626
|
+
if (!subscription) {
|
|
5627
|
+
queuePendingNotification(decoded.subId, { payload: decoded.payload });
|
|
5628
|
+
return;
|
|
5629
|
+
}
|
|
5630
|
+
dispatchNotification(subscription, { payload: decoded.payload });
|
|
4917
5631
|
} catch {}
|
|
4918
5632
|
});
|
|
4919
5633
|
};
|
|
5634
|
+
const queuePendingNotification = (subId, notification) => {
|
|
5635
|
+
const existing = pendingNotificationsBySubId.get(subId);
|
|
5636
|
+
if (existing) {
|
|
5637
|
+
existing.push(notification);
|
|
5638
|
+
return;
|
|
5639
|
+
}
|
|
5640
|
+
pendingNotificationsBySubId.set(subId, [notification]);
|
|
5641
|
+
};
|
|
5642
|
+
const flushPendingNotifications = (subId) => {
|
|
5643
|
+
const pending = pendingNotificationsBySubId.get(subId);
|
|
5644
|
+
if (!pending || pending.length === 0) return;
|
|
5645
|
+
pendingNotificationsBySubId.delete(subId);
|
|
5646
|
+
const subscription = Array.from(subscriptionsByPattern.values()).find((entry) => entry.subId === subId);
|
|
5647
|
+
if (!subscription) return;
|
|
5648
|
+
for (const notification of pending) dispatchNotification(subscription, notification);
|
|
5649
|
+
};
|
|
5650
|
+
const dispatchNotification = (subscription, notification) => {
|
|
5651
|
+
for (const handler of subscription.handlers.values()) connection.dispatchAsyncHandler(async () => {
|
|
5652
|
+
await handler(notification);
|
|
5653
|
+
});
|
|
5654
|
+
};
|
|
4920
5655
|
const assertSuccess = (payload, operation) => {
|
|
4921
5656
|
const result = parseStandardResponse(payload);
|
|
4922
5657
|
if (result.success) return result.data;
|
|
@@ -4933,7 +5668,8 @@ function createScheduleClient(connection) {
|
|
|
4933
5668
|
create,
|
|
4934
5669
|
cancel,
|
|
4935
5670
|
list,
|
|
4936
|
-
subscribe
|
|
5671
|
+
subscribe,
|
|
5672
|
+
waitForNotifications
|
|
4937
5673
|
};
|
|
4938
5674
|
}
|
|
4939
5675
|
const ScheduleClient = function(connection) {
|
|
@@ -4944,6 +5680,39 @@ function assertConcreteScheduleRoute(route) {
|
|
|
4944
5680
|
}
|
|
4945
5681
|
//#endregion
|
|
4946
5682
|
//#region src/client/client.ts
|
|
5683
|
+
const abortError = () => {
|
|
5684
|
+
const error = /* @__PURE__ */ new Error("The operation was aborted");
|
|
5685
|
+
error.name = "AbortError";
|
|
5686
|
+
return error;
|
|
5687
|
+
};
|
|
5688
|
+
const throwIfAborted = (signal) => {
|
|
5689
|
+
if (signal?.aborted) throw abortError();
|
|
5690
|
+
};
|
|
5691
|
+
const waitForSharedPromise = async (promise, signal) => {
|
|
5692
|
+
if (!signal) return promise;
|
|
5693
|
+
if (signal.aborted) throw abortError();
|
|
5694
|
+
return await new Promise((resolve, reject) => {
|
|
5695
|
+
let settled = false;
|
|
5696
|
+
const cleanup = () => {
|
|
5697
|
+
signal.removeEventListener("abort", onAbort);
|
|
5698
|
+
};
|
|
5699
|
+
const settle = (callback) => {
|
|
5700
|
+
if (settled) return;
|
|
5701
|
+
settled = true;
|
|
5702
|
+
cleanup();
|
|
5703
|
+
callback();
|
|
5704
|
+
};
|
|
5705
|
+
const onAbort = () => {
|
|
5706
|
+
settle(() => reject(abortError()));
|
|
5707
|
+
};
|
|
5708
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
5709
|
+
promise.then((value) => {
|
|
5710
|
+
settle(() => resolve(value));
|
|
5711
|
+
}, (error) => {
|
|
5712
|
+
settle(() => reject(error));
|
|
5713
|
+
});
|
|
5714
|
+
});
|
|
5715
|
+
};
|
|
4947
5716
|
function createClient(config) {
|
|
4948
5717
|
const observability = config.observability;
|
|
4949
5718
|
const resolvedConfig = {
|
|
@@ -4955,12 +5724,25 @@ function createClient(config) {
|
|
|
4955
5724
|
maxRequestQueueSize: 1024,
|
|
4956
5725
|
observability: config.observability ?? {},
|
|
4957
5726
|
reconnect: {
|
|
4958
|
-
enabled:
|
|
5727
|
+
enabled: true,
|
|
4959
5728
|
maxAttempts: Infinity,
|
|
4960
5729
|
backoffMs: 250,
|
|
4961
5730
|
maxBackoffMs: 5e3,
|
|
4962
5731
|
...config.reconnect
|
|
4963
5732
|
},
|
|
5733
|
+
retry: {
|
|
5734
|
+
enabled: true,
|
|
5735
|
+
maxAttempts: 3,
|
|
5736
|
+
backoffMs: 100,
|
|
5737
|
+
maxBackoffMs: 1e3,
|
|
5738
|
+
...config.retry
|
|
5739
|
+
},
|
|
5740
|
+
heartbeat: {
|
|
5741
|
+
enabled: true,
|
|
5742
|
+
intervalMs: 1e4,
|
|
5743
|
+
timeoutMs: 3e4,
|
|
5744
|
+
...config.heartbeat
|
|
5745
|
+
},
|
|
4964
5746
|
asyncHandlers: {
|
|
4965
5747
|
maxConcurrency: Infinity,
|
|
4966
5748
|
timeoutMs: 3e4,
|
|
@@ -4977,34 +5759,72 @@ function createClient(config) {
|
|
|
4977
5759
|
let noticeClient = null;
|
|
4978
5760
|
let streamClient = null;
|
|
4979
5761
|
let scheduleClient = null;
|
|
5762
|
+
let clientClosed = false;
|
|
5763
|
+
let pendingConnectPromise = null;
|
|
4980
5764
|
const resolveTokenProvider = () => {
|
|
4981
5765
|
if (resolvedConfig.tokenProvider) return resolvedConfig.tokenProvider;
|
|
4982
5766
|
return () => "";
|
|
4983
5767
|
};
|
|
4984
5768
|
const ensureConnection = () => {
|
|
5769
|
+
if (clientClosed) throw new ConnectionError("Client is closed", { state: "CLOSED" });
|
|
4985
5770
|
if (!connection) throw new ConnectionError("Not connected to Fitz server. Call connect() first.", { state: getState() });
|
|
4986
5771
|
return connection;
|
|
4987
5772
|
};
|
|
4988
|
-
const
|
|
4989
|
-
if (connection
|
|
5773
|
+
const createOwnedConnection = () => {
|
|
5774
|
+
if (connection) return connection;
|
|
4990
5775
|
connection = createConnection(() => createTransport(resolvedConfig.url, resolvedConfig.transport, {
|
|
4991
5776
|
timeout: resolvedConfig.timeout,
|
|
4992
|
-
maxFrameSize: resolvedConfig.maxFrameSize
|
|
5777
|
+
maxFrameSize: resolvedConfig.maxFrameSize,
|
|
5778
|
+
receiveTimeout: resolvedConfig.heartbeat?.enabled === false
|
|
4993
5779
|
}), resolveTokenProvider(), {
|
|
4994
5780
|
timeout: resolvedConfig.timeout,
|
|
4995
5781
|
authSettleDelayMs: resolvedConfig.authSettleDelayMs,
|
|
4996
5782
|
maxInFlightRequests: resolvedConfig.maxInFlightRequests,
|
|
4997
5783
|
maxRequestQueueSize: resolvedConfig.maxRequestQueueSize,
|
|
4998
5784
|
reconnect: resolvedConfig.reconnect,
|
|
5785
|
+
retry: resolvedConfig.retry,
|
|
5786
|
+
heartbeat: resolvedConfig.heartbeat,
|
|
4999
5787
|
observability,
|
|
5000
5788
|
asyncHandlers: resolvedConfig.asyncHandlers
|
|
5001
5789
|
});
|
|
5002
|
-
|
|
5790
|
+
return connection;
|
|
5791
|
+
};
|
|
5792
|
+
const connect = async (options = {}) => {
|
|
5793
|
+
if (clientClosed) throw new ConnectionError("Client is closed", { state: "CLOSED" });
|
|
5794
|
+
throwIfAborted(options.signal);
|
|
5795
|
+
const activeConnection = createOwnedConnection();
|
|
5796
|
+
if (activeConnection.isConnected()) return;
|
|
5797
|
+
if (pendingConnectPromise) {
|
|
5798
|
+
await waitForSharedPromise(pendingConnectPromise, options.signal);
|
|
5799
|
+
return;
|
|
5800
|
+
}
|
|
5801
|
+
const state = activeConnection.getState();
|
|
5802
|
+
const trackedConnectPromise = (state === "CONNECTING" || state === "CONNECTED" || state === "AUTHENTICATING" || state === "RECONNECTING" || state === "DISCONNECTED" && typeof activeConnection.shouldWaitForReconnect === "function" && activeConnection.shouldWaitForReconnect() ? typeof activeConnection.waitUntilReady === "function" ? activeConnection.waitUntilReady(void 0, resolvedConfig.timeout) : activeConnection.connect() : activeConnection.connect(options)).finally(() => {
|
|
5803
|
+
if (pendingConnectPromise === trackedConnectPromise) pendingConnectPromise = null;
|
|
5804
|
+
});
|
|
5805
|
+
pendingConnectPromise = trackedConnectPromise;
|
|
5806
|
+
await waitForSharedPromise(trackedConnectPromise, options.signal);
|
|
5003
5807
|
};
|
|
5004
5808
|
const close = async () => {
|
|
5809
|
+
if (clientClosed && !connection) {
|
|
5810
|
+
kvClient = null;
|
|
5811
|
+
queueClient = null;
|
|
5812
|
+
rpcClient = null;
|
|
5813
|
+
leaseClient = null;
|
|
5814
|
+
noticeClient = null;
|
|
5815
|
+
streamClient = null;
|
|
5816
|
+
scheduleClient = null;
|
|
5817
|
+
return;
|
|
5818
|
+
}
|
|
5819
|
+
clientClosed = true;
|
|
5820
|
+
pendingConnectPromise = null;
|
|
5005
5821
|
if (connection) {
|
|
5006
|
-
|
|
5007
|
-
|
|
5822
|
+
const activeConnection = connection;
|
|
5823
|
+
try {
|
|
5824
|
+
await activeConnection.close();
|
|
5825
|
+
} finally {
|
|
5826
|
+
if (connection === activeConnection) connection = null;
|
|
5827
|
+
}
|
|
5008
5828
|
}
|
|
5009
5829
|
kvClient = null;
|
|
5010
5830
|
queueClient = null;
|
|
@@ -5015,7 +5835,7 @@ function createClient(config) {
|
|
|
5015
5835
|
scheduleClient = null;
|
|
5016
5836
|
};
|
|
5017
5837
|
const isConnected = () => {
|
|
5018
|
-
return connection?.isConnected() ?? false;
|
|
5838
|
+
return !clientClosed && (connection?.isConnected() ?? false);
|
|
5019
5839
|
};
|
|
5020
5840
|
const kv = () => {
|
|
5021
5841
|
const activeConnection = ensureConnection();
|
|
@@ -5056,7 +5876,10 @@ function createClient(config) {
|
|
|
5056
5876
|
return ensureConnection().getUrl();
|
|
5057
5877
|
};
|
|
5058
5878
|
const getState = () => {
|
|
5059
|
-
|
|
5879
|
+
if (clientClosed) return "CLOSED";
|
|
5880
|
+
if (!connection) return "DISCONNECTED";
|
|
5881
|
+
const state = connection.getState();
|
|
5882
|
+
return state === "CLOSED" ? "DISCONNECTED" : state;
|
|
5060
5883
|
};
|
|
5061
5884
|
return {
|
|
5062
5885
|
config: resolvedConfig,
|
|
@@ -5282,6 +6105,7 @@ exports.TimeoutError = TimeoutError;
|
|
|
5282
6105
|
exports.TransportError = TransportError;
|
|
5283
6106
|
exports.createClient = createClient;
|
|
5284
6107
|
exports.createTaskGroup = createTaskGroup;
|
|
6108
|
+
exports.createWakeGate = createWakeGate;
|
|
5285
6109
|
exports.isRetryable = isRetryable;
|
|
5286
6110
|
|
|
5287
6111
|
//# sourceMappingURL=index.cjs.map
|