@actuallab/rpc 12.4.1 → 12.4.7

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
@@ -990,6 +990,118 @@ declare class RpcSystemCallHandler {
990
990
  private _handleReconnect;
991
991
  }
992
992
 
993
+ /**
994
+ * Server-side RPC stream producer — sends items to a remote consumer via
995
+ * $sys.I / $sys.B / $sys.End system calls.
996
+ *
997
+ * Wire protocol (server perspective):
998
+ * - Server returns the stream reference string as $sys.Ok
999
+ * - Client sends Ack(0, hostId) to start the stream
1000
+ * - Server sends I (single item) / B (batch) messages
1001
+ * - Client acks every ackPeriod items for flow control
1002
+ * - Server sends End to signal completion (with optional error)
1003
+ * - Client sends AckEnd to acknowledge completion
1004
+ *
1005
+ * The pump is ACK-driven: the main loop blocks on `_whenAckReady` until a
1006
+ * client ACK is queued, processes it (updating `_lastAckedIndex`, rewinding
1007
+ * `_index` on mustReset, trimming the replay buffer), then sends items up
1008
+ * to `ack.nextIndex + bufferSize`. While the peer is disconnected no ACKs
1009
+ * arrive, so the source is never pulled — the sender naturally pauses
1010
+ * instead of spinning.
1011
+ *
1012
+ * Direct port of .NET `RpcSharedStream<T>` at
1013
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs.
1014
+ */
1015
+ declare class RpcStreamSender<T> implements IRpcObject {
1016
+ private static readonly _emptyGuid;
1017
+ readonly id: RpcObjectId;
1018
+ readonly kind: 0;
1019
+ readonly allowReconnect: boolean;
1020
+ readonly isRealTime: boolean;
1021
+ readonly canSkipTo: (item: T) => boolean;
1022
+ readonly peer: RpcPeer;
1023
+ readonly ackPeriod: number;
1024
+ readonly bufferSize: number;
1025
+ /**
1026
+ * If true, the source was produced by a factory `(abortSignal) => AsyncIterable<T>`
1027
+ * and may honor the AbortSignal to exit gracefully. `disconnect()` first aborts
1028
+ * the signal and gives the source up to `RpcStream.disconnectGracePeriodMs` ms
1029
+ * before force-closing via `iterator.return()`.
1030
+ *
1031
+ * If false, the source is a plain AsyncIterable that cannot observe the signal,
1032
+ * so `disconnect()` force-closes immediately.
1033
+ */
1034
+ readonly sourceUsesAbortSignal: boolean;
1035
+ /** Next stream index that will be sent (matches .NET's `index`). */
1036
+ private _nextIndex;
1037
+ private _ended;
1038
+ private _started;
1039
+ /** Highest `nextIndex` observed in an ACK so far (matches .NET's `lastAckNextIndex` idiom). */
1040
+ private _lastAckedIndex;
1041
+ private _abortController;
1042
+ private _iterator;
1043
+ /** Total items skipped via real-time `canSkipTo` compaction. */
1044
+ private _skipCount;
1045
+ /** Fires after a $sys.Ack has been processed (post-compaction). The
1046
+ * recorder's quality controller uses this to bump a "last ACK at"
1047
+ * watchdog so it can distinguish "stuck" (no ACK > N s) from
1048
+ * "throttled" (ACKs flowing). */
1049
+ onAckProcessed?: () => void;
1050
+ get nextIndex(): number;
1051
+ get lastAckIndex(): number;
1052
+ get skipCount(): number;
1053
+ private _acks;
1054
+ private _whenAckReady;
1055
+ constructor(peer: RpcPeer, ackPeriod?: number, bufferSize?: number, allowReconnect?: boolean, isRealTime?: boolean, canSkipTo?: (item: T) => boolean, sourceUsesAbortSignal?: boolean);
1056
+ /** AbortSignal that is aborted when the sender is disconnected. */
1057
+ get abortSignal(): AbortSignal;
1058
+ /** Returns the stream reference string for the $sys.Ok response. */
1059
+ toRef(): string;
1060
+ /**
1061
+ * Called by system call handler when $sys.Ack is received from the client.
1062
+ *
1063
+ * Mirrors .NET `RpcSharedStream<T>.OnAck` at
1064
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:68-101.
1065
+ */
1066
+ onAck(nextIndex: number, hostId: string): void;
1067
+ /** Called by system call handler when $sys.AckEnd is received from the client. */
1068
+ onAckEnd(_hostId: string): void;
1069
+ /**
1070
+ * Send a single item to the client at the current `_nextIndex`.
1071
+ *
1072
+ * Advances `_nextIndex` unconditionally — even if the peer is disconnected
1073
+ * the item is considered "sent" from the pump's perspective so the replay
1074
+ * buffer indexing stays consistent. If a connection is absent, the wire
1075
+ * message is dropped; the item will be resent from the replay buffer
1076
+ * after a reconnect (client sends `Ack(N, mustReset=true)`).
1077
+ */
1078
+ sendItem(item: T): void;
1079
+ /** Send a batch of items to the client. See {@link sendItem} for disconnect semantics. */
1080
+ sendBatch(items: T[]): void;
1081
+ /** Signal stream completion to the client. */
1082
+ sendEnd(error?: Error | null): void;
1083
+ /**
1084
+ * Consume an AsyncIterable and send all items to the client.
1085
+ *
1086
+ * Matches .NET `RpcSharedStream<T>.OnRun` at
1087
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs.
1088
+ */
1089
+ writeFrom(source: AsyncIterable<T>): Promise<void>;
1090
+ /**
1091
+ * ACK-driven pump. Non-real-time streams apply normal backpressure at
1092
+ * `bufferSize`; real-time streams may compact the already-buffered unsent
1093
+ * suffix to the latest buffered `canSkipTo` item after the consumer ACKs.
1094
+ */
1095
+ private _run;
1096
+ /** Drain all queued ACKs, returning the most recent one (or null if none). */
1097
+ private _tryProcessAcks;
1098
+ /** Wait for the ACK queue to have at least one entry (or `_ended`). */
1099
+ private _waitAckReady;
1100
+ reconnect(): void;
1101
+ disconnect(): void;
1102
+ private _forceCloseIterator;
1103
+ }
1104
+
993
1105
  /** Parsed stream reference from the server's result payload. */
