@powersync/service-module-mongodb-storage 0.15.4 → 0.16.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 (193) hide show
  1. package/CHANGELOG.md +35 -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 +2 -2
  7. package/dist/storage/MongoBucketStorage.js +47 -34
  8. package/dist/storage/MongoBucketStorage.js.map +1 -1
  9. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +17 -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/MongoBucketBatch.d.ts +16 -14
  13. package/dist/storage/implementation/MongoBucketBatch.js +80 -115
  14. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  15. package/dist/storage/implementation/MongoBucketBatchShared.d.ts +5 -0
  16. package/dist/storage/implementation/MongoBucketBatchShared.js +8 -0
  17. package/dist/storage/implementation/MongoBucketBatchShared.js.map +1 -0
  18. package/dist/storage/implementation/MongoChecksums.d.ts +28 -17
  19. package/dist/storage/implementation/MongoChecksums.js +13 -72
  20. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  21. package/dist/storage/implementation/MongoCompactor.d.ts +98 -58
  22. package/dist/storage/implementation/MongoCompactor.js +229 -296
  23. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  24. package/dist/storage/implementation/MongoParameterCompactor.d.ts +11 -6
  25. package/dist/storage/implementation/MongoParameterCompactor.js +11 -8
  26. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  27. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +14 -0
  28. package/dist/storage/implementation/MongoPersistedSyncRules.js +64 -0
  29. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -0
  30. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +3 -0
  31. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +9 -0
  32. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  33. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +47 -29
  34. package/dist/storage/implementation/MongoSyncBucketStorage.js +94 -387
  35. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  36. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +5 -3
  37. package/dist/storage/implementation/MongoSyncRulesLock.js +12 -10
  38. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  39. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +1 -1
  40. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  41. package/dist/storage/implementation/OperationBatch.js +1 -1
  42. package/dist/storage/implementation/common/BucketDataDoc.d.ts +35 -0
  43. package/dist/storage/implementation/common/BucketDataDoc.js +2 -0
  44. package/dist/storage/implementation/common/BucketDataDoc.js.map +1 -0
  45. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +13 -0
  46. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +2 -0
  47. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +1 -0
  48. package/dist/storage/implementation/common/PersistedBatch.d.ts +108 -0
  49. package/dist/storage/implementation/common/PersistedBatch.js +237 -0
  50. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -0
  51. package/dist/storage/implementation/common/SingleBucketStore.d.ts +54 -0
  52. package/dist/storage/implementation/common/SingleBucketStore.js +3 -0
  53. package/dist/storage/implementation/common/SingleBucketStore.js.map +1 -0
  54. package/dist/storage/implementation/common/SourceRecordStore.d.ts +36 -0
  55. package/dist/storage/implementation/common/SourceRecordStore.js +2 -0
  56. package/dist/storage/implementation/common/SourceRecordStore.js.map +1 -0
  57. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +27 -0
  58. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +57 -0
  59. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -0
  60. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +7 -0
  61. package/dist/storage/implementation/createMongoSyncBucketStorage.js +9 -0
  62. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -0
  63. package/dist/storage/implementation/db.d.ts +32 -35
  64. package/dist/storage/implementation/db.js +77 -99
  65. package/dist/storage/implementation/db.js.map +1 -1
  66. package/dist/storage/implementation/models.d.ts +62 -33
  67. package/dist/storage/implementation/models.js +20 -1
  68. package/dist/storage/implementation/models.js.map +1 -1
  69. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +13 -0
  70. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +22 -0
  71. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -0
  72. package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +12 -0
  73. package/dist/storage/implementation/v1/MongoChecksumsV1.js +56 -0
  74. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -0
  75. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +23 -0
  76. package/dist/storage/implementation/v1/MongoCompactorV1.js +52 -0
  77. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -0
  78. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +9 -0
  79. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -0
  80. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -0
  81. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +41 -0
  82. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +283 -0
  83. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -0
  84. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +26 -0
  85. package/dist/storage/implementation/v1/PersistedBatchV1.js +183 -0
  86. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -0
  87. package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +18 -0
  88. package/dist/storage/implementation/v1/SingleBucketStoreV1.js +57 -0
  89. package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -0
  90. package/dist/storage/implementation/v1/SourceRecordStoreV1.d.ts +19 -0
  91. package/dist/storage/implementation/v1/SourceRecordStoreV1.js +105 -0
  92. package/dist/storage/implementation/v1/SourceRecordStoreV1.js.map +1 -0
  93. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +12 -0
  94. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +20 -0
  95. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -0
  96. package/dist/storage/implementation/v1/models.d.ts +34 -0
  97. package/dist/storage/implementation/v1/models.js +37 -0
  98. package/dist/storage/implementation/v1/models.js.map +1 -0
  99. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +13 -0
  100. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +34 -0
  101. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -0
  102. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +15 -0
  103. package/dist/storage/implementation/v3/MongoChecksumsV3.js +84 -0
  104. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -0
  105. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -0
  106. package/dist/storage/implementation/v3/MongoCompactorV3.js +68 -0
  107. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -0
  108. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +9 -0
  109. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +18 -0
  110. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  111. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +5 -0
  112. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +9 -0
  113. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +1 -0
  114. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +41 -0
  115. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +407 -0
  116. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -0
  117. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +29 -0
  118. package/dist/storage/implementation/v3/PersistedBatchV3.js +259 -0
  119. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -0
  120. package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +18 -0
  121. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +48 -0
  122. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -0
  123. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +22 -0
  124. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +164 -0
  125. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -0
  126. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +21 -0
  127. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +71 -0
  128. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -0
  129. package/dist/storage/implementation/v3/models.d.ts +43 -0
  130. package/dist/storage/implementation/v3/models.js +34 -0
  131. package/dist/storage/implementation/v3/models.js.map +1 -0
  132. package/dist/storage/storage-index.d.ts +6 -3
  133. package/dist/storage/storage-index.js +6 -3
  134. package/dist/storage/storage-index.js.map +1 -1
  135. package/dist/utils/util.d.ts +10 -3
  136. package/dist/utils/util.js +24 -3
  137. package/dist/utils/util.js.map +1 -1
  138. package/package.json +9 -9
  139. package/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts +1 -1
  140. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +6 -6
  141. package/src/storage/MongoBucketStorage.ts +92 -59
  142. package/src/storage/implementation/BucketDefinitionMapping.ts +72 -0
  143. package/src/storage/implementation/MongoBucketBatch.ts +110 -144
  144. package/src/storage/implementation/MongoBucketBatchShared.ts +11 -0
  145. package/src/storage/implementation/MongoChecksums.ts +52 -75
  146. package/src/storage/implementation/MongoCompactor.ts +374 -404
  147. package/src/storage/implementation/MongoParameterCompactor.ts +37 -24
  148. package/src/storage/implementation/MongoPersistedSyncRules.ts +76 -0
  149. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +17 -0
  150. package/src/storage/implementation/MongoSyncBucketStorage.ts +181 -455
  151. package/src/storage/implementation/MongoSyncRulesLock.ts +11 -13
  152. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +3 -1
  153. package/src/storage/implementation/OperationBatch.ts +1 -1
  154. package/src/storage/implementation/common/BucketDataDoc.ts +37 -0
  155. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +15 -0
  156. package/src/storage/implementation/common/PersistedBatch.ts +364 -0
  157. package/src/storage/implementation/common/SingleBucketStore.ts +63 -0
  158. package/src/storage/implementation/common/SourceRecordStore.ts +49 -0
  159. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +80 -0
  160. package/src/storage/implementation/createMongoSyncBucketStorage.ts +25 -0
  161. package/src/storage/implementation/db.ts +105 -129
  162. package/src/storage/implementation/models.ts +82 -36
  163. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +32 -0
  164. package/src/storage/implementation/v1/MongoChecksumsV1.ts +75 -0
  165. package/src/storage/implementation/v1/MongoCompactorV1.ts +93 -0
  166. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +26 -0
  167. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +448 -0
  168. package/src/storage/implementation/v1/PersistedBatchV1.ts +230 -0
  169. package/src/storage/implementation/v1/SingleBucketStoreV1.ts +74 -0
  170. package/src/storage/implementation/v1/SourceRecordStoreV1.ts +156 -0
  171. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +28 -0
  172. package/src/storage/implementation/v1/models.ts +84 -0
  173. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +44 -0
  174. package/src/storage/implementation/v3/MongoChecksumsV3.ts +120 -0
  175. package/src/storage/implementation/v3/MongoCompactorV3.ts +107 -0
  176. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +24 -0
  177. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +12 -0
  178. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +550 -0
  179. package/src/storage/implementation/v3/PersistedBatchV3.ts +318 -0
  180. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +68 -0
  181. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +226 -0
  182. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +112 -0
  183. package/src/storage/implementation/v3/models.ts +96 -0
  184. package/src/storage/storage-index.ts +6 -3
  185. package/src/utils/util.ts +34 -5
  186. package/test/src/storage_compacting.test.ts +57 -29
  187. package/test/src/storage_sync.test.ts +351 -5
  188. package/test/tsconfig.json +0 -1
  189. package/tsconfig.tsbuildinfo +1 -1
  190. package/dist/storage/implementation/PersistedBatch.d.ts +0 -71
  191. package/dist/storage/implementation/PersistedBatch.js +0 -354
  192. package/dist/storage/implementation/PersistedBatch.js.map +0 -1
  193. package/src/storage/implementation/PersistedBatch.ts +0 -432
