| name | messageport-dispatcher |
| description | How to use @actualwave/messageport-dispatcher — cross-origin event communication via MessagePort. Use when adding typed named-event messaging between iframes, Web Workers, or any window-like target; when replacing raw postMessage/message-event boilerplate with named events; or when working in a project that imports from this package. |
| license | MIT |
| metadata | {"package":"@actualwave/messageport-dispatcher","version":"1.1.7","repository":"https://github.com/burdiuz/js-messageport-event-dispatcher"} |
@actualwave/messageport-dispatcher
Cross-origin EventDispatcher built on the MessagePort API. Replaces raw postMessage / message event boilerplate with typed named events on both sides of a communication channel. Internally it uses two @actualwave/event-dispatcher instances — one for incoming (receiver) and one for outgoing (sender) events.
Installation
npm install @actualwave/messageport-dispatcher
yarn add @actualwave/messageport-dispatcher
Core concepts
MessagePortDispatcher — main class. Wraps a target (Window, Worker, or any MessagePort-like object).
receiver — IEventDispatcher that fires incoming events from the other side. The public addEventListener / removeEventListener methods delegate here.
sender — IEventDispatcher that fires when YOU send an event (the echo). Listen here to observe your own outgoing traffic locally.
dispatcherId — unique ID stamped on every outgoing packet. When a message echoes back on the same channel the dispatcher routes it to sender, not receiver, preventing false local dispatch.
targetOrigin — passed to postMessage. Defaults to '*'. Set to a specific origin in production.
Usage patterns
1. Iframe — outer document
import { MessagePortDispatcher } from '@actualwave/messageport-dispatcher';
const dispatcher = new MessagePortDispatcher(iframeElement.contentWindow);
dispatcher.addEventListener('ready', (event) => {
console.log('iframe is ready', event.data);
});
dispatcher.dispatchEvent('init', { config: { theme: 'dark' } });
2. Iframe — inside the iframe
import { getForParent } from '@actualwave/messageport-dispatcher';
const dispatcher = getForParent();
dispatcher.dispatchEvent('ready', { version: '1.0' });
dispatcher.addEventListener('init', (event) => {
applyConfig((event.data as any).config);
});
getForSelf(), getForParent(), and getForTop() return cached singletons — always the same instance per execution context.
3. Web Worker
Main thread:
import { MessagePortDispatcher } from '@actualwave/messageport-dispatcher';
const worker = new Worker('./worker.js');
const dispatcher = new MessagePortDispatcher(worker);
dispatcher.addEventListener('result', (event) => console.log(event.data));
dispatcher.dispatchEvent('compute', { input: [1, 2, 3] });
Inside the worker:
import { getForSelf } from '@actualwave/messageport-dispatcher';
const dispatcher = getForSelf();
dispatcher.addEventListener('compute', (event) => {
dispatcher.dispatchEvent('result', process((event.data as any).input));
});
4. Custom transport adapter
Any object with postMessage, addEventListener, and removeEventListener works:
const target = {
postMessage: (data: unknown, origin: string) => myTransport.send(data),
addEventListener: (type: string, handler: (e: any) => void) => myTransport.on(type, handler),
removeEventListener: (type: string, handler: (e: any) => void) => myTransport.off(type, handler),
};
const dispatcher = new MessagePortDispatcher(target);
5. MessagePortTarget — split sender / receiver
Use when sending and receiving go through different objects. Typical for same-page iframe setups where you send to iframe.contentWindow but receive on window:
import { MessagePortDispatcher, MessagePortTarget } from '@actualwave/messageport-dispatcher';
const target = new MessagePortTarget(iframeElement.contentWindow, window);
const dispatcher = new MessagePortDispatcher(target);
Arrays are accepted for broadcasting to or receiving from multiple targets:
const target = new MessagePortTarget(
[iframe1.contentWindow, iframe2.contentWindow],
window,
);
Sender vs receiver — which to use
dispatcher.addEventListener('event', handler);
dispatcher.receiver.addEventListener('event', handler);
dispatcher.sender.addEventListener('event', handler);
dispatcher.dispatchEvent('event', data);
dispatcher.sender.dispatchEvent('event', data);
Event preprocessors
Transform every event before it reaches listeners (receiver) or before it is sent (sender):
import type { EventProcessor } from '@actualwave/event-dispatcher';
const addTimestamp: EventProcessor = (event) => ({
...event,
data: { ...(event.data as object), ts: Date.now() },
});
const dispatcher = new MessagePortDispatcher(
target,
null,
addTimestamp,
addTimestamp,
);
Custom postMessage handler
Replace the default target.postMessage() call entirely:
const dispatcher = new MessagePortDispatcher(
target,
function (data, targetOrigin, transferList) {
console.log('Sending', data);
this.target.postMessage(data, targetOrigin, transferList);
},
);
API reference
MessagePortDispatcher constructor
| Parameter | Type | Default | Description |
|---|
target | MessagePortLike | null | self | Object with postMessage and addEventListener. |
customPostMessageHandler | Function | null | null | Replaces default target.postMessage(). Receives (data, targetOrigin, transferList?). |
receiverEventPreprocessor | EventProcessor | null | null | Transforms incoming events before listeners fire. |
senderEventPreprocessor | EventProcessor | null | null | Transforms outgoing events before postMessage. |
Instance members
| Member | Type | Description |
|---|
dispatcherId | string | Unique ID stamped on every outgoing packet. |
targetOrigin | string | Passed to postMessage. Default '*'. |
target | MessagePortLike | The underlying port object. |
customPostMessageHandler | PostMessageHandler | null | The custom handler passed to the constructor, if any. |
senderEventPreprocessor | EventProcessor | null | The outgoing event preprocessor passed to the constructor, if any. |
sender | IEventDispatcher | Fires for outgoing echoes. |
receiver | IEventDispatcher | Fires for incoming events. |
addEventListener(type, listener, priority?) | void | Adds listener to receiver. |
hasEventListener(type) | boolean | Checks receiver. |
removeEventListener(type, listener) | void | Removes from receiver. |
removeAllEventListeners(type) | void | Clears all receiver listeners for a type. |
dispatchEvent(eventType, data?, transferList?) | void | Serialises and sends via postMessage. |
Factory functions
| Function | Description |
|---|
createMessagePortDispatcher(target?, ...) | Creates a new MessagePortDispatcher. Same signature as constructor. |
getForSelf() | Cached singleton for self. |
getForParent() | Cached singleton for parent. |
getForTop() | Cached singleton for top. |
MessagePortTarget
new MessagePortTarget(
sender?: PostMessageTarget | PostMessageTarget[] | null,
receiver?: EventListenerTarget | EventListenerTarget[] | null,
)
PostMessageTarget needs postMessage(...args). EventListenerTarget needs addEventListener and removeEventListener.
Data serialisation
Only serialisable data crosses the boundary. Before sending, the library inspects the event data value:
- Has
toJSON() method → its return value is sent as-is (structured clone path). The developer is responsible for converting all nested objects.
- No
toJSON() → JSON.stringify is applied, then parsed back on the receiving side.
class MyPayload {
constructor(public items: Item[]) {}
toJSON() {
return { items: this.items.map((i) => i.serialize()) };
}
}
dispatcher.dispatchEvent('update', new MyPayload(items));
Wire format and message parsing utilities
Every outgoing event is wrapped in a MessagePortEvent envelope before being passed to postMessage. The wire format is:
{ event: serialisedEventObject, dispatcherId: string }
Where serialisedEventObject is either the return value of event.toJSON() (if implemented) or JSON.stringify(event).
Three utilities for working with this envelope are exported:
import {
MessagePortEvent,
isMessagePortEvent,
parseMessagePortEvent,
} from '@actualwave/messageport-dispatcher';
MessagePortEvent
The envelope class used internally for every packet:
class MessagePortEvent {
event: unknown;
dispatcherId: string;
constructor(event: unknown, dispatcherId: string);
toJSON(): { event: unknown; dispatcherId: string };
}
isMessagePortEvent
Type guard that checks whether a value has the { event, dispatcherId } shape without parsing:
isMessagePortEvent(object: unknown): object is { event: unknown; dispatcherId: string }
parseMessagePortEvent
Parses a raw postMessage payload into a MessagePortEvent, or returns null if the value is not a valid envelope. Handles both JSON strings and structured-clone objects:
parseMessagePortEvent(object: unknown): MessagePortEvent | null
This is most useful when you need to intercept raw message events and route them manually — for example, a server-side handler (Service Worker, SharedWorker) that must extract a MessagePort from the native event before forwarding to a per-client dispatcher:
import { parseMessagePortEvent } from '@actualwave/messageport-dispatcher';
import type { EventObject } from '@actualwave/event-dispatcher';
self.addEventListener('message', (nativeEvent: MessageEvent) => {
const message = parseMessagePortEvent(nativeEvent.data);
if (!message) return;
const { type, data } = message.event as EventObject;
const clientPort = nativeEvent.ports[0];
});
TypeScript types
import type {
EventObject,
DispatchedEvent,
EventType,
EventListener,
EventProcessor,
IEventDispatcher,
} from '@actualwave/event-dispatcher';
import type { MessagePortLike } from '@actualwave/messageport-dispatcher';
Common edge cases
- Echo suppression:
window.postMessage echoes messages back to the sender window. MessagePortDispatcher handles this automatically via dispatcherId — echoed events go to sender, never receiver.
- Same-side dispatch:
dispatcher.dispatchEvent('e') does NOT fire on the same dispatcher's addEventListener. Use MessagePortTarget with the same window on both sides if loopback is needed.
targetOrigin in production: Default is '*'. For cross-origin iframes carrying sensitive data, set dispatcher.targetOrigin = 'https://trusted-origin.com' before dispatching.
- Singleton factories:
getForSelf(), getForParent(), getForTop() are cached per module. They cannot be reset — create a new MessagePortDispatcher() directly if you need multiple instances for the same global target.
- Worker lifecycle: The worker must set up its own
MessagePortDispatcher (or call getForSelf()) before the main thread sends the first event.
toJSON responsibility: When toJSON() is present, the library passes its return value through without further processing. Nested objects are your responsibility.
- Non-serialisable values: Functions, class instances without
toJSON, Map, Set, undefined, and circular references will throw or silently lose data during JSON.stringify. Use plain objects or implement toJSON.
- React Native:
_postMessageListener checks event.nativeEvent ?? event before parsing. This makes MessagePortDispatcher compatible with React Native WebViews, which wrap the native event inside a .nativeEvent property, without any extra configuration.