@zlink-systems/stream-connector 0.16.0 → 0.18.0

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.
@@ -659,6 +659,53 @@ function utf8Decode2(value) {
659
659
  return new TextDecoder().decode(value);
660
660
  }
661
661
 
662
+ // packages/stream-connector/src/Runtime/ZlinkStreamAssertions.ts
663
+ var zlinkStreamAssert = {
664
+ ensure(condition, message) {
665
+ if (typeof message !== "string" || message.trim().length === 0) {
666
+ throw connectorError("validationFailed" /* ValidationFailed */, "zlinkStreamAssert.ensure requires a non-empty diagnostic message.");
667
+ }
668
+ if (!condition) {
669
+ throw connectorError("validationFailed" /* ValidationFailed */, message);
670
+ }
671
+ },
672
+ async expectFailure(action, errorKind) {
673
+ let failure;
674
+ try {
675
+ await action();
676
+ } catch (error) {
677
+ failure = error;
678
+ }
679
+ if (failure === void 0) {
680
+ throw connectorError("validationFailed" /* ValidationFailed */, "Expected action to fail.");
681
+ }
682
+ const streamError = unwrapStreamError(failure);
683
+ if (errorKind !== void 0 && streamError.code !== errorKind) {
684
+ throw connectorError(
685
+ "validationFailed" /* ValidationFailed */,
686
+ `Expected failure kind '${errorKind}', got '${streamError.code}'.`,
687
+ failure
688
+ );
689
+ }
690
+ return streamError;
691
+ },
692
+ async expectTimeout(action) {
693
+ let failure;
694
+ try {
695
+ await action();
696
+ } catch (error) {
697
+ failure = error;
698
+ }
699
+ if (failure === void 0) {
700
+ throw connectorError("validationFailed" /* ValidationFailed */, "Expected action to time out.");
701
+ }
702
+ const code = unwrapStreamError(failure).code;
703
+ if (code !== "requestTimeout" /* RequestTimeout */ && code !== "connectTimeout" /* ConnectTimeout */) {
704
+ throw failure;
705
+ }
706
+ }
707
+ };
708
+
662
709
  // packages/stream-connector/src/Runtime/Protocol/ZlinkStreamPacketNameValidator.ts
