From time to time, developers need to establish communication between several browser tabs to be able to send messages from one tab to another and receive responses. We have also faced this need at some point.

Some solutions already exist (like, for instance, BroadcastChannel API). However, its browser support leaves a lot to be desired, so we decided to use our own library. When the library was ready, that functionality was no longer required. Nevertheless, another task emerged: communication between an iframe and the main window.

On closer examination, it turned out that two-thirds of the library would not have to be changed — only some code refactoring was necessary. The library is a communication PROTOCOL that can work with text data. It can be applied in all cases in which text is transferred, such as iframes, window.open, worker, browser tabs or WebSocket.

How it works

Currently, the protocol has two functions: sending messages and subscription to events. Any message in the protocol is a data object. For us, the main field in that object is type, which tells us what kind of message it is. The type field is an enum with the following values:

0 — sending a message

1 — sending a request

2 — receiving a response.

Sending a message

Sending a message doesn't imply a response. To send a message, we create an object with the following fields:

type — event type 0

— event type 0 name — user event name

— user event name data — user data (JSON-like).

On receiving a message on the other side with the type field = 0, we know it is an event, with an existing event name and data. All we have to do is broadcast the event (almost a standard EventEmitter pattern).

How it works in a simple schema:

Sending a request

Sending a request implies that within the library, a request id is created and the library will wait for a response with the id. Upon successfully receiving a response, all auxiliary fields will be removed from it, and the response will be returned to the user. Also, the maximum response time can be set.

As for requests, this is a bit more complicated. To respond to a request, you need to announce the methods that are available in our protocol. This is done with the registerRequestHandler method. It accepts the name of a request for a response and a function that returns the response. To create a request, we need an id, and we can basically use timestamp, but it is not very convenient to adjust. So, this is a class ID that sends a response + response number + string literal. Now we create an object with the following fields: id, type = 1, name as request name and data as user data (JSON-like).

On receiving a request, we check if we have an API for responding to this request, and if we don't, we return an error message. If we have an API, we return the result of executing the function from registerRequestHandler, with the respective request name.

For the response, we create an object with the fields: type = 2, id as the ID of the message to which we respond, status as a field that says if this response is an error (if we don't have an API, or the handler incurred an error, or the user returned a rejected promise, or another error occurs (serialise)), and content as response data.

So, we have described the operation of the protocol, which executes the Bus class but has not explained the process of sending and receiving messages. For that, we need class adapters with three methods.

send is a method that is basically responsible for sending messages

is a method that is basically responsible for sending messages addListener is a method for subscribing to events

is a method for subscribing to events destroy is a method for deleting subscriptions when deleting Bus.

Adapters. Execution of the protocol

To launch the protocol, currently, only the adapter for working with iframe/window is ready. It uses postMessage and addEventListener. It's pretty straightforward: you need to send a message to postMessage with a correct origin and listen to messages over addEventListener on the "message" event.

We encountered a few nuances:

You should always listen to responses on YOUR window and send them on the OTHER window (iframe, opener, parent, worker, etc). If you try to listen to a message on the OTHER window and the origin differs from the current one, an error will occur.

On receiving a message, make sure that it was directed to you: the window can accommodate many analytics messages, WebStorm (if you use it) and other iframes, so you need to be sure the event is in your protocol and intended for you.

You can't return a promise with a Window copy, because promise when returning the result, will try to check if the result has the then method. If you don't have access to the window (for instance, a window with another origin), an error will occur (although not in all browsers). To avoid this issue, it would be enough to wrap the window in the object and put an object into the promise that has a link to the correct window.

Usage examples:

The library is available in NPM and you can easily install it via your package manager — @waves/waves-browser-bus

To establish two-way communication with an iframe, it is enough to use this code:

import { Bus, WindowAdapter } from '@waves/waves-browser-bus'; const url = 'https://some-iframe-content-url.com'; const iframe = document.createElement('iframe'); WindowAdapter.createSimpleWindowAdapter(iframe).then(adapter => { const bus = new Bus(adapter); bus.once('ready', () => { // A message from iframe received }); }); iframe.src = url; // It's preferable to assign a url after calling WindowAdapter.createSimpleWindowAdapter document.body.appendChild(iframe);

Inside iframe:

import { Bus, WindowAdapter } from '@waves/waves-browser-bus'; WindowAdapter.createSimpleWindowAdapter().then(adapter => { const bus = new Bus(adapter); bus.dispatchEvent('ready', null); // A message has been sent to the parent window });

What's next?

We have a flexible and versatile protocol that can be used in any situation. Next, I plan to separate the adapters from the protocol and put them into separate npm packages, and add adapters for worker and browser tabs. I want writing adapters executing the protocol for any other purposes to be as easy as possible. If you want to join the development process or have ideas regarding the library's functionality, you are welcome to get in touch in the repo.