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 TSDoc to all public interfaces

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

+84
+6
src/mo.ts
··· 3 3 4 4 const counters = new Map<string, number>(); 5 5 6 + /** 7 + * Wraps a function to run on a worker thread. Must be called at module scope. 8 + * @param importMeta - The `import.meta` of the calling module, used to identify the source file. 9 + * @param fn - The function to offload to a worker thread. 10 + * @returns A function that creates a {@link Task} when called. 11 + */ 6 12 export function mo<A extends unknown[], R>(importMeta: ImportMeta, fn: (...args: A) => R): (...args: A) => Task<R> { 7 13 const url = importMeta.url; 8 14
+1
src/runner.ts
··· 2 2 3 3 type TaskResults<T extends Task<any>[]> = { [K in keyof T]: T[K] extends Task<infer R> ? R : never }; 4 4 5 + /** A callable that dispatches tasks to a worker pool. Disposable via `using` or `[Symbol.dispose]()`. */ 5 6 export type Runner = { 6 7 <T>(task: Task<T>): Promise<T>; 7 8 <T extends Task<any>[]>(tasks: [...T]): Promise<TaskResults<T>>;
+9
src/shared/bytes.ts
··· 2 2 3 3 const SHARED = Symbol.for('moroutine.shared'); 4 4 5 + /** A fixed-size shared byte buffer backed by SharedArrayBuffer. */ 5 6 export class Bytes implements Loadable<Readonly<Uint8Array>> { 6 7 readonly size: number; 7 8 readonly view: Uint8Array; ··· 15 16 this.view = new Uint8Array(buf, offset, size); 16 17 } 17 18 19 + /** 20 + * Returns a readonly view of the buffer. No copy — mutations via `view` are visible. 21 + * @returns A readonly typed array view into the shared buffer. 22 + */ 18 23 load(): Readonly<Uint8Array> { 19 24 return this.view as Readonly<Uint8Array>; 20 25 } 21 26 27 + /** 28 + * Writes data into the buffer. Must be exact length. 29 + * @param value - A Uint8Array whose length must equal the buffer's size. 30 + */ 22 31 store(value: Uint8Array): void { 23 32 if (value.length !== this.size) { 24 33 throw new RangeError(`Expected Uint8Array of length ${this.size}, got ${value.length}`);
+14
src/shared/descriptors.ts
··· 21 21 import { Mutex } from './mutex.ts'; 22 22 import { RwLock } from './rwlock.ts'; 23 23 24 + /** A schema token describing a shared-memory type. Callable to create a standalone instance. */ 24 25 export interface Descriptor<T> { 25 26 (): T; 26 27 byteSize: number; ··· 37 38 return Object.assign(factory, { byteSize, byteAlignment, _class }); 38 39 } 39 40 41 + /** Non-atomic shared primitives. Use inside a lock for thread safety. */ 40 42 export const int8: Descriptor<Int8> = makeDescriptor(() => new Int8(), Int8.byteSize, Int8.byteAlignment, Int8); 41 43 export const uint8: Descriptor<Uint8> = makeDescriptor(() => new Uint8(), Uint8.byteSize, Uint8.byteAlignment, Uint8); 42 44 export const int16: Descriptor<Int16> = makeDescriptor(() => new Int16(), Int16.byteSize, Int16.byteAlignment, Int16); ··· 62 64 ); 63 65 export const bool: Descriptor<Bool> = makeDescriptor(() => new Bool(), Bool.byteSize, Bool.byteAlignment, Bool); 64 66 67 + /** Atomic shared primitives. Thread-safe without a lock. */ 65 68 export const int8atomic: Descriptor<Int8Atomic> = makeDescriptor( 66 69 () => new Int8Atomic(), 67 70 Int8Atomic.byteSize, ··· 117 120 BoolAtomic, 118 121 ); 119 122 123 + /** Shared-memory locks. */ 120 124 export const mutex: Descriptor<Mutex> = makeDescriptor(() => new Mutex(), Mutex.byteSize, Mutex.byteAlignment, Mutex); 121 125 export const rwlock: Descriptor<RwLock> = makeDescriptor( 122 126 () => new RwLock(), ··· 132 136 _size: number; 133 137 } 134 138 139 + /** 140 + * Creates a fixed-size shared byte buffer. Acts as both a standalone factory and a schema descriptor. 141 + * @param size - The buffer capacity in bytes. 142 + * @returns A {@link Bytes} instance with descriptor metadata for use with `shared()`. 143 + */ 135 144 export function bytes(size: number): BytesDescriptor { 136 145 const instance = new Bytes(size); 137 146 return Object.assign(instance, { ··· 149 158 _maxBytes: number; 150 159 } 151 160 161 + /** 162 + * Creates a variable-length shared UTF-8 string with a max byte length. Acts as both a standalone factory and a schema descriptor. 163 + * @param maxBytes - Maximum number of bytes for the encoded string. 164 + * @returns A {@link SharedString} instance with descriptor metadata for use with `shared()`. 165 + */ 152 166 export function string(maxBytes: number): StringDescriptor { 153 167 const instance = new SharedString(maxBytes); 154 168 return Object.assign(instance, {
+1
src/shared/loadable.ts
··· 1 + /** A shared-memory value that can be read with `load()` and written with `store()`. */ 1 2 export interface Loadable<T> { 2 3 load(): T; 3 4 store(value: T): void;
+7
src/shared/mutex.ts
··· 3 3 const UNLOCKED = 0; 4 4 const LOCKED = 1; 5 5 6 + /** Disposes by calling `unlock()` on the associated mutex. */ 6 7 export class MutexGuard { 7 8 private readonly mutex: Mutex; 8 9 ··· 15 16 } 16 17 } 17 18 19 + /** An async mutual exclusion lock backed by SharedArrayBuffer. Works across threads. */ 18 20 export class Mutex { 19 21 static readonly byteSize = 4; 20 22 static readonly byteAlignment = 4; ··· 26 28 this.view = new Int32Array(buf, offset, 1); 27 29 } 28 30 31 + /** 32 + * Acquires the lock, waiting asynchronously if held by another thread. 33 + * @returns A disposable {@link MutexGuard} that releases the lock on dispose. 34 + */ 29 35 async lock(): Promise<MutexGuard> { 30 36 while (true) { 31 37 // Try to acquire: CAS from UNLOCKED to LOCKED ··· 40 46 } 41 47 } 42 48 49 + /** Releases the lock and wakes one waiting thread. */ 43 50 unlock(): void { 44 51 Atomics.store(this.view, 0, UNLOCKED); 45 52 Atomics.notify(this.view, 0, 1);
+13
src/shared/rwlock.ts
··· 3 3 const UNLOCKED = 0; 4 4 const WRITE_LOCKED = -1; 5 5 6 + /** Disposes by calling `readUnlock()` on the associated rwlock. */ 6 7 export class ReadGuard { 7 8 private readonly rwlock: RwLock; 8 9 ··· 15 16 } 16 17 } 17 18 19 + /** Disposes by calling `writeUnlock()` on the associated rwlock. */ 18 20 export class WriteGuard { 19 21 private readonly rwlock: RwLock; 20 22 ··· 27 29 } 28 30 } 29 31 32 + /** An async reader-writer lock backed by SharedArrayBuffer. Multiple readers or one exclusive writer. */ 30 33 export class RwLock { 31 34 static readonly byteSize = 4; 32 35 static readonly byteAlignment = 4; ··· 38 41 this.view = new Int32Array(buf, offset, 1); 39 42 } 40 43 44 + /** 45 + * Acquires a read lock. Multiple readers can hold the lock concurrently. 46 + * @returns A disposable {@link ReadGuard} that releases the read lock on dispose. 47 + */ 41 48 async readLock(): Promise<ReadGuard> { 42 49 while (true) { 43 50 const state = Atomics.load(this.view, 0); ··· 56 63 } 57 64 } 58 65 66 + /** Releases a read lock. Wakes a waiting writer if this was the last reader. */ 59 67 readUnlock(): void { 60 68 const prev = Atomics.sub(this.view, 0, 1); 61 69 if (prev === 1) { ··· 64 72 } 65 73 } 66 74 75 + /** 76 + * Acquires an exclusive write lock. Waits for all readers and writers to release. 77 + * @returns A disposable {@link WriteGuard} that releases the write lock on dispose. 78 + */ 67 79 async writeLock(): Promise<WriteGuard> { 68 80 while (true) { 69 81 // Can only acquire from unlocked state ··· 81 93 } 82 94 } 83 95 96 + /** Releases the write lock and wakes all waiting threads. */ 84 97 writeUnlock(): void { 85 98 Atomics.store(this.view, 0, UNLOCKED); 86 99 // Wake all waiters — both readers and writers may be waiting
+1
src/shared/shared-struct.ts
··· 21 21 22 22 const SHARED = Symbol.for('moroutine.shared'); 23 23 24 + /** A named group of shared-memory fields with bulk `load()`/`store()` access. */ 24 25 export class SharedStruct<T extends Record<string, unknown>> implements Loadable<FieldValues<T>> { 25 26 readonly fields: T; 26 27
+7
src/shared/shared.ts
··· 361 361 type ResolveTuple<T extends readonly unknown[]> = Tuple<ResolveTupleElements<T>>; 362 362 type ResolveTupleElements<T extends readonly unknown[]> = { [K in keyof T]: ResolveField<T[K]> } & Loadable<any>[]; 363 363 364 + /** 365 + * Allocates shared memory from a schema. Accepts descriptors, plain objects (structs), 366 + * arrays (tuples), or primitive values (shorthand). Compound schemas pack all fields 367 + * into a single SharedArrayBuffer. 368 + * @param schema - A descriptor, plain object, array, or primitive value defining the shape. 369 + * @returns A {@link Loadable} instance (or struct/tuple/lock) backed by shared memory. 370 + */ 364 371 export function shared<T extends Descriptor<any>>(schema: T): ReturnType<T>; 365 372 export function shared(schema: BytesDescriptor): Bytes; 366 373 export function shared(schema: StringDescriptor): SharedString;
+9
src/shared/string.ts
··· 4 4 const encoder = new TextEncoder(); 5 5 const decoder = new TextDecoder(); 6 6 7 + /** A variable-length shared UTF-8 string with a max byte capacity, backed by SharedArrayBuffer. */ 7 8 export class SharedString implements Loadable<string> { 8 9 readonly maxBytes: number; 9 10 private readonly lengthView: Uint32Array; ··· 20 21 this.dataView = new Uint8Array(buf, offset + 4, maxBytes); 21 22 } 22 23 24 + /** 25 + * Decodes and returns the stored string. 26 + * @returns The UTF-8 decoded string. 27 + */ 23 28 load(): string { 24 29 const len = this.lengthView[0]; 25 30 if (len === 0) return ''; 26 31 return decoder.decode(this.dataView.subarray(0, len)); 27 32 } 28 33 34 + /** 35 + * Encodes and stores a string. Throws if the encoded bytes exceed the max capacity. 36 + * @param value - The string to encode and store. 37 + */ 29 38 store(value: string): void { 30 39 if (value === '') { 31 40 this.lengthView[0] = 0;
+5
src/shared/tuple.ts
··· 4 4 5 5 const SHARED = Symbol.for('moroutine.shared'); 6 6 7 + /** A fixed-length ordered list of shared-memory values with bulk `load()`/`store()` access. */ 7 8 export class Tuple<T extends Loadable<any>[]> implements Loadable<TupleValues<T>> { 8 9 private readonly elements: T; 9 10 readonly length: number; ··· 13 14 this.length = elements.length; 14 15 } 15 16 17 + /** 18 + * Returns the Loadable at the given index. 19 + * @param index - Zero-based element index. Throws if out of bounds. 20 + */ 16 21 get(index: number): T[number] { 17 22 if (index < 0 || index >= this.length) { 18 23 throw new RangeError(`Index ${index} out of bounds for tuple of length ${this.length}`);
+1
src/task.ts
··· 1 1 import { runOnDedicated } from './dedicated-runner.ts'; 2 2 3 + /** A deferred computation that runs on a worker thread when awaited. */ 3 4 export class Task<T> { 4 5 readonly id: string; 5 6 readonly args: unknown[];
+5
src/transfer.ts
··· 8 8 readonly value: T; 9 9 } 10 10 11 + /** 12 + * Marks a value for zero-copy transfer via postMessage. The original becomes detached after sending. 13 + * @param value - The value to transfer (ArrayBuffer, TypedArray, MessagePort, or stream). 14 + * @returns The same value, typed unchanged for transparent use as a moroutine argument. 15 + */ 11 16 export function transfer<T>(value: T): T { 12 17 return { [TRANSFER]: true as const, value } as unknown as T; 13 18 }
+5
src/worker-pool.ts
··· 5 5 6 6 const workerEntryUrl = new URL('./worker-entry.ts', import.meta.url); 7 7 8 + /** 9 + * Creates a pool of worker threads that dispatch tasks with round-robin scheduling. 10 + * @param size - Number of worker threads in the pool. 11 + * @returns A disposable {@link Runner} for dispatching tasks. 12 + */ 8 13 export function workers(size: number): Runner { 9 14 const pool: Worker[] = []; 10 15 for (let i = 0; i < size; i++) {