@actuallab/rpc 12.3.14 → 12.3.29

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,
@@ -125,7 +127,43 @@ var ARG_DELIMITER = "";
125
127
  var FRAME_DELIMITER = "\n";
126
128
 
127
129
  // src/rpc-serialization.ts
130
+ var import_msgpack2 = require("@msgpack/msgpack");
131
+
132
+ // src/msgpack-map-patch.ts
128
133
  var import_msgpack = require("@msgpack/msgpack");
134
+ function patchMsgpackEncoderForMaps() {
135
+ const proto = import_msgpack.Encoder.prototype;
136
+ if (proto._hasMapPatch) return;
137
+ proto._hasMapPatch = true;
138
+ const originalDoEncode = proto.doEncode;
139
+ proto.doEncode = function(object, depth) {
140
+ if (object instanceof Map) {
141
+ _encodeMapInstance.call(this, object, depth);
142
+ return;
143
+ }
144
+ originalDoEncode.call(this, object, depth);
145
+ };
146
+ function _encodeMapInstance(m, depth) {
147
+ const self = this;
148
+ const size = m.size;
149
+ if (size < 16) {
150
+ self.writeU8(128 + size);
151
+ } else if (size < 65536) {
152
+ self.writeU8(222);
153
+ self.writeU16(size);
154
+ } else {
155
+ self.writeU8(223);
156
+ self.writeU32(size);
157
+ }
158
+ for (const [key, value] of m) {
159
+ self.doEncode(key, depth + 1);
160
+ self.doEncode(value, depth + 1);
161
+ }
162
+ }
163
+ }
164
+ patchMsgpackEncoderForMaps();
165
+
166
+ // src/rpc-serialization.ts
129
167
  function serializeMessage(message, args) {
130
168
  const envelope = JSON.stringify(message);
131
169
  if (args === void 0 || args.length === 0) {
@@ -166,15 +204,15 @@ function deserializeMessage(raw) {
166
204
  var textEncoder = new TextEncoder();
167
205
  var textDecoder = new TextDecoder();
168
206
  var INITIAL_ENCODER_BUFFER_SIZE = 32 * 1024;
169
- var defaultBinaryEncoder = new import_msgpack.Encoder(
207
+ var defaultBinaryEncoder = new import_msgpack2.Encoder(
170
208
  void 0,
171
209
  void 0,
172
210
  void 0,
173
211
  INITIAL_ENCODER_BUFFER_SIZE
174
212
  );
175
- var defaultBinaryDecoder = new import_msgpack.Decoder();
213
+ var defaultBinaryDecoder = new import_msgpack2.Decoder();
176
214
  function createBinaryEncoder() {
177
- return new import_msgpack.Encoder(
215
+ return new import_msgpack2.Encoder(
178
216
  void 0,
179
217
  void 0,
180
218
  void 0,
@@ -411,7 +449,7 @@ function splitCompactBinaryFrame(frame, registry, decoder = defaultBinaryDecoder
411
449
  }
412
450
 
413
451
  // src/rpc-connection.ts
414
- var import_msgpack2 = require("@msgpack/msgpack");
452
+ var import_msgpack3 = require("@msgpack/msgpack");
415
453
  var import_core = require("@actuallab/core");
416
454
  var WebSocketState = {
417
455
  CONNECTING: 0,
@@ -430,7 +468,7 @@ var RpcWebSocketConnection = class {
430
468
  _encoder = createBinaryEncoder();
431
469
  /** Per-connection msgpack decoder — reused across inbound messages
432
470
  * rather than reconstructing one for every `decodeMulti` call. */
433
- _decoder = new import_msgpack2.Decoder();
471
+ _decoder = new import_msgpack3.Decoder();
434
472
  /** Exposed for callers who need to encode outbound binary messages
435
473
  * through this connection (e.g. `serializeBinaryMessage`). Safe to
436
474
  * pass to synchronous encode calls on the same event-loop turn. */
@@ -660,6 +698,21 @@ function defineRpcService(name, methods) {
660
698
 
661
699
  // src/rpc-call-tracker.ts
662
700
  var import_core3 = require("@actuallab/core");
701
+
702
+ // src/rpc-call-stage.ts
703
+ var RpcCallStage = {
704
+ /** The call's result has been received from the peer. */
705
+ ResultReady: 1,
706
+ /** The compute call's result has been invalidated (compute-call specific). */
707
+ Invalidated: 3,
708
+ /**
709
+ * Bit flag added on top of ResultReady/Invalidated when the outbound
710
+ * call has been removed from the tracker (unregistered).
711
+ */
712
+ Unregistered: 4096
713
+ };
714
+
715
+ // src/rpc-call-tracker.ts
663
716
  var RpcOutboundCall = class {
664
717
  callId;
665
718
  method;
@@ -671,11 +724,35 @@ var RpcOutboundCall = class {
671
724
  removeOnOk = true;
672
725
  /** The serialized wire data — stored for re-sending on reconnect. */
673
726
  serializedWireData = "";
727
+ /**
728
+ * Bitfield of {@link RpcCallStage} flags indicating how far this call
729
+ * has progressed. Used by the `$sys.Reconnect` protocol to tell the
730
+ * remote peer which calls are still pending its attention.
731
+ *
732
+ * - 0: brand new, no result yet (the peer is expected to be actively processing).
733
+ * - `ResultReady` (1): `$sys.Ok` received for this call.
734
+ * - `Invalidated` (3): result was invalidated (compute calls only).
735
+ * - `Unregistered` (0x1000): removed from the tracker.
736
+ */
737
+ completedStage = 0;
674
738
  constructor(callId, method, remoteExecutionMode = 7) {
675
739
  this.callId = callId;
676
740
  this.method = method;
677
741
  this.remoteExecutionMode = remoteExecutionMode;
678
742
  }
743
+ /**
744
+ * Returns the stage to report to the remote peer via `$sys.Reconnect`,
745
+ * or `null` if this call should not be reconciled and must be aborted.
746
+ *
747
+ * Mirrors .NET `RpcOutboundCall.GetReconnectStage` at
748
+ * src/ActualLab.Rpc/Infrastructure/RpcOutboundCall.cs.
749
+ */
750
+ getReconnectStage(isPeerChanged) {
751
+ const mode = this.remoteExecutionMode;
752
+ if (!(mode & RpcRemoteExecutionMode.AllowReconnect)) return null;
753
+ if (isPeerChanged && !(mode & RpcRemoteExecutionMode.AllowResend)) return null;
754
+ return this.completedStage;
755
+ }
679
756
  /** Called when the connection is lost. Subclasses can override to resolve invalidation promises. */
680
757
  onDisconnect() {
681
758
  }
@@ -697,7 +774,10 @@ var RpcOutboundCallTracker = class {
697
774
  }
698
775
  remove(callId) {
699
776
  const call = this._calls.get(callId);
700
- if (call !== void 0) this._calls.delete(callId);
777
+ if (call !== void 0) {
778
+ this._calls.delete(callId);
779
+ call.completedStage |= RpcCallStage.Unregistered;
780
+ }
701
781
  return call;
702
782
  }
703
783
  values() {
@@ -760,6 +840,97 @@ var RpcInboundCallTracker = class {
760
840
  }
761
841
  };
762
842
 
843
+ // src/increasing-seq-compressor.ts
844
+ var IncreasingSeqCompressor = {
845
+ /**
846
+ * Serialize a non-decreasing sequence of non-negative integers as
847
+ * LEB128-encoded deltas.
848
+ *
849
+ * Throws `RangeError` if the input is not monotonically non-decreasing,
850
+ * or if a value exceeds `Number.MAX_SAFE_INTEGER`.
851
+ */
852
+ serialize(values) {
853
+ const deltas = [];
854
+ let last = 0;
855
+ let totalBytes = 0;
856
+ for (const value of values) {
857
+ if (!Number.isSafeInteger(value) || value < 0)
858
+ throw new RangeError(
859
+ `IncreasingSeqCompressor: value must be a non-negative safe integer (got ${value}).`
860
+ );
861
+ const delta = value - last;
862
+ if (delta < 0)
863
+ throw new RangeError(
864
+ `IncreasingSeqCompressor: sequence must be non-decreasing (saw ${value} after ${last}).`
865
+ );
866
+ deltas.push(delta);
867
+ last = value;
868
+ totalBytes += _varUintByteLength(delta);
869
+ }
870
+ const out = new Uint8Array(totalBytes);
871
+ let offset = 0;
872
+ for (const delta of deltas) {
873
+ offset = _writeVarUint(out, delta, offset);
874
+ }
875
+ return out;
876
+ },
877
+ /**
878
+ * Decode bytes produced by {@link serialize} back into the original
879
+ * sorted sequence of non-negative integers.
880
+ */
881
+ deserialize(data) {
882
+ const result = [];
883
+ let last = 0;
884
+ let offset = 0;
885
+ while (offset < data.length) {
886
+ const [delta, next] = _readVarUint(data, offset);
887
+ offset = next;
888
+ last += delta;
889
+ result.push(last);
890
+ }
891
+ return result;
892
+ }
893
+ };
894
+ var LOW_BITS = 127;
895
+ var HIGH_BIT = 128;
896
+ function _varUintByteLength(value) {
897
+ let n = 0;
898
+ let v = value;
899
+ while (v >= HIGH_BIT) {
900
+ n++;
901
+ v = Math.floor(v / 128);
902
+ }
903
+ return n + 1;
904
+ }
905
+ function _writeVarUint(out, value, offset) {
906
+ let v = value;
907
+ while (v >= HIGH_BIT) {
908
+ out[offset++] = (HIGH_BIT | v & LOW_BITS) & 255;
909
+ v = Math.floor(v / 128);
910
+ }
911
+ out[offset++] = v & 255;
912
+ return offset;
913
+ }
914
+ function _readVarUint(data, offset) {
915
+ let value = 0;
916
+ let shift = 0;
917
+ while (shift < 63) {
918
+ if (offset >= data.length)
919
+ throw new RangeError("IncreasingSeqCompressor: truncated VarUInt.");
920
+ const b2 = data[offset++];
921
+ value += (b2 & LOW_BITS) * Math.pow(2, shift);
922
+ if (b2 <= LOW_BITS) return [value, offset];
923
+ shift += 7;
924
+ }
925
+ if (offset >= data.length)
926
+ throw new RangeError("IncreasingSeqCompressor: truncated VarUInt.");
927
+ const b = data[offset++];
928
+ if (b > 1)
929
+ throw new RangeError("IncreasingSeqCompressor: VarUInt value exceeds 64 bits.");
930
+ value += b * Math.pow(2, 63);
931
+ return [value, offset];
932
+ }
933
+
763
934
  // src/rpc-stream.ts
764
935
  var import_denque = __toESM(require("denque"), 1);
765
936
  var import_core5 = require("@actuallab/core");
@@ -781,15 +952,29 @@ var RpcStreamSender = class _RpcStreamSender {
781
952
  peer;
782
953
  ackPeriod;
783
954
  ackAdvance;
955
+ /**
956
+ * If true, the source was produced by a factory `(abortSignal) => AsyncIterable<T>`
957
+ * and may honor the AbortSignal to exit gracefully. `disconnect()` first aborts
958
+ * the signal and gives the source up to `RpcStream.disconnectGracePeriodMs` ms
959
+ * before force-closing via `iterator.return()`.
960
+ *
961
+ * If false, the source is a plain AsyncIterable that cannot observe the signal,
962
+ * so `disconnect()` force-closes immediately.
963
+ */
964
+ sourceUsesAbortSignal;
965
+ // -- Pump state --
966
+ /** Next stream index that will be sent (matches .NET's `index`). */
784
967
  _nextIndex = 0;
785
968
  _ended = false;
786
969
  _started = new import_core4.PromiseSource();
970
+ /** Highest `nextIndex` observed in an ACK so far (matches .NET's `lastAckNextIndex` idiom). */
787
971
  _lastAckedIndex = 0;
788
- _ackWaiting = null;
789
- _resetRequested = false;
790
972
  _abortController = new AbortController();
791
973
  _iterator = null;
792
- constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true, isRealTime = false, canSkipTo = () => true) {
974
+ // -- ACK queue (analog of .NET's Channel<(long, bool)>) --
975
+ _acks = [];
976
+ _whenAckReady = null;
977
+ constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true, isRealTime = false, canSkipTo = () => true, sourceUsesAbortSignal = false) {
793
978
  const localId = peer.sharedObjects.nextId();
794
979
  this.id = { hostId: peer.hub.hubId, localId };
795
980
  this.allowReconnect = allowReconnect;
@@ -798,6 +983,7 @@ var RpcStreamSender = class _RpcStreamSender {
798
983
  this.peer = peer;
799
984
  this.ackPeriod = ackPeriod;
800
985
  this.ackAdvance = ackAdvance;
986
+ this.sourceUsesAbortSignal = sourceUsesAbortSignal;
801
987
  }
802
988
  /** AbortSignal that is aborted when the sender is disconnected. */
803
989
  get abortSignal() {
@@ -807,56 +993,61 @@ var RpcStreamSender = class _RpcStreamSender {
807
993
  toRef() {
808
994
  return `${this.id.hostId},${this.id.localId},${this.ackPeriod},${this.ackAdvance},${this.allowReconnect ? "1" : "0"},${this.isRealTime ? "1" : "0"}`;
809
995
  }
810
- /** Called by system call handler when $sys.Ack is received from the client. */
996
+ /**
997
+ * Called by system call handler when $sys.Ack is received from the client.
998
+ *
999
+ * Mirrors .NET `RpcSharedStream<T>.OnAck` at
1000
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:68-101.
1001
+ */
811
1002
  onAck(nextIndex, hostId) {
812
1003
  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;
1004
+ if (!this._started.isCompleted) this._started.resolve();
1005
+ this._acks.push({ nextIndex, mustReset });
1006
+ if (this._whenAckReady) {
1007
+ this._whenAckReady.resolve();
1008
+ this._whenAckReady = null;
828
1009
  }
829
1010
  }
830
1011
  /** Called by system call handler when $sys.AckEnd is received from the client. */
831
1012
  onAckEnd(_hostId) {
832
1013
  this.disconnect();
833
1014
  }
834
- /** Send a single item to the client. */
1015
+ /**
1016
+ * Send a single item to the client at the current `_nextIndex`.
1017
+ *
1018
+ * Advances `_nextIndex` unconditionally — even if the peer is disconnected
1019
+ * the item is considered "sent" from the pump's perspective so the replay
1020
+ * buffer indexing stays consistent. If a connection is absent, the wire
1021
+ * message is dropped; the item will be resent from the replay buffer
1022
+ * after a reconnect (client sends `Ack(N, mustReset=true)`).
1023
+ */
835
1024
  sendItem(item) {
836
1025
  if (this._ended) return;
837
1026
  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
- );
1027
+ if (conn) {
1028
+ this.peer.hub.systemCallSender.item(
1029
+ conn,
1030
+ this.peer.format,
1031
+ this.id.localId,
1032
+ this._nextIndex,
1033
+ item
1034
+ );
1035
+ }
846
1036
  this._nextIndex++;
847
1037
  }
848
- /** Send a batch of items to the client. */
1038
+ /** Send a batch of items to the client. See {@link sendItem} for disconnect semantics. */
849
1039
  sendBatch(items) {
850
1040
  if (this._ended || items.length === 0) return;
851
1041
  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
- );
1042
+ if (conn) {
1043
+ this.peer.hub.systemCallSender.batch(
1044
+ conn,
1045
+ this.peer.format,
1046
+ this.id.localId,
1047
+ this._nextIndex,
1048
+ items
1049
+ );
1050
+ }
860
1051
  this._nextIndex += items.length;
861
1052
  }
862
1053
  /** Signal stream completion to the client. */
@@ -876,76 +1067,270 @@ var RpcStreamSender = class _RpcStreamSender {
876
1067
  }
877
1068
  /**
878
1069
  * Consume an AsyncIterable and send all items to the client.
879
- * Waits for the client's initial Ack before starting to pump items.
880
1070
  *
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.
1071
+ * Dispatches to the non-real-time or real-time pump, matching .NET
1072
+ * `RpcSharedStream<T>.OnRun` at
1073
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:105-108.
885
1074
  */
886
1075
  async writeFrom(source) {
887
1076
  await this._started.promise;
1077
+ if (this._ended) return;
888
1078
  const iterator = source[Symbol.asyncIterator]();
889
1079
  this._iterator = iterator;
1080
+ const state = { iteratorDone: false };
890
1081
  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;
1082
+ if (this.isRealTime) {
1083
+ await this._runRealTime(iterator, state);
1084
+ } else {
1085
+ await this._runNonRealTime(iterator, state);
1086
+ }
1087
+ } finally {
1088
+ if (state.iteratorDone && this._iterator === iterator) {
1089
+ this._iterator = null;
1090
+ }
1091
+ }
1092
+ }
1093
+ /**
1094
+ * Non-real-time pump — back-pressure through buffered ACKs.
1095
+ *
1096
+ * Direct port of .NET `OnRunNonRealTime` at
1097
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:110-217.
1098
+ */
1099
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition -- _ended changes across awaits */
1100
+ async _runNonRealTime(iterator, state) {
1101
+ const buffer = new import_core4.RingBuffer(this.ackAdvance + 1);
1102
+ let bufferStart = 0;
1103
+ let isFullyBuffered = false;
1104
+ while (true) {
1105
+ let ack = this._tryProcessAcks();
1106
+ if (!ack) {
1107
+ await this._waitAckReady();
1108
+ if (this._ended) return;
1109
+ ack = this._tryProcessAcks();
1110
+ if (!ack) {
1111
+ return;
900
1112
  }
901
- if (this._nextIndex < this._lastAckedIndex + this.ackAdvance) {
902
- this.sendItem(item);
1113
+ }
1114
+ this._lastAckedIndex = ack.nextIndex;
1115
+ const shift = _clamp(ack.nextIndex - bufferStart, 0, buffer.count);
1116
+ if (shift > 0) {
1117
+ buffer.moveHead(shift);
1118
+ bufferStart += shift;
1119
+ }
1120
+ if (this._nextIndex < bufferStart) {
1121
+ if (!this._ended)
1122
+ this.sendEnd(new Error("Stream position unavailable."));
1123
+ return;
1124
+ }
1125
+ let bufferIndex = this._nextIndex - bufferStart;
1126
+ const maxIndex = ack.nextIndex + this.ackAdvance;
1127
+ while (this._nextIndex < maxIndex) {
1128
+ if (this._ended) return;
1129
+ if (bufferIndex < buffer.count) {
1130
+ const item = buffer.get(bufferIndex++);
1131
+ if (item.kind === "value") {
1132
+ this.sendItem(item.value);
1133
+ } else if (item.kind === "end") {
1134
+ if (!this._ended) this.sendEnd();
1135
+ return;
1136
+ } else {
1137
+ if (!this._ended) this.sendEnd(item.error);
1138
+ return;
1139
+ }
903
1140
  continue;
904
1141
  }
905
- if (!this.isRealTime) {
906
- await this._waitForAckBudget();
1142
+ if (isFullyBuffered) break;
1143
+ if (!buffer.hasRemainingCapacity) break;
1144
+ let pulled;
1145
+ try {
1146
+ const r = await iterator.next();
907
1147
  if (this._ended) return;
908
- this.sendItem(item);
909
- continue;
1148
+ if (r.done) {
1149
+ pulled = _endItem;
1150
+ isFullyBuffered = true;
1151
+ state.iteratorDone = true;
1152
+ } else {
1153
+ pulled = { kind: "value", value: r.value };
1154
+ }
1155
+ } catch (e) {
1156
+ state.iteratorDone = true;
1157
+ isFullyBuffered = true;
1158
+ pulled = {
1159
+ kind: "error",
1160
+ error: e instanceof Error ? e : new Error(String(e))
1161
+ };
910
1162
  }
911
- let latestSkipTarget = this.canSkipTo(item) ? item : void 0;
912
- let sourceExhausted = false;
913
- while (this._nextIndex >= this._lastAckedIndex + this.ackAdvance) {
1163
+ buffer.pushTail(pulled);
1164
+ }
1165
+ }
1166
+ }
1167
+ /**
1168
+ * Real-time pump — drops stale items to stay close to the source's head.
1169
+ *
1170
+ * Direct port of .NET `OnRunRealTime` at
1171
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:219-420.
1172
+ */
1173
+ async _runRealTime(iterator, state) {
1174
+ const buffer = new import_core4.RingBuffer(this.ackAdvance + 1);
1175
+ let bufferStart = 0;
1176
+ let isFullyBuffered = false;
1177
+ while (true) {
1178
+ let ack = this._tryProcessAcks();
1179
+ if (!ack) {
1180
+ await this._waitAckReady();
1181
+ if (this._ended) return;
1182
+ ack = this._tryProcessAcks();
1183
+ if (!ack) return;
1184
+ }
1185
+ this._lastAckedIndex = ack.nextIndex;
1186
+ if (ack.mustReset && !isFullyBuffered) {
1187
+ buffer.clear();
1188
+ bufferStart = this._nextIndex;
1189
+ while (true) {
914
1190
  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;
1191
+ let item;
1192
+ let accepted = false;
1193
+ try {
1194
+ const r = await iterator.next();
1195
+ if (this._ended) return;
1196
+ if (r.done) {
1197
+ item = _endItem;
1198
+ isFullyBuffered = true;
1199
+ state.iteratorDone = true;
1200
+ } else if (this.canSkipTo(r.value)) {
1201
+ item = { kind: "value", value: r.value };
1202
+ accepted = true;
1203
+ } else {
1204
+ continue;
1205
+ }
1206
+ } catch (e) {
1207
+ state.iteratorDone = true;
1208
+ isFullyBuffered = true;
1209
+ item = {
1210
+ kind: "error",
1211
+ error: e instanceof Error ? e : new Error(String(e))
1212
+ };
922
1213
  }
1214
+ buffer.pushTail(item);
1215
+ if (accepted || isFullyBuffered) break;
923
1216
  }
1217
+ }
1218
+ const shift = _clamp(ack.nextIndex - bufferStart, 0, buffer.count);
1219
+ if (shift > 0) {
1220
+ buffer.moveHead(shift);
1221
+ bufferStart += shift;
1222
+ }
1223
+ if (this._nextIndex < bufferStart) {
1224
+ if (!this._ended)
1225
+ this.sendEnd(new Error("Stream position unavailable."));
1226
+ return;
1227
+ }
1228
+ let bufferIndex = this._nextIndex - bufferStart;
1229
+ const maxIndex = ack.nextIndex + this.ackAdvance;
1230
+ while (this._nextIndex < maxIndex) {
924
1231
  if (this._ended) return;
925
- if (latestSkipTarget !== void 0) {
926
- this.sendItem(latestSkipTarget);
1232
+ if (this._acks.length > 0) break;
1233
+ if (bufferIndex < buffer.count) {
1234
+ const item = buffer.get(bufferIndex++);
1235
+ if (item.kind === "value") {
1236
+ this.sendItem(item.value);
1237
+ } else if (item.kind === "end") {
1238
+ if (!this._ended) this.sendEnd();
1239
+ return;
1240
+ } else {
1241
+ if (!this._ended) this.sendEnd(item.error);
1242
+ return;
1243
+ }
1244
+ continue;
927
1245
  }
928
- if (sourceExhausted) {
929
- if (!this._ended) this.sendEnd();
930
- return;
1246
+ if (isFullyBuffered) break;
1247
+ if (!buffer.hasRemainingCapacity) break;
1248
+ let pulled;
1249
+ try {
1250
+ const r = await iterator.next();
1251
+ if (this._ended) return;
1252
+ if (r.done) {
1253
+ pulled = _endItem;
1254
+ isFullyBuffered = true;
1255
+ state.iteratorDone = true;
1256
+ } else {
1257
+ pulled = { kind: "value", value: r.value };
1258
+ }
1259
+ } catch (e) {
1260
+ state.iteratorDone = true;
1261
+ isFullyBuffered = true;
1262
+ pulled = {
1263
+ kind: "error",
1264
+ error: e instanceof Error ? e : new Error(String(e))
1265
+ };
931
1266
  }
1267
+ buffer.pushTail(pulled);
932
1268
  }
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)));
1269
+ if (this._nextIndex >= maxIndex && !isFullyBuffered && !this._ended) {
1270
+ let latestValue;
1271
+ let haveLatest = false;
1272
+ let exitReason = "ack";
1273
+ let exitError = null;
1274
+ drain: while (!this._ended) {
1275
+ if (this._acks.length > 0) {
1276
+ exitReason = "ack";
1277
+ break drain;
1278
+ }
1279
+ let r;
1280
+ try {
1281
+ r = await iterator.next();
1282
+ } catch (e) {
1283
+ state.iteratorDone = true;
1284
+ isFullyBuffered = true;
1285
+ exitReason = "error";
1286
+ exitError = e instanceof Error ? e : new Error(String(e));
1287
+ break drain;
1288
+ }
1289
+ if (this._ended) return;
1290
+ if (r.done) {
1291
+ state.iteratorDone = true;
1292
+ isFullyBuffered = true;
1293
+ exitReason = "end";
1294
+ break drain;
1295
+ }
1296
+ if (this.canSkipTo(r.value)) {
1297
+ latestValue = r.value;
1298
+ haveLatest = true;
1299
+ }
1300
+ }
1301
+ if (exitReason === "end" || exitReason === "error") {
1302
+ if (haveLatest && !this._ended) this.sendItem(latestValue);
1303
+ if (!this._ended) this.sendEnd(exitError);
1304
+ return;
1305
+ }
1306
+ if (haveLatest) {
1307
+ const wasFull = buffer.isFull;
1308
+ buffer.pushTailAndMoveHeadIfFull({ kind: "value", value: latestValue });
1309
+ if (wasFull) bufferStart++;
1310
+ }
939
1311
  }
940
1312
  }
941
1313
  }
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;
1314
+ /* eslint-enable @typescript-eslint/no-unnecessary-condition */
1315
+ /** Drain all queued ACKs, returning the most recent one (or null if none). */
1316
+ _tryProcessAcks() {
1317
+ if (this._acks.length === 0) return null;
1318
+ let last = null;
1319
+ while (this._acks.length > 0) {
1320
+ const a = this._acks.shift();
1321
+ last = a;
1322
+ if (a.mustReset || this._nextIndex < a.nextIndex) {
1323
+ this._nextIndex = a.nextIndex;
1324
+ }
948
1325
  }
1326
+ return last;
1327
+ }
1328
+ /** Wait for the ACK queue to have at least one entry (or `_ended`). */
1329
+ async _waitAckReady() {
1330
+ if (this._acks.length > 0 || this._ended) return;
1331
+ this._whenAckReady = new import_core4.PromiseSource();
1332
+ await this._whenAckReady.promise;
1333
+ this._whenAckReady = null;
949
1334
  }
950
1335
  // -- IRpcObject --
951
1336
  reconnect() {
@@ -957,17 +1342,38 @@ var RpcStreamSender = class _RpcStreamSender {
957
1342
  if (!this._started.isCompleted) {
958
1343
  this._started.resolve();
959
1344
  }
960
- if (this._ackWaiting) {
961
- this._ackWaiting.resolve();
962
- this._ackWaiting = null;
1345
+ if (this._whenAckReady) {
1346
+ this._whenAckReady.resolve();
1347
+ this._whenAckReady = null;
963
1348
  }
964
- if (this._iterator) {
965
- void this._iterator.return?.(void 0);
1349
+ this.peer.sharedObjects.unregister(this);
1350
+ const iterator = this._iterator;
1351
+ if (!iterator) return;
1352
+ if (!this.sourceUsesAbortSignal) {
966
1353
  this._iterator = null;
1354
+ this._forceCloseIterator(iterator);
1355
+ return;
1356
+ }
1357
+ setTimeout(() => {
1358
+ if (this._iterator === iterator) {
1359
+ this._iterator = null;
1360
+ this._forceCloseIterator(iterator);
1361
+ }
1362
+ }, RpcStream.disconnectGracePeriodMs);
1363
+ }
1364
+ _forceCloseIterator(iterator) {
1365
+ try {
1366
+ const ret = iterator.return?.(void 0);
1367
+ if (ret) void ret.catch(() => {
1368
+ });
1369
+ } catch {
967
1370
  }
968
- this.peer.sharedObjects.unregister(this);
969
1371
  }
970
1372
  };
1373
+ var _endItem = { kind: "end" };
1374
+ function _clamp(value, min, max) {
1375
+ return value < min ? min : value > max ? max : value;
1376
+ }
971
1377
 
972
1378
  // src/rpc-stream.ts
973
1379
  var DEFAULT_ACK_PERIOD2 = 30;
@@ -1003,6 +1409,15 @@ function parseStreamRef(value) {
1003
1409
  return null;
1004
1410
  }
1005
1411
  var RpcStream = class _RpcStream {
1412
+ /**
1413
+ * Grace period (ms) given to a factory-form local source to honor its
1414
+ * AbortSignal and exit naturally before the sender force-closes the
1415
+ * iterator via `iterator.return()`.
1416
+ *
1417
+ * Only applies to sources provided as `(abortSignal) => AsyncIterable<T>`.
1418
+ * Plain `AsyncIterable<T>` sources are force-closed immediately on disconnect.
1419
+ */
1420
+ static disconnectGracePeriodMs = 100;
1006
1421
  id;
1007
1422
  kind;
1008
1423
  allowReconnect;
@@ -1080,17 +1495,19 @@ var RpcStream = class _RpcStream {
1080
1495
  throw new Error("toRef() was already called with a different peer.");
1081
1496
  return this._toRefValue;
1082
1497
  }
1498
+ const isFactory = typeof this.localSource === "function";
1083
1499
  const sender = new RpcStreamSender(
1084
1500
  peer,
1085
1501
  this.ackPeriod,
1086
1502
  this.ackAdvance,
1087
1503
  this.allowReconnect,
1088
1504
  this.isRealTime,
1089
- this.canSkipTo
1505
+ this.canSkipTo,
1506
+ isFactory
1090
1507
  );
1091
1508
  peer.sharedObjects.register(sender);
1092
1509
  this._sender = sender;
1093
- const source = typeof this.localSource === "function" ? this.localSource(sender.abortSignal) : this.localSource;
1510
+ const source = isFactory ? this.localSource(sender.abortSignal) : this.localSource;
1094
1511
  this._whenSent = sender.writeFrom(source);
1095
1512
  this._toRefValue = peer.format.isBinary ? {
1096
1513
  SerializedId: [sender.id.hostId, sender.id.localId],
@@ -1300,6 +1717,25 @@ function resolveStreamRefs(value, peer) {
1300
1717
  return value;
1301
1718
  }
1302
1719
 
1720
+ // src/base64.ts
1721
+ function base64Encode(bytes) {
1722
+ const chunkSize = 32768;
1723
+ let binary = "";
1724
+ for (let i = 0; i < bytes.length; i += chunkSize) {
1725
+ binary += String.fromCharCode.apply(
1726
+ null,
1727
+ bytes.subarray(i, Math.min(bytes.length, i + chunkSize))
1728
+ );
1729
+ }
1730
+ return btoa(binary);
1731
+ }
1732
+ function base64Decode(encoded) {
1733
+ const binary = atob(encoded);
1734
+ const out = new Uint8Array(binary.length);
1735
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
1736
+ return out;
1737
+ }
1738
+
1303
1739
  // src/rpc-system-call-handler.ts
1304
1740
  var RpcSystemCallHandler = class {
1305
1741
  handle(message, args, peer) {
@@ -1309,6 +1745,7 @@ var RpcSystemCallHandler = class {
1309
1745
  case RpcSystemCalls.ok: {
1310
1746
  const call = peer.outbound.get(relatedId);
1311
1747
  if (call !== void 0) {
1748
+ call.completedStage |= RpcCallStage.ResultReady;
1312
1749
  if (call.removeOnOk) {
1313
1750
  peer.outbound.remove(relatedId);
1314
1751
  }
@@ -1316,6 +1753,10 @@ var RpcSystemCallHandler = class {
1316
1753
  }
1317
1754
  break;
1318
1755
  }
1756
+ case RpcSystemCalls.reconnect: {
1757
+ this._handleReconnect(relatedId, args, peer);
1758
+ break;
1759
+ }
1319
1760
  case RpcSystemCalls.error: {
1320
1761
  const call = peer.outbound.remove(relatedId);
1321
1762
  if (call !== void 0) {
@@ -1404,7 +1845,54 @@ var RpcSystemCallHandler = class {
1404
1845
  }
1405
1846
  }
1406
1847
  }
1848
+ /**
1849
+ * Server-side handler for the `$sys.Reconnect` call. Parses the peer's
1850
+ * report of in-flight call IDs (grouped by stage, IncreasingSeqCompressor-
1851
+ * encoded, base64-wrapped for JSON transport), computes the subset this
1852
+ * peer does not recognize, and replies with a compressed byte[] of the
1853
+ * unknown IDs wrapped in `$sys.Ok`.
1854
+ *
1855
+ * @param relatedId The outbound call ID the client used for this
1856
+ * `$sys.Reconnect` invocation — we send `$sys.Ok` back
1857
+ * with the same `RelatedId`.
1858
+ */
1859
+ _handleReconnect(relatedId, args, peer) {
1860
+ if (peer.connection === void 0) return;
1861
+ let unknownIds = [];
1862
+ try {
1863
+ const stagesRaw = args[1];
1864
+ if (stagesRaw && typeof stagesRaw === "object" && !Array.isArray(stagesRaw)) {
1865
+ const unknownSet = /* @__PURE__ */ new Set();
1866
+ for (const [, rawValue] of Object.entries(stagesRaw)) {
1867
+ const bytes = _toBytes(rawValue);
1868
+ if (bytes === null) continue;
1869
+ const callIds = IncreasingSeqCompressor.deserialize(bytes);
1870
+ for (const callId of callIds) {
1871
+ if (peer.inbound.get(callId) === void 0) {
1872
+ unknownSet.add(callId);
1873
+ }
1874
+ }
1875
+ }
1876
+ unknownIds = [...unknownSet].sort((a, b) => a - b);
1877
+ }
1878
+ } catch {
1879
+ }
1880
+ const responseBytes = IncreasingSeqCompressor.serialize(unknownIds);
1881
+ const responseValue = peer.format.isBinary ? responseBytes : base64Encode(responseBytes);
1882
+ peer.hub.systemCallSender.ok(peer.connection, peer.format, relatedId, responseValue);
1883
+ }
1407
1884
  };
1885
+ function _toBytes(value) {
1886
+ if (value instanceof Uint8Array) return value;
1887
+ if (typeof value === "string") {
1888
+ try {
1889
+ return base64Decode(value);
1890
+ } catch {
1891
+ return null;
1892
+ }
1893
+ }
1894
+ return null;
1895
+ }
1408
1896
 
1409
1897
  // src/rpc-system-call-sender.ts
1410
1898
  var RpcSystemCallSender = class _RpcSystemCallSender {
@@ -1592,7 +2080,7 @@ var RpcClientPeerReconnectDelayer = class extends import_core6.RetryDelayer {
1592
2080
  };
1593
2081
 
1594
2082
  // src/rpc-serialization-format.ts
1595
- var import_msgpack3 = require("@msgpack/msgpack");
2083
+ var import_msgpack4 = require("@msgpack/msgpack");
1596
2084
  var RpcSerializationFormat = class {
1597
2085
  /** Create a reusable binary encoder for this format. Returns undefined for text formats. */
1598
2086
  createEncoder() {
@@ -1666,7 +2154,7 @@ var RpcMessagePackSerializationFormat = class extends RpcSerializationFormat {
1666
2154
  return createBinaryEncoder();
1667
2155
  }
1668
2156
  createDecoder() {
1669
- return new import_msgpack3.Decoder();
2157
+ return new import_msgpack4.Decoder();
1670
2158
  }
1671
2159
  };
1672
2160
  var RpcMessagePackCompactSerializationFormat = class extends RpcSerializationFormat {
@@ -1698,7 +2186,7 @@ var RpcMessagePackCompactSerializationFormat = class extends RpcSerializationFor
1698
2186
  return createBinaryEncoder();
1699
2187
  }
1700
2188
  createDecoder() {
1701
- return new import_msgpack3.Decoder();
2189
+ return new import_msgpack4.Decoder();
1702
2190
  }
1703
2191
  };
1704
2192
  RpcSerializationFormat.SystemJsonV5 = new RpcJsonSerializationFormat("json5");
@@ -1795,9 +2283,17 @@ var RpcPeer = class {
1795
2283
  this._startKeepAlive();
1796
2284
  });
1797
2285
  }
1798
- /** The serialization format used by this peer. Override in subclasses. */
2286
+ /**
2287
+ * The serialization format used by this peer for outgoing messages.
2288
+ * If not set explicitly, defaults to the resolver's default (JSON /
2289
+ * json5np). Subclasses can pre-assign in their constructor.
2290
+ */
2291
+ _format;
1799
2292
  get format() {
1800
- return RpcSerializationFormatResolver.Default.defaultFormat;
2293
+ return this._format ?? RpcSerializationFormatResolver.Default.defaultFormat;
2294
+ }
2295
+ set format(value) {
2296
+ this._format = value;
1801
2297
  }
1802
2298
  call(method, args, options) {
1803
2299
  const callId = this.outbound.nextId();
@@ -1906,8 +2402,16 @@ var RpcPeer = class {
1906
2402
  }
1907
2403
  const method = message.Method ?? "";
1908
2404
  if (method === RpcSystemCalls.handshake) {
1909
- const handshake = args[0];
1910
- if (handshake !== void 0) this._onHandshakeReceived(handshake);
2405
+ const raw = args[0];
2406
+ if (raw !== void 0) {
2407
+ const handshake = Array.isArray(raw) ? {
2408
+ RemotePeerId: raw[0],
2409
+ RemoteHubId: raw[2],
2410
+ ProtocolVersion: raw[3],
2411
+ Index: raw[4]
2412
+ } : raw;
2413
+ this._onHandshakeReceived(handshake);
2414
+ }
1911
2415
  return;
1912
2416
  }
1913
2417
  if (method.startsWith("$sys")) {
@@ -1997,6 +2501,12 @@ var RpcClientPeer = class extends RpcPeer {
1997
2501
  _connectionKind = 0 /* Disconnected */;
1998
2502
  _tryIndex = 0;
1999
2503
  _handshakeIndex = 0;
2504
+ /** Index from the MOST RECENTLY RECEIVED server handshake — sent back
2505
+ * to the server in `$sys.Reconnect:3` so the server can verify we're
2506
+ * talking about the current connection. Mirrors .NET's
2507
+ * `connectionState.Handshake.Index` path in
2508
+ * `RpcOutboundCallTracker.TryReconnect`. */
2509
+ _remoteHandshakeIndex = 0;
2000
2510
  _lastRemoteHubId;
2001
2511
  _pendingHandshake;
2002
2512
  _reconnectsAt = 0;
@@ -2023,24 +2533,24 @@ var RpcClientPeer = class extends RpcPeer {
2023
2533
  this._reconnectsAt = value;
2024
2534
  this.reconnectsAtChanged.trigger();
2025
2535
  }
2026
- _format;
2027
2536
  constructor(hub, url, serializationFormat) {
2028
2537
  super(url, hub);
2029
2538
  this.clientId = guidToBase64Url(this.id);
2030
2539
  const resolver = RpcSerializationFormatResolver.Default;
2031
2540
  this.serializationFormat = serializationFormat ?? resolver.defaultFormatKey;
2032
- this._format = resolver.get(this.serializationFormat);
2033
- }
2034
- get format() {
2035
- return this._format;
2541
+ this.format = resolver.get(this.serializationFormat);
2036
2542
  }
2037
2543
  get connectionKind() {
2038
2544
  return this._connectionKind;
2039
2545
  }
2040
- /** One-shot connection for tests — no reconnection loop, no handshake. */
2041
- connectWith(conn) {
2546
+ /** One-shot connection for tests — no reconnection loop, no handshake.
2547
+ * @param isPeerChanged Whether to treat this as a peer-identity change
2548
+ * (default `true` — no handshake was exchanged, so stage-3 compute calls
2549
+ * self-invalidate and remote/shared objects are disposed). Pass `false`
2550
+ * to simulate a same-peer reconnect in tests. */
2551
+ connectWith(conn, isPeerChanged = true) {
2042
2552
  this.setupConnection(conn);
2043
- this._reconnect(true);
2553
+ void this._reconnect(isPeerChanged);
2044
2554
  }
2045
2555
  /** Start the reconnection loop — runs until disposed. */
2046
2556
  async run(wsFactory) {
@@ -2084,6 +2594,7 @@ var RpcClientPeer = class extends RpcPeer {
2084
2594
  closedRejection
2085
2595
  ]);
2086
2596
  this._pendingHandshake = void 0;
2597
+ this._remoteHandshakeIndex = remoteHandshake.Index ?? 0;
2087
2598
  const remoteHubId = remoteHandshake.RemoteHubId;
2088
2599
  let isPeerChanged = false;
2089
2600
  if (remoteHubId !== void 0) {
@@ -2097,7 +2608,7 @@ var RpcClientPeer = class extends RpcPeer {
2097
2608
  this._connection = conn;
2098
2609
  this._connectionKind = 2 /* Connected */;
2099
2610
  this._tryIndex = 0;
2100
- this._reconnect(isPeerChanged);
2611
+ await this._reconnect(isPeerChanged);
2101
2612
  await new Promise((r) => conn.closed.add(() => r()));
2102
2613
  } catch {
2103
2614
  }
@@ -2122,42 +2633,129 @@ var RpcClientPeer = class extends RpcPeer {
2122
2633
  }
2123
2634
  }
2124
2635
  }
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) {
2636
+ /**
2637
+ * Reconcile outbound calls after reconnection, then flush pending sends.
2638
+ *
2639
+ * On peer change (different server hubId) or with binary wire format —
2640
+ * we blind-resend every eligible call.
2641
+ *
2642
+ * On a same-peer reconnect with JSON transport, first call
2643
+ * `$sys.Reconnect` to ask the server which call IDs it no longer
2644
+ * recognizes (e.g. inbound tracker was cleared, or the call never
2645
+ * reached the server). Only those get resent; the rest stay in the
2646
+ * tracker awaiting their $sys.Ok. This prevents duplicate server-side
2647
+ * invocation of in-flight streaming calls on every transient
2648
+ * reconnect — mirrors .NET `RpcOutboundCallTracker.Reconnect` at
2649
+ * src/ActualLab.Rpc/Infrastructure/RpcCallTrackers.cs:209-279.
2650
+ *
2651
+ * Stage-3 compute calls (result already resolved, awaiting invalidation)
2652
+ * are always self-invalidated — TS doesn't track compute-call stages
2653
+ * across reconnect, so the safest behavior is to force a fresh compute.
2654
+ */
2655
+ async _reconnect(isPeerChanged) {
2133
2656
  if (this._connection === void 0) return;
2134
- if (_isPeerChanged) {
2657
+ if (isPeerChanged) {
2135
2658
  this.remoteObjects.disconnectAll();
2659
+ this.sharedObjects.disconnectAll();
2136
2660
  } else {
2137
2661
  this.remoteObjects.reconnectAll();
2138
2662
  }
2139
- const trackerCalls = [...this.outbound.values()];
2140
- for (const call of trackerCalls) {
2663
+ const eligible = [];
2664
+ for (const call of [...this.outbound.values()]) {
2141
2665
  const mode = call.remoteExecutionMode;
2142
2666
  if (!(mode & 2)) {
2143
2667
  if (!call.result.isCompleted)
2144
- call.result.reject(new Error("Outbound call failed: disconnected and AllowReconnect is not set."));
2668
+ call.result.reject(new Error(
2669
+ "Outbound call failed: disconnected and AllowReconnect is not set."
2670
+ ));
2145
2671
  call.onDisconnect();
2146
2672
  this.outbound.remove(call.callId);
2147
- } else if (_isPeerChanged && !(mode & 4)) {
2673
+ continue;
2674
+ }
2675
+ if (isPeerChanged && !(mode & 4)) {
2148
2676
  if (!call.result.isCompleted)
2149
- call.result.reject(new Error("Outbound call failed: reconnected to a different peer and AllowResend is not set."));
2677
+ call.result.reject(new Error(
2678
+ "Outbound call failed: reconnected to a different peer and AllowResend is not set."
2679
+ ));
2150
2680
  call.onDisconnect();
2151
2681
  this.outbound.remove(call.callId);
2152
- } else if (!call.removeOnOk && call.result.isCompleted) {
2682
+ continue;
2683
+ }
2684
+ eligible.push(call);
2685
+ }
2686
+ let unknownIds = null;
2687
+ if (!isPeerChanged && eligible.length > 0) {
2688
+ unknownIds = await this._reconcileReconnect(eligible);
2689
+ }
2690
+ for (const call of eligible) {
2691
+ if (!call.removeOnOk && call.result.isCompleted) {
2153
2692
  call.onDisconnect();
2154
2693
  this.outbound.remove(call.callId);
2155
- } else {
2156
- this._sendWireData(call.serializedWireData);
2694
+ continue;
2695
+ }
2696
+ if (unknownIds !== null && !unknownIds.has(call.callId)) {
2697
+ continue;
2157
2698
  }
2699
+ this._sendWireData(call.serializedWireData);
2158
2700
  }
2159
2701
  this._flushPendingSends();
2160
2702
  }
2703
+ /**
2704
+ * Ask the server which call IDs in `eligible` it no longer recognizes
2705
+ * via `$sys.Reconnect:3`. Returns the set of call IDs that need to be
2706
+ * resent. On any failure, falls back to "resend everything".
2707
+ *
2708
+ * Wire format of the `completedStages` argument is chosen per wire format:
2709
+ * - JSON (json5np): plain object `{"<stage>": "<base64-of-bytes>"}` —
2710
+ * matches .NET `System.Text.Json`'s default for `Dictionary<int, byte[]>`.
2711
+ * - MessagePack (msgpack6/msgpack6c): `Map<number, Uint8Array>` —
2712
+ * the patched encoder (see `./msgpack-map-patch.ts`) writes this as a
2713
+ * native msgpack `map<int, bin>`, matching .NET MessagePack-CSharp's
2714
+ * default for `Dictionary<int, byte[]>`.
2715
+ */
2716
+ async _reconcileReconnect(eligible) {
2717
+ try {
2718
+ const byStage = /* @__PURE__ */ new Map();
2719
+ for (const call of eligible) {
2720
+ const stage = call.getReconnectStage(false);
2721
+ if (stage === null) continue;
2722
+ let arr = byStage.get(stage);
2723
+ if (!arr) {
2724
+ arr = [];
2725
+ byStage.set(stage, arr);
2726
+ }
2727
+ arr.push(call.callId);
2728
+ }
2729
+ if (byStage.size === 0) {
2730
+ return new Set(eligible.map((c) => c.callId));
2731
+ }
2732
+ const isBinary = this.format.isBinary;
2733
+ const stagesArg = isBinary ? /* @__PURE__ */ new Map() : {};
2734
+ for (const [stage, ids] of byStage) {
2735
+ ids.sort((a, b) => a - b);
2736
+ const bytes2 = IncreasingSeqCompressor.serialize(ids);
2737
+ if (isBinary) {
2738
+ stagesArg.set(stage, bytes2);
2739
+ } else {
2740
+ stagesArg[String(stage)] = base64Encode(bytes2);
2741
+ }
2742
+ }
2743
+ const reconnectCall = this.call(
2744
+ RpcSystemCalls.reconnect,
2745
+ [this._remoteHandshakeIndex, stagesArg],
2746
+ {
2747
+ remoteExecutionMode: 1
2748
+ /* AwaitForConnection only; must not itself reconnect */
2749
+ }
2750
+ );
2751
+ const result = await reconnectCall.result.promise;
2752
+ const bytes = _toBytes2(result);
2753
+ if (bytes === null) return new Set(eligible.map((c) => c.callId));
2754
+ return new Set(IncreasingSeqCompressor.deserialize(bytes));
2755
+ } catch {
2756
+ return new Set(eligible.map((c) => c.callId));
2757
+ }
2758
+ }
2161
2759
  _onHandshakeReceived(handshake) {
2162
2760
  this._pendingHandshake?.resolve(handshake);
2163
2761
  }
@@ -2205,6 +2803,17 @@ function guidToBase64Url(uuid) {
2205
2803
  const binary = String.fromCharCode(...bytes);
2206
2804
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2207
2805
  }
2806
+ function _toBytes2(value) {
2807
+ if (value instanceof Uint8Array) return value;
2808
+ if (typeof value === "string") {
2809
+ try {
2810
+ return base64Decode(value);
2811
+ } catch {
2812
+ return null;
2813
+ }
2814
+ }
2815
+ return null;
2816
+ }
2208
2817
 
2209
2818
  // src/rpc-service-host.ts
2210
2819
  var RpcServiceHost = class {
@@ -3208,7 +3817,9 @@ var RpcPeerStateMonitor = class {
3208
3817
  ARG_DELIMITER,
3209
3818
  ENVELOPE_DELIMITER,
3210
3819
  FRAME_DELIMITER,
3820
+ IncreasingSeqCompressor,
3211
3821
  RPC_CLOSE_CODE_UNSUPPORTED_FORMAT,
3822
+ RpcCallStage,
3212
3823
  RpcCallTypeId,
3213
3824
  RpcClientPeer,
3214
3825
  RpcClientPeerReconnectDelayer,