···3344const counters = new Map<string, number>();
5566+/**
77+ * Wraps a function to run on a worker thread. Must be called at module scope.
88+ * @param importMeta - The `import.meta` of the calling module, used to identify the source file.
99+ * @param fn - The function to offload to a worker thread.
1010+ * @returns A function that creates a {@link Task} when called.
1111+ */
612export function mo<A extends unknown[], R>(importMeta: ImportMeta, fn: (...args: A) => R): (...args: A) => Task<R> {
713 const url = importMeta.url;
814
+1
src/runner.ts
···2233type TaskResults<T extends Task<any>[]> = { [K in keyof T]: T[K] extends Task<infer R> ? R : never };
4455+/** A callable that dispatches tasks to a worker pool. Disposable via `using` or `[Symbol.dispose]()`. */
56export type Runner = {
67 <T>(task: Task<T>): Promise<T>;
78 <T extends Task<any>[]>(tasks: [...T]): Promise<TaskResults<T>>;
+9
src/shared/bytes.ts
···2233const SHARED = Symbol.for('moroutine.shared');
4455+/** A fixed-size shared byte buffer backed by SharedArrayBuffer. */
56export class Bytes implements Loadable<Readonly<Uint8Array>> {
67 readonly size: number;
78 readonly view: Uint8Array;
···1516 this.view = new Uint8Array(buf, offset, size);
1617 }
17181919+ /**
2020+ * Returns a readonly view of the buffer. No copy — mutations via `view` are visible.
2121+ * @returns A readonly typed array view into the shared buffer.
2222+ */
1823 load(): Readonly<Uint8Array> {
1924 return this.view as Readonly<Uint8Array>;
2025 }
21262727+ /**
2828+ * Writes data into the buffer. Must be exact length.
2929+ * @param value - A Uint8Array whose length must equal the buffer's size.
3030+ */
2231 store(value: Uint8Array): void {
2332 if (value.length !== this.size) {
2433 throw new RangeError(`Expected Uint8Array of length ${this.size}, got ${value.length}`);
+14
src/shared/descriptors.ts
···2121import { Mutex } from './mutex.ts';
2222import { RwLock } from './rwlock.ts';
23232424+/** A schema token describing a shared-memory type. Callable to create a standalone instance. */
2425export interface Descriptor<T> {
2526 (): T;
2627 byteSize: number;
···3738 return Object.assign(factory, { byteSize, byteAlignment, _class });
3839}
39404141+/** Non-atomic shared primitives. Use inside a lock for thread safety. */
4042export const int8: Descriptor<Int8> = makeDescriptor(() => new Int8(), Int8.byteSize, Int8.byteAlignment, Int8);
4143export const uint8: Descriptor<Uint8> = makeDescriptor(() => new Uint8(), Uint8.byteSize, Uint8.byteAlignment, Uint8);
4244export const int16: Descriptor<Int16> = makeDescriptor(() => new Int16(), Int16.byteSize, Int16.byteAlignment, Int16);
···6264);
6365export const bool: Descriptor<Bool> = makeDescriptor(() => new Bool(), Bool.byteSize, Bool.byteAlignment, Bool);
64666767+/** Atomic shared primitives. Thread-safe without a lock. */
6568export const int8atomic: Descriptor<Int8Atomic> = makeDescriptor(
6669 () => new Int8Atomic(),
6770 Int8Atomic.byteSize,
···117120 BoolAtomic,
118121);
119122123123+/** Shared-memory locks. */
120124export const mutex: Descriptor<Mutex> = makeDescriptor(() => new Mutex(), Mutex.byteSize, Mutex.byteAlignment, Mutex);
121125export const rwlock: Descriptor<RwLock> = makeDescriptor(
122126 () => new RwLock(),
···132136 _size: number;
133137}
134138139139+/**
140140+ * Creates a fixed-size shared byte buffer. Acts as both a standalone factory and a schema descriptor.
141141+ * @param size - The buffer capacity in bytes.
142142+ * @returns A {@link Bytes} instance with descriptor metadata for use with `shared()`.
143143+ */
135144export function bytes(size: number): BytesDescriptor {
136145 const instance = new Bytes(size);
137146 return Object.assign(instance, {
···149158 _maxBytes: number;
150159}
151160161161+/**
162162+ * Creates a variable-length shared UTF-8 string with a max byte length. Acts as both a standalone factory and a schema descriptor.
163163+ * @param maxBytes - Maximum number of bytes for the encoded string.
164164+ * @returns A {@link SharedString} instance with descriptor metadata for use with `shared()`.
165165+ */
152166export function string(maxBytes: number): StringDescriptor {
153167 const instance = new SharedString(maxBytes);
154168 return Object.assign(instance, {
+1
src/shared/loadable.ts
···11+/** A shared-memory value that can be read with `load()` and written with `store()`. */
12export interface Loadable<T> {
23 load(): T;
34 store(value: T): void;
+7
src/shared/mutex.ts
···33const UNLOCKED = 0;
44const LOCKED = 1;
5566+/** Disposes by calling `unlock()` on the associated mutex. */
67export class MutexGuard {
78 private readonly mutex: Mutex;
89···1516 }
1617}
17181919+/** An async mutual exclusion lock backed by SharedArrayBuffer. Works across threads. */
1820export class Mutex {
1921 static readonly byteSize = 4;
2022 static readonly byteAlignment = 4;
···2628 this.view = new Int32Array(buf, offset, 1);
2729 }
28303131+ /**
3232+ * Acquires the lock, waiting asynchronously if held by another thread.
3333+ * @returns A disposable {@link MutexGuard} that releases the lock on dispose.
3434+ */
2935 async lock(): Promise<MutexGuard> {
3036 while (true) {
3137 // Try to acquire: CAS from UNLOCKED to LOCKED
···4046 }
4147 }
42484949+ /** Releases the lock and wakes one waiting thread. */
4350 unlock(): void {
4451 Atomics.store(this.view, 0, UNLOCKED);
4552 Atomics.notify(this.view, 0, 1);
+13
src/shared/rwlock.ts
···33const UNLOCKED = 0;
44const WRITE_LOCKED = -1;
5566+/** Disposes by calling `readUnlock()` on the associated rwlock. */
67export class ReadGuard {
78 private readonly rwlock: RwLock;
89···1516 }
1617}
17181919+/** Disposes by calling `writeUnlock()` on the associated rwlock. */
1820export class WriteGuard {
1921 private readonly rwlock: RwLock;
2022···2729 }
2830}
29313232+/** An async reader-writer lock backed by SharedArrayBuffer. Multiple readers or one exclusive writer. */
3033export class RwLock {
3134 static readonly byteSize = 4;
3235 static readonly byteAlignment = 4;
···3841 this.view = new Int32Array(buf, offset, 1);
3942 }
40434444+ /**
4545+ * Acquires a read lock. Multiple readers can hold the lock concurrently.
4646+ * @returns A disposable {@link ReadGuard} that releases the read lock on dispose.
4747+ */
4148 async readLock(): Promise<ReadGuard> {
4249 while (true) {
4350 const state = Atomics.load(this.view, 0);
···5663 }
5764 }
58656666+ /** Releases a read lock. Wakes a waiting writer if this was the last reader. */
5967 readUnlock(): void {
6068 const prev = Atomics.sub(this.view, 0, 1);
6169 if (prev === 1) {
···6472 }
6573 }
66747575+ /**
7676+ * Acquires an exclusive write lock. Waits for all readers and writers to release.
7777+ * @returns A disposable {@link WriteGuard} that releases the write lock on dispose.
7878+ */
6779 async writeLock(): Promise<WriteGuard> {
6880 while (true) {
6981 // Can only acquire from unlocked state
···8193 }
8294 }
83959696+ /** Releases the write lock and wakes all waiting threads. */
8497 writeUnlock(): void {
8598 Atomics.store(this.view, 0, UNLOCKED);
8699 // Wake all waiters — both readers and writers may be waiting
+1
src/shared/shared-struct.ts
···21212222const SHARED = Symbol.for('moroutine.shared');
23232424+/** A named group of shared-memory fields with bulk `load()`/`store()` access. */
2425export class SharedStruct<T extends Record<string, unknown>> implements Loadable<FieldValues<T>> {
2526 readonly fields: T;
2627
+7
src/shared/shared.ts
···361361type ResolveTuple<T extends readonly unknown[]> = Tuple<ResolveTupleElements<T>>;
362362type ResolveTupleElements<T extends readonly unknown[]> = { [K in keyof T]: ResolveField<T[K]> } & Loadable<any>[];
363363364364+/**
365365+ * Allocates shared memory from a schema. Accepts descriptors, plain objects (structs),
366366+ * arrays (tuples), or primitive values (shorthand). Compound schemas pack all fields
367367+ * into a single SharedArrayBuffer.
368368+ * @param schema - A descriptor, plain object, array, or primitive value defining the shape.
369369+ * @returns A {@link Loadable} instance (or struct/tuple/lock) backed by shared memory.
370370+ */
364371export function shared<T extends Descriptor<any>>(schema: T): ReturnType<T>;
365372export function shared(schema: BytesDescriptor): Bytes;
366373export function shared(schema: StringDescriptor): SharedString;
+9
src/shared/string.ts
···44const encoder = new TextEncoder();
55const decoder = new TextDecoder();
6677+/** A variable-length shared UTF-8 string with a max byte capacity, backed by SharedArrayBuffer. */
78export class SharedString implements Loadable<string> {
89 readonly maxBytes: number;
910 private readonly lengthView: Uint32Array;
···2021 this.dataView = new Uint8Array(buf, offset + 4, maxBytes);
2122 }
22232424+ /**
2525+ * Decodes and returns the stored string.
2626+ * @returns The UTF-8 decoded string.
2727+ */
2328 load(): string {
2429 const len = this.lengthView[0];
2530 if (len === 0) return '';
2631 return decoder.decode(this.dataView.subarray(0, len));
2732 }
28333434+ /**
3535+ * Encodes and stores a string. Throws if the encoded bytes exceed the max capacity.
3636+ * @param value - The string to encode and store.
3737+ */
2938 store(value: string): void {
3039 if (value === '') {
3140 this.lengthView[0] = 0;
+5
src/shared/tuple.ts
···4455const SHARED = Symbol.for('moroutine.shared');
6677+/** A fixed-length ordered list of shared-memory values with bulk `load()`/`store()` access. */
78export class Tuple<T extends Loadable<any>[]> implements Loadable<TupleValues<T>> {
89 private readonly elements: T;
910 readonly length: number;
···1314 this.length = elements.length;
1415 }
15161717+ /**
1818+ * Returns the Loadable at the given index.
1919+ * @param index - Zero-based element index. Throws if out of bounds.
2020+ */
1621 get(index: number): T[number] {
1722 if (index < 0 || index >= this.length) {
1823 throw new RangeError(`Index ${index} out of bounds for tuple of length ${this.length}`);
+1
src/task.ts
···11import { runOnDedicated } from './dedicated-runner.ts';
2233+/** A deferred computation that runs on a worker thread when awaited. */
34export class Task<T> {
45 readonly id: string;
56 readonly args: unknown[];
+5
src/transfer.ts
···88 readonly value: T;
99}
10101111+/**
1212+ * Marks a value for zero-copy transfer via postMessage. The original becomes detached after sending.
1313+ * @param value - The value to transfer (ArrayBuffer, TypedArray, MessagePort, or stream).
1414+ * @returns The same value, typed unchanged for transparent use as a moroutine argument.
1515+ */
1116export function transfer<T>(value: T): T {
1217 return { [TRANSFER]: true as const, value } as unknown as T;
1318}
+5
src/worker-pool.ts
···5566const workerEntryUrl = new URL('./worker-entry.ts', import.meta.url);
7788+/**
99+ * Creates a pool of worker threads that dispatch tasks with round-robin scheduling.
1010+ * @param size - Number of worker threads in the pool.
1111+ * @returns A disposable {@link Runner} for dispatching tasks.
1212+ */
813export function workers(size: number): Runner {
914 const pool: Worker[] = [];
1015 for (let i = 0; i < size; i++) {