From 3e22860e2174da1d0fa5614ea5ba395d5fdf2018 Mon Sep 17 00:00:00 2001 From: Bingbing Date: Wed, 15 Jul 2026 16:02:15 +0800 Subject: [PATCH] update --- .../src/data-process/data-process-manager.ts | 13 ++- .../src/data-process/port-rpc.test.ts | 88 +++++++++++++++++++ .../src/data-process/serialization.test.ts | 36 ++++++++ packages/insomnia/src/entry.data.ts | 8 +- .../src/entry.hidden-window-preload.ts | 12 +-- packages/insomnia/src/entry.main.ts | 3 +- 6 files changed, 146 insertions(+), 14 deletions(-) create mode 100644 packages/insomnia/src/data-process/port-rpc.test.ts create mode 100644 packages/insomnia/src/data-process/serialization.test.ts diff --git a/packages/insomnia/src/data-process/data-process-manager.ts b/packages/insomnia/src/data-process/data-process-manager.ts index 43f66a4bf1..3f71110781 100644 --- a/packages/insomnia/src/data-process/data-process-manager.ts +++ b/packages/insomnia/src/data-process/data-process-manager.ts @@ -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 { 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 { 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); } }); diff --git a/packages/insomnia/src/data-process/port-rpc.test.ts b/packages/insomnia/src/data-process/port-rpc.test.ts new file mode 100644 index 0000000000..816608729f --- /dev/null +++ b/packages/insomnia/src/data-process/port-rpc.test.ts @@ -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 }); + }); +}); diff --git a/packages/insomnia/src/data-process/serialization.test.ts b/packages/insomnia/src/data-process/serialization.test.ts new file mode 100644 index 0000000000..244818f9ae --- /dev/null +++ b/packages/insomnia/src/data-process/serialization.test.ts @@ -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); + }); +}); diff --git a/packages/insomnia/src/entry.data.ts b/packages/insomnia/src/entry.data.ts index 73e8149df4..51a5e3e471 100644 --- a/packages/insomnia/src/entry.data.ts +++ b/packages/insomnia/src/entry.data.ts @@ -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 }), ); diff --git a/packages/insomnia/src/entry.hidden-window-preload.ts b/packages/insomnia/src/entry.hidden-window-preload.ts index 7261b83e39..d03ddaf61e 100644 --- a/packages/insomnia/src/entry.hidden-window-preload.ts +++ b/packages/insomnia/src/entry.hidden-window-preload.ts @@ -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; } diff --git a/packages/insomnia/src/entry.main.ts b/packages/insomnia/src/entry.main.ts index 980cf43985..271ffb7631 100644 --- a/packages/insomnia/src/entry.main.ts +++ b/packages/insomnia/src/entry.main.ts @@ -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) }, });