@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.
@@ -0,0 +1,182 @@
1
+ // In-process UDS transport tests: UdsIpcServer + UdsIpcClient round-trips,
2
+ // zero-length responses, disconnect handling and oversized-frame rejection.
3
+ // Run via `yarn test` (node --test against the compiled dest/ output).
4
+ import { test } from "node:test";
5
+ import * as assert from "node:assert/strict";
6
+ import * as net from "node:net";
7
+ import * as fs from "node:fs";
8
+ import * as os from "node:os";
9
+ import * as path from "node:path";
10
+ import { UdsIpcClient } from "./uds_client.js";
11
+ import { UdsIpcServer } from "./uds_server.js";
12
+ function tmpSocketPath(tag) {
13
+ return path.join(os.tmpdir(), `ipc_ts_test_${tag}_${process.pid}.sock`);
14
+ }
15
+ test("echo round-trip", async () => {
16
+ const socketPath = tmpSocketPath("echo");
17
+ const server = await UdsIpcServer.listen(socketPath, (_id, req) => req);
18
+ const client = await UdsIpcClient.connect(socketPath);
19
+ try {
20
+ const payload = new Uint8Array([1, 2, 3, 4, 5]);
21
+ const resp = await client.call(payload);
22
+ assert.deepEqual(resp, payload);
23
+ // Pipelined calls resolve FIFO.
24
+ const [a, b] = await Promise.all([
25
+ client.call(new Uint8Array([7])),
26
+ client.call(new Uint8Array([8, 9])),
27
+ ]);
28
+ assert.deepEqual(a, new Uint8Array([7]));
29
+ assert.deepEqual(b, new Uint8Array([8, 9]));
30
+ }
31
+ finally {
32
+ await client.destroy();
33
+ await server.close();
34
+ }
35
+ assert.equal(fs.existsSync(socketPath), false, "socket unlinked on close");
36
+ });
37
+ test("socket file is chmod 0600", async () => {
38
+ const socketPath = tmpSocketPath("chmod");
39
+ const server = await UdsIpcServer.listen(socketPath, (_id, req) => req);
40
+ try {
41
+ const mode = fs.statSync(socketPath).mode & 0o777;
42
+ assert.equal(mode, 0o600);
43
+ }
44
+ finally {
45
+ await server.close();
46
+ }
47
+ });
48
+ test("zero-length response resolves (not a hang/error)", async () => {
49
+ const socketPath = tmpSocketPath("zlen");
50
+ const server = await UdsIpcServer.listen(socketPath, () => new Uint8Array(0));
51
+ const client = await UdsIpcClient.connect(socketPath);
52
+ try {
53
+ const resp = await client.call(new Uint8Array([42]));
54
+ assert.equal(resp.length, 0);
55
+ }
56
+ finally {
57
+ await client.destroy();
58
+ await server.close();
59
+ }
60
+ });
61
+ test("disconnect rejects pending calls and fails fast afterwards", async () => {
62
+ const socketPath = tmpSocketPath("disc");
63
+ // Raw server that accepts, reads, then kills the connection without
64
+ // responding.
65
+ const rawServer = net.createServer((conn) => {
66
+ conn.once("data", () => conn.destroy());
67
+ });
68
+ await new Promise((resolve) => rawServer.listen(socketPath, () => resolve()));
69
+ const client = await UdsIpcClient.connect(socketPath);
70
+ try {
71
+ await assert.rejects(client.call(new Uint8Array([1])));
72
+ // Socket is dead — further calls fail fast instead of queueing.
73
+ await assert.rejects(client.call(new Uint8Array([2])), /closed/);
74
+ }
75
+ finally {
76
+ await client.destroy();
77
+ rawServer.close();
78
+ fs.rmSync(socketPath, { force: true });
79
+ }
80
+ });
81
+ test("client rejects oversized frame from server", async () => {
82
+ const socketPath = tmpSocketPath("oversize_cli");
83
+ // Raw server that answers any request with a corrupt 0xFFFFFFFF length
84
+ // prefix.
85
+ const rawServer = net.createServer((conn) => {
86
+ conn.once("data", () => {
87
+ const bogus = Buffer.allocUnsafe(4);
88
+ bogus.writeUInt32LE(0xffffffff, 0);
89
+ conn.write(bogus);
90
+ });
91
+ });
92
+ await new Promise((resolve) => rawServer.listen(socketPath, () => resolve()));
93
+ const client = await UdsIpcClient.connect(socketPath);
94
+ try {
95
+ await assert.rejects(client.call(new Uint8Array([1])), /oversized frame/);
96
+ }
97
+ finally {
98
+ await client.destroy();
99
+ rawServer.close();
100
+ fs.rmSync(socketPath, { force: true });
101
+ }
102
+ });
103
+ test("client fails all pending calls on a response with an unknown request id", async () => {
104
+ const socketPath = tmpSocketPath("unknown_id");
105
+ // Raw server that answers with a well-formed frame whose request id matches
106
+ // nothing the client sent — the correlation-desync case.
107
+ const rawServer = net.createServer((conn) => {
108
+ conn.once("data", () => {
109
+ const frame = Buffer.allocUnsafe(12 + 1);
110
+ frame.writeUInt32LE(1 + 8, 0);
111
+ frame.writeBigUInt64LE(0xdeadbeefn, 4);
112
+ frame.writeUInt8(42, 12);
113
+ conn.write(frame);
114
+ });
115
+ });
116
+ await new Promise((resolve) => rawServer.listen(socketPath, () => resolve()));
117
+ const client = await UdsIpcClient.connect(socketPath);
118
+ try {
119
+ await assert.rejects(client.call(new Uint8Array([1])), /unknown request id/);
120
+ }
121
+ finally {
122
+ await client.destroy();
123
+ rawServer.close();
124
+ fs.rmSync(socketPath, { force: true });
125
+ }
126
+ });
127
+ test("client fails loudly on an id-less (old-protocol) frame", async () => {
128
+ const socketPath = tmpSocketPath("idless");
129
+ // Raw server speaking the pre-envelope-id protocol: [4B len][payload] with
130
+ // len < 8.
131
+ const rawServer = net.createServer((conn) => {
132
+ conn.once("data", () => {
133
+ const frame = Buffer.allocUnsafe(4 + 1);
134
+ frame.writeUInt32LE(1, 0);
135
+ frame.writeUInt8(42, 4);
136
+ conn.write(frame);
137
+ });
138
+ });
139
+ await new Promise((resolve) => rawServer.listen(socketPath, () => resolve()));
140
+ const client = await UdsIpcClient.connect(socketPath);
141
+ try {
142
+ await assert.rejects(client.call(new Uint8Array([1])), /protocol mismatch/);
143
+ }
144
+ finally {
145
+ await client.destroy();
146
+ rawServer.close();
147
+ fs.rmSync(socketPath, { force: true });
148
+ }
149
+ });
150
+ test("server drops connection on oversized frame", async () => {
151
+ const socketPath = tmpSocketPath("oversize_srv");
152
+ const server = await UdsIpcServer.listen(socketPath, (_id, req) => req);
153
+ const conn = net.createConnection(socketPath);
154
+ try {
155
+ await new Promise((resolve, reject) => {
156
+ conn.once("connect", () => resolve());
157
+ conn.once("error", reject);
158
+ });
159
+ const bogus = Buffer.allocUnsafe(4);
160
+ bogus.writeUInt32LE(0xffffffff, 0);
161
+ conn.write(bogus);
162
+ await new Promise((resolve, reject) => {
163
+ const timer = setTimeout(() => reject(new Error("server did not close the connection")), 5000);
164
+ conn.once("close", () => {
165
+ clearTimeout(timer);
166
+ resolve();
167
+ });
168
+ conn.once("error", () => {
169
+ /* RST is fine — close follows */
170
+ });
171
+ });
172
+ }
173
+ finally {
174
+ conn.destroy();
175
+ await server.close();
176
+ }
177
+ });
178
+ test("connect times out against a bound-but-unresponsive path", async () => {
179
+ const socketPath = tmpSocketPath("noaccept");
180
+ fs.rmSync(socketPath, { force: true });
181
+ await assert.rejects(UdsIpcClient.connect(socketPath, { connectTimeoutMs: 300 }), /timed out/);
182
+ });
@@ -0,0 +1,42 @@
1
+ import * as net from "node:net";
2
+ import { IpcClientAsync } from "./types.js";
3
+ export interface UdsIpcClientConnectOptions {
4
+ /** Mark the socket as unref'd so it doesn't keep the Node event loop alive when idle. */
5
+ unref?: boolean;
6
+ /**
7
+ * Retry budget (ms) for the initial connect when the server has bound the
8
+ * path but not yet called listen(). Set to 0 to fail immediately on
9
+ * ECONNREFUSED. Default CONNECT_RETRY_BUDGET_MS (5000).
10
+ */
11
+ connectTimeoutMs?: number;
12
+ }
13
+ /**
14
+ * Async IPC client over a Unix Domain Socket. Wire format matches the C++
15
+ * ipc::IpcServer/IpcClient socket transport: 4-byte little-endian length
16
+ * prefix, 8-byte little-endian request id, then the msgpack payload (the
17
+ * length counts the id plus the payload), per direction.
18
+ *
19
+ * Supports pipelining: each call carries a unique request id which the
20
+ * server echoes on the response, so responses are paired to callers by id
21
+ * and the server may complete requests in any order. Ids start at a random
22
+ * point per connection.
23
+ */
24
+ export declare class UdsIpcClient implements IpcClientAsync {
25
+ private conn;
26
+ private buffer;
27
+ private pending;
28
+ private nextRequestId;
29
+ private destroyed;
30
+ /** Set once the socket has errored/closed; new calls fail fast. */
31
+ private closed;
32
+ private constructor();
33
+ static connect(socketPath: string, opts?: UdsIpcClientConnectOptions): Promise<UdsIpcClient>;
34
+ /** Number of in-flight calls awaiting a response. */
35
+ get inflight(): number;
36
+ /** Underlying socket — exposed for ref/unref control (event-loop tuning). */
37
+ get socket(): net.Socket;
38
+ call(input: Uint8Array): Promise<Uint8Array>;
39
+ destroy(): Promise<void>;
40
+ private onData;
41
+ private failAll;
42
+ }
@@ -0,0 +1,183 @@
1
+ import * as net from "node:net";
2
+ import { IpcTransportError } from "./errors.js";
3
+ import { CONNECT_RETRY_BUDGET_MS, MAX_FRAME_SIZE, } from "./types.js";
4
+ /**
5
+ * Async IPC client over a Unix Domain Socket. Wire format matches the C++
6
+ * ipc::IpcServer/IpcClient socket transport: 4-byte little-endian length
7
+ * prefix, 8-byte little-endian request id, then the msgpack payload (the
8
+ * length counts the id plus the payload), per direction.
9
+ *
10
+ * Supports pipelining: each call carries a unique request id which the
11
+ * server echoes on the response, so responses are paired to callers by id
12
+ * and the server may complete requests in any order. Ids start at a random
13
+ * point per connection.
14
+ */
15
+ export class UdsIpcClient {
16
+ conn;
17
+ buffer = Buffer.alloc(0);
18
+ pending = new Map();
19
+ nextRequestId = (BigInt(Math.floor(Math.random() * 0xffffffff)) << 16n) + 1n;
20
+ destroyed = false;
21
+ /** Set once the socket has errored/closed; new calls fail fast. */
22
+ closed = false;
23
+ constructor(conn) {
24
+ this.conn = conn;
25
+ conn.on("data", (chunk) => this.onData(chunk));
26
+ conn.on("error", (err) => this.failAll(new IpcTransportError(`UdsIpcClient: socket error: ${err.message}`, {
27
+ cause: err,
28
+ })));
29
+ conn.on("close", () => this.failAll(new IpcTransportError("socket closed")));
30
+ }
31
+ static async connect(socketPath, opts) {
32
+ const conn = await connectWithRetry(socketPath, opts?.connectTimeoutMs ?? CONNECT_RETRY_BUDGET_MS);
33
+ conn.setNoDelay(true);
34
+ if (opts?.unref)
35
+ conn.unref();
36
+ return new UdsIpcClient(conn);
37
+ }
38
+ /** Number of in-flight calls awaiting a response. */
39
+ get inflight() {
40
+ return this.pending.size;
41
+ }
42
+ /** Underlying socket — exposed for ref/unref control (event-loop tuning). */
43
+ get socket() {
44
+ return this.conn;
45
+ }
46
+ async call(input) {
47
+ if (this.destroyed) {
48
+ throw new IpcTransportError("UdsIpcClient: call() after destroy()");
49
+ }
50
+ if (this.closed) {
51
+ throw new IpcTransportError("UdsIpcClient: call() on a closed/errored socket");
52
+ }
53
+ return new Promise((resolve, reject) => {
54
+ const requestId = this.nextRequestId++;
55
+ this.pending.set(requestId, { resolve, reject });
56
+ const header = Buffer.allocUnsafe(12);
57
+ header.writeUInt32LE(input.length + 8, 0); // length counts id + payload
58
+ header.writeBigUInt64LE(requestId, 4);
59
+ this.conn.write(header);
60
+ this.conn.write(input);
61
+ });
62
+ }
63
+ async destroy() {
64
+ this.destroyed = true;
65
+ this.conn.removeAllListeners();
66
+ this.conn.destroy();
67
+ this.failAll(new IpcTransportError("UdsIpcClient destroyed"));
68
+ }
69
+ onData(chunk) {
70
+ this.buffer =
71
+ this.buffer.length === 0
72
+ ? Buffer.from(chunk)
73
+ : Buffer.concat([this.buffer, chunk]);
74
+ while (this.buffer.length >= 4) {
75
+ const len = this.buffer.readUInt32LE(0);
76
+ if (len > MAX_FRAME_SIZE) {
77
+ // Corrupt/malicious frame — close instead of buffering up to the
78
+ // claimed size.
79
+ this.conn.destroy();
80
+ this.failAll(new IpcTransportError(`UdsIpcClient: oversized frame (${len} bytes exceeds MAX_FRAME_SIZE)`));
81
+ return;
82
+ }
83
+ if (len < 8) {
84
+ // Shorter than the request-id field: the server speaks the id-less
85
+ // protocol. Fail loudly instead of misparsing.
86
+ this.conn.destroy();
87
+ this.failAll(new IpcTransportError(`UdsIpcClient: ${len}-byte frame is shorter than the request-id field — ` +
88
+ "IPC protocol mismatch (envelope ids); update the peer binary/package"));
89
+ return;
90
+ }
91
+ if (this.buffer.length < 4 + len)
92
+ return;
93
+ const requestId = this.buffer.readBigUInt64LE(4);
94
+ const payload = this.buffer.subarray(12, 4 + len);
95
+ this.buffer = this.buffer.subarray(4 + len);
96
+ const next = this.pending.get(requestId);
97
+ if (next) {
98
+ this.pending.delete(requestId);
99
+ next.resolve(new Uint8Array(payload));
100
+ }
101
+ else {
102
+ // A response that pairs with no pending call means the stream's
103
+ // correlation is broken — fail everything loudly rather than
104
+ // continuing on a connection we can no longer trust.
105
+ this.conn.destroy();
106
+ this.failAll(new IpcTransportError(`UdsIpcClient: response for unknown request id ${requestId} — protocol desync`));
107
+ return;
108
+ }
109
+ }
110
+ }
111
+ failAll(err) {
112
+ this.closed = true;
113
+ const pending = [...this.pending.values()];
114
+ this.pending.clear();
115
+ for (const p of pending)
116
+ p.reject(err);
117
+ }
118
+ }
119
+ /**
120
+ * Connect to `socketPath`, retrying "server not ready" errors until
121
+ * `timeoutMs` elapses: ENOENT (socket file not created yet), ECONNREFUSED
122
+ * (the window between the server's bind() and listen()), EAGAIN (Linux
123
+ * reports this for a UDS connect when the accept backlog is momentarily
124
+ * full), and ECONNRESET (a connect racing the server's accept loop under
125
+ * connection churn). Other errors fail immediately. Each attempt is also
126
+ * capped at the remaining budget, so a bound-but-never-accepting server
127
+ * cannot hang the connect past the deadline.
128
+ */
129
+ async function connectWithRetry(socketPath, timeoutMs) {
130
+ const deadline = Date.now() + timeoutMs;
131
+ let attempt = 0;
132
+ let lastErr;
133
+ while (true) {
134
+ try {
135
+ const remainingMs = Math.max(1, deadline - Date.now());
136
+ return await attemptConnect(socketPath, remainingMs);
137
+ }
138
+ catch (err) {
139
+ lastErr = err;
140
+ const code = err.code;
141
+ if (code !== "ECONNREFUSED" &&
142
+ code !== "ECONNRESET" &&
143
+ code !== "ENOENT" &&
144
+ code !== "ETIMEDOUT" &&
145
+ code !== "EAGAIN") {
146
+ throw new IpcTransportError(`UdsIpcClient: connect failed: ${lastErr.message}`, { cause: lastErr });
147
+ }
148
+ if (Date.now() >= deadline) {
149
+ throw new IpcTransportError(`UdsIpcClient: connect timed out: ${lastErr.message}`, { cause: lastErr });
150
+ }
151
+ const delay = Math.min(50, 5 * 2 ** attempt++);
152
+ await new Promise((resolve) => setTimeout(resolve, delay));
153
+ }
154
+ }
155
+ }
156
+ function attemptConnect(socketPath, timeoutMs) {
157
+ return new Promise((resolve, reject) => {
158
+ const conn = net.createConnection(socketPath);
159
+ const cleanup = () => {
160
+ conn.removeListener("connect", onConnect);
161
+ conn.removeListener("error", onError);
162
+ clearTimeout(timer);
163
+ };
164
+ const onError = (err) => {
165
+ cleanup();
166
+ conn.destroy();
167
+ reject(err);
168
+ };
169
+ const onConnect = () => {
170
+ cleanup();
171
+ resolve(conn);
172
+ };
173
+ const timer = setTimeout(() => {
174
+ cleanup();
175
+ conn.destroy();
176
+ const err = new Error(`connect attempt timed out after ${timeoutMs}ms`);
177
+ err.code = "ETIMEDOUT";
178
+ reject(err);
179
+ }, timeoutMs);
180
+ conn.once("connect", onConnect);
181
+ conn.once("error", onError);
182
+ });
183
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Handler signature mirrors the C++ ipc::IpcServer::Handler: receive raw
3
+ * bytes, return raw bytes. msgpack decode/encode and command dispatch are
4
+ * the caller's responsibility (or the codegen's, when a generated dispatcher
5
+ * is wired in).
6
+ */
7
+ export type IpcServerHandler = (clientId: number, request: Uint8Array) => Promise<Uint8Array> | Uint8Array;
8
+ /**
9
+ * UDS server with the same wire format as UdsIpcClient and the C++
10
+ * ipc::IpcServer socket transport: 4-byte LE length prefix, 8-byte LE request
11
+ * id (echoed on the response), then the payload; the length counts the id
12
+ * plus the payload. Accepts multiple concurrent connections; handler
13
+ * invocations are serialised per-connection.
14
+ *
15
+ * Signal handling is the caller's responsibility (unlike the C++ server's
16
+ * install_default_signal_handlers); the socket file is unlinked on close()
17
+ * and best-effort on process exit.
18
+ */
19
+ export declare class UdsIpcServer {
20
+ private socketPath;
21
+ private server;
22
+ private nextClientId;
23
+ private connections;
24
+ private readonly unlinkOnExit;
25
+ private constructor();
26
+ static listen(socketPath: string, handler: IpcServerHandler): Promise<UdsIpcServer>;
27
+ close(): Promise<void>;
28
+ private handleConnection;
29
+ }
@@ -0,0 +1,135 @@
1
+ import * as net from "node:net";
2
+ import * as fs from "node:fs";
3
+ import { MAX_FRAME_SIZE } from "./types.js";
4
+ /**
5
+ * UDS server with the same wire format as UdsIpcClient and the C++
6
+ * ipc::IpcServer socket transport: 4-byte LE length prefix, 8-byte LE request
7
+ * id (echoed on the response), then the payload; the length counts the id
8
+ * plus the payload. Accepts multiple concurrent connections; handler
9
+ * invocations are serialised per-connection.
10
+ *
11
+ * Signal handling is the caller's responsibility (unlike the C++ server's
12
+ * install_default_signal_handlers); the socket file is unlinked on close()
13
+ * and best-effort on process exit.
14
+ */
15
+ export class UdsIpcServer {
16
+ socketPath;
17
+ server;
18
+ nextClientId = 0;
19
+ connections = new Set();
20
+ unlinkOnExit = () => {
21
+ try {
22
+ fs.unlinkSync(this.socketPath);
23
+ }
24
+ catch {
25
+ /* may already be gone */
26
+ }
27
+ };
28
+ constructor(server, socketPath) {
29
+ this.socketPath = socketPath;
30
+ this.server = server;
31
+ }
32
+ static async listen(socketPath, handler) {
33
+ try {
34
+ fs.unlinkSync(socketPath);
35
+ }
36
+ catch {
37
+ /* socket file may not exist; ignore */
38
+ }
39
+ const server = net.createServer();
40
+ const instance = new UdsIpcServer(server, socketPath);
41
+ server.on("connection", (conn) => instance.handleConnection(conn, handler));
42
+ await new Promise((resolve, reject) => {
43
+ const onError = (err) => {
44
+ server.removeListener("listening", onListening);
45
+ reject(err);
46
+ };
47
+ const onListening = () => {
48
+ server.removeListener("error", onError);
49
+ resolve();
50
+ };
51
+ server.once("error", onError);
52
+ server.once("listening", onListening);
53
+ server.listen(socketPath);
54
+ });
55
+ // Restrict the socket to the owner, matching the C++ server (and the
56
+ // 0600 mode used for SHM segments).
57
+ fs.chmodSync(socketPath, 0o600);
58
+ // Best-effort cleanup if the process exits without close().
59
+ process.on("exit", instance.unlinkOnExit);
60
+ return instance;
61
+ }
62
+ async close() {
63
+ // Force-close live connections (matching the C++ server's shutdown) so close()
64
+ // resolves promptly instead of blocking until every client happens to disconnect.
65
+ for (const conn of this.connections) {
66
+ conn.destroy();
67
+ }
68
+ this.connections.clear();
69
+ await new Promise((resolve) => this.server.close(() => resolve()));
70
+ process.removeListener("exit", this.unlinkOnExit);
71
+ try {
72
+ fs.unlinkSync(this.socketPath);
73
+ }
74
+ catch {
75
+ /* may already be gone */
76
+ }
77
+ }
78
+ handleConnection(conn, handler) {
79
+ const clientId = this.nextClientId++;
80
+ this.connections.add(conn);
81
+ conn.on("close", () => this.connections.delete(conn));
82
+ let buffer = Buffer.alloc(0);
83
+ let chain = Promise.resolve();
84
+ conn.on("data", (chunk) => {
85
+ buffer =
86
+ buffer.length === 0
87
+ ? Buffer.from(chunk)
88
+ : Buffer.concat([buffer, chunk]);
89
+ while (buffer.length >= 4) {
90
+ const len = buffer.readUInt32LE(0);
91
+ if (len > MAX_FRAME_SIZE) {
92
+ // Corrupt/malicious frame — drop the connection instead of
93
+ // buffering up to the claimed size.
94
+ conn.destroy(new Error(`UdsIpcServer: oversized frame (${len} bytes exceeds MAX_FRAME_SIZE)`));
95
+ return;
96
+ }
97
+ if (len < 8) {
98
+ // Shorter than the request-id field: the peer speaks the id-less
99
+ // protocol. Drop the connection with a clear reason.
100
+ conn.destroy(new Error(`UdsIpcServer: ${len}-byte frame is shorter than the request-id field — ` +
101
+ "IPC protocol mismatch (envelope ids); update the peer binary/package"));
102
+ return;
103
+ }
104
+ if (buffer.length < 4 + len)
105
+ break;
106
+ const requestId = buffer.readBigUInt64LE(4);
107
+ // Copy into a standalone Buffer (not a subarray view, and not a plain Uint8Array): handlers
108
+ // decode with msgpackr, which relies on Buffer semantics for correct string/binary decoding.
109
+ const payload = Buffer.from(buffer.subarray(12, 4 + len));
110
+ buffer = buffer.subarray(4 + len);
111
+ const prev = chain;
112
+ chain = (async () => {
113
+ await prev;
114
+ try {
115
+ const resp = await handler(clientId, payload);
116
+ const header = Buffer.allocUnsafe(12);
117
+ header.writeUInt32LE(resp.length + 8, 0); // length counts id + payload
118
+ header.writeBigUInt64LE(requestId, 4);
119
+ conn.write(header);
120
+ conn.write(resp);
121
+ }
122
+ catch (err) {
123
+ conn.destroy(err);
124
+ }
125
+ })();
126
+ void chain.catch(() => {
127
+ /* errors already handled by destroying the connection */
128
+ });
129
+ }
130
+ });
131
+ conn.on("error", () => {
132
+ /* swallowed — clients reconnect */
133
+ });
134
+ }
135
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@aztec-foundation/ipc-runtime",
3
+ "packageManager": "yarn@4.13.0",
4
+ "version": "0.0.1-commit.b66364b",
5
+ "type": "module",
6
+ "main": "dest/index.js",
7
+ "types": "dest/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dest/index.d.ts",
11
+ "import": "./dest/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json",
16
+ "clean": "rm -rf dest",
17
+ "test": "tsc -p tsconfig.json && node --test dest/*.test.js"
18
+ },
19
+ "files": [
20
+ "dest",
21
+ "src",
22
+ "build"
23
+ ],
24
+ "devDependencies": {
25
+ "@types/node": "^22",
26
+ "typescript": "^5.6.3"
27
+ }
28
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,46 @@
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
+ constructor(
13
+ message: string,
14
+ public readonly retry: boolean,
15
+ options?: { cause?: unknown },
16
+ ) {
17
+ super(message, options);
18
+ this.name = new.target.name;
19
+ }
20
+ }
21
+
22
+ /** The connection to the server broke while calls were in flight or before they could be sent. */
23
+ export class IpcTransportError extends IpcError {
24
+ constructor(message: string, options?: { cause?: unknown }) {
25
+ super(message, /*retry=*/ true, options);
26
+ }
27
+ }
28
+
29
+ /** The spawned server process exited; carries the exit cause and, when captured, the log path. */
30
+ export class IpcProcessExitedError extends IpcError {
31
+ constructor(
32
+ message: string,
33
+ public readonly code: number | null,
34
+ public readonly signal: NodeJS.Signals | null,
35
+ public readonly logPath?: string,
36
+ ) {
37
+ super(message, /*retry=*/ true);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * The server process could not be started. Environmental failures (spawn
43
+ * raced a loaded machine, a wedged process hit the connect backstop) are
44
+ * retryable; configuration failures (binary not found) are not.
45
+ */
46
+ export class IpcSpawnError extends IpcError {}
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ export type { IpcClientAsync, IpcClientSync } from "./types.js";
2
+ export {
3
+ MAX_FRAME_SIZE,
4
+ CONNECT_RETRY_BUDGET_MS,
5
+ DEFAULT_RING_SIZE,
6
+ SOCKET_BACKLOG,
7
+ DEFAULT_CALL_TIMEOUT_NS,
8
+ } from "./types.js";
9
+ export {
10
+ IpcError,
11
+ IpcTransportError,
12
+ IpcProcessExitedError,
13
+ IpcSpawnError,
14
+ } from "./errors.js";
15
+ export {
16
+ SpawnedProcessBackend,
17
+ type SpawnedProcessBackendOptions,
18
+ type SpawnedTransport,
19
+ } from "./spawned_backend.js";
20
+ export { UdsIpcClient, type UdsIpcClientConnectOptions } from "./uds_client.js";
21
+ export { UdsIpcServer, type IpcServerHandler } from "./uds_server.js";
22
+ export {
23
+ NapiShmSyncClient,
24
+ NapiShmAsyncClient,
25
+ createNapiShmSyncClient,
26
+ createNapiShmAsyncClient,
27
+ type NapiMsgpackClientSync,
28
+ type NapiMsgpackClientAsync,
29
+ } from "./shm_client.js";
30
+ export {
31
+ findIpcRuntimeNapi,
32
+ loadIpcRuntimeNapi,
33
+ type Platform,
34
+ } from "./native_loader.js";