rxdb
Version:
A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/
548 lines (520 loc) • 19 kB
JavaScript
import _createClass from "@babel/runtime/helpers/createClass";
import { IdleQueue } from 'custom-idle-queue';
import { ObliviousSet } from 'oblivious-set';
import { pluginMissing, flatClone, PROMISE_RESOLVE_FALSE, randomToken, ensureNotFalsy, getDefaultRevision, getDefaultRxDocumentMeta, defaultHashSha256, RXDB_VERSION } from "./plugins/utils/index.js";
import { newRxError } from "./rx-error.js";
import { createRxSchema } from "./rx-schema.js";
import { runPluginHooks, runAsyncPluginHooks } from "./hooks.js";
import { Subject } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { createRxCollection } from "./rx-collection.js";
import { flatCloneDocWithMeta, getSingleDocument, getWrappedStorageInstance, INTERNAL_STORAGE_NAME } from "./rx-storage-helper.js";
import { ensureStorageTokenDocumentExists, getAllCollectionDocuments, getPrimaryKeyOfInternalDocument, INTERNAL_CONTEXT_COLLECTION, INTERNAL_STORE_SCHEMA, _collectionNamePrimary } from "./rx-database-internal-store.js";
import { removeCollectionStorages } from "./rx-collection-helper.js";
import { overwritable } from "./overwritable.js";
import { rxChangeEventBulkToRxChangeEvents } from "./rx-change-event.js";
/**
* stores the used database names+storage names
* so we can throw when the same database is created more then once.
*/
var USED_DATABASE_NAMES = new Set();
var DB_COUNT = 0;
export var RxDatabaseBase = /*#__PURE__*/function () {
/**
* Contains all known non-closed storage instances
* that belong to this database.
* Used in plugins and unit tests.
*/
function RxDatabaseBase(name,
/**
* Uniquely identifies the instance
* of this RxDatabase.
*/
token, storage, instanceCreationOptions, password, multiInstance, eventReduce = false, options = {},
/**
* Stores information documents about the collections of the database
*/
internalStore, hashFunction, cleanupPolicy, allowSlowCount, reactivity) {
this.idleQueue = new IdleQueue();
this.rxdbVersion = RXDB_VERSION;
this.storageInstances = new Set();
this._subs = [];
this.startupErrors = [];
this.onClose = [];
this.closed = false;
this.collections = {};
this.states = {};
this.eventBulks$ = new Subject();
this.observable$ = this.eventBulks$.pipe(mergeMap(changeEventBulk => rxChangeEventBulkToRxChangeEvents(changeEventBulk)));
this.storageToken = PROMISE_RESOLVE_FALSE;
this.storageTokenDocument = PROMISE_RESOLVE_FALSE;
this.emittedEventBulkIds = new ObliviousSet(60 * 1000);
this.name = name;
this.token = token;
this.storage = storage;
this.instanceCreationOptions = instanceCreationOptions;
this.password = password;
this.multiInstance = multiInstance;
this.eventReduce = eventReduce;
this.options = options;
this.internalStore = internalStore;
this.hashFunction = hashFunction;
this.cleanupPolicy = cleanupPolicy;
this.allowSlowCount = allowSlowCount;
this.reactivity = reactivity;
DB_COUNT++;
/**
* In the dev-mode, we create a pseudoInstance
* to get all properties of RxDatabase and ensure they do not
* conflict with the collection names etc.
* So only if it is not pseudoInstance,
* we have all values to prepare a real RxDatabase.
*
* TODO this is ugly, we should use a different way in the dev-mode
* so that all non-dev-mode code can be cleaner.
*/
if (this.name !== 'pseudoInstance') {
/**
* Wrap the internal store
* to ensure that calls to it also end up in
* calculation of the idle state and the hooks.
*/
this.internalStore = getWrappedStorageInstance(this.asRxDatabase, internalStore, INTERNAL_STORE_SCHEMA);
/**
* Start writing the storage token.
* Do not await the creation because it would run
* in a critical path that increases startup time.
*
* Writing the token takes about 20 milliseconds
* even on a fast adapter, so this is worth it.
*/
this.storageTokenDocument = ensureStorageTokenDocumentExists(this.asRxDatabase).catch(err => this.startupErrors.push(err));
this.storageToken = this.storageTokenDocument.then(doc => doc.data.token).catch(err => this.startupErrors.push(err));
}
}
var _proto = RxDatabaseBase.prototype;
_proto.getReactivityFactory = function getReactivityFactory() {
if (!this.reactivity) {
throw newRxError('DB14', {
database: this.name
});
}
return this.reactivity;
}
/**
* Because having unhandled exceptions would fail,
* we have to store the async errors of the constructor here
* so we can throw them later.
*/
/**
* When the database is closed,
* these functions will be called an awaited.
* Used to automatically clean up stuff that
* belongs to this collection.
*/
/**
* Internally only use eventBulks$
* Do not use .$ or .observable$ because that has to transform
* the events which decreases performance.
*/
/**
* Unique token that is stored with the data.
* Used to detect if the dataset has been deleted
* and if two RxDatabase instances work on the same dataset or not.
*
* Because reading and writing the storageToken runs in the hot path
* of database creation, we do not await the storageWrites but instead
* work with the promise when we need the value.
*/
/**
* Stores the whole state of the internal storage token document.
* We need this in some plugins.
*/
/**
* Contains the ids of all event bulks that have been emitted
* by the database.
* Used to detect duplicates that come in again via BroadcastChannel
* or other streams.
* In the past we tried to remove this and to ensure
* all storages only emit the same event bulks only once
* but it turns out this is just not possible for all storages.
* JavaScript processes, workers and browser tabs can be closed and started at any time
* which can cause cases where it is not possible to know if an event bulk has been emitted already.
*/;
/**
* This is the main handle-point for all change events
* ChangeEvents created by this instance go:
* RxDocument -> RxCollection -> RxDatabase.$emit -> MultiInstance
* ChangeEvents created by other instances go:
* MultiInstance -> RxDatabase.$emit -> RxCollection -> RxDatabase
*/
_proto.$emit = function $emit(changeEventBulk) {
if (this.emittedEventBulkIds.has(changeEventBulk.id)) {
return;
}
this.emittedEventBulkIds.add(changeEventBulk.id);
this.eventBulks$.next(changeEventBulk);
}
/**
* removes the collection-doc from the internalStore
*/;
_proto.removeCollectionDoc = async function removeCollectionDoc(name, schema) {
var doc = await getSingleDocument(this.internalStore, getPrimaryKeyOfInternalDocument(_collectionNamePrimary(name, schema), INTERNAL_CONTEXT_COLLECTION));
if (!doc) {
throw newRxError('SNH', {
name,
schema
});
}
var writeDoc = flatCloneDocWithMeta(doc);
writeDoc._deleted = true;
await this.internalStore.bulkWrite([{
document: writeDoc,
previous: doc
}], 'rx-database-remove-collection');
}
/**
* creates multiple RxCollections at once
* to be much faster by saving db txs and doing stuff in bulk-operations
* This function is not called often, but mostly in the critical path at the initial page load
* So it must be as fast as possible.
*/;
_proto.addCollections = async function addCollections(collectionCreators) {
var jsonSchemas = {};
var schemas = {};
var bulkPutDocs = [];
var useArgsByCollectionName = {};
await Promise.all(Object.entries(collectionCreators).map(async ([name, args]) => {
var collectionName = name;
var rxJsonSchema = args.schema;
jsonSchemas[collectionName] = rxJsonSchema;
var schema = createRxSchema(rxJsonSchema, this.hashFunction);
schemas[collectionName] = schema;
// collection already exists
if (this.collections[name]) {
throw newRxError('DB3', {
name
});
}
var collectionNameWithVersion = _collectionNamePrimary(name, rxJsonSchema);
var collectionDocData = {
id: getPrimaryKeyOfInternalDocument(collectionNameWithVersion, INTERNAL_CONTEXT_COLLECTION),
key: collectionNameWithVersion,
context: INTERNAL_CONTEXT_COLLECTION,
data: {
name: collectionName,
schemaHash: await schema.hash,
schema: schema.jsonSchema,
version: schema.version,
connectedStorages: []
},
_deleted: false,
_meta: getDefaultRxDocumentMeta(),
_rev: getDefaultRevision(),
_attachments: {}
};
bulkPutDocs.push({
document: collectionDocData
});
var useArgs = Object.assign({}, args, {
name: collectionName,
schema,
database: this
});
// run hooks
var hookData = flatClone(args);
hookData.database = this;
hookData.name = name;
runPluginHooks('preCreateRxCollection', hookData);
useArgs.conflictHandler = hookData.conflictHandler;
useArgsByCollectionName[collectionName] = useArgs;
}));
var putDocsResult = await this.internalStore.bulkWrite(bulkPutDocs, 'rx-database-add-collection');
await ensureNoStartupErrors(this);
await Promise.all(putDocsResult.error.map(async error => {
if (error.status !== 409) {
throw newRxError('DB12', {
database: this.name,
writeError: error
});
}
var docInDb = ensureNotFalsy(error.documentInDb);
var collectionName = docInDb.data.name;
var schema = schemas[collectionName];
// collection already exists but has different schema
if (docInDb.data.schemaHash !== (await schema.hash)) {
throw newRxError('DB6', {
database: this.name,
collection: collectionName,
previousSchemaHash: docInDb.data.schemaHash,
schemaHash: await schema.hash,
previousSchema: docInDb.data.schema,
schema: ensureNotFalsy(jsonSchemas[collectionName])
});
}
}));
var ret = {};
await Promise.all(Object.keys(collectionCreators).map(async collectionName => {
var useArgs = useArgsByCollectionName[collectionName];
var collection = await createRxCollection(useArgs);
ret[collectionName] = collection;
// set as getter to the database
this.collections[collectionName] = collection;
if (!this[collectionName]) {
Object.defineProperty(this, collectionName, {
get: () => this.collections[collectionName]
});
}
}));
return ret;
}
/**
* runs the given function between idleQueue-locking
*/;
_proto.lockedRun = function lockedRun(fn) {
return this.idleQueue.wrapCall(fn);
};
_proto.requestIdlePromise = function requestIdlePromise() {
return this.idleQueue.requestIdlePromise();
}
/**
* Export database to a JSON friendly format.
*/;
_proto.exportJSON = function exportJSON(_collections) {
throw pluginMissing('json-dump');
};
_proto.addState = function addState(_name) {
throw pluginMissing('state');
}
/**
* Import the parsed JSON export into the collection.
* @param _exportedJSON The previously exported data from the `<db>.exportJSON()` method.
* @note When an interface is loaded in this collection all base properties of the type are typed as `any`
* since data could be encrypted.
*/;
_proto.importJSON = function importJSON(_exportedJSON) {
throw pluginMissing('json-dump');
};
_proto.backup = function backup(_options) {
throw pluginMissing('backup');
};
_proto.leaderElector = function leaderElector() {
throw pluginMissing('leader-election');
};
_proto.isLeader = function isLeader() {
throw pluginMissing('leader-election');
}
/**
* returns a promise which resolves when the instance becomes leader
*/;
_proto.waitForLeadership = function waitForLeadership() {
throw pluginMissing('leader-election');
};
_proto.migrationStates = function migrationStates() {
throw pluginMissing('migration-schema');
}
/**
* closes the database-instance and all collections
*/;
_proto.close = async function close() {
if (this.closed) {
return PROMISE_RESOLVE_FALSE;
}
// settings closed = true must be the first thing to do.
this.closed = true;
await runAsyncPluginHooks('preCloseRxDatabase', this);
/**
* Complete the event stream
* to stop all subscribers who forgot to unsubscribe.
*/
this.eventBulks$.complete();
DB_COUNT--;
this._subs.map(sub => sub.unsubscribe());
/**
* closing the pseudo instance will throw
* because stuff is missing
* TODO we should not need the pseudo instance on runtime.
* we should generate the property list on build time.
*/
if (this.name === 'pseudoInstance') {
return PROMISE_RESOLVE_FALSE;
}
/**
* First wait until the database is idle
*/
return this.requestIdlePromise().then(() => Promise.all(this.onClose.map(fn => fn())))
// close all collections
.then(() => Promise.all(Object.keys(this.collections).map(key => this.collections[key]).map(col => col.close())))
// close internal storage instances
.then(() => this.internalStore.close())
// remove combination from USED_COMBINATIONS-map
.then(() => USED_DATABASE_NAMES.delete(this.storage.name + '|' + this.name)).then(() => true);
}
/**
* deletes the database and its stored data.
* Returns the names of all removed collections.
*/;
_proto.remove = function remove() {
return this.close().then(() => removeRxDatabase(this.name, this.storage, this.multiInstance, this.password));
};
return _createClass(RxDatabaseBase, [{
key: "$",
get: function () {
return this.observable$;
}
}, {
key: "asRxDatabase",
get: function () {
return this;
}
}]);
}();
/**
* checks if an instance with same name and storage already exists
* @throws {RxError} if used
*/
function throwIfDatabaseNameUsed(name, storage) {
var key = storage.name + '|' + name;
if (!USED_DATABASE_NAMES.has(key)) {
return;
} else {
throw newRxError('DB8', {
name,
storage: storage.name,
link: 'https://rxdb.info/rx-database.html#ignoreduplicate'
});
}
}
/**
* Creates the storage instances that are used internally in the database
* to store schemas and other configuration stuff.
*/
export async function createRxDatabaseStorageInstance(databaseInstanceToken, storage, databaseName, options, multiInstance, password) {
var internalStore = await storage.createStorageInstance({
databaseInstanceToken,
databaseName,
collectionName: INTERNAL_STORAGE_NAME,
schema: INTERNAL_STORE_SCHEMA,
options,
multiInstance,
password,
devMode: overwritable.isDevMode()
});
return internalStore;
}
export function createRxDatabase({
storage,
instanceCreationOptions,
name,
password,
multiInstance = true,
eventReduce = true,
ignoreDuplicate = false,
options = {},
cleanupPolicy,
allowSlowCount = false,
localDocuments = false,
hashFunction = defaultHashSha256,
reactivity
}) {
runPluginHooks('preCreateRxDatabase', {
storage,
instanceCreationOptions,
name,
password,
multiInstance,
eventReduce,
ignoreDuplicate,
options,
localDocuments
});
// check if combination already used
if (!ignoreDuplicate) {
throwIfDatabaseNameUsed(name, storage);
} else {
if (!overwritable.isDevMode()) {
throw newRxError('DB9', {
database: name
});
}
}
USED_DATABASE_NAMES.add(storage.name + '|' + name);
var databaseInstanceToken = randomToken(10);
return createRxDatabaseStorageInstance(databaseInstanceToken, storage, name, instanceCreationOptions, multiInstance, password)
/**
* Creating the internal store might fail
* if some RxStorage wrapper is used that does some checks
* and then throw.
* In that case we have to properly clean up the database.
*/.catch(err => {
USED_DATABASE_NAMES.delete(storage.name + '|' + name);
throw err;
}).then(storageInstance => {
var rxDatabase = new RxDatabaseBase(name, databaseInstanceToken, storage, instanceCreationOptions, password, multiInstance, eventReduce, options, storageInstance, hashFunction, cleanupPolicy, allowSlowCount, reactivity);
return runAsyncPluginHooks('createRxDatabase', {
database: rxDatabase,
creator: {
storage,
instanceCreationOptions,
name,
password,
multiInstance,
eventReduce,
ignoreDuplicate,
options,
localDocuments
}
}).then(() => rxDatabase);
});
}
/**
* Removes the database and all its known data
* with all known collections and all internal meta data.
*
* Returns the names of the removed collections.
*/
export async function removeRxDatabase(databaseName, storage, multiInstance = true, password) {
var databaseInstanceToken = randomToken(10);
var dbInternalsStorageInstance = await createRxDatabaseStorageInstance(databaseInstanceToken, storage, databaseName, {}, multiInstance, password);
var collectionDocs = await getAllCollectionDocuments(dbInternalsStorageInstance);
var collectionNames = new Set();
collectionDocs.forEach(doc => collectionNames.add(doc.data.name));
var removedCollectionNames = Array.from(collectionNames);
await Promise.all(removedCollectionNames.map(collectionName => removeCollectionStorages(storage, dbInternalsStorageInstance, databaseInstanceToken, databaseName, collectionName, multiInstance, password)));
await runAsyncPluginHooks('postRemoveRxDatabase', {
databaseName,
storage
});
await dbInternalsStorageInstance.remove();
return removedCollectionNames;
}
export function isRxDatabase(obj) {
return obj instanceof RxDatabaseBase;
}
export function dbCount() {
return DB_COUNT;
}
/**
* Returns true if the given RxDatabase was the first
* instance that was created on the storage with this name.
*
* Can be used for some optimizations because on the first instantiation,
* we can assume that no data was written before.
*/
export async function isRxDatabaseFirstTimeInstantiated(database) {
var tokenDoc = await database.storageTokenDocument;
return tokenDoc.data.instanceToken === database.token;
}
/**
* For better performance some tasks run async
* and are awaited later.
* But we still have to ensure that there have been no errors
* on database creation.
*/
export async function ensureNoStartupErrors(rxDatabase) {
await rxDatabase.storageToken;
if (rxDatabase.startupErrors[0]) {
throw rxDatabase.startupErrors[0];
}
}
//# sourceMappingURL=rx-database.js.map