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

ts
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.

js
const chan = new MessageChannel();
chan.port2.onmessage = (ev) => console.log(ev.data);
chan.port1.postMessage({ hello: "world" });
// → { hello: "world" }

MessagePort

ts
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 message event 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().

js
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

ts
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