diff --git a/packages/excalidraw/subset/subset-main.ts b/packages/excalidraw/subset/subset-main.ts index 2bde24c6c..d5e4ba7be 100644 --- a/packages/excalidraw/subset/subset-main.ts +++ b/packages/excalidraw/subset/subset-main.ts @@ -23,7 +23,7 @@ export const subsetWoff2GlyphsByCodepoints = async ( codePoints: Array, ): Promise => { const { Commands, subsetToBase64, toBase64 } = - await lazyLoadSubsetSharedChunk(); + await lazyLoadSharedSubsetChunk(); if (!shouldUseWorkers) { return subsetToBase64(arrayBuffer, codePoints); @@ -75,7 +75,7 @@ export const subsetWoff2GlyphsByCodepoints = async ( let subsetWorker: Promise | null = null; let subsetShared: Promise | null = null; -const lazyLoadSubsetWorkerChunk = async () => { +const lazyLoadWorkerSubsetChunk = async () => { if (!subsetWorker) { subsetWorker = import("./subset-worker.chunk"); } @@ -83,7 +83,7 @@ const lazyLoadSubsetWorkerChunk = async () => { return subsetWorker; }; -const lazyLoadSubsetSharedChunk = async () => { +const lazyLoadSharedSubsetChunk = async () => { if (!subsetShared) { // load dynamically to force create a shared chunk reused between main thread and the worker thread subsetShared = import("./subset-shared.chunk"); @@ -93,20 +93,17 @@ const lazyLoadSubsetSharedChunk = async () => { }; // could be extended with multiple commands in the future -export type SubsetWorkerInput = { +type SubsetWorkerData = { command: typeof Commands.Subset; arrayBuffer: ArrayBuffer; codePoints: Array; }; -export type SubsetWorkerOutput = +type SubsetWorkerResult = T extends typeof Commands.Subset ? ArrayBuffer : never; let workerPool: Promise< - WorkerPool< - SubsetWorkerInput, - SubsetWorkerOutput - > + WorkerPool> > | null = null; /** @@ -118,11 +115,11 @@ const getOrCreateWorkerPool = () => { if (!workerPool) { // immediate concurrent-friendly return, to ensure we have only one pool instance workerPool = promiseTry(async () => { - const { WorkerUrl } = await lazyLoadSubsetWorkerChunk(); + const { WorkerUrl } = await lazyLoadWorkerSubsetChunk(); const pool = WorkerPool.create< - SubsetWorkerInput, - SubsetWorkerOutput + SubsetWorkerData, + SubsetWorkerResult >(WorkerUrl); return pool; diff --git a/packages/excalidraw/subset/subset-worker.chunk.ts b/packages/excalidraw/subset/subset-worker.chunk.ts index b689b91cf..5f4e92bfc 100644 --- a/packages/excalidraw/subset/subset-worker.chunk.ts +++ b/packages/excalidraw/subset/subset-worker.chunk.ts @@ -9,8 +9,6 @@ import { Commands, subsetToBinary } from "./subset-shared.chunk"; -import type { SubsetWorkerInput } from "./subset-main"; - /** * Due to this export (and related dynamic import), this worker code will be included in the bundle automatically (as a separate chunk), * without the need for esbuild / vite /rollup plugins and special browser / server treatment. @@ -23,7 +21,13 @@ export const WorkerUrl: URL | undefined = import.meta.url // run only in the worker context if (typeof window === "undefined" && typeof self !== "undefined") { - self.onmessage = async (e: MessageEvent) => { + self.onmessage = async (e: { + data: { + command: typeof Commands.Subset; + arrayBuffer: ArrayBuffer; + codePoints: Array; + }; + }) => { switch (e.data.command) { case Commands.Subset: const buffer = await subsetToBinary( diff --git a/packages/excalidraw/workers.ts b/packages/excalidraw/workers.ts index 9de0f33a8..38efda102 100644 --- a/packages/excalidraw/workers.ts +++ b/packages/excalidraw/workers.ts @@ -16,28 +16,24 @@ class IdleWorker { } /** - * Pool of idle short-lived workers, so that they can be reused in a short period of time (`ttl`), instead of having to create a new worker from scratch. + * Pool of idle short-lived workers. + * + * IMPORTANT: for simplicity it does not limit the number of newly created workers, leaving it up to the caller to manage the pool size. */ export class WorkerPool { private idleWorkers: Set = new Set(); - private activeWorkers: Set = new Set(); - private readonly workerUrl: URL; private readonly workerTTL: number; - private readonly maxPoolSize: number; private constructor( workerUrl: URL, options: { ttl?: number; - maxPoolSize?: number; }, ) { this.workerUrl = workerUrl; // by default, active & idle workers will be terminated after 1s of inactivity this.workerTTL = options.ttl || 1000; - // by default, active workers are limited to 3 instances - this.maxPoolSize = options.maxPoolSize || 3; } /** @@ -52,7 +48,6 @@ export class WorkerPool { workerUrl: URL | undefined, options: { ttl?: number; - maxPoolSize?: number; } = {}, ): WorkerPool { if (!workerUrl) { @@ -77,18 +72,13 @@ export class WorkerPool { let worker: IdleWorker; const idleWorker = Array.from(this.idleWorkers).shift(); - if (idleWorker) { this.idleWorkers.delete(idleWorker); worker = idleWorker; - } else if (this.activeWorkers.size < this.maxPoolSize) { - worker = await this.createWorker(); } else { - worker = await this.waitForActiveWorker(); + worker = await this.createWorker(); } - this.activeWorkers.add(worker); - return new Promise((resolve, reject) => { worker.instance.onmessage = this.onMessageHandler(worker, resolve); worker.instance.onerror = this.onErrorHandler(worker, reject); @@ -111,13 +101,7 @@ export class WorkerPool { worker.instance.terminate(); } - for (const worker of this.activeWorkers) { - worker.debounceTerminate.cancel(); - worker.instance.terminate(); - } - this.idleWorkers.clear(); - this.activeWorkers.clear(); } /** @@ -146,25 +130,9 @@ export class WorkerPool { return worker; } - private waitForActiveWorker(): Promise { - return Promise.race( - Array.from(this.activeWorkers).map( - (worker) => - new Promise((resolve) => { - const originalOnMessage = worker.instance.onmessage; - worker.instance.onmessage = (e) => { - worker.instance.onmessage = originalOnMessage; - resolve(worker); - }; - }), - ), - ); - } - private onMessageHandler(worker: IdleWorker, resolve: (value: R) => void) { return (e: { data: R }) => { worker.debounceTerminate(); - this.activeWorkers.delete(worker); this.idleWorkers.add(worker); resolve(e.data); }; @@ -175,8 +143,6 @@ export class WorkerPool { reject: (reason: ErrorEvent) => void, ) { return (e: ErrorEvent) => { - this.activeWorkers.delete(worker); - // terminate the worker immediately before rejection worker.debounceTerminate(() => reject(e)); worker.debounceTerminate.flush(); diff --git a/scripts/buildPackage.js b/scripts/buildPackage.js index 193fe7c0e..baf20615f 100644 --- a/scripts/buildPackage.js +++ b/scripts/buildPackage.js @@ -16,15 +16,17 @@ const ENV_VARS = { }, }; -const rawConfigCommon = { +// excludes all external dependencies and bundles only the source code +const getConfig = (outdir) => ({ + outdir, bundle: true, + splitting: true, format: "esm", + packages: "external", plugins: [sassPlugin()], + target: "es2020", assetNames: "[dir]/[name]", chunkNames: "[dir]/[name]-[hash]", - // chunks are always external, so they are not bundled within and get build separately - external: ["*.chunk"], - packages: "external", alias: { "@excalidraw/common": path.resolve(__dirname, "../packages/common/src"), "@excalidraw/element": path.resolve(__dirname, "../packages/element/src"), @@ -35,57 +37,47 @@ const rawConfigCommon = { loader: { ".woff2": "file", }, -}; +}); -const rawConfigIndex = { - ...rawConfigCommon, - entryPoints: ["index.tsx"], -}; - -const rawConfigChunks = { - ...rawConfigCommon, - // create a separate chunk for each - entryPoints: ["**/*.chunk.ts"], - entryNames: "[name]", -}; - -function buildDev(chunkConfig) { - const config = { - ...chunkConfig, +function buildDev(config) { + return build({ + ...config, sourcemap: true, define: { "import.meta.env": JSON.stringify(ENV_VARS.development), }, - outdir: "dist/dev", - }; - - return build(config); + }); } -function buildProd(chunkConfig) { - const config = { - ...chunkConfig, +function buildProd(config) { + return build({ + ...config, minify: true, define: { "import.meta.env": JSON.stringify(ENV_VARS.production), }, - outdir: "dist/prod", - }; - - return build(config); + }); } const createESMRawBuild = async () => { + const chunksConfig = { + entryPoints: ["index.tsx", "**/*.chunk.ts"], + entryNames: "[name]", + }; + // development unminified build with source maps - await buildDev(rawConfigIndex); - await buildDev(rawConfigChunks); + await buildDev({ + ...getConfig("dist/dev"), + ...chunksConfig, + }); // production minified buld without sourcemaps - await buildProd(rawConfigIndex); - await buildProd(rawConfigChunks); + await buildProd({ + ...getConfig("dist/prod"), + ...chunksConfig, + }); }; -// otherwise throws "ERROR: Could not resolve "./subset-worker.chunk" (async () => { await createESMRawBuild(); })();