@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.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$2();
|
|
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$2());
|
|
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$2();
|
|
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$2();
|
|
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$2() {
|
|
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$1();
|
|
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$1());
|
|
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$1();
|
|
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$1());
|
|
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$1() {
|
|
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$1();
|
|
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$1();
|
|
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$1()));
|
|
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,10 @@ 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;
|
|
1399
1491
|
const maxInFlightRequests = options.maxInFlightRequests ?? 256;
|
|
1400
1492
|
const maxRequestQueueSize = options.maxRequestQueueSize ?? 1024;
|
|
1401
1493
|
const observability = options.observability;
|
|
@@ -1409,10 +1501,18 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1409
1501
|
let receiveLoop = null;
|
|
1410
1502
|
let receiveLoopAbort = false;
|
|
1411
1503
|
let closeRequested = false;
|
|
1504
|
+
let permanentlyClosed = false;
|
|
1505
|
+
let connectPromise = null;
|
|
1412
1506
|
let reconnectPromise = null;
|
|
1507
|
+
let reconnectRestoreActive = false;
|
|
1413
1508
|
let authOutcome = null;
|
|
1414
1509
|
let authRejected = false;
|
|
1510
|
+
let hasEstablishedSession = false;
|
|
1511
|
+
let reconnectExhausted = false;
|
|
1512
|
+
let readyWaiterCount = 0;
|
|
1513
|
+
const closeAbortController = new AbortController();
|
|
1415
1514
|
const connectionScope = createScope("connection");
|
|
1515
|
+
const readyListeners = /* @__PURE__ */ new Set();
|
|
1416
1516
|
const log = (level, event, fields) => {
|
|
1417
1517
|
observability?.logger?.log(level, event, fields);
|
|
1418
1518
|
};
|
|
@@ -1443,6 +1543,8 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1443
1543
|
};
|
|
1444
1544
|
const handlePossibleTransportFailure = (error) => {
|
|
1445
1545
|
if (closeRequested) return;
|
|
1546
|
+
if (state !== "AUTHENTICATED") return;
|
|
1547
|
+
if (getResilienceMeta(error)?.boundary === "pre-send") return;
|
|
1446
1548
|
if (error instanceof TransportError || error instanceof ConnectionError || error instanceof AuthenticationError) handleConnectionLoss(error);
|
|
1447
1549
|
};
|
|
1448
1550
|
const multiplexer = new Multiplexer({
|
|
@@ -1453,17 +1555,34 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1453
1555
|
log("warn", "fitz.connection.handler_failed", { error: describeError(error) });
|
|
1454
1556
|
});
|
|
1455
1557
|
const connect = async (options = {}) => {
|
|
1456
|
-
closeRequested
|
|
1558
|
+
if (permanentlyClosed || closeRequested) throw connectionClosedError();
|
|
1559
|
+
throwIfAborted$1(options.signal);
|
|
1560
|
+
if (state === "AUTHENTICATED") return;
|
|
1561
|
+
if (connectPromise) {
|
|
1562
|
+
await waitForSharedPromise$1(connectPromise, options.signal);
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
if (reconnectPromise || state === "CONNECTING" || state === "CONNECTED" || state === "AUTHENTICATING" || state === "RECONNECTING" || state === "DISCONNECTED" && canWaitForReconnect()) {
|
|
1566
|
+
await waitForReady(options.signal, timeout);
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1457
1569
|
authRejected = false;
|
|
1458
|
-
|
|
1570
|
+
reconnectExhausted = false;
|
|
1571
|
+
const sharedConnectPromise = openAndAuthenticate(false, options.signal).finally(() => {
|
|
1572
|
+
if (connectPromise === sharedConnectPromise) connectPromise = null;
|
|
1573
|
+
});
|
|
1574
|
+
connectPromise = sharedConnectPromise;
|
|
1575
|
+
await sharedConnectPromise;
|
|
1459
1576
|
};
|
|
1460
1577
|
const close = async () => {
|
|
1461
1578
|
if (state === "CLOSED" && !transport) {
|
|
1462
1579
|
await connectionScope.dispose();
|
|
1463
1580
|
return;
|
|
1464
1581
|
}
|
|
1582
|
+
permanentlyClosed = true;
|
|
1465
1583
|
closeRequested = true;
|
|
1466
1584
|
receiveLoopAbort = true;
|
|
1585
|
+
closeAbortController.abort();
|
|
1467
1586
|
asyncHandlerDispatcher.close();
|
|
1468
1587
|
const scopeDisposePromise = connectionScope.dispose();
|
|
1469
1588
|
setState("CLOSED");
|
|
@@ -1484,15 +1603,34 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1484
1603
|
await asyncHandlerDispatcher.drain();
|
|
1485
1604
|
await scopeDisposePromise;
|
|
1486
1605
|
};
|
|
1487
|
-
const
|
|
1606
|
+
const waitForRequestReady = async (signal, allowReconnectRestore = false) => {
|
|
1607
|
+
if (allowReconnectRestore && reconnectRestoreActive && state === "AUTHENTICATING" && !closeRequested && !authRejected && transport) return;
|
|
1608
|
+
const releaseReadyWaitSlot = acquireReadyWaitSlot();
|
|
1609
|
+
try {
|
|
1610
|
+
await waitForReady(signal, timeout);
|
|
1611
|
+
} finally {
|
|
1612
|
+
releaseReadyWaitSlot?.();
|
|
1613
|
+
}
|
|
1488
1614
|
ensureAuthenticated();
|
|
1615
|
+
};
|
|
1616
|
+
const requestInternal = async (messageType, requestPayload, signal, allowReconnectRestore = false) => {
|
|
1617
|
+
let sendStarted = false;
|
|
1618
|
+
await waitForRequestReady(signal, allowReconnectRestore);
|
|
1489
1619
|
const releaseRequestSlot = await requestGate.acquire(signal);
|
|
1490
1620
|
const startedAt = Date.now();
|
|
1491
1621
|
try {
|
|
1492
1622
|
const activeTransport = ensureTransport();
|
|
1493
1623
|
const frame = FrameCodec.encodeFrame(messageType, requestPayload);
|
|
1494
|
-
return await multiplexer.request(messageType, frame, (data) =>
|
|
1624
|
+
return await multiplexer.request(messageType, frame, (data) => {
|
|
1625
|
+
sendStarted = true;
|
|
1626
|
+
return sendSerialized(activeTransport, data);
|
|
1627
|
+
}, timeout, signal);
|
|
1495
1628
|
} catch (error) {
|
|
1629
|
+
attachResilienceMeta(error, {
|
|
1630
|
+
boundary: sendStarted ? "post-send" : "pre-send",
|
|
1631
|
+
failureKind: classifyFailureKind(error),
|
|
1632
|
+
explicitNegative: false
|
|
1633
|
+
});
|
|
1496
1634
|
log("error", "fitz.connection.request_failed", {
|
|
1497
1635
|
operation: "request",
|
|
1498
1636
|
state,
|
|
@@ -1507,13 +1645,34 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1507
1645
|
releaseRequestSlot();
|
|
1508
1646
|
}
|
|
1509
1647
|
};
|
|
1510
|
-
const
|
|
1648
|
+
const request = async (messageType, requestPayload, signal) => {
|
|
1649
|
+
return await requestInternal(messageType, requestPayload, signal);
|
|
1650
|
+
};
|
|
1651
|
+
const requestDuringReconnectRestore = async (messageType, requestPayload, signal) => {
|
|
1652
|
+
return await requestInternal(messageType, requestPayload, signal, true);
|
|
1653
|
+
};
|
|
1654
|
+
const send = async (messageType, requestPayload, signal) => {
|
|
1655
|
+
let sendStarted = false;
|
|
1656
|
+
const releaseReadyWaitSlot = acquireReadyWaitSlot();
|
|
1657
|
+
try {
|
|
1658
|
+
await waitForReady(signal, timeout);
|
|
1659
|
+
} finally {
|
|
1660
|
+
releaseReadyWaitSlot?.();
|
|
1661
|
+
}
|
|
1511
1662
|
ensureAuthenticated();
|
|
1512
|
-
const releaseRequestSlot = await requestGate.acquire();
|
|
1663
|
+
const releaseRequestSlot = await requestGate.acquire(signal);
|
|
1513
1664
|
const startedAt = Date.now();
|
|
1514
1665
|
try {
|
|
1515
|
-
|
|
1666
|
+
const activeTransport = ensureTransport();
|
|
1667
|
+
const frame = FrameCodec.encodeFrame(messageType, requestPayload);
|
|
1668
|
+
sendStarted = true;
|
|
1669
|
+
await sendSerialized(activeTransport, frame);
|
|
1516
1670
|
} catch (error) {
|
|
1671
|
+
attachResilienceMeta(error, {
|
|
1672
|
+
boundary: sendStarted ? "post-send" : "pre-send",
|
|
1673
|
+
failureKind: classifyFailureKind(error),
|
|
1674
|
+
explicitNegative: false
|
|
1675
|
+
});
|
|
1517
1676
|
log("error", "fitz.connection.send_failed", {
|
|
1518
1677
|
operation: "send",
|
|
1519
1678
|
state,
|
|
@@ -1528,8 +1687,8 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1528
1687
|
releaseRequestSlot();
|
|
1529
1688
|
}
|
|
1530
1689
|
};
|
|
1531
|
-
const sendFireAndForget = async (messageType, requestPayload) => {
|
|
1532
|
-
await send(messageType, requestPayload);
|
|
1690
|
+
const sendFireAndForget = async (messageType, requestPayload, signal) => {
|
|
1691
|
+
await send(messageType, requestPayload, signal);
|
|
1533
1692
|
};
|
|
1534
1693
|
const registerNotificationHandler = (messageType, handler) => {
|
|
1535
1694
|
multiplexer.registerNotificationHandler(messageType, handler);
|
|
@@ -1557,8 +1716,138 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1557
1716
|
const getState = () => state;
|
|
1558
1717
|
const isConnected = () => state === "AUTHENTICATED";
|
|
1559
1718
|
const getUrl = () => ensureTransport().getUrl();
|
|
1719
|
+
const canWaitForReconnect = () => {
|
|
1720
|
+
return reconnectEnabled && hasEstablishedSession && !reconnectExhausted && !authRejected;
|
|
1721
|
+
};
|
|
1722
|
+
const readyFailure = () => {
|
|
1723
|
+
if (state === "AUTHENTICATED") return null;
|
|
1724
|
+
if (closeRequested || state === "CLOSED") return connectionClosedError();
|
|
1725
|
+
if (authRejected) return new AuthenticationError("Authentication rejected", { state });
|
|
1726
|
+
if (state === "CONNECTING" || state === "CONNECTED" || state === "AUTHENTICATING" || state === "RECONNECTING") return null;
|
|
1727
|
+
if (state === "DISCONNECTED" && canWaitForReconnect()) return null;
|
|
1728
|
+
return new ConnectionError(`Cannot use connection while state is ${state}`, { state });
|
|
1729
|
+
};
|
|
1730
|
+
const notifyReadyListeners = () => {
|
|
1731
|
+
for (const listener of readyListeners) listener();
|
|
1732
|
+
};
|
|
1733
|
+
const acquireReadyWaitSlot = () => {
|
|
1734
|
+
const failure = readyFailure();
|
|
1735
|
+
if (state === "AUTHENTICATED" || failure) return null;
|
|
1736
|
+
if (readyWaiterCount >= maxRequestQueueSize) throw new RequestQueueFullError();
|
|
1737
|
+
readyWaiterCount += 1;
|
|
1738
|
+
let released = false;
|
|
1739
|
+
return () => {
|
|
1740
|
+
if (released) return;
|
|
1741
|
+
released = true;
|
|
1742
|
+
readyWaiterCount = Math.max(readyWaiterCount - 1, 0);
|
|
1743
|
+
};
|
|
1744
|
+
};
|
|
1745
|
+
const waitForReady = async (signal, waitTimeoutMs = timeout) => {
|
|
1746
|
+
throwIfAborted$1(signal);
|
|
1747
|
+
const immediateFailure = readyFailure();
|
|
1748
|
+
if (!immediateFailure) {
|
|
1749
|
+
if (state === "AUTHENTICATED") return;
|
|
1750
|
+
} else throw immediateFailure;
|
|
1751
|
+
await new Promise((resolve, reject) => {
|
|
1752
|
+
let settled = false;
|
|
1753
|
+
let timeoutId = null;
|
|
1754
|
+
const cleanup = () => {
|
|
1755
|
+
readyListeners.delete(onStateChange);
|
|
1756
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
1757
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1758
|
+
};
|
|
1759
|
+
const settle = (cb) => {
|
|
1760
|
+
if (settled) return;
|
|
1761
|
+
settled = true;
|
|
1762
|
+
cleanup();
|
|
1763
|
+
cb();
|
|
1764
|
+
};
|
|
1765
|
+
const onAbort = () => {
|
|
1766
|
+
settle(() => reject(abortError$1()));
|
|
1767
|
+
};
|
|
1768
|
+
const onStateChange = () => {
|
|
1769
|
+
const failure = readyFailure();
|
|
1770
|
+
if (state === "AUTHENTICATED") {
|
|
1771
|
+
settle(resolve);
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
if (failure) settle(() => reject(failure));
|
|
1775
|
+
};
|
|
1776
|
+
readyListeners.add(onStateChange);
|
|
1777
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1778
|
+
timeoutId = setTimeout(() => {
|
|
1779
|
+
settle(() => reject(new ConnectionError("Timed out waiting for connection to become ready", { state })));
|
|
1780
|
+
}, waitTimeoutMs);
|
|
1781
|
+
onStateChange();
|
|
1782
|
+
});
|
|
1783
|
+
};
|
|
1784
|
+
const waitUntilReady = async (signal, waitTimeoutMs = timeout) => {
|
|
1785
|
+
await waitForReady(signal, waitTimeoutMs);
|
|
1786
|
+
};
|
|
1787
|
+
const shouldWaitForReconnect = () => {
|
|
1788
|
+
return reconnectPromise !== null || state === "RECONNECTING" || state === "DISCONNECTED" && canWaitForReconnect();
|
|
1789
|
+
};
|
|
1790
|
+
const getRetryDelayMs = (baseDelayMs) => {
|
|
1791
|
+
const jitter = Math.floor(Math.random() * baseDelayMs * .5);
|
|
1792
|
+
return Math.min(Math.max(baseDelayMs + jitter, 1), retryMaxBackoffMs);
|
|
1793
|
+
};
|
|
1794
|
+
const recordRetry = (operation, attempt, delayMs, error) => {
|
|
1795
|
+
const meta = getResilienceMeta(error);
|
|
1796
|
+
log("warn", "fitz.request.retry", {
|
|
1797
|
+
domain: operation.domain,
|
|
1798
|
+
operation: operation.operation,
|
|
1799
|
+
attempt,
|
|
1800
|
+
delayMs,
|
|
1801
|
+
boundary: meta?.boundary ?? "unknown",
|
|
1802
|
+
error: describeError(error),
|
|
1803
|
+
...describeErrorFields(error)
|
|
1804
|
+
});
|
|
1805
|
+
observability?.meter?.counter("fitz.request.retry", 1, {
|
|
1806
|
+
domain: operation.domain,
|
|
1807
|
+
operation: operation.operation,
|
|
1808
|
+
boundary: meta?.boundary ?? "unknown"
|
|
1809
|
+
});
|
|
1810
|
+
};
|
|
1811
|
+
const recordRetryExhausted = (operation, attempt, error) => {
|
|
1812
|
+
const meta = getResilienceMeta(error);
|
|
1813
|
+
log("warn", "fitz.request.retry_exhausted", {
|
|
1814
|
+
domain: operation.domain,
|
|
1815
|
+
operation: operation.operation,
|
|
1816
|
+
attempt,
|
|
1817
|
+
boundary: meta?.boundary ?? "unknown",
|
|
1818
|
+
error: describeError(error),
|
|
1819
|
+
...describeErrorFields(error)
|
|
1820
|
+
});
|
|
1821
|
+
observability?.meter?.counter("fitz.request.retry_exhausted", 1, {
|
|
1822
|
+
domain: operation.domain,
|
|
1823
|
+
operation: operation.operation,
|
|
1824
|
+
boundary: meta?.boundary ?? "unknown"
|
|
1825
|
+
});
|
|
1826
|
+
};
|
|
1827
|
+
const executeWithRetry = async (operation, task) => {
|
|
1828
|
+
if (!retryEnabled || operation.retryClass === "wait_only") return task();
|
|
1829
|
+
let attempt = 0;
|
|
1830
|
+
let delayMs = retryBackoffMs;
|
|
1831
|
+
while (true) {
|
|
1832
|
+
attempt += 1;
|
|
1833
|
+
try {
|
|
1834
|
+
return await task();
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
if (isAbortError$1(error)) throw error;
|
|
1837
|
+
if (!shouldRetryOperation(operation.retryClass, error)) throw error;
|
|
1838
|
+
if (attempt >= retryMaxAttempts) {
|
|
1839
|
+
recordRetryExhausted(operation, attempt, error);
|
|
1840
|
+
throw error;
|
|
1841
|
+
}
|
|
1842
|
+
const actualDelayMs = getRetryDelayMs(delayMs);
|
|
1843
|
+
recordRetry(operation, attempt, actualDelayMs, error);
|
|
1844
|
+
await sleepWithAbort(actualDelayMs, operation.signal);
|
|
1845
|
+
delayMs = Math.min(delayMs * 2, retryMaxBackoffMs);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1560
1849
|
const openAndAuthenticate = async (isReconnect, signal) => {
|
|
1561
|
-
throwIfAborted(signal);
|
|
1850
|
+
throwIfAborted$1(signal);
|
|
1562
1851
|
receiveLoopAbort = false;
|
|
1563
1852
|
frameParser.parseFrames(new Uint8Array(0));
|
|
1564
1853
|
requestGate = createRequestGate(maxInFlightRequests, maxRequestQueueSize);
|
|
@@ -1572,7 +1861,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1572
1861
|
if (transport === activeTransport) transport = null;
|
|
1573
1862
|
throw connectionClosedError();
|
|
1574
1863
|
}
|
|
1575
|
-
throwIfAborted(signal);
|
|
1864
|
+
throwIfAborted$1(signal);
|
|
1576
1865
|
receiveLoop = startReceiveLoop();
|
|
1577
1866
|
setState("CONNECTED");
|
|
1578
1867
|
setState("AUTHENTICATING");
|
|
@@ -1581,21 +1870,30 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1581
1870
|
try {
|
|
1582
1871
|
await sendConnect();
|
|
1583
1872
|
if (closeRequested) throw connectionClosedError();
|
|
1584
|
-
throwIfAborted(signal);
|
|
1873
|
+
throwIfAborted$1(signal);
|
|
1585
1874
|
await Promise.race([authOutcome.promise, sleep(authSettleDelayMs)]);
|
|
1586
1875
|
if (closeRequested) throw connectionClosedError();
|
|
1587
|
-
throwIfAborted(signal);
|
|
1876
|
+
throwIfAborted$1(signal);
|
|
1588
1877
|
authOutcome?.resolve();
|
|
1589
1878
|
authOutcome = null;
|
|
1590
1879
|
if (isReconnect) {
|
|
1591
|
-
|
|
1592
|
-
|
|
1880
|
+
multiplexer.setConnected();
|
|
1881
|
+
reconnectRestoreActive = true;
|
|
1882
|
+
try {
|
|
1883
|
+
await restoreReconnectState();
|
|
1884
|
+
if (closeRequested) throw connectionClosedError();
|
|
1885
|
+
} finally {
|
|
1886
|
+
reconnectRestoreActive = false;
|
|
1887
|
+
}
|
|
1593
1888
|
}
|
|
1889
|
+
hasEstablishedSession = true;
|
|
1890
|
+
reconnectExhausted = false;
|
|
1594
1891
|
setState("AUTHENTICATED");
|
|
1595
|
-
multiplexer.setConnected();
|
|
1892
|
+
if (!isReconnect) multiplexer.setConnected();
|
|
1596
1893
|
emitLifecycleEvent(isReconnect ? "reconnect_succeeded" : "connect_succeeded");
|
|
1597
1894
|
} catch (error) {
|
|
1598
1895
|
authOutcome = null;
|
|
1896
|
+
reconnectRestoreActive = false;
|
|
1599
1897
|
multiplexer.setDisconnected();
|
|
1600
1898
|
emitDisconnect();
|
|
1601
1899
|
if (activeTransport) await activeTransport.close().catch(() => void 0);
|
|
@@ -1605,7 +1903,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1605
1903
|
if (closeRequested) setState("CLOSED");
|
|
1606
1904
|
else setState(rejectedAuth ? "CLOSED" : "DISCONNECTED");
|
|
1607
1905
|
emitLifecycleEvent(isReconnect ? "reconnect_failed" : "connect_failed", error);
|
|
1608
|
-
if (isAbortError$1(error)) throw abortError();
|
|
1906
|
+
if (isAbortError$1(error)) throw abortError$1();
|
|
1609
1907
|
throw error;
|
|
1610
1908
|
}
|
|
1611
1909
|
};
|
|
@@ -1646,6 +1944,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1646
1944
|
emitLifecycleEvent("auth_rejected", error);
|
|
1647
1945
|
return;
|
|
1648
1946
|
}
|
|
1947
|
+
reconnectExhausted = false;
|
|
1649
1948
|
setState("DISCONNECTED");
|
|
1650
1949
|
emitLifecycleEvent("connection_lost", error);
|
|
1651
1950
|
if (!reconnectEnabled) return;
|
|
@@ -1667,12 +1966,13 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1667
1966
|
emitLifecycleEvent("reconnect_scheduled", void 0, attempts);
|
|
1668
1967
|
const actualDelayMs = getReconnectDelayMs(delayMs);
|
|
1669
1968
|
try {
|
|
1670
|
-
await
|
|
1969
|
+
await sleepWithAbort(actualDelayMs, closeAbortController.signal);
|
|
1671
1970
|
if (closeRequested) return;
|
|
1672
1971
|
await openAndAuthenticate(true);
|
|
1673
1972
|
return;
|
|
1674
1973
|
} catch (error) {
|
|
1675
1974
|
if (closeRequested) return;
|
|
1975
|
+
if (isAbortError$1(error)) return;
|
|
1676
1976
|
log("warn", "fitz.connection.reconnect_retry", {
|
|
1677
1977
|
attempts,
|
|
1678
1978
|
delayMs: actualDelayMs,
|
|
@@ -1686,6 +1986,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1686
1986
|
setState("CLOSED");
|
|
1687
1987
|
return;
|
|
1688
1988
|
}
|
|
1989
|
+
reconnectExhausted = true;
|
|
1689
1990
|
setState("DISCONNECTED");
|
|
1690
1991
|
emitLifecycleEvent("reconnect_exhausted", void 0, attempts);
|
|
1691
1992
|
};
|
|
@@ -1706,6 +2007,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1706
2007
|
};
|
|
1707
2008
|
const setState = (newState) => {
|
|
1708
2009
|
state = newState;
|
|
2010
|
+
notifyReadyListeners();
|
|
1709
2011
|
};
|
|
1710
2012
|
const sendSerialized = async (transport, data) => {
|
|
1711
2013
|
const prior = writeChain;
|
|
@@ -1745,6 +2047,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1745
2047
|
connect,
|
|
1746
2048
|
close,
|
|
1747
2049
|
request,
|
|
2050
|
+
requestDuringReconnectRestore,
|
|
1748
2051
|
send,
|
|
1749
2052
|
sendFireAndForget,
|
|
1750
2053
|
registerNotificationHandler,
|
|
@@ -1753,6 +2056,9 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
|
|
|
1753
2056
|
onDisconnect,
|
|
1754
2057
|
getMultiplexer,
|
|
1755
2058
|
dispatchAsyncHandler,
|
|
2059
|
+
executeWithRetry,
|
|
2060
|
+
waitUntilReady,
|
|
2061
|
+
shouldWaitForReconnect,
|
|
1756
2062
|
getScope,
|
|
1757
2063
|
getState,
|
|
1758
2064
|
isConnected,
|
|
@@ -2136,11 +2442,23 @@ function createTransport(url, transportType = "auto", options = {}) {
|
|
|
2136
2442
|
//#region src/domains/base.ts
|
|
2137
2443
|
function createDomainClient(connection) {
|
|
2138
2444
|
const requestFrame = async (messageType, payload, signal) => connection.request(messageType, payload, signal);
|
|
2445
|
+
const requestReconnectFrame = async (messageType, payload, signal) => {
|
|
2446
|
+
const resilientConnection = connection;
|
|
2447
|
+
if (typeof resilientConnection.requestDuringReconnectRestore === "function") return await resilientConnection.requestDuringReconnectRestore(messageType, payload, signal);
|
|
2448
|
+
return await connection.request(messageType, payload, signal);
|
|
2449
|
+
};
|
|
2139
2450
|
const sendFrame = async (messageType, payload) => connection.send(messageType, payload);
|
|
2451
|
+
const runWithRetry = async (operation, task) => {
|
|
2452
|
+
const resilientConnection = connection;
|
|
2453
|
+
if (typeof resilientConnection.executeWithRetry === "function") return resilientConnection.executeWithRetry(operation, task);
|
|
2454
|
+
return task();
|
|
2455
|
+
};
|
|
2140
2456
|
return {
|
|
2141
2457
|
connection,
|
|
2142
2458
|
requestFrame,
|
|
2143
|
-
|
|
2459
|
+
requestReconnectFrame,
|
|
2460
|
+
sendFrame,
|
|
2461
|
+
runWithRetry
|
|
2144
2462
|
};
|
|
2145
2463
|
}
|
|
2146
2464
|
//#endregion
|
|
@@ -2318,8 +2636,11 @@ function createAsyncIterableIterator(iterator) {
|
|
|
2318
2636
|
//#region src/domains/kv/transaction.ts
|
|
2319
2637
|
function createKvTransaction(connection, route, txId) {
|
|
2320
2638
|
let closed = false;
|
|
2321
|
-
const
|
|
2639
|
+
const resilientConnection = connection;
|
|
2640
|
+
let unsubscribeDisconnect = () => void 0;
|
|
2641
|
+
unsubscribeDisconnect = connection.onDisconnect(() => {
|
|
2322
2642
|
closed = true;
|
|
2643
|
+
unsubscribeDisconnect();
|
|
2323
2644
|
});
|
|
2324
2645
|
const ensureOpen = () => {
|
|
2325
2646
|
if (closed) throw new KvError("Transaction already closed", "TX_CLOSED");
|
|
@@ -2334,6 +2655,10 @@ function createKvTransaction(connection, route, txId) {
|
|
|
2334
2655
|
[5]: "OperationNotAllowed"
|
|
2335
2656
|
}[status] ?? `Unknown(${status})`}`, operation, status);
|
|
2336
2657
|
};
|
|
2658
|
+
const runWithRetry = async (operation, task) => {
|
|
2659
|
+
if (typeof resilientConnection.executeWithRetry === "function") return resilientConnection.executeWithRetry(operation, task);
|
|
2660
|
+
return task();
|
|
2661
|
+
};
|
|
2337
2662
|
const put = async (key, value, signal) => {
|
|
2338
2663
|
ensureOpen();
|
|
2339
2664
|
const payload = KvCodec.encodePut(txId, route, key, value);
|
|
@@ -2348,15 +2673,22 @@ function createKvTransaction(connection, route, txId) {
|
|
|
2348
2673
|
};
|
|
2349
2674
|
const get = async (key, signal) => {
|
|
2350
2675
|
ensureOpen();
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2676
|
+
return runWithRetry({
|
|
2677
|
+
domain: "kv",
|
|
2678
|
+
operation: "get",
|
|
2679
|
+
retryClass: "replayable_read",
|
|
2680
|
+
signal
|
|
2681
|
+
}, async () => {
|
|
2682
|
+
const payload = KvCodec.encodeGet(txId, route, key);
|
|
2683
|
+
const response = await connection.request(103, payload, signal);
|
|
2684
|
+
const decoded = KvCodec.decodeGetResponse(response);
|
|
2685
|
+
checkStatus(decoded.status, "GET");
|
|
2686
|
+
if (!decoded.found || !decoded.value) return { type: "not-found" };
|
|
2687
|
+
return {
|
|
2688
|
+
type: "found",
|
|
2689
|
+
value: decoded.value
|
|
2690
|
+
};
|
|
2691
|
+
});
|
|
2360
2692
|
};
|
|
2361
2693
|
const deleteItem = async (key, signal) => {
|
|
2362
2694
|
ensureOpen();
|
|
@@ -2372,11 +2704,18 @@ function createKvTransaction(connection, route, txId) {
|
|
|
2372
2704
|
};
|
|
2373
2705
|
const scan = async (options = {}, signal) => {
|
|
2374
2706
|
ensureOpen();
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2707
|
+
return runWithRetry({
|
|
2708
|
+
domain: "kv",
|
|
2709
|
+
operation: "scan",
|
|
2710
|
+
retryClass: "replayable_read",
|
|
2711
|
+
signal
|
|
2712
|
+
}, async () => {
|
|
2713
|
+
const payload = KvCodec.encodeScan(txId, route, options);
|
|
2714
|
+
const response = await connection.request(108, payload, signal);
|
|
2715
|
+
const decoded = KvCodec.decodeScanResponse(response);
|
|
2716
|
+
checkStatus(decoded.status, "SCAN");
|
|
2717
|
+
return createAsyncIterableIterator(createSliceIterator(decoded.keys));
|
|
2718
|
+
});
|
|
2380
2719
|
};
|
|
2381
2720
|
const commit = async (signal) => {
|
|
2382
2721
|
ensureOpen();
|
|
@@ -2656,7 +2995,17 @@ const QueueCodec = {
|
|
|
2656
2995
|
//#endregion
|
|
2657
2996
|
//#region src/domains/queue/types.ts
|
|
2658
2997
|
function createQueueItem(id, token, body, route, connection) {
|
|
2998
|
+
let closed = false;
|
|
2999
|
+
let unsubscribeDisconnect = () => void 0;
|
|
3000
|
+
unsubscribeDisconnect = connection.onDisconnect(() => {
|
|
3001
|
+
closed = true;
|
|
3002
|
+
unsubscribeDisconnect();
|
|
3003
|
+
});
|
|
3004
|
+
const ensureOpen = () => {
|
|
3005
|
+
if (closed) throw new QueueError("Queue item is no longer valid after disconnect", "ITEM_CLOSED");
|
|
3006
|
+
};
|
|
2659
3007
|
const extend = async (leaseSecs, signal) => {
|
|
3008
|
+
ensureOpen();
|
|
2660
3009
|
const payload = QueueCodec.encodeExtend(route, id, token, leaseSecs);
|
|
2661
3010
|
const response = await connection.request(203, payload, signal);
|
|
2662
3011
|
const decoded = QueueCodec.decodeExtendResponse(response);
|
|
@@ -2667,6 +3016,7 @@ function createQueueItem(id, token, body, route, connection) {
|
|
|
2667
3016
|
}
|
|
2668
3017
|
};
|
|
2669
3018
|
const complete = async (signal) => {
|
|
3019
|
+
ensureOpen();
|
|
2670
3020
|
const requestPayload = QueueCodec.encodeComplete(route, id, token);
|
|
2671
3021
|
const response = await connection.request(204, requestPayload, signal);
|
|
2672
3022
|
const decoded = QueueCodec.decodeCompleteResponse(response);
|
|
@@ -2675,9 +3025,12 @@ function createQueueItem(id, token, body, route, connection) {
|
|
|
2675
3025
|
const statusName = QueueStatus[errorCode] || `Unknown(${errorCode})`;
|
|
2676
3026
|
throw new QueueError(`COMPLETE failed: ${decoded.errorMessage ?? statusName}`, statusName, errorCode);
|
|
2677
3027
|
}
|
|
3028
|
+
closed = true;
|
|
3029
|
+
unsubscribeDisconnect();
|
|
2678
3030
|
};
|
|
2679
3031
|
const testOnlyInvalidToken = () => id + 1n;
|
|
2680
3032
|
const testOnlyCompleteWithToken = async (tokenToUse, signal) => {
|
|
3033
|
+
ensureOpen();
|
|
2681
3034
|
const requestPayload = QueueCodec.encodeComplete(route, id, tokenToUse);
|
|
2682
3035
|
const response = await connection.request(204, requestPayload, signal);
|
|
2683
3036
|
const decoded = QueueCodec.decodeCompleteResponse(response);
|
|
@@ -2686,6 +3039,8 @@ function createQueueItem(id, token, body, route, connection) {
|
|
|
2686
3039
|
const statusName = QueueStatus[errorCode] || `Unknown(${errorCode})`;
|
|
2687
3040
|
throw new QueueError(`COMPLETE failed: ${decoded.errorMessage ?? statusName}`, statusName, errorCode);
|
|
2688
3041
|
}
|
|
3042
|
+
closed = true;
|
|
3043
|
+
unsubscribeDisconnect();
|
|
2689
3044
|
};
|
|
2690
3045
|
return {
|
|
2691
3046
|
body,
|
|
@@ -2723,7 +3078,7 @@ let QueueStatus = /* @__PURE__ */ function(QueueStatus) {
|
|
|
2723
3078
|
* Queue domain client.
|
|
2724
3079
|
*/
|
|
2725
3080
|
function createQueueClient(connection) {
|
|
2726
|
-
const { requestFrame } = createDomainClient(connection);
|
|
3081
|
+
const { requestFrame, requestReconnectFrame, runWithRetry } = createDomainClient(connection);
|
|
2727
3082
|
const subscriptionsByPattern = /* @__PURE__ */ new Map();
|
|
2728
3083
|
const patternsBySubId = /* @__PURE__ */ new Map();
|
|
2729
3084
|
let notificationHandlerRegistered = false;
|
|
@@ -2737,7 +3092,7 @@ function createQueueClient(connection) {
|
|
|
2737
3092
|
subscriptionsByPattern.clear();
|
|
2738
3093
|
patternsBySubId.clear();
|
|
2739
3094
|
for (const subscription of subscriptions) {
|
|
2740
|
-
const subId = await subscribeWire(subscription.pattern);
|
|
3095
|
+
const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
|
|
2741
3096
|
subscriptionsByPattern.set(subscription.pattern, {
|
|
2742
3097
|
subId,
|
|
2743
3098
|
handlers: new Map(subscription.handlers)
|
|
@@ -2747,11 +3102,17 @@ function createQueueClient(connection) {
|
|
|
2747
3102
|
});
|
|
2748
3103
|
const enqueue = async (route, body, options) => {
|
|
2749
3104
|
assertQueueRoute(route);
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
3105
|
+
return runWithRetry({
|
|
3106
|
+
domain: "queue",
|
|
3107
|
+
operation: "enqueue",
|
|
3108
|
+
retryClass: "confirmed_negative_retry"
|
|
3109
|
+
}, async () => {
|
|
3110
|
+
const response = await requestFrame(200, QueueCodec.encodeEnqueue(route, body, options));
|
|
3111
|
+
const decoded = QueueCodec.decodeEnqueueResponse(response);
|
|
3112
|
+
checkStatus(decoded, "ENQUEUE");
|
|
3113
|
+
if (decoded.messageId === void 0) throw new QueueError("ENQUEUE response missing messageId", "MISSING_MESSAGE_ID");
|
|
3114
|
+
return decoded.messageId;
|
|
3115
|
+
});
|
|
2755
3116
|
};
|
|
2756
3117
|
const reserve = async (route, leaseSeconds, batchSize = 1, waitSeconds = 0) => {
|
|
2757
3118
|
assertQueueReserveRoute(route);
|
|
@@ -2807,8 +3168,8 @@ function createQueueClient(connection) {
|
|
|
2807
3168
|
if (existing) return addLocalSubscription(pattern, existing.subId, handler);
|
|
2808
3169
|
return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
|
|
2809
3170
|
};
|
|
2810
|
-
const subscribeWire = async (pattern) => {
|
|
2811
|
-
const response = await
|
|
3171
|
+
const subscribeWire = async (pattern, request = requestFrame) => {
|
|
3172
|
+
const response = await request(207, QueueCodec.encodeSubscribe(wireWatchPattern(pattern)));
|
|
2812
3173
|
const decoded = QueueCodec.decodeSubscribeResponse(response);
|
|
2813
3174
|
checkStatus(decoded, "SUBSCRIBE");
|
|
2814
3175
|
if (decoded.subId === void 0) throw new QueueError("SUBSCRIBE response missing subId", "MISSING_SUB_ID");
|
|
@@ -2875,7 +3236,11 @@ function createQueueClient(connection) {
|
|
|
2875
3236
|
[4]: "QueueFull",
|
|
2876
3237
|
[5]: "InvalidDelay"
|
|
2877
3238
|
}[errorCode] ?? `Unknown(${errorCode})`;
|
|
2878
|
-
throw new QueueError(`${operation} failed: ${response.errorMessage ?? statusName}`, statusName, errorCode)
|
|
3239
|
+
throw attachResilienceMeta(new QueueError(`${operation} failed: ${response.errorMessage ?? statusName}`, statusName, errorCode), {
|
|
3240
|
+
boundary: "post-send",
|
|
3241
|
+
failureKind: "domain",
|
|
3242
|
+
explicitNegative: true
|
|
3243
|
+
});
|
|
2879
3244
|
};
|
|
2880
3245
|
return {
|
|
2881
3246
|
enqueue,
|
|
@@ -3190,16 +3555,35 @@ function createRpcSubscription(route, unsubscribeFn) {
|
|
|
3190
3555
|
*/
|
|
3191
3556
|
function createRpcResponseWriter(connection, correlationId) {
|
|
3192
3557
|
let sequence = 0n;
|
|
3558
|
+
let stale = false;
|
|
3559
|
+
let unsubscribeDisconnect = () => void 0;
|
|
3560
|
+
const dispose = () => {
|
|
3561
|
+
if (stale) return;
|
|
3562
|
+
stale = true;
|
|
3563
|
+
unsubscribeDisconnect();
|
|
3564
|
+
unsubscribeDisconnect = () => void 0;
|
|
3565
|
+
};
|
|
3566
|
+
unsubscribeDisconnect = connection.onDisconnect(() => {
|
|
3567
|
+
dispose();
|
|
3568
|
+
});
|
|
3193
3569
|
const send = async (body, isEnd) => {
|
|
3570
|
+
if (stale) throw new ConnectionError("RPC response writer is no longer valid");
|
|
3194
3571
|
const payload = RpcCodec.encodeResponse(correlationId, sequence++, body, isEnd);
|
|
3195
3572
|
try {
|
|
3196
3573
|
await connection.send(303, payload);
|
|
3574
|
+
if (isEnd) dispose();
|
|
3197
3575
|
} catch (error) {
|
|
3198
|
-
if (isBenignShutdownError(error, connection))
|
|
3576
|
+
if (isBenignShutdownError(error, connection)) {
|
|
3577
|
+
dispose();
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3199
3580
|
throw error;
|
|
3200
3581
|
}
|
|
3201
3582
|
};
|
|
3202
|
-
return {
|
|
3583
|
+
return {
|
|
3584
|
+
send,
|
|
3585
|
+
dispose
|
|
3586
|
+
};
|
|
3203
3587
|
}
|
|
3204
3588
|
function isBenignShutdownError(error, connection) {
|
|
3205
3589
|
if (connection.getState() !== "AUTHENTICATED") return true;
|
|
@@ -3347,7 +3731,7 @@ const RpcClient = function(connection) {
|
|
|
3347
3731
|
return createRpcClient(connection);
|
|
3348
3732
|
};
|
|
3349
3733
|
function createRpcClient(connection) {
|
|
3350
|
-
const { requestFrame } = createDomainClient(connection);
|
|
3734
|
+
const { requestFrame, requestReconnectFrame } = createDomainClient(connection);
|
|
3351
3735
|
const pendingRpcs = /* @__PURE__ */ new Map();
|
|
3352
3736
|
const workers = /* @__PURE__ */ new Map();
|
|
3353
3737
|
let initialized = false;
|
|
@@ -3365,7 +3749,7 @@ function createRpcClient(connection) {
|
|
|
3365
3749
|
if (workers.size === 0) return;
|
|
3366
3750
|
const registeredWorkers = Array.from(workers.entries());
|
|
3367
3751
|
workers.clear();
|
|
3368
|
-
for (const [route, handler] of registeredWorkers) await
|
|
3752
|
+
for (const [route, handler] of registeredWorkers) await registerWorkerInternal(route, handler, requestReconnectFrame);
|
|
3369
3753
|
});
|
|
3370
3754
|
const call = async (route, body, options) => {
|
|
3371
3755
|
assertRpcRoute(route);
|
|
@@ -3391,13 +3775,16 @@ function createRpcClient(connection) {
|
|
|
3391
3775
|
throw error;
|
|
3392
3776
|
}
|
|
3393
3777
|
};
|
|
3394
|
-
const
|
|
3395
|
-
|
|
3396
|
-
initRpcHandler();
|
|
3397
|
-
const response = await requestFrame(300, RpcCodec.encodeSubscribeWorker(route));
|
|
3778
|
+
const registerWorkerInternal = async (route, handler, request = requestFrame) => {
|
|
3779
|
+
const response = await request(300, RpcCodec.encodeSubscribeWorker(route));
|
|
3398
3780
|
const decoded = RpcCodec.decodeSubscribeWorkerResponse(response);
|
|
3399
3781
|
if (decoded.status !== 0) throw new RpcError(`RPC SUBSCRIBE_WORKER failed: status ${decoded.status}`, "SUBSCRIBE_FAILED", decoded.status);
|
|
3400
3782
|
workers.set(route, handler);
|
|
3783
|
+
};
|
|
3784
|
+
const registerWorker = async (route, handler) => {
|
|
3785
|
+
assertRpcRoute(route);
|
|
3786
|
+
initRpcHandler();
|
|
3787
|
+
await registerWorkerInternal(route, handler);
|
|
3401
3788
|
const unsubscribeFn = async (registeredRoute) => {
|
|
3402
3789
|
await unregisterWorker(registeredRoute);
|
|
3403
3790
|
};
|
|
@@ -3466,6 +3853,8 @@ function createRpcClient(connection) {
|
|
|
3466
3853
|
try {
|
|
3467
3854
|
await writer.send(utf8Encoder.encode(`Handler error: ${message}`), true);
|
|
3468
3855
|
} catch {}
|
|
3856
|
+
} finally {
|
|
3857
|
+
writer.dispose();
|
|
3469
3858
|
}
|
|
3470
3859
|
});
|
|
3471
3860
|
};
|
|
@@ -3701,7 +4090,17 @@ function createLeaseSubscription(subId, pattern, unsubscribeFn) {
|
|
|
3701
4090
|
function createLease(token, expiresAt, route, connection) {
|
|
3702
4091
|
let currentToken = token;
|
|
3703
4092
|
let currentExpiry = expiresAt;
|
|
4093
|
+
let closed = false;
|
|
4094
|
+
let unsubscribeDisconnect = () => void 0;
|
|
4095
|
+
unsubscribeDisconnect = connection.onDisconnect(() => {
|
|
4096
|
+
closed = true;
|
|
4097
|
+
unsubscribeDisconnect();
|
|
4098
|
+
});
|
|
4099
|
+
const ensureOpen = () => {
|
|
4100
|
+
if (closed) throw new LeaseError("Lease handle is no longer valid after disconnect", "CLOSED");
|
|
4101
|
+
};
|
|
3704
4102
|
const extend = async (ttlSecs, signal) => {
|
|
4103
|
+
ensureOpen();
|
|
3705
4104
|
const requestPayload = LeaseCodec.encodeExtend(route, currentToken, ttlSecs);
|
|
3706
4105
|
const data = assertSuccess(await connection.request(401, requestPayload, signal), "EXTEND");
|
|
3707
4106
|
if (data && data.length >= 8) currentToken = new BufferReader(data).readU64BE();
|
|
@@ -3709,12 +4108,16 @@ function createLease(token, expiresAt, route, connection) {
|
|
|
3709
4108
|
return currentExpiry;
|
|
3710
4109
|
};
|
|
3711
4110
|
const release = async (signal) => {
|
|
4111
|
+
ensureOpen();
|
|
3712
4112
|
const payload = LeaseCodec.encodeRelease(route, currentToken);
|
|
3713
4113
|
assertSuccess(await connection.request(402, payload, signal), "RELEASE");
|
|
4114
|
+
closed = true;
|
|
4115
|
+
unsubscribeDisconnect();
|
|
3714
4116
|
};
|
|
3715
4117
|
const getExpiry = () => currentExpiry;
|
|
3716
4118
|
const testOnlyInvalidToken = () => currentToken + 1n;
|
|
3717
4119
|
const testOnlyExtendWithToken = async (tokenToUse, ttlSecs, signal) => {
|
|
4120
|
+
ensureOpen();
|
|
3718
4121
|
const requestPayload = LeaseCodec.encodeExtend(route, tokenToUse, ttlSecs);
|
|
3719
4122
|
const data = assertSuccess(await connection.request(401, requestPayload, signal), "EXTEND");
|
|
3720
4123
|
if (data && data.length >= 8) currentToken = new BufferReader(data).readU64BE();
|
|
@@ -3722,8 +4125,11 @@ function createLease(token, expiresAt, route, connection) {
|
|
|
3722
4125
|
return currentExpiry;
|
|
3723
4126
|
};
|
|
3724
4127
|
const testOnlyReleaseWithToken = async (tokenToUse, signal) => {
|
|
4128
|
+
ensureOpen();
|
|
3725
4129
|
const payload = LeaseCodec.encodeRelease(route, tokenToUse);
|
|
3726
4130
|
assertSuccess(await connection.request(402, payload, signal), "RELEASE");
|
|
4131
|
+
closed = true;
|
|
4132
|
+
unsubscribeDisconnect();
|
|
3727
4133
|
};
|
|
3728
4134
|
return {
|
|
3729
4135
|
extend,
|
|
@@ -3740,7 +4146,7 @@ function createLease(token, expiresAt, route, connection) {
|
|
|
3740
4146
|
* Lease domain client.
|
|
3741
4147
|
*/
|
|
3742
4148
|
function createLeaseClient(connection) {
|
|
3743
|
-
const { requestFrame } = createDomainClient(connection);
|
|
4149
|
+
const { requestFrame, requestReconnectFrame, runWithRetry } = createDomainClient(connection);
|
|
3744
4150
|
const subscriptionsByPattern = /* @__PURE__ */ new Map();
|
|
3745
4151
|
let initialized = false;
|
|
3746
4152
|
let nextHandlerId = 1;
|
|
@@ -3752,7 +4158,7 @@ function createLeaseClient(connection) {
|
|
|
3752
4158
|
}));
|
|
3753
4159
|
subscriptionsByPattern.clear();
|
|
3754
4160
|
for (const subscription of subscriptions) {
|
|
3755
|
-
const subId = await subscribeWire(subscription.pattern);
|
|
4161
|
+
const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
|
|
3756
4162
|
subscriptionsByPattern.set(subscription.pattern, {
|
|
3757
4163
|
subId,
|
|
3758
4164
|
handlers: new Map(subscription.handlers)
|
|
@@ -3769,16 +4175,22 @@ function createLeaseClient(connection) {
|
|
|
3769
4175
|
};
|
|
3770
4176
|
const query = async (route) => {
|
|
3771
4177
|
assertExactLeaseRoute(route);
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
4178
|
+
return runWithRetry({
|
|
4179
|
+
domain: "lease",
|
|
4180
|
+
operation: "query",
|
|
4181
|
+
retryClass: "replayable_read"
|
|
4182
|
+
}, async () => {
|
|
4183
|
+
const response = await requestFrame(403, LeaseCodec.encodeQuery(route));
|
|
4184
|
+
const decoded = LeaseCodec.decodeQueryResponse(response);
|
|
4185
|
+
if (decoded.status !== 0) throw new LeaseError("QUERY failed", "QUERY_FAILED", decoded.status);
|
|
4186
|
+
return {
|
|
4187
|
+
isHeld: decoded.isHeld ?? false,
|
|
4188
|
+
owner: decoded.owner,
|
|
4189
|
+
token: decoded.token,
|
|
4190
|
+
ttlRemainingSecs: decoded.ttlRemainingSecs,
|
|
4191
|
+
expiresAt: decoded.expiresAt
|
|
4192
|
+
};
|
|
4193
|
+
});
|
|
3782
4194
|
};
|
|
3783
4195
|
const subscribe = async (pattern, handler) => {
|
|
3784
4196
|
assertExactLeaseRoute(pattern);
|
|
@@ -3787,8 +4199,8 @@ function createLeaseClient(connection) {
|
|
|
3787
4199
|
if (existing) return addLocalSubscription(pattern, existing.subId, handler);
|
|
3788
4200
|
return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
|
|
3789
4201
|
};
|
|
3790
|
-
const subscribeWire = async (pattern) => {
|
|
3791
|
-
const response = await
|
|
4202
|
+
const subscribeWire = async (pattern, request = requestFrame) => {
|
|
4203
|
+
const response = await request(407, LeaseCodec.encodeSubscribe(pattern));
|
|
3792
4204
|
const decoded = LeaseCodec.decodeSubscribeResponse(response);
|
|
3793
4205
|
if (decoded.subId === void 0) throw new LeaseError("SUBSCRIBE failed", "SUBSCRIBE_FAILED");
|
|
3794
4206
|
return decoded.subId;
|
|
@@ -3935,7 +4347,7 @@ function createNoticeSubscription(subId, pattern, unsubscribeFn) {
|
|
|
3935
4347
|
* Notice domain client.
|
|
3936
4348
|
*/
|
|
3937
4349
|
function createNoticeClient(connection) {
|
|
3938
|
-
const { requestFrame } = createDomainClient(connection);
|
|
4350
|
+
const { requestFrame, requestReconnectFrame } = createDomainClient(connection);
|
|
3939
4351
|
const subscriptionsByPattern = /* @__PURE__ */ new Map();
|
|
3940
4352
|
const patternsBySubId = /* @__PURE__ */ new Map();
|
|
3941
4353
|
let initialized = false;
|
|
@@ -3949,7 +4361,7 @@ function createNoticeClient(connection) {
|
|
|
3949
4361
|
subscriptionsByPattern.clear();
|
|
3950
4362
|
patternsBySubId.clear();
|
|
3951
4363
|
for (const subscription of subscriptions) {
|
|
3952
|
-
const subId = await subscribeWire(subscription.pattern);
|
|
4364
|
+
const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
|
|
3953
4365
|
subscriptionsByPattern.set(subscription.pattern, {
|
|
3954
4366
|
subId,
|
|
3955
4367
|
handlers: new Map(subscription.handlers)
|
|
@@ -3975,8 +4387,8 @@ function createNoticeClient(connection) {
|
|
|
3975
4387
|
if (existing) return addLocalSubscription(pattern, existing.subId, handler);
|
|
3976
4388
|
return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
|
|
3977
4389
|
};
|
|
3978
|
-
const subscribeWire = async (pattern) => {
|
|
3979
|
-
const response = await
|
|
4390
|
+
const subscribeWire = async (pattern, request = requestFrame) => {
|
|
4391
|
+
const response = await request(501, NoticeCodec.encodeSubscribe(pattern));
|
|
3980
4392
|
const decoded = NoticeCodec.decodeSubscribeResponse(response);
|
|
3981
4393
|
if (decoded.subId === void 0) throw new NoticeError("SUBSCRIBE response missing subId", "MISSING_SUB_ID");
|
|
3982
4394
|
return decoded.subId;
|
|
@@ -4141,7 +4553,7 @@ const StreamCodec = {
|
|
|
4141
4553
|
},
|
|
4142
4554
|
/**
|
|
4143
4555
|
* Encode READ request
|
|
4144
|
-
* Payload: [route: string][start_offset: u64][limit: u64][has_max_bytes: u8][max_bytes?: u64][has_filter: u8][filter_length?:
|
|
4556
|
+
* Payload: [route: string][start_offset: u64][limit: u64][has_max_bytes: u8][max_bytes?: u64][has_filter: u8][filter_length?: u32_be][filter?: custom]
|
|
4145
4557
|
*/
|
|
4146
4558
|
encodeRead(route, startOffset, limit, options) {
|
|
4147
4559
|
const writer = new BufferWriter(256);
|
|
@@ -4155,11 +4567,11 @@ const StreamCodec = {
|
|
|
4155
4567
|
const filter = options?.filter;
|
|
4156
4568
|
if (filter && filter.clauses.length > 0) {
|
|
4157
4569
|
writer.writeU8(1);
|
|
4158
|
-
const
|
|
4159
|
-
|
|
4160
|
-
const
|
|
4161
|
-
|
|
4162
|
-
writer.
|
|
4570
|
+
const filterWriter = new BufferWriter(64);
|
|
4571
|
+
encodeStreamFilterSet(filter, filterWriter);
|
|
4572
|
+
const filterBytes = filterWriter.getBufferView();
|
|
4573
|
+
writer.writeU32BE(filterBytes.length);
|
|
4574
|
+
writer.writeBytes(filterBytes);
|
|
4163
4575
|
} else writer.writeU8(0);
|
|
4164
4576
|
return writer.getBufferView();
|
|
4165
4577
|
},
|
|
@@ -4377,27 +4789,29 @@ const StreamCodec = {
|
|
|
4377
4789
|
}
|
|
4378
4790
|
};
|
|
4379
4791
|
function encodeStreamFilterSet(filter, writer) {
|
|
4380
|
-
writer.
|
|
4792
|
+
writer.writeU8(0);
|
|
4793
|
+
writer.writeU8(241);
|
|
4794
|
+
writer.writeU32BE(filter.clauses.length);
|
|
4381
4795
|
for (const clause of filter.clauses) encodeStreamFilterClause(writer, clause);
|
|
4382
4796
|
}
|
|
4383
4797
|
function encodeStreamFilterClause(writer, clause) {
|
|
4384
4798
|
switch (clause.kind) {
|
|
4385
4799
|
case "Equals":
|
|
4386
|
-
writer.
|
|
4387
|
-
writer.
|
|
4800
|
+
writer.writeU8(0);
|
|
4801
|
+
writer.writeString(clause.value);
|
|
4388
4802
|
return;
|
|
4389
4803
|
case "NotEquals":
|
|
4390
|
-
writer.
|
|
4391
|
-
writer.
|
|
4804
|
+
writer.writeU8(1);
|
|
4805
|
+
writer.writeString(clause.value);
|
|
4392
4806
|
return;
|
|
4393
4807
|
case "StartsWith":
|
|
4394
|
-
writer.
|
|
4395
|
-
writer.
|
|
4808
|
+
writer.writeU8(2);
|
|
4809
|
+
writer.writeString(clause.value);
|
|
4396
4810
|
return;
|
|
4397
4811
|
case "AnyOf":
|
|
4398
|
-
writer.
|
|
4399
|
-
writer.
|
|
4400
|
-
for (const value of clause.values) writer.
|
|
4812
|
+
writer.writeU8(3);
|
|
4813
|
+
writer.writeU32BE(clause.values.length);
|
|
4814
|
+
for (const value of clause.values) writer.writeString(value);
|
|
4401
4815
|
return;
|
|
4402
4816
|
}
|
|
4403
4817
|
}
|
|
@@ -4416,8 +4830,10 @@ function createStreamSubscription(subId, pattern, unsubscribeFn) {
|
|
|
4416
4830
|
//#region src/domains/stream/session.ts
|
|
4417
4831
|
function createStreamSession(connection, _route, sessionId) {
|
|
4418
4832
|
let closed = false;
|
|
4419
|
-
|
|
4833
|
+
let unsubscribeDisconnect = () => void 0;
|
|
4834
|
+
unsubscribeDisconnect = connection.onDisconnect(() => {
|
|
4420
4835
|
closed = true;
|
|
4836
|
+
unsubscribeDisconnect();
|
|
4421
4837
|
});
|
|
4422
4838
|
const ensureOpen = () => {
|
|
4423
4839
|
if (closed) throw new StreamError("Stream session already closed", "SESSION_CLOSED");
|
|
@@ -4491,7 +4907,7 @@ function isAbortSignal(value) {
|
|
|
4491
4907
|
* 3. `commit()` or `rollback()` finalizes the session
|
|
4492
4908
|
*/
|
|
4493
4909
|
function createStreamClient(connection) {
|
|
4494
|
-
const { requestFrame } = createDomainClient(connection);
|
|
4910
|
+
const { requestFrame, requestReconnectFrame, runWithRetry } = createDomainClient(connection);
|
|
4495
4911
|
const subscriptionsByPattern = /* @__PURE__ */ new Map();
|
|
4496
4912
|
const patternsBySubId = /* @__PURE__ */ new Map();
|
|
4497
4913
|
let initialized = false;
|
|
@@ -4505,7 +4921,7 @@ function createStreamClient(connection) {
|
|
|
4505
4921
|
subscriptionsByPattern.clear();
|
|
4506
4922
|
patternsBySubId.clear();
|
|
4507
4923
|
for (const subscription of snapshot) {
|
|
4508
|
-
const subId = await subscribeWire(subscription.pattern);
|
|
4924
|
+
const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
|
|
4509
4925
|
subscriptionsByPattern.set(subscription.pattern, {
|
|
4510
4926
|
subId,
|
|
4511
4927
|
handlers: new Map(subscription.handlers)
|
|
@@ -4523,18 +4939,25 @@ function createStreamClient(connection) {
|
|
|
4523
4939
|
};
|
|
4524
4940
|
const readPage = async (route, startOffset, limit = 100, options) => {
|
|
4525
4941
|
assertStreamPattern(route);
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4942
|
+
return runWithRetry({
|
|
4943
|
+
domain: "stream",
|
|
4944
|
+
operation: "read",
|
|
4945
|
+
retryClass: "replayable_read",
|
|
4946
|
+
signal: options?.signal
|
|
4947
|
+
}, async () => {
|
|
4948
|
+
const response = await requestFrame(604, StreamCodec.encodeRead(route, startOffset, limit, options), options?.signal);
|
|
4949
|
+
const decoded = StreamCodec.decodeReadResponse(response);
|
|
4950
|
+
checkStatus(decoded.status, "READ");
|
|
4951
|
+
return {
|
|
4952
|
+
items: decoded.items,
|
|
4953
|
+
cursor: decoded.cursor ?? {
|
|
4954
|
+
lastResourceOffset: startOffset,
|
|
4955
|
+
lastAreaOffset: void 0,
|
|
4956
|
+
lastRealmOffset: void 0,
|
|
4957
|
+
hasMore: false
|
|
4958
|
+
}
|
|
4959
|
+
};
|
|
4960
|
+
});
|
|
4538
4961
|
};
|
|
4539
4962
|
const read = async (route, startOffset, limit = 100, options) => {
|
|
4540
4963
|
const page = await readPage(route, startOffset, limit, options);
|
|
@@ -4545,21 +4968,33 @@ function createStreamClient(connection) {
|
|
|
4545
4968
|
};
|
|
4546
4969
|
const peek = async (route) => {
|
|
4547
4970
|
assertStreamRoute(route);
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4971
|
+
return runWithRetry({
|
|
4972
|
+
domain: "stream",
|
|
4973
|
+
operation: "last",
|
|
4974
|
+
retryClass: "replayable_read"
|
|
4975
|
+
}, async () => {
|
|
4976
|
+
const response = await requestFrame(605, StreamCodec.encodeLast(route));
|
|
4977
|
+
const decoded = StreamCodec.decodeLastResponse(response);
|
|
4978
|
+
checkStatus(decoded.status, "LAST");
|
|
4979
|
+
return decoded.record ?? null;
|
|
4980
|
+
});
|
|
4552
4981
|
};
|
|
4553
4982
|
const metadata = async (route) => {
|
|
4554
4983
|
assertStreamRoute(route);
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4984
|
+
return runWithRetry({
|
|
4985
|
+
domain: "stream",
|
|
4986
|
+
operation: "metadata",
|
|
4987
|
+
retryClass: "replayable_read"
|
|
4988
|
+
}, async () => {
|
|
4989
|
+
const response = await requestFrame(606, StreamCodec.encodeMetadata(route));
|
|
4990
|
+
const decoded = StreamCodec.decodeMetadataResponse(response);
|
|
4991
|
+
checkStatus(decoded.status, "GET_METADATA");
|
|
4992
|
+
return decoded.metadata ?? {
|
|
4993
|
+
firstOffset: 0n,
|
|
4994
|
+
lastOffset: 0n,
|
|
4995
|
+
recordCount: 0n
|
|
4996
|
+
};
|
|
4997
|
+
});
|
|
4563
4998
|
};
|
|
4564
4999
|
const subscribe = async (pattern, handler) => {
|
|
4565
5000
|
assertStreamPattern(pattern);
|
|
@@ -4568,8 +5003,8 @@ function createStreamClient(connection) {
|
|
|
4568
5003
|
if (existing) return addLocalSubscription(pattern, existing.subId, handler);
|
|
4569
5004
|
return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
|
|
4570
5005
|
};
|
|
4571
|
-
const subscribeWire = async (pattern) => {
|
|
4572
|
-
const response = await
|
|
5006
|
+
const subscribeWire = async (pattern, request = requestFrame) => {
|
|
5007
|
+
const response = await request(607, StreamCodec.encodeSubscribe(pattern));
|
|
4573
5008
|
const decoded = StreamCodec.decodeSubscribeResponse(response);
|
|
4574
5009
|
checkStatus(decoded.status, "SUBSCRIBE");
|
|
4575
5010
|
if (decoded.subId === void 0) throw new StreamError("SUBSCRIBE response missing subId", "MISSING_SESSION_ID");
|
|
@@ -4824,7 +5259,7 @@ var ScheduleError$1 = class extends Error {
|
|
|
4824
5259
|
* Schedule domain client.
|
|
4825
5260
|
*/
|
|
4826
5261
|
function createScheduleClient(connection) {
|
|
4827
|
-
const { requestFrame } = createDomainClient(connection);
|
|
5262
|
+
const { requestFrame, requestReconnectFrame } = createDomainClient(connection);
|
|
4828
5263
|
const subscriptionsByPattern = /* @__PURE__ */ new Map();
|
|
4829
5264
|
const patternsBySubId = /* @__PURE__ */ new Map();
|
|
4830
5265
|
let notifyHandlerInitialized = false;
|
|
@@ -4838,7 +5273,7 @@ function createScheduleClient(connection) {
|
|
|
4838
5273
|
subscriptionsByPattern.clear();
|
|
4839
5274
|
patternsBySubId.clear();
|
|
4840
5275
|
for (const subscription of subscriptions) {
|
|
4841
|
-
const subId = await subscribeWire(subscription.pattern);
|
|
5276
|
+
const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
|
|
4842
5277
|
subscriptionsByPattern.set(subscription.pattern, {
|
|
4843
5278
|
subId,
|
|
4844
5279
|
handlers: new Map(subscription.handlers)
|
|
@@ -4868,8 +5303,8 @@ function createScheduleClient(connection) {
|
|
|
4868
5303
|
if (existing) return addLocalSubscription(pattern, existing.subId, handler);
|
|
4869
5304
|
return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
|
|
4870
5305
|
};
|
|
4871
|
-
const subscribeWire = async (pattern) => {
|
|
4872
|
-
const response = await
|
|
5306
|
+
const subscribeWire = async (pattern, request = requestFrame) => {
|
|
5307
|
+
const response = await request(703, ScheduleCodec.encodeSubscribe(pattern));
|
|
4873
5308
|
return ScheduleCodec.decodeSubscribeResponse(assertSuccess(response, "SUBSCRIBE")).subId;
|
|
4874
5309
|
};
|
|
4875
5310
|
const addLocalSubscription = (pattern, subId, handler) => {
|
|
@@ -4942,6 +5377,39 @@ function assertConcreteScheduleRoute(route) {
|
|
|
4942
5377
|
}
|
|
4943
5378
|
//#endregion
|
|
4944
5379
|
//#region src/client/client.ts
|
|
5380
|
+
const abortError = () => {
|
|
5381
|
+
const error = /* @__PURE__ */ new Error("The operation was aborted");
|
|
5382
|
+
error.name = "AbortError";
|
|
5383
|
+
return error;
|
|
5384
|
+
};
|
|
5385
|
+
const throwIfAborted = (signal) => {
|
|
5386
|
+
if (signal?.aborted) throw abortError();
|
|
5387
|
+
};
|
|
5388
|
+
const waitForSharedPromise = async (promise, signal) => {
|
|
5389
|
+
if (!signal) return promise;
|
|
5390
|
+
if (signal.aborted) throw abortError();
|
|
5391
|
+
return await new Promise((resolve, reject) => {
|
|
5392
|
+
let settled = false;
|
|
5393
|
+
const cleanup = () => {
|
|
5394
|
+
signal.removeEventListener("abort", onAbort);
|
|
5395
|
+
};
|
|
5396
|
+
const settle = (callback) => {
|
|
5397
|
+
if (settled) return;
|
|
5398
|
+
settled = true;
|
|
5399
|
+
cleanup();
|
|
5400
|
+
callback();
|
|
5401
|
+
};
|
|
5402
|
+
const onAbort = () => {
|
|
5403
|
+
settle(() => reject(abortError()));
|
|
5404
|
+
};
|
|
5405
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
5406
|
+
promise.then((value) => {
|
|
5407
|
+
settle(() => resolve(value));
|
|
5408
|
+
}, (error) => {
|
|
5409
|
+
settle(() => reject(error));
|
|
5410
|
+
});
|
|
5411
|
+
});
|
|
5412
|
+
};
|
|
4945
5413
|
function createClient(config) {
|
|
4946
5414
|
const observability = config.observability;
|
|
4947
5415
|
const resolvedConfig = {
|
|
@@ -4953,12 +5421,19 @@ function createClient(config) {
|
|
|
4953
5421
|
maxRequestQueueSize: 1024,
|
|
4954
5422
|
observability: config.observability ?? {},
|
|
4955
5423
|
reconnect: {
|
|
4956
|
-
enabled:
|
|
5424
|
+
enabled: true,
|
|
4957
5425
|
maxAttempts: Infinity,
|
|
4958
5426
|
backoffMs: 250,
|
|
4959
5427
|
maxBackoffMs: 5e3,
|
|
4960
5428
|
...config.reconnect
|
|
4961
5429
|
},
|
|
5430
|
+
retry: {
|
|
5431
|
+
enabled: true,
|
|
5432
|
+
maxAttempts: 3,
|
|
5433
|
+
backoffMs: 100,
|
|
5434
|
+
maxBackoffMs: 1e3,
|
|
5435
|
+
...config.retry
|
|
5436
|
+
},
|
|
4962
5437
|
asyncHandlers: {
|
|
4963
5438
|
maxConcurrency: Infinity,
|
|
4964
5439
|
timeoutMs: 3e4,
|
|
@@ -4975,16 +5450,19 @@ function createClient(config) {
|
|
|
4975
5450
|
let noticeClient = null;
|
|
4976
5451
|
let streamClient = null;
|
|
4977
5452
|
let scheduleClient = null;
|
|
5453
|
+
let clientClosed = false;
|
|
5454
|
+
let pendingConnectPromise = null;
|
|
4978
5455
|
const resolveTokenProvider = () => {
|
|
4979
5456
|
if (resolvedConfig.tokenProvider) return resolvedConfig.tokenProvider;
|
|
4980
5457
|
return () => "";
|
|
4981
5458
|
};
|
|
4982
5459
|
const ensureConnection = () => {
|
|
5460
|
+
if (clientClosed) throw new ConnectionError("Client is closed", { state: "CLOSED" });
|
|
4983
5461
|
if (!connection) throw new ConnectionError("Not connected to Fitz server. Call connect() first.", { state: getState() });
|
|
4984
5462
|
return connection;
|
|
4985
5463
|
};
|
|
4986
|
-
const
|
|
4987
|
-
if (connection
|
|
5464
|
+
const createOwnedConnection = () => {
|
|
5465
|
+
if (connection) return connection;
|
|
4988
5466
|
connection = createConnection(() => createTransport(resolvedConfig.url, resolvedConfig.transport, {
|
|
4989
5467
|
timeout: resolvedConfig.timeout,
|
|
4990
5468
|
maxFrameSize: resolvedConfig.maxFrameSize
|
|
@@ -4994,15 +5472,48 @@ function createClient(config) {
|
|
|
4994
5472
|
maxInFlightRequests: resolvedConfig.maxInFlightRequests,
|
|
4995
5473
|
maxRequestQueueSize: resolvedConfig.maxRequestQueueSize,
|
|
4996
5474
|
reconnect: resolvedConfig.reconnect,
|
|
5475
|
+
retry: resolvedConfig.retry,
|
|
4997
5476
|
observability,
|
|
4998
5477
|
asyncHandlers: resolvedConfig.asyncHandlers
|
|
4999
5478
|
});
|
|
5000
|
-
|
|
5479
|
+
return connection;
|
|
5480
|
+
};
|
|
5481
|
+
const connect = async (options = {}) => {
|
|
5482
|
+
if (clientClosed) throw new ConnectionError("Client is closed", { state: "CLOSED" });
|
|
5483
|
+
throwIfAborted(options.signal);
|
|
5484
|
+
const activeConnection = createOwnedConnection();
|
|
5485
|
+
if (activeConnection.isConnected()) return;
|
|
5486
|
+
if (pendingConnectPromise) {
|
|
5487
|
+
await waitForSharedPromise(pendingConnectPromise, options.signal);
|
|
5488
|
+
return;
|
|
5489
|
+
}
|
|
5490
|
+
const state = activeConnection.getState();
|
|
5491
|
+
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(() => {
|
|
5492
|
+
if (pendingConnectPromise === trackedConnectPromise) pendingConnectPromise = null;
|
|
5493
|
+
});
|
|
5494
|
+
pendingConnectPromise = trackedConnectPromise;
|
|
5495
|
+
await waitForSharedPromise(trackedConnectPromise, options.signal);
|
|
5001
5496
|
};
|
|
5002
5497
|
const close = async () => {
|
|
5498
|
+
if (clientClosed && !connection) {
|
|
5499
|
+
kvClient = null;
|
|
5500
|
+
queueClient = null;
|
|
5501
|
+
rpcClient = null;
|
|
5502
|
+
leaseClient = null;
|
|
5503
|
+
noticeClient = null;
|
|
5504
|
+
streamClient = null;
|
|
5505
|
+
scheduleClient = null;
|
|
5506
|
+
return;
|
|
5507
|
+
}
|
|
5508
|
+
clientClosed = true;
|
|
5509
|
+
pendingConnectPromise = null;
|
|
5003
5510
|
if (connection) {
|
|
5004
|
-
|
|
5005
|
-
|
|
5511
|
+
const activeConnection = connection;
|
|
5512
|
+
try {
|
|
5513
|
+
await activeConnection.close();
|
|
5514
|
+
} finally {
|
|
5515
|
+
if (connection === activeConnection) connection = null;
|
|
5516
|
+
}
|
|
5006
5517
|
}
|
|
5007
5518
|
kvClient = null;
|
|
5008
5519
|
queueClient = null;
|
|
@@ -5013,7 +5524,7 @@ function createClient(config) {
|
|
|
5013
5524
|
scheduleClient = null;
|
|
5014
5525
|
};
|
|
5015
5526
|
const isConnected = () => {
|
|
5016
|
-
return connection?.isConnected() ?? false;
|
|
5527
|
+
return !clientClosed && (connection?.isConnected() ?? false);
|
|
5017
5528
|
};
|
|
5018
5529
|
const kv = () => {
|
|
5019
5530
|
const activeConnection = ensureConnection();
|
|
@@ -5054,7 +5565,10 @@ function createClient(config) {
|
|
|
5054
5565
|
return ensureConnection().getUrl();
|
|
5055
5566
|
};
|
|
5056
5567
|
const getState = () => {
|
|
5057
|
-
|
|
5568
|
+
if (clientClosed) return "CLOSED";
|
|
5569
|
+
if (!connection) return "DISCONNECTED";
|
|
5570
|
+
const state = connection.getState();
|
|
5571
|
+
return state === "CLOSED" ? "DISCONNECTED" : state;
|
|
5058
5572
|
};
|
|
5059
5573
|
return {
|
|
5060
5574
|
config: resolvedConfig,
|