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');