@powersync/service-module-mongodb-storage 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +35 -15
  3. package/dist/storage/MongoBucketStorage.js +309 -86
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/CheckpointState.d.ts +20 -0
  6. package/dist/storage/implementation/CheckpointState.js +31 -0
  7. package/dist/storage/implementation/CheckpointState.js.map +1 -0
  8. package/dist/storage/implementation/MongoBucketBatch.d.ts +69 -39
  9. package/dist/storage/implementation/MongoBucketBatch.js +104 -313
  10. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  11. package/dist/storage/implementation/MongoChecksums.d.ts +11 -9
  12. package/dist/storage/implementation/MongoChecksums.js +3 -111
  13. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  14. package/dist/storage/implementation/MongoCompactor.d.ts +3 -1
  15. package/dist/storage/implementation/MongoCompactor.js +18 -9
  16. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  17. package/dist/storage/implementation/MongoHydrationState.d.ts +11 -0
  18. package/dist/storage/implementation/MongoHydrationState.js +49 -0
  19. package/dist/storage/implementation/MongoHydrationState.js.map +1 -0
  20. package/dist/storage/implementation/MongoParameterCompactor.d.ts +6 -5
  21. package/dist/storage/implementation/MongoParameterCompactor.js +24 -1
  22. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  23. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +13 -0
  24. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +64 -0
  25. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -0
  26. package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +36 -0
  27. package/dist/storage/implementation/MongoPersistedReplicationStream.js +90 -0
  28. package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -0
  29. package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +25 -0
  30. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +121 -0
  31. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
  32. package/dist/storage/implementation/MongoStorageProvider.js +5 -2
  33. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  34. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +50 -21
  35. package/dist/storage/implementation/MongoSyncBucketStorage.js +62 -199
  36. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  37. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +5 -1
  38. package/dist/storage/implementation/MongoSyncRulesLock.js +9 -5
  39. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  40. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +14 -0
  41. package/dist/storage/implementation/SyncRuleStateUpdate.js +36 -0
  42. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -0
  43. package/dist/storage/implementation/common/BucketDataDoc.d.ts +1 -1
  44. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +6 -0
  45. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js +2 -0
  46. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js.map +1 -0
  47. package/dist/storage/implementation/common/PersistedBatch.d.ts +2 -3
  48. package/dist/storage/implementation/common/PersistedBatch.js +5 -0
  49. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  50. package/dist/storage/implementation/common/SourceRecordStore.d.ts +1 -2
  51. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +1 -1
  52. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +1 -1
  53. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  54. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
  55. package/dist/storage/implementation/createMongoSyncBucketStorage.js +4 -4
  56. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
  57. package/dist/storage/implementation/db.d.ts +10 -2
  58. package/dist/storage/implementation/db.js +9 -14
  59. package/dist/storage/implementation/db.js.map +1 -1
  60. package/dist/storage/implementation/models.d.ts +25 -50
  61. package/dist/storage/implementation/models.js.map +1 -1
  62. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +23 -1
  63. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +403 -2
  64. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  65. package/dist/storage/implementation/v1/MongoChecksumsV1.js +114 -3
  66. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
  67. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +1 -1
  68. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +22 -15
  69. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +100 -27
  70. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  71. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +1 -2
  72. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
  73. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +2 -1
  74. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -0
  75. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
  76. package/dist/storage/implementation/v1/models.d.ts +21 -1
  77. package/dist/storage/implementation/v1/models.js.map +1 -1
  78. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +21 -0
  79. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +459 -3
  80. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  81. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +27 -7
  82. package/dist/storage/implementation/v3/MongoChecksumsV3.js +202 -18
  83. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  84. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +20 -3
  85. package/dist/storage/implementation/v3/MongoCompactorV3.js +511 -22
  86. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  87. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +62 -0
  88. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +350 -0
  89. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
  90. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +50 -16
  91. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +292 -75
  92. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  93. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +2 -3
  94. package/dist/storage/implementation/v3/PersistedBatchV3.js +43 -20
  95. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  96. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +52 -16
  97. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -1
  98. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +3 -3
  99. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +13 -11
  100. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
  101. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +16 -14
  102. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +32 -27
  103. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  104. package/dist/storage/implementation/v3/bucket-format.d.ts +4 -0
  105. package/dist/storage/implementation/v3/bucket-format.js +58 -0
  106. package/dist/storage/implementation/v3/bucket-format.js.map +1 -0
  107. package/dist/storage/implementation/v3/chunking.d.ts +10 -0
  108. package/dist/storage/implementation/v3/chunking.js +28 -0
  109. package/dist/storage/implementation/v3/chunking.js.map +1 -0
  110. package/dist/storage/implementation/v3/models.d.ts +111 -10
  111. package/dist/storage/implementation/v3/models.js +11 -28
  112. package/dist/storage/implementation/v3/models.js.map +1 -1
  113. package/dist/storage/implementation/v3/source-table-utils.d.ts +55 -0
  114. package/dist/storage/implementation/v3/source-table-utils.js +214 -0
  115. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
  116. package/dist/storage/storage-index.d.ts +4 -4
  117. package/dist/storage/storage-index.js +4 -4
  118. package/dist/storage/storage-index.js.map +1 -1
  119. package/dist/types/types.d.ts +2 -2
  120. package/dist/utils/test-utils.d.ts +1 -2
  121. package/dist/utils/test-utils.js +5 -1
  122. package/dist/utils/test-utils.js.map +1 -1
  123. package/dist/utils/util.d.ts +1 -1
  124. package/dist/utils/util.js +3 -2
  125. package/dist/utils/util.js.map +1 -1
  126. package/package.json +9 -19
  127. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +1 -1
  128. package/src/storage/MongoBucketStorage.ts +464 -95
  129. package/src/storage/implementation/CheckpointState.ts +59 -0
  130. package/src/storage/implementation/MongoBucketBatch.ts +159 -405
  131. package/src/storage/implementation/MongoChecksums.ts +7 -138
  132. package/src/storage/implementation/MongoCompactor.ts +20 -10
  133. package/src/storage/implementation/MongoHydrationState.ts +61 -0
  134. package/src/storage/implementation/MongoParameterCompactor.ts +24 -5
  135. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +88 -0
  136. package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
  137. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +152 -0
  138. package/src/storage/implementation/MongoStorageProvider.ts +5 -2
  139. package/src/storage/implementation/MongoSyncBucketStorage.ts +89 -250
  140. package/src/storage/implementation/MongoSyncRulesLock.ts +14 -5
  141. package/src/storage/implementation/SyncRuleStateUpdate.ts +38 -0
  142. package/src/storage/implementation/common/BucketDataDoc.ts +1 -1
  143. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +7 -0
  144. package/src/storage/implementation/common/PersistedBatch.ts +8 -4
  145. package/src/storage/implementation/common/SourceRecordStore.ts +1 -2
  146. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
  147. package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
  148. package/src/storage/implementation/db.ts +16 -21
  149. package/src/storage/implementation/models.ts +28 -61
  150. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +503 -5
  151. package/src/storage/implementation/v1/MongoChecksumsV1.ts +137 -4
  152. package/src/storage/implementation/v1/MongoCompactorV1.ts +1 -1
  153. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +150 -49
  154. package/src/storage/implementation/v1/PersistedBatchV1.ts +1 -2
  155. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +11 -1
  156. package/src/storage/implementation/v1/models.ts +25 -0
  157. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +611 -4
  158. package/src/storage/implementation/v3/MongoChecksumsV3.ts +262 -26
  159. package/src/storage/implementation/v3/MongoCompactorV3.ts +647 -30
  160. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +480 -0
  161. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +397 -111
  162. package/src/storage/implementation/v3/PersistedBatchV3.ts +63 -38
  163. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +59 -19
  164. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
  165. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +48 -38
  166. package/src/storage/implementation/v3/bucket-format.ts +70 -0
  167. package/src/storage/implementation/v3/chunking.ts +38 -0
  168. package/src/storage/implementation/v3/models.ts +150 -44
  169. package/src/storage/implementation/v3/source-table-utils.ts +392 -0
  170. package/src/storage/storage-index.ts +12 -4
  171. package/src/utils/test-utils.ts +6 -3
  172. package/src/utils/util.ts +3 -2
  173. package/test/src/__snapshots__/storage.test.ts.snap +0 -75
  174. package/test/src/__snapshots__/storage_sync.test.ts.snap +48 -48
  175. package/test/src/cleanup-stopped-sync-configs.test.ts +648 -0
  176. package/test/src/storage.test.ts +11 -11
  177. package/test/src/storage_compacting.test.ts +1956 -6
  178. package/test/src/storage_sync.test.ts +1429 -42
  179. package/test/src/storeCurrentData.test.ts +221 -0
  180. package/test/src/util.ts +2 -1
  181. package/tsconfig.json +1 -4
  182. package/tsconfig.tsbuildinfo +1 -1
  183. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +0 -17
  184. package/dist/storage/implementation/BucketDefinitionMapping.js +0 -58
  185. package/dist/storage/implementation/BucketDefinitionMapping.js.map +0 -1
  186. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -14
  187. package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -64
  188. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
  189. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -15
  190. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -42
  191. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
  192. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +0 -13
  193. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +0 -2
  194. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +0 -1
  195. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +0 -9
  196. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +0 -18
  197. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +0 -1
  198. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +0 -5
  199. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +0 -9
  200. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +0 -1
  201. package/src/storage/implementation/BucketDefinitionMapping.ts +0 -72
  202. package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -76
  203. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -56
  204. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +0 -15
  205. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +0 -24
  206. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -12
