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: add load balancing example and changeset

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

+63 -5
+9
.changeset/load-balancing.md
··· 1 + --- 2 + 'moroutine': minor 3 + --- 4 + 5 + Configurable load balancing for worker pools 6 + 7 + - `workers(size, { balance: leastBusy() })` routes tasks to the worker with the fewest in-flight tasks 8 + - `roundRobin()` cycles through workers in order (default, same as before) 9 + - Custom balancers implement `Balancer.select(workers, task)` for full control over scheduling
+6 -5
README.md
··· 111 111 Custom balancers implement the `Balancer` interface: 112 112 113 113 ```ts 114 - import type { Balancer } from 'moroutine'; 114 + import type { Balancer, WorkerHandle, Task, StreamTask } from 'moroutine'; 115 115 116 - const myBalancer: Balancer = { 117 - select(workers, task) { 118 - return workers[0]; // always use first worker 116 + const random: Balancer = { 117 + select(workers: readonly WorkerHandle[], task: Task<any> | StreamTask<any>) { 118 + return workers[Math.floor(Math.random() * workers.length)]; 119 119 }, 120 120 }; 121 121 ``` 122 122 123 - Each worker handle exposes `thread` (the underlying `worker_threads.Worker`) and `activeCount` for building custom strategies. 123 + Each `WorkerHandle` exposes `activeCount` (in-flight tasks) and `thread` (the underlying `worker_threads.Worker`) for building custom strategies. 124 124 125 125 ### Dedicated Workers 126 126 ··· 428 428 - [`examples/sqlite`](examples/sqlite) -- shared SQLite database on a worker via task-arg caching 429 429 - [`examples/pipeline`](examples/pipeline) -- streaming pipeline across dedicated workers 430 430 - [`examples/channel-fanout`](examples/channel-fanout) -- fan-out a channel to multiple workers via work stealing 431 + - [`examples/load-balancing`](examples/load-balancing) -- round-robin vs least-busy with variable-cost tasks 431 432 - [`examples/benchmark`](examples/benchmark) -- roundtrip channel throughput with 1–N workers
+39
examples/load-balancing/main.ts
··· 1 + // Compare load balancing strategies with variable-cost work. 2 + // Requires Node v24+. 3 + // 4 + // Run: node examples/load-balancing/main.ts 5 + 6 + import { setTimeout } from 'node:timers/promises'; 7 + import { workers, roundRobin, leastBusy } from '../../src/index.ts'; 8 + import { Balancer, Runner, WorkerHandle, Task } from '../../src/index.ts'; 9 + import { work } from './work.ts'; 10 + 11 + // Lopsided task costs (ms) — round-robin assigns by position and 12 + // ignores how long each worker has been busy, so one worker gets 13 + // all the heavy items. Least-busy routes to whichever worker has 14 + // fewer in-flight tasks, spreading the load more evenly. 15 + const tasks = [300, 30, 300, 30, 300, 30]; 16 + 17 + async function bench(label: string, run: Runner): Promise<void> { 18 + const promises: Promise<number>[] = []; 19 + const start = performance.now(); 20 + for (const ms of tasks) { 21 + promises.push(run(work(ms))); 22 + // Small delay so short tasks can complete between dispatches, 23 + // giving least-busy useful active-count information. 24 + await setTimeout(40); 25 + } 26 + await Promise.all(promises); 27 + const elapsed = (performance.now() - start).toFixed(0); 28 + console.log(`${label}: ${elapsed}ms`); 29 + } 30 + 31 + { 32 + using run = workers(2, { balance: roundRobin() }); 33 + await bench('Round-robin', run); 34 + } 35 + 36 + { 37 + using run = workers(2, { balance: leastBusy() }); 38 + await bench('Least-busy', run); 39 + }
+9
examples/load-balancing/work.ts
··· 1 + import { mo } from '../../src/index.ts'; 2 + 3 + export const work = mo(import.meta, (ms: number): number => { 4 + const start = Date.now(); 5 + while (Date.now() - start < ms) { 6 + /* busy wait */ 7 + } 8 + return ms; 9 + });