663
710
  function validateName(name, allowReserved = false) {
664
711
  if (name.length === 0) {
@@ -895,6 +942,9 @@ var ZlinkStreamSequenceBuilder = class {
895
942
  this.timeoutMs = timeoutMs;
896
943
  return this;
897
944
  }
945
+ // Spec stream-connector 32 §10.1: the predicate reads the whole message, and
946
+ // the call answers with the messages themselves, so a caller can assert on the
947
+ // packet name and the metadata and not only on the payload.
898
948
  async run(signal) {
899
949
  this.markExecuted();
900
950
  if (this.predicates.length === 0) {
@@ -902,13 +952,13 @@ var ZlinkStreamSequenceBuilder = class {
902
952
  }
903
953
  const timeoutMs = this.timeoutMs ?? this.connector.options.waitTimeoutMs;
904
954
  const deadline = Date.now() + timeoutMs;
905
- const payloads = [];
955
+ const messages = [];
906
956
  for (const predicate of this.predicates) {
907
957
  const message = await this.connector.waitForMessage(
908
958
  this.name,
909
959
  Math.max(0, deadline - Date.now()),
910
960
  (candidate) => {
911
- if (!predicate(candidate.payload)) {
961
+ if (!predicate(candidate)) {
912
962
  throw connectorError(
913
963
  "validationFailed" /* ValidationFailed */,
914
964
  `Message '${this.name}' arrived out of the expected sequence.`
@@ -918,9 +968,9 @@ var ZlinkStreamSequenceBuilder = class {
918
968
  },
919
969
  signal
920
970
  );
921
- payloads.push(message.payload);
971
+ messages.push(message);
922
972
  }
923
- return payloads;
973
+ return messages;
924
974
  }
925
975
  ensureConfigurable() {
926
976
  if (this.executed) {
@@ -1021,7 +1071,7 @@ function splitZlinkStreamFrames(chunk) {
1021
1071
  }
1022
1072
  function validatePayload(payloadLength, maxPayloadSize) {
1023
1073
  if (payloadLength > maxPayloadSize) {
1024
- throw connectorError("frameTooLarge" /* FrameTooLarge */, "Payload exceeds MaxSendPayloadSize.");
1074
+ throw connectorError("validationFailed" /* ValidationFailed */, "Payload exceeds MaxSendPayloadSize.");
1025
1075
  }
1026
1076
  }
1027
1077
 
@@ -1303,22 +1353,30 @@ function normalizeOptions(options, defaultTransportFactory) {
1303
1353
  initialDelayMs: options.reconnect?.initialDelayMs ?? 250,
1304
1354
  maxDelayMs: options.reconnect?.maxDelayMs ?? 5e3,
1305
1355
  backoffFactor: options.reconnect?.backoffFactor ?? 2,
1306
- maxAttempts: options.reconnect?.maxAttempts ?? 3
1356
+ maxAttempts: options.reconnect?.maxAttempts === void 0 ? 3 : options.reconnect.maxAttempts
1307
1357
  },
1308
1358
  maxSendPayloadSize: options.maxSendPayloadSize ?? 64 * 1024,
1309
1359
  maxReceivePayloadSize: options.maxReceivePayloadSize ?? 64 * 1024,
1310
1360
  dispatchMode: options.dispatchMode ?? "manual" /* Manual */,
1311
1361
  compression: options.compression ?? "lz4" /* Lz4 */,
1312
1362
  compressionCodec: resolveCompressionCodec2(options),
1313
- nameResolver: options.nameResolver ?? { resolve: (type) => type.name },
1363
+ nameResolver: options.nameResolver ?? defaultPacketNameResolver,
1314
1364
  transportFactory: options.transportFactory ?? defaultTransportFactory,
1315
1365
  codec: options.codec,
1316
- meterProvider: options.meterProvider,
1317
1366
  // Spec 26 §4: the default diagnostics level is Errors, which preserves
1318
1367
  // the connector's established wire behavior.
1319
1368
  diagnosticsLevel: options.diagnosticsLevel ?? "errors" /* Errors */
1320
1369
  };
1321
1370
  }
1371
+ var defaultPacketNameResolver = {
1372
+ resolve(payloadType) {
1373
+ const declared = payloadType.packetName;
1374
+ if (typeof declared === "string" && declared.length > 0) {
1375
+ return declared;
1376
+ }
1377
+ return payloadType.name;
1378
+ }
1379
+ };
1322
1380
  function resolveCompressionCodec2(options) {
1323
1381
  const compression = options.compression ?? "lz4" /* Lz4 */;
1324
1382
  if (compression === "none" /* None */) {
@@ -1362,7 +1420,8 @@ function validateReconnect(options) {
1362
1420
  if ((options?.backoffFactor ?? 2) < 1) {
1363
1421
  throw connectorError("validationFailed" /* ValidationFailed */, "Reconnect BackoffFactor must be at least 1.0.");
1364
1422
  }
1365
- if ((options?.maxAttempts ?? 3) <= 0) {
1423
+ const maxAttempts = options?.maxAttempts === void 0 ? 3 : options.maxAttempts;
1424
+ if (maxAttempts !== null && !(Number.isFinite(maxAttempts) && maxAttempts > 0)) {
1366
1425
  throw connectorError("validationFailed" /* ValidationFailed */, "Reconnect MaxAttempts must be null or positive.");
1367
1426
  }
1368
1427
  }
@@ -1486,6 +1545,16 @@ var ZlinkStreamReceivedMessages = class {
1486
1545
  queueHead = 0;
1487
1546
  queuedCount = 0;
1488
1547
  drainTask;
1548
+ // True for as long as `drain` is on the stack, handler awaits included. It
1549
+ // marks the execution context a registered handler runs in, so a `dispatch`
1550
+ // made from inside a handler is recognised as re-entry rather than a fresh
1551
+ // pump. It is not a lock: a single event loop admits no second thread, and
1552
+ // nothing ever waits for this flag to fall.
1553
+ draining = false;
1554
+ // Spec stream-connector 32 §10: arrivals per packet name on the current
1555
+ // connection. It is raised where a packet arrives, never where one is taken,
1556
+ // so consuming does not lower it and the dispatch mode does not change it.
1557
+ receivedCounts = /* @__PURE__ */ new Map();
1489
1558
  on(name, handler) {
1490
1559
  validateName(name);
1491
1560
  let set = this.handlers.get(name);
@@ -1511,30 +1580,78 @@ var ZlinkStreamReceivedMessages = class {
1511
1580
  * `Manual` needs no dispatch pump to complete a wait. The queue is scanned in
1512
1581
  * a microtask so a message that arrived before the wait started is still
1513
1582
  * observed, and so the caller has its subscription in hand by then.
1583
+ *
1584
+ * @param onConnectionEnded Called, instead of {@link observer}, when
1585
+ * {@link resetForNewConnection} abandons this registration because the
1586
+ * connection it was watching ended before a message matched (spec
1587
+ * stream-connector 32 §10.1: "연결이 끝나 대기를 이어갈 수 없으면
1588
+ * `Disconnected`다").
1514
1589
  */
1515
- observe(name, observer) {
1590
+ observe(name, observer, onConnectionEnded) {
1516
1591
  validateName(name);
1517
1592
  let set = this.observers.get(name);
1518
1593
  if (set === void 0) {
1519
1594
  set = /* @__PURE__ */ new Set();
1520
1595
  this.observers.set(name, set);
1521
1596
  }
1522
- set.add(observer);
1597
+ const registration = { consume: observer, onConnectionEnded };
1598
+ set.add(registration);
1523
1599
  queueMicrotask(() => {
1524
- if (this.observers.get(name)?.has(observer) === true) {
1525
- this.offerQueued(name, observer);
1600
+ if (this.observers.get(name)?.has(registration) === true) {
1601
+ this.offerQueued(name, registration);
1526
1602
  }
1527
1603
  });
1528
1604
  return subscription(() => {
1529
- set.delete(observer);
1605
+ set.delete(registration);
1530
1606
  if (set.size === 0 && this.observers.get(name) === set) {
1531
1607
  this.observers.delete(name);
1532
1608
  }
1533
1609
  });
1534
1610
  }
1611
+ /** Spec stream-connector 32 §10: arrivals under `name` on this connection. */
1612
+ receivedCount(name) {
1613
+ return this.receivedCounts.get(name) ?? 0;
1614
+ }
1615
+ /**
1616
+ * Rebaselines the queue for a connection that was just established. Spec
1617
+ * stream-connector 32 §10 (line ~649): the reference point is the moment a
1618
+ * connection is established, so counts restart at 0 and whatever the
1619
+ * previous connection left unconsumed goes with it — keeping the queue
1620
+ * while only the counts reset would let counts and queue describe two
1621
+ * different connections, and let `waitFor` hand back a packet from before
1622
+ * the drop as if the new connection had delivered it.
1623
+ *
1624
+ * `ZlinkStreamMessage`/`ZlinkStreamEncodedPayload` are plain data (name,
1625
+ * metadata, a `Uint8Array` payload) with no dispose/close of their own —
1626
+ * unlike Java's queued frames, which `closeMessage` releases — so dropping
1627
+ * the queue's references is the whole of the release here.
1628
+ *
1629
+ * @param replacesAnEarlierConnection False for the very first connection:
1630
+ * there is no earlier queue or wait to abandon yet. True for a reconnect,
1631
+ * which also fails every wait surface still registered from the
1632
+ * connection that just ended with `Disconnected` (Java
1633
+ * `ZLinkStreamDispatchQueue.resetForNewConnection` parity) — that
1634
+ * registration was watching a queue this call just discarded, so letting
1635
+ * it keep watching would silently rebind it to the new connection.
1636
+ */
1637
+ resetForNewConnection(replacesAnEarlierConnection) {
1638
+ this.receivedCounts.clear();
1639
+ this.queue.length = 0;
1640
+ this.queueHead = 0;
1641
+ this.queuedCount = 0;
1642
+ if (!replacesAnEarlierConnection || this.observers.size === 0) {
1643
+ return;
1644
+ }
1645
+ const abandoned = [...this.observers.values()].flatMap((set) => [...set]);
1646
+ this.observers.clear();
1647
+ for (const registration of abandoned) {
1648
+ registration.onConnectionEnded();
1649
+ }
1650
+ }
1535
1651
  enqueue(message, signal) {
1536
- for (const observer of [...this.observers.get(message.name) ?? []]) {
1537
- if (observer(message)) {
1652
+ this.receivedCounts.set(message.name, (this.receivedCounts.get(message.name) ?? 0) + 1);
1653
+ for (const registration of [...this.observers.get(message.name) ?? []]) {
1654
+ if (registration.consume(message)) {
1538
1655
  return;
1539
1656
  }
1540
1657
  }
@@ -1547,18 +1664,28 @@ var ZlinkStreamReceivedMessages = class {
1547
1664
  /**
1548
1665
  * Runs the registered handlers the receive path left queued. `Manual` calls
1549
1666
  * this from `dispatch`; `Immediate` has already drained on arrival.
1667
+ *
1668
+ * A handler that calls `dispatch` arrives back here from inside the drain it
1669
+ * was started by. `scheduleDrain` would find `drainTask` already set and
1670
+ * return, and the await below would then be the drain waiting on itself —
1671
+ * a deadlock with neither timeout nor error. The drain loop already takes
1672
+ * every message a handler exists for, so there is nothing a second drain
1673
+ * would deliver and returning is the whole of the correct behaviour.
1550
1674
  */
1551
1675
  async pump() {
1676
+ if (this.draining) {
1677
+ return;
1678
+ }
1552
1679
  this.scheduleDrain();
1553
1680
  await this.drainTask;
1554
1681
  }
1555
- offerQueued(name, observer) {
1682
+ offerQueued(name, registration) {
1556
1683
  for (let index = this.queueHead; index < this.queue.length; index += 1) {
1557
1684
  const queued = this.queue[index];
1558
1685
  if (queued === void 0 || queued.message.name !== name) {
1559
1686
  continue;
1560
1687
  }
1561
- if (!observer(queued.message)) {
1688
+ if (!registration.consume(queued.message)) {
1562
1689
  continue;
1563
1690
  }
1564
1691
  this.removeAt(index);
@@ -1577,23 +1704,28 @@ var ZlinkStreamReceivedMessages = class {
1577
1704
  });
1578
1705
  }
1579
1706
  async drain() {
1580
- for (let index = this.findDeliverableIndex(); index >= 0; index = this.findDeliverableIndex()) {
1581
- const queued = this.queue[index];
1582
- if (queued === void 0) continue;
1583
- this.removeAt(index);
1584
- const { message, signal } = queued;
1585
- const handlers = [...this.handlers.get(message.name)];
1586
- for (const handler of handlers) {
1587
- try {
1588
- await handler(message, signal);
1589
- } catch (cause) {
1590
- await this.events.publishError({
1591
- code: "userCallbackFailed" /* UserCallbackFailed */,
1592
- message: "Typed message handler failed.",
1593
- cause
1594
- }, signal);
1707
+ this.draining = true;
1708
+ try {
1709
+ for (let index = this.findDeliverableIndex(); index >= 0; index = this.findDeliverableIndex()) {
1710
+ const queued = this.queue[index];
1711
+ if (queued === void 0) continue;
1712
+ this.removeAt(index);
1713
+ const { message, signal } = queued;
1714
+ const handlers = [...this.handlers.get(message.name)];
1715
+ for (const handler of handlers) {
1716
+ try {
1717
+ await handler(message, signal);
1718
+ } catch (cause) {
1719
+ await this.events.publishError({
1720
+ code: "userCallbackFailed" /* UserCallbackFailed */,
1721
+ message: "Typed message handler failed.",
1722
+ cause
1723
+ }, signal);
1724
+ }
1595
1725
  }
1596
1726
  }
1727
+ } finally {
1728
+ this.draining = false;
1597
1729
  }
1598
1730
  }
1599
1731
  removeAt(index) {
@@ -1637,10 +1769,9 @@ var ZlinkStreamReceivedMessages = class {
1637
1769
 
1638
1770
  // packages/stream-connector/src/Runtime/ZlinkStreamFrameSender.ts
1639
1771
  var ZlinkStreamFrameSender = class {
1640
- constructor(protocol, flowContext, metrics) {
1772
+ constructor(protocol, flowContext) {
1641
1773
  this.protocol = protocol;
1642
1774
  this.flowContext = flowContext;
1643
- this.metrics = metrics;
1644
1775
  }
1645
1776
  pendingWrites = /* @__PURE__ */ new Set();
1646
1777
  async send(connection, kind, name, payload, metadata, compress, requestSeq, signal, correlationId, explicitFlow) {
@@ -1666,7 +1797,6 @@ var ZlinkStreamFrameSender = class {
1666
1797
  this.pendingWrites.add(write);
1667
1798
  try {
1668
1799
  await write;
1669
- this.metrics.outbound(frame.byteLength);
1670
1800
  } finally {
1671
1801
  this.pendingWrites.delete(write);
1672
1802
  }
@@ -1695,17 +1825,29 @@ function decodeSessionClosing(payload) {
1695
1825
 
1696
1826
  // packages/stream-connector/src/Runtime/ZlinkStreamReceiveDispatcher.ts
1697
1827
  var ZlinkStreamReceiveDispatcher = class {
1698
- constructor(protocol, pendingRequests, receivedMessages, frameSender, events, flowContext, metrics, serverClosing) {
1828
+ constructor(protocol, pendingRequests, receivedMessages, frameSender, events, flowContext, serverClosing) {
1699
1829
  this.protocol = protocol;
1700
1830
  this.pendingRequests = pendingRequests;
1701
1831
  this.receivedMessages = receivedMessages;
1702
1832
  this.frameSender = frameSender;
1703
1833
  this.events = events;
1704
1834
  this.flowContext = flowContext;
1705
- this.metrics = metrics;
1706
1835
  this.serverClosing = serverClosing;
1707
1836
  }
1708
- async readAndDispatch(connection, signal, isCurrent) {
1837
+ /**
1838
+ * @param isCurrent Tells whether the connection this batch was read from is
1839
+ * still the connector's connection. It is asked again at the head of every
1840
+ * frame, not only after the read: `dispatch` awaits application code —
1841
+ * registered handlers in `Immediate`, the error handler on a failed frame —
1842
+ * and a disconnect and reconnect can complete inside that await. Without
1843
+ * the recheck the rest of a dead connection's batch lands on the new
1844
+ * connection's state, and the §10 arrival counts, which start at 0 when a
1845
+ * connection is established, take the stale frames on top.
1846
+ * @param connectionForSend Resolves the connection a reply belongs on at the
1847
+ * moment it is written. The captured `connection` is the one the batch was
1848
+ * read from, which a reconnect may already have replaced.
1849
+ */
1850
+ async readAndDispatch(connection, signal, isCurrent, connectionForSend) {
1709
1851
  if (connection?.read === void 0) {
1710
1852
  return { available: false, inbound: false };
1711
1853
  }
@@ -1716,7 +1858,6 @@ var ZlinkStreamReceiveDispatcher = class {
1716
1858
  if (frameBytes === void 0) {
1717
1859
  return { available: false, inbound: false };
1718
1860
  }
1719
- this.metrics.inbound(frameBytes.byteLength);
1720
1861
  const flowEnabled = this.protocol.flowEnabled();
1721
1862
  let frames;
1722
1863
  try {
@@ -1729,8 +1870,11 @@ var ZlinkStreamReceiveDispatcher = class {
1729
1870
  return { available: true, inbound: false };
1730
1871
  }
1731
1872
  for (const frame of frames) {
1873
+ if (isCurrent !== void 0 && !isCurrent()) {
1874
+ break;
1875
+ }
1732
1876
  try {
1733
- await this.dispatch(connection, frame.header, frame.payload, signal, flowEnabled);
1877
+ await this.dispatch(connection, frame.header, frame.payload, signal, flowEnabled, connectionForSend);
1734
1878
  } catch (cause) {
1735
1879
  if (frame.header.kind === 5 /* Control */ && frame.header.name === ZLINK_STREAM_HEARTBEAT_PING) {
1736
1880
  throw cause;
@@ -1743,7 +1887,7 @@ var ZlinkStreamReceiveDispatcher = class {
1743
1887
  }
1744
1888
  return { available: true, inbound: true };
1745
1889
  }
1746
- async dispatch(connection, header, payload, signal, flowEnabled) {
1890
+ async dispatch(connection, header, payload, signal, flowEnabled, connectionForSend) {
1747
1891
  if (header.kind === 3 /* Response */ && header.requestSeq !== void 0) {
1748
1892
  try {
1749
1893
  if (!this.pendingRequests.resolve(header.requestSeq, {
@@ -1796,7 +1940,7 @@ var ZlinkStreamReceiveDispatcher = class {
1796
1940
  return;
1797
1941
  }
1798
1942
  if (header.kind === 5 /* Control */) {
1799
- await this.dispatchControl(connection, header, payload, signal);
1943
+ await this.dispatchControl(connection, header, payload, signal, connectionForSend);
1800
1944
  return;
1801
1945
  }
1802
1946
  if (header.kind === 1 /* Send */) {
@@ -1810,7 +1954,7 @@ var ZlinkStreamReceiveDispatcher = class {
1810
1954
  }, signal);
1811
1955
  }
1812
1956
  }
1813
- async dispatchControl(connection, header, payload, signal) {
1957
+ async dispatchControl(connection, header, payload, signal, connectionForSend) {
1814
1958
  if (header.name === ZLINK_SESSION_CLOSING) {
1815
1959
  const closing = decodeSessionClosing(payload);
1816
1960
  await this.serverClosing?.(closing.closeReason);
@@ -1821,7 +1965,11 @@ var ZlinkStreamReceiveDispatcher = class {
1821
1965
  }
1822
1966
  if (header.name === ZLINK_STREAM_HEARTBEAT_PING) {
1823
1967
  try {
1824
- await this.frameSender.sendControl(connection, ZLINK_STREAM_HEARTBEAT_PONG, signal);
1968
+ await this.frameSender.sendControl(
1969
+ connectionForSend?.() ?? connection,
1970
+ ZLINK_STREAM_HEARTBEAT_PONG,
1971
+ signal
1972
+ );
1825
1973
  } catch (cause) {
1826
1974
  throw connectorError(
1827
1975
  "sendFailed" /* SendFailed */,
@@ -1854,15 +2002,17 @@ function decodeRemoteError(protocol, header, payload) {
1854
2002
  }
1855
2003
 
1856
2004
  // packages/stream-connector/src/Runtime/ZlinkStreamConnectorLifecycle.ts
2005
+ function randomizedDelay(baseDelayMs) {
2006
+ return Math.round(baseDelayMs * (0.5 + Math.random() * 0.5));
2007
+ }
1857
2008
  var ZlinkStreamConnectorLifecycle = class {
1858
- constructor(options, pendingRequests, frameSender, receiveDispatcher, receivedMessages, events, metrics) {
2009
+ constructor(options, pendingRequests, frameSender, receiveDispatcher, receivedMessages, events) {
1859
2010
  this.options = options;
1860
2011
  this.pendingRequests = pendingRequests;
1861
2012
  this.frameSender = frameSender;
1862
2013
  this.receiveDispatcher = receiveDispatcher;
1863
2014
  this.receivedMessages = receivedMessages;
1864
2015
  this.events = events;
1865
- this.metrics = metrics;
1866
2016
  }
1867
2017
  receiveLoopAbort;
1868
2018
  receiveLoopSleeping = false;
@@ -1872,6 +2022,7 @@ var ZlinkStreamConnectorLifecycle = class {
1872
2022
  connectionGeneration = 0;
1873
2023
  currentState = "created" /* Created */;
1874
2024
  heartbeatTimer;
2025
+ heartbeatTickRunning = false;
1875
2026
  lastInboundAt = 0;
1876
2027
  closeTask;
1877
2028
  connectTask;
@@ -1922,6 +2073,7 @@ var ZlinkStreamConnectorLifecycle = class {
1922
2073
  this.currentConnection = connection;
1923
2074
  this.connectionGeneration += 1;
1924
2075
  this.disconnectedPublished = false;
2076
+ this.receivedMessages.resetForNewConnection(this.connectionGeneration > 1);
1925
2077
  this.lastInboundAt = Date.now();
1926
2078
  await this.setState("connected" /* Connected */, void 0, signal);
1927
2079
  this.startHeartbeat();
@@ -1933,7 +2085,9 @@ var ZlinkStreamConnectorLifecycle = class {
1933
2085
  throw new ZlinkStreamException(error2);
1934
2086
  }
1935
2087
  const error = toStreamError(cause, "connectTimeout" /* ConnectTimeout */, "Connect failed.");
2088
+ this.closeReasonValue ??= "TransportError";
1936
2089
  await this.setState("disconnected" /* Disconnected */, error, signal);
2090
+ await this.publishDisconnectedOnce(signal);
1937
2091
  throw new ZlinkStreamException(error);
1938
2092
  }
1939
2093
  }
@@ -1980,7 +2134,7 @@ var ZlinkStreamConnectorLifecycle = class {
1980
2134
  }
1981
2135
  this.pendingRequests.failAll({ code: "disconnected" /* Disconnected */, message: "Connector closed." });
1982
2136
  await this.setState("closed" /* Closed */, void 0, signal);
1983
- await this.publishDisconnectedOnce(signal);
2137
+ this.publishDisconnectedWithoutWaiting(signal);
1984
2138
  if (errors.length === 1) throw errors[0];
1985
2139
  if (errors.length > 1) throw new AggregateError(errors, "Stream connector close failed.");
1986
2140
  }
@@ -2011,7 +2165,8 @@ var ZlinkStreamConnectorLifecycle = class {
2011
2165
  const result = await this.receiveDispatcher.readAndDispatch(
2012
2166
  connection,
2013
2167
  signal,
2014
- () => this.isCurrentConnection(connection, generation)
2168
+ () => this.isCurrentConnection(connection, generation),
2169
+ () => this.connectionForSend()
2015
2170
  );
2016
2171
  if (result.inbound && this.isCurrentConnection(connection, generation)) {
2017
2172
  this.lastInboundAt = Date.now();
@@ -2023,25 +2178,18 @@ var ZlinkStreamConnectorLifecycle = class {
2023
2178
  let delayMs = this.options.reconnect.initialDelayMs;
2024
2179
  let lastError;
2025
2180
  const maxAttempts = this.options.reconnect.enabled ? this.options.reconnect.maxAttempts : 1;
2026
- while (attempt < maxAttempts) {
2181
+ const unlimited = maxAttempts === null;
2182
+ while (unlimited || attempt < maxAttempts) {
2027
2183
  attempt += 1;
2028
- if (attempt > 1) {
2029
- this.metrics.reconnect();
2030
- }
2031
- const handshakeStartedAt = performance.now();
2032
2184
  try {
2033
- const connection = await this.options.transportFactory.connect(this.options, signal);
2034
- this.metrics.handshakeCompleted(handshakeStartedAt);
2035
- return connection;
2185
+ return await this.options.transportFactory.connect(this.options, signal);
2036
2186
  } catch (cause) {
2037
- this.metrics.handshakeCompleted(handshakeStartedAt);
2038
- this.metrics.handshakeFailed(cause);
2039
2187
  lastError = toStreamError(cause, "connectTimeout" /* ConnectTimeout */, "Connect failed.");
2040
- if (!this.options.reconnect.enabled || attempt >= maxAttempts) {
2188
+ if (!this.options.reconnect.enabled || !unlimited && attempt >= maxAttempts) {
2041
2189
  break;
2042
2190
  }
2043
2191
  await this.setState("reconnecting" /* Reconnecting */, lastError, signal);
2044
- await delay(delayMs, signal);
2192
+ await delay(randomizedDelay(delayMs), signal);
2045
2193
  delayMs = Math.min(
2046
2194
  this.options.reconnect.maxDelayMs,
2047
2195
  Math.ceil(delayMs * this.options.reconnect.backoffFactor)
@@ -2057,15 +2205,25 @@ var ZlinkStreamConnectorLifecycle = class {
2057
2205
  if (!this.options.heartbeat.enabled) {
2058
2206
  return;
2059
2207
  }
2060
- this.heartbeatTimer = setInterval(() => {
2061
- void this.runHeartbeatTick();
2208
+ const timer = setInterval(() => {
2209
+ if (this.heartbeatTickRunning) {
2210
+ return;
2211
+ }
2212
+ this.heartbeatTickRunning = true;
2213
+ void this.runHeartbeatTick().finally(() => {
2214
+ if (this.heartbeatTimer === timer) {
2215
+ this.heartbeatTickRunning = false;
2216
+ }
2217
+ });
2062
2218
  }, this.options.heartbeat.intervalMs);
2219
+ this.heartbeatTimer = timer;
2063
2220
  }
2064
2221
  stopHeartbeat() {
2065
2222
  if (this.heartbeatTimer !== void 0) {
2066
2223
  clearInterval(this.heartbeatTimer);
2067
2224
  this.heartbeatTimer = void 0;
2068
2225
  }
2226
+ this.heartbeatTickRunning = false;
2069
2227
  }
2070
2228
  // Spec stream-connector 32 §7: the receive loop runs in both dispatch modes.
2071
2229
  // `Manual` only changes what the loop does with a frame — it queues the
@@ -2180,15 +2338,17 @@ var ZlinkStreamConnectorLifecycle = class {
2180
2338
  if (this.disconnectTask !== void 0) {
2181
2339
  return await this.disconnectTask;
2182
2340
  }
2183
- this.disconnectTask = this.disconnectOnce(error).finally(() => {
2341
+ this.disconnectTask = this.tearDownConnection(error).finally(() => {
2184
2342
  this.disconnectTask = void 0;
2185
2343
  });
2186
- return await this.disconnectTask;
2344
+ await this.disconnectTask;
2345
+ await this.announceDisconnect(error);
2187
2346
  }
2188
2347
  isCurrentConnection(connection, generation) {
2189
2348
  return !this.closeRequested && this.currentConnection === connection && this.connectionGeneration === generation;
2190
2349
  }
2191
- async disconnectOnce(error) {
2350
+ /** Transport teardown only — no application callback runs from here. */
2351
+ async tearDownConnection(error) {
2192
2352
  this.stopHeartbeat();
2193
2353
  this.stopReceiveLoop();
2194
2354
  const connection = this.currentConnection;
@@ -2198,23 +2358,75 @@ var ZlinkStreamConnectorLifecycle = class {
2198
2358
  await connection?.close();
2199
2359
  } catch {
2200
2360
  }
2361
+ }
2362
+ /**
2363
+ * Runs once the teardown promise has settled, so a handler reached from here
2364
+ * may call `connect` without waiting for a task its own caller still holds.
2365
+ * The reconnect is queued before the notification is awaited for the same
2366
+ * reason: spec stream-connector 32 §6 has reconnect on by default, and a
2367
+ * handler that is slow — or whose promise never settles at all — must not
2368
+ * cost the connector the attempt. The state is already `Disconnected` and the
2369
+ * state handlers are already invoked by the time the queued microtask runs,
2370
+ * because `setState` records the state and hands the change to the handlers
2371
+ * before it awaits any of them.
2372
+ */
2373
+ async announceDisconnect(error) {
2201
2374
  if (this.closeRequested) return;
2202
- await this.setState("disconnected" /* Disconnected */, error);
2203
- await this.publishDisconnectedOnce();
2375
+ const announce = this.claimDisconnectedPublish();
2376
+ const notified = (async () => {
2377
+ await this.setState("disconnected" /* Disconnected */, error);
2378
+ if (announce) {
2379
+ await this.events.publishDisconnected();
2380
+ }
2381
+ })();
2204
2382
  if (this.shouldReconnect()) {
2205
2383
  queueMicrotask(() => {
2206
2384
  void this.connect().catch(() => void 0);
2207
2385
  });
2208
2386
  }
2387
+ await notified;
2209
2388
  }
2210
2389
  shouldReconnect() {
2211
2390
  return this.options.reconnect.enabled && !this.closeRequested;
2212
2391
  }
2213
- async publishDisconnectedOnce(signal) {
2214
- if (this.disconnectedPublished) return;
2392
+ // Synchronous test-and-set, taken before any await, so the single disconnect
2393
+ // notification the spec promises is claimed by exactly one caller even when
2394
+ // the publishing itself is deferred.
2395
+ claimDisconnectedPublish() {
2396
+ if (this.disconnectedPublished) return false;
2215
2397
  this.disconnectedPublished = true;
2398
+ return true;
2399
+ }
2400
+ async publishDisconnectedOnce(signal) {
2401
+ if (!this.claimDisconnectedPublish()) return;
2216
2402
  await this.events.publishDisconnected(signal);
2217
2403
  }
2404
+ /**
2405
+ * Spec stream-connector 32 §7: the connector runs the registered handler, it
2406
+ * does not wait for it to finish. `close` returns once its own work is done —
2407
+ * the drain, the transport close, the pending requests — having run the
2408
+ * disconnect handler but without looking at whether that handler has ended.
2409
+ *
2410
+ * Starting the publish without awaiting it still runs every handler right
2411
+ * here: an `async` function body runs synchronously up to its first `await`,
2412
+ * and `publishDisconnected` reaches each handler before that point. So the
2413
+ * handler has run by the time `close` returns, which is what the spec asks
2414
+ * for, while a handler that calls `close` no longer waits for the very
2415
+ * `closeTask` it is running inside. Java and C++ hand the handler to a queue
2416
+ * and return the same way.
2417
+ *
2418
+ * `claimDisconnectedPublish` is taken by the same call and before any await,
2419
+ * so deferring the completion never turns the one notification into two.
2420
+ *
2421
+ * Nothing awaits the promise, so a rejection would reach the process as an
2422
+ * unhandled rejection and kill it. Handler failures are already contained
2423
+ * where they were before this call stopped waiting — the `Promise.allSettled`
2424
+ * in `publishDisconnected` — and this `catch` covers what is left rather than
2425
+ * reporting the same failure a second time on the error surface.
2426
+ */
2427
+ publishDisconnectedWithoutWaiting(signal) {
2428
+ void this.publishDisconnectedOnce(signal).catch(() => void 0);
2429
+ }
2218
2430
  async setState(current, error, signal) {
2219
2431
  const previous = this.currentState;
2220
2432
  this.currentState = current;
@@ -2396,9 +2608,6 @@ var BrowserWebSocketConnection = class {
2396
2608
  this.readWaiter = void 0;
2397
2609
  resolve();
2398
2610
  };
2399
- if (this.hasQueuedMessage() || this.closed) {
2400
- this.wakeReader();
2401
- }
2402
2611
  });
2403
2612
  }
2404
2613
  wakeReader() {
@@ -2478,50 +2687,6 @@ function toUint8Array(data) {
2478
2687
  throw connectorError("frameDecodeFailed" /* FrameDecodeFailed */, "WebSocket text messages are not supported.");
2479
2688
  }
2480
2689
 
2481
- // packages/stream-connector/src/Runtime/ZlinkStreamRuntimeMetrics.ts
2482
- var ZlinkStreamRuntimeMetrics = class {
2483
- constructor(options) {
2484
- this.options = options;
2485
- const meter = options.meterProvider?.getMeter("zlink.framework");
2486
- this.reconnects = meter?.createCounter("zlink.stream.reconnects", { unit: "{event}" });
2487
- this.handshakeDuration = meter?.createHistogram("zlink.stream.handshake.duration", { unit: "s" });
2488
- this.handshakeFailures = meter?.createCounter("zlink.stream.handshake.failures", { unit: "{failure}" });
2489
- this.inboundBytes = meter?.createCounter("zlink.stream.inbound.bytes", { unit: "By" });
2490
- this.outboundBytes = meter?.createCounter("zlink.stream.outbound.bytes", { unit: "By" });
2491
- }
2492
- reconnects;
2493
- handshakeDuration;
2494
- handshakeFailures;
2495
- inboundBytes;
2496
- outboundBytes;
2497
- reconnect() {
2498
- this.safe(() => this.reconnects?.add(1, { transport: this.transportLabel() }));
2499
- }
2500
- handshakeCompleted(startedAt) {
2501
- const seconds = (performance.now() - startedAt) / 1e3;
2502
- this.safe(() => this.handshakeDuration?.record(seconds, { transport: this.transportLabel() }));
2503
- }
2504
- handshakeFailed(error) {
2505
- const reason = error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError") ? "canceled" : "transport_error";
2506
- this.safe(() => this.handshakeFailures?.add(1, { transport: this.transportLabel(), reason }));
2507
- }
2508
- inbound(byteCount) {
2509
- this.safe(() => this.inboundBytes?.add(byteCount, { transport: this.transportLabel() }));
2510
- }
2511
- outbound(byteCount) {
2512
- this.safe(() => this.outboundBytes?.add(byteCount, { transport: this.transportLabel() }));
2513
- }
2514
- transportLabel() {
2515
- return this.options.endpoint.startsWith("wss:") ? "wss" : "ws";
2516
- }
2517
- safe(record) {
2518
- try {
2519
- record();
2520
- } catch {
2521
- }
2522
- }
2523
- };
2524
-
2525
2690
  // packages/stream-connector/src/Runtime/ZlinkStreamConnector.ts
2526
2691
  var DefaultZlinkStreamConnector = class {
2527
2692
  static heartbeatPingName = ZLINK_STREAM_HEARTBEAT_PING;
@@ -2544,9 +2709,8 @@ var DefaultZlinkStreamConnector = class {
2544
2709
  configurable: true,
2545
2710
  get: () => this.diagnosticsLevelCell.level
2546
2711
  });
2547
- const metrics = new ZlinkStreamRuntimeMetrics(this.options);
2548
2712
  const protocol = new ZlinkStreamFrameProtocol(this.options);
2549
- this.frameSender = new ZlinkStreamFrameSender(protocol, flowContext, metrics);
2713
+ this.frameSender = new ZlinkStreamFrameSender(protocol, flowContext);
2550
2714
  this.receivedMessages = new ZlinkStreamReceivedMessages(
2551
2715
  this.events,
2552
2716
  this.options.dispatchMode === "immediate" /* Immediate */
@@ -2558,7 +2722,6 @@ var DefaultZlinkStreamConnector = class {
2558
2722
  this.frameSender,
2559
2723
  this.events,
2560
2724
  flowContext,
2561
- metrics,
2562
2725
  (reason) => this.lifecycle.serverClosing(reason)
2563
2726
  );
2564
2727
  this.lifecycle = new ZlinkStreamConnectorLifecycle(
@@ -2567,8 +2730,7 @@ var DefaultZlinkStreamConnector = class {
2567
2730
  this.frameSender,
2568
2731
  this.receiveDispatcher,
2569
2732
  this.receivedMessages,
2570
- this.events,
2571
- metrics
2733
+ this.events
2572
2734
  );
2573
2735
  }
2574
2736
  get isConnected() {
@@ -2583,6 +2745,17 @@ var DefaultZlinkStreamConnector = class {
2583
2745
  get pendingDispatchCount() {
2584
2746
  return this.pendingRequests.count;
2585
2747
  }
2748
+ /**
2749
+ * Spec stream-connector 32 §10: how many packets carrying `name` arrived on
2750
+ * the current connection. Arrivals are what is counted, so a message a
2751
+ * handler dispatched or a wait surface consumed still counts, and `Manual`
2752
+ * and `Immediate` report the same number. Establishing a connection resets
2753
+ * the count to 0, a reconnect included.
2754
+ */
2755
+ receivedCount(name) {
2756
+ validateName(name);
2757
+ return this.receivedMessages.receivedCount(name);
2758
+ }
2586
2759
  /**
2587
2760
  * Current diagnostics level (spec 26 §4.1, spec stream-connector 32 §13).
2588
2761
  * Reflects the level set by the most recent {@link setDiagnosticsLevel}
@@ -2598,14 +2771,20 @@ var DefaultZlinkStreamConnector = class {
2598
2771
  * state update: it applies to processing points that read the level after
2599
2772
  * this call returns and is never applied retroactively to frames already
2600
2773
  * built. Rejects unknown values with {@link ZlinkStreamErrorCode.ConfigurationError}.
2601
- * Do not call this synchronous bridge from a framework execution context such
2602
- * as a handler or callback; use setDiagnosticsLevelAsync there.
2774
+ * Spec stream-connector 32 §13: this surface changes the value without
2775
+ * waiting for anything; it is not a blocking call over the asynchronous pair,
2776
+ * which a receive callback would otherwise make wait for its own completion.
2603
2777
  */
2604
2778
  setDiagnosticsLevel(level) {
2605
- void this.setDiagnosticsLevelAsync(level);
2779
+ this.diagnosticsLevelCell.set(level);
2606
2780
  }
2781
+ /**
2782
+ * Asynchronous counterpart of {@link setDiagnosticsLevel} (spec
2783
+ * stream-connector 32 §13). It changes the same value; awaiting it is how a
2784
+ * caller observes the change, and it never replaces the synchronous surface.
2785
+ */
2607
2786
  setDiagnosticsLevelAsync(level) {
2608
- this.diagnosticsLevelCell.set(level);
2787
+ this.setDiagnosticsLevel(level);
2609
2788
  return Promise.resolve();
2610
2789
  }
2611
2790
  onErrorReceived(handler) {
@@ -2644,17 +2823,28 @@ var DefaultZlinkStreamConnector = class {
2644
2823
  }, signal);
2645
2824
  return this.receivedMessages.on(name, encodedHandler);
2646
2825
  }
2647
- waitFor(name) {
2648
- validateName(name);
2649
- return new ZlinkStreamWaitBuilder(this, name);
2826
+ waitFor(nameOrType) {
2827
+ return new ZlinkStreamWaitBuilder(this, this.observedName(nameOrType));
2650
2828
  }
2651
- expectNone(name) {
2652
- validateName(name);
2653
- return new ZlinkStreamExpectNoneBuilder(this, name);
2829
+ expectNone(nameOrType) {
2830
+ return new ZlinkStreamExpectNoneBuilder(this, this.observedName(nameOrType));
2831
+ }
2832
+ waitForSequence(nameOrType) {
2833
+ return new ZlinkStreamSequenceBuilder(this, this.observedName(nameOrType));
2654
2834
  }
2655
- waitForSequence(name) {
2835
+ /**
2836
+ * Spec stream-connector 32 §10.1: each wait surface offers both ways of
2837
+ * naming a packet. A string is the name the caller states; a constructor
2838
+ * goes through the options' `nameResolver`, which is the same resolver
2839
+ * `send` and `request` use, so both paths land on one name for one type.
2840
+ */
2841
+ observedName(nameOrType) {
2842
+ const name = typeof nameOrType === "function" ? this.options.nameResolver.resolve(nameOrType) : nameOrType;
2843
+ if (typeof name !== "string") {
2844
+ throw connectorError("validationFailed" /* ValidationFailed */, "Packet name must be a string or a payload constructor.");
2845
+ }
2656
2846
  validateName(name);
2657
- return new ZlinkStreamSequenceBuilder(this, name);
2847
+ return name;
2658
2848
  }
2659
2849
  waitForMessage(name, timeoutMs, predicate, signal) {
2660
2850
  validateName(name);
@@ -2707,7 +2897,10 @@ var DefaultZlinkStreamConnector = class {
2707
2897
  finish(cause);
2708
2898
  }
2709
2899
  return true;
2710
- });
2900
+ }, () => finish(connectorError(
2901
+ "disconnected" /* Disconnected */,
2902
+ `The connection this wait for '${name}' observed has ended.`
2903
+ )));
2711
2904
  });
2712
2905
  }
2713
2906
  encodePayload(payload, messageType) {
@@ -2786,50 +2979,6 @@ function isEncodedPayload(value) {
2786
2979
  return typeof candidate.codec === "number" && candidate.payload instanceof Uint8Array;
2787
2980
  }
2788
2981
 
2789
- // packages/stream-connector/src/Runtime/ZlinkStreamAssertions.ts
2790
- var zlinkStreamAssert = {
2791
- ensure(condition, message) {
2792
- if (!condition) {
2793
- throw connectorError("validationFailed" /* ValidationFailed */, message);
2794
- }
2795
- },
2796
- async expectFailure(action, errorKind) {
2797
- let failure;
2798
- try {
2799
- await action();
2800
- } catch (error) {
2801
- failure = error;
2802
- }
2803
- if (failure === void 0) {
2804
- throw connectorError("validationFailed" /* ValidationFailed */, "Expected action to fail.");
2805
- }
2806
- const streamError = unwrapStreamError(failure);
2807
- if (errorKind !== void 0 && streamError.code !== errorKind) {
2808
- throw connectorError(
2809
- "validationFailed" /* ValidationFailed */,
2810
- `Expected failure kind '${errorKind}', got '${streamError.code}'.`,
2811
- failure
2812
- );
2813
- }
2814
- return streamError;
2815
- },
2816
- async expectTimeout(action) {
2817
- let failure;
2818
- try {
2819
- await action();
2820
- } catch (error) {
2821
- failure = error;
2822
- }
2823
- if (failure === void 0) {
2824
- throw connectorError("validationFailed" /* ValidationFailed */, "Expected action to time out.");
2825
- }
2826
- const code = unwrapStreamError(failure).code;
2827
- if (code !== "requestTimeout" /* RequestTimeout */ && code !== "connectTimeout" /* ConnectTimeout */) {
2828
- throw failure;
2829
- }
2830
- }
2831
- };
2832
-
2833
2982
  // packages/stream-connector/src/index.ts
2834
2983
  var zlinkStreamConnectorFactory = {
2835
2984
  create(options) {
@@ -2837,7 +2986,6 @@ var zlinkStreamConnectorFactory = {
2837
2986
  }
2838
2987
  };
2839
2988
  export {
2840
- DefaultZlinkStreamConnector,
2841
2989
  ZlinkStreamCodec,
2842
2990
  ZlinkStreamCompression,
2843
2991
  ZlinkStreamConnectionState,