@holokai/neuron-sdk 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ipc.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ import { T as Transport, S as ServerToNeuronFrame, d as ClientFrame, P as PostPath, N as NeuronLogger, c as TransportCallbacks } from './transport-QqkooS7K.js';
2
+ import 'zod';
3
+
4
+ /**
5
+ * Renderer-side bridge to the BigBrain gateway running in the Electron
6
+ * main process. The SDK does NOT depend on Electron — the renderer's
7
+ * preload script is responsible for constructing a concrete bridge that
8
+ * forwards calls over `ipcRenderer.invoke` and `ipcRenderer.on`, then
9
+ * exposes it via `contextBridge.exposeInMainWorld`.
10
+ *
11
+ * The four-method shape mirrors what the gateway needs:
12
+ *
13
+ * - `attach(neuronId)` — open the inbound channel for this neuron.
14
+ * Resolves with the per-neuron event channel name the bridge will
15
+ * deliver server frames on. Concretely, in Electron main this is
16
+ * where the gateway constructs an `IpcSSEWriter` and stores it
17
+ * against the session.
18
+ * - `subscribe(channel, cb)` — start receiving frames on the channel
19
+ * `attach` handed back. Returns an unsubscribe function.
20
+ * - `detach(neuronId)` — close the inbound channel; the gateway tears
21
+ * down its writer for this neuron.
22
+ * - `invoke(path, frame)` — fire a typed POST. In Electron main this
23
+ * is `app.inject({ method: 'POST', url: path, payload: frame, headers })`
24
+ * so the same Fastify auth/validation chain runs as for HTTP neurons.
25
+ *
26
+ * The bridge is responsible for stamping authentication onto outbound
27
+ * calls — the IPC transport never sees the JWT.
28
+ */
29
+ interface NeuronIpcBridge {
30
+ /**
31
+ * Open the inbound channel for `neuronId`. Returns the per-neuron event
32
+ * channel that `subscribe` should listen on.
33
+ */
34
+ attach(neuronId: string): Promise<{
35
+ channel: string;
36
+ }>;
37
+ /** Close the inbound channel. */
38
+ detach(neuronId: string): Promise<void>;
39
+ /** Subscribe to inbound frames on `channel`. Returns an unsubscribe fn. */
40
+ subscribe(channel: string, cb: (frame: ServerToNeuronFrame) => void): () => void;
41
+ /** Send a typed client frame outbound. Resolves with the parsed body. */
42
+ invoke<T extends ClientFrame>(path: PostPath, frame: T): Promise<unknown>;
43
+ }
44
+ interface IpcTransportOptions {
45
+ /**
46
+ * Concrete bridge — provided by the renderer's preload script. The SDK
47
+ * has no opinion on its construction; it only calls the four methods.
48
+ */
49
+ bridge: NeuronIpcBridge;
50
+ /**
51
+ * Stable per-process identifier. Reused across `start`/`stop` cycles
52
+ * so the gateway can correlate the new attach with the same neuron.
53
+ */
54
+ neuronId: string;
55
+ /** Logger; defaults to a silent no-op. */
56
+ logger?: NeuronLogger;
57
+ }
58
+ /**
59
+ * IPC transport for renderer-process neurons. Differences vs `HttpTransport`:
60
+ *
61
+ * - No reconnect logic — IPC has no transient socket. If the bridge
62
+ * throws on `invoke` or `attach`, the failure surfaces via `onError`
63
+ * and the consumer (typically a `Neuron`) decides what to do.
64
+ * - No auth callback — the renderer never sees the JWT. Main attaches
65
+ * `Authorization` headers when it forwards to Fastify via `inject()`.
66
+ * - No keep-alive — IPC channels are process-local; nothing in between
67
+ * to time out idle connections.
68
+ *
69
+ * Same `Transport` contract as `HttpTransport`, so the rest of the SDK
70
+ * (`Neuron`, `HeartbeatLoop`, `runLease`) works against either without
71
+ * any conditional branching.
72
+ */
73
+ declare class IpcTransport implements Transport {
74
+ private readonly bridge;
75
+ private readonly neuronId;
76
+ private readonly logger;
77
+ private callbacks;
78
+ private connected;
79
+ private unsubscribe;
80
+ constructor(opts: IpcTransportOptions);
81
+ subscribe(callbacks: TransportCallbacks): void;
82
+ isConnected(): boolean;
83
+ start(): Promise<void>;
84
+ stop(): Promise<void>;
85
+ post<T extends ClientFrame>(path: PostPath, frame: T): Promise<unknown>;
86
+ }
87
+
88
+ export { IpcTransport, type IpcTransportOptions, type NeuronIpcBridge };
package/dist/ipc.js ADDED
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ // src/logger.ts
4
+ var noopLogger = {
5
+ debug() {
6
+ },
7
+ info() {
8
+ },
9
+ warn() {
10
+ },
11
+ error() {
12
+ }
13
+ };
14
+
15
+ // src/ipc-transport.ts
16
+ var IpcTransport = class {
17
+ bridge;
18
+ neuronId;
19
+ logger;
20
+ callbacks = {};
21
+ connected = false;
22
+ unsubscribe = null;
23
+ constructor(opts) {
24
+ this.bridge = opts.bridge;
25
+ this.neuronId = opts.neuronId;
26
+ this.logger = opts.logger ?? noopLogger;
27
+ }
28
+ subscribe(callbacks) {
29
+ this.callbacks = callbacks;
30
+ }
31
+ isConnected() {
32
+ return this.connected;
33
+ }
34
+ async start() {
35
+ if (this.connected) return;
36
+ let channel;
37
+ try {
38
+ ({ channel } = await this.bridge.attach(this.neuronId));
39
+ } catch (err) {
40
+ const wrapped = err instanceof Error ? err : new Error(String(err));
41
+ this.callbacks.onError?.(wrapped);
42
+ throw wrapped;
43
+ }
44
+ try {
45
+ this.unsubscribe = this.bridge.subscribe(channel, (frame) => {
46
+ this.callbacks.onFrame?.(frame);
47
+ });
48
+ } catch (err) {
49
+ const wrapped = err instanceof Error ? err : new Error(String(err));
50
+ try {
51
+ await this.bridge.detach(this.neuronId);
52
+ } catch (detachErr) {
53
+ this.logger.warn(
54
+ { err: detachErr, neuronId: this.neuronId },
55
+ "ipc detach during start-failure unwind also failed; main-side writer may leak"
56
+ );
57
+ }
58
+ this.callbacks.onError?.(wrapped);
59
+ throw wrapped;
60
+ }
61
+ this.connected = true;
62
+ this.callbacks.onOpen?.();
63
+ }
64
+ async stop() {
65
+ if (!this.connected) return;
66
+ this.connected = false;
67
+ this.unsubscribe?.();
68
+ this.unsubscribe = null;
69
+ try {
70
+ await this.bridge.detach(this.neuronId);
71
+ } catch (err) {
72
+ this.logger.warn(
73
+ { err, neuronId: this.neuronId },
74
+ "ipc detach failed; continuing shutdown"
75
+ );
76
+ }
77
+ this.callbacks.onClose?.("stopped");
78
+ }
79
+ async post(path, frame) {
80
+ return this.bridge.invoke(path, frame);
81
+ }
82
+ };
83
+
84
+ exports.IpcTransport = IpcTransport;
85
+ //# sourceMappingURL=ipc.js.map
86
+ //# sourceMappingURL=ipc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/logger.ts","../src/ipc-transport.ts"],"names":[],"mappings":"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","file":"ipc.js","sourcesContent":["import type { NeuronLogger } from './types.js';\n\n/**\n * Silent logger used as a default when no consumer provides one. Single\n * shared instance to avoid each module defining its own copy.\n */\nexport const noopLogger: NeuronLogger = {\n debug() {},\n info() {},\n warn() {},\n error() {},\n};\n","import type { PostPath, ServerToNeuronFrame } from '@workflow/neuron-protocol';\n\nimport { noopLogger } from './logger.js';\nimport type { Transport, TransportCallbacks } from './transport.js';\nimport type { ClientFrame, NeuronLogger } from './types.js';\n\n/**\n * Renderer-side bridge to the BigBrain gateway running in the Electron\n * main process. The SDK does NOT depend on Electron — the renderer's\n * preload script is responsible for constructing a concrete bridge that\n * forwards calls over `ipcRenderer.invoke` and `ipcRenderer.on`, then\n * exposes it via `contextBridge.exposeInMainWorld`.\n *\n * The four-method shape mirrors what the gateway needs:\n *\n * - `attach(neuronId)` — open the inbound channel for this neuron.\n * Resolves with the per-neuron event channel name the bridge will\n * deliver server frames on. Concretely, in Electron main this is\n * where the gateway constructs an `IpcSSEWriter` and stores it\n * against the session.\n * - `subscribe(channel, cb)` — start receiving frames on the channel\n * `attach` handed back. Returns an unsubscribe function.\n * - `detach(neuronId)` — close the inbound channel; the gateway tears\n * down its writer for this neuron.\n * - `invoke(path, frame)` — fire a typed POST. In Electron main this\n * is `app.inject({ method: 'POST', url: path, payload: frame, headers })`\n * so the same Fastify auth/validation chain runs as for HTTP neurons.\n *\n * The bridge is responsible for stamping authentication onto outbound\n * calls — the IPC transport never sees the JWT.\n */\nexport interface NeuronIpcBridge {\n /**\n * Open the inbound channel for `neuronId`. Returns the per-neuron event\n * channel that `subscribe` should listen on.\n */\n attach(neuronId: string): Promise<{ channel: string }>;\n /** Close the inbound channel. */\n detach(neuronId: string): Promise<void>;\n /** Subscribe to inbound frames on `channel`. Returns an unsubscribe fn. */\n subscribe(\n channel: string,\n cb: (frame: ServerToNeuronFrame) => void,\n ): () => void;\n /** Send a typed client frame outbound. Resolves with the parsed body. */\n invoke<T extends ClientFrame>(path: PostPath, frame: T): Promise<unknown>;\n}\n\nexport interface IpcTransportOptions {\n /**\n * Concrete bridge — provided by the renderer's preload script. The SDK\n * has no opinion on its construction; it only calls the four methods.\n */\n bridge: NeuronIpcBridge;\n /**\n * Stable per-process identifier. Reused across `start`/`stop` cycles\n * so the gateway can correlate the new attach with the same neuron.\n */\n neuronId: string;\n /** Logger; defaults to a silent no-op. */\n logger?: NeuronLogger;\n}\n\n/**\n * IPC transport for renderer-process neurons. Differences vs `HttpTransport`:\n *\n * - No reconnect logic — IPC has no transient socket. If the bridge\n * throws on `invoke` or `attach`, the failure surfaces via `onError`\n * and the consumer (typically a `Neuron`) decides what to do.\n * - No auth callback — the renderer never sees the JWT. Main attaches\n * `Authorization` headers when it forwards to Fastify via `inject()`.\n * - No keep-alive — IPC channels are process-local; nothing in between\n * to time out idle connections.\n *\n * Same `Transport` contract as `HttpTransport`, so the rest of the SDK\n * (`Neuron`, `HeartbeatLoop`, `runLease`) works against either without\n * any conditional branching.\n */\nexport class IpcTransport implements Transport {\n private readonly bridge: NeuronIpcBridge;\n private readonly neuronId: string;\n private readonly logger: NeuronLogger;\n private callbacks: TransportCallbacks = {};\n private connected = false;\n private unsubscribe: (() => void) | null = null;\n\n constructor(opts: IpcTransportOptions) {\n this.bridge = opts.bridge;\n this.neuronId = opts.neuronId;\n this.logger = opts.logger ?? noopLogger;\n }\n\n subscribe(callbacks: TransportCallbacks): void {\n this.callbacks = callbacks;\n }\n\n isConnected(): boolean {\n return this.connected;\n }\n\n async start(): Promise<void> {\n if (this.connected) return;\n let channel: string;\n try {\n ({ channel } = await this.bridge.attach(this.neuronId));\n } catch (err) {\n const wrapped = err instanceof Error ? err : new Error(String(err));\n this.callbacks.onError?.(wrapped);\n throw wrapped;\n }\n\n // `attach` has succeeded — anything that throws past this point must\n // unwind it, otherwise the main-side writer is registered with no\n // way to reach it. Wrap subscribe in try/catch and best-effort detach\n // on failure.\n try {\n this.unsubscribe = this.bridge.subscribe(channel, (frame) => {\n this.callbacks.onFrame?.(frame);\n });\n } catch (err) {\n const wrapped = err instanceof Error ? err : new Error(String(err));\n try {\n await this.bridge.detach(this.neuronId);\n } catch (detachErr) {\n this.logger.warn(\n { err: detachErr, neuronId: this.neuronId },\n 'ipc detach during start-failure unwind also failed; main-side writer may leak',\n );\n }\n this.callbacks.onError?.(wrapped);\n throw wrapped;\n }\n\n this.connected = true;\n this.callbacks.onOpen?.();\n }\n\n async stop(): Promise<void> {\n if (!this.connected) return;\n this.connected = false;\n this.unsubscribe?.();\n this.unsubscribe = null;\n try {\n await this.bridge.detach(this.neuronId);\n } catch (err) {\n // Detach failure is informational only — the channel is gone from\n // our side either way. Surface for observability and continue.\n this.logger.warn(\n { err, neuronId: this.neuronId },\n 'ipc detach failed; continuing shutdown',\n );\n }\n this.callbacks.onClose?.('stopped');\n }\n\n async post<T extends ClientFrame>(path: PostPath, frame: T): Promise<unknown> {\n return this.bridge.invoke(path, frame);\n }\n}\n"]}
package/dist/ipc.mjs ADDED
@@ -0,0 +1,84 @@
1
+ // src/logger.ts
2
+ var noopLogger = {
3
+ debug() {
4
+ },
5
+ info() {
6
+ },
7
+ warn() {
8
+ },
9
+ error() {
10
+ }
11
+ };
12
+
13
+ // src/ipc-transport.ts
14
+ var IpcTransport = class {
15
+ bridge;
16
+ neuronId;
17
+ logger;
18
+ callbacks = {};
19
+ connected = false;
20
+ unsubscribe = null;
21
+ constructor(opts) {
22
+ this.bridge = opts.bridge;
23
+ this.neuronId = opts.neuronId;
24
+ this.logger = opts.logger ?? noopLogger;
25
+ }
26
+ subscribe(callbacks) {
27
+ this.callbacks = callbacks;
28
+ }
29
+ isConnected() {
30
+ return this.connected;
31
+ }
32
+ async start() {
33
+ if (this.connected) return;
34
+ let channel;
35
+ try {
36
+ ({ channel } = await this.bridge.attach(this.neuronId));
37
+ } catch (err) {
38
+ const wrapped = err instanceof Error ? err : new Error(String(err));
39
+ this.callbacks.onError?.(wrapped);
40
+ throw wrapped;
41
+ }
42
+ try {
43
+ this.unsubscribe = this.bridge.subscribe(channel, (frame) => {
44
+ this.callbacks.onFrame?.(frame);
45
+ });
46
+ } catch (err) {
47
+ const wrapped = err instanceof Error ? err : new Error(String(err));
48
+ try {
49
+ await this.bridge.detach(this.neuronId);
50
+ } catch (detachErr) {
51
+ this.logger.warn(
52
+ { err: detachErr, neuronId: this.neuronId },
53
+ "ipc detach during start-failure unwind also failed; main-side writer may leak"
54
+ );
55
+ }
56
+ this.callbacks.onError?.(wrapped);
57
+ throw wrapped;
58
+ }
59
+ this.connected = true;
60
+ this.callbacks.onOpen?.();
61
+ }
62
+ async stop() {
63
+ if (!this.connected) return;
64
+ this.connected = false;
65
+ this.unsubscribe?.();
66
+ this.unsubscribe = null;
67
+ try {
68
+ await this.bridge.detach(this.neuronId);
69
+ } catch (err) {
70
+ this.logger.warn(
71
+ { err, neuronId: this.neuronId },
72
+ "ipc detach failed; continuing shutdown"
73
+ );
74
+ }
75
+ this.callbacks.onClose?.("stopped");
76
+ }
77
+ async post(path, frame) {
78
+ return this.bridge.invoke(path, frame);
79
+ }
80
+ };
81
+
82
+ export { IpcTransport };
83
+ //# sourceMappingURL=ipc.mjs.map
84
+ //# sourceMappingURL=ipc.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/logger.ts","../src/ipc-transport.ts"],"names":[],"mappings":";AAMO,IAAM,UAAA,GAA2B;AAAA,EACtC,KAAA,GAAQ;AAAA,EAAC,CAAA;AAAA,EACT,IAAA,GAAO;AAAA,EAAC,CAAA;AAAA,EACR,IAAA,GAAO;AAAA,EAAC,CAAA;AAAA,EACR,KAAA,GAAQ;AAAA,EAAC;AACX,CAAA;;;ACmEO,IAAM,eAAN,MAAwC;AAAA,EAC5B,MAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACT,YAAgC,EAAC;AAAA,EACjC,SAAA,GAAY,KAAA;AAAA,EACZ,WAAA,GAAmC,IAAA;AAAA,EAE3C,YAAY,IAAA,EAA2B;AACrC,IAAA,IAAA,CAAK,SAAS,IAAA,CAAK,MAAA;AACnB,IAAA,IAAA,CAAK,WAAW,IAAA,CAAK,QAAA;AACrB,IAAA,IAAA,CAAK,MAAA,GAAS,KAAK,MAAA,IAAU,UAAA;AAAA,EAC/B;AAAA,EAEA,UAAU,SAAA,EAAqC;AAC7C,IAAA,IAAA,CAAK,SAAA,GAAY,SAAA;AAAA,EACnB;AAAA,EAEA,WAAA,GAAuB;AACrB,IAAA,OAAO,IAAA,CAAK,SAAA;AAAA,EACd;AAAA,EAEA,MAAM,KAAA,GAAuB;AAC3B,IAAA,IAAI,KAAK,SAAA,EAAW;AACpB,IAAA,IAAI,OAAA;AACJ,IAAA,IAAI;AACF,MAAA,CAAC,EAAE,SAAQ,GAAI,MAAM,KAAK,MAAA,CAAO,MAAA,CAAO,KAAK,QAAQ,CAAA;AAAA,IACvD,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,OAAA,GAAU,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAClE,MAAA,IAAA,CAAK,SAAA,CAAU,UAAU,OAAO,CAAA;AAChC,MAAA,MAAM,OAAA;AAAA,IACR;AAMA,IAAA,IAAI;AACF,MAAA,IAAA,CAAK,cAAc,IAAA,CAAK,MAAA,CAAO,SAAA,CAAU,OAAA,EAAS,CAAC,KAAA,KAAU;AAC3D,QAAA,IAAA,CAAK,SAAA,CAAU,UAAU,KAAK,CAAA;AAAA,MAChC,CAAC,CAAA;AAAA,IACH,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,OAAA,GAAU,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAClE,MAAA,IAAI;AACF,QAAA,MAAM,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAA;AAAA,MACxC,SAAS,SAAA,EAAW;AAClB,QAAA,IAAA,CAAK,MAAA,CAAO,IAAA;AAAA,UACV,EAAE,GAAA,EAAK,SAAA,EAAW,QAAA,EAAU,KAAK,QAAA,EAAS;AAAA,UAC1C;AAAA,SACF;AAAA,MACF;AACA,MAAA,IAAA,CAAK,SAAA,CAAU,UAAU,OAAO,CAAA;AAChC,MAAA,MAAM,OAAA;AAAA,IACR;AAEA,IAAA,IAAA,CAAK,SAAA,GAAY,IAAA;AACjB,IAAA,IAAA,CAAK,UAAU,MAAA,IAAS;AAAA,EAC1B;AAAA,EAEA,MAAM,IAAA,GAAsB;AAC1B,IAAA,IAAI,CAAC,KAAK,SAAA,EAAW;AACrB,IAAA,IAAA,CAAK,SAAA,GAAY,KAAA;AACjB,IAAA,IAAA,CAAK,WAAA,IAAc;AACnB,IAAA,IAAA,CAAK,WAAA,GAAc,IAAA;AACnB,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAA;AAAA,IACxC,SAAS,GAAA,EAAK;AAGZ,MAAA,IAAA,CAAK,MAAA,CAAO,IAAA;AAAA,QACV,EAAE,GAAA,EAAK,QAAA,EAAU,IAAA,CAAK,QAAA,EAAS;AAAA,QAC/B;AAAA,OACF;AAAA,IACF;AACA,IAAA,IAAA,CAAK,SAAA,CAAU,UAAU,SAAS,CAAA;AAAA,EACpC;AAAA,EAEA,MAAM,IAAA,CAA4B,IAAA,EAAgB,KAAA,EAA4B;AAC5E,IAAA,OAAO,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,IAAA,EAAM,KAAK,CAAA;AAAA,EACvC;AACF","file":"ipc.mjs","sourcesContent":["import type { NeuronLogger } from './types.js';\n\n/**\n * Silent logger used as a default when no consumer provides one. Single\n * shared instance to avoid each module defining its own copy.\n */\nexport const noopLogger: NeuronLogger = {\n debug() {},\n info() {},\n warn() {},\n error() {},\n};\n","import type { PostPath, ServerToNeuronFrame } from '@workflow/neuron-protocol';\n\nimport { noopLogger } from './logger.js';\nimport type { Transport, TransportCallbacks } from './transport.js';\nimport type { ClientFrame, NeuronLogger } from './types.js';\n\n/**\n * Renderer-side bridge to the BigBrain gateway running in the Electron\n * main process. The SDK does NOT depend on Electron — the renderer's\n * preload script is responsible for constructing a concrete bridge that\n * forwards calls over `ipcRenderer.invoke` and `ipcRenderer.on`, then\n * exposes it via `contextBridge.exposeInMainWorld`.\n *\n * The four-method shape mirrors what the gateway needs:\n *\n * - `attach(neuronId)` — open the inbound channel for this neuron.\n * Resolves with the per-neuron event channel name the bridge will\n * deliver server frames on. Concretely, in Electron main this is\n * where the gateway constructs an `IpcSSEWriter` and stores it\n * against the session.\n * - `subscribe(channel, cb)` — start receiving frames on the channel\n * `attach` handed back. Returns an unsubscribe function.\n * - `detach(neuronId)` — close the inbound channel; the gateway tears\n * down its writer for this neuron.\n * - `invoke(path, frame)` — fire a typed POST. In Electron main this\n * is `app.inject({ method: 'POST', url: path, payload: frame, headers })`\n * so the same Fastify auth/validation chain runs as for HTTP neurons.\n *\n * The bridge is responsible for stamping authentication onto outbound\n * calls — the IPC transport never sees the JWT.\n */\nexport interface NeuronIpcBridge {\n /**\n * Open the inbound channel for `neuronId`. Returns the per-neuron event\n * channel that `subscribe` should listen on.\n */\n attach(neuronId: string): Promise<{ channel: string }>;\n /** Close the inbound channel. */\n detach(neuronId: string): Promise<void>;\n /** Subscribe to inbound frames on `channel`. Returns an unsubscribe fn. */\n subscribe(\n channel: string,\n cb: (frame: ServerToNeuronFrame) => void,\n ): () => void;\n /** Send a typed client frame outbound. Resolves with the parsed body. */\n invoke<T extends ClientFrame>(path: PostPath, frame: T): Promise<unknown>;\n}\n\nexport interface IpcTransportOptions {\n /**\n * Concrete bridge — provided by the renderer's preload script. The SDK\n * has no opinion on its construction; it only calls the four methods.\n */\n bridge: NeuronIpcBridge;\n /**\n * Stable per-process identifier. Reused across `start`/`stop` cycles\n * so the gateway can correlate the new attach with the same neuron.\n */\n neuronId: string;\n /** Logger; defaults to a silent no-op. */\n logger?: NeuronLogger;\n}\n\n/**\n * IPC transport for renderer-process neurons. Differences vs `HttpTransport`:\n *\n * - No reconnect logic — IPC has no transient socket. If the bridge\n * throws on `invoke` or `attach`, the failure surfaces via `onError`\n * and the consumer (typically a `Neuron`) decides what to do.\n * - No auth callback — the renderer never sees the JWT. Main attaches\n * `Authorization` headers when it forwards to Fastify via `inject()`.\n * - No keep-alive — IPC channels are process-local; nothing in between\n * to time out idle connections.\n *\n * Same `Transport` contract as `HttpTransport`, so the rest of the SDK\n * (`Neuron`, `HeartbeatLoop`, `runLease`) works against either without\n * any conditional branching.\n */\nexport class IpcTransport implements Transport {\n private readonly bridge: NeuronIpcBridge;\n private readonly neuronId: string;\n private readonly logger: NeuronLogger;\n private callbacks: TransportCallbacks = {};\n private connected = false;\n private unsubscribe: (() => void) | null = null;\n\n constructor(opts: IpcTransportOptions) {\n this.bridge = opts.bridge;\n this.neuronId = opts.neuronId;\n this.logger = opts.logger ?? noopLogger;\n }\n\n subscribe(callbacks: TransportCallbacks): void {\n this.callbacks = callbacks;\n }\n\n isConnected(): boolean {\n return this.connected;\n }\n\n async start(): Promise<void> {\n if (this.connected) return;\n let channel: string;\n try {\n ({ channel } = await this.bridge.attach(this.neuronId));\n } catch (err) {\n const wrapped = err instanceof Error ? err : new Error(String(err));\n this.callbacks.onError?.(wrapped);\n throw wrapped;\n }\n\n // `attach` has succeeded — anything that throws past this point must\n // unwind it, otherwise the main-side writer is registered with no\n // way to reach it. Wrap subscribe in try/catch and best-effort detach\n // on failure.\n try {\n this.unsubscribe = this.bridge.subscribe(channel, (frame) => {\n this.callbacks.onFrame?.(frame);\n });\n } catch (err) {\n const wrapped = err instanceof Error ? err : new Error(String(err));\n try {\n await this.bridge.detach(this.neuronId);\n } catch (detachErr) {\n this.logger.warn(\n { err: detachErr, neuronId: this.neuronId },\n 'ipc detach during start-failure unwind also failed; main-side writer may leak',\n );\n }\n this.callbacks.onError?.(wrapped);\n throw wrapped;\n }\n\n this.connected = true;\n this.callbacks.onOpen?.();\n }\n\n async stop(): Promise<void> {\n if (!this.connected) return;\n this.connected = false;\n this.unsubscribe?.();\n this.unsubscribe = null;\n try {\n await this.bridge.detach(this.neuronId);\n } catch (err) {\n // Detach failure is informational only — the channel is gone from\n // our side either way. Surface for observability and continue.\n this.logger.warn(\n { err, neuronId: this.neuronId },\n 'ipc detach failed; continuing shutdown',\n );\n }\n this.callbacks.onClose?.('stopped');\n }\n\n async post<T extends ClientFrame>(path: PostPath, frame: T): Promise<unknown> {\n return this.bridge.invoke(path, frame);\n }\n}\n"]}