@push.rocks/smartrust 2.4.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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.rustbridge.d.ts +20 -2
- package/dist_ts/classes.rustbridge.js +164 -55
- package/dist_ts/classes.rustbridgerequesterror.d.ts +28 -0
- package/dist_ts/classes.rustbridgerequesterror.js +8 -1
- package/dist_ts/index.d.ts +1 -1
- package/dist_ts/index.js +1 -1
- package/dist_ts/interfaces/config.d.ts +16 -1
- package/dist_ts/interfaces/ipc.d.ts +17 -0
- package/package.json +1 -1
- package/readme.md +95 -18
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.rustbridge.ts +223 -63
- package/ts/classes.rustbridgerequesterror.ts +35 -0
- package/ts/index.ts +2 -0
- package/ts/interfaces/config.ts +17 -1
- package/ts/interfaces/ipc.ts +18 -0
package/ts/classes.rustbridge.ts
CHANGED
|
@@ -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 {
|
|
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.
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
545
|
-
if (
|
|
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.
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -1194,17 +1359,12 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
1194
1359
|
for (const [id, pending] of [...this.pendingRequests]) {
|
|
1195
1360
|
this.settlePendingRequest(id, {
|
|
1196
1361
|
outcome: 'reject',
|
|
1197
|
-
error:
|
|
1198
|
-
|
|
1199
|
-
:
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
requestId: id,
|
|
1204
|
-
method: pending.method,
|
|
1205
|
-
cause,
|
|
1206
|
-
},
|
|
1207
|
-
),
|
|
1362
|
+
error: new RustBridgeRequestError(message, {
|
|
1363
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_TRANSPORT',
|
|
1364
|
+
requestId: id,
|
|
1365
|
+
method: pending.method,
|
|
1366
|
+
cause,
|
|
1367
|
+
}),
|
|
1208
1368
|
});
|
|
1209
1369
|
}
|
|
1210
1370
|
}
|
|
@@ -11,6 +11,33 @@ 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;
|
|
@@ -18,6 +45,7 @@ export interface IRustBridgeRequestErrorOptions {
|
|
|
18
45
|
requestId?: string;
|
|
19
46
|
method?: string;
|
|
20
47
|
cause?: unknown;
|
|
48
|
+
remoteSettlement?: Promise<IRustBridgeRemoteSettlement>;
|
|
21
49
|
}
|
|
22
50
|
|
|
23
51
|
/**
|
|
@@ -34,6 +62,12 @@ export class RustBridgeRequestError extends Error {
|
|
|
34
62
|
public readonly responseErrorData?: unknown;
|
|
35
63
|
public readonly requestId?: string;
|
|
36
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>;
|
|
37
71
|
|
|
38
72
|
constructor(message: string, options: IRustBridgeRequestErrorOptions) {
|
|
39
73
|
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
@@ -43,5 +77,6 @@ export class RustBridgeRequestError extends Error {
|
|
|
43
77
|
this.responseErrorData = options.responseErrorData;
|
|
44
78
|
this.requestId = options.requestId;
|
|
45
79
|
this.method = options.method;
|
|
80
|
+
this.remoteSettlement = options.remoteSettlement;
|
|
46
81
|
}
|
|
47
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';
|
package/ts/interfaces/config.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
/**
|
package/ts/interfaces/ipc.ts
CHANGED
|
@@ -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;
|
|
@@ -32,6 +43,8 @@ export interface IManagementResponse {
|
|
|
32
43
|
errorCode?: string;
|
|
33
44
|
/** Optional structured details of a failure, for example per-field validation errors. */
|
|
34
45
|
errorData?: unknown;
|
|
46
|
+
/** Set with `success: false` when the command stopped because of a cancel frame. */
|
|
47
|
+
cancelled?: boolean;
|
|
35
48
|
/** Required after the receiver consumed the exact associated binary frame. */
|
|
36
49
|
sensitiveAck?: IManagementSensitiveAck;
|
|
37
50
|
}
|
|
@@ -42,6 +55,11 @@ export interface IManagementResponse {
|
|
|
42
55
|
export interface IManagementEvent {
|
|
43
56
|
event: string;
|
|
44
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[];
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
/**
|