feat(electron): move preload to infra (#3011)

This commit is contained in:
Alex Yang
2023-07-05 00:43:30 +08:00
committed by GitHub
parent 24be73ef63
commit dfbec46ded
20 changed files with 372 additions and 188 deletions

View File

@@ -1,3 +1,4 @@
import type { RendererToHelper } from '@toeverything/infra/preload/electron';
import { AsyncCall } from 'async-call-rpc';
import { events, handlers } from './exposed';
@@ -30,7 +31,7 @@ function setupRendererConnection(rendererPort: Electron.MessagePortMain) {
});
}
);
const rpc = AsyncCall<PeersAPIs.RendererToHelper>(
const rpc = AsyncCall<RendererToHelper>(
Object.fromEntries(flattenedHandlers),
{
channel: {

View File

@@ -1,12 +1,16 @@
import type {
HelperToMain,
MainToHelper,
} from '@toeverything/infra/preload/electron';
import { AsyncCall } from 'async-call-rpc';
import { getExposedMeta } from './exposed';
const helperToMainServer: PeersAPIs.HelperToMain = {
const helperToMainServer: HelperToMain = {
getMeta: () => getExposedMeta(),
};
export const mainRPC = AsyncCall<PeersAPIs.MainToHelper>(helperToMainServer, {
export const mainRPC = AsyncCall<MainToHelper>(helperToMainServer, {
strict: {
unknownMessage: false,
},

View File

@@ -1,5 +1,9 @@
import path from 'node:path';
import type {
HelperToMain,
MainToHelper,
} from '@toeverything/infra/preload/electron';
import { type _AsyncVersionOf, AsyncCall } from 'async-call-rpc';
import {
app,
@@ -36,7 +40,7 @@ class HelperProcessManager {
#process: UtilityProcess;
// a rpc server for the main process -> helper process
rpc?: _AsyncVersionOf<PeersAPIs.HelperToMain>;
rpc?: _AsyncVersionOf<HelperToMain>;
static instance = new HelperProcessManager();
@@ -86,13 +90,13 @@ class HelperProcessManager {
]);
const appMethods = pickAndBind(app, ['getPath']);
const mainToHelperServer: PeersAPIs.MainToHelper = {
const mainToHelperServer: MainToHelper = {
...dialogMethods,
...shellMethods,
...appMethods,
};
this.rpc = AsyncCall<PeersAPIs.HelperToMain>(mainToHelperServer, {
this.rpc = AsyncCall<HelperToMain>(mainToHelperServer, {
strict: {
// the channel is shared for other purposes as well so that we do not want to
// restrict to only JSONRPC messages

View File

@@ -1,193 +0,0 @@
// NOTE: we will generate preload types from this file
import { AsyncCall, type EventBasedChannel } from 'async-call-rpc';
import { ipcRenderer } from 'electron';
import { Subject } from 'rxjs';
type ExposedMeta = {
handlers: [namespace: string, handlerNames: string[]][];
events: [namespace: string, eventNames: string[]][];
};
export function getAffineAPIs() {
const mainAPIs = getMainAPIs();
const helperAPIs = getHelperAPIs();
return {
apis: {
...mainAPIs.apis,
...helperAPIs.apis,
},
events: {
...mainAPIs.events,
...helperAPIs.events,
},
};
}
export const appInfo = {
electron: true,
};
function getMainAPIs() {
const meta: ExposedMeta = (() => {
const val = process.argv
.find(arg => arg.startsWith('--main-exposed-meta='))
?.split('=')[1];
return val ? JSON.parse(val) : null;
})();
// main handlers that can be invoked from the renderer process
const apis: any = (() => {
const { handlers: handlersMeta } = meta;
const all = handlersMeta.map(([namespace, functionNames]) => {
const namespaceApis = functionNames.map(name => {
const channel = `${namespace}:${name}`;
return [
name,
(...args: any[]) => {
return ipcRenderer.invoke(channel, ...args);
},
];
});
return [namespace, Object.fromEntries(namespaceApis)];
});
return Object.fromEntries(all);
})();
// main events that can be listened to from the renderer process
const events: any = (() => {
const { events: eventsMeta } = meta;
// NOTE: ui may try to listen to a lot of the same events, so we increase the limit...
ipcRenderer.setMaxListeners(100);
const all = eventsMeta.map(([namespace, eventNames]) => {
const namespaceEvents = eventNames.map(name => {
const channel = `${namespace}:${name}`;
return [
name,
(callback: (...args: any[]) => void) => {
const fn: (
event: Electron.IpcRendererEvent,
...args: any[]
) => void = (_, ...args) => {
callback(...args);
};
ipcRenderer.on(channel, fn);
return () => {
ipcRenderer.off(channel, fn);
};
},
];
});
return [namespace, Object.fromEntries(namespaceEvents)];
});
return Object.fromEntries(all);
})();
return { apis, events };
}
const helperPort$ = new Promise<MessagePort>(resolve =>
ipcRenderer.on('helper-connection', async e => {
console.info('[preload] helper-connection', e);
resolve(e.ports[0]);
})
);
const createMessagePortChannel = (port: MessagePort): EventBasedChannel => {
return {
on(listener) {
port.onmessage = e => {
listener(e.data);
};
port.start();
return () => {
port.onmessage = null;
port.close();
};
},
send(data) {
port.postMessage(data);
},
};
};
function getHelperAPIs() {
const events$ = new Subject<{ channel: string; args: any[] }>();
const meta: ExposedMeta = (() => {
const val = process.argv
.find(arg => arg.startsWith('--helper-exposed-meta='))
?.split('=')[1];
return val ? JSON.parse(val) : null;
})();
const rendererToHelperServer: PeersAPIs.RendererToHelper = {
postEvent: (channel, ...args) => {
events$.next({ channel, args });
},
};
const rpc = AsyncCall<PeersAPIs.HelperToRenderer>(rendererToHelperServer, {
channel: helperPort$.then(helperPort =>
createMessagePortChannel(helperPort)
),
log: false,
});
const toHelperHandler = (namespace: string, name: string) => {
return rpc[`${namespace}:${name}`];
};
const toHelperEventSubscriber = (namespace: string, name: string) => {
return (callback: (...args: any[]) => void) => {
const subscription = events$.subscribe(({ channel, args }) => {
if (channel === `${namespace}:${name}`) {
callback(...args);
}
});
return () => {
subscription.unsubscribe();
};
};
};
const setup = (meta: ExposedMeta) => {
const { handlers: handlersMeta, events: eventsMeta } = meta;
const helperHandlers = Object.fromEntries(
handlersMeta.map(([namespace, functionNames]) => {
return [
namespace,
Object.fromEntries(
functionNames.map(name => {
return [name, toHelperHandler(namespace, name)];
})
),
];
})
);
const helperEvents = Object.fromEntries(
eventsMeta.map(([namespace, eventNames]) => {
return [
namespace,
Object.fromEntries(
eventNames.map(name => {
return [name, toHelperEventSubscriber(namespace, name)];
})
),
];
})
);
return [helperHandlers, helperEvents];
};
const [apis, events] = setup(meta);
return { apis, events };
}

View File

@@ -1,8 +1,10 @@
import { contextBridge, ipcRenderer } from 'electron';
(async () => {
const { appInfo, getAffineAPIs } = await import('./affine-apis');
const { apis, events } = getAffineAPIs();
const { appInfo, getElectronAPIs } = await import(
'@toeverything/infra/preload/electron'
);
const { apis, events } = getElectronAPIs();
contextBridge.exposeInMainWorld('appInfo', appInfo);
contextBridge.exposeInMainWorld('apis', apis);

View File

@@ -1,35 +0,0 @@
declare namespace PeersAPIs {
import type { app, dialog, shell } from 'electron';
interface ExposedMeta {
handlers: [string, string[]][];
events: [string, string[]][];
}
// render <-> helper
interface RendererToHelper {
postEvent: (channel: string, ...args: any[]) => void;
}
interface HelperToRenderer {
[key: string]: (...args: any[]) => Promise<any>;
}
// helper <-> main
interface HelperToMain {
getMeta: () => ExposedMeta;
}
type MainToHelper = Pick<
typeof dialog & typeof shell & typeof app,
| 'showOpenDialog'
| 'showSaveDialog'
| 'openExternal'
| 'showItemInFolder'
| 'getPath'
>;
// render <-> main
// these are handled via IPC
// TODO: fix type
}