···11+---
22+'moroutine': minor
33+---
44+55+Configurable load balancing for worker pools
66+77+- `workers(size, { balance: leastBusy() })` routes tasks to the worker with the fewest in-flight tasks
88+- `roundRobin()` cycles through workers in order (default, same as before)
99+- Custom balancers implement `Balancer.select(workers, task)` for full control over scheduling
+6-5
README.md
···111111Custom balancers implement the `Balancer` interface:
112112113113```ts
114114-import type { Balancer } from 'moroutine';
114114+import type { Balancer, WorkerHandle, Task, StreamTask } from 'moroutine';
115115116116-const myBalancer: Balancer = {
117117- select(workers, task) {
118118- return workers[0]; // always use first worker
116116+const random: Balancer = {
117117+ select(workers: readonly WorkerHandle[], task: Task<any> | StreamTask<any>) {
118118+ return workers[Math.floor(Math.random() * workers.length)];
119119 },
120120};
121121```
122122123123-Each worker handle exposes `thread` (the underlying `worker_threads.Worker`) and `activeCount` for building custom strategies.
123123+Each `WorkerHandle` exposes `activeCount` (in-flight tasks) and `thread` (the underlying `worker_threads.Worker`) for building custom strategies.
124124125125### Dedicated Workers
126126···428428- [`examples/sqlite`](examples/sqlite) -- shared SQLite database on a worker via task-arg caching
429429- [`examples/pipeline`](examples/pipeline) -- streaming pipeline across dedicated workers
430430- [`examples/channel-fanout`](examples/channel-fanout) -- fan-out a channel to multiple workers via work stealing
431431+- [`examples/load-balancing`](examples/load-balancing) -- round-robin vs least-busy with variable-cost tasks
431432- [`examples/benchmark`](examples/benchmark) -- roundtrip channel throughput with 1–N workers
+39
examples/load-balancing/main.ts
···11+// Compare load balancing strategies with variable-cost work.
22+// Requires Node v24+.
33+//
44+// Run: node examples/load-balancing/main.ts
55+66+import { setTimeout } from 'node:timers/promises';
77+import { workers, roundRobin, leastBusy } from '../../src/index.ts';
88+import { Balancer, Runner, WorkerHandle, Task } from '../../src/index.ts';
99+import { work } from './work.ts';
1010+1111+// Lopsided task costs (ms) — round-robin assigns by position and
1212+// ignores how long each worker has been busy, so one worker gets
1313+// all the heavy items. Least-busy routes to whichever worker has
1414+// fewer in-flight tasks, spreading the load more evenly.
1515+const tasks = [300, 30, 300, 30, 300, 30];
1616+1717+async function bench(label: string, run: Runner): Promise<void> {
1818+ const promises: Promise<number>[] = [];
1919+ const start = performance.now();
2020+ for (const ms of tasks) {
2121+ promises.push(run(work(ms)));
2222+ // Small delay so short tasks can complete between dispatches,
2323+ // giving least-busy useful active-count information.
2424+ await setTimeout(40);
2525+ }
2626+ await Promise.all(promises);
2727+ const elapsed = (performance.now() - start).toFixed(0);
2828+ console.log(`${label}: ${elapsed}ms`);
2929+}
3030+3131+{
3232+ using run = workers(2, { balance: roundRobin() });
3333+ await bench('Round-robin', run);
3434+}
3535+3636+{
3737+ using run = workers(2, { balance: leastBusy() });
3838+ await bench('Least-busy', run);
3939+}
+9
examples/load-balancing/work.ts
···11+import { mo } from '../../src/index.ts';
22+33+export const work = mo(import.meta, (ms: number): number => {
44+ const start = Date.now();
55+ while (Date.now() - start < ms) {
66+ /* busy wait */
77+ }
88+ return ms;
99+});