UNPKG

@toruslabs/broadcast-channel

Version:

A BroadcastChannel that works in New Browsers, Old Browsers, WebWorkers

154 lines (150 loc) 6 kB
'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;