@actuallab/rpc 14.3.34 → 14.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -879,7 +879,8 @@ var RpcCallTimeouts = class _RpcCallTimeouts {
879
879
  static None = new _RpcCallTimeouts(Infinity, Infinity);
880
880
  /** Command default: 1.5 s connect / 10 s run (.NET `RpcCallTimeouts.Default.Command`). */
881
881
  static Command = new _RpcCallTimeouts(1500, 1e4);
882
- /** Max time a call may wait for the connection before it is sent. */
882
+ /** Max time a call may wait for a connection - before it is sent, and again for the
883
+ * duration of any outage it is still pending through. */
883
884
  connectTimeoutMs;
884
885
  /** Max time a sent call may stay unanswered before it is failed. */
885
886
  runTimeoutMs;
@@ -1078,13 +1079,17 @@ var RpcSystemCallHandler = class {
1078
1079
  case RpcSystemCalls.keepAlive: {
1079
1080
  peer.notifyKeepAliveReceived();
1080
1081
  const keepAliveIds = args[0];
1081
- if (Array.isArray(keepAliveIds) && keepAliveIds.length > 0 && peer.connection !== void 0) {
1082
+ if (Array.isArray(keepAliveIds) && keepAliveIds.length > 0 && peer.wireConnection !== void 0) {
1082
1083
  const unknownIds = [];
1083
1084
  for (const id of keepAliveIds)
1084
1085
  if (peer.sharedObjects.get(id) === void 0)
1085
1086
  unknownIds.push(id);
1086
1087
  if (unknownIds.length > 0)
1087
- peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, unknownIds);
1088
+ peer.hub.systemCallSender.disconnect(
1089
+ peer.wireConnection,
1090
+ peer.serializationFormat,
1091
+ unknownIds
1092
+ );
1088
1093
  }
1089
1094
  break;
1090
1095
  }
@@ -1130,16 +1135,24 @@ var RpcSystemCallHandler = class {
1130
1135
  const sender = peer.sharedObjects.get(relatedId);
1131
1136
  if (sender)
1132
1137
  sender.onAck(args[0], args[1]);
1133
- else if (peer.connection !== void 0)
1134
- peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, [relatedId]);
1138
+ else if (peer.wireConnection !== void 0)
1139
+ peer.hub.systemCallSender.disconnect(
1140
+ peer.wireConnection,
1141
+ peer.serializationFormat,
1142
+ [relatedId]
1143
+ );
1135
1144
  break;
1136
1145
  }
1137
1146
  case RpcSystemCalls.ackEnd: {
1138
1147
  const sender = peer.sharedObjects.get(relatedId);
1139
1148
  if (sender)
1140
1149
  sender.onAckEnd(args[0]);
1141
- else if (peer.connection !== void 0)
1142
- peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, [relatedId]);
1150
+ else if (peer.wireConnection !== void 0)
1151
+ peer.hub.systemCallSender.disconnect(
1152
+ peer.wireConnection,
1153
+ peer.serializationFormat,
1154
+ [relatedId]
1155
+ );
1143
1156
  break;
1144
1157
  }
