Launch OpenGPEX

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 (merge, transform, transcode). 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 merge/transform
  • Bundler-friendlynew 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;
}

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