diff --git a/.vscode/launch.json b/.vscode/launch.json index f3d1a6383b..0b852b8ce6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -40,6 +40,20 @@ "webRoot": "${workspaceFolder}/packages/insomnia/src", "timeout": 60000 }, + { + "name": "Electron: data-process", + "type": "node", + "request": "attach", + "protocol": "inspector", + "sourceMaps": true, + "presentation": { + "hidden": true, + "group": "Insomnia", + "order": 3 + }, + "port": 9229, + "timeout": 60000 + }, { "type": "node", "request": "launch", @@ -98,7 +112,7 @@ }, "stopAll": true, "preLaunchTask": "Insomnia: Compile (Watch)", - "configurations": ["Electron: main", "Electron: renderer"] + "configurations": ["Electron: main", "Electron: renderer", "Electron: data-process"] } ] } diff --git a/packages/insomnia-data/setup-vitest.ts b/packages/insomnia-data/setup-vitest.ts index 9fca452f2f..afd2565bd8 100644 --- a/packages/insomnia-data/setup-vitest.ts +++ b/packages/insomnia-data/setup-vitest.ts @@ -4,7 +4,7 @@ import { vi } from 'vitest'; const database = createNedbDatabase(); await initDatabase(database, { inMemoryOnly: true }, true); -await initServices(servicesNodeImpl); +initServices(servicesNodeImpl); import { v4Mock } from './__mocks__/uuid'; diff --git a/packages/insomnia/esbuild.entrypoints.ts b/packages/insomnia/esbuild.entrypoints.ts index 8d8ae56aa1..6493a11442 100644 --- a/packages/insomnia/esbuild.entrypoints.ts +++ b/packages/insomnia/esbuild.entrypoints.ts @@ -123,6 +123,21 @@ export default async function build(options: Options) { }, }; + const dataBuildOptions: BuildOptions = { + entryPoints: ['./src/entry.data.ts'], + outfile: path.join(outdir, 'entry.data.min.js'), + bundle: true, + platform: 'node', + sourcemap: true, + format: 'cjs', + plugins: [rendererToNodePlugin], + define: { + ...env, + '__IS_RENDERER__': JSON.stringify(false), + }, + external: [...Object.keys(builtinModules), 'electron'] + }; + const mainBuildOptions: BuildOptions = { entryPoints: ['./src/entry.main.ts'], outfile: path.join(outdir, 'entry.main.min.js'), @@ -194,10 +209,10 @@ export default async function build(options: Options) { build.onEnd(() => { buildCount++; // first build after main/preload/hiddenWindows/pluginWindows is built - if (buildCount === 6) { + if (buildCount === 7) { console.log('[Dev Build] Build complete, start Electron'); startElectron(); - } else if (buildCount > 6) { + } else if (buildCount > 7) { console.log(`[Dev Build] Finish rebuilding ${scriptName}, restarting Electron`); restartElectronProcess(); } else { @@ -230,6 +245,10 @@ export default async function build(options: Options) { ...pluginWindowPreloadBuildOptions, plugins: [restartElectronPlugin('plugin-window-preload')], }); + const dataContext = await esbuild.context({ + ...dataBuildOptions, + plugins: [restartElectronPlugin('data')], + }); const restartElectronProcess = () => { console.log('[Dev Build] Start restarting Electron'); @@ -251,6 +270,7 @@ export default async function build(options: Options) { const hiddenWindowPreloadWatch = await hiddenPreloadContext.watch(); const pluginWindowWatch = await pluginWindowContext.watch(); const pluginWindowPreloadWatch = await pluginWindowPreloadContext.watch(); + const dataWatch = await dataContext.watch(); return Promise.all([ preloadWatch, hiddenWindowPreloadWatch, @@ -258,6 +278,7 @@ export default async function build(options: Options) { hiddenWindowWatch, pluginWindowWatch, pluginWindowPreloadWatch, + dataWatch, ]); } const preload = esbuild.build(preloadBuildOptions); @@ -266,6 +287,7 @@ export default async function build(options: Options) { const pluginWindow = esbuild.build(pluginWindowBuildOptions); const pluginWindowPreload = esbuild.build(pluginWindowPreloadBuildOptions); const main = esbuild.build(mainBuildOptions); + const data = esbuild.build(dataBuildOptions); return Promise.all([ main, preload, @@ -273,6 +295,7 @@ export default async function build(options: Options) { hiddenBrowserWindowPreload, pluginWindow, pluginWindowPreload, + data, ]).catch(err => { console.error('[Build] Build failed:', err); }); diff --git a/packages/insomnia/setup-vitest.ts b/packages/insomnia/setup-vitest.ts index 6f731aad7c..c69fd85652 100644 --- a/packages/insomnia/setup-vitest.ts +++ b/packages/insomnia/setup-vitest.ts @@ -1,16 +1,15 @@ import { initDatabase, initServices } from 'insomnia-data'; -import { servicesNodeImpl } from 'insomnia-data/node'; +import { createNedbDatabase, servicesNodeImpl } from 'insomnia-data/node'; import { vi } from 'vitest'; // eslint-disable-next-line no-restricted-imports import { v4Mock } from '../insomnia-data/__mocks__/uuid'; import { nodeLibcurlMock } from './src/__mocks__/@getinsomnia/node-libcurl'; import { electronMock } from './src/__mocks__/electron'; -import { mainDatabase } from './src/main/database.main'; import { initRuntime } from './src/runtimes'; import { nodeRuntime } from './src/runtimes/runtime.node'; -await initDatabase(mainDatabase, { inMemoryOnly: true }, true); +await initDatabase(createNedbDatabase(), { inMemoryOnly: true }, true); await initServices(servicesNodeImpl); initRuntime(nodeRuntime); diff --git a/packages/insomnia/src/data-process/README.md b/packages/insomnia/src/data-process/README.md new file mode 100644 index 0000000000..9b37dffb18 --- /dev/null +++ b/packages/insomnia/src/data-process/README.md @@ -0,0 +1,93 @@ +# data-process + +Isolated utility process that owns the NeDB database and all service implementations. Main and renderer communicate with it via MessagePort RPC. + +## Topology + +```mermaid +flowchart LR + subgraph Renderer + R["entry.*.ts\ninitDataBridge(window.invokeDataPort)"] + end + subgraph Preload + P["attachDataPortRpc()\nContextBridge → invokeDataPort"] + end + subgraph Main + M["entry.main.ts\ninitDataBridge(mainRpc.invoke)"] + end + subgraph DataProcess["Data Process (utilityProcess)"] + S["server.ts\nNeDB + servicesNodeImpl"] + end + + R -- "MessagePort (via contextBridge)" --> P + P -- "MessagePort" --> S + M -- "MessagePortMain" --> S +``` + +Each process creates a `PortRpc`, attaches it to its transport, then calls `initDataBridge(invoke)` to wire up database and services proxies. + +### Init sequence + +```mermaid +sequenceDiagram + participant M as Main + participant D as Data Process + participant P as Preload + participant R as Renderer + + M->>D: fork(entry.data.min.js) + M->>D: postMessage({ dbPath }) + D-->>M: postMessage({ type: 'ready' }) + M->>M: mainRpc.attach(port2) + P->>M: ipcRenderer.invoke('data-process.request-port') + M->>P: webContents.postMessage('data-process.port', [port]) + P->>P: rpc.attach(port) + R->>R: initDataBridge(window.invokeDataPort) +``` + +## File map + +| File | Role | +|---|---| +| `port-rpc.ts` | Transport-agnostic RPC client. `attach` / `invalidate` / `invoke`. | +| `server.ts` | Data-process side. Listens for `new-port` messages, routes `invoke` requests to NeDB or services. | +| `data-process-manager.ts` | Main-process side. `spawnDataProcess` forks the utility process, exports `mainRpc`. `issuePort(window)` hands a port to a renderer window. Restarts on crash. | +| `data-port-preload.ts` | `attachDataPortRpc(context)` — wires up `PortRpc` in a preload script and returns `InvokeFn`. | +| `init-data-bridge.ts` | `initDataBridge(invoke, options?)` — builds database + services proxies and calls `initDatabase` / `initServices`. Used by every renderer entry and the main process. | +| `serialization.ts` | `serializeError` / `deserializeError` for structured error transport across MessagePort. | + +## Usage per process + +### Preload (all windows) + +```ts +const invokeDataPort = attachDataPortRpc('my-window-preload'); +contextBridge.exposeInMainWorld('invokeDataPort', invokeDataPort); +``` + +### Renderer entry + +```ts +await initDataBridge(window.invokeDataPort, { + database: { onChange: listener => ... }, // optional overrides +}); +``` + +### Main process + +```ts +await spawnDataProcess(dbPath); +await initDataBridge(mainRpc.invoke, { + database: { init: async () => {}, onChange: listener => registerMainProcessChangeListener(listener) }, +}); +``` + +## Adding a new window + +1. Preload: `attachDataPortRpc('my-window')`, expose result as `window.invokeDataPort`. +2. Entry: `await initDataBridge(window.invokeDataPort)`. +3. Main: `issuePort(window)` is called automatically when the window sends `data-process.request-port`. + +## Debugging + +In development, `spawnDataProcess` passes `--inspect=9229` to the utility process. The VSCode `Insomnia` compound attaches to it automatically alongside the main process and renderer. diff --git a/packages/insomnia/src/data-process/data-port-preload.ts b/packages/insomnia/src/data-process/data-port-preload.ts new file mode 100644 index 0000000000..60758fc8df --- /dev/null +++ b/packages/insomnia/src/data-process/data-port-preload.ts @@ -0,0 +1,41 @@ +import type { IpcRendererEvent } from 'electron'; +import { ipcRenderer } from 'electron'; + +import { type InvokeFn, PortRpc } from './port-rpc'; + +/** + * Wires up a PortRpc instance to the data-process IPC channel and returns + * the invoke function to expose via contextBridge / window. + * + * @param context - Identifies the caller in error/log messages (e.g. 'preload', 'hidden-window-preload'). + */ +export function attachDataPortRpc(context: string): InvokeFn { + const rpc = new PortRpc(); + + ipcRenderer.on('data-process.port', (event: IpcRendererEvent) => { + const port = event.ports[0]; + if (port) { + rpc.attach( + m => port.postMessage(m), + h => { + port.onmessage = (e: MessageEvent) => h(e.data); + port.start(); + }, + ); + console.log(`[${context}] data-process port received and attached`); + } else { + console.warn(`[${context}] data-process.port event received but no port transferred`); + } + }); + + ipcRenderer.on('data-process.restarting', () => { + console.warn(`[${context}] data-process restarting – invalidating RPC`); + rpc.invalidate('data-process restarting'); + }); + + ipcRenderer.invoke('data-process.request-port').catch((e: unknown) => { + console.error(`[${context}] failed to request data-process port:`, e); + }); + + return rpc.invoke; +} diff --git a/packages/insomnia/src/data-process/data-process-manager.ts b/packages/insomnia/src/data-process/data-process-manager.ts new file mode 100644 index 0000000000..43f66a4bf1 --- /dev/null +++ b/packages/insomnia/src/data-process/data-process-manager.ts @@ -0,0 +1,125 @@ +import path from 'node:path'; + +import type { UtilityProcess } from 'electron'; +import { BrowserWindow, MessageChannelMain, utilityProcess } from 'electron'; +import type { ChangeListener } from 'insomnia-data'; + +import { isDevelopment } from '../common/constants'; +import { PortRpc } from './port-rpc'; + +let child: UtilityProcess | null = null; + +const mainProcessChangeListeners: ChangeListener[] = []; + +export function registerMainProcessChangeListener(listener: ChangeListener): void { + mainProcessChangeListeners.push(listener); +} + +function getDataProcessPath(): string { + return path.join(__dirname, 'entry.data.min.js'); +} + +// Messages from a UtilityProcess arrive as the raw value passed to +// process.parentPort.postMessage() on the child side - NOT wrapped in +// an { data: ... } envelope. Use this helper to cast safely. +function msgOf(event: Electron.MessageEvent): Record { + return event as unknown as Record; +} + +// Main process's own RPC client for the data-process. +// After spawnDataProcess() completes, this is attached and ready to use. +export const mainRpc = new PortRpc(); + +export async function spawnDataProcess(dbPath: string): Promise { + const inspectArgs = isDevelopment() ? ['--inspect=9229'] : []; + console.log('[data-process] spawning...', { dbPath, inspectArgs }); + child = utilityProcess.fork(getDataProcessPath(), [], { + execArgv: inspectArgs, + stdio: 'pipe', + }); + + child.stdout?.on('data', (d: Buffer) => process.stdout.write(`[data-process:stdout] ${d}`)); + child.stderr?.on('data', (d: Buffer) => process.stderr.write(`[data-process:stderr] ${d}`)); + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('[data-process] timed out waiting for ready signal')); + }, 30_000); + + const onMessage = (event: Electron.MessageEvent) => { + const msg = msgOf(event); + if (msg.type === 'ready') { + clearTimeout(timeout); + child!.off('message', onMessage); + console.log('[data-process] ready'); + resolve(); + } else if (msg.type === 'error') { + clearTimeout(timeout); + child!.off('message', onMessage); + reject(new Error(`[data-process] init failed: ${msg.message}`)); + } + }; + + child!.on('message', onMessage); + + child!.once('exit', code => { + clearTimeout(timeout); + reject(new Error(`[data-process] exited with code ${code} before ready`)); + }); + + child!.postMessage({ dbPath }); + }); + + // Create a dedicated MessagePort for the main process's own RPC. + const { port1, port2 } = new MessageChannelMain(); + child.postMessage({ type: 'new-port' }, [port1]); + mainRpc.attach( + m => port2.postMessage(m), + h => { + port2.on('message', (e: Electron.MessageEvent) => h(e.data as any)); + port2.start(); + }, + ); + console.log('[data-process] main-process RPC port attached'); + + child.on('message', (event: Electron.MessageEvent) => { + const msg = msgOf(event); + if (msg.type === 'db.changes') { + BrowserWindow.getAllWindows().forEach(w => { + w.webContents.send('db.changes', msg.changes); + }); + mainProcessChangeListeners.forEach(listener => listener(msg.changes as any)); + } + }); + + child.on('exit', code => { + console.warn(`[data-process] exited with code ${code}`); + handleExit(dbPath); + }); +} + +export function issuePort(window: BrowserWindow): void { + if (!child) { + console.error('[data-process] cannot issue port - data process not running'); + return; + } + const { port1, port2 } = new MessageChannelMain(); + child.postMessage({ type: 'new-port' }, [port1]); + window.webContents.postMessage('data-process.port', null, [port2]); + console.debug('[data-process] port issued to window', { windowId: window.id }); +} + +function handleExit(dbPath: string): void { + console.warn('[data-process] restarting...'); + mainRpc.invalidate('data-process restarting'); + BrowserWindow.getAllWindows().forEach(w => w.webContents.send('data-process.restarting')); + child = null; + spawnDataProcess(dbPath) + .then(() => { + console.log('[data-process] restarted, re-issuing ports'); + BrowserWindow.getAllWindows().forEach(w => issuePort(w)); + }) + .catch(e => { + console.error('[data-process] failed to restart:', e); + }); +} diff --git a/packages/insomnia/src/data-process/init-data-bridge.ts b/packages/insomnia/src/data-process/init-data-bridge.ts new file mode 100644 index 0000000000..ae675aabb6 --- /dev/null +++ b/packages/insomnia/src/data-process/init-data-bridge.ts @@ -0,0 +1,47 @@ +import type { IDatabase, Services } from 'insomnia-data'; +import { initDatabase, initServices } from 'insomnia-data'; + +import { type InvokeFn } from './port-rpc'; + +export interface DataBridgeOptions { + database?: Partial; +} + +/** + * Initialises the database and services bridges from a single invoke function. + * + * @param invoke - The RPC invoke function (InvokeFn from PortRpc, or mainRpc.invoke). + * May be undefined when called from a renderer — an error is thrown in that case. + * @param options.database - Optional Partial overrides (e.g. onChange for main process). + */ +export async function initDataBridge( + invoke: InvokeFn | undefined, + options?: DataBridgeOptions, +): Promise { + if (!invoke) { + throw new Error( + 'Data port bridge is not available. This entrypoint must run in an environment with the preload bridge.', + ); + } + + await initDatabase( + new Proxy((options?.database ?? {}) as IDatabase, { + get(target, prop) { + if (prop in target) return target[prop as keyof IDatabase]; + return (...args: unknown[]) => invoke('database', prop as string, ...args); + }, + }), + ); + + initServices( + new Proxy({} as Services, { + get(_target, serviceName: string) { + return new Proxy({}, { + get(_target, methodName: string) { + return (...args: unknown[]) => invoke('services', `${serviceName}.${methodName}`, ...args); + }, + }); + }, + }), + ); +} diff --git a/packages/insomnia/src/data-process/port-rpc.ts b/packages/insomnia/src/data-process/port-rpc.ts new file mode 100644 index 0000000000..2ff3ea4426 --- /dev/null +++ b/packages/insomnia/src/data-process/port-rpc.ts @@ -0,0 +1,54 @@ +import { deserializeError, type SerializedError } from './serialization'; + +export type InvokeFn = (namespace: 'services' | 'database', method: string, ...args: unknown[]) => Promise; + +interface DataResponse { + id: string; + ok: boolean; + result?: unknown; + error?: SerializedError; +} + +/** + * Transport-agnostic RPC client for the data-process. + * + * Works with any port-like transport (MessagePort in renderer, MessagePortMain + * in main process). The caller provides `send` and `onMessage` adapters in + * `attach()`. Handles request/response correlation, error deserialization, and + * port lifecycle (invalidate on restart). + */ +export class PortRpc { + private send: ((msg: unknown) => void) | null = null; + private pending = new Map void; reject: (e: Error) => void }>(); + + attach(send: (msg: unknown) => void, onMessage: (handler: (data: DataResponse) => void) => void): void { + this.send = send; + onMessage(data => { + const p = this.pending.get(data.id); + if (!p) return; + this.pending.delete(data.id); + data.ok ? p.resolve(data.result) : p.reject(deserializeError(data.error!)); + }); + console.debug('[port-rpc] attached', { pendingCount: this.pending.size }); + } + + invalidate(reason: string): void { + const pendingCount = this.pending.size; + this.pending.forEach(({ reject }) => reject(new Error(reason))); + this.pending.clear(); + this.send = null; + console.warn(`[port-rpc] invalidated: ${reason}`, { rejectedCount: pendingCount }); + } + + invoke: InvokeFn = (namespace, method, ...args) => { + if (!this.send) { + console.error(`[port-rpc] invoke failed – port not available: ${namespace}.${method}`); + return Promise.reject(new Error('data port not available')); + } + const id = crypto.randomUUID(); + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject }); + this.send!({ id, type: 'invoke', namespace, method, args }); + }); + }; +} diff --git a/packages/insomnia/src/data-process/serialization.ts b/packages/insomnia/src/data-process/serialization.ts new file mode 100644 index 0000000000..ed9d49e82a --- /dev/null +++ b/packages/insomnia/src/data-process/serialization.ts @@ -0,0 +1,19 @@ +export interface SerializedError { + name: string; + message: string; + stack: string; +} + +export function serializeError(e: unknown): SerializedError { + if (e instanceof Error) { + return { name: e.name, message: e.message, stack: e.stack ?? '' }; + } + return { name: 'Error', message: String(e), stack: '' }; +} + +export function deserializeError(s: SerializedError): Error { + const err = new Error(s.message); + err.name = s.name; + err.stack = s.stack; + return err; +} diff --git a/packages/insomnia/src/data-process/server.ts b/packages/insomnia/src/data-process/server.ts new file mode 100644 index 0000000000..953ad71a2c --- /dev/null +++ b/packages/insomnia/src/data-process/server.ts @@ -0,0 +1,87 @@ +import type { IDatabase } from 'insomnia-data'; +import { database, type Services, services } from 'insomnia-data'; + +import { type SerializedError, serializeError } from './serialization'; + +export interface DataRequest { + id: string; + type: 'invoke'; + namespace: 'services' | 'database'; + /** 'services' namespace: "serviceName.methodName", e.g. "request.getById" + * 'database' namespace: method name on IDatabase, e.g. "find" + */ + method: string; + args: unknown[]; +} + +interface DataResponseOk { + id: string; + ok: true; + result: unknown; +} +interface DataResponseError { + id: string; + ok: false; + error: SerializedError; +} +type DataResponse = DataResponseOk | DataResponseError; + +async function dispatch(req: DataRequest): Promise { + if (req.namespace === 'database') { + const fn = database[req.method as keyof IDatabase]; + if (typeof fn !== 'function') { + throw new TypeError(`Unknown database method: ${req.method}`); + } + return (fn as (...args: unknown[]) => unknown).call(database, ...req.args); + } + if (req.namespace === 'services') { + const [serviceName, methodName] = req.method.split('.'); + if (!serviceName || !methodName) { + throw new TypeError(`Invalid services method format: ${req.method}`); + } + const service = services[serviceName as keyof Services]; + if (!service) { + throw new TypeError(`Unknown service: ${serviceName}`); + } + const fn = service[methodName as keyof typeof service]; + if (typeof fn !== 'function') { + throw new TypeError(`Unknown service method: ${serviceName}.${methodName}`); + } + return (fn as (...args: unknown[]) => unknown).call(service, ...req.args); + } + throw new TypeError(`Unknown namespace: ${req.namespace}`); +} + +async function handleRequest(port: Electron.MessagePortMain, req: DataRequest): Promise { + let response: DataResponse; + try { + const raw = await dispatch(req); + response = { id: req.id, ok: true, result: raw }; + } catch (e) { + response = { id: req.id, ok: false, error: serializeError(e) }; + } + port.postMessage(response); +} + +function attachPort(port: Electron.MessagePortMain): void { + console.debug('[data-process] port attached'); + port.on('message', (event: Electron.MessageEvent) => { + handleRequest(port, event.data as DataRequest).catch(e => { + console.error('[data-process] unhandled error in handleRequest:', e); + }); + }); + port.start(); +} + +export function startDataProcessServer(): void { + process.parentPort.on('message', (event: Electron.MessageEvent) => { + const msg = event.data as Record; + + if (msg?.type === 'new-port') { + const port = event.ports[0]; + if (port) { + attachPort(port); + } + } + }); +} diff --git a/packages/insomnia/src/entry.client.tsx b/packages/insomnia/src/entry.client.tsx index 3531655d6d..1977ead268 100644 --- a/packages/insomnia/src/entry.client.tsx +++ b/packages/insomnia/src/entry.client.tsx @@ -2,19 +2,18 @@ import './ui/renderer-listeners'; import './ui/log'; import { configureFetch } from 'insomnia-api'; -import { initDatabase, initServices, services } from 'insomnia-data'; +import { services } from 'insomnia-data'; import { startTransition, StrictMode } from 'react'; import { hydrateRoot } from 'react-dom/client'; import { HydratedRouter } from 'react-router/dom'; import { insomniaFetch } from '~/common/insomnia-fetch'; import { setTemplatingDbAuthToken } from '~/common/templating/liquid-extension-worker'; +import { initDataBridge } from '~/data-process/init-data-bridge'; import { initRuntime } from '~/runtimes'; import { rendererRuntime } from '~/runtimes/runtime.renderer'; import { migrateFromLocalStorage, type SessionData, setSessionData, setVaultSessionData } from '~/ui/account/session'; -import { database as clientDatabase } from '~/ui/database.client'; import { applyColorScheme } from '~/ui/plugins/misc'; -import { createServicesProxy } from '~/ui/services-proxy'; import { clearOAuthWindowSessionId } from '~/ui/spawn-oauth-window'; import { getInitialEntry } from '~/ui/utils/router'; @@ -33,19 +32,9 @@ initializeSentry(); // Fetch the templating-db auth token once so it's available for every templating call in this window. setTemplatingDbAuthToken(await window.main.templatingDb.getAuthToken()); -// Initialize database for renderer process -await initDatabase(clientDatabase); -// Initialize services for renderer process. -// With contextIsolation the preload exposes a flat invoke (a Proxy can't cross -// the bridge), so rebuild the Proxy here. Without it, the Proxy is on window directly. -const dataServices = - window._dataServices ?? (window._dataServicesInvoke ? createServicesProxy(window._dataServicesInvoke) : undefined); -if (!dataServices) { - throw new Error( - 'Services bridge is not available. This entrypoint must run in an environment with the preload bridge.', - ); -} -initServices(dataServices); +await initDataBridge(window.invokeDataPort, { + database: { init: async () => {}, onChange: () => {} }, +}); initRuntime(rendererRuntime); configureFetch(options => insomniaFetch({ ...options, onDeepLink: (uri: string) => window.main.openDeepLink(uri) })); diff --git a/packages/insomnia/src/entry.data.ts b/packages/insomnia/src/entry.data.ts new file mode 100644 index 0000000000..73e8149df4 --- /dev/null +++ b/packages/insomnia/src/entry.data.ts @@ -0,0 +1,53 @@ +import { net } from 'electron/utility'; +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 { startDataProcessServer } from './data-process/server'; + +process.on('uncaughtException', err => { + process.stdout.write(`[data-process] uncaughtException: ${err.stack ?? err.message}\n`); +}); + +process.on('unhandledRejection', reason => { + const msg = reason instanceof Error ? (reason.stack ?? reason.message) : String(reason); + process.stdout.write(`[data-process] unhandledRejection: ${msg}\n`); +}); + +process.parentPort.once('message', async (event: Electron.MessageEvent) => { + const { dbPath } = event.data as { dbPath: string }; + + const keepAlive = setInterval(() => {}, 30_000); + + try { + const dataProcessDatabase = createNedbDatabase(nedbDatabase => ({ + ...nedbDatabase, + init: async (config = {}, forceReset = false) => { + await nedbDatabase.init({ dbPath, ...config }, forceReset); + }, + flushChanges: async function (id = 0, fake = false) { + const changes = await flushChangesImpl(id, fake); + if (changes) { + process.parentPort.postMessage({ type: 'db.changes', changes }); + } + }, + })); + + await initDatabase(dataProcessDatabase); + 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 }), + ); + initServices(servicesNodeImpl); + startDataProcessServer(); + process.parentPort.postMessage({ type: 'ready' }); + } catch (err) { + clearInterval(keepAlive); + const e = err instanceof Error ? err : new Error(String(err)); + process.stdout.write(`[data-process] init failed: ${e.stack ?? e.message}\n`); + process.parentPort.postMessage({ type: 'error', message: e.message }); + } +}); diff --git a/packages/insomnia/src/entry.hidden-window-preload.ts b/packages/insomnia/src/entry.hidden-window-preload.ts index 897e24c5ae..7261b83e39 100644 --- a/packages/insomnia/src/entry.hidden-window-preload.ts +++ b/packages/insomnia/src/entry.hidden-window-preload.ts @@ -3,8 +3,6 @@ import * as fs from 'node:fs'; import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron'; import type { Compression } from 'insomnia-data'; -import { servicesProxy } from '~/ui/renderer-services-proxy'; - import { asyncTasksAllSettled, OriginalPromise, @@ -13,6 +11,7 @@ import { resetAsyncTasks, stopMonitorAsyncTasks, } from '../../insomnia-scripting-environment/src/objects'; +import { attachDataPortRpc } from './data-process/data-port-preload'; // this will also import lots of node_modules into the preload script, consider moving this file insomnia-scripting-environment import { requireInterceptor } from './scripting/require-interceptor'; @@ -64,9 +63,15 @@ const bridge: HiddenBrowserWindowToMainBridgeAPI = { if (process.contextIsolated) { contextBridge.exposeInMainWorld('bridge', bridge); contextBridge.exposeInMainWorld('Promise', ProxiedPromise); - contextBridge.exposeInMainWorld('_dataServices', servicesProxy); } else { window.bridge = bridge; window.Promise = ProxiedPromise; - window._dataServices = servicesProxy; +} + +const rpc = attachDataPortRpc('hidden-window-preload'); + +if (process.contextIsolated) { + contextBridge.exposeInMainWorld('invokeDataPort', rpc); +} else { + window.invokeDataPort = rpc; } diff --git a/packages/insomnia/src/entry.hidden-window.ts b/packages/insomnia/src/entry.hidden-window.ts index 525196767b..0851f1c162 100644 --- a/packages/insomnia/src/entry.hidden-window.ts +++ b/packages/insomnia/src/entry.hidden-window.ts @@ -1,8 +1,8 @@ import * as Sentry from '@sentry/electron/renderer'; import { SENTRY_OPTIONS } from 'insomnia/src/common/sentry'; -import { initServices } from 'insomnia-data'; import type { RequestContext } from '../../insomnia-scripting-environment/src/objects'; +import { initDataBridge } from './data-process/init-data-bridge'; import { initRuntime } from './runtimes'; import { rendererRuntime } from './runtimes/runtime.renderer'; import { runScript } from './scripting/run-script'; @@ -20,19 +20,13 @@ Sentry.init({ ...SENTRY_OPTIONS, }); -// Initialize services for hidden renderer process -if (!window._dataServices) { - throw new Error( - 'window._dataServices is not available. This entrypoint must run in an environment with the preload bridge.', - ); -} -initServices(window._dataServices); -// Remove the global services reference after initialization to improve security by preventing unintended access from the global scope. -delete window._dataServices; +// Initialize data bridge for hidden renderer process +const initPromise = initDataBridge(window.invokeDataPort); initRuntime(rendererRuntime); window.bridge.onmessage( async (data: { script: string; context: RequestContext }, callback: ({ error }: { error: string }) => void) => { + await initPromise; window.bridge.setBusy(true); try { diff --git a/packages/insomnia/src/entry.main.ts b/packages/insomnia/src/entry.main.ts index 7af48ab595..980cf43985 100644 --- a/packages/insomnia/src/entry.main.ts +++ b/packages/insomnia/src/entry.main.ts @@ -8,12 +8,12 @@ import contextMenu from 'electron-context-menu'; import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer'; import { configureFetch } from 'insomnia-api'; import type { Stats } from 'insomnia-data'; -import { initDatabase, initServices, models, services } from 'insomnia-data'; +import { models, services } from 'insomnia-data'; import { isMac } from 'insomnia-data/common'; -import { servicesNodeImpl } from 'insomnia-data/node'; import { insomniaFetch, setFetchImplementation } from '~/common/insomnia-fetch'; -import { mainDatabase } from '~/main/database.main'; +import { issuePort, mainRpc, 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'; import { registerPathHandlers } from '~/main/ipc/path'; @@ -31,7 +31,7 @@ import { backupIfNewerVersionAvailable } from './main/backup'; import { registerSyncHandlers } from './main/cloud-sync/ipc'; import { registerGitServiceAPI } from './main/git-service'; import { registerCookieHandlers } from './main/ipc/cookies'; -import { ipcMainOn, ipcMainOnce, registerElectronHandlers } from './main/ipc/electron'; +import { ipcMainHandle, ipcMainOn, ipcMainOnce, registerElectronHandlers } from './main/ipc/electron'; import { registerElectronStorageHandlers } from './main/ipc/electron-storage'; import { registergRPCHandlers } from './main/ipc/grpc'; import { registerMainHandlers } from './main/ipc/main'; @@ -137,10 +137,14 @@ app.on('ready', async () => { } } - // Init some important things first - await initDatabase(mainDatabase); - // Initialize services for main process - initServices(servicesNodeImpl); + ipcMainHandle('data-process.request-port', event => { + const window = BrowserWindow.fromWebContents(event.sender); + if (window) issuePort(window); + }); + await spawnDataProcess(dataPath); + await initDataBridge(mainRpc.invoke, { + database: { init: async () => {}, onChange: listener => registerMainProcessChangeListener(listener) }, + }); initRuntime(nodeRuntime); await _createModelInstances(); // proxy has to be set up before backup's net.fetch below diff --git a/packages/insomnia/src/entry.plugin-window-preload.ts b/packages/insomnia/src/entry.plugin-window-preload.ts index 8576a5820d..90d5661078 100644 --- a/packages/insomnia/src/entry.plugin-window-preload.ts +++ b/packages/insomnia/src/entry.plugin-window-preload.ts @@ -1,12 +1,14 @@ import { ipcRenderer } from 'electron'; -// Provide window.app so plugin-loading code (which checks __IS_RENDERER__) can resolve the userData path without needing the main renderer's full preload. +import { attachDataPortRpc } from './data-process/data-port-preload'; + window.app = { getPath: (name: string) => ipcRenderer.sendSync('getPath', name) as string, getAppPath: () => ipcRenderer.sendSync('getAppPath') as string, process: { platform: process.platform as NodeJS.Platform }, }; +window.invokeDataPort = attachDataPortRpc('plugin-window-preload'); // Bridge plugin UI calls to the main renderer window via IPC. // The plugin window has no visible DOM; these methods forward to the main renderer. window.showAlert = (options?: Record) => { diff --git a/packages/insomnia/src/entry.plugin-window.ts b/packages/insomnia/src/entry.plugin-window.ts index 9b3a3136ef..c83c15a449 100644 --- a/packages/insomnia/src/entry.plugin-window.ts +++ b/packages/insomnia/src/entry.plugin-window.ts @@ -1,12 +1,10 @@ import { ipcRenderer } from 'electron'; -import { initDatabase, initServices } from 'insomnia-data'; import { setTemplatingDbAuthToken } from './common/templating/liquid-extension-worker'; -import { pluginWindowDatabase } from './main/database.plugin-window'; +import { initDataBridge } from './data-process/init-data-bridge'; import { invokePluginMethod } from './plugins/invoke-method'; import { initRuntime } from './runtimes'; import { rendererRuntime } from './runtimes/runtime.renderer'; -import { servicesProxy } from './ui/renderer-services-proxy'; interface PluginInvokeMessage { id: string; @@ -25,14 +23,15 @@ ipcRenderer.on('plugins.invoke', async (_event, { id, method, args }: PluginInvo } }); -// Initialize database (via IPC proxy) and services before signalling readiness. +// Initialize database (via data-port) and services before signalling readiness. // getPlugins() calls services.settings.get(), which requires this to be done first. (async () => { try { // Fetch the auth token before any plugin code can call fetchFromTemplateWorkerDatabase. setTemplatingDbAuthToken(await ipcRenderer.invoke('templatingDb.getAuthToken')); - await initDatabase(pluginWindowDatabase); - initServices(servicesProxy); + await initDataBridge(window.invokeDataPort, { + database: { init: async () => {}, onChange: () => {} }, + }); initRuntime(rendererRuntime); ipcRenderer.send('plugins.windowReady'); } catch (err) { diff --git a/packages/insomnia/src/entry.preload.ts b/packages/insomnia/src/entry.preload.ts index 8e1ee1db4a..3782c237eb 100644 --- a/packages/insomnia/src/entry.preload.ts +++ b/packages/insomnia/src/entry.preload.ts @@ -12,11 +12,11 @@ import type { import type { GenerateMcpSamplingResponseFunction } from '~/common/plugins/types'; import type { RenderedRequest } from '~/common/templating/types'; import { invariant } from '~/common/utils/invariant'; +import { attachDataPortRpc } from '~/data-process/data-port-preload'; import { invokeWithNormalizedError } from '~/main/ipc/invoke'; import type { LLMBackend, LLMConfig, LLMConfigServiceAPI } from '~/main/llm-config-service'; import type { PluginInvokeMethod } from '~/plugins/invoke-method'; import { isUserAbortResolveMergeConflictError, UserAbortResolveMergeConflictError } from '~/sync/vcs/errors'; -import { servicesProxy } from '~/ui/renderer-services-proxy'; import type { SyncBridgeAPI } from './main/cloud-sync/ipc'; import type { GitServiceAPI } from './main/git-service'; @@ -29,8 +29,11 @@ import type { CurlBridgeAPI } from './main/network/curl'; import type { McpBridgeAPI } from './main/network/mcp'; import type { SocketIOBridgeAPI } from './main/network/socket-io'; import type { WebSocketBridgeAPI } from './main/network/websocket'; + const ports = new Map<'hiddenWindowPort', MessagePort>(); +const invokeDataPort = attachDataPortRpc('preload'); + type PluginMethodResult = T extends keyof PluginsBridgeAPI ? Awaited> : never; @@ -496,10 +499,6 @@ const clipboard: Window['clipboard'] = { const webUtils: Window['webUtils'] = { getPathForFile: (file: File) => webUtilities.getPathForFile(file), }; -const database: Window['database'] = { - invoke: (fnName, ...args) => invokeWithNormalizedError('database.invoke', fnName, ...args), -}; - const env: Window['env'] = { // GitLab OAuth — redirect URI, client ID, and API URL allow dev/enterprise overrides INSOMNIA_GITLAB_REDIRECT_URI: process.env.INSOMNIA_GITLAB_REDIRECT_URI, @@ -547,14 +546,7 @@ if (process.contextIsolated) { contextBridge.exposeInMainWorld('clipboard', clipboard); contextBridge.exposeInMainWorld('webUtils', webUtils); contextBridge.exposeInMainWorld('path', path); - contextBridge.exposeInMainWorld('database', database); - // A Proxy cannot be cloned across the contextBridge, so expose a flat invoke - // function and rebuild the services Proxy in the isolated renderer world. - contextBridge.exposeInMainWorld( - '_dataServicesInvoke', - (serviceName: string, methodName: string, ...args: unknown[]) => - invokeWithNormalizedError('services.invoke', serviceName, methodName, ...args), - ); + contextBridge.exposeInMainWorld('invokeDataPort', invokeDataPort); contextBridge.exposeInMainWorld('env', env); } else { window.main = main; @@ -564,7 +556,6 @@ if (process.contextIsolated) { window.clipboard = clipboard; window.webUtils = webUtils; window.path = path; - window.database = database; - window._dataServices = servicesProxy; + window.invokeDataPort = invokeDataPort; window.env = env; } diff --git a/packages/insomnia/src/konnect/__tests__/sync.test.ts b/packages/insomnia/src/konnect/__tests__/sync.test.ts index 5d257877fe..14b1680f02 100644 --- a/packages/insomnia/src/konnect/__tests__/sync.test.ts +++ b/packages/insomnia/src/konnect/__tests__/sync.test.ts @@ -6,12 +6,12 @@ */ import { initDatabase, models, services as insoservices } from 'insomnia-data'; +import { createNedbDatabase } from 'insomnia-data/node'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; // eslint-disable-next-line no-restricted-imports import { resetV4Counter } from '../../../../insomnia-data/__mocks__/uuid'; import { database as db } from '../../common/database'; -import { mainDatabase } from '../../main/database.main'; import type { KonnectControlPlane, KonnectRoute, KonnectService } from '../api'; import { syncKonnect } from '../sync'; @@ -114,7 +114,7 @@ const trackAnalyticsEvent = vi.fn(); beforeEach(async () => { // Re-init with fresh in-memory NeDB buckets — clean slate for every test. - await initDatabase(mainDatabase, { inMemoryOnly: true }, true); + await initDatabase(createNedbDatabase(), { inMemoryOnly: true }, true); resetV4Counter(); vi.stubGlobal('window', { main: { trackAnalyticsEvent } }); }); diff --git a/packages/insomnia/src/main/database.main.ts b/packages/insomnia/src/main/database.main.ts deleted file mode 100644 index f532474aae..0000000000 --- a/packages/insomnia/src/main/database.main.ts +++ /dev/null @@ -1,37 +0,0 @@ -import electron from 'electron'; -import type { DataStoreOptions, IDatabase } from 'insomnia-data'; -import { createNedbDatabase, flushChangesImpl } from 'insomnia-data/node'; - -export const mainDatabase: IDatabase = createNedbDatabase(nedbDatabase => ({ - ...nedbDatabase, - init: async (config: DataStoreOptions = {}, forceReset = false) => { - const dbPath = process.env['INSOMNIA_DATA_PATH'] || electron.app.getPath('userData'); - await nedbDatabase.init( - { - dbPath, - ...config, - }, - forceReset, - ); - - // Register IPC handler for renderer process bridge calls - electron.ipcMain.handle('database.invoke', async (_e, fnName: string, ...args: unknown[]) => { - const fn = mainDatabase[fnName as keyof IDatabase] as (...args: unknown[]) => unknown; - if (typeof fn !== 'function') { - throw new TypeError(`Unknown database method: ${fnName}`); - } - return fn(...args); - }); - }, - flushChanges: async function (id = 0, fake = false) { - const changes = await flushChangesImpl(id, fake); - - if (changes) { - const windows = electron.BrowserWindow.getAllWindows(); - - for (const window of windows) { - window.webContents.send('db.changes', changes); - } - } - }, -})); diff --git a/packages/insomnia/src/main/database.plugin-window.ts b/packages/insomnia/src/main/database.plugin-window.ts deleted file mode 100644 index 63758e0786..0000000000 --- a/packages/insomnia/src/main/database.plugin-window.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ipcRenderer } from 'electron'; -import type { IDatabase } from 'insomnia-data'; - -// Routes all database calls to the main process via the 'database.invoke' IPC handler -// that mainDatabase registers on startup. The plugin window must not open a second -// NeDB connection to the same files, so this proxy is the correct approach. -export const pluginWindowDatabase: IDatabase = new Proxy({} as IDatabase, { - get(_target, fnName) { - if (typeof fnName === 'symbol') { - return; - } - if (fnName === 'init') { - // Main process already initialised the database. - return async () => {}; - } - if (fnName === 'flushChanges') { - // The plugin window never needs to broadcast db.changes to other windows. - return async () => ({ changes: [], deletedIds: [] }); - } - return (...args: unknown[]) => ipcRenderer.invoke('database.invoke', fnName, ...args); - }, -}); diff --git a/packages/insomnia/src/main/ipc/database.ts b/packages/insomnia/src/main/ipc/database.ts deleted file mode 100644 index bb5db7bf0d..0000000000 --- a/packages/insomnia/src/main/ipc/database.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface DatabaseBridgeAPI { - invoke: (fnName: string, ...args: any[]) => Promise; -} diff --git a/packages/insomnia/src/main/ipc/electron.ts b/packages/insomnia/src/main/ipc/electron.ts index 40614437ef..074b0e2902 100644 --- a/packages/insomnia/src/main/ipc/electron.ts +++ b/packages/insomnia/src/main/ipc/electron.ts @@ -42,7 +42,6 @@ export type HandleChannels = | 'createPlugin' | 'curlRequest' | 'database.caCertificate.create' - | 'services.invoke' | 'extractJsonFileFromPostmanDataDumpArchive' | 'generateCommitsFromDiff' | 'generateMockRouteDataFromSpec' @@ -206,7 +205,8 @@ export type HandleChannels = | 'cookies.toString' | 'cookies.getCookiesForUrl' | 'cookies.addSetCookies' - | 'cookies.getResponseCookiesFromHeaders'; + | 'cookies.getResponseCookiesFromHeaders' + | 'data-process.request-port'; export const ipcMainHandle = ( channel: HandleChannels, @@ -269,6 +269,7 @@ export type MainOnChannels = export type RendererOnChannels = | 'contextMenuCommand' + | 'data-process.restarting' | 'db.changes' | 'edit:undo' | 'edit:redo' diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index a009e0a864..f74f42e96c 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -18,7 +18,7 @@ import { import type { UtilityProcess } from 'electron/main'; import { availableTargets, HTTPSnippet } from 'httpsnippet'; import iconv from 'iconv-lite'; -import type { AuthTypeOAuth2, OAuth2Token, RequestHeader, Services, TestResults } from 'insomnia-data'; +import type { AuthTypeOAuth2, OAuth2Token, RequestHeader, TestResults } from 'insomnia-data'; import { services } from 'insomnia-data'; import { runTests } from 'insomnia-testing/src/run/run'; @@ -399,18 +399,6 @@ export function registerMainHandlers() { ipcMainHandle('createPlugin', async (_, options: { pluginName: string; mainJs: string }) => { return createPlugin(options.pluginName, options.mainJs); }); - ipcMainHandle('services.invoke', async (_, serviceName: string, methodName: string, ...args: unknown[]) => { - const service = services[serviceName as keyof Services]; - if (!service) { - throw new TypeError(`Unknown service: ${serviceName}`); - } - const fn = service[methodName as keyof typeof service]; - if (typeof fn !== 'function') { - throw new TypeError(`Unknown service method: ${serviceName}.${methodName}`); - } - const result = await (fn as (...args: unknown[]) => unknown).call(service, ...args); - return Buffer.isBuffer(result) ? new Uint8Array(result) : result; - }); ipcMainHandle('multipartBufferToArray', async (_, options) => { return multipartBufferToArray(options); }); diff --git a/packages/insomnia/src/ui/database.client.ts b/packages/insomnia/src/ui/database.client.ts deleted file mode 100644 index 05ff7aa365..0000000000 --- a/packages/insomnia/src/ui/database.client.ts +++ /dev/null @@ -1,95 +0,0 @@ -// Bridge Database implementation for renderer process -// Uses window.database.invoke API exposed by contextBridge from preload - -import type { AllTypes, BaseModel, IDatabase, Operation, Query } from 'insomnia-data'; - -/** - * Bridge database implementation for renderer process. - * Uses window.database.invoke() API exposed by contextBridge in preload. - */ -export const database: IDatabase = { - batchModifyDocs: async function (operation: Operation) { - return window.database.invoke('batchModifyDocs', operation); - }, - - bufferChanges: async function (millis = 1000) { - return window.database.invoke('bufferChanges', millis); - }, - - bufferChangesIndefinitely: async function () { - return window.database.invoke('bufferChangesIndefinitely'); - }, - - count: async function (type: AllTypes, query: Query = {}) { - return window.database.invoke('count', type, query); - }, - - docCreate: async (type: AllTypes, ...patches: Partial[]) => { - return window.database.invoke('docCreate', type, ...patches); - }, - - docUpdate: async (originalDoc: T, ...patches: Partial[]) => { - return window.database.invoke('docUpdate', originalDoc, ...patches); - }, - - duplicate: async function (originalDoc: T, patch: Partial = {}) { - return window.database.invoke('duplicate', originalDoc, patch); - }, - - find: async function ( - type: AllTypes, - query: Query | string = {}, - sort: Record = { created: 1 }, - limit = 0, - ): Promise { - return window.database.invoke('find', type, query, sort, limit); - }, - - findOne: async function ( - type: AllTypes, - query: Query | string = {}, - sort: Record = { created: 1 }, - ): Promise { - return window.database.invoke('findOne', type, query, sort); - }, - - flushChanges: async function (id = 0, fake = false) { - return window.database.invoke('flushChanges', id, fake); - }, - - init: async () => { - // No-op for renderer process - main process handles initialization - }, - - insert: async function (doc: T) { - return window.database.invoke('insert', doc); - }, - - onChange: () => { - // No-op for renderer - change listeners are handled via IPC - }, - - remove: async function (doc: T) { - return window.database.invoke('remove', doc); - }, - - removeWhere: async function (type: AllTypes, query: Query) { - return window.database.invoke('removeWhere', type, query); - }, - - unsafeRemove: async function (doc: T) { - return window.database.invoke('unsafeRemove', doc); - }, - - update: async function (doc: T, patches: Partial[] = []) { - return window.database.invoke('update', doc, patches); - }, - - withAncestors: async function (doc: T | undefined, types: AllTypes[] = []) { - return window.database.invoke('withAncestors', doc, types); - }, - - getWithDescendants: async function (doc: T, types: AllTypes[] = []) { - return window.database.invoke('getWithDescendants', doc, types); - }, -}; diff --git a/packages/insomnia/src/ui/renderer-services-proxy.ts b/packages/insomnia/src/ui/renderer-services-proxy.ts deleted file mode 100644 index de084c80d4..0000000000 --- a/packages/insomnia/src/ui/renderer-services-proxy.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { invokeWithNormalizedError } from '~/main/ipc/invoke'; -import { createServicesProxy } from '~/ui/services-proxy'; - -export const servicesProxy = createServicesProxy((serviceName, methodName, ...args) => - invokeWithNormalizedError('services.invoke', serviceName, methodName, ...args), -); diff --git a/packages/insomnia/src/ui/services-proxy.ts b/packages/insomnia/src/ui/services-proxy.ts deleted file mode 100644 index e1e01c63da..0000000000 --- a/packages/insomnia/src/ui/services-proxy.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Services } from 'insomnia-data'; - -export type ServicesInvoke = (serviceName: string, methodName: string, ...args: unknown[]) => Promise; - -// Build the services proxy from a generic invoke function. Kept free of any -// `electron` import so it can run in the isolated renderer world, where the -// bridged invoke is the only available transport (see entry.client.tsx). -export const createServicesProxy = (invoke: ServicesInvoke): Services => - new Proxy({} as Services, { - get(_target, serviceName: string) { - return new Proxy( - {}, - { - get(_target, methodName: string) { - return (...args: unknown[]) => invoke(serviceName, methodName, ...args); - }, - }, - ); - }, - }); diff --git a/packages/insomnia/types/global.d.ts b/packages/insomnia/types/global.d.ts index 61c19659d7..9253ab1925 100644 --- a/packages/insomnia/types/global.d.ts +++ b/packages/insomnia/types/global.d.ts @@ -1,9 +1,8 @@ /// import type { HiddenBrowserWindowToMainBridgeAPI } from '../src/hidden-window-preload'; import type { RendererToMainBridgeAPI } from '../src/main/ipc/main'; -import type { DatabaseBridgeAPI } from '../src/main/ipc/database'; +import type { InvokeFn } from '../src/data-process/port-rpc'; import type { DiffMatchPatch, DiffOp } from 'diff-match-patch-ts'; -import type { Services } from 'insomnia-data'; type RendererEnv = { INSOMNIA_GITLAB_REDIRECT_URI: string | undefined; @@ -38,11 +37,7 @@ declare global { env: RendererEnv; main: RendererToMainBridgeAPI; bridge: HiddenBrowserWindowToMainBridgeAPI; - database: DatabaseBridgeAPI; - // This is a temporary measure to provide access to services on the global window object. It will be removed in the future once all usages are updated to import services directly from the insomnia-data package. - _dataServices?: Services; - // Under contextIsolation the services Proxy can't cross the bridge; the preload exposes this flat invoke instead and the renderer rebuilds the Proxy. - _dataServicesInvoke?: (serviceName: string, methodName: string, ...args: unknown[]) => Promise; + invokeDataPort: InvokeFn; dialog: Pick; app: Pick & { process: { platform: NodeJS.Platform } }; shell: Pick;