···11+# Per-Worker Dispatch
22+33+## Goal
44+55+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.
66+77+## `WorkerHandle`
88+99+A lightweight handle representing a single worker in the pool.
1010+1111+```ts
1212+interface WorkerHandle {
1313+ exec<T>(task: Task<T>): Promise<T>;
1414+ exec<T>(task: StreamTask<T>, opts?: ChannelOptions): AsyncIterable<T>;
1515+}
1616+```
1717+1818+`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.
1919+2020+## `assign()`
2121+2222+Stamps a `WorkerHandle` onto a task and returns it. The task's type is preserved.
2323+2424+```ts
2525+function assign<T>(worker: WorkerHandle, task: Task<T>): Task<T>;
2626+function assign<T>(worker: WorkerHandle, task: StreamTask<T>): StreamTask<T>;
2727+```
2828+2929+Internally sets `task.worker = worker` and returns the same object.
3030+3131+## `Task` / `StreamTask` Changes
3232+3333+Both classes gain an optional `worker?: WorkerHandle` property. When set, `run()` dispatches to that worker instead of using round-robin.
3434+3535+## `Runner` Changes
3636+3737+```ts
3838+type Runner = {
3939+ <T>(task: Task<T>): Promise<T>;
4040+ <T extends Task<any>[]>(tasks: [...T]): Promise<TaskResults<T>>;
4141+ <T>(task: StreamTask<T>, opts?: ChannelOptions): AsyncIterable<T>;
4242+ readonly signal: AbortSignal;
4343+ readonly workers: readonly WorkerHandle[];
4444+ [Symbol.dispose](): void;
4545+ [Symbol.asyncDispose](): Promise<void>;
4646+};
4747+```
4848+4949+`workers` is a frozen array of handles, one per pool worker.
5050+5151+## Dispatch Behavior
5252+5353+- `task.worker` set → dispatch to that worker (pinned).
5454+- `task.worker` not set → round-robin (existing behavior).
5555+- Pinned dispatches do NOT bump the round-robin counter.
5656+- Pinned dispatches ARE tracked in the in-flight set.
5757+- After dispose, pinned dispatches reject (regular) or throw (streaming).
5858+5959+## Usage
6060+6161+```ts
6262+// Pin each worker to a channel consumer
6363+{
6464+ using run = workers();
6565+ const ch = channel(generate(200));
6666+ const results = await run(run.workers.map(w => assign(w, process(ch))));
6767+}
6868+6969+// Mix pinned and unpinned in a batch
7070+await run([assign(run.workers[0], taskA), taskB]);
7171+7272+// Direct exec on a handle
7373+const result = await run.workers[0].exec(someTask());
7474+```
7575+7676+## Files Changed
7777+7878+- `src/runner.ts` — add `WorkerHandle`, add `workers` to `Runner`
7979+- `src/task.ts` — add optional `worker` property
8080+- `src/stream-task.ts` — add optional `worker` property
8181+- `src/assign.ts` — new file, `assign()` function
8282+- `src/worker-pool.ts` — build frozen `WorkerHandle[]`, check `task.worker` in dispatch
8383+- `src/index.ts` — export `assign`, `WorkerHandle`
8484+- `examples/channel-fanout/main.ts` — update to use new API
8585+- Tests