@aztec-foundation/ipc-runtime 7.0.0-nightly.20261014 → 7.0.0-nightly.20261024
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/build/amd64-linux/ipc_runtime_napi.node +0 -0
- package/build/amd64-macos/ipc_runtime_napi.node +0 -0
- package/build/arm64-linux/ipc_runtime_napi.node +0 -0
- package/build/arm64-macos/ipc_runtime_napi.node +0 -0
- package/dest/frame_reader.d.ts +17 -0
- package/dest/frame_reader.js +68 -0
- package/dest/frame_reader.test.d.ts +1 -0
- package/dest/frame_reader.test.js +37 -0
- package/dest/index.d.ts +1 -1
- package/dest/index.js +1 -1
- package/dest/native_loader.d.ts +1 -1
- package/dest/pending_queue.d.ts +28 -0
- package/dest/pending_queue.js +42 -0
- package/dest/shm_client.d.ts +18 -11
- package/dest/shm_client.js +32 -30
- package/dest/shm_client.test.js +1 -1
- package/dest/spawned_backend.d.ts +46 -1
- package/dest/spawned_backend.js +251 -58
- package/dest/spawned_backend.test.js +39 -0
- package/dest/types.d.ts +11 -4
- package/dest/types.js +5 -4
- package/dest/uds.test.js +44 -25
- package/dest/uds_client.d.ts +21 -7
- package/dest/uds_client.js +72 -39
- package/dest/uds_server.js +11 -18
- package/package.json +1 -1
- package/src/frame_reader.test.ts +39 -0
- package/src/frame_reader.ts +72 -0
- package/src/index.ts +1 -0
- package/src/native_loader.ts +5 -1
- package/src/pending_queue.ts +55 -0
- package/src/shm_client.test.ts +5 -5
- package/src/shm_client.ts +51 -40
- package/src/spawned_backend.test.ts +51 -0
- package/src/spawned_backend.ts +367 -83
- package/src/types.ts +12 -4
- package/src/uds.test.ts +44 -28
- package/src/uds_client.ts +96 -49
- package/src/uds_server.ts +10 -22
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reassembles length-prefixed frames (`u32 len | len bytes`) from stream
|
|
3
|
+
* chunks. Chunks are queued as they arrive and each frame is copied out once
|
|
4
|
+
* it is complete, so a large frame costs one copy rather than one per chunk,
|
|
5
|
+
* and memory grows only with the bytes the peer has actually sent.
|
|
6
|
+
*/
|
|
7
|
+
export declare class FrameReader {
|
|
8
|
+
private chunks;
|
|
9
|
+
private buffered;
|
|
10
|
+
push(chunk: Buffer): void;
|
|
11
|
+
/** The next frame's length prefix, once its 4 bytes have arrived. */
|
|
12
|
+
peekLength(): number | undefined;
|
|
13
|
+
/** The next whole frame, length prefix included, once it has fully arrived. */
|
|
14
|
+
next(): Buffer | undefined;
|
|
15
|
+
private copyOut;
|
|
16
|
+
private consume;
|
|
17
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reassembles length-prefixed frames (`u32 len | len bytes`) from stream
|
|
3
|
+
* chunks. Chunks are queued as they arrive and each frame is copied out once
|
|
4
|
+
* it is complete, so a large frame costs one copy rather than one per chunk,
|
|
5
|
+
* and memory grows only with the bytes the peer has actually sent.
|
|
6
|
+
*/
|
|
7
|
+
export class FrameReader {
|
|
8
|
+
chunks = [];
|
|
9
|
+
buffered = 0;
|
|
10
|
+
push(chunk) {
|
|
11
|
+
this.chunks.push(chunk);
|
|
12
|
+
this.buffered += chunk.length;
|
|
13
|
+
}
|
|
14
|
+
/** The next frame's length prefix, once its 4 bytes have arrived. */
|
|
15
|
+
peekLength() {
|
|
16
|
+
if (this.buffered < 4) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const head = this.chunks[0];
|
|
20
|
+
if (head.length >= 4) {
|
|
21
|
+
return head.readUInt32LE(0);
|
|
22
|
+
}
|
|
23
|
+
return this.copyOut(4, /*consume=*/ false).readUInt32LE(0);
|
|
24
|
+
}
|
|
25
|
+
/** The next whole frame, length prefix included, once it has fully arrived. */
|
|
26
|
+
next() {
|
|
27
|
+
const len = this.peekLength();
|
|
28
|
+
if (len === undefined || this.buffered < 4 + len) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return this.copyOut(4 + len, /*consume=*/ true);
|
|
32
|
+
}
|
|
33
|
+
copyOut(n, consume) {
|
|
34
|
+
const head = this.chunks[0];
|
|
35
|
+
if (head.length >= n) {
|
|
36
|
+
if (consume) {
|
|
37
|
+
this.consume(n);
|
|
38
|
+
}
|
|
39
|
+
return head.subarray(0, n);
|
|
40
|
+
}
|
|
41
|
+
const out = Buffer.allocUnsafe(n);
|
|
42
|
+
let copied = 0;
|
|
43
|
+
for (const chunk of this.chunks) {
|
|
44
|
+
copied += chunk.copy(out, copied, 0, Math.min(chunk.length, n - copied));
|
|
45
|
+
if (copied === n) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (consume) {
|
|
50
|
+
this.consume(n);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
consume(n) {
|
|
55
|
+
this.buffered -= n;
|
|
56
|
+
while (n > 0) {
|
|
57
|
+
const head = this.chunks[0];
|
|
58
|
+
if (head.length <= n) {
|
|
59
|
+
n -= head.length;
|
|
60
|
+
this.chunks.shift();
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
this.chunks[0] = head.subarray(n);
|
|
64
|
+
n = 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import * as assert from "node:assert/strict";
|
|
3
|
+
import { FrameReader } from "./frame_reader.js";
|
|
4
|
+
function frame(payload) {
|
|
5
|
+
const header = Buffer.allocUnsafe(4);
|
|
6
|
+
header.writeUInt32LE(payload.length, 0);
|
|
7
|
+
return Buffer.concat([header, payload]);
|
|
8
|
+
}
|
|
9
|
+
test("reassembles a frame split across chunks, including a split prefix", () => {
|
|
10
|
+
const payload = Buffer.alloc(300_000);
|
|
11
|
+
for (let i = 0; i < payload.length; i++)
|
|
12
|
+
payload[i] = i & 0xff;
|
|
13
|
+
const bytes = frame(payload);
|
|
14
|
+
const reader = new FrameReader();
|
|
15
|
+
const cuts = [1, 3, 9, 1000, 150_000, bytes.length];
|
|
16
|
+
let start = 0;
|
|
17
|
+
for (const cut of cuts) {
|
|
18
|
+
assert.equal(reader.next(), undefined);
|
|
19
|
+
reader.push(bytes.subarray(start, cut));
|
|
20
|
+
start = cut;
|
|
21
|
+
}
|
|
22
|
+
assert.deepEqual(reader.next(), bytes);
|
|
23
|
+
assert.equal(reader.next(), undefined);
|
|
24
|
+
});
|
|
25
|
+
test("yields several frames from one chunk and keeps the remainder", () => {
|
|
26
|
+
const a = frame(Buffer.from([1, 2]));
|
|
27
|
+
const b = frame(Buffer.from([]));
|
|
28
|
+
const c = frame(Buffer.from([3, 4, 5]));
|
|
29
|
+
const reader = new FrameReader();
|
|
30
|
+
reader.push(Buffer.concat([a, b, c.subarray(0, 5)]));
|
|
31
|
+
assert.deepEqual(reader.next(), a);
|
|
32
|
+
assert.deepEqual(reader.next(), b);
|
|
33
|
+
assert.equal(reader.next(), undefined);
|
|
34
|
+
assert.equal(reader.peekLength(), 3);
|
|
35
|
+
reader.push(c.subarray(5));
|
|
36
|
+
assert.deepEqual(reader.next(), c);
|
|
37
|
+
});
|
package/dest/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type { IpcClientAsync, IpcClientSync } from "./types.js";
|
|
2
2
|
export { MAX_FRAME_SIZE, CONNECT_RETRY_BUDGET_MS, DEFAULT_RING_SIZE, SOCKET_BACKLOG, DEFAULT_CALL_TIMEOUT_NS, } from "./types.js";
|
|
3
3
|
export { IpcError, IpcTransportError, IpcProcessExitedError, IpcSpawnError, } from "./errors.js";
|
|
4
|
-
export { SpawnedProcessBackend, type SpawnedProcessBackendOptions, type SpawnedTransport, } from "./spawned_backend.js";
|
|
4
|
+
export { SpawnedProcessBackend, SpawnedProcessBackendSync, type SpawnedProcessBackendOptions, type SpawnedTransport, } from "./spawned_backend.js";
|
|
5
5
|
export { UdsIpcClient, type UdsIpcClientConnectOptions } from "./uds_client.js";
|
|
6
6
|
export { UdsIpcServer, type IpcServerHandler } from "./uds_server.js";
|
|
7
7
|
export { NapiShmSyncClient, NapiShmAsyncClient, createNapiShmSyncClient, createNapiShmAsyncClient, type NapiMsgpackClientSync, type NapiMsgpackClientAsync, } from "./shm_client.js";
|
package/dest/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { MAX_FRAME_SIZE, CONNECT_RETRY_BUDGET_MS, DEFAULT_RING_SIZE, SOCKET_BACKLOG, DEFAULT_CALL_TIMEOUT_NS, } from "./types.js";
|
|
2
2
|
export { IpcError, IpcTransportError, IpcProcessExitedError, IpcSpawnError, } from "./errors.js";
|
|
3
|
-
export { SpawnedProcessBackend, } from "./spawned_backend.js";
|
|
3
|
+
export { SpawnedProcessBackend, SpawnedProcessBackendSync, } from "./spawned_backend.js";
|
|
4
4
|
export { UdsIpcClient } from "./uds_client.js";
|
|
5
5
|
export { UdsIpcServer } from "./uds_server.js";
|
|
6
6
|
export { NapiShmSyncClient, NapiShmAsyncClient, createNapiShmSyncClient, createNapiShmAsyncClient, } from "./shm_client.js";
|
package/dest/native_loader.d.ts
CHANGED
|
@@ -11,6 +11,6 @@ export declare function findIpcRuntimeNapi(customPath?: string): string | null;
|
|
|
11
11
|
* the addon cannot be located or fails to dlopen.
|
|
12
12
|
*/
|
|
13
13
|
export declare function loadIpcRuntimeNapi(customPath?: string): {
|
|
14
|
-
MsgpackClient: new (shmName: string, clientId?: number) => any;
|
|
14
|
+
MsgpackClient: new (shmName: string, clientId?: number, serverPid?: number) => any;
|
|
15
15
|
MsgpackClientAsync: new (shmName: string, clientId?: number) => any;
|
|
16
16
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Request ids stay below 2^30 so V8 keeps them as small integers (no heap allocation per call). */
|
|
2
|
+
export declare const REQUEST_ID_SPACE: number;
|
|
3
|
+
/** A call awaiting its response. */
|
|
4
|
+
export interface PendingCall {
|
|
5
|
+
requestId: number;
|
|
6
|
+
resolve: (data: Uint8Array) => void;
|
|
7
|
+
reject: (error: unknown) => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* In-flight calls in issue order, paired to responses by request id. Servers
|
|
11
|
+
* that answer in order (e.g. a serial `run()` loop) always match the head, so
|
|
12
|
+
* the common path is a shift and an integer compare; out-of-order responses
|
|
13
|
+
* fall back to a search. Ids start at a random point so a stale frame from a
|
|
14
|
+
* previous occupant of the channel is unlikely to pair with a live call.
|
|
15
|
+
*/
|
|
16
|
+
export declare class PendingQueue {
|
|
17
|
+
private calls;
|
|
18
|
+
private nextRequestId;
|
|
19
|
+
get length(): number;
|
|
20
|
+
/** Enqueues a call under a fresh request id and returns the id. */
|
|
21
|
+
push(resolve: PendingCall["resolve"], reject: PendingCall["reject"]): number;
|
|
22
|
+
/** Removes the most recently pushed call (to unwind a failed send). */
|
|
23
|
+
pop(): PendingCall | undefined;
|
|
24
|
+
/** Removes and returns the call for `requestId`, or undefined if none is pending. */
|
|
25
|
+
take(requestId: number): PendingCall | undefined;
|
|
26
|
+
/** Removes and returns every pending call. */
|
|
27
|
+
drain(): PendingCall[];
|
|
28
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Request ids stay below 2^30 so V8 keeps them as small integers (no heap allocation per call). */
|
|
2
|
+
export const REQUEST_ID_SPACE = 2 ** 30;
|
|
3
|
+
/**
|
|
4
|
+
* In-flight calls in issue order, paired to responses by request id. Servers
|
|
5
|
+
* that answer in order (e.g. a serial `run()` loop) always match the head, so
|
|
6
|
+
* the common path is a shift and an integer compare; out-of-order responses
|
|
7
|
+
* fall back to a search. Ids start at a random point so a stale frame from a
|
|
8
|
+
* previous occupant of the channel is unlikely to pair with a live call.
|
|
9
|
+
*/
|
|
10
|
+
export class PendingQueue {
|
|
11
|
+
calls = [];
|
|
12
|
+
nextRequestId = Math.floor(Math.random() * REQUEST_ID_SPACE);
|
|
13
|
+
get length() {
|
|
14
|
+
return this.calls.length;
|
|
15
|
+
}
|
|
16
|
+
/** Enqueues a call under a fresh request id and returns the id. */
|
|
17
|
+
push(resolve, reject) {
|
|
18
|
+
const requestId = this.nextRequestId;
|
|
19
|
+
this.nextRequestId = (requestId + 1) % REQUEST_ID_SPACE;
|
|
20
|
+
this.calls.push({ requestId, resolve, reject });
|
|
21
|
+
return requestId;
|
|
22
|
+
}
|
|
23
|
+
/** Removes the most recently pushed call (to unwind a failed send). */
|
|
24
|
+
pop() {
|
|
25
|
+
return this.calls.pop();
|
|
26
|
+
}
|
|
27
|
+
/** Removes and returns the call for `requestId`, or undefined if none is pending. */
|
|
28
|
+
take(requestId) {
|
|
29
|
+
const calls = this.calls;
|
|
30
|
+
if (calls.length > 0 && calls[0].requestId === requestId) {
|
|
31
|
+
return calls.shift();
|
|
32
|
+
}
|
|
33
|
+
const i = calls.findIndex((c) => c.requestId === requestId);
|
|
34
|
+
return i === -1 ? undefined : calls.splice(i, 1)[0];
|
|
35
|
+
}
|
|
36
|
+
/** Removes and returns every pending call. */
|
|
37
|
+
drain() {
|
|
38
|
+
const calls = this.calls;
|
|
39
|
+
this.calls = [];
|
|
40
|
+
return calls;
|
|
41
|
+
}
|
|
42
|
+
}
|
package/dest/shm_client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IpcClientAsync, IpcClientSync } from "./types.js";
|
|
1
|
+
import { IpcClientAsync, IpcClientSync, IpcErrorMapper } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Minimum surface a NAPI msgpack client must expose. Satisfied by the
|
|
4
4
|
* `MsgpackClient` / `MsgpackClientAsync` classes exported from this
|
|
@@ -21,8 +21,8 @@ export interface NapiMsgpackClientSync {
|
|
|
21
21
|
close(): void;
|
|
22
22
|
}
|
|
23
23
|
export interface NapiMsgpackClientAsync {
|
|
24
|
-
setResponseCallback(cb: (requestId:
|
|
25
|
-
call(requestId:
|
|
24
|
+
setResponseCallback(cb: (requestId: number, response: Buffer) => void): void;
|
|
25
|
+
call(requestId: number, input: Buffer): void;
|
|
26
26
|
acquire(): void;
|
|
27
27
|
release(): void;
|
|
28
28
|
/** Stop the native poll thread, release any held TSFN ref, close the client. */
|
|
@@ -37,12 +37,9 @@ export declare class NapiShmSyncClient implements IpcClientSync {
|
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
39
|
* Wraps the fire-and-forget async NAPI msgpack client behind the
|
|
40
|
-
* `IpcClientAsync` interface.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* caller by the echoed id. Ids start at a random point per client so a
|
|
44
|
-
* stale frame left in a recycled SHM ring slot by a previous occupant
|
|
45
|
-
* cannot pair with a live call.
|
|
40
|
+
* `IpcClientAsync` interface. The C++ background polling thread invokes
|
|
41
|
+
* `setResponseCallback` once per response (in completion order), and this
|
|
42
|
+
* wrapper pairs it to its caller by the echoed id (see PendingQueue).
|
|
46
43
|
*
|
|
47
44
|
* `acquire` / `release` are reference-count hooks the NAPI exposes so the
|
|
48
45
|
* libuv loop is kept alive only while requests are outstanding — without
|
|
@@ -50,11 +47,12 @@ export declare class NapiShmSyncClient implements IpcClientSync {
|
|
|
50
47
|
*/
|
|
51
48
|
export declare class NapiShmAsyncClient implements IpcClientAsync {
|
|
52
49
|
private inner;
|
|
50
|
+
private readonly mapError?;
|
|
53
51
|
private readonly pending;
|
|
54
|
-
private nextRequestId;
|
|
55
52
|
private destroyed;
|
|
56
|
-
constructor(inner: NapiMsgpackClientAsync);
|
|
53
|
+
constructor(inner: NapiMsgpackClientAsync, mapError?: IpcErrorMapper | undefined);
|
|
57
54
|
call(input: Uint8Array): Promise<Uint8Array>;
|
|
55
|
+
private fail;
|
|
58
56
|
destroy(): Promise<void>;
|
|
59
57
|
}
|
|
60
58
|
export interface CreateNapiShmOptions {
|
|
@@ -62,6 +60,15 @@ export interface CreateNapiShmOptions {
|
|
|
62
60
|
clientId?: number;
|
|
63
61
|
/** Override addon path lookup. Rarely needed; useful for tests / unusual deployments. */
|
|
64
62
|
customAddonPath?: string;
|
|
63
|
+
/** Applied to every rejected call's error. */
|
|
64
|
+
mapError?: IpcErrorMapper;
|
|
65
|
+
/**
|
|
66
|
+
* Sync client only: the server's pid. A sync call holds the main thread, so
|
|
67
|
+
* nothing else can notice the server dying; with the pid, a call to a dead
|
|
68
|
+
* server throws (with `serverExited`, `exitCode` and `signal` set) instead
|
|
69
|
+
* of waiting forever. Calls have no deadline either way.
|
|
70
|
+
*/
|
|
71
|
+
serverPid?: number;
|
|
65
72
|
}
|
|
66
73
|
/**
|
|
67
74
|
* Factories that load the bundled `ipc_runtime_napi.node` addon and
|
package/dest/shm_client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadIpcRuntimeNapi } from "./native_loader.js";
|
|
2
|
+
import { PendingQueue } from "./pending_queue.js";
|
|
2
3
|
/** Wraps a sync NAPI msgpack client behind the IpcClientSync interface. */
|
|
3
4
|
export class NapiShmSyncClient {
|
|
4
5
|
inner;
|
|
@@ -18,12 +19,9 @@ export class NapiShmSyncClient {
|
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
21
|
* Wraps the fire-and-forget async NAPI msgpack client behind the
|
|
21
|
-
* `IpcClientAsync` interface.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* caller by the echoed id. Ids start at a random point per client so a
|
|
25
|
-
* stale frame left in a recycled SHM ring slot by a previous occupant
|
|
26
|
-
* cannot pair with a live call.
|
|
22
|
+
* `IpcClientAsync` interface. The C++ background polling thread invokes
|
|
23
|
+
* `setResponseCallback` once per response (in completion order), and this
|
|
24
|
+
* wrapper pairs it to its caller by the echoed id (see PendingQueue).
|
|
27
25
|
*
|
|
28
26
|
* `acquire` / `release` are reference-count hooks the NAPI exposes so the
|
|
29
27
|
* libuv loop is kept alive only while requests are outstanding — without
|
|
@@ -31,22 +29,22 @@ export class NapiShmSyncClient {
|
|
|
31
29
|
*/
|
|
32
30
|
export class NapiShmAsyncClient {
|
|
33
31
|
inner;
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
mapError;
|
|
33
|
+
pending = new PendingQueue();
|
|
36
34
|
destroyed = false;
|
|
37
|
-
constructor(inner) {
|
|
35
|
+
constructor(inner, mapError) {
|
|
38
36
|
this.inner = inner;
|
|
37
|
+
this.mapError = mapError;
|
|
39
38
|
this.inner.setResponseCallback((requestId, response) => {
|
|
40
39
|
if (this.destroyed) {
|
|
41
40
|
// Late response delivered after destroy(); the native close already
|
|
42
41
|
// balanced the TSFN reference.
|
|
43
42
|
return;
|
|
44
43
|
}
|
|
45
|
-
const cb = this.pending.
|
|
44
|
+
const cb = this.pending.take(requestId);
|
|
46
45
|
if (cb) {
|
|
47
|
-
this.pending.delete(requestId);
|
|
48
46
|
cb.resolve(new Uint8Array(response));
|
|
49
|
-
if (this.pending.
|
|
47
|
+
if (this.pending.length === 0) {
|
|
50
48
|
this.inner.release();
|
|
51
49
|
}
|
|
52
50
|
}
|
|
@@ -64,32 +62,39 @@ export class NapiShmAsyncClient {
|
|
|
64
62
|
}
|
|
65
63
|
call(input) {
|
|
66
64
|
if (this.destroyed) {
|
|
67
|
-
return Promise.reject
|
|
65
|
+
return new Promise((_, reject) => this.fail(reject, new Error("NapiShmAsyncClient: call() after destroy()")));
|
|
68
66
|
}
|
|
69
67
|
const buf = Buffer.isBuffer(input)
|
|
70
68
|
? input
|
|
71
69
|
: Buffer.from(input.buffer, input.byteOffset, input.byteLength);
|
|
72
70
|
return new Promise((resolve, reject) => {
|
|
73
|
-
|
|
74
|
-
if (this.pending.size === 0) {
|
|
71
|
+
if (this.pending.length === 0) {
|
|
75
72
|
this.inner.acquire();
|
|
76
73
|
}
|
|
77
|
-
this.pending.
|
|
74
|
+
const requestId = this.pending.push(resolve, reject);
|
|
78
75
|
try {
|
|
79
76
|
this.inner.call(requestId, buf);
|
|
80
77
|
}
|
|
81
78
|
catch (err) {
|
|
82
|
-
// Send failed — unwind the
|
|
83
|
-
this.pending.
|
|
84
|
-
if (this.pending.
|
|
79
|
+
// Send failed — unwind the entry we just added.
|
|
80
|
+
this.pending.pop();
|
|
81
|
+
if (this.pending.length === 0) {
|
|
85
82
|
this.inner.release();
|
|
86
83
|
}
|
|
87
|
-
reject
|
|
84
|
+
this.fail(reject, err instanceof Error
|
|
88
85
|
? err
|
|
89
86
|
: new Error(`SHM async call failed: ${String(err)}`));
|
|
90
87
|
}
|
|
91
88
|
});
|
|
92
89
|
}
|
|
90
|
+
fail(reject, err) {
|
|
91
|
+
if (this.mapError) {
|
|
92
|
+
this.mapError(err).then(reject, reject);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
reject(err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
93
98
|
async destroy() {
|
|
94
99
|
if (this.destroyed) {
|
|
95
100
|
return;
|
|
@@ -97,12 +102,11 @@ export class NapiShmAsyncClient {
|
|
|
97
102
|
this.destroyed = true;
|
|
98
103
|
// Reject anything still in flight.
|
|
99
104
|
const err = new Error("ipc-runtime SHM client destroyed before response");
|
|
100
|
-
for (const cb of this.pending.
|
|
101
|
-
cb.reject
|
|
105
|
+
for (const cb of this.pending.drain()) {
|
|
106
|
+
this.fail(cb.reject, err);
|
|
102
107
|
}
|
|
103
|
-
this.pending.clear();
|
|
104
108
|
// Stops the native poll thread and releases the TSFN reference taken
|
|
105
|
-
// when the
|
|
109
|
+
// when the queue went 0 → 1 — without this, Node never exits when
|
|
106
110
|
// destroyed with calls in flight.
|
|
107
111
|
this.inner.close();
|
|
108
112
|
}
|
|
@@ -116,11 +120,9 @@ export class NapiShmAsyncClient {
|
|
|
116
120
|
export function createNapiShmSyncClient(shmName, options = {}) {
|
|
117
121
|
const napi = loadIpcRuntimeNapi(options.customAddonPath);
|
|
118
122
|
return new NapiShmSyncClient(
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
options.clientId
|
|
122
|
-
? new napi.MsgpackClient(shmName)
|
|
123
|
-
: new napi.MsgpackClient(shmName, options.clientId));
|
|
123
|
+
// An undefined slot id makes the native side self-allocate a free slot
|
|
124
|
+
// (kAutoClientId) instead of aliasing every client onto slot 0.
|
|
125
|
+
new napi.MsgpackClient(shmName, options.clientId, options.serverPid));
|
|
124
126
|
}
|
|
125
127
|
export function createNapiShmAsyncClient(shmName, options = {}) {
|
|
126
128
|
const napi = loadIpcRuntimeNapi(options.customAddonPath);
|
|
@@ -129,5 +131,5 @@ export function createNapiShmAsyncClient(shmName, options = {}) {
|
|
|
129
131
|
// slot (kAutoClientId) instead of aliasing every client onto slot 0.
|
|
130
132
|
options.clientId === undefined
|
|
131
133
|
? new napi.MsgpackClientAsync(shmName)
|
|
132
|
-
: new napi.MsgpackClientAsync(shmName, options.clientId));
|
|
134
|
+
: new napi.MsgpackClientAsync(shmName, options.clientId), options.mapError);
|
|
133
135
|
}
|
package/dest/shm_client.test.js
CHANGED
|
@@ -35,7 +35,7 @@ test("shm async client discards a stale frame and still resolves the live call",
|
|
|
35
35
|
const liveId = addon.sent[0].requestId;
|
|
36
36
|
// A leftover frame from a ring's previous occupant: unknown id. Must be
|
|
37
37
|
// discarded — not resolve the live call, not reject anything.
|
|
38
|
-
addon.deliver(liveId ^
|
|
38
|
+
addon.deliver(liveId ^ 0x5eadbee, Buffer.from([0xba, 0xad]));
|
|
39
39
|
// The real response still pairs and resolves.
|
|
40
40
|
addon.deliver(liveId, Buffer.from([9, 9]));
|
|
41
41
|
assert.deepEqual(await pending, new Uint8Array([9, 9]));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IpcClientAsync } from "./types.js";
|
|
1
|
+
import { IpcClientAsync, IpcClientSync } from "./types.js";
|
|
2
2
|
export type SpawnedTransport = "uds" | "shm";
|
|
3
3
|
export interface SpawnedProcessBackendOptions {
|
|
4
4
|
/** Absolute path of the server binary to spawn. Callers resolve it (and fail with retry=false if missing). */
|
|
@@ -23,6 +23,20 @@ export interface SpawnedProcessBackendOptions {
|
|
|
23
23
|
* still reject (with retry=true); only later calls see the fresh process.
|
|
24
24
|
*/
|
|
25
25
|
respawn?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Unref the child process (and, over UDS, the idle socket) so a backend
|
|
28
|
+
* that is never destroy()ed cannot hold the Node event loop open. Calls in
|
|
29
|
+
* flight still keep the loop alive until their response arrives.
|
|
30
|
+
*/
|
|
31
|
+
unref?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Also unref the child's stdout/stderr pipes, which exist only when
|
|
34
|
+
* `logger` is set. Separate from `unref` because those pipes are how the
|
|
35
|
+
* caller sees the child's output: unref'ing them lets the process exit with
|
|
36
|
+
* log lines still unread, so it is opt-in even when the child itself is
|
|
37
|
+
* unref'd.
|
|
38
|
+
*/
|
|
39
|
+
unrefStdio?: boolean;
|
|
26
40
|
/** SHM only: fixed client slot id. When unset the client self-allocates a free slot. */
|
|
27
41
|
clientId?: number;
|
|
28
42
|
/** SHM only: override the native addon path. */
|
|
@@ -50,6 +64,7 @@ export declare class SpawnedProcessBackend implements IpcClientAsync {
|
|
|
50
64
|
static spawn(options: SpawnedProcessBackendOptions): Promise<SpawnedProcessBackend>;
|
|
51
65
|
getIpcPath(): string;
|
|
52
66
|
call(input: Uint8Array): Promise<Uint8Array>;
|
|
67
|
+
private callWithRespawn;
|
|
53
68
|
sendProcessSignal(signal: NodeJS.Signals): void;
|
|
54
69
|
destroy(): Promise<void>;
|
|
55
70
|
/** Return the live incarnation, lazily respawning one when allowed. */
|
|
@@ -59,6 +74,12 @@ export declare class SpawnedProcessBackend implements IpcClientAsync {
|
|
|
59
74
|
* actually happened: the socket breaks before the child's 'exit' event
|
|
60
75
|
* lands, so wait a short grace for exit attribution before giving up and
|
|
61
76
|
* rethrowing the transport error as-is.
|
|
77
|
+
*
|
|
78
|
+
* A broken connection to a process that is still running is retired by
|
|
79
|
+
* killing that process. The client cannot reconnect, so otherwise the
|
|
80
|
+
* backend would keep the dead connection, failing every later call, never
|
|
81
|
+
* respawning, and leaving the server running for the life of the caller.
|
|
82
|
+
* Killing it routes the failure through the ordinary death path instead.
|
|
62
83
|
*/
|
|
63
84
|
private attributeCallError;
|
|
64
85
|
/** Spawn the server process and connect to it; kills the child on any failure. */
|
|
@@ -70,3 +91,27 @@ export declare class SpawnedProcessBackend implements IpcClientAsync {
|
|
|
70
91
|
private asSpawnError;
|
|
71
92
|
private cleanupIpcPath;
|
|
72
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* A synchronous IpcClientSync backed by a spawned server process, for callers
|
|
96
|
+
* whose API cannot await (BarretenbergSync and friends). Shares process setup,
|
|
97
|
+
* stale-path removal and the connect backstop with SpawnedProcessBackend; only
|
|
98
|
+
* the request path differs, blocking in the NAPI client instead of returning a
|
|
99
|
+
* promise.
|
|
100
|
+
*
|
|
101
|
+
* SHM only: a synchronous request needs the shared-memory client, as a socket
|
|
102
|
+
* round trip cannot block the event loop without deadlocking it. Construction
|
|
103
|
+
* is still async — the server has to come up before the first call — so only
|
|
104
|
+
* call() and destroy() are synchronous.
|
|
105
|
+
*/
|
|
106
|
+
export declare class SpawnedProcessBackendSync implements IpcClientSync {
|
|
107
|
+
private readonly options;
|
|
108
|
+
private readonly child;
|
|
109
|
+
private readonly client;
|
|
110
|
+
private readonly ipcPath;
|
|
111
|
+
private readonly logPath?;
|
|
112
|
+
private destroyed;
|
|
113
|
+
private constructor();
|
|
114
|
+
static spawn(options: SpawnedProcessBackendOptions): Promise<SpawnedProcessBackendSync>;
|
|
115
|
+
call(input: Uint8Array): Uint8Array;
|
|
116
|
+
destroy(): void;
|
|
117
|
+
}
|