@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 +742 -131
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +206 -26
- package/dist/index.d.ts +206 -26
- package/dist/index.js +736 -127
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
this.
|
|
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
|
-
/**
|
|
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 (
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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 (
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
|
|
802
|
-
|
|
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 (
|
|
806
|
-
|
|
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
|
-
|
|
809
|
-
|
|
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
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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 (
|
|
826
|
-
|
|
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 (
|
|
829
|
-
|
|
830
|
-
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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.
|
|
861
|
-
this.
|
|
862
|
-
this.
|
|
1243
|
+
if (this._whenAckReady) {
|
|
1244
|
+
this._whenAckReady.resolve();
|
|
1245
|
+
this._whenAckReady = null;
|
|
863
1246
|
}
|
|
864
|
-
|
|
865
|
-
|
|
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 =
|
|
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
|
-
/**
|
|
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
|
|
1810
|
-
if (
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
-
/**
|
|
2026
|
-
*
|
|
2027
|
-
*
|
|
2028
|
-
*
|
|
2029
|
-
*
|
|
2030
|
-
*
|
|
2031
|
-
*
|
|
2032
|
-
|
|
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 (
|
|
2555
|
+
if (isPeerChanged) {
|
|
2035
2556
|
this.remoteObjects.disconnectAll();
|
|
2557
|
+
this.sharedObjects.disconnectAll();
|
|
2036
2558
|
} else {
|
|
2037
2559
|
this.remoteObjects.reconnectAll();
|
|
2038
2560
|
}
|
|
2039
|
-
const
|
|
2040
|
-
for (const call of
|
|
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(
|
|
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
|
-
|
|
2571
|
+
continue;
|
|
2572
|
+
}
|
|
2573
|
+
if (isPeerChanged && !(mode & 4)) {
|
|
2048
2574
|
if (!call.result.isCompleted)
|
|
2049
|
-
call.result.reject(new Error(
|
|
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
|
-
|
|
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
|
-
|
|
2056
|
-
|
|
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,
|