@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.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
|
|
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
|
|
213
|
+
var defaultBinaryDecoder = new import_msgpack2.Decoder();
|
|
176
214
|
function createBinaryEncoder() {
|
|
177
|
-
return new
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
this.
|
|
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
|
-
/**
|
|
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 (
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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 (
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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
|
-
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
-
|
|
902
|
-
|
|
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 (
|
|
906
|
-
|
|
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
|
-
|
|
909
|
-
|
|
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
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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 (
|
|
926
|
-
|
|
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 (
|
|
929
|
-
|
|
930
|
-
|
|
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
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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.
|
|
961
|
-
this.
|
|
962
|
-
this.
|
|
1345
|
+
if (this._whenAckReady) {
|
|
1346
|
+
this._whenAckReady.resolve();
|
|
1347
|
+
this._whenAckReady = null;
|
|
963
1348
|
}
|
|
964
|
-
|
|
965
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
1910
|
-
if (
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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
|
-
/**
|
|
2126
|
-
*
|
|
2127
|
-
*
|
|
2128
|
-
*
|
|
2129
|
-
*
|
|
2130
|
-
*
|
|
2131
|
-
*
|
|
2132
|
-
|
|
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 (
|
|
2657
|
+
if (isPeerChanged) {
|
|
2135
2658
|
this.remoteObjects.disconnectAll();
|
|
2659
|
+
this.sharedObjects.disconnectAll();
|
|
2136
2660
|
} else {
|
|
2137
2661
|
this.remoteObjects.reconnectAll();
|
|
2138
2662
|
}
|
|
2139
|
-
const
|
|
2140
|
-
for (const call of
|
|
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(
|
|
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
|
-
|
|
2673
|
+
continue;
|
|
2674
|
+
}
|
|
2675
|
+
if (isPeerChanged && !(mode & 4)) {
|
|
2148
2676
|
if (!call.result.isCompleted)
|
|
2149
|
-
call.result.reject(new Error(
|
|
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
|
-
|
|
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
|
-
|
|
2156
|
-
|
|
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,
|