feat(core): new worker workspace engine (#9257)

This commit is contained in:
EYHN
2025-01-17 00:22:18 +08:00
committed by GitHub
parent 7dc470e7ea
commit a2ffdb4047
219 changed files with 4267 additions and 7194 deletions

View File

@@ -15,6 +15,7 @@ import {
switchToPreviousTab,
undoCloseTab,
} from '../windows-manager';
import { WorkerManager } from '../worker/pool';
import { applicationMenuSubjects } from './subject';
// Unique id for menuitems
@@ -113,6 +114,21 @@ export function createApplicationMenu() {
showDevTools();
},
},
{
label: 'Open worker devtools',
click: () => {
Menu.buildFromTemplate(
Array.from(WorkerManager.instance.workers.values()).map(item => ({
label: `${item.key}`,
click: () => {
item.browserWindow.webContents.openDevTools({
mode: 'undocked',
});
},
}))
).popup();
},
},
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
@@ -199,7 +215,7 @@ export function createApplicationMenu() {
{
label: 'Learn More',
click: async () => {
// oxlint-disable-next-line
// oxlint-disable-next-line no-var-requires
const { shell } = require('electron');
await shell.openExternal('https://affine.pro/');
},
@@ -220,7 +236,7 @@ export function createApplicationMenu() {
{
label: 'Documentation',
click: async () => {
// oxlint-disable-next-line
// oxlint-disable-next-line no-var-requires
const { shell } = require('electron');
await shell.openExternal(
'https://docs.affine.pro/docs/hello-bonjour-aloha-你好'

View File

@@ -1,4 +1,5 @@
export const mainWindowOrigin = process.env.DEV_SERVER_URL || 'file://.';
export const onboardingViewUrl = `${mainWindowOrigin}${mainWindowOrigin.endsWith('/') ? '' : '/'}onboarding`;
export const shellViewUrl = `${mainWindowOrigin}${mainWindowOrigin.endsWith('/') ? '' : '/'}shell.html`;
export const backgroundWorkerViewUrl = `${mainWindowOrigin}${mainWindowOrigin.endsWith('/') ? '' : '/'}background-worker.html`;
export const customThemeViewUrl = `${mainWindowOrigin}${mainWindowOrigin.endsWith('/') ? '' : '/'}theme-editor`;

View File

@@ -8,6 +8,7 @@ import { getLogFilePath, logger, revealLogFile } from './logger';
import { sharedStorageHandlers } from './shared-storage';
import { uiHandlers } from './ui/handlers';
import { updaterHandlers } from './updater';
import { workerHandlers } from './worker/handlers';
export const debugHandlers = {
revealLogFile: async () => {
@@ -27,6 +28,7 @@ export const allHandlers = {
configStorage: configStorageHandlers,
findInPage: findInPageHandlers,
sharedStorage: sharedStorageHandlers,
worker: workerHandlers,
};
export const registerHandlers = () => {

View File

@@ -25,7 +25,6 @@ import {
import { isMacOS } from '../../shared/utils';
import { beforeAppQuit } from '../cleanup';
import { isDev } from '../config';
import { mainWindowOrigin, shellViewUrl } from '../constants';
import { ensureHelperProcess } from '../helper-process';
import { logger } from '../logger';
@@ -871,9 +870,6 @@ export class WebContentViewsManager {
});
view.webContents.loadURL(shellViewUrl).catch(err => logger.error(err));
if (isDev) {
view.webContents.openDevTools();
}
}
view.webContents.on('destroyed', () => {

View File

@@ -0,0 +1,19 @@
import type { NamespaceHandlers } from '../type';
import { WorkerManager } from './pool';
export const workerHandlers = {
connectWorker: async (e, key: string, portId: string) => {
const { portForRenderer } = await WorkerManager.instance.connectWorker(
key,
portId,
e.sender
);
e.sender.postMessage('worker-connect', { portId }, [portForRenderer]);
return {
portId: portId,
};
},
disconnectWorker: async (_, key: string, portId: string) => {
WorkerManager.instance.disconnectWorker(key, portId);
},
} satisfies NamespaceHandlers;

View File

@@ -0,0 +1,96 @@
import { join } from 'node:path';
import { BrowserWindow, MessageChannelMain, type WebContents } from 'electron';
import { backgroundWorkerViewUrl } from '../constants';
import { ensureHelperProcess } from '../helper-process';
import { logger } from '../logger';
async function getAdditionalArguments() {
const { getExposedMeta } = await import('../exposed');
const mainExposedMeta = getExposedMeta();
const helperProcessManager = await ensureHelperProcess();
const helperExposedMeta = await helperProcessManager.rpc?.getMeta();
return [
`--main-exposed-meta=` + JSON.stringify(mainExposedMeta),
`--helper-exposed-meta=` + JSON.stringify(helperExposedMeta),
`--window-name=worker`,
];
}
export class WorkerManager {
static readonly instance = new WorkerManager();
workers = new Map<
string,
{ browserWindow: BrowserWindow; ports: Set<string>; key: string }
>();
private async getOrCreateWorker(key: string) {
const additionalArguments = await getAdditionalArguments();
const helperProcessManager = await ensureHelperProcess();
const exists = this.workers.get(key);
if (exists) {
return exists;
} else {
const worker = new BrowserWindow({
width: 1200,
height: 600,
webPreferences: {
preload: join(__dirname, './preload.js'),
additionalArguments: additionalArguments,
},
show: false,
});
let disconnectHelperProcess: (() => void) | null = null;
worker.on('close', e => {
e.preventDefault();
if (worker && !worker.isDestroyed()) {
worker.destroy();
this.workers.delete(key);
disconnectHelperProcess?.();
}
});
worker.loadURL(backgroundWorkerViewUrl).catch(e => {
logger.error('failed to load url', e);
});
worker.webContents.addListener('did-finish-load', () => {
disconnectHelperProcess = helperProcessManager.connectRenderer(
worker.webContents
);
});
const record = { browserWindow: worker, ports: new Set<string>(), key };
this.workers.set(key, record);
return record;
}
}
async connectWorker(
key: string,
portId: string,
bindWebContent: WebContents
) {
bindWebContent.addListener('destroyed', () => {
this.disconnectWorker(key, portId);
});
const worker = await this.getOrCreateWorker(key);
const { port1: portForWorker, port2: portForRenderer } =
new MessageChannelMain();
worker.browserWindow.webContents.postMessage('worker-connect', { portId }, [
portForWorker,
]);
return { portForRenderer, portId };
}
disconnectWorker(key: string, portId: string) {
const worker = this.workers.get(key);
if (worker) {
worker.ports.delete(portId);
if (worker.ports.size === 0) {
worker.browserWindow.destroy();
this.workers.delete(key);
}
}
}
}

View File

@@ -2,11 +2,13 @@ import '@sentry/electron/preload';
import { contextBridge } from 'electron';
import { apis, appInfo, events, requestWebWorkerPort } from './electron-api';
import { apis, appInfo, events } from './electron-api';
import { sharedStorage } from './shared-storage';
import { listenWorkerApis } from './worker';
contextBridge.exposeInMainWorld('__appInfo', appInfo);
contextBridge.exposeInMainWorld('__apis', apis);
contextBridge.exposeInMainWorld('__events', events);
contextBridge.exposeInMainWorld('__sharedStorage', sharedStorage);
contextBridge.exposeInMainWorld('__requestWebWorkerPort', requestWebWorkerPort);
listenWorkerApis();

View File

@@ -248,53 +248,3 @@ export const events = {
...mainAPIs.events,
...helperAPIs.events,
};
/**
* Create MessagePort that can be used by web workers
*
* !!!
* SHOULD ONLY BE USED IN RENDERER PROCESS
* !!!
*/
export function requestWebWorkerPort() {
const ch = new MessageChannel();
const localPort = ch.port1;
const remotePort = ch.port2;
// todo: should be able to let the web worker use the electron APIs directly for better performance
const flattenedAPIs = Object.entries(apis).flatMap(([namespace, api]) => {
return Object.entries(api as any).map(([method, fn]) => [
`${namespace}:${method}`,
fn,
]);
});
AsyncCall(Object.fromEntries(flattenedAPIs), {
channel: createMessagePortChannel(localPort),
log: false,
});
const cleanup = () => {
remotePort.close();
localPort.close();
};
const portId = crypto.randomUUID();
setTimeout(() => {
// @ts-expect-error this function should only be evaluated in the renderer process
window.postMessage(
{
type: 'electron:request-api-port',
portId,
ports: [remotePort],
},
'*',
[remotePort]
);
});
localPort.start();
return { portId, cleanup };
}

View File

@@ -0,0 +1,33 @@
import { ipcRenderer } from 'electron';
export function listenWorkerApis() {
ipcRenderer.on('worker-connect', (ev, data) => {
const portForRenderer = ev.ports[0];
// @ts-expect-error this function should only be evaluated in the renderer process
if (document.readyState === 'complete') {
// @ts-expect-error this function should only be evaluated in the renderer process
window.postMessage(
{
type: 'electron:worker-connect',
portId: data.portId,
},
'*',
[portForRenderer]
);
} else {
// @ts-expect-error this function should only be evaluated in the renderer process
window.addEventListener('load', () => {
// @ts-expect-error this function should only be evaluated in the renderer process
window.postMessage(
{
type: 'electron:worker-connect',
portId: data.portId,
},
'*',
[portForRenderer]
);
});
}
});
}