rxdb
Version:
A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/
458 lines (446 loc) • 18.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.RxReplicationState = exports.REPLICATION_STATE_BY_COLLECTION = void 0;
exports.replicateRxCollection = replicateRxCollection;
exports.startReplicationOnLeaderShip = startReplicationOnLeaderShip;
var _rxjs = require("rxjs");
var _index = require("../leader-election/index.js");
var _index2 = require("../../plugins/utils/index.js");
var _index3 = require("../../replication-protocol/index.js");
var _rxError = require("../../rx-error.js");
var _replicationHelper = require("./replication-helper.js");
var _rxDatabaseInternalStore = require("../../rx-database-internal-store.js");
var _plugin = require("../../plugin.js");
var _rxStorageHelper = require("../../rx-storage-helper.js");
var _overwritable = require("../../overwritable.js");
var _hooks = require("../../hooks.js");
/**
* This plugin contains the primitives to create
* a RxDB client-server replication.
* It is used in the other replication plugins
* but also can be used as standalone with a custom replication handler.
*/
var REPLICATION_STATE_BY_COLLECTION = exports.REPLICATION_STATE_BY_COLLECTION = new WeakMap();
var RxReplicationState = exports.RxReplicationState = /*#__PURE__*/function () {
function RxReplicationState(
/**
* The identifier, used to flag revisions
* and to identify which documents state came from the remote.
*/
replicationIdentifier, collection, deletedField, pull, push, live, retryTime, autoStart, toggleOnDocumentVisible) {
this.subs = [];
this.subjects = {
received: new _rxjs.Subject(),
// all documents that are received from the endpoint
sent: new _rxjs.Subject(),
// all documents that are send to the endpoint
error: new _rxjs.Subject(),
// all errors that are received from the endpoint, emits new Error() objects
canceled: new _rxjs.BehaviorSubject(false),
// true when the replication was canceled
active: new _rxjs.BehaviorSubject(false) // true when something is running, false when not
};
this.received$ = this.subjects.received.asObservable();
this.sent$ = this.subjects.sent.asObservable();
this.error$ = this.subjects.error.asObservable();
this.canceled$ = this.subjects.canceled.asObservable();
this.active$ = this.subjects.active.asObservable();
this.wasStarted = false;
this.onCancel = [];
this.callOnStart = undefined;
this.remoteEvents$ = new _rxjs.Subject();
this.replicationIdentifier = replicationIdentifier;
this.collection = collection;
this.deletedField = deletedField;
this.pull = pull;
this.push = push;
this.live = live;
this.retryTime = retryTime;
this.autoStart = autoStart;
this.toggleOnDocumentVisible = toggleOnDocumentVisible;
this.metaInfoPromise = (async () => {
var metaInstanceCollectionName = 'rx-replication-meta-' + (await collection.database.hashFunction([this.collection.name, this.replicationIdentifier].join('-')));
var metaInstanceSchema = (0, _index3.getRxReplicationMetaInstanceSchema)(this.collection.schema.jsonSchema, (0, _rxStorageHelper.hasEncryption)(this.collection.schema.jsonSchema));
return {
collectionName: metaInstanceCollectionName,
schema: metaInstanceSchema
};
})();
var replicationStates = (0, _index2.getFromMapOrCreate)(REPLICATION_STATE_BY_COLLECTION, collection, () => []);
replicationStates.push(this);
// stop the replication when the collection gets closed
this.collection.onClose.push(() => this.cancel());
// create getters for the observables
Object.keys(this.subjects).forEach(key => {
Object.defineProperty(this, key + '$', {
get: function () {
return this.subjects[key].asObservable();
}
});
});
var startPromise = new Promise(res => {
this.callOnStart = res;
});
this.startPromise = startPromise;
}
var _proto = RxReplicationState.prototype;
_proto.start = async function start() {
if (this.isStopped()) {
return;
}
if (this.internalReplicationState) {
this.internalReplicationState.events.paused.next(false);
}
/**
* If started after a pause,
* just re-sync once and continue.
*/
if (this.wasStarted) {
this.reSync();
return;
}
this.wasStarted = true;
if (!this.toggleOnDocumentVisible) {
(0, _replicationHelper.preventHibernateBrowserTab)(this);
}
// fill in defaults for pull & push
var pullModifier = this.pull && this.pull.modifier ? this.pull.modifier : _replicationHelper.DEFAULT_MODIFIER;
var pushModifier = this.push && this.push.modifier ? this.push.modifier : _replicationHelper.DEFAULT_MODIFIER;
var database = this.collection.database;
var metaInfo = await this.metaInfoPromise;
var [metaInstance] = await Promise.all([this.collection.database.storage.createStorageInstance({
databaseName: database.name,
collectionName: metaInfo.collectionName,
databaseInstanceToken: database.token,
multiInstance: database.multiInstance,
options: {},
schema: metaInfo.schema,
password: database.password,
devMode: _overwritable.overwritable.isDevMode()
}), (0, _rxDatabaseInternalStore.addConnectedStorageToCollection)(this.collection, metaInfo.collectionName, metaInfo.schema)]);
this.metaInstance = metaInstance;
this.internalReplicationState = (0, _index3.replicateRxStorageInstance)({
pushBatchSize: this.push && this.push.batchSize ? this.push.batchSize : 100,
pullBatchSize: this.pull && this.pull.batchSize ? this.pull.batchSize : 100,
initialCheckpoint: {
upstream: this.push ? this.push.initialCheckpoint : undefined,
downstream: this.pull ? this.pull.initialCheckpoint : undefined
},
forkInstance: this.collection.storageInstance,
metaInstance: this.metaInstance,
hashFunction: database.hashFunction,
identifier: 'rxdbreplication' + this.replicationIdentifier,
conflictHandler: this.collection.conflictHandler,
replicationHandler: {
masterChangeStream$: this.remoteEvents$.asObservable().pipe((0, _rxjs.filter)(_v => !!this.pull), (0, _rxjs.mergeMap)(async ev => {
if (ev === 'RESYNC') {
return ev;
}
var useEv = (0, _index2.flatClone)(ev);
useEv.documents = (0, _replicationHelper.handlePulledDocuments)(this.collection, this.deletedField, useEv.documents);
useEv.documents = await Promise.all(useEv.documents.map(d => pullModifier(d)));
return useEv;
})),
masterChangesSince: async (checkpoint, batchSize) => {
if (!this.pull) {
return {
checkpoint: null,
documents: []
};
}
/**
* Retries must be done here in the replication primitives plugin,
* because the replication protocol itself has no
* error handling.
*/
var done = false;
var result = {};
while (!done && !this.isStoppedOrPaused()) {
try {
result = await this.pull.handler(checkpoint, batchSize);
done = true;
} catch (err) {
var emitError = (0, _rxError.newRxError)('RC_PULL', {
checkpoint,
errors: (0, _index2.toArray)(err).map(er => (0, _index2.errorToPlainJson)(er)),
direction: 'pull'
});
this.subjects.error.next(emitError);
await (0, _replicationHelper.awaitRetry)(this.collection, (0, _index2.ensureNotFalsy)(this.retryTime));
}
}
if (this.isStoppedOrPaused()) {
return {
checkpoint: null,
documents: []
};
}
var useResult = (0, _index2.flatClone)(result);
useResult.documents = (0, _replicationHelper.handlePulledDocuments)(this.collection, this.deletedField, useResult.documents);
useResult.documents = await Promise.all(useResult.documents.map(d => pullModifier(d)));
return useResult;
},
masterWrite: async rows => {
if (!this.push) {
return [];
}
var done = false;
await (0, _hooks.runAsyncPluginHooks)('preReplicationMasterWrite', {
rows,
collection: this.collection
});
var useRowsOrNull = await Promise.all(rows.map(async row => {
row.newDocumentState = await pushModifier(row.newDocumentState);
if (row.newDocumentState === null) {
return null;
}
if (row.assumedMasterState) {
row.assumedMasterState = await pushModifier(row.assumedMasterState);
}
if (this.deletedField !== '_deleted') {
row.newDocumentState = (0, _replicationHelper.swapDefaultDeletedTodeletedField)(this.deletedField, row.newDocumentState);
if (row.assumedMasterState) {
row.assumedMasterState = (0, _replicationHelper.swapDefaultDeletedTodeletedField)(this.deletedField, row.assumedMasterState);
}
}
return row;
}));
var useRows = useRowsOrNull.filter(_index2.arrayFilterNotEmpty);
var result = null;
// In case all the rows have been filtered and nothing has to be sent
if (useRows.length === 0) {
done = true;
result = [];
}
while (!done && !this.isStoppedOrPaused()) {
try {
result = await this.push.handler(useRows);
/**
* It is a common problem that people have wrongly behaving backend
* that do not return an array with the conflicts on push requests.
* So we run this check here to make it easier to debug.
* @link https://github.com/pubkey/rxdb/issues/4103
*/
if (!Array.isArray(result)) {
throw (0, _rxError.newRxError)('RC_PUSH_NO_AR', {
pushRows: rows,
direction: 'push',
args: {
result
}
});
}
done = true;
} catch (err) {
var emitError = err.rxdb ? err : (0, _rxError.newRxError)('RC_PUSH', {
pushRows: rows,
errors: (0, _index2.toArray)(err).map(er => (0, _index2.errorToPlainJson)(er)),
direction: 'push'
});
this.subjects.error.next(emitError);
await (0, _replicationHelper.awaitRetry)(this.collection, (0, _index2.ensureNotFalsy)(this.retryTime));
}
}
if (this.isStoppedOrPaused()) {
return [];
}
await (0, _hooks.runAsyncPluginHooks)('preReplicationMasterWriteDocumentsHandle', {
result,
collection: this.collection
});
var conflicts = (0, _replicationHelper.handlePulledDocuments)(this.collection, this.deletedField, (0, _index2.ensureNotFalsy)(result));
return conflicts;
}
}
});
this.subs.push(this.internalReplicationState.events.error.subscribe(err => {
this.subjects.error.next(err);
}), this.internalReplicationState.events.processed.down.subscribe(row => this.subjects.received.next(row.document)), this.internalReplicationState.events.processed.up.subscribe(writeToMasterRow => {
this.subjects.sent.next(writeToMasterRow.newDocumentState);
}), (0, _rxjs.combineLatest)([this.internalReplicationState.events.active.down, this.internalReplicationState.events.active.up]).subscribe(([down, up]) => {
var isActive = down || up;
this.subjects.active.next(isActive);
}));
if (this.pull && this.pull.stream$ && this.live) {
this.subs.push(this.pull.stream$.subscribe({
next: ev => {
if (!this.isStoppedOrPaused()) {
this.remoteEvents$.next(ev);
}
},
error: err => {
this.subjects.error.next(err);
}
}));
}
/**
* Non-live replications run once
* and then automatically get canceled.
*/
if (!this.live) {
await (0, _index3.awaitRxStorageReplicationFirstInSync)(this.internalReplicationState);
await (0, _index3.awaitRxStorageReplicationInSync)(this.internalReplicationState);
await this.cancel();
}
this.callOnStart();
};
_proto.pause = function pause() {
(0, _index2.ensureNotFalsy)(this.internalReplicationState).events.paused.next(true);
};
_proto.isPaused = function isPaused() {
return this.internalReplicationState ? this.internalReplicationState.events.paused.getValue() : false;
};
_proto.isStopped = function isStopped() {
if (this.subjects.canceled.getValue()) {
return true;
}
return false;
};
_proto.isStoppedOrPaused = function isStoppedOrPaused() {
return this.isPaused() || this.isStopped();
};
_proto.awaitInitialReplication = async function awaitInitialReplication() {
await this.startPromise;
return (0, _index3.awaitRxStorageReplicationFirstInSync)((0, _index2.ensureNotFalsy)(this.internalReplicationState));
}
/**
* Returns a promise that resolves when:
* - All local data is replicated with the remote
* - No replication cycle is running or in retry-state
*
* WARNING: USing this function directly in a multi-tab browser application
* is dangerous because only the leading instance will ever be replicated,
* so this promise will not resolve in the other tabs.
* For multi-tab support you should set and observe a flag in a local document.
*/;
_proto.awaitInSync = async function awaitInSync() {
await this.startPromise;
await (0, _index3.awaitRxStorageReplicationFirstInSync)((0, _index2.ensureNotFalsy)(this.internalReplicationState));
/**
* To reduce the amount of re-renders and make testing
* and to make the whole behavior more predictable,
* we await these things multiple times.
* For example the state might be in sync already and at the
* exact same time a pull.stream$ event comes in and we want to catch
* that in the same call to awaitInSync() instead of resolving
* while actually the state is not in sync.
*/
var t = 2;
while (t > 0) {
t--;
/**
* Often awaitInSync() is called directly after a document write,
* like in the unit tests.
* So we first have to await the idleness to ensure that all RxChangeEvents
* are processed already.
*/
await this.collection.database.requestIdlePromise();
await (0, _index3.awaitRxStorageReplicationInSync)((0, _index2.ensureNotFalsy)(this.internalReplicationState));
}
return true;
};
_proto.reSync = function reSync() {
this.remoteEvents$.next('RESYNC');
};
_proto.emitEvent = function emitEvent(ev) {
this.remoteEvents$.next(ev);
};
_proto.cancel = async function cancel() {
if (this.isStopped()) {
return _index2.PROMISE_RESOLVE_FALSE;
}
var promises = this.onCancel.map(fn => (0, _index2.toPromise)(fn()));
if (this.internalReplicationState) {
await (0, _index3.cancelRxStorageReplication)(this.internalReplicationState);
}
if (this.metaInstance) {
promises.push((0, _index2.ensureNotFalsy)(this.internalReplicationState).checkpointQueue.then(() => (0, _index2.ensureNotFalsy)(this.metaInstance).close()));
}
this.subs.forEach(sub => sub.unsubscribe());
this.subjects.canceled.next(true);
this.subjects.active.complete();
this.subjects.canceled.complete();
this.subjects.error.complete();
this.subjects.received.complete();
this.subjects.sent.complete();
return Promise.all(promises);
};
_proto.remove = async function remove() {
await (0, _index2.ensureNotFalsy)(this.metaInstance).remove();
var metaInfo = await this.metaInfoPromise;
await this.cancel();
await (0, _rxDatabaseInternalStore.removeConnectedStorageFromCollection)(this.collection, metaInfo.collectionName, metaInfo.schema);
};
return RxReplicationState;
}();
function replicateRxCollection({
replicationIdentifier,
collection,
deletedField = '_deleted',
pull,
push,
live = true,
retryTime = 1000 * 5,
waitForLeadership = true,
autoStart = true,
toggleOnDocumentVisible = false
}) {
(0, _plugin.addRxPlugin)(_index.RxDBLeaderElectionPlugin);
/**
* It is a common error to forget to add these config
* objects. So we check here because it makes no sense
* to start a replication with neither push nor pull.
*/
if (!pull && !push) {
throw (0, _rxError.newRxError)('UT3', {
collection: collection.name,
args: {
replicationIdentifier
}
});
}
var replicationState = new RxReplicationState(replicationIdentifier, collection, deletedField, pull, push, live, retryTime, autoStart, toggleOnDocumentVisible);
if (toggleOnDocumentVisible && typeof document !== 'undefined' && typeof document.addEventListener === 'function' && typeof document.visibilityState === 'string') {
var handler = () => {
if (replicationState.isStopped()) {
return;
}
var isVisible = document.visibilityState;
if (isVisible) {
replicationState.start();
} else {
/**
* Only pause if not the current leader.
* If no tab is visible, the elected leader should still continue
* the replication.
*/
if (!collection.database.isLeader()) {
replicationState.pause();
}
}
};
document.addEventListener('visibilitychange', handler);
replicationState.onCancel.push(() => document.removeEventListener('visibilitychange', handler));
}
startReplicationOnLeaderShip(waitForLeadership, replicationState);
return replicationState;
}
function startReplicationOnLeaderShip(waitForLeadership, replicationState) {
/**
* Always await this Promise to ensure that the current instance
* is leader when waitForLeadership=true
*/
var mustWaitForLeadership = waitForLeadership && replicationState.collection.database.multiInstance;
var waitTillRun = mustWaitForLeadership ? replicationState.collection.database.waitForLeadership() : _index2.PROMISE_RESOLVE_TRUE;
return waitTillRun.then(() => {
if (replicationState.isStopped()) {
return;
}
if (replicationState.autoStart) {
replicationState.start();
}
});
}
//# sourceMappingURL=index.js.map