"use strict"; 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 };