@actuallab/rpc 14.3.32 → 14.4.3

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,6 +56,7 @@ __export(index_exports, {
56
56
  RpcOutboundCall: () => RpcOutboundCall,
57
57
  RpcOutboundCallTracker: () => RpcOutboundCallTracker,
58
58
  RpcPeer: () => RpcPeer,
59
+ RpcPeerChangeKind: () => RpcPeerChangeKind,
59
60
  RpcPeerStateKind: () => RpcPeerStateKind,
60
61
  RpcPeerStateMonitor: () => RpcPeerStateMonitor,
61
62
  RpcRefBuilder: () => RpcRefBuilder,
@@ -864,6 +865,16 @@ var RpcOutboundCallTracker = class {
864
865
  activeCallIds() {
865
866
  return [...this._calls.keys()];
866
867
  }
868
+ /** Calls handed to the transport at least once. Must be snapshotted BEFORE the
869
+ * peer enters `Connected` — that flushes every queued call, making queued and
870
+ * in-flight calls indistinguishable. Mirrors .NET's `GetSentCalls`. */
871
+ getSentCalls() {
872
+ const calls = [];
873
+ for (const call of this._calls.values())
874
+ if (call.sentAt !== 0)
875
+ calls.push(call);
876
+ return calls;
877
+ }
867
878
  /** Reject all pending calls with the given error.
868
879
  * Stage-3 compute calls (result resolved, awaiting invalidation) are kept in the tracker. */
869
880
  rejectAll(error) {
@@ -982,7 +993,8 @@ var RpcCallTimeouts = class _RpcCallTimeouts {
982
993
  static None = new _RpcCallTimeouts(Infinity, Infinity);
983
994
  /** Command default: 1.5 s connect / 10 s run (.NET `RpcCallTimeouts.Default.Command`). */
984
995
  static Command = new _RpcCallTimeouts(1500, 1e4);
985
- /** Max time a call may wait for the connection before it is sent. */
996
+ /** Max time a call may wait for a connection - before it is sent, and again for the
997
+ * duration of any outage it is still pending through. */
986
998
  connectTimeoutMs;
987
999
  /** Max time a sent call may stay unanswered before it is failed. */
988
1000
  runTimeoutMs;
@@ -2776,6 +2788,8 @@ var RpcPeer = class {
2776
2788
  /** Wall-clock time of the last inbound `$sys.KeepAlive`, or 0 if none yet
2777
2789
  * for the current connection. Useful for diagnostics. */
2778
2790
  _lastKeepAliveAt = 0;
2791
+ /** When the current outage began; 0 until the peer has been connected at least once. */
2792
+ _disconnectedAt = 0;
2779
2793
  /** Outbound keep-alive send period. Initialized from `hub.limits` at
2780
2794
  * construction; set directly to override on a single peer. */
2781
2795
  keepAlivePeriodMs;
@@ -2796,9 +2810,10 @@ var RpcPeer = class {
2796
2810
  }
2797
2811
  /** Fail outbound calls whose per-call {@link RpcCallTimeouts} elapsed —
2798
2812
  * the connect timeout while a call still awaits the wire, the run timeout
2799
- * once it has been sent. Mirrors .NET `RpcOutboundCallTracker.Maintain`
2800
- * (RpcCallTrackers.cs:108-207). Calls without timeouts (queries) never
2801
- * time out here. */
2813
+ * once it has been sent, and the connect timeout again for any pending call while the
2814
+ * peer is away. Mirrors .NET `RpcOutboundCallTracker.HandleDisconnect` (ConnectTimeout,
2815
+ * per outage) plus `Maintain` (RunTimeout, per connection). Calls without timeouts
2816
+ * (queries) never time out here. */
2802
2817
  _maintainOutboundCalls() {
2803
2818
  const now = Date.now();
2804
2819
  const canRunTimeout = this._isConnected && (this._lastKeepAliveAt === 0 || now - this._lastKeepAliveAt <= this.keepAliveTimeoutMs);
@@ -2807,7 +2822,11 @@ var RpcPeer = class {
2807
2822
  if (timeouts === void 0 || call.result.isCompleted)
2808
2823
  continue;
2809
2824
  let timedOut;
2810
- if (call.sentAt === 0) {
2825
+ if (!this._isConnected) {
2826
+ const since = Math.max(this._disconnectedAt, call.startedAt);
2827
+ if (now - since >= timeouts.connectTimeoutMs)
2828
+ timedOut = `connect timeout (${timeouts.connectTimeoutMs}ms)`;
2829
+ } else if (call.sentAt === 0) {
2811
2830
  if (now - call.startedAt >= timeouts.connectTimeoutMs)
2812
2831
  timedOut = `connect timeout (${timeouts.connectTimeoutMs}ms)`;
2813
2832
  } else if (canRunTimeout && now - call.sentAt >= timeouts.runTimeoutMs)
@@ -2850,10 +2869,13 @@ var RpcPeer = class {
2850
2869
  this._connectionState = value;
2851
2870
  if (value === 3 /* Connected */) {
2852
2871
  this._isConnected = true;
2872
+ this._disconnectedAt = 0;
2853
2873
  this._armKeepAliveWatchdog();
2854
2874
  } else if (value === 0 /* Disconnected */) {
2855
- if (previousState === 3 /* Connected */)
2875
+ if (previousState === 3 /* Connected */) {
2856
2876
  this._isConnected = false;
2877
+ this._disconnectedAt = Date.now();
2878
+ }
2857
2879
  this._disarmKeepAliveWatchdog();
2858
2880
  }
2859
2881
  this.connectionStateChanged.trigger(value);
@@ -2878,6 +2900,8 @@ var RpcPeer = class {
2878
2900
  conn.closed.add((ev) => {
2879
2901
  if (this._connection !== conn) return;
2880
2902
  this._connection = void 0;
2903
+ if (this._isConnected)
2904
+ this._disconnectedAt = Date.now();
2881
2905
  this._isConnected = false;
2882
2906
  if (ev.reason)
2883
2907
  infoLog?.log(`'${this.ref}': Disconnected: ${ev.reason} (code=${ev.code})`);
@@ -3178,6 +3202,13 @@ var RpcConnectionState = /* @__PURE__ */ ((RpcConnectionState2) => {
3178
3202
  RpcConnectionState2[RpcConnectionState2["Connected"] = 3] = "Connected";
3179
3203
  return RpcConnectionState2;
3180
3204
  })(RpcConnectionState || {});
3205
+ var RpcPeerChangeKind = /* @__PURE__ */ ((RpcPeerChangeKind2) => {
3206
+ RpcPeerChangeKind2[RpcPeerChangeKind2["Unchanged"] = 0] = "Unchanged";
3207
+ RpcPeerChangeKind2[RpcPeerChangeKind2["ChangedToVeryFirst"] = 1] = "ChangedToVeryFirst";
3208
+ RpcPeerChangeKind2[RpcPeerChangeKind2["Changed"] = 2] = "Changed";
3209
+ return RpcPeerChangeKind2;
3210
+ })(RpcPeerChangeKind || {});
3211
+ var peerChangeKindNames = ["Unchanged", "ChangedToVeryFirst", "Changed"];
3181
3212
  var RpcClientPeer = class extends RpcPeer {
3182
3213
  _disposed = false;
3183
3214
  /** Aborted when the peer is stopped/disposed — threaded into the reconnect
@@ -3190,7 +3221,10 @@ var RpcClientPeer = class extends RpcPeer {
3190
3221
  * `connectionState.Handshake.Index` path in
3191
3222
  * `RpcOutboundCallTracker.TryReconnect`. */
3192
3223
  _remoteHandshakeIndex = 0;
3193
- _lastRemotePeerId;
3224
+ /** Mirrors .NET's `lastHandshake` local in `RpcPeer.OnRun`: first-ness is keyed
3225
+ * off "was there a previous handshake", not off `RemotePeerId` — a legacy server
3226
+ * sends none, so every one of its handshakes would look like the first. */
3227
+ _lastHandshake;
3194
3228
  _pendingHandshake;
3195
3229
  _reconnectsAt = 0;
3196
3230
  /** Server-issued reconnect secret, from the most recent handshake that
@@ -3271,10 +3305,11 @@ var RpcClientPeer = class extends RpcPeer {
3271
3305
  * self-invalidate and remote/shared objects are disposed). Pass `false`
3272
3306
  * to simulate a same-peer reconnect in tests. */
3273
3307
  connectWith(conn, isPeerChanged = true) {
3308
+ const sentCalls = this.outboundCalls.getSentCalls();
3274
3309
  this.setupConnection(conn);
3275
3310
  this._isConnected = true;
3276
3311
  this.connectionStateChanged.trigger(3 /* Connected */);
3277
- void this._reconnect(isPeerChanged);
3312
+ void this._reconnect(sentCalls, isPeerChanged);
3278
3313
  }
3279
3314
  /** Kick off the reconnect loop. Idempotent — subsequent calls are
3280
3315
  * no-ops; await `peer.whenRunning` if you need the loop's completion
@@ -3405,11 +3440,19 @@ var RpcClientPeer = class extends RpcPeer {
3405
3440
  this._remoteHandshakeIndex = remoteHandshake.Index ?? 0;
3406
3441
  if (remoteHandshake.Secret)
3407
3442
  this._reconnectSecret = remoteHandshake.Secret;
3408
- const isPeerChanged = this._detectPeerChange(remoteHandshake);
3409
- infoLog?.log(`'${this.ref}': Handshake succeeded, peerChanged=${isPeerChanged}`);
3443
+ const peerChangeKind = this._detectPeerChange(remoteHandshake);
3444
+ infoLog?.log(
3445
+ `'${this.ref}': Handshake succeeded, peerChangeKind=${peerChangeKindNames[peerChangeKind]}`
3446
+ );
3447
+ const sentCalls = this.outboundCalls.getSentCalls();
3410
3448
  this._setConnectionState(3 /* Connected */);
3411
3449
  connectedAt = Date.now();
3412
- await this._reconnect(isPeerChanged, closedController.signal);
3450
+ if (peerChangeKind !== 1 /* ChangedToVeryFirst */)
3451
+ await this._reconnect(
3452
+ sentCalls,
3453
+ peerChangeKind === 2 /* Changed */,
3454
+ closedController.signal
3455
+ );
3413
3456
  await whenClosed;
3414
3457
  } catch (e) {
3415
3458
  debugLog2?.log(`'${this.ref}': run() iteration failed:`, e);
@@ -3431,7 +3474,10 @@ var RpcClientPeer = class extends RpcPeer {
3431
3474
  }
3432
3475
  }
3433
3476
  /**
3434
- * Reconcile outbound calls after reconnection, then flush pending sends.
3477
+ * Reconcile the calls that were in flight on the connection that just died
3478
+ * (`sentCalls`), then resend the survivors. Calls still waiting for a
3479
+ * connection are deliberately NOT passed in — `AllowReconnect`/`AllowResend`
3480
+ * don't apply to them, and entering `Connected` already sends them.
3435
3481
  *
3436
3482
  * On peer change (different server hubId) or with binary wire format —
3437
3483
  * we blind-resend every eligible call.
@@ -3449,7 +3495,7 @@ var RpcClientPeer = class extends RpcPeer {
3449
3495
  * are always self-invalidated — TS doesn't track compute-call stages
3450
3496
  * across reconnect, so the safest behavior is to force a fresh compute.
3451
3497
  */
3452
- async _reconnect(isPeerChanged, closedSignal) {
3498
+ async _reconnect(sentCalls, isPeerChanged, closedSignal) {
3453
3499
  if (this._connection === void 0) return;
3454
3500
  if (isPeerChanged) {
3455
3501
  this.remoteObjects.disconnectAll();
@@ -3458,7 +3504,7 @@ var RpcClientPeer = class extends RpcPeer {
3458
3504
  this.remoteObjects.reconnectAll();
3459
3505
  }
3460
3506
  const eligible = [];
3461
- for (const call of [...this.outboundCalls.values()]) {
3507
+ for (const call of sentCalls) {
3462
3508
  const mode = call.remoteExecutionMode;
3463
3509
  if (!(mode & 2)) {
3464
3510
  if (!call.result.isCompleted)
@@ -3479,8 +3525,7 @@ var RpcClientPeer = class extends RpcPeer {
3479
3525
  continue;
3480
3526
  }
3481
3527
  eligible.push(call);
3482
- if (call.sentAt !== 0)
3483
- call.sentAt = Date.now();
3528
+ call.sentAt = Date.now();
3484
3529
  }
3485
3530
  let unknownIds = null;
3486
3531
  if (!isPeerChanged && eligible.length > 0) {
@@ -3568,19 +3613,18 @@ var RpcClientPeer = class extends RpcPeer {
3568
3613
  * `RpcHandshake.GetPeerChangeKind` keys off `RemotePeerId`, not
3569
3614
  * `RemoteHubId` (an idle server peer can be replaced while keeping the
3570
3615
  * same hub id). On a change it clears the inbound tracker and fires
3571
- * `peerChanged`; the very first handshake is never a change. */
3616
+ * `peerChanged`; the very first handshake is `ChangedToVeryFirst`. */
3572
3617
  _detectPeerChange(handshake) {
3573
- const remotePeerId = handshake.RemotePeerId;
3574
- if (remotePeerId === void 0)
3575
- return false;
3576
- const isPeerChanged = this._lastRemotePeerId !== void 0 && this._lastRemotePeerId !== remotePeerId;
3577
- this._lastRemotePeerId = remotePeerId;
3578
- if (isPeerChanged) {
3579
- this.inboundCalls.clear();
3580
- infoLog?.log(`'${this.ref}': Peer changed`);
3581
- this.peerChanged.trigger();
3582
- }
3583
- return isPeerChanged;
3618
+ const lastHandshake = this._lastHandshake;
3619
+ this._lastHandshake = handshake;
3620
+ if (lastHandshake === void 0)
3621
+ return 1 /* ChangedToVeryFirst */;
3622
+ if (handshake.RemotePeerId === lastHandshake.RemotePeerId)
3623
+ return 0 /* Unchanged */;
3624
+ this.inboundCalls.clear();
3625
+ infoLog?.log(`'${this.ref}': Peer changed`);
3626
+ this.peerChanged.trigger();
3627
+ return 2 /* Changed */;
3584
3628
  }
3585
3629
  _onHandshakeReceived(handshake) {
3586
3630
  this._pendingHandshake?.resolve(handshake);
@@ -4880,6 +4924,7 @@ var RpcPeerStateMonitor = class {
4880
4924
  RpcOutboundCall,
4881
4925
  RpcOutboundCallTracker,
4882
4926
  RpcPeer,
4927
+ RpcPeerChangeKind,
4883
4928
  RpcPeerStateKind,
4884
4929
  RpcPeerStateMonitor,
4885
4930
  RpcRefBuilder,