@toruslabs/broadcast-channel
Version:
A BroadcastChannel that works in New Browsers, Old Browsers, WebWorkers
156 lines (149 loc) • 5.9 kB
JavaScript
import _objectSpread from '@babel/runtime/helpers/objectSpread2';
import _defineProperty from '@babel/runtime/helpers/defineProperty';
import { BroadcastChannel } from './broadcast-channel.js';
import { type as type$1 } from './methods/localstorage.js';
import { type } from './methods/native.js';
import { type as type$2 } from './methods/server.js';
import { type as type$3 } from './methods/simulate.js';
import { log } from './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 = [type, type$1, type$2];
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 === type$3) {
this.methodPriority = [type$3];
}
// iterates through the methodPriority array, attempting to create a new BroadcastChannel for each method
this.methodPriority.forEach(method => {
try {
const channel = new BroadcastChannel(this.name, _objectSpread(_objectSpread({}, this.options), {}, {
type: method
}));
this.channels.set(method, channel);
log.debug(`Succeeded to initialize ${method} method in channel ${this.name}`);
// listening on every method
channel.onmessage = event => this.handleMessage(event);
} catch (error) {
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 => {
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;
}
}
export { RedundantAdaptiveBroadcastChannel };