@toruslabs/broadcast-channel
Version:
A BroadcastChannel that works in New Browsers, Old Browsers, WebWorkers
154 lines (150 loc) • 6 kB
JavaScript
'use strict';
var _objectSpread = require('@babel/runtime/helpers/objectSpread2');
var _defineProperty = require('@babel/runtime/helpers/defineProperty');
var broadcastChannel = require('./broadcast-channel.js');
var localstorage = require('./methods/localstorage.js');
var native = require('./methods/native.js');
var server = require('./methods/server.js');
var simulate = require('./methods/simulate.js');
var util = require('./util.js');
/**
* The RedundantAdaptiveBroadcastChannel class is designed to add fallback to during channel post message and synchronization issues between senders and receivers in a broadcast communication scenario. It achieves this by:
* Creating a separate channel for each communication method, allowing all methods to listen simultaneously.
* Implementing redundant message delivery by attempting to send messages through multiple channels when the primary channel fails.
* Ensuring message delivery by using multiple communication methods simultaneously while preventing duplicate message processing.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
class RedundantAdaptiveBroadcastChannel {
constructor(name, options = {}) {
_defineProperty(this, "name", void 0);
_defineProperty(this, "options", void 0);
_defineProperty(this, "closed", void 0);
_defineProperty(this, "onML", void 0);
_defineProperty(this, "methodPriority", void 0);
_defineProperty(this, "channels", void 0);
_defineProperty(this, "listeners", void 0);
_defineProperty(this, "processedNonces", void 0);
_defineProperty(this, "nonce", void 0);
this.name = name;
this.options = options;
this.closed = false;
this.onML = null;
// order from fastest to slowest
this.methodPriority = [native.type, localstorage.type, server.type];
this.channels = new Map();
this.listeners = new Set();
this.processedNonces = new Set();
this.nonce = 0;
this.initChannels();
}
set onmessage(fn) {
this.removeEventListener("message", this.onML);
if (fn && typeof fn === "function") {
this.onML = fn;
this.addEventListener("message", fn);
} else {
this.onML = null;
}
}
initChannels() {
// only use simulate if type simulate ( for testing )
if (this.options.type === simulate.type) {
this.methodPriority = [simulate.type];
}
// iterates through the methodPriority array, attempting to create a new BroadcastChannel for each method
this.methodPriority.forEach(method => {
try {
const channel = new broadcastChannel.BroadcastChannel(this.name, _objectSpread(_objectSpread({}, this.options), {}, {
type: method
}));
this.channels.set(method, channel);
util.log.debug(`Succeeded to initialize ${method} method in channel ${this.name}`);
// listening on every method
channel.onmessage = event => this.handleMessage(event);
} catch (error) {
util.log.warn(`Failed to initialize ${method} method in channel ${this.name}: ${error instanceof Error ? error.message : String(error)}`);
}
});
if (this.channels.size === 0) {
throw new Error("Failed to initialize any communication method");
}
}
allChannels() {
return Array.from(this.channels.keys());
}
hasChannel(method) {
return this.channels.has(method);
}
handleMessage(event) {
if (event && event.nonce) {
if (this.processedNonces.has(event.nonce)) {
// log.debug(`Duplicate message received via ${method}, nonce: ${event.nonce}`);
return;
}
this.processedNonces.add(event.nonce);
// Cleanup old nonces (keeping last 1000 to prevent memory issues)
if (this.processedNonces.size > 1000) {
const nonces = Array.from(this.processedNonces);
const oldestNonce = nonces.sort()[0];
this.processedNonces.delete(oldestNonce);
}
this.listeners.forEach(listener => {
listener(event.message);
});
}
}
async postMessage(message) {
if (this.closed) {
throw new Error("AdaptiveBroadcastChannel.postMessage(): " + `Cannot post message after channel has closed ${
/**
* In the past when this error appeared, it was realy hard to debug.
* So now we log the msg together with the error so it at least
* gives some clue about where in your application this happens.
*/
JSON.stringify(message)}`);
}
const nonce = this.generateNonce();
const wrappedMessage = {
nonce,
message
};
const postPromises = Array.from(this.channels.entries()).map(([method, channel]) => channel.postMessage(wrappedMessage).catch(error => {
util.log.warn(`Failed to send via ${method}: ${error.message}`);
throw error;
}));
const result = await Promise.allSettled(postPromises);
// Check if at least one promise resolved successfully
const anySuccessful = result.some(p => p.status === "fulfilled");
if (!anySuccessful) {
throw new Error("Failed to send message through any method");
}
return message;
}
generateNonce() {
return `${Date.now()}-${this.nonce++}`;
}
addEventListener(_type, listener) {
// type params is not being used, it's there to keep same interface as BroadcastChannel
this.listeners.add(listener);
}
removeEventListener(_type, listener) {
// type params is not being used, it's there to keep same interface as BroadcastChannel
this.listeners.delete(listener);
}
async close() {
if (this.closed) {
return;
}
this.onML = null;
// use for loop instead of channels.values().map because of bug in safari Map
const promises = [];
for (const c of this.channels.values()) {
promises.push(c.close());
}
await Promise.all(promises);
this.channels.clear();
this.listeners.clear();
this.closed = true;
}
}
exports.RedundantAdaptiveBroadcastChannel = RedundantAdaptiveBroadcastChannel;