@toruslabs/broadcast-channel
Version:
A BroadcastChannel that works in New Browsers, Old Browsers, WebWorkers
1,420 lines (1,374 loc) • 664 kB
JavaScript
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
'use strict';
var _defineProperty = require('@babel/runtime/helpers/defineProperty');
var methodChooser = require('./method-chooser.js');
var options = require('./options.js');
var util = require('./util.js');
let ENFORCED_OPTIONS;
function enforceOptions(options) {
ENFORCED_OPTIONS = options;
}
/**
* Contains all open channels,
* used in tests to ensure everything is closed.
*/
const OPEN_BROADCAST_CHANNELS = new Set();
let lastId = 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
class BroadcastChannel {
// beforeClose
constructor(name, options$1) {
_defineProperty(this, "id", void 0);
_defineProperty(this, "name", void 0);
_defineProperty(this, "options", void 0);
_defineProperty(this, "method", void 0);
_defineProperty(this, "closed", void 0);
_defineProperty(this, "_addEL", void 0);
_defineProperty(this, "_prepP", void 0);
// preparePromise
_defineProperty(this, "_state", void 0);
_defineProperty(this, "_uMP", void 0);
// unsent message promises
_defineProperty(this, "_iL", void 0);
// isListening
_defineProperty(this, "_onML", void 0);
// onMessageListener
_defineProperty(this, "_befC", void 0);
this.id = lastId++;
OPEN_BROADCAST_CHANNELS.add(this);
this.name = name;
if (ENFORCED_OPTIONS) {
options$1 = ENFORCED_OPTIONS;
}
this.options = options.fillOptionsWithDefaults(options$1 || {});
this.method = methodChooser.chooseMethod(this.options);
this.closed = false;
this._iL = false;
this._onML = null;
this._addEL = {
message: [],
internal: []
};
this._uMP = new Set();
this._befC = [];
this._prepP = null;
_prepareChannel(this);
}
get type() {
return this.method.type;
}
get isClosed() {
return this.closed;
}
set onmessage(fn) {
const time = this.method.microSeconds();
const listenObj = {
time,
fn: fn
};
_removeListenerObject(this, "message", this._onML);
if (fn && typeof fn === "function") {
this._onML = listenObj;
_addListenerObject(this, "message", listenObj);
} else {
this._onML = null;
}
}
postMessage(msg) {
if (this.closed) {
throw new Error(`BroadcastChannel.postMessage(): Cannot post message after channel has closed ${JSON.stringify(msg)}`);
}
return _post(this, "message", msg);
}
postInternal(msg) {
return _post(this, "internal", msg);
}
addEventListener(type, fn) {
const time = this.method.microSeconds();
const listenObj = {
time,
fn: fn
};
_addListenerObject(this, type, listenObj);
}
removeEventListener(type, fn) {
const obj = this._addEL[type].find(o => o.fn === fn);
_removeListenerObject(this, type, obj);
}
close() {
if (this.closed) {
return Promise.resolve();
}
OPEN_BROADCAST_CHANNELS.delete(this);
this.closed = true;
const awaitPrepare = this._prepP ? this._prepP : util.PROMISE_RESOLVED_VOID;
this._onML = null;
this._addEL.message = [];
return awaitPrepare.then(() => Promise.all(Array.from(this._uMP))).then(() => Promise.all(this._befC.map(fn => fn()))).then(() => this.method.close ? this.method.close(this._state) : util.PROMISE_RESOLVED_VOID);
}
}
_defineProperty(BroadcastChannel, "_pubkey", true);
function _post(broadcastChannel, type, msg) {
const time = broadcastChannel.method.microSeconds();
const msgObj = {
time,
type,
data: msg
};
const awaitPrepare = broadcastChannel._prepP ? broadcastChannel._prepP : util.PROMISE_RESOLVED_VOID;
return awaitPrepare.then(() => {
const sendPromise = broadcastChannel.method.postMessage(broadcastChannel._state, msgObj);
broadcastChannel._uMP.add(sendPromise);
// eslint-disable-next-line promise/catch-or-return
sendPromise.catch(() => {}).then(() => broadcastChannel._uMP.delete(sendPromise));
return sendPromise;
});
}
function _prepareChannel(channel) {
const maybePromise = channel.method.create(channel.name, channel.options);
if (util.isPromise(maybePromise)) {
const promise = maybePromise;
channel._prepP = promise;
promise.then(s => {
channel._state = s;
return s;
}).catch(err => {
throw err;
});
} else {
channel._state = maybePromise;
}
}
function _hasMessageListeners(channel) {
if (channel._addEL.message.length > 0) return true;
if (channel._addEL.internal.length > 0) return true;
return false;
}
function _startListening(channel) {
if (!channel._iL && _hasMessageListeners(channel)) {
const listenerFn = msgObj => {
channel._addEL[msgObj.type].forEach(listenerObject => {
if (msgObj.time >= listenerObject.time) {
listenerObject.fn(msgObj.data);
} else if (channel.method.type === "server") {
listenerObject.fn(msgObj.data);
}
});
};
const time = channel.method.microSeconds();
if (channel._prepP) {
channel._prepP.then(() => {
channel._iL = true;
channel.method.onMessage(channel._state, listenerFn, time);
return true;
}).catch(err => {
throw err;
});
} else {
channel._iL = true;
channel.method.onMessage(channel._state, listenerFn, time);
}
}
}
function _stopListening(channel) {
if (channel._iL && !_hasMessageListeners(channel)) {
channel._iL = false;
const time = channel.method.microSeconds();
channel.method.onMessage(channel._state, null, time);
}
}
function _addListenerObject(channel, type, obj) {
channel._addEL[type].push(obj);
_startListening(channel);
}
function _removeListenerObject(channel, type, obj) {
if (obj) {
channel._addEL[type] = channel._addEL[type].filter(o => o !== obj);
_stopListening(channel);
}
}
exports.BroadcastChannel = BroadcastChannel;
exports.OPEN_BROADCAST_CHANNELS = OPEN_BROADCAST_CHANNELS;
exports.enforceOptions = enforceOptions;
},{"./method-chooser.js":3,"./options.js":9,"./util.js":11,"@babel/runtime/helpers/defineProperty":12}],2:[function(require,module,exports){
'use strict';
var indexedDb = require('./methods/indexed-db.js');
var localstorage = require('./methods/localstorage.js');
var native = require('./methods/native.js');
var server = require('./methods/server.js');
var broadcastChannel = require('./broadcast-channel.js');
var methodChooser = require('./method-chooser.js');
var redundantAdaptiveBroadcastChannel = require('./redundant-adaptive-broadcast-channel.js');
exports.IndexedDbMethod = indexedDb;
exports.LocalstorageMethod = localstorage;
exports.NativeMethod = native;
exports.ServerMethod = server;
exports.BroadcastChannel = broadcastChannel.BroadcastChannel;
exports.OPEN_BROADCAST_CHANNELS = broadcastChannel.OPEN_BROADCAST_CHANNELS;
exports.enforceOptions = broadcastChannel.enforceOptions;
exports.chooseMethod = methodChooser.chooseMethod;
exports.RedundantAdaptiveBroadcastChannel = redundantAdaptiveBroadcastChannel.RedundantAdaptiveBroadcastChannel;
},{"./broadcast-channel.js":1,"./method-chooser.js":3,"./methods/indexed-db.js":4,"./methods/localstorage.js":5,"./methods/native.js":6,"./methods/server.js":7,"./redundant-adaptive-broadcast-channel.js":10}],3:[function(require,module,exports){
'use strict';
var indexedDb = require('./methods/indexed-db.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');
// order is important
const METHODS = [native,
// fastest
indexedDb, localstorage, server];
function chooseMethod(options) {
let chooseMethods = [].concat(options.methods || [], METHODS).filter(Boolean);
// directly chosen
if (options.type) {
if (options.type === "simulate") {
// only use simulate-method if directly chosen
return simulate;
}
const ret = chooseMethods.find(m => m.type === options.type);
if (!ret) throw new Error(`method-type ${options.type} not found`);else return ret;
}
/**
* if no webworker support is needed,
* remove idb from the list so that localstorage is been chosen
*/
if (!options.webWorkerSupport) {
chooseMethods = chooseMethods.filter(m => m.type !== "idb");
}
const useMethod = chooseMethods.find(method => method.canBeUsed(options));
if (!useMethod) throw new Error(`No useable method found in ${JSON.stringify(METHODS.map(m => m.type))}`);else return useMethod;
}
exports.chooseMethod = chooseMethod;
},{"./methods/indexed-db.js":4,"./methods/localstorage.js":5,"./methods/native.js":6,"./methods/server.js":7,"./methods/simulate.js":8}],4:[function(require,module,exports){
'use strict';
var util = require('../util.js');
var obliviousSet = require('oblivious-set');
var options = require('../options.js');
/**
* this method uses indexeddb to store the messages
* There is currently no observerAPI for idb
* @link https://github.com/w3c/IndexedDB/issues/51
*
* When working on this, ensure to use these performance optimizations:
* @link https://rxdb.info/slow-indexeddb.html
*/
const microSeconds = util.microSeconds;
const DB_PREFIX = "pubkey.broadcast-channel-0-";
const OBJECT_STORE_ID = "messages";
/**
* Use relaxed durability for faster performance on all transactions.
* @link https://nolanlawson.com/2021/08/22/speeding-up-indexeddb-reads-and-writes/
*/
const TRANSACTION_SETTINGS = {
durability: "relaxed"
};
const type = "idb";
function getIdb() {
if (typeof indexedDB !== "undefined") return indexedDB;
if (typeof window !== "undefined") {
const extWindow = window;
if (typeof extWindow.mozIndexedDB !== "undefined") return extWindow.mozIndexedDB;
if (typeof extWindow.webkitIndexedDB !== "undefined") return extWindow.webkitIndexedDB;
if (typeof extWindow.msIndexedDB !== "undefined") return extWindow.msIndexedDB;
}
return false;
}
/**
* If possible, we should explicitly commit IndexedDB transactions
* for better performance.
* @link https://nolanlawson.com/2021/08/22/speeding-up-indexeddb-reads-and-writes/
*/
function commitIndexedDBTransaction(tx) {
if (tx.commit) {
tx.commit();
}
}
function createDatabase(channelName) {
const IndexedDB = getIdb();
if (!IndexedDB) return Promise.reject(new Error("IndexedDB not available"));
// create table
const dbName = DB_PREFIX + channelName;
/**
* All IndexedDB databases are opened without version
* because it is a bit faster, especially on firefox
* @link http://nparashuram.com/IndexedDB/perf/#Open%20Database%20with%20version
*/
const openRequest = IndexedDB.open(dbName);
openRequest.onupgradeneeded = ev => {
const db = ev.target.result;
db.createObjectStore(OBJECT_STORE_ID, {
keyPath: "id",
autoIncrement: true
});
};
const dbPromise = new Promise((resolve, reject) => {
openRequest.onerror = ev => reject(ev);
openRequest.onsuccess = () => {
resolve(openRequest.result);
};
});
return dbPromise;
}
/**
* writes the new message to the database
* so other readers can find it
*/
function writeMessage(db, readerUuid, messageJson) {
const time = Date.now();
const writeObject = {
uuid: readerUuid,
time,
data: messageJson
};
const tx = db.transaction([OBJECT_STORE_ID], "readwrite", TRANSACTION_SETTINGS);
return new Promise((resolve, reject) => {
tx.oncomplete = () => resolve();
tx.onerror = ev => reject(ev);
const objectStore = tx.objectStore(OBJECT_STORE_ID);
objectStore.add(writeObject);
commitIndexedDBTransaction(tx);
});
}
function getAllMessages(db) {
const tx = db.transaction(OBJECT_STORE_ID, "readonly", TRANSACTION_SETTINGS);
const objectStore = tx.objectStore(OBJECT_STORE_ID);
const ret = [];
return new Promise(resolve => {
objectStore.openCursor().onsuccess = ev => {
const cursor = ev.target.result;
if (cursor) {
ret.push(cursor.value);
cursor.continue();
} else {
commitIndexedDBTransaction(tx);
resolve(ret);
}
};
});
}
function getMessagesHigherThan(db, lastCursorId) {
const tx = db.transaction(OBJECT_STORE_ID, "readonly", TRANSACTION_SETTINGS);
const objectStore = tx.objectStore(OBJECT_STORE_ID);
const ret = [];
let keyRangeValue = IDBKeyRange.bound(lastCursorId + 1, Infinity);
/**
* Optimization shortcut,
* if getAll() can be used, do not use a cursor.
* @link https://rxdb.info/slow-indexeddb.html
*/
if (objectStore.getAll) {
const getAllRequest = objectStore.getAll(keyRangeValue);
return new Promise((resolve, reject) => {
getAllRequest.onerror = err => reject(err);
getAllRequest.onsuccess = function (e) {
resolve(e.target.result);
};
});
}
function openCursor() {
// Occasionally Safari will fail on IDBKeyRange.bound, this
// catches that error, having it open the cursor to the first
// item. When it gets data it will advance to the desired key.
try {
keyRangeValue = IDBKeyRange.bound(lastCursorId + 1, Infinity);
return objectStore.openCursor(keyRangeValue);
} catch {
return objectStore.openCursor();
}
}
return new Promise((resolve, reject) => {
const openCursorRequest = openCursor();
openCursorRequest.onerror = err => reject(err);
openCursorRequest.onsuccess = ev => {
const cursor = ev.target.result;
if (cursor) {
if (cursor.value.id < lastCursorId + 1) {
cursor.continue(lastCursorId + 1);
} else {
ret.push(cursor.value);
cursor.continue();
}
} else {
commitIndexedDBTransaction(tx);
resolve(ret);
}
};
});
}
function removeMessagesById(db, ids) {
const tx = db.transaction([OBJECT_STORE_ID], "readwrite", TRANSACTION_SETTINGS);
const objectStore = tx.objectStore(OBJECT_STORE_ID);
return Promise.all(ids.map(id => {
const deleteRequest = objectStore.delete(id);
return new Promise(resolve => {
deleteRequest.onsuccess = () => resolve();
});
}));
}
function getOldMessages(db, ttl) {
const olderThen = Date.now() - ttl;
const tx = db.transaction(OBJECT_STORE_ID, "readonly", TRANSACTION_SETTINGS);
const objectStore = tx.objectStore(OBJECT_STORE_ID);
const ret = [];
return new Promise(resolve => {
objectStore.openCursor().onsuccess = ev => {
const cursor = ev.target.result;
if (cursor) {
const msgObk = cursor.value;
if (msgObk.time < olderThen) {
ret.push(msgObk);
cursor.continue();
} else {
// no more old messages,
commitIndexedDBTransaction(tx);
resolve(ret);
}
} else {
resolve(ret);
}
};
});
}
function cleanOldMessages(db, ttl) {
return getOldMessages(db, ttl).then(tooOld => {
return removeMessagesById(db, tooOld.map(msg => msg.id));
});
}
function create(channelName, options$1) {
options$1 = options.fillOptionsWithDefaults(options$1);
return createDatabase(channelName).then(db => {
const state = {
closed: false,
lastCursorId: 0,
channelName,
options: options$1,
uuid: util.generateRandomId(),
/**
* emittedMessagesIds
* contains all messages that have been emitted before
* @type {ObliviousSet}
*/
eMIs: new obliviousSet.ObliviousSet(options$1.idb.ttl * 2),
// ensures we do not read messages in parrallel
writeBlockPromise: util.PROMISE_RESOLVED_VOID,
messagesCallback: null,
readQueuePromises: [],
db,
time: util.microSeconds()
};
/**
* Handle abrupt closes that do not originate from db.close().
* This could happen, for example, if the underlying storage is
* removed or if the user clears the database in the browser's
* history preferences.
*/
db.onclose = function () {
state.closed = true;
if (options$1.idb.onclose) options$1.idb.onclose();
};
/**
* if service-workers are used,
* we have no 'storage'-event if they post a message,
* therefore we also have to set an interval
*/
_readLoop(state);
return state;
});
}
function _readLoop(state) {
if (state.closed) return;
readNewMessages(state).then(() => util.sleep(state.options.idb.fallbackInterval)).then(() => _readLoop(state)).catch(e => {
throw e;
});
}
function _filterMessage(msgObj, state) {
if (msgObj.uuid === state.uuid) return false; // send by own
if (state.eMIs.has(msgObj.id)) return false; // already emitted
if (msgObj.data.time < state.messagesCallbackTime) return false; // older then onMessageCallback
return true;
}
/**
* reads all new messages from the database and emits them
*/
function readNewMessages(state) {
// channel already closed
if (state.closed) return util.PROMISE_RESOLVED_VOID;
// if no one is listening, we do not need to scan for new messages
if (!state.messagesCallback) return util.PROMISE_RESOLVED_VOID;
return getMessagesHigherThan(state.db, state.lastCursorId).then(newerMessages => {
const useMessages = newerMessages
/**
* there is a bug in iOS where the msgObj can be undefined some times
* so we filter them out
* @link https://github.com/pubkey/broadcast-channel/issues/19
*/.filter(msgObj => !!msgObj).map(msgObj => {
if (msgObj.id > state.lastCursorId) {
state.lastCursorId = msgObj.id;
}
return msgObj;
}).filter(msgObj => _filterMessage(msgObj, state)).sort((msgObjA, msgObjB) => msgObjA.time - msgObjB.time); // sort by time
useMessages.forEach(msgObj => {
if (state.messagesCallback) {
state.eMIs.add(msgObj.id);
state.messagesCallback(msgObj.data);
}
});
return util.PROMISE_RESOLVED_VOID;
});
}
function close(channelState) {
channelState.closed = true;
channelState.db.close();
}
function postMessage(channelState, messageJson) {
channelState.writeBlockPromise = channelState.writeBlockPromise.then(() => writeMessage(channelState.db, channelState.uuid, messageJson)).then(() => {
if (util.randomInt(0, 10) === 0) {
/* await (do not await) */
cleanOldMessages(channelState.db, channelState.options.idb.ttl);
}
return util.PROMISE_RESOLVED_VOID;
});
return channelState.writeBlockPromise;
}
function onMessage(channelState, fn, time) {
channelState.messagesCallbackTime = time;
channelState.messagesCallback = fn;
readNewMessages(channelState);
}
function canBeUsed() {
const idb = getIdb();
if (!idb) return false;
return true;
}
function averageResponseTime(options) {
return options.idb.fallbackInterval * 2;
}
exports.TRANSACTION_SETTINGS = TRANSACTION_SETTINGS;
exports.averageResponseTime = averageResponseTime;
exports.canBeUsed = canBeUsed;
exports.cleanOldMessages = cleanOldMessages;
exports.close = close;
exports.commitIndexedDBTransaction = commitIndexedDBTransaction;
exports.create = create;
exports.createDatabase = createDatabase;
exports.getAllMessages = getAllMessages;
exports.getIdb = getIdb;
exports.getMessagesHigherThan = getMessagesHigherThan;
exports.getOldMessages = getOldMessages;
exports.microSeconds = microSeconds;
exports.onMessage = onMessage;
exports.postMessage = postMessage;
exports.removeMessagesById = removeMessagesById;
exports.type = type;
exports.writeMessage = writeMessage;
},{"../options.js":9,"../util.js":11,"oblivious-set":144}],5:[function(require,module,exports){
'use strict';
var obliviousSet = require('oblivious-set');
var options = require('../options.js');
var util = require('../util.js');
/**
* A localStorage-only method which uses localstorage and its 'storage'-event
* This does not work inside of webworkers because they have no access to locastorage
* This is basically implemented to support IE9 or your grandmothers toaster.
* @link https://caniuse.com/#feat=namevalue-storage
* @link https://caniuse.com/#feat=indexeddb
*/
const microSeconds = util.microSeconds;
const KEY_PREFIX = "pubkey.broadcastChannel-";
const type = "localstorage";
/**
* copied from crosstab
* @link https://github.com/tejacques/crosstab/blob/master/src/crosstab.js#L32
*/
function getLocalStorage() {
let localStorage = null;
if (typeof window === "undefined") return null;
try {
localStorage = window.localStorage;
localStorage = window["ie8-eventlistener/storage"] || window.localStorage;
} catch {
// New versions of Firefox throw a Security exception
// if cookies are disabled. See
// https://bugzilla.mozilla.org/show_bug.cgi?id=1028153
}
return localStorage;
}
function storageKey(channelName) {
return KEY_PREFIX + channelName;
}
/**
* writes the new message to the storage
* and fires the storage-event so other readers can find it
*/
function postMessage(channelState, messageJson) {
return new Promise((resolve, reject) => {
util.sleep().then(() => {
var _getLocalStorage;
const key = storageKey(channelState.channelName);
const writeObj = {
token: util.generateRandomId(),
time: Date.now(),
data: messageJson,
uuid: channelState.uuid
};
const value = JSON.stringify(writeObj);
// eslint-disable-next-line promise/always-return
(_getLocalStorage = getLocalStorage()) === null || _getLocalStorage === void 0 || _getLocalStorage.setItem(key, value);
/**
* StorageEvent does not fire the 'storage' event
* in the window that changes the state of the local storage.
* So we fire it manually
*/
const ev = document.createEvent("StorageEvent");
ev.initStorageEvent("storage", true, true, key, null, value, "", null);
window.dispatchEvent(ev);
resolve();
}).catch(reject);
});
}
function addStorageEventListener(channelName, fn) {
const key = storageKey(channelName);
const listener = ev => {
if (ev.key === key && ev.newValue) {
fn(JSON.parse(ev.newValue));
}
};
window.addEventListener("storage", listener);
return listener;
}
function removeStorageEventListener(listener) {
window.removeEventListener("storage", listener);
}
function canBeUsed() {
const ls = getLocalStorage();
if (!ls) return false;
try {
const key = "__broadcastchannel_check";
ls.setItem(key, "works");
ls.removeItem(key);
} catch {
// Safari 10 in private mode will not allow write access to local
// storage and fail with a QuotaExceededError. See
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API#Private_Browsing_Incognito_modes
return false;
}
return true;
}
function create(channelName, options$1) {
const filledOptions = options.fillOptionsWithDefaults(options$1);
if (!canBeUsed()) {
throw new Error("BroadcastChannel: localstorage cannot be used");
}
const uuid = util.generateRandomId();
/**
* eMIs
* contains all messages that have been emitted before
*/
const eMIs = new obliviousSet.ObliviousSet(filledOptions.localstorage.removeTimeout);
const state = {
channelName,
uuid,
time: util.microSeconds(),
eMIs // emittedMessagesIds
};
state.listener = addStorageEventListener(channelName, msgObj => {
if (!state.messagesCallback) return; // no listener
if (msgObj.uuid === uuid) return; // own message
if (!msgObj.token || eMIs.has(msgObj.token)) return; // already emitted
if (msgObj.data.time && msgObj.data.time < (state.messagesCallbackTime || 0)) return; // too old
eMIs.add(msgObj.token);
state.messagesCallback(msgObj.data);
});
return state;
}
function close(channelState) {
if (channelState.listener) {
removeStorageEventListener(channelState.listener);
}
}
function onMessage(channelState, fn, time) {
channelState.messagesCallbackTime = time;
channelState.messagesCallback = fn;
}
function averageResponseTime() {
const defaultTime = 120;
const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.includes("safari") && !userAgent.includes("chrome")) {
// safari is much slower so this time is higher
return defaultTime * 2;
}
return defaultTime;
}
exports.addStorageEventListener = addStorageEventListener;
exports.averageResponseTime = averageResponseTime;
exports.canBeUsed = canBeUsed;
exports.close = close;
exports.create = create;
exports.getLocalStorage = getLocalStorage;
exports.microSeconds = microSeconds;
exports.onMessage = onMessage;
exports.postMessage = postMessage;
exports.removeStorageEventListener = removeStorageEventListener;
exports.storageKey = storageKey;
exports.type = type;
},{"../options.js":9,"../util.js":11,"oblivious-set":144}],6:[function(require,module,exports){
'use strict';
var util = require('../util.js');
const microSeconds = util.microSeconds;
const type = "native";
function create(channelName) {
const state = {
time: util.microSeconds(),
messagesCallback: null,
bc: new BroadcastChannel(channelName),
subFns: [] // subscriberFunctions
};
state.bc.onmessage = msg => {
if (state.messagesCallback) {
state.messagesCallback(msg.data);
}
};
return state;
}
function close(channelState) {
channelState.bc.close();
channelState.subFns = [];
}
function postMessage(channelState, messageJson) {
try {
channelState.bc.postMessage(messageJson);
return util.PROMISE_RESOLVED_VOID;
} catch (err) {
return Promise.reject(err);
}
}
function onMessage(channelState, fn) {
channelState.messagesCallback = fn;
}
function canBeUsed() {
/**
* in the electron-renderer, isNode will be true even if we are in browser-context
* so we also check if window is undefined
*/
if (typeof window === "undefined") return false;
if (typeof BroadcastChannel === "function") {
if (BroadcastChannel._pubkey) {
throw new Error("BroadcastChannel: Do not overwrite window.BroadcastChannel with this module, this is not a polyfill");
}
return true;
}
return false;
}
function averageResponseTime() {
return 150;
}
exports.averageResponseTime = averageResponseTime;
exports.canBeUsed = canBeUsed;
exports.close = close;
exports.create = create;
exports.microSeconds = microSeconds;
exports.onMessage = onMessage;
exports.postMessage = postMessage;
exports.type = type;
},{"../util.js":11}],7:[function(require,module,exports){
(function (Buffer){(function (){
'use strict';
var eccrypto = require('@toruslabs/eccrypto');
var metadataHelpers = require('@toruslabs/metadata-helpers');
var obliviousSet = require('oblivious-set');
var socket_ioClient = require('socket.io-client');
var options = require('../options.js');
var util = require('../util.js');
/**
* A localStorage-only method which uses localstorage and its 'storage'-event
* This does not work inside of webworkers because they have no access to locastorage
* This is basically implemented to support IE9 or your grandmothers toaster.
* @link https://caniuse.com/#feat=namevalue-storage
* @link https://caniuse.com/#feat=indexeddb
*/
const microSeconds = util.microSeconds;
const KEY_PREFIX = "pubkey.broadcastChannel-";
const type = "server";
let SOCKET_CONN_INSTANCE = null;
// used to decide to reconnect socket e.g. when socket connection is disconnected unexpectedly
const runningChannels = new Set();
function storageKey(channelName) {
return KEY_PREFIX + channelName;
}
/**
* writes the new message to the storage
* and fires the storage-event so other readers can find it
*/
function postMessage(channelState, messageJson) {
return new Promise((resolve, reject) => {
util.sleep().then(async () => {
const key = storageKey(channelState.channelName);
const channelEncPrivKey = metadataHelpers.keccak256(Buffer.from(key, "utf8"));
const encData = await metadataHelpers.encryptData(channelEncPrivKey.toString("hex"), {
token: util.generateRandomId(),
time: Date.now(),
data: messageJson,
uuid: channelState.uuid
});
const body = {
allowedOrigin: channelState.server.allowed_origin,
sameIpCheck: true,
key: eccrypto.getPublic(channelEncPrivKey).toString("hex"),
data: encData,
signature: (await eccrypto.sign(channelEncPrivKey, metadataHelpers.keccak256(Buffer.from(encData, "utf8")))).toString("hex")
};
if (channelState.timeout) body.timeout = channelState.timeout;
return fetch(`${channelState.server.api_url}/channel/set`, {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json; charset=utf-8"
}
}).then(resolve).catch(reject);
}).catch(reject);
});
}
function getSocketInstance(socketUrl) {
if (SOCKET_CONN_INSTANCE) {
return SOCKET_CONN_INSTANCE;
}
const SOCKET_CONN = socket_ioClient.io(socketUrl, {
transports: ["websocket", "polling"],
// use WebSocket first, if available
withCredentials: true,
reconnectionDelayMax: 10000,
reconnectionAttempts: 10
});
SOCKET_CONN.on("connect_error", err => {
// revert to classic upgrade
SOCKET_CONN.io.opts.transports = ["polling", "websocket"];
util.log.error("connect error", err);
});
SOCKET_CONN.on("connect", async () => {
const {
engine
} = SOCKET_CONN.io;
util.log.debug("initially connected to", engine.transport.name); // in most cases, prints "polling"
engine.once("upgrade", () => {
// called when the transport is upgraded (i.e. from HTTP long-polling to WebSocket)
util.log.debug("upgraded", engine.transport.name); // in most cases, prints "websocket"
});
engine.once("close", reason => {
// called when the underlying connection is closed
util.log.debug("connection closed", reason);
});
});
SOCKET_CONN.on("error", err => {
util.log.error("socket errored", err);
SOCKET_CONN.disconnect();
});
SOCKET_CONN_INSTANCE = SOCKET_CONN;
return SOCKET_CONN;
}
function setupSocketConnection(socketUrl, channelState, fn) {
const socketConn = getSocketInstance(socketUrl);
const key = storageKey(channelState.channelName);
const channelEncPrivKey = metadataHelpers.keccak256(Buffer.from(key, "utf8"));
const channelPubKey = eccrypto.getPublic(channelEncPrivKey).toString("hex");
if (socketConn.connected) {
socketConn.emit("v2:check_auth_status", channelPubKey, {
sameIpCheck: true,
allowedOrigin: channelState.server.allowed_origin
});
} else {
socketConn.once("connect", () => {
util.log.debug("connected with socket");
socketConn.emit("v2:check_auth_status", channelPubKey, {
sameIpCheck: true,
allowedOrigin: channelState.server.allowed_origin
});
});
}
const reconnect = () => {
socketConn.once("connect", async () => {
if (runningChannels.has(channelState.channelName)) {
socketConn.emit("v2:check_auth_status", channelPubKey, {
sameIpCheck: true,
allowedOrigin: channelState.server.allowed_origin
});
}
});
};
const visibilityListener = () => {
// if channel is closed, then remove the listener.
if (!socketConn || !runningChannels.has(channelState.channelName)) {
document.removeEventListener("visibilitychange", visibilityListener);
return;
}
// if not connected, then wait for connection and ping server for latest msg.
if (!socketConn.connected && document.visibilityState === "visible") {
reconnect();
}
};
const listener = async ev => {
try {
const decData = await metadataHelpers.decryptData(channelEncPrivKey.toString("hex"), ev);
util.log.info(decData);
fn(decData);
} catch (error) {
util.log.error(error);
}
};
socketConn.on("disconnect", () => {
util.log.debug("socket disconnected");
if (runningChannels.has(channelState.channelName)) {
util.log.error("socket disconnected unexpectedly, reconnecting socket");
reconnect();
}
});
socketConn.on(`${channelPubKey}_success`, listener);
if (typeof document !== "undefined") document.addEventListener("visibilitychange", visibilityListener);
return socketConn;
}
function removeStorageEventListener() {
if (SOCKET_CONN_INSTANCE) {
SOCKET_CONN_INSTANCE.disconnect();
}
}
function canBeUsed() {
return true;
}
function create(channelName, options$1) {
options$1 = options.fillOptionsWithDefaults(options$1);
const uuid = util.generateRandomId();
/**
* eMIs
* contains all messages that have been emitted before
* @type {ObliviousSet}
*/
const eMIs = new obliviousSet.ObliviousSet(options$1.server.removeTimeout);
const state = {
channelName,
uuid,
eMIs,
// emittedMessagesIds
server: {
api_url: options$1.server.api_url,
socket_url: options$1.server.socket_url,
allowed_origin: options$1.server.allowed_origin
},
time: util.microSeconds()
};
if (options$1.server.timeout) state.timeout = options$1.server.timeout;
setupSocketConnection(options$1.server.socket_url, state, msgObj => {
if (!state.messagesCallback) return; // no listener
if (msgObj.uuid === state.uuid) return; // own message
if (!msgObj.token || state.eMIs.has(msgObj.token)) return; // already emitted
// if (msgObj.data.time && msgObj.data.time < state.messagesCallbackTime) return; // too old
state.eMIs.add(msgObj.token);
state.messagesCallback(msgObj.data);
});
runningChannels.add(channelName);
return state;
}
function close(channelState) {
runningChannels.delete(channelState.channelName);
// give 2 sec for all msgs which are in transit to be consumed
// by receiver.
// window.setTimeout(() => {
// removeStorageEventListener(channelState);
// SOCKET_CONN_INSTANCE = null;
// }, 1000);
}
function onMessage(channelState, fn, time) {
channelState.messagesCallbackTime = time;
channelState.messagesCallback = fn;
}
function averageResponseTime() {
const defaultTime = 500;
// TODO: Maybe increase it based on operation
return defaultTime;
}
exports.averageResponseTime = averageResponseTime;
exports.canBeUsed = canBeUsed;
exports.close = close;
exports.create = create;
exports.getSocketInstance = getSocketInstance;
exports.microSeconds = microSeconds;
exports.onMessage = onMessage;
exports.postMessage = postMessage;
exports.removeStorageEventListener = removeStorageEventListener;
exports.setupSocketConnection = setupSocketConnection;
exports.storageKey = storageKey;
exports.type = type;
}).call(this)}).call(this,require("buffer").Buffer)
},{"../options.js":9,"../util.js":11,"@toruslabs/eccrypto":27,"@toruslabs/metadata-helpers":30,"buffer":36,"oblivious-set":144,"socket.io-client":149}],8:[function(require,module,exports){
'use strict';
var util = require('../util.js');
const microSeconds = util.microSeconds;
const type = "simulate";
const SIMULATE_CHANNELS = new Set();
const SIMULATE_DELAY_TIME = 5;
function create(channelName) {
const state = {
time: util.microSeconds(),
name: channelName,
messagesCallback: null
};
SIMULATE_CHANNELS.add(state);
return state;
}
function close(channelState) {
SIMULATE_CHANNELS.delete(channelState);
}
function postMessage(channelState, messageJson) {
return new Promise(resolve => {
setTimeout(() => {
const channelArray = Array.from(SIMULATE_CHANNELS);
channelArray.forEach(channel => {
if (channel.name === channelState.name &&
// has same name
channel !== channelState &&
// not own channel
!!channel.messagesCallback &&
// has subscribers
channel.time < messageJson.time // channel not created after postMessage() call
) {
channel.messagesCallback(messageJson);
}
});
resolve();
}, SIMULATE_DELAY_TIME);
});
}
function onMessage(channelState, fn) {
channelState.messagesCallback = fn;
}
function canBeUsed() {
return true;
}
function averageResponseTime() {
return SIMULATE_DELAY_TIME;
}
exports.SIMULATE_DELAY_TIME = SIMULATE_DELAY_TIME;
exports.averageResponseTime = averageResponseTime;
exports.canBeUsed = canBeUsed;
exports.close = close;
exports.create = create;
exports.microSeconds = microSeconds;
exports.onMessage = onMessage;
exports.postMessage = postMessage;
exports.type = type;
},{"../util.js":11}],9:[function(require,module,exports){
'use strict';
var constants = require('@toruslabs/constants');
function fillOptionsWithDefaults(originalOptions = {}) {
const options = JSON.parse(JSON.stringify(originalOptions));
// main
if (typeof options.webWorkerSupport === "undefined") options.webWorkerSupport = true;
// indexed-db
if (!options.idb) options.idb = {};
// after this time the messages get deleted
if (!options.idb.ttl) options.idb.ttl = 1000 * 45;
if (!options.idb.fallbackInterval) options.idb.fallbackInterval = 150;
// handles abrupt db onclose events.
if (originalOptions.idb && typeof originalOptions.idb.onclose === "function") options.idb.onclose = originalOptions.idb.onclose;
// localstorage
if (!options.localstorage) options.localstorage = {};
if (!options.localstorage.removeTimeout) options.localstorage.removeTimeout = 1000 * 60;
// server
if (!options.server) options.server = {};
if (!options.server.api_url) options.server.api_url = `${constants.SESSION_SERVER_API_URL}/v2`;
if (!options.server.socket_url) options.server.socket_url = `${constants.SESSION_SERVER_SOCKET_URL}`;
if (!options.server.removeTimeout) options.server.removeTimeout = 1000 * 60 * 5; // 5 minutes
// custom methods
if (originalOptions.methods) options.methods = originalOptions.methods;
return options;
}
exports.fillOptionsWithDefaults = fillOptionsWithDefaults;
},{"@toruslabs/constants":25}],10:[function(require,module,exports){
'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;
},{"./broadcast-channel.js":1,"./methods/localstorage.js":5,"./methods/native.js":6,"./methods/server.js":7,"./methods/simulate.js":8,"./util.js":11,"@babel/runtime/helpers/defineProperty":12,"@babel/runtime/helpers/objectSpread2":13}],11:[function(require,module,exports){
'use strict';
var loglevel = require('loglevel');
// import Bowser from 'bowser';
/**
* returns true if the given object is a promise
*/
function isPromise(obj) {
if (obj && typeof obj.then === "function") {
return true;
}
return false;
}
Promise.resolve(false);
Promise.resolve(true);
const PROMISE_RESOLVED_VOID = Promise.resolve();
function sleep(time, resolveWith) {
if (!time) time = 0;
return new Promise(resolve => {
setTimeout(() => resolve(resolveWith), time);
});
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
/**
* https://stackoverflow.com/a/8084248
*/
function generateRandomId() {
return Math.random().toString(36).substring(2);
}
let lastMs = 0;
/**
* returns the current time in micro-seconds,
* WARNING: This is a pseudo-function
* Performance.now is not reliable in webworkers, so we just make sure to never return the same time.
* This is enough in browsers, and this function will not be used in nodejs.
* The main reason for this hack is to ensure that BroadcastChannel behaves equal to production when it is used in fast-running unit tests.
*/
function microSeconds() {
let ret = Date.now() * 1000; // milliseconds to microseconds
if (ret <= lastMs) {
ret = lastMs + 1;
}
lastMs = ret;
return ret;
}
// the problem is only in iframes. we should default to server in case of iframes.
// storage scoping is present in all browsers now
// Safari and other browsers support native Broadcast channel now. It's in LS.
// test here: https://pubkey.github.io/broadcast-channel/e2e.html?methodType=native
// https://caniuse.com/broadcastchannel
// export function are3PCSupported() {
// if (typeof navigator === 'undefined') return false;
// const browserInfo = Bowser.parse(navigator.userAgent);
// log.info(JSON.stringify(browserInfo), 'current browser info');
// let thirdPartyCookieSupport = true;
// // brave
// if (navigator.brave) {
// thirdPartyCookieSupport = false;
// }
// // All webkit & gecko engine instances use itp (intelligent tracking prevention -
// // https://webkit.org/tracking-prevention/#intelligent-tracking-prevention-itp)
// if (browserInfo.engine.name === Bowser.ENGINE_MAP.WebKit || browserInfo.engine.name === Bowser.ENGINE_MAP.Gecko) {
// thirdPartyCookieSupport = false;
// }
// return thirdPartyCookieSupport;
// }
const log = loglevel.getLogger("broadcast-channel");
log.setLevel("error");
exports.PROMISE_RESOLVED_VOID = PROMISE_RESOLVED_VOID;
exports.generateRandomId = generateRandomId;
exports.isPromise = isPromise;
exports.log = log;
exports.microSeconds = microSeconds;
exports.randomInt = randomInt;
exports.sleep = sleep;
},{"loglevel":129}],12:[function(require,module,exports){
var toPropertyKey = require("./toPropertyKey.js");
function _defineProperty(e, r, t) {
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: !0,
configurable: !0,
writable: !0
}) : e[r] = t, e;
}
module.exports = _defineProperty, module.exports.__esModule = true, module.exports["default"] = module.exports;
},{"./toPropertyKey.js":15}],13:[function(require,module,exports){
var defineProperty = require("./defineProperty.js");
function ownKeys(e, r) {
var t = Object.keys(e);
if (Object.getOwnPropertySymbols) {
var o = Object.getOwnPropertySymbols(e);
r && (o = o.filter(function (r) {
return Object.getOwnPropertyDescriptor(e, r).enumerable;
})), t.push.apply(t, o);
}
return t;
}
function _objectSpread2(e) {
for (var r = 1; r < arguments.length; r++) {
var t = null != arguments[r] ? arguments[r] : {};
r % 2 ? ownKeys(Object(t), !0).forEach(function (r) {
defineProperty(e, r, t[r]);
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
});
}
return e;
}
module.exports = _objectSpread2, module.exports.__esModule = true, module.exports["default"] = module.exports;
},{"./defineProperty.js":12}],14:[function(require,module,exports){
var _typeof = require("./typeof.js")["default"];
function toPrimitive(t, r) {
if ("object" != _typeof(t) || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != _typeof(i)) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
module.exports = toPrimitive, module.exports.__esModule = true, module.exports["default"] = module.exports;
},{"./typeof.js":16}],15:[function(require,module,exports){
var _typeof = require("./typeof.js")["default"];
var toPrimitive = require("./toPrimitive.js");
function toPropertyKey(t) {
var i = toPrimitive(t, "string");
return "symbol" == _typeof(i) ? i : i + "";
}
module.exports = toPropertyKey, module.exports.__esModule = true, module.exports["default"] = module.exports;
},{"./toPrimitive.js":14,"./typeof.js":16}],16:[function(require,module,exports){
function _typeof(o) {
"@babel/helpers - typeof";
return module.exports = _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;