@push.rocks/smartrust 1.3.2 → 1.4.1
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/.smartconfig.json +42 -0
- package/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/classes.rustbridge.d.ts +5 -2
- package/dist_ts/classes.rustbridge.js +158 -40
- package/dist_ts/classes.rustbridgerequesterror.d.ts +23 -0
- package/dist_ts/classes.rustbridgerequesterror.js +20 -0
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces/config.d.ts +9 -0
- package/license +19 -0
- package/package.json +41 -12
- package/readme.md +58 -6
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/classes.rustbridge.ts +204 -44
- package/ts/classes.rustbridgerequesterror.ts +37 -0
- package/ts/index.ts +5 -0
- package/ts/interfaces/config.ts +10 -0
- package/.gitea/workflows/default_nottags.yaml +0 -66
- package/.gitea/workflows/default_tags.yaml +0 -124
- package/.vscode/launch.json +0 -11
- package/.vscode/settings.json +0 -26
- package/changelog.md +0 -84
- package/npmextra.json +0 -24
- package/test/helpers/mock-rust-binary.mjs +0 -104
- package/test/helpers/mock-socket-server.mjs +0 -124
- package/test/test.linescanner.node.ts +0 -89
- package/test/test.rustbinarylocator.node.ts +0 -98
- package/test/test.rustbridge.node.ts +0 -440
- package/test/test.sockettransport.node.ts +0 -312
- package/test/test.ts +0 -12
- package/tsconfig.json +0 -12
package/readme.md
CHANGED
|
@@ -9,9 +9,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|
|
9
9
|
## Install 📦
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
|
|
13
|
-
# or
|
|
14
|
-
pnpm install @push.rocks/smartrust
|
|
12
|
+
pnpm add @push.rocks/smartrust
|
|
15
13
|
```
|
|
16
14
|
|
|
17
15
|
## Overview 🔭
|
|
@@ -105,6 +103,57 @@ bridge.on('management:configChanged', (data) => {
|
|
|
105
103
|
bridge.kill();
|
|
106
104
|
```
|
|
107
105
|
|
|
106
|
+
### Per-Command Deadlines and Cancellation
|
|
107
|
+
|
|
108
|
+
Non-streaming commands accept an optional `IRustBridgeRequestOptions` argument:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { RustBridgeRequestError } from '@push.rocks/smartrust';
|
|
112
|
+
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const metrics = await bridge.sendCommand(
|
|
117
|
+
'getMetrics',
|
|
118
|
+
{},
|
|
119
|
+
{
|
|
120
|
+
timeoutMs: 2000,
|
|
121
|
+
signal: controller.signal,
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error instanceof RustBridgeRequestError) {
|
|
126
|
+
console.error(error.code, error.message);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`timeoutMs` must be a positive safe integer. When it is omitted, the bridge's
|
|
132
|
+
constructor-level `requestTimeoutMs` remains the default. An already-aborted
|
|
133
|
+
signal rejects before a request ID is allocated or anything is written.
|
|
134
|
+
|
|
135
|
+
Timeout and abort only abandon the local TypeScript wait. They do not kill or
|
|
136
|
+
disconnect the shared bridge, late responses are ignored, and later commands
|
|
137
|
+
can continue using the same bridge.
|
|
138
|
+
|
|
139
|
+
> The JSON-lines protocol has no cancellation message. After a command has
|
|
140
|
+
> been written, Rust may still execute it even if the local wait times out or
|
|
141
|
+
> is aborted. Do not blindly retry mutating commands; first establish whether
|
|
142
|
+
> the original command executed or design the command to be idempotent.
|
|
143
|
+
|
|
144
|
+
Timeout, cancellation, transport-write/exit, and Rust response failures use
|
|
145
|
+
`RustBridgeRequestError`.
|
|
146
|
+
Existing message text remains available, while `code` is stable for
|
|
147
|
+
programmatic handling:
|
|
148
|
+
|
|
149
|
+
| Code | Meaning |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `ERR_RUST_BRIDGE_REQUEST_TIMEOUT` | The local request deadline elapsed |
|
|
152
|
+
| `ERR_RUST_BRIDGE_REQUEST_ABORTED` | The caller's signal aborted the local wait |
|
|
153
|
+
| `ERR_RUST_BRIDGE_REQUEST_WRITE` | The transport rejected the request write |
|
|
154
|
+
| `ERR_RUST_BRIDGE_REQUEST_TRANSPORT` | The transport exited, disconnected, or was killed |
|
|
155
|
+
| `ERR_RUST_BRIDGE_REQUEST_RUST_RESPONSE` | Rust returned `success: false` |
|
|
156
|
+
|
|
108
157
|
### Socket Mode — Connect to a Running Daemon 🔗
|
|
109
158
|
|
|
110
159
|
When the Rust binary runs as a system service (e.g., via `systemd`, `launchd`, or a Windows Service), use `connect()` to talk to it over a Unix socket or named pipe:
|
|
@@ -284,7 +333,7 @@ const bridge = new RustBridge<TMyCommands>({
|
|
|
284
333
|
|
|
285
334
|
// --- Bridge Options ---
|
|
286
335
|
cliArgs: ['--management'], // optional: args passed to binary (default: ['--management'])
|
|
287
|
-
requestTimeoutMs: 30000, // optional:
|
|
336
|
+
requestTimeoutMs: 30000, // optional: default request timeout (default: 30000)
|
|
288
337
|
streamTimeoutMs: 30000, // optional: streaming inactivity timeout (default: requestTimeoutMs)
|
|
289
338
|
readyTimeoutMs: 10000, // optional: ready event timeout (default: 10000)
|
|
290
339
|
maxPayloadSize: 50 * 1024 * 1024, // optional: max message size in bytes (default: 50MB)
|
|
@@ -488,7 +537,7 @@ fn main() {
|
|
|
488
537
|
| `constructor` | `new RustBridge<T>(options: IRustBridgeOptions)` | Create a new bridge instance |
|
|
489
538
|
| `spawn()` | `Promise<boolean>` | **Stdio mode**: Spawn the binary and wait for ready; returns `false` on failure |
|
|
490
539
|
| `connect(socketPath, options?)` | `Promise<boolean>` | **Socket mode**: Connect to a running daemon; returns `false` on failure |
|
|
491
|
-
| `sendCommand(method, params)` | `Promise<TCommands[K]['result']>` | Send a typed command
|
|
540
|
+
| `sendCommand(method, params, options?)` | `Promise<TCommands[K]['result']>` | Send a typed command with an optional per-call timeout or abort signal |
|
|
492
541
|
| `sendCommandStreaming(method, params)` | `StreamingResponse<TChunk, TResult>` | Send a streaming command; returns immediately |
|
|
493
542
|
| `kill()` | `void` | Stdio: SIGTERM the process, SIGKILL after 5s. Socket: close the connection (daemon stays alive) |
|
|
494
543
|
| `running` | `boolean` | Whether the bridge is currently connected and ready |
|
|
@@ -541,6 +590,7 @@ fn main() {
|
|
|
541
590
|
| Interface / Type | Description |
|
|
542
591
|
|---|---|
|
|
543
592
|
| `IRustBridgeOptions` | Full configuration for `RustBridge` |
|
|
593
|
+
| `IRustBridgeRequestOptions` | Per-command options: `{ signal?, timeoutMs? }` |
|
|
544
594
|
| `IBinaryLocatorOptions` | Configuration for `RustBinaryLocator` |
|
|
545
595
|
| `ISocketConnectOptions` | Socket connection options (reconnect settings) |
|
|
546
596
|
| `IRustBridgeLogger` | Logger interface: `{ log(level, message, data?) }` |
|
|
@@ -553,10 +603,12 @@ fn main() {
|
|
|
553
603
|
| `TCommandMap` | `Record<string, ICommandDefinition>` |
|
|
554
604
|
| `TStreamingCommandKeys<T>` | Extracts keys from a command map that have a `chunk` field |
|
|
555
605
|
| `TExtractChunk<T>` | Extracts the chunk type from a streaming command definition |
|
|
606
|
+
| `RustBridgeRequestError` | Request error class with stable `code`, `requestId`, and `method` fields |
|
|
607
|
+
| `TRustBridgeRequestErrorCode` | Union of stable request error code strings |
|
|
556
608
|
|
|
557
609
|
## License and Legal Information
|
|
558
610
|
|
|
559
|
-
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [
|
|
611
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
|
560
612
|
|
|
561
613
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
562
614
|
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/classes.rustbridge.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { RustBinaryLocator } from './classes.rustbinarylocator.js';
|
|
3
|
+
import { RustBridgeRequestError } from './classes.rustbridgerequesterror.js';
|
|
3
4
|
import { StreamingResponse } from './classes.streamingresponse.js';
|
|
4
5
|
import { StdioTransport } from './classes.stdiotransport.js';
|
|
5
6
|
import { SocketTransport } from './classes.sockettransport.js';
|
|
6
7
|
import type {
|
|
7
8
|
IRustBridgeOptions,
|
|
9
|
+
IRustBridgeRequestOptions,
|
|
8
10
|
IRustBridgeLogger,
|
|
9
11
|
ISocketConnectOptions,
|
|
10
12
|
TCommandMap,
|
|
@@ -20,6 +22,20 @@ const defaultLogger: IRustBridgeLogger = {
|
|
|
20
22
|
log() {},
|
|
21
23
|
};
|
|
22
24
|
|
|
25
|
+
interface IPendingRequest {
|
|
26
|
+
resolve: (value: any) => void;
|
|
27
|
+
reject: (error: Error) => void;
|
|
28
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
29
|
+
method: string;
|
|
30
|
+
abortSignal?: AbortSignal;
|
|
31
|
+
abortHandler?: () => void;
|
|
32
|
+
streaming?: StreamingResponse<any, any>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type TPendingRequestSettlement =
|
|
36
|
+
| { outcome: 'resolve'; value: any }
|
|
37
|
+
| { outcome: 'reject'; error: Error };
|
|
38
|
+
|
|
23
39
|
/**
|
|
24
40
|
* Generic bridge between TypeScript and a Rust binary.
|
|
25
41
|
* Communicates via JSON-over-stdin/stdout IPC protocol (stdio mode)
|
|
@@ -32,12 +48,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
32
48
|
private options: Required<Pick<IRustBridgeOptions, 'cliArgs' | 'requestTimeoutMs' | 'readyTimeoutMs' | 'readyEventName' | 'maxPayloadSize'>> & IRustBridgeOptions;
|
|
33
49
|
private logger: IRustBridgeLogger;
|
|
34
50
|
private transport: IRustTransport | null = null;
|
|
35
|
-
private pendingRequests = new Map<string,
|
|
36
|
-
resolve: (value: any) => void;
|
|
37
|
-
reject: (error: Error) => void;
|
|
38
|
-
timer: ReturnType<typeof setTimeout>;
|
|
39
|
-
streaming?: StreamingResponse<any, any>;
|
|
40
|
-
}>();
|
|
51
|
+
private pendingRequests = new Map<string, IPendingRequest>();
|
|
41
52
|
private requestCounter = 0;
|
|
42
53
|
private isRunning = false;
|
|
43
54
|
private binaryPath: string | null = null;
|
|
@@ -116,13 +127,13 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
116
127
|
|
|
117
128
|
transport.on('close', (...args: any[]) => {
|
|
118
129
|
this.logger.log('info', `Transport closed`);
|
|
119
|
-
this.cleanup();
|
|
130
|
+
this.cleanup(`${this.options.binaryName} process exited`);
|
|
120
131
|
this.emit('exit', ...args);
|
|
121
132
|
});
|
|
122
133
|
|
|
123
134
|
transport.on('error', (err: Error) => {
|
|
124
135
|
this.logger.log('error', `Transport error: ${err.message}`);
|
|
125
|
-
this.cleanup();
|
|
136
|
+
this.cleanup(`${this.options.binaryName} process exited`, err);
|
|
126
137
|
resolve(false);
|
|
127
138
|
});
|
|
128
139
|
|
|
@@ -164,11 +175,32 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
164
175
|
public async sendCommand<K extends string & keyof TCommands>(
|
|
165
176
|
method: K,
|
|
166
177
|
params: TCommands[K]['params'],
|
|
178
|
+
options?: IRustBridgeRequestOptions,
|
|
167
179
|
): Promise<TCommands[K]['result']> {
|
|
180
|
+
const timeoutMs = options?.timeoutMs ?? this.options.requestTimeoutMs;
|
|
181
|
+
if (
|
|
182
|
+
options?.timeoutMs !== undefined
|
|
183
|
+
&& (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0)
|
|
184
|
+
) {
|
|
185
|
+
throw new RangeError('RustBridge request timeoutMs must be a positive safe integer');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (options?.signal?.aborted) {
|
|
189
|
+
throw new RustBridgeRequestError(
|
|
190
|
+
`Command '${method}' was aborted before it was sent`,
|
|
191
|
+
{
|
|
192
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_ABORTED',
|
|
193
|
+
method,
|
|
194
|
+
cause: options.signal.reason,
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
168
199
|
if (!this.transport?.connected || !this.isRunning) {
|
|
169
200
|
throw new Error(`${this.options.binaryName} bridge is not running`);
|
|
170
201
|
}
|
|
171
202
|
|
|
203
|
+
const transport = this.transport;
|
|
172
204
|
const id = `req_${++this.requestCounter}`;
|
|
173
205
|
const request: IManagementRequest = { id, method, params };
|
|
174
206
|
const json = JSON.stringify(request);
|
|
@@ -182,18 +214,76 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
182
214
|
}
|
|
183
215
|
|
|
184
216
|
return new Promise<TCommands[K]['result']>((resolve, reject) => {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
reject
|
|
188
|
-
|
|
217
|
+
const pending: IPendingRequest = {
|
|
218
|
+
resolve,
|
|
219
|
+
reject,
|
|
220
|
+
timer: null,
|
|
221
|
+
method,
|
|
222
|
+
abortSignal: options?.signal,
|
|
223
|
+
};
|
|
224
|
+
this.pendingRequests.set(id, pending);
|
|
225
|
+
|
|
226
|
+
if (options?.signal) {
|
|
227
|
+
pending.abortHandler = () => {
|
|
228
|
+
this.settlePendingRequest(id, {
|
|
229
|
+
outcome: 'reject',
|
|
230
|
+
error: new RustBridgeRequestError(
|
|
231
|
+
`Command '${method}' was aborted`,
|
|
232
|
+
{
|
|
233
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_ABORTED',
|
|
234
|
+
requestId: id,
|
|
235
|
+
method,
|
|
236
|
+
cause: options.signal?.reason,
|
|
237
|
+
},
|
|
238
|
+
),
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
options.signal.addEventListener('abort', pending.abortHandler, { once: true });
|
|
242
|
+
}
|
|
189
243
|
|
|
190
|
-
|
|
244
|
+
pending.timer = setTimeout(() => {
|
|
245
|
+
this.settlePendingRequest(id, {
|
|
246
|
+
outcome: 'reject',
|
|
247
|
+
error: new RustBridgeRequestError(
|
|
248
|
+
`Command '${method}' timed out after ${timeoutMs}ms`,
|
|
249
|
+
{
|
|
250
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_TIMEOUT',
|
|
251
|
+
requestId: id,
|
|
252
|
+
method,
|
|
253
|
+
},
|
|
254
|
+
),
|
|
255
|
+
});
|
|
256
|
+
}, timeoutMs);
|
|
191
257
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
258
|
+
try {
|
|
259
|
+
void transport.write(json + '\n').catch((err: unknown) => {
|
|
260
|
+
this.settlePendingRequest(id, {
|
|
261
|
+
outcome: 'reject',
|
|
262
|
+
error: new RustBridgeRequestError(
|
|
263
|
+
`Failed to write to transport: ${this.getErrorMessage(err)}`,
|
|
264
|
+
{
|
|
265
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_WRITE',
|
|
266
|
+
requestId: id,
|
|
267
|
+
method,
|
|
268
|
+
cause: err,
|
|
269
|
+
},
|
|
270
|
+
),
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
} catch (err: unknown) {
|
|
274
|
+
this.settlePendingRequest(id, {
|
|
275
|
+
outcome: 'reject',
|
|
276
|
+
error: new RustBridgeRequestError(
|
|
277
|
+
`Failed to write to transport: ${this.getErrorMessage(err)}`,
|
|
278
|
+
{
|
|
279
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_WRITE',
|
|
280
|
+
requestId: id,
|
|
281
|
+
method,
|
|
282
|
+
cause: err,
|
|
283
|
+
},
|
|
284
|
+
),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
197
287
|
});
|
|
198
288
|
}
|
|
199
289
|
|
|
@@ -227,22 +317,33 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
227
317
|
|
|
228
318
|
const timeoutMs = this.options.streamTimeoutMs ?? this.options.requestTimeoutMs;
|
|
229
319
|
const timer = setTimeout(() => {
|
|
230
|
-
this.
|
|
231
|
-
|
|
320
|
+
this.settlePendingRequest(id, {
|
|
321
|
+
outcome: 'reject',
|
|
322
|
+
error: new Error(`Streaming command '${method}' timed out after ${timeoutMs}ms`),
|
|
323
|
+
});
|
|
232
324
|
}, timeoutMs);
|
|
233
325
|
|
|
234
326
|
this.pendingRequests.set(id, {
|
|
235
327
|
resolve: (result: any) => streaming.finish(result),
|
|
236
328
|
reject: (error: Error) => streaming.fail(error),
|
|
237
329
|
timer,
|
|
330
|
+
method,
|
|
238
331
|
streaming,
|
|
239
332
|
});
|
|
240
333
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
334
|
+
try {
|
|
335
|
+
void this.transport!.write(json + '\n').catch((err: unknown) => {
|
|
336
|
+
this.settlePendingRequest(id, {
|
|
337
|
+
outcome: 'reject',
|
|
338
|
+
error: new Error(`Failed to write to transport: ${this.getErrorMessage(err)}`),
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
} catch (err: unknown) {
|
|
342
|
+
this.settlePendingRequest(id, {
|
|
343
|
+
outcome: 'reject',
|
|
344
|
+
error: new Error(`Failed to write to transport: ${this.getErrorMessage(err)}`),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
246
347
|
|
|
247
348
|
return streaming;
|
|
248
349
|
}
|
|
@@ -259,11 +360,7 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
259
360
|
this.isRunning = false;
|
|
260
361
|
|
|
261
362
|
// Reject pending requests
|
|
262
|
-
|
|
263
|
-
clearTimeout(pending.timer);
|
|
264
|
-
pending.reject(new Error(`${this.options.binaryName} process killed`));
|
|
265
|
-
}
|
|
266
|
-
this.pendingRequests.clear();
|
|
363
|
+
this.rejectAllPendingRequests(`${this.options.binaryName} process killed`);
|
|
267
364
|
|
|
268
365
|
transport.removeAllListeners();
|
|
269
366
|
transport.disconnect();
|
|
@@ -300,11 +397,15 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
300
397
|
const pending = this.pendingRequests.get(parsed.id);
|
|
301
398
|
if (pending?.streaming) {
|
|
302
399
|
// Reset inactivity timeout
|
|
303
|
-
|
|
400
|
+
if (pending.timer) {
|
|
401
|
+
clearTimeout(pending.timer);
|
|
402
|
+
}
|
|
304
403
|
const timeoutMs = this.options.streamTimeoutMs ?? this.options.requestTimeoutMs;
|
|
305
404
|
pending.timer = setTimeout(() => {
|
|
306
|
-
this.
|
|
307
|
-
|
|
405
|
+
this.settlePendingRequest(parsed.id, {
|
|
406
|
+
outcome: 'reject',
|
|
407
|
+
error: new Error(`Streaming command timed out after ${timeoutMs}ms of inactivity`),
|
|
408
|
+
});
|
|
308
409
|
}, timeoutMs);
|
|
309
410
|
pending.streaming.pushChunk(parsed.data);
|
|
310
411
|
}
|
|
@@ -316,26 +417,85 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
316
417
|
const response = parsed as IManagementResponse;
|
|
317
418
|
const pending = this.pendingRequests.get(response.id);
|
|
318
419
|
if (pending) {
|
|
319
|
-
clearTimeout(pending.timer);
|
|
320
|
-
this.pendingRequests.delete(response.id);
|
|
321
420
|
if (response.success) {
|
|
322
|
-
|
|
421
|
+
this.settlePendingRequest(response.id, {
|
|
422
|
+
outcome: 'resolve',
|
|
423
|
+
value: response.result,
|
|
424
|
+
});
|
|
323
425
|
} else {
|
|
324
|
-
|
|
426
|
+
const message = response.error || 'Unknown error from Rust process';
|
|
427
|
+
this.settlePendingRequest(response.id, {
|
|
428
|
+
outcome: 'reject',
|
|
429
|
+
error: pending.streaming
|
|
430
|
+
? new Error(message)
|
|
431
|
+
: new RustBridgeRequestError(
|
|
432
|
+
message,
|
|
433
|
+
{
|
|
434
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_RUST_RESPONSE',
|
|
435
|
+
requestId: response.id,
|
|
436
|
+
method: pending.method,
|
|
437
|
+
},
|
|
438
|
+
),
|
|
439
|
+
});
|
|
325
440
|
}
|
|
326
441
|
}
|
|
327
442
|
}
|
|
328
443
|
}
|
|
329
444
|
|
|
330
|
-
private
|
|
331
|
-
|
|
332
|
-
|
|
445
|
+
private settlePendingRequest(
|
|
446
|
+
id: string,
|
|
447
|
+
settlement: TPendingRequestSettlement,
|
|
448
|
+
): boolean {
|
|
449
|
+
const pending = this.pendingRequests.get(id);
|
|
450
|
+
if (!pending) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
333
453
|
|
|
334
|
-
|
|
335
|
-
|
|
454
|
+
this.pendingRequests.delete(id);
|
|
455
|
+
if (pending.timer) {
|
|
336
456
|
clearTimeout(pending.timer);
|
|
337
|
-
pending.
|
|
457
|
+
pending.timer = null;
|
|
458
|
+
}
|
|
459
|
+
if (pending.abortSignal && pending.abortHandler) {
|
|
460
|
+
pending.abortSignal.removeEventListener('abort', pending.abortHandler);
|
|
461
|
+
pending.abortHandler = undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (settlement.outcome === 'resolve') {
|
|
465
|
+
pending.resolve(settlement.value);
|
|
466
|
+
} else {
|
|
467
|
+
pending.reject(settlement.error);
|
|
338
468
|
}
|
|
339
|
-
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private rejectAllPendingRequests(message: string, cause?: unknown): void {
|
|
473
|
+
for (const [id, pending] of [...this.pendingRequests]) {
|
|
474
|
+
this.settlePendingRequest(id, {
|
|
475
|
+
outcome: 'reject',
|
|
476
|
+
error: pending.streaming
|
|
477
|
+
? new Error(message)
|
|
478
|
+
: new RustBridgeRequestError(
|
|
479
|
+
message,
|
|
480
|
+
{
|
|
481
|
+
code: 'ERR_RUST_BRIDGE_REQUEST_TRANSPORT',
|
|
482
|
+
requestId: id,
|
|
483
|
+
method: pending.method,
|
|
484
|
+
cause,
|
|
485
|
+
},
|
|
486
|
+
),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private cleanup(message: string, cause?: unknown): void {
|
|
492
|
+
this.isRunning = false;
|
|
493
|
+
this.transport = null;
|
|
494
|
+
|
|
495
|
+
this.rejectAllPendingRequests(message, cause);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private getErrorMessage(error: unknown): string {
|
|
499
|
+
return error instanceof Error ? error.message : String(error);
|
|
340
500
|
}
|
|
341
501
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable error codes for failures while waiting for a Rust bridge request.
|
|
3
|
+
*/
|
|
4
|
+
export type TRustBridgeRequestErrorCode =
|
|
5
|
+
| 'ERR_RUST_BRIDGE_REQUEST_TIMEOUT'
|
|
6
|
+
| 'ERR_RUST_BRIDGE_REQUEST_ABORTED'
|
|
7
|
+
| 'ERR_RUST_BRIDGE_REQUEST_WRITE'
|
|
8
|
+
| 'ERR_RUST_BRIDGE_REQUEST_TRANSPORT'
|
|
9
|
+
| 'ERR_RUST_BRIDGE_REQUEST_RUST_RESPONSE';
|
|
10
|
+
|
|
11
|
+
export interface IRustBridgeRequestErrorOptions {
|
|
12
|
+
code: TRustBridgeRequestErrorCode;
|
|
13
|
+
requestId?: string;
|
|
14
|
+
method?: string;
|
|
15
|
+
cause?: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Error raised while preparing or waiting for a Rust bridge request.
|
|
20
|
+
*
|
|
21
|
+
* Existing error messages are preserved while `code` provides a stable way to
|
|
22
|
+
* distinguish timeout, cancellation, transport, write, and Rust response
|
|
23
|
+
* failures.
|
|
24
|
+
*/
|
|
25
|
+
export class RustBridgeRequestError extends Error {
|
|
26
|
+
public readonly code: TRustBridgeRequestErrorCode;
|
|
27
|
+
public readonly requestId?: string;
|
|
28
|
+
public readonly method?: string;
|
|
29
|
+
|
|
30
|
+
constructor(message: string, options: IRustBridgeRequestErrorOptions) {
|
|
31
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
32
|
+
this.name = 'RustBridgeRequestError';
|
|
33
|
+
this.code = options.code;
|
|
34
|
+
this.requestId = options.requestId;
|
|
35
|
+
this.method = options.method;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/ts/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export { RustBridge } from './classes.rustbridge.js';
|
|
2
|
+
export {
|
|
3
|
+
RustBridgeRequestError,
|
|
4
|
+
type IRustBridgeRequestErrorOptions,
|
|
5
|
+
type TRustBridgeRequestErrorCode,
|
|
6
|
+
} from './classes.rustbridgerequesterror.js';
|
|
2
7
|
export { RustBinaryLocator } from './classes.rustbinarylocator.js';
|
|
3
8
|
export { StreamingResponse } from './classes.streamingresponse.js';
|
|
4
9
|
export { StdioTransport } from './classes.stdiotransport.js';
|
package/ts/interfaces/config.ts
CHANGED
|
@@ -5,6 +5,16 @@ export interface IRustBridgeLogger {
|
|
|
5
5
|
log(level: string, message: string, data?: Record<string, any>): void;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Options for one non-streaming Rust bridge request.
|
|
10
|
+
*/
|
|
11
|
+
export interface IRustBridgeRequestOptions {
|
|
12
|
+
/** Abort only the local wait for this request. */
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
/** Override the bridge request timeout for this call, in milliseconds. */
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
/**
|
|
9
19
|
* Options for locating a Rust binary.
|
|
10
20
|
*/
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
name: Default (not tags)
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags-ignore:
|
|
6
|
-
- '**'
|
|
7
|
-
|
|
8
|
-
env:
|
|
9
|
-
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
|
10
|
-
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@code.foss.global/${{gitea.repository}}.git
|
|
11
|
-
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
|
12
|
-
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
|
13
|
-
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
|
14
|
-
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
|
15
|
-
|
|
16
|
-
jobs:
|
|
17
|
-
security:
|
|
18
|
-
runs-on: ubuntu-latest
|
|
19
|
-
continue-on-error: true
|
|
20
|
-
container:
|
|
21
|
-
image: ${{ env.IMAGE }}
|
|
22
|
-
|
|
23
|
-
steps:
|
|
24
|
-
- uses: actions/checkout@v3
|
|
25
|
-
|
|
26
|
-
- name: Install pnpm and npmci
|
|
27
|
-
run: |
|
|
28
|
-
pnpm install -g pnpm
|
|
29
|
-
pnpm install -g @ship.zone/npmci
|
|
30
|
-
|
|
31
|
-
- name: Run npm prepare
|
|
32
|
-
run: npmci npm prepare
|
|
33
|
-
|
|
34
|
-
- name: Audit production dependencies
|
|
35
|
-
run: |
|
|
36
|
-
npmci command npm config set registry https://registry.npmjs.org
|
|
37
|
-
npmci command pnpm audit --audit-level=high --prod
|
|
38
|
-
continue-on-error: true
|
|
39
|
-
|
|
40
|
-
- name: Audit development dependencies
|
|
41
|
-
run: |
|
|
42
|
-
npmci command npm config set registry https://registry.npmjs.org
|
|
43
|
-
npmci command pnpm audit --audit-level=high --dev
|
|
44
|
-
continue-on-error: true
|
|
45
|
-
|
|
46
|
-
test:
|
|
47
|
-
if: ${{ always() }}
|
|
48
|
-
needs: security
|
|
49
|
-
runs-on: ubuntu-latest
|
|
50
|
-
container:
|
|
51
|
-
image: ${{ env.IMAGE }}
|
|
52
|
-
|
|
53
|
-
steps:
|
|
54
|
-
- uses: actions/checkout@v3
|
|
55
|
-
|
|
56
|
-
- name: Test stable
|
|
57
|
-
run: |
|
|
58
|
-
npmci node install stable
|
|
59
|
-
npmci npm install
|
|
60
|
-
npmci npm test
|
|
61
|
-
|
|
62
|
-
- name: Test build
|
|
63
|
-
run: |
|
|
64
|
-
npmci node install stable
|
|
65
|
-
npmci npm install
|
|
66
|
-
npmci npm build
|