@powersync/service-module-mongodb-storage 0.15.4 → 0.17.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 (202) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js +1 -1
  3. package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js.map +1 -1
  4. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js +2 -2
  5. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +1 -1
  6. package/dist/storage/MongoBucketStorage.d.ts +8 -6
  7. package/dist/storage/MongoBucketStorage.js +153 -66
  8. package/dist/storage/MongoBucketStorage.js.map +1 -1
  9. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +15 -0
  10. package/dist/storage/implementation/BucketDefinitionMapping.js +58 -0
  11. package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
  12. package/dist/storage/implementation/CheckpointState.d.ts +20 -0
  13. package/dist/storage/implementation/CheckpointState.js +31 -0
  14. package/dist/storage/implementation/CheckpointState.js.map +1 -0
  15. package/dist/storage/implementation/MongoBucketBatch.d.ts +48 -35
  16. package/dist/storage/implementation/MongoBucketBatch.js +118 -379
  17. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  18. package/dist/storage/implementation/MongoBucketBatchShared.d.ts +5 -0
  19. package/dist/storage/implementation/MongoBucketBatchShared.js +8 -0
  20. package/dist/storage/implementation/MongoBucketBatchShared.js.map +1 -0
  21. package/dist/storage/implementation/MongoChecksums.d.ts +29 -17
  22. package/dist/storage/implementation/MongoChecksums.js +13 -72
  23. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  24. package/dist/storage/implementation/MongoCompactor.d.ts +98 -58
  25. package/dist/storage/implementation/MongoCompactor.js +229 -296
  26. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  27. package/dist/storage/implementation/MongoParameterCompactor.d.ts +11 -6
  28. package/dist/storage/implementation/MongoParameterCompactor.js +11 -8
  29. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  30. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +14 -0
  31. package/dist/storage/implementation/MongoPersistedSyncRules.js +67 -0
  32. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -0
  33. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +22 -5
  34. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +56 -13
  35. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  36. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +61 -32
  37. package/dist/storage/implementation/MongoSyncBucketStorage.js +85 -523
  38. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  39. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +10 -4
  40. package/dist/storage/implementation/MongoSyncRulesLock.js +19 -13
  41. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  42. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +1 -1
  43. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  44. package/dist/storage/implementation/OperationBatch.js +1 -1
  45. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +14 -0
  46. package/dist/storage/implementation/SyncRuleStateUpdate.js +36 -0
  47. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -0
  48. package/dist/storage/implementation/common/BucketDataDoc.d.ts +35 -0
  49. package/dist/storage/implementation/common/BucketDataDoc.js +2 -0
  50. package/dist/storage/implementation/common/BucketDataDoc.js.map +1 -0
  51. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +13 -0
  52. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +2 -0
  53. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +1 -0
  54. package/dist/storage/implementation/common/PersistedBatch.d.ts +108 -0
  55. package/dist/storage/implementation/common/PersistedBatch.js +237 -0
  56. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -0
  57. package/dist/storage/implementation/common/SingleBucketStore.d.ts +54 -0
  58. package/dist/storage/implementation/common/SingleBucketStore.js +3 -0
  59. package/dist/storage/implementation/common/SingleBucketStore.js.map +1 -0
  60. package/dist/storage/implementation/common/SourceRecordStore.d.ts +35 -0
  61. package/dist/storage/implementation/common/SourceRecordStore.js +2 -0
  62. package/dist/storage/implementation/common/SourceRecordStore.js.map +1 -0
  63. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +27 -0
  64. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +57 -0
  65. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -0
  66. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +7 -0
  67. package/dist/storage/implementation/createMongoSyncBucketStorage.js +9 -0
  68. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -0
  69. package/dist/storage/implementation/db.d.ts +41 -36
  70. package/dist/storage/implementation/db.js +77 -99
  71. package/dist/storage/implementation/db.js.map +1 -1
  72. package/dist/storage/implementation/models.d.ts +79 -66
  73. package/dist/storage/implementation/models.js +20 -1
  74. package/dist/storage/implementation/models.js.map +1 -1
  75. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +27 -0
  76. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +407 -0
  77. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -0
  78. package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +12 -0
  79. package/dist/storage/implementation/v1/MongoChecksumsV1.js +56 -0
  80. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -0
  81. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +23 -0
  82. package/dist/storage/implementation/v1/MongoCompactorV1.js +52 -0
  83. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -0
  84. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +9 -0
  85. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -0
  86. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -0
  87. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +50 -0
  88. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +354 -0
  89. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -0
  90. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +25 -0
  91. package/dist/storage/implementation/v1/PersistedBatchV1.js +183 -0
  92. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -0
  93. package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +18 -0
  94. package/dist/storage/implementation/v1/SingleBucketStoreV1.js +57 -0
  95. package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -0
  96. package/dist/storage/implementation/v1/SourceRecordStoreV1.d.ts +19 -0
  97. package/dist/storage/implementation/v1/SourceRecordStoreV1.js +105 -0
  98. package/dist/storage/implementation/v1/SourceRecordStoreV1.js.map +1 -0
  99. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +12 -0
  100. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +20 -0
  101. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -0
  102. package/dist/storage/implementation/v1/models.d.ts +45 -0
  103. package/dist/storage/implementation/v1/models.js +37 -0
  104. package/dist/storage/implementation/v1/models.js.map +1 -0
  105. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +30 -0
  106. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +463 -0
  107. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -0
  108. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +15 -0
  109. package/dist/storage/implementation/v3/MongoChecksumsV3.js +84 -0
  110. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -0
  111. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -0
  112. package/dist/storage/implementation/v3/MongoCompactorV3.js +68 -0
  113. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -0
  114. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +9 -0
  115. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +18 -0
  116. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  117. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +4 -0
  118. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +9 -0
  119. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +1 -0
  120. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +63 -0
  121. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +508 -0
  122. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -0
  123. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +28 -0
  124. package/dist/storage/implementation/v3/PersistedBatchV3.js +259 -0
  125. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -0
  126. package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +18 -0
  127. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +48 -0
  128. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -0
  129. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +22 -0
  130. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +164 -0
  131. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -0
  132. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +22 -0
  133. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +74 -0
  134. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -0
  135. package/dist/storage/implementation/v3/models.d.ts +101 -0
  136. package/dist/storage/implementation/v3/models.js +34 -0
  137. package/dist/storage/implementation/v3/models.js.map +1 -0
  138. package/dist/storage/storage-index.d.ts +6 -3
  139. package/dist/storage/storage-index.js +6 -3
  140. package/dist/storage/storage-index.js.map +1 -1
  141. package/dist/utils/util.d.ts +10 -3
  142. package/dist/utils/util.js +24 -3
  143. package/dist/utils/util.js.map +1 -1
  144. package/package.json +9 -9
  145. package/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts +1 -1
  146. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +7 -7
  147. package/src/storage/MongoBucketStorage.ts +254 -99
  148. package/src/storage/implementation/BucketDefinitionMapping.ts +75 -0
  149. package/src/storage/implementation/CheckpointState.ts +59 -0
  150. package/src/storage/implementation/MongoBucketBatch.ts +182 -490
  151. package/src/storage/implementation/MongoBucketBatchShared.ts +11 -0
  152. package/src/storage/implementation/MongoChecksums.ts +53 -75
  153. package/src/storage/implementation/MongoCompactor.ts +374 -404
  154. package/src/storage/implementation/MongoParameterCompactor.ts +37 -24
  155. package/src/storage/implementation/MongoPersistedSyncRules.ts +82 -0
  156. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +78 -16
  157. package/src/storage/implementation/MongoSyncBucketStorage.ts +179 -628
  158. package/src/storage/implementation/MongoSyncRulesLock.ts +20 -16
  159. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +3 -1
  160. package/src/storage/implementation/OperationBatch.ts +1 -1
  161. package/src/storage/implementation/SyncRuleStateUpdate.ts +38 -0
  162. package/src/storage/implementation/common/BucketDataDoc.ts +37 -0
  163. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +15 -0
  164. package/src/storage/implementation/common/PersistedBatch.ts +364 -0
  165. package/src/storage/implementation/common/SingleBucketStore.ts +63 -0
  166. package/src/storage/implementation/common/SourceRecordStore.ts +48 -0
  167. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +80 -0
  168. package/src/storage/implementation/createMongoSyncBucketStorage.ts +25 -0
  169. package/src/storage/implementation/db.ts +110 -131
  170. package/src/storage/implementation/models.ts +102 -79
  171. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +509 -0
  172. package/src/storage/implementation/v1/MongoChecksumsV1.ts +75 -0
  173. package/src/storage/implementation/v1/MongoCompactorV1.ts +93 -0
  174. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +26 -0
  175. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +543 -0
  176. package/src/storage/implementation/v1/PersistedBatchV1.ts +229 -0
  177. package/src/storage/implementation/v1/SingleBucketStoreV1.ts +74 -0
  178. package/src/storage/implementation/v1/SourceRecordStoreV1.ts +156 -0
  179. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +28 -0
  180. package/src/storage/implementation/v1/models.ts +99 -0
  181. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +607 -0
  182. package/src/storage/implementation/v3/MongoChecksumsV3.ts +120 -0
  183. package/src/storage/implementation/v3/MongoCompactorV3.ts +107 -0
  184. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +24 -0
  185. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +11 -0
  186. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +678 -0
  187. package/src/storage/implementation/v3/PersistedBatchV3.ts +317 -0
  188. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +68 -0
  189. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +226 -0
  190. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +117 -0
  191. package/src/storage/implementation/v3/models.ts +164 -0
  192. package/src/storage/storage-index.ts +6 -3
  193. package/src/utils/util.ts +34 -5
  194. package/test/src/storage_compacting.test.ts +57 -29
  195. package/test/src/storage_sync.test.ts +767 -5
  196. package/test/src/storeCurrentData.test.ts +211 -0
  197. package/test/tsconfig.json +0 -1
  198. package/tsconfig.tsbuildinfo +1 -1
  199. package/dist/storage/implementation/PersistedBatch.d.ts +0 -71
  200. package/dist/storage/implementation/PersistedBatch.js +0 -354
  201. package/dist/storage/implementation/PersistedBatch.js.map +0 -1
  202. package/src/storage/implementation/PersistedBatch.ts +0 -432
