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