aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/workers.ts
diff options
context:
space:
mode:
authorkj_sh6042026-03-15 16:19:35 -0400
committerkj_sh6042026-03-15 16:19:35 -0400
commit6ec259a0e71174651bae95d4628138bf6fd68742 (patch)
tree5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/workers.ts
parent16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff)
refactor: packages/
Diffstat (limited to 'packages/excalidraw/workers.ts')
-rw-r--r--packages/excalidraw/workers.ts153
1 files changed, 153 insertions, 0 deletions
diff --git a/packages/excalidraw/workers.ts b/packages/excalidraw/workers.ts
new file mode 100644
index 0000000..f5964d0
--- /dev/null
+++ b/packages/excalidraw/workers.ts
@@ -0,0 +1,153 @@
+import { WorkerInTheMainChunkError, WorkerUrlNotDefinedError } from "./errors";
+import { debounce } from "./utils";
+
+class IdleWorker {
+ public instance: Worker;
+
+ constructor(workerUrl: URL) {
+ this.instance = new Worker(workerUrl, { type: "module" });
+ }
+
+ /**
+ * Use to prolong the worker's life by `workerTTL` or terminate it with a flush immediately.
+ */
+ public debounceTerminate!: ReturnType<typeof debounce>;
+}
+
+/**
+ * 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<T, R> {
+ private idleWorkers: Set<IdleWorker> = new Set();
+ private readonly workerUrl: URL;
+ private readonly workerTTL: number;
+
+ private constructor(
+ workerUrl: URL,
+ options: {
+ ttl?: number;
+ },
+ ) {
+ this.workerUrl = workerUrl;
+ // by default, active & idle workers will be terminated after 1s of inactivity
+ this.workerTTL = options.ttl || 1000;
+ }
+
+ /**
+ * Create a new worker pool.
+ *
+ * @param workerUrl - The URL of the worker file.
+ * @param options - The options for the worker pool.
+ * @throws If the worker is bundled into the main chunk.
+ * @returns A new worker pool instance.
+ */
+ public static create<T, R>(
+ workerUrl: URL | undefined,
+ options: {
+ ttl?: number;
+ } = {},
+ ): WorkerPool<T, R> {
+ if (!workerUrl) {
+ throw new WorkerUrlNotDefinedError();
+ }
+
+ if (!import.meta.url || workerUrl.toString() === import.meta.url) {
+ // in case the worker code is bundled into the main chunk
+ throw new WorkerInTheMainChunkError();
+ }
+
+ return new WorkerPool(workerUrl, options);
+ }
+
+ /**
+ * Take idle worker from the pool or create a new one and post a message to it.
+ */
+ public async postMessage(
+ data: T,
+ options: StructuredSerializeOptions,
+ ): Promise<R> {
+ let worker: IdleWorker;
+
+ const idleWorker = Array.from(this.idleWorkers).shift();
+ if (idleWorker) {
+ this.idleWorkers.delete(idleWorker);
+ worker = idleWorker;
+ } else {
+ worker = await this.createWorker();
+ }
+
+ return new Promise((resolve, reject) => {
+ worker.instance.onmessage = this.onMessageHandler(worker, resolve);
+ worker.instance.onerror = this.onErrorHandler(worker, reject);
+
+ worker.instance.postMessage(data, options);
+ worker.debounceTerminate(() =>
+ reject(
+ new Error(`Active worker did not respond for ${this.workerTTL}ms!`),
+ ),
+ );
+ });
+ }
+
+ /**
+ * Terminate the idle workers in the pool.
+ */
+ public async clear() {
+ for (const worker of this.idleWorkers) {
+ worker.debounceTerminate.cancel();
+ worker.instance.terminate();
+ }
+
+ this.idleWorkers.clear();
+ }
+
+ /**
+ * Used to get a worker from the pool or create a new one if there is no idle available.
+ */
+ private async createWorker(): Promise<IdleWorker> {
+ const worker = new IdleWorker(this.workerUrl);
+
+ worker.debounceTerminate = debounce((reject?: () => void) => {
+ worker.instance.terminate();
+
+ if (this.idleWorkers.has(worker)) {
+ this.idleWorkers.delete(worker);
+
+ // eslint-disable-next-line no-console
+ console.debug(
+ "Job finished! Idle worker has been released from the pool.",
+ );
+ } else if (reject) {
+ reject();
+ } else {
+ console.error("Worker has been terminated!");
+ }
+ }, this.workerTTL);
+
+ return worker;
+ }
+
+ private onMessageHandler(worker: IdleWorker, resolve: (value: R) => void) {
+ return (e: { data: R }) => {
+ worker.debounceTerminate();
+ this.idleWorkers.add(worker);
+ resolve(e.data);
+ };
+ }
+
+ private onErrorHandler(
+ worker: IdleWorker,
+ reject: (reason: ErrorEvent) => void,
+ ) {
+ return (e: ErrorEvent) => {
+ // terminate the worker immediately before rejection
+ worker.debounceTerminate(() => reject(e));
+ worker.debounceTerminate.flush();
+
+ // clear the worker pool in case there are some idle workers left
+ this.clear();
+ };
+ }
+}