@actuallab/rpc 13.0.20 → 13.0.163

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
@@ -165,6 +165,24 @@ function readVarUint(data, offset) {
165
165
  } while (shift < 35);
166
166
  return { value: value >>> 0, bytesRead };
167
167
  }
168
+ function skipHeaders(data, pos, headerCount) {
169
+ for (let h = 0; h < headerCount; h++) {
170
+ const keyLen = data[pos++];
171
+ pos += keyLen;
172
+ const valLen = readVarUint(data, pos);
173
+ pos += valLen.bytesRead + valLen.value;
174
+ }
175
+ return pos;
176
+ }
177
+ function readPolymorphismMarker(data, offset) {
178
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
179
+ const length = view.getUint16(offset, true);
180
+ if (length === 0)
181
+ return { bytesRead: 2, isNull: false };
182
+ if (length === 1)
183
+ return { bytesRead: 2, isNull: true };
184
+ throw new Error("polymorphic payloads are not supported by the TS client");
185
+ }
168
186
  function concatUint8Arrays(arrays) {
169
187
  let totalLength = 0;
170
188
  for (const arr of arrays) totalLength += arr.length;
@@ -228,14 +246,6 @@ function deserializeBinaryMessage(data, offset, decoder = defaultBinaryDecoder)
228
246
  pos += methodLen.value;
229
247
  const argDataLen = view.getInt32(pos, true);
230
248
  pos += 4;
231
- if (headerCount > 0) {
232
- for (let h = 0; h < headerCount; h++) {
233
- const keyLen = data[pos++];
234
- pos += keyLen;
235
- const valLen = readVarUint(data, pos);
236
- pos += valLen.bytesRead + valLen.value;
237
- }
238
- }
239
249
  const args = [];
240
250
  const argEnd = pos + argDataLen;
241
251
  if (argDataLen > 0) {
@@ -243,8 +253,10 @@ function deserializeBinaryMessage(data, offset, decoder = defaultBinaryDecoder)
243
253
  for (const decoded of decoder.decodeMulti(argSlice)) {
244
254
  args.push(decoded);
245
255
  }
246
- pos = argEnd;
247
256
  }
257
+ pos = argEnd;
258
+ if (headerCount > 0)
259
+ pos = skipHeaders(data, pos, headerCount);
248
260
  const message = {
249
261
  Method: method,
250
262
  RelatedId: relId.value,
@@ -322,14 +334,6 @@ function deserializeCompactBinaryMessage(data, offset, registry, decoder = defau
322
334
  const method = registry?.getName(methodHash | 0) ?? `<hash:0x${methodHash.toString(16).padStart(8, "0")}>`;
323
335
  const argDataLen = view.getInt32(pos, true);
324
336
  pos += 4;
325
- if (headerCount > 0) {
326
- for (let h = 0; h < headerCount; h++) {
327
- const keyLen = data[pos++];
328
- pos += keyLen;
329
- const valLen = readVarUint(data, pos);
330
- pos += valLen.bytesRead + valLen.value;
331
- }
332
- }
333
337
  const args = [];
334
338
  const argEnd = pos + argDataLen;
335
339
  if (argDataLen > 0) {
@@ -337,8 +341,10 @@ function deserializeCompactBinaryMessage(data, offset, registry, decoder = defau
337
341
  for (const decoded of decoder.decodeMulti(argSlice)) {
338
342
  args.push(decoded);
339
343
  }
340
- pos = argEnd;
341
344
  }
345
+ pos = argEnd;
346
+ if (headerCount > 0)
347
+ pos = skipHeaders(data, pos, headerCount);
342
348
  const message = {
343
349
  Method: method,
344
350
  RelatedId: relId.value,
@@ -608,7 +614,8 @@ function defineRpcService(name, methods) {
608
614
  callTypeId: input.callTypeId ?? 0,
609
615
  stream: input.returns === RpcType.stream,
610
616
  noWait: isNoWait,
611
- remoteExecutionMode: isNoWait ? 0 : input.remoteExecutionMode ?? RpcRemoteExecutionMode.Default
617
+ remoteExecutionMode: isNoWait ? 0 : input.remoteExecutionMode ?? RpcRemoteExecutionMode.Default,
618
+ timeouts: input.timeouts
612
619
  });
613
620
  }
614
621
  return { name, methods: map };
@@ -642,6 +649,14 @@ var RpcOutboundCall = class {
642
649
  removeOnOk = true;
643
650
  /** The serialized wire data — stored for re-sending on reconnect. */
644
651
  serializedWireData = "";
652
+ /** Per-call connect/run timeouts enforced by the peer's maintenance loop
653
+ * (R12). `undefined` means unbounded (the query default). */
654
+ timeouts;
655
+ /** `Date.now()` when this call was created (used for the connect timeout). */
656
+ startedAt = Date.now();
657
+ /** `Date.now()` when this call was last put on the wire, or 0 if it hasn't
658
+ * been sent yet (used for the run timeout). */
659
+ sentAt = 0;
645
660
  /**
646
661
  * Bitfield of {@link RpcCallStage} flags indicating how far this call
647
662
  * has progressed. Used by the `$sys.Reconnect` protocol to tell the
@@ -731,20 +746,77 @@ var RpcInboundCall = class {
731
746
  callId;
732
747
  method;
733
748
  args;
749
+ /** 0 = still processing, 1 = result computed and sent at least once. */
750
+ completedStage = 0;
751
+ _resend;
752
+ _abortController = new AbortController();
734
753
  constructor(callId, method, args) {
735
754
  this.callId = callId;
736
755
  this.method = method;
737
756
  this.args = args;
738
757
  }
758
+ /** Aborted when the remote peer cancels this call via `$sys.Cancel` (R17).
759
+ * Threaded into the service handler so it can abort in-flight work. */
760
+ get signal() {
761
+ return this._abortController.signal;
762
+ }
763
+ /** True once the remote peer cancelled this call — the response is then
764
+ * suppressed even though the call may still be registered (R9 dedup). */
765
+ get isCancelled() {
766
+ return this._abortController.signal.aborted;
767
+ }
768
+ cancel() {
769
+ this._abortController.abort();
770
+ }
771
+ // Records the closure that re-sends this call's already-computed result;
772
+ // marks the call as completed. Mirrors .NET's post-`ProcessStage1Plus` state.
773
+ setResult(resend) {
774
+ this._resend = resend;
775
+ this.completedStage = 1;
776
+ }
777
+ // Re-sends the result to the remote peer if it was already computed.
778
+ // A resent frame for an in-flight call is a no-op — the original dispatch
779
+ // will send the result when it completes. Mirrors .NET
780
+ // `RpcInboundCall.TryReprocess`.
781
+ resendResult() {
782
+ if (this.completedStage >= 1)
783
+ this._resend?.();
784
+ }
739
785
  };
740
786
  var RpcInboundCallTracker = class {
741
787
  _calls = /* @__PURE__ */ new Map();
788
+ _completedIds = [];
789
+ /** Cap on retained completed calls — see `RpcLimits.completedInboundCallsLimit`. */
790
+ completedCallsLimit = 1e3;
742
791
  get size() {
743
792
  return this._calls.size;
744
793
  }
745
794
  register(call) {
746
795
  this._calls.set(call.callId, call);
747
796
  }
797
+ // Registers `call` unless its id is already tracked, in which case the
798
+ // existing call is returned. Mirrors .NET `RpcInboundCallTracker.GetOrRegister`.
799
+ getOrRegister(call) {
800
+ const existing = this._calls.get(call.callId);
801
+ if (existing !== void 0)
802
+ return existing;
803
+ this._calls.set(call.callId, call);
804
+ return call;
805
+ }
806
+ // Keeps the completed `call` registered for duplicate-frame dedup, evicting
807
+ // the oldest completed calls beyond `completedCallsLimit`. Deviation from
808
+ // .NET (which unregisters a call once its result is sent): TS peers
809
+ // blind-resend after reconnect, so a completed call must stay resolvable
810
+ // to avoid re-executing the handler — but only within a bounded window.
811
+ markCompleted(call) {
812
+ if (this._calls.get(call.callId) !== call)
813
+ return;
814
+ this._completedIds.push(call.callId);
815
+ while (this._completedIds.length > this.completedCallsLimit) {
816
+ const id = this._completedIds.shift();
817
+ this._calls.delete(id);
818
+ }
819
+ }
748
820
  get(callId) {
749
821
  return this._calls.get(callId);
750
822
  }
@@ -755,6 +827,23 @@ var RpcInboundCallTracker = class {
755
827
  }
756
828
  clear() {
757
829
  this._calls.clear();
830
+ this._completedIds.length = 0;
831
+ }
832
+ };
833
+
834
+ // src/rpc-call-timeouts.ts
835
+ var RpcCallTimeouts = class _RpcCallTimeouts {
836
+ /** Both timeouts unbounded — the query default (.NET `RpcCallTimeouts.None`). */
837
+ static None = new _RpcCallTimeouts(Infinity, Infinity);
838
+ /** Command default: 1.5 s connect / 10 s run (.NET `RpcCallTimeouts.Default.Command`). */
839
+ static Command = new _RpcCallTimeouts(1500, 1e4);
840
+ /** Max time a call may wait for the connection before it is sent. */
841
+ connectTimeoutMs;
842
+ /** Max time a sent call may stay unanswered before it is failed. */
843
+ runTimeoutMs;
844
+ constructor(connectTimeoutMs = Infinity, runTimeoutMs = Infinity) {
845
+ this.connectTimeoutMs = connectTimeoutMs;
846
+ this.runTimeoutMs = runTimeoutMs;
758
847
  }
759
848
  };
760
849
 
@@ -768,6 +857,12 @@ var RpcError = class extends Error {
768
857
  this.typeName = typeName;
769
858
  }
770
859
  };
860
+ var REMOTE_EXCEPTION_TYPE_REF = "ActualLab.Serialization.RemoteException, ActualLab.Core";
861
+ function toExceptionInfo(error) {
862
+ const name = error instanceof Error ? error.name : "Error";
863
+ const message = error instanceof Error ? error.message : String(error);
864
+ return { TypeRef: REMOTE_EXCEPTION_TYPE_REF, Message: `${name}: ${message}` };
865
+ }
771
866
 
772
867
  // src/increasing-seq-compressor.ts
773
868
  var IncreasingSeqCompressor = {
@@ -972,13 +1067,38 @@ var RpcStreamSender = class _RpcStreamSender {
972
1067
  */
973
1068
  onAck(nextIndex, hostId) {
974
1069
  const mustReset = hostId !== "" && hostId !== _RpcStreamSender._emptyGuid;
975
- if (!this._started.isCompleted) this._started.resolve();
1070
+ if (mustReset && hostId !== this.id.hostId) {
1071
+ this._sendDisconnect();
1072
+ return;
1073
+ }
1074
+ if (!this._started.isCompleted) {
1075
+ if (mustReset && nextIndex === 0)
1076
+ this._started.resolve();
1077
+ else {
1078
+ this._sendDisconnect();
1079
+ return;
1080
+ }
1081
+ } else if (this._ended) {
1082
+ this._sendDisconnect();
1083
+ return;
1084
+ } else if (mustReset && !this.allowReconnect) {
1085
+ this._sendDisconnect();
1086
+ return;
1087
+ }
976
1088
  this._acks.push({ nextIndex, mustReset });
977
1089
  if (this._whenAckReady) {
978
1090
  this._whenAckReady.resolve();
979
1091
  this._whenAckReady = null;
980
1092
  }
981
1093
  }
1094
+ // Tells the consumer this shared stream is gone, then disposes it —
1095
+ // mirrors RpcSharedStream.SendDisconnect (RpcSharedStream.cs:284-289).
1096
+ _sendDisconnect() {
1097
+ const conn = this.peer.connection;
1098
+ if (conn)
1099
+ this.peer.hub.systemCallSender.disconnect(conn, this.peer.serializationFormat, [this.id.localId]);
1100
+ this.disconnect();
1101
+ }
982
1102
  /** Called by system call handler when $sys.AckEnd is received from the client. */
983
1103
  onAckEnd(_hostId) {
984
1104
  this.disconnect();
@@ -1027,7 +1147,7 @@ var RpcStreamSender = class _RpcStreamSender {
1027
1147
  this._ended = true;
1028
1148
  const conn = this.peer.connection;
1029
1149
  if (!conn) return;
1030
- const errorInfo = error ? { TypeRef: "System.Exception", Message: error.message } : { TypeRef: "", Message: "" };
1150
+ const errorInfo = error ? toExceptionInfo(error) : { TypeRef: "", Message: "" };
1031
1151
  this.peer.hub.systemCallSender.end(
1032
1152
  conn,
1033
1153
  this.peer.serializationFormat,
@@ -1469,11 +1589,7 @@ var RpcStream = class _RpcStream {
1469
1589
  if (index > this._nextExpectedIndex) {
1470
1590
  warnLog4?.log(`item index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
1471
1591
  if (!this.allowReconnect) {
1472
- this._completed = true;
1473
- this._completionError = new Error(
1474
- `Stream gap at index ${index} (expected ${this._nextExpectedIndex}); reconnect not allowed`
1475
- );
1476
- this._notifyConsumer();
1592
+ this._complete(this._gapError(index));
1477
1593
  return;
1478
1594
  }
1479
1595
  this._sendAck(this._nextExpectedIndex, true);
@@ -1493,11 +1609,7 @@ var RpcStream = class _RpcStream {
1493
1609
  if (index > this._nextExpectedIndex) {
1494
1610
  warnLog4?.log(`batch index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
1495
1611
  if (!this.allowReconnect) {
1496
- this._completed = true;
1497
- this._completionError = new Error(
1498
- `Stream gap at index ${index} (expected ${this._nextExpectedIndex}); reconnect not allowed`
1499
- );
1500
- this._notifyConsumer();
1612
+ this._complete(this._gapError(index));
1501
1613
  return;
1502
1614
  }
1503
1615
  this._sendAck(this._nextExpectedIndex, true);
@@ -1514,9 +1626,20 @@ var RpcStream = class _RpcStream {
1514
1626
  /** Called by system call handler when the stream ends ($sys.End). */
1515
1627
  onEnd(index, error) {
1516
1628
  if (this._disposed || this._completed) return;
1517
- this._completed = true;
1518
- this._completionError = error;
1519
- this._notifyConsumer();
1629
+ if (index > this._nextExpectedIndex) {
1630
+ warnLog4?.log(`end index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
1631
+ if (!this.allowReconnect) {
1632
+ this._complete(this._gapError(index));
1633
+ return;
1634
+ }
1635
+ this._sendAck(this._nextExpectedIndex, true);
1636
+ return;
1637
+ }
1638
+ if (index < this._nextExpectedIndex) {
1639
+ this._maybeSendAck(Math.min(index, this._nextConsumedIndex));
1640
+ return;
1641
+ }
1642
+ this._complete(error);
1520
1643
  }
1521
1644
  // -- IRpcObject --
1522
1645
  reconnect() {
@@ -1550,6 +1673,7 @@ var RpcStream = class _RpcStream {
1550
1673
  next: async () => {
1551
1674
  if (!this._started) {
1552
1675
  this._started = true;
1676
+ this.peer.remoteObjects.register(this);
1553
1677
  this._sendAck(0, true);
1554
1678
  }
1555
1679
  this._maybeSendAck(this._nextConsumedIndex);
@@ -1560,6 +1684,7 @@ var RpcStream = class _RpcStream {
1560
1684
  return { value, done: false };
1561
1685
  }
1562
1686
  if (this._completed) {
1687
+ this.dispose();
1563
1688
  if (this._completionError) throw this._completionError;
1564
1689
  return { value: void 0, done: true };
1565
1690
  }
@@ -1568,7 +1693,8 @@ var RpcStream = class _RpcStream {
1568
1693
  }
1569
1694
  },
1570
1695
  return: () => {
1571
- this._sendAckEnd();
1696
+ if (this._started && !this._completed)
1697
+ this._sendAckEnd();
1572
1698
  this.dispose();
1573
1699
  return Promise.resolve({ value: void 0, done: true });
1574
1700
  }
@@ -1584,6 +1710,22 @@ var RpcStream = class _RpcStream {
1584
1710
  this._notifyConsumer();
1585
1711
  }
1586
1712
  // -- Private --
1713
+ // Mirrors .NET RpcStream.CloseFromLock: acknowledge the end to the sender
1714
+ // (so its RpcSharedStream is released) and unregister immediately.
1715
+ _complete(error) {
1716
+ if (this._completed) return;
1717
+ this._completed = true;
1718
+ this._completionError = error;
1719
+ this._sendAckEnd();
1720
+ if (this.kind === RpcObjectKind.Remote)
1721
+ this.peer.remoteObjects.unregister(this);
1722
+ this._notifyConsumer();
1723
+ }
1724
+ _gapError(index) {
1725
+ return new Error(
1726
+ `Stream gap at index ${index} (expected ${this._nextExpectedIndex}); reconnect not allowed`
1727
+ );
1728
+ }
1587
1729
  _notifyConsumer() {
1588
1730
  if (this._consumerWaiting !== null) {
1589
1731
  this._consumerWaiting.resolve();
@@ -1600,7 +1742,7 @@ var RpcStream = class _RpcStream {
1600
1742
  static _emptyGuid = "00000000-0000-0000-0000-000000000000";
1601
1743
  _sendAck(nextIndex, mustReset) {
1602
1744
  this._ackSentUpTo = nextIndex;
1603
- const conn = this.peer.connection;
1745
+ const conn = this.peer.isConnected ? this.peer.connection : void 0;
1604
1746
  if (conn) {
1605
1747
  const hostId = mustReset ? this.id.hostId : _RpcStream._emptyGuid;
1606
1748
  this.peer.hub.systemCallSender.ack(
@@ -1613,7 +1755,7 @@ var RpcStream = class _RpcStream {
1613
1755
  }
1614
1756
  }
1615
1757
  _sendAckEnd() {
1616
- const conn = this.peer.connection;
1758
+ const conn = this.peer.isConnected ? this.peer.connection : void 0;
1617
1759
  if (conn) {
1618
1760
  this.peer.hub.systemCallSender.ackEnd(
1619
1761
  conn,
@@ -1633,9 +1775,7 @@ function resolveStreamRefs(value, peer) {
1633
1775
  if (typeof value === "string") {
1634
1776
  const ref = parseStreamRef(value);
1635
1777
  if (ref !== null) {
1636
- const stream = new RpcStream(ref, peer);
1637
- peer.remoteObjects.register(stream);
1638
- return stream;
1778
+ return new RpcStream(ref, peer);
1639
1779
  }
1640
1780
  return value;
1641
1781
  }
@@ -1688,6 +1828,11 @@ var RpcSystemCallHandler = class {
1688
1828
  case RpcSystemCalls.ok: {
1689
1829
  const call = peer.outboundCalls.get(relatedId);
1690
1830
  if (call !== void 0) {
1831
+ if (args.length > 1) {
1832
+ peer.outboundCalls.remove(relatedId);
1833
+ call.result.reject(polymorphicPayloadError(method, args.length, 1));
1834
+ break;
1835
+ }
1691
1836
  call.completedStage |= RpcCallStage.ResultReady;
1692
1837
  if (call.removeOnOk) {
1693
1838
  peer.outboundCalls.remove(relatedId);
@@ -1701,19 +1846,15 @@ var RpcSystemCallHandler = class {
1701
1846
  break;
1702
1847
  }
1703
1848
  case RpcSystemCalls.error: {
1704
- const call = peer.outboundCalls.remove(relatedId);
1849
+ const call = peer.outboundCalls.get(relatedId);
1705
1850
  if (call !== void 0) {
1851
+ call.completedStage |= RpcCallStage.ResultReady;
1852
+ if (call.removeOnOk) {
1853
+ peer.outboundCalls.remove(relatedId);
1854
+ }
1706
1855
  const errorInfo = args[0];
1707
1856
  const msg = errorInfo?.Message ?? errorInfo?.message ?? "RPC error";
1708
- const errorType = errorInfo?.TypeRef ?? errorInfo?.typeRef;
1709
- let typeName;
1710
- if (typeof errorType === "string") {
1711
- const commaIdx = errorType.indexOf(",");
1712
- typeName = commaIdx >= 0 ? errorType.slice(0, commaIdx) : errorType;
1713
- } else if (errorType !== null && typeof errorType === "object") {
1714
- const t = errorType;
1715
- typeName = t.TypeName ?? t.typeName;
1716
- }
1857
+ const typeName = extractTypeName(errorInfo?.TypeRef ?? errorInfo?.typeRef);
1717
1858
  if (typeName === "ActualLab.Rpc.RpcRerouteException")
1718
1859
  warnLog5?.log("Got RpcRerouteException from remote peer:", msg);
1719
1860
  call.result.reject(new RpcError(msg, typeName));
@@ -1721,17 +1862,29 @@ var RpcSystemCallHandler = class {
1721
1862
  break;
1722
1863
  }
1723
1864
  case RpcSystemCalls.cancel: {
1724
- peer.inboundCalls.remove(relatedId);
1865
+ const call = peer.inboundCalls.remove(relatedId);
1866
+ call?.cancel();
1725
1867
  break;
1726
1868
  }
1727
1869
  case RpcSystemCalls.keepAlive: {
1728
1870
  peer.notifyKeepAliveReceived();
1871
+ const keepAliveIds = args[0];
1872
+ if (Array.isArray(keepAliveIds) && keepAliveIds.length > 0 && peer.connection !== void 0) {
1873
+ const unknownIds = [];
1874
+ for (const id of keepAliveIds)
1875
+ if (peer.sharedObjects.get(id) === void 0)
1876
+ unknownIds.push(id);
1877
+ if (unknownIds.length > 0)
1878
+ peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, unknownIds);
1879
+ }
1729
1880
  break;
1730
1881
  }
1731
1882
  case RpcSystemCalls.item: {
1732
1883
  const stream = peer.remoteObjects.get(relatedId);
1733
1884
  if (!stream)
1734
1885
  debugLog?.log(`$sys.I: no stream for relatedId=${relatedId}`);
1886
+ else if (args.length > 2)
1887
+ stream.onEnd(args[0], polymorphicPayloadError(method, args.length, 2));
1735
1888
  else
1736
1889
  stream.onItem(
1737
1890
  args[0],
@@ -1743,6 +1896,8 @@ var RpcSystemCallHandler = class {
1743
1896
  const stream = peer.remoteObjects.get(relatedId);
1744
1897
  if (!stream)
1745
1898
  debugLog?.log(`$sys.B: no stream for relatedId=${relatedId}`);
1899
+ else if (args.length > 2)
1900
+ stream.onEnd(args[0], polymorphicPayloadError(method, args.length, 2));
1746
1901
  else if (Array.isArray(args[1])) {
1747
1902
  const items = args[1];
1748
1903
  for (let i = 0; i < items.length; i++)
@@ -1757,20 +1912,32 @@ var RpcSystemCallHandler = class {
1757
1912
  debugLog?.log(`$sys.End: no stream for relatedId=${relatedId}`);
1758
1913
  } else {
1759
1914
  const errorInfo = args[1];
1915
+ const typeName = extractTypeName(errorInfo?.TypeRef ?? errorInfo?.typeRef);
1760
1916
  const msg = errorInfo?.Message ?? errorInfo?.message;
1761
- const error = msg ? new Error(msg) : null;
1917
+ let error = null;
1918
+ if (typeName || msg) {
1919
+ error = new Error(msg ?? typeName ?? "RPC stream error");
1920
+ if (typeName)
1921
+ error.name = typeName;
1922
+ }
1762
1923
  stream.onEnd(args[0], error);
1763
1924
  }
1764
1925
  break;
1765
1926
  }
1766
1927
  case RpcSystemCalls.ack: {
1767
1928
  const sender = peer.sharedObjects.get(relatedId);
1768
- sender?.onAck(args[0], args[1]);
1929
+ if (sender)
1930
+ sender.onAck(args[0], args[1]);
1931
+ else if (peer.connection !== void 0)
1932
+ peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, [relatedId]);
1769
1933
  break;
1770
1934
  }
1771
1935
  case RpcSystemCalls.ackEnd: {
1772
1936
  const sender = peer.sharedObjects.get(relatedId);
1773
- sender?.onAckEnd(args[0]);
1937
+ if (sender)
1938
+ sender.onAckEnd(args[0]);
1939
+ else if (peer.connection !== void 0)
1940
+ peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, [relatedId]);
1774
1941
  break;
1775
1942
  }
1776
1943
  case RpcSystemCalls.disconnect: {
@@ -1804,19 +1971,34 @@ var RpcSystemCallHandler = class {
1804
1971
  */
1805
1972
  _handleReconnect(relatedId, args, peer) {
1806
1973
  if (peer.connection === void 0) return;
1974
+ const handshakeIndex = args[0];
1975
+ if (typeof handshakeIndex === "number" && handshakeIndex !== peer.ownHandshakeIndex) {
1976
+ peer.hub.systemCallSender.error(
1977
+ peer.connection,
1978
+ peer.serializationFormat,
1979
+ relatedId,
1980
+ new Error(
1981
+ `TooLateToReconnect: own handshake index ${peer.ownHandshakeIndex} != ${handshakeIndex}`
1982
+ )
1983
+ );
1984
+ return;
1985
+ }
1807
1986
  let unknownIds = [];
1808
1987
  try {
1809
1988
  const stagesRaw = args[1];
1810
1989
  if (stagesRaw && typeof stagesRaw === "object" && !Array.isArray(stagesRaw)) {
1811
1990
  const unknownSet = /* @__PURE__ */ new Set();
1812
- for (const [, rawValue] of Object.entries(stagesRaw)) {
1991
+ for (const [stageKey, rawValue] of Object.entries(stagesRaw)) {
1813
1992
  const bytes = _toBytes(rawValue);
1814
1993
  if (bytes === null) continue;
1994
+ const completedStage = parseInt(stageKey, 10) || 0;
1815
1995
  const callIds = IncreasingSeqCompressor.deserialize(bytes);
1816
1996
  for (const callId of callIds) {
1817
- if (peer.inboundCalls.get(callId) === void 0) {
1997
+ const call = peer.inboundCalls.get(callId);
1998
+ if (call === void 0)
1818
1999
  unknownSet.add(callId);
1819
- }
2000
+ else if (completedStage < 1)
2001
+ call.resendResult();
1820
2002
  }
1821
2003
  }
1822
2004
  unknownIds = [...unknownSet].sort((a, b) => a - b);
@@ -1828,6 +2010,29 @@ var RpcSystemCallHandler = class {
1828
2010
  peer.hub.systemCallSender.ok(peer.connection, peer.serializationFormat, relatedId, responseValue);
1829
2011
  }
1830
2012
  };
2013
+ function polymorphicPayloadError(method, got, expected) {
2014
+ return new Error(
2015
+ `${method}: expected ${expected} value(s), got ${got} \u2014 polymorphic payloads are not supported by the TS client`
2016
+ );
2017
+ }
2018
+ function extractTypeName(typeRef) {
2019
+ if (typeof typeRef === "string") {
2020
+ if (typeRef.length === 0)
2021
+ return void 0;
2022
+ const commaIdx = typeRef.indexOf(",");
2023
+ return commaIdx >= 0 ? typeRef.slice(0, commaIdx) : typeRef;
2024
+ }
2025
+ if (typeRef !== null && typeof typeRef === "object") {
2026
+ const t = typeRef;
2027
+ const aqn = t.AssemblyQualifiedName ?? t.assemblyQualifiedName;
2028
+ if (typeof aqn === "string" && aqn.length > 0) {
2029
+ const commaIdx = aqn.indexOf(",");
2030
+ return commaIdx >= 0 ? aqn.slice(0, commaIdx) : aqn;
2031
+ }
2032
+ return t.TypeName ?? t.typeName;
2033
+ }
2034
+ return void 0;
2035
+ }
1831
2036
  function _toBytes(value) {
1832
2037
  if (value instanceof Uint8Array) return value;
1833
2038
  if (typeof value === "string") {
@@ -1911,12 +2116,11 @@ var RpcSystemCallSender = class _RpcSystemCallSender {
1911
2116
  }
1912
2117
  }
1913
2118
  error(conn, format, relatedId, error) {
1914
- const message = error instanceof Error ? error.message : String(error);
1915
2119
  this._send(
1916
2120
  conn,
1917
2121
  format,
1918
2122
  { Method: RpcSystemCalls.error, RelatedId: relatedId },
1919
- [{ Message: message }]
2123
+ [toExceptionInfo(error)]
1920
2124
  );
1921
2125
  }
1922
2126
  cancel(conn, format, relatedId) {
@@ -1930,6 +2134,14 @@ var RpcSystemCallSender = class _RpcSystemCallSender {
1930
2134
  activeCallIds
1931
2135
  ]);
1932
2136
  }
2137
+ // Tells the remote peer the listed shared-object ids are gone on our side,
2138
+ // so its matching remote objects (e.g. RpcStream consumers) fail fast
2139
+ // instead of hanging. Mirrors .NET RpcSystemCallSender.Disconnect.
2140
+ disconnect(conn, format, localIds) {
2141
+ this._send(conn, format, { Method: RpcSystemCalls.disconnect }, [
2142
+ localIds
2143
+ ]);
2144
+ }
1933
2145
  item(conn, format, localId, index, item) {
1934
2146
  this._itemEnv.RelatedId = localId;
1935
2147
  this._itemArgs[0] = index;
@@ -1967,7 +2179,13 @@ var RpcSystemCallSender = class _RpcSystemCallSender {
1967
2179
  var RpcRemoteObjectTracker = class {
1968
2180
  _objects = /* @__PURE__ */ new Map();
1969
2181
  register(obj) {
1970
- this._objects.set(obj.id.localId, obj);
2182
+ const localId = obj.id.localId;
2183
+ const existing = this._objects.get(localId);
2184
+ if (existing === obj)
2185
+ return;
2186
+ if (existing !== void 0)
2187
+ existing.disconnect();
2188
+ this._objects.set(localId, obj);
1971
2189
  }
1972
2190
  get(localId) {
1973
2191
  return this._objects.get(localId);
@@ -1976,7 +2194,9 @@ var RpcRemoteObjectTracker = class {
1976
2194
  return this._objects.keys();
1977
2195
  }
1978
2196
  unregister(obj) {
1979
- this._objects.delete(obj.id.localId);
2197
+ const localId = obj.id.localId;
2198
+ if (this._objects.get(localId) === obj)
2199
+ this._objects.delete(localId);
1980
2200
  }
1981
2201
  disconnectAll() {
1982
2202
  for (const obj of this._objects.values()) {
@@ -2000,7 +2220,10 @@ var RpcSharedObjectTracker = class {
2000
2220
  return this._nextId++;
2001
2221
  }
2002
2222
  register(obj) {
2003
- this._objects.set(obj.id.localId, obj);
2223
+ const localId = obj.id.localId;
2224
+ if (this._objects.has(localId))
2225
+ throw new Error(`RPC shared object with localId ${localId} is already registered.`);
2226
+ this._objects.set(localId, obj);
2004
2227
  }
2005
2228
  get(localId) {
2006
2229
  return this._objects.get(localId);
@@ -2009,7 +2232,9 @@ var RpcSharedObjectTracker = class {
2009
2232
  return this._objects.keys();
2010
2233
  }
2011
2234
  unregister(obj) {
2012
- this._objects.delete(obj.id.localId);
2235
+ const localId = obj.id.localId;
2236
+ if (this._objects.get(localId) === obj)
2237
+ this._objects.delete(localId);
2013
2238
  }
2014
2239
  disconnectAll() {
2015
2240
  for (const obj of this._objects.values()) {
@@ -2020,7 +2245,7 @@ var RpcSharedObjectTracker = class {
2020
2245
  };
2021
2246
 
2022
2247
  // src/rpc-peer.ts
2023
- import { EventHandlerSet as EventHandlerSet3, PromiseSource as PromiseSource5, TimeoutError, withTimeout } from "@actuallab/core";
2248
+ import { EventHandlerSet as EventHandlerSet3, PromiseSource as PromiseSource5, TimeoutError, cancellationError, withTimeout } from "@actuallab/core";
2024
2249
 
2025
2250
  // src/rpc-serialization-format.ts
2026
2251
  import { Decoder as Decoder3 } from "@msgpack/msgpack";
@@ -2039,9 +2264,7 @@ var RpcSerializationFormat = class {
2039
2264
  static NewtonsoftJsonV5;
2040
2265
  static NewtonsoftJsonV5NP;
2041
2266
  static MessagePackV6;
2042
- static MemoryPackV6;
2043
2267
  static MessagePackV6C;
2044
- static MemoryPackV6C;
2045
2268
  static All;
2046
2269
  /** Convenience shortcut for RpcSerializationFormatResolver.Default.get(key). */
2047
2270
  static get(key) {
@@ -2137,18 +2360,14 @@ RpcSerializationFormat.SystemJsonV5NP = new RpcJsonSerializationFormat("json5np"
2137
2360
  RpcSerializationFormat.NewtonsoftJsonV5 = new RpcJsonSerializationFormat("njson5");
2138
2361
  RpcSerializationFormat.NewtonsoftJsonV5NP = new RpcJsonSerializationFormat("njson5np");
2139
2362
  RpcSerializationFormat.MessagePackV6 = new RpcMessagePackSerializationFormat("msgpack6");
2140
- RpcSerializationFormat.MemoryPackV6 = new RpcMessagePackSerializationFormat("mempack6");
2141
2363
  RpcSerializationFormat.MessagePackV6C = new RpcMessagePackCompactSerializationFormat("msgpack6c");
2142
- RpcSerializationFormat.MemoryPackV6C = new RpcMessagePackCompactSerializationFormat("mempack6c");
2143
2364
  RpcSerializationFormat.All = [
2144
2365
  RpcSerializationFormat.SystemJsonV5,
2145
2366
  RpcSerializationFormat.SystemJsonV5NP,
2146
2367
  RpcSerializationFormat.NewtonsoftJsonV5,
2147
2368
  RpcSerializationFormat.NewtonsoftJsonV5NP,
2148
2369
  RpcSerializationFormat.MessagePackV6,
2149
- RpcSerializationFormat.MemoryPackV6,
2150
- RpcSerializationFormat.MessagePackV6C,
2151
- RpcSerializationFormat.MemoryPackV6C
2370
+ RpcSerializationFormat.MessagePackV6C
2152
2371
  ];
2153
2372
  var RpcSerializationFormatResolver = class _RpcSerializationFormatResolver {
2154
2373
  static DefaultFormats = RpcSerializationFormat.All;
@@ -2168,6 +2387,8 @@ var RpcSerializationFormatResolver = class _RpcSerializationFormatResolver {
2168
2387
  this.formats = map;
2169
2388
  }
2170
2389
  get(key) {
2390
+ if (isMemoryPackKey(key))
2391
+ throw new Error(`Serialization format '${key}': MemoryPack formats are not supported by the TS client.`);
2171
2392
  const format = this.tryGet(key);
2172
2393
  if (!format)
2173
2394
  throw new Error(`No format with key '${key}'.`);
@@ -2175,9 +2396,14 @@ var RpcSerializationFormatResolver = class _RpcSerializationFormatResolver {
2175
2396
  }
2176
2397
  tryGet(key) {
2177
2398
  if (!key) key = this.defaultFormatKey;
2399
+ if (isMemoryPackKey(key))
2400
+ throw new Error(`Serialization format '${key}': MemoryPack formats are not supported by the TS client.`);
2178
2401
  return this.formats.get(key);
2179
2402
  }
2180
2403
  };
2404
+ function isMemoryPackKey(key) {
2405
+ return key.startsWith("mempack");
2406
+ }
2181
2407
 
2182
2408
  // src/rpc-peer.ts
2183
2409
  function yieldToEventLoop() {
@@ -2231,6 +2457,10 @@ var RpcPeer = class {
2231
2457
  * attempt (the reconnect delay is part of this phase); `Connected` is
2232
2458
  * entered only after the handshake has succeeded. */
2233
2459
  connectionStateChanged = new EventHandlerSet3();
2460
+ /** Index of the handshake THIS peer most recently sent. Used to reject a
2461
+ * stale `$sys.Reconnect` that references an older connection generation
2462
+ * (C# `RpcSystemCalls.Reconnect`'s `ownHandshake.Index` check). */
2463
+ _ownHandshakeIndex = 0;
2234
2464
  _connection;
2235
2465
  /** Mirror of `_connectionState === Connected`. True only after the
2236
2466
  * handshake round-trip completes; outbound `call()` checks this to
@@ -2242,6 +2472,7 @@ var RpcPeer = class {
2242
2472
  _connectionState = 0 /* Disconnected */;
2243
2473
  _keepAliveTimer;
2244
2474
  _keepAliveWatchdog;
2475
+ _maintainTimer;
2245
2476
  /** Wall-clock time of the last inbound `$sys.KeepAlive`, or 0 if none yet
2246
2477
  * for the current connection. Useful for diagnostics. */
2247
2478
  _lastKeepAliveAt = 0;
@@ -2256,6 +2487,37 @@ var RpcPeer = class {
2256
2487
  this.ref = ref;
2257
2488
  this.keepAlivePeriodMs = hub.limits.keepAlivePeriodMs;
2258
2489
  this.keepAliveTimeoutMs = hub.limits.keepAliveTimeoutMs;
2490
+ this.inboundCalls.completedCallsLimit = hub.limits.completedInboundCallsLimit;
2491
+ const checkPeriod = hub.limits.callTimeoutCheckPeriodMs;
2492
+ if (checkPeriod > 0 && Number.isFinite(checkPeriod)) {
2493
+ this._maintainTimer = setInterval(() => this._maintainOutboundCalls(), checkPeriod);
2494
+ this._maintainTimer.unref?.();
2495
+ }
2496
+ }
2497
+ /** Fail outbound calls whose per-call {@link RpcCallTimeouts} elapsed —
2498
+ * the connect timeout while a call still awaits the wire, the run timeout
2499
+ * once it has been sent. Mirrors .NET `RpcOutboundCallTracker.Maintain`
2500
+ * (RpcCallTrackers.cs:108-207). Calls without timeouts (queries) never
2501
+ * time out here. */
2502
+ _maintainOutboundCalls() {
2503
+ const now = Date.now();
2504
+ const canRunTimeout = this._isConnected && (this._lastKeepAliveAt === 0 || now - this._lastKeepAliveAt <= this.keepAliveTimeoutMs);
2505
+ for (const call of [...this.outboundCalls.values()]) {
2506
+ const timeouts = call.timeouts;
2507
+ if (timeouts === void 0 || call.result.isCompleted)
2508
+ continue;
2509
+ let timedOut;
2510
+ if (call.sentAt === 0) {
2511
+ if (now - call.startedAt >= timeouts.connectTimeoutMs)
2512
+ timedOut = `connect timeout (${timeouts.connectTimeoutMs}ms)`;
2513
+ } else if (canRunTimeout && now - call.sentAt >= timeouts.runTimeoutMs)
2514
+ timedOut = `run timeout (${timeouts.runTimeoutMs}ms)`;
2515
+ if (timedOut === void 0)
2516
+ continue;
2517
+ this.outboundCalls.remove(call.callId);
2518
+ call.result.reject(new TimeoutError(`'${this.ref}': call ${call.method}#${call.callId} timed out \u2014 ${timedOut}`));
2519
+ call.onDisconnect();
2520
+ }
2259
2521
  }
2260
2522
  get connection() {
2261
2523
  return this._connection;
@@ -2266,6 +2528,9 @@ var RpcPeer = class {
2266
2528
  get isConnected() {
2267
2529
  return this._isConnected;
2268
2530
  }
2531
+ get ownHandshakeIndex() {
2532
+ return this._ownHandshakeIndex;
2533
+ }
2269
2534
  _setConnectionState(value) {
2270
2535
  if (this._connectionState === value) return;
2271
2536
  const previousState = this._connectionState;
@@ -2337,14 +2602,17 @@ var RpcPeer = class {
2337
2602
  this._connection?.encoder,
2338
2603
  this.hub.registry
2339
2604
  );
2605
+ outboundCall.timeouts = options?.timeouts;
2340
2606
  this.outboundCalls.register(outboundCall);
2341
2607
  if (this._isConnected) {
2608
+ outboundCall.sentAt = Date.now();
2342
2609
  this._sendWireData(outboundCall.serializedWireData);
2343
2610
  } else {
2344
2611
  const stateHandler = (state) => {
2345
2612
  if (state !== 3 /* Connected */) return;
2346
2613
  this.connectionStateChanged.remove(stateHandler);
2347
2614
  if (outboundCall.result.isCompleted) return;
2615
+ outboundCall.sentAt = Date.now();
2348
2616
  this._sendWireData(outboundCall.serializedWireData);
2349
2617
  };
2350
2618
  this.connectionStateChanged.add(stateHandler);
@@ -2354,9 +2622,9 @@ var RpcPeer = class {
2354
2622
  if (signal !== void 0) {
2355
2623
  const onAbort = () => {
2356
2624
  if (this.outboundCalls.remove(callId) !== void 0) {
2357
- outboundCall.result.reject(new Error("Call cancelled."));
2625
+ outboundCall.result.reject(cancellationError("Call cancelled."));
2358
2626
  outboundCall.onDisconnect();
2359
- if (this._connection !== void 0)
2627
+ if (this._isConnected && this._connection !== void 0)
2360
2628
  this.hub.systemCallSender.cancel(this._connection, this.serializationFormat, callId);
2361
2629
  }
2362
2630
  };
@@ -2386,10 +2654,15 @@ var RpcPeer = class {
2386
2654
  }
2387
2655
  close() {
2388
2656
  infoLog?.log(`'${this.ref}': Stopping`);
2657
+ if (this._maintainTimer !== void 0) {
2658
+ clearInterval(this._maintainTimer);
2659
+ this._maintainTimer = void 0;
2660
+ }
2389
2661
  this.remoteObjects.disconnectAll();
2390
2662
  this.sharedObjects.disconnectAll();
2391
- this.outboundCalls.rejectAll(new Error("Peer closed."));
2663
+ this.outboundCalls.rejectAll(cancellationError("Peer closed."));
2392
2664
  this.outboundCalls.invalidateAll();
2665
+ this.inboundCalls.clear();
2393
2666
  this._connection?.close();
2394
2667
  this.hub.peers.delete(this.ref);
2395
2668
  this._setConnectionState(0 /* Disconnected */);
@@ -2428,12 +2701,18 @@ var RpcPeer = class {
2428
2701
  if (method === RpcSystemCalls.handshake) {
2429
2702
  const raw = args[0];
2430
2703
  if (raw !== void 0) {
2704
+ const obj = raw ?? {};
2431
2705
  const handshake = Array.isArray(raw) ? {
2432
2706
  RemotePeerId: raw[0],
2433
2707
  RemoteHubId: raw[2],
2434
2708
  ProtocolVersion: raw[3],
2435
2709
  Index: raw[4]
2436
- } : raw;
2710
+ } : {
2711
+ RemotePeerId: obj.RemotePeerId ?? obj.remotePeerId,
2712
+ RemoteHubId: obj.RemoteHubId ?? obj.remoteHubId,
2713
+ ProtocolVersion: obj.ProtocolVersion ?? obj.protocolVersion,
2714
+ Index: obj.Index ?? obj.index
2715
+ };
2437
2716
  this._onHandshakeReceived(handshake);
2438
2717
  }
2439
2718
  return;
@@ -2447,57 +2726,85 @@ var RpcPeer = class {
2447
2726
  _handleInbound(message, args) {
2448
2727
  const method = message.Method ?? "";
2449
2728
  const relatedId = message.RelatedId ?? 0;
2450
- const call = new RpcInboundCall(relatedId, method, args);
2451
2729
  const methodDef = this.hub.serviceHost.getMethodDef(method);
2452
2730
  const isNoWait = methodDef?.noWait === true;
2453
- if (!isNoWait) {
2454
- this.inboundCalls.register(call);
2455
- }
2456
2731
  const serviceHost = this.hub.serviceHost;
2457
- void (async () => {
2732
+ const dispatch = async (call2) => {
2458
2733
  try {
2459
2734
  const context = this._connection !== void 0 ? {
2460
2735
  __rpcDispatch: true,
2461
2736
  callId: relatedId,
2462
- connection: this._connection
2737
+ callType: message.CallType ?? 0,
2738
+ connection: this._connection,
2739
+ signal: call2?.signal,
2740
+ peer: this
2463
2741
  } : void 0;
2464
- const result = await serviceHost.dispatch(
2465
- method,
2466
- args,
2467
- context
2468
- );
2469
- if (methodDef?.stream === true && this._connection !== void 0) {
2470
- const stream = result instanceof RpcStream ? result : new RpcStream(result);
2471
- this.hub.systemCallSender.ok(
2472
- this._connection,
2473
- this.serializationFormat,
2474
- relatedId,
2475
- stream.toRef(this)
2476
- );
2477
- } else if (!isNoWait && this._connection !== void 0) {
2478
- this.hub.systemCallSender.ok(
2479
- this._connection,
2480
- this.serializationFormat,
2481
- relatedId,
2482
- result
2483
- );
2742
+ const result = await serviceHost.dispatch(method, args, context);
2743
+ if (methodDef?.stream === true) {
2744
+ if (this._connection !== void 0) {
2745
+ const stream = result instanceof RpcStream ? result : new RpcStream(result);
2746
+ const ref = stream.toRef(this);
2747
+ const send = () => {
2748
+ if (call2?.isCancelled === true) return;
2749
+ if (this._connection !== void 0)
2750
+ this.hub.systemCallSender.ok(
2751
+ this._connection,
2752
+ this.serializationFormat,
2753
+ relatedId,
2754
+ ref
2755
+ );
2756
+ };
2757
+ if (call2 !== void 0) {
2758
+ call2.setResult(send);
2759
+ this.inboundCalls.markCompleted(call2);
2760
+ }
2761
+ send();
2762
+ }
2763
+ } else if (call2 !== void 0) {
2764
+ const send = () => {
2765
+ if (call2.isCancelled) return;
2766
+ if (this._connection !== void 0)
2767
+ this.hub.systemCallSender.ok(
2768
+ this._connection,
2769
+ this.serializationFormat,
2770
+ relatedId,
2771
+ result
2772
+ );
2773
+ };
2774
+ call2.setResult(send);
2775
+ this.inboundCalls.markCompleted(call2);
2776
+ send();
2484
2777
  }
2485
2778
  } catch (e) {
2486
- if (!isNoWait && this._connection !== void 0) {
2487
- this.hub.systemCallSender.error(
2488
- this._connection,
2489
- this.serializationFormat,
2490
- relatedId,
2491
- e
2492
- );
2779
+ if (call2 !== void 0) {
2780
+ const send = () => {
2781
+ if (call2.isCancelled) return;
2782
+ if (this._connection !== void 0)
2783
+ this.hub.systemCallSender.error(
2784
+ this._connection,
2785
+ this.serializationFormat,
2786
+ relatedId,
2787
+ e
2788
+ );
2789
+ };
2790
+ call2.setResult(send);
2791
+ this.inboundCalls.markCompleted(call2);
2792
+ send();
2493
2793
  }
2494
2794
  debugLog2?.log(`'${this.ref}': Inbound call failed: ${method}#${relatedId}`, e);
2495
- } finally {
2496
- if (!isNoWait) {
2497
- this.inboundCalls.remove(relatedId);
2498
- }
2499
2795
  }
2500
- })();
2796
+ };
2797
+ if (isNoWait) {
2798
+ void dispatch(void 0);
2799
+ return;
2800
+ }
2801
+ const call = new RpcInboundCall(relatedId, method, args);
2802
+ const existing = this.inboundCalls.getOrRegister(call);
2803
+ if (existing !== call) {
2804
+ existing.resendResult();
2805
+ return;
2806
+ }
2807
+ void dispatch(call);
2501
2808
  }
2502
2809
  /** Start sending `$sys.KeepAlive` pings and arm the silence watchdog.
2503
2810
  * Called when the peer transitions into `Connected` (after handshake).
@@ -2553,15 +2860,17 @@ var RpcConnectionState = /* @__PURE__ */ ((RpcConnectionState2) => {
2553
2860
  })(RpcConnectionState || {});
2554
2861
  var RpcClientPeer = class extends RpcPeer {
2555
2862
  _disposed = false;
2863
+ /** Aborted when the peer is stopped/disposed — threaded into the reconnect
2864
+ * delay so a pending delay rejects promptly instead of parking. */
2865
+ _stopController = new AbortController();
2556
2866
  _tryIndex = 0;
2557
- _handshakeIndex = 0;
2558
2867
  /** Index from the MOST RECENTLY RECEIVED server handshake — sent back
2559
2868
  * to the server in `$sys.Reconnect:3` so the server can verify we're
2560
2869
  * talking about the current connection. Mirrors .NET's
2561
2870
  * `connectionState.Handshake.Index` path in
2562
2871
  * `RpcOutboundCallTracker.TryReconnect`. */
2563
2872
  _remoteHandshakeIndex = 0;
2564
- _lastRemoteHubId;
2873
+ _lastRemotePeerId;
2565
2874
  _pendingHandshake;
2566
2875
  _reconnectsAt = 0;
2567
2876
  /** Base64url-encoded peer ID — matches .NET's RpcClientPeer.ClientId (Guid.ToBase64Url). */
@@ -2582,6 +2891,10 @@ var RpcClientPeer = class extends RpcPeer {
2582
2891
  * the connection attempt. Initialized from `hub.limits` at construction;
2583
2892
  * set directly to override on a single peer. */
2584
2893
  handshakeTimeoutMs;
2894
+ /** A connection that lived less than this before dropping does NOT reset
2895
+ * `_tryIndex`, so a crash-looping server sees growing reconnect delays.
2896
+ * Initialized from `hub.limits` at construction; override per peer. */
2897
+ prematureDisconnectTimeoutMs;
2585
2898
  peerChanged = new EventHandlerSet3();
2586
2899
  /** Fired when the server rejects the connection due to an unsupported serialization format (close code 4010). */
2587
2900
  unsupportedFormat = new EventHandlerSet3();
@@ -2614,6 +2927,7 @@ var RpcClientPeer = class extends RpcPeer {
2614
2927
  super(hub, url);
2615
2928
  this.connectTimeoutMs = hub.limits.connectTimeoutMs;
2616
2929
  this.handshakeTimeoutMs = hub.limits.handshakeTimeoutMs;
2930
+ this.prematureDisconnectTimeoutMs = hub.limits.prematureDisconnectTimeoutMs;
2617
2931
  this.clientId = guidToBase64Url(this.id);
2618
2932
  const resolver = RpcSerializationFormatResolver.Default;
2619
2933
  this.serializationFormat = resolver.get(parseFormatFromUrl(url) ?? resolver.defaultFormatKey);
@@ -2650,7 +2964,7 @@ var RpcClientPeer = class extends RpcPeer {
2650
2964
  try {
2651
2965
  while (!this._disposed) {
2652
2966
  this._setConnectionState(1 /* Connecting */);
2653
- const retryDelay = this.hub.reconnectDelayer.getDelay(this._tryIndex);
2967
+ const retryDelay = this.hub.reconnectDelayer.getDelay(this._tryIndex, this._stopController.signal);
2654
2968
  if (retryDelay.isLimitExceeded) {
2655
2969
  warnLog6?.log(`'${this.ref}': Reconnect limit exceeded - shutting down`);
2656
2970
  this._disposed = true;
@@ -2659,12 +2973,14 @@ var RpcClientPeer = class extends RpcPeer {
2659
2973
  this._setReconnectsAt(retryDelay.endsAt);
2660
2974
  try {
2661
2975
  await retryDelay.promise;
2976
+ } catch {
2662
2977
  } finally {
2663
2978
  this._setReconnectsAt(0);
2664
2979
  }
2665
2980
  if (this._disposed) break;
2666
2981
  let lastCloseCode = 0;
2667
2982
  let lastCloseReason = "";
2983
+ let connectedAt = 0;
2668
2984
  try {
2669
2985
  const connUrl = await this.connectionUrlResolver(this);
2670
2986
  infoLog?.log(`'${this.ref}': Connecting to ${sanitizeUrl(connUrl)}`);
@@ -2711,7 +3027,7 @@ var RpcClientPeer = class extends RpcPeer {
2711
3027
  this.serializationFormat,
2712
3028
  this.id,
2713
3029
  this.hub.hubId,
2714
- ++this._handshakeIndex
3030
+ ++this._ownHandshakeIndex
2715
3031
  );
2716
3032
  } catch (e) {
2717
3033
  warnLog6?.log(`'${this.ref}': Failed to send handshake`, e);
@@ -2735,20 +3051,10 @@ var RpcClientPeer = class extends RpcPeer {
2735
3051
  this._pendingHandshake = void 0;
2736
3052
  }
2737
3053
  this._remoteHandshakeIndex = remoteHandshake.Index ?? 0;
2738
- const remoteHubId = remoteHandshake.RemoteHubId;
2739
- let isPeerChanged = false;
2740
- if (remoteHubId !== void 0) {
2741
- isPeerChanged = this._lastRemoteHubId !== void 0 && this._lastRemoteHubId !== remoteHubId;
2742
- if (isPeerChanged) {
2743
- this.inboundCalls.clear();
2744
- infoLog?.log(`'${this.ref}': Peer changed`);
2745
- this.peerChanged.trigger();
2746
- }
2747
- this._lastRemoteHubId = remoteHubId;
2748
- }
3054
+ const isPeerChanged = this._detectPeerChange(remoteHandshake);
2749
3055
  infoLog?.log(`'${this.ref}': Handshake succeeded, peerChanged=${isPeerChanged}`);
2750
3056
  this._setConnectionState(3 /* Connected */);
2751
- this._tryIndex = 0;
3057
+ connectedAt = Date.now();
2752
3058
  await this._reconnect(isPeerChanged, closedController.signal);
2753
3059
  await whenClosed;
2754
3060
  } catch (e) {
@@ -2762,6 +3068,8 @@ var RpcClientPeer = class extends RpcPeer {
2762
3068
  this._disposed = true;
2763
3069
  break;
2764
3070
  }
3071
+ if (connectedAt !== 0 && Date.now() - connectedAt >= this.prematureDisconnectTimeoutMs)
3072
+ this._tryIndex = 0;
2765
3073
  this._tryIndex++;
2766
3074
  }
2767
3075
  } finally {
@@ -2817,6 +3125,8 @@ var RpcClientPeer = class extends RpcPeer {
2817
3125
  continue;
2818
3126
  }
2819
3127
  eligible.push(call);
3128
+ if (call.sentAt !== 0)
3129
+ call.sentAt = Date.now();
2820
3130
  }
2821
3131
  let unknownIds = null;
2822
3132
  if (!isPeerChanged && eligible.length > 0) {
@@ -2833,6 +3143,7 @@ var RpcClientPeer = class extends RpcPeer {
2833
3143
  if (unknownIds !== null && !unknownIds.has(call.callId)) {
2834
3144
  continue;
2835
3145
  }
3146
+ call.sentAt = Date.now();
2836
3147
  this._sendWireData(call.serializedWireData);
2837
3148
  resentSinceYield++;
2838
3149
  if (resentSinceYield >= YIELD_BATCH) {
@@ -2898,30 +3209,80 @@ var RpcClientPeer = class extends RpcPeer {
2898
3209
  return new Set(eligible.map((c) => c.callId));
2899
3210
  }
2900
3211
  }
3212
+ /** Detects a server-peer identity change by comparing the incoming
3213
+ * handshake's `RemotePeerId` with the last one — C#
3214
+ * `RpcHandshake.GetPeerChangeKind` keys off `RemotePeerId`, not
3215
+ * `RemoteHubId` (an idle server peer can be replaced while keeping the
3216
+ * same hub id). On a change it clears the inbound tracker and fires
3217
+ * `peerChanged`; the very first handshake is never a change. */
3218
+ _detectPeerChange(handshake) {
3219
+ const remotePeerId = handshake.RemotePeerId;
3220
+ if (remotePeerId === void 0)
3221
+ return false;
3222
+ const isPeerChanged = this._lastRemotePeerId !== void 0 && this._lastRemotePeerId !== remotePeerId;
3223
+ this._lastRemotePeerId = remotePeerId;
3224
+ if (isPeerChanged) {
3225
+ this.inboundCalls.clear();
3226
+ infoLog?.log(`'${this.ref}': Peer changed`);
3227
+ this.peerChanged.trigger();
3228
+ }
3229
+ return isPeerChanged;
3230
+ }
2901
3231
  _onHandshakeReceived(handshake) {
2902
3232
  this._pendingHandshake?.resolve(handshake);
2903
3233
  }
2904
3234
  close() {
2905
3235
  this._disposed = true;
3236
+ this._stopController.abort();
2906
3237
  super.close();
2907
3238
  }
2908
3239
  };
2909
3240
  var RpcServerPeer = class extends RpcPeer {
2910
- // Needed to widen the base `protected constructor` to public.
2911
- // eslint-disable-next-line @typescript-eslint/no-useless-constructor
3241
+ /** Grace period before an idle (disconnected) server peer is stopped and
3242
+ * removed from `hub.peers`; a re-`accept()` within it cancels removal.
3243
+ * Snapshotted from `hub.limits` at construction (F11). */
3244
+ serverPeerCloseTimeoutMs;
3245
+ _closeTimer;
3246
+ _isClosed = false;
2912
3247
  constructor(hub, ref) {
2913
3248
  super(hub, ref);
3249
+ this.serverPeerCloseTimeoutMs = hub.limits.serverPeerCloseTimeoutMs;
2914
3250
  }
2915
3251
  /** Accept an incoming connection — sets up message handling and handshake response. */
2916
3252
  accept(conn) {
3253
+ this._clearCloseTimer();
2917
3254
  this._setConnectionState(1 /* Connecting */);
2918
3255
  this.setupConnection(conn);
2919
3256
  this._setConnectionState(2 /* Handshaking */);
2920
3257
  conn.closed.add(() => {
2921
3258
  if (this._connection !== conn && this._connection !== void 0) return;
2922
3259
  this.sharedObjects.disconnectAll();
3260
+ this._armCloseTimer();
2923
3261
  });
2924
3262
  }
3263
+ close() {
3264
+ this._isClosed = true;
3265
+ this._clearCloseTimer();
3266
+ super.close();
3267
+ }
3268
+ _armCloseTimer() {
3269
+ this._clearCloseTimer();
3270
+ if (this._isClosed) return;
3271
+ const timeout = this.serverPeerCloseTimeoutMs;
3272
+ if (!(timeout > 0) || !Number.isFinite(timeout)) return;
3273
+ this._closeTimer = setTimeout(() => {
3274
+ this._closeTimer = void 0;
3275
+ if (this._connection === void 0)
3276
+ this.close();
3277
+ }, timeout);
3278
+ this._closeTimer.unref?.();
3279
+ }
3280
+ _clearCloseTimer() {
3281
+ if (this._closeTimer !== void 0) {
3282
+ clearTimeout(this._closeTimer);
3283
+ this._closeTimer = void 0;
3284
+ }
3285
+ }
2925
3286
  _onHandshakeReceived(_handshake) {
2926
3287
  if (this._connection !== void 0) {
2927
3288
  this.hub.systemCallSender.handshake(
@@ -2929,7 +3290,7 @@ var RpcServerPeer = class extends RpcPeer {
2929
3290
  this.serializationFormat,
2930
3291
  this.id,
2931
3292
  this.hub.hubId,
2932
- 0
3293
+ ++this._ownHandshakeIndex
2933
3294
  );
2934
3295
  this._setConnectionState(3 /* Connected */);
2935
3296
  }
@@ -2968,6 +3329,12 @@ var RpcLimits = class _RpcLimits {
2968
3329
  /** Process-wide default. Hubs read from this when they're constructed
2969
3330
  * unless caller assigns a custom instance to `hub.limits`. */
2970
3331
  static Default = new _RpcLimits();
3332
+ /** Dev preset with a relaxed keep-alive timeout (5 min), matching .NET's
3333
+ * debugger-attached `KeepAlivePeriod` (RpcLimits.cs:46-54). Activate
3334
+ * explicitly — e.g. `RpcLimits.Default = RpcLimits.Debug` or per-hub
3335
+ * `hub.limits = RpcLimits.Debug`. Never switched on automatically: the
3336
+ * TS side can't detect a debugger on the server (decision D4). */
3337
+ static Debug = new _RpcLimits({ keepAliveTimeoutMs: 3e5 });
2971
3338
  /** Max time to wait for the WebSocket to enter the OPEN state. On a
2972
3339
  * hung connect (mobile after network change, half-open after sleep)
2973
3340
  * the browser can take ~2 min to emit `onerror`/`onclose`; without
@@ -2975,6 +3342,22 @@ var RpcLimits = class _RpcLimits {
2975
3342
  connectTimeoutMs = 1e4;
2976
3343
  /** Max time to wait for the server's handshake response after WS opens. */
2977
3344
  handshakeTimeoutMs = 1e4;
3345
+ /** If a connection lived less than this before dropping, a graceful close
3346
+ * still counts as a failed attempt — the client keeps its growing
3347
+ * `_tryIndex` instead of resetting it, so a crash-looping server sees
3348
+ * increasing reconnect delays via `RetryDelaySeq`. Mirrors .NET
3349
+ * `RpcLimits.PrematureDisconnectTimeout` (RpcLimits.cs:18). */
3350
+ prematureDisconnectTimeoutMs = 15e3;
3351
+ /** Max completed inbound calls retained for duplicate-frame dedup
3352
+ * (a resent call id re-sends the computed result instead of
3353
+ * re-executing the handler). Deviation from .NET, which unregisters a
3354
+ * call once its result is sent: TS clients blind-resend on reconnect,
3355
+ * so completed calls are retained — bounded by this limit (oldest
3356
+ * evicted first) to cap memory on long-lived connections. */
3357
+ completedInboundCallsLimit = 1e3;
3358
+ /** How often each peer's maintenance loop scans outbound calls for
3359
+ * timeouts (R12). Mirrors .NET `RpcLimits.CallTimeoutCheckPeriod`. */
3360
+ callTimeoutCheckPeriodMs = 1e3;
2978
3361
  /** Outbound `$sys.KeepAlive` send period. */
2979
3362
  keepAlivePeriodMs = 1e4;
2980
3363
  /** Reaper threshold — connection is force-closed (and the reconnect
@@ -2984,6 +3367,13 @@ var RpcLimits = class _RpcLimits {
2984
3367
  * `keepAlivePeriodMs + stall_duration`, so with these defaults the
2985
3368
  * tolerated stall is ~15 s. */
2986
3369
  keepAliveTimeoutMs = 25e3;
3370
+ /** How long a server peer stays in `hub.peers` after its connection closes
3371
+ * before it's stopped and removed. A same-peer reconnect (`accept()`)
3372
+ * within this window cancels the pending removal, so brief drops don't
3373
+ * discard the peer's trackers. Mirrors .NET's server-peer shutdown timeout
3374
+ * (`RpcPeerOptions.ServerPeerShutdownTimeoutProvider`, 3-15 min); the TS
3375
+ * port uses a fixed floor-matching default. */
3376
+ serverPeerCloseTimeoutMs = 18e4;
2987
3377
  constructor(overrides) {
2988
3378
  if (overrides) Object.assign(this, overrides);
2989
3379
  }
@@ -2994,7 +3384,7 @@ import { RetryDelaySeq, RetryDelayer } from "@actuallab/core";
2994
3384
  var RpcClientPeerReconnectDelayer = class extends RetryDelayer {
2995
3385
  constructor() {
2996
3386
  super();
2997
- this.delays = RetryDelaySeq.fixed(100);
3387
+ this.delays = RetryDelaySeq.exp(100, 1e4);
2998
3388
  }
2999
3389
  };
3000
3390
 
@@ -3002,13 +3392,14 @@ var RpcClientPeerReconnectDelayer = class extends RetryDelayer {
3002
3392
  var { warnLog: warnLog7 } = getLogs("RpcServiceHost");
3003
3393
  var RpcServiceHost = class {
3004
3394
  _methods = /* @__PURE__ */ new Map();
3005
- register(def, impl) {
3395
+ register(def, impl, receiver = impl) {
3006
3396
  for (const methodDef of def.methods.values()) {
3007
3397
  const fn = impl[methodDef.name];
3008
3398
  if (!fn) continue;
3009
3399
  this._methods.set(wireMethodName(methodDef), {
3010
3400
  def: methodDef,
3011
- fn
3401
+ fn,
3402
+ receiver
3012
3403
  });
3013
3404
  }
3014
3405
  }
@@ -3019,9 +3410,9 @@ var RpcServiceHost = class {
3019
3410
  throw new Error(`Method not found: ${wireMethod}`);
3020
3411
  }
3021
3412
  if (context !== void 0 && entry.def.callTypeId !== 0) {
3022
- return await entry.fn(...args, context);
3413
+ return await entry.fn.call(entry.receiver, ...args, context);
3023
3414
  }
3024
- return await entry.fn(...args);
3415
+ return await entry.fn.call(entry.receiver, ...args);
3025
3416
  }
3026
3417
  getMethodDef(wireMethod) {
3027
3418
  return this._methods.get(wireMethod)?.def;
@@ -3029,6 +3420,7 @@ var RpcServiceHost = class {
3029
3420
  };
3030
3421
 
3031
3422
  // src/rpc-decorators.ts
3423
+ import { ownMetadata, resolveArgCount } from "@actuallab/core";
3032
3424
  var METHODS_META = /* @__PURE__ */ Symbol.for("actuallab.methods");
3033
3425
  var SERVICE_META = /* @__PURE__ */ Symbol.for("actuallab.service");
3034
3426
  function getMethodsMeta(cls) {
@@ -3039,17 +3431,17 @@ function getServiceMeta(cls) {
3039
3431
  }
3040
3432
  function rpcService(serviceName) {
3041
3433
  return function(_target, context) {
3042
- const meta = context.metadata[SERVICE_META] ??= {};
3434
+ const meta = ownMetadata(context.metadata, SERVICE_META);
3043
3435
  meta.name = serviceName;
3044
3436
  };
3045
3437
  }
3046
3438
  function rpcMethod(options) {
3047
3439
  return function(target, context) {
3048
3440
  const methodName = String(context.name);
3049
- const methods = context.metadata[METHODS_META] ??= {};
3441
+ const methods = ownMetadata(context.metadata, METHODS_META);
3050
3442
  methods[methodName] = {
3051
3443
  ...methods[methodName],
3052
- argCount: target.length,
3444
+ argCount: resolveArgCount(target, options?.argCount, methodName),
3053
3445
  returns: options?.returns,
3054
3446
  remoteExecutionMode: options?.remoteExecutionMode
3055
3447
  };
@@ -3595,7 +3987,7 @@ var RpcHub = class {
3595
3987
  hubId;
3596
3988
  peers = /* @__PURE__ */ new Map();
3597
3989
  serviceHost;
3598
- systemCallSender = new RpcSystemCallSender();
3990
+ systemCallSender;
3599
3991
  systemCallHandler = new RpcSystemCallHandler();
3600
3992
  /** Method registry for compact format hash ↔ name resolution.
3601
3993
  * Created lazily, populated by addService/addClient. */
@@ -3603,6 +3995,14 @@ var RpcHub = class {
3603
3995
  /** Shared reconnect delayer used by every client peer in this hub. Swap in
3604
3996
  * a custom subclass (e.g. app-level signal-gated) before peers start. */
3605
3997
  reconnectDelayer = new RpcClientPeerReconnectDelayer();
3998
+ /** Cache of client proxies keyed by peer, then by service name — so
3999
+ * repeated {@link addClient} calls for the same service+peer return one
4000
+ * proxy. All consumers then share a single computed/call/invalidation
4001
+ * stream per logical value, matching .NET's singleton client proxies (F8).
4002
+ * A later def registered under the same name extends the existing proxy
4003
+ * with its extra methods (superset), so no consumer ever receives a proxy
4004
+ * missing methods the def declares. */
4005
+ _clientProxies = /* @__PURE__ */ new WeakMap();
3606
4006
  /** Connection-lifecycle timing limits. Peers constructed against this hub
3607
4007
  * read their initial `*Ms` field values from this instance; later
3608
4008
  * mutations affect only peers built afterward (existing peers can be
@@ -3620,6 +4020,7 @@ var RpcHub = class {
3620
4020
  constructor(hubId) {
3621
4021
  this.hubId = hubId ?? crypto.randomUUID();
3622
4022
  this.serviceHost = new RpcServiceHost();
4023
+ this.systemCallSender = this._createSystemCallSender();
3623
4024
  this.systemCallSender.registry = this.registry;
3624
4025
  for (const methodName of Object.values(RpcSystemCalls)) {
3625
4026
  this.registry.register(methodName);
@@ -3680,50 +4081,38 @@ var RpcHub = class {
3680
4081
  if (!fn) continue;
3681
4082
  wrappedImpl[methodDef.name] = methodDef.callTypeId !== 0 ? this._wrapServerMethod(methodDef, fn, impl) : fn;
3682
4083
  }
3683
- this.serviceHost.register(def, wrappedImpl);
4084
+ this.serviceHost.register(def, wrappedImpl, impl);
3684
4085
  this.registry.registerService(def.name, def.methods);
3685
4086
  }
3686
- /** Create a typed client proxy for a service on a remote peer. */
4087
+ /** Create a typed client proxy for a service on a remote peer. Repeated calls
4088
+ * for the same (peer, service name) return one shared proxy; a def carrying
4089
+ * extra methods extends it (F8). */
3687
4090
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T is used for caller-specified proxy type
3688
4091
  addClient(peer, defOrContract) {
3689
4092
  const def = this._resolveServiceDef(defOrContract);
3690
4093
  this.registry.registerService(def.name, def.methods);
3691
- const overloads = /* @__PURE__ */ new Map();
3692
- for (const methodDef of def.methods.values()) {
3693
- let byArgCount = overloads.get(methodDef.name);
3694
- if (!byArgCount) {
3695
- byArgCount = /* @__PURE__ */ new Map();
3696
- overloads.set(methodDef.name, byArgCount);
3697
- }
3698
- byArgCount.set(
3699
- methodDef.argCount,
3700
- this._createClientMethod(peer, methodDef)
3701
- );
4094
+ let byService = this._clientProxies.get(peer);
4095
+ if (byService === void 0) {
4096
+ byService = /* @__PURE__ */ new Map();
4097
+ this._clientProxies.set(peer, byService);
3702
4098
  }
3703
- const methods = /* @__PURE__ */ new Map();
3704
- for (const [name, byArgCount] of overloads) {
3705
- if (byArgCount.size === 1) {
3706
- const [[, singleFn]] = byArgCount;
3707
- methods.set(name, singleFn);
3708
- } else {
3709
- methods.set(name, (...args) => {
3710
- const fn = byArgCount.get(args.length);
3711
- if (!fn)
3712
- throw new Error(
3713
- `No overload of ${name} accepts ${args.length} arguments`
3714
- );
3715
- return fn(...args);
3716
- });
3717
- }
4099
+ let entry = byService.get(def.name);
4100
+ if (entry === void 0) {
4101
+ entry = this._createClientProxy();
4102
+ byService.set(def.name, entry);
3718
4103
  }
3719
- return new Proxy({}, {
3720
- get: (_target, prop) => typeof prop === "string" ? methods.get(prop) : void 0
3721
- });
4104
+ this._extendClientProxy(peer, entry, def);
4105
+ return entry.proxy;
3722
4106
  }
3723
4107
  close() {
3724
4108
  for (const peer of this.peers.values()) peer.close();
3725
4109
  this.peers.clear();
3726
4110
  }
4111
+ /** Create the hub's system-call sender. Override in FusionHub to add
4112
+ * Fusion-specific system calls (e.g. $sys-c.Invalidate). */
4113
+ _createSystemCallSender() {
4114
+ return new RpcSystemCallSender();
4115
+ }
3727
4116
  /** Build an RpcServiceDef from decorator metadata on a contract class. Override in FusionHub for compute support. */
3728
4117
  _buildServiceDef(cls) {
3729
4118
  const svcMeta = getServiceMeta(cls);
@@ -3781,12 +4170,10 @@ var RpcHub = class {
3781
4170
  throw new Error(
3782
4171
  `Expected stream reference, got: ${JSON.stringify(result)}`
3783
4172
  );
3784
- const stream = new RpcStream(ref, peer);
3785
- peer.remoteObjects.register(stream);
3786
- return stream;
4173
+ return new RpcStream(ref, peer);
3787
4174
  };
3788
4175
  }
3789
- const callOptions = methodDef.callTypeId !== 0 || mode !== RpcRemoteExecutionMode.Default ? { callTypeId: methodDef.callTypeId, remoteExecutionMode: mode } : void 0;
4176
+ const callOptions = methodDef.callTypeId !== 0 || mode !== RpcRemoteExecutionMode.Default || methodDef.timeouts !== void 0 ? { callTypeId: methodDef.callTypeId, remoteExecutionMode: mode, timeouts: methodDef.timeouts } : void 0;
3790
4177
  return async (...args) => {
3791
4178
  if (mustCheckConnection && !peer.isConnected)
3792
4179
  throw new Error("Outbound call failed: not connected and AwaitForConnection is not set.");
@@ -3796,7 +4183,58 @@ var RpcHub = class {
3796
4183
  return resolveStreamRefs(result, peer);
3797
4184
  };
3798
4185
  }
4186
+ // Private methods
4187
+ _createClientProxy() {
4188
+ const methods = /* @__PURE__ */ new Map();
4189
+ const proxy = new Proxy({}, {
4190
+ get: (_target, prop) => typeof prop === "string" ? methods.get(prop) : void 0
4191
+ });
4192
+ return { proxy, methods, overloads: /* @__PURE__ */ new Map() };
4193
+ }
4194
+ _extendClientProxy(peer, entry, def) {
4195
+ for (const methodDef of def.methods.values()) {
4196
+ const existing = entry.overloads.get(methodDef.name)?.get(methodDef.argCount);
4197
+ if (existing !== void 0 && !isSameWireMethod(existing.def, methodDef))
4198
+ throw new Error(
4199
+ `RpcHub.addClient: conflicting definition of '${wireMethodName(methodDef)}' under service '${def.name}' \u2014 a different signature is already registered.`
4200
+ );
4201
+ }
4202
+ const changed = /* @__PURE__ */ new Set();
4203
+ for (const methodDef of def.methods.values()) {
4204
+ let byArgCount = entry.overloads.get(methodDef.name);
4205
+ if (byArgCount === void 0) {
4206
+ byArgCount = /* @__PURE__ */ new Map();
4207
+ entry.overloads.set(methodDef.name, byArgCount);
4208
+ }
4209
+ if (byArgCount.has(methodDef.argCount))
4210
+ continue;
4211
+ byArgCount.set(methodDef.argCount, {
4212
+ def: methodDef,
4213
+ fn: this._createClientMethod(peer, methodDef)
4214
+ });
4215
+ changed.add(methodDef.name);
4216
+ }
4217
+ for (const name of changed) {
4218
+ const byArgCount = entry.overloads.get(name);
4219
+ entry.methods.set(name, buildOverloadFn(name, byArgCount));
4220
+ }
4221
+ }
3799
4222
  };
4223
+ function buildOverloadFn(name, byArgCount) {
4224
+ if (byArgCount.size === 1) {
4225
+ const [[, only]] = byArgCount;
4226
+ return only.fn;
4227
+ }
4228
+ return (...args) => {
4229
+ const overload = byArgCount.get(args.length);
4230
+ if (overload === void 0)
4231
+ throw new Error(`No overload of ${name} accepts ${args.length} arguments`);
4232
+ return overload.fn(...args);
4233
+ };
4234
+ }
4235
+ function isSameWireMethod(a, b) {
4236
+ return a.wireArgCount === b.wireArgCount && a.callTypeId === b.callTypeId && a.stream === b.stream && a.noWait === b.noWait && a.remoteExecutionMode === b.remoteExecutionMode;
4237
+ }
3800
4238
 
3801
4239
  // src/rpc-peer-ref-builder.ts
3802
4240
  var RpcPeerRefBuilder = class {
@@ -3863,7 +4301,7 @@ function createMethod(peer, methodDef) {
3863
4301
  throw new Error("Outbound call failed: not connected and AwaitForConnection is not set.");
3864
4302
  const callArgs = args.slice(0, methodDef.argCount);
3865
4303
  const wireName = wireMethodName(methodDef);
3866
- const options = methodDef.callTypeId !== 0 || mode !== RpcRemoteExecutionMode.Default ? { callTypeId: methodDef.callTypeId, remoteExecutionMode: mode } : void 0;
4304
+ const options = methodDef.callTypeId !== 0 || mode !== RpcRemoteExecutionMode.Default || methodDef.timeouts !== void 0 ? { callTypeId: methodDef.callTypeId, remoteExecutionMode: mode, timeouts: methodDef.timeouts } : void 0;
3867
4305
  const outboundCall = peer.call(wireName, callArgs, options);
3868
4306
  return outboundCall.result.promise;
3869
4307
  };
@@ -4058,8 +4496,10 @@ export {
4058
4496
  ENVELOPE_DELIMITER,
4059
4497
  FRAME_DELIMITER,
4060
4498
  IncreasingSeqCompressor,
4499
+ REMOTE_EXCEPTION_TYPE_REF,
4061
4500
  RPC_CLOSE_CODE_UNSUPPORTED_FORMAT,
4062
4501
  RpcCallStage,
4502
+ RpcCallTimeouts,
4063
4503
  RpcCallTypeId,
4064
4504
  RpcClientPeer,
4065
4505
  RpcClientPeerReconnectDelayer,
@@ -4114,6 +4554,7 @@ export {
4114
4554
  isConnected,
4115
4555
  likelyConnected,
4116
4556
  parseStreamRef,
4557
+ readPolymorphismMarker,
4117
4558
  resolveStreamRefs,
4118
4559
  rpcMethod,
4119
4560
  rpcService,
@@ -4125,6 +4566,7 @@ export {
4125
4566
  splitBinaryFrame,
4126
4567
  splitCompactBinaryFrame,
4127
4568
  splitFrame,
4569
+ toExceptionInfo,
4128
4570
  wireMethodName,
4129
4571
  xxh3_64,
4130
4572
  xxh3_64str