@oldzy/conduit-electron-adapter 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Conduit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # Conduit Electron Adapter
2
+
3
+ Electron IPC adapter for [@oldzy/conduit](https://github.com/oldzy/conduit), enabling seamless communication between Electron's main and renderer processes.
4
+
5
+ ## Features
6
+
7
+ - 🔌 IPC-based communication between main and renderer processes
8
+ - 🎯 Request/Response pattern with UUID tracking
9
+ - 📡 Support for streaming responses
10
+ - 🚫 Request cancellation support
11
+ - 💉 Automatic dependency injection for Electron App and IpcMain
12
+ - 🔄 AbortController integration for async operations
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @oldzy/conduit-electron-adapter
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### Main Process
23
+
24
+ ```typescript
25
+ import { Application } from '@oldzy/conduit';
26
+ import { ElectronAdapter } from '@oldzy/conduit-electron-adapter';
27
+
28
+ const app = new Application();
29
+ const electronAdapter = new ElectronAdapter();
30
+
31
+ app.useAdapter(electronAdapter);
32
+
33
+ // Register your handlers...
34
+ await app.build();
35
+ ```
36
+
37
+ ### Preload Script
38
+
39
+ Configure your Electron window to use the preload script:
40
+
41
+ ```typescript
42
+ import { BrowserWindow } from 'electron';
43
+ import path from 'path';
44
+
45
+ // Use require.resolve to get the preload path from node_modules
46
+ const preloadPath = require.resolve('@oldzy/conduit-electron-adapter/preload');
47
+
48
+ const mainWindow = new BrowserWindow({
49
+ webPreferences: {
50
+ preload: preloadPath,
51
+ contextIsolation: true,
52
+ nodeIntegration: false
53
+ }
54
+ });
55
+ ```
56
+
57
+ The preload script automatically:
58
+ - Exposes `electronService` to the renderer via `contextBridge`
59
+ - Handles all IPC listeners for streaming, errors, and cancellation
60
+ - Manages request timeout (30 seconds)
61
+ - Cleans up listeners automatically
62
+
63
+ ### Renderer Process
64
+
65
+ ```typescript
66
+ // TypeScript: add types to window
67
+ declare global {
68
+ interface Window {
69
+ electronService: IElectronService;
70
+ }
71
+ }
72
+
73
+ // Simple request/response
74
+ const response = await window.electronService.send({
75
+ uuid: crypto.randomUUID(),
76
+ // ... your request data
77
+ });
78
+
79
+ // Streaming request with callback
80
+ await window.electronService.send(
81
+ {
82
+ uuid: crypto.randomUUID(),
83
+ // ... your request data
84
+ },
85
+ (chunk) => {
86
+ console.log('Received chunk:', chunk);
87
+ }
88
+ );
89
+
90
+ // Cancel a request
91
+ await window.electronService.cancel('request-uuid');
92
+ ```
93
+
94
+ ## API
95
+
96
+ ### ElectronAdapter (Main Process)
97
+
98
+ The adapter automatically:
99
+ - Registers `conduit:send` IPC handler for sending requests
100
+ - Registers `conduit:cancel` IPC handler for cancelling requests
101
+ - Injects `ElectronApp` (app instance) and `IpcMain` into the service container
102
+ - Manages streaming responses via IPC events
103
+ - Handles request cancellation with AbortController
104
+
105
+ ### IElectronService (Renderer Process)
106
+
107
+ ```typescript
108
+ interface IElectronService {
109
+ send: (
110
+ request: BaseRequest,
111
+ onData?: (response: BaseResponse) => void
112
+ ) => Promise<BaseResponse | void>;
113
+ cancel: (requestUuid: string) => Promise<void>;
114
+ }
115
+ ```
116
+
117
+ #### Methods
118
+
119
+ - **`send(request, onData?)`** - Send a request to the main process
120
+ - Returns the response for simple requests
121
+ - Calls `onData` callback for each chunk in streaming responses
122
+ - Resolves when streaming is complete
123
+ - Automatically handles timeout (30s) and cleanup
124
+
125
+ - **`cancel(requestUuid)`** - Cancel a pending request by UUID
126
+
127
+ ### IPC Channels
128
+
129
+ Internal channels (handled automatically by the preload script):
130
+ - `conduit:send` - Send a request (invoke)
131
+ - `conduit:cancel` - Cancel a pending request (invoke)
132
+ - `conduit:response:data:{uuid}` - Streaming response data (listener)
133
+ - `conduit:response:complete:{uuid}` - Stream completed (listener)
134
+ - `conduit:response:error:{uuid}` - Error occurred (listener)
135
+ - `conduit:response:cancelled:{uuid}` - Request was cancelled (listener)
136
+
137
+ ### Types
138
+
139
+ ```typescript
140
+ interface ErrorResponse {
141
+ success: false;
142
+ requestUuid: string;
143
+ timestamp: Date;
144
+ error: string;
145
+ stack?: string;
146
+ }
147
+ ```
148
+
149
+ ## License
150
+
151
+ MIT
@@ -0,0 +1,25 @@
1
+ import { IApplicationAdapter, ServiceCollection, Application } from '@oldzy/conduit';
2
+ export { IElectronService } from './preload.mjs';
3
+
4
+ declare class ElectronAdapter implements IApplicationAdapter {
5
+ name: string;
6
+ private app?;
7
+ private requestControllers;
8
+ configure(services: ServiceCollection): void | Promise<void>;
9
+ initialize(app: Application): void | Promise<void>;
10
+ private handleRequest;
11
+ private handleStream;
12
+ private handleCancel;
13
+ private createErrorResponse;
14
+ dispose(): void | Promise<void>;
15
+ }
16
+
17
+ interface ErrorResponse {
18
+ success: false;
19
+ requestUuid: string;
20
+ timestamp: Date;
21
+ error: string;
22
+ stack?: string;
23
+ }
24
+
25
+ export { ElectronAdapter, type ErrorResponse };
@@ -0,0 +1,25 @@
1
+ import { IApplicationAdapter, ServiceCollection, Application } from '@oldzy/conduit';
2
+ export { IElectronService } from './preload.js';
3
+
4
+ declare class ElectronAdapter implements IApplicationAdapter {
5
+ name: string;
6
+ private app?;
7
+ private requestControllers;
8
+ configure(services: ServiceCollection): void | Promise<void>;
9
+ initialize(app: Application): void | Promise<void>;
10
+ private handleRequest;
11
+ private handleStream;
12
+ private handleCancel;
13
+ private createErrorResponse;
14
+ dispose(): void | Promise<void>;
15
+ }
16
+
17
+ interface ErrorResponse {
18
+ success: false;
19
+ requestUuid: string;
20
+ timestamp: Date;
21
+ error: string;
22
+ stack?: string;
23
+ }
24
+
25
+ export { ElectronAdapter, type ErrorResponse };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ 'use strict';var electron=require('electron'),conduit=require('@oldzy/conduit');/* conduit - MIT License */
2
+ var c=Object.defineProperty;var h=(o,s,e)=>s in o?c(o,s,{enumerable:true,configurable:true,writable:true,value:e}):o[s]=e;var u=(o,s)=>c(o,"name",{value:s,configurable:true});var a=(o,s,e)=>h(o,typeof s!="symbol"?s+"":s,e);var l=class l{constructor(){a(this,"name","ElectronAdapter");a(this,"app");a(this,"requestControllers",new Map);}configure(s){s.addSingleton("ElectronApp",electron.app),s.addSingleton("IpcMain",electron.ipcMain);}initialize(s){if(this.app=s,!this.app)throw new Error("Application not initialized");electron.ipcMain.handle("conduit:send",async(e,n)=>await this.handleRequest(e,n)),electron.ipcMain.handle("conduit:cancel",async(e,n)=>{await this.handleCancel(e,n);});}async handleRequest(s,e){try{let n=new AbortController;this.requestControllers.set(e.uuid,n);let r=await this.app.send(e);if(r instanceof conduit.BaseResponse)return this.requestControllers.delete(e.uuid),r;await this.handleStream(s,e,r,n);}catch(n){let r=this.createErrorResponse(e.uuid,n);s.sender.send(`conduit:response:error:${e.uuid}`,r),this.requestControllers.delete(e.uuid);}}async handleStream(s,e,n,r){try{for await(let d of n){if(r.signal.aborted){s.sender.send(`conduit:response:cancelled:${e.uuid}`,{success:!1,requestUuid:e.uuid,error:"Request was cancelled"});return}s.sender.send(`conduit:response:data:${e.uuid}`,d);}s.sender.send(`conduit:response:complete:${e.uuid}`);}catch(d){if(r.signal.aborted)s.sender.send(`conduit:response:cancelled:${e.uuid}`,{success:false,requestUuid:e.uuid,error:"Request was cancelled"});else {let p=this.createErrorResponse(e.uuid,d);s.sender.send(`conduit:response:error:${e.uuid}`,p);}}finally{this.requestControllers.delete(e.uuid);}}async handleCancel(s,e){let n=this.requestControllers.get(e);n&&(n.abort(),this.requestControllers.delete(e),s.sender.send(`conduit:response:cancelled:${e}`,{success:false,requestUuid:e,error:"Request cancelled by user"}));}createErrorResponse(s,e){return {success:false,requestUuid:s,timestamp:new Date,error:e instanceof Error?e.message:"Unknown error",stack:e instanceof Error?e.stack:void 0}}dispose(){for(let[s,e]of this.requestControllers.entries())e.abort(),this.requestControllers.delete(s);electron.ipcMain.removeHandler("conduit:send"),electron.ipcMain.removeHandler("conduit:cancel");}};u(l,"ElectronAdapter");var i=l;exports.ElectronAdapter=i;
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import {app,ipcMain}from'electron';import {BaseResponse}from'@oldzy/conduit';/* conduit - MIT License */
2
+ var c=Object.defineProperty;var h=(o,s,e)=>s in o?c(o,s,{enumerable:true,configurable:true,writable:true,value:e}):o[s]=e;var u=(o,s)=>c(o,"name",{value:s,configurable:true});var a=(o,s,e)=>h(o,typeof s!="symbol"?s+"":s,e);var l=class l{constructor(){a(this,"name","ElectronAdapter");a(this,"app");a(this,"requestControllers",new Map);}configure(s){s.addSingleton("ElectronApp",app),s.addSingleton("IpcMain",ipcMain);}initialize(s){if(this.app=s,!this.app)throw new Error("Application not initialized");ipcMain.handle("conduit:send",async(e,n)=>await this.handleRequest(e,n)),ipcMain.handle("conduit:cancel",async(e,n)=>{await this.handleCancel(e,n);});}async handleRequest(s,e){try{let n=new AbortController;this.requestControllers.set(e.uuid,n);let r=await this.app.send(e);if(r instanceof BaseResponse)return this.requestControllers.delete(e.uuid),r;await this.handleStream(s,e,r,n);}catch(n){let r=this.createErrorResponse(e.uuid,n);s.sender.send(`conduit:response:error:${e.uuid}`,r),this.requestControllers.delete(e.uuid);}}async handleStream(s,e,n,r){try{for await(let d of n){if(r.signal.aborted){s.sender.send(`conduit:response:cancelled:${e.uuid}`,{success:!1,requestUuid:e.uuid,error:"Request was cancelled"});return}s.sender.send(`conduit:response:data:${e.uuid}`,d);}s.sender.send(`conduit:response:complete:${e.uuid}`);}catch(d){if(r.signal.aborted)s.sender.send(`conduit:response:cancelled:${e.uuid}`,{success:false,requestUuid:e.uuid,error:"Request was cancelled"});else {let p=this.createErrorResponse(e.uuid,d);s.sender.send(`conduit:response:error:${e.uuid}`,p);}}finally{this.requestControllers.delete(e.uuid);}}async handleCancel(s,e){let n=this.requestControllers.get(e);n&&(n.abort(),this.requestControllers.delete(e),s.sender.send(`conduit:response:cancelled:${e}`,{success:false,requestUuid:e,error:"Request cancelled by user"}));}createErrorResponse(s,e){return {success:false,requestUuid:s,timestamp:new Date,error:e instanceof Error?e.message:"Unknown error",stack:e instanceof Error?e.stack:void 0}}dispose(){for(let[s,e]of this.requestControllers.entries())e.abort(),this.requestControllers.delete(s);ipcMain.removeHandler("conduit:send"),ipcMain.removeHandler("conduit:cancel");}};u(l,"ElectronAdapter");var i=l;export{i as ElectronAdapter};
@@ -0,0 +1,8 @@
1
+ import { BaseRequest, BaseResponse } from '@oldzy/conduit';
2
+
3
+ interface IElectronService {
4
+ send: (request: BaseRequest, onData?: (response: BaseResponse) => void) => Promise<BaseResponse | void>;
5
+ cancel: (requestUuid: string) => Promise<void>;
6
+ }
7
+
8
+ export type { IElectronService };
@@ -0,0 +1,8 @@
1
+ import { BaseRequest, BaseResponse } from '@oldzy/conduit';
2
+
3
+ interface IElectronService {
4
+ send: (request: BaseRequest, onData?: (response: BaseResponse) => void) => Promise<BaseResponse | void>;
5
+ cancel: (requestUuid: string) => Promise<void>;
6
+ }
7
+
8
+ export type { IElectronService };
@@ -0,0 +1,2 @@
1
+ 'use strict';var electron=require('electron');/* conduit - MIT License */
2
+ var m=Object.defineProperty;var c=(e,o)=>m(e,"name",{value:o,configurable:true});function l(e,o){return setTimeout(()=>{e(),o({success:false,error:"The request timed out",timestamp:new Date});},3e4)}c(l,"setTimeoutError");electron.contextBridge.exposeInMainWorld("electronService",{send:c(async(e,o)=>new Promise(async(d,u)=>{let i=c(()=>{clearTimeout(s),electron.ipcRenderer.removeAllListeners(`conduit:response:data:${e.uuid}`),electron.ipcRenderer.removeAllListeners(`conduit:response:complete:${e.uuid}`),electron.ipcRenderer.removeAllListeners(`conduit:response:error:${e.uuid}`),electron.ipcRenderer.removeAllListeners(`conduit:response:cancelled:${e.uuid}`);},"cleanup"),s=l(i,u);electron.ipcRenderer.on(`conduit:response:data:${e.uuid}`,(t,r)=>{clearTimeout(s),s=l(i,u),o&&o(r);}),electron.ipcRenderer.on(`conduit:response:complete:${e.uuid}`,()=>{i(),d();}),electron.ipcRenderer.on(`conduit:response:error:${e.uuid}`,(t,r)=>{i(),u(r);}),electron.ipcRenderer.on(`conduit:response:cancelled:${e.uuid}`,(t,r)=>{i(),u(r);}),electron.ipcRenderer.invoke("conduit:send",e).then(t=>{t&&(i(),d(t));});}),"send"),cancel:c(async e=>electron.ipcRenderer.invoke("conduit:cancel",e),"cancel")});
@@ -0,0 +1,2 @@
1
+ import {contextBridge,ipcRenderer}from'electron';/* conduit - MIT License */
2
+ var m=Object.defineProperty;var c=(e,o)=>m(e,"name",{value:o,configurable:true});function l(e,o){return setTimeout(()=>{e(),o({success:false,error:"The request timed out",timestamp:new Date});},3e4)}c(l,"setTimeoutError");contextBridge.exposeInMainWorld("electronService",{send:c(async(e,o)=>new Promise(async(d,u)=>{let i=c(()=>{clearTimeout(s),ipcRenderer.removeAllListeners(`conduit:response:data:${e.uuid}`),ipcRenderer.removeAllListeners(`conduit:response:complete:${e.uuid}`),ipcRenderer.removeAllListeners(`conduit:response:error:${e.uuid}`),ipcRenderer.removeAllListeners(`conduit:response:cancelled:${e.uuid}`);},"cleanup"),s=l(i,u);ipcRenderer.on(`conduit:response:data:${e.uuid}`,(t,r)=>{clearTimeout(s),s=l(i,u),o&&o(r);}),ipcRenderer.on(`conduit:response:complete:${e.uuid}`,()=>{i(),d();}),ipcRenderer.on(`conduit:response:error:${e.uuid}`,(t,r)=>{i(),u(r);}),ipcRenderer.on(`conduit:response:cancelled:${e.uuid}`,(t,r)=>{i(),u(r);}),ipcRenderer.invoke("conduit:send",e).then(t=>{t&&(i(),d(t));});}),"send"),cancel:c(async e=>ipcRenderer.invoke("conduit:cancel",e),"cancel")});
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@oldzy/conduit-electron-adapter",
3
+ "version": "1.0.0",
4
+ "description": "Electron IPC adapter for @oldzy/conduit - enables seamless communication between main and renderer processes",
5
+ "author": "oldzy",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/oldzy/conduit-electron-adapter.git"
10
+ },
11
+ "main": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "require": "./dist/index.js",
17
+ "import": "./dist/index.mjs"
18
+ },
19
+ "./preload": {
20
+ "types": "./dist/preload.d.ts",
21
+ "require": "./dist/preload.js",
22
+ "import": "./dist/preload.mjs"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsup",
32
+ "build:prod": "cross-env NODE_ENV=production tsup",
33
+ "dev": "tsup --watch",
34
+ "test": "vitest",
35
+ "test:ui": "vitest --ui",
36
+ "test:coverage": "vitest --coverage",
37
+ "test:watch": "vitest --watch",
38
+ "lint": "eslint src",
39
+ "prepublishOnly": "npm run build:prod && npm test"
40
+ },
41
+ "keywords": [
42
+ "conduit",
43
+ "electron",
44
+ "ipc",
45
+ "adapter",
46
+ "electron-adapter",
47
+ "electron-ipc",
48
+ "mediator",
49
+ "request-response",
50
+ "streaming",
51
+ "contextbridge",
52
+ "preload"
53
+ ],
54
+ "devDependencies": {
55
+ "@swc/core": "^1.15.2",
56
+ "@types/node": "^24.10.1",
57
+ "@vitest/coverage-v8": "^4.0.9",
58
+ "@vitest/ui": "^4.0.9",
59
+ "cross-env": "^10.1.0",
60
+ "eslint": "^9.39.1",
61
+ "tsup": "^8.5.1",
62
+ "typescript": "^5.9.3",
63
+ "unplugin-swc": "^1.5.8",
64
+ "vitest": "^4.0.9"
65
+ },
66
+ "dependencies": {
67
+ "@oldzy/conduit": "^1.0.2",
68
+ "electron": "^39.2.7",
69
+ "reflect-metadata": "^0.2.2",
70
+ "uuid": "^13.0.0"
71
+ },
72
+ "engines": {
73
+ "node": ">=18.0.0"
74
+ }
75
+ }