@actuallab/rpc 12.5.2 → 13.0.12

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.d.cts CHANGED
@@ -558,6 +558,48 @@ declare const IncreasingSeqCompressor: {
558
558
  readonly deserialize: (data: Uint8Array) => number[];
559
559
  };
560
560
 
561
+ /**
562
+ * Configurable timing limits for RPC connection lifecycle. Mirrors .NET's
563
+ * `RpcLimits` (src/ActualLab.Rpc/Configuration/RpcLimits.cs).
564
+ *
565
+ * Override paths, from broadest to narrowest:
566
+ * - **Process-wide**: mutate fields on {@link RpcLimits.Default} (affects
567
+ * every hub that hasn't been given its own instance — hubs default to
568
+ * using the shared `Default`, so mutations propagate live), or replace
569
+ * `RpcLimits.Default = new RpcLimits({ … })` (affects hubs constructed
570
+ * *after* the replacement).
571
+ * - **Per-hub**: assign `hub.limits = new RpcLimits({ … })` before peers
572
+ * are created from that hub.
573
+ * - **Per-peer**: set the matching `*Ms` field on the peer directly
574
+ * (e.g. `peer.keepAliveTimeoutMs = 60_000`) before `start()`.
575
+ *
576
+ * Peers snapshot the relevant values from `hub.limits` at construction —
577
+ * later mutations to `hub.limits` don't retroactively affect already-built
578
+ * peers; that's by design and matches the .NET semantics.
579
+ */
580
+ declare class RpcLimits {
581
+ /** Process-wide default. Hubs read from this when they're constructed
582
+ * unless caller assigns a custom instance to `hub.limits`. */
583
+ static Default: RpcLimits;
584
+ /** Max time to wait for the WebSocket to enter the OPEN state. On a
585
+ * hung connect (mobile after network change, half-open after sleep)
586
+ * the browser can take ~2 min to emit `onerror`/`onclose`; without
587
+ * this cap the reconnect loop blocks for that whole window. */
588
+ connectTimeoutMs: number;
589
+ /** Max time to wait for the server's handshake response after WS opens. */
590
+ handshakeTimeoutMs: number;
591
+ /** Outbound `$sys.KeepAlive` send period. */
592
+ keepAlivePeriodMs: number;
593
+ /** Reaper threshold — connection is force-closed (and the reconnect
594
+ * loop takes over) if no inbound `$sys.KeepAlive` has been seen for
595
+ * this long. Sized to tolerate a complete server stall plus most of
596
+ * one keepalive cycle: the worst-case age of `_lastKeepAliveAt` is
597
+ * `keepAlivePeriodMs + stall_duration`, so with these defaults the
598
+ * tolerated stall is ~15 s. */
599
+ keepAliveTimeoutMs: number;
600
+ constructor(overrides?: Partial<RpcLimits>);
601
+ }
602
+
561
603
  declare class RpcClientPeerReconnectDelayer extends RetryDelayer {
562
604
  constructor();
563
605
  }
