Skip to content

Commit bbae5b4

Browse files
committed
add ipc of main and nextjs
1 parent 6b7c7b8 commit bbae5b4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+554
-73
lines changed

apps/desktop/electron.vite.config.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
2+
import { resolve } from 'node:path';
23

34
export default defineConfig({
45
main: {
56
build: {
67
outDir: 'dist/main',
78
},
89
plugins: [externalizeDepsPlugin({})],
10+
resolve: {
11+
alias: {
12+
'@lobechat/web': resolve(__dirname, '../../src'),
13+
},
14+
},
915
},
1016
preload: {
1117
build: {

apps/desktop/src/main/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { BrowserWindow, app, protocol, shell } from 'electron';
33
import { createHandler } from 'next-electron-rsc';
44
import path, { join } from 'node:path';
55

6+
// import {lambdaRouter} from "@lobechat/web/server/routers/lambda";
7+
68
const isDev = process.env.NODE_ENV === 'development';
79
const appPath = app.getAppPath();
810
const localhostUrl = 'http://localhost:3010'; // must match Next.js dev server

apps/desktop/tsconfig.node.json

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
{
2-
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
3-
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"],
42
"compilerOptions": {
53
"composite": true,
4+
"paths": {
5+
"@lobechat/web/*": ["../../src/*"]
6+
},
7+
"baseUrl": ".",
68
"types": ["electron-vite/node"]
7-
}
9+
},
10+
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
11+
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"]
812
}

apps/desktop/tsconfig.web.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
3-
"include": ["src/renderer/**/*.ts", "src/preload/*.d.ts"],
42
"compilerOptions": {
53
"composite": true
6-
}
4+
},
5+
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
6+
"include": ["src/preload/*.d.ts"]
77
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
"@icons-pack/react-simple-icons": "9.6.0",
131131
"@khmyznikov/pwa-install": "0.3.9",
132132
"@langchain/community": "^0.3.22",
133+
"@lobechat/electron-server-ipc-client": "workspace:*",
133134
"@lobechat/web-crawler": "workspace:*",
134135
"@lobehub/charts": "^1.12.0",
135136
"@lobehub/chat-plugin-sdk": "^1.32.4",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "@lobechat/electron-server-ipc-client",
3+
"version": "1.0.0",
4+
"private": true,
5+
"main": "src/index.ts",
6+
"types": "src/index.ts"
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './ipcClient';
2+
export * from './type';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
// ElectronIpcClient.test.ts
2+
import fs from 'node:fs';
3+
import net from 'node:net';
4+
import os from 'node:os';
5+
import path from 'node:path';
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7+
8+
import { ElectronIpcClient } from './ipcClient';
9+
import { ElectronIPCMethods } from './type';
10+
11+
// Mock node modules
12+
vi.mock('node:fs');
13+
vi.mock('node:net');
14+
vi.mock('node:os');
15+
vi.mock('node:path');
16+
17+
describe('ElectronIpcClient', () => {
18+
// Mock data
19+
const mockTempDir = '/mock/temp/dir';
20+
const mockSocketInfoPath = '/mock/temp/dir/lobechat-electron-ipc-info.json';
21+
const mockSocketInfo = { socketPath: '/mock/socket/path' };
22+
23+
// Mock socket
24+
const mockSocket = {
25+
on: vi.fn(),
26+
write: vi.fn(),
27+
end: vi.fn(),
28+
};
29+
30+
beforeEach(() => {
31+
// Use fake timers
32+
vi.useFakeTimers();
33+
34+
// Reset all mocks
35+
vi.resetAllMocks();
36+
37+
// Setup common mocks
38+
vi.mocked(os.tmpdir).mockReturnValue(mockTempDir);
39+
vi.mocked(path.join).mockImplementation((...args) => args.join('/'));
40+
vi.mocked(net.createConnection).mockReturnValue(mockSocket as unknown as net.Socket);
41+
42+
// Mock console methods
43+
vi.spyOn(console, 'error').mockImplementation(() => {});
44+
vi.spyOn(console, 'log').mockImplementation(() => {});
45+
});
46+
47+
afterEach(() => {
48+
vi.restoreAllMocks();
49+
vi.useRealTimers();
50+
});
51+
52+
describe('initialization', () => {
53+
it('should initialize with socket path from info file if it exists', () => {
54+
// Setup
55+
vi.mocked(fs.existsSync).mockReturnValue(true);
56+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSocketInfo));
57+
58+
// Execute
59+
new ElectronIpcClient();
60+
61+
// Verify
62+
expect(fs.existsSync).toHaveBeenCalledWith(mockSocketInfoPath);
63+
expect(fs.readFileSync).toHaveBeenCalledWith(mockSocketInfoPath, 'utf8');
64+
});
65+
66+
it('should initialize with default socket path if info file does not exist', () => {
67+
// Setup
68+
vi.mocked(fs.existsSync).mockReturnValue(false);
69+
70+
// Execute
71+
new ElectronIpcClient();
72+
73+
// Verify
74+
expect(fs.existsSync).toHaveBeenCalledWith(mockSocketInfoPath);
75+
expect(fs.readFileSync).not.toHaveBeenCalled();
76+
77+
// Test platform-specific behavior
78+
const originalPlatform = process.platform;
79+
Object.defineProperty(process, 'platform', { value: 'win32' });
80+
new ElectronIpcClient();
81+
Object.defineProperty(process, 'platform', { value: originalPlatform });
82+
});
83+
84+
it('should handle initialization errors gracefully', () => {
85+
// Setup - Mock the error
86+
vi.mocked(fs.existsSync).mockImplementation(() => {
87+
throw new Error('Mock file system error');
88+
});
89+
90+
// Execute
91+
new ElectronIpcClient();
92+
93+
// Verify
94+
expect(console.error).toHaveBeenCalledWith(
95+
'Failed to initialize IPC client:',
96+
expect.objectContaining({ message: 'Mock file system error' }),
97+
);
98+
});
99+
});
100+
101+
describe('connection and request handling', () => {
102+
let client: ElectronIpcClient;
103+
104+
beforeEach(() => {
105+
// Setup a client with a known socket path
106+
vi.mocked(fs.existsSync).mockReturnValue(true);
107+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSocketInfo));
108+
client = new ElectronIpcClient();
109+
110+
// Reset socket mocks for each test
111+
mockSocket.on.mockReset();
112+
mockSocket.write.mockReset();
113+
114+
// Default implementation for socket.on
115+
mockSocket.on.mockImplementation((event, callback) => {
116+
return mockSocket;
117+
});
118+
119+
// Default implementation for socket.write
120+
mockSocket.write.mockImplementation((data, callback) => {
121+
if (callback) callback();
122+
return true;
123+
});
124+
});
125+
126+
it('should handle connection errors', async () => {
127+
// Start request - but don't await it yet
128+
const requestPromise = client.sendRequest(ElectronIPCMethods.GET_DATABASE_PATH);
129+
130+
// Find the error event handler
131+
const errorCallArgs = mockSocket.on.mock.calls.find((call) => call[0] === 'error');
132+
if (errorCallArgs && typeof errorCallArgs[1] === 'function') {
133+
const errorHandler = errorCallArgs[1];
134+
135+
// Trigger the error handler
136+
errorHandler(new Error('Connection error'));
137+
}
138+
139+
// Now await the promise
140+
await expect(requestPromise).rejects.toThrow('Connection error');
141+
});
142+
143+
it('should handle write errors', async () => {
144+
// Setup connection callback
145+
let connectionCallback: Function | undefined;
146+
vi.mocked(net.createConnection).mockImplementation((path, callback) => {
147+
connectionCallback = callback as Function;
148+
return mockSocket as unknown as net.Socket;
149+
});
150+
151+
// Setup write to fail
152+
mockSocket.write.mockImplementation((data, callback) => {
153+
if (callback) callback(new Error('Write error'));
154+
return true;
155+
});
156+
157+
// Start request
158+
const requestPromise = client.sendRequest(ElectronIPCMethods.GET_DATABASE_PATH);
159+
160+
// Simulate connection established
161+
if (connectionCallback) connectionCallback();
162+
163+
// Now await the promise
164+
await expect(requestPromise).rejects.toThrow('Write error');
165+
});
166+
});
167+
168+
describe('API methods', () => {
169+
let client: ElectronIpcClient;
170+
171+
beforeEach(() => {
172+
// Setup a client with a known socket path
173+
vi.mocked(fs.existsSync).mockReturnValue(true);
174+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSocketInfo));
175+
client = new ElectronIpcClient();
176+
177+
// Mock sendRequest method
178+
vi.spyOn(client, 'sendRequest').mockImplementation((method, params) => {
179+
if (method === ElectronIPCMethods.GET_DATABASE_PATH) {
180+
return Promise.resolve('/path/to/database');
181+
} else if (method === ElectronIPCMethods.GET_USER_DATA_PATH) {
182+
return Promise.resolve('/path/to/user/data');
183+
}
184+
return Promise.reject(new Error('Unknown method'));
185+
});
186+
});
187+
188+
it('should get database path correctly', async () => {
189+
const result = await client.getDatabasePath();
190+
expect(result).toBe('/path/to/database');
191+
expect(client.sendRequest).toHaveBeenCalledWith(ElectronIPCMethods.GET_DATABASE_PATH);
192+
});
193+
194+
it('should get user data path correctly', async () => {
195+
const result = await client.getUserDataPath();
196+
expect(result).toBe('/path/to/user/data');
197+
expect(client.sendRequest).toHaveBeenCalledWith(ElectronIPCMethods.GET_USER_DATA_PATH);
198+
});
199+
});
200+
201+
describe('close method', () => {
202+
let client: ElectronIpcClient;
203+
204+
beforeEach(() => {
205+
// Setup a client with a known socket path
206+
vi.mocked(fs.existsSync).mockReturnValue(true);
207+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSocketInfo));
208+
client = new ElectronIpcClient();
209+
210+
// Setup socket.on
211+
mockSocket.on.mockImplementation((event, callback) => {
212+
return mockSocket;
213+
});
214+
});
215+
216+
it('should close the socket connection', async () => {
217+
// Setup connection callback
218+
let connectionCallback: Function | undefined;
219+
vi.mocked(net.createConnection).mockImplementation((path, callback) => {
220+
connectionCallback = callback as Function;
221+
return mockSocket as unknown as net.Socket;
222+
});
223+
224+
// Start a request to establish connection (but don't wait for it)
225+
const requestPromise = client
226+
.sendRequest(ElectronIPCMethods.GET_DATABASE_PATH)
227+
.catch(() => {}); // Ignore any errors
228+
229+
// Simulate connection
230+
if (connectionCallback) connectionCallback();
231+
232+
// Close the connection
233+
client.close();
234+
235+
// Verify
236+
expect(mockSocket.end).toHaveBeenCalled();
237+
});
238+
239+
it('should handle close when not connected', () => {
240+
// Close without connecting
241+
client.close();
242+
243+
// Verify no errors
244+
expect(mockSocket.end).not.toHaveBeenCalled();
245+
});
246+
});
247+
});

0 commit comments

Comments
 (0)