API Reference
defineWorkerHandler()
Worker-side. Wires the run/cancel message protocol automatically — you only write the handler function.
import { defineWorkerHandler, type WorkerContext } from 'vue-worker-kit/worker'
export default defineWorkerHandler(async (input: In, ctx: WorkerContext): Promise<Out> => {
// ...
})ctx: WorkerContext:
| Field | Type | Description |
|---|---|---|
signal | AbortSignal | Aborted when the task is cancelled from the main thread — checking it is optional, cancellation is cooperative |
reportProgress(value) | (0..1) => void | Sends progress to the main thread, throttled to ~20 messages/sec |
transfer(...transferables) | (...Transferable[]) => void | Marks objects to send back zero-copy with the result instead of structured-clone copying — see Transferables |
reportChunk(chunk) | (chunk: unknown) => void | Sends 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.
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:
| Option | Type | Default | Description |
|---|---|---|---|
idleTimeout | number | false | 30000 | Worker self-terminates after this many ms idle (frees memory); the next run() transparently recreates it |
retries | number | 0 | Automatic retries on rejection — never applied to cancellations (AbortError always rejects immediately) |
retryDelay | (attempt: number) => number | — | Delay before each retry — see Retry Strategy with Backoff |
hardCancelOnAbort | boolean | false | On 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 |
streaming | boolean | false | Enables 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 currentrun()call(s) that didn't receive their ownsignalwarmup(): Promise<void>— pre-creates the worker without executing a task (useful for avoiding cold-start latency)chunks?: ShallowRef<unknown[]>— present only whenstreaming: true(see Streaming / Chunked Results)- automatic
terminate()ononScopeDisposewhen 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.
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 functionsignal?: AbortSignal— global cancellation signal for all items
pool.stats: ComputedRef<{ busy: number; idle: number; queued: number }>— reactive, used by the devtools panelpool.terminate()— kills the whole poolpool.warmup(): Promise<void>— pre-creates all workers up tosizewithout executing tasks- workers are created lazily, up to
size, as tasks arrive — not all at once size(option) defaults tonavigator.hardwareConcurrency— the browser's own count of logical cores/threads on the machine actually running your app, not a number picked at development time. Passsizeexplicitly to override it (e.g. to cap it, or ifnavigatorreports something you don't want to trust — some privacy-hardened browsers cap or round it). Falls back to4wherenavigatordoesn't exist (SSR).useWorkerPool()is the same API withonScopeDisposeauto-termination for use directly insetup()
useWorkerComputed()
vue-worker-kit/computed. A computed() that recalculates inside a worker whenever its reactive source changes, with stale runs discarded automatically.
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.errorRace 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.
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 ononScopeDisposewhen used insidesetup().portCount: Ref<number>— number of tabs the worker has seen connect, as last broadcast by the worker itself. Best-effort: aMessagePorthas no platform-level "the other end went away" notification, so this only decrements on a cooperativedisconnect()call — a crashed or force-closed tab is never subtracted.run(input, options?),isRunning,progress,error,cancel()— same semantics asuseWorker()- Options:
retries,retryDelay,cache,streaming— same asuseWorker(). There is noidleTimeout/hardCancelOnAbort: a shared worker's lifetime isn't owned by any single tab, soconnect()/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
SharedWorkerconstructor exists there at all.connect()/run()throwWorkerUnavailableErrorin that case, the same wayuseWorker()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.
// 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).
import { createWorkerActivityMonitor, WorkerActivityPanel } from 'vue-worker-kit/devtools'
const monitor = createWorkerActivityMonitor(pool) // or a single useWorker()/useSharedWorker() instance<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).