A type-safe Request/Reply and Pub/Sub messaging library for cross-context communication in browsers, enabling seamless message exchange between the main window, iframes, and web workers.
- Request/Reply Pattern: Send requests and receive responses asynchronously
- Pub/Sub Pattern: Publish messages to multiple subscribers
- Cross-Context Communication: Works seamlessly between:
- Main window ↔ iframes
- Main window ↔ web workers
- iframe ↔ iframe
- Components within the same context
- Type-Safe: Built with TypeScript for excellent type inference
- Promise-Based: Modern async/await API
- Secure: Uses MessageChannel API for isolated communication
npm i @knowledgecode/messengerimport { MessengerClient, MessengerServer } from '@knowledgecode/messenger';For legacy environments without ES module support:
<script src="./node_modules/@knowledgecode/messenger/dist/messenger.js"></script>
<script>
const { MessengerClient, MessengerServer } = self.messenger;
</script>For workers in legacy environments:
// worker.ts (legacy)
importScripts('./node_modules/@knowledgecode/messenger/dist/messenger.js');
const { MessengerServer } = self.messenger;Note: The examples below are written in TypeScript. Build them to JavaScript files using TypeScript compiler or a bundler before running in the browser.
import { MessengerClient } from '@knowledgecode/messenger';
const messenger = new MessengerClient();
const worker = new Worker('./worker.js', { type: 'module' });
(async () => {
// Connect to the worker's server named 'calculator'
await messenger.connect('calculator', worker);
// Request/Reply: Send a request and wait for response
const result = await messenger.req<number>('add', { x: 2, y: 3 });
console.log(result); // => 5
// Send: Fire and forget
messenger.send('close');
messenger.disconnect();
})();import { MessengerServer } from '@knowledgecode/messenger';
interface AddRequest {
x: number;
y: number;
}
const messenger = new MessengerServer('calculator', self);
// Bind handler for 'add' topic
messenger.bind<AddRequest>('add', (data) => {
if (!data) {
return 0;
}
return data.x + data.y;
});
// Bind handler for 'close' topic
messenger.bind<void>('close', () => {
messenger.close();
self.close();
});import { MessengerClient } from '@knowledgecode/messenger';
interface StatusUpdate {
status: string;
timestamp: number;
}
interface User {
id: number;
name: string;
email: string;
}
const messenger = new MessengerClient();
const iframe = document.querySelector('iframe');
(async () => {
// Connect to iframe's server with targetOrigin for security
await messenger.connect('iframe-app', iframe!.contentWindow!, {
targetOrigin: 'https://example.com', // Use '*' only for development
timeout: 5000
});
// Subscribe to messages published from the iframe
messenger.subscribe<StatusUpdate>('status-update', (data) => {
if (data) {
console.log('Status:', data.status);
}
});
// Send request to iframe
const userData = await messenger.req<User>('get-user', { id: 123 });
console.log(userData);
})();import { MessengerServer } from '@knowledgecode/messenger';
interface GetUserRequest {
id: number;
}
interface User {
id: number;
name: string;
email: string;
}
const messenger = new MessengerServer('iframe-app', self);
messenger.bind<GetUserRequest>('get-user', async (data) => {
if (!data) {
throw new Error('User ID is required');
}
const response = await fetch(`/api/users/${data.id}`);
return await response.json() as User;
});
// Publish status updates to subscribers
setInterval(() => {
messenger.publish('status-update', { status: 'running', timestamp: Date.now() });
}, 1000);You can use Messenger for communication between components within the same window or worker context.
import { MessengerServer } from '@knowledgecode/messenger';
interface DataItem {
id: number;
value: string;
}
const messenger = new MessengerServer('data-service', self);
messenger.bind<void>('get-data', (): DataItem[] => {
return [
{ id: 1, value: 'Item 1' },
{ id: 2, value: 'Item 2' },
{ id: 3, value: 'Item 3' }
];
});import { MessengerClient } from '@knowledgecode/messenger';
interface DataItem {
id: number;
value: string;
}
const messenger = new MessengerClient();
(async () => {
// Connect to the data service in the same context
await messenger.connect('data-service', self);
// Request data from the service
const items = await messenger.req<DataItem[]>('get-data');
console.log('Received items:', items);
messenger.disconnect();
})();Client for connecting to a MessengerServer and sending messages.
Creates a new MessengerClient instance.
const messenger = new MessengerClient();Establishes a connection to a MessengerServer.
Parameters:
name(string): Unique name of the MessengerServer to connect toendpoint(Window | Worker, optional): Target context that haspostMessage()method. Defaults toselfoptions(object, optional): Connection optionstargetOrigin(string, optional): Target origin for security (iframe only). Defaults to'*'. For production, always specify the exact origintimeout(number, optional): Connection timeout in milliseconds. If omitted, waits indefinitely
Returns: Promise<void> - Resolves when connection is established
Throws:
Errorif endpoint doesn't havepostMessage()methodErrorif connection times out
Examples:
// Connect to iframe with security and timeout
const iframe = document.querySelector('iframe');
await messenger.connect('my-iframe', iframe!.contentWindow!, {
targetOrigin: 'https://trusted-domain.com',
timeout: 5000
});// Connect to worker
const worker = new Worker('./worker.js', { type: 'module' });
await messenger.connect('my-worker', worker, { timeout: 3000 });// Connect from within a worker to parent
await messenger.connect('main', self);Disconnects from the server, clears all subscriptions, and cleans up resources.
messenger.disconnect();Sends a one-way message to a topic. Does not wait for a response.
Parameters:
topic(string): Topic namedata(unknown): Data to send
Throws: Error if not connected
messenger.send('log', { level: 'info', message: 'Task completed' });Sends a request to a topic and waits for a response.
Type Parameters:
T(optional): The expected response type. Defaults tounknown
Parameters:
topic(string): Topic namedata(unknown, optional): Data to sendtimeout(number, optional): Request timeout in milliseconds. If omitted, waits indefinitely
Returns: Promise<T> - Resolves with the response data of type T
Throws:
Errorif not connectedErrorif request times outErrorif the topic is not bound on the server
Examples:
// Simple request with type inference
const result = await messenger.req<number>('calculate', { operation: 'add', values: [1, 2, 3] });// Request with timeout and type safety
interface DataResponse {
id: number;
value: string;
}
try {
const data = await messenger.req<DataResponse>('fetch-data', { id: 123 }, 5000);
console.log(data.value); // TypeScript knows about the 'value' property
} catch (error) {
console.error('Request failed:', (error as Error).message);
}Subscribes to messages published on a topic.
Type Parameters:
T(optional): The expected message data type. Defaults tounknown
Parameters:
topic(string): Topic namelistener(function): Callback function invoked when messages are published- Signature:
(data?: T) => void
- Signature:
Throws: Error if not connected
interface Notification {
title: string;
message: string;
timestamp: number;
}
messenger.subscribe<Notification>('notifications', (data) => {
if (data) {
console.log('Notification received:', data.title);
}
});Unsubscribes from a topic.
Parameters:
topic(string, optional): Topic name. If omitted, clears all subscriptionslistener(function, optional): Specific listener to remove. If omitted, removes all listeners for the topic
// Remove specific listener
interface UpdateData {
version: string;
}
const listener = (data?: UpdateData) => {
if (data) {
console.log(data.version);
}
};
messenger.subscribe<UpdateData>('updates', listener);
messenger.unsubscribe('updates', listener);
// Remove all listeners for a topic
messenger.unsubscribe('updates');
// Remove all subscriptions
messenger.unsubscribe();Server for accepting client connections and handling messages.
Creates a new MessengerServer instance.
Parameters:
name(string): Unique name for this server. Clients use this name to connectendpoint(Window | Worker, optional): Context to listen on. Defaults toself
// In a worker
const messenger = new MessengerServer('my-worker', self);
// In an iframe
const messenger = new MessengerServer('my-iframe', self);
// In main window (listening for messages from a specific worker)
const worker = new Worker('./worker.js', { type: 'module' });
const messenger = new MessengerServer('worker-listener', worker);Binds a handler to a topic for receiving messages.
Type Parameters:
T(optional): The expected message data type. Defaults tounknown
Parameters:
topic(string): Topic name (must be unique per server)listener(function): Handler function- Signature:
(data?: T) => unknown - For
send()messages: return value is ignored - For
req()messages: return value (or resolved Promise value) is sent back to client
- Signature:
Returns: boolean - true if bound successfully, false if topic already bound
Examples:
// Handle one-way messages
interface LogMessage {
level: string;
message: string;
}
messenger.bind<LogMessage>('log', (data) => {
if (data) {
console.log(`[${data.level}] ${data.message}`);
}
});
// Handle requests (synchronous)
interface AddRequest {
x: number;
y: number;
}
messenger.bind<AddRequest>('add', (data) => {
if (!data) {
return 0;
}
return data.x + data.y;
});
// Handle requests (asynchronous)
interface FetchUserRequest {
id: number;
}
interface User {
id: number;
name: string;
}
messenger.bind<FetchUserRequest>('fetch-user', async (data) => {
if (!data) {
throw new Error('User ID is required');
}
const response = await fetch(`/api/users/${data.id}`);
return await response.json() as User;
});
// Check binding result
if (!messenger.bind<void>('duplicate-topic', () => {})) {
console.error('Topic already bound');
}Removes the handler for a topic.
Parameters:
topic(string): Topic name
messenger.unbind('old-topic');Publishes a message to all subscribed clients on a topic.
Parameters:
topic(string): Topic namedata(unknown, optional): Data to publish
This method does not wait for responses and succeeds even if there are no subscribers.
// Notify all subscribers
messenger.publish('status-change', { status: 'ready', timestamp: Date.now() });
// Broadcast to all clients
setInterval(() => {
messenger.publish('heartbeat', { timestamp: Date.now() });
}, 1000);Closes all client connections, removes all handlers and subscriptions, and shuts down the server.
messenger.close();When connecting to iframes from different origins, always specify the exact targetOrigin instead of using '*':
// ❌ Insecure - allows any origin
await messenger.connect('iframe', iframe!.contentWindow!, { targetOrigin: '*' });
// ✅ Secure - restricts to specific origin
await messenger.connect('iframe', iframe!.contentWindow!, {
targetOrigin: 'https://trusted-domain.com'
});This library is written in TypeScript and provides full type safety with generic support.
import { MessengerClient } from '@knowledgecode/messenger';
interface User {
id: number;
name: string;
}
const messenger = new MessengerClient();
// Type-safe request - result is typed as User
const user = await messenger.req<User>('get-user', { id: 123 });
console.log(user.name); // TypeScript knows user has a name propertyThe listener receives data as T | undefined because messages may be published without data.
interface StatusUpdate {
status: 'online' | 'offline';
timestamp: number;
}
// Type-safe subscribe - data parameter is StatusUpdate | undefined
messenger.subscribe<StatusUpdate>('status-change', (data) => {
if (data) {
console.log(`Status: ${data.status} at ${data.timestamp}`);
}
});Handlers receive data as T | undefined because clients may send requests or messages without data.
interface CalculateRequest {
operation: 'add' | 'subtract' | 'multiply' | 'divide';
a: number;
b: number;
}
interface CalculateResponse {
result: number;
}
const messenger = new MessengerServer('calculator', self);
// Type-safe handler - data parameter is CalculateRequest | undefined
messenger.bind<CalculateRequest>('calculate', (data) => {
if (!data) {
return { result: 0 };
}
switch (data.operation) {
case 'add':
return { result: data.a + data.b };
case 'subtract':
return { result: data.a - data.b };
case 'multiply':
return { result: data.a * data.b };
case 'divide':
return { result: data.a / data.b };
}
});
// Client side - response is typed as CalculateResponse
const client = new MessengerClient();
await client.connect('calculator', self);
const response = await client.req<CalculateResponse>('calculate', {
operation: 'add',
a: 5,
b: 3
});
console.log(response.result); // TypeScript knows about result propertyMIT