@@ -0,0 +1,607 @@
1
+ import * as lib_mongo from '@powersync/lib-service-mongodb';
2
+ import { ReplicationAssertionError } from '@powersync/lib-services-framework';
3
+ import { ColumnDescriptor, storage } from '@powersync/service-core';
4
+ import { HydratedSyncConfig, MatchingSources } from '@powersync/service-sync-rules';
5
+ import * as bson from 'bson';
6
+ import { mongoTableId } from '../../../utils/util.js';
7
+ import { calculateCheckpointState } from '../CheckpointState.js';
8
+ import { MongoBucketBatch, MongoBucketBatchOptions } from '../MongoBucketBatch.js';
9
+ import { syncRuleStateUpdatePipeline } from '../SyncRuleStateUpdate.js';
10
+ import { PersistedBatch } from '../common/PersistedBatch.js';
11
+ import { SourceRecordStore } from '../common/SourceRecordStore.js';
12
+ import { PersistedBatchV3 } from './PersistedBatchV3.js';
13
+ import { SourceRecordStoreV3 } from './SourceRecordStoreV3.js';
14
+ import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
15
+ import { ReplicationStreamDocumentV3, SourceTableDocumentV3 } from './models.js';
16
+
17
+ function sameStringArray(left: string[], right: string[]) {
18
+ return left.length == right.length && left.every((value, index) => value == right[index]);
19
+ }
20
+
21
+ export class MongoBucketBatchV3 extends MongoBucketBatch {
22
+ declare public readonly db: VersionedPowerSyncMongoV3;
23
+
24
+ private readonly store: SourceRecordStore;
25
+ private readonly syncConfigId: bson.ObjectId;
26
+ private needsActivationV3 = true;
27
+ private lastWaitingLogThrottledV3 = 0;
28
+
29
+ constructor(options: MongoBucketBatchOptions) {
30
+ super(options);
31
+ if (options.syncConfigId == null) {
32
+ throw new ReplicationAssertionError('Missing sync config id for v3 batch');
33
+ }
34
+ this.syncConfigId = options.syncConfigId;
35
+ this.store = new SourceRecordStoreV3(this.db, this.group_id, this.mapping);
36
+ }
37
+
38
+ protected createPersistedBatch(writtenSize: number): PersistedBatch {
39
+ return new PersistedBatchV3(this.db, this.group_id, this.mapping, writtenSize, {
40
+ logger: this.logger
41
+ });
42
+ }
43
+
44
+ protected get sourceRecordStore(): SourceRecordStore {
45
+ return this.store;
46
+ }
47
+
48
+ protected async cleanupDroppedSourceTables(sourceTables: storage.SourceTable[]) {
49
+ for (const table of sourceTables) {
50
+ await this.db
51
+ .sourceRecordsV3(this.group_id, mongoTableId(table.id))
52
+ .drop()
53
+ .catch((error) => {
54
+ if (lib_mongo.isMongoServerError(error) && error.codeName === 'NamespaceNotFound') {
55
+ return;
56
+ }
57
+ throw error;
58
+ });
59
+ }
60
+ }
61
+
62
+ async resolveTables(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult> {
63
+ const ref = options.source;
64
+ const syncRules = options.syncRules ?? this.sync_rules;
65
+ const matchingSources = syncRules.getMatchingSources(ref);
66
+
67
+ const { connection_id, source } = options;
68
+ const { schema, name, objectId, replicaIdColumns, connectionTag, sendsCompleteRows } = source;
69
+ const normalizedReplicaIdColumns = replicaIdColumns.map((column) => ({
70
+ name: column.name,
71
+ type: column.type,
72
+ type_oid: column.typeId
73
+ }));
74
+
75
+ let result: storage.ResolveTablesResult | null = null;
76
+ const initializeSourceRecordsFor: bson.ObjectId[] = [];
77
+
78
+ await this.db.client.withSession(async (session) => {
79
+ const col = this.db.commonSourceTables(this.group_id);
80
+ const exactFilter: Record<string, unknown> = {
81
+ connection_id,
82
+ schema_name: schema,
83
+ table_name: name,
84
+ replica_id_columns2: normalizedReplicaIdColumns
85
+ };
86
+ if (objectId != null) {
87
+ exactFilter.relation_id = objectId;
88
+ }
89
+
90
+ const exactDocs = (await col.find(exactFilter, { session }).toArray()) as SourceTableDocumentV3[];
91
+ const bucketSourceById = new Map(
92
+ matchingSources.bucketDataSources.map((source) => [this.mapping.bucketSourceId(source), source] as const)
93
+ );
94
+ const parameterLookupSourceById = new Map(
95
+ matchingSources.parameterLookupSources.map(
96
+ (source) => [this.mapping.parameterLookupId(source), source] as const
97
+ )
98
+ );
99
+ const desiredBucketIds = new Set(bucketSourceById.keys());
100
+ const desiredLookupIds = new Set(parameterLookupSourceById.keys());
101
+ const desiredHasMembership = desiredBucketIds.size > 0 || desiredLookupIds.size > 0;
102
+ const triggersEvent = syncRules.tableTriggersEvent(ref);
103
+
104
+ const coveredBucketIds = new Set<string>();
105
+ const coveredLookupIds = new Set<string>();
106
+ const retainedDocIds: bson.ObjectId[] = [];
107
+ const tables: storage.SourceTable[] = [];
108
+ let retainedEventOnlyTable = false;
109
+
110
+ for (const doc of exactDocs) {
111
+ const bucketDataSourceIds = doc.bucket_data_source_ids.filter((id) => desiredBucketIds.has(id));
112
+ const parameterLookupSourceIds = doc.parameter_lookup_source_ids.filter((id) => desiredLookupIds.has(id));
113
+ const coversDesiredMembership = bucketDataSourceIds.length > 0 || parameterLookupSourceIds.length > 0;
114
+ const coversEventOnlyTable = !desiredHasMembership && triggersEvent && !retainedEventOnlyTable;
115
+
116
+ for (const id of bucketDataSourceIds) {
117
+ coveredBucketIds.add(id);
118
+ }
119
+ for (const id of parameterLookupSourceIds) {
120
+ coveredLookupIds.add(id);
121
+ }
122
+
123
+ const updates: Partial<SourceTableDocumentV3> = {};
124
+ if (
125
+ !sameStringArray(doc.bucket_data_source_ids, bucketDataSourceIds) ||
126
+ !sameStringArray(doc.parameter_lookup_source_ids, parameterLookupSourceIds)
127
+ ) {
128
+ updates.bucket_data_source_ids = bucketDataSourceIds;
129
+ updates.parameter_lookup_source_ids = parameterLookupSourceIds;
130
+ }
131
+ if (Object.keys(updates).length > 0) {
132
+ await col.updateOne({ _id: doc._id }, { $set: updates }, { session });
133
+ }
134
+
135
+ if (coversDesiredMembership || coversEventOnlyTable) {
136
+ if (coversEventOnlyTable) {
137
+ retainedEventOnlyTable = true;
138
+ }
139
+ retainedDocIds.push(doc._id);
140
+ const table = this.sourceTableFromDocument(
141
+ {
142
+ ...doc,
143
+ bucket_data_source_ids: bucketDataSourceIds,
144
+ parameter_lookup_source_ids: parameterLookupSourceIds
145
+ },
146
+ connectionTag,
147
+ syncRules,
148
+ {
149
+ bucketDataSources: bucketDataSourceIds.map((id) => bucketSourceById.get(id)!),
150
+ parameterLookupSources: parameterLookupSourceIds.map((id) => parameterLookupSourceById.get(id)!)
151
+ }
152
+ );
153
+ table.storeCurrentData = sendsCompleteRows !== true;
154
+ tables.push(table);
155
+ }
156
+ }
157
+
158
+ const uncoveredBucketIds = [...desiredBucketIds].filter((id) => !coveredBucketIds.has(id));
159
+ const uncoveredLookupIds = [...desiredLookupIds].filter((id) => !coveredLookupIds.has(id));
160
+
161
+ if (uncoveredBucketIds.length > 0 || uncoveredLookupIds.length > 0 || (triggersEvent && tables.length == 0)) {
162
+ const id = options.idGenerator ? (options.idGenerator() as bson.ObjectId) : new bson.ObjectId();
163
+ const sourceTable = new storage.SourceTable({
164
+ id,
165
+ ref,
166
+ objectId,
167
+ replicaIdColumns,
168
+ snapshotComplete: false,
169
+ bucketDataSources: uncoveredBucketIds.map((id) => bucketSourceById.get(id)!),
170
+ parameterLookupSources: uncoveredLookupIds.map((id) => parameterLookupSourceById.get(id)!)
171
+ });
172
+ sourceTable.syncData = uncoveredBucketIds.length > 0;
173
+ sourceTable.syncParameters = uncoveredLookupIds.length > 0;
174
+ sourceTable.syncEvent = triggersEvent;
175
+ sourceTable.storeCurrentData = sendsCompleteRows !== true;
176
+
177
+ const createDoc: SourceTableDocumentV3 = {
178
+ _id: id,
179
+ connection_id,
180
+ relation_id: objectId,
181
+ schema_name: schema,
182
+ table_name: name,
183
+ replica_id_columns: null,
184
+ replica_id_columns2: normalizedReplicaIdColumns,
185
+ snapshot_done: false,
186
+ snapshot_status: undefined,
187
+ bucket_data_source_ids: uncoveredBucketIds,
188
+ parameter_lookup_source_ids: uncoveredLookupIds
189
+ };
190
+
191
+ await col.insertOne(createDoc, { session });
192
+ initializeSourceRecordsFor.push(createDoc._id);
193
+ retainedDocIds.push(createDoc._id);
194
+ tables.push(sourceTable);
195
+ }
196
+
197
+ const conflictFilter = [{ schema_name: schema, table_name: name }] as Record<string, unknown>[];
198
+ if (objectId != null) {
199
+ conflictFilter.push({ relation_id: objectId });
200
+ }
201
+ const dropTables = await col
202
+ .find(
203
+ {
204
+ connection_id,
205
+ _id: { $nin: retainedDocIds },
206
+ $or: conflictFilter
207
+ },
208
+ { session }
209
+ )
210
+ .toArray();
211
+
212
+ result = {
213
+ tables,
214
+ dropTables: dropTables.map((doc) =>
215
+ this.sourceTableFromDocument(doc as SourceTableDocumentV3, connectionTag, syncRules)
216
+ )
217
+ };
218
+ });
219
+
220
+ for (const sourceTableId of initializeSourceRecordsFor) {
221
+ await this.db.initializeSourceRecordsCollection(this.group_id, sourceTableId);
222
+ }
223
+
224
+ return result!;
225
+ }
226
+
227
+ private sourceTableFromDocument(
228
+ doc: SourceTableDocumentV3,
229
+ connectionTag: string,
230
+ syncRules: HydratedSyncConfig,
231
+ memberships?: MatchingSources
232
+ ): storage.SourceTable {
233
+ const resolvedMemberships = memberships ?? this.sourceTableMembershipsFromDocument(doc, syncRules);
234
+ const table = new storage.SourceTable({
235
+ id: doc._id,
236
+ ref: {
237
+ connectionTag,
238
+ schema: doc.schema_name,
239
+ name: doc.table_name
240
+ },
241
+ objectId: doc.relation_id,
242
+ replicaIdColumns: doc.replica_id_columns2!.map(
243
+ (c) => ({ name: c.name, typeId: c.type_oid, type: c.type }) satisfies ColumnDescriptor
244
+ ),
245
+ snapshotComplete: doc.snapshot_done ?? true,
246
+ bucketDataSources: resolvedMemberships.bucketDataSources,
247
+ parameterLookupSources: resolvedMemberships.parameterLookupSources
248
+ });
249
+ table.syncData = table.bucketDataSources.length > 0;
250
+ table.syncParameters = table.parameterLookupSources.length > 0;
251
+ table.syncEvent = syncRules.tableTriggersEvent(table.ref);
252
+ table.snapshotStatus =
253
+ doc.snapshot_status == null
254
+ ? undefined
255
+ : {
256
+ lastKey: doc.snapshot_status.last_key?.buffer ?? null,
257
+ totalEstimatedCount: doc.snapshot_status.total_estimated_count,
258
+ replicatedCount: doc.snapshot_status.replicated_count
259
+ };
260
+ return table;
261
+ }
262
+
263
+ private sourceTableMembershipsFromDocument(
264
+ doc: SourceTableDocumentV3,
265
+ syncRules: HydratedSyncConfig
266
+ ): MatchingSources {
267
+ const bucketDataSourceIds = new Set(doc.bucket_data_source_ids);
268
+ const parameterLookupSourceIds = new Set(doc.parameter_lookup_source_ids);
269
+
270
+ return {
271
+ bucketDataSources: syncRules.bucketDataSources.filter((source) =>
272
+ bucketDataSourceIds.has(this.mapping.bucketSourceId(source))
273
+ ),
274
+ parameterLookupSources: syncRules.bucketParameterLookupSources.filter((source) =>
275
+ parameterLookupSourceIds.has(this.mapping.parameterLookupId(source))
276
+ )
277
+ };
278
+ }
279
+
280
+ async getSourceTableStatus(table: storage.SourceTable): Promise<storage.SourceTable | null> {
281
+ const doc = (await this.db
282
+ .commonSourceTables(this.group_id)
283
+ .findOne({ _id: mongoTableId(table.id) }, { session: this.session })) as SourceTableDocumentV3 | null;
284
+ if (doc == null) {
285
+ return null;
286
+ }
287
+
288
+ return this.sourceTableFromDocument(doc, table.ref.connectionTag, this.sync_rules);
289
+ }
290
+
291
+ async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {
292
+ const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
293
+
294
+ await this.flush(options);
295
+
296
+ const now = new Date();
297
+
298
+ await this.db.write_checkpoints.updateMany(
299
+ {
300
+ processed_at_lsn: null,
301
+ 'lsns.1': { $lte: lsn }
302
+ },
303
+ {
304
+ $set: {
305
+ processed_at_lsn: lsn
306
+ }
307
+ },
308
+ {
309
+ session: this.session
310
+ }
311
+ );
312
+
313
+ const preUpdateDocument = await this.db.sync_rules.findOne(
314
+ {
315
+ _id: this.group_id,
316
+ 'sync_configs._id': this.syncConfigId
317
+ },
318
+ {
319
+ session: this.session,
320
+ projection: {
321
+ snapshot_lsn: 1,
322
+ sync_configs: {
323
+ $elemMatch: {
324
+ _id: this.syncConfigId
325
+ }
326
+ }
327
+ }
328
+ }
329
+ );
330
+
331
+ const state = (preUpdateDocument as ReplicationStreamDocumentV3)?.sync_configs?.[0];
332
+ if (state == null) {
333
+ throw new ReplicationAssertionError(
334
+ `Failed to update checkpoint - no matching sync_config for _id: ${this.group_id}/${this.syncConfigId.toHexString()}`
335
+ );
336
+ }
337
+
338
+ const checkpointState = calculateCheckpointState({
339
+ lsn,
340
+ snapshotDone: state.snapshot_done === true,
341
+ lastCheckpointLsn: state.last_checkpoint_lsn,
342
+ noCheckpointBefore: state.no_checkpoint_before,
343
+ keepaliveOp: state.keepalive_op == null ? null : BigInt(state.keepalive_op),
344
+ lastCheckpoint: state.last_checkpoint,
345
+ persistedOp: this.persisted_op,
346
+ createEmptyCheckpoints
347
+ });
348
+
349
+ const updateSet: Record<string, any> = {
350
+ last_keepalive_ts: now,
351
+ last_fatal_error: null,
352
+ last_fatal_error_ts: null,
353
+ 'sync_configs.$[config].keepalive_op': checkpointState.newKeepaliveOp,
354
+ 'sync_configs.$[config].last_checkpoint': checkpointState.newLastCheckpoint
355
+ };
356
+ if (checkpointState.checkpointCreated) {
357
+ updateSet['sync_configs.$[config].last_checkpoint_lsn'] = lsn;
358
+ updateSet['snapshot_lsn'] = null;
359
+ updateSet['last_checkpoint_ts'] = now;
360
+ }
361
+
362
+ await this.db.sync_rules.updateOne(
363
+ {
364
+ _id: this.group_id,
365
+ 'sync_configs._id': this.syncConfigId
366
+ },
367
+ {
368
+ $set: updateSet
369
+ },
370
+ {
371
+ session: this.session,
372
+ arrayFilters: [{ 'config._id': this.syncConfigId }]
373
+ }
374
+ );
375
+
376
+ if (checkpointState.checkpointBlocked) {
377
+ if (Date.now() - this.lastWaitingLogThrottledV3 > 5_000) {
378
+ this.logger.info(
379
+ `Waiting before creating checkpoint, currently at ${lsn} / ${checkpointState.newKeepaliveOp}. Current state: ${JSON.stringify(
380
+ {
381
+ snapshot_done: state.snapshot_done,
382
+ last_checkpoint_lsn: state.last_checkpoint_lsn,
383
+ no_checkpoint_before: state.no_checkpoint_before
384
+ }
385
+ )}`
386
+ );
387
+ this.lastWaitingLogThrottledV3 = Date.now();
388
+ }
389
+ } else {
390
+ if (checkpointState.checkpointCreated) {
391
+ this.logger.debug(`Created checkpoint at ${lsn} / ${checkpointState.newLastCheckpoint}`);
392
+ }
393
+ await this.autoActivateV3(lsn);
394
+ await this.db.notifyCheckpoint();
395
+ this.persisted_op = null;
396
+ this.last_checkpoint_lsn = lsn;
397
+ if (checkpointState.newLastCheckpoint != null) {
398
+ await this.sourceRecordStore.postCommitCleanup(checkpointState.newLastCheckpoint, this.logger);
399
+ }
400
+ }
401
+ return {
402
+ checkpointBlocked: checkpointState.checkpointBlocked,
403
+ checkpointCreated: checkpointState.checkpointCreated
404
+ };
405
+ }
406
+
407
+ async keepalive(lsn: string): Promise<storage.CheckpointResult> {
408
+ return await this.commit(lsn, { createEmptyCheckpoints: true });
409
+ }
410
+
411
+ async setResumeLsn(lsn: string): Promise<void> {
412
+ await this.db.sync_rules.updateOne(
413
+ {
414
+ _id: this.group_id,
415
+ 'sync_configs._id': this.syncConfigId
416
+ },
417
+ {
418
+ $set: {
419
+ snapshot_lsn: lsn
420
+ }
421
+ },
422
+ { session: this.session }
423
+ );
424
+ }
425
+
426
+ private async autoActivateV3(lsn: string): Promise<void> {
427
+ if (!this.needsActivationV3) {
428
+ return;
429
+ }
430
+
431
+ const session = this.session;
432
+ let activated = false;
433
+ await session.withTransaction(async () => {
434
+ const doc = await this.db.sync_rules.findOne(
435
+ {
436
+ _id: this.group_id,
437
+ 'sync_configs._id': this.syncConfigId
438
+ },
439
+ {
440
+ session,
441
+ projection: {
442
+ state: 1,
443
+ sync_configs: {
444
+ $elemMatch: {
445
+ _id: this.syncConfigId
446
+ }
447
+ }
448
+ }
449
+ }
450
+ );
451
+ const state = (doc as ReplicationStreamDocumentV3)?.sync_configs?.[0];
452
+ if (
453
+ doc &&
454
+ doc.state == storage.SyncRuleState.PROCESSING &&
455
+ state?.state == storage.SyncRuleState.PROCESSING &&
456
+ state.snapshot_done &&
457
+ state.last_checkpoint != null
458
+ ) {
459
+ await this.db.sync_rules.updateOne(
460
+ {
461
+ _id: this.group_id,
462
+ 'sync_configs._id': this.syncConfigId
463
+ },
464
+ {
465
+ $set: {
466
+ state: storage.SyncRuleState.ACTIVE,
467
+ 'sync_configs.$[config].state': storage.SyncRuleState.ACTIVE
468
+ }
469
+ },
470
+ {
471
+ session,
472
+ arrayFilters: [{ 'config._id': this.syncConfigId }]
473
+ }
474
+ );
475
+
476
+ await this.db.sync_rules.updateMany(
477
+ {
478
+ _id: { $ne: this.group_id },
479
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
480
+ },
481
+ syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP),
482
+ { session }
483
+ );
484
+ activated = true;
485
+ } else if (doc?.state != storage.SyncRuleState.PROCESSING) {
486
+ this.needsActivationV3 = false;
487
+ }
488
+ });
489
+ if (activated) {
490
+ this.logger.info(`Activated new replication stream at ${lsn}`);
491
+ await this.db.notifyCheckpoint();
492
+ this.needsActivationV3 = false;
493
+ }
494
+ }
495
+
496
+ async markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void> {
497
+ await this.db.sync_rules.updateOne(
498
+ {
499
+ _id: this.group_id,
500
+ 'sync_configs._id': this.syncConfigId
501
+ },
502
+ {
503
+ $set: {
504
+ 'sync_configs.$[config].snapshot_done': true,
505
+ last_keepalive_ts: new Date()
506
+ },
507
+ $max: {
508
+ 'sync_configs.$[config].no_checkpoint_before': no_checkpoint_before_lsn
509
+ }
510
+ },
511
+ {
512
+ session: this.session,
513
+ arrayFilters: [{ 'config._id': this.syncConfigId }]
514
+ }
515
+ );
516
+ }
517
+
518
+ async markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void> {
519
+ await this.withTransaction(async () => {
520
+ // Protect against race conditions
521
+ const count = await this.db.sourceTablesV3(this.group_id).countDocuments(
522
+ {
523
+ snapshot_done: false
524
+ },
525
+ { session: this.session }
526
+ );
527
+ if (count > 0) {
528
+ if (options?.throwOnConflict ?? true) {
529
+ throw new ReplicationAssertionError(
530
+ `Cannot mark snapshot done while ${count} source table${count == 1 ? '' : 's'} still require snapshotting`
531
+ );
532
+ } else {
533
+ return;
534
+ }
535
+ }
536
+
537
+ await this.markAllSnapshotDone(no_checkpoint_before_lsn);
538
+ });
539
+ }
540
+
541
+ async markTableSnapshotRequired(_table: storage.SourceTable): Promise<void> {
542
+ await this.db.sync_rules.updateOne(
543
+ {
544
+ _id: this.group_id,
545
+ 'sync_configs._id': this.syncConfigId
546
+ },
547
+ {
548
+ $set: {
549
+ 'sync_configs.$[config].snapshot_done': false
550
+ }
551
+ },
552
+ {
553
+ session: this.session,
554
+ arrayFilters: [{ 'config._id': this.syncConfigId }]
555
+ }
556
+ );
557
+ }
558
+
559
+ async markTableSnapshotDone(
560
+ tables: storage.SourceTable[],
561
+ no_checkpoint_before_lsn?: string
562
+ ): Promise<storage.SourceTable[]> {
563
+ const session = this.session;
564
+ const ids = tables.map((table) => mongoTableId(table.id));
565
+
566
+ await this.withTransaction(async () => {
567
+ await this.db.commonSourceTables(this.group_id).updateMany(
568
+ { _id: { $in: ids } },
569
+ {
570
+ $set: {
571
+ snapshot_done: true
572
+ },
573
+ $unset: {
574
+ snapshot_status: 1
575
+ }
576
+ },
577
+ { session }
578
+ );
579
+
580
+ if (no_checkpoint_before_lsn != null) {
581
+ await this.db.sync_rules.updateOne(
582
+ {
583
+ _id: this.group_id,
584
+ 'sync_configs._id': this.syncConfigId
585
+ },
586
+ {
587
+ $set: {
588
+ last_keepalive_ts: new Date()
589
+ },
590
+ $max: {
591
+ 'sync_configs.$[config].no_checkpoint_before': no_checkpoint_before_lsn
592
+ }
593
+ },
594
+ {
595
+ session: this.session,
596
+ arrayFilters: [{ 'config._id': this.syncConfigId }]
597
+ }
598
+ );
599
+ }
600
+ });
601
+ return tables.map((table) => {
602
+ const copy = table.clone();
603
+ copy.snapshotComplete = true;
604
+ return copy;
605
+ });
606
+ }
607
+ }