|
1 | | -import { isMongoNetworkTimeoutError, isMongoServerError, mongo } from '@powersync/lib-service-mongodb'; |
| 1 | +import { mongo } from '@powersync/lib-service-mongodb'; |
2 | 2 | import { |
3 | 3 | container, |
4 | 4 | DatabaseConnectionError, |
@@ -31,7 +31,7 @@ import { |
31 | 31 | STANDALONE_CHECKPOINT_ID |
32 | 32 | } from './MongoRelation.js'; |
33 | 33 | import { ChunkedSnapshotQuery } from './MongoSnapshotQuery.js'; |
34 | | -import { CHECKPOINTS_COLLECTION, timestampToDate } from './replication-utils.js'; |
| 34 | +import { CHECKPOINTS_COLLECTION, rawChangeStreamBatches, timestampToDate } from './replication-utils.js'; |
35 | 35 |
|
36 | 36 | export interface ChangeStreamOptions { |
37 | 37 | connections: MongoManager; |
@@ -710,124 +710,23 @@ export class ChangeStream { |
710 | 710 | } |
711 | 711 | } |
712 | 712 |
|
713 | | - private async *rawChangeStreamBatches(options: { |
| 713 | + private rawChangeStreamBatches(options: { |
714 | 714 | lsn: string | null; |
715 | 715 | maxAwaitTimeMs?: number; |
716 | 716 | batchSize?: number; |
717 | 717 | filters: { $match: any; multipleDatabases: boolean }; |
718 | 718 | signal?: AbortSignal; |
719 | 719 | }): AsyncIterableIterator<{ eventBatch: mongo.ChangeStreamDocument[]; resumeToken: unknown }> { |
720 | | - const lastLsn = options.lsn ? MongoLSN.fromSerialized(options.lsn) : null; |
721 | | - const startAfter = lastLsn?.timestamp; |
722 | | - const resumeAfter = lastLsn?.resumeToken; |
723 | | - |
724 | | - const filters = options.filters; |
725 | | - |
726 | | - let fullDocument: 'required' | 'updateLookup'; |
727 | | - |
728 | | - if (this.usePostImages) { |
729 | | - // 'read_only' or 'auto_configure' |
730 | | - // Configuration happens during snapshot, or when we see new |
731 | | - // collections. |
732 | | - fullDocument = 'required'; |
733 | | - } else { |
734 | | - fullDocument = 'updateLookup'; |
735 | | - } |
736 | | - |
737 | | - const streamOptions: mongo.ChangeStreamOptions = { |
738 | | - showExpandedEvents: true, |
739 | | - fullDocument: fullDocument |
740 | | - }; |
741 | | - /** |
742 | | - * Only one of these options can be supplied at a time. |
743 | | - */ |
744 | | - if (resumeAfter) { |
745 | | - streamOptions.resumeAfter = resumeAfter; |
746 | | - } else { |
747 | | - // Legacy: We don't persist lsns without resumeTokens anymore, but we do still handle the |
748 | | - // case if we have an old one. |
749 | | - streamOptions.startAtOperationTime = startAfter; |
750 | | - } |
751 | | - |
752 | | - const pipeline: mongo.Document[] = [ |
753 | | - { |
754 | | - $changeStream: streamOptions |
755 | | - }, |
756 | | - { |
757 | | - $match: filters.$match |
758 | | - }, |
759 | | - { $changeStreamSplitLargeEvent: {} } |
760 | | - ]; |
761 | | - |
762 | | - let cursorId: bigint | null = null; |
763 | | - |
764 | | - const db = filters.multipleDatabases ? this.client.db('admin') : this.defaultDb; |
765 | | - const maxTimeMS = options.maxAwaitTimeMs ?? this.maxAwaitTimeMS; |
766 | | - const batchSize = options.batchSize ?? this.snapshotChunkLength; |
767 | | - options?.signal?.addEventListener('abort', () => { |
768 | | - if (cursorId != null && cursorId !== 0n) { |
769 | | - // This would result in a CursorKilled error. |
770 | | - db.command({ |
771 | | - killCursors: '$cmd.aggregate', |
772 | | - cursors: [cursorId] |
773 | | - }); |
774 | | - } |
| 720 | + return rawChangeStreamBatches({ |
| 721 | + client: this.client, |
| 722 | + filters: options.filters, |
| 723 | + db: options.filters.multipleDatabases ? this.client.db('admin') : this.defaultDb, |
| 724 | + batchSize: options.batchSize ?? this.snapshotChunkLength, |
| 725 | + maxAwaitTimeMs: options.maxAwaitTimeMs ?? this.maxAwaitTimeMS, |
| 726 | + lsn: options.lsn, |
| 727 | + usePostImages: this.usePostImages, |
| 728 | + signal: options.signal |
775 | 729 | }); |
776 | | - |
777 | | - const session = this.client.startSession(); |
778 | | - try { |
779 | | - // Step 1: Send the aggregate command to start the change stream |
780 | | - const aggregateResult = await db |
781 | | - .command( |
782 | | - { |
783 | | - aggregate: 1, |
784 | | - pipeline, |
785 | | - cursor: { batchSize } |
786 | | - }, |
787 | | - { session } |
788 | | - ) |
789 | | - .catch((e) => { |
790 | | - throw mapChangeStreamError(e); |
791 | | - }); |
792 | | - |
793 | | - cursorId = BigInt(aggregateResult.cursor.id); |
794 | | - let batch = aggregateResult.cursor.firstBatch; |
795 | | - |
796 | | - yield { eventBatch: batch, resumeToken: aggregateResult.cursor.postBatchResumeToken }; |
797 | | - |
798 | | - // Step 2: Poll using getMore until the cursor is closed |
799 | | - while (cursorId && cursorId !== 0n) { |
800 | | - if (options.signal?.aborted) { |
801 | | - break; |
802 | | - } |
803 | | - const getMoreResult: mongo.Document = await db |
804 | | - .command( |
805 | | - { |
806 | | - getMore: cursorId, |
807 | | - collection: '$cmd.aggregate', |
808 | | - batchSize, |
809 | | - maxTimeMS |
810 | | - }, |
811 | | - { session } |
812 | | - ) |
813 | | - .catch((e) => { |
814 | | - throw mapChangeStreamError(e); |
815 | | - }); |
816 | | - |
817 | | - cursorId = BigInt(getMoreResult.cursor.id); |
818 | | - const nextBatch = getMoreResult.cursor.nextBatch; |
819 | | - |
820 | | - yield { eventBatch: nextBatch, resumeToken: getMoreResult.cursor.postBatchResumeToken }; |
821 | | - } |
822 | | - } finally { |
823 | | - if (cursorId != null && cursorId !== 0n) { |
824 | | - await db.command({ |
825 | | - killCursors: '$cmd.aggregate', |
826 | | - cursors: [cursorId] |
827 | | - }); |
828 | | - } |
829 | | - await session.endSession(); |
830 | | - } |
831 | 730 | } |
832 | 731 |
|
833 | 732 | async streamChangesInternal() { |
@@ -1128,21 +1027,3 @@ export class ChangeStream { |
1128 | 1027 | } |
1129 | 1028 | } |
1130 | 1029 | } |
1131 | | - |
1132 | | -function mapChangeStreamError(e: any) { |
1133 | | - if (isMongoNetworkTimeoutError(e)) { |
1134 | | - // This typically has an unhelpful message like "connection 2 to 159.41.94.47:27017 timed out". |
1135 | | - // We wrap the error to make it more useful. |
1136 | | - throw new DatabaseConnectionError(ErrorCode.PSYNC_S1345, `Timeout while reading MongoDB ChangeStream`, e); |
1137 | | - } else if ( |
1138 | | - isMongoServerError(e) && |
1139 | | - e.codeName == 'NoMatchingDocument' && |
1140 | | - e.errmsg?.includes('post-image was not found') |
1141 | | - ) { |
1142 | | - throw new ChangeStreamInvalidatedError(e.errmsg, e); |
1143 | | - } else if (isMongoServerError(e) && e.hasErrorLabel('NonResumableChangeStreamError')) { |
1144 | | - throw new ChangeStreamInvalidatedError(e.message, e); |
1145 | | - } else { |
1146 | | - throw new DatabaseConnectionError(ErrorCode.PSYNC_S1346, `Error reading MongoDB ChangeStream`, e); |
1147 | | - } |
1148 | | -} |
0 commit comments