broadcast-channel
Version:
A BroadcastChannel that works in New Browsers, Old Browsers, WebWorkers, NodeJs, Deno and iframes
368 lines (356 loc) • 11.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.TRANSACTION_SETTINGS = exports.IndexedDBMethod = void 0;
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 = void 0;
exports.onMessage = onMessage;
exports.postMessage = postMessage;
exports.removeMessagesById = removeMessagesById;
exports.type = void 0;
exports.writeMessage = writeMessage;
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
*/
var microSeconds = exports.microSeconds = _util.microSeconds;
var DB_PREFIX = 'pubkey.broadcast-channel-0-';
var 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/
*/
var TRANSACTION_SETTINGS = exports.TRANSACTION_SETTINGS = {
durability: 'relaxed'
};
var type = exports.type = 'idb';
function getIdb() {
if (typeof indexedDB !== 'undefined') return indexedDB;
if (typeof window !== 'undefined') {
if (typeof window.mozIndexedDB !== 'undefined') return window.mozIndexedDB;
if (typeof window.webkitIndexedDB !== 'undefined') return window.webkitIndexedDB;
if (typeof window.msIndexedDB !== 'undefined') return window.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) {
var IndexedDB = getIdb();
// create table
var 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
*/
var openRequest = IndexedDB.open(dbName);
openRequest.onupgradeneeded = function (ev) {
var db = ev.target.result;
db.createObjectStore(OBJECT_STORE_ID, {
keyPath: 'id',
autoIncrement: true
});
};
return new Promise(function (res, rej) {
openRequest.onerror = function (ev) {
return rej(ev);
};
openRequest.onsuccess = function () {
res(openRequest.result);
};
});
}
/**
* writes the new message to the database
* so other readers can find it
*/
function writeMessage(db, readerUuid, messageJson) {
var time = Date.now();
var writeObject = {
uuid: readerUuid,
time: time,
data: messageJson
};
var tx = db.transaction([OBJECT_STORE_ID], 'readwrite', TRANSACTION_SETTINGS);
return new Promise(function (res, rej) {
tx.oncomplete = function () {
return res();
};
tx.onerror = function (ev) {
return rej(ev);
};
var objectStore = tx.objectStore(OBJECT_STORE_ID);
objectStore.add(writeObject);
commitIndexedDBTransaction(tx);
});
}
function getAllMessages(db) {
var tx = db.transaction(OBJECT_STORE_ID, 'readonly', TRANSACTION_SETTINGS);
var objectStore = tx.objectStore(OBJECT_STORE_ID);
var ret = [];
return new Promise(function (res) {
objectStore.openCursor().onsuccess = function (ev) {
var cursor = ev.target.result;
if (cursor) {
ret.push(cursor.value);
//alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
cursor["continue"]();
} else {
commitIndexedDBTransaction(tx);
res(ret);
}
};
});
}
function getMessagesHigherThan(db, lastCursorId) {
var tx = db.transaction(OBJECT_STORE_ID, 'readonly', TRANSACTION_SETTINGS);
var objectStore = tx.objectStore(OBJECT_STORE_ID);
var ret = [];
var 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) {
var getAllRequest = objectStore.getAll(keyRangeValue);
return new Promise(function (res, rej) {
getAllRequest.onerror = function (err) {
return rej(err);
};
getAllRequest.onsuccess = function (e) {
res(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 (e) {
return objectStore.openCursor();
}
}
return new Promise(function (res, rej) {
var openCursorRequest = openCursor();
openCursorRequest.onerror = function (err) {
return rej(err);
};
openCursorRequest.onsuccess = function (ev) {
var 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);
res(ret);
}
};
});
}
function removeMessagesById(channelState, ids) {
if (channelState.closed) {
return Promise.resolve([]);
}
var tx = channelState.db.transaction(OBJECT_STORE_ID, 'readwrite', TRANSACTION_SETTINGS);
var objectStore = tx.objectStore(OBJECT_STORE_ID);
return Promise.all(ids.map(function (id) {
var deleteRequest = objectStore["delete"](id);
return new Promise(function (res) {
deleteRequest.onsuccess = function () {
return res();
};
});
}));
}
function getOldMessages(db, ttl) {
var olderThen = Date.now() - ttl;
var tx = db.transaction(OBJECT_STORE_ID, 'readonly', TRANSACTION_SETTINGS);
var objectStore = tx.objectStore(OBJECT_STORE_ID);
var ret = [];
return new Promise(function (res) {
objectStore.openCursor().onsuccess = function (ev) {
var cursor = ev.target.result;
if (cursor) {
var msgObk = cursor.value;
if (msgObk.time < olderThen) {
ret.push(msgObk);
//alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
cursor["continue"]();
} else {
// no more old messages,
commitIndexedDBTransaction(tx);
res(ret);
}
} else {
res(ret);
}
};
});
}
function cleanOldMessages(channelState) {
return getOldMessages(channelState.db, channelState.options.idb.ttl).then(function (tooOld) {
return removeMessagesById(channelState, tooOld.map(function (msg) {
return msg.id;
}));
});
}
function create(channelName, options) {
options = (0, _options.fillOptionsWithDefaults)(options);
return createDatabase(channelName).then(function (db) {
var state = {
closed: false,
lastCursorId: 0,
channelName: channelName,
options: options,
uuid: (0, _util.randomToken)(),
/**
* emittedMessagesIds
* contains all messages that have been emitted before
* @type {ObliviousSet}
*/
eMIs: new _obliviousSet.ObliviousSet(options.idb.ttl * 2),
// ensures we do not read messages in parallel
writeBlockPromise: _util.PROMISE_RESOLVED_VOID,
messagesCallback: null,
readQueuePromises: [],
db: db
};
/**
* 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.idb.onclose) options.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(function () {
return (0, _util.sleep)(state.options.idb.fallbackInterval);
}).then(function () {
return _readLoop(state);
});
}
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(function (newerMessages) {
var useMessages = newerMessages
/**
* there is a bug in iOS where the msgObj can be undefined sometimes
* so we filter them out
* @link https://github.com/pubkey/broadcast-channel/issues/19
*/.filter(function (msgObj) {
return !!msgObj;
}).map(function (msgObj) {
if (msgObj.id > state.lastCursorId) {
state.lastCursorId = msgObj.id;
}
return msgObj;
}).filter(function (msgObj) {
return _filterMessage(msgObj, state);
}).sort(function (msgObjA, msgObjB) {
return msgObjA.time - msgObjB.time;
}); // sort by time
useMessages.forEach(function (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(function () {
return writeMessage(channelState.db, channelState.uuid, messageJson);
}).then(function () {
if ((0, _util.randomInt)(0, 10) === 0) {
/* await (do not await) */
cleanOldMessages(channelState);
}
});
return channelState.writeBlockPromise;
}
function onMessage(channelState, fn, time) {
channelState.messagesCallbackTime = time;
channelState.messagesCallback = fn;
readNewMessages(channelState);
}
function canBeUsed() {
return !!getIdb();
}
function averageResponseTime(options) {
return options.idb.fallbackInterval * 2;
}
var IndexedDBMethod = exports.IndexedDBMethod = {
create: create,
close: close,
onMessage: onMessage,
postMessage: postMessage,
canBeUsed: canBeUsed,
type: type,
averageResponseTime: averageResponseTime,
microSeconds: microSeconds
};