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.

feat: add isTask() type guard for narrowing tasks to a moroutine

- Moroutines returned by mo() expose a readonly .id
- isTask(mo, task) narrows task to Task<T, A> inferred from the
moroutine's return type; useful when a pool handles tasks from
multiple moroutines and you want to recover the specific shape
- Task<T, A>.args is now typed as A (previously unknown[]), so
narrowing propagates through task.args — backward compatible
for Task<T> where A defaults to unknown[]

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

+117 -8
+16
.changeset/is-task.md
··· 1 + --- 2 + 'moroutine': minor 3 + --- 4 + 5 + Add `isTask()` type guard for narrowing tasks to a specific moroutine 6 + 7 + ```ts 8 + if (isTask(isPrime, task)) { 9 + // task: Task<boolean, [n: number]> 10 + const [n] = task.args; 11 + } 12 + ``` 13 + 14 + - Moroutines returned by `mo()` now expose a readonly `id` for stable identity 15 + - `isTask(mo, task)` returns `true` when `task` was produced by `mo`, and narrows the task to the descriptor type produced by that moroutine 16 + - `Task<T, A>.args` is now typed as `A` (previously `unknown[]`) so narrowing propagates to argument access — unchanged for `Task<T>` without a specialized arg tuple
+1
src/index.ts
··· 8 8 export { transfer } from './transfer.ts'; 9 9 export { assign } from './assign.ts'; 10 10 export { inert } from './inert.ts'; 11 + export { isTask } from './is-task.ts'; 11 12 export { map } from './map.ts'; 12 13 export type { MapOptions } from './map.ts'; 13 14 export { roundRobin, leastBusy } from './balancers.ts';
+27
src/is-task.ts
··· 1 + import type { Task } from './runner.ts'; 2 + 3 + type TaskOf<M> = M extends { (...args: any[]): infer R } 4 + ? R extends Task<infer T, infer A> 5 + ? Task<T, A> 6 + : never 7 + : never; 8 + 9 + /** 10 + * Narrows a task to the descriptor type produced by a specific moroutine. 11 + * 12 + * ```ts 13 + * if (isTask(isPrime, task)) { 14 + * // task: Task<boolean, [n: number]> 15 + * } 16 + * ``` 17 + * 18 + * @param moroutine - A function returned by {@link mo}. 19 + * @param task - A task to test. 20 + * @returns `true` if `task` was created by `moroutine`. 21 + */ 22 + export function isTask<M extends { readonly id: string; (...args: any[]): Task<any, any> }>( 23 + moroutine: M, 24 + task: Task, 25 + ): task is TaskOf<M> { 26 + return task.id === moroutine.id; 27 + }
+9 -4
src/mo.ts
··· 20 20 type Moroutine<A extends unknown[], R> = { 21 21 (...args: A): Task<Awaited<R>, A> & PromiseLike<Awaited<R>>; 22 22 (...args: TaskableArgs<A>): Task<Awaited<R>, A> & PromiseLike<Awaited<R>>; 23 + /** Stable id tying tasks back to this moroutine. Used by {@link isTask}. */ 24 + readonly id: string; 23 25 }; 24 26 25 27 type AsyncIterableMoroutine<A extends unknown[], Y> = { 26 28 (...args: A): Task<AsyncIterable<Y>, A> & AsyncIterable<Y>; 27 29 (...args: TaskableArgs<A>): Task<AsyncIterable<Y>, A> & AsyncIterable<Y>; 30 + /** Stable id tying tasks back to this moroutine. Used by {@link isTask}. */ 31 + readonly id: string; 28 32 }; 29 33 30 34 type IsNever<T> = [T] extends [never] ? true : false; ··· 55 59 56 60 registry.set(id, fn); 57 61 58 - if (isAsyncGeneratorFunction(fn)) { 59 - return ((...args: unknown[]) => new AsyncIterableTask(id, args)) as any; 60 - } 61 - return ((...args: unknown[]) => new PromiseLikeTask<R>(id, args)) as any; 62 + const factory = isAsyncGeneratorFunction(fn) 63 + ? (...args: unknown[]) => new AsyncIterableTask(id, args) 64 + : (...args: unknown[]) => new PromiseLikeTask<R>(id, args); 65 + Object.defineProperty(factory, 'id', { value: id, enumerable: true }); 66 + return factory as unknown as MoReturn<A, R>; 62 67 }
+1 -4
src/runner.ts
··· 2 2 import type { ChannelOptions } from './channel.ts'; 3 3 4 4 declare const resultBrand: unique symbol; 5 - declare const argsBrand: unique symbol; 6 5 7 6 /** An inert task descriptor. Carries the result type `T` and argument types `A` 8 7 * at the type level, but is not itself a `PromiseLike` or `AsyncIterable`. ··· 10 9 export type Task<T = unknown, A extends unknown[] = unknown[]> = { 11 10 readonly uid: number; 12 11 readonly id: string; 13 - readonly args: unknown[]; 12 + readonly args: A; 14 13 worker?: WorkerHandle; 15 14 /** @internal Type brand for result type inference. Not present at runtime. */ 16 15 readonly [resultBrand]?: T; 17 - /** @internal Type brand for argument type inference. Not present at runtime. */ 18 - readonly [argsBrand]?: A; 19 16 }; 20 17 21 18 /** Resolves the dispatch return type: `AsyncIterable<U>` for streaming tasks, `Promise<T>` otherwise. */
+13
test/fixtures/is-task.ts
··· 1 + import { mo } from 'moroutine'; 2 + 3 + export const checkPrime = mo(import.meta, (n: number): boolean => { 4 + if (n < 2) return false; 5 + for (let i = 2; i * i <= n; i++) if (n % i === 0) return false; 6 + return true; 7 + }); 8 + 9 + export const upper = mo(import.meta, (s: string): string => s.toUpperCase()); 10 + 11 + export const countUp = mo(import.meta, async function* (n: number) { 12 + for (let i = 0; i < n; i++) yield i; 13 + });
+50
test/is-task.test.ts
··· 1 + import { describe, it } from 'node:test'; 2 + import assert from 'node:assert/strict'; 3 + import { inert, isTask, workers } from 'moroutine'; 4 + import type { Task } from 'moroutine'; 5 + import { checkPrime, upper, countUp } from './fixtures/is-task.ts'; 6 + 7 + describe('isTask', () => { 8 + it('exposes a stable id on the moroutine', () => { 9 + assert.equal(typeof checkPrime.id, 'string'); 10 + assert.ok(checkPrime.id.length > 0); 11 + assert.notEqual(checkPrime.id, upper.id); 12 + }); 13 + 14 + it('identifies tasks produced by a moroutine', () => { 15 + const t = inert(checkPrime(7)); 16 + assert.ok(isTask(checkPrime, t)); 17 + assert.ok(!isTask(upper, t)); 18 + }); 19 + 20 + it('works across value and streaming moroutines', () => { 21 + const v = inert(checkPrime(7)); 22 + const s = inert(countUp(3)); 23 + assert.ok(isTask(checkPrime, v)); 24 + assert.ok(!isTask(countUp, v)); 25 + assert.ok(isTask(countUp, s)); 26 + assert.ok(!isTask(checkPrime, s)); 27 + }); 28 + 29 + it('narrows task args for downstream dispatch', async () => { 30 + using run = workers(1); 31 + const tasks: Array<Task<boolean, [n: number]> | Task<string, [s: string]>> = [ 32 + inert(checkPrime(5)), 33 + inert(upper('hi')), 34 + ]; 35 + const results: Array<boolean | string> = []; 36 + for (const t of tasks) { 37 + if (isTask(checkPrime, t)) { 38 + // t is Task<boolean, [n: number]> — args typed as [n: number] 39 + const [n] = t.args; 40 + assert.equal(typeof n, 'number'); 41 + results.push(await run(t)); 42 + } else { 43 + const [s] = t.args; 44 + assert.equal(typeof s, 'string'); 45 + results.push(await run(t)); 46 + } 47 + } 48 + assert.deepEqual(results, [true, 'HI']); 49 + }); 50 + });