@zlink-systems/stream-connector 0.25.0 → 0.26.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.
@@ -9,11 +9,7 @@ function lz4PickleUncompressed(payload) {
9
9
  pickled.set(payload, 1);
10
10
  return pickled;
11
11
  }
12
- function lz4UnpicklePayload(payload, maxSize) {
13
- const maxDecompressedSize = maxSize ?? defaultMaxDecompressedPayloadSize;
14
- if (payload.length === 0) {
15
- return new Uint8Array();
16
- }
12
+ function readLz4Pickle(payload) {
17
13
  const header = payload[0];
18
14
  if ((header & 7) !== 0) {
19
15
  throw new Error("Unexpected LZ4 pickle version.");
@@ -25,7 +21,17 @@ function lz4UnpicklePayload(payload, maxSize) {
25
21
  }
26
22
  const data = payload.subarray(dataOffset);
27
23
  const resultDiff = sizeOfDiff === 0 ? 0 : readLittleEndian(payload, 1, sizeOfDiff);
28
- const resultLength = data.length + resultDiff;
24
+ return { data, resultDiff, resultLength: data.length + resultDiff };
25
+ }
26
+ function lz4PickledLength(payload) {
27
+ return payload.length === 0 ? 0 : readLz4Pickle(payload).resultLength;
28
+ }
29
+ function lz4UnpicklePayload(payload, maxSize) {
30
+ const maxDecompressedSize = maxSize ?? defaultMaxDecompressedPayloadSize;
31
+ if (payload.length === 0) {
32
+ return new Uint8Array();
33
+ }
34
+ const { data, resultDiff, resultLength } = readLz4Pickle(payload);
29
35
  if (resultLength > maxDecompressedSize) {
30
36
  throw new Error("LZ4 decoded payload exceeds maximum stream payload size.");
31
37
  }
@@ -376,6 +382,9 @@ function decodeStreamWireMetadata(metadata) {
376
382
  function lz4PickleUncompressed2(payload) {
377
383
  return lz4PickleUncompressed(payload);
378
384
  }
385
+ function lz4PickledLength2(payload) {
386
+ return lz4PickledLength(payload);
387
+ }
379
388
  function lz4UnpicklePayload2(payload, maxDecompressedSize) {
380
389
  return lz4UnpicklePayload(payload, maxDecompressedSize ?? defaultMaxDecompressedPayloadSize);
381
390
  }
@@ -817,9 +826,10 @@ var ZlinkStreamSendBuilder = class {
817
826
  }
818
827
  };
819
828
  var ZlinkStreamRequestBuilder = class {
820
- constructor(connector, name, payload, actorSlot, validateActor) {
829
+ constructor(connector, name, payload, enqueueCallback, actorSlot, validateActor) {
821
830
  this.connector = connector;
822
831
  this.payload = payload;
832
+ this.enqueueCallback = enqueueCallback;
823
833
  this.state = new ZlinkStreamCallBuilderState(name, actorSlot, validateActor);
824
834
  }
825
835
  state;
@@ -853,8 +863,10 @@ var ZlinkStreamRequestBuilder = class {
853
863
  );
854
864
  if (typeof signalOrCallback === "function") {
855
865
  operation.then(
856
- (value) => signalOrCallback({ isSuccess: true, value }),
857
- (error) => signalOrCallback({ isSuccess: false, error: unwrapStreamError(error) })
866
+ (value) => this.enqueueCallback(() => signalOrCallback({ isSuccess: true, value })),
867
+ (error) => this.enqueueCallback(
868
+ () => signalOrCallback({ isSuccess: false, error: unwrapStreamError(error) })
869
+ )
858
870
  );
859
871
  return;
860
872
  }
@@ -1059,6 +1071,12 @@ var zlinkStreamLz4CompressionCodec = {
1059
1071
  return lz4PickleUncompressed2(payload);
1060
1072
  },
1061
1073
  decompress(payload, maxDecompressedSize) {
1074
+ if (lz4PickledLength2(payload) > maxDecompressedSize) {
1075
+ throw connectorError(
1076
+ "frameTooLarge" /* FrameTooLarge */,
1077
+ "LZ4 decoded payload exceeds MaxReceivePayloadSize."
1078
+ );
1079
+ }
1062
1080
  return lz4UnpicklePayload2(payload, maxDecompressedSize);
1063
1081
  }
1064
1082
  };
@@ -1070,7 +1088,11 @@ function compressPayload(payload, compression, compressionCodec) {
1070
1088
  "Compression codec is not configured."
1071
1089
  );
1072
1090
  }
1073
- return codec.compress(payload);
1091
+ try {
1092
+ return codec.compress(payload);
1093
+ } catch (cause) {
1094
+ throw connectorError("compressionFailed" /* CompressionFailed */, "Compression failed.", cause);
1095
+ }
1074
1096
  }
1075
1097
  function decompressIfNeeded(header, payload, compression, compressionCodec, maxDecompressedSize) {
1076
1098
  if ((header.flags & 4 /* PayloadCompressed */) === 0) {
@@ -1083,15 +1105,20 @@ function decompressIfNeeded(header, payload, compression, compressionCodec, maxD
1083
1105
  "Compression codec is not configured."
1084
1106
  );
1085
1107
  }
1108
+ let decompressed;
1086
1109
  try {
1087
- const decompressed = codec.decompress(payload, maxDecompressedSize);
1088
- if (decompressed.length > maxDecompressedSize) {
1089
- throw new Error("Decoded payload exceeds MaxReceivePayloadSize.");
1090
- }
1091
- return decompressed;
1110
+ decompressed = codec.decompress(payload, maxDecompressedSize);
1092
1111
  } catch (cause) {
1112
+ if (cause instanceof ZlinkStreamException) throw cause;
1093
1113
  throw connectorError("decompressionFailed" /* DecompressionFailed */, "Decompression failed.", cause);
1094
1114
  }
1115
+ if (decompressed.length > maxDecompressedSize) {
1116
+ throw connectorError(
1117
+ "frameTooLarge" /* FrameTooLarge */,
1118
+ "Decompressed payload exceeds MaxReceivePayloadSize."
1119
+ );
1120
+ }
1121
+ return decompressed;
1095
1122
  }
1096
1123
  function resolveCompressionCodec(compression, compressionCodec) {
1097
1124
  if (compression === "none" /* None */) {
@@ -1459,6 +1486,40 @@ function normalizeOptions(options, defaultTransportFactory) {
1459
1486
  validatePositive(options.maxReceivePayloadSize ?? 64 * 1024, "MaxReceivePayloadSize");
1460
1487
  validateHeartbeat(options.heartbeat);
1461
1488
  validateReconnect(options.reconnect);
1489
+ if (options.heartbeat?.enabled !== void 0 && typeof options.heartbeat.enabled !== "boolean") {
1490
+ throw connectorError(
1491
+ "validationFailed" /* ValidationFailed */,
1492
+ "Heartbeat enabled must be boolean."
1493
+ );
1494
+ }
1495
+ if (options.reconnect?.enabled !== void 0 && typeof options.reconnect.enabled !== "boolean") {
1496
+ throw connectorError(
1497
+ "validationFailed" /* ValidationFailed */,
1498
+ "Reconnect enabled must be boolean."
1499
+ );
1500
+ }
1501
+ if (options.compressionCodec !== void 0 && !hasMethods(options.compressionCodec, "compress", "decompress")) {
1502
+ throw connectorError("validationFailed" /* ValidationFailed */, "Compression codec is invalid.");
1503
+ }
1504
+ if (options.codec !== void 0 && !hasMethods(options.codec, "encode", "decode")) {
1505
+ throw connectorError("validationFailed" /* ValidationFailed */, "Payload codec is invalid.");
1506
+ }
1507
+ if (options.nameResolver !== void 0 && !hasMethods(options.nameResolver, "resolve")) {
1508
+ throw connectorError("validationFailed" /* ValidationFailed */, "Packet name resolver is invalid.");
1509
+ }
1510
+ if (options.transportFactory !== void 0 && !hasMethods(options.transportFactory, "connect")) {
1511
+ throw connectorError("validationFailed" /* ValidationFailed */, "Transport factory is invalid.");
1512
+ }
1513
+ if (!Object.values(ZlinkStreamDispatchMode).includes(
1514
+ options.dispatchMode ?? "manual" /* Manual */
1515
+ )) {
1516
+ throw connectorError("validationFailed" /* ValidationFailed */, "DispatchMode is invalid.");
1517
+ }
1518
+ if (!Object.values(ZlinkStreamCompression).includes(
1519
+ options.compression ?? "lz4" /* Lz4 */
1520
+ )) {
1521
+ throw connectorError("validationFailed" /* ValidationFailed */, "Compression is invalid.");
1522
+ }
1462
1523
  return {
1463
1524
  endpoint,
1464
1525
  transport: inferredTransport,
@@ -1510,39 +1571,25 @@ function resolveCompressionCodec2(options) {
1510
1571
  return options.compressionCodec;
1511
1572
  }
1512
1573
  function validatePositive(value, name) {
1513
- if (value <= 0) {
1574
+ if (!Number.isFinite(value) || value <= 0) {
1514
1575
  throw connectorError("validationFailed" /* ValidationFailed */, `${name} must be positive.`);
1515
1576
  }
1516
1577
  }
1578
+ function hasMethods(value, ...names) {
1579
+ if (value === null || typeof value !== "object") return false;
1580
+ const candidate = value;
1581
+ return names.every((name) => typeof candidate[name] === "function");
1582
+ }
1517
1583
  function validateHeartbeat(options) {
1518
- const enabled = options?.enabled ?? true;
1519
1584
  const intervalMs = options?.intervalMs ?? 1e3;
1520
1585
  const timeoutMs = options?.timeoutMs ?? 5e3;
1521
- if (!enabled) {
1522
- return;
1523
- }
1524
1586
  validatePositive(intervalMs, "Heartbeat interval");
1525
1587
  validatePositive(timeoutMs, "Heartbeat timeout");
1526
- if (timeoutMs <= intervalMs) {
1527
- throw connectorError(
1528
- "validationFailed" /* ValidationFailed */,
1529
- "Heartbeat timeout must be greater than the heartbeat interval."
1530
- );
1531
- }
1532
1588
  }
1533
1589
  function validateReconnect(options) {
1534
- const enabled = options?.enabled ?? true;
1535
- if (!enabled) {
1536
- return;
1537
- }
1538
1590
  validatePositive(options?.initialDelayMs ?? 250, "Reconnect InitialDelay");
1539
1591
  validatePositive(options?.maxDelayMs ?? 5e3, "Reconnect MaxDelay");
1540
- if ((options?.backoffFactor ?? 2) < 1) {
1541
- throw connectorError(
1542
- "validationFailed" /* ValidationFailed */,
1543
- "Reconnect BackoffFactor must be at least 1.0."
1544
- );
1545
- }
1592
+ validatePositive(options?.backoffFactor ?? 2, "Reconnect BackoffFactor");
1546
1593
  const maxAttempts = options?.maxAttempts === void 0 ? 3 : options.maxAttempts;
1547
1594
  if (maxAttempts !== null && !(Number.isFinite(maxAttempts) && maxAttempts > 0)) {
1548
1595
  throw connectorError(
@@ -1556,32 +1603,33 @@ function validateReconnect(options) {
1556
1603
  var ZlinkStreamPendingRequests = class {
1557
1604
  nextRequestSeq = 1n;
1558
1605
  active = /* @__PURE__ */ new Map();
1559
- get count() {
1560
- return this.active.size;
1561
- }
1562
1606
  create(packetName, timeoutMs) {
1607
+ if (this.nextRequestSeq > 0xffffffffffffffffn) {
1608
+ throw connectorError("sendFailed" /* SendFailed */, "Request sequence is exhausted.");
1609
+ }
1563
1610
  const requestSeq = this.nextRequestSeq++;
1564
1611
  let timeout;
1565
1612
  let resolvePending;
1566
1613
  let rejectPending;
1567
1614
  const promise = new Promise(
1568
1615
  (resolve, reject) => {
1569
- timeout = setTimeout(() => {
1570
- this.active.delete(requestSeq);
1571
- reject(
1572
- connectorError(
1573
- "requestTimeout" /* RequestTimeout */,
1574
- `Request '${packetName}' timed out.`
1575
- )
1576
- );
1577
- }, timeoutMs);
1578
1616
  resolvePending = resolve;
1579
1617
  rejectPending = (error) => reject(connectorError(error.code, error.message, error.cause));
1580
1618
  }
1581
1619
  );
1582
- this.active.set(requestSeq, {
1620
+ const tracked = {
1583
1621
  packetName,
1584
1622
  promise,
1623
+ startTimeout: () => {
1624
+ if (timeout !== void 0 || this.active.get(requestSeq) !== tracked) return;
1625
+ timeout = setTimeout(() => {
1626
+ this.active.delete(requestSeq);
1627
+ rejectPending({
1628
+ code: "requestTimeout" /* RequestTimeout */,
1629
+ message: `Request '${packetName}' timed out.`
1630
+ });
1631
+ }, timeoutMs);
1632
+ },
1585
1633
  resolve: (value) => {
1586
1634
  if (timeout !== void 0) {
1587
1635
  clearTimeout(timeout);
@@ -1599,8 +1647,9 @@ var ZlinkStreamPendingRequests = class {
1599
1647
  clearTimeout(timeout);
1600
1648
  }
1601
1649
  }
1602
- });
1603
- return { requestSeq, promise };
1650
+ };
1651
+ this.active.set(requestSeq, tracked);
1652
+ return { requestSeq, promise, startTimeout: tracked.startTimeout };
1604
1653
  }
1605
1654
  /* stream connector spec §5.2: a pending request is matched by request_seq alone. */
1606
1655
  resolve(requestSeq, value, metadata) {
@@ -1608,8 +1657,9 @@ var ZlinkStreamPendingRequests = class {
1608
1657
  if (pending === void 0) {
1609
1658
  return false;
1610
1659
  }
1660
+ const payload = value();
1611
1661
  this.active.delete(requestSeq);
1612
- pending.resolve({ name: pending.packetName, metadata, payload: value });
1662
+ pending.resolve({ name: pending.packetName, metadata, payload });
1613
1663
  return true;
1614
1664
  }
1615
1665
  reject(requestSeq, error) {
@@ -1637,7 +1687,164 @@ var ZlinkStreamPendingRequests = class {
1637
1687
  }
1638
1688
  };
1639
1689
 
1690
+ // packages/stream-connector/src/Runtime/ZlinkStreamActors.ts
1691
+ var ACTOR_BOUND = "$zlink.actor.bound";
1692
+ var ACTOR_UNBOUND = "$zlink.actor.unbound";
1693
+ var zlinkStreamActorBinding = Symbol("zlink.stream.actorBinding");
1694
+ var DefaultZlinkStreamActor = class {
1695
+ constructor(connector, actorId, slot) {
1696
+ this.connector = connector;
1697
+ this.actorId = actorId;
1698
+ this.slot = slot;
1699
+ }
1700
+ bound = true;
1701
+ get isBound() {
1702
+ return this.bound;
1703
+ }
1704
+ send(payload, messageType) {
1705
+ this.ensureBound();
1706
+ return this.connector.sendForActor(this, payload, messageType);
1707
+ }
1708
+ request(payload, messageType) {
1709
+ this.ensureBound();
1710
+ return this.connector.requestForActor(this, payload, messageType);
1711
+ }
1712
+ on(nameOrType, handler, messageType) {
1713
+ return this.connector.onActorMessage(this, nameOrType, handler, messageType);
1714
+ }
1715
+ ensureBound() {
1716
+ if (!this.bound) {
1717
+ throw connectorError(
1718
+ "validationFailed" /* ValidationFailed */,
1719
+ `Actor '${this.actorId}' is no longer bound.`
1720
+ );
1721
+ }
1722
+ }
1723
+ close() {
1724
+ this.bound = false;
1725
+ }
1726
+ };
1727
+ var ZlinkStreamActors = class {
1728
+ constructor(connector, receivedMessages, events) {
1729
+ this.connector = connector;
1730
+ this.receivedMessages = receivedMessages;
1731
+ this.events = events;
1732
+ }
1733
+ bySlot = /* @__PURE__ */ new Map();
1734
+ byId = /* @__PURE__ */ new Map();
1735
+ issued = [];
1736
+ boundHandlers = /* @__PURE__ */ new Set();
1737
+ unboundHandlers = /* @__PURE__ */ new Set();
1738
+ get snapshot() {
1739
+ return Object.freeze(Array.from(this.bySlot.values()));
1740
+ }
1741
+ find(actorId) {
1742
+ return this.byId.get(actorId);
1743
+ }
1744
+ onBound(handler) {
1745
+ this.boundHandlers.add(handler);
1746
+ return subscription(() => this.boundHandlers.delete(handler));
1747
+ }
1748
+ onUnbound(handler) {
1749
+ this.unboundHandlers.add(handler);
1750
+ return subscription(() => this.unboundHandlers.delete(handler));
1751
+ }
1752
+ processControl(name, payload, signal) {
1753
+ if (name === ACTOR_BOUND) {
1754
+ this.bind(payload, signal);
1755
+ return true;
1756
+ }
1757
+ if (name === ACTOR_UNBOUND) {
1758
+ this.unbind(payload, signal);
1759
+ return true;
1760
+ }
1761
+ return false;
1762
+ }
1763
+ resolve(slot) {
1764
+ const actor = this.bySlot.get(slot);
1765
+ if (actor === void 0) {
1766
+ throw connectorError(
1767
+ "frameDecodeFailed" /* FrameDecodeFailed */,
1768
+ `Actor slot '${slot}' is not bound.`
1769
+ );
1770
+ }
1771
+ return actor;
1772
+ }
1773
+ closeAll(signal) {
1774
+ for (const actor of this.issued) {
1775
+ if (!actor.isBound) continue;
1776
+ this.bySlot.delete(actor.slot);
1777
+ this.byId.delete(actor.actorId);
1778
+ actor.close();
1779
+ this.queue(this.unboundHandlers, actor, signal);
1780
+ }
1781
+ this.issued.length = 0;
1782
+ }
1783
+ bind(payload, signal) {
1784
+ if (payload.length < 5 || payload[0] !== 1) {
1785
+ throw invalidControl("Actor bound payload is invalid.");
1786
+ }
1787
+ const slot = payload[1] << 8 | payload[2];
1788
+ const idLength = payload[3];
1789
+ if (slot === 0 || idLength === 0 || payload.length !== 4 + idLength) {
1790
+ throw invalidControl("Actor bound payload is invalid.");
1791
+ }
1792
+ let actorId;
1793
+ try {
1794
+ actorId = new TextDecoder("utf-8", { fatal: true }).decode(payload.subarray(4));
1795
+ } catch (cause) {
1796
+ throw connectorError(
1797
+ "frameDecodeFailed" /* FrameDecodeFailed */,
1798
+ "Actor id is not valid UTF-8.",
1799
+ cause
1800
+ );
1801
+ }
1802
+ if (actorId.length === 0 || this.bySlot.has(slot) || this.byId.has(actorId)) {
1803
+ throw invalidControl("Actor bound identity is already in use.");
1804
+ }
1805
+ const actor = new DefaultZlinkStreamActor(this.connector, actorId, slot);
1806
+ this.bySlot.set(slot, actor);
1807
+ this.byId.set(actorId, actor);
1808
+ this.issued.push(actor);
1809
+ this.queue(this.boundHandlers, actor, signal);
1810
+ }
1811
+ unbind(payload, signal) {
1812
+ if (payload.length !== 3 || payload[0] !== 1) {
1813
+ throw invalidControl("Actor unbound payload is invalid.");
1814
+ }
1815
+ const slot = payload[1] << 8 | payload[2];
1816
+ const actor = this.bySlot.get(slot);
1817
+ if (slot === 0 || actor === void 0) {
1818
+ throw invalidControl(`Actor slot '${slot}' is not bound.`);
1819
+ }
1820
+ this.bySlot.delete(slot);
1821
+ this.byId.delete(actor.actorId);
1822
+ actor.close();
1823
+ this.queue(this.unboundHandlers, actor, signal);
1824
+ }
1825
+ queue(handlers, actor, signal) {
1826
+ this.receivedMessages.enqueueCallback(
1827
+ () => {
1828
+ for (const handler of Array.from(handlers)) {
1829
+ this.events.runUserCallback(
1830
+ () => handler(actor, signal),
1831
+ "Actor lifecycle handler failed.",
1832
+ signal
1833
+ );
1834
+ }
1835
+ },
1836
+ () => handlers.size
1837
+ );
1838
+ }
1839
+ };
1840
+ function invalidControl(message) {
1841
+ return connectorError("frameDecodeFailed" /* FrameDecodeFailed */, message);
1842
+ }
1843
+
1640
1844
  // packages/stream-connector/src/Runtime/ZlinkStreamReceivedMessages.ts
1845
+ function receives(registration, message) {
1846
+ return registration.actor === void 0 || registration.actor === message[zlinkStreamActorBinding];
1847
+ }
1641
1848
  var ZlinkStreamReceivedMessages = class {
1642
1849
  /**
1643
1850
  * @param deliverOnArrival `Immediate` runs registered handlers on the receive
@@ -1655,10 +1862,10 @@ var ZlinkStreamReceivedMessages = class {
1655
1862
  // queue is not a simple FIFO. Tombstones let us remove a deliverable entry
1656
1863
  // without shifting every later message on the hot receive path.
1657
1864
  queue = [];
1865
+ deliverable = [];
1658
1866
  queueHead = 0;
1659
1867
  queuedCount = 0;
1660
- drainTask;
1661
- // True for as long as `drain` is on the stack, handler awaits included. It
1868
+ // True for as long as `pump` is on the stack. It
1662
1869
  // marks the execution context a registered handler runs in, so a `dispatch`
1663
1870
  // made from inside a handler is recognised as re-entry rather than a fresh
1664
1871
  // pump. It is not a lock: a single event loop admits no second thread, and
@@ -1668,19 +1875,32 @@ var ZlinkStreamReceivedMessages = class {
1668
1875
  // connection. It is raised where a packet arrives, never where one is taken,
1669
1876
  // so consuming does not lower it and the dispatch mode does not change it.
1670
1877
  receivedCounts = /* @__PURE__ */ new Map();
1671
- on(name, handler) {
1878
+ /**
1879
+ * @param actor The Actor handle that registers the handler, if any. Its
1880
+ * handler receives only that Actor's packets.
1881
+ */
1882
+ on(name, handler, actor) {
1672
1883
  validateName(name);
1673
1884
  let set = this.handlers.get(name);
1674
1885
  if (set === void 0) {
1675
1886
  set = /* @__PURE__ */ new Set();
1676
1887
  this.handlers.set(name, set);
1677
1888
  }
1678
- set.add(handler);
1679
- if (this.deliverOnArrival && this.hasQueuedMessage(name)) {
1680
- queueMicrotask(() => this.scheduleDrain());
1889
+ const registration = { handle: handler, actor };
1890
+ set.add(registration);
1891
+ let newlyDeliverable = false;
1892
+ for (let index = this.queueHead; index < this.queue.length; index += 1) {
1893
+ const queued = this.queue[index];
1894
+ if (queued?.kind === "message" && queued.indexed !== true && queued.message.name === name && receives(registration, queued.message)) {
1895
+ this.indexDeliverable(index);
1896
+ newlyDeliverable = true;
1897
+ }
1898
+ }
1899
+ if (this.deliverOnArrival && newlyDeliverable) {
1900
+ queueMicrotask(() => this.pump());
1681
1901
  }
1682
1902
  return subscription(() => {
1683
- set.delete(handler);
1903
+ set.delete(registration);
1684
1904
  if (set.size === 0 && this.handlers.get(name) === set) {
1685
1905
  this.handlers.delete(name);
1686
1906
  }
@@ -1750,6 +1970,11 @@ var ZlinkStreamReceivedMessages = class {
1750
1970
  this.queue.push(...submittedCallbacks);
1751
1971
  this.queueHead = 0;
1752
1972
  this.queuedCount = submittedCallbacks.length;
1973
+ this.deliverable.length = 0;
1974
+ for (let index = 0; index < this.queue.length; index += 1) {
1975
+ this.queue[index].indexed = false;
1976
+ this.indexDeliverable(index);
1977
+ }
1753
1978
  }
1754
1979
  /**
1755
1980
  * Releases every registered wait surface because the connection it was
@@ -1780,60 +2005,38 @@ var ZlinkStreamReceivedMessages = class {
1780
2005
  }
1781
2006
  this.queue.push({ kind: "message", message, signal });
1782
2007
  this.queuedCount += 1;
2008
+ this.indexDeliverable(this.queue.length - 1);
1783
2009
  if (this.deliverOnArrival) {
1784
- this.scheduleDrain();
2010
+ this.pump();
1785
2011
  }
1786
2012
  }
1787
- enqueueCallback(callback) {
1788
- this.queue.push({ kind: "callback", callback });
2013
+ /**
2014
+ * @param callbacks The number of handler calls `callback` makes when it runs
2015
+ * with the handlers registered at that moment; a callback that runs a set
2016
+ * of handlers reads the set's size. One callback by default.
2017
+ */
2018
+ enqueueCallback(callback, callbacks = () => 1) {
2019
+ this.queue.push({ kind: "callback", callback, callbacks });
1789
2020
  this.queuedCount += 1;
2021
+ this.indexDeliverable(this.queue.length - 1);
1790
2022
  if (this.deliverOnArrival) {
1791
- this.scheduleDrain();
2023
+ this.pump();
1792
2024
  }
1793
2025
  }
1794
2026
  /**
1795
2027
  * Runs the registered handlers the receive path left queued. `Manual` calls
1796
- * this from `dispatch`; `Immediate` has already drained on arrival.
2028
+ * this from `dispatch`; `Immediate` drains on arrival.
1797
2029
  *
1798
- * A handler that calls `dispatch` arrives back here from inside the drain it
1799
- * was started by. `scheduleDrain` would find `drainTask` already set and
1800
- * return, and the await below would then be the drain waiting on itself —
1801
- * a deadlock with neither timeout nor error. The drain loop already takes
1802
- * every message a handler exists for, so there is nothing a second drain
1803
- * would deliver and returning is the whole of the correct behaviour.
2030
+ * The drain is synchronous: the connector does not wait for a handler
2031
+ * (spec stream-connector 32 §7), so nothing inside it awaits. A handler that
2032
+ * calls `dispatch` arrives back here from inside the drain it was started by;
2033
+ * the running loop already takes every deliverable entry, so returning is the
2034
+ * whole of the correct behaviour.
1804
2035
  */
1805
- async pump() {
2036
+ pump() {
1806
2037
  if (this.draining) {
1807
2038
  return;
1808
2039
  }
1809
- this.scheduleDrain();
1810
- await this.drainTask;
1811
- }
1812
- offerQueued(name, registration) {
1813
- for (let index = this.queueHead; index < this.queue.length; index += 1) {
1814
- const queued = this.queue[index];
1815
- if (queued === void 0 || queued.kind !== "message" || queued.message.name !== name) {
1816
- continue;
1817
- }
1818
- if (!registration.consume(queued.message)) {
1819
- continue;
1820
- }
1821
- this.removeAt(index);
1822
- return;
1823
- }
1824
- }
1825
- scheduleDrain() {
1826
- if (this.drainTask !== void 0) {
1827
- return;
1828
- }
1829
- this.drainTask = this.drain().finally(() => {
1830
- this.drainTask = void 0;
1831
- if (this.deliverOnArrival && this.findDeliverableIndex() >= 0) {
1832
- this.scheduleDrain();
1833
- }
1834
- });
1835
- }
1836
- async drain() {
1837
2040
  this.draining = true;
1838
2041
  try {
1839
2042
  for (let index = this.findDeliverableIndex(); index >= 0; index = this.findDeliverableIndex()) {
@@ -1841,51 +2044,113 @@ var ZlinkStreamReceivedMessages = class {
1841
2044
  if (queued === void 0) continue;
1842
2045
  this.removeAt(index);
1843
2046
  if (queued.kind === "callback") {
1844
- await queued.callback();
2047
+ this.events.runUserCallback(queued.callback, "Connector callback failed.");
1845
2048
  continue;
1846
2049
  }
1847
2050
  const { message, signal } = queued;
1848
- const handlers = Array.from(this.handlers.get(message.name));
2051
+ const handlers = this.receiversOf(message);
1849
2052
  for (const handler of handlers) {
1850
- try {
1851
- await handler(message, signal);
1852
- } catch (cause) {
1853
- await this.events.publishError(
1854
- {
1855
- code: "userCallbackFailed" /* UserCallbackFailed */,
1856
- message: "Typed message handler failed.",
1857
- cause
1858
- },
1859
- signal
1860
- );
1861
- }
2053
+ this.events.runUserCallback(
2054
+ () => handler.handle(message, signal),
2055
+ "Typed message handler failed.",
2056
+ signal
2057
+ );
1862
2058
  }
1863
2059
  }
1864
2060
  } finally {
1865
2061
  this.draining = false;
1866
2062
  }
1867
2063
  }
1868
- removeAt(index) {
1869
- this.queue[index] = void 0;
1870
- this.queuedCount -= 1;
1871
- this.advanceHead();
1872
- this.compactQueue();
2064
+ /**
2065
+ * True while a registered callback runs from {@link pump}: the execution
2066
+ * context spec stream-connector 32 §7 calls "inside a handler".
2067
+ */
2068
+ get dispatching() {
2069
+ return this.draining;
1873
2070
  }
1874
- findDeliverableIndex() {
2071
+ get pendingCallbacks() {
2072
+ let count = 0;
1875
2073
  for (let index = this.queueHead; index < this.queue.length; index += 1) {
1876
2074
  const queued = this.queue[index];
1877
- if (queued !== void 0 && (queued.kind === "callback" || (this.handlers.get(queued.message.name)?.size ?? 0) > 0)) {
2075
+ if (queued?.kind === "callback" && queued.callbacks() > 0) count += 1;
2076
+ if (queued?.kind === "message" && this.receiversOf(queued.message).length > 0) count += 1;
2077
+ }
2078
+ return count;
2079
+ }
2080
+ offerQueued(name, registration) {
2081
+ for (let index = this.queueHead; index < this.queue.length; index += 1) {
2082
+ const queued = this.queue[index];
2083
+ if (queued === void 0 || queued.kind !== "message" || queued.message.name !== name) {
2084
+ continue;
2085
+ }
2086
+ if (!registration.consume(queued.message)) {
2087
+ continue;
2088
+ }
2089
+ this.removeAt(index);
2090
+ return;
2091
+ }
2092
+ }
2093
+ removeAt(index) {
2094
+ this.queue[index] = void 0;
2095
+ this.queuedCount -= 1;
2096
+ this.advanceHead();
2097
+ this.compactQueue();
2098
+ }
2099
+ findDeliverableIndex() {
2100
+ while (this.deliverable.length > 0) {
2101
+ const index = this.deliverable[0];
2102
+ const queued = this.queue[index];
2103
+ if (this.isDeliverable(queued)) {
1878
2104
  return index;
1879
2105
  }
2106
+ if (queued !== void 0) queued.indexed = false;
2107
+ this.removeDeliverable();
1880
2108
  }
1881
2109
  return -1;
1882
2110
  }
1883
- hasQueuedMessage(name) {
1884
- for (let index = this.queueHead; index < this.queue.length; index += 1) {
1885
- const queued = this.queue[index];
1886
- if (queued?.kind === "message" && queued.message.name === name) return true;
2111
+ indexDeliverable(index) {
2112
+ const queued = this.queue[index];
2113
+ if (queued === void 0 || !this.isDeliverable(queued) || queued.indexed === true) return;
2114
+ queued.indexed = true;
2115
+ let position = this.deliverable.length;
2116
+ this.deliverable.push(index);
2117
+ while (position > 0) {
2118
+ const parent = Math.floor((position - 1) / 2);
2119
+ if (this.deliverable[parent] <= index) break;
2120
+ this.deliverable[position] = this.deliverable[parent];
2121
+ position = parent;
1887
2122
  }
1888
- return false;
2123
+ this.deliverable[position] = index;
2124
+ }
2125
+ isDeliverable(queued) {
2126
+ return queued?.kind === "callback" || queued?.kind === "message" && this.receiversOf(queued.message).length > 0;
2127
+ }
2128
+ /**
2129
+ * Spec stream-connector 32 §7 and §10: the handlers that receive `message`
2130
+ * are the connector handlers for its name and the handle handlers of the
2131
+ * Actor it carries. A packet no handler receives stays in the queue for the
2132
+ * wait surfaces. This is the one place that decides it.
2133
+ */
2134
+ receiversOf(message) {
2135
+ const set = this.handlers.get(message.name);
2136
+ if (set === void 0) return [];
2137
+ return Array.from(set).filter((registration) => receives(registration, message));
2138
+ }
2139
+ removeDeliverable() {
2140
+ if (this.deliverable.length === 0) return;
2141
+ const last = this.deliverable.pop();
2142
+ if (this.deliverable.length === 0) return;
2143
+ let position = 0;
2144
+ for (; ; ) {
2145
+ const left = position * 2 + 1;
2146
+ if (left >= this.deliverable.length) break;
2147
+ const right = left + 1;
2148
+ const child = right < this.deliverable.length && this.deliverable[right] < this.deliverable[left] ? right : left;
2149
+ if (this.deliverable[child] >= last) break;
2150
+ this.deliverable[position] = this.deliverable[child];
2151
+ position = child;
2152
+ }
2153
+ this.deliverable[position] = last;
1889
2154
  }
1890
2155
  advanceHead() {
1891
2156
  while (this.queueHead < this.queue.length && this.queue[this.queueHead] === void 0) {
@@ -1896,22 +2161,99 @@ var ZlinkStreamReceivedMessages = class {
1896
2161
  if (this.queuedCount === 0) {
1897
2162
  this.queue.length = 0;
1898
2163
  this.queueHead = 0;
2164
+ this.deliverable.length = 0;
1899
2165
  return;
1900
2166
  }
1901
2167
  if (this.queueHead >= 1024 && this.queueHead * 2 >= this.queue.length) {
1902
2168
  this.queue.splice(0, this.queueHead);
1903
2169
  this.queueHead = 0;
2170
+ this.deliverable.length = 0;
2171
+ for (let index = 0; index < this.queue.length; index += 1) {
2172
+ const queued = this.queue[index];
2173
+ if (queued === void 0) continue;
2174
+ queued.indexed = false;
2175
+ this.indexDeliverable(index);
2176
+ }
1904
2177
  }
1905
2178
  }
1906
2179
  };
1907
2180
 
1908
2181
  // packages/stream-connector/src/Runtime/ZlinkStreamFrameSender.ts
2182
+ var ZlinkStreamConnectionWriteQueue = class {
2183
+ /**
2184
+ * @param writeFailed Ends the connection whose transport write failed (spec
2185
+ * stream-connector 32 §9: a transport write failure ends the connection as
2186
+ * `TransportError`).
2187
+ */
2188
+ constructor(connection, writeFailed) {
2189
+ this.connection = connection;
2190
+ this.writeFailed = writeFailed;
2191
+ }
2192
+ queue = /* @__PURE__ */ new Set();
2193
+ active;
2194
+ enqueue(operation) {
2195
+ this.queue.add(operation);
2196
+ this.advance();
2197
+ }
2198
+ cancel(operation, error) {
2199
+ if (this.queue.has(operation) || this.active === operation) operation.fail(error);
2200
+ }
2201
+ /**
2202
+ * Spec stream-connector 32 §7: when the connection ends, frames that have not
2203
+ * reached the transport are not written and the frame being written is not
2204
+ * waited for. The operations of both fail with `error`.
2205
+ */
2206
+ failAll(error) {
2207
+ const operations = this.active === void 0 ? [...this.queue] : [this.active, ...this.queue];
2208
+ this.queue.clear();
2209
+ this.active = void 0;
2210
+ for (const operation of operations) operation.fail(error);
2211
+ }
2212
+ advance() {
2213
+ if (this.active !== void 0) return;
2214
+ const next = this.queue.values().next().value;
2215
+ if (next === void 0) return;
2216
+ this.queue.delete(next);
2217
+ this.active = next;
2218
+ let write;
2219
+ try {
2220
+ write = Promise.resolve(this.connection.write(next.frame));
2221
+ } catch (error) {
2222
+ write = Promise.reject(error);
2223
+ }
2224
+ void write.then(
2225
+ () => next.complete(),
2226
+ (cause) => this.failWrite(next, cause)
2227
+ ).finally(() => {
2228
+ if (this.active !== next) return;
2229
+ this.active = void 0;
2230
+ this.advance();
2231
+ });
2232
+ }
2233
+ /**
2234
+ * Spec stream-connector 32 §9: a transport write failure is `SendFailed` for
2235
+ * the operation of that write, and it ends the connection. The ending fails
2236
+ * every other operation of the connection with `Disconnected`. A write that
2237
+ * fails after the connection already ended belongs to that ending, which has
2238
+ * already failed its operation with `Disconnected`.
2239
+ */
2240
+ failWrite(operation, cause) {
2241
+ if (this.active !== operation) return;
2242
+ const error = {
2243
+ code: "sendFailed" /* SendFailed */,
2244
+ message: `Frame write failed: ${cause instanceof Error ? cause.message : String(cause)}`,
2245
+ cause
2246
+ };
2247
+ operation.fail(new ZlinkStreamException(error));
2248
+ this.writeFailed(error);
2249
+ }
2250
+ };
1909
2251
  var ZlinkStreamFrameSender = class {
1910
2252
  constructor(protocol) {
1911
2253
  this.protocol = protocol;
1912
2254
  }
1913
- pendingWrites = /* @__PURE__ */ new Set();
1914
- async send(connection, kind, name, payload, metadata, compress, requestSeq, signal, correlationId, actorSlot) {
2255
+ queues = /* @__PURE__ */ new Map();
2256
+ async send(connection, kind, name, payload, metadata, compress, requestSeq, signal, correlationId, actorSlot, expiry, onAccepted) {
1915
2257
  throwIfAborted(signal);
1916
2258
  await this.write(
1917
2259
  connection,
@@ -1925,26 +2267,78 @@ var ZlinkStreamFrameSender = class {
1925
2267
  correlationId,
1926
2268
  actorSlot
1927
2269
  ),
1928
- signal
2270
+ signal,
2271
+ expiry,
2272
+ onAccepted
1929
2273
  );
1930
2274
  }
1931
- async sendControl(connection, name, signal) {
1932
- await this.write(connection, this.protocol.encodeControl(name), signal);
2275
+ /**
2276
+ * Writes a control frame (heartbeat ping or pong). A control frame belongs to
2277
+ * no operation: its write failure ends the connection through the write
2278
+ * queue like any other write (spec stream-connector 32 §9), and a connection
2279
+ * that has already ended does not write it. The promise settles when the
2280
+ * write has ended either way.
2281
+ */
2282
+ sendControl(connection, name) {
2283
+ const queue = this.queues.get(connection);
2284
+ if (queue === void 0) return Promise.resolve();
2285
+ const frame = this.protocol.encodeControl(name);
2286
+ return new Promise((resolve) => {
2287
+ queue.enqueue({ frame, complete: resolve, fail: () => resolve() });
2288
+ });
1933
2289
  }
1934
- async drain(signal) {
1935
- while (this.pendingWrites.size > 0) {
1936
- throwIfAborted(signal);
1937
- await Promise.allSettled(Array.from(this.pendingWrites));
1938
- }
2290
+ /**
2291
+ * Gives a connection that has just been established its own write queue.
2292
+ * `writeFailed` ends that connection when one of its transport writes fails.
2293
+ */
2294
+ open(connection, writeFailed) {
2295
+ this.queues.set(connection, new ZlinkStreamConnectionWriteQueue(connection, writeFailed));
1939
2296
  }
1940
- async write(connection, frame, signal) {
1941
- const write = connection.write(frame, signal);
1942
- this.pendingWrites.add(write);
1943
- try {
1944
- await write;
1945
- } finally {
1946
- this.pendingWrites.delete(write);
2297
+ /**
2298
+ * Ends the write queue of a connection that has ended, by close and by
2299
+ * transport loss alike. Every operation still in it fails with `error`.
2300
+ */
2301
+ failUnwritten(connection, error) {
2302
+ const queue = this.queues.get(connection);
2303
+ if (queue === void 0) return;
2304
+ this.queues.delete(connection);
2305
+ queue.failAll(error);
2306
+ }
2307
+ write(connection, frame, signal, expiry, onAccepted) {
2308
+ throwIfAborted(signal);
2309
+ const queue = this.queues.get(connection);
2310
+ if (queue === void 0) {
2311
+ throw connectorError("disconnected" /* Disconnected */, "Connector is not connected.");
1947
2312
  }
2313
+ let operation;
2314
+ let settled = false;
2315
+ const promise = new Promise((resolve, reject) => {
2316
+ const cleanup = () => signal?.removeEventListener("abort", onAbort);
2317
+ const onAbort = () => queue.cancel(
2318
+ operation,
2319
+ connectorError("disconnected" /* Disconnected */, "Operation canceled.")
2320
+ );
2321
+ operation = {
2322
+ frame,
2323
+ complete: () => {
2324
+ if (settled) return;
2325
+ settled = true;
2326
+ cleanup();
2327
+ resolve();
2328
+ },
2329
+ fail: (error) => {
2330
+ if (settled) return;
2331
+ settled = true;
2332
+ cleanup();
2333
+ reject(error);
2334
+ }
2335
+ };
2336
+ signal?.addEventListener("abort", onAbort, { once: true });
2337
+ onAccepted?.();
2338
+ expiry?.then(void 0, (error) => queue.cancel(operation, error));
2339
+ queue.enqueue(operation);
2340
+ });
2341
+ return promise;
1948
2342
  }
1949
2343
  };
1950
2344
 
@@ -1970,177 +2364,16 @@ function decodeSessionClosing(payload) {
1970
2364
  return { closeReason, diagnostic };
1971
2365
  }
1972
2366
 
1973
- // packages/stream-connector/src/Runtime/ZlinkStreamActors.ts
1974
- var ACTOR_BOUND = "$zlink.actor.bound";
1975
- var ACTOR_UNBOUND = "$zlink.actor.unbound";
1976
- var zlinkStreamActorBinding = Symbol("zlink.stream.actorBinding");
1977
- var DefaultZlinkStreamActor = class {
1978
- constructor(connector, actorId, slot) {
1979
- this.connector = connector;
1980
- this.actorId = actorId;
1981
- this.slot = slot;
1982
- }
1983
- bound = true;
1984
- get isBound() {
1985
- return this.bound;
1986
- }
1987
- send(payload, messageType) {
1988
- this.ensureBound();
1989
- return this.connector.sendForActor(this, payload, messageType);
1990
- }
1991
- request(payload, messageType) {
1992
- this.ensureBound();
1993
- return this.connector.requestForActor(this, payload, messageType);
1994
- }
1995
- on(nameOrType, handler, messageType) {
1996
- return this.connector.onActorMessage(this, nameOrType, handler, messageType);
1997
- }
1998
- ensureBound() {
1999
- if (!this.bound) {
2000
- throw connectorError(
2001
- "validationFailed" /* ValidationFailed */,
2002
- `Actor '${this.actorId}' is no longer bound.`
2003
- );
2004
- }
2005
- }
2006
- close() {
2007
- this.bound = false;
2008
- }
2009
- };
2010
- var ZlinkStreamActors = class {
2011
- constructor(connector, receivedMessages, events) {
2012
- this.connector = connector;
2013
- this.receivedMessages = receivedMessages;
2014
- this.events = events;
2015
- }
2016
- bySlot = /* @__PURE__ */ new Map();
2017
- byId = /* @__PURE__ */ new Map();
2018
- issued = [];
2019
- boundHandlers = /* @__PURE__ */ new Set();
2020
- unboundHandlers = /* @__PURE__ */ new Set();
2021
- get snapshot() {
2022
- return Object.freeze(Array.from(this.bySlot.values()));
2023
- }
2024
- find(actorId) {
2025
- return this.byId.get(actorId);
2026
- }
2027
- onBound(handler) {
2028
- this.boundHandlers.add(handler);
2029
- return subscription(() => this.boundHandlers.delete(handler));
2030
- }
2031
- onUnbound(handler) {
2032
- this.unboundHandlers.add(handler);
2033
- return subscription(() => this.unboundHandlers.delete(handler));
2034
- }
2035
- processControl(name, payload, signal) {
2036
- if (name === ACTOR_BOUND) {
2037
- this.bind(payload, signal);
2038
- return true;
2039
- }
2040
- if (name === ACTOR_UNBOUND) {
2041
- this.unbind(payload, signal);
2042
- return true;
2043
- }
2044
- return false;
2045
- }
2046
- resolve(slot) {
2047
- const actor = this.bySlot.get(slot);
2048
- if (actor === void 0) {
2049
- throw connectorError(
2050
- "frameDecodeFailed" /* FrameDecodeFailed */,
2051
- `Actor slot '${slot}' is not bound.`
2052
- );
2053
- }
2054
- return actor;
2055
- }
2056
- closeAll(signal) {
2057
- for (const actor of this.issued) {
2058
- if (!actor.isBound) continue;
2059
- this.bySlot.delete(actor.slot);
2060
- this.byId.delete(actor.actorId);
2061
- actor.close();
2062
- this.queue(this.unboundHandlers, actor, signal);
2063
- }
2064
- this.issued.length = 0;
2065
- }
2066
- bind(payload, signal) {
2067
- if (payload.length < 5 || payload[0] !== 1) {
2068
- throw invalidControl("Actor bound payload is invalid.");
2069
- }
2070
- const slot = payload[1] << 8 | payload[2];
2071
- const idLength = payload[3];
2072
- if (slot === 0 || idLength === 0 || payload.length !== 4 + idLength) {
2073
- throw invalidControl("Actor bound payload is invalid.");
2074
- }
2075
- let actorId;
2076
- try {
2077
- actorId = new TextDecoder("utf-8", { fatal: true }).decode(payload.subarray(4));
2078
- } catch (cause) {
2079
- throw connectorError(
2080
- "frameDecodeFailed" /* FrameDecodeFailed */,
2081
- "Actor id is not valid UTF-8.",
2082
- cause
2083
- );
2084
- }
2085
- if (actorId.length === 0 || this.bySlot.has(slot) || this.byId.has(actorId)) {
2086
- throw invalidControl("Actor bound identity is already in use.");
2087
- }
2088
- const actor = new DefaultZlinkStreamActor(this.connector, actorId, slot);
2089
- this.bySlot.set(slot, actor);
2090
- this.byId.set(actorId, actor);
2091
- this.issued.push(actor);
2092
- this.queue(this.boundHandlers, actor, signal);
2093
- }
2094
- unbind(payload, signal) {
2095
- if (payload.length !== 3 || payload[0] !== 1) {
2096
- throw invalidControl("Actor unbound payload is invalid.");
2097
- }
2098
- const slot = payload[1] << 8 | payload[2];
2099
- const actor = this.bySlot.get(slot);
2100
- if (slot === 0 || actor === void 0) {
2101
- throw invalidControl(`Actor slot '${slot}' is not bound.`);
2102
- }
2103
- this.bySlot.delete(slot);
2104
- this.byId.delete(actor.actorId);
2105
- actor.close();
2106
- this.queue(this.unboundHandlers, actor, signal);
2107
- }
2108
- queue(handlers, actor, signal) {
2109
- this.receivedMessages.enqueueCallback(() => {
2110
- for (const handler of Array.from(handlers)) {
2111
- this.invoke(handler, actor, signal);
2112
- }
2113
- });
2114
- }
2115
- invoke(handler, actor, signal) {
2116
- try {
2117
- Promise.resolve(handler(actor, signal)).catch((cause) => {
2118
- void this.events.publishError(
2119
- {
2120
- code: "userCallbackFailed" /* UserCallbackFailed */,
2121
- message: "Actor lifecycle handler failed.",
2122
- cause
2123
- },
2124
- signal
2125
- );
2126
- });
2127
- } catch (cause) {
2128
- void this.events.publishError(
2129
- {
2130
- code: "userCallbackFailed" /* UserCallbackFailed */,
2131
- message: "Actor lifecycle handler failed.",
2132
- cause
2133
- },
2134
- signal
2135
- );
2136
- }
2367
+ // packages/stream-connector/src/Runtime/ZlinkStreamReceiveDispatcher.ts
2368
+ function closeReasonFor(error) {
2369
+ return error.code === "frameDecodeFailed" /* FrameDecodeFailed */ || error.code === "frameTooLarge" /* FrameTooLarge */ ? "ProtocolError" : "TransportError";
2370
+ }
2371
+ var ZlinkStreamConnectionEnd = class extends Error {
2372
+ constructor(error) {
2373
+ super(error.message);
2374
+ this.error = error;
2137
2375
  }
2138
2376
  };
2139
- function invalidControl(message) {
2140
- return connectorError("frameDecodeFailed" /* FrameDecodeFailed */, message);
2141
- }
2142
-
2143
- // packages/stream-connector/src/Runtime/ZlinkStreamReceiveDispatcher.ts
2144
2377
  var ZlinkStreamReceiveDispatcher = class {
2145
2378
  constructor(protocol, pendingRequests, receivedMessages, frameSender, events, actors, serverClosing) {
2146
2379
  this.protocol = protocol;
@@ -2160,15 +2393,20 @@ var ZlinkStreamReceiveDispatcher = class {
2160
2393
  * the recheck the rest of a dead connection's batch lands on the new
2161
2394
  * connection's state, and the §10 arrival counts, which start at 0 when a
2162
2395
  * connection is established, take the stale frames on top.
2163
- * @param connectionForSend Resolves the connection a reply belongs on at the
2164
- * moment it is written. The captured `connection` is the one the batch was
2165
- * read from, which a reconnect may already have replaced.
2166
2396
  */
2167
- async readAndDispatch(connection, signal, isCurrent, connectionForSend) {
2397
+ async readAndDispatch(connection, signal, isCurrent) {
2168
2398
  if (connection?.read === void 0) {
2169
2399
  return { available: false, inbound: false };
2170
2400
  }
2171
- const frameBytes = await connection.read(signal);
2401
+ let frameBytes;
2402
+ try {
2403
+ frameBytes = await connection.read(signal);
2404
+ } catch (cause) {
2405
+ if (signal?.aborted === true) throw cause;
2406
+ throw new ZlinkStreamConnectionEnd(
2407
+ toStreamError(cause, "disconnected" /* Disconnected */, "Transport read failed.")
2408
+ );
2409
+ }
2172
2410
  if (isCurrent !== void 0 && !isCurrent()) {
2173
2411
  return { available: false, inbound: false };
2174
2412
  }
@@ -2179,87 +2417,102 @@ var ZlinkStreamReceiveDispatcher = class {
2179
2417
  try {
2180
2418
  frames = this.protocol.decodeFrames(frameBytes);
2181
2419
  } catch (cause) {
2182
- throw cause;
2420
+ throw new ZlinkStreamConnectionEnd(
2421
+ toStreamError(cause, "frameDecodeFailed" /* FrameDecodeFailed */, "Frame decode failed.")
2422
+ );
2183
2423
  }
2184
2424
  for (const frame of frames) {
2185
2425
  if (isCurrent !== void 0 && !isCurrent()) {
2186
2426
  break;
2187
2427
  }
2188
2428
  try {
2189
- await this.dispatch(connection, frame.header, frame.payload, signal, connectionForSend);
2429
+ await this.dispatch(connection, frame.header, frame.payload, signal);
2190
2430
  } catch (cause) {
2191
- if (frame.header.kind === 5 /* Control */ && frame.header.name === ZLINK_STREAM_HEARTBEAT_PING) {
2192
- throw cause;
2193
- }
2431
+ if (cause instanceof ZlinkStreamConnectionEnd) throw cause;
2194
2432
  const error = toStreamError(
2195
2433
  cause,
2196
2434
  "frameDecodeFailed" /* FrameDecodeFailed */,
2197
2435
  "Frame dispatch failed."
2198
2436
  );
2199
2437
  if (error.code === "frameDecodeFailed" /* FrameDecodeFailed */) {
2200
- throw cause;
2438
+ throw new ZlinkStreamConnectionEnd(error);
2201
2439
  }
2202
- await this.events.publishError(error, signal);
2440
+ this.events.publishError(error, signal);
2203
2441
  }
2204
2442
  }
2205
2443
  return { available: true, inbound: true };
2206
2444
  }
2207
- async dispatch(connection, header, payload, signal, connectionForSend) {
2445
+ /**
2446
+ * Spec stream-connector 32 §4.7 and §9: a decompressed payload over the
2447
+ * receive limit is `FrameTooLarge` and ends the connection as a protocol
2448
+ * error, like a wire payload over the limit. A payload that does not
2449
+ * decompress fails only its packet.
2450
+ */
2451
+ decodePayload(header, payload) {
2452
+ try {
2453
+ return this.protocol.decodePayload(header, payload);
2454
+ } catch (cause) {
2455
+ const error = toStreamError(
2456
+ cause,
2457
+ "decompressionFailed" /* DecompressionFailed */,
2458
+ "Decompression failed."
2459
+ );
2460
+ if (error.code === "frameTooLarge" /* FrameTooLarge */) {
2461
+ throw new ZlinkStreamConnectionEnd(error);
2462
+ }
2463
+ throw cause;
2464
+ }
2465
+ }
2466
+ async dispatch(connection, header, payload, signal) {
2208
2467
  const actor = header.actorSlot === void 0 ? void 0 : this.actors.resolve(header.actorSlot);
2209
2468
  if (header.kind === 3 /* Response */ && header.requestSeq !== void 0) {
2210
2469
  try {
2211
- if (!this.pendingRequests.resolve(
2470
+ this.pendingRequests.resolve(
2212
2471
  header.requestSeq,
2213
- {
2472
+ () => ({
2214
2473
  codec: header.codec,
2215
- payload: this.protocol.decodePayload(header, payload)
2216
- },
2474
+ payload: this.decodePayload(header, payload)
2475
+ }),
2217
2476
  header.metadata
2218
- )) {
2219
- await this.events.publishError(
2220
- {
2221
- code: "frameDecodeFailed" /* FrameDecodeFailed */,
2222
- message: `Response request sequence '${header.requestSeq}' has no pending request.`
2223
- },
2224
- signal
2225
- );
2226
- }
2477
+ );
2227
2478
  } catch (cause) {
2479
+ if (cause instanceof ZlinkStreamConnectionEnd) throw cause;
2228
2480
  const decodeError = toStreamError(
2229
2481
  cause,
2230
2482
  "decompressionFailed" /* DecompressionFailed */,
2231
2483
  "Decompression failed."
2232
2484
  );
2233
2485
  if (!this.pendingRequests.reject(header.requestSeq, decodeError)) {
2234
- await this.events.publishError(decodeError, signal);
2486
+ this.events.publishError(decodeError, signal);
2235
2487
  }
2236
2488
  }
2237
2489
  return;
2238
2490
  }
2239
2491
  if (header.kind === 4 /* Error */ && header.requestSeq !== void 0) {
2240
2492
  try {
2241
- const remoteError = decodeRemoteError(this.protocol, header, payload);
2493
+ const remoteError = decodeRemoteError(this.decodePayload(header, payload));
2242
2494
  if (!this.pendingRequests.reject(header.requestSeq, remoteError)) {
2243
- await this.events.publishError(remoteError, signal);
2495
+ this.events.publishError(remoteError, signal);
2244
2496
  }
2245
2497
  } catch (cause) {
2498
+ if (cause instanceof ZlinkStreamConnectionEnd) throw cause;
2246
2499
  const decodeError = toStreamError(
2247
2500
  cause,
2248
2501
  "frameDecodeFailed" /* FrameDecodeFailed */,
2249
2502
  "Remote error payload is invalid."
2250
2503
  );
2251
2504
  if (!this.pendingRequests.reject(header.requestSeq, decodeError)) {
2252
- await this.events.publishError(decodeError, signal);
2505
+ this.events.publishError(decodeError, signal);
2253
2506
  }
2254
2507
  }
2255
2508
  return;
2256
2509
  }
2257
2510
  if (header.kind === 4 /* Error */) {
2258
- await this.events.publishError(decodeRemoteError(this.protocol, header, payload), signal);
2511
+ this.events.publishError(decodeRemoteError(this.decodePayload(header, payload)), signal);
2259
2512
  return;
2260
2513
  }
2261
2514
  if (header.kind === 5 /* Control */) {
2262
- await this.dispatchControl(connection, header, payload, signal, connectionForSend);
2515
+ await this.dispatchControl(connection, header, payload, signal);
2263
2516
  return;
2264
2517
  }
2265
2518
  if (header.kind === 1 /* Send */) {
@@ -2267,7 +2520,7 @@ var ZlinkStreamReceiveDispatcher = class {
2267
2520
  {
2268
2521
  name: header.name,
2269
2522
  metadata: header.metadata,
2270
- payload: { codec: header.codec, payload: this.protocol.decodePayload(header, payload) },
2523
+ payload: { codec: header.codec, payload: this.decodePayload(header, payload) },
2271
2524
  actorId: actor?.actorId,
2272
2525
  [zlinkStreamActorBinding]: actor
2273
2526
  },
@@ -2275,7 +2528,7 @@ var ZlinkStreamReceiveDispatcher = class {
2275
2528
  );
2276
2529
  }
2277
2530
  }
2278
- async dispatchControl(connection, header, payload, signal, connectionForSend) {
2531
+ async dispatchControl(connection, header, payload, signal) {
2279
2532
  if (this.actors.processControl(header.name, payload, signal)) {
2280
2533
  return;
2281
2534
  }
@@ -2291,18 +2544,7 @@ var ZlinkStreamReceiveDispatcher = class {
2291
2544
  );
2292
2545
  }
2293
2546
  if (header.name === ZLINK_STREAM_HEARTBEAT_PING) {
2294
- try {
2295
- await this.frameSender.sendControl(
2296
- connectionForSend?.() ?? connection,
2297
- ZLINK_STREAM_HEARTBEAT_PONG,
2298
- signal
2299
- );
2300
- } catch (cause) {
2301
- throw connectorError(
2302
- "sendFailed" /* SendFailed */,
2303
- cause instanceof Error ? cause.message : "Heartbeat pong send failed."
2304
- );
2305
- }
2547
+ await this.frameSender.sendControl(connection, ZLINK_STREAM_HEARTBEAT_PONG);
2306
2548
  return;
2307
2549
  }
2308
2550
  if (header.name !== ZLINK_STREAM_HEARTBEAT_PONG) {
@@ -2310,8 +2552,7 @@ var ZlinkStreamReceiveDispatcher = class {
2310
2552
  }
2311
2553
  }
2312
2554
  };
2313
- function decodeRemoteError(protocol, header, payload) {
2314
- const decodedPayload = protocol.decodePayload(header, payload);
2555
+ function decodeRemoteError(decodedPayload) {
2315
2556
  let decoded;
2316
2557
  try {
2317
2558
  decoded = JSON.parse(utf8Decode2(decodedPayload));
@@ -2358,6 +2599,7 @@ var ZlinkStreamConnectorLifecycle = class {
2358
2599
  lastInboundAt = 0;
2359
2600
  closeTask;
2360
2601
  connectTask;
2602
+ connectAbort;
2361
2603
  disconnectTask;
2362
2604
  closeRequested = false;
2363
2605
  disconnectedPublished = false;
@@ -2389,10 +2631,20 @@ var ZlinkStreamConnectorLifecycle = class {
2389
2631
  });
2390
2632
  return await this.connectTask;
2391
2633
  }
2634
+ /**
2635
+ * Spec stream-connector 32 §7: close does not wait for a connect attempt or a
2636
+ * reconnect delay to run its course. The attempts listen to one controller
2637
+ * the lifecycle owns; close aborts it, and the caller's signal is forwarded
2638
+ * to it.
2639
+ */
2392
2640
  async connectOnce(signal) {
2393
- await this.setState("connecting" /* Connecting */, void 0, signal);
2641
+ const attempts = new AbortController();
2642
+ const forwardAbort = () => attempts.abort();
2643
+ signal?.addEventListener("abort", forwardAbort, { once: true });
2644
+ this.connectAbort = attempts;
2645
+ this.setState("connecting" /* Connecting */, void 0, signal);
2394
2646
  try {
2395
- const connection = await this.connectWithReconnect(signal);
2647
+ const connection = await this.connectWithReconnect(attempts.signal);
2396
2648
  if (this.closeRequested) {
2397
2649
  try {
2398
2650
  await connection.close(signal);
@@ -2407,41 +2659,61 @@ var ZlinkStreamConnectorLifecycle = class {
2407
2659
  }
2408
2660
  this.currentConnection = connection;
2409
2661
  this.connectionGeneration += 1;
2662
+ const generation = this.connectionGeneration;
2663
+ this.frameSender.open(connection, (error) => {
2664
+ void this.disconnectForTransportFailure(error, connection, generation);
2665
+ });
2410
2666
  this.disconnectedPublished = false;
2411
2667
  this.receivedMessages.resetForNewConnection();
2412
2668
  this.lastInboundAt = Date.now();
2413
- await this.setState("connected" /* Connected */, void 0, signal);
2669
+ this.setState("connected" /* Connected */, void 0, signal);
2414
2670
  this.startHeartbeat();
2415
2671
  this.startReceiveLoop();
2416
2672
  } catch (cause) {
2417
2673
  if (this.closeRequested) {
2418
2674
  const message = cause instanceof Error ? cause.message : "Connector closed while connecting.";
2419
- const error2 = toStreamError(cause, "disconnected" /* Disconnected */, message);
2420
- throw new ZlinkStreamException(error2);
2675
+ throw connectorError("disconnected" /* Disconnected */, message, cause);
2676
+ }
2677
+ if (signal?.aborted === true) {
2678
+ this.setState("disconnected" /* Disconnected */, void 0, signal);
2679
+ throwIfAborted(signal);
2421
2680
  }
2422
2681
  const error = toStreamError(cause, "connectTimeout" /* ConnectTimeout */, "Connect failed.");
2423
- this.closeReasonValue ??= "TransportError";
2424
- void this.setState("disconnected" /* Disconnected */, error, signal);
2425
- this.publishDisconnectedWithoutWaiting(signal);
2682
+ this.closeReasonValue = "TransportError";
2683
+ this.setState("disconnected" /* Disconnected */, error, signal);
2684
+ this.disconnectedPublished = false;
2685
+ this.publishDisconnected(signal);
2426
2686
  throw new ZlinkStreamException(error);
2687
+ } finally {
2688
+ signal?.removeEventListener("abort", forwardAbort);
2689
+ this.connectAbort = void 0;
2427
2690
  }
2428
2691
  }
2692
+ /**
2693
+ * Spec stream-connector 32 §7: a `close` called outside the registered
2694
+ * handlers and callbacks returns once the close work has ended. One called
2695
+ * from inside them returns right after starting it; the result goes to the
2696
+ * outside caller, so a callback never waits for the close of the path that
2697
+ * is running it.
2698
+ */
2429
2699
  async close(signal) {
2430
- this.closeReasonValue = "ClientClose";
2431
- this.closeRequested = true;
2432
- if (this.closeTask !== void 0) {
2433
- return await this.closeTask;
2700
+ if (this.currentConnection !== void 0 || this.closeReasonValue === void 0) {
2701
+ this.closeReasonValue = "ClientClose";
2434
2702
  }
2435
- if (this.currentState === "closed" /* Closed */) {
2703
+ this.closeRequested = true;
2704
+ if (this.closeTask === void 0 && this.currentState === "closed" /* Closed */) {
2436
2705
  return;
2437
2706
  }
2438
- this.closeTask = this.closeOnce(signal).finally(() => {
2707
+ const closeTask = this.closeTask ??= this.closeOnce(signal).finally(() => {
2439
2708
  this.closeTask = void 0;
2440
2709
  });
2441
- return await this.closeTask;
2710
+ if (this.receivedMessages.dispatching) {
2711
+ void closeTask.catch(() => void 0);
2712
+ return;
2713
+ }
2714
+ return await closeTask;
2442
2715
  }
2443
2716
  async serverClosing(reason) {
2444
- this.closeReasonValue = reason;
2445
2717
  const error = {
2446
2718
  code: "disconnected" /* Disconnected */,
2447
2719
  message: `Server closed the session: ${reason}.`
@@ -2449,39 +2721,29 @@ var ZlinkStreamConnectorLifecycle = class {
2449
2721
  await this.disconnectForTransportFailure(
2450
2722
  error,
2451
2723
  this.currentConnection,
2452
- this.connectionGeneration
2724
+ this.connectionGeneration,
2725
+ reason
2453
2726
  );
2454
2727
  }
2455
2728
  async closeOnce(signal) {
2729
+ this.connectAbort?.abort();
2456
2730
  await this.connectTask?.catch(() => void 0);
2457
2731
  await this.disconnectTask?.catch(() => void 0);
2458
- const connection = this.currentConnection;
2459
- this.stopHeartbeat();
2460
- this.stopReceiveLoop();
2461
- this.currentConnection = void 0;
2462
2732
  const errors = [];
2463
2733
  if (this.lateConnectCleanupError !== void 0) {
2464
2734
  errors.push(this.lateConnectCleanupError);
2465
2735
  this.lateConnectCleanupError = void 0;
2466
2736
  }
2467
2737
  try {
2468
- await this.frameSender.drain(signal);
2469
- } catch (error) {
2470
- errors.push(error);
2471
- }
2472
- try {
2473
- await connection?.close(signal);
2738
+ await this.tearDownConnection(
2739
+ { code: "disconnected" /* Disconnected */, message: "Connector closed." },
2740
+ signal
2741
+ );
2474
2742
  } catch (error) {
2475
2743
  errors.push(error);
2476
2744
  }
2477
- this.pendingRequests.failAll({
2478
- code: "disconnected" /* Disconnected */,
2479
- message: "Connector closed."
2480
- });
2481
- this.receivedMessages.connectionEnded();
2482
- this.actors.closeAll(signal);
2483
- this.queueState("closed" /* Closed */, void 0, signal);
2484
- this.queueDisconnected(signal);
2745
+ this.setState("closed" /* Closed */, void 0, signal);
2746
+ this.publishDisconnected(signal);
2485
2747
  if (errors.length === 1) throw errors[0];
2486
2748
  if (errors.length > 1) throw new AggregateError(errors, "Stream connector close failed.");
2487
2749
  }
@@ -2499,7 +2761,7 @@ var ZlinkStreamConnectorLifecycle = class {
2499
2761
  if (this.options.dispatchMode !== "immediate" /* Immediate */) {
2500
2762
  await this.settleReceiveLoop();
2501
2763
  }
2502
- await this.receivedMessages.pump();
2764
+ this.receivedMessages.pump();
2503
2765
  }
2504
2766
  connectionForSend() {
2505
2767
  if (this.currentConnection === void 0 || this.currentState !== "connected" /* Connected */) {
@@ -2512,8 +2774,7 @@ var ZlinkStreamConnectorLifecycle = class {
2512
2774
  const result = await this.receiveDispatcher.readAndDispatch(
2513
2775
  connection,
2514
2776
  signal,
2515
- () => this.isCurrentConnection(connection, generation),
2516
- () => this.connectionForSend()
2777
+ () => this.isCurrentConnection(connection, generation)
2517
2778
  );
2518
2779
  if (result.inbound && this.isCurrentConnection(connection, generation)) {
2519
2780
  this.lastInboundAt = Date.now();
@@ -2522,7 +2783,10 @@ var ZlinkStreamConnectorLifecycle = class {
2522
2783
  }
2523
2784
  async connectWithReconnect(signal) {
2524
2785
  let attempt = 0;
2525
- let delayMs = this.options.reconnect.initialDelayMs;
2786
+ let delayMs = Math.min(
2787
+ this.options.reconnect.initialDelayMs,
2788
+ this.options.reconnect.maxDelayMs
2789
+ );
2526
2790
  let lastError;
2527
2791
  const maxAttempts = this.options.reconnect.enabled ? this.options.reconnect.maxAttempts : 1;
2528
2792
  const unlimited = maxAttempts === null;
@@ -2532,10 +2796,10 @@ var ZlinkStreamConnectorLifecycle = class {
2532
2796
  return await this.options.transportFactory.connect(this.options, signal);
2533
2797
  } catch (cause) {
2534
2798
  lastError = toStreamError(cause, "connectTimeout" /* ConnectTimeout */, "Connect failed.");
2535
- if (!this.options.reconnect.enabled || !unlimited && attempt >= maxAttempts) {
2799
+ if (this.closeRequested || !this.options.reconnect.enabled || !unlimited && attempt >= maxAttempts) {
2536
2800
  break;
2537
2801
  }
2538
- await this.setState("reconnecting" /* Reconnecting */, lastError, signal);
2802
+ this.setState("reconnecting" /* Reconnecting */, lastError, signal);
2539
2803
  await delay(randomizedDelay(delayMs), signal);
2540
2804
  delayMs = Math.min(
2541
2805
  this.options.reconnect.maxDelayMs,
@@ -2603,12 +2867,8 @@ var ZlinkStreamConnectorLifecycle = class {
2603
2867
  }
2604
2868
  } catch (cause) {
2605
2869
  if (signal.aborted) return;
2606
- const error = toStreamError(
2607
- cause,
2608
- "frameDecodeFailed" /* FrameDecodeFailed */,
2609
- "Receive loop failed."
2610
- );
2611
- await this.disconnectForTransportFailure(error, connection, generation);
2870
+ if (!(cause instanceof ZlinkStreamConnectionEnd)) throw cause;
2871
+ await this.disconnectForTransportFailure(cause.error, connection, generation);
2612
2872
  } finally {
2613
2873
  this.receiveLoopSleeping = false;
2614
2874
  this.releaseReceiveLoopSettled();
@@ -2664,36 +2924,35 @@ var ZlinkStreamConnectorLifecycle = class {
2664
2924
  return;
2665
2925
  }
2666
2926
  if (Date.now() - this.lastInboundAt > this.options.heartbeat.timeoutMs) {
2667
- this.closeReasonValue = "HeartbeatTimeout";
2668
2927
  const error = { code: "disconnected" /* Disconnected */, message: "Heartbeat timed out." };
2669
2928
  await this.disconnectForTransportFailure(
2670
2929
  error,
2671
2930
  this.currentConnection,
2672
- this.connectionGeneration
2931
+ this.connectionGeneration,
2932
+ "HeartbeatTimeout"
2673
2933
  );
2674
2934
  return;
2675
2935
  }
2676
- const connection = this.currentConnection;
2677
- const generation = this.connectionGeneration;
2678
- try {
2679
- await this.frameSender.sendControl(this.connectionForSend(), ZLINK_STREAM_HEARTBEAT_PING);
2680
- } catch (cause) {
2681
- const error = toStreamError(cause, "sendFailed" /* SendFailed */, "Heartbeat send failed.");
2682
- await this.disconnectForTransportFailure(error, connection, generation);
2683
- }
2936
+ await this.frameSender.sendControl(this.connectionForSend(), ZLINK_STREAM_HEARTBEAT_PING);
2684
2937
  }
2685
- async disconnectForTransportFailure(error, origin, generation) {
2686
- this.closeReasonValue ??= "TransportError";
2938
+ /**
2939
+ * @param reason Given only where the reason is not read from `error`: a
2940
+ * server `session-closing` and the heartbeat timeout. Every other ending
2941
+ * takes it from {@link closeReasonFor}.
2942
+ */
2943
+ async disconnectForTransportFailure(error, origin, generation, reason = closeReasonFor(error)) {
2687
2944
  if (this.closeRequested || this.currentState === "closed" /* Closed */) {
2688
2945
  return;
2689
2946
  }
2690
2947
  if (origin !== void 0 && !this.isCurrentConnection(origin, generation)) {
2691
2948
  return;
2692
2949
  }
2950
+ this.closeReasonValue = reason;
2693
2951
  if (this.disconnectTask !== void 0) {
2694
2952
  return await this.disconnectTask;
2695
2953
  }
2696
- this.disconnectTask = this.tearDownConnection(error).finally(() => {
2954
+ this.disconnectTask = this.tearDownConnection(error).catch(() => {
2955
+ }).finally(() => {
2697
2956
  this.disconnectTask = void 0;
2698
2957
  });
2699
2958
  await this.disconnectTask;
@@ -2702,19 +2961,30 @@ var ZlinkStreamConnectorLifecycle = class {
2702
2961
  isCurrentConnection(connection, generation) {
2703
2962
  return !this.closeRequested && this.currentConnection === connection && this.connectionGeneration === generation;
2704
2963
  }
2705
- /** Transport teardown only — no application callback runs from here. */
2706
- async tearDownConnection(error) {
2964
+ /**
2965
+ * Ends the current connection, for close and for transport loss alike. No
2966
+ * application callback runs from here. Spec stream-connector 32 §7 and §9:
2967
+ * every operation the ending connection fails (the frames it has not
2968
+ * written, the one it is writing and the pending requests) fails with
2969
+ * `Disconnected`, whatever ended it; the cause stays in the close reason.
2970
+ * The transport is closed without waiting for the frames.
2971
+ */
2972
+ async tearDownConnection(error, signal) {
2707
2973
  this.stopHeartbeat();
2708
2974
  this.stopReceiveLoop();
2709
2975
  const connection = this.currentConnection;
2710
2976
  this.currentConnection = void 0;
2711
- this.pendingRequests.failAll(error);
2712
- this.receivedMessages.connectionEnded();
2713
- this.actors.closeAll();
2714
- try {
2715
- await connection?.close();
2716
- } catch {
2977
+ const disconnected = { code: "disconnected" /* Disconnected */, message: error.message };
2978
+ if (connection !== void 0) {
2979
+ this.frameSender.failUnwritten(
2980
+ connection,
2981
+ connectorError(disconnected.code, disconnected.message)
2982
+ );
2717
2983
  }
2984
+ this.pendingRequests.failAll(disconnected);
2985
+ this.receivedMessages.connectionEnded();
2986
+ this.actors.closeAll(signal);
2987
+ await connection?.close(signal);
2718
2988
  }
2719
2989
  /**
2720
2990
  * Runs once the teardown promise has settled, so a handler reached from here
@@ -2726,8 +2996,8 @@ var ZlinkStreamConnectorLifecycle = class {
2726
2996
  */
2727
2997
  async announceDisconnect(error) {
2728
2998
  if (this.closeRequested) return;
2729
- this.queueState("disconnected" /* Disconnected */, error);
2730
- this.queueDisconnected();
2999
+ this.setState("disconnected" /* Disconnected */, error);
3000
+ this.publishDisconnected();
2731
3001
  if (this.shouldReconnect()) {
2732
3002
  queueMicrotask(() => {
2733
3003
  void this.connect().catch(() => void 0);
@@ -2737,78 +3007,36 @@ var ZlinkStreamConnectorLifecycle = class {
2737
3007
  shouldReconnect() {
2738
3008
  return this.options.reconnect.enabled && !this.closeRequested;
2739
3009
  }
2740
- // Synchronous test-and-set, taken before any await, so the single disconnect
2741
- // notification the spec promises is claimed by exactly one caller even when
2742
- // the publishing itself is deferred.
2743
- claimDisconnectedPublish() {
2744
- if (this.disconnectedPublished) return false;
2745
- this.disconnectedPublished = true;
2746
- return true;
2747
- }
2748
- async publishDisconnectedOnce(signal) {
2749
- if (!this.claimDisconnectedPublish()) return;
2750
- await this.events.publishDisconnected(signal);
2751
- }
2752
3010
  /**
2753
- * Spec stream-connector 32 §7: the connector runs the registered handler, it
2754
- * does not wait for it to finish. `close` returns once its own work is done —
2755
- * the drain, the transport close, the pending requests — having run the
2756
- * disconnect handler but without looking at whether that handler has ended.
2757
- *
2758
- * Starting the publish without awaiting it still runs every handler right
2759
- * here: an `async` function body runs synchronously up to its first `await`,
2760
- * and `publishDisconnected` reaches each handler before that point. So the
2761
- * handler has run by the time `close` returns, which is what the spec asks
2762
- * for, while a handler that calls `close` no longer waits for the very
2763
- * `closeTask` it is running inside. Java and C++ hand the handler to a queue
2764
- * and return the same way.
2765
- *
2766
- * `claimDisconnectedPublish` is taken by the same call and before any await,
2767
- * so deferring the completion never turns the one notification into two.
2768
- *
2769
- * Nothing awaits the promise, so a rejection would reach the process as an
2770
- * unhandled rejection and kill it. Handler failures are already contained
2771
- * where they were before this call stopped waiting — the `Promise.allSettled`
2772
- * in `publishDisconnected` — and this `catch` covers what is left rather than
2773
- * reporting the same failure a second time on the error surface.
3011
+ * Spec stream-connector 32 §6 and §7: one disconnect notification per
3012
+ * lifecycle event, handed to the dispatch queue. The connector does not wait
3013
+ * for the handler; `Manual` runs it when the application pumps dispatch.
3014
+ * The flag is tested and set synchronously, so concurrent callers for the
3015
+ * same event claim it once.
2774
3016
  */
2775
- publishDisconnectedWithoutWaiting(signal) {
2776
- void this.publishDisconnectedOnce(signal).catch(() => void 0);
2777
- }
2778
- queueDisconnected(signal) {
2779
- if (!this.claimDisconnectedPublish()) return;
2780
- this.receivedMessages.enqueueCallback(() => {
2781
- void this.events.publishDisconnected(signal).catch(() => void 0);
2782
- });
2783
- }
2784
- queueState(current, error, signal) {
2785
- const change = this.transitionState(current, error);
2786
- if (change === void 0) return;
2787
- this.receivedMessages.enqueueCallback(() => {
2788
- void this.publishStateChange(change, signal).catch(() => void 0);
2789
- });
2790
- }
2791
- async setState(current, error, signal) {
2792
- const change = this.transitionState(current, error);
2793
- if (change === void 0) return;
2794
- await this.publishStateChange(change, signal);
3017
+ publishDisconnected(signal) {
3018
+ if (this.disconnectedPublished) return;
3019
+ this.disconnectedPublished = true;
3020
+ this.events.publishDisconnected(signal);
2795
3021
  }
2796
- transitionState(current, error) {
3022
+ setState(current, error, signal) {
2797
3023
  const previous = this.currentState;
2798
3024
  this.currentState = current;
2799
- if (previous === current && error === void 0) {
2800
- return void 0;
2801
- }
2802
- return { previous, current, error };
2803
- }
2804
- async publishStateChange(change, signal) {
2805
- await this.events.publishStateChanged(change, signal);
2806
- if (change.error !== void 0) await this.events.publishError(change.error, signal);
3025
+ if (previous === current && error === void 0) return;
3026
+ this.events.publishStateChanged({ previous, current, error }, signal);
3027
+ if (error !== void 0) this.events.publishError(error, signal);
2807
3028
  }
2808
3029
  };
2809
3030
 
2810
3031
  // packages/stream-connector/src/Runtime/ZlinkStreamConnectorEvents.ts
2811
3032
  var ZlinkStreamConnectorEvents = class {
3033
+ /**
3034
+ * @param enqueueCallback Queues a callback that calls the number of handlers
3035
+ * `callbacks` reports for the handlers registered at that moment.
3036
+ */
3037
+ constructor(enqueueCallback) {
3038
+ this.enqueueCallback = enqueueCallback;
3039
+ }
2812
3040
  errorHandlers = /* @__PURE__ */ new Set();
2813
3041
  disconnectedHandlers = /* @__PURE__ */ new Set();
2814
3042
  stateHandlers = /* @__PURE__ */ new Set();
@@ -2824,23 +3052,78 @@ var ZlinkStreamConnectorEvents = class {
2824
3052
  this.stateHandlers.add(handler);
2825
3053
  return subscription(() => this.stateHandlers.delete(handler));
2826
3054
  }
2827
- async publishError(error, signal) {
2828
- await this.publish(
2829
- Array.from(this.errorHandlers).map((handler) => () => handler(error, signal))
3055
+ publishError(error, signal) {
3056
+ this.publish(this.errorHandlers, (handler) => handler(error, signal), signal);
3057
+ }
3058
+ publishDisconnected(signal) {
3059
+ this.publish(this.disconnectedHandlers, (handler) => handler(signal), signal);
3060
+ }
3061
+ publishStateChanged(change, signal) {
3062
+ this.publish(this.stateHandlers, (handler) => handler(change, signal), signal);
3063
+ }
3064
+ /**
3065
+ * Spec stream-connector 32 §7: the connector runs a registered callback and
3066
+ * does not wait for it to finish. A thrown or rejected failure reaches the
3067
+ * error handlers as `UserCallbackFailed`.
3068
+ */
3069
+ runUserCallback(run, failureMessage, signal) {
3070
+ this.invoke(
3071
+ run,
3072
+ (cause) => this.publishError(
3073
+ { code: "userCallbackFailed" /* UserCallbackFailed */, message: failureMessage, cause },
3074
+ signal
3075
+ )
2830
3076
  );
2831
3077
  }
2832
- async publishDisconnected(signal) {
2833
- await this.publish(
2834
- Array.from(this.disconnectedHandlers).map((handler) => () => handler(signal))
3078
+ publish(handlers, invoke, signal) {
3079
+ this.enqueueCallback(
3080
+ () => {
3081
+ for (const handler of Array.from(handlers)) {
3082
+ const report = (cause) => this.reportFailure(
3083
+ cause,
3084
+ signal,
3085
+ handlers === this.errorHandlers ? handler : void 0
3086
+ );
3087
+ this.invoke(() => invoke(handler), report);
3088
+ }
3089
+ },
3090
+ () => handlers.size
2835
3091
  );
2836
3092
  }
2837
- async publishStateChanged(change, signal) {
2838
- await this.publish(
2839
- Array.from(this.stateHandlers).map((handler) => () => handler(change, signal))
3093
+ reportFailure(cause, signal, failedHandler) {
3094
+ const error = {
3095
+ code: "userCallbackFailed" /* UserCallbackFailed */,
3096
+ message: "Connector event handler failed.",
3097
+ cause
3098
+ };
3099
+ if (failedHandler === void 0) {
3100
+ this.publishError(error, signal);
3101
+ return;
3102
+ }
3103
+ this.reportToRemaining(error, signal, /* @__PURE__ */ new Set([failedHandler]));
3104
+ }
3105
+ reportToRemaining(error, signal, attempted) {
3106
+ const remaining = Array.from(this.errorHandlers).filter(
3107
+ (candidate) => !attempted.has(candidate)
3108
+ );
3109
+ if (remaining.length === 0) return;
3110
+ const nextAttempted = /* @__PURE__ */ new Set([...attempted, ...remaining]);
3111
+ this.enqueueCallback(
3112
+ () => {
3113
+ for (const handler of remaining) {
3114
+ const report = (cause) => this.reportToRemaining({ ...error, cause }, signal, nextAttempted);
3115
+ this.invoke(() => handler(error, signal), report);
3116
+ }
3117
+ },
3118
+ () => remaining.length
2840
3119
  );
2841
3120
  }
2842
- async publish(handlers) {
2843
- await Promise.allSettled(handlers.map(async (handler) => handler()));
3121
+ invoke(run, report) {
3122
+ try {
3123
+ Promise.resolve(run()).catch(report);
3124
+ } catch (cause) {
3125
+ report(cause);
3126
+ }
2844
3127
  }
2845
3128
  };
2846
3129
 
@@ -2878,11 +3161,7 @@ var BrowserWebSocketConnection = class {
2878
3161
  if (this.closed || this.socket.readyState !== 1) {
2879
3162
  throw connectorError("disconnected" /* Disconnected */, "Remote stream is not connected.");
2880
3163
  }
2881
- try {
2882
- this.socket.send(frame);
2883
- } catch (cause) {
2884
- throw connectorError("sendFailed" /* SendFailed */, "Send failed.", cause);
2885
- }
3164
+ this.socket.send(frame);
2886
3165
  }
2887
3166
  async read(signal) {
2888
3167
  throwIfAborted(signal);
@@ -2900,18 +3179,19 @@ var BrowserWebSocketConnection = class {
2900
3179
  await this.waitForMessage(signal);
2901
3180
  }
2902
3181
  }
3182
+ /**
3183
+ * Spec stream-connector 32 §7: closing does not wait for the peer to read or
3184
+ * answer. `WebSocket.close` sends the close frame after the data already
3185
+ * handed to `send`, so a frame whose write completed is not torn.
3186
+ */
2903
3187
  async close(signal) {
2904
3188
  throwIfAborted(signal);
3189
+ this.removeListeners();
2905
3190
  if (!this.closed) {
2906
3191
  this.closed = true;
2907
3192
  this.socket.close();
2908
- this.wakeReader();
2909
- }
2910
- try {
2911
- await waitForClose(this.socket, signal);
2912
- } finally {
2913
- this.removeListeners();
2914
3193
  }
3194
+ this.wakeReader();
2915
3195
  }
2916
3196
  onMessage = (event) => {
2917
3197
  try {
@@ -2989,21 +3269,6 @@ var BrowserWebSocketConnection = class {
2989
3269
  return message;
2990
3270
  }
2991
3271
  };
2992
- function waitForClose(socket, signal) {
2993
- if (socket.readyState === 3) return Promise.resolve();
2994
- return new Promise((resolve, reject) => {
2995
- const onClose = () => finish();
2996
- const onAbort = () => finish(connectorError("disconnected" /* Disconnected */, "Close canceled."));
2997
- const finish = (error) => {
2998
- socket.removeEventListener("close", onClose);
2999
- signal?.removeEventListener("abort", onAbort);
3000
- if (error === void 0) resolve();
3001
- else reject(error);
3002
- };
3003
- socket.addEventListener("close", onClose, { once: true });
3004
- signal?.addEventListener("abort", onAbort, { once: true });
3005
- });
3006
- }
3007
3272
  async function waitForOpen(socket, connectTimeoutMs, signal) {
3008
3273
  throwIfAborted(signal);
3009
3274
  await new Promise((resolve, reject) => {
@@ -3053,7 +3318,9 @@ var DefaultZlinkStreamConnector = class {
3053
3318
  static heartbeatPongName = ZLINK_STREAM_HEARTBEAT_PONG;
3054
3319
  receivedMessages;
3055
3320
  lifecycle;
3056
- events = new ZlinkStreamConnectorEvents();
3321
+ events = new ZlinkStreamConnectorEvents(
3322
+ (callback, callbacks) => this.receivedMessages.enqueueCallback(callback, callbacks)
3323
+ );
3057
3324
  correlationCounter = 0n;
3058
3325
  pendingRequests = new ZlinkStreamPendingRequests();
3059
3326
  frameSender;
@@ -3100,7 +3367,7 @@ var DefaultZlinkStreamConnector = class {
3100
3367
  return this.lifecycle.state;
3101
3368
  }
3102
3369
  get pendingDispatchCount() {
3103
- return this.pendingRequests.count;
3370
+ return this.receivedMessages.pendingCallbacks;
3104
3371
  }
3105
3372
  get actors() {
3106
3373
  return this.boundActors.snapshot;
@@ -3157,7 +3424,12 @@ var DefaultZlinkStreamConnector = class {
3157
3424
  }
3158
3425
  request(payload, messageType) {
3159
3426
  const encoded = this.encodePayload(payload, messageType);
3160
- return new ZlinkStreamRequestBuilder(this, this.resolveNameOrDefault(encoded), encoded);
3427
+ return new ZlinkStreamRequestBuilder(
3428
+ this,
3429
+ this.resolveNameOrDefault(encoded),
3430
+ encoded,
3431
+ (callback) => this.receivedMessages.enqueueCallback(callback)
3432
+ );
3161
3433
  }
3162
3434
  on(nameOrType, handler, messageType) {
3163
3435
  const encodedHandler = (message, signal) => handler(
@@ -3190,29 +3462,25 @@ var DefaultZlinkStreamConnector = class {
3190
3462
  this,
3191
3463
  this.resolveNameOrDefault(encoded),
3192
3464
  encoded,
3465
+ (callback) => this.receivedMessages.enqueueCallback(callback),
3193
3466
  actor.slot,
3194
3467
  () => actor.ensureBound()
3195
3468
  );
3196
3469
  }
3197
3470
  onActorMessage(actor, nameOrType, handler, messageType) {
3198
- const encodedHandler = (message, signal) => {
3199
- if (message[zlinkStreamActorBinding] !== actor) {
3200
- return;
3201
- }
3202
- return handler(
3203
- {
3204
- name: message.name,
3205
- metadata: message.metadata,
3206
- payload: this.decodePayload(
3207
- message.payload,
3208
- messageType ?? (typeof nameOrType === "function" ? nameOrType : void 0)
3209
- ),
3210
- actorId: message.actorId
3211
- },
3212
- signal
3213
- );
3214
- };
3215
- return this.receivedMessages.on(this.observedName(nameOrType), encodedHandler);
3471
+ const encodedHandler = (message, signal) => handler(
3472
+ {
3473
+ name: message.name,
3474
+ metadata: message.metadata,
3475
+ payload: this.decodePayload(
3476
+ message.payload,
3477
+ messageType ?? (typeof nameOrType === "function" ? nameOrType : void 0)
3478
+ ),
3479
+ actorId: message.actorId
3480
+ },
3481
+ signal
3482
+ );
3483
+ return this.receivedMessages.on(this.observedName(nameOrType), encodedHandler, actor);
3216
3484
  }
3217
3485
  waitFor(nameOrType) {
3218
3486
  return new ZlinkStreamWaitBuilder(this, this.observedName(nameOrType));
@@ -3334,7 +3602,7 @@ var DefaultZlinkStreamConnector = class {
3334
3602
  }
3335
3603
  return payload;
3336
3604
  }
3337
- async sendEncoded(kind, name, payload, metadata, compress, requestSeq, signal, correlationId, actorSlot) {
3605
+ async sendEncoded(kind, name, payload, metadata, compress, requestSeq, signal, correlationId, actorSlot, expiry, onAccepted) {
3338
3606
  await this.frameSender.send(
3339
3607
  this.lifecycle.connectionForSend(),
3340
3608
  kind,
@@ -3345,7 +3613,9 @@ var DefaultZlinkStreamConnector = class {
3345
3613
  requestSeq,
3346
3614
  signal,
3347
3615
  correlationId,
3348
- actorSlot
3616
+ actorSlot,
3617
+ expiry,
3618
+ onAccepted
3349
3619
  );
3350
3620
  }
3351
3621
  /**
@@ -3368,37 +3638,48 @@ var DefaultZlinkStreamConnector = class {
3368
3638
  }
3369
3639
  };
3370
3640
  let pending;
3641
+ let stopCancellation;
3371
3642
  try {
3372
3643
  throwIfAborted(signal);
3373
3644
  for (const handler of Array.from(this.requestSendingHandlers)) {
3374
3645
  try {
3375
3646
  handler(sendingContext);
3376
3647
  } catch (cause) {
3377
- queueMicrotask(() => {
3378
- void this.events.publishError(
3379
- toStreamError(
3380
- cause,
3381
- "userCallbackFailed" /* UserCallbackFailed */,
3382
- "Request sending hook failed."
3383
- ),
3384
- signal
3385
- );
3386
- });
3648
+ this.events.publishError(
3649
+ {
3650
+ code: "userCallbackFailed" /* UserCallbackFailed */,
3651
+ message: "Request sending hook failed.",
3652
+ cause
3653
+ },
3654
+ signal
3655
+ );
3387
3656
  }
3388
3657
  }
3389
3658
  pending = this.pendingRequests.create(name, timeoutMs);
3390
- await this.sendEncoded(
3659
+ const accepted = pending;
3660
+ const write = this.sendEncoded(
3391
3661
  2 /* Request */,
3392
3662
  name,
3393
3663
  payload,
3394
3664
  requestMetadata,
3395
3665
  compress,
3396
- pending.requestSeq,
3666
+ accepted.requestSeq,
3397
3667
  signal,
3398
3668
  this.nextCorrelationId(),
3399
- actorSlot
3669
+ actorSlot,
3670
+ accepted.promise,
3671
+ accepted.startTimeout
3400
3672
  );
3401
- const reply = await pending.promise;
3673
+ const canceled = new Promise((_, reject) => {
3674
+ const onAbort = () => reject(connectorError("disconnected" /* Disconnected */, "Operation canceled."));
3675
+ signal?.addEventListener("abort", onAbort, { once: true });
3676
+ stopCancellation = () => signal?.removeEventListener("abort", onAbort);
3677
+ });
3678
+ const reply = await Promise.race([
3679
+ write.then(() => accepted.promise),
3680
+ accepted.promise,
3681
+ canceled
3682
+ ]);
3402
3683
  this.publishReplyReceived(
3403
3684
  {
3404
3685
  requestPacketName: name,
@@ -3412,6 +3693,7 @@ var DefaultZlinkStreamConnector = class {
3412
3693
  return reply.payload;
3413
3694
  } catch (error) {
3414
3695
  if (pending !== void 0) this.pendingRequests.cancel(pending.requestSeq);
3696
+ if (signal?.aborted === true) throw error;
3415
3697
  this.publishReplyReceived(
3416
3698
  {
3417
3699
  requestPacketName: name,
@@ -3423,26 +3705,24 @@ var DefaultZlinkStreamConnector = class {
3423
3705
  signal
3424
3706
  );
3425
3707
  throw error;
3708
+ } finally {
3709
+ stopCancellation?.();
3426
3710
  }
3427
3711
  }
3428
3712
  publishReplyReceived(context, signal) {
3429
3713
  if (this.replyReceivedHandlers.size === 0) return;
3430
- this.receivedMessages.enqueueCallback(async () => {
3431
- for (const handler of Array.from(this.replyReceivedHandlers)) {
3432
- try {
3433
- await handler(context, signal);
3434
- } catch (cause) {
3435
- await this.events.publishError(
3436
- toStreamError(
3437
- cause,
3438
- "userCallbackFailed" /* UserCallbackFailed */,
3439
- "Reply received hook failed."
3440
- ),
3714
+ this.receivedMessages.enqueueCallback(
3715
+ () => {
3716
+ for (const handler of Array.from(this.replyReceivedHandlers)) {
3717
+ this.events.runUserCallback(
3718
+ () => handler(context, signal),
3719
+ "Reply received hook failed.",
3441
3720
  signal
3442
3721
  );
3443
3722
  }
3444
- }
3445
- });
3723
+ },
3724
+ () => this.replyReceivedHandlers.size
3725
+ );
3446
3726
  }
3447
3727
  resolveNameOrDefault(payload) {
3448
3728
  if (payload.messageType === void 0) {