UNPKG

@toruslabs/broadcast-channel

Version:

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

1,420 lines (1,374 loc) 664 kB
(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;