npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@enclosurejs/compute

v1.1.0

Published

> [!IMPORTANT] > This is a **universal module** for running computation off the main thread. It wraps the standard Web Worker API into three typed primitives — Channel, Caller, and Pool — with `Disposable` lifecycle, `AbortSignal` cancellation, and `Trans

Readme

@enclosurejs/compute — Off-thread computation via Web Workers

[!IMPORTANT] This is a universal module for running computation off the main thread. It wraps the standard Web Worker API into three typed primitives — Channel, Caller, and Pool — with Disposable lifecycle, AbortSignal cancellation, and Transferable support. Works identically in browser, Electron renderer, and Tauri webview. Zero platform abstraction needed — Web Workers are the same everywhere.

The Problem

Heavy computation on the main thread blocks UI: tag evaluation engines, data transformers, image processors, parsers, and indexers all need to run off-thread. The raw Worker API is low-level — no typing on messages, no structured cleanup, no error isolation, no pool management, no timeout/cancel. Every project reinvents the same new Worker() + postMessage + onmessage + terminate boilerplate with ad-hoc switch routing inside the worker.

@enclosurejs/compute solves this with three primitives:

  • Channel — stateful, long-lived, bidirectional streaming (evaluator, polling, pipeline)
  • Caller — one-shot request/response with timeout and abort (parsing, hashing, reports)
  • Pool — N workers with a shared task queue for parallel batch processing (image resize, indexing)

All three implement Disposable, support Transferable zero-copy, and work on every Enclosure target platform without any platform-specific code.

Architecture

┌─────────────────── UI Thread ───────────────────┐
│                                                   │
│  import { createChannel } from '@enclosurejs/compute'│
│                                                   │
│  channel.send({ type: 'updated', data })          │
│           │                                       │
│           │  postMessage (structured clone)        │
│           │  ──────────── or ──────────────        │
│           │  postMessage (transfer, zero-copy)     │
│           ▼                                       │
│  channel.messages ← ReadableStream<TOut>          │
│                                                   │
└───────────┬───────────────────────────────────────┘
            │
┌───────────┴───────────────────────────────────────┐
│                  Web Worker                        │
│                                                   │
│  import { handleMessages, reply }                  │
│    from '@enclosurejs/compute/worker'                │
│                                                   │
│  handleMessages({                                  │
│      init(data)    { /* ... */ reply(result) },     │
│      updated(data) { /* ... */ reply(result) },     │
│  })                                                │
│                                                   │
└───────────────────────────────────────────────────┘

No platform bridge, no IPC serialization — direct postMessage in the same JS runtime.

Why not a platform capability?

Web Workers are a standard Web API — they work identically in every environment that runs JavaScript (browser, Electron renderer, Tauri webview, Capacitor webview, mobile webview). Unlike ShellService (which needs child_process on Electron, plugin-shell on Tauri), there is nothing platform-specific to abstract. This is the same reasoning that led to rejecting WebSocketService and CryptoService — standard APIs don't need a platform bridge.

Performance characteristics

| Data path | Copies | Transfer | SharedArrayBuffer | Latency 1KB | Latency 10MB | | ------------------------ | ------ | --------- | ----------------- | ----------- | ------------ | | postMessage (clone) | 1 | — | — | ~0.01ms | ~30ms | | postMessage (transfer) | 0 | zero-copy | — | ~0.01ms | ~0.01ms | | SharedArrayBuffer | 0 | — | shared memory | ~0.01ms | ~0.01ms |

All paths are available on all six target platforms (web, Electron win/linux/mac, Tauri win/linux/mac/android/ios).

Quick Start

Channel — bidirectional, long-lived

import { createChannel } from '@enclosurejs/compute';

interface EvalInput {
    type: string;
    data: unknown;
}
interface EvalOutput {
    period: number;
    result: unknown[];
}

const channel = createChannel<EvalInput, EvalOutput>({
    id: 'evaluator',
    src: new URL('./evaluator-worker.js', import.meta.url),
});

channel.send({ type: 'init', data: tasks });
channel.send({ type: 'updated', data: values });

const reader = channel.messages.getReader();
for (;;) {
    const { value, done } = await reader.read();
    if (done) break;
    updateUI(value.result);
}

channel.dispose();

Caller — one-shot request/response

import { createCaller } from '@enclosurejs/compute';

const parser = createCaller<ArrayBuffer, ParsedData>({
    id: 'csv-parser',
    src: new URL('./csv-worker.js', import.meta.url),
    type: 'module',
});

const result = await parser.invoke(csvBuffer, {
    transfer: [csvBuffer],
    timeout: 5000,
    signal: abortController.signal,
});

Pool — parallel batch processing

import { createPool } from '@enclosurejs/compute';

const pool = createPool<ImageData, Uint8Array>(
    { id: 'image-resize', src: '/workers/resize.js' },
    navigator.hardwareConcurrency,
);

const thumbnail = await pool.submit(largeImage);

// Yields results in completion order (not submission order)
for await (const resized of pool.map(images)) {
    appendToGallery(resized);
}

await pool.drain();

pool.dispose();

Worker-side helpers (optional)

// evaluator-worker.ts
import { handleMessages, reply } from '@enclosurejs/compute/worker';

handleMessages({
    init(data) {
        const tasks = data as Task[];
        // ... process ...
        reply({ period: 1.2, result });
    },
    updated(data) {
        // ... evaluate ...
        reply({ period: 0.3, result });
    },
});

The worker does not have to use these helpers — raw self.onmessage / self.postMessage works fine. The helpers add typed routing and transfer-aware reply().

API

Host-side (@enclosurejs/compute)

| Export | Kind | Purpose | | ---------------------- | ------- | ---------------------------------------------------- | | createComputeService | factory | Returns a ComputeService with all three primitives | | createChannel | factory | Stateful bidirectional worker channel | | createCaller | factory | One-shot request/response worker | | createPool | factory | N-worker pool with task queue |

Worker-side (@enclosurejs/compute/worker)

| Export | Kind | Purpose | | ---------------- | -------- | ------------------------------------------------------------- | | handleMessages | function | Typed message router ({ type: handler }self.onmessage) | | reply | function | Transfer-aware self.postMessage wrapper |

WorkerDescriptor

| Field | Type | Required | Description | | ------ | ----------------------- | -------- | --------------------------------------------------- | | id | string | Yes | Identifier for debugging (e.g. "evaluator") | | src | string \| URL | Yes | Worker script path/URL, passed to new Worker(src) | | type | 'module' \| 'classic' | No | Script type. Defaults to 'classic' |

ComputeChannel<TIn, TOut>

Extends Disposable.

| Member | Type | Description | | -------------------------- | ---------------------- | --------------------------------------------------------- | | id | string | Auto-generated identifier ("evaluator:1") | | alive | boolean | true while the worker is running | | send(message, transfer?) | method | Post a message; throws CoreError (TERMINATED) if dead | | messages | ReadableStream<TOut> | Errors on worker onerror; closes on terminate | | onError(handler) | method | Subscribe to worker errors → Disposable | | terminate() | method | Kill the worker, close the stream. Idempotent | | dispose() | method | Alias for terminate() |

ComputeCaller<TIn, TOut>

Extends Disposable.

| Member | Type | Description | | ------------------------- | ------ | ----------------------------------------------------------------------------------- | | invoke(input, options?) | method | Spawns a dedicated worker per call. Rejects with CoreError on error/abort/timeout | | dispose() | method | Rejects any in-flight invoke and terminates its worker |

CallOptions

| Field | Type | Description | | ---------- | ---------------- | ----------------------------------------------------- | | signal | AbortSignal | Cancel the pending call (rejects with code ABORTED) | | timeout | number | Milliseconds before auto-rejection (code TIMEOUT) | | transfer | Transferable[] | Zero-copy buffer transfer |

ComputePool<TIn, TOut>

Extends Disposable.

| Member | Type | Description | | ------------------------- | -------- | ------------------------------------------------------------------------- | | size | number | Current worker count | | pending | number | Tasks waiting in queue (not yet assigned to a worker) | | submit(input, options?) | method | Submit one task, resolve on completion | | map(inputs, options?) | method | Completion-order AsyncGenerator<TOut>. Throws on first task failure | | drain() | method | Wait until all tasks complete | | resize(count) | method | Add/remove workers (minimum 1). Busy workers finish before retiring | | dispose() | method | Terminate all workers, reject queued and in-flight tasks with CoreError |

WorkerMessage<T> (worker-side)

| Field | Type | Description | | ------ | ------------ | ------------------------ | | type | T (string) | Message type for routing | | data | unknown | Payload (optional) |

Configuration

Zero configuration. No config files, no environment variables. Pass a WorkerDescriptor to any factory — that's it.

Pool size defaults to navigator.hardwareConcurrency (typically CPU core count) or 4 as fallback.

Types Exported

Types other packages and application code depend on:

| Type | Used by | | --------------------------- | ---------------------------------------- | | ComputeService | Application code creating workers via DI | | ComputeChannel<TIn, TOut> | Long-lived worker consumers | | ComputeCaller<TIn, TOut> | One-shot worker consumers | | ComputePool<TIn, TOut> | Batch processing consumers | | WorkerDescriptor | Worker configuration | | CallOptions | Caller and pool consumers | | WorkerMessage<T> | Worker scripts using handleMessages | | MessageHandlers<T> | Worker scripts using handleMessages |

Entrypoint separation keeps worker-side types out of host bundles:

| Import path | Contains | Runs in | | --------------------------- | ------------------------------ | ------------- | | @enclosurejs/compute | Channel, Caller, Pool, Service | UI thread | | @enclosurejs/compute/worker | handleMessages, reply | Worker thread |

Safety

Error Model

All errors thrown or rejected by this package are CoreError instances (domain 'compute'):

| Code | When | | -------------- | ----------------------------------------------------- | | TERMINATED | send() on a dead channel | | DISPOSED | invoke()/submit() after dispose | | SUPERSEDED | A new invoke() on a Caller aborted the previous one | | WORKER_ERROR | Worker fires onerror | | ABORTED | AbortSignal triggered | | TIMEOUT | Response not received within deadline |

Lifecycle Safety

  • All three primitives implement Disposable — add to DisposableGroup for automatic cleanup
  • channel.alive and disposed state prevent use-after-terminate errors
  • send() on a terminated channel throws immediately
  • invoke() on a disposed caller rejects immediately
  • submit() on a disposed pool rejects immediately
  • dispose() on a caller rejects any in-flight promise and terminates the worker
  • dispose() on a pool rejects all in-flight and queued tasks

Error Isolation

  • Worker crashes are caught via onerror — the host thread never crashes
  • Channel: errors forwarded to both onError handlers and the messages ReadableStream
  • Pool: idle worker onerror triggers automatic respawn to maintain pool size
  • Throwing onError handlers are isolated — one bad handler doesn't affect others

Cancellation Safety

  • AbortSignal support on Caller and Pool — Caller terminates the worker on abort; Pool frees the worker back to the idle set (reused, not terminated)
  • Timeout support — Caller terminates the worker on timeout; Pool frees the worker (same as abort)
  • Timer cleanup on normal completion — no dangling setTimeout

Transfer Safety

  • Transferable arrays are forwarded to postMessage for zero-copy handoff
  • Empty transfer arrays are handled (no unnecessary structured clone overhead)

Caveats

Caller: one invoke at a time

ComputeCaller allows only one in-flight invocation. Calling invoke() a second time before the first settles automatically aborts the previous call — the previous promise rejects with SUPERSEDED and its worker is terminated. No resources are orphaned. If you need concurrent invocations, use a Pool instead.

Pool: map() error behavior

map() yields results in completion order and throws on the first task failure, terminating the generator. Remaining in-flight tasks are not automatically cancelled — they continue executing in the pool. If you need per-task error handling without stopping iteration, use submit() in a loop with individual try/catch.

Pool: dispatch() and array mutation

dispatch() iterates the workers array with for...of. Recursive calls (from onAbortfinishTaskdispatch()) create a new iterator over the potentially-mutated array. This is safe today because finishTask only removes retiring workers (which are already busy=true → skipped by the outer iterator), and queue.shift() guards against double-dispatch. But modifications to the dispatch logic should be tested carefully for iterator invalidation.

Benchmarks

Not applicable in Node.js. Web Workers are a browser APInew Worker() requires a DOM-capable environment (browser, Electron renderer, Tauri webview). The unit tests use mock workers (vi.stubGlobal) for deterministic verification of lifecycle, error handling, abort/timeout, and pool scheduling logic. Real-world performance depends on:

  • postMessage structured clone overhead (~30ms for 10MB data)
  • Transferable zero-copy transfer (~0.01ms regardless of size)
  • Worker startup time (~5–50ms depending on script complexity)
  • Pool saturation — navigator.hardwareConcurrency workers can process N tasks in parallel

For production benchmarking, use the browser's Performance API inside your worker scripts.

Bundle Size

| Output | File | Size | | ------------ | ------------- | -------- | | Runtime (JS) | index.js | 11.68 KB | | | worker.js | 454 B | | Types (DTS) | index.d.ts | 3.72 KB | | | worker.d.ts | 782 B | | Total JS | | 12.13 KB | | Total | | 16.64 KB |

index.js contains all host-side primitives (Channel, Caller, Pool, ComputeService). worker.js contains only handleMessages + reply — shipped separately because it runs in the Worker thread. Single external dependency (@enclosurejs/core) is workspace-only and marked as external in the build.

Quality

| Metric | Value | | --------------------- | ---------------------------------------------------------------------- | | Unit tests | 72 (all pass) | | Test files | 5 (channel, caller, pool, compute, worker) | | Source files | 7 (types, channel, caller, pool, compute, worker, index) | | Dependencies | 1 (@enclosurejs/core — workspace) | | External dependencies | 0 (devDependencies only: tsup) | | Coverage thresholds | statements >= 90%, branches >= 85%, functions >= 95%, lines >= 90% |

Quality Layers

Layer 1: STATIC ANALYSIS (every commit)
  tsc --noEmit        strict mode, zero errors
  eslint              ESLint 9 flat config, zero warnings
  prettier --check    formatting

Layer 2: UNIT TESTS (every commit)
  72 tests            channel (16), caller (17), pool (23), compute (6), worker (10)
                      covers lifecycle, errors, abort, timeout, transfer, dispose,
                      stream error propagation, idle worker respawn, unordered map,
                      in-flight dispose, supersede, handler isolation
  v8 coverage         statements >= 90%, branches >= 85%, functions >= 95%, lines >= 90%

Layer 3: BENCHMARKS
  N/A                 browser-only API, not benchmarkable in Node.js

Layer 4: PACKAGE HEALTH
  1 workspace dep     @enclosurejs/core (types + Deferred, externalized in build)
  tsup build          ESM + DTS output, separate entrypoints (host + worker)

File Structure

packages/compute/
├── src/
│   ├── index.ts              Barrel: createComputeService, createChannel, createCaller, createPool
│   ├── types.ts              All interfaces (ComputeService, Channel, Caller, Pool, Descriptor)
│   ├── compute.ts            createComputeService() factory (shared channel id counter)
│   ├── channel.ts            createChannel() — bidirectional Worker + ReadableStream
│   ├── caller.ts             createCaller() — one-shot with in-flight dispose
│   ├── pool.ts               createPool() — N workers + FIFO queue + auto-respawn
│   ├── worker.ts             handleMessages(), reply() — worker-side helpers
│   └── __tests__/
│       ├── channel.test.ts   16 tests — send, stream, error→stream, handler isolation, lifecycle
│       ├── caller.test.ts    17 tests — invoke, errors, abort, timeout, idempotency, supersede, in-flight dispose
│       ├── pool.test.ts      23 tests — submit, drain, dispose, resize, map (unordered), respawn, abort, timeout
│       ├── compute.test.ts    6 tests — service factory, shared/independent id counters
│       └── worker.test.ts    10 tests — routing, unknown types, empty map, reply, transfer
├── .prettierignore
├── package.json
├── tsconfig.json
├── tsup.config.ts
└── README.md

License

MIT