Web Messaging
Elide implements the [HTML §9 Web Messaging][spec] surface — MessageChannel, MessagePort,
and MessageEvent — as part of the [WinterCG Minimum Common API][wintercg]. The implementation
is single-realm, same-thread: there is no Worker boundary, so the entire channel lives inside
the realm that constructed it.
[wintercg]: https://min-common-api.proposal.wintercg.org/ [spec]: https://html.spec.whatwg.org/multipage/web-messaging.html
MessageChannel
declare class MessageChannel {
constructor();
readonly port1: MessagePort;
readonly port2: MessagePort;
}The constructor builds two entangled MessagePort instances. They are wired as partners at
construction time and never re-attached. Either side may be start()ed, close()d, or used
independently — the relationship is fully symmetric.
const chan = new MessageChannel();
chan.port2.onmessage = (ev) => console.log(ev.data);
chan.port1.postMessage({ hello: "world" });
// → { hello: "world" }MessagePort
declare class MessagePort extends EventTarget {
postMessage(message: any, transfer?: Transferable[]): void;
postMessage(message: any, options?: { transfer?: Transferable[] }): void;
start(): void;
close(): void;
onmessage: ((this: MessagePort, ev: MessageEvent) => any) | null;
onmessageerror: ((this: MessagePort, ev: MessageEvent) => any) | null;
onclose: ((this: MessagePort, ev: Event) => any) | null;
}MessagePort is not constructible from JavaScript. new MessagePort() raises TypeError;
the only way to obtain a port is through MessageChannel.
postMessage(message, transferOrOptions?)
Delivers a [structured-clone][clone] copy of message to the entangled partner. The second
argument is honored for transferable streams — ReadableStream, WritableStream, and
TransformStream in the transfer list are transferred to the entangled partner. Other
transferables in the list are accepted but not given special transfer semantics.
[clone]: https://html.spec.whatwg.org/multipage/structured-data.html#structured-clone
Behaviour by partner state:
- partner is started — the
messageevent fires synchronously on the partner. - partner is not started — the event is buffered FIFO into the partner's inbox and drained when the partner is started.
- partner is detached or closed — the call is a silent no-op.
start()
Idempotently enables the port's message queue and drains any events that were buffered before
the queue was started. Calling start() a second time is a no-op.
close()
Marks the port closed, drops any buffered events, detaches from the partner, and fires a
trusted close event at the partner (if the partner is still open). Subsequent postMessage
calls on a closed port are silent no-ops, matching the spec's "no error path" rule.
Event-handler properties
onmessage, onmessageerror, and onclose follow the standard
[event-handler reflection algorithm][handlers]. Setting a callable installs it as a listener for
the matching event type; setting any non-callable value (null, undefined, a string)
removes the previously installed handler. Reading the property returns the currently registered
function, or null when none is installed.
[handlers]: https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-attributes
Assignment to onmessage additionally auto-starts the port — this matches the HTML
specification's port-message-queue activation rule, which treats handler assignment as an
implicit start().
const c = new MessageChannel();
c.port1.postMessage("buffered"); // queues into port2's inbox
c.port2.onmessage = (ev) => log(ev.data); // implicitly starts port2 → drains "buffered"MessageEvent
declare class MessageEvent extends Event {
constructor(type: string, eventInitDict?: MessageEventInit);
readonly data: any;
readonly origin: string;
readonly lastEventId: string;
readonly source: object | null;
readonly ports: ReadonlyArray<MessagePort>;
initMessageEvent(
type: string,
bubbles?: boolean,
cancelable?: boolean,
data?: any,
origin?: string,
lastEventId?: string,
source?: object | null,
ports?: MessagePort[],
): void;
}
interface MessageEventInit extends EventInit {
data?: any;
origin?: string;
lastEventId?: string;
source?: object | null;
ports?: MessagePort[];
}MessageEvent extends Event. MessageEvent.prototype inherits from Event.prototype, so
stopPropagation(), preventDefault(), composedPath(), and the rest of the Event surface
are available unchanged.
Defaults match the WebIDL: data = null, origin = "", lastEventId = "", source = null,
ports = [] (a frozen empty array shared across reads). Events constructed by the runtime —
the message event delivered by a MessagePort — are dispatched with isTrusted = true.
initMessageEvent(...) is the legacy HTML §9.4.7 entry point. It re-initialises every
attribute (type, bubbles, cancelable, data, origin, lastEventId, source, ports) on an
already-constructed event. Calling it on an event that is currently dispatching is a no-op
per spec.
Structured clone
The postMessage clone path covers the same-realm same-thread subset of the
[HTML structured-clone algorithm][clone]:
- primitives (string, number, bigint, boolean,
null,undefined) — pass-through; - plain objects (
{...}) — recursive clone of enumerable own keys; - arrays — element-wise clone, dense layout preserved;
- cycles — handled via an identity-keyed visited map, so a self-referential object clones to a self-referential clone with the same shape.
Values outside this subset — functions, symbols, Date, RegExp, Map, Set,
ArrayBuffer, and host-foreign values — raise a
[DataCloneError][datacloneerror] DOMException immediately, so callers see a typed failure
rather than silent loss of fidelity. Future iterations will broaden the supported subset; the
current contract is conservative on purpose.
[datacloneerror]: ./api-wintercg-dom-exception.md