rxdb
Version:
A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/
225 lines (220 loc) • 10.5 kB
JavaScript
import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose";
import { appendToArray, asyncFilter, ensureNotFalsy, errorToPlainJson, flatClone, lastOfArray, toArray } from "../../plugins/utils/index.js";
import { doc, query, where, orderBy, limit, getDocs, onSnapshot, runTransaction, writeBatch, serverTimestamp, waitForPendingWrites, documentId } from 'firebase/firestore';
import { RxDBLeaderElectionPlugin } from "../leader-election/index.js";
import { RxReplicationState, startReplicationOnLeaderShip } from "../replication/index.js";
import { addRxPlugin, getSchemaByObjectPath, newRxError } from "../../index.js";
import { Subject } from 'rxjs';
import { firestoreRowToDocData, getContentByIds, isoStringToServerTimestamp, serverTimestampToIsoString, stripPrimaryKey, stripServerTimestampField } from "./firestore-helper.js";
export * from "./firestore-helper.js";
export * from "./firestore-types.js";
export var RxFirestoreReplicationState = /*#__PURE__*/function (_RxReplicationState) {
function RxFirestoreReplicationState(firestore, replicationIdentifierHash, collection, pull, push, live = true, retryTime = 1000 * 5, autoStart = true) {
var _this;
_this = _RxReplicationState.call(this, replicationIdentifierHash, collection, '_deleted', pull, push, live, retryTime, autoStart) || this;
_this.firestore = firestore;
_this.replicationIdentifierHash = replicationIdentifierHash;
_this.collection = collection;
_this.pull = pull;
_this.push = push;
_this.live = live;
_this.retryTime = retryTime;
_this.autoStart = autoStart;
return _this;
}
_inheritsLoose(RxFirestoreReplicationState, _RxReplicationState);
return RxFirestoreReplicationState;
}(RxReplicationState);
export function replicateFirestore(options) {
var collection = options.collection;
addRxPlugin(RxDBLeaderElectionPlugin);
var pullStream$ = new Subject();
var replicationPrimitivesPull;
options.live = typeof options.live === 'undefined' ? true : options.live;
options.waitForLeadership = typeof options.waitForLeadership === 'undefined' ? true : options.waitForLeadership;
var serverTimestampField = typeof options.serverTimestampField === 'undefined' ? 'serverTimestamp' : options.serverTimestampField;
options.serverTimestampField = serverTimestampField;
var primaryPath = collection.schema.primaryPath;
/**
* The serverTimestampField MUST NOT be part of the collections RxJsonSchema.
*/
var schemaPart = getSchemaByObjectPath(collection.schema.jsonSchema, serverTimestampField);
if (schemaPart ||
// also must not be nested.
serverTimestampField.includes('.')) {
throw newRxError('RC6', {
field: serverTimestampField,
schema: collection.schema.jsonSchema
});
}
var pullFilters = options.pull?.filter !== undefined ? toArray(options.pull.filter) : [];
var pullQuery = query(options.firestore.collection, ...pullFilters);
if (options.pull) {
replicationPrimitivesPull = {
async handler(lastPulledCheckpoint, batchSize) {
var newerQuery;
var sameTimeQuery;
if (lastPulledCheckpoint) {
var lastServerTimestamp = isoStringToServerTimestamp(lastPulledCheckpoint.serverTimestamp);
newerQuery = query(pullQuery, where(serverTimestampField, '>', lastServerTimestamp), orderBy(serverTimestampField, 'asc'), limit(batchSize));
sameTimeQuery = query(pullQuery, where(serverTimestampField, '==', lastServerTimestamp), where(documentId(), '>', lastPulledCheckpoint.id), orderBy(documentId(), 'asc'), limit(batchSize));
} else {
newerQuery = query(pullQuery, orderBy(serverTimestampField, 'asc'), limit(batchSize));
}
var mustsReRun = true;
var useDocs = [];
while (mustsReRun) {
/**
* Local writes that have not been persisted to the server
* are in pending state and do not have a correct serverTimestamp set.
* We have to ensure we only use document states that are in sync with the server.
* @link https://medium.com/firebase-developers/the-secrets-of-firestore-fieldvalue-servertimestamp-revealed-29dd7a38a82b
*/
await waitForPendingWrites(options.firestore.database);
await runTransaction(options.firestore.database, async _tx => {
useDocs = [];
var [newerQueryResult, sameTimeQueryResult] = await Promise.all([getDocs(newerQuery), sameTimeQuery ? getDocs(sameTimeQuery) : undefined]);
if (newerQueryResult.metadata.hasPendingWrites || sameTimeQuery && ensureNotFalsy(sameTimeQueryResult).metadata.hasPendingWrites) {
return;
} else {
mustsReRun = false;
if (sameTimeQuery) {
useDocs = ensureNotFalsy(sameTimeQueryResult).docs;
}
var missingAmount = batchSize - useDocs.length;
if (missingAmount > 0) {
var additionalDocs = newerQueryResult.docs.slice(0, missingAmount).filter(x => !!x);
appendToArray(useDocs, additionalDocs);
}
}
});
}
if (useDocs.length === 0) {
return {
checkpoint: lastPulledCheckpoint ?? null,
documents: []
};
}
var lastDoc = ensureNotFalsy(lastOfArray(useDocs));
var documents = useDocs.map(row => firestoreRowToDocData(serverTimestampField, primaryPath, row));
var newCheckpoint = {
id: lastDoc.id,
serverTimestamp: serverTimestampToIsoString(serverTimestampField, lastDoc.data())
};
var ret = {
documents: documents,
checkpoint: newCheckpoint
};
return ret;
},
batchSize: ensureNotFalsy(options.pull).batchSize,
modifier: ensureNotFalsy(options.pull).modifier,
stream$: pullStream$.asObservable()
};
}
var replicationPrimitivesPush;
if (options.push) {
var pushFilter = options.push?.filter;
replicationPrimitivesPush = {
async handler(rows) {
if (pushFilter !== undefined) {
rows = await asyncFilter(rows, row => pushFilter(row.newDocumentState));
}
var writeRowsById = {};
var docIds = rows.map(row => {
var docId = row.newDocumentState[primaryPath];
writeRowsById[docId] = row;
return docId;
});
await waitForPendingWrites(options.firestore.database);
var conflicts = [];
/**
* Everything must run INSIDE of the transaction
* because on tx-errors, firebase will re-run the transaction on some cases.
* @link https://firebase.google.com/docs/firestore/manage-data/transactions#transaction_failure
* @link https://firebase.google.com/docs/firestore/manage-data/transactions
*/
await runTransaction(options.firestore.database, async _tx => {
conflicts = []; // reset in case the tx has re-run.
/**
* @link https://stackoverflow.com/a/48423626/3443137
*/
var getQuery = ids => {
return getDocs(query(options.firestore.collection, where(documentId(), 'in', ids)));
};
var docsInDbResult = await getContentByIds(docIds, getQuery);
var docsInDbById = {};
docsInDbResult.forEach(row => {
var docDataInDb = stripServerTimestampField(serverTimestampField, row.data());
var docId = row.id;
docDataInDb[primaryPath] = docId;
docsInDbById[docId] = docDataInDb;
});
/**
* @link https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes
*/
var batch = writeBatch(options.firestore.database);
var hasWrite = false;
await Promise.all(Object.entries(writeRowsById).map(async ([docId, writeRow]) => {
var docInDb = docsInDbById[docId];
if (docInDb && (!writeRow.assumedMasterState || collection.conflictHandler.isEqual(docInDb, writeRow.assumedMasterState, 'replication-firestore-push') === false)) {
// conflict
conflicts.push(docInDb);
} else {
// no conflict
hasWrite = true;
var docRef = doc(options.firestore.collection, docId);
var writeDocData = flatClone(writeRow.newDocumentState);
writeDocData[serverTimestampField] = serverTimestamp();
if (!docInDb) {
// insert
batch.set(docRef, stripPrimaryKey(primaryPath, writeDocData));
} else {
// update
batch.update(docRef, stripPrimaryKey(primaryPath, writeDocData));
}
}
}));
if (hasWrite) {
await batch.commit();
}
});
await waitForPendingWrites(options.firestore.database);
return conflicts;
},
batchSize: options.push.batchSize,
modifier: options.push.modifier
};
}
var replicationState = new RxFirestoreReplicationState(options.firestore, options.replicationIdentifier, collection, replicationPrimitivesPull, replicationPrimitivesPush, options.live, options.retryTime, options.autoStart);
/**
* Use long polling to get live changes for the pull.stream$
*/
if (options.live && options.pull) {
var startBefore = replicationState.start.bind(replicationState);
var cancelBefore = replicationState.cancel.bind(replicationState);
replicationState.start = () => {
var lastChangeQuery = query(pullQuery, orderBy(serverTimestampField, 'desc'), limit(1));
var unsubscribe = onSnapshot(lastChangeQuery, _querySnapshot => {
/**
* There is no good way to observe the event stream in firestore.
* So instead we listen to any write to the collection
* and then emit a 'RESYNC' flag.
*/
replicationState.reSync();
}, error => {
replicationState.subjects.error.next(newRxError('RC_STREAM', {
error: errorToPlainJson(error)
}));
});
replicationState.cancel = () => {
unsubscribe();
return cancelBefore();
};
return startBefore();
};
}
startReplicationOnLeaderShip(options.waitForLeadership, replicationState);
return replicationState;
}
//# sourceMappingURL=index.js.map