@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,509 @@
1
+ import { ReplicationAssertionError } from '@powersync/lib-services-framework';
2
+ import { ColumnDescriptor, SourceTable, storage } from '@powersync/service-core';
3
+ import * as bson from 'bson';
4
+ import { mongoTableId } from '../../../utils/util.js';
5
+ import { calculateCheckpointState } from '../CheckpointState.js';
6
+ import { MongoBucketBatch, MongoBucketBatchOptions } from '../MongoBucketBatch.js';
7
+ import { PersistedBatch } from '../common/PersistedBatch.js';
8
+ import { SourceRecordStore } from '../common/SourceRecordStore.js';
9
+ import { PersistedBatchV1 } from './PersistedBatchV1.js';
10
+ import { SourceRecordStoreV1 } from './SourceRecordStoreV1.js';
11
+ import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
12
+ import { SourceTableDocumentV1, SyncRuleDocumentV1 } from './models.js';
13
+
14
+ export class MongoBucketBatchV1 extends MongoBucketBatch {
15
+ declare public readonly db: VersionedPowerSyncMongoV1;
16
+
17
+ private readonly store: SourceRecordStore;
18
+ private needsActivation = true;
19
+ private lastWaitingLogThrottled = 0;
20
+
21
+ constructor(options: MongoBucketBatchOptions) {
22
+ super(options);
23
+ this.store = new SourceRecordStoreV1(this.db, this.group_id);
24
+ }
25
+
26
+ protected createPersistedBatch(writtenSize: number): PersistedBatch {
27
+ return new PersistedBatchV1(this.db, this.group_id, this.mapping, writtenSize, {
28
+ logger: this.logger
29
+ });
30
+ }
31
+
32
+ protected get sourceRecordStore(): SourceRecordStore {
33
+ return this.store;
34
+ }
35
+
36
+ protected async cleanupDroppedSourceTables(_tables: SourceTable[]) {
37
+ // No-op for V1: source records live in a shared collection.
38
+ }
39
+
40
+ async resolveTables(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult> {
41
+ const syncRules = options.syncRules ?? this.sync_rules;
42
+ const { connection_id, source } = options;
43
+ const { schema, name, objectId, replicaIdColumns, connectionTag, sendsCompleteRows } = source;
44
+
45
+ const normalizedReplicaIdColumns = replicaIdColumns.map((column) => ({
46
+ name: column.name,
47
+ type: column.type,
48
+ type_oid: column.typeId
49
+ }));
50
+
51
+ let result: storage.ResolveTablesResult | null = null;
52
+ await this.db.client.withSession(async (session) => {
53
+ const col = this.db.commonSourceTables(this.group_id);
54
+ const filter: any = {
55
+ group_id: this.group_id,
56
+ connection_id,
57
+ schema_name: schema,
58
+ table_name: name,
59
+ replica_id_columns2: normalizedReplicaIdColumns
60
+ };
61
+ if (objectId != null) {
62
+ filter.relation_id = objectId;
63
+ }
64
+
65
+ let doc = await col.findOne(filter, { session });
66
+ if (doc == null) {
67
+ doc = {
68
+ _id: options.idGenerator ? (options.idGenerator() as bson.ObjectId) : new bson.ObjectId(),
69
+ group_id: this.group_id,
70
+ connection_id,
71
+ relation_id: objectId,
72
+ schema_name: schema,
73
+ table_name: name,
74
+ replica_id_columns: null,
75
+ replica_id_columns2: normalizedReplicaIdColumns,
76
+ snapshot_done: false,
77
+ snapshot_status: undefined
78
+ };
79
+ await col.insertOne(doc, { session });
80
+ }
81
+
82
+ const sourceTable = new storage.SourceTable({
83
+ id: doc._id,
84
+ ref: source,
85
+ objectId,
86
+ replicaIdColumns,
87
+ snapshotComplete: doc.snapshot_done ?? true,
88
+ ...syncRules.getMatchingSources(source)
89
+ });
90
+ sourceTable.syncEvent = syncRules.tableTriggersEvent(source);
91
+ sourceTable.syncData = sourceTable.bucketDataSources.length > 0;
92
+ sourceTable.syncParameters = sourceTable.parameterLookupSources.length > 0;
93
+ sourceTable.storeCurrentData = sendsCompleteRows !== true;
94
+ sourceTable.snapshotStatus =
95
+ doc.snapshot_status == null
96
+ ? undefined
97
+ : {
98
+ lastKey: doc.snapshot_status.last_key?.buffer ?? null,
99
+ totalEstimatedCount: doc.snapshot_status.total_estimated_count,
100
+ replicatedCount: doc.snapshot_status.replicated_count
101
+ };
102
+
103
+ const truncateFilter = [{ schema_name: schema, table_name: name }] as any[];
104
+ if (objectId != null) {
105
+ truncateFilter.push({ relation_id: objectId });
106
+ }
107
+ const truncate = await col
108
+ .find(
109
+ {
110
+ group_id: this.group_id,
111
+ connection_id,
112
+ _id: { $ne: doc._id },
113
+ $or: truncateFilter
114
+ },
115
+ { session }
116
+ )
117
+ .toArray();
118
+ const dropTables = truncate.map((dropDoc) => {
119
+ const ref = {
120
+ connectionTag,
121
+ schema: dropDoc.schema_name,
122
+ name: dropDoc.table_name
123
+ };
124
+ const table = new storage.SourceTable({
125
+ id: dropDoc._id,
126
+ ref,
127
+ objectId: dropDoc.relation_id,
128
+ replicaIdColumns:
129
+ dropDoc.replica_id_columns2?.map(
130
+ (c) => ({ name: c.name, typeId: c.type_oid, type: c.type }) satisfies ColumnDescriptor
131
+ ) ?? [],
132
+ snapshotComplete: dropDoc.snapshot_done ?? true,
133
+ ...syncRules.getMatchingSources(ref)
134
+ });
135
+ table.syncEvent = syncRules.tableTriggersEvent(ref);
136
+ table.syncData = table.bucketDataSources.length > 0;
137
+ table.syncParameters = table.parameterLookupSources.length > 0;
138
+ return table;
139
+ });
140
+
141
+ result = { tables: [sourceTable], dropTables };
142
+ });
143
+
144
+ return result!;
145
+ }
146
+
147
+ async getSourceTableStatus(table: storage.SourceTable): Promise<storage.SourceTable | null> {
148
+ const doc = (await this.db.commonSourceTables(this.group_id).findOne(
149
+ {
150
+ group_id: this.group_id,
151
+ _id: mongoTableId(table.id)
152
+ },
153
+ { session: this.session }
154
+ )) as SourceTableDocumentV1 | null;
155
+ if (doc == null) {
156
+ return null;
157
+ }
158
+
159
+ const ref = {
160
+ connectionTag: table.ref.connectionTag,
161
+ schema: doc.schema_name,
162
+ name: doc.table_name
163
+ };
164
+ const sourceTable = new storage.SourceTable({
165
+ id: doc._id,
166
+ ref,
167
+ objectId: doc.relation_id,
168
+ replicaIdColumns:
169
+ doc.replica_id_columns2?.map(
170
+ (c) => ({ name: c.name, typeId: c.type_oid, type: c.type }) satisfies ColumnDescriptor
171
+ ) ?? [],
172
+ snapshotComplete: doc.snapshot_done ?? true,
173
+ ...this.sync_rules.getMatchingSources(ref)
174
+ });
175
+ sourceTable.syncEvent = this.sync_rules.tableTriggersEvent(ref);
176
+ sourceTable.syncData = sourceTable.bucketDataSources.length > 0;
177
+ sourceTable.syncParameters = sourceTable.parameterLookupSources.length > 0;
178
+ sourceTable.snapshotStatus =
179
+ doc.snapshot_status == null
180
+ ? undefined
181
+ : {
182
+ lastKey: doc.snapshot_status.last_key?.buffer ?? null,
183
+ totalEstimatedCount: doc.snapshot_status.total_estimated_count,
184
+ replicatedCount: doc.snapshot_status.replicated_count
185
+ };
186
+ return sourceTable;
187
+ }
188
+
189
+ async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {
190
+ const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
191
+
192
+ await this.flush(options);
193
+
194
+ const now = new Date();
195
+
196
+ await this.db.write_checkpoints.updateMany(
197
+ {
198
+ processed_at_lsn: null,
199
+ 'lsns.1': { $lte: lsn }
200
+ },
201
+ {
202
+ $set: {
203
+ processed_at_lsn: lsn
204
+ }
205
+ },
206
+ {
207
+ session: this.session
208
+ }
209
+ );
210
+
211
+ const can_checkpoint = {
212
+ $and: [
213
+ { $eq: ['$snapshot_done', true] },
214
+ {
215
+ $or: [{ $eq: ['$last_checkpoint_lsn', null] }, { $lte: ['$last_checkpoint_lsn', { $literal: lsn }] }]
216
+ },
217
+ {
218
+ $or: [{ $eq: ['$no_checkpoint_before', null] }, { $lte: ['$no_checkpoint_before', { $literal: lsn }] }]
219
+ }
220
+ ]
221
+ };
222
+
223
+ const new_keepalive_op = {
224
+ $cond: [
225
+ can_checkpoint,
226
+ { $literal: null },
227
+ {
228
+ $toString: {
229
+ $max: [{ $toLong: '$keepalive_op' }, { $literal: this.persisted_op }, 0n]
230
+ }
231
+ }
232
+ ]
233
+ };
234
+
235
+ const new_last_checkpoint = {
236
+ $cond: [
237
+ can_checkpoint,
238
+ {
239
+ $max: ['$last_checkpoint', { $literal: this.persisted_op }, { $toLong: '$keepalive_op' }, 0n]
240
+ },
241
+ '$last_checkpoint'
242
+ ]
243
+ };
244
+
245
+ const preUpdateDocument = (await this.db.sync_rules.findOneAndUpdate(
246
+ { _id: this.group_id },
247
+ [
248
+ {
249
+ $set: {
250
+ _can_checkpoint: can_checkpoint,
251
+ _not_empty: createEmptyCheckpoints
252
+ ? true
253
+ : {
254
+ $or: [
255
+ { $literal: createEmptyCheckpoints },
256
+ { $ne: ['$keepalive_op', new_keepalive_op] },
257
+ { $ne: ['$last_checkpoint', new_last_checkpoint] }
258
+ ]
259
+ }
260
+ }
261
+ },
262
+ {
263
+ $set: {
264
+ last_checkpoint_lsn: {
265
+ $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: lsn }, '$last_checkpoint_lsn']
266
+ },
267
+ last_checkpoint_ts: {
268
+ $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: now }, '$last_checkpoint_ts']
269
+ },
270
+ last_keepalive_ts: { $literal: now },
271
+ last_fatal_error: { $literal: null },
272
+ last_fatal_error_ts: { $literal: null },
273
+ keepalive_op: new_keepalive_op,
274
+ last_checkpoint: new_last_checkpoint,
275
+ snapshot_lsn: {
276
+ $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: null }, '$snapshot_lsn']
277
+ }
278
+ }
279
+ },
280
+ {
281
+ $unset: ['_can_checkpoint', '_not_empty']
282
+ }
283
+ ],
284
+ {
285
+ session: this.session,
286
+ returnDocument: 'before',
287
+ projection: {
288
+ snapshot_done: 1,
289
+ last_checkpoint_lsn: 1,
290
+ no_checkpoint_before: 1,
291
+ keepalive_op: 1,
292
+ last_checkpoint: 1
293
+ }
294
+ }
295
+ )) as SyncRuleDocumentV1;
296
+
297
+ if (preUpdateDocument == null) {
298
+ throw new ReplicationAssertionError(
299
+ 'Failed to update checkpoint - no matching sync_rules document for _id: ' + this.group_id
300
+ );
301
+ }
302
+
303
+ const checkpointState = calculateCheckpointState({
304
+ lsn,
305
+ snapshotDone: preUpdateDocument.snapshot_done === true,
306
+ lastCheckpointLsn: preUpdateDocument.last_checkpoint_lsn,
307
+ noCheckpointBefore: preUpdateDocument.no_checkpoint_before,
308
+ keepaliveOp: preUpdateDocument.keepalive_op == null ? null : BigInt(preUpdateDocument.keepalive_op),
309
+ lastCheckpoint: preUpdateDocument.last_checkpoint,
310
+ persistedOp: this.persisted_op,
311
+ createEmptyCheckpoints
312
+ });
313
+ if (checkpointState.checkpointBlocked) {
314
+ if (Date.now() - this.lastWaitingLogThrottled > 5_000) {
315
+ this.logger.info(
316
+ `Waiting before creating checkpoint, currently at ${lsn} / ${checkpointState.newKeepaliveOp}. Current state: ${JSON.stringify(
317
+ {
318
+ snapshot_done: preUpdateDocument.snapshot_done,
319
+ last_checkpoint_lsn: preUpdateDocument.last_checkpoint_lsn,
320
+ no_checkpoint_before: preUpdateDocument.no_checkpoint_before
321
+ }
322
+ )}`
323
+ );
324
+ this.lastWaitingLogThrottled = Date.now();
325
+ }
326
+ } else {
327
+ if (checkpointState.checkpointCreated) {
328
+ this.logger.debug(`Created checkpoint at ${lsn} / ${checkpointState.newLastCheckpoint}`);
329
+ }
330
+ await this.autoActivate(lsn);
331
+ await this.db.notifyCheckpoint();
332
+ this.persisted_op = null;
333
+ this.last_checkpoint_lsn = lsn;
334
+ if (checkpointState.newLastCheckpoint != null) {
335
+ await this.sourceRecordStore.postCommitCleanup(checkpointState.newLastCheckpoint, this.logger);
336
+ }
337
+ }
338
+
339
+ return {
340
+ checkpointBlocked: checkpointState.checkpointBlocked,
341
+ checkpointCreated: checkpointState.checkpointCreated
342
+ };
343
+ }
344
+
345
+ async keepalive(lsn: string): Promise<storage.CheckpointResult> {
346
+ return await this.commit(lsn, { createEmptyCheckpoints: true });
347
+ }
348
+
349
+ async setResumeLsn(lsn: string): Promise<void> {
350
+ await this.db.sync_rules.updateOne(
351
+ {
352
+ _id: this.group_id
353
+ },
354
+ {
355
+ $set: {
356
+ snapshot_lsn: lsn
357
+ }
358
+ },
359
+ { session: this.session }
360
+ );
361
+ }
362
+
363
+ async markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void> {
364
+ await this.db.sync_rules.updateOne(
365
+ {
366
+ _id: this.group_id
367
+ },
368
+ {
369
+ $set: {
370
+ snapshot_done: true,
371
+ last_keepalive_ts: new Date()
372
+ },
373
+ $max: {
374
+ no_checkpoint_before: no_checkpoint_before_lsn
375
+ }
376
+ },
377
+ { session: this.session }
378
+ );
379
+ }
380
+
381
+ async markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void> {
382
+ await this.withTransaction(async () => {
383
+ // Protect against race conditions
384
+ const count = await this.db.commonSourceTables(this.group_id).countDocuments(
385
+ {
386
+ group_id: this.group_id,
387
+ snapshot_done: false
388
+ },
389
+ { session: this.session }
390
+ );
391
+ if (count > 0) {
392
+ if (options?.throwOnConflict ?? true) {
393
+ throw new ReplicationAssertionError(
394
+ `Cannot mark snapshot done while ${count} source table${count == 1 ? '' : 's'} still require snapshotting`
395
+ );
396
+ } else {
397
+ return;
398
+ }
399
+ }
400
+
401
+ await this.markAllSnapshotDone(no_checkpoint_before_lsn);
402
+ });
403
+ }
404
+
405
+ async markTableSnapshotRequired(_table: storage.SourceTable): Promise<void> {
406
+ await this.db.sync_rules.updateOne(
407
+ {
408
+ _id: this.group_id
409
+ },
410
+ {
411
+ $set: {
412
+ snapshot_done: false
413
+ }
414
+ },
415
+ { session: this.session }
416
+ );
417
+ }
418
+
419
+ async markTableSnapshotDone(
420
+ tables: storage.SourceTable[],
421
+ no_checkpoint_before_lsn?: string
422
+ ): Promise<storage.SourceTable[]> {
423
+ const session = this.session;
424
+ const ids = tables.map((table) => mongoTableId(table.id));
425
+
426
+ await this.withTransaction(async () => {
427
+ await this.db.commonSourceTables(this.group_id).updateMany(
428
+ { _id: { $in: ids } },
429
+ {
430
+ $set: {
431
+ snapshot_done: true
432
+ },
433
+ $unset: {
434
+ snapshot_status: 1
435
+ }
436
+ },
437
+ { session }
438
+ );
439
+
440
+ if (no_checkpoint_before_lsn != null) {
441
+ await this.db.sync_rules.updateOne(
442
+ {
443
+ _id: this.group_id
444
+ },
445
+ {
446
+ $set: {
447
+ last_keepalive_ts: new Date()
448
+ },
449
+ $max: {
450
+ no_checkpoint_before: no_checkpoint_before_lsn
451
+ }
452
+ },
453
+ { session: this.session }
454
+ );
455
+ }
456
+ });
457
+ return tables.map((table) => {
458
+ const copy = table.clone();
459
+ copy.snapshotComplete = true;
460
+ return copy;
461
+ });
462
+ }
463
+
464
+ private async autoActivate(lsn: string): Promise<void> {
465
+ if (!this.needsActivation) {
466
+ return;
467
+ }
468
+
469
+ const session = this.session;
470
+ let activated = false;
471
+ await session.withTransaction(async () => {
472
+ const doc = (await this.db.sync_rules.findOne({ _id: this.group_id }, { session })) as SyncRuleDocumentV1;
473
+ if (doc && doc.state == storage.SyncRuleState.PROCESSING && doc.snapshot_done && doc.last_checkpoint != null) {
474
+ await this.db.sync_rules.updateOne(
475
+ {
476
+ _id: this.group_id
477
+ },
478
+ {
479
+ $set: {
480
+ state: storage.SyncRuleState.ACTIVE
481
+ }
482
+ },
483
+ { session }
484
+ );
485
+
486
+ await this.db.sync_rules.updateMany(
487
+ {
488
+ _id: { $ne: this.group_id },
489
+ state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
490
+ },
491
+ {
492
+ $set: {
493
+ state: storage.SyncRuleState.STOP
494
+ }
495
+ },
496
+ { session }
497
+ );
498
+ activated = true;
499
+ } else if (doc?.state != storage.SyncRuleState.PROCESSING) {
500
+ this.needsActivation = false;
501
+ }
502
+ });
503
+ if (activated) {
504
+ this.logger.info(`Activated new replication stream at ${lsn}`);
505
+ await this.db.notifyCheckpoint();
506
+ this.needsActivation = false;
507
+ }
508
+ }
509
+ }
@@ -0,0 +1,75 @@
1
+ import {
2
+ bson,
3
+ BucketChecksum,
4
+ FetchPartialBucketChecksum,
5
+ InternalOpId,
6
+ PartialChecksumMap
7
+ } from '@powersync/service-core';
8
+ import { FetchPartialBucketChecksumByBucket, MongoChecksums } from '../MongoChecksums.js';
9
+ import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
10
+
11
+ export class MongoChecksumsV1 extends MongoChecksums {
12
+ declare protected readonly db: VersionedPowerSyncMongoV1;
13
+
14
+ async computePartialChecksumsDirectByBucket(
15
+ batch: FetchPartialBucketChecksumByBucket[]
16
+ ): Promise<PartialChecksumMap> {
17
+ return this.computePartialChecksumsForCollection(batch, this.db.bucketDataV1, (request) => ({
18
+ _id: {
19
+ $gt: {
20
+ g: this.group_id,
21
+ b: request.bucket,
22
+ o: request.start ?? new bson.MinKey()
23
+ },
24
+ $lte: {
25
+ g: this.group_id,
26
+ b: request.bucket,
27
+ o: request.end
28
+ }
29
+ }
30
+ }));
31
+ }
32
+
33
+ protected async fetchPreStates(
34
+ batch: FetchPartialBucketChecksum[]
35
+ ): Promise<Map<string, { opId: InternalOpId; checksum: BucketChecksum }>> {
36
+ const preFilters = batch
37
+ .filter((request) => request.start == null)
38
+ .map((request) => ({
39
+ _id: {
40
+ g: this.group_id,
41
+ b: request.bucket
42
+ },
43
+ 'compacted_state.op_id': { $exists: true, $lte: request.end }
44
+ }));
45
+
46
+ const preStates = new Map<string, { opId: InternalOpId; checksum: BucketChecksum }>();
47
+ if (preFilters.length == 0) {
48
+ return preStates;
49
+ }
50
+
51
+ const states = await this.db.bucketStateV1
52
+ .find({
53
+ $or: preFilters
54
+ })
55
+ .toArray();
56
+
57
+ for (const state of states) {
58
+ const compactedState = state.compacted_state!;
59
+ preStates.set(state._id.b, {
60
+ opId: compactedState.op_id,
61
+ checksum: {
62
+ bucket: state._id.b,
63
+ checksum: Number(compactedState.checksum),
64
+ count: compactedState.count
65
+ }
66
+ });
67
+ }
68
+
69
+ return preStates;
70
+ }
71
+
72
+ protected async computePartialChecksumsInternal(batch: FetchPartialBucketChecksum[]): Promise<PartialChecksumMap> {
73
+ return this.computePartialChecksumsDirectByBucket(batch);
74
+ }
75
+ }
@@ -0,0 +1,93 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { ReplicationAssertionError } from '@powersync/lib-services-framework';
3
+ import { storage } from '@powersync/service-core';
4
+ import { BucketDefinitionId } from '@powersync/service-sync-rules';
5
+ import { SingleBucketStore } from '../common/SingleBucketStore.js';
6
+ import { BucketStateDocumentBase, LEGACY_BUCKET_DATA_DEFINITION_ID } from '../models.js';
7
+ import { DirtyBucket, MongoCompactor } from '../MongoCompactor.js';
8
+ import { BucketStateDocumentV1 } from './models.js';
9
+ import type { MongoSyncBucketStorageV1 } from './MongoSyncBucketStorageV1.js';
10
+ import { SingleBucketStoreV1 } from './SingleBucketStoreV1.js';
11
+ import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
12
+
13
+ export class MongoCompactorV1 extends MongoCompactor {
14
+ // Override types to the more specific ones
15
+ declare protected readonly db: VersionedPowerSyncMongoV1;
16
+ declare protected readonly storage: MongoSyncBucketStorageV1;
17
+
18
+ public async *dirtyBucketBatches(options: {
19
+ minBucketChanges: number;
20
+ minChangeRatio: number;
21
+ }): AsyncGenerator<DirtyBucket[]> {
22
+ if (options.minBucketChanges <= 0) {
23
+ throw new ReplicationAssertionError('minBucketChanges must be >= 1');
24
+ }
25
+ // Previously, we used an index on {_id.g: 1, estimate_since_compact.count: 1} to only scan buckets with changes.
26
+ // That works well if there are only a small number of dirty buckets, but it causes repeated rescans while data is
27
+ // still changing. We now iterate through all V1 bucket_state rows for the group and filter after projecting.
28
+ yield* this.dirtyBucketBatchesForCollection(
29
+ this.db.bucketStateV1,
30
+ { g: this.group_id, b: new mongo.MinKey() as any },
31
+ { g: this.group_id, b: new mongo.MaxKey() as any },
32
+ options,
33
+ () => null
34
+ );
35
+ }
36
+
37
+ public async dirtyBucketBatchForChecksums(options: { minBucketChanges: number }): Promise<DirtyBucket[]> {
38
+ if (options.minBucketChanges <= 0) {
39
+ throw new ReplicationAssertionError('minBucketChanges must be >= 1');
40
+ }
41
+ // Unlike dirtyBucketBatches, this path is resumable after restart because populateChecksums resets
42
+ // estimate_since_compact as it progresses.
43
+ return this.dirtyBucketBatchForChecksumsForCollection(
44
+ this.db.bucketStateV1,
45
+ {
46
+ '_id.g': this.group_id,
47
+ 'estimate_since_compact.count': { $gte: options.minBucketChanges }
48
+ },
49
+ () => null
50
+ );
51
+ }
52
+
53
+ protected async writeBucketStateUpdates(): Promise<void> {
54
+ await this.db.bucketStateV1.bulkWrite(
55
+ this.bucketStateUpdates as mongo.AnyBulkWriteOperation<BucketStateDocumentV1>[],
56
+ { ordered: false }
57
+ );
58
+ }
59
+
60
+ protected async computeChecksumsForBuckets(
61
+ buckets: Pick<DirtyBucket, 'bucket' | 'definitionId'>[]
62
+ ): Promise<storage.PartialChecksumMap> {
63
+ return this.storage.checksums.computePartialChecksumsDirectByBucket(
64
+ buckets.map(({ bucket }) => ({
65
+ bucket,
66
+ end: this.maxOpId
67
+ }))
68
+ );
69
+ }
70
+
71
+ protected bucketStateFilter(
72
+ bucket: string,
73
+ _definitionId: BucketDefinitionId | null
74
+ ): mongo.Filter<BucketStateDocumentBase> {
75
+ return {
76
+ _id: {
77
+ g: this.group_id,
78
+ b: bucket
79
+ }
80
+ };
81
+ }
82
+
83
+ protected async getBucketDataContext(
84
+ bucket: string,
85
+ _definitionId: BucketDefinitionId | null
86
+ ): Promise<SingleBucketStore | null> {
87
+ return new SingleBucketStoreV1(this.db, {
88
+ replicationStreamId: this.group_id,
89
+ definitionId: LEGACY_BUCKET_DATA_DEFINITION_ID,
90
+ bucket
91
+ });
92
+ }
93
+ }
@@ -0,0 +1,26 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { MongoParameterCompactor } from '../MongoParameterCompactor.js';
3
+ import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
4
+
5
+ export class MongoParameterCompactorV1 extends MongoParameterCompactor {
6
+ declare protected readonly db: VersionedPowerSyncMongoV1;
7
+
8
+ protected async getCollections(): Promise<mongo.Collection<mongo.Document>[]> {
9
+ return [this.db.parameterIndexV1 as unknown as mongo.Collection<mongo.Document>];
10
+ }
11
+
12
+ protected collectionFilter(): mongo.Document {
13
+ return {
14
+ 'key.g': this.group_id
15
+ };
16
+ }
17
+
18
+ protected deleteFilter(doc: mongo.Document): mongo.Document {
19
+ return {
20
+ 'key.g': doc.key.g as number,
21
+ lookup: doc.lookup,
22
+ _id: { $lte: doc._id },
23
+ key: doc.key
24
+ };
25
+ }
26
+ }