Offload functions to worker threads with shared memory primitives for Node.js.
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: per-worker dispatch design spec

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+85
+85
docs/superpowers/specs/2026-04-13-per-worker-dispatch-design.md
··· 1 + # Per-Worker Dispatch 2 + 3 + ## Goal 4 + 5 + Allow users to pin tasks to specific workers, removing the need to know worker count or assume round-robin scheduling. Primary use case: channel fan-out where each worker gets a task consuming from the same channel. 6 + 7 + ## `WorkerHandle` 8 + 9 + A lightweight handle representing a single worker in the pool. 10 + 11 + ```ts 12 + interface WorkerHandle { 13 + exec<T>(task: Task<T>): Promise<T>; 14 + exec<T>(task: StreamTask<T>, opts?: ChannelOptions): AsyncIterable<T>; 15 + } 16 + ``` 17 + 18 + `exec()` dispatches a task pinned to this specific worker. Bypasses round-robin. Does not bump the pool's round-robin counter. Tracked in the in-flight set (async dispose waits). Rejects/throws after dispose. 19 + 20 + ## `assign()` 21 + 22 + Stamps a `WorkerHandle` onto a task and returns it. The task's type is preserved. 23 + 24 + ```ts 25 + function assign<T>(worker: WorkerHandle, task: Task<T>): Task<T>; 26 + function assign<T>(worker: WorkerHandle, task: StreamTask<T>): StreamTask<T>; 27 + ``` 28 + 29 + Internally sets `task.worker = worker` and returns the same object. 30 + 31 + ## `Task` / `StreamTask` Changes 32 + 33 + Both classes gain an optional `worker?: WorkerHandle` property. When set, `run()` dispatches to that worker instead of using round-robin. 34 + 35 + ## `Runner` Changes 36 + 37 + ```ts 38 + type Runner = { 39 + <T>(task: Task<T>): Promise<T>; 40 + <T extends Task<any>[]>(tasks: [...T]): Promise<TaskResults<T>>; 41 + <T>(task: StreamTask<T>, opts?: ChannelOptions): AsyncIterable<T>; 42 + readonly signal: AbortSignal; 43 + readonly workers: readonly WorkerHandle[]; 44 + [Symbol.dispose](): void; 45 + [Symbol.asyncDispose](): Promise<void>; 46 + }; 47 + ``` 48 + 49 + `workers` is a frozen array of handles, one per pool worker. 50 + 51 + ## Dispatch Behavior 52 + 53 + - `task.worker` set → dispatch to that worker (pinned). 54 + - `task.worker` not set → round-robin (existing behavior). 55 + - Pinned dispatches do NOT bump the round-robin counter. 56 + - Pinned dispatches ARE tracked in the in-flight set. 57 + - After dispose, pinned dispatches reject (regular) or throw (streaming). 58 + 59 + ## Usage 60 + 61 + ```ts 62 + // Pin each worker to a channel consumer 63 + { 64 + using run = workers(); 65 + const ch = channel(generate(200)); 66 + const results = await run(run.workers.map(w => assign(w, process(ch)))); 67 + } 68 + 69 + // Mix pinned and unpinned in a batch 70 + await run([assign(run.workers[0], taskA), taskB]); 71 + 72 + // Direct exec on a handle 73 + const result = await run.workers[0].exec(someTask()); 74 + ``` 75 + 76 + ## Files Changed 77 + 78 + - `src/runner.ts` — add `WorkerHandle`, add `workers` to `Runner` 79 + - `src/task.ts` — add optional `worker` property 80 + - `src/stream-task.ts` — add optional `worker` property 81 + - `src/assign.ts` — new file, `assign()` function 82 + - `src/worker-pool.ts` — build frozen `WorkerHandle[]`, check `task.worker` in dispatch 83 + - `src/index.ts` — export `assign`, `WorkerHandle` 84 + - `examples/channel-fanout/main.ts` — update to use new API 85 + - Tests