994
1106
  interface RpcStreamRef {
995
1107
  readonly hostId: string;
@@ -1072,6 +1184,9 @@ declare class RpcStream<T> implements AsyncIterable<T>, IRpcObject {
1072
1184
  * Rejects if the source throws. Throws if `toRef()` hasn't been called yet.
1073
1185
  */
1074
1186
  get whenSent(): Promise<void>;
1187
+ /** Local sender backing this stream after `toRef()` has been called.
1188
+ * `undefined` for remote streams or before `toRef()`. */
1189
+ get sender(): RpcStreamSender<T> | undefined;
1075
1190
  /**
1076
1191
  * For local streams: bind to a peer, create and register an `RpcStreamSender`,
1077
1192
  * start pumping items from the local source in the background, and return the
@@ -1108,108 +1223,6 @@ declare class RpcStream<T> implements AsyncIterable<T>, IRpcObject {
1108
1223
  */
1109
1224
  declare function resolveStreamRefs(value: unknown, peer: RpcPeer): unknown;
1110
1225
 
1111
- /**
1112
- * Server-side RPC stream producer — sends items to a remote consumer via
1113
- * $sys.I / $sys.B / $sys.End system calls.
1114
- *
1115
- * Wire protocol (server perspective):
1116
- * - Server returns the stream reference string as $sys.Ok
1117
- * - Client sends Ack(0, hostId) to start the stream
1118
- * - Server sends I (single item) / B (batch) messages
1119
- * - Client acks every ackPeriod items for flow control
1120
- * - Server sends End to signal completion (with optional error)
1121
- * - Client sends AckEnd to acknowledge completion
1122
- *
1123
- * The pump is ACK-driven: the main loop blocks on `_whenAckReady` until a
1124
- * client ACK is queued, processes it (updating `_lastAckedIndex`, rewinding
1125
- * `_index` on mustReset, trimming the replay buffer), then sends items up
1126
- * to `ack.nextIndex + bufferSize`. While the peer is disconnected no ACKs
1127
- * arrive, so the source is never pulled — the sender naturally pauses
1128
- * instead of spinning.
1129
- *
1130
- * Direct port of .NET `RpcSharedStream<T>` at
1131
- * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs.
1132
- */
1133
- declare class RpcStreamSender<T> implements IRpcObject {
1134
- private static readonly _emptyGuid;
1135
- readonly id: RpcObjectId;
1136
- readonly kind: 0;
1137
- readonly allowReconnect: boolean;
1138
- readonly isRealTime: boolean;
1139
- readonly canSkipTo: (item: T) => boolean;
1140
- readonly peer: RpcPeer;
1141
- readonly ackPeriod: number;
1142
- readonly bufferSize: number;
1143
- /**
1144
- * If true, the source was produced by a factory `(abortSignal) => AsyncIterable<T>`
1145
- * and may honor the AbortSignal to exit gracefully. `disconnect()` first aborts
1146
- * the signal and gives the source up to `RpcStream.disconnectGracePeriodMs` ms
1147
- * before force-closing via `iterator.return()`.
1148
- *
1149
- * If false, the source is a plain AsyncIterable that cannot observe the signal,
1150
- * so `disconnect()` force-closes immediately.
1151
- */
1152
- readonly sourceUsesAbortSignal: boolean;
1153
- /** Next stream index that will be sent (matches .NET's `index`). */
1154
- private _nextIndex;
1155
- private _ended;
1156
- private _started;
1157
- /** Highest `nextIndex` observed in an ACK so far (matches .NET's `lastAckNextIndex` idiom). */
1158
- private _lastAckedIndex;
1159
- private _abortController;
1160
- private _iterator;
1161
- private _acks;
1162
- private _whenAckReady;
1163
- constructor(peer: RpcPeer, ackPeriod?: number, bufferSize?: number, allowReconnect?: boolean, isRealTime?: boolean, canSkipTo?: (item: T) => boolean, sourceUsesAbortSignal?: boolean);
1164
- /** AbortSignal that is aborted when the sender is disconnected. */
1165
- get abortSignal(): AbortSignal;
1166
- /** Returns the stream reference string for the $sys.Ok response. */
1167
- toRef(): string;
1168
- /**
1169
- * Called by system call handler when $sys.Ack is received from the client.
1170
- *
1171
- * Mirrors .NET `RpcSharedStream<T>.OnAck` at
1172
- * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:68-101.
1173
- */
1174
- onAck(nextIndex: number, hostId: string): void;
1175
- /** Called by system call handler when $sys.AckEnd is received from the client. */
1176
- onAckEnd(_hostId: string): void;
1177
- /**
1178
- * Send a single item to the client at the current `_nextIndex`.
1179
- *
1180
- * Advances `_nextIndex` unconditionally — even if the peer is disconnected
1181
- * the item is considered "sent" from the pump's perspective so the replay
1182
- * buffer indexing stays consistent. If a connection is absent, the wire
1183
- * message is dropped; the item will be resent from the replay buffer
1184
- * after a reconnect (client sends `Ack(N, mustReset=true)`).
1185
- */
1186
- sendItem(item: T): void;
1187
- /** Send a batch of items to the client. See {@link sendItem} for disconnect semantics. */
1188
- sendBatch(items: T[]): void;
1189
- /** Signal stream completion to the client. */
1190
- sendEnd(error?: Error | null): void;
1191
- /**
1192
- * Consume an AsyncIterable and send all items to the client.
1193
- *
1194
- * Matches .NET `RpcSharedStream<T>.OnRun` at
1195
- * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs.
1196
- */
1197
- writeFrom(source: AsyncIterable<T>): Promise<void>;
1198
- /**
1199
- * ACK-driven pump. Non-real-time streams apply normal backpressure at
1200
- * `bufferSize`; real-time streams may compact the already-buffered unsent
1201
- * suffix to the latest buffered `canSkipTo` item after the consumer ACKs.
1202
- */
1203
- private _run;
1204
- /** Drain all queued ACKs, returning the most recent one (or null if none). */
1205
- private _tryProcessAcks;
1206
- /** Wait for the ACK queue to have at least one entry (or `_ended`). */
1207
- private _waitAckReady;
1208
- reconnect(): void;
1209
- disconnect(): void;
1210
- private _forceCloseIterator;
1211
- }
1212
-
1213
1226
  declare class RpcPeerRefBuilder {
1214
1227
  /** Build a client peer ref from a WebSocket URL, optionally baking in the
1215
1228
  * serialization format as a `f=` query parameter.
package/dist/index.d.ts CHANGED
@@ -990,6 +990,118 @@ declare class RpcSystemCallHandler {
990
990
  private _handleReconnect;
991
991
  }
992
992
 
993
+ /**
994
+ * Server-side RPC stream producer — sends items to a remote consumer via
995
+ * $sys.I / $sys.B / $sys.End system calls.
996
+ *
997
+ * Wire protocol (server perspective):
998
+ * - Server returns the stream reference string as $sys.Ok
999
+ * - Client sends Ack(0, hostId) to start the stream
1000
+ * - Server sends I (single item) / B (batch) messages
1001
+ * - Client acks every ackPeriod items for flow control
1002
+ * - Server sends End to signal completion (with optional error)
1003
+ * - Client sends AckEnd to acknowledge completion
1004
+ *
1005
+ * The pump is ACK-driven: the main loop blocks on `_whenAckReady` until a
1006
+ * client ACK is queued, processes it (updating `_lastAckedIndex`, rewinding
1007
+ * `_index` on mustReset, trimming the replay buffer), then sends items up
1008
+ * to `ack.nextIndex + bufferSize`. While the peer is disconnected no ACKs
1009
+ * arrive, so the source is never pulled — the sender naturally pauses
1010
+ * instead of spinning.
1011
+ *
1012
+ * Direct port of .NET `RpcSharedStream<T>` at
1013
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs.
1014
+ */
1015
+ declare class RpcStreamSender<T> implements IRpcObject {
1016
+ private static readonly _emptyGuid;
1017
+ readonly id: RpcObjectId;
1018
+ readonly kind: 0;
1019
+ readonly allowReconnect: boolean;
1020
+ readonly isRealTime: boolean;
1021
+ readonly canSkipTo: (item: T) => boolean;
1022
+ readonly peer: RpcPeer;
1023
+ readonly ackPeriod: number;
1024
+ readonly bufferSize: number;
1025
+ /**
1026
+ * If true, the source was produced by a factory `(abortSignal) => AsyncIterable<T>`
1027
+ * and may honor the AbortSignal to exit gracefully. `disconnect()` first aborts
1028
+ * the signal and gives the source up to `RpcStream.disconnectGracePeriodMs` ms
1029
+ * before force-closing via `iterator.return()`.
1030
+ *
1031
+ * If false, the source is a plain AsyncIterable that cannot observe the signal,
1032
+ * so `disconnect()` force-closes immediately.
1033
+ */
1034
+ readonly sourceUsesAbortSignal: boolean;
1035
+ /** Next stream index that will be sent (matches .NET's `index`). */
1036
+ private _nextIndex;
1037
+ private _ended;
1038
+ private _started;
1039
+ /** Highest `nextIndex` observed in an ACK so far (matches .NET's `lastAckNextIndex` idiom). */
1040
+ private _lastAckedIndex;
1041
+ private _abortController;
1042
+ private _iterator;
1043
+ /** Total items skipped via real-time `canSkipTo` compaction. */
1044
+ private _skipCount;
1045
+ /** Fires after a $sys.Ack has been processed (post-compaction). The
1046
+ * recorder's quality controller uses this to bump a "last ACK at"
1047
+ * watchdog so it can distinguish "stuck" (no ACK > N s) from
1048
+ * "throttled" (ACKs flowing). */
1049
+ onAckProcessed?: () => void;
1050
+ get nextIndex(): number;
1051
+ get lastAckIndex(): number;
1052
+ get skipCount(): number;
1053
+ private _acks;
1054
+ private _whenAckReady;
1055
+ constructor(peer: RpcPeer, ackPeriod?: number, bufferSize?: number, allowReconnect?: boolean, isRealTime?: boolean, canSkipTo?: (item: T) => boolean, sourceUsesAbortSignal?: boolean);
1056
+ /** AbortSignal that is aborted when the sender is disconnected. */
1057
+ get abortSignal(): AbortSignal;
1058
+ /** Returns the stream reference string for the $sys.Ok response. */
1059
+ toRef(): string;
1060
+ /**
1061
+ * Called by system call handler when $sys.Ack is received from the client.
1062
+ *
1063
+ * Mirrors .NET `RpcSharedStream<T>.OnAck` at
1064
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:68-101.
1065
+ */
1066
+ onAck(nextIndex: number, hostId: string): void;
1067
+ /** Called by system call handler when $sys.AckEnd is received from the client. */
1068
+ onAckEnd(_hostId: string): void;
1069
+ /**
1070
+ * Send a single item to the client at the current `_nextIndex`.
1071
+ *
1072
+ * Advances `_nextIndex` unconditionally — even if the peer is disconnected
1073
+ * the item is considered "sent" from the pump's perspective so the replay
1074
+ * buffer indexing stays consistent. If a connection is absent, the wire
1075
+ * message is dropped; the item will be resent from the replay buffer
1076
+ * after a reconnect (client sends `Ack(N, mustReset=true)`).
1077
+ */
1078
+ sendItem(item: T): void;
1079
+ /** Send a batch of items to the client. See {@link sendItem} for disconnect semantics. */
1080
+ sendBatch(items: T[]): void;
1081
+ /** Signal stream completion to the client. */
1082
+ sendEnd(error?: Error | null): void;
1083
+ /**
1084
+ * Consume an AsyncIterable and send all items to the client.
1085
+ *
1086
+ * Matches .NET `RpcSharedStream<T>.OnRun` at
1087
+ * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs.
1088
+ */
1089
+ writeFrom(source: AsyncIterable<T>): Promise<void>;
1090
+ /**
1091
+ * ACK-driven pump. Non-real-time streams apply normal backpressure at
1092
+ * `bufferSize`; real-time streams may compact the already-buffered unsent
1093
+ * suffix to the latest buffered `canSkipTo` item after the consumer ACKs.
1094
+ */
1095
+ private _run;
1096
+ /** Drain all queued ACKs, returning the most recent one (or null if none). */
1097
+ private _tryProcessAcks;
1098
+ /** Wait for the ACK queue to have at least one entry (or `_ended`). */
1099
+ private _waitAckReady;
1100
+ reconnect(): void;
1101
+ disconnect(): void;
1102
+ private _forceCloseIterator;
1103
+ }
1104
+
993
1105
  /** Parsed stream reference from the server's result payload. */
994
1106
  interface RpcStreamRef {
995
1107
  readonly hostId: string;
@@ -1072,6 +1184,9 @@ declare class RpcStream<T> implements AsyncIterable<T>, IRpcObject {
1072
1184
  * Rejects if the source throws. Throws if `toRef()` hasn't been called yet.
1073
1185
  */
1074
1186
  get whenSent(): Promise<void>;
1187
+ /** Local sender backing this stream after `toRef()` has been called.
1188
+ * `undefined` for remote streams or before `toRef()`. */
1189
+ get sender(): RpcStreamSender<T> | undefined;
1075
1190
  /**
1076
1191
  * For local streams: bind to a peer, create and register an `RpcStreamSender`,
1077
1192
  * start pumping items from the local source in the background, and return the
@@ -1108,108 +1223,6 @@ declare class RpcStream<T> implements AsyncIterable<T>, IRpcObject {
1108
1223
  */
1109
1224
  declare function resolveStreamRefs(value: unknown, peer: RpcPeer): unknown;
1110
1225
 
1111
- /**
1112
- * Server-side RPC stream producer — sends items to a remote consumer via
1113
- * $sys.I / $sys.B / $sys.End system calls.
1114
- *
1115
- * Wire protocol (server perspective):
1116
- * - Server returns the stream reference string as $sys.Ok
1117
- * - Client sends Ack(0, hostId) to start the stream
1118
- * - Server sends I (single item) / B (batch) messages
1119
- * - Client acks every ackPeriod items for flow control
1120
- * - Server sends End to signal completion (with optional error)
1121
- * - Client sends AckEnd to acknowledge completion
1122
- *
1123
- * The pump is ACK-driven: the main loop blocks on `_whenAckReady` until a
1124
- * client ACK is queued, processes it (updating `_lastAckedIndex`, rewinding
1125
- * `_index` on mustReset, trimming the replay buffer), then sends items up
1126
- * to `ack.nextIndex + bufferSize`. While the peer is disconnected no ACKs
1127
- * arrive, so the source is never pulled — the sender naturally pauses
1128
- * instead of spinning.
1129
- *
1130
- * Direct port of .NET `RpcSharedStream<T>` at
1131
- * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs.
1132
- */
1133
- declare class RpcStreamSender<T> implements IRpcObject {
1134
- private static readonly _emptyGuid;
1135
- readonly id: RpcObjectId;
1136
- readonly kind: 0;
1137
- readonly allowReconnect: boolean;
1138
- readonly isRealTime: boolean;
1139
- readonly canSkipTo: (item: T) => boolean;
1140
- readonly peer: RpcPeer;
1141
- readonly ackPeriod: number;
1142
- readonly bufferSize: number;
1143
- /**
1144
- * If true, the source was produced by a factory `(abortSignal) => AsyncIterable<T>`
1145
- * and may honor the AbortSignal to exit gracefully. `disconnect()` first aborts
1146
- * the signal and gives the source up to `RpcStream.disconnectGracePeriodMs` ms
1147
- * before force-closing via `iterator.return()`.
1148
- *
1149
- * If false, the source is a plain AsyncIterable that cannot observe the signal,
1150
- * so `disconnect()` force-closes immediately.
1151
- */
1152
- readonly sourceUsesAbortSignal: boolean;
1153
- /** Next stream index that will be sent (matches .NET's `index`). */
1154
- private _nextIndex;
1155
- private _ended;
1156
- private _started;
1157
- /** Highest `nextIndex` observed in an ACK so far (matches .NET's `lastAckNextIndex` idiom). */
1158
- private _lastAckedIndex;
1159
- private _abortController;
1160
- private _iterator;
1161
- private _acks;
1162
- private _whenAckReady;
1163
- constructor(peer: RpcPeer, ackPeriod?: number, bufferSize?: number, allowReconnect?: boolean, isRealTime?: boolean, canSkipTo?: (item: T) => boolean, sourceUsesAbortSignal?: boolean);
1164
- /** AbortSignal that is aborted when the sender is disconnected. */
1165
- get abortSignal(): AbortSignal;
1166
- /** Returns the stream reference string for the $sys.Ok response. */
1167
- toRef(): string;
1168
- /**
1169
- * Called by system call handler when $sys.Ack is received from the client.
1170
- *
1171
- * Mirrors .NET `RpcSharedStream<T>.OnAck` at
1172
- * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs:68-101.
1173
- */
1174
- onAck(nextIndex: number, hostId: string): void;
1175
- /** Called by system call handler when $sys.AckEnd is received from the client. */
1176
- onAckEnd(_hostId: string): void;
1177
- /**
1178
- * Send a single item to the client at the current `_nextIndex`.
1179
- *
1180
- * Advances `_nextIndex` unconditionally — even if the peer is disconnected
1181
- * the item is considered "sent" from the pump's perspective so the replay
1182
- * buffer indexing stays consistent. If a connection is absent, the wire
1183
- * message is dropped; the item will be resent from the replay buffer
1184
- * after a reconnect (client sends `Ack(N, mustReset=true)`).
1185
- */
1186
- sendItem(item: T): void;
1187
- /** Send a batch of items to the client. See {@link sendItem} for disconnect semantics. */
1188
- sendBatch(items: T[]): void;
1189
- /** Signal stream completion to the client. */
1190
- sendEnd(error?: Error | null): void;
1191
- /**
1192
- * Consume an AsyncIterable and send all items to the client.
1193
- *
1194
- * Matches .NET `RpcSharedStream<T>.OnRun` at
1195
- * src/ActualLab.Rpc/Infrastructure/RpcSharedStream.cs.
1196
- */
1197
- writeFrom(source: AsyncIterable<T>): Promise<void>;
1198
- /**
1199
- * ACK-driven pump. Non-real-time streams apply normal backpressure at
1200
- * `bufferSize`; real-time streams may compact the already-buffered unsent
1201
- * suffix to the latest buffered `canSkipTo` item after the consumer ACKs.
1202
- */
1203
- private _run;
1204
- /** Drain all queued ACKs, returning the most recent one (or null if none). */
1205
- private _tryProcessAcks;
1206
- /** Wait for the ACK queue to have at least one entry (or `_ended`). */
1207
- private _waitAckReady;
1208
- reconnect(): void;
1209
- disconnect(): void;
1210
- private _forceCloseIterator;
1211
- }
1212
-
1213
1226
  declare class RpcPeerRefBuilder {
1214
1227
  /** Build a client peer ref from a WebSocket URL, optionally baking in the
1215
1228
  * serialization format as a `f=` query parameter.
package/dist/index.js CHANGED
@@ -900,6 +900,23 @@ var RpcStreamSender = class _RpcStreamSender {
900
900
  _lastAckedIndex = 0;
901
901
  _abortController = new AbortController();
902
902
  _iterator = null;
903
+ // -- Recorder-controller metrics (Step 9.1) --
904
+ /** Total items skipped via real-time `canSkipTo` compaction. */
905
+ _skipCount = 0;
906
+ /** Fires after a $sys.Ack has been processed (post-compaction). The
907
+ * recorder's quality controller uses this to bump a "last ACK at"
908
+ * watchdog so it can distinguish "stuck" (no ACK > N s) from
909
+ * "throttled" (ACKs flowing). */
910
+ onAckProcessed;
911
+ get nextIndex() {
912
+ return this._nextIndex;
913
+ }
914
+ get lastAckIndex() {
915
+ return this._lastAckedIndex;
916
+ }
917
+ get skipCount() {
918
+ return this._skipCount;
919
+ }
903
920
  // -- ACK queue (analog of .NET's Channel<(long, bool)>) --
904
921
  _acks = [];
905
922
  _whenAckReady = null;
@@ -1134,8 +1151,16 @@ var RpcStreamSender = class _RpcStreamSender {
1134
1151
  if (bufferIndex >= buffer.count && !isFullyBuffered && buffer.hasRemainingCapacity && this._acks.length === 0) {
1135
1152
  await bufferNext(false, true);
1136
1153
  }
1137
- if (ack.nextIndex > 0)
1138
- bufferIndex = _compactBufferedUnsentSuffix(buffer, bufferIndex, this.canSkipTo);
1154
+ if (ack.nextIndex > 0) {
1155
+ const result = _compactBufferedUnsentSuffix(
1156
+ buffer,
1157
+ bufferIndex,
1158
+ this.canSkipTo
1159
+ );
1160
+ bufferIndex = result.newIndex;
1161
+ if (result.skipped > 0)
1162
+ this._skipCount += result.skipped;
1163
+ }
1139
1164
  if (this._acks.length > 0) break;
1140
1165
  }
1141
1166
  while (this._nextIndex < maxIndex && bufferIndex < buffer.count) {
@@ -1171,6 +1196,10 @@ var RpcStreamSender = class _RpcStreamSender {
1171
1196
  this._nextIndex = a.nextIndex;
1172
1197
  }
1173
1198
  }
1199
+ try {
1200
+ this.onAckProcessed?.();
1201
+ } catch {
1202
+ }
1174
1203
  return last;
1175
1204
  }
1176
1205
  /** Wait for the ACK queue to have at least one entry (or `_ended`). */
@@ -1230,14 +1259,16 @@ function _compactBufferedUnsentSuffix(buffer, firstUnsentIndex, canSkipTo) {
1230
1259
  skipToIndex = i;
1231
1260
  }
1232
1261
  if (skipToIndex <= firstUnsentIndex)
1233
- return firstUnsentIndex;
1262
+ return { newIndex: firstUnsentIndex, skipped: 0 };
1263
+ const cutFrom = firstUnsentIndex;
1264
+ const cutTo = skipToIndex;
1234
1265
  const items = buffer.toArray();
1235
1266
  buffer.clear();
1236
- for (let i = 0; i < firstUnsentIndex; i++)
1267
+ for (let i = 0; i < cutFrom; i++)
1237
1268
  buffer.pushTail(items[i]);
1238
- for (let i = skipToIndex; i < items.length; i++)
1269
+ for (let i = cutTo; i < items.length; i++)
1239
1270
  buffer.pushTail(items[i]);
1240
- return firstUnsentIndex;
1271
+ return { newIndex: firstUnsentIndex, skipped: skipToIndex - firstUnsentIndex };
1241
1272
  }
1242
1273
 
1243
1274
  // src/rpc-stream.ts
@@ -1343,6 +1374,11 @@ var RpcStream = class _RpcStream {
1343
1374
  throw new Error("toRef() must be called before awaiting whenSent.");
1344
1375
  return this._whenSent;
1345
1376
  }
1377
+ /** Local sender backing this stream after `toRef()` has been called.
1378
+ * `undefined` for remote streams or before `toRef()`. */
1379
+ get sender() {
1380
+ return this._sender;
1381
+ }
1346
1382
  /**
1347
1383
  * For local streams: bind to a peer, create and register an `RpcStreamSender`,
1348
1384
  * start pumping items from the local source in the background, and return the