UNPKG

rxdb

Version:

A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/

352 lines (343 loc) 14.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RxMigrationState = void 0; var _rxjs = require("rxjs"); var _rxError = require("../../rx-error.js"); var _migrationHelpers = require("./migration-helpers.js"); var _index = require("../utils/index.js"); var _rxStorageHelper = require("../../rx-storage-helper.js"); var _broadcastChannel = require("broadcast-channel"); var _index2 = require("../../replication-protocol/index.js"); var _overwritable = require("../../overwritable.js"); var _rxDatabaseInternalStore = require("../../rx-database-internal-store.js"); var _rxQueryHelper = require("../../rx-query-helper.js"); var RxMigrationState = exports.RxMigrationState = /*#__PURE__*/function () { function RxMigrationState(collection, migrationStrategies, statusDocKey = [collection.name, 'v', collection.schema.version].join('-')) { this.started = false; this.updateStatusHandlers = []; this.updateStatusQueue = _index.PROMISE_RESOLVE_TRUE; this.collection = collection; this.migrationStrategies = migrationStrategies; this.statusDocKey = statusDocKey; this.database = collection.database; this.oldCollectionMeta = (0, _migrationHelpers.getOldCollectionMeta)(this); this.mustMigrate = (0, _migrationHelpers.mustMigrate)(this); this.statusDocId = (0, _rxDatabaseInternalStore.getPrimaryKeyOfInternalDocument)(this.statusDocKey, _rxDatabaseInternalStore.INTERNAL_CONTEXT_MIGRATION_STATUS); (0, _migrationHelpers.addMigrationStateToDatabase)(this); this.$ = (0, _rxStorageHelper.observeSingle)(this.database.internalStore, this.statusDocId).pipe((0, _rxjs.filter)(d => !!d), (0, _rxjs.map)(d => (0, _index.ensureNotFalsy)(d).data), (0, _rxjs.shareReplay)(_index.RXJS_SHARE_REPLAY_DEFAULTS)); } var _proto = RxMigrationState.prototype; _proto.getStatus = function getStatus() { return (0, _rxjs.firstValueFrom)(this.$); } /** * Starts the migration. * Returns void so that people to not get the idea to await * this function. * Instead use migratePromise() if you want to await * the migration. This ensures it works even if the migration * is run on a different browser tab. */; _proto.startMigration = async function startMigration(batchSize = _migrationHelpers.MIGRATION_DEFAULT_BATCH_SIZE) { var must = await this.mustMigrate; if (!must) { return; } if (this.started) { throw (0, _rxError.newRxError)('DM1'); } this.started = true; var broadcastChannel = undefined; /** * To ensure that multiple tabs do not migrate the same collection, * we use a new broadcastChannel/leaderElector for each collection. * This is required because collections can be added dynamically and * not all tabs might know about this collection. */ if (this.database.multiInstance) { broadcastChannel = new _broadcastChannel.BroadcastChannel(['rx-migration-state', this.database.name, this.collection.name, this.collection.schema.version].join('|')); var leaderElector = (0, _broadcastChannel.createLeaderElection)(broadcastChannel); await leaderElector.awaitLeadership(); } /** * Instead of writing a custom migration protocol, * we do a push-only replication from the old collection data to the new one. * This also ensure that restarting the replication works without problems. */ var oldCollectionMeta = await this.oldCollectionMeta; var oldStorageInstance = await this.database.storage.createStorageInstance({ databaseName: this.database.name, collectionName: this.collection.name, databaseInstanceToken: this.database.token, multiInstance: this.database.multiInstance, options: {}, schema: oldCollectionMeta.data.schema, password: this.database.password, devMode: _overwritable.overwritable.isDevMode() }); var connectedInstances = await this.getConnectedStorageInstances(); /** * Initially write the migration status into a meta document. */ var totalCount = await this.countAllDoucments([oldStorageInstance].concat(connectedInstances.map(r => r.oldStorage))); await this.updateStatus(s => { s.count.total = totalCount; return s; }); try { /** * First migrate the connected storages, * afterwards migrate the normal collection. */ await Promise.all(connectedInstances.map(async connectedInstance => { await (0, _rxDatabaseInternalStore.addConnectedStorageToCollection)(this.collection, connectedInstance.newStorage.collectionName, connectedInstance.newStorage.schema); await this.migrateStorage(connectedInstance.oldStorage, connectedInstance.newStorage, batchSize); await connectedInstance.newStorage.close(); })); await this.migrateStorage(oldStorageInstance, /** * Use the originalStorageInstance here * so that the _meta.lwt time keeps the same * and our replication checkpoints still point to the * correct checkpoint. */ this.collection.storageInstance.originalStorageInstance, batchSize); } catch (err) { await oldStorageInstance.close(); await this.updateStatus(s => { s.status = 'ERROR'; s.error = (0, _index.errorToPlainJson)(err); return s; }); return; } // remove old collection meta doc await (0, _rxStorageHelper.writeSingle)(this.database.internalStore, { previous: oldCollectionMeta, document: Object.assign({}, oldCollectionMeta, { _deleted: true }) }, 'rx-migration-remove-collection-meta'); await this.updateStatus(s => { s.status = 'DONE'; return s; }); if (broadcastChannel) { await broadcastChannel.close(); } }; _proto.updateStatus = function updateStatus(handler) { this.updateStatusHandlers.push(handler); this.updateStatusQueue = this.updateStatusQueue.then(async () => { if (this.updateStatusHandlers.length === 0) { return; } // re-run until no conflict var useHandlers = this.updateStatusHandlers; this.updateStatusHandlers = []; while (true) { var previous = await (0, _rxStorageHelper.getSingleDocument)(this.database.internalStore, this.statusDocId); var newDoc = (0, _index.clone)(previous); if (!previous) { newDoc = { id: this.statusDocId, key: this.statusDocKey, context: _rxDatabaseInternalStore.INTERNAL_CONTEXT_MIGRATION_STATUS, data: { collectionName: this.collection.name, status: 'RUNNING', count: { total: 0, handled: 0, percent: 0 } }, _deleted: false, _meta: (0, _index.getDefaultRxDocumentMeta)(), _rev: (0, _index.getDefaultRevision)(), _attachments: {} }; } var status = (0, _index.ensureNotFalsy)(newDoc).data; for (var oneHandler of useHandlers) { status = oneHandler(status); } status.count.percent = Math.round(status.count.handled / status.count.total * 100); if (newDoc && previous && (0, _index.deepEqual)(newDoc.data, previous.data)) { break; } try { await (0, _rxStorageHelper.writeSingle)(this.database.internalStore, { previous, document: (0, _index.ensureNotFalsy)(newDoc) }, _rxDatabaseInternalStore.INTERNAL_CONTEXT_MIGRATION_STATUS); // write successful break; } catch (err) { // ignore conflicts if (!(0, _rxError.isBulkWriteConflictError)(err)) { throw err; } } } }); return this.updateStatusQueue; }; _proto.migrateStorage = async function migrateStorage(oldStorage, newStorage, batchSize) { var replicationMetaStorageInstance = await this.database.storage.createStorageInstance({ databaseName: this.database.name, collectionName: 'rx-migration-state-meta-' + this.collection.name + '-' + this.collection.schema.version, databaseInstanceToken: this.database.token, multiInstance: this.database.multiInstance, options: {}, schema: (0, _index2.getRxReplicationMetaInstanceSchema)(oldStorage.schema, (0, _rxStorageHelper.hasEncryption)(oldStorage.schema)), password: this.database.password, devMode: _overwritable.overwritable.isDevMode() }); var replicationHandlerBase = (0, _index2.rxStorageInstanceToReplicationHandler)(newStorage, /** * Ignore push-conflicts. * If this happens we drop the 'old' document state. */ _index2.defaultConflictHandler, this.database.token, true); var replicationState = (0, _index2.replicateRxStorageInstance)({ keepMeta: true, identifier: ['rx-migration-state', this.collection.name, oldStorage.schema.version, this.collection.schema.version].join('-'), replicationHandler: { masterChangesSince() { return Promise.resolve({ checkpoint: null, documents: [] }); }, masterWrite: async rows => { rows = await Promise.all(rows.map(async row => { var newDocData = row.newDocumentState; if (newStorage.schema.title === _index2.META_INSTANCE_SCHEMA_TITLE) { newDocData = row.newDocumentState.docData; if (row.newDocumentState.isCheckpoint === '1') { return { assumedMasterState: undefined, newDocumentState: row.newDocumentState }; } } var migratedDocData = await (0, _migrationHelpers.migrateDocumentData)(this.collection, oldStorage.schema.version, newDocData); var newRow = { // drop the assumed master state, we do not have to care about conflicts here. assumedMasterState: undefined, newDocumentState: newStorage.schema.title === _index2.META_INSTANCE_SCHEMA_TITLE ? Object.assign({}, row.newDocumentState, { docData: migratedDocData }) : migratedDocData }; return newRow; })); // filter out the documents where the migration strategy returned null rows = rows.filter(row => !!row.newDocumentState); var result = await replicationHandlerBase.masterWrite(rows); return result; }, masterChangeStream$: new _rxjs.Subject().asObservable() }, forkInstance: oldStorage, metaInstance: replicationMetaStorageInstance, pushBatchSize: batchSize, pullBatchSize: 0, conflictHandler: _index2.defaultConflictHandler, hashFunction: this.database.hashFunction }); var hasError = false; replicationState.events.error.subscribe(err => hasError = err); // update replication status on each change replicationState.events.processed.up.subscribe(() => { this.updateStatus(status => { status.count.handled = status.count.handled + 1; return status; }); }); await (0, _index2.awaitRxStorageReplicationFirstInSync)(replicationState); await (0, _index2.cancelRxStorageReplication)(replicationState); await this.updateStatusQueue; if (hasError) { await replicationMetaStorageInstance.close(); throw hasError; } // cleanup old storages await Promise.all([oldStorage.remove(), replicationMetaStorageInstance.remove()]); }; _proto.countAllDoucments = async function countAllDoucments(storageInstances) { var ret = 0; await Promise.all(storageInstances.map(async instance => { var preparedQuery = (0, _rxQueryHelper.prepareQuery)(instance.schema, (0, _rxQueryHelper.normalizeMangoQuery)(instance.schema, { selector: {} })); var countResult = await instance.count(preparedQuery); ret += countResult.count; })); return ret; }; _proto.getConnectedStorageInstances = async function getConnectedStorageInstances() { var oldCollectionMeta = await this.oldCollectionMeta; var ret = []; await Promise.all(await Promise.all(oldCollectionMeta.data.connectedStorages.map(async connectedStorage => { // atm we can only migrate replication states. if (connectedStorage.schema.title !== _index2.META_INSTANCE_SCHEMA_TITLE) { throw new Error('unknown migration handling for schema'); } var newSchema = (0, _index2.getRxReplicationMetaInstanceSchema)((0, _index.clone)(this.collection.schema.jsonSchema), (0, _rxStorageHelper.hasEncryption)(connectedStorage.schema)); newSchema.version = this.collection.schema.version; var [oldStorage, newStorage] = await Promise.all([this.database.storage.createStorageInstance({ databaseInstanceToken: this.database.token, databaseName: this.database.name, devMode: _overwritable.overwritable.isDevMode(), multiInstance: this.database.multiInstance, options: {}, schema: connectedStorage.schema, password: this.database.password, collectionName: connectedStorage.collectionName }), this.database.storage.createStorageInstance({ databaseInstanceToken: this.database.token, databaseName: this.database.name, devMode: _overwritable.overwritable.isDevMode(), multiInstance: this.database.multiInstance, options: {}, schema: newSchema, password: this.database.password, collectionName: connectedStorage.collectionName })]); ret.push({ oldStorage, newStorage }); }))); return ret; }; _proto.migratePromise = async function migratePromise(batchSize) { this.startMigration(batchSize); var must = await this.mustMigrate; if (!must) { return { status: 'DONE', collectionName: this.collection.name, count: { handled: 0, percent: 0, total: 0 } }; } var result = await Promise.race([(0, _rxjs.firstValueFrom)(this.$.pipe((0, _rxjs.filter)(d => d.status === 'DONE'))), (0, _rxjs.firstValueFrom)(this.$.pipe((0, _rxjs.filter)(d => d.status === 'ERROR')))]); if (result.status === 'ERROR') { throw (0, _rxError.newRxError)('DM4', { collection: this.collection.name, error: result.error }); } else { return result; } }; return RxMigrationState; }(); //# sourceMappingURL=rx-migration-state.js.map