@actuallab/rpc 12.3.6 → 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 +816 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +245 -31
- package/dist/index.d.ts +245 -31
- package/dist/index.js +809 -132
- 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,
|
|
@@ -52,6 +54,7 @@ __export(index_exports, {
|
|
|
52
54
|
RpcPeerConnectionKind: () => RpcPeerConnectionKind,
|
|
53
55
|
RpcPeerStateKind: () => RpcPeerStateKind,
|
|
54
56
|
RpcPeerStateMonitor: () => RpcPeerStateMonitor,
|
|
57
|
+
RpcRemoteExecutionMode: () => RpcRemoteExecutionMode,
|
|
55
58
|
RpcRemoteObjectTracker: () => RpcRemoteObjectTracker,
|
|
56
59
|
RpcSerializationFormat: () => RpcSerializationFormat,
|
|
57
60
|
RpcSerializationFormatResolver: () => RpcSerializationFormatResolver,
|
|
@@ -124,7 +127,43 @@ var ARG_DELIMITER = "";
|
|
|
124
127
|
var FRAME_DELIMITER = "\n";
|
|
125
128
|
|
|
126
129
|
// src/rpc-serialization.ts
|
|
130
|
+
var import_msgpack2 = require("@msgpack/msgpack");
|
|
131
|
+
|
|
132
|
+
// src/msgpack-map-patch.ts
|
|
127
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
|
|
128
167
|
function serializeMessage(message, args) {
|
|
129
168
|
const envelope = JSON.stringify(message);
|
|
130
169
|
if (args === void 0 || args.length === 0) {
|
|
@@ -165,15 +204,15 @@ function deserializeMessage(raw) {
|
|
|
165
204
|
var textEncoder = new TextEncoder();
|
|
166
205
|
var textDecoder = new TextDecoder();
|
|
167
206
|
var INITIAL_ENCODER_BUFFER_SIZE = 32 * 1024;
|
|
168
|
-
var defaultBinaryEncoder = new
|
|
207
|
+
var defaultBinaryEncoder = new import_msgpack2.Encoder(
|
|
169
208
|
void 0,
|
|
170
209
|
void 0,
|
|
171
210
|
void 0,
|
|
172
211
|
INITIAL_ENCODER_BUFFER_SIZE
|
|
173
212
|
);
|
|
174
|
-
var defaultBinaryDecoder = new
|
|
213
|
+
var defaultBinaryDecoder = new import_msgpack2.Decoder();
|
|
175
214
|
function createBinaryEncoder() {
|
|
176
|
-
return new
|
|
215
|
+
return new import_msgpack2.Encoder(
|
|
177
216
|
void 0,
|
|
178
217
|
void 0,
|
|
179
218
|
void 0,
|
|
@@ -410,7 +449,7 @@ function splitCompactBinaryFrame(frame, registry, decoder = defaultBinaryDecoder
|
|
|
410
449
|
}
|
|
411
450
|
|
|
412
451
|
// src/rpc-connection.ts
|
|
413
|
-
var
|
|
452
|
+
var import_msgpack3 = require("@msgpack/msgpack");
|
|
414
453
|
var import_core = require("@actuallab/core");
|
|
415
454
|
var WebSocketState = {
|
|
416
455
|
CONNECTING: 0,
|
|
@@ -429,7 +468,7 @@ var RpcWebSocketConnection = class {
|
|
|
429
468
|
_encoder = createBinaryEncoder();
|
|
430
469
|
/** Per-connection msgpack decoder — reused across inbound messages
|
|
431
470
|
* rather than reconstructing one for every `decodeMulti` call. */
|
|
432
|
-
_decoder = new
|
|
471
|
+
_decoder = new import_msgpack3.Decoder();
|
|
433
472
|
/** Exposed for callers who need to encode outbound binary messages
|
|
434
473
|
* through this connection (e.g. `serializeBinaryMessage`). Safe to
|
|
435
474
|
* pass to synchronous encode calls on the same event-loop turn. */
|
|
@@ -614,6 +653,18 @@ function createMessageChannelPair() {
|
|
|
614
653
|
}
|
|
615
654
|
|
|
616
655
|
// src/rpc-service-def.ts
|
|
656
|
+
var RpcRemoteExecutionMode = {
|
|
657
|
+
/** Wait for connection if disconnected when the call is made. */
|
|
658
|
+
AwaitForConnection: 1,
|
|
659
|
+
/** Resend on reconnection to the same peer. */
|
|
660
|
+
AllowReconnect: 2,
|
|
661
|
+
/** Resend on reconnection to a different peer (implies AllowReconnect). */
|
|
662
|
+
AllowResend: 6,
|
|
663
|
+
// 4 | AllowReconnect
|
|
664
|
+
/** Default: wait + reconnect + resend. */
|
|
665
|
+
Default: 7
|
|
666
|
+
// AwaitForConnection | AllowResend
|
|
667
|
+
};
|
|
617
668
|
var RpcType = {
|
|
618
669
|
object: /* @__PURE__ */ Symbol("object"),
|
|
619
670
|
stream: /* @__PURE__ */ Symbol("stream"),
|
|
@@ -630,6 +681,7 @@ function defineRpcService(name, methods) {
|
|
|
630
681
|
const cleanName = colonIndex >= 0 ? key.substring(0, colonIndex) : key;
|
|
631
682
|
const wireArgCount = input.wireArgCount ?? input.args.length + 1;
|
|
632
683
|
const mapKey = `${cleanName}:${wireArgCount}`;
|
|
684
|
+
const isNoWait = input.returns === RpcType.noWait;
|
|
633
685
|
map.set(mapKey, {
|
|
634
686
|
name: cleanName,
|
|
635
687
|
serviceName: name,
|
|
@@ -637,7 +689,8 @@ function defineRpcService(name, methods) {
|
|
|
637
689
|
wireArgCount,
|
|
638
690
|
callTypeId: input.callTypeId ?? 0,
|
|
639
691
|
stream: input.returns === RpcType.stream,
|
|
640
|
-
noWait:
|
|
692
|
+
noWait: isNoWait,
|
|
693
|
+
remoteExecutionMode: isNoWait ? 0 : input.remoteExecutionMode ?? RpcRemoteExecutionMode.Default
|
|
641
694
|
});
|
|
642
695
|
}
|
|
643
696
|
return { name, methods: map };
|
|
@@ -645,18 +698,60 @@ function defineRpcService(name, methods) {
|
|
|
645
698
|
|
|
646
699
|
// src/rpc-call-tracker.ts
|
|
647
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
|
|
648
716
|
var RpcOutboundCall = class {
|
|
649
717
|
callId;
|
|
650
718
|
method;
|
|
651
719
|
result = new import_core3.PromiseSource();
|
|
720
|
+
/** Bitfield of RpcRemoteExecutionMode flags controlling reconnect/resend behavior. */
|
|
721
|
+
remoteExecutionMode;
|
|
652
722
|
/** Whether to remove this call from the tracker on $sys.Ok. Default: true.
|
|
653
723
|
* Subclasses (e.g. compute calls) override to false to stay in tracker for invalidation. */
|
|
654
724
|
removeOnOk = true;
|
|
655
725
|
/** The serialized wire data — stored for re-sending on reconnect. */
|
|
656
726
|
serializedWireData = "";
|
|
657
|
-
|
|
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;
|
|
738
|
+
constructor(callId, method, remoteExecutionMode = 7) {
|
|
658
739
|
this.callId = callId;
|
|
659
740
|
this.method = method;
|
|
741
|
+
this.remoteExecutionMode = remoteExecutionMode;
|
|
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;
|
|
660
755
|
}
|
|
661
756
|
/** Called when the connection is lost. Subclasses can override to resolve invalidation promises. */
|
|
662
757
|
onDisconnect() {
|
|
@@ -679,7 +774,10 @@ var RpcOutboundCallTracker = class {
|
|
|
679
774
|
}
|
|
680
775
|
remove(callId) {
|
|
681
776
|
const call = this._calls.get(callId);
|
|
682
|
-
if (call !== void 0)
|
|
777
|
+
if (call !== void 0) {
|
|
778
|
+
this._calls.delete(callId);
|
|
779
|
+
call.completedStage |= RpcCallStage.Unregistered;
|
|
780
|
+
}
|
|
683
781
|
return call;
|
|
684
782
|
}
|
|
685
783
|
values() {
|
|
@@ -742,6 +840,97 @@ var RpcInboundCallTracker = class {
|
|
|
742
840
|
}
|
|
743
841
|
};
|
|
744
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
|
+
|
|
745
934
|
// src/rpc-stream.ts
|
|
746
935
|
var import_denque = __toESM(require("denque"), 1);
|
|
747
936
|
var import_core5 = require("@actuallab/core");
|
|
@@ -763,13 +952,29 @@ var RpcStreamSender = class _RpcStreamSender {
|
|
|
763
952
|
peer;
|
|
764
953
|
ackPeriod;
|
|
765
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`). */
|
|
766
967
|
_nextIndex = 0;
|
|
767
968
|
_ended = false;
|
|
768
969
|
_started = new import_core4.PromiseSource();
|
|
970
|
+
/** Highest `nextIndex` observed in an ACK so far (matches .NET's `lastAckNextIndex` idiom). */
|
|
769
971
|
_lastAckedIndex = 0;
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
972
|
+
_abortController = new AbortController();
|
|
973
|
+
_iterator = null;
|
|
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) {
|
|
773
978
|
const localId = peer.sharedObjects.nextId();
|
|
774
979
|
this.id = { hostId: peer.hub.hubId, localId };
|
|
775
980
|
this.allowReconnect = allowReconnect;
|
|
@@ -778,62 +983,71 @@ var RpcStreamSender = class _RpcStreamSender {
|
|
|
778
983
|
this.peer = peer;
|
|
779
984
|
this.ackPeriod = ackPeriod;
|
|
780
985
|
this.ackAdvance = ackAdvance;
|
|
986
|
+
this.sourceUsesAbortSignal = sourceUsesAbortSignal;
|
|
987
|
+
}
|
|
988
|
+
/** AbortSignal that is aborted when the sender is disconnected. */
|
|
989
|
+
get abortSignal() {
|
|
990
|
+
return this._abortController.signal;
|
|
781
991
|
}
|
|
782
992
|
/** Returns the stream reference string for the $sys.Ok response. */
|
|
783
993
|
toRef() {
|
|
784
994
|
return `${this.id.hostId},${this.id.localId},${this.ackPeriod},${this.ackAdvance},${this.allowReconnect ? "1" : "0"},${this.isRealTime ? "1" : "0"}`;
|
|
785
995
|
}
|
|
786
|
-
/**
|
|
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
|
+
*/
|
|
787
1002
|
onAck(nextIndex, hostId) {
|
|
788
1003
|
const mustReset = hostId !== "" && hostId !== _RpcStreamSender._emptyGuid;
|
|
789
|
-
if (!this._started.isCompleted)
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
this.
|
|
794
|
-
this._lastAckedIndex = nextIndex;
|
|
795
|
-
this._resetRequested = true;
|
|
796
|
-
} else {
|
|
797
|
-
if (mustReset || nextIndex > this._lastAckedIndex) {
|
|
798
|
-
this._lastAckedIndex = nextIndex;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
if (this._ackWaiting) {
|
|
802
|
-
this._ackWaiting.resolve();
|
|
803
|
-
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;
|
|
804
1009
|
}
|
|
805
1010
|
}
|
|
806
1011
|
/** Called by system call handler when $sys.AckEnd is received from the client. */
|
|
807
1012
|
onAckEnd(_hostId) {
|
|
808
|
-
this.
|
|
809
|
-
this.peer.sharedObjects.unregister(this);
|
|
1013
|
+
this.disconnect();
|
|
810
1014
|
}
|
|
811
|
-
/**
|
|
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
|
+
*/
|
|
812
1024
|
sendItem(item) {
|
|
813
1025
|
if (this._ended) return;
|
|
814
1026
|
const conn = this.peer.connection;
|
|
815
|
-
if (
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
+
}
|
|
823
1036
|
this._nextIndex++;
|
|
824
1037
|
}
|
|
825
|
-
/** Send a batch of items to the client. */
|
|
1038
|
+
/** Send a batch of items to the client. See {@link sendItem} for disconnect semantics. */
|
|
826
1039
|
sendBatch(items) {
|
|
827
1040
|
if (this._ended || items.length === 0) return;
|
|
828
1041
|
const conn = this.peer.connection;
|
|
829
|
-
if (
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
+
}
|
|
837
1051
|
this._nextIndex += items.length;
|
|
838
1052
|
}
|
|
839
1053
|
/** Signal stream completion to the client. */
|
|
@@ -853,87 +1067,313 @@ var RpcStreamSender = class _RpcStreamSender {
|
|
|
853
1067
|
}
|
|
854
1068
|
/**
|
|
855
1069
|
* Consume an AsyncIterable and send all items to the client.
|
|
856
|
-
* Waits for the client's initial Ack before starting to pump items.
|
|
857
1070
|
*
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
* 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.
|
|
862
1074
|
*/
|
|
863
1075
|
async writeFrom(source) {
|
|
864
1076
|
await this._started.promise;
|
|
1077
|
+
if (this._ended) return;
|
|
865
1078
|
const iterator = source[Symbol.asyncIterator]();
|
|
1079
|
+
this._iterator = iterator;
|
|
1080
|
+
const state = { iteratorDone: false };
|
|
866
1081
|
try {
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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;
|
|
876
1112
|
}
|
|
877
|
-
|
|
878
|
-
|
|
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
|
+
}
|
|
879
1140
|
continue;
|
|
880
1141
|
}
|
|
881
|
-
if (
|
|
882
|
-
|
|
1142
|
+
if (isFullyBuffered) break;
|
|
1143
|
+
if (!buffer.hasRemainingCapacity) break;
|
|
1144
|
+
let pulled;
|
|
1145
|
+
try {
|
|
1146
|
+
const r = await iterator.next();
|
|
883
1147
|
if (this._ended) return;
|
|
884
|
-
|
|
885
|
-
|
|
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
|
+
};
|
|
886
1162
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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) {
|
|
890
1190
|
if (this._ended) return;
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
+
};
|
|
898
1213
|
}
|
|
1214
|
+
buffer.pushTail(item);
|
|
1215
|
+
if (accepted || isFullyBuffered) break;
|
|
899
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) {
|
|
900
1231
|
if (this._ended) return;
|
|
901
|
-
if (
|
|
902
|
-
|
|
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;
|
|
903
1245
|
}
|
|
904
|
-
if (
|
|
905
|
-
|
|
906
|
-
|
|
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
|
+
};
|
|
907
1266
|
}
|
|
1267
|
+
buffer.pushTail(pulled);
|
|
908
1268
|
}
|
|
909
|
-
if (!this._ended) {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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
|
+
}
|
|
915
1311
|
}
|
|
916
1312
|
}
|
|
917
1313
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
+
}
|
|
924
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;
|
|
925
1334
|
}
|
|
926
1335
|
// -- IRpcObject --
|
|
927
1336
|
reconnect() {
|
|
928
1337
|
}
|
|
929
1338
|
disconnect() {
|
|
1339
|
+
if (this._ended) return;
|
|
930
1340
|
this._ended = true;
|
|
1341
|
+
this._abortController.abort();
|
|
931
1342
|
if (!this._started.isCompleted) {
|
|
932
1343
|
this._started.resolve();
|
|
933
1344
|
}
|
|
1345
|
+
if (this._whenAckReady) {
|
|
1346
|
+
this._whenAckReady.resolve();
|
|
1347
|
+
this._whenAckReady = null;
|
|
1348
|
+
}
|
|
934
1349
|
this.peer.sharedObjects.unregister(this);
|
|
1350
|
+
const iterator = this._iterator;
|
|
1351
|
+
if (!iterator) return;
|
|
1352
|
+
if (!this.sourceUsesAbortSignal) {
|
|
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 {
|
|
1370
|
+
}
|
|
935
1371
|
}
|
|
936
1372
|
};
|
|
1373
|
+
var _endItem = { kind: "end" };
|
|
1374
|
+
function _clamp(value, min, max) {
|
|
1375
|
+
return value < min ? min : value > max ? max : value;
|
|
1376
|
+
}
|
|
937
1377
|
|
|
938
1378
|
// src/rpc-stream.ts
|
|
939
1379
|
var DEFAULT_ACK_PERIOD2 = 30;
|
|
@@ -969,6 +1409,15 @@ function parseStreamRef(value) {
|
|
|
969
1409
|
return null;
|
|
970
1410
|
}
|
|
971
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;
|
|
972
1421
|
id;
|
|
973
1422
|
kind;
|
|
974
1423
|
allowReconnect;
|
|
@@ -995,7 +1444,7 @@ var RpcStream = class _RpcStream {
|
|
|
995
1444
|
_iterating = false;
|
|
996
1445
|
_ackSentUpTo = 0;
|
|
997
1446
|
constructor(sourceOrRef, optionsOrPeer) {
|
|
998
|
-
if (
|
|
1447
|
+
if (_isStreamSource(sourceOrRef)) {
|
|
999
1448
|
const options = optionsOrPeer ?? {};
|
|
1000
1449
|
this.kind = RpcObjectKind.Local;
|
|
1001
1450
|
this.localSource = sourceOrRef;
|
|
@@ -1046,17 +1495,20 @@ var RpcStream = class _RpcStream {
|
|
|
1046
1495
|
throw new Error("toRef() was already called with a different peer.");
|
|
1047
1496
|
return this._toRefValue;
|
|
1048
1497
|
}
|
|
1498
|
+
const isFactory = typeof this.localSource === "function";
|
|
1049
1499
|
const sender = new RpcStreamSender(
|
|
1050
1500
|
peer,
|
|
1051
1501
|
this.ackPeriod,
|
|
1052
1502
|
this.ackAdvance,
|
|
1053
1503
|
this.allowReconnect,
|
|
1054
1504
|
this.isRealTime,
|
|
1055
|
-
this.canSkipTo
|
|
1505
|
+
this.canSkipTo,
|
|
1506
|
+
isFactory
|
|
1056
1507
|
);
|
|
1057
1508
|
peer.sharedObjects.register(sender);
|
|
1058
1509
|
this._sender = sender;
|
|
1059
|
-
|
|
1510
|
+
const source = isFactory ? this.localSource(sender.abortSignal) : this.localSource;
|
|
1511
|
+
this._whenSent = sender.writeFrom(source);
|
|
1060
1512
|
this._toRefValue = peer.format.isBinary ? {
|
|
1061
1513
|
SerializedId: [sender.id.hostId, sender.id.localId],
|
|
1062
1514
|
AckPeriod: sender.ackPeriod,
|
|
@@ -1136,7 +1588,10 @@ var RpcStream = class _RpcStream {
|
|
|
1136
1588
|
}
|
|
1137
1589
|
}
|
|
1138
1590
|
disconnect() {
|
|
1139
|
-
if (this.kind === RpcObjectKind.Local)
|
|
1591
|
+
if (this.kind === RpcObjectKind.Local) {
|
|
1592
|
+
this._sender?.disconnect();
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1140
1595
|
if (this._completed) return;
|
|
1141
1596
|
this._completed = true;
|
|
1142
1597
|
this._completionError = new Error("Peer disconnected.");
|
|
@@ -1145,7 +1600,8 @@ var RpcStream = class _RpcStream {
|
|
|
1145
1600
|
// -- AsyncIterable --
|
|
1146
1601
|
[Symbol.asyncIterator]() {
|
|
1147
1602
|
if (this.kind === RpcObjectKind.Local) {
|
|
1148
|
-
|
|
1603
|
+
const source = typeof this.localSource === "function" ? this.localSource(new AbortController().signal) : this.localSource;
|
|
1604
|
+
return source[Symbol.asyncIterator]();
|
|
1149
1605
|
}
|
|
1150
1606
|
if (this._iterating)
|
|
1151
1607
|
throw new Error("RpcStream can only be iterated once.");
|
|
@@ -1226,7 +1682,8 @@ var RpcStream = class _RpcStream {
|
|
|
1226
1682
|
}
|
|
1227
1683
|
}
|
|
1228
1684
|
};
|
|
1229
|
-
function
|
|
1685
|
+
function _isStreamSource(value) {
|
|
1686
|
+
if (typeof value === "function") return true;
|
|
1230
1687
|
return value !== null && typeof value === "object" && Symbol.asyncIterator in value;
|
|
1231
1688
|
}
|
|
1232
1689
|
function resolveStreamRefs(value, peer) {
|
|
@@ -1260,6 +1717,25 @@ function resolveStreamRefs(value, peer) {
|
|
|
1260
1717
|
return value;
|
|
1261
1718
|
}
|
|
1262
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
|
+
|
|
1263
1739
|
// src/rpc-system-call-handler.ts
|
|
1264
1740
|
var RpcSystemCallHandler = class {
|
|
1265
1741
|
handle(message, args, peer) {
|
|
@@ -1269,6 +1745,7 @@ var RpcSystemCallHandler = class {
|
|
|
1269
1745
|
case RpcSystemCalls.ok: {
|
|
1270
1746
|
const call = peer.outbound.get(relatedId);
|
|
1271
1747
|
if (call !== void 0) {
|
|
1748
|
+
call.completedStage |= RpcCallStage.ResultReady;
|
|
1272
1749
|
if (call.removeOnOk) {
|
|
1273
1750
|
peer.outbound.remove(relatedId);
|
|
1274
1751
|
}
|
|
@@ -1276,6 +1753,10 @@ var RpcSystemCallHandler = class {
|
|
|
1276
1753
|
}
|
|
1277
1754
|
break;
|
|
1278
1755
|
}
|
|
1756
|
+
case RpcSystemCalls.reconnect: {
|
|
1757
|
+
this._handleReconnect(relatedId, args, peer);
|
|
1758
|
+
break;
|
|
1759
|
+
}
|
|
1279
1760
|
case RpcSystemCalls.error: {
|
|
1280
1761
|
const call = peer.outbound.remove(relatedId);
|
|
1281
1762
|
if (call !== void 0) {
|
|
@@ -1364,7 +1845,54 @@ var RpcSystemCallHandler = class {
|
|
|
1364
1845
|
}
|
|
1365
1846
|
}
|
|
1366
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
|
+
}
|
|
1367
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
|
+
}
|
|
1368
1896
|
|
|
1369
1897
|
// src/rpc-system-call-sender.ts
|
|
1370
1898
|
var RpcSystemCallSender = class _RpcSystemCallSender {
|
|
@@ -1552,7 +2080,7 @@ var RpcClientPeerReconnectDelayer = class extends import_core6.RetryDelayer {
|
|
|
1552
2080
|
};
|
|
1553
2081
|
|
|
1554
2082
|
// src/rpc-serialization-format.ts
|
|
1555
|
-
var
|
|
2083
|
+
var import_msgpack4 = require("@msgpack/msgpack");
|
|
1556
2084
|
var RpcSerializationFormat = class {
|
|
1557
2085
|
/** Create a reusable binary encoder for this format. Returns undefined for text formats. */
|
|
1558
2086
|
createEncoder() {
|
|
@@ -1626,7 +2154,7 @@ var RpcMessagePackSerializationFormat = class extends RpcSerializationFormat {
|
|
|
1626
2154
|
return createBinaryEncoder();
|
|
1627
2155
|
}
|
|
1628
2156
|
createDecoder() {
|
|
1629
|
-
return new
|
|
2157
|
+
return new import_msgpack4.Decoder();
|
|
1630
2158
|
}
|
|
1631
2159
|
};
|
|
1632
2160
|
var RpcMessagePackCompactSerializationFormat = class extends RpcSerializationFormat {
|
|
@@ -1658,7 +2186,7 @@ var RpcMessagePackCompactSerializationFormat = class extends RpcSerializationFor
|
|
|
1658
2186
|
return createBinaryEncoder();
|
|
1659
2187
|
}
|
|
1660
2188
|
createDecoder() {
|
|
1661
|
-
return new
|
|
2189
|
+
return new import_msgpack4.Decoder();
|
|
1662
2190
|
}
|
|
1663
2191
|
};
|
|
1664
2192
|
RpcSerializationFormat.SystemJsonV5 = new RpcJsonSerializationFormat("json5");
|
|
@@ -1755,13 +2283,21 @@ var RpcPeer = class {
|
|
|
1755
2283
|
this._startKeepAlive();
|
|
1756
2284
|
});
|
|
1757
2285
|
}
|
|
1758
|
-
/**
|
|
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;
|
|
1759
2292
|
get format() {
|
|
1760
|
-
return RpcSerializationFormatResolver.Default.defaultFormat;
|
|
2293
|
+
return this._format ?? RpcSerializationFormatResolver.Default.defaultFormat;
|
|
2294
|
+
}
|
|
2295
|
+
set format(value) {
|
|
2296
|
+
this._format = value;
|
|
1761
2297
|
}
|
|
1762
2298
|
call(method, args, options) {
|
|
1763
2299
|
const callId = this.outbound.nextId();
|
|
1764
|
-
const outboundCall = options?.outboundCallFactory ? options.outboundCallFactory(callId, method) : new RpcOutboundCall(callId, method);
|
|
2300
|
+
const outboundCall = options?.outboundCallFactory ? options.outboundCallFactory(callId, method) : new RpcOutboundCall(callId, method, options?.remoteExecutionMode ?? 7);
|
|
1765
2301
|
const envelope = {
|
|
1766
2302
|
Method: method,
|
|
1767
2303
|
RelatedId: callId,
|
|
@@ -1866,8 +2402,16 @@ var RpcPeer = class {
|
|
|
1866
2402
|
}
|
|
1867
2403
|
const method = message.Method ?? "";
|
|
1868
2404
|
if (method === RpcSystemCalls.handshake) {
|
|
1869
|
-
const
|
|
1870
|
-
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
|
+
}
|
|
1871
2415
|
return;
|
|
1872
2416
|
}
|
|
1873
2417
|
if (method.startsWith("$sys")) {
|
|
@@ -1957,6 +2501,12 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
1957
2501
|
_connectionKind = 0 /* Disconnected */;
|
|
1958
2502
|
_tryIndex = 0;
|
|
1959
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;
|
|
1960
2510
|
_lastRemoteHubId;
|
|
1961
2511
|
_pendingHandshake;
|
|
1962
2512
|
_reconnectsAt = 0;
|
|
@@ -1983,24 +2533,24 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
1983
2533
|
this._reconnectsAt = value;
|
|
1984
2534
|
this.reconnectsAtChanged.trigger();
|
|
1985
2535
|
}
|
|
1986
|
-
_format;
|
|
1987
2536
|
constructor(hub, url, serializationFormat) {
|
|
1988
2537
|
super(url, hub);
|
|
1989
2538
|
this.clientId = guidToBase64Url(this.id);
|
|
1990
2539
|
const resolver = RpcSerializationFormatResolver.Default;
|
|
1991
2540
|
this.serializationFormat = serializationFormat ?? resolver.defaultFormatKey;
|
|
1992
|
-
this.
|
|
1993
|
-
}
|
|
1994
|
-
get format() {
|
|
1995
|
-
return this._format;
|
|
2541
|
+
this.format = resolver.get(this.serializationFormat);
|
|
1996
2542
|
}
|
|
1997
2543
|
get connectionKind() {
|
|
1998
2544
|
return this._connectionKind;
|
|
1999
2545
|
}
|
|
2000
|
-
/** One-shot connection for tests — no reconnection loop, no handshake.
|
|
2001
|
-
|
|
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) {
|
|
2002
2552
|
this.setupConnection(conn);
|
|
2003
|
-
this._reconnect(
|
|
2553
|
+
void this._reconnect(isPeerChanged);
|
|
2004
2554
|
}
|
|
2005
2555
|
/** Start the reconnection loop — runs until disposed. */
|
|
2006
2556
|
async run(wsFactory) {
|
|
@@ -2044,6 +2594,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2044
2594
|
closedRejection
|
|
2045
2595
|
]);
|
|
2046
2596
|
this._pendingHandshake = void 0;
|
|
2597
|
+
this._remoteHandshakeIndex = remoteHandshake.Index ?? 0;
|
|
2047
2598
|
const remoteHubId = remoteHandshake.RemoteHubId;
|
|
2048
2599
|
let isPeerChanged = false;
|
|
2049
2600
|
if (remoteHubId !== void 0) {
|
|
@@ -2057,7 +2608,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2057
2608
|
this._connection = conn;
|
|
2058
2609
|
this._connectionKind = 2 /* Connected */;
|
|
2059
2610
|
this._tryIndex = 0;
|
|
2060
|
-
this._reconnect(isPeerChanged);
|
|
2611
|
+
await this._reconnect(isPeerChanged);
|
|
2061
2612
|
await new Promise((r) => conn.closed.add(() => r()));
|
|
2062
2613
|
} catch {
|
|
2063
2614
|
}
|
|
@@ -2082,31 +2633,129 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2082
2633
|
}
|
|
2083
2634
|
}
|
|
2084
2635
|
}
|
|
2085
|
-
/**
|
|
2086
|
-
*
|
|
2087
|
-
*
|
|
2088
|
-
*
|
|
2089
|
-
*
|
|
2090
|
-
*
|
|
2091
|
-
*
|
|
2092
|
-
|
|
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) {
|
|
2093
2656
|
if (this._connection === void 0) return;
|
|
2094
|
-
if (
|
|
2657
|
+
if (isPeerChanged) {
|
|
2095
2658
|
this.remoteObjects.disconnectAll();
|
|
2659
|
+
this.sharedObjects.disconnectAll();
|
|
2096
2660
|
} else {
|
|
2097
2661
|
this.remoteObjects.reconnectAll();
|
|
2098
2662
|
}
|
|
2099
|
-
const
|
|
2100
|
-
for (const call of
|
|
2663
|
+
const eligible = [];
|
|
2664
|
+
for (const call of [...this.outbound.values()]) {
|
|
2665
|
+
const mode = call.remoteExecutionMode;
|
|
2666
|
+
if (!(mode & 2)) {
|
|
2667
|
+
if (!call.result.isCompleted)
|
|
2668
|
+
call.result.reject(new Error(
|
|
2669
|
+
"Outbound call failed: disconnected and AllowReconnect is not set."
|
|
2670
|
+
));
|
|
2671
|
+
call.onDisconnect();
|
|
2672
|
+
this.outbound.remove(call.callId);
|
|
2673
|
+
continue;
|
|
2674
|
+
}
|
|
2675
|
+
if (isPeerChanged && !(mode & 4)) {
|
|
2676
|
+
if (!call.result.isCompleted)
|
|
2677
|
+
call.result.reject(new Error(
|
|
2678
|
+
"Outbound call failed: reconnected to a different peer and AllowResend is not set."
|
|
2679
|
+
));
|
|
2680
|
+
call.onDisconnect();
|
|
2681
|
+
this.outbound.remove(call.callId);
|
|
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) {
|
|
2101
2691
|
if (!call.removeOnOk && call.result.isCompleted) {
|
|
2102
2692
|
call.onDisconnect();
|
|
2103
2693
|
this.outbound.remove(call.callId);
|
|
2104
|
-
|
|
2105
|
-
this._sendWireData(call.serializedWireData);
|
|
2694
|
+
continue;
|
|
2106
2695
|
}
|
|
2696
|
+
if (unknownIds !== null && !unknownIds.has(call.callId)) {
|
|
2697
|
+
continue;
|
|
2698
|
+
}
|
|
2699
|
+
this._sendWireData(call.serializedWireData);
|
|
2107
2700
|
}
|
|
2108
2701
|
this._flushPendingSends();
|
|
2109
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
|
+
}
|
|
2110
2759
|
_onHandshakeReceived(handshake) {
|
|
2111
2760
|
this._pendingHandshake?.resolve(handshake);
|
|
2112
2761
|
}
|
|
@@ -2122,6 +2771,9 @@ var RpcServerPeer = class extends RpcPeer {
|
|
|
2122
2771
|
/** Accept an incoming connection — sets up message handling and handshake response. */
|
|
2123
2772
|
accept(conn) {
|
|
2124
2773
|
this.setupConnection(conn);
|
|
2774
|
+
conn.closed.add(() => {
|
|
2775
|
+
this.sharedObjects.disconnectAll();
|
|
2776
|
+
});
|
|
2125
2777
|
}
|
|
2126
2778
|
_onHandshakeReceived(_handshake) {
|
|
2127
2779
|
if (this._connection !== void 0) {
|
|
@@ -2151,6 +2803,17 @@ function guidToBase64Url(uuid) {
|
|
|
2151
2803
|
const binary = String.fromCharCode(...bytes);
|
|
2152
2804
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2153
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
|
+
}
|
|
2154
2817
|
|
|
2155
2818
|
// src/rpc-service-host.ts
|
|
2156
2819
|
var RpcServiceHost = class {
|
|
@@ -2201,7 +2864,8 @@ function rpcMethod(options) {
|
|
|
2201
2864
|
methods[methodName] = {
|
|
2202
2865
|
...methods[methodName],
|
|
2203
2866
|
argCount: target.length,
|
|
2204
|
-
returns: options?.returns
|
|
2867
|
+
returns: options?.returns,
|
|
2868
|
+
remoteExecutionMode: options?.remoteExecutionMode
|
|
2205
2869
|
};
|
|
2206
2870
|
return target;
|
|
2207
2871
|
};
|
|
@@ -2816,7 +3480,8 @@ var RpcHub = class {
|
|
|
2816
3480
|
const methods = /* @__PURE__ */ new Map();
|
|
2817
3481
|
for (const [name, byArgCount] of overloads) {
|
|
2818
3482
|
if (byArgCount.size === 1) {
|
|
2819
|
-
|
|
3483
|
+
const [[, singleFn]] = byArgCount;
|
|
3484
|
+
methods.set(name, singleFn);
|
|
2820
3485
|
} else {
|
|
2821
3486
|
methods.set(name, (...args) => {
|
|
2822
3487
|
const fn = byArgCount.get(args.length);
|
|
@@ -2846,6 +3511,7 @@ var RpcHub = class {
|
|
|
2846
3511
|
for (const [name, meta] of Object.entries(methodsMeta)) {
|
|
2847
3512
|
const wireArgCount = meta.argCount + 1;
|
|
2848
3513
|
const mapKey = `${name}:${wireArgCount}`;
|
|
3514
|
+
const isNoWait = meta.returns === RpcType.noWait;
|
|
2849
3515
|
methods.set(mapKey, {
|
|
2850
3516
|
name,
|
|
2851
3517
|
serviceName: svcMeta.name,
|
|
@@ -2853,7 +3519,8 @@ var RpcHub = class {
|
|
|
2853
3519
|
wireArgCount,
|
|
2854
3520
|
callTypeId: 0,
|
|
2855
3521
|
stream: meta.returns === RpcType.stream,
|
|
2856
|
-
noWait:
|
|
3522
|
+
noWait: isNoWait,
|
|
3523
|
+
remoteExecutionMode: isNoWait ? 0 : meta.remoteExecutionMode ?? RpcRemoteExecutionMode.Default
|
|
2857
3524
|
});
|
|
2858
3525
|
}
|
|
2859
3526
|
return { name: svcMeta.name, methods };
|
|
@@ -2871,6 +3538,8 @@ var RpcHub = class {
|
|
|
2871
3538
|
/** Create a client method for the given methodDef. Override in FusionHub for custom call types. */
|
|
2872
3539
|
_createClientMethod(peer, methodDef) {
|
|
2873
3540
|
const wireName = wireMethodName(methodDef);
|
|
3541
|
+
const mode = methodDef.remoteExecutionMode;
|
|
3542
|
+
const mustCheckConnection = !(mode & RpcRemoteExecutionMode.AwaitForConnection);
|
|
2874
3543
|
if (methodDef.noWait) {
|
|
2875
3544
|
return (...args) => {
|
|
2876
3545
|
const callArgs = args.slice(0, methodDef.argCount);
|
|
@@ -2879,6 +3548,8 @@ var RpcHub = class {
|
|
|
2879
3548
|
}
|
|
2880
3549
|
if (methodDef.stream) {
|
|
2881
3550
|
return async (...args) => {
|
|
3551
|
+
if (mustCheckConnection && !peer.isConnected)
|
|
3552
|
+
throw new Error("Outbound call failed: not connected and AwaitForConnection is not set.");
|
|
2882
3553
|
const callArgs = args.slice(0, methodDef.argCount);
|
|
2883
3554
|
const outboundCall = peer.call(wireName, callArgs);
|
|
2884
3555
|
const result = await outboundCall.result.promise;
|
|
@@ -2892,8 +3563,10 @@ var RpcHub = class {
|
|
|
2892
3563
|
return stream;
|
|
2893
3564
|
};
|
|
2894
3565
|
}
|
|
2895
|
-
const callOptions = methodDef.callTypeId !== 0 ? { callTypeId: methodDef.callTypeId } : void 0;
|
|
3566
|
+
const callOptions = methodDef.callTypeId !== 0 || mode !== RpcRemoteExecutionMode.Default ? { callTypeId: methodDef.callTypeId, remoteExecutionMode: mode } : void 0;
|
|
2896
3567
|
return async (...args) => {
|
|
3568
|
+
if (mustCheckConnection && !peer.isConnected)
|
|
3569
|
+
throw new Error("Outbound call failed: not connected and AwaitForConnection is not set.");
|
|
2897
3570
|
const callArgs = args.slice(0, methodDef.argCount);
|
|
2898
3571
|
const outboundCall = peer.call(wireName, callArgs, callOptions);
|
|
2899
3572
|
const result = await outboundCall.result.promise;
|
|
@@ -2916,7 +3589,7 @@ function createRpcClient(peer, serviceDef) {
|
|
|
2916
3589
|
const methods = /* @__PURE__ */ new Map();
|
|
2917
3590
|
for (const [name, byArgCount] of overloads) {
|
|
2918
3591
|
if (byArgCount.size === 1) {
|
|
2919
|
-
const methodDef = byArgCount
|
|
3592
|
+
const [[, methodDef]] = byArgCount;
|
|
2920
3593
|
methods.set(name, createMethod(peer, methodDef));
|
|
2921
3594
|
} else {
|
|
2922
3595
|
const fns = /* @__PURE__ */ new Map();
|
|
@@ -2941,10 +3614,14 @@ function createRpcClient(peer, serviceDef) {
|
|
|
2941
3614
|
});
|
|
2942
3615
|
}
|
|
2943
3616
|
function createMethod(peer, methodDef) {
|
|
3617
|
+
const mode = methodDef.remoteExecutionMode;
|
|
3618
|
+
const mustCheckConnection = !(mode & RpcRemoteExecutionMode.AwaitForConnection);
|
|
2944
3619
|
return async (...args) => {
|
|
3620
|
+
if (mustCheckConnection && !peer.isConnected)
|
|
3621
|
+
throw new Error("Outbound call failed: not connected and AwaitForConnection is not set.");
|
|
2945
3622
|
const callArgs = args.slice(0, methodDef.argCount);
|
|
2946
3623
|
const wireName = wireMethodName(methodDef);
|
|
2947
|
-
const options = methodDef.callTypeId !== 0 ? { callTypeId: methodDef.callTypeId } : void 0;
|
|
3624
|
+
const options = methodDef.callTypeId !== 0 || mode !== RpcRemoteExecutionMode.Default ? { callTypeId: methodDef.callTypeId, remoteExecutionMode: mode } : void 0;
|
|
2948
3625
|
const outboundCall = peer.call(wireName, callArgs, options);
|
|
2949
3626
|
return outboundCall.result.promise;
|
|
2950
3627
|
};
|
|
@@ -3140,7 +3817,9 @@ var RpcPeerStateMonitor = class {
|
|
|
3140
3817
|
ARG_DELIMITER,
|
|
3141
3818
|
ENVELOPE_DELIMITER,
|
|
3142
3819
|
FRAME_DELIMITER,
|
|
3820
|
+
IncreasingSeqCompressor,
|
|
3143
3821
|
RPC_CLOSE_CODE_UNSUPPORTED_FORMAT,
|
|
3822
|
+
RpcCallStage,
|
|
3144
3823
|
RpcCallTypeId,
|
|
3145
3824
|
RpcClientPeer,
|
|
3146
3825
|
RpcClientPeerReconnectDelayer,
|
|
@@ -3159,6 +3838,7 @@ var RpcPeerStateMonitor = class {
|
|
|
3159
3838
|
RpcPeerConnectionKind,
|
|
3160
3839
|
RpcPeerStateKind,
|
|
3161
3840
|
RpcPeerStateMonitor,
|
|
3841
|
+
RpcRemoteExecutionMode,
|
|
3162
3842
|
RpcRemoteObjectTracker,
|
|
3163
3843
|
RpcSerializationFormat,
|
|
3164
3844
|
RpcSerializationFormatResolver,
|