@actuallab/rpc 12.3.14 → 12.3.31
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 +890 -205
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +211 -27
- package/dist/index.d.ts +211 -27
- package/dist/index.js +857 -175
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
// src/logging.ts
|
|
2
|
+
import { createLogProvider, LogLevel } from "@actuallab/core";
|
|
3
|
+
var scopeDefaults = {
|
|
4
|
+
RpcCallTracker: LogLevel.Warn,
|
|
5
|
+
RpcClient: LogLevel.Warn,
|
|
6
|
+
RpcClientPeerReconnectDelayer: LogLevel.Warn,
|
|
7
|
+
RpcConnection: LogLevel.Warn,
|
|
8
|
+
RpcHub: LogLevel.Warn,
|
|
9
|
+
RpcInterceptor: LogLevel.Warn,
|
|
10
|
+
RpcMessageChannelConnection: LogLevel.Warn,
|
|
11
|
+
RpcMethodRegistry: LogLevel.Warn,
|
|
12
|
+
RpcOutboundCall: LogLevel.Warn,
|
|
13
|
+
RpcPeer: LogLevel.Info,
|
|
14
|
+
RpcPeerStateMonitor: LogLevel.Warn,
|
|
15
|
+
RpcRemoteObjectTracker: LogLevel.Warn,
|
|
16
|
+
RpcSerialization: LogLevel.Warn,
|
|
17
|
+
RpcServiceHost: LogLevel.Warn,
|
|
18
|
+
RpcSharedObjectTracker: LogLevel.Warn,
|
|
19
|
+
RpcSharedStream: LogLevel.Warn,
|
|
20
|
+
RpcStream: LogLevel.Warn,
|
|
21
|
+
RpcStreamSender: LogLevel.Warn,
|
|
22
|
+
RpcSystemCallHandler: LogLevel.Warn,
|
|
23
|
+
RpcSystemCallSender: LogLevel.Warn,
|
|
24
|
+
RpcWebSocketClient: LogLevel.Warn
|
|
25
|
+
};
|
|
26
|
+
var getLogs = createLogProvider("rpc.", scopeDefaults);
|
|
27
|
+
|
|
1
28
|
// src/rpc-message.ts
|
|
2
29
|
var RpcCallTypeId = /* @__PURE__ */ ((RpcCallTypeId2) => {
|
|
3
30
|
RpcCallTypeId2[RpcCallTypeId2["Regular"] = 0] = "Regular";
|
|
@@ -23,9 +50,46 @@ var FRAME_DELIMITER = "\n";
|
|
|
23
50
|
|
|
24
51
|
// src/rpc-serialization.ts
|
|
25
52
|
import {
|
|
26
|
-
Encoder,
|
|
53
|
+
Encoder as Encoder2,
|
|
27
54
|
Decoder
|
|
28
55
|
} from "@msgpack/msgpack";
|
|
56
|
+
|
|
57
|
+
// src/msgpack-map-patch.ts
|
|
58
|
+
import { Encoder } from "@msgpack/msgpack";
|
|
59
|
+
function patchMsgpackEncoderForMaps() {
|
|
60
|
+
const proto = Encoder.prototype;
|
|
61
|
+
if (proto._hasMapPatch) return;
|
|
62
|
+
proto._hasMapPatch = true;
|
|
63
|
+
const originalDoEncode = proto.doEncode;
|
|
64
|
+
proto.doEncode = function(object, depth) {
|
|
65
|
+
if (object instanceof Map) {
|
|
66
|
+
_encodeMapInstance.call(this, object, depth);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
originalDoEncode.call(this, object, depth);
|
|
70
|
+
};
|
|
71
|
+
function _encodeMapInstance(m, depth) {
|
|
72
|
+
const self = this;
|
|
73
|
+
const size = m.size;
|
|
74
|
+
if (size < 16) {
|
|
75
|
+
self.writeU8(128 + size);
|
|
76
|
+
} else if (size < 65536) {
|
|
77
|
+
self.writeU8(222);
|
|
78
|
+
self.writeU16(size);
|
|
79
|
+
} else {
|
|
80
|
+
self.writeU8(223);
|
|
81
|
+
self.writeU32(size);
|
|
82
|
+
}
|
|
83
|
+
for (const [key, value] of m) {
|
|
84
|
+
self.doEncode(key, depth + 1);
|
|
85
|
+
self.doEncode(value, depth + 1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
patchMsgpackEncoderForMaps();
|
|
90
|
+
|
|
91
|
+
// src/rpc-serialization.ts
|
|
92
|
+
var { warnLog } = getLogs("RpcSerialization");
|
|
29
93
|
function serializeMessage(message, args) {
|
|
30
94
|
const envelope = JSON.stringify(message);
|
|
31
95
|
if (args === void 0 || args.length === 0) {
|
|
@@ -55,9 +119,7 @@ function deserializeMessage(raw) {
|
|
|
55
119
|
try {
|
|
56
120
|
return JSON.parse(s);
|
|
57
121
|
} catch {
|
|
58
|
-
|
|
59
|
-
`[RpcSerialization] failed to parse arg as JSON: ${s.substring(0, 100)}`
|
|
60
|
-
);
|
|
122
|
+
warnLog?.log(`failed to parse arg as JSON: ${s.substring(0, 100)}`);
|
|
61
123
|
return s;
|
|
62
124
|
}
|
|
63
125
|
});
|
|
@@ -66,7 +128,7 @@ function deserializeMessage(raw) {
|
|
|
66
128
|
var textEncoder = new TextEncoder();
|
|
67
129
|
var textDecoder = new TextDecoder();
|
|
68
130
|
var INITIAL_ENCODER_BUFFER_SIZE = 32 * 1024;
|
|
69
|
-
var defaultBinaryEncoder = new
|
|
131
|
+
var defaultBinaryEncoder = new Encoder2(
|
|
70
132
|
void 0,
|
|
71
133
|
void 0,
|
|
72
134
|
void 0,
|
|
@@ -74,7 +136,7 @@ var defaultBinaryEncoder = new Encoder(
|
|
|
74
136
|
);
|
|
75
137
|
var defaultBinaryDecoder = new Decoder();
|
|
76
138
|
function createBinaryEncoder() {
|
|
77
|
-
return new
|
|
139
|
+
return new Encoder2(
|
|
78
140
|
void 0,
|
|
79
141
|
void 0,
|
|
80
142
|
void 0,
|
|
@@ -313,6 +375,7 @@ function splitCompactBinaryFrame(frame, registry, decoder = defaultBinaryDecoder
|
|
|
313
375
|
// src/rpc-connection.ts
|
|
314
376
|
import { Decoder as Decoder2 } from "@msgpack/msgpack";
|
|
315
377
|
import { PromiseSource, EventHandlerSet } from "@actuallab/core";
|
|
378
|
+
var { warnLog: warnLog2 } = getLogs("RpcConnection");
|
|
316
379
|
var WebSocketState = {
|
|
317
380
|
CONNECTING: 0,
|
|
318
381
|
OPEN: 1,
|
|
@@ -374,6 +437,7 @@ var RpcWebSocketConnection = class {
|
|
|
374
437
|
});
|
|
375
438
|
}
|
|
376
439
|
} catch (e) {
|
|
440
|
+
warnLog2?.log("Failed to split binary frame:", e);
|
|
377
441
|
this.error.trigger(e);
|
|
378
442
|
}
|
|
379
443
|
} else if (typeof Blob !== "undefined" && ev.data instanceof Blob && this.binaryMode) {
|
|
@@ -389,6 +453,7 @@ var RpcWebSocketConnection = class {
|
|
|
389
453
|
});
|
|
390
454
|
}
|
|
391
455
|
} catch (e) {
|
|
456
|
+
warnLog2?.log("Failed to split binary blob frame:", e);
|
|
392
457
|
this.error.trigger(e);
|
|
393
458
|
}
|
|
394
459
|
});
|
|
@@ -560,6 +625,21 @@ function defineRpcService(name, methods) {
|
|
|
560
625
|
|
|
561
626
|
// src/rpc-call-tracker.ts
|
|
562
627
|
import { PromiseSource as PromiseSource2 } from "@actuallab/core";
|
|
628
|
+
|
|
629
|
+
// src/rpc-call-stage.ts
|
|
630
|
+
var RpcCallStage = {
|
|
631
|
+
/** The call's result has been received from the peer. */
|
|
632
|
+
ResultReady: 1,
|
|
633
|
+
/** The compute call's result has been invalidated (compute-call specific). */
|
|
634
|
+
Invalidated: 3,
|
|
635
|
+
/**
|
|
636
|
+
* Bit flag added on top of ResultReady/Invalidated when the outbound
|
|
637
|
+
* call has been removed from the tracker (unregistered).
|
|
638
|
+
*/
|
|
639
|
+
Unregistered: 4096
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// src/rpc-call-tracker.ts
|
|
563
643
|
var RpcOutboundCall = class {
|
|
564
644
|
callId;
|
|
565
645
|
method;
|
|
@@ -571,11 +651,35 @@ var RpcOutboundCall = class {
|
|
|
571
651
|
removeOnOk = true;
|
|
572
652
|
/** The serialized wire data — stored for re-sending on reconnect. */
|
|
573
653
|
serializedWireData = "";
|
|
654
|
+
/**
|
|
655
|
+
* Bitfield of {@link RpcCallStage} flags indicating how far this call
|
|
656
|
+
* has progressed. Used by the `$sys.Reconnect` protocol to tell the
|
|
657
|
+
* remote peer which calls are still pending its attention.
|
|
658
|
+
*
|
|
659
|
+
* - 0: brand new, no result yet (the peer is expected to be actively processing).
|
|
660
|
+
* - `ResultReady` (1): `$sys.Ok` received for this call.
|
|
661
|
+
* - `Invalidated` (3): result was invalidated (compute calls only).
|
|
662
|
+
* - `Unregistered` (0x1000): removed from the tracker.
|
|
663
|
+
*/
|
|
664
|
+
completedStage = 0;
|
|
574
665
|
constructor(callId, method, remoteExecutionMode = 7) {
|
|
575
666
|
this.callId = callId;
|
|
576
667
|
this.method = method;
|
|
577
668
|
this.remoteExecutionMode = remoteExecutionMode;
|
|
578
669
|
}
|
|
670
|
+
/**
|
|
671
|
+
* Returns the stage to report to the remote peer via `$sys.Reconnect`,
|
|
672
|
+
* or `null` if this call should not be reconciled and must be aborted.
|
|
673
|
+
*
|
|
674
|
+
* Mirrors .NET `RpcOutboundCall.GetReconnectStage` at
|
|
675
|
+
* src/ActualLab.Rpc/Infrastructure/RpcOutboundCall.cs.
|
|
676
|
+
*/
|
|
677
|
+
getReconnectStage(isPeerChanged) {
|
|
678
|
+
const mode = this.remoteExecutionMode;
|
|
679
|
+
if (!(mode & RpcRemoteExecutionMode.AllowReconnect)) return null;
|
|
680
|
+
if (isPeerChanged && !(mode & RpcRemoteExecutionMode.AllowResend)) return null;
|
|
681
|
+
return this.completedStage;
|
|
682
|
+
}
|
|
579
683
|
/** Called when the connection is lost. Subclasses can override to resolve invalidation promises. */
|
|
580
684
|
onDisconnect() {
|
|
581
685
|
}
|
|
@@ -597,7 +701,10 @@ var RpcOutboundCallTracker = class {
|
|
|
597
701
|
}
|
|
598
702
|
remove(callId) {
|
|
599
703
|
const call = this._calls.get(callId);
|
|
600
|
-
if (call !== void 0)
|
|
704
|
+
if (call !== void 0) {
|
|
705
|
+
this._calls.delete(callId);
|
|
706
|
+
call.completedStage |= RpcCallStage.Unregistered;
|
|
707
|
+
}
|
|
601
708
|
return call;
|
|
602
709
|
}
|
|
603
710
|
values() {
|
|
@@ -660,6 +767,97 @@ var RpcInboundCallTracker = class {
|
|
|
660
767
|
}
|
|
661
768
|
};
|
|
662
769
|
|
|
770
|
+
// src/increasing-seq-compressor.ts
|
|
771
|
+
var IncreasingSeqCompressor = {
|
|
772
|
+
/**
|
|
773
|
+
* Serialize a non-decreasing sequence of non-negative integers as
|
|
774
|
+
* LEB128-encoded deltas.
|
|
775
|
+
*
|
|
776
|
+
* Throws `RangeError` if the input is not monotonically non-decreasing,
|
|
777
|
+
* or if a value exceeds `Number.MAX_SAFE_INTEGER`.
|
|
778
|
+
*/
|
|
779
|
+
serialize(values) {
|
|
780
|
+
const deltas = [];
|
|
781
|
+
let last = 0;
|
|
782
|
+
let totalBytes = 0;
|
|
783
|
+
for (const value of values) {
|
|
784
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
785
|
+
throw new RangeError(
|
|
786
|
+
`IncreasingSeqCompressor: value must be a non-negative safe integer (got ${value}).`
|
|
787
|
+
);
|
|
788
|
+
const delta = value - last;
|
|
789
|
+
if (delta < 0)
|
|
790
|
+
throw new RangeError(
|
|
791
|
+
`IncreasingSeqCompressor: sequence must be non-decreasing (saw ${value} after ${last}).`
|
|
792
|
+
);
|
|
793
|
+
deltas.push(delta);
|
|
794
|
+
last = value;
|
|
795
|
+
totalBytes += _varUintByteLength(delta);
|
|
796
|
+
}
|
|
797
|
+
const out = new Uint8Array(totalBytes);
|
|
798
|
+
let offset = 0;
|
|
799
|
+
for (const delta of deltas) {
|
|
800
|
+
offset = _writeVarUint(out, delta, offset);
|
|
801
|
+
}
|
|
802
|
+
return out;
|
|
803
|
+
},
|
|
804
|
+
/**
|
|
805
|
+
* Decode bytes produced by {@link serialize} back into the original
|
|
806
|
+
* sorted sequence of non-negative integers.
|
|
807
|
+
*/
|
|
808
|
+
deserialize(data) {
|
|
809
|
+
const result = [];
|
|
810
|
+
let last = 0;
|
|
811
|
+
let offset = 0;
|
|
812
|
+
while (offset < data.length) {
|
|
813
|
+
const [delta, next] = _readVarUint(data, offset);
|
|
814
|
+
offset = next;
|
|
815
|
+
last += delta;
|
|
816
|
+
result.push(last);
|
|
817
|
+
}
|
|
818
|
+
return result;
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
var LOW_BITS = 127;
|
|
822
|
+
var HIGH_BIT = 128;
|
|
823
|
+
function _varUintByteLength(value) {
|
|
824
|
+
let n = 0;
|
|
825
|
+
let v = value;
|
|
826
|
+
while (v >= HIGH_BIT) {
|
|
827
|
+
n++;
|
|
828
|
+
v = Math.floor(v / 128);
|
|
829
|
+
}
|
|
830
|
+
return n + 1;
|
|
831
|
+
}
|
|
832
|
+
function _writeVarUint(out, value, offset) {
|
|
833
|
+
let v = value;
|
|
834
|
+
while (v >= HIGH_BIT) {
|
|
835
|
+
out[offset++] = (HIGH_BIT | v & LOW_BITS) & 255;
|
|
836
|
+
v = Math.floor(v / 128);
|
|
837
|
+
}
|
|
838
|
+
out[offset++] = v & 255;
|
|
839
|
+
return offset;
|
|
840
|
+
}
|
|
841
|
+
function _readVarUint(data, offset) {
|
|
842
|
+
let value = 0;
|
|
843
|
+
let shift = 0;
|
|
844
|
+
while (shift < 63) {
|
|
845
|
+
if (offset >= data.length)
|
|
846
|
+
throw new RangeError("IncreasingSeqCompressor: truncated VarUInt.");
|
|
847
|
+
const b2 = data[offset++];
|
|
848
|
+
value += (b2 & LOW_BITS) * Math.pow(2, shift);
|
|
849
|
+
if (b2 <= LOW_BITS) return [value, offset];
|
|
850
|
+
shift += 7;
|
|
851
|
+
}
|
|
852
|
+
if (offset >= data.length)
|
|
853
|
+
throw new RangeError("IncreasingSeqCompressor: truncated VarUInt.");
|
|
854
|
+
const b = data[offset++];
|
|
855
|
+
if (b > 1)
|
|
856
|
+
throw new RangeError("IncreasingSeqCompressor: VarUInt value exceeds 64 bits.");
|
|
857
|
+
value += b * Math.pow(2, 63);
|
|
858
|
+
return [value, offset];
|
|
859
|
+
}
|
|
860
|
+
|
|
663
861
|
// src/rpc-stream.ts
|
|
664
862
|
import Denque from "denque";
|
|
665
863
|
import { PromiseSource as PromiseSource4 } from "@actuallab/core";
|
|
@@ -668,7 +866,8 @@ import { PromiseSource as PromiseSource4 } from "@actuallab/core";
|
|
|
668
866
|
var RpcObjectKind = { Local: 0, Remote: 1 };
|
|
669
867
|
|
|
670
868
|
// src/rpc-stream-sender.ts
|
|
671
|
-
import { PromiseSource as PromiseSource3 } from "@actuallab/core";
|
|
869
|
+
import { PromiseSource as PromiseSource3, RingBuffer } from "@actuallab/core";
|
|
870
|
+
var { warnLog: warnLog3 } = getLogs("RpcSharedStream");
|
|
672
871
|
var DEFAULT_ACK_PERIOD = 256;
|
|
673
872
|
var DEFAULT_ACK_ADVANCE = 128;
|
|
674
873
|
var RpcStreamSender = class _RpcStreamSender {
|
|
@@ -681,15 +880,29 @@ var RpcStreamSender = class _RpcStreamSender {
|
|
|
681
880
|
peer;
|
|
682
881
|
ackPeriod;
|
|
683
882
|
ackAdvance;
|
|
883
|
+
/**
|
|
884
|
+
* If true, the source was produced by a factory `(abortSignal) => AsyncIterable<T>`
|
|
885
|
+
* and may honor the AbortSignal to exit gracefully. `disconnect()` first aborts
|
|
886
|
+
* the signal and gives the source up to `RpcStream.disconnectGracePeriodMs` ms
|
|
887
|
+
* before force-closing via `iterator.return()`.
|
|
888
|
+
*
|
|
889
|
+
* If false, the source is a plain AsyncIterable that cannot observe the signal,
|
|
890
|
+
* so `disconnect()` force-closes immediately.
|
|
891
|
+
*/
|
|
892
|
+
sourceUsesAbortSignal;
|
|
893
|
+
// -- Pump state --
|
|
894
|
+
/** Next stream index that will be sent (matches .NET's `index`). */
|
|
684
895
|
_nextIndex = 0;
|
|
685
896
|
_ended = false;
|
|
686
897
|
_started = new PromiseSource3();
|
|
898
|
+
/** Highest `nextIndex` observed in an ACK so far (matches .NET's `lastAckNextIndex` idiom). */
|
|
687
899
|
_lastAckedIndex = 0;
|
|
688
|
-
_ackWaiting = null;
|
|
689
|
-
_resetRequested = false;
|
|
690
900
|
_abortController = new AbortController();
|
|
691
901
|
_iterator = null;
|
|
692
|
-
|
|
902
|
+
// -- ACK queue (analog of .NET's Channel<(long, bool)>) --
|
|
903
|
+
_acks = [];
|
|
904
|
+
_whenAckReady = null;
|
|
905
|
+
constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true, isRealTime = false, canSkipTo = () => true, sourceUsesAbortSignal = false) {
|
|
693
906
|
const localId = peer.sharedObjects.nextId();
|
|
694
907
|
this.id = { hostId: peer.hub.hubId, localId };
|
|
695
908
|
this.allowReconnect = allowReconnect;
|
|
@@ -698,6 +911,7 @@ var RpcStreamSender = class _RpcStreamSender {
|
|
|
698
911
|
this.peer = peer;
|
|
699
912
|
this.ackPeriod = ackPeriod;
|
|
700
913
|
this.ackAdvance = ackAdvance;
|
|
914
|
+
this.sourceUsesAbortSignal = sourceUsesAbortSignal;
|
|
701
915
|
}
|
|
702
916
|
/** AbortSignal that is aborted when the sender is disconnected. */
|
|
703
917
|
get abortSignal() {
|
|
@@ -707,56 +921,61 @@ var RpcStreamSender = class _RpcStreamSender {
|
|
|
707
921
|
toRef() {
|
|
708
922
|
return `${this.id.hostId},${this.id.localId},${this.ackPeriod},${this.ackAdvance},${this.allowReconnect ? "1" : "0"},${this.isRealTime ? "1" : "0"}`;
|
|
709
923
|
}
|
|
710
|
-
/**
|
|
924
|
+
/**
|
|
925
|
+
* Called by system call handler when $sys.Ack is received from the client.
|
|
926
|
+
*
|
|
927
|
+
* Mirrors .NET `RpcSharedStream<T>.OnAck` at
|
|
928
|
+
* src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:68-101.
|
|
929
|
+
*/
|
|
711
930
|
onAck(nextIndex, hostId) {
|
|
712
931
|
const mustReset = hostId !== "" && hostId !== _RpcStreamSender._emptyGuid;
|
|
713
|
-
if (!this._started.isCompleted)
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
this.
|
|
718
|
-
this._lastAckedIndex = nextIndex;
|
|
719
|
-
this._resetRequested = true;
|
|
720
|
-
} else {
|
|
721
|
-
if (mustReset || nextIndex > this._lastAckedIndex) {
|
|
722
|
-
this._lastAckedIndex = nextIndex;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
if (this._ackWaiting) {
|
|
726
|
-
this._ackWaiting.resolve();
|
|
727
|
-
this._ackWaiting = null;
|
|
932
|
+
if (!this._started.isCompleted) this._started.resolve();
|
|
933
|
+
this._acks.push({ nextIndex, mustReset });
|
|
934
|
+
if (this._whenAckReady) {
|
|
935
|
+
this._whenAckReady.resolve();
|
|
936
|
+
this._whenAckReady = null;
|
|
728
937
|
}
|
|
729
938
|
}
|
|
730
939
|
/** Called by system call handler when $sys.AckEnd is received from the client. */
|
|
731
940
|
onAckEnd(_hostId) {
|
|
732
941
|
this.disconnect();
|
|
733
942
|
}
|
|
734
|
-
/**
|
|
943
|
+
/**
|
|
944
|
+
* Send a single item to the client at the current `_nextIndex`.
|
|
945
|
+
*
|
|
946
|
+
* Advances `_nextIndex` unconditionally — even if the peer is disconnected
|
|
947
|
+
* the item is considered "sent" from the pump's perspective so the replay
|
|
948
|
+
* buffer indexing stays consistent. If a connection is absent, the wire
|
|
949
|
+
* message is dropped; the item will be resent from the replay buffer
|
|
950
|
+
* after a reconnect (client sends `Ack(N, mustReset=true)`).
|
|
951
|
+
*/
|
|
735
952
|
sendItem(item) {
|
|
736
953
|
if (this._ended) return;
|
|
737
954
|
const conn = this.peer.connection;
|
|
738
|
-
if (
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
955
|
+
if (conn) {
|
|
956
|
+
this.peer.hub.systemCallSender.item(
|
|
957
|
+
conn,
|
|
958
|
+
this.peer.format,
|
|
959
|
+
this.id.localId,
|
|
960
|
+
this._nextIndex,
|
|
961
|
+
item
|
|
962
|
+
);
|
|
963
|
+
}
|
|
746
964
|
this._nextIndex++;
|
|
747
965
|
}
|
|
748
|
-
/** Send a batch of items to the client. */
|
|
966
|
+
/** Send a batch of items to the client. See {@link sendItem} for disconnect semantics. */
|
|
749
967
|
sendBatch(items) {
|
|
750
968
|
if (this._ended || items.length === 0) return;
|
|
751
969
|
const conn = this.peer.connection;
|
|
752
|
-
if (
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
970
|
+
if (conn) {
|
|
971
|
+
this.peer.hub.systemCallSender.batch(
|
|
972
|
+
conn,
|
|
973
|
+
this.peer.format,
|
|
974
|
+
this.id.localId,
|
|
975
|
+
this._nextIndex,
|
|
976
|
+
items
|
|
977
|
+
);
|
|
978
|
+
}
|
|
760
979
|
this._nextIndex += items.length;
|
|
761
980
|
}
|
|
762
981
|
/** Signal stream completion to the client. */
|
|
@@ -776,76 +995,274 @@ var RpcStreamSender = class _RpcStreamSender {
|
|
|
776
995
|
}
|
|
777
996
|
/**
|
|
778
997
|
* Consume an AsyncIterable and send all items to the client.
|
|
779
|
-
* Waits for the client's initial Ack before starting to pump items.
|
|
780
998
|
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
* one where `canSkipTo` returns true.
|
|
999
|
+
* Dispatches to the non-real-time or real-time pump, matching .NET
|
|
1000
|
+
* `RpcSharedStream<T>.OnRun` at
|
|
1001
|
+
* src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:105-108.
|
|
785
1002
|
*/
|
|
786
1003
|
async writeFrom(source) {
|
|
787
1004
|
await this._started.promise;
|
|
1005
|
+
if (this._ended) return;
|
|
788
1006
|
const iterator = source[Symbol.asyncIterator]();
|
|
789
1007
|
this._iterator = iterator;
|
|
1008
|
+
const state = { iteratorDone: false };
|
|
790
1009
|
try {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
1010
|
+
if (this.isRealTime) {
|
|
1011
|
+
await this._runRealTime(iterator, state);
|
|
1012
|
+
} else {
|
|
1013
|
+
await this._runNonRealTime(iterator, state);
|
|
1014
|
+
}
|
|
1015
|
+
} finally {
|
|
1016
|
+
if (state.iteratorDone && this._iterator === iterator) {
|
|
1017
|
+
this._iterator = null;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Non-real-time pump — back-pressure through buffered ACKs.
|
|
1023
|
+
*
|
|
1024
|
+
* Direct port of .NET `OnRunNonRealTime` at
|
|
1025
|
+
* src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:110-217.
|
|
1026
|
+
*/
|
|
1027
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-condition -- _ended changes across awaits */
|
|
1028
|
+
async _runNonRealTime(iterator, state) {
|
|
1029
|
+
const buffer = new RingBuffer(this.ackAdvance + 1);
|
|
1030
|
+
let bufferStart = 0;
|
|
1031
|
+
let isFullyBuffered = false;
|
|
1032
|
+
while (true) {
|
|
1033
|
+
let ack = this._tryProcessAcks();
|
|
1034
|
+
if (!ack) {
|
|
1035
|
+
await this._waitAckReady();
|
|
1036
|
+
if (this._ended) return;
|
|
1037
|
+
ack = this._tryProcessAcks();
|
|
1038
|
+
if (!ack) {
|
|
1039
|
+
warnLog3?.log("Something is off: couldn't read an acknowledgement");
|
|
1040
|
+
return;
|
|
800
1041
|
}
|
|
801
|
-
|
|
802
|
-
|
|
1042
|
+
}
|
|
1043
|
+
this._lastAckedIndex = ack.nextIndex;
|
|
1044
|
+
const shift = _clamp(ack.nextIndex - bufferStart, 0, buffer.count);
|
|
1045
|
+
if (shift > 0) {
|
|
1046
|
+
buffer.moveHead(shift);
|
|
1047
|
+
bufferStart += shift;
|
|
1048
|
+
}
|
|
1049
|
+
if (this._nextIndex < bufferStart) {
|
|
1050
|
+
if (!this._ended)
|
|
1051
|
+
this.sendEnd(new Error("Stream position unavailable."));
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
let bufferIndex = this._nextIndex - bufferStart;
|
|
1055
|
+
const maxIndex = ack.nextIndex + this.ackAdvance;
|
|
1056
|
+
while (this._nextIndex < maxIndex) {
|
|
1057
|
+
if (this._ended) return;
|
|
1058
|
+
if (bufferIndex < buffer.count) {
|
|
1059
|
+
const item = buffer.get(bufferIndex++);
|
|
1060
|
+
if (item.kind === "value") {
|
|
1061
|
+
this.sendItem(item.value);
|
|
1062
|
+
} else if (item.kind === "end") {
|
|
1063
|
+
if (!this._ended) this.sendEnd();
|
|
1064
|
+
return;
|
|
1065
|
+
} else {
|
|
1066
|
+
if (!this._ended) this.sendEnd(item.error);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
803
1069
|
continue;
|
|
804
1070
|
}
|
|
805
|
-
if (
|
|
806
|
-
|
|
1071
|
+
if (isFullyBuffered) break;
|
|
1072
|
+
if (!buffer.hasRemainingCapacity) break;
|
|
1073
|
+
let pulled;
|
|
1074
|
+
try {
|
|
1075
|
+
const r = await iterator.next();
|
|
807
1076
|
if (this._ended) return;
|
|
808
|
-
|
|
809
|
-
|
|
1077
|
+
if (r.done) {
|
|
1078
|
+
pulled = _endItem;
|
|
1079
|
+
isFullyBuffered = true;
|
|
1080
|
+
state.iteratorDone = true;
|
|
1081
|
+
} else {
|
|
1082
|
+
pulled = { kind: "value", value: r.value };
|
|
1083
|
+
}
|
|
1084
|
+
} catch (e) {
|
|
1085
|
+
state.iteratorDone = true;
|
|
1086
|
+
isFullyBuffered = true;
|
|
1087
|
+
pulled = {
|
|
1088
|
+
kind: "error",
|
|
1089
|
+
error: e instanceof Error ? e : new Error(String(e))
|
|
1090
|
+
};
|
|
810
1091
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1092
|
+
buffer.pushTail(pulled);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Real-time pump — drops stale items to stay close to the source's head.
|
|
1098
|
+
*
|
|
1099
|
+
* Direct port of .NET `OnRunRealTime` at
|
|
1100
|
+
* src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:219-420.
|
|
1101
|
+
*/
|
|
1102
|
+
async _runRealTime(iterator, state) {
|
|
1103
|
+
const buffer = new RingBuffer(this.ackAdvance + 1);
|
|
1104
|
+
let bufferStart = 0;
|
|
1105
|
+
let isFullyBuffered = false;
|
|
1106
|
+
while (true) {
|
|
1107
|
+
let ack = this._tryProcessAcks();
|
|
1108
|
+
if (!ack) {
|
|
1109
|
+
await this._waitAckReady();
|
|
1110
|
+
if (this._ended) return;
|
|
1111
|
+
ack = this._tryProcessAcks();
|
|
1112
|
+
if (!ack) {
|
|
1113
|
+
warnLog3?.log("Something is off: couldn't read an acknowledgement");
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
this._lastAckedIndex = ack.nextIndex;
|
|
1118
|
+
if (ack.mustReset && !isFullyBuffered) {
|
|
1119
|
+
buffer.clear();
|
|
1120
|
+
bufferStart = this._nextIndex;
|
|
1121
|
+
while (true) {
|
|
814
1122
|
if (this._ended) return;
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1123
|
+
let item;
|
|
1124
|
+
let accepted = false;
|
|
1125
|
+
try {
|
|
1126
|
+
const r = await iterator.next();
|
|
1127
|
+
if (this._ended) return;
|
|
1128
|
+
if (r.done) {
|
|
1129
|
+
item = _endItem;
|
|
1130
|
+
isFullyBuffered = true;
|
|
1131
|
+
state.iteratorDone = true;
|
|
1132
|
+
} else if (this.canSkipTo(r.value)) {
|
|
1133
|
+
item = { kind: "value", value: r.value };
|
|
1134
|
+
accepted = true;
|
|
1135
|
+
} else {
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
} catch (e) {
|
|
1139
|
+
state.iteratorDone = true;
|
|
1140
|
+
isFullyBuffered = true;
|
|
1141
|
+
item = {
|
|
1142
|
+
kind: "error",
|
|
1143
|
+
error: e instanceof Error ? e : new Error(String(e))
|
|
1144
|
+
};
|
|
822
1145
|
}
|
|
1146
|
+
buffer.pushTail(item);
|
|
1147
|
+
if (accepted || isFullyBuffered) break;
|
|
823
1148
|
}
|
|
1149
|
+
}
|
|
1150
|
+
const shift = _clamp(ack.nextIndex - bufferStart, 0, buffer.count);
|
|
1151
|
+
if (shift > 0) {
|
|
1152
|
+
buffer.moveHead(shift);
|
|
1153
|
+
bufferStart += shift;
|
|
1154
|
+
}
|
|
1155
|
+
if (this._nextIndex < bufferStart) {
|
|
1156
|
+
if (!this._ended)
|
|
1157
|
+
this.sendEnd(new Error("Stream position unavailable."));
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
let bufferIndex = this._nextIndex - bufferStart;
|
|
1161
|
+
const maxIndex = ack.nextIndex + this.ackAdvance;
|
|
1162
|
+
while (this._nextIndex < maxIndex) {
|
|
824
1163
|
if (this._ended) return;
|
|
825
|
-
if (
|
|
826
|
-
|
|
1164
|
+
if (this._acks.length > 0) break;
|
|
1165
|
+
if (bufferIndex < buffer.count) {
|
|
1166
|
+
const item = buffer.get(bufferIndex++);
|
|
1167
|
+
if (item.kind === "value") {
|
|
1168
|
+
this.sendItem(item.value);
|
|
1169
|
+
} else if (item.kind === "end") {
|
|
1170
|
+
if (!this._ended) this.sendEnd();
|
|
1171
|
+
return;
|
|
1172
|
+
} else {
|
|
1173
|
+
if (!this._ended) this.sendEnd(item.error);
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
continue;
|
|
827
1177
|
}
|
|
828
|
-
if (
|
|
829
|
-
|
|
830
|
-
|
|
1178
|
+
if (isFullyBuffered) break;
|
|
1179
|
+
if (!buffer.hasRemainingCapacity) break;
|
|
1180
|
+
let pulled;
|
|
1181
|
+
try {
|
|
1182
|
+
const r = await iterator.next();
|
|
1183
|
+
if (this._ended) return;
|
|
1184
|
+
if (r.done) {
|
|
1185
|
+
pulled = _endItem;
|
|
1186
|
+
isFullyBuffered = true;
|
|
1187
|
+
state.iteratorDone = true;
|
|
1188
|
+
} else {
|
|
1189
|
+
pulled = { kind: "value", value: r.value };
|
|
1190
|
+
}
|
|
1191
|
+
} catch (e) {
|
|
1192
|
+
state.iteratorDone = true;
|
|
1193
|
+
isFullyBuffered = true;
|
|
1194
|
+
pulled = {
|
|
1195
|
+
kind: "error",
|
|
1196
|
+
error: e instanceof Error ? e : new Error(String(e))
|
|
1197
|
+
};
|
|
831
1198
|
}
|
|
1199
|
+
buffer.pushTail(pulled);
|
|
832
1200
|
}
|
|
833
|
-
if (!this._ended) {
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1201
|
+
if (this._nextIndex >= maxIndex && !isFullyBuffered && !this._ended) {
|
|
1202
|
+
let latestValue;
|
|
1203
|
+
let haveLatest = false;
|
|
1204
|
+
let exitReason = "ack";
|
|
1205
|
+
let exitError = null;
|
|
1206
|
+
drain: while (!this._ended) {
|
|
1207
|
+
if (this._acks.length > 0) {
|
|
1208
|
+
exitReason = "ack";
|
|
1209
|
+
break drain;
|
|
1210
|
+
}
|
|
1211
|
+
let r;
|
|
1212
|
+
try {
|
|
1213
|
+
r = await iterator.next();
|
|
1214
|
+
} catch (e) {
|
|
1215
|
+
state.iteratorDone = true;
|
|
1216
|
+
isFullyBuffered = true;
|
|
1217
|
+
exitReason = "error";
|
|
1218
|
+
exitError = e instanceof Error ? e : new Error(String(e));
|
|
1219
|
+
break drain;
|
|
1220
|
+
}
|
|
1221
|
+
if (this._ended) return;
|
|
1222
|
+
if (r.done) {
|
|
1223
|
+
state.iteratorDone = true;
|
|
1224
|
+
isFullyBuffered = true;
|
|
1225
|
+
exitReason = "end";
|
|
1226
|
+
break drain;
|
|
1227
|
+
}
|
|
1228
|
+
if (this.canSkipTo(r.value)) {
|
|
1229
|
+
latestValue = r.value;
|
|
1230
|
+
haveLatest = true;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (exitReason === "end" || exitReason === "error") {
|
|
1234
|
+
if (haveLatest && !this._ended) this.sendItem(latestValue);
|
|
1235
|
+
if (!this._ended) this.sendEnd(exitError);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (haveLatest) {
|
|
1239
|
+
const wasFull = buffer.isFull;
|
|
1240
|
+
buffer.pushTailAndMoveHeadIfFull({ kind: "value", value: latestValue });
|
|
1241
|
+
if (wasFull) bufferStart++;
|
|
1242
|
+
}
|
|
839
1243
|
}
|
|
840
1244
|
}
|
|
841
1245
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1246
|
+
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
|
|
1247
|
+
/** Drain all queued ACKs, returning the most recent one (or null if none). */
|
|
1248
|
+
_tryProcessAcks() {
|
|
1249
|
+
if (this._acks.length === 0) return null;
|
|
1250
|
+
let last = null;
|
|
1251
|
+
while (this._acks.length > 0) {
|
|
1252
|
+
const a = this._acks.shift();
|
|
1253
|
+
last = a;
|
|
1254
|
+
if (a.mustReset || this._nextIndex < a.nextIndex) {
|
|
1255
|
+
this._nextIndex = a.nextIndex;
|
|
1256
|
+
}
|
|
848
1257
|
}
|
|
1258
|
+
return last;
|
|
1259
|
+
}
|
|
1260
|
+
/** Wait for the ACK queue to have at least one entry (or `_ended`). */
|
|
1261
|
+
async _waitAckReady() {
|
|
1262
|
+
if (this._acks.length > 0 || this._ended) return;
|
|
1263
|
+
this._whenAckReady = new PromiseSource3();
|
|
1264
|
+
await this._whenAckReady.promise;
|
|
1265
|
+
this._whenAckReady = null;
|
|
849
1266
|
}
|
|
850
1267
|
// -- IRpcObject --
|
|
851
1268
|
reconnect() {
|
|
@@ -857,19 +1274,41 @@ var RpcStreamSender = class _RpcStreamSender {
|
|
|
857
1274
|
if (!this._started.isCompleted) {
|
|
858
1275
|
this._started.resolve();
|
|
859
1276
|
}
|
|
860
|
-
if (this.
|
|
861
|
-
this.
|
|
862
|
-
this.
|
|
1277
|
+
if (this._whenAckReady) {
|
|
1278
|
+
this._whenAckReady.resolve();
|
|
1279
|
+
this._whenAckReady = null;
|
|
863
1280
|
}
|
|
864
|
-
|
|
865
|
-
|
|
1281
|
+
this.peer.sharedObjects.unregister(this);
|
|
1282
|
+
const iterator = this._iterator;
|
|
1283
|
+
if (!iterator) return;
|
|
1284
|
+
if (!this.sourceUsesAbortSignal) {
|
|
866
1285
|
this._iterator = null;
|
|
1286
|
+
this._forceCloseIterator(iterator);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
setTimeout(() => {
|
|
1290
|
+
if (this._iterator === iterator) {
|
|
1291
|
+
this._iterator = null;
|
|
1292
|
+
this._forceCloseIterator(iterator);
|
|
1293
|
+
}
|
|
1294
|
+
}, RpcStream.disconnectGracePeriodMs);
|
|
1295
|
+
}
|
|
1296
|
+
_forceCloseIterator(iterator) {
|
|
1297
|
+
try {
|
|
1298
|
+
const ret = iterator.return?.(void 0);
|
|
1299
|
+
if (ret) void ret.catch(() => {
|
|
1300
|
+
});
|
|
1301
|
+
} catch {
|
|
867
1302
|
}
|
|
868
|
-
this.peer.sharedObjects.unregister(this);
|
|
869
1303
|
}
|
|
870
1304
|
};
|
|
1305
|
+
var _endItem = { kind: "end" };
|
|
1306
|
+
function _clamp(value, min, max) {
|
|
1307
|
+
return value < min ? min : value > max ? max : value;
|
|
1308
|
+
}
|
|
871
1309
|
|
|
872
1310
|
// src/rpc-stream.ts
|
|
1311
|
+
var { warnLog: warnLog4 } = getLogs("RpcStream");
|
|
873
1312
|
var DEFAULT_ACK_PERIOD2 = 30;
|
|
874
1313
|
var DEFAULT_ACK_ADVANCE2 = 61;
|
|
875
1314
|
function parseStreamRef(value) {
|
|
@@ -903,6 +1342,15 @@ function parseStreamRef(value) {
|
|
|
903
1342
|
return null;
|
|
904
1343
|
}
|
|
905
1344
|
var RpcStream = class _RpcStream {
|
|
1345
|
+
/**
|
|
1346
|
+
* Grace period (ms) given to a factory-form local source to honor its
|
|
1347
|
+
* AbortSignal and exit naturally before the sender force-closes the
|
|
1348
|
+
* iterator via `iterator.return()`.
|
|
1349
|
+
*
|
|
1350
|
+
* Only applies to sources provided as `(abortSignal) => AsyncIterable<T>`.
|
|
1351
|
+
* Plain `AsyncIterable<T>` sources are force-closed immediately on disconnect.
|
|
1352
|
+
*/
|
|
1353
|
+
static disconnectGracePeriodMs = 100;
|
|
906
1354
|
id;
|
|
907
1355
|
kind;
|
|
908
1356
|
allowReconnect;
|
|
@@ -980,17 +1428,19 @@ var RpcStream = class _RpcStream {
|
|
|
980
1428
|
throw new Error("toRef() was already called with a different peer.");
|
|
981
1429
|
return this._toRefValue;
|
|
982
1430
|
}
|
|
1431
|
+
const isFactory = typeof this.localSource === "function";
|
|
983
1432
|
const sender = new RpcStreamSender(
|
|
984
1433
|
peer,
|
|
985
1434
|
this.ackPeriod,
|
|
986
1435
|
this.ackAdvance,
|
|
987
1436
|
this.allowReconnect,
|
|
988
1437
|
this.isRealTime,
|
|
989
|
-
this.canSkipTo
|
|
1438
|
+
this.canSkipTo,
|
|
1439
|
+
isFactory
|
|
990
1440
|
);
|
|
991
1441
|
peer.sharedObjects.register(sender);
|
|
992
1442
|
this._sender = sender;
|
|
993
|
-
const source =
|
|
1443
|
+
const source = isFactory ? this.localSource(sender.abortSignal) : this.localSource;
|
|
994
1444
|
this._whenSent = sender.writeFrom(source);
|
|
995
1445
|
this._toRefValue = peer.format.isBinary ? {
|
|
996
1446
|
SerializedId: [sender.id.hostId, sender.id.localId],
|
|
@@ -1004,9 +1454,7 @@ var RpcStream = class _RpcStream {
|
|
|
1004
1454
|
onItem(index, item) {
|
|
1005
1455
|
if (this._disposed || this._completed) return;
|
|
1006
1456
|
if (index > this._nextExpectedIndex) {
|
|
1007
|
-
|
|
1008
|
-
`[RpcStream] item index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`
|
|
1009
|
-
);
|
|
1457
|
+
warnLog4?.log(`item index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
|
|
1010
1458
|
if (!this.allowReconnect) {
|
|
1011
1459
|
this._completed = true;
|
|
1012
1460
|
this._completionError = new Error(
|
|
@@ -1031,9 +1479,7 @@ var RpcStream = class _RpcStream {
|
|
|
1031
1479
|
onBatch(index, items) {
|
|
1032
1480
|
if (this._disposed || this._completed) return;
|
|
1033
1481
|
if (index > this._nextExpectedIndex) {
|
|
1034
|
-
|
|
1035
|
-
`[RpcStream] batch index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`
|
|
1036
|
-
);
|
|
1482
|
+
warnLog4?.log(`batch index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
|
|
1037
1483
|
if (!this.allowReconnect) {
|
|
1038
1484
|
this._completed = true;
|
|
1039
1485
|
this._completionError = new Error(
|
|
@@ -1200,15 +1646,37 @@ function resolveStreamRefs(value, peer) {
|
|
|
1200
1646
|
return value;
|
|
1201
1647
|
}
|
|
1202
1648
|
|
|
1649
|
+
// src/base64.ts
|
|
1650
|
+
function base64Encode(bytes) {
|
|
1651
|
+
const chunkSize = 32768;
|
|
1652
|
+
let binary = "";
|
|
1653
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
1654
|
+
binary += String.fromCharCode.apply(
|
|
1655
|
+
null,
|
|
1656
|
+
bytes.subarray(i, Math.min(bytes.length, i + chunkSize))
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
return btoa(binary);
|
|
1660
|
+
}
|
|
1661
|
+
function base64Decode(encoded) {
|
|
1662
|
+
const binary = atob(encoded);
|
|
1663
|
+
const out = new Uint8Array(binary.length);
|
|
1664
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
1665
|
+
return out;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1203
1668
|
// src/rpc-system-call-handler.ts
|
|
1669
|
+
var { debugLog, warnLog: warnLog5 } = getLogs("RpcSystemCallHandler");
|
|
1204
1670
|
var RpcSystemCallHandler = class {
|
|
1205
1671
|
handle(message, args, peer) {
|
|
1206
1672
|
const method = message.Method;
|
|
1207
1673
|
const relatedId = message.RelatedId ?? 0;
|
|
1674
|
+
debugLog?.log(`'${peer.ref}': handle ${method}#${relatedId}`);
|
|
1208
1675
|
switch (method) {
|
|
1209
1676
|
case RpcSystemCalls.ok: {
|
|
1210
1677
|
const call = peer.outbound.get(relatedId);
|
|
1211
1678
|
if (call !== void 0) {
|
|
1679
|
+
call.completedStage |= RpcCallStage.ResultReady;
|
|
1212
1680
|
if (call.removeOnOk) {
|
|
1213
1681
|
peer.outbound.remove(relatedId);
|
|
1214
1682
|
}
|
|
@@ -1216,11 +1684,21 @@ var RpcSystemCallHandler = class {
|
|
|
1216
1684
|
}
|
|
1217
1685
|
break;
|
|
1218
1686
|
}
|
|
1687
|
+
case RpcSystemCalls.reconnect: {
|
|
1688
|
+
this._handleReconnect(relatedId, args, peer);
|
|
1689
|
+
break;
|
|
1690
|
+
}
|
|
1219
1691
|
case RpcSystemCalls.error: {
|
|
1220
1692
|
const call = peer.outbound.remove(relatedId);
|
|
1221
1693
|
if (call !== void 0) {
|
|
1222
1694
|
const errorInfo = args[0];
|
|
1223
1695
|
const msg = errorInfo?.Message ?? errorInfo?.message ?? "RPC error";
|
|
1696
|
+
const errorType = errorInfo?.TypeRef ?? errorInfo?.typeRef;
|
|
1697
|
+
if (errorType !== null && typeof errorType === "object") {
|
|
1698
|
+
const t = errorType;
|
|
1699
|
+
if (t.TypeName === "RpcRerouteException" || t.typeName === "RpcRerouteException")
|
|
1700
|
+
warnLog5?.log("Got RpcRerouteException from remote peer:", msg);
|
|
1701
|
+
}
|
|
1224
1702
|
call.result.reject(new Error(msg));
|
|
1225
1703
|
}
|
|
1226
1704
|
break;
|
|
@@ -1234,25 +1712,20 @@ var RpcSystemCallHandler = class {
|
|
|
1234
1712
|
}
|
|
1235
1713
|
case RpcSystemCalls.item: {
|
|
1236
1714
|
const stream = peer.remoteObjects.get(relatedId);
|
|
1237
|
-
if (!stream)
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
);
|
|
1241
|
-
} else {
|
|
1715
|
+
if (!stream)
|
|
1716
|
+
debugLog?.log(`$sys.I: no stream for relatedId=${relatedId}`);
|
|
1717
|
+
else
|
|
1242
1718
|
stream.onItem(
|
|
1243
1719
|
args[0],
|
|
1244
1720
|
resolveStreamRefs(args[1], peer)
|
|
1245
1721
|
);
|
|
1246
|
-
}
|
|
1247
1722
|
break;
|
|
1248
1723
|
}
|
|
1249
1724
|
case RpcSystemCalls.batch: {
|
|
1250
1725
|
const stream = peer.remoteObjects.get(relatedId);
|
|
1251
|
-
if (!stream)
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
);
|
|
1255
|
-
} else if (Array.isArray(args[1])) {
|
|
1726
|
+
if (!stream)
|
|
1727
|
+
debugLog?.log(`$sys.B: no stream for relatedId=${relatedId}`);
|
|
1728
|
+
else if (Array.isArray(args[1])) {
|
|
1256
1729
|
const items = args[1];
|
|
1257
1730
|
for (let i = 0; i < items.length; i++)
|
|
1258
1731
|
items[i] = resolveStreamRefs(items[i], peer);
|
|
@@ -1263,9 +1736,7 @@ var RpcSystemCallHandler = class {
|
|
|
1263
1736
|
case RpcSystemCalls.end: {
|
|
1264
1737
|
const stream = peer.remoteObjects.get(relatedId);
|
|
1265
1738
|
if (!stream) {
|
|
1266
|
-
|
|
1267
|
-
`[RpcSysHandler] $sys.End: no stream for relatedId=${relatedId}`
|
|
1268
|
-
);
|
|
1739
|
+
debugLog?.log(`$sys.End: no stream for relatedId=${relatedId}`);
|
|
1269
1740
|
} else {
|
|
1270
1741
|
const errorInfo = args[1];
|
|
1271
1742
|
const msg = errorInfo?.Message ?? errorInfo?.message;
|
|
@@ -1304,9 +1775,57 @@ var RpcSystemCallHandler = class {
|
|
|
1304
1775
|
}
|
|
1305
1776
|
}
|
|
1306
1777
|
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Server-side handler for the `$sys.Reconnect` call. Parses the peer's
|
|
1780
|
+
* report of in-flight call IDs (grouped by stage, IncreasingSeqCompressor-
|
|
1781
|
+
* encoded, base64-wrapped for JSON transport), computes the subset this
|
|
1782
|
+
* peer does not recognize, and replies with a compressed byte[] of the
|
|
1783
|
+
* unknown IDs wrapped in `$sys.Ok`.
|
|
1784
|
+
*
|
|
1785
|
+
* @param relatedId The outbound call ID the client used for this
|
|
1786
|
+
* `$sys.Reconnect` invocation — we send `$sys.Ok` back
|
|
1787
|
+
* with the same `RelatedId`.
|
|
1788
|
+
*/
|
|
1789
|
+
_handleReconnect(relatedId, args, peer) {
|
|
1790
|
+
if (peer.connection === void 0) return;
|
|
1791
|
+
let unknownIds = [];
|
|
1792
|
+
try {
|
|
1793
|
+
const stagesRaw = args[1];
|
|
1794
|
+
if (stagesRaw && typeof stagesRaw === "object" && !Array.isArray(stagesRaw)) {
|
|
1795
|
+
const unknownSet = /* @__PURE__ */ new Set();
|
|
1796
|
+
for (const [, rawValue] of Object.entries(stagesRaw)) {
|
|
1797
|
+
const bytes = _toBytes(rawValue);
|
|
1798
|
+
if (bytes === null) continue;
|
|
1799
|
+
const callIds = IncreasingSeqCompressor.deserialize(bytes);
|
|
1800
|
+
for (const callId of callIds) {
|
|
1801
|
+
if (peer.inbound.get(callId) === void 0) {
|
|
1802
|
+
unknownSet.add(callId);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
unknownIds = [...unknownSet].sort((a, b) => a - b);
|
|
1807
|
+
}
|
|
1808
|
+
} catch {
|
|
1809
|
+
}
|
|
1810
|
+
const responseBytes = IncreasingSeqCompressor.serialize(unknownIds);
|
|
1811
|
+
const responseValue = peer.format.isBinary ? responseBytes : base64Encode(responseBytes);
|
|
1812
|
+
peer.hub.systemCallSender.ok(peer.connection, peer.format, relatedId, responseValue);
|
|
1813
|
+
}
|
|
1307
1814
|
};
|
|
1815
|
+
function _toBytes(value) {
|
|
1816
|
+
if (value instanceof Uint8Array) return value;
|
|
1817
|
+
if (typeof value === "string") {
|
|
1818
|
+
try {
|
|
1819
|
+
return base64Decode(value);
|
|
1820
|
+
} catch {
|
|
1821
|
+
return null;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return null;
|
|
1825
|
+
}
|
|
1308
1826
|
|
|
1309
1827
|
// src/rpc-system-call-sender.ts
|
|
1828
|
+
var { errorLog } = getLogs("RpcSystemCallSender");
|
|
1310
1829
|
var RpcSystemCallSender = class _RpcSystemCallSender {
|
|
1311
1830
|
// Reusable envelopes — one per hot-path method.
|
|
1312
1831
|
_itemEnv = {
|
|
@@ -1363,12 +1882,17 @@ var RpcSystemCallSender = class _RpcSystemCallSender {
|
|
|
1363
1882
|
]);
|
|
1364
1883
|
}
|
|
1365
1884
|
ok(conn, format, relatedId, result) {
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1885
|
+
try {
|
|
1886
|
+
this._send(
|
|
1887
|
+
conn,
|
|
1888
|
+
format,
|
|
1889
|
+
{ Method: RpcSystemCalls.ok, RelatedId: relatedId },
|
|
1890
|
+
[result]
|
|
1891
|
+
);
|
|
1892
|
+
} catch (e) {
|
|
1893
|
+
errorLog?.log(`Failed to send Ok response for call #${relatedId}`, e);
|
|
1894
|
+
this.error(conn, format, relatedId, e);
|
|
1895
|
+
}
|
|
1372
1896
|
}
|
|
1373
1897
|
error(conn, format, relatedId, error) {
|
|
1374
1898
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1650,6 +2174,7 @@ var RpcSerializationFormatResolver = class _RpcSerializationFormatResolver {
|
|
|
1650
2174
|
|
|
1651
2175
|
// src/rpc-peer.ts
|
|
1652
2176
|
var RPC_CLOSE_CODE_UNSUPPORTED_FORMAT = 4001;
|
|
2177
|
+
var { debugLog: debugLog2, infoLog, warnLog: warnLog6, errorLog: errorLog2 } = getLogs("RpcPeer");
|
|
1653
2178
|
var defaultConnectionUrlResolver = (peer) => {
|
|
1654
2179
|
const sep = peer.ref.includes("?") ? "&" : "?";
|
|
1655
2180
|
return peer.ref + sep + `clientId=${peer.clientId}&f=${peer.serializationFormat}`;
|
|
@@ -1688,16 +2213,29 @@ var RpcPeer = class {
|
|
|
1688
2213
|
conn.closed.add((ev) => {
|
|
1689
2214
|
this._connection = void 0;
|
|
1690
2215
|
this._stopKeepAlive();
|
|
2216
|
+
if (ev.reason)
|
|
2217
|
+
infoLog?.log(`'${this.ref}': Disconnected: ${ev.reason} (code=${ev.code})`);
|
|
2218
|
+
else
|
|
2219
|
+
infoLog?.log(`'${this.ref}': Disconnected`);
|
|
1691
2220
|
this.disconnected.trigger(ev);
|
|
1692
2221
|
});
|
|
1693
2222
|
void conn.whenConnected.then(() => {
|
|
2223
|
+
infoLog?.log(`'${this.ref}': Connected`);
|
|
1694
2224
|
this.connected.trigger();
|
|
1695
2225
|
this._startKeepAlive();
|
|
1696
2226
|
});
|
|
1697
2227
|
}
|
|
1698
|
-
/**
|
|
2228
|
+
/**
|
|
2229
|
+
* The serialization format used by this peer for outgoing messages.
|
|
2230
|
+
* If not set explicitly, defaults to the resolver's default (JSON /
|
|
2231
|
+
* json5np). Subclasses can pre-assign in their constructor.
|
|
2232
|
+
*/
|
|
2233
|
+
_format;
|
|
1699
2234
|
get format() {
|
|
1700
|
-
return RpcSerializationFormatResolver.Default.defaultFormat;
|
|
2235
|
+
return this._format ?? RpcSerializationFormatResolver.Default.defaultFormat;
|
|
2236
|
+
}
|
|
2237
|
+
set format(value) {
|
|
2238
|
+
this._format = value;
|
|
1701
2239
|
}
|
|
1702
2240
|
call(method, args, options) {
|
|
1703
2241
|
const callId = this.outbound.nextId();
|
|
@@ -1757,6 +2295,7 @@ var RpcPeer = class {
|
|
|
1757
2295
|
this._sendWireData(wireData);
|
|
1758
2296
|
}
|
|
1759
2297
|
close() {
|
|
2298
|
+
infoLog?.log(`'${this.ref}': Stopping`);
|
|
1760
2299
|
this._stopKeepAlive();
|
|
1761
2300
|
this.remoteObjects.disconnectAll();
|
|
1762
2301
|
this.sharedObjects.disconnectAll();
|
|
@@ -1770,6 +2309,7 @@ var RpcPeer = class {
|
|
|
1770
2309
|
this.outbound.invalidateAll();
|
|
1771
2310
|
this._connection?.close();
|
|
1772
2311
|
this._hub.peers.delete(this.ref);
|
|
2312
|
+
infoLog?.log(`'${this.ref}': Stopped`);
|
|
1773
2313
|
}
|
|
1774
2314
|
/** Send any messages buffered while disconnected. Call after connection + handshake are ready. */
|
|
1775
2315
|
_flushPendingSends() {
|
|
@@ -1796,18 +2336,32 @@ var RpcPeer = class {
|
|
|
1796
2336
|
_handleMessage(received) {
|
|
1797
2337
|
let message;
|
|
1798
2338
|
let args;
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
2339
|
+
try {
|
|
2340
|
+
if (received.kind === "binary") {
|
|
2341
|
+
message = received.message;
|
|
2342
|
+
args = received.args;
|
|
2343
|
+
} else {
|
|
2344
|
+
const parsed = deserializeMessage(received.raw);
|
|
2345
|
+
message = parsed.message;
|
|
2346
|
+
args = parsed.args;
|
|
2347
|
+
}
|
|
2348
|
+
} catch (e) {
|
|
2349
|
+
errorLog2?.log(`'${this.ref}': Failed to process inbound message:`, received, e);
|
|
2350
|
+
return;
|
|
1806
2351
|
}
|
|
1807
2352
|
const method = message.Method ?? "";
|
|
2353
|
+
debugLog2?.log(`'${this.ref}': <- ${method}#${message.RelatedId ?? 0}`);
|
|
1808
2354
|
if (method === RpcSystemCalls.handshake) {
|
|
1809
|
-
const
|
|
1810
|
-
if (
|
|
2355
|
+
const raw = args[0];
|
|
2356
|
+
if (raw !== void 0) {
|
|
2357
|
+
const handshake = Array.isArray(raw) ? {
|
|
2358
|
+
RemotePeerId: raw[0],
|
|
2359
|
+
RemoteHubId: raw[2],
|
|
2360
|
+
ProtocolVersion: raw[3],
|
|
2361
|
+
Index: raw[4]
|
|
2362
|
+
} : raw;
|
|
2363
|
+
this._onHandshakeReceived(handshake);
|
|
2364
|
+
}
|
|
1811
2365
|
return;
|
|
1812
2366
|
}
|
|
1813
2367
|
if (method.startsWith("$sys")) {
|
|
@@ -1863,6 +2417,7 @@ var RpcPeer = class {
|
|
|
1863
2417
|
e
|
|
1864
2418
|
);
|
|
1865
2419
|
}
|
|
2420
|
+
debugLog2?.log(`'${this.ref}': Inbound call failed: ${method}#${relatedId}`, e);
|
|
1866
2421
|
} finally {
|
|
1867
2422
|
if (!isNoWait) {
|
|
1868
2423
|
this.inbound.remove(relatedId);
|
|
@@ -1897,6 +2452,12 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
1897
2452
|
_connectionKind = 0 /* Disconnected */;
|
|
1898
2453
|
_tryIndex = 0;
|
|
1899
2454
|
_handshakeIndex = 0;
|
|
2455
|
+
/** Index from the MOST RECENTLY RECEIVED server handshake — sent back
|
|
2456
|
+
* to the server in `$sys.Reconnect:3` so the server can verify we're
|
|
2457
|
+
* talking about the current connection. Mirrors .NET's
|
|
2458
|
+
* `connectionState.Handshake.Index` path in
|
|
2459
|
+
* `RpcOutboundCallTracker.TryReconnect`. */
|
|
2460
|
+
_remoteHandshakeIndex = 0;
|
|
1900
2461
|
_lastRemoteHubId;
|
|
1901
2462
|
_pendingHandshake;
|
|
1902
2463
|
_reconnectsAt = 0;
|
|
@@ -1923,32 +2484,34 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
1923
2484
|
this._reconnectsAt = value;
|
|
1924
2485
|
this.reconnectsAtChanged.trigger();
|
|
1925
2486
|
}
|
|
1926
|
-
_format;
|
|
1927
2487
|
constructor(hub, url, serializationFormat) {
|
|
1928
2488
|
super(url, hub);
|
|
1929
2489
|
this.clientId = guidToBase64Url(this.id);
|
|
1930
2490
|
const resolver = RpcSerializationFormatResolver.Default;
|
|
1931
2491
|
this.serializationFormat = serializationFormat ?? resolver.defaultFormatKey;
|
|
1932
|
-
this.
|
|
1933
|
-
}
|
|
1934
|
-
get format() {
|
|
1935
|
-
return this._format;
|
|
2492
|
+
this.format = resolver.get(this.serializationFormat);
|
|
1936
2493
|
}
|
|
1937
2494
|
get connectionKind() {
|
|
1938
2495
|
return this._connectionKind;
|
|
1939
2496
|
}
|
|
1940
|
-
/** One-shot connection for tests — no reconnection loop, no handshake.
|
|
1941
|
-
|
|
2497
|
+
/** One-shot connection for tests — no reconnection loop, no handshake.
|
|
2498
|
+
* @param isPeerChanged Whether to treat this as a peer-identity change
|
|
2499
|
+
* (default `true` — no handshake was exchanged, so stage-3 compute calls
|
|
2500
|
+
* self-invalidate and remote/shared objects are disposed). Pass `false`
|
|
2501
|
+
* to simulate a same-peer reconnect in tests. */
|
|
2502
|
+
connectWith(conn, isPeerChanged = true) {
|
|
1942
2503
|
this.setupConnection(conn);
|
|
1943
|
-
this._reconnect(
|
|
2504
|
+
void this._reconnect(isPeerChanged);
|
|
1944
2505
|
}
|
|
1945
2506
|
/** Start the reconnection loop — runs until disposed. */
|
|
1946
2507
|
async run(wsFactory) {
|
|
1947
2508
|
const connUrl = this.connectionUrlResolver(this);
|
|
2509
|
+
infoLog?.log(`'${this.ref}': Started (Client)`);
|
|
1948
2510
|
while (!this._disposed) {
|
|
1949
2511
|
let lastCloseCode = 0;
|
|
1950
2512
|
let lastCloseReason = "";
|
|
1951
2513
|
try {
|
|
2514
|
+
infoLog?.log(`'${this.ref}': Connecting to ${connUrl}`);
|
|
1952
2515
|
const ws = wsFactory?.(connUrl) ?? new WebSocket(connUrl);
|
|
1953
2516
|
const conn = new RpcWebSocketConnection(
|
|
1954
2517
|
ws,
|
|
@@ -1972,38 +2535,48 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
1972
2535
|
closedRejection.catch(() => {
|
|
1973
2536
|
});
|
|
1974
2537
|
await Promise.race([conn.whenConnected, closedRejection]);
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2538
|
+
try {
|
|
2539
|
+
this._hub.systemCallSender.handshake(
|
|
2540
|
+
conn,
|
|
2541
|
+
this.format,
|
|
2542
|
+
this.id,
|
|
2543
|
+
this._hub.hubId,
|
|
2544
|
+
++this._handshakeIndex
|
|
2545
|
+
);
|
|
2546
|
+
} catch (e) {
|
|
2547
|
+
warnLog6?.log(`'${this.ref}': Failed to send handshake`, e);
|
|
2548
|
+
throw e;
|
|
2549
|
+
}
|
|
1982
2550
|
const remoteHandshake = await Promise.race([
|
|
1983
2551
|
this._pendingHandshake.promise,
|
|
1984
2552
|
closedRejection
|
|
1985
2553
|
]);
|
|
1986
2554
|
this._pendingHandshake = void 0;
|
|
2555
|
+
this._remoteHandshakeIndex = remoteHandshake.Index ?? 0;
|
|
1987
2556
|
const remoteHubId = remoteHandshake.RemoteHubId;
|
|
1988
2557
|
let isPeerChanged = false;
|
|
1989
2558
|
if (remoteHubId !== void 0) {
|
|
1990
2559
|
isPeerChanged = this._lastRemoteHubId !== void 0 && this._lastRemoteHubId !== remoteHubId;
|
|
1991
2560
|
if (isPeerChanged) {
|
|
1992
2561
|
this.inbound.clear();
|
|
2562
|
+
infoLog?.log(`'${this.ref}': Peer changed`);
|
|
1993
2563
|
this.peerChanged.trigger();
|
|
1994
2564
|
}
|
|
1995
2565
|
this._lastRemoteHubId = remoteHubId;
|
|
1996
2566
|
}
|
|
2567
|
+
infoLog?.log(`'${this.ref}': Handshake succeeded, peerChanged=${isPeerChanged}`);
|
|
1997
2568
|
this._connection = conn;
|
|
1998
2569
|
this._connectionKind = 2 /* Connected */;
|
|
1999
2570
|
this._tryIndex = 0;
|
|
2000
|
-
this._reconnect(isPeerChanged);
|
|
2571
|
+
await this._reconnect(isPeerChanged);
|
|
2001
2572
|
await new Promise((r) => conn.closed.add(() => r()));
|
|
2002
|
-
} catch {
|
|
2573
|
+
} catch (e) {
|
|
2574
|
+
debugLog2?.log(`'${this.ref}': run() iteration failed:`, e);
|
|
2003
2575
|
}
|
|
2004
2576
|
this._connectionKind = 0 /* Disconnected */;
|
|
2005
2577
|
if (this._disposed) break;
|
|
2006
2578
|
if (lastCloseCode === RPC_CLOSE_CODE_UNSUPPORTED_FORMAT) {
|
|
2579
|
+
warnLog6?.log(`'${this.ref}': Unsupported serialization format - shutting down`);
|
|
2007
2580
|
this.unsupportedFormat.trigger({ reason: lastCloseReason });
|
|
2008
2581
|
this._disposed = true;
|
|
2009
2582
|
break;
|
|
@@ -2011,6 +2584,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2011
2584
|
this._tryIndex++;
|
|
2012
2585
|
const delay = this.reconnectDelayer.getDelay(this._tryIndex);
|
|
2013
2586
|
if (delay.isLimitExceeded) {
|
|
2587
|
+
warnLog6?.log(`'${this.ref}': Reconnect limit exceeded - shutting down`);
|
|
2014
2588
|
this._disposed = true;
|
|
2015
2589
|
break;
|
|
2016
2590
|
}
|
|
@@ -2022,42 +2596,129 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2022
2596
|
}
|
|
2023
2597
|
}
|
|
2024
2598
|
}
|
|
2025
|
-
/**
|
|
2026
|
-
*
|
|
2027
|
-
*
|
|
2028
|
-
*
|
|
2029
|
-
*
|
|
2030
|
-
*
|
|
2031
|
-
*
|
|
2032
|
-
|
|
2599
|
+
/**
|
|
2600
|
+
* Reconcile outbound calls after reconnection, then flush pending sends.
|
|
2601
|
+
*
|
|
2602
|
+
* On peer change (different server hubId) or with binary wire format —
|
|
2603
|
+
* we blind-resend every eligible call.
|
|
2604
|
+
*
|
|
2605
|
+
* On a same-peer reconnect with JSON transport, first call
|
|
2606
|
+
* `$sys.Reconnect` to ask the server which call IDs it no longer
|
|
2607
|
+
* recognizes (e.g. inbound tracker was cleared, or the call never
|
|
2608
|
+
* reached the server). Only those get resent; the rest stay in the
|
|
2609
|
+
* tracker awaiting their $sys.Ok. This prevents duplicate server-side
|
|
2610
|
+
* invocation of in-flight streaming calls on every transient
|
|
2611
|
+
* reconnect — mirrors .NET `RpcOutboundCallTracker.Reconnect` at
|
|
2612
|
+
* src/ActualLab.Rpc/Infrastructure/RpcCallTrackers.cs:209-279.
|
|
2613
|
+
*
|
|
2614
|
+
* Stage-3 compute calls (result already resolved, awaiting invalidation)
|
|
2615
|
+
* are always self-invalidated — TS doesn't track compute-call stages
|
|
2616
|
+
* across reconnect, so the safest behavior is to force a fresh compute.
|
|
2617
|
+
*/
|
|
2618
|
+
async _reconnect(isPeerChanged) {
|
|
2033
2619
|
if (this._connection === void 0) return;
|
|
2034
|
-
if (
|
|
2620
|
+
if (isPeerChanged) {
|
|
2035
2621
|
this.remoteObjects.disconnectAll();
|
|
2622
|
+
this.sharedObjects.disconnectAll();
|
|
2036
2623
|
} else {
|
|
2037
2624
|
this.remoteObjects.reconnectAll();
|
|
2038
2625
|
}
|
|
2039
|
-
const
|
|
2040
|
-
for (const call of
|
|
2626
|
+
const eligible = [];
|
|
2627
|
+
for (const call of [...this.outbound.values()]) {
|
|
2041
2628
|
const mode = call.remoteExecutionMode;
|
|
2042
2629
|
if (!(mode & 2)) {
|
|
2043
2630
|
if (!call.result.isCompleted)
|
|
2044
|
-
call.result.reject(new Error(
|
|
2631
|
+
call.result.reject(new Error(
|
|
2632
|
+
"Outbound call failed: disconnected and AllowReconnect is not set."
|
|
2633
|
+
));
|
|
2045
2634
|
call.onDisconnect();
|
|
2046
2635
|
this.outbound.remove(call.callId);
|
|
2047
|
-
|
|
2636
|
+
continue;
|
|
2637
|
+
}
|
|
2638
|
+
if (isPeerChanged && !(mode & 4)) {
|
|
2048
2639
|
if (!call.result.isCompleted)
|
|
2049
|
-
call.result.reject(new Error(
|
|
2640
|
+
call.result.reject(new Error(
|
|
2641
|
+
"Outbound call failed: reconnected to a different peer and AllowResend is not set."
|
|
2642
|
+
));
|
|
2050
2643
|
call.onDisconnect();
|
|
2051
2644
|
this.outbound.remove(call.callId);
|
|
2052
|
-
|
|
2645
|
+
continue;
|
|
2646
|
+
}
|
|
2647
|
+
eligible.push(call);
|
|
2648
|
+
}
|
|
2649
|
+
let unknownIds = null;
|
|
2650
|
+
if (!isPeerChanged && eligible.length > 0) {
|
|
2651
|
+
unknownIds = await this._reconcileReconnect(eligible);
|
|
2652
|
+
}
|
|
2653
|
+
for (const call of eligible) {
|
|
2654
|
+
if (!call.removeOnOk && call.result.isCompleted) {
|
|
2053
2655
|
call.onDisconnect();
|
|
2054
2656
|
this.outbound.remove(call.callId);
|
|
2055
|
-
|
|
2056
|
-
|
|
2657
|
+
continue;
|
|
2658
|
+
}
|
|
2659
|
+
if (unknownIds !== null && !unknownIds.has(call.callId)) {
|
|
2660
|
+
continue;
|
|
2057
2661
|
}
|
|
2662
|
+
this._sendWireData(call.serializedWireData);
|
|
2058
2663
|
}
|
|
2059
2664
|
this._flushPendingSends();
|
|
2060
2665
|
}
|
|
2666
|
+
/**
|
|
2667
|
+
* Ask the server which call IDs in `eligible` it no longer recognizes
|
|
2668
|
+
* via `$sys.Reconnect:3`. Returns the set of call IDs that need to be
|
|
2669
|
+
* resent. On any failure, falls back to "resend everything".
|
|
2670
|
+
*
|
|
2671
|
+
* Wire format of the `completedStages` argument is chosen per wire format:
|
|
2672
|
+
* - JSON (json5np): plain object `{"<stage>": "<base64-of-bytes>"}` —
|
|
2673
|
+
* matches .NET `System.Text.Json`'s default for `Dictionary<int, byte[]>`.
|
|
2674
|
+
* - MessagePack (msgpack6/msgpack6c): `Map<number, Uint8Array>` —
|
|
2675
|
+
* the patched encoder (see `./msgpack-map-patch.ts`) writes this as a
|
|
2676
|
+
* native msgpack `map<int, bin>`, matching .NET MessagePack-CSharp's
|
|
2677
|
+
* default for `Dictionary<int, byte[]>`.
|
|
2678
|
+
*/
|
|
2679
|
+
async _reconcileReconnect(eligible) {
|
|
2680
|
+
try {
|
|
2681
|
+
const byStage = /* @__PURE__ */ new Map();
|
|
2682
|
+
for (const call of eligible) {
|
|
2683
|
+
const stage = call.getReconnectStage(false);
|
|
2684
|
+
if (stage === null) continue;
|
|
2685
|
+
let arr = byStage.get(stage);
|
|
2686
|
+
if (!arr) {
|
|
2687
|
+
arr = [];
|
|
2688
|
+
byStage.set(stage, arr);
|
|
2689
|
+
}
|
|
2690
|
+
arr.push(call.callId);
|
|
2691
|
+
}
|
|
2692
|
+
if (byStage.size === 0) {
|
|
2693
|
+
return new Set(eligible.map((c) => c.callId));
|
|
2694
|
+
}
|
|
2695
|
+
const isBinary = this.format.isBinary;
|
|
2696
|
+
const stagesArg = isBinary ? /* @__PURE__ */ new Map() : {};
|
|
2697
|
+
for (const [stage, ids] of byStage) {
|
|
2698
|
+
ids.sort((a, b) => a - b);
|
|
2699
|
+
const bytes2 = IncreasingSeqCompressor.serialize(ids);
|
|
2700
|
+
if (isBinary) {
|
|
2701
|
+
stagesArg.set(stage, bytes2);
|
|
2702
|
+
} else {
|
|
2703
|
+
stagesArg[String(stage)] = base64Encode(bytes2);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
const reconnectCall = this.call(
|
|
2707
|
+
RpcSystemCalls.reconnect,
|
|
2708
|
+
[this._remoteHandshakeIndex, stagesArg],
|
|
2709
|
+
{
|
|
2710
|
+
remoteExecutionMode: 1
|
|
2711
|
+
/* AwaitForConnection only; must not itself reconnect */
|
|
2712
|
+
}
|
|
2713
|
+
);
|
|
2714
|
+
const result = await reconnectCall.result.promise;
|
|
2715
|
+
const bytes = _toBytes2(result);
|
|
2716
|
+
if (bytes === null) return new Set(eligible.map((c) => c.callId));
|
|
2717
|
+
return new Set(IncreasingSeqCompressor.deserialize(bytes));
|
|
2718
|
+
} catch {
|
|
2719
|
+
return new Set(eligible.map((c) => c.callId));
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2061
2722
|
_onHandshakeReceived(handshake) {
|
|
2062
2723
|
this._pendingHandshake?.resolve(handshake);
|
|
2063
2724
|
}
|
|
@@ -2105,8 +2766,20 @@ function guidToBase64Url(uuid) {
|
|
|
2105
2766
|
const binary = String.fromCharCode(...bytes);
|
|
2106
2767
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2107
2768
|
}
|
|
2769
|
+
function _toBytes2(value) {
|
|
2770
|
+
if (value instanceof Uint8Array) return value;
|
|
2771
|
+
if (typeof value === "string") {
|
|
2772
|
+
try {
|
|
2773
|
+
return base64Decode(value);
|
|
2774
|
+
} catch {
|
|
2775
|
+
return null;
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
return null;
|
|
2779
|
+
}
|
|
2108
2780
|
|
|
2109
2781
|
// src/rpc-service-host.ts
|
|
2782
|
+
var { warnLog: warnLog7 } = getLogs("RpcServiceHost");
|
|
2110
2783
|
var RpcServiceHost = class {
|
|
2111
2784
|
_methods = /* @__PURE__ */ new Map();
|
|
2112
2785
|
register(def, impl) {
|
|
@@ -2121,8 +2794,10 @@ var RpcServiceHost = class {
|
|
|
2121
2794
|
}
|
|
2122
2795
|
async dispatch(wireMethod, args, context) {
|
|
2123
2796
|
const entry = this._methods.get(wireMethod);
|
|
2124
|
-
if (entry === void 0)
|
|
2797
|
+
if (entry === void 0) {
|
|
2798
|
+
warnLog7?.log(`Method not found: ${wireMethod}`);
|
|
2125
2799
|
throw new Error(`Method not found: ${wireMethod}`);
|
|
2800
|
+
}
|
|
2126
2801
|
if (context !== void 0 && entry.def.callTypeId !== 0) {
|
|
2127
2802
|
return await entry.fn(...args, context);
|
|
2128
2803
|
}
|
|
@@ -2652,6 +3327,7 @@ function computeMethodHash(methodName) {
|
|
|
2652
3327
|
}
|
|
2653
3328
|
|
|
2654
3329
|
// src/rpc-method-registry.ts
|
|
3330
|
+
var { warnLog: warnLog8 } = getLogs("RpcMethodRegistry");
|
|
2655
3331
|
var RpcMethodRegistry = class {
|
|
2656
3332
|
_nameToHash = /* @__PURE__ */ new Map();
|
|
2657
3333
|
_hashToName = /* @__PURE__ */ new Map();
|
|
@@ -2661,9 +3337,7 @@ var RpcMethodRegistry = class {
|
|
|
2661
3337
|
const hash = computeMethodHash(methodName);
|
|
2662
3338
|
const existing = this._hashToName.get(hash);
|
|
2663
3339
|
if (existing !== void 0 && existing !== methodName) {
|
|
2664
|
-
|
|
2665
|
-
`[RpcMethodRegistry] hash collision: "${methodName}" and "${existing}" both hash to ${hash}`
|
|
2666
|
-
);
|
|
3340
|
+
warnLog8?.log(`hash collision: "${methodName}" and "${existing}" both hash to ${hash}`);
|
|
2667
3341
|
return;
|
|
2668
3342
|
}
|
|
2669
3343
|
this._nameToHash.set(methodName, hash);
|
|
@@ -2696,6 +3370,7 @@ var RpcMethodRegistry = class {
|
|
|
2696
3370
|
};
|
|
2697
3371
|
|
|
2698
3372
|
// src/rpc-hub.ts
|
|
3373
|
+
var { warnLog: warnLog9 } = getLogs("RpcHub");
|
|
2699
3374
|
var RpcHub = class {
|
|
2700
3375
|
hubId;
|
|
2701
3376
|
peers = /* @__PURE__ */ new Map();
|
|
@@ -2721,6 +3396,7 @@ var RpcHub = class {
|
|
|
2721
3396
|
if (peer !== void 0) {
|
|
2722
3397
|
peer.close();
|
|
2723
3398
|
this.peers.delete(ref);
|
|
3399
|
+
warnLog9?.log(`'${ref}': peer is removed from RpcHub`);
|
|
2724
3400
|
}
|
|
2725
3401
|
}
|
|
2726
3402
|
/** Get or create a peer by ref. Server peers have "server://" prefix, everything else is a client peer. */
|
|
@@ -2953,6 +3629,7 @@ function getStateDescription(state, useLastError = false) {
|
|
|
2953
3629
|
|
|
2954
3630
|
// src/rpc-peer-state-monitor.ts
|
|
2955
3631
|
import { EventHandlerSet as EventHandlerSet4 } from "@actuallab/core";
|
|
3632
|
+
var { infoLog: infoLog2 } = getLogs("RpcPeerStateMonitor");
|
|
2956
3633
|
var JustConnectedPeriodMs = 1500;
|
|
2957
3634
|
var JustDisconnectedPeriodMs = 3e3;
|
|
2958
3635
|
var MinReconnectsInMs = 1e3;
|
|
@@ -3014,6 +3691,7 @@ var RpcPeerStateMonitor = class {
|
|
|
3014
3691
|
this._scheduleRecompute(
|
|
3015
3692
|
this._rawConnected ? JustConnectedPeriodMs : JustDisconnectedPeriodMs
|
|
3016
3693
|
);
|
|
3694
|
+
infoLog2?.log(`'${peer.ref}': monitor started`);
|
|
3017
3695
|
}
|
|
3018
3696
|
get state() {
|
|
3019
3697
|
return this._state;
|
|
@@ -3029,6 +3707,7 @@ var RpcPeerStateMonitor = class {
|
|
|
3029
3707
|
this.peer.reconnectDelayer.cancelDelaysChanged.remove(
|
|
3030
3708
|
this._onCancelDelaysChanged
|
|
3031
3709
|
);
|
|
3710
|
+
infoLog2?.log(`'${this.peer.ref}': monitor stopped`);
|
|
3032
3711
|
}
|
|
3033
3712
|
_recompute() {
|
|
3034
3713
|
if (this._timer !== void 0) {
|
|
@@ -3107,7 +3786,9 @@ export {
|
|
|
3107
3786
|
ARG_DELIMITER,
|
|
3108
3787
|
ENVELOPE_DELIMITER,
|
|
3109
3788
|
FRAME_DELIMITER,
|
|
3789
|
+
IncreasingSeqCompressor,
|
|
3110
3790
|
RPC_CLOSE_CODE_UNSUPPORTED_FORMAT,
|
|
3791
|
+
RpcCallStage,
|
|
3111
3792
|
RpcCallTypeId,
|
|
3112
3793
|
RpcClientPeer,
|
|
3113
3794
|
RpcClientPeerReconnectDelayer,
|
|
@@ -3152,6 +3833,7 @@ export {
|
|
|
3152
3833
|
deserializeBinaryMessage,
|
|
3153
3834
|
deserializeCompactBinaryMessage,
|
|
3154
3835
|
deserializeMessage,
|
|
3836
|
+
getLogs,
|
|
3155
3837
|
getMethodsMeta,
|
|
3156
3838
|
getServiceMeta,
|
|
3157
3839
|
getStateDescription,
|