@actuallab/rpc 12.3.14 → 12.3.31

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
@@ -1,5 +1,9 @@
1
- import { Encoder, Decoder } from '@msgpack/msgpack';
1
+ import * as _actuallab_core from '@actuallab/core';
2
2
  import { EventHandlerSet, PromiseSource, RetryDelayer } from '@actuallab/core';
3
+ import { Encoder, Decoder } from '@msgpack/msgpack';
4
+
5
+ type RpcLogScope = 'RpcCallTracker' | 'RpcClient' | 'RpcClientPeerReconnectDelayer' | 'RpcConnection' | 'RpcHub' | 'RpcInterceptor' | 'RpcMessageChannelConnection' | 'RpcMethodRegistry' | 'RpcOutboundCall' | 'RpcPeer' | 'RpcPeerStateMonitor' | 'RpcRemoteObjectTracker' | 'RpcSerialization' | 'RpcServiceHost' | 'RpcSharedObjectTracker' | 'RpcSharedStream' | 'RpcStream' | 'RpcStreamSender' | 'RpcSystemCallHandler' | 'RpcSystemCallSender' | 'RpcWebSocketClient';
6
+ declare const getLogs: (scope: RpcLogScope) => _actuallab_core.LogScopeFns;
3
7
 
4
8
  /** Identifies the type of an RPC call on the wire. */
