@actuallab/rpc 13.0.25 → 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 +617 -177
- 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 +612 -176
- 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 */);
|
|
@@ -2453,57 +2726,85 @@ var RpcPeer = class {
|
|
|
2453
2726
|
_handleInbound(message, args) {
|
|
2454
2727
|
const method = message.Method ?? "";
|
|
2455
2728
|
const relatedId = message.RelatedId ?? 0;
|
|
2456
|
-
const call = new RpcInboundCall(relatedId, method, args);
|
|
2457
2729
|
const methodDef = this.hub.serviceHost.getMethodDef(method);
|
|
2458
2730
|
const isNoWait = methodDef?.noWait === true;
|
|
2459
|
-
if (!isNoWait) {
|
|
2460
|
-
this.inboundCalls.register(call);
|
|
2461
|
-
}
|
|
2462
2731
|
const serviceHost = this.hub.serviceHost;
|
|
2463
|
-
|
|
2732
|
+
const dispatch = async (call2) => {
|
|
2464
2733
|
try {
|
|
2465
2734
|
const context = this._connection !== void 0 ? {
|
|
2466
2735
|
__rpcDispatch: true,
|
|
2467
2736
|
callId: relatedId,
|
|
2468
|
-
|
|
2737
|
+
callType: message.CallType ?? 0,
|
|
2738
|
+
connection: this._connection,
|
|
2739
|
+
signal: call2?.signal,
|
|
2740
|
+
peer: this
|
|
2469
2741
|
} : void 0;
|
|
2470
|
-
const result = await serviceHost.dispatch(
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
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();
|
|
2490
2777
|
}
|
|
2491
2778
|
} catch (e) {
|
|
2492
|
-
if (
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
this.
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
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();
|
|
2499
2793
|
}
|
|
2500
2794
|
debugLog2?.log(`'${this.ref}': Inbound call failed: ${method}#${relatedId}`, e);
|
|
2501
|
-
} finally {
|
|
2502
|
-
if (!isNoWait) {
|
|
2503
|
-
this.inboundCalls.remove(relatedId);
|
|
2504
|
-
}
|
|
2505
2795
|
}
|
|
2506
|
-
}
|
|
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);
|
|
2507
2808
|
}
|
|
2508
2809
|
/** Start sending `$sys.KeepAlive` pings and arm the silence watchdog.
|
|
2509
2810
|
* Called when the peer transitions into `Connected` (after handshake).
|
|
@@ -2559,15 +2860,17 @@ var RpcConnectionState = /* @__PURE__ */ ((RpcConnectionState2) => {
|
|
|
2559
2860
|
})(RpcConnectionState || {});
|
|
2560
2861
|
var RpcClientPeer = class extends RpcPeer {
|
|
2561
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();
|
|
2562
2866
|
_tryIndex = 0;
|
|
2563
|
-
_handshakeIndex = 0;
|
|
2564
2867
|
/** Index from the MOST RECENTLY RECEIVED server handshake — sent back
|
|
2565
2868
|
* to the server in `$sys.Reconnect:3` so the server can verify we're
|
|
2566
2869
|
* talking about the current connection. Mirrors .NET's
|
|
2567
2870
|
* `connectionState.Handshake.Index` path in
|
|
2568
2871
|
* `RpcOutboundCallTracker.TryReconnect`. */
|
|
2569
2872
|
_remoteHandshakeIndex = 0;
|
|
2570
|
-
|
|
2873
|
+
_lastRemotePeerId;
|
|
2571
2874
|
_pendingHandshake;
|
|
2572
2875
|
_reconnectsAt = 0;
|
|
2573
2876
|
/** Base64url-encoded peer ID — matches .NET's RpcClientPeer.ClientId (Guid.ToBase64Url). */
|
|
@@ -2588,6 +2891,10 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2588
2891
|
* the connection attempt. Initialized from `hub.limits` at construction;
|
|
2589
2892
|
* set directly to override on a single peer. */
|
|
2590
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;
|
|
2591
2898
|
peerChanged = new EventHandlerSet3();
|
|
2592
2899
|
/** Fired when the server rejects the connection due to an unsupported serialization format (close code 4010). */
|
|
2593
2900
|
unsupportedFormat = new EventHandlerSet3();
|
|
@@ -2620,6 +2927,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2620
2927
|
super(hub, url);
|
|
2621
2928
|
this.connectTimeoutMs = hub.limits.connectTimeoutMs;
|
|
2622
2929
|
this.handshakeTimeoutMs = hub.limits.handshakeTimeoutMs;
|
|
2930
|
+
this.prematureDisconnectTimeoutMs = hub.limits.prematureDisconnectTimeoutMs;
|
|
2623
2931
|
this.clientId = guidToBase64Url(this.id);
|
|
2624
2932
|
const resolver = RpcSerializationFormatResolver.Default;
|
|
2625
2933
|
this.serializationFormat = resolver.get(parseFormatFromUrl(url) ?? resolver.defaultFormatKey);
|
|
@@ -2656,7 +2964,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2656
2964
|
try {
|
|
2657
2965
|
while (!this._disposed) {
|
|
2658
2966
|
this._setConnectionState(1 /* Connecting */);
|
|
2659
|
-
const retryDelay = this.hub.reconnectDelayer.getDelay(this._tryIndex);
|
|
2967
|
+
const retryDelay = this.hub.reconnectDelayer.getDelay(this._tryIndex, this._stopController.signal);
|
|
2660
2968
|
if (retryDelay.isLimitExceeded) {
|
|
2661
2969
|
warnLog6?.log(`'${this.ref}': Reconnect limit exceeded - shutting down`);
|
|
2662
2970
|
this._disposed = true;
|
|
@@ -2665,12 +2973,14 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2665
2973
|
this._setReconnectsAt(retryDelay.endsAt);
|
|
2666
2974
|
try {
|
|
2667
2975
|
await retryDelay.promise;
|
|
2976
|
+
} catch {
|
|
2668
2977
|
} finally {
|
|
2669
2978
|
this._setReconnectsAt(0);
|
|
2670
2979
|
}
|
|
2671
2980
|
if (this._disposed) break;
|
|
2672
2981
|
let lastCloseCode = 0;
|
|
2673
2982
|
let lastCloseReason = "";
|
|
2983
|
+
let connectedAt = 0;
|
|
2674
2984
|
try {
|
|
2675
2985
|
const connUrl = await this.connectionUrlResolver(this);
|
|
2676
2986
|
infoLog?.log(`'${this.ref}': Connecting to ${sanitizeUrl(connUrl)}`);
|
|
@@ -2717,7 +3027,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2717
3027
|
this.serializationFormat,
|
|
2718
3028
|
this.id,
|
|
2719
3029
|
this.hub.hubId,
|
|
2720
|
-
++this.
|
|
3030
|
+
++this._ownHandshakeIndex
|
|
2721
3031
|
);
|
|
2722
3032
|
} catch (e) {
|
|
2723
3033
|
warnLog6?.log(`'${this.ref}': Failed to send handshake`, e);
|
|
@@ -2741,20 +3051,10 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2741
3051
|
this._pendingHandshake = void 0;
|
|
2742
3052
|
}
|
|
2743
3053
|
this._remoteHandshakeIndex = remoteHandshake.Index ?? 0;
|
|
2744
|
-
const
|
|
2745
|
-
let isPeerChanged = false;
|
|
2746
|
-
if (remoteHubId !== void 0) {
|
|
2747
|
-
isPeerChanged = this._lastRemoteHubId !== void 0 && this._lastRemoteHubId !== remoteHubId;
|
|
2748
|
-
if (isPeerChanged) {
|
|
2749
|
-
this.inboundCalls.clear();
|
|
2750
|
-
infoLog?.log(`'${this.ref}': Peer changed`);
|
|
2751
|
-
this.peerChanged.trigger();
|
|
2752
|
-
}
|
|
2753
|
-
this._lastRemoteHubId = remoteHubId;
|
|
2754
|
-
}
|
|
3054
|
+
const isPeerChanged = this._detectPeerChange(remoteHandshake);
|
|
2755
3055
|
infoLog?.log(`'${this.ref}': Handshake succeeded, peerChanged=${isPeerChanged}`);
|
|
2756
3056
|
this._setConnectionState(3 /* Connected */);
|
|
2757
|
-
|
|
3057
|
+
connectedAt = Date.now();
|
|
2758
3058
|
await this._reconnect(isPeerChanged, closedController.signal);
|
|
2759
3059
|
await whenClosed;
|
|
2760
3060
|
} catch (e) {
|
|
@@ -2768,6 +3068,8 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2768
3068
|
this._disposed = true;
|
|
2769
3069
|
break;
|
|
2770
3070
|
}
|
|
3071
|
+
if (connectedAt !== 0 && Date.now() - connectedAt >= this.prematureDisconnectTimeoutMs)
|
|
3072
|
+
this._tryIndex = 0;
|
|
2771
3073
|
this._tryIndex++;
|
|
2772
3074
|
}
|
|
2773
3075
|
} finally {
|
|
@@ -2823,6 +3125,8 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2823
3125
|
continue;
|
|
2824
3126
|
}
|
|
2825
3127
|
eligible.push(call);
|
|
3128
|
+
if (call.sentAt !== 0)
|
|
3129
|
+
call.sentAt = Date.now();
|
|
2826
3130
|
}
|
|
2827
3131
|
let unknownIds = null;
|
|
2828
3132
|
if (!isPeerChanged && eligible.length > 0) {
|
|
@@ -2839,6 +3143,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2839
3143
|
if (unknownIds !== null && !unknownIds.has(call.callId)) {
|
|
2840
3144
|
continue;
|
|
2841
3145
|
}
|
|
3146
|
+
call.sentAt = Date.now();
|
|
2842
3147
|
this._sendWireData(call.serializedWireData);
|
|
2843
3148
|
resentSinceYield++;
|
|
2844
3149
|
if (resentSinceYield >= YIELD_BATCH) {
|
|
@@ -2904,30 +3209,80 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2904
3209
|
return new Set(eligible.map((c) => c.callId));
|
|
2905
3210
|
}
|
|
2906
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
|
+
}
|
|
2907
3231
|
_onHandshakeReceived(handshake) {
|
|
2908
3232
|
this._pendingHandshake?.resolve(handshake);
|
|
2909
3233
|
}
|
|
2910
3234
|
close() {
|
|
2911
3235
|
this._disposed = true;
|
|
3236
|
+
this._stopController.abort();
|
|
2912
3237
|
super.close();
|
|
2913
3238
|
}
|
|
2914
3239
|
};
|
|
2915
3240
|
var RpcServerPeer = class extends RpcPeer {
|
|
2916
|
-
|
|
2917
|
-
|
|
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;
|
|
2918
3247
|
constructor(hub, ref) {
|
|
2919
3248
|
super(hub, ref);
|
|
3249
|
+
this.serverPeerCloseTimeoutMs = hub.limits.serverPeerCloseTimeoutMs;
|
|
2920
3250
|
}
|
|
2921
3251
|
/** Accept an incoming connection — sets up message handling and handshake response. */
|
|
2922
3252
|
accept(conn) {
|
|
3253
|
+
this._clearCloseTimer();
|
|
2923
3254
|
this._setConnectionState(1 /* Connecting */);
|
|
2924
3255
|
this.setupConnection(conn);
|
|
2925
3256
|
this._setConnectionState(2 /* Handshaking */);
|
|
2926
3257
|
conn.closed.add(() => {
|
|
2927
3258
|
if (this._connection !== conn && this._connection !== void 0) return;
|
|
2928
3259
|
this.sharedObjects.disconnectAll();
|
|
3260
|
+
this._armCloseTimer();
|
|
2929
3261
|
});
|
|
2930
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
|
+
}
|
|
2931
3286
|
_onHandshakeReceived(_handshake) {
|
|
2932
3287
|
if (this._connection !== void 0) {
|
|
2933
3288
|
this.hub.systemCallSender.handshake(
|
|
@@ -2935,7 +3290,7 @@ var RpcServerPeer = class extends RpcPeer {
|
|
|
2935
3290
|
this.serializationFormat,
|
|
2936
3291
|
this.id,
|
|
2937
3292
|
this.hub.hubId,
|
|
2938
|
-
|
|
3293
|
+
++this._ownHandshakeIndex
|
|
2939
3294
|
);
|
|
2940
3295
|
this._setConnectionState(3 /* Connected */);
|
|
2941
3296
|
}
|
|
@@ -2974,6 +3329,12 @@ var RpcLimits = class _RpcLimits {
|
|
|
2974
3329
|
/** Process-wide default. Hubs read from this when they're constructed
|
|
2975
3330
|
* unless caller assigns a custom instance to `hub.limits`. */
|
|
2976
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 });
|
|
2977
3338
|
/** Max time to wait for the WebSocket to enter the OPEN state. On a
|
|
2978
3339
|
* hung connect (mobile after network change, half-open after sleep)
|
|
2979
3340
|
* the browser can take ~2 min to emit `onerror`/`onclose`; without
|
|
@@ -2981,6 +3342,22 @@ var RpcLimits = class _RpcLimits {
|
|
|
2981
3342
|
connectTimeoutMs = 1e4;
|
|
2982
3343
|
/** Max time to wait for the server's handshake response after WS opens. */
|
|
2983
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;
|
|
2984
3361
|
/** Outbound `$sys.KeepAlive` send period. */
|
|
2985
3362
|
keepAlivePeriodMs = 1e4;
|
|
2986
3363
|
/** Reaper threshold — connection is force-closed (and the reconnect
|
|
@@ -2990,6 +3367,13 @@ var RpcLimits = class _RpcLimits {
|
|
|
2990
3367
|
* `keepAlivePeriodMs + stall_duration`, so with these defaults the
|
|
2991
3368
|
* tolerated stall is ~15 s. */
|
|
2992
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;
|
|
2993
3377
|
constructor(overrides) {
|
|
2994
3378
|
if (overrides) Object.assign(this, overrides);
|
|
2995
3379
|
}
|
|
@@ -3000,7 +3384,7 @@ import { RetryDelaySeq, RetryDelayer } from "@actuallab/core";
|
|
|
3000
3384
|
var RpcClientPeerReconnectDelayer = class extends RetryDelayer {
|
|
3001
3385
|
constructor() {
|
|
3002
3386
|
super();
|
|
3003
|
-
this.delays = RetryDelaySeq.
|
|
3387
|
+
this.delays = RetryDelaySeq.exp(100, 1e4);
|
|
3004
3388
|
}
|
|
3005
3389
|
};
|
|
3006
3390
|
|
|
@@ -3008,13 +3392,14 @@ var RpcClientPeerReconnectDelayer = class extends RetryDelayer {
|
|
|
3008
3392
|
var { warnLog: warnLog7 } = getLogs("RpcServiceHost");
|
|
3009
3393
|
var RpcServiceHost = class {
|
|
3010
3394
|
_methods = /* @__PURE__ */ new Map();
|
|
3011
|
-
register(def, impl) {
|
|
3395
|
+
register(def, impl, receiver = impl) {
|
|
3012
3396
|
for (const methodDef of def.methods.values()) {
|
|
3013
3397
|
const fn = impl[methodDef.name];
|
|
3014
3398
|
if (!fn) continue;
|
|
3015
3399
|
this._methods.set(wireMethodName(methodDef), {
|
|
3016
3400
|
def: methodDef,
|
|
3017
|
-
fn
|
|
3401
|
+
fn,
|
|
3402
|
+
receiver
|
|
3018
3403
|
});
|
|
3019
3404
|
}
|
|
3020
3405
|
}
|
|
@@ -3025,9 +3410,9 @@ var RpcServiceHost = class {
|
|
|
3025
3410
|
throw new Error(`Method not found: ${wireMethod}`);
|
|
3026
3411
|
}
|
|
3027
3412
|
if (context !== void 0 && entry.def.callTypeId !== 0) {
|
|
3028
|
-
return await entry.fn(...args, context);
|
|
3413
|
+
return await entry.fn.call(entry.receiver, ...args, context);
|
|
3029
3414
|
}
|
|
3030
|
-
return await entry.fn(...args);
|
|
3415
|
+
return await entry.fn.call(entry.receiver, ...args);
|
|
3031
3416
|
}
|
|
3032
3417
|
getMethodDef(wireMethod) {
|
|
3033
3418
|
return this._methods.get(wireMethod)?.def;
|
|
@@ -3035,6 +3420,7 @@ var RpcServiceHost = class {
|
|
|
3035
3420
|
};
|
|
3036
3421
|
|
|
3037
3422
|
// src/rpc-decorators.ts
|
|
3423
|
+
import { ownMetadata, resolveArgCount } from "@actuallab/core";
|
|
3038
3424
|
var METHODS_META = /* @__PURE__ */ Symbol.for("actuallab.methods");
|
|
3039
3425
|
var SERVICE_META = /* @__PURE__ */ Symbol.for("actuallab.service");
|
|
3040
3426
|
function getMethodsMeta(cls) {
|
|
@@ -3045,17 +3431,17 @@ function getServiceMeta(cls) {
|
|
|
3045
3431
|
}
|
|
3046
3432
|
function rpcService(serviceName) {
|
|
3047
3433
|
return function(_target, context) {
|
|
3048
|
-
const meta = context.metadata
|
|
3434
|
+
const meta = ownMetadata(context.metadata, SERVICE_META);
|
|
3049
3435
|
meta.name = serviceName;
|
|
3050
3436
|
};
|
|
3051
3437
|
}
|
|
3052
3438
|
function rpcMethod(options) {
|
|
3053
3439
|
return function(target, context) {
|
|
3054
3440
|
const methodName = String(context.name);
|
|
3055
|
-
const methods = context.metadata
|
|
3441
|
+
const methods = ownMetadata(context.metadata, METHODS_META);
|
|
3056
3442
|
methods[methodName] = {
|
|
3057
3443
|
...methods[methodName],
|
|
3058
|
-
argCount: target
|
|
3444
|
+
argCount: resolveArgCount(target, options?.argCount, methodName),
|
|
3059
3445
|
returns: options?.returns,
|
|
3060
3446
|
remoteExecutionMode: options?.remoteExecutionMode
|
|
3061
3447
|
};
|
|
@@ -3601,7 +3987,7 @@ var RpcHub = class {
|
|
|
3601
3987
|
hubId;
|
|
3602
3988
|
peers = /* @__PURE__ */ new Map();
|
|
3603
3989
|
serviceHost;
|
|
3604
|
-
systemCallSender
|
|
3990
|
+
systemCallSender;
|
|
3605
3991
|
systemCallHandler = new RpcSystemCallHandler();
|
|
3606
3992
|
/** Method registry for compact format hash ↔ name resolution.
|
|
3607
3993
|
* Created lazily, populated by addService/addClient. */
|
|
@@ -3609,6 +3995,14 @@ var RpcHub = class {
|
|
|
3609
3995
|
/** Shared reconnect delayer used by every client peer in this hub. Swap in
|
|
3610
3996
|
* a custom subclass (e.g. app-level signal-gated) before peers start. */
|
|
3611
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();
|
|
3612
4006
|
/** Connection-lifecycle timing limits. Peers constructed against this hub
|
|
3613
4007
|
* read their initial `*Ms` field values from this instance; later
|
|
3614
4008
|
* mutations affect only peers built afterward (existing peers can be
|
|
@@ -3626,6 +4020,7 @@ var RpcHub = class {
|
|
|
3626
4020
|
constructor(hubId) {
|
|
3627
4021
|
this.hubId = hubId ?? crypto.randomUUID();
|
|
3628
4022
|
this.serviceHost = new RpcServiceHost();
|
|
4023
|
+
this.systemCallSender = this._createSystemCallSender();
|
|
3629
4024
|
this.systemCallSender.registry = this.registry;
|
|
3630
4025
|
for (const methodName of Object.values(RpcSystemCalls)) {
|
|
3631
4026
|
this.registry.register(methodName);
|
|
@@ -3686,50 +4081,38 @@ var RpcHub = class {
|
|
|
3686
4081
|
if (!fn) continue;
|
|
3687
4082
|
wrappedImpl[methodDef.name] = methodDef.callTypeId !== 0 ? this._wrapServerMethod(methodDef, fn, impl) : fn;
|
|
3688
4083
|
}
|
|
3689
|
-
this.serviceHost.register(def, wrappedImpl);
|
|
4084
|
+
this.serviceHost.register(def, wrappedImpl, impl);
|
|
3690
4085
|
this.registry.registerService(def.name, def.methods);
|
|
3691
4086
|
}
|
|
3692
|
-
/** 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). */
|
|
3693
4090
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T is used for caller-specified proxy type
|
|
3694
4091
|
addClient(peer, defOrContract) {
|
|
3695
4092
|
const def = this._resolveServiceDef(defOrContract);
|
|
3696
4093
|
this.registry.registerService(def.name, def.methods);
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
byArgCount = /* @__PURE__ */ new Map();
|
|
3702
|
-
overloads.set(methodDef.name, byArgCount);
|
|
3703
|
-
}
|
|
3704
|
-
byArgCount.set(
|
|
3705
|
-
methodDef.argCount,
|
|
3706
|
-
this._createClientMethod(peer, methodDef)
|
|
3707
|
-
);
|
|
4094
|
+
let byService = this._clientProxies.get(peer);
|
|
4095
|
+
if (byService === void 0) {
|
|
4096
|
+
byService = /* @__PURE__ */ new Map();
|
|
4097
|
+
this._clientProxies.set(peer, byService);
|
|
3708
4098
|
}
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
methods.set(name, singleFn);
|
|
3714
|
-
} else {
|
|
3715
|
-
methods.set(name, (...args) => {
|
|
3716
|
-
const fn = byArgCount.get(args.length);
|
|
3717
|
-
if (!fn)
|
|
3718
|
-
throw new Error(
|
|
3719
|
-
`No overload of ${name} accepts ${args.length} arguments`
|
|
3720
|
-
);
|
|
3721
|
-
return fn(...args);
|
|
3722
|
-
});
|
|
3723
|
-
}
|
|
4099
|
+
let entry = byService.get(def.name);
|
|
4100
|
+
if (entry === void 0) {
|
|
4101
|
+
entry = this._createClientProxy();
|
|
4102
|
+
byService.set(def.name, entry);
|
|
3724
4103
|
}
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
});
|
|
4104
|
+
this._extendClientProxy(peer, entry, def);
|
|
4105
|
+
return entry.proxy;
|
|
3728
4106
|
}
|
|
3729
4107
|
close() {
|
|
3730
4108
|
for (const peer of this.peers.values()) peer.close();
|
|
3731
4109
|
this.peers.clear();
|
|
3732
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
|
+
}
|
|
3733
4116
|
/** Build an RpcServiceDef from decorator metadata on a contract class. Override in FusionHub for compute support. */
|
|
3734
4117
|
_buildServiceDef(cls) {
|
|
3735
4118
|
const svcMeta = getServiceMeta(cls);
|
|
@@ -3787,12 +4170,10 @@ var RpcHub = class {
|
|
|
3787
4170
|
throw new Error(
|
|
3788
4171
|
`Expected stream reference, got: ${JSON.stringify(result)}`
|
|
3789
4172
|
);
|
|
3790
|
-
|
|
3791
|
-
peer.remoteObjects.register(stream);
|
|
3792
|
-
return stream;
|
|
4173
|
+
return new RpcStream(ref, peer);
|
|
3793
4174
|
};
|
|
3794
4175
|
}
|
|
3795
|
-
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;
|
|
3796
4177
|
return async (...args) => {
|
|
3797
4178
|
if (mustCheckConnection && !peer.isConnected)
|
|
3798
4179
|
throw new Error("Outbound call failed: not connected and AwaitForConnection is not set.");
|
|
@@ -3802,7 +4183,58 @@ var RpcHub = class {
|
|
|
3802
4183
|
return resolveStreamRefs(result, peer);
|
|
3803
4184
|
};
|
|
3804
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
|
+
}
|
|
3805
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
|
+
}
|
|
3806
4238
|
|
|
3807
4239
|
// src/rpc-peer-ref-builder.ts
|
|
3808
4240
|
var RpcPeerRefBuilder = class {
|
|
@@ -3869,7 +4301,7 @@ function createMethod(peer, methodDef) {
|
|
|
3869
4301
|
throw new Error("Outbound call failed: not connected and AwaitForConnection is not set.");
|
|
3870
4302
|
const callArgs = args.slice(0, methodDef.argCount);
|
|
3871
4303
|
const wireName = wireMethodName(methodDef);
|
|
3872
|
-
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;
|
|
3873
4305
|
const outboundCall = peer.call(wireName, callArgs, options);
|
|
3874
4306
|
return outboundCall.result.promise;
|
|
3875
4307
|
};
|
|
@@ -4064,8 +4496,10 @@ export {
|
|
|
4064
4496
|
ENVELOPE_DELIMITER,
|
|
4065
4497
|
FRAME_DELIMITER,
|
|
4066
4498
|
IncreasingSeqCompressor,
|
|
4499
|
+
REMOTE_EXCEPTION_TYPE_REF,
|
|
4067
4500
|
RPC_CLOSE_CODE_UNSUPPORTED_FORMAT,
|
|
4068
4501
|
RpcCallStage,
|
|
4502
|
+
RpcCallTimeouts,
|
|
4069
4503
|
RpcCallTypeId,
|
|
4070
4504
|
RpcClientPeer,
|
|
4071
4505
|
RpcClientPeerReconnectDelayer,
|
|
@@ -4120,6 +4554,7 @@ export {
|
|
|
4120
4554
|
isConnected,
|
|
4121
4555
|
likelyConnected,
|
|
4122
4556
|
parseStreamRef,
|
|
4557
|
+
readPolymorphismMarker,
|
|
4123
4558
|
resolveStreamRefs,
|
|
4124
4559
|
rpcMethod,
|
|
4125
4560
|
rpcService,
|
|
@@ -4131,6 +4566,7 @@ export {
|
|
|
4131
4566
|
splitBinaryFrame,
|
|
4132
4567
|
splitCompactBinaryFrame,
|
|
4133
4568
|
splitFrame,
|
|
4569
|
+
toExceptionInfo,
|
|
4134
4570
|
wireMethodName,
|
|
4135
4571
|
xxh3_64,
|
|
4136
4572
|
xxh3_64str
|