@push.rocks/smartrust 2.3.0 → 2.5.0

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.
@@ -1,18 +1,23 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import { RustBinaryLocator } from './classes.rustbinarylocator.js';
3
3
  import { RustBinaryLocatorError } from './classes.rustbinarylocatorerror.js';
4
- import { RustBridgeRequestError } from './classes.rustbridgerequesterror.js';
4
+ import {
5
+ RustBridgeRequestError,
6
+ type IRustBridgeRemoteSettlement,
7
+ } from './classes.rustbridgerequesterror.js';
5
8
  import { StreamingResponse } from './classes.streamingresponse.js';
6
9
  import { StdioTransport } from './classes.stdiotransport.js';
7
10
  import { SocketTransport } from './classes.sockettransport.js';
8
11
  import type {
9
12
  IRustBridgeOptions,
10
13
  IRustBridgeRequestOptions,
14
+ IRustBridgeStreamingRequestOptions,
11
15
  IRustBridgeTerminateOptions,
12
16
  IRustBridgeLogger,
13
17
  ISocketConnectOptions,
14
18
  TCommandMap,
15
19
  IManagementRequest,
20
+ IManagementCancelRequest,
16
21
  IManagementResponse,
17
22
  IManagementEvent,
18
23
  TStreamingCommandKeys,
@@ -35,6 +40,12 @@ interface IPendingRequest {
35
40
  abortHandler?: () => void;
36
41
  streaming?: StreamingResponse<any, any>;
37
42
  admissionReservation?: IRequestAdmissionReservation;
43
+ /**
44
+ * Transport the request line was handed to, recorded only when that
45
+ * connection advertised the `cancel` capability and the request is not
46
+ * sensitive. A later timeout or abort sends its cancel frame here.
47
+ */
48
+ cancelTransport?: TConfirmableRustTransport;
38
49
  sensitive?: {
39
50
  byteLength: number;
40
51
  writeComplete: boolean;
@@ -42,6 +53,12 @@ interface IPendingRequest {
42
53
  };
43
54
  }
44
55
 
56
+ /** A request abandoned locally whose cancel frame awaits Rust's answer. */
57
+ interface IRemoteCancellation {
58
+ method: string;
59
+ settle: (settlementArg: IRustBridgeRemoteSettlement) => void;
60
+ }
61
+
45
62
  interface IRequestAdmissionReservation {
46
63
  method: string;
47
64
  released: boolean;
@@ -81,6 +98,9 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
81
98
  private sensitiveRequestId: string | null = null;
82
99
  private sensitiveUsedGeneration: number | null = null;
83
100
  private regularRequestPreparations = 0;
101
+ /** Whether the current transport generation's ready event advertised `cancel`. */
102
+ private remoteCancelSupported = false;
103
+ private remoteCancellations = new Map<string, IRemoteCancellation>();
84
104
 
85
105
  constructor(options: IRustBridgeOptions) {
86
106
  super();
@@ -211,6 +231,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
211
231
  const generation = ++this.lifecycleGeneration;
212
232
  this.sensitiveRequestId = null;
213
233
  this.sensitiveUsedGeneration = null;
234
+ this.remoteCancelSupported = false;
214
235
  this.lifecycleState = 'starting';
215
236
 
216
237
  const activationPromise = (async (): Promise<boolean> => {
@@ -262,6 +283,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
262
283
  throw new Error(`RustBridge cannot activate while lifecycle state is '${this.lifecycleState}'`);
263
284
  }
264
285
  this.lifecycleState = 'starting';
286
+ this.remoteCancelSupported = false;
265
287
  this.transport = transportArg;
266
288
  }
267
289
 
@@ -309,7 +331,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
309
331
  if (!isCurrent()) {
310
332
  return;
311
333
  }
312
- this.handleLine(line, () => {
334
+ this.handleLine(line, (readyEvent: IManagementEvent) => {
313
335
  if (!isCurrent() || this.lifecycleState !== 'starting') {
314
336
  return;
315
337
  }
@@ -317,6 +339,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
317
339
  clearTimeout(reconnectReadyTimeout);
318
340
  reconnectReadyTimeout = null;
319
341
  }
342
+ this.remoteCancelSupported = this.readCancelCapability(readyEvent);
320
343
  this.lifecycleState = 'ready';
321
344
  this.isRunning = true;
322
345
  this.logger.log('info', `Bridge connected to ${this.options.binaryName}`);
@@ -346,7 +369,9 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
346
369
  };
347
370
  this.logger.log('info', 'Transport closed');
348
371
  this.isRunning = false;
372
+ this.remoteCancelSupported = false;
349
373
  this.rejectAllPendingRequests(`${this.options.binaryName} process exited`);
374
+ this.settleAllRemoteCancellations();
350
375
  if (!intentional && transportArg.reconnecting === true) {
351
376
  if (reconnectReadyTimeout) {
352
377
  clearTimeout(reconnectReadyTimeout);
@@ -385,6 +410,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
385
410
  }
386
411
  this.logger.log('info', 'Transport reconnected, waiting for ready event');
387
412
  this.lifecycleState = 'starting';
413
+ this.remoteCancelSupported = false;
388
414
  if (reconnectReadyTimeout) {
389
415
  clearTimeout(reconnectReadyTimeout);
390
416
  }
@@ -527,39 +553,28 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
527
553
 
528
554
  try {
529
555
  if (options?.signal) {
556
+ const signal = options.signal;
530
557
  pending.abortHandler = () => {
531
- this.settlePendingRequest(id, {
532
- outcome: 'reject',
533
- error: new RustBridgeRequestError(
534
- `Command '${method}' was aborted`,
535
- {
536
- code: 'ERR_RUST_BRIDGE_REQUEST_ABORTED',
537
- requestId: id,
538
- method,
539
- cause: options.signal?.reason,
540
- },
541
- ),
542
- });
558
+ this.abandonPendingRequest(
559
+ id,
560
+ 'ERR_RUST_BRIDGE_REQUEST_ABORTED',
561
+ `Command '${method}' was aborted`,
562
+ signal.reason,
563
+ );
543
564
  };
544
- options.signal.addEventListener('abort', pending.abortHandler, { once: true });
545
- if (options.signal.aborted) {
565
+ signal.addEventListener('abort', pending.abortHandler, { once: true });
566
+ if (signal.aborted) {
546
567
  pending.abortHandler();
547
568
  return;
548
569
  }
549
570
  }
550
571
 
551
572
  pending.timer = setTimeout(() => {
552
- this.settlePendingRequest(id, {
553
- outcome: 'reject',
554
- error: new RustBridgeRequestError(
555
- `Command '${method}' timed out after ${timeoutMs}ms`,
556
- {
557
- code: 'ERR_RUST_BRIDGE_REQUEST_TIMEOUT',
558
- requestId: id,
559
- method,
560
- },
561
- ),
562
- });
573
+ this.abandonPendingRequest(
574
+ id,
575
+ 'ERR_RUST_BRIDGE_REQUEST_TIMEOUT',
576
+ `Command '${method}' timed out after ${timeoutMs}ms`,
577
+ );
563
578
  }, timeoutMs);
564
579
 
565
580
  const currentAvailabilityError = this.getRequestAvailabilityError(
@@ -577,6 +592,9 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
577
592
  return;
578
593
  }
579
594
 
595
+ if (this.remoteCancelSupported) {
596
+ pending.cancelTransport = transport;
597
+ }
580
598
  void transport.write(json + '\n').catch((err: unknown) => {
581
599
  this.settlePendingRequest(id, {
582
600
  outcome: 'reject',
@@ -809,8 +827,18 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
809
827
  public sendCommandStreaming<K extends string & TStreamingCommandKeys<TCommands>>(
810
828
  method: K,
811
829
  params: TCommands[K]['params'],
830
+ options?: IRustBridgeStreamingRequestOptions,
812
831
  ): StreamingResponse<TExtractChunk<TCommands[K]>, TCommands[K]['result']> {
813
832
  const streaming = new StreamingResponse<TExtractChunk<TCommands[K]>, TCommands[K]['result']>();
833
+ const signal = options?.signal;
834
+
835
+ if (signal?.aborted) {
836
+ streaming.fail(new RustBridgeRequestError(
837
+ `Command '${method}' was aborted before it was sent`,
838
+ { code: 'ERR_RUST_BRIDGE_REQUEST_ABORTED', method, cause: signal.reason },
839
+ ));
840
+ return streaming;
841
+ }
814
842
 
815
843
  if (this.sensitiveRequestId !== null) {
816
844
  streaming.fail(new RustBridgeRequestError(
@@ -848,7 +876,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
848
876
 
849
877
  const availabilityError = this.getRequestAvailabilityError(
850
878
  method,
851
- undefined,
879
+ signal,
852
880
  transport,
853
881
  lifecycleGeneration,
854
882
  );
@@ -856,29 +884,36 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
856
884
  throw availabilityError;
857
885
  }
858
886
 
859
- this.pendingRequests.set(id, {
887
+ const pending: IPendingRequest = {
860
888
  resolve: (result: any) => streaming.finish(result),
861
889
  reject: (error: Error) => streaming.fail(error),
862
890
  timer: null,
863
891
  method,
892
+ abortSignal: signal,
864
893
  streaming,
865
894
  admissionReservation,
866
- });
895
+ };
896
+ this.pendingRequests.set(id, pending);
867
897
  reservationTransferred = true;
868
898
 
869
899
  try {
870
- const timeoutMs = this.options.streamTimeoutMs ?? this.options.requestTimeoutMs;
871
- const pending = this.pendingRequests.get(id)!;
872
- pending.timer = setTimeout(() => {
873
- this.settlePendingRequest(id, {
874
- outcome: 'reject',
875
- error: new Error(`Streaming command '${method}' timed out after ${timeoutMs}ms`),
876
- });
877
- }, timeoutMs);
900
+ if (signal) {
901
+ pending.abortHandler = () => {
902
+ this.abandonPendingRequest(
903
+ id,
904
+ 'ERR_RUST_BRIDGE_REQUEST_ABORTED',
905
+ `Streaming command '${method}' was aborted`,
906
+ signal.reason,
907
+ );
908
+ };
909
+ signal.addEventListener('abort', pending.abortHandler, { once: true });
910
+ }
911
+
912
+ this.armStreamingTimeout(id, pending, false);
878
913
 
879
914
  const currentAvailabilityError = this.getRequestAvailabilityError(
880
915
  method,
881
- undefined,
916
+ signal,
882
917
  transport,
883
918
  lifecycleGeneration,
884
919
  id,
@@ -891,6 +926,9 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
891
926
  return streaming;
892
927
  }
893
928
 
929
+ if (this.remoteCancelSupported) {
930
+ pending.cancelTransport = transport;
931
+ }
894
932
  void transport.write(json + '\n').catch((err: unknown) => {
895
933
  this.settlePendingRequest(id, {
896
934
  outcome: 'reject',
@@ -997,6 +1035,8 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
997
1035
  this.transport = null;
998
1036
  this.isRunning = false;
999
1037
  this.lifecycleState = 'idle';
1038
+ // The transport is confirmed closed, so no answer can still arrive.
1039
+ this.settleAllRemoteCancellations();
1000
1040
  })();
1001
1041
 
1002
1042
  this.terminationPromise = terminationPromise;
@@ -1058,7 +1098,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
1058
1098
  );
1059
1099
  }
1060
1100
 
1061
- private handleLine(line: string, onReadyArg: () => void): void {
1101
+ private handleLine(line: string, onReadyArg: (readyEventArg: IManagementEvent) => void): void {
1062
1102
  if (!line) return;
1063
1103
 
1064
1104
  let parsed: any;
@@ -1073,27 +1113,26 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
1073
1113
  if ('event' in parsed && !('id' in parsed)) {
1074
1114
  const event = parsed as IManagementEvent;
1075
1115
  if (event.event === this.options.readyEventName) {
1076
- onReadyArg();
1116
+ onReadyArg(event);
1077
1117
  }
1078
1118
  this.emit(`management:${event.event}`, event.data);
1079
1119
  return;
1080
1120
  }
1081
1121
 
1122
+ // Late lines of a request abandoned after a cancel frame. Chunks are
1123
+ // dropped; the one final answer settles the remote cancellation.
1124
+ if ('id' in parsed && this.remoteCancellations.has(parsed.id)) {
1125
+ if (parsed.stream !== true) {
1126
+ this.settleRemoteCancellation(parsed as IManagementResponse);
1127
+ }
1128
+ return;
1129
+ }
1130
+
1082
1131
  // Stream chunk (has 'id' + stream === true + 'data')
1083
1132
  if ('id' in parsed && parsed.stream === true && 'data' in parsed) {
1084
1133
  const pending = this.pendingRequests.get(parsed.id);
1085
1134
  if (pending?.streaming) {
1086
- // Reset inactivity timeout
1087
- if (pending.timer) {
1088
- clearTimeout(pending.timer);
1089
- }
1090
- const timeoutMs = this.options.streamTimeoutMs ?? this.options.requestTimeoutMs;
1091
- pending.timer = setTimeout(() => {
1092
- this.settlePendingRequest(parsed.id, {
1093
- outcome: 'reject',
1094
- error: new Error(`Streaming command timed out after ${timeoutMs}ms of inactivity`),
1095
- });
1096
- }, timeoutMs);
1135
+ this.armStreamingTimeout(parsed.id, pending, true);
1097
1136
  pending.streaming.pushChunk(parsed.data);
1098
1137
  }
1099
1138
  return;
@@ -1110,6 +1149,132 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
1110
1149
  }
1111
1150
  }
1112
1151
 
1152
+ /** Start or restart the inactivity deadline of one streaming request. */
1153
+ private armStreamingTimeout(id: string, pending: IPendingRequest, afterChunkArg: boolean): void {
1154
+ if (pending.timer) {
1155
+ clearTimeout(pending.timer);
1156
+ }
1157
+ const timeoutMs = this.options.streamTimeoutMs ?? this.options.requestTimeoutMs;
1158
+ const message = afterChunkArg
1159
+ ? `Streaming command '${pending.method}' timed out after ${timeoutMs}ms of inactivity`
1160
+ : `Streaming command '${pending.method}' timed out after ${timeoutMs}ms`;
1161
+ pending.timer = setTimeout(() => {
1162
+ this.abandonPendingRequest(id, 'ERR_RUST_BRIDGE_REQUEST_TIMEOUT', message);
1163
+ }, timeoutMs);
1164
+ }
1165
+
1166
+ /**
1167
+ * Reject a pending request on timeout or abort. When the request was
1168
+ * already written to a transport that accepts cancel frames, send one and
1169
+ * attach the remote settlement to the rejection.
1170
+ */
1171
+ private abandonPendingRequest(
1172
+ id: string,
1173
+ code: 'ERR_RUST_BRIDGE_REQUEST_TIMEOUT' | 'ERR_RUST_BRIDGE_REQUEST_ABORTED',
1174
+ message: string,
1175
+ cause?: unknown,
1176
+ ): void {
1177
+ const pending = this.pendingRequests.get(id);
1178
+ if (!pending) {
1179
+ return;
1180
+ }
1181
+ const remoteSettlement = this.sendRemoteCancel(id, pending);
1182
+ this.settlePendingRequest(id, {
1183
+ outcome: 'reject',
1184
+ error: new RustBridgeRequestError(message, {
1185
+ code,
1186
+ requestId: id,
1187
+ method: pending.method,
1188
+ cause,
1189
+ remoteSettlement,
1190
+ }),
1191
+ });
1192
+ }
1193
+
1194
+ /** Write one cancel frame and track the original id until Rust answers it. */
1195
+ private sendRemoteCancel(
1196
+ id: string,
1197
+ pending: IPendingRequest,
1198
+ ): Promise<IRustBridgeRemoteSettlement> | undefined {
1199
+ const transport = pending.cancelTransport;
1200
+ if (!transport || pending.sensitive || this.transport !== transport || !transport.connected) {
1201
+ return undefined;
1202
+ }
1203
+
1204
+ let settle!: (settlementArg: IRustBridgeRemoteSettlement) => void;
1205
+ const remoteSettlement = new Promise<IRustBridgeRemoteSettlement>((resolve) => {
1206
+ settle = resolve;
1207
+ });
1208
+ this.remoteCancellations.set(id, { method: pending.method, settle });
1209
+
1210
+ const frame: IManagementCancelRequest = { id, cancel: true };
1211
+ const logWriteFailure = (error: unknown): void => {
1212
+ // A failed write means the transport is closing; its close settles
1213
+ // the cancellation as disconnected.
1214
+ this.logger.log(
1215
+ 'warn',
1216
+ `Failed to write cancel for '${pending.method}' (${id}): ${this.getErrorMessage(error)}`,
1217
+ );
1218
+ };
1219
+ try {
1220
+ void transport.write(JSON.stringify(frame) + '\n').catch(logWriteFailure);
1221
+ } catch (error: unknown) {
1222
+ logWriteFailure(error);
1223
+ }
1224
+ return remoteSettlement;
1225
+ }
1226
+
1227
+ private settleRemoteCancellation(response: IManagementResponse): void {
1228
+ const cancellation = this.remoteCancellations.get(response.id);
1229
+ if (!cancellation) {
1230
+ return;
1231
+ }
1232
+ this.remoteCancellations.delete(response.id);
1233
+
1234
+ if (response.success) {
1235
+ cancellation.settle({ outcome: 'completed', result: response.result });
1236
+ return;
1237
+ }
1238
+ const settlement: IRustBridgeRemoteSettlement = {
1239
+ outcome: response.cancelled === true ? 'cancelled' : 'failed',
1240
+ };
1241
+ if (typeof response.error === 'string') {
1242
+ settlement.error = response.error;
1243
+ }
1244
+ if (typeof response.errorCode === 'string') {
1245
+ settlement.errorCode = response.errorCode;
1246
+ }
1247
+ if (response.errorData !== undefined) {
1248
+ settlement.errorData = response.errorData;
1249
+ }
1250
+ cancellation.settle(settlement);
1251
+ }
1252
+
1253
+ /** Settle every tracked cancellation as disconnected after transport closure. */
1254
+ private settleAllRemoteCancellations(): void {
1255
+ const cancellations = [...this.remoteCancellations.values()];
1256
+ this.remoteCancellations.clear();
1257
+ for (const cancellation of cancellations) {
1258
+ cancellation.settle({ outcome: 'disconnected' });
1259
+ }
1260
+ }
1261
+
1262
+ /** Read the `cancel` opt-in from a ready event's `capabilities`. */
1263
+ private readCancelCapability(readyEvent: IManagementEvent): boolean {
1264
+ const capabilities: unknown = readyEvent.capabilities;
1265
+ if (capabilities === undefined) {
1266
+ return false;
1267
+ }
1268
+ if (!Array.isArray(capabilities) || !capabilities.every((entry) => typeof entry === 'string')) {
1269
+ this.logger.log(
1270
+ 'warn',
1271
+ `Ignoring malformed ready capabilities from ${this.options.binaryName}`,
1272
+ );
1273
+ return false;
1274
+ }
1275
+ return capabilities.includes('cancel');
1276
+ }
1277
+
1113
1278
  private acceptSensitiveResponse(response: IManagementResponse, pending: IPendingRequest): void {
1114
1279
  const state = pending.sensitive!;
1115
1280
  const ack = response.sensitiveAck;
@@ -1155,6 +1320,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
1155
1320
  error: new RustBridgeRequestError(message, {
1156
1321
  code: 'ERR_RUST_BRIDGE_REQUEST_RUST_RESPONSE',
1157
1322
  responseErrorCode: typeof response.errorCode === 'string' ? response.errorCode : undefined,
1323
+ responseErrorData: response.errorData,
1158
1324
  requestId: response.id,
1159
1325
  method: pending.method,
1160
1326
  }),
@@ -1193,17 +1359,12 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
1193
1359
  for (const [id, pending] of [...this.pendingRequests]) {
1194
1360
  this.settlePendingRequest(id, {
1195
1361
  outcome: 'reject',
1196
- error: pending.streaming
1197
- ? new Error(message)
1198
- : new RustBridgeRequestError(
1199
- message,
1200
- {
1201
- code: 'ERR_RUST_BRIDGE_REQUEST_TRANSPORT',
1202
- requestId: id,
1203
- method: pending.method,
1204
- cause,
1205
- },
1206
- ),
1362
+ error: new RustBridgeRequestError(message, {
1363
+ code: 'ERR_RUST_BRIDGE_REQUEST_TRANSPORT',
1364
+ requestId: id,
1365
+ method: pending.method,
1366
+ cause,
1367
+ }),
1207
1368
  });
1208
1369
  }
1209
1370
  }
@@ -11,12 +11,41 @@ export type TRustBridgeRequestErrorCode =
11
11
  | 'ERR_RUST_BRIDGE_REQUEST_SENSITIVE_PROTOCOL'
12
12
  | 'ERR_RUST_BRIDGE_REQUEST_RUST_RESPONSE';
13
13
 
14
+ /**
15
+ * How the Rust side settled a request after the bridge sent it a cancel frame.
16
+ *
17
+ * - `cancelled`: Rust stopped the command and answered `cancelled: true`.
18
+ * - `completed`: Rust finished the command anyway and answered `success: true`.
19
+ * - `failed`: Rust answered `success: false` without `cancelled: true`.
20
+ * - `disconnected`: the transport closed before Rust answered; the outcome is unknown.
21
+ */
22
+ export type TRustBridgeRemoteSettlementOutcome =
23
+ | 'cancelled'
24
+ | 'completed'
25
+ | 'failed'
26
+ | 'disconnected';
27
+
28
+ /** Remote outcome of a locally abandoned request, as reported by Rust. */
29
+ export interface IRustBridgeRemoteSettlement {
30
+ outcome: TRustBridgeRemoteSettlementOutcome;
31
+ /** The command's result, present only for `completed`. */
32
+ result?: unknown;
33
+ /** Rust's error text, for `cancelled` and `failed` when Rust sent one. */
34
+ error?: string;
35
+ /** Rust's string `errorCode`, for `cancelled` and `failed` when Rust sent one. */
36
+ errorCode?: string;
37
+ /** Rust's `errorData` as sent, for `cancelled` and `failed` when Rust sent it. */
38
+ errorData?: unknown;
39
+ }
40
+
14
41
  export interface IRustBridgeRequestErrorOptions {
15
42
  code: TRustBridgeRequestErrorCode;
16
43
  responseErrorCode?: string;
44
+ responseErrorData?: unknown;
17
45
  requestId?: string;
18
46
  method?: string;
19
47
  cause?: unknown;
48
+ remoteSettlement?: Promise<IRustBridgeRemoteSettlement>;
20
49
  }
21
50
 
22
51
  /**
@@ -29,15 +58,25 @@ export interface IRustBridgeRequestErrorOptions {
29
58
  export class RustBridgeRequestError extends Error {
30
59
  public readonly code: TRustBridgeRequestErrorCode;
31
60
  public readonly responseErrorCode?: string;
61
+ /** Structured details from the Rust error response's `errorData`, as sent. */
62
+ public readonly responseErrorData?: unknown;
32
63
  public readonly requestId?: string;
33
64
  public readonly method?: string;
65
+ /**
66
+ * Present when a timeout or abort made the bridge send a cancel frame for a
67
+ * request already written to Rust. Settles once Rust answers the original
68
+ * request or the transport closes. It never rejects.
69
+ */
70
+ public readonly remoteSettlement?: Promise<IRustBridgeRemoteSettlement>;
34
71
 
35
72
  constructor(message: string, options: IRustBridgeRequestErrorOptions) {
36
73
  super(message, options.cause === undefined ? undefined : { cause: options.cause });
37
74
  this.name = 'RustBridgeRequestError';
38
75
  this.code = options.code;
39
76
  this.responseErrorCode = options.responseErrorCode;
77
+ this.responseErrorData = options.responseErrorData;
40
78
  this.requestId = options.requestId;
41
79
  this.method = options.method;
80
+ this.remoteSettlement = options.remoteSettlement;
42
81
  }
43
82
  }
package/ts/index.ts CHANGED
@@ -2,6 +2,8 @@ export { RustBridge } from './classes.rustbridge.js';
2
2
  export {
3
3
  RustBridgeRequestError,
4
4
  type IRustBridgeRequestErrorOptions,
5
+ type IRustBridgeRemoteSettlement,
6
+ type TRustBridgeRemoteSettlementOutcome,
5
7
  type TRustBridgeRequestErrorCode,
6
8
  } from './classes.rustbridgerequesterror.js';
7
9
  export { RustBinaryLocator } from './classes.rustbinarylocator.js';
@@ -9,12 +9,28 @@ export interface IRustBridgeLogger {
9
9
  * Options for one non-streaming Rust bridge request.
10
10
  */
11
11
  export interface IRustBridgeRequestOptions {
12
- /** Abort only the local wait for this request. */
12
+ /**
13
+ * Abort the local wait for this request. When the binary advertised the
14
+ * `cancel` capability and the request was already written, the bridge also
15
+ * sends a cancel frame.
16
+ */
13
17
  signal?: AbortSignal;
14
18
  /** Override the bridge request timeout for this call, in milliseconds. */
15
19
  timeoutMs?: number;
16
20
  }
17
21
 
22
+ /**
23
+ * Options for one streaming Rust bridge request.
24
+ */
25
+ export interface IRustBridgeStreamingRequestOptions {
26
+ /**
27
+ * Abort the local stream. When the binary advertised the `cancel`
28
+ * capability and the request was already written, the bridge also sends a
29
+ * cancel frame.
30
+ */
31
+ signal?: AbortSignal;
32
+ }
33
+
18
34
  /** Options for confirmed bridge termination. */
19
35
  export interface IRustBridgeTerminateOptions {
20
36
  /**
@@ -9,6 +9,17 @@ export interface IManagementRequest {
9
9
  sensitive?: IManagementSensitivePayload;
10
10
  }
11
11
 
12
+ /**
13
+ * Cancel frame sent to a binary that advertised the `cancel` capability, after
14
+ * a timeout or abort abandoned a request that was already written. The binary
15
+ * ignores it for ids it no longer runs and otherwise still answers the
16
+ * original id exactly once.
17
+ */
18
+ export interface IManagementCancelRequest {
19
+ id: string;
20
+ cancel: true;
21
+ }
22
+
12
23
  export interface IManagementSensitivePayload {
13
24
  protocol: 'smartrust-sensitive-v1';
14
25
  fileDescriptor: number;
@@ -30,6 +41,10 @@ export interface IManagementResponse {
30
41
  result?: any;
31
42
  error?: string;
32
43
  errorCode?: string;
44
+ /** Optional structured details of a failure, for example per-field validation errors. */
45
+ errorData?: unknown;
46
+ /** Set with `success: false` when the command stopped because of a cancel frame. */
47
+ cancelled?: boolean;
33
48
  /** Required after the receiver consumed the exact associated binary frame. */
34
49
  sensitiveAck?: IManagementSensitiveAck;
35
50
  }
@@ -40,6 +55,11 @@ export interface IManagementResponse {
40
55
  export interface IManagementEvent {
41
56
  event: string;
42
57
  data: any;
58
+ /**
59
+ * Protocol capabilities, read only from the ready event. `cancel` opts the
60
+ * current transport generation in to cancel frames.
61
+ */
62
+ capabilities?: string[];
43
63
  }
44
64
 
45
65
  /**