Comcat
Share single connection between multiple browser tabs/windows and more.
Install / Use
/learn @afterwind-io/ComcatREADME
:cat:Comcat
[![Version][version-badge]][npm] ![License][license-badge]
<!-- ![Downloads][download-badge] -->Share single connection between multiple browser tabs/windows and more.
Introduction
This library is currently aimed to solve a common problem:
I want to consume some messages pushed by the backend, but ...
- I don't want to create a new connection every time I open a new tab/window;
- Or, I want a single tab to receive the messages, and shared them between other tabs.
Comcat offers some critical features around this topic, including:
- Broadcast messages to all tabs;
- Keep one and only one connection alive across tabs;
With the unique characteristics of [SharedWorker][mdn-sharedworker], Comcat can automatically reconnect if the tab owns the connection is closed or even crashed. If you are keen on how it is accomplished, please refer to How it works.
Disclaimer
Comcat guarantees eliminating duplicate connections or messages, but does not guarantee the integrity of all incoming messages. That means, messages may be lost in certain edge cases.
If it is a major concern over the message integrity, Comcat may not be suit to your app. Relevant details are discussed in Caveats.
Get Started
Install
npm install comcat
OR
yarn add comcat
Usage
For demonstration purpose, here we implement a minimal example to share time information across tabs.
//examplePump.ts
import { ComcatPump } from 'comcat';
const POLLING_INTERVAL = 60 * 1000;
let intervalHandler = -1;
/**
* First we create a `pump` to pull messages from the backend.
*/
const pump = new ComcatPump({
category: 'example',
});
/**
* Define how we connect to the backend.
*/
pump.onConnect = () => {
intervalHandler = setInterval(() => {
fetch('http://worldtimeapi.org/api/ip')
.then((res) => {
return res.json();
})
.then((data) => {
/**
* When messages come, push them to the consumer...
* ...with a customized topic.
*/
pump.pump('Time', data.datetime);
pump.pump('Unix', data.unixtime);
});
}, POLLING_INTERVAL);
};
/**
* Define how we disconnect from it.
*/
pump.onDisconnect = () => {
clearInterval(intervalHandler);
};
/**
* Start the engine!
*/
pump.start();
// examplePipe.ts
import { ComcatPipe } from 'comcat';
/**
* Then we create a `pipe` to receive messages from `pump`s.
*/
const pipe = new ComcatPipe({
/**
* Choose the topic we care about...
*/
topic: 'Time',
});
pipe.onMessage = (topic, data) => {
/**
* ...And do something with the messages.
*/
console.log('The current time is: ', data);
};
/**
* Start rolling!
*/
pipe.start();
/**
* Don't forget to dispose the pipe when it is no longer used.
*/
// pipe.stop();
The Concepts
Overview

