@actuallab/rpc 14.0.17 → 14.2.23

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