rxdb
Version:
A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/
183 lines (180 loc) • 6.83 kB
JavaScript
import * as path from 'node:path';
import { BehaviorSubject, firstValueFrom, Subject } from 'rxjs';
import { filter, map } from 'rxjs';
import { getFromMapOrCreate, PROMISE_RESOLVE_FALSE, PROMISE_RESOLVE_TRUE, PROMISE_RESOLVE_VOID } from "../../plugins/utils/index.js";
import { clearFolder, deleteFolder, documentFolder, ensureFolderExists, getMeta, prepareFolders, setMeta, writeJsonToFile, writeToFile } from "./file-util.js";
import { getChangedDocumentsSince } from "../../rx-storage-helper.js";
/**
* Backups a single documents,
* returns the paths to all written files
*/
export async function backupSingleDocument(rxDocument, options) {
var data = rxDocument.toJSON(true);
var writtenFiles = [];
var docFolder = documentFolder(options, rxDocument.primary);
await clearFolder(docFolder);
var fileLocation = path.join(docFolder, 'document.json');
await writeJsonToFile(fileLocation, data);
writtenFiles.push(fileLocation);
if (options.attachments) {
var attachmentsFolder = path.join(docFolder, 'attachments');
ensureFolderExists(attachmentsFolder);
var attachments = rxDocument.allAttachments();
await Promise.all(attachments.map(async attachment => {
var content = await attachment.getData();
var attachmentFileLocation = path.join(attachmentsFolder, attachment.id);
await writeToFile(attachmentFileLocation, content);
writtenFiles.push(attachmentFileLocation);
}));
}
return writtenFiles;
}
var BACKUP_STATES_BY_DB = new WeakMap();
function addToBackupStates(db, state) {
var ar = getFromMapOrCreate(BACKUP_STATES_BY_DB, db, () => []);
ar.push(state);
}
export var RxBackupState = /*#__PURE__*/function () {
function RxBackupState(database, options) {
this.isStopped = false;
this.subs = [];
this.persistRunning = PROMISE_RESOLVE_VOID;
this.initialReplicationDone$ = new BehaviorSubject(false);
this.internalWriteEvents$ = new Subject();
this.writeEvents$ = this.internalWriteEvents$.asObservable();
this.database = database;
this.options = options;
if (!this.options.batchSize) {
this.options.batchSize = 10;
}
addToBackupStates(database, this);
prepareFolders(database, options);
}
/**
* Persists all data from all collections,
* beginning from the oldest sequence checkpoint
* to the newest one.
* Do not call this while it is already running.
* Returns true if there are more documents to process
*/
var _proto = RxBackupState.prototype;
_proto.persistOnce = function persistOnce() {
return this.persistRunning = this.persistRunning.then(() => this._persistOnce());
};
_proto._persistOnce = async function _persistOnce() {
var _this = this;
var meta = await getMeta(this.options);
await Promise.all(Object.entries(this.database.collections).map(async ([collectionName, collection]) => {
var primaryKey = collection.schema.primaryPath;
var processedDocuments = new Set();
await this.database.requestIdlePromise();
if (!meta.collectionStates[collectionName]) {
meta.collectionStates[collectionName] = {};
}
var lastCheckpoint = meta.collectionStates[collectionName].checkpoint;
var hasMore = true;
var _loop = async function () {
await _this.database.requestIdlePromise();
var changesResult = await getChangedDocumentsSince(collection.storageInstance, _this.options.batchSize ? _this.options.batchSize : 0, lastCheckpoint);
lastCheckpoint = changesResult.documents.length > 0 ? changesResult.checkpoint : lastCheckpoint;
meta.collectionStates[collectionName].checkpoint = lastCheckpoint;
var docIds = changesResult.documents.map(doc => doc[primaryKey]).filter(id => {
if (processedDocuments.has(id)) {
return false;
} else {
processedDocuments.add(id);
return true;
}
}).filter((elem, pos, arr) => arr.indexOf(elem) === pos); // unique
await _this.database.requestIdlePromise();
var docs = await collection.findByIds(docIds).exec();
if (docs.size === 0) {
hasMore = false;
return 1; // continue
}
await Promise.all(Array.from(docs.values()).map(async doc => {
var writtenFiles = await backupSingleDocument(doc, _this.options);
_this.internalWriteEvents$.next({
collectionName: collection.name,
documentId: doc.primary,
files: writtenFiles,
deleted: false
});
}));
// handle deleted documents
await Promise.all(docIds.filter(docId => !docs.has(docId)).map(async docId => {
await deleteFolder(documentFolder(_this.options, docId));
_this.internalWriteEvents$.next({
collectionName: collection.name,
documentId: docId,
files: [],
deleted: true
});
}));
};
while (hasMore && !this.isStopped) {
if (await _loop()) continue;
}
meta.collectionStates[collectionName].checkpoint = lastCheckpoint;
await setMeta(this.options, meta);
}));
if (!this.initialReplicationDone$.getValue()) {
this.initialReplicationDone$.next(true);
}
};
_proto.watchForChanges = function watchForChanges() {
var collections = Object.values(this.database.collections);
collections.forEach(collection => {
var changes$ = collection.storageInstance.changeStream();
var sub = changes$.subscribe(() => {
this.persistOnce();
});
this.subs.push(sub);
});
}
/**
* Returns a promise that resolves when the initial backup is done
* and the filesystem is in sync with the database state
*/;
_proto.awaitInitialBackup = function awaitInitialBackup() {
return firstValueFrom(this.initialReplicationDone$.pipe(filter(v => !!v), map(() => true)));
};
_proto.cancel = function cancel() {
if (this.isStopped) {
return PROMISE_RESOLVE_FALSE;
}
this.isStopped = true;
this.subs.forEach(sub => sub.unsubscribe());
return PROMISE_RESOLVE_TRUE;
};
return RxBackupState;
}();
export function backup(options) {
var backupState = new RxBackupState(this, options);
backupState.persistOnce();
if (options.live) {
backupState.watchForChanges();
}
return backupState;
}
export * from "./file-util.js";
export var RxDBBackupPlugin = {
name: 'backup',
rxdb: true,
prototypes: {
RxDatabase(proto) {
proto.backup = backup;
}
},
hooks: {
preCloseRxDatabase: {
after: function preCloseRxDatabase(db) {
var states = BACKUP_STATES_BY_DB.get(db);
if (states) {
states.forEach(state => state.cancel());
}
}
}
}
};
//# sourceMappingURL=index.js.map