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: update examples list in README, note Node v24 requirement

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

+246
+246
README.md
··· 1 + # moroutine 2 + 3 + > Offload functions to worker threads with shared memory primitives for Node.js. 4 + 5 + ## Quick Start 6 + 7 + ```ts 8 + // is-prime.ts 9 + import { mo } from 'moroutine'; 10 + 11 + export const isPrime = mo(import.meta, (n: number): boolean => { 12 + if (n < 2) return false; 13 + for (let i = 2; i * i <= n; i++) { 14 + if (n % i === 0) return false; 15 + } 16 + return true; 17 + }); 18 + ``` 19 + 20 + ```ts 21 + // main.ts 22 + import { workers } from 'moroutine'; 23 + import { isPrime } from './is-prime.ts'; 24 + 25 + using run = workers(4); 26 + const results = await run([isPrime(999_999_937), isPrime(1_000_000_007)]); 27 + console.log(results); // [true, true] 28 + ``` 29 + 30 + Define a function with `mo()` in its own module, then import and run it on a worker pool. Moroutine modules must be side-effect free — workers import them to find the registered functions. 31 + 32 + ## Core API 33 + 34 + ### `mo(import.meta, fn)` 35 + 36 + Wraps a function so it runs on a worker thread. The function must be defined at module scope (not dynamically). 37 + 38 + ```ts 39 + // math.ts 40 + import { mo } from 'moroutine'; 41 + 42 + export const add = mo(import.meta, (a: number, b: number): number => { 43 + return a + b; 44 + }); 45 + ``` 46 + 47 + ### `workers(size)` 48 + 49 + Creates a pool of worker threads. Returns a `Runner` that dispatches tasks with round-robin scheduling. Disposable via `using` or `[Symbol.dispose]()`. 50 + 51 + ```ts 52 + import { workers } from 'moroutine'; 53 + import { add } from './math.ts'; 54 + 55 + { 56 + using run = workers(2); 57 + 58 + const result = await run(add(3, 4)); // single task 59 + const [a, b] = await run([add(1, 2), add(3, 4)]); // batch 60 + } 61 + ``` 62 + 63 + ### Dedicated Workers 64 + 65 + Awaiting a task directly (without a pool) runs it on a dedicated worker thread, one per moroutine function. 66 + 67 + ```ts 68 + const result = await add(3, 4); // runs on a dedicated worker for `add` 69 + ``` 70 + 71 + ## Shared Memory 72 + 73 + ### Descriptors and `shared()` 74 + 75 + Shared-memory types are created with descriptor functions or the `shared()` allocator. 76 + 77 + ```ts 78 + import { shared, int32, bool, mutex, string, bytes } from 'moroutine'; 79 + ``` 80 + 81 + #### Primitives 82 + 83 + ```ts 84 + const counter = int32(); // standalone Int32 85 + const flag = bool(); // standalone Bool 86 + const big = int64(); // standalone Int64 (bigint) 87 + ``` 88 + 89 + #### Atomics 90 + 91 + Atomic variants use `Atomics.*` for thread-safe operations without a lock. 92 + 93 + ```ts 94 + const counter = int32atomic(); 95 + counter.add(1); // atomic increment, returns previous value 96 + counter.load(); // atomic read 97 + ``` 98 + 99 + Full atomic operations: `load`, `store`, `add`, `sub`, `and`, `or`, `xor`, `exchange`, `compareExchange`. 100 + 101 + #### Structs 102 + 103 + Plain objects in `shared()` create structs backed by a single `SharedArrayBuffer`. 104 + 105 + ```ts 106 + const point = shared({ x: int32, y: int32 }); 107 + 108 + point.load(); // { x: 0, y: 0 } 109 + point.store({ x: 10, y: 20 }); 110 + point.fields.x.store(10); // direct field access 111 + ``` 112 + 113 + Structs nest: 114 + 115 + ```ts 116 + const rect = shared({ 117 + pos: { x: int32, y: int32 }, 118 + size: { w: int32, h: int32 }, 119 + }); 120 + ``` 121 + 122 + #### Tuples 123 + 124 + Arrays in `shared()` create fixed-length tuples. 125 + 126 + ```ts 127 + const pair = shared([int32, bool]); 128 + pair.load(); // [0, false] 129 + pair.store([42, true]); 130 + pair.get(0).store(99); 131 + ``` 132 + 133 + #### Bytes and Strings 134 + 135 + ```ts 136 + const buf = bytes(32); // fixed 32-byte buffer 137 + buf.store(new Uint8Array(32)); // exact length required 138 + buf.load(); // Readonly<Uint8Array> view 139 + buf.view[0] = 0xff; // direct mutable access 140 + 141 + const name = string(64); // UTF-8, max 64 bytes 142 + name.store('hello'); 143 + name.load(); // 'hello' 144 + ``` 145 + 146 + #### Value Shorthand 147 + 148 + Primitive values in schemas infer their type. 149 + 150 + ```ts 151 + shared(0) // Int32 initialized to 0 152 + shared(true) // Bool initialized to true 153 + shared(0n) // Int64 initialized to 0n 154 + shared({ x: 10, y: 20 }) // struct with Int32 fields 155 + ``` 156 + 157 + ### Locks 158 + 159 + #### Mutex 160 + 161 + ```ts 162 + const mu = mutex(); 163 + 164 + using guard = await mu.lock(); 165 + // exclusive access 166 + // auto-unlocks when guard is disposed 167 + 168 + // or manually: 169 + await mu.lock(); 170 + mu.unlock(); 171 + ``` 172 + 173 + #### RwLock 174 + 175 + ```ts 176 + const rw = rwlock(); 177 + 178 + using guard = await rw.readLock(); // multiple readers OK 179 + using guard = await rw.writeLock(); // exclusive access 180 + ``` 181 + 182 + ### Using with Workers 183 + 184 + Shared-memory types pass through `postMessage` automatically. They're reconstructed on the worker side with the same shared backing memory. 185 + 186 + ```ts 187 + // update-position.ts 188 + import { mo } from 'moroutine'; 189 + import type { Mutex, SharedStruct, Int32 } from 'moroutine'; 190 + 191 + type Position = SharedStruct<{ x: Int32; y: Int32 }>; 192 + 193 + export const updatePosition = mo( 194 + import.meta, 195 + async (mu: Mutex, pos: Position, dx: number, dy: number): Promise<void> => { 196 + using guard = await mu.lock(); 197 + const current = pos.load(); 198 + pos.store({ x: current.x + dx, y: current.y + dy }); 199 + }, 200 + ); 201 + ``` 202 + 203 + ```ts 204 + // main.ts 205 + import { workers, shared, int32, mutex } from 'moroutine'; 206 + import { updatePosition } from './update-position.ts'; 207 + 208 + const mu = mutex(); 209 + const pos = shared({ x: int32, y: int32 }); 210 + 211 + { 212 + using run = workers(4); 213 + await run([ 214 + updatePosition(mu, pos, 1, 0), 215 + updatePosition(mu, pos, 0, 1), 216 + ]); 217 + } 218 + 219 + console.log(pos.load()); // { x: 1, y: 1 } 220 + ``` 221 + 222 + ## Transfers 223 + 224 + Use `transfer()` for zero-copy movement of `ArrayBuffer`, `TypedArray`, `MessagePort`, or streams. 225 + 226 + ```ts 227 + import { transfer } from 'moroutine'; 228 + 229 + const buf = new ArrayBuffer(1024); 230 + await run(processData(transfer(buf))); 231 + // buf is now detached (zero-length) — ownership moved to worker 232 + ``` 233 + 234 + Return values from workers are auto-transferred when possible. 235 + 236 + ## Examples 237 + 238 + All examples require Node v24+ and can be run directly, e.g. `node examples/primes/main.ts`. 239 + 240 + - [`examples/primes`](examples/primes) -- CPU-bound prime checking on a dedicated worker 241 + - [`examples/non-blocking`](examples/non-blocking) -- main thread stays responsive during heavy computation 242 + - [`examples/parallel-batch`](examples/parallel-batch) -- sequential vs parallel batch processing 243 + - [`examples/atomics`](examples/atomics) -- shared atomic counter across workers 244 + - [`examples/shared-state`](examples/shared-state) -- mutex-protected shared struct 245 + - [`examples/multi-module`](examples/multi-module) -- moroutines from multiple modules on one worker 246 + - [`examples/transfer`](examples/transfer) -- zero-copy buffer transfer to and from a worker