Launch OpenGPEX

OpenGPEX 1.x documentation (legacy editor).

v2 Docs

Plugin Workers

How to offload heavy computation (AI inference, encoding, complex algorithms) to dedicated Web Workers within your plugin.


Design Principle

The core engine has a shared Worker for pixel operations — its job router dispatches compositing (COMPOSITE via CompositorHandler / Canvas2dBackend), FILTER, DECODE, RESAMPLE, RASTERIZE, high-depth FILE_IO, EXTRACT_PIXELS, histogram, and tile-serving jobs (see core/engine/worker/router.ts). Plugins must never invade the core Worker. Instead, create your own Worker within your plugin directory.

Benefits:

  • On-demand loading — Users who don't use your feature pay no startup cost
  • Dependency isolation — WASM modules, AI model libraries stay in your Worker chunk
  • Fault isolation — Your Worker crash doesn't affect core compositing, filtering, and decoding
  • Bundler-friendly — new URL('./worker.ts', import.meta.url) triggers automatic code-splitting

Directory Structure

MyPlugin/
├── index.tsx
├── protocols.ts
├── commands.ts
├── hooks.ts
├── components.tsx
└── worker/                     # Plugin-private Worker directory
    ├── protocol.ts             # Message types (Request/Response/Progress)
    ├── client.ts               # Main-thread client (lazy init, request dispatch)
    └── my-task.worker.ts       # Worker implementation

Two Lifecycle Modes

Mode A: Fire-and-Forget (Lightweight)

For simple, infrequent, stateless tasks:

// In commands.ts
execute: async (ctx) => {
  ctx.scoped!.setBusy(true);
  try {
    const worker = new Worker(
      new URL('./worker/encode.worker.ts', import.meta.url),
      { type: 'module' }
    );
    const result = await new Promise((resolve, reject) => {
      worker.onmessage = (e) => { resolve(e.data); worker.terminate(); };
      worker.onerror = (e) => { reject(e); worker.terminate(); };
      worker.postMessage(payload, [transferable]);
    });
    // Use result...
  } finally {
    ctx.scoped!.setBusy(false);
  }
}

Mode B: Persistent Singleton (High-Frequency)

For tasks with expensive initialization (AI model loading, WASM warm-up):

// worker/protocol.ts
export interface WorkerRequest {
  type: 'run';
  reqId: number;
  payload: ImageData;
}
export interface ProgressMessage {
  type: 'progress';
  reqId: number;
  stage: 'downloading' | 'processing' | 'postprocessing';
  progress: number;  // 0.0 ~ 1.0
}
export interface ResultMessage {
  type: 'result';
  reqId: number;
  data: Uint8Array;
}
export interface ErrorMessage {
  type: 'error';
  reqId: number;
  error: string;
}
export type WorkerMessage = ProgressMessage | ResultMessage | ErrorMessage;
// worker/client.ts
let worker: Worker | null = null;
let reqCounter = 0;

function ensure(): Worker {
  if (!worker) {
    worker = new Worker(
      new URL('./my-task.worker.ts', import.meta.url),
      { type: 'module' }
    );
  }
  return worker;
}

export function run(payload: ImageData): Promise<Uint8Array> {
  const w = ensure();
  const reqId = ++reqCounter;
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      reject(new Error('Worker timeout'));
      dispose();
    }, 30000);

    w.onmessage = (e: MessageEvent<WorkerMessage>) => {
      if (e.data.reqId !== reqId) return; // stale response guard
      if (e.data.type === 'result') {
        clearTimeout(timeout);
        resolve(e.data.data);
      } else if (e.data.type === 'error') {
        clearTimeout(timeout);
        reject(new Error(e.data.error));
      }
      // 'progress' messages can drive UI updates
    };
    w.postMessage({ type: 'run', reqId, payload }, [payload.data.buffer]);
  });
}

export function dispose() {
  worker?.terminate();
  worker = null;
}

📌 In-tree example: the AI tools (BgRemover, Upscale, Segmentation, Inpaint) share a generic createWorkerClient factory (plugins/base/drawers/AIToolsDrawer/_shared/control/createWorkerClient.ts). It wraps exactly this pattern — lazy singleton Worker, monotonic reqId stale-response guard, AbortSignal cancellation, configurable per-request timeout, zero-copy Transferable extraction, and optional auto-dispose on WebGPU/ORT runtime errors. If your plugin is a direct sibling of the AI tools, reuse it; otherwise copy the pattern into your own plugin directory.


Mandatory Rules

# Rule Reason
1 Use Transferable for large buffers postMessage(msg, [buffer]) — zero-copy transfer. Never structured-clone megabyte ArrayBuffers
2 Timeout protection Tasks > 3s MUST have a timeout. On timeout: terminate() + show error to user
3 No main-thread blocking Never use Atomics.wait or synchronous waits for Worker responses
4 Error bubbling Worker errors → postMessage error back → main thread shows toast/HUD
5 No DOM in Workers Workers cannot import React, document, window — will crash on startup
6 setBusy(true/false) Always wrap async Worker calls in setBusy with a finally block

Progress Reporting

For long-running tasks (AI inference, model downloads), report progress:

// Inside the worker
self.postMessage({
  type: 'progress',
  reqId,
  stage: 'downloading',
  progress: 0.45,
  detail: 'Loading model weights...',
});

The main-thread client can forward progress to UI via signals or direct ref updates.


Next Steps


Last updated: 2026-07-30