@aztec-foundation/ipc-runtime 0.0.1-commit.b66364b
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/errors.d.ts +36 -0
- package/dest/errors.js +43 -0
- package/dest/index.d.ts +8 -0
- package/dest/index.js +7 -0
- package/dest/native_loader.d.ts +16 -0
- package/dest/native_loader.js +81 -0
- package/dest/shm_client.d.ts +73 -0
- package/dest/shm_client.js +133 -0
- package/dest/shm_client.test.d.ts +1 -0
- package/dest/shm_client.test.js +60 -0
- package/dest/spawned_backend.d.ts +72 -0
- package/dest/spawned_backend.js +342 -0
- package/dest/spawned_backend.test.d.ts +1 -0
- package/dest/spawned_backend.test.js +173 -0
- package/dest/types.d.ts +29 -0
- package/dest/types.js +19 -0
- package/dest/uds.test.d.ts +1 -0
- package/dest/uds.test.js +182 -0
- package/dest/uds_client.d.ts +42 -0
- package/dest/uds_client.js +183 -0
- package/dest/uds_server.d.ts +29 -0
- package/dest/uds_server.js +135 -0
- package/package.json +28 -0
- package/src/errors.ts +46 -0
- package/src/index.ts +34 -0
- package/src/native_loader.ts +97 -0
- package/src/shm_client.test.ts +72 -0
- package/src/shm_client.ts +194 -0
- package/src/spawned_backend.test.ts +225 -0
- package/src/spawned_backend.ts +460 -0
- package/src/types.ts +38 -0
- package/src/uds.test.ts +207 -0
- package/src/uds_client.ts +250 -0
- package/src/uds_server.ts +166 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dest/errors.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors thrown by the ipc-runtime transports and process backends.
|
|
3
|
+
*
|
|
4
|
+
* The cross-layer contract is the bare `retry` property, not these classes:
|
|
5
|
+
* an error with `retry === true` failed for environmental reasons (process
|
|
6
|
+
* death, machine load, a broken connection) and the operation may be retried;
|
|
7
|
+
* `retry === false` (or no `retry` property at all) means retrying cannot
|
|
8
|
+
* help. Consumers should feature-detect the property rather than import
|
|
9
|
+
* these types, so the convention survives package boundaries.
|
|
10
|
+
*/
|
|
11
|
+
export declare class IpcError extends Error {
|
|
12
|
+
readonly retry: boolean;
|
|
13
|
+
constructor(message: string, retry: boolean, options?: {
|
|
14
|
+
cause?: unknown;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/** The connection to the server broke while calls were in flight or before they could be sent. */
|
|
18
|
+
export declare class IpcTransportError extends IpcError {
|
|
19
|
+
constructor(message: string, options?: {
|
|
20
|
+
cause?: unknown;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** The spawned server process exited; carries the exit cause and, when captured, the log path. */
|
|
24
|
+
export declare class IpcProcessExitedError extends IpcError {
|
|
25
|
+
readonly code: number | null;
|
|
26
|
+
readonly signal: NodeJS.Signals | null;
|
|
27
|
+
readonly logPath?: string | undefined;
|
|
28
|
+
constructor(message: string, code: number | null, signal: NodeJS.Signals | null, logPath?: string | undefined);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The server process could not be started. Environmental failures (spawn
|
|
32
|
+
* raced a loaded machine, a wedged process hit the connect backstop) are
|
|
33
|
+
* retryable; configuration failures (binary not found) are not.
|
|
34
|
+
*/
|
|
35
|
+
export declare class IpcSpawnError extends IpcError {
|
|
36
|
+
}
|
package/dest/errors.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors thrown by the ipc-runtime transports and process backends.
|
|
3
|
+
*
|
|
4
|
+
* The cross-layer contract is the bare `retry` property, not these classes:
|
|
5
|
+
* an error with `retry === true` failed for environmental reasons (process
|
|
6
|
+
* death, machine load, a broken connection) and the operation may be retried;
|
|
7
|
+
* `retry === false` (or no `retry` property at all) means retrying cannot
|
|
8
|
+
* help. Consumers should feature-detect the property rather than import
|
|
9
|
+
* these types, so the convention survives package boundaries.
|
|
10
|
+
*/
|
|
11
|
+
export class IpcError extends Error {
|
|
12
|
+
retry;
|
|
13
|
+
constructor(message, retry, options) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
this.retry = retry;
|
|
16
|
+
this.name = new.target.name;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** The connection to the server broke while calls were in flight or before they could be sent. */
|
|
20
|
+
export class IpcTransportError extends IpcError {
|
|
21
|
+
constructor(message, options) {
|
|
22
|
+
super(message, /*retry=*/ true, options);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** The spawned server process exited; carries the exit cause and, when captured, the log path. */
|
|
26
|
+
export class IpcProcessExitedError extends IpcError {
|
|
27
|
+
code;
|
|
28
|
+
signal;
|
|
29
|
+
logPath;
|
|
30
|
+
constructor(message, code, signal, logPath) {
|
|
31
|
+
super(message, /*retry=*/ true);
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.signal = signal;
|
|
34
|
+
this.logPath = logPath;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The server process could not be started. Environmental failures (spawn
|
|
39
|
+
* raced a loaded machine, a wedged process hit the connect backstop) are
|
|
40
|
+
* retryable; configuration failures (binary not found) are not.
|
|
41
|
+
*/
|
|
42
|
+
export class IpcSpawnError extends IpcError {
|
|
43
|
+
}
|
package/dest/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { IpcClientAsync, IpcClientSync } from "./types.js";
|
|
2
|
+
export { MAX_FRAME_SIZE, CONNECT_RETRY_BUDGET_MS, DEFAULT_RING_SIZE, SOCKET_BACKLOG, DEFAULT_CALL_TIMEOUT_NS, } from "./types.js";
|
|
3
|
+
export { IpcError, IpcTransportError, IpcProcessExitedError, IpcSpawnError, } from "./errors.js";
|
|
4
|
+
export { SpawnedProcessBackend, type SpawnedProcessBackendOptions, type SpawnedTransport, } from "./spawned_backend.js";
|
|
5
|
+
export { UdsIpcClient, type UdsIpcClientConnectOptions } from "./uds_client.js";
|
|
6
|
+
export { UdsIpcServer, type IpcServerHandler } from "./uds_server.js";
|
|
7
|
+
export { NapiShmSyncClient, NapiShmAsyncClient, createNapiShmSyncClient, createNapiShmAsyncClient, type NapiMsgpackClientSync, type NapiMsgpackClientAsync, } from "./shm_client.js";
|
|
8
|
+
export { findIpcRuntimeNapi, loadIpcRuntimeNapi, type Platform, } from "./native_loader.js";
|
package/dest/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { MAX_FRAME_SIZE, CONNECT_RETRY_BUDGET_MS, DEFAULT_RING_SIZE, SOCKET_BACKLOG, DEFAULT_CALL_TIMEOUT_NS, } from "./types.js";
|
|
2
|
+
export { IpcError, IpcTransportError, IpcProcessExitedError, IpcSpawnError, } from "./errors.js";
|
|
3
|
+
export { SpawnedProcessBackend, } from "./spawned_backend.js";
|
|
4
|
+
export { UdsIpcClient } from "./uds_client.js";
|
|
5
|
+
export { UdsIpcServer } from "./uds_server.js";
|
|
6
|
+
export { NapiShmSyncClient, NapiShmAsyncClient, createNapiShmSyncClient, createNapiShmAsyncClient, } from "./shm_client.js";
|
|
7
|
+
export { findIpcRuntimeNapi, loadIpcRuntimeNapi, } from "./native_loader.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type Platform = "x86_64-linux" | "x86_64-darwin" | "aarch64-linux" | "aarch64-darwin";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the path of `ipc_runtime_napi.node` for the current platform.
|
|
4
|
+
* Returns null if either the platform is unsupported or the artifact is
|
|
5
|
+
* absent (typical reason: ipc-runtime/bootstrap.sh hasn't run yet).
|
|
6
|
+
*/
|
|
7
|
+
export declare function findIpcRuntimeNapi(customPath?: string): string | null;
|
|
8
|
+
/**
|
|
9
|
+
* Load `ipc_runtime_napi.node` and return its native exports
|
|
10
|
+
* (`MsgpackClient`, `MsgpackClientAsync`). Throws a descriptive error when
|
|
11
|
+
* the addon cannot be located or fails to dlopen.
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadIpcRuntimeNapi(customPath?: string): {
|
|
14
|
+
MsgpackClient: new (shmName: string, clientId?: number) => any;
|
|
15
|
+
MsgpackClientAsync: new (shmName: string, clientId?: number) => any;
|
|
16
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Locate the prebuilt ipc_runtime_napi.node addon shipped with this package.
|
|
2
|
+
//
|
|
3
|
+
// The addon is built by `ipc-runtime/bootstrap.sh` (CMake target
|
|
4
|
+
// `ipc_runtime_napi`) and copied into `build/<arch>-<os>/` next to this
|
|
5
|
+
// package's `package.json`. Resolution walks up from this file's URL to the
|
|
6
|
+
// first `package.json` adjacent to a `build/` directory — that's the
|
|
7
|
+
// package root in both `file:`-linked and published consumption.
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
const PLATFORM_TO_BUILD_DIR = {
|
|
13
|
+
"x86_64-linux": "amd64-linux",
|
|
14
|
+
"x86_64-darwin": "amd64-macos",
|
|
15
|
+
"aarch64-linux": "arm64-linux",
|
|
16
|
+
"aarch64-darwin": "arm64-macos",
|
|
17
|
+
};
|
|
18
|
+
function detectPlatform() {
|
|
19
|
+
const arch = process.arch;
|
|
20
|
+
const platform = process.platform;
|
|
21
|
+
if (arch === "x64" && platform === "linux")
|
|
22
|
+
return "x86_64-linux";
|
|
23
|
+
if (arch === "x64" && platform === "darwin")
|
|
24
|
+
return "x86_64-darwin";
|
|
25
|
+
if (arch === "arm64" && platform === "linux")
|
|
26
|
+
return "aarch64-linux";
|
|
27
|
+
if (arch === "arm64" && platform === "darwin")
|
|
28
|
+
return "aarch64-darwin";
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function findPackageRoot() {
|
|
32
|
+
// `import.meta.url` after tsc compile points at the .js file under
|
|
33
|
+
// <pkg>/dest/...; climb until we find package.json with a sibling build/.
|
|
34
|
+
let currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const root = path.parse(currentDir).root;
|
|
36
|
+
while (currentDir !== root) {
|
|
37
|
+
const packageJsonPath = path.join(currentDir, "package.json");
|
|
38
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
39
|
+
const buildDir = path.join(currentDir, "build");
|
|
40
|
+
if (fs.existsSync(buildDir)) {
|
|
41
|
+
return currentDir;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
currentDir = path.dirname(currentDir);
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the path of `ipc_runtime_napi.node` for the current platform.
|
|
50
|
+
* Returns null if either the platform is unsupported or the artifact is
|
|
51
|
+
* absent (typical reason: ipc-runtime/bootstrap.sh hasn't run yet).
|
|
52
|
+
*/
|
|
53
|
+
export function findIpcRuntimeNapi(customPath) {
|
|
54
|
+
if (customPath) {
|
|
55
|
+
return fs.existsSync(customPath) ? path.resolve(customPath) : null;
|
|
56
|
+
}
|
|
57
|
+
const platform = detectPlatform();
|
|
58
|
+
if (!platform)
|
|
59
|
+
return null;
|
|
60
|
+
const packageRoot = findPackageRoot();
|
|
61
|
+
if (!packageRoot)
|
|
62
|
+
return null;
|
|
63
|
+
const buildDir = PLATFORM_TO_BUILD_DIR[platform];
|
|
64
|
+
const candidate = path.join(packageRoot, "build", buildDir, "ipc_runtime_napi.node");
|
|
65
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Load `ipc_runtime_napi.node` and return its native exports
|
|
69
|
+
* (`MsgpackClient`, `MsgpackClientAsync`). Throws a descriptive error when
|
|
70
|
+
* the addon cannot be located or fails to dlopen.
|
|
71
|
+
*/
|
|
72
|
+
export function loadIpcRuntimeNapi(customPath) {
|
|
73
|
+
const addonPath = findIpcRuntimeNapi(customPath);
|
|
74
|
+
if (!addonPath) {
|
|
75
|
+
throw new Error("Could not locate ipc_runtime_napi.node. Build with `ipc-runtime/bootstrap.sh` " +
|
|
76
|
+
"or set the optional `customPath` argument to point at a prebuilt addon.");
|
|
77
|
+
}
|
|
78
|
+
// createRequire so this works in both ESM and CJS callers.
|
|
79
|
+
const require = createRequire(import.meta.url);
|
|
80
|
+
return require(addonPath);
|
|
81
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { IpcClientAsync, IpcClientSync } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Minimum surface a NAPI msgpack client must expose. Satisfied by the
|
|
4
|
+
* `MsgpackClient` / `MsgpackClientAsync` classes exported from this
|
|
5
|
+
* package's own `ipc_runtime_napi.node` addon (see ipc-runtime/cpp/napi/),
|
|
6
|
+
* which wraps the C++ ipc::IpcClient.
|
|
7
|
+
*
|
|
8
|
+
* The interface is exposed for tests / consumers that want to inject a
|
|
9
|
+
* mock or alternative implementation; the standard production path is the
|
|
10
|
+
* `createNapiShm{Sync,Async}Client` factories below, which load the
|
|
11
|
+
* prebuilt addon shipped in this package's `build/<arch>-<os>/` directory.
|
|
12
|
+
*
|
|
13
|
+
* Note on the async contract: `MsgpackClientAsync.call` is *fire and
|
|
14
|
+
* forget*. Responses arrive via `setResponseCallback` in COMPLETION order on
|
|
15
|
+
* a background-thread → main-thread bridge (Napi::ThreadSafeFunction), each
|
|
16
|
+
* carrying its echoed request id. The TS wrapper below owns the pending map
|
|
17
|
+
* and pairs responses to callers by id.
|
|
18
|
+
*/
|
|
19
|
+
export interface NapiMsgpackClientSync {
|
|
20
|
+
call(input: Buffer): Buffer;
|
|
21
|
+
close(): void;
|
|
22
|
+
}
|
|
23
|
+
export interface NapiMsgpackClientAsync {
|
|
24
|
+
setResponseCallback(cb: (requestId: bigint, response: Buffer) => void): void;
|
|
25
|
+
call(requestId: bigint, input: Buffer): void;
|
|
26
|
+
acquire(): void;
|
|
27
|
+
release(): void;
|
|
28
|
+
/** Stop the native poll thread, release any held TSFN ref, close the client. */
|
|
29
|
+
close(): void;
|
|
30
|
+
}
|
|
31
|
+
/** Wraps a sync NAPI msgpack client behind the IpcClientSync interface. */
|
|
32
|
+
export declare class NapiShmSyncClient implements IpcClientSync {
|
|
33
|
+
private inner;
|
|
34
|
+
constructor(inner: NapiMsgpackClientSync);
|
|
35
|
+
call(input: Uint8Array): Uint8Array;
|
|
36
|
+
destroy(): void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Wraps the fire-and-forget async NAPI msgpack client behind the
|
|
40
|
+
* `IpcClientAsync` interface. Owns a map of pending calls keyed by request
|
|
41
|
+
* id; the C++ background polling thread invokes `setResponseCallback` once
|
|
42
|
+
* per response (in completion order), and this wrapper pairs it to its
|
|
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.
|
|
46
|
+
*
|
|
47
|
+
* `acquire` / `release` are reference-count hooks the NAPI exposes so the
|
|
48
|
+
* libuv loop is kept alive only while requests are outstanding — without
|
|
49
|
+
* them a `node script.js` would never exit naturally.
|
|
50
|
+
*/
|
|
51
|
+
export declare class NapiShmAsyncClient implements IpcClientAsync {
|
|
52
|
+
private inner;
|
|
53
|
+
private readonly pending;
|
|
54
|
+
private nextRequestId;
|
|
55
|
+
private destroyed;
|
|
56
|
+
constructor(inner: NapiMsgpackClientAsync);
|
|
57
|
+
call(input: Uint8Array): Promise<Uint8Array>;
|
|
58
|
+
destroy(): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
export interface CreateNapiShmOptions {
|
|
61
|
+
/** MPSC client slot id (default 0). Distinct clients on the same shmName must use distinct slots. */
|
|
62
|
+
clientId?: number;
|
|
63
|
+
/** Override addon path lookup. Rarely needed; useful for tests / unusual deployments. */
|
|
64
|
+
customAddonPath?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Factories that load the bundled `ipc_runtime_napi.node` addon and
|
|
68
|
+
* construct an MPSC-SHM client wrapped behind the `IpcClient*` interface.
|
|
69
|
+
* Matches the transport used by `ipc::make_server` on the C++ side, so any
|
|
70
|
+
* server started via that helper accepts these clients directly.
|
|
71
|
+
*/
|
|
72
|
+
export declare function createNapiShmSyncClient(shmName: string, options?: CreateNapiShmOptions): NapiShmSyncClient;
|
|
73
|
+
export declare function createNapiShmAsyncClient(shmName: string, options?: CreateNapiShmOptions): NapiShmAsyncClient;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { loadIpcRuntimeNapi } from "./native_loader.js";
|
|
2
|
+
/** Wraps a sync NAPI msgpack client behind the IpcClientSync interface. */
|
|
3
|
+
export class NapiShmSyncClient {
|
|
4
|
+
inner;
|
|
5
|
+
constructor(inner) {
|
|
6
|
+
this.inner = inner;
|
|
7
|
+
}
|
|
8
|
+
call(input) {
|
|
9
|
+
const buf = Buffer.isBuffer(input)
|
|
10
|
+
? input
|
|
11
|
+
: Buffer.from(input.buffer, input.byteOffset, input.byteLength);
|
|
12
|
+
const resp = this.inner.call(buf);
|
|
13
|
+
return new Uint8Array(resp.buffer, resp.byteOffset, resp.byteLength);
|
|
14
|
+
}
|
|
15
|
+
destroy() {
|
|
16
|
+
this.inner.close();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Wraps the fire-and-forget async NAPI msgpack client behind the
|
|
21
|
+
* `IpcClientAsync` interface. Owns a map of pending calls keyed by request
|
|
22
|
+
* id; the C++ background polling thread invokes `setResponseCallback` once
|
|
23
|
+
* per response (in completion order), and this wrapper pairs it to its
|
|
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.
|
|
27
|
+
*
|
|
28
|
+
* `acquire` / `release` are reference-count hooks the NAPI exposes so the
|
|
29
|
+
* libuv loop is kept alive only while requests are outstanding — without
|
|
30
|
+
* them a `node script.js` would never exit naturally.
|
|
31
|
+
*/
|
|
32
|
+
export class NapiShmAsyncClient {
|
|
33
|
+
inner;
|
|
34
|
+
pending = new Map();
|
|
35
|
+
nextRequestId = (BigInt(Math.floor(Math.random() * 0xffffffff)) << 16n) + 1n;
|
|
36
|
+
destroyed = false;
|
|
37
|
+
constructor(inner) {
|
|
38
|
+
this.inner = inner;
|
|
39
|
+
this.inner.setResponseCallback((requestId, response) => {
|
|
40
|
+
if (this.destroyed) {
|
|
41
|
+
// Late response delivered after destroy(); the native close already
|
|
42
|
+
// balanced the TSFN reference.
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const cb = this.pending.get(requestId);
|
|
46
|
+
if (cb) {
|
|
47
|
+
this.pending.delete(requestId);
|
|
48
|
+
cb.resolve(new Uint8Array(response));
|
|
49
|
+
if (this.pending.size === 0) {
|
|
50
|
+
this.inner.release();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// SHM rings persist across occupants (slot reclaim / reattach), so a
|
|
55
|
+
// frame addressed to a previous occupant's id is an anticipated
|
|
56
|
+
// leftover — discard it and keep serving live calls. Log it so that if
|
|
57
|
+
// a genuinely lost pairing ever hangs a caller, the evidence is in the
|
|
58
|
+
// log rather than silently dropped. Don't release: no acquire was
|
|
59
|
+
// taken for an orphan response.
|
|
60
|
+
console.warn(`NapiShmAsyncClient: discarding response for unknown request id ${requestId} ` +
|
|
61
|
+
"(stale frame from a previous ring occupant?)");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
call(input) {
|
|
66
|
+
if (this.destroyed) {
|
|
67
|
+
return Promise.reject(new Error("NapiShmAsyncClient: call() after destroy()"));
|
|
68
|
+
}
|
|
69
|
+
const buf = Buffer.isBuffer(input)
|
|
70
|
+
? input
|
|
71
|
+
: Buffer.from(input.buffer, input.byteOffset, input.byteLength);
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const requestId = this.nextRequestId++;
|
|
74
|
+
if (this.pending.size === 0) {
|
|
75
|
+
this.inner.acquire();
|
|
76
|
+
}
|
|
77
|
+
this.pending.set(requestId, { resolve, reject });
|
|
78
|
+
try {
|
|
79
|
+
this.inner.call(requestId, buf);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
// Send failed — unwind the map entry we just added.
|
|
83
|
+
this.pending.delete(requestId);
|
|
84
|
+
if (this.pending.size === 0) {
|
|
85
|
+
this.inner.release();
|
|
86
|
+
}
|
|
87
|
+
reject(err instanceof Error
|
|
88
|
+
? err
|
|
89
|
+
: new Error(`SHM async call failed: ${String(err)}`));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async destroy() {
|
|
94
|
+
if (this.destroyed) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.destroyed = true;
|
|
98
|
+
// Reject anything still in flight.
|
|
99
|
+
const err = new Error("ipc-runtime SHM client destroyed before response");
|
|
100
|
+
for (const cb of this.pending.values()) {
|
|
101
|
+
cb.reject(err);
|
|
102
|
+
}
|
|
103
|
+
this.pending.clear();
|
|
104
|
+
// Stops the native poll thread and releases the TSFN reference taken
|
|
105
|
+
// when the map went 0 → 1 — without this, Node never exits when
|
|
106
|
+
// destroyed with calls in flight.
|
|
107
|
+
this.inner.close();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Factories that load the bundled `ipc_runtime_napi.node` addon and
|
|
112
|
+
* construct an MPSC-SHM client wrapped behind the `IpcClient*` interface.
|
|
113
|
+
* Matches the transport used by `ipc::make_server` on the C++ side, so any
|
|
114
|
+
* server started via that helper accepts these clients directly.
|
|
115
|
+
*/
|
|
116
|
+
export function createNapiShmSyncClient(shmName, options = {}) {
|
|
117
|
+
const napi = loadIpcRuntimeNapi(options.customAddonPath);
|
|
118
|
+
return new NapiShmSyncClient(
|
|
119
|
+
// Omit the slot id when not given so the native side self-allocates a free
|
|
120
|
+
// slot (kAutoClientId) instead of aliasing every client onto slot 0.
|
|
121
|
+
options.clientId === undefined
|
|
122
|
+
? new napi.MsgpackClient(shmName)
|
|
123
|
+
: new napi.MsgpackClient(shmName, options.clientId));
|
|
124
|
+
}
|
|
125
|
+
export function createNapiShmAsyncClient(shmName, options = {}) {
|
|
126
|
+
const napi = loadIpcRuntimeNapi(options.customAddonPath);
|
|
127
|
+
return new NapiShmAsyncClient(
|
|
128
|
+
// Omit the slot id when not given so the native side self-allocates a free
|
|
129
|
+
// slot (kAutoClientId) instead of aliasing every client onto slot 0.
|
|
130
|
+
options.clientId === undefined
|
|
131
|
+
? new napi.MsgpackClientAsync(shmName)
|
|
132
|
+
: new napi.MsgpackClientAsync(shmName, options.clientId));
|
|
133
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { NapiShmAsyncClient } from "./shm_client.js";
|
|
4
|
+
/**
|
|
5
|
+
* Drives NapiShmAsyncClient through a mock addon, so the id-pairing logic is
|
|
6
|
+
* testable without the native module or a live server.
|
|
7
|
+
*/
|
|
8
|
+
class MockAddon {
|
|
9
|
+
deliver;
|
|
10
|
+
sent = [];
|
|
11
|
+
acquires = 0;
|
|
12
|
+
releases = 0;
|
|
13
|
+
closed = false;
|
|
14
|
+
setResponseCallback(cb) {
|
|
15
|
+
this.deliver = cb;
|
|
16
|
+
}
|
|
17
|
+
call(requestId, input) {
|
|
18
|
+
this.sent.push({ requestId, input });
|
|
19
|
+
}
|
|
20
|
+
acquire() {
|
|
21
|
+
this.acquires++;
|
|
22
|
+
}
|
|
23
|
+
release() {
|
|
24
|
+
this.releases++;
|
|
25
|
+
}
|
|
26
|
+
close() {
|
|
27
|
+
this.closed = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
test("shm async client discards a stale frame and still resolves the live call", async () => {
|
|
31
|
+
const addon = new MockAddon();
|
|
32
|
+
const client = new NapiShmAsyncClient(addon);
|
|
33
|
+
const pending = client.call(new Uint8Array([1, 2, 3]));
|
|
34
|
+
assert.equal(addon.sent.length, 1);
|
|
35
|
+
const liveId = addon.sent[0].requestId;
|
|
36
|
+
// A leftover frame from a ring's previous occupant: unknown id. Must be
|
|
37
|
+
// discarded — not resolve the live call, not reject anything.
|
|
38
|
+
addon.deliver(liveId ^ 0xdeadbeefn, Buffer.from([0xba, 0xad]));
|
|
39
|
+
// The real response still pairs and resolves.
|
|
40
|
+
addon.deliver(liveId, Buffer.from([9, 9]));
|
|
41
|
+
assert.deepEqual(await pending, new Uint8Array([9, 9]));
|
|
42
|
+
// Refcount stayed balanced: one acquire for the call, one release when the
|
|
43
|
+
// live response drained the map (the stale frame must not release).
|
|
44
|
+
assert.equal(addon.acquires, 1);
|
|
45
|
+
assert.equal(addon.releases, 1);
|
|
46
|
+
await client.destroy();
|
|
47
|
+
});
|
|
48
|
+
test("shm async client pairs out-of-order responses to the right callers", async () => {
|
|
49
|
+
const addon = new MockAddon();
|
|
50
|
+
const client = new NapiShmAsyncClient(addon);
|
|
51
|
+
const a = client.call(new Uint8Array([0xaa]));
|
|
52
|
+
const b = client.call(new Uint8Array([0xbb]));
|
|
53
|
+
const [idA, idB] = addon.sent.map((s) => s.requestId);
|
|
54
|
+
// Complete in reverse order; each caller must get its own payload.
|
|
55
|
+
addon.deliver(idB, Buffer.from([2]));
|
|
56
|
+
addon.deliver(idA, Buffer.from([1]));
|
|
57
|
+
assert.deepEqual(await b, new Uint8Array([2]));
|
|
58
|
+
assert.deepEqual(await a, new Uint8Array([1]));
|
|
59
|
+
await client.destroy();
|
|
60
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { IpcClientAsync } from "./types.js";
|
|
2
|
+
export type SpawnedTransport = "uds" | "shm";
|
|
3
|
+
export interface SpawnedProcessBackendOptions {
|
|
4
|
+
/** Absolute path of the server binary to spawn. Callers resolve it (and fail with retry=false if missing). */
|
|
5
|
+
binaryPath: string;
|
|
6
|
+
/** Binary name used in error messages and log labels. */
|
|
7
|
+
binaryName: string;
|
|
8
|
+
/** Prefix for the per-instance ipc path (socket / shm name). */
|
|
9
|
+
instancePrefix: string;
|
|
10
|
+
/** Argv template; each '{path}' is replaced with the backend's ipc path. */
|
|
11
|
+
ipcPathArgs: string[];
|
|
12
|
+
transport: SpawnedTransport;
|
|
13
|
+
/** Receives the child's stdout/stderr lines. Without it, output is captured to a temp log file. */
|
|
14
|
+
logger?: (msg: string) => void;
|
|
15
|
+
connectTimeoutMs?: number;
|
|
16
|
+
env?: NodeJS.ProcessEnv;
|
|
17
|
+
extraArgs?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* Respawn the server on the next call() after it dies, instead of failing all
|
|
20
|
+
* subsequent calls. Only safe for stateless servers: a respawned process
|
|
21
|
+
* remembers nothing, so any server-side session state (forks, cursors) held
|
|
22
|
+
* by callers would silently dangle. In-flight calls at the time of death
|
|
23
|
+
* still reject (with retry=true); only later calls see the fresh process.
|
|
24
|
+
*/
|
|
25
|
+
respawn?: boolean;
|
|
26
|
+
/** SHM only: fixed client slot id. When unset the client self-allocates a free slot. */
|
|
27
|
+
clientId?: number;
|
|
28
|
+
/** SHM only: override the native addon path. */
|
|
29
|
+
napiPath?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* An IpcClientAsync backed by a spawned server process. Owns the process
|
|
33
|
+
* lifecycle end to end: spawn, connect (raced against child death, with a
|
|
34
|
+
* kill-on-expiry backstop), death detection, optional lazy respawn, and
|
|
35
|
+
* teardown with SIGTERM→SIGKILL escalation. Failures surface as IpcError
|
|
36
|
+
* subclasses whose `retry` property tells callers whether the operation may
|
|
37
|
+
* be retried; no process or transport state is exposed on the API.
|
|
38
|
+
*/
|
|
39
|
+
export declare class SpawnedProcessBackend implements IpcClientAsync {
|
|
40
|
+
private readonly options;
|
|
41
|
+
private current?;
|
|
42
|
+
private starting?;
|
|
43
|
+
private destroying;
|
|
44
|
+
/** Set when the process died and respawn is disabled; fails all later calls. */
|
|
45
|
+
private exitError?;
|
|
46
|
+
private readonly respawn;
|
|
47
|
+
private readonly ipcPath;
|
|
48
|
+
private readonly logPath?;
|
|
49
|
+
private constructor();
|
|
50
|
+
static spawn(options: SpawnedProcessBackendOptions): Promise<SpawnedProcessBackend>;
|
|
51
|
+
getIpcPath(): string;
|
|
52
|
+
call(input: Uint8Array): Promise<Uint8Array>;
|
|
53
|
+
sendProcessSignal(signal: NodeJS.Signals): void;
|
|
54
|
+
destroy(): Promise<void>;
|
|
55
|
+
/** Return the live incarnation, lazily respawning one when allowed. */
|
|
56
|
+
private ensureUp;
|
|
57
|
+
/**
|
|
58
|
+
* Convert a failed call into the death of the process when that is what
|
|
59
|
+
* actually happened: the socket breaks before the child's 'exit' event
|
|
60
|
+
* lands, so wait a short grace for exit attribution before giving up and
|
|
61
|
+
* rethrowing the transport error as-is.
|
|
62
|
+
*/
|
|
63
|
+
private attributeCallError;
|
|
64
|
+
/** Spawn the server process and connect to it; kills the child on any failure. */
|
|
65
|
+
private spawnIncarnation;
|
|
66
|
+
/** Handles an incarnation's death: reject/replace state so later calls behave per the respawn policy. */
|
|
67
|
+
private onIncarnationExit;
|
|
68
|
+
private connectClient;
|
|
69
|
+
/** Wrap a spawn/connect failure as IpcSpawnError and point at the captured log. */
|
|
70
|
+
private asSpawnError;
|
|
71
|
+
private cleanupIpcPath;
|
|
72
|
+
}
|