diff --git a/src/components/frame/XImgCache.ts b/src/components/frame/XImgCache.ts index 28ab824f..b441a8f5 100644 --- a/src/components/frame/XImgCache.ts +++ b/src/components/frame/XImgCache.ts @@ -1,11 +1,10 @@ import { API } from '@services/API'; import { onDOMLoaded } from '@services/utils'; -import { workerImporter } from '@services/worker'; -import type * as w from './XImgWorker'; +import { importWorker } from '@services/worker'; +import type XImgWorker from './XImgWorker'; // Global web worker to fetch images -let worker: Worker; -let importer: ReturnType; +let worker: typeof XImgWorker; // Memcache for blob URLs const BLOB_CACHE = new Map() as Map; @@ -17,12 +16,11 @@ const BLOB_STICKY = new Map(); function startWorker() { if (worker || _m.mode !== 'user') return; - // Start worker - worker = new Worker(new URL('./XImgWorkerStub.ts', import.meta.url)); - importer = workerImporter(worker); + // Get typed worker + worker = importWorker(new Worker(new URL('./XImgWorkerStub.ts', import.meta.url))); // Configure worker - importer('configure')({ + worker.configure({ multiUrl: API.IMAGE_MULTIPREVIEW(), }); } @@ -69,7 +67,7 @@ export async function fetchImage(url: string) { if (entry) return entry[1]; // Fetch image - const blobUrl = await importer('fetchImageSrc')(url); + const blobUrl = await worker.fetchImageSrc(url); // Check memcache entry again and revoke if it was added in the meantime if ((entry = BLOB_CACHE.get(url))) { diff --git a/src/components/frame/XImgWorker.ts b/src/components/frame/XImgWorker.ts index 163d9709..65247730 100644 --- a/src/components/frame/XImgWorker.ts +++ b/src/components/frame/XImgWorker.ts @@ -1,5 +1,5 @@ import { CacheExpiration } from 'workbox-expiration'; -import { workerExport } from '@services/worker'; +import { exportWorker } from '@services/worker'; declare var self: ServiceWorkerGlobalScope; @@ -312,14 +312,14 @@ async function fetchMultipreview(files: any[]) { /** Will be configured after the worker starts */ let config: { multiUrl: string }; -export async function configure(_config: typeof config) { +function configure(_config: typeof config) { config = _config; } /** Get BLOB url for image */ -export async function fetchImageSrc(url: string) { +async function fetchImageSrc(url: string) { return URL.createObjectURL(await fetchImage(url)); } // Exports to main thread -workerExport({ fetchImageSrc, configure }); +export default exportWorker({ fetchImageSrc, configure }); diff --git a/src/services/worker.ts b/src/services/worker.ts index 9c60c142..12badf07 100644 --- a/src/services/worker.ts +++ b/src/services/worker.ts @@ -1,51 +1,105 @@ -/** Set the receiver function for a worker */ -export function workerExport(handlers: Record Promise>): void { - /** Promise API for web worker */ - self.onmessage = async ({ - data, - }: { - data: { - id: number; - name: string; - args: any[]; - }; - }) => { +/** + * Data sent from main thread to worker. + */ +type CommRequest = { + reqid: number; + name: string; + args: any[]; +}; + +/** + * Data sent from worker to main thread. + */ +type CommResult = { + reqid: number; + resolve?: any; + reject?: string; +}; + +/** + * Export methods from a worker to the main thread. + * + * @param handlers Object with methods to export + * + * @example + * ```ts + * // my-worker.ts + * function foo() { return 'bar'; } + * + * async function asyncFoo() { return 'bar'; } + * + * export default exportWorker({ + * foo, + * asyncFoo, + * inline: () => 'bar', + * }); + */ +export function exportWorker(handlers: T): T { + self.onmessage = async ({ data }: { data: CommRequest }) => { try { + // Get handler from registrations const handler = handlers[data.name]; - if (!handler) throw new Error(`No handler for type ${data.name}`); - const res = await handler.apply(self, data.args); - self.postMessage({ - id: data.id, - resolve: res, - }); + if (!handler) throw new Error(`[BUG] No handler for type ${data.name}`); + + // Run handler + let result = handler.apply(self, data.args); + if (result instanceof Promise) { + result = await result; + } + + // Success - post back to main thread + self.postMessage({ reqid: data.reqid, resolve: result } as CommResult); } catch (e) { - self.postMessage({ - id: data.id, - reject: e.message, - }); + // Error - post back rejection + self.postMessage({ reqid: data.reqid, reject: e.message } as CommResult); } }; + + return null as unknown as T; } -/** Get the CALL function for a worker. Call this only once. */ -export function workerImporter(worker: Worker) { - const promises = new Map(); +/** + * Import a worker exported with `exportWorker`. + * + * @param worker Worker to import + * + * @example + * ```ts + * // main.ts + * import type MyWorker from './my-worker.ts'; + * + * const worker = importWorker(new Worker(new URL('./XImgWorkerStub.ts', import.meta.url))); + * + * async (() => { + * // all methods are async + * console.assert(await worker.foo() === 'bar'); + * console.assert(await worker.asyncFoo() === 'bar'); + * console.assert(await worker.inline() === 'bar'); + * }); + */ +export function importWorker(worker: Worker) { + const promises = new Map(); - worker.onmessage = ({ data }: { data: any }) => { - const { id, resolve, reject } = data; - if (resolve) promises.get(id)?.resolve(resolve); - if (reject) promises.get(id)?.reject(reject); - promises.delete(id); + // Handle messages from worker + worker.onmessage = ({ data }: { data: CommResult }) => { + const { reqid, resolve, reject } = data; + if (resolve) promises.get(reqid)?.resolve(resolve); + if (reject) promises.get(reqid)?.reject(reject); + promises.delete(reqid); }; - type PromiseFun = (...args: any) => Promise; - return function importer(name: string) { - return async function fun(...args: Parameters) { - return await new Promise>>((resolve, reject) => { - const id = Math.random(); - promises.set(id, { resolve, reject }); - worker.postMessage({ id, name, args }); - }); - }; - }; + // Create proxy to call worker methods + const proxy = new Proxy(worker, { + get(target: Worker, name: string) { + return async function wrapper(...args: any[]) { + return await new Promise((resolve, reject) => { + const reqid = Math.random(); + promises.set(reqid, { resolve, reject }); + target.postMessage({ reqid, name, args } as CommRequest); + }); + }; + }, + }); + + return proxy as T; }