Documentation menu

Messages and transfers

On this page
Guides0.1.0-alpha.27

Choose whether each value is copied, moved, or shared before sending it between isolates. Ordinary messages are structured clones; objects do not keep a shared identity across heaps.

Transfer an ArrayBuffer

Save these files together:

transfer-task.ts
import { transfer } from "maligator:workers";
import type { TaskContext } from "maligator:workers";

export function reverse(context: TaskContext, buffer: ArrayBuffer) {
	context.throwIfCancelled();
	new Uint8Array(buffer).reverse();
	return transfer(buffer, [buffer]);
}
transfer.ts
import { createPool, createWorkerUrl } from "maligator:workers";

const tasks = createWorkerUrl<typeof import("./transfer-task.ts")>(
	"./transfer-task.ts",
	import.meta.url,
);
const pool = createPool(tasks, { size: 1 });

try {
	await pool.ready;
	const bytes = new Uint8Array([1, 2, 3]);
	const pending = pool.run("reverse", [bytes.buffer], {
		transfer: [bytes.buffer],
	});
	console.log(bytes.byteLength);
	const result = new Uint8Array(await pending);
	console.log(Array.from(result).join(","));
} finally {
	await pool.close();
}
shell
maligator run transfer.ts

The application prints 0, then 3,2,1. The input buffer detaches when run returns normally, before the task promise settles. The worker mutates its owned buffer and uses transfer to publish the result without copying that buffer back.

Creating a transfer result envelope does not detach a buffer. Publication commits the transfer. Validation failures, closed pools, already-aborted signals, and queue saturation occur before admission and leave the sender's transferables unchanged.

SharedArrayBuffer shares its backing memory when cloned and cannot appear in a transfer list. Coordinate shared access with Atomics. A transferred MessagePort moves endpoint ownership to the receiver.

Exchange messages with a worker

echo.ts
import { parentPort } from "maligator:workers";

if (parentPort === null) throw new Error("Run this module as a worker");
const port = parentPort;
port.onmessage = (event) => {
	port.postMessage(String(event.data));
};
port.start();
worker.ts
import { createWorkerUrl, Worker } from "maligator:workers";

const entry = createWorkerUrl("./echo.ts", import.meta.url);
const worker = new Worker<string, string>(entry);

try {
	const reply = new Promise<string>((resolve) => {
		worker.port.onmessage = (event) => resolve(event.data);
	});
	worker.port.start();
	await worker.ready;
	worker.port.postMessage("workers");
	console.log(await reply);
} finally {
	const exit = await worker.terminate();
	console.log(exit.reason);
}
shell
maligator run worker.ts

The application prints workers and then the terminal reason after shutdown. Install the receiving handler before sending. Setting onmessage starts delivery; call start() when using addEventListener. Messages on one endpoint retain admission order.

parentPort is null in the main isolate. In a worker it is the endpoint connected to the parent. workerData is the worker-owned snapshot of WorkerOptions.data; validate its shape because its type is unknown. Later changes to the caller's original object are not shared.

Set queue limits

Use WorkerOptions and MessageChannelOptions to bound pending messages and bytes. Defaults are 4096 messages, 64 MiB queued, and 16 MiB per message per endpoint. Native process-wide admission limits also apply: 65536 pending messages and 512 MiB across endpoints. A large local limit does not reserve host capacity.

A rejected message can throw DataCloneError for unsupported values or transfer lists, and QueueFullError for saturation. Keep a retry policy at the application layer and retain ownership until admission succeeds. Do not repeatedly post a detached buffer.

Close standalone channel endpoints when finished. Join workers with terminate() or closed; see cancellation and shutdown.

Maligator 0.1.0-alpha.27 · Experimental · Source 96c331078104
Edit this page · Compatibility

Search guides and API reference.

Browse the API · Troubleshooting