@actuallab/rpc 13.0.20 → 13.0.163
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 +624 -178
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +167 -11
- package/dist/index.d.ts +167 -11
- package/dist/index.js +619 -177
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -165,6 +165,24 @@ function readVarUint(data, offset) {
|
|
|
165
165
|
} while (shift < 35);
|
|
166
166
|
return { value: value >>> 0, bytesRead };
|
|
167
167
|
}
|
|
168
|
+
function skipHeaders(data, pos, headerCount) {
|
|
169
|
+
for (let h = 0; h < headerCount; h++) {
|
|
170
|
+
const keyLen = data[pos++];
|
|
171
|
+
pos += keyLen;
|
|
172
|
+
const valLen = readVarUint(data, pos);
|
|
173
|
+
pos += valLen.bytesRead + valLen.value;
|
|
174
|
+
}
|
|
175
|
+
return pos;
|
|
176
|
+
}
|
|
177
|
+
function readPolymorphismMarker(data, offset) {
|
|
178
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
179
|
+
const length = view.getUint16(offset, true);
|
|
180
|
+
if (length === 0)
|
|
181
|
+
return { bytesRead: 2, isNull: false };
|
|
182
|
+
if (length === 1)
|
|
183
|
+
return { bytesRead: 2, isNull: true };
|
|
184
|
+
throw new Error("polymorphic payloads are not supported by the TS client");
|
|
185
|
+
}
|
|
168
186
|
function concatUint8Arrays(arrays) {
|
|
169
187
|
let totalLength = 0;
|
|
170
188
|
for (const arr of arrays) totalLength += arr.length;
|
|
@@ -228,14 +246,6 @@ function deserializeBinaryMessage(data, offset, decoder = defaultBinaryDecoder)
|
|
|
228
246
|
pos += methodLen.value;
|
|
229
247
|
const argDataLen = view.getInt32(pos, true);
|
|
230
248
|
pos += 4;
|
|
231
|
-
if (headerCount > 0) {
|
|
232
|
-
for (let h = 0; h < headerCount; h++) {
|
|
233
|
-
const keyLen = data[pos++];
|
|
234
|
-
pos += keyLen;
|
|
235
|
-
const valLen = readVarUint(data, pos);
|
|
236
|
-
pos += valLen.bytesRead + valLen.value;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
249
|
const args = [];
|
|
240
250
|
const argEnd = pos + argDataLen;
|
|
241
251
|
if (argDataLen > 0) {
|
|
@@ -243,8 +253,10 @@ function deserializeBinaryMessage(data, offset, decoder = defaultBinaryDecoder)
|
|
|
243
253
|
for (const decoded of decoder.decodeMulti(argSlice)) {
|
|
244
254
|
args.push(decoded);
|
|
245
255
|
}
|
|
246
|
-
pos = argEnd;
|
|
247
256
|
}
|
|
257
|
+
pos = argEnd;
|
|
258
|
+
if (headerCount > 0)
|
|
259
|
+
pos = skipHeaders(data, pos, headerCount);
|
|
248
260
|
const message = {
|
|
249
261
|
Method: method,
|
|
250
262
|
RelatedId: relId.value,
|
|
@@ -322,14 +334,6 @@ function deserializeCompactBinaryMessage(data, offset, registry, decoder = defau
|
|
|
322
334
|
const method = registry?.getName(methodHash | 0) ?? `<hash:0x${methodHash.toString(16).padStart(8, "0")}>`;
|
|
323
335
|
const argDataLen = view.getInt32(pos, true);
|
|
324
336
|
pos += 4;
|
|
325
|
-
if (headerCount > 0) {
|
|
326
|
-
for (let h = 0; h < headerCount; h++) {
|
|
327
|
-
const keyLen = data[pos++];
|
|
328
|
-
pos += keyLen;
|
|
329
|
-
const valLen = readVarUint(data, pos);
|
|
330
|
-
pos += valLen.bytesRead + valLen.value;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
337
|
const args = [];
|
|
334
338
|
const argEnd = pos + argDataLen;
|
|
335
339
|
if (argDataLen > 0) {
|
|
@@ -337,8 +341,10 @@ function deserializeCompactBinaryMessage(data, offset, registry, decoder = defau
|
|
|
337
341
|
for (const decoded of decoder.decodeMulti(argSlice)) {
|
|
338
342
|
args.push(decoded);
|
|
339
343
|
}
|
|
340
|
-
pos = argEnd;
|
|
341
344
|
}
|
|
345
|
+
pos = argEnd;
|
|
346
|
+
if (headerCount > 0)
|
|
347
|
+
pos = skipHeaders(data, pos, headerCount);
|
|
342
348
|
const message = {
|
|
343
349
|
Method: method,
|
|
344
350
|
RelatedId: relId.value,
|
|
@@ -608,7 +614,8 @@ function defineRpcService(name, methods) {
|
|
|
608
614
|
callTypeId: input.callTypeId ?? 0,
|
|
609
615
|
stream: input.returns === RpcType.stream,
|
|
610
616
|
noWait: isNoWait,
|
|
611
|
-
remoteExecutionMode: isNoWait ? 0 : input.remoteExecutionMode ?? RpcRemoteExecutionMode.Default
|
|
617
|
+
remoteExecutionMode: isNoWait ? 0 : input.remoteExecutionMode ?? RpcRemoteExecutionMode.Default,
|
|
618
|
+
timeouts: input.timeouts
|
|
612
619
|
});
|
|
613
620
|
}
|
|
614
621
|
return { name, methods: map };
|
|
@@ -642,6 +649,14 @@ var RpcOutboundCall = class {
|
|
|
642
649
|
removeOnOk = true;
|
|
643
650
|
/** The serialized wire data — stored for re-sending on reconnect. */
|
|
644
651
|
serializedWireData = "";
|
|
652
|
+
/** Per-call connect/run timeouts enforced by the peer's maintenance loop
|
|
653
|
+
* (R12). `undefined` means unbounded (the query default). */
|
|
654
|
+
timeouts;
|
|
655
|
+
/** `Date.now()` when this call was created (used for the connect timeout). */
|
|
656
|
+
startedAt = Date.now();
|
|
657
|
+
/** `Date.now()` when this call was last put on the wire, or 0 if it hasn't
|
|
658
|
+
* been sent yet (used for the run timeout). */
|
|
659
|
+
sentAt = 0;
|
|
645
660
|
/**
|
|
646
661
|
* Bitfield of {@link RpcCallStage} flags indicating how far this call
|
|
647
662
|
* has progressed. Used by the `$sys.Reconnect` protocol to tell the
|
|
@@ -731,20 +746,77 @@ var RpcInboundCall = class {
|
|
|
731
746
|
callId;
|
|
732
747
|
method;
|
|
733
748
|
args;
|
|
749
|
+
/** 0 = still processing, 1 = result computed and sent at least once. */
|
|
750
|
+
completedStage = 0;
|
|
751
|
+
_resend;
|
|
752
|
+
_abortController = new AbortController();
|
|
734
753
|
constructor(callId, method, args) {
|
|
735
754
|
this.callId = callId;
|
|
736
755
|
this.method = method;
|
|
737
756
|
this.args = args;
|
|
738
757
|
}
|
|
758
|
+
/** Aborted when the remote peer cancels this call via `$sys.Cancel` (R17).
|
|
759
|
+
* Threaded into the service handler so it can abort in-flight work. */
|
|
760
|
+
get signal() {
|
|
761
|
+
return this._abortController.signal;
|
|
762
|
+
}
|
|
763
|
+
/** True once the remote peer cancelled this call — the response is then
|
|
764
|
+
* suppressed even though the call may still be registered (R9 dedup). */
|
|
765
|
+
get isCancelled() {
|
|
766
|
+
return this._abortController.signal.aborted;
|
|
767
|
+
}
|
|
768
|
+
cancel() {
|
|
769
|
+
this._abortController.abort();
|
|
770
|
+
}
|
|
771
|
+
// Records the closure that re-sends this call's already-computed result;
|
|
772
|
+
// marks the call as completed. Mirrors .NET's post-`ProcessStage1Plus` state.
|
|
773
|
+
setResult(resend) {
|
|
774
|
+
this._resend = resend;
|
|
775
|
+
this.completedStage = 1;
|
|
776
|
+
}
|
|
777
|
+
// Re-sends the result to the remote peer if it was already computed.
|
|
778
|
+
// A resent frame for an in-flight call is a no-op — the original dispatch
|
|
779
|
+
// will send the result when it completes. Mirrors .NET
|
|
780
|
+
// `RpcInboundCall.TryReprocess`.
|
|
781
|
+
resendResult() {
|
|
782
|
+
if (this.completedStage >= 1)
|
|
783
|
+
this._resend?.();
|
|
784
|
+
}
|
|
739
785
|
};
|
|
740
786
|
var RpcInboundCallTracker = class {
|
|
741
787
|
_calls = /* @__PURE__ */ new Map();
|
|
788
|
+
_completedIds = [];
|
|
789
|
+
/** Cap on retained completed calls — see `RpcLimits.completedInboundCallsLimit`. */
|
|
790
|
+
completedCallsLimit = 1e3;
|
|
742
791
|
get size() {
|
|
743
792
|
return this._calls.size;
|
|
744
793
|
}
|
|
745
794
|
register(call) {
|
|
746
795
|
this._calls.set(call.callId, call);
|
|
747
796
|
}
|
|
797
|
+
// Registers `call` unless its id is already tracked, in which case the
|
|
798
|
+
// existing call is returned. Mirrors .NET `RpcInboundCallTracker.GetOrRegister`.
|
|
799
|
+
getOrRegister(call) {
|
|
800
|
+
const existing = this._calls.get(call.callId);
|
|
801
|
+
if (existing !== void 0)
|
|
802
|
+
return existing;
|
|
803
|
+
this._calls.set(call.callId, call);
|
|
804
|
+
return call;
|
|
805
|
+
}
|
|
806
|
+
// Keeps the completed `call` registered for duplicate-frame dedup, evicting
|
|
807
|
+
// the oldest completed calls beyond `completedCallsLimit`. Deviation from
|
|
808
|
+
// .NET (which unregisters a call once its result is sent): TS peers
|
|
809
|
+
// blind-resend after reconnect, so a completed call must stay resolvable
|
|
810
|
+
// to avoid re-executing the handler — but only within a bounded window.
|
|
811
|
+
markCompleted(call) {
|
|
812
|
+
if (this._calls.get(call.callId) !== call)
|
|
813
|
+
return;
|
|
814
|
+
this._completedIds.push(call.callId);
|
|
815
|
+
while (this._completedIds.length > this.completedCallsLimit) {
|
|
816
|
+
const id = this._completedIds.shift();
|
|
817
|
+
this._calls.delete(id);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
748
820
|
get(callId) {
|
|
749
821
|
return this._calls.get(callId);
|
|
750
822
|
}
|
|
@@ -755,6 +827,23 @@ var RpcInboundCallTracker = class {
|
|
|
755
827
|
}
|
|
756
828
|
clear() {
|
|
757
829
|
this._calls.clear();
|
|
830
|
+
this._completedIds.length = 0;
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
// src/rpc-call-timeouts.ts
|
|
835
|
+
var RpcCallTimeouts = class _RpcCallTimeouts {
|
|
836
|
+
/** Both timeouts unbounded — the query default (.NET `RpcCallTimeouts.None`). */
|
|
837
|
+
static None = new _RpcCallTimeouts(Infinity, Infinity);
|
|
838
|
+
/** Command default: 1.5 s connect / 10 s run (.NET `RpcCallTimeouts.Default.Command`). */
|
|
839
|
+
static Command = new _RpcCallTimeouts(1500, 1e4);
|
|
840
|
+
/** Max time a call may wait for the connection before it is sent. */
|
|
841
|
+
connectTimeoutMs;
|
|
842
|
+
/** Max time a sent call may stay unanswered before it is failed. */
|
|
843
|
+
runTimeoutMs;
|
|
844
|
+
constructor(connectTimeoutMs = Infinity, runTimeoutMs = Infinity) {
|
|
845
|
+
this.connectTimeoutMs = connectTimeoutMs;
|
|
846
|
+
this.runTimeoutMs = runTimeoutMs;
|
|
758
847
|
}
|
|
759
848
|
};
|
|
760
849
|
|
|
@@ -768,6 +857,12 @@ var RpcError = class extends Error {
|
|
|
768
857
|
this.typeName = typeName;
|
|
769
858
|
}
|
|
770
859
|
};
|
|
860
|
+
var REMOTE_EXCEPTION_TYPE_REF = "ActualLab.Serialization.RemoteException, ActualLab.Core";
|
|
861
|
+
function toExceptionInfo(error) {
|
|
862
|
+
const name = error instanceof Error ? error.name : "Error";
|
|
863
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
864
|
+
return { TypeRef: REMOTE_EXCEPTION_TYPE_REF, Message: `${name}: ${message}` };
|
|
865
|
+
}
|
|
771
866
|
|
|
772
867
|
// src/increasing-seq-compressor.ts
|
|
773
868
|
var IncreasingSeqCompressor = {
|
|
@@ -972,13 +1067,38 @@ var RpcStreamSender = class _RpcStreamSender {
|
|
|
972
1067
|
*/
|
|
973
1068
|
onAck(nextIndex, hostId) {
|
|
974
1069
|
const mustReset = hostId !== "" && hostId !== _RpcStreamSender._emptyGuid;
|
|
975
|
-
if (
|
|
1070
|
+
if (mustReset && hostId !== this.id.hostId) {
|
|
1071
|
+
this._sendDisconnect();
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
if (!this._started.isCompleted) {
|
|
1075
|
+
if (mustReset && nextIndex === 0)
|
|
1076
|
+
this._started.resolve();
|
|
1077
|
+
else {
|
|
1078
|
+
this._sendDisconnect();
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
} else if (this._ended) {
|
|
1082
|
+
this._sendDisconnect();
|
|
1083
|
+
return;
|
|
1084
|
+
} else if (mustReset && !this.allowReconnect) {
|
|
1085
|
+
this._sendDisconnect();
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
976
1088
|
this._acks.push({ nextIndex, mustReset });
|
|
977
1089
|
if (this._whenAckReady) {
|
|
978
1090
|
this._whenAckReady.resolve();
|
|
979
1091
|
this._whenAckReady = null;
|
|
980
1092
|
}
|
|
981
1093
|
}
|
|
1094
|
+
// Tells the consumer this shared stream is gone, then disposes it —
|
|
1095
|
+
// mirrors RpcSharedStream.SendDisconnect (RpcSharedStream.cs:284-289).
|
|
1096
|
+
_sendDisconnect() {
|
|
1097
|
+
const conn = this.peer.connection;
|
|
1098
|
+
if (conn)
|
|
1099
|
+
this.peer.hub.systemCallSender.disconnect(conn, this.peer.serializationFormat, [this.id.localId]);
|
|
1100
|
+
this.disconnect();
|
|
1101
|
+
}
|
|
982
1102
|
/** Called by system call handler when $sys.AckEnd is received from the client. */
|
|
983
1103
|
onAckEnd(_hostId) {
|
|
984
1104
|
this.disconnect();
|
|
@@ -1027,7 +1147,7 @@ var RpcStreamSender = class _RpcStreamSender {
|
|
|
1027
1147
|
this._ended = true;
|
|
1028
1148
|
const conn = this.peer.connection;
|
|
1029
1149
|
if (!conn) return;
|
|
1030
|
-
const errorInfo = error ?
|
|
1150
|
+
const errorInfo = error ? toExceptionInfo(error) : { TypeRef: "", Message: "" };
|
|
1031
1151
|
this.peer.hub.systemCallSender.end(
|
|
1032
1152
|
conn,
|
|
1033
1153
|
this.peer.serializationFormat,
|
|
@@ -1469,11 +1589,7 @@ var RpcStream = class _RpcStream {
|
|
|
1469
1589
|
if (index > this._nextExpectedIndex) {
|
|
1470
1590
|
warnLog4?.log(`item index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
|
|
1471
1591
|
if (!this.allowReconnect) {
|
|
1472
|
-
this.
|
|
1473
|
-
this._completionError = new Error(
|
|
1474
|
-
`Stream gap at index ${index} (expected ${this._nextExpectedIndex}); reconnect not allowed`
|
|
1475
|
-
);
|
|
1476
|
-
this._notifyConsumer();
|
|
1592
|
+
this._complete(this._gapError(index));
|
|
1477
1593
|
return;
|
|
1478
1594
|
}
|
|
1479
1595
|
this._sendAck(this._nextExpectedIndex, true);
|
|
@@ -1493,11 +1609,7 @@ var RpcStream = class _RpcStream {
|
|
|
1493
1609
|
if (index > this._nextExpectedIndex) {
|
|
1494
1610
|
warnLog4?.log(`batch index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
|
|
1495
1611
|
if (!this.allowReconnect) {
|
|
1496
|
-
this.
|
|
1497
|
-
this._completionError = new Error(
|
|
1498
|
-
`Stream gap at index ${index} (expected ${this._nextExpectedIndex}); reconnect not allowed`
|
|
1499
|
-
);
|
|
1500
|
-
this._notifyConsumer();
|
|
1612
|
+
this._complete(this._gapError(index));
|
|
1501
1613
|
return;
|
|
1502
1614
|
}
|
|
1503
1615
|
this._sendAck(this._nextExpectedIndex, true);
|
|
@@ -1514,9 +1626,20 @@ var RpcStream = class _RpcStream {
|
|
|
1514
1626
|
/** Called by system call handler when the stream ends ($sys.End). */
|
|
1515
1627
|
onEnd(index, error) {
|
|
1516
1628
|
if (this._disposed || this._completed) return;
|
|
1517
|
-
this.
|
|
1518
|
-
|
|
1519
|
-
|
|
1629
|
+
if (index > this._nextExpectedIndex) {
|
|
1630
|
+
warnLog4?.log(`end index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
|
|
1631
|
+
if (!this.allowReconnect) {
|
|
1632
|
+
this._complete(this._gapError(index));
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
this._sendAck(this._nextExpectedIndex, true);
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
if (index < this._nextExpectedIndex) {
|
|
1639
|
+
this._maybeSendAck(Math.min(index, this._nextConsumedIndex));
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
this._complete(error);
|
|
1520
1643
|
}
|
|
1521
1644
|
// -- IRpcObject --
|
|
1522
1645
|
reconnect() {
|
|
@@ -1550,6 +1673,7 @@ var RpcStream = class _RpcStream {
|
|
|
1550
1673
|
next: async () => {
|
|
1551
1674
|
if (!this._started) {
|
|
1552
1675
|
this._started = true;
|
|
1676
|
+
this.peer.remoteObjects.register(this);
|
|
1553
1677
|
this._sendAck(0, true);
|
|
1554
1678
|
}
|
|
1555
1679
|
this._maybeSendAck(this._nextConsumedIndex);
|
|
@@ -1560,6 +1684,7 @@ var RpcStream = class _RpcStream {
|
|
|
1560
1684
|
return { value, done: false };
|
|
1561
1685
|
}
|
|
1562
1686
|
if (this._completed) {
|
|
1687
|
+
this.dispose();
|
|
1563
1688
|
if (this._completionError) throw this._completionError;
|
|
1564
1689
|
return { value: void 0, done: true };
|
|
1565
1690
|
}
|
|
@@ -1568,7 +1693,8 @@ var RpcStream = class _RpcStream {
|
|
|
1568
1693
|
}
|
|
1569
1694
|
},
|
|
1570
1695
|
return: () => {
|
|
1571
|
-
this.
|
|
1696
|
+
if (this._started && !this._completed)
|
|
1697
|
+
this._sendAckEnd();
|
|
1572
1698
|
this.dispose();
|
|
1573
1699
|
return Promise.resolve({ value: void 0, done: true });
|
|
1574
1700
|
}
|
|
@@ -1584,6 +1710,22 @@ var RpcStream = class _RpcStream {
|
|
|
1584
1710
|
this._notifyConsumer();
|
|
1585
1711
|
}
|
|
1586
1712
|
// -- Private --
|
|
1713
|
+
// Mirrors .NET RpcStream.CloseFromLock: acknowledge the end to the sender
|
|
1714
|
+
// (so its RpcSharedStream is released) and unregister immediately.
|
|
1715
|
+
_complete(error) {
|
|
1716
|
+
if (this._completed) return;
|
|
1717
|
+
this._completed = true;
|
|
1718
|
+
this._completionError = error;
|
|
1719
|
+
this._sendAckEnd();
|
|
1720
|
+
if (this.kind === RpcObjectKind.Remote)
|
|
1721
|
+
this.peer.remoteObjects.unregister(this);
|
|
1722
|
+
this._notifyConsumer();
|
|
1723
|
+
}
|
|
1724
|
+
_gapError(index) {
|
|
1725
|
+
return new Error(
|
|
1726
|
+
`Stream gap at index ${index} (expected ${this._nextExpectedIndex}); reconnect not allowed`
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1587
1729
|
_notifyConsumer() {
|
|
1588
1730
|
if (this._consumerWaiting !== null) {
|
|
1589
1731
|
this._consumerWaiting.resolve();
|
|
@@ -1600,7 +1742,7 @@ var RpcStream = class _RpcStream {
|
|
|
1600
1742
|
static _emptyGuid = "00000000-0000-0000-0000-000000000000";
|
|
1601
1743
|
_sendAck(nextIndex, mustReset) {
|
|
1602
1744
|
this._ackSentUpTo = nextIndex;
|
|
1603
|
-
const conn = this.peer.connection;
|
|
1745
|
+
const conn = this.peer.isConnected ? this.peer.connection : void 0;
|
|
1604
1746
|
if (conn) {
|
|
1605
1747
|
const hostId = mustReset ? this.id.hostId : _RpcStream._emptyGuid;
|
|
1606
1748
|
this.peer.hub.systemCallSender.ack(
|
|
@@ -1613,7 +1755,7 @@ var RpcStream = class _RpcStream {
|
|
|
1613
1755
|
}
|
|
1614
1756
|
}
|
|
1615
1757
|
_sendAckEnd() {
|
|
1616
|
-
const conn = this.peer.connection;
|
|
1758
|
+
const conn = this.peer.isConnected ? this.peer.connection : void 0;
|
|
1617
1759
|
if (conn) {
|
|
1618
1760
|
this.peer.hub.systemCallSender.ackEnd(
|
|
1619
1761
|
conn,
|
|
@@ -1633,9 +1775,7 @@ function resolveStreamRefs(value, peer) {
|
|
|
1633
1775
|
if (typeof value === "string") {
|
|
1634
1776
|
const ref = parseStreamRef(value);
|
|
1635
1777
|
if (ref !== null) {
|
|
1636
|
-
|
|
1637
|
-
peer.remoteObjects.register(stream);
|
|
1638
|
-
return stream;
|
|
1778
|
+
return new RpcStream(ref, peer);
|
|
1639
1779
|
}
|
|
1640
1780
|
return value;
|
|
1641
1781
|
}
|
|
@@ -1688,6 +1828,11 @@ var RpcSystemCallHandler = class {
|
|
|
1688
1828
|
case RpcSystemCalls.ok: {
|
|
1689
1829
|
const call = peer.outboundCalls.get(relatedId);
|
|
1690
1830
|
if (call !== void 0) {
|
|
1831
|
+
if (args.length > 1) {
|
|
1832
|
+
peer.outboundCalls.remove(relatedId);
|
|
1833
|
+
call.result.reject(polymorphicPayloadError(method, args.length, 1));
|
|
1834
|
+
break;
|
|
1835
|
+
}
|
|
1691
1836
|
call.completedStage |= RpcCallStage.ResultReady;
|
|
1692
1837
|
if (call.removeOnOk) {
|
|
1693
1838
|
peer.outboundCalls.remove(relatedId);
|
|
@@ -1701,19 +1846,15 @@ var RpcSystemCallHandler = class {
|
|
|
1701
1846
|
break;
|
|
1702
1847
|
}
|
|
1703
1848
|
case RpcSystemCalls.error: {
|
|
1704
|
-
const call = peer.outboundCalls.
|
|
1849
|
+
const call = peer.outboundCalls.get(relatedId);
|
|
1705
1850
|
if (call !== void 0) {
|
|
1851
|
+
call.completedStage |= RpcCallStage.ResultReady;
|
|
1852
|
+
if (call.removeOnOk) {
|
|
1853
|
+
peer.outboundCalls.remove(relatedId);
|
|
1854
|
+
}
|
|
1706
1855
|
const errorInfo = args[0];
|
|
1707
1856
|
const msg = errorInfo?.Message ?? errorInfo?.message ?? "RPC error";
|
|
1708
|
-
const
|
|
1709
|
-
let typeName;
|
|
1710
|
-
if (typeof errorType === "string") {
|
|
1711
|
-
const commaIdx = errorType.indexOf(",");
|
|
1712
|
-
typeName = commaIdx >= 0 ? errorType.slice(0, commaIdx) : errorType;
|
|
1713
|
-
} else if (errorType !== null && typeof errorType === "object") {
|
|
1714
|
-
const t = errorType;
|
|
1715
|
-
typeName = t.TypeName ?? t.typeName;
|
|
1716
|
-
}
|
|
1857
|
+
const typeName = extractTypeName(errorInfo?.TypeRef ?? errorInfo?.typeRef);
|
|
1717
1858
|
if (typeName === "ActualLab.Rpc.RpcRerouteException")
|
|
1718
1859
|
warnLog5?.log("Got RpcRerouteException from remote peer:", msg);
|
|
1719
1860
|
call.result.reject(new RpcError(msg, typeName));
|
|
@@ -1721,17 +1862,29 @@ var RpcSystemCallHandler = class {
|
|
|
1721
1862
|
break;
|
|
1722
1863
|
}
|
|
1723
1864
|
case RpcSystemCalls.cancel: {
|
|
1724
|
-
peer.inboundCalls.remove(relatedId);
|
|
1865
|
+
const call = peer.inboundCalls.remove(relatedId);
|
|
1866
|
+
call?.cancel();
|
|
1725
1867
|
break;
|
|
1726
1868
|
}
|
|
1727
1869
|
case RpcSystemCalls.keepAlive: {
|
|
1728
1870
|
peer.notifyKeepAliveReceived();
|
|
1871
|
+
const keepAliveIds = args[0];
|
|
1872
|
+
if (Array.isArray(keepAliveIds) && keepAliveIds.length > 0 && peer.connection !== void 0) {
|
|
1873
|
+
const unknownIds = [];
|
|
1874
|
+
for (const id of keepAliveIds)
|
|
1875
|
+
if (peer.sharedObjects.get(id) === void 0)
|
|
1876
|
+
unknownIds.push(id);
|
|
1877
|
+
if (unknownIds.length > 0)
|
|
1878
|
+
peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, unknownIds);
|
|
1879
|
+
}
|
|
1729
1880
|
break;
|
|
1730
1881
|
}
|
|
1731
1882
|
case RpcSystemCalls.item: {
|
|
1732
1883
|
const stream = peer.remoteObjects.get(relatedId);
|
|
1733
1884
|
if (!stream)
|
|
1734
1885
|
debugLog?.log(`$sys.I: no stream for relatedId=${relatedId}`);
|
|
1886
|
+
else if (args.length > 2)
|
|
1887
|
+
stream.onEnd(args[0], polymorphicPayloadError(method, args.length, 2));
|
|
1735
1888
|
else
|
|
1736
1889
|
stream.onItem(
|
|
1737
1890
|
args[0],
|
|
@@ -1743,6 +1896,8 @@ var RpcSystemCallHandler = class {
|
|
|
1743
1896
|
const stream = peer.remoteObjects.get(relatedId);
|
|
1744
1897
|
if (!stream)
|
|
1745
1898
|
debugLog?.log(`$sys.B: no stream for relatedId=${relatedId}`);
|
|
1899
|
+
else if (args.length > 2)
|
|
1900
|
+
stream.onEnd(args[0], polymorphicPayloadError(method, args.length, 2));
|
|
1746
1901
|
else if (Array.isArray(args[1])) {
|
|
1747
1902
|
const items = args[1];
|
|
1748
1903
|
for (let i = 0; i < items.length; i++)
|
|
@@ -1757,20 +1912,32 @@ var RpcSystemCallHandler = class {
|
|
|
1757
1912
|
debugLog?.log(`$sys.End: no stream for relatedId=${relatedId}`);
|
|
1758
1913
|
} else {
|
|
1759
1914
|
const errorInfo = args[1];
|
|
1915
|
+
const typeName = extractTypeName(errorInfo?.TypeRef ?? errorInfo?.typeRef);
|
|
1760
1916
|
const msg = errorInfo?.Message ?? errorInfo?.message;
|
|
1761
|
-
|
|
1917
|
+
let error = null;
|
|
1918
|
+
if (typeName || msg) {
|
|
1919
|
+
error = new Error(msg ?? typeName ?? "RPC stream error");
|
|
1920
|
+
if (typeName)
|
|
1921
|
+
error.name = typeName;
|
|
1922
|
+
}
|
|
1762
1923
|
stream.onEnd(args[0], error);
|
|
1763
1924
|
}
|
|
1764
1925
|
break;
|
|
1765
1926
|
}
|
|
1766
1927
|
case RpcSystemCalls.ack: {
|
|
1767
1928
|
const sender = peer.sharedObjects.get(relatedId);
|
|
1768
|
-
sender
|
|
1929
|
+
if (sender)
|
|
1930
|
+
sender.onAck(args[0], args[1]);
|
|
1931
|
+
else if (peer.connection !== void 0)
|
|
1932
|
+
peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, [relatedId]);
|
|
1769
1933
|
break;
|
|
1770
1934
|
}
|
|
1771
1935
|
case RpcSystemCalls.ackEnd: {
|
|
1772
1936
|
const sender = peer.sharedObjects.get(relatedId);
|
|
1773
|
-
sender
|
|
1937
|
+
if (sender)
|
|
1938
|
+
sender.onAckEnd(args[0]);
|
|
1939
|
+
else if (peer.connection !== void 0)
|
|
1940
|
+
peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, [relatedId]);
|
|
1774
1941
|
break;
|
|
1775
1942
|
}
|
|
1776
1943
|
case RpcSystemCalls.disconnect: {
|
|
@@ -1804,19 +1971,34 @@ var RpcSystemCallHandler = class {
|
|
|
1804
1971
|
*/
|
|
1805
1972
|
_handleReconnect(relatedId, args, peer) {
|
|
1806
1973
|
if (peer.connection === void 0) return;
|
|
1974
|
+
const handshakeIndex = args[0];
|
|
1975
|
+
if (typeof handshakeIndex === "number" && handshakeIndex !== peer.ownHandshakeIndex) {
|
|
1976
|
+
peer.hub.systemCallSender.error(
|
|
1977
|
+
peer.connection,
|
|
1978
|
+
peer.serializationFormat,
|
|
1979
|
+
relatedId,
|
|
1980
|
+
new Error(
|
|
1981
|
+
`TooLateToReconnect: own handshake index ${peer.ownHandshakeIndex} != ${handshakeIndex}`
|
|
1982
|
+
)
|
|
1983
|
+
);
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1807
1986
|
let unknownIds = [];
|
|
1808
1987
|
try {
|
|
1809
1988
|
const stagesRaw = args[1];
|
|
1810
1989
|
if (stagesRaw && typeof stagesRaw === "object" && !Array.isArray(stagesRaw)) {
|
|
1811
1990
|
const unknownSet = /* @__PURE__ */ new Set();
|
|
1812
|
-
for (const [, rawValue] of Object.entries(stagesRaw)) {
|
|
1991
|
+
for (const [stageKey, rawValue] of Object.entries(stagesRaw)) {
|
|
1813
1992
|
const bytes = _toBytes(rawValue);
|
|
1814
1993
|
if (bytes === null) continue;
|
|
1994
|
+
const completedStage = parseInt(stageKey, 10) || 0;
|
|
1815
1995
|
const callIds = IncreasingSeqCompressor.deserialize(bytes);
|
|
1816
1996
|
for (const callId of callIds) {
|
|
1817
|
-
|
|
1997
|
+
const call = peer.inboundCalls.get(callId);
|
|
1998
|
+
if (call === void 0)
|
|
1818
1999
|
unknownSet.add(callId);
|
|
1819
|
-
|
|
2000
|
+
else if (completedStage < 1)
|
|
2001
|
+
call.resendResult();
|
|
1820
2002
|
}
|
|
1821
2003
|
}
|
|
1822
2004
|
unknownIds = [...unknownSet].sort((a, b) => a - b);
|
|
@@ -1828,6 +2010,29 @@ var RpcSystemCallHandler = class {
|
|
|
1828
2010
|
peer.hub.systemCallSender.ok(peer.connection, peer.serializationFormat, relatedId, responseValue);
|
|
1829
2011
|
}
|
|
1830
2012
|
};
|
|
2013
|
+
function polymorphicPayloadError(method, got, expected) {
|
|
2014
|
+
return new Error(
|
|
2015
|
+
`${method}: expected ${expected} value(s), got ${got} \u2014 polymorphic payloads are not supported by the TS client`
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
function extractTypeName(typeRef) {
|
|
2019
|
+
if (typeof typeRef === "string") {
|
|
2020
|
+
if (typeRef.length === 0)
|
|
2021
|
+
return void 0;
|
|
2022
|
+
const commaIdx = typeRef.indexOf(",");
|
|
2023
|
+
return commaIdx >= 0 ? typeRef.slice(0, commaIdx) : typeRef;
|
|
2024
|
+
}
|
|
2025
|
+
if (typeRef !== null && typeof typeRef === "object") {
|
|
2026
|
+
const t = typeRef;
|
|
2027
|
+
const aqn = t.AssemblyQualifiedName ?? t.assemblyQualifiedName;
|
|
2028
|
+
if (typeof aqn === "string" && aqn.length > 0) {
|
|
2029
|
+
const commaIdx = aqn.indexOf(",");
|
|
2030
|
+
return commaIdx >= 0 ? aqn.slice(0, commaIdx) : aqn;
|
|
2031
|
+
}
|
|
2032
|
+
return t.TypeName ?? t.typeName;
|
|
2033
|
+
}
|
|
2034
|
+
return void 0;
|
|
2035
|
+
}
|
|
1831
2036
|
function _toBytes(value) {
|
|
1832
2037
|
if (value instanceof Uint8Array) return value;
|
|
1833
2038
|
if (typeof value === "string") {
|
|
@@ -1911,12 +2116,11 @@ var RpcSystemCallSender = class _RpcSystemCallSender {
|
|
|
1911
2116
|
}
|
|
1912
2117
|
}
|
|
1913
2118
|
error(conn, format, relatedId, error) {
|
|
1914
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1915
2119
|
this._send(
|
|
1916
2120
|
conn,
|
|
1917
2121
|
format,
|
|
1918
2122
|
{ Method: RpcSystemCalls.error, RelatedId: relatedId },
|
|
1919
|
-
[
|
|
2123
|
+
[toExceptionInfo(error)]
|
|
1920
2124
|
);
|
|
1921
2125
|
}
|
|
1922
2126
|
cancel(conn, format, relatedId) {
|
|
@@ -1930,6 +2134,14 @@ var RpcSystemCallSender = class _RpcSystemCallSender {
|
|
|
1930
2134
|
activeCallIds
|
|
1931
2135
|
]);
|
|
1932
2136
|
}
|
|
2137
|
+
// Tells the remote peer the listed shared-object ids are gone on our side,
|
|
2138
|
+
// so its matching remote objects (e.g. RpcStream consumers) fail fast
|
|
2139
|
+
// instead of hanging. Mirrors .NET RpcSystemCallSender.Disconnect.
|
|
2140
|
+
disconnect(conn, format, localIds) {
|
|
2141
|
+
this._send(conn, format, { Method: RpcSystemCalls.disconnect }, [
|
|
2142
|
+
localIds
|
|
2143
|
+
]);
|
|
2144
|
+
}
|
|
1933
2145
|
item(conn, format, localId, index, item) {
|
|
1934
2146
|
this._itemEnv.RelatedId = localId;
|
|
1935
2147
|
this._itemArgs[0] = index;
|
|
@@ -1967,7 +2179,13 @@ var RpcSystemCallSender = class _RpcSystemCallSender {
|
|
|
1967
2179
|
var RpcRemoteObjectTracker = class {
|
|
1968
2180
|
_objects = /* @__PURE__ */ new Map();
|
|
1969
2181
|
register(obj) {
|
|
1970
|
-
|
|
2182
|
+
const localId = obj.id.localId;
|
|
2183
|
+
const existing = this._objects.get(localId);
|
|
2184
|
+
if (existing === obj)
|
|
2185
|
+
return;
|
|
2186
|
+
if (existing !== void 0)
|
|
2187
|
+
existing.disconnect();
|
|
2188
|
+
this._objects.set(localId, obj);
|
|
1971
2189
|
}
|
|
1972
2190
|
get(localId) {
|
|
1973
2191
|
return this._objects.get(localId);
|
|
@@ -1976,7 +2194,9 @@ var RpcRemoteObjectTracker = class {
|
|
|
1976
2194
|
return this._objects.keys();
|
|
1977
2195
|
}
|
|
1978
2196
|
unregister(obj) {
|
|
1979
|
-
|
|
2197
|
+
const localId = obj.id.localId;
|
|
2198
|
+
if (this._objects.get(localId) === obj)
|
|
2199
|
+
this._objects.delete(localId);
|
|
1980
2200
|
}
|
|
1981
2201
|
disconnectAll() {
|
|
1982
2202
|
for (const obj of this._objects.values()) {
|
|
@@ -2000,7 +2220,10 @@ var RpcSharedObjectTracker = class {
|
|
|
2000
2220
|
return this._nextId++;
|
|
2001
2221
|
}
|
|
2002
2222
|
register(obj) {
|
|
2003
|
-
|
|
2223
|
+
const localId = obj.id.localId;
|
|
2224
|
+
if (this._objects.has(localId))
|
|
2225
|
+
throw new Error(`RPC shared object with localId ${localId} is already registered.`);
|
|
2226
|
+
this._objects.set(localId, obj);
|
|
2004
2227
|
}
|
|
2005
2228
|
get(localId) {
|
|
2006
2229
|
return this._objects.get(localId);
|
|
@@ -2009,7 +2232,9 @@ var RpcSharedObjectTracker = class {
|
|
|
2009
2232
|
return this._objects.keys();
|
|
2010
2233
|
}
|
|
2011
2234
|
unregister(obj) {
|
|
2012
|
-
|
|
2235
|
+
const localId = obj.id.localId;
|
|
2236
|
+
if (this._objects.get(localId) === obj)
|
|
2237
|
+
this._objects.delete(localId);
|
|
2013
2238
|
}
|
|
2014
2239
|
disconnectAll() {
|
|
2015
2240
|
for (const obj of this._objects.values()) {
|
|
@@ -2020,7 +2245,7 @@ var RpcSharedObjectTracker = class {
|
|
|
2020
2245
|
};
|
|
2021
2246
|
|
|
2022
2247
|
// src/rpc-peer.ts
|
|
2023
|
-
import { EventHandlerSet as EventHandlerSet3, PromiseSource as PromiseSource5, TimeoutError, withTimeout } from "@actuallab/core";
|
|
2248
|
+
import { EventHandlerSet as EventHandlerSet3, PromiseSource as PromiseSource5, TimeoutError, cancellationError, withTimeout } from "@actuallab/core";
|
|
2024
2249
|
|
|
2025
2250
|
// src/rpc-serialization-format.ts
|
|
2026
2251
|
import { Decoder as Decoder3 } from "@msgpack/msgpack";
|
|
@@ -2039,9 +2264,7 @@ var RpcSerializationFormat = class {
|
|
|
2039
2264
|
static NewtonsoftJsonV5;
|
|
2040
2265
|
static NewtonsoftJsonV5NP;
|
|
2041
2266
|
static MessagePackV6;
|
|
2042
|
-
static MemoryPackV6;
|
|
2043
2267
|
static MessagePackV6C;
|
|
2044
|
-
static MemoryPackV6C;
|
|
2045
2268
|
static All;
|
|
2046
2269
|
/** Convenience shortcut for RpcSerializationFormatResolver.Default.get(key). */
|
|
2047
2270
|
static get(key) {
|
|
@@ -2137,18 +2360,14 @@ RpcSerializationFormat.SystemJsonV5NP = new RpcJsonSerializationFormat("json5np"
|
|
|
2137
2360
|
RpcSerializationFormat.NewtonsoftJsonV5 = new RpcJsonSerializationFormat("njson5");
|
|
2138
2361
|
RpcSerializationFormat.NewtonsoftJsonV5NP = new RpcJsonSerializationFormat("njson5np");
|
|
2139
2362
|
RpcSerializationFormat.MessagePackV6 = new RpcMessagePackSerializationFormat("msgpack6");
|
|
2140
|
-
RpcSerializationFormat.MemoryPackV6 = new RpcMessagePackSerializationFormat("mempack6");
|
|
2141
2363
|
RpcSerializationFormat.MessagePackV6C = new RpcMessagePackCompactSerializationFormat("msgpack6c");
|
|
2142
|
-
RpcSerializationFormat.MemoryPackV6C = new RpcMessagePackCompactSerializationFormat("mempack6c");
|
|
2143
2364
|
RpcSerializationFormat.All = [
|
|
2144
2365
|
RpcSerializationFormat.SystemJsonV5,
|
|
2145
2366
|
RpcSerializationFormat.SystemJsonV5NP,
|
|
2146
2367
|
RpcSerializationFormat.NewtonsoftJsonV5,
|
|
2147
2368
|
RpcSerializationFormat.NewtonsoftJsonV5NP,
|
|
2148
2369
|
RpcSerializationFormat.MessagePackV6,
|
|
2149
|
-
RpcSerializationFormat.
|
|
2150
|
-
RpcSerializationFormat.MessagePackV6C,
|
|
2151
|
-
RpcSerializationFormat.MemoryPackV6C
|
|
2370
|
+
RpcSerializationFormat.MessagePackV6C
|
|
2152
2371
|
];
|
|
2153
2372
|
var RpcSerializationFormatResolver = class _RpcSerializationFormatResolver {
|
|
2154
2373
|
static DefaultFormats = RpcSerializationFormat.All;
|
|
@@ -2168,6 +2387,8 @@ var RpcSerializationFormatResolver = class _RpcSerializationFormatResolver {
|
|
|
2168
2387
|
this.formats = map;
|
|
2169
2388
|
}
|
|
2170
2389
|
get(key) {
|
|
2390
|
+
if (isMemoryPackKey(key))
|
|
2391
|
+
throw new Error(`Serialization format '${key}': MemoryPack formats are not supported by the TS client.`);
|
|
2171
2392
|
const format = this.tryGet(key);
|
|
2172
2393
|
if (!format)
|
|
2173
2394
|
throw new Error(`No format with key '${key}'.`);
|
|
@@ -2175,9 +2396,14 @@ var RpcSerializationFormatResolver = class _RpcSerializationFormatResolver {
|
|
|
2175
2396
|
}
|
|
2176
2397
|
tryGet(key) {
|
|
2177
2398
|
if (!key) key = this.defaultFormatKey;
|
|
2399
|
+
if (isMemoryPackKey(key))
|
|
2400
|
+
throw new Error(`Serialization format '${key}': MemoryPack formats are not supported by the TS client.`);
|
|
2178
2401
|
return this.formats.get(key);
|
|
2179
2402
|
}
|
|
2180
2403
|
};
|
|
2404
|
+
function isMemoryPackKey(key) {
|
|
2405
|
+
return key.startsWith("mempack");
|
|
2406
|
+
}
|
|
2181
2407
|
|
|
2182
2408
|
// src/rpc-peer.ts
|
|
2183
2409
|
function yieldToEventLoop() {
|
|
@@ -2231,6 +2457,10 @@ var RpcPeer = class {
|
|
|
2231
2457
|
* attempt (the reconnect delay is part of this phase); `Connected` is
|
|
2232
2458
|
* entered only after the handshake has succeeded. */
|
|
2233
2459
|
connectionStateChanged = new EventHandlerSet3();
|
|
2460
|
+
/** Index of the handshake THIS peer most recently sent. Used to reject a
|
|
2461
|
+
* stale `$sys.Reconnect` that references an older connection generation
|
|
2462
|
+
* (C# `RpcSystemCalls.Reconnect`'s `ownHandshake.Index` check). */
|
|
2463
|
+
_ownHandshakeIndex = 0;
|
|
2234
2464
|
_connection;
|
|
2235
2465
|
/** Mirror of `_connectionState === Connected`. True only after the
|
|
2236
2466
|
* handshake round-trip completes; outbound `call()` checks this to
|
|
@@ -2242,6 +2472,7 @@ var RpcPeer = class {
|
|
|
2242
2472
|
_connectionState = 0 /* Disconnected */;
|
|
2243
2473
|
_keepAliveTimer;
|
|
2244
2474
|
_keepAliveWatchdog;
|
|
2475
|
+
_maintainTimer;
|
|
2245
2476
|
/** Wall-clock time of the last inbound `$sys.KeepAlive`, or 0 if none yet
|
|
2246
2477
|
* for the current connection. Useful for diagnostics. */
|
|
2247
2478
|
_lastKeepAliveAt = 0;
|
|
@@ -2256,6 +2487,37 @@ var RpcPeer = class {
|
|
|
2256
2487
|
this.ref = ref;
|
|
2257
2488
|
this.keepAlivePeriodMs = hub.limits.keepAlivePeriodMs;
|
|
2258
2489
|
this.keepAliveTimeoutMs = hub.limits.keepAliveTimeoutMs;
|
|
2490
|
+
this.inboundCalls.completedCallsLimit = hub.limits.completedInboundCallsLimit;
|
|
2491
|
+
const checkPeriod = hub.limits.callTimeoutCheckPeriodMs;
|
|
2492
|
+
if (checkPeriod > 0 && Number.isFinite(checkPeriod)) {
|
|
2493
|
+
this._maintainTimer = setInterval(() => this._maintainOutboundCalls(), checkPeriod);
|
|
2494
|
+
this._maintainTimer.unref?.();
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
/** Fail outbound calls whose per-call {@link RpcCallTimeouts} elapsed —
|
|
2498
|
+
* the connect timeout while a call still awaits the wire, the run timeout
|
|
2499
|
+
* once it has been sent. Mirrors .NET `RpcOutboundCallTracker.Maintain`
|
|
2500
|
+
* (RpcCallTrackers.cs:108-207). Calls without timeouts (queries) never
|
|
2501
|
+
* time out here. */
|
|
2502
|
+
_maintainOutboundCalls() {
|
|
2503
|
+
const now = Date.now();
|
|
2504
|
+
const canRunTimeout = this._isConnected && (this._lastKeepAliveAt === 0 || now - this._lastKeepAliveAt <= this.keepAliveTimeoutMs);
|
|
2505
|
+
for (const call of [...this.outboundCalls.values()]) {
|
|
2506
|
+
const timeouts = call.timeouts;
|
|
2507
|
+
if (timeouts === void 0 || call.result.isCompleted)
|
|
2508
|
+
continue;
|
|
2509
|
+
let timedOut;
|
|
2510
|
+
if (call.sentAt === 0) {
|
|
2511
|
+
if (now - call.startedAt >= timeouts.connectTimeoutMs)
|
|
2512
|
+
timedOut = `connect timeout (${timeouts.connectTimeoutMs}ms)`;
|
|
2513
|
+
} else if (canRunTimeout && now - call.sentAt >= timeouts.runTimeoutMs)
|
|
2514
|
+
timedOut = `run timeout (${timeouts.runTimeoutMs}ms)`;
|
|
2515
|
+
if (timedOut === void 0)
|
|
2516
|
+
continue;
|
|
2517
|
+
this.outboundCalls.remove(call.callId);
|
|
2518
|
+
call.result.reject(new TimeoutError(`'${this.ref}': call ${call.method}#${call.callId} timed out \u2014 ${timedOut}`));
|
|
2519
|
+
call.onDisconnect();
|
|
2520
|
+
}
|
|
2259
2521
|
}
|
|
2260
2522
|
get connection() {
|
|
2261
2523
|
return this._connection;
|
|
@@ -2266,6 +2528,9 @@ var RpcPeer = class {
|
|
|
2266
2528
|
get isConnected() {
|
|
2267
2529
|
return this._isConnected;
|
|
2268
2530
|
}
|
|
2531
|
+
get ownHandshakeIndex() {
|
|
2532
|
+
return this._ownHandshakeIndex;
|
|
2533
|
+
}
|
|
2269
2534
|
_setConnectionState(value) {
|
|
2270
2535
|
if (this._connectionState === value) return;
|
|
2271
2536
|
const previousState = this._connectionState;
|
|
@@ -2337,14 +2602,17 @@ var RpcPeer = class {
|
|
|
2337
2602
|
this._connection?.encoder,
|
|
2338
2603
|
this.hub.registry
|
|
2339
2604
|
);
|
|
2605
|
+
outboundCall.timeouts = options?.timeouts;
|
|
2340
2606
|
this.outboundCalls.register(outboundCall);
|
|
2341
2607
|
if (this._isConnected) {
|
|
2608
|
+
outboundCall.sentAt = Date.now();
|
|
2342
2609
|
this._sendWireData(outboundCall.serializedWireData);
|
|
2343
2610
|
} else {
|
|
2344
2611
|
const stateHandler = (state) => {
|
|
2345
2612
|
if (state !== 3 /* Connected */) return;
|
|
2346
2613
|
this.connectionStateChanged.remove(stateHandler);
|
|
2347
2614
|
if (outboundCall.result.isCompleted) return;
|
|
2615
|
+
outboundCall.sentAt = Date.now();
|
|
2348
2616
|
this._sendWireData(outboundCall.serializedWireData);
|
|
2349
2617
|
};
|
|
2350
2618
|
this.connectionStateChanged.add(stateHandler);
|
|
@@ -2354,9 +2622,9 @@ var RpcPeer = class {
|
|
|
2354
2622
|
if (signal !== void 0) {
|
|
2355
2623
|
const onAbort = () => {
|
|
2356
2624
|
if (this.outboundCalls.remove(callId) !== void 0) {
|
|
2357
|
-
outboundCall.result.reject(
|
|
2625
|
+
outboundCall.result.reject(cancellationError("Call cancelled."));
|
|
2358
2626
|
outboundCall.onDisconnect();
|
|
2359
|
-
if (this._connection !== void 0)
|
|
2627
|
+
if (this._isConnected && this._connection !== void 0)
|
|
2360
2628
|
this.hub.systemCallSender.cancel(this._connection, this.serializationFormat, callId);
|
|
2361
2629
|
}
|
|
2362
2630
|
};
|
|
@@ -2386,10 +2654,15 @@ var RpcPeer = class {
|
|
|
2386
2654
|
}
|
|
2387
2655
|
close() {
|
|
2388
2656
|
infoLog?.log(`'${this.ref}': Stopping`);
|
|
2657
|
+
if (this._maintainTimer !== void 0) {
|
|
2658
|
+
clearInterval(this._maintainTimer);
|
|
2659
|
+
this._maintainTimer = void 0;
|
|
2660
|
+
}
|
|
2389
2661
|
this.remoteObjects.disconnectAll();
|
|
2390
2662
|
this.sharedObjects.disconnectAll();
|
|
2391
|
-
this.outboundCalls.rejectAll(
|
|
2663
|
+
this.outboundCalls.rejectAll(cancellationError("Peer closed."));
|
|
2392
2664
|
this.outboundCalls.invalidateAll();
|
|
2665
|
+
this.inboundCalls.clear();
|
|
2393
2666
|
this._connection?.close();
|
|
2394
2667
|
this.hub.peers.delete(this.ref);
|
|
2395
2668
|
this._setConnectionState(0 /* Disconnected */);
|
|
@@ -2428,12 +2701,18 @@ var RpcPeer = class {
|
|
|
2428
2701
|
if (method === RpcSystemCalls.handshake) {
|
|
2429
2702
|
const raw = args[0];
|
|
2430
2703
|
if (raw !== void 0) {
|
|
2704
|
+
const obj = raw ?? {};
|
|
2431
2705
|
const handshake = Array.isArray(raw) ? {
|
|
2432
2706
|
RemotePeerId: raw[0],
|
|
2433
2707
|
RemoteHubId: raw[2],
|
|
2434
2708
|
ProtocolVersion: raw[3],
|
|
2435
2709
|
Index: raw[4]
|
|
2436
|
-
} :
|
|
2710
|
+
} : {
|
|
2711
|
+
RemotePeerId: obj.RemotePeerId ?? obj.remotePeerId,
|
|
2712
|
+
RemoteHubId: obj.RemoteHubId ?? obj.remoteHubId,
|
|
2713
|
+
ProtocolVersion: obj.ProtocolVersion ?? obj.protocolVersion,
|
|
2714
|
+
Index: obj.Index ?? obj.index
|
|
2715
|
+
};
|
|
2437
2716
|
this._onHandshakeReceived(handshake);
|
|
2438
2717
|
}
|
|
2439
2718
|
return;
|
|
@@ -2447,57 +2726,85 @@ var RpcPeer = class {
|
|
|
2447
2726
|
_handleInbound(message, args) {
|
|
2448
2727
|
const method = message.Method ?? "";
|
|
2449
2728
|
const relatedId = message.RelatedId ?? 0;
|
|
2450
|
-
const call = new RpcInboundCall(relatedId, method, args);
|
|
2451
2729
|
const methodDef = this.hub.serviceHost.getMethodDef(method);
|
|
2452
2730
|
const isNoWait = methodDef?.noWait === true;
|
|
2453
|
-
if (!isNoWait) {
|
|
2454
|
-
this.inboundCalls.register(call);
|
|
2455
|
-
}
|
|
2456
2731
|
const serviceHost = this.hub.serviceHost;
|
|
2457
|
-
|
|
2732
|
+
const dispatch = async (call2) => {
|
|
2458
2733
|
try {
|
|
2459
2734
|
const context = this._connection !== void 0 ? {
|
|
2460
2735
|
__rpcDispatch: true,
|
|
2461
2736
|
callId: relatedId,
|
|
2462
|
-
|
|
2737
|
+
callType: message.CallType ?? 0,
|
|
2738
|
+
connection: this._connection,
|
|
2739
|
+
signal: call2?.signal,
|
|
2740
|
+
peer: this
|
|
2463
2741
|
} : void 0;
|
|
2464
|
-
const result = await serviceHost.dispatch(
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2742
|
+
const result = await serviceHost.dispatch(method, args, context);
|
|
2743
|
+
if (methodDef?.stream === true) {
|
|
2744
|
+
if (this._connection !== void 0) {
|
|
2745
|
+
const stream = result instanceof RpcStream ? result : new RpcStream(result);
|
|
2746
|
+
const ref = stream.toRef(this);
|
|
2747
|
+
const send = () => {
|
|
2748
|
+
if (call2?.isCancelled === true) return;
|
|
2749
|
+
if (this._connection !== void 0)
|
|
2750
|
+
this.hub.systemCallSender.ok(
|
|
2751
|
+
this._connection,
|
|
2752
|
+
this.serializationFormat,
|
|
2753
|
+
relatedId,
|
|
2754
|
+
ref
|
|
2755
|
+
);
|
|
2756
|
+
};
|
|
2757
|
+
if (call2 !== void 0) {
|
|
2758
|
+
call2.setResult(send);
|
|
2759
|
+
this.inboundCalls.markCompleted(call2);
|
|
2760
|
+
}
|
|
2761
|
+
send();
|
|
2762
|
+
}
|
|
2763
|
+
} else if (call2 !== void 0) {
|
|
2764
|
+
const send = () => {
|
|
2765
|
+
if (call2.isCancelled) return;
|
|
2766
|
+
if (this._connection !== void 0)
|
|
2767
|
+
this.hub.systemCallSender.ok(
|
|
2768
|
+
this._connection,
|
|
2769
|
+
this.serializationFormat,
|
|
2770
|
+
relatedId,
|
|
2771
|
+
result
|
|
2772
|
+
);
|
|
2773
|
+
};
|
|
2774
|
+
call2.setResult(send);
|
|
2775
|
+
this.inboundCalls.markCompleted(call2);
|
|
2776
|
+
send();
|
|
2484
2777
|
}
|
|
2485
2778
|
} catch (e) {
|
|
2486
|
-
if (
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
this.
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2779
|
+
if (call2 !== void 0) {
|
|
2780
|
+
const send = () => {
|
|
2781
|
+
if (call2.isCancelled) return;
|
|
2782
|
+
if (this._connection !== void 0)
|
|
2783
|
+
this.hub.systemCallSender.error(
|
|
2784
|
+
this._connection,
|
|
2785
|
+
this.serializationFormat,
|
|
2786
|
+
relatedId,
|
|
2787
|
+
e
|
|
2788
|
+
);
|
|
2789
|
+
};
|
|
2790
|
+
call2.setResult(send);
|
|
2791
|
+
this.inboundCalls.markCompleted(call2);
|
|
2792
|
+
send();
|
|
2493
2793
|
}
|
|
2494
2794
|
debugLog2?.log(`'${this.ref}': Inbound call failed: ${method}#${relatedId}`, e);
|
|
2495
|
-
} finally {
|
|
2496
|
-
if (!isNoWait) {
|
|
2497
|
-
this.inboundCalls.remove(relatedId);
|
|
2498
|
-
}
|
|
2499
2795
|
}
|
|
2500
|
-
}
|
|
2796
|
+
};
|
|
2797
|
+
if (isNoWait) {
|
|
2798
|
+
void dispatch(void 0);
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
const call = new RpcInboundCall(relatedId, method, args);
|
|
2802
|
+
const existing = this.inboundCalls.getOrRegister(call);
|
|
2803
|
+
if (existing !== call) {
|
|
2804
|
+
existing.resendResult();
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2807
|
+
void dispatch(call);
|
|
2501
2808
|
}
|
|
2502
2809
|
/** Start sending `$sys.KeepAlive` pings and arm the silence watchdog.
|
|
2503
2810
|
* Called when the peer transitions into `Connected` (after handshake).
|
|
@@ -2553,15 +2860,17 @@ var RpcConnectionState = /* @__PURE__ */ ((RpcConnectionState2) => {
|
|
|
2553
2860
|
})(RpcConnectionState || {});
|
|
2554
2861
|
var RpcClientPeer = class extends RpcPeer {
|
|
2555
2862
|
_disposed = false;
|
|
2863
|
+
/** Aborted when the peer is stopped/disposed — threaded into the reconnect
|
|
2864
|
+
* delay so a pending delay rejects promptly instead of parking. */
|
|
2865
|
+
_stopController = new AbortController();
|
|
2556
2866
|
_tryIndex = 0;
|
|
2557
|
-
_handshakeIndex = 0;
|
|
2558
2867
|
/** Index from the MOST RECENTLY RECEIVED server handshake — sent back
|
|
2559
2868
|
* to the server in `$sys.Reconnect:3` so the server can verify we're
|
|
2560
2869
|
* talking about the current connection. Mirrors .NET's
|
|
2561
2870
|
* `connectionState.Handshake.Index` path in
|
|
2562
2871
|
* `RpcOutboundCallTracker.TryReconnect`. */
|
|
2563
2872
|
_remoteHandshakeIndex = 0;
|
|
2564
|
-
|
|
2873
|
+
_lastRemotePeerId;
|
|
2565
2874
|
_pendingHandshake;
|
|
2566
2875
|
_reconnectsAt = 0;
|
|
2567
2876
|
/** Base64url-encoded peer ID — matches .NET's RpcClientPeer.ClientId (Guid.ToBase64Url). */
|
|
@@ -2582,6 +2891,10 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2582
2891
|
* the connection attempt. Initialized from `hub.limits` at construction;
|
|
2583
2892
|
* set directly to override on a single peer. */
|
|
2584
2893
|
handshakeTimeoutMs;
|
|
2894
|
+
/** A connection that lived less than this before dropping does NOT reset
|
|
2895
|
+
* `_tryIndex`, so a crash-looping server sees growing reconnect delays.
|
|
2896
|
+
* Initialized from `hub.limits` at construction; override per peer. */
|
|
2897
|
+
prematureDisconnectTimeoutMs;
|
|
2585
2898
|
peerChanged = new EventHandlerSet3();
|
|
2586
2899
|
/** Fired when the server rejects the connection due to an unsupported serialization format (close code 4010). */
|
|
2587
2900
|
unsupportedFormat = new EventHandlerSet3();
|
|
@@ -2614,6 +2927,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2614
2927
|
super(hub, url);
|
|
2615
2928
|
this.connectTimeoutMs = hub.limits.connectTimeoutMs;
|
|
2616
2929
|
this.handshakeTimeoutMs = hub.limits.handshakeTimeoutMs;
|
|
2930
|
+
this.prematureDisconnectTimeoutMs = hub.limits.prematureDisconnectTimeoutMs;
|
|
2617
2931
|
this.clientId = guidToBase64Url(this.id);
|
|
2618
2932
|
const resolver = RpcSerializationFormatResolver.Default;
|
|
2619
2933
|
this.serializationFormat = resolver.get(parseFormatFromUrl(url) ?? resolver.defaultFormatKey);
|
|
@@ -2650,7 +2964,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2650
2964
|
try {
|
|
2651
2965
|
while (!this._disposed) {
|
|
2652
2966
|
this._setConnectionState(1 /* Connecting */);
|
|
2653
|
-
const retryDelay = this.hub.reconnectDelayer.getDelay(this._tryIndex);
|
|
2967
|
+
const retryDelay = this.hub.reconnectDelayer.getDelay(this._tryIndex, this._stopController.signal);
|
|
2654
2968
|
if (retryDelay.isLimitExceeded) {
|
|
2655
2969
|
warnLog6?.log(`'${this.ref}': Reconnect limit exceeded - shutting down`);
|
|
2656
2970
|
this._disposed = true;
|
|
@@ -2659,12 +2973,14 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2659
2973
|
this._setReconnectsAt(retryDelay.endsAt);
|
|
2660
2974
|
try {
|
|
2661
2975
|
await retryDelay.promise;
|
|
2976
|
+
} catch {
|
|
2662
2977
|
} finally {
|
|
2663
2978
|
this._setReconnectsAt(0);
|
|
2664
2979
|
}
|
|
2665
2980
|
if (this._disposed) break;
|
|
2666
2981
|
let lastCloseCode = 0;
|
|
2667
2982
|
let lastCloseReason = "";
|
|
2983
|
+
let connectedAt = 0;
|
|
2668
2984
|
try {
|
|
2669
2985
|
const connUrl = await this.connectionUrlResolver(this);
|
|
2670
2986
|
infoLog?.log(`'${this.ref}': Connecting to ${sanitizeUrl(connUrl)}`);
|
|
@@ -2711,7 +3027,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2711
3027
|
this.serializationFormat,
|
|
2712
3028
|
this.id,
|
|
2713
3029
|
this.hub.hubId,
|
|
2714
|
-
++this.
|
|
3030
|
+
++this._ownHandshakeIndex
|
|
2715
3031
|
);
|
|
2716
3032
|
} catch (e) {
|
|
2717
3033
|
warnLog6?.log(`'${this.ref}': Failed to send handshake`, e);
|
|
@@ -2735,20 +3051,10 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2735
3051
|
this._pendingHandshake = void 0;
|
|
2736
3052
|
}
|
|
2737
3053
|
this._remoteHandshakeIndex = remoteHandshake.Index ?? 0;
|
|
2738
|
-
const
|
|
2739
|
-
let isPeerChanged = false;
|
|
2740
|
-
if (remoteHubId !== void 0) {
|
|
2741
|
-
isPeerChanged = this._lastRemoteHubId !== void 0 && this._lastRemoteHubId !== remoteHubId;
|
|
2742
|
-
if (isPeerChanged) {
|
|
2743
|
-
this.inboundCalls.clear();
|
|
2744
|
-
infoLog?.log(`'${this.ref}': Peer changed`);
|
|
2745
|
-
this.peerChanged.trigger();
|
|
2746
|
-
}
|
|
2747
|
-
this._lastRemoteHubId = remoteHubId;
|
|
2748
|
-
}
|
|
3054
|
+
const isPeerChanged = this._detectPeerChange(remoteHandshake);
|
|
2749
3055
|
infoLog?.log(`'${this.ref}': Handshake succeeded, peerChanged=${isPeerChanged}`);
|
|
2750
3056
|
this._setConnectionState(3 /* Connected */);
|
|
2751
|
-
|
|
3057
|
+
connectedAt = Date.now();
|
|
2752
3058
|
await this._reconnect(isPeerChanged, closedController.signal);
|
|
2753
3059
|
await whenClosed;
|
|
2754
3060
|
} catch (e) {
|
|
@@ -2762,6 +3068,8 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2762
3068
|
this._disposed = true;
|
|
2763
3069
|
break;
|
|
2764
3070
|
}
|
|
3071
|
+
if (connectedAt !== 0 && Date.now() - connectedAt >= this.prematureDisconnectTimeoutMs)
|
|
3072
|
+
this._tryIndex = 0;
|
|
2765
3073
|
this._tryIndex++;
|
|
2766
3074
|
}
|
|
2767
3075
|
} finally {
|
|
@@ -2817,6 +3125,8 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2817
3125
|
continue;
|
|
2818
3126
|
}
|
|
2819
3127
|
eligible.push(call);
|
|
3128
|
+
if (call.sentAt !== 0)
|
|
3129
|
+
call.sentAt = Date.now();
|
|
2820
3130
|
}
|
|
2821
3131
|
let unknownIds = null;
|
|
2822
3132
|
if (!isPeerChanged && eligible.length > 0) {
|
|
@@ -2833,6 +3143,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2833
3143
|
if (unknownIds !== null && !unknownIds.has(call.callId)) {
|
|
2834
3144
|
continue;
|
|
2835
3145
|
}
|
|
3146
|
+
call.sentAt = Date.now();
|
|
2836
3147
|
this._sendWireData(call.serializedWireData);
|
|
2837
3148
|
resentSinceYield++;
|
|
2838
3149
|
if (resentSinceYield >= YIELD_BATCH) {
|
|
@@ -2898,30 +3209,80 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2898
3209
|
return new Set(eligible.map((c) => c.callId));
|
|
2899
3210
|
}
|
|
2900
3211
|
}
|
|
3212
|
+
/** Detects a server-peer identity change by comparing the incoming
|
|
3213
|
+
* handshake's `RemotePeerId` with the last one — C#
|
|
3214
|
+
* `RpcHandshake.GetPeerChangeKind` keys off `RemotePeerId`, not
|
|
3215
|
+
* `RemoteHubId` (an idle server peer can be replaced while keeping the
|
|
3216
|
+
* same hub id). On a change it clears the inbound tracker and fires
|
|
3217
|
+
* `peerChanged`; the very first handshake is never a change. */
|
|
3218
|
+
_detectPeerChange(handshake) {
|
|
3219
|
+
const remotePeerId = handshake.RemotePeerId;
|
|
3220
|
+
if (remotePeerId === void 0)
|
|
3221
|
+
return false;
|
|
3222
|
+
const isPeerChanged = this._lastRemotePeerId !== void 0 && this._lastRemotePeerId !== remotePeerId;
|
|
3223
|
+
this._lastRemotePeerId = remotePeerId;
|
|
3224
|
+
if (isPeerChanged) {
|
|
3225
|
+
this.inboundCalls.clear();
|
|
3226
|
+
infoLog?.log(`'${this.ref}': Peer changed`);
|
|
3227
|
+
this.peerChanged.trigger();
|
|
3228
|
+
}
|
|
3229
|
+
return isPeerChanged;
|
|
3230
|
+
}
|
|
2901
3231
|
_onHandshakeReceived(handshake) {
|
|
2902
3232
|
this._pendingHandshake?.resolve(handshake);
|
|
2903
3233
|
}
|
|
2904
3234
|
close() {
|
|
2905
3235
|
this._disposed = true;
|
|
3236
|
+
this._stopController.abort();
|
|
2906
3237
|
super.close();
|
|
2907
3238
|
}
|
|
2908
3239
|
};
|
|
2909
3240
|
var RpcServerPeer = class extends RpcPeer {
|
|
2910
|
-
|
|
2911
|
-
|
|
3241
|
+
/** Grace period before an idle (disconnected) server peer is stopped and
|
|
3242
|
+
* removed from `hub.peers`; a re-`accept()` within it cancels removal.
|
|
3243
|
+
* Snapshotted from `hub.limits` at construction (F11). */
|
|
3244
|
+
serverPeerCloseTimeoutMs;
|
|
3245
|
+
_closeTimer;
|
|
3246
|
+
_isClosed = false;
|
|
2912
3247
|
constructor(hub, ref) {
|
|
2913
3248
|
super(hub, ref);
|
|
3249
|
+
this.serverPeerCloseTimeoutMs = hub.limits.serverPeerCloseTimeoutMs;
|
|
2914
3250
|
}
|
|
2915
3251
|
/** Accept an incoming connection — sets up message handling and handshake response. */
|
|
2916
3252
|
accept(conn) {
|
|
3253
|
+
this._clearCloseTimer();
|
|
2917
3254
|
this._setConnectionState(1 /* Connecting */);
|
|
2918
3255
|
this.setupConnection(conn);
|
|
2919
3256
|
this._setConnectionState(2 /* Handshaking */);
|
|
2920
3257
|
conn.closed.add(() => {
|
|
2921
3258
|
if (this._connection !== conn && this._connection !== void 0) return;
|
|
2922
3259
|
this.sharedObjects.disconnectAll();
|
|
3260
|
+
this._armCloseTimer();
|
|
2923
3261
|
});
|
|
2924
3262
|
}
|
|
3263
|
+
close() {
|
|
3264
|
+
this._isClosed = true;
|
|
3265
|
+
this._clearCloseTimer();
|
|
3266
|
+
super.close();
|
|
3267
|
+
}
|
|
3268
|
+
_armCloseTimer() {
|
|
3269
|
+
this._clearCloseTimer();
|
|
3270
|
+
if (this._isClosed) return;
|
|
3271
|
+
const timeout = this.serverPeerCloseTimeoutMs;
|
|
3272
|
+
if (!(timeout > 0) || !Number.isFinite(timeout)) return;
|
|
3273
|
+
this._closeTimer = setTimeout(() => {
|
|
3274
|
+
this._closeTimer = void 0;
|
|
3275
|
+
if (this._connection === void 0)
|
|
3276
|
+
this.close();
|
|
3277
|
+
}, timeout);
|
|
3278
|
+
this._closeTimer.unref?.();
|
|
3279
|
+
}
|
|
3280
|
+
_clearCloseTimer() {
|
|
3281
|
+
if (this._closeTimer !== void 0) {
|
|
3282
|
+
clearTimeout(this._closeTimer);
|
|
3283
|
+
this._closeTimer = void 0;
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
2925
3286
|
_onHandshakeReceived(_handshake) {
|
|
2926
3287
|
if (this._connection !== void 0) {
|
|
2927
3288
|
this.hub.systemCallSender.handshake(
|
|
@@ -2929,7 +3290,7 @@ var RpcServerPeer = class extends RpcPeer {
|
|
|
2929
3290
|
this.serializationFormat,
|
|
2930
3291
|
this.id,
|
|
2931
3292
|
this.hub.hubId,
|
|
2932
|
-
|
|
3293
|
+
++this._ownHandshakeIndex
|
|
2933
3294
|
);
|
|
2934
3295
|
this._setConnectionState(3 /* Connected */);
|
|
2935
3296
|
}
|
|
@@ -2968,6 +3329,12 @@ var RpcLimits = class _RpcLimits {
|
|
|
2968
3329
|
/** Process-wide default. Hubs read from this when they're constructed
|
|
2969
3330
|
* unless caller assigns a custom instance to `hub.limits`. */
|
|
2970
3331
|
static Default = new _RpcLimits();
|
|
3332
|
+
/** Dev preset with a relaxed keep-alive timeout (5 min), matching .NET's
|
|
3333
|
+
* debugger-attached `KeepAlivePeriod` (RpcLimits.cs:46-54). Activate
|
|
3334
|
+
* explicitly — e.g. `RpcLimits.Default = RpcLimits.Debug` or per-hub
|
|
3335
|
+
* `hub.limits = RpcLimits.Debug`. Never switched on automatically: the
|
|
3336
|
+
* TS side can't detect a debugger on the server (decision D4). */
|
|
3337
|
+
static Debug = new _RpcLimits({ keepAliveTimeoutMs: 3e5 });
|
|
2971
3338
|
/** Max time to wait for the WebSocket to enter the OPEN state. On a
|
|
2972
3339
|
* hung connect (mobile after network change, half-open after sleep)
|
|
2973
3340
|
* the browser can take ~2 min to emit `onerror`/`onclose`; without
|
|
@@ -2975,6 +3342,22 @@ var RpcLimits = class _RpcLimits {
|
|
|
2975
3342
|
connectTimeoutMs = 1e4;
|
|
2976
3343
|
/** Max time to wait for the server's handshake response after WS opens. */
|
|
2977
3344
|
handshakeTimeoutMs = 1e4;
|
|
3345
|
+
/** If a connection lived less than this before dropping, a graceful close
|
|
3346
|
+
* still counts as a failed attempt — the client keeps its growing
|
|
3347
|
+
* `_tryIndex` instead of resetting it, so a crash-looping server sees
|
|
3348
|
+
* increasing reconnect delays via `RetryDelaySeq`. Mirrors .NET
|
|
3349
|
+
* `RpcLimits.PrematureDisconnectTimeout` (RpcLimits.cs:18). */
|
|
3350
|
+
prematureDisconnectTimeoutMs = 15e3;
|
|
3351
|
+
/** Max completed inbound calls retained for duplicate-frame dedup
|
|
3352
|
+
* (a resent call id re-sends the computed result instead of
|
|
3353
|
+
* re-executing the handler). Deviation from .NET, which unregisters a
|
|
3354
|
+
* call once its result is sent: TS clients blind-resend on reconnect,
|
|
3355
|
+
* so completed calls are retained — bounded by this limit (oldest
|
|
3356
|
+
* evicted first) to cap memory on long-lived connections. */
|
|
3357
|
+
completedInboundCallsLimit = 1e3;
|
|
3358
|
+
/** How often each peer's maintenance loop scans outbound calls for
|
|
3359
|
+
* timeouts (R12). Mirrors .NET `RpcLimits.CallTimeoutCheckPeriod`. */
|
|
3360
|
+
callTimeoutCheckPeriodMs = 1e3;
|
|
2978
3361
|
/** Outbound `$sys.KeepAlive` send period. */
|
|
2979
3362
|
keepAlivePeriodMs = 1e4;
|
|
2980
3363
|
/** Reaper threshold — connection is force-closed (and the reconnect
|
|
@@ -2984,6 +3367,13 @@ var RpcLimits = class _RpcLimits {
|
|
|
2984
3367
|
* `keepAlivePeriodMs + stall_duration`, so with these defaults the
|
|
2985
3368
|
* tolerated stall is ~15 s. */
|
|
2986
3369
|
keepAliveTimeoutMs = 25e3;
|
|
3370
|
+
/** How long a server peer stays in `hub.peers` after its connection closes
|
|
3371
|
+
* before it's stopped and removed. A same-peer reconnect (`accept()`)
|
|
3372
|
+
* within this window cancels the pending removal, so brief drops don't
|
|
3373
|
+
* discard the peer's trackers. Mirrors .NET's server-peer shutdown timeout
|
|
3374
|
+
* (`RpcPeerOptions.ServerPeerShutdownTimeoutProvider`, 3-15 min); the TS
|
|
3375
|
+
* port uses a fixed floor-matching default. */
|
|
3376
|
+
serverPeerCloseTimeoutMs = 18e4;
|
|
2987
3377
|
constructor(overrides) {
|
|
2988
3378
|
if (overrides) Object.assign(this, overrides);
|
|
2989
3379
|
}
|
|
@@ -2994,7 +3384,7 @@ import { RetryDelaySeq, RetryDelayer } from "@actuallab/core";
|
|
|
2994
3384
|
var RpcClientPeerReconnectDelayer = class extends RetryDelayer {
|
|
2995
3385
|
constructor() {
|
|
2996
3386
|
super();
|
|
2997
|
-
this.delays = RetryDelaySeq.
|
|
3387
|
+
this.delays = RetryDelaySeq.exp(100, 1e4);
|
|
2998
3388
|
}
|
|
2999
3389
|
};
|
|
3000
3390
|
|
|
@@ -3002,13 +3392,14 @@ var RpcClientPeerReconnectDelayer = class extends RetryDelayer {
|
|
|
3002
3392
|
var { warnLog: warnLog7 } = getLogs("RpcServiceHost");
|
|
3003
3393
|
var RpcServiceHost = class {
|
|
3004
3394
|
_methods = /* @__PURE__ */ new Map();
|
|
3005
|
-
register(def, impl) {
|
|
3395
|
+
register(def, impl, receiver = impl) {
|
|
3006
3396
|
for (const methodDef of def.methods.values()) {
|
|
3007
3397
|
const fn = impl[methodDef.name];
|
|
3008
3398
|
if (!fn) continue;
|
|
3009
3399
|
this._methods.set(wireMethodName(methodDef), {
|
|
3010
3400
|
def: methodDef,
|
|
3011
|
-
fn
|
|
3401
|
+
fn,
|
|
3402
|
+
receiver
|
|
3012
3403
|
});
|
|
3013
3404
|
}
|
|
3014
3405
|
}
|
|
@@ -3019,9 +3410,9 @@ var RpcServiceHost = class {
|
|
|
3019
3410
|
throw new Error(`Method not found: ${wireMethod}`);
|
|
3020
3411
|
}
|
|
3021
3412
|
if (context !== void 0 && entry.def.callTypeId !== 0) {
|
|
3022
|
-
return await entry.fn(...args, context);
|
|
3413
|
+
return await entry.fn.call(entry.receiver, ...args, context);
|
|
3023
3414
|
}
|
|
3024
|
-
return await entry.fn(...args);
|
|
3415
|
+
return await entry.fn.call(entry.receiver, ...args);
|
|
3025
3416
|
}
|
|
3026
3417
|
getMethodDef(wireMethod) {
|
|
3027
3418
|
return this._methods.get(wireMethod)?.def;
|
|
@@ -3029,6 +3420,7 @@ var RpcServiceHost = class {
|
|
|
3029
3420
|
};
|
|
3030
3421
|
|
|
3031
3422
|
// src/rpc-decorators.ts
|
|
3423
|
+
import { ownMetadata, resolveArgCount } from "@actuallab/core";
|
|
3032
3424
|
var METHODS_META = /* @__PURE__ */ Symbol.for("actuallab.methods");
|
|
3033
3425
|
var SERVICE_META = /* @__PURE__ */ Symbol.for("actuallab.service");
|
|
3034
3426
|
function getMethodsMeta(cls) {
|
|
@@ -3039,17 +3431,17 @@ function getServiceMeta(cls) {
|
|
|
3039
3431
|
}
|
|
3040
3432
|
function rpcService(serviceName) {
|
|
3041
3433
|
return function(_target, context) {
|
|
3042
|
-
const meta = context.metadata
|
|
3434
|
+
const meta = ownMetadata(context.metadata, SERVICE_META);
|
|
3043
3435
|
meta.name = serviceName;
|
|
3044
3436
|
};
|
|
3045
3437
|
}
|
|
3046
3438
|
function rpcMethod(options) {
|
|
3047
3439
|
return function(target, context) {
|
|
3048
3440
|
const methodName = String(context.name);
|
|
3049
|
-
const methods = context.metadata
|
|
3441
|
+
const methods = ownMetadata(context.metadata, METHODS_META);
|
|
3050
3442
|
methods[methodName] = {
|
|
3051
3443
|
...methods[methodName],
|
|
3052
|
-
argCount: target
|
|
3444
|
+
argCount: resolveArgCount(target, options?.argCount, methodName),
|
|
3053
3445
|
returns: options?.returns,
|
|
3054
3446
|
remoteExecutionMode: options?.remoteExecutionMode
|
|
3055
3447
|
};
|
|
@@ -3595,7 +3987,7 @@ var RpcHub = class {
|
|
|
3595
3987
|
hubId;
|
|
3596
3988
|
peers = /* @__PURE__ */ new Map();
|
|
3597
3989
|
serviceHost;
|
|
3598
|
-
systemCallSender
|
|
3990
|
+
systemCallSender;
|
|
3599
3991
|
systemCallHandler = new RpcSystemCallHandler();
|
|
3600
3992
|
/** Method registry for compact format hash ↔ name resolution.
|
|
3601
3993
|
* Created lazily, populated by addService/addClient. */
|
|
@@ -3603,6 +3995,14 @@ var RpcHub = class {
|
|
|
3603
3995
|
/** Shared reconnect delayer used by every client peer in this hub. Swap in
|
|
3604
3996
|
* a custom subclass (e.g. app-level signal-gated) before peers start. */
|
|
3605
3997
|
reconnectDelayer = new RpcClientPeerReconnectDelayer();
|
|
3998
|
+
/** Cache of client proxies keyed by peer, then by service name — so
|
|
3999
|
+
* repeated {@link addClient} calls for the same service+peer return one
|
|
4000
|
+
* proxy. All consumers then share a single computed/call/invalidation
|
|
4001
|
+
* stream per logical value, matching .NET's singleton client proxies (F8).
|
|
4002
|
+
* A later def registered under the same name extends the existing proxy
|
|
4003
|
+
* with its extra methods (superset), so no consumer ever receives a proxy
|
|
4004
|
+
* missing methods the def declares. */
|
|
4005
|
+
_clientProxies = /* @__PURE__ */ new WeakMap();
|
|
3606
4006
|
/** Connection-lifecycle timing limits. Peers constructed against this hub
|
|
3607
4007
|
* read their initial `*Ms` field values from this instance; later
|
|
3608
4008
|
* mutations affect only peers built afterward (existing peers can be
|
|
@@ -3620,6 +4020,7 @@ var RpcHub = class {
|
|
|
3620
4020
|
constructor(hubId) {
|
|
3621
4021
|
this.hubId = hubId ?? crypto.randomUUID();
|
|
3622
4022
|
this.serviceHost = new RpcServiceHost();
|
|
4023
|
+
this.systemCallSender = this._createSystemCallSender();
|
|
3623
4024
|
this.systemCallSender.registry = this.registry;
|
|
3624
4025
|
for (const methodName of Object.values(RpcSystemCalls)) {
|
|
3625
4026
|
this.registry.register(methodName);
|
|
@@ -3680,50 +4081,38 @@ var RpcHub = class {
|
|
|
3680
4081
|
if (!fn) continue;
|
|
3681
4082
|
wrappedImpl[methodDef.name] = methodDef.callTypeId !== 0 ? this._wrapServerMethod(methodDef, fn, impl) : fn;
|
|
3682
4083
|
}
|
|
3683
|
-
this.serviceHost.register(def, wrappedImpl);
|
|
4084
|
+
this.serviceHost.register(def, wrappedImpl, impl);
|
|
3684
4085
|
this.registry.registerService(def.name, def.methods);
|
|
3685
4086
|
}
|
|
3686
|
-
/** Create a typed client proxy for a service on a remote peer.
|
|
4087
|
+
/** Create a typed client proxy for a service on a remote peer. Repeated calls
|
|
4088
|
+
* for the same (peer, service name) return one shared proxy; a def carrying
|
|
4089
|
+
* extra methods extends it (F8). */
|
|
3687
4090
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T is used for caller-specified proxy type
|
|
3688
4091
|
addClient(peer, defOrContract) {
|
|
3689
4092
|
const def = this._resolveServiceDef(defOrContract);
|
|
3690
4093
|
this.registry.registerService(def.name, def.methods);
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
byArgCount = /* @__PURE__ */ new Map();
|
|
3696
|
-
overloads.set(methodDef.name, byArgCount);
|
|
3697
|
-
}
|
|
3698
|
-
byArgCount.set(
|
|
3699
|
-
methodDef.argCount,
|
|
3700
|
-
this._createClientMethod(peer, methodDef)
|
|
3701
|
-
);
|
|
4094
|
+
let byService = this._clientProxies.get(peer);
|
|
4095
|
+
if (byService === void 0) {
|
|
4096
|
+
byService = /* @__PURE__ */ new Map();
|
|
4097
|
+
this._clientProxies.set(peer, byService);
|
|
3702
4098
|
}
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
methods.set(name, singleFn);
|
|
3708
|
-
} else {
|
|
3709
|
-
methods.set(name, (...args) => {
|
|
3710
|
-
const fn = byArgCount.get(args.length);
|
|
3711
|
-
if (!fn)
|
|
3712
|
-
throw new Error(
|
|
3713
|
-
`No overload of ${name} accepts ${args.length} arguments`
|
|
3714
|
-
);
|
|
3715
|
-
return fn(...args);
|
|
3716
|
-
});
|
|
3717
|
-
}
|
|
4099
|
+
let entry = byService.get(def.name);
|
|
4100
|
+
if (entry === void 0) {
|
|
4101
|
+
entry = this._createClientProxy();
|
|
4102
|
+
byService.set(def.name, entry);
|
|
3718
4103
|
}
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
});
|
|
4104
|
+
this._extendClientProxy(peer, entry, def);
|
|
4105
|
+
return entry.proxy;
|
|
3722
4106
|
}
|
|
3723
4107
|
close() {
|
|
3724
4108
|
for (const peer of this.peers.values()) peer.close();
|
|
3725
4109
|
this.peers.clear();
|
|
3726
4110
|
}
|
|
4111
|
+
/** Create the hub's system-call sender. Override in FusionHub to add
|
|
4112
|
+
* Fusion-specific system calls (e.g. $sys-c.Invalidate). */
|
|
4113
|
+
_createSystemCallSender() {
|
|
4114
|
+
return new RpcSystemCallSender();
|
|
4115
|
+
}
|
|
3727
4116
|
/** Build an RpcServiceDef from decorator metadata on a contract class. Override in FusionHub for compute support. */
|
|
3728
4117
|
_buildServiceDef(cls) {
|
|
3729
4118
|
const svcMeta = getServiceMeta(cls);
|
|
@@ -3781,12 +4170,10 @@ var RpcHub = class {
|
|
|
3781
4170
|
throw new Error(
|
|
3782
4171
|
`Expected stream reference, got: ${JSON.stringify(result)}`
|
|
3783
4172
|
);
|
|
3784
|
-
|
|
3785
|
-
peer.remoteObjects.register(stream);
|
|
3786
|
-
return stream;
|
|
4173
|
+
return new RpcStream(ref, peer);
|
|
3787
4174
|
};
|
|
3788
4175
|
}
|
|
3789
|
-
const callOptions = methodDef.callTypeId !== 0 || mode !== RpcRemoteExecutionMode.Default ? { callTypeId: methodDef.callTypeId, remoteExecutionMode: mode } : void 0;
|
|
4176
|
+
const callOptions = methodDef.callTypeId !== 0 || mode !== RpcRemoteExecutionMode.Default || methodDef.timeouts !== void 0 ? { callTypeId: methodDef.callTypeId, remoteExecutionMode: mode, timeouts: methodDef.timeouts } : void 0;
|
|
3790
4177
|
return async (...args) => {
|
|
3791
4178
|
if (mustCheckConnection && !peer.isConnected)
|
|
3792
4179
|
throw new Error("Outbound call failed: not connected and AwaitForConnection is not set.");
|
|
@@ -3796,7 +4183,58 @@ var RpcHub = class {
|
|
|
3796
4183
|
return resolveStreamRefs(result, peer);
|
|
3797
4184
|
};
|
|
3798
4185
|
}
|
|
4186
|
+
// Private methods
|
|
4187
|
+
_createClientProxy() {
|
|
4188
|
+
const methods = /* @__PURE__ */ new Map();
|
|
4189
|
+
const proxy = new Proxy({}, {
|
|
4190
|
+
get: (_target, prop) => typeof prop === "string" ? methods.get(prop) : void 0
|
|
4191
|
+
});
|
|
4192
|
+
return { proxy, methods, overloads: /* @__PURE__ */ new Map() };
|
|
4193
|
+
}
|
|
4194
|
+
_extendClientProxy(peer, entry, def) {
|
|
4195
|
+
for (const methodDef of def.methods.values()) {
|
|
4196
|
+
const existing = entry.overloads.get(methodDef.name)?.get(methodDef.argCount);
|
|
4197
|
+
if (existing !== void 0 && !isSameWireMethod(existing.def, methodDef))
|
|
4198
|
+
throw new Error(
|
|
4199
|
+
`RpcHub.addClient: conflicting definition of '${wireMethodName(methodDef)}' under service '${def.name}' \u2014 a different signature is already registered.`
|
|
4200
|
+
);
|
|
4201
|
+
}
|
|
4202
|
+
const changed = /* @__PURE__ */ new Set();
|
|
4203
|
+
for (const methodDef of def.methods.values()) {
|
|
4204
|
+
let byArgCount = entry.overloads.get(methodDef.name);
|
|
4205
|
+
if (byArgCount === void 0) {
|
|
4206
|
+
byArgCount = /* @__PURE__ */ new Map();
|
|
4207
|
+
entry.overloads.set(methodDef.name, byArgCount);
|
|
4208
|
+
}
|
|
4209
|
+
if (byArgCount.has(methodDef.argCount))
|
|
4210
|
+
continue;
|
|
4211
|
+
byArgCount.set(methodDef.argCount, {
|
|
4212
|
+
def: methodDef,
|
|
4213
|
+
fn: this._createClientMethod(peer, methodDef)
|
|
4214
|
+
});
|
|
4215
|
+
changed.add(methodDef.name);
|
|
4216
|
+
}
|
|
4217
|
+
for (const name of changed) {
|
|
4218
|
+
const byArgCount = entry.overloads.get(name);
|
|
4219
|
+
entry.methods.set(name, buildOverloadFn(name, byArgCount));
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
3799
4222
|
};
|
|
4223
|
+
function buildOverloadFn(name, byArgCount) {
|
|
4224
|
+
if (byArgCount.size === 1) {
|
|
4225
|
+
const [[, only]] = byArgCount;
|
|
4226
|
+
return only.fn;
|
|
4227
|
+
}
|
|
4228
|
+
return (...args) => {
|
|
4229
|
+
const overload = byArgCount.get(args.length);
|
|
4230
|
+
if (overload === void 0)
|
|
4231
|
+
throw new Error(`No overload of ${name} accepts ${args.length} arguments`);
|
|
4232
|
+
return overload.fn(...args);
|
|
4233
|
+
};
|
|
4234
|
+
}
|
|
4235
|
+
function isSameWireMethod(a, b) {
|
|
4236
|
+
return a.wireArgCount === b.wireArgCount && a.callTypeId === b.callTypeId && a.stream === b.stream && a.noWait === b.noWait && a.remoteExecutionMode === b.remoteExecutionMode;
|
|
4237
|
+
}
|
|
3800
4238
|
|
|
3801
4239
|
// src/rpc-peer-ref-builder.ts
|
|
3802
4240
|
var RpcPeerRefBuilder = class {
|
|
@@ -3863,7 +4301,7 @@ function createMethod(peer, methodDef) {
|
|
|
3863
4301
|
throw new Error("Outbound call failed: not connected and AwaitForConnection is not set.");
|
|
3864
4302
|
const callArgs = args.slice(0, methodDef.argCount);
|
|
3865
4303
|
const wireName = wireMethodName(methodDef);
|
|
3866
|
-
const options = methodDef.callTypeId !== 0 || mode !== RpcRemoteExecutionMode.Default ? { callTypeId: methodDef.callTypeId, remoteExecutionMode: mode } : void 0;
|
|
4304
|
+
const options = methodDef.callTypeId !== 0 || mode !== RpcRemoteExecutionMode.Default || methodDef.timeouts !== void 0 ? { callTypeId: methodDef.callTypeId, remoteExecutionMode: mode, timeouts: methodDef.timeouts } : void 0;
|
|
3867
4305
|
const outboundCall = peer.call(wireName, callArgs, options);
|
|
3868
4306
|
return outboundCall.result.promise;
|
|
3869
4307
|
};
|
|
@@ -4058,8 +4496,10 @@ export {
|
|
|
4058
4496
|
ENVELOPE_DELIMITER,
|
|
4059
4497
|
FRAME_DELIMITER,
|
|
4060
4498
|
IncreasingSeqCompressor,
|
|
4499
|
+
REMOTE_EXCEPTION_TYPE_REF,
|
|
4061
4500
|
RPC_CLOSE_CODE_UNSUPPORTED_FORMAT,
|
|
4062
4501
|
RpcCallStage,
|
|
4502
|
+
RpcCallTimeouts,
|
|
4063
4503
|
RpcCallTypeId,
|
|
4064
4504
|
RpcClientPeer,
|
|
4065
4505
|
RpcClientPeerReconnectDelayer,
|
|
@@ -4114,6 +4554,7 @@ export {
|
|
|
4114
4554
|
isConnected,
|
|
4115
4555
|
likelyConnected,
|
|
4116
4556
|
parseStreamRef,
|
|
4557
|
+
readPolymorphismMarker,
|
|
4117
4558
|
resolveStreamRefs,
|
|
4118
4559
|
rpcMethod,
|
|
4119
4560
|
rpcService,
|
|
@@ -4125,6 +4566,7 @@ export {
|
|
|
4125
4566
|
splitBinaryFrame,
|
|
4126
4567
|
splitCompactBinaryFrame,
|
|
4127
4568
|
splitFrame,
|
|
4569
|
+
toExceptionInfo,
|
|
4128
4570
|
wireMethodName,
|
|
4129
4571
|
xxh3_64,
|
|
4130
4572
|
xxh3_64str
|