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