{"_id":"@capacities/courier","_rev":"2-f796f33aa09bec76eb4fa929dbb3a066","name":"@capacities/courier","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@capacities/courier","version":"1.0.0","author":{"url":"Capacities Labs GmbH","name":"Luca Raúl Joos","email":"luca@capacities.io"},"license":"MIT","_id":"@capacities/courier@1.0.0","maintainers":[{"name":"lucajoos","email":"me@lucajoos.de"},{"name":"capacities-team","email":"team@capacities.io"}],"homepage":"https://github.com/capacities/courier#readme","bugs":{"url":"https://github.com/capacities/courier/issues"},"dist":{"shasum":"7282c0010b65e02b7a4d6cde56934d9aee08dc65","tarball":"https://registry.npmjs.org/@capacities/courier/-/courier-1.0.0.tgz","fileCount":19,"integrity":"sha512-eLt8/Qmnrm6LyXAAHHz3JamEjKmUuH3Z/WV2VisGmgoFFjaU9TQahOOnrDKW2mMBCUmY8sQZp27/bdapwQKt6w==","signatures":[{"sig":"MEQCIBfDqynDGHP3ZuJ2I209oktUdpYRhJJADS+X3DdRwGESAiBnG4Qa7e9YIl8fX3mStSmlMyvdSlNKGVJA7c9AQQdOBA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":99584},"type":"module","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"f5cb707f0cda792686b0310d60a1ac89b1d18e4a","scripts":{"lint":"eslint .","test":"vitest run","build":"rm -rf dist && tsc -p tsconfig.build.json","format":"prettier --write .","lint:fix":"eslint . --fix","typecheck":"tsc --noEmit","format:check":"prettier --check .","prepublishOnly":"pnpm build"},"_npmUser":{"name":"capacities-team","email":"team@capacities.io"},"repository":{"url":"git+https://github.com/capacities/courier.git","type":"git"},"_npmVersion":"10.9.8","description":"Type-safe RPC for Web Workers over MessagePort","directories":{},"_nodeVersion":"22.22.3","dependencies":{"zod":"^4.4.3","uuid":"^14.0.1","lodash-es":"^4.18.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^10.5.0","vitest":"^4.1.9","prettier":"^3.8.4","@eslint/js":"^10.0.1","typescript":"^6.0.3","@eslint/compat":"^2.1.0","@eslint/eslintrc":"^3.3.5","@types/lodash-es":"^4.17.12","eslint-plugin-import":"^2.32.0","eslint-config-prettier":"^10.1.8","eslint-plugin-prettier":"^5.5.6","@typescript-eslint/parser":"^8.62.0","eslint-plugin-unused-imports":"^4.4.1","@typescript-eslint/eslint-plugin":"^8.62.0"},"_npmOperationalInternal":{"tmp":"tmp/courier_1.0.0_1782306402290_0.1810687575267409","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@capacities/courier","version":"1.0.1","description":"Type-safe RPC for Web Workers over MessagePort","type":"module","keywords":["webworker","rpc","typescript"],"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"publishConfig":{"access":"public"},"scripts":{"build":"rm -rf dist && tsc -p tsconfig.build.json","prepublishOnly":"pnpm build","format":"prettier --write .","format:check":"prettier --check .","lint":"eslint .","lint:fix":"eslint . --fix","test":"vitest run","typecheck":"tsc --noEmit"},"repository":{"type":"git","url":"git+https://github.com/capacities/courier.git"},"author":{"name":"Luca Raúl Joos","email":"luca@capacities.io","url":"Capacities Labs GmbH"},"bugs":{"url":"https://github.com/capacities/courier/issues"},"homepage":"https://github.com/capacities/courier#readme","license":"MIT","devDependencies":{"@eslint/compat":"^2.1.0","@eslint/eslintrc":"^3.3.5","@eslint/js":"^10.0.1","@types/lodash-es":"^4.17.12","@typescript-eslint/eslint-plugin":"^8.62.0","@typescript-eslint/parser":"^8.62.0","eslint":"^10.5.0","eslint-config-prettier":"^10.1.8","eslint-plugin-import":"^2.32.0","eslint-plugin-prettier":"^5.5.6","eslint-plugin-unused-imports":"^4.4.1","prettier":"^3.8.4","typescript":"^6.0.3","vitest":"^4.1.9"},"dependencies":{"lodash-es":"^4.18.1","uuid":"^14.0.1","zod":"^4.4.3"},"_id":"@capacities/courier@1.0.1","gitHead":"357755de11f7d99e0f2f7eade48014489023c7d1","_nodeVersion":"22.22.3","_npmVersion":"10.9.8","dist":{"integrity":"sha512-45Rp0BO3EYju2e32Qaf1D2xqGcojvsVldgwQjBl1gE9BU1Df4ZropPZ9ThJ3uv9dfpSIaWTH0IwyHIjBvwHV/w==","shasum":"92b3aeb0f1b85bb408a6d7c899b71d6e51f3e757","tarball":"https://registry.npmjs.org/@capacities/courier/-/courier-1.0.1.tgz","fileCount":19,"unpackedSize":102908,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCIo2lTbRB2wf3EgFje3Bj4jJ4MBj3YyeE5ZgziycSU0QIgJw/4Fs1seuoyRKV3SZ2MCp1d1I5W6AU3wQVFxI15MZI="}]},"_npmUser":{"name":"capacities-team","email":"team@capacities.io"},"directories":{},"maintainers":[{"name":"lucajoos","email":"me@lucajoos.de"},{"name":"capacities-team","email":"team@capacities.io"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/courier_1.0.1_1782307092631_0.14221020120636174"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-24T13:06:42.095Z","modified":"2026-06-24T13:18:12.985Z","1.0.0":"2026-06-24T13:06:42.520Z","1.0.1":"2026-06-24T13:18:12.782Z"},"bugs":{"url":"https://github.com/capacities/courier/issues"},"author":{"name":"Luca Raúl Joos","email":"luca@capacities.io","url":"Capacities Labs GmbH"},"license":"MIT","homepage":"https://github.com/capacities/courier#readme","repository":{"type":"git","url":"git+https://github.com/capacities/courier.git"},"description":"Type-safe RPC for Web Workers over MessagePort","maintainers":[{"name":"lucajoos","email":"me@lucajoos.de"},{"name":"capacities-team","email":"team@capacities.io"}],"readme":"# @capacities/courier\n\nType-safe RPC for [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) over `MessagePort`.\n\nCourier gives you a type-safe RPC layer between a host and one or more workers. You define the message contract once in TypeScript; `emit`, `on` and `bind` stay fully typed on both sides.\n\nBuilt at [Capacities](https://capacities.io) and used in our own codebase.\n\n## Why Courier?\n\n- **End-to-end TypeScript** - define your contract once; `emit`, `on` and handler arguments/returns are checked on host and every worker.\n- **Direct worker-to-worker calls** - the host builds a full mesh of `MessageChannel`s, so any participant can call any other without relaying through the main thread.\n- **Production-ready protocol** - initialization handshake, optional heartbeat, per-event timeouts and disconnect handling are built in.\n- **Request middleware** - `intercept` lets you gate, log, or reject incoming RPCs before they hit your handlers.\n- **Chrome DevTools profiling** - `isProfiling` records round-trip measurements with handler vs messaging breakdown. See [Profiling](#profiling).\n\nWith three participants, a worker can call another worker directly:\n\n```ts\ntype Definition = {\n  main: {\n    onSaved: (id: string) => void\n  }\n  processing: {\n    ingest: (raw: string) => { id: string; title: string; tokens: string[] }\n  }\n  database: {\n    save: (record: { id: string; title: string; tokens: string[] }) => void\n    get: (id: string) => { id: string; title: string; tokens: string[] } | null\n  }\n}\n\n// Inside the processing worker - no need to bounce through main\nconst ipc = await courier.worker<Definition, 'processing'>('processing', self)\n\nipc.on('ingest', async (raw) => {\n  const record = {\n    id: crypto.randomUUID(),\n    title: raw.split('\\n')[0] ?? '',\n    tokens: raw.toLowerCase().split(/\\s+/),\n  }\n\n  await ipc.emit('database', 'save', record)\n  await ipc.emit('main', 'onSaved', record.id)\n\n  return record\n})\n```\n\n## Comparison\n\n|                               | **[Courier](https://github.com/capacities/courier)**      | **[Comlink](https://github.com/GoogleChromeLabs/comlink)** | **[threads.js](https://github.com/andywer/threads.js)** | **Raw `postMessage`** |\n| ----------------------------- | --------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------- | --------------------- |\n| **Model**                     | Named participants, shared contract                       | Proxy over an exposed object                               | Thread pool for parallel tasks                          | Ad-hoc messages       |\n| **API**                       | `emit(target, event, …)` / `on(event, handler)`           | `Comlink.wrap(endpoint).method()`                          | `pool.queue(fn)`                                        | Custom protocol       |\n| **Typing**                    | ✅ One `Definition` types every participant's `emit`/`on` | ✅ `Remote<T>` from `wrap<T>()`¹                           | ✅ via `spawn<T>()`¹                                    | ❌                    |\n| **Worker-to-worker**          | ✅ Mesh wired by `courier.host`                           | ⚠️ Works over `MessagePort`; channels are DIY              | ❌ Not the focus                                        | ⚠️ DIY                |\n| **Multi-worker setup**        | ✅ `courier.host` connects all ports                      | ⚠️ One `wrap()` per endpoint                               | ✅ Worker pool                                          | ⚠️ DIY                |\n| **Handshake & heartbeat**     | ✅ Built-in                                               | ❌                                                         | ❌                                                      | ❌                    |\n| **RPC timeouts & disconnect** | ✅ Built-in                                               | ❌                                                         | ❌                                                      | ❌                    |\n| **Per-RPC DevTools tracks**   | ✅ Optional `isProfiling` (handler vs messaging split)²   | ❌ No built-in instrumentation                             | ❌ No built-in instrumentation                          | ❌                    |\n| **Transferables / callbacks** | 🔜 Planned                                                | ✅ `Comlink.transfer`, `Comlink.proxy`                     | ✅ Via Comlink                                          | ⚠️ Manual             |\n\n¹ Comlink and threads.js both provide real generic typing (`Remote<T>`, typed spawn). The gap vs Courier is mostly **shape**: Courier's single `Definition` schema types named participants and cross-worker routes together; Comlink/threads.js type the exposed API surface, with occasional `as unknown as` at the edges.\n\n² Chrome can profile any worker natively (`chrome://inspect`). This row is about **library-level** `performance.measure()` tracks per RPC, not whether profiling is possible at all.\n\n✅ Built-in · ⚠️ Possible, not provided · ❌ Not provided · 🔜 Planned\n\n## Install\n\n```bash\nnpm install @capacities/courier\n```\n\n## Quick start\n\nDefine a **definition** - a map of participant names to their exposed handlers:\n\n```ts\ntype Definition = {\n  main: {\n    onResult: (value: number) => void\n  }\n  worker: {\n    compute: (a: number, b: number) => number\n  }\n}\n```\n\n### Host\n\n```ts\nimport { courier } from '@capacities/courier'\n\nconst ipc = courier.host<Definition, 'main'>('main', {\n  worker: () => new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }),\n})\n\nipc.on('onResult', (value) => {\n  console.log('result:', value)\n})\n\nconst sum = await ipc.emit('worker', 'compute', 2, 3)\nconsole.log(sum) // 5\n```\n\n### Worker\n\n```ts\nimport { courier } from '@capacities/courier'\n\nconst ipc = await courier.worker<Definition, 'worker'>('worker', self)\n\nipc.on('compute', (a, b) => a + b)\n\nawait ipc.emit('main', 'onResult', 42)\n```\n\nCourier handles the handshake: the host spins up `MessageChannel`s between all participants, transfers ports, waits for initialization, then signals readiness.\n\n## API\n\n### `courier.host(self, workers, options?)`\n\nCreates the host-side IPC interface. `workers` maps each worker name to a factory that returns a `Worker` (or mock).\n\nReturns an `Courier.IPC` instance synchronously. Workers are terminated when `close()` is called.\n\n### `courier.worker(self, scope, options?)`\n\nBootstraps a worker. Pass `self` (the global `WorkerGlobalScope` in a real worker).\n\nReturns a `Promise<Courier.IPC>` that resolves once the host signals readiness.\n\n### `courier.ipc({ self, ports, ready }, options)`\n\nLow-level setup when you manage `MessagePort`s yourself.\n\n### `courier.mock(self, callback, options?)`\n\nCreates an in-memory host/worker pair for tests - no real `Worker` thread needed.\n\n```ts\nimport { courier } from '@capacities/courier'\n\nconst mock = courier.mock<Definition, 'worker'>('worker', (ipc) => {\n  ipc.on('compute', (a, b) => a + b)\n})\n\nconst host = courier.host<Definition, 'main'>('main', {\n  worker: () => mock,\n})\n\nawait host.emit('worker', 'compute', 3, 9) // 12\n```\n\n## IPC interface\n\nEach side exposes:\n\n| Method                         | Description                                                                 |\n| ------------------------------ | --------------------------------------------------------------------------- |\n| `emit(target, event, ...args)` | Call a handler on another participant. Returns a `Promise` with the result. |\n| `on(event, callback)`          | Register a handler for incoming requests. Returns an unsubscribe function.  |\n| `once(event, callback)`        | Same as `on`, but runs at most once.                                        |\n| `bind(handlers)`               | Register multiple handlers at once.                                         |\n| `close()`                      | Tear down listeners and ports.                                              |\n| `self`                         | This participant's name.                                                    |\n\nHandlers may be sync or async - return values and thrown errors are propagated back to the caller.\n\n## Options\n\nPass partial options to override defaults from `Courier.DEFAULTS` (host, worker and low-level `ipc` each accept a slightly different subset):\n\n```ts\ncourier.host(\n  'main',\n  { worker: () => new Worker('./worker.js') },\n  {\n    timeouts: {\n      // (Host only) Max time to wait for every worker to finish the initialize handshake.\n      initialization: 5_000,\n\n      // Max time an `intercept` callback may run before the request is rejected.\n      intercept: 5_000,\n\n      // Max time an `emit` may take before its promise rejects.\n      // Use a number for one global limit, or an object for per-target/per-event overrides.\n      events: Infinity,\n    },\n\n    // Optional liveness checks. Sends periodic pings; marks a target dead after too many misses.\n    heartbeat: {\n      interval: 1_000, // How often to ping each target (ms).\n      timeout: 500, // How long to wait for a pong before counting a miss (ms).\n      threshold: 3, // Consecutive misses before `onDead` runs and the target is closed.\n      isPaused: () => document.hidden, // When true, pauses probes and resets miss counters.\n      targets: ['worker'], // Which peers to monitor. Defaults to all connected targets.\n      onDead: (target) => console.warn(`${target} is unresponsive`),\n    },\n\n    // Middleware invoked on incoming requests before they reach your handler.\n    // Call `proceed()` to continue or `reject(reason)` to fail the request.\n    // Set to `null` (default) to disable.\n    intercept: (event, parameters, { proceed, reject }) => {\n      proceed()\n    },\n\n    // Called when a participant disconnects or is closed (including after heartbeat failure).\n    onClose: (target) => console.warn(`${target} disconnected`),\n\n    // Log internal warnings (handshake failures, port errors, heartbeat issues, etc.).\n    isVerbose: false,\n\n    // (Host only) Record `performance.measure()` entries for Chrome DevTools. See Profiling below.\n    isProfiling: false,\n  }\n)\n```\n\nWorker options are the same except there is no `timeouts.initialization` and `isProfiling` / `isVerbose` are inherited from the host during handshake rather than set on the worker directly.\n\n`bind(handlers, { isOnce: true })` accepts a listen option to auto-unsubscribe after the first matching request.\n\n## Profiling\n\nCourier has built-in support for the [Performance API](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API) and Chrome DevTools custom tracks. Enable it on the host - the setting is forwarded to every worker during handshake:\n\n```ts\nconst ipc = courier.host<Definition, 'main'>(\n  'main',\n  { worker: () => new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }) },\n  { isProfiling: true }\n)\n```\n\nEvery `emit` then records a `performance.measure()` entry that shows up in the **Chrome Performance** panel.\n\n**Typical workflow:**\n\n1. Set `isProfiling: true` in development (keep it off in production).\n2. Open Chrome DevTools → **Performance**.\n3. Record a session while your app runs worker RPCs.\n4. Look for the **Courier** track group - each bar is one typed `emit`, hover for the computation/communication breakdown.\n\n## Requirements\n\n- **Browser** - relies on `Worker`, `MessageChannel` and `MessagePort`\n- **ESM** - this package ships as ES modules only\n\n## Development\n\n```bash\npnpm install\npnpm test\npnpm typecheck\npnpm lint\npnpm build\n```\n\n## License\n\n[MIT](./LICENSE) © Capacities Labs GmbH\n","readmeFilename":"README.md","keywords":["webworker","rpc","typescript"]}