js/globals/messaging
js/globals/messaging.ts
MessageEvent, MessagePort, and MessageChannel globals.
HTML channel messaging model: https://html.spec.whatwg.org/multipage/web-messaging.html#channel-messaging
Message payloads use the structured-clone subset documented on
structuredClone() in js/globals/encoding.ts. That subset is intentionally
narrower than the complete HTML structured clone algorithm: functions,
symbols, weak collections, custom prototypes, streams, and direct
MessagePort values without transfer reject with DataCloneError.
IntraPort transport: same-Isolate Realms exchange messages via direct JS object references + structuredClone. postMessage clones the value and pushes it to the partner port's queue. _flushPorts() dispatches all queued messages on started ports; called from driveLoop between tick() and drainMicrotasks().
MessagePort transfer: a MessagePort can be transferred via postMessage. For same-Isolate transfers a fresh receiver-side port replaces matching MessagePort references in the cloned message data and is also exposed through MessageEvent.ports. For cross-Isolate transfers a transit channel is created (internal:transit-port) and the partner port is upgraded in-place to use cross-thread messaging.
Realm transport matrix:
| Transport | Clone path | Transfer support |
|---|---|---|
Same-isolate MessagePort |
Runtime structured-clone subset. | ArrayBuffer and MessagePort. |
Thread ThreadPort |
Serializer transport. | ArrayBuffer and MessagePort. |
Process ProcessPort |
Serializer transport over process realm handles. | ArrayBuffer; MessagePort rejects. |
| Remote/cluster calls | Cluster transport serialization. | No live MessagePort transfer contract. |
Example
const channel = new MessageChannel();
channel.port2.onmessage = (event) => {
console.log(event.data);
};
channel.port2.start();
channel.port1.postMessage({ type: 'ready' });
Interfaces
interface MessageEventInit {
Initialization object for MessageEvent.
const init: MessageEventInit = { data: 'hello', origin: 'fino' };
new MessageEvent('message', init);
Properties
data?: any
Message payload exposed by event.data.
const init: MessageEventInit = { data: { ok: true } };
origin?: string
Origin string for browser-compatible APIs.
Fino messaging usually leaves this as the empty string.
const init: MessageEventInit = { origin: 'https://example.com' };
lastEventId?: string
Last event id for EventSource-compatible payloads.
const init: MessageEventInit = { lastEventId: '42' };
source?: MessagePort | null
Source MessagePort, or null when there is no source.
const init: MessageEventInit = { source: null };
ports?: MessagePort[]
Transferred MessagePort objects attached to this message.
const channel = new MessageChannel();
const init: MessageEventInit = { ports: [channel.port1] };
Classes
class MessageEvent extends Event {
Event subclass used for message and messageerror delivery.
Data defaults to null, string fields default to empty string, source defaults to null, and ports is a frozen copy of the supplied array.
const event = new MessageEvent('message', { data: 'hello' });
event.data; // "hello"
Constructors
constructor(type: string, init?: MessageEventInit)
Create a MessageEvent.
The event type is usually "message" or "messageerror".
new MessageEvent('message', { data: 1 }).type; // "message"
Getters
get data()
Message payload.
new MessageEvent('message', { data: 1 }).data; // 1
get origin()
Origin string for compatibility with browser MessageEvent.
new MessageEvent('message').origin; // ""
get lastEventId()
Last event id string.
new MessageEvent('message').lastEventId; // ""
get source()
Source MessagePort or null.
new MessageEvent('message').source; // null
get ports()
Frozen transferred ports array.
const ports = new MessageEvent('message').ports;
ports.length; // 0
class MessagePort extends EventTarget {
MessagePort for same-isolate and transit cross-isolate messaging.
Same-isolate messages are structured-cloned into the partner's queue and
dispatched as message events on the next loop step. Transit mode — entered
when the port's partner has been transferred to another Isolate — serializes
messages through the runtime serializer and delivers them via wake pipes.
A port holds incoming messages until it starts: call start() explicitly
when using addEventListener, or assign onmessage, which starts the port
implicitly. close() (or using disposal) permanently stops delivery, and
transferring a port via another port's postMessage neuters the local
endpoint.
const { port1, port2 } = new MessageChannel();
port2.onmessage = (event) => console.log(event.data);
port1.postMessage('hello');
Methods
postMessage(message: any, transferOrOpts?: Transferable[] | StructuredSerializeOptions): void
Send a structured-clone copy of message to the entangled partner.
The second argument may be a transfer array or a { transfer } options
bag. Transfer lists may contain ArrayBuffers (detached at the sender) and
MessagePorts: a transferred MessagePort is neutered at the sending side,
reconstructed as a fresh port for the receiver, replaces matching
references inside the cloned data, and also appears on
MessageEvent.ports. Delivery is queued on the partner and dispatched on
the next loop step once the partner has started.
Closed or neutered ports silently ignore sends. The transfer list is
validated before anything is cloned or neutered, so a failed call leaves
every port untouched. Throws a DataCloneError DOMException for duplicate
transfer entries, closed or neutered ports in the transfer list, an
attempt to transfer this port through itself, transfer values other than
ArrayBuffer/MessagePort, or a payload outside the runtime's
structured-clone subset.
const { port1, port2 } = new MessageChannel();
port2.onmessage = (event) => event.ports[0]?.postMessage('got it');
const inner = new MessageChannel();
inner.port2.onmessage = (event) => console.log(event.data);
port1.postMessage({ reply: inner.port1 }, [inner.port1]);
start(): void
Begin dispatching queued messages.
Setting onmessage also starts the port. Repeated calls are no-ops.
const channel = new MessageChannel();
channel.port1.start();
close(): void
Close this port and release runtime read watchers.
After close(), postMessage is ignored and pending same-isolate messages are not dispatched.
const channel = new MessageChannel();
channel.port1.close();
Getters
get onmessage()
Message event handler property.
Assigning a function registers it as a message listener and starts the port. Assigning null clears the previous handler.
const channel = new MessageChannel();
channel.port2.onmessage = (event) => console.log(event.data);
get onmessageerror()
Message error handler property.
const channel = new MessageChannel();
channel.port2.onmessageerror = (event) => console.log(event.data);
Setters
set onmessage(fn: ((ev: MessageEvent) => void) | null)
Set or clear the message handler property.
const channel = new MessageChannel();
channel.port2.onmessage = null;
set onmessageerror(fn: ((ev: MessageEvent) => void) | null)
Set or clear the messageerror handler property.
const channel = new MessageChannel();
channel.port2.onmessageerror = null;
class MessageChannel {
Pair of entangled MessagePort endpoints.
Anything posted on port1 arrives on port2 and vice versa. A common
pattern is to keep one port locally and hand the other to another component
(or transfer it to another Realm) as a private two-way channel.
const { port1, port2 } = new MessageChannel();
port2.onmessage = (event) => console.log('received', event.data);
port1.postMessage({ hello: 'world' });
Readonly Properties
readonly port1: MessagePort
First endpoint of the channel.
const channel = new MessageChannel();
channel.port1.start();
readonly port2: MessagePort
Second endpoint of the channel.
const channel = new MessageChannel();
channel.port2.start();
Constructors
constructor()
Create two entangled MessagePort instances.
const { port1, port2 } = new MessageChannel();