@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,317 @@
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, BucketDefinitionId } from '@powersync/service-sync-rules';
5
+ import * as bson from 'bson';
6
+ import { mongoTableId } from '../../../utils/util.js';
7
+ import {
8
+ BucketStateUpdate,
9
+ PersistedBatch,
10
+ SaveParameterDataOptions,
11
+ UpsertCurrentDataOptions
12
+ } from '../common/PersistedBatch.js';
13
+ import { SourceTableKey } from '../models.js';
14
+ import {
15
+ BucketParameterDocumentV3,
16
+ BucketStateDocumentV3,
17
+ CurrentDataDocumentV3,
18
+ serializeBucketDataV3,
19
+ SourceTableDocumentV3,
20
+ taggedBucketParameterDocumentToV3
21
+ } from './models.js';
22
+ import { serializeParameterLookupV3 } from './MongoParameterLookupV3.js';
23
+ import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
24
+
25
+ export class PersistedBatchV3 extends PersistedBatch {
26
+ declare protected readonly db: VersionedPowerSyncMongoV3;
27
+
28
+ currentData: { sourceTableId: bson.ObjectId; operation: mongo.AnyBulkWriteOperation<CurrentDataDocumentV3> }[] = [];
29
+ sourceTablePendingDeletes = new Map<string, InternalOpId>();
30
+
31
+ protected checkDefinitionId(definitionId: BucketDefinitionId | null): BucketDefinitionId {
32
+ if (definitionId == null) {
33
+ // This is required for V3 storage.
34
+ throw new ReplicationAssertionError('Expected v3 bucket when incrementalReprocessing is enabled');
35
+ }
36
+ return definitionId;
37
+ }
38
+
39
+ protected getBucketDefinitionId(bucketSource: BucketDataSource): BucketDefinitionId {
40
+ return this.mapping.bucketSourceId(bucketSource);
41
+ }
42
+
43
+ saveParameterData(data: SaveParameterDataOptions) {
44
+ const { sourceTable, sourceKey, evaluated } = data;
45
+ const remaining_lookups = new Map<string, SaveParameterDataOptions['existing_lookups'][number]>();
46
+
47
+ for (let lookup of data.existing_lookups) {
48
+ if (lookup.indexId == null) {
49
+ throw new ReplicationAssertionError('Expected v3 lookup when incrementalReprocessing is enabled');
50
+ }
51
+ remaining_lookups.set(`${lookup.indexId}.${lookup.lookup.toString('base64')}`, lookup);
52
+ }
53
+
54
+ for (let result of evaluated) {
55
+ const sourceDefinitionId = this.mapping.parameterLookupId(result.lookup.source);
56
+ const binLookup = serializeParameterLookupV3(result.lookup);
57
+ remaining_lookups.delete(`${sourceDefinitionId}.${binLookup.toString('base64')}`);
58
+
59
+ const op_id = data.op_seq.next();
60
+ this.debugLastOpId = op_id;
61
+ const values: BucketParameterDocumentV3 = {
62
+ _id: op_id,
63
+ key: {
64
+ t: mongoTableId(sourceTable.id),
65
+ k: sourceKey
66
+ } satisfies SourceTableKey,
67
+ lookup: binLookup,
68
+ bucket_parameters: result.bucketParameters
69
+ };
70
+ this.bucketParameters.push({
71
+ ...values,
72
+ index: sourceDefinitionId
73
+ });
74
+
75
+ this.currentSize += 200;
76
+ }
77
+
78
+ for (let lookup of remaining_lookups.values()) {
79
+ const op_id = data.op_seq.next();
80
+ this.debugLastOpId = op_id;
81
+ const indexId = lookup.indexId;
82
+ if (indexId == null) {
83
+ throw new ReplicationAssertionError('Expected v3 lookup when incrementalReprocessing is enabled');
84
+ }
85
+ const values: BucketParameterDocumentV3 = {
86
+ _id: op_id,
87
+ key: {
88
+ t: mongoTableId(sourceTable.id),
89
+ k: sourceKey
90
+ } satisfies SourceTableKey,
91
+ lookup: lookup.lookup,
92
+ bucket_parameters: []
93
+ };
94
+ this.bucketParameters.push({
95
+ ...values,
96
+ index: indexId
97
+ });
98
+
99
+ this.currentSize += 200;
100
+ }
101
+ }
102
+
103
+ hardDeleteCurrentData(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId) {
104
+ this.currentData.push({
105
+ sourceTableId,
106
+ operation: {
107
+ deleteOne: {
108
+ filter: { _id: replicaId }
109
+ }
110
+ }
111
+ });
112
+ this.currentSize += 50;
113
+ }
114
+
115
+ softDeleteCurrentData(
116
+ sourceTableId: bson.ObjectId,
117
+ replicaId: storage.ReplicaId,
118
+ checkpointGreaterThan: InternalOpId
119
+ ) {
120
+ this.currentData.push({
121
+ sourceTableId,
122
+ operation: {
123
+ updateOne: {
124
+ filter: { _id: replicaId },
125
+ update: {
126
+ $set: {
127
+ data: null,
128
+ buckets: [] as CurrentDataDocumentV3['buckets'],
129
+ lookups: [] as CurrentDataDocumentV3['lookups'],
130
+ pending_delete: checkpointGreaterThan
131
+ }
132
+ },
133
+ upsert: true
134
+ }
135
+ }
136
+ });
137
+ const sourceTableKey = sourceTableId.toHexString();
138
+ const existingPendingDelete = this.sourceTablePendingDeletes.get(sourceTableKey);
139
+ if (existingPendingDelete == null || checkpointGreaterThan > existingPendingDelete) {
140
+ this.sourceTablePendingDeletes.set(sourceTableKey, checkpointGreaterThan);
141
+ }
142
+
143
+ this.currentSize += 50;
144
+ }
145
+
146
+ upsertCurrentData(values: UpsertCurrentDataOptions) {
147
+ const buckets = values.buckets.map((bucket) => {
148
+ if (bucket.definitionId == null) {
149
+ throw new ReplicationAssertionError('Expected v3 bucket when incrementalReprocessing is enabled');
150
+ }
151
+ return {
152
+ def: bucket.definitionId,
153
+ bucket: bucket.bucket,
154
+ table: bucket.table,
155
+ id: bucket.id
156
+ };
157
+ });
158
+ const lookups = values.lookups.map((lookup) => {
159
+ if (lookup.indexId == null) {
160
+ throw new ReplicationAssertionError('Expected v3 lookup when incrementalReprocessing is enabled');
161
+ }
162
+ return {
163
+ i: lookup.indexId,
164
+ l: lookup.lookup
165
+ };
166
+ });
167
+
168
+ this.currentData.push({
169
+ sourceTableId: values.sourceTableId,
170
+ operation: {
171
+ updateOne: {
172
+ filter: { _id: values.replicaId },
173
+ update: {
174
+ $set: {
175
+ data: values.data,
176
+ buckets,
177
+ lookups
178
+ },
179
+ $unset: { pending_delete: 1 }
180
+ },
181
+ upsert: true
182
+ }
183
+ }
184
+ });
185
+ this.currentSize += (values.data?.length() ?? 0) + 100;
186
+ }
187
+
188
+ protected get currentDataCount() {
189
+ return this.currentData.length;
190
+ }
191
+
192
+ protected async flushBucketData(session: mongo.ClientSession) {
193
+ const operationsByDefinition = new Map<BucketDefinitionId, typeof this.bucketData>();
194
+ for (const document of this.bucketData) {
195
+ const existing = operationsByDefinition.get(document.bucketKey.definitionId) ?? [];
196
+ existing.push(document);
197
+ operationsByDefinition.set(document.bucketKey.definitionId, existing);
198
+ }
199
+
200
+ for (const [definitionId, documents] of operationsByDefinition.entries()) {
201
+ await this.db.bucketDataV3(this.group_id, definitionId).bulkWrite(
202
+ documents.map((document) => ({
203
+ insertOne: {
204
+ document: serializeBucketDataV3(document)
205
+ }
206
+ })),
207
+ {
208
+ session,
209
+ ordered: false
210
+ }
211
+ );
212
+ }
213
+ }
214
+
215
+ protected async flushBucketParameters(session: mongo.ClientSession) {
216
+ const operationsByIndex = new Map<string, typeof this.bucketParameters>();
217
+ for (const document of this.bucketParameters) {
218
+ const existing = operationsByIndex.get(document.index) ?? [];
219
+ existing.push(document);
220
+ operationsByIndex.set(document.index, existing);
221
+ }
222
+
223
+ for (const [indexId, documents] of operationsByIndex.entries()) {
224
+ await this.db.parameterIndexV3(this.group_id, indexId).bulkWrite(
225
+ documents.map((document) => ({
226
+ insertOne: {
227
+ document: taggedBucketParameterDocumentToV3(document)
228
+ }
229
+ })),
230
+ {
231
+ session,
232
+ ordered: false
233
+ }
234
+ );
235
+ }
236
+ }
237
+
238
+ protected async flushCurrentData(session: mongo.ClientSession) {
239
+ const operationsBySourceTable = new Map<string, typeof this.currentData>();
240
+ for (const operation of this.currentData) {
241
+ const sourceTableId = operation.sourceTableId.toHexString();
242
+ const existing = operationsBySourceTable.get(sourceTableId) ?? [];
243
+ existing.push(operation);
244
+ operationsBySourceTable.set(sourceTableId, existing);
245
+ }
246
+
247
+ const sourceTableUpdates: mongo.AnyBulkWriteOperation<SourceTableDocumentV3>[] = [
248
+ ...this.sourceTablePendingDeletes.entries()
249
+ ].map(([key, value]) => {
250
+ return {
251
+ updateOne: {
252
+ filter: { _id: new bson.ObjectId(key) },
253
+ update: {
254
+ $max: {
255
+ latest_pending_delete: value
256
+ }
257
+ }
258
+ }
259
+ };
260
+ });
261
+
262
+ if (sourceTableUpdates.length > 0) {
263
+ await this.db.sourceTablesV3(this.group_id).bulkWrite(sourceTableUpdates, { session, ordered: false });
264
+ }
265
+
266
+ for (const operations of operationsBySourceTable.values()) {
267
+ const sourceTableId = operations[0]!.sourceTableId;
268
+ await this.db.sourceRecordsV3(this.group_id, sourceTableId).bulkWrite(
269
+ operations.map((entry) => entry.operation),
270
+ {
271
+ session,
272
+ ordered: true
273
+ }
274
+ );
275
+ }
276
+ }
277
+
278
+ protected async flushBucketStates(session: mongo.ClientSession) {
279
+ await this.db.bucketStateV3(this.group_id).bulkWrite(this.getBucketStateUpdates(), {
280
+ session,
281
+ ordered: false
282
+ });
283
+ }
284
+
285
+ protected resetCurrentData() {
286
+ this.currentData = [];
287
+ this.sourceTablePendingDeletes.clear();
288
+ }
289
+
290
+ private getBucketStateUpdates(): mongo.AnyBulkWriteOperation<BucketStateDocumentV3>[] {
291
+ return Array.from(this.bucketStates.values()).map((state: BucketStateUpdate) => {
292
+ if (state.definitionId == null) {
293
+ throw new ReplicationAssertionError('Expected bucket definition id when incrementalReprocessing is enabled');
294
+ }
295
+ return {
296
+ updateOne: {
297
+ filter: {
298
+ _id: {
299
+ d: state.definitionId,
300
+ b: state.bucket
301
+ }
302
+ },
303
+ update: {
304
+ $set: {
305
+ last_op: state.lastOp
306
+ },
307
+ $inc: {
308
+ 'estimate_since_compact.count': state.incrementCount,
309
+ 'estimate_since_compact.bytes': state.incrementBytes
310
+ }
311
+ },
312
+ upsert: true
313
+ }
314
+ } satisfies mongo.AnyBulkWriteOperation<BucketStateDocumentV3>;
315
+ });
316
+ }
317
+ }
@@ -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
+ }