Skip to content

Commit

Permalink
feat(electron): expose electron apis to web worker (#9441)
Browse files Browse the repository at this point in the history
  • Loading branch information
pengx17 committed Dec 31, 2024
1 parent 6883cc2 commit 8877321
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 22 deletions.
5 changes: 2 additions & 3 deletions packages/frontend/apps/electron/src/preload/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import '@sentry/electron/preload';

import { contextBridge } from 'electron';

import { appInfo, getElectronAPIs } from './electron-api';
import { apis, appInfo, events, requestWebWorkerPort } from './electron-api';
import { sharedStorage } from './shared-storage';

const { apis, events } = getElectronAPIs();

contextBridge.exposeInMainWorld('__appInfo', appInfo);
contextBridge.exposeInMainWorld('__apis', apis);
contextBridge.exposeInMainWorld('__events', events);
contextBridge.exposeInMainWorld('__sharedStorage', sharedStorage);
contextBridge.exposeInMainWorld('__requestWebWorkerPort', requestWebWorkerPort);
73 changes: 57 additions & 16 deletions packages/frontend/apps/electron/src/preload/electron-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,6 @@ import {
type RendererToHelper,
} from '../shared/type';

export function getElectronAPIs() {
const mainAPIs = getMainAPIs();
const helperAPIs = getHelperAPIs();

return {
apis: {
...mainAPIs.apis,
...helperAPIs.apis,
},
events: {
...mainAPIs.events,
...helperAPIs.events,
},
};
}

type Schema =
| 'affine'
| 'affine-canary'
Expand Down Expand Up @@ -248,3 +232,60 @@ function getHelperAPIs() {
return { apis: {}, events: {} };
}
}

const mainAPIs = getMainAPIs();
const helperAPIs = getHelperAPIs();

export const apis = {
...mainAPIs.apis,
...helperAPIs.apis,
};

export const events = {
...mainAPIs.events,
...helperAPIs.events,
};

// Create MessagePort that can be used by web workers
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(() => {
window.postMessage(
{
type: 'electron:request-api-port',
portId,
ports: [remotePort],
},
'*',
[remotePort]
);
});

localPort.start();

return { portId, cleanup };
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getElectronAPIs } from '@affine/electron-api/web-worker';
import type {
AttachmentBlockModel,
BookmarkBlockModel,
Expand Down Expand Up @@ -43,6 +44,11 @@ const LRU_CACHE_SIZE = 5;
// lru cache for ydoc instances, last used at the end of the array
const lruCache = [] as { doc: YDoc; hash: string }[];

const electronAPIs = BUILD_CONFIG.isElectron ? getElectronAPIs() : null;

// @ts-expect-error test
globalThis.__electronAPIs = electronAPIs;

async function digest(data: Uint8Array) {
if (
globalThis.crypto &&
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DebugLogger } from '@affine/debug';
import { connectWebWorker } from '@affine/electron-api/web-worker';
import { MANUALLY_STOP, throwIfAborted } from '@toeverything/infra';

import type {
Expand All @@ -12,6 +13,7 @@ const logger = new DebugLogger('affine:indexer-worker');

export async function createWorker(abort: AbortSignal) {
let worker: Worker | null = null;
let electronApiCleanup: (() => void) | null = null;
while (throwIfAborted(abort)) {
try {
worker = await new Promise<Worker>((resolve, reject) => {
Expand All @@ -29,6 +31,11 @@ export async function createWorker(abort: AbortSignal) {
}
});
worker.postMessage({ type: 'init', msgId: 0 } as WorkerIngoingMessage);

if (BUILD_CONFIG.isElectron) {
electronApiCleanup = connectWebWorker(worker);
}

setTimeout(() => {
reject('timeout');
}, 1000 * 30 /* 30 sec */);
Expand Down Expand Up @@ -97,6 +104,7 @@ export async function createWorker(abort: AbortSignal) {
dispose: () => {
terminateAbort.abort(MANUALLY_STOP);
worker.terminate();
electronApiCleanup?.();
},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import {
import { useSortable } from '@dnd-kit/sortable';
import { useLiveData, useService } from '@toeverything/infra';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import type { SetStateAction } from 'jotai';
import type {
Dispatch,
HTMLAttributes,
PropsWithChildren,
RefObject,
SetStateAction,
} from 'react';
import {
memo,
Expand Down
8 changes: 6 additions & 2 deletions packages/frontend/electron-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
"private": true,
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts"
".": "./src/index.ts",
"./web-worker": "./src/web-worker.ts"
},
"dependencies": {
"async-call-rpc": "^6.4.2"
}
}
}
83 changes: 83 additions & 0 deletions packages/frontend/electron-api/src/web-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { AsyncCall, type EventBasedChannel } from 'async-call-rpc';

import type { ClientHandler } from '.';

const WORKER_PORT_MESSAGE_TYPE = 'electron-api-port';

// connect web worker to preload, so that the web worker can use the electron APIs
export function connectWebWorker(worker: Worker) {
const { portId, cleanup } = (globalThis as any).__requestWebWorkerPort();

const portMessageListener = (event: MessageEvent) => {
if (
event.data.type === 'electron:request-api-port' &&
event.data.portId === portId
) {
const [port] = event.data.ports as MessagePort[];

// worker should be ready to receive message
worker.postMessage(
{
type: WORKER_PORT_MESSAGE_TYPE,
ports: [port],
},
[port]
);
}
};

window.addEventListener('message', portMessageListener);

return () => {
window.removeEventListener('message', portMessageListener);
cleanup();
};
}

const createMessagePortChannel = (port: MessagePort): EventBasedChannel => {
return {
on(listener) {
port.onmessage = e => {
listener(e.data);
};
port.start();
return () => {
port.onmessage = null;
try {
port.close();
} catch (err) {
console.error('[worker] close port error', err);
}
};
},
send(data) {
port.postMessage(data);
},
};
};

// get the electron APIs for the web worker (should be called in the web worker)
export function getElectronAPIs(): ClientHandler {
const { promise, resolve } = Promise.withResolvers<MessagePort>();
globalThis.addEventListener('message', event => {
if (event.data.type === WORKER_PORT_MESSAGE_TYPE) {
const [port] = event.ports;
resolve(port);
}
});

const rpc = AsyncCall<Record<string, any>>(null, {
channel: promise.then(p => createMessagePortChannel(p)),
log: false,
});

return new Proxy<ClientHandler>(rpc as any, {
get(_, namespace: string) {
return new Proxy(rpc as any, {
get(_, method: string) {
return rpc[`${namespace}:${method}`];
},
});
},
});
}
2 changes: 2 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,8 @@ __metadata:
"@affine/electron-api@workspace:*, @affine/electron-api@workspace:packages/frontend/electron-api":
version: 0.0.0-use.local
resolution: "@affine/electron-api@workspace:packages/frontend/electron-api"
dependencies:
async-call-rpc: "npm:^6.4.2"
languageName: unknown
linkType: soft

Expand Down

0 comments on commit 8877321

Please sign in to comment.