@@ -1,25 +1,48 @@
1
1
  import * as lib_mongo from '@powersync/lib-service-mongodb';
2
+ import { ReplicationAssertionError } from '@powersync/lib-services-framework';
2
3
  import { storage } from '@powersync/service-core';
4
+ import * as bson from 'bson';
3
5
  import { mongoTableId } from '../../../utils/util.js';
6
+ import { canCheckpointState } from '../CheckpointState.js';
4
7
  import { MongoBucketBatch, MongoBucketBatchOptions } from '../MongoBucketBatch.js';
8
+ import { MongoParsedSyncConfigSet } from '../MongoParsedSyncConfigSet.js';
9
+ import { syncRuleStateUpdatePipeline } from '../SyncRuleStateUpdate.js';
5
10
  import { PersistedBatch } from '../common/PersistedBatch.js';
6
11
  import { SourceRecordStore } from '../common/SourceRecordStore.js';
7
12
  import { PersistedBatchV3 } from './PersistedBatchV3.js';
8
13
  import { SourceRecordStoreV3 } from './SourceRecordStoreV3.js';
9
14
  import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
15
+ import { ReplicationStreamDocumentV3, SourceTableDocumentV3 } from './models.js';
16
+ import {
17
+ createNewSourceTable,
18
+ designateEventCarrier,
19
+ overlappingSourceTableFilter,
20
+ planSourceTableReconciliation,
21
+ sourceTableDesiredResolution,
22
+ sourceTableFromDocument,
23
+ SourceTableReconciliationContext
24
+ } from './source-table-utils.js';
10
25
 
