···11+---
22+"moroutine": minor
33+---
44+55+Auto-detect and transfer AbortSignal arguments to workers
66+77+AbortSignal args are automatically detected, marked transferable via `util.transferableAbortSignal()`, and transferred to the worker. Works with regular tasks, streaming moroutines, and dedicated workers.
+7
src/execute.ts
···11import type { Worker } from 'node:worker_threads';
22import { MessageChannel } from 'node:worker_threads';
33import type { MessagePort, Transferable } from 'node:worker_threads';
44+import { transferableAbortSignal } from 'node:util';
45import { freezeModule } from './registry.ts';
56import { serializeArg, deserializeArg } from './shared/reconstruct.ts';
67import { extractTransferables, collectTransferables } from './transfer.ts';
···9091}
91929293function prepareArg(arg: unknown): unknown {
9494+ // Auto-detect AbortSignal args — mark transferable and include in transfer list
9595+ if (arg instanceof AbortSignal) {
9696+ const signal = transferableAbortSignal(arg);
9797+ streamPortStack[streamPortStack.length - 1].push(signal as unknown as MessagePort);
9898+ return signal;
9999+ }
93100 // Auto-detect AsyncGenerator args — pipe via MessageChannel
94101 if (isAsyncGenerator(arg)) {
95102 const { port1, port2 } = new MessageChannel();
+43
test/abort-signal.test.ts
···11+import { describe, it } from 'node:test';
22+import assert from 'node:assert/strict';
33+import { workers } from 'moroutine';
44+import { waitForAbort, collectUntilAbort } from './fixtures/abort-signal.ts';
55+66+describe('AbortSignal transfer', () => {
77+ it('transfers AbortSignal to a worker task', async () => {
88+ const ac = new AbortController();
99+ using run = workers(1);
1010+ const promise = run(waitForAbort(ac.signal));
1111+ setTimeout(() => ac.abort(), 50);
1212+ const result = await promise;
1313+ assert.equal(result, 'aborted');
1414+ });
1515+1616+ it('transfers already-aborted signal', async () => {
1717+ const ac = new AbortController();
1818+ ac.abort();
1919+ using run = workers(1);
2020+ const result = await run(waitForAbort(ac.signal));
2121+ assert.equal(result, 'already-aborted');
2222+ });
2323+2424+ it('transfers AbortSignal to a streaming moroutine', async () => {
2525+ const ac = new AbortController();
2626+ using run = workers(1);
2727+ const results: number[] = [];
2828+ setTimeout(() => ac.abort(), 80);
2929+ for await (const n of run(collectUntilAbort(ac.signal))) {
3030+ results.push(n);
3131+ }
3232+ assert.ok(results.length >= 2, `expected at least 2 items, got ${results.length}`);
3333+ assert.equal(results[results.length - 1], -1, 'last item should be -1 sentinel');
3434+ });
3535+3636+ it('transfers AbortSignal on dedicated worker', async () => {
3737+ const ac = new AbortController();
3838+ const promise = waitForAbort(ac.signal);
3939+ setTimeout(() => ac.abort(), 50);
4040+ const result = await promise;
4141+ assert.equal(result, 'aborted');
4242+ });
4343+});
+23
test/fixtures/abort-signal.ts
···11+import { setTimeout } from 'node:timers/promises';
22+import { mo } from 'moroutine';
33+44+export const waitForAbort = mo(import.meta, (signal: AbortSignal): Promise<string> => {
55+ return new Promise((resolve) => {
66+ if (signal.aborted) {
77+ resolve('already-aborted');
88+ return;
99+ }
1010+ signal.addEventListener('abort', () => {
1111+ resolve('aborted');
1212+ });
1313+ });
1414+});
1515+1616+export const collectUntilAbort = mo(import.meta, async function* (signal: AbortSignal) {
1717+ let i = 0;
1818+ while (!signal.aborted) {
1919+ yield i++;
2020+ await setTimeout(10);
2121+ }
2222+ yield -1; // sentinel indicating abort was seen
2323+});