@cntryl/fitz 0.0.1 → 0.0.3

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