11
26
  export class MongoBucketBatchV3 extends MongoBucketBatch {
12
- declare public readonly db: VersionedPowerSyncMongoV3;
27
+ declare readonly db: VersionedPowerSyncMongoV3;
13
28
 
14
29
  private readonly store: SourceRecordStore;
30
+ private readonly syncConfigIds: bson.ObjectId[];
31
+ private needsActivationV3 = true;
32
+ private lastWaitingLogThrottledV3 = 0;
15
33
 
16
34
  constructor(options: MongoBucketBatchOptions) {
17
35
  super(options);
18
- this.store = new SourceRecordStoreV3(this.db, this.group_id, this.mapping);
36
+ const syncConfigIds = options.syncConfigIds ?? [];
37
+ if (syncConfigIds.length == 0) {
38
+ throw new ReplicationAssertionError('Missing sync config id for v3 batch');
39
+ }
40
+ this.syncConfigIds = syncConfigIds;
41
+ this.store = new SourceRecordStoreV3(this.db, this.replicationStreamId, this.mapping);
19
42
  }
20
43
 
21
44
  protected createPersistedBatch(writtenSize: number): PersistedBatch {
22
- return new PersistedBatchV3(this.db, this.group_id, this.mapping, writtenSize, {
45
+ return new PersistedBatchV3(this.db, this.replicationStreamId, this.mapping, writtenSize, {
23
46
  logger: this.logger
24
47
  });
25
48
  }
@@ -28,10 +51,83 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
28
51
  return this.store;
29
52
  }
30
53
 
54
+ protected override async onReplicationTransactionFlush(
55
+ session: lib_mongo.mongo.ClientSession,
56
+ lastOp: bigint
57
+ ): Promise<void> {
58
+ // Durably advance the stream-level head of persisted ops within the flush transaction.
59
+ // This ensures a checkpoint created later (even by an empty commit, or by a freshly-appended
60
+ // config that replicates nothing) covers all ops persisted before a potential crash.
61
+ await this.db.sync_rules.updateOne(
62
+ {
63
+ _id: this.replicationStreamId
64
+ },
65
+ {
66
+ $max: {
67
+ last_persisted_op: lastOp
68
+ }
69
+ },
70
+ { session }
71
+ );
72
+ }
73
+
74
+ private selectedSyncConfigObjectIds(syncConfigIds: string[]): bson.ObjectId[] {
75
+ const selectedIds = new Set(this.syncConfigIds.map((id) => id.toHexString()));
76
+ return syncConfigIds
77
+ .filter((id) => selectedIds.has(id) && bson.ObjectId.isValid(id))
78
+ .map((id) => new bson.ObjectId(id));
79
+ }
80
+
81
+ private relevantSyncConfigIds(table: storage.SourceTable): bson.ObjectId[] {
82
+ const bucketDataSourceIds = table.bucketDataSources.map((source) => this.mapping.bucketSourceId(source));
83
+ const parameterLookupSourceIds = table.parameterLookupSources.map((source) =>
84
+ this.mapping.parameterLookupId(source)
85
+ );
86
+ return this.selectedSyncConfigObjectIds(
87
+ this.mapping.syncConfigIdsForSourceTable(
88
+ this.syncConfigIds.map((id) => id.toHexString()),
89
+ table.ref,
90
+ bucketDataSourceIds,
91
+ parameterLookupSourceIds
92
+ )
93
+ );
94
+ }
95
+
96
+ private relevantSyncConfigIdsForTables(tables: storage.SourceTable[]): bson.ObjectId[] {
97
+ const ids = new Map<string, bson.ObjectId>();
98
+ for (const table of tables) {
99
+ for (const id of this.relevantSyncConfigIds(table)) {
100
+ ids.set(id.toHexString(), id);
101
+ }
102
+ }
103
+ return [...ids.values()];
104
+ }
105
+
106
+ private snapshotBlockingSourceTablesFilter(): Record<string, unknown> {
107
+ const clauses = this.syncConfigIds.flatMap((syncConfigId) => {
108
+ const filter = this.mapping.snapshotBlockingSourceTablesFilter(syncConfigId.toHexString()) as {
109
+ $or?: Record<string, unknown>[];
110
+ };
111
+ return filter.$or ?? [];
112
+ });
113
+
114
+ if (clauses.length == 0) {
115
+ return {
116
+ snapshot_done: false,
117
+ _id: { $exists: false }
118
+ };
119
+ }
120
+
121
+ return {
122
+ snapshot_done: false,
123
+ $or: clauses
124
+ };
125
+ }
126
+
31
127
  protected async cleanupDroppedSourceTables(sourceTables: storage.SourceTable[]) {
32
128
  for (const table of sourceTables) {
33
129
  await this.db
34
- .sourceRecordsV3(this.group_id, mongoTableId(table.id))
130
+ .sourceRecords(this.replicationStreamId, mongoTableId(table.id))
35
131
  .drop()
36
132
  .catch((error) => {
37
133
  if (lib_mongo.isMongoServerError(error) && error.codeName === 'NamespaceNotFound') {
@@ -41,4 +137,515 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
41
137
  });
42
138
  }
43
139
  }
140
+
141
+ async resolveTables(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult> {
142
+ // The test-only override is a whole parsed set, so the sync rules and the mapping
143
+ // used below always come from the same parse.
144
+ const parsedOverride = options.parsedSyncConfig as MongoParsedSyncConfigSet | undefined;
145
+ const syncConfig = parsedOverride?.hydratedSyncConfig ?? this.sync_rules;
146
+ const mapping = parsedOverride?.mapping ?? this.mapping;
147
+
148
+ const { connection_id, source } = options;
149
+ const context: SourceTableReconciliationContext = {
150
+ connectionId: connection_id,
151
+ connectionTag: source.connectionTag,
152
+ identity: {
153
+ schema: source.schema,
154
+ name: source.name,
155
+ objectId: source.objectId,
156
+ replicaIdColumns: source.replicaIdColumns.map((column) => ({
157
+ name: column.name,
158
+ type: column.type,
159
+ type_oid: column.typeId
160
+ }))
161
+ },
162
+ storeCurrentData: source.sendsCompleteRows !== true,
163
+ syncConfig,
164
+ mapping,
165
+ desired: sourceTableDesiredResolution(syncConfig, source, mapping)
166
+ };
167
+
168
+ let result: storage.ResolveTablesResult | null = null;
169
+ const session = this.db.client.startSession();
170
+ await using _ = { [Symbol.asyncDispose]: () => session.endSession() };
171
+
172
+ await session.withTransaction(async () => {
173
+ const col = this.db.sourceTables(this.replicationStreamId);
174
+
175
+ // Fetch every persisted source-table doc that can overlap this physical table.
176
+ // Exact-identity docs are candidates for reuse; non-exact overlaps are possible drops.
177
+ const candidateDocs = await col
178
+ .find(overlappingSourceTableFilter(connection_id, context.identity), { session })
179
+ .toArray();
180
+
181
+ // Pure planning: which docs to retain (and narrow), whether a new doc is needed for
182
+ // uncovered memberships, and which docs conflict with the current identity.
183
+ const plan = planSourceTableReconciliation(candidateDocs, context);
184
+
185
+ // Persist narrowing for incomplete snapshots only. Snapshot-complete docs keep stale
186
+ // coverage ids so compatible future configs can reuse already-snapshotted data.
187
+ // Narrowing occurs after removing a sync config, meaning we don't process those
188
+ // definitions anymore.
189
+ for (const update of plan.narrowingUpdates) {
190
+ await col.updateOne(
191
+ { _id: update.id },
192
+ {
193
+ $set: {
194
+ bucket_data_source_ids: update.memberships.bucketDataSourceIds,
195
+ parameter_lookup_source_ids: update.memberships.parameterLookupSourceIds
196
+ }
197
+ },
198
+ { session }
199
+ );
200
+ }
201
+
202
+ // Any desired membership not covered by an existing doc gets a new source table.
203
+ // That table snapshots only the uncovered memberships.
204
+ if (plan.newTableMemberships != null) {
205
+ const id = options.idGenerator ? (options.idGenerator() as bson.ObjectId) : new bson.ObjectId();
206
+ const { doc, table } = createNewSourceTable(id, plan.newTableMemberships, context);
207
+
208
+ await col.insertOne(doc, { session });
209
+ await this.db.initializeSourceRecordsCollection(this.replicationStreamId, doc._id, session);
210
+ plan.tables.push(table);
211
+ }
212
+
213
+ // If memberships are split across multiple source tables, only one may fire events.
214
+ designateEventCarrier(plan.tables, context.desired.triggersEvent);
215
+
216
+ result = {
217
+ tables: plan.tables,
218
+ dropTables: plan.dropDocs.map((doc) => sourceTableFromDocument(doc, context.connectionTag, syncConfig, mapping))
219
+ };
220
+ });
221
+
222
+ return result!;
223
+ }
224
+
225
+ async getSourceTableStatus(table: storage.SourceTable): Promise<storage.SourceTable | null> {
226
+ const doc = (await this.db
227
+ .sourceTables(this.replicationStreamId)
228
+ .findOne({ _id: mongoTableId(table.id) }, { session: this.session })) as SourceTableDocumentV3 | null;
229
+ if (doc == null) {
230
+ return null;
231
+ }
232
+
233
+ const refreshed = sourceTableFromDocument(doc, table.ref.connectionTag, this.sync_rules, this.mapping);
234
+ // The event-carrier designation is decided per resolveTables result and not persisted -
235
+ // preserve the caller's designation instead of recomputing it from the ref, so that
236
+ // refreshing a non-carrier table does not make it fire events.
237
+ refreshed.syncEvent = table.syncEvent;
238
+ return refreshed;
239
+ }
240
+
241
+ async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {
242
+ const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
243
+
244
+ await this.flush(options);
245
+
246
+ const now = new Date();
247
+
248
+ await this.db.write_checkpoints.updateMany(
249
+ {
250
+ processed_at_lsn: null,
251
+ 'lsns.1': { $lte: lsn }
252
+ },
253
+ {
254
+ $set: {
255
+ processed_at_lsn: lsn
256
+ }
257
+ },
258
+ {
259
+ session: this.session
260
+ }
261
+ );
262
+
263
+ const preUpdateDocument = (await this.db.sync_rules.findOne(
264
+ {
265
+ _id: this.replicationStreamId,
266
+ 'sync_configs._id': { $in: this.syncConfigIds }
267
+ },
268
+ {
269
+ session: this.session,
270
+ projection: {
271
+ sync_configs: 1,
272
+ last_persisted_op: 1
273
+ }
274
+ }
275
+ )) as ReplicationStreamDocumentV3 | null;
276
+
277
+ const states =
278
+ preUpdateDocument?.sync_configs?.filter((config) => this.syncConfigIds.some((id) => id.equals(config._id))) ?? [];
279
+ if (states.length == 0) {
280
+ throw new ReplicationAssertionError(
281
+ `Failed to update checkpoint - no matching sync_config for _id: ${this.replicationStreamId}/${this.syncConfigIds
282
+ .map((id) => id.toHexString())
283
+ .join(',')}`
284
+ );
285
+ }
286
+ // The replication job / MongoBucketBatch must be constructed with all replicating (PROCESSING / ACTIVE)
287
+ // sync configs in the stream, otherwise we'll get inconsistencies. Configs in other states (e.g. STOP)
288
+ // remain embedded in the document and are ignored here.
289
+ const missingSyncConfig = preUpdateDocument!.sync_configs.find(
290
+ (config) =>
291
+ [storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE].includes(config.state) &&
292
+ !this.syncConfigIds.some((id) => id.equals(config._id))
293
+ );
294
+ if (missingSyncConfig != null) {
295
+ throw new ReplicationAssertionError(`Replication job not configured for sync config ${missingSyncConfig._id}`);
296
+ }
297
+
298
+ // Effective head of the stream's op sequence.
299
+ // last_persisted_op is $max-advanced durably in the same transaction as every flush, and this
300
+ // read uses the same session, so it covers all ops persisted by this batch.
301
+ const newCheckpoint =
302
+ preUpdateDocument?.last_persisted_op == null ? 0n : BigInt(preUpdateDocument.last_persisted_op);
303
+
304
+ let checkpointBlocked = false;
305
+ let checkpointCreated = false;
306
+ let checkpointLogState: unknown = null;
307
+ const unblockedConfigIds: bson.ObjectId[] = [];
308
+
309
+ for (const state of states) {
310
+ if (state.last_checkpoint != null && state.last_checkpoint > newCheckpoint) {
311
+ // last_persisted_op is $max-advanced durably in the same transaction as every flush, and
312
+ // checkpoints are only ever created at that head, so a checkpoint past the head means the
313
+ // op sequence or the stored state is corrupt.
314
+ throw new ReplicationAssertionError(
315
+ `Invariant violation: sync config ${state._id} has last_checkpoint ${state.last_checkpoint} > stream head ${newCheckpoint}`
316
+ );
317
+ }
318
+
319
+ const canCheckpoint = canCheckpointState(lsn, {
320
+ snapshotDone: state.snapshot_done === true,
321
+ lastCheckpointLsn: state.last_checkpoint_lsn,
322
+ noCheckpointBefore: state.no_checkpoint_before
323
+ });
324
+
325
+ if (!canCheckpoint) {
326
+ checkpointBlocked = true;
327
+ // Log the first blocked config's state.
328
+ checkpointLogState ??= {
329
+ snapshot_done: state.snapshot_done,
330
+ last_checkpoint_lsn: state.last_checkpoint_lsn,
331
+ no_checkpoint_before: state.no_checkpoint_before
332
+ };
333
+ continue;
334
+ }
335
+
336
+ checkpointCreated ||= createEmptyCheckpoints || state.last_checkpoint !== newCheckpoint;
337
+ unblockedConfigIds.push(state._id);
338
+ }
339
+
340
+ // Every commit advances the stream's resume position: commit() flushes first, so all
341
+ // source changes up to this lsn have been persisted, even when checkpoints are blocked.
342
+ // In the future we could also advance this on flush, when the connector provides the
343
+ // current position (see setResumeLsn, which connectors may already call after flushing).
344
+ const resumeLsnUpdate = { resume_lsn: lsn };
345
+
346
+ if (unblockedConfigIds.length > 0) {
347
+ // All unblocked configs get the SAME new value, so we apply it with a single updateOne
348
+ // (single-document atomicity).
349
+ const updateSet: Record<string, any> = {
350
+ last_keepalive_ts: now,
351
+ last_fatal_error: null,
352
+ last_fatal_error_ts: null
353
+ };
354
+ // Only advance checkpoint fields when an actual (non-empty) checkpoint is created, matching
355
+ // the previous per-config / v1 behaviour.
356
+ if (checkpointCreated) {
357
+ updateSet['sync_configs.$[config].last_checkpoint'] = newCheckpoint;
358
+ updateSet['sync_configs.$[config].last_checkpoint_lsn'] = lsn;
359
+ updateSet['last_checkpoint_ts'] = now;
360
+ }
361
+
362
+ await this.db.sync_rules.updateOne(
363
+ {
364
+ _id: this.replicationStreamId,
365
+ 'sync_configs._id': { $in: unblockedConfigIds }
366
+ },
367
+ { $set: updateSet, $max: resumeLsnUpdate },
368
+ {
369
+ session: this.session,
370
+ arrayFilters: checkpointCreated ? [{ 'config._id': { $in: unblockedConfigIds } }] : undefined
371
+ }
372
+ );
373
+ } else {
374
+ // All selected configs are blocked - only update keepalive/error tracking and the
375
+ // resume position.
376
+ await this.db.sync_rules.updateOne(
377
+ {
378
+ _id: this.replicationStreamId
379
+ },
380
+ {
381
+ $set: {
382
+ last_keepalive_ts: now,
383
+ last_fatal_error: null,
384
+ last_fatal_error_ts: null
385
+ },
386
+ $max: resumeLsnUpdate
387
+ },
388
+ { session: this.session }
389
+ );
390
+ }
391
+
392
+ if (checkpointBlocked) {
393
+ if (Date.now() - this.lastWaitingLogThrottledV3 > 5_000) {
394
+ this.logger.info(
395
+ `Waiting before creating checkpoint, currently at ${lsn}. Current state: ${JSON.stringify(checkpointLogState)}`
396
+ );
397
+ this.lastWaitingLogThrottledV3 = Date.now();
398
+ }
399
+ } else {
400
+ if (checkpointCreated) {
401
+ this.logger.debug(`Created checkpoint at ${lsn}`);
402
+ }
403
+ await this.autoActivateV3(lsn);
404
+ // All configs are now checkpointed at newCheckpoint (the stream head).
405
+ await this.sourceRecordStore.postCommitCleanup(newCheckpoint, this.logger);
406
+ }
407
+ if (checkpointCreated) {
408
+ await this.db.notifyCheckpoint();
409
+ }
410
+ return {
411
+ checkpointBlocked,
412
+ checkpointCreated
413
+ };
414
+ }
415
+
416
+ async keepalive(lsn: string): Promise<storage.CheckpointResult> {
417
+ return await this.commit(lsn, { createEmptyCheckpoints: true });
418
+ }
419
+
420
+ async setResumeLsn(lsn: string): Promise<void> {
421
+ await this.db.sync_rules.updateOne(
422
+ {
423
+ _id: this.replicationStreamId
424
+ },
425
+ {
426
+ $set: {
427
+ resume_lsn: lsn
428
+ }
429
+ },
430
+ { session: this.session }
431
+ );
432
+ }
433
+
434
+ private async autoActivateV3(lsn: string): Promise<void> {
435
+ if (!this.needsActivationV3) {
436
+ return;
437
+ }
438
+
439
+ const session = this.session;
440
+ let activated = false;
441
+ await session.withTransaction(async () => {
442
+ const doc = await this.db.sync_rules.findOne(
443
+ {
444
+ _id: this.replicationStreamId,
445
+ 'sync_configs._id': { $in: this.syncConfigIds }
446
+ },
447
+ {
448
+ session,
449
+ projection: {
450
+ state: 1,
451
+ sync_configs: 1
452
+ }
453
+ }
454
+ );
455
+ const states =
456
+ (doc as ReplicationStreamDocumentV3)?.sync_configs?.filter((config) =>
457
+ this.syncConfigIds.some((id) => id.equals(config._id))
458
+ ) ?? [];
459
+ if (doc == null || states.length == 0) {
460
+ return;
461
+ }
462
+
463
+ const processingStates = states.filter((state) => state.state == storage.SyncRuleState.PROCESSING);
464
+ if (
465
+ doc.state == storage.SyncRuleState.PROCESSING &&
466
+ processingStates.length == states.length &&
467
+ states.every((state) => state.snapshot_done && state.last_checkpoint != null)
468
+ ) {
469
+ await this.db.sync_rules.updateOne(
470
+ {
471
+ _id: this.replicationStreamId,
472
+ 'sync_configs._id': { $in: this.syncConfigIds }
473
+ },
474
+ {
475
+ $set: {
476
+ state: storage.SyncRuleState.ACTIVE,
477
+ 'sync_configs.$[config].state': storage.SyncRuleState.ACTIVE
478
+ }
479
+ },
480
+ {
481
+ session,
482
+ arrayFilters: [{ 'config._id': { $in: this.syncConfigIds } }]
483
+ }
484
+ );
485
+
486
+ await this.db.sync_rules.updateMany(
487
+ {
488
+ _id: { $ne: this.replicationStreamId },
489
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
490
+ },
491
+ syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP),
492
+ { session }
493
+ );
494
+ activated = true;
495
+ } else if (
496
+ doc.state == storage.SyncRuleState.ACTIVE &&
497
+ processingStates.length > 0 &&
498
+ processingStates.every((state) => state.snapshot_done && state.last_checkpoint != null)
499
+ ) {
500
+ await this.db.sync_rules.updateOne(
501
+ {
502
+ _id: this.replicationStreamId,
503
+ 'sync_configs._id': { $in: processingStates.map((state) => state._id) }
504
+ },
505
+ {
506
+ $set: {
507
+ 'sync_configs.$[activeConfig].state': storage.SyncRuleState.STOP,
508
+ 'sync_configs.$[processingConfig].state': storage.SyncRuleState.ACTIVE
509
+ }
510
+ },
511
+ {
512
+ session,
513
+ arrayFilters: [
514
+ { 'activeConfig.state': storage.SyncRuleState.ACTIVE },
515
+ { 'processingConfig._id': { $in: processingStates.map((state) => state._id) } }
516
+ ]
517
+ }
518
+ );
519
+ activated = true;
520
+ } else if (doc.state != storage.SyncRuleState.PROCESSING && doc.state != storage.SyncRuleState.ACTIVE) {
521
+ this.needsActivationV3 = false;
522
+ }
523
+ });
524
+ if (activated) {
525
+ this.logger.info(`Activated new replication stream at ${lsn}`);
526
+ await this.db.notifyCheckpoint();
527
+ this.needsActivationV3 = false;
528
+ }
529
+ }
530
+
531
+ async markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void> {
532
+ await this.db.sync_rules.updateOne(
533
+ {
534
+ _id: this.replicationStreamId,
535
+ 'sync_configs._id': { $in: this.syncConfigIds }
536
+ },
537
+ {
538
+ $set: {
539
+ 'sync_configs.$[config].snapshot_done': true,
540
+ last_keepalive_ts: new Date()
541
+ },
542
+ $max: {
543
+ 'sync_configs.$[config].no_checkpoint_before': no_checkpoint_before_lsn
544
+ }
545
+ },
546
+ {
547
+ session: this.session,
548
+ arrayFilters: [{ 'config._id': { $in: this.syncConfigIds } }]
549
+ }
550
+ );
551
+ }
552
+
553
+ async markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void> {
554
+ await this.withTransaction(async () => {
555
+ // Protect against race conditions
556
+ const blockingTables = await this.db
557
+ .sourceTables(this.replicationStreamId)
558
+ .find(this.snapshotBlockingSourceTablesFilter(), {
559
+ session: this.session,
560
+ projection: { schema_name: 1, table_name: 1 }
561
+ })
562
+ .toArray();
563
+
564
+ if (blockingTables.length > 0) {
565
+ if (options?.throwOnConflict ?? true) {
566
+ throw new ReplicationAssertionError(
567
+ `Cannot mark snapshot done while source tables still require snapshotting. Tables: ${blockingTables.map((t) => `${t.schema_name}.${t.table_name}`).join(', ')}`
568
+ );
569
+ } else {
570
+ return;
571
+ }
572
+ }
573
+
574
+ await this.markAllSnapshotDone(no_checkpoint_before_lsn);
575
+ });
576
+ }
577
+
578
+ async markTableSnapshotRequired(table: storage.SourceTable): Promise<void> {
579
+ const syncConfigIds = this.relevantSyncConfigIds(table);
580
+ if (syncConfigIds.length == 0) {
581
+ return;
582
+ }
583
+
584
+ await this.db.sync_rules.updateOne(
585
+ {
586
+ _id: this.replicationStreamId,
587
+ 'sync_configs._id': { $in: syncConfigIds }
588
+ },
589
+ {
590
+ $set: {
591
+ 'sync_configs.$[config].snapshot_done': false
592
+ }
593
+ },
594
+ {
595
+ session: this.session,
596
+ arrayFilters: [{ 'config._id': { $in: syncConfigIds } }]
597
+ }
598
+ );
599
+ }
600
+
601
+ async markTableSnapshotDone(
602
+ tables: storage.SourceTable[],
603
+ no_checkpoint_before_lsn?: string
604
+ ): Promise<storage.SourceTable[]> {
605
+ const session = this.session;
606
+ const ids = tables.map((table) => mongoTableId(table.id));
607
+ const syncConfigIds = this.relevantSyncConfigIdsForTables(tables);
608
+
609
+ await this.withTransaction(async () => {
610
+ await this.db.sourceTables(this.replicationStreamId).updateMany(
611
+ { _id: { $in: ids } },
612
+ {
613
+ $set: {
614
+ snapshot_done: true
615
+ },
616
+ $unset: {
617
+ snapshot_status: 1
618
+ }
619
+ },
620
+ { session }
621
+ );
622
+
623
+ if (no_checkpoint_before_lsn != null && syncConfigIds.length > 0) {
624
+ await this.db.sync_rules.updateOne(
625
+ {
626
+ _id: this.replicationStreamId,
627
+ 'sync_configs._id': { $in: syncConfigIds }
628
+ },
629
+ {
630
+ $set: {
631
+ last_keepalive_ts: new Date()
632
+ },
633
+ $max: {
634
+ 'sync_configs.$[config].no_checkpoint_before': no_checkpoint_before_lsn
635
+ }
636
+ },
637
+ {
638
+ session: this.session,
639
+ // Only set for sync configs that use this table
640
+ arrayFilters: [{ 'config._id': { $in: syncConfigIds } }]
641
+ }
642
+ );
643
+ }
644
+ });
645
+ return tables.map((table) => {
646
+ const copy = table.clone();
647
+ copy.snapshotComplete = true;
648
+ return copy;
649
+ });
650
+ }
44
651
  }