@cntryl/fitz 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +24 -2
  2. package/dist/index.cjs +998 -174
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.mjs +998 -175
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/types/client/client.d.ts +1 -1
  7. package/dist/types/client/client.d.ts.map +1 -1
  8. package/dist/types/client/connection.d.ts +10 -3
  9. package/dist/types/client/connection.d.ts.map +1 -1
  10. package/dist/types/client/resilience.d.ts +21 -0
  11. package/dist/types/client/resilience.d.ts.map +1 -0
  12. package/dist/types/core/errors.d.ts.map +1 -1
  13. package/dist/types/core/types.d.ts +13 -0
  14. package/dist/types/core/types.d.ts.map +1 -1
  15. package/dist/types/core/wake-gate.d.ts +11 -0
  16. package/dist/types/core/wake-gate.d.ts.map +1 -0
  17. package/dist/types/domains/base.d.ts +9 -2
  18. package/dist/types/domains/base.d.ts.map +1 -1
  19. package/dist/types/domains/kv/transaction.d.ts.map +1 -1
  20. package/dist/types/domains/lease/client.d.ts.map +1 -1
  21. package/dist/types/domains/lease/types.d.ts.map +1 -1
  22. package/dist/types/domains/queue/client.d.ts +5 -0
  23. package/dist/types/domains/queue/client.d.ts.map +1 -1
  24. package/dist/types/domains/queue/types.d.ts.map +1 -1
  25. package/dist/types/domains/rpc/client.d.ts.map +1 -1
  26. package/dist/types/domains/schedule/client.d.ts +4 -1
  27. package/dist/types/domains/schedule/client.d.ts.map +1 -1
  28. package/dist/types/domains/stream/client.d.ts +7 -0
  29. package/dist/types/domains/stream/client.d.ts.map +1 -1
  30. package/dist/types/domains/stream/session.d.ts.map +1 -1
  31. package/dist/types/index.d.ts +3 -1
  32. package/dist/types/index.d.ts.map +1 -1
  33. package/dist/types/transport/tcp.d.ts.map +1 -1
  34. package/dist/types/transport/types.d.ts +7 -0
  35. package/dist/types/transport/types.d.ts.map +1 -1
  36. package/dist/types/transport/websocket.d.ts.map +1 -1
  37. 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
- return key !== null && retryableErrorCodes.has(key);
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$1();
984
+ const error = abortError$3();
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$1());
1058
+ failRequest(abortError$3());
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$1();
1078
+ if (signal?.aborted) throw abortError$3();
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$1();
1091
+ if (signal?.aborted) throw abortError$3();
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$1() {
1203
+ function abortError$3() {
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$2();
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$2());
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$2();
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$2());
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$2() {
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$2();
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$2();
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$2()));
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,13 @@ 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;
1494
+ const heartbeatEnabled = options.heartbeat?.enabled ?? true;
1495
+ const heartbeatIntervalMs = options.heartbeat?.intervalMs ?? 1e4;
1496
+ const heartbeatTimeoutMs = options.heartbeat?.timeoutMs ?? 3e4;
1402
1497
  const maxInFlightRequests = options.maxInFlightRequests ?? 256;
1403
1498
  const maxRequestQueueSize = options.maxRequestQueueSize ?? 1024;
1404
1499
  const observability = options.observability;
@@ -1412,10 +1507,23 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1412
1507
  let receiveLoop = null;
1413
1508
  let receiveLoopAbort = false;
1414
1509
  let closeRequested = false;
1510
+ let permanentlyClosed = false;
1511
+ let connectPromise = null;
1415
1512
  let reconnectPromise = null;
1513
+ let connectionLossPromise = null;
1514
+ let reconnectRestoreActive = false;
1416
1515
  let authOutcome = null;
1417
1516
  let authRejected = false;
1517
+ let hasEstablishedSession = false;
1518
+ let reconnectExhausted = false;
1519
+ let readyWaiterCount = 0;
1520
+ const closeAbortController = new AbortController();
1418
1521
  const connectionScope = createScope("connection");
1522
+ const readyListeners = /* @__PURE__ */ new Set();
1523
+ let heartbeatTimer = null;
1524
+ let heartbeatTransport = null;
1525
+ let heartbeatPending = false;
1526
+ let lastActivityAt = Date.now();
1419
1527
  const log = (level, event, fields) => {
1420
1528
  observability?.logger?.log(level, event, fields);
1421
1529
  };
@@ -1446,6 +1554,8 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1446
1554
  };
1447
1555
  const handlePossibleTransportFailure = (error) => {
1448
1556
  if (closeRequested) return;
1557
+ if (state !== "AUTHENTICATED") return;
1558
+ if (getResilienceMeta(error)?.boundary === "pre-send") return;
1449
1559
  if (error instanceof TransportError || error instanceof ConnectionError || error instanceof AuthenticationError) handleConnectionLoss(error);
1450
1560
  };
1451
1561
  const multiplexer = new Multiplexer({
@@ -1456,17 +1566,35 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1456
1566
  log("warn", "fitz.connection.handler_failed", { error: describeError(error) });
1457
1567
  });
1458
1568
  const connect = async (options = {}) => {
1459
- closeRequested = false;
1569
+ if (permanentlyClosed || closeRequested) throw connectionClosedError();
1570
+ throwIfAborted$1(options.signal);
1571
+ if (state === "AUTHENTICATED") return;
1572
+ if (connectPromise) {
1573
+ await waitForSharedPromise$1(connectPromise, options.signal);
1574
+ return;
1575
+ }
1576
+ if (reconnectPromise || state === "CONNECTING" || state === "CONNECTED" || state === "AUTHENTICATING" || state === "RECONNECTING" || state === "DISCONNECTED" && canWaitForReconnect()) {
1577
+ await waitForReady(options.signal, timeout);
1578
+ return;
1579
+ }
1460
1580
  authRejected = false;
1461
- await openAndAuthenticate(false, options.signal);
1581
+ reconnectExhausted = false;
1582
+ const sharedConnectPromise = openAndAuthenticate(false, options.signal).finally(() => {
1583
+ if (connectPromise === sharedConnectPromise) connectPromise = null;
1584
+ });
1585
+ connectPromise = sharedConnectPromise;
1586
+ await sharedConnectPromise;
1462
1587
  };
1463
1588
  const close = async () => {
1464
1589
  if (state === "CLOSED" && !transport) {
1465
1590
  await connectionScope.dispose();
1466
1591
  return;
1467
1592
  }
1593
+ permanentlyClosed = true;
1468
1594
  closeRequested = true;
1469
1595
  receiveLoopAbort = true;
1596
+ stopHeartbeat();
1597
+ closeAbortController.abort();
1470
1598
  asyncHandlerDispatcher.close();
1471
1599
  const scopeDisposePromise = connectionScope.dispose();
1472
1600
  setState("CLOSED");
@@ -1487,15 +1615,34 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1487
1615
  await asyncHandlerDispatcher.drain();
1488
1616
  await scopeDisposePromise;
1489
1617
  };
