Skip to content

API Reference

defineWorkerHandler()

Worker-side. Wires the run/cancel message protocol automatically — you only write the handler function.

ts
import { defineWorkerHandler, type WorkerContext } from 'vue-worker-kit/worker'

export default defineWorkerHandler(async (input: In, ctx: WorkerContext): Promise<Out> => {
  // ...
})

ctx: WorkerContext:

FieldTypeDescription
signalAbortSignalAborted when the task is cancelled from the main thread — checking it is optional, cancellation is cooperative
reportProgress(value)(0..1) => voidSends progress to the main thread, throttled to ~20 messages/sec
transfer(...transferables)(...Transferable[]) => voidMarks objects to send back zero-copy with the result instead of structured-clone copying — see Transferables
reportChunk(chunk)(chunk: unknown) => voidSends an intermediate result, unthrottled — see Streaming / Chunked Results

defineWorkerHandler only starts a message loop when it actually runs inside a dedicated- or shared-worker global scope (checked via self instanceof DedicatedWorkerGlobalScope/SharedWorkerGlobalScope). Importing the file anywhere else — e.g. accidentally from the main bundle — is a no-op. The same file works for both new Worker(...) (via useWorker()/createWorkerPool()) and new SharedWorker(...) (via useSharedWorker()) — see useSharedWorker().

You don't need to call reportProgress(1) yourself right before returning — a final, unthrottled progress update of 1 is always sent right before the result, regardless of what your last throttled call was. Without this, a handler that only reports at periodic checkpoints (e.g. every 5%) could leave the main thread's progress stuck below 1 forever, since the checkpoint closest to the end can land inside the previous call's throttle window and get silently dropped.

useWorker()

Main-thread composable, wraps a single lazily-created worker.

ts
const { run, isRunning, progress, error, cancel, warmup } = useWorker<typeof import('./x.worker')>(
  () => new Worker(new URL('./x.worker.ts', import.meta.url), { type: 'module' }),
  { idleTimeout: 30_000, retries: 0 },
)

// Optional: pre-create the worker without running a task (avoids cold-start latency on first run)
await warmup()

const output = await run(input, { transfer: [input.buffer], signal: controller.signal })

Options:

OptionTypeDefaultDescription
idleTimeoutnumber | false30000Worker self-terminates after this many ms idle (frees memory); the next run() transparently recreates it
retriesnumber0Automatic retries on rejection — never applied to cancellations (AbortError always rejects immediately)
retryDelay(attempt: number) => numberDelay before each retry — see Retry Strategy with Backoff
hardCancelOnAbortbooleanfalseOn abort(), terminate and recreate the worker immediately instead of waiting for cooperative ctx.signal handling
cache{ cache: 'lru', maxCacheSize?: number }Memoizes results by input — see Memoization / Result Cache
streamingbooleanfalseEnables ctx.reportChunk()/chunks — see Streaming / Chunked Results

Returns:

  • run(input, options?) => Promise<Output>options: { transfer?: Transferable[], signal?: AbortSignal }
  • isRunning: ComputedRef<boolean>, progress: ShallowRef<number>, error: ShallowRef<WorkerError | null>
  • cancel() — aborts the current run() call(s) that didn't receive their own signal
  • warmup(): Promise<void> — pre-creates the worker without executing a task (useful for avoiding cold-start latency)
  • chunks?: ShallowRef<unknown[]> — present only when streaming: true (see Streaming / Chunked Results)
  • automatic terminate() on onScopeDispose when called inside an active effect scope

run()'s input is passed through toRaw() before being posted — a ref/reactive value read straight off a component (() => list.value) is not structured-cloneable as a live Proxy, so the raw snapshot is what actually gets sent.

createWorkerPool() / useWorkerPool()

For many small, independent tasks (resizing hundreds of images, etc.) — vue-worker-kit/pool.

ts
import { createWorkerPool } from 'vue-worker-kit/pool'

const pool = createWorkerPool<typeof import('./resize.worker')>(
  () => new Worker(new URL('./resize.worker.ts', import.meta.url), { type: 'module' }),
)

// Pre-create all workers up to size (optional, avoids cold-start latency on first tasks)
await pool.warmup()

// Process array with per-item transfer and global cancellation signal
const thumbnails = await pool.map(files, {
  concurrency: 4,
  transfer: (file) => [file.buffer], // zero-copy per item
  signal: abortController.signal, // cancel all running tasks
})

