@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.cjs CHANGED
@@ -34,8 +34,10 @@ __export(index_exports, {
34
34
  ENVELOPE_DELIMITER: () => ENVELOPE_DELIMITER,
35
35
  FRAME_DELIMITER: () => FRAME_DELIMITER,
36
36
  IncreasingSeqCompressor: () => IncreasingSeqCompressor,
37
+ REMOTE_EXCEPTION_TYPE_REF: () => REMOTE_EXCEPTION_TYPE_REF,
37
38
  RPC_CLOSE_CODE_UNSUPPORTED_FORMAT: () => RPC_CLOSE_CODE_UNSUPPORTED_FORMAT,
38
39
  RpcCallStage: () => RpcCallStage,
40
+ RpcCallTimeouts: () => RpcCallTimeouts,
39
41
  RpcCallTypeId: () => RpcCallTypeId,
40
42
  RpcClientPeer: () => RpcClientPeer,
41
43
  RpcClientPeerReconnectDelayer: () => RpcClientPeerReconnectDelayer,
@@ -90,6 +92,7 @@ __export(index_exports, {
90
92
  isConnected: () => isConnected,
91
93
  likelyConnected: () => likelyConnected,
92
94
  parseStreamRef: () => parseStreamRef,
95
+ readPolymorphismMarker: () => readPolymorphismMarker,
93
96
  resolveStreamRefs: () => resolveStreamRefs,
94
97
  rpcMethod: () => rpcMethod,
95
98
  rpcService: () => rpcService,
@@ -101,6 +104,7 @@ __export(index_exports, {
101
104
  splitBinaryFrame: () => splitBinaryFrame,
102
105
  splitCompactBinaryFrame: () => splitCompactBinaryFrame,
103
106
  splitFrame: () => splitFrame,
107
+ toExceptionInfo: () => toExceptionInfo,
104
108
  wireMethodName: () => wireMethodName,
105
109
  xxh3_64: () => xxh3_64,
106
110
  xxh3_64str: () => xxh3_64str
@@ -271,6 +275,24 @@ function readVarUint(data, offset) {
271
275
  } while (shift < 35);
272
276
  return { value: value >>> 0, bytesRead };
273
277
  }
278
+ function skipHeaders(data, pos, headerCount) {
279
+ for (let h = 0; h < headerCount; h++) {
280
+ const keyLen = data[pos++];
281
+ pos += keyLen;
282
+ const valLen = readVarUint(data, pos);
283
+ pos += valLen.bytesRead + valLen.value;
284
+ }
285
+ return pos;
286
+ }
287
+ function readPolymorphismMarker(data, offset) {
288
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
289
+ const length = view.getUint16(offset, true);
290
+ if (length === 0)
291
+ return { bytesRead: 2, isNull: false };
292
+ if (length === 1)
293
+ return { bytesRead: 2, isNull: true };
294
+ throw new Error("polymorphic payloads are not supported by the TS client");
295
+ }
274
296
  function concatUint8Arrays(arrays) {
275
297
  let totalLength = 0;
276
298
  for (const arr of arrays) totalLength += arr.length;
@@ -334,14 +356,6 @@ function deserializeBinaryMessage(data, offset, decoder = defaultBinaryDecoder)
334
356
  pos += methodLen.value;
335
357
  const argDataLen = view.getInt32(pos, true);
336
358
  pos += 4;
337
- if (headerCount > 0) {
338
- for (let h = 0; h < headerCount; h++) {
339
- const keyLen = data[pos++];
340
- pos += keyLen;
341
- const valLen = readVarUint(data, pos);
342
- pos += valLen.bytesRead + valLen.value;
343
- }
344
- }
345
359
  const args = [];
346
360
  const argEnd = pos + argDataLen;
347
361
  if (argDataLen > 0) {
@@ -349,8 +363,10 @@ function deserializeBinaryMessage(data, offset, decoder = defaultBinaryDecoder)
349
363
  for (const decoded of decoder.decodeMulti(argSlice)) {
350
364
  args.push(decoded);
351
365
  }
352
- pos = argEnd;
353
366
  }
367
+ pos = argEnd;
368
+ if (headerCount > 0)
369
+ pos = skipHeaders(data, pos, headerCount);
354
370
  const message = {
355
371
  Method: method,
356
372
  RelatedId: relId.value,
@@ -428,14 +444,6 @@ function deserializeCompactBinaryMessage(data, offset, registry, decoder = defau
428
444
  const method = registry?.getName(methodHash | 0) ?? `<hash:0x${methodHash.toString(16).padStart(8, "0")}>`;
429
445
  const argDataLen = view.getInt32(pos, true);
430
446
  pos += 4;
431
- if (headerCount > 0) {
432
- for (let h = 0; h < headerCount; h++) {
433
- const keyLen = data[pos++];
434
- pos += keyLen;
435
- const valLen = readVarUint(data, pos);
436
- pos += valLen.bytesRead + valLen.value;
437
- }
438
- }
439
447
  const args = [];
440
448
  const argEnd = pos + argDataLen;
441
449
  if (argDataLen > 0) {
@@ -443,8 +451,10 @@ function deserializeCompactBinaryMessage(data, offset, registry, decoder = defau
443
451
  for (const decoded of decoder.decodeMulti(argSlice)) {
444
452
  args.push(decoded);
445
453
  }
446
- pos = argEnd;
447
454
  }
455
+ pos = argEnd;
456
+ if (headerCount > 0)
457
+ pos = skipHeaders(data, pos, headerCount);
448
458
  const message = {
449
459
  Method: method,
450
460
  RelatedId: relId.value,
@@ -714,7 +724,8 @@ function defineRpcService(name, methods) {
714
724
  callTypeId: input.callTypeId ?? 0,
715
725
  stream: input.returns === RpcType.stream,
716
726
  noWait: isNoWait,
717
- remoteExecutionMode: isNoWait ? 0 : input.remoteExecutionMode ?? RpcRemoteExecutionMode.Default
727
+ remoteExecutionMode: isNoWait ? 0 : input.remoteExecutionMode ?? RpcRemoteExecutionMode.Default,
728
+ timeouts: input.timeouts
718
729
  });
719
730
  }
720
731
  return { name, methods: map };
@@ -748,6 +759,14 @@ var RpcOutboundCall = class {
748
759
  removeOnOk = true;
749
760
  /** The serialized wire data — stored for re-sending on reconnect. */
750
761
  serializedWireData = "";
762
+ /** Per-call connect/run timeouts enforced by the peer's maintenance loop
763
+ * (R12). `undefined` means unbounded (the query default). */
764
+ timeouts;
765
+ /** `Date.now()` when this call was created (used for the connect timeout). */
766
+ startedAt = Date.now();
767
+ /** `Date.now()` when this call was last put on the wire, or 0 if it hasn't
768
+ * been sent yet (used for the run timeout). */
769
+ sentAt = 0;
751
770
  /**
752
771
  * Bitfield of {@link RpcCallStage} flags indicating how far this call
753
772
  * has progressed. Used by the `$sys.Reconnect` protocol to tell the
@@ -837,20 +856,77 @@ var RpcInboundCall = class {
837
856
  callId;
838
857
  method;
839
858
  args;
859
+ /** 0 = still processing, 1 = result computed and sent at least once. */
860
+ completedStage = 0;
861
+ _resend;
862
+ _abortController = new AbortController();
840
863
  constructor(callId, method, args) {
841
864
  this.callId = callId;
842
865
  this.method = method;
843
866
  this.args = args;
844
867
  }
868
+ /** Aborted when the remote peer cancels this call via `$sys.Cancel` (R17).
869
+ * Threaded into the service handler so it can abort in-flight work. */
870
+ get signal() {
871
+ return this._abortController.signal;
872
+ }
873
+ /** True once the remote peer cancelled this call — the response is then
874
+ * suppressed even though the call may still be registered (R9 dedup). */
875
+ get isCancelled() {
876
+ return this._abortController.signal.aborted;
877
+ }
878
+ cancel() {
879
+ this._abortController.abort();
880
+ }
881
+ // Records the closure that re-sends this call's already-computed result;
882
+ // marks the call as completed. Mirrors .NET's post-`ProcessStage1Plus` state.
883
+ setResult(resend) {
884
+ this._resend = resend;
885
+ this.completedStage = 1;
886
+ }
887
+ // Re-sends the result to the remote peer if it was already computed.
888
+ // A resent frame for an in-flight call is a no-op — the original dispatch
889
+ // will send the result when it completes. Mirrors .NET
890
+ // `RpcInboundCall.TryReprocess`.
891
+ resendResult() {
892
+ if (this.completedStage >= 1)
893
+ this._resend?.();
894
+ }
845
895
  };
846
896
  var RpcInboundCallTracker = class {
847
897
  _calls = /* @__PURE__ */ new Map();
898
+ _completedIds = [];
899
+ /** Cap on retained completed calls — see `RpcLimits.completedInboundCallsLimit`. */
900
+ completedCallsLimit = 1e3;
848
901
  get size() {
849
902
  return this._calls.size;
850
903
  }
851
904
  register(call) {
852
905
  this._calls.set(call.callId, call);
853
906
  }
907
+ // Registers `call` unless its id is already tracked, in which case the
908
+ // existing call is returned. Mirrors .NET `RpcInboundCallTracker.GetOrRegister`.
909
+ getOrRegister(call) {
910
+ const existing = this._calls.get(call.callId);
911
+ if (existing !== void 0)
912
+ return existing;
913
+ this._calls.set(call.callId, call);
914
+ return call;
915
+ }
916
+ // Keeps the completed `call` registered for duplicate-frame dedup, evicting
917
+ // the oldest completed calls beyond `completedCallsLimit`. Deviation from
918
+ // .NET (which unregisters a call once its result is sent): TS peers
919
+ // blind-resend after reconnect, so a completed call must stay resolvable
920
+ // to avoid re-executing the handler — but only within a bounded window.
921
+ markCompleted(call) {
922
+ if (this._calls.get(call.callId) !== call)
923
+ return;
924
+ this._completedIds.push(call.callId);
925
+ while (this._completedIds.length > this.completedCallsLimit) {
926
+ const id = this._completedIds.shift();
927
+ this._calls.delete(id);
928
+ }
929
+ }
854
930
  get(callId) {
855
931
  return this._calls.get(callId);
856
932
  }
@@ -861,6 +937,23 @@ var RpcInboundCallTracker = class {
861
937
  }
862
938
  clear() {
863
939
  this._calls.clear();
940
+ this._completedIds.length = 0;
941
+ }
942
+ };
943
+
944
+ // src/rpc-call-timeouts.ts
945
+ var RpcCallTimeouts = class _RpcCallTimeouts {
946
+ /** Both timeouts unbounded — the query default (.NET `RpcCallTimeouts.None`). */
947
+ static None = new _RpcCallTimeouts(Infinity, Infinity);
948
+ /** Command default: 1.5 s connect / 10 s run (.NET `RpcCallTimeouts.Default.Command`). */
949
+ static Command = new _RpcCallTimeouts(1500, 1e4);
950
+ /** Max time a call may wait for the connection before it is sent. */
951
+ connectTimeoutMs;
952
+ /** Max time a sent call may stay unanswered before it is failed. */
953
+ runTimeoutMs;
954
+ constructor(connectTimeoutMs = Infinity, runTimeoutMs = Infinity) {
955
+ this.connectTimeoutMs = connectTimeoutMs;
956
+ this.runTimeoutMs = runTimeoutMs;
864
957
  }
865
958
  };
866
959
 
@@ -874,6 +967,12 @@ var RpcError = class extends Error {
874
967
  this.typeName = typeName;
875
968
  }
876
969
  };
970
+ var REMOTE_EXCEPTION_TYPE_REF = "ActualLab.Serialization.RemoteException, ActualLab.Core";
971
+ function toExceptionInfo(error) {
972
+ const name = error instanceof Error ? error.name : "Error";
973
+ const message = error instanceof Error ? error.message : String(error);
974
+ return { TypeRef: REMOTE_EXCEPTION_TYPE_REF, Message: `${name}: ${message}` };
975
+ }
877
976
 
878
977
  // src/increasing-seq-compressor.ts
879
978
  var IncreasingSeqCompressor = {
@@ -1078,13 +1177,38 @@ var RpcStreamSender = class _RpcStreamSender {
1078
1177
  */
1079
1178
  onAck(nextIndex, hostId) {
1080
1179
  const mustReset = hostId !== "" && hostId !== _RpcStreamSender._emptyGuid;
1081
- if (!this._started.isCompleted) this._started.resolve();
1180
+ if (mustReset && hostId !== this.id.hostId) {
1181
+ this._sendDisconnect();
1182
+ return;
1183
+ }
1184
+ if (!this._started.isCompleted) {
1185
+ if (mustReset && nextIndex === 0)
1186
+ this._started.resolve();
1187
+ else {
1188
+ this._sendDisconnect();
1189
+ return;
1190
+ }
1191
+ } else if (this._ended) {
1192
+ this._sendDisconnect();
1193
+ return;
1194
+ } else if (mustReset && !this.allowReconnect) {
1195
+ this._sendDisconnect();
1196
+ return;
1197
+ }
1082
1198
  this._acks.push({ nextIndex, mustReset });
1083
1199
  if (this._whenAckReady) {
1084
1200
  this._whenAckReady.resolve();
1085
1201
  this._whenAckReady = null;
1086
1202
  }
1087
1203
  }
1204
+ // Tells the consumer this shared stream is gone, then disposes it —
1205
+ // mirrors RpcSharedStream.SendDisconnect (RpcSharedStream.cs:284-289).
1206
+ _sendDisconnect() {
1207
+ const conn = this.peer.connection;
1208
+ if (conn)
1209
+ this.peer.hub.systemCallSender.disconnect(conn, this.peer.serializationFormat, [this.id.localId]);
1210
+ this.disconnect();
1211
+ }
1088
1212
  /** Called by system call handler when $sys.AckEnd is received from the client. */
1089
1213
  onAckEnd(_hostId) {
1090
1214
  this.disconnect();
@@ -1133,7 +1257,7 @@ var RpcStreamSender = class _RpcStreamSender {
1133
1257
  this._ended = true;
1134
1258
  const conn = this.peer.connection;
1135
1259
  if (!conn) return;
1136
- const errorInfo = error ? { TypeRef: "System.Exception", Message: error.message } : { TypeRef: "", Message: "" };
1260
+ const errorInfo = error ? toExceptionInfo(error) : { TypeRef: "", Message: "" };
1137
1261
  this.peer.hub.systemCallSender.end(
1138
1262
  conn,
1139
1263
  this.peer.serializationFormat,
@@ -1575,11 +1699,7 @@ var RpcStream = class _RpcStream {
1575
1699
  if (index > this._nextExpectedIndex) {
1576
1700
  warnLog4?.log(`item index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
1577
1701
  if (!this.allowReconnect) {
1578
- this._completed = true;
1579
- this._completionError = new Error(
1580
- `Stream gap at index ${index} (expected ${this._nextExpectedIndex}); reconnect not allowed`
1581
- );
1582
- this._notifyConsumer();
1702
+ this._complete(this._gapError(index));
1583
1703
  return;
1584
1704
  }
1585
1705
  this._sendAck(this._nextExpectedIndex, true);
@@ -1599,11 +1719,7 @@ var RpcStream = class _RpcStream {
1599
1719
  if (index > this._nextExpectedIndex) {
1600
1720
  warnLog4?.log(`batch index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
1601
1721
  if (!this.allowReconnect) {
1602
- this._completed = true;
1603
- this._completionError = new Error(
1604
- `Stream gap at index ${index} (expected ${this._nextExpectedIndex}); reconnect not allowed`
1605
- );
1606
- this._notifyConsumer();
1722
+ this._complete(this._gapError(index));
1607
1723
  return;
1608
1724
  }
1609
1725
  this._sendAck(this._nextExpectedIndex, true);
@@ -1620,9 +1736,20 @@ var RpcStream = class _RpcStream {
1620
1736
  /** Called by system call handler when the stream ends ($sys.End). */
1621
1737
  onEnd(index, error) {
1622
1738
  if (this._disposed || this._completed) return;
1623
- this._completed = true;
1624
- this._completionError = error;
1625
- this._notifyConsumer();
1739
+ if (index > this._nextExpectedIndex) {
1740
+ warnLog4?.log(`end index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
1741
+ if (!this.allowReconnect) {
1742
+ this._complete(this._gapError(index));
1743
+ return;
1744
+ }
1745
+ this._sendAck(this._nextExpectedIndex, true);
1746
+ return;
1747
+ }
1748
+ if (index < this._nextExpectedIndex) {
1749
+ this._maybeSendAck(Math.min(index, this._nextConsumedIndex));
1750
+ return;
1751
+ }
1752
+ this._complete(error);
1626
1753
  }
1627
1754
  // -- IRpcObject --
1628
1755
  reconnect() {
@@ -1656,6 +1783,7 @@ var RpcStream = class _RpcStream {
1656
1783
  next: async () => {
1657
1784
  if (!this._started) {
1658
1785
  this._started = true;
1786
+ this.peer.remoteObjects.register(this);
1659
1787
  this._sendAck(0, true);
1660
1788
  }
1661
1789
  this._maybeSendAck(this._nextConsumedIndex);
@@ -1666,6 +1794,7 @@ var RpcStream = class _RpcStream {
1666
1794
  return { value, done: false };
1667
1795
  }
1668
1796
  if (this._completed) {
1797
+ this.dispose();
1669
1798
  if (this._completionError) throw this._completionError;
1670
1799
  return { value: void 0, done: true };
1671
1800
  }
@@ -1674,7 +1803,8 @@ var RpcStream = class _RpcStream {
1674
1803
  }
1675
1804
  },
1676
1805
  return: () => {
1677
- this._sendAckEnd();
1806
+ if (this._started && !this._completed)
1807
+ this._sendAckEnd();
1678
1808
  this.dispose();
1679
1809
  return Promise.resolve({ value: void 0, done: true });
1680
1810
  }
@@ -1690,6 +1820,22 @@ var RpcStream = class _RpcStream {
1690
1820
  this._notifyConsumer();
1691
1821
  }
1692
1822
  // -- Private --
1823
+ // Mirrors .NET RpcStream.CloseFromLock: acknowledge the end to the sender
1824
+ // (so its RpcSharedStream is released) and unregister immediately.
1825
+ _complete(error) {
1826
+ if (this._completed) return;
1827
+ this._completed = true;
1828
+ this._completionError = error;
1829
+ this._sendAckEnd();
1830
+ if (this.kind === RpcObjectKind.Remote)
1831
+ this.peer.remoteObjects.unregister(this);
1832
+ this._notifyConsumer();
1833
+ }
1834
+ _gapError(index) {
1835
+ return new Error(
1836
+ `Stream gap at index ${index} (expected ${this._nextExpectedIndex}); reconnect not allowed`
1837
+ );
1838
+ }
1693
1839
  _notifyConsumer() {
1694
1840
  if (this._consumerWaiting !== null) {
1695
1841
  this._consumerWaiting.resolve();
@@ -1706,7 +1852,7 @@ var RpcStream = class _RpcStream {
1706
1852
  static _emptyGuid = "00000000-0000-0000-0000-000000000000";
1707
1853
  _sendAck(nextIndex, mustReset) {
1708
1854
  this._ackSentUpTo = nextIndex;
1709
- const conn = this.peer.connection;
1855
+ const conn = this.peer.isConnected ? this.peer.connection : void 0;
1710
1856
  if (conn) {
1711
1857
  const hostId = mustReset ? this.id.hostId : _RpcStream._emptyGuid;
1712
1858
  this.peer.hub.systemCallSender.ack(
@@ -1719,7 +1865,7 @@ var RpcStream = class _RpcStream {
1719
1865
  }
1720
1866
  }
1721
1867
  _sendAckEnd() {
1722
- const conn = this.peer.connection;
1868
+ const conn = this.peer.isConnected ? this.peer.connection : void 0;
1723
1869
  if (conn) {
1724
1870
  this.peer.hub.systemCallSender.ackEnd(
1725
1871
  conn,
@@ -1739,9 +1885,7 @@ function resolveStreamRefs(value, peer) {
1739
1885
  if (typeof value === "string") {
1740
1886
  const ref = parseStreamRef(value);
1741
1887
  if (ref !== null) {
1742
- const stream = new RpcStream(ref, peer);
1743
- peer.remoteObjects.register(stream);
1744
- return stream;
1888
+ return new RpcStream(ref, peer);
1745
1889
  }
1746
1890
  return value;
1747
1891
  }
@@ -1794,6 +1938,11 @@ var RpcSystemCallHandler = class {
1794
1938
  case RpcSystemCalls.ok: {
1795
1939
  const call = peer.outboundCalls.get(relatedId);
1796
1940
  if (call !== void 0) {
1941
+ if (args.length > 1) {
1942
+ peer.outboundCalls.remove(relatedId);
1943
+ call.result.reject(polymorphicPayloadError(method, args.length, 1));
1944
+ break;
1945
+ }
1797
1946
  call.completedStage |= RpcCallStage.ResultReady;
1798
1947
  if (call.removeOnOk) {
1799
1948
  peer.outboundCalls.remove(relatedId);
@@ -1807,19 +1956,15 @@ var RpcSystemCallHandler = class {
1807
1956
  break;
1808
1957
  }
1809
1958
  case RpcSystemCalls.error: {
1810
- const call = peer.outboundCalls.remove(relatedId);
1959
+ const call = peer.outboundCalls.get(relatedId);
1811
1960
  if (call !== void 0) {
1961
+ call.completedStage |= RpcCallStage.ResultReady;
1962
+ if (call.removeOnOk) {
1963
+ peer.outboundCalls.remove(relatedId);
1964
+ }
1812
1965
  const errorInfo = args[0];
1813
1966
  const msg = errorInfo?.Message ?? errorInfo?.message ?? "RPC error";
1814
- const errorType = errorInfo?.TypeRef ?? errorInfo?.typeRef;
1815
- let typeName;
1816
- if (typeof errorType === "string") {
1817
- const commaIdx = errorType.indexOf(",");
1818
- typeName = commaIdx >= 0 ? errorType.slice(0, commaIdx) : errorType;
1819
- } else if (errorType !== null && typeof errorType === "object") {
1820
- const t = errorType;
1821
- typeName = t.TypeName ?? t.typeName;
1822
- }
1967
+ const typeName = extractTypeName(errorInfo?.TypeRef ?? errorInfo?.typeRef);
1823
1968
  if (typeName === "ActualLab.Rpc.RpcRerouteException")
1824
1969
  warnLog5?.log("Got RpcRerouteException from remote peer:", msg);
1825
1970
  call.result.reject(new RpcError(msg, typeName));
@@ -1827,17 +1972,29 @@ var RpcSystemCallHandler = class {
1827
1972
  break;
1828
1973
  }
1829
1974
  case RpcSystemCalls.cancel: {
1830
- peer.inboundCalls.remove(relatedId);
1975
+ const call = peer.inboundCalls.remove(relatedId);
1976
+ call?.cancel();
1831
1977
  break;
1832
1978
  }
1833
1979
  case RpcSystemCalls.keepAlive: {
1834
1980
  peer.notifyKeepAliveReceived();
1981
+ const keepAliveIds = args[0];
1982
+ if (Array.isArray(keepAliveIds) && keepAliveIds.length > 0 && peer.connection !== void 0) {
1983
+ const unknownIds = [];
1984
+ for (const id of keepAliveIds)
1985
+ if (peer.sharedObjects.get(id) === void 0)
1986
+ unknownIds.push(id);
1987
+ if (unknownIds.length > 0)
1988
+ peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, unknownIds);
1989
+ }
1835
1990
  break;
1836
1991
  }
1837
1992
  case RpcSystemCalls.item: {
1838
1993
  const stream = peer.remoteObjects.get(relatedId);
1839
1994
  if (!stream)
1840
1995
  debugLog?.log(`$sys.I: no stream for relatedId=${relatedId}`);
1996
+ else if (args.length > 2)
1997
+ stream.onEnd(args[0], polymorphicPayloadError(method, args.length, 2));
1841
1998
  else
1842
1999
  stream.onItem(
1843
2000
  args[0],
@@ -1849,6 +2006,8 @@ var RpcSystemCallHandler = class {
1849
2006
  const stream = peer.remoteObjects.get(relatedId);
1850
2007
  if (!stream)
1851
2008
  debugLog?.log(`$sys.B: no stream for relatedId=${relatedId}`);
2009
+ else if (args.length > 2)
2010
+ stream.onEnd(args[0], polymorphicPayloadError(method, args.length, 2));
1852
2011
  else if (Array.isArray(args[1])) {
1853
2012
  const items = args[1];
1854
2013
  for (let i = 0; i < items.length; i++)
@@ -1863,20 +2022,32 @@ var RpcSystemCallHandler = class {
1863
2022
  debugLog?.log(`$sys.End: no stream for relatedId=${relatedId}`);
1864
2023
  } else {
1865
2024
  const errorInfo = args[1];
2025
+ const typeName = extractTypeName(errorInfo?.TypeRef ?? errorInfo?.typeRef);
1866
2026
  const msg = errorInfo?.Message ?? errorInfo?.message;
1867
- const error = msg ? new Error(msg) : null;
2027
+ let error = null;
2028
+ if (typeName || msg) {
2029
+ error = new Error(msg ?? typeName ?? "RPC stream error");
2030
+ if (typeName)
2031
+ error.name = typeName;
2032
+ }
1868
2033
  stream.onEnd(args[0], error);
1869
2034
  }
1870
2035
  break;
1871
2036
  }
1872
2037
  case RpcSystemCalls.ack: {
1873
2038
  const sender = peer.sharedObjects.get(relatedId);
1874
- sender?.onAck(args[0], args[1]);
2039
+ if (sender)
2040
+ sender.onAck(args[0], args[1]);
2041
+ else if (peer.connection !== void 0)
2042
+ peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, [relatedId]);
1875
2043
  break;
1876
2044
  }
1877
2045
  case RpcSystemCalls.ackEnd: {
1878
2046
  const sender = peer.sharedObjects.get(relatedId);
1879
- sender?.onAckEnd(args[0]);
2047
+ if (sender)
2048
+ sender.onAckEnd(args[0]);
2049
+ else if (peer.connection !== void 0)
2050
+ peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, [relatedId]);
1880
2051
  break;
1881
2052
  }
1882
2053
  case RpcSystemCalls.disconnect: {
@@ -1910,19 +2081,34 @@ var RpcSystemCallHandler = class {
1910
2081
  */
1911
2082
  _handleReconnect(relatedId, args, peer) {
1912
2083
  if (peer.connection === void 0) return;
2084
+ const handshakeIndex = args[0];
2085
+ if (typeof handshakeIndex === "number" && handshakeIndex !== peer.ownHandshakeIndex) {
2086
+ peer.hub.systemCallSender.error(
2087
+ peer.connection,
2088
+ peer.serializationFormat,
2089
+ relatedId,
2090
+ new Error(
2091
+ `TooLateToReconnect: own handshake index ${peer.ownHandshakeIndex} != ${handshakeIndex}`
2092
+ )
2093
+ );
2094
+ return;
2095
+ }
1913
2096
  let unknownIds = [];
1914
2097
  try {
1915
2098
  const stagesRaw = args[1];
1916
2099
  if (stagesRaw && typeof stagesRaw === "object" && !Array.isArray(stagesRaw)) {
1917
2100
  const unknownSet = /* @__PURE__ */ new Set();
1918
- for (const [, rawValue] of Object.entries(stagesRaw)) {
2101
+ for (const [stageKey, rawValue] of Object.entries(stagesRaw)) {
1919
2102
  const bytes = _toBytes(rawValue);
1920
2103
  if (bytes === null) continue;
2104
+ const completedStage = parseInt(stageKey, 10) || 0;
1921
2105
  const callIds = IncreasingSeqCompressor.deserialize(bytes);
1922
2106
  for (const callId of callIds) {
1923
- if (peer.inboundCalls.get(callId) === void 0) {
2107
+ const call = peer.inboundCalls.get(callId);
2108
+ if (call === void 0)
1924
2109
  unknownSet.add(callId);
1925
- }
2110
+ else if (completedStage < 1)
2111
+ call.resendResult();
1926
2112
  }
1927
2113
  }
1928
2114
  unknownIds = [...unknownSet].sort((a, b) => a - b);
@@ -1934,6 +2120,29 @@ var RpcSystemCallHandler = class {
1934
2120
  peer.hub.systemCallSender.ok(peer.connection, peer.serializationFormat, relatedId, responseValue);
1935
2121
  }
1936
2122
  };
2123
+ function polymorphicPayloadError(method, got, expected) {
2124
+ return new Error(
2125
+ `${method}: expected ${expected} value(s), got ${got} \u2014 polymorphic payloads are not supported by the TS client`
2126
+ );
2127
+ }
2128
+ function extractTypeName(typeRef) {
2129
+ if (typeof typeRef === "string") {
2130
+ if (typeRef.length === 0)
2131
+ return void 0;
2132
+ const commaIdx = typeRef.indexOf(",");
2133
+ return commaIdx >= 0 ? typeRef.slice(0, commaIdx) : typeRef;
2134
+ }
2135
+ if (typeRef !== null && typeof typeRef === "object") {
2136
+ const t = typeRef;
2137
+ const aqn = t.AssemblyQualifiedName ?? t.assemblyQualifiedName;
2138
+ if (typeof aqn === "string" && aqn.length > 0) {
2139
+ const commaIdx = aqn.indexOf(",");
2140
+ return commaIdx >= 0 ? aqn.slice(0, commaIdx) : aqn;
2141
+ }
2142
+ return t.TypeName ?? t.typeName;
2143
+ }
2144
+ return void 0;
2145
+ }
1937
2146
  function _toBytes(value) {
1938
2147
  if (value instanceof Uint8Array) return value;
1939
2148
  if (typeof value === "string") {
@@ -2017,12 +2226,11 @@ var RpcSystemCallSender = class _RpcSystemCallSender {
2017
2226
  }
2018
2227
  }
2019
2228
  error(conn, format, relatedId, error) {
2020
- const message = error instanceof Error ? error.message : String(error);
2021
2229
  this._send(
2022
2230
  conn,
2023
2231
  format,
2024
2232
  { Method: RpcSystemCalls.error, RelatedId: relatedId },
2025
- [{ Message: message }]
2233
+ [toExceptionInfo(error)]
2026
2234
  );
2027
2235
  }
2028
2236
  cancel(conn, format, relatedId) {
@@ -2036,6 +2244,14 @@ var RpcSystemCallSender = class _RpcSystemCallSender {
2036
2244
  activeCallIds
2037
2245
  ]);
2038
2246
  }
2247
+ // Tells the remote peer the listed shared-object ids are gone on our side,
2248
+ // so its matching remote objects (e.g. RpcStream consumers) fail fast
2249
+ // instead of hanging. Mirrors .NET RpcSystemCallSender.Disconnect.
2250
+ disconnect(conn, format, localIds) {
2251
+ this._send(conn, format, { Method: RpcSystemCalls.disconnect }, [
2252
+ localIds
2253
+ ]);
2254
+ }
2039
2255
  item(conn, format, localId, index, item) {
2040
2256
  this._itemEnv.RelatedId = localId;
2041
2257
  this._itemArgs[0] = index;
@@ -2073,7 +2289,13 @@ var RpcSystemCallSender = class _RpcSystemCallSender {
2073
2289
  var RpcRemoteObjectTracker = class {
2074
2290
  _objects = /* @__PURE__ */ new Map();
2075
2291
  register(obj) {
2076
- this._objects.set(obj.id.localId, obj);
2292
+ const localId = obj.id.localId;
2293
+ const existing = this._objects.get(localId);
2294
+ if (existing === obj)
2295
+ return;
2296
+ if (existing !== void 0)
2297
+ existing.disconnect();
2298
+ this._objects.set(localId, obj);
2077
2299
  }
2078
2300
  get(localId) {
2079
2301
  return this._objects.get(localId);
@@ -2082,7 +2304,9 @@ var RpcRemoteObjectTracker = class {
2082
2304
  return this._objects.keys();
2083
2305
  }
2084
2306
  unregister(obj) {
2085
- this._objects.delete(obj.id.localId);
2307
+ const localId = obj.id.localId;
2308
+ if (this._objects.get(localId) === obj)
2309
+ this._objects.delete(localId);
2086
2310
  }
2087
2311
  disconnectAll() {
2088
2312
  for (const obj of this._objects.values()) {
@@ -2106,7 +2330,10 @@ var RpcSharedObjectTracker = class {
2106
2330
  return this._nextId++;
2107
2331
  }
2108
2332
  register(obj) {
2109
- this._objects.set(obj.id.localId, obj);
2333
+ const localId = obj.id.localId;
2334
+ if (this._objects.has(localId))
2335
+ throw new Error(`RPC shared object with localId ${localId} is already registered.`);
2336
+ this._objects.set(localId, obj);
2110
2337
  }
2111
2338
  get(localId) {
2112
2339
  return this._objects.get(localId);
@@ -2115,7 +2342,9 @@ var RpcSharedObjectTracker = class {
2115
2342
  return this._objects.keys();
2116
2343
  }
2117
2344
  unregister(obj) {
2118
- this._objects.delete(obj.id.localId);
2345
+ const localId = obj.id.localId;
2346
+ if (this._objects.get(localId) === obj)
2347
+ this._objects.delete(localId);
2119
2348
  }
2120
2349
  disconnectAll() {
2121
2350
  for (const obj of this._objects.values()) {
@@ -2145,9 +2374,7 @@ var RpcSerializationFormat = class {
2145
2374
  static NewtonsoftJsonV5;
2146
2375
  static NewtonsoftJsonV5NP;
2147
2376
  static MessagePackV6;
2148
- static MemoryPackV6;
2149
2377
  static MessagePackV6C;
2150
- static MemoryPackV6C;
2151
2378
  static All;
2152
2379
  /** Convenience shortcut for RpcSerializationFormatResolver.Default.get(key). */
2153
2380
  static get(key) {
@@ -2243,18 +2470,14 @@ RpcSerializationFormat.SystemJsonV5NP = new RpcJsonSerializationFormat("json5np"
2243
2470
  RpcSerializationFormat.NewtonsoftJsonV5 = new RpcJsonSerializationFormat("njson5");
2244
2471
  RpcSerializationFormat.NewtonsoftJsonV5NP = new RpcJsonSerializationFormat("njson5np");
2245
2472
  RpcSerializationFormat.MessagePackV6 = new RpcMessagePackSerializationFormat("msgpack6");
2246
- RpcSerializationFormat.MemoryPackV6 = new RpcMessagePackSerializationFormat("mempack6");
2247
2473
  RpcSerializationFormat.MessagePackV6C = new RpcMessagePackCompactSerializationFormat("msgpack6c");
2248
- RpcSerializationFormat.MemoryPackV6C = new RpcMessagePackCompactSerializationFormat("mempack6c");
2249
2474
  RpcSerializationFormat.All = [
2250
2475
  RpcSerializationFormat.SystemJsonV5,
2251
2476
  RpcSerializationFormat.SystemJsonV5NP,
2252
2477
  RpcSerializationFormat.NewtonsoftJsonV5,
2253
2478
  RpcSerializationFormat.NewtonsoftJsonV5NP,
2254
2479
  RpcSerializationFormat.MessagePackV6,
2255
- RpcSerializationFormat.MemoryPackV6,
2256
- RpcSerializationFormat.MessagePackV6C,
2257
- RpcSerializationFormat.MemoryPackV6C
2480
+ RpcSerializationFormat.MessagePackV6C
2258
2481
  ];
2259
2482
  var RpcSerializationFormatResolver = class _RpcSerializationFormatResolver {
2260
2483
  static DefaultFormats = RpcSerializationFormat.All;
@@ -2274,6 +2497,8 @@ var RpcSerializationFormatResolver = class _RpcSerializationFormatResolver {
2274
2497
  this.formats = map;
2275
2498
  }
2276
2499
  get(key) {
2500
+ if (isMemoryPackKey(key))
2501
+ throw new Error(`Serialization format '${key}': MemoryPack formats are not supported by the TS client.`);
2277
2502
  const format = this.tryGet(key);
2278
2503
  if (!format)
2279
2504
  throw new Error(`No format with key '${key}'.`);
@@ -2281,9 +2506,14 @@ var RpcSerializationFormatResolver = class _RpcSerializationFormatResolver {
2281
2506
  }
2282
2507
  tryGet(key) {
2283
2508
  if (!key) key = this.defaultFormatKey;
2509
+ if (isMemoryPackKey(key))
2510
+ throw new Error(`Serialization format '${key}': MemoryPack formats are not supported by the TS client.`);
2284
2511
  return this.formats.get(key);
2285
2512
  }
2286
2513
  };
2514
+ function isMemoryPackKey(key) {
2515
+ return key.startsWith("mempack");
2516
+ }
2287
2517
 
2288
2518
  // src/rpc-peer.ts
2289
2519
  function yieldToEventLoop() {
@@ -2337,6 +2567,10 @@ var RpcPeer = class {
2337
2567
  * attempt (the reconnect delay is part of this phase); `Connected` is
2338
2568
  * entered only after the handshake has succeeded. */
2339
2569
  connectionStateChanged = new import_core7.EventHandlerSet();
2570
+ /** Index of the handshake THIS peer most recently sent. Used to reject a
2571
+ * stale `$sys.Reconnect` that references an older connection generation
2572
+ * (C# `RpcSystemCalls.Reconnect`'s `ownHandshake.Index` check). */
2573
+ _ownHandshakeIndex = 0;
2340
2574
  _connection;
2341
2575
  /** Mirror of `_connectionState === Connected`. True only after the
2342
2576
  * handshake round-trip completes; outbound `call()` checks this to
@@ -2348,6 +2582,7 @@ var RpcPeer = class {
2348
2582
  _connectionState = 0 /* Disconnected */;
2349
2583
  _keepAliveTimer;
2350
2584
  _keepAliveWatchdog;
2585
+ _maintainTimer;
2351
2586
  /** Wall-clock time of the last inbound `$sys.KeepAlive`, or 0 if none yet
2352
2587
  * for the current connection. Useful for diagnostics. */
2353
2588
  _lastKeepAliveAt = 0;
@@ -2362,6 +2597,37 @@ var RpcPeer = class {
2362
2597
  this.ref = ref;
2363
2598
  this.keepAlivePeriodMs = hub.limits.keepAlivePeriodMs;
2364
2599
  this.keepAliveTimeoutMs = hub.limits.keepAliveTimeoutMs;
2600
+ this.inboundCalls.completedCallsLimit = hub.limits.completedInboundCallsLimit;
2601
+ const checkPeriod = hub.limits.callTimeoutCheckPeriodMs;
2602
+ if (checkPeriod > 0 && Number.isFinite(checkPeriod)) {
2603
+ this._maintainTimer = setInterval(() => this._maintainOutboundCalls(), checkPeriod);
2604
+ this._maintainTimer.unref?.();
2605
+ }
2606
+ }
2607
+ /** Fail outbound calls whose per-call {@link RpcCallTimeouts} elapsed —
2608
+ * the connect timeout while a call still awaits the wire, the run timeout
2609
+ * once it has been sent. Mirrors .NET `RpcOutboundCallTracker.Maintain`
2610
+ * (RpcCallTrackers.cs:108-207). Calls without timeouts (queries) never
2611
+ * time out here. */
2612
+ _maintainOutboundCalls() {
2613
+ const now = Date.now();
2614
+ const canRunTimeout = this._isConnected && (this._lastKeepAliveAt === 0 || now - this._lastKeepAliveAt <= this.keepAliveTimeoutMs);
2615
+ for (const call of [...this.outboundCalls.values()]) {
2616
+ const timeouts = call.timeouts;
2617
+ if (timeouts === void 0 || call.result.isCompleted)
2618
+ continue;
2619
+ let timedOut;
2620
+ if (call.sentAt === 0) {
2621
+ if (now - call.startedAt >= timeouts.connectTimeoutMs)
2622
+ timedOut = `connect timeout (${timeouts.connectTimeoutMs}ms)`;
2623
+ } else if (canRunTimeout && now - call.sentAt >= timeouts.runTimeoutMs)
2624
+ timedOut = `run timeout (${timeouts.runTimeoutMs}ms)`;
2625
+ if (timedOut === void 0)
2626
+ continue;
2627
+ this.outboundCalls.remove(call.callId);
2628
+ call.result.reject(new import_core7.TimeoutError(`'${this.ref}': call ${call.method}#${call.callId} timed out \u2014 ${timedOut}`));
2629
+ call.onDisconnect();
2630
+ }
2365
2631
  }
2366
2632
  get connection() {
2367
2633
  return this._connection;
@@ -2372,6 +2638,9 @@ var RpcPeer = class {
2372
2638
  get isConnected() {
2373
2639
  return this._isConnected;
2374
2640
  }
2641
+ get ownHandshakeIndex() {
2642
+ return this._ownHandshakeIndex;
2643
+ }
2375
2644
  _setConnectionState(value) {
2376
2645
  if (this._connectionState === value) return;
2377
2646
  const previousState = this._connectionState;
@@ -2443,14 +2712,17 @@ var RpcPeer = class {
2443
2712
  this._connection?.encoder,
2444
2713
  this.hub.registry
2445
2714
  );
2715
+ outboundCall.timeouts = options?.timeouts;
2446
2716
  this.outboundCalls.register(outboundCall);
2447
2717
  if (this._isConnected) {
2718
+ outboundCall.sentAt = Date.now();
2448
2719
  this._sendWireData(outboundCall.serializedWireData);
2449
2720
  } else {
2450
2721
  const stateHandler = (state) => {
2451
2722
  if (state !== 3 /* Connected */) return;
2452
2723
  this.connectionStateChanged.remove(stateHandler);
2453
2724
  if (outboundCall.result.isCompleted) return;
2725
+ outboundCall.sentAt = Date.now();
2454
2726
  this._sendWireData(outboundCall.serializedWireData);
2455
2727
  };
2456
2728
  this.connectionStateChanged.add(stateHandler);
@@ -2460,9 +2732,9 @@ var RpcPeer = class {
2460
2732
  if (signal !== void 0) {
2461
2733
  const onAbort = () => {
2462
2734
  if (this.outboundCalls.remove(callId) !== void 0) {
2463
- outboundCall.result.reject(new Error("Call cancelled."));
2735
+ outboundCall.result.reject((0, import_core7.cancellationError)("Call cancelled."));
2464
2736
  outboundCall.onDisconnect();
2465
- if (this._connection !== void 0)
2737
+ if (this._isConnected && this._connection !== void 0)
2466
2738
  this.hub.systemCallSender.cancel(this._connection, this.serializationFormat, callId);
2467
2739
  }
2468
2740
  };
@@ -2492,10 +2764,15 @@ var RpcPeer = class {
2492
2764
  }
2493
2765
  close() {
2494
2766
  infoLog?.log(`'${this.ref}': Stopping`);
2767
+ if (this._maintainTimer !== void 0) {
2768
+ clearInterval(this._maintainTimer);
2769
+ this._maintainTimer = void 0;
2770
+ }
2495
2771
  this.remoteObjects.disconnectAll();
2496
2772
  this.sharedObjects.disconnectAll();
2497
- this.outboundCalls.rejectAll(new Error("Peer closed."));
2773
+ this.outboundCalls.rejectAll((0, import_core7.cancellationError)("Peer closed."));
2498
2774
  this.outboundCalls.invalidateAll();
2775
+ this.inboundCalls.clear();
2499
2776
  this._connection?.close();
2500
2777
  this.hub.peers.delete(this.ref);
2501
2778
  this._setConnectionState(0 /* Disconnected */);
@@ -2534,12 +2811,18 @@ var RpcPeer = class {
2534
2811
  if (method === RpcSystemCalls.handshake) {
2535
2812
  const raw = args[0];
2536
2813
  if (raw !== void 0) {
2814
+ const obj = raw ?? {};
2537
2815
  const handshake = Array.isArray(raw) ? {
2538
2816
  RemotePeerId: raw[0],
2539
2817
  RemoteHubId: raw[2],
2540
2818
  ProtocolVersion: raw[3],
2541
2819
  Index: raw[4]
2542
- } : raw;
2820
+ } : {
2821
+ RemotePeerId: obj.RemotePeerId ?? obj.remotePeerId,
2822
+ RemoteHubId: obj.RemoteHubId ?? obj.remoteHubId,
2823
+ ProtocolVersion: obj.ProtocolVersion ?? obj.protocolVersion,
2824
+ Index: obj.Index ?? obj.index
2825
+ };
2543
2826
  this._onHandshakeReceived(handshake);
2544
2827
  }
2545
2828
  return;
@@ -2553,57 +2836,85 @@ var RpcPeer = class {
2553
2836
  _handleInbound(message, args) {
2554
2837
  const method = message.Method ?? "";
2555
2838
  const relatedId = message.RelatedId ?? 0;
2556
- const call = new RpcInboundCall(relatedId, method, args);
2557
2839
  const methodDef = this.hub.serviceHost.getMethodDef(method);
2558
2840
  const isNoWait = methodDef?.noWait === true;
2559
- if (!isNoWait) {
2560
- this.inboundCalls.register(call);
2561
- }
2562
2841
  const serviceHost = this.hub.serviceHost;
2563
- void (async () => {
2842
+ const dispatch = async (call2) => {
2564
2843
  try {
2565
2844
  const context = this._connection !== void 0 ? {
2566
2845
  __rpcDispatch: true,
2567
2846
  callId: relatedId,
2568
- connection: this._connection
2847
+ callType: message.CallType ?? 0,
2848
+ connection: this._connection,
2849
+ signal: call2?.signal,
2850
+ peer: this
2569
2851
  } : void 0;
2570
- const result = await serviceHost.dispatch(
2571
- method,
2572
- args,
2573
- context
2574
- );
2575
- if (methodDef?.stream === true && this._connection !== void 0) {
2576
- const stream = result instanceof RpcStream ? result : new RpcStream(result);
2577
- this.hub.systemCallSender.ok(
2578
- this._connection,
2579
- this.serializationFormat,
2580
- relatedId,
2581
- stream.toRef(this)
2582
- );
2583
- } else if (!isNoWait && this._connection !== void 0) {
2584
- this.hub.systemCallSender.ok(
2585
- this._connection,
2586
- this.serializationFormat,
2587
- relatedId,
2588
- result
2589
- );
2852
+ const result = await serviceHost.dispatch(method, args, context);
2853
+ if (methodDef?.stream === true) {
2854
+ if (this._connection !== void 0) {
2855
+ const stream = result instanceof RpcStream ? result : new RpcStream(result);
2856
+ const ref = stream.toRef(this);
2857
+ const send = () => {
2858
+ if (call2?.isCancelled === true) return;
2859
+ if (this._connection !== void 0)
2860
+ this.hub.systemCallSender.ok(
2861
+ this._connection,
2862
+ this.serializationFormat,
2863
+ relatedId,
2864
+ ref
2865
+ );
2866
+ };
2867
+ if (call2 !== void 0) {
2868
+ call2.setResult(send);
2869
+ this.inboundCalls.markCompleted(call2);
2870
+ }
2871
+ send();
2872
+ }
2873
+ } else if (call2 !== void 0) {
2874
+ const send = () => {
2875
+ if (call2.isCancelled) return;
2876
+ if (this._connection !== void 0)
2877
+ this.hub.systemCallSender.ok(
2878
+ this._connection,
2879
+ this.serializationFormat,
2880
+ relatedId,
2881
+ result
2882
+ );
2883
+ };
2884
+ call2.setResult(send);
2885
+ this.inboundCalls.markCompleted(call2);
2886
+ send();
2590
2887
  }
2591
2888
  } catch (e) {
2592
- if (!isNoWait && this._connection !== void 0) {
2593
- this.hub.systemCallSender.error(
2594
- this._connection,
2595
- this.serializationFormat,
2596
- relatedId,
2597
- e
2598
- );
2889
+ if (call2 !== void 0) {
2890
+ const send = () => {
2891
+ if (call2.isCancelled) return;
2892
+ if (this._connection !== void 0)
2893
+ this.hub.systemCallSender.error(
2894
+ this._connection,
2895
+ this.serializationFormat,
2896
+ relatedId,
2897
+ e
2898
+ );
2899
+ };
2900
+ call2.setResult(send);
2901
+ this.inboundCalls.markCompleted(call2);
2902
+ send();
2599
2903
  }
2600
2904
  debugLog2?.log(`'${this.ref}': Inbound call failed: ${method}#${relatedId}`, e);
2601
- } finally {
2602
- if (!isNoWait) {
2603
- this.inboundCalls.remove(relatedId);
2604
- }
2605
2905
  }
2606
- })();
2906
+ };
2907
+ if (isNoWait) {
2908
+ void dispatch(void 0);
2909
+ return;
2910
+ }
2911
+ const call = new RpcInboundCall(relatedId, method, args);
2912
+ const existing = this.inboundCalls.getOrRegister(call);
2913
+ if (existing !== call) {
2914
+ existing.resendResult();
2915
+ return;
2916
+ }
2917
+ void dispatch(call);
2607
2918
  }
2608
2919
  /** Start sending `$sys.KeepAlive` pings and arm the silence watchdog.
2609
2920
  * Called when the peer transitions into `Connected` (after handshake).
@@ -2659,15 +2970,17 @@ var RpcConnectionState = /* @__PURE__ */ ((RpcConnectionState2) => {
2659
2970
  })(RpcConnectionState || {});
2660
2971
  var RpcClientPeer = class extends RpcPeer {
2661
2972
  _disposed = false;
2973
+ /** Aborted when the peer is stopped/disposed — threaded into the reconnect
2974
+ * delay so a pending delay rejects promptly instead of parking. */
2975
+ _stopController = new AbortController();
2662
2976
  _tryIndex = 0;
2663
- _handshakeIndex = 0;
2664
2977
  /** Index from the MOST RECENTLY RECEIVED server handshake — sent back
2665
2978
  * to the server in `$sys.Reconnect:3` so the server can verify we're
2666
2979
  * talking about the current connection. Mirrors .NET's
2667
2980
  * `connectionState.Handshake.Index` path in
2668
2981
  * `RpcOutboundCallTracker.TryReconnect`. */
2669
2982
  _remoteHandshakeIndex = 0;
2670
- _lastRemoteHubId;
2983
+ _lastRemotePeerId;
2671
2984
  _pendingHandshake;
2672
2985
  _reconnectsAt = 0;
2673
2986
  /** Base64url-encoded peer ID — matches .NET's RpcClientPeer.ClientId (Guid.ToBase64Url). */
@@ -2688,6 +3001,10 @@ var RpcClientPeer = class extends RpcPeer {
2688
3001
  * the connection attempt. Initialized from `hub.limits` at construction;
2689
3002
  * set directly to override on a single peer. */
2690
3003
  handshakeTimeoutMs;
3004
+ /** A connection that lived less than this before dropping does NOT reset
3005
+ * `_tryIndex`, so a crash-looping server sees growing reconnect delays.
3006
+ * Initialized from `hub.limits` at construction; override per peer. */
3007
+ prematureDisconnectTimeoutMs;
2691
3008
  peerChanged = new import_core7.EventHandlerSet();
2692
3009
  /** Fired when the server rejects the connection due to an unsupported serialization format (close code 4010). */
2693
3010
  unsupportedFormat = new import_core7.EventHandlerSet();
@@ -2720,6 +3037,7 @@ var RpcClientPeer = class extends RpcPeer {
2720
3037
  super(hub, url);
2721
3038
  this.connectTimeoutMs = hub.limits.connectTimeoutMs;
2722
3039
  this.handshakeTimeoutMs = hub.limits.handshakeTimeoutMs;
3040
+ this.prematureDisconnectTimeoutMs = hub.limits.prematureDisconnectTimeoutMs;
2723
3041
  this.clientId = guidToBase64Url(this.id);
2724
3042
  const resolver = RpcSerializationFormatResolver.Default;
2725
3043
  this.serializationFormat = resolver.get(parseFormatFromUrl(url) ?? resolver.defaultFormatKey);
@@ -2756,7 +3074,7 @@ var RpcClientPeer = class extends RpcPeer {
2756
3074
  try {
2757
3075
  while (!this._disposed) {
2758
3076
  this._setConnectionState(1 /* Connecting */);
2759
- const retryDelay = this.hub.reconnectDelayer.getDelay(this._tryIndex);
3077
+ const retryDelay = this.hub.reconnectDelayer.getDelay(this._tryIndex, this._stopController.signal);
2760
3078
  if (retryDelay.isLimitExceeded) {
2761
3079
  warnLog6?.log(`'${this.ref}': Reconnect limit exceeded - shutting down`);
2762
3080
  this._disposed = true;
@@ -2765,12 +3083,14 @@ var RpcClientPeer = class extends RpcPeer {
2765
3083
  this._setReconnectsAt(retryDelay.endsAt);
2766
3084
  try {
2767
3085
  await retryDelay.promise;
3086
+ } catch {
2768
3087
  } finally {
2769
3088
  this._setReconnectsAt(0);
2770
3089
  }
2771
3090
  if (this._disposed) break;
2772
3091
  let lastCloseCode = 0;
2773
3092
  let lastCloseReason = "";
3093
+ let connectedAt = 0;
2774
3094
  try {
2775
3095
  const connUrl = await this.connectionUrlResolver(this);
2776
3096
  infoLog?.log(`'${this.ref}': Connecting to ${sanitizeUrl(connUrl)}`);
@@ -2817,7 +3137,7 @@ var RpcClientPeer = class extends RpcPeer {
2817
3137
  this.serializationFormat,
2818
3138
  this.id,
2819
3139
  this.hub.hubId,
2820
- ++this._handshakeIndex
3140
+ ++this._ownHandshakeIndex
2821
3141
  );
2822
3142
  } catch (e) {
2823
3143
  warnLog6?.log(`'${this.ref}': Failed to send handshake`, e);
@@ -2841,20 +3161,10 @@ var RpcClientPeer = class extends RpcPeer {
2841
3161
  this._pendingHandshake = void 0;
2842
3162
  }
2843
3163
  this._remoteHandshakeIndex = remoteHandshake.Index ?? 0;
2844
- const remoteHubId = remoteHandshake.RemoteHubId;
2845
- let isPeerChanged = false;
2846
- if (remoteHubId !== void 0) {
2847
- isPeerChanged = this._lastRemoteHubId !== void 0 && this._lastRemoteHubId !== remoteHubId;
2848
- if (isPeerChanged) {
2849
- this.inboundCalls.clear();
2850
- infoLog?.log(`'${this.ref}': Peer changed`);
2851
- this.peerChanged.trigger();
2852
- }
2853
- this._lastRemoteHubId = remoteHubId;
2854
- }
3164
+ const isPeerChanged = this._detectPeerChange(remoteHandshake);
2855
3165
  infoLog?.log(`'${this.ref}': Handshake succeeded, peerChanged=${isPeerChanged}`);
2856
3166
  this._setConnectionState(3 /* Connected */);
2857
- this._tryIndex = 0;
3167
+ connectedAt = Date.now();
2858
3168
  await this._reconnect(isPeerChanged, closedController.signal);
2859
3169
  await whenClosed;
2860
3170
  } catch (e) {
@@ -2868,6 +3178,8 @@ var RpcClientPeer = class extends RpcPeer {
2868
3178
  this._disposed = true;
2869
3179
  break;
2870
3180
  }
3181
+ if (connectedAt !== 0 && Date.now() - connectedAt >= this.prematureDisconnectTimeoutMs)
3182
+ this._tryIndex = 0;
2871
3183
  this._tryIndex++;
2872
3184
  }
2873
3185
  } finally {
@@ -2923,6 +3235,8 @@ var RpcClientPeer = class extends RpcPeer {
2923
3235
  continue;
2924
3236
  }
2925
3237
  eligible.push(call);
3238
+ if (call.sentAt !== 0)
3239
+ call.sentAt = Date.now();
2926
3240
  }
2927
3241
  let unknownIds = null;
2928
3242
  if (!isPeerChanged && eligible.length > 0) {
@@ -2939,6 +3253,7 @@ var RpcClientPeer = class extends RpcPeer {
2939
3253
  if (unknownIds !== null && !unknownIds.has(call.callId)) {
2940
3254
  continue;
2941
3255
  }
3256
+ call.sentAt = Date.now();
2942
3257
  this._sendWireData(call.serializedWireData);
2943
3258
  resentSinceYield++;
2944
3259
  if (resentSinceYield >= YIELD_BATCH) {
@@ -3004,30 +3319,80 @@ var RpcClientPeer = class extends RpcPeer {
3004
3319
  return new Set(eligible.map((c) => c.callId));
3005
3320
  }
3006
3321
  }
3322
+ /** Detects a server-peer identity change by comparing the incoming
3323
+ * handshake's `RemotePeerId` with the last one — C#
3324
+ * `RpcHandshake.GetPeerChangeKind` keys off `RemotePeerId`, not
3325
+ * `RemoteHubId` (an idle server peer can be replaced while keeping the
3326
+ * same hub id). On a change it clears the inbound tracker and fires
3327
+ * `peerChanged`; the very first handshake is never a change. */
3328
+ _detectPeerChange(handshake) {
3329
+ const remotePeerId = handshake.RemotePeerId;
3330
+ if (remotePeerId === void 0)
3331
+ return false;
3332
+ const isPeerChanged = this._lastRemotePeerId !== void 0 && this._lastRemotePeerId !== remotePeerId;
3333
+ this._lastRemotePeerId = remotePeerId;
3334
+ if (isPeerChanged) {
3335
+ this.inboundCalls.clear();
3336
+ infoLog?.log(`'${this.ref}': Peer changed`);
3337
+ this.peerChanged.trigger();
3338
+ }
3339
+ return isPeerChanged;
3340
+ }
3007
3341
  _onHandshakeReceived(handshake) {
3008
3342
  this._pendingHandshake?.resolve(handshake);
3009
3343
  }
3010
3344
  close() {
3011
3345
  this._disposed = true;
3346
+ this._stopController.abort();
3012
3347
  super.close();
3013
3348
  }
3014
3349
  };
3015
3350
  var RpcServerPeer = class extends RpcPeer {
3016
- // Needed to widen the base `protected constructor` to public.
3017
- // eslint-disable-next-line @typescript-eslint/no-useless-constructor
3351
+ /** Grace period before an idle (disconnected) server peer is stopped and
3352
+ * removed from `hub.peers`; a re-`accept()` within it cancels removal.
3353
+ * Snapshotted from `hub.limits` at construction (F11). */
3354
+ serverPeerCloseTimeoutMs;
3355
+ _closeTimer;
3356
+ _isClosed = false;
3018
3357
  constructor(hub, ref) {
3019
3358
  super(hub, ref);
3359
+ this.serverPeerCloseTimeoutMs = hub.limits.serverPeerCloseTimeoutMs;
3020
3360
  }
3021
3361
  /** Accept an incoming connection — sets up message handling and handshake response. */
3022
3362
  accept(conn) {
3363
+ this._clearCloseTimer();
3023
3364
  this._setConnectionState(1 /* Connecting */);
3024
3365
  this.setupConnection(conn);
3025
3366
  this._setConnectionState(2 /* Handshaking */);
3026
3367
  conn.closed.add(() => {
3027
3368
  if (this._connection !== conn && this._connection !== void 0) return;
3028
3369
  this.sharedObjects.disconnectAll();
3370
+ this._armCloseTimer();
3029
3371
  });
3030
3372
  }
3373
+ close() {
3374
+ this._isClosed = true;
3375
+ this._clearCloseTimer();
3376
+ super.close();
3377
+ }
3378
+ _armCloseTimer() {
3379
+ this._clearCloseTimer();
3380
+ if (this._isClosed) return;
3381
+ const timeout = this.serverPeerCloseTimeoutMs;
3382
+ if (!(timeout > 0) || !Number.isFinite(timeout)) return;
3383
+ this._closeTimer = setTimeout(() => {
3384
+ this._closeTimer = void 0;
3385
+ if (this._connection === void 0)
3386
+ this.close();
3387
+ }, timeout);
3388
+ this._closeTimer.unref?.();
3389
+ }
3390
+ _clearCloseTimer() {
3391
+ if (this._closeTimer !== void 0) {
3392
+ clearTimeout(this._closeTimer);
3393
+ this._closeTimer = void 0;
3394
+ }
3395
+ }
3031
3396
  _onHandshakeReceived(_handshake) {
3032
3397
  if (this._connection !== void 0) {
3033
3398
  this.hub.systemCallSender.handshake(
@@ -3035,7 +3400,7 @@ var RpcServerPeer = class extends RpcPeer {
3035
3400
  this.serializationFormat,
3036
3401
  this.id,
3037
3402
  this.hub.hubId,
3038
- 0
3403
+ ++this._ownHandshakeIndex
3039
3404
  );
3040
3405
  this._setConnectionState(3 /* Connected */);
3041
3406
  }
@@ -3074,6 +3439,12 @@ var RpcLimits = class _RpcLimits {
3074
3439
  /** Process-wide default. Hubs read from this when they're constructed
3075
3440
  * unless caller assigns a custom instance to `hub.limits`. */
3076
3441
  static Default = new _RpcLimits();
3442
+ /** Dev preset with a relaxed keep-alive timeout (5 min), matching .NET's
3443
+ * debugger-attached `KeepAlivePeriod` (RpcLimits.cs:46-54). Activate
3444
+ * explicitly — e.g. `RpcLimits.Default = RpcLimits.Debug` or per-hub
3445
+ * `hub.limits = RpcLimits.Debug`. Never switched on automatically: the
3446
+ * TS side can't detect a debugger on the server (decision D4). */
3447
+ static Debug = new _RpcLimits({ keepAliveTimeoutMs: 3e5 });
3077
3448
  /** Max time to wait for the WebSocket to enter the OPEN state. On a
3078
3449
  * hung connect (mobile after network change, half-open after sleep)
3079
3450
  * the browser can take ~2 min to emit `onerror`/`onclose`; without
@@ -3081,6 +3452,22 @@ var RpcLimits = class _RpcLimits {
3081
3452
  connectTimeoutMs = 1e4;
3082
3453
  /** Max time to wait for the server's handshake response after WS opens. */
3083
3454
  handshakeTimeoutMs = 1e4;
3455
+ /** If a connection lived less than this before dropping, a graceful close
3456
+ * still counts as a failed attempt — the client keeps its growing
3457
+ * `_tryIndex` instead of resetting it, so a crash-looping server sees
3458
+ * increasing reconnect delays via `RetryDelaySeq`. Mirrors .NET
3459
+ * `RpcLimits.PrematureDisconnectTimeout` (RpcLimits.cs:18). */
3460
+ prematureDisconnectTimeoutMs = 15e3;
3461
+ /** Max completed inbound calls retained for duplicate-frame dedup
3462
+ * (a resent call id re-sends the computed result instead of
3463
+ * re-executing the handler). Deviation from .NET, which unregisters a
3464
+ * call once its result is sent: TS clients blind-resend on reconnect,
3465
+ * so completed calls are retained — bounded by this limit (oldest
3466
+ * evicted first) to cap memory on long-lived connections. */
3467
+ completedInboundCallsLimit = 1e3;
3468
+ /** How often each peer's maintenance loop scans outbound calls for
3469
+ * timeouts (R12). Mirrors .NET `RpcLimits.CallTimeoutCheckPeriod`. */
3470
+ callTimeoutCheckPeriodMs = 1e3;
3084
3471
  /** Outbound `$sys.KeepAlive` send period. */
3085
3472
  keepAlivePeriodMs = 1e4;
3086
3473
  /** Reaper threshold — connection is force-closed (and the reconnect
@@ -3090,6 +3477,13 @@ var RpcLimits = class _RpcLimits {
3090
3477
  * `keepAlivePeriodMs + stall_duration`, so with these defaults the
3091
3478
  * tolerated stall is ~15 s. */
3092
3479
  keepAliveTimeoutMs = 25e3;
3480
+ /** How long a server peer stays in `hub.peers` after its connection closes
3481
+ * before it's stopped and removed. A same-peer reconnect (`accept()`)
3482
+ * within this window cancels the pending removal, so brief drops don't
3483
+ * discard the peer's trackers. Mirrors .NET's server-peer shutdown timeout
3484
+ * (`RpcPeerOptions.ServerPeerShutdownTimeoutProvider`, 3-15 min); the TS
3485
+ * port uses a fixed floor-matching default. */
3486
+ serverPeerCloseTimeoutMs = 18e4;
3093
3487
  constructor(overrides) {
3094
3488
  if (overrides) Object.assign(this, overrides);
3095
3489
  }
@@ -3100,7 +3494,7 @@ var import_core8 = require("@actuallab/core");
3100
3494
  var RpcClientPeerReconnectDelayer = class extends import_core8.RetryDelayer {
3101
3495
  constructor() {
3102
3496
  super();
3103
- this.delays = import_core8.RetryDelaySeq.fixed(100);
3497
+ this.delays = import_core8.RetryDelaySeq.exp(100, 1e4);
3104
3498
  }
3105
3499
  };
3106
3500
 
@@ -3108,13 +3502,14 @@ var RpcClientPeerReconnectDelayer = class extends import_core8.RetryDelayer {
3108
3502
  var { warnLog: warnLog7 } = getLogs("RpcServiceHost");
3109
3503
  var RpcServiceHost = class {
3110
3504
  _methods = /* @__PURE__ */ new Map();
3111
- register(def, impl) {
3505
+ register(def, impl, receiver = impl) {
3112
3506
  for (const methodDef of def.methods.values()) {
3113
3507
  const fn = impl[methodDef.name];
3114
3508
  if (!fn) continue;
3115
3509
  this._methods.set(wireMethodName(methodDef), {
3116
3510
  def: methodDef,
3117
- fn
3511
+ fn,
3512
+ receiver
3118
3513
  });
3119
3514
  }
3120
3515
  }
@@ -3125,9 +3520,9 @@ var RpcServiceHost = class {
3125
3520
  throw new Error(`Method not found: ${wireMethod}`);
3126
3521
  }
3127
3522
  if (context !== void 0 && entry.def.callTypeId !== 0) {
3128
- return await entry.fn(...args, context);
3523
+ return await entry.fn.call(entry.receiver, ...args, context);
3129
3524
  }
3130
- return await entry.fn(...args);
3525
+ return await entry.fn.call(entry.receiver, ...args);
3131
3526
  }
3132
3527
  getMethodDef(wireMethod) {
3133
3528
  return this._methods.get(wireMethod)?.def;
@@ -3135,6 +3530,7 @@ var RpcServiceHost = class {
3135
3530
  };
3136
3531
 
3137
3532
  // src/rpc-decorators.ts
3533
+ var import_core9 = require("@actuallab/core");
3138
3534
  var METHODS_META = /* @__PURE__ */ Symbol.for("actuallab.methods");
3139
3535
  var SERVICE_META = /* @__PURE__ */ Symbol.for("actuallab.service");
3140
3536
  function getMethodsMeta(cls) {
@@ -3145,17 +3541,17 @@ function getServiceMeta(cls) {
3145
3541
  }
3146
3542
  function rpcService(serviceName) {
3147
3543
  return function(_target, context) {
3148
- const meta = context.metadata[SERVICE_META] ??= {};
3544
+ const meta = (0, import_core9.ownMetadata)(context.metadata, SERVICE_META);
3149
3545
  meta.name = serviceName;
3150
3546
  };
3151
3547
  }
3152
3548
  function rpcMethod(options) {
3153
3549
  return function(target, context) {
3154
3550
  const methodName = String(context.name);
3155
- const methods = context.metadata[METHODS_META] ??= {};
3551
+ const methods = (0, import_core9.ownMetadata)(context.metadata, METHODS_META);
3156
3552
  methods[methodName] = {
3157
3553
  ...methods[methodName],
3158
- argCount: target.length,
3554
+ argCount: (0, import_core9.resolveArgCount)(target, options?.argCount, methodName),
3159
3555
  returns: options?.returns,
3160
3556
  remoteExecutionMode: options?.remoteExecutionMode
3161
3557
  };
@@ -3701,7 +4097,7 @@ var RpcHub = class {
3701
4097
  hubId;
3702
4098
  peers = /* @__PURE__ */ new Map();
3703
4099
  serviceHost;
3704
- systemCallSender = new RpcSystemCallSender();
4100
+ systemCallSender;
3705
4101
  systemCallHandler = new RpcSystemCallHandler();
3706
4102
  /** Method registry for compact format hash ↔ name resolution.
3707
4103
  * Created lazily, populated by addService/addClient. */
@@ -3709,6 +4105,14 @@ var RpcHub = class {
3709
4105
  /** Shared reconnect delayer used by every client peer in this hub. Swap in
3710
4106
  * a custom subclass (e.g. app-level signal-gated) before peers start. */
3711
4107
  reconnectDelayer = new RpcClientPeerReconnectDelayer();
4108
+ /** Cache of client proxies keyed by peer, then by service name — so
4109
+ * repeated {@link addClient} calls for the same service+peer return one
4110
+ * proxy. All consumers then share a single computed/call/invalidation
4111
+ * stream per logical value, matching .NET's singleton client proxies (F8).
4112
+ * A later def registered under the same name extends the existing proxy
4113
+ * with its extra methods (superset), so no consumer ever receives a proxy
4114
+ * missing methods the def declares. */
4115
+ _clientProxies = /* @__PURE__ */ new WeakMap();
3712
4116
  /** Connection-lifecycle timing limits. Peers constructed against this hub
3713
4117
  * read their initial `*Ms` field values from this instance; later
3714
4118
  * mutations affect only peers built afterward (existing peers can be
@@ -3726,6 +4130,7 @@ var RpcHub = class {
3726
4130
  constructor(hubId) {
3727
4131
  this.hubId = hubId ?? crypto.randomUUID();
3728
4132
  this.serviceHost = new RpcServiceHost();
4133
+ this.systemCallSender = this._createSystemCallSender();
3729
4134
  this.systemCallSender.registry = this.registry;
3730
4135
  for (const methodName of Object.values(RpcSystemCalls)) {
3731
4136
  this.registry.register(methodName);
@@ -3786,50 +4191,38 @@ var RpcHub = class {
3786
4191
  if (!fn) continue;
3787
4192
  wrappedImpl[methodDef.name] = methodDef.callTypeId !== 0 ? this._wrapServerMethod(methodDef, fn, impl) : fn;
3788
4193
  }
3789
- this.serviceHost.register(def, wrappedImpl);
4194
+ this.serviceHost.register(def, wrappedImpl, impl);
3790
4195
  this.registry.registerService(def.name, def.methods);
3791
4196
  }
3792
- /** Create a typed client proxy for a service on a remote peer. */
4197
+ /** Create a typed client proxy for a service on a remote peer. Repeated calls
4198
+ * for the same (peer, service name) return one shared proxy; a def carrying
4199
+ * extra methods extends it (F8). */
3793
4200
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T is used for caller-specified proxy type
3794
4201
  addClient(peer, defOrContract) {
3795
4202
  const def = this._resolveServiceDef(defOrContract);
3796
4203
  this.registry.registerService(def.name, def.methods);
3797
- const overloads = /* @__PURE__ */ new Map();
3798
- for (const methodDef of def.methods.values()) {
3799
- let byArgCount = overloads.get(methodDef.name);
3800
- if (!byArgCount) {
3801
- byArgCount = /* @__PURE__ */ new Map();
3802
- overloads.set(methodDef.name, byArgCount);
3803
- }
3804
- byArgCount.set(
3805
- methodDef.argCount,
3806
- this._createClientMethod(peer, methodDef)
3807
- );
4204
+ let byService = this._clientProxies.get(peer);
4205
+ if (byService === void 0) {
4206
+ byService = /* @__PURE__ */ new Map();
4207
+ this._clientProxies.set(peer, byService);
3808
4208
  }
3809
- const methods = /* @__PURE__ */ new Map();
3810
- for (const [name, byArgCount] of overloads) {
3811
- if (byArgCount.size === 1) {
3812
- const [[, singleFn]] = byArgCount;
3813
- methods.set(name, singleFn);
3814
- } else {
3815
- methods.set(name, (...args) => {
3816
- const fn = byArgCount.get(args.length);
3817
- if (!fn)
3818
- throw new Error(
3819
- `No overload of ${name} accepts ${args.length} arguments`
3820
- );
3821
- return fn(...args);
3822
- });
3823
- }
4209
+ let entry = byService.get(def.name);
4210
+ if (entry === void 0) {
4211
+ entry = this._createClientProxy();
4212
+ byService.set(def.name, entry);
3824
4213
  }
3825
- return new Proxy({}, {
3826
- get: (_target, prop) => typeof prop === "string" ? methods.get(prop) : void 0
3827
- });
4214
+ this._extendClientProxy(peer, entry, def);
4215
+ return entry.proxy;
3828
4216
  }
3829
4217
  close() {
3830
4218
  for (const peer of this.peers.values()) peer.close();
3831
4219
  this.peers.clear();
3832
4220
  }
4221
+ /** Create the hub's system-call sender. Override in FusionHub to add
4222
+ * Fusion-specific system calls (e.g. $sys-c.Invalidate). */
4223
+ _createSystemCallSender() {
4224
+ return new RpcSystemCallSender();
4225
+ }
3833
4226
  /** Build an RpcServiceDef from decorator metadata on a contract class. Override in FusionHub for compute support. */
3834
4227
  _buildServiceDef(cls) {
3835
4228
  const svcMeta = getServiceMeta(cls);
@@ -3887,12 +4280,10 @@ var RpcHub = class {
3887
4280
  throw new Error(
3888
4281
  `Expected stream reference, got: ${JSON.stringify(result)}`
3889
4282
  );
3890
- const stream = new RpcStream(ref, peer);
3891
- peer.remoteObjects.register(stream);
3892
- return stream;
4283
+ return new RpcStream(ref, peer);
3893
4284
  };
3894
4285
  }
3895
- const callOptions = methodDef.callTypeId !== 0 || mode !== RpcRemoteExecutionMode.Default ? { callTypeId: methodDef.callTypeId, remoteExecutionMode: mode } : void 0;
4286
+ const callOptions = methodDef.callTypeId !== 0 || mode !== RpcRemoteExecutionMode.Default || methodDef.timeouts !== void 0 ? { callTypeId: methodDef.callTypeId, remoteExecutionMode: mode, timeouts: methodDef.timeouts } : void 0;
3896
4287
  return async (...args) => {
3897
4288
  if (mustCheckConnection && !peer.isConnected)
3898
4289
  throw new Error("Outbound call failed: not connected and AwaitForConnection is not set.");
@@ -3902,7 +4293,58 @@ var RpcHub = class {
3902
4293
  return resolveStreamRefs(result, peer);
3903
4294
  };
3904
4295
  }
4296
+ // Private methods
4297
+ _createClientProxy() {
4298
+ const methods = /* @__PURE__ */ new Map();
4299
+ const proxy = new Proxy({}, {
4300
+ get: (_target, prop) => typeof prop === "string" ? methods.get(prop) : void 0
4301
+ });
4302
+ return { proxy, methods, overloads: /* @__PURE__ */ new Map() };
4303
+ }
4304
+ _extendClientProxy(peer, entry, def) {
4305
+ for (const methodDef of def.methods.values()) {
4306
+ const existing = entry.overloads.get(methodDef.name)?.get(methodDef.argCount);
4307
+ if (existing !== void 0 && !isSameWireMethod(existing.def, methodDef))
4308
+ throw new Error(
4309
+ `RpcHub.addClient: conflicting definition of '${wireMethodName(methodDef)}' under service '${def.name}' \u2014 a different signature is already registered.`
4310
+ );
4311
+ }
4312
+ const changed = /* @__PURE__ */ new Set();
4313
+ for (const methodDef of def.methods.values()) {
4314
+ let byArgCount = entry.overloads.get(methodDef.name);
4315
+ if (byArgCount === void 0) {
4316
+ byArgCount = /* @__PURE__ */ new Map();
4317
+ entry.overloads.set(methodDef.name, byArgCount);
4318
+ }
4319
+ if (byArgCount.has(methodDef.argCount))
4320
+ continue;
4321
+ byArgCount.set(methodDef.argCount, {
4322
+ def: methodDef,
4323
+ fn: this._createClientMethod(peer, methodDef)
4324
+ });
4325
+ changed.add(methodDef.name);
4326
+ }
4327
+ for (const name of changed) {
4328
+ const byArgCount = entry.overloads.get(name);
4329
+ entry.methods.set(name, buildOverloadFn(name, byArgCount));
4330
+ }
4331
+ }
3905
4332
  };
4333
+ function buildOverloadFn(name, byArgCount) {
4334
+ if (byArgCount.size === 1) {
4335
+ const [[, only]] = byArgCount;
4336
+ return only.fn;
4337
+ }
4338
+ return (...args) => {
4339
+ const overload = byArgCount.get(args.length);
4340
+ if (overload === void 0)
4341
+ throw new Error(`No overload of ${name} accepts ${args.length} arguments`);
4342
+ return overload.fn(...args);
4343
+ };
4344
+ }
4345
+ function isSameWireMethod(a, b) {
4346
+ return a.wireArgCount === b.wireArgCount && a.callTypeId === b.callTypeId && a.stream === b.stream && a.noWait === b.noWait && a.remoteExecutionMode === b.remoteExecutionMode;
4347
+ }
3906
4348
 
3907
4349
  // src/rpc-peer-ref-builder.ts
3908
4350
  var RpcPeerRefBuilder = class {
@@ -3969,7 +4411,7 @@ function createMethod(peer, methodDef) {
3969
4411
  throw new Error("Outbound call failed: not connected and AwaitForConnection is not set.");
3970
4412
  const callArgs = args.slice(0, methodDef.argCount);
3971
4413
  const wireName = wireMethodName(methodDef);
3972
- const options = methodDef.callTypeId !== 0 || mode !== RpcRemoteExecutionMode.Default ? { callTypeId: methodDef.callTypeId, remoteExecutionMode: mode } : void 0;
4414
+ const options = methodDef.callTypeId !== 0 || mode !== RpcRemoteExecutionMode.Default || methodDef.timeouts !== void 0 ? { callTypeId: methodDef.callTypeId, remoteExecutionMode: mode, timeouts: methodDef.timeouts } : void 0;
3973
4415
  const outboundCall = peer.call(wireName, callArgs, options);
3974
4416
  return outboundCall.result.promise;
3975
4417
  };
@@ -4009,14 +4451,14 @@ function getStateDescription(state, useLastError = false) {
4009
4451
  }
4010
4452
 
4011
4453
  // src/rpc-peer-state-monitor.ts
4012
- var import_core9 = require("@actuallab/core");
4454
+ var import_core10 = require("@actuallab/core");
4013
4455
  var { infoLog: infoLog2 } = getLogs("RpcPeerStateMonitor");
4014
4456
  var JustConnectedPeriodMs = 1500;
4015
4457
  var JustDisconnectedPeriodMs = 3e3;
4016
4458
  var MinReconnectsInMs = 1e3;
4017
4459
  var RpcPeerStateMonitor = class {
4018
4460
  peer;
4019
- stateChanged = new import_core9.EventHandlerSet();
4461
+ stateChanged = new import_core10.EventHandlerSet();
4020
4462
  _rawConnected;
4021
4463
  _connectedAt = 0;
4022
4464
  _disconnectedAt = 0;
@@ -4165,8 +4607,10 @@ var RpcPeerStateMonitor = class {
4165
4607
  ENVELOPE_DELIMITER,
4166
4608
  FRAME_DELIMITER,
4167
4609
  IncreasingSeqCompressor,
4610
+ REMOTE_EXCEPTION_TYPE_REF,
4168
4611
  RPC_CLOSE_CODE_UNSUPPORTED_FORMAT,
4169
4612
  RpcCallStage,
4613
+ RpcCallTimeouts,
4170
4614
  RpcCallTypeId,
4171
4615
  RpcClientPeer,
4172
4616
  RpcClientPeerReconnectDelayer,
@@ -4221,6 +4665,7 @@ var RpcPeerStateMonitor = class {
4221
4665
  isConnected,
4222
4666
  likelyConnected,
4223
4667
  parseStreamRef,
4668
+ readPolymorphismMarker,
4224
4669
  resolveStreamRefs,
4225
4670
  rpcMethod,
4226
4671
  rpcService,
@@ -4232,6 +4677,7 @@ var RpcPeerStateMonitor = class {
4232
4677
  splitBinaryFrame,
4233
4678
  splitCompactBinaryFrame,
4234
4679
  splitFrame,
4680
+ toExceptionInfo,
4235
4681
  wireMethodName,
4236
4682
  xxh3_64,
4237
4683
  xxh3_64str