diff --git a/src/mo.ts b/src/mo.ts index f90fd02..7026162 100644 --- a/src/mo.ts +++ b/src/mo.ts @@ -3,6 +3,12 @@ import { Task } from './task.ts'; const counters = new Map(); +/** + * Wraps a function to run on a worker thread. Must be called at module scope. + * @param importMeta - The `import.meta` of the calling module, used to identify the source file. + * @param fn - The function to offload to a worker thread. + * @returns A function that creates a {@link Task} when called. + */ export function mo(importMeta: ImportMeta, fn: (...args: A) => R): (...args: A) => Task { const url = importMeta.url; diff --git a/src/runner.ts b/src/runner.ts index 87009c7..edd2a49 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -2,6 +2,7 @@ import type { Task } from './task.ts'; type TaskResults[]> = { [K in keyof T]: T[K] extends Task ? R : never }; +/** A callable that dispatches tasks to a worker pool. Disposable via `using` or `[Symbol.dispose]()`. */ export type Runner = { (task: Task): Promise; []>(tasks: [...T]): Promise>; diff --git a/src/shared/bytes.ts b/src/shared/bytes.ts index bfec6f8..5504b7f 100644 --- a/src/shared/bytes.ts +++ b/src/shared/bytes.ts @@ -2,6 +2,7 @@ import type { Loadable } from './loadable.ts'; const SHARED = Symbol.for('moroutine.shared'); +/** A fixed-size shared byte buffer backed by SharedArrayBuffer. */ export class Bytes implements Loadable> { readonly size: number; readonly view: Uint8Array; @@ -15,10 +16,18 @@ export class Bytes implements Loadable> { this.view = new Uint8Array(buf, offset, size); } + /** + * Returns a readonly view of the buffer. No copy — mutations via `view` are visible. + * @returns A readonly typed array view into the shared buffer. + */ load(): Readonly { return this.view as Readonly; } + /** + * Writes data into the buffer. Must be exact length. + * @param value - A Uint8Array whose length must equal the buffer's size. + */ store(value: Uint8Array): void { if (value.length !== this.size) { throw new RangeError(`Expected Uint8Array of length ${this.size}, got ${value.length}`); diff --git a/src/shared/descriptors.ts b/src/shared/descriptors.ts index 5f0d965..4603a69 100644 --- a/src/shared/descriptors.ts +++ b/src/shared/descriptors.ts @@ -21,6 +21,7 @@ import { BoolAtomic } from './bool-atomic.ts'; import { Mutex } from './mutex.ts'; import { RwLock } from './rwlock.ts'; +/** A schema token describing a shared-memory type. Callable to create a standalone instance. */ export interface Descriptor { (): T; byteSize: number; @@ -37,6 +38,7 @@ function makeDescriptor( return Object.assign(factory, { byteSize, byteAlignment, _class }); } +/** Non-atomic shared primitives. Use inside a lock for thread safety. */ export const int8: Descriptor = makeDescriptor(() => new Int8(), Int8.byteSize, Int8.byteAlignment, Int8); export const uint8: Descriptor = makeDescriptor(() => new Uint8(), Uint8.byteSize, Uint8.byteAlignment, Uint8); export const int16: Descriptor = makeDescriptor(() => new Int16(), Int16.byteSize, Int16.byteAlignment, Int16); @@ -62,6 +64,7 @@ export const uint64: Descriptor = makeDescriptor( ); export const bool: Descriptor = makeDescriptor(() => new Bool(), Bool.byteSize, Bool.byteAlignment, Bool); +/** Atomic shared primitives. Thread-safe without a lock. */ export const int8atomic: Descriptor = makeDescriptor( () => new Int8Atomic(), Int8Atomic.byteSize, @@ -117,6 +120,7 @@ export const boolatomic: Descriptor = makeDescriptor( BoolAtomic, ); +/** Shared-memory locks. */ export const mutex: Descriptor = makeDescriptor(() => new Mutex(), Mutex.byteSize, Mutex.byteAlignment, Mutex); export const rwlock: Descriptor = makeDescriptor( () => new RwLock(), @@ -132,6 +136,11 @@ export interface BytesDescriptor extends Bytes { _size: number; } +/** + * Creates a fixed-size shared byte buffer. Acts as both a standalone factory and a schema descriptor. + * @param size - The buffer capacity in bytes. + * @returns A {@link Bytes} instance with descriptor metadata for use with `shared()`. + */ export function bytes(size: number): BytesDescriptor { const instance = new Bytes(size); return Object.assign(instance, { @@ -149,6 +158,11 @@ export interface StringDescriptor extends SharedString { _maxBytes: number; } +/** + * Creates a variable-length shared UTF-8 string with a max byte length. Acts as both a standalone factory and a schema descriptor. + * @param maxBytes - Maximum number of bytes for the encoded string. + * @returns A {@link SharedString} instance with descriptor metadata for use with `shared()`. + */ export function string(maxBytes: number): StringDescriptor { const instance = new SharedString(maxBytes); return Object.assign(instance, { diff --git a/src/shared/loadable.ts b/src/shared/loadable.ts index fc4cf8f..6a6496d 100644 --- a/src/shared/loadable.ts +++ b/src/shared/loadable.ts @@ -1,3 +1,4 @@ +/** A shared-memory value that can be read with `load()` and written with `store()`. */ export interface Loadable { load(): T; store(value: T): void; diff --git a/src/shared/mutex.ts b/src/shared/mutex.ts index 0c0cd14..ea69f74 100644 --- a/src/shared/mutex.ts +++ b/src/shared/mutex.ts @@ -3,6 +3,7 @@ import { registerSync } from './reconstruct.ts'; const UNLOCKED = 0; const LOCKED = 1; +/** Disposes by calling `unlock()` on the associated mutex. */ export class MutexGuard { private readonly mutex: Mutex; @@ -15,6 +16,7 @@ export class MutexGuard { } } +/** An async mutual exclusion lock backed by SharedArrayBuffer. Works across threads. */ export class Mutex { static readonly byteSize = 4; static readonly byteAlignment = 4; @@ -26,6 +28,10 @@ export class Mutex { this.view = new Int32Array(buf, offset, 1); } + /** + * Acquires the lock, waiting asynchronously if held by another thread. + * @returns A disposable {@link MutexGuard} that releases the lock on dispose. + */ async lock(): Promise { while (true) { // Try to acquire: CAS from UNLOCKED to LOCKED @@ -40,6 +46,7 @@ export class Mutex { } } + /** Releases the lock and wakes one waiting thread. */ unlock(): void { Atomics.store(this.view, 0, UNLOCKED); Atomics.notify(this.view, 0, 1); diff --git a/src/shared/rwlock.ts b/src/shared/rwlock.ts index 848fead..88df8d1 100644 --- a/src/shared/rwlock.ts +++ b/src/shared/rwlock.ts @@ -3,6 +3,7 @@ import { registerSync } from './reconstruct.ts'; const UNLOCKED = 0; const WRITE_LOCKED = -1; +/** Disposes by calling `readUnlock()` on the associated rwlock. */ export class ReadGuard { private readonly rwlock: RwLock; @@ -15,6 +16,7 @@ export class ReadGuard { } } +/** Disposes by calling `writeUnlock()` on the associated rwlock. */ export class WriteGuard { private readonly rwlock: RwLock; @@ -27,6 +29,7 @@ export class WriteGuard { } } +/** An async reader-writer lock backed by SharedArrayBuffer. Multiple readers or one exclusive writer. */ export class RwLock { static readonly byteSize = 4; static readonly byteAlignment = 4; @@ -38,6 +41,10 @@ export class RwLock { this.view = new Int32Array(buf, offset, 1); } + /** + * Acquires a read lock. Multiple readers can hold the lock concurrently. + * @returns A disposable {@link ReadGuard} that releases the read lock on dispose. + */ async readLock(): Promise { while (true) { const state = Atomics.load(this.view, 0); @@ -56,6 +63,7 @@ export class RwLock { } } + /** Releases a read lock. Wakes a waiting writer if this was the last reader. */ readUnlock(): void { const prev = Atomics.sub(this.view, 0, 1); if (prev === 1) { @@ -64,6 +72,10 @@ export class RwLock { } } + /** + * Acquires an exclusive write lock. Waits for all readers and writers to release. + * @returns A disposable {@link WriteGuard} that releases the write lock on dispose. + */ async writeLock(): Promise { while (true) { // Can only acquire from unlocked state @@ -81,6 +93,7 @@ export class RwLock { } } + /** Releases the write lock and wakes all waiting threads. */ writeUnlock(): void { Atomics.store(this.view, 0, UNLOCKED); // Wake all waiters — both readers and writers may be waiting diff --git a/src/shared/shared-struct.ts b/src/shared/shared-struct.ts index 2c72c7e..3793f00 100644 --- a/src/shared/shared-struct.ts +++ b/src/shared/shared-struct.ts @@ -21,6 +21,7 @@ function isLoadable(value: unknown): value is Loadable { const SHARED = Symbol.for('moroutine.shared'); +/** A named group of shared-memory fields with bulk `load()`/`store()` access. */ export class SharedStruct> implements Loadable> { readonly fields: T; diff --git a/src/shared/shared.ts b/src/shared/shared.ts index bb32c24..cec97a0 100644 --- a/src/shared/shared.ts +++ b/src/shared/shared.ts @@ -361,6 +361,13 @@ type ResolveStruct> = SharedStruct<{ [K in key type ResolveTuple = Tuple>; type ResolveTupleElements = { [K in keyof T]: ResolveField } & Loadable[]; +/** + * Allocates shared memory from a schema. Accepts descriptors, plain objects (structs), + * arrays (tuples), or primitive values (shorthand). Compound schemas pack all fields + * into a single SharedArrayBuffer. + * @param schema - A descriptor, plain object, array, or primitive value defining the shape. + * @returns A {@link Loadable} instance (or struct/tuple/lock) backed by shared memory. + */ export function shared>(schema: T): ReturnType; export function shared(schema: BytesDescriptor): Bytes; export function shared(schema: StringDescriptor): SharedString; diff --git a/src/shared/string.ts b/src/shared/string.ts index b5c5cd6..da4e745 100644 --- a/src/shared/string.ts +++ b/src/shared/string.ts @@ -4,6 +4,7 @@ const SHARED = Symbol.for('moroutine.shared'); const encoder = new TextEncoder(); const decoder = new TextDecoder(); +/** A variable-length shared UTF-8 string with a max byte capacity, backed by SharedArrayBuffer. */ export class SharedString implements Loadable { readonly maxBytes: number; private readonly lengthView: Uint32Array; @@ -20,12 +21,20 @@ export class SharedString implements Loadable { this.dataView = new Uint8Array(buf, offset + 4, maxBytes); } + /** + * Decodes and returns the stored string. + * @returns The UTF-8 decoded string. + */ load(): string { const len = this.lengthView[0]; if (len === 0) return ''; return decoder.decode(this.dataView.subarray(0, len)); } + /** + * Encodes and stores a string. Throws if the encoded bytes exceed the max capacity. + * @param value - The string to encode and store. + */ store(value: string): void { if (value === '') { this.lengthView[0] = 0; diff --git a/src/shared/tuple.ts b/src/shared/tuple.ts index acda0fc..52b8630 100644 --- a/src/shared/tuple.ts +++ b/src/shared/tuple.ts @@ -4,6 +4,7 @@ type TupleValues[]> = { [K in keyof T]: T[K] extends Loa const SHARED = Symbol.for('moroutine.shared'); +/** A fixed-length ordered list of shared-memory values with bulk `load()`/`store()` access. */ export class Tuple[]> implements Loadable> { private readonly elements: T; readonly length: number; @@ -13,6 +14,10 @@ export class Tuple[]> implements Loadable this.length = elements.length; } + /** + * Returns the Loadable at the given index. + * @param index - Zero-based element index. Throws if out of bounds. + */ get(index: number): T[number] { if (index < 0 || index >= this.length) { throw new RangeError(`Index ${index} out of bounds for tuple of length ${this.length}`); diff --git a/src/task.ts b/src/task.ts index 6d6ef1c..44238bd 100644 --- a/src/task.ts +++ b/src/task.ts @@ -1,5 +1,6 @@ import { runOnDedicated } from './dedicated-runner.ts'; +/** A deferred computation that runs on a worker thread when awaited. */ export class Task { readonly id: string; readonly args: unknown[]; diff --git a/src/transfer.ts b/src/transfer.ts index 6e6a3fd..e9d55d9 100644 --- a/src/transfer.ts +++ b/src/transfer.ts @@ -8,6 +8,11 @@ export interface Transferred { readonly value: T; } +/** + * Marks a value for zero-copy transfer via postMessage. The original becomes detached after sending. + * @param value - The value to transfer (ArrayBuffer, TypedArray, MessagePort, or stream). + * @returns The same value, typed unchanged for transparent use as a moroutine argument. + */ export function transfer(value: T): T { return { [TRANSFER]: true as const, value } as unknown as T; } diff --git a/src/worker-pool.ts b/src/worker-pool.ts index 1c67a14..92a321b 100644 --- a/src/worker-pool.ts +++ b/src/worker-pool.ts @@ -5,6 +5,11 @@ import type { Runner } from './runner.ts'; const workerEntryUrl = new URL('./worker-entry.ts', import.meta.url); +/** + * Creates a pool of worker threads that dispatch tasks with round-robin scheduling. + * @param size - Number of worker threads in the pool. + * @returns A disposable {@link Runner} for dispatching tasks. + */ export function workers(size: number): Runner { const pool: Worker[] = []; for (let i = 0; i < size; i++) {