@cntryl/fitz 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -981,7 +981,7 @@ function createMultiplexer(observability = {}) {
981
981
  const span = tracer?.startSpan("fitz.request", attributes);
982
982
  let spanEnded = false;
983
983
  if (signal?.aborted) {
984
- const error = abortError$2();
984
+ const error = abortError$3();
985
985
  span?.recordException(error);
986
986
  span?.end();
987
987
  meter?.counter("fitz.request.failed", 1, {
@@ -1055,7 +1055,7 @@ function createMultiplexer(observability = {}) {
1055
1055
  heapifyUp(requestEntry.timeoutIndex);
1056
1056
  if (signal) {
1057
1057
  onAbort = () => {
1058
- failRequest(abortError$2());
1058
+ failRequest(abortError$3());
1059
1059
  };
1060
1060
  signal.addEventListener("abort", onAbort, { once: true });
1061
1061
  }
@@ -1075,7 +1075,7 @@ function createMultiplexer(observability = {}) {
1075
1075
  return deferred.promise;
1076
1076
  }, (err) => {
1077
1077
  if (!finalize()) {
1078
- if (signal?.aborted) throw abortError$2();
1078
+ if (signal?.aborted) throw abortError$3();
1079
1079
  throw err;
1080
1080
  }
1081
1081
  unregisterRequest(messageType, requestEntry);
@@ -1088,7 +1088,7 @@ function createMultiplexer(observability = {}) {
1088
1088
  span?.end();
1089
1089
  spanEnded = true;
1090
1090
  }
1091
- if (signal?.aborted) throw abortError$2();
1091
+ if (signal?.aborted) throw abortError$3();
1092
1092
  throw err;
1093
1093
  });
1094
1094
  };
@@ -1200,7 +1200,7 @@ function createMultiplexer(observability = {}) {
1200
1200
  const Multiplexer = function(observability = {}) {
1201
1201
  return createMultiplexer(observability);
1202
1202
  };
1203
- function abortError$2() {
1203
+ function abortError$3() {
1204
1204
  const error = /* @__PURE__ */ new Error("The operation was aborted");
1205
1205
  error.name = "AbortError";
1206
1206
  return error;
@@ -1284,7 +1284,7 @@ function shouldRetryOperation(retryClass, error) {
1284
1284
  //#region src/client/connection.ts
1285
1285
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1286
1286
  const sleepWithAbort = async (ms, signal) => {
1287
- if (signal?.aborted) throw abortError$1();
1287
+ if (signal?.aborted) throw abortError$2();
1288
1288
  if (ms <= 0) return;
1289
1289
  await new Promise((resolve, reject) => {
1290
1290
  const timer = setTimeout(() => {
@@ -1293,7 +1293,7 @@ const sleepWithAbort = async (ms, signal) => {
1293
1293
  }, ms);
1294
1294
  const onAbort = () => {
1295
1295
  cleanup();
1296
- reject(abortError$1());
1296
+ reject(abortError$2());
1297
1297
  };
1298
1298
  const cleanup = () => {
1299
1299
  clearTimeout(timer);
@@ -1369,7 +1369,7 @@ function createRequestGate(maxConcurrency, maxQueueSize) {
1369
1369
  let closed = false;
1370
1370
  const queue = [];
1371
1371
  const acquire = async (signal) => {
1372
- if (signal?.aborted) throw abortError$1();
1372
+ if (signal?.aborted) throw abortError$2();
1373
1373
  if (closed) throw connectionClosedError();
1374
1374
  return await new Promise((resolve, reject) => {
1375
1375
  const grant = () => {
@@ -1392,7 +1392,7 @@ function createRequestGate(maxConcurrency, maxQueueSize) {
1392
1392
  waiter.onAbort = () => {
1393
1393
  removeWaiter(waiter);
1394
1394
  cleanup();
1395
- reject(abortError$1());
1395
+ reject(abortError$2());
1396
1396
  };
1397
1397
  if (signal) signal.addEventListener("abort", waiter.onAbort, { once: true });
1398
1398
  if (activeCount < maxConcurrency) {
@@ -1441,7 +1441,7 @@ function createRequestGate(maxConcurrency, maxQueueSize) {
1441
1441
  close
1442
1442
  };
1443
1443
  }
1444
- function abortError$1() {
1444
+ function abortError$2() {
1445
1445
  const error = /* @__PURE__ */ new Error("The operation was aborted");
1446
1446
  error.name = "AbortError";
1447
1447
  return error;
@@ -1450,14 +1450,14 @@ function connectionClosedError() {
1450
1450
  return new ConnectionError("Connection closed", { state: "CLOSED" });
1451
1451
  }
1452
1452
  function throwIfAborted$1(signal) {
1453
- if (signal?.aborted) throw abortError$1();
1453
+ if (signal?.aborted) throw abortError$2();
1454
1454
  }
1455
1455
  function isAbortError$1(error) {
1456
1456
  return error instanceof Error && error.name === "AbortError";
1457
1457
  }
1458
1458
  const waitForSharedPromise$1 = async (promise, signal) => {
1459
1459
  if (!signal) return promise;
1460
- if (signal.aborted) throw abortError$1();
1460
+ if (signal.aborted) throw abortError$2();
1461
1461
  return await new Promise((resolve, reject) => {
1462
1462
  let settled = false;
1463
1463
  const cleanup = () => {
@@ -1470,7 +1470,7 @@ const waitForSharedPromise$1 = async (promise, signal) => {
1470
1470
  callback();
1471
1471
  };
1472
1472
  const onAbort = () => {
1473
- settle(() => reject(abortError$1()));
1473
+ settle(() => reject(abortError$2()));
1474
1474
  };
1475
1475
  signal.addEventListener("abort", onAbort, { once: true });
1476
1476
  promise.then((value) => {
@@ -1491,6 +1491,9 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1491
1491
  const retryMaxAttempts = options.retry?.maxAttempts ?? 3;
1492
1492
  const retryBackoffMs = options.retry?.backoffMs ?? 100;
1493
1493
  const retryMaxBackoffMs = options.retry?.maxBackoffMs ?? 1e3;
1494
+ const heartbeatEnabled = options.heartbeat?.enabled ?? true;
1495
+ const heartbeatIntervalMs = options.heartbeat?.intervalMs ?? 1e4;
1496
+ const heartbeatTimeoutMs = options.heartbeat?.timeoutMs ?? 3e4;
1494
1497
  const maxInFlightRequests = options.maxInFlightRequests ?? 256;
1495
1498
  const maxRequestQueueSize = options.maxRequestQueueSize ?? 1024;
1496
1499
  const observability = options.observability;
@@ -1507,6 +1510,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1507
1510
  let permanentlyClosed = false;
1508
1511
  let connectPromise = null;
1509
1512
  let reconnectPromise = null;
1513
+ let connectionLossPromise = null;
1510
1514
  let reconnectRestoreActive = false;
1511
1515
  let authOutcome = null;
1512
1516
  let authRejected = false;
@@ -1516,6 +1520,10 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1516
1520
  const closeAbortController = new AbortController();
1517
1521
  const connectionScope = createScope("connection");
1518
1522
  const readyListeners = /* @__PURE__ */ new Set();
1523
+ let heartbeatTimer = null;
1524
+ let heartbeatTransport = null;
1525
+ let heartbeatPending = false;
1526
+ let lastActivityAt = Date.now();
1519
1527
  const log = (level, event, fields) => {
1520
1528
  observability?.logger?.log(level, event, fields);
1521
1529
  };
@@ -1585,6 +1593,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1585
1593
  permanentlyClosed = true;
1586
1594
  closeRequested = true;
1587
1595
  receiveLoopAbort = true;
1596
+ stopHeartbeat();
1588
1597
  closeAbortController.abort();
1589
1598
  asyncHandlerDispatcher.close();
1590
1599
  const scopeDisposePromise = connectionScope.dispose();
@@ -1766,7 +1775,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1766
1775
  cb();
1767
1776
  };
1768
1777
  const onAbort = () => {
1769
- settle(() => reject(abortError$1()));
1778
+ settle(() => reject(abortError$2()));
1770
1779
  };
1771
1780
  const onStateChange = () => {
1772
1781
  const failure = readyFailure();
@@ -1849,6 +1858,75 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1849
1858
  }
1850
1859
  }
1851
1860
  };
1861
+ const withWriteLock = async (operation) => {
1862
+ const prior = writeChain;
1863
+ let release;
1864
+ writeChain = new Promise((resolve) => {
1865
+ release = resolve;
1866
+ });
1867
+ await prior;
1868
+ try {
1869
+ return await operation();
1870
+ } finally {
1871
+ release();
1872
+ }
1873
+ };
1874
+ const markOutboundActivity = () => {
1875
+ lastActivityAt = Date.now();
1876
+ };
1877
+ const markRemoteActivity = () => {
1878
+ lastActivityAt = Date.now();
1879
+ };
1880
+ const stopHeartbeat = () => {
1881
+ if (heartbeatTimer) {
1882
+ clearTimeout(heartbeatTimer);
1883
+ heartbeatTimer = null;
1884
+ }
1885
+ heartbeatTransport = null;
1886
+ heartbeatPending = false;
1887
+ };
1888
+ const startHeartbeat = (activeTransport) => {
1889
+ if (!heartbeatEnabled) return;
1890
+ stopHeartbeat();
1891
+ activeTransport.enableKeepAlive?.(heartbeatIntervalMs);
1892
+ heartbeatTransport = activeTransport;
1893
+ markRemoteActivity();
1894
+ const scheduleNext = () => {
1895
+ if (closeRequested || receiveLoopAbort || heartbeatTransport !== activeTransport) return;
1896
+ heartbeatTimer = setTimeout(tick, heartbeatIntervalMs);
1897
+ };
1898
+ const tick = () => {
1899
+ heartbeatTimer = null;
1900
+ if (closeRequested || receiveLoopAbort || heartbeatTransport !== activeTransport) return;
1901
+ const now = Date.now();
1902
+ if (now - lastActivityAt < heartbeatIntervalMs) {
1903
+ scheduleNext();
1904
+ return;
1905
+ }
1906
+ const supportsHeartbeat = activeTransport.supportsHeartbeat?.() ?? true;
1907
+ if (!heartbeatPending && supportsHeartbeat && activeTransport.sendHeartbeat) {
1908
+ heartbeatPending = true;
1909
+ const heartbeatSentAt = now;
1910
+ const dispatchHeartbeat = (heartbeat) => activeTransport.sendHeartbeat(heartbeat);
1911
+ withWriteLock(async () => {
1912
+ await dispatchHeartbeat({ timeoutMs: heartbeatTimeoutMs });
1913
+ }).then(() => {
1914
+ if (heartbeatTransport !== activeTransport) return;
1915
+ heartbeatPending = false;
1916
+ markRemoteActivity();
1917
+ }).catch((error) => {
1918
+ if (heartbeatTransport !== activeTransport) return;
1919
+ heartbeatPending = false;
1920
+ if (lastActivityAt > heartbeatSentAt) return;
1921
+ const heartbeatError = new TransportError(`Heartbeat failed: ${describeError(error)}`);
1922
+ activeTransport.close().catch(() => void 0);
1923
+ handleConnectionLoss(heartbeatError);
1924
+ });
1925
+ }
1926
+ scheduleNext();
1927
+ };
1928
+ scheduleNext();
1929
+ };
1852
1930
  const openAndAuthenticate = async (isReconnect, signal) => {
1853
1931
  throwIfAborted$1(signal);
1854
1932
  receiveLoopAbort = false;
@@ -1856,10 +1934,13 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1856
1934
  requestGate = createRequestGate(maxInFlightRequests, maxRequestQueueSize);
1857
1935
  const activeTransport = transportFactory();
1858
1936
  transport = activeTransport;
1937
+ stopHeartbeat();
1859
1938
  setState(isReconnect ? "RECONNECTING" : "CONNECTING");
1860
1939
  emitLifecycleEvent(isReconnect ? "reconnect_start" : "connect_start");
1861
1940
  await activeTransport.connect();
1941
+ markRemoteActivity();
1862
1942
  if (closeRequested) {
1943
+ stopHeartbeat();
1863
1944
  await activeTransport.close().catch(() => void 0);
1864
1945
  if (transport === activeTransport) transport = null;
1865
1946
  throw connectionClosedError();
@@ -1892,6 +1973,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1892
1973
  hasEstablishedSession = true;
1893
1974
  reconnectExhausted = false;
1894
1975
  setState("AUTHENTICATED");
1976
+ startHeartbeat(activeTransport);
1895
1977
  if (!isReconnect) multiplexer.setConnected();
1896
1978
  emitLifecycleEvent(isReconnect ? "reconnect_succeeded" : "connect_succeeded");
1897
1979
  } catch (error) {
@@ -1899,6 +1981,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1899
1981
  reconnectRestoreActive = false;
1900
1982
  multiplexer.setDisconnected();
1901
1983
  emitDisconnect();
1984
+ stopHeartbeat();
1902
1985
  if (activeTransport) await activeTransport.close().catch(() => void 0);
1903
1986
  if (transport === activeTransport) transport = null;
1904
1987
  const rejectedAuth = error instanceof AuthenticationError;
@@ -1906,7 +1989,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1906
1989
  if (closeRequested) setState("CLOSED");
1907
1990
  else setState(rejectedAuth ? "CLOSED" : "DISCONNECTED");
1908
1991
  emitLifecycleEvent(isReconnect ? "reconnect_failed" : "connect_failed", error);
1909
- if (isAbortError$1(error)) throw abortError$1();
1992
+ if (isAbortError$1(error)) throw abortError$2();
1910
1993
  throw error;
1911
1994
  }
1912
1995
  };
@@ -1914,10 +1997,12 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1914
1997
  const token = await tokenProvider();
1915
1998
  const frame = FrameCodec.encodeFrame(1, utf8Encoder.encode(token));
1916
1999
  await ensureTransport().send(frame);
2000
+ markOutboundActivity();
1917
2001
  };
1918
2002
  const startReceiveLoop = async () => {
1919
2003
  while (!receiveLoopAbort && !closeRequested) try {
1920
2004
  const data = await ensureTransport().receive();
2005
+ markRemoteActivity();
1921
2006
  const frames = frameParser.parseFrames(data);
1922
2007
  for (const frame of frames) multiplexer.dispatch(frame.messageType, frame.payload);
1923
2008
  } catch (error) {
@@ -1927,6 +2012,17 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1927
2012
  }
1928
2013
  };
1929
2014
  const handleConnectionLoss = async (error) => {
2015
+ if (connectionLossPromise) {
2016
+ await connectionLossPromise;
2017
+ return;
2018
+ }
2019
+ connectionLossPromise = handleConnectionLossOnce(error).finally(() => {
2020
+ connectionLossPromise = null;
2021
+ });
2022
+ await connectionLossPromise;
2023
+ };
2024
+ const handleConnectionLossOnce = async (error) => {
2025
+ stopHeartbeat();
1930
2026
  multiplexer.setDisconnected();
1931
2027
  requestGate.close();
1932
2028
  emitDisconnect();
@@ -2013,17 +2109,10 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
2013
2109
  notifyReadyListeners();
2014
2110
  };
2015
2111
  const sendSerialized = async (transport, data) => {
2016
- const prior = writeChain;
2017
- let release;
2018
- writeChain = new Promise((resolve) => {
2019
- release = resolve;
2020
- });
2021
- await prior;
2022
- try {
2112
+ await withWriteLock(async () => {
2023
2113
  await transport.send(data);
2024
- } finally {
2025
- release();
2026
- }
2114
+ markOutboundActivity();
2115
+ });
2027
2116
  };
2028
2117
  const emitLifecycleEvent = (event, error, attempt) => {
2029
2118
  const payload = {
@@ -2099,6 +2188,7 @@ function createWebSocketTransport(url, options = {}) {
2099
2188
  let receiverResolve = null;
2100
2189
  const timeout = options.timeout ?? 3e4;
2101
2190
  const maxFrameSize = options.maxFrameSize ?? 65535;
2191
+ const receiveTimeoutEnabled = options.receiveTimeout ?? true;
2102
2192
  const enqueueMessage = (data) => {
2103
2193
  if (receiverResolve) {
2104
2194
  receiverResolve(data);
@@ -2172,6 +2262,56 @@ function createWebSocketTransport(url, options = {}) {
2172
2262
  }
2173
2263
  });
2174
2264
  };
2265
+ const sendHeartbeat = async (heartbeatOptions) => {
2266
+ if (!connected || !ws) throw new TransportError("WebSocket is not connected");
2267
+ const activeWs = ws;
2268
+ if (typeof activeWs.ping !== "function" || typeof activeWs.once !== "function" || typeof activeWs.removeListener !== "function") throw new TransportError("WebSocket heartbeat is not supported");
2269
+ const socket = activeWs;
2270
+ return new Promise((resolve, reject) => {
2271
+ let settled = false;
2272
+ let timeoutId = null;
2273
+ const cleanup = () => {
2274
+ if (timeoutId) clearTimeout(timeoutId);
2275
+ socket.removeListener("pong", onPong);
2276
+ socket.removeListener("close", onClose);
2277
+ socket.removeListener("error", onError);
2278
+ };
2279
+ const settle = (callback) => {
2280
+ if (settled) return;
2281
+ settled = true;
2282
+ cleanup();
2283
+ callback();
2284
+ };
2285
+ const onPong = () => {
2286
+ settle(resolve);
2287
+ };
2288
+ const onClose = () => {
2289
+ settle(() => reject(new TransportError("WebSocket closed during heartbeat")));
2290
+ };
2291
+ const onError = (...args) => {
2292
+ const event = args[0];
2293
+ const message = event instanceof Error ? event.message : event?.message || "unknown error";
2294
+ settle(() => reject(new TransportError(`WebSocket heartbeat failed: ${message}`)));
2295
+ };
2296
+ timeoutId = setTimeout(() => {
2297
+ settle(() => reject(new TimeoutError(`WebSocket heartbeat timeout after ${heartbeatOptions.timeoutMs}ms`)));
2298
+ }, heartbeatOptions.timeoutMs);
2299
+ socket.once("pong", onPong);
2300
+ socket.once("close", onClose);
2301
+ socket.once("error", onError);
2302
+ try {
2303
+ socket.ping(new Uint8Array(), void 0, (err) => {
2304
+ if (err) settle(() => reject(new TransportError(`WebSocket ping failed: ${err.message}`)));
2305
+ });
2306
+ } catch (err) {
2307
+ settle(() => reject(new TransportError(`WebSocket heartbeat error: ${err instanceof Error ? err.message : String(err)}`)));
2308
+ }
2309
+ });
2310
+ };
2311
+ const supportsHeartbeat = () => {
2312
+ return typeof ws?.ping === "function" && typeof ws?.once === "function" && typeof ws?.removeListener === "function";
2313
+ };
2314
+ const enableKeepAlive = () => void 0;
2175
2315
  const receive = async () => {
2176
2316
  if (receiveQueue.length > 0) {
2177
2317
  const message = receiveQueue.shift();
@@ -2180,12 +2320,12 @@ function createWebSocketTransport(url, options = {}) {
2180
2320
  }
2181
2321
  if (!connected) throw new TransportError("Connection closed");
2182
2322
  return new Promise((resolve, reject) => {
2183
- const timeoutId = setTimeout(() => {
2323
+ const timeoutId = receiveTimeoutEnabled ? setTimeout(() => {
2184
2324
  receiverResolve = null;
2185
2325
  reject(new TimeoutError(`WebSocket receive timeout after ${timeout}ms`));
2186
- }, timeout);
2326
+ }, timeout) : null;
2187
2327
  receiverResolve = (data) => {
2188
- clearTimeout(timeoutId);
2328
+ if (timeoutId) clearTimeout(timeoutId);
2189
2329
  receiverResolve = null;
2190
2330
  if (data === null) {
2191
2331
  reject(new TransportError("Connection closed"));
@@ -2220,6 +2360,9 @@ function createWebSocketTransport(url, options = {}) {
2220
2360
  connect,
2221
2361
  send,
2222
2362
  receive,
2363
+ sendHeartbeat,
2364
+ supportsHeartbeat,
2365
+ enableKeepAlive,
2223
2366
  close,
2224
2367
  getUrl,
2225
2368
  isConnected
@@ -2245,6 +2388,7 @@ function createTcpTransport(url, options = {}) {
2245
2388
  let receiverResolve = null;
2246
2389
  const timeout = options.timeout ?? 3e4;
2247
2390
  const maxFrameSize = options.maxFrameSize ?? 65535;
2391
+ const receiveTimeoutEnabled = options.receiveTimeout ?? true;
2248
2392
  let lengthBuffer = new Uint8Array(4);
2249
2393
  let lengthOffset = 0;
2250
2394
  let currentMessageLength = null;
@@ -2319,7 +2463,7 @@ function createTcpTransport(url, options = {}) {
2319
2463
  clearTimeout(connectTimeout);
2320
2464
  connected = true;
2321
2465
  activeSocket.setNoDelay(true);
2322
- activeSocket.setTimeout(timeout);
2466
+ activeSocket.setTimeout(receiveTimeoutEnabled ? timeout : 0);
2323
2467
  resolve();
2324
2468
  });
2325
2469
  activeSocket.on("data", (chunk) => {
@@ -2337,8 +2481,10 @@ function createTcpTransport(url, options = {}) {
2337
2481
  if (receiverResolve) receiverResolve(new Uint8Array(0));
2338
2482
  });
2339
2483
  activeSocket.on("timeout", () => {
2340
- activeSocket.destroy();
2341
- connected = false;
2484
+ if (receiveTimeoutEnabled) {
2485
+ activeSocket.destroy();
2486
+ connected = false;
2487
+ }
2342
2488
  });
2343
2489
  } catch (err) {
2344
2490
  reject(new TransportError(`Failed to create TCP socket: ${err instanceof Error ? err.message : String(err)}`));
@@ -2363,6 +2509,10 @@ function createTcpTransport(url, options = {}) {
2363
2509
  });
2364
2510
  });
2365
2511
  };
2512
+ const enableKeepAlive = (intervalMs) => {
2513
+ socket?.setKeepAlive(true, intervalMs);
2514
+ };
2515
+ const supportsHeartbeat = () => false;
2366
2516
  const receive = async () => {
2367
2517
  if (receiveQueue.length > 0) {
2368
2518
  const message = receiveQueue.shift();
@@ -2370,12 +2520,12 @@ function createTcpTransport(url, options = {}) {
2370
2520
  return message;
2371
2521
  }
2372
2522
  return new Promise((resolve, reject) => {
2373
- const timeoutId = setTimeout(() => {
2523
+ const timeoutId = receiveTimeoutEnabled ? setTimeout(() => {
2374
2524
  receiverResolve = null;
2375
2525
  reject(new TimeoutError(`TCP receive timeout after ${timeout}ms`));
2376
- }, timeout);
2526
+ }, timeout) : null;
2377
2527
  receiverResolve = (data) => {
2378
- clearTimeout(timeoutId);
2528
+ if (timeoutId) clearTimeout(timeoutId);
2379
2529
  receiverResolve = null;
2380
2530
  if (data.length === 0) reject(new TransportError("Connection closed"));
2381
2531
  else resolve(data);
@@ -2407,6 +2557,8 @@ function createTcpTransport(url, options = {}) {
2407
2557
  connect,
2408
2558
  send,
2409
2559
  receive,
2560
+ supportsHeartbeat,
2561
+ enableKeepAlive,
2410
2562
  close,
2411
2563
  getUrl,
2412
2564
  isConnected
@@ -2785,6 +2937,64 @@ const KvClient = function(connection) {
2785
2937
  return createKvClient(connection);
2786
2938
  };
2787
2939
  //#endregion
2940
+ //#region src/core/wake-gate.ts
2941
+ function abortError$1() {
2942
+ const error = /* @__PURE__ */ new Error("The operation was aborted");
2943
+ error.name = "AbortError";
2944
+ return error;
2945
+ }
2946
+ function createWakeGate() {
2947
+ let currentVersion = 0;
2948
+ const waiters = /* @__PURE__ */ new Set();
2949
+ const cleanup = (waiter) => {
2950
+ waiters.delete(waiter);
2951
+ if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener("abort", waiter.onAbort);
2952
+ };
2953
+ const wake = () => {
2954
+ currentVersion += 1;
2955
+ const version = currentVersion;
2956
+ for (const waiter of Array.from(waiters)) {
2957
+ cleanup(waiter);
2958
+ waiter.resolve(version);
2959
+ }
2960
+ return version;
2961
+ };
2962
+ const waitAfter = async (observedVersion, options = {}) => {
2963
+ if (currentVersion > observedVersion) return currentVersion;
2964
+ if (options.signal?.aborted) throw abortError$1();
2965
+ return await new Promise((resolve, reject) => {
2966
+ const waiter = {
2967
+ observedVersion,
2968
+ resolve,
2969
+ reject,
2970
+ signal: options.signal
2971
+ };
2972
+ waiter.onAbort = () => {
2973
+ cleanup(waiter);
2974
+ reject(abortError$1());
2975
+ };
2976
+ if (options.signal) options.signal.addEventListener("abort", waiter.onAbort, { once: true });
2977
+ if (currentVersion > observedVersion) {
2978
+ cleanup(waiter);
2979
+ resolve(currentVersion);
2980
+ return;
2981
+ }
2982
+ waiters.add(waiter);
2983
+ });
2984
+ };
2985
+ const wait = async (options) => {
2986
+ return await waitAfter(currentVersion, options);
2987
+ };
2988
+ return {
2989
+ get version() {
2990
+ return currentVersion;
2991
+ },
2992
+ wake,
2993
+ waitAfter,
2994
+ wait
2995
+ };
2996
+ }
2997
+ //#endregion
2788
2998
  //#region src/domains/queue/codec.ts
2789
2999
  /**
2790
3000
  * Queue domain codec for encoding and decoding protocol messages.
@@ -3123,43 +3333,55 @@ function createQueueClient(connection) {
3123
3333
  let items = await reserveOnce(route, leaseSeconds, batchSize);
3124
3334
  if (items.length > 0) return items;
3125
3335
  const deadline = Date.now() + waitSeconds * 1e3;
3126
- let pendingNotifications = 0;
3127
- let waiter;
3128
- const subscription = await subscribe(route, async () => {
3129
- pendingNotifications += 1;
3130
- if (!waiter) return;
3131
- const resolve = waiter;
3132
- waiter = void 0;
3133
- pendingNotifications = 0;
3134
- resolve();
3336
+ const wakeGate = createWakeGate();
3337
+ const subscription = await subscribe(route, () => {
3338
+ wakeGate.wake();
3135
3339
  });
3136
3340
  try {
3137
3341
  while (true) {
3342
+ const observed = wakeGate.version;
3138
3343
  items = await reserveOnce(route, leaseSeconds, batchSize);
3139
3344
  if (items.length > 0) return items;
3140
3345
  const remainingMs = deadline - Date.now();
3141
3346
  if (remainingMs <= 0) return items;
3142
- await new Promise((resolve) => {
3143
- if (pendingNotifications > 0) {
3144
- pendingNotifications = 0;
3145
- resolve();
3146
- return;
3147
- }
3148
- const release = () => {
3149
- clearTimeout(timeoutId);
3150
- if (waiter === release) waiter = void 0;
3151
- resolve();
3152
- };
3153
- const timeoutId = setTimeout(release, remainingMs);
3154
- waiter = release;
3347
+ const waitPromise = wakeGate.waitAfter(observed);
3348
+ let timeoutId = null;
3349
+ const timeoutPromise = new Promise((resolve) => {
3350
+ timeoutId = setTimeout(() => {
3351
+ resolve("timeout");
3352
+ }, remainingMs);
3155
3353
  });
3354
+ if (await Promise.race([waitPromise.then(() => {
3355
+ if (timeoutId) clearTimeout(timeoutId);
3356
+ return "wake";
3357
+ }), timeoutPromise]) === "timeout") return items;
3156
3358
  }
3157
3359
  } finally {
3158
- await subscription.unsubscribe();
3360
+ await subscription.unsubscribe().catch(() => void 0);
3159
3361
  }
3160
3362
  };
3161
- const reserveOnce = async (route, leaseSeconds, batchSize) => {
3162
- const response = await requestFrame(202, QueueCodec.encodeReserve(route, leaseSeconds, batchSize));
3363
+ const reserveWhenAvailable = async function* (route, options) {
3364
+ assertQueueReserveRoute(route);
3365
+ const wakeGate = createWakeGate();
3366
+ const subscription = await subscribe(route, () => {
3367
+ wakeGate.wake();
3368
+ });
3369
+ try {
3370
+ while (true) {
3371
+ const observed = wakeGate.version;
3372
+ const items = await reserveOnce(route, options.leaseSeconds, options.batchSize ?? 1, options.signal);
3373
+ if (items.length > 0) {
3374
+ yield items;
3375
+ continue;
3376
+ }
3377
+ await wakeGate.waitAfter(observed, { signal: options.signal });
3378
+ }
3379
+ } finally {
3380
+ await subscription.unsubscribe().catch(() => void 0);
3381
+ }
3382
+ };
3383
+ const reserveOnce = async (route, leaseSeconds, batchSize, signal) => {
3384
+ const response = await requestFrame(202, QueueCodec.encodeReserve(route, leaseSeconds, batchSize), signal);
3163
3385
  const decoded = QueueCodec.decodeReserveResponse(response);
3164
3386
  checkStatus(decoded, "RESERVE");
3165
3387
  return (decoded.items ?? []).map((item) => createQueueItem(item.id, item.token, item.body, route, connection));
@@ -3248,6 +3470,7 @@ function createQueueClient(connection) {
3248
3470
  return {
3249
3471
  enqueue,
3250
3472
  reserve,
3473
+ reserveWhenAvailable,
3251
3474
  subscribe
3252
3475
  };
3253
3476
  }
@@ -4966,6 +5189,33 @@ function createStreamClient(connection) {
4966
5189
  const page = await readPage(route, startOffset, limit, options);
4967
5190
  return StreamCodec.flattenStreamReadItems(page.items);
4968
5191
  };
5192
+ const readWhenCommitted = async function* (route, options) {
5193
+ assertStreamPattern(route);
5194
+ const wakeGate = createWakeGate();
5195
+ const subscription = await subscribe(route, () => {
5196
+ wakeGate.wake();
5197
+ });
5198
+ try {
5199
+ let offset = options.offset;
5200
+ while (true) {
5201
+ const observed = wakeGate.version;
5202
+ const page = await readPage(route, offset, options.batchSize ?? 100, {
5203
+ maxBytes: options.maxBytes,
5204
+ filter: options.filter,
5205
+ signal: options.signal
5206
+ });
5207
+ if (page.items.length > 0) {
5208
+ offset = page.cursor.lastResourceOffset + 1n;
5209
+ const records = StreamCodec.flattenStreamReadItems(page.items);
5210
+ if (records.length > 0) yield records;
5211
+ }
5212
+ if (page.cursor.hasMore) continue;
5213
+ await wakeGate.waitAfter(observed, { signal: options.signal });
5214
+ }
5215
+ } finally {
5216
+ await subscription.unsubscribe().catch(() => void 0);
5217
+ }
5218
+ };
4969
5219
  const consume = async (route, startOffset, limit = 100, options) => {
4970
5220
  return createAsyncIterableIterator(createSliceIterator(await read(route, startOffset, limit, options)));
4971
5221
  };
@@ -5085,6 +5335,7 @@ function createStreamClient(connection) {
5085
5335
  begin,
5086
5336
  readPage,
5087
5337
  read,
5338
+ readWhenCommitted,
5088
5339
  consume,
5089
5340
  peek,
5090
5341
  metadata,
@@ -5265,6 +5516,7 @@ function createScheduleClient(connection) {
5265
5516
  const { requestFrame, requestReconnectFrame } = createDomainClient(connection);
5266
5517
  const subscriptionsByPattern = /* @__PURE__ */ new Map();
5267
5518
  const patternsBySubId = /* @__PURE__ */ new Map();
5519
+ const pendingNotificationsBySubId = /* @__PURE__ */ new Map();
5268
5520
  let notifyHandlerInitialized = false;
5269
5521
  let nextHandlerId = 1;
5270
5522
  connection.onReconnect(async () => {
@@ -5275,6 +5527,7 @@ function createScheduleClient(connection) {
5275
5527
  }));
5276
5528
  subscriptionsByPattern.clear();
5277
5529
  patternsBySubId.clear();
5530
+ pendingNotificationsBySubId.clear();
5278
5531
  for (const subscription of subscriptions) {
5279
5532
  const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
5280
5533
  subscriptionsByPattern.set(subscription.pattern, {
@@ -5299,6 +5552,29 @@ function createScheduleClient(connection) {
5299
5552
  const decoded = ScheduleCodec.decodeListResponse(assertSuccess(response, "LIST"));
5300
5553
  return [decoded.entries, decoded.totalCount];
5301
5554
  };
5555
+ const waitForNotifications = async function* (route, options = {}) {
5556
+ assertConcreteScheduleRoute(route);
5557
+ const wakeGate = createWakeGate();
5558
+ const pendingNotifications = [];
5559
+ const subscription = await subscribe(route, (notification) => {
5560
+ pendingNotifications.push(notification);
5561
+ wakeGate.wake();
5562
+ });
5563
+ try {
5564
+ while (true) {
5565
+ const notification = pendingNotifications.shift();
5566
+ if (notification) {
5567
+ yield notification;
5568
+ continue;
5569
+ }
5570
+ const observed = wakeGate.version;
5571
+ if (pendingNotifications.length > 0) continue;
5572
+ await wakeGate.waitAfter(observed, { signal: options.signal });
5573
+ }
5574
+ } finally {
5575
+ await subscription.unsubscribe().catch(() => void 0);
5576
+ }
5577
+ };
5302
5578
  const subscribe = async (pattern, handler) => {
5303
5579
  assertConcreteScheduleRoute(pattern);
5304
5580
  initNotifyHandler();
@@ -5322,6 +5598,7 @@ function createScheduleClient(connection) {
5322
5598
  patternsBySubId.set(subId, pattern);
5323
5599
  }
5324
5600
  subscription.handlers.set(handlerId, handler);
5601
+ flushPendingNotifications(subId);
5325
5602
  return createScheduleSubscription(subId, pattern, async () => {
5326
5603
  await unsubscribe(pattern, handlerId);
5327
5604
  });
@@ -5333,6 +5610,7 @@ function createScheduleClient(connection) {
5333
5610
  if (subscription.handlers.size > 0) return;
5334
5611
  subscriptionsByPattern.delete(pattern);
5335
5612
  patternsBySubId.delete(subscription.subId);
5613
+ pendingNotificationsBySubId.delete(subscription.subId);
5336
5614
  const response = await requestFrame(704, ScheduleCodec.encodeUnsubscribe(pattern));
5337
5615
  ScheduleCodec.decodeUnsubscribeResponse(assertSuccess(response, "UNSUBSCRIBE"));
5338
5616
  };
@@ -5343,16 +5621,40 @@ function createScheduleClient(connection) {
5343
5621
  try {
5344
5622
  const decoded = ScheduleCodec.decodeNotification(payload);
5345
5623
  const pattern = patternsBySubId.get(decoded.subId);
5346
- if (!pattern) return;
5624
+ if (!pattern) {
5625
+ queuePendingNotification(decoded.subId, { payload: decoded.payload });
5626
+ return;
5627
+ }
5347
5628
  const subscription = subscriptionsByPattern.get(pattern);
5348
- if (!subscription) return;
5349
- const notification = { payload: decoded.payload };
5350
- for (const handler of subscription.handlers.values()) connection.dispatchAsyncHandler(async () => {
5351
- await handler(notification);
5352
- });
5629
+ if (!subscription) {
5630
+ queuePendingNotification(decoded.subId, { payload: decoded.payload });
5631
+ return;
5632
+ }
5633
+ dispatchNotification(subscription, { payload: decoded.payload });
5353
5634
  } catch {}
5354
5635
  });
5355
5636
  };
5637
+ const queuePendingNotification = (subId, notification) => {
5638
+ const existing = pendingNotificationsBySubId.get(subId);
5639
+ if (existing) {
5640
+ existing.push(notification);
5641
+ return;
5642
+ }
5643
+ pendingNotificationsBySubId.set(subId, [notification]);
5644
+ };
5645
+ const flushPendingNotifications = (subId) => {
5646
+ const pending = pendingNotificationsBySubId.get(subId);
5647
+ if (!pending || pending.length === 0) return;
5648
+ pendingNotificationsBySubId.delete(subId);
5649
+ const subscription = Array.from(subscriptionsByPattern.values()).find((entry) => entry.subId === subId);
5650
+ if (!subscription) return;
5651
+ for (const notification of pending) dispatchNotification(subscription, notification);
5652
+ };
5653
+ const dispatchNotification = (subscription, notification) => {
5654
+ for (const handler of subscription.handlers.values()) connection.dispatchAsyncHandler(async () => {
5655
+ await handler(notification);
5656
+ });
5657
+ };
5356
5658
  const assertSuccess = (payload, operation) => {
5357
5659
  const result = parseStandardResponse(payload);
5358
5660
  if (result.success) return result.data;
@@ -5369,7 +5671,8 @@ function createScheduleClient(connection) {
5369
5671
  create,
5370
5672
  cancel,
5371
5673
  list,
5372
- subscribe
5674
+ subscribe,
5675
+ waitForNotifications
5373
5676
  };
5374
5677
  }
5375
5678
  const ScheduleClient = function(connection) {
@@ -5437,6 +5740,12 @@ function createClient(config) {
5437
5740
  maxBackoffMs: 1e3,
5438
5741
  ...config.retry
5439
5742
  },
5743
+ heartbeat: {
5744
+ enabled: true,
5745
+ intervalMs: 1e4,
5746
+ timeoutMs: 3e4,
5747
+ ...config.heartbeat
5748
+ },
5440
5749
  asyncHandlers: {
5441
5750
  maxConcurrency: Infinity,
5442
5751
  timeoutMs: 3e4,
@@ -5468,7 +5777,8 @@ function createClient(config) {
5468
5777
  if (connection) return connection;
5469
5778
  connection = createConnection(() => createTransport(resolvedConfig.url, resolvedConfig.transport, {
5470
5779
  timeout: resolvedConfig.timeout,
5471
- maxFrameSize: resolvedConfig.maxFrameSize
5780
+ maxFrameSize: resolvedConfig.maxFrameSize,
5781
+ receiveTimeout: resolvedConfig.heartbeat?.enabled === false
5472
5782
  }), resolveTokenProvider(), {
5473
5783
  timeout: resolvedConfig.timeout,
5474
5784
  authSettleDelayMs: resolvedConfig.authSettleDelayMs,
@@ -5476,6 +5786,7 @@ function createClient(config) {
5476
5786
  maxRequestQueueSize: resolvedConfig.maxRequestQueueSize,
5477
5787
  reconnect: resolvedConfig.reconnect,
5478
5788
  retry: resolvedConfig.retry,
5789
+ heartbeat: resolvedConfig.heartbeat,
5479
5790
  observability,
5480
5791
  asyncHandlers: resolvedConfig.asyncHandlers
5481
5792
  });
@@ -5731,6 +6042,6 @@ function isAbortError(error) {
5731
6042
  return error instanceof Error && error.name === "AbortError";
5732
6043
  }
5733
6044
  //#endregion
5734
- export { AuthenticationError, Client, CodecError, ConnectionError, ConnectionState, ErrCodeKvBackendError, ErrCodeKvIsolationConflict, ErrCodeLeaseHeld, ErrCodeQueueFull, ErrCodeRpcBackpressure, ErrCodeRpcCorrelationNotFound, ErrCodeRpcRouteNotRegistered, ErrCodeRpcTimeout, ErrCodeRpcUnauthorized, ErrCodeRpcWorkerNotFound, ErrKvConflictingWrite, ErrKvKeyNotFound, ErrKvLeaseExpired, ErrKvOperationNotAllowed, ErrKvTransactionAborted, ErrLeaseHeld, ErrLeaseInvalidToken, ErrLeaseNotFound, ErrNoticeGeneral, ErrQueueFull, ErrQueueInvalidDelay, ErrQueueInvalidToken, ErrQueueMessageNotFound, ErrQueueNotFound, ErrRpcHandlerError, ErrRpcHandlerNotFound, ErrRpcInvalidRequest, ErrRpcTimeout, ErrScheduleInvalidCron, ErrScheduleInvalidDelay, ErrScheduleInvalidTimestamp, ErrScheduleNotFound, ErrScheduleTaskNotFound, ErrStreamExpectedOffsetMismatch, ErrStreamFull, ErrStreamInvalidOffset, ErrStreamNotFound, ErrStreamOffsetOutOfRange, ErrStreamSessionClosed, ErrStreamSessionNotFound, FitzError, KvClient, KvError, LeaseClient, LeaseError, NoticeClient, NoticeError, ProtocolError, QueueClient, QueueError, RequestQueueFullError, RpcClient, RpcError, ScheduleClient, ScheduleError, StreamClient, StreamError, TimeoutError, TransportError, createClient, createTaskGroup, isRetryable };
6045
+ export { AuthenticationError, Client, CodecError, ConnectionError, ConnectionState, ErrCodeKvBackendError, ErrCodeKvIsolationConflict, ErrCodeLeaseHeld, ErrCodeQueueFull, ErrCodeRpcBackpressure, ErrCodeRpcCorrelationNotFound, ErrCodeRpcRouteNotRegistered, ErrCodeRpcTimeout, ErrCodeRpcUnauthorized, ErrCodeRpcWorkerNotFound, ErrKvConflictingWrite, ErrKvKeyNotFound, ErrKvLeaseExpired, ErrKvOperationNotAllowed, ErrKvTransactionAborted, ErrLeaseHeld, ErrLeaseInvalidToken, ErrLeaseNotFound, ErrNoticeGeneral, ErrQueueFull, ErrQueueInvalidDelay, ErrQueueInvalidToken, ErrQueueMessageNotFound, ErrQueueNotFound, ErrRpcHandlerError, ErrRpcHandlerNotFound, ErrRpcInvalidRequest, ErrRpcTimeout, ErrScheduleInvalidCron, ErrScheduleInvalidDelay, ErrScheduleInvalidTimestamp, ErrScheduleNotFound, ErrScheduleTaskNotFound, ErrStreamExpectedOffsetMismatch, ErrStreamFull, ErrStreamInvalidOffset, ErrStreamNotFound, ErrStreamOffsetOutOfRange, ErrStreamSessionClosed, ErrStreamSessionNotFound, FitzError, KvClient, KvError, LeaseClient, LeaseError, NoticeClient, NoticeError, ProtocolError, QueueClient, QueueError, RequestQueueFullError, RpcClient, RpcError, ScheduleClient, ScheduleError, StreamClient, StreamError, TimeoutError, TransportError, createClient, createTaskGroup, createWakeGate, isRetryable };
5735
6046
 
5736
6047
  //# sourceMappingURL=index.mjs.map