@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.js CHANGED
@@ -23,9 +23,45 @@ var FRAME_DELIMITER = "\n";
23
23
 
24
24
  // src/rpc-serialization.ts
25
25
  import {
26
- Encoder,
26
+ Encoder as Encoder2,
27
27
  Decoder
28
28
  } from "@msgpack/msgpack";
29
+
30
+ // src/msgpack-map-patch.ts
31
+ import { Encoder } from "@msgpack/msgpack";
32
+ function patchMsgpackEncoderForMaps() {
33
+ const proto = Encoder.prototype;
34
+ if (proto._hasMapPatch) return;
35
+ proto._hasMapPatch = true;
36
+ const originalDoEncode = proto.doEncode;
37
+ proto.doEncode = function(object, depth) {
38
+ if (object instanceof Map) {
39
+ _encodeMapInstance.call(this, object, depth);
40
+ return;
41
+ }
42
+ originalDoEncode.call(this, object, depth);
43
+ };
44
+ function _encodeMapInstance(m, depth) {
45
+ const self = this;
46
+ const size = m.size;
47
+ if (size < 16) {
48
+ self.writeU8(128 + size);
49
+ } else if (size < 65536) {
50
+ self.writeU8(222);
51
+ self.writeU16(size);
52
+ } else {
53
+ self.writeU8(223);
54
+ self.writeU32(size);
55
+ }
56
+ for (const [key, value] of m) {
57
+ self.doEncode(key, depth + 1);
58
+ self.doEncode(value, depth + 1);
59
+ }
60
+ }
61
+ }
62
+ patchMsgpackEncoderForMaps();
63
+
64
+ // src/rpc-serialization.ts
29
65
  function serializeMessage(message, args) {
30
66
  const envelope = JSON.stringify(message);
31
67
  if (args === void 0 || args.length === 0) {
@@ -66,7 +102,7 @@ function deserializeMessage(raw) {
66
102
  var textEncoder = new TextEncoder();
67
103
  var textDecoder = new TextDecoder();
68
104
  var INITIAL_ENCODER_BUFFER_SIZE = 32 * 1024;
69
- var defaultBinaryEncoder = new Encoder(
105
+ var defaultBinaryEncoder = new Encoder2(
70
106
  void 0,
71
107
  void 0,
72
108
  void 0,
@@ -74,7 +110,7 @@ var defaultBinaryEncoder = new Encoder(
74
110
  );
75
111
  var defaultBinaryDecoder = new Decoder();
76
112
  function createBinaryEncoder() {
77
- return new Encoder(
113
+ return new Encoder2(
78
114
  void 0,
79
115
  void 0,
80
116
  void 0,
@@ -560,6 +596,21 @@ function defineRpcService(name, methods) {
560
596
 
561
597
  // src/rpc-call-tracker.ts
562
598
  import { PromiseSource as PromiseSource2 } from "@actuallab/core";
599
+
600
+ // src/rpc-call-stage.ts
601
+ var RpcCallStage = {
602
+ /** The call's result has been received from the peer. */
603
+ ResultReady: 1,
604
+ /** The compute call's result has been invalidated (compute-call specific). */
605
+ Invalidated: 3,
606
+ /**
607
+ * Bit flag added on top of ResultReady/Invalidated when the outbound
608
+ * call has been removed from the tracker (unregistered).
609
+ */
610
+ Unregistered: 4096
611
+ };
612
+
613
+ // src/rpc-call-tracker.ts
563
614
  var RpcOutboundCall = class {
564
615
  callId;
565
616
  method;
@@ -571,11 +622,35 @@ var RpcOutboundCall = class {
571
622
  removeOnOk = true;
572
623
  /** The serialized wire data — stored for re-sending on reconnect. */
573
624
  serializedWireData = "";
625
+ /**
626
+ * Bitfield of {@link RpcCallStage} flags indicating how far this call
627
+ * has progressed. Used by the `$sys.Reconnect` protocol to tell the
628
+ * remote peer which calls are still pending its attention.
629
+ *
630
+ * - 0: brand new, no result yet (the peer is expected to be actively processing).
631
+ * - `ResultReady` (1): `$sys.Ok` received for this call.
632
+ * - `Invalidated` (3): result was invalidated (compute calls only).
633
+ * - `Unregistered` (0x1000): removed from the tracker.
634
+ */
635
+ completedStage = 0;
574
636
  constructor(callId, method, remoteExecutionMode = 7) {
575
637
  this.callId = callId;
576
638
  this.method = method;
577
639
  this.remoteExecutionMode = remoteExecutionMode;
578
640
  }
641
+ /**
642
+ * Returns the stage to report to the remote peer via `$sys.Reconnect`,
643
+ * or `null` if this call should not be reconciled and must be aborted.
644
+ *
645
+ * Mirrors .NET `RpcOutboundCall.GetReconnectStage` at
646
+ * src/ActualLab.Rpc/Infrastructure/RpcOutboundCall.cs.
647
+ */
648
+ getReconnectStage(isPeerChanged) {
649
+ const mode = this.remoteExecutionMode;
650
+ if (!(mode & RpcRemoteExecutionMode.AllowReconnect)) return null;
651
+ if (isPeerChanged && !(mode & RpcRemoteExecutionMode.AllowResend)) return null;
652
+ return this.completedStage;
653
+ }
579
654
  /** Called when the connection is lost. Subclasses can override to resolve invalidation promises. */
580
655
  onDisconnect() {
581
656
  }
@@ -597,7 +672,10 @@ var RpcOutboundCallTracker = class {
597
672
  }
598
673
  remove(callId) {
599
674
  const call = this._calls.get(callId);
600
- if (call !== void 0) this._calls.delete(callId);
675
+ if (call !== void 0) {
676
+ this._calls.delete(callId);
677
+ call.completedStage |= RpcCallStage.Unregistered;
678
+ }
601
679
  return call;
602
680
  }
603
681
  values() {
@@ -660,6 +738,97 @@ var RpcInboundCallTracker = class {
660
738
  }
661
739
  };
662
740
 
741
+ // src/increasing-seq-compressor.ts
742
+ var IncreasingSeqCompressor = {
743
+ /**
744
+ * Serialize a non-decreasing sequence of non-negative integers as
745
+ * LEB128-encoded deltas.
746
+ *
747
+ * Throws `RangeError` if the input is not monotonically non-decreasing,
748
+ * or if a value exceeds `Number.MAX_SAFE_INTEGER`.
749
+ */
750
+ serialize(values) {
751
+ const deltas = [];
752
+ let last = 0;
753
+ let totalBytes = 0;
754
+ for (const value of values) {
755
+ if (!Number.isSafeInteger(value) || value < 0)
756
+ throw new RangeError(
757
+ `IncreasingSeqCompressor: value must be a non-negative safe integer (got ${value}).`
758
+ );
759
+ const delta = value - last;
760
+ if (delta < 0)
761
+ throw new RangeError(
762
+ `IncreasingSeqCompressor: sequence must be non-decreasing (saw ${value} after ${last}).`
763
+ );
764
+ deltas.push(delta);
765
+ last = value;
766
+ totalBytes += _varUintByteLength(delta);
767
+ }
768
+ const out = new Uint8Array(totalBytes);
769
+ let offset = 0;
770
+ for (const delta of deltas) {
771
+ offset = _writeVarUint(out, delta, offset);
772
+ }
773
+ return out;
774
+ },
775
+ /**
776
+ * Decode bytes produced by {@link serialize} back into the original
777
+ * sorted sequence of non-negative integers.
778
+ */
779
+ deserialize(data) {
780
+ const result = [];
781
+ let last = 0;
782
+ let offset = 0;
783
+ while (offset < data.length) {
784
+ const [delta, next] = _readVarUint(data, offset);
785
+ offset = next;
786
+ last += delta;
787
+ result.push(last);
788
+ }
789
+ return result;
790
+ }
791
+ };
792
+ var LOW_BITS = 127;
793
+ var HIGH_BIT = 128;
794
+ function _varUintByteLength(value) {
795
+ let n = 0;
796
+ let v = value;
797
+ while (v >= HIGH_BIT) {
798
+ n++;
799
+ v = Math.floor(v / 128);
800
+ }
801
+ return n + 1;
802
+ }
803
+ function _writeVarUint(out, value, offset) {
804
+ let v = value;
805
+ while (v >= HIGH_BIT) {
806
+ out[offset++] = (HIGH_BIT | v & LOW_BITS) & 255;
807
+ v = Math.floor(v / 128);
808
+ }
809
+ out[offset++] = v & 255;
810
+ return offset;
811
+ }
812
+ function _readVarUint(data, offset) {
813
+ let value = 0;
814
+ let shift = 0;
815
+ while (shift < 63) {
816
+ if (offset >= data.length)
817
+ throw new RangeError("IncreasingSeqCompressor: truncated VarUInt.");
818
+ const b2 = data[offset++];
819
+ value += (b2 & LOW_BITS) * Math.pow(2, shift);
820
+ if (b2 <= LOW_BITS) return [value, offset];
821
+ shift += 7;
822
+ }
823
+ if (offset >= data.length)
824
+ throw new RangeError("IncreasingSeqCompressor: truncated VarUInt.");
825
+ const b = data[offset++];
826
+ if (b > 1)
827
+ throw new RangeError("IncreasingSeqCompressor: VarUInt value exceeds 64 bits.");
828
+ value += b * Math.pow(2, 63);
829
+ return [value, offset];
830
+ }
831
+
663
832
  // src/rpc-stream.ts
664
833
  import Denque from "denque";
665
834
  import { PromiseSource as PromiseSource4 } from "@actuallab/core";
@@ -668,7 +837,7 @@ import { PromiseSource as PromiseSource4 } from "@actuallab/core";
668
837
  var RpcObjectKind = { Local: 0, Remote: 1 };
669
838
 
670
839
  // src/rpc-stream-sender.ts
671
- import { PromiseSource as PromiseSource3 } from "@actuallab/core";
840
+ import { PromiseSource as PromiseSource3, RingBuffer } from "@actuallab/core";
672
841
  var DEFAULT_ACK_PERIOD = 256;
673
842
  var DEFAULT_ACK_ADVANCE = 128;
674
843
  var RpcStreamSender = class _RpcStreamSender {
@@ -681,15 +850,29 @@ var RpcStreamSender = class _RpcStreamSender {
681
850
  peer;
682
851
  ackPeriod;
683
852
  ackAdvance;
853
+ /**
854
+ * If true, the source was produced by a factory `(abortSignal) => AsyncIterable<T>`
855
+ * and may honor the AbortSignal to exit gracefully. `disconnect()` first aborts
856
+ * the signal and gives the source up to `RpcStream.disconnectGracePeriodMs` ms
857
+ * before force-closing via `iterator.return()`.
858
+ *
859
+ * If false, the source is a plain AsyncIterable that cannot observe the signal,
860
+ * so `disconnect()` force-closes immediately.
861
+ */
862
+ sourceUsesAbortSignal;
863
+ // -- Pump state --
864
+ /** Next stream index that will be sent (matches .NET's `index`). */
684
865
  _nextIndex = 0;
685
866
  _ended = false;
686
867
  _started = new PromiseSource3();
868
+ /** Highest `nextIndex` observed in an ACK so far (matches .NET's `lastAckNextIndex` idiom). */
687
869
  _lastAckedIndex = 0;
688
- _ackWaiting = null;
689
- _resetRequested = false;
690
870
  _abortController = new AbortController();
691
871
  _iterator = null;
692
- constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true, isRealTime = false, canSkipTo = () => true) {
872
+ // -- ACK queue (analog of .NET's Channel<(long, bool)>) --
873
+ _acks = [];
874
+ _whenAckReady = null;
875
+ constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true, isRealTime = false, canSkipTo = () => true, sourceUsesAbortSignal = false) {
693
876
  const localId = peer.sharedObjects.nextId();
694
877
  this.id = { hostId: peer.hub.hubId, localId };
695
878
  this.allowReconnect = allowReconnect;
@@ -698,6 +881,7 @@ var RpcStreamSender = class _RpcStreamSender {
698
881
  this.peer = peer;
699
882
  this.ackPeriod = ackPeriod;
700
883
  this.ackAdvance = ackAdvance;
884
+ this.sourceUsesAbortSignal = sourceUsesAbortSignal;
701
885
  }
702
886
  /** AbortSignal that is aborted when the sender is disconnected. */
703
887
  get abortSignal() {
@@ -707,56 +891,61 @@ var RpcStreamSender = class _RpcStreamSender {
707
891
  toRef() {
708
892
  return `${this.id.hostId},${this.id.localId},${this.ackPeriod},${this.ackAdvance},${this.allowReconnect ? "1" : "0"},${this.isRealTime ? "1" : "0"}`;
709
893
  }
710
- /** Called by system call handler when $sys.Ack is received from the client. */
894
+ /**
895
+ * Called by system call handler when $sys.Ack is received from the client.
896
+ *
897
+ * Mirrors .NET `RpcSharedStream<T>.OnAck` at
898
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:68-101.
899
+ */
711
900
  onAck(nextIndex, hostId) {
712
901
  const mustReset = hostId !== "" && hostId !== _RpcStreamSender._emptyGuid;
713
- if (!this._started.isCompleted) {
714
- this._started.resolve();
715
- }
716
- if (mustReset && this.isRealTime) {
717
- this._nextIndex = nextIndex;
718
- this._lastAckedIndex = nextIndex;
719
- this._resetRequested = true;
720
- } else {
721
- if (mustReset || nextIndex > this._lastAckedIndex) {
722
- this._lastAckedIndex = nextIndex;
723
- }
724
- }
725
- if (this._ackWaiting) {
726
- this._ackWaiting.resolve();
727
- this._ackWaiting = null;
902
+ if (!this._started.isCompleted) this._started.resolve();
903
+ this._acks.push({ nextIndex, mustReset });
904
+ if (this._whenAckReady) {
905
+ this._whenAckReady.resolve();
906
+ this._whenAckReady = null;
728
907
  }
729
908
  }
730
909
  /** Called by system call handler when $sys.AckEnd is received from the client. */
731
910
  onAckEnd(_hostId) {
732
911
  this.disconnect();
733
912
  }
734
- /** Send a single item to the client. */
913
+ /**
914
+ * Send a single item to the client at the current `_nextIndex`.
915
+ *
916
+ * Advances `_nextIndex` unconditionally — even if the peer is disconnected
917
+ * the item is considered "sent" from the pump's perspective so the replay
918
+ * buffer indexing stays consistent. If a connection is absent, the wire
919
+ * message is dropped; the item will be resent from the replay buffer
920
+ * after a reconnect (client sends `Ack(N, mustReset=true)`).
921
+ */
735
922
  sendItem(item) {
736
923
  if (this._ended) return;
737
924
  const conn = this.peer.connection;
738
- if (!conn) return;
739
- this.peer.hub.systemCallSender.item(
740
- conn,
741
- this.peer.format,
742
- this.id.localId,
743
- this._nextIndex,
744
- item
745
- );
925
+ if (conn) {
926
+ this.peer.hub.systemCallSender.item(
927
+ conn,
928
+ this.peer.format,
929
+ this.id.localId,
930
+ this._nextIndex,
931
+ item
932
+ );
933
+ }
746
934
  this._nextIndex++;
747
935
  }
748
- /** Send a batch of items to the client. */
936
+ /** Send a batch of items to the client. See {@link sendItem} for disconnect semantics. */
749
937
  sendBatch(items) {
750
938
  if (this._ended || items.length === 0) return;
751
939
  const conn = this.peer.connection;
752
- if (!conn) return;
753
- this.peer.hub.systemCallSender.batch(
754
- conn,
755
- this.peer.format,
756
- this.id.localId,
757
- this._nextIndex,
758
- items
759
- );
940
+ if (conn) {
941
+ this.peer.hub.systemCallSender.batch(
942
+ conn,
943
+ this.peer.format,
944
+ this.id.localId,
945
+ this._nextIndex,
946
+ items
947
+ );
948
+ }
760
949
  this._nextIndex += items.length;
761
950
  }
762
951
  /** Signal stream completion to the client. */
@@ -776,76 +965,270 @@ var RpcStreamSender = class _RpcStreamSender {
776
965
  }
777
966
  /**
778
967
  * Consume an AsyncIterable and send all items to the client.
779
- * Waits for the client's initial Ack before starting to pump items.
780
968
  *
781
- * When `isRealTime` is true, applies flow control based on `ackAdvance`:
782
- * if the sender is `ackAdvance` items ahead of the last ACK, it enters
783
- * skip mode — draining the source and discarding items until it finds
784
- * one where `canSkipTo` returns true.
969
+ * Dispatches to the non-real-time or real-time pump, matching .NET
970
+ * `RpcSharedStream<T>.OnRun` at
971
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:105-108.
785
972
  */
786
973
  async writeFrom(source) {
787
974
  await this._started.promise;
975
+ if (this._ended) return;
788
976
  const iterator = source[Symbol.asyncIterator]();
789
977
  this._iterator = iterator;
978
+ const state = { iteratorDone: false };
790
979
  try {
791
- for (; ; ) {
792
- const next = await iterator.next();
793
- if (next.done || this._ended) break;
794
- const item = next.value;
795
- if (this._resetRequested) {
796
- if (!this.canSkipTo(item)) continue;
797
- this._resetRequested = false;
798
- this.sendItem(item);
799
- continue;
980
+ if (this.isRealTime) {
981
+ await this._runRealTime(iterator, state);
982
+ } else {
983
+ await this._runNonRealTime(iterator, state);
984
+ }
985
+ } finally {
986
+ if (state.iteratorDone && this._iterator === iterator) {
987
+ this._iterator = null;
988
+ }
989
+ }
990
+ }
991
+ /**
992
+ * Non-real-time pump — back-pressure through buffered ACKs.
993
+ *
994
+ * Direct port of .NET `OnRunNonRealTime` at
995
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:110-217.
996
+ */
997
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition -- _ended changes across awaits */
998
+ async _runNonRealTime(iterator, state) {
999
+ const buffer = new RingBuffer(this.ackAdvance + 1);
1000
+ let bufferStart = 0;
1001
+ let isFullyBuffered = false;
1002
+ while (true) {
1003
+ let ack = this._tryProcessAcks();
1004
+ if (!ack) {
1005
+ await this._waitAckReady();
1006
+ if (this._ended) return;
1007
+ ack = this._tryProcessAcks();
1008
+ if (!ack) {
1009
+ return;
800
1010
  }
801
- if (this._nextIndex < this._lastAckedIndex + this.ackAdvance) {
802
- this.sendItem(item);
1011
+ }
1012
+ this._lastAckedIndex = ack.nextIndex;
1013
+ const shift = _clamp(ack.nextIndex - bufferStart, 0, buffer.count);
1014
+ if (shift > 0) {
1015
+ buffer.moveHead(shift);
1016
+ bufferStart += shift;
1017
+ }
1018
+ if (this._nextIndex < bufferStart) {
1019
+ if (!this._ended)
1020
+ this.sendEnd(new Error("Stream position unavailable."));
1021
+ return;
1022
+ }
1023
+ let bufferIndex = this._nextIndex - bufferStart;
1024
+ const maxIndex = ack.nextIndex + this.ackAdvance;
1025
+ while (this._nextIndex < maxIndex) {
1026
+ if (this._ended) return;
1027
+ if (bufferIndex < buffer.count) {
1028
+ const item = buffer.get(bufferIndex++);
1029
+ if (item.kind === "value") {
1030
+ this.sendItem(item.value);
1031
+ } else if (item.kind === "end") {
1032
+ if (!this._ended) this.sendEnd();
1033
+ return;
1034
+ } else {
1035
+ if (!this._ended) this.sendEnd(item.error);
1036
+ return;
1037
+ }
803
1038
  continue;
804
1039
  }
805
- if (!this.isRealTime) {
806
- await this._waitForAckBudget();
1040
+ if (isFullyBuffered) break;
1041
+ if (!buffer.hasRemainingCapacity) break;
1042
+ let pulled;
1043
+ try {
1044
+ const r = await iterator.next();
807
1045
  if (this._ended) return;
808
- this.sendItem(item);
809
- continue;
1046
+ if (r.done) {
1047
+ pulled = _endItem;
1048
+ isFullyBuffered = true;
1049
+ state.iteratorDone = true;
1050
+ } else {
1051
+ pulled = { kind: "value", value: r.value };
1052
+ }
1053
+ } catch (e) {
1054
+ state.iteratorDone = true;
1055
+ isFullyBuffered = true;
1056
+ pulled = {
1057
+ kind: "error",
1058
+ error: e instanceof Error ? e : new Error(String(e))
1059
+ };
810
1060
  }
811
- let latestSkipTarget = this.canSkipTo(item) ? item : void 0;
812
- let sourceExhausted = false;
813
- while (this._nextIndex >= this._lastAckedIndex + this.ackAdvance) {
1061
+ buffer.pushTail(pulled);
1062
+ }
1063
+ }
1064
+ }
1065
+ /**
1066
+ * Real-time pump — drops stale items to stay close to the source's head.
1067
+ *
1068
+ * Direct port of .NET `OnRunRealTime` at
1069
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:219-420.
1070
+ */
1071
+ async _runRealTime(iterator, state) {
1072
+ const buffer = new RingBuffer(this.ackAdvance + 1);
1073
+ let bufferStart = 0;
1074
+ let isFullyBuffered = false;
1075
+ while (true) {
1076
+ let ack = this._tryProcessAcks();
1077
+ if (!ack) {
1078
+ await this._waitAckReady();
1079
+ if (this._ended) return;
1080
+ ack = this._tryProcessAcks();
1081
+ if (!ack) return;
1082
+ }
1083
+ this._lastAckedIndex = ack.nextIndex;
1084
+ if (ack.mustReset && !isFullyBuffered) {
1085
+ buffer.clear();
1086
+ bufferStart = this._nextIndex;
1087
+ while (true) {
814
1088
  if (this._ended) return;
815
- const n = await iterator.next();
816
- if (n.done) {
817
- sourceExhausted = true;
818
- break;
819
- }
820
- if (this.canSkipTo(n.value)) {
821
- latestSkipTarget = n.value;
1089
+ let item;
1090
+ let accepted = false;
1091
+ try {
1092
+ const r = await iterator.next();
1093
+ if (this._ended) return;
1094
+ if (r.done) {
1095
+ item = _endItem;
1096
+ isFullyBuffered = true;
1097
+ state.iteratorDone = true;
1098
+ } else if (this.canSkipTo(r.value)) {
1099
+ item = { kind: "value", value: r.value };
1100
+ accepted = true;
1101
+ } else {
1102
+ continue;
1103
+ }
1104
+ } catch (e) {
1105
+ state.iteratorDone = true;
1106
+ isFullyBuffered = true;
1107
+ item = {
1108
+ kind: "error",
1109
+ error: e instanceof Error ? e : new Error(String(e))
1110
+ };
822
1111
  }
1112
+ buffer.pushTail(item);
1113
+ if (accepted || isFullyBuffered) break;
823
1114
  }
1115
+ }
1116
+ const shift = _clamp(ack.nextIndex - bufferStart, 0, buffer.count);
1117
+ if (shift > 0) {
1118
+ buffer.moveHead(shift);
1119
+ bufferStart += shift;
1120
+ }
1121
+ if (this._nextIndex < bufferStart) {
1122
+ if (!this._ended)
1123
+ this.sendEnd(new Error("Stream position unavailable."));
1124
+ return;
1125
+ }
1126
+ let bufferIndex = this._nextIndex - bufferStart;
1127
+ const maxIndex = ack.nextIndex + this.ackAdvance;
1128
+ while (this._nextIndex < maxIndex) {
824
1129
  if (this._ended) return;
825
- if (latestSkipTarget !== void 0) {
826
- this.sendItem(latestSkipTarget);
1130
+ if (this._acks.length > 0) break;
1131
+ if (bufferIndex < buffer.count) {
1132
+ const item = buffer.get(bufferIndex++);
1133
+ if (item.kind === "value") {
1134
+ this.sendItem(item.value);
1135
+ } else if (item.kind === "end") {
1136
+ if (!this._ended) this.sendEnd();
1137
+ return;
1138
+ } else {
1139
+ if (!this._ended) this.sendEnd(item.error);
1140
+ return;
1141
+ }
1142
+ continue;
827
1143
  }
828
- if (sourceExhausted) {
829
- if (!this._ended) this.sendEnd();
830
- return;
1144
+ if (isFullyBuffered) break;
1145
+ if (!buffer.hasRemainingCapacity) break;
1146
+ let pulled;
1147
+ try {
1148
+ const r = await iterator.next();
1149
+ if (this._ended) return;
1150
+ if (r.done) {
1151
+ pulled = _endItem;
1152
+ isFullyBuffered = true;
1153
+ state.iteratorDone = true;
1154
+ } else {
1155
+ pulled = { kind: "value", value: r.value };
1156
+ }
1157
+ } catch (e) {
1158
+ state.iteratorDone = true;
1159
+ isFullyBuffered = true;
1160
+ pulled = {
1161
+ kind: "error",
1162
+ error: e instanceof Error ? e : new Error(String(e))
1163
+ };
831
1164
  }
1165
+ buffer.pushTail(pulled);
832
1166
  }
833
- if (!this._ended) {
834
- this.sendEnd();
835
- }
836
- } catch (e) {
837
- if (!this._ended) {
838
- this.sendEnd(e instanceof Error ? e : new Error(String(e)));
1167
+ if (this._nextIndex >= maxIndex && !isFullyBuffered && !this._ended) {
1168
+ let latestValue;
1169
+ let haveLatest = false;
1170
+ let exitReason = "ack";
1171
+ let exitError = null;
1172
+ drain: while (!this._ended) {
1173
+ if (this._acks.length > 0) {
1174
+ exitReason = "ack";
1175
+ break drain;
1176
+ }
1177
+ let r;
1178
+ try {
1179
+ r = await iterator.next();
1180
+ } catch (e) {
1181
+ state.iteratorDone = true;
1182
+ isFullyBuffered = true;
1183
+ exitReason = "error";
1184
+ exitError = e instanceof Error ? e : new Error(String(e));
1185
+ break drain;
1186
+ }
1187
+ if (this._ended) return;
1188
+ if (r.done) {
1189
+ state.iteratorDone = true;
1190
+ isFullyBuffered = true;
1191
+ exitReason = "end";
1192
+ break drain;
1193
+ }
1194
+ if (this.canSkipTo(r.value)) {
1195
+ latestValue = r.value;
1196
+ haveLatest = true;
1197
+ }
1198
+ }
1199
+ if (exitReason === "end" || exitReason === "error") {
1200
+ if (haveLatest && !this._ended) this.sendItem(latestValue);
1201
+ if (!this._ended) this.sendEnd(exitError);
1202
+ return;
1203
+ }
1204
+ if (haveLatest) {
1205
+ const wasFull = buffer.isFull;
1206
+ buffer.pushTailAndMoveHeadIfFull({ kind: "value", value: latestValue });
1207
+ if (wasFull) bufferStart++;
1208
+ }
839
1209
  }
840
1210
  }
841
1211
  }
842
- /** Wait until _nextIndex < _lastAckedIndex + ackAdvance. */
843
- async _waitForAckBudget() {
844
- while (this._nextIndex >= this._lastAckedIndex + this.ackAdvance) {
845
- if (this._ended) return;
846
- this._ackWaiting = new PromiseSource3();
847
- await this._ackWaiting.promise;
1212
+ /* eslint-enable @typescript-eslint/no-unnecessary-condition */
1213
+ /** Drain all queued ACKs, returning the most recent one (or null if none). */
1214
+ _tryProcessAcks() {
1215
+ if (this._acks.length === 0) return null;
1216
+ let last = null;
1217
+ while (this._acks.length > 0) {
1218
+ const a = this._acks.shift();
1219
+ last = a;
1220
+ if (a.mustReset || this._nextIndex < a.nextIndex) {
1221
+ this._nextIndex = a.nextIndex;
1222
+ }
848
1223
  }
1224
+ return last;
1225
+ }
1226
+ /** Wait for the ACK queue to have at least one entry (or `_ended`). */
1227
+ async _waitAckReady() {
1228
+ if (this._acks.length > 0 || this._ended) return;
1229
+ this._whenAckReady = new PromiseSource3();
1230
+ await this._whenAckReady.promise;
1231
+ this._whenAckReady = null;
849
1232
  }
850
1233
  // -- IRpcObject --
851
1234
  reconnect() {
@@ -857,17 +1240,38 @@ var RpcStreamSender = class _RpcStreamSender {
857
1240
  if (!this._started.isCompleted) {
858
1241
  this._started.resolve();
859
1242
  }
860
- if (this._ackWaiting) {
861
- this._ackWaiting.resolve();
862
- this._ackWaiting = null;
1243
+ if (this._whenAckReady) {
1244
+ this._whenAckReady.resolve();
1245
+ this._whenAckReady = null;
863
1246
  }
864
- if (this._iterator) {
865
- void this._iterator.return?.(void 0);
1247
+ this.peer.sharedObjects.unregister(this);
1248
+ const iterator = this._iterator;
1249
+ if (!iterator) return;
1250
+ if (!this.sourceUsesAbortSignal) {
866
1251
  this._iterator = null;
1252
+ this._forceCloseIterator(iterator);
1253
+ return;
1254
+ }
1255
+ setTimeout(() => {
1256
+ if (this._iterator === iterator) {
1257
+ this._iterator = null;
1258
+ this._forceCloseIterator(iterator);
1259
+ }
1260
+ }, RpcStream.disconnectGracePeriodMs);
1261
+ }
1262
+ _forceCloseIterator(iterator) {
1263
+ try {
1264
+ const ret = iterator.return?.(void 0);
1265
+ if (ret) void ret.catch(() => {
1266
+ });
1267
+ } catch {
867
1268
  }
868
- this.peer.sharedObjects.unregister(this);
869
1269
  }
870
1270
  };
1271
+ var _endItem = { kind: "end" };
1272
+ function _clamp(value, min, max) {
1273
+ return value < min ? min : value > max ? max : value;
1274
+ }
871
1275
 
872
1276
  // src/rpc-stream.ts
873
1277
  var DEFAULT_ACK_PERIOD2 = 30;
@@ -903,6 +1307,15 @@ function parseStreamRef(value) {
903
1307
  return null;
904
1308
  }
905
1309
  var RpcStream = class _RpcStream {
1310
+ /**
1311
+ * Grace period (ms) given to a factory-form local source to honor its
1312
+ * AbortSignal and exit naturally before the sender force-closes the
1313
+ * iterator via `iterator.return()`.
1314
+ *
1315
+ * Only applies to sources provided as `(abortSignal) => AsyncIterable<T>`.
1316
+ * Plain `AsyncIterable<T>` sources are force-closed immediately on disconnect.
1317
+ */
1318
+ static disconnectGracePeriodMs = 100;
906
1319
  id;
907
1320
  kind;
908
1321
  allowReconnect;
@@ -980,17 +1393,19 @@ var RpcStream = class _RpcStream {
980
1393
  throw new Error("toRef() was already called with a different peer.");
981
1394
  return this._toRefValue;
982
1395
  }
1396
+ const isFactory = typeof this.localSource === "function";
983
1397
  const sender = new RpcStreamSender(
984
1398
  peer,
985
1399
  this.ackPeriod,
986
1400
  this.ackAdvance,
987
1401
  this.allowReconnect,
988
1402
  this.isRealTime,
989
- this.canSkipTo
1403
+ this.canSkipTo,
1404
+ isFactory
990
1405
  );
991
1406
  peer.sharedObjects.register(sender);
992
1407
  this._sender = sender;
993
- const source = typeof this.localSource === "function" ? this.localSource(sender.abortSignal) : this.localSource;
1408
+ const source = isFactory ? this.localSource(sender.abortSignal) : this.localSource;
994
1409
  this._whenSent = sender.writeFrom(source);
995
1410
  this._toRefValue = peer.format.isBinary ? {
996
1411
  SerializedId: [sender.id.hostId, sender.id.localId],
@@ -1200,6 +1615,25 @@ function resolveStreamRefs(value, peer) {
1200
1615
  return value;
1201
1616
  }
1202
1617
 
1618
+ // src/base64.ts
1619
+ function base64Encode(bytes) {
1620
+ const chunkSize = 32768;
1621
+ let binary = "";
1622
+ for (let i = 0; i < bytes.length; i += chunkSize) {
1623
+ binary += String.fromCharCode.apply(
1624
+ null,
1625
+ bytes.subarray(i, Math.min(bytes.length, i + chunkSize))
1626
+ );
1627
+ }
1628
+ return btoa(binary);
1629
+ }
1630
+ function base64Decode(encoded) {
1631
+ const binary = atob(encoded);
1632
+ const out = new Uint8Array(binary.length);
1633
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
1634
+ return out;
1635
+ }
1636
+
1203
1637
  // src/rpc-system-call-handler.ts
1204
1638
  var RpcSystemCallHandler = class {
1205
1639
  handle(message, args, peer) {
@@ -1209,6 +1643,7 @@ var RpcSystemCallHandler = class {
1209
1643
  case RpcSystemCalls.ok: {
1210
1644
  const call = peer.outbound.get(relatedId);
1211
1645
  if (call !== void 0) {
1646
+ call.completedStage |= RpcCallStage.ResultReady;
1212
1647
  if (call.removeOnOk) {
1213
1648
  peer.outbound.remove(relatedId);
1214
1649
  }
@@ -1216,6 +1651,10 @@ var RpcSystemCallHandler = class {
1216
1651
  }
1217
1652
  break;
1218
1653
  }
1654
+ case RpcSystemCalls.reconnect: {
1655
+ this._handleReconnect(relatedId, args, peer);
1656
+ break;
1657
+ }
1219
1658
  case RpcSystemCalls.error: {
1220
1659
  const call = peer.outbound.remove(relatedId);
1221
1660
  if (call !== void 0) {
@@ -1304,7 +1743,54 @@ var RpcSystemCallHandler = class {
1304
1743
  }
1305
1744
  }
1306
1745
  }
1746
+ /**
1747
+ * Server-side handler for the `$sys.Reconnect` call. Parses the peer's
1748
+ * report of in-flight call IDs (grouped by stage, IncreasingSeqCompressor-
1749
+ * encoded, base64-wrapped for JSON transport), computes the subset this
1750
+ * peer does not recognize, and replies with a compressed byte[] of the
1751
+ * unknown IDs wrapped in `$sys.Ok`.
1752
+ *
1753
+ * @param relatedId The outbound call ID the client used for this
1754
+ * `$sys.Reconnect` invocation — we send `$sys.Ok` back
1755
+ * with the same `RelatedId`.
1756
+ */
1757
+ _handleReconnect(relatedId, args, peer) {
1758
+ if (peer.connection === void 0) return;
1759
+ let unknownIds = [];
1760
+ try {
1761
+ const stagesRaw = args[1];
1762
+ if (stagesRaw && typeof stagesRaw === "object" && !Array.isArray(stagesRaw)) {
1763
+ const unknownSet = /* @__PURE__ */ new Set();
1764
+ for (const [, rawValue] of Object.entries(stagesRaw)) {
1765
+ const bytes = _toBytes(rawValue);
1766
+ if (bytes === null) continue;
1767
+ const callIds = IncreasingSeqCompressor.deserialize(bytes);
1768
+ for (const callId of callIds) {
1769
+ if (peer.inbound.get(callId) === void 0) {
1770
+ unknownSet.add(callId);
1771
+ }
1772
+ }
1773
+ }
1774
+ unknownIds = [...unknownSet].sort((a, b) => a - b);
1775
+ }
1776
+ } catch {
1777
+ }
1778
+ const responseBytes = IncreasingSeqCompressor.serialize(unknownIds);
1779
+ const responseValue = peer.format.isBinary ? responseBytes : base64Encode(responseBytes);
1780
+ peer.hub.systemCallSender.ok(peer.connection, peer.format, relatedId, responseValue);
1781
+ }
1307
1782
  };
1783
+ function _toBytes(value) {
1784
+ if (value instanceof Uint8Array) return value;
1785
+ if (typeof value === "string") {
1786
+ try {
1787
+ return base64Decode(value);
1788
+ } catch {
1789
+ return null;
1790
+ }
1791
+ }
1792
+ return null;
1793
+ }
1308
1794
 
1309
1795
  // src/rpc-system-call-sender.ts
1310
1796
  var RpcSystemCallSender = class _RpcSystemCallSender {
@@ -1695,9 +2181,17 @@ var RpcPeer = class {
1695
2181
  this._startKeepAlive();
1696
2182
  });
1697
2183
  }
1698
- /** The serialization format used by this peer. Override in subclasses. */
2184
+ /**
2185
+ * The serialization format used by this peer for outgoing messages.
2186
+ * If not set explicitly, defaults to the resolver's default (JSON /
2187
+ * json5np). Subclasses can pre-assign in their constructor.
2188
+ */
2189
+ _format;
1699
2190
  get format() {
1700
- return RpcSerializationFormatResolver.Default.defaultFormat;
2191
+ return this._format ?? RpcSerializationFormatResolver.Default.defaultFormat;
2192
+ }
2193
+ set format(value) {
2194
+ this._format = value;
1701
2195
  }
1702
2196
  call(method, args, options) {
1703
2197
  const callId = this.outbound.nextId();
@@ -1806,8 +2300,16 @@ var RpcPeer = class {
1806
2300
  }
1807
2301
  const method = message.Method ?? "";
1808
2302
  if (method === RpcSystemCalls.handshake) {
1809
- const handshake = args[0];
1810
- if (handshake !== void 0) this._onHandshakeReceived(handshake);
2303
+ const raw = args[0];
2304
+ if (raw !== void 0) {
2305
+ const handshake = Array.isArray(raw) ? {
2306
+ RemotePeerId: raw[0],
2307
+ RemoteHubId: raw[2],
2308
+ ProtocolVersion: raw[3],
2309
+ Index: raw[4]
2310
+ } : raw;
2311
+ this._onHandshakeReceived(handshake);
2312
+ }
1811
2313
  return;
1812
2314
  }
1813
2315
  if (method.startsWith("$sys")) {
@@ -1897,6 +2399,12 @@ var RpcClientPeer = class extends RpcPeer {
1897
2399
  _connectionKind = 0 /* Disconnected */;
1898
2400
  _tryIndex = 0;
1899
2401
  _handshakeIndex = 0;
2402
+ /** Index from the MOST RECENTLY RECEIVED server handshake — sent back
2403
+ * to the server in `$sys.Reconnect:3` so the server can verify we're
2404
+ * talking about the current connection. Mirrors .NET's
2405
+ * `connectionState.Handshake.Index` path in
2406
+ * `RpcOutboundCallTracker.TryReconnect`. */
2407
+ _remoteHandshakeIndex = 0;
1900
2408
  _lastRemoteHubId;
1901
2409
  _pendingHandshake;
1902
2410
  _reconnectsAt = 0;
@@ -1923,24 +2431,24 @@ var RpcClientPeer = class extends RpcPeer {
1923
2431
  this._reconnectsAt = value;
1924
2432
  this.reconnectsAtChanged.trigger();
1925
2433
  }
1926
- _format;
1927
2434
  constructor(hub, url, serializationFormat) {
1928
2435
  super(url, hub);
1929
2436
  this.clientId = guidToBase64Url(this.id);
1930
2437
  const resolver = RpcSerializationFormatResolver.Default;
1931
2438
  this.serializationFormat = serializationFormat ?? resolver.defaultFormatKey;
1932
- this._format = resolver.get(this.serializationFormat);
1933
- }
1934
- get format() {
1935
- return this._format;
2439
+ this.format = resolver.get(this.serializationFormat);
1936
2440
  }
1937
2441
  get connectionKind() {
1938
2442
  return this._connectionKind;
1939
2443
  }
1940
- /** One-shot connection for tests — no reconnection loop, no handshake. */
1941
- connectWith(conn) {
2444
+ /** One-shot connection for tests — no reconnection loop, no handshake.
2445
+ * @param isPeerChanged Whether to treat this as a peer-identity change
2446
+ * (default `true` — no handshake was exchanged, so stage-3 compute calls
2447
+ * self-invalidate and remote/shared objects are disposed). Pass `false`
2448
+ * to simulate a same-peer reconnect in tests. */
2449
+ connectWith(conn, isPeerChanged = true) {
1942
2450
  this.setupConnection(conn);
1943
- this._reconnect(true);
2451
+ void this._reconnect(isPeerChanged);
1944
2452
  }
1945
2453
  /** Start the reconnection loop — runs until disposed. */
1946
2454
  async run(wsFactory) {
@@ -1984,6 +2492,7 @@ var RpcClientPeer = class extends RpcPeer {
1984
2492
  closedRejection
1985
2493
  ]);
1986
2494
  this._pendingHandshake = void 0;
2495
+ this._remoteHandshakeIndex = remoteHandshake.Index ?? 0;
1987
2496
  const remoteHubId = remoteHandshake.RemoteHubId;
1988
2497
  let isPeerChanged = false;
1989
2498
  if (remoteHubId !== void 0) {
@@ -1997,7 +2506,7 @@ var RpcClientPeer = class extends RpcPeer {
1997
2506
  this._connection = conn;
1998
2507
  this._connectionKind = 2 /* Connected */;
1999
2508
  this._tryIndex = 0;
2000
- this._reconnect(isPeerChanged);
2509
+ await this._reconnect(isPeerChanged);
2001
2510
  await new Promise((r) => conn.closed.add(() => r()));
2002
2511
  } catch {
2003
2512
  }
@@ -2022,42 +2531,129 @@ var RpcClientPeer = class extends RpcPeer {
2022
2531
  }
2023
2532
  }
2024
2533
  }
2025
- /** Re-send outbound calls after reconnection + flush pending sends.
2026
- * Stage-3 compute calls are always self-invalidated: without $sys.Reconnect
2027
- * protocol, the server's invalidation tracking is lost on disconnect, and
2028
- * re-sending would get a duplicate $sys.Ok that is ignored (PromiseSource
2029
- * already resolved). Self-invalidation forces a fresh recompute that
2030
- * establishes new invalidation tracking on the new connection.
2031
- * Regular in-flight calls are re-sent transparently. */
2032
- _reconnect(_isPeerChanged) {
2534
+ /**
2535
+ * Reconcile outbound calls after reconnection, then flush pending sends.
2536
+ *
2537
+ * On peer change (different server hubId) or with binary wire format —
2538
+ * we blind-resend every eligible call.
2539
+ *
2540
+ * On a same-peer reconnect with JSON transport, first call
2541
+ * `$sys.Reconnect` to ask the server which call IDs it no longer
2542
+ * recognizes (e.g. inbound tracker was cleared, or the call never
2543
+ * reached the server). Only those get resent; the rest stay in the
2544
+ * tracker awaiting their $sys.Ok. This prevents duplicate server-side
2545
+ * invocation of in-flight streaming calls on every transient
2546
+ * reconnect — mirrors .NET `RpcOutboundCallTracker.Reconnect` at
2547
+ * src/ActualLab.Rpc/Infrastructure/RpcCallTrackers.cs:209-279.
2548
+ *
2549
+ * Stage-3 compute calls (result already resolved, awaiting invalidation)
2550
+ * are always self-invalidated — TS doesn't track compute-call stages
2551
+ * across reconnect, so the safest behavior is to force a fresh compute.
2552
+ */
2553
+ async _reconnect(isPeerChanged) {
2033
2554
  if (this._connection === void 0) return;
2034
- if (_isPeerChanged) {
2555
+ if (isPeerChanged) {
2035
2556
  this.remoteObjects.disconnectAll();
2557
+ this.sharedObjects.disconnectAll();
2036
2558
  } else {
2037
2559
  this.remoteObjects.reconnectAll();
2038
2560
  }
2039
- const trackerCalls = [...this.outbound.values()];
2040
- for (const call of trackerCalls) {
2561
+ const eligible = [];
2562
+ for (const call of [...this.outbound.values()]) {
2041
2563
  const mode = call.remoteExecutionMode;
2042
2564
  if (!(mode & 2)) {
2043
2565
  if (!call.result.isCompleted)
2044
- call.result.reject(new Error("Outbound call failed: disconnected and AllowReconnect is not set."));
2566
+ call.result.reject(new Error(
2567
+ "Outbound call failed: disconnected and AllowReconnect is not set."
2568
+ ));
2045
2569
  call.onDisconnect();
2046
2570
  this.outbound.remove(call.callId);
2047
- } else if (_isPeerChanged && !(mode & 4)) {
2571
+ continue;
2572
+ }
2573
+ if (isPeerChanged && !(mode & 4)) {
2048
2574
  if (!call.result.isCompleted)
2049
- call.result.reject(new Error("Outbound call failed: reconnected to a different peer and AllowResend is not set."));
2575
+ call.result.reject(new Error(
2576
+ "Outbound call failed: reconnected to a different peer and AllowResend is not set."
2577
+ ));
2050
2578
  call.onDisconnect();
2051
2579
  this.outbound.remove(call.callId);
2052
- } else if (!call.removeOnOk && call.result.isCompleted) {
2580
+ continue;
2581
+ }
2582
+ eligible.push(call);
2583
+ }
2584
+ let unknownIds = null;
2585
+ if (!isPeerChanged && eligible.length > 0) {
2586
+ unknownIds = await this._reconcileReconnect(eligible);
2587
+ }
2588
+ for (const call of eligible) {
2589
+ if (!call.removeOnOk && call.result.isCompleted) {
2053
2590
  call.onDisconnect();
2054
2591
  this.outbound.remove(call.callId);
2055
- } else {
2056
- this._sendWireData(call.serializedWireData);
2592
+ continue;
2593
+ }
2594
+ if (unknownIds !== null && !unknownIds.has(call.callId)) {
2595
+ continue;
2057
2596
  }
2597
+ this._sendWireData(call.serializedWireData);
2058
2598
  }
2059
2599
  this._flushPendingSends();
2060
2600
  }
2601
+ /**
2602
+ * Ask the server which call IDs in `eligible` it no longer recognizes
2603
+ * via `$sys.Reconnect:3`. Returns the set of call IDs that need to be
2604
+ * resent. On any failure, falls back to "resend everything".
2605
+ *
2606
+ * Wire format of the `completedStages` argument is chosen per wire format:
2607
+ * - JSON (json5np): plain object `{"<stage>": "<base64-of-bytes>"}` —
2608
+ * matches .NET `System.Text.Json`'s default for `Dictionary<int, byte[]>`.
2609
+ * - MessagePack (msgpack6/msgpack6c): `Map<number, Uint8Array>` —
2610
+ * the patched encoder (see `./msgpack-map-patch.ts`) writes this as a
2611
+ * native msgpack `map<int, bin>`, matching .NET MessagePack-CSharp's
2612
+ * default for `Dictionary<int, byte[]>`.
2613
+ */
2614
+ async _reconcileReconnect(eligible) {
2615
+ try {
2616
+ const byStage = /* @__PURE__ */ new Map();
2617
+ for (const call of eligible) {
2618
+ const stage = call.getReconnectStage(false);
2619
+ if (stage === null) continue;
2620
+ let arr = byStage.get(stage);
2621
+ if (!arr) {
2622
+ arr = [];
2623
+ byStage.set(stage, arr);
2624
+ }
2625
+ arr.push(call.callId);
2626
+ }
2627
+ if (byStage.size === 0) {
2628
+ return new Set(eligible.map((c) => c.callId));
2629
+ }
2630
+ const isBinary = this.format.isBinary;
2631
+ const stagesArg = isBinary ? /* @__PURE__ */ new Map() : {};
2632
+ for (const [stage, ids] of byStage) {
2633
+ ids.sort((a, b) => a - b);
2634
+ const bytes2 = IncreasingSeqCompressor.serialize(ids);
2635
+ if (isBinary) {
2636
+ stagesArg.set(stage, bytes2);
2637
+ } else {
2638
+ stagesArg[String(stage)] = base64Encode(bytes2);
2639
+ }
2640
+ }
2641
+ const reconnectCall = this.call(
2642
+ RpcSystemCalls.reconnect,
2643
+ [this._remoteHandshakeIndex, stagesArg],
2644
+ {
2645
+ remoteExecutionMode: 1
2646
+ /* AwaitForConnection only; must not itself reconnect */
2647
+ }
2648
+ );
2649
+ const result = await reconnectCall.result.promise;
2650
+ const bytes = _toBytes2(result);
2651
+ if (bytes === null) return new Set(eligible.map((c) => c.callId));
2652
+ return new Set(IncreasingSeqCompressor.deserialize(bytes));
2653
+ } catch {
2654
+ return new Set(eligible.map((c) => c.callId));
2655
+ }
2656
+ }
2061
2657
  _onHandshakeReceived(handshake) {
2062
2658
  this._pendingHandshake?.resolve(handshake);
2063
2659
  }
@@ -2105,6 +2701,17 @@ function guidToBase64Url(uuid) {
2105
2701
  const binary = String.fromCharCode(...bytes);
2106
2702
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2107
2703
  }
2704
+ function _toBytes2(value) {
2705
+ if (value instanceof Uint8Array) return value;
2706
+ if (typeof value === "string") {
2707
+ try {
2708
+ return base64Decode(value);
2709
+ } catch {
2710
+ return null;
2711
+ }
2712
+ }
2713
+ return null;
2714
+ }
2108
2715
 
2109
2716
  // src/rpc-service-host.ts
2110
2717
  var RpcServiceHost = class {
@@ -3107,7 +3714,9 @@ export {
3107
3714
  ARG_DELIMITER,
3108
3715
  ENVELOPE_DELIMITER,
3109
3716
  FRAME_DELIMITER,
3717
+ IncreasingSeqCompressor,
3110
3718
  RPC_CLOSE_CODE_UNSUPPORTED_FORMAT,
3719
+ RpcCallStage,
3111
3720
  RpcCallTypeId,
3112
3721
  RpcClientPeer,
3113
3722
  RpcClientPeerReconnectDelayer,