@push.rocks/smartrust 2.4.0 → 2.5.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.linescanner.d.ts +13 -1
- package/dist_ts/classes.linescanner.js +57 -24
- 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 +5 -5
- package/readme.md +95 -18
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.linescanner.ts +62 -25
- 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
|
@@ -12,6 +12,12 @@ export class RustBridgeRequestError extends Error {
|
|
|
12
12
|
responseErrorData;
|
|
13
13
|
requestId;
|
|
14
14
|
method;
|
|
15
|
+
/**
|
|
16
|
+
* Present when a timeout or abort made the bridge send a cancel frame for a
|
|
17
|
+
* request already written to Rust. Settles once Rust answers the original
|
|
18
|
+
* request or the transport closes. It never rejects.
|
|
19
|
+
*/
|
|
20
|
+
remoteSettlement;
|
|
15
21
|
constructor(message, options) {
|
|
16
22
|
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
17
23
|
this.name = 'RustBridgeRequestError';
|
|
@@ -20,6 +26,7 @@ export class RustBridgeRequestError extends Error {
|
|
|
20
26
|
this.responseErrorData = options.responseErrorData;
|
|
21
27
|
this.requestId = options.requestId;
|
|
22
28
|
this.method = options.method;
|
|
29
|
+
this.remoteSettlement = options.remoteSettlement;
|
|
23
30
|
}
|
|
24
31
|
}
|
|
25
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
32
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5ydXN0YnJpZGdlcmVxdWVzdGVycm9yLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvY2xhc3Nlcy5ydXN0YnJpZGdlcmVxdWVzdGVycm9yLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQWtEQTs7Ozs7O0dBTUc7QUFDSCxNQUFNLE9BQU8sc0JBQXVCLFNBQVEsS0FBSztJQUMvQixJQUFJLENBQThCO0lBQ2xDLGlCQUFpQixDQUFVO0lBQzNDLDhFQUE4RTtJQUM5RCxpQkFBaUIsQ0FBVztJQUM1QixTQUFTLENBQVU7SUFDbkIsTUFBTSxDQUFVO0lBQ2hDOzs7O09BSUc7SUFDYSxnQkFBZ0IsQ0FBd0M7SUFFeEUsWUFBWSxPQUFlLEVBQUUsT0FBdUM7UUFDbEUsS0FBSyxDQUFDLE9BQU8sRUFBRSxPQUFPLENBQUMsS0FBSyxLQUFLLFNBQVMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxFQUFFLEtBQUssRUFBRSxPQUFPLENBQUMsS0FBSyxFQUFFLENBQUMsQ0FBQztRQUNuRixJQUFJLENBQUMsSUFBSSxHQUFHLHdCQUF3QixDQUFDO1FBQ3JDLElBQUksQ0FBQyxJQUFJLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQztRQUN6QixJQUFJLENBQUMsaUJBQWlCLEdBQUcsT0FBTyxDQUFDLGlCQUFpQixDQUFDO1FBQ25ELElBQUksQ0FBQyxpQkFBaUIsR0FBRyxPQUFPLENBQUMsaUJBQWlCLENBQUM7UUFDbkQsSUFBSSxDQUFDLFNBQVMsR0FBRyxPQUFPLENBQUMsU0FBUyxDQUFDO1FBQ25DLElBQUksQ0FBQyxNQUFNLEdBQUcsT0FBTyxDQUFDLE1BQU0sQ0FBQztRQUM3QixJQUFJLENBQUMsZ0JBQWdCLEdBQUcsT0FBTyxDQUFDLGdCQUFnQixDQUFDO0lBQ25ELENBQUM7Q0FDRiJ9
|
package/dist_ts/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { RustBridge } from './classes.rustbridge.js';
|
|
2
|
-
export { RustBridgeRequestError, type IRustBridgeRequestErrorOptions, type TRustBridgeRequestErrorCode, } from './classes.rustbridgerequesterror.js';
|
|
2
|
+
export { RustBridgeRequestError, type IRustBridgeRequestErrorOptions, type IRustBridgeRemoteSettlement, type TRustBridgeRemoteSettlementOutcome, type TRustBridgeRequestErrorCode, } from './classes.rustbridgerequesterror.js';
|
|
3
3
|
export { RustBinaryLocator } from './classes.rustbinarylocator.js';
|
|
4
4
|
export { RustBinaryLocatorError } from './classes.rustbinarylocatorerror.js';
|
|
5
5
|
export { StreamingResponse } from './classes.streamingresponse.js';
|
package/dist_ts/index.js
CHANGED
|
@@ -7,4 +7,4 @@ export { StdioTransport } from './classes.stdiotransport.js';
|
|
|
7
7
|
export { SocketTransport } from './classes.sockettransport.js';
|
|
8
8
|
export { LineScanner } from './classes.linescanner.js';
|
|
9
9
|
export * from './interfaces/index.js';
|
|
10
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
10
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDckQsT0FBTyxFQUNMLHNCQUFzQixHQUt2QixNQUFNLHFDQUFxQyxDQUFDO0FBQzdDLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLGdDQUFnQyxDQUFDO0FBQ25FLE9BQU8sRUFBRSxzQkFBc0IsRUFBRSxNQUFNLHFDQUFxQyxDQUFDO0FBQzdFLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLGdDQUFnQyxDQUFDO0FBQ25FLE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUM3RCxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFDL0QsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBQ3ZELGNBQWMsdUJBQXVCLENBQUMifQ==
|
|
@@ -8,11 +8,26 @@ export interface IRustBridgeLogger {
|
|
|
8
8
|
* Options for one non-streaming Rust bridge request.
|
|
9
9
|
*/
|
|
10
10
|
export interface IRustBridgeRequestOptions {
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Abort the local wait for this request. When the binary advertised the
|
|
13
|
+
* `cancel` capability and the request was already written, the bridge also
|
|
14
|
+
* sends a cancel frame.
|
|
15
|
+
*/
|
|
12
16
|
signal?: AbortSignal;
|
|
13
17
|
/** Override the bridge request timeout for this call, in milliseconds. */
|
|
14
18
|
timeoutMs?: number;
|
|
15
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Options for one streaming Rust bridge request.
|
|
22
|
+
*/
|
|
23
|
+
export interface IRustBridgeStreamingRequestOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Abort the local stream. When the binary advertised the `cancel`
|
|
26
|
+
* capability and the request was already written, the bridge also sends a
|
|
27
|
+
* cancel frame.
|
|
28
|
+
*/
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
}
|
|
16
31
|
/** Options for confirmed bridge termination. */
|
|
17
32
|
export interface IRustBridgeTerminateOptions {
|
|
18
33
|
/**
|
|
@@ -8,6 +8,16 @@ export interface IManagementRequest {
|
|
|
8
8
|
/** Value-free metadata for an associated one-shot binary frame. */
|
|
9
9
|
sensitive?: IManagementSensitivePayload;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Cancel frame sent to a binary that advertised the `cancel` capability, after
|
|
13
|
+
* a timeout or abort abandoned a request that was already written. The binary
|
|
14
|
+
* ignores it for ids it no longer runs and otherwise still answers the
|
|
15
|
+
* original id exactly once.
|
|
16
|
+
*/
|
|
17
|
+
export interface IManagementCancelRequest {
|
|
18
|
+
id: string;
|
|
19
|
+
cancel: true;
|
|
20
|
+
}
|
|
11
21
|
export interface IManagementSensitivePayload {
|
|
12
22
|
protocol: 'smartrust-sensitive-v1';
|
|
13
23
|
fileDescriptor: number;
|
|
@@ -29,6 +39,8 @@ export interface IManagementResponse {
|
|
|
29
39
|
errorCode?: string;
|
|
30
40
|
/** Optional structured details of a failure, for example per-field validation errors. */
|
|
31
41
|
errorData?: unknown;
|
|
42
|
+
/** Set with `success: false` when the command stopped because of a cancel frame. */
|
|
43
|
+
cancelled?: boolean;
|
|
32
44
|
/** Required after the receiver consumed the exact associated binary frame. */
|
|
33
45
|
sensitiveAck?: IManagementSensitiveAck;
|
|
34
46
|
}
|
|
@@ -38,6 +50,11 @@ export interface IManagementResponse {
|
|
|
38
50
|
export interface IManagementEvent {
|
|
39
51
|
event: string;
|
|
40
52
|
data: any;
|
|
53
|
+
/**
|
|
54
|
+
* Protocol capabilities, read only from the ready event. `cancel` opts the
|
|
55
|
+
* current transport generation in to cancel frames.
|
|
56
|
+
*/
|
|
57
|
+
capabilities?: string[];
|
|
41
58
|
}
|
|
42
59
|
/**
|
|
43
60
|
* Definition of a single command supported by a Rust binary.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartrust",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A type-safe bridge between JavaScript engines and Rust binaries.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"author": "Task Venture Capital GmbH",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@git.zone/tsbuild": "^5.
|
|
13
|
-
"@git.zone/tsrun": "^3.0.
|
|
14
|
-
"@git.zone/tstest": "^
|
|
15
|
-
"@types/node": "^26.6.
|
|
12
|
+
"@git.zone/tsbuild": "^5.1.2",
|
|
13
|
+
"@git.zone/tsrun": "^3.0.2",
|
|
14
|
+
"@git.zone/tstest": "^7.1.0",
|
|
15
|
+
"@types/node": "^26.6.3"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@push.rocks/smartpath": "^6.0.0"
|
package/readme.md
CHANGED
|
@@ -50,6 +50,9 @@ If you're integrating Rust into a Node.js project, you'll inevitably need:
|
|
|
50
50
|
| **Rust → TS** (Error) | `{"id": "req_1", "success": false, "error": "msg", "errorCode": "EAPP_INVALID", "errorData": {...}}` | Error correlated by ID; `errorCode` and `errorData` are optional |
|
|
51
51
|
| **Rust → TS** (Stream Chunk) | `{"id": "req_1", "stream": true, "data": {...}}` | Intermediate chunk (zero or more) |
|
|
52
52
|
| **Rust → TS** (Event) | `{"event": "ready", "data": {...}}` | Unsolicited event (no ID) |
|
|
53
|
+
| **Rust → TS** (Ready with capabilities) | `{"event": "ready", "data": {...}, "capabilities": ["cancel"]}` | Optional opt-in to cancel frames for this connection |
|
|
54
|
+
| **TS → Rust** (Cancel) | `{"id": "req_1", "cancel": true}` | Sent only to binaries that advertised `cancel`, after a timeout or abort |
|
|
55
|
+
| **Rust → TS** (Cancelled) | `{"id": "req_1", "success": false, "cancelled": true, "error": "msg"}` | The one answer to a cancelled request; `errorCode` and `errorData` are optional |
|
|
53
56
|
|
|
54
57
|
This protocol works identically over stdio and socket transports. Your Rust binary reads JSON lines from one end and writes JSON lines to the other. That's it.
|
|
55
58
|
|
|
@@ -222,9 +225,10 @@ try {
|
|
|
222
225
|
constructor-level `requestTimeoutMs` remains the default. An already-aborted
|
|
223
226
|
signal rejects before a request ID is allocated or anything is written.
|
|
224
227
|
|
|
225
|
-
Timeout and abort
|
|
226
|
-
disconnect the shared bridge,
|
|
227
|
-
|
|
228
|
+
Timeout and abort reject the local wait at once. They do not kill or
|
|
229
|
+
disconnect the shared bridge, and later commands can continue using the same
|
|
230
|
+
bridge. When the binary supports it, the bridge also asks Rust to stop the
|
|
231
|
+
command; see [Cancelling the Rust Side](#cancelling-the-rust-side).
|
|
228
232
|
|
|
229
233
|
Use `maxPendingRequestsByMethod` to fail fast before serialization and transport
|
|
230
234
|
writes when one command type reaches its local pending-request limit. Limits are
|
|
@@ -244,14 +248,66 @@ Capacity is released when the local request resolves, rejects, times out, or is
|
|
|
244
248
|
aborted, when its transport write fails, or when the bridge terminates. Regular
|
|
245
249
|
and streaming calls to the same method share its capacity. Every configured
|
|
246
250
|
limit must be a positive safe integer; invalid values throw `RangeError` during
|
|
247
|
-
construction.
|
|
248
|
-
|
|
249
|
-
|
|
251
|
+
construction. The limit bounds local pending state, not remote execution: a
|
|
252
|
+
timed-out or aborted request frees its capacity immediately, even while Rust is
|
|
253
|
+
still working on it or answering its cancel frame.
|
|
250
254
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
+
#### Cancelling the Rust Side
|
|
256
|
+
|
|
257
|
+
A binary opts in to cancellation by adding `"capabilities": ["cancel"]` to its
|
|
258
|
+
ready event. The bridge records this per connection: a respawn or socket
|
|
259
|
+
reconnect starts without it until the next ready event advertises it again.
|
|
260
|
+
Binaries that do not advertise `cancel` never receive a cancel frame, so the
|
|
261
|
+
Rust command may keep running after the local wait ends.
|
|
262
|
+
|
|
263
|
+
With the capability, a timeout or abort of a request that was already written
|
|
264
|
+
sends one cancel frame, `{"id":"req_1","cancel":true}`. No cancel frame is sent
|
|
265
|
+
when the response already arrived, when the signal was aborted before the
|
|
266
|
+
request was written, or for `sendCommandWithSensitiveBytes`, whose failures
|
|
267
|
+
terminate the child instead.
|
|
268
|
+
|
|
269
|
+
The rejection's `remoteSettlement` then reports how Rust settled the request.
|
|
270
|
+
It resolves once and never rejects:
|
|
271
|
+
|
|
272
|
+
| `outcome` | Meaning |
|
|
273
|
+
|---|---|
|
|
274
|
+
| `cancelled` | Rust stopped and answered `success: false, cancelled: true` |
|
|
275
|
+
| `completed` | Rust finished anyway; `result` holds the result |
|
|
276
|
+
| `failed` | Rust answered `success: false` without `cancelled: true` |
|
|
277
|
+
| `disconnected` | The transport closed before Rust answered; the outcome is unknown |
|
|
278
|
+
|
|
279
|
+
`cancelled` and `failed` carry Rust's `error`, string `errorCode` and
|
|
280
|
+
`errorData` when it sent them. `remoteSettlement` is undefined when no cancel
|
|
281
|
+
frame was sent. Stream chunks and answers that arrive for a cancelled request
|
|
282
|
+
are consumed by the bridge and never reach the caller.
|
|
283
|
+
|
|
284
|
+
A Rust command may take a while to reach a point where it can stop, and a
|
|
285
|
+
faulty binary may never answer. Bound the wait yourself and terminate the child
|
|
286
|
+
when it runs out; the settlement then resolves as `disconnected`:
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
try {
|
|
290
|
+
await bridge.sendCommand('ingest', params, { timeoutMs: 60_000 });
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error instanceof RustBridgeRequestError && error.remoteSettlement) {
|
|
293
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
294
|
+
const deadline = new Promise<null>((resolve) => {
|
|
295
|
+
timer = setTimeout(() => resolve(null), 30_000);
|
|
296
|
+
});
|
|
297
|
+
const settlement = await Promise.race([error.remoteSettlement, deadline]);
|
|
298
|
+
clearTimeout(timer);
|
|
299
|
+
if (!settlement) {
|
|
300
|
+
await bridge.terminate(); // respawn with bridge.spawn() on next use
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
> Without the `cancel` capability, or when the settlement is `completed` or
|
|
308
|
+
> `disconnected`, Rust may have executed the command. Do not blindly retry
|
|
309
|
+
> mutating commands; first establish whether the original command executed or
|
|
310
|
+
> design the command to be idempotent.
|
|
255
311
|
|
|
256
312
|
Timeout, cancellation, transport-write/exit, and Rust response failures use
|
|
257
313
|
`RustBridgeRequestError`.
|
|
@@ -260,8 +316,8 @@ programmatic handling:
|
|
|
260
316
|
|
|
261
317
|
| Code | Meaning |
|
|
262
318
|
|---|---|
|
|
263
|
-
| `ERR_RUST_BRIDGE_REQUEST_TIMEOUT` | The local request deadline elapsed |
|
|
264
|
-
| `ERR_RUST_BRIDGE_REQUEST_ABORTED` | The caller's signal aborted the local wait |
|
|
319
|
+
| `ERR_RUST_BRIDGE_REQUEST_TIMEOUT` | The local request deadline or streaming inactivity deadline elapsed |
|
|
320
|
+
| `ERR_RUST_BRIDGE_REQUEST_ABORTED` | The caller's signal aborted the local wait or stream |
|
|
265
321
|
| `ERR_RUST_BRIDGE_REQUEST_LIMIT` | The configured pending-request limit for this method is full |
|
|
266
322
|
| `ERR_RUST_BRIDGE_REQUEST_WRITE` | The transport rejected the request write |
|
|
267
323
|
| `ERR_RUST_BRIDGE_REQUEST_TRANSPORT` | The transport exited, disconnected, or was killed |
|
|
@@ -447,11 +503,26 @@ try {
|
|
|
447
503
|
A final `success: false` response rejects both paths with the same
|
|
448
504
|
`RustBridgeRequestError`. When the response includes an `errorCode` or
|
|
449
505
|
`errorData`, they are available as `responseErrorCode` and `responseErrorData`
|
|
450
|
-
on that error.
|
|
506
|
+
on that error. When the transport exits, disconnects or is terminated, both
|
|
507
|
+
paths reject with a `RustBridgeRequestError` whose code is
|
|
508
|
+
`ERR_RUST_BRIDGE_REQUEST_TRANSPORT`, as regular commands do.
|
|
509
|
+
|
|
510
|
+
#### Aborting a Stream
|
|
511
|
+
|
|
512
|
+
Pass an `AbortSignal` to stop consuming a stream. The iterator and `.result`
|
|
513
|
+
reject with `ERR_RUST_BRIDGE_REQUEST_ABORTED`, and a binary with the `cancel`
|
|
514
|
+
capability receives a cancel frame, exactly as for regular commands:
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
const controller = new AbortController();
|
|
518
|
+
const stream = bridge.sendCommandStreaming('processData', { count: 100 }, {
|
|
519
|
+
signal: controller.signal,
|
|
520
|
+
});
|
|
521
|
+
```
|
|
451
522
|
|
|
452
523
|
#### Stream Timeout
|
|
453
524
|
|
|
454
|
-
By default, streaming commands use the same timeout as regular commands (`requestTimeoutMs`). The timeout **resets on each chunk received**, so it acts as an inactivity timeout rather than an absolute timeout. You can configure it separately:
|
|
525
|
+
By default, streaming commands use the same timeout as regular commands (`requestTimeoutMs`). The timeout **resets on each chunk received**, so it acts as an inactivity timeout rather than an absolute timeout. When it elapses, the stream rejects with a `RustBridgeRequestError` whose code is `ERR_RUST_BRIDGE_REQUEST_TIMEOUT`, and a binary with the `cancel` capability receives a cancel frame. You can configure it separately:
|
|
455
526
|
|
|
456
527
|
```typescript
|
|
457
528
|
const bridge = new RustBridge<TMyCommands>({
|
|
@@ -621,6 +692,8 @@ Your Rust binary needs to implement a simple protocol. The transport (stdio or s
|
|
|
621
692
|
|
|
622
693
|
6. **Use stderr** for logging — it won't interfere with the IPC protocol
|
|
623
694
|
|
|
695
|
+
7. **Optionally support cancellation**: add `"capabilities":["cancel"]` to the ready event, then treat `{"id":"...","cancel":true}` as a request to stop that command at its next safe point. Answer the original id exactly once, either `{"id":"...","success":false,"cancelled":true,"error":"..."}` or its normal result when the command could no longer stop. Ignore cancel frames for ids that already finished or are unknown, and keep reading stdin while commands run so cancel frames arrive promptly.
|
|
696
|
+
|
|
624
697
|
#### Socket Mode (Daemon)
|
|
625
698
|
|
|
626
699
|
1. **Listen** on a Unix socket (e.g., `/var/run/my-daemon.sock`) or Windows named pipe
|
|
@@ -754,7 +827,7 @@ fn main() {
|
|
|
754
827
|
| `connect(socketPath, options?)` | `Promise<boolean>` | **Socket mode**: Connect to a running daemon; returns `false` on connection/readiness failure and rejects stdio-only descriptor/byte-channel options |
|
|
755
828
|
| `sendCommand(method, params, options?)` | `Promise<TCommands[K]['result']>` | Send a typed command with an optional per-call timeout or abort signal |
|
|
756
829
|
| `sendCommandWithSensitiveBytes(method, params, bytes, options?)` | `Promise<TCommands[K]['result']>` | Send one exclusive JSON command with its raw bytes on the generation-owned one-shot pipe |
|
|
757
|
-
| `sendCommandStreaming(method, params)` | `StreamingResponse<TChunk, TResult>` | Send a streaming command; returns immediately |
|
|
830
|
+
| `sendCommandStreaming(method, params, options?)` | `StreamingResponse<TChunk, TResult>` | Send a streaming command with an optional abort signal; returns immediately |
|
|
758
831
|
| `terminate(options?)` | `Promise<void>` | Confirm stdio child exit or local socket closure; `gracePeriodMs` controls SIGKILL escalation |
|
|
759
832
|
| `kill()` | `void` | Fire-and-forget compatibility wrapper around `terminate()` |
|
|
760
833
|
| `running` | `boolean` | Whether the bridge is currently connected and ready |
|
|
@@ -811,6 +884,9 @@ fn main() {
|
|
|
811
884
|
|---|---|
|
|
812
885
|
| `IRustBridgeOptions` | Full configuration for `RustBridge` |
|
|
813
886
|
| `IRustBridgeRequestOptions` | Per-command options: `{ signal?, timeoutMs? }` |
|
|
887
|
+
| `IRustBridgeStreamingRequestOptions` | Per-stream options: `{ signal? }` |
|
|
888
|
+
| `IRustBridgeRemoteSettlement` | Remote outcome of a cancelled request: `{ outcome, result?, error?, errorCode?, errorData? }` |
|
|
889
|
+
| `TRustBridgeRemoteSettlementOutcome` | `'cancelled' \| 'completed' \| 'failed' \| 'disconnected'` |
|
|
814
890
|
| `IRustBridgeTerminateOptions` | Confirmed termination options: `{ gracePeriodMs? }` |
|
|
815
891
|
| `IRustTransportDisconnectOptions` | Optional confirmed-disconnect grace period for owned process transports |
|
|
816
892
|
| `IBinaryLocatorOptions` | Configuration for `RustBinaryLocator` |
|
|
@@ -818,14 +894,15 @@ fn main() {
|
|
|
818
894
|
| `IRustBridgeLogger` | Logger interface: `{ log(level, message, data?) }` |
|
|
819
895
|
| `IRustTransport` | Transport interface (extends `EventEmitter`) |
|
|
820
896
|
| `IManagementRequest` | IPC request shape: `{ id, method, params }` |
|
|
821
|
-
| `IManagementResponse` | IPC response shape: `{ id, success, result?, error?, errorCode?, errorData? }` |
|
|
822
|
-
| `
|
|
897
|
+
| `IManagementResponse` | IPC response shape: `{ id, success, result?, error?, errorCode?, errorData?, cancelled? }` |
|
|
898
|
+
| `IManagementCancelRequest` | IPC cancel frame shape: `{ id, cancel: true }` |
|
|
899
|
+
| `IManagementEvent` | IPC event shape: `{ event, data, capabilities? }` |
|
|
823
900
|
| `IManagementStreamChunk` | IPC stream chunk shape: `{ id, stream: true, data }` |
|
|
824
901
|
| `ICommandDefinition` | Single command definition: `{ params, result }` |
|
|
825
902
|
| `TCommandMap` | `Record<string, ICommandDefinition>` |
|
|
826
903
|
| `TStreamingCommandKeys<T>` | Extracts keys from a command map that have a `chunk` field |
|
|
827
904
|
| `TExtractChunk<T>` | Extracts the chunk type from a streaming command definition |
|
|
828
|
-
| `RustBridgeRequestError` | Request error class with stable `code`, optional `responseErrorCode`, `responseErrorData`, `requestId`, and `
|
|
905
|
+
| `RustBridgeRequestError` | Request error class with stable `code`, optional `responseErrorCode`, `responseErrorData`, `requestId`, `method`, and `remoteSettlement` fields |
|
|
829
906
|
| `TRustBridgeRequestErrorCode` | Union of stable request error code strings |
|
|
830
907
|
|
|
831
908
|
## License and Legal Information
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -4,9 +4,16 @@ import type { IRustBridgeLogger } from './interfaces/index.js';
|
|
|
4
4
|
* Buffer-based newline scanner for streaming binary data.
|
|
5
5
|
* Accumulates chunks and emits complete lines via callback.
|
|
6
6
|
* Used by both StdioTransport and SocketTransport.
|
|
7
|
+
*
|
|
8
|
+
* Each byte is scanned once and copied at most once into its line, so a line that arrives in many
|
|
9
|
+
* chunks costs time linear in its length.
|
|
7
10
|
*/
|
|
8
11
|
export class LineScanner {
|
|
9
|
-
|
|
12
|
+
/** Chunks of the line being received; none of them contains a newline. */
|
|
13
|
+
private pending: Buffer[] = [];
|
|
14
|
+
private pendingLength = 0;
|
|
15
|
+
/** Bytes after a line whose callback threw; they may contain newlines and are scanned first. */
|
|
16
|
+
private unscanned: Buffer = Buffer.alloc(0);
|
|
10
17
|
private maxPayloadSize: number;
|
|
11
18
|
private logger: IRustBridgeLogger;
|
|
12
19
|
|
|
@@ -19,44 +26,74 @@ export class LineScanner {
|
|
|
19
26
|
* Feed a chunk of data. Calls onLine for each complete newline-terminated line found.
|
|
20
27
|
*/
|
|
21
28
|
public push(chunk: Buffer, onLine: (line: string) => void): void {
|
|
22
|
-
this.
|
|
29
|
+
const input = this.unscanned.length > 0 ? Buffer.concat([this.unscanned, chunk]) : chunk;
|
|
30
|
+
this.unscanned = Buffer.alloc(0);
|
|
31
|
+
let start = 0;
|
|
23
32
|
|
|
24
33
|
try {
|
|
25
34
|
let newlineIndex: number;
|
|
26
|
-
while ((newlineIndex =
|
|
27
|
-
const lineBuffer = this.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (lineBuffer.length > this.maxPayloadSize) {
|
|
31
|
-
this.logger.log('error', `Inbound message exceeds maxPayloadSize (${lineBuffer.length} bytes), dropping`);
|
|
35
|
+
while ((newlineIndex = input.indexOf(0x0A, start)) !== -1) {
|
|
36
|
+
const lineBuffer = this.takeLine(input.subarray(start, newlineIndex));
|
|
37
|
+
start = newlineIndex + 1;
|
|
38
|
+
if (!lineBuffer) {
|
|
32
39
|
continue;
|
|
33
40
|
}
|
|
34
|
-
|
|
35
41
|
const line = lineBuffer.toString('utf8').trim();
|
|
36
42
|
if (line) {
|
|
37
43
|
onLine(line);
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
} finally {
|
|
47
|
-
// A subarray retains its entire backing allocation, even when empty.
|
|
48
|
-
// Compact once per chunk, including when a callback throws, without
|
|
49
|
-
// repeatedly copying the unread suffix for each parsed line.
|
|
50
|
-
if (this.buffer.length === 0) {
|
|
51
|
-
this.buffer = Buffer.alloc(0);
|
|
52
|
-
} else if (this.buffer.buffer.byteLength > Math.max(Buffer.poolSize, this.buffer.length * 2)) {
|
|
53
|
-
this.buffer = Buffer.from(this.buffer);
|
|
54
|
-
}
|
|
46
|
+
this.appendPartial(input.subarray(start));
|
|
47
|
+
} catch (error) {
|
|
48
|
+
// Keep what follows the failed line for the next push, without retaining what was consumed.
|
|
49
|
+
this.unscanned = LineScanner.compact(input.subarray(start));
|
|
50
|
+
throw error;
|
|
55
51
|
}
|
|
56
52
|
}
|
|
57
53
|
|
|
58
54
|
/** Reset the internal buffer. */
|
|
59
55
|
public clear(): void {
|
|
60
|
-
this.
|
|
56
|
+
this.pending = [];
|
|
57
|
+
this.pendingLength = 0;
|
|
58
|
+
this.unscanned = Buffer.alloc(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Joins the pending chunks with the line's last piece; null when the line is too large. */
|
|
62
|
+
private takeLine(tail: Buffer): Buffer | null {
|
|
63
|
+
const length = this.pendingLength + tail.length;
|
|
64
|
+
const pending = this.pending;
|
|
65
|
+
this.pending = [];
|
|
66
|
+
this.pendingLength = 0;
|
|
67
|
+
if (length > this.maxPayloadSize) {
|
|
68
|
+
this.logger.log('error', `Inbound message exceeds maxPayloadSize (${length} bytes), dropping`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return pending.length === 0 ? tail : Buffer.concat([...pending, tail], length);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private appendPartial(partial: Buffer): void {
|
|
75
|
+
if (partial.length === 0) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Prevent OOM if sender never sends newline
|
|
79
|
+
if (this.pendingLength + partial.length > this.maxPayloadSize) {
|
|
80
|
+
this.logger.log(
|
|
81
|
+
'error',
|
|
82
|
+
`Buffer exceeded maxPayloadSize (${this.pendingLength + partial.length} bytes) without newline, clearing`,
|
|
83
|
+
);
|
|
84
|
+
this.pending = [];
|
|
85
|
+
this.pendingLength = 0;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.pending.push(LineScanner.compact(partial));
|
|
89
|
+
this.pendingLength += partial.length;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A subarray retains its entire backing allocation; copy a small piece of a large one. */
|
|
93
|
+
private static compact(piece: Buffer): Buffer {
|
|
94
|
+
if (piece.length === 0) {
|
|
95
|
+
return Buffer.alloc(0);
|
|
96
|
+
}
|
|
97
|
+
return piece.buffer.byteLength > Math.max(Buffer.poolSize, piece.length * 2) ? Buffer.from(piece) : piece;
|
|
61
98
|
}
|
|
62
99
|
}
|