1145
1158
  case RpcSystemCalls.disconnect: {
@@ -1168,12 +1181,13 @@ var RpcSystemCallHandler = class {
1168
1181
  * with the same `RelatedId`.
1169
1182
  */
1170
1183
  _handleReconnect(relatedId, args, peer) {
1171
- if (peer.connection === void 0) return;
1184
+ const conn = peer.wireConnection;
1185
+ if (conn === void 0) return;
1172
1186
  const handshakeIndex = args[0];
1173
1187
  if (typeof handshakeIndex !== "number" || handshakeIndex !== peer.ownHandshakeIndex) {
1174
1188
  const got = typeof handshakeIndex === "number" ? handshakeIndex : `<${typeof handshakeIndex}>`;
1175
1189
  peer.hub.systemCallSender.error(
1176
- peer.connection,
1190
+ conn,
1177
1191
  peer.serializationFormat,
1178
1192
  relatedId,
1179
1193
  new Error(
@@ -1184,7 +1198,7 @@ var RpcSystemCallHandler = class {
1184
1198
  }
1185
1199
  if (!peer.tryClaimReconnect()) {
1186
1200
  peer.hub.systemCallSender.error(
1187
- peer.connection,
1201
+ conn,
1188
1202
  peer.serializationFormat,
1189
1203
  relatedId,
1190
1204
  new Error("TooLateToReconnect: this connection was already reconnected")
@@ -1215,7 +1229,7 @@ var RpcSystemCallHandler = class {
1215
1229
  }
1216
1230
  const responseBytes = IncreasingSeqCompressor.serialize(unknownIds);
1217
1231
  const responseValue = peer.serializationFormat.isBinary ? responseBytes : base64Encode(responseBytes);
1218
- peer.hub.systemCallSender.ok(peer.connection, peer.serializationFormat, relatedId, responseValue);
1232
+ peer.hub.systemCallSender.ok(conn, peer.serializationFormat, relatedId, responseValue);
1219
1233
  }
1220
1234
  };
1221
1235
  function polymorphicPayloadError(method, got, expected) {
@@ -1398,6 +1412,8 @@ import { PromiseSource as PromiseSource4 } from "@actuallab/core";
1398
1412
  // src/rpc-stream-sender.ts
1399
1413
  import { PromiseSource as PromiseSource3, RingBuffer } from "@actuallab/core";
1400
1414
  var { warnLog: warnLog5 } = getLogs("RpcSharedStream");
1415
+ function _ignore() {
1416
+ }
1401
1417
  var DEFAULT_ACK_PERIOD = 256;
1402
1418
  var DEFAULT_ACK_ADVANCE = 128;
1403
1419
  var RTT_WINDOW_MS = 2e4;
@@ -1437,7 +1453,11 @@ var RpcStreamSender = class _RpcStreamSender {
1437
1453
  // -- Pump state --
1438
1454
  /** Next stream index that will be sent (matches .NET's `index`). */
1439
1455
  _nextIndex = 0;
1440
- _ended = false;
1456
+ /** Torn down — `disconnect()` ran. .NET's counterpart is disposal; note it
1457
+ * does NOT mean "an End was sent", which .NET does not track at all. */
1458
+ _isDisposed = false;
1459
+ /** The pump has returned. .NET's `WhenRunning.IsCompleted`. */
1460
+ _isCompleted = false;
1441
1461
  _started = new PromiseSource3();
1442
1462
  /** Highest `nextIndex` observed in an ACK so far (matches .NET's `lastAckNextIndex` idiom). */
1443
1463
  _lastAckedIndex = 0;
@@ -1544,7 +1564,7 @@ var RpcStreamSender = class _RpcStreamSender {
1544
1564
  this._sendDisconnect();
1545
1565
  return;
1546
1566
  }
1547
- } else if (this._ended) {
1567
+ } else if (this._isCompleted) {
1548
1568
  this._sendDisconnect();
1549
1569
  return;
1550
1570
  } else if (mustReset && !this.allowReconnect) {
@@ -1565,7 +1585,7 @@ var RpcStreamSender = class _RpcStreamSender {
1565
1585
  // Tells the consumer this shared stream is gone, then disposes it —
1566
1586
  // mirrors RpcSharedStream.SendDisconnect (RpcSharedStream.cs:284-289).
1567
1587
  _sendDisconnect() {
1568
- const conn = this.peer.connection;
1588
+ const conn = this.peer.wireConnection;
1569
1589
  if (conn)
1570
1590
  this.peer.hub.systemCallSender.disconnect(conn, this.peer.serializationFormat, [this.id.localId]);
1571
1591
  this.disconnect();
@@ -1579,13 +1599,13 @@ var RpcStreamSender = class _RpcStreamSender {
1579
1599
  *
1580
1600
  * Advances `_nextIndex` unconditionally — even if the peer is disconnected
1581
1601
  * the item is considered "sent" from the pump's perspective so the replay
1582
- * buffer indexing stays consistent. If a connection is absent, the wire
1602
+ * buffer indexing stays consistent. If the peer isn't connected, the wire
1583
1603
  * message is dropped; the item will be resent from the replay buffer
1584
1604
  * after a reconnect (client sends `Ack(N, mustReset=true)`).
1585
1605
  */
1586
1606
  sendItem(item) {
1587
- if (this._ended) return;
1588
- const conn = this.peer.connection;
1607
+ if (this._isDisposed) return;
1608
+ const conn = this.peer.wireConnection;
1589
1609
  if (conn) {
1590
1610
  this.peer.hub.systemCallSender.item(
1591
1611
  conn,
@@ -1602,8 +1622,8 @@ var RpcStreamSender = class _RpcStreamSender {
1602
1622
  }
1603
1623
  /** Send a batch of items to the client. See {@link sendItem} for disconnect semantics. */
1604
1624
  sendBatch(items) {
1605
- if (this._ended || items.length === 0) return;
1606
- const conn = this.peer.connection;
1625
+ if (this._isDisposed || items.length === 0) return;
1626
+ const conn = this.peer.wireConnection;
1607
1627
  if (conn) {
1608
1628
  this.peer.hub.systemCallSender.batch(
1609
1629
  conn,
@@ -1615,18 +1635,21 @@ var RpcStreamSender = class _RpcStreamSender {
1615
1635
  }
1616
1636
  this._nextIndex += items.length;
1617
1637
  }
1618
- /** Signal stream completion to the client. */
1638
+ /** Signal stream completion to the client. Re-sendable on purpose: if the
1639
+ * peer was not connected the frame is dropped, and the End stays in the
1640
+ * replay buffer for the next reset ack — .NET has no "already ended" latch
1641
+ * (the End is an ordinary buffer entry, RpcSharedStream.cs:185/239). */
1619
1642
  sendEnd(error) {
1620
- if (this._ended) return;
1621
- this._ended = true;
1622
- const conn = this.peer.connection;
1643
+ if (this._isDisposed) return;
1644
+ const index = this._nextIndex++;
1645
+ const conn = this.peer.wireConnection;
1623
1646
  if (!conn) return;
1624
1647
  const errorInfo = error ? toExceptionInfo(error) : { TypeRef: "", Message: "" };
1625
1648
  this.peer.hub.systemCallSender.end(
1626
1649
  conn,
1627
1650
  this.peer.serializationFormat,
1628
1651
  this.id.localId,
1629
- this._nextIndex,
1652
+ index,
1630
1653
  errorInfo
1631
1654
  );
1632
1655
  }
@@ -1638,13 +1661,14 @@ var RpcStreamSender = class _RpcStreamSender {
1638
1661
  */
1639
1662
  async writeFrom(source) {
1640
1663
  await this._started;
1641
- if (this._ended) return;
1664
+ if (this._isDisposed) return;
1642
1665
  const iterator = source[Symbol.asyncIterator]();
1643
1666
  this._iterator = iterator;
1644
1667
  const state = { iteratorDone: false };
1645
1668
  try {
1646
1669
  await this._run(iterator, state);
1647
1670
  } finally {
1671
+ this._isCompleted = true;
1648
1672
  if (state.iteratorDone && this._iterator === iterator) {
1649
1673
  this._iterator = null;
1650
1674
  }
@@ -1657,7 +1681,7 @@ var RpcStreamSender = class _RpcStreamSender {
1657
1681
  * With `bufferSize > ackAdvance` the local ring buffer holds more than the
1658
1682
  * in-flight window — real-time mode uses the extra space to pre-buffer.
1659
1683
  */
1660
- /* eslint-disable @typescript-eslint/no-unnecessary-condition -- _ended changes across awaits */
1684
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition -- _isDisposed changes across awaits */
1661
1685
  async _run(iterator, state) {
1662
1686
  const buffer = new RingBuffer(this.bufferSize + 1);
1663
1687
  const isRealTime = this.isRealTime;
@@ -1690,7 +1714,7 @@ var RpcStreamSender = class _RpcStreamSender {
1690
1714
  if (r === pending)
1691
1715
  return false;
1692
1716
  whenMovedNext = null;
1693
- if (this._ended) return false;
1717
+ if (this._isDisposed) return false;
1694
1718
  if (r.done) {
1695
1719
  buffer.pushTail(_endItem);
1696
1720
  isFullyBuffered = true;
@@ -1714,105 +1738,107 @@ var RpcStreamSender = class _RpcStreamSender {
1714
1738
  return true;
1715
1739
  }
1716
1740
  };
1717
- while (true) {
1718
- let ack = this._tryProcessAcks();
1719
- if (!ack) {
1720
- await this._waitAckReady();
1721
- if (this._ended) return;
1722
- ack = this._tryProcessAcks();
1741
+ nextAck:
1742
+ while (true) {
1743
+ let ack = this._tryProcessAcks();
1723
1744
  if (!ack) {
1724
- warnLog5?.log("Something is off: couldn't read an acknowledgement");
1725
- return;
1745
+ await this._waitAckReady();
1746
+ if (this._isDisposed) return;
1747
+ ack = this._tryProcessAcks();
1748
+ if (!ack) {
1749
+ warnLog5?.log("Something is off: couldn't read an acknowledgement");
1750
+ return;
1751
+ }
1726
1752
  }
1727
- }
1728
- this._lastAckedIndex = ack.nextIndex;
1729
- if (isRealTime && ack.mustReset && !isFullyBuffered) {
1730
- buffer.clear();
1731
- bufferStart = this._nextIndex;
1732
- while (true) {
1733
- if (this._ended) return;
1734
- let item;
1735
- let accepted = false;
1736
- try {
1737
- const r = await readNext();
1738
- if (this._ended) return;
1739
- if (r.done) {
1740
- item = _endItem;
1741
- isFullyBuffered = true;
1753
+ this._lastAckedIndex = ack.nextIndex;
1754
+ if (isRealTime && ack.mustReset && !isFullyBuffered) {
1755
+ buffer.clear();
1756
+ bufferStart = this._nextIndex;
1757
+ while (true) {
1758
+ if (this._isDisposed) return;
1759
+ let item;
1760
+ let accepted = false;
1761
+ try {
1762
+ const r = await readNext();
1763
+ if (this._isDisposed) return;
1764
+ if (r.done) {
1765
+ item = _endItem;
1766
+ isFullyBuffered = true;
1767
+ state.iteratorDone = true;
1768
+ } else if (this.canSkipTo(r.value)) {
1769
+ item = { kind: "value", value: r.value };
1770
+ accepted = true;
1771
+ } else {
1772
+ continue;
1773
+ }
1774
+ } catch (e) {
1742
1775
  state.iteratorDone = true;
1743
- } else if (this.canSkipTo(r.value)) {
1744
- item = { kind: "value", value: r.value };
1745
- accepted = true;
1746
- } else {
1747
- continue;
1776
+ isFullyBuffered = true;
1777
+ item = {
1778
+ kind: "error",
1779
+ error: e instanceof Error ? e : new Error(String(e))
1780
+ };
1748
1781
  }
1749
- } catch (e) {
1750
- state.iteratorDone = true;
1751
- isFullyBuffered = true;
1752
- item = {
1753
- kind: "error",
1754
- error: e instanceof Error ? e : new Error(String(e))
1755
- };
1782
+ buffer.pushTail(item);
1783
+ fireBuffered();
1784
+ if (accepted || isFullyBuffered) break;
1756
1785
  }
1757
- buffer.pushTail(item);
1758
- fireBuffered();
1759
- if (accepted || isFullyBuffered) break;
1760
1786
  }
1761
- }
1762
- const shift = _clamp(ack.nextIndex - bufferStart, 0, buffer.count);
1763
- if (shift > 0) {
1764
- buffer.moveHead(shift);
1765
- bufferStart += shift;
1766
- }
1767
- if (this._nextIndex < bufferStart) {
1768
- if (!this._ended)
1769
- this.sendEnd(new Error("Stream position unavailable."));
1770
- return;
1771
- }
1772
- let bufferIndex = this._nextIndex - bufferStart;
1773
- const maxIndex = ack.nextIndex + this.ackAdvance;
1774
- while (this._nextIndex < maxIndex) {
1775
- if (this._ended) return;
1776
- if (isRealTime) {
1777
- while (buffer.hasRemainingCapacity && !isFullyBuffered && !this._hasPendingAck) {
1778
- if (!await bufferNext(true, true))
1779
- break;
1780
- }
1781
- if (bufferIndex >= buffer.count && !isFullyBuffered && buffer.hasRemainingCapacity && !this._hasPendingAck) {
1782
- await bufferNext(false, true);
1783
- }
1784
- if (ack.nextIndex > 0) {
1785
- const result = _compactBufferedUnsentSuffix(
1786
- buffer,
1787
- bufferIndex,
1788
- this.canSkipTo
1789
- );
1790
- bufferIndex = result.newIndex;
1791
- if (result.skipped > 0)
1792
- this._skipCount += result.skipped;
1793
- }
1794
- if (this._hasPendingAck) break;
1787
+ const shift = _clamp(ack.nextIndex - bufferStart, 0, buffer.count);
1788
+ if (shift > 0) {
1789
+ buffer.moveHead(shift);
1790
+ bufferStart += shift;
1795
1791
  }
1796
- while (this._nextIndex < maxIndex && bufferIndex < buffer.count) {
1797
- const item = buffer.get(bufferIndex++);
1798
- if (item.kind === "value") {
1799
- this.sendItem(item.value);
1800
- } else if (item.kind === "end") {
1801
- if (!this._ended) this.sendEnd();
1802
- return;
1803
- } else {
1804
- if (!this._ended) this.sendEnd(item.error);
1805
- return;
1792
+ if (this._nextIndex < bufferStart) {
1793
+ if (!this._isDisposed)
1794
+ this.sendEnd(new Error("Stream position unavailable."));
1795
+ return;
1796
+ }
1797
+ let bufferIndex = this._nextIndex - bufferStart;
1798
+ const maxIndex = ack.nextIndex + this.ackAdvance;
1799
+ while (this._nextIndex < maxIndex) {
1800
+ if (this._isDisposed) return;
1801
+ if (isRealTime) {
1802
+ while (buffer.hasRemainingCapacity && !isFullyBuffered && !this._hasPendingAck) {
1803
+ if (!await bufferNext(true, true))
1804
+ break;
1805
+ }
1806
+ if (bufferIndex >= buffer.count && !isFullyBuffered && buffer.hasRemainingCapacity && !this._hasPendingAck) {
1807
+ whenMovedNext ??= iterator.next();
1808
+ const whenAckReady = this._whenAckReady ??= new PromiseSource3();
1809
+ await Promise.race([whenMovedNext.then(_ignore, _ignore), whenAckReady]);
1810
+ if (!this._hasPendingAck)
1811
+ await bufferNext(false, true);
1812
+ }
1813
+ if (ack.nextIndex > 0) {
1814
+ const result = _compactBufferedUnsentSuffix(
1815
+ buffer,
1816
+ bufferIndex,
1817
+ this.canSkipTo
1818
+ );
1819
+ bufferIndex = result.newIndex;
1820
+ if (result.skipped > 0)
1821
+ this._skipCount += result.skipped;
1822
+ }
1823
+ if (this._hasPendingAck) break;
1806
1824
  }
1825
+ while (this._nextIndex < maxIndex && bufferIndex < buffer.count) {
1826
+ const item = buffer.get(bufferIndex++);
1827
+ if (item.kind === "value") {
1828
+ this.sendItem(item.value);
1829
+ } else {
1830
+ this.sendEnd(item.kind === "error" ? item.error : void 0);
1831
+ continue nextAck;
1832
+ }
1833
+ }
1834
+ if (this._nextIndex >= maxIndex)
1835
+ break;
1836
+ if (isFullyBuffered) break;
1837
+ if (!buffer.hasRemainingCapacity) break;
1838
+ if (!isRealTime)
1839
+ await bufferNext(false, false);
1807
1840
  }
1808
- if (this._nextIndex >= maxIndex)
1809
- break;
1810
- if (isFullyBuffered) break;
1811
- if (!buffer.hasRemainingCapacity) break;
1812
- if (!isRealTime)
1813
- await bufferNext(false, false);
1814
1841
  }
1815
- }
1816
1842
  }
1817
1843
  /* eslint-enable @typescript-eslint/no-unnecessary-condition */
1818
1844
  get _hasPendingAck() {
@@ -1837,13 +1863,14 @@ var RpcStreamSender = class _RpcStreamSender {
1837
1863
  }
1838
1864
  return { nextIndex, mustReset };
1839
1865
  }
1840
- /** Wait until an ACK is pending (or `_ended`). */
1866
+ /** Wait until an ACK is pending (or the sender is torn down). */
1841
1867
  async _waitAckReady() {
1842
- if (this._hasPendingAck || this._ended)
1868
+ if (this._hasPendingAck || this._isDisposed)
1843
1869
  return;
1844
- this._whenAckReady = new PromiseSource3();
1845
- await this._whenAckReady;
1846
- this._whenAckReady = null;
1870
+ const whenAckReady = this._whenAckReady ??= new PromiseSource3();
1871
+ await whenAckReady;
1872
+ if (this._whenAckReady === whenAckReady)
1873
+ this._whenAckReady = null;
1847
1874
  }
1848
1875
  // One RTT sample per ack: time from sending the newest acked item to the
1849
1876
  // ack's arrival (a slight upper bound — includes the server's ack cadence).
@@ -1869,8 +1896,8 @@ var RpcStreamSender = class _RpcStreamSender {
1869
1896
  reconnect() {
1870
1897
  }
1871
1898
  disconnect() {
1872
- if (this._ended) return;
1873
- this._ended = true;
1899
+ if (this._isDisposed) return;
1900
+ this._isDisposed = true;
1874
1901
  this._abortController.abort();
1875
1902
  if (!this._started.isCompleted) {
1876
1903
  this._started.resolve();
@@ -2029,8 +2056,11 @@ var RpcStream = class _RpcStream {
2029
2056
  }
2030
2057
  }
2031
2058
  /**
2032
- * For local streams: resolves when the sender finishes pumping (after `toRef()` was called).
2033
- * Rejects if the source throws. Throws if `toRef()` hasn't been called yet.
2059
+ * For local streams: resolves once the consumer acknowledges the end
2060
+ * ($sys.AckEnd) or the sender is disconnected — NOT when the source drains,
2061
+ * since the sender keeps serving acks after End so a reconnect can replay it.
2062
+ * A source error reaches the consumer as $sys.End(error) instead of rejecting
2063
+ * this. Throws if `toRef()` hasn't been called yet.
2034
2064
  */
2035
2065
  get whenSent() {
2036
2066
  if (!this._whenSent)
@@ -2296,7 +2326,7 @@ var RpcStream = class _RpcStream {
2296
2326
  static _emptyGuid = "00000000-0000-0000-0000-000000000000";
2297
2327
  _sendAck(nextIndex, mustReset) {
2298
2328
  this._ackSentUpTo = nextIndex;
2299
- const conn = this.peer.isConnected ? this.peer.connection : void 0;
2329
+ const conn = this.peer.wireConnection;
2300
2330
  if (conn) {
2301
2331
  const hostId = mustReset ? this.id.hostId : _RpcStream._emptyGuid;
2302
2332
  this.peer.hub.systemCallSender.ack(
@@ -2309,7 +2339,7 @@ var RpcStream = class _RpcStream {
2309
2339
  }
2310
2340
  }
2311
2341
  _sendAckEnd() {
2312
- const conn = this.peer.isConnected ? this.peer.connection : void 0;
2342
+ const conn = this.peer.wireConnection;
2313
2343
  if (conn) {
2314
2344
  this.peer.hub.systemCallSender.ackEnd(
2315
2345
  conn,
@@ -2673,6 +2703,8 @@ var RpcPeer = class {
2673
2703
  /** Wall-clock time of the last inbound `$sys.KeepAlive`, or 0 if none yet
2674
2704
  * for the current connection. Useful for diagnostics. */
2675
2705
  _lastKeepAliveAt = 0;
2706
+ /** When the current outage began; 0 until the peer has been connected at least once. */
2707
+ _disconnectedAt = 0;
2676
2708
  /** Outbound keep-alive send period. Initialized from `hub.limits` at
2677
2709
  * construction; set directly to override on a single peer. */
2678
2710
  keepAlivePeriodMs;
@@ -2693,9 +2725,10 @@ var RpcPeer = class {
2693
2725
  }
2694
2726
  /** Fail outbound calls whose per-call {@link RpcCallTimeouts} elapsed —
2695
2727
  * the connect timeout while a call still awaits the wire, the run timeout
2696
- * once it has been sent. Mirrors .NET `RpcOutboundCallTracker.Maintain`
2697
- * (RpcCallTrackers.cs:108-207). Calls without timeouts (queries) never
2698
- * time out here. */
2728
+ * once it has been sent, and the connect timeout again for any pending call while the
2729
+ * peer is away. Mirrors .NET `RpcOutboundCallTracker.HandleDisconnect` (ConnectTimeout,
2730
+ * per outage) plus `Maintain` (RunTimeout, per connection). Calls without timeouts
2731
+ * (queries) never time out here. */
2699
2732
  _maintainOutboundCalls() {
2700
2733
  const now = Date.now();
2701
2734
  const canRunTimeout = this._isConnected && (this._lastKeepAliveAt === 0 || now - this._lastKeepAliveAt <= this.keepAliveTimeoutMs);
@@ -2704,7 +2737,11 @@ var RpcPeer = class {
2704
2737
  if (timeouts === void 0 || call.result.isCompleted)
2705
2738
  continue;
2706
2739
  let timedOut;
2707
- if (call.sentAt === 0) {
2740
+ if (!this._isConnected) {
2741
+ const since = Math.max(this._disconnectedAt, call.startedAt);
2742
+ if (now - since >= timeouts.connectTimeoutMs)
2743
+ timedOut = `connect timeout (${timeouts.connectTimeoutMs}ms)`;
2744
+ } else if (call.sentAt === 0) {
2708
2745
  if (now - call.startedAt >= timeouts.connectTimeoutMs)
2709
2746
  timedOut = `connect timeout (${timeouts.connectTimeoutMs}ms)`;
2710
2747
  } else if (canRunTimeout && now - call.sentAt >= timeouts.runTimeoutMs)
@@ -2719,6 +2756,18 @@ var RpcPeer = class {
2719
2756
  get connection() {
2720
2757
  return this._connection;
2721
2758
  }
2759
+ // The connection you may send a non-handshake message on, or `undefined`.
2760
+ // `_connection` is set before the socket even opens and stays set through
2761
+ // the handshake, and `RpcConnection` buffers pre-OPEN sends and flushes them
2762
+ // from `onopen` — so writing through it during Connecting or Handshaking puts
2763
+ // the frame on the wire ahead of $sys.Handshake and corrupts the remote's
2764
+ // handshake. `_isConnected` is the only flag false across both phases.
2765
+ // Port of C#'s `RpcPeer.Transport` (RpcPeer.cs:29-52), which hides the
2766
+ // transport for exactly this reason. $sys.Handshake itself must use
2767
+ // `connection` — it is the frame this gate exists to let through first.
2768
+ get wireConnection() {
2769
+ return this._isConnected ? this._connection : void 0;
2770
+ }
2722
2771
  get connectionState() {
2723
2772
  return this._connectionState;
2724
2773
  }
@@ -2747,10 +2796,13 @@ var RpcPeer = class {
2747
2796
  this._connectionState = value;
2748
2797
  if (value === 3 /* Connected */) {
2749
2798
  this._isConnected = true;
2799
+ this._disconnectedAt = 0;
2750
2800
  this._armKeepAliveWatchdog();
2751
- } else if (value === 0 /* Disconnected */) {
2752
- if (previousState === 3 /* Connected */)
2801
+ } else {
2802
+ if (previousState === 3 /* Connected */) {
2753
2803
  this._isConnected = false;
2804
+ this._disconnectedAt = Date.now();
2805
+ }
2754
2806
  this._disarmKeepAliveWatchdog();
2755
2807
  }
2756
2808
  this.connectionStateChanged.trigger(value);
@@ -2775,6 +2827,8 @@ var RpcPeer = class {
2775
2827
  conn.closed.add((ev) => {
2776
2828
  if (this._connection !== conn) return;
2777
2829
  this._connection = void 0;
2830
+ if (this._isConnected)
2831
+ this._disconnectedAt = Date.now();
2778
2832
  this._isConnected = false;
2779
2833
  if (ev.reason)
2780
2834
  infoLog?.log(`'${this.ref}': Disconnected: ${ev.reason} (code=${ev.code})`);
@@ -2834,8 +2888,9 @@ var RpcPeer = class {
2834
2888
  if (this.outboundCalls.remove(callId) !== void 0) {
2835
2889
  outboundCall.result.reject(cancellationError("Call cancelled."));
2836
2890
  outboundCall.onDisconnect();
2837
- if (this._isConnected && this._connection !== void 0)
2838
- this.hub.systemCallSender.cancel(this._connection, this.serializationFormat, callId);
2891
+ const conn = this.wireConnection;
2892
+ if (conn !== void 0)
2893
+ this.hub.systemCallSender.cancel(conn, this.serializationFormat, callId);
2839
2894
  }
2840
2895
  };
2841
2896
  signal.addEventListener("abort", onAbort, { once: true });
@@ -2878,9 +2933,11 @@ var RpcPeer = class {
2878
2933
  this._setConnectionState(0 /* Disconnected */);
2879
2934
  infoLog?.log(`'${this.ref}': Stopped`);
2880
2935
  }
2881
- /** Send pre-serialized wire data through the current connection. */
2936
+ /** Send pre-serialized wire data through the current connection. Callers are
2937
+ * already gated on `_isConnected`; the check is repeated here so the next one
2938
+ * cannot reintroduce a pre-handshake write. $sys.Handshake does not use this. */
2882
2939
  _sendWireData(data) {
2883
- if (this._connection === void 0) return;
2940
+ if (!this._isConnected || this._connection === void 0) return;
2884
2941
  if (typeof data === "string") {
2885
2942
  this._connection.send(data);
2886
2943
  } else {
@@ -2963,9 +3020,10 @@ var RpcPeer = class {
2963
3020
  const ref = stream.toRef(this);
2964
3021
  const send = () => {
2965
3022
  if (call2?.isCancelled === true) return;
2966
- if (this._connection !== void 0)
3023
+ const conn = this.wireConnection;
3024
+ if (conn !== void 0)
2967
3025
  this.hub.systemCallSender.ok(
2968
- this._connection,
3026
+ conn,
2969
3027
  this.serializationFormat,
2970
3028
  relatedId,
2971
3029
  ref
@@ -2980,9 +3038,10 @@ var RpcPeer = class {
2980
3038
  } else if (call2 !== void 0) {
2981
3039
  const send = () => {
2982
3040
  if (call2.isCancelled) return;
2983
- if (this._connection !== void 0)
3041
+ const conn = this.wireConnection;
3042
+ if (conn !== void 0)
2984
3043
  this.hub.systemCallSender.ok(
2985
- this._connection,
3044
+ conn,
2986
3045
  this.serializationFormat,
2987
3046
  relatedId,
2988
3047
  result
@@ -2996,9 +3055,10 @@ var RpcPeer = class {
2996
3055
  if (call2 !== void 0) {
2997
3056
  const send = () => {
2998
3057
  if (call2.isCancelled) return;
2999
- if (this._connection !== void 0)
3058
+ const conn = this.wireConnection;
3059
+ if (conn !== void 0)
3000
3060
  this.hub.systemCallSender.error(
3001
- this._connection,
3061
+ conn,
3002
3062
  this.serializationFormat,
3003
3063
  relatedId,
3004
3064
  e
@@ -3031,8 +3091,9 @@ var RpcPeer = class {
3031
3091
  * Mirrors .NET `RpcObjectTrackers.Maintain`'s `KeepAliveTimeout` branch. */
3032
3092
  _armKeepAliveWatchdog() {
3033
3093
  this._keepAliveTimer ??= setInterval(() => {
3034
- if (this._connection !== void 0) {
3035
- this.hub.systemCallSender.keepAlive(this._connection, this.serializationFormat, [
3094
+ const conn = this.wireConnection;
3095
+ if (conn !== void 0) {
3096
+ this.hub.systemCallSender.keepAlive(conn, this.serializationFormat, [
3036
3097
  ...this.remoteObjects.keys()
3037
3098
  ]);
3038
3099
  }
@@ -3180,9 +3241,26 @@ var RpcClientPeer = class extends RpcPeer {
3180
3241
  connectWith(conn, isPeerChanged = true) {
3181
3242
  const sentCalls = this.outboundCalls.getSentCalls();
3182
3243
  this.setupConnection(conn);
3244
+ const whenHandshake = isPeerChanged ? void 0 : this._pendingHandshake = new PromiseSource5();
3245
+ this.hub.systemCallSender.handshake(
3246
+ conn,
3247
+ this.serializationFormat,
3248
+ this.id,
3249
+ this.hub.hubId,
3250
+ this.nextOwnHandshakeIndex()
3251
+ );
3183
3252
  this._isConnected = true;
3184
3253
  this.connectionStateChanged.trigger(3 /* Connected */);
3185
- void this._reconnect(sentCalls, isPeerChanged);
3254
+ if (whenHandshake === void 0) {
3255
+ void this._reconnect(sentCalls, true);
3256
+ return;
3257
+ }
3258
+ const whenClosed = new PromiseSource5();
3259
+ conn.closed.add(() => whenClosed.resolve());
3260
+ void Promise.race([whenHandshake, whenClosed]).then(() => {
3261
+ this._pendingHandshake = void 0;
3262
+ return this._reconnect(sentCalls, false);
3263
+ });
3186
3264
  }
3187
3265
  /** Kick off the reconnect loop. Idempotent — subsequent calls are
3188
3266
  * no-ops; await `peer.whenRunning` if you need the loop's completion
@@ -3500,6 +3578,7 @@ var RpcClientPeer = class extends RpcPeer {
3500
3578
  return 2 /* Changed */;
3501
3579
  }
3502
3580
  _onHandshakeReceived(handshake) {
3581
+ this._remoteHandshakeIndex = handshake.Index ?? 0;
3503
3582
  this._pendingHandshake?.resolve(handshake);
3504
3583
  }
3505
3584
  close() {