@isdk/tool-electron 1.0.0

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,463 @@
1
+ import { ClientToolTransport, Funcs, ActionName, ServerToolTransport, RpcServerDispatcher, ToolRpcRequest, ToolRpcResponse } from '@isdk/tool-rpc';
2
+ import { IpcMainEvent, WebContents } from 'electron';
3
+ import { IPubSubServerTransport, PubSubServerSession, PubSubCtx, IPubSubClientTransport, PubSubClientStream } from '@isdk/tool-event';
4
+
5
+ /**
6
+ * Bridge type for Electron IPC transports.
7
+ *
8
+ * All four bridge types are the single source of truth for injectable
9
+ * IPC methods used by the renderer-side (`Bridge`, `PubSubBridge`) and
10
+ * main-side (`ServerIpcMain`, `ServerPubSubIpcMain`) transports.
11
+ *
12
+ * Import directly:
13
+ * ```ts
14
+ * import { Bridge, ServerIpcMain, PubSubBridge, ServerPubSubIpcMain } from '@isdk/tool-electron';
15
+ * ```
16
+ */
17
+ /**
18
+ * Renderer-side RPC bridge.
19
+ * Injected into {@link import('./ipc-client').IpcClientToolTransport}
20
+ * to call `ipcRenderer.invoke` via `contextBridge` instead of importing `electron` directly.
21
+ */
22
+ type Bridge = {
23
+ invoke: (channel: string, ...args: any[]) => Promise<any>;
24
+ };
25
+ /**
26
+ * Main-side RPC bridge.
27
+ * Injected into {@link import('./ipc-server').IpcServerToolTransport}
28
+ * to register `ipcMain.handle` handlers via a custom object (e.g. mock).
29
+ */
30
+ type ServerIpcMain = {
31
+ handle(channel: string, handler: (event: any, ...args: any[]) => any): void;
32
+ removeHandler(channel: string): void;
33
+ };
34
+ /**
35
+ * Renderer-side Pub/Sub bridge.
36
+ * Injected into {@link import('./pubsub/electron-client').ElectronClientPubSubTransport}
37
+ * to listen, remove listeners, and send IPC messages for the event bus.
38
+ */
39
+ type PubSubBridge = {
40
+ on(channel: string, listener: (event: any, ...args: any[]) => void): void;
41
+ off(channel: string, listener: (event: any, ...args: any[]) => void): void;
42
+ send(channel: string, ...args: any[]): void;
43
+ };
44
+ /**
45
+ * Main-side Pub/Sub bridge.
46
+ * Injected into {@link import('./pubsub/electron-server').ElectronServerPubSubTransport}
47
+ * to register `ipcMain.on` listeners and clean them up via a custom object.
48
+ */
49
+ type ServerPubSubIpcMain = {
50
+ on(channel: string, listener: (...args: any[]) => void): void;
51
+ removeAllListeners(channel?: string): void;
52
+ };
53
+ type Channels = {
54
+ discover: string;
55
+ rpc: string;
56
+ };
57
+
58
+ /**
59
+ * IpcClientToolTransport — Electron renderer-side RPC transport.
60
+ *
61
+ * Uses `ipcRenderer.invoke` (or an injectable bridge) to call server tools
62
+ * over Electron IPC channels. The `apiUrl` follows the `electron://` URI convention:
63
+ *
64
+ * ```ts
65
+ * const transport = new IpcClientToolTransport('electron://my-app', { bridge });
66
+ * // → channels: my-app:discover, my-app:rpc
67
+ * ```
68
+ *
69
+ * The `electron` scheme is auto-registered with `RpcTransportManager` on import,
70
+ * so `RpcTransportManager.instance.getClient('electron://my-app')` works automatically.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * // preload.ts — expose minimal bridge
75
+ * import { contextBridge, ipcRenderer } from 'electron';
76
+ * contextBridge.exposeInMainWorld('electronIpc', {
77
+ * invoke: (ch: string, ...args: any[]) => ipcRenderer.invoke(ch, ...args),
78
+ * });
79
+ *
80
+ * // renderer.ts
81
+ * const transport = new IpcClientToolTransport('electron://my-app', {
82
+ * bridge: (window as any).electronIpc,
83
+ * });
84
+ * await transport.loadApis();
85
+ * const result = await transport._fetch('greet', { name: 'World' });
86
+ * ```
87
+ */
88
+
89
+ type IpcClientToolTransportOptions = {
90
+ apiUrl?: string;
91
+ /**
92
+ * Optional custom bridge object with an `invoke` method,
93
+ * for use with contextBridge in secure Electron apps.
94
+ * When provided, this bridge's invoke is used instead of directly
95
+ * importing `ipcRenderer`.
96
+ */
97
+ bridge?: Bridge;
98
+ [key: string]: any;
99
+ };
100
+ /**
101
+ * Renderer-side RPC transport over Electron IPC.
102
+ *
103
+ * @param apiUrl - URI in `electron://host` format; defaults to `IPCDefaultChannelName`
104
+ * (see {@link normalizeNamespace} for URI extraction logic).
105
+ * @param options - Optional config.
106
+ * @param options.bridge - Injectable bridge object with `invoke` method.
107
+ * When provided, used instead of directly importing `ipcRenderer`.
108
+ * Required when `contextIsolation` is enabled.
109
+ */
110
+ declare class IpcClientToolTransport extends ClientToolTransport {
111
+ channels: Channels;
112
+ private _bridge?;
113
+ constructor(apiUrl?: string, options?: IpcClientToolTransportOptions);
114
+ /**
115
+ * Mount the transport to a tools class and register with RpcTransportManager.
116
+ * @param toolsClass - The client tools class to mount (e.g., `ClientTools`).
117
+ */
118
+ mount(toolsClass: any): void;
119
+ /**
120
+ * Set the API URL and derive IPC channel names.
121
+ * @param apiUrl - URI in `electron://host` format (e.g., `'electron://my-app'`).
122
+ * The host is extracted as the namespace for channel generation:
123
+ * `electron://my-app` → channels `my-app:discover`, `my-app:rpc`.
124
+ */
125
+ setApiUrl(apiUrl: string): void;
126
+ /** @deprecated Use {@link setApiUrl} instead */
127
+ setApiRoot(apiRoot: string): void;
128
+ /**
129
+ * Load available tool definitions from the server via IPC.
130
+ * Invokes the `{namespace}:discover` channel to fetch the tool manifest.
131
+ * @returns A promise resolving to the tool function definitions.
132
+ * @throws If `apiUrl` has not been set (channels not initialized).
133
+ */
134
+ loadApis(): Promise<Funcs>;
135
+ /**
136
+ * Call a remote tool via IPC.
137
+ * Sends an RPC request on the `{namespace}:rpc` channel and returns the result.
138
+ * Server errors (`{ __error__: true, error: ... }`) are automatically re-thrown as `CommonError`.
139
+ *
140
+ * @param name - Tool name (becomes `toolId` in the IPC payload).
141
+ * @param args - Parameters to pass to the tool.
142
+ * @param act - Optional action name for REST-style sub-resources.
143
+ * @param subName - Optional sub-resource identifier.
144
+ * @param _fetchOptions - Optional fetch options (e.g., `{ signal: AbortSignal }` for cancellation).
145
+ * @returns The tool's return value.
146
+ * @throws {CommonError} If the server returns an error response.
147
+ * @throws {AbortError} If the request is cancelled via `AbortSignal`.
148
+ */
149
+ _fetch(name: string, args?: any, act?: ActionName | string, subName?: any, _fetchOptions?: any): Promise<any>;
150
+ /**
151
+ * Convert an IPC response to a plain object.
152
+ * If the response contains a server error (`{ __error__: true }`),
153
+ * it is re-thrown as a `CommonError`.
154
+ * Otherwise, the response is passed through as-is.
155
+ *
156
+ * @param res - The raw response from the IPC channel.
157
+ * @returns The deserialized result.
158
+ * @throws {CommonError} If the response indicates a server error.
159
+ * @deprecated Use `_fetch` directly, which throws `CommonError` automatically.
160
+ */
161
+ toObject(res: any): Promise<any>;
162
+ }
163
+
164
+ /**
165
+ * Main-process RPC transport over Electron IPC.
166
+ *
167
+ * Registers `ipcMain.handle` listeners for tool discovery and RPC calls.
168
+ * Supports injectable `ipcMain` bridge for testing or custom setups.
169
+ */
170
+ declare class IpcServerToolTransport extends ServerToolTransport {
171
+ private channels;
172
+ private started;
173
+ private _ipcMain;
174
+ /**
175
+ * Creates an IPC server transport.
176
+ * @param options.apiUrl - URI in `electron://host` format (e.g., `'electron://my-app'`).
177
+ * The host is extracted as the namespace for IPC channel generation:
178
+ * `electron://my-app` → channels `my-app:discover`, `my-app:rpc`.
179
+ * Defaults to {@link IPCDefaultChannelName} (`'electron://ai-tool'`).
180
+ * @param options.dispatcher - Optional RpcServerDispatcher for v2.6 dispatch pattern.
181
+ * @param options.ipcMain - Optional custom ipcMain bridge (with `handle` and `removeHandler` methods).
182
+ * When provided, used instead of the direct `electron` import.
183
+ * Useful for testing or custom main-process IPC setups.
184
+ */
185
+ constructor(options?: {
186
+ apiUrl?: string;
187
+ dispatcher?: RpcServerDispatcher;
188
+ ipcMain?: ServerIpcMain;
189
+ [key: string]: any;
190
+ });
191
+ /**
192
+ * Set the API URL and derive IPC channel names.
193
+ * @param apiUrl - URI in `electron://host` format (e.g., `'electron://my-app'`).
194
+ * The host is extracted as the namespace for channel generation:
195
+ * `electron://my-app` → channels `my-app:discover`, `my-app:rpc`.
196
+ */
197
+ setApiUrl(apiUrl: string): void;
198
+ /** @deprecated Use setApiUrl instead */
199
+ setApiRoot(apiRoot: string): void;
200
+ private ensureChannels;
201
+ /**
202
+ * Register an IPC handler for tool discovery.
203
+ * Listens on the `{namespace}:discover` channel and returns the tool manifest.
204
+ * Automatically removes any previous handler for the same channel to prevent duplicates.
205
+ *
206
+ * @param apiPrefix - URI or namespace for the IPC channel (e.g., `'electron://my-app'`).
207
+ * @param handler - Function returning the tool definitions.
208
+ * If the return value has a `toJSON()` method, it is called for serialization.
209
+ */
210
+ addDiscoveryHandler(apiPrefix: string, handler: () => any): void;
211
+ /**
212
+ * Register an IPC handler for RPC calls.
213
+ * Listens on the `{namespace}:rpc` channel and routes incoming requests
214
+ * to the appropriate tool handler. In v2.6, uses the dispatcher for routing
215
+ * when available; falls back to direct tool lookup for backward compatibility.
216
+ *
217
+ * @param apiPrefix - URI or namespace for the IPC channel (e.g., `'electron://my-app'`).
218
+ * @param options - Optional config.
219
+ * @param options.registry - A Tools class (with static `.get()`) or a map-like registry
220
+ * (with `.get()` method). Defaults to `ServerTools`.
221
+ * @param options.tools - Alias for `options.registry`. If both are provided, `tools` takes precedence.
222
+ */
223
+ addRpcHandler(apiPrefix: string, options?: {
224
+ registry?: any;
225
+ tools?: any;
226
+ [key: string]: any;
227
+ }): void;
228
+ protected toRpcRequest(rawReq: any, rawRes?: any): Promise<ToolRpcRequest>;
229
+ protected sendRpcResponse(rpcRes: ToolRpcResponse, rawRes: any): Promise<void>;
230
+ private toErrorResponse;
231
+ /**
232
+ * Start the transport (marks as started).
233
+ * IPC handlers are already registered via `addRpcHandler`/`addDiscoveryHandler`,
234
+ * so this method primarily serves as a lifecycle flag.
235
+ */
236
+ _start(): Promise<void>;
237
+ /**
238
+ * Stop the transport and remove IPC handlers.
239
+ * @param force - If `true`, skip the `started` check and always stop.
240
+ */
241
+ stop(force?: boolean): Promise<void>;
242
+ /**
243
+ * Get the raw IPC main object and channel names.
244
+ * Useful for testing and introspection.
245
+ */
246
+ getRaw(): {
247
+ ipcMain: ServerIpcMain;
248
+ channels: Channels;
249
+ };
250
+ /**
251
+ * Get the listen address (URI) for this transport.
252
+ * Returns the `apiUrl` in `electron://` format (e.g., `'electron://my-app'`),
253
+ * or the default `IPCDefaultChannelName` if not set.
254
+ */
255
+ getListenAddr(): string;
256
+ /**
257
+ * Get the routes exposed by this transport.
258
+ * IPC transports expose a single root route `['/']`.
259
+ */
260
+ getRoutes(): string[];
261
+ }
262
+
263
+ /**
264
+ * ElectronServerPubSubTransport — Electron main-process Pub/Sub transport.
265
+ *
266
+ * Manages client sessions and broadcasts events over Electron IPC channels.
267
+ * Compatible with {@link ElectronClientPubSubTransport} in the renderer.
268
+ *
269
+ * IPC channel naming (derived from the namespace via {@link normalizeNamespace}):
270
+ * - `pubsub-downstream:{ns}` — server → client events
271
+ * - `pubsub-upstream:{ns}` — client → server events
272
+ * - `pubsub-connect:{ns}` — initiate connection
273
+ * - `pubsub-disconnect:{ns}` — disconnect session
274
+ *
275
+ * @example
276
+ * ```ts
277
+ * const pubSubServer = new ElectronServerPubSubTransport('electron://my-app-events');
278
+ * pubSubServer.listen();
279
+ * pubSubServer.publish('server-event', { msg: 'hello' });
280
+ * ```
281
+ */
282
+
283
+ /**
284
+ * Main-process Pub/Sub transport over Electron IPC.
285
+ *
286
+ * @param namespace - Namespace in `electron://` URI format (e.g., `'electron://my-app'`).
287
+ * Used to derive all IPC channel names for PubSub communication.
288
+ * Must be non-empty.
289
+ * @param options - Optional config.
290
+ * @param options.ipcMain - Injectable bridge with `on`, `removeAllListeners` methods.
291
+ * When provided, used instead of directly importing `ipcMain`.
292
+ */
293
+ declare class ElectronServerPubSubTransport implements IPubSubServerTransport {
294
+ readonly name = "electron";
295
+ readonly protocol: "electron";
296
+ private sessions;
297
+ private subscriptions;
298
+ private onConn?;
299
+ private onDis?;
300
+ private onMsg?;
301
+ private channels;
302
+ private _listenersInitialized;
303
+ private _ipcMain;
304
+ constructor(namespace: string, options?: {
305
+ ipcMain?: ServerPubSubIpcMain;
306
+ });
307
+ /**
308
+ * Activates the transport by setting up listeners for IPC events.
309
+ * This method should be called once in the main process during application setup.
310
+ */
311
+ listen(): void;
312
+ /**
313
+ * Connect a client session.
314
+ * Called internally by the IPC listener (registered in {@link listen}) when
315
+ * a client sends a `pubsub-connect:{ns}` message. Creates a `PubSubServerSession`
316
+ * for the new client and stores it for event broadcasting.
317
+ *
318
+ * @param options.req - The `IpcMainEvent` from Electron IPC.
319
+ * @param options.res - The `WebContents` of the client window.
320
+ * @param options.events - Optional initial event subscriptions for the session.
321
+ * @returns The newly created `PubSubServerSession`.
322
+ * @throws If `req` or `res` are not provided.
323
+ */
324
+ connect(options?: {
325
+ req: IpcMainEvent;
326
+ res: WebContents;
327
+ events?: string[];
328
+ }): PubSubServerSession;
329
+ private handleDisconnect;
330
+ /**
331
+ * Subscribe a client session to one or more events.
332
+ * Events must be subscribed before they can be received by the client.
333
+ * @param session - The client session to subscribe.
334
+ * @param events - Array of event names to subscribe to.
335
+ */
336
+ subscribe(session: PubSubServerSession, events: string[]): void;
337
+ /**
338
+ * Unsubscribe a client session from one or more events.
339
+ * @param session - The client session to unsubscribe.
340
+ * @param events - Array of event names to unsubscribe from.
341
+ */
342
+ unsubscribe(session: PubSubServerSession, events: string[]): void;
343
+ /**
344
+ * Publish an event to connected clients.
345
+ * If `target.clientId` is provided, only the specified client(s) receive the event.
346
+ * Otherwise, the event is broadcast to all subscribed clients.
347
+ *
348
+ * @param event - The event name to publish.
349
+ * @param data - The event payload.
350
+ * @param target - Optional target specification for directed delivery.
351
+ * @param target.clientId - A single client ID or array of client IDs to target.
352
+ * @param ctx - Optional PubSub context.
353
+ */
354
+ publish(event: string, data: any, target?: {
355
+ clientId?: string | string[];
356
+ }, ctx?: PubSubCtx): void;
357
+ /**
358
+ * Register a callback for new client connections.
359
+ * @param cb - Callback receiving the new `PubSubServerSession`.
360
+ */
361
+ onConnection(cb: (s: PubSubServerSession) => void): void;
362
+ /**
363
+ * Register a callback for client disconnections.
364
+ * @param cb - Callback receiving the disconnected `PubSubServerSession`.
365
+ */
366
+ onDisconnect(cb: (s: PubSubServerSession) => void): void;
367
+ /**
368
+ * Register a callback for incoming messages from clients.
369
+ * @param cb - Callback receiving the session, event name, data, and optional context.
370
+ */
371
+ onMessage(cb: (session: PubSubServerSession, event: string, data: any, ctx?: PubSubCtx) => void): void;
372
+ /**
373
+ * Clean up all IPC listeners and session state.
374
+ * Removes `connect`, `disconnect`, and `upstream` IPC listeners,
375
+ * clears all sessions and subscriptions.
376
+ * Should be called when the transport is no longer needed (e.g., during app shutdown).
377
+ */
378
+ cleanup(): void;
379
+ /**
380
+ * Find a client session by its IPC request event.
381
+ * @param req - The `IpcMainEvent` to look up.
382
+ * @returns The corresponding `PubSubServerSession`, or `undefined` if not found.
383
+ */
384
+ getSessionFromReq(req: IpcMainEvent): PubSubServerSession | undefined;
385
+ }
386
+
387
+ /**
388
+ * ElectronClientPubSubTransport — Electron renderer-side Pub/Sub transport.
389
+ *
390
+ * Uses `ipcRenderer` (or an injectable bridge) to send and receive real-time
391
+ * events over Electron IPC PubSub channels.
392
+ *
393
+ * IPC channel naming (derived from the namespace via {@link normalizeNamespace}):
394
+ * - `pubsub-downstream:{ns}` — server → client events
395
+ * - `pubsub-upstream:{ns}` — client → server events
396
+ * - `pubsub-connect:{ns}` — initiate connection
397
+ * - `pubsub-disconnect:{ns}` — disconnect session
398
+ *
399
+ * @example
400
+ * ```ts
401
+ * const pubsub = new ElectronClientPubSubTransport('electron://my-app-events', {
402
+ * bridge: { on: ipcRenderer.on, off: ipcRenderer.off, send: ipcRenderer.send },
403
+ * });
404
+ * const stream = pubsub.connect('my-app-events');
405
+ * stream.on('server-event', (data) => console.log('received:', data));
406
+ * ```
407
+ */
408
+
409
+ /**
410
+ * Renderer-side Pub/Sub transport over Electron IPC.
411
+ *
412
+ * @param apiRoot - Namespace for IPC channel generation. Supports `electron://` URI format
413
+ * (e.g., `'electron://my-app'` → namespace `my-app`). Defaults to empty string
414
+ * (call {@link setApiRoot} before {@link connect}).
415
+ * @param options - Optional config.
416
+ * @param options.bridge - Injectable bridge with `on`, `off`, and `send` methods.
417
+ * When provided, used instead of directly importing `ipcRenderer`.
418
+ */
419
+ declare class ElectronClientPubSubTransport implements IPubSubClientTransport {
420
+ private apiRoot;
421
+ private channels?;
422
+ private ipcListenerAttached;
423
+ private allListeners;
424
+ private _bridge?;
425
+ constructor(apiRoot?: string, options?: {
426
+ bridge?: PubSubBridge;
427
+ });
428
+ /**
429
+ * Set the API root (namespace) and derive IPC channel names.
430
+ * @param apiRoot - Namespace in `electron://` URI format (e.g., `'electron://my-app'`).
431
+ * The host is extracted as the namespace for PubSub channel generation.
432
+ */
433
+ setApiRoot(apiRoot: string): void;
434
+ private onMessage;
435
+ private ensureIpcListener;
436
+ /**
437
+ * Establish a PubSub connection stream.
438
+ * Sends a `pubsub-connect:{ns}` IPC message and returns a `PubSubClientStream`
439
+ * for subscribing to events and sending messages upstream.
440
+ *
441
+ * @param toolName - Logical connection name (not used for channel routing;
442
+ * channels are derived from the constructor's `apiRoot`).
443
+ * @param params - Optional connection parameters (e.g., `{ events: ['event-a', 'event-b'] }`
444
+ * for initial event subscriptions).
445
+ * @returns A `PubSubClientStream` instance.
446
+ */
447
+ connect(toolName: string, params?: any): PubSubClientStream;
448
+ /**
449
+ * Disconnect a PubSub stream.
450
+ * Delegates to `stream.close()` which sends a `pubsub-disconnect:{ns}` IPC message
451
+ * and cleans up local event listeners.
452
+ * @param stream - The stream to disconnect.
453
+ */
454
+ disconnect(stream: PubSubClientStream): void;
455
+ /**
456
+ * Clean up all IPC listeners and event subscriptions.
457
+ * Removes the downstream IPC listener and clears all registered event handlers.
458
+ * Should be called when the transport is no longer needed (e.g., when the window closes).
459
+ */
460
+ cleanup(): Promise<void>;
461
+ }
462
+
463
+ export { type Bridge, type Channels, ElectronClientPubSubTransport, ElectronServerPubSubTransport, IpcClientToolTransport, IpcServerToolTransport, type PubSubBridge, type ServerIpcMain, type ServerPubSubIpcMain };