@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,342 @@
1
+ import { spawn } from "node:child_process";
2
+ import { open, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { threadId } from "node:worker_threads";
6
+ import { IpcError, IpcProcessExitedError, IpcSpawnError } from "./errors.js";
7
+ import { createNapiShmAsyncClient } from "./shm_client.js";
8
+ import { UdsIpcClient } from "./uds_client.js";
9
+ // Backstop for a spawned server that is alive but never reaches listen().
10
+ // This is a broken-process detector, not a performance expectation: servers
11
+ // create their socket before any heavy initialization, so the timed window
12
+ // covers only exec + linking + minimal init, and requests issued before the
13
+ // server is fully initialized simply wait in the socket buffer. When the
14
+ // backstop fires, the wedged process is killed rather than orphaned.
15
+ const DEFAULT_CONNECT_TIMEOUT_MS = 60_000;
16
+ // After destroy() sends SIGTERM, how long to wait before escalating to
17
+ // SIGKILL so teardown cannot hang on a server that is stuck before its signal
18
+ // handlers were installed (or is wedged inside them).
19
+ const SIGTERM_GRACE_MS = 5_000;
20
+ // How long a failed call() waits for the child's 'exit' event before deciding
21
+ // the process is still alive. The socket usually breaks before 'exit' lands,
22
+ // so without this grace a death would be misreported as a bare transport error.
23
+ const EXIT_ATTRIBUTION_GRACE_MS = 250;
24
+ let instanceCounter = 0;
25
+ /**
26
+ * An IpcClientAsync backed by a spawned server process. Owns the process
27
+ * lifecycle end to end: spawn, connect (raced against child death, with a
28
+ * kill-on-expiry backstop), death detection, optional lazy respawn, and
29
+ * teardown with SIGTERM→SIGKILL escalation. Failures surface as IpcError
30
+ * subclasses whose `retry` property tells callers whether the operation may
31
+ * be retried; no process or transport state is exposed on the API.
32
+ */
33
+ export class SpawnedProcessBackend {
34
+ options;
35
+ current;
36
+ starting;
37
+ destroying = false;
38
+ /** Set when the process died and respawn is disabled; fails all later calls. */
39
+ exitError;
40
+ respawn;
41
+ ipcPath;
42
+ logPath;
43
+ constructor(options) {
44
+ this.options = options;
45
+ this.respawn = options.respawn ?? false;
46
+ const instanceId = `${options.instancePrefix}-${process.pid}-${threadId}-${instanceCounter++}`;
47
+ // The ipc path is per-backend, not per-incarnation, so getIpcPath() stays
48
+ // stable across respawns for anyone who was handed the path.
49
+ this.ipcPath =
50
+ options.transport === "shm"
51
+ ? `${instanceId}.shm`
52
+ : join(tmpdir(), `${instanceId}.sock`);
53
+ // Same for the log file: respawns append, preserving the death's last words.
54
+ this.logPath = options.logger
55
+ ? undefined
56
+ : join(tmpdir(), `${instanceId}.log`);
57
+ }
58
+ static async spawn(options) {
59
+ if (options.respawn && options.transport === "shm") {
60
+ throw new IpcError(`respawn is not supported over the shm transport`,
61
+ /*retry=*/ false);
62
+ }
63
+ const backend = new SpawnedProcessBackend(options);
64
+ backend.current = await backend.spawnIncarnation();
65
+ return backend;
66
+ }
67
+ getIpcPath() {
68
+ return this.ipcPath;
69
+ }
70
+ async call(input) {
71
+ const incarnation = await this.ensureUp();
72
+ try {
73
+ return await incarnation.client.call(input);
74
+ }
75
+ catch (err) {
76
+ throw await this.attributeCallError(incarnation, err);
77
+ }
78
+ }
79
+ sendProcessSignal(signal) {
80
+ const child = this.current?.child;
81
+ if (child && child.exitCode === null && child.signalCode === null) {
82
+ child.kill(signal);
83
+ }
84
+ }
85
+ async destroy() {
86
+ // Mark intentional teardown so the exit handler doesn't report it as an
87
+ // unexpected death (or trigger a respawn).
88
+ this.destroying = true;
89
+ // A respawn may be mid-flight; let it settle so its child can't leak.
90
+ await this.starting?.then((incarnation) => {
91
+ this.current = incarnation;
92
+ }, () => { });
93
+ const incarnation = this.current;
94
+ this.current = undefined;
95
+ if (!incarnation) {
96
+ return;
97
+ }
98
+ await incarnation.client.destroy();
99
+ const { child } = incarnation;
100
+ let killTimer;
101
+ if (child.exitCode === null && child.signalCode === null) {
102
+ child.kill("SIGTERM");
103
+ killTimer = setTimeout(() => {
104
+ if (child.exitCode === null && child.signalCode === null) {
105
+ child.kill("SIGKILL");
106
+ }
107
+ }, SIGTERM_GRACE_MS);
108
+ }
109
+ await incarnation.exitPromise;
110
+ if (killTimer !== undefined) {
111
+ clearTimeout(killTimer);
112
+ }
113
+ child.stdout?.destroy();
114
+ child.stderr?.destroy();
115
+ child.removeAllListeners();
116
+ await this.cleanupIpcPath();
117
+ }
118
+ /** Return the live incarnation, lazily respawning one when allowed. */
119
+ async ensureUp() {
120
+ if (this.destroying) {
121
+ throw new IpcError(`${this.options.binaryName} backend destroyed`,
122
+ /*retry=*/ false);
123
+ }
124
+ if (this.current) {
125
+ return this.current;
126
+ }
127
+ if (this.exitError) {
128
+ throw this.exitError;
129
+ }
130
+ // Lazy, shared respawn: the first caller after a death starts it, everyone
131
+ // else awaits the same attempt. Lazy (rather than eager-on-exit) so a
132
+ // crashing binary cannot respawn-loop with no one asking for it.
133
+ this.starting ??= this.spawnIncarnation()
134
+ .then((incarnation) => {
135
+ if (this.destroying) {
136
+ // destroy() raced us and already awaited this promise; it owns teardown.
137
+ return incarnation;
138
+ }
139
+ this.current = incarnation;
140
+ return incarnation;
141
+ })
142
+ .finally(() => {
143
+ this.starting = undefined;
144
+ });
145
+ return await this.starting;
146
+ }
147
+ /**
148
+ * Convert a failed call into the death of the process when that is what
149
+ * actually happened: the socket breaks before the child's 'exit' event
150
+ * lands, so wait a short grace for exit attribution before giving up and
151
+ * rethrowing the transport error as-is.
152
+ */
153
+ async attributeCallError(incarnation, err) {
154
+ if (!incarnation.exitInfo && !this.destroying) {
155
+ await Promise.race([
156
+ incarnation.exitPromise,
157
+ new Promise((resolve) => setTimeout(resolve, EXIT_ATTRIBUTION_GRACE_MS)),
158
+ ]);
159
+ }
160
+ if (incarnation.exitInfo && !this.destroying) {
161
+ const { code, signal } = incarnation.exitInfo;
162
+ return new IpcProcessExitedError(`${this.options.binaryName} exited unexpectedly (code=${code}, signal=${signal})` +
163
+ (this.logPath !== undefined ? `; see logs: ${this.logPath}` : ""), code, signal, this.logPath);
164
+ }
165
+ return err;
166
+ }
167
+ /** Spawn the server process and connect to it; kills the child on any failure. */
168
+ async spawnIncarnation() {
169
+ const { options } = this;
170
+ if (options.transport === "uds") {
171
+ await rm(this.ipcPath, { force: true });
172
+ }
173
+ // Without a live logger, capture the child's stdout/stderr to the backend's
174
+ // log file (a plain fd, not a pipe — a pipe would keep the libuv loop
175
+ // referenced and break clean process exit). The path is surfaced on
176
+ // failures so the child's errors are recoverable instead of vanishing.
177
+ // Async fs throughout: this runs on respawn under exactly the machine
178
+ // conditions where a sync open against a struggling disk could stall the
179
+ // whole event loop.
180
+ const logFile = this.logPath !== undefined ? await open(this.logPath, "a") : undefined;
181
+ const child = spawn(options.binaryPath, [
182
+ ...options.ipcPathArgs.map((arg) => arg === "{path}" ? this.ipcPath : arg),
183
+ ...(options.extraArgs ?? []),
184
+ ], {
185
+ stdio: [
186
+ "ignore",
187
+ options.logger ? "pipe" : logFile.fd,
188
+ options.logger ? "pipe" : logFile.fd,
189
+ ],
190
+ env: { ...process.env, ...(options.env ?? {}) },
191
+ });
192
+ if (options.logger) {
193
+ child.stdout?.on("data", (data) => options.logger?.(`[${options.binaryName} stdout] ${data.toString().trimEnd()}`));
194
+ child.stderr?.on("data", (data) => options.logger?.(`[${options.binaryName} stderr] ${data.toString().trimEnd()}`));
195
+ }
196
+ const incarnation = {
197
+ child,
198
+ };
199
+ const exitPromise = new Promise((resolve) => {
200
+ child.on("exit", (code, signal) => {
201
+ incarnation.exitInfo = { code, signal };
202
+ this.onIncarnationExit(incarnation, code, signal);
203
+ resolve();
204
+ });
205
+ });
206
+ incarnation.exitPromise = exitPromise;
207
+ const childReadyFailure = new Promise((_, reject) => {
208
+ // Spawn syscall failures are the one place errno distinguishes a
209
+ // configuration error (missing/non-executable binary → retrying cannot
210
+ // help) from an environmental one.
211
+ child.once("error", (err) => {
212
+ const code = err.code;
213
+ const retry = code !== "ENOENT" && code !== "EACCES";
214
+ reject(new IpcSpawnError(`Failed to spawn ${options.binaryName}: ${err.message}`, retry, { cause: err }));
215
+ });
216
+ child.once("exit", (code, signal) => {
217
+ reject(new IpcSpawnError(`${options.binaryName} exited before IPC connection was ready (code=${code}, signal=${signal})`,
218
+ /*retry=*/ true));
219
+ });
220
+ });
221
+ // Observe immediately: childReadyFailure can reject during the awaits
222
+ // below (spawn failures land on nextTick), before Promise.race attaches
223
+ // its handler — without this it would count as an unhandled rejection.
224
+ childReadyFailure.catch(() => { });
225
+ if (logFile !== undefined) {
226
+ // spawn() dups the fd synchronously; the parent's handle isn't needed.
227
+ // Closed only now, after the 'error'/'exit' listeners are attached: spawn
228
+ // failures are emitted on nextTick, so an await between spawn() and the
229
+ // listeners would let them fire unhandled.
230
+ await logFile.close();
231
+ }
232
+ // Liveness-based startup: wait on the connect for as long as the process
233
+ // is alive (the connect path retries socket-not-ready errors internally),
234
+ // and fail immediately with the real cause if the process dies first. On
235
+ // any failure, reap the child and its ipc path so a failed spawn cannot
236
+ // leak an orphan process still holding sockets or database locks.
237
+ try {
238
+ incarnation.client = await Promise.race([
239
+ this.connectClient(),
240
+ childReadyFailure,
241
+ ]);
242
+ }
243
+ catch (err) {
244
+ if (child.pid !== undefined) {
245
+ // SIGKILL, not SIGTERM: the process never became ready, so it has no
246
+ // state to flush, and a wedged process may not honour SIGTERM.
247
+ if (child.exitCode === null && child.signalCode === null) {
248
+ child.kill("SIGKILL");
249
+ }
250
+ await exitPromise;
251
+ }
252
+ child.stdout?.destroy();
253
+ child.stderr?.destroy();
254
+ child.removeAllListeners();
255
+ await this.cleanupIpcPath();
256
+ throw this.asSpawnError(err);
257
+ }
258
+ return incarnation;
259
+ }
260
+ /** Handles an incarnation's death: reject/replace state so later calls behave per the respawn policy. */
261
+ onIncarnationExit(incarnation, code, signal) {
262
+ // Break the connection so in-flight calls reject rather than wait forever
263
+ // (matters over SHM, where there is no socket to break).
264
+ void incarnation.client?.destroy();
265
+ if (this.destroying || this.current !== incarnation) {
266
+ return;
267
+ }
268
+ this.current = undefined;
269
+ if (!this.respawn) {
270
+ this.exitError = new IpcProcessExitedError(`${this.options.binaryName} exited unexpectedly (code=${code}, signal=${signal})` +
271
+ (this.logPath !== undefined ? `; see logs: ${this.logPath}` : ""), code, signal, this.logPath);
272
+ console.error(this.exitError.message);
273
+ }
274
+ else {
275
+ console.error(`${this.options.binaryName} exited unexpectedly (code=${code}, signal=${signal}); will respawn on next call`);
276
+ }
277
+ }
278
+ async connectClient() {
279
+ const { options } = this;
280
+ const timeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
281
+ if (options.transport === "uds") {
282
+ // UdsIpcClient.connect retries socket-not-ready errors (path not yet
283
+ // created, server not yet accepting, backlog momentarily full) until the
284
+ // budget expires, and fails immediately on hard errors. Process death is
285
+ // raced against this by the caller, so a dead server short-circuits the
286
+ // wait with its real exit cause.
287
+ return await UdsIpcClient.connect(this.ipcPath, {
288
+ connectTimeoutMs: timeoutMs,
289
+ });
290
+ }
291
+ // The SHM client attaches to server-created rings, so creation can race
292
+ // server startup; retry until the backstop expires.
293
+ const deadline = Date.now() + timeoutMs;
294
+ let lastError;
295
+ while (Date.now() <= deadline) {
296
+ try {
297
+ return createNapiShmAsyncClient(this.ipcPath.replace(/\.shm$/, ""), {
298
+ clientId: options.clientId,
299
+ customAddonPath: options.napiPath,
300
+ });
301
+ }
302
+ catch (err) {
303
+ lastError = err;
304
+ await new Promise((resolve) => setTimeout(resolve, 50));
305
+ }
306
+ }
307
+ const message = lastError instanceof Error ? lastError.message : String(lastError);
308
+ throw new IpcSpawnError(`Timed out connecting to ${this.options.binaryName}: ${message}`,
309
+ /*retry=*/ true, { cause: lastError });
310
+ }
311
+ /** Wrap a spawn/connect failure as IpcSpawnError and point at the captured log. */
312
+ asSpawnError(err) {
313
+ const logHint = this.logPath !== undefined ? `; see logs: ${this.logPath}` : "";
314
+ if (err instanceof IpcSpawnError) {
315
+ // Already classified at its source (child spawn 'error', exit-before-ready).
316
+ return new IpcSpawnError(err.message + logHint, err.retry, {
317
+ cause: err.cause ?? err,
318
+ });
319
+ }
320
+ // Everything else reaching here (connect backstop expiry, transient
321
+ // transport errors) is environmental.
322
+ const message = err instanceof Error ? err.message : String(err);
323
+ return new IpcSpawnError(`Failed to start ${this.options.binaryName}: ${message}${logHint}`,
324
+ /*retry=*/ true, { cause: err });
325
+ }
326
+ async cleanupIpcPath() {
327
+ try {
328
+ if (this.options.transport === "uds") {
329
+ await rm(this.ipcPath, { force: true });
330
+ }
331
+ if (this.options.transport === "shm") {
332
+ const shmName = this.ipcPath.replace(/\.shm$/, "");
333
+ for (const suffix of ["_request", "_response"]) {
334
+ await rm(`/dev/shm/${shmName}${suffix}`, { force: true });
335
+ }
336
+ }
337
+ }
338
+ catch {
339
+ // Cleanup is best-effort; the paths live under tmpdir.
340
+ }
341
+ }
342
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,173 @@
1
+ import assert from "node:assert/strict";
2
+ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { after, test } from "node:test";
6
+ import { IpcProcessExitedError, IpcSpawnError } from "./errors.js";
7
+ import { SpawnedProcessBackend } from "./spawned_backend.js";
8
+ const scratch = mkdtempSync(join(tmpdir(), "spawned-backend-test-"));
9
+ after(() => rmSync(scratch, { recursive: true, force: true }));
10
+ // A minimal length-prefixed echo server speaking the UdsIpcClient wire format.
11
+ // Replies to each request with its own pid as a decimal string, so tests can
12
+ // tell process incarnations apart.
13
+ const PID_ECHO_SERVER = join(scratch, "pid_echo_server.cjs");
14
+ writeFileSync(PID_ECHO_SERVER, `
15
+ const net = require('node:net');
16
+ const server = net.createServer(conn => {
17
+ let buf = Buffer.alloc(0);
18
+ conn.on('data', chunk => {
19
+ buf = Buffer.concat([buf, chunk]);
20
+ while (buf.length >= 4) {
21
+ const len = buf.readUInt32LE(0);
22
+ if (buf.length < 4 + len) return;
23
+ const requestId = buf.readBigUInt64LE(4); // frame = [len][8B id][payload]
24
+ buf = buf.subarray(4 + len);
25
+ const payload = Buffer.from(String(process.pid));
26
+ const out = Buffer.alloc(12 + payload.length);
27
+ out.writeUInt32LE(payload.length + 8, 0);
28
+ out.writeBigUInt64LE(requestId, 4);
29
+ payload.copy(out, 12);
30
+ conn.write(out);
31
+ }
32
+ });
33
+ });
34
+ server.listen(process.argv[2]);
35
+ `);
36
+ function shellScript(name, body) {
37
+ const path = join(scratch, name);
38
+ writeFileSync(path, `#!/bin/sh\n${body}\n`);
39
+ chmodSync(path, 0o755);
40
+ return path;
41
+ }
42
+ function spawnPidEcho(opts = {}) {
43
+ return SpawnedProcessBackend.spawn({
44
+ binaryPath: process.execPath,
45
+ binaryName: "pid_echo_server",
46
+ instancePrefix: "pid-echo-test",
47
+ ipcPathArgs: [PID_ECHO_SERVER, "{path}"],
48
+ transport: "uds",
49
+ ...opts,
50
+ });
51
+ }
52
+ async function callPid(backend) {
53
+ return Buffer.from(await backend.call(Uint8Array.from([1]))).toString();
54
+ }
55
+ function processAlive(pid) {
56
+ try {
57
+ process.kill(pid, 0);
58
+ return true;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ async function waitFor(cond, timeoutMs) {
65
+ const deadline = Date.now() + timeoutMs;
66
+ while (Date.now() < deadline) {
67
+ if (cond())
68
+ return true;
69
+ await new Promise((resolve) => setTimeout(resolve, 25));
70
+ }
71
+ return cond();
72
+ }
73
+ test("spawns, round-trips calls, and destroys cleanly", async () => {
74
+ const backend = await spawnPidEcho();
75
+ const pid = await callPid(backend);
76
+ assert.match(pid, /^\d+$/);
77
+ await backend.destroy();
78
+ assert.equal(processAlive(parseInt(pid, 10)), false);
79
+ });
80
+ test("without respawn, death rejects in-flight and later calls with a retryable exit error", async () => {
81
+ const backend = await spawnPidEcho();
82
+ const pid = parseInt(await callPid(backend), 10);
83
+ process.kill(pid, "SIGKILL");
84
+ await waitFor(() => !processAlive(pid), 2_000);
85
+ const err = await callPid(backend).then(() => undefined, (e) => e);
86
+ assert.ok(err instanceof IpcProcessExitedError, `expected IpcProcessExitedError, got ${err}`);
87
+ assert.equal(err.retry, true);
88
+ assert.equal(err.signal, "SIGKILL");
89
+ // Still down: same classified error, no zombie respawn.
90
+ await assert.rejects(callPid(backend), IpcProcessExitedError);
91
+ await backend.destroy();
92
+ });
93
+ test("with respawn, the next call after a death transparently gets a fresh process", async () => {
94
+ const backend = await spawnPidEcho({ respawn: true });
95
+ const firstPid = parseInt(await callPid(backend), 10);
96
+ process.kill(firstPid, "SIGKILL");
97
+ await waitFor(() => !processAlive(firstPid), 2_000);
98
+ // The first call after the death may race the exit event; it is allowed to
99
+ // fail (with a retryable error) or succeed against the respawned process.
100
+ let secondPid;
101
+ try {
102
+ secondPid = parseInt(await callPid(backend), 10);
103
+ }
104
+ catch (err) {
105
+ assert.equal(err.retry, true);
106
+ secondPid = parseInt(await callPid(backend), 10);
107
+ }
108
+ assert.notEqual(secondPid, firstPid);
109
+ assert.equal(processAlive(secondPid), true);
110
+ // getIpcPath is stable across incarnations.
111
+ const path = backend.getIpcPath();
112
+ await backend.destroy();
113
+ assert.equal(processAlive(secondPid), false);
114
+ assert.equal(backend.getIpcPath(), path);
115
+ });
116
+ test("a missing binary fails with retry=false", async () => {
117
+ const err = await SpawnedProcessBackend.spawn({
118
+ binaryPath: join(scratch, "does_not_exist"),
119
+ binaryName: "ghost",
120
+ instancePrefix: "ghost-test",
121
+ ipcPathArgs: ["{path}"],
122
+ transport: "uds",
123
+ }).then(() => undefined, (e) => e);
124
+ assert.ok(err instanceof IpcSpawnError, `expected IpcSpawnError, got ${err}`);
125
+ assert.equal(err.retry, false);
126
+ });
127
+ test("a server dying before listen fails promptly with retry=true and the exit code", async () => {
128
+ const dying = shellScript("dying_server.sh", "exit 7");
129
+ const started = Date.now();
130
+ const err = await SpawnedProcessBackend.spawn({
131
+ binaryPath: dying,
132
+ binaryName: "dying_server",
133
+ instancePrefix: "dying-test",
134
+ ipcPathArgs: ["{path}"],
135
+ transport: "uds",
136
+ }).then(() => undefined, (e) => e);
137
+ assert.ok(err instanceof IpcSpawnError, `expected IpcSpawnError, got ${err}`);
138
+ assert.equal(err.retry, true);
139
+ assert.match(err.message, /code=7/);
140
+ assert.ok(Date.now() - started < 5_000, "failed before the connect backstop");
141
+ });
142
+ test("a wedged server is killed when the connect backstop fires", async () => {
143
+ const pidFile = join(scratch, "wedged.pid");
144
+ const wedged = shellScript("wedged_server.sh", `echo $$ > "${pidFile}"\nexec sleep 600`);
145
+ const err = await SpawnedProcessBackend.spawn({
146
+ binaryPath: wedged,
147
+ binaryName: "wedged_server",
148
+ instancePrefix: "wedged-test",
149
+ ipcPathArgs: ["{path}"],
150
+ transport: "uds",
151
+ connectTimeoutMs: 1_000,
152
+ }).then(() => undefined, (e) => e);
153
+ assert.ok(err instanceof IpcSpawnError, `expected IpcSpawnError, got ${err}`);
154
+ assert.equal(err.retry, true);
155
+ const { readFileSync } = await import("node:fs");
156
+ const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
157
+ assert.equal(await waitFor(() => !processAlive(pid), 5_000), true, "wedged process was reaped");
158
+ });
159
+ test("destroy during a pending respawn does not leak the fresh process", async () => {
160
+ const backend = await spawnPidEcho({ respawn: true });
161
+ const firstPid = parseInt(await callPid(backend), 10);
162
+ process.kill(firstPid, "SIGKILL");
163
+ await waitFor(() => !processAlive(firstPid), 2_000);
164
+ // Trigger the lazy respawn, then destroy while it may still be in flight.
165
+ const pending = callPid(backend).catch(() => undefined);
166
+ await backend.destroy();
167
+ const result = await pending;
168
+ if (result !== undefined) {
169
+ // The respawn won the race and served the call; destroy() must still have
170
+ // reaped the fresh process afterwards.
171
+ assert.equal(await waitFor(() => !processAlive(parseInt(result, 10)), 2_000), true);
172
+ }
173
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Minimal byte-in / byte-out interface that the ipc-codegen-emitted
3
+ * <Service>Api types consume. Both UDS and SHM transports satisfy this.
4
+ */
5
+ export interface IpcClientAsync {
6
+ call(input: Uint8Array): Promise<Uint8Array>;
7
+ destroy(): Promise<void>;
8
+ }
9
+ export interface IpcClientSync {
10
+ call(input: Uint8Array): Uint8Array;
11
+ destroy(): void;
12
+ }
13
+ /**
14
+ * Maximum length-prefix value accepted on receive. A frame claiming more
15
+ * than this is treated as corruption and the connection is closed instead
16
+ * of buffering the claimed size.
17
+ */
18
+ export declare const MAX_FRAME_SIZE: number;
19
+ /**
20
+ * Total budget (ms) for connect() retry loops, covering the window where
21
+ * the server process is still starting up.
22
+ */
23
+ export declare const CONNECT_RETRY_BUDGET_MS = 5000;
24
+ /** Default ring size for SHM transports (per direction, per client). */
25
+ export declare const DEFAULT_RING_SIZE: number;
26
+ /** Default listen backlog for UDS servers. */
27
+ export declare const SOCKET_BACKLOG = 10;
28
+ /** Default per-call timeout: 0 = infinite (matches the C++ client APIs). */
29
+ export declare const DEFAULT_CALL_TIMEOUT_NS = 0;
package/dest/types.js ADDED
@@ -0,0 +1,19 @@
1
+ // Shared transport constants, mirroring cpp/ipc_runtime/constants.hpp —
2
+ // keep the two in sync.
3
+ /**
4
+ * Maximum length-prefix value accepted on receive. A frame claiming more
5
+ * than this is treated as corruption and the connection is closed instead
6
+ * of buffering the claimed size.
7
+ */
8
+ export const MAX_FRAME_SIZE = 256 * 1024 * 1024; // 256 MiB
9
+ /**
10
+ * Total budget (ms) for connect() retry loops, covering the window where
11
+ * the server process is still starting up.
12
+ */
13
+ export const CONNECT_RETRY_BUDGET_MS = 5000;
14
+ /** Default ring size for SHM transports (per direction, per client). */
15
+ export const DEFAULT_RING_SIZE = 4 * 1024 * 1024; // 4 MiB
16
+ /** Default listen backlog for UDS servers. */
17
+ export const SOCKET_BACKLOG = 10;
18
+ /** Default per-call timeout: 0 = infinite (matches the C++ client APIs). */
19
+ export const DEFAULT_CALL_TIMEOUT_NS = 0;
@@ -0,0 +1 @@
1
+ export {};