Messaging

js/realm/realm-messaging.md

Every realm has a parent-side port exposed as realm.port. Reactor-pooled, process, and remote transports all share the same addEventListener / postMessage / start / close interface.

Parent-side port

Listen for messages from the child and send messages to it through realm.port:

import { Realm } from 'fino:realm';

const realm = new Realm({ entry: './worker.ts' });

realm.port.addEventListener('message', (ev) => {
  console.log('from child:', ev.data);
});
realm.port.start();  // required before messages are delivered
realm.port.postMessage({ type: 'ping' });

await realm.run();

Calling start() is required to begin receiving messages. As a shortcut, assigning realm.port.onmessage starts the port automatically:

realm.port.onmessage = (ev) => console.log(ev.data);

Child-side port — reactor-pooled realms

In a reactor-pooled child realm, the child accesses its port from fino:realm/self:

// child entry
import { port } from 'fino:realm/self';

port?.addEventListener('message', (ev) => {
  port?.postMessage({ echo: ev.data });
});
port?.start();

port is undefined in root and process realms. Always guard with port?. unless the module only runs as a reactor-pooled child.

Child-side port — process realms

Process realms access their parent-side channel through globalThis.realmPort, which is injected by the runtime bootstrap:

// child entry (process realm)
globalThis.realmPort.addEventListener('message', (ev) => {
  globalThis.realmPort.postMessage({ got: ev.data });
});
globalThis.realmPort.start();

This is wired up automatically. The child does not need to import anything to access it.

Creating new channels

fino:realm/messaging re-exports the standard MessageChannel, MessagePort, and MessageEvent types. Use MessageChannel when you need a fresh pair of entangled ports:

import { MessageChannel } from 'fino:realm/messaging';

const { port1, port2 } = new MessageChannel();
// transfer port1 to the child via the existing realm port
realm.port.postMessage('here is your extra channel', [port1]);

Transfer rules

What can be included in a postMessage transfer list depends on the realm type:

Realm type ArrayBuffer transfer MessagePort transfer
Reactor-pooled Yes Yes
Process Yes No — throws TypeError
Remote No stable contract No stable contract

A transferred ArrayBuffer is detached on the sending side after postMessage returns. A transferred MessagePort is neutered on the sender and a new entangled port is installed in the receiver's message event's ports array.

Values that the serializer cannot clone — functions, symbols, WeakMap, WeakSet, and class instances with non-plain prototypes — cause postMessage to throw a DataCloneError synchronously. This applies to all realm transport types.

BroadcastChannel

BroadcastChannel is a runtime global (no import required) that provides one-to-many pub/sub across realms by channel name:

// parent realm
const bc = new BroadcastChannel('cache-updates');
bc.postMessage({ key: 'users:42', ts: Date.now() });
bc.close();

// any other realm
const bc = new BroadcastChannel('cache-updates');
bc.onmessage = (ev) => {
  console.log('invalidate', ev.data.key);
};

Delivery is asynchronous. The sender does not receive its own messages. BroadcastChannel works across reactor-pooled and process realms — the runtime broadcast registry fans out serialized bytes to every subscriber on the same channel name.

BroadcastChannel does not accept transfer lists. Passing a function or symbol in postMessage throws a DataCloneError synchronously. Messages that cannot be deserialized on the receiver arrive as messageerror events with data === null.

Close the channel when done to release the subscription:

bc.close();
// or use explicit resource management:
using bc = new BroadcastChannel('updates');