A tinny(~3kb) utility that can simplify cross window / iframes / workers communications, even with progress feedback support.
+
A tinny(~4kb) utility that can simplify cross window / iframes / workers communications, even with progress feedback support.
-## ๐ Table of Contents
+>[!NOTE]
+> This utility is designed to simplify the communication between different window/node contexts(windows, iframes, workers, etc) in the browser, it's a peer-to-peer communication tool, that means you can send messages to a peer and get its response, or listen to messages from a peer and respond to it, but it's not a traditional pub/sub tool, you won't be able to listen message from it self.
+
+## ๐Table of Contents
- [Features](#features)
- [Install](#install)
- [Example](#example)
- [Usage](#usage)
- - [PostMessageHub](#postmessagehub) for windows / iframes / workers messaging
- - [postMessageHub.emit](#postmessagehubemit)
- - [postMessageHub.on](#postmessagehubon)
- - [progress for PostMessageHub](#progress-for-postmessagehub)
- - [postMessageHub.off](#postmessagehuboff)
- - [postMessageHub.createDedicatedMessageHub](#postmessagehubcreatededicatedmessagehub)
- - [postMessageHub.createProxy](#postmessagehubcreateproxy)
- - [StorageMessageHub](#storagemessagehub)
- - [storageMessageHub.emit](#storagemessagehubemit)
- - [storageMessageHub.on](#storagemessagehubon)
- - [progress for storageMessageHub](#progress-for-storagemessagehub)
- - [storageMessageHub.off](#storagemessagehuboff)
- - [PageScriptMessageHub](#pagescriptmessagehub)
- - [pageScriptMessageHub.emit](#pagescriptmessagehubemit)
- - [pageScriptMessageHub.on](#pagescriptmessagehubon)
- - [progress for PageScriptMessageHub](#progress-for-pagescriptmessagehub)
- - [pageScriptMessageHub.off](#pagescriptmessagehuboff)
+ - [import \& debug](#import--debug)
+ - [constructor](#constructor)
+ - [Shared instance](#shared-instance)
+ - [on](#on)
+ - [emit](#emit)
+ - [off](#off)
+ - [destroy](#destroy)
+ - [progress](#progress)
+ - [proxy for PostMessageHub](#proxy-for-postmessagehub)
+ - [dedicated instance for PostMessageHub](#dedicated-instance-for-postmessagehub)
- [Error](#error)
- - [Debug](#debug)
+- [Development](#development)
+
## Features
* **Simple API**: `on` `emit` and `off` are all you need
@@ -70,7 +70,7 @@ npm install duplex-message -S
The following example shows you how to use it to make normal window and its iframe communicate easily
-in main window
+**in main window**
```js
import { PostMessageHub } from "duplex-message"
@@ -127,7 +127,7 @@ postMessageHub.emit(peerWin, "some-not-existing-method").then(resp => {
})
```
-in iframe window
+**in iframe window**
```js
import { PostMessageHub } from "duplex-message"
@@ -170,539 +170,373 @@ function fib(n) {
return fib(n - 1) + fib(n - 2)
}
```
-## Usage
-
-### PostMessageHub
-`PostMessageHub` works in browser and use `postMessage` under the hood, it enable you:
-1. communicate between multi **windows / iframes / workers / window.openers** easily at the same time
-2. listen and respond messages with the same code.
+For more examples, check the [demo folder](./demo/).
-When to use it:
-> 1. you have iframes / workers / windows opened by another window
-> 2. you need to communicate between them.
+## Usage
-`PostMessageHub` is a class, `new` an instance before using it
-```js
-import { PostMessageHub } from "duplex-message"
+This utility has 4 classes for different scenarios:
-const postMessageHub = new PostMessageHub()
-```
+* `PostMessageHub` for windows/iframes/workers communication via `postMessage`
+* `StorageMessageHub`for same-origin pages messaging via localStorage's `storage` event
+* `PageScriptMessageHub` for isolated JS environments (e.g., UserScripts) in the same window context via `customEvent`, or it can be used as an event bus in the same window context
+* `BroadcastMessageHub` for broadcast messaging in same-origin pages (documents) or different Node.js threads (workers) via `BroadcastChannel`
-Tips:
-> in most cases, you only need one instance in a window/worker context, you can use
->> `PostMessageHub.shared` instead of new an instance.
-> e.g.:
-> `PostMessageHub.shared.on(peerWin, 'xxx', () => {...})`
+They all implement the same class `AbstractHub`, so they have a similar API and can be used in the same way.
-#### postMessageHub.emit
-Send a message to peer, invoking `methodName` registered on the peer via [`on`](#postmessagehubon) with all its arguments `args`:
+### Import & Debug
+This utility has a debug mode:
+* When the environment variable `process.env.NODE_ENV` is set to any value other than `production`, like `development`, it will log some debug info to the console.
+* When the environment variable `process.env.NODE_ENV` is set to `production`, there will be no debug logs. With a bundler tool like `webpack` or `vite`, the output will be optimized and the debug code will be stripped out.
```ts
-// in typescript, use ResponseType to specify response type
-postMessageHub.emit(peer: Window | Worker, methodName: string, ...args: any[]) => Promise
-```
+// Use ES6 import
+import { PostMessageHub, StorageMessageHub, PageScriptMessageHub, BroadcastMessageHub } from "duplex-message"
-This api return a promise, you can get the response or catch an exception via it.
+// Or use CommonJS require
+const { PostMessageHub, StorageMessageHub, PageScriptMessageHub, BroadcastMessageHub } = require("duplex-message")
-e.g.
-```js
-// for ts
-postMessageHub
- .emit(peerWindow, 'some-method', 'arg1', 'arg2', 'otherArgs')
- // res will be inferred as a string
- .then(res => console.log('success', res))
- .catch(err => console.warn('error', err))
-
-// for js
-postMessageHub
- .emit(peerWindow, 'some-method', 'arg1', 'arg2', 'otherArgs')
- // res will be inferred as a string
- .then(res => console.log('success', res))
- .catch(err => console.warn('error', err))
-```
+// If you don't want to use the debug mode, or have trouble with the environment variable, you can use a production version
-Notice:
-1. If there are multi peers listening to the same message, you'll only get the first one who respond, others will be ignored.
-2. look into [Error](#error) when you catch an error
-3. set `peer` to `self` if you want to send message from worker to outside
-4. omit args if no args are required, e.g `postMessageHub.emit(peerWindow, 'some-method')`
-5. you may need to handle the promise returned by `emit` if some linters warning unhandled promise(or floating promise)
+// Use ES6 import
+import { PostMessageHub, StorageMessageHub, PageScriptMessageHub, BroadcastMessageHub } from "duplex-message/dist/duplex-message.production.es"
-
-#### postMessageHub.on
-Listen messages sent from peer, it has following forms:
-```ts
-// register(listen)) one handler for methodName when message received from peer
-// * means all peers, same as below
-postMessageHub.on(peer: Window | Worker | '*', methodName: string, handler: Function)
-// register(listen)) multi handlers
-postMessageHub.on(peer: Window | Worker | '*', handlerMap: Record)
-// register only one handler to deal with all messages from peer
-postMessageHub.on(peer: Window | Worker | '*', singleHandler: Function)
+// Or use CommonJS require
+const { PostMessageHub, StorageMessageHub, PageScriptMessageHub, BroadcastMessageHub } = require("duplex-message/dist/duplex-message.production.umd")
```
-e.g.
-```js
-// listen multi messages from peerWindow by passing a handler map
-postMessageHub.on(peerWindow, {
- hi (name) {
- console.log(`name ${name}`)
- // response by return
- return `hi ${name}`
- },
- 'some-method': function (a, b) {
- ...
- }
-})
-
-// listen 'some-other-method' from peerWindow with an async callback
-postMessageHub.on(peerWindow, 'some-other-method', async function (a, b, c) {
- try {
- const result = await someAsyncFn(a, b, c)
- return result
- } catch (e) {
- throw e
- }
-})
+### constructor
+All classes have a constructor with an optional `options` object, you can set some options when creating an instance
-// listen all peers' 'some-common-method' with a same callback
-postMessageHub.on('*', 'some-common-method', async function (a, b) {
- ...
-})
-
-// listen all messages to peerWindow2 with on callback (first arg is the methodName)
-postMessageHub.on(peerWindow2, async function (methodName, ...args) {
- ...
-})
-```
+>[!WARNING]
+>instances from different classes can't communicate with each other, they are isolated
-Notice:
-1. you should only listen a message once, it will override existing listener when do it again
-2. set `peer` to `self` if you want to listen messages in worker
-3. set `peer` to a `Worker` instance(e.g `new Worker('./xxxx.js')`) if you want to listen its messages in a normal window context
-4. the specified callback will be called if you listen same `methodName` in specified peer and `*`
-5. if you want worker's messages handled by callbacks registered via peer `*` , **you must call `postMessageHub.on` with worker(e.g `postMessageHub.on(worker, {})`) to register worker due to worker's restrictions**
+```ts
+// all classes have the following options
+// different classes may its own options
+interface IAbstractHubOptions {
+ /**
+ * custom instance id, used for identifying different instances
+ * if not set, a random id will be generated
+ */
+ instanceID?: string | null
+}
+// new an instance with options
+const postMessageHub = new PostMessageHub({ instanceID: 'some-id' })
+// or use default options
+const pageScriptMessageHub = new PageScriptMessageHub()
-#### progress for PostMessageHub
-If you need progress feedback when peer handling you requests, you can do it by setting the first argument as an object and has a function property named `onprogress` when `emit` messages, and call `onprogress` in `on` on the peer's side.
+// StorageMessageHub has its own options keyPrefix
+interface IStorageMessageHubOptions extends IAbstractHubOptions {
+ /** localStorage key prefix to store message, default: $$xiu */
+ keyPrefix?: string
+}
-e.g.
-```js
-// in normal window, send message to worker, get progress
-postMessageHub.emit(workerInstance, 'download', {
- onprogress(p) {console.log('progress: ' + p.count)}
-}).then(e => {
- console.log('success: ', e)
-}).catch(err => {
- console.log('error: ' + err)
-})
+// new an instance with keyPrefix and instanceID
+// different instances must have same keyPrefix to communicate
+const storageMessageHub = new StorageMessageHub({ instanceID: 'some-other-id', keyPrefix: 'my-key-prefix' })
+// const storageMessageHub = new StorageMessageHub() // use default options also works
-// in worker thread
-// listen download from main window
-workerMessageHub.on(self, {
- // download with progress support
- download: (msg) => {
- return new Promise((resolve, reject) => {
- let hiCount = 0
- const tid = setInterval(() => {
- if (hiCount >= 100) {
- clearInterval(tid)
- return resolve('done')
- }
- // call onprogress if exists
- msg && msg.onprogress && msg.onprogress({count: hiCount += 10})
- }, 200)
- })
- }
+// BroadcastMessageHub has its own options channelName
+interface IBroadcastMessageHubOptions extends IAbstractHubOptions {
+ /** custom broadcast channel name, default: message-hub */
+ channelName?: string
}
+// new an instance with channelName and instanceID
+// different instances must have same channelName to communicate
+const broadcastMessageHub = new BroadcastMessageHub({ instanceID: 'some-other-id', channelName: 'my-channel-name' })
+// const broadcastMessageHub = new BroadcastMessageHub() // use default options also works
```
-#### postMessageHub.off
-Remove message handlers, if `methodName` presented, remove `methodName`'s listener, or remove the whole peer's listener
+### Shared instance
+Each class has a shared instance, you can use it directly without new an instance.
```ts
-postMessageHub.off(peer: Window | Worker | '*', methodName?: string)
+const postMessageHub = PostMessageHub.shared
+const storageMessageHub = StorageMessageHub.shared
+const pageScriptMessageHub = PageScriptMessageHub.shared
+const broadcastMessageHub = BroadcastMessageHub.shared
```
-#### postMessageHub.destroy
-Destroy instance: remove all message handlers and references of objects.
-Any invoking of destroyed instance's methods will throw an exception
+You will always get the same instance when you use `shared` property in the same context, it's a singleton.
-```ts
-postMessageHub.destroy()
-```
-#### postMessageHub.createDedicatedMessageHub
-Create a dedicated message-hub for specified peer, so that you won't need to pass peer every time:
+### on
+Listen messages sent from peer then respond it by return a value in handler, it has following forms:
```ts
-/**
-* @param peer peer window to communicate with, or you can set it later via `setPeer`
-* @param silent when peer not exists, keep silent instead of throw an error when call emit, on, off
-*/
-postMessageHub.createDedicatedMessageHub (peer?: Window | Worker, silent?: boolean) => IDedicatedMessageHub
-
-interface IDedicatedMessageHub {
- /** if you didn't set a peer when invoking createDedicatedMessageHub, then you can use `setPeer` to set it when it's ready*/
- setPeer: (peer: Window | Worker) => void;
- // in typescript, use ResponseType to specify response type
- emit: (methodName: string, ...args: any[]) => Promise;
- on: (methodName: string, handler: Function) => void;
- on: (handlerMap: Record) => void;
- off: (methodName?: string) => any;
-}
+// listen one message from peer, and respond it by return in handler
+instance.on(peer: any, methodName: string, handler: Function)
+// listen multi messages from peer by passing a handler map, and respond it by return in handler.
+// * multi handlers are supported for one message
+instance.on(peer: any, handlerMap: Record)
+// listen all messages from peer with one handler, and respond it by return in handler, the first arg of the generalHandler is the methodName
+instance.on(peer: any, generalHandler: Function)
```
-e.g.
-```js
-// create without a peer
-const dedicatedMessageHub = postMessageHub.createDedicatedMessageHub (null, true)
-
-// this won't work, but won't throw an error neither
-dedicatedMessageHub.on('a', () => {...})
-
-dedicatedMessageHub.setPeer(someWorker)
-
-dedicatedMessageHub.on('xx', () => {...})
-// in ts, specify response type
-dedicatedMessageHub.emit<{title: string}>('some-method', 'xx', 'xxx').then((res) => {
- console.log(res.title)
-}).catch(() => {...})
-
-// in js
-dedicatedMessageHub.emit<{title: string}>('some-method', 'xx', 'xxx').then((res) => {
- console.log(res.title)
-}).catch(() => {...})
-```
+The parameter `peer` may be ignored in some instances, e.g. `PageScriptMessageHub`, because it's isolated in the same window context.
-#### postMessageHub.createProxy
-Forward all messages from `fromWin` to `toWin` then forward `toWin`'s response to the `fromWin`, instead of handle messages by self
-
-```ts
-postMessageHub.createProxy(fromWin: Window | Worker, toWin: Window | Worker)
-```
-
-e.g.
```ts
-// forward all messages from `someWorker` to `someIframeWin`
-postMessageHub.createProxy(someWorker, someIframeWin)
+// PostMessageHub requires a `peer` to listen messages from, it could be a `Window`, `Worker`, or '*'(any window/worker)
+postMessageHub.on(peerWindow, {
+ hi (name) {
+ console.log(`name ${name}`)
+ // response by return
+ return `hi ${name}`
+ },
+ 'some-method': function (a, b) {
+ ...
+ }
+})
+// listen a message from a worker
+postMessageHub.on(workerInstance, 'some-other-method', console.log)
+// listen message from a iframe
+postMessageHub.on(someIframe.contentWindow, 'some-iframe-method', console.log)
+// in worker context itself, just set peer to self to listen messages from outside
+postMessageHub.on(self, 'some-worker-method', console.log)
+// listen all messages from all workers/windows. if the methodName has been listened by a specific peer, the handlers from * will be ignored
+postMessageHub.on('*', 'some-common-method', console.log)
+// listen all message from win in one handler
+postMessageHub.on(win, async function (methodName, ...args) {
+ ...
+})
-```
+// -------------------
-There is a funny use case(transparent proxy):
-If you got two iframes in your page, you can make them communicate directly by following code
-```ts
-postMessageHub.createProxy(frame1Win, frame2Win) // forward message from frame1Win to frame2Win
-postMessageHub.createProxy(frame2Win, frame1Win) // forward message from frame2Win to frame1Win
-```
+// StorageMessageHub doesn't require a `peer` to listen messages from, it's isolated in the same window context
+storageMessageHub.on('async-add', async function (a, b) {
+ return new Promise((resolve, reject) => {
+ resolve(a + b)
+ })
+})
-### StorageMessageHub
-`StorageMessageHub` works in browser and use `storage` event(trigger via changing localStorage) under the hood, it enable you communicating between pages with the [same origin](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy)(aka with the same `location.origin`(protocol + domain + port)) in a simple way.
+// -------------------
-When to use it:
-> 1. pages you want to share messages are with the same origin
-> 2. they are not(all) managed(opened) by a same page
+// PageScriptMessageHub doesn't require a `peer` to listen messages from, it's isolated in the same window context
+pageScriptMessageHub.on('async-add', async function (a, b) {
+ return new Promise((resolve, reject) => {
+ resolve(a + b)
+ })
+})
-`StorageMessageHub` is a class, new an instance before using it:
-```js
-import { StorageMessageHub } from "duplex-message"
+// -------------------
-const storageMessageHub = new StorageMessageHub(options?: IStorageMessageHubOptions)
+// BroadcastMessageHub doesn't require a `peer` to listen messages from, it's required to set the same channelName to communicate(and same-origin in browser)
+broadcastMessageHub.on('async-add', async function (a, b) {
+ return new Promise((resolve, reject) => {
+ resolve(a + b)
+ })
+})
-interface IStorageMessageHubOptions {
- /** custom instance id for communicating in emit */
- instanceID?: string
- /** timeout number(millisecond as unit) when no response is received, default: 1000 milliseconds */
- timeout?: number
- /** localStorage key prefix to store message, default: $$xiu */
- keyPrefix?: string
- /** a customable identity that can make your self identified by others, will be used by StorageMessageHub.getPeerIdentifies */
- identity?: any
-}
```
-Tips:
-> in most cases, you only need one instance in one page, you can use
->> `StorageMessageHub.shared` instead of new an instance.
-> e.g.:
-> `StorageMessageHub.shared.on('xxx', () => {...})`
-Notice:
-> Web pages in browser with same origin are weak connected, they just share one same localStorage area. Sending a message via localStorage just like sending a broadcast, there maybe no listener, or more than one listeners. So, a `timeout` is necessary in case of there is no listener can respond your messages, or they don't respond in time.
+>[!WARNING]
+>Although a message can be listened by multiple handlers, only one response from a handler will be sent to the peer. It follows the first-come-first-serve rule:
+> 1. All handlers will be called when a message is received at the same time.
+> 2. The first handler that successfully returns a non-undefined value will be the response; others will be ignored.
+> 3. If no handler returns a non-undefined value, then the response will be `undefined`.
+> 4. Errors thrown by handlers will be ignored if there is a successful call.
+> 5. If all handlers throw an error, the last error will be caught and sent to the peer.
-#### storageMessageHub.emit
-Broadcast(or you can also send to specified peer) a message, invoking `methodName` registered on the peers via [`on`](#storageMessageHubbon) with all its arguments `args`, return a promise with result.
+### emit
+Emit a message to a peer, invoking `methodName` registered on the peer via [`on`](#on) with all its arguments `args`, and return a promise with the response from the peer.
```ts
-// send a message and get response
-//
-// if no toInstance specified, promise resolve when `first success` response received(
-// there may be more than one peers, they all will respond this message,
-// you will get the first success response, rest responses will be discarded)
-// or the specified instance will respond your call
-//
-// in typescript, use ResponseType to specify response type
-storageMessageHub.emit(methodName: string | IMethodNameConfig, ...args: any[]) => Promise
interface IMethodNameConfig {
methodName: string
- /** peer's instance id */
- toInstance?: string
+ /** peer instance ID that can receive the message */
+ to?: string
}
-```
-Notice:
-1. you should only listen a message once, it will override existing listener when do it again
-2. If there are multi webpages listening to the same message, you'll only get the first one who respond, others will be ignored.
-3. look into [Error](#error) when you catch an error
-4. arguments must be stringify-able, due to localStorage's restrictions
-5. you may need to handle the promise returned by `emit` if some linters warning unhandled promise(or floating promise)
-
-e.g.
-```js
-// broadcast that user has logout
-storageMessageHub.emit('user-logout')
-
-// send a message and get the first success response
-// in ts, specify response type
-storageMessageHub.emit<{name: string, email: string}>('get-some-info').then(res => {
- console.log(res.name)
-}).catch(err => { console.error(err)})
-// in js
-storageMessageHub.emit('get-some-info').then(res => {
- console.log(res.name)
-}).catch(err => { console.error(err)})
+// In TypeScript, use ResponseType to specify the response type
+// Send a message and get a response
+instance.emit(peer: any, methodName: string | IMethodNameConfig, ...args: any[]) => Promise
```
-#### storageMessageHub.on
-Listen messages sent from peer, it has following forms:
+Just like [`on`](#on), the parameter `peer` may be ignored in some instances, e.g., `PageScriptMessageHub`, because it's isolated in the same window context.
+
```ts
-// register(listen)) one handler for methodName when message received from main process
-storageMessageHub.on(methodName: string, handler: Function)
-// register(listen)) multi handlers
-storageMessageHub.on(handlerMap: Record)
-// register only one handler to deal with all messages from process
-storageMessageHub.on(singleHandler: Function)
-```
-e.g.
-```js
-storageMessageHub.on('async-add', async function (a, b) {
- return new Promise((resolve, reject) => {
- resolve(a + b)
- })
+// PostMessageHub requires a `peer` to send messages to; it could be a `Window` or `Worker`. (`*` is not allowed when emitting messages)
+postMessageHub.emit(peerWindow, 'some-method', 'arg1', 'arg2', 'otherArgs').then(res => {
+ console.log('success', res)
+}).catch(err => {
+ console.warn('error', err)
+})
+// Send a message to a worker that has instance ID 'custom-instanceID'. If the instance ID does not match, an error will be thrown
+postMessageHub.emit(workerInstance, { methodName: 'some-method', instanceID: 'custom-instanceID'}, 'arg1', 'arg2', 'otherArgs').then(res => {
+ console.log('success', res)
+}).catch(err => {
+ console.warn('error', err)
})
-storageMessageHub.on({
- 'method1': function () {...},
- 'method2': function (a, b, c) {...}
+// in worker context itself, just set peer to self to send messages to outside
+postMessageHub.emit(self, 'another-method', 'arg1', 'arg2', 'otherArgs').then(res => {
+ console.log('success', res)
+}).catch(err => {
+ console.warn('error', err)
})
-// listen all messages
-anotherStorageMessageHub.on((methodName, ...args) => {
- ...
+// -------------------
+
+// StorageMessageHub doesn't require a `peer` to send messages to, it's isolated in the same window context
+storageMessageHub.emit('async-add', 23, 12).then(res => {
+ console.log('success', res)
+}).catch(err => {
+ console.warn('error', err)
})
-```
-#### progress for storageMessageHub
-If you need progress feedback when peer handling you requests, you can do it by setting the first argument as an object and has a function property named `onprogress` when `emit` messages, and call `onprogress` in `on` on the peer's side.
-e.g.
-```js
-// in normal window, send message to worker, get progress
-// you must get peer's instanceID via `storageMessageHub.getPeerIdentifies` before using it
-storageMessageHub.emit('download', {
- onprogress(p) {console.log('progress: ' + p.count)}
-}).then(e => {
- console.log('success: ', e)
+// -------------------
+
+// PageScriptMessageHub doesn't require a `peer` to send messages to, it's isolated in the same window context
+pageScriptMessageHub.emit('async-add', 223, 89).then(res => {
+ console.log('success', res)
}).catch(err => {
- console.log('error: ' + err)
+ console.warn('error', err)
})
-// listen download from another window that has instance id 'kh9uxd11iyc-kh9uxd11iyc'
-anotherWindowStorageMessageHub.on({
- // download with progress support
- download: (msg) => {
- return new Promise((resolve, reject) => {
- let hiCount = 0
- const tid = setInterval(() => {
- if (hiCount >= 100) {
- clearInterval(tid)
- return resolve('done')
- }
- msg.onprogress({count: hiCount += 10})
- }, 200)
- })
- }
-}
-```
+// -------------------
-#### storageMessageHub.off
-Remove message handlers, if `methodName` presented, remove `methodName`'s listener, or remove the whole peer's listener
+// BroadcastMessageHub doesn't require a `peer` to send messages to, it's required to set the same channelName to communicate(and same-origin in browser)
+broadcastMessageHub.emit('async-add', 223, 89).then(res => {
+ console.log('success', res)
+}).catch(err => {
+ console.warn('error', err)
+})
-```ts
-storageMessageHub.off(methodName?: string)
```
-#### storageMessageHub.destroy
-Destroy instance: remove all message handlers and references of objects.
-Any invoking of destroyed instance's methods will throw an exception
+>[!WARNING]
+> 1. If there are multiple peers listening to the same message, you'll only get the first one who responds; others will be ignored. However, all handlers from different peers will be called.
+> 2. If you want to send a message to a specific peer, you should set the `to` property in the `methodName` object, and the peer must have the same instance ID as the `to` property.
+> 3. If the message has no listener, the promise will be rejected with an error `no corresponding handler found for method ${methodName}`
+> 4. If the handler throws an error, the promise will be rejected with the error thrown by the handler (error object may be lost in some cases due to serialization issues)
+> 5. Please always handle the promise rejection returned by `emit` to avoid unhandled promise warnings
+
+### off
+Remove message handlers.
```ts
-storageMessageHub.destroy()
+// 1. If handler is provided, remove the specific handler.
+// 2. If only methodName is provided, remove all handlers for that methodName.
+// 3. If no methodName is provided, remove all handlers for the peer.
+instance.off(peer: any, methodName?: string, handler?: Function)
```
-### PageScriptMessageHub
-`PageScriptMessageHub` works in browser and use `customEvent` under the hood, it enable you:
-1. communicate between isolated javascript environment in same window context
-2. listen and respond messages with the same code.
+Just like [`on`](#on), the parameter `peer` may be ignored in some instances, e.g., `PageScriptMessageHub`, because it's isolated in the same window context.
-When to use it:
-> 1. when your javascript codes are isolated in same window context, e.g. chrome extension's content script with webpage's js code
-> 2. you need to communicate between them.
-> **3. you may also use it as an event-bus. The difference is that, it create a new client every time when `new`**
-`PageScriptMessageHub` is a class, `new` an instance in every peer before using it
-```js
-import { PageScriptMessageHub } from "duplex-message"
+```ts
+postMessageHub.off(peerWindow, 'some-method', someHandler)
+postMessageHub.off(peerWindow, 'some-method')
+postMessageHub.off(peerWindow)
-const pageScriptMessageHub = new PageScriptMessageHub(options?: IPageScriptMessageHubOptions)
+storageMessageHub.off('async-add', someHandler)
+storageMessageHub.off('async-add')
+storageMessageHub.off()
-interface IPageScriptMessageHubOptions {
- /** custom event name, default: message-hub */
- customEventName?: string
- /** custom instance id for communicating in emit */
- instanceID?: string
-}
-```
+pageScriptMessageHub.off('async-add', someHandler)
+pageScriptMessageHub.off('async-add')
+pageScriptMessageHub.off()
-Tips:
-> in most cases, you only need one instance in one javascript context, you can use
->> `PageScriptMessageHub.shared` instead of new an instance.
-> e.g.:
-> `PageScriptMessageHub.shared.on('xxx', () => {...})`
-> if you want to use it as an event-bus, you should use `new PageScriptMessageHub()` to create clients
+broadcastMessageHub.off('async-add', someHandler)
+broadcastMessageHub.off('async-add')
+broadcastMessageHub.off()
+```
-#### pageScriptMessageHub.emit
-Send a message to peer, invoking `methodName` registered on the peer via [`on`](#pagescriptmessagehubon) with all its arguments `args`:
+### destroy
+Destroy the instance: remove all message handlers and references to objects.
```ts
-// in typescript, use ResponseType to specify response type
-pageScriptMessageHub.emit(methodName: string | IMethodNameConfig, ...args: any[]) => Promise
-
-interface IMethodNameConfig {
- methodName: string
- /** peer's instance id */
- toInstance?: string
-}
+// You can't use the instance after destroy, it will throw an exception.
+instance.destroy()
```
-e.g.
+### progress
+Track the progress of a long-running task.
+
+If you need progress feedback when peer handling you requests, you can do it by setting the first argument as an object and has a function property named `onprogress` when `emit` messages, and call `onprogress` in `on` on the peer's side.
+
```ts
-// in ts
-pageScriptMessageHub
- .emit<{status: string, message: string}>('stop-download')
- .then(res => console.log('status', res.status))
- .catch(err => console.warn('error', err))
-// in js
-pageScriptMessageHub
- .emit('stop-download')
- .then(res => console.log('status', res.status))
- .catch(err => console.warn('error', err))
+// emit message with onprogress callback in the first message argument
+// * the parameter `peer` is only required in PostMessageHub
+instance.emit(peer, methodName: string, {
+ onprogress: (progressData: any) => void
+ ...
+}, ...args: any[])
+
+// listen message and call onprogress if exists in the first message argument
+// * the parameter `instance` is only required in PostMessageHub
+peer.on(instance, methodName: string, async (msg, ...args) => {
+ // call onprogress if exists
+ msg.onprogress && msg.onprogress({progress: 10})
+ // update progress
+ ...
+ await someAsyncFn()
+ return 'done'
+})
```
-Notice:
-1. you should only listen a message once, it will override existing listener when do it again
-2. If there are multi instances listening to the same message, you'll only get the first one who respond, others will be ignored.
-3. look into [Error](#error) when you catch an error
-4. omit args if no args are required, e.g `pageScriptMessageHub.emit('some-method')`
-5. you may need to handle the promise returned by `emit` if some lint tools warning unhandled promise(or floating promise)
+### proxy for PostMessageHub
+PostMessageHub has a `createProxy` method that can forward all messages from `fromWin` to `toWin` then forward `toWin`'s response to the `fromWin`, instead of handle messages by self, it's useful when you want to make two windows communicate directly.
+```ts
+// create proxy message from `fromWin` to `toWin`
+postMessageHub.createProxy(fromWin: Window | Worker, toWin: Window | Worker)
+// stop proxy message from a peer
+postMessageHub.stopProxy(fromWin: Window | Worker)
-#### pageScriptMessageHub.on
-Listen messages sent from peer, it has following forms:
-```ts
-// register(listen)) one handler for methodName when message received from main process
-pageScriptMessageHub.on(methodName: string, handler: Function)
-// register(listen)) multi handlers
-pageScriptMessageHub.on(handlerMap: Record)
-// register only one handler to deal with all messages from process
-pageScriptMessageHub.on(singleHandler: Function)
-```
-e.g.
-```js
-// in renderer process
-pageScriptMessageHub.on('async-add', async function (a, b) {
- return new Promise((resolve, reject) => {
- resolve(a + b)
- })
-})
+// forward all messages from `someWorker` to `someIframeWin`
+postMessageHub.createProxy(someWorker, someIframeWin)
+// then forward all messages from `someIframeWin` to `someWorker`
+postMessageHub.createProxy(someIframeWin, someWorker)
+// with above two lines, `someWorker` and `someIframeWin` can communicate directly, postMessageHub will be a transparent proxy(bridge) between them
-pageScriptMessageHub.on({
- 'method1': function () {...},
- 'method2': function (a, b, c) {...}
-})
+// stop proxy
+postMessageHub.stopProxy(someWorker)
+postMessageHub.stopProxy(someIframeWin)
-// listen all messages from peer with one handler
-anotherPageScriptMessageHub.on((methodName, ...args) => {
- ...
-})
```
-#### progress for PageScriptMessageHub
-If you need progress feedback when peer handling you request, you can do it by setting the first argument as an object and has a function property named `onprogress` when `emit` messages, and call `onprogress` in `on` on the peer's side.
+### dedicated instance for PostMessageHub
+PostMessageHub has a `createDedicatedMessageHub` method that can create a dedicated message-hub for specified peer, so that you won't need to pass peer every time.
-e.g.
-```js
-// listen download from peer
-pageScriptMessageHub.on({
- // download with progress support
- download: (msg) => {
- return new Promise((resolve, reject) => {
- let hiCount = 0
- const tid = setInterval(() => {
- if (hiCount >= 100) {
- clearInterval(tid)
- return resolve('done')
- }
- // send feedback by calling onprogress if it exists
- msg && msg.onprogress && msg.onprogress({count: hiCount += 10})
- }, 200)
- })
- }
+```ts
+
+/**
+* @param peer peer window to communicate with, or you can set it later via `setPeer`
+* @param silent when peer not exists, keep silent instead of throw an error when call emit, on, off
+*/
+const dedicatedMessageHub = postMessageHub.createDedicatedMessageHub (peer?: Window | Worker, silent?: boolean) => IDedicatedMessageHub
+
+interface IDedicatedMessageHub {
+ /** if you didn't set a peer when invoking createDedicatedMessageHub, then you can use `setPeer` to set it when it's ready*/
+ setPeer: (peer: Window | Worker) => void;
+ // in typescript, use ResponseType to specify response type
+ emit: (methodName: string, ...args: any[]) => Promise;
+ on: (methodName: string, handler: Function) => void;
+ on: (handlerMap: Record) => void;
+ off: (methodName?: string) => any;
}
-// in the peer's code
-peerPageScriptMessageHub.emit('download', {
- onprogress(p) {console.log('progress: ' + p.count)}
-}).then(e => {
- console.log('success: ', e)
+dedicatedMessageHub.emit('some-method', 'arg1', 'arg2').then(res => {
+ console.log('success', res)
}).catch(err => {
- console.log('error: ' + err)
+ console.warn('error', err)
})
-```
-
-#### pageScriptMessageHub.off
-Remove message handlers, if `methodName` presented, remove `methodName`'s listener, or remove the whole peer's listener
-```ts
-// in renderer process
-pageScriptMessageHub.off(methodName?: string)
+dedicatedMessageHub.on('some-method', console.log)
+dedicatedMessageHub.off('some-method')
```
-#### pageScriptMessageHub.destroy
-Destroy instance: remove all message handlers and references of objects.
-Any invoking of destroyed instance's methods will throw an exception
-
-```ts
-pageScriptMessageHub.destroy()
-```
### Error
when you catch an error from `emit`, it conforms the following structure `IError`
@@ -733,11 +567,23 @@ enum EErrorCode {
}
```
-### Debug
-if you has some issues when using this lib, you can enable debug mode to see debug logs in console:
+## Development
-```ts
-import { setConfig } from 'duplex-message'
-setConfig({ debug: true })
+```sh
+# install dependencies, exec in root of the repo
+pnpm install
+# dev
+pnpm dev
+
+#!! make sure install chromium before running playwright tests by following command
+pnpm playwright install chromium
+
+# test
+pnpm test
+
+# build
+pnpm build
```
+## License
+MIT
diff --git a/packages/duplex-message/build.config.ts b/packages/duplex-message/build.config.ts
new file mode 100644
index 0000000..8d6750c
--- /dev/null
+++ b/packages/duplex-message/build.config.ts
@@ -0,0 +1,38 @@
+import { build, UserConfig } from 'vite'
+import dts from 'vite-plugin-dts'
+
+const defaultConfig: UserConfig = {
+ plugins: [dts({
+ rollupTypes: true,
+ exclude: ['test/**', 'demo/**'],
+ })],
+ build: {
+ lib: {
+ entry: 'src/index.ts',
+ name: 'duplex-message',
+ formats: ['umd', 'es'],
+ fileName: (format) => (format === 'umd' ? 'index.umd.js' : 'index.es.js'),
+ },
+ },
+}
+
+const productionConfig: UserConfig = {
+ build: {
+ emptyOutDir: false,
+ // @ts-ignore
+ lib: {
+ ...defaultConfig.build!.lib,
+ fileName: (format) => (format === 'umd' ? 'index.production.umd.js' : 'index.production.es.js')
+ }
+ },
+ define: {
+ 'process.env.NODE_ENV': '"production"',
+ }
+}
+
+async function main() {
+ await build(defaultConfig)
+ await build(productionConfig)
+}
+
+main()
diff --git a/packages/duplex-message/test/worker/index.html b/packages/duplex-message/demo/broadcast/index.html
similarity index 96%
rename from packages/duplex-message/test/worker/index.html
rename to packages/duplex-message/demo/broadcast/index.html
index a3df341..ac8b189 100644
--- a/packages/duplex-message/test/worker/index.html
+++ b/packages/duplex-message/demo/broadcast/index.html
@@ -54,7 +54,7 @@
You can reveal the devtools console to see extra message
send hi to worker, you can see message in devtools console
-
+