@actuallab/rpc 14.0.17 → 14.2.23
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 +1421 -1255
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -18
- package/dist/index.d.ts +73 -18
- package/dist/index.js +1416 -1253
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -56,9 +56,9 @@ __export(index_exports, {
|
|
|
56
56
|
RpcOutboundCall: () => RpcOutboundCall,
|
|
57
57
|
RpcOutboundCallTracker: () => RpcOutboundCallTracker,
|
|
58
58
|
RpcPeer: () => RpcPeer,
|
|
59
|
-
RpcPeerRefBuilder: () => RpcPeerRefBuilder,
|
|
60
59
|
RpcPeerStateKind: () => RpcPeerStateKind,
|
|
61
60
|
RpcPeerStateMonitor: () => RpcPeerStateMonitor,
|
|
61
|
+
RpcRefBuilder: () => RpcRefBuilder,
|
|
62
62
|
RpcRemoteExecutionMode: () => RpcRemoteExecutionMode,
|
|
63
63
|
RpcRemoteObjectTracker: () => RpcRemoteObjectTracker,
|
|
64
64
|
RpcSerializationFormat: () => RpcSerializationFormat,
|
|
@@ -75,6 +75,7 @@ __export(index_exports, {
|
|
|
75
75
|
RpcWebSocketConnection: () => RpcWebSocketConnection,
|
|
76
76
|
WebSocketState: () => WebSocketState,
|
|
77
77
|
computeMethodHash: () => computeMethodHash,
|
|
78
|
+
computeReconnectProof: () => computeReconnectProof,
|
|
78
79
|
createBinaryEncoder: () => createBinaryEncoder,
|
|
79
80
|
createMessageChannelPair: () => createMessageChannelPair,
|
|
80
81
|
createRpcClient: () => createRpcClient,
|
|
@@ -85,15 +86,16 @@ __export(index_exports, {
|
|
|
85
86
|
deserializeBinaryMessage: () => deserializeBinaryMessage,
|
|
86
87
|
deserializeCompactBinaryMessage: () => deserializeCompactBinaryMessage,
|
|
87
88
|
deserializeMessage: () => deserializeMessage,
|
|
89
|
+
genericErrorFilter: () => genericErrorFilter,
|
|
88
90
|
getLogs: () => getLogs,
|
|
89
91
|
getMethodsMeta: () => getMethodsMeta,
|
|
90
92
|
getServiceMeta: () => getServiceMeta,
|
|
91
93
|
getStateDescription: () => getStateDescription,
|
|
92
94
|
isConnected: () => isConnected,
|
|
95
|
+
isReconnectProofSupported: () => isReconnectProofSupported,
|
|
93
96
|
likelyConnected: () => likelyConnected,
|
|
94
97
|
parseStreamRef: () => parseStreamRef,
|
|
95
98
|
readPolymorphismMarker: () => readPolymorphismMarker,
|
|
96
|
-
resolveStreamRefs: () => resolveStreamRefs,
|
|
97
99
|
rpcMethod: () => rpcMethod,
|
|
98
100
|
rpcService: () => rpcService,
|
|
99
101
|
serializeBinaryFrame: () => serializeBinaryFrame,
|
|
@@ -105,6 +107,7 @@ __export(index_exports, {
|
|
|
105
107
|
splitCompactBinaryFrame: () => splitCompactBinaryFrame,
|
|
106
108
|
splitFrame: () => splitFrame,
|
|
107
109
|
toExceptionInfo: () => toExceptionInfo,
|
|
110
|
+
toRpcStream: () => toRpcStream,
|
|
108
111
|
wireMethodName: () => wireMethodName,
|
|
109
112
|
xxh3_64: () => xxh3_64,
|
|
110
113
|
xxh3_64str: () => xxh3_64str
|
|
@@ -275,12 +278,24 @@ function readVarUint(data, offset) {
|
|
|
275
278
|
} while (shift < 35);
|
|
276
279
|
return { value: value >>> 0, bytesRead };
|
|
277
280
|
}
|
|
281
|
+
var MIN_BINARY_ENVELOPE_SIZE = 7;
|
|
282
|
+
var MIN_COMPACT_BINARY_ENVELOPE_SIZE = 10;
|
|
283
|
+
function requireInBounds(data, pos, length, what) {
|
|
284
|
+
const end = pos + length;
|
|
285
|
+
if (!Number.isSafeInteger(length) || length < 0 || end > data.length)
|
|
286
|
+
throw new Error(`Invalid binary RPC message: ${what} length ${length} at ${pos} is out of bounds.`);
|
|
287
|
+
return end;
|
|
288
|
+
}
|
|
278
289
|
function skipHeaders(data, pos, headerCount) {
|
|
279
290
|
for (let h = 0; h < headerCount; h++) {
|
|
291
|
+
if (pos >= data.length)
|
|
292
|
+
throw new Error("Invalid binary RPC message: truncated header.");
|
|
280
293
|
const keyLen = data[pos++];
|
|
281
|
-
pos
|
|
294
|
+
pos = requireInBounds(data, pos, keyLen, "header key");
|
|
282
295
|
const valLen = readVarUint(data, pos);
|
|
283
|
-
|
|
296
|
+
if (valLen.bytesRead === 0)
|
|
297
|
+
throw new Error("Invalid binary RPC message: truncated header value length.");
|
|
298
|
+
pos = requireInBounds(data, pos + valLen.bytesRead, valLen.value, "header value");
|
|
284
299
|
}
|
|
285
300
|
return pos;
|
|
286
301
|
}
|
|
@@ -343,6 +358,8 @@ function serializeBinaryMessage(message, args, encoder = defaultBinaryEncoder) {
|
|
|
343
358
|
}
|
|
344
359
|
function deserializeBinaryMessage(data, offset, decoder = defaultBinaryDecoder) {
|
|
345
360
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
361
|
+
if (offset + MIN_BINARY_ENVELOPE_SIZE > data.length)
|
|
362
|
+
throw new Error(`Invalid binary RPC message: truncated envelope at ${offset}.`);
|
|
346
363
|
let pos = offset;
|
|
347
364
|
const byte0 = data[pos++];
|
|
348
365
|
const callTypeId = byte0 >> 5 & 7;
|
|
@@ -351,13 +368,15 @@ function deserializeBinaryMessage(data, offset, decoder = defaultBinaryDecoder)
|
|
|
351
368
|
pos += relId.bytesRead;
|
|
352
369
|
const methodLen = readVarUint(data, pos);
|
|
353
370
|
pos += methodLen.bytesRead;
|
|
354
|
-
const
|
|
355
|
-
const method = textDecoder.decode(
|
|
356
|
-
pos
|
|
357
|
-
|
|
371
|
+
const methodEnd = requireInBounds(data, pos, methodLen.value, "method");
|
|
372
|
+
const method = textDecoder.decode(data.subarray(pos, methodEnd));
|
|
373
|
+
pos = methodEnd;
|
|
374
|
+
if (pos + 4 > data.length)
|
|
375
|
+
throw new Error(`Invalid binary RPC message: truncated argument data length at ${pos}.`);
|
|
376
|
+
const argDataLen = view.getUint32(pos, true);
|
|
358
377
|
pos += 4;
|
|
359
378
|
const args = [];
|
|
360
|
-
const argEnd = pos
|
|
379
|
+
const argEnd = requireInBounds(data, pos, argDataLen, "argument data");
|
|
361
380
|
if (argDataLen > 0) {
|
|
362
381
|
const argSlice = data.subarray(pos, argEnd);
|
|
363
382
|
for (const decoded of decoder.decodeMulti(argSlice)) {
|
|
@@ -378,12 +397,16 @@ function splitBinaryFrame(frame, decoder = defaultBinaryDecoder) {
|
|
|
378
397
|
const results = [];
|
|
379
398
|
let offset = 0;
|
|
380
399
|
while (offset < frame.length) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
offset,
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
400
|
+
let parsed;
|
|
401
|
+
try {
|
|
402
|
+
parsed = deserializeBinaryMessage(frame, offset, decoder);
|
|
403
|
+
} catch (e) {
|
|
404
|
+
warnLog?.log(`Failed to parse a binary message at offset ${offset}:`, e);
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
const { message, args, bytesRead } = parsed;
|
|
408
|
+
if (!Number.isFinite(bytesRead) || bytesRead < MIN_BINARY_ENVELOPE_SIZE)
|
|
409
|
+
break;
|
|
387
410
|
results.push({ message, args });
|
|
388
411
|
offset += bytesRead;
|
|
389
412
|
}
|
|
@@ -433,19 +456,23 @@ function serializeCompactBinaryMessage(message, args, registry, encoder = defaul
|
|
|
433
456
|
}
|
|
434
457
|
function deserializeCompactBinaryMessage(data, offset, registry, decoder = defaultBinaryDecoder) {
|
|
435
458
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
459
|
+
if (offset + MIN_COMPACT_BINARY_ENVELOPE_SIZE > data.length)
|
|
460
|
+
throw new Error(`Invalid compact binary RPC message: truncated envelope at ${offset}.`);
|
|
436
461
|
let pos = offset;
|
|
437
462
|
const byte0 = data[pos++];
|
|
438
463
|
const callTypeId = byte0 >> 5 & 7;
|
|
439
464
|
const headerCount = byte0 & 31;
|
|
440
465
|
const relId = readVarUint(data, pos);
|
|
441
466
|
pos += relId.bytesRead;
|
|
467
|
+
if (pos + 8 > data.length)
|
|
468
|
+
throw new Error(`Invalid compact binary RPC message: truncated envelope at ${offset}.`);
|
|
442
469
|
const methodHash = view.getUint32(pos, true);
|
|
443
470
|
pos += 4;
|
|
444
471
|
const method = registry?.getName(methodHash | 0) ?? `<hash:0x${methodHash.toString(16).padStart(8, "0")}>`;
|
|
445
|
-
const argDataLen = view.
|
|
472
|
+
const argDataLen = view.getUint32(pos, true);
|
|
446
473
|
pos += 4;
|
|
447
474
|
const args = [];
|
|
448
|
-
const argEnd = pos
|
|
475
|
+
const argEnd = requireInBounds(data, pos, argDataLen, "argument data");
|
|
449
476
|
if (argDataLen > 0) {
|
|
450
477
|
const argSlice = data.subarray(pos, argEnd);
|
|
451
478
|
for (const decoded of decoder.decodeMulti(argSlice)) {
|
|
@@ -466,13 +493,16 @@ function splitCompactBinaryFrame(frame, registry, decoder = defaultBinaryDecoder
|
|
|
466
493
|
const results = [];
|
|
467
494
|
let offset = 0;
|
|
468
495
|
while (offset < frame.length) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
offset,
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
496
|
+
let parsed;
|
|
497
|
+
try {
|
|
498
|
+
parsed = deserializeCompactBinaryMessage(frame, offset, registry, decoder);
|
|
499
|
+
} catch (e) {
|
|
500
|
+
warnLog?.log(`Failed to parse a compact binary message at offset ${offset}:`, e);
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
const { message, args, bytesRead } = parsed;
|
|
504
|
+
if (!Number.isFinite(bytesRead) || bytesRead < MIN_COMPACT_BINARY_ENVELOPE_SIZE)
|
|
505
|
+
break;
|
|
476
506
|
results.push({ message, args });
|
|
477
507
|
offset += bytesRead;
|
|
478
508
|
}
|
|
@@ -534,46 +564,38 @@ var RpcWebSocketConnection = class {
|
|
|
534
564
|
ws.onmessage = (ev) => {
|
|
535
565
|
if (ev.data instanceof Uint8Array || ev.data instanceof ArrayBuffer) {
|
|
536
566
|
const frame = ev.data instanceof Uint8Array ? ev.data : new Uint8Array(ev.data);
|
|
567
|
+
let messages;
|
|
537
568
|
try {
|
|
538
|
-
|
|
539
|
-
for (const { message, args } of messages) {
|
|
540
|
-
this.messageReceived.trigger({
|
|
541
|
-
kind: "binary",
|
|
542
|
-
message,
|
|
543
|
-
args
|
|
544
|
-
});
|
|
545
|
-
}
|
|
569
|
+
messages = this._splitBinary(frame);
|
|
546
570
|
} catch (e) {
|
|
547
571
|
warnLog2?.log("Failed to split binary frame:", e);
|
|
548
572
|
this.error.trigger(e);
|
|
573
|
+
return;
|
|
549
574
|
}
|
|
575
|
+
for (const { message, args } of messages)
|
|
576
|
+
this._dispatchBinary(message, args);
|
|
550
577
|
} else if (typeof Blob !== "undefined" && ev.data instanceof Blob && this.binaryMode) {
|
|
551
578
|
void ev.data.arrayBuffer().then((ab) => {
|
|
552
579
|
const frame = new Uint8Array(ab);
|
|
580
|
+
let messages;
|
|
553
581
|
try {
|
|
554
|
-
|
|
555
|
-
for (const { message, args } of messages) {
|
|
556
|
-
this.messageReceived.trigger({
|
|
557
|
-
kind: "binary",
|
|
558
|
-
message,
|
|
559
|
-
args
|
|
560
|
-
});
|
|
561
|
-
}
|
|
582
|
+
messages = this._splitBinary(frame);
|
|
562
583
|
} catch (e) {
|
|
563
584
|
warnLog2?.log("Failed to split binary blob frame:", e);
|
|
564
585
|
this.error.trigger(e);
|
|
586
|
+
return;
|
|
565
587
|
}
|
|
588
|
+
for (const { message, args } of messages)
|
|
589
|
+
this._dispatchBinary(message, args);
|
|
566
590
|
});
|
|
567
591
|
} else {
|
|
568
592
|
const data = typeof ev.data === "string" ? ev.data : String(ev.data);
|
|
569
|
-
const
|
|
570
|
-
for (const msg of messages) {
|
|
593
|
+
for (const msg of splitFrame(data))
|
|
571
594
|
if (msg.length > 0)
|
|
572
|
-
this.messageReceived.
|
|
573
|
-
kind: "text",
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
595
|
+
this.messageReceived.triggerSafe(
|
|
596
|
+
{ kind: "text", raw: msg },
|
|
597
|
+
(e) => warnLog2?.log("Failed to handle a text message:", e)
|
|
598
|
+
);
|
|
577
599
|
}
|
|
578
600
|
};
|
|
579
601
|
ws.onclose = (ev) => {
|
|
@@ -612,6 +634,15 @@ var RpcWebSocketConnection = class {
|
|
|
612
634
|
}
|
|
613
635
|
return splitBinaryFrame(frame, this._decoder);
|
|
614
636
|
}
|
|
637
|
+
// Containment matters here: .NET batches many RPC messages per WebSocket frame,
|
|
638
|
+
// so letting one bad message escape would drop every other message in the frame -
|
|
639
|
+
// including $sys.Ok replies, whose per-call timeouts are unbounded by default.
|
|
640
|
+
_dispatchBinary(message, args) {
|
|
641
|
+
this.messageReceived.triggerSafe(
|
|
642
|
+
{ kind: "binary", message, args },
|
|
643
|
+
(e) => warnLog2?.log("Failed to handle a binary message:", e)
|
|
644
|
+
);
|
|
645
|
+
}
|
|
615
646
|
close(code, reason) {
|
|
616
647
|
this._ws.close(code, reason);
|
|
617
648
|
}
|
|
@@ -637,6 +668,7 @@ var RpcWebSocketConnection = class {
|
|
|
637
668
|
|
|
638
669
|
// src/rpc-message-channel-connection.ts
|
|
639
670
|
var import_core3 = require("@actuallab/core");
|
|
671
|
+
var { warnLog: warnLog3 } = getLogs("RpcMessageChannelConnection");
|
|
640
672
|
var RpcMessageChannelConnection = class {
|
|
641
673
|
_port;
|
|
642
674
|
_open = true;
|
|
@@ -651,7 +683,10 @@ var RpcMessageChannelConnection = class {
|
|
|
651
683
|
const data = typeof ev.data === "string" ? ev.data : String(ev.data);
|
|
652
684
|
for (const msg of splitFrame(data))
|
|
653
685
|
if (msg.length > 0)
|
|
654
|
-
this.messageReceived.
|
|
686
|
+
this.messageReceived.triggerSafe(
|
|
687
|
+
{ kind: "text", raw: msg },
|
|
688
|
+
(e) => warnLog3?.log("Failed to handle a text message:", e)
|
|
689
|
+
);
|
|
655
690
|
};
|
|
656
691
|
}
|
|
657
692
|
get isOpen() {
|
|
@@ -968,10 +1003,18 @@ var RpcError = class extends Error {
|
|
|
968
1003
|
}
|
|
969
1004
|
};
|
|
970
1005
|
var REMOTE_EXCEPTION_TYPE_REF = "ActualLab.Serialization.RemoteException, ActualLab.Core";
|
|
971
|
-
|
|
1006
|
+
var genericErrorFilter = () => ({
|
|
1007
|
+
TypeRef: REMOTE_EXCEPTION_TYPE_REF,
|
|
1008
|
+
Message: "Error: An error occurred while processing the request."
|
|
1009
|
+
});
|
|
1010
|
+
function toExceptionInfo(error, filter) {
|
|
972
1011
|
const name = error instanceof Error ? error.name : "Error";
|
|
973
1012
|
const message = error instanceof Error ? error.message : String(error);
|
|
974
|
-
|
|
1013
|
+
const info = {
|
|
1014
|
+
TypeRef: REMOTE_EXCEPTION_TYPE_REF,
|
|
1015
|
+
Message: `${name}: ${message}`
|
|
1016
|
+
};
|
|
1017
|
+
return filter === void 0 ? info : filter(error, info);
|
|
975
1018
|
}
|
|
976
1019
|
|
|
977
1020
|
// src/increasing-seq-compressor.ts
|
|
@@ -1065,1271 +1108,1308 @@ function _readVarUint(data, offset) {
|
|
|
1065
1108
|
return [value, offset];
|
|
1066
1109
|
}
|
|
1067
1110
|
|
|
1068
|
-
// src/
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1111
|
+
// src/base64.ts
|
|
1112
|
+
function base64Encode(bytes) {
|
|
1113
|
+
const chunkSize = 32768;
|
|
1114
|
+
let binary = "";
|
|
1115
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
1116
|
+
binary += String.fromCharCode(
|
|
1117
|
+
...bytes.subarray(i, Math.min(bytes.length, i + chunkSize))
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
return btoa(binary);
|
|
1121
|
+
}
|
|
1122
|
+
function base64UrlEncode(bytes) {
|
|
1123
|
+
return base64Encode(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1124
|
+
}
|
|
1125
|
+
function base64Decode(encoded) {
|
|
1126
|
+
const binary = atob(encoded);
|
|
1127
|
+
const out = new Uint8Array(binary.length);
|
|
1128
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
1129
|
+
return out;
|
|
1130
|
+
}
|
|
1074
1131
|
|
|
1075
|
-
// src/rpc-
|
|
1076
|
-
var
|
|
1077
|
-
var
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1132
|
+
// src/rpc-system-call-handler.ts
|
|
1133
|
+
var { debugLog, warnLog: warnLog4 } = getLogs("RpcSystemCallHandler");
|
|
1134
|
+
var RpcSystemCallHandler = class {
|
|
1135
|
+
handle(message, args, peer) {
|
|
1136
|
+
const method = message.Method;
|
|
1137
|
+
const relatedId = message.RelatedId ?? 0;
|
|
1138
|
+
debugLog?.log(`'${peer.ref}': handle ${method}#${relatedId}`);
|
|
1139
|
+
switch (method) {
|
|
1140
|
+
case RpcSystemCalls.ok: {
|
|
1141
|
+
const call = peer.outboundCalls.get(relatedId);
|
|
1142
|
+
if (call !== void 0) {
|
|
1143
|
+
if (args.length > 1) {
|
|
1144
|
+
peer.outboundCalls.remove(relatedId);
|
|
1145
|
+
call.result.reject(polymorphicPayloadError(method, args.length, 1));
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
call.completedStage |= RpcCallStage.ResultReady;
|
|
1149
|
+
if (call.removeOnOk) {
|
|
1150
|
+
peer.outboundCalls.remove(relatedId);
|
|
1151
|
+
}
|
|
1152
|
+
call.result.resolve(args[0]);
|
|
1153
|
+
}
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
case RpcSystemCalls.reconnect: {
|
|
1157
|
+
this._handleReconnect(relatedId, args, peer);
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
case RpcSystemCalls.error: {
|
|
1161
|
+
const call = peer.outboundCalls.get(relatedId);
|
|
1162
|
+
if (call !== void 0) {
|
|
1163
|
+
call.completedStage |= RpcCallStage.ResultReady;
|
|
1164
|
+
if (call.removeOnOk) {
|
|
1165
|
+
peer.outboundCalls.remove(relatedId);
|
|
1166
|
+
}
|
|
1167
|
+
const errorInfo = args[0];
|
|
1168
|
+
const msg = errorInfo?.Message ?? errorInfo?.message ?? "RPC error";
|
|
1169
|
+
const typeName = extractTypeName(errorInfo?.TypeRef ?? errorInfo?.typeRef);
|
|
1170
|
+
if (typeName === "ActualLab.Rpc.RpcRerouteException")
|
|
1171
|
+
warnLog4?.log("Got RpcRerouteException from remote peer:", msg);
|
|
1172
|
+
call.result.reject(new RpcError(msg, typeName));
|
|
1173
|
+
}
|
|
1174
|
+
break;
|
|
1175
|
+
}
|
|
1176
|
+
case RpcSystemCalls.cancel: {
|
|
1177
|
+
const call = peer.inboundCalls.remove(relatedId);
|
|
1178
|
+
call?.cancel();
|
|
1179
|
+
break;
|
|
1180
|
+
}
|
|
1181
|
+
case RpcSystemCalls.keepAlive: {
|
|
1182
|
+
peer.notifyKeepAliveReceived();
|
|
1183
|
+
const keepAliveIds = args[0];
|
|
1184
|
+
if (Array.isArray(keepAliveIds) && keepAliveIds.length > 0 && peer.connection !== void 0) {
|
|
1185
|
+
const unknownIds = [];
|
|
1186
|
+
for (const id of keepAliveIds)
|
|
1187
|
+
if (peer.sharedObjects.get(id) === void 0)
|
|
1188
|
+
unknownIds.push(id);
|
|
1189
|
+
if (unknownIds.length > 0)
|
|
1190
|
+
peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, unknownIds);
|
|
1191
|
+
}
|
|
1192
|
+
break;
|
|
1193
|
+
}
|
|
1194
|
+
case RpcSystemCalls.item: {
|
|
1195
|
+
const stream = peer.remoteObjects.get(relatedId);
|
|
1196
|
+
if (!stream)
|
|
1197
|
+
debugLog?.log(`$sys.I: no stream for relatedId=${relatedId}`);
|
|
1198
|
+
else if (args.length > 2)
|
|
1199
|
+
stream.onEnd(args[0], polymorphicPayloadError(method, args.length, 2));
|
|
1200
|
+
else
|
|
1201
|
+
stream.onItem(args[0], args[1]);
|
|
1202
|
+
break;
|
|
1203
|
+
}
|
|
1204
|
+
case RpcSystemCalls.batch: {
|
|
1205
|
+
const stream = peer.remoteObjects.get(relatedId);
|
|
1206
|
+
if (!stream)
|
|
1207
|
+
debugLog?.log(`$sys.B: no stream for relatedId=${relatedId}`);
|
|
1208
|
+
else if (args.length > 2)
|
|
1209
|
+
stream.onEnd(args[0], polymorphicPayloadError(method, args.length, 2));
|
|
1210
|
+
else if (Array.isArray(args[1]))
|
|
1211
|
+
stream.onBatch(args[0], args[1]);
|
|
1212
|
+
break;
|
|
1213
|
+
}
|
|
1214
|
+
case RpcSystemCalls.end: {
|
|
1215
|
+
const stream = peer.remoteObjects.get(relatedId);
|
|
1216
|
+
if (!stream) {
|
|
1217
|
+
debugLog?.log(`$sys.End: no stream for relatedId=${relatedId}`);
|
|
1218
|
+
} else {
|
|
1219
|
+
const errorInfo = args[1];
|
|
1220
|
+
const typeName = extractTypeName(errorInfo?.TypeRef ?? errorInfo?.typeRef);
|
|
1221
|
+
const msg = errorInfo?.Message ?? errorInfo?.message;
|
|
1222
|
+
let error = null;
|
|
1223
|
+
if (typeName || msg) {
|
|
1224
|
+
error = new Error(msg ?? typeName ?? "RPC stream error");
|
|
1225
|
+
if (typeName)
|
|
1226
|
+
error.name = typeName;
|
|
1227
|
+
}
|
|
1228
|
+
stream.onEnd(args[0], error);
|
|
1229
|
+
}
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1232
|
+
case RpcSystemCalls.ack: {
|
|
1233
|
+
const sender = peer.sharedObjects.get(relatedId);
|
|
1234
|
+
if (sender)
|
|
1235
|
+
sender.onAck(args[0], args[1]);
|
|
1236
|
+
else if (peer.connection !== void 0)
|
|
1237
|
+
peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, [relatedId]);
|
|
1238
|
+
break;
|
|
1239
|
+
}
|
|
1240
|
+
case RpcSystemCalls.ackEnd: {
|
|
1241
|
+
const sender = peer.sharedObjects.get(relatedId);
|
|
1242
|
+
if (sender)
|
|
1243
|
+
sender.onAckEnd(args[0]);
|
|
1244
|
+
else if (peer.connection !== void 0)
|
|
1245
|
+
peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, [relatedId]);
|
|
1246
|
+
break;
|
|
1247
|
+
}
|
|
1248
|
+
case RpcSystemCalls.disconnect: {
|
|
1249
|
+
const ids = args[0];
|
|
1250
|
+
if (Array.isArray(ids)) {
|
|
1251
|
+
for (const id of ids) {
|
|
1252
|
+
const remoteObj = peer.remoteObjects.get(id);
|
|
1253
|
+
if (remoteObj && typeof remoteObj.disconnect === "function") {
|
|
1254
|
+
remoteObj.disconnect();
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1103
1262
|
/**
|
|
1104
|
-
*
|
|
1105
|
-
*
|
|
1106
|
-
*
|
|
1107
|
-
*
|
|
1263
|
+
* Server-side handler for the `$sys.Reconnect` call. Parses the peer's
|
|
1264
|
+
* report of in-flight call IDs (grouped by stage, IncreasingSeqCompressor-
|
|
1265
|
+
* encoded, base64-wrapped for JSON transport), computes the subset this
|
|
1266
|
+
* peer does not recognize, and replies with a compressed byte[] of the
|
|
1267
|
+
* unknown IDs wrapped in `$sys.Ok`.
|
|
1108
1268
|
*
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1269
|
+
* @param relatedId The outbound call ID the client used for this
|
|
1270
|
+
* `$sys.Reconnect` invocation — we send `$sys.Ok` back
|
|
1271
|
+
* with the same `RelatedId`.
|
|
1111
1272
|
*/
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
_rttSamples = [];
|
|
1125
|
-
// -- Recorder-controller metrics (Step 9.1) --
|
|
1126
|
-
/** Total items skipped via real-time `canSkipTo` compaction. */
|
|
1127
|
-
_skipCount = 0;
|
|
1128
|
-
/** Fires after a $sys.Ack has been processed (post-compaction). The
|
|
1129
|
-
* recorder's quality controller uses this to bump a "last ACK at"
|
|
1130
|
-
* watchdog so it can distinguish "stuck" (no ACK > N s) from
|
|
1131
|
-
* "throttled" (ACKs flowing). */
|
|
1132
|
-
onAckProcessed;
|
|
1133
|
-
/** Fires after each item is pushed onto the local ring buffer. The
|
|
1134
|
-
* argument is the buffer count *after* the push. Together with
|
|
1135
|
-
* `onAckProcessed` (which is the only place items leave the buffer),
|
|
1136
|
-
* this gives controllers a complete picture of buffer utilization.
|
|
1137
|
-
* When `bufferedCount === bufferSize`, the source pull is paused
|
|
1138
|
-
* until the next ACK frees space. Listener errors are swallowed. */
|
|
1139
|
-
onBuffered;
|
|
1140
|
-
get nextIndex() {
|
|
1141
|
-
return this._nextIndex;
|
|
1142
|
-
}
|
|
1143
|
-
get lastAckIndex() {
|
|
1144
|
-
return this._lastAckedIndex;
|
|
1145
|
-
}
|
|
1146
|
-
get skipCount() {
|
|
1147
|
-
return this._skipCount;
|
|
1148
|
-
}
|
|
1149
|
-
// Windowed-MIN ack round trip (ms, -1 until sampled): converges on propagation
|
|
1150
|
-
// RTT — unlike a mean/EMA it is not inflated by self-induced queuing.
|
|
1151
|
-
get minRttMs() {
|
|
1152
|
-
const cutoffMs = Date.now() - RTT_WINDOW_MS;
|
|
1153
|
-
while (this._rttSamples.length > 0 && this._rttSamples[0].atMs < cutoffMs)
|
|
1154
|
-
this._rttSamples.shift();
|
|
1155
|
-
if (this._rttSamples.length === 0)
|
|
1156
|
-
return -1;
|
|
1157
|
-
let min = this._rttSamples[0].rttMs;
|
|
1158
|
-
for (const s of this._rttSamples) {
|
|
1159
|
-
if (s.rttMs < min)
|
|
1160
|
-
min = s.rttMs;
|
|
1161
|
-
}
|
|
1162
|
-
return min;
|
|
1163
|
-
}
|
|
1164
|
-
// -- ACK queue (analog of .NET's Channel<(long, bool)>) --
|
|
1165
|
-
_acks = [];
|
|
1166
|
-
_whenAckReady = null;
|
|
1167
|
-
constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true, isRealTime = false, canSkipTo = () => true, sourceUsesAbortSignal = false, bufferSize) {
|
|
1168
|
-
const localId = peer.sharedObjects.nextId();
|
|
1169
|
-
this.id = { hostId: peer.hub.hubId, localId };
|
|
1170
|
-
this.allowReconnect = allowReconnect;
|
|
1171
|
-
this.isRealTime = isRealTime;
|
|
1172
|
-
this.canSkipTo = canSkipTo;
|
|
1173
|
-
this.peer = peer;
|
|
1174
|
-
this.ackPeriod = ackPeriod;
|
|
1175
|
-
this.ackAdvance = ackAdvance;
|
|
1176
|
-
this.sourceUsesAbortSignal = sourceUsesAbortSignal;
|
|
1177
|
-
if (bufferSize !== void 0 && bufferSize !== 0 && bufferSize < ackAdvance)
|
|
1178
|
-
warnLog3?.log(
|
|
1179
|
-
`RpcStream bufferSize (${bufferSize}) is below ackAdvance (${ackAdvance}); using ackAdvance as buffer size.`
|
|
1273
|
+
_handleReconnect(relatedId, args, peer) {
|
|
1274
|
+
if (peer.connection === void 0) return;
|
|
1275
|
+
const handshakeIndex = args[0];
|
|
1276
|
+
if (typeof handshakeIndex !== "number" || handshakeIndex !== peer.ownHandshakeIndex) {
|
|
1277
|
+
const got = typeof handshakeIndex === "number" ? handshakeIndex : `<${typeof handshakeIndex}>`;
|
|
1278
|
+
peer.hub.systemCallSender.error(
|
|
1279
|
+
peer.connection,
|
|
1280
|
+
peer.serializationFormat,
|
|
1281
|
+
relatedId,
|
|
1282
|
+
new Error(
|
|
1283
|
+
`TooLateToReconnect: own handshake index ${peer.ownHandshakeIndex} != ${got}`
|
|
1284
|
+
)
|
|
1180
1285
|
);
|
|
1181
|
-
this.bufferSize = bufferSize !== void 0 && bufferSize >= ackAdvance ? bufferSize : ackAdvance;
|
|
1182
|
-
}
|
|
1183
|
-
/** AbortSignal that is aborted when the sender is disconnected. */
|
|
1184
|
-
get abortSignal() {
|
|
1185
|
-
return this._abortController.signal;
|
|
1186
|
-
}
|
|
1187
|
-
/** Returns the stream reference string for the $sys.Ok response. */
|
|
1188
|
-
toRef() {
|
|
1189
|
-
return `${this.id.hostId},${this.id.localId},${this.ackPeriod},${this.ackAdvance},${this.allowReconnect ? "1" : "0"},${this.isRealTime ? "1" : "0"}`;
|
|
1190
|
-
}
|
|
1191
|
-
/**
|
|
1192
|
-
* Called by system call handler when $sys.Ack is received from the client.
|
|
1193
|
-
*
|
|
1194
|
-
* Mirrors .NET `RpcSharedStream<T>.OnAck` at
|
|
1195
|
-
* src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:68-101.
|
|
1196
|
-
*/
|
|
1197
|
-
onAck(nextIndex, hostId) {
|
|
1198
|
-
const mustReset = hostId !== "" && hostId !== _RpcStreamSender._emptyGuid;
|
|
1199
|
-
if (mustReset && hostId !== this.id.hostId) {
|
|
1200
|
-
this._sendDisconnect();
|
|
1201
1286
|
return;
|
|
1202
1287
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1288
|
+
let unknownIds = [];
|
|
1289
|
+
try {
|
|
1290
|
+
const stagesRaw = args[1];
|
|
1291
|
+
if (stagesRaw && typeof stagesRaw === "object" && !Array.isArray(stagesRaw)) {
|
|
1292
|
+
const unknownSet = /* @__PURE__ */ new Set();
|
|
1293
|
+
for (const [stageKey, rawValue] of Object.entries(stagesRaw)) {
|
|
1294
|
+
const bytes = _toBytes(rawValue);
|
|
1295
|
+
if (bytes === null) continue;
|
|
1296
|
+
const completedStage = parseInt(stageKey, 10) || 0;
|
|
1297
|
+
const callIds = IncreasingSeqCompressor.deserialize(bytes);
|
|
1298
|
+
for (const callId of callIds) {
|
|
1299
|
+
const call = peer.inboundCalls.get(callId);
|
|
1300
|
+
if (call === void 0)
|
|
1301
|
+
unknownSet.add(callId);
|
|
1302
|
+
else if (completedStage < 1)
|
|
1303
|
+
call.resendResult();
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
unknownIds = [...unknownSet].sort((a, b) => a - b);
|
|
1209
1307
|
}
|
|
1210
|
-
}
|
|
1211
|
-
this._sendDisconnect();
|
|
1212
|
-
return;
|
|
1213
|
-
} else if (mustReset && !this.allowReconnect) {
|
|
1214
|
-
this._sendDisconnect();
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
this._sampleRtt(nextIndex, mustReset);
|
|
1218
|
-
this._acks.push({ nextIndex, mustReset });
|
|
1219
|
-
if (this._whenAckReady) {
|
|
1220
|
-
this._whenAckReady.resolve();
|
|
1221
|
-
this._whenAckReady = null;
|
|
1308
|
+
} catch {
|
|
1222
1309
|
}
|
|
1310
|
+
const responseBytes = IncreasingSeqCompressor.serialize(unknownIds);
|
|
1311
|
+
const responseValue = peer.serializationFormat.isBinary ? responseBytes : base64Encode(responseBytes);
|
|
1312
|
+
peer.hub.systemCallSender.ok(peer.connection, peer.serializationFormat, relatedId, responseValue);
|
|
1223
1313
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1314
|
+
};
|
|
1315
|
+
function polymorphicPayloadError(method, got, expected) {
|
|
1316
|
+
return new Error(
|
|
1317
|
+
`${method}: expected ${expected} value(s), got ${got} \u2014 polymorphic payloads are not supported by the TS client`
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
function extractTypeName(typeRef) {
|
|
1321
|
+
if (typeof typeRef === "string") {
|
|
1322
|
+
if (typeRef.length === 0)
|
|
1323
|
+
return void 0;
|
|
1324
|
+
const commaIdx = typeRef.indexOf(",");
|
|
1325
|
+
return commaIdx >= 0 ? typeRef.slice(0, commaIdx) : typeRef;
|
|
1231
1326
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1327
|
+
if (typeRef !== null && typeof typeRef === "object") {
|
|
1328
|
+
const t = typeRef;
|
|
1329
|
+
const aqn = t.AssemblyQualifiedName ?? t.assemblyQualifiedName;
|
|
1330
|
+
if (typeof aqn === "string" && aqn.length > 0) {
|
|
1331
|
+
const commaIdx = aqn.indexOf(",");
|
|
1332
|
+
return commaIdx >= 0 ? aqn.slice(0, commaIdx) : aqn;
|
|
1333
|
+
}
|
|
1334
|
+
return t.TypeName ?? t.typeName;
|
|
1235
1335
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
sendItem(item) {
|
|
1246
|
-
if (this._ended) return;
|
|
1247
|
-
const conn = this.peer.connection;
|
|
1248
|
-
if (conn) {
|
|
1249
|
-
this.peer.hub.systemCallSender.item(
|
|
1250
|
-
conn,
|
|
1251
|
-
this.peer.serializationFormat,
|
|
1252
|
-
this.id.localId,
|
|
1253
|
-
this._nextIndex,
|
|
1254
|
-
item
|
|
1255
|
-
);
|
|
1256
|
-
this._pendingSendTimes.push({ index: this._nextIndex, sentAtMs: Date.now() });
|
|
1336
|
+
return void 0;
|
|
1337
|
+
}
|
|
1338
|
+
function _toBytes(value) {
|
|
1339
|
+
if (value instanceof Uint8Array) return value;
|
|
1340
|
+
if (typeof value === "string") {
|
|
1341
|
+
try {
|
|
1342
|
+
return base64Decode(value);
|
|
1343
|
+
} catch {
|
|
1344
|
+
return null;
|
|
1257
1345
|
}
|
|
1258
|
-
this._nextIndex++;
|
|
1259
1346
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1347
|
+
return null;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// src/rpc-system-call-sender.ts
|
|
1351
|
+
var { errorLog } = getLogs("RpcSystemCallSender");
|
|
1352
|
+
var RpcSystemCallSender = class _RpcSystemCallSender {
|
|
1353
|
+
// Reusable envelopes — one per hot-path method.
|
|
1354
|
+
_itemEnv = {
|
|
1355
|
+
Method: RpcSystemCalls.item,
|
|
1356
|
+
RelatedId: 0
|
|
1357
|
+
};
|
|
1358
|
+
_batchEnv = {
|
|
1359
|
+
Method: RpcSystemCalls.batch,
|
|
1360
|
+
RelatedId: 0
|
|
1361
|
+
};
|
|
1362
|
+
_endEnv = {
|
|
1363
|
+
Method: RpcSystemCalls.end,
|
|
1364
|
+
RelatedId: 0
|
|
1365
|
+
};
|
|
1366
|
+
_ackEnv = {
|
|
1367
|
+
Method: RpcSystemCalls.ack,
|
|
1368
|
+
RelatedId: 0
|
|
1369
|
+
};
|
|
1370
|
+
_ackEndEnv = {
|
|
1371
|
+
Method: RpcSystemCalls.ackEnd,
|
|
1372
|
+
RelatedId: 0
|
|
1373
|
+
};
|
|
1374
|
+
// Reusable arg arrays.
|
|
1375
|
+
_itemArgs = [0, null];
|
|
1376
|
+
_batchArgs = [0, null];
|
|
1377
|
+
_endArgs = [0, { TypeRef: "", Message: "" }];
|
|
1378
|
+
_ackArgs = [0, ""];
|
|
1379
|
+
_ackEndArgs = [""];
|
|
1380
|
+
static NO_ERROR = Object.freeze({ TypeRef: "", Message: "" });
|
|
1381
|
+
registry;
|
|
1382
|
+
/** Applied to every error this hub sends to a remote peer. Undefined means
|
|
1383
|
+
* pass through, i.e. the peer sees the original message — set it (e.g. to
|
|
1384
|
+
* {@link genericErrorFilter}) on a TS-hosted server, where handler errors
|
|
1385
|
+
* would otherwise disclose internal details to unauthenticated callers. */
|
|
1386
|
+
errorFilter;
|
|
1387
|
+
_send(conn, format, envelope, args) {
|
|
1388
|
+
const wireData = format.serializeMessage(
|
|
1389
|
+
envelope,
|
|
1390
|
+
args,
|
|
1391
|
+
conn.encoder,
|
|
1392
|
+
this.registry
|
|
1393
|
+
);
|
|
1394
|
+
if (typeof wireData === "string") {
|
|
1395
|
+
conn.send(wireData);
|
|
1396
|
+
} else {
|
|
1397
|
+
conn.sendBinary(wireData);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
handshake(conn, format, peerId, hubId, index) {
|
|
1401
|
+
const handshakeArg = format.isBinary ? [peerId, null, hubId, 2, index] : {
|
|
1402
|
+
RemotePeerId: peerId,
|
|
1403
|
+
RemoteApiVersionSet: null,
|
|
1404
|
+
RemoteHubId: hubId,
|
|
1405
|
+
ProtocolVersion: 2,
|
|
1406
|
+
Index: index
|
|
1407
|
+
};
|
|
1408
|
+
this._send(conn, format, { Method: RpcSystemCalls.handshake }, [
|
|
1409
|
+
handshakeArg
|
|
1410
|
+
]);
|
|
1411
|
+
}
|
|
1412
|
+
ok(conn, format, relatedId, result) {
|
|
1413
|
+
try {
|
|
1414
|
+
this._send(
|
|
1266
1415
|
conn,
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
items
|
|
1416
|
+
format,
|
|
1417
|
+
{ Method: RpcSystemCalls.ok, RelatedId: relatedId },
|
|
1418
|
+
[result]
|
|
1271
1419
|
);
|
|
1420
|
+
} catch (e) {
|
|
1421
|
+
errorLog?.log(`Failed to send Ok response for call #${relatedId}`, e);
|
|
1422
|
+
this.error(conn, format, relatedId, e);
|
|
1272
1423
|
}
|
|
1273
|
-
this._nextIndex += items.length;
|
|
1274
1424
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
if (this._ended) return;
|
|
1278
|
-
this._ended = true;
|
|
1279
|
-
const conn = this.peer.connection;
|
|
1280
|
-
if (!conn) return;
|
|
1281
|
-
const errorInfo = error ? toExceptionInfo(error) : { TypeRef: "", Message: "" };
|
|
1282
|
-
this.peer.hub.systemCallSender.end(
|
|
1425
|
+
error(conn, format, relatedId, error) {
|
|
1426
|
+
this._send(
|
|
1283
1427
|
conn,
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
this.
|
|
1287
|
-
errorInfo
|
|
1428
|
+
format,
|
|
1429
|
+
{ Method: RpcSystemCalls.error, RelatedId: relatedId },
|
|
1430
|
+
[toExceptionInfo(error, this.errorFilter)]
|
|
1288
1431
|
);
|
|
1289
1432
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1433
|
+
cancel(conn, format, relatedId) {
|
|
1434
|
+
this._send(conn, format, {
|
|
1435
|
+
Method: RpcSystemCalls.cancel,
|
|
1436
|
+
RelatedId: relatedId
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
keepAlive(conn, format, activeCallIds) {
|
|
1440
|
+
this._send(conn, format, { Method: RpcSystemCalls.keepAlive }, [
|
|
1441
|
+
activeCallIds
|
|
1442
|
+
]);
|
|
1443
|
+
}
|
|
1444
|
+
// Tells the remote peer the listed shared-object ids are gone on our side,
|
|
1445
|
+
// so its matching remote objects (e.g. RpcStream consumers) fail fast
|
|
1446
|
+
// instead of hanging. Mirrors .NET RpcSystemCallSender.Disconnect.
|
|
1447
|
+
disconnect(conn, format, localIds) {
|
|
1448
|
+
this._send(conn, format, { Method: RpcSystemCalls.disconnect }, [
|
|
1449
|
+
localIds
|
|
1450
|
+
]);
|
|
1451
|
+
}
|
|
1452
|
+
item(conn, format, localId, index, item) {
|
|
1453
|
+
this._itemEnv.RelatedId = localId;
|
|
1454
|
+
this._itemArgs[0] = index;
|
|
1455
|
+
this._itemArgs[1] = item;
|
|
1456
|
+
this._send(conn, format, this._itemEnv, this._itemArgs);
|
|
1457
|
+
this._itemArgs[1] = null;
|
|
1458
|
+
}
|
|
1459
|
+
batch(conn, format, localId, index, items) {
|
|
1460
|
+
this._batchEnv.RelatedId = localId;
|
|
1461
|
+
this._batchArgs[0] = index;
|
|
1462
|
+
this._batchArgs[1] = items;
|
|
1463
|
+
this._send(conn, format, this._batchEnv, this._batchArgs);
|
|
1464
|
+
this._batchArgs[1] = null;
|
|
1465
|
+
}
|
|
1466
|
+
end(conn, format, localId, index, error = _RpcSystemCallSender.NO_ERROR) {
|
|
1467
|
+
this._endEnv.RelatedId = localId;
|
|
1468
|
+
this._endArgs[0] = index;
|
|
1469
|
+
this._endArgs[1] = error;
|
|
1470
|
+
this._send(conn, format, this._endEnv, this._endArgs);
|
|
1471
|
+
}
|
|
1472
|
+
ack(conn, format, localId, nextIndex, hostId) {
|
|
1473
|
+
this._ackEnv.RelatedId = localId;
|
|
1474
|
+
this._ackArgs[0] = nextIndex;
|
|
1475
|
+
this._ackArgs[1] = hostId;
|
|
1476
|
+
this._send(conn, format, this._ackEnv, this._ackArgs);
|
|
1477
|
+
}
|
|
1478
|
+
ackEnd(conn, format, localId, hostId) {
|
|
1479
|
+
this._ackEndEnv.RelatedId = localId;
|
|
1480
|
+
this._ackEndArgs[0] = hostId;
|
|
1481
|
+
this._send(conn, format, this._ackEndEnv, this._ackEndArgs);
|
|
1309
1482
|
}
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
// src/rpc-object.ts
|
|
1486
|
+
var RpcObjectKind = { Local: 0, Remote: 1 };
|
|
1487
|
+
|
|
1488
|
+
// src/rpc-stream.ts
|
|
1489
|
+
var import_denque = __toESM(require("denque"), 1);
|
|
1490
|
+
var import_core6 = require("@actuallab/core");
|
|
1491
|
+
|
|
1492
|
+
// src/rpc-stream-sender.ts
|
|
1493
|
+
var import_core5 = require("@actuallab/core");
|
|
1494
|
+
var { warnLog: warnLog5 } = getLogs("RpcSharedStream");
|
|
1495
|
+
var DEFAULT_ACK_PERIOD = 256;
|
|
1496
|
+
var DEFAULT_ACK_ADVANCE = 128;
|
|
1497
|
+
var RTT_WINDOW_MS = 2e4;
|
|
1498
|
+
var MAX_PENDING_SEND_TIMES = 4096;
|
|
1499
|
+
var RpcStreamSender = class _RpcStreamSender {
|
|
1500
|
+
static _emptyGuid = "00000000-0000-0000-0000-000000000000";
|
|
1501
|
+
id;
|
|
1502
|
+
kind = RpcObjectKind.Local;
|
|
1503
|
+
allowReconnect;
|
|
1504
|
+
isRealTime;
|
|
1505
|
+
canSkipTo;
|
|
1506
|
+
peer;
|
|
1507
|
+
ackPeriod;
|
|
1310
1508
|
/**
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
1313
|
-
*
|
|
1314
|
-
* With `bufferSize > ackAdvance` the local ring buffer holds more than the
|
|
1315
|
-
* in-flight window — real-time mode uses the extra space to pre-buffer.
|
|
1509
|
+
* Wire-level flow-control window: the maximum index the sender may
|
|
1510
|
+
* advance past the most recently acknowledged index. Mirrors .NET
|
|
1511
|
+
* `RpcStream.AckAdvance`.
|
|
1316
1512
|
*/
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
state.iteratorDone = true;
|
|
1365
|
-
isFullyBuffered = true;
|
|
1366
|
-
buffer.pushTail({
|
|
1367
|
-
kind: "error",
|
|
1368
|
-
error: e instanceof Error ? e : new Error(String(e))
|
|
1369
|
-
});
|
|
1370
|
-
fireBuffered();
|
|
1371
|
-
return true;
|
|
1372
|
-
}
|
|
1373
|
-
};
|
|
1374
|
-
while (true) {
|
|
1375
|
-
let ack = this._tryProcessAcks();
|
|
1376
|
-
if (!ack) {
|
|
1377
|
-
await this._waitAckReady();
|
|
1378
|
-
if (this._ended) return;
|
|
1379
|
-
ack = this._tryProcessAcks();
|
|
1380
|
-
if (!ack) {
|
|
1381
|
-
warnLog3?.log("Something is off: couldn't read an acknowledgement");
|
|
1382
|
-
return;
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
this._lastAckedIndex = ack.nextIndex;
|
|
1386
|
-
if (isRealTime && ack.mustReset && !isFullyBuffered) {
|
|
1387
|
-
buffer.clear();
|
|
1388
|
-
bufferStart = this._nextIndex;
|
|
1389
|
-
while (true) {
|
|
1390
|
-
if (this._ended) return;
|
|
1391
|
-
let item;
|
|
1392
|
-
let accepted = false;
|
|
1393
|
-
try {
|
|
1394
|
-
const r = await readNext();
|
|
1395
|
-
if (this._ended) return;
|
|
1396
|
-
if (r.done) {
|
|
1397
|
-
item = _endItem;
|
|
1398
|
-
isFullyBuffered = true;
|
|
1399
|
-
state.iteratorDone = true;
|
|
1400
|
-
} else if (this.canSkipTo(r.value)) {
|
|
1401
|
-
item = { kind: "value", value: r.value };
|
|
1402
|
-
accepted = true;
|
|
1403
|
-
} else {
|
|
1404
|
-
continue;
|
|
1405
|
-
}
|
|
1406
|
-
} catch (e) {
|
|
1407
|
-
state.iteratorDone = true;
|
|
1408
|
-
isFullyBuffered = true;
|
|
1409
|
-
item = {
|
|
1410
|
-
kind: "error",
|
|
1411
|
-
error: e instanceof Error ? e : new Error(String(e))
|
|
1412
|
-
};
|
|
1413
|
-
}
|
|
1414
|
-
buffer.pushTail(item);
|
|
1415
|
-
fireBuffered();
|
|
1416
|
-
if (accepted || isFullyBuffered) break;
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
const shift = _clamp(ack.nextIndex - bufferStart, 0, buffer.count);
|
|
1420
|
-
if (shift > 0) {
|
|
1421
|
-
buffer.moveHead(shift);
|
|
1422
|
-
bufferStart += shift;
|
|
1423
|
-
}
|
|
1424
|
-
if (this._nextIndex < bufferStart) {
|
|
1425
|
-
if (!this._ended)
|
|
1426
|
-
this.sendEnd(new Error("Stream position unavailable."));
|
|
1427
|
-
return;
|
|
1428
|
-
}
|
|
1429
|
-
let bufferIndex = this._nextIndex - bufferStart;
|
|
1430
|
-
const maxIndex = ack.nextIndex + this.ackAdvance;
|
|
1431
|
-
while (this._nextIndex < maxIndex) {
|
|
1432
|
-
if (this._ended) return;
|
|
1433
|
-
if (isRealTime) {
|
|
1434
|
-
while (buffer.hasRemainingCapacity && !isFullyBuffered && this._acks.length === 0) {
|
|
1435
|
-
if (!await bufferNext(true, true))
|
|
1436
|
-
break;
|
|
1437
|
-
}
|
|
1438
|
-
if (bufferIndex >= buffer.count && !isFullyBuffered && buffer.hasRemainingCapacity && this._acks.length === 0) {
|
|
1439
|
-
await bufferNext(false, true);
|
|
1440
|
-
}
|
|
1441
|
-
if (ack.nextIndex > 0) {
|
|
1442
|
-
const result = _compactBufferedUnsentSuffix(
|
|
1443
|
-
buffer,
|
|
1444
|
-
bufferIndex,
|
|
1445
|
-
this.canSkipTo
|
|
1446
|
-
);
|
|
1447
|
-
bufferIndex = result.newIndex;
|
|
1448
|
-
if (result.skipped > 0)
|
|
1449
|
-
this._skipCount += result.skipped;
|
|
1450
|
-
}
|
|
1451
|
-
if (this._acks.length > 0) break;
|
|
1452
|
-
}
|
|
1453
|
-
while (this._nextIndex < maxIndex && bufferIndex < buffer.count) {
|
|
1454
|
-
const item = buffer.get(bufferIndex++);
|
|
1455
|
-
if (item.kind === "value") {
|
|
1456
|
-
this.sendItem(item.value);
|
|
1457
|
-
} else if (item.kind === "end") {
|
|
1458
|
-
if (!this._ended) this.sendEnd();
|
|
1459
|
-
return;
|
|
1460
|
-
} else {
|
|
1461
|
-
if (!this._ended) this.sendEnd(item.error);
|
|
1462
|
-
return;
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
if (this._nextIndex >= maxIndex)
|
|
1466
|
-
break;
|
|
1467
|
-
if (isFullyBuffered) break;
|
|
1468
|
-
if (!buffer.hasRemainingCapacity) break;
|
|
1469
|
-
if (!isRealTime)
|
|
1470
|
-
await bufferNext(false, false);
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1513
|
+
ackAdvance;
|
|
1514
|
+
/**
|
|
1515
|
+
* Effective local ring buffer capacity used by the pump. Set larger
|
|
1516
|
+
* than `ackAdvance` to pre-buffer items beyond the in-flight window;
|
|
1517
|
+
* values below `ackAdvance` are clamped up (with a warning) at
|
|
1518
|
+
* construction time. Not transmitted on the wire.
|
|
1519
|
+
*/
|
|
1520
|
+
bufferSize;
|
|
1521
|
+
/**
|
|
1522
|
+
* If true, the source was produced by a factory `(abortSignal) => AsyncIterable<T>`
|
|
1523
|
+
* and may honor the AbortSignal to exit gracefully. `disconnect()` first aborts
|
|
1524
|
+
* the signal and gives the source up to `RpcStream.disconnectGracePeriodMs` ms
|
|
1525
|
+
* before force-closing via `iterator.return()`.
|
|
1526
|
+
*
|
|
1527
|
+
* If false, the source is a plain AsyncIterable that cannot observe the signal,
|
|
1528
|
+
* so `disconnect()` force-closes immediately.
|
|
1529
|
+
*/
|
|
1530
|
+
sourceUsesAbortSignal;
|
|
1531
|
+
// -- Pump state --
|
|
1532
|
+
/** Next stream index that will be sent (matches .NET's `index`). */
|
|
1533
|
+
_nextIndex = 0;
|
|
1534
|
+
_ended = false;
|
|
1535
|
+
_started = new import_core5.PromiseSource();
|
|
1536
|
+
/** Highest `nextIndex` observed in an ACK so far (matches .NET's `lastAckNextIndex` idiom). */
|
|
1537
|
+
_lastAckedIndex = 0;
|
|
1538
|
+
_abortController = new AbortController();
|
|
1539
|
+
_iterator = null;
|
|
1540
|
+
// -- RTT sampling (send → matching ack round trips, windowed minimum) --
|
|
1541
|
+
_pendingSendTimes = [];
|
|
1542
|
+
_rttSamples = [];
|
|
1543
|
+
// -- Recorder-controller metrics (Step 9.1) --
|
|
1544
|
+
/** Total items skipped via real-time `canSkipTo` compaction. */
|
|
1545
|
+
_skipCount = 0;
|
|
1546
|
+
/** Fires after a $sys.Ack has been processed (post-compaction). The
|
|
1547
|
+
* recorder's quality controller uses this to bump a "last ACK at"
|
|
1548
|
+
* watchdog so it can distinguish "stuck" (no ACK > N s) from
|
|
1549
|
+
* "throttled" (ACKs flowing). */
|
|
1550
|
+
onAckProcessed;
|
|
1551
|
+
/** Fires after each item is pushed onto the local ring buffer. The
|
|
1552
|
+
* argument is the buffer count *after* the push. Together with
|
|
1553
|
+
* `onAckProcessed` (which is the only place items leave the buffer),
|
|
1554
|
+
* this gives controllers a complete picture of buffer utilization.
|
|
1555
|
+
* When `bufferedCount === bufferSize`, the source pull is paused
|
|
1556
|
+
* until the next ACK frees space. Listener errors are swallowed. */
|
|
1557
|
+
onBuffered;
|
|
1558
|
+
get nextIndex() {
|
|
1559
|
+
return this._nextIndex;
|
|
1473
1560
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
* A reconnect's reset ACK may be drained in one batch with regular
|
|
1477
|
-
* consumption ACKs the client sends right after it, so `mustReset` is
|
|
1478
|
-
* OR-ed over the whole batch — the last ACK alone can't represent it. */
|
|
1479
|
-
_tryProcessAcks() {
|
|
1480
|
-
if (this._acks.length === 0) return null;
|
|
1481
|
-
let nextIndex = 0;
|
|
1482
|
-
let mustReset = false;
|
|
1483
|
-
while (this._acks.length > 0) {
|
|
1484
|
-
const a = this._acks.shift();
|
|
1485
|
-
nextIndex = a.nextIndex;
|
|
1486
|
-
mustReset ||= a.mustReset;
|
|
1487
|
-
if (a.mustReset || this._nextIndex < a.nextIndex) {
|
|
1488
|
-
this._nextIndex = a.nextIndex;
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
try {
|
|
1492
|
-
this.onAckProcessed?.();
|
|
1493
|
-
} catch {
|
|
1494
|
-
}
|
|
1495
|
-
return { nextIndex, mustReset };
|
|
1561
|
+
get lastAckIndex() {
|
|
1562
|
+
return this._lastAckedIndex;
|
|
1496
1563
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
if (this._acks.length > 0 || this._ended) return;
|
|
1500
|
-
this._whenAckReady = new import_core5.PromiseSource();
|
|
1501
|
-
await this._whenAckReady;
|
|
1502
|
-
this._whenAckReady = null;
|
|
1564
|
+
get skipCount() {
|
|
1565
|
+
return this._skipCount;
|
|
1503
1566
|
}
|
|
1504
|
-
//
|
|
1505
|
-
//
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
this._pendingSendTimes.length = 0;
|
|
1509
|
-
return;
|
|
1510
|
-
}
|
|
1511
|
-
let sentAtMs = -1;
|
|
1512
|
-
while (this._pendingSendTimes.length > 0 && this._pendingSendTimes[0].index < ackNextIndex) {
|
|
1513
|
-
sentAtMs = this._pendingSendTimes[0].sentAtMs;
|
|
1514
|
-
this._pendingSendTimes.shift();
|
|
1515
|
-
}
|
|
1516
|
-
if (sentAtMs < 0)
|
|
1517
|
-
return;
|
|
1518
|
-
const nowMs = Date.now();
|
|
1519
|
-
const cutoffMs = nowMs - RTT_WINDOW_MS;
|
|
1567
|
+
// Windowed-MIN ack round trip (ms, -1 until sampled): converges on propagation
|
|
1568
|
+
// RTT — unlike a mean/EMA it is not inflated by self-induced queuing.
|
|
1569
|
+
get minRttMs() {
|
|
1570
|
+
const cutoffMs = Date.now() - RTT_WINDOW_MS;
|
|
1520
1571
|
while (this._rttSamples.length > 0 && this._rttSamples[0].atMs < cutoffMs)
|
|
1521
1572
|
this._rttSamples.shift();
|
|
1522
|
-
this._rttSamples.
|
|
1573
|
+
if (this._rttSamples.length === 0)
|
|
1574
|
+
return -1;
|
|
1575
|
+
let min = this._rttSamples[0].rttMs;
|
|
1576
|
+
for (const s of this._rttSamples) {
|
|
1577
|
+
if (s.rttMs < min)
|
|
1578
|
+
min = s.rttMs;
|
|
1579
|
+
}
|
|
1580
|
+
return min;
|
|
1523
1581
|
}
|
|
1524
|
-
// --
|
|
1525
|
-
|
|
1582
|
+
// -- Coalesced ACK state (analog of .NET's Channel<(long, bool)>) --
|
|
1583
|
+
// The pump reduces any queued ACKs to the newest index plus the OR of
|
|
1584
|
+
// `mustReset` anyway, so we accumulate exactly that at receipt: an ACK flood
|
|
1585
|
+
// then costs O(1) memory and O(1) work instead of an unbounded array drained
|
|
1586
|
+
// with repeated `shift()`.
|
|
1587
|
+
/** Newest unprocessed ACK index; -1 means "nothing pending". */
|
|
1588
|
+
_ackIndex = -1;
|
|
1589
|
+
_ackMustReset = false;
|
|
1590
|
+
/** Highest ACK index accepted so far — the monotonicity baseline a
|
|
1591
|
+
* non-reset ACK may not regress below. */
|
|
1592
|
+
_maxAckIndex = 0;
|
|
1593
|
+
_whenAckReady = null;
|
|
1594
|
+
constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true, isRealTime = false, canSkipTo = () => true, sourceUsesAbortSignal = false, bufferSize) {
|
|
1595
|
+
const localId = peer.sharedObjects.nextId();
|
|
1596
|
+
this.id = { hostId: peer.hub.hubId, localId };
|
|
1597
|
+
this.allowReconnect = allowReconnect;
|
|
1598
|
+
this.isRealTime = isRealTime;
|
|
1599
|
+
this.canSkipTo = canSkipTo;
|
|
1600
|
+
this.peer = peer;
|
|
1601
|
+
this.ackPeriod = ackPeriod;
|
|
1602
|
+
this.ackAdvance = ackAdvance;
|
|
1603
|
+
this.sourceUsesAbortSignal = sourceUsesAbortSignal;
|
|
1604
|
+
if (bufferSize !== void 0 && bufferSize !== 0 && bufferSize < ackAdvance)
|
|
1605
|
+
warnLog5?.log(
|
|
1606
|
+
`RpcStream bufferSize (${bufferSize}) is below ackAdvance (${ackAdvance}); using ackAdvance as buffer size.`
|
|
1607
|
+
);
|
|
1608
|
+
this.bufferSize = bufferSize !== void 0 && bufferSize >= ackAdvance ? bufferSize : ackAdvance;
|
|
1526
1609
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
this.
|
|
1530
|
-
|
|
1610
|
+
/** AbortSignal that is aborted when the sender is disconnected. */
|
|
1611
|
+
get abortSignal() {
|
|
1612
|
+
return this._abortController.signal;
|
|
1613
|
+
}
|
|
1614
|
+
/** Returns the stream reference string for the $sys.Ok response. */
|
|
1615
|
+
toRef() {
|
|
1616
|
+
return `${this.id.hostId},${this.id.localId},${this.ackPeriod},${this.ackAdvance},${this.allowReconnect ? "1" : "0"},${this.isRealTime ? "1" : "0"}`;
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Called by system call handler when $sys.Ack is received from the client.
|
|
1620
|
+
*
|
|
1621
|
+
* Mirrors .NET `RpcSharedStream<T>.OnAck` at
|
|
1622
|
+
* src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:68-101.
|
|
1623
|
+
*/
|
|
1624
|
+
onAck(nextIndex, hostId) {
|
|
1625
|
+
const mustReset = hostId !== "" && hostId !== _RpcStreamSender._emptyGuid;
|
|
1626
|
+
if (mustReset && hostId !== this.id.hostId) {
|
|
1627
|
+
this._sendDisconnect();
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
if (!Number.isSafeInteger(nextIndex) || nextIndex < 0 || nextIndex > this._nextIndex) {
|
|
1631
|
+
this._sendDisconnect();
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1531
1634
|
if (!this._started.isCompleted) {
|
|
1532
|
-
|
|
1635
|
+
if (mustReset && nextIndex === 0)
|
|
1636
|
+
this._started.resolve();
|
|
1637
|
+
else {
|
|
1638
|
+
this._sendDisconnect();
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
} else if (this._ended) {
|
|
1642
|
+
this._sendDisconnect();
|
|
1643
|
+
return;
|
|
1644
|
+
} else if (mustReset && !this.allowReconnect) {
|
|
1645
|
+
this._sendDisconnect();
|
|
1646
|
+
return;
|
|
1533
1647
|
}
|
|
1648
|
+
if (!mustReset && nextIndex < this._maxAckIndex)
|
|
1649
|
+
return;
|
|
1650
|
+
this._maxAckIndex = nextIndex;
|
|
1651
|
+
this._sampleRtt(nextIndex, mustReset);
|
|
1652
|
+
this._ackIndex = nextIndex;
|
|
1653
|
+
this._ackMustReset ||= mustReset;
|
|
1534
1654
|
if (this._whenAckReady) {
|
|
1535
1655
|
this._whenAckReady.resolve();
|
|
1536
1656
|
this._whenAckReady = null;
|
|
1537
1657
|
}
|
|
1538
|
-
this.peer.sharedObjects.unregister(this);
|
|
1539
|
-
const iterator = this._iterator;
|
|
1540
|
-
if (!iterator) return;
|
|
1541
|
-
if (!this.sourceUsesAbortSignal) {
|
|
1542
|
-
this._iterator = null;
|
|
1543
|
-
this._forceCloseIterator(iterator);
|
|
1544
|
-
return;
|
|
1545
|
-
}
|
|
1546
|
-
setTimeout(() => {
|
|
1547
|
-
if (this._iterator === iterator) {
|
|
1548
|
-
this._iterator = null;
|
|
1549
|
-
this._forceCloseIterator(iterator);
|
|
1550
|
-
}
|
|
1551
|
-
}, RpcStream.disconnectGracePeriodMs);
|
|
1552
1658
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
}
|
|
1561
|
-
};
|
|
1562
|
-
var _endItem = { kind: "end" };
|
|
1563
|
-
function _clamp(value, min, max) {
|
|
1564
|
-
return value < min ? min : value > max ? max : value;
|
|
1565
|
-
}
|
|
1566
|
-
function _compactBufferedUnsentSuffix(buffer, firstUnsentIndex, canSkipTo) {
|
|
1567
|
-
let skipToIndex = -1;
|
|
1568
|
-
for (let i = firstUnsentIndex; i < buffer.count; i++) {
|
|
1569
|
-
const item = buffer.get(i);
|
|
1570
|
-
if (item.kind === "value" && canSkipTo(item.value))
|
|
1571
|
-
skipToIndex = i;
|
|
1572
|
-
}
|
|
1573
|
-
if (skipToIndex <= firstUnsentIndex)
|
|
1574
|
-
return { newIndex: firstUnsentIndex, skipped: 0 };
|
|
1575
|
-
const cutFrom = firstUnsentIndex;
|
|
1576
|
-
const cutTo = skipToIndex;
|
|
1577
|
-
const items = buffer.toArray();
|
|
1578
|
-
buffer.clear();
|
|
1579
|
-
for (let i = 0; i < cutFrom; i++)
|
|
1580
|
-
buffer.pushTail(items[i]);
|
|
1581
|
-
for (let i = cutTo; i < items.length; i++)
|
|
1582
|
-
buffer.pushTail(items[i]);
|
|
1583
|
-
return { newIndex: firstUnsentIndex, skipped: skipToIndex - firstUnsentIndex };
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
// src/rpc-stream.ts
|
|
1587
|
-
var { warnLog: warnLog4 } = getLogs("RpcStream");
|
|
1588
|
-
var DEFAULT_ACK_PERIOD2 = 30;
|
|
1589
|
-
var DEFAULT_ACK_ADVANCE2 = 61;
|
|
1590
|
-
function parseStreamRef(value) {
|
|
1591
|
-
if (typeof value === "string") {
|
|
1592
|
-
const parts = value.split(",");
|
|
1593
|
-
if (parts.length < 4 || parts.length > 6) return null;
|
|
1594
|
-
const hostId = parts[0];
|
|
1595
|
-
const localId = parseInt(parts[1], 10);
|
|
1596
|
-
const ackPeriod = parseInt(parts[2], 10);
|
|
1597
|
-
const ackAdvance = parseInt(parts[3], 10);
|
|
1598
|
-
if (isNaN(localId) || isNaN(ackPeriod) || isNaN(ackAdvance))
|
|
1599
|
-
return null;
|
|
1600
|
-
const allowReconnect = parts.length < 5 || parts[4] !== "0";
|
|
1601
|
-
const isRealTime = parts.length >= 6 && parts[5] === "1";
|
|
1602
|
-
return { hostId, localId, ackPeriod, ackAdvance, allowReconnect, isRealTime };
|
|
1659
|
+
// Tells the consumer this shared stream is gone, then disposes it —
|
|
1660
|
+
// mirrors RpcSharedStream.SendDisconnect (RpcSharedStream.cs:284-289).
|
|
1661
|
+
_sendDisconnect() {
|
|
1662
|
+
const conn = this.peer.connection;
|
|
1663
|
+
if (conn)
|
|
1664
|
+
this.peer.hub.systemCallSender.disconnect(conn, this.peer.serializationFormat, [this.id.localId]);
|
|
1665
|
+
this.disconnect();
|
|
1603
1666
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
if (!Array.isArray(serializedId) || serializedId.length < 2)
|
|
1608
|
-
return null;
|
|
1609
|
-
const rawHostId = serializedId[0];
|
|
1610
|
-
const hostId = String(rawHostId);
|
|
1611
|
-
const localId = Number(serializedId[1]);
|
|
1612
|
-
const ackPeriod = Number(obj.AckPeriod ?? 256);
|
|
1613
|
-
const ackAdvance = Number(obj.AckAdvance ?? 128);
|
|
1614
|
-
const allowReconnect = obj.AllowReconnect !== false;
|
|
1615
|
-
const isRealTime = obj.IsRealTime === true;
|
|
1616
|
-
return { hostId, localId, ackPeriod, ackAdvance, allowReconnect, isRealTime };
|
|
1667
|
+
/** Called by system call handler when $sys.AckEnd is received from the client. */
|
|
1668
|
+
onAckEnd(_hostId) {
|
|
1669
|
+
this.disconnect();
|
|
1617
1670
|
}
|
|
1618
|
-
return null;
|
|
1619
|
-
}
|
|
1620
|
-
var RpcStream = class _RpcStream {
|
|
1621
1671
|
/**
|
|
1622
|
-
*
|
|
1623
|
-
* AbortSignal and exit naturally before the sender force-closes the
|
|
1624
|
-
* iterator via `iterator.return()`.
|
|
1672
|
+
* Send a single item to the client at the current `_nextIndex`.
|
|
1625
1673
|
*
|
|
1626
|
-
*
|
|
1627
|
-
*
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
kind;
|
|
1632
|
-
allowReconnect;
|
|
1633
|
-
isRealTime;
|
|
1634
|
-
canSkipTo;
|
|
1635
|
-
ackPeriod;
|
|
1636
|
-
ackAdvance;
|
|
1637
|
-
/**
|
|
1638
|
-
* Local-only ring buffer capacity hint for the sender. Undefined means
|
|
1639
|
-
* "use ackAdvance as the buffer size". Not part of the wire ref.
|
|
1674
|
+
* Advances `_nextIndex` unconditionally — even if the peer is disconnected
|
|
1675
|
+
* the item is considered "sent" from the pump's perspective so the replay
|
|
1676
|
+
* buffer indexing stays consistent. If a connection is absent, the wire
|
|
1677
|
+
* message is dropped; the item will be resent from the replay buffer
|
|
1678
|
+
* after a reconnect (client sends `Ack(N, mustReset=true)`).
|
|
1640
1679
|
*/
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
_completionError = null;
|
|
1656
|
-
_disposed = false;
|
|
1657
|
-
_started = false;
|
|
1658
|
-
_iterating = false;
|
|
1659
|
-
_ackSentUpTo = 0;
|
|
1660
|
-
_nextConsumedIndex = 0;
|
|
1661
|
-
constructor(sourceOrRef, optionsOrPeer) {
|
|
1662
|
-
if (_isStreamSource(sourceOrRef)) {
|
|
1663
|
-
const options = optionsOrPeer ?? {};
|
|
1664
|
-
this.kind = RpcObjectKind.Local;
|
|
1665
|
-
this.localSource = sourceOrRef;
|
|
1666
|
-
this.id = { hostId: "", localId: 0 };
|
|
1667
|
-
this.allowReconnect = options.allowReconnect ?? true;
|
|
1668
|
-
this.isRealTime = options.isRealTime ?? false;
|
|
1669
|
-
this.canSkipTo = options.canSkipTo ?? (() => true);
|
|
1670
|
-
this.ackPeriod = options.ackPeriod ?? DEFAULT_ACK_PERIOD2;
|
|
1671
|
-
this.ackAdvance = options.ackAdvance ?? DEFAULT_ACK_ADVANCE2;
|
|
1672
|
-
this.bufferSize = options.bufferSize;
|
|
1673
|
-
} else {
|
|
1674
|
-
const ref = sourceOrRef;
|
|
1675
|
-
const peer = optionsOrPeer;
|
|
1676
|
-
this.kind = RpcObjectKind.Remote;
|
|
1677
|
-
this.id = { hostId: ref.hostId, localId: ref.localId };
|
|
1678
|
-
this.peer = peer;
|
|
1679
|
-
this.allowReconnect = ref.allowReconnect;
|
|
1680
|
-
this.isRealTime = ref.isRealTime;
|
|
1681
|
-
this.canSkipTo = () => true;
|
|
1682
|
-
this.ackPeriod = ref.ackPeriod;
|
|
1683
|
-
this.ackAdvance = ref.ackAdvance;
|
|
1680
|
+
sendItem(item) {
|
|
1681
|
+
if (this._ended) return;
|
|
1682
|
+
const conn = this.peer.connection;
|
|
1683
|
+
if (conn) {
|
|
1684
|
+
this.peer.hub.systemCallSender.item(
|
|
1685
|
+
conn,
|
|
1686
|
+
this.peer.serializationFormat,
|
|
1687
|
+
this.id.localId,
|
|
1688
|
+
this._nextIndex,
|
|
1689
|
+
item
|
|
1690
|
+
);
|
|
1691
|
+
this._pendingSendTimes.push({ index: this._nextIndex, sentAtMs: Date.now() });
|
|
1692
|
+
if (this._pendingSendTimes.length > MAX_PENDING_SEND_TIMES)
|
|
1693
|
+
this._pendingSendTimes.splice(0, MAX_PENDING_SEND_TIMES >> 1);
|
|
1684
1694
|
}
|
|
1695
|
+
this._nextIndex++;
|
|
1685
1696
|
}
|
|
1686
|
-
/**
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1697
|
+
/** Send a batch of items to the client. See {@link sendItem} for disconnect semantics. */
|
|
1698
|
+
sendBatch(items) {
|
|
1699
|
+
if (this._ended || items.length === 0) return;
|
|
1700
|
+
const conn = this.peer.connection;
|
|
1701
|
+
if (conn) {
|
|
1702
|
+
this.peer.hub.systemCallSender.batch(
|
|
1703
|
+
conn,
|
|
1704
|
+
this.peer.serializationFormat,
|
|
1705
|
+
this.id.localId,
|
|
1706
|
+
this._nextIndex,
|
|
1707
|
+
items
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
this._nextIndex += items.length;
|
|
1694
1711
|
}
|
|
1695
|
-
/**
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1712
|
+
/** Signal stream completion to the client. */
|
|
1713
|
+
sendEnd(error) {
|
|
1714
|
+
if (this._ended) return;
|
|
1715
|
+
this._ended = true;
|
|
1716
|
+
const conn = this.peer.connection;
|
|
1717
|
+
if (!conn) return;
|
|
1718
|
+
const errorInfo = error ? toExceptionInfo(error) : { TypeRef: "", Message: "" };
|
|
1719
|
+
this.peer.hub.systemCallSender.end(
|
|
1720
|
+
conn,
|
|
1721
|
+
this.peer.serializationFormat,
|
|
1722
|
+
this.id.localId,
|
|
1723
|
+
this._nextIndex,
|
|
1724
|
+
errorInfo
|
|
1725
|
+
);
|
|
1699
1726
|
}
|
|
1700
1727
|
/**
|
|
1701
|
-
*
|
|
1702
|
-
* start pumping items from the local source in the background, and return the
|
|
1703
|
-
* stream reference.
|
|
1704
|
-
*
|
|
1705
|
-
* For binary peers the ref is a MessagePack-compatible object
|
|
1706
|
-
* (`{ SerializedId, AckPeriod, AckAdvance, AllowReconnect }`);
|
|
1707
|
-
* for text peers it is a comma-separated string.
|
|
1728
|
+
* Consume an AsyncIterable and send all items to the client.
|
|
1708
1729
|
*
|
|
1709
|
-
*
|
|
1730
|
+
* Matches .NET `RpcSharedStream<T>.OnRun` at
|
|
1731
|
+
* src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs.
|
|
1710
1732
|
*/
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1733
|
+
async writeFrom(source) {
|
|
1734
|
+
await this._started;
|
|
1735
|
+
if (this._ended) return;
|
|
1736
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
1737
|
+
this._iterator = iterator;
|
|
1738
|
+
const state = { iteratorDone: false };
|
|
1739
|
+
try {
|
|
1740
|
+
await this._run(iterator, state);
|
|
1741
|
+
} finally {
|
|
1742
|
+
if (state.iteratorDone && this._iterator === iterator) {
|
|
1743
|
+
this._iterator = null;
|
|
1744
|
+
}
|
|
1718
1745
|
}
|
|
1719
|
-
const isFactory = typeof this.localSource === "function";
|
|
1720
|
-
const sender = new RpcStreamSender(
|
|
1721
|
-
peer,
|
|
1722
|
-
this.ackPeriod,
|
|
1723
|
-
this.ackAdvance,
|
|
1724
|
-
this.allowReconnect,
|
|
1725
|
-
this.isRealTime,
|
|
1726
|
-
this.canSkipTo,
|
|
1727
|
-
isFactory,
|
|
1728
|
-
this.bufferSize
|
|
1729
|
-
);
|
|
1730
|
-
peer.sharedObjects.register(sender);
|
|
1731
|
-
this._sender = sender;
|
|
1732
|
-
const source = isFactory ? this.localSource(sender.abortSignal) : this.localSource;
|
|
1733
|
-
this._whenSent = sender.writeFrom(source);
|
|
1734
|
-
this._toRefValue = peer.serializationFormat.isBinary ? {
|
|
1735
|
-
SerializedId: [sender.id.hostId, sender.id.localId],
|
|
1736
|
-
AckPeriod: sender.ackPeriod,
|
|
1737
|
-
AckAdvance: sender.ackAdvance,
|
|
1738
|
-
AllowReconnect: sender.allowReconnect
|
|
1739
|
-
} : sender.toRef();
|
|
1740
|
-
return this._toRefValue;
|
|
1741
1746
|
}
|
|
1742
|
-
/**
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1747
|
+
/**
|
|
1748
|
+
* ACK-driven pump. Non-real-time streams apply normal backpressure at
|
|
1749
|
+
* `ackAdvance`; real-time streams may compact the already-buffered unsent
|
|
1750
|
+
* suffix to the latest buffered `canSkipTo` item after the consumer ACKs.
|
|
1751
|
+
* With `bufferSize > ackAdvance` the local ring buffer holds more than the
|
|
1752
|
+
* in-flight window — real-time mode uses the extra space to pre-buffer.
|
|
1753
|
+
*/
|
|
1754
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-condition -- _ended changes across awaits */
|
|
1755
|
+
async _run(iterator, state) {
|
|
1756
|
+
const buffer = new import_core5.RingBuffer(this.bufferSize + 1);
|
|
1757
|
+
const isRealTime = this.isRealTime;
|
|
1758
|
+
let bufferStart = 0;
|
|
1759
|
+
let isFullyBuffered = false;
|
|
1760
|
+
let whenMovedNext = null;
|
|
1761
|
+
const pending = /* @__PURE__ */ Symbol("pending");
|
|
1762
|
+
const readNext = async () => {
|
|
1763
|
+
const next = whenMovedNext ?? iterator.next();
|
|
1764
|
+
whenMovedNext = null;
|
|
1765
|
+
return await next;
|
|
1766
|
+
};
|
|
1767
|
+
const tryReadReady = async () => {
|
|
1768
|
+
whenMovedNext ??= iterator.next();
|
|
1769
|
+
return await Promise.race([
|
|
1770
|
+
whenMovedNext,
|
|
1771
|
+
Promise.resolve(pending)
|
|
1772
|
+
]);
|
|
1773
|
+
};
|
|
1774
|
+
const fireBuffered = () => {
|
|
1775
|
+
try {
|
|
1776
|
+
this.onBuffered?.(buffer.count);
|
|
1777
|
+
} catch {
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
const bufferNext = async (onlyIfReady, prefetchNext) => {
|
|
1781
|
+
let r;
|
|
1782
|
+
try {
|
|
1783
|
+
r = onlyIfReady ? await tryReadReady() : await readNext();
|
|
1784
|
+
if (r === pending)
|
|
1785
|
+
return false;
|
|
1786
|
+
whenMovedNext = null;
|
|
1787
|
+
if (this._ended) return false;
|
|
1788
|
+
if (r.done) {
|
|
1789
|
+
buffer.pushTail(_endItem);
|
|
1790
|
+
isFullyBuffered = true;
|
|
1791
|
+
state.iteratorDone = true;
|
|
1792
|
+
} else {
|
|
1793
|
+
buffer.pushTail({ kind: "value", value: r.value });
|
|
1794
|
+
if (prefetchNext)
|
|
1795
|
+
whenMovedNext = iterator.next();
|
|
1796
|
+
}
|
|
1797
|
+
fireBuffered();
|
|
1798
|
+
return true;
|
|
1799
|
+
} catch (e) {
|
|
1800
|
+
whenMovedNext = null;
|
|
1801
|
+
state.iteratorDone = true;
|
|
1802
|
+
isFullyBuffered = true;
|
|
1803
|
+
buffer.pushTail({
|
|
1804
|
+
kind: "error",
|
|
1805
|
+
error: e instanceof Error ? e : new Error(String(e))
|
|
1806
|
+
});
|
|
1807
|
+
fireBuffered();
|
|
1808
|
+
return true;
|
|
1750
1809
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
/** Called by system call handler when a batch arrives ($sys.B). */
|
|
1763
|
-
onBatch(index, items) {
|
|
1764
|
-
if (this._disposed || this._completed) return;
|
|
1765
|
-
if (index > this._nextExpectedIndex) {
|
|
1766
|
-
warnLog4?.log(`batch index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
|
|
1767
|
-
if (!this.allowReconnect) {
|
|
1768
|
-
this._complete(this._gapError(index));
|
|
1769
|
-
return;
|
|
1810
|
+
};
|
|
1811
|
+
while (true) {
|
|
1812
|
+
let ack = this._tryProcessAcks();
|
|
1813
|
+
if (!ack) {
|
|
1814
|
+
await this._waitAckReady();
|
|
1815
|
+
if (this._ended) return;
|
|
1816
|
+
ack = this._tryProcessAcks();
|
|
1817
|
+
if (!ack) {
|
|
1818
|
+
warnLog5?.log("Something is off: couldn't read an acknowledgement");
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1770
1821
|
}
|
|
1771
|
-
this.
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1822
|
+
this._lastAckedIndex = ack.nextIndex;
|
|
1823
|
+
if (isRealTime && ack.mustReset && !isFullyBuffered) {
|
|
1824
|
+
buffer.clear();
|
|
1825
|
+
bufferStart = this._nextIndex;
|
|
1826
|
+
while (true) {
|
|
1827
|
+
if (this._ended) return;
|
|
1828
|
+
let item;
|
|
1829
|
+
let accepted = false;
|
|
1830
|
+
try {
|
|
1831
|
+
const r = await readNext();
|
|
1832
|
+
if (this._ended) return;
|
|
1833
|
+
if (r.done) {
|
|
1834
|
+
item = _endItem;
|
|
1835
|
+
isFullyBuffered = true;
|
|
1836
|
+
state.iteratorDone = true;
|
|
1837
|
+
} else if (this.canSkipTo(r.value)) {
|
|
1838
|
+
item = { kind: "value", value: r.value };
|
|
1839
|
+
accepted = true;
|
|
1840
|
+
} else {
|
|
1841
|
+
continue;
|
|
1842
|
+
}
|
|
1843
|
+
} catch (e) {
|
|
1844
|
+
state.iteratorDone = true;
|
|
1845
|
+
isFullyBuffered = true;
|
|
1846
|
+
item = {
|
|
1847
|
+
kind: "error",
|
|
1848
|
+
error: e instanceof Error ? e : new Error(String(e))
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
buffer.pushTail(item);
|
|
1852
|
+
fireBuffered();
|
|
1853
|
+
if (accepted || isFullyBuffered) break;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
const shift = _clamp(ack.nextIndex - bufferStart, 0, buffer.count);
|
|
1857
|
+
if (shift > 0) {
|
|
1858
|
+
buffer.moveHead(shift);
|
|
1859
|
+
bufferStart += shift;
|
|
1860
|
+
}
|
|
1861
|
+
if (this._nextIndex < bufferStart) {
|
|
1862
|
+
if (!this._ended)
|
|
1863
|
+
this.sendEnd(new Error("Stream position unavailable."));
|
|
1789
1864
|
return;
|
|
1790
1865
|
}
|
|
1791
|
-
this.
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
}
|
|
1800
|
-
// -- IRpcObject --
|
|
1801
|
-
reconnect() {
|
|
1802
|
-
if (this.kind === RpcObjectKind.Local) return;
|
|
1803
|
-
if (this.allowReconnect) {
|
|
1804
|
-
this._sendAck(this._nextExpectedIndex, true);
|
|
1805
|
-
} else {
|
|
1806
|
-
this.disconnect();
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
disconnect() {
|
|
1810
|
-
if (this.kind === RpcObjectKind.Local) {
|
|
1811
|
-
this._sender?.disconnect();
|
|
1812
|
-
return;
|
|
1813
|
-
}
|
|
1814
|
-
if (this._completed) return;
|
|
1815
|
-
this._completed = true;
|
|
1816
|
-
this._completionError = new Error("Peer disconnected.");
|
|
1817
|
-
this._notifyConsumer();
|
|
1818
|
-
}
|
|
1819
|
-
// -- AsyncIterable --
|
|
1820
|
-
[Symbol.asyncIterator]() {
|
|
1821
|
-
if (this.kind === RpcObjectKind.Local) {
|
|
1822
|
-
const source = typeof this.localSource === "function" ? this.localSource(new AbortController().signal) : this.localSource;
|
|
1823
|
-
return source[Symbol.asyncIterator]();
|
|
1824
|
-
}
|
|
1825
|
-
if (this._iterating)
|
|
1826
|
-
throw new Error("RpcStream can only be iterated once.");
|
|
1827
|
-
this._iterating = true;
|
|
1828
|
-
return {
|
|
1829
|
-
next: async () => {
|
|
1830
|
-
if (!this._started) {
|
|
1831
|
-
this._started = true;
|
|
1832
|
-
this.peer.remoteObjects.register(this);
|
|
1833
|
-
this._sendAck(0, true);
|
|
1834
|
-
}
|
|
1835
|
-
this._maybeSendAck(this._nextConsumedIndex);
|
|
1836
|
-
for (; ; ) {
|
|
1837
|
-
if (!this._buffer.isEmpty()) {
|
|
1838
|
-
const value = this._buffer.shift();
|
|
1839
|
-
this._nextConsumedIndex++;
|
|
1840
|
-
return { value, done: false };
|
|
1866
|
+
let bufferIndex = this._nextIndex - bufferStart;
|
|
1867
|
+
const maxIndex = ack.nextIndex + this.ackAdvance;
|
|
1868
|
+
while (this._nextIndex < maxIndex) {
|
|
1869
|
+
if (this._ended) return;
|
|
1870
|
+
if (isRealTime) {
|
|
1871
|
+
while (buffer.hasRemainingCapacity && !isFullyBuffered && !this._hasPendingAck) {
|
|
1872
|
+
if (!await bufferNext(true, true))
|
|
1873
|
+
break;
|
|
1841
1874
|
}
|
|
1842
|
-
if (this.
|
|
1843
|
-
|
|
1844
|
-
if (this._completionError) throw this._completionError;
|
|
1845
|
-
return { value: void 0, done: true };
|
|
1875
|
+
if (bufferIndex >= buffer.count && !isFullyBuffered && buffer.hasRemainingCapacity && !this._hasPendingAck) {
|
|
1876
|
+
await bufferNext(false, true);
|
|
1846
1877
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1878
|
+
if (ack.nextIndex > 0) {
|
|
1879
|
+
const result = _compactBufferedUnsentSuffix(
|
|
1880
|
+
buffer,
|
|
1881
|
+
bufferIndex,
|
|
1882
|
+
this.canSkipTo
|
|
1883
|
+
);
|
|
1884
|
+
bufferIndex = result.newIndex;
|
|
1885
|
+
if (result.skipped > 0)
|
|
1886
|
+
this._skipCount += result.skipped;
|
|
1887
|
+
}
|
|
1888
|
+
if (this._hasPendingAck) break;
|
|
1849
1889
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1890
|
+
while (this._nextIndex < maxIndex && bufferIndex < buffer.count) {
|
|
1891
|
+
const item = buffer.get(bufferIndex++);
|
|
1892
|
+
if (item.kind === "value") {
|
|
1893
|
+
this.sendItem(item.value);
|
|
1894
|
+
} else if (item.kind === "end") {
|
|
1895
|
+
if (!this._ended) this.sendEnd();
|
|
1896
|
+
return;
|
|
1897
|
+
} else {
|
|
1898
|
+
if (!this._ended) this.sendEnd(item.error);
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
if (this._nextIndex >= maxIndex)
|
|
1903
|
+
break;
|
|
1904
|
+
if (isFullyBuffered) break;
|
|
1905
|
+
if (!buffer.hasRemainingCapacity) break;
|
|
1906
|
+
if (!isRealTime)
|
|
1907
|
+
await bufferNext(false, false);
|
|
1856
1908
|
}
|
|
1857
|
-
};
|
|
1858
|
-
}
|
|
1859
|
-
dispose() {
|
|
1860
|
-
if (this._disposed) return;
|
|
1861
|
-
this._disposed = true;
|
|
1862
|
-
this._completed = true;
|
|
1863
|
-
if (this.kind === RpcObjectKind.Remote) {
|
|
1864
|
-
this.peer.remoteObjects.unregister(this);
|
|
1865
1909
|
}
|
|
1866
|
-
this._notifyConsumer();
|
|
1867
1910
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
_complete(error) {
|
|
1872
|
-
if (this._completed) return;
|
|
1873
|
-
this._completed = true;
|
|
1874
|
-
this._completionError = error;
|
|
1875
|
-
this._sendAckEnd();
|
|
1876
|
-
if (this.kind === RpcObjectKind.Remote)
|
|
1877
|
-
this.peer.remoteObjects.unregister(this);
|
|
1878
|
-
this._notifyConsumer();
|
|
1911
|
+
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
|
|
1912
|
+
get _hasPendingAck() {
|
|
1913
|
+
return this._ackIndex >= 0;
|
|
1879
1914
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1915
|
+
/** Consume the coalesced ACK state (or null if nothing is pending).
|
|
1916
|
+
* A reconnect's reset ACK may be coalesced with the regular consumption
|
|
1917
|
+
* ACKs the client sends right after it, so `mustReset` accumulates across
|
|
1918
|
+
* the whole batch — the newest ACK alone can't represent it. */
|
|
1919
|
+
_tryProcessAcks() {
|
|
1920
|
+
const nextIndex = this._ackIndex;
|
|
1921
|
+
if (nextIndex < 0)
|
|
1922
|
+
return null;
|
|
1923
|
+
const mustReset = this._ackMustReset;
|
|
1924
|
+
this._ackIndex = -1;
|
|
1925
|
+
this._ackMustReset = false;
|
|
1926
|
+
if (mustReset || this._nextIndex < nextIndex)
|
|
1927
|
+
this._nextIndex = nextIndex;
|
|
1928
|
+
try {
|
|
1929
|
+
this.onAckProcessed?.();
|
|
1930
|
+
} catch {
|
|
1931
|
+
}
|
|
1932
|
+
return { nextIndex, mustReset };
|
|
1884
1933
|
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1934
|
+
/** Wait until an ACK is pending (or `_ended`). */
|
|
1935
|
+
async _waitAckReady() {
|
|
1936
|
+
if (this._hasPendingAck || this._ended)
|
|
1937
|
+
return;
|
|
1938
|
+
this._whenAckReady = new import_core5.PromiseSource();
|
|
1939
|
+
await this._whenAckReady;
|
|
1940
|
+
this._whenAckReady = null;
|
|
1941
|
+
}
|
|
1942
|
+
// One RTT sample per ack: time from sending the newest acked item to the
|
|
1943
|
+
// ack's arrival (a slight upper bound — includes the server's ack cadence).
|
|
1944
|
+
_sampleRtt(ackNextIndex, mustReset) {
|
|
1945
|
+
if (mustReset) {
|
|
1946
|
+
this._pendingSendTimes.length = 0;
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
let sentAtMs = -1;
|
|
1950
|
+
while (this._pendingSendTimes.length > 0 && this._pendingSendTimes[0].index < ackNextIndex) {
|
|
1951
|
+
sentAtMs = this._pendingSendTimes[0].sentAtMs;
|
|
1952
|
+
this._pendingSendTimes.shift();
|
|
1889
1953
|
}
|
|
1954
|
+
if (sentAtMs < 0)
|
|
1955
|
+
return;
|
|
1956
|
+
const nowMs = Date.now();
|
|
1957
|
+
const cutoffMs = nowMs - RTT_WINDOW_MS;
|
|
1958
|
+
while (this._rttSamples.length > 0 && this._rttSamples[0].atMs < cutoffMs)
|
|
1959
|
+
this._rttSamples.shift();
|
|
1960
|
+
this._rttSamples.push({ atMs: nowMs, rttMs: nowMs - sentAtMs });
|
|
1961
|
+
}
|
|
1962
|
+
// -- IRpcObject --
|
|
1963
|
+
reconnect() {
|
|
1890
1964
|
}
|
|
1891
|
-
|
|
1892
|
-
if (this.
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1965
|
+
disconnect() {
|
|
1966
|
+
if (this._ended) return;
|
|
1967
|
+
this._ended = true;
|
|
1968
|
+
this._abortController.abort();
|
|
1969
|
+
if (!this._started.isCompleted) {
|
|
1970
|
+
this._started.resolve();
|
|
1896
1971
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
nextIndex,
|
|
1909
|
-
hostId
|
|
1910
|
-
);
|
|
1972
|
+
if (this._whenAckReady) {
|
|
1973
|
+
this._whenAckReady.resolve();
|
|
1974
|
+
this._whenAckReady = null;
|
|
1975
|
+
}
|
|
1976
|
+
this.peer.sharedObjects.unregister(this);
|
|
1977
|
+
const iterator = this._iterator;
|
|
1978
|
+
if (!iterator) return;
|
|
1979
|
+
if (!this.sourceUsesAbortSignal) {
|
|
1980
|
+
this._iterator = null;
|
|
1981
|
+
this._forceCloseIterator(iterator);
|
|
1982
|
+
return;
|
|
1911
1983
|
}
|
|
1984
|
+
setTimeout(() => {
|
|
1985
|
+
if (this._iterator === iterator) {
|
|
1986
|
+
this._iterator = null;
|
|
1987
|
+
this._forceCloseIterator(iterator);
|
|
1988
|
+
}
|
|
1989
|
+
}, RpcStream.disconnectGracePeriodMs);
|
|
1912
1990
|
}
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
this.id.localId,
|
|
1920
|
-
this.id.hostId
|
|
1921
|
-
);
|
|
1991
|
+
_forceCloseIterator(iterator) {
|
|
1992
|
+
try {
|
|
1993
|
+
const ret = iterator.return?.(void 0);
|
|
1994
|
+
if (ret) void ret.catch(() => {
|
|
1995
|
+
});
|
|
1996
|
+
} catch {
|
|
1922
1997
|
}
|
|
1923
1998
|
}
|
|
1924
1999
|
};
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
return value
|
|
2000
|
+
var _endItem = { kind: "end" };
|
|
2001
|
+
function _clamp(value, min, max) {
|
|
2002
|
+
return value < min ? min : value > max ? max : value;
|
|
1928
2003
|
}
|
|
1929
|
-
function
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
const
|
|
1933
|
-
if (
|
|
1934
|
-
|
|
1935
|
-
}
|
|
1936
|
-
return value;
|
|
1937
|
-
}
|
|
1938
|
-
if (Array.isArray(value)) {
|
|
1939
|
-
for (let i = 0; i < value.length; i++) {
|
|
1940
|
-
value[i] = resolveStreamRefs(value[i], peer);
|
|
1941
|
-
}
|
|
1942
|
-
return value;
|
|
1943
|
-
}
|
|
1944
|
-
if (typeof value === "object") {
|
|
1945
|
-
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer)
|
|
1946
|
-
return value;
|
|
1947
|
-
for (const key of Object.keys(value)) {
|
|
1948
|
-
value[key] = resolveStreamRefs(
|
|
1949
|
-
value[key],
|
|
1950
|
-
peer
|
|
1951
|
-
);
|
|
1952
|
-
}
|
|
1953
|
-
return value;
|
|
2004
|
+
function _compactBufferedUnsentSuffix(buffer, firstUnsentIndex, canSkipTo) {
|
|
2005
|
+
let skipToIndex = -1;
|
|
2006
|
+
for (let i = firstUnsentIndex; i < buffer.count; i++) {
|
|
2007
|
+
const item = buffer.get(i);
|
|
2008
|
+
if (item.kind === "value" && canSkipTo(item.value))
|
|
2009
|
+
skipToIndex = i;
|
|
1954
2010
|
}
|
|
1955
|
-
|
|
2011
|
+
if (skipToIndex <= firstUnsentIndex)
|
|
2012
|
+
return { newIndex: firstUnsentIndex, skipped: 0 };
|
|
2013
|
+
const cutFrom = firstUnsentIndex;
|
|
2014
|
+
const cutTo = skipToIndex;
|
|
2015
|
+
const items = buffer.toArray();
|
|
2016
|
+
buffer.clear();
|
|
2017
|
+
for (let i = 0; i < cutFrom; i++)
|
|
2018
|
+
buffer.pushTail(items[i]);
|
|
2019
|
+
for (let i = cutTo; i < items.length; i++)
|
|
2020
|
+
buffer.pushTail(items[i]);
|
|
2021
|
+
return { newIndex: firstUnsentIndex, skipped: skipToIndex - firstUnsentIndex };
|
|
1956
2022
|
}
|
|
1957
2023
|
|
|
1958
|
-
// src/
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
);
|
|
2024
|
+
// src/rpc-stream.ts
|
|
2025
|
+
var { warnLog: warnLog6 } = getLogs("RpcStream");
|
|
2026
|
+
var DEFAULT_ACK_PERIOD2 = 30;
|
|
2027
|
+
var DEFAULT_ACK_ADVANCE2 = 61;
|
|
2028
|
+
function parseStreamRef(value) {
|
|
2029
|
+
if (typeof value === "string") {
|
|
2030
|
+
const parts = value.split(",");
|
|
2031
|
+
if (parts.length < 4 || parts.length > 6) return null;
|
|
2032
|
+
const hostId = parts[0];
|
|
2033
|
+
const localId = parseInt(parts[1], 10);
|
|
2034
|
+
const ackPeriod = parseInt(parts[2], 10);
|
|
2035
|
+
const ackAdvance = parseInt(parts[3], 10);
|
|
2036
|
+
if (isNaN(localId) || isNaN(ackPeriod) || isNaN(ackAdvance))
|
|
2037
|
+
return null;
|
|
2038
|
+
const allowReconnect = parts.length < 5 || parts[4] !== "0";
|
|
2039
|
+
const isRealTime = parts.length >= 6 && parts[5] === "1";
|
|
2040
|
+
return { hostId, localId, ackPeriod, ackAdvance, allowReconnect, isRealTime };
|
|
1966
2041
|
}
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
resolveStreamRefs(args[1], peer)
|
|
2048
|
-
);
|
|
2049
|
-
break;
|
|
2050
|
-
}
|
|
2051
|
-
case RpcSystemCalls.batch: {
|
|
2052
|
-
const stream = peer.remoteObjects.get(relatedId);
|
|
2053
|
-
if (!stream)
|
|
2054
|
-
debugLog?.log(`$sys.B: no stream for relatedId=${relatedId}`);
|
|
2055
|
-
else if (args.length > 2)
|
|
2056
|
-
stream.onEnd(args[0], polymorphicPayloadError(method, args.length, 2));
|
|
2057
|
-
else if (Array.isArray(args[1])) {
|
|
2058
|
-
const items = args[1];
|
|
2059
|
-
for (let i = 0; i < items.length; i++)
|
|
2060
|
-
items[i] = resolveStreamRefs(items[i], peer);
|
|
2061
|
-
stream.onBatch(args[0], items);
|
|
2062
|
-
}
|
|
2063
|
-
break;
|
|
2064
|
-
}
|
|
2065
|
-
case RpcSystemCalls.end: {
|
|
2066
|
-
const stream = peer.remoteObjects.get(relatedId);
|
|
2067
|
-
if (!stream) {
|
|
2068
|
-
debugLog?.log(`$sys.End: no stream for relatedId=${relatedId}`);
|
|
2069
|
-
} else {
|
|
2070
|
-
const errorInfo = args[1];
|
|
2071
|
-
const typeName = extractTypeName(errorInfo?.TypeRef ?? errorInfo?.typeRef);
|
|
2072
|
-
const msg = errorInfo?.Message ?? errorInfo?.message;
|
|
2073
|
-
let error = null;
|
|
2074
|
-
if (typeName || msg) {
|
|
2075
|
-
error = new Error(msg ?? typeName ?? "RPC stream error");
|
|
2076
|
-
if (typeName)
|
|
2077
|
-
error.name = typeName;
|
|
2078
|
-
}
|
|
2079
|
-
stream.onEnd(args[0], error);
|
|
2080
|
-
}
|
|
2081
|
-
break;
|
|
2082
|
-
}
|
|
2083
|
-
case RpcSystemCalls.ack: {
|
|
2084
|
-
const sender = peer.sharedObjects.get(relatedId);
|
|
2085
|
-
if (sender)
|
|
2086
|
-
sender.onAck(args[0], args[1]);
|
|
2087
|
-
else if (peer.connection !== void 0)
|
|
2088
|
-
peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, [relatedId]);
|
|
2089
|
-
break;
|
|
2090
|
-
}
|
|
2091
|
-
case RpcSystemCalls.ackEnd: {
|
|
2092
|
-
const sender = peer.sharedObjects.get(relatedId);
|
|
2093
|
-
if (sender)
|
|
2094
|
-
sender.onAckEnd(args[0]);
|
|
2095
|
-
else if (peer.connection !== void 0)
|
|
2096
|
-
peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, [relatedId]);
|
|
2097
|
-
break;
|
|
2098
|
-
}
|
|
2099
|
-
case RpcSystemCalls.disconnect: {
|
|
2100
|
-
const ids = args[0];
|
|
2101
|
-
if (Array.isArray(ids)) {
|
|
2102
|
-
for (const id of ids) {
|
|
2103
|
-
const remoteObj = peer.remoteObjects.get(id);
|
|
2104
|
-
if (remoteObj && typeof remoteObj.disconnect === "function") {
|
|
2105
|
-
remoteObj.disconnect();
|
|
2106
|
-
}
|
|
2107
|
-
const sharedObj = peer.sharedObjects.get(id);
|
|
2108
|
-
if (sharedObj && typeof sharedObj.disconnect === "function") {
|
|
2109
|
-
sharedObj.disconnect();
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2113
|
-
break;
|
|
2114
|
-
}
|
|
2042
|
+
if (typeof value === "object" && value !== null) {
|
|
2043
|
+
const obj = value;
|
|
2044
|
+
const serializedId = obj.SerializedId;
|
|
2045
|
+
if (!Array.isArray(serializedId) || serializedId.length < 2)
|
|
2046
|
+
return null;
|
|
2047
|
+
const rawHostId = serializedId[0];
|
|
2048
|
+
const hostId = String(rawHostId);
|
|
2049
|
+
const localId = Number(serializedId[1]);
|
|
2050
|
+
const ackPeriod = Number(obj.AckPeriod ?? 256);
|
|
2051
|
+
const ackAdvance = Number(obj.AckAdvance ?? 128);
|
|
2052
|
+
const allowReconnect = obj.AllowReconnect !== false;
|
|
2053
|
+
const isRealTime = obj.IsRealTime === true;
|
|
2054
|
+
return { hostId, localId, ackPeriod, ackAdvance, allowReconnect, isRealTime };
|
|
2055
|
+
}
|
|
2056
|
+
return null;
|
|
2057
|
+
}
|
|
2058
|
+
var RpcStream = class _RpcStream {
|
|
2059
|
+
/**
|
|
2060
|
+
* Grace period (ms) given to a factory-form local source to honor its
|
|
2061
|
+
* AbortSignal and exit naturally before the sender force-closes the
|
|
2062
|
+
* iterator via `iterator.return()`.
|
|
2063
|
+
*
|
|
2064
|
+
* Only applies to sources provided as `(abortSignal) => AsyncIterable<T>`.
|
|
2065
|
+
* Plain `AsyncIterable<T>` sources are force-closed immediately on disconnect.
|
|
2066
|
+
*/
|
|
2067
|
+
static disconnectGracePeriodMs = 100;
|
|
2068
|
+
id;
|
|
2069
|
+
kind;
|
|
2070
|
+
allowReconnect;
|
|
2071
|
+
isRealTime;
|
|
2072
|
+
canSkipTo;
|
|
2073
|
+
ackPeriod;
|
|
2074
|
+
ackAdvance;
|
|
2075
|
+
/**
|
|
2076
|
+
* Local-only ring buffer capacity hint for the sender. Undefined means
|
|
2077
|
+
* "use ackAdvance as the buffer size". Not part of the wire ref.
|
|
2078
|
+
*/
|
|
2079
|
+
bufferSize;
|
|
2080
|
+
// Local (origin) mode
|
|
2081
|
+
localSource;
|
|
2082
|
+
// Remote (target) mode
|
|
2083
|
+
peer;
|
|
2084
|
+
// Local-only state
|
|
2085
|
+
_sender;
|
|
2086
|
+
_toRefValue;
|
|
2087
|
+
_whenSent;
|
|
2088
|
+
// Remote-only state
|
|
2089
|
+
_buffer = new import_denque.default();
|
|
2090
|
+
_nextExpectedIndex = 0;
|
|
2091
|
+
_consumerWaiting = null;
|
|
2092
|
+
_completed = false;
|
|
2093
|
+
_completionError = null;
|
|
2094
|
+
_disposed = false;
|
|
2095
|
+
_started = false;
|
|
2096
|
+
_iterating = false;
|
|
2097
|
+
_ackSentUpTo = 0;
|
|
2098
|
+
_nextConsumedIndex = 0;
|
|
2099
|
+
constructor(sourceOrRef, optionsOrPeer) {
|
|
2100
|
+
if (_isStreamSource(sourceOrRef)) {
|
|
2101
|
+
const options = optionsOrPeer ?? {};
|
|
2102
|
+
this.kind = RpcObjectKind.Local;
|
|
2103
|
+
this.localSource = sourceOrRef;
|
|
2104
|
+
this.id = { hostId: "", localId: 0 };
|
|
2105
|
+
this.allowReconnect = options.allowReconnect ?? true;
|
|
2106
|
+
this.isRealTime = options.isRealTime ?? false;
|
|
2107
|
+
this.canSkipTo = options.canSkipTo ?? (() => true);
|
|
2108
|
+
this.ackPeriod = options.ackPeriod ?? DEFAULT_ACK_PERIOD2;
|
|
2109
|
+
this.ackAdvance = options.ackAdvance ?? DEFAULT_ACK_ADVANCE2;
|
|
2110
|
+
this.bufferSize = options.bufferSize;
|
|
2111
|
+
} else {
|
|
2112
|
+
const ref = sourceOrRef;
|
|
2113
|
+
const peer = optionsOrPeer;
|
|
2114
|
+
this.kind = RpcObjectKind.Remote;
|
|
2115
|
+
this.id = { hostId: ref.hostId, localId: ref.localId };
|
|
2116
|
+
this.peer = peer;
|
|
2117
|
+
this.allowReconnect = ref.allowReconnect;
|
|
2118
|
+
this.isRealTime = ref.isRealTime;
|
|
2119
|
+
this.canSkipTo = () => true;
|
|
2120
|
+
this.ackPeriod = ref.ackPeriod;
|
|
2121
|
+
this.ackAdvance = ref.ackAdvance;
|
|
2115
2122
|
}
|
|
2116
2123
|
}
|
|
2117
2124
|
/**
|
|
2118
|
-
*
|
|
2119
|
-
*
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2125
|
+
* For local streams: resolves when the sender finishes pumping (after `toRef()` was called).
|
|
2126
|
+
* Rejects if the source throws. Throws if `toRef()` hasn't been called yet.
|
|
2127
|
+
*/
|
|
2128
|
+
get whenSent() {
|
|
2129
|
+
if (!this._whenSent)
|
|
2130
|
+
throw new Error("toRef() must be called before awaiting whenSent.");
|
|
2131
|
+
return this._whenSent;
|
|
2132
|
+
}
|
|
2133
|
+
/** Local sender backing this stream after `toRef()` has been called.
|
|
2134
|
+
* `undefined` for remote streams or before `toRef()`. */
|
|
2135
|
+
get sender() {
|
|
2136
|
+
return this._sender;
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* For local streams: bind to a peer, create and register an `RpcStreamSender`,
|
|
2140
|
+
* start pumping items from the local source in the background, and return the
|
|
2141
|
+
* stream reference.
|
|
2123
2142
|
*
|
|
2124
|
-
*
|
|
2125
|
-
*
|
|
2126
|
-
*
|
|
2143
|
+
* For binary peers the ref is a MessagePack-compatible object
|
|
2144
|
+
* (`{ SerializedId, AckPeriod, AckAdvance, AllowReconnect }`);
|
|
2145
|
+
* for text peers it is a comma-separated string.
|
|
2146
|
+
*
|
|
2147
|
+
* Can only be called once per stream.
|
|
2127
2148
|
*/
|
|
2128
|
-
|
|
2129
|
-
if (
|
|
2130
|
-
|
|
2131
|
-
if (
|
|
2132
|
-
|
|
2133
|
-
peer.
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2149
|
+
toRef(peer) {
|
|
2150
|
+
if (this.kind !== RpcObjectKind.Local)
|
|
2151
|
+
throw new Error("toRef() is only valid for local streams.");
|
|
2152
|
+
if (this._sender) {
|
|
2153
|
+
if (this._sender.peer !== peer)
|
|
2154
|
+
throw new Error("toRef() was already called with a different peer.");
|
|
2155
|
+
return this._toRefValue;
|
|
2156
|
+
}
|
|
2157
|
+
const isFactory = typeof this.localSource === "function";
|
|
2158
|
+
const sender = new RpcStreamSender(
|
|
2159
|
+
peer,
|
|
2160
|
+
this.ackPeriod,
|
|
2161
|
+
this.ackAdvance,
|
|
2162
|
+
this.allowReconnect,
|
|
2163
|
+
this.isRealTime,
|
|
2164
|
+
this.canSkipTo,
|
|
2165
|
+
isFactory,
|
|
2166
|
+
this.bufferSize
|
|
2167
|
+
);
|
|
2168
|
+
peer.sharedObjects.register(sender);
|
|
2169
|
+
this._sender = sender;
|
|
2170
|
+
const source = isFactory ? this.localSource(sender.abortSignal) : this.localSource;
|
|
2171
|
+
this._whenSent = sender.writeFrom(source);
|
|
2172
|
+
this._toRefValue = peer.serializationFormat.isBinary ? {
|
|
2173
|
+
SerializedId: [sender.id.hostId, sender.id.localId],
|
|
2174
|
+
AckPeriod: sender.ackPeriod,
|
|
2175
|
+
AckAdvance: sender.ackAdvance,
|
|
2176
|
+
AllowReconnect: sender.allowReconnect
|
|
2177
|
+
} : sender.toRef();
|
|
2178
|
+
return this._toRefValue;
|
|
2179
|
+
}
|
|
2180
|
+
/** Called by system call handler when a single item arrives ($sys.I). */
|
|
2181
|
+
onItem(index, item) {
|
|
2182
|
+
if (this._disposed || this._completed) return;
|
|
2183
|
+
if (!_isValidIndex(index)) {
|
|
2184
|
+
this._failProtocol(`invalid $sys.I index: ${String(index)}`);
|
|
2140
2185
|
return;
|
|
2141
2186
|
}
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
for (const [stageKey, rawValue] of Object.entries(stagesRaw)) {
|
|
2148
|
-
const bytes = _toBytes(rawValue);
|
|
2149
|
-
if (bytes === null) continue;
|
|
2150
|
-
const completedStage = parseInt(stageKey, 10) || 0;
|
|
2151
|
-
const callIds = IncreasingSeqCompressor.deserialize(bytes);
|
|
2152
|
-
for (const callId of callIds) {
|
|
2153
|
-
const call = peer.inboundCalls.get(callId);
|
|
2154
|
-
if (call === void 0)
|
|
2155
|
-
unknownSet.add(callId);
|
|
2156
|
-
else if (completedStage < 1)
|
|
2157
|
-
call.resendResult();
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
unknownIds = [...unknownSet].sort((a, b) => a - b);
|
|
2187
|
+
if (index > this._nextExpectedIndex) {
|
|
2188
|
+
warnLog6?.log(`item index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
|
|
2189
|
+
if (!this.allowReconnect) {
|
|
2190
|
+
this._complete(this._gapError(index));
|
|
2191
|
+
return;
|
|
2161
2192
|
}
|
|
2162
|
-
|
|
2193
|
+
this._sendAck(this._nextExpectedIndex, true);
|
|
2194
|
+
return;
|
|
2163
2195
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2196
|
+
if (index < this._nextExpectedIndex) {
|
|
2197
|
+
this._maybeSendAck(Math.min(index + 1, this._nextConsumedIndex));
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
if (this._isBeyondAckWindow(index + 1)) {
|
|
2201
|
+
this._failProtocol(this._ackWindowError(index + 1));
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
this._buffer.push(item);
|
|
2205
|
+
this._nextExpectedIndex = index + 1;
|
|
2206
|
+
this._notifyConsumer();
|
|
2167
2207
|
}
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2208
|
+
/** Called by system call handler when a batch arrives ($sys.B). */
|
|
2209
|
+
onBatch(index, items) {
|
|
2210
|
+
if (this._disposed || this._completed) return;
|
|
2211
|
+
if (!_isValidIndex(index) || !_isValidIndex(items.length)) {
|
|
2212
|
+
this._failProtocol(`invalid $sys.B index/length: ${String(index)}/${String(items.length)}`);
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
if (index > this._nextExpectedIndex) {
|
|
2216
|
+
warnLog6?.log(`batch index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
|
|
2217
|
+
if (!this.allowReconnect) {
|
|
2218
|
+
this._complete(this._gapError(index));
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
this._sendAck(this._nextExpectedIndex, true);
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
if (index < this._nextExpectedIndex) {
|
|
2225
|
+
this._maybeSendAck(Math.min(index + items.length, this._nextConsumedIndex));
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
if (this._isBeyondAckWindow(index + items.length)) {
|
|
2229
|
+
this._failProtocol(this._ackWindowError(index + items.length));
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
for (const item of items) this._buffer.push(item);
|
|
2233
|
+
this._nextExpectedIndex = index + items.length;
|
|
2234
|
+
this._notifyConsumer();
|
|
2180
2235
|
}
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
if (
|
|
2185
|
-
|
|
2186
|
-
return
|
|
2236
|
+
/** Called by system call handler when the stream ends ($sys.End). */
|
|
2237
|
+
onEnd(index, error) {
|
|
2238
|
+
if (this._disposed || this._completed) return;
|
|
2239
|
+
if (!_isValidIndex(index)) {
|
|
2240
|
+
this._failProtocol(`invalid $sys.End index: ${String(index)}`);
|
|
2241
|
+
return;
|
|
2187
2242
|
}
|
|
2188
|
-
|
|
2243
|
+
if (index > this._nextExpectedIndex) {
|
|
2244
|
+
warnLog6?.log(`end index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
|
|
2245
|
+
if (!this.allowReconnect) {
|
|
2246
|
+
this._complete(this._gapError(index));
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
this._sendAck(this._nextExpectedIndex, true);
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
if (index < this._nextExpectedIndex) {
|
|
2253
|
+
this._maybeSendAck(Math.min(index, this._nextConsumedIndex));
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
this._complete(error);
|
|
2189
2257
|
}
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
} catch {
|
|
2198
|
-
return null;
|
|
2258
|
+
// -- IRpcObject --
|
|
2259
|
+
reconnect() {
|
|
2260
|
+
if (this.kind === RpcObjectKind.Local) return;
|
|
2261
|
+
if (this.allowReconnect) {
|
|
2262
|
+
this._sendAck(this._nextExpectedIndex, true);
|
|
2263
|
+
} else {
|
|
2264
|
+
this.disconnect();
|
|
2199
2265
|
}
|
|
2200
2266
|
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
var { errorLog } = getLogs("RpcSystemCallSender");
|
|
2206
|
-
var RpcSystemCallSender = class _RpcSystemCallSender {
|
|
2207
|
-
// Reusable envelopes — one per hot-path method.
|
|
2208
|
-
_itemEnv = {
|
|
2209
|
-
Method: RpcSystemCalls.item,
|
|
2210
|
-
RelatedId: 0
|
|
2211
|
-
};
|
|
2212
|
-
_batchEnv = {
|
|
2213
|
-
Method: RpcSystemCalls.batch,
|
|
2214
|
-
RelatedId: 0
|
|
2215
|
-
};
|
|
2216
|
-
_endEnv = {
|
|
2217
|
-
Method: RpcSystemCalls.end,
|
|
2218
|
-
RelatedId: 0
|
|
2219
|
-
};
|
|
2220
|
-
_ackEnv = {
|
|
2221
|
-
Method: RpcSystemCalls.ack,
|
|
2222
|
-
RelatedId: 0
|
|
2223
|
-
};
|
|
2224
|
-
_ackEndEnv = {
|
|
2225
|
-
Method: RpcSystemCalls.ackEnd,
|
|
2226
|
-
RelatedId: 0
|
|
2227
|
-
};
|
|
2228
|
-
// Reusable arg arrays.
|
|
2229
|
-
_itemArgs = [0, null];
|
|
2230
|
-
_batchArgs = [0, null];
|
|
2231
|
-
_endArgs = [0, { TypeRef: "", Message: "" }];
|
|
2232
|
-
_ackArgs = [0, ""];
|
|
2233
|
-
_ackEndArgs = [""];
|
|
2234
|
-
static NO_ERROR = Object.freeze({ TypeRef: "", Message: "" });
|
|
2235
|
-
registry;
|
|
2236
|
-
_send(conn, format, envelope, args) {
|
|
2237
|
-
const wireData = format.serializeMessage(
|
|
2238
|
-
envelope,
|
|
2239
|
-
args,
|
|
2240
|
-
conn.encoder,
|
|
2241
|
-
this.registry
|
|
2242
|
-
);
|
|
2243
|
-
if (typeof wireData === "string") {
|
|
2244
|
-
conn.send(wireData);
|
|
2245
|
-
} else {
|
|
2246
|
-
conn.sendBinary(wireData);
|
|
2267
|
+
disconnect() {
|
|
2268
|
+
if (this.kind === RpcObjectKind.Local) {
|
|
2269
|
+
this._sender?.disconnect();
|
|
2270
|
+
return;
|
|
2247
2271
|
}
|
|
2272
|
+
if (this._completed) return;
|
|
2273
|
+
this._completed = true;
|
|
2274
|
+
this._completionError = new Error("Peer disconnected.");
|
|
2275
|
+
this._notifyConsumer();
|
|
2248
2276
|
}
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2277
|
+
// -- AsyncIterable --
|
|
2278
|
+
[Symbol.asyncIterator]() {
|
|
2279
|
+
if (this.kind === RpcObjectKind.Local) {
|
|
2280
|
+
const source = typeof this.localSource === "function" ? this.localSource(new AbortController().signal) : this.localSource;
|
|
2281
|
+
return source[Symbol.asyncIterator]();
|
|
2282
|
+
}
|
|
2283
|
+
if (this._iterating)
|
|
2284
|
+
throw new Error("RpcStream can only be iterated once.");
|
|
2285
|
+
this._iterating = true;
|
|
2286
|
+
return {
|
|
2287
|
+
next: async () => {
|
|
2288
|
+
if (!this._started) {
|
|
2289
|
+
this._started = true;
|
|
2290
|
+
this.peer.remoteObjects.register(this);
|
|
2291
|
+
this._sendAck(0, true);
|
|
2292
|
+
}
|
|
2293
|
+
this._maybeSendAck(this._nextConsumedIndex);
|
|
2294
|
+
for (; ; ) {
|
|
2295
|
+
if (!this._buffer.isEmpty()) {
|
|
2296
|
+
const value = this._buffer.shift();
|
|
2297
|
+
this._nextConsumedIndex++;
|
|
2298
|
+
return { value, done: false };
|
|
2299
|
+
}
|
|
2300
|
+
if (this._completed) {
|
|
2301
|
+
this.dispose();
|
|
2302
|
+
if (this._completionError) throw this._completionError;
|
|
2303
|
+
return { value: void 0, done: true };
|
|
2304
|
+
}
|
|
2305
|
+
this._consumerWaiting = new import_core6.PromiseSource();
|
|
2306
|
+
await this._consumerWaiting;
|
|
2307
|
+
}
|
|
2308
|
+
},
|
|
2309
|
+
return: () => {
|
|
2310
|
+
if (this._started && !this._completed)
|
|
2311
|
+
this._sendAckEnd();
|
|
2312
|
+
this.dispose();
|
|
2313
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
2314
|
+
}
|
|
2256
2315
|
};
|
|
2257
|
-
this._send(conn, format, { Method: RpcSystemCalls.handshake }, [
|
|
2258
|
-
handshakeArg
|
|
2259
|
-
]);
|
|
2260
2316
|
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
[result]
|
|
2268
|
-
);
|
|
2269
|
-
} catch (e) {
|
|
2270
|
-
errorLog?.log(`Failed to send Ok response for call #${relatedId}`, e);
|
|
2271
|
-
this.error(conn, format, relatedId, e);
|
|
2317
|
+
dispose() {
|
|
2318
|
+
if (this._disposed) return;
|
|
2319
|
+
this._disposed = true;
|
|
2320
|
+
this._completed = true;
|
|
2321
|
+
if (this.kind === RpcObjectKind.Remote) {
|
|
2322
|
+
this.peer.remoteObjects.unregister(this);
|
|
2272
2323
|
}
|
|
2324
|
+
this._notifyConsumer();
|
|
2273
2325
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2326
|
+
// -- Private --
|
|
2327
|
+
// Mirrors .NET RpcStream.CloseFromLock: acknowledge the end to the sender
|
|
2328
|
+
// (so its RpcSharedStream is released) and unregister immediately.
|
|
2329
|
+
_complete(error) {
|
|
2330
|
+
if (this._completed) return;
|
|
2331
|
+
this._completed = true;
|
|
2332
|
+
this._completionError = error;
|
|
2333
|
+
this._sendAckEnd();
|
|
2334
|
+
if (this.kind === RpcObjectKind.Remote)
|
|
2335
|
+
this.peer.remoteObjects.unregister(this);
|
|
2336
|
+
this._notifyConsumer();
|
|
2281
2337
|
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
});
|
|
2338
|
+
// Terminates the stream the way the non-reconnectable gap path does: the
|
|
2339
|
+
// consumer sees the error and the sender gets $sys.AckEnd, so nothing is
|
|
2340
|
+
// left pumping into a stream we no longer read.
|
|
2341
|
+
_failProtocol(message) {
|
|
2342
|
+
warnLog6?.log(`localId=${this.id.localId}: ${message}`);
|
|
2343
|
+
this._complete(new Error(message));
|
|
2287
2344
|
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2345
|
+
// The remote advertised `ackAdvance` as the furthest it may run ahead of our
|
|
2346
|
+
// last acknowledged index. The sending side is the only place that window
|
|
2347
|
+
// was ever enforced, which left a flooding remote free to grow our buffer
|
|
2348
|
+
// without bound (I6) — so we enforce our half of the contract here.
|
|
2349
|
+
_isBeyondAckWindow(nextExpectedIndex) {
|
|
2350
|
+
return this.ackAdvance > 0 && nextExpectedIndex > this._ackSentUpTo + this.ackAdvance;
|
|
2292
2351
|
}
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
// instead of hanging. Mirrors .NET RpcSystemCallSender.Disconnect.
|
|
2296
|
-
disconnect(conn, format, localIds) {
|
|
2297
|
-
this._send(conn, format, { Method: RpcSystemCalls.disconnect }, [
|
|
2298
|
-
localIds
|
|
2299
|
-
]);
|
|
2352
|
+
_ackWindowError(nextExpectedIndex) {
|
|
2353
|
+
return `Stream ran past its ack window: reached ${nextExpectedIndex}, acked ${this._ackSentUpTo}, ackAdvance ${this.ackAdvance}`;
|
|
2300
2354
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
this._send(conn, format, this._itemEnv, this._itemArgs);
|
|
2306
|
-
this._itemArgs[1] = null;
|
|
2355
|
+
_gapError(index) {
|
|
2356
|
+
return new Error(
|
|
2357
|
+
`Stream gap at index ${index} (expected ${this._nextExpectedIndex}); reconnect not allowed`
|
|
2358
|
+
);
|
|
2307
2359
|
}
|
|
2308
|
-
|
|
2309
|
-
this.
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
this._batchArgs[1] = null;
|
|
2360
|
+
_notifyConsumer() {
|
|
2361
|
+
if (this._consumerWaiting !== null) {
|
|
2362
|
+
this._consumerWaiting.resolve();
|
|
2363
|
+
this._consumerWaiting = null;
|
|
2364
|
+
}
|
|
2314
2365
|
}
|
|
2315
|
-
|
|
2316
|
-
this.
|
|
2317
|
-
this.
|
|
2318
|
-
|
|
2319
|
-
|
|
2366
|
+
_maybeSendAck(nextIndex) {
|
|
2367
|
+
if (this.ackPeriod <= 0) return;
|
|
2368
|
+
const threshold = this._ackSentUpTo + this.ackPeriod;
|
|
2369
|
+
if (nextIndex >= threshold) {
|
|
2370
|
+
this._sendAck(nextIndex, false);
|
|
2371
|
+
}
|
|
2320
2372
|
}
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
this.
|
|
2324
|
-
this.
|
|
2325
|
-
|
|
2373
|
+
static _emptyGuid = "00000000-0000-0000-0000-000000000000";
|
|
2374
|
+
_sendAck(nextIndex, mustReset) {
|
|
2375
|
+
this._ackSentUpTo = nextIndex;
|
|
2376
|
+
const conn = this.peer.isConnected ? this.peer.connection : void 0;
|
|
2377
|
+
if (conn) {
|
|
2378
|
+
const hostId = mustReset ? this.id.hostId : _RpcStream._emptyGuid;
|
|
2379
|
+
this.peer.hub.systemCallSender.ack(
|
|
2380
|
+
conn,
|
|
2381
|
+
this.peer.serializationFormat,
|
|
2382
|
+
this.id.localId,
|
|
2383
|
+
nextIndex,
|
|
2384
|
+
hostId
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2326
2387
|
}
|
|
2327
|
-
|
|
2328
|
-
this.
|
|
2329
|
-
|
|
2330
|
-
|
|
2388
|
+
_sendAckEnd() {
|
|
2389
|
+
const conn = this.peer.isConnected ? this.peer.connection : void 0;
|
|
2390
|
+
if (conn) {
|
|
2391
|
+
this.peer.hub.systemCallSender.ackEnd(
|
|
2392
|
+
conn,
|
|
2393
|
+
this.peer.serializationFormat,
|
|
2394
|
+
this.id.localId,
|
|
2395
|
+
this.id.hostId
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2331
2398
|
}
|
|
2332
2399
|
};
|
|
2400
|
+
function _isValidIndex(index) {
|
|
2401
|
+
return Number.isSafeInteger(index) && index >= 0;
|
|
2402
|
+
}
|
|
2403
|
+
function _isStreamSource(value) {
|
|
2404
|
+
if (typeof value === "function") return true;
|
|
2405
|
+
return value !== null && typeof value === "object" && Symbol.asyncIterator in value;
|
|
2406
|
+
}
|
|
2407
|
+
function toRpcStream(value, peer) {
|
|
2408
|
+
const ref = parseStreamRef(value);
|
|
2409
|
+
if (ref === null)
|
|
2410
|
+
return null;
|
|
2411
|
+
return new RpcStream(ref, peer);
|
|
2412
|
+
}
|
|
2333
2413
|
|
|
2334
2414
|
// src/rpc-remote-object-tracker.ts
|
|
2335
2415
|
var RpcRemoteObjectTracker = class {
|
|
@@ -2561,33 +2641,68 @@ function isMemoryPackKey(key) {
|
|
|
2561
2641
|
return key.startsWith("mempack");
|
|
2562
2642
|
}
|
|
2563
2643
|
|
|
2644
|
+
// src/reconnect-proof.ts
|
|
2645
|
+
var textEncoder2 = new TextEncoder();
|
|
2646
|
+
function isReconnectProofSupported() {
|
|
2647
|
+
return typeof crypto !== "undefined" && crypto.subtle !== void 0;
|
|
2648
|
+
}
|
|
2649
|
+
async function computeReconnectProof(secret, clientId, counterText) {
|
|
2650
|
+
const key = await crypto.subtle.importKey(
|
|
2651
|
+
"raw",
|
|
2652
|
+
textEncoder2.encode(secret),
|
|
2653
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
2654
|
+
false,
|
|
2655
|
+
["sign"]
|
|
2656
|
+
);
|
|
2657
|
+
const signature = await crypto.subtle.sign(
|
|
2658
|
+
"HMAC",
|
|
2659
|
+
key,
|
|
2660
|
+
textEncoder2.encode(`${clientId}
|
|
2661
|
+
${counterText}`)
|
|
2662
|
+
);
|
|
2663
|
+
return base64UrlEncode(new Uint8Array(signature));
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2564
2666
|
// src/rpc-peer.ts
|
|
2565
2667
|
function yieldToEventLoop() {
|
|
2566
2668
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
2567
2669
|
}
|
|
2568
2670
|
var RPC_CLOSE_CODE_UNSUPPORTED_FORMAT = 4001;
|
|
2569
|
-
var { debugLog: debugLog2, infoLog, warnLog:
|
|
2570
|
-
var defaultConnectionUrlResolver = (peer) => {
|
|
2671
|
+
var { debugLog: debugLog2, infoLog, warnLog: warnLog7, errorLog: errorLog2 } = getLogs("RpcPeer");
|
|
2672
|
+
var defaultConnectionUrlResolver = async (peer) => {
|
|
2571
2673
|
const formatKey = peer.serializationFormat.key;
|
|
2674
|
+
const proof = await peer.computeReconnectProof();
|
|
2572
2675
|
try {
|
|
2573
2676
|
const url = new URL(peer.ref);
|
|
2574
2677
|
url.searchParams.set("clientId", peer.clientId);
|
|
2575
2678
|
url.searchParams.set("f", formatKey);
|
|
2679
|
+
if (proof !== void 0) {
|
|
2680
|
+
url.searchParams.set("c", proof.counter);
|
|
2681
|
+
url.searchParams.set("p", proof.proof);
|
|
2682
|
+
}
|
|
2576
2683
|
return url.toString();
|
|
2577
2684
|
} catch {
|
|
2578
2685
|
const sep = peer.ref.includes("?") ? "&" : "?";
|
|
2579
|
-
|
|
2686
|
+
let url = peer.ref + sep + `clientId=${peer.clientId}&f=${formatKey}`;
|
|
2687
|
+
if (proof !== void 0)
|
|
2688
|
+
url += `&c=${proof.counter}&p=${proof.proof}`;
|
|
2689
|
+
return url;
|
|
2580
2690
|
}
|
|
2581
2691
|
};
|
|
2692
|
+
var RedactedParameterNames = ["session", "clientId", "p"];
|
|
2582
2693
|
var sanitizeUrl = (url) => {
|
|
2583
2694
|
try {
|
|
2584
2695
|
const parsed = new URL(url);
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2696
|
+
let isRedacted = false;
|
|
2697
|
+
for (const name of RedactedParameterNames) {
|
|
2698
|
+
if (!parsed.searchParams.has(name))
|
|
2699
|
+
continue;
|
|
2700
|
+
parsed.searchParams.set(name, "<redacted>");
|
|
2701
|
+
isRedacted = true;
|
|
2702
|
+
}
|
|
2703
|
+
return isRedacted ? parsed.toString() : url;
|
|
2589
2704
|
} catch {
|
|
2590
|
-
return url.replace(/([?&]session=)[^&]*/
|
|
2705
|
+
return url.replace(/([?&](?:session|clientId|p)=)[^&]*/gi, "$1<redacted>");
|
|
2591
2706
|
}
|
|
2592
2707
|
};
|
|
2593
2708
|
function parseFormatFromUrl(url) {
|
|
@@ -2836,21 +2951,26 @@ var RpcPeer = class {
|
|
|
2836
2951
|
/** Override in subclasses to handle the remote peer's handshake response. */
|
|
2837
2952
|
_onHandshakeReceived(_handshake) {
|
|
2838
2953
|
}
|
|
2954
|
+
// The try covers dispatch too, not just deserialization: a handler that throws
|
|
2955
|
+
// here would otherwise escape into the transport's onmessage - dropping every
|
|
2956
|
+
// remaining message of the frame, or killing a Node process outright.
|
|
2839
2957
|
_handleMessage(received) {
|
|
2840
|
-
let message;
|
|
2841
|
-
let args;
|
|
2842
2958
|
try {
|
|
2843
|
-
|
|
2844
|
-
message = received.message;
|
|
2845
|
-
args = received.args;
|
|
2846
|
-
} else {
|
|
2847
|
-
const parsed = deserializeMessage(received.raw);
|
|
2848
|
-
message = parsed.message;
|
|
2849
|
-
args = parsed.args;
|
|
2850
|
-
}
|
|
2959
|
+
this._processMessage(received);
|
|
2851
2960
|
} catch (e) {
|
|
2852
2961
|
errorLog2?.log(`'${this.ref}': Failed to process inbound message:`, received, e);
|
|
2853
|
-
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
_processMessage(received) {
|
|
2965
|
+
let message;
|
|
2966
|
+
let args;
|
|
2967
|
+
if (received.kind === "binary") {
|
|
2968
|
+
message = received.message;
|
|
2969
|
+
args = received.args;
|
|
2970
|
+
} else {
|
|
2971
|
+
const parsed = deserializeMessage(received.raw);
|
|
2972
|
+
message = parsed.message;
|
|
2973
|
+
args = parsed.args;
|
|
2854
2974
|
}
|
|
2855
2975
|
const method = message.Method ?? "";
|
|
2856
2976
|
debugLog2?.log(`'${this.ref}': <- ${method}#${message.RelatedId ?? 0}`);
|
|
@@ -2862,12 +2982,14 @@ var RpcPeer = class {
|
|
|
2862
2982
|
RemotePeerId: raw[0],
|
|
2863
2983
|
RemoteHubId: raw[2],
|
|
2864
2984
|
ProtocolVersion: raw[3],
|
|
2865
|
-
Index: raw[4]
|
|
2985
|
+
Index: raw[4],
|
|
2986
|
+
Secret: raw[5]
|
|
2866
2987
|
} : {
|
|
2867
2988
|
RemotePeerId: obj.RemotePeerId ?? obj.remotePeerId,
|
|
2868
2989
|
RemoteHubId: obj.RemoteHubId ?? obj.remoteHubId,
|
|
2869
2990
|
ProtocolVersion: obj.ProtocolVersion ?? obj.protocolVersion,
|
|
2870
|
-
Index: obj.Index ?? obj.index
|
|
2991
|
+
Index: obj.Index ?? obj.index,
|
|
2992
|
+
Secret: obj.Secret ?? obj.secret
|
|
2871
2993
|
};
|
|
2872
2994
|
this._onHandshakeReceived(handshake);
|
|
2873
2995
|
}
|
|
@@ -2981,7 +3103,7 @@ var RpcPeer = class {
|
|
|
2981
3103
|
this._keepAliveWatchdog = setTimeout(() => {
|
|
2982
3104
|
this._keepAliveWatchdog = void 0;
|
|
2983
3105
|
if (this._connection === void 0) return;
|
|
2984
|
-
|
|
3106
|
+
warnLog7?.log(
|
|
2985
3107
|
`'${this.ref}': no keep-alive for ${this.keepAliveTimeoutMs}ms \u2014 force-closing connection`
|
|
2986
3108
|
);
|
|
2987
3109
|
this._connection.close();
|
|
@@ -3029,6 +3151,16 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
3029
3151
|
_lastRemotePeerId;
|
|
3030
3152
|
_pendingHandshake;
|
|
3031
3153
|
_reconnectsAt = 0;
|
|
3154
|
+
/** Server-issued reconnect secret, from the most recent handshake that
|
|
3155
|
+
* carried one. In-memory only — never localStorage/sessionStorage/cookie.
|
|
3156
|
+
* A tab reload mints a new `clientId`, so a persisted secret would be
|
|
3157
|
+
* useless anyway. */
|
|
3158
|
+
_reconnectSecret;
|
|
3159
|
+
/** Incremented once per *connect attempt*, in `computeReconnectProof()`. A
|
|
3160
|
+
* failed attempt still burns its value: the server may have advanced its
|
|
3161
|
+
* own counter at the gate before the connection died, and reusing the
|
|
3162
|
+
* value would then be rejected forever. */
|
|
3163
|
+
_reconnectCounter = 0;
|
|
3032
3164
|
/** Base64url-encoded peer ID — matches .NET's RpcClientPeer.ClientId (Guid.ToBase64Url). */
|
|
3033
3165
|
clientId;
|
|
3034
3166
|
/** Builds the WebSocket URL for this peer. Replace to customize URL construction. */
|
|
@@ -3072,7 +3204,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
3072
3204
|
* @param url Full WebSocket URL — the serialization format is taken from
|
|
3073
3205
|
* the `f=` query parameter (e.g. `wss://host/rpc/ws?f=msgpack6`);
|
|
3074
3206
|
* falls back to the resolver's default when absent. Use
|
|
3075
|
-
* {@link
|
|
3207
|
+
* {@link RpcRefBuilder.forClient} to compose URLs with a format.
|
|
3076
3208
|
* @param mustStart Kick off the reconnect loop automatically at the end of
|
|
3077
3209
|
* construction (default `true`). Pass `false` if you need to
|
|
3078
3210
|
* tweak `reconnectDelayer`, `handshakeTimeoutMs`, or similar
|
|
@@ -3109,6 +3241,28 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
3109
3241
|
start() {
|
|
3110
3242
|
void this.run();
|
|
3111
3243
|
}
|
|
3244
|
+
/** Builds the `c` / `p` connect-URL parameters proving possession of the
|
|
3245
|
+
* server-issued reconnect secret, or `undefined` when there is no secret
|
|
3246
|
+
* yet or WebCrypto is unavailable. Consumes one counter value per proof
|
|
3247
|
+
* produced, so call it exactly once per connect attempt. */
|
|
3248
|
+
async computeReconnectProof() {
|
|
3249
|
+
const secret = this._reconnectSecret;
|
|
3250
|
+
if (secret === void 0)
|
|
3251
|
+
return void 0;
|
|
3252
|
+
if (!isReconnectProofSupported()) {
|
|
3253
|
+
warnReconnectProofUnavailable(this.ref, "crypto.subtle is unavailable (insecure context?)");
|
|
3254
|
+
return void 0;
|
|
3255
|
+
}
|
|
3256
|
+
const counter = (this._reconnectCounter + 1).toString();
|
|
3257
|
+
try {
|
|
3258
|
+
const proof = await computeReconnectProof(secret, this.clientId, counter);
|
|
3259
|
+
this._reconnectCounter++;
|
|
3260
|
+
return { counter, proof };
|
|
3261
|
+
} catch (e) {
|
|
3262
|
+
warnReconnectProofUnavailable(this.ref, `HMAC-SHA256 failed: ${String(e)}`);
|
|
3263
|
+
return void 0;
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3112
3266
|
/** The reconnect loop itself. Exposed as `protected` so subclasses (and
|
|
3113
3267
|
* `start()`) can drive it; public callers should use {@link start}. */
|
|
3114
3268
|
run() {
|
|
@@ -3122,7 +3276,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
3122
3276
|
this._setConnectionState(1 /* Connecting */);
|
|
3123
3277
|
const retryDelay = this.hub.reconnectDelayer.getDelay(this._tryIndex, this._stopController.signal);
|
|
3124
3278
|
if (retryDelay.isLimitExceeded) {
|
|
3125
|
-
|
|
3279
|
+
warnLog7?.log(`'${this.ref}': Reconnect limit exceeded - shutting down`);
|
|
3126
3280
|
this._disposed = true;
|
|
3127
3281
|
break;
|
|
3128
3282
|
}
|
|
@@ -3171,7 +3325,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
3171
3325
|
);
|
|
3172
3326
|
} catch (e) {
|
|
3173
3327
|
if (e instanceof import_core7.TimeoutError) {
|
|
3174
|
-
|
|
3328
|
+
warnLog7?.log(`'${this.ref}': Connect timed out after ${connectTimeoutMs}ms`);
|
|
3175
3329
|
conn.close();
|
|
3176
3330
|
}
|
|
3177
3331
|
throw e;
|
|
@@ -3186,7 +3340,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
3186
3340
|
++this._ownHandshakeIndex
|
|
3187
3341
|
);
|
|
3188
3342
|
} catch (e) {
|
|
3189
|
-
|
|
3343
|
+
warnLog7?.log(`'${this.ref}': Failed to send handshake`, e);
|
|
3190
3344
|
throw e;
|
|
3191
3345
|
}
|
|
3192
3346
|
const handshakeTimeoutMs = this.handshakeTimeoutMs;
|
|
@@ -3199,7 +3353,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
3199
3353
|
);
|
|
3200
3354
|
} catch (e) {
|
|
3201
3355
|
if (e instanceof import_core7.TimeoutError) {
|
|
3202
|
-
|
|
3356
|
+
warnLog7?.log(`'${this.ref}': Handshake timed out after ${handshakeTimeoutMs}ms`);
|
|
3203
3357
|
conn.close();
|
|
3204
3358
|
}
|
|
3205
3359
|
throw e;
|
|
@@ -3207,6 +3361,8 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
3207
3361
|
this._pendingHandshake = void 0;
|
|
3208
3362
|
}
|
|
3209
3363
|
this._remoteHandshakeIndex = remoteHandshake.Index ?? 0;
|
|
3364
|
+
if (remoteHandshake.Secret)
|
|
3365
|
+
this._reconnectSecret = remoteHandshake.Secret;
|
|
3210
3366
|
const isPeerChanged = this._detectPeerChange(remoteHandshake);
|
|
3211
3367
|
infoLog?.log(`'${this.ref}': Handshake succeeded, peerChanged=${isPeerChanged}`);
|
|
3212
3368
|
this._setConnectionState(3 /* Connected */);
|
|
@@ -3219,7 +3375,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
3219
3375
|
this._setConnectionState(0 /* Disconnected */);
|
|
3220
3376
|
if (this._disposed) break;
|
|
3221
3377
|
if (lastCloseCode === RPC_CLOSE_CODE_UNSUPPORTED_FORMAT) {
|
|
3222
|
-
|
|
3378
|
+
warnLog7?.log(`'${this.ref}': Unsupported serialization format - shutting down`);
|
|
3223
3379
|
this.unsupportedFormat.trigger({ reason: lastCloseReason });
|
|
3224
3380
|
this._disposed = true;
|
|
3225
3381
|
break;
|
|
@@ -3465,8 +3621,16 @@ function guidToBase64Url(uuid) {
|
|
|
3465
3621
|
bytes[7] = parseInt(hex.slice(12, 14), 16);
|
|
3466
3622
|
for (let i = 8; i < 16; i++)
|
|
3467
3623
|
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
3468
|
-
|
|
3469
|
-
|
|
3624
|
+
return base64UrlEncode(bytes);
|
|
3625
|
+
}
|
|
3626
|
+
var isReconnectProofUnavailableWarned = false;
|
|
3627
|
+
function warnReconnectProofUnavailable(ref, reason) {
|
|
3628
|
+
if (isReconnectProofUnavailableWarned)
|
|
3629
|
+
return;
|
|
3630
|
+
isReconnectProofUnavailableWarned = true;
|
|
3631
|
+
warnLog7?.log(
|
|
3632
|
+
`'${ref}': ${reason} - connecting without a reconnect proof. This fails if the server requires one (RequireReconnectProof).`
|
|
3633
|
+
);
|
|
3470
3634
|
}
|
|
3471
3635
|
function _toBytes2(value) {
|
|
3472
3636
|
if (value instanceof Uint8Array) return value;
|
|
@@ -3545,7 +3709,7 @@ var RpcClientPeerReconnectDelayer = class extends import_core8.RetryDelayer {
|
|
|
3545
3709
|
};
|
|
3546
3710
|
|
|
3547
3711
|
// src/rpc-service-host.ts
|
|
3548
|
-
var { warnLog:
|
|
3712
|
+
var { warnLog: warnLog8 } = getLogs("RpcServiceHost");
|
|
3549
3713
|
var RpcServiceHost = class {
|
|
3550
3714
|
_methods = /* @__PURE__ */ new Map();
|
|
3551
3715
|
register(def, impl, receiver = impl) {
|
|
@@ -3562,7 +3726,7 @@ var RpcServiceHost = class {
|
|
|
3562
3726
|
async dispatch(wireMethod, args, context) {
|
|
3563
3727
|
const entry = this._methods.get(wireMethod);
|
|
3564
3728
|
if (entry === void 0) {
|
|
3565
|
-
|
|
3729
|
+
warnLog8?.log(`Method not found: ${wireMethod}`);
|
|
3566
3730
|
throw new Error(`Method not found: ${wireMethod}`);
|
|
3567
3731
|
}
|
|
3568
3732
|
if (context !== void 0 && entry.def.callTypeId !== 0) {
|
|
@@ -4070,7 +4234,7 @@ function hashLongWithSeed(input, length, seed) {
|
|
|
4070
4234
|
}
|
|
4071
4235
|
return hashLong(input, length, secret, secret.length);
|
|
4072
4236
|
}
|
|
4073
|
-
var
|
|
4237
|
+
var textEncoder3 = new TextEncoder();
|
|
4074
4238
|
function xxh3_64(input, seed = 0n) {
|
|
4075
4239
|
const len = input.length;
|
|
4076
4240
|
if (len <= XXH3_MIDSIZE_MAX) {
|
|
@@ -4079,10 +4243,10 @@ function xxh3_64(input, seed = 0n) {
|
|
|
4079
4243
|
return hashLongWithSeed(input, len, u64(seed));
|
|
4080
4244
|
}
|
|
4081
4245
|
function xxh3_64str(input, seed = 0n) {
|
|
4082
|
-
return xxh3_64(
|
|
4246
|
+
return xxh3_64(textEncoder3.encode(input), seed);
|
|
4083
4247
|
}
|
|
4084
4248
|
function computeMethodHash(methodName) {
|
|
4085
|
-
const utf8 =
|
|
4249
|
+
const utf8 = textEncoder3.encode(methodName);
|
|
4086
4250
|
const prefixValue = Math.imul(67211, utf8.length) >>> 0;
|
|
4087
4251
|
const prefixed = new Uint8Array(4 + utf8.length);
|
|
4088
4252
|
prefixed[0] = prefixValue & 255;
|
|
@@ -4095,7 +4259,7 @@ function computeMethodHash(methodName) {
|
|
|
4095
4259
|
}
|
|
4096
4260
|
|
|
4097
4261
|
// src/rpc-method-registry.ts
|
|
4098
|
-
var { warnLog:
|
|
4262
|
+
var { warnLog: warnLog9 } = getLogs("RpcMethodRegistry");
|
|
4099
4263
|
var RpcMethodRegistry = class {
|
|
4100
4264
|
_nameToHash = /* @__PURE__ */ new Map();
|
|
4101
4265
|
_hashToName = /* @__PURE__ */ new Map();
|
|
@@ -4105,7 +4269,7 @@ var RpcMethodRegistry = class {
|
|
|
4105
4269
|
const hash = computeMethodHash(methodName);
|
|
4106
4270
|
const existing = this._hashToName.get(hash);
|
|
4107
4271
|
if (existing !== void 0 && existing !== methodName) {
|
|
4108
|
-
|
|
4272
|
+
warnLog9?.log(`hash collision: "${methodName}" and "${existing}" both hash to ${hash}`);
|
|
4109
4273
|
return;
|
|
4110
4274
|
}
|
|
4111
4275
|
this._nameToHash.set(methodName, hash);
|
|
@@ -4138,7 +4302,7 @@ var RpcMethodRegistry = class {
|
|
|
4138
4302
|
};
|
|
4139
4303
|
|
|
4140
4304
|
// src/rpc-hub.ts
|
|
4141
|
-
var { warnLog:
|
|
4305
|
+
var { warnLog: warnLog10 } = getLogs("RpcHub");
|
|
4142
4306
|
var RpcHub = class {
|
|
4143
4307
|
hubId;
|
|
4144
4308
|
peers = /* @__PURE__ */ new Map();
|
|
@@ -4203,7 +4367,7 @@ var RpcHub = class {
|
|
|
4203
4367
|
if (peer !== void 0) {
|
|
4204
4368
|
peer.close();
|
|
4205
4369
|
this.peers.delete(ref);
|
|
4206
|
-
|
|
4370
|
+
warnLog10?.log(`'${ref}': peer is removed from RpcHub`);
|
|
4207
4371
|
}
|
|
4208
4372
|
}
|
|
4209
4373
|
/** Get or create a peer by ref. If the peer does not exist:
|
|
@@ -4321,12 +4485,12 @@ var RpcHub = class {
|
|
|
4321
4485
|
const callArgs = args.slice(0, methodDef.argCount);
|
|
4322
4486
|
const outboundCall = peer.call(wireName, callArgs);
|
|
4323
4487
|
const result = await outboundCall.result;
|
|
4324
|
-
const
|
|
4325
|
-
if (!
|
|
4488
|
+
const stream = toRpcStream(result, peer);
|
|
4489
|
+
if (!stream)
|
|
4326
4490
|
throw new Error(
|
|
4327
4491
|
`Expected stream reference, got: ${JSON.stringify(result)}`
|
|
4328
4492
|
);
|
|
4329
|
-
return
|
|
4493
|
+
return stream;
|
|
4330
4494
|
};
|
|
4331
4495
|
}
|
|
4332
4496
|
const callOptions = methodDef.callTypeId !== 0 || mode !== RpcRemoteExecutionMode.Default || methodDef.timeouts !== void 0 ? { callTypeId: methodDef.callTypeId, remoteExecutionMode: mode, timeouts: methodDef.timeouts } : void 0;
|
|
@@ -4335,8 +4499,7 @@ var RpcHub = class {
|
|
|
4335
4499
|
throw new Error("Outbound call failed: not connected and AwaitForConnection is not set.");
|
|
4336
4500
|
const callArgs = args.slice(0, methodDef.argCount);
|
|
4337
4501
|
const outboundCall = peer.call(wireName, callArgs, callOptions);
|
|
4338
|
-
|
|
4339
|
-
return resolveStreamRefs(result, peer);
|
|
4502
|
+
return await outboundCall.result;
|
|
4340
4503
|
};
|
|
4341
4504
|
}
|
|
4342
4505
|
// Private methods
|
|
@@ -4392,8 +4555,8 @@ function isSameWireMethod(a, b) {
|
|
|
4392
4555
|
return a.wireArgCount === b.wireArgCount && a.callTypeId === b.callTypeId && a.stream === b.stream && a.noWait === b.noWait && a.remoteExecutionMode === b.remoteExecutionMode;
|
|
4393
4556
|
}
|
|
4394
4557
|
|
|
4395
|
-
// src/rpc-
|
|
4396
|
-
var
|
|
4558
|
+
// src/rpc-ref-builder.ts
|
|
4559
|
+
var RpcRefBuilder = class {
|
|
4397
4560
|
/** Build a client peer ref from a WebSocket URL, optionally baking in the
|
|
4398
4561
|
* serialization format as a `f=` query parameter.
|
|
4399
4562
|
* @param url Base WebSocket URL, e.g. `wss://host/rpc/ws`.
|
|
@@ -4675,9 +4838,9 @@ var RpcPeerStateMonitor = class {
|
|
|
4675
4838
|
RpcOutboundCall,
|
|
4676
4839
|
RpcOutboundCallTracker,
|
|
4677
4840
|
RpcPeer,
|
|
4678
|
-
RpcPeerRefBuilder,
|
|
4679
4841
|
RpcPeerStateKind,
|
|
4680
4842
|
RpcPeerStateMonitor,
|
|
4843
|
+
RpcRefBuilder,
|
|
4681
4844
|
RpcRemoteExecutionMode,
|
|
4682
4845
|
RpcRemoteObjectTracker,
|
|
4683
4846
|
RpcSerializationFormat,
|
|
@@ -4694,6 +4857,7 @@ var RpcPeerStateMonitor = class {
|
|
|
4694
4857
|
RpcWebSocketConnection,
|
|
4695
4858
|
WebSocketState,
|
|
4696
4859
|
computeMethodHash,
|
|
4860
|
+
computeReconnectProof,
|
|
4697
4861
|
createBinaryEncoder,
|
|
4698
4862
|
createMessageChannelPair,
|
|
4699
4863
|
createRpcClient,
|
|
@@ -4704,15 +4868,16 @@ var RpcPeerStateMonitor = class {
|
|
|
4704
4868
|
deserializeBinaryMessage,
|
|
4705
4869
|
deserializeCompactBinaryMessage,
|
|
4706
4870
|
deserializeMessage,
|
|
4871
|
+
genericErrorFilter,
|
|
4707
4872
|
getLogs,
|
|
4708
4873
|
getMethodsMeta,
|
|
4709
4874
|
getServiceMeta,
|
|
4710
4875
|
getStateDescription,
|
|
4711
4876
|
isConnected,
|
|
4877
|
+
isReconnectProofSupported,
|
|
4712
4878
|
likelyConnected,
|
|
4713
4879
|
parseStreamRef,
|
|
4714
4880
|
readPolymorphismMarker,
|
|
4715
|
-
resolveStreamRefs,
|
|
4716
4881
|
rpcMethod,
|
|
4717
4882
|
rpcService,
|
|
4718
4883
|
serializeBinaryFrame,
|
|
@@ -4724,6 +4889,7 @@ var RpcPeerStateMonitor = class {
|
|
|
4724
4889
|
splitCompactBinaryFrame,
|
|
4725
4890
|
splitFrame,
|
|
4726
4891
|
toExceptionInfo,
|
|
4892
|
+
toRpcStream,
|
|
4727
4893
|
wireMethodName,
|
|
4728
4894
|
xxh3_64,
|
|
4729
4895
|
xxh3_64str
|