@toruslabs/broadcast-channel
Version:
A BroadcastChannel that works in New Browsers, Old Browsers, WebWorkers
146 lines (137 loc) • 4.86 kB
JavaScript
import { ObliviousSet } from 'oblivious-set';
import { fillOptionsWithDefaults } from '../options.js';
import { microSeconds as microSeconds$1, sleep, generateRandomId } from '../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 = microSeconds$1;
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) => {
sleep().then(() => {
var _getLocalStorage;
const key = storageKey(channelState.channelName);
const writeObj = {
token: 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) {
const filledOptions = fillOptionsWithDefaults(options);
if (!canBeUsed()) {
throw new Error("BroadcastChannel: localstorage cannot be used");
}
const uuid = generateRandomId();
/**
* eMIs
* contains all messages that have been emitted before
*/
const eMIs = new ObliviousSet(filledOptions.localstorage.removeTimeout);
const state = {
channelName,
uuid,
time: microSeconds$1(),
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;
}
export { addStorageEventListener, averageResponseTime, canBeUsed, close, create, getLocalStorage, microSeconds, onMessage, postMessage, removeStorageEventListener, storageKey, type };