@actuallab/rpc 14.3.5 → 14.3.26

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
@@ -1285,6 +1285,15 @@ var RpcSystemCallHandler = class {
1285
1285
  );
1286
1286
  return;
1287
1287
  }
1288
+ if (!peer.tryClaimReconnect()) {
1289
+ peer.hub.systemCallSender.error(
1290
+ peer.connection,
1291
+ peer.serializationFormat,
1292
+ relatedId,
1293
+ new Error("TooLateToReconnect: this connection was already reconnected")
1294
+ );
1295
+ return;
1296
+ }
1288
1297
  let unknownIds = [];
1289
1298
  try {
1290
1299
  const stagesRaw = args[1];
@@ -2025,6 +2034,7 @@ function _compactBufferedUnsentSuffix(buffer, firstUnsentIndex, canSkipTo) {
2025
2034
  var { warnLog: warnLog6 } = getLogs("RpcStream");
2026
2035
  var DEFAULT_ACK_PERIOD2 = 30;
2027
2036
  var DEFAULT_ACK_ADVANCE2 = 61;
2037
+ var MAX_BATCH_SIZE = 1024;
2028
2038
  function parseStreamRef(value) {
2029
2039
  if (typeof value === "string") {
2030
2040
  const parts = value.split(",");
@@ -2212,6 +2222,10 @@ var RpcStream = class _RpcStream {
2212
2222
  this._failProtocol(`invalid $sys.B index/length: ${String(index)}/${String(items.length)}`);
2213
2223
  return;
2214
2224
  }
2225
+ if (items.length > MAX_BATCH_SIZE) {
2226
+ this._failProtocol(`$sys.B batch is too large: ${items.length} > ${MAX_BATCH_SIZE}`);
2227
+ return;
2228
+ }
2215
2229
  if (index > this._nextExpectedIndex) {
2216
2230
  warnLog6?.log(`batch index gap: localId=${this.id.localId}, expected=${this._nextExpectedIndex}, received=${index}`);
2217
2231
  if (!this.allowReconnect) {
@@ -2346,11 +2360,23 @@ var RpcStream = class _RpcStream {
2346
2360
  // last acknowledged index. The sending side is the only place that window
2347
2361
  // was ever enforced, which left a flooding remote free to grow our buffer
2348
2362
  // without bound (I6) — so we enforce our half of the contract here.
2363
+ //
2364
+ // The window is credited by consumption, never by `_ackSentUpTo`: a reset ack
2365
+ // re-bases to what we've *received*, so crediting it would let a peer ratchet
2366
+ // its own limit up by alternating "fill the window" with a forced gap.
2367
+ // Twice ackAdvance is what a conforming sender can leave buffered (ackAdvance
2368
+ // in steady state, plus ackAdvance more after a reset re-bases it), and
2369
+ // ackPeriod covers the lag of acks behind consumption.
2349
2370
  _isBeyondAckWindow(nextExpectedIndex) {
2350
- return this.ackAdvance > 0 && nextExpectedIndex > this._ackSentUpTo + this.ackAdvance;
2371
+ if (this.ackAdvance <= 0)
2372
+ return false;
2373
+ return nextExpectedIndex > this._nextConsumedIndex + this._ackWindowSize();
2374
+ }
2375
+ _ackWindowSize() {
2376
+ return 2 * this.ackAdvance + this.ackPeriod;
2351
2377
  }
2352
2378
  _ackWindowError(nextExpectedIndex) {
2353
- return `Stream ran past its ack window: reached ${nextExpectedIndex}, acked ${this._ackSentUpTo}, ackAdvance ${this.ackAdvance}`;
2379
+ return `Stream ran past its ack window: reached ${nextExpectedIndex}, consumed ${this._nextConsumedIndex}, ackAdvance ${this.ackAdvance}`;
2354
2380
  }
2355
2381
  _gapError(index) {
2356
2382
  return new Error(
@@ -2732,6 +2758,9 @@ var RpcPeer = class {
2732
2758
  * stale `$sys.Reconnect` that references an older connection generation
2733
2759
  * (C# `RpcSystemCalls.Reconnect`'s `ownHandshake.Index` check). */
2734
2760
  _ownHandshakeIndex = 0;
2761
+ /** Whether the current connection generation has already used its single
2762
+ * `$sys.Reconnect`. Mirrors C# `RpcPeerConnectionState.TryClaimReconnect`. */
2763
+ _isReconnectClaimed = false;
2735
2764
  _connection;
2736
2765
  /** Mirror of `_connectionState === Connected`. True only after the
2737
2766
  * handshake round-trip completes; outbound `call()` checks this to
@@ -2802,6 +2831,19 @@ var RpcPeer = class {
2802
2831
  get ownHandshakeIndex() {
2803
2832
  return this._ownHandshakeIndex;
2804
2833
  }
2834
+ // A conforming peer sends `$sys.Reconnect` at most once per connection, so anything
2835
+ // beyond that is a replay - and the reconciliation it performs is proportional to the
2836
+ // call ids it carries, with nothing else capping how often it can arrive.
2837
+ tryClaimReconnect() {
2838
+ if (this._isReconnectClaimed)
2839
+ return false;
2840
+ this._isReconnectClaimed = true;
2841
+ return true;
2842
+ }
2843
+ nextOwnHandshakeIndex() {
2844
+ this._isReconnectClaimed = false;
2845
+ return ++this._ownHandshakeIndex;
2846
+ }
2805
2847
  _setConnectionState(value) {
2806
2848
  if (this._connectionState === value) return;
2807
2849
  const previousState = this._connectionState;
@@ -3337,7 +3379,7 @@ var RpcClientPeer = class extends RpcPeer {
3337
3379
  this.serializationFormat,
3338
3380
  this.id,
3339
3381
  this.hub.hubId,
3340
- ++this._ownHandshakeIndex
3382
+ this.nextOwnHandshakeIndex()
3341
3383
  );
3342
3384
  } catch (e) {
3343
3385
  warnLog7?.log(`'${this.ref}': Failed to send handshake`, e);
@@ -3602,7 +3644,7 @@ var RpcServerPeer = class extends RpcPeer {
3602
3644
  this.serializationFormat,
3603
3645
  this.id,
3604
3646
  this.hub.hubId,
3605
- ++this._ownHandshakeIndex
3647
+ this.nextOwnHandshakeIndex()
3606
3648
  );
3607
3649
  this._setConnectionState(3 /* Connected */);
3608
3650
  }