This commit is contained in:
Bingbing
2026-07-15 16:02:15 +08:00
parent bbbf14e5e3
commit 3e22860e21
6 changed files with 146 additions and 14 deletions

View File

@@ -2,7 +2,7 @@ import path from 'node:path';
import type { UtilityProcess } from 'electron';
import { BrowserWindow, MessageChannelMain, utilityProcess } from 'electron';
import type { ChangeListener } from 'insomnia-data';
import type { ChangeBufferEvent, ChangeListener } from 'insomnia-data';
import { isDevelopment } from '../common/constants';
import { PortRpc } from './port-rpc';
@@ -10,11 +10,16 @@ import { PortRpc } from './port-rpc';
let child: UtilityProcess | null = null;
const mainProcessChangeListeners: ChangeListener[] = [];
let deepLinkHandler: ((uri: string) => void) | null = null;
export function registerMainProcessChangeListener(listener: ChangeListener): void {
mainProcessChangeListeners.push(listener);
}
export function registerDeepLinkHandler(handler: (uri: string) => void): void {
deepLinkHandler = handler;
}
function getDataProcessPath(): string {
return path.join(__dirname, 'entry.data.min.js');
}
@@ -76,7 +81,7 @@ export async function spawnDataProcess(dbPath: string): Promise<void> {
mainRpc.attach(
m => port2.postMessage(m),
h => {
port2.on('message', (e: Electron.MessageEvent) => h(e.data as any));
port2.on('message', (e: Electron.MessageEvent) => h(e.data));
port2.start();
},
);
@@ -88,7 +93,9 @@ export async function spawnDataProcess(dbPath: string): Promise<void> {
BrowserWindow.getAllWindows().forEach(w => {
w.webContents.send('db.changes', msg.changes);
});
mainProcessChangeListeners.forEach(listener => listener(msg.changes as any));
mainProcessChangeListeners.forEach(listener => listener(msg.changes as ChangeBufferEvent[]));
} else if (msg.type === 'deep-link' && deepLinkHandler) {
deepLinkHandler(msg.uri as string);
}
});

View File

@@ -0,0 +1,88 @@
import { describe, expect, it } from 'vitest';
import { PortRpc } from './port-rpc';
function createMockTransport() {
let handler: ((data: any) => void) | null = null;
const sent: unknown[] = [];
return {
send: (msg: unknown) => sent.push(msg),
onMessage: (h: (data: any) => void) => { handler = h; },
simulateResponse: (data: any) => handler?.(data),
sent,
};
}
describe('PortRpc', () => {
it('invoke rejects when not attached', async () => {
const rpc = new PortRpc();
await expect(rpc.invoke('database', 'find')).rejects.toThrow('data port not available');
});
it('invoke sends a message and resolves on ok response', async () => {
const rpc = new PortRpc();
const transport = createMockTransport();
rpc.attach(transport.send, transport.onMessage);
const promise = rpc.invoke('database', 'find', { type: 'Request' });
expect(transport.sent).toHaveLength(1);
const msg = transport.sent[0] as any;
expect(msg.type).toBe('invoke');
expect(msg.namespace).toBe('database');
expect(msg.method).toBe('find');
expect(msg.args).toEqual([{ type: 'Request' }]);
transport.simulateResponse({ id: msg.id, ok: true, result: [{ _id: '1' }] });
await expect(promise).resolves.toEqual([{ _id: '1' }]);
});
it('invoke rejects on error response with deserialized error', async () => {
const rpc = new PortRpc();
const transport = createMockTransport();
rpc.attach(transport.send, transport.onMessage);
const promise = rpc.invoke('services', 'request.getById', 'req_1');
const msg = transport.sent[0] as any;
transport.simulateResponse({
id: msg.id,
ok: false,
error: { name: 'TypeError', message: 'not found', stack: '' },
});
await expect(promise).rejects.toThrow('not found');
});
it('invalidate rejects all pending requests', async () => {
const rpc = new PortRpc();
const transport = createMockTransport();
rpc.attach(transport.send, transport.onMessage);
const p1 = rpc.invoke('database', 'find');
const p2 = rpc.invoke('database', 'count');
rpc.invalidate('restart');
await expect(p1).rejects.toThrow('restart');
await expect(p2).rejects.toThrow('restart');
});
it('invoke rejects after invalidate', async () => {
const rpc = new PortRpc();
const transport = createMockTransport();
rpc.attach(transport.send, transport.onMessage);
rpc.invalidate('gone');
await expect(rpc.invoke('database', 'find')).rejects.toThrow('data port not available');
});
it('ignores responses for unknown request ids', () => {
const rpc = new PortRpc();
const transport = createMockTransport();
rpc.attach(transport.send, transport.onMessage);
// Should not throw
transport.simulateResponse({ id: 'unknown-id', ok: true, result: null });
});
});