Pump
Pump behaves like a "Message Provider". It is responsible for creating the connection to the backend and then feed the messages to the further consumer.
You have full control of how to connect, and disconnect, to the source of the message, often the server. Moreover, it is up to you to determine when and what to send. Comcat only manages the timing of connection and disconnection for you.
If needed, you can create multiple Pumps to deal with various sources. Comcat
uses category to identify different groups of pumps. There is only one active connection in each group, thus the tabs own the active connections may not be the same.
Pipe
Pipe is the "Message Receiver". It notifies the user when the message arrives, and is meant to be a intermediary between Comcat and the consumer.
Pipe provides basic filtering based on topic, but you can choose whether to accept the incoming message, or even modify the content before
it is pushed to the consumer.
Recipes
The repository contains several examples covering the basic usage. Please see [example/README.md][example-readme]
APIs
ComcatPump
The base class for constructing Comcat pumps.
A typical customized pump looks like this:
// myPump.ts
import { ComcatPump } from 'comcat';
const pump = new ComcatPump({
category: 'MyCategory',
});
pump.onConnect = () => {
/**
* Do the connection here.
*
* ...and some other works maybe
*/
};
pump.onDisconnect = () => {
/**
* Do the disconnection here.
*/
};
new ComcatPump(options)
public constructor(options: ComcatPumpOptions);
interface ComcatPumpOptions {
category: string;
}
category:
An identifier to classify different message sources.
Each category is coupled with only one type of connection, so you can not create multiple pumps with same category.
ComcatPump.start
public start: () => Promise<boolean>;
Register the pump and try to start the underlying connection.
Because the connection is managed by Comcat, it may be postponed until scheduled.
Returns true if registry succeeds, or vice versa.
ComcatPump.stop
public stop: () => void;
Close the pump and the underlying connection.
In practice, Comcat will close the pump when the current tab is closed, so usually you wont need to trigger this by hand.
If somehow you still want to do it yourself, please note that once the pump is closed, it is fully disposed and cannot be started again. In order to restarting a new pump with the same category, instantiate a new ComcatPump.
ComcatPump.onConnect
public onConnect: () => Promise<boolean> | void;
:warning: The default method is only a placeholder. Always override with your own callback.
Invoked when Comcat tries to connect to your backend. Basically your connection code goes here.
You can fine-tune the inner behavior by returning a flag indicates whether the connection is successful. If the return value is false, or an error is raised, Comcat will either retry the connection after a short period of time, or schedule another tab to do the job. If no value is returned, Comcat will treat the result as successful anyway.
ComcatPump.onDisconnect
public onDisconnect: () => void;
:warning: The default method is only a placeholder. Always override with your own callback.
Invoked when Comcat tries to disconnect to your backend. Basically your disconnection code goes here.
Don't permanently dispose anything here, because your pump may be rescheduled connecting again.
ComcatPump.pump
public pump: (topic: string, data: any) => Promise<void>;
Send the message with a specified topic.
topic:
The category of the message. It is used to help filtering messages in different aspects.
data:
The content of the message. Can be anything that SharedWorker supports, but with some restrictions. Please see [Transferring data to and from workers: further details][mdn-transfer].
ComcatPipe
The base class for constructing Comcat pipes.
A typical customized pipe looks like this:
import { ComcatPipe } from 'comcat';
const pipe = new ComcatPipe({
topic: 'MyTopic',
});
pipe.onMessage = (topic, data) => {
/**
* Do some works with the data.
*/
};
new ComcatPipe(options)
public constructor(options?: ComcatPipeOptions);
interface ComcatPipeOptions {
topic?: string | RegExp;
}
topic: [optional]
The expected category of the messages. It can be either string or RegExp. If applied, the incoming message is filtered unless its topic exactly matches the provided string, or passes the RegExp test.
ComcatPipe.start
public start: () => Promise<boolean>;
Register the pipe and start listening for the messages from the upstream.
Returns true if registry succeeds, or vice versa.
ComcatPipe.stop
public stop: () => void;
Unregister the pipe and stop listening for the messages.
It is strongly recommended that to prevent potential memory leaks, pipes should be closed immediately when they are no longer in use.
ComcatPipe.onMessage
public onMessage: (topic: string, data: any) => void;
:warning: The default method is only a placeholder. Always override with your own callback.
Invoked when messages arrive.
Note that the messages arrives here have already been filtered by the topic provided in construction options.
topic:
The topic of the message;
data:
The content of the message;
Comcat
Provides global settings that alter how Comcat works.
Comcat.setMode
public setMode: (mode: 'default' | 'direct' = 'default') => void;
Specify the underlying implementation.
By default Comcat uses SharedWebworker to share connection and send messages across tabs/windows. If SharedWebworker is not supported, Comcat will fall back to the direct mode.
When running in direct Mode, all cross-tab features are disabled. The connection activated by pump is created per tab. The messages sent by pump are broadcasted back to the pipes on the same tab. Thus, it behaves just like a normal event bus.
Usually you should just leave it be.
import { Comcat } from 'comcat';
Comcat.setMode('direct');
Comcat.enableDebug
public enableDebug: (flag: boolean) => void;
Determines whether enabling the full debug logging, including inner status and transport information.
Comcat will log every message through the transport, and some basic status information to the console. By default, these output is suppressed.
You can enable the full log like following:
import { Comcat } from 'comcat';
Comcat.enableDebug(true);
Be careful, this may output enormous content. However, the logs from the worker is always com
