350 lines
9.3 KiB
JavaScript
350 lines
9.3 KiB
JavaScript
"use strict";
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.getIdb = getIdb;
|
|
exports.createDatabase = createDatabase;
|
|
exports.writeMessage = writeMessage;
|
|
exports.getAllMessages = getAllMessages;
|
|
exports.getMessagesHigherThan = getMessagesHigherThan;
|
|
exports.removeMessageById = removeMessageById;
|
|
exports.getOldMessages = getOldMessages;
|
|
exports.cleanOldMessages = cleanOldMessages;
|
|
exports.create = create;
|
|
exports.close = close;
|
|
exports.postMessage = postMessage;
|
|
exports.onMessage = onMessage;
|
|
exports.canBeUsed = canBeUsed;
|
|
exports.averageResponseTime = averageResponseTime;
|
|
exports["default"] = exports.type = exports.microSeconds = void 0;
|
|
|
|
var _util = require("../util.js");
|
|
|
|
var _obliviousSet = require("oblivious-set");
|
|
|
|
var _options = require("../options");
|
|
|
|
/**
|
|
* this method uses indexeddb to store the messages
|
|
* There is currently no observerAPI for idb
|
|
* @link https://github.com/w3c/IndexedDB/issues/51
|
|
*/
|
|
var microSeconds = _util.microSeconds;
|
|
exports.microSeconds = microSeconds;
|
|
var DB_PREFIX = 'pubkey.broadcast-channel-0-';
|
|
var OBJECT_STORE_ID = 'messages';
|
|
var type = 'idb';
|
|
exports.type = type;
|
|
|
|
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;
|
|
}
|
|
|
|
function createDatabase(channelName) {
|
|
var IndexedDB = getIdb(); // create table
|
|
|
|
var dbName = DB_PREFIX + channelName;
|
|
var openRequest = IndexedDB.open(dbName, 1);
|
|
|
|
openRequest.onupgradeneeded = function (ev) {
|
|
var db = ev.target.result;
|
|
db.createObjectStore(OBJECT_STORE_ID, {
|
|
keyPath: 'id',
|
|
autoIncrement: true
|
|
});
|
|
};
|
|
|
|
var dbPromise = new Promise(function (res, rej) {
|
|
openRequest.onerror = function (ev) {
|
|
return rej(ev);
|
|
};
|
|
|
|
openRequest.onsuccess = function () {
|
|
res(openRequest.result);
|
|
};
|
|
});
|
|
return dbPromise;
|
|
}
|
|
/**
|
|
* writes the new message to the database
|
|
* so other readers can find it
|
|
*/
|
|
|
|
|
|
function writeMessage(db, readerUuid, messageJson) {
|
|
var time = new Date().getTime();
|
|
var writeObject = {
|
|
uuid: readerUuid,
|
|
time: time,
|
|
data: messageJson
|
|
};
|
|
var transaction = db.transaction([OBJECT_STORE_ID], 'readwrite');
|
|
return new Promise(function (res, rej) {
|
|
transaction.oncomplete = function () {
|
|
return res();
|
|
};
|
|
|
|
transaction.onerror = function (ev) {
|
|
return rej(ev);
|
|
};
|
|
|
|
var objectStore = transaction.objectStore(OBJECT_STORE_ID);
|
|
objectStore.add(writeObject);
|
|
});
|
|
}
|
|
|
|
function getAllMessages(db) {
|
|
var objectStore = db.transaction(OBJECT_STORE_ID).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 {
|
|
res(ret);
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function getMessagesHigherThan(db, lastCursorId) {
|
|
var objectStore = db.transaction(OBJECT_STORE_ID).objectStore(OBJECT_STORE_ID);
|
|
var ret = [];
|
|
|
|
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 {
|
|
var keyRangeValue = IDBKeyRange.bound(lastCursorId + 1, Infinity);
|
|
return objectStore.openCursor(keyRangeValue);
|
|
} catch (e) {
|
|
return objectStore.openCursor();
|
|
}
|
|
}
|
|
|
|
return new Promise(function (res) {
|
|
openCursor().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 {
|
|
res(ret);
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function removeMessageById(db, id) {
|
|
var request = db.transaction([OBJECT_STORE_ID], 'readwrite').objectStore(OBJECT_STORE_ID)["delete"](id);
|
|
return new Promise(function (res) {
|
|
request.onsuccess = function () {
|
|
return res();
|
|
};
|
|
});
|
|
}
|
|
|
|
function getOldMessages(db, ttl) {
|
|
var olderThen = new Date().getTime() - ttl;
|
|
var objectStore = db.transaction(OBJECT_STORE_ID).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,
|
|
res(ret);
|
|
return;
|
|
}
|
|
} else {
|
|
res(ret);
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function cleanOldMessages(db, ttl) {
|
|
return getOldMessages(db, ttl).then(function (tooOld) {
|
|
return Promise.all(tooOld.map(function (msgObj) {
|
|
return removeMessageById(db, msgObj.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 parrallel
|
|
writeBlockPromise: Promise.resolve(),
|
|
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 Promise.resolve(); // if no one is listening, we do not need to scan for new messages
|
|
|
|
if (!state.messagesCallback) return Promise.resolve();
|
|
return getMessagesHigherThan(state.db, state.lastCursorId).then(function (newerMessages) {
|
|
var 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(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 Promise.resolve();
|
|
});
|
|
}
|
|
|
|
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.db, channelState.options.idb.ttl);
|
|
}
|
|
});
|
|
return channelState.writeBlockPromise;
|
|
}
|
|
|
|
function onMessage(channelState, fn, time) {
|
|
channelState.messagesCallbackTime = time;
|
|
channelState.messagesCallback = fn;
|
|
readNewMessages(channelState);
|
|
}
|
|
|
|
function canBeUsed() {
|
|
if (_util.isNode) return false;
|
|
var idb = getIdb();
|
|
if (!idb) return false;
|
|
return true;
|
|
}
|
|
|
|
function averageResponseTime(options) {
|
|
return options.idb.fallbackInterval * 2;
|
|
}
|
|
|
|
var _default = {
|
|
create: create,
|
|
close: close,
|
|
onMessage: onMessage,
|
|
postMessage: postMessage,
|
|
canBeUsed: canBeUsed,
|
|
type: type,
|
|
averageResponseTime: averageResponseTime,
|
|
microSeconds: microSeconds
|
|
};
|
|
exports["default"] = _default; |