@actuallab/rpc 14.1.5 → 14.3.5
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 +1416 -1250
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -15
- package/dist/index.d.ts +70 -15
- package/dist/index.js +1412 -1249
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -165,12 +165,24 @@ function readVarUint(data, offset) {
|
|
|
165
165
|
} while (shift < 35);
|
|
166
166
|
return { value: value >>> 0, bytesRead };
|
|
167
167
|
}
|
|
168
|
+
var MIN_BINARY_ENVELOPE_SIZE = 7;
|
|
169
|
+
var MIN_COMPACT_BINARY_ENVELOPE_SIZE = 10;
|
|
170
|
+
function requireInBounds(data, pos, length, what) {
|
|
171
|
+
const end = pos + length;
|
|
172
|
+
if (!Number.isSafeInteger(length) || length < 0 || end > data.length)
|
|
173
|
+
throw new Error(`Invalid binary RPC message: ${what} length ${length} at ${pos} is out of bounds.`);
|
|
174
|
+
return end;
|
|
175
|
+
}
|
|
168
176
|
function skipHeaders(data, pos, headerCount) {
|
|
169
177
|
for (let h = 0; h < headerCount; h++) {
|
|
178
|
+
if (pos >= data.length)
|
|
179
|
+
throw new Error("Invalid binary RPC message: truncated header.");
|
|
170
180
|
const keyLen = data[pos++];
|
|
171
|
-
pos
|
|
181
|
+
pos = requireInBounds(data, pos, keyLen, "header key");
|
|
172
182
|
const valLen = readVarUint(data, pos);
|
|
173
|
-
|
|
183
|
+
if (valLen.bytesRead === 0)
|
|
184
|
+
throw new Error("Invalid binary RPC message: truncated header value length.");
|
|
185
|
+
pos = requireInBounds(data, pos + valLen.bytesRead, valLen.value, "header value");
|
|
174
186
|
}
|
|
175
187
|
return pos;
|
|
176
188
|
}
|
|
@@ -233,6 +245,8 @@ function serializeBinaryMessage(message, args, encoder = defaultBinaryEncoder) {
|
|
|
233
245
|
}
|
|
234
246
|
function deserializeBinaryMessage(data, offset, decoder = defaultBinaryDecoder) {
|
|
235
247
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
248
|
+
if (offset + MIN_BINARY_ENVELOPE_SIZE > data.length)
|
|
249
|
+
throw new Error(`Invalid binary RPC message: truncated envelope at ${offset}.`);
|
|
236
250
|
let pos = offset;
|
|
237
251
|
const byte0 = data[pos++];
|
|
238
252
|
const callTypeId = byte0 >> 5 & 7;
|
|
@@ -241,13 +255,15 @@ function deserializeBinaryMessage(data, offset, decoder = defaultBinaryDecoder)
|
|
|
241
255
|
pos += relId.bytesRead;
|
|
242
256
|
const methodLen = readVarUint(data, pos);
|
|
243
257
|
pos += methodLen.bytesRead;
|
|
244
|
-
const
|
|
245
|
-
const method = textDecoder.decode(
|
|
246
|
-
pos
|
|
247
|
-
|
|
258
|
+
const methodEnd = requireInBounds(data, pos, methodLen.value, "method");
|
|
259
|
+
const method = textDecoder.decode(data.subarray(pos, methodEnd));
|
|
260
|
+
pos = methodEnd;
|
|
261
|
+
if (pos + 4 > data.length)
|
|
262
|
+
throw new Error(`Invalid binary RPC message: truncated argument data length at ${pos}.`);
|
|
263
|
+
const argDataLen = view.getUint32(pos, true);
|
|
248
264
|
pos += 4;
|
|
249
265
|
const args = [];
|
|
250
|
-
const argEnd = pos
|
|
266
|
+
const argEnd = requireInBounds(data, pos, argDataLen, "argument data");
|
|
251
267
|
if (argDataLen > 0) {
|
|
252
268
|
const argSlice = data.subarray(pos, argEnd);
|
|
253
269
|
for (const decoded of decoder.decodeMulti(argSlice)) {
|
|
@@ -268,12 +284,16 @@ function splitBinaryFrame(frame, decoder = defaultBinaryDecoder) {
|
|
|
268
284
|
const results = [];
|
|
269
285
|
let offset = 0;
|
|
270
286
|
while (offset < frame.length) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
offset,
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
287
|
+
let parsed;
|
|
288
|
+
try {
|
|
289
|
+
parsed = deserializeBinaryMessage(frame, offset, decoder);
|
|
290
|
+
} catch (e) {
|
|
291
|
+
warnLog?.log(`Failed to parse a binary message at offset ${offset}:`, e);
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
const { message, args, bytesRead } = parsed;
|
|
295
|
+
if (!Number.isFinite(bytesRead) || bytesRead < MIN_BINARY_ENVELOPE_SIZE)
|
|
296
|
+
break;
|
|
277
297
|
results.push({ message, args });
|
|
278
298
|
offset += bytesRead;
|
|
279
299
|
}
|
|
@@ -323,19 +343,23 @@ function serializeCompactBinaryMessage(message, args, registry, encoder = defaul
|
|
|
323
343
|
}
|
|
324
344
|
function deserializeCompactBinaryMessage(data, offset, registry, decoder = defaultBinaryDecoder) {
|
|
325
345
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
346
|
+
if (offset + MIN_COMPACT_BINARY_ENVELOPE_SIZE > data.length)
|
|
347
|
+
throw new Error(`Invalid compact binary RPC message: truncated envelope at ${offset}.`);
|
|
326
348
|
let pos = offset;
|
|
327
349
|
const byte0 = data[pos++];
|
|
328
350
|
const callTypeId = byte0 >> 5 & 7;
|
|
329
351
|
const headerCount = byte0 & 31;
|
|
330
352
|
const relId = readVarUint(data, pos);
|
|
331
353
|
pos += relId.bytesRead;
|
|
354
|
+
if (pos + 8 > data.length)
|
|
355
|
+
throw new Error(`Invalid compact binary RPC message: truncated envelope at ${offset}.`);
|
|
332
356
|
const methodHash = view.getUint32(pos, true);
|
|
333
357
|
pos += 4;
|
|
334
358
|
const method = registry?.getName(methodHash | 0) ?? `<hash:0x${methodHash.toString(16).padStart(8, "0")}>`;
|
|
335
|
-
const argDataLen = view.
|
|
359
|
+
const argDataLen = view.getUint32(pos, true);
|
|
336
360
|
pos += 4;
|
|
337
361
|
const args = [];
|
|
338
|
-
const argEnd = pos
|
|
362
|
+
const argEnd = requireInBounds(data, pos, argDataLen, "argument data");
|
|
339
363
|
if (argDataLen > 0) {
|
|
340
364
|
const argSlice = data.subarray(pos, argEnd);
|
|
341
365
|
for (const decoded of decoder.decodeMulti(argSlice)) {
|
|
@@ -356,13 +380,16 @@ function splitCompactBinaryFrame(frame, registry, decoder = defaultBinaryDecoder
|
|
|
356
380
|
const results = [];
|
|
357
381
|
let offset = 0;
|
|
358
382
|
while (offset < frame.length) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
offset,
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
383
|
+
let parsed;
|
|
384
|
+
try {
|
|
385
|
+
parsed = deserializeCompactBinaryMessage(frame, offset, registry, decoder);
|
|
386
|
+
} catch (e) {
|
|
387
|
+
warnLog?.log(`Failed to parse a compact binary message at offset ${offset}:`, e);
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
const { message, args, bytesRead } = parsed;
|
|
391
|
+
if (!Number.isFinite(bytesRead) || bytesRead < MIN_COMPACT_BINARY_ENVELOPE_SIZE)
|
|
392
|
+
break;
|
|
366
393
|
results.push({ message, args });
|
|
367
394
|
offset += bytesRead;
|
|
368
395
|
}
|
|
@@ -424,46 +451,38 @@ var RpcWebSocketConnection = class {
|
|
|
424
451
|
ws.onmessage = (ev) => {
|
|
425
452
|
if (ev.data instanceof Uint8Array || ev.data instanceof ArrayBuffer) {
|
|
426
453
|
const frame = ev.data instanceof Uint8Array ? ev.data : new Uint8Array(ev.data);
|
|
454
|
+
let messages;
|
|
427
455
|
try {
|
|
428
|
-
|
|
429
|
-
for (const { message, args } of messages) {
|
|
430
|
-
this.messageReceived.trigger({
|
|
431
|
-
kind: "binary",
|
|
432
|
-
message,
|
|
433
|
-
args
|
|
434
|
-
});
|
|
435
|
-
}
|
|
456
|
+
messages = this._splitBinary(frame);
|
|
436
457
|
} catch (e) {
|
|
437
458
|
warnLog2?.log("Failed to split binary frame:", e);
|
|
438
459
|
this.error.trigger(e);
|
|
460
|
+
return;
|
|
439
461
|
}
|
|
462
|
+
for (const { message, args } of messages)
|
|
463
|
+
this._dispatchBinary(message, args);
|
|
440
464
|
} else if (typeof Blob !== "undefined" && ev.data instanceof Blob && this.binaryMode) {
|
|
441
465
|
void ev.data.arrayBuffer().then((ab) => {
|
|
442
466
|
const frame = new Uint8Array(ab);
|
|
467
|
+
let messages;
|
|
443
468
|
try {
|
|
444
|
-
|
|
445
|
-
for (const { message, args } of messages) {
|
|
446
|
-
this.messageReceived.trigger({
|
|
447
|
-
kind: "binary",
|
|
448
|
-
message,
|
|
449
|
-
args
|
|
450
|
-
});
|
|
451
|
-
}
|
|
469
|
+
messages = this._splitBinary(frame);
|
|
452
470
|
} catch (e) {
|
|
453
471
|
warnLog2?.log("Failed to split binary blob frame:", e);
|
|
454
472
|
this.error.trigger(e);
|
|
473
|
+
return;
|
|
455
474
|
}
|
|
475
|
+
for (const { message, args } of messages)
|
|
476
|
+
this._dispatchBinary(message, args);
|
|
456
477
|
});
|
|
457
478
|
} else {
|
|
458
479
|
const data = typeof ev.data === "string" ? ev.data : String(ev.data);
|
|
459
|
-
const
|
|
460
|
-
for (const msg of messages) {
|
|
480
|
+
for (const msg of splitFrame(data))
|
|
461
481
|
if (msg.length > 0)
|
|
462
|
-
this.messageReceived.
|
|
463
|
-
kind: "text",
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
482
|
+
this.messageReceived.triggerSafe(
|
|
483
|
+
{ kind: "text", raw: msg },
|
|
484
|
+
(e) => warnLog2?.log("Failed to handle a text message:", e)
|
|
485
|
+
);
|
|
467
486
|
}
|
|
468
487
|
};
|
|
469
488
|
ws.onclose = (ev) => {
|
|
@@ -502,6 +521,15 @@ var RpcWebSocketConnection = class {
|
|
|
502
521
|
}
|
|
503
522
|
return splitBinaryFrame(frame, this._decoder);
|
|
504
523
|
}
|
|
524
|
+
// Containment matters here: .NET batches many RPC messages per WebSocket frame,
|
|
525
|
+
// so letting one bad message escape would drop every other message in the frame -
|
|
526
|
+
// including $sys.Ok replies, whose per-call timeouts are unbounded by default.
|
|
527
|
+
_dispatchBinary(message, args) {
|
|
528
|
+
this.messageReceived.triggerSafe(
|
|
529
|
+
{ kind: "binary", message, args },
|
|
530
|
+
(e) => warnLog2?.log("Failed to handle a binary message:", e)
|
|
531
|
+
);
|
|
532
|
+
}
|
|
505
533
|
close(code, reason) {
|
|
506
534
|
this._ws.close(code, reason);
|
|
507
535
|
}
|
|
@@ -527,6 +555,7 @@ var RpcWebSocketConnection = class {
|
|
|
527
555
|
|
|
528
556
|
// src/rpc-message-channel-connection.ts
|
|
529
557
|
import { EventHandlerSet as EventHandlerSet2 } from "@actuallab/core";
|
|
558
|
+
var { warnLog: warnLog3 } = getLogs("RpcMessageChannelConnection");
|
|
530
559
|
var RpcMessageChannelConnection = class {
|
|
531
560
|
_port;
|
|
532
561
|
_open = true;
|
|
@@ -541,7 +570,10 @@ var RpcMessageChannelConnection = class {
|
|
|
541
570
|
const data = typeof ev.data === "string" ? ev.data : String(ev.data);
|
|
542
571
|
for (const msg of splitFrame(data))
|
|
543
572
|
if (msg.length > 0)
|
|
544
|
-
this.messageReceived.
|
|
573
|
+
this.messageReceived.triggerSafe(
|
|
574
|
+
{ kind: "text", raw: msg },
|
|
575
|
+
(e) => warnLog3?.log("Failed to handle a text message:", e)
|
|
576
|
+
);
|
|
545
577
|
};
|
|
546
578
|
}
|
|
547
579
|
get isOpen() {
|
|
@@ -858,10 +890,18 @@ var RpcError = class extends Error {
|
|
|
858
890
|
}
|
|
859
891
|
};
|
|
860
892
|
var REMOTE_EXCEPTION_TYPE_REF = "ActualLab.Serialization.RemoteException, ActualLab.Core";
|
|
861
|
-
|
|
893
|
+
var genericErrorFilter = () => ({
|
|
894
|
+
TypeRef: REMOTE_EXCEPTION_TYPE_REF,
|
|
895
|
+
Message: "Error: An error occurred while processing the request."
|
|
896
|
+
});
|
|
897
|
+
function toExceptionInfo(error, filter) {
|
|
862
898
|
const name = error instanceof Error ? error.name : "Error";
|
|
863
899
|
const message = error instanceof Error ? error.message : String(error);
|
|
864
|
-
|
|
900
|
+
const info = {
|
|
901
|
+
TypeRef: REMOTE_EXCEPTION_TYPE_REF,
|
|
902
|
+
Message: `${name}: ${message}`
|
|
903
|
+
};
|
|
904
|
+
return filter === void 0 ? info : filter(error, info);
|
|
865
905
|
}
|
|
866
906
|
|
|
867
907
|
// src/increasing-seq-compressor.ts
|
|
@@ -955,1271 +995,1308 @@ function _readVarUint(data, offset) {
|
|
|
955
995
|
return [value, offset];
|
|
956
996
|
}
|
|
957
997
|
|
|
958
|
-
// src/
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
998
|
+
// src/base64.ts
|
|
999
|
+
function base64Encode(bytes) {
|
|
1000
|
+
const chunkSize = 32768;
|
|
1001
|
+
let binary = "";
|
|
1002
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
1003
|
+
binary += String.fromCharCode(
|
|
1004
|
+
...bytes.subarray(i, Math.min(bytes.length, i + chunkSize))
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
return btoa(binary);
|
|
1008
|
+
}
|
|
1009
|
+
function base64UrlEncode(bytes) {
|
|
1010
|
+
return base64Encode(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1011
|
+
}
|
|
1012
|
+
function base64Decode(encoded) {
|
|
1013
|
+
const binary = atob(encoded);
|
|
1014
|
+
const out = new Uint8Array(binary.length);
|
|
1015
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
1016
|
+
return out;
|
|
1017
|
+
}
|
|
964
1018
|
|
|
965
|
-
// src/rpc-
|
|
966
|
-
|
|
967
|
-
var
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1019
|
+
// src/rpc-system-call-handler.ts
|
|
1020
|
+
var { debugLog, warnLog: warnLog4 } = getLogs("RpcSystemCallHandler");
|
|
1021
|
+
var RpcSystemCallHandler = class {
|
|
1022
|
+
handle(message, args, peer) {
|
|
1023
|
+
const method = message.Method;
|
|
1024
|
+
const relatedId = message.RelatedId ?? 0;
|
|
1025
|
+
debugLog?.log(`'${peer.ref}': handle ${method}#${relatedId}`);
|
|
1026
|
+
switch (method) {
|
|
1027
|
+
case RpcSystemCalls.ok: {
|
|
1028
|
+
const call = peer.outboundCalls.get(relatedId);
|
|
1029
|
+
if (call !== void 0) {
|
|
1030
|
+
if (args.length > 1) {
|
|
1031
|
+
peer.outboundCalls.remove(relatedId);
|
|
1032
|
+
call.result.reject(polymorphicPayloadError(method, args.length, 1));
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
call.completedStage |= RpcCallStage.ResultReady;
|
|
1036
|
+
if (call.removeOnOk) {
|
|
1037
|
+
peer.outboundCalls.remove(relatedId);
|
|
1038
|
+
}
|
|
1039
|
+
call.result.resolve(args[0]);
|
|
1040
|
+
}
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
case RpcSystemCalls.reconnect: {
|
|
1044
|
+
this._handleReconnect(relatedId, args, peer);
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
case RpcSystemCalls.error: {
|
|
1048
|
+
const call = peer.outboundCalls.get(relatedId);
|
|
1049
|
+
if (call !== void 0) {
|
|
1050
|
+
call.completedStage |= RpcCallStage.ResultReady;
|
|
1051
|
+
if (call.removeOnOk) {
|
|
1052
|
+
peer.outboundCalls.remove(relatedId);
|
|
1053
|
+
}
|
|
1054
|
+
const errorInfo = args[0];
|
|
1055
|
+
const msg = errorInfo?.Message ?? errorInfo?.message ?? "RPC error";
|
|
1056
|
+
const typeName = extractTypeName(errorInfo?.TypeRef ?? errorInfo?.typeRef);
|
|
1057
|
+
if (typeName === "ActualLab.Rpc.RpcRerouteException")
|
|
1058
|
+
warnLog4?.log("Got RpcRerouteException from remote peer:", msg);
|
|
1059
|
+
call.result.reject(new RpcError(msg, typeName));
|
|
1060
|
+
}
|
|
1061
|
+
break;
|
|
1062
|
+
}
|
|
1063
|
+
case RpcSystemCalls.cancel: {
|
|
1064
|
+
const call = peer.inboundCalls.remove(relatedId);
|
|
1065
|
+
call?.cancel();
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
case RpcSystemCalls.keepAlive: {
|
|
1069
|
+
peer.notifyKeepAliveReceived();
|
|
1070
|
+
const keepAliveIds = args[0];
|
|
1071
|
+
if (Array.isArray(keepAliveIds) && keepAliveIds.length > 0 && peer.connection !== void 0) {
|
|
1072
|
+
const unknownIds = [];
|
|
1073
|
+
for (const id of keepAliveIds)
|
|
1074
|
+
if (peer.sharedObjects.get(id) === void 0)
|
|
1075
|
+
unknownIds.push(id);
|
|
1076
|
+
if (unknownIds.length > 0)
|
|
1077
|
+
peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, unknownIds);
|
|
1078
|
+
}
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
1081
|
+
case RpcSystemCalls.item: {
|
|
1082
|
+
const stream = peer.remoteObjects.get(relatedId);
|
|
1083
|
+
if (!stream)
|
|
1084
|
+
debugLog?.log(`$sys.I: no stream for relatedId=${relatedId}`);
|
|
1085
|
+
else if (args.length > 2)
|
|
1086
|
+
stream.onEnd(args[0], polymorphicPayloadError(method, args.length, 2));
|
|
1087
|
+
else
|
|
1088
|
+
stream.onItem(args[0], args[1]);
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
case RpcSystemCalls.batch: {
|
|
1092
|
+
const stream = peer.remoteObjects.get(relatedId);
|
|
1093
|
+
if (!stream)
|
|
1094
|
+
debugLog?.log(`$sys.B: no stream for relatedId=${relatedId}`);
|
|
1095
|
+
else if (args.length > 2)
|
|
1096
|
+
stream.onEnd(args[0], polymorphicPayloadError(method, args.length, 2));
|
|
1097
|
+
else if (Array.isArray(args[1]))
|
|
1098
|
+
stream.onBatch(args[0], args[1]);
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
case RpcSystemCalls.end: {
|
|
1102
|
+
const stream = peer.remoteObjects.get(relatedId);
|
|
1103
|
+
if (!stream) {
|
|
1104
|
+
debugLog?.log(`$sys.End: no stream for relatedId=${relatedId}`);
|
|
1105
|
+
} else {
|
|
1106
|
+
const errorInfo = args[1];
|
|
1107
|
+
const typeName = extractTypeName(errorInfo?.TypeRef ?? errorInfo?.typeRef);
|
|
1108
|
+
const msg = errorInfo?.Message ?? errorInfo?.message;
|
|
1109
|
+
let error = null;
|
|
1110
|
+
if (typeName || msg) {
|
|
1111
|
+
error = new Error(msg ?? typeName ?? "RPC stream error");
|
|
1112
|
+
if (typeName)
|
|
1113
|
+
error.name = typeName;
|
|
1114
|
+
}
|
|
1115
|
+
stream.onEnd(args[0], error);
|
|
1116
|
+
}
|
|
1117
|
+
break;
|
|
1118
|
+
}
|
|
1119
|
+
case RpcSystemCalls.ack: {
|
|
1120
|
+
const sender = peer.sharedObjects.get(relatedId);
|
|
1121
|
+
if (sender)
|
|
1122
|
+
sender.onAck(args[0], args[1]);
|
|
1123
|
+
else if (peer.connection !== void 0)
|
|
1124
|
+
peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, [relatedId]);
|
|
1125
|
+
break;
|
|
1126
|
+
}
|
|
1127
|
+
case RpcSystemCalls.ackEnd: {
|
|
1128
|
+
const sender = peer.sharedObjects.get(relatedId);
|
|
1129
|
+
if (sender)
|
|
1130
|
+
sender.onAckEnd(args[0]);
|
|
1131
|
+
else if (peer.connection !== void 0)
|
|
1132
|
+
peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, [relatedId]);
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
case RpcSystemCalls.disconnect: {
|
|
1136
|
+
const ids = args[0];
|
|
1137
|
+
if (Array.isArray(ids)) {
|
|
1138
|
+
for (const id of ids) {
|
|
1139
|
+
const remoteObj = peer.remoteObjects.get(id);
|
|
1140
|
+
if (remoteObj && typeof remoteObj.disconnect === "function") {
|
|
1141
|
+
remoteObj.disconnect();
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
993
1149
|
/**
|
|
994
|
-
*
|
|
995
|
-
*
|
|
996
|
-
*
|
|
997
|
-
*
|
|
1150
|
+
* Server-side handler for the `$sys.Reconnect` call. Parses the peer's
|
|
1151
|
+
* report of in-flight call IDs (grouped by stage, IncreasingSeqCompressor-
|
|
1152
|
+
* encoded, base64-wrapped for JSON transport), computes the subset this
|
|
1153
|
+
* peer does not recognize, and replies with a compressed byte[] of the
|
|
1154
|
+
* unknown IDs wrapped in `$sys.Ok`.
|
|
998
1155
|
*
|
|
999
|
-
*
|
|
1000
|
-
*
|
|
1156
|
+
* @param relatedId The outbound call ID the client used for this
|
|
1157
|
+
* `$sys.Reconnect` invocation — we send `$sys.Ok` back
|
|
1158
|
+
* with the same `RelatedId`.
|
|
1001
1159
|
*/
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
_rttSamples = [];
|
|
1015
|
-
// -- Recorder-controller metrics (Step 9.1) --
|
|
1016
|
-
/** Total items skipped via real-time `canSkipTo` compaction. */
|
|
1017
|
-
_skipCount = 0;
|
|
1018
|
-
/** Fires after a $sys.Ack has been processed (post-compaction). The
|
|
1019
|
-
* recorder's quality controller uses this to bump a "last ACK at"
|
|
1020
|
-
* watchdog so it can distinguish "stuck" (no ACK > N s) from
|
|
1021
|
-
* "throttled" (ACKs flowing). */
|
|
1022
|
-
onAckProcessed;
|
|
1023
|
-
/** Fires after each item is pushed onto the local ring buffer. The
|
|
1024
|
-
* argument is the buffer count *after* the push. Together with
|
|
1025
|
-
* `onAckProcessed` (which is the only place items leave the buffer),
|
|
1026
|
-
* this gives controllers a complete picture of buffer utilization.
|
|
1027
|
-
* When `bufferedCount === bufferSize`, the source pull is paused
|
|
1028
|
-
* until the next ACK frees space. Listener errors are swallowed. */
|
|
1029
|
-
onBuffered;
|
|
1030
|
-
get nextIndex() {
|
|
1031
|
-
return this._nextIndex;
|
|
1032
|
-
}
|
|
1033
|
-
get lastAckIndex() {
|
|
1034
|
-
return this._lastAckedIndex;
|
|
1035
|
-
}
|
|
1036
|
-
get skipCount() {
|
|
1037
|
-
return this._skipCount;
|
|
1038
|
-
}
|
|
1039
|
-
// Windowed-MIN ack round trip (ms, -1 until sampled): converges on propagation
|
|
1040
|
-
// RTT — unlike a mean/EMA it is not inflated by self-induced queuing.
|
|
1041
|
-
get minRttMs() {
|
|
1042
|
-
const cutoffMs = Date.now() - RTT_WINDOW_MS;
|
|
1043
|
-
while (this._rttSamples.length > 0 && this._rttSamples[0].atMs < cutoffMs)
|
|
1044
|
-
this._rttSamples.shift();
|
|
1045
|
-
if (this._rttSamples.length === 0)
|
|
1046
|
-
return -1;
|
|
1047
|
-
let min = this._rttSamples[0].rttMs;
|
|
1048
|
-
for (const s of this._rttSamples) {
|
|
1049
|
-
if (s.rttMs < min)
|
|
1050
|
-
min = s.rttMs;
|
|
1051
|
-
}
|
|
1052
|
-
return min;
|
|
1053
|
-
}
|
|
1054
|
-
// -- ACK queue (analog of .NET's Channel<(long, bool)>) --
|
|
1055
|
-
_acks = [];
|
|
1056
|
-
_whenAckReady = null;
|
|
1057
|
-
constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true, isRealTime = false, canSkipTo = () => true, sourceUsesAbortSignal = false, bufferSize) {
|
|
1058
|
-
const localId = peer.sharedObjects.nextId();
|
|
1059
|
-
this.id = { hostId: peer.hub.hubId, localId };
|
|
1060
|
-
this.allowReconnect = allowReconnect;
|
|
1061
|
-
this.isRealTime = isRealTime;
|
|
1062
|
-
this.canSkipTo = canSkipTo;
|
|
1063
|
-
this.peer = peer;
|
|
1064
|
-
this.ackPeriod = ackPeriod;
|
|
1065
|
-
this.ackAdvance = ackAdvance;
|
|
1066
|
-
this.sourceUsesAbortSignal = sourceUsesAbortSignal;
|
|
1067
|
-
if (bufferSize !== void 0 && bufferSize !== 0 && bufferSize < ackAdvance)
|
|
1068
|
-
warnLog3?.log(
|
|
1069
|
-
`RpcStream bufferSize (${bufferSize}) is below ackAdvance (${ackAdvance}); using ackAdvance as buffer size.`
|
|
1160
|
+
_handleReconnect(relatedId, args, peer) {
|
|
1161
|
+
if (peer.connection === void 0) return;
|
|
1162
|
+
const handshakeIndex = args[0];
|
|
1163
|
+
if (typeof handshakeIndex !== "number" || handshakeIndex !== peer.ownHandshakeIndex) {
|
|
1164
|
+
const got = typeof handshakeIndex === "number" ? handshakeIndex : `<${typeof handshakeIndex}>`;
|
|
1165
|
+
peer.hub.systemCallSender.error(
|
|
1166
|
+
peer.connection,
|
|
1167
|
+
peer.serializationFormat,
|
|
1168
|
+
relatedId,
|
|
1169
|
+
new Error(
|
|
1170
|
+
`TooLateToReconnect: own handshake index ${peer.ownHandshakeIndex} != ${got}`
|
|
1171
|
+
)
|
|
1070
1172
|
);
|
|
1071
|
-
this.bufferSize = bufferSize !== void 0 && bufferSize >= ackAdvance ? bufferSize : ackAdvance;
|
|
1072
|
-
}
|
|
1073
|
-
/** AbortSignal that is aborted when the sender is disconnected. */
|
|
1074
|
-
get abortSignal() {
|
|
1075
|
-
return this._abortController.signal;
|
|
1076
|
-
}
|
|
1077
|
-
/** Returns the stream reference string for the $sys.Ok response. */
|
|
1078
|
-
toRef() {
|
|
1079
|
-
return `${this.id.hostId},${this.id.localId},${this.ackPeriod},${this.ackAdvance},${this.allowReconnect ? "1" : "0"},${this.isRealTime ? "1" : "0"}`;
|
|
1080
|
-
}
|
|
1081
|
-
/**
|
|
1082
|
-
* Called by system call handler when $sys.Ack is received from the client.
|
|
1083
|
-
*
|
|
1084
|
-
* Mirrors .NET `RpcSharedStream<T>.OnAck` at
|
|
1085
|
-
* src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:68-101.
|
|
1086
|
-
*/
|
|
1087
|
-
onAck(nextIndex, hostId) {
|
|
1088
|
-
const mustReset = hostId !== "" && hostId !== _RpcStreamSender._emptyGuid;
|
|
1089
|
-
if (mustReset && hostId !== this.id.hostId) {
|
|
1090
|
-
this._sendDisconnect();
|
|
1091
1173
|
return;
|
|
1092
1174
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1175
|
+
let unknownIds = [];
|
|
1176
|
+
try {
|
|
1177
|
+
const stagesRaw = args[1];
|
|
1178
|
+
if (stagesRaw && typeof stagesRaw === "object" && !Array.isArray(stagesRaw)) {
|
|
1179
|
+
const unknownSet = /* @__PURE__ */ new Set();
|
|
1180
|
+
for (const [stageKey, rawValue] of Object.entries(stagesRaw)) {
|
|
1181
|
+
const bytes = _toBytes(rawValue);
|
|
1182
|
+
if (bytes === null) continue;
|
|
1183
|
+
const completedStage = parseInt(stageKey, 10) || 0;
|
|
1184
|
+
const callIds = IncreasingSeqCompressor.deserialize(bytes);
|
|
1185
|
+
for (const callId of callIds) {
|
|
1186
|
+
const call = peer.inboundCalls.get(callId);
|
|
1187
|
+
if (call === void 0)
|
|
1188
|
+
unknownSet.add(callId);
|
|
1189
|
+
else if (completedStage < 1)
|
|
1190
|
+
call.resendResult();
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
unknownIds = [...unknownSet].sort((a, b) => a - b);
|
|
1099
1194
|
}
|
|
1100
|
-
}
|
|
1101
|
-
this._sendDisconnect();
|
|
1102
|
-
return;
|
|
1103
|
-
} else if (mustReset && !this.allowReconnect) {
|
|
1104
|
-
this._sendDisconnect();
|
|
1105
|
-
return;
|
|
1106
|
-
}
|
|
1107
|
-
this._sampleRtt(nextIndex, mustReset);
|
|
1108
|
-
this._acks.push({ nextIndex, mustReset });
|
|
1109
|
-
if (this._whenAckReady) {
|
|
1110
|
-
this._whenAckReady.resolve();
|
|
1111
|
-
this._whenAckReady = null;
|
|
1195
|
+
} catch {
|
|
1112
1196
|
}
|
|
1197
|
+
const responseBytes = IncreasingSeqCompressor.serialize(unknownIds);
|
|
1198
|
+
const responseValue = peer.serializationFormat.isBinary ? responseBytes : base64Encode(responseBytes);
|
|
1199
|
+
peer.hub.systemCallSender.ok(peer.connection, peer.serializationFormat, relatedId, responseValue);
|
|
1113
1200
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1201
|
+
};
|
|
1202
|
+
function polymorphicPayloadError(method, got, expected) {
|
|
1203
|
+
return new Error(
|
|
1204
|
+
`${method}: expected ${expected} value(s), got ${got} \u2014 polymorphic payloads are not supported by the TS client`
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
function extractTypeName(typeRef) {
|
|
1208
|
+
if (typeof typeRef === "string") {
|
|
1209
|
+
if (typeRef.length === 0)
|
|
1210
|
+
return void 0;
|
|
1211
|
+
const commaIdx = typeRef.indexOf(",");
|
|
1212
|
+
return commaIdx >= 0 ? typeRef.slice(0, commaIdx) : typeRef;
|
|
1121
1213
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1214
|
+
if (typeRef !== null && typeof typeRef === "object") {
|
|
1215
|
+
const t = typeRef;
|
|
1216
|
+
const aqn = t.AssemblyQualifiedName ?? t.assemblyQualifiedName;
|
|
1217
|
+
if (typeof aqn === "string" && aqn.length > 0) {
|
|
1218
|
+
const commaIdx = aqn.indexOf(",");
|
|
1219
|
+
return commaIdx >= 0 ? aqn.slice(0, commaIdx) : aqn;
|
|
1220
|
+
}
|
|
1221
|
+
return t.TypeName ?? t.typeName;
|
|
1125
1222
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
sendItem(item) {
|
|
1136
|
-
if (this._ended) return;
|
|
1137
|
-
const conn = this.peer.connection;
|
|
1138
|
-
if (conn) {
|
|
1139
|
-
this.peer.hub.systemCallSender.item(
|
|
1140
|
-
conn,
|
|
1141
|
-
this.peer.serializationFormat,
|
|
1142
|
-
this.id.localId,
|
|
1143
|
-
this._nextIndex,
|
|
1144
|
-
item
|
|
1145
|
-
);
|
|
1146
|
-
this._pendingSendTimes.push({ index: this._nextIndex, sentAtMs: Date.now() });
|
|
1223
|
+
return void 0;
|
|
1224
|
+
}
|
|
1225
|
+
function _toBytes(value) {
|
|
1226
|
+
if (value instanceof Uint8Array) return value;
|
|
1227
|
+
if (typeof value === "string") {
|
|
1228
|
+
try {
|
|
1229
|
+
return base64Decode(value);
|
|
1230
|
+
} catch {
|
|
1231
|
+
return null;
|
|
1147
1232
|
}
|
|
1148
|
-
this._nextIndex++;
|
|
1149
1233
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// src/rpc-system-call-sender.ts
|
|
1238
|
+
var { errorLog } = getLogs("RpcSystemCallSender");
|
|
1239
|
+
var RpcSystemCallSender = class _RpcSystemCallSender {
|
|
1240
|
+
// Reusable envelopes — one per hot-path method.
|
|
1241
|
+
_itemEnv = {
|
|
1242
|
+
Method: RpcSystemCalls.item,
|
|
1243
|
+
RelatedId: 0
|
|
1244
|
+
};
|
|
1245
|
+
_batchEnv = {
|
|
1246
|
+
Method: RpcSystemCalls.batch,
|
|
1247
|
+
RelatedId: 0
|
|
1248
|
+
};
|
|
1249
|
+
_endEnv = {
|
|
1250
|
+
Method: RpcSystemCalls.end,
|
|
1251
|
+
RelatedId: 0
|
|
1252
|
+
};
|
|
1253
|
+
_ackEnv = {
|
|
1254
|
+
Method: RpcSystemCalls.ack,
|
|
1255
|
+
RelatedId: 0
|
|
1256
|
+
};
|
|
1257
|
+
_ackEndEnv = {
|
|
1258
|
+
Method: RpcSystemCalls.ackEnd,
|
|
1259
|
+
RelatedId: 0
|
|
1260
|
+
};
|
|
1261
|
+
// Reusable arg arrays.
|
|
1262
|
+
_itemArgs = [0, null];
|
|
1263
|
+
_batchArgs = [0, null];
|
|
1264
|
+
_endArgs = [0, { TypeRef: "", Message: "" }];
|
|
1265
|
+
_ackArgs = [0, ""];
|
|
1266
|
+
_ackEndArgs = [""];
|
|
1267
|
+
static NO_ERROR = Object.freeze({ TypeRef: "", Message: "" });
|
|
1268
|
+
registry;
|
|
1269
|
+
/** Applied to every error this hub sends to a remote peer. Undefined means
|
|
1270
|
+
* pass through, i.e. the peer sees the original message — set it (e.g. to
|
|
1271
|
+
* {@link genericErrorFilter}) on a TS-hosted server, where handler errors
|
|
1272
|
+
* would otherwise disclose internal details to unauthenticated callers. */
|
|
1273
|
+
errorFilter;
|
|
1274
|
+
_send(conn, format, envelope, args) {
|
|
1275
|
+
const wireData = format.serializeMessage(
|
|
1276
|
+
envelope,
|
|
1277
|
+
args,
|
|
1278
|
+
conn.encoder,
|
|
1279
|
+
this.registry
|
|
1280
|
+
);
|
|
1281
|
+
if (typeof wireData === "string") {
|
|
1282
|
+
conn.send(wireData);
|
|
1283
|
+
} else {
|
|
1284
|
+
conn.sendBinary(wireData);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
handshake(conn, format, peerId, hubId, index) {
|
|
1288
|
+
const handshakeArg = format.isBinary ? [peerId, null, hubId, 2, index] : {
|
|
1289
|
+
RemotePeerId: peerId,
|
|
1290
|
+
RemoteApiVersionSet: null,
|
|
1291
|
+
RemoteHubId: hubId,
|
|
1292
|
+
ProtocolVersion: 2,
|
|
1293
|
+
Index: index
|
|
1294
|
+
};
|
|
1295
|
+
this._send(conn, format, { Method: RpcSystemCalls.handshake }, [
|
|
1296
|
+
handshakeArg
|
|
1297
|
+
]);
|
|
1298
|
+
}
|
|
1299
|
+
ok(conn, format, relatedId, result) {
|
|
1300
|
+
try {
|
|
1301
|
+
this._send(
|
|
1156
1302
|
conn,
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
items
|
|
1303
|
+
format,
|
|
1304
|
+
{ Method: RpcSystemCalls.ok, RelatedId: relatedId },
|
|
1305
|
+
[result]
|
|
1161
1306
|
);
|
|
1307
|
+
} catch (e) {
|
|
1308
|
+
errorLog?.log(`Failed to send Ok response for call #${relatedId}`, e);
|
|
1309
|
+
this.error(conn, format, relatedId, e);
|
|
1162
1310
|
}
|
|
1163
|
-
this._nextIndex += items.length;
|
|
1164
1311
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
if (this._ended) return;
|
|
1168
|
-
this._ended = true;
|
|
1169
|
-
const conn = this.peer.connection;
|
|
1170
|
-
if (!conn) return;
|
|
1171
|
-
const errorInfo = error ? toExceptionInfo(error) : { TypeRef: "", Message: "" };
|
|
1172
|
-
this.peer.hub.systemCallSender.end(
|
|
1312
|
+
error(conn, format, relatedId, error) {
|
|
1313
|
+
this._send(
|
|
1173
1314
|
conn,
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
this.
|
|
1177
|
-
errorInfo
|
|
1315
|
+
format,
|
|
1316
|
+
{ Method: RpcSystemCalls.error, RelatedId: relatedId },
|
|
1317
|
+
[toExceptionInfo(error, this.errorFilter)]
|
|
1178
1318
|
);
|
|
1179
1319
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1320
|
+
cancel(conn, format, relatedId) {
|
|
1321
|
+
this._send(conn, format, {
|
|
1322
|
+
Method: RpcSystemCalls.cancel,
|
|
1323
|
+
RelatedId: relatedId
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
keepAlive(conn, format, activeCallIds) {
|
|
1327
|
+
this._send(conn, format, { Method: RpcSystemCalls.keepAlive }, [
|
|
1328
|
+
activeCallIds
|
|
1329
|
+
]);
|
|
1330
|
+
}
|
|
1331
|
+
// Tells the remote peer the listed shared-object ids are gone on our side,
|
|
1332
|
+
// so its matching remote objects (e.g. RpcStream consumers) fail fast
|
|
1333
|
+
// instead of hanging. Mirrors .NET RpcSystemCallSender.Disconnect.
|
|
1334
|
+
disconnect(conn, format, localIds) {
|
|
1335
|
+
this._send(conn, format, { Method: RpcSystemCalls.disconnect }, [
|
|
1336
|
+
localIds
|
|
1337
|
+
]);
|
|
1338
|
+
}
|
|
1339
|
+
item(conn, format, localId, index, item) {
|
|
1340
|
+
this._itemEnv.RelatedId = localId;
|
|
1341
|
+
this._itemArgs[0] = index;
|
|
1342
|
+
this._itemArgs[1] = item;
|
|
1343
|
+
this._send(conn, format, this._itemEnv, this._itemArgs);
|
|
1344
|
+
this._itemArgs[1] = null;
|
|
1345
|
+
}
|
|
1346
|
+
batch(conn, format, localId, index, items) {
|
|
1347
|
+
this._batchEnv.RelatedId = localId;
|
|
1348
|
+
this._batchArgs[0] = index;
|
|
1349
|
+
this._batchArgs[1] = items;
|
|
1350
|
+
this._send(conn, format, this._batchEnv, this._batchArgs);
|
|
1351
|
+
this._batchArgs[1] = null;
|
|
1352
|
+
}
|
|
1353
|
+
end(conn, format, localId, index, error = _RpcSystemCallSender.NO_ERROR) {
|
|
1354
|
+
this._endEnv.RelatedId = localId;
|
|
1355
|
+
this._endArgs[0] = index;
|
|
1356
|
+
this._endArgs[1] = error;
|
|
1357
|
+
this._send(conn, format, this._endEnv, this._endArgs);
|
|
1358
|
+
}
|
|
1359
|
+
ack(conn, format, localId, nextIndex, hostId) {
|
|
1360
|
+
this._ackEnv.RelatedId = localId;
|
|
1361
|
+
this._ackArgs[0] = nextIndex;
|
|
1362
|
+
this._ackArgs[1] = hostId;
|
|
1363
|
+
this._send(conn, format, this._ackEnv, this._ackArgs);
|
|
1364
|
+
}
|
|
1365
|
+
ackEnd(conn, format, localId, hostId) {
|
|
1366
|
+
this._ackEndEnv.RelatedId = localId;
|
|
1367
|
+
this._ackEndArgs[0] = hostId;
|
|
1368
|
+
this._send(conn, format, this._ackEndEnv, this._ackEndArgs);
|
|
1199
1369
|
}
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
// src/rpc-object.ts
|
|
1373
|
+
var RpcObjectKind = { Local: 0, Remote: 1 };
|
|
1374
|
+
|
|
1375
|
+
// src/rpc-stream.ts
|
|
1376
|
+
import Denque from "denque";
|
|
1377
|
+
import { PromiseSource as PromiseSource4 } from "@actuallab/core";
|
|
1378
|
+
|
|
1379
|
+
// src/rpc-stream-sender.ts
|
|
1380
|
+
import { PromiseSource as PromiseSource3, RingBuffer } from "@actuallab/core";
|
|
1381
|
+
var { warnLog: warnLog5 } = getLogs("RpcSharedStream");
|
|
1382
|
+
var DEFAULT_ACK_PERIOD = 256;
|
|
1383
|
+
var DEFAULT_ACK_ADVANCE = 128;
|
|
1384
|
+
var RTT_WINDOW_MS = 2e4;
|
|
1385
|
+
var MAX_PENDING_SEND_TIMES = 4096;
|
|
1386
|
+
var RpcStreamSender = class _RpcStreamSender {
|
|
1387
|
+
static _emptyGuid = "00000000-0000-0000-0000-000000000000";
|
|
1388
|
+
id;
|
|
1389
|
+
kind = RpcObjectKind.Local;
|
|
1390
|
+
allowReconnect;
|
|
1391
|
+
isRealTime;
|
|
1392
|
+
canSkipTo;
|
|
1393
|
+
peer;
|
|
1394
|
+
ackPeriod;
|
|
1200
1395
|
/**
|
|
1201
|
-
*
|
|
1202
|
-
*
|
|
1203
|
-
*
|
|
1204
|
-
* With `bufferSize > ackAdvance` the local ring buffer holds more than the
|
|
1205
|
-
* in-flight window — real-time mode uses the extra space to pre-buffer.
|
|
1396
|
+
* Wire-level flow-control window: the maximum index the sender may
|
|
1397
|
+
* advance past the most recently acknowledged index. Mirrors .NET
|
|
1398
|
+
* `RpcStream.AckAdvance`.
|
|
1206
1399
|
*/
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
state.iteratorDone = true;
|
|
1255
|
-
isFullyBuffered = true;
|
|
1256
|
-
buffer.pushTail({
|
|
1257
|
-
kind: "error",
|
|
1258
|
-
error: e instanceof Error ? e : new Error(String(e))
|
|
1259
|
-
});
|
|
1260
|
-
fireBuffered();
|
|
1261
|
-
return true;
|
|
1262
|
-
}
|
|
1263
|
-
};
|
|
1264
|
-
while (true) {
|
|
1265
|
-
let ack = this._tryProcessAcks();
|
|
1266
|
-
if (!ack) {
|
|
1267
|
-
await this._waitAckReady();
|
|
1268
|
-
if (this._ended) return;
|
|
1269
|
-
ack = this._tryProcessAcks();
|
|
1270
|
-
if (!ack) {
|
|
1271
|
-
warnLog3?.log("Something is off: couldn't read an acknowledgement");
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
this._lastAckedIndex = ack.nextIndex;
|
|
1276
|
-
if (isRealTime && ack.mustReset && !isFullyBuffered) {
|
|
1277
|
-
buffer.clear();
|
|
1278
|
-
bufferStart = this._nextIndex;
|
|
1279
|
-
while (true) {
|
|
1280
|
-
if (this._ended) return;
|
|
1281
|
-
let item;
|
|
1282
|
-
let accepted = false;
|
|
1283
|
-
try {
|
|
1284
|
-
const r = await readNext();
|
|
1285
|
-
if (this._ended) return;
|
|
1286
|
-
if (r.done) {
|
|
1287
|
-
item = _endItem;
|
|
1288
|
-
isFullyBuffered = true;
|
|
1289
|
-
state.iteratorDone = true;
|
|
1290
|
-
} else if (this.canSkipTo(r.value)) {
|
|
1291
|
-
item = { kind: "value", value: r.value };
|
|
1292
|
-
accepted = true;
|
|
1293
|
-
} else {
|
|
1294
|
-
continue;
|
|
1295
|
-
}
|
|
1296
|
-
} catch (e) {
|
|
1297
|
-
state.iteratorDone = true;
|
|
1298
|
-
isFullyBuffered = true;
|
|
1299
|
-
item = {
|
|
1300
|
-
kind: "error",
|
|
1301
|
-
error: e instanceof Error ? e : new Error(String(e))
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
buffer.pushTail(item);
|
|
1305
|
-
fireBuffered();
|
|
1306
|
-
if (accepted || isFullyBuffered) break;
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
const shift = _clamp(ack.nextIndex - bufferStart, 0, buffer.count);
|
|
1310
|
-
if (shift > 0) {
|
|
1311
|
-
buffer.moveHead(shift);
|
|
1312
|
-
bufferStart += shift;
|
|
1313
|
-
}
|
|
1314
|
-
if (this._nextIndex < bufferStart) {
|
|
1315
|
-
if (!this._ended)
|
|
1316
|
-
this.sendEnd(new Error("Stream position unavailable."));
|
|
1317
|
-
return;
|
|
1318
|
-
}
|
|
1319
|
-
let bufferIndex = this._nextIndex - bufferStart;
|
|
1320
|
-
const maxIndex = ack.nextIndex + this.ackAdvance;
|
|
1321
|
-
while (this._nextIndex < maxIndex) {
|
|
1322
|
-
if (this._ended) return;
|
|
1323
|
-
if (isRealTime) {
|
|
1324
|
-
while (buffer.hasRemainingCapacity && !isFullyBuffered && this._acks.length === 0) {
|
|
1325
|
-
if (!await bufferNext(true, true))
|
|
1326
|
-
break;
|
|
1327
|
-
}
|
|
1328
|
-
if (bufferIndex >= buffer.count && !isFullyBuffered && buffer.hasRemainingCapacity && this._acks.length === 0) {
|
|
1329
|
-
await bufferNext(false, true);
|
|
1330
|
-
}
|
|
1331
|
-
if (ack.nextIndex > 0) {
|
|
1332
|
-
const result = _compactBufferedUnsentSuffix(
|
|
1333
|
-
buffer,
|
|
1334
|
-
bufferIndex,
|
|
1335
|
-
this.canSkipTo
|
|
1336
|
-
);
|
|
1337
|
-
bufferIndex = result.newIndex;
|
|
1338
|
-
if (result.skipped > 0)
|
|
1339
|
-
this._skipCount += result.skipped;
|
|
1340
|
-
}
|
|
1341
|
-
if (this._acks.length > 0) break;
|
|
1342
|
-
}
|
|
1343
|
-
while (this._nextIndex < maxIndex && bufferIndex < buffer.count) {
|
|
1344
|
-
const item = buffer.get(bufferIndex++);
|
|
1345
|
-
if (item.kind === "value") {
|
|
1346
|
-
this.sendItem(item.value);
|
|
1347
|
-
} else if (item.kind === "end") {
|
|
1348
|
-
if (!this._ended) this.sendEnd();
|
|
1349
|
-
return;
|
|
1350
|
-
} else {
|
|
1351
|
-
if (!this._ended) this.sendEnd(item.error);
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
if (this._nextIndex >= maxIndex)
|
|
1356
|
-
break;
|
|
1357
|
-
if (isFullyBuffered) break;
|
|
1358
|
-
if (!buffer.hasRemainingCapacity) break;
|
|
1359
|
-
if (!isRealTime)
|
|
1360
|
-
await bufferNext(false, false);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1400
|
+
ackAdvance;
|
|
1401
|
+
/**
|
|
1402
|
+
* Effective local ring buffer capacity used by the pump. Set larger
|
|
1403
|
+
* than `ackAdvance` to pre-buffer items beyond the in-flight window;
|
|
1404
|
+
* values below `ackAdvance` are clamped up (with a warning) at
|
|
1405
|
+
* construction time. Not transmitted on the wire.
|
|
1406
|
+
*/
|
|
1407
|
+
bufferSize;
|
|
1408
|
+
/**
|
|
1409
|
+
* If true, the source was produced by a factory `(abortSignal) => AsyncIterable<T>`
|
|
1410
|
+
* and may honor the AbortSignal to exit gracefully. `disconnect()` first aborts
|
|
1411
|
+
* the signal and gives the source up to `RpcStream.disconnectGracePeriodMs` ms
|
|
1412
|
+
* before force-closing via `iterator.return()`.
|
|
1413
|
+
*
|
|
1414
|
+
* If false, the source is a plain AsyncIterable that cannot observe the signal,
|
|
1415
|
+
* so `disconnect()` force-closes immediately.
|
|
1416
|
+
*/
|
|
1417
|
+
sourceUsesAbortSignal;
|
|
1418
|
+
// -- Pump state --
|
|
1419
|
+
/** Next stream index that will be sent (matches .NET's `index`). */
|
|
1420
|
+
_nextIndex = 0;
|
|
1421
|
+
_ended = false;
|
|
1422
|
+
_started = new PromiseSource3();
|
|
1423
|
+
/** Highest `nextIndex` observed in an ACK so far (matches .NET's `lastAckNextIndex` idiom). */
|
|
1424
|
+
_lastAckedIndex = 0;
|
|
1425
|
+
_abortController = new AbortController();
|
|
1426
|
+
_iterator = null;
|
|
1427
|
+
// -- RTT sampling (send → matching ack round trips, windowed minimum) --
|
|
1428
|
+
_pendingSendTimes = [];
|
|
1429
|
+
_rttSamples = [];
|
|
1430
|
+
// -- Recorder-controller metrics (Step 9.1) --
|
|
1431
|
+
/** Total items skipped via real-time `canSkipTo` compaction. */
|
|
1432
|
+
_skipCount = 0;
|
|
1433
|
+
/** Fires after a $sys.Ack has been processed (post-compaction). The
|
|
1434
|
+
* recorder's quality controller uses this to bump a "last ACK at"
|
|
1435
|
+
* watchdog so it can distinguish "stuck" (no ACK > N s) from
|
|
1436
|
+
* "throttled" (ACKs flowing). */
|
|
1437
|
+
onAckProcessed;
|
|
1438
|
+
/** Fires after each item is pushed onto the local ring buffer. The
|
|
1439
|
+
* argument is the buffer count *after* the push. Together with
|
|
1440
|
+
* `onAckProcessed` (which is the only place items leave the buffer),
|
|
1441
|
+
* this gives controllers a complete picture of buffer utilization.
|
|
1442
|
+
* When `bufferedCount === bufferSize`, the source pull is paused
|
|
1443
|
+
* until the next ACK frees space. Listener errors are swallowed. */
|
|
1444
|
+
onBuffered;
|
|
1445
|
+
get nextIndex() {
|
|
1446
|
+
return this._nextIndex;
|
|
1363
1447
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
* A reconnect's reset ACK may be drained in one batch with regular
|
|
1367
|
-
* consumption ACKs the client sends right after it, so `mustReset` is
|
|
1368
|
-
* OR-ed over the whole batch — the last ACK alone can't represent it. */
|
|
1369
|
-
_tryProcessAcks() {
|
|
1370
|
-
if (this._acks.length === 0) return null;
|
|
1371
|
-
let nextIndex = 0;
|
|
1372
|
-
let mustReset = false;
|
|
1373
|
-
while (this._acks.length > 0) {
|
|
1374
|
-
const a = this._acks.shift();
|
|
1375
|
-
nextIndex = a.nextIndex;
|
|
1376
|
-
mustReset ||= a.mustReset;
|
|
1377
|
-
if (a.mustReset || this._nextIndex < a.nextIndex) {
|
|
1378
|
-
this._nextIndex = a.nextIndex;
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
try {
|
|
1382
|
-
this.onAckProcessed?.();
|
|
1383
|
-
} catch {
|
|
1384
|
-
}
|
|
1385
|
-
return { nextIndex, mustReset };
|
|
1448
|
+
get lastAckIndex() {
|
|
1449
|
+
return this._lastAckedIndex;
|
|
1386
1450
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
if (this._acks.length > 0 || this._ended) return;
|
|
1390
|
-
this._whenAckReady = new PromiseSource3();
|
|
1391
|
-
await this._whenAckReady;
|
|
1392
|
-
this._whenAckReady = null;
|
|
1451
|
+
get skipCount() {
|
|
1452
|
+
return this._skipCount;
|
|
1393
1453
|
}
|
|
1394
|
-
//
|
|
1395
|
-
//
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
this._pendingSendTimes.length = 0;
|
|
1399
|
-
return;
|
|
1400
|
-
}
|
|
1401
|
-
let sentAtMs = -1;
|
|
1402
|
-
while (this._pendingSendTimes.length > 0 && this._pendingSendTimes[0].index < ackNextIndex) {
|
|
1403
|
-
sentAtMs = this._pendingSendTimes[0].sentAtMs;
|
|
1404
|
-
this._pendingSendTimes.shift();
|
|
1405
|
-
}
|
|
1406
|
-
if (sentAtMs < 0)
|
|
1407
|
-
return;
|
|
1408
|
-
const nowMs = Date.now();
|
|
1409
|
-
const cutoffMs = nowMs - RTT_WINDOW_MS;
|
|
1454
|
+
// Windowed-MIN ack round trip (ms, -1 until sampled): converges on propagation
|
|
1455
|
+
// RTT — unlike a mean/EMA it is not inflated by self-induced queuing.
|
|
1456
|
+
get minRttMs() {
|
|
1457
|
+
const cutoffMs = Date.now() - RTT_WINDOW_MS;
|
|
1410
1458
|
while (this._rttSamples.length > 0 && this._rttSamples[0].atMs < cutoffMs)
|
|
1411
1459
|
this._rttSamples.shift();
|
|
1412
|
-
this._rttSamples.
|
|
1460
|
+
if (this._rttSamples.length === 0)
|
|
1461
|
+
return -1;
|
|
1462
|
+
let min = this._rttSamples[0].rttMs;
|
|
1463
|
+
for (const s of this._rttSamples) {
|
|
1464
|
+
if (s.rttMs < min)
|
|
1465
|
+
min = s.rttMs;
|
|
1466
|
+
}
|
|
1467
|
+
return min;
|
|
1413
1468
|
}
|
|
1414
|
-
// --
|
|
1415
|
-
|
|
1469
|
+
// -- Coalesced ACK state (analog of .NET's Channel<(long, bool)>) --
|
|
1470
|
+
// The pump reduces any queued ACKs to the newest index plus the OR of
|
|
1471
|
+
// `mustReset` anyway, so we accumulate exactly that at receipt: an ACK flood
|
|
1472
|
+
// then costs O(1) memory and O(1) work instead of an unbounded array drained
|
|
1473
|
+
// with repeated `shift()`.
|
|
1474
|
+
/** Newest unprocessed ACK index; -1 means "nothing pending". */
|
|
1475
|
+
_ackIndex = -1;
|
|
1476
|
+
_ackMustReset = false;
|
|
1477
|
+
/** Highest ACK index accepted so far — the monotonicity baseline a
|
|
1478
|
+
* non-reset ACK may not regress below. */
|
|
1479
|
+
_maxAckIndex = 0;
|
|
1480
|
+
_whenAckReady = null;
|
|
1481
|
+
constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true, isRealTime = false, canSkipTo = () => true, sourceUsesAbortSignal = false, bufferSize) {
|
|
1482
|
+
const localId = peer.sharedObjects.nextId();
|
|
1483
|
+
this.id = { hostId: peer.hub.hubId, localId };
|
|
1484
|
+
this.allowReconnect = allowReconnect;
|
|
1485
|
+
this.isRealTime = isRealTime;
|
|
1486
|
+
this.canSkipTo = canSkipTo;
|
|
1487
|
+
this.peer = peer;
|
|
1488
|
+
this.ackPeriod = ackPeriod;
|
|
1489
|
+
this.ackAdvance = ackAdvance;
|
|
1490
|
+
this.sourceUsesAbortSignal = sourceUsesAbortSignal;
|
|
1491
|
+
if (bufferSize !== void 0 && bufferSize !== 0 && bufferSize < ackAdvance)
|
|
1492
|
+
warnLog5?.log(
|
|
1493
|
+
`RpcStream bufferSize (${bufferSize}) is below ackAdvance (${ackAdvance}); using ackAdvance as buffer size.`
|
|
1494
|
+
);
|
|
1495
|
+
this.bufferSize = bufferSize !== void 0 && bufferSize >= ackAdvance ? bufferSize : ackAdvance;
|
|
1416
1496
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
this.
|
|
1420
|
-
|
|
1497
|
+
/** AbortSignal that is aborted when the sender is disconnected. */
|
|
1498
|
+
get abortSignal() {
|
|
1499
|
+
return this._abortController.signal;
|
|
1500
|
+
}
|
|
1501
|
+
/** Returns the stream reference string for the $sys.Ok response. */
|
|
1502
|
+
toRef() {
|
|
1503
|
+
return `${this.id.hostId},${this.id.localId},${this.ackPeriod},${this.ackAdvance},${this.allowReconnect ? "1" : "0"},${this.isRealTime ? "1" : "0"}`;
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Called by system call handler when $sys.Ack is received from the client.
|
|
1507
|
+
*
|
|
1508
|
+
* Mirrors .NET `RpcSharedStream<T>.OnAck` at
|
|
1509
|
+
* src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:68-101.
|
|
1510
|
+
*/
|
|
1511
|
+
onAck(nextIndex, hostId) {
|
|
1512
|
+
const mustReset = hostId !== "" && hostId !== _RpcStreamSender._emptyGuid;
|
|
1513
|
+
if (mustReset && hostId !== this.id.hostId) {
|
|
1514
|
+
this._sendDisconnect();
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
if (!Number.isSafeInteger(nextIndex) || nextIndex < 0 || nextIndex > this._nextIndex) {
|
|
1518
|
+
this._sendDisconnect();
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1421
1521
|
if (!this._started.isCompleted) {
|
|
1422
|
-
|
|
1522
|
+
if (mustReset && nextIndex === 0)
|
|
1523
|
+
this._started.resolve();
|
|
1524
|
+
else {
|
|
1525
|
+
this._sendDisconnect();
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
} else if (this._ended) {
|
|
1529
|
+
this._sendDisconnect();
|
|
1530
|
+
return;
|
|
1531
|
+
} else if (mustReset && !this.allowReconnect) {
|
|
1532
|
+
this._sendDisconnect();
|
|
1533
|
+
return;
|
|
1423
1534
|
}
|
|
1535
|
+
if (!mustReset && nextIndex < this._maxAckIndex)
|
|
1536
|
+
return;
|
|
1537
|
+
this._maxAckIndex = nextIndex;
|
|
1538
|
+
this._sampleRtt(nextIndex, mustReset);
|
|
1539
|
+
this._ackIndex = nextIndex;
|
|
1540
|
+
this._ackMustReset ||= mustReset;
|
|
1424
1541
|
if (this._whenAckReady) {
|
|
1425
1542
|
this._whenAckReady.resolve();
|
|
1426
1543
|
this._whenAckReady = null;
|
|
1427
1544
|
}
|
|
1428
|
-
this.peer.sharedObjects.unregister(this);
|
|
1429
|
-
const iterator = this._iterator;
|
|
1430
|
-
if (!iterator) return;
|
|
1431
|
-
if (!this.sourceUsesAbortSignal) {
|
|
1432
|
-
this._iterator = null;
|
|
1433
|
-
this._forceCloseIterator(iterator);
|
|
1434
|
-
return;
|
|
1435
|
-
}
|
|
1436
|
-
setTimeout(() => {
|
|
1437
|
-
if (this._iterator === iterator) {
|
|
1438
|
-
this._iterator = null;
|
|
1439
|
-
this._forceCloseIterator(iterator);
|
|
1440
|
-
}
|
|
1441
|
-
}, RpcStream.disconnectGracePeriodMs);
|
|
1442
|
-
}
|
|
1443
|
-
_forceCloseIterator(iterator) {
|
|
1444
|
-
try {
|
|
1445
|
-
const ret = iterator.return?.(void 0);
|
|
1446
|
-
if (ret) void ret.catch(() => {
|
|
1447
|
-
});
|
|
1448
|
-
} catch {
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
};
|
|
1452
|
-
var _endItem = { kind: "end" };
|
|
1453
|
-
function _clamp(value, min, max) {
|
|
1454
|
-
return value < min ? min : value > max ? max : value;
|
|
1455
|
-
}
|
|
1456
|
-
function _compactBufferedUnsentSuffix(buffer, firstUnsentIndex, canSkipTo) {
|
|
1457
|
-
let skipToIndex = -1;
|
|
1458
|
-
for (let i = firstUnsentIndex; i < buffer.count; i++) {
|
|
1459
|
-
const item = buffer.get(i);
|
|
1460
|
-
if (item.kind === "value" && canSkipTo(item.value))
|
|
1461
|
-
skipToIndex = i;
|
|
1462
1545
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
buffer.pushTail(items[i]);
|
|
1471
|
-
for (let i = cutTo; i < items.length; i++)
|
|
1472
|
-
buffer.pushTail(items[i]);
|
|
1473
|
-
return { newIndex: firstUnsentIndex, skipped: skipToIndex - firstUnsentIndex };
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
// src/rpc-stream.ts
|
|
1477
|
-
var { warnLog: warnLog4 } = getLogs("RpcStream");
|
|
1478
|
-
var DEFAULT_ACK_PERIOD2 = 30;
|
|
1479
|
-
var DEFAULT_ACK_ADVANCE2 = 61;
|
|
1480
|
-
function parseStreamRef(value) {
|
|
1481
|
-
if (typeof value === "string") {
|
|
1482
|
-
const parts = value.split(",");
|
|
1483
|
-
if (parts.length < 4 || parts.length > 6) return null;
|
|
1484
|
-
const hostId = parts[0];
|
|
1485
|
-
const localId = parseInt(parts[1], 10);
|
|
1486
|
-
const ackPeriod = parseInt(parts[2], 10);
|
|
1487
|
-
const ackAdvance = parseInt(parts[3], 10);
|
|
1488
|
-
if (isNaN(localId) || isNaN(ackPeriod) || isNaN(ackAdvance))
|
|
1489
|
-
return null;
|
|
1490
|
-
const allowReconnect = parts.length < 5 || parts[4] !== "0";
|
|
1491
|
-
const isRealTime = parts.length >= 6 && parts[5] === "1";
|
|
1492
|
-
return { hostId, localId, ackPeriod, ackAdvance, allowReconnect, isRealTime };
|
|
1546
|
+
// Tells the consumer this shared stream is gone, then disposes it —
|
|
1547
|
+
// mirrors RpcSharedStream.SendDisconnect (RpcSharedStream.cs:284-289).
|
|
1548
|
+
_sendDisconnect() {
|
|
1549
|
+
const conn = this.peer.connection;
|
|
1550
|
+
if (conn)
|
|
1551
|
+
this.peer.hub.systemCallSender.disconnect(conn, this.peer.serializationFormat, [this.id.localId]);
|
|
1552
|
+
this.disconnect();
|
|
1493
1553
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
if (!Array.isArray(serializedId) || serializedId.length < 2)
|
|
1498
|
-
return null;
|
|
1499
|
-
const rawHostId = serializedId[0];
|
|
1500
|
-
const hostId = String(rawHostId);
|
|
1501
|
-
const localId = Number(serializedId[1]);
|
|
1502
|
-
const ackPeriod = Number(obj.AckPeriod ?? 256);
|
|
1503
|
-
const ackAdvance = Number(obj.AckAdvance ?? 128);
|
|
1504
|
-
const allowReconnect = obj.AllowReconnect !== false;
|
|
1505
|
-
const isRealTime = obj.IsRealTime === true;
|
|
1506
|
-
return { hostId, localId, ackPeriod, ackAdvance, allowReconnect, isRealTime };
|
|
1554
|
+
/** Called by system call handler when $sys.AckEnd is received from the client. */
|
|
1555
|
+
onAckEnd(_hostId) {
|
|
1556
|
+
this.disconnect();
|
|
1507
1557
|
}
|
|
1508
|
-
return null;
|
|
1509
|
-
}
|
|
1510
|
-
var RpcStream = class _RpcStream {
|
|
1511
1558
|
/**
|
|
1512
|
-
*
|
|
1513
|
-
* AbortSignal and exit naturally before the sender force-closes the
|
|
1514
|
-
* iterator via `iterator.return()`.
|
|
1559
|
+
* Send a single item to the client at the current `_nextIndex`.
|
|
1515
1560
|
*
|
|
1516
|
-
*
|
|
1517
|
-
*
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
kind;
|
|
1522
|
-
allowReconnect;
|
|
1523
|
-
isRealTime;
|
|
1524
|
-
canSkipTo;
|
|
1525
|
-
ackPeriod;
|
|
1526
|
-
ackAdvance;
|
|
1527
|
-
/**
|
|
1528
|
-
* Local-only ring buffer capacity hint for the sender. Undefined means
|
|
1529
|
-
* "use ackAdvance as the buffer size". Not part of the wire ref.
|
|
1561
|
+
* Advances `_nextIndex` unconditionally — even if the peer is disconnected
|
|
1562
|
+
* the item is considered "sent" from the pump's perspective so the replay
|
|
1563
|
+
* buffer indexing stays consistent. If a connection is absent, the wire
|
|
1564
|
+
* message is dropped; the item will be resent from the replay buffer
|
|
1565
|
+
* after a reconnect (client sends `Ack(N, mustReset=true)`).
|
|
1530
1566
|
*/
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
_completionError = null;
|
|
1546
|
-
_disposed = false;
|
|
1547
|
-
_started = false;
|
|
1548
|
-
_iterating = false;
|
|
1549
|
-
_ackSentUpTo = 0;
|
|
1550
|
-
_nextConsumedIndex = 0;
|
|
1551
|
-
constructor(sourceOrRef, optionsOrPeer) {
|
|
1552
|
-
if (_isStreamSource(sourceOrRef)) {
|
|
1553
|
-
const options = optionsOrPeer ?? {};
|
|
1554
|
-
this.kind = RpcObjectKind.Local;
|
|
1555
|
-
this.localSource = sourceOrRef;
|
|
1556
|
-
this.id = { hostId: "", localId: 0 };
|
|
1557
|
-
this.allowReconnect = options.allowReconnect ?? true;
|
|
1558
|
-
this.isRealTime = options.isRealTime ?? false;
|
|
1559
|
-
this.canSkipTo = options.canSkipTo ?? (() => true);
|
|
1560
|
-
this.ackPeriod = options.ackPeriod ?? DEFAULT_ACK_PERIOD2;
|
|
1561
|
-
this.ackAdvance = options.ackAdvance ?? DEFAULT_ACK_ADVANCE2;
|
|
1562
|
-
this.bufferSize = options.bufferSize;
|
|
1563
|
-
} else {
|
|
1564
|
-
const ref = sourceOrRef;
|
|
1565
|
-
const peer = optionsOrPeer;
|
|
1566
|
-
this.kind = RpcObjectKind.Remote;
|
|
1567
|
-
this.id = { hostId: ref.hostId, localId: ref.localId };
|
|
1568
|
-
this.peer = peer;
|
|
1569
|
-
this.allowReconnect = ref.allowReconnect;
|
|
1570
|
-
this.isRealTime = ref.isRealTime;
|
|
1571
|
-
this.canSkipTo = () => true;
|
|
1572
|
-
this.ackPeriod = ref.ackPeriod;
|
|
1573
|
-
this.ackAdvance = ref.ackAdvance;
|
|
1567
|
+
sendItem(item) {
|
|
1568
|
+
if (this._ended) return;
|
|
1569
|
+
const conn = this.peer.connection;
|
|
1570
|
+
if (conn) {
|
|
1571
|
+
this.peer.hub.systemCallSender.item(
|
|
1572
|
+
conn,
|
|
1573
|
+
this.peer.serializationFormat,
|
|
1574
|
+
this.id.localId,
|
|
1575
|
+
this._nextIndex,
|
|
1576
|
+
item
|
|
1577
|
+
);
|
|
1578
|
+
this._pendingSendTimes.push({ index: this._nextIndex, sentAtMs: Date.now() });
|
|
1579
|
+
if (this._pendingSendTimes.length > MAX_PENDING_SEND_TIMES)
|
|
1580
|
+
this._pendingSendTimes.splice(0, MAX_PENDING_SEND_TIMES >> 1);
|
|
1574
1581
|
}
|
|
1582
|
+
this._nextIndex++;
|
|
1575
1583
|
}
|
|
1576
|
-
/**
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
+
/** Send a batch of items to the client. See {@link sendItem} for disconnect semantics. */
|
|
1585
|
+
sendBatch(items) {
|
|
1586
|
+
if (this._ended || items.length === 0) return;
|
|
1587
|
+
const conn = this.peer.connection;
|
|
1588
|
+
if (conn) {
|
|
1589
|
+
this.peer.hub.systemCallSender.batch(
|
|
1590
|
+
conn,
|
|
1591
|
+
this.peer.serializationFormat,
|
|
1592
|
+
this.id.localId,
|
|
1593
|
+
this._nextIndex,
|
|
1594
|
+
items
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
this._nextIndex += items.length;
|
|
1584
1598
|
}
|
|
1585
|
-
/**
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1599
|
+
/** Signal stream completion to the client. */
|
|
1600
|
+
sendEnd(error) {
|
|
1601
|
+
if (this._ended) return;
|
|
1602
|
+
this._ended = true;
|
|
1603
|
+
const conn = this.peer.connection;
|
|
1604
|
+
if (!conn) return;
|
|
1605
|
+
const errorInfo = error ? toExceptionInfo(error) : { TypeRef: "", Message: "" };
|
|
1606
|
+
this.peer.hub.systemCallSender.end(
|
|
1607
|
+
conn,
|
|
1608
|
+
this.peer.serializationFormat,
|
|
1609
|
+
this.id.localId,
|
|
1610
|
+
this._nextIndex,
|
|
1611
|
+
errorInfo
|
|
1612
|
+
);
|
|
1589
1613
|
}
|
|
1590
1614
|
/**
|
|
1591
|
-
*
|
|
1592
|
-
* start pumping items from the local source in the background, and return the
|
|
1593
|
-
* stream reference.
|
|
1594
|
-
*
|
|
1595
|
-
* For binary peers the ref is a MessagePack-compatible object
|
|
1596
|
-
* (`{ SerializedId, AckPeriod, AckAdvance, AllowReconnect }`);
|
|
1597
|
-
* for text peers it is a comma-separated string.
|
|
1615
|
+
* Consume an AsyncIterable and send all items to the client.
|
|
1598
1616
|
*
|
|
1599
|
-
*
|
|
1617
|
+
* Matches .NET `RpcSharedStream<T>.OnRun` at
|
|
1618
|
+
* src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs.
|
|
1600
1619
|
*/
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1620
|
+
async writeFrom(source) {
|
|
1621
|
+
await this._started;
|
|
1622
|
+
if (this._ended) return;
|
|
1623
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
1624
|
+
this._iterator = iterator;
|
|
1625
|
+
const state = { iteratorDone: false };
|
|
1626
|
+
try {
|
|
1627
|
+
await this._run(iterator, state);
|
|
1628
|
+
} finally {
|
|
1629
|
+
if (state.iteratorDone && this._iterator === iterator) {
|
|
1630
|
+
this._iterator = null;
|
|
1631
|
+
}
|
|
1608
1632
|
}
|
|
1609
|
-
const isFactory = typeof this.localSource === "function";
|
|
1610
|
-
const sender = new RpcStreamSender(
|
|
1611
|
-
peer,
|
|
1612
|
-
this.ackPeriod,
|
|
1613
|
-
this.ackAdvance,
|
|
1614
|
-
this.allowReconnect,
|
|
1615
|
-
this.isRealTime,
|
|
1616
|
-
this.canSkipTo,
|
|
1617
|
-
isFactory,
|
|
1618
|
-
this.bufferSize
|
|
1619
|
-
);
|
|
1620
|
-
peer.sharedObjects.register(sender);
|
|
1621
|
-
this._sender = sender;
|
|
1622
|
-
const source = isFactory ? this.localSource(sender.abortSignal) : this.localSource;
|
|
1623
|
-
this._whenSent = sender.writeFrom(source);
|
|
1624
|
-
this._toRefValue = peer.serializationFormat.isBinary ? {
|
|
1625
|
-
SerializedId: [sender.id.hostId, sender.id.localId],
|
|
1626
|
-
AckPeriod: sender.ackPeriod,
|
|
1627
|
-
AckAdvance: sender.ackAdvance,
|
|
1628
|
-
AllowReconnect: sender.allowReconnect
|
|
1629
|
-
} : sender.toRef();
|
|
1630
|
-
return this._toRefValue;
|
|
1631
1633
|
}
|
|
1632
|
-
/**
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1634
|
+
/**
|
|
1635
|
+
* ACK-driven pump. Non-real-time streams apply normal backpressure at
|
|
1636
|
+
* `ackAdvance`; real-time streams may compact the already-buffered unsent
|
|
1637
|
+
* suffix to the latest buffered `canSkipTo` item after the consumer ACKs.
|
|
1638
|
+
* With `bufferSize > ackAdvance` the local ring buffer holds more than the
|
|
1639
|
+
* in-flight window — real-time mode uses the extra space to pre-buffer.
|
|
1640
|
+
*/
|
|
1641
|
+
/* eslint-disable @typescript-eslint/no-unnecessary-condition -- _ended changes across awaits */
|
|
1642
|
+
async _run(iterator, state) {
|
|
1643
|
+
const buffer = new RingBuffer(this.bufferSize + 1);
|
|
1644
|
+
const isRealTime = this.isRealTime;
|
|
1645
|
+
let bufferStart = 0;
|
|
1646
|
+
let isFullyBuffered = false;
|
|
1647
|
+
let whenMovedNext = null;
|
|
1648
|
+
const pending = /* @__PURE__ */ Symbol("pending");
|
|
1649
|
+
const readNext = async () => {
|
|
1650
|
+
const next = whenMovedNext ?? iterator.next();
|
|
1651
|
+
whenMovedNext = null;
|
|
1652
|
+
return await next;
|
|
1653
|
+
};
|
|
1654
|
+
const tryReadReady = async () => {
|
|
1655
|
+
whenMovedNext ??= iterator.next();
|
|
1656
|
+
return await Promise.race([
|
|
1657
|
+
whenMovedNext,
|
|
1658
|
+
Promise.resolve(pending)
|
|
1659
|
+
]);
|
|
1660
|
+
};
|
|
1661
|
+
const fireBuffered = () => {
|
|
1662
|
+
try {
|
|
1663
|
+
this.onBuffered?.(buffer.count);
|
|
1664
|
+
} catch {
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
const bufferNext = async (onlyIfReady, prefetchNext) => {
|
|
1668
|
+
let r;
|
|
1669
|
+
try {
|
|
1670
|
+
r = onlyIfReady ? await tryReadReady() : await readNext();
|
|
1671
|
+
if (r === pending)
|
|
1672
|
+
return false;
|
|
1673
|
+
whenMovedNext = null;
|
|
1674
|
+
if (this._ended) return false;
|
|
1675
|
+
if (r.done) {
|
|
1676
|
+
buffer.pushTail(_endItem);
|
|
1677
|
+
isFullyBuffered = true;
|
|
1678
|
+
state.iteratorDone = true;
|
|
1679
|
+
} else {
|
|
1680
|
+
buffer.pushTail({ kind: "value", value: r.value });
|
|
1681
|
+
if (prefetchNext)
|
|
1682
|
+
whenMovedNext = iterator.next();
|
|
1683
|
+
}
|
|
1684
|
+
fireBuffered();
|
|
1685
|
+
return true;
|
|
1686
|
+
} catch (e) {
|
|
1687
|
+
whenMovedNext = null;
|
|
1688
|
+
state.iteratorDone = true;
|
|
1689
|
+
isFullyBuffered = true;
|
|
1690
|
+
buffer.pushTail({
|
|
1691
|
+
kind: "error",
|
|
1692
|
+
error: e instanceof Error ? e : new Error(String(e))
|
|
1693
|
+
});
|
|
1694
|
+
fireBuffered();
|
|
1695
|
+
return true;
|
|
1640
1696
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
/** Called by system call handler when a batch arrives ($sys.B). */
|
|
1653
|
-
onBatch(index, items) {
|
|
1654
|
-
if (this._disposed || this._completed) return;
|
|
1655
|
-
if (index > this._nextExpectedIndex) {
|
|
1656
|
-
warnLog4?.log(`batch index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
|
|
1657
|
-
if (!this.allowReconnect) {
|
|
1658
|
-
this._complete(this._gapError(index));
|
|
1659
|
-
return;
|
|
1697
|
+
};
|
|
1698
|
+
while (true) {
|
|
1699
|
+
let ack = this._tryProcessAcks();
|
|
1700
|
+
if (!ack) {
|
|
1701
|
+
await this._waitAckReady();
|
|
1702
|
+
if (this._ended) return;
|
|
1703
|
+
ack = this._tryProcessAcks();
|
|
1704
|
+
if (!ack) {
|
|
1705
|
+
warnLog5?.log("Something is off: couldn't read an acknowledgement");
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1660
1708
|
}
|
|
1661
|
-
this.
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1709
|
+
this._lastAckedIndex = ack.nextIndex;
|
|
1710
|
+
if (isRealTime && ack.mustReset && !isFullyBuffered) {
|
|
1711
|
+
buffer.clear();
|
|
1712
|
+
bufferStart = this._nextIndex;
|
|
1713
|
+
while (true) {
|
|
1714
|
+
if (this._ended) return;
|
|
1715
|
+
let item;
|
|
1716
|
+
let accepted = false;
|
|
1717
|
+
try {
|
|
1718
|
+
const r = await readNext();
|
|
1719
|
+
if (this._ended) return;
|
|
1720
|
+
if (r.done) {
|
|
1721
|
+
item = _endItem;
|
|
1722
|
+
isFullyBuffered = true;
|
|
1723
|
+
state.iteratorDone = true;
|
|
1724
|
+
} else if (this.canSkipTo(r.value)) {
|
|
1725
|
+
item = { kind: "value", value: r.value };
|
|
1726
|
+
accepted = true;
|
|
1727
|
+
} else {
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
} catch (e) {
|
|
1731
|
+
state.iteratorDone = true;
|
|
1732
|
+
isFullyBuffered = true;
|
|
1733
|
+
item = {
|
|
1734
|
+
kind: "error",
|
|
1735
|
+
error: e instanceof Error ? e : new Error(String(e))
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
buffer.pushTail(item);
|
|
1739
|
+
fireBuffered();
|
|
1740
|
+
if (accepted || isFullyBuffered) break;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
const shift = _clamp(ack.nextIndex - bufferStart, 0, buffer.count);
|
|
1744
|
+
if (shift > 0) {
|
|
1745
|
+
buffer.moveHead(shift);
|
|
1746
|
+
bufferStart += shift;
|
|
1747
|
+
}
|
|
1748
|
+
if (this._nextIndex < bufferStart) {
|
|
1749
|
+
if (!this._ended)
|
|
1750
|
+
this.sendEnd(new Error("Stream position unavailable."));
|
|
1679
1751
|
return;
|
|
1680
1752
|
}
|
|
1681
|
-
this.
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
}
|
|
1690
|
-
// -- IRpcObject --
|
|
1691
|
-
reconnect() {
|
|
1692
|
-
if (this.kind === RpcObjectKind.Local) return;
|
|
1693
|
-
if (this.allowReconnect) {
|
|
1694
|
-
this._sendAck(this._nextExpectedIndex, true);
|
|
1695
|
-
} else {
|
|
1696
|
-
this.disconnect();
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
disconnect() {
|
|
1700
|
-
if (this.kind === RpcObjectKind.Local) {
|
|
1701
|
-
this._sender?.disconnect();
|
|
1702
|
-
return;
|
|
1703
|
-
}
|
|
1704
|
-
if (this._completed) return;
|
|
1705
|
-
this._completed = true;
|
|
1706
|
-
this._completionError = new Error("Peer disconnected.");
|
|
1707
|
-
this._notifyConsumer();
|
|
1708
|
-
}
|
|
1709
|
-
// -- AsyncIterable --
|
|
1710
|
-
[Symbol.asyncIterator]() {
|
|
1711
|
-
if (this.kind === RpcObjectKind.Local) {
|
|
1712
|
-
const source = typeof this.localSource === "function" ? this.localSource(new AbortController().signal) : this.localSource;
|
|
1713
|
-
return source[Symbol.asyncIterator]();
|
|
1714
|
-
}
|
|
1715
|
-
if (this._iterating)
|
|
1716
|
-
throw new Error("RpcStream can only be iterated once.");
|
|
1717
|
-
this._iterating = true;
|
|
1718
|
-
return {
|
|
1719
|
-
next: async () => {
|
|
1720
|
-
if (!this._started) {
|
|
1721
|
-
this._started = true;
|
|
1722
|
-
this.peer.remoteObjects.register(this);
|
|
1723
|
-
this._sendAck(0, true);
|
|
1724
|
-
}
|
|
1725
|
-
this._maybeSendAck(this._nextConsumedIndex);
|
|
1726
|
-
for (; ; ) {
|
|
1727
|
-
if (!this._buffer.isEmpty()) {
|
|
1728
|
-
const value = this._buffer.shift();
|
|
1729
|
-
this._nextConsumedIndex++;
|
|
1730
|
-
return { value, done: false };
|
|
1753
|
+
let bufferIndex = this._nextIndex - bufferStart;
|
|
1754
|
+
const maxIndex = ack.nextIndex + this.ackAdvance;
|
|
1755
|
+
while (this._nextIndex < maxIndex) {
|
|
1756
|
+
if (this._ended) return;
|
|
1757
|
+
if (isRealTime) {
|
|
1758
|
+
while (buffer.hasRemainingCapacity && !isFullyBuffered && !this._hasPendingAck) {
|
|
1759
|
+
if (!await bufferNext(true, true))
|
|
1760
|
+
break;
|
|
1731
1761
|
}
|
|
1732
|
-
if (this.
|
|
1733
|
-
|
|
1734
|
-
if (this._completionError) throw this._completionError;
|
|
1735
|
-
return { value: void 0, done: true };
|
|
1762
|
+
if (bufferIndex >= buffer.count && !isFullyBuffered && buffer.hasRemainingCapacity && !this._hasPendingAck) {
|
|
1763
|
+
await bufferNext(false, true);
|
|
1736
1764
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1765
|
+
if (ack.nextIndex > 0) {
|
|
1766
|
+
const result = _compactBufferedUnsentSuffix(
|
|
1767
|
+
buffer,
|
|
1768
|
+
bufferIndex,
|
|
1769
|
+
this.canSkipTo
|
|
1770
|
+
);
|
|
1771
|
+
bufferIndex = result.newIndex;
|
|
1772
|
+
if (result.skipped > 0)
|
|
1773
|
+
this._skipCount += result.skipped;
|
|
1774
|
+
}
|
|
1775
|
+
if (this._hasPendingAck) break;
|
|
1739
1776
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1777
|
+
while (this._nextIndex < maxIndex && bufferIndex < buffer.count) {
|
|
1778
|
+
const item = buffer.get(bufferIndex++);
|
|
1779
|
+
if (item.kind === "value") {
|
|
1780
|
+
this.sendItem(item.value);
|
|
1781
|
+
} else if (item.kind === "end") {
|
|
1782
|
+
if (!this._ended) this.sendEnd();
|
|
1783
|
+
return;
|
|
1784
|
+
} else {
|
|
1785
|
+
if (!this._ended) this.sendEnd(item.error);
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
if (this._nextIndex >= maxIndex)
|
|
1790
|
+
break;
|
|
1791
|
+
if (isFullyBuffered) break;
|
|
1792
|
+
if (!buffer.hasRemainingCapacity) break;
|
|
1793
|
+
if (!isRealTime)
|
|
1794
|
+
await bufferNext(false, false);
|
|
1746
1795
|
}
|
|
1747
|
-
};
|
|
1748
|
-
}
|
|
1749
|
-
dispose() {
|
|
1750
|
-
if (this._disposed) return;
|
|
1751
|
-
this._disposed = true;
|
|
1752
|
-
this._completed = true;
|
|
1753
|
-
if (this.kind === RpcObjectKind.Remote) {
|
|
1754
|
-
this.peer.remoteObjects.unregister(this);
|
|
1755
1796
|
}
|
|
1756
|
-
this._notifyConsumer();
|
|
1757
1797
|
}
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
_complete(error) {
|
|
1762
|
-
if (this._completed) return;
|
|
1763
|
-
this._completed = true;
|
|
1764
|
-
this._completionError = error;
|
|
1765
|
-
this._sendAckEnd();
|
|
1766
|
-
if (this.kind === RpcObjectKind.Remote)
|
|
1767
|
-
this.peer.remoteObjects.unregister(this);
|
|
1768
|
-
this._notifyConsumer();
|
|
1798
|
+
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
|
|
1799
|
+
get _hasPendingAck() {
|
|
1800
|
+
return this._ackIndex >= 0;
|
|
1769
1801
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1802
|
+
/** Consume the coalesced ACK state (or null if nothing is pending).
|
|
1803
|
+
* A reconnect's reset ACK may be coalesced with the regular consumption
|
|
1804
|
+
* ACKs the client sends right after it, so `mustReset` accumulates across
|
|
1805
|
+
* the whole batch — the newest ACK alone can't represent it. */
|
|
1806
|
+
_tryProcessAcks() {
|
|
1807
|
+
const nextIndex = this._ackIndex;
|
|
1808
|
+
if (nextIndex < 0)
|
|
1809
|
+
return null;
|
|
1810
|
+
const mustReset = this._ackMustReset;
|
|
1811
|
+
this._ackIndex = -1;
|
|
1812
|
+
this._ackMustReset = false;
|
|
1813
|
+
if (mustReset || this._nextIndex < nextIndex)
|
|
1814
|
+
this._nextIndex = nextIndex;
|
|
1815
|
+
try {
|
|
1816
|
+
this.onAckProcessed?.();
|
|
1817
|
+
} catch {
|
|
1818
|
+
}
|
|
1819
|
+
return { nextIndex, mustReset };
|
|
1774
1820
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1821
|
+
/** Wait until an ACK is pending (or `_ended`). */
|
|
1822
|
+
async _waitAckReady() {
|
|
1823
|
+
if (this._hasPendingAck || this._ended)
|
|
1824
|
+
return;
|
|
1825
|
+
this._whenAckReady = new PromiseSource3();
|
|
1826
|
+
await this._whenAckReady;
|
|
1827
|
+
this._whenAckReady = null;
|
|
1828
|
+
}
|
|
1829
|
+
// One RTT sample per ack: time from sending the newest acked item to the
|
|
1830
|
+
// ack's arrival (a slight upper bound — includes the server's ack cadence).
|
|
1831
|
+
_sampleRtt(ackNextIndex, mustReset) {
|
|
1832
|
+
if (mustReset) {
|
|
1833
|
+
this._pendingSendTimes.length = 0;
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
let sentAtMs = -1;
|
|
1837
|
+
while (this._pendingSendTimes.length > 0 && this._pendingSendTimes[0].index < ackNextIndex) {
|
|
1838
|
+
sentAtMs = this._pendingSendTimes[0].sentAtMs;
|
|
1839
|
+
this._pendingSendTimes.shift();
|
|
1779
1840
|
}
|
|
1841
|
+
if (sentAtMs < 0)
|
|
1842
|
+
return;
|
|
1843
|
+
const nowMs = Date.now();
|
|
1844
|
+
const cutoffMs = nowMs - RTT_WINDOW_MS;
|
|
1845
|
+
while (this._rttSamples.length > 0 && this._rttSamples[0].atMs < cutoffMs)
|
|
1846
|
+
this._rttSamples.shift();
|
|
1847
|
+
this._rttSamples.push({ atMs: nowMs, rttMs: nowMs - sentAtMs });
|
|
1848
|
+
}
|
|
1849
|
+
// -- IRpcObject --
|
|
1850
|
+
reconnect() {
|
|
1780
1851
|
}
|
|
1781
|
-
|
|
1782
|
-
if (this.
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1852
|
+
disconnect() {
|
|
1853
|
+
if (this._ended) return;
|
|
1854
|
+
this._ended = true;
|
|
1855
|
+
this._abortController.abort();
|
|
1856
|
+
if (!this._started.isCompleted) {
|
|
1857
|
+
this._started.resolve();
|
|
1786
1858
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
nextIndex,
|
|
1799
|
-
hostId
|
|
1800
|
-
);
|
|
1859
|
+
if (this._whenAckReady) {
|
|
1860
|
+
this._whenAckReady.resolve();
|
|
1861
|
+
this._whenAckReady = null;
|
|
1862
|
+
}
|
|
1863
|
+
this.peer.sharedObjects.unregister(this);
|
|
1864
|
+
const iterator = this._iterator;
|
|
1865
|
+
if (!iterator) return;
|
|
1866
|
+
if (!this.sourceUsesAbortSignal) {
|
|
1867
|
+
this._iterator = null;
|
|
1868
|
+
this._forceCloseIterator(iterator);
|
|
1869
|
+
return;
|
|
1801
1870
|
}
|
|
1871
|
+
setTimeout(() => {
|
|
1872
|
+
if (this._iterator === iterator) {
|
|
1873
|
+
this._iterator = null;
|
|
1874
|
+
this._forceCloseIterator(iterator);
|
|
1875
|
+
}
|
|
1876
|
+
}, RpcStream.disconnectGracePeriodMs);
|
|
1802
1877
|
}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
this.id.localId,
|
|
1810
|
-
this.id.hostId
|
|
1811
|
-
);
|
|
1878
|
+
_forceCloseIterator(iterator) {
|
|
1879
|
+
try {
|
|
1880
|
+
const ret = iterator.return?.(void 0);
|
|
1881
|
+
if (ret) void ret.catch(() => {
|
|
1882
|
+
});
|
|
1883
|
+
} catch {
|
|
1812
1884
|
}
|
|
1813
1885
|
}
|
|
1814
1886
|
};
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
return value
|
|
1887
|
+
var _endItem = { kind: "end" };
|
|
1888
|
+
function _clamp(value, min, max) {
|
|
1889
|
+
return value < min ? min : value > max ? max : value;
|
|
1818
1890
|
}
|
|
1819
|
-
function
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
const
|
|
1823
|
-
if (
|
|
1824
|
-
|
|
1825
|
-
}
|
|
1826
|
-
return value;
|
|
1827
|
-
}
|
|
1828
|
-
if (Array.isArray(value)) {
|
|
1829
|
-
for (let i = 0; i < value.length; i++) {
|
|
1830
|
-
value[i] = resolveStreamRefs(value[i], peer);
|
|
1831
|
-
}
|
|
1832
|
-
return value;
|
|
1833
|
-
}
|
|
1834
|
-
if (typeof value === "object") {
|
|
1835
|
-
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer)
|
|
1836
|
-
return value;
|
|
1837
|
-
for (const key of Object.keys(value)) {
|
|
1838
|
-
value[key] = resolveStreamRefs(
|
|
1839
|
-
value[key],
|
|
1840
|
-
peer
|
|
1841
|
-
);
|
|
1842
|
-
}
|
|
1843
|
-
return value;
|
|
1891
|
+
function _compactBufferedUnsentSuffix(buffer, firstUnsentIndex, canSkipTo) {
|
|
1892
|
+
let skipToIndex = -1;
|
|
1893
|
+
for (let i = firstUnsentIndex; i < buffer.count; i++) {
|
|
1894
|
+
const item = buffer.get(i);
|
|
1895
|
+
if (item.kind === "value" && canSkipTo(item.value))
|
|
1896
|
+
skipToIndex = i;
|
|
1844
1897
|
}
|
|
1845
|
-
|
|
1898
|
+
if (skipToIndex <= firstUnsentIndex)
|
|
1899
|
+
return { newIndex: firstUnsentIndex, skipped: 0 };
|
|
1900
|
+
const cutFrom = firstUnsentIndex;
|
|
1901
|
+
const cutTo = skipToIndex;
|
|
1902
|
+
const items = buffer.toArray();
|
|
1903
|
+
buffer.clear();
|
|
1904
|
+
for (let i = 0; i < cutFrom; i++)
|
|
1905
|
+
buffer.pushTail(items[i]);
|
|
1906
|
+
for (let i = cutTo; i < items.length; i++)
|
|
1907
|
+
buffer.pushTail(items[i]);
|
|
1908
|
+
return { newIndex: firstUnsentIndex, skipped: skipToIndex - firstUnsentIndex };
|
|
1846
1909
|
}
|
|
1847
1910
|
|
|
1848
|
-
// src/
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
);
|
|
1911
|
+
// src/rpc-stream.ts
|
|
1912
|
+
var { warnLog: warnLog6 } = getLogs("RpcStream");
|
|
1913
|
+
var DEFAULT_ACK_PERIOD2 = 30;
|
|
1914
|
+
var DEFAULT_ACK_ADVANCE2 = 61;
|
|
1915
|
+
function parseStreamRef(value) {
|
|
1916
|
+
if (typeof value === "string") {
|
|
1917
|
+
const parts = value.split(",");
|
|
1918
|
+
if (parts.length < 4 || parts.length > 6) return null;
|
|
1919
|
+
const hostId = parts[0];
|
|
1920
|
+
const localId = parseInt(parts[1], 10);
|
|
1921
|
+
const ackPeriod = parseInt(parts[2], 10);
|
|
1922
|
+
const ackAdvance = parseInt(parts[3], 10);
|
|
1923
|
+
if (isNaN(localId) || isNaN(ackPeriod) || isNaN(ackAdvance))
|
|
1924
|
+
return null;
|
|
1925
|
+
const allowReconnect = parts.length < 5 || parts[4] !== "0";
|
|
1926
|
+
const isRealTime = parts.length >= 6 && parts[5] === "1";
|
|
1927
|
+
return { hostId, localId, ackPeriod, ackAdvance, allowReconnect, isRealTime };
|
|
1856
1928
|
}
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
resolveStreamRefs(args[1], peer)
|
|
1938
|
-
);
|
|
1939
|
-
break;
|
|
1940
|
-
}
|
|
1941
|
-
case RpcSystemCalls.batch: {
|
|
1942
|
-
const stream = peer.remoteObjects.get(relatedId);
|
|
1943
|
-
if (!stream)
|
|
1944
|
-
debugLog?.log(`$sys.B: no stream for relatedId=${relatedId}`);
|
|
1945
|
-
else if (args.length > 2)
|
|
1946
|
-
stream.onEnd(args[0], polymorphicPayloadError(method, args.length, 2));
|
|
1947
|
-
else if (Array.isArray(args[1])) {
|
|
1948
|
-
const items = args[1];
|
|
1949
|
-
for (let i = 0; i < items.length; i++)
|
|
1950
|
-
items[i] = resolveStreamRefs(items[i], peer);
|
|
1951
|
-
stream.onBatch(args[0], items);
|
|
1952
|
-
}
|
|
1953
|
-
break;
|
|
1954
|
-
}
|
|
1955
|
-
case RpcSystemCalls.end: {
|
|
1956
|
-
const stream = peer.remoteObjects.get(relatedId);
|
|
1957
|
-
if (!stream) {
|
|
1958
|
-
debugLog?.log(`$sys.End: no stream for relatedId=${relatedId}`);
|
|
1959
|
-
} else {
|
|
1960
|
-
const errorInfo = args[1];
|
|
1961
|
-
const typeName = extractTypeName(errorInfo?.TypeRef ?? errorInfo?.typeRef);
|
|
1962
|
-
const msg = errorInfo?.Message ?? errorInfo?.message;
|
|
1963
|
-
let error = null;
|
|
1964
|
-
if (typeName || msg) {
|
|
1965
|
-
error = new Error(msg ?? typeName ?? "RPC stream error");
|
|
1966
|
-
if (typeName)
|
|
1967
|
-
error.name = typeName;
|
|
1968
|
-
}
|
|
1969
|
-
stream.onEnd(args[0], error);
|
|
1970
|
-
}
|
|
1971
|
-
break;
|
|
1972
|
-
}
|
|
1973
|
-
case RpcSystemCalls.ack: {
|
|
1974
|
-
const sender = peer.sharedObjects.get(relatedId);
|
|
1975
|
-
if (sender)
|
|
1976
|
-
sender.onAck(args[0], args[1]);
|
|
1977
|
-
else if (peer.connection !== void 0)
|
|
1978
|
-
peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, [relatedId]);
|
|
1979
|
-
break;
|
|
1980
|
-
}
|
|
1981
|
-
case RpcSystemCalls.ackEnd: {
|
|
1982
|
-
const sender = peer.sharedObjects.get(relatedId);
|
|
1983
|
-
if (sender)
|
|
1984
|
-
sender.onAckEnd(args[0]);
|
|
1985
|
-
else if (peer.connection !== void 0)
|
|
1986
|
-
peer.hub.systemCallSender.disconnect(peer.connection, peer.serializationFormat, [relatedId]);
|
|
1987
|
-
break;
|
|
1988
|
-
}
|
|
1989
|
-
case RpcSystemCalls.disconnect: {
|
|
1990
|
-
const ids = args[0];
|
|
1991
|
-
if (Array.isArray(ids)) {
|
|
1992
|
-
for (const id of ids) {
|
|
1993
|
-
const remoteObj = peer.remoteObjects.get(id);
|
|
1994
|
-
if (remoteObj && typeof remoteObj.disconnect === "function") {
|
|
1995
|
-
remoteObj.disconnect();
|
|
1996
|
-
}
|
|
1997
|
-
const sharedObj = peer.sharedObjects.get(id);
|
|
1998
|
-
if (sharedObj && typeof sharedObj.disconnect === "function") {
|
|
1999
|
-
sharedObj.disconnect();
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
break;
|
|
2004
|
-
}
|
|
1929
|
+
if (typeof value === "object" && value !== null) {
|
|
1930
|
+
const obj = value;
|
|
1931
|
+
const serializedId = obj.SerializedId;
|
|
1932
|
+
if (!Array.isArray(serializedId) || serializedId.length < 2)
|
|
1933
|
+
return null;
|
|
1934
|
+
const rawHostId = serializedId[0];
|
|
1935
|
+
const hostId = String(rawHostId);
|
|
1936
|
+
const localId = Number(serializedId[1]);
|
|
1937
|
+
const ackPeriod = Number(obj.AckPeriod ?? 256);
|
|
1938
|
+
const ackAdvance = Number(obj.AckAdvance ?? 128);
|
|
1939
|
+
const allowReconnect = obj.AllowReconnect !== false;
|
|
1940
|
+
const isRealTime = obj.IsRealTime === true;
|
|
1941
|
+
return { hostId, localId, ackPeriod, ackAdvance, allowReconnect, isRealTime };
|
|
1942
|
+
}
|
|
1943
|
+
return null;
|
|
1944
|
+
}
|
|
1945
|
+
var RpcStream = class _RpcStream {
|
|
1946
|
+
/**
|
|
1947
|
+
* Grace period (ms) given to a factory-form local source to honor its
|
|
1948
|
+
* AbortSignal and exit naturally before the sender force-closes the
|
|
1949
|
+
* iterator via `iterator.return()`.
|
|
1950
|
+
*
|
|
1951
|
+
* Only applies to sources provided as `(abortSignal) => AsyncIterable<T>`.
|
|
1952
|
+
* Plain `AsyncIterable<T>` sources are force-closed immediately on disconnect.
|
|
1953
|
+
*/
|
|
1954
|
+
static disconnectGracePeriodMs = 100;
|
|
1955
|
+
id;
|
|
1956
|
+
kind;
|
|
1957
|
+
allowReconnect;
|
|
1958
|
+
isRealTime;
|
|
1959
|
+
canSkipTo;
|
|
1960
|
+
ackPeriod;
|
|
1961
|
+
ackAdvance;
|
|
1962
|
+
/**
|
|
1963
|
+
* Local-only ring buffer capacity hint for the sender. Undefined means
|
|
1964
|
+
* "use ackAdvance as the buffer size". Not part of the wire ref.
|
|
1965
|
+
*/
|
|
1966
|
+
bufferSize;
|
|
1967
|
+
// Local (origin) mode
|
|
1968
|
+
localSource;
|
|
1969
|
+
// Remote (target) mode
|
|
1970
|
+
peer;
|
|
1971
|
+
// Local-only state
|
|
1972
|
+
_sender;
|
|
1973
|
+
_toRefValue;
|
|
1974
|
+
_whenSent;
|
|
1975
|
+
// Remote-only state
|
|
1976
|
+
_buffer = new Denque();
|
|
1977
|
+
_nextExpectedIndex = 0;
|
|
1978
|
+
_consumerWaiting = null;
|
|
1979
|
+
_completed = false;
|
|
1980
|
+
_completionError = null;
|
|
1981
|
+
_disposed = false;
|
|
1982
|
+
_started = false;
|
|
1983
|
+
_iterating = false;
|
|
1984
|
+
_ackSentUpTo = 0;
|
|
1985
|
+
_nextConsumedIndex = 0;
|
|
1986
|
+
constructor(sourceOrRef, optionsOrPeer) {
|
|
1987
|
+
if (_isStreamSource(sourceOrRef)) {
|
|
1988
|
+
const options = optionsOrPeer ?? {};
|
|
1989
|
+
this.kind = RpcObjectKind.Local;
|
|
1990
|
+
this.localSource = sourceOrRef;
|
|
1991
|
+
this.id = { hostId: "", localId: 0 };
|
|
1992
|
+
this.allowReconnect = options.allowReconnect ?? true;
|
|
1993
|
+
this.isRealTime = options.isRealTime ?? false;
|
|
1994
|
+
this.canSkipTo = options.canSkipTo ?? (() => true);
|
|
1995
|
+
this.ackPeriod = options.ackPeriod ?? DEFAULT_ACK_PERIOD2;
|
|
1996
|
+
this.ackAdvance = options.ackAdvance ?? DEFAULT_ACK_ADVANCE2;
|
|
1997
|
+
this.bufferSize = options.bufferSize;
|
|
1998
|
+
} else {
|
|
1999
|
+
const ref = sourceOrRef;
|
|
2000
|
+
const peer = optionsOrPeer;
|
|
2001
|
+
this.kind = RpcObjectKind.Remote;
|
|
2002
|
+
this.id = { hostId: ref.hostId, localId: ref.localId };
|
|
2003
|
+
this.peer = peer;
|
|
2004
|
+
this.allowReconnect = ref.allowReconnect;
|
|
2005
|
+
this.isRealTime = ref.isRealTime;
|
|
2006
|
+
this.canSkipTo = () => true;
|
|
2007
|
+
this.ackPeriod = ref.ackPeriod;
|
|
2008
|
+
this.ackAdvance = ref.ackAdvance;
|
|
2005
2009
|
}
|
|
2006
2010
|
}
|
|
2007
2011
|
/**
|
|
2008
|
-
*
|
|
2009
|
-
*
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2012
|
+
* For local streams: resolves when the sender finishes pumping (after `toRef()` was called).
|
|
2013
|
+
* Rejects if the source throws. Throws if `toRef()` hasn't been called yet.
|
|
2014
|
+
*/
|
|
2015
|
+
get whenSent() {
|
|
2016
|
+
if (!this._whenSent)
|
|
2017
|
+
throw new Error("toRef() must be called before awaiting whenSent.");
|
|
2018
|
+
return this._whenSent;
|
|
2019
|
+
}
|
|
2020
|
+
/** Local sender backing this stream after `toRef()` has been called.
|
|
2021
|
+
* `undefined` for remote streams or before `toRef()`. */
|
|
2022
|
+
get sender() {
|
|
2023
|
+
return this._sender;
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* For local streams: bind to a peer, create and register an `RpcStreamSender`,
|
|
2027
|
+
* start pumping items from the local source in the background, and return the
|
|
2028
|
+
* stream reference.
|
|
2013
2029
|
*
|
|
2014
|
-
*
|
|
2015
|
-
*
|
|
2016
|
-
*
|
|
2030
|
+
* For binary peers the ref is a MessagePack-compatible object
|
|
2031
|
+
* (`{ SerializedId, AckPeriod, AckAdvance, AllowReconnect }`);
|
|
2032
|
+
* for text peers it is a comma-separated string.
|
|
2033
|
+
*
|
|
2034
|
+
* Can only be called once per stream.
|
|
2017
2035
|
*/
|
|
2018
|
-
|
|
2019
|
-
if (
|
|
2020
|
-
|
|
2021
|
-
if (
|
|
2022
|
-
|
|
2023
|
-
peer.
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2036
|
+
toRef(peer) {
|
|
2037
|
+
if (this.kind !== RpcObjectKind.Local)
|
|
2038
|
+
throw new Error("toRef() is only valid for local streams.");
|
|
2039
|
+
if (this._sender) {
|
|
2040
|
+
if (this._sender.peer !== peer)
|
|
2041
|
+
throw new Error("toRef() was already called with a different peer.");
|
|
2042
|
+
return this._toRefValue;
|
|
2043
|
+
}
|
|
2044
|
+
const isFactory = typeof this.localSource === "function";
|
|
2045
|
+
const sender = new RpcStreamSender(
|
|
2046
|
+
peer,
|
|
2047
|
+
this.ackPeriod,
|
|
2048
|
+
this.ackAdvance,
|
|
2049
|
+
this.allowReconnect,
|
|
2050
|
+
this.isRealTime,
|
|
2051
|
+
this.canSkipTo,
|
|
2052
|
+
isFactory,
|
|
2053
|
+
this.bufferSize
|
|
2054
|
+
);
|
|
2055
|
+
peer.sharedObjects.register(sender);
|
|
2056
|
+
this._sender = sender;
|
|
2057
|
+
const source = isFactory ? this.localSource(sender.abortSignal) : this.localSource;
|
|
2058
|
+
this._whenSent = sender.writeFrom(source);
|
|
2059
|
+
this._toRefValue = peer.serializationFormat.isBinary ? {
|
|
2060
|
+
SerializedId: [sender.id.hostId, sender.id.localId],
|
|
2061
|
+
AckPeriod: sender.ackPeriod,
|
|
2062
|
+
AckAdvance: sender.ackAdvance,
|
|
2063
|
+
AllowReconnect: sender.allowReconnect
|
|
2064
|
+
} : sender.toRef();
|
|
2065
|
+
return this._toRefValue;
|
|
2066
|
+
}
|
|
2067
|
+
/** Called by system call handler when a single item arrives ($sys.I). */
|
|
2068
|
+
onItem(index, item) {
|
|
2069
|
+
if (this._disposed || this._completed) return;
|
|
2070
|
+
if (!_isValidIndex(index)) {
|
|
2071
|
+
this._failProtocol(`invalid $sys.I index: ${String(index)}`);
|
|
2030
2072
|
return;
|
|
2031
2073
|
}
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
for (const [stageKey, rawValue] of Object.entries(stagesRaw)) {
|
|
2038
|
-
const bytes = _toBytes(rawValue);
|
|
2039
|
-
if (bytes === null) continue;
|
|
2040
|
-
const completedStage = parseInt(stageKey, 10) || 0;
|
|
2041
|
-
const callIds = IncreasingSeqCompressor.deserialize(bytes);
|
|
2042
|
-
for (const callId of callIds) {
|
|
2043
|
-
const call = peer.inboundCalls.get(callId);
|
|
2044
|
-
if (call === void 0)
|
|
2045
|
-
unknownSet.add(callId);
|
|
2046
|
-
else if (completedStage < 1)
|
|
2047
|
-
call.resendResult();
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
unknownIds = [...unknownSet].sort((a, b) => a - b);
|
|
2074
|
+
if (index > this._nextExpectedIndex) {
|
|
2075
|
+
warnLog6?.log(`item index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
|
|
2076
|
+
if (!this.allowReconnect) {
|
|
2077
|
+
this._complete(this._gapError(index));
|
|
2078
|
+
return;
|
|
2051
2079
|
}
|
|
2052
|
-
|
|
2080
|
+
this._sendAck(this._nextExpectedIndex, true);
|
|
2081
|
+
return;
|
|
2053
2082
|
}
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2083
|
+
if (index < this._nextExpectedIndex) {
|
|
2084
|
+
this._maybeSendAck(Math.min(index + 1, this._nextConsumedIndex));
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
if (this._isBeyondAckWindow(index + 1)) {
|
|
2088
|
+
this._failProtocol(this._ackWindowError(index + 1));
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
this._buffer.push(item);
|
|
2092
|
+
this._nextExpectedIndex = index + 1;
|
|
2093
|
+
this._notifyConsumer();
|
|
2057
2094
|
}
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2095
|
+
/** Called by system call handler when a batch arrives ($sys.B). */
|
|
2096
|
+
onBatch(index, items) {
|
|
2097
|
+
if (this._disposed || this._completed) return;
|
|
2098
|
+
if (!_isValidIndex(index) || !_isValidIndex(items.length)) {
|
|
2099
|
+
this._failProtocol(`invalid $sys.B index/length: ${String(index)}/${String(items.length)}`);
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
if (index > this._nextExpectedIndex) {
|
|
2103
|
+
warnLog6?.log(`batch index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
|
|
2104
|
+
if (!this.allowReconnect) {
|
|
2105
|
+
this._complete(this._gapError(index));
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
this._sendAck(this._nextExpectedIndex, true);
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
if (index < this._nextExpectedIndex) {
|
|
2112
|
+
this._maybeSendAck(Math.min(index + items.length, this._nextConsumedIndex));
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
if (this._isBeyondAckWindow(index + items.length)) {
|
|
2116
|
+
this._failProtocol(this._ackWindowError(index + items.length));
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
for (const item of items) this._buffer.push(item);
|
|
2120
|
+
this._nextExpectedIndex = index + items.length;
|
|
2121
|
+
this._notifyConsumer();
|
|
2070
2122
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
if (
|
|
2075
|
-
|
|
2076
|
-
return
|
|
2123
|
+
/** Called by system call handler when the stream ends ($sys.End). */
|
|
2124
|
+
onEnd(index, error) {
|
|
2125
|
+
if (this._disposed || this._completed) return;
|
|
2126
|
+
if (!_isValidIndex(index)) {
|
|
2127
|
+
this._failProtocol(`invalid $sys.End index: ${String(index)}`);
|
|
2128
|
+
return;
|
|
2077
2129
|
}
|
|
2078
|
-
|
|
2130
|
+
if (index > this._nextExpectedIndex) {
|
|
2131
|
+
warnLog6?.log(`end index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
|
|
2132
|
+
if (!this.allowReconnect) {
|
|
2133
|
+
this._complete(this._gapError(index));
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
this._sendAck(this._nextExpectedIndex, true);
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
if (index < this._nextExpectedIndex) {
|
|
2140
|
+
this._maybeSendAck(Math.min(index, this._nextConsumedIndex));
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
this._complete(error);
|
|
2079
2144
|
}
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
} catch {
|
|
2088
|
-
return null;
|
|
2145
|
+
// -- IRpcObject --
|
|
2146
|
+
reconnect() {
|
|
2147
|
+
if (this.kind === RpcObjectKind.Local) return;
|
|
2148
|
+
if (this.allowReconnect) {
|
|
2149
|
+
this._sendAck(this._nextExpectedIndex, true);
|
|
2150
|
+
} else {
|
|
2151
|
+
this.disconnect();
|
|
2089
2152
|
}
|
|
2090
2153
|
}
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
var { errorLog } = getLogs("RpcSystemCallSender");
|
|
2096
|
-
var RpcSystemCallSender = class _RpcSystemCallSender {
|
|
2097
|
-
// Reusable envelopes — one per hot-path method.
|
|
2098
|
-
_itemEnv = {
|
|
2099
|
-
Method: RpcSystemCalls.item,
|
|
2100
|
-
RelatedId: 0
|
|
2101
|
-
};
|
|
2102
|
-
_batchEnv = {
|
|
2103
|
-
Method: RpcSystemCalls.batch,
|
|
2104
|
-
RelatedId: 0
|
|
2105
|
-
};
|
|
2106
|
-
_endEnv = {
|
|
2107
|
-
Method: RpcSystemCalls.end,
|
|
2108
|
-
RelatedId: 0
|
|
2109
|
-
};
|
|
2110
|
-
_ackEnv = {
|
|
2111
|
-
Method: RpcSystemCalls.ack,
|
|
2112
|
-
RelatedId: 0
|
|
2113
|
-
};
|
|
2114
|
-
_ackEndEnv = {
|
|
2115
|
-
Method: RpcSystemCalls.ackEnd,
|
|
2116
|
-
RelatedId: 0
|
|
2117
|
-
};
|
|
2118
|
-
// Reusable arg arrays.
|
|
2119
|
-
_itemArgs = [0, null];
|
|
2120
|
-
_batchArgs = [0, null];
|
|
2121
|
-
_endArgs = [0, { TypeRef: "", Message: "" }];
|
|
2122
|
-
_ackArgs = [0, ""];
|
|
2123
|
-
_ackEndArgs = [""];
|
|
2124
|
-
static NO_ERROR = Object.freeze({ TypeRef: "", Message: "" });
|
|
2125
|
-
registry;
|
|
2126
|
-
_send(conn, format, envelope, args) {
|
|
2127
|
-
const wireData = format.serializeMessage(
|
|
2128
|
-
envelope,
|
|
2129
|
-
args,
|
|
2130
|
-
conn.encoder,
|
|
2131
|
-
this.registry
|
|
2132
|
-
);
|
|
2133
|
-
if (typeof wireData === "string") {
|
|
2134
|
-
conn.send(wireData);
|
|
2135
|
-
} else {
|
|
2136
|
-
conn.sendBinary(wireData);
|
|
2154
|
+
disconnect() {
|
|
2155
|
+
if (this.kind === RpcObjectKind.Local) {
|
|
2156
|
+
this._sender?.disconnect();
|
|
2157
|
+
return;
|
|
2137
2158
|
}
|
|
2159
|
+
if (this._completed) return;
|
|
2160
|
+
this._completed = true;
|
|
2161
|
+
this._completionError = new Error("Peer disconnected.");
|
|
2162
|
+
this._notifyConsumer();
|
|
2138
2163
|
}
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2164
|
+
// -- AsyncIterable --
|
|
2165
|
+
[Symbol.asyncIterator]() {
|
|
2166
|
+
if (this.kind === RpcObjectKind.Local) {
|
|
2167
|
+
const source = typeof this.localSource === "function" ? this.localSource(new AbortController().signal) : this.localSource;
|
|
2168
|
+
return source[Symbol.asyncIterator]();
|
|
2169
|
+
}
|
|
2170
|
+
if (this._iterating)
|
|
2171
|
+
throw new Error("RpcStream can only be iterated once.");
|
|
2172
|
+
this._iterating = true;
|
|
2173
|
+
return {
|
|
2174
|
+
next: async () => {
|
|
2175
|
+
if (!this._started) {
|
|
2176
|
+
this._started = true;
|
|
2177
|
+
this.peer.remoteObjects.register(this);
|
|
2178
|
+
this._sendAck(0, true);
|
|
2179
|
+
}
|
|
2180
|
+
this._maybeSendAck(this._nextConsumedIndex);
|
|
2181
|
+
for (; ; ) {
|
|
2182
|
+
if (!this._buffer.isEmpty()) {
|
|
2183
|
+
const value = this._buffer.shift();
|
|
2184
|
+
this._nextConsumedIndex++;
|
|
2185
|
+
return { value, done: false };
|
|
2186
|
+
}
|
|
2187
|
+
if (this._completed) {
|
|
2188
|
+
this.dispose();
|
|
2189
|
+
if (this._completionError) throw this._completionError;
|
|
2190
|
+
return { value: void 0, done: true };
|
|
2191
|
+
}
|
|
2192
|
+
this._consumerWaiting = new PromiseSource4();
|
|
2193
|
+
await this._consumerWaiting;
|
|
2194
|
+
}
|
|
2195
|
+
},
|
|
2196
|
+
return: () => {
|
|
2197
|
+
if (this._started && !this._completed)
|
|
2198
|
+
this._sendAckEnd();
|
|
2199
|
+
this.dispose();
|
|
2200
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
2201
|
+
}
|
|
2146
2202
|
};
|
|
2147
|
-
this._send(conn, format, { Method: RpcSystemCalls.handshake }, [
|
|
2148
|
-
handshakeArg
|
|
2149
|
-
]);
|
|
2150
2203
|
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
[result]
|
|
2158
|
-
);
|
|
2159
|
-
} catch (e) {
|
|
2160
|
-
errorLog?.log(`Failed to send Ok response for call #${relatedId}`, e);
|
|
2161
|
-
this.error(conn, format, relatedId, e);
|
|
2204
|
+
dispose() {
|
|
2205
|
+
if (this._disposed) return;
|
|
2206
|
+
this._disposed = true;
|
|
2207
|
+
this._completed = true;
|
|
2208
|
+
if (this.kind === RpcObjectKind.Remote) {
|
|
2209
|
+
this.peer.remoteObjects.unregister(this);
|
|
2162
2210
|
}
|
|
2211
|
+
this._notifyConsumer();
|
|
2163
2212
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2213
|
+
// -- Private --
|
|
2214
|
+
// Mirrors .NET RpcStream.CloseFromLock: acknowledge the end to the sender
|
|
2215
|
+
// (so its RpcSharedStream is released) and unregister immediately.
|
|
2216
|
+
_complete(error) {
|
|
2217
|
+
if (this._completed) return;
|
|
2218
|
+
this._completed = true;
|
|
2219
|
+
this._completionError = error;
|
|
2220
|
+
this._sendAckEnd();
|
|
2221
|
+
if (this.kind === RpcObjectKind.Remote)
|
|
2222
|
+
this.peer.remoteObjects.unregister(this);
|
|
2223
|
+
this._notifyConsumer();
|
|
2171
2224
|
}
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
});
|
|
2225
|
+
// Terminates the stream the way the non-reconnectable gap path does: the
|
|
2226
|
+
// consumer sees the error and the sender gets $sys.AckEnd, so nothing is
|
|
2227
|
+
// left pumping into a stream we no longer read.
|
|
2228
|
+
_failProtocol(message) {
|
|
2229
|
+
warnLog6?.log(`localId=${this.id.localId}: ${message}`);
|
|
2230
|
+
this._complete(new Error(message));
|
|
2177
2231
|
}
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2232
|
+
// The remote advertised `ackAdvance` as the furthest it may run ahead of our
|
|
2233
|
+
// last acknowledged index. The sending side is the only place that window
|
|
2234
|
+
// was ever enforced, which left a flooding remote free to grow our buffer
|
|
2235
|
+
// without bound (I6) — so we enforce our half of the contract here.
|
|
2236
|
+
_isBeyondAckWindow(nextExpectedIndex) {
|
|
2237
|
+
return this.ackAdvance > 0 && nextExpectedIndex > this._ackSentUpTo + this.ackAdvance;
|
|
2182
2238
|
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
// instead of hanging. Mirrors .NET RpcSystemCallSender.Disconnect.
|
|
2186
|
-
disconnect(conn, format, localIds) {
|
|
2187
|
-
this._send(conn, format, { Method: RpcSystemCalls.disconnect }, [
|
|
2188
|
-
localIds
|
|
2189
|
-
]);
|
|
2239
|
+
_ackWindowError(nextExpectedIndex) {
|
|
2240
|
+
return `Stream ran past its ack window: reached ${nextExpectedIndex}, acked ${this._ackSentUpTo}, ackAdvance ${this.ackAdvance}`;
|
|
2190
2241
|
}
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
this._send(conn, format, this._itemEnv, this._itemArgs);
|
|
2196
|
-
this._itemArgs[1] = null;
|
|
2242
|
+
_gapError(index) {
|
|
2243
|
+
return new Error(
|
|
2244
|
+
`Stream gap at index ${index} (expected ${this._nextExpectedIndex}); reconnect not allowed`
|
|
2245
|
+
);
|
|
2197
2246
|
}
|
|
2198
|
-
|
|
2199
|
-
this.
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
this._batchArgs[1] = null;
|
|
2247
|
+
_notifyConsumer() {
|
|
2248
|
+
if (this._consumerWaiting !== null) {
|
|
2249
|
+
this._consumerWaiting.resolve();
|
|
2250
|
+
this._consumerWaiting = null;
|
|
2251
|
+
}
|
|
2204
2252
|
}
|
|
2205
|
-
|
|
2206
|
-
this.
|
|
2207
|
-
this.
|
|
2208
|
-
|
|
2209
|
-
|
|
2253
|
+
_maybeSendAck(nextIndex) {
|
|
2254
|
+
if (this.ackPeriod <= 0) return;
|
|
2255
|
+
const threshold = this._ackSentUpTo + this.ackPeriod;
|
|
2256
|
+
if (nextIndex >= threshold) {
|
|
2257
|
+
this._sendAck(nextIndex, false);
|
|
2258
|
+
}
|
|
2210
2259
|
}
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
this.
|
|
2214
|
-
this.
|
|
2215
|
-
|
|
2260
|
+
static _emptyGuid = "00000000-0000-0000-0000-000000000000";
|
|
2261
|
+
_sendAck(nextIndex, mustReset) {
|
|
2262
|
+
this._ackSentUpTo = nextIndex;
|
|
2263
|
+
const conn = this.peer.isConnected ? this.peer.connection : void 0;
|
|
2264
|
+
if (conn) {
|
|
2265
|
+
const hostId = mustReset ? this.id.hostId : _RpcStream._emptyGuid;
|
|
2266
|
+
this.peer.hub.systemCallSender.ack(
|
|
2267
|
+
conn,
|
|
2268
|
+
this.peer.serializationFormat,
|
|
2269
|
+
this.id.localId,
|
|
2270
|
+
nextIndex,
|
|
2271
|
+
hostId
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
2216
2274
|
}
|
|
2217
|
-
|
|
2218
|
-
this.
|
|
2219
|
-
|
|
2220
|
-
|
|
2275
|
+
_sendAckEnd() {
|
|
2276
|
+
const conn = this.peer.isConnected ? this.peer.connection : void 0;
|
|
2277
|
+
if (conn) {
|
|
2278
|
+
this.peer.hub.systemCallSender.ackEnd(
|
|
2279
|
+
conn,
|
|
2280
|
+
this.peer.serializationFormat,
|
|
2281
|
+
this.id.localId,
|
|
2282
|
+
this.id.hostId
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2221
2285
|
}
|
|
2222
2286
|
};
|
|
2287
|
+
function _isValidIndex(index) {
|
|
2288
|
+
return Number.isSafeInteger(index) && index >= 0;
|
|
2289
|
+
}
|
|
2290
|
+
function _isStreamSource(value) {
|
|
2291
|
+
if (typeof value === "function") return true;
|
|
2292
|
+
return value !== null && typeof value === "object" && Symbol.asyncIterator in value;
|
|
2293
|
+
}
|
|
2294
|
+
function toRpcStream(value, peer) {
|
|
2295
|
+
const ref = parseStreamRef(value);
|
|
2296
|
+
if (ref === null)
|
|
2297
|
+
return null;
|
|
2298
|
+
return new RpcStream(ref, peer);
|
|
2299
|
+
}
|
|
2223
2300
|
|
|
2224
2301
|
// src/rpc-remote-object-tracker.ts
|
|
2225
2302
|
var RpcRemoteObjectTracker = class {
|
|
@@ -2451,33 +2528,68 @@ function isMemoryPackKey(key) {
|
|
|
2451
2528
|
return key.startsWith("mempack");
|
|
2452
2529
|
}
|
|
2453
2530
|
|
|
2531
|
+
// src/reconnect-proof.ts
|
|
2532
|
+
var textEncoder2 = new TextEncoder();
|
|
2533
|
+
function isReconnectProofSupported() {
|
|
2534
|
+
return typeof crypto !== "undefined" && crypto.subtle !== void 0;
|
|
2535
|
+
}
|
|
2536
|
+
async function computeReconnectProof(secret, clientId, counterText) {
|
|
2537
|
+
const key = await crypto.subtle.importKey(
|
|
2538
|
+
"raw",
|
|
2539
|
+
textEncoder2.encode(secret),
|
|
2540
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
2541
|
+
false,
|
|
2542
|
+
["sign"]
|
|
2543
|
+
);
|
|
2544
|
+
const signature = await crypto.subtle.sign(
|
|
2545
|
+
"HMAC",
|
|
2546
|
+
key,
|
|
2547
|
+
textEncoder2.encode(`${clientId}
|
|
2548
|
+
${counterText}`)
|
|
2549
|
+
);
|
|
2550
|
+
return base64UrlEncode(new Uint8Array(signature));
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2454
2553
|
// src/rpc-peer.ts
|
|
2455
2554
|
function yieldToEventLoop() {
|
|
2456
2555
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
2457
2556
|
}
|
|
2458
2557
|
var RPC_CLOSE_CODE_UNSUPPORTED_FORMAT = 4001;
|
|
2459
|
-
var { debugLog: debugLog2, infoLog, warnLog:
|
|
2460
|
-
var defaultConnectionUrlResolver = (peer) => {
|
|
2558
|
+
var { debugLog: debugLog2, infoLog, warnLog: warnLog7, errorLog: errorLog2 } = getLogs("RpcPeer");
|
|
2559
|
+
var defaultConnectionUrlResolver = async (peer) => {
|
|
2461
2560
|
const formatKey = peer.serializationFormat.key;
|
|
2561
|
+
const proof = await peer.computeReconnectProof();
|
|
2462
2562
|
try {
|
|
2463
2563
|
const url = new URL(peer.ref);
|
|
2464
2564
|
url.searchParams.set("clientId", peer.clientId);
|
|
2465
2565
|
url.searchParams.set("f", formatKey);
|
|
2566
|
+
if (proof !== void 0) {
|
|
2567
|
+
url.searchParams.set("c", proof.counter);
|
|
2568
|
+
url.searchParams.set("p", proof.proof);
|
|
2569
|
+
}
|
|
2466
2570
|
return url.toString();
|
|
2467
2571
|
} catch {
|
|
2468
2572
|
const sep = peer.ref.includes("?") ? "&" : "?";
|
|
2469
|
-
|
|
2573
|
+
let url = peer.ref + sep + `clientId=${peer.clientId}&f=${formatKey}`;
|
|
2574
|
+
if (proof !== void 0)
|
|
2575
|
+
url += `&c=${proof.counter}&p=${proof.proof}`;
|
|
2576
|
+
return url;
|
|
2470
2577
|
}
|
|
2471
2578
|
};
|
|
2579
|
+
var RedactedParameterNames = ["session", "clientId", "p"];
|
|
2472
2580
|
var sanitizeUrl = (url) => {
|
|
2473
2581
|
try {
|
|
2474
2582
|
const parsed = new URL(url);
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2583
|
+
let isRedacted = false;
|
|
2584
|
+
for (const name of RedactedParameterNames) {
|
|
2585
|
+
if (!parsed.searchParams.has(name))
|
|
2586
|
+
continue;
|
|
2587
|
+
parsed.searchParams.set(name, "<redacted>");
|
|
2588
|
+
isRedacted = true;
|
|
2589
|
+
}
|
|
2590
|
+
return isRedacted ? parsed.toString() : url;
|
|
2479
2591
|
} catch {
|
|
2480
|
-
return url.replace(/([?&]session=)[^&]*/
|
|
2592
|
+
return url.replace(/([?&](?:session|clientId|p)=)[^&]*/gi, "$1<redacted>");
|
|
2481
2593
|
}
|
|
2482
2594
|
};
|
|
2483
2595
|
function parseFormatFromUrl(url) {
|
|
@@ -2726,21 +2838,26 @@ var RpcPeer = class {
|
|
|
2726
2838
|
/** Override in subclasses to handle the remote peer's handshake response. */
|
|
2727
2839
|
_onHandshakeReceived(_handshake) {
|
|
2728
2840
|
}
|
|
2841
|
+
// The try covers dispatch too, not just deserialization: a handler that throws
|
|
2842
|
+
// here would otherwise escape into the transport's onmessage - dropping every
|
|
2843
|
+
// remaining message of the frame, or killing a Node process outright.
|
|
2729
2844
|
_handleMessage(received) {
|
|
2730
|
-
let message;
|
|
2731
|
-
let args;
|
|
2732
2845
|
try {
|
|
2733
|
-
|
|
2734
|
-
message = received.message;
|
|
2735
|
-
args = received.args;
|
|
2736
|
-
} else {
|
|
2737
|
-
const parsed = deserializeMessage(received.raw);
|
|
2738
|
-
message = parsed.message;
|
|
2739
|
-
args = parsed.args;
|
|
2740
|
-
}
|
|
2846
|
+
this._processMessage(received);
|
|
2741
2847
|
} catch (e) {
|
|
2742
2848
|
errorLog2?.log(`'${this.ref}': Failed to process inbound message:`, received, e);
|
|
2743
|
-
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
_processMessage(received) {
|
|
2852
|
+
let message;
|
|
2853
|
+
let args;
|
|
2854
|
+
if (received.kind === "binary") {
|
|
2855
|
+
message = received.message;
|
|
2856
|
+
args = received.args;
|
|
2857
|
+
} else {
|
|
2858
|
+
const parsed = deserializeMessage(received.raw);
|
|
2859
|
+
message = parsed.message;
|
|
2860
|
+
args = parsed.args;
|
|
2744
2861
|
}
|
|
2745
2862
|
const method = message.Method ?? "";
|
|
2746
2863
|
debugLog2?.log(`'${this.ref}': <- ${method}#${message.RelatedId ?? 0}`);
|
|
@@ -2752,12 +2869,14 @@ var RpcPeer = class {
|
|
|
2752
2869
|
RemotePeerId: raw[0],
|
|
2753
2870
|
RemoteHubId: raw[2],
|
|
2754
2871
|
ProtocolVersion: raw[3],
|
|
2755
|
-
Index: raw[4]
|
|
2872
|
+
Index: raw[4],
|
|
2873
|
+
Secret: raw[5]
|
|
2756
2874
|
} : {
|
|
2757
2875
|
RemotePeerId: obj.RemotePeerId ?? obj.remotePeerId,
|
|
2758
2876
|
RemoteHubId: obj.RemoteHubId ?? obj.remoteHubId,
|
|
2759
2877
|
ProtocolVersion: obj.ProtocolVersion ?? obj.protocolVersion,
|
|
2760
|
-
Index: obj.Index ?? obj.index
|
|
2878
|
+
Index: obj.Index ?? obj.index,
|
|
2879
|
+
Secret: obj.Secret ?? obj.secret
|
|
2761
2880
|
};
|
|
2762
2881
|
this._onHandshakeReceived(handshake);
|
|
2763
2882
|
}
|
|
@@ -2871,7 +2990,7 @@ var RpcPeer = class {
|
|
|
2871
2990
|
this._keepAliveWatchdog = setTimeout(() => {
|
|
2872
2991
|
this._keepAliveWatchdog = void 0;
|
|
2873
2992
|
if (this._connection === void 0) return;
|
|
2874
|
-
|
|
2993
|
+
warnLog7?.log(
|
|
2875
2994
|
`'${this.ref}': no keep-alive for ${this.keepAliveTimeoutMs}ms \u2014 force-closing connection`
|
|
2876
2995
|
);
|
|
2877
2996
|
this._connection.close();
|
|
@@ -2919,6 +3038,16 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2919
3038
|
_lastRemotePeerId;
|
|
2920
3039
|
_pendingHandshake;
|
|
2921
3040
|
_reconnectsAt = 0;
|
|
3041
|
+
/** Server-issued reconnect secret, from the most recent handshake that
|
|
3042
|
+
* carried one. In-memory only — never localStorage/sessionStorage/cookie.
|
|
3043
|
+
* A tab reload mints a new `clientId`, so a persisted secret would be
|
|
3044
|
+
* useless anyway. */
|
|
3045
|
+
_reconnectSecret;
|
|
3046
|
+
/** Incremented once per *connect attempt*, in `computeReconnectProof()`. A
|
|
3047
|
+
* failed attempt still burns its value: the server may have advanced its
|
|
3048
|
+
* own counter at the gate before the connection died, and reusing the
|
|
3049
|
+
* value would then be rejected forever. */
|
|
3050
|
+
_reconnectCounter = 0;
|
|
2922
3051
|
/** Base64url-encoded peer ID — matches .NET's RpcClientPeer.ClientId (Guid.ToBase64Url). */
|
|
2923
3052
|
clientId;
|
|
2924
3053
|
/** Builds the WebSocket URL for this peer. Replace to customize URL construction. */
|
|
@@ -2999,6 +3128,28 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2999
3128
|
start() {
|
|
3000
3129
|
void this.run();
|
|
3001
3130
|
}
|
|
3131
|
+
/** Builds the `c` / `p` connect-URL parameters proving possession of the
|
|
3132
|
+
* server-issued reconnect secret, or `undefined` when there is no secret
|
|
3133
|
+
* yet or WebCrypto is unavailable. Consumes one counter value per proof
|
|
3134
|
+
* produced, so call it exactly once per connect attempt. */
|
|
3135
|
+
async computeReconnectProof() {
|
|
3136
|
+
const secret = this._reconnectSecret;
|
|
3137
|
+
if (secret === void 0)
|
|
3138
|
+
return void 0;
|
|
3139
|
+
if (!isReconnectProofSupported()) {
|
|
3140
|
+
warnReconnectProofUnavailable(this.ref, "crypto.subtle is unavailable (insecure context?)");
|
|
3141
|
+
return void 0;
|
|
3142
|
+
}
|
|
3143
|
+
const counter = (this._reconnectCounter + 1).toString();
|
|
3144
|
+
try {
|
|
3145
|
+
const proof = await computeReconnectProof(secret, this.clientId, counter);
|
|
3146
|
+
this._reconnectCounter++;
|
|
3147
|
+
return { counter, proof };
|
|
3148
|
+
} catch (e) {
|
|
3149
|
+
warnReconnectProofUnavailable(this.ref, `HMAC-SHA256 failed: ${String(e)}`);
|
|
3150
|
+
return void 0;
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3002
3153
|
/** The reconnect loop itself. Exposed as `protected` so subclasses (and
|
|
3003
3154
|
* `start()`) can drive it; public callers should use {@link start}. */
|
|
3004
3155
|
run() {
|
|
@@ -3012,7 +3163,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
3012
3163
|
this._setConnectionState(1 /* Connecting */);
|
|
3013
3164
|
const retryDelay = this.hub.reconnectDelayer.getDelay(this._tryIndex, this._stopController.signal);
|
|
3014
3165
|
if (retryDelay.isLimitExceeded) {
|
|
3015
|
-
|
|
3166
|
+
warnLog7?.log(`'${this.ref}': Reconnect limit exceeded - shutting down`);
|
|
3016
3167
|
this._disposed = true;
|
|
3017
3168
|
break;
|
|
3018
3169
|
}
|
|
@@ -3061,7 +3212,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
3061
3212
|
);
|
|
3062
3213
|
} catch (e) {
|
|
3063
3214
|
if (e instanceof TimeoutError) {
|
|
3064
|
-
|
|
3215
|
+
warnLog7?.log(`'${this.ref}': Connect timed out after ${connectTimeoutMs}ms`);
|
|
3065
3216
|
conn.close();
|
|
3066
3217
|
}
|
|
3067
3218
|
throw e;
|
|
@@ -3076,7 +3227,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
3076
3227
|
++this._ownHandshakeIndex
|
|
3077
3228
|
);
|
|
3078
3229
|
} catch (e) {
|
|
3079
|
-
|
|
3230
|
+
warnLog7?.log(`'${this.ref}': Failed to send handshake`, e);
|
|
3080
3231
|
throw e;
|
|
3081
3232
|
}
|
|
3082
3233
|
const handshakeTimeoutMs = this.handshakeTimeoutMs;
|
|
@@ -3089,7 +3240,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
3089
3240
|
);
|
|
3090
3241
|
} catch (e) {
|
|
3091
3242
|
if (e instanceof TimeoutError) {
|
|
3092
|
-
|
|
3243
|
+
warnLog7?.log(`'${this.ref}': Handshake timed out after ${handshakeTimeoutMs}ms`);
|
|
3093
3244
|
conn.close();
|
|
3094
3245
|
}
|
|
3095
3246
|
throw e;
|
|
@@ -3097,6 +3248,8 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
3097
3248
|
this._pendingHandshake = void 0;
|
|
3098
3249
|
}
|
|
3099
3250
|
this._remoteHandshakeIndex = remoteHandshake.Index ?? 0;
|
|
3251
|
+
if (remoteHandshake.Secret)
|
|
3252
|
+
this._reconnectSecret = remoteHandshake.Secret;
|
|
3100
3253
|
const isPeerChanged = this._detectPeerChange(remoteHandshake);
|
|
3101
3254
|
infoLog?.log(`'${this.ref}': Handshake succeeded, peerChanged=${isPeerChanged}`);
|
|
3102
3255
|
this._setConnectionState(3 /* Connected */);
|
|
@@ -3109,7 +3262,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
3109
3262
|
this._setConnectionState(0 /* Disconnected */);
|
|
3110
3263
|
if (this._disposed) break;
|
|
3111
3264
|
if (lastCloseCode === RPC_CLOSE_CODE_UNSUPPORTED_FORMAT) {
|
|
3112
|
-
|
|
3265
|
+
warnLog7?.log(`'${this.ref}': Unsupported serialization format - shutting down`);
|
|
3113
3266
|
this.unsupportedFormat.trigger({ reason: lastCloseReason });
|
|
3114
3267
|
this._disposed = true;
|
|
3115
3268
|
break;
|
|
@@ -3355,8 +3508,16 @@ function guidToBase64Url(uuid) {
|
|
|
3355
3508
|
bytes[7] = parseInt(hex.slice(12, 14), 16);
|
|
3356
3509
|
for (let i = 8; i < 16; i++)
|
|
3357
3510
|
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
3358
|
-
|
|
3359
|
-
|
|
3511
|
+
return base64UrlEncode(bytes);
|
|
3512
|
+
}
|
|
3513
|
+
var isReconnectProofUnavailableWarned = false;
|
|
3514
|
+
function warnReconnectProofUnavailable(ref, reason) {
|
|
3515
|
+
if (isReconnectProofUnavailableWarned)
|
|
3516
|
+
return;
|
|
3517
|
+
isReconnectProofUnavailableWarned = true;
|
|
3518
|
+
warnLog7?.log(
|
|
3519
|
+
`'${ref}': ${reason} - connecting without a reconnect proof. This fails if the server requires one (RequireReconnectProof).`
|
|
3520
|
+
);
|
|
3360
3521
|
}
|
|
3361
3522
|
function _toBytes2(value) {
|
|
3362
3523
|
if (value instanceof Uint8Array) return value;
|
|
@@ -3435,7 +3596,7 @@ var RpcClientPeerReconnectDelayer = class extends RetryDelayer {
|
|
|
3435
3596
|
};
|
|
3436
3597
|
|
|
3437
3598
|
// src/rpc-service-host.ts
|
|
3438
|
-
var { warnLog:
|
|
3599
|
+
var { warnLog: warnLog8 } = getLogs("RpcServiceHost");
|
|
3439
3600
|
var RpcServiceHost = class {
|
|
3440
3601
|
_methods = /* @__PURE__ */ new Map();
|
|
3441
3602
|
register(def, impl, receiver = impl) {
|
|
@@ -3452,7 +3613,7 @@ var RpcServiceHost = class {
|
|
|
3452
3613
|
async dispatch(wireMethod, args, context) {
|
|
3453
3614
|
const entry = this._methods.get(wireMethod);
|
|
3454
3615
|
if (entry === void 0) {
|
|
3455
|
-
|
|
3616
|
+
warnLog8?.log(`Method not found: ${wireMethod}`);
|
|
3456
3617
|
throw new Error(`Method not found: ${wireMethod}`);
|
|
3457
3618
|
}
|
|
3458
3619
|
if (context !== void 0 && entry.def.callTypeId !== 0) {
|
|
@@ -3960,7 +4121,7 @@ function hashLongWithSeed(input, length, seed) {
|
|
|
3960
4121
|
}
|
|
3961
4122
|
return hashLong(input, length, secret, secret.length);
|
|
3962
4123
|
}
|
|
3963
|
-
var
|
|
4124
|
+
var textEncoder3 = new TextEncoder();
|
|
3964
4125
|
function xxh3_64(input, seed = 0n) {
|
|
3965
4126
|
const len = input.length;
|
|
3966
4127
|
if (len <= XXH3_MIDSIZE_MAX) {
|
|
@@ -3969,10 +4130,10 @@ function xxh3_64(input, seed = 0n) {
|
|
|
3969
4130
|
return hashLongWithSeed(input, len, u64(seed));
|
|
3970
4131
|
}
|
|
3971
4132
|
function xxh3_64str(input, seed = 0n) {
|
|
3972
|
-
return xxh3_64(
|
|
4133
|
+
return xxh3_64(textEncoder3.encode(input), seed);
|
|
3973
4134
|
}
|
|
3974
4135
|
function computeMethodHash(methodName) {
|
|
3975
|
-
const utf8 =
|
|
4136
|
+
const utf8 = textEncoder3.encode(methodName);
|
|
3976
4137
|
const prefixValue = Math.imul(67211, utf8.length) >>> 0;
|
|
3977
4138
|
const prefixed = new Uint8Array(4 + utf8.length);
|
|
3978
4139
|
prefixed[0] = prefixValue & 255;
|
|
@@ -3985,7 +4146,7 @@ function computeMethodHash(methodName) {
|
|
|
3985
4146
|
}
|
|
3986
4147
|
|
|
3987
4148
|
// src/rpc-method-registry.ts
|
|
3988
|
-
var { warnLog:
|
|
4149
|
+
var { warnLog: warnLog9 } = getLogs("RpcMethodRegistry");
|
|
3989
4150
|
var RpcMethodRegistry = class {
|
|
3990
4151
|
_nameToHash = /* @__PURE__ */ new Map();
|
|
3991
4152
|
_hashToName = /* @__PURE__ */ new Map();
|
|
@@ -3995,7 +4156,7 @@ var RpcMethodRegistry = class {
|
|
|
3995
4156
|
const hash = computeMethodHash(methodName);
|
|
3996
4157
|
const existing = this._hashToName.get(hash);
|
|
3997
4158
|
if (existing !== void 0 && existing !== methodName) {
|
|
3998
|
-
|
|
4159
|
+
warnLog9?.log(`hash collision: "${methodName}" and "${existing}" both hash to ${hash}`);
|
|
3999
4160
|
return;
|
|
4000
4161
|
}
|
|
4001
4162
|
this._nameToHash.set(methodName, hash);
|
|
@@ -4028,7 +4189,7 @@ var RpcMethodRegistry = class {
|
|
|
4028
4189
|
};
|
|
4029
4190
|
|
|
4030
4191
|
// src/rpc-hub.ts
|
|
4031
|
-
var { warnLog:
|
|
4192
|
+
var { warnLog: warnLog10 } = getLogs("RpcHub");
|
|
4032
4193
|
var RpcHub = class {
|
|
4033
4194
|
hubId;
|
|
4034
4195
|
peers = /* @__PURE__ */ new Map();
|
|
@@ -4093,7 +4254,7 @@ var RpcHub = class {
|
|
|
4093
4254
|
if (peer !== void 0) {
|
|
4094
4255
|
peer.close();
|
|
4095
4256
|
this.peers.delete(ref);
|
|
4096
|
-
|
|
4257
|
+
warnLog10?.log(`'${ref}': peer is removed from RpcHub`);
|
|
4097
4258
|
}
|
|
4098
4259
|
}
|
|
4099
4260
|
/** Get or create a peer by ref. If the peer does not exist:
|
|
@@ -4211,12 +4372,12 @@ var RpcHub = class {
|
|
|
4211
4372
|
const callArgs = args.slice(0, methodDef.argCount);
|
|
4212
4373
|
const outboundCall = peer.call(wireName, callArgs);
|
|
4213
4374
|
const result = await outboundCall.result;
|
|
4214
|
-
const
|
|
4215
|
-
if (!
|
|
4375
|
+
const stream = toRpcStream(result, peer);
|
|
4376
|
+
if (!stream)
|
|
4216
4377
|
throw new Error(
|
|
4217
4378
|
`Expected stream reference, got: ${JSON.stringify(result)}`
|
|
4218
4379
|
);
|
|
4219
|
-
return
|
|
4380
|
+
return stream;
|
|
4220
4381
|
};
|
|
4221
4382
|
}
|
|
4222
4383
|
const callOptions = methodDef.callTypeId !== 0 || mode !== RpcRemoteExecutionMode.Default || methodDef.timeouts !== void 0 ? { callTypeId: methodDef.callTypeId, remoteExecutionMode: mode, timeouts: methodDef.timeouts } : void 0;
|
|
@@ -4225,8 +4386,7 @@ var RpcHub = class {
|
|
|
4225
4386
|
throw new Error("Outbound call failed: not connected and AwaitForConnection is not set.");
|
|
4226
4387
|
const callArgs = args.slice(0, methodDef.argCount);
|
|
4227
4388
|
const outboundCall = peer.call(wireName, callArgs, callOptions);
|
|
4228
|
-
|
|
4229
|
-
return resolveStreamRefs(result, peer);
|
|
4389
|
+
return await outboundCall.result;
|
|
4230
4390
|
};
|
|
4231
4391
|
}
|
|
4232
4392
|
// Private methods
|
|
@@ -4583,6 +4743,7 @@ export {
|
|
|
4583
4743
|
RpcWebSocketConnection,
|
|
4584
4744
|
WebSocketState,
|
|
4585
4745
|
computeMethodHash,
|
|
4746
|
+
computeReconnectProof,
|
|
4586
4747
|
createBinaryEncoder,
|
|
4587
4748
|
createMessageChannelPair,
|
|
4588
4749
|
createRpcClient,
|
|
@@ -4593,15 +4754,16 @@ export {
|
|
|
4593
4754
|
deserializeBinaryMessage,
|
|
4594
4755
|
deserializeCompactBinaryMessage,
|
|
4595
4756
|
deserializeMessage,
|
|
4757
|
+
genericErrorFilter,
|
|
4596
4758
|
getLogs,
|
|
4597
4759
|
getMethodsMeta,
|
|
4598
4760
|
getServiceMeta,
|
|
4599
4761
|
getStateDescription,
|
|
4600
4762
|
isConnected,
|
|
4763
|
+
isReconnectProofSupported,
|
|
4601
4764
|
likelyConnected,
|
|
4602
4765
|
parseStreamRef,
|
|
4603
4766
|
readPolymorphismMarker,
|
|
4604
|
-
resolveStreamRefs,
|
|
4605
4767
|
rpcMethod,
|
|
4606
4768
|
rpcService,
|
|
4607
4769
|
serializeBinaryFrame,
|
|
@@ -4613,6 +4775,7 @@ export {
|
|
|
4613
4775
|
splitCompactBinaryFrame,
|
|
4614
4776
|
splitFrame,
|
|
4615
4777
|
toExceptionInfo,
|
|
4778
|
+
toRpcStream,
|
|
4616
4779
|
wireMethodName,
|
|
4617
4780
|
xxh3_64,
|
|
4618
4781
|
xxh3_64str
|