mirror of
https://github.com/Kong/insomnia.git
synced 2026-07-30 17:26:28 -04:00
refactor: move database and services to a dedicated utility process
Replace IPC-based database/services bridge (database.main, database.client, services-proxy) with a MessagePort RPC layer backed by an Electron utilityProcess (entry.data.ts). - Add PortRpc transport, data-process server, and crash-restart manager - Unify all entry points to use initDataBridge(invokeDataPort)
This commit is contained in:
16
.vscode/launch.json
vendored
16
.vscode/launch.json
vendored
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
93
packages/insomnia/src/data-process/README.md
Normal file
93
packages/insomnia/src/data-process/README.md
Normal file
@@ -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.
|
||||
41
packages/insomnia/src/data-process/data-port-preload.ts
Normal file
41
packages/insomnia/src/data-process/data-port-preload.ts
Normal file
@@ -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;
|
||||
}
|
||||
125
packages/insomnia/src/data-process/data-process-manager.ts
Normal file
125
packages/insomnia/src/data-process/data-process-manager.ts
Normal file
@@ -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<string, unknown> {
|
||||
return event as unknown as Record<string, unknown>;
|
||||
}
|
||||
|
||||
// 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<void> {
|
||||
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<void>((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);
|
||||
});
|
||||
}
|
||||
47
packages/insomnia/src/data-process/init-data-bridge.ts
Normal file
47
packages/insomnia/src/data-process/init-data-bridge.ts
Normal file
@@ -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<IDatabase>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<IDatabase> overrides (e.g. onChange for main process).
|
||||
*/
|
||||
export async function initDataBridge(
|
||||
invoke: InvokeFn | undefined,
|
||||
options?: DataBridgeOptions,
|
||||
): Promise<void> {
|
||||
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);
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
54
packages/insomnia/src/data-process/port-rpc.ts
Normal file
54
packages/insomnia/src/data-process/port-rpc.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { deserializeError, type SerializedError } from './serialization';
|
||||
|
||||
export type InvokeFn = (namespace: 'services' | 'database', method: string, ...args: unknown[]) => Promise<unknown>;
|
||||
|
||||
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<string, { resolve: (v: unknown) => 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 });
|
||||
});
|
||||
};
|
||||
}
|
||||
19
packages/insomnia/src/data-process/serialization.ts
Normal file
19
packages/insomnia/src/data-process/serialization.ts
Normal file
@@ -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;
|
||||
}
|
||||
87
packages/insomnia/src/data-process/server.ts
Normal file
87
packages/insomnia/src/data-process/server.ts
Normal file
@@ -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<unknown> {
|
||||
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<void> {
|
||||
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<string, unknown>;
|
||||
|
||||
if (msg?.type === 'new-port') {
|
||||
const port = event.ports[0];
|
||||
if (port) {
|
||||
attachPort(port);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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) }));
|
||||
|
||||
53
packages/insomnia/src/entry.data.ts
Normal file
53
packages/insomnia/src/entry.data.ts
Normal file
@@ -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 });
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, any>) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 PluginInvokeMethod> = T extends keyof PluginsBridgeAPI
|
||||
? Awaited<ReturnType<PluginsBridgeAPI[T]>>
|
||||
: 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;
|
||||
}
|
||||
|
||||
@@ -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 } });
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
export interface DatabaseBridgeAPI {
|
||||
invoke: <T = any>(fnName: string, ...args: any[]) => Promise<T>;
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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<void>('batchModifyDocs', operation);
|
||||
},
|
||||
|
||||
bufferChanges: async function (millis = 1000) {
|
||||
return window.database.invoke<number>('bufferChanges', millis);
|
||||
},
|
||||
|
||||
bufferChangesIndefinitely: async function () {
|
||||
return window.database.invoke<number>('bufferChangesIndefinitely');
|
||||
},
|
||||
|
||||
count: async function <T extends BaseModel>(type: AllTypes, query: Query<T> = {}) {
|
||||
return window.database.invoke<number>('count', type, query);
|
||||
},
|
||||
|
||||
docCreate: async <T extends BaseModel>(type: AllTypes, ...patches: Partial<T>[]) => {
|
||||
return window.database.invoke<T>('docCreate', type, ...patches);
|
||||
},
|
||||
|
||||
docUpdate: async <T extends BaseModel>(originalDoc: T, ...patches: Partial<T>[]) => {
|
||||
return window.database.invoke<T>('docUpdate', originalDoc, ...patches);
|
||||
},
|
||||
|
||||
duplicate: async function <T extends BaseModel>(originalDoc: T, patch: Partial<T> = {}) {
|
||||
return window.database.invoke<T>('duplicate', originalDoc, patch);
|
||||
},
|
||||
|
||||
find: async function <T extends BaseModel>(
|
||||
type: AllTypes,
|
||||
query: Query<T> | string = {},
|
||||
sort: Record<string, any> = { created: 1 },
|
||||
limit = 0,
|
||||
): Promise<T[]> {
|
||||
return window.database.invoke<T[]>('find', type, query, sort, limit);
|
||||
},
|
||||
|
||||
findOne: async function <T extends BaseModel>(
|
||||
type: AllTypes,
|
||||
query: Query<T> | string = {},
|
||||
sort: Record<string, any> = { created: 1 },
|
||||
): Promise<T | undefined> {
|
||||
return window.database.invoke<T | undefined>('findOne', type, query, sort);
|
||||
},
|
||||
|
||||
flushChanges: async function (id = 0, fake = false) {
|
||||
return window.database.invoke<void>('flushChanges', id, fake);
|
||||
},
|
||||
|
||||
init: async () => {
|
||||
// No-op for renderer process - main process handles initialization
|
||||
},
|
||||
|
||||
insert: async function <T extends BaseModel>(doc: T) {
|
||||
return window.database.invoke<T>('insert', doc);
|
||||
},
|
||||
|
||||
onChange: () => {
|
||||
// No-op for renderer - change listeners are handled via IPC
|
||||
},
|
||||
|
||||
remove: async function <T extends BaseModel>(doc: T) {
|
||||
return window.database.invoke<void>('remove', doc);
|
||||
},
|
||||
|
||||
removeWhere: async function <T extends BaseModel>(type: AllTypes, query: Query<T>) {
|
||||
return window.database.invoke<void>('removeWhere', type, query);
|
||||
},
|
||||
|
||||
unsafeRemove: async function <T extends BaseModel>(doc: T) {
|
||||
return window.database.invoke<void>('unsafeRemove', doc);
|
||||
},
|
||||
|
||||
update: async function <T extends BaseModel>(doc: T, patches: Partial<T>[] = []) {
|
||||
return window.database.invoke<T>('update', doc, patches);
|
||||
},
|
||||
|
||||
withAncestors: async function <T extends BaseModel>(doc: T | undefined, types: AllTypes[] = []) {
|
||||
return window.database.invoke<T[]>('withAncestors', doc, types);
|
||||
},
|
||||
|
||||
getWithDescendants: async function <T extends BaseModel>(doc: T, types: AllTypes[] = []) {
|
||||
return window.database.invoke<BaseModel[]>('getWithDescendants', doc, types);
|
||||
},
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { invokeWithNormalizedError } from '~/main/ipc/invoke';
|
||||
import { createServicesProxy } from '~/ui/services-proxy';
|
||||
|
||||
export const servicesProxy = createServicesProxy((serviceName, methodName, ...args) =>
|
||||
invokeWithNormalizedError<unknown>('services.invoke', serviceName, methodName, ...args),
|
||||
);
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { Services } from 'insomnia-data';
|
||||
|
||||
export type ServicesInvoke = (serviceName: string, methodName: string, ...args: unknown[]) => Promise<unknown>;
|
||||
|
||||
// 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);
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
9
packages/insomnia/types/global.d.ts
vendored
9
packages/insomnia/types/global.d.ts
vendored
@@ -1,9 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
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<unknown>;
|
||||
invokeDataPort: InvokeFn;
|
||||
dialog: Pick<Electron.Dialog, 'showOpenDialog' | 'showSaveDialog'>;
|
||||
app: Pick<Electron.App, 'getPath' | 'getAppPath'> & { process: { platform: NodeJS.Platform } };
|
||||
shell: Pick<Electron.Shell, 'showItemInFolder' | 'openPath'>;
|
||||
|
||||
Reference in New Issue
Block a user