From 0c29a726988903e221100ba361b2a857264655f1 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Tue, 1 Apr 2025 11:49:07 +1100 Subject: [PATCH] use workerpool and polyfill --- packages/excalidraw/lasso/index.ts | 107 ++++++++++++++---- .../excalidraw/lasso/lasso-worker.chunk.ts | 78 +++++++++++++ packages/excalidraw/lasso/lasso.test.tsx | 2 +- .../excalidraw/lasso/{worker.ts => utils.ts} | 71 ------------ 4 files changed, 162 insertions(+), 96 deletions(-) create mode 100644 packages/excalidraw/lasso/lasso-worker.chunk.ts rename packages/excalidraw/lasso/{worker.ts => utils.ts} (61%) diff --git a/packages/excalidraw/lasso/index.ts b/packages/excalidraw/lasso/index.ts index 783b4dca5..a1bc73f01 100644 --- a/packages/excalidraw/lasso/index.ts +++ b/packages/excalidraw/lasso/index.ts @@ -17,7 +17,7 @@ import { selectGroupsForSelectedElements } from "@excalidraw/element/groups"; import { getContainerElement } from "@excalidraw/element/textElement"; -import { arrayToMap, easeOut } from "@excalidraw/common"; +import { arrayToMap, easeOut, promiseTry } from "@excalidraw/common"; import type { ExcalidrawElement, @@ -29,7 +29,9 @@ import { type AnimationFrameHandler } from "../animation-frame-handler"; import { AnimatedTrail } from "../animated-trail"; -import LassoWorker from "./worker?worker&inline"; +import { WorkerPool } from "../workers"; + +import { updateSelection } from "./utils"; import type App from "../components/App"; import type { LassoWorkerInput, LassoWorkerOutput } from "./types"; @@ -37,7 +39,6 @@ import type { LassoWorkerInput, LassoWorkerOutput } from "./types"; export class LassoTrail extends AnimatedTrail { private intersectedElements: Set = new Set(); private enclosedElements: Set = new Set(); - private worker: Worker | null = null; private elementsSegments: Map[]> | null = null; private keepPreviousSelection: boolean = false; @@ -64,7 +65,7 @@ export class LassoTrail extends AnimatedTrail { }); } - startPath(x: number, y: number, keepPreviousSelection = false) { + async startPath(x: number, y: number, keepPreviousSelection = false) { // clear any existing trails just in case this.endPath(); @@ -81,21 +82,6 @@ export class LassoTrail extends AnimatedTrail { selectedLinearElement: null, }); } - - try { - this.worker = new LassoWorker(); - - this.worker.onmessage = (event: MessageEvent) => { - const { selectedElementIds } = event.data; - this.selectElementsFromIds(selectedElementIds); - }; - - this.worker.onerror = (error) => { - console.error("Worker error:", error); - }; - } catch (error) { - console.error("Failed to start worker", error); - } } selectElementsFromIds = (ids: string[]) => { @@ -165,7 +151,11 @@ export class LassoTrail extends AnimatedTrail { }); }; - addPointToPath = (x: number, y: number, keepPreviousSelection = false) => { + addPointToPath = async ( + x: number, + y: number, + keepPreviousSelection = false, + ) => { super.addPointToPath(x, y); this.keepPreviousSelection = keepPreviousSelection; @@ -179,10 +169,10 @@ export class LassoTrail extends AnimatedTrail { }, }); - this.updateSelection(); + await this.updateSelection(); }; - private updateSelection = () => { + private updateSelection = async () => { const lassoPath = super .getCurrentTrail() ?.originalPoints?.map((p) => pointFrom(p[0], p[1])); @@ -206,7 +196,15 @@ export class LassoTrail extends AnimatedTrail { simplifyDistance: 5 / this.app.state.zoom.value, }; - this.worker?.postMessage(message); + const workerPool = await getOrCreateWorkerPool(); + const result = await workerPool.postMessage(message, {}); + + const { selectedElementIds } = result; + if (!selectedElementIds || !Array.isArray(selectedElementIds)) { + return; + } + + this.selectElementsFromIds(selectedElementIds); } }; @@ -219,6 +217,67 @@ export class LassoTrail extends AnimatedTrail { this.app.setState({ lassoSelection: null, }); - this.worker?.terminate(); + + getOrCreateWorkerPool() + .then((workerPool) => { + workerPool.clear(); + }) + .catch((error) => { + console.error("Failed to clear worker pool", error); + }); + } +} + +let lassoWorker: Promise | null = null; + +const lazyLoadLassoWorkerChunk = async () => { + if (!lassoWorker) { + lassoWorker = import("./lasso-worker.chunk"); + } + + return lassoWorker; +}; + +let workerPool: Promise< + WorkerPool +> | null = null; + +const getOrCreateWorkerPool = async () => { + if (!workerPool) { + workerPool = promiseTry(async () => { + if (typeof Worker !== "undefined") { + const { WorkerUrl } = await lazyLoadLassoWorkerChunk(); + + return WorkerPool.create( + WorkerUrl, + ); + } + return WorkerPolyfillPool.create() as unknown as WorkerPool< + LassoWorkerInput, + LassoWorkerOutput + >; + }); + } + return workerPool; +}; + +class WorkerPolyfillPool { + static create() { + return new WorkerPolyfillPool(); + } + + async postMessage(data: LassoWorkerInput): Promise { + return new Promise((resolve, reject) => { + try { + const selectedElementIds = updateSelection(data); + resolve(selectedElementIds); + } catch (error) { + reject(error); + } + }); + } + + clear() { + // no-op for polyfill } } diff --git a/packages/excalidraw/lasso/lasso-worker.chunk.ts b/packages/excalidraw/lasso/lasso-worker.chunk.ts new file mode 100644 index 000000000..f6f0d0fe9 --- /dev/null +++ b/packages/excalidraw/lasso/lasso-worker.chunk.ts @@ -0,0 +1,78 @@ +import { updateSelection } from "./utils"; + +import type { LassoWorkerInput } from "./types"; + +export const WorkerUrl: URL | undefined = import.meta.url + ? new URL(import.meta.url) + : undefined; + +// only run in the worker context +if (typeof window === "undefined" && typeof self !== "undefined") { + // variables to track processing state and latest input data + // for "backpressure" purposes + let isProcessing: boolean = false; + let latestInputData: LassoWorkerInput | null = null; + + self.onmessage = (event: MessageEvent) => { + if (!event.data) { + self.postMessage({ + error: "No data received", + selectedElementIds: [], + }); + return; + } + + latestInputData = event.data; + + if (!isProcessing) { + processInputData(); + } + }; + + // function to process the latest data + const processInputData = () => { + // If no data to process, return + if (!latestInputData) { + return; + } + + // capture the current data to process and reset latestData + const dataToProcess = latestInputData; + latestInputData = null; // reset to avoid re-processing the same data + isProcessing = true; + + try { + const { lassoPath, elements, intersectedElements, enclosedElements } = + dataToProcess; + + if (!Array.isArray(lassoPath) || !Array.isArray(elements)) { + throw new Error("Invalid input: lassoPath and elements must be arrays"); + } + + if ( + !(intersectedElements instanceof Set) || + !(enclosedElements instanceof Set) + ) { + throw new Error( + "Invalid input: intersectedElements and enclosedElements must be Sets", + ); + } + + const result = updateSelection(dataToProcess); + self.postMessage(result); + } catch (error) { + self.postMessage({ + error: + error instanceof Error ? error.message : "Unknown error occurred", + selectedElementIds: [], + }); + } finally { + isProcessing = false; + // if new data arrived during processing, process it + // as we're done with processing the previous data + if (latestInputData) { + processInputData(); + } + } + }; +} diff --git a/packages/excalidraw/lasso/lasso.test.tsx b/packages/excalidraw/lasso/lasso.test.tsx index 58bbabd2c..bde0e8ff2 100644 --- a/packages/excalidraw/lasso/lasso.test.tsx +++ b/packages/excalidraw/lasso/lasso.test.tsx @@ -30,7 +30,7 @@ import { Excalidraw } from "../index"; import { getSelectedElements } from "../scene"; -import { updateSelection } from "./worker"; +import { updateSelection } from "./utils"; import type { ElementsSegmentsMap } from "./types"; diff --git a/packages/excalidraw/lasso/worker.ts b/packages/excalidraw/lasso/utils.ts similarity index 61% rename from packages/excalidraw/lasso/worker.ts rename to packages/excalidraw/lasso/utils.ts index d2fd0a73f..32aeb9160 100644 --- a/packages/excalidraw/lasso/worker.ts +++ b/packages/excalidraw/lasso/utils.ts @@ -19,75 +19,6 @@ import type { LassoWorkerOutput, } from "./types"; -const ctx = self as unknown as Worker; - -// variables to track processing state and latest input data -// for "backpressure" purposes -let isProcessing: boolean = false; -let latestInputData: LassoWorkerInput | null = null; - -self.onmessage = (event: MessageEvent) => { - if (!event.data) { - self.postMessage({ - error: "No data received", - selectedElementIds: [], - }); - return; - } - - latestInputData = event.data; - - if (!isProcessing) { - processInputData(); - } -}; - -// function to process the latest data -const processInputData = () => { - // If no data to process, return - if (!latestInputData) { - return; - } - - // capture the current data to process and reset latestData - const dataToProcess = latestInputData; - latestInputData = null; // reset to avoid re-processing the same data - isProcessing = true; - - try { - const { lassoPath, elements, intersectedElements, enclosedElements } = - dataToProcess; - - if (!Array.isArray(lassoPath) || !Array.isArray(elements)) { - throw new Error("Invalid input: lassoPath and elements must be arrays"); - } - - if ( - !(intersectedElements instanceof Set) || - !(enclosedElements instanceof Set) - ) { - throw new Error( - "Invalid input: intersectedElements and enclosedElements must be Sets", - ); - } - - const result = updateSelection(dataToProcess); - self.postMessage(result); - } catch (error) { - self.postMessage({ - error: error instanceof Error ? error.message : "Unknown error occurred", - selectedElementIds: [], - }); - } finally { - isProcessing = false; - // if new data arrived during processing, process it - // as we're done with processing the previous data - if (latestInputData) { - processInputData(); - } - } -}; - export const updateSelection = (input: LassoWorkerInput): LassoWorkerOutput => { const { lassoPath, @@ -176,5 +107,3 @@ const intersectionTest = ( ), ); }; - -export default ctx;