@actuallab/rpc 12.3.83 → 12.4.1

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
@@ -976,7 +976,7 @@ var RpcObjectKind = { Local: 0, Remote: 1 };
976
976
  var import_core5 = require("@actuallab/core");
977
977
  var { warnLog: warnLog3 } = getLogs("RpcSharedStream");
978
978
  var DEFAULT_ACK_PERIOD = 256;
979
- var DEFAULT_ACK_ADVANCE = 128;
979
+ var DEFAULT_BUFFER_SIZE = 128;
980
980
  var RpcStreamSender = class _RpcStreamSender {
981
981
  static _emptyGuid = "00000000-0000-0000-0000-000000000000";
982
982
  id;
@@ -986,7 +986,7 @@ var RpcStreamSender = class _RpcStreamSender {
986
986
  canSkipTo;
987
987
  peer;
988
988
  ackPeriod;
989
- ackAdvance;
989
+ bufferSize;
990
990
  /**
991
991
  * If true, the source was produced by a factory `(abortSignal) => AsyncIterable<T>`
992
992
  * and may honor the AbortSignal to exit gracefully. `disconnect()` first aborts
@@ -1009,7 +1009,7 @@ var RpcStreamSender = class _RpcStreamSender {
1009
1009
  // -- ACK queue (analog of .NET's Channel<(long, bool)>) --
1010
1010
  _acks = [];
1011
1011
  _whenAckReady = null;
1012
- constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true, isRealTime = false, canSkipTo = () => true, sourceUsesAbortSignal = false) {
1012
+ constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, bufferSize = DEFAULT_BUFFER_SIZE, allowReconnect = true, isRealTime = false, canSkipTo = () => true, sourceUsesAbortSignal = false) {
1013
1013
  const localId = peer.sharedObjects.nextId();
1014
1014
  this.id = { hostId: peer.hub.hubId, localId };
1015
1015
  this.allowReconnect = allowReconnect;
@@ -1017,7 +1017,7 @@ var RpcStreamSender = class _RpcStreamSender {
1017
1017
  this.canSkipTo = canSkipTo;
1018
1018
  this.peer = peer;
1019
1019
  this.ackPeriod = ackPeriod;
1020
- this.ackAdvance = ackAdvance;
1020
+ this.bufferSize = bufferSize;
1021
1021
  this.sourceUsesAbortSignal = sourceUsesAbortSignal;
1022
1022
  }
1023
1023
  /** AbortSignal that is aborted when the sender is disconnected. */
@@ -1026,7 +1026,7 @@ var RpcStreamSender = class _RpcStreamSender {
1026
1026
  }
1027
1027
  /** Returns the stream reference string for the $sys.Ok response. */
1028
1028
  toRef() {
1029
- return `${this.id.hostId},${this.id.localId},${this.ackPeriod},${this.ackAdvance},${this.allowReconnect ? "1" : "0"},${this.isRealTime ? "1" : "0"}`;
1029
+ return `${this.id.hostId},${this.id.localId},${this.ackPeriod},${this.bufferSize},${this.allowReconnect ? "1" : "0"},${this.isRealTime ? "1" : "0"}`;
1030
1030
  }
1031
1031
  /**
1032
1032
  * Called by system call handler when $sys.Ack is received from the client.
@@ -1103,9 +1103,8 @@ var RpcStreamSender = class _RpcStreamSender {
1103
1103
  /**
1104
1104
  * Consume an AsyncIterable and send all items to the client.
1105
1105
  *
1106
- * Dispatches to the non-real-time or real-time pump, matching .NET
1107
- * `RpcSharedStream<T>.OnRun` at
1108
- * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:105-108.
1106
+ * Matches .NET `RpcSharedStream<T>.OnRun` at
1107
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs.
1109
1108
  */
1110
1109
  async writeFrom(source) {
1111
1110
  await this._started.promise;
@@ -1114,11 +1113,7 @@ var RpcStreamSender = class _RpcStreamSender {
1114
1113
  this._iterator = iterator;
1115
1114
  const state = { iteratorDone: false };
1116
1115
  try {
1117
- if (this.isRealTime) {
1118
- await this._runRealTime(iterator, state);
1119
- } else {
1120
- await this._runNonRealTime(iterator, state);
1121
- }
1116
+ await this._run(iterator, state);
1122
1117
  } finally {
1123
1118
  if (state.iteratorDone && this._iterator === iterator) {
1124
1119
  this._iterator = null;
@@ -1126,90 +1121,59 @@ var RpcStreamSender = class _RpcStreamSender {
1126
1121
  }
1127
1122
  }
1128
1123
  /**
1129
- * Non-real-time pump — back-pressure through buffered ACKs.
1130
- *
1131
- * Direct port of .NET `OnRunNonRealTime` at
1132
- * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:110-217.
1124
+ * ACK-driven pump. Non-real-time streams apply normal backpressure at
1125
+ * `bufferSize`; real-time streams may compact the already-buffered unsent
1126
+ * suffix to the latest buffered `canSkipTo` item after the consumer ACKs.
1133
1127
  */
1134
1128
  /* eslint-disable @typescript-eslint/no-unnecessary-condition -- _ended changes across awaits */
1135
- async _runNonRealTime(iterator, state) {
1136
- const buffer = new import_core5.RingBuffer(this.ackAdvance + 1);
1129
+ async _run(iterator, state) {
1130
+ const buffer = new import_core5.RingBuffer(this.bufferSize + 1);
1131
+ const isRealTime = this.isRealTime;
1137
1132
  let bufferStart = 0;
1138
1133
  let isFullyBuffered = false;
1139
- while (true) {
1140
- let ack = this._tryProcessAcks();
1141
- if (!ack) {
1142
- await this._waitAckReady();
1143
- if (this._ended) return;
1144
- ack = this._tryProcessAcks();
1145
- if (!ack) {
1146
- warnLog3?.log("Something is off: couldn't read an acknowledgement");
1147
- return;
1148
- }
1149
- }
1150
- this._lastAckedIndex = ack.nextIndex;
1151
- const shift = _clamp(ack.nextIndex - bufferStart, 0, buffer.count);
1152
- if (shift > 0) {
1153
- buffer.moveHead(shift);
1154
- bufferStart += shift;
1155
- }
1156
- if (this._nextIndex < bufferStart) {
1157
- if (!this._ended)
1158
- this.sendEnd(new Error("Stream position unavailable."));
1159
- return;
1160
- }
1161
- let bufferIndex = this._nextIndex - bufferStart;
1162
- const maxIndex = ack.nextIndex + this.ackAdvance;
1163
- while (this._nextIndex < maxIndex) {
1164
- if (this._ended) return;
1165
- if (bufferIndex < buffer.count) {
1166
- const item = buffer.get(bufferIndex++);
1167
- if (item.kind === "value") {
1168
- this.sendItem(item.value);
1169
- } else if (item.kind === "end") {
1170
- if (!this._ended) this.sendEnd();
1171
- return;
1172
- } else {
1173
- if (!this._ended) this.sendEnd(item.error);
1174
- return;
1175
- }
1176
- continue;
1177
- }
1178
- if (isFullyBuffered) break;
1179
- if (!buffer.hasRemainingCapacity) break;
1180
- let pulled;
1181
- try {
1182
- const r = await iterator.next();
1183
- if (this._ended) return;
1184
- if (r.done) {
1185
- pulled = _endItem;
1186
- isFullyBuffered = true;
1187
- state.iteratorDone = true;
1188
- } else {
1189
- pulled = { kind: "value", value: r.value };
1190
- }
1191
- } catch (e) {
1192
- state.iteratorDone = true;
1134
+ let whenMovedNext = null;
1135
+ const pending = /* @__PURE__ */ Symbol("pending");
1136
+ const readNext = async () => {
1137
+ const next = whenMovedNext ?? iterator.next();
1138
+ whenMovedNext = null;
1139
+ return await next;
1140
+ };
1141
+ const tryReadReady = async () => {
1142
+ whenMovedNext ??= iterator.next();
1143
+ return await Promise.race([
1144
+ whenMovedNext,
1145
+ Promise.resolve(pending)
1146
+ ]);
1147
+ };
1148
+ const bufferNext = async (onlyIfReady, prefetchNext) => {
1149
+ let r;
1150
+ try {
1151
+ r = onlyIfReady ? await tryReadReady() : await readNext();
1152
+ if (r === pending)
1153
+ return false;
1154
+ whenMovedNext = null;
1155
+ if (this._ended) return false;
1156
+ if (r.done) {
1157
+ buffer.pushTail(_endItem);
1193
1158
  isFullyBuffered = true;
1194
- pulled = {
1195
- kind: "error",
1196
- error: e instanceof Error ? e : new Error(String(e))
1197
- };
1159
+ state.iteratorDone = true;
1160
+ } else {
1161
+ buffer.pushTail({ kind: "value", value: r.value });
1162
+ if (prefetchNext)
1163
+ whenMovedNext = iterator.next();
1198
1164
  }
1199
- buffer.pushTail(pulled);
1165
+ return true;
1166
+ } catch (e) {
1167
+ whenMovedNext = null;
1168
+ state.iteratorDone = true;
1169
+ isFullyBuffered = true;
1170
+ buffer.pushTail({
1171
+ kind: "error",
1172
+ error: e instanceof Error ? e : new Error(String(e))
1173
+ });
1174
+ return true;
1200
1175
  }
1201
- }
1202
- }
1203
- /**
1204
- * Real-time pump — drops stale items to stay close to the source's head.
1205
- *
1206
- * Direct port of .NET `OnRunRealTime` at
1207
- * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:219-420.
1208
- */
1209
- async _runRealTime(iterator, state) {
1210
- const buffer = new import_core5.RingBuffer(this.ackAdvance + 1);
1211
- let bufferStart = 0;
1212
- let isFullyBuffered = false;
1176
+ };
1213
1177
  while (true) {
1214
1178
  let ack = this._tryProcessAcks();
1215
1179
  if (!ack) {
@@ -1222,7 +1186,7 @@ var RpcStreamSender = class _RpcStreamSender {
1222
1186
  }
1223
1187
  }
1224
1188
  this._lastAckedIndex = ack.nextIndex;
1225
- if (ack.mustReset && !isFullyBuffered) {
1189
+ if (isRealTime && ack.mustReset && !isFullyBuffered) {
1226
1190
  buffer.clear();
1227
1191
  bufferStart = this._nextIndex;
1228
1192
  while (true) {
@@ -1230,7 +1194,7 @@ var RpcStreamSender = class _RpcStreamSender {
1230
1194
  let item;
1231
1195
  let accepted = false;
1232
1196
  try {
1233
- const r = await iterator.next();
1197
+ const r = await readNext();
1234
1198
  if (this._ended) return;
1235
1199
  if (r.done) {
1236
1200
  item = _endItem;
@@ -1265,11 +1229,22 @@ var RpcStreamSender = class _RpcStreamSender {
1265
1229
  return;
1266
1230
  }
1267
1231
  let bufferIndex = this._nextIndex - bufferStart;
1268
- const maxIndex = ack.nextIndex + this.ackAdvance;
1232
+ const maxIndex = ack.nextIndex + this.bufferSize;
1269
1233
  while (this._nextIndex < maxIndex) {
1270
1234
  if (this._ended) return;
1271
- if (this._acks.length > 0) break;
1272
- if (bufferIndex < buffer.count) {
1235
+ if (isRealTime) {
1236
+ while (buffer.hasRemainingCapacity && !isFullyBuffered && this._acks.length === 0) {
1237
+ if (!await bufferNext(true, true))
1238
+ break;
1239
+ }
1240
+ if (bufferIndex >= buffer.count && !isFullyBuffered && buffer.hasRemainingCapacity && this._acks.length === 0) {
1241
+ await bufferNext(false, true);
1242
+ }
1243
+ if (ack.nextIndex > 0)
1244
+ bufferIndex = _compactBufferedUnsentSuffix(buffer, bufferIndex, this.canSkipTo);
1245
+ if (this._acks.length > 0) break;
1246
+ }
1247
+ while (this._nextIndex < maxIndex && bufferIndex < buffer.count) {
1273
1248
  const item = buffer.get(bufferIndex++);
1274
1249
  if (item.kind === "value") {
1275
1250
  this.sendItem(item.value);
@@ -1280,73 +1255,13 @@ var RpcStreamSender = class _RpcStreamSender {
1280
1255
  if (!this._ended) this.sendEnd(item.error);
1281
1256
  return;
1282
1257
  }
1283
- continue;
1284
1258
  }
1259
+ if (this._nextIndex >= maxIndex)
1260
+ break;
1285
1261
  if (isFullyBuffered) break;
1286
1262
  if (!buffer.hasRemainingCapacity) break;
1287
- let pulled;
1288
- try {
1289
- const r = await iterator.next();
1290
- if (this._ended) return;
1291
- if (r.done) {
1292
- pulled = _endItem;
1293
- isFullyBuffered = true;
1294
- state.iteratorDone = true;
1295
- } else {
1296
- pulled = { kind: "value", value: r.value };
1297
- }
1298
- } catch (e) {
1299
- state.iteratorDone = true;
1300
- isFullyBuffered = true;
1301
- pulled = {
1302
- kind: "error",
1303
- error: e instanceof Error ? e : new Error(String(e))
1304
- };
1305
- }
1306
- buffer.pushTail(pulled);
1307
- }
1308
- if (this._nextIndex >= maxIndex && !isFullyBuffered && !this._ended) {
1309
- let latestValue;
1310
- let haveLatest = false;
1311
- let exitReason = "ack";
1312
- let exitError = null;
1313
- drain: while (!this._ended) {
1314
- if (this._acks.length > 0) {
1315
- exitReason = "ack";
1316
- break drain;
1317
- }
1318
- let r;
1319
- try {
1320
- r = await iterator.next();
1321
- } catch (e) {
1322
- state.iteratorDone = true;
1323
- isFullyBuffered = true;
1324
- exitReason = "error";
1325
- exitError = e instanceof Error ? e : new Error(String(e));
1326
- break drain;
1327
- }
1328
- if (this._ended) return;
1329
- if (r.done) {
1330
- state.iteratorDone = true;
1331
- isFullyBuffered = true;
1332
- exitReason = "end";
1333
- break drain;
1334
- }
1335
- if (this.canSkipTo(r.value)) {
1336
- latestValue = r.value;
1337
- haveLatest = true;
1338
- }
1339
- }
1340
- if (exitReason === "end" || exitReason === "error") {
1341
- if (haveLatest && !this._ended) this.sendItem(latestValue);
1342
- if (!this._ended) this.sendEnd(exitError);
1343
- return;
1344
- }
1345
- if (haveLatest) {
1346
- const wasFull = buffer.isFull;
1347
- buffer.pushTailAndMoveHeadIfFull({ kind: "value", value: latestValue });
1348
- if (wasFull) bufferStart++;
1349
- }
1263
+ if (!isRealTime)
1264
+ await bufferNext(false, false);
1350
1265
  }
1351
1266
  }
1352
1267
  }
@@ -1413,11 +1328,28 @@ var _endItem = { kind: "end" };
1413
1328
  function _clamp(value, min, max) {
1414
1329
  return value < min ? min : value > max ? max : value;
1415
1330
  }
1331
+ function _compactBufferedUnsentSuffix(buffer, firstUnsentIndex, canSkipTo) {
1332
+ let skipToIndex = -1;
1333
+ for (let i = firstUnsentIndex; i < buffer.count; i++) {
1334
+ const item = buffer.get(i);
1335
+ if (item.kind === "value" && canSkipTo(item.value))
1336
+ skipToIndex = i;
1337
+ }
1338
+ if (skipToIndex <= firstUnsentIndex)
1339
+ return firstUnsentIndex;
1340
+ const items = buffer.toArray();
1341
+ buffer.clear();
1342
+ for (let i = 0; i < firstUnsentIndex; i++)
1343
+ buffer.pushTail(items[i]);
1344
+ for (let i = skipToIndex; i < items.length; i++)
1345
+ buffer.pushTail(items[i]);
1346
+ return firstUnsentIndex;
1347
+ }
1416
1348
 
1417
1349
  // src/rpc-stream.ts
1418
1350
  var { warnLog: warnLog4 } = getLogs("RpcStream");
1419
1351
  var DEFAULT_ACK_PERIOD2 = 30;
1420
- var DEFAULT_ACK_ADVANCE2 = 61;
1352
+ var DEFAULT_BUFFER_SIZE2 = 61;
1421
1353
  function parseStreamRef(value) {
1422
1354
  if (typeof value === "string") {
1423
1355
  const parts = value.split(",");
@@ -1425,12 +1357,12 @@ function parseStreamRef(value) {
1425
1357
  const hostId = parts[0];
1426
1358
  const localId = parseInt(parts[1], 10);
1427
1359
  const ackPeriod = parseInt(parts[2], 10);
1428
- const ackAdvance = parseInt(parts[3], 10);
1429
- if (isNaN(localId) || isNaN(ackPeriod) || isNaN(ackAdvance))
1360
+ const bufferSize = parseInt(parts[3], 10);
1361
+ if (isNaN(localId) || isNaN(ackPeriod) || isNaN(bufferSize))
1430
1362
  return null;
1431
1363
  const allowReconnect = parts.length < 5 || parts[4] !== "0";
1432
1364
  const isRealTime = parts.length >= 6 && parts[5] === "1";
1433
- return { hostId, localId, ackPeriod, ackAdvance, allowReconnect, isRealTime };
1365
+ return { hostId, localId, ackPeriod, bufferSize, allowReconnect, isRealTime };
1434
1366
  }
1435
1367
  if (typeof value === "object" && value !== null) {
1436
1368
  const obj = value;
@@ -1441,10 +1373,10 @@ function parseStreamRef(value) {
1441
1373
  const hostId = String(rawHostId);
1442
1374
  const localId = Number(serializedId[1]);
1443
1375
  const ackPeriod = Number(obj.AckPeriod ?? 256);
1444
- const ackAdvance = Number(obj.AckAdvance ?? 128);
1376
+ const bufferSize = Number(obj.BufferSize ?? 128);
1445
1377
  const allowReconnect = obj.AllowReconnect !== false;
1446
1378
  const isRealTime = obj.IsRealTime === true;
1447
- return { hostId, localId, ackPeriod, ackAdvance, allowReconnect, isRealTime };
1379
+ return { hostId, localId, ackPeriod, bufferSize, allowReconnect, isRealTime };
1448
1380
  }
1449
1381
  return null;
1450
1382
  }
@@ -1464,7 +1396,7 @@ var RpcStream = class _RpcStream {
1464
1396
  isRealTime;
1465
1397
  canSkipTo;
1466
1398
  ackPeriod;
1467
- ackAdvance;
1399
+ bufferSize;
1468
1400
  // Local (origin) mode
1469
1401
  localSource;
1470
1402
  // Remote (target) mode
@@ -1494,7 +1426,7 @@ var RpcStream = class _RpcStream {
1494
1426
  this.isRealTime = options.isRealTime ?? false;
1495
1427
  this.canSkipTo = options.canSkipTo ?? (() => true);
1496
1428
  this.ackPeriod = options.ackPeriod ?? DEFAULT_ACK_PERIOD2;
1497
- this.ackAdvance = options.ackAdvance ?? DEFAULT_ACK_ADVANCE2;
1429
+ this.bufferSize = options.bufferSize ?? DEFAULT_BUFFER_SIZE2;
1498
1430
  } else {
1499
1431
  const ref = sourceOrRef;
1500
1432
  const peer = optionsOrPeer;
@@ -1505,7 +1437,7 @@ var RpcStream = class _RpcStream {
1505
1437
  this.isRealTime = ref.isRealTime;
1506
1438
  this.canSkipTo = () => true;
1507
1439
  this.ackPeriod = ref.ackPeriod;
1508
- this.ackAdvance = ref.ackAdvance;
1440
+ this.bufferSize = ref.bufferSize;
1509
1441
  }
1510
1442
  }
1511
1443
  /**
@@ -1523,7 +1455,7 @@ var RpcStream = class _RpcStream {
1523
1455
  * stream reference.
1524
1456
  *
1525
1457
  * For binary peers the ref is a MessagePack-compatible object
1526
- * (`{ SerializedId, AckPeriod, AckAdvance, AllowReconnect }`);
1458
+ * (`{ SerializedId, AckPeriod, BufferSize, AllowReconnect }`);
1527
1459
  * for text peers it is a comma-separated string.
1528
1460
  *
1529
1461
  * Can only be called once per stream.
@@ -1540,7 +1472,7 @@ var RpcStream = class _RpcStream {
1540
1472
  const sender = new RpcStreamSender(
1541
1473
  peer,
1542
1474
  this.ackPeriod,
1543
- this.ackAdvance,
1475
+ this.bufferSize,
1544
1476
  this.allowReconnect,
1545
1477
  this.isRealTime,
1546
1478
  this.canSkipTo,
@@ -1553,7 +1485,7 @@ var RpcStream = class _RpcStream {
1553
1485
  this._toRefValue = peer.serializationFormat.isBinary ? {
1554
1486
  SerializedId: [sender.id.hostId, sender.id.localId],
1555
1487
  AckPeriod: sender.ackPeriod,
1556
- AckAdvance: sender.ackAdvance,
1488
+ BufferSize: sender.bufferSize,
1557
1489
  AllowReconnect: sender.allowReconnect
1558
1490
  } : sender.toRef();
1559
1491
  return this._toRefValue;
@@ -2294,6 +2226,17 @@ var defaultConnectionUrlResolver = (peer) => {
2294
2226
  return peer.ref + sep + `clientId=${peer.clientId}&f=${formatKey}`;
2295
2227
  }
2296
2228
  };
2229
+ var sanitizeUrl = (url) => {
2230
+ try {
2231
+ const parsed = new URL(url);
2232
+ if (!parsed.searchParams.has("session"))
2233
+ return url;
2234
+ parsed.searchParams.set("session", "<redacted>");
2235
+ return parsed.toString();
2236
+ } catch {
2237
+ return url.replace(/([?&]session=)[^&]*/i, "$1<redacted>");
2238
+ }
2239
+ };
2297
2240
  function parseFormatFromUrl(url) {
2298
2241
  try {
2299
2242
  const f = new URL(url).searchParams.get("f");
@@ -2741,7 +2684,6 @@ var RpcClientPeer = class extends RpcPeer {
2741
2684
  }
2742
2685
  async _runImpl() {
2743
2686
  if (this._disposed) return;
2744
- const connUrl = this.connectionUrlResolver(this);
2745
2687
  infoLog?.log(`'${this.ref}': Started (Client)`);
2746
2688
  try {
2747
2689
  while (!this._disposed) {
@@ -2762,7 +2704,8 @@ var RpcClientPeer = class extends RpcPeer {
2762
2704
  let lastCloseCode = 0;
2763
2705
  let lastCloseReason = "";
2764
2706
  try {
2765
- infoLog?.log(`'${this.ref}': Connecting to ${connUrl}`);
2707
+ const connUrl = await this.connectionUrlResolver(this);
2708
+ infoLog?.log(`'${this.ref}': Connecting to ${sanitizeUrl(connUrl)}`);
2766
2709
  const ws = this.webSocketFactory?.(connUrl) ?? new WebSocket(connUrl);
2767
2710
  const conn = new RpcWebSocketConnection(
2768
2711
  ws,