UNPKG

rxdb

Version:

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

333 lines (312 loc) 12.4 kB
/** * These files contain the replication protocol. * It can be used to replicated RxStorageInstances or RxCollections * or even to do a client(s)-server replication. */ import { BehaviorSubject, combineLatest, filter, firstValueFrom, mergeMap, Subject } from 'rxjs'; import { getPrimaryFieldOfPrimaryKey } from '../rx-schema-helper.ts'; import type { BulkWriteRow, ById, DocumentsWithCheckpoint, RxConflictHandler, RxDocumentData, RxReplicationHandler, RxReplicationWriteToMasterRow, RxStorageInstance, RxStorageInstanceReplicationInput, RxStorageInstanceReplicationState, WithDeleted } from '../types/index.d.ts'; import { clone, ensureNotFalsy, flatClone, PROMISE_RESOLVE_VOID } from '../plugins/utils/index.ts'; import { getCheckpointKey } from './checkpoint.ts'; import { startReplicationDownstream } from './downstream.ts'; import { docStateToWriteDoc, getUnderlyingPersistentStorage, writeDocToDocState } from './helper.ts'; import { startReplicationUpstream } from './upstream.ts'; import { fillWriteDataForAttachmentsChange } from '../plugins/attachments/index.ts'; import { getChangedDocumentsSince } from '../rx-storage-helper.ts'; import { newRxError } from '../rx-error.ts'; export * from './checkpoint.ts'; export * from './downstream.ts'; export * from './upstream.ts'; export * from './meta-instance.ts'; export * from './conflicts.ts'; export * from './helper.ts'; export * from './default-conflict-handler.ts'; export function replicateRxStorageInstance<RxDocType>( input: RxStorageInstanceReplicationInput<RxDocType> ): RxStorageInstanceReplicationState<RxDocType> { input = flatClone(input); input.forkInstance = getUnderlyingPersistentStorage(input.forkInstance); input.metaInstance = getUnderlyingPersistentStorage(input.metaInstance); const checkpointKeyPromise = getCheckpointKey(input); const state: RxStorageInstanceReplicationState<RxDocType> = { primaryPath: getPrimaryFieldOfPrimaryKey(input.forkInstance.schema.primaryKey), hasAttachments: !!input.forkInstance.schema.attachments, input, checkpointKey: checkpointKeyPromise, downstreamBulkWriteFlag: checkpointKeyPromise.then(checkpointKey => 'replication-downstream-' + checkpointKey), events: { canceled: new BehaviorSubject<boolean>(false), paused: new BehaviorSubject<boolean>(false), active: { down: new BehaviorSubject<boolean>(true), up: new BehaviorSubject<boolean>(true) }, processed: { down: new Subject(), up: new Subject() }, resolvedConflicts: new Subject(), error: new Subject() }, stats: { down: { addNewTask: 0, downstreamProcessChanges: 0, downstreamResyncOnce: 0, masterChangeStreamEmit: 0, persistFromMaster: 0 }, up: { forkChangeStreamEmit: 0, persistToMaster: 0, persistToMasterConflictWrites: 0, persistToMasterHadConflicts: 0, processTasks: 0, upstreamInitialSync: 0 } }, firstSyncDone: { down: new BehaviorSubject<boolean>(false), up: new BehaviorSubject<boolean>(false) }, streamQueue: { down: PROMISE_RESOLVE_VOID, up: PROMISE_RESOLVE_VOID }, checkpointQueue: PROMISE_RESOLVE_VOID, lastCheckpointDoc: {} }; startReplicationDownstream(state); startReplicationUpstream(state); return state; } export function awaitRxStorageReplicationFirstInSync( state: RxStorageInstanceReplicationState<any> ): Promise<void> { return firstValueFrom( combineLatest([ state.firstSyncDone.down.pipe( filter(v => !!v) ), state.firstSyncDone.up.pipe( filter(v => !!v) ) ]) ).then(() => { }); } export function awaitRxStorageReplicationInSync( replicationState: RxStorageInstanceReplicationState<any> ) { return Promise.all([ replicationState.streamQueue.up, replicationState.streamQueue.down, replicationState.checkpointQueue ]); } export async function awaitRxStorageReplicationIdle( state: RxStorageInstanceReplicationState<any> ) { await awaitRxStorageReplicationFirstInSync(state); while (true) { const { down, up } = state.streamQueue; await Promise.all([ up, down ]); /** * If the Promises have not been reassigned * after awaiting them, we know that the replication * is in idle state at this point in time. */ if ( down === state.streamQueue.down && up === state.streamQueue.up ) { return; } } } export function rxStorageInstanceToReplicationHandler<RxDocType, MasterCheckpointType>( instance: RxStorageInstance<RxDocType, any, any, MasterCheckpointType>, conflictHandler: RxConflictHandler<RxDocType>, databaseInstanceToken: string, /** * If set to true, * the _meta.lwt from the pushed documents is kept. * (Used in the migration to ensure checkpoints are still valid) */ keepMeta: boolean = false ): RxReplicationHandler<RxDocType, MasterCheckpointType> { instance = getUnderlyingPersistentStorage(instance); const hasAttachments = !!instance.schema.attachments; const primaryPath = getPrimaryFieldOfPrimaryKey(instance.schema.primaryKey); const replicationHandler: RxReplicationHandler<RxDocType, MasterCheckpointType> = { masterChangeStream$: instance.changeStream().pipe( mergeMap(async (eventBulk) => { const ret: DocumentsWithCheckpoint<RxDocType, MasterCheckpointType> = { checkpoint: eventBulk.checkpoint, documents: await Promise.all( eventBulk.events.map(async (event) => { let docData = writeDocToDocState(event.documentData, hasAttachments, keepMeta); if (hasAttachments) { docData = await fillWriteDataForAttachmentsChange( primaryPath, instance, clone(docData), /** * Notice that the master never knows * the client state of the document. * Therefore we always send all attachments data. */ undefined ); } return docData; }) ) }; return ret; }) ), masterChangesSince( checkpoint, batchSize ) { return getChangedDocumentsSince( instance, batchSize, checkpoint ).then(async (result) => { return { checkpoint: result.documents.length > 0 ? result.checkpoint : checkpoint, documents: await Promise.all( result.documents.map(async (plainDocumentData) => { let docData = writeDocToDocState(plainDocumentData, hasAttachments, keepMeta); if (hasAttachments) { docData = await fillWriteDataForAttachmentsChange( primaryPath, instance, clone(docData), /** * Notice the the master never knows * the client state of the document. * Therefore we always send all attachments data. */ undefined ); } return docData; }) ) }; }); }, async masterWrite( rows ) { const rowById: ById<RxReplicationWriteToMasterRow<RxDocType>> = {}; rows.forEach(row => { const docId: string = (row.newDocumentState as any)[primaryPath]; rowById[docId] = row; }); const ids = Object.keys(rowById); const masterDocsStateList = await instance.findDocumentsById( ids, true ); const masterDocsState = new Map<string, RxDocumentData<RxDocType>>(); masterDocsStateList.forEach(doc => masterDocsState.set((doc as any)[primaryPath], doc)); const conflicts: WithDeleted<RxDocType>[] = []; const writeRows: BulkWriteRow<RxDocType>[] = []; await Promise.all( Object.entries(rowById) .map(([id, row]) => { const masterState = masterDocsState.get(id); if (!masterState) { writeRows.push({ document: docStateToWriteDoc(databaseInstanceToken, hasAttachments, keepMeta, row.newDocumentState) }); } else if ( masterState && !row.assumedMasterState ) { conflicts.push(writeDocToDocState(masterState, hasAttachments, keepMeta)); } else if ( conflictHandler.isEqual( writeDocToDocState(masterState, hasAttachments, keepMeta), ensureNotFalsy(row.assumedMasterState), 'rxStorageInstanceToReplicationHandler-masterWrite' ) === true ) { writeRows.push({ previous: masterState, document: docStateToWriteDoc(databaseInstanceToken, hasAttachments, keepMeta, row.newDocumentState, masterState) }); } else { conflicts.push(writeDocToDocState(masterState, hasAttachments, keepMeta)); } }) ); if (writeRows.length > 0) { const result = await instance.bulkWrite( writeRows, 'replication-master-write' ); result.error.forEach(err => { if (err.status !== 409) { throw newRxError('SNH', { name: 'non conflict error', error: err as any }); } else { conflicts.push( writeDocToDocState(ensureNotFalsy(err.documentInDb), hasAttachments, keepMeta) ); } }); } return conflicts; } }; return replicationHandler; } export async function cancelRxStorageReplication( replicationState: RxStorageInstanceReplicationState<any> ) { replicationState.events.canceled.next(true); replicationState.events.active.up.complete(); replicationState.events.active.down.complete(); replicationState.events.processed.up.complete(); replicationState.events.processed.down.complete(); replicationState.events.resolvedConflicts.complete(); replicationState.events.canceled.complete(); await replicationState.checkpointQueue; }