const one = await pool.run(files[0])
  • pool.run(input, options?) — queues the task on the first free worker, options: { transfer?: Transferable[], signal?: AbortSignal }
  • pool.map(items, options?) — processes array with bounded parallelism, results in input order. Options:
    • concurrency?: number — max parallel tasks (default: pool.size)
    • transfer?: (item: T) => Transferable[] — per-item zero-copy transfer function
    • signal?: AbortSignal — global cancellation signal for all items
  • pool.stats: ComputedRef<{ busy: number; idle: number; queued: number }> — reactive, used by the devtools panel
  • pool.terminate() — kills the whole pool
  • pool.warmup(): Promise<void> — pre-creates all workers up to size without executing tasks
  • workers are created lazily, up to size, as tasks arrive — not all at once
  • size (option) defaults to navigator.hardwareConcurrency — the browser's own count of logical cores/threads on the machine actually running your app, not a number picked at development time. Pass size explicitly to override it (e.g. to cap it, or if navigator reports something you don't want to trust — some privacy-hardened browsers cap or round it). Falls back to 4 where navigator doesn't exist (SSR).
  • useWorkerPool() is the same API with onScopeDispose auto-termination for use directly in setup()

useWorkerComputed()

vue-worker-kit/computed. A computed() that recalculates inside a worker whenever its reactive source changes, with stale runs discarded automatically.

ts
import { useWorkerComputed } from 'vue-worker-kit/computed'

const sorted = useWorkerComputed<typeof import('./heavy-sort.worker')>(
  () => new Worker(new URL('./heavy-sort.worker.ts', import.meta.url), { type: 'module' }),
  () => list.value, // tracked like a watchEffect source
  { debounce: 150 },
)

// sorted.value — undefined until the first result, then the latest CURRENT result
// sorted.isRunning, sorted.error

Race handling: every run gets an internal generation number. If the source changes again before a run's result arrives, that result is simply dropped on arrival (never rolls sorted.value back to a stale value), and the superseded run's ctx.signal is aborted (cooperative — the handler decides whether to check it). debounce (ms) prevents firing the worker on every reactive tick (e.g. on each keystroke).

useSharedWorker()

vue-worker-kit/shared — reuses a single SharedWorker across every tab/window of the same origin that connects to it, instead of one worker per tab.

ts
import { useSharedWorker } from 'vue-worker-kit/shared'

const { run, connect, disconnect, portCount } = useSharedWorker<typeof import('./shared.worker')>(
  () => new SharedWorker(new URL('./shared.worker.ts', import.meta.url), { type: 'module' }),
)

// Optional — run() connects lazily on its own; call this to connect ahead of time.
connect()

const result = await run(data)

// Closes this tab's port. Does NOT terminate the worker — other tabs stay connected to it.
disconnect()
  • connect(): void — establishes this tab's connection (idempotent; run() also calls it lazily if you skip this)
  • disconnect(): void — closes this tab's port only; the shared worker keeps running for every other connected tab. Called automatically on onScopeDispose when used inside setup().
  • portCount: Ref<number> — number of tabs the worker has seen connect, as last broadcast by the worker itself. Best-effort: a MessagePort has no platform-level "the other end went away" notification, so this only decrements on a cooperative disconnect() call — a crashed or force-closed tab is never subtracted.
  • run(input, options?), isRunning, progress, error, cancel() — same semantics as useWorker()
  • Options: retries, retryDelay, cache, streaming — same as useWorker(). There is no idleTimeout/hardCancelOnAbort: a shared worker's lifetime isn't owned by any single tab, so connect()/disconnect() is the whole lifecycle story, not an idle timer.
  • Browser support: Chrome, Firefox, Edge Desktop. ❌ Not supported in Safari iOS or Chrome Android — no SharedWorker constructor exists there at all. connect()/run() throw WorkerUnavailableError in that case, the same way useWorker() does under SSR.

The worker-side file is a normal defineWorkerHandler() module — the exact same file works with both new Worker(...) (via useWorker()) and new SharedWorker(...) (via useSharedWorker()); it doesn't need to know which one it's running under.

ts
// shared.worker.ts
import { defineWorkerHandler } from 'vue-worker-kit/worker'

export default defineWorkerHandler(async (data: In, ctx) => {
  return processData(data)
})

Devtools

vue-worker-kit/devtools — a standalone debug panel, no @vue/devtools-api dependency (keeps the package dependency-free).

ts
import { createWorkerActivityMonitor, WorkerActivityPanel } from 'vue-worker-kit/devtools'

const monitor = createWorkerActivityMonitor(pool) // or a single useWorker()/useSharedWorker() instance
vue
<WorkerActivityPanel :monitor="monitor" />

Shows busy/idle worker counts, queue length, average task time, and the last N errors — reactive, driven by an internal subscription (no polling).