@cntryl/fitz 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/dist/index.cjs +652 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +652 -138
- 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 +9 -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 +7 -0
- package/dist/types/core/types.d.ts.map +1 -1
- 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.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.map +1 -1
- package/dist/types/domains/stream/client.d.ts.map +1 -1
- package/dist/types/domains/stream/codec.d.ts +1 -1
- package/dist/types/domains/stream/session.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -3
package/dist/index.mjs
CHANGED
|
@@ -531,11 +531,18 @@ function retryableKey(error) {
|
|
|
531
531
|
if (error.domainCode === void 0) return null;
|
|
532
532
|
return `${prefix}_${error.domainCode}`;
|
|
533
533
|
}
|
|
534
|
+
function isTransientQueueCommitFailure(error) {
|
|
535
|
+
if (!error.code.startsWith("QUEUE_")) return false;
|
|
536
|
+
const message = error.message.toLowerCase();
|
|
537
|
+
if (!message.includes("failed to commit transaction:")) return false;
|
|
538
|
+
return message.includes("writestall(") || message.includes("memory budget exceeded") || message.includes("lease heartbeat reports unhealthy") || message.includes("refusing writes");
|
|
539
|
+
}
|
|
534
540
|
function isRetryable(error) {
|
|
535
541
|
if (!(error instanceof FitzError)) return false;
|
|
536
542
|
if (error instanceof TimeoutError || error instanceof TransportError) return true;
|
|
537
543
|
const key = retryableKey(error);
|
|
538
|
-
|
|
544
|
+
if (key !== null && retryableErrorCodes.has(key)) return true;
|
|
545
|
+
return isTransientQueueCommitFailure(error);
|
|
539
546
|
}
|
|
540
547
|
/**
|
|
541
548
|
* Error types for Fitz client
|
|
@@ -974,7 +981,7 @@ function createMultiplexer(observability = {}) {
|
|
|
974
981
|
const span = tracer?.startSpan("fitz.request", attributes);
|
|
975
982
|
let spanEnded = false;
|
|
976
983
|
if (signal?.aborted) {
|
|
977
|
-
const error = abortError$
|
|
984
|
+
const error = abortError$2();
|
|
978
985
|
span?.recordException(error);
|
|
979
986
|
span?.end();
|
|
980
987
|
meter?.counter("fitz.request.failed", 1, {
|
|
@@ -1048,7 +1055,7 @@ function createMultiplexer(observability = {}) {
|
|
|
1048
1055
|
heapifyUp(requestEntry.timeoutIndex);
|
|
1049
1056
|
if (signal) {
|
|
1050
1057
|
onAbort = () => {
|
|
1051
|
-
failRequest(abortError$
|
|
1058
|
+
failRequest(abortError$2());
|
|
1052
1059
|
};
|
|
1053
1060
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1054
1061
|
}
|
|
@@ -1068,7 +1075,7 @@ function createMultiplexer(observability = {}) {
|
|
|
1068
1075
|
return deferred.promise;
|
|
1069
1076
|
}, (err) => {
|
|
1070
1077
|
if (!finalize()) {
|
|
1071
|
-
if (signal?.aborted) throw abortError$
|
|
1078
|
+
if (signal?.aborted) throw abortError$2();
|
|
1072
1079
|
throw err;
|
|
1073
1080
|
}
|
|
1074
1081
|
unregisterRequest(messageType, requestEntry);
|
|
@@ -1081,7 +1088,7 @@ function createMultiplexer(observability = {}) {
|
|
|
1081
1088
|
span?.end();
|
|
1082
1089
|
spanEnded = true;
|
|
1083
1090
|
}
|
|
1084
|
-
if (signal?.aborted) throw abortError$
|
|
1091
|
+
if (signal?.aborted) throw abortError$2();
|
|
1085
1092
|
throw err;
|
|
1086
1093
|
});
|
|
1087
1094
|
};
|
|
@@ -1193,7 +1200,7 @@ function createMultiplexer(observability = {}) {
|
|
|
1193
1200
|
const Multiplexer = function(observability = {}) {
|
|
1194
1201
|
return createMultiplexer(observability);
|
|
1195
1202
|
};
|
|
1196
|
-
function abortError$
|
|
1203
|
+
function abortError$2() {
|
|
1197
1204
|
const error = /* @__PURE__ */ new Error("The operation was aborted");
|
|
1198
1205
|
error.name = "AbortError";
|
|
1199
1206
|
return error;
|
|
@@ -1237,8 +1244,64 @@ function readU32BE(payload, offset) {
|
|
|
1237
1244
|
return (payload[offset] << 24 | payload[offset + 1] << 16 | payload[offset + 2] << 8 | payload[offset + 3]) >>> 0;
|
|
1238
1245
|
}
|
|
1239
1246
|
//#endregion
|
|
1247
|
+
//#region src/client/resilience.ts
|
|
1248
|
+
const resilienceMetaSymbol = Symbol("fitz.resilience.meta");
|
|
1249
|
+
function attachResilienceMeta(error, meta) {
|
|
1250
|
+
if (error && (typeof error === "object" || typeof error === "function")) Object.defineProperty(error, resilienceMetaSymbol, {
|
|
1251
|
+
value: meta,
|
|
1252
|
+
configurable: true,
|
|
1253
|
+
enumerable: false,
|
|
1254
|
+
writable: true
|
|
1255
|
+
});
|
|
1256
|
+
return error;
|
|
1257
|
+
}
|
|
1258
|
+
function getResilienceMeta(error) {
|
|
1259
|
+
if (!error || typeof error !== "object" && typeof error !== "function") return;
|
|
1260
|
+
return error[resilienceMetaSymbol];
|
|
1261
|
+
}
|
|
1262
|
+
function classifyFailureKind(error) {
|
|
1263
|
+
if (error instanceof TimeoutError) return "timeout";
|
|
1264
|
+
if (error instanceof TransportError) return "transport";
|
|
1265
|
+
if (error instanceof ConnectionError) return "connection";
|
|
1266
|
+
if (error instanceof FitzError) return "domain";
|
|
1267
|
+
return "other";
|
|
1268
|
+
}
|
|
1269
|
+
function isTransientRetryError(error) {
|
|
1270
|
+
return error instanceof TimeoutError || error instanceof TransportError || error instanceof ConnectionError || isRetryable(error);
|
|
1271
|
+
}
|
|
1272
|
+
function shouldRetryOperation(retryClass, error) {
|
|
1273
|
+
switch (retryClass) {
|
|
1274
|
+
case "wait_only": return false;
|
|
1275
|
+
case "replayable_read": return isTransientRetryError(error);
|
|
1276
|
+
case "confirmed_negative_retry": {
|
|
1277
|
+
const meta = getResilienceMeta(error);
|
|
1278
|
+
return meta?.explicitNegative === true && meta.boundary === "post-send" && isRetryable(error);
|
|
1279
|
+
}
|
|
1280
|
+
default: return false;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
//#endregion
|
|
1240
1284
|
//#region src/client/connection.ts
|
|
1241
1285
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1286
|
+
const sleepWithAbort = async (ms, signal) => {
|
|
1287
|
+
if (signal?.aborted) throw abortError$1();
|
|
1288
|
+
if (ms <= 0) return;
|
|
1289
|
+
await new Promise((resolve, reject) => {
|
|
1290
|
+
const timer = setTimeout(() => {
|
|
1291
|
+
cleanup();
|
|
1292
|
+
resolve();
|
|
1293
|
+
}, ms);
|
|
1294
|
+
const onAbort = () => {
|
|
1295
|
+
cleanup();
|
|
1296
|
+
reject(abortError$1());
|
|
1297
|
+
};
|
|
1298
|
+
const cleanup = () => {
|
|
1299
|
+
clearTimeout(timer);
|
|
1300
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1301
|
+
};
|
|
1302
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1303
|
+
});
|
|
1304
|
+
};
|
|
1242
1305
|
function createAsyncHandlerDispatcher(maxConcurrency, timeoutMs, onError) {
|
|
1243
1306
|
let activeCount = 0;
|
|
1244
1307
|
let closed = false;
|
|
@@ -1306,7 +1369,7 @@ function createRequestGate(maxConcurrency, maxQueueSize) {
|
|
|
1306
1369
|
let closed = false;
|
|
1307
1370
|
const queue = [];
|
|
1308
1371
|
const acquire = async (signal) => {
|
|
1309
|
-
if (signal?.aborted) throw abortError();
|
|
1372
|
+
if (signal?.aborted) throw abortError$1();
|
|
1310
1373
|
if (closed) throw connectionClosedError();
|
|
1311
1374
|
return await new Promise((resolve, reject) => {
|
|
1312
1375
|
const grant = () => {
|
|
@@ -1329,7 +1392,7 @@ function createRequestGate(maxConcurrency, maxQueueSize) {
|
|
|
1329
1392
|
waiter.onAbort = () => {
|
|
1330
1393
|
removeWaiter(waiter);
|
|
1331
1394
|
cleanup();
|
|
1332
|
-
reject(abortError());
|
|
1395
|
+
reject(abortError$1());
|
|
1333
1396
|
};
|
|
1334
1397
|
if (signal) signal.addEventListener("abort", waiter.onAbort, { once: true });
|
|
1335
1398
|
if (activeCount < maxConcurrency) {
|
|
@@ -1378,7 +1441,7 @@ function createRequestGate(maxConcurrency, maxQueueSize) {
|
|
|
1378
1441
|
close
|
|
1379
1442
|
};
|
|
1380
1443
|
}
|
|
1381
|
-
function abortError() {
|
|
1444
|
+
function abortError$1() {
|
|
1382
1445
|
const error = /* @__PURE__ */ new Error("The operation was aborted");
|
|
1383
1446
|
error.name = "AbortError";
|
|
1384
1447
|
return error;
|
|
@@ -1386,12 +1449,37 @@ function abortError() {
|
|
|
1386
1449
|
function connectionClosedError() {
|
|
1387
1450
|
return new ConnectionError("Connection closed", { state: "CLOSED" });
|
|
1388
1451
|
}
|
|
1389
|
-
function throwIfAborted(signal) {
|
|
1390
|
-
if (signal?.aborted) throw abortError();
|
|
1452
|
+
function throwIfAborted$1(signal) {
|
|
1453
|
+
if (signal?.aborted) throw abortError$1();
|
|
1391
1454
|
}
|
|
1392
1455
|
function isAbortError$1(error) {
|
|
1393
1456
|
return error instanceof Error && error.name === "AbortError";
|
|
1394
1457
|
}
|
|
1458
|
+
const waitForSharedPromise$1 = async (promise, signal) => {
|
|
1459
|
+
if (!signal) return promise;
|
|
1460
|
+
if (signal.aborted) throw abortError$1();
|
|
1461
|
+
return await new Promise((resolve, reject) => {
|
|
1462
|
+
let settled = false;
|
|
1463
|
+
const cleanup = () => {
|
|
1464
|
+
signal.removeEventListener("abort", onAbort);
|
|
1465
|
+
};
|
|
1466
|
+
const settle = (callback) => {
|
|
1467
|
+
if (settled) return;
|
|
1468
|
+
settled = true;
|
|
1469
|
+
cleanup();
|
|
1470
|
+
callback();
|
|
1471
|
+
};
|
|
1472
|
+
const onAbort = () => {
|
|
1473
|
+
settle(() => reject(abortError$1()));
|
|
1474
|
+
};
|
|
1475
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1476
|
+
promise.then((value) => {
|
|
1477
|
+
settle(() => resolve(value));
|
|
1478
|
+
}, (error) => {
|
|
1479
|
+
settle(() => reject(error));
|
|
1480
|
+
});
|
|
1481
|
+
});
|
|
1482
|
+
};
|
|
1395
1483
|
function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
1396
1484
|
const timeout = options.timeout ?? 3e4;
|
|
1397
1485
|
const authSettleDelayMs = options.authSettleDelayMs ?? 100;
|
|
@@ -1399,6 +1487,10 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1399
1487
|
const reconnectMaxAttempts = options.reconnect?.maxAttempts ?? Infinity;
|
|
1400
1488
|
const reconnectBackoffMs = options.reconnect?.backoffMs ?? 250;
|
|
1401
1489
|
const reconnectMaxBackoffMs = options.reconnect?.maxBackoffMs ?? 5e3;
|
|
1490
|
+
const retryEnabled = options.retry?.enabled ?? true;
|
|
1491
|
+
const retryMaxAttempts = options.retry?.maxAttempts ?? 3;
|
|
1492
|
+
const retryBackoffMs = options.retry?.backoffMs ?? 100;
|
|
1493
|
+
const retryMaxBackoffMs = options.retry?.maxBackoffMs ?? 1e3;
|
|
1402
1494
|
const maxInFlightRequests = options.maxInFlightRequests ?? 256;
|
|
1403
1495
|
const maxRequestQueueSize = options.maxRequestQueueSize ?? 1024;
|
|
1404
1496
|
const observability = options.observability;
|
|
@@ -1412,10 +1504,18 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1412
1504
|
let receiveLoop = null;
|
|
1413
1505
|
let receiveLoopAbort = false;
|
|
1414
1506
|
let closeRequested = false;
|
|
1507
|
+
let permanentlyClosed = false;
|
|
1508
|
+
let connectPromise = null;
|
|
1415
1509
|
let reconnectPromise = null;
|
|
1510
|
+
let reconnectRestoreActive = false;
|
|
1416
1511
|
let authOutcome = null;
|
|
1417
1512
|
let authRejected = false;
|
|
1513
|
+
let hasEstablishedSession = false;
|
|
1514
|
+
let reconnectExhausted = false;
|
|
1515
|
+
let readyWaiterCount = 0;
|
|
1516
|
+
const closeAbortController = new AbortController();
|
|
1418
1517
|
const connectionScope = createScope("connection");
|
|
1518
|
+
const readyListeners = /* @__PURE__ */ new Set();
|
|
1419
1519
|
const log = (level, event, fields) => {
|
|
1420
1520
|
observability?.logger?.log(level, event, fields);
|
|
1421
1521
|
};
|
|
@@ -1446,6 +1546,8 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1446
1546
|
};
|
|
1447
1547
|
const handlePossibleTransportFailure = (error) => {
|
|
1448
1548
|
if (closeRequested) return;
|
|
1549
|
+
if (state !== "AUTHENTICATED") return;
|
|
1550
|
+
if (getResilienceMeta(error)?.boundary === "pre-send") return;
|
|
1449
1551
|
if (error instanceof TransportError || error instanceof ConnectionError || error instanceof AuthenticationError) handleConnectionLoss(error);
|
|
1450
1552
|
};
|
|
1451
1553
|
const multiplexer = new Multiplexer({
|
|
@@ -1456,17 +1558,34 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1456
1558
|
log("warn", "fitz.connection.handler_failed", { error: describeError(error) });
|
|
1457
1559
|
});
|
|
1458
1560
|
const connect = async (options = {}) => {
|
|
1459
|
-
closeRequested
|
|
1561
|
+
if (permanentlyClosed || closeRequested) throw connectionClosedError();
|
|
1562
|
+
throwIfAborted$1(options.signal);
|
|
1563
|
+
if (state === "AUTHENTICATED") return;
|
|
1564
|
+
if (connectPromise) {
|
|
1565
|
+
await waitForSharedPromise$1(connectPromise, options.signal);
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
if (reconnectPromise || state === "CONNECTING" || state === "CONNECTED" || state === "AUTHENTICATING" || state === "RECONNECTING" || state === "DISCONNECTED" && canWaitForReconnect()) {
|
|
1569
|
+
await waitForReady(options.signal, timeout);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1460
1572
|
authRejected = false;
|
|
1461
|
-
|
|
1573
|
+
reconnectExhausted = false;
|
|
1574
|
+
const sharedConnectPromise = openAndAuthenticate(false, options.signal).finally(() => {
|
|
1575
|
+
if (connectPromise === sharedConnectPromise) connectPromise = null;
|
|
1576
|
+
});
|
|
1577
|
+
connectPromise = sharedConnectPromise;
|
|
1578
|
+
await sharedConnectPromise;
|
|
1462
1579
|
};
|
|
1463
1580
|
const close = async () => {
|
|
1464
1581
|
if (state === "CLOSED" && !transport) {
|
|
1465
1582
|
await connectionScope.dispose();
|
|
1466
1583
|
return;
|
|
1467
1584
|
}
|
|
1585
|
+
permanentlyClosed = true;
|
|
1468
1586
|
closeRequested = true;
|
|
1469
1587
|
receiveLoopAbort = true;
|
|
1588
|
+
closeAbortController.abort();
|
|
1470
1589
|
asyncHandlerDispatcher.close();
|
|
1471
1590
|
const scopeDisposePromise = connectionScope.dispose();
|
|
1472
1591
|
setState("CLOSED");
|
|
@@ -1487,15 +1606,34 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1487
1606
|
await asyncHandlerDispatcher.drain();
|
|
1488
1607
|
await scopeDisposePromise;
|
|
1489
1608
|
};
|
|
1490
|
-
const
|
|
1609
|
+
const waitForRequestReady = async (signal, allowReconnectRestore = false) => {
|
|
1610
|
+
if (allowReconnectRestore && reconnectRestoreActive && state === "AUTHENTICATING" && !closeRequested && !authRejected && transport) return;
|
|
1611
|
+
const releaseReadyWaitSlot = acquireReadyWaitSlot();
|
|
1612
|
+
try {
|
|
1613
|
+
await waitForReady(signal, timeout);
|
|
1614
|
+
} finally {
|
|
1615
|
+
releaseReadyWaitSlot?.();
|
|
1616
|
+
}
|
|
1491
1617
|
ensureAuthenticated();
|
|
1618
|
+
};
|
|
1619
|
+
const requestInternal = async (messageType, requestPayload, signal, allowReconnectRestore = false) => {
|
|
1620
|
+
let sendStarted = false;
|
|
1621
|
+
await waitForRequestReady(signal, allowReconnectRestore);
|
|
1492
1622
|
const releaseRequestSlot = await requestGate.acquire(signal);
|
|
1493
1623
|
const startedAt = Date.now();
|
|
1494
1624
|
try {
|
|
1495
1625
|
const activeTransport = ensureTransport();
|
|
1496
1626
|
const frame = FrameCodec.encodeFrame(messageType, requestPayload);
|
|
1497
|
-
return await multiplexer.request(messageType, frame, (data) =>
|
|
1627
|
+
return await multiplexer.request(messageType, frame, (data) => {
|
|
1628
|
+
sendStarted = true;
|
|
1629
|
+
return sendSerialized(activeTransport, data);
|
|
1630
|
+
}, timeout, signal);
|
|
1498
1631
|
} catch (error) {
|
|
1632
|
+
attachResilienceMeta(error, {
|
|
1633
|
+
boundary: sendStarted ? "post-send" : "pre-send",
|
|
1634
|
+
failureKind: classifyFailureKind(error),
|
|
1635
|
+
explicitNegative: false
|
|
1636
|
+
});
|
|
1499
1637
|
log("error", "fitz.connection.request_failed", {
|
|
1500
1638
|
operation: "request",
|
|
1501
1639
|
state,
|
|
@@ -1510,13 +1648,34 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1510
1648
|
releaseRequestSlot();
|
|
1511
1649
|
}
|
|
1512
1650
|
};
|
|
1513
|
-
const
|
|
1651
|
+
const request = async (messageType, requestPayload, signal) => {
|
|
1652
|
+
return await requestInternal(messageType, requestPayload, signal);
|
|
1653
|
+
};
|
|
1654
|
+
const requestDuringReconnectRestore = async (messageType, requestPayload, signal) => {
|
|
1655
|
+
return await requestInternal(messageType, requestPayload, signal, true);
|
|
1656
|
+
};
|
|
1657
|
+
const send = async (messageType, requestPayload, signal) => {
|
|
1658
|
+
let sendStarted = false;
|
|
1659
|
+
const releaseReadyWaitSlot = acquireReadyWaitSlot();
|
|
1660
|
+
try {
|
|
1661
|
+
await waitForReady(signal, timeout);
|
|
1662
|
+
} finally {
|
|
1663
|
+
releaseReadyWaitSlot?.();
|
|
1664
|
+
}
|
|
1514
1665
|
ensureAuthenticated();
|
|
1515
|
-
const releaseRequestSlot = await requestGate.acquire();
|
|
1666
|
+
const releaseRequestSlot = await requestGate.acquire(signal);
|
|
1516
1667
|
const startedAt = Date.now();
|
|
1517
1668
|
try {
|
|
1518
|
-
|
|
1669
|
+
const activeTransport = ensureTransport();
|
|
1670
|
+
const frame = FrameCodec.encodeFrame(messageType, requestPayload);
|
|
1671
|
+
sendStarted = true;
|
|
1672
|
+
await sendSerialized(activeTransport, frame);
|
|
1519
1673
|
} catch (error) {
|
|
1674
|
+
attachResilienceMeta(error, {
|
|
1675
|
+
boundary: sendStarted ? "post-send" : "pre-send",
|
|
1676
|
+
failureKind: classifyFailureKind(error),
|
|
1677
|
+
explicitNegative: false
|
|
1678
|
+
});
|
|
1520
1679
|
log("error", "fitz.connection.send_failed", {
|
|
1521
1680
|
operation: "send",
|
|
1522
1681
|
state,
|
|
@@ -1531,8 +1690,8 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1531
1690
|
releaseRequestSlot();
|
|
1532
1691
|
}
|
|
1533
1692
|
};
|
|
1534
|
-
const sendFireAndForget = async (messageType, requestPayload) => {
|
|
1535
|
-
await send(messageType, requestPayload);
|
|
1693
|
+
const sendFireAndForget = async (messageType, requestPayload, signal) => {
|
|
1694
|
+
await send(messageType, requestPayload, signal);
|
|
1536
1695
|
};
|
|
1537
1696
|
const registerNotificationHandler = (messageType, handler) => {
|
|
1538
1697
|
multiplexer.registerNotificationHandler(messageType, handler);
|
|
@@ -1560,8 +1719,138 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1560
1719
|
const getState = () => state;
|
|
1561
1720
|
const isConnected = () => state === "AUTHENTICATED";
|
|
1562
1721
|
const getUrl = () => ensureTransport().getUrl();
|
|
1722
|
+
const canWaitForReconnect = () => {
|
|
1723
|
+
return reconnectEnabled && hasEstablishedSession && !reconnectExhausted && !authRejected;
|
|
1724
|
+
};
|
|
1725
|
+
const readyFailure = () => {
|
|
1726
|
+
if (state === "AUTHENTICATED") return null;
|
|
1727
|
+
if (closeRequested || state === "CLOSED") return connectionClosedError();
|
|
1728
|
+
if (authRejected) return new AuthenticationError("Authentication rejected", { state });
|
|
1729
|
+
if (state === "CONNECTING" || state === "CONNECTED" || state === "AUTHENTICATING" || state === "RECONNECTING") return null;
|
|
1730
|
+
if (state === "DISCONNECTED" && canWaitForReconnect()) return null;
|
|
1731
|
+
return new ConnectionError(`Cannot use connection while state is ${state}`, { state });
|
|
1732
|
+
};
|
|
1733
|
+
const notifyReadyListeners = () => {
|
|
1734
|
+
for (const listener of readyListeners) listener();
|
|
1735
|
+
};
|
|
1736
|
+
const acquireReadyWaitSlot = () => {
|
|
1737
|
+
const failure = readyFailure();
|
|
1738
|
+
if (state === "AUTHENTICATED" || failure) return null;
|
|
1739
|
+
if (readyWaiterCount >= maxRequestQueueSize) throw new RequestQueueFullError();
|
|
1740
|
+
readyWaiterCount += 1;
|
|
1741
|
+
let released = false;
|
|
1742
|
+
return () => {
|
|
1743
|
+
if (released) return;
|
|
1744
|
+
released = true;
|
|
1745
|
+
readyWaiterCount = Math.max(readyWaiterCount - 1, 0);
|
|
1746
|
+
};
|
|
1747
|
+
};
|
|
1748
|
+
const waitForReady = async (signal, waitTimeoutMs = timeout) => {
|
|
1749
|
+
throwIfAborted$1(signal);
|
|
1750
|
+
const immediateFailure = readyFailure();
|
|
1751
|
+
if (!immediateFailure) {
|
|
1752
|
+
if (state === "AUTHENTICATED") return;
|
|
1753
|
+
} else throw immediateFailure;
|
|
1754
|
+
await new Promise((resolve, reject) => {
|
|
1755
|
+
let settled = false;
|
|
1756
|
+
let timeoutId = null;
|
|
1757
|
+
const cleanup = () => {
|
|
1758
|
+
readyListeners.delete(onStateChange);
|
|
1759
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
1760
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1761
|
+
};
|
|
1762
|
+
const settle = (cb) => {
|
|
1763
|
+
if (settled) return;
|
|
1764
|
+
settled = true;
|
|
1765
|
+
cleanup();
|
|
1766
|
+
cb();
|
|
1767
|
+
};
|
|
1768
|
+
const onAbort = () => {
|
|
1769
|
+
settle(() => reject(abortError$1()));
|
|
1770
|
+
};
|
|
1771
|
+
const onStateChange = () => {
|
|
1772
|
+
const failure = readyFailure();
|
|
1773
|
+
if (state === "AUTHENTICATED") {
|
|
1774
|
+
settle(resolve);
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
if (failure) settle(() => reject(failure));
|
|
1778
|
+
};
|
|
1779
|
+
readyListeners.add(onStateChange);
|
|
1780
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1781
|
+
timeoutId = setTimeout(() => {
|
|
1782
|
+
settle(() => reject(new ConnectionError("Timed out waiting for connection to become ready", { state })));
|
|
1783
|
+
}, waitTimeoutMs);
|
|
1784
|
+
onStateChange();
|
|
1785
|
+
});
|
|
1786
|
+
};
|
|
1787
|
+
const waitUntilReady = async (signal, waitTimeoutMs = timeout) => {
|
|
1788
|
+
await waitForReady(signal, waitTimeoutMs);
|
|
1789
|
+
};
|
|
1790
|
+
const shouldWaitForReconnect = () => {
|
|
1791
|
+
return reconnectPromise !== null || state === "RECONNECTING" || state === "DISCONNECTED" && canWaitForReconnect();
|
|
1792
|
+
};
|
|
1793
|
+
const getRetryDelayMs = (baseDelayMs) => {
|
|
1794
|
+
const jitter = Math.floor(Math.random() * baseDelayMs * .5);
|
|
1795
|
+
return Math.min(Math.max(baseDelayMs + jitter, 1), retryMaxBackoffMs);
|
|
1796
|
+
};
|
|
1797
|
+
const recordRetry = (operation, attempt, delayMs, error) => {
|
|
1798
|
+
const meta = getResilienceMeta(error);
|
|
1799
|
+
log("warn", "fitz.request.retry", {
|
|
1800
|
+
domain: operation.domain,
|
|
1801
|
+
operation: operation.operation,
|
|
1802
|
+
attempt,
|
|
1803
|
+
delayMs,
|
|
1804
|
+
boundary: meta?.boundary ?? "unknown",
|
|
1805
|
+
error: describeError(error),
|
|
1806
|
+
...describeErrorFields(error)
|
|
1807
|
+
});
|
|
1808
|
+
observability?.meter?.counter("fitz.request.retry", 1, {
|
|
1809
|
+
domain: operation.domain,
|
|
1810
|
+
operation: operation.operation,
|
|
1811
|
+
boundary: meta?.boundary ?? "unknown"
|
|
1812
|
+
});
|
|
1813
|
+
};
|
|
1814
|
+
const recordRetryExhausted = (operation, attempt, error) => {
|
|
1815
|
+
const meta = getResilienceMeta(error);
|
|
1816
|
+
log("warn", "fitz.request.retry_exhausted", {
|
|
1817
|
+
domain: operation.domain,
|
|
1818
|
+
operation: operation.operation,
|
|
1819
|
+
attempt,
|
|
1820
|
+
boundary: meta?.boundary ?? "unknown",
|
|
1821
|
+
error: describeError(error),
|
|
1822
|
+
...describeErrorFields(error)
|
|
1823
|
+
});
|
|
1824
|
+
observability?.meter?.counter("fitz.request.retry_exhausted", 1, {
|
|
1825
|
+
domain: operation.domain,
|
|
1826
|
+
operation: operation.operation,
|
|
1827
|
+
boundary: meta?.boundary ?? "unknown"
|
|
1828
|
+
});
|
|
1829
|
+
};
|
|
1830
|
+
const executeWithRetry = async (operation, task) => {
|
|
1831
|
+
if (!retryEnabled || operation.retryClass === "wait_only") return task();
|
|
1832
|
+
let attempt = 0;
|
|
1833
|
+
let delayMs = retryBackoffMs;
|
|
1834
|
+
while (true) {
|
|
1835
|
+
attempt += 1;
|
|
1836
|
+
try {
|
|
1837
|
+
return await task();
|
|
1838
|
+
} catch (error) {
|
|
1839
|
+
if (isAbortError$1(error)) throw error;
|
|
1840
|
+
if (!shouldRetryOperation(operation.retryClass, error)) throw error;
|
|
1841
|
+
if (attempt >= retryMaxAttempts) {
|
|
1842
|
+
recordRetryExhausted(operation, attempt, error);
|
|
1843
|
+
throw error;
|
|
1844
|
+
}
|
|
1845
|
+
const actualDelayMs = getRetryDelayMs(delayMs);
|
|
1846
|
+
recordRetry(operation, attempt, actualDelayMs, error);
|
|
1847
|
+
await sleepWithAbort(actualDelayMs, operation.signal);
|
|
1848
|
+
delayMs = Math.min(delayMs * 2, retryMaxBackoffMs);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1563
1852
|
const openAndAuthenticate = async (isReconnect, signal) => {
|
|
1564
|
-
throwIfAborted(signal);
|
|
1853
|
+
throwIfAborted$1(signal);
|
|
1565
1854
|
receiveLoopAbort = false;
|
|
1566
1855
|
frameParser.parseFrames(new Uint8Array(0));
|
|
1567
1856
|
requestGate = createRequestGate(maxInFlightRequests, maxRequestQueueSize);
|
|
@@ -1575,7 +1864,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1575
1864
|
if (transport === activeTransport) transport = null;
|
|
1576
1865
|
throw connectionClosedError();
|
|
1577
1866
|
}
|
|
1578
|
-
throwIfAborted(signal);
|
|
1867
|
+
throwIfAborted$1(signal);
|
|
1579
1868
|
receiveLoop = startReceiveLoop();
|
|
1580
1869
|
setState("CONNECTED");
|
|
1581
1870
|
setState("AUTHENTICATING");
|
|
@@ -1584,21 +1873,30 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1584
1873
|
try {
|
|
1585
1874
|
await sendConnect();
|
|
1586
1875
|
if (closeRequested) throw connectionClosedError();
|
|
1587
|
-
throwIfAborted(signal);
|
|
1876
|
+
throwIfAborted$1(signal);
|
|
1588
1877
|
await Promise.race([authOutcome.promise, sleep(authSettleDelayMs)]);
|
|
1589
1878
|
if (closeRequested) throw connectionClosedError();
|
|
1590
|
-
throwIfAborted(signal);
|
|
1879
|
+
throwIfAborted$1(signal);
|
|
1591
1880
|
authOutcome?.resolve();
|
|
1592
1881
|
authOutcome = null;
|
|
1593
1882
|
if (isReconnect) {
|
|
1594
|
-
|
|
1595
|
-
|
|
1883
|
+
multiplexer.setConnected();
|
|
1884
|
+
reconnectRestoreActive = true;
|
|
1885
|
+
try {
|
|
1886
|
+
await restoreReconnectState();
|
|
1887
|
+
if (closeRequested) throw connectionClosedError();
|
|
1888
|
+
} finally {
|
|
1889
|
+
reconnectRestoreActive = false;
|
|
1890
|
+
}
|
|
1596
1891
|
}
|
|
1892
|
+
hasEstablishedSession = true;
|
|
1893
|
+
reconnectExhausted = false;
|
|
1597
1894
|
setState("AUTHENTICATED");
|
|
1598
|
-
multiplexer.setConnected();
|
|
1895
|
+
if (!isReconnect) multiplexer.setConnected();
|
|
1599
1896
|
emitLifecycleEvent(isReconnect ? "reconnect_succeeded" : "connect_succeeded");
|
|
1600
1897
|
} catch (error) {
|
|
1601
1898
|
authOutcome = null;
|
|
1899
|
+
reconnectRestoreActive = false;
|
|
1602
1900
|
multiplexer.setDisconnected();
|
|
1603
1901
|
emitDisconnect();
|
|
1604
1902
|
if (activeTransport) await activeTransport.close().catch(() => void 0);
|
|
@@ -1608,7 +1906,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1608
1906
|
if (closeRequested) setState("CLOSED");
|
|
1609
1907
|
else setState(rejectedAuth ? "CLOSED" : "DISCONNECTED");
|
|
1610
1908
|
emitLifecycleEvent(isReconnect ? "reconnect_failed" : "connect_failed", error);
|
|
1611
|
-
if (isAbortError$1(error)) throw abortError();
|
|
1909
|
+
if (isAbortError$1(error)) throw abortError$1();
|
|
1612
1910
|
throw error;
|
|
1613
1911
|
}
|
|
1614
1912
|
};
|
|
@@ -1649,6 +1947,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1649
1947
|
emitLifecycleEvent("auth_rejected", error);
|
|
1650
1948
|
return;
|
|
1651
1949
|
}
|
|
1950
|
+
reconnectExhausted = false;
|
|
1652
1951
|
setState("DISCONNECTED");
|
|
1653
1952
|
emitLifecycleEvent("connection_lost", error);
|
|
1654
1953
|
if (!reconnectEnabled) return;
|
|
@@ -1670,12 +1969,13 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1670
1969
|
emitLifecycleEvent("reconnect_scheduled", void 0, attempts);
|
|
1671
1970
|
const actualDelayMs = getReconnectDelayMs(delayMs);
|
|
1672
1971
|
try {
|
|
1673
|
-
await
|
|
1972
|
+
await sleepWithAbort(actualDelayMs, closeAbortController.signal);
|
|
1674
1973
|
if (closeRequested) return;
|
|
1675
1974
|
await openAndAuthenticate(true);
|
|
1676
1975
|
return;
|
|
1677
1976
|
} catch (error) {
|
|
1678
1977
|
if (closeRequested) return;
|
|
1978
|
+
if (isAbortError$1(error)) return;
|
|
1679
1979
|
log("warn", "fitz.connection.reconnect_retry", {
|
|
1680
1980
|
attempts,
|
|
1681
1981
|
delayMs: actualDelayMs,
|
|
@@ -1689,6 +1989,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1689
1989
|
setState("CLOSED");
|
|
1690
1990
|
return;
|
|
1691
1991
|
}
|
|
1992
|
+
reconnectExhausted = true;
|
|
1692
1993
|
setState("DISCONNECTED");
|
|
1693
1994
|
emitLifecycleEvent("reconnect_exhausted", void 0, attempts);
|
|
1694
1995
|
};
|
|
@@ -1709,6 +2010,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1709
2010
|
};
|
|
1710
2011
|
const setState = (newState) => {
|
|
1711
2012
|
state = newState;
|
|
2013
|
+
notifyReadyListeners();
|
|
1712
2014
|
};
|
|
1713
2015
|
const sendSerialized = async (transport, data) => {
|
|
1714
2016
|
const prior = writeChain;
|
|
@@ -1748,6 +2050,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1748
2050
|
connect,
|
|
1749
2051
|
close,
|
|
1750
2052
|
request,
|
|
2053
|
+
requestDuringReconnectRestore,
|
|
1751
2054
|
send,
|
|
1752
2055
|
sendFireAndForget,
|
|
1753
2056
|
registerNotificationHandler,
|
|
@@ -1756,6 +2059,9 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1756
2059
|
onDisconnect,
|
|
1757
2060
|
getMultiplexer,
|
|
1758
2061
|
dispatchAsyncHandler,
|
|
2062
|
+
executeWithRetry,
|
|
2063
|
+
waitUntilReady,
|
|
2064
|
+
shouldWaitForReconnect,
|
|
1759
2065
|
getScope,
|
|
1760
2066
|
getState,
|
|
1761
2067
|
isConnected,
|
|
@@ -2139,11 +2445,23 @@ function createTransport(url, transportType = "auto", options = {}) {
|
|
|
2139
2445
|
//#region src/domains/base.ts
|
|
2140
2446
|
function createDomainClient(connection) {
|
|
2141
2447
|
const requestFrame = async (messageType, payload, signal) => connection.request(messageType, payload, signal);
|
|
2448
|
+
const requestReconnectFrame = async (messageType, payload, signal) => {
|
|
2449
|
+
const resilientConnection = connection;
|
|
2450
|
+
if (typeof resilientConnection.requestDuringReconnectRestore === "function") return await resilientConnection.requestDuringReconnectRestore(messageType, payload, signal);
|
|
2451
|
+
return await connection.request(messageType, payload, signal);
|
|
2452
|
+
};
|
|
2142
2453
|
const sendFrame = async (messageType, payload) => connection.send(messageType, payload);
|
|
2454
|
+
const runWithRetry = async (operation, task) => {
|
|
2455
|
+
const resilientConnection = connection;
|
|
2456
|
+
if (typeof resilientConnection.executeWithRetry === "function") return resilientConnection.executeWithRetry(operation, task);
|
|
2457
|
+
return task();
|
|
2458
|
+
};
|
|
2143
2459
|
return {
|
|
2144
2460
|
connection,
|
|
2145
2461
|
requestFrame,
|
|
2146
|
-
|
|
2462
|
+
requestReconnectFrame,
|
|
2463
|
+
sendFrame,
|
|
2464
|
+
runWithRetry
|
|
2147
2465
|
};
|
|
2148
2466
|
}
|
|
2149
2467
|
//#endregion
|
|
@@ -2321,8 +2639,11 @@ function createAsyncIterableIterator(iterator) {
|
|
|
2321
2639
|
//#region src/domains/kv/transaction.ts
|
|
2322
2640
|
function createKvTransaction(connection, route, txId) {
|
|
2323
2641
|
let closed = false;
|
|
2324
|
-
const
|
|
2642
|
+
const resilientConnection = connection;
|
|
2643
|
+
let unsubscribeDisconnect = () => void 0;
|
|
2644
|
+
unsubscribeDisconnect = connection.onDisconnect(() => {
|
|
2325
2645
|
closed = true;
|
|
2646
|
+
unsubscribeDisconnect();
|
|
2326
2647
|
});
|
|
2327
2648
|
const ensureOpen = () => {
|
|
2328
2649
|
if (closed) throw new KvError("Transaction already closed", "TX_CLOSED");
|
|
@@ -2337,6 +2658,10 @@ function createKvTransaction(connection, route, txId) {
|
|
|
2337
2658
|
[5]: "OperationNotAllowed"
|
|
2338
2659
|
}[status] ?? `Unknown(${status})`}`, operation, status);
|
|
2339
2660
|
};
|
|
2661
|
+
const runWithRetry = async (operation, task) => {
|
|
2662
|
+
if (typeof resilientConnection.executeWithRetry === "function") return resilientConnection.executeWithRetry(operation, task);
|
|
2663
|
+
return task();
|
|
2664
|
+
};
|
|
2340
2665
|
const put = async (key, value, signal) => {
|
|
2341
2666
|
ensureOpen();
|
|
2342
2667
|
const payload = KvCodec.encodePut(txId, route, key, value);
|
|
@@ -2351,15 +2676,22 @@ function createKvTransaction(connection, route, txId) {
|
|
|
2351
2676
|
};
|
|
2352
2677
|
const get = async (key, signal) => {
|
|
2353
2678
|
ensureOpen();
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2679
|
+
return runWithRetry({
|
|
2680
|
+
domain: "kv",
|
|
2681
|
+
operation: "get",
|
|
2682
|
+
retryClass: "replayable_read",
|
|
2683
|
+
signal
|
|
2684
|
+
}, async () => {
|
|
2685
|
+
const payload = KvCodec.encodeGet(txId, route, key);
|
|
2686
|
+
const response = await connection.request(103, payload, signal);
|
|
2687
|
+
const decoded = KvCodec.decodeGetResponse(response);
|
|
2688
|
+
checkStatus(decoded.status, "GET");
|
|
2689
|
+
if (!decoded.found || !decoded.value) return { type: "not-found" };
|
|
2690
|
+
return {
|
|
2691
|
+
type: "found",
|
|
2692
|
+
value: decoded.value
|
|
2693
|
+
};
|
|
2694
|
+
});
|
|
2363
2695
|
};
|
|
2364
2696
|
const deleteItem = async (key, signal) => {
|
|
2365
2697
|
ensureOpen();
|
|
@@ -2375,11 +2707,18 @@ function createKvTransaction(connection, route, txId) {
|
|
|
2375
2707
|
};
|
|
2376
2708
|
const scan = async (options = {}, signal) => {
|
|
2377
2709
|
ensureOpen();
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2710
|
+
return runWithRetry({
|
|
2711
|
+
domain: "kv",
|
|
2712
|
+
operation: "scan",
|
|
2713
|
+
retryClass: "replayable_read",
|
|
2714
|
+
signal
|
|
2715
|
+
}, async () => {
|
|
2716
|
+
const payload = KvCodec.encodeScan(txId, route, options);
|
|
2717
|
+
const response = await connection.request(108, payload, signal);
|
|
2718
|
+
const decoded = KvCodec.decodeScanResponse(response);
|
|
2719
|
+
checkStatus(decoded.status, "SCAN");
|
|
2720
|
+
return createAsyncIterableIterator(createSliceIterator(decoded.keys));
|
|
2721
|
+
});
|
|
2383
2722
|
};
|
|
2384
2723
|
const commit = async (signal) => {
|
|
2385
2724
|
ensureOpen();
|
|
@@ -2659,7 +2998,17 @@ const QueueCodec = {
|
|
|
2659
2998
|
//#endregion
|
|
2660
2999
|
//#region src/domains/queue/types.ts
|
|
2661
3000
|
function createQueueItem(id, token, body, route, connection) {
|
|
3001
|
+
let closed = false;
|
|
3002
|
+
let unsubscribeDisconnect = () => void 0;
|
|
3003
|
+
unsubscribeDisconnect = connection.onDisconnect(() => {
|
|
3004
|
+
closed = true;
|
|
3005
|
+
unsubscribeDisconnect();
|
|
3006
|
+
});
|
|
3007
|
+
const ensureOpen = () => {
|
|
3008
|
+
if (closed) throw new QueueError("Queue item is no longer valid after disconnect", "ITEM_CLOSED");
|
|
3009
|
+
};
|
|
2662
3010
|
const extend = async (leaseSecs, signal) => {
|
|
3011
|
+
ensureOpen();
|
|
2663
3012
|
const payload = QueueCodec.encodeExtend(route, id, token, leaseSecs);
|
|
2664
3013
|
const response = await connection.request(203, payload, signal);
|
|
2665
3014
|
const decoded = QueueCodec.decodeExtendResponse(response);
|
|
@@ -2670,6 +3019,7 @@ function createQueueItem(id, token, body, route, connection) {
|
|
|
2670
3019
|
}
|
|
2671
3020
|
};
|
|
2672
3021
|
const complete = async (signal) => {
|
|
3022
|
+
ensureOpen();
|
|
2673
3023
|
const requestPayload = QueueCodec.encodeComplete(route, id, token);
|
|
2674
3024
|
const response = await connection.request(204, requestPayload, signal);
|
|
2675
3025
|
const decoded = QueueCodec.decodeCompleteResponse(response);
|
|
@@ -2678,9 +3028,12 @@ function createQueueItem(id, token, body, route, connection) {
|
|
|
2678
3028
|
const statusName = QueueStatus[errorCode] || `Unknown(${errorCode})`;
|
|
2679
3029
|
throw new QueueError(`COMPLETE failed: ${decoded.errorMessage ?? statusName}`, statusName, errorCode);
|
|
2680
3030
|
}
|
|
3031
|
+
closed = true;
|
|
3032
|
+
unsubscribeDisconnect();
|
|
2681
3033
|
};
|
|
2682
3034
|
const testOnlyInvalidToken = () => id + 1n;
|
|
2683
3035
|
const testOnlyCompleteWithToken = async (tokenToUse, signal) => {
|
|
3036
|
+
ensureOpen();
|
|
2684
3037
|
const requestPayload = QueueCodec.encodeComplete(route, id, tokenToUse);
|
|
2685
3038
|
const response = await connection.request(204, requestPayload, signal);
|
|
2686
3039
|
const decoded = QueueCodec.decodeCompleteResponse(response);
|
|
@@ -2689,6 +3042,8 @@ function createQueueItem(id, token, body, route, connection) {
|
|
|
2689
3042
|
const statusName = QueueStatus[errorCode] || `Unknown(${errorCode})`;
|
|
2690
3043
|
throw new QueueError(`COMPLETE failed: ${decoded.errorMessage ?? statusName}`, statusName, errorCode);
|
|
2691
3044
|
}
|
|
3045
|
+
closed = true;
|
|
3046
|
+
unsubscribeDisconnect();
|
|
2692
3047
|
};
|
|
2693
3048
|
return {
|
|
2694
3049
|
body,
|
|
@@ -2726,7 +3081,7 @@ let QueueStatus = /* @__PURE__ */ function(QueueStatus) {
|
|
|
2726
3081
|
* Queue domain client.
|
|
2727
3082
|
*/
|
|
2728
3083
|
function createQueueClient(connection) {
|
|
2729
|
-
const { requestFrame } = createDomainClient(connection);
|
|
3084
|
+
const { requestFrame, requestReconnectFrame, runWithRetry } = createDomainClient(connection);
|
|
2730
3085
|
const subscriptionsByPattern = /* @__PURE__ */ new Map();
|
|
2731
3086
|
const patternsBySubId = /* @__PURE__ */ new Map();
|
|
2732
3087
|
let notificationHandlerRegistered = false;
|
|
@@ -2740,7 +3095,7 @@ function createQueueClient(connection) {
|
|
|
2740
3095
|
subscriptionsByPattern.clear();
|
|
2741
3096
|
patternsBySubId.clear();
|
|
2742
3097
|
for (const subscription of subscriptions) {
|
|
2743
|
-
const subId = await subscribeWire(subscription.pattern);
|
|
3098
|
+
const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
|
|
2744
3099
|
subscriptionsByPattern.set(subscription.pattern, {
|
|
2745
3100
|
subId,
|
|
2746
3101
|
handlers: new Map(subscription.handlers)
|
|
@@ -2750,11 +3105,17 @@ function createQueueClient(connection) {
|
|
|
2750
3105
|
});
|
|
2751
3106
|
const enqueue = async (route, body, options) => {
|
|
2752
3107
|
assertQueueRoute(route);
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
3108
|
+
return runWithRetry({
|
|
3109
|
+
domain: "queue",
|
|
3110
|
+
operation: "enqueue",
|
|
3111
|
+
retryClass: "confirmed_negative_retry"
|
|
3112
|
+
}, async () => {
|
|
3113
|
+
const response = await requestFrame(200, QueueCodec.encodeEnqueue(route, body, options));
|
|
3114
|
+
const decoded = QueueCodec.decodeEnqueueResponse(response);
|
|
3115
|
+
checkStatus(decoded, "ENQUEUE");
|
|
3116
|
+
if (decoded.messageId === void 0) throw new QueueError("ENQUEUE response missing messageId", "MISSING_MESSAGE_ID");
|
|
3117
|
+
return decoded.messageId;
|
|
3118
|
+
});
|
|
2758
3119
|
};
|
|
2759
3120
|
const reserve = async (route, leaseSeconds, batchSize = 1, waitSeconds = 0) => {
|
|
2760
3121
|
assertQueueReserveRoute(route);
|
|
@@ -2810,8 +3171,8 @@ function createQueueClient(connection) {
|
|
|
2810
3171
|
if (existing) return addLocalSubscription(pattern, existing.subId, handler);
|
|
2811
3172
|
return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
|
|
2812
3173
|
};
|
|
2813
|
-
const subscribeWire = async (pattern) => {
|
|
2814
|
-
const response = await
|
|
3174
|
+
const subscribeWire = async (pattern, request = requestFrame) => {
|
|
3175
|
+
const response = await request(207, QueueCodec.encodeSubscribe(wireWatchPattern(pattern)));
|
|
2815
3176
|
const decoded = QueueCodec.decodeSubscribeResponse(response);
|
|
2816
3177
|
checkStatus(decoded, "SUBSCRIBE");
|
|
2817
3178
|
if (decoded.subId === void 0) throw new QueueError("SUBSCRIBE response missing subId", "MISSING_SUB_ID");
|
|
@@ -2878,7 +3239,11 @@ function createQueueClient(connection) {
|
|
|
2878
3239
|
[4]: "QueueFull",
|
|
2879
3240
|
[5]: "InvalidDelay"
|
|
2880
3241
|
}[errorCode] ?? `Unknown(${errorCode})`;
|
|
2881
|
-
throw new QueueError(`${operation} failed: ${response.errorMessage ?? statusName}`, statusName, errorCode)
|
|
3242
|
+
throw attachResilienceMeta(new QueueError(`${operation} failed: ${response.errorMessage ?? statusName}`, statusName, errorCode), {
|
|
3243
|
+
boundary: "post-send",
|
|
3244
|
+
failureKind: "domain",
|
|
3245
|
+
explicitNegative: true
|
|
3246
|
+
});
|
|
2882
3247
|
};
|
|
2883
3248
|
return {
|
|
2884
3249
|
enqueue,
|
|
@@ -3193,16 +3558,35 @@ function createRpcSubscription(route, unsubscribeFn) {
|
|
|
3193
3558
|
*/
|
|
3194
3559
|
function createRpcResponseWriter(connection, correlationId) {
|
|
3195
3560
|
let sequence = 0n;
|
|
3561
|
+
let stale = false;
|
|
3562
|
+
let unsubscribeDisconnect = () => void 0;
|
|
3563
|
+
const dispose = () => {
|
|
3564
|
+
if (stale) return;
|
|
3565
|
+
stale = true;
|
|
3566
|
+
unsubscribeDisconnect();
|
|
3567
|
+
unsubscribeDisconnect = () => void 0;
|
|
3568
|
+
};
|
|
3569
|
+
unsubscribeDisconnect = connection.onDisconnect(() => {
|
|
3570
|
+
dispose();
|
|
3571
|
+
});
|
|
3196
3572
|
const send = async (body, isEnd) => {
|
|
3573
|
+
if (stale) throw new ConnectionError("RPC response writer is no longer valid");
|
|
3197
3574
|
const payload = RpcCodec.encodeResponse(correlationId, sequence++, body, isEnd);
|
|
3198
3575
|
try {
|
|
3199
3576
|
await connection.send(303, payload);
|
|
3577
|
+
if (isEnd) dispose();
|
|
3200
3578
|
} catch (error) {
|
|
3201
|
-
if (isBenignShutdownError(error, connection))
|
|
3579
|
+
if (isBenignShutdownError(error, connection)) {
|
|
3580
|
+
dispose();
|
|
3581
|
+
return;
|
|
3582
|
+
}
|
|
3202
3583
|
throw error;
|
|
3203
3584
|
}
|
|
3204
3585
|
};
|
|
3205
|
-
return {
|
|
3586
|
+
return {
|
|
3587
|
+
send,
|
|
3588
|
+
dispose
|
|
3589
|
+
};
|
|
3206
3590
|
}
|
|
3207
3591
|
function isBenignShutdownError(error, connection) {
|
|
3208
3592
|
if (connection.getState() !== "AUTHENTICATED") return true;
|
|
@@ -3350,7 +3734,7 @@ const RpcClient = function(connection) {
|
|
|
3350
3734
|
return createRpcClient(connection);
|
|
3351
3735
|
};
|
|
3352
3736
|
function createRpcClient(connection) {
|
|
3353
|
-
const { requestFrame } = createDomainClient(connection);
|
|
3737
|
+
const { requestFrame, requestReconnectFrame } = createDomainClient(connection);
|
|
3354
3738
|
const pendingRpcs = /* @__PURE__ */ new Map();
|
|
3355
3739
|
const workers = /* @__PURE__ */ new Map();
|
|
3356
3740
|
let initialized = false;
|
|
@@ -3368,7 +3752,7 @@ function createRpcClient(connection) {
|
|
|
3368
3752
|
if (workers.size === 0) return;
|
|
3369
3753
|
const registeredWorkers = Array.from(workers.entries());
|
|
3370
3754
|
workers.clear();
|
|
3371
|
-
for (const [route, handler] of registeredWorkers) await
|
|
3755
|
+
for (const [route, handler] of registeredWorkers) await registerWorkerInternal(route, handler, requestReconnectFrame);
|
|
3372
3756
|
});
|
|
3373
3757
|
const call = async (route, body, options) => {
|
|
3374
3758
|
assertRpcRoute(route);
|
|
@@ -3394,13 +3778,16 @@ function createRpcClient(connection) {
|
|
|
3394
3778
|
throw error;
|
|
3395
3779
|
}
|
|
3396
3780
|
};
|
|
3397
|
-
const
|
|
3398
|
-
|
|
3399
|
-
initRpcHandler();
|
|
3400
|
-
const response = await requestFrame(300, RpcCodec.encodeSubscribeWorker(route));
|
|
3781
|
+
const registerWorkerInternal = async (route, handler, request = requestFrame) => {
|
|
3782
|
+
const response = await request(300, RpcCodec.encodeSubscribeWorker(route));
|
|
3401
3783
|
const decoded = RpcCodec.decodeSubscribeWorkerResponse(response);
|
|
3402
3784
|
if (decoded.status !== 0) throw new RpcError(`RPC SUBSCRIBE_WORKER failed: status ${decoded.status}`, "SUBSCRIBE_FAILED", decoded.status);
|
|
3403
3785
|
workers.set(route, handler);
|
|
3786
|
+
};
|
|
3787
|
+
const registerWorker = async (route, handler) => {
|
|
3788
|
+
assertRpcRoute(route);
|
|
3789
|
+
initRpcHandler();
|
|
3790
|
+
await registerWorkerInternal(route, handler);
|
|
3404
3791
|
const unsubscribeFn = async (registeredRoute) => {
|
|
3405
3792
|
await unregisterWorker(registeredRoute);
|
|
3406
3793
|
};
|
|
@@ -3469,6 +3856,8 @@ function createRpcClient(connection) {
|
|
|
3469
3856
|
try {
|
|
3470
3857
|
await writer.send(utf8Encoder.encode(`Handler error: ${message}`), true);
|
|
3471
3858
|
} catch {}
|
|
3859
|
+
} finally {
|
|
3860
|
+
writer.dispose();
|
|
3472
3861
|
}
|
|
3473
3862
|
});
|
|
3474
3863
|
};
|
|
@@ -3704,7 +4093,17 @@ function createLeaseSubscription(subId, pattern, unsubscribeFn) {
|
|
|
3704
4093
|
function createLease(token, expiresAt, route, connection) {
|
|
3705
4094
|
let currentToken = token;
|
|
3706
4095
|
let currentExpiry = expiresAt;
|
|
4096
|
+
let closed = false;
|
|
4097
|
+
let unsubscribeDisconnect = () => void 0;
|
|
4098
|
+
unsubscribeDisconnect = connection.onDisconnect(() => {
|
|
4099
|
+
closed = true;
|
|
4100
|
+
unsubscribeDisconnect();
|
|
4101
|
+
});
|
|
4102
|
+
const ensureOpen = () => {
|
|
4103
|
+
if (closed) throw new LeaseError("Lease handle is no longer valid after disconnect", "CLOSED");
|
|
4104
|
+
};
|
|
3707
4105
|
const extend = async (ttlSecs, signal) => {
|
|
4106
|
+
ensureOpen();
|
|
3708
4107
|
const requestPayload = LeaseCodec.encodeExtend(route, currentToken, ttlSecs);
|
|
3709
4108
|
const data = assertSuccess(await connection.request(401, requestPayload, signal), "EXTEND");
|
|
3710
4109
|
if (data && data.length >= 8) currentToken = new BufferReader(data).readU64BE();
|
|
@@ -3712,12 +4111,16 @@ function createLease(token, expiresAt, route, connection) {
|
|
|
3712
4111
|
return currentExpiry;
|
|
3713
4112
|
};
|
|
3714
4113
|
const release = async (signal) => {
|
|
4114
|
+
ensureOpen();
|
|
3715
4115
|
const payload = LeaseCodec.encodeRelease(route, currentToken);
|
|
3716
4116
|
assertSuccess(await connection.request(402, payload, signal), "RELEASE");
|
|
4117
|
+
closed = true;
|
|
4118
|
+
unsubscribeDisconnect();
|
|
3717
4119
|
};
|
|
3718
4120
|
const getExpiry = () => currentExpiry;
|
|
3719
4121
|
const testOnlyInvalidToken = () => currentToken + 1n;
|
|
3720
4122
|
const testOnlyExtendWithToken = async (tokenToUse, ttlSecs, signal) => {
|
|
4123
|
+
ensureOpen();
|
|
3721
4124
|
const requestPayload = LeaseCodec.encodeExtend(route, tokenToUse, ttlSecs);
|
|
3722
4125
|
const data = assertSuccess(await connection.request(401, requestPayload, signal), "EXTEND");
|
|
3723
4126
|
if (data && data.length >= 8) currentToken = new BufferReader(data).readU64BE();
|
|
@@ -3725,8 +4128,11 @@ function createLease(token, expiresAt, route, connection) {
|
|
|
3725
4128
|
return currentExpiry;
|
|
3726
4129
|
};
|
|
3727
4130
|
const testOnlyReleaseWithToken = async (tokenToUse, signal) => {
|
|
4131
|
+
ensureOpen();
|
|
3728
4132
|
const payload = LeaseCodec.encodeRelease(route, tokenToUse);
|
|
3729
4133
|
assertSuccess(await connection.request(402, payload, signal), "RELEASE");
|
|
4134
|
+
closed = true;
|
|
4135
|
+
unsubscribeDisconnect();
|
|
3730
4136
|
};
|
|
3731
4137
|
return {
|
|
3732
4138
|
extend,
|
|
@@ -3743,7 +4149,7 @@ function createLease(token, expiresAt, route, connection) {
|
|
|
3743
4149
|
* Lease domain client.
|
|
3744
4150
|
*/
|
|
3745
4151
|
function createLeaseClient(connection) {
|
|
3746
|
-
const { requestFrame } = createDomainClient(connection);
|
|
4152
|
+
const { requestFrame, requestReconnectFrame, runWithRetry } = createDomainClient(connection);
|
|
3747
4153
|
const subscriptionsByPattern = /* @__PURE__ */ new Map();
|
|
3748
4154
|
let initialized = false;
|
|
3749
4155
|
let nextHandlerId = 1;
|
|
@@ -3755,7 +4161,7 @@ function createLeaseClient(connection) {
|
|
|
3755
4161
|
}));
|
|
3756
4162
|
subscriptionsByPattern.clear();
|
|
3757
4163
|
for (const subscription of subscriptions) {
|
|
3758
|
-
const subId = await subscribeWire(subscription.pattern);
|
|
4164
|
+
const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
|
|
3759
4165
|
subscriptionsByPattern.set(subscription.pattern, {
|
|
3760
4166
|
subId,
|
|
3761
4167
|
handlers: new Map(subscription.handlers)
|
|
@@ -3772,16 +4178,22 @@ function createLeaseClient(connection) {
|
|
|
3772
4178
|
};
|
|
3773
4179
|
const query = async (route) => {
|
|
3774
4180
|
assertExactLeaseRoute(route);
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
4181
|
+
return runWithRetry({
|
|
4182
|
+
domain: "lease",
|
|
4183
|
+
operation: "query",
|
|
4184
|
+
retryClass: "replayable_read"
|
|
4185
|
+
}, async () => {
|
|
4186
|
+
const response = await requestFrame(403, LeaseCodec.encodeQuery(route));
|
|
4187
|
+
const decoded = LeaseCodec.decodeQueryResponse(response);
|
|
4188
|
+
if (decoded.status !== 0) throw new LeaseError("QUERY failed", "QUERY_FAILED", decoded.status);
|
|
4189
|
+
return {
|
|
4190
|
+
isHeld: decoded.isHeld ?? false,
|
|
4191
|
+
owner: decoded.owner,
|
|
4192
|
+
token: decoded.token,
|
|
4193
|
+
ttlRemainingSecs: decoded.ttlRemainingSecs,
|
|
4194
|
+
expiresAt: decoded.expiresAt
|
|
4195
|
+
};
|
|
4196
|
+
});
|
|
3785
4197
|
};
|
|
3786
4198
|
const subscribe = async (pattern, handler) => {
|
|
3787
4199
|
assertExactLeaseRoute(pattern);
|
|
@@ -3790,8 +4202,8 @@ function createLeaseClient(connection) {
|
|
|
3790
4202
|
if (existing) return addLocalSubscription(pattern, existing.subId, handler);
|
|
3791
4203
|
return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
|
|
3792
4204
|
};
|
|
3793
|
-
const subscribeWire = async (pattern) => {
|
|
3794
|
-
const response = await
|
|
4205
|
+
const subscribeWire = async (pattern, request = requestFrame) => {
|
|
4206
|
+
const response = await request(407, LeaseCodec.encodeSubscribe(pattern));
|
|
3795
4207
|
const decoded = LeaseCodec.decodeSubscribeResponse(response);
|
|
3796
4208
|
if (decoded.subId === void 0) throw new LeaseError("SUBSCRIBE failed", "SUBSCRIBE_FAILED");
|
|
3797
4209
|
return decoded.subId;
|
|
@@ -3938,7 +4350,7 @@ function createNoticeSubscription(subId, pattern, unsubscribeFn) {
|
|
|
3938
4350
|
* Notice domain client.
|
|
3939
4351
|
*/
|
|
3940
4352
|
function createNoticeClient(connection) {
|
|
3941
|
-
const { requestFrame } = createDomainClient(connection);
|
|
4353
|
+
const { requestFrame, requestReconnectFrame } = createDomainClient(connection);
|
|
3942
4354
|
const subscriptionsByPattern = /* @__PURE__ */ new Map();
|
|
3943
4355
|
const patternsBySubId = /* @__PURE__ */ new Map();
|
|
3944
4356
|
let initialized = false;
|
|
@@ -3952,7 +4364,7 @@ function createNoticeClient(connection) {
|
|
|
3952
4364
|
subscriptionsByPattern.clear();
|
|
3953
4365
|
patternsBySubId.clear();
|
|
3954
4366
|
for (const subscription of subscriptions) {
|
|
3955
|
-
const subId = await subscribeWire(subscription.pattern);
|
|
4367
|
+
const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
|
|
3956
4368
|
subscriptionsByPattern.set(subscription.pattern, {
|
|
3957
4369
|
subId,
|
|
3958
4370
|
handlers: new Map(subscription.handlers)
|
|
@@ -3978,8 +4390,8 @@ function createNoticeClient(connection) {
|
|
|
3978
4390
|
if (existing) return addLocalSubscription(pattern, existing.subId, handler);
|
|
3979
4391
|
return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
|
|
3980
4392
|
};
|
|
3981
|
-
const subscribeWire = async (pattern) => {
|
|
3982
|
-
const response = await
|
|
4393
|
+
const subscribeWire = async (pattern, request = requestFrame) => {
|
|
4394
|
+
const response = await request(501, NoticeCodec.encodeSubscribe(pattern));
|
|
3983
4395
|
const decoded = NoticeCodec.decodeSubscribeResponse(response);
|
|
3984
4396
|
if (decoded.subId === void 0) throw new NoticeError("SUBSCRIBE response missing subId", "MISSING_SUB_ID");
|
|
3985
4397
|
return decoded.subId;
|
|
@@ -4144,7 +4556,7 @@ const StreamCodec = {
|
|
|
4144
4556
|
},
|
|
4145
4557
|
/**
|
|
4146
4558
|
* Encode READ request
|
|
4147
|
-
* Payload: [route: string][start_offset: u64][limit: u64][has_max_bytes: u8][max_bytes?: u64][has_filter: u8][filter_length?:
|
|
4559
|
+
* Payload: [route: string][start_offset: u64][limit: u64][has_max_bytes: u8][max_bytes?: u64][has_filter: u8][filter_length?: u32_be][filter?: custom]
|
|
4148
4560
|
*/
|
|
4149
4561
|
encodeRead(route, startOffset, limit, options) {
|
|
4150
4562
|
const writer = new BufferWriter(256);
|
|
@@ -4158,11 +4570,11 @@ const StreamCodec = {
|
|
|
4158
4570
|
const filter = options?.filter;
|
|
4159
4571
|
if (filter && filter.clauses.length > 0) {
|
|
4160
4572
|
writer.writeU8(1);
|
|
4161
|
-
const
|
|
4162
|
-
|
|
4163
|
-
const
|
|
4164
|
-
|
|
4165
|
-
writer.
|
|
4573
|
+
const filterWriter = new BufferWriter(64);
|
|
4574
|
+
encodeStreamFilterSet(filter, filterWriter);
|
|
4575
|
+
const filterBytes = filterWriter.getBufferView();
|
|
4576
|
+
writer.writeU32BE(filterBytes.length);
|
|
4577
|
+
writer.writeBytes(filterBytes);
|
|
4166
4578
|
} else writer.writeU8(0);
|
|
4167
4579
|
return writer.getBufferView();
|
|
4168
4580
|
},
|
|
@@ -4380,27 +4792,29 @@ const StreamCodec = {
|
|
|
4380
4792
|
}
|
|
4381
4793
|
};
|
|
4382
4794
|
function encodeStreamFilterSet(filter, writer) {
|
|
4383
|
-
writer.
|
|
4795
|
+
writer.writeU8(0);
|
|
4796
|
+
writer.writeU8(241);
|
|
4797
|
+
writer.writeU32BE(filter.clauses.length);
|
|
4384
4798
|
for (const clause of filter.clauses) encodeStreamFilterClause(writer, clause);
|
|
4385
4799
|
}
|
|
4386
4800
|
function encodeStreamFilterClause(writer, clause) {
|
|
4387
4801
|
switch (clause.kind) {
|
|
4388
4802
|
case "Equals":
|
|
4389
|
-
writer.
|
|
4390
|
-
writer.
|
|
4803
|
+
writer.writeU8(0);
|
|
4804
|
+
writer.writeString(clause.value);
|
|
4391
4805
|
return;
|
|
4392
4806
|
case "NotEquals":
|
|
4393
|
-
writer.
|
|
4394
|
-
writer.
|
|
4807
|
+
writer.writeU8(1);
|
|
4808
|
+
writer.writeString(clause.value);
|
|
4395
4809
|
return;
|
|
4396
4810
|
case "StartsWith":
|
|
4397
|
-
writer.
|
|
4398
|
-
writer.
|
|
4811
|
+
writer.writeU8(2);
|
|
4812
|
+
writer.writeString(clause.value);
|
|
4399
4813
|
return;
|
|
4400
4814
|
case "AnyOf":
|
|
4401
|
-
writer.
|
|
4402
|
-
writer.
|
|
4403
|
-
for (const value of clause.values) writer.
|
|
4815
|
+
writer.writeU8(3);
|
|
4816
|
+
writer.writeU32BE(clause.values.length);
|
|
4817
|
+
for (const value of clause.values) writer.writeString(value);
|
|
4404
4818
|
return;
|
|
4405
4819
|
}
|
|
4406
4820
|
}
|
|
@@ -4419,8 +4833,10 @@ function createStreamSubscription(subId, pattern, unsubscribeFn) {
|
|
|
4419
4833
|
//#region src/domains/stream/session.ts
|
|
4420
4834
|
function createStreamSession(connection, _route, sessionId) {
|
|
4421
4835
|
let closed = false;
|
|
4422
|
-
|
|
4836
|
+
let unsubscribeDisconnect = () => void 0;
|
|
4837
|
+
unsubscribeDisconnect = connection.onDisconnect(() => {
|
|
4423
4838
|
closed = true;
|
|
4839
|
+
unsubscribeDisconnect();
|
|
4424
4840
|
});
|
|
4425
4841
|
const ensureOpen = () => {
|
|
4426
4842
|
if (closed) throw new StreamError("Stream session already closed", "SESSION_CLOSED");
|
|
@@ -4494,7 +4910,7 @@ function isAbortSignal(value) {
|
|
|
4494
4910
|
* 3. `commit()` or `rollback()` finalizes the session
|
|
4495
4911
|
*/
|
|
4496
4912
|
function createStreamClient(connection) {
|
|
4497
|
-
const { requestFrame } = createDomainClient(connection);
|
|
4913
|
+
const { requestFrame, requestReconnectFrame, runWithRetry } = createDomainClient(connection);
|
|
4498
4914
|
const subscriptionsByPattern = /* @__PURE__ */ new Map();
|
|
4499
4915
|
const patternsBySubId = /* @__PURE__ */ new Map();
|
|
4500
4916
|
let initialized = false;
|
|
@@ -4508,7 +4924,7 @@ function createStreamClient(connection) {
|
|
|
4508
4924
|
subscriptionsByPattern.clear();
|
|
4509
4925
|
patternsBySubId.clear();
|
|
4510
4926
|
for (const subscription of snapshot) {
|
|
4511
|
-
const subId = await subscribeWire(subscription.pattern);
|
|
4927
|
+
const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
|
|
4512
4928
|
subscriptionsByPattern.set(subscription.pattern, {
|
|
4513
4929
|
subId,
|
|
4514
4930
|
handlers: new Map(subscription.handlers)
|
|
@@ -4526,18 +4942,25 @@ function createStreamClient(connection) {
|
|
|
4526
4942
|
};
|
|
4527
4943
|
const readPage = async (route, startOffset, limit = 100, options) => {
|
|
4528
4944
|
assertStreamPattern(route);
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4945
|
+
return runWithRetry({
|
|
4946
|
+
domain: "stream",
|
|
4947
|
+
operation: "read",
|
|
4948
|
+
retryClass: "replayable_read",
|
|
4949
|
+
signal: options?.signal
|
|
4950
|
+
}, async () => {
|
|
4951
|
+
const response = await requestFrame(604, StreamCodec.encodeRead(route, startOffset, limit, options), options?.signal);
|
|
4952
|
+
const decoded = StreamCodec.decodeReadResponse(response);
|
|
4953
|
+
checkStatus(decoded.status, "READ");
|
|
4954
|
+
return {
|
|
4955
|
+
items: decoded.items,
|
|
4956
|
+
cursor: decoded.cursor ?? {
|
|
4957
|
+
lastResourceOffset: startOffset,
|
|
4958
|
+
lastAreaOffset: void 0,
|
|
4959
|
+
lastRealmOffset: void 0,
|
|
4960
|
+
hasMore: false
|
|
4961
|
+
}
|
|
4962
|
+
};
|
|
4963
|
+
});
|
|
4541
4964
|
};
|
|
4542
4965
|
const read = async (route, startOffset, limit = 100, options) => {
|
|
4543
4966
|
const page = await readPage(route, startOffset, limit, options);
|
|
@@ -4548,21 +4971,33 @@ function createStreamClient(connection) {
|
|
|
4548
4971
|
};
|
|
4549
4972
|
const peek = async (route) => {
|
|
4550
4973
|
assertStreamRoute(route);
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4974
|
+
return runWithRetry({
|
|
4975
|
+
domain: "stream",
|
|
4976
|
+
operation: "last",
|
|
4977
|
+
retryClass: "replayable_read"
|
|
4978
|
+
}, async () => {
|
|
4979
|
+
const response = await requestFrame(605, StreamCodec.encodeLast(route));
|
|
4980
|
+
const decoded = StreamCodec.decodeLastResponse(response);
|
|
4981
|
+
checkStatus(decoded.status, "LAST");
|
|
4982
|
+
return decoded.record ?? null;
|
|
4983
|
+
});
|
|
4555
4984
|
};
|
|
4556
4985
|
const metadata = async (route) => {
|
|
4557
4986
|
assertStreamRoute(route);
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4987
|
+
return runWithRetry({
|
|
4988
|
+
domain: "stream",
|
|
4989
|
+
operation: "metadata",
|
|
4990
|
+
retryClass: "replayable_read"
|
|
4991
|
+
}, async () => {
|
|
4992
|
+
const response = await requestFrame(606, StreamCodec.encodeMetadata(route));
|
|
4993
|
+
const decoded = StreamCodec.decodeMetadataResponse(response);
|
|
4994
|
+
checkStatus(decoded.status, "GET_METADATA");
|
|
4995
|
+
return decoded.metadata ?? {
|
|
4996
|
+
firstOffset: 0n,
|
|
4997
|
+
lastOffset: 0n,
|
|
4998
|
+
recordCount: 0n
|
|
4999
|
+
};
|
|
5000
|
+
});
|
|
4566
5001
|
};
|
|
4567
5002
|
const subscribe = async (pattern, handler) => {
|
|
4568
5003
|
assertStreamPattern(pattern);
|
|
@@ -4571,8 +5006,8 @@ function createStreamClient(connection) {
|
|
|
4571
5006
|
if (existing) return addLocalSubscription(pattern, existing.subId, handler);
|
|
4572
5007
|
return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
|
|
4573
5008
|
};
|
|
4574
|
-
const subscribeWire = async (pattern) => {
|
|
4575
|
-
const response = await
|
|
5009
|
+
const subscribeWire = async (pattern, request = requestFrame) => {
|
|
5010
|
+
const response = await request(607, StreamCodec.encodeSubscribe(pattern));
|
|
4576
5011
|
const decoded = StreamCodec.decodeSubscribeResponse(response);
|
|
4577
5012
|
checkStatus(decoded.status, "SUBSCRIBE");
|
|
4578
5013
|
if (decoded.subId === void 0) throw new StreamError("SUBSCRIBE response missing subId", "MISSING_SESSION_ID");
|
|
@@ -4827,7 +5262,7 @@ var ScheduleError$1 = class extends Error {
|
|
|
4827
5262
|
* Schedule domain client.
|
|
4828
5263
|
*/
|
|
4829
5264
|
function createScheduleClient(connection) {
|
|
4830
|
-
const { requestFrame } = createDomainClient(connection);
|
|
5265
|
+
const { requestFrame, requestReconnectFrame } = createDomainClient(connection);
|
|
4831
5266
|
const subscriptionsByPattern = /* @__PURE__ */ new Map();
|
|
4832
5267
|
const patternsBySubId = /* @__PURE__ */ new Map();
|
|
4833
5268
|
let notifyHandlerInitialized = false;
|
|
@@ -4841,7 +5276,7 @@ function createScheduleClient(connection) {
|
|
|
4841
5276
|
subscriptionsByPattern.clear();
|
|
4842
5277
|
patternsBySubId.clear();
|
|
4843
5278
|
for (const subscription of subscriptions) {
|
|
4844
|
-
const subId = await subscribeWire(subscription.pattern);
|
|
5279
|
+
const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
|
|
4845
5280
|
subscriptionsByPattern.set(subscription.pattern, {
|
|
4846
5281
|
subId,
|
|
4847
5282
|
handlers: new Map(subscription.handlers)
|
|
@@ -4871,8 +5306,8 @@ function createScheduleClient(connection) {
|
|
|
4871
5306
|
if (existing) return addLocalSubscription(pattern, existing.subId, handler);
|
|
4872
5307
|
return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
|
|
4873
5308
|
};
|
|
4874
|
-
const subscribeWire = async (pattern) => {
|
|
4875
|
-
const response = await
|
|
5309
|
+
const subscribeWire = async (pattern, request = requestFrame) => {
|
|
5310
|
+
const response = await request(703, ScheduleCodec.encodeSubscribe(pattern));
|
|
4876
5311
|
return ScheduleCodec.decodeSubscribeResponse(assertSuccess(response, "SUBSCRIBE")).subId;
|
|
4877
5312
|
};
|
|
4878
5313
|
const addLocalSubscription = (pattern, subId, handler) => {
|
|
@@ -4945,6 +5380,39 @@ function assertConcreteScheduleRoute(route) {
|
|
|
4945
5380
|
}
|
|
4946
5381
|
//#endregion
|
|
4947
5382
|
//#region src/client/client.ts
|
|
5383
|
+
const abortError = () => {
|
|
5384
|
+
const error = /* @__PURE__ */ new Error("The operation was aborted");
|
|
5385
|
+
error.name = "AbortError";
|
|
5386
|
+
return error;
|
|
5387
|
+
};
|
|
5388
|
+
const throwIfAborted = (signal) => {
|
|
5389
|
+
if (signal?.aborted) throw abortError();
|
|
5390
|
+
};
|
|
5391
|
+
const waitForSharedPromise = async (promise, signal) => {
|
|
5392
|
+
if (!signal) return promise;
|
|
5393
|
+
if (signal.aborted) throw abortError();
|
|
5394
|
+
return await new Promise((resolve, reject) => {
|
|
5395
|
+
let settled = false;
|
|
5396
|
+
const cleanup = () => {
|
|
5397
|
+
signal.removeEventListener("abort", onAbort);
|
|
5398
|
+
};
|
|
5399
|
+
const settle = (callback) => {
|
|
5400
|
+
if (settled) return;
|
|
5401
|
+
settled = true;
|
|
5402
|
+
cleanup();
|
|
5403
|
+
callback();
|
|
5404
|
+
};
|
|
5405
|
+
const onAbort = () => {
|
|
5406
|
+
settle(() => reject(abortError()));
|
|
5407
|
+
};
|
|
5408
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
5409
|
+
promise.then((value) => {
|
|
5410
|
+
settle(() => resolve(value));
|
|
5411
|
+
}, (error) => {
|
|
5412
|
+
settle(() => reject(error));
|
|
5413
|
+
});
|
|
5414
|
+
});
|
|
5415
|
+
};
|
|
4948
5416
|
function createClient(config) {
|
|
4949
5417
|
const observability = config.observability;
|
|
4950
5418
|
const resolvedConfig = {
|
|
@@ -4956,12 +5424,19 @@ function createClient(config) {
|
|
|
4956
5424
|
maxRequestQueueSize: 1024,
|
|
4957
5425
|
observability: config.observability ?? {},
|
|
4958
5426
|
reconnect: {
|
|
4959
|
-
enabled:
|
|
5427
|
+
enabled: true,
|
|
4960
5428
|
maxAttempts: Infinity,
|
|
4961
5429
|
backoffMs: 250,
|
|
4962
5430
|
maxBackoffMs: 5e3,
|
|
4963
5431
|
...config.reconnect
|
|
4964
5432
|
},
|
|
5433
|
+
retry: {
|
|
5434
|
+
enabled: true,
|
|
5435
|
+
maxAttempts: 3,
|
|
5436
|
+
backoffMs: 100,
|
|
5437
|
+
maxBackoffMs: 1e3,
|
|
5438
|
+
...config.retry
|
|
5439
|
+
},
|
|
4965
5440
|
asyncHandlers: {
|
|
4966
5441
|
maxConcurrency: Infinity,
|
|
4967
5442
|
timeoutMs: 3e4,
|
|
@@ -4978,16 +5453,19 @@ function createClient(config) {
|
|
|
4978
5453
|
let noticeClient = null;
|
|
4979
5454
|
let streamClient = null;
|
|
4980
5455
|
let scheduleClient = null;
|
|
5456
|
+
let clientClosed = false;
|
|
5457
|
+
let pendingConnectPromise = null;
|
|
4981
5458
|
const resolveTokenProvider = () => {
|
|
4982
5459
|
if (resolvedConfig.tokenProvider) return resolvedConfig.tokenProvider;
|
|
4983
5460
|
return () => "";
|
|
4984
5461
|
};
|
|
4985
5462
|
const ensureConnection = () => {
|
|
5463
|
+
if (clientClosed) throw new ConnectionError("Client is closed", { state: "CLOSED" });
|
|
4986
5464
|
if (!connection) throw new ConnectionError("Not connected to Fitz server. Call connect() first.", { state: getState() });
|
|
4987
5465
|
return connection;
|
|
4988
5466
|
};
|
|
4989
|
-
const
|
|
4990
|
-
if (connection
|
|
5467
|
+
const createOwnedConnection = () => {
|
|
5468
|
+
if (connection) return connection;
|
|
4991
5469
|
connection = createConnection(() => createTransport(resolvedConfig.url, resolvedConfig.transport, {
|
|
4992
5470
|
timeout: resolvedConfig.timeout,
|
|
4993
5471
|
maxFrameSize: resolvedConfig.maxFrameSize
|
|
@@ -4997,15 +5475,48 @@ function createClient(config) {
|
|
|
4997
5475
|
maxInFlightRequests: resolvedConfig.maxInFlightRequests,
|
|
4998
5476
|
maxRequestQueueSize: resolvedConfig.maxRequestQueueSize,
|
|
4999
5477
|
reconnect: resolvedConfig.reconnect,
|
|
5478
|
+
retry: resolvedConfig.retry,
|
|
5000
5479
|
observability,
|
|
5001
5480
|
asyncHandlers: resolvedConfig.asyncHandlers
|
|
5002
5481
|
});
|
|
5003
|
-
|
|
5482
|
+
return connection;
|
|
5483
|
+
};
|
|
5484
|
+
const connect = async (options = {}) => {
|
|
5485
|
+
if (clientClosed) throw new ConnectionError("Client is closed", { state: "CLOSED" });
|
|
5486
|
+
throwIfAborted(options.signal);
|
|
5487
|
+
const activeConnection = createOwnedConnection();
|
|
5488
|
+
if (activeConnection.isConnected()) return;
|
|
5489
|
+
if (pendingConnectPromise) {
|
|
5490
|
+
await waitForSharedPromise(pendingConnectPromise, options.signal);
|
|
5491
|
+
return;
|
|
5492
|
+
}
|
|
5493
|
+
const state = activeConnection.getState();
|
|
5494
|
+
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(() => {
|
|
5495
|
+
if (pendingConnectPromise === trackedConnectPromise) pendingConnectPromise = null;
|
|
5496
|
+
});
|
|
5497
|
+
pendingConnectPromise = trackedConnectPromise;
|
|
5498
|
+
await waitForSharedPromise(trackedConnectPromise, options.signal);
|
|
5004
5499
|
};
|
|
5005
5500
|
const close = async () => {
|
|
5501
|
+
if (clientClosed && !connection) {
|
|
5502
|
+
kvClient = null;
|
|
5503
|
+
queueClient = null;
|
|
5504
|
+
rpcClient = null;
|
|
5505
|
+
leaseClient = null;
|
|
5506
|
+
noticeClient = null;
|
|
5507
|
+
streamClient = null;
|
|
5508
|
+
scheduleClient = null;
|
|
5509
|
+
return;
|
|
5510
|
+
}
|
|
5511
|
+
clientClosed = true;
|
|
5512
|
+
pendingConnectPromise = null;
|
|
5006
5513
|
if (connection) {
|
|
5007
|
-
|
|
5008
|
-
|
|
5514
|
+
const activeConnection = connection;
|
|
5515
|
+
try {
|
|
5516
|
+
await activeConnection.close();
|
|
5517
|
+
} finally {
|
|
5518
|
+
if (connection === activeConnection) connection = null;
|
|
5519
|
+
}
|
|
5009
5520
|
}
|
|
5010
5521
|
kvClient = null;
|
|
5011
5522
|
queueClient = null;
|
|
@@ -5016,7 +5527,7 @@ function createClient(config) {
|
|
|
5016
5527
|
scheduleClient = null;
|
|
5017
5528
|
};
|
|
5018
5529
|
const isConnected = () => {
|
|
5019
|
-
return connection?.isConnected() ?? false;
|
|
5530
|
+
return !clientClosed && (connection?.isConnected() ?? false);
|
|
5020
5531
|
};
|
|
5021
5532
|
const kv = () => {
|
|
5022
5533
|
const activeConnection = ensureConnection();
|
|
@@ -5057,7 +5568,10 @@ function createClient(config) {
|
|
|
5057
5568
|
return ensureConnection().getUrl();
|
|
5058
5569
|
};
|
|
5059
5570
|
const getState = () => {
|
|
5060
|
-
|
|
5571
|
+
if (clientClosed) return "CLOSED";
|
|
5572
|
+
if (!connection) return "DISCONNECTED";
|
|
5573
|
+
const state = connection.getState();
|
|
5574
|
+
return state === "CLOSED" ? "DISCONNECTED" : state;
|
|
5061
5575
|
};
|
|
5062
5576
|
return {
|
|
5063
5577
|
config: resolvedConfig,
|