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: auto-transfer return values from workers

Worker return values that are transferable (ArrayBuffer, TypedArray,
streams, etc.) are automatically added to the postMessage transfer
list. No explicit transfer() wrapping needed on the return path since
the worker is done with the value.

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

+29 -6
+1 -1
src/transfer.ts
··· 25 25 return { args: processedArgs, transfer: transferList }; 26 26 } 27 27 28 - function collectTransferables(value: unknown, list: Transferable[]): void { 28 + export function collectTransferables(value: unknown, list: Transferable[]): void { 29 29 if (!(typeof value === 'object' && value !== null)) return; 30 30 31 31 // Directly transferable
+6 -4
src/worker-entry.ts
··· 1 1 import { parentPort } from 'node:worker_threads'; 2 2 import { registry } from './registry.ts'; 3 3 import { deserializeArg, serializeArg } from './sync/reconstruct.ts'; 4 - import { extractTransferables } from './transfer.ts'; 4 + import { collectTransferables } from './transfer.ts'; 5 + import type { Transferable } from 'node:worker_threads'; 5 6 6 7 const imported = new Set<string>(); 7 8 ··· 19 20 20 21 const deserializedArgs = args.map(deserializeArg); 21 22 const value = await fn(...deserializedArgs); 22 - const extracted = extractTransferables([value]); 23 - const returnValue = serializeArg(extracted.args[0]); 24 - parentPort!.postMessage({ callId, value: returnValue }, extracted.transfer); 23 + const returnValue = serializeArg(value); 24 + const transferList: Transferable[] = []; 25 + collectTransferables(value, transferList); 26 + parentPort!.postMessage({ callId, value: returnValue }, transferList); 25 27 } catch (err) { 26 28 const message = err instanceof Error ? err.message : String(err); 27 29 parentPort!.postMessage({ callId, error: message });
+9
test/fixtures/transfer.ts
··· 16 16 } 17 17 return sum; 18 18 }); 19 + 20 + export const makeBuffer = mo(import.meta, (size: number): ArrayBuffer => { 21 + const buf = new ArrayBuffer(size); 22 + const view = new Uint8Array(buf); 23 + for (let i = 0; i < size; i++) { 24 + view[i] = i + 1; 25 + } 26 + return buf; 27 + });
+13 -1
test/transfer.test.ts
··· 1 1 import { describe, it } from 'node:test'; 2 2 import assert from 'node:assert/strict'; 3 3 import { workerPool, transfer } from 'moroutine'; 4 - import { sumBuffer, sumUint8 } from './fixtures/transfer.ts'; 4 + import { sumBuffer, sumUint8, makeBuffer } from './fixtures/transfer.ts'; 5 5 6 6 describe('transfer', () => { 7 7 it('transfers an ArrayBuffer to a worker (zero-copy)', async () => { ··· 30 30 assert.equal(sum, 50); 31 31 // Underlying buffer should be detached after transfer 32 32 assert.equal(originalBuffer.byteLength, 0); 33 + } finally { 34 + pool[Symbol.dispose](); 35 + } 36 + }); 37 + 38 + it('auto-transfers return values from worker (zero-copy)', async () => { 39 + const pool = workerPool(1); 40 + try { 41 + const buf = await pool(makeBuffer(4)); 42 + const view = new Uint8Array(buf); 43 + assert.equal(buf.byteLength, 4); 44 + assert.deepEqual([...view], [1, 2, 3, 4]); 33 45 } finally { 34 46 pool[Symbol.dispose](); 35 47 }