Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fallback broadcast channel #19

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const configuration = {
basePath: '',
frameworks: [
'mocha',
'sinon',
'browserify',
'detectBrowsers'
],
Expand Down Expand Up @@ -38,6 +39,7 @@ const configuration = {
// Karma plugins loaded
plugins: [
'karma-mocha',
'karma-sinon',
'karma-browserify',
'karma-chrome-launcher',
'karma-edge-launcher',
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,14 @@
"karma-mocha": "2.0.1",
"karma-opera-launcher": "1.0.0",
"karma-safari-launcher": "1.0.0",
"karma-sinon": "^1.0.5",
"mocha": "10.4.0",
"pre-commit": "1.2.2",
"random-int": "3.0.0",
"random-token": "0.0.8",
"rimraf": "^5.0.7",
"rollup": "4.18.0",
"sinon": "^19.0.2",
"testcafe": "3.6.1",
"ts-node": "10.9.2",
"typescript": "5.4.5",
Expand Down
14 changes: 7 additions & 7 deletions src/broadcast-channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@ BroadcastChannel.prototype = {
if (this.closed) {
throw new Error(
'BroadcastChannel.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(msg)
'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(msg)
);
}
return _post(this, 'message', msg);
Expand Down
2 changes: 2 additions & 0 deletions src/index-umd.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { BroadcastChannel } from './broadcast-channel';
import { RedundantAdaptiveBroadcastChannel } from './redundant-adaptive-broadcast-channel';
import { encode, decode, toBase64, fromBase64, toBuffer } from 'base64url';

if (typeof window !== 'undefined') {
window.broadcastChannelLib = {};
window.broadcastChannelLib.BroadcastChannel = BroadcastChannel;
window.broadcastChannelLib.RedundantAdaptiveBroadcastChannel = RedundantAdaptiveBroadcastChannel;
window.base64urlLib = {};
window.base64urlLib.encode = encode;
window.base64urlLib.decode = decode;
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import * as ServerMethod from './methods/server';

export { BroadcastChannel, enforceOptions, OPEN_BROADCAST_CHANNELS } from './broadcast-channel';
export * from './method-chooser';
export { RedundantAdaptiveBroadcastChannel } from './redundant-adaptive-broadcast-channel';
export { NativeMethod, IndexedDbMethod, LocalstorageMethod, ServerMethod };
150 changes: 150 additions & 0 deletions src/redundant-adaptive-broadcast-channel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { BroadcastChannel } from './broadcast-channel';
import * as NativeMethod from './methods/native.js';
import * as IndexedDbMethod from './methods/indexed-db.js';
import * as LocalstorageMethod from './methods/localstorage.js';
import * as ServerMethod from './methods/server.js';
import * as SimulateMethod from './methods/simulate.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.
*/
export class RedundantAdaptiveBroadcastChannel {
constructor(name, options = {}) {
this.name = name;
this.options = options;
this.closed = false;
this.onML = null;
// order from fastest to slowest
this.methodPriority = [NativeMethod.type, IndexedDbMethod.type, LocalstorageMethod.type, ServerMethod.type];
this.channels = new Map();
this.listeners = new Set();
this.processedNonces = new Set();
this.nonce = 0;
this.initChannels();
}

initChannels() {
// only use simulate if type simulate ( for testing )
if (this.options.type === SimulateMethod.type) {
this.methodPriority = [SimulateMethod.type];
}

// 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, {
...this.options,
type: method
});
this.channels.set(method, channel);
// listening on every method
channel.onmessage = (event) => this.handleMessage(event, method);
} catch (error) {
console.warn(`Failed to initialize ${method} method: ${error.message}`);
}
});

if (this.channels.size === 0) {
throw new Error('Failed to initialize any communication method');
}
}

handleMessage(event) {
if (event && event.nonce) {
if (this.processedNonces.has(event.nonce)) {
// console.log(`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 oldestNonce = Math.min(...this.processedNonces);
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 => {
console.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');
}
}

generateNonce() {
return `${Date.now()}-${this.nonce++}`;
}

set onmessage(fn) {
this.removeEventListener('message', this.onML);
if (fn && typeof fn === 'function') {
this.onML = fn;
this.addEventListener('message', fn);
} else {
this.onML = null;
}
}

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;
}
}
Loading
Loading