Skip to content

Advanced Features

Transferables

Into the worker, via RunOptions.transfer:

ts
const buffer = new ArrayBuffer(1024 * 1024)
const result = await run(buffer, { transfer: [buffer] })
// buffer.byteLength === 0 immediately — it was detached, not copied

Back out of the worker, via ctx.transfer(...) — the mirror of the above, for a handler that wants to hand back a large buffer (e.g. a resized image, an OffscreenCanvas-rendered frame) without copying it:

ts
// resize.worker.ts
export default defineWorkerHandler((input: ResizeInput, ctx) => {
  const output = resize(input) // produces a fresh ArrayBuffer
  ctx.transfer(output) // sent back zero-copy instead of structured-clone copied
  return output
})

ctx.transfer() doesn't require the transferred object to be part of the returned value — call it with whatever transferables should ride along with the result. Safe to call more than once; every object passed across all calls is included.

Streaming / Chunked Results

For large datasets where you want intermediate results without waiting for full completion:

ts
import { useWorker } from 'vue-worker-kit'

const { run, chunks, isRunning } = useWorker<typeof import('./process.worker')>(
  () => new Worker(new URL('./process.worker.ts', import.meta.url), { type: 'module' }),
  { streaming: true }, // required — without it `chunks` is `undefined`, not a ref
)

// Process large dataset with streaming results
const finalResult = await run(largeDataset)

// chunks.value contains all intermediate results as they arrive
watch(chunks, (newChunks) => {
  console.log('Received chunk:', newChunks[newChunks.length - 1])
})

Worker-side:

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

export default defineWorkerHandler(async (items: LargeDataset[], ctx) => {
  const results: Result[] = []

  for (let i = 0; i < items.length; i += 100) {
    const batch = items.slice(i, i + 100)
    const processed = await processBatch(batch)

    // Send intermediate result immediately
    ctx.reportChunk(processed)

    results.push(...processed)

    if (ctx.signal.aborted) throw ctx.signal.reason
  }

  return results // Final result
})
  • chunks: ShallowRef<unknown[]> — reactive array of all reported chunks
  • ctx.reportChunk(data) — sends partial result to main thread (unthrottled)
  • Chunks accumulate in order; final result is separate from chunks
  • Useful for progressive rendering, real-time updates, or memory-efficient processing

Cancellation

ts
const controller = new AbortController()
const promise = run(input, { signal: controller.signal })
controller.abort() // promise rejects with AbortError, immediately — regardless of what the worker does

If you don't pass your own signal, run() creates one internally; cancel() aborts it. retries never applies to an aborted run.

Retry Strategy with Backoff

For transient failures, configure automatic retries with exponential backoff:

ts
import { useWorker } from 'vue-worker-kit'

const { run, error } = useWorker<typeof import('./api.worker')>(
  () => new Worker(new URL('./api.worker.ts', import.meta.url), { type: 'module' }),
  {
    retries: 3,
    retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 10000), // 1s, 2s, 4s, capped at 10s
  },
)

// On failure, automatically retries with increasing delay
const result = await run(data)
  • retries: number — max retry attempts (default: 0, no retries)
  • retryDelay: number | ((attempt: number) => number) — delay between retries
    • If number: constant delay in ms
    • If function: dynamic delay based on attempt number (1-indexed)
  • Retries only apply to non-abort errors (AbortError rejects immediately)
  • Common pattern: exponential backoff with jitter for API calls or flaky operations
ts
// Advanced: exponential backoff with random jitter
{
  retries: 5,
  retryDelay: (attempt) => {
    const baseDelay = 1000 * 2 ** attempt
    const jitter = Math.random() * 1000
    return Math.min(baseDelay + jitter, 30000)
  },
}

Memoization / Result Cache

For pure worker functions (same input → same output), enable LRU caching:

ts
import { useWorker } from 'vue-worker-kit'

const { run } = useWorker<typeof import('./hash.worker')>(
  () => new Worker(new URL('./hash.worker.ts', import.meta.url), { type: 'module' }),
  {
    cache: { cache: 'lru', maxCacheSize: 100 }, // keep the last 100 results
  },
)

// First call — executes in worker
const hash1 = await run(data)

// Second call with the same input (compared via JSON.stringify) — returns the cached
// result instantly, no worker invocation, no postMessage round-trip
const hash2 = await run(data) // hash1 === hash2

Options (cache: UseWorkerCacheOptions):

  • cache: 'lru' — enable the LRU cache (unset/omitted disables it)
  • maxCacheSize: number — max entries before evicting the oldest (default: 50)

The cache key is JSON.stringify(input) (exported as createCacheKey() if you want to reason about collisions yourself) — inputs that stringify the same (including object key order) share a cache entry.

useWorkerComputed() doesn't have a cache option — its own generation-number mechanism already discards stale/superseded results, and its source() typically produces a fresh input on every reactive tick anyway, so key-based memoization wouldn't have much to hit.

Error handling

  • A thrown error inside the handler is serialized as { name, message, stack } and reconstructed on the main thread as a WorkerError. .workerStack is the original in-worker stack; .cause is a synthetic error created at the run() call site (before crossing into the worker) — so both ends of the failure show up together in the console/Sentry.
  • A protocol-level failure (e.g. an object that doesn't structured-clone) becomes a WorkerError with name: 'DataCloneError', not an unhandled exception.
  • WorkerUnavailableError is thrown instead of a raw ReferenceError: Worker is not defined when run() is called somewhere with no global Worker (typically SSR) — it is never wrapped or retried.

Worker lifecycle

  • Idle timeout — a worker idle longer than idleTimeout is terminated; the next run() transparently spins up a new one (small latency on the first call after idling — expected).
  • Scope-based auto-terminationuseWorker/useWorkerPool called inside setup() terminate their worker(s) on onScopeDispose, avoiding the classic SPA-navigation leak.
  • Pool workers are lazy — created as tasks arrive, up to size, not all at createWorkerPool() time.

Warmup

To avoid cold-start latency on the first task, you can pre-create workers without executing any work:

ts
// Single worker
const { warmup, run } = useWorker<typeof import('./x.worker')>(
  () => new Worker(new URL('./x.worker.ts', import.meta.url), { type: 'module' }),
)
await warmup() // Worker is now instantiated and ready
const result = await run(data) // No worker creation delay

// Pool - pre-create all workers up to size
const pool = createWorkerPool<typeof import('./resize.worker')>(
  () => new Worker(new URL('./resize.worker.ts', import.meta.url), { type: 'module' }),
  { size: 4 },
)
await pool.warmup() // All 4 workers are now instantiated
const results = await pool.map(items) // Immediate execution, no cold starts

Warmup is useful when you know a worker-intensive operation is about to happen (e.g., user clicks "Process" button) and you want to eliminate the ~50-200ms worker creation latency. Call it during idle time (e.g., onMounted, or after initial page load) to keep interactions snappy.