@@ -663,6 +705,13 @@ declare class RpcHub {
663
705
  /** Shared reconnect delayer used by every client peer in this hub. Swap in
664
706
  * a custom subclass (e.g. app-level signal-gated) before peers start. */
665
707
  reconnectDelayer: RpcClientPeerReconnectDelayer;
708
+ /** Connection-lifecycle timing limits. Peers constructed against this hub
709
+ * read their initial `*Ms` field values from this instance; later
710
+ * mutations affect only peers built afterward (existing peers can be
711
+ * retuned by setting their `*Ms` fields directly). Defaults to the
712
+ * process-wide {@link RpcLimits.Default} so a single global override
713
+ * propagates to every hub that hasn't replaced this. */
714
+ limits: RpcLimits;
666
715
  /** URL used by {@link defaultPeer} to resolve / create the default client
667
716
  * peer. Must be set before the first {@link defaultPeer} access. */
668
717
  defaultPeerUrl: string | undefined;
@@ -746,8 +795,6 @@ declare class RpcSharedObjectTracker {
746
795
 
747
796
  /** WebSocket close code sent by the server when the client's serialization format is unsupported. */
748
797
  declare const RPC_CLOSE_CODE_UNSUPPORTED_FORMAT = 4001;
749
- /** Maximum time to wait for the server's handshake response after opening the socket. */
750
- declare const HANDSHAKE_TIMEOUT_MS = 10000;
751
798
  /** Builds the WebSocket connection URL for an RpcClientPeer. */
752
799
  type RpcConnectionUrlResolver = (peer: RpcClientPeer) => string | Promise<string>;
753
800
  /** Default connection URL provider — sets `clientId` and `f` query parameters,
@@ -802,9 +849,11 @@ declare abstract class RpcPeer {
802
849
  /** Wall-clock time of the last inbound `$sys.KeepAlive`, or 0 if none yet
803
850
  * for the current connection. Useful for diagnostics. */
804
851
  private _lastKeepAliveAt;
805
- /** Outbound keep-alive send period (per-peer override for tests). */
852
+ /** Outbound keep-alive send period. Initialized from `hub.limits` at
853
+ * construction; set directly to override on a single peer. */
806
854
  keepAlivePeriodMs: number;
807
- /** Inbound keep-alive silence tolerance (per-peer override for tests). */
855
+ /** Inbound keep-alive silence tolerance. Initialized from `hub.limits`
856
+ * at construction; set directly to override on a single peer. */
808
857
  keepAliveTimeoutMs: number;
809
858
  protected constructor(hub: RpcHub, ref: string);
810
859
  get connection(): RpcConnection | undefined;
@@ -886,8 +935,13 @@ declare class RpcClientPeer extends RpcPeer {
886
935
  * `Cookie` headers to the WS upgrade, which the browser `WebSocket`
887
936
  * constructor can't do). Must be set before `start()`. */
888
937
  webSocketFactory: ((url: string) => WebSocketLike) | undefined;
938
+ /** Max time to wait for the WebSocket to reach OPEN before aborting the
939
+ * connection attempt. Initialized from `hub.limits` at construction;
940
+ * set directly to override on a single peer. */
941
+ connectTimeoutMs: number;
889
942
  /** Max time to wait for the server's handshake response before aborting
890
- * the connection attempt. Mirrors .NET's `Hub.Limits.HandshakeTimeout`. */
943
+ * the connection attempt. Initialized from `hub.limits` at construction;
944
+ * set directly to override on a single peer. */
891
945
  handshakeTimeoutMs: number;
892
946
  readonly peerChanged: EventHandlerSet<void>;
893
947
  /** Fired when the server rejects the connection due to an unsupported serialization format (close code 4010). */
@@ -1342,4 +1396,4 @@ declare function xxh3_64str(input: string, seed?: bigint): bigint;
1342
1396
  */
1343
1397
  declare function computeMethodHash(methodName: string): number;
1344
1398
 
1345
- export { ARG_DELIMITER, ENVELOPE_DELIMITER, FRAME_DELIMITER, HANDSHAKE_TIMEOUT_MS, type IRpcObject, IncreasingSeqCompressor, type MethodMeta, RPC_CLOSE_CODE_UNSUPPORTED_FORMAT, type RemoteHandshake, type RpcCallOptions, RpcCallStage, RpcCallTypeId, RpcClientPeer, RpcClientPeerReconnectDelayer, type RpcConnection, RpcConnectionState, type RpcConnectionUrlResolver, type RpcDeserializedMessage, type RpcDispatchContext, RpcError, RpcHub, RpcInboundCall, RpcInboundCallTracker, RpcJsonSerializationFormat, type RpcLogScope, type RpcMessage, RpcMessageChannelConnection, RpcMessagePackCompactSerializationFormat, RpcMessagePackSerializationFormat, type RpcMethodDef, type RpcMethodDefInput, RpcMethodRegistry, type RpcObjectId, RpcObjectKind, RpcOutboundCall, RpcOutboundCallTracker, RpcPeer, type RpcPeerFactory, RpcPeerRefBuilder, type RpcPeerState, RpcPeerStateKind, RpcPeerStateMonitor, type RpcReceivedMessage, RpcRemoteExecutionMode, RpcRemoteObjectTracker, RpcSerializationFormat, RpcSerializationFormatResolver, RpcServerPeer, type RpcServiceDef, RpcServiceHost, type RpcServiceImpl, RpcSharedObjectTracker, RpcStream, type RpcStreamOptions, type RpcStreamRef, RpcStreamSender, type RpcStreamSource, RpcSystemCallHandler, RpcSystemCallSender, RpcSystemCalls, RpcType, RpcWebSocketConnection, type RpcWireData, type ServiceMeta, type WebSocketLike, type WebSocketServer, WebSocketState, computeMethodHash, createBinaryEncoder, createMessageChannelPair, createRpcClient, defaultBinaryDecoder, defaultBinaryEncoder, defaultConnectionUrlResolver, defineRpcService, deserializeBinaryMessage, deserializeCompactBinaryMessage, deserializeMessage, getLogs, getMethodsMeta, getServiceMeta, getStateDescription, isConnected, likelyConnected, parseStreamRef, resolveStreamRefs, rpcMethod, rpcService, serializeBinaryFrame, serializeBinaryMessage, serializeCompactBinaryMessage, serializeFrame, serializeMessage, splitBinaryFrame, splitCompactBinaryFrame, splitFrame, wireMethodName, xxh3_64, xxh3_64str };
1399
+ export { ARG_DELIMITER, ENVELOPE_DELIMITER, FRAME_DELIMITER, type IRpcObject, IncreasingSeqCompressor, type MethodMeta, RPC_CLOSE_CODE_UNSUPPORTED_FORMAT, type RemoteHandshake, type RpcCallOptions, RpcCallStage, RpcCallTypeId, RpcClientPeer, RpcClientPeerReconnectDelayer, type RpcConnection, RpcConnectionState, type RpcConnectionUrlResolver, type RpcDeserializedMessage, type RpcDispatchContext, RpcError, RpcHub, RpcInboundCall, RpcInboundCallTracker, RpcJsonSerializationFormat, RpcLimits, type RpcLogScope, type RpcMessage, RpcMessageChannelConnection, RpcMessagePackCompactSerializationFormat, RpcMessagePackSerializationFormat, type RpcMethodDef, type RpcMethodDefInput, RpcMethodRegistry, type RpcObjectId, RpcObjectKind, RpcOutboundCall, RpcOutboundCallTracker, RpcPeer, type RpcPeerFactory, RpcPeerRefBuilder, type RpcPeerState, RpcPeerStateKind, RpcPeerStateMonitor, type RpcReceivedMessage, RpcRemoteExecutionMode, RpcRemoteObjectTracker, RpcSerializationFormat, RpcSerializationFormatResolver, RpcServerPeer, type RpcServiceDef, RpcServiceHost, type RpcServiceImpl, RpcSharedObjectTracker, RpcStream, type RpcStreamOptions, type RpcStreamRef, RpcStreamSender, type RpcStreamSource, RpcSystemCallHandler, RpcSystemCallSender, RpcSystemCalls, RpcType, RpcWebSocketConnection, type RpcWireData, type ServiceMeta, type WebSocketLike, type WebSocketServer, WebSocketState, computeMethodHash, createBinaryEncoder, createMessageChannelPair, createRpcClient, defaultBinaryDecoder, defaultBinaryEncoder, defaultConnectionUrlResolver, defineRpcService, deserializeBinaryMessage, deserializeCompactBinaryMessage, deserializeMessage, getLogs, getMethodsMeta, getServiceMeta, getStateDescription, isConnected, likelyConnected, parseStreamRef, resolveStreamRefs, rpcMethod, rpcService, serializeBinaryFrame, serializeBinaryMessage, serializeCompactBinaryMessage, serializeFrame, serializeMessage, splitBinaryFrame, splitCompactBinaryFrame, splitFrame, wireMethodName, xxh3_64, xxh3_64str };
package/dist/index.d.ts CHANGED
@@ -558,6 +558,48 @@ declare const IncreasingSeqCompressor: {
558
558
  readonly deserialize: (data: Uint8Array) => number[];
559
559
  };
560
560
 
561
+ /**
562
+ * Configurable timing limits for RPC connection lifecycle. Mirrors .NET's
563
+ * `RpcLimits` (src/ActualLab.Rpc/Configuration/RpcLimits.cs).
564
+ *
565
+ * Override paths, from broadest to narrowest:
566
+ * - **Process-wide**: mutate fields on {@link RpcLimits.Default} (affects
567
+ * every hub that hasn't been given its own instance — hubs default to
568
+ * using the shared `Default`, so mutations propagate live), or replace
569
+ * `RpcLimits.Default = new RpcLimits({ … })` (affects hubs constructed
570
+ * *after* the replacement).
571
+ * - **Per-hub**: assign `hub.limits = new RpcLimits({ … })` before peers
572
+ * are created from that hub.
573
+ * - **Per-peer**: set the matching `*Ms` field on the peer directly
574
+ * (e.g. `peer.keepAliveTimeoutMs = 60_000`) before `start()`.
575
+ *
576
+ * Peers snapshot the relevant values from `hub.limits` at construction —
577
+ * later mutations to `hub.limits` don't retroactively affect already-built
578
+ * peers; that's by design and matches the .NET semantics.
579
+ */
580
+ declare class RpcLimits {
581
+ /** Process-wide default. Hubs read from this when they're constructed
582
+ * unless caller assigns a custom instance to `hub.limits`. */
583
+ static Default: RpcLimits;
584
+ /** Max time to wait for the WebSocket to enter the OPEN state. On a
585
+ * hung connect (mobile after network change, half-open after sleep)
586
+ * the browser can take ~2 min to emit `onerror`/`onclose`; without
587
+ * this cap the reconnect loop blocks for that whole window. */
588
+ connectTimeoutMs: number;
589
+ /** Max time to wait for the server's handshake response after WS opens. */
590
+ handshakeTimeoutMs: number;
591
+ /** Outbound `$sys.KeepAlive` send period. */
592
+ keepAlivePeriodMs: number;
593
+ /** Reaper threshold — connection is force-closed (and the reconnect
594
+ * loop takes over) if no inbound `$sys.KeepAlive` has been seen for
595
+ * this long. Sized to tolerate a complete server stall plus most of
596
+ * one keepalive cycle: the worst-case age of `_lastKeepAliveAt` is
597
+ * `keepAlivePeriodMs + stall_duration`, so with these defaults the
598
+ * tolerated stall is ~15 s. */
599
+ keepAliveTimeoutMs: number;
600
+ constructor(overrides?: Partial<RpcLimits>);
601
+ }
602
+
561
603
  declare class RpcClientPeerReconnectDelayer extends RetryDelayer {
562
604
  constructor();
563
605
  }
@@ -663,6 +705,13 @@ declare class RpcHub {
663
705
  /** Shared reconnect delayer used by every client peer in this hub. Swap in
664
706
  * a custom subclass (e.g. app-level signal-gated) before peers start. */
665
707
  reconnectDelayer: RpcClientPeerReconnectDelayer;
708
+ /** Connection-lifecycle timing limits. Peers constructed against this hub
709
+ * read their initial `*Ms` field values from this instance; later
710
+ * mutations affect only peers built afterward (existing peers can be
711
+ * retuned by setting their `*Ms` fields directly). Defaults to the
712
+ * process-wide {@link RpcLimits.Default} so a single global override
713
+ * propagates to every hub that hasn't replaced this. */
714
+ limits: RpcLimits;
666
715
  /** URL used by {@link defaultPeer} to resolve / create the default client
667
716
  * peer. Must be set before the first {@link defaultPeer} access. */
668
717
  defaultPeerUrl: string | undefined;
@@ -746,8 +795,6 @@ declare class RpcSharedObjectTracker {
746
795
 
747
796
  /** WebSocket close code sent by the server when the client's serialization format is unsupported. */
748
797
  declare const RPC_CLOSE_CODE_UNSUPPORTED_FORMAT = 4001;
749
- /** Maximum time to wait for the server's handshake response after opening the socket. */
750
- declare const HANDSHAKE_TIMEOUT_MS = 10000;
751
798
  /** Builds the WebSocket connection URL for an RpcClientPeer. */
752
799
  type RpcConnectionUrlResolver = (peer: RpcClientPeer) => string | Promise<string>;
753
800
  /** Default connection URL provider — sets `clientId` and `f` query parameters,
@@ -802,9 +849,11 @@ declare abstract class RpcPeer {
802
849
  /** Wall-clock time of the last inbound `$sys.KeepAlive`, or 0 if none yet
803
850
  * for the current connection. Useful for diagnostics. */
804
851
  private _lastKeepAliveAt;
805
- /** Outbound keep-alive send period (per-peer override for tests). */
852
+ /** Outbound keep-alive send period. Initialized from `hub.limits` at
853
+ * construction; set directly to override on a single peer. */
806
854
  keepAlivePeriodMs: number;
807
- /** Inbound keep-alive silence tolerance (per-peer override for tests). */
855
+ /** Inbound keep-alive silence tolerance. Initialized from `hub.limits`
856
+ * at construction; set directly to override on a single peer. */
808
857
  keepAliveTimeoutMs: number;
809
858
  protected constructor(hub: RpcHub, ref: string);
810
859
  get connection(): RpcConnection | undefined;
@@ -886,8 +935,13 @@ declare class RpcClientPeer extends RpcPeer {
886
935
  * `Cookie` headers to the WS upgrade, which the browser `WebSocket`
887
936
  * constructor can't do). Must be set before `start()`. */
888
937
  webSocketFactory: ((url: string) => WebSocketLike) | undefined;
938
+ /** Max time to wait for the WebSocket to reach OPEN before aborting the
939
+ * connection attempt. Initialized from `hub.limits` at construction;
940
+ * set directly to override on a single peer. */
941
+ connectTimeoutMs: number;
889
942
  /** Max time to wait for the server's handshake response before aborting
890
- * the connection attempt. Mirrors .NET's `Hub.Limits.HandshakeTimeout`. */
943
+ * the connection attempt. Initialized from `hub.limits` at construction;
944
+ * set directly to override on a single peer. */
891
945
  handshakeTimeoutMs: number;
892
946
  readonly peerChanged: EventHandlerSet<void>;
893
947
  /** Fired when the server rejects the connection due to an unsupported serialization format (close code 4010). */
@@ -1342,4 +1396,4 @@ declare function xxh3_64str(input: string, seed?: bigint): bigint;
1342
1396
  */
1343
1397
  declare function computeMethodHash(methodName: string): number;
1344
1398
 
1345
- export { ARG_DELIMITER, ENVELOPE_DELIMITER, FRAME_DELIMITER, HANDSHAKE_TIMEOUT_MS, type IRpcObject, IncreasingSeqCompressor, type MethodMeta, RPC_CLOSE_CODE_UNSUPPORTED_FORMAT, type RemoteHandshake, type RpcCallOptions, RpcCallStage, RpcCallTypeId, RpcClientPeer, RpcClientPeerReconnectDelayer, type RpcConnection, RpcConnectionState, type RpcConnectionUrlResolver, type RpcDeserializedMessage, type RpcDispatchContext, RpcError, RpcHub, RpcInboundCall, RpcInboundCallTracker, RpcJsonSerializationFormat, type RpcLogScope, type RpcMessage, RpcMessageChannelConnection, RpcMessagePackCompactSerializationFormat, RpcMessagePackSerializationFormat, type RpcMethodDef, type RpcMethodDefInput, RpcMethodRegistry, type RpcObjectId, RpcObjectKind, RpcOutboundCall, RpcOutboundCallTracker, RpcPeer, type RpcPeerFactory, RpcPeerRefBuilder, type RpcPeerState, RpcPeerStateKind, RpcPeerStateMonitor, type RpcReceivedMessage, RpcRemoteExecutionMode, RpcRemoteObjectTracker, RpcSerializationFormat, RpcSerializationFormatResolver, RpcServerPeer, type RpcServiceDef, RpcServiceHost, type RpcServiceImpl, RpcSharedObjectTracker, RpcStream, type RpcStreamOptions, type RpcStreamRef, RpcStreamSender, type RpcStreamSource, RpcSystemCallHandler, RpcSystemCallSender, RpcSystemCalls, RpcType, RpcWebSocketConnection, type RpcWireData, type ServiceMeta, type WebSocketLike, type WebSocketServer, WebSocketState, computeMethodHash, createBinaryEncoder, createMessageChannelPair, createRpcClient, defaultBinaryDecoder, defaultBinaryEncoder, defaultConnectionUrlResolver, defineRpcService, deserializeBinaryMessage, deserializeCompactBinaryMessage, deserializeMessage, getLogs, getMethodsMeta, getServiceMeta, getStateDescription, isConnected, likelyConnected, parseStreamRef, resolveStreamRefs, rpcMethod, rpcService, serializeBinaryFrame, serializeBinaryMessage, serializeCompactBinaryMessage, serializeFrame, serializeMessage, splitBinaryFrame, splitCompactBinaryFrame, splitFrame, wireMethodName, xxh3_64, xxh3_64str };
1399
+ export { ARG_DELIMITER, ENVELOPE_DELIMITER, FRAME_DELIMITER, type IRpcObject, IncreasingSeqCompressor, type MethodMeta, RPC_CLOSE_CODE_UNSUPPORTED_FORMAT, type RemoteHandshake, type RpcCallOptions, RpcCallStage, RpcCallTypeId, RpcClientPeer, RpcClientPeerReconnectDelayer, type RpcConnection, RpcConnectionState, type RpcConnectionUrlResolver, type RpcDeserializedMessage, type RpcDispatchContext, RpcError, RpcHub, RpcInboundCall, RpcInboundCallTracker, RpcJsonSerializationFormat, RpcLimits, type RpcLogScope, type RpcMessage, RpcMessageChannelConnection, RpcMessagePackCompactSerializationFormat, RpcMessagePackSerializationFormat, type RpcMethodDef, type RpcMethodDefInput, RpcMethodRegistry, type RpcObjectId, RpcObjectKind, RpcOutboundCall, RpcOutboundCallTracker, RpcPeer, type RpcPeerFactory, RpcPeerRefBuilder, type RpcPeerState, RpcPeerStateKind, RpcPeerStateMonitor, type RpcReceivedMessage, RpcRemoteExecutionMode, RpcRemoteObjectTracker, RpcSerializationFormat, RpcSerializationFormatResolver, RpcServerPeer, type RpcServiceDef, RpcServiceHost, type RpcServiceImpl, RpcSharedObjectTracker, RpcStream, type RpcStreamOptions, type RpcStreamRef, RpcStreamSender, type RpcStreamSource, RpcSystemCallHandler, RpcSystemCallSender, RpcSystemCalls, RpcType, RpcWebSocketConnection, type RpcWireData, type ServiceMeta, type WebSocketLike, type WebSocketServer, WebSocketState, computeMethodHash, createBinaryEncoder, createMessageChannelPair, createRpcClient, defaultBinaryDecoder, defaultBinaryEncoder, defaultConnectionUrlResolver, defineRpcService, deserializeBinaryMessage, deserializeCompactBinaryMessage, deserializeMessage, getLogs, getMethodsMeta, getServiceMeta, getStateDescription, isConnected, likelyConnected, parseStreamRef, resolveStreamRefs, rpcMethod, rpcService, serializeBinaryFrame, serializeBinaryMessage, serializeCompactBinaryMessage, serializeFrame, serializeMessage, splitBinaryFrame, splitCompactBinaryFrame, splitFrame, wireMethodName, xxh3_64, xxh3_64str };
package/dist/index.js CHANGED
@@ -2020,7 +2020,7 @@ var RpcSharedObjectTracker = class {
2020
2020
  };
2021
2021
 
2022
2022
  // src/rpc-peer.ts
2023
- import { EventHandlerSet as EventHandlerSet3, PromiseSource as PromiseSource5 } from "@actuallab/core";
2023
+ import { EventHandlerSet as EventHandlerSet3, PromiseSource as PromiseSource5, TimeoutError, withTimeout } from "@actuallab/core";
2024
2024
 
2025
2025
  // src/rpc-serialization-format.ts
2026
2026
  import { Decoder as Decoder3 } from "@msgpack/msgpack";
@@ -2184,9 +2184,6 @@ function yieldToEventLoop() {
2184
2184
  return new Promise((resolve) => setTimeout(resolve, 0));
2185
2185
  }
2186
2186
  var RPC_CLOSE_CODE_UNSUPPORTED_FORMAT = 4001;
2187
- var HANDSHAKE_TIMEOUT_MS = 1e4;
2188
- var KEEP_ALIVE_PERIOD_MS = 15e3;
2189
- var KEEP_ALIVE_TIMEOUT_MS = 55e3;
2190
2187
  var { debugLog: debugLog2, infoLog, warnLog: warnLog6, errorLog: errorLog2 } = getLogs("RpcPeer");
2191
2188
  var defaultConnectionUrlResolver = (peer) => {
2192
2189
  const formatKey = peer.serializationFormat.key;
@@ -2248,13 +2245,17 @@ var RpcPeer = class {
2248
2245
  /** Wall-clock time of the last inbound `$sys.KeepAlive`, or 0 if none yet
2249
2246
  * for the current connection. Useful for diagnostics. */
2250
2247
  _lastKeepAliveAt = 0;
2251
- /** Outbound keep-alive send period (per-peer override for tests). */
2252
- keepAlivePeriodMs = KEEP_ALIVE_PERIOD_MS;
2253
- /** Inbound keep-alive silence tolerance (per-peer override for tests). */
2254
- keepAliveTimeoutMs = KEEP_ALIVE_TIMEOUT_MS;
2248
+ /** Outbound keep-alive send period. Initialized from `hub.limits` at
2249
+ * construction; set directly to override on a single peer. */
2250
+ keepAlivePeriodMs;
2251
+ /** Inbound keep-alive silence tolerance. Initialized from `hub.limits`
2252
+ * at construction; set directly to override on a single peer. */
2253
+ keepAliveTimeoutMs;
2255
2254
  constructor(hub, ref) {
2256
2255
  this.hub = hub;
2257
2256
  this.ref = ref;
2257
+ this.keepAlivePeriodMs = hub.limits.keepAlivePeriodMs;
2258
+ this.keepAliveTimeoutMs = hub.limits.keepAliveTimeoutMs;
2258
2259
  }
2259
2260
  get connection() {
2260
2261
  return this._connection;
@@ -2573,9 +2574,14 @@ var RpcClientPeer = class extends RpcPeer {
2573
2574
  * `Cookie` headers to the WS upgrade, which the browser `WebSocket`
2574
2575
  * constructor can't do). Must be set before `start()`. */
2575
2576
  webSocketFactory;
2577
+ /** Max time to wait for the WebSocket to reach OPEN before aborting the
2578
+ * connection attempt. Initialized from `hub.limits` at construction;
2579
+ * set directly to override on a single peer. */
2580
+ connectTimeoutMs;
2576
2581
  /** Max time to wait for the server's handshake response before aborting
2577
- * the connection attempt. Mirrors .NET's `Hub.Limits.HandshakeTimeout`. */
2578
- handshakeTimeoutMs = HANDSHAKE_TIMEOUT_MS;
2582
+ * the connection attempt. Initialized from `hub.limits` at construction;
2583
+ * set directly to override on a single peer. */
2584
+ handshakeTimeoutMs;
2579
2585
  peerChanged = new EventHandlerSet3();
2580
2586
  /** Fired when the server rejects the connection due to an unsupported serialization format (close code 4010). */
2581
2587
  unsupportedFormat = new EventHandlerSet3();
@@ -2606,6 +2612,8 @@ var RpcClientPeer = class extends RpcPeer {
2606
2612
  */
2607
2613
  constructor(hub, url, mustStart = true) {
2608
2614
  super(hub, url);
2615
+ this.connectTimeoutMs = hub.limits.connectTimeoutMs;
2616
+ this.handshakeTimeoutMs = hub.limits.handshakeTimeoutMs;
2609
2617
  this.clientId = guidToBase64Url(this.id);
2610
2618
  const resolver = RpcSerializationFormatResolver.Default;
2611
2619
  this.serializationFormat = resolver.get(parseFormatFromUrl(url) ?? resolver.defaultFormatKey);
@@ -2682,7 +2690,20 @@ var RpcClientPeer = class extends RpcPeer {
2682
2690
  });
2683
2691
  this._pendingHandshake = new PromiseSource5();
2684
2692
  this.setupConnection(conn);
2685
- await Promise.race([conn.whenConnected, closedRejection]);
2693
+ const connectTimeoutMs = this.connectTimeoutMs;
2694
+ try {
2695
+ await withTimeout(
2696
+ Promise.race([conn.whenConnected, closedRejection]),
2697
+ connectTimeoutMs,
2698
+ "Connect timeout"
2699
+ );
2700
+ } catch (e) {
2701
+ if (e instanceof TimeoutError) {
2702
+ warnLog6?.log(`'${this.ref}': Connect timed out after ${connectTimeoutMs}ms`);
2703
+ conn.close();
2704
+ }
2705
+ throw e;
2706
+ }
2686
2707
  this._setConnectionState(2 /* Handshaking */);
2687
2708
  try {
2688
2709
  this.hub.systemCallSender.handshake(
@@ -2697,29 +2718,20 @@ var RpcClientPeer = class extends RpcPeer {
2697
2718
  throw e;
2698
2719
  }
2699
2720
  const handshakeTimeoutMs = this.handshakeTimeoutMs;
2700
- let handshakeTimer;
2701
- const handshakeTimeout = new Promise((_, reject) => {
2702
- handshakeTimer = setTimeout(() => {
2703
- reject(new Error("Handshake timeout"));
2704
- }, handshakeTimeoutMs);
2705
- });
2706
- handshakeTimeout.catch(() => {
2707
- });
2708
2721
  let remoteHandshake;
2709
2722
  try {
2710
- remoteHandshake = await Promise.race([
2711
- this._pendingHandshake.promise,
2712
- closedRejection,
2713
- handshakeTimeout
2714
- ]);
2723
+ remoteHandshake = await withTimeout(
2724
+ Promise.race([this._pendingHandshake.promise, closedRejection]),
2725
+ handshakeTimeoutMs,
2726
+ "Handshake timeout"
2727
+ );
2715
2728
  } catch (e) {
2716
- if (e instanceof Error && e.message === "Handshake timeout") {
2729
+ if (e instanceof TimeoutError) {
2717
2730
  warnLog6?.log(`'${this.ref}': Handshake timed out after ${handshakeTimeoutMs}ms`);
2718
2731
  conn.close();
2719
2732
  }
2720
2733
  throw e;
2721
2734
  } finally {
2722
- if (handshakeTimer !== void 0) clearTimeout(handshakeTimer);
2723
2735
  this._pendingHandshake = void 0;
2724
2736
  }
2725
2737
  this._remoteHandshakeIndex = remoteHandshake.Index ?? 0;
@@ -2951,6 +2963,32 @@ function _toBytes2(value) {
2951
2963
  return null;
2952
2964
  }
2953
2965
 
2966
+ // src/rpc-limits.ts
2967
+ var RpcLimits = class _RpcLimits {
2968
+ /** Process-wide default. Hubs read from this when they're constructed
2969
+ * unless caller assigns a custom instance to `hub.limits`. */
2970
+ static Default = new _RpcLimits();
2971
+ /** Max time to wait for the WebSocket to enter the OPEN state. On a
2972
+ * hung connect (mobile after network change, half-open after sleep)
2973
+ * the browser can take ~2 min to emit `onerror`/`onclose`; without
2974
+ * this cap the reconnect loop blocks for that whole window. */
2975
+ connectTimeoutMs = 1e4;
2976
+ /** Max time to wait for the server's handshake response after WS opens. */
2977
+ handshakeTimeoutMs = 1e4;
2978
+ /** Outbound `$sys.KeepAlive` send period. */
2979
+ keepAlivePeriodMs = 1e4;
2980
+ /** Reaper threshold — connection is force-closed (and the reconnect
2981
+ * loop takes over) if no inbound `$sys.KeepAlive` has been seen for
2982
+ * this long. Sized to tolerate a complete server stall plus most of
2983
+ * one keepalive cycle: the worst-case age of `_lastKeepAliveAt` is
2984
+ * `keepAlivePeriodMs + stall_duration`, so with these defaults the
2985
+ * tolerated stall is ~15 s. */
2986
+ keepAliveTimeoutMs = 25e3;
2987
+ constructor(overrides) {
2988
+ if (overrides) Object.assign(this, overrides);
2989
+ }
2990
+ };
2991
+
2954
2992
  // src/rpc-client-peer-reconnect-delayer.ts
2955
2993
  import { RetryDelaySeq, RetryDelayer } from "@actuallab/core";
2956
2994
  var RpcClientPeerReconnectDelayer = class extends RetryDelayer {
@@ -3565,6 +3603,13 @@ var RpcHub = class {
3565
3603
  /** Shared reconnect delayer used by every client peer in this hub. Swap in
3566
3604
  * a custom subclass (e.g. app-level signal-gated) before peers start. */
3567
3605
  reconnectDelayer = new RpcClientPeerReconnectDelayer();
3606
+ /** Connection-lifecycle timing limits. Peers constructed against this hub
3607
+ * read their initial `*Ms` field values from this instance; later
3608
+ * mutations affect only peers built afterward (existing peers can be
3609
+ * retuned by setting their `*Ms` fields directly). Defaults to the
3610
+ * process-wide {@link RpcLimits.Default} so a single global override
3611
+ * propagates to every hub that hasn't replaced this. */
3612
+ limits = RpcLimits.Default;
3568
3613
  /** URL used by {@link defaultPeer} to resolve / create the default client
3569
3614
  * peer. Must be set before the first {@link defaultPeer} access. */
3570
3615
  defaultPeerUrl;
@@ -4012,7 +4057,6 @@ export {
4012
4057
  ARG_DELIMITER,
4013
4058
  ENVELOPE_DELIMITER,
4014
4059
  FRAME_DELIMITER,
4015
- HANDSHAKE_TIMEOUT_MS,
4016
4060
  IncreasingSeqCompressor,
4017
4061
  RPC_CLOSE_CODE_UNSUPPORTED_FORMAT,
4018
4062
  RpcCallStage,
@@ -4025,6 +4069,7 @@ export {
4025
4069
  RpcInboundCall,
4026
4070
  RpcInboundCallTracker,
4027
4071
  RpcJsonSerializationFormat,
4072
+ RpcLimits,
4028
4073
  RpcMessageChannelConnection,
4029
4074
  RpcMessagePackCompactSerializationFormat,
4030
4075
  RpcMessagePackSerializationFormat,