@@ -0,0 +1,318 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { ReplicationAssertionError } from '@powersync/lib-services-framework';
3
+ import { InternalOpId, storage } from '@powersync/service-core';
4
+ import { BucketDataSource } from '@powersync/service-sync-rules';
5
+ import * as bson from 'bson';
6
+ import { mongoTableId } from '../../../utils/util.js';
7
+ import { BucketDefinitionId } from '../BucketDefinitionMapping.js';
8
+ import {
9
+ BucketStateUpdate,
10
+ PersistedBatch,
11
+ SaveParameterDataOptions,
12
+ UpsertCurrentDataOptions
13
+ } from '../common/PersistedBatch.js';
14
+ import { SourceTableKey } from '../models.js';
15
+ import {
16
+ BucketParameterDocumentV3,
17
+ BucketStateDocumentV3,
18
+ CurrentDataDocumentV3,
19
+ serializeBucketDataV3,
20
+ SourceTableDocumentV3,
21
+ taggedBucketParameterDocumentToV3
22
+ } from './models.js';
23
+ import { serializeParameterLookupV3 } from './MongoParameterLookupV3.js';
24
+ import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
25
+
26
+ export class PersistedBatchV3 extends PersistedBatch {
27
+ declare protected readonly db: VersionedPowerSyncMongoV3;
28
+
29
+ currentData: { sourceTableId: bson.ObjectId; operation: mongo.AnyBulkWriteOperation<CurrentDataDocumentV3> }[] = [];
30
+ sourceTablePendingDeletes = new Map<string, InternalOpId>();
31
+
32
+ protected checkDefinitionId(definitionId: BucketDefinitionId | null): BucketDefinitionId {
33
+ if (definitionId == null) {
34
+ // This is required for V3 storage.
35
+ throw new ReplicationAssertionError('Expected v3 bucket when incrementalReprocessing is enabled');
36
+ }
37
+ return definitionId;
38
+ }
39
+
40
+ protected getBucketDefinitionId(bucketSource: BucketDataSource): BucketDefinitionId {
41
+ return this.mapping.bucketSourceId(bucketSource);
42
+ }
43
+
44
+ saveParameterData(data: SaveParameterDataOptions) {
45
+ const { sourceTable, sourceKey, evaluated } = data;
46
+ const remaining_lookups = new Map<string, SaveParameterDataOptions['existing_lookups'][number]>();
47
+
48
+ for (let lookup of data.existing_lookups) {
49
+ if (lookup.indexId == null) {
50
+ throw new ReplicationAssertionError('Expected v3 lookup when incrementalReprocessing is enabled');
51
+ }
52
+ remaining_lookups.set(`${lookup.indexId}.${lookup.lookup.toString('base64')}`, lookup);
53
+ }
54
+
55
+ for (let result of evaluated) {
56
+ const sourceDefinitionId = this.mapping.parameterLookupId(result.lookup.source);
57
+ const binLookup = serializeParameterLookupV3(result.lookup);
58
+ remaining_lookups.delete(`${sourceDefinitionId}.${binLookup.toString('base64')}`);
59
+
60
+ const op_id = data.op_seq.next();
61
+ this.debugLastOpId = op_id;
62
+ const values: BucketParameterDocumentV3 = {
63
+ _id: op_id,
64
+ key: {
65
+ t: mongoTableId(sourceTable.id),
66
+ k: sourceKey
67
+ } satisfies SourceTableKey,
68
+ lookup: binLookup,
69
+ bucket_parameters: result.bucketParameters
70
+ };
71
+ this.bucketParameters.push({
72
+ ...values,
73
+ index: sourceDefinitionId
74
+ });
75
+
76
+ this.currentSize += 200;
77
+ }
78
+
79
+ for (let lookup of remaining_lookups.values()) {
80
+ const op_id = data.op_seq.next();
81
+ this.debugLastOpId = op_id;
82
+ const indexId = lookup.indexId;
83
+ if (indexId == null) {
84
+ throw new ReplicationAssertionError('Expected v3 lookup when incrementalReprocessing is enabled');
85
+ }
86
+ const values: BucketParameterDocumentV3 = {
87
+ _id: op_id,
88
+ key: {
89
+ t: mongoTableId(sourceTable.id),
90
+ k: sourceKey
91
+ } satisfies SourceTableKey,
92
+ lookup: lookup.lookup,
93
+ bucket_parameters: []
94
+ };
95
+ this.bucketParameters.push({
96
+ ...values,
97
+ index: indexId
98
+ });
99
+
100
+ this.currentSize += 200;
101
+ }
102
+ }
103
+
104
+ hardDeleteCurrentData(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId) {
105
+ this.currentData.push({
106
+ sourceTableId,
107
+ operation: {
108
+ deleteOne: {
109
+ filter: { _id: replicaId }
110
+ }
111
+ }
112
+ });
113
+ this.currentSize += 50;
114
+ }
115
+
116
+ softDeleteCurrentData(
117
+ sourceTableId: bson.ObjectId,
118
+ replicaId: storage.ReplicaId,
119
+ checkpointGreaterThan: InternalOpId
120
+ ) {
121
+ this.currentData.push({
122
+ sourceTableId,
123
+ operation: {
124
+ updateOne: {
125
+ filter: { _id: replicaId },
126
+ update: {
127
+ $set: {
128
+ data: null,
129
+ buckets: [] as CurrentDataDocumentV3['buckets'],
130
+ lookups: [] as CurrentDataDocumentV3['lookups'],
131
+ pending_delete: checkpointGreaterThan
132
+ }
133
+ },
134
+ upsert: true
135
+ }
136
+ }
137
+ });
138
+ const sourceTableKey = sourceTableId.toHexString();
139
+ const existingPendingDelete = this.sourceTablePendingDeletes.get(sourceTableKey);
140
+ if (existingPendingDelete == null || checkpointGreaterThan > existingPendingDelete) {
141
+ this.sourceTablePendingDeletes.set(sourceTableKey, checkpointGreaterThan);
142
+ }
143
+
144
+ this.currentSize += 50;
145
+ }
146
+
147
+ upsertCurrentData(values: UpsertCurrentDataOptions) {
148
+ const buckets = values.buckets.map((bucket) => {
149
+ if (bucket.definitionId == null) {
150
+ throw new ReplicationAssertionError('Expected v3 bucket when incrementalReprocessing is enabled');
151
+ }
152
+ return {
153
+ def: bucket.definitionId,
154
+ bucket: bucket.bucket,
155
+ table: bucket.table,
156
+ id: bucket.id
157
+ };
158
+ });
159
+ const lookups = values.lookups.map((lookup) => {
160
+ if (lookup.indexId == null) {
161
+ throw new ReplicationAssertionError('Expected v3 lookup when incrementalReprocessing is enabled');
162
+ }
163
+ return {
164
+ i: lookup.indexId,
165
+ l: lookup.lookup
166
+ };
167
+ });
168
+
169
+ this.currentData.push({
170
+ sourceTableId: values.sourceTableId,
171
+ operation: {
172
+ updateOne: {
173
+ filter: { _id: values.replicaId },
174
+ update: {
175
+ $set: {
176
+ data: values.data,
177
+ buckets,
178
+ lookups
179
+ },
180
+ $unset: { pending_delete: 1 }
181
+ },
182
+ upsert: true
183
+ }
184
+ }
185
+ });
186
+ this.currentSize += (values.data?.length() ?? 0) + 100;
187
+ }
188
+
189
+ protected get currentDataCount() {
190
+ return this.currentData.length;
191
+ }
192
+
193
+ protected async flushBucketData(session: mongo.ClientSession) {
194
+ const operationsByDefinition = new Map<BucketDefinitionId, typeof this.bucketData>();
195
+ for (const document of this.bucketData) {
196
+ const existing = operationsByDefinition.get(document.bucketKey.definitionId) ?? [];
197
+ existing.push(document);
198
+ operationsByDefinition.set(document.bucketKey.definitionId, existing);
199
+ }
200
+
201
+ for (const [definitionId, documents] of operationsByDefinition.entries()) {
202
+ await this.db.bucketDataV3(this.group_id, definitionId).bulkWrite(
203
+ documents.map((document) => ({
204
+ insertOne: {
205
+ document: serializeBucketDataV3(document)
206
+ }
207
+ })),
208
+ {
209
+ session,
210
+ ordered: false
211
+ }
212
+ );
213
+ }
214
+ }
215
+
216
+ protected async flushBucketParameters(session: mongo.ClientSession) {
217
+ const operationsByIndex = new Map<string, typeof this.bucketParameters>();
218
+ for (const document of this.bucketParameters) {
219
+ const existing = operationsByIndex.get(document.index) ?? [];
220
+ existing.push(document);
221
+ operationsByIndex.set(document.index, existing);
222
+ }
223
+
224
+ for (const [indexId, documents] of operationsByIndex.entries()) {
225
+ await this.db.parameterIndexV3(this.group_id, indexId).bulkWrite(
226
+ documents.map((document) => ({
227
+ insertOne: {
228
+ document: taggedBucketParameterDocumentToV3(document)
229
+ }
230
+ })),
231
+ {
232
+ session,
233
+ ordered: false
234
+ }
235
+ );
236
+ }
237
+ }
238
+
239
+ protected async flushCurrentData(session: mongo.ClientSession) {
240
+ const operationsBySourceTable = new Map<string, typeof this.currentData>();
241
+ for (const operation of this.currentData) {
242
+ const sourceTableId = operation.sourceTableId.toHexString();
243
+ const existing = operationsBySourceTable.get(sourceTableId) ?? [];
244
+ existing.push(operation);
245
+ operationsBySourceTable.set(sourceTableId, existing);
246
+ }
247
+
248
+ const sourceTableUpdates: mongo.AnyBulkWriteOperation<SourceTableDocumentV3>[] = [
249
+ ...this.sourceTablePendingDeletes.entries()
250
+ ].map(([key, value]) => {
251
+ return {
252
+ updateOne: {
253
+ filter: { _id: new bson.ObjectId(key) },
254
+ update: {
255
+ $max: {
256
+ latest_pending_delete: value
257
+ }
258
+ }
259
+ }
260
+ };
261
+ });
262
+
263
+ if (sourceTableUpdates.length > 0) {
264
+ await this.db.sourceTablesV3(this.group_id).bulkWrite(sourceTableUpdates, { session, ordered: false });
265
+ }
266
+
267
+ for (const operations of operationsBySourceTable.values()) {
268
+ const sourceTableId = operations[0]!.sourceTableId;
269
+ await this.db.sourceRecordsV3(this.group_id, sourceTableId).bulkWrite(
270
+ operations.map((entry) => entry.operation),
271
+ {
272
+ session,
273
+ ordered: true
274
+ }
275
+ );
276
+ }
277
+ }
278
+
279
+ protected async flushBucketStates(session: mongo.ClientSession) {
280
+ await this.db.bucketStateV3(this.group_id).bulkWrite(this.getBucketStateUpdates(), {
281
+ session,
282
+ ordered: false
283
+ });
284
+ }
285
+
286
+ protected resetCurrentData() {
287
+ this.currentData = [];
288
+ this.sourceTablePendingDeletes.clear();
289
+ }
290
+
291
+ private getBucketStateUpdates(): mongo.AnyBulkWriteOperation<BucketStateDocumentV3>[] {
292
+ return Array.from(this.bucketStates.values()).map((state: BucketStateUpdate) => {
293
+ if (state.definitionId == null) {
294
+ throw new ReplicationAssertionError('Expected bucket definition id when incrementalReprocessing is enabled');
295
+ }
296
+ return {
297
+ updateOne: {
298
+ filter: {
299
+ _id: {
300
+ d: state.definitionId,
301
+ b: state.bucket
302
+ }
303
+ },
304
+ update: {
305
+ $set: {
306
+ last_op: state.lastOp
307
+ },
308
+ $inc: {
309
+ 'estimate_since_compact.count': state.incrementCount,
310
+ 'estimate_since_compact.bytes': state.incrementBytes
311
+ }
312
+ },
313
+ upsert: true
314
+ }
315
+ } satisfies mongo.AnyBulkWriteOperation<BucketStateDocumentV3>;
316
+ });
317
+ }
318
+ }
@@ -0,0 +1,68 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { InternalOpId } from '@powersync/service-core';
3
+ import { BucketDataDoc, BucketKey } from '../common/BucketDataDoc.js';
4
+ import {
5
+ BucketDataDocumentGeneric,
6
+ BucketDataDocumentGenericId,
7
+ SingleBucketStore
8
+ } from '../common/SingleBucketStore.js';
9
+ import { BucketDataProperties } from '../models.js';
10
+ import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
11
+ import { BucketDataDocumentV3, BucketDataKeyV3, loadBucketDataDocumentV3, serializeBucketDataV3 } from './models.js';
12
+
13
+ export class SingleBucketStoreV3 implements SingleBucketStore {
14
+ public readonly collection: mongo.Collection<BucketDataDocumentGeneric>;
15
+
16
+ constructor(
17
+ private db: VersionedPowerSyncMongoV3,
18
+ public readonly key: BucketKey
19
+ ) {
20
+ this.collection = db.bucketDataV3(
21
+ key.replicationStreamId,
22
+ key.definitionId
23
+ ) as unknown as mongo.Collection<BucketDataDocumentGeneric>;
24
+ }
25
+
26
+ docId(o: InternalOpId): BucketDataDocumentGenericId {
27
+ // `satisfies BucketDataKeyV3` checks that we use the correct type for V3 storage
28
+ // `as BucketDataDocumentGenericId` does a cast to get the interface virtual type
29
+ return {
30
+ b: this.key.bucket,
31
+ o
32
+ } satisfies BucketDataKeyV3 as BucketDataDocumentGenericId;
33
+ }
34
+
35
+ get minId(): BucketDataDocumentGenericId {
36
+ return {
37
+ b: this.key.bucket,
38
+ o: new mongo.MinKey()
39
+ } as any; // No way to properly type this
40
+ }
41
+
42
+ get maxId(): BucketDataDocumentGenericId {
43
+ return {
44
+ b: this.key.bucket,
45
+ o: new mongo.MaxKey()
46
+ } as any; // No way to properly type this
47
+ }
48
+
49
+ toPersistedDocument(source: Omit<BucketDataDoc, 'bucketKey'>): BucketDataDocumentGeneric {
50
+ return serializeBucketDataV3({ bucketKey: this.key, ...source }) as BucketDataDocumentGeneric;
51
+ }
52
+
53
+ fromPersistedDocument(doc: BucketDataDocumentGeneric): BucketDataDoc {
54
+ return loadBucketDataDocumentV3(this.key, doc as BucketDataDocumentV3);
55
+ }
56
+
57
+ fromPartialPersistedDocument<T extends keyof BucketDataProperties>(
58
+ doc: Pick<BucketDataDocumentGeneric, '_id' | T>
59
+ ): Pick<BucketDataDoc, 'bucketKey' | 'o' | T> {
60
+ const document = doc as Pick<BucketDataDocumentV3, '_id' | T>;
61
+ const { _id, ...rest } = document;
62
+ return {
63
+ bucketKey: this.key,
64
+ o: _id.o,
65
+ ...rest
66
+ } as Pick<BucketDataDoc, 'bucketKey' | 'o' | T>;
67
+ }
68
+ }
@@ -0,0 +1,226 @@
1
+ import * as lib_mongo from '@powersync/lib-service-mongodb';
2
+ import { mongo } from '@powersync/lib-service-mongodb';
3
+ import { Logger } from '@powersync/lib-services-framework';
4
+ import { storage } from '@powersync/service-core';
5
+ import { EvaluatedParameters, EvaluatedRow } from '@powersync/service-sync-rules';
6
+ import * as bson from 'bson';
7
+ import { retryOnMongoMaxTimeMSExpired } from '../../../utils/util.js';
8
+ import { BucketDefinitionMapping } from '../BucketDefinitionMapping.js';
9
+ import { cacheKey } from '../OperationBatch.js';
10
+ import { LoadedSourceRecord, SourceRecordLookupEntry, SourceRecordStore } from '../common/SourceRecordStore.js';
11
+ import { serializeParameterLookupV3 } from './MongoParameterLookupV3.js';
12
+ import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
13
+ import { CurrentDataDocumentV3, SourceTableDocumentV3 } from './models.js';
14
+
15
+ export class SourceRecordStoreV3 implements SourceRecordStore {
16
+ constructor(
17
+ private readonly db: VersionedPowerSyncMongoV3,
18
+ private readonly groupId: number,
19
+ private readonly mapping: BucketDefinitionMapping
20
+ ) {}
21
+
22
+ mapEvaluatedBuckets(evaluated: EvaluatedRow[]): LoadedSourceRecord['buckets'] {
23
+ return evaluated.map((entry) => ({
24
+ definitionId: this.mapping.bucketSourceId(entry.source),
25
+ bucket: entry.bucket,
26
+ table: entry.table,
27
+ id: entry.id
28
+ }));
29
+ }
30
+
31
+ mapParameterLookups(paramEvaluated: EvaluatedParameters[]): LoadedSourceRecord['lookups'] {
32
+ return paramEvaluated.map((entry) => ({
33
+ indexId: this.mapping.parameterLookupId(entry.lookup.source),
34
+ lookup: serializeParameterLookupV3(entry.lookup)
35
+ }));
36
+ }
37
+
38
+ private createLoadedDocument(
39
+ sourceTableId: bson.ObjectId,
40
+ id: storage.ReplicaId,
41
+ data: bson.Binary | null,
42
+ buckets: CurrentDataDocumentV3['buckets'],
43
+ lookups: CurrentDataDocumentV3['lookups']
44
+ ): LoadedSourceRecord {
45
+ return {
46
+ sourceTableId,
47
+ replicaId: id,
48
+ data,
49
+ buckets: buckets.map((bucket) => ({
50
+ definitionId: bucket.def,
51
+ bucket: bucket.bucket,
52
+ table: bucket.table,
53
+ id: bucket.id
54
+ })),
55
+ lookups: lookups.map((lookup) => ({
56
+ indexId: lookup.i,
57
+ lookup: lookup.l
58
+ })),
59
+ cacheKey: cacheKey(sourceTableId, id)
60
+ };
61
+ }
62
+
63
+ async loadSizes(session: mongo.ClientSession, entries: SourceRecordLookupEntry[]): Promise<Map<string, number>> {
64
+ const sizes = new Map<string, number>();
65
+ for (const [sourceTableId, replicaIds] of this.groupEntries(entries)) {
66
+ const filter = {
67
+ _id: { $in: replicaIds as any[] }
68
+ } as unknown as mongo.Filter<CurrentDataDocumentV3>;
69
+ const sizeCursor: mongo.AggregationCursor<CurrentDataDocumentV3 & { size: number }> = this.db
70
+ .sourceRecordsV3(this.groupId, sourceTableId)
71
+ .aggregate(
72
+ [
73
+ {
74
+ $match: filter
75
+ },
76
+ {
77
+ $project: {
78
+ _id: 1,
79
+ size: { $bsonSize: '$$ROOT' }
80
+ }
81
+ }
82
+ ],
83
+ { session }
84
+ );
85
+ for await (const doc of sizeCursor.stream()) {
86
+ sizes.set(cacheKey(sourceTableId, doc._id), doc.size);
87
+ }
88
+ }
89
+ return sizes;
90
+ }
91
+
92
+ async loadDocuments(
93
+ session: mongo.ClientSession,
94
+ entries: SourceRecordLookupEntry[],
95
+ idsOnly: boolean
96
+ ): Promise<Map<string, LoadedSourceRecord>> {
97
+ const documents = new Map<string, LoadedSourceRecord>();
98
+ const projection = idsOnly ? { _id: 1 } : undefined;
99
+ for (const [sourceTableId, replicaIds] of this.groupEntries(entries)) {
100
+ const filter = {
101
+ _id: { $in: replicaIds as any[] }
102
+ } as unknown as mongo.Filter<CurrentDataDocumentV3>;
103
+ const cursor = this.db.sourceRecordsV3(this.groupId, sourceTableId).find(filter, { session, projection });
104
+ for await (const doc of cursor.stream()) {
105
+ const loaded = this.createLoadedDocument(
106
+ sourceTableId,
107
+ doc._id,
108
+ idsOnly ? null : doc.data,
109
+ idsOnly ? [] : doc.buckets,
110
+ idsOnly ? [] : doc.lookups
111
+ );
112
+ documents.set(loaded.cacheKey, loaded);
113
+ }
114
+ }
115
+ return documents;
116
+ }
117
+
118
+ async loadTruncateBatch(
119
+ session: mongo.ClientSession,
120
+ sourceTableId: bson.ObjectId,
121
+ limit: number
122
+ ): Promise<LoadedSourceRecord[]> {
123
+ const cursor = this.db.sourceRecordsV3(this.groupId, sourceTableId).find(
124
+ {
125
+ pending_delete: { $exists: false }
126
+ },
127
+ {
128
+ projection: {
129
+ _id: 1,
130
+ buckets: 1,
131
+ lookups: 1
132
+ },
133
+ limit,
134
+ session
135
+ }
136
+ );
137
+ return (await cursor.toArray()).map((doc) =>
138
+ this.createLoadedDocument(sourceTableId, doc._id, null, doc.buckets, doc.lookups)
139
+ );
140
+ }
141
+
142
+ async postCommitCleanup(lastCheckpoint: bigint, logger: Logger): Promise<void> {
143
+ // This cleans up soft deletes in source_records collections.
144
+ // Since there may be a lot (100+) of these collections in some cases, we track which
145
+ // ones have dirty deletes in source_tables.
146
+
147
+ const dirtySourceTables = await this.db
148
+ .sourceTablesV3(this.groupId)
149
+ .find(
150
+ {
151
+ latest_pending_delete: { $exists: true }
152
+ },
153
+ {
154
+ projection: { _id: 1, latest_pending_delete: 1 }
155
+ }
156
+ )
157
+ .toArray();
158
+
159
+ let deletedCount = 0;
160
+ const sourceTableUpdates: mongo.AnyBulkWriteOperation<SourceTableDocumentV3>[] = [];
161
+ for (const sourceTable of dirtySourceTables) {
162
+ const collection = this.db.sourceRecordsV3(this.groupId, sourceTable._id);
163
+ const result = await this.deletePendingDeletes(collection, sourceTable._id, lastCheckpoint, logger);
164
+ deletedCount += result.deletedCount;
165
+
166
+ if (sourceTable.latest_pending_delete != null && sourceTable.latest_pending_delete <= lastCheckpoint) {
167
+ sourceTableUpdates.push({
168
+ updateOne: {
169
+ filter: {
170
+ _id: sourceTable._id,
171
+ // If the source table received more writes in the meantime, this will filter it out
172
+ latest_pending_delete: sourceTable.latest_pending_delete
173
+ },
174
+ update: {
175
+ $unset: {
176
+ latest_pending_delete: 1
177
+ }
178
+ }
179
+ }
180
+ });
181
+ }
182
+ }
183
+
184
+ if (sourceTableUpdates.length > 0) {
185
+ await this.db.sourceTablesV3(this.groupId).bulkWrite(sourceTableUpdates, { ordered: false });
186
+ }
187
+ if (deletedCount > 0) {
188
+ logger.info(`Cleaned up ${deletedCount} pending delete current_data records for checkpoint ${lastCheckpoint}`);
189
+ }
190
+ }
191
+
192
+ private async deletePendingDeletes(
193
+ collection: mongo.Collection<CurrentDataDocumentV3>,
194
+ sourceTableId: bson.ObjectId,
195
+ lastCheckpoint: bigint,
196
+ logger: Logger
197
+ ) {
198
+ return retryOnMongoMaxTimeMSExpired(
199
+ () =>
200
+ collection.deleteMany(
201
+ {
202
+ pending_delete: { $exists: true, $lte: lastCheckpoint }
203
+ },
204
+ {
205
+ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS
206
+ }
207
+ ),
208
+ {
209
+ retryDelayMs: lib_mongo.db.MONGO_OPERATION_TIMEOUT_MS / 5,
210
+ onRetry: (n: number) => {
211
+ logger.warn(`Cleared batch ${n} of pending deletes for source table ${sourceTableId}, continuing...`);
212
+ }
213
+ }
214
+ );
215
+ }
216
+
217
+ private groupEntries(entries: SourceRecordLookupEntry[]): Map<bson.ObjectId, storage.ReplicaId[]> {
218
+ const grouped = new Map<bson.ObjectId, storage.ReplicaId[]>();
219
+ for (const entry of entries) {
220
+ const existing = grouped.get(entry.sourceTableId) ?? [];
221
+ existing.push(entry.replicaId);
222
+ grouped.set(entry.sourceTableId, existing);
223
+ }
224
+ return grouped;
225
+ }
226
+ }