View File

@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';
import { deserializeError, serializeError } from './serialization';
describe('serializeError', () => {
it('serializes an Error instance', () => {
const err = new TypeError('bad input');
const s = serializeError(err);
expect(s.name).toBe('TypeError');
expect(s.message).toBe('bad input');
expect(s.stack).toContain('bad input');
});
it('serializes a non-Error value', () => {
const s = serializeError('string failure');
expect(s).toEqual({ name: 'Error', message: 'string failure', stack: '' });
});
});
describe('deserializeError', () => {
it('reconstructs an Error with name, message, and stack', () => {
const err = deserializeError({ name: 'RangeError', message: 'out of bounds', stack: 'fake stack' });
expect(err).toBeInstanceOf(Error);
expect(err.name).toBe('RangeError');
expect(err.message).toBe('out of bounds');
expect(err.stack).toBe('fake stack');
});
it('roundtrips through serialize → deserialize', () => {
const original = new Error('roundtrip');
const restored = deserializeError(serializeError(original));
expect(restored.name).toBe(original.name);
expect(restored.message).toBe(original.message);
expect(restored.stack).toBe(original.stack);
});
});

View File

@@ -1,9 +1,10 @@
import { net } from 'electron/utility';
import { configureFetch } from 'insomnia-api';
import { initDatabase, initServices } from 'insomnia-data';
import { createNedbDatabase, flushChangesImpl, servicesNodeImpl } from 'insomnia-data/node';
import { configureV3ClientDefaults } from './common/configure-v3-client';
import { setFetchImplementation } from './common/insomnia-fetch';
import { insomniaFetch, setFetchImplementation } from './common/insomnia-fetch';
import { startDataProcessServer } from './data-process/server';
process.on('uncaughtException', err => {
@@ -35,9 +36,12 @@ process.parentPort.once('message', async (event: Electron.MessageEvent) => {
}));
await initDatabase(dataProcessDatabase);
configureFetch(options => insomniaFetch({
...options,
onDeepLink: (uri: string) => process.parentPort.postMessage({ type: 'deep-link', uri }),
}));
configureV3ClientDefaults();
// net.fetch picks up the proxy + OS certs like the renderer; node fetch does neither.
// only works post-ready, which is fine — nothing calls this earlier. 'omit' = no cookies, same as before.
setFetchImplementation((input, init) =>
net.fetch(input, { ...init, credentials: 'omit', bypassCustomProtocolHandlers: true }),
);

View File

@@ -60,18 +60,14 @@ const bridge: HiddenBrowserWindowToMainBridgeAPI = {
resetAsyncTasks,
};
if (process.contextIsolated) {
contextBridge.exposeInMainWorld('bridge', bridge);
contextBridge.exposeInMainWorld('Promise', ProxiedPromise);
} else {
window.bridge = bridge;
window.Promise = ProxiedPromise;
}
const rpc = attachDataPortRpc('hidden-window-preload');
if (process.contextIsolated) {
contextBridge.exposeInMainWorld('bridge', bridge);
contextBridge.exposeInMainWorld('Promise', ProxiedPromise);
contextBridge.exposeInMainWorld('invokeDataPort', rpc);
} else {
window.bridge = bridge;
window.Promise = ProxiedPromise;
window.invokeDataPort = rpc;
}

View File

@@ -12,7 +12,7 @@ import { models, services } from 'insomnia-data';
import { isMac } from 'insomnia-data/common';
import { insomniaFetch, setFetchImplementation } from '~/common/insomnia-fetch';
import { issuePort, mainRpc, registerMainProcessChangeListener, spawnDataProcess } from '~/data-process/data-process-manager';
import { issuePort, mainRpc, registerDeepLinkHandler, registerMainProcessChangeListener, spawnDataProcess } from '~/data-process/data-process-manager';
import { initDataBridge } from '~/data-process/init-data-bridge';
import { initElectronStorage } from '~/main/electron-storage';
import { runGitCredentialsMigration } from '~/main/git/migrations';
@@ -142,6 +142,7 @@ app.on('ready', async () => {
if (window) issuePort(window);
});
await spawnDataProcess(dataPath);
registerDeepLinkHandler(uri => openDeepLinkUrl(uri));
await initDataBridge(mainRpc.invoke, {
database: { init: async () => {}, onChange: listener => registerMainProcessChangeListener(listener) },
});