5
9
  declare const enum RpcCallTypeId {
@@ -440,7 +444,26 @@ declare class RpcOutboundCall {
440
444
  readonly removeOnOk: boolean;
441
445
  /** The serialized wire data — stored for re-sending on reconnect. */
442
446
  serializedWireData: string | Uint8Array;
447
+ /**
448
+ * Bitfield of {@link RpcCallStage} flags indicating how far this call
449
+ * has progressed. Used by the `$sys.Reconnect` protocol to tell the
450
+ * remote peer which calls are still pending its attention.
451
+ *
452
+ * - 0: brand new, no result yet (the peer is expected to be actively processing).
453
+ * - `ResultReady` (1): `$sys.Ok` received for this call.
454
+ * - `Invalidated` (3): result was invalidated (compute calls only).
455
+ * - `Unregistered` (0x1000): removed from the tracker.
456
+ */
457
+ completedStage: number;
443
458
  constructor(callId: number, method: string, remoteExecutionMode?: number);
459
+ /**
460
+ * Returns the stage to report to the remote peer via `$sys.Reconnect`,
461
+ * or `null` if this call should not be reconciled and must be aborted.
462
+ *
463
+ * Mirrors .NET `RpcOutboundCall.GetReconnectStage` at
464
+ * src/ActualLab.Rpc/Infrastructure/RpcOutboundCall.cs.
465
+ */
466
+ getReconnectStage(isPeerChanged: boolean): number | null;
444
467
  /** Called when the connection is lost. Subclasses can override to resolve invalidation promises. */
445
468
  onDisconnect(): void;
446
469
  }
@@ -479,6 +502,55 @@ declare class RpcInboundCallTracker {
479
502
  clear(): void;
480
503
  }
481
504
 
505
+ /**
506
+ * Well-known RPC call completion stage constants, used by the
507
+ * `$sys.Reconnect` protocol to tell the peer which calls have already
508
+ * reached which stage and therefore don't need to be re-processed.
509
+ *
510
+ * Direct port of .NET `ActualLab.Rpc.Infrastructure.RpcCallStage` at
511
+ * src/ActualLab.Rpc/Infrastructure/RpcCallStage.cs.
512
+ */
513
+ declare const RpcCallStage: {
514
+ /** The call's result has been received from the peer. */
515
+ readonly ResultReady: 1;
516
+ /** The compute call's result has been invalidated (compute-call specific). */
517
+ readonly Invalidated: 3;
518
+ /**
519
+ * Bit flag added on top of ResultReady/Invalidated when the outbound
520
+ * call has been removed from the tracker (unregistered).
521
+ */
522
+ readonly Unregistered: 4096;
523
+ };
524
+ type RpcCallStage = number;
525
+
526
+ /**
527
+ * Compresses and decompresses monotonically increasing sequences of
528
+ * integers using variable-length LEB128 encoding of successive deltas.
529
+ *
530
+ * Wire-compatible with .NET `ActualLab.Rpc.Internal.IncreasingSeqCompressor`
531
+ * (src/ActualLab.Rpc/Internal/IncreasingSeqCompressor.cs) — which
532
+ * underlies the `$sys.Reconnect` protocol.
533
+ *
534
+ * Inputs MUST be sorted non-decreasing. Maximum supported value is
535
+ * 2^53 - 1 (JavaScript `Number` safe-integer limit); call IDs in TS
536
+ * never exceed this because they're generated from a 32-bit counter.
537
+ */
538
+ declare const IncreasingSeqCompressor: {
539
+ /**
540
+ * Serialize a non-decreasing sequence of non-negative integers as
541
+ * LEB128-encoded deltas.
542
+ *
543
+ * Throws `RangeError` if the input is not monotonically non-decreasing,
544
+ * or if a value exceeds `Number.MAX_SAFE_INTEGER`.
545
+ */
546
+ readonly serialize: (values: Iterable<number>) => Uint8Array;
547
+ /**
548
+ * Decode bytes produced by {@link serialize} back into the original
549
+ * sorted sequence of non-negative integers.
550
+ */
551
+ readonly deserialize: (data: Uint8Array) => number[];
552
+ };
553
+
482
554
  declare class RpcClientPeerReconnectDelayer extends RetryDelayer {
483
555
  constructor();
484
556
  }
@@ -686,8 +758,14 @@ declare abstract class RpcPeer {
686
758
  get connection(): RpcConnection | undefined;
687
759
  get isConnected(): boolean;
688
760
  protected setupConnection(conn: RpcConnection): void;
689
- /** The serialization format used by this peer. Override in subclasses. */
761
+ /**
762
+ * The serialization format used by this peer for outgoing messages.
763
+ * If not set explicitly, defaults to the resolver's default (JSON /
764
+ * json5np). Subclasses can pre-assign in their constructor.
765
+ */
766
+ private _format;
690
767
  get format(): RpcSerializationFormat;
768
+ set format(value: RpcSerializationFormat);
691
769
  call(method: string, args?: unknown[], options?: RpcCallOptions): RpcOutboundCall;
692
770
  callNoWait(method: string, args?: unknown[]): void;
693
771
  close(): void;
@@ -714,6 +792,12 @@ declare class RpcClientPeer extends RpcPeer {
714
792
  private _connectionKind;
715
793
  private _tryIndex;
716
794
  private _handshakeIndex;
795
+ /** Index from the MOST RECENTLY RECEIVED server handshake — sent back
796
+ * to the server in `$sys.Reconnect:3` so the server can verify we're
797
+ * talking about the current connection. Mirrors .NET's
798
+ * `connectionState.Handshake.Index` path in
799
+ * `RpcOutboundCallTracker.TryReconnect`. */
800
+ private _remoteHandshakeIndex;
717
801
  private _lastRemoteHubId;
718
802
  private _pendingHandshake;
719
803
  private _reconnectsAt;
@@ -736,22 +820,50 @@ declare class RpcClientPeer extends RpcPeer {
736
820
  readonly reconnectsAtChanged: EventHandlerSet<void>;
737
821
  get reconnectsAt(): number;
738
822
  private _setReconnectsAt;
739
- private _format;
740
823
  constructor(hub: RpcHub, url: string, serializationFormat?: string);
741
- get format(): RpcSerializationFormat;
742
824
  get connectionKind(): RpcPeerConnectionKind;
743
- /** One-shot connection for tests — no reconnection loop, no handshake. */
744
- connectWith(conn: RpcConnection): void;
825
+ /** One-shot connection for tests — no reconnection loop, no handshake.
826
+ * @param isPeerChanged Whether to treat this as a peer-identity change
827
+ * (default `true` — no handshake was exchanged, so stage-3 compute calls
828
+ * self-invalidate and remote/shared objects are disposed). Pass `false`
829
+ * to simulate a same-peer reconnect in tests. */
830
+ connectWith(conn: RpcConnection, isPeerChanged?: boolean): void;
745
831
  /** Start the reconnection loop — runs until disposed. */
746
832
  run(wsFactory?: (url: string) => WebSocketLike): Promise<void>;
747
- /** Re-send outbound calls after reconnection + flush pending sends.
748
- * Stage-3 compute calls are always self-invalidated: without $sys.Reconnect
749
- * protocol, the server's invalidation tracking is lost on disconnect, and
750
- * re-sending would get a duplicate $sys.Ok that is ignored (PromiseSource
751
- * already resolved). Self-invalidation forces a fresh recompute that
752
- * establishes new invalidation tracking on the new connection.
753
- * Regular in-flight calls are re-sent transparently. */
833
+ /**
834
+ * Reconcile outbound calls after reconnection, then flush pending sends.
835
+ *
836
+ * On peer change (different server hubId) or with binary wire format —
837
+ * we blind-resend every eligible call.
838
+ *
839
+ * On a same-peer reconnect with JSON transport, first call
840
+ * `$sys.Reconnect` to ask the server which call IDs it no longer
841
+ * recognizes (e.g. inbound tracker was cleared, or the call never
842
+ * reached the server). Only those get resent; the rest stay in the
843
+ * tracker awaiting their $sys.Ok. This prevents duplicate server-side
844
+ * invocation of in-flight streaming calls on every transient
845
+ * reconnect — mirrors .NET `RpcOutboundCallTracker.Reconnect` at
846
+ * src/ActualLab.Rpc/Infrastructure/RpcCallTrackers.cs:209-279.
847
+ *
848
+ * Stage-3 compute calls (result already resolved, awaiting invalidation)
849
+ * are always self-invalidated — TS doesn't track compute-call stages
850
+ * across reconnect, so the safest behavior is to force a fresh compute.
851
+ */
754
852
  private _reconnect;
853
+ /**
854
+ * Ask the server which call IDs in `eligible` it no longer recognizes
855
+ * via `$sys.Reconnect:3`. Returns the set of call IDs that need to be
856
+ * resent. On any failure, falls back to "resend everything".
857
+ *
858
+ * Wire format of the `completedStages` argument is chosen per wire format:
859
+ * - JSON (json5np): plain object `{"<stage>": "<base64-of-bytes>"}` —
860
+ * matches .NET `System.Text.Json`'s default for `Dictionary<int, byte[]>`.
861
+ * - MessagePack (msgpack6/msgpack6c): `Map<number, Uint8Array>` —
862
+ * the patched encoder (see `./msgpack-map-patch.ts`) writes this as a
863
+ * native msgpack `map<int, bin>`, matching .NET MessagePack-CSharp's
864
+ * default for `Dictionary<int, byte[]>`.
865
+ */
866
+ private _reconcileReconnect;
755
867
  protected _onHandshakeReceived(handshake: RemoteHandshake): void;
756
868
  close(): void;
757
869
  }
@@ -766,6 +878,18 @@ declare class RpcServerPeer extends RpcPeer {
766
878
  /** Handles incoming system call messages — class-based equivalent of the former standalone function. */
767
879
  declare class RpcSystemCallHandler {
768
880
  handle(message: RpcMessage, args: unknown[], peer: RpcPeer): void;
881
+ /**
882
+ * Server-side handler for the `$sys.Reconnect` call. Parses the peer's
883
+ * report of in-flight call IDs (grouped by stage, IncreasingSeqCompressor-
884
+ * encoded, base64-wrapped for JSON transport), computes the subset this
885
+ * peer does not recognize, and replies with a compressed byte[] of the
886
+ * unknown IDs wrapped in `$sys.Ok`.
887
+ *
888
+ * @param relatedId The outbound call ID the client used for this
889
+ * `$sys.Reconnect` invocation — we send `$sys.Ok` back
890
+ * with the same `RelatedId`.
891
+ */
892
+ private _handleReconnect;
769
893
  }
770
894
 
771
895
  /** Parsed stream reference from the server's result payload. */
@@ -810,6 +934,15 @@ declare function parseStreamRef(value: unknown): RpcStreamRef | null;
810
934
  * This mirrors the .NET RpcStream<T> design where the same type serves both roles.
811
935
  */
812
936
  declare class RpcStream<T> implements AsyncIterable<T>, IRpcObject {
937
+ /**
938
+ * Grace period (ms) given to a factory-form local source to honor its
939
+ * AbortSignal and exit naturally before the sender force-closes the
940
+ * iterator via `iterator.return()`.
941
+ *
942
+ * Only applies to sources provided as `(abortSignal) => AsyncIterable<T>`.
943
+ * Plain `AsyncIterable<T>` sources are force-closed immediately on disconnect.
944
+ */
945
+ static disconnectGracePeriodMs: number;
813
946
  readonly id: RpcObjectId;
814
947
  readonly kind: RpcObjectKind;
815
948
  readonly allowReconnect: boolean;
@@ -887,6 +1020,17 @@ declare function resolveStreamRefs(value: unknown, peer: RpcPeer): unknown;
887
1020
  * - Client acks every ackPeriod items for flow control
888
1021
  * - Server sends End to signal completion (with optional error)
889
1022
  * - Client sends AckEnd to acknowledge completion
1023
+ *
1024
+ * The pump is ACK-driven: the main loop blocks on `_whenAckReady` until a
1025
+ * client ACK is queued, processes it (updating `_lastAckedIndex`, rewinding
1026
+ * `_index` on mustReset, trimming the replay buffer), then sends items up
1027
+ * to `ack.nextIndex + ackAdvance`. While the peer is disconnected no ACKs
1028
+ * arrive, so the source is never pulled — the sender naturally pauses
1029
+ * instead of spinning.
1030
+ *
1031
+ * Direct port of .NET `RpcSharedStream<T>` at
1032
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs — including the
1033
+ * separate non-real-time / real-time pump variants.
890
1034
  */
891
1035
  declare class RpcStreamSender<T> implements IRpcObject {
892
1036
  private static readonly _emptyGuid;
@@ -898,43 +1042,83 @@ declare class RpcStreamSender<T> implements IRpcObject {
898
1042
  readonly peer: RpcPeer;
899
1043
  readonly ackPeriod: number;
900
1044
  readonly ackAdvance: number;
1045
+ /**
1046
+ * If true, the source was produced by a factory `(abortSignal) => AsyncIterable<T>`
1047
+ * and may honor the AbortSignal to exit gracefully. `disconnect()` first aborts
1048
+ * the signal and gives the source up to `RpcStream.disconnectGracePeriodMs` ms
1049
+ * before force-closing via `iterator.return()`.
1050
+ *
1051
+ * If false, the source is a plain AsyncIterable that cannot observe the signal,
1052
+ * so `disconnect()` force-closes immediately.
1053
+ */
1054
+ readonly sourceUsesAbortSignal: boolean;
1055
+ /** Next stream index that will be sent (matches .NET's `index`). */
901
1056
  private _nextIndex;
902
1057
  private _ended;
903
1058
  private _started;
1059
+ /** Highest `nextIndex` observed in an ACK so far (matches .NET's `lastAckNextIndex` idiom). */
904
1060
  private _lastAckedIndex;
905
- private _ackWaiting;
906
- private _resetRequested;
907
1061
  private _abortController;
908
1062
  private _iterator;
909
- constructor(peer: RpcPeer, ackPeriod?: number, ackAdvance?: number, allowReconnect?: boolean, isRealTime?: boolean, canSkipTo?: (item: T) => boolean);
1063
+ private _acks;
1064
+ private _whenAckReady;
1065
+ constructor(peer: RpcPeer, ackPeriod?: number, ackAdvance?: number, allowReconnect?: boolean, isRealTime?: boolean, canSkipTo?: (item: T) => boolean, sourceUsesAbortSignal?: boolean);
910
1066
  /** AbortSignal that is aborted when the sender is disconnected. */
911
1067
  get abortSignal(): AbortSignal;
912
1068
  /** Returns the stream reference string for the $sys.Ok response. */
913
1069
  toRef(): string;
914
- /** Called by system call handler when $sys.Ack is received from the client. */
1070
+ /**
1071
+ * Called by system call handler when $sys.Ack is received from the client.
1072
+ *
1073
+ * Mirrors .NET `RpcSharedStream<T>.OnAck` at
1074
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:68-101.
1075
+ */
915
1076
  onAck(nextIndex: number, hostId: string): void;
916
1077
  /** Called by system call handler when $sys.AckEnd is received from the client. */
917
1078
  onAckEnd(_hostId: string): void;
918
- /** Send a single item to the client. */
1079
+ /**
1080
+ * Send a single item to the client at the current `_nextIndex`.
1081
+ *
1082
+ * Advances `_nextIndex` unconditionally — even if the peer is disconnected
1083
+ * the item is considered "sent" from the pump's perspective so the replay
1084
+ * buffer indexing stays consistent. If a connection is absent, the wire
1085
+ * message is dropped; the item will be resent from the replay buffer
1086
+ * after a reconnect (client sends `Ack(N, mustReset=true)`).
1087
+ */
919
1088
  sendItem(item: T): void;
920
- /** Send a batch of items to the client. */
1089
+ /** Send a batch of items to the client. See {@link sendItem} for disconnect semantics. */
921
1090
  sendBatch(items: T[]): void;
922
1091
  /** Signal stream completion to the client. */
923
1092
  sendEnd(error?: Error | null): void;
924
1093
  /**
925
1094
  * Consume an AsyncIterable and send all items to the client.
926
- * Waits for the client's initial Ack before starting to pump items.
927
1095
  *
928
- * When `isRealTime` is true, applies flow control based on `ackAdvance`:
929
- * if the sender is `ackAdvance` items ahead of the last ACK, it enters
930
- * skip mode — draining the source and discarding items until it finds
931
- * one where `canSkipTo` returns true.
1096
+ * Dispatches to the non-real-time or real-time pump, matching .NET
1097
+ * `RpcSharedStream<T>.OnRun` at
1098
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:105-108.
932
1099
  */
933
1100
  writeFrom(source: AsyncIterable<T>): Promise<void>;
934
- /** Wait until _nextIndex < _lastAckedIndex + ackAdvance. */
935
- private _waitForAckBudget;
1101
+ /**
1102
+ * Non-real-time pump — back-pressure through buffered ACKs.
1103
+ *
1104
+ * Direct port of .NET `OnRunNonRealTime` at
1105
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:110-217.
1106
+ */
1107
+ private _runNonRealTime;
1108
+ /**
1109
+ * Real-time pump — drops stale items to stay close to the source's head.
1110
+ *
1111
+ * Direct port of .NET `OnRunRealTime` at
1112
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:219-420.
1113
+ */
1114
+ private _runRealTime;
1115
+ /** Drain all queued ACKs, returning the most recent one (or null if none). */
1116
+ private _tryProcessAcks;
1117
+ /** Wait for the ACK queue to have at least one entry (or `_ended`). */
1118
+ private _waitAckReady;
936
1119
  reconnect(): void;
937
1120
  disconnect(): void;
1121
+ private _forceCloseIterator;
938
1122
  }
939
1123
 
940
1124
  /** Creates a typed RPC client proxy — intercepts method calls and sends them over RPC. */
@@ -1002,4 +1186,4 @@ declare function xxh3_64str(input: string, seed?: bigint): bigint;
1002
1186
  */
1003
1187
  declare function computeMethodHash(methodName: string): number;
1004
1188
 
1005
- export { ARG_DELIMITER, ENVELOPE_DELIMITER, FRAME_DELIMITER, type IRpcObject, type MethodMeta, RPC_CLOSE_CODE_UNSUPPORTED_FORMAT, type RemoteHandshake, type RpcCallOptions, RpcCallTypeId, RpcClientPeer, RpcClientPeerReconnectDelayer, type RpcConnection, type RpcConnectionUrlResolver, type RpcDeserializedMessage, type RpcDispatchContext, RpcHub, RpcInboundCall, RpcInboundCallTracker, RpcJsonSerializationFormat, type RpcMessage, RpcMessageChannelConnection, RpcMessagePackCompactSerializationFormat, RpcMessagePackSerializationFormat, type RpcMethodDef, type RpcMethodDefInput, RpcMethodRegistry, type RpcObjectId, RpcObjectKind, RpcOutboundCall, RpcOutboundCallTracker, RpcPeer, RpcPeerConnectionKind, 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, getMethodsMeta, getServiceMeta, getStateDescription, isConnected, likelyConnected, parseStreamRef, resolveStreamRefs, rpcMethod, rpcService, serializeBinaryFrame, serializeBinaryMessage, serializeCompactBinaryMessage, serializeFrame, serializeMessage, splitBinaryFrame, splitCompactBinaryFrame, splitFrame, wireMethodName, xxh3_64, xxh3_64str };
1189
+ 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, type RpcConnectionUrlResolver, type RpcDeserializedMessage, type RpcDispatchContext, RpcHub, RpcInboundCall, RpcInboundCallTracker, RpcJsonSerializationFormat, type RpcLogScope, type RpcMessage, RpcMessageChannelConnection, RpcMessagePackCompactSerializationFormat, RpcMessagePackSerializationFormat, type RpcMethodDef, type RpcMethodDefInput, RpcMethodRegistry, type RpcObjectId, RpcObjectKind, RpcOutboundCall, RpcOutboundCallTracker, RpcPeer, RpcPeerConnectionKind, 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 };