@actuallab/rpc 12.3.14 → 12.3.31

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
@@ -33,7 +33,9 @@ __export(index_exports, {
33
33
  ARG_DELIMITER: () => ARG_DELIMITER,
34
34
  ENVELOPE_DELIMITER: () => ENVELOPE_DELIMITER,
35
35
  FRAME_DELIMITER: () => FRAME_DELIMITER,
36
+ IncreasingSeqCompressor: () => IncreasingSeqCompressor,
36
37
  RPC_CLOSE_CODE_UNSUPPORTED_FORMAT: () => RPC_CLOSE_CODE_UNSUPPORTED_FORMAT,
38
+ RpcCallStage: () => RpcCallStage,
37
39
  RpcCallTypeId: () => RpcCallTypeId,
38
40
  RpcClientPeer: () => RpcClientPeer,
39
41
  RpcClientPeerReconnectDelayer: () => RpcClientPeerReconnectDelayer,
@@ -78,6 +80,7 @@ __export(index_exports, {
78
80
  deserializeBinaryMessage: () => deserializeBinaryMessage,
79
81
  deserializeCompactBinaryMessage: () => deserializeCompactBinaryMessage,
80
82
  deserializeMessage: () => deserializeMessage,
83
+ getLogs: () => getLogs,
81
84
  getMethodsMeta: () => getMethodsMeta,
82
85
  getServiceMeta: () => getServiceMeta,
83
86
  getStateDescription: () => getStateDescription,
@@ -101,6 +104,33 @@ __export(index_exports, {
101
104
  });
102
105
  module.exports = __toCommonJS(index_exports);
103
106
 
107
+ // src/logging.ts
108
+ var import_core = require("@actuallab/core");
109
+ var scopeDefaults = {
110
+ RpcCallTracker: import_core.LogLevel.Warn,
111
+ RpcClient: import_core.LogLevel.Warn,
112
+ RpcClientPeerReconnectDelayer: import_core.LogLevel.Warn,
113
+ RpcConnection: import_core.LogLevel.Warn,
114
+ RpcHub: import_core.LogLevel.Warn,
115
+ RpcInterceptor: import_core.LogLevel.Warn,
116
+ RpcMessageChannelConnection: import_core.LogLevel.Warn,
117
+ RpcMethodRegistry: import_core.LogLevel.Warn,
118
+ RpcOutboundCall: import_core.LogLevel.Warn,
119
+ RpcPeer: import_core.LogLevel.Info,
120
+ RpcPeerStateMonitor: import_core.LogLevel.Warn,
121
+ RpcRemoteObjectTracker: import_core.LogLevel.Warn,
122
+ RpcSerialization: import_core.LogLevel.Warn,
123
+ RpcServiceHost: import_core.LogLevel.Warn,
124
+ RpcSharedObjectTracker: import_core.LogLevel.Warn,
125
+ RpcSharedStream: import_core.LogLevel.Warn,
126
+ RpcStream: import_core.LogLevel.Warn,
127
+ RpcStreamSender: import_core.LogLevel.Warn,
128
+ RpcSystemCallHandler: import_core.LogLevel.Warn,
129
+ RpcSystemCallSender: import_core.LogLevel.Warn,
130
+ RpcWebSocketClient: import_core.LogLevel.Warn
131
+ };
132
+ var getLogs = (0, import_core.createLogProvider)("rpc.", scopeDefaults);
133
+
104
134
  // src/rpc-message.ts
105
135
  var RpcCallTypeId = /* @__PURE__ */ ((RpcCallTypeId2) => {
106
136
  RpcCallTypeId2[RpcCallTypeId2["Regular"] = 0] = "Regular";
@@ -125,7 +155,44 @@ var ARG_DELIMITER = "";
125
155
  var FRAME_DELIMITER = "\n";
126
156
 
127
157
  // src/rpc-serialization.ts
158
+ var import_msgpack2 = require("@msgpack/msgpack");
159
+
160
+ // src/msgpack-map-patch.ts
128
161
  var import_msgpack = require("@msgpack/msgpack");
162
+ function patchMsgpackEncoderForMaps() {
163
+ const proto = import_msgpack.Encoder.prototype;
164
+ if (proto._hasMapPatch) return;
165
+ proto._hasMapPatch = true;
166
+ const originalDoEncode = proto.doEncode;
167
+ proto.doEncode = function(object, depth) {
168
+ if (object instanceof Map) {
169
+ _encodeMapInstance.call(this, object, depth);
170
+ return;
171
+ }
172
+ originalDoEncode.call(this, object, depth);
173
+ };
174
+ function _encodeMapInstance(m, depth) {
175
+ const self = this;
176
+ const size = m.size;
177
+ if (size < 16) {
178
+ self.writeU8(128 + size);
179
+ } else if (size < 65536) {
180
+ self.writeU8(222);
181
+ self.writeU16(size);
182
+ } else {
183
+ self.writeU8(223);
184
+ self.writeU32(size);
185
+ }
186
+ for (const [key, value] of m) {
187
+ self.doEncode(key, depth + 1);
188
+ self.doEncode(value, depth + 1);
189
+ }
190
+ }
191
+ }
192
+ patchMsgpackEncoderForMaps();
193
+
194
+ // src/rpc-serialization.ts
195
+ var { warnLog } = getLogs("RpcSerialization");
129
196
  function serializeMessage(message, args) {
130
197
  const envelope = JSON.stringify(message);
131
198
  if (args === void 0 || args.length === 0) {
@@ -155,9 +222,7 @@ function deserializeMessage(raw) {
155
222
  try {
156
223
  return JSON.parse(s);
157
224
  } catch {
158
- console.warn(
159
- `[RpcSerialization] failed to parse arg as JSON: ${s.substring(0, 100)}`
160
- );
225
+ warnLog?.log(`failed to parse arg as JSON: ${s.substring(0, 100)}`);
161
226
  return s;
162
227
  }
163
228
  });
@@ -166,15 +231,15 @@ function deserializeMessage(raw) {
166
231
  var textEncoder = new TextEncoder();
167
232
  var textDecoder = new TextDecoder();
168
233
  var INITIAL_ENCODER_BUFFER_SIZE = 32 * 1024;
169
- var defaultBinaryEncoder = new import_msgpack.Encoder(
234
+ var defaultBinaryEncoder = new import_msgpack2.Encoder(
170
235
  void 0,
171
236
  void 0,
172
237
  void 0,
173
238
  INITIAL_ENCODER_BUFFER_SIZE
174
239
  );
175
- var defaultBinaryDecoder = new import_msgpack.Decoder();
240
+ var defaultBinaryDecoder = new import_msgpack2.Decoder();
176
241
  function createBinaryEncoder() {
177
- return new import_msgpack.Encoder(
242
+ return new import_msgpack2.Encoder(
178
243
  void 0,
179
244
  void 0,
180
245
  void 0,
@@ -411,8 +476,9 @@ function splitCompactBinaryFrame(frame, registry, decoder = defaultBinaryDecoder
411
476
  }
412
477
 
413
478
  // src/rpc-connection.ts
414
- var import_msgpack2 = require("@msgpack/msgpack");
415
- var import_core = require("@actuallab/core");
479
+ var import_msgpack3 = require("@msgpack/msgpack");
480
+ var import_core2 = require("@actuallab/core");
481
+ var { warnLog: warnLog2 } = getLogs("RpcConnection");
416
482
  var WebSocketState = {
417
483
  CONNECTING: 0,
418
484
  OPEN: 1,
@@ -422,7 +488,7 @@ var WebSocketState = {
422
488
  var RpcWebSocketConnection = class {
423
489
  _ws;
424
490
  _sendBuffer = [];
425
- _connected = new import_core.PromiseSource();
491
+ _connected = new import_core2.PromiseSource();
426
492
  /** Per-connection msgpack encoder — reused across outbound messages to
427
493
  * amortise construction cost and to keep the internal write buffer at
428
494
  * its largest observed size (avoids repeated `resizeBuffer` growth on
@@ -430,7 +496,7 @@ var RpcWebSocketConnection = class {
430
496
  _encoder = createBinaryEncoder();
431
497
  /** Per-connection msgpack decoder — reused across inbound messages
432
498
  * rather than reconstructing one for every `decodeMulti` call. */
433
- _decoder = new import_msgpack2.Decoder();
499
+ _decoder = new import_msgpack3.Decoder();
434
500
  /** Exposed for callers who need to encode outbound binary messages
435
501
  * through this connection (e.g. `serializeBinaryMessage`). Safe to
436
502
  * pass to synchronous encode calls on the same event-loop turn. */
@@ -443,9 +509,9 @@ var RpcWebSocketConnection = class {
443
509
  _format;
444
510
  _methodRegistry;
445
511
  binaryMode;
446
- messageReceived = new import_core.EventHandlerSet();
447
- closed = new import_core.EventHandlerSet();
448
- error = new import_core.EventHandlerSet();
512
+ messageReceived = new import_core2.EventHandlerSet();
513
+ closed = new import_core2.EventHandlerSet();
514
+ error = new import_core2.EventHandlerSet();
449
515
  constructor(ws, binaryMode = false, format, methodRegistry) {
450
516
  this._ws = ws;
451
517
  this.binaryMode = binaryMode;
@@ -474,6 +540,7 @@ var RpcWebSocketConnection = class {
474
540
  });
475
541
  }
476
542
  } catch (e) {
543
+ warnLog2?.log("Failed to split binary frame:", e);
477
544
  this.error.trigger(e);
478
545
  }
479
546
  } else if (typeof Blob !== "undefined" && ev.data instanceof Blob && this.binaryMode) {
@@ -489,6 +556,7 @@ var RpcWebSocketConnection = class {
489
556
  });
490
557
  }
491
558
  } catch (e) {
559
+ warnLog2?.log("Failed to split binary blob frame:", e);
492
560
  this.error.trigger(e);
493
561
  }
494
562
  });
@@ -564,13 +632,13 @@ var RpcWebSocketConnection = class {
564
632
  };
565
633
 
566
634
  // src/rpc-message-channel-connection.ts
567
- var import_core2 = require("@actuallab/core");
635
+ var import_core3 = require("@actuallab/core");
568
636
  var RpcMessageChannelConnection = class {
569
637
  _port;
570
638
  _open = true;
571
639
  binaryMode = false;
572
- messageReceived = new import_core2.EventHandlerSet();
573
- closed = new import_core2.EventHandlerSet();
640
+ messageReceived = new import_core3.EventHandlerSet();
641
+ closed = new import_core3.EventHandlerSet();
574
642
  whenConnected = Promise.resolve();
575
643
  // immediately connected
576
644
  constructor(port) {
@@ -659,11 +727,26 @@ function defineRpcService(name, methods) {
659
727
  }
660
728
 
661
729
  // src/rpc-call-tracker.ts
662
- var import_core3 = require("@actuallab/core");
730
+ var import_core4 = require("@actuallab/core");
731
+
732
+ // src/rpc-call-stage.ts
733
+ var RpcCallStage = {
734
+ /** The call's result has been received from the peer. */
735
+ ResultReady: 1,
736
+ /** The compute call's result has been invalidated (compute-call specific). */
737
+ Invalidated: 3,
738
+ /**
739
+ * Bit flag added on top of ResultReady/Invalidated when the outbound
740
+ * call has been removed from the tracker (unregistered).
741
+ */
742
+ Unregistered: 4096
743
+ };
744
+
745
+ // src/rpc-call-tracker.ts
663
746
  var RpcOutboundCall = class {
664
747
  callId;
665
748
  method;
666
- result = new import_core3.PromiseSource();
749
+ result = new import_core4.PromiseSource();
667
750
  /** Bitfield of RpcRemoteExecutionMode flags controlling reconnect/resend behavior. */
668
751
  remoteExecutionMode;
669
752
  /** Whether to remove this call from the tracker on $sys.Ok. Default: true.
@@ -671,11 +754,35 @@ var RpcOutboundCall = class {
671
754
  removeOnOk = true;
672
755
  /** The serialized wire data — stored for re-sending on reconnect. */
673
756
  serializedWireData = "";
757
+ /**
758
+ * Bitfield of {@link RpcCallStage} flags indicating how far this call
759
+ * has progressed. Used by the `$sys.Reconnect` protocol to tell the
760
+ * remote peer which calls are still pending its attention.
761
+ *
762
+ * - 0: brand new, no result yet (the peer is expected to be actively processing).
763
+ * - `ResultReady` (1): `$sys.Ok` received for this call.
764
+ * - `Invalidated` (3): result was invalidated (compute calls only).
765
+ * - `Unregistered` (0x1000): removed from the tracker.
766
+ */
767
+ completedStage = 0;
674
768
  constructor(callId, method, remoteExecutionMode = 7) {
675
769
  this.callId = callId;
676
770
  this.method = method;
677
771
  this.remoteExecutionMode = remoteExecutionMode;
678
772
  }
773
+ /**
774
+ * Returns the stage to report to the remote peer via `$sys.Reconnect`,
775
+ * or `null` if this call should not be reconciled and must be aborted.
776
+ *
777
+ * Mirrors .NET `RpcOutboundCall.GetReconnectStage` at
778
+ * src/ActualLab.Rpc/Infrastructure/RpcOutboundCall.cs.
779
+ */
780
+ getReconnectStage(isPeerChanged) {
781
+ const mode = this.remoteExecutionMode;
782
+ if (!(mode & RpcRemoteExecutionMode.AllowReconnect)) return null;
783
+ if (isPeerChanged && !(mode & RpcRemoteExecutionMode.AllowResend)) return null;
784
+ return this.completedStage;
785
+ }
679
786
  /** Called when the connection is lost. Subclasses can override to resolve invalidation promises. */
680
787
  onDisconnect() {
681
788
  }
@@ -697,7 +804,10 @@ var RpcOutboundCallTracker = class {
697
804
  }
698
805
  remove(callId) {
699
806
  const call = this._calls.get(callId);
700
- if (call !== void 0) this._calls.delete(callId);
807
+ if (call !== void 0) {
808
+ this._calls.delete(callId);
809
+ call.completedStage |= RpcCallStage.Unregistered;
810
+ }
701
811
  return call;
702
812
  }
703
813
  values() {
@@ -760,15 +870,107 @@ var RpcInboundCallTracker = class {
760
870
  }
761
871
  };
762
872
 
873
+ // src/increasing-seq-compressor.ts
874
+ var IncreasingSeqCompressor = {
875
+ /**
876
+ * Serialize a non-decreasing sequence of non-negative integers as
877
+ * LEB128-encoded deltas.
878
+ *
879
+ * Throws `RangeError` if the input is not monotonically non-decreasing,
880
+ * or if a value exceeds `Number.MAX_SAFE_INTEGER`.
881
+ */
882
+ serialize(values) {
883
+ const deltas = [];
884
+ let last = 0;
885
+ let totalBytes = 0;
886
+ for (const value of values) {
887
+ if (!Number.isSafeInteger(value) || value < 0)
888
+ throw new RangeError(
889
+ `IncreasingSeqCompressor: value must be a non-negative safe integer (got ${value}).`
890
+ );
891
+ const delta = value - last;
892
+ if (delta < 0)
893
+ throw new RangeError(
894
+ `IncreasingSeqCompressor: sequence must be non-decreasing (saw ${value} after ${last}).`
895
+ );
896
+ deltas.push(delta);
897
+ last = value;
898
+ totalBytes += _varUintByteLength(delta);
899
+ }
900
+ const out = new Uint8Array(totalBytes);
901
+ let offset = 0;
902
+ for (const delta of deltas) {
903
+ offset = _writeVarUint(out, delta, offset);
904
+ }
905
+ return out;
906
+ },
907
+ /**
908
+ * Decode bytes produced by {@link serialize} back into the original
909
+ * sorted sequence of non-negative integers.
910
+ */
911
+ deserialize(data) {
912
+ const result = [];
913
+ let last = 0;
914
+ let offset = 0;
915
+ while (offset < data.length) {
916
+ const [delta, next] = _readVarUint(data, offset);
917
+ offset = next;
918
+ last += delta;
919
+ result.push(last);
920
+ }
921
+ return result;
922
+ }
923
+ };
924
+ var LOW_BITS = 127;
925
+ var HIGH_BIT = 128;
926
+ function _varUintByteLength(value) {
927
+ let n = 0;
928
+ let v = value;
929
+ while (v >= HIGH_BIT) {
930
+ n++;
931
+ v = Math.floor(v / 128);
932
+ }
933
+ return n + 1;
934
+ }
935
+ function _writeVarUint(out, value, offset) {
936
+ let v = value;
937
+ while (v >= HIGH_BIT) {
938
+ out[offset++] = (HIGH_BIT | v & LOW_BITS) & 255;
939
+ v = Math.floor(v / 128);
940
+ }
941
+ out[offset++] = v & 255;
942
+ return offset;
943
+ }
944
+ function _readVarUint(data, offset) {
945
+ let value = 0;
946
+ let shift = 0;
947
+ while (shift < 63) {
948
+ if (offset >= data.length)
949
+ throw new RangeError("IncreasingSeqCompressor: truncated VarUInt.");
950
+ const b2 = data[offset++];
951
+ value += (b2 & LOW_BITS) * Math.pow(2, shift);
952
+ if (b2 <= LOW_BITS) return [value, offset];
953
+ shift += 7;
954
+ }
955
+ if (offset >= data.length)
956
+ throw new RangeError("IncreasingSeqCompressor: truncated VarUInt.");
957
+ const b = data[offset++];
958
+ if (b > 1)
959
+ throw new RangeError("IncreasingSeqCompressor: VarUInt value exceeds 64 bits.");
960
+ value += b * Math.pow(2, 63);
961
+ return [value, offset];
962
+ }
963
+
763
964
  // src/rpc-stream.ts
764
965
  var import_denque = __toESM(require("denque"), 1);
765
- var import_core5 = require("@actuallab/core");
966
+ var import_core6 = require("@actuallab/core");
766
967
 
767
968
  // src/rpc-object.ts
768
969
  var RpcObjectKind = { Local: 0, Remote: 1 };
769
970
 
770
971
  // src/rpc-stream-sender.ts
771
- var import_core4 = require("@actuallab/core");
972
+ var import_core5 = require("@actuallab/core");
973
+ var { warnLog: warnLog3 } = getLogs("RpcSharedStream");
772
974
  var DEFAULT_ACK_PERIOD = 256;
773
975
  var DEFAULT_ACK_ADVANCE = 128;
774
976
  var RpcStreamSender = class _RpcStreamSender {
@@ -781,15 +983,29 @@ var RpcStreamSender = class _RpcStreamSender {
781
983
  peer;
782
984
  ackPeriod;
783
985
  ackAdvance;
986
+ /**
987
+ * If true, the source was produced by a factory `(abortSignal) => AsyncIterable<T>`
988
+ * and may honor the AbortSignal to exit gracefully. `disconnect()` first aborts
989
+ * the signal and gives the source up to `RpcStream.disconnectGracePeriodMs` ms
990
+ * before force-closing via `iterator.return()`.
991
+ *
992
+ * If false, the source is a plain AsyncIterable that cannot observe the signal,
993
+ * so `disconnect()` force-closes immediately.
994
+ */
995
+ sourceUsesAbortSignal;
996
+ // -- Pump state --
997
+ /** Next stream index that will be sent (matches .NET's `index`). */
784
998
  _nextIndex = 0;
785
999
  _ended = false;
786
- _started = new import_core4.PromiseSource();
1000
+ _started = new import_core5.PromiseSource();
1001
+ /** Highest `nextIndex` observed in an ACK so far (matches .NET's `lastAckNextIndex` idiom). */
787
1002
  _lastAckedIndex = 0;
788
- _ackWaiting = null;
789
- _resetRequested = false;
790
1003
  _abortController = new AbortController();
791
1004
  _iterator = null;
792
- constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true, isRealTime = false, canSkipTo = () => true) {
1005
+ // -- ACK queue (analog of .NET's Channel<(long, bool)>) --
1006
+ _acks = [];
1007
+ _whenAckReady = null;
1008
+ constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true, isRealTime = false, canSkipTo = () => true, sourceUsesAbortSignal = false) {
793
1009
  const localId = peer.sharedObjects.nextId();
794
1010
  this.id = { hostId: peer.hub.hubId, localId };
795
1011
  this.allowReconnect = allowReconnect;
@@ -798,6 +1014,7 @@ var RpcStreamSender = class _RpcStreamSender {
798
1014
  this.peer = peer;
799
1015
  this.ackPeriod = ackPeriod;
800
1016
  this.ackAdvance = ackAdvance;
1017
+ this.sourceUsesAbortSignal = sourceUsesAbortSignal;
801
1018
  }
802
1019
  /** AbortSignal that is aborted when the sender is disconnected. */
803
1020
  get abortSignal() {
@@ -807,56 +1024,61 @@ var RpcStreamSender = class _RpcStreamSender {
807
1024
  toRef() {
808
1025
  return `${this.id.hostId},${this.id.localId},${this.ackPeriod},${this.ackAdvance},${this.allowReconnect ? "1" : "0"},${this.isRealTime ? "1" : "0"}`;
809
1026
  }
810
- /** Called by system call handler when $sys.Ack is received from the client. */
1027
+ /**
1028
+ * Called by system call handler when $sys.Ack is received from the client.
1029
+ *
1030
+ * Mirrors .NET `RpcSharedStream<T>.OnAck` at
1031
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:68-101.
1032
+ */
811
1033
  onAck(nextIndex, hostId) {
812
1034
  const mustReset = hostId !== "" && hostId !== _RpcStreamSender._emptyGuid;
813
- if (!this._started.isCompleted) {
814
- this._started.resolve();
815
- }
816
- if (mustReset && this.isRealTime) {
817
- this._nextIndex = nextIndex;
818
- this._lastAckedIndex = nextIndex;
819
- this._resetRequested = true;
820
- } else {
821
- if (mustReset || nextIndex > this._lastAckedIndex) {
822
- this._lastAckedIndex = nextIndex;
823
- }
824
- }
825
- if (this._ackWaiting) {
826
- this._ackWaiting.resolve();
827
- this._ackWaiting = null;
1035
+ if (!this._started.isCompleted) this._started.resolve();
1036
+ this._acks.push({ nextIndex, mustReset });
1037
+ if (this._whenAckReady) {
1038
+ this._whenAckReady.resolve();
1039
+ this._whenAckReady = null;
828
1040
  }
829
1041
  }
830
1042
  /** Called by system call handler when $sys.AckEnd is received from the client. */
831
1043
  onAckEnd(_hostId) {
832
1044
  this.disconnect();
833
1045
  }
834
- /** Send a single item to the client. */
1046
+ /**
1047
+ * Send a single item to the client at the current `_nextIndex`.
1048
+ *
1049
+ * Advances `_nextIndex` unconditionally — even if the peer is disconnected
1050
+ * the item is considered "sent" from the pump's perspective so the replay
1051
+ * buffer indexing stays consistent. If a connection is absent, the wire
1052
+ * message is dropped; the item will be resent from the replay buffer
1053
+ * after a reconnect (client sends `Ack(N, mustReset=true)`).
1054
+ */
835
1055
  sendItem(item) {
836
1056
  if (this._ended) return;
837
1057
  const conn = this.peer.connection;
838
- if (!conn) return;
839
- this.peer.hub.systemCallSender.item(
840
- conn,
841
- this.peer.format,
842
- this.id.localId,
843
- this._nextIndex,
844
- item
845
- );
1058
+ if (conn) {
1059
+ this.peer.hub.systemCallSender.item(
1060
+ conn,
1061
+ this.peer.format,
1062
+ this.id.localId,
1063
+ this._nextIndex,
1064
+ item
1065
+ );
1066
+ }
846
1067
  this._nextIndex++;
847
1068
  }
848
- /** Send a batch of items to the client. */
1069
+ /** Send a batch of items to the client. See {@link sendItem} for disconnect semantics. */
849
1070
  sendBatch(items) {
850
1071
  if (this._ended || items.length === 0) return;
851
1072
  const conn = this.peer.connection;
852
- if (!conn) return;
853
- this.peer.hub.systemCallSender.batch(
854
- conn,
855
- this.peer.format,
856
- this.id.localId,
857
- this._nextIndex,
858
- items
859
- );
1073
+ if (conn) {
1074
+ this.peer.hub.systemCallSender.batch(
1075
+ conn,
1076
+ this.peer.format,
1077
+ this.id.localId,
1078
+ this._nextIndex,
1079
+ items
1080
+ );
1081
+ }
860
1082
  this._nextIndex += items.length;
861
1083
  }
862
1084
  /** Signal stream completion to the client. */
@@ -876,76 +1098,274 @@ var RpcStreamSender = class _RpcStreamSender {
876
1098
  }
877
1099
  /**
878
1100
  * Consume an AsyncIterable and send all items to the client.
879
- * Waits for the client's initial Ack before starting to pump items.
880
1101
  *
881
- * When `isRealTime` is true, applies flow control based on `ackAdvance`:
882
- * if the sender is `ackAdvance` items ahead of the last ACK, it enters
883
- * skip mode — draining the source and discarding items until it finds
884
- * one where `canSkipTo` returns true.
1102
+ * Dispatches to the non-real-time or real-time pump, matching .NET
1103
+ * `RpcSharedStream<T>.OnRun` at
1104
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:105-108.
885
1105
  */
886
1106
  async writeFrom(source) {
887
1107
  await this._started.promise;
1108
+ if (this._ended) return;
888
1109
  const iterator = source[Symbol.asyncIterator]();
889
1110
  this._iterator = iterator;
1111
+ const state = { iteratorDone: false };
890
1112
  try {
891
- for (; ; ) {
892
- const next = await iterator.next();
893
- if (next.done || this._ended) break;
894
- const item = next.value;
895
- if (this._resetRequested) {
896
- if (!this.canSkipTo(item)) continue;
897
- this._resetRequested = false;
898
- this.sendItem(item);
899
- continue;
1113
+ if (this.isRealTime) {
1114
+ await this._runRealTime(iterator, state);
1115
+ } else {
1116
+ await this._runNonRealTime(iterator, state);
1117
+ }
1118
+ } finally {
1119
+ if (state.iteratorDone && this._iterator === iterator) {
1120
+ this._iterator = null;
1121
+ }
1122
+ }
1123
+ }
1124
+ /**
1125
+ * Non-real-time pump — back-pressure through buffered ACKs.
1126
+ *
1127
+ * Direct port of .NET `OnRunNonRealTime` at
1128
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:110-217.
1129
+ */
1130
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition -- _ended changes across awaits */
1131
+ async _runNonRealTime(iterator, state) {
1132
+ const buffer = new import_core5.RingBuffer(this.ackAdvance + 1);
1133
+ let bufferStart = 0;
1134
+ let isFullyBuffered = false;
1135
+ while (true) {
1136
+ let ack = this._tryProcessAcks();
1137
+ if (!ack) {
1138
+ await this._waitAckReady();
1139
+ if (this._ended) return;
1140
+ ack = this._tryProcessAcks();
1141
+ if (!ack) {
1142
+ warnLog3?.log("Something is off: couldn't read an acknowledgement");
1143
+ return;
900
1144
  }
901
- if (this._nextIndex < this._lastAckedIndex + this.ackAdvance) {
902
- this.sendItem(item);
1145
+ }
1146
+ this._lastAckedIndex = ack.nextIndex;
1147
+ const shift = _clamp(ack.nextIndex - bufferStart, 0, buffer.count);
1148
+ if (shift > 0) {
1149
+ buffer.moveHead(shift);
1150
+ bufferStart += shift;
1151
+ }
1152
+ if (this._nextIndex < bufferStart) {
1153
+ if (!this._ended)
1154
+ this.sendEnd(new Error("Stream position unavailable."));
1155
+ return;
1156
+ }
1157
+ let bufferIndex = this._nextIndex - bufferStart;
1158
+ const maxIndex = ack.nextIndex + this.ackAdvance;
1159
+ while (this._nextIndex < maxIndex) {
1160
+ if (this._ended) return;
1161
+ if (bufferIndex < buffer.count) {
1162
+ const item = buffer.get(bufferIndex++);
1163
+ if (item.kind === "value") {
1164
+ this.sendItem(item.value);
1165
+ } else if (item.kind === "end") {
1166
+ if (!this._ended) this.sendEnd();
1167
+ return;
1168
+ } else {
1169
+ if (!this._ended) this.sendEnd(item.error);
1170
+ return;
1171
+ }
903
1172
  continue;
904
1173
  }
905
- if (!this.isRealTime) {
906
- await this._waitForAckBudget();
1174
+ if (isFullyBuffered) break;
1175
+ if (!buffer.hasRemainingCapacity) break;
1176
+ let pulled;
1177
+ try {
1178
+ const r = await iterator.next();
907
1179
  if (this._ended) return;
908
- this.sendItem(item);
909
- continue;
1180
+ if (r.done) {
1181
+ pulled = _endItem;
1182
+ isFullyBuffered = true;
1183
+ state.iteratorDone = true;
1184
+ } else {
1185
+ pulled = { kind: "value", value: r.value };
1186
+ }
1187
+ } catch (e) {
1188
+ state.iteratorDone = true;
1189
+ isFullyBuffered = true;
1190
+ pulled = {
1191
+ kind: "error",
1192
+ error: e instanceof Error ? e : new Error(String(e))
1193
+ };
910
1194
  }
911
- let latestSkipTarget = this.canSkipTo(item) ? item : void 0;
912
- let sourceExhausted = false;
913
- while (this._nextIndex >= this._lastAckedIndex + this.ackAdvance) {
1195
+ buffer.pushTail(pulled);
1196
+ }
1197
+ }
1198
+ }
1199
+ /**
1200
+ * Real-time pump — drops stale items to stay close to the source's head.
1201
+ *
1202
+ * Direct port of .NET `OnRunRealTime` at
1203
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:219-420.
1204
+ */
1205
+ async _runRealTime(iterator, state) {
1206
+ const buffer = new import_core5.RingBuffer(this.ackAdvance + 1);
1207
+ let bufferStart = 0;
1208
+ let isFullyBuffered = false;
1209
+ while (true) {
1210
+ let ack = this._tryProcessAcks();
1211
+ if (!ack) {
1212
+ await this._waitAckReady();
1213
+ if (this._ended) return;
1214
+ ack = this._tryProcessAcks();
1215
+ if (!ack) {
1216
+ warnLog3?.log("Something is off: couldn't read an acknowledgement");
1217
+ return;
1218
+ }
1219
+ }
1220
+ this._lastAckedIndex = ack.nextIndex;
1221
+ if (ack.mustReset && !isFullyBuffered) {
1222
+ buffer.clear();
1223
+ bufferStart = this._nextIndex;
1224
+ while (true) {
914
1225
  if (this._ended) return;
915
- const n = await iterator.next();
916
- if (n.done) {
917
- sourceExhausted = true;
918
- break;
919
- }
920
- if (this.canSkipTo(n.value)) {
921
- latestSkipTarget = n.value;
1226
+ let item;
1227
+ let accepted = false;
1228
+ try {
1229
+ const r = await iterator.next();
1230
+ if (this._ended) return;
1231
+ if (r.done) {
1232
+ item = _endItem;
1233
+ isFullyBuffered = true;
1234
+ state.iteratorDone = true;
1235
+ } else if (this.canSkipTo(r.value)) {
1236
+ item = { kind: "value", value: r.value };
1237
+ accepted = true;
1238
+ } else {
1239
+ continue;
1240
+ }
1241
+ } catch (e) {
1242
+ state.iteratorDone = true;
1243
+ isFullyBuffered = true;
1244
+ item = {
1245
+ kind: "error",
1246
+ error: e instanceof Error ? e : new Error(String(e))
1247
+ };
922
1248
  }
1249
+ buffer.pushTail(item);
1250
+ if (accepted || isFullyBuffered) break;
923
1251
  }
1252
+ }
1253
+ const shift = _clamp(ack.nextIndex - bufferStart, 0, buffer.count);
1254
+ if (shift > 0) {
1255
+ buffer.moveHead(shift);
1256
+ bufferStart += shift;
1257
+ }
1258
+ if (this._nextIndex < bufferStart) {
1259
+ if (!this._ended)
1260
+ this.sendEnd(new Error("Stream position unavailable."));
1261
+ return;
1262
+ }
1263
+ let bufferIndex = this._nextIndex - bufferStart;
1264
+ const maxIndex = ack.nextIndex + this.ackAdvance;
1265
+ while (this._nextIndex < maxIndex) {
924
1266
  if (this._ended) return;
925
- if (latestSkipTarget !== void 0) {
926
- this.sendItem(latestSkipTarget);
1267
+ if (this._acks.length > 0) break;
1268
+ if (bufferIndex < buffer.count) {
1269
+ const item = buffer.get(bufferIndex++);
1270
+ if (item.kind === "value") {
1271
+ this.sendItem(item.value);
1272
+ } else if (item.kind === "end") {
1273
+ if (!this._ended) this.sendEnd();
1274
+ return;
1275
+ } else {
1276
+ if (!this._ended) this.sendEnd(item.error);
1277
+ return;
1278
+ }
1279
+ continue;
927
1280
  }
928
- if (sourceExhausted) {
929
- if (!this._ended) this.sendEnd();
930
- return;
1281
+ if (isFullyBuffered) break;
1282
+ if (!buffer.hasRemainingCapacity) break;
1283
+ let pulled;
1284
+ try {
1285
+ const r = await iterator.next();
1286
+ if (this._ended) return;
1287
+ if (r.done) {
1288
+ pulled = _endItem;
1289
+ isFullyBuffered = true;
1290
+ state.iteratorDone = true;
1291
+ } else {
1292
+ pulled = { kind: "value", value: r.value };
1293
+ }
1294
+ } catch (e) {
1295
+ state.iteratorDone = true;
1296
+ isFullyBuffered = true;
1297
+ pulled = {
1298
+ kind: "error",
1299
+ error: e instanceof Error ? e : new Error(String(e))
1300
+ };
931
1301
  }
1302
+ buffer.pushTail(pulled);
932
1303
  }
933
- if (!this._ended) {
934
- this.sendEnd();
935
- }
936
- } catch (e) {
937
- if (!this._ended) {
938
- this.sendEnd(e instanceof Error ? e : new Error(String(e)));
1304
+ if (this._nextIndex >= maxIndex && !isFullyBuffered && !this._ended) {
1305
+ let latestValue;
1306
+ let haveLatest = false;
1307
+ let exitReason = "ack";
1308
+ let exitError = null;
1309
+ drain: while (!this._ended) {
1310
+ if (this._acks.length > 0) {
1311
+ exitReason = "ack";
1312
+ break drain;
1313
+ }
1314
+ let r;
1315
+ try {
1316
+ r = await iterator.next();
1317
+ } catch (e) {
1318
+ state.iteratorDone = true;
1319
+ isFullyBuffered = true;
1320
+ exitReason = "error";
1321
+ exitError = e instanceof Error ? e : new Error(String(e));
1322
+ break drain;
1323
+ }
1324
+ if (this._ended) return;
1325
+ if (r.done) {
1326
+ state.iteratorDone = true;
1327
+ isFullyBuffered = true;
1328
+ exitReason = "end";
1329
+ break drain;
1330
+ }
1331
+ if (this.canSkipTo(r.value)) {
1332
+ latestValue = r.value;
1333
+ haveLatest = true;
1334
+ }
1335
+ }
1336
+ if (exitReason === "end" || exitReason === "error") {
1337
+ if (haveLatest && !this._ended) this.sendItem(latestValue);
1338
+ if (!this._ended) this.sendEnd(exitError);
1339
+ return;
1340
+ }
1341
+ if (haveLatest) {
1342
+ const wasFull = buffer.isFull;
1343
+ buffer.pushTailAndMoveHeadIfFull({ kind: "value", value: latestValue });
1344
+ if (wasFull) bufferStart++;
1345
+ }
939
1346
  }
940
1347
  }
941
1348
  }
942
- /** Wait until _nextIndex < _lastAckedIndex + ackAdvance. */
943
- async _waitForAckBudget() {
944
- while (this._nextIndex >= this._lastAckedIndex + this.ackAdvance) {
945
- if (this._ended) return;
946
- this._ackWaiting = new import_core4.PromiseSource();
947
- await this._ackWaiting.promise;
1349
+ /* eslint-enable @typescript-eslint/no-unnecessary-condition */
1350
+ /** Drain all queued ACKs, returning the most recent one (or null if none). */
1351
+ _tryProcessAcks() {
1352
+ if (this._acks.length === 0) return null;
1353
+ let last = null;
1354
+ while (this._acks.length > 0) {
1355
+ const a = this._acks.shift();
1356
+ last = a;
1357
+ if (a.mustReset || this._nextIndex < a.nextIndex) {
1358
+ this._nextIndex = a.nextIndex;
1359
+ }
948
1360
  }
1361
+ return last;
1362
+ }
1363
+ /** Wait for the ACK queue to have at least one entry (or `_ended`). */
1364
+ async _waitAckReady() {
1365
+ if (this._acks.length > 0 || this._ended) return;
1366
+ this._whenAckReady = new import_core5.PromiseSource();
1367
+ await this._whenAckReady.promise;
1368
+ this._whenAckReady = null;
949
1369
  }
950
1370
  // -- IRpcObject --
951
1371
  reconnect() {
@@ -957,19 +1377,41 @@ var RpcStreamSender = class _RpcStreamSender {
957
1377
  if (!this._started.isCompleted) {
958
1378
  this._started.resolve();
959
1379
  }
960
- if (this._ackWaiting) {
961
- this._ackWaiting.resolve();
962
- this._ackWaiting = null;
1380
+ if (this._whenAckReady) {
1381
+ this._whenAckReady.resolve();
1382
+ this._whenAckReady = null;
963
1383
  }
964
- if (this._iterator) {
965
- void this._iterator.return?.(void 0);
1384
+ this.peer.sharedObjects.unregister(this);
1385
+ const iterator = this._iterator;
1386
+ if (!iterator) return;
1387
+ if (!this.sourceUsesAbortSignal) {
966
1388
  this._iterator = null;
1389
+ this._forceCloseIterator(iterator);
1390
+ return;
1391
+ }
1392
+ setTimeout(() => {
1393
+ if (this._iterator === iterator) {
1394
+ this._iterator = null;
1395
+ this._forceCloseIterator(iterator);
1396
+ }
1397
+ }, RpcStream.disconnectGracePeriodMs);
1398
+ }
1399
+ _forceCloseIterator(iterator) {
1400
+ try {
1401
+ const ret = iterator.return?.(void 0);
1402
+ if (ret) void ret.catch(() => {
1403
+ });
1404
+ } catch {
967
1405
  }
968
- this.peer.sharedObjects.unregister(this);
969
1406
  }
970
1407
  };
1408
+ var _endItem = { kind: "end" };
1409
+ function _clamp(value, min, max) {
1410
+ return value < min ? min : value > max ? max : value;
1411
+ }
971
1412
 
972
1413
  // src/rpc-stream.ts
1414
+ var { warnLog: warnLog4 } = getLogs("RpcStream");
973
1415
  var DEFAULT_ACK_PERIOD2 = 30;
974
1416
  var DEFAULT_ACK_ADVANCE2 = 61;
975
1417
  function parseStreamRef(value) {
@@ -1003,6 +1445,15 @@ function parseStreamRef(value) {
1003
1445
  return null;
1004
1446
  }
1005
1447
  var RpcStream = class _RpcStream {
1448
+ /**
1449
+ * Grace period (ms) given to a factory-form local source to honor its
1450
+ * AbortSignal and exit naturally before the sender force-closes the
1451
+ * iterator via `iterator.return()`.
1452
+ *
1453
+ * Only applies to sources provided as `(abortSignal) => AsyncIterable<T>`.
1454
+ * Plain `AsyncIterable<T>` sources are force-closed immediately on disconnect.
1455
+ */
1456
+ static disconnectGracePeriodMs = 100;
1006
1457
  id;
1007
1458
  kind;
1008
1459
  allowReconnect;
@@ -1080,17 +1531,19 @@ var RpcStream = class _RpcStream {
1080
1531
  throw new Error("toRef() was already called with a different peer.");
1081
1532
  return this._toRefValue;
1082
1533
  }
1534
+ const isFactory = typeof this.localSource === "function";
1083
1535
  const sender = new RpcStreamSender(
1084
1536
  peer,
1085
1537
  this.ackPeriod,
1086
1538
  this.ackAdvance,
1087
1539
  this.allowReconnect,
1088
1540
  this.isRealTime,
1089
- this.canSkipTo
1541
+ this.canSkipTo,
1542
+ isFactory
1090
1543
  );
1091
1544
  peer.sharedObjects.register(sender);
1092
1545
  this._sender = sender;
1093
- const source = typeof this.localSource === "function" ? this.localSource(sender.abortSignal) : this.localSource;
1546
+ const source = isFactory ? this.localSource(sender.abortSignal) : this.localSource;
1094
1547
  this._whenSent = sender.writeFrom(source);
1095
1548
  this._toRefValue = peer.format.isBinary ? {
1096
1549
  SerializedId: [sender.id.hostId, sender.id.localId],
@@ -1104,9 +1557,7 @@ var RpcStream = class _RpcStream {
1104
1557
  onItem(index, item) {
1105
1558
  if (this._disposed || this._completed) return;
1106
1559
  if (index > this._nextExpectedIndex) {
1107
- console.warn(
1108
- `[RpcStream] item index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`
1109
- );
1560
+ warnLog4?.log(`item index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
1110
1561
  if (!this.allowReconnect) {
1111
1562
  this._completed = true;
1112
1563
  this._completionError = new Error(
@@ -1131,9 +1582,7 @@ var RpcStream = class _RpcStream {
1131
1582
  onBatch(index, items) {
1132
1583
  if (this._disposed || this._completed) return;
1133
1584
  if (index > this._nextExpectedIndex) {
1134
- console.warn(
1135
- `[RpcStream] batch index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`
1136
- );
1585
+ warnLog4?.log(`batch index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
1137
1586
  if (!this.allowReconnect) {
1138
1587
  this._completed = true;
1139
1588
  this._completionError = new Error(
@@ -1204,7 +1653,7 @@ var RpcStream = class _RpcStream {
1204
1653
  if (this._completionError) throw this._completionError;
1205
1654
  return { value: void 0, done: true };
1206
1655
  }
1207
- this._consumerWaiting = new import_core5.PromiseSource();
1656
+ this._consumerWaiting = new import_core6.PromiseSource();
1208
1657
  await this._consumerWaiting.promise;
1209
1658
  }
1210
1659
  },
@@ -1300,15 +1749,37 @@ function resolveStreamRefs(value, peer) {
1300
1749
  return value;
1301
1750
  }
1302
1751
 
1752
+ // src/base64.ts
1753
+ function base64Encode(bytes) {
1754
+ const chunkSize = 32768;
1755
+ let binary = "";
1756
+ for (let i = 0; i < bytes.length; i += chunkSize) {
1757
+ binary += String.fromCharCode.apply(
1758
+ null,
1759
+ bytes.subarray(i, Math.min(bytes.length, i + chunkSize))
1760
+ );
1761
+ }
1762
+ return btoa(binary);
1763
+ }
1764
+ function base64Decode(encoded) {
1765
+ const binary = atob(encoded);
1766
+ const out = new Uint8Array(binary.length);
1767
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
1768
+ return out;
1769
+ }
1770
+
1303
1771
  // src/rpc-system-call-handler.ts
1772
+ var { debugLog, warnLog: warnLog5 } = getLogs("RpcSystemCallHandler");
1304
1773
  var RpcSystemCallHandler = class {
1305
1774
  handle(message, args, peer) {
1306
1775
  const method = message.Method;
1307
1776
  const relatedId = message.RelatedId ?? 0;
1777
+ debugLog?.log(`'${peer.ref}': handle ${method}#${relatedId}`);
1308
1778
  switch (method) {
1309
1779
  case RpcSystemCalls.ok: {
1310
1780
  const call = peer.outbound.get(relatedId);
1311
1781
  if (call !== void 0) {
1782
+ call.completedStage |= RpcCallStage.ResultReady;
1312
1783
  if (call.removeOnOk) {
1313
1784
  peer.outbound.remove(relatedId);
1314
1785
  }
@@ -1316,11 +1787,21 @@ var RpcSystemCallHandler = class {
1316
1787
  }
1317
1788
  break;
1318
1789
  }
1790
+ case RpcSystemCalls.reconnect: {
1791
+ this._handleReconnect(relatedId, args, peer);
1792
+ break;
1793
+ }
1319
1794
  case RpcSystemCalls.error: {
1320
1795
  const call = peer.outbound.remove(relatedId);
1321
1796
  if (call !== void 0) {
1322
1797
  const errorInfo = args[0];
1323
1798
  const msg = errorInfo?.Message ?? errorInfo?.message ?? "RPC error";
1799
+ const errorType = errorInfo?.TypeRef ?? errorInfo?.typeRef;
1800
+ if (errorType !== null && typeof errorType === "object") {
1801
+ const t = errorType;
1802
+ if (t.TypeName === "RpcRerouteException" || t.typeName === "RpcRerouteException")
1803
+ warnLog5?.log("Got RpcRerouteException from remote peer:", msg);
1804
+ }
1324
1805
  call.result.reject(new Error(msg));
1325
1806
  }
1326
1807
  break;
@@ -1334,25 +1815,20 @@ var RpcSystemCallHandler = class {
1334
1815
  }
1335
1816
  case RpcSystemCalls.item: {
1336
1817
  const stream = peer.remoteObjects.get(relatedId);
1337
- if (!stream) {
1338
- console.warn(
1339
- `[RpcSysHandler] $sys.I: no stream for relatedId=${relatedId}`
1340
- );
1341
- } else {
1818
+ if (!stream)
1819
+ debugLog?.log(`$sys.I: no stream for relatedId=${relatedId}`);
1820
+ else
1342
1821
  stream.onItem(
1343
1822
  args[0],
1344
1823
  resolveStreamRefs(args[1], peer)
1345
1824
  );
1346
- }
1347
1825
  break;
1348
1826
  }
1349
1827
  case RpcSystemCalls.batch: {
1350
1828
  const stream = peer.remoteObjects.get(relatedId);
1351
- if (!stream) {
1352
- console.warn(
1353
- `[RpcSysHandler] $sys.B: no stream for relatedId=${relatedId}`
1354
- );
1355
- } else if (Array.isArray(args[1])) {
1829
+ if (!stream)
1830
+ debugLog?.log(`$sys.B: no stream for relatedId=${relatedId}`);
1831
+ else if (Array.isArray(args[1])) {
1356
1832
  const items = args[1];
1357
1833
  for (let i = 0; i < items.length; i++)
1358
1834
  items[i] = resolveStreamRefs(items[i], peer);
@@ -1363,9 +1839,7 @@ var RpcSystemCallHandler = class {
1363
1839
  case RpcSystemCalls.end: {
1364
1840
  const stream = peer.remoteObjects.get(relatedId);
1365
1841
  if (!stream) {
1366
- console.warn(
1367
- `[RpcSysHandler] $sys.End: no stream for relatedId=${relatedId}`
1368
- );
1842
+ debugLog?.log(`$sys.End: no stream for relatedId=${relatedId}`);
1369
1843
  } else {
1370
1844
  const errorInfo = args[1];
1371
1845
  const msg = errorInfo?.Message ?? errorInfo?.message;
@@ -1404,9 +1878,57 @@ var RpcSystemCallHandler = class {
1404
1878
  }
1405
1879
  }
1406
1880
  }
1881
+ /**
1882
+ * Server-side handler for the `$sys.Reconnect` call. Parses the peer's
1883
+ * report of in-flight call IDs (grouped by stage, IncreasingSeqCompressor-
1884
+ * encoded, base64-wrapped for JSON transport), computes the subset this
1885
+ * peer does not recognize, and replies with a compressed byte[] of the
1886
+ * unknown IDs wrapped in `$sys.Ok`.
1887
+ *
1888
+ * @param relatedId The outbound call ID the client used for this
1889
+ * `$sys.Reconnect` invocation — we send `$sys.Ok` back
1890
+ * with the same `RelatedId`.
1891
+ */
1892
+ _handleReconnect(relatedId, args, peer) {
1893
+ if (peer.connection === void 0) return;
1894
+ let unknownIds = [];
1895
+ try {
1896
+ const stagesRaw = args[1];
1897
+ if (stagesRaw && typeof stagesRaw === "object" && !Array.isArray(stagesRaw)) {
1898
+ const unknownSet = /* @__PURE__ */ new Set();
1899
+ for (const [, rawValue] of Object.entries(stagesRaw)) {
1900
+ const bytes = _toBytes(rawValue);
1901
+ if (bytes === null) continue;
1902
+ const callIds = IncreasingSeqCompressor.deserialize(bytes);
1903
+ for (const callId of callIds) {
1904
+ if (peer.inbound.get(callId) === void 0) {
1905
+ unknownSet.add(callId);
1906
+ }
1907
+ }
1908
+ }
1909
+ unknownIds = [...unknownSet].sort((a, b) => a - b);
1910
+ }
1911
+ } catch {
1912
+ }
1913
+ const responseBytes = IncreasingSeqCompressor.serialize(unknownIds);
1914
+ const responseValue = peer.format.isBinary ? responseBytes : base64Encode(responseBytes);
1915
+ peer.hub.systemCallSender.ok(peer.connection, peer.format, relatedId, responseValue);
1916
+ }
1407
1917
  };
1918
+ function _toBytes(value) {
1919
+ if (value instanceof Uint8Array) return value;
1920
+ if (typeof value === "string") {
1921
+ try {
1922
+ return base64Decode(value);
1923
+ } catch {
1924
+ return null;
1925
+ }
1926
+ }
1927
+ return null;
1928
+ }
1408
1929
 
1409
1930
  // src/rpc-system-call-sender.ts
1931
+ var { errorLog } = getLogs("RpcSystemCallSender");
1410
1932
  var RpcSystemCallSender = class _RpcSystemCallSender {
1411
1933
  // Reusable envelopes — one per hot-path method.
1412
1934
  _itemEnv = {
@@ -1463,12 +1985,17 @@ var RpcSystemCallSender = class _RpcSystemCallSender {
1463
1985
  ]);
1464
1986
  }
1465
1987
  ok(conn, format, relatedId, result) {
1466
- this._send(
1467
- conn,
1468
- format,
1469
- { Method: RpcSystemCalls.ok, RelatedId: relatedId },
1470
- [result]
1471
- );
1988
+ try {
1989
+ this._send(
1990
+ conn,
1991
+ format,
1992
+ { Method: RpcSystemCalls.ok, RelatedId: relatedId },
1993
+ [result]
1994
+ );
1995
+ } catch (e) {
1996
+ errorLog?.log(`Failed to send Ok response for call #${relatedId}`, e);
1997
+ this.error(conn, format, relatedId, e);
1998
+ }
1472
1999
  }
1473
2000
  error(conn, format, relatedId, error) {
1474
2001
  const message = error instanceof Error ? error.message : String(error);
@@ -1580,19 +2107,19 @@ var RpcSharedObjectTracker = class {
1580
2107
  };
1581
2108
 
1582
2109
  // src/rpc-peer.ts
1583
- var import_core7 = require("@actuallab/core");
2110
+ var import_core8 = require("@actuallab/core");
1584
2111
 
1585
2112
  // src/rpc-client-peer-reconnect-delayer.ts
1586
- var import_core6 = require("@actuallab/core");
1587
- var RpcClientPeerReconnectDelayer = class extends import_core6.RetryDelayer {
2113
+ var import_core7 = require("@actuallab/core");
2114
+ var RpcClientPeerReconnectDelayer = class extends import_core7.RetryDelayer {
1588
2115
  constructor() {
1589
2116
  super();
1590
- this.delays = import_core6.RetryDelaySeq.exp(1e3, 6e4);
2117
+ this.delays = import_core7.RetryDelaySeq.exp(1e3, 6e4);
1591
2118
  }
1592
2119
  };
1593
2120
 
1594
2121
  // src/rpc-serialization-format.ts
1595
- var import_msgpack3 = require("@msgpack/msgpack");
2122
+ var import_msgpack4 = require("@msgpack/msgpack");
1596
2123
  var RpcSerializationFormat = class {
1597
2124
  /** Create a reusable binary encoder for this format. Returns undefined for text formats. */
1598
2125
  createEncoder() {
@@ -1666,7 +2193,7 @@ var RpcMessagePackSerializationFormat = class extends RpcSerializationFormat {
1666
2193
  return createBinaryEncoder();
1667
2194
  }
1668
2195
  createDecoder() {
1669
- return new import_msgpack3.Decoder();
2196
+ return new import_msgpack4.Decoder();
1670
2197
  }
1671
2198
  };
1672
2199
  var RpcMessagePackCompactSerializationFormat = class extends RpcSerializationFormat {
@@ -1698,7 +2225,7 @@ var RpcMessagePackCompactSerializationFormat = class extends RpcSerializationFor
1698
2225
  return createBinaryEncoder();
1699
2226
  }
1700
2227
  createDecoder() {
1701
- return new import_msgpack3.Decoder();
2228
+ return new import_msgpack4.Decoder();
1702
2229
  }
1703
2230
  };
1704
2231
  RpcSerializationFormat.SystemJsonV5 = new RpcJsonSerializationFormat("json5");
@@ -1750,6 +2277,7 @@ var RpcSerializationFormatResolver = class _RpcSerializationFormatResolver {
1750
2277
 
1751
2278
  // src/rpc-peer.ts
1752
2279
  var RPC_CLOSE_CODE_UNSUPPORTED_FORMAT = 4001;
2280
+ var { debugLog: debugLog2, infoLog, warnLog: warnLog6, errorLog: errorLog2 } = getLogs("RpcPeer");
1753
2281
  var defaultConnectionUrlResolver = (peer) => {
1754
2282
  const sep = peer.ref.includes("?") ? "&" : "?";
1755
2283
  return peer.ref + sep + `clientId=${peer.clientId}&f=${peer.serializationFormat}`;
@@ -1765,8 +2293,8 @@ var RpcPeer = class {
1765
2293
  inbound = new RpcInboundCallTracker();
1766
2294
  remoteObjects = new RpcRemoteObjectTracker();
1767
2295
  sharedObjects = new RpcSharedObjectTracker();
1768
- connected = new import_core7.EventHandlerSet();
1769
- disconnected = new import_core7.EventHandlerSet();
2296
+ connected = new import_core8.EventHandlerSet();
2297
+ disconnected = new import_core8.EventHandlerSet();
1770
2298
  _pendingSends = [];
1771
2299
  _keepAliveTimer;
1772
2300
  constructor(ref, hub) {
@@ -1788,16 +2316,29 @@ var RpcPeer = class {
1788
2316
  conn.closed.add((ev) => {
1789
2317
  this._connection = void 0;
1790
2318
  this._stopKeepAlive();
2319
+ if (ev.reason)
2320
+ infoLog?.log(`'${this.ref}': Disconnected: ${ev.reason} (code=${ev.code})`);
2321
+ else
2322
+ infoLog?.log(`'${this.ref}': Disconnected`);
1791
2323
  this.disconnected.trigger(ev);
1792
2324
  });
1793
2325
  void conn.whenConnected.then(() => {
2326
+ infoLog?.log(`'${this.ref}': Connected`);
1794
2327
  this.connected.trigger();
1795
2328
  this._startKeepAlive();
1796
2329
  });
1797
2330
  }
1798
- /** The serialization format used by this peer. Override in subclasses. */
2331
+ /**
2332
+ * The serialization format used by this peer for outgoing messages.
2333
+ * If not set explicitly, defaults to the resolver's default (JSON /
2334
+ * json5np). Subclasses can pre-assign in their constructor.
2335
+ */
2336
+ _format;
1799
2337
  get format() {
1800
- return RpcSerializationFormatResolver.Default.defaultFormat;
2338
+ return this._format ?? RpcSerializationFormatResolver.Default.defaultFormat;
2339
+ }
2340
+ set format(value) {
2341
+ this._format = value;
1801
2342
  }
1802
2343
  call(method, args, options) {
1803
2344
  const callId = this.outbound.nextId();
@@ -1857,6 +2398,7 @@ var RpcPeer = class {
1857
2398
  this._sendWireData(wireData);
1858
2399
  }
1859
2400
  close() {
2401
+ infoLog?.log(`'${this.ref}': Stopping`);
1860
2402
  this._stopKeepAlive();
1861
2403
  this.remoteObjects.disconnectAll();
1862
2404
  this.sharedObjects.disconnectAll();
@@ -1870,6 +2412,7 @@ var RpcPeer = class {
1870
2412
  this.outbound.invalidateAll();
1871
2413
  this._connection?.close();
1872
2414
  this._hub.peers.delete(this.ref);
2415
+ infoLog?.log(`'${this.ref}': Stopped`);
1873
2416
  }
1874
2417
  /** Send any messages buffered while disconnected. Call after connection + handshake are ready. */
1875
2418
  _flushPendingSends() {
@@ -1896,18 +2439,32 @@ var RpcPeer = class {
1896
2439
  _handleMessage(received) {
1897
2440
  let message;
1898
2441
  let args;
1899
- if (received.kind === "binary") {
1900
- message = received.message;
1901
- args = received.args;
1902
- } else {
1903
- const parsed = deserializeMessage(received.raw);
1904
- message = parsed.message;
1905
- args = parsed.args;
2442
+ try {
2443
+ if (received.kind === "binary") {
2444
+ message = received.message;
2445
+ args = received.args;
2446
+ } else {
2447
+ const parsed = deserializeMessage(received.raw);
2448
+ message = parsed.message;
2449
+ args = parsed.args;
2450
+ }
2451
+ } catch (e) {
2452
+ errorLog2?.log(`'${this.ref}': Failed to process inbound message:`, received, e);
2453
+ return;
1906
2454
  }
1907
2455
  const method = message.Method ?? "";
2456
+ debugLog2?.log(`'${this.ref}': <- ${method}#${message.RelatedId ?? 0}`);
1908
2457
  if (method === RpcSystemCalls.handshake) {
1909
- const handshake = args[0];
1910
- if (handshake !== void 0) this._onHandshakeReceived(handshake);
2458
+ const raw = args[0];
2459
+ if (raw !== void 0) {
2460
+ const handshake = Array.isArray(raw) ? {
2461
+ RemotePeerId: raw[0],
2462
+ RemoteHubId: raw[2],
2463
+ ProtocolVersion: raw[3],
2464
+ Index: raw[4]
2465
+ } : raw;
2466
+ this._onHandshakeReceived(handshake);
2467
+ }
1911
2468
  return;
1912
2469
  }
1913
2470
  if (method.startsWith("$sys")) {
@@ -1963,6 +2520,7 @@ var RpcPeer = class {
1963
2520
  e
1964
2521
  );
1965
2522
  }
2523
+ debugLog2?.log(`'${this.ref}': Inbound call failed: ${method}#${relatedId}`, e);
1966
2524
  } finally {
1967
2525
  if (!isNoWait) {
1968
2526
  this.inbound.remove(relatedId);
@@ -1997,6 +2555,12 @@ var RpcClientPeer = class extends RpcPeer {
1997
2555
  _connectionKind = 0 /* Disconnected */;
1998
2556
  _tryIndex = 0;
1999
2557
  _handshakeIndex = 0;
2558
+ /** Index from the MOST RECENTLY RECEIVED server handshake — sent back
2559
+ * to the server in `$sys.Reconnect:3` so the server can verify we're
2560
+ * talking about the current connection. Mirrors .NET's
2561
+ * `connectionState.Handshake.Index` path in
2562
+ * `RpcOutboundCallTracker.TryReconnect`. */
2563
+ _remoteHandshakeIndex = 0;
2000
2564
  _lastRemoteHubId;
2001
2565
  _pendingHandshake;
2002
2566
  _reconnectsAt = 0;
@@ -2010,11 +2574,11 @@ var RpcClientPeer = class extends RpcPeer {
2010
2574
  serializationFormat;
2011
2575
  /** Builds the WebSocket URL for this peer. Replace to customize URL construction. */
2012
2576
  connectionUrlResolver = defaultConnectionUrlResolver;
2013
- peerChanged = new import_core7.EventHandlerSet();
2577
+ peerChanged = new import_core8.EventHandlerSet();
2014
2578
  /** Fired when the server rejects the connection due to an unsupported serialization format (close code 4010). */
2015
- unsupportedFormat = new import_core7.EventHandlerSet();
2579
+ unsupportedFormat = new import_core8.EventHandlerSet();
2016
2580
  reconnectDelayer = new RpcClientPeerReconnectDelayer();
2017
- reconnectsAtChanged = new import_core7.EventHandlerSet();
2581
+ reconnectsAtChanged = new import_core8.EventHandlerSet();
2018
2582
  get reconnectsAt() {
2019
2583
  return this._reconnectsAt;
2020
2584
  }
@@ -2023,32 +2587,34 @@ var RpcClientPeer = class extends RpcPeer {
2023
2587
  this._reconnectsAt = value;
2024
2588
  this.reconnectsAtChanged.trigger();
2025
2589
  }
2026
- _format;
2027
2590
  constructor(hub, url, serializationFormat) {
2028
2591
  super(url, hub);
2029
2592
  this.clientId = guidToBase64Url(this.id);
2030
2593
  const resolver = RpcSerializationFormatResolver.Default;
2031
2594
  this.serializationFormat = serializationFormat ?? resolver.defaultFormatKey;
2032
- this._format = resolver.get(this.serializationFormat);
2033
- }
2034
- get format() {
2035
- return this._format;
2595
+ this.format = resolver.get(this.serializationFormat);
2036
2596
  }
2037
2597
  get connectionKind() {
2038
2598
  return this._connectionKind;
2039
2599
  }
2040
- /** One-shot connection for tests — no reconnection loop, no handshake. */
2041
- connectWith(conn) {
2600
+ /** One-shot connection for tests — no reconnection loop, no handshake.
2601
+ * @param isPeerChanged Whether to treat this as a peer-identity change
2602
+ * (default `true` — no handshake was exchanged, so stage-3 compute calls
2603
+ * self-invalidate and remote/shared objects are disposed). Pass `false`
2604
+ * to simulate a same-peer reconnect in tests. */
2605
+ connectWith(conn, isPeerChanged = true) {
2042
2606
  this.setupConnection(conn);
2043
- this._reconnect(true);
2607
+ void this._reconnect(isPeerChanged);
2044
2608
  }
2045
2609
  /** Start the reconnection loop — runs until disposed. */
2046
2610
  async run(wsFactory) {
2047
2611
  const connUrl = this.connectionUrlResolver(this);
2612
+ infoLog?.log(`'${this.ref}': Started (Client)`);
2048
2613
  while (!this._disposed) {
2049
2614
  let lastCloseCode = 0;
2050
2615
  let lastCloseReason = "";
2051
2616
  try {
2617
+ infoLog?.log(`'${this.ref}': Connecting to ${connUrl}`);
2052
2618
  const ws = wsFactory?.(connUrl) ?? new WebSocket(connUrl);
2053
2619
  const conn = new RpcWebSocketConnection(
2054
2620
  ws,
@@ -2061,7 +2627,7 @@ var RpcClientPeer = class extends RpcPeer {
2061
2627
  lastCloseCode = ev.code;
2062
2628
  lastCloseReason = ev.reason;
2063
2629
  });
2064
- this._pendingHandshake = new import_core7.PromiseSource();
2630
+ this._pendingHandshake = new import_core8.PromiseSource();
2065
2631
  this.setupConnection(conn);
2066
2632
  this._connection = void 0;
2067
2633
  const closedRejection = new Promise(
@@ -2072,38 +2638,48 @@ var RpcClientPeer = class extends RpcPeer {
2072
2638
  closedRejection.catch(() => {
2073
2639
  });
2074
2640
  await Promise.race([conn.whenConnected, closedRejection]);
2075
- this._hub.systemCallSender.handshake(
2076
- conn,
2077
- this.format,
2078
- this.id,
2079
- this._hub.hubId,
2080
- ++this._handshakeIndex
2081
- );
2641
+ try {
2642
+ this._hub.systemCallSender.handshake(
2643
+ conn,
2644
+ this.format,
2645
+ this.id,
2646
+ this._hub.hubId,
2647
+ ++this._handshakeIndex
2648
+ );
2649
+ } catch (e) {
2650
+ warnLog6?.log(`'${this.ref}': Failed to send handshake`, e);
2651
+ throw e;
2652
+ }
2082
2653
  const remoteHandshake = await Promise.race([
2083
2654
  this._pendingHandshake.promise,
2084
2655
  closedRejection
2085
2656
  ]);
2086
2657
  this._pendingHandshake = void 0;
2658
+ this._remoteHandshakeIndex = remoteHandshake.Index ?? 0;
2087
2659
  const remoteHubId = remoteHandshake.RemoteHubId;
2088
2660
  let isPeerChanged = false;
2089
2661
  if (remoteHubId !== void 0) {
2090
2662
  isPeerChanged = this._lastRemoteHubId !== void 0 && this._lastRemoteHubId !== remoteHubId;
2091
2663
  if (isPeerChanged) {
2092
2664
  this.inbound.clear();
2665
+ infoLog?.log(`'${this.ref}': Peer changed`);
2093
2666
  this.peerChanged.trigger();
2094
2667
  }
2095
2668
  this._lastRemoteHubId = remoteHubId;
2096
2669
  }
2670
+ infoLog?.log(`'${this.ref}': Handshake succeeded, peerChanged=${isPeerChanged}`);
2097
2671
  this._connection = conn;
2098
2672
  this._connectionKind = 2 /* Connected */;
2099
2673
  this._tryIndex = 0;
2100
- this._reconnect(isPeerChanged);
2674
+ await this._reconnect(isPeerChanged);
2101
2675
  await new Promise((r) => conn.closed.add(() => r()));
2102
- } catch {
2676
+ } catch (e) {
2677
+ debugLog2?.log(`'${this.ref}': run() iteration failed:`, e);
2103
2678
  }
2104
2679
  this._connectionKind = 0 /* Disconnected */;
2105
2680
  if (this._disposed) break;
2106
2681
  if (lastCloseCode === RPC_CLOSE_CODE_UNSUPPORTED_FORMAT) {
2682
+ warnLog6?.log(`'${this.ref}': Unsupported serialization format - shutting down`);
2107
2683
  this.unsupportedFormat.trigger({ reason: lastCloseReason });
2108
2684
  this._disposed = true;
2109
2685
  break;
@@ -2111,6 +2687,7 @@ var RpcClientPeer = class extends RpcPeer {
2111
2687
  this._tryIndex++;
2112
2688
  const delay = this.reconnectDelayer.getDelay(this._tryIndex);
2113
2689
  if (delay.isLimitExceeded) {
2690
+ warnLog6?.log(`'${this.ref}': Reconnect limit exceeded - shutting down`);
2114
2691
  this._disposed = true;
2115
2692
  break;
2116
2693
  }
@@ -2122,42 +2699,129 @@ var RpcClientPeer = class extends RpcPeer {
2122
2699
  }
2123
2700
  }
2124
2701
  }
2125
- /** Re-send outbound calls after reconnection + flush pending sends.
2126
- * Stage-3 compute calls are always self-invalidated: without $sys.Reconnect
2127
- * protocol, the server's invalidation tracking is lost on disconnect, and
2128
- * re-sending would get a duplicate $sys.Ok that is ignored (PromiseSource
2129
- * already resolved). Self-invalidation forces a fresh recompute that
2130
- * establishes new invalidation tracking on the new connection.
2131
- * Regular in-flight calls are re-sent transparently. */
2132
- _reconnect(_isPeerChanged) {
2702
+ /**
2703
+ * Reconcile outbound calls after reconnection, then flush pending sends.
2704
+ *
2705
+ * On peer change (different server hubId) or with binary wire format —
2706
+ * we blind-resend every eligible call.
2707
+ *
2708
+ * On a same-peer reconnect with JSON transport, first call
2709
+ * `$sys.Reconnect` to ask the server which call IDs it no longer
2710
+ * recognizes (e.g. inbound tracker was cleared, or the call never
2711
+ * reached the server). Only those get resent; the rest stay in the
2712
+ * tracker awaiting their $sys.Ok. This prevents duplicate server-side
2713
+ * invocation of in-flight streaming calls on every transient
2714
+ * reconnect — mirrors .NET `RpcOutboundCallTracker.Reconnect` at
2715
+ * src/ActualLab.Rpc/Infrastructure/RpcCallTrackers.cs:209-279.
2716
+ *
2717
+ * Stage-3 compute calls (result already resolved, awaiting invalidation)
2718
+ * are always self-invalidated — TS doesn't track compute-call stages
2719
+ * across reconnect, so the safest behavior is to force a fresh compute.
2720
+ */
2721
+ async _reconnect(isPeerChanged) {
2133
2722
  if (this._connection === void 0) return;
2134
- if (_isPeerChanged) {
2723
+ if (isPeerChanged) {
2135
2724
  this.remoteObjects.disconnectAll();
2725
+ this.sharedObjects.disconnectAll();
2136
2726
  } else {
2137
2727
  this.remoteObjects.reconnectAll();
2138
2728
  }
2139
- const trackerCalls = [...this.outbound.values()];
2140
- for (const call of trackerCalls) {
2729
+ const eligible = [];
2730
+ for (const call of [...this.outbound.values()]) {
2141
2731
  const mode = call.remoteExecutionMode;
2142
2732
  if (!(mode & 2)) {
2143
2733
  if (!call.result.isCompleted)
2144
- call.result.reject(new Error("Outbound call failed: disconnected and AllowReconnect is not set."));
2734
+ call.result.reject(new Error(
2735
+ "Outbound call failed: disconnected and AllowReconnect is not set."
2736
+ ));
2145
2737
  call.onDisconnect();
2146
2738
  this.outbound.remove(call.callId);
2147
- } else if (_isPeerChanged && !(mode & 4)) {
2739
+ continue;
2740
+ }
2741
+ if (isPeerChanged && !(mode & 4)) {
2148
2742
  if (!call.result.isCompleted)
2149
- call.result.reject(new Error("Outbound call failed: reconnected to a different peer and AllowResend is not set."));
2743
+ call.result.reject(new Error(
2744
+ "Outbound call failed: reconnected to a different peer and AllowResend is not set."
2745
+ ));
2150
2746
  call.onDisconnect();
2151
2747
  this.outbound.remove(call.callId);
2152
- } else if (!call.removeOnOk && call.result.isCompleted) {
2748
+ continue;
2749
+ }
2750
+ eligible.push(call);
2751
+ }
2752
+ let unknownIds = null;
2753
+ if (!isPeerChanged && eligible.length > 0) {
2754
+ unknownIds = await this._reconcileReconnect(eligible);
2755
+ }
2756
+ for (const call of eligible) {
2757
+ if (!call.removeOnOk && call.result.isCompleted) {
2153
2758
  call.onDisconnect();
2154
2759
  this.outbound.remove(call.callId);
2155
- } else {
2156
- this._sendWireData(call.serializedWireData);
2760
+ continue;
2761
+ }
2762
+ if (unknownIds !== null && !unknownIds.has(call.callId)) {
2763
+ continue;
2157
2764
  }
2765
+ this._sendWireData(call.serializedWireData);
2158
2766
  }
2159
2767
  this._flushPendingSends();
2160
2768
  }
2769
+ /**
2770
+ * Ask the server which call IDs in `eligible` it no longer recognizes
2771
+ * via `$sys.Reconnect:3`. Returns the set of call IDs that need to be
2772
+ * resent. On any failure, falls back to "resend everything".
2773
+ *
2774
+ * Wire format of the `completedStages` argument is chosen per wire format:
2775
+ * - JSON (json5np): plain object `{"<stage>": "<base64-of-bytes>"}` —
2776
+ * matches .NET `System.Text.Json`'s default for `Dictionary<int, byte[]>`.
2777
+ * - MessagePack (msgpack6/msgpack6c): `Map<number, Uint8Array>` —
2778
+ * the patched encoder (see `./msgpack-map-patch.ts`) writes this as a
2779
+ * native msgpack `map<int, bin>`, matching .NET MessagePack-CSharp's
2780
+ * default for `Dictionary<int, byte[]>`.
2781
+ */
2782
+ async _reconcileReconnect(eligible) {
2783
+ try {
2784
+ const byStage = /* @__PURE__ */ new Map();
2785
+ for (const call of eligible) {
2786
+ const stage = call.getReconnectStage(false);
2787
+ if (stage === null) continue;
2788
+ let arr = byStage.get(stage);
2789
+ if (!arr) {
2790
+ arr = [];
2791
+ byStage.set(stage, arr);
2792
+ }
2793
+ arr.push(call.callId);
2794
+ }
2795
+ if (byStage.size === 0) {
2796
+ return new Set(eligible.map((c) => c.callId));
2797
+ }
2798
+ const isBinary = this.format.isBinary;
2799
+ const stagesArg = isBinary ? /* @__PURE__ */ new Map() : {};
2800
+ for (const [stage, ids] of byStage) {
2801
+ ids.sort((a, b) => a - b);
2802
+ const bytes2 = IncreasingSeqCompressor.serialize(ids);
2803
+ if (isBinary) {
2804
+ stagesArg.set(stage, bytes2);
2805
+ } else {
2806
+ stagesArg[String(stage)] = base64Encode(bytes2);
2807
+ }
2808
+ }
2809
+ const reconnectCall = this.call(
2810
+ RpcSystemCalls.reconnect,
2811
+ [this._remoteHandshakeIndex, stagesArg],
2812
+ {
2813
+ remoteExecutionMode: 1
2814
+ /* AwaitForConnection only; must not itself reconnect */
2815
+ }
2816
+ );
2817
+ const result = await reconnectCall.result.promise;
2818
+ const bytes = _toBytes2(result);
2819
+ if (bytes === null) return new Set(eligible.map((c) => c.callId));
2820
+ return new Set(IncreasingSeqCompressor.deserialize(bytes));
2821
+ } catch {
2822
+ return new Set(eligible.map((c) => c.callId));
2823
+ }
2824
+ }
2161
2825
  _onHandshakeReceived(handshake) {
2162
2826
  this._pendingHandshake?.resolve(handshake);
2163
2827
  }
@@ -2205,8 +2869,20 @@ function guidToBase64Url(uuid) {
2205
2869
  const binary = String.fromCharCode(...bytes);
2206
2870
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2207
2871
  }
2872
+ function _toBytes2(value) {
2873
+ if (value instanceof Uint8Array) return value;
2874
+ if (typeof value === "string") {
2875
+ try {
2876
+ return base64Decode(value);
2877
+ } catch {
2878
+ return null;
2879
+ }
2880
+ }
2881
+ return null;
2882
+ }
2208
2883
 
2209
2884
  // src/rpc-service-host.ts
2885
+ var { warnLog: warnLog7 } = getLogs("RpcServiceHost");
2210
2886
  var RpcServiceHost = class {
2211
2887
  _methods = /* @__PURE__ */ new Map();
2212
2888
  register(def, impl) {
@@ -2221,8 +2897,10 @@ var RpcServiceHost = class {
2221
2897
  }
2222
2898
  async dispatch(wireMethod, args, context) {
2223
2899
  const entry = this._methods.get(wireMethod);
2224
- if (entry === void 0)
2900
+ if (entry === void 0) {
2901
+ warnLog7?.log(`Method not found: ${wireMethod}`);
2225
2902
  throw new Error(`Method not found: ${wireMethod}`);
2903
+ }
2226
2904
  if (context !== void 0 && entry.def.callTypeId !== 0) {
2227
2905
  return await entry.fn(...args, context);
2228
2906
  }
@@ -2752,6 +3430,7 @@ function computeMethodHash(methodName) {
2752
3430
  }
2753
3431
 
2754
3432
  // src/rpc-method-registry.ts
3433
+ var { warnLog: warnLog8 } = getLogs("RpcMethodRegistry");
2755
3434
  var RpcMethodRegistry = class {
2756
3435
  _nameToHash = /* @__PURE__ */ new Map();
2757
3436
  _hashToName = /* @__PURE__ */ new Map();
@@ -2761,9 +3440,7 @@ var RpcMethodRegistry = class {
2761
3440
  const hash = computeMethodHash(methodName);
2762
3441
  const existing = this._hashToName.get(hash);
2763
3442
  if (existing !== void 0 && existing !== methodName) {
2764
- console.warn(
2765
- `[RpcMethodRegistry] hash collision: "${methodName}" and "${existing}" both hash to ${hash}`
2766
- );
3443
+ warnLog8?.log(`hash collision: "${methodName}" and "${existing}" both hash to ${hash}`);
2767
3444
  return;
2768
3445
  }
2769
3446
  this._nameToHash.set(methodName, hash);
@@ -2796,6 +3473,7 @@ var RpcMethodRegistry = class {
2796
3473
  };
2797
3474
 
2798
3475
  // src/rpc-hub.ts
3476
+ var { warnLog: warnLog9 } = getLogs("RpcHub");
2799
3477
  var RpcHub = class {
2800
3478
  hubId;
2801
3479
  peers = /* @__PURE__ */ new Map();
@@ -2821,6 +3499,7 @@ var RpcHub = class {
2821
3499
  if (peer !== void 0) {
2822
3500
  peer.close();
2823
3501
  this.peers.delete(ref);
3502
+ warnLog9?.log(`'${ref}': peer is removed from RpcHub`);
2824
3503
  }
2825
3504
  }
2826
3505
  /** Get or create a peer by ref. Server peers have "server://" prefix, everything else is a client peer. */
@@ -3052,13 +3731,14 @@ function getStateDescription(state, useLastError = false) {
3052
3731
  }
3053
3732
 
3054
3733
  // src/rpc-peer-state-monitor.ts
3055
- var import_core8 = require("@actuallab/core");
3734
+ var import_core9 = require("@actuallab/core");
3735
+ var { infoLog: infoLog2 } = getLogs("RpcPeerStateMonitor");
3056
3736
  var JustConnectedPeriodMs = 1500;
3057
3737
  var JustDisconnectedPeriodMs = 3e3;
3058
3738
  var MinReconnectsInMs = 1e3;
3059
3739
  var RpcPeerStateMonitor = class {
3060
3740
  peer;
3061
- stateChanged = new import_core8.EventHandlerSet();
3741
+ stateChanged = new import_core9.EventHandlerSet();
3062
3742
  _rawConnected;
3063
3743
  _connectedAt = 0;
3064
3744
  _disconnectedAt = 0;
@@ -3114,6 +3794,7 @@ var RpcPeerStateMonitor = class {
3114
3794
  this._scheduleRecompute(
3115
3795
  this._rawConnected ? JustConnectedPeriodMs : JustDisconnectedPeriodMs
3116
3796
  );
3797
+ infoLog2?.log(`'${peer.ref}': monitor started`);
3117
3798
  }
3118
3799
  get state() {
3119
3800
  return this._state;
@@ -3129,6 +3810,7 @@ var RpcPeerStateMonitor = class {
3129
3810
  this.peer.reconnectDelayer.cancelDelaysChanged.remove(
3130
3811
  this._onCancelDelaysChanged
3131
3812
  );
3813
+ infoLog2?.log(`'${this.peer.ref}': monitor stopped`);
3132
3814
  }
3133
3815
  _recompute() {
3134
3816
  if (this._timer !== void 0) {
@@ -3208,7 +3890,9 @@ var RpcPeerStateMonitor = class {
3208
3890
  ARG_DELIMITER,
3209
3891
  ENVELOPE_DELIMITER,
3210
3892
  FRAME_DELIMITER,
3893
+ IncreasingSeqCompressor,
3211
3894
  RPC_CLOSE_CODE_UNSUPPORTED_FORMAT,
3895
+ RpcCallStage,
3212
3896
  RpcCallTypeId,
3213
3897
  RpcClientPeer,
3214
3898
  RpcClientPeerReconnectDelayer,
@@ -3253,6 +3937,7 @@ var RpcPeerStateMonitor = class {
3253
3937
  deserializeBinaryMessage,
3254
3938
  deserializeCompactBinaryMessage,
3255
3939
  deserializeMessage,
3940
+ getLogs,
3256
3941
  getMethodsMeta,
3257
3942
  getServiceMeta,
3258
3943
  getStateDescription,