1490
- const request = async (messageType, requestPayload, signal) => {
1618
+ const waitForRequestReady = async (signal, allowReconnectRestore = false) => {
1619
+ if (allowReconnectRestore && reconnectRestoreActive && state === "AUTHENTICATING" && !closeRequested && !authRejected && transport) return;
1620
+ const releaseReadyWaitSlot = acquireReadyWaitSlot();
1621
+ try {
1622
+ await waitForReady(signal, timeout);
1623
+ } finally {
1624
+ releaseReadyWaitSlot?.();
1625
+ }
1491
1626
  ensureAuthenticated();
1627
+ };
1628
+ const requestInternal = async (messageType, requestPayload, signal, allowReconnectRestore = false) => {
1629
+ let sendStarted = false;
1630
+ await waitForRequestReady(signal, allowReconnectRestore);
1492
1631
  const releaseRequestSlot = await requestGate.acquire(signal);
1493
1632
  const startedAt = Date.now();
1494
1633
  try {
1495
1634
  const activeTransport = ensureTransport();
1496
1635
  const frame = FrameCodec.encodeFrame(messageType, requestPayload);
1497
- return await multiplexer.request(messageType, frame, (data) => sendSerialized(activeTransport, data), timeout, signal);
1636
+ return await multiplexer.request(messageType, frame, (data) => {
1637
+ sendStarted = true;
1638
+ return sendSerialized(activeTransport, data);
1639
+ }, timeout, signal);
1498
1640
  } catch (error) {
1641
+ attachResilienceMeta(error, {
1642
+ boundary: sendStarted ? "post-send" : "pre-send",
1643
+ failureKind: classifyFailureKind(error),
1644
+ explicitNegative: false
1645
+ });
1499
1646
  log("error", "fitz.connection.request_failed", {
1500
1647
  operation: "request",
1501
1648
  state,
@@ -1510,13 +1657,34 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1510
1657
  releaseRequestSlot();
1511
1658
  }
1512
1659
  };
1513
- const send = async (messageType, requestPayload) => {
1660
+ const request = async (messageType, requestPayload, signal) => {
1661
+ return await requestInternal(messageType, requestPayload, signal);
1662
+ };
1663
+ const requestDuringReconnectRestore = async (messageType, requestPayload, signal) => {
1664
+ return await requestInternal(messageType, requestPayload, signal, true);
1665
+ };
1666
+ const send = async (messageType, requestPayload, signal) => {
1667
+ let sendStarted = false;
1668
+ const releaseReadyWaitSlot = acquireReadyWaitSlot();
1669
+ try {
1670
+ await waitForReady(signal, timeout);
1671
+ } finally {
1672
+ releaseReadyWaitSlot?.();
1673
+ }
1514
1674
  ensureAuthenticated();
1515
- const releaseRequestSlot = await requestGate.acquire();
1675
+ const releaseRequestSlot = await requestGate.acquire(signal);
1516
1676
  const startedAt = Date.now();
1517
1677
  try {
1518
- await sendSerialized(ensureTransport(), FrameCodec.encodeFrame(messageType, requestPayload));
1678
+ const activeTransport = ensureTransport();
1679
+ const frame = FrameCodec.encodeFrame(messageType, requestPayload);
1680
+ sendStarted = true;
1681
+ await sendSerialized(activeTransport, frame);
1519
1682
  } catch (error) {
1683
+ attachResilienceMeta(error, {
1684
+ boundary: sendStarted ? "post-send" : "pre-send",
1685
+ failureKind: classifyFailureKind(error),
1686
+ explicitNegative: false
1687
+ });
1520
1688
  log("error", "fitz.connection.send_failed", {
1521
1689
  operation: "send",
1522
1690
  state,
@@ -1531,8 +1699,8 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1531
1699
  releaseRequestSlot();
1532
1700
  }
1533
1701
  };
1534
- const sendFireAndForget = async (messageType, requestPayload) => {
1535
- await send(messageType, requestPayload);
1702
+ const sendFireAndForget = async (messageType, requestPayload, signal) => {
1703
+ await send(messageType, requestPayload, signal);
1536
1704
  };
1537
1705
  const registerNotificationHandler = (messageType, handler) => {
1538
1706
  multiplexer.registerNotificationHandler(messageType, handler);
@@ -1560,22 +1728,224 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1560
1728
  const getState = () => state;
1561
1729
  const isConnected = () => state === "AUTHENTICATED";
1562
1730
  const getUrl = () => ensureTransport().getUrl();
1731
+ const canWaitForReconnect = () => {
1732
+ return reconnectEnabled && hasEstablishedSession && !reconnectExhausted && !authRejected;
1733
+ };
1734
+ const readyFailure = () => {
1735
+ if (state === "AUTHENTICATED") return null;
1736
+ if (closeRequested || state === "CLOSED") return connectionClosedError();
1737
+ if (authRejected) return new AuthenticationError("Authentication rejected", { state });
1738
+ if (state === "CONNECTING" || state === "CONNECTED" || state === "AUTHENTICATING" || state === "RECONNECTING") return null;
1739
+ if (state === "DISCONNECTED" && canWaitForReconnect()) return null;
1740
+ return new ConnectionError(`Cannot use connection while state is ${state}`, { state });
1741
+ };
1742
+ const notifyReadyListeners = () => {
1743
+ for (const listener of readyListeners) listener();
1744
+ };
1745
+ const acquireReadyWaitSlot = () => {
1746
+ const failure = readyFailure();
1747
+ if (state === "AUTHENTICATED" || failure) return null;
1748
+ if (readyWaiterCount >= maxRequestQueueSize) throw new RequestQueueFullError();
1749
+ readyWaiterCount += 1;
1750
+ let released = false;
1751
+ return () => {
1752
+ if (released) return;
1753
+ released = true;
1754
+ readyWaiterCount = Math.max(readyWaiterCount - 1, 0);
1755
+ };
1756
+ };
1757
+ const waitForReady = async (signal, waitTimeoutMs = timeout) => {
1758
+ throwIfAborted$1(signal);
1759
+ const immediateFailure = readyFailure();
1760
+ if (!immediateFailure) {
1761
+ if (state === "AUTHENTICATED") return;
1762
+ } else throw immediateFailure;
1763
+ await new Promise((resolve, reject) => {
1764
+ let settled = false;
1765
+ let timeoutId = null;
1766
+ const cleanup = () => {
1767
+ readyListeners.delete(onStateChange);
1768
+ if (timeoutId) clearTimeout(timeoutId);
1769
+ signal?.removeEventListener("abort", onAbort);
1770
+ };
1771
+ const settle = (cb) => {
1772
+ if (settled) return;
1773
+ settled = true;
1774
+ cleanup();
1775
+ cb();
1776
+ };
1777
+ const onAbort = () => {
1778
+ settle(() => reject(abortError$2()));
1779
+ };
1780
+ const onStateChange = () => {
1781
+ const failure = readyFailure();
1782
+ if (state === "AUTHENTICATED") {
1783
+ settle(resolve);
1784
+ return;
1785
+ }
1786
+ if (failure) settle(() => reject(failure));
1787
+ };
1788
+ readyListeners.add(onStateChange);
1789
+ signal?.addEventListener("abort", onAbort, { once: true });
1790
+ timeoutId = setTimeout(() => {
1791
+ settle(() => reject(new ConnectionError("Timed out waiting for connection to become ready", { state })));
1792
+ }, waitTimeoutMs);
1793
+ onStateChange();
1794
+ });
1795
+ };
1796
+ const waitUntilReady = async (signal, waitTimeoutMs = timeout) => {
1797
+ await waitForReady(signal, waitTimeoutMs);
1798
+ };
1799
+ const shouldWaitForReconnect = () => {
1800
+ return reconnectPromise !== null || state === "RECONNECTING" || state === "DISCONNECTED" && canWaitForReconnect();
1801
+ };
1802
+ const getRetryDelayMs = (baseDelayMs) => {
1803
+ const jitter = Math.floor(Math.random() * baseDelayMs * .5);
1804
+ return Math.min(Math.max(baseDelayMs + jitter, 1), retryMaxBackoffMs);
1805
+ };
1806
+ const recordRetry = (operation, attempt, delayMs, error) => {
1807
+ const meta = getResilienceMeta(error);
1808
+ log("warn", "fitz.request.retry", {
1809
+ domain: operation.domain,
1810
+ operation: operation.operation,
1811
+ attempt,
1812
+ delayMs,
1813
+ boundary: meta?.boundary ?? "unknown",
1814
+ error: describeError(error),
1815
+ ...describeErrorFields(error)
1816
+ });
1817
+ observability?.meter?.counter("fitz.request.retry", 1, {
1818
+ domain: operation.domain,
1819
+ operation: operation.operation,
1820
+ boundary: meta?.boundary ?? "unknown"
1821
+ });
1822
+ };
1823
+ const recordRetryExhausted = (operation, attempt, error) => {
1824
+ const meta = getResilienceMeta(error);
1825
+ log("warn", "fitz.request.retry_exhausted", {
1826
+ domain: operation.domain,
1827
+ operation: operation.operation,
1828
+ attempt,
1829
+ boundary: meta?.boundary ?? "unknown",
1830
+ error: describeError(error),
1831
+ ...describeErrorFields(error)
1832
+ });
1833
+ observability?.meter?.counter("fitz.request.retry_exhausted", 1, {
1834
+ domain: operation.domain,
1835
+ operation: operation.operation,
1836
+ boundary: meta?.boundary ?? "unknown"
1837
+ });
1838
+ };
1839
+ const executeWithRetry = async (operation, task) => {
1840
+ if (!retryEnabled || operation.retryClass === "wait_only") return task();
1841
+ let attempt = 0;
1842
+ let delayMs = retryBackoffMs;
1843
+ while (true) {
1844
+ attempt += 1;
1845
+ try {
1846
+ return await task();
1847
+ } catch (error) {
1848
+ if (isAbortError$1(error)) throw error;
1849
+ if (!shouldRetryOperation(operation.retryClass, error)) throw error;
1850
+ if (attempt >= retryMaxAttempts) {
1851
+ recordRetryExhausted(operation, attempt, error);
1852
+ throw error;
1853
+ }
1854
+ const actualDelayMs = getRetryDelayMs(delayMs);
1855
+ recordRetry(operation, attempt, actualDelayMs, error);
1856
+ await sleepWithAbort(actualDelayMs, operation.signal);
1857
+ delayMs = Math.min(delayMs * 2, retryMaxBackoffMs);
1858
+ }
1859
+ }
1860
+ };
1861
+ const withWriteLock = async (operation) => {
1862
+ const prior = writeChain;
1863
+ let release;
1864
+ writeChain = new Promise((resolve) => {
1865
+ release = resolve;
1866
+ });
1867
+ await prior;
1868
+ try {
1869
+ return await operation();
1870
+ } finally {
1871
+ release();
1872
+ }
1873
+ };
1874
+ const markOutboundActivity = () => {
1875
+ lastActivityAt = Date.now();
1876
+ };
1877
+ const markRemoteActivity = () => {
1878
+ lastActivityAt = Date.now();
1879
+ };
1880
+ const stopHeartbeat = () => {
1881
+ if (heartbeatTimer) {
1882
+ clearTimeout(heartbeatTimer);
1883
+ heartbeatTimer = null;
1884
+ }
1885
+ heartbeatTransport = null;
1886
+ heartbeatPending = false;
1887
+ };
1888
+ const startHeartbeat = (activeTransport) => {
1889
+ if (!heartbeatEnabled) return;
1890
+ stopHeartbeat();
1891
+ activeTransport.enableKeepAlive?.(heartbeatIntervalMs);
1892
+ heartbeatTransport = activeTransport;
1893
+ markRemoteActivity();
1894
+ const scheduleNext = () => {
1895
+ if (closeRequested || receiveLoopAbort || heartbeatTransport !== activeTransport) return;
1896
+ heartbeatTimer = setTimeout(tick, heartbeatIntervalMs);
1897
+ };
1898
+ const tick = () => {
1899
+ heartbeatTimer = null;
1900
+ if (closeRequested || receiveLoopAbort || heartbeatTransport !== activeTransport) return;
1901
+ const now = Date.now();
1902
+ if (now - lastActivityAt < heartbeatIntervalMs) {
1903
+ scheduleNext();
1904
+ return;
1905
+ }
1906
+ const supportsHeartbeat = activeTransport.supportsHeartbeat?.() ?? true;
1907
+ if (!heartbeatPending && supportsHeartbeat && activeTransport.sendHeartbeat) {
1908
+ heartbeatPending = true;
1909
+ const heartbeatSentAt = now;
1910
+ const dispatchHeartbeat = (heartbeat) => activeTransport.sendHeartbeat(heartbeat);
1911
+ withWriteLock(async () => {
1912
+ await dispatchHeartbeat({ timeoutMs: heartbeatTimeoutMs });
1913
+ }).then(() => {
1914
+ if (heartbeatTransport !== activeTransport) return;
1915
+ heartbeatPending = false;
1916
+ markRemoteActivity();
1917
+ }).catch((error) => {
1918
+ if (heartbeatTransport !== activeTransport) return;
1919
+ heartbeatPending = false;
1920
+ if (lastActivityAt > heartbeatSentAt) return;
1921
+ const heartbeatError = new TransportError(`Heartbeat failed: ${describeError(error)}`);
1922
+ activeTransport.close().catch(() => void 0);
1923
+ handleConnectionLoss(heartbeatError);
1924
+ });
1925
+ }
1926
+ scheduleNext();
1927
+ };
1928
+ scheduleNext();
1929
+ };
1563
1930
  const openAndAuthenticate = async (isReconnect, signal) => {
1564
- throwIfAborted(signal);
1931
+ throwIfAborted$1(signal);
1565
1932
  receiveLoopAbort = false;
1566
1933
  frameParser.parseFrames(new Uint8Array(0));
1567
1934
  requestGate = createRequestGate(maxInFlightRequests, maxRequestQueueSize);
1568
1935
  const activeTransport = transportFactory();
1569
1936
  transport = activeTransport;
1937
+ stopHeartbeat();
1570
1938
  setState(isReconnect ? "RECONNECTING" : "CONNECTING");
1571
1939
  emitLifecycleEvent(isReconnect ? "reconnect_start" : "connect_start");
1572
1940
  await activeTransport.connect();
1941
+ markRemoteActivity();
1573
1942
  if (closeRequested) {
1943
+ stopHeartbeat();
1574
1944
  await activeTransport.close().catch(() => void 0);
1575
1945
  if (transport === activeTransport) transport = null;
1576
1946
  throw connectionClosedError();
1577
1947
  }
1578
- throwIfAborted(signal);
1948
+ throwIfAborted$1(signal);
1579
1949
  receiveLoop = startReceiveLoop();
1580
1950
  setState("CONNECTED");
1581
1951
  setState("AUTHENTICATING");
@@ -1584,23 +1954,34 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1584
1954
  try {
1585
1955
  await sendConnect();
1586
1956
  if (closeRequested) throw connectionClosedError();
1587
- throwIfAborted(signal);
1957
+ throwIfAborted$1(signal);
1588
1958
  await Promise.race([authOutcome.promise, sleep(authSettleDelayMs)]);
1589
1959
  if (closeRequested) throw connectionClosedError();
1590
- throwIfAborted(signal);
1960
+ throwIfAborted$1(signal);
1591
1961
  authOutcome?.resolve();
1592
1962
  authOutcome = null;
1593
1963
  if (isReconnect) {
1594
- await restoreReconnectState();
1595
- if (closeRequested) throw connectionClosedError();
1964
+ multiplexer.setConnected();
1965
+ reconnectRestoreActive = true;
1966
+ try {
1967
+ await restoreReconnectState();
1968
+ if (closeRequested) throw connectionClosedError();
1969
+ } finally {
1970
+ reconnectRestoreActive = false;
1971
+ }
1596
1972
  }
1973
+ hasEstablishedSession = true;
1974
+ reconnectExhausted = false;
1597
1975
  setState("AUTHENTICATED");
1598
- multiplexer.setConnected();
1976
+ startHeartbeat(activeTransport);
1977
+ if (!isReconnect) multiplexer.setConnected();
1599
1978
  emitLifecycleEvent(isReconnect ? "reconnect_succeeded" : "connect_succeeded");
1600
1979
  } catch (error) {
1601
1980
  authOutcome = null;
1981
+ reconnectRestoreActive = false;
1602
1982
  multiplexer.setDisconnected();
1603
1983
  emitDisconnect();
1984
+ stopHeartbeat();
1604
1985
  if (activeTransport) await activeTransport.close().catch(() => void 0);
1605
1986
  if (transport === activeTransport) transport = null;
1606
1987
  const rejectedAuth = error instanceof AuthenticationError;
@@ -1608,7 +1989,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1608
1989
  if (closeRequested) setState("CLOSED");
1609
1990
  else setState(rejectedAuth ? "CLOSED" : "DISCONNECTED");
1610
1991
  emitLifecycleEvent(isReconnect ? "reconnect_failed" : "connect_failed", error);
1611
- if (isAbortError$1(error)) throw abortError();
1992
+ if (isAbortError$1(error)) throw abortError$2();
1612
1993
  throw error;
1613
1994
  }
1614
1995
  };
@@ -1616,10 +1997,12 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1616
1997
  const token = await tokenProvider();
1617
1998
  const frame = FrameCodec.encodeFrame(1, utf8Encoder.encode(token));
1618
1999
  await ensureTransport().send(frame);
2000
+ markOutboundActivity();
1619
2001
  };
1620
2002
  const startReceiveLoop = async () => {
1621
2003
  while (!receiveLoopAbort && !closeRequested) try {
1622
2004
  const data = await ensureTransport().receive();
2005
+ markRemoteActivity();
1623
2006
  const frames = frameParser.parseFrames(data);
1624
2007
  for (const frame of frames) multiplexer.dispatch(frame.messageType, frame.payload);
1625
2008
  } catch (error) {
@@ -1629,6 +2012,17 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1629
2012
  }
1630
2013
  };
1631
2014
  const handleConnectionLoss = async (error) => {
2015
+ if (connectionLossPromise) {
2016
+ await connectionLossPromise;
2017
+ return;
2018
+ }
2019
+ connectionLossPromise = handleConnectionLossOnce(error).finally(() => {
2020
+ connectionLossPromise = null;
2021
+ });
2022
+ await connectionLossPromise;
2023
+ };
2024
+ const handleConnectionLossOnce = async (error) => {
2025
+ stopHeartbeat();
1632
2026
  multiplexer.setDisconnected();
1633
2027
  requestGate.close();
1634
2028
  emitDisconnect();
@@ -1649,6 +2043,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1649
2043
  emitLifecycleEvent("auth_rejected", error);
1650
2044
  return;
1651
2045
  }
2046
+ reconnectExhausted = false;
1652
2047
  setState("DISCONNECTED");
1653
2048
  emitLifecycleEvent("connection_lost", error);
1654
2049
  if (!reconnectEnabled) return;
@@ -1670,12 +2065,13 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1670
2065
  emitLifecycleEvent("reconnect_scheduled", void 0, attempts);
1671
2066
  const actualDelayMs = getReconnectDelayMs(delayMs);
1672
2067
  try {
1673
- await sleep(actualDelayMs);
2068
+ await sleepWithAbort(actualDelayMs, closeAbortController.signal);
1674
2069
  if (closeRequested) return;
1675
2070
  await openAndAuthenticate(true);
1676
2071
  return;
1677
2072
  } catch (error) {
1678
2073
  if (closeRequested) return;
2074
+ if (isAbortError$1(error)) return;
1679
2075
  log("warn", "fitz.connection.reconnect_retry", {
1680
2076
  attempts,
1681
2077
  delayMs: actualDelayMs,
@@ -1689,6 +2085,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1689
2085
  setState("CLOSED");
1690
2086
  return;
1691
2087
  }
2088
+ reconnectExhausted = true;
1692
2089
  setState("DISCONNECTED");
1693
2090
  emitLifecycleEvent("reconnect_exhausted", void 0, attempts);
1694
2091
  };
@@ -1709,19 +2106,13 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1709
2106
  };
1710
2107
  const setState = (newState) => {
1711
2108
  state = newState;
2109
+ notifyReadyListeners();
1712
2110
  };
1713
2111
  const sendSerialized = async (transport, data) => {
1714
- const prior = writeChain;
1715
- let release;
1716
- writeChain = new Promise((resolve) => {
1717
- release = resolve;
1718
- });
1719
- await prior;
1720
- try {
2112
+ await withWriteLock(async () => {
1721
2113
  await transport.send(data);
1722
- } finally {
1723
- release();
1724
- }
2114
+ markOutboundActivity();
2115
+ });
1725
2116
  };
1726
2117
  const emitLifecycleEvent = (event, error, attempt) => {
1727
2118
  const payload = {
@@ -1748,6 +2139,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1748
2139
  connect,
1749
2140
  close,
1750
2141
  request,
2142
+ requestDuringReconnectRestore,
1751
2143
  send,
1752
2144
  sendFireAndForget,
1753
2145
  registerNotificationHandler,
@@ -1756,6 +2148,9 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1756
2148
  onDisconnect,
1757
2149
  getMultiplexer,
1758
2150
  dispatchAsyncHandler,
2151
+ executeWithRetry,
2152
+ waitUntilReady,
2153
+ shouldWaitForReconnect,
1759
2154
  getScope,
1760
2155
  getState,
1761
2156
  isConnected,
@@ -1793,6 +2188,7 @@ function createWebSocketTransport(url, options = {}) {
1793
2188
  let receiverResolve = null;
1794
2189
  const timeout = options.timeout ?? 3e4;
1795
2190
  const maxFrameSize = options.maxFrameSize ?? 65535;
2191
+ const receiveTimeoutEnabled = options.receiveTimeout ?? true;
1796
2192
  const enqueueMessage = (data) => {
1797
2193
  if (receiverResolve) {
1798
2194
  receiverResolve(data);
@@ -1866,6 +2262,56 @@ function createWebSocketTransport(url, options = {}) {
1866
2262
  }
1867
2263
  });
1868
2264
  };
2265
+ const sendHeartbeat = async (heartbeatOptions) => {
2266
+ if (!connected || !ws) throw new TransportError("WebSocket is not connected");
2267
+ const activeWs = ws;
2268
+ if (typeof activeWs.ping !== "function" || typeof activeWs.once !== "function" || typeof activeWs.removeListener !== "function") throw new TransportError("WebSocket heartbeat is not supported");
2269
+ const socket = activeWs;
2270
+ return new Promise((resolve, reject) => {
2271
+ let settled = false;
2272
+ let timeoutId = null;
2273
+ const cleanup = () => {
2274
+ if (timeoutId) clearTimeout(timeoutId);
2275
+ socket.removeListener("pong", onPong);
2276
+ socket.removeListener("close", onClose);
2277
+ socket.removeListener("error", onError);
2278
+ };
2279
+ const settle = (callback) => {
2280
+ if (settled) return;
2281
+ settled = true;
2282
+ cleanup();
2283
+ callback();
2284
+ };
2285
+ const onPong = () => {
2286
+ settle(resolve);
2287
+ };
2288
+ const onClose = () => {
2289
+ settle(() => reject(new TransportError("WebSocket closed during heartbeat")));
2290
+ };
2291
+ const onError = (...args) => {
2292
+ const event = args[0];
2293
+ const message = event instanceof Error ? event.message : event?.message || "unknown error";
2294
+ settle(() => reject(new TransportError(`WebSocket heartbeat failed: ${message}`)));
2295
+ };
2296
+ timeoutId = setTimeout(() => {
2297
+ settle(() => reject(new TimeoutError(`WebSocket heartbeat timeout after ${heartbeatOptions.timeoutMs}ms`)));
2298
+ }, heartbeatOptions.timeoutMs);
2299
+ socket.once("pong", onPong);
2300
+ socket.once("close", onClose);
2301
+ socket.once("error", onError);
2302
+ try {
2303
+ socket.ping(new Uint8Array(), void 0, (err) => {
2304
+ if (err) settle(() => reject(new TransportError(`WebSocket ping failed: ${err.message}`)));
2305
+ });
2306
+ } catch (err) {
2307
+ settle(() => reject(new TransportError(`WebSocket heartbeat error: ${err instanceof Error ? err.message : String(err)}`)));
2308
+ }
2309
+ });
2310
+ };
2311
+ const supportsHeartbeat = () => {
2312
+ return typeof ws?.ping === "function" && typeof ws?.once === "function" && typeof ws?.removeListener === "function";
2313
+ };
2314
+ const enableKeepAlive = () => void 0;
1869
2315
  const receive = async () => {
1870
2316
  if (receiveQueue.length > 0) {
1871
2317
  const message = receiveQueue.shift();
@@ -1874,12 +2320,12 @@ function createWebSocketTransport(url, options = {}) {
1874
2320
  }
1875
2321
  if (!connected) throw new TransportError("Connection closed");
1876
2322
  return new Promise((resolve, reject) => {
1877
- const timeoutId = setTimeout(() => {
2323
+ const timeoutId = receiveTimeoutEnabled ? setTimeout(() => {
1878
2324
  receiverResolve = null;
1879
2325
  reject(new TimeoutError(`WebSocket receive timeout after ${timeout}ms`));
1880
- }, timeout);
2326
+ }, timeout) : null;
1881
2327
  receiverResolve = (data) => {
1882
- clearTimeout(timeoutId);
2328
+ if (timeoutId) clearTimeout(timeoutId);
1883
2329
  receiverResolve = null;
1884
2330
  if (data === null) {
1885
2331
  reject(new TransportError("Connection closed"));
@@ -1914,6 +2360,9 @@ function createWebSocketTransport(url, options = {}) {
1914
2360
  connect,
1915
2361
  send,
1916
2362
  receive,
2363
+ sendHeartbeat,
2364
+ supportsHeartbeat,
2365
+ enableKeepAlive,
1917
2366
  close,
1918
2367
  getUrl,
1919
2368
  isConnected
@@ -1939,6 +2388,7 @@ function createTcpTransport(url, options = {}) {
1939
2388
  let receiverResolve = null;
1940
2389
  const timeout = options.timeout ?? 3e4;
1941
2390
  const maxFrameSize = options.maxFrameSize ?? 65535;
2391
+ const receiveTimeoutEnabled = options.receiveTimeout ?? true;
1942
2392
  let lengthBuffer = new Uint8Array(4);
1943
2393
  let lengthOffset = 0;
1944
2394
  let currentMessageLength = null;
@@ -2013,7 +2463,7 @@ function createTcpTransport(url, options = {}) {
2013
2463
  clearTimeout(connectTimeout);
2014
2464
  connected = true;
2015
2465
  activeSocket.setNoDelay(true);
2016
- activeSocket.setTimeout(timeout);
2466
+ activeSocket.setTimeout(receiveTimeoutEnabled ? timeout : 0);
2017
2467
  resolve();
2018
2468
  });
2019
2469
  activeSocket.on("data", (chunk) => {
@@ -2031,8 +2481,10 @@ function createTcpTransport(url, options = {}) {
2031
2481
  if (receiverResolve) receiverResolve(new Uint8Array(0));
2032
2482
  });
2033
2483
  activeSocket.on("timeout", () => {
2034
- activeSocket.destroy();
2035
- connected = false;
2484
+ if (receiveTimeoutEnabled) {
2485
+ activeSocket.destroy();
2486
+ connected = false;
2487
+ }
2036
2488
  });
2037
2489
  } catch (err) {
2038
2490
  reject(new TransportError(`Failed to create TCP socket: ${err instanceof Error ? err.message : String(err)}`));
@@ -2057,6 +2509,10 @@ function createTcpTransport(url, options = {}) {
2057
2509
  });
2058
2510
  });
2059
2511
  };
2512
+ const enableKeepAlive = (intervalMs) => {
2513
+ socket?.setKeepAlive(true, intervalMs);
2514
+ };
2515
+ const supportsHeartbeat = () => false;
2060
2516
  const receive = async () => {
2061
2517
  if (receiveQueue.length > 0) {
2062
2518
  const message = receiveQueue.shift();
@@ -2064,12 +2520,12 @@ function createTcpTransport(url, options = {}) {
2064
2520
  return message;
2065
2521
  }
2066
2522
  return new Promise((resolve, reject) => {
2067
- const timeoutId = setTimeout(() => {
2523
+ const timeoutId = receiveTimeoutEnabled ? setTimeout(() => {
2068
2524
  receiverResolve = null;
2069
2525
  reject(new TimeoutError(`TCP receive timeout after ${timeout}ms`));
2070
- }, timeout);
2526
+ }, timeout) : null;
2071
2527
  receiverResolve = (data) => {
2072
- clearTimeout(timeoutId);
2528
+ if (timeoutId) clearTimeout(timeoutId);
2073
2529
  receiverResolve = null;
2074
2530
  if (data.length === 0) reject(new TransportError("Connection closed"));
2075
2531
  else resolve(data);
@@ -2101,6 +2557,8 @@ function createTcpTransport(url, options = {}) {
2101
2557
  connect,
2102
2558
  send,
2103
2559
  receive,
2560
+ supportsHeartbeat,
2561
+ enableKeepAlive,
2104
2562
  close,
2105
2563
  getUrl,
2106
2564
  isConnected
@@ -2139,11 +2597,23 @@ function createTransport(url, transportType = "auto", options = {}) {
2139
2597
  //#region src/domains/base.ts
2140
2598
  function createDomainClient(connection) {
2141
2599
  const requestFrame = async (messageType, payload, signal) => connection.request(messageType, payload, signal);
2600
+ const requestReconnectFrame = async (messageType, payload, signal) => {
2601
+ const resilientConnection = connection;
2602
+ if (typeof resilientConnection.requestDuringReconnectRestore === "function") return await resilientConnection.requestDuringReconnectRestore(messageType, payload, signal);
2603
+ return await connection.request(messageType, payload, signal);
2604
+ };
2142
2605
  const sendFrame = async (messageType, payload) => connection.send(messageType, payload);
2606
+ const runWithRetry = async (operation, task) => {
2607
+ const resilientConnection = connection;
2608
+ if (typeof resilientConnection.executeWithRetry === "function") return resilientConnection.executeWithRetry(operation, task);
2609
+ return task();
2610
+ };
2143
2611
  return {
2144
2612
  connection,
2145
2613
  requestFrame,
2146
- sendFrame
2614
+ requestReconnectFrame,
2615
+ sendFrame,
2616
+ runWithRetry
2147
2617
  };
2148
2618
  }
2149
2619
  //#endregion
@@ -2321,8 +2791,11 @@ function createAsyncIterableIterator(iterator) {
2321
2791
  //#region src/domains/kv/transaction.ts
2322
2792
  function createKvTransaction(connection, route, txId) {
2323
2793
  let closed = false;
2324
- const unsubscribeDisconnect = connection.onDisconnect(() => {
2794
+ const resilientConnection = connection;
2795
+ let unsubscribeDisconnect = () => void 0;
2796
+ unsubscribeDisconnect = connection.onDisconnect(() => {
2325
2797
  closed = true;
2798
+ unsubscribeDisconnect();
2326
2799
  });
2327
2800
  const ensureOpen = () => {
2328
2801
  if (closed) throw new KvError("Transaction already closed", "TX_CLOSED");
@@ -2337,6 +2810,10 @@ function createKvTransaction(connection, route, txId) {
2337
2810
  [5]: "OperationNotAllowed"
2338
2811
  }[status] ?? `Unknown(${status})`}`, operation, status);
2339
2812
  };
2813
+ const runWithRetry = async (operation, task) => {
2814
+ if (typeof resilientConnection.executeWithRetry === "function") return resilientConnection.executeWithRetry(operation, task);
2815
+ return task();
2816
+ };
2340
2817
  const put = async (key, value, signal) => {
2341
2818
  ensureOpen();
2342
2819
  const payload = KvCodec.encodePut(txId, route, key, value);
@@ -2351,15 +2828,22 @@ function createKvTransaction(connection, route, txId) {
2351
2828
  };
2352
2829
  const get = async (key, signal) => {
2353
2830
  ensureOpen();
2354
- const payload = KvCodec.encodeGet(txId, route, key);
2355
- const response = await connection.request(103, payload, signal);
2356
- const decoded = KvCodec.decodeGetResponse(response);
2357
- checkStatus(decoded.status, "GET");
2358
- if (!decoded.found || !decoded.value) return { type: "not-found" };
2359
- return {
2360
- type: "found",
2361
- value: decoded.value
2362
- };
2831
+ return runWithRetry({
2832
+ domain: "kv",
2833
+ operation: "get",
2834
+ retryClass: "replayable_read",
2835
+ signal
2836
+ }, async () => {
2837
+ const payload = KvCodec.encodeGet(txId, route, key);
2838
+ const response = await connection.request(103, payload, signal);
2839
+ const decoded = KvCodec.decodeGetResponse(response);
2840
+ checkStatus(decoded.status, "GET");
2841
+ if (!decoded.found || !decoded.value) return { type: "not-found" };
2842
+ return {
2843
+ type: "found",
2844
+ value: decoded.value
2845
+ };
2846
+ });
2363
2847
  };
2364
2848
  const deleteItem = async (key, signal) => {
2365
2849
  ensureOpen();
@@ -2375,11 +2859,18 @@ function createKvTransaction(connection, route, txId) {
2375
2859
  };
2376
2860
  const scan = async (options = {}, signal) => {
2377
2861
  ensureOpen();
2378
- const payload = KvCodec.encodeScan(txId, route, options);
2379
- const response = await connection.request(108, payload, signal);
2380
- const decoded = KvCodec.decodeScanResponse(response);
2381
- checkStatus(decoded.status, "SCAN");
2382
- return createAsyncIterableIterator(createSliceIterator(decoded.keys));
2862
+ return runWithRetry({
2863
+ domain: "kv",
2864
+ operation: "scan",
2865
+ retryClass: "replayable_read",
2866
+ signal
2867
+ }, async () => {
2868
+ const payload = KvCodec.encodeScan(txId, route, options);
2869
+ const response = await connection.request(108, payload, signal);
2870
+ const decoded = KvCodec.decodeScanResponse(response);
2871
+ checkStatus(decoded.status, "SCAN");
2872
+ return createAsyncIterableIterator(createSliceIterator(decoded.keys));
2873
+ });
2383
2874
  };
2384
2875
  const commit = async (signal) => {
2385
2876
  ensureOpen();
@@ -2446,6 +2937,64 @@ const KvClient = function(connection) {
2446
2937
  return createKvClient(connection);
2447
2938
  };
2448
2939
  //#endregion
2940
+ //#region src/core/wake-gate.ts
2941
+ function abortError$1() {
2942
+ const error = /* @__PURE__ */ new Error("The operation was aborted");
2943
+ error.name = "AbortError";
2944
+ return error;
2945
+ }
2946
+ function createWakeGate() {
2947
+ let currentVersion = 0;
2948
+ const waiters = /* @__PURE__ */ new Set();
2949
+ const cleanup = (waiter) => {
2950
+ waiters.delete(waiter);
2951
+ if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener("abort", waiter.onAbort);
2952
+ };
2953
+ const wake = () => {
2954
+ currentVersion += 1;
2955
+ const version = currentVersion;
2956
+ for (const waiter of Array.from(waiters)) {
2957
+ cleanup(waiter);
2958
+ waiter.resolve(version);
2959
+ }
2960
+ return version;
2961
+ };
2962
+ const waitAfter = async (observedVersion, options = {}) => {
2963
+ if (currentVersion > observedVersion) return currentVersion;
2964
+ if (options.signal?.aborted) throw abortError$1();
2965
+ return await new Promise((resolve, reject) => {
2966
+ const waiter = {
2967
+ observedVersion,
2968
+ resolve,
2969
+ reject,
2970
+ signal: options.signal
2971
+ };
2972
+ waiter.onAbort = () => {
2973
+ cleanup(waiter);
2974
+ reject(abortError$1());
2975
+ };
2976
+ if (options.signal) options.signal.addEventListener("abort", waiter.onAbort, { once: true });
2977
+ if (currentVersion > observedVersion) {
2978
+ cleanup(waiter);
2979
+ resolve(currentVersion);
2980
+ return;
2981
+ }
2982
+ waiters.add(waiter);
2983
+ });
2984
+ };
2985
+ const wait = async (options) => {
2986
+ return await waitAfter(currentVersion, options);
2987
+ };
2988
+ return {
2989
+ get version() {
2990
+ return currentVersion;
2991
+ },
2992
+ wake,
2993
+ waitAfter,
2994
+ wait
2995
+ };
2996
+ }
2997
+ //#endregion
2449
2998
  //#region src/domains/queue/codec.ts
2450
2999
  /**
2451
3000
  * Queue domain codec for encoding and decoding protocol messages.
@@ -2659,7 +3208,17 @@ const QueueCodec = {
2659
3208
  //#endregion
2660
3209
  //#region src/domains/queue/types.ts
2661
3210
  function createQueueItem(id, token, body, route, connection) {
3211
+ let closed = false;
3212
+ let unsubscribeDisconnect = () => void 0;
3213
+ unsubscribeDisconnect = connection.onDisconnect(() => {
3214
+ closed = true;
3215
+ unsubscribeDisconnect();
3216
+ });
3217
+ const ensureOpen = () => {
3218
+ if (closed) throw new QueueError("Queue item is no longer valid after disconnect", "ITEM_CLOSED");
3219
+ };
2662
3220
  const extend = async (leaseSecs, signal) => {
3221
+ ensureOpen();
2663
3222
  const payload = QueueCodec.encodeExtend(route, id, token, leaseSecs);
2664
3223
  const response = await connection.request(203, payload, signal);
2665
3224
  const decoded = QueueCodec.decodeExtendResponse(response);
@@ -2670,6 +3229,7 @@ function createQueueItem(id, token, body, route, connection) {
2670
3229
  }
2671
3230
  };
2672
3231
  const complete = async (signal) => {
3232
+ ensureOpen();
2673
3233
  const requestPayload = QueueCodec.encodeComplete(route, id, token);
2674
3234
  const response = await connection.request(204, requestPayload, signal);
2675
3235
  const decoded = QueueCodec.decodeCompleteResponse(response);
@@ -2678,9 +3238,12 @@ function createQueueItem(id, token, body, route, connection) {
2678
3238
  const statusName = QueueStatus[errorCode] || `Unknown(${errorCode})`;
2679
3239
  throw new QueueError(`COMPLETE failed: ${decoded.errorMessage ?? statusName}`, statusName, errorCode);
2680
3240
  }
3241
+ closed = true;
3242
+ unsubscribeDisconnect();
2681
3243
  };
2682
3244
  const testOnlyInvalidToken = () => id + 1n;
2683
3245
  const testOnlyCompleteWithToken = async (tokenToUse, signal) => {
3246
+ ensureOpen();
2684
3247
  const requestPayload = QueueCodec.encodeComplete(route, id, tokenToUse);
2685
3248
  const response = await connection.request(204, requestPayload, signal);
2686
3249
  const decoded = QueueCodec.decodeCompleteResponse(response);
@@ -2689,6 +3252,8 @@ function createQueueItem(id, token, body, route, connection) {
2689
3252
  const statusName = QueueStatus[errorCode] || `Unknown(${errorCode})`;
2690
3253
  throw new QueueError(`COMPLETE failed: ${decoded.errorMessage ?? statusName}`, statusName, errorCode);
2691
3254
  }
3255
+ closed = true;
3256
+ unsubscribeDisconnect();
2692
3257
  };
2693
3258
  return {
2694
3259
  body,
@@ -2726,7 +3291,7 @@ let QueueStatus = /* @__PURE__ */ function(QueueStatus) {
2726
3291
  * Queue domain client.
2727
3292
  */
2728
3293
  function createQueueClient(connection) {
2729
- const { requestFrame } = createDomainClient(connection);
3294
+ const { requestFrame, requestReconnectFrame, runWithRetry } = createDomainClient(connection);
2730
3295
  const subscriptionsByPattern = /* @__PURE__ */ new Map();
2731
3296
  const patternsBySubId = /* @__PURE__ */ new Map();
2732
3297
  let notificationHandlerRegistered = false;
@@ -2740,7 +3305,7 @@ function createQueueClient(connection) {
2740
3305
  subscriptionsByPattern.clear();
2741
3306
  patternsBySubId.clear();
2742
3307
  for (const subscription of subscriptions) {
2743
- const subId = await subscribeWire(subscription.pattern);
3308
+ const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
2744
3309
  subscriptionsByPattern.set(subscription.pattern, {
2745
3310
  subId,
2746
3311
  handlers: new Map(subscription.handlers)
@@ -2750,11 +3315,17 @@ function createQueueClient(connection) {
2750
3315
  });
2751
3316
  const enqueue = async (route, body, options) => {
2752
3317
  assertQueueRoute(route);
2753
- const response = await requestFrame(200, QueueCodec.encodeEnqueue(route, body, options));
2754
- const decoded = QueueCodec.decodeEnqueueResponse(response);
2755
- checkStatus(decoded, "ENQUEUE");
2756
- if (decoded.messageId === void 0) throw new QueueError("ENQUEUE response missing messageId", "MISSING_MESSAGE_ID");
2757
- return decoded.messageId;
3318
+ return runWithRetry({
3319
+ domain: "queue",
3320
+ operation: "enqueue",
3321
+ retryClass: "confirmed_negative_retry"
3322
+ }, async () => {
3323
+ const response = await requestFrame(200, QueueCodec.encodeEnqueue(route, body, options));
3324
+ const decoded = QueueCodec.decodeEnqueueResponse(response);
3325
+ checkStatus(decoded, "ENQUEUE");
3326
+ if (decoded.messageId === void 0) throw new QueueError("ENQUEUE response missing messageId", "MISSING_MESSAGE_ID");
3327
+ return decoded.messageId;
3328
+ });
2758
3329
  };
2759
3330
  const reserve = async (route, leaseSeconds, batchSize = 1, waitSeconds = 0) => {
2760
3331
  assertQueueReserveRoute(route);
@@ -2762,43 +3333,55 @@ function createQueueClient(connection) {
2762
3333
  let items = await reserveOnce(route, leaseSeconds, batchSize);
2763
3334
  if (items.length > 0) return items;
2764
3335
  const deadline = Date.now() + waitSeconds * 1e3;
2765
- let pendingNotifications = 0;
2766
- let waiter;
2767
- const subscription = await subscribe(route, async () => {
2768
- pendingNotifications += 1;
2769
- if (!waiter) return;
2770
- const resolve = waiter;
2771
- waiter = void 0;
2772
- pendingNotifications = 0;
2773
- resolve();
3336
+ const wakeGate = createWakeGate();
3337
+ const subscription = await subscribe(route, () => {
3338
+ wakeGate.wake();
2774
3339
  });
2775
3340
  try {
2776
3341
  while (true) {
3342
+ const observed = wakeGate.version;
2777
3343
  items = await reserveOnce(route, leaseSeconds, batchSize);
2778
3344
  if (items.length > 0) return items;
2779
3345
  const remainingMs = deadline - Date.now();
2780
3346
  if (remainingMs <= 0) return items;
2781
- await new Promise((resolve) => {
2782
- if (pendingNotifications > 0) {
2783
- pendingNotifications = 0;
2784
- resolve();
2785
- return;
2786
- }
2787
- const release = () => {
2788
- clearTimeout(timeoutId);
2789
- if (waiter === release) waiter = void 0;
2790
- resolve();
2791
- };
2792
- const timeoutId = setTimeout(release, remainingMs);
2793
- waiter = release;
3347
+ const waitPromise = wakeGate.waitAfter(observed);
3348
+ let timeoutId = null;
3349
+ const timeoutPromise = new Promise((resolve) => {
3350
+ timeoutId = setTimeout(() => {
3351
+ resolve("timeout");
3352
+ }, remainingMs);
2794
3353
  });
3354
+ if (await Promise.race([waitPromise.then(() => {
3355
+ if (timeoutId) clearTimeout(timeoutId);
3356
+ return "wake";
3357
+ }), timeoutPromise]) === "timeout") return items;
2795
3358
  }
2796
3359
  } finally {
2797
- await subscription.unsubscribe();
3360
+ await subscription.unsubscribe().catch(() => void 0);
2798
3361
  }
2799
3362
  };
2800
- const reserveOnce = async (route, leaseSeconds, batchSize) => {
2801
- const response = await requestFrame(202, QueueCodec.encodeReserve(route, leaseSeconds, batchSize));
3363
+ const reserveWhenAvailable = async function* (route, options) {
3364
+ assertQueueReserveRoute(route);
3365
+ const wakeGate = createWakeGate();
3366
+ const subscription = await subscribe(route, () => {
3367
+ wakeGate.wake();
3368
+ });
3369
+ try {
3370
+ while (true) {
3371
+ const observed = wakeGate.version;
3372
+ const items = await reserveOnce(route, options.leaseSeconds, options.batchSize ?? 1, options.signal);
3373
+ if (items.length > 0) {
3374
+ yield items;
3375
+ continue;
3376
+ }
3377
+ await wakeGate.waitAfter(observed, { signal: options.signal });
3378
+ }
3379
+ } finally {
3380
+ await subscription.unsubscribe().catch(() => void 0);
3381
+ }
3382
+ };
3383
+ const reserveOnce = async (route, leaseSeconds, batchSize, signal) => {
3384
+ const response = await requestFrame(202, QueueCodec.encodeReserve(route, leaseSeconds, batchSize), signal);
2802
3385
  const decoded = QueueCodec.decodeReserveResponse(response);
2803
3386
  checkStatus(decoded, "RESERVE");
2804
3387
  return (decoded.items ?? []).map((item) => createQueueItem(item.id, item.token, item.body, route, connection));
@@ -2810,8 +3393,8 @@ function createQueueClient(connection) {
2810
3393
  if (existing) return addLocalSubscription(pattern, existing.subId, handler);
2811
3394
  return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
2812
3395
  };
2813
- const subscribeWire = async (pattern) => {
2814
- const response = await requestFrame(207, QueueCodec.encodeSubscribe(wireWatchPattern(pattern)));
3396
+ const subscribeWire = async (pattern, request = requestFrame) => {
3397
+ const response = await request(207, QueueCodec.encodeSubscribe(wireWatchPattern(pattern)));
2815
3398
  const decoded = QueueCodec.decodeSubscribeResponse(response);
2816
3399
  checkStatus(decoded, "SUBSCRIBE");
2817
3400
  if (decoded.subId === void 0) throw new QueueError("SUBSCRIBE response missing subId", "MISSING_SUB_ID");
@@ -2878,11 +3461,16 @@ function createQueueClient(connection) {
2878
3461
  [4]: "QueueFull",
2879
3462
  [5]: "InvalidDelay"
2880
3463
  }[errorCode] ?? `Unknown(${errorCode})`;
2881
- throw new QueueError(`${operation} failed: ${response.errorMessage ?? statusName}`, statusName, errorCode);
3464
+ throw attachResilienceMeta(new QueueError(`${operation} failed: ${response.errorMessage ?? statusName}`, statusName, errorCode), {
3465
+ boundary: "post-send",
3466
+ failureKind: "domain",
3467
+ explicitNegative: true
3468
+ });
2882
3469
  };
2883
3470
  return {
2884
3471
  enqueue,
2885
3472
  reserve,
3473
+ reserveWhenAvailable,
2886
3474
  subscribe
2887
3475
  };
2888
3476
  }
@@ -3193,16 +3781,35 @@ function createRpcSubscription(route, unsubscribeFn) {
3193
3781
  */
3194
3782
  function createRpcResponseWriter(connection, correlationId) {
3195
3783
  let sequence = 0n;
3784
+ let stale = false;
3785
+ let unsubscribeDisconnect = () => void 0;
3786
+ const dispose = () => {
3787
+ if (stale) return;
3788
+ stale = true;
3789
+ unsubscribeDisconnect();
3790
+ unsubscribeDisconnect = () => void 0;
3791
+ };
3792
+ unsubscribeDisconnect = connection.onDisconnect(() => {
3793
+ dispose();
3794
+ });
3196
3795
  const send = async (body, isEnd) => {
3796
+ if (stale) throw new ConnectionError("RPC response writer is no longer valid");
3197
3797
  const payload = RpcCodec.encodeResponse(correlationId, sequence++, body, isEnd);
3198
3798
  try {
3199
3799
  await connection.send(303, payload);
3800
+ if (isEnd) dispose();
3200
3801
  } catch (error) {
3201
- if (isBenignShutdownError(error, connection)) return;
3802
+ if (isBenignShutdownError(error, connection)) {
3803
+ dispose();
3804
+ return;
3805
+ }
3202
3806
  throw error;
3203
3807
  }
3204
3808
  };
3205
- return { send };
3809
+ return {
3810
+ send,
3811
+ dispose
3812
+ };
3206
3813
  }
3207
3814
  function isBenignShutdownError(error, connection) {
3208
3815
  if (connection.getState() !== "AUTHENTICATED") return true;
@@ -3350,7 +3957,7 @@ const RpcClient = function(connection) {
3350
3957
  return createRpcClient(connection);
3351
3958
  };
3352
3959
  function createRpcClient(connection) {
3353
- const { requestFrame } = createDomainClient(connection);
3960
+ const { requestFrame, requestReconnectFrame } = createDomainClient(connection);
3354
3961
  const pendingRpcs = /* @__PURE__ */ new Map();
3355
3962
  const workers = /* @__PURE__ */ new Map();
3356
3963
  let initialized = false;
@@ -3368,7 +3975,7 @@ function createRpcClient(connection) {
3368
3975
  if (workers.size === 0) return;
3369
3976
  const registeredWorkers = Array.from(workers.entries());
3370
3977
  workers.clear();
3371
- for (const [route, handler] of registeredWorkers) await registerWorker(route, handler);
3978
+ for (const [route, handler] of registeredWorkers) await registerWorkerInternal(route, handler, requestReconnectFrame);
3372
3979
  });
3373
3980
  const call = async (route, body, options) => {
3374
3981
  assertRpcRoute(route);
@@ -3394,13 +4001,16 @@ function createRpcClient(connection) {
3394
4001
  throw error;
3395
4002
  }
3396
4003
  };
3397
- const registerWorker = async (route, handler) => {
3398
- assertRpcRoute(route);
3399
- initRpcHandler();
3400
- const response = await requestFrame(300, RpcCodec.encodeSubscribeWorker(route));
4004
+ const registerWorkerInternal = async (route, handler, request = requestFrame) => {
4005
+ const response = await request(300, RpcCodec.encodeSubscribeWorker(route));
3401
4006
  const decoded = RpcCodec.decodeSubscribeWorkerResponse(response);
3402
4007
  if (decoded.status !== 0) throw new RpcError(`RPC SUBSCRIBE_WORKER failed: status ${decoded.status}`, "SUBSCRIBE_FAILED", decoded.status);
3403
4008
  workers.set(route, handler);
4009
+ };
4010
+ const registerWorker = async (route, handler) => {
4011
+ assertRpcRoute(route);
4012
+ initRpcHandler();
4013
+ await registerWorkerInternal(route, handler);
3404
4014
  const unsubscribeFn = async (registeredRoute) => {
3405
4015
  await unregisterWorker(registeredRoute);
3406
4016
  };
@@ -3469,6 +4079,8 @@ function createRpcClient(connection) {
3469
4079
  try {
3470
4080
  await writer.send(utf8Encoder.encode(`Handler error: ${message}`), true);
3471
4081
  } catch {}
4082
+ } finally {
4083
+ writer.dispose();
3472
4084
  }
3473
4085
  });
3474
4086
  };
@@ -3704,7 +4316,17 @@ function createLeaseSubscription(subId, pattern, unsubscribeFn) {
3704
4316
  function createLease(token, expiresAt, route, connection) {
3705
4317
  let currentToken = token;
3706
4318
  let currentExpiry = expiresAt;
4319
+ let closed = false;
4320
+ let unsubscribeDisconnect = () => void 0;
4321
+ unsubscribeDisconnect = connection.onDisconnect(() => {
4322
+ closed = true;
4323
+ unsubscribeDisconnect();
4324
+ });
4325
+ const ensureOpen = () => {
4326
+ if (closed) throw new LeaseError("Lease handle is no longer valid after disconnect", "CLOSED");
4327
+ };
3707
4328
  const extend = async (ttlSecs, signal) => {
4329
+ ensureOpen();
3708
4330
  const requestPayload = LeaseCodec.encodeExtend(route, currentToken, ttlSecs);
3709
4331
  const data = assertSuccess(await connection.request(401, requestPayload, signal), "EXTEND");
3710
4332
  if (data && data.length >= 8) currentToken = new BufferReader(data).readU64BE();
@@ -3712,12 +4334,16 @@ function createLease(token, expiresAt, route, connection) {
3712
4334
  return currentExpiry;
3713
4335
  };
3714
4336
  const release = async (signal) => {
4337
+ ensureOpen();
3715
4338
  const payload = LeaseCodec.encodeRelease(route, currentToken);
3716
4339
  assertSuccess(await connection.request(402, payload, signal), "RELEASE");
4340
+ closed = true;
4341
+ unsubscribeDisconnect();
3717
4342
  };
3718
4343
  const getExpiry = () => currentExpiry;
3719
4344
  const testOnlyInvalidToken = () => currentToken + 1n;
3720
4345
  const testOnlyExtendWithToken = async (tokenToUse, ttlSecs, signal) => {
4346
+ ensureOpen();
3721
4347
  const requestPayload = LeaseCodec.encodeExtend(route, tokenToUse, ttlSecs);
3722
4348
  const data = assertSuccess(await connection.request(401, requestPayload, signal), "EXTEND");
3723
4349
  if (data && data.length >= 8) currentToken = new BufferReader(data).readU64BE();
@@ -3725,8 +4351,11 @@ function createLease(token, expiresAt, route, connection) {
3725
4351
  return currentExpiry;
3726
4352
  };
3727
4353
  const testOnlyReleaseWithToken = async (tokenToUse, signal) => {
4354
+ ensureOpen();
3728
4355
  const payload = LeaseCodec.encodeRelease(route, tokenToUse);
3729
4356
  assertSuccess(await connection.request(402, payload, signal), "RELEASE");
4357
+ closed = true;
4358
+ unsubscribeDisconnect();
3730
4359
  };
3731
4360
  return {
3732
4361
  extend,
@@ -3743,7 +4372,7 @@ function createLease(token, expiresAt, route, connection) {
3743
4372
  * Lease domain client.
3744
4373
  */
3745
4374
  function createLeaseClient(connection) {
3746
- const { requestFrame } = createDomainClient(connection);
4375
+ const { requestFrame, requestReconnectFrame, runWithRetry } = createDomainClient(connection);
3747
4376
  const subscriptionsByPattern = /* @__PURE__ */ new Map();
3748
4377
  let initialized = false;
3749
4378
  let nextHandlerId = 1;
@@ -3755,7 +4384,7 @@ function createLeaseClient(connection) {
3755
4384
  }));
3756
4385
  subscriptionsByPattern.clear();
3757
4386
  for (const subscription of subscriptions) {
3758
- const subId = await subscribeWire(subscription.pattern);
4387
+ const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
3759
4388
  subscriptionsByPattern.set(subscription.pattern, {
3760
4389
  subId,
3761
4390
  handlers: new Map(subscription.handlers)
@@ -3772,16 +4401,22 @@ function createLeaseClient(connection) {
3772
4401
  };
3773
4402
  const query = async (route) => {
3774
4403
  assertExactLeaseRoute(route);
3775
- const response = await requestFrame(403, LeaseCodec.encodeQuery(route));
3776
- const decoded = LeaseCodec.decodeQueryResponse(response);
3777
- if (decoded.status !== 0) throw new LeaseError("QUERY failed", "QUERY_FAILED", decoded.status);
3778
- return {
3779
- isHeld: decoded.isHeld ?? false,
3780
- owner: decoded.owner,
3781
- token: decoded.token,
3782
- ttlRemainingSecs: decoded.ttlRemainingSecs,
3783
- expiresAt: decoded.expiresAt
3784
- };
4404
+ return runWithRetry({
4405
+ domain: "lease",
4406
+ operation: "query",
4407
+ retryClass: "replayable_read"
4408
+ }, async () => {
4409
+ const response = await requestFrame(403, LeaseCodec.encodeQuery(route));
4410
+ const decoded = LeaseCodec.decodeQueryResponse(response);
4411
+ if (decoded.status !== 0) throw new LeaseError("QUERY failed", "QUERY_FAILED", decoded.status);
4412
+ return {
4413
+ isHeld: decoded.isHeld ?? false,
4414
+ owner: decoded.owner,
4415
+ token: decoded.token,
4416
+ ttlRemainingSecs: decoded.ttlRemainingSecs,
4417
+ expiresAt: decoded.expiresAt
4418
+ };
4419
+ });
3785
4420
  };
3786
4421
  const subscribe = async (pattern, handler) => {
3787
4422
  assertExactLeaseRoute(pattern);
@@ -3790,8 +4425,8 @@ function createLeaseClient(connection) {
3790
4425
  if (existing) return addLocalSubscription(pattern, existing.subId, handler);
3791
4426
  return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
3792
4427
  };
3793
- const subscribeWire = async (pattern) => {
3794
- const response = await requestFrame(407, LeaseCodec.encodeSubscribe(pattern));
4428
+ const subscribeWire = async (pattern, request = requestFrame) => {
4429
+ const response = await request(407, LeaseCodec.encodeSubscribe(pattern));
3795
4430
  const decoded = LeaseCodec.decodeSubscribeResponse(response);
3796
4431
  if (decoded.subId === void 0) throw new LeaseError("SUBSCRIBE failed", "SUBSCRIBE_FAILED");
3797
4432
  return decoded.subId;
@@ -3938,7 +4573,7 @@ function createNoticeSubscription(subId, pattern, unsubscribeFn) {
3938
4573
  * Notice domain client.
3939
4574
  */
3940
4575
  function createNoticeClient(connection) {
3941
- const { requestFrame } = createDomainClient(connection);
4576
+ const { requestFrame, requestReconnectFrame } = createDomainClient(connection);
3942
4577
  const subscriptionsByPattern = /* @__PURE__ */ new Map();
3943
4578
  const patternsBySubId = /* @__PURE__ */ new Map();
3944
4579
  let initialized = false;
@@ -3952,7 +4587,7 @@ function createNoticeClient(connection) {
3952
4587
  subscriptionsByPattern.clear();
3953
4588
  patternsBySubId.clear();
3954
4589
  for (const subscription of subscriptions) {
3955
- const subId = await subscribeWire(subscription.pattern);
4590
+ const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
3956
4591
  subscriptionsByPattern.set(subscription.pattern, {
3957
4592
  subId,
3958
4593
  handlers: new Map(subscription.handlers)
@@ -3978,8 +4613,8 @@ function createNoticeClient(connection) {
3978
4613
  if (existing) return addLocalSubscription(pattern, existing.subId, handler);
3979
4614
  return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
3980
4615
  };
3981
- const subscribeWire = async (pattern) => {
3982
- const response = await requestFrame(501, NoticeCodec.encodeSubscribe(pattern));
4616
+ const subscribeWire = async (pattern, request = requestFrame) => {
4617
+ const response = await request(501, NoticeCodec.encodeSubscribe(pattern));
3983
4618
  const decoded = NoticeCodec.decodeSubscribeResponse(response);
3984
4619
  if (decoded.subId === void 0) throw new NoticeError("SUBSCRIBE response missing subId", "MISSING_SUB_ID");
3985
4620
  return decoded.subId;
@@ -4421,8 +5056,10 @@ function createStreamSubscription(subId, pattern, unsubscribeFn) {
4421
5056
  //#region src/domains/stream/session.ts
4422
5057
  function createStreamSession(connection, _route, sessionId) {
4423
5058
  let closed = false;
4424
- const unsubscribeDisconnect = connection.onDisconnect(() => {
5059
+ let unsubscribeDisconnect = () => void 0;
5060
+ unsubscribeDisconnect = connection.onDisconnect(() => {
4425
5061
  closed = true;
5062
+ unsubscribeDisconnect();
4426
5063
  });
4427
5064
  const ensureOpen = () => {
4428
5065
  if (closed) throw new StreamError("Stream session already closed", "SESSION_CLOSED");
@@ -4496,7 +5133,7 @@ function isAbortSignal(value) {
4496
5133
  * 3. `commit()` or `rollback()` finalizes the session
4497
5134
  */
4498
5135
  function createStreamClient(connection) {
4499
- const { requestFrame } = createDomainClient(connection);
5136
+ const { requestFrame, requestReconnectFrame, runWithRetry } = createDomainClient(connection);
4500
5137
  const subscriptionsByPattern = /* @__PURE__ */ new Map();
4501
5138
  const patternsBySubId = /* @__PURE__ */ new Map();
4502
5139
  let initialized = false;
@@ -4510,7 +5147,7 @@ function createStreamClient(connection) {
4510
5147
  subscriptionsByPattern.clear();
4511
5148
  patternsBySubId.clear();
4512
5149
  for (const subscription of snapshot) {
4513
- const subId = await subscribeWire(subscription.pattern);
5150
+ const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
4514
5151
  subscriptionsByPattern.set(subscription.pattern, {
4515
5152
  subId,
4516
5153
  handlers: new Map(subscription.handlers)
@@ -4528,43 +5165,89 @@ function createStreamClient(connection) {
4528
5165
  };
4529
5166
  const readPage = async (route, startOffset, limit = 100, options) => {
4530
5167
  assertStreamPattern(route);
4531
- const response = await requestFrame(604, StreamCodec.encodeRead(route, startOffset, limit, options), options?.signal);
4532
- const decoded = StreamCodec.decodeReadResponse(response);
4533
- checkStatus(decoded.status, "READ");
4534
- return {
4535
- items: decoded.items,
4536
- cursor: decoded.cursor ?? {
4537
- lastResourceOffset: startOffset,
4538
- lastAreaOffset: void 0,
4539
- lastRealmOffset: void 0,
4540
- hasMore: false
4541
- }
4542
- };
5168
+ return runWithRetry({
5169
+ domain: "stream",
5170
+ operation: "read",
5171
+ retryClass: "replayable_read",
5172
+ signal: options?.signal
5173
+ }, async () => {
5174
+ const response = await requestFrame(604, StreamCodec.encodeRead(route, startOffset, limit, options), options?.signal);
5175
+ const decoded = StreamCodec.decodeReadResponse(response);
5176
+ checkStatus(decoded.status, "READ");
5177
+ return {
5178
+ items: decoded.items,
5179
+ cursor: decoded.cursor ?? {
5180
+ lastResourceOffset: startOffset,
5181
+ lastAreaOffset: void 0,
5182
+ lastRealmOffset: void 0,
5183
+ hasMore: false
5184
+ }
5185
+ };
5186
+ });
4543
5187
  };
4544
5188
  const read = async (route, startOffset, limit = 100, options) => {
4545
5189
  const page = await readPage(route, startOffset, limit, options);
4546
5190
  return StreamCodec.flattenStreamReadItems(page.items);
4547
5191
  };
5192
+ const readWhenCommitted = async function* (route, options) {
5193
+ assertStreamPattern(route);
5194
+ const wakeGate = createWakeGate();
5195
+ const subscription = await subscribe(route, () => {
5196
+ wakeGate.wake();
5197
+ });
5198
+ try {
5199
+ let offset = options.offset;
5200
+ while (true) {
5201
+ const observed = wakeGate.version;
5202
+ const page = await readPage(route, offset, options.batchSize ?? 100, {
5203
+ maxBytes: options.maxBytes,
5204
+ filter: options.filter,
5205
+ signal: options.signal
5206
+ });
5207
+ if (page.items.length > 0) {
5208
+ offset = page.cursor.lastResourceOffset + 1n;
5209
+ const records = StreamCodec.flattenStreamReadItems(page.items);
5210
+ if (records.length > 0) yield records;
5211
+ }
5212
+ if (page.cursor.hasMore) continue;
5213
+ await wakeGate.waitAfter(observed, { signal: options.signal });
5214
+ }
5215
+ } finally {
5216
+ await subscription.unsubscribe().catch(() => void 0);
5217
+ }
5218
+ };
4548
5219
  const consume = async (route, startOffset, limit = 100, options) => {
4549
5220
  return createAsyncIterableIterator(createSliceIterator(await read(route, startOffset, limit, options)));
4550
5221
  };
4551
5222
  const peek = async (route) => {
4552
5223
  assertStreamRoute(route);
4553
- const response = await requestFrame(605, StreamCodec.encodeLast(route));
4554
- const decoded = StreamCodec.decodeLastResponse(response);
4555
- checkStatus(decoded.status, "LAST");
4556
- return decoded.record ?? null;
5224
+ return runWithRetry({
5225
+ domain: "stream",
5226
+ operation: "last",
5227
+ retryClass: "replayable_read"
5228
+ }, async () => {
5229
+ const response = await requestFrame(605, StreamCodec.encodeLast(route));
5230
+ const decoded = StreamCodec.decodeLastResponse(response);
5231
+ checkStatus(decoded.status, "LAST");
5232
+ return decoded.record ?? null;
5233
+ });
4557
5234
  };
4558
5235
  const metadata = async (route) => {
4559
5236
  assertStreamRoute(route);
4560
- const response = await requestFrame(606, StreamCodec.encodeMetadata(route));
4561
- const decoded = StreamCodec.decodeMetadataResponse(response);
4562
- checkStatus(decoded.status, "GET_METADATA");
4563
- return decoded.metadata ?? {
4564
- firstOffset: 0n,
4565
- lastOffset: 0n,
4566
- recordCount: 0n
4567
- };
5237
+ return runWithRetry({
5238
+ domain: "stream",
5239
+ operation: "metadata",
5240
+ retryClass: "replayable_read"
5241
+ }, async () => {
5242
+ const response = await requestFrame(606, StreamCodec.encodeMetadata(route));
5243
+ const decoded = StreamCodec.decodeMetadataResponse(response);
5244
+ checkStatus(decoded.status, "GET_METADATA");
5245
+ return decoded.metadata ?? {
5246
+ firstOffset: 0n,
5247
+ lastOffset: 0n,
5248
+ recordCount: 0n
5249
+ };
5250
+ });
4568
5251
  };
4569
5252
  const subscribe = async (pattern, handler) => {
4570
5253
  assertStreamPattern(pattern);
@@ -4573,8 +5256,8 @@ function createStreamClient(connection) {
4573
5256
  if (existing) return addLocalSubscription(pattern, existing.subId, handler);
4574
5257
  return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
4575
5258
  };
4576
- const subscribeWire = async (pattern) => {
4577
- const response = await requestFrame(607, StreamCodec.encodeSubscribe(pattern));
5259
+ const subscribeWire = async (pattern, request = requestFrame) => {
5260
+ const response = await request(607, StreamCodec.encodeSubscribe(pattern));
4578
5261
  const decoded = StreamCodec.decodeSubscribeResponse(response);
4579
5262
  checkStatus(decoded.status, "SUBSCRIBE");
4580
5263
  if (decoded.subId === void 0) throw new StreamError("SUBSCRIBE response missing subId", "MISSING_SESSION_ID");
@@ -4652,6 +5335,7 @@ function createStreamClient(connection) {
4652
5335
  begin,
4653
5336
  readPage,
4654
5337
  read,
5338
+ readWhenCommitted,
4655
5339
  consume,
4656
5340
  peek,
4657
5341
  metadata,
@@ -4829,9 +5513,10 @@ var ScheduleError$1 = class extends Error {
4829
5513
  * Schedule domain client.
4830
5514
  */
4831
5515
  function createScheduleClient(connection) {
4832
- const { requestFrame } = createDomainClient(connection);
5516
+ const { requestFrame, requestReconnectFrame } = createDomainClient(connection);
4833
5517
  const subscriptionsByPattern = /* @__PURE__ */ new Map();
4834
5518
  const patternsBySubId = /* @__PURE__ */ new Map();
5519
+ const pendingNotificationsBySubId = /* @__PURE__ */ new Map();
4835
5520
  let notifyHandlerInitialized = false;
4836
5521
  let nextHandlerId = 1;
4837
5522
  connection.onReconnect(async () => {
@@ -4842,8 +5527,9 @@ function createScheduleClient(connection) {
4842
5527
  }));
4843
5528
  subscriptionsByPattern.clear();
4844
5529
  patternsBySubId.clear();
5530
+ pendingNotificationsBySubId.clear();
4845
5531
  for (const subscription of subscriptions) {
4846
- const subId = await subscribeWire(subscription.pattern);
5532
+ const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
4847
5533
  subscriptionsByPattern.set(subscription.pattern, {
4848
5534
  subId,
4849
5535
  handlers: new Map(subscription.handlers)
@@ -4866,6 +5552,29 @@ function createScheduleClient(connection) {
4866
5552
  const decoded = ScheduleCodec.decodeListResponse(assertSuccess(response, "LIST"));
4867
5553
  return [decoded.entries, decoded.totalCount];
4868
5554
  };
5555
+ const waitForNotifications = async function* (route, options = {}) {
5556
+ assertConcreteScheduleRoute(route);
5557
+ const wakeGate = createWakeGate();
5558
+ const pendingNotifications = [];
5559
+ const subscription = await subscribe(route, (notification) => {
5560
+ pendingNotifications.push(notification);
5561
+ wakeGate.wake();
5562
+ });
5563
+ try {
5564
+ while (true) {
5565
+ const notification = pendingNotifications.shift();
5566
+ if (notification) {
5567
+ yield notification;
5568
+ continue;
5569
+ }
5570
+ const observed = wakeGate.version;
5571
+ if (pendingNotifications.length > 0) continue;
5572
+ await wakeGate.waitAfter(observed, { signal: options.signal });
5573
+ }
5574
+ } finally {
5575
+ await subscription.unsubscribe().catch(() => void 0);
5576
+ }
5577
+ };
4869
5578
  const subscribe = async (pattern, handler) => {
4870
5579
  assertConcreteScheduleRoute(pattern);
4871
5580
  initNotifyHandler();
@@ -4873,8 +5582,8 @@ function createScheduleClient(connection) {
4873
5582
  if (existing) return addLocalSubscription(pattern, existing.subId, handler);
4874
5583
  return addLocalSubscription(pattern, await subscribeWire(pattern), handler);
4875
5584
  };
4876
- const subscribeWire = async (pattern) => {
4877
- const response = await requestFrame(703, ScheduleCodec.encodeSubscribe(pattern));
5585
+ const subscribeWire = async (pattern, request = requestFrame) => {
5586
+ const response = await request(703, ScheduleCodec.encodeSubscribe(pattern));
4878
5587
  return ScheduleCodec.decodeSubscribeResponse(assertSuccess(response, "SUBSCRIBE")).subId;
4879
5588
  };
4880
5589
  const addLocalSubscription = (pattern, subId, handler) => {
@@ -4889,6 +5598,7 @@ function createScheduleClient(connection) {
4889
5598
  patternsBySubId.set(subId, pattern);
4890
5599
  }
4891
5600
  subscription.handlers.set(handlerId, handler);
5601
+ flushPendingNotifications(subId);
4892
5602
  return createScheduleSubscription(subId, pattern, async () => {
4893
5603
  await unsubscribe(pattern, handlerId);
4894
5604
  });
@@ -4900,6 +5610,7 @@ function createScheduleClient(connection) {
4900
5610
  if (subscription.handlers.size > 0) return;
4901
5611
  subscriptionsByPattern.delete(pattern);
4902
5612
  patternsBySubId.delete(subscription.subId);
5613
+ pendingNotificationsBySubId.delete(subscription.subId);
4903
5614
  const response = await requestFrame(704, ScheduleCodec.encodeUnsubscribe(pattern));
4904
5615
  ScheduleCodec.decodeUnsubscribeResponse(assertSuccess(response, "UNSUBSCRIBE"));
4905
5616
  };
@@ -4910,16 +5621,40 @@ function createScheduleClient(connection) {
4910
5621
  try {
4911
5622
  const decoded = ScheduleCodec.decodeNotification(payload);
4912
5623
  const pattern = patternsBySubId.get(decoded.subId);
4913
- if (!pattern) return;
5624
+ if (!pattern) {
5625
+ queuePendingNotification(decoded.subId, { payload: decoded.payload });
5626
+ return;
5627
+ }
4914
5628
  const subscription = subscriptionsByPattern.get(pattern);
4915
- if (!subscription) return;
4916
- const notification = { payload: decoded.payload };
4917
- for (const handler of subscription.handlers.values()) connection.dispatchAsyncHandler(async () => {
4918
- await handler(notification);
4919
- });
5629
+ if (!subscription) {
5630
+ queuePendingNotification(decoded.subId, { payload: decoded.payload });
5631
+ return;
5632
+ }
5633
+ dispatchNotification(subscription, { payload: decoded.payload });
4920
5634
  } catch {}
4921
5635
  });
4922
5636
  };
5637
+ const queuePendingNotification = (subId, notification) => {
5638
+ const existing = pendingNotificationsBySubId.get(subId);
5639
+ if (existing) {
5640
+ existing.push(notification);
5641
+ return;
5642
+ }
5643
+ pendingNotificationsBySubId.set(subId, [notification]);
5644
+ };
5645
+ const flushPendingNotifications = (subId) => {
5646
+ const pending = pendingNotificationsBySubId.get(subId);
5647
+ if (!pending || pending.length === 0) return;
5648
+ pendingNotificationsBySubId.delete(subId);
5649
+ const subscription = Array.from(subscriptionsByPattern.values()).find((entry) => entry.subId === subId);
5650
+ if (!subscription) return;
5651
+ for (const notification of pending) dispatchNotification(subscription, notification);
5652
+ };
5653
+ const dispatchNotification = (subscription, notification) => {
5654
+ for (const handler of subscription.handlers.values()) connection.dispatchAsyncHandler(async () => {
5655
+ await handler(notification);
5656
+ });
5657
+ };
4923
5658
  const assertSuccess = (payload, operation) => {
4924
5659
  const result = parseStandardResponse(payload);
4925
5660
  if (result.success) return result.data;
@@ -4936,7 +5671,8 @@ function createScheduleClient(connection) {
4936
5671
  create,
4937
5672
  cancel,
4938
5673
  list,
4939
- subscribe
5674
+ subscribe,
5675
+ waitForNotifications
4940
5676
  };
4941
5677
  }
4942
5678
  const ScheduleClient = function(connection) {
@@ -4947,6 +5683,39 @@ function assertConcreteScheduleRoute(route) {
4947
5683
  }
4948
5684
  //#endregion
4949
5685
  //#region src/client/client.ts
5686
+ const abortError = () => {
5687
+ const error = /* @__PURE__ */ new Error("The operation was aborted");
5688
+ error.name = "AbortError";
5689
+ return error;
5690
+ };
5691
+ const throwIfAborted = (signal) => {
5692
+ if (signal?.aborted) throw abortError();
5693
+ };
5694
+ const waitForSharedPromise = async (promise, signal) => {
5695
+ if (!signal) return promise;
5696
+ if (signal.aborted) throw abortError();
5697
+ return await new Promise((resolve, reject) => {
5698
+ let settled = false;
5699
+ const cleanup = () => {
5700
+ signal.removeEventListener("abort", onAbort);
5701
+ };
5702
+ const settle = (callback) => {
5703
+ if (settled) return;
5704
+ settled = true;
5705
+ cleanup();
5706
+ callback();
5707
+ };
5708
+ const onAbort = () => {
5709
+ settle(() => reject(abortError()));
5710
+ };
5711
+ signal.addEventListener("abort", onAbort, { once: true });
5712
+ promise.then((value) => {
5713
+ settle(() => resolve(value));
5714
+ }, (error) => {
5715
+ settle(() => reject(error));
5716
+ });
5717
+ });
5718
+ };
4950
5719
  function createClient(config) {
4951
5720
  const observability = config.observability;
4952
5721
  const resolvedConfig = {
@@ -4958,12 +5727,25 @@ function createClient(config) {
4958
5727
  maxRequestQueueSize: 1024,
4959
5728
  observability: config.observability ?? {},
4960
5729
  reconnect: {
4961
- enabled: false,
5730
+ enabled: true,
4962
5731
  maxAttempts: Infinity,
4963
5732
  backoffMs: 250,
4964
5733
  maxBackoffMs: 5e3,
4965
5734
  ...config.reconnect
4966
5735
  },
5736
+ retry: {
5737
+ enabled: true,
5738
+ maxAttempts: 3,
5739
+ backoffMs: 100,
5740
+ maxBackoffMs: 1e3,
5741
+ ...config.retry
5742
+ },
5743
+ heartbeat: {
5744
+ enabled: true,
5745
+ intervalMs: 1e4,
5746
+ timeoutMs: 3e4,
5747
+ ...config.heartbeat
5748
+ },
4967
5749
  asyncHandlers: {
4968
5750
  maxConcurrency: Infinity,
4969
5751
  timeoutMs: 3e4,
@@ -4980,34 +5762,72 @@ function createClient(config) {
4980
5762
  let noticeClient = null;
4981
5763
  let streamClient = null;
4982
5764
  let scheduleClient = null;
5765
+ let clientClosed = false;
5766
+ let pendingConnectPromise = null;
4983
5767
  const resolveTokenProvider = () => {
4984
5768
  if (resolvedConfig.tokenProvider) return resolvedConfig.tokenProvider;
4985
5769
  return () => "";
4986
5770
  };
4987
5771
  const ensureConnection = () => {
5772
+ if (clientClosed) throw new ConnectionError("Client is closed", { state: "CLOSED" });
4988
5773
  if (!connection) throw new ConnectionError("Not connected to Fitz server. Call connect() first.", { state: getState() });
4989
5774
  return connection;
4990
5775
  };
4991
- const connect = async (options = {}) => {
4992
- if (connection?.isConnected()) return;
5776
+ const createOwnedConnection = () => {
5777
+ if (connection) return connection;
4993
5778
  connection = createConnection(() => createTransport(resolvedConfig.url, resolvedConfig.transport, {
4994
5779
  timeout: resolvedConfig.timeout,
4995
- maxFrameSize: resolvedConfig.maxFrameSize
5780
+ maxFrameSize: resolvedConfig.maxFrameSize,
5781
+ receiveTimeout: resolvedConfig.heartbeat?.enabled === false
4996
5782
  }), resolveTokenProvider(), {
4997
5783
  timeout: resolvedConfig.timeout,
4998
5784
  authSettleDelayMs: resolvedConfig.authSettleDelayMs,
4999
5785
  maxInFlightRequests: resolvedConfig.maxInFlightRequests,
5000
5786
  maxRequestQueueSize: resolvedConfig.maxRequestQueueSize,
5001
5787
  reconnect: resolvedConfig.reconnect,
5788
+ retry: resolvedConfig.retry,
5789
+ heartbeat: resolvedConfig.heartbeat,
5002
5790
  observability,
5003
5791
  asyncHandlers: resolvedConfig.asyncHandlers
5004
5792
  });
5005
- await connection.connect(options);
5793
+ return connection;
5794
+ };
5795
+ const connect = async (options = {}) => {
5796
+ if (clientClosed) throw new ConnectionError("Client is closed", { state: "CLOSED" });
5797
+ throwIfAborted(options.signal);
5798
+ const activeConnection = createOwnedConnection();
5799
+ if (activeConnection.isConnected()) return;
5800
+ if (pendingConnectPromise) {
5801
+ await waitForSharedPromise(pendingConnectPromise, options.signal);
5802
+ return;
5803
+ }
5804
+ const state = activeConnection.getState();
5805
+ 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(() => {
5806
+ if (pendingConnectPromise === trackedConnectPromise) pendingConnectPromise = null;
5807
+ });
5808
+ pendingConnectPromise = trackedConnectPromise;
5809
+ await waitForSharedPromise(trackedConnectPromise, options.signal);
5006
5810
  };
5007
5811
  const close = async () => {
5812
+ if (clientClosed && !connection) {
5813
+ kvClient = null;
5814
+ queueClient = null;
5815
+ rpcClient = null;
5816
+ leaseClient = null;
5817
+ noticeClient = null;
5818
+ streamClient = null;
5819
+ scheduleClient = null;
5820
+ return;
5821
+ }
5822
+ clientClosed = true;
5823
+ pendingConnectPromise = null;
5008
5824
  if (connection) {
5009
- await connection.close();
5010
- connection = null;
5825
+ const activeConnection = connection;
5826
+ try {
5827
+ await activeConnection.close();
5828
+ } finally {
5829
+ if (connection === activeConnection) connection = null;
5830
+ }
5011
5831
  }
5012
5832
  kvClient = null;
5013
5833
  queueClient = null;
@@ -5018,7 +5838,7 @@ function createClient(config) {
5018
5838
  scheduleClient = null;
5019
5839
  };
5020
5840
  const isConnected = () => {
5021
- return connection?.isConnected() ?? false;
5841
+ return !clientClosed && (connection?.isConnected() ?? false);
5022
5842
  };
5023
5843
  const kv = () => {
5024
5844
  const activeConnection = ensureConnection();
@@ -5059,7 +5879,10 @@ function createClient(config) {
5059
5879
  return ensureConnection().getUrl();
5060
5880
  };
5061
5881
  const getState = () => {
5062
- return connection?.getState() ?? "DISCONNECTED";
5882
+ if (clientClosed) return "CLOSED";
5883
+ if (!connection) return "DISCONNECTED";
5884
+ const state = connection.getState();
5885
+ return state === "CLOSED" ? "DISCONNECTED" : state;
5063
5886
  };
5064
5887
  return {
5065
5888
  config: resolvedConfig,
@@ -5219,6 +6042,6 @@ function isAbortError(error) {
5219
6042
  return error instanceof Error && error.name === "AbortError";
5220
6043
  }
5221
6044
  //#endregion
5222
- export { AuthenticationError, Client, CodecError, ConnectionError, ConnectionState, ErrCodeKvBackendError, ErrCodeKvIsolationConflict, ErrCodeLeaseHeld, ErrCodeQueueFull, ErrCodeRpcBackpressure, ErrCodeRpcCorrelationNotFound, ErrCodeRpcRouteNotRegistered, ErrCodeRpcTimeout, ErrCodeRpcUnauthorized, ErrCodeRpcWorkerNotFound, ErrKvConflictingWrite, ErrKvKeyNotFound, ErrKvLeaseExpired, ErrKvOperationNotAllowed, ErrKvTransactionAborted, ErrLeaseHeld, ErrLeaseInvalidToken, ErrLeaseNotFound, ErrNoticeGeneral, ErrQueueFull, ErrQueueInvalidDelay, ErrQueueInvalidToken, ErrQueueMessageNotFound, ErrQueueNotFound, ErrRpcHandlerError, ErrRpcHandlerNotFound, ErrRpcInvalidRequest, ErrRpcTimeout, ErrScheduleInvalidCron, ErrScheduleInvalidDelay, ErrScheduleInvalidTimestamp, ErrScheduleNotFound, ErrScheduleTaskNotFound, ErrStreamExpectedOffsetMismatch, ErrStreamFull, ErrStreamInvalidOffset, ErrStreamNotFound, ErrStreamOffsetOutOfRange, ErrStreamSessionClosed, ErrStreamSessionNotFound, FitzError, KvClient, KvError, LeaseClient, LeaseError, NoticeClient, NoticeError, ProtocolError, QueueClient, QueueError, RequestQueueFullError, RpcClient, RpcError, ScheduleClient, ScheduleError, StreamClient, StreamError, TimeoutError, TransportError, createClient, createTaskGroup, isRetryable };
6045
+ export { AuthenticationError, Client, CodecError, ConnectionError, ConnectionState, ErrCodeKvBackendError, ErrCodeKvIsolationConflict, ErrCodeLeaseHeld, ErrCodeQueueFull, ErrCodeRpcBackpressure, ErrCodeRpcCorrelationNotFound, ErrCodeRpcRouteNotRegistered, ErrCodeRpcTimeout, ErrCodeRpcUnauthorized, ErrCodeRpcWorkerNotFound, ErrKvConflictingWrite, ErrKvKeyNotFound, ErrKvLeaseExpired, ErrKvOperationNotAllowed, ErrKvTransactionAborted, ErrLeaseHeld, ErrLeaseInvalidToken, ErrLeaseNotFound, ErrNoticeGeneral, ErrQueueFull, ErrQueueInvalidDelay, ErrQueueInvalidToken, ErrQueueMessageNotFound, ErrQueueNotFound, ErrRpcHandlerError, ErrRpcHandlerNotFound, ErrRpcInvalidRequest, ErrRpcTimeout, ErrScheduleInvalidCron, ErrScheduleInvalidDelay, ErrScheduleInvalidTimestamp, ErrScheduleNotFound, ErrScheduleTaskNotFound, ErrStreamExpectedOffsetMismatch, ErrStreamFull, ErrStreamInvalidOffset, ErrStreamNotFound, ErrStreamOffsetOutOfRange, ErrStreamSessionClosed, ErrStreamSessionNotFound, FitzError, KvClient, KvError, LeaseClient, LeaseError, NoticeClient, NoticeError, ProtocolError, QueueClient, QueueError, RequestQueueFullError, RpcClient, RpcError, ScheduleClient, ScheduleError, StreamClient, StreamError, TimeoutError, TransportError, createClient, createTaskGroup, createWakeGate, isRetryable };
5223
6046
 
5224
6047
  //# sourceMappingURL=index.mjs.map