@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.cjs CHANGED
@@ -978,7 +978,7 @@ function createMultiplexer(observability = {}) {
978
978
  const span = tracer?.startSpan("fitz.request", attributes);
979
979
  let spanEnded = false;
980
980
  if (signal?.aborted) {
981
- const error = abortError$2();
981
+ const error = abortError$3();
982
982
  span?.recordException(error);
983
983
  span?.end();
984
984
  meter?.counter("fitz.request.failed", 1, {
@@ -1052,7 +1052,7 @@ function createMultiplexer(observability = {}) {
1052
1052
  heapifyUp(requestEntry.timeoutIndex);
1053
1053
  if (signal) {
1054
1054
  onAbort = () => {
1055
- failRequest(abortError$2());
1055
+ failRequest(abortError$3());
1056
1056
  };
1057
1057
  signal.addEventListener("abort", onAbort, { once: true });
1058
1058
  }
@@ -1072,7 +1072,7 @@ function createMultiplexer(observability = {}) {
1072
1072
  return deferred.promise;
1073
1073
  }, (err) => {
1074
1074
  if (!finalize()) {
1075
- if (signal?.aborted) throw abortError$2();
1075
+ if (signal?.aborted) throw abortError$3();
1076
1076
  throw err;
1077
1077
  }
1078
1078
  unregisterRequest(messageType, requestEntry);
@@ -1085,7 +1085,7 @@ function createMultiplexer(observability = {}) {
1085
1085
  span?.end();
1086
1086
  spanEnded = true;
1087
1087
  }
1088
- if (signal?.aborted) throw abortError$2();
1088
+ if (signal?.aborted) throw abortError$3();
1089
1089
  throw err;
1090
1090
  });
1091
1091
  };
@@ -1197,7 +1197,7 @@ function createMultiplexer(observability = {}) {
1197
1197
  const Multiplexer = function(observability = {}) {
1198
1198
  return createMultiplexer(observability);
1199
1199
  };
1200
- function abortError$2() {
1200
+ function abortError$3() {
1201
1201
  const error = /* @__PURE__ */ new Error("The operation was aborted");
1202
1202
  error.name = "AbortError";
1203
1203
  return error;
@@ -1281,7 +1281,7 @@ function shouldRetryOperation(retryClass, error) {
1281
1281
  //#region src/client/connection.ts
1282
1282
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1283
1283
  const sleepWithAbort = async (ms, signal) => {
1284
- if (signal?.aborted) throw abortError$1();
1284
+ if (signal?.aborted) throw abortError$2();
1285
1285
  if (ms <= 0) return;
1286
1286
  await new Promise((resolve, reject) => {
1287
1287
  const timer = setTimeout(() => {
@@ -1290,7 +1290,7 @@ const sleepWithAbort = async (ms, signal) => {
1290
1290
  }, ms);
1291
1291
  const onAbort = () => {
1292
1292
  cleanup();
1293
- reject(abortError$1());
1293
+ reject(abortError$2());
1294
1294
  };
1295
1295
  const cleanup = () => {
1296
1296
  clearTimeout(timer);
@@ -1366,7 +1366,7 @@ function createRequestGate(maxConcurrency, maxQueueSize) {
1366
1366
  let closed = false;
1367
1367
  const queue = [];
1368
1368
  const acquire = async (signal) => {
1369
- if (signal?.aborted) throw abortError$1();
1369
+ if (signal?.aborted) throw abortError$2();
1370
1370
  if (closed) throw connectionClosedError();
1371
1371
  return await new Promise((resolve, reject) => {
1372
1372
  const grant = () => {
@@ -1389,7 +1389,7 @@ function createRequestGate(maxConcurrency, maxQueueSize) {
1389
1389
  waiter.onAbort = () => {
1390
1390
  removeWaiter(waiter);
1391
1391
  cleanup();
1392
- reject(abortError$1());
1392
+ reject(abortError$2());
1393
1393
  };
1394
1394
  if (signal) signal.addEventListener("abort", waiter.onAbort, { once: true });
1395
1395
  if (activeCount < maxConcurrency) {
@@ -1438,7 +1438,7 @@ function createRequestGate(maxConcurrency, maxQueueSize) {
1438
1438
  close
1439
1439
  };
1440
1440
  }
1441
- function abortError$1() {
1441
+ function abortError$2() {
1442
1442
  const error = /* @__PURE__ */ new Error("The operation was aborted");
1443
1443
  error.name = "AbortError";
1444
1444
  return error;
@@ -1447,14 +1447,14 @@ function connectionClosedError() {
1447
1447
  return new ConnectionError("Connection closed", { state: "CLOSED" });
1448
1448
  }
1449
1449
  function throwIfAborted$1(signal) {
1450
- if (signal?.aborted) throw abortError$1();
1450
+ if (signal?.aborted) throw abortError$2();
1451
1451
  }
1452
1452
  function isAbortError$1(error) {
1453
1453
  return error instanceof Error && error.name === "AbortError";
1454
1454
  }
1455
1455
  const waitForSharedPromise$1 = async (promise, signal) => {
1456
1456
  if (!signal) return promise;
1457
- if (signal.aborted) throw abortError$1();
1457
+ if (signal.aborted) throw abortError$2();
1458
1458
  return await new Promise((resolve, reject) => {
1459
1459
  let settled = false;
1460
1460
  const cleanup = () => {
@@ -1467,7 +1467,7 @@ const waitForSharedPromise$1 = async (promise, signal) => {
1467
1467
  callback();
1468
1468
  };
1469
1469
  const onAbort = () => {
1470
- settle(() => reject(abortError$1()));
1470
+ settle(() => reject(abortError$2()));
1471
1471
  };
1472
1472
  signal.addEventListener("abort", onAbort, { once: true });
1473
1473
  promise.then((value) => {
@@ -1488,6 +1488,9 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1488
1488
  const retryMaxAttempts = options.retry?.maxAttempts ?? 3;
1489
1489
  const retryBackoffMs = options.retry?.backoffMs ?? 100;
1490
1490
  const retryMaxBackoffMs = options.retry?.maxBackoffMs ?? 1e3;
1491
+ const heartbeatEnabled = options.heartbeat?.enabled ?? true;
1492
+ const heartbeatIntervalMs = options.heartbeat?.intervalMs ?? 1e4;
1493
+ const heartbeatTimeoutMs = options.heartbeat?.timeoutMs ?? 3e4;
1491
1494
  const maxInFlightRequests = options.maxInFlightRequests ?? 256;
1492
1495
  const maxRequestQueueSize = options.maxRequestQueueSize ?? 1024;
1493
1496
  const observability = options.observability;
@@ -1504,6 +1507,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1504
1507
  let permanentlyClosed = false;
1505
1508
  let connectPromise = null;
1506
1509
  let reconnectPromise = null;
1510
+ let connectionLossPromise = null;
1507
1511
  let reconnectRestoreActive = false;
1508
1512
  let authOutcome = null;
1509
1513
  let authRejected = false;
@@ -1513,6 +1517,10 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1513
1517
  const closeAbortController = new AbortController();
1514
1518
  const connectionScope = createScope("connection");
1515
1519
  const readyListeners = /* @__PURE__ */ new Set();
1520
+ let heartbeatTimer = null;
1521
+ let heartbeatTransport = null;
1522
+ let heartbeatPending = false;
1523
+ let lastActivityAt = Date.now();
1516
1524
  const log = (level, event, fields) => {
1517
1525
  observability?.logger?.log(level, event, fields);
1518
1526
  };
@@ -1582,6 +1590,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1582
1590
  permanentlyClosed = true;
1583
1591
  closeRequested = true;
1584
1592
  receiveLoopAbort = true;
1593
+ stopHeartbeat();
1585
1594
  closeAbortController.abort();
1586
1595
  asyncHandlerDispatcher.close();
1587
1596
  const scopeDisposePromise = connectionScope.dispose();
@@ -1763,7 +1772,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1763
1772
  cb();
1764
1773
  };
1765
1774
  const onAbort = () => {
1766
- settle(() => reject(abortError$1()));
1775
+ settle(() => reject(abortError$2()));
1767
1776
  };
1768
1777
  const onStateChange = () => {
1769
1778
  const failure = readyFailure();
@@ -1846,6 +1855,75 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1846
1855
  }
1847
1856
  }
1848
1857
  };
1858
+ const withWriteLock = async (operation) => {
1859
+ const prior = writeChain;
1860
+ let release;
1861
+ writeChain = new Promise((resolve) => {
1862
+ release = resolve;
1863
+ });
1864
+ await prior;
1865
+ try {
1866
+ return await operation();
1867
+ } finally {
1868
+ release();
1869
+ }
1870
+ };
1871
+ const markOutboundActivity = () => {
1872
+ lastActivityAt = Date.now();
1873
+ };
1874
+ const markRemoteActivity = () => {
1875
+ lastActivityAt = Date.now();
1876
+ };
1877
+ const stopHeartbeat = () => {
1878
+ if (heartbeatTimer) {
1879
+ clearTimeout(heartbeatTimer);
1880
+ heartbeatTimer = null;
1881
+ }
1882
+ heartbeatTransport = null;
1883
+ heartbeatPending = false;
1884
+ };
1885
+ const startHeartbeat = (activeTransport) => {
1886
+ if (!heartbeatEnabled) return;
1887
+ stopHeartbeat();
1888
+ activeTransport.enableKeepAlive?.(heartbeatIntervalMs);
1889
+ heartbeatTransport = activeTransport;
1890
+ markRemoteActivity();
1891
+ const scheduleNext = () => {
1892
+ if (closeRequested || receiveLoopAbort || heartbeatTransport !== activeTransport) return;
1893
+ heartbeatTimer = setTimeout(tick, heartbeatIntervalMs);
1894
+ };
1895
+ const tick = () => {
1896
+ heartbeatTimer = null;
1897
+ if (closeRequested || receiveLoopAbort || heartbeatTransport !== activeTransport) return;
1898
+ const now = Date.now();
1899
+ if (now - lastActivityAt < heartbeatIntervalMs) {
1900
+ scheduleNext();
1901
+ return;
1902
+ }
1903
+ const supportsHeartbeat = activeTransport.supportsHeartbeat?.() ?? true;
1904
+ if (!heartbeatPending && supportsHeartbeat && activeTransport.sendHeartbeat) {
1905
+ heartbeatPending = true;
1906
+ const heartbeatSentAt = now;
1907
+ const dispatchHeartbeat = (heartbeat) => activeTransport.sendHeartbeat(heartbeat);
1908
+ withWriteLock(async () => {
1909
+ await dispatchHeartbeat({ timeoutMs: heartbeatTimeoutMs });
1910
+ }).then(() => {
1911
+ if (heartbeatTransport !== activeTransport) return;
1912
+ heartbeatPending = false;
1913
+ markRemoteActivity();
1914
+ }).catch((error) => {
1915
+ if (heartbeatTransport !== activeTransport) return;
1916
+ heartbeatPending = false;
1917
+ if (lastActivityAt > heartbeatSentAt) return;
1918
+ const heartbeatError = new TransportError(`Heartbeat failed: ${describeError(error)}`);
1919
+ activeTransport.close().catch(() => void 0);
1920
+ handleConnectionLoss(heartbeatError);
1921
+ });
1922
+ }
1923
+ scheduleNext();
1924
+ };
1925
+ scheduleNext();
1926
+ };
1849
1927
  const openAndAuthenticate = async (isReconnect, signal) => {
1850
1928
  throwIfAborted$1(signal);
1851
1929
  receiveLoopAbort = false;
@@ -1853,10 +1931,13 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1853
1931
  requestGate = createRequestGate(maxInFlightRequests, maxRequestQueueSize);
1854
1932
  const activeTransport = transportFactory();
1855
1933
  transport = activeTransport;
1934
+ stopHeartbeat();
1856
1935
  setState(isReconnect ? "RECONNECTING" : "CONNECTING");
1857
1936
  emitLifecycleEvent(isReconnect ? "reconnect_start" : "connect_start");
1858
1937
  await activeTransport.connect();
1938
+ markRemoteActivity();
1859
1939
  if (closeRequested) {
1940
+ stopHeartbeat();
1860
1941
  await activeTransport.close().catch(() => void 0);
1861
1942
  if (transport === activeTransport) transport = null;
1862
1943
  throw connectionClosedError();
@@ -1889,6 +1970,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1889
1970
  hasEstablishedSession = true;
1890
1971
  reconnectExhausted = false;
1891
1972
  setState("AUTHENTICATED");
1973
+ startHeartbeat(activeTransport);
1892
1974
  if (!isReconnect) multiplexer.setConnected();
1893
1975
  emitLifecycleEvent(isReconnect ? "reconnect_succeeded" : "connect_succeeded");
1894
1976
  } catch (error) {
@@ -1896,6 +1978,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1896
1978
  reconnectRestoreActive = false;
1897
1979
  multiplexer.setDisconnected();
1898
1980
  emitDisconnect();
1981
+ stopHeartbeat();
1899
1982
  if (activeTransport) await activeTransport.close().catch(() => void 0);
1900
1983
  if (transport === activeTransport) transport = null;
1901
1984
  const rejectedAuth = error instanceof AuthenticationError;
@@ -1903,7 +1986,7 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1903
1986
  if (closeRequested) setState("CLOSED");
1904
1987
  else setState(rejectedAuth ? "CLOSED" : "DISCONNECTED");
1905
1988
  emitLifecycleEvent(isReconnect ? "reconnect_failed" : "connect_failed", error);
1906
- if (isAbortError$1(error)) throw abortError$1();
1989
+ if (isAbortError$1(error)) throw abortError$2();
1907
1990
  throw error;
1908
1991
  }
1909
1992
  };
@@ -1911,10 +1994,12 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1911
1994
  const token = await tokenProvider();
1912
1995
  const frame = FrameCodec.encodeFrame(1, utf8Encoder.encode(token));
1913
1996
  await ensureTransport().send(frame);
1997
+ markOutboundActivity();
1914
1998
  };
1915
1999
  const startReceiveLoop = async () => {
1916
2000
  while (!receiveLoopAbort && !closeRequested) try {
1917
2001
  const data = await ensureTransport().receive();
2002
+ markRemoteActivity();
1918
2003
  const frames = frameParser.parseFrames(data);
1919
2004
  for (const frame of frames) multiplexer.dispatch(frame.messageType, frame.payload);
1920
2005
  } catch (error) {
@@ -1924,6 +2009,17 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
1924
2009
  }
1925
2010
  };
1926
2011
  const handleConnectionLoss = async (error) => {
2012
+ if (connectionLossPromise) {
2013
+ await connectionLossPromise;
2014
+ return;
2015
+ }
2016
+ connectionLossPromise = handleConnectionLossOnce(error).finally(() => {
2017
+ connectionLossPromise = null;
2018
+ });
2019
+ await connectionLossPromise;
2020
+ };
2021
+ const handleConnectionLossOnce = async (error) => {
2022
+ stopHeartbeat();
1927
2023
  multiplexer.setDisconnected();
1928
2024
  requestGate.close();
1929
2025
  emitDisconnect();
@@ -2010,17 +2106,10 @@ function createConnection(transportFactory, tokenProvider, options = {}) {
2010
2106
  notifyReadyListeners();
2011
2107
  };
2012
2108
  const sendSerialized = async (transport, data) => {
2013
- const prior = writeChain;
2014
- let release;
2015
- writeChain = new Promise((resolve) => {
2016
- release = resolve;
2017
- });
2018
- await prior;
2019
- try {
2109
+ await withWriteLock(async () => {
2020
2110
  await transport.send(data);
2021
- } finally {
2022
- release();
2023
- }
2111
+ markOutboundActivity();
2112
+ });
2024
2113
  };
2025
2114
  const emitLifecycleEvent = (event, error, attempt) => {
2026
2115
  const payload = {
@@ -2096,6 +2185,7 @@ function createWebSocketTransport(url, options = {}) {
2096
2185
  let receiverResolve = null;
2097
2186
  const timeout = options.timeout ?? 3e4;
2098
2187
  const maxFrameSize = options.maxFrameSize ?? 65535;
2188
+ const receiveTimeoutEnabled = options.receiveTimeout ?? true;
2099
2189
  const enqueueMessage = (data) => {
2100
2190
  if (receiverResolve) {
2101
2191
  receiverResolve(data);
@@ -2169,6 +2259,56 @@ function createWebSocketTransport(url, options = {}) {
2169
2259
  }
2170
2260
  });
2171
2261
  };
2262
+ const sendHeartbeat = async (heartbeatOptions) => {
2263
+ if (!connected || !ws) throw new TransportError("WebSocket is not connected");
2264
+ const activeWs = ws;
2265
+ if (typeof activeWs.ping !== "function" || typeof activeWs.once !== "function" || typeof activeWs.removeListener !== "function") throw new TransportError("WebSocket heartbeat is not supported");
2266
+ const socket = activeWs;
2267
+ return new Promise((resolve, reject) => {
2268
+ let settled = false;
2269
+ let timeoutId = null;
2270
+ const cleanup = () => {
2271
+ if (timeoutId) clearTimeout(timeoutId);
2272
+ socket.removeListener("pong", onPong);
2273
+ socket.removeListener("close", onClose);
2274
+ socket.removeListener("error", onError);
2275
+ };
2276
+ const settle = (callback) => {
2277
+ if (settled) return;
2278
+ settled = true;
2279
+ cleanup();
2280
+ callback();
2281
+ };
2282
+ const onPong = () => {
2283
+ settle(resolve);
2284
+ };
2285
+ const onClose = () => {
2286
+ settle(() => reject(new TransportError("WebSocket closed during heartbeat")));
2287
+ };
2288
+ const onError = (...args) => {
2289
+ const event = args[0];
2290
+ const message = event instanceof Error ? event.message : event?.message || "unknown error";
2291
+ settle(() => reject(new TransportError(`WebSocket heartbeat failed: ${message}`)));
2292
+ };
2293
+ timeoutId = setTimeout(() => {
2294
+ settle(() => reject(new TimeoutError(`WebSocket heartbeat timeout after ${heartbeatOptions.timeoutMs}ms`)));
2295
+ }, heartbeatOptions.timeoutMs);
2296
+ socket.once("pong", onPong);
2297
+ socket.once("close", onClose);
2298
+ socket.once("error", onError);
2299
+ try {
2300
+ socket.ping(new Uint8Array(), void 0, (err) => {
2301
+ if (err) settle(() => reject(new TransportError(`WebSocket ping failed: ${err.message}`)));
2302
+ });
2303
+ } catch (err) {
2304
+ settle(() => reject(new TransportError(`WebSocket heartbeat error: ${err instanceof Error ? err.message : String(err)}`)));
2305
+ }
2306
+ });
2307
+ };
2308
+ const supportsHeartbeat = () => {
2309
+ return typeof ws?.ping === "function" && typeof ws?.once === "function" && typeof ws?.removeListener === "function";
2310
+ };
2311
+ const enableKeepAlive = () => void 0;
2172
2312
  const receive = async () => {
2173
2313
  if (receiveQueue.length > 0) {
2174
2314
  const message = receiveQueue.shift();
@@ -2177,12 +2317,12 @@ function createWebSocketTransport(url, options = {}) {
2177
2317
  }
2178
2318
  if (!connected) throw new TransportError("Connection closed");
2179
2319
  return new Promise((resolve, reject) => {
2180
- const timeoutId = setTimeout(() => {
2320
+ const timeoutId = receiveTimeoutEnabled ? setTimeout(() => {
2181
2321
  receiverResolve = null;
2182
2322
  reject(new TimeoutError(`WebSocket receive timeout after ${timeout}ms`));
2183
- }, timeout);
2323
+ }, timeout) : null;
2184
2324
  receiverResolve = (data) => {
2185
- clearTimeout(timeoutId);
2325
+ if (timeoutId) clearTimeout(timeoutId);
2186
2326
  receiverResolve = null;
2187
2327
  if (data === null) {
2188
2328
  reject(new TransportError("Connection closed"));
@@ -2217,6 +2357,9 @@ function createWebSocketTransport(url, options = {}) {
2217
2357
  connect,
2218
2358
  send,
2219
2359
  receive,
2360
+ sendHeartbeat,
2361
+ supportsHeartbeat,
2362
+ enableKeepAlive,
2220
2363
  close,
2221
2364
  getUrl,
2222
2365
  isConnected
@@ -2242,6 +2385,7 @@ function createTcpTransport(url, options = {}) {
2242
2385
  let receiverResolve = null;
2243
2386
  const timeout = options.timeout ?? 3e4;
2244
2387
  const maxFrameSize = options.maxFrameSize ?? 65535;
2388
+ const receiveTimeoutEnabled = options.receiveTimeout ?? true;
2245
2389
  let lengthBuffer = new Uint8Array(4);
2246
2390
  let lengthOffset = 0;
2247
2391
  let currentMessageLength = null;
@@ -2316,7 +2460,7 @@ function createTcpTransport(url, options = {}) {
2316
2460
  clearTimeout(connectTimeout);
2317
2461
  connected = true;
2318
2462
  activeSocket.setNoDelay(true);
2319
- activeSocket.setTimeout(timeout);
2463
+ activeSocket.setTimeout(receiveTimeoutEnabled ? timeout : 0);
2320
2464
  resolve();
2321
2465
  });
2322
2466
  activeSocket.on("data", (chunk) => {
@@ -2334,8 +2478,10 @@ function createTcpTransport(url, options = {}) {
2334
2478
  if (receiverResolve) receiverResolve(new Uint8Array(0));
2335
2479
  });
2336
2480
  activeSocket.on("timeout", () => {
2337
- activeSocket.destroy();
2338
- connected = false;
2481
+ if (receiveTimeoutEnabled) {
2482
+ activeSocket.destroy();
2483
+ connected = false;
2484
+ }
2339
2485
  });
2340
2486
  } catch (err) {
2341
2487
  reject(new TransportError(`Failed to create TCP socket: ${err instanceof Error ? err.message : String(err)}`));
@@ -2360,6 +2506,10 @@ function createTcpTransport(url, options = {}) {
2360
2506
  });
2361
2507
  });
2362
2508
  };
2509
+ const enableKeepAlive = (intervalMs) => {
2510
+ socket?.setKeepAlive(true, intervalMs);
2511
+ };
2512
+ const supportsHeartbeat = () => false;
2363
2513
  const receive = async () => {
2364
2514
  if (receiveQueue.length > 0) {
2365
2515
  const message = receiveQueue.shift();
@@ -2367,12 +2517,12 @@ function createTcpTransport(url, options = {}) {
2367
2517
  return message;
2368
2518
  }
2369
2519
  return new Promise((resolve, reject) => {
2370
- const timeoutId = setTimeout(() => {
2520
+ const timeoutId = receiveTimeoutEnabled ? setTimeout(() => {
2371
2521
  receiverResolve = null;
2372
2522
  reject(new TimeoutError(`TCP receive timeout after ${timeout}ms`));
2373
- }, timeout);
2523
+ }, timeout) : null;
2374
2524
  receiverResolve = (data) => {
2375
- clearTimeout(timeoutId);
2525
+ if (timeoutId) clearTimeout(timeoutId);
2376
2526
  receiverResolve = null;
2377
2527
  if (data.length === 0) reject(new TransportError("Connection closed"));
2378
2528
  else resolve(data);
@@ -2404,6 +2554,8 @@ function createTcpTransport(url, options = {}) {
2404
2554
  connect,
2405
2555
  send,
2406
2556
  receive,
2557
+ supportsHeartbeat,
2558
+ enableKeepAlive,
2407
2559
  close,
2408
2560
  getUrl,
2409
2561
  isConnected
@@ -2782,6 +2934,64 @@ const KvClient = function(connection) {
2782
2934
  return createKvClient(connection);
2783
2935
  };
2784
2936
  //#endregion
2937
+ //#region src/core/wake-gate.ts
2938
+ function abortError$1() {
2939
+ const error = /* @__PURE__ */ new Error("The operation was aborted");
2940
+ error.name = "AbortError";
2941
+ return error;
2942
+ }
2943
+ function createWakeGate() {
2944
+ let currentVersion = 0;
2945
+ const waiters = /* @__PURE__ */ new Set();
2946
+ const cleanup = (waiter) => {
2947
+ waiters.delete(waiter);
2948
+ if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener("abort", waiter.onAbort);
2949
+ };
2950
+ const wake = () => {
2951
+ currentVersion += 1;
2952
+ const version = currentVersion;
2953
+ for (const waiter of Array.from(waiters)) {
2954
+ cleanup(waiter);
2955
+ waiter.resolve(version);
2956
+ }
2957
+ return version;
2958
+ };
2959
+ const waitAfter = async (observedVersion, options = {}) => {
2960
+ if (currentVersion > observedVersion) return currentVersion;
2961
+ if (options.signal?.aborted) throw abortError$1();
2962
+ return await new Promise((resolve, reject) => {
2963
+ const waiter = {
2964
+ observedVersion,
2965
+ resolve,
2966
+ reject,
2967
+ signal: options.signal
2968
+ };
2969
+ waiter.onAbort = () => {
2970
+ cleanup(waiter);
2971
+ reject(abortError$1());
2972
+ };
2973
+ if (options.signal) options.signal.addEventListener("abort", waiter.onAbort, { once: true });
2974
+ if (currentVersion > observedVersion) {
2975
+ cleanup(waiter);
2976
+ resolve(currentVersion);
2977
+ return;
2978
+ }
2979
+ waiters.add(waiter);
2980
+ });
2981
+ };
2982
+ const wait = async (options) => {
2983
+ return await waitAfter(currentVersion, options);
2984
+ };
2985
+ return {
2986
+ get version() {
2987
+ return currentVersion;
2988
+ },
2989
+ wake,
2990
+ waitAfter,
2991
+ wait
2992
+ };
2993
+ }
2994
+ //#endregion
2785
2995
  //#region src/domains/queue/codec.ts
2786
2996
  /**
2787
2997
  * Queue domain codec for encoding and decoding protocol messages.
@@ -3120,43 +3330,55 @@ function createQueueClient(connection) {
3120
3330
  let items = await reserveOnce(route, leaseSeconds, batchSize);
3121
3331
  if (items.length > 0) return items;
3122
3332
  const deadline = Date.now() + waitSeconds * 1e3;
3123
- let pendingNotifications = 0;
3124
- let waiter;
3125
- const subscription = await subscribe(route, async () => {
3126
- pendingNotifications += 1;
3127
- if (!waiter) return;
3128
- const resolve = waiter;
3129
- waiter = void 0;
3130
- pendingNotifications = 0;
3131
- resolve();
3333
+ const wakeGate = createWakeGate();
3334
+ const subscription = await subscribe(route, () => {
3335
+ wakeGate.wake();
3132
3336
  });
3133
3337
  try {
3134
3338
  while (true) {
3339
+ const observed = wakeGate.version;
3135
3340
  items = await reserveOnce(route, leaseSeconds, batchSize);
3136
3341
  if (items.length > 0) return items;
3137
3342
  const remainingMs = deadline - Date.now();
3138
3343
  if (remainingMs <= 0) return items;
3139
- await new Promise((resolve) => {
3140
- if (pendingNotifications > 0) {
3141
- pendingNotifications = 0;
3142
- resolve();
3143
- return;
3144
- }
3145
- const release = () => {
3146
- clearTimeout(timeoutId);
3147
- if (waiter === release) waiter = void 0;
3148
- resolve();
3149
- };
3150
- const timeoutId = setTimeout(release, remainingMs);
3151
- waiter = release;
3344
+ const waitPromise = wakeGate.waitAfter(observed);
3345
+ let timeoutId = null;
3346
+ const timeoutPromise = new Promise((resolve) => {
3347
+ timeoutId = setTimeout(() => {
3348
+ resolve("timeout");
3349
+ }, remainingMs);
3152
3350
  });
3351
+ if (await Promise.race([waitPromise.then(() => {
3352
+ if (timeoutId) clearTimeout(timeoutId);
3353
+ return "wake";
3354
+ }), timeoutPromise]) === "timeout") return items;
3153
3355
  }
3154
3356
  } finally {
3155
- await subscription.unsubscribe();
3357
+ await subscription.unsubscribe().catch(() => void 0);
3156
3358
  }
3157
3359
  };
3158
- const reserveOnce = async (route, leaseSeconds, batchSize) => {
3159
- const response = await requestFrame(202, QueueCodec.encodeReserve(route, leaseSeconds, batchSize));
3360
+ const reserveWhenAvailable = async function* (route, options) {
3361
+ assertQueueReserveRoute(route);
3362
+ const wakeGate = createWakeGate();
3363
+ const subscription = await subscribe(route, () => {
3364
+ wakeGate.wake();
3365
+ });
3366
+ try {
3367
+ while (true) {
3368
+ const observed = wakeGate.version;
3369
+ const items = await reserveOnce(route, options.leaseSeconds, options.batchSize ?? 1, options.signal);
3370
+ if (items.length > 0) {
3371
+ yield items;
3372
+ continue;
3373
+ }
3374
+ await wakeGate.waitAfter(observed, { signal: options.signal });
3375
+ }
3376
+ } finally {
3377
+ await subscription.unsubscribe().catch(() => void 0);
3378
+ }
3379
+ };
3380
+ const reserveOnce = async (route, leaseSeconds, batchSize, signal) => {
3381
+ const response = await requestFrame(202, QueueCodec.encodeReserve(route, leaseSeconds, batchSize), signal);
3160
3382
  const decoded = QueueCodec.decodeReserveResponse(response);
3161
3383
  checkStatus(decoded, "RESERVE");
3162
3384
  return (decoded.items ?? []).map((item) => createQueueItem(item.id, item.token, item.body, route, connection));
@@ -3245,6 +3467,7 @@ function createQueueClient(connection) {
3245
3467
  return {
3246
3468
  enqueue,
3247
3469
  reserve,
3470
+ reserveWhenAvailable,
3248
3471
  subscribe
3249
3472
  };
3250
3473
  }
@@ -4963,6 +5186,33 @@ function createStreamClient(connection) {
4963
5186
  const page = await readPage(route, startOffset, limit, options);
4964
5187
  return StreamCodec.flattenStreamReadItems(page.items);
4965
5188
  };
5189
+ const readWhenCommitted = async function* (route, options) {
5190
+ assertStreamPattern(route);
5191
+ const wakeGate = createWakeGate();
5192
+ const subscription = await subscribe(route, () => {
5193
+ wakeGate.wake();
5194
+ });
5195
+ try {
5196
+ let offset = options.offset;
5197
+ while (true) {
5198
+ const observed = wakeGate.version;
5199
+ const page = await readPage(route, offset, options.batchSize ?? 100, {
5200
+ maxBytes: options.maxBytes,
5201
+ filter: options.filter,
5202
+ signal: options.signal
5203
+ });
5204
+ if (page.items.length > 0) {
5205
+ offset = page.cursor.lastResourceOffset + 1n;
5206
+ const records = StreamCodec.flattenStreamReadItems(page.items);
5207
+ if (records.length > 0) yield records;
5208
+ }
5209
+ if (page.cursor.hasMore) continue;
5210
+ await wakeGate.waitAfter(observed, { signal: options.signal });
5211
+ }
5212
+ } finally {
5213
+ await subscription.unsubscribe().catch(() => void 0);
5214
+ }
5215
+ };
4966
5216
  const consume = async (route, startOffset, limit = 100, options) => {
4967
5217
  return createAsyncIterableIterator(createSliceIterator(await read(route, startOffset, limit, options)));
4968
5218
  };
@@ -5082,6 +5332,7 @@ function createStreamClient(connection) {
5082
5332
  begin,
5083
5333
  readPage,
5084
5334
  read,
5335
+ readWhenCommitted,
5085
5336
  consume,
5086
5337
  peek,
5087
5338
  metadata,
@@ -5262,6 +5513,7 @@ function createScheduleClient(connection) {
5262
5513
  const { requestFrame, requestReconnectFrame } = createDomainClient(connection);
5263
5514
  const subscriptionsByPattern = /* @__PURE__ */ new Map();
5264
5515
  const patternsBySubId = /* @__PURE__ */ new Map();
5516
+ const pendingNotificationsBySubId = /* @__PURE__ */ new Map();
5265
5517
  let notifyHandlerInitialized = false;
5266
5518
  let nextHandlerId = 1;
5267
5519
  connection.onReconnect(async () => {
@@ -5272,6 +5524,7 @@ function createScheduleClient(connection) {
5272
5524
  }));
5273
5525
  subscriptionsByPattern.clear();
5274
5526
  patternsBySubId.clear();
5527
+ pendingNotificationsBySubId.clear();
5275
5528
  for (const subscription of subscriptions) {
5276
5529
  const subId = await subscribeWire(subscription.pattern, requestReconnectFrame);
5277
5530
  subscriptionsByPattern.set(subscription.pattern, {
@@ -5296,6 +5549,29 @@ function createScheduleClient(connection) {
5296
5549
  const decoded = ScheduleCodec.decodeListResponse(assertSuccess(response, "LIST"));
5297
5550
  return [decoded.entries, decoded.totalCount];
5298
5551
  };
5552
+ const waitForNotifications = async function* (route, options = {}) {
5553
+ assertConcreteScheduleRoute(route);
5554
+ const wakeGate = createWakeGate();
5555
+ const pendingNotifications = [];
5556
+ const subscription = await subscribe(route, (notification) => {
5557
+ pendingNotifications.push(notification);
5558
+ wakeGate.wake();
5559
+ });
5560
+ try {
5561
+ while (true) {
5562
+ const notification = pendingNotifications.shift();
5563
+ if (notification) {
5564
+ yield notification;
5565
+ continue;
5566
+ }
5567
+ const observed = wakeGate.version;
5568
+ if (pendingNotifications.length > 0) continue;
5569
+ await wakeGate.waitAfter(observed, { signal: options.signal });
5570
+ }
5571
+ } finally {
5572
+ await subscription.unsubscribe().catch(() => void 0);
5573
+ }
5574
+ };
5299
5575
  const subscribe = async (pattern, handler) => {
5300
5576
  assertConcreteScheduleRoute(pattern);
5301
5577
  initNotifyHandler();
@@ -5319,6 +5595,7 @@ function createScheduleClient(connection) {
5319
5595
  patternsBySubId.set(subId, pattern);
5320
5596
  }
5321
5597
  subscription.handlers.set(handlerId, handler);
5598
+ flushPendingNotifications(subId);
5322
5599
  return createScheduleSubscription(subId, pattern, async () => {
5323
5600
  await unsubscribe(pattern, handlerId);
5324
5601
  });
@@ -5330,6 +5607,7 @@ function createScheduleClient(connection) {
5330
5607
  if (subscription.handlers.size > 0) return;
5331
5608
  subscriptionsByPattern.delete(pattern);
5332
5609
  patternsBySubId.delete(subscription.subId);
5610
+ pendingNotificationsBySubId.delete(subscription.subId);
5333
5611
  const response = await requestFrame(704, ScheduleCodec.encodeUnsubscribe(pattern));
5334
5612
  ScheduleCodec.decodeUnsubscribeResponse(assertSuccess(response, "UNSUBSCRIBE"));
5335
5613
  };
@@ -5340,16 +5618,40 @@ function createScheduleClient(connection) {
5340
5618
  try {
5341
5619
  const decoded = ScheduleCodec.decodeNotification(payload);
5342
5620
  const pattern = patternsBySubId.get(decoded.subId);
5343
- if (!pattern) return;
5621
+ if (!pattern) {
5622
+ queuePendingNotification(decoded.subId, { payload: decoded.payload });
5623
+ return;
5624
+ }
5344
5625
  const subscription = subscriptionsByPattern.get(pattern);
5345
- if (!subscription) return;
5346
- const notification = { payload: decoded.payload };
5347
- for (const handler of subscription.handlers.values()) connection.dispatchAsyncHandler(async () => {
5348
- await handler(notification);
5349
- });
5626
+ if (!subscription) {
5627
+ queuePendingNotification(decoded.subId, { payload: decoded.payload });
5628
+ return;
5629
+ }
5630
+ dispatchNotification(subscription, { payload: decoded.payload });
5350
5631
  } catch {}
5351
5632
  });
5352
5633
  };
5634
+ const queuePendingNotification = (subId, notification) => {
5635
+ const existing = pendingNotificationsBySubId.get(subId);
5636
+ if (existing) {
5637
+ existing.push(notification);
5638
+ return;
5639
+ }
5640
+ pendingNotificationsBySubId.set(subId, [notification]);
5641
+ };
5642
+ const flushPendingNotifications = (subId) => {
5643
+ const pending = pendingNotificationsBySubId.get(subId);
5644
+ if (!pending || pending.length === 0) return;
5645
+ pendingNotificationsBySubId.delete(subId);
5646
+ const subscription = Array.from(subscriptionsByPattern.values()).find((entry) => entry.subId === subId);
5647
+ if (!subscription) return;
5648
+ for (const notification of pending) dispatchNotification(subscription, notification);
5649
+ };
5650
+ const dispatchNotification = (subscription, notification) => {
5651
+ for (const handler of subscription.handlers.values()) connection.dispatchAsyncHandler(async () => {
5652
+ await handler(notification);
5653
+ });
5654
+ };
5353
5655
  const assertSuccess = (payload, operation) => {
5354
5656
  const result = parseStandardResponse(payload);
5355
5657
  if (result.success) return result.data;
@@ -5366,7 +5668,8 @@ function createScheduleClient(connection) {
5366
5668
  create,
5367
5669
  cancel,
5368
5670
  list,
5369
- subscribe
5671
+ subscribe,
5672
+ waitForNotifications
5370
5673
  };
5371
5674
  }
5372
5675
  const ScheduleClient = function(connection) {
@@ -5434,6 +5737,12 @@ function createClient(config) {
5434
5737
  maxBackoffMs: 1e3,
5435
5738
  ...config.retry
5436
5739
  },
5740
+ heartbeat: {
5741
+ enabled: true,
5742
+ intervalMs: 1e4,
5743
+ timeoutMs: 3e4,
5744
+ ...config.heartbeat
5745
+ },
5437
5746
  asyncHandlers: {
5438
5747
  maxConcurrency: Infinity,
5439
5748
  timeoutMs: 3e4,
@@ -5465,7 +5774,8 @@ function createClient(config) {
5465
5774
  if (connection) return connection;
5466
5775
  connection = createConnection(() => createTransport(resolvedConfig.url, resolvedConfig.transport, {
5467
5776
  timeout: resolvedConfig.timeout,
5468
- maxFrameSize: resolvedConfig.maxFrameSize
5777
+ maxFrameSize: resolvedConfig.maxFrameSize,
5778
+ receiveTimeout: resolvedConfig.heartbeat?.enabled === false
5469
5779
  }), resolveTokenProvider(), {
5470
5780
  timeout: resolvedConfig.timeout,
5471
5781
  authSettleDelayMs: resolvedConfig.authSettleDelayMs,
@@ -5473,6 +5783,7 @@ function createClient(config) {
5473
5783
  maxRequestQueueSize: resolvedConfig.maxRequestQueueSize,
5474
5784
  reconnect: resolvedConfig.reconnect,
5475
5785
  retry: resolvedConfig.retry,
5786
+ heartbeat: resolvedConfig.heartbeat,
5476
5787
  observability,
5477
5788
  asyncHandlers: resolvedConfig.asyncHandlers
5478
5789
  });
@@ -5794,6 +6105,7 @@ exports.TimeoutError = TimeoutError;
5794
6105
  exports.TransportError = TransportError;
5795
6106
  exports.createClient = createClient;
5796
6107
  exports.createTaskGroup = createTaskGroup;
6108
+ exports.createWakeGate = createWakeGate;
5797
6109
  exports.isRetryable = isRetryable;
5798
6110
 
5799
6111
  //# sourceMappingURL=index.cjs.map