rxdb
Version:
A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/
450 lines (435 loc) • 15.2 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.RX_STORAGE_NAME_LOKIJS = exports.OPEN_LOKIJS_STORAGE_INSTANCES = exports.LOKI_KEY_OBJECT_BROADCAST_CHANNEL_MESSAGE_TYPE = exports.LOKI_BROADCAST_CHANNEL_MESSAGE_TYPE = exports.LOKIJS_COLLECTION_DEFAULT_OPTIONS = exports.CHANGES_COLLECTION_SUFFIX = void 0;
exports.closeLokiCollections = closeLokiCollections;
exports.getLokiDatabase = getLokiDatabase;
exports.getLokiLeaderElector = getLokiLeaderElector;
exports.getLokiSortComparator = getLokiSortComparator;
exports.handleRemoteRequest = handleRemoteRequest;
exports.mustUseLocalState = mustUseLocalState;
exports.requestRemoteInstance = requestRemoteInstance;
exports.stripLokiKey = stripLokiKey;
exports.transformRegexToRegExp = transformRegexToRegExp;
exports.waitUntilHasLeader = waitUntilHasLeader;
var _rxStorageInstanceLoki = require("./rx-storage-instance-loki.js");
var _lokijs = _interopRequireDefault(require("lokijs"));
var _unload = require("unload");
var _index = require("../utils/index.js");
var _lokiSaveQueue = require("./loki-save-queue.js");
var _rxError = require("../../rx-error.js");
var _rxStorageMultiinstance = require("../../rx-storage-multiinstance.js");
var _index2 = require("../leader-election/index.js");
var _overwritable = require("../../overwritable.js");
var CHANGES_COLLECTION_SUFFIX = exports.CHANGES_COLLECTION_SUFFIX = '-rxdb-changes';
var LOKI_BROADCAST_CHANNEL_MESSAGE_TYPE = exports.LOKI_BROADCAST_CHANNEL_MESSAGE_TYPE = 'rxdb-lokijs-remote-request';
var LOKI_KEY_OBJECT_BROADCAST_CHANNEL_MESSAGE_TYPE = exports.LOKI_KEY_OBJECT_BROADCAST_CHANNEL_MESSAGE_TYPE = 'rxdb-lokijs-remote-request-key-object';
var RX_STORAGE_NAME_LOKIJS = exports.RX_STORAGE_NAME_LOKIJS = 'lokijs';
/**
* Loki attaches a $loki property to all data
* which must be removed before returning the data back to RxDB.
*/
function stripLokiKey(docData) {
if (!docData.$loki) {
return docData;
}
var cloned = (0, _index.flatClone)(docData);
/**
* In RxDB version 12.0.0,
* we introduced the _meta field that already contains the last write time.
* To be backwards compatible, we have to move the $lastWriteAt to the _meta field.
* TODO remove this in the next major version.
*/
if (cloned.$lastWriteAt) {
cloned._meta = {
lwt: cloned.$lastWriteAt
};
delete cloned.$lastWriteAt;
}
delete cloned.$loki;
return cloned;
}
/**
* Used to check in tests if all instances have been cleaned up.
*/
var OPEN_LOKIJS_STORAGE_INSTANCES = exports.OPEN_LOKIJS_STORAGE_INSTANCES = new Set();
var LOKIJS_COLLECTION_DEFAULT_OPTIONS = exports.LOKIJS_COLLECTION_DEFAULT_OPTIONS = {
disableChangesApi: true,
disableMeta: true,
disableDeltaChangesApi: true,
disableFreeze: true,
// TODO use 'immutable' like WatermelonDB does it
cloneMethod: 'shallow-assign',
clone: false,
transactional: false,
autoupdate: false
};
var LOKI_DATABASE_STATE_BY_NAME = new Map();
function getLokiDatabase(databaseName, databaseSettings) {
return (0, _index.getFromMapOrCreate)(LOKI_DATABASE_STATE_BY_NAME, databaseName, () => {
/**
* We assume that as soon as an adapter is passed,
* the database has to be persistent.
*/
var hasPersistence = !!databaseSettings.adapter;
var databaseState = (async () => {
var persistenceMethod = hasPersistence ? 'adapter' : 'memory';
if (databaseSettings.persistenceMethod) {
persistenceMethod = databaseSettings.persistenceMethod;
}
var useSettings = Object.assign(
// defaults
{
autoload: hasPersistence,
persistenceMethod,
verbose: true
}, databaseSettings,
// overwrites
{
/**
* RxDB uses its custom load and save handling
* so we disable the LokiJS save/load handlers.
*/
autoload: false,
autosave: false,
throttledSaves: false
});
var database = new _lokijs.default(databaseName + '.db', (0, _index.flatClone)(useSettings));
var lokiSaveQueue = new _lokiSaveQueue.LokiSaveQueue(database, useSettings);
/**
* Wait until all data is loaded from persistence adapter.
* Wrap the loading into the saveQueue to ensure that when many
* collections are created at the same time, the load-calls do not interfere
* with each other and cause error logs.
*/
if (hasPersistence) {
var loadDatabasePromise = new Promise((res, rej) => {
try {
database.loadDatabase({
recursiveWait: false
}, err => {
if (useSettings.autoloadCallback) {
useSettings.autoloadCallback(err);
}
if (err) {
rej(err);
} else {
res();
}
});
} catch (err) {
rej(err);
}
});
lokiSaveQueue.saveQueue = lokiSaveQueue.saveQueue.then(() => loadDatabasePromise);
await loadDatabasePromise;
}
/**
* Autosave database on process end
*/
var unloads = [];
if (hasPersistence) {
unloads.push((0, _unload.add)(() => lokiSaveQueue.run()));
}
var state = {
database,
databaseSettings: useSettings,
saveQueue: lokiSaveQueue,
collections: {},
unloads
};
return state;
})();
return databaseState;
});
}
async function closeLokiCollections(databaseName, collections) {
var databaseState = await LOKI_DATABASE_STATE_BY_NAME.get(databaseName);
if (!databaseState) {
// already closed
return;
}
await databaseState.saveQueue.run();
collections.forEach(collection => {
var collectionName = collection.name;
delete databaseState.collections[collectionName];
});
if (Object.keys(databaseState.collections).length === 0) {
// all collections closed -> also close database
LOKI_DATABASE_STATE_BY_NAME.delete(databaseName);
databaseState.unloads.forEach(u => u.remove());
await new Promise((res, rej) => {
databaseState.database.close(err => {
if (err) {
rej(err);
} else {
res();
}
});
});
}
}
/**
* This function is at lokijs-helper
* because we need it in multiple places.
*/
function getLokiSortComparator(_schema, query) {
if (!query.sort) {
throw (0, _rxError.newRxError)('SNH', {
query
});
}
var sortOptions = query.sort;
var fun = (a, b) => {
var compareResult = 0; // 1 | -1
sortOptions.find(sortPart => {
var fieldName = Object.keys(sortPart)[0];
var direction = Object.values(sortPart)[0];
var directionMultiplier = direction === 'asc' ? 1 : -1;
var valueA = (0, _index.getProperty)(a, fieldName);
var valueB = (0, _index.getProperty)(b, fieldName);
if (valueA === valueB) {
return false;
} else {
if (valueA > valueB) {
compareResult = 1 * directionMultiplier;
return true;
} else {
compareResult = -1 * directionMultiplier;
return true;
}
}
});
/**
* Two different objects should never have the same sort position.
* We ensure this by having the unique primaryKey in the sort params
* which is added by RxDB if not existing yet.
*/
if (!compareResult) {
throw (0, _rxError.newRxError)('SNH', {
args: {
query,
a,
b
}
});
}
return compareResult;
};
return fun;
}
function getLokiLeaderElector(databaseInstanceToken, broadcastChannelRefObject, databaseName) {
var broadcastChannel = (0, _rxStorageMultiinstance.getBroadcastChannelReference)(RX_STORAGE_NAME_LOKIJS, databaseInstanceToken, databaseName, broadcastChannelRefObject);
var elector = (0, _index2.getLeaderElectorByBroadcastChannel)(broadcastChannel);
return elector;
}
/**
* For multi-instance usage, we send requests to the RxStorage
* to the current leading instance over the BroadcastChannel.
*/
async function requestRemoteInstance(instance, operation, params) {
var isRxStorageInstanceLoki = typeof instance.query === 'function';
var messageType = isRxStorageInstanceLoki ? LOKI_BROADCAST_CHANNEL_MESSAGE_TYPE : LOKI_KEY_OBJECT_BROADCAST_CHANNEL_MESSAGE_TYPE;
var leaderElector = (0, _index.ensureNotFalsy)(instance.internals.leaderElector);
await waitUntilHasLeader(leaderElector);
var broadcastChannel = leaderElector.broadcastChannel;
var whenDeathListener;
var leaderDeadPromise = new Promise(res => {
whenDeathListener = msg => {
if (msg.context === 'leader' && msg.action === 'death') {
res({
retry: true
});
}
};
broadcastChannel.addEventListener('internal', whenDeathListener);
});
var requestId = (0, _index.randomCouchString)(12);
var responseListener;
var responsePromise = new Promise((res, _rej) => {
responseListener = msg => {
if (msg.type === messageType && msg.response === true && msg.requestId === requestId) {
if (msg.isError) {
res({
retry: false,
error: msg.result
});
} else {
res({
retry: false,
result: msg.result
});
}
}
};
broadcastChannel.addEventListener('message', responseListener);
});
// send out the request to the other instance
broadcastChannel.postMessage({
response: false,
type: messageType,
operation,
params,
requestId,
databaseName: instance.databaseName,
collectionName: instance.collectionName
});
var timeout;
return Promise.race([leaderDeadPromise, responsePromise
// // comment in timeout to debug
// new Promise<WinningPromise>(res => {
// timeout = setTimeout(() => {
// res({ error: new Error('requestRemoteInstance() timeout errorored'), retry: false });
// }, 500);
// })
]).then(firstResolved => {
if (timeout) {
clearTimeout(timeout);
}
// clean up listeners
broadcastChannel.removeEventListener('message', responseListener);
broadcastChannel.removeEventListener('internal', whenDeathListener);
if (firstResolved.retry) {
/**
* The leader died while a remote request was running
* we re-run the whole operation.
* We cannot just re-run requestRemoteInstance()
* because the current instance might be the new leader now
* and then we have to use the local state instead of requesting the remote.
*/
return instance[operation](...params);
} else {
if (firstResolved.error) {
throw firstResolved.error;
} else {
return firstResolved.result;
}
}
});
}
/**
* Handles a request that came from a remote instance via requestRemoteInstance()
* Runs the requested operation over the local db instance and sends back the result.
*/
async function handleRemoteRequest(instance, msg) {
if (msg.type === LOKI_BROADCAST_CHANNEL_MESSAGE_TYPE && msg.requestId && msg.databaseName === instance.databaseName && msg.collectionName === instance.collectionName && !msg.response) {
var operation = msg.operation;
var params = msg.params;
var result;
var isError = false;
try {
result = await instance[operation](...params);
} catch (err) {
console.dir(err);
isError = true;
result = err;
}
var response = {
response: true,
requestId: msg.requestId,
databaseName: instance.databaseName,
collectionName: instance.collectionName,
result,
isError,
type: msg.type
};
(0, _index.ensureNotFalsy)(instance.internals.leaderElector).broadcastChannel.postMessage(response);
}
}
async function waitUntilHasLeader(leaderElector) {
leaderElector.awaitLeadership().catch(() => {});
await (0, _index.promiseWait)(0);
while (true) {
var has = await leaderElector.hasLeader();
if (has || leaderElector.broadcastChannel.isClosed || leaderElector.isDead) {
return;
}
if (leaderElector.applyOnce) {
await leaderElector.applyOnce();
} else {
/**
* Trigger applying for leadership
* but do not await it in case another
* instance becomes leader first.
*/
leaderElector.awaitLeadership().catch(() => {});
}
await (0, _index.promiseWait)(20);
}
}
/**
* If the local state must be used, that one is returned.
* Returns false if a remote instance must be used.
*/
async function mustUseLocalState(instance) {
if (instance.closed) {
/**
* If this happens, it means that RxDB made a call to an already closed storage instance.
* This must never happen because when RxDB closes a collection or database,
* all tasks must be cleared so that no more calls are made the the storage.
*/
throw new Error('already closed ' + JSON.stringify({
instanceClosed: instance.closed,
databaseName: instance.databaseName,
collectionName: instance.collectionName
}));
}
if (instance.internals.localState) {
return instance.internals.localState;
}
var leaderElector = (0, _index.ensureNotFalsy)(instance.internals.leaderElector);
await waitUntilHasLeader(leaderElector);
/**
* It might already have a localState after the applying
* because another subtask also called mustUSeLocalState()
*/
if (instance.internals.localState) {
return instance.internals.localState;
}
if (leaderElector.isLeader && !instance.internals.localState) {
// own is leader, use local instance
instance.internals.localState = (0, _rxStorageInstanceLoki.createLokiLocalState)({
databaseInstanceToken: instance.databaseInstanceToken,
databaseName: instance.databaseName,
collectionName: instance.collectionName,
options: instance.options,
schema: instance.schema,
multiInstance: instance.internals.leaderElector ? true : false,
devMode: _overwritable.overwritable.isDevMode()
}, instance.databaseSettings);
return (0, _index.ensureNotFalsy)(instance.internals.localState);
} else {
// other is leader, send message to remote leading instance
return false;
}
}
/**
* LokiJS does not understand the 'official' $regex operator,
* so we have to transform these back into RegExp objects.
* @recursive
*/
function transformRegexToRegExp(selector) {
if (typeof selector !== 'object' || selector === null) {
return selector;
}
var keys = Object.keys(selector);
var ret = {};
keys.forEach(key => {
var value = selector[key];
if (key === '$options') {
return;
}
if (key === '$regex' && !(value instanceof RegExp)) {
var opts = selector['$options'];
ret[key] = new RegExp(value, opts);
} else if (Array.isArray(value)) {
ret[key] = value.map(item => transformRegexToRegExp(item));
} else {
ret[key] = transformRegexToRegExp(value);
}
});
return ret;
}
//# sourceMappingURL=lokijs-helper.js.map