@cntryl/fitz 0.0.2 → 0.0.3

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