New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

@knowledgecode/messenger

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@knowledgecode/messenger - npm Package Compare versions

Comparing version
0.5.0
to
0.6.0
+93
dist/index.d.ts
type Endpoint = Window | Worker;
type TopicListener<T = unknown> = (data?: T) => unknown;
type Packet<T = unknown> = {
method: 'ack';
payload: {
client: string;
};
} | {
method: 'reply';
payload: {
id: string;
topic: string;
data?: T;
error?: Error;
};
} | {
method: 'publish';
payload: {
topic: string;
data?: T;
};
} | {
method: 'connect';
name: string;
} | {
method: 'disconnect';
payload: {
client: string;
};
} | {
method: 'send';
payload: {
topic: string;
data?: T;
};
} | {
method: 'request';
payload: {
client: string;
id: string;
topic: string;
data?: T;
};
} | {
method: 'subscribe';
payload: {
client: string;
topic: string;
};
} | {
method: 'unsubscribe';
payload: {
client: string;
topic: string;
};
};
declare class Server {
private readonly _clients;
private readonly _listeners;
private readonly _subscribers;
private _endpoint;
private readonly _accept;
constructor(name: string, endpoint?: Endpoint);
_listener(evt: MessageEvent<Packet>): void;
_postMessage(client: string, message: Packet): void;
bind<T = unknown>(topic: string, listener: TopicListener<T>): boolean;
unbind(topic: string): void;
publish(topic: string, data?: unknown): void;
close(): void;
}
interface ClientOptions {
targetOrigin?: string;
timeout?: number;
}
declare class Client {
private _id;
private _port;
private _connected;
private readonly _reps;
private readonly _subscribers;
constructor();
_listener(evt: MessageEvent<Packet>): void;
_postMessage(message: Packet): void;
connect(name: string, endpoint?: Endpoint, options?: ClientOptions): Promise<void>;
disconnect(): void;
send(topic: string, data: unknown): void;
req<T = unknown>(topic: string, data?: unknown, timeout?: number): Promise<T>;
subscribe<T = unknown>(topic: string, listener: TopicListener<T>): void;
unsubscribe(topic?: string, listener?: TopicListener): void;
}
export { Client as MessengerClient, Server as MessengerServer };
/**
* @license
* Copyright 2019 KNOWLEDGECODE
* SPDX-License-Identifier: MIT
*/
class e{constructor(){this._id=void 0,this._port=void 0,this._connected=void 0,this._reps=new Map,this._subscribers=new Map}_listener(e){switch(e.data.method){case"reply":{const{id:t,data:s,error:i}=e.data.payload,o=this._reps.get(t);o&&(o.timerId&&clearTimeout(o.timerId),i?o.reject(i):o.resolve(s),this._reps.delete(t));break}case"publish":{const{topic:t,data:s}=e.data.payload,i=this._subscribers.get(t);if(i)for(const e of i.values())e(s);break}}}_postMessage(e){var t;null==(t=this._port)||t.postMessage(e)}connect(e,t=self,s={}){return this._connected?this._connected:"postMessage"in t?(this._connected=new Promise((i,o)=>{const{port1:r,port2:n}=new MessageChannel,{targetOrigin:a,timeout:c}=s,d=(null!=c?c:0)>0?setTimeout(()=>{var e;null==(e=this._port)||e.close(),this._port=void 0,this._connected=void 0,o(new Error("Connection timed out."))},c):0,h={method:"connect",name:e};this._port=r,this._port.onmessage=e=>{const{method:t}=e.data;this._port&&"ack"===t&&(this._id=e.data.payload.client,this._port.onmessage=e=>this._listener(e),d&&clearTimeout(d),i())},t.postMessage(h,{targetOrigin:null!=a?a:"*",transfer:[n]})}),this._connected):Promise.reject(new Error("The endpoint has no postMessage method."))}disconnect(){this.unsubscribe();for(const e of this._reps.values())clearTimeout(e.timerId);this._reps.clear(),this._id&&this._port&&(this._postMessage({method:"disconnect",payload:{client:this._id}}),this._port.close(),this._port=void 0,this._id=void 0),this._connected=void 0}send(e,t){if(!this._port)throw new Error("Not connected.");this._postMessage({method:"send",payload:{topic:e,data:t}})}req(e,t,s=0){return new Promise((i,o)=>{if(!this._id||!this._port)return void o(new Error("Not connected."));const r=Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2),n=s>0?self.setTimeout(()=>{const e=this._reps.get(r);e&&(e.reject(new Error("Request timed out.")),this._reps.delete(r))},s):0;this._reps.set(r,{resolve:i,reject:o,timerId:n}),this._postMessage({method:"request",payload:{client:this._id,id:r,topic:e,data:t}})})}subscribe(e,t){var s;if(!this._id||!this._port)throw new Error("Not connected.");const i=null!=(s=this._subscribers.get(e))?s:new Set;i.add(t),this._subscribers.set(e,i),this._postMessage({method:"subscribe",payload:{client:this._id,topic:e}})}unsubscribe(e,t){if(e)if(t){const s=this._subscribers.get(e);s&&s.delete(t)}else this._subscribers.delete(e);else this._subscribers.clear()}}class t{constructor(e,t=self){this._clients=new Map,this._listeners=new Map,this._subscribers=new Map,this._endpoint=t,this._accept=t=>{if("connect"!==t.data.method||t.data.name!==e)return;t.stopImmediatePropagation();const[s]=t.ports,i=Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2);s.onmessage=e=>this._listener(e),this._clients.set(i,s),s.postMessage({method:"ack",payload:{client:i}})},this._endpoint.addEventListener("message",this._accept)}_listener(e){var t,s,i,o;switch(e.data.method){case"send":{const{topic:s,data:i}=e.data.payload;null==(t=this._listeners.get(s))||t(i);break}case"request":{const{client:t,id:s,topic:i,data:r}=e.data.payload,n=this._listeners.get(i);n?(o=n(r),o instanceof Promise?o:Promise.resolve(o)).then(e=>this._postMessage(t,{method:"reply",payload:{id:s,topic:i,data:e}})).catch(e=>this._postMessage(t,{method:"reply",payload:{id:s,topic:i,error:e instanceof Error?e:new Error(String(e))}})):this._postMessage(t,{method:"reply",payload:{id:s,topic:i,error:new Error("Topic is not bound.")}});break}case"subscribe":{const{client:t,topic:i}=e.data.payload,o=null!=(s=this._subscribers.get(i))?s:new Set;o.add(t),this._subscribers.set(i,o);break}case"unsubscribe":{const{client:t,topic:s}=e.data.payload,i=this._subscribers.get(s);null==i||i.delete(t);break}case"disconnect":{const{client:t}=e.data.payload;null==(i=this._clients.get(t))||i.close(),this._clients.delete(t);for(const e of this._subscribers.values())e.delete(t);break}}}_postMessage(e,t){const s=this._clients.get(e);null==s||s.postMessage(t)}bind(e,t){return!this._listeners.has(e)&&(this._listeners.set(e,t),!0)}unbind(e){this._listeners.delete(e)}publish(e,t){const s=this._subscribers.get(e);if(s){const i={method:"publish",payload:{topic:e,data:t}};for(const e of s.values())this._postMessage(e,i)}}close(){for(const e of this._clients.values())e.close();this._clients.clear(),this._listeners.clear(),this._subscribers.clear(),this._endpoint&&(this._endpoint.removeEventListener("message",this._accept),this._endpoint=void 0)}}export{e as MessengerClient,t as MessengerServer};
/**
* @license
* Copyright 2019 KNOWLEDGECODE
* SPDX-License-Identifier: MIT
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).messenger={})}(this,function(e){"use strict";e.MessengerClient=class{constructor(){this._id=void 0,this._port=void 0,this._connected=void 0,this._reps=new Map,this._subscribers=new Map}_listener(e){switch(e.data.method){case"reply":{const{id:t,data:s,error:i}=e.data.payload,o=this._reps.get(t);o&&(o.timerId&&clearTimeout(o.timerId),i?o.reject(i):o.resolve(s),this._reps.delete(t));break}case"publish":{const{topic:t,data:s}=e.data.payload,i=this._subscribers.get(t);if(i)for(const e of i.values())e(s);break}}}_postMessage(e){var t;null==(t=this._port)||t.postMessage(e)}connect(e,t=self,s={}){return this._connected?this._connected:"postMessage"in t?(this._connected=new Promise((i,o)=>{const{port1:r,port2:n}=new MessageChannel,{targetOrigin:a,timeout:c}=s,d=(null!=c?c:0)>0?setTimeout(()=>{var e;null==(e=this._port)||e.close(),this._port=void 0,this._connected=void 0,o(new Error("Connection timed out."))},c):0,h={method:"connect",name:e};this._port=r,this._port.onmessage=e=>{const{method:t}=e.data;this._port&&"ack"===t&&(this._id=e.data.payload.client,this._port.onmessage=e=>this._listener(e),d&&clearTimeout(d),i())},t.postMessage(h,{targetOrigin:null!=a?a:"*",transfer:[n]})}),this._connected):Promise.reject(new Error("The endpoint has no postMessage method."))}disconnect(){this.unsubscribe();for(const e of this._reps.values())clearTimeout(e.timerId);this._reps.clear(),this._id&&this._port&&(this._postMessage({method:"disconnect",payload:{client:this._id}}),this._port.close(),this._port=void 0,this._id=void 0),this._connected=void 0}send(e,t){if(!this._port)throw new Error("Not connected.");this._postMessage({method:"send",payload:{topic:e,data:t}})}req(e,t,s=0){return new Promise((i,o)=>{if(!this._id||!this._port)return void o(new Error("Not connected."));const r=Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2),n=s>0?self.setTimeout(()=>{const e=this._reps.get(r);e&&(e.reject(new Error("Request timed out.")),this._reps.delete(r))},s):0;this._reps.set(r,{resolve:i,reject:o,timerId:n}),this._postMessage({method:"request",payload:{client:this._id,id:r,topic:e,data:t}})})}subscribe(e,t){var s;if(!this._id||!this._port)throw new Error("Not connected.");const i=null!=(s=this._subscribers.get(e))?s:new Set;i.add(t),this._subscribers.set(e,i),this._postMessage({method:"subscribe",payload:{client:this._id,topic:e}})}unsubscribe(e,t){if(e)if(t){const s=this._subscribers.get(e);s&&s.delete(t)}else this._subscribers.delete(e);else this._subscribers.clear()}},e.MessengerServer=class{constructor(e,t=self){this._clients=new Map,this._listeners=new Map,this._subscribers=new Map,this._endpoint=t,this._accept=t=>{if("connect"!==t.data.method||t.data.name!==e)return;t.stopImmediatePropagation();const[s]=t.ports,i=Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2);s.onmessage=e=>this._listener(e),this._clients.set(i,s),s.postMessage({method:"ack",payload:{client:i}})},this._endpoint.addEventListener("message",this._accept)}_listener(e){var t,s,i,o;switch(e.data.method){case"send":{const{topic:s,data:i}=e.data.payload;null==(t=this._listeners.get(s))||t(i);break}case"request":{const{client:t,id:s,topic:i,data:r}=e.data.payload,n=this._listeners.get(i);n?(o=n(r),o instanceof Promise?o:Promise.resolve(o)).then(e=>this._postMessage(t,{method:"reply",payload:{id:s,topic:i,data:e}})).catch(e=>this._postMessage(t,{method:"reply",payload:{id:s,topic:i,error:e instanceof Error?e:new Error(String(e))}})):this._postMessage(t,{method:"reply",payload:{id:s,topic:i,error:new Error("Topic is not bound.")}});break}case"subscribe":{const{client:t,topic:i}=e.data.payload,o=null!=(s=this._subscribers.get(i))?s:new Set;o.add(t),this._subscribers.set(i,o);break}case"unsubscribe":{const{client:t,topic:s}=e.data.payload,i=this._subscribers.get(s);null==i||i.delete(t);break}case"disconnect":{const{client:t}=e.data.payload;null==(i=this._clients.get(t))||i.close(),this._clients.delete(t);for(const e of this._subscribers.values())e.delete(t);break}}}_postMessage(e,t){const s=this._clients.get(e);null==s||s.postMessage(t)}bind(e,t){return!this._listeners.has(e)&&(this._listeners.set(e,t),!0)}unbind(e){this._listeners.delete(e)}publish(e,t){const s=this._subscribers.get(e);if(s){const i={method:"publish",payload:{topic:e,data:t}};for(const e of s.values())this._postMessage(e,i)}}close(){for(const e of this._clients.values())e.close();this._clients.clear(),this._listeners.clear(),this._subscribers.clear(),this._endpoint&&(this._endpoint.removeEventListener("message",this._accept),this._endpoint=void 0)}}});
+52
-32
{
"name": "@knowledgecode/messenger",
"version": "0.5.0",
"description": "Req/Rep Pub/Sub library for iframes and workers",
"main": "dist/umd/messenger.js",
"module": "dist/esm/messenger.mjs",
"types": "src/index.d.ts",
"scripts": {
"build": "rollup -c",
"test": "npm run build && karma start",
"test:ts": "tsd -f test/ts"
},
"repository": {
"type": "git",
"url": "git+https://github.com/knowledgecode/messenger.git"
},
"version": "0.6.0",
"description": "Type-safe Request/Reply and Pub/Sub messaging library for browser applications",
"keywords": [
"req",
"rep",
"pub",
"sub",
"request-reply",
"pub-sub",
"pubsub",
"messaging",
"communication",
"typescript",
"type-safe",
"iframe",
"worker"
"worker",
"browser",
"message-channel",
"components"
],
"author": "KNOWLEDGECODE",
"license": "MIT",
"homepage": "https://github.com/knowledgecode/messenger#readme",
"bugs": {
"url": "https://github.com/knowledgecode/messenger/issues"
},
"homepage": "https://github.com/knowledgecode/messenger#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/knowledgecode/messenger.git"
},
"license": "MIT",
"author": "KNOWLEDGECODE",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"LICENSE",
"README.md",
"dist/**/*"
],
"sideEffects": false,
"scripts": {
"build": "rollup -c",
"lint": "eslint",
"test": "vitest run"
},
"devDependencies": {
"@rollup/plugin-terser": "^0.3.0",
"chai": "^4.3.7",
"karma": "^6.4.1",
"karma-chai": "^0.1.0",
"karma-chrome-launcher": "^3.1.1",
"karma-mocha": "^2.0.1",
"karma-mocha-reporter": "^2.2.5",
"mocha": "^10.2.0",
"rollup": "^3.9.1",
"tsd": "^0.25.0"
"@eslint/js": "^9.39.2",
"@rollup/plugin-terser": "^0.4.4",
"@stylistic/eslint-plugin": "^5.6.1",
"@vitest/browser-playwright": "^4.0.16",
"eslint": "^9.39.2",
"playwright": "^1.57.0",
"rollup": "^4.54.0",
"rollup-plugin-dts": "^6.3.0",
"rollup-plugin-esbuild": "^6.2.1",
"rollup-plugin-license": "^3.6.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.50.1",
"vitest": "^4.0.16"
}
}
+536
-119
# Messenger
Messenger is a `Req/Rep` `Pub/Sub` library for iframes and workers.
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.
## Features
Allows messages to be exchanged between ...
- **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
- the main window and one or more iframes / workers.
- multiple iframes.
- multiple components.
## Installation
via npm:
```shell

@@ -23,20 +26,15 @@ npm i @knowledgecode/messenger

```javascript
### Node.js / Bundlers (Recommended)
```typescript
import { MessengerClient, MessengerServer } from '@knowledgecode/messenger';
```
ES Modules:
### Browser (UMD - Legacy)
```html
<script type="module">
import { MessengerClient, MessengerServer } from '/path/to/esm/messenger.js';
</script>
```
For legacy environments without ES module support:
Traditional:
```html
<script src="/path/to/umd/messenger.js"></script>
<script src="./node_modules/@knowledgecode/messenger/dist/messenger.js"></script>
<script>
// It is provided with the global variable name "messenger".
const { MessengerClient, MessengerServer } = self.messenger;

@@ -46,190 +44,609 @@ </script>

## Simple Example
For workers in legacy environments:
main.js
```javascript
import { MessengerClient } from '/path/to/esm/messenger.js';
```typescript
// worker.ts (legacy)
importScripts('./node_modules/@knowledgecode/messenger/dist/messenger.js');
const { MessengerServer } = self.messenger;
```
## Quick Start
> **Note**: The examples below are written in TypeScript. Build them to JavaScript files using TypeScript compiler or a bundler before running in the browser.
### Example: Main Window ↔ Worker
#### main.ts (build to main.js)
```typescript
import { MessengerClient } from '@knowledgecode/messenger';
const messenger = new MessengerClient();
const worker = new Worker('/path/to/worker.js');
const worker = new Worker('./worker.js', { type: 'module' });
(async () => {
await messenger.connect('example', worker);
// Connect to the worker's server named 'calculator'
await messenger.connect('calculator', worker);
const answer = await messenger.req('add', { x: 2, y: 3 });
// Request/Reply: Send a request and wait for response
const result = await messenger.req<number>('add', { x: 2, y: 3 });
console.log(result); // => 5
console.log(answer); // => 5
// Send: Fire and forget
messenger.send('close');
messenger.disconnect();
})();
```
messenger.send('close');
messenger.disconnect();
#### worker.ts (build to worker.js)
```typescript
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();
});
```
### Example: Main Window ↔ iframe
#### main.ts (build to main.js)
```typescript
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);
})();
```
worker.js
```javascript
importScripts('/path/to/umd/messenger.js');
#### iframe.ts (build to iframe.js)
const { MessengerServer } = self.messenger;
const messenger = new MessengerServer('example', self);
```typescript
import { MessengerServer } from '@knowledgecode/messenger';
messenger.bind('add', data => {
return data.x + data.y;
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;
});
messenger.bind('close', () => {
messenger.close();
// Close this worker.
self.close();
// Publish status updates to subscribers
setInterval(() => {
messenger.publish('status-update', { status: 'running', timestamp: Date.now() });
}, 1000);
```
### Example: Same-Context Communication (Component to Component)
You can use Messenger for communication between components within the same window or worker context.
#### data-service.ts (build to data-service.js)
```typescript
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' }
];
});
```
## MessengerClient API
#### app.ts (build to app.js)
### `constructor()`
```typescript
import { MessengerClient } from '@knowledgecode/messenger';
```javascript
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();
})();
```
### `connect(name, [endpoint[, options]])`
## API Reference
- {**string**} name - unique name of the MessengerServer to connect to
- {**Object**} [endpoint] - an object that actually executes the `postMessage()`
- {**Object**} [options] - connection options
### MessengerClient
The `MessengerClient` must connect to a `MessengerServer` via `endpoint` before communication can begin. To identify the `MessengerServer` to connect to, pass the unique name of the `MessengerServer` as the first argument. The `endpoint` is the object that actually executes the `postMessage()`. If omitted, it is assumed that `self` is set. The `options` are connection options and members of this object are `targetOrigin` and `timeout` (msec). If the `timeout` is omitted, this method will wait forever for a successful connection.
Client for connecting to a MessengerServer and sending messages.
```javascript
// To connect from the main window to a iframe.
const iframe = window.frames[0];
#### `constructor()`
await messenger.connect('iframe', iframe, { targetOrigin: '*', timeout: 1000 })
.catch(e => console.log(e));
Creates a new MessengerClient instance.
```typescript
const messenger = new MessengerClient();
```
```javascript
// To connect from the main window to a worker.
const worker = new Worker('/path/to/worker.js');
#### `connect(name, endpoint, options)`
await messenger.connect('worker', worker, { timeout: 1000 })
.catch(e => console.log(e));
Establishes a connection to a MessengerServer.
**Parameters:**
- `name` (**string**): Unique name of the MessengerServer to connect to
- `endpoint` (**Window | Worker**, optional): Target context that has `postMessage()` method. Defaults to `self`
- `options` (**object**, optional): Connection options
- `targetOrigin` (**string**, optional): Target origin for security (iframe only). Defaults to `'*'`. For production, always specify the exact origin
- `timeout` (**number**, optional): Connection timeout in milliseconds. If omitted, waits indefinitely
**Returns:** `Promise<void>` - Resolves when connection is established
**Throws:**
- `Error` if endpoint doesn't have `postMessage()` method
- `Error` if connection times out
**Examples:**
```typescript
// 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
});
```
### `disconnect()`
```typescript
// Connect to worker
const worker = new Worker('./worker.js', { type: 'module' });
await messenger.connect('my-worker', worker, { timeout: 3000 });
```
Disconnects from the server.
```typescript
// Connect from within a worker to parent
await messenger.connect('main', self);
```
```javascript
#### `disconnect()`
Disconnects from the server, clears all subscriptions, and cleans up resources.
```typescript
messenger.disconnect();
```
### `send(topic[, data])`
#### `send(topic, data)`
- {**string**} topic - topic name
- {**Object**} [data] - an object to send
Sends a one-way message to a topic. Does not wait for a response.
Sends a message (some object) to a topic. This method does not wait for any reply. A `MessengerServer` can receive the message if it is bound to the same topic name in advance.
**Parameters:**
```javascript
messenger.send('greeting', { hello: 'world' });
- `topic` (**string**): Topic name
- `data` (**unknown**): Data to send
**Throws:** `Error` if not connected
```typescript
messenger.send('log', { level: 'info', message: 'Task completed' });
```
### `req(topic[, data[, timeout]])`
#### `req<T>(topic, data, timeout)`
- {**string**} topic - topic name
- {**Object**} [data] - an object to send
- {**number**} [timeout] - timeout (msec)
Sends a request to a topic and waits for a response.
Sends a message (some object) to a topic. This method waits for some reply unlike `send()`. If `timeout` (msec) is omitted, this method waits forever for some reply.
**Type Parameters:**
```javascript
const answer = await messenger.req('add', { x: 2, y: 3 })
- `T` (optional): The expected response type. Defaults to `unknown`
console.log(answer);
**Parameters:**
- `topic` (**string**): Topic name
- `data` (**unknown**, optional): Data to send
- `timeout` (**number**, optional): Request timeout in milliseconds. If omitted, waits indefinitely
**Returns:** `Promise<T>` - Resolves with the response data of type `T`
**Throws:**
- `Error` if not connected
- `Error` if request times out
- `Error` if the topic is not bound on the server
**Examples:**
```typescript
// Simple request with type inference
const result = await messenger.req<number>('calculate', { operation: 'add', values: [1, 2, 3] });
```
```javascript
await messenger.req('add', { x: 2, y: 3 }, 5000)
.catch(e => console.log(e)); // Catch timeout error.
```typescript
// 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);
}
```
### `subscribe(topic, listener)`
#### `subscribe<T>(topic, listener)`
- {**string**} topic - topic name
- {**Function**} listener - a listener to receive published messages
Subscribes to messages published on a topic.
Subscribes to messages on a topic.
**Type Parameters:**
```javascript
messenger.subscribe('news', data => console.log(data));
- `T` (optional): The expected message data type. Defaults to `unknown`
**Parameters:**
- `topic` (**string**): Topic name
- `listener` (**function**): Callback function invoked when messages are published
- Signature: `(data?: T) => void`
**Throws:** `Error` if not connected
```typescript
interface Notification {
title: string;
message: string;
timestamp: number;
}
messenger.subscribe<Notification>('notifications', (data) => {
if (data) {
console.log('Notification received:', data.title);
}
});
```
### `unsubscribe(topic[, listener])`
#### `unsubscribe(topic, listener)`
Unsubscribes to messages on a topic. If listener is omitted, all listeners for the topic are cleared.
Unsubscribes from a topic.
- {**string**} topic - topic name
- {**Function**} [listener] - a listener to receive published messages
**Parameters:**
```javascript
const listener = data => console.log(data);
messenger.subscribe('news', listener);
- `topic` (**string**, optional): Topic name. If omitted, clears all subscriptions
- `listener` (**function**, optional): Specific listener to remove. If omitted, removes all listeners for the topic
messenger.unsubscribe('news', listener);
```typescript
// 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();
```
## MessengerServer API
---
### `constructor(name, [endpoint])`
### MessengerServer
- {**string**} name - unique name of the MessengerServer
- {**Object**} [endpoint] - an object that actually executes the `postMessage()`
Server for accepting client connections and handling messages.
```javascript
const messenger = new MessengerServer('server', self);
#### `constructor(name, endpoint)`
Creates a new MessengerServer instance.
**Parameters:**
- `name` (**string**): Unique name for this server. Clients use this name to connect
- `endpoint` (**Window | Worker**, optional): Context to listen on. Defaults to `self`
```typescript
// 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);
```
The `name` is a unique name by which clients identify this MessengerServer. The `endpoint` is the object that actually executes the `postMessage()`. If omitted, it is assumed that `self` is set.
#### `bind<T>(topic, listener)`
### `bind(topic, listener)`
Binds a handler to a topic for receiving messages.
- {**string**} topic - topic name
- {**Function**} listener - a listener to receive messages
**Type Parameters:**
Binds a listener to listen for messages on a topic. The topic names must be unique, no other listener than the first can bind on the same topic name. This method returns `true` or `false` as binding result.
- `T` (optional): The expected message data type. Defaults to `unknown`
```javascript
messenger.bind('greeting', data => console.log(data));
**Parameters:**
messenger.bind('add', data => {
// Reply to client.
return data.x + data.y;
- `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
**Returns:** `boolean` - `true` if bound successfully, `false` if topic already bound
**Examples:**
```typescript
// 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');
}
```
### `publish(topic, data)`
#### `unbind(topic)`
- {**string**} topic - topic name
- {**Object**} data - an object to publish
Removes the handler for a topic.
Publish a message (some object) to all subscribers on a topic. This method does not wait for reply from the subscribers, and also does not fail even there are no subscribers at all.
**Parameters:**
```javascript
messenger.publish('notification', 'The process completed successfully.');
- `topic` (**string**): Topic name
```typescript
messenger.unbind('old-topic');
```
### `close()`
#### `publish(topic, data)`
Closes all connections and shuts down the server.
Publishes a message to all subscribed clients on a topic.
```javascript
**Parameters:**
- `topic` (**string**): Topic name
- `data` (**unknown**, optional): Data to publish
This method does not wait for responses and succeeds even if there are no subscribers.
```typescript
// Notify all subscribers
messenger.publish('status-change', { status: 'ready', timestamp: Date.now() });
// Broadcast to all clients
setInterval(() => {
messenger.publish('heartbeat', { timestamp: Date.now() });
}, 1000);
```
#### `close()`
Closes all client connections, removes all handlers and subscriptions, and shuts down the server.
```typescript
messenger.close();
```
## Browser support
## Security Considerations
Chrome, Safari, Firefox, Edge
When connecting to iframes from different origins, always specify the exact `targetOrigin` instead of using `'*'`:
```typescript
// ❌ 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'
});
```
## TypeScript
This library is written in TypeScript and provides full type safety with generic support.
### Type-safe Requests
```typescript
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 property
```
### Type-safe Subscriptions
The listener receives `data` as `T | undefined` because messages may be published without data.
```typescript
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}`);
}
});
```
### Type-safe Handlers
Handlers receive `data` as `T | undefined` because clients may send requests or messages without data.
```typescript
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 property
```
## License
MIT
class s{constructor(){this._id=void 0,this._port=void 0,this._reps=new Map,this._subscribers=new Map}_listener(s){const{method:e}=s.data||{},{id:t,topic:i,data:r,error:o}=(s.data||{}).payload||{};switch(e){case"reply":const s=this._reps.get(t);s&&(s.timerId&&clearTimeout(s.timerId),o?s.reject(o):s.resolve(r),this._reps.delete(t));break;case"publish":const e=this._subscribers.get(i);if(e)for(const s of e.values())s(r)}}_postMessage(s,e,t,i){this._port.postMessage({method:s,payload:{client:this._id,id:e,topic:t,data:i}})}connect(s,e=self,t={}){return this._port?Promise.resolve():"postMessage"in e?new Promise(((i,r)=>{const{port1:o,port2:n}=new MessageChannel,{targetOrigin:a,timeout:c}=t,h=c>0?setTimeout((()=>{this._port.close(),this._port=void 0,r(new Error("Connection timed out."))}),c):0;this._port=o,this._port.onmessage=s=>{const{method:e}=s.data||{},{client:t}=(s.data||{}).payload||{};"ack"===e&&(this._id=t,this._port.onmessage=s=>this._listener(s),h&&clearTimeout(h),i())},e.postMessage({name:s,method:"connect"},{targetOrigin:a||"/",transfer:[n]})})):Promise.reject(new Error("The endpoint has no postMessage method."))}disconnect(){this._port&&(this._postMessage("disconnect"),this._port.close(),this._port=void 0),this._reps.clear(),this.unsubscribe()}send(s,e){if(!this._port)throw new Error("No connected.");this._postMessage("send","",s,e)}req(s,e,t=0){return new Promise(((i,r)=>{if(!this._port)return void r(new Error("No connected."));const o=Math.random().toString(36).slice(2),n=t>0?setTimeout((()=>{const s=this._reps.get(o);s&&(s.reject(new Error("Request timed out.")),this._reps.delete(o))}),t):0;this._reps.set(o,{resolve:i,reject:r,timerId:n}),this._postMessage("request",o,s,e)}))}subscribe(s,e){if(!this._port)throw new Error("No connected.");const t=this._subscribers.get(s)||new Set;t.add(e),this._subscribers.set(s,t),this._postMessage("subscribe","",s)}unsubscribe(s,e){if(e){const t=this._subscribers.get(s);t&&t.delete(e)}else s?this._subscribers.delete(s):this._subscribers.clear()}}class e{constructor(s,e=self){this._clients=new Map,this._listeners=new Map,this._subscribers=new Map,this._endpoint=e,this._accept=e=>{const{name:t,method:i}=e.data||{},[r]=e.ports||[];if(s!==t||"connect"!==i||!r)return;e.stopImmediatePropagation();const o=Math.random().toString(36).slice(2);r.onmessage=s=>this._listener(s),this._clients.set(o,r),r.postMessage({method:"ack",payload:{client:o}})},this._endpoint.addEventListener("message",this._accept)}_listener(s){const{method:e}=s.data||{},{client:t,id:i,topic:r,data:o}=(s.data||{}).payload||{};switch(e){case"send":this._listeners.has(r)&&this._listeners.get(r)(o);break;case"request":this._listeners.has(r)?(n=this._listeners.get(r)(o),n instanceof Promise?n:Promise.resolve(n)).then((s=>this._postMessage(t,"reply",i,r,s))).catch((s=>this._postMessage(t,"reply",i,r,void 0,s))):this._postMessage(t,"reply",i,r,void 0,"Topic is not bound.");break;case"subscribe":const s=this._subscribers.get(r)||new Set;s.add(t),this._subscribers.set(r,s);break;case"disconnect":this._clients.has(t)&&(this._clients.get(t).close(),this._clients.delete(t))}var n}_postMessage(s,e,t,i,r,o){const n=this._clients.get(s);n&&n.postMessage({method:e,payload:{id:t,topic:i,data:r,error:o}})}bind(s,e){return!this._listeners.has(s)&&(this._listeners.set(s,e),!0)}unbind(s){this._listeners.delete(s)}publish(s,e){const t=this._subscribers.get(s);if(t)for(const i of t.values())this._postMessage(i,"publish","",s,e)}close(){for(const s of this._clients.values())s.close();this._clients.clear(),this._listeners.clear(),this._subscribers.clear(),this._endpoint.removeEventListener("message",this._accept),this._endpoint=void 0}}export{s as MessengerClient,e as MessengerServer};
class s{constructor(){this._id=void 0,this._port=void 0,this._reps=new Map,this._subscribers=new Map}_listener(s){const{method:e}=s.data||{},{id:t,topic:i,data:r,error:o}=(s.data||{}).payload||{};switch(e){case"reply":const s=this._reps.get(t);s&&(s.timerId&&clearTimeout(s.timerId),o?s.reject(o):s.resolve(r),this._reps.delete(t));break;case"publish":const e=this._subscribers.get(i);if(e)for(const s of e.values())s(r)}}_postMessage(s,e,t,i){this._port.postMessage({method:s,payload:{client:this._id,id:e,topic:t,data:i}})}connect(s,e=self,t={}){return this._port?Promise.resolve():"postMessage"in e?new Promise(((i,r)=>{const{port1:o,port2:n}=new MessageChannel,{targetOrigin:a,timeout:c}=t,h=c>0?setTimeout((()=>{this._port.close(),this._port=void 0,r(new Error("Connection timed out."))}),c):0;this._port=o,this._port.onmessage=s=>{const{method:e}=s.data||{},{client:t}=(s.data||{}).payload||{};"ack"===e&&(this._id=t,this._port.onmessage=s=>this._listener(s),h&&clearTimeout(h),i())},e.postMessage({name:s,method:"connect"},{targetOrigin:a||"/",transfer:[n]})})):Promise.reject(new Error("The endpoint has no postMessage method."))}disconnect(){this._port&&(this._postMessage("disconnect"),this._port.close(),this._port=void 0),this._reps.clear(),this.unsubscribe()}send(s,e){if(!this._port)throw new Error("No connected.");this._postMessage("send","",s,e)}req(s,e,t=0){return new Promise(((i,r)=>{if(!this._port)return void r(new Error("No connected."));const o=Math.random().toString(36).slice(2),n=t>0?setTimeout((()=>{const s=this._reps.get(o);s&&(s.reject(new Error("Request timed out.")),this._reps.delete(o))}),t):0;this._reps.set(o,{resolve:i,reject:r,timerId:n}),this._postMessage("request",o,s,e)}))}subscribe(s,e){if(!this._port)throw new Error("No connected.");const t=this._subscribers.get(s)||new Set;t.add(e),this._subscribers.set(s,t),this._postMessage("subscribe","",s)}unsubscribe(s,e){if(e){const t=this._subscribers.get(s);t&&t.delete(e)}else s?this._subscribers.delete(s):this._subscribers.clear()}}class e{constructor(s,e=self){this._clients=new Map,this._listeners=new Map,this._subscribers=new Map,this._endpoint=e,this._accept=e=>{const{name:t,method:i}=e.data||{},[r]=e.ports||[];if(s!==t||"connect"!==i||!r)return;e.stopImmediatePropagation();const o=Math.random().toString(36).slice(2);r.onmessage=s=>this._listener(s),this._clients.set(o,r),r.postMessage({method:"ack",payload:{client:o}})},this._endpoint.addEventListener("message",this._accept)}_listener(s){const{method:e}=s.data||{},{client:t,id:i,topic:r,data:o}=(s.data||{}).payload||{};switch(e){case"send":this._listeners.has(r)&&this._listeners.get(r)(o);break;case"request":this._listeners.has(r)?(n=this._listeners.get(r)(o),n instanceof Promise?n:Promise.resolve(n)).then((s=>this._postMessage(t,"reply",i,r,s))).catch((s=>this._postMessage(t,"reply",i,r,void 0,s))):this._postMessage(t,"reply",i,r,void 0,"Topic is not bound.");break;case"subscribe":const s=this._subscribers.get(r)||new Set;s.add(t),this._subscribers.set(r,s);break;case"disconnect":this._clients.has(t)&&(this._clients.get(t).close(),this._clients.delete(t))}var n}_postMessage(s,e,t,i,r,o){const n=this._clients.get(s);n&&n.postMessage({method:e,payload:{id:t,topic:i,data:r,error:o}})}bind(s,e){return!this._listeners.has(s)&&(this._listeners.set(s,e),!0)}unbind(s){this._listeners.delete(s)}publish(s,e){const t=this._subscribers.get(s);if(t)for(const i of t.values())this._postMessage(i,"publish","",s,e)}close(){for(const s of this._clients.values())s.close();this._clients.clear(),this._listeners.clear(),this._subscribers.clear(),this._endpoint.removeEventListener("message",this._accept),this._endpoint=void 0}}export{s as MessengerClient,e as MessengerServer};
!function(e,s){"object"==typeof exports&&"undefined"!=typeof module?s(exports):"function"==typeof define&&define.amd?define(["exports"],s):s((e="undefined"!=typeof globalThis?globalThis:e||self).messenger={})}(this,(function(e){"use strict";e.MessengerClient=class{constructor(){this._id=void 0,this._port=void 0,this._reps=new Map,this._subscribers=new Map}_listener(e){const{method:s}=e.data||{},{id:t,topic:i,data:o,error:r}=(e.data||{}).payload||{};switch(s){case"reply":const e=this._reps.get(t);e&&(e.timerId&&clearTimeout(e.timerId),r?e.reject(r):e.resolve(o),this._reps.delete(t));break;case"publish":const s=this._subscribers.get(i);if(s)for(const e of s.values())e(o)}}_postMessage(e,s,t,i){this._port.postMessage({method:e,payload:{client:this._id,id:s,topic:t,data:i}})}connect(e,s=self,t={}){return this._port?Promise.resolve():"postMessage"in s?new Promise(((i,o)=>{const{port1:r,port2:n}=new MessageChannel,{targetOrigin:c,timeout:a}=t,h=a>0?setTimeout((()=>{this._port.close(),this._port=void 0,o(new Error("Connection timed out."))}),a):0;this._port=r,this._port.onmessage=e=>{const{method:s}=e.data||{},{client:t}=(e.data||{}).payload||{};"ack"===s&&(this._id=t,this._port.onmessage=e=>this._listener(e),h&&clearTimeout(h),i())},s.postMessage({name:e,method:"connect"},{targetOrigin:c||"/",transfer:[n]})})):Promise.reject(new Error("The endpoint has no postMessage method."))}disconnect(){this._port&&(this._postMessage("disconnect"),this._port.close(),this._port=void 0),this._reps.clear(),this.unsubscribe()}send(e,s){if(!this._port)throw new Error("No connected.");this._postMessage("send","",e,s)}req(e,s,t=0){return new Promise(((i,o)=>{if(!this._port)return void o(new Error("No connected."));const r=Math.random().toString(36).slice(2),n=t>0?setTimeout((()=>{const e=this._reps.get(r);e&&(e.reject(new Error("Request timed out.")),this._reps.delete(r))}),t):0;this._reps.set(r,{resolve:i,reject:o,timerId:n}),this._postMessage("request",r,e,s)}))}subscribe(e,s){if(!this._port)throw new Error("No connected.");const t=this._subscribers.get(e)||new Set;t.add(s),this._subscribers.set(e,t),this._postMessage("subscribe","",e)}unsubscribe(e,s){if(s){const t=this._subscribers.get(e);t&&t.delete(s)}else e?this._subscribers.delete(e):this._subscribers.clear()}},e.MessengerServer=class{constructor(e,s=self){this._clients=new Map,this._listeners=new Map,this._subscribers=new Map,this._endpoint=s,this._accept=s=>{const{name:t,method:i}=s.data||{},[o]=s.ports||[];if(e!==t||"connect"!==i||!o)return;s.stopImmediatePropagation();const r=Math.random().toString(36).slice(2);o.onmessage=e=>this._listener(e),this._clients.set(r,o),o.postMessage({method:"ack",payload:{client:r}})},this._endpoint.addEventListener("message",this._accept)}_listener(e){const{method:s}=e.data||{},{client:t,id:i,topic:o,data:r}=(e.data||{}).payload||{};switch(s){case"send":this._listeners.has(o)&&this._listeners.get(o)(r);break;case"request":this._listeners.has(o)?(n=this._listeners.get(o)(r),n instanceof Promise?n:Promise.resolve(n)).then((e=>this._postMessage(t,"reply",i,o,e))).catch((e=>this._postMessage(t,"reply",i,o,void 0,e))):this._postMessage(t,"reply",i,o,void 0,"Topic is not bound.");break;case"subscribe":const e=this._subscribers.get(o)||new Set;e.add(t),this._subscribers.set(o,e);break;case"disconnect":this._clients.has(t)&&(this._clients.get(t).close(),this._clients.delete(t))}var n}_postMessage(e,s,t,i,o,r){const n=this._clients.get(e);n&&n.postMessage({method:s,payload:{id:t,topic:i,data:o,error:r}})}bind(e,s){return!this._listeners.has(e)&&(this._listeners.set(e,s),!0)}unbind(e){this._listeners.delete(e)}publish(e,s){const t=this._subscribers.get(e);if(t)for(const i of t.values())this._postMessage(i,"publish","",e,s)}close(){for(const e of this._clients.values())e.close();this._clients.clear(),this._listeners.clear(),this._subscribers.clear(),this._endpoint.removeEventListener("message",this._accept),this._endpoint=void 0}}}));
export class MessengerClient {
constructor();
connect(name: string, endpoint?: any, options?: { targetOrigin?: string, timeout?: number }): Promise<void>;
disconnect(): void;
send(topic: string, data?: any): void;
req(topic: string, data?: any, timeout?: number): Promise<unknown>;
subscribe(topic: string, listener: (data: unknown) => void): void;
unsubscribe(topic?: string, listener?: (data: unknown) => void): void;
}
export class MessengerServer {
constructor(name: string, endpoint?: any);
bind(topic: string, listener: (data: unknown) => void): boolean;
unbind(topic: string): void;
publish(topic: string, data?: any): void;
close(): void;
}