@powersync/service-module-mongodb-storage 0.17.0 → 0.18.1

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 (185) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +34 -16
  3. package/dist/storage/MongoBucketStorage.js +219 -70
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +42 -23
  6. package/dist/storage/implementation/MongoBucketBatch.js +61 -44
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoChecksums.d.ts +10 -9
  9. package/dist/storage/implementation/MongoChecksums.js +3 -111
  10. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  11. package/dist/storage/implementation/MongoCompactor.d.ts +2 -0
  12. package/dist/storage/implementation/MongoCompactor.js +18 -9
  13. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  14. package/dist/storage/implementation/MongoHydrationState.d.ts +11 -0
  15. package/dist/storage/implementation/MongoHydrationState.js +49 -0
  16. package/dist/storage/implementation/MongoHydrationState.js.map +1 -0
  17. package/dist/storage/implementation/MongoParameterCompactor.d.ts +6 -5
  18. package/dist/storage/implementation/MongoParameterCompactor.js +24 -1
  19. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  20. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +13 -0
  21. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +64 -0
  22. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -0
  23. package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +36 -0
  24. package/dist/storage/implementation/MongoPersistedReplicationStream.js +90 -0
  25. package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -0
  26. package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +25 -0
  27. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +123 -0
  28. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
  29. package/dist/storage/implementation/MongoStorageProvider.js +5 -2
  30. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  31. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +41 -23
  32. package/dist/storage/implementation/MongoSyncBucketStorage.js +51 -43
  33. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  34. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +1 -1
  35. package/dist/storage/implementation/MongoSyncRulesLock.js +3 -3
  36. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  37. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +6 -0
  38. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js +2 -0
  39. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js.map +1 -0
  40. package/dist/storage/implementation/common/PersistedBatch.d.ts +1 -2
  41. package/dist/storage/implementation/common/PersistedBatch.js +5 -0
  42. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  43. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +1 -1
  44. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  45. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
  46. package/dist/storage/implementation/createMongoSyncBucketStorage.js +4 -4
  47. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
  48. package/dist/storage/implementation/db.js +9 -14
  49. package/dist/storage/implementation/db.js.map +1 -1
  50. package/dist/storage/implementation/models.d.ts +4 -13
  51. package/dist/storage/implementation/models.js.map +1 -1
  52. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +9 -1
  53. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +42 -26
  54. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  55. package/dist/storage/implementation/v1/MongoChecksumsV1.js +114 -3
  56. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
  57. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +15 -17
  58. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +47 -45
  59. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  60. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +2 -1
  61. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -0
  62. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
  63. package/dist/storage/implementation/v1/models.d.ts +10 -1
  64. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -4
  65. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +286 -259
  66. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  67. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +27 -7
  68. package/dist/storage/implementation/v3/MongoChecksumsV3.js +202 -18
  69. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  70. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +19 -2
  71. package/dist/storage/implementation/v3/MongoCompactorV3.js +511 -22
  72. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  73. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +62 -0
  74. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +350 -0
  75. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
  76. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +40 -28
  77. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +232 -116
  78. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  79. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +1 -1
  80. package/dist/storage/implementation/v3/PersistedBatchV3.js +43 -20
  81. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  82. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +52 -16
  83. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -1
  84. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +3 -3
  85. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +13 -11
  86. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
  87. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +13 -12
  88. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +29 -27
  89. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  90. package/dist/storage/implementation/v3/bucket-format.d.ts +4 -0
  91. package/dist/storage/implementation/v3/bucket-format.js +58 -0
  92. package/dist/storage/implementation/v3/bucket-format.js.map +1 -0
  93. package/dist/storage/implementation/v3/chunking.d.ts +10 -0
  94. package/dist/storage/implementation/v3/chunking.js +28 -0
  95. package/dist/storage/implementation/v3/chunking.js.map +1 -0
  96. package/dist/storage/implementation/v3/models.d.ts +64 -21
  97. package/dist/storage/implementation/v3/models.js +11 -28
  98. package/dist/storage/implementation/v3/models.js.map +1 -1
  99. package/dist/storage/implementation/v3/source-table-utils.d.ts +55 -0
  100. package/dist/storage/implementation/v3/source-table-utils.js +214 -0
  101. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
  102. package/dist/storage/storage-index.d.ts +4 -4
  103. package/dist/storage/storage-index.js +4 -4
  104. package/dist/storage/storage-index.js.map +1 -1
  105. package/dist/types/types.d.ts +2 -2
  106. package/dist/utils/test-utils.d.ts +1 -2
  107. package/dist/utils/test-utils.js +5 -1
  108. package/dist/utils/test-utils.js.map +1 -1
  109. package/dist/utils/util.d.ts +1 -1
  110. package/dist/utils/util.js +3 -2
  111. package/dist/utils/util.js.map +1 -1
  112. package/package.json +9 -19
  113. package/src/storage/MongoBucketStorage.ts +334 -87
  114. package/src/storage/implementation/MongoBucketBatch.ts +86 -58
  115. package/src/storage/implementation/MongoChecksums.ts +6 -138
  116. package/src/storage/implementation/MongoCompactor.ts +19 -9
  117. package/src/storage/implementation/MongoHydrationState.ts +61 -0
  118. package/src/storage/implementation/MongoParameterCompactor.ts +24 -5
  119. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +88 -0
  120. package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
  121. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +154 -0
  122. package/src/storage/implementation/MongoStorageProvider.ts +5 -2
  123. package/src/storage/implementation/MongoSyncBucketStorage.ts +70 -56
  124. package/src/storage/implementation/MongoSyncRulesLock.ts +7 -4
  125. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +7 -0
  126. package/src/storage/implementation/common/PersistedBatch.ts +7 -3
  127. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
  128. package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
  129. package/src/storage/implementation/db.ts +11 -19
  130. package/src/storage/implementation/models.ts +4 -14
  131. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +55 -34
  132. package/src/storage/implementation/v1/MongoChecksumsV1.ts +137 -4
  133. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +70 -64
  134. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +11 -1
  135. package/src/storage/implementation/v1/models.ts +11 -1
  136. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +358 -314
  137. package/src/storage/implementation/v3/MongoChecksumsV3.ts +262 -26
  138. package/src/storage/implementation/v3/MongoCompactorV3.ts +646 -29
  139. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +480 -0
  140. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +315 -157
  141. package/src/storage/implementation/v3/PersistedBatchV3.ts +62 -36
  142. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +59 -19
  143. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
  144. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +41 -36
  145. package/src/storage/implementation/v3/bucket-format.ts +70 -0
  146. package/src/storage/implementation/v3/chunking.ts +38 -0
  147. package/src/storage/implementation/v3/models.ts +93 -55
  148. package/src/storage/implementation/v3/source-table-utils.ts +392 -0
  149. package/src/storage/storage-index.ts +12 -4
  150. package/src/utils/test-utils.ts +6 -3
  151. package/src/utils/util.ts +3 -2
  152. package/test/src/__snapshots__/storage.test.ts.snap +0 -75
  153. package/test/src/__snapshots__/storage_sync.test.ts.snap +48 -48
  154. package/test/src/cleanup-stopped-sync-configs.test.ts +648 -0
  155. package/test/src/storage.test.ts +11 -11
  156. package/test/src/storage_compacting.test.ts +1956 -6
  157. package/test/src/storage_sync.test.ts +1077 -68
  158. package/test/src/storeCurrentData.test.ts +17 -7
  159. package/test/src/util.ts +2 -1
  160. package/tsconfig.json +1 -4
  161. package/tsconfig.tsbuildinfo +1 -1
  162. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +0 -15
  163. package/dist/storage/implementation/BucketDefinitionMapping.js +0 -58
  164. package/dist/storage/implementation/BucketDefinitionMapping.js.map +0 -1
  165. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -14
  166. package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -67
  167. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
  168. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -29
  169. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -76
  170. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
  171. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +0 -13
  172. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +0 -2
  173. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +0 -1
  174. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +0 -9
  175. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +0 -18
  176. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +0 -1
  177. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +0 -4
  178. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +0 -9
  179. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +0 -1
  180. package/src/storage/implementation/BucketDefinitionMapping.ts +0 -75
  181. package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -82
  182. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -101
  183. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +0 -15
  184. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +0 -24
  185. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -11
@@ -4,66 +4,76 @@ import { InternalOpId, storage } from '@powersync/service-core';
4
4
  import { BucketDataSource, BucketDefinitionId } from '@powersync/service-sync-rules';
5
5
  import * as bson from 'bson';
6
6
  import { mongoTableId } from '../../../utils/util.js';
7
+ import { BucketDataDoc } from '../common/BucketDataDoc.js';
7
8
  import {
8
9
  BucketStateUpdate,
9
10
  PersistedBatch,
10
11
  SaveParameterDataOptions,
11
12
  UpsertCurrentDataOptions
12
13
  } from '../common/PersistedBatch.js';
13
- import { SourceTableKey } from '../models.js';
14
+ import { SourceRecordLookupState } from '../common/SourceRecordStore.js';
15
+ import { serializeBucketData } from './bucket-format.js';
16
+ import { chunkBucketData } from './chunking.js';
14
17
  import {
15
- BucketParameterDocumentV3,
18
+ BucketDataDocumentV3,
16
19
  BucketStateDocumentV3,
17
20
  CurrentDataDocumentV3,
18
- serializeBucketDataV3,
21
+ serializeParameterLookup,
19
22
  SourceTableDocumentV3,
20
- taggedBucketParameterDocumentToV3
23
+ taggedBucketParameterDocumentToTagged
21
24
  } from './models.js';
22
- import { serializeParameterLookupV3 } from './MongoParameterLookupV3.js';
23
25
  import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
24
26
 
25
27
  export class PersistedBatchV3 extends PersistedBatch {
26
- declare protected readonly db: VersionedPowerSyncMongoV3;
27
-
28
28
  currentData: { sourceTableId: bson.ObjectId; operation: mongo.AnyBulkWriteOperation<CurrentDataDocumentV3> }[] = [];
29
29
  sourceTablePendingDeletes = new Map<string, InternalOpId>();
30
30
 
31
- protected checkDefinitionId(definitionId: BucketDefinitionId | null): BucketDefinitionId {
31
+ declare protected readonly db: VersionedPowerSyncMongoV3;
32
+
33
+ // Abstract override from PersistedBatch (V3-specific error message)
34
+
35
+ protected override checkDefinitionId(definitionId: BucketDefinitionId | null): BucketDefinitionId {
32
36
  if (definitionId == null) {
33
- // This is required for V3 storage.
34
37
  throw new ReplicationAssertionError('Expected v3 bucket when incrementalReprocessing is enabled');
35
38
  }
36
39
  return definitionId;
37
40
  }
38
41
 
42
+ // Concrete implementations from PersistedBatchShared
43
+
39
44
  protected getBucketDefinitionId(bucketSource: BucketDataSource): BucketDefinitionId {
40
45
  return this.mapping.bucketSourceId(bucketSource);
41
46
  }
42
47
 
43
48
  saveParameterData(data: SaveParameterDataOptions) {
44
49
  const { sourceTable, sourceKey, evaluated } = data;
45
- const remaining_lookups = new Map<string, SaveParameterDataOptions['existing_lookups'][number]>();
50
+ const remaining_lookups = new Map<string, SourceRecordLookupState>();
46
51
 
47
52
  for (let lookup of data.existing_lookups) {
48
53
  if (lookup.indexId == null) {
49
- throw new ReplicationAssertionError('Expected v3 lookup when incrementalReprocessing is enabled');
54
+ throw new ReplicationAssertionError('Expected lookup when incrementalReprocessing is enabled');
55
+ }
56
+ if (sourceTable.parameterLookupSourceIds != null && !sourceTable.parameterLookupSourceIds.has(lookup.indexId)) {
57
+ // The parameter index is not active anymore.
58
+ // We don't cleanup these references upfront, but do need to ignore them after the definition is removed.
59
+ continue;
50
60
  }
51
61
  remaining_lookups.set(`${lookup.indexId}.${lookup.lookup.toString('base64')}`, lookup);
52
62
  }
53
63
 
54
64
  for (let result of evaluated) {
55
65
  const sourceDefinitionId = this.mapping.parameterLookupId(result.lookup.source);
56
- const binLookup = serializeParameterLookupV3(result.lookup);
66
+ const binLookup = serializeParameterLookup(result.lookup);
57
67
  remaining_lookups.delete(`${sourceDefinitionId}.${binLookup.toString('base64')}`);
58
68
 
59
69
  const op_id = data.op_seq.next();
60
70
  this.debugLastOpId = op_id;
61
- const values: BucketParameterDocumentV3 = {
71
+ const values = {
62
72
  _id: op_id,
63
73
  key: {
64
74
  t: mongoTableId(sourceTable.id),
65
75
  k: sourceKey
66
- } satisfies SourceTableKey,
76
+ },
67
77
  lookup: binLookup,
68
78
  bucket_parameters: result.bucketParameters
69
79
  };
@@ -80,14 +90,14 @@ export class PersistedBatchV3 extends PersistedBatch {
80
90
  this.debugLastOpId = op_id;
81
91
  const indexId = lookup.indexId;
82
92
  if (indexId == null) {
83
- throw new ReplicationAssertionError('Expected v3 lookup when incrementalReprocessing is enabled');
93
+ throw new ReplicationAssertionError('Expected lookup when incrementalReprocessing is enabled');
84
94
  }
85
- const values: BucketParameterDocumentV3 = {
95
+ const values = {
86
96
  _id: op_id,
87
97
  key: {
88
98
  t: mongoTableId(sourceTable.id),
89
99
  k: sourceKey
90
- } satisfies SourceTableKey,
100
+ },
91
101
  lookup: lookup.lookup,
92
102
  bucket_parameters: []
93
103
  };
@@ -125,8 +135,8 @@ export class PersistedBatchV3 extends PersistedBatch {
125
135
  update: {
126
136
  $set: {
127
137
  data: null,
128
- buckets: [] as CurrentDataDocumentV3['buckets'],
129
- lookups: [] as CurrentDataDocumentV3['lookups'],
138
+ buckets: [],
139
+ lookups: [],
130
140
  pending_delete: checkpointGreaterThan
131
141
  }
132
142
  },
@@ -146,7 +156,7 @@ export class PersistedBatchV3 extends PersistedBatch {
146
156
  upsertCurrentData(values: UpsertCurrentDataOptions) {
147
157
  const buckets = values.buckets.map((bucket) => {
148
158
  if (bucket.definitionId == null) {
149
- throw new ReplicationAssertionError('Expected v3 bucket when incrementalReprocessing is enabled');
159
+ throw new ReplicationAssertionError('Expected bucket when incrementalReprocessing is enabled');
150
160
  }
151
161
  return {
152
162
  def: bucket.definitionId,
@@ -157,7 +167,7 @@ export class PersistedBatchV3 extends PersistedBatch {
157
167
  });
158
168
  const lookups = values.lookups.map((lookup) => {
159
169
  if (lookup.indexId == null) {
160
- throw new ReplicationAssertionError('Expected v3 lookup when incrementalReprocessing is enabled');
170
+ throw new ReplicationAssertionError('Expected lookup when incrementalReprocessing is enabled');
161
171
  }
162
172
  return {
163
173
  i: lookup.indexId,
@@ -189,8 +199,10 @@ export class PersistedBatchV3 extends PersistedBatch {
189
199
  return this.currentData.length;
190
200
  }
191
201
 
202
+ // Flush methods
203
+
192
204
  protected async flushBucketData(session: mongo.ClientSession) {
193
- const operationsByDefinition = new Map<BucketDefinitionId, typeof this.bucketData>();
205
+ const operationsByDefinition = new Map<BucketDefinitionId, BucketDataDoc[]>();
194
206
  for (const document of this.bucketData) {
195
207
  const existing = operationsByDefinition.get(document.bucketKey.definitionId) ?? [];
196
208
  existing.push(document);
@@ -198,17 +210,31 @@ export class PersistedBatchV3 extends PersistedBatch {
198
210
  }
199
211
 
200
212
  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
- {
213
+ const operationsByBucket = new Map<string, BucketDataDoc[]>();
214
+ for (const document of documents) {
215
+ const existing = operationsByBucket.get(document.bucketKey.bucket) ?? [];
216
+ existing.push(document);
217
+ operationsByBucket.set(document.bucketKey.bucket, existing);
218
+ }
219
+
220
+ const inserts: mongo.AnyBulkWriteOperation<BucketDataDocumentV3>[] = [];
221
+ for (const [bucket, ops] of operationsByBucket.entries()) {
222
+ const chunks = chunkBucketData(ops);
223
+ for (const chunk of chunks) {
224
+ inserts.push({
225
+ insertOne: {
226
+ document: serializeBucketData(bucket, chunk)
227
+ }
228
+ });
229
+ }
230
+ }
231
+
232
+ if (inserts.length > 0) {
233
+ await this.db.bucketData(this.group_id, definitionId).bulkWrite(inserts, {
208
234
  session,
209
235
  ordered: false
210
- }
211
- );
236
+ });
237
+ }
212
238
  }
213
239
  }
214
240
 
@@ -221,10 +247,10 @@ export class PersistedBatchV3 extends PersistedBatch {
221
247
  }
222
248
 
223
249
  for (const [indexId, documents] of operationsByIndex.entries()) {
224
- await this.db.parameterIndexV3(this.group_id, indexId).bulkWrite(
250
+ await this.db.parameterIndex(this.group_id, indexId).bulkWrite(
225
251
  documents.map((document) => ({
226
252
  insertOne: {
227
- document: taggedBucketParameterDocumentToV3(document)
253
+ document: taggedBucketParameterDocumentToTagged(document)
228
254
  }
229
255
  })),
230
256
  {
@@ -260,12 +286,12 @@ export class PersistedBatchV3 extends PersistedBatch {
260
286
  });
261
287
 
262
288
  if (sourceTableUpdates.length > 0) {
263
- await this.db.sourceTablesV3(this.group_id).bulkWrite(sourceTableUpdates, { session, ordered: false });
289
+ await this.db.sourceTables(this.group_id).bulkWrite(sourceTableUpdates, { session, ordered: false });
264
290
  }
265
291
 
266
292
  for (const operations of operationsBySourceTable.values()) {
267
293
  const sourceTableId = operations[0]!.sourceTableId;
268
- await this.db.sourceRecordsV3(this.group_id, sourceTableId).bulkWrite(
294
+ await this.db.sourceRecords(this.group_id, sourceTableId).bulkWrite(
269
295
  operations.map((entry) => entry.operation),
270
296
  {
271
297
  session,
@@ -276,7 +302,7 @@ export class PersistedBatchV3 extends PersistedBatch {
276
302
  }
277
303
 
278
304
  protected async flushBucketStates(session: mongo.ClientSession) {
279
- await this.db.bucketStateV3(this.group_id).bulkWrite(this.getBucketStateUpdates(), {
305
+ await this.db.bucketState(this.group_id).bulkWrite(this.getBucketStateUpdates(), {
280
306
  session,
281
307
  ordered: false
282
308
  });
@@ -6,9 +6,38 @@ import {
6
6
  BucketDataDocumentGenericId,
7
7
  SingleBucketStore
8
8
  } from '../common/SingleBucketStore.js';
9
- import { BucketDataProperties } from '../models.js';
9
+ import { BucketDataKey, BucketDataProperties } from '../models.js';
10
+ import { loadBucketDataDocument, serializeBucketData } from './bucket-format.js';
11
+ import { BucketDataDocumentV3 } from './models.js';
10
12
  import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
11
- import { BucketDataDocumentV3, BucketDataKeyV3, loadBucketDataDocumentV3, serializeBucketDataV3 } from './models.js';
13
+
14
+ function extractPartialDocumentFields(doc: unknown): { _id: { o: bigint }; [key: string]: unknown } {
15
+ if (typeof doc !== 'object' || doc === null) {
16
+ throw new Error('Invalid partial document: expected object');
17
+ }
18
+ const d = doc as Record<string, unknown>;
19
+ const id = d._id;
20
+ if (typeof id !== 'object' || id === null || !('o' in id)) {
21
+ throw new Error('Invalid partial document: missing _id.o');
22
+ }
23
+ return d as { _id: { o: bigint }; [key: string]: unknown };
24
+ }
25
+
26
+ // MongoDB's MinKey/MaxKey are special sentinel values that don't match the bigint type
27
+ // for _id.o in BucketDataDocumentGenericId, so we need an explicit cast.
28
+ function minKeyForBucket(bucket: string): BucketDataDocumentGenericId {
29
+ return {
30
+ b: bucket,
31
+ o: new mongo.MinKey()
32
+ } as unknown as BucketDataDocumentGenericId;
33
+ }
34
+
35
+ function maxKeyForBucket(bucket: string): BucketDataDocumentGenericId {
36
+ return {
37
+ b: bucket,
38
+ o: new mongo.MaxKey()
39
+ } as unknown as BucketDataDocumentGenericId;
40
+ }
12
41
 
13
42
  export class SingleBucketStoreV3 implements SingleBucketStore {
14
43
  public readonly collection: mongo.Collection<BucketDataDocumentGeneric>;
@@ -17,52 +46,63 @@ export class SingleBucketStoreV3 implements SingleBucketStore {
17
46
  private db: VersionedPowerSyncMongoV3,
18
47
  public readonly key: BucketKey
19
48
  ) {
20
- this.collection = db.bucketDataV3(
49
+ // Cast from the version-specific collection type to the generic interface
50
+ // used across storage versions.
51
+ this.collection = db.bucketData(
21
52
  key.replicationStreamId,
22
53
  key.definitionId
23
54
  ) as unknown as mongo.Collection<BucketDataDocumentGeneric>;
24
55
  }
25
56
 
26
57
  docId(o: InternalOpId): BucketDataDocumentGenericId {
27
- // `satisfies BucketDataKeyV3` checks that we use the correct type for V3 storage
58
+ // `satisfies BucketDataKey` checks that we use the correct type for V3 storage
28
59
  // `as BucketDataDocumentGenericId` does a cast to get the interface virtual type
29
60
  return {
30
61
  b: this.key.bucket,
31
62
  o
32
- } satisfies BucketDataKeyV3 as BucketDataDocumentGenericId;
63
+ } satisfies BucketDataKey as BucketDataDocumentGenericId;
33
64
  }
34
65
 
35
66
  get minId(): BucketDataDocumentGenericId {
36
- return {
37
- b: this.key.bucket,
38
- o: new mongo.MinKey()
39
- } as any; // No way to properly type this
67
+ return minKeyForBucket(this.key.bucket);
40
68
  }
41
69
 
42
70
  get maxId(): BucketDataDocumentGenericId {
43
- return {
44
- b: this.key.bucket,
45
- o: new mongo.MaxKey()
46
- } as any; // No way to properly type this
71
+ return maxKeyForBucket(this.key.bucket);
47
72
  }
48
73
 
49
74
  toPersistedDocument(source: Omit<BucketDataDoc, 'bucketKey'>): BucketDataDocumentGeneric {
50
- return serializeBucketDataV3({ bucketKey: this.key, ...source }) as BucketDataDocumentGeneric;
75
+ return serializeBucketData(this.key.bucket, [
76
+ { bucketKey: this.key, ...source }
77
+ ]) as unknown as BucketDataDocumentGeneric;
51
78
  }
52
79
 
53
80
  fromPersistedDocument(doc: BucketDataDocumentGeneric): BucketDataDoc {
54
- return loadBucketDataDocumentV3(this.key, doc as BucketDataDocumentV3);
81
+ const generator = loadBucketDataDocument(this.key, doc as unknown as BucketDataDocumentV3);
82
+ const first = generator.next();
83
+ if (first.done) {
84
+ throw new Error('Empty ops array in BucketDataDocumentV3');
85
+ }
86
+ return first.value;
55
87
  }
56
88
 
57
89
  fromPartialPersistedDocument<T extends keyof BucketDataProperties>(
58
90
  doc: Pick<BucketDataDocumentGeneric, '_id' | T>
59
91
  ): Pick<BucketDataDoc, 'bucketKey' | 'o' | T> {
60
- const document = doc as Pick<BucketDataDocumentV3, '_id' | T>;
61
- const { _id, ...rest } = document;
92
+ const document = doc as unknown as Pick<BucketDataDocumentV3, '_id' | 'ops'>;
93
+ const op = document.ops?.[0];
94
+ if (op == null) {
95
+ const fields = extractPartialDocumentFields(doc);
96
+ const { _id, ...rest } = fields;
97
+ return {
98
+ bucketKey: this.key,
99
+ o: _id.o,
100
+ ...rest
101
+ } as Pick<BucketDataDoc, 'bucketKey' | 'o' | T>;
102
+ }
62
103
  return {
63
104
  bucketKey: this.key,
64
- o: _id.o,
65
- ...rest
105
+ ...op
66
106
  } as Pick<BucketDataDoc, 'bucketKey' | 'o' | T>;
67
107
  }
68
108
  }
@@ -1,21 +1,19 @@
1
1
  import * as lib_mongo from '@powersync/lib-service-mongodb';
2
2
  import { mongo } from '@powersync/lib-service-mongodb';
3
3
  import { Logger } from '@powersync/lib-services-framework';
4
- import { storage } from '@powersync/service-core';
4
+ import { BucketDefinitionMapping, storage } from '@powersync/service-core';
5
5
  import { EvaluatedParameters, EvaluatedRow } from '@powersync/service-sync-rules';
6
6
  import * as bson from 'bson';
7
7
  import { retryOnMongoMaxTimeMSExpired } from '../../../utils/util.js';
8
- import { BucketDefinitionMapping } from '../BucketDefinitionMapping.js';
9
8
  import { cacheKey } from '../OperationBatch.js';
10
9
  import { LoadedSourceRecord, SourceRecordLookupEntry, SourceRecordStore } from '../common/SourceRecordStore.js';
11
- import { serializeParameterLookupV3 } from './MongoParameterLookupV3.js';
12
10
  import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
13
- import { CurrentDataDocumentV3, SourceTableDocumentV3 } from './models.js';
11
+ import { CurrentDataDocumentV3, serializeParameterLookup, SourceTableDocumentV3 } from './models.js';
14
12
 
15
13
  export class SourceRecordStoreV3 implements SourceRecordStore {
16
14
  constructor(
17
15
  private readonly db: VersionedPowerSyncMongoV3,
18
- private readonly groupId: number,
16
+ private readonly replicationStreamId: number,
19
17
  private readonly mapping: BucketDefinitionMapping
20
18
  ) {}
21
19
 
@@ -31,7 +29,7 @@ export class SourceRecordStoreV3 implements SourceRecordStore {
31
29
  mapParameterLookups(paramEvaluated: EvaluatedParameters[]): LoadedSourceRecord['lookups'] {
32
30
  return paramEvaluated.map((entry) => ({
33
31
  indexId: this.mapping.parameterLookupId(entry.lookup.source),
34
- lookup: serializeParameterLookupV3(entry.lookup)
32
+ lookup: serializeParameterLookup(entry.lookup)
35
33
  }));
36
34
  }
37
35
 
@@ -67,7 +65,7 @@ export class SourceRecordStoreV3 implements SourceRecordStore {
67
65
  _id: { $in: replicaIds as any[] }
68
66
  } as unknown as mongo.Filter<CurrentDataDocumentV3>;
69
67
  const sizeCursor: mongo.AggregationCursor<CurrentDataDocumentV3 & { size: number }> = this.db
70
- .sourceRecordsV3(this.groupId, sourceTableId)
68
+ .sourceRecords(this.replicationStreamId, sourceTableId)
71
69
  .aggregate(
72
70
  [
73
71
  {
@@ -100,7 +98,9 @@ export class SourceRecordStoreV3 implements SourceRecordStore {
100
98
  const filter = {
101
99
  _id: { $in: replicaIds as any[] }
102
100
  } as unknown as mongo.Filter<CurrentDataDocumentV3>;
103
- const cursor = this.db.sourceRecordsV3(this.groupId, sourceTableId).find(filter, { session, projection });
101
+ const cursor = this.db
102
+ .sourceRecords(this.replicationStreamId, sourceTableId)
103
+ .find(filter, { session, projection });
104
104
  for await (const doc of cursor.stream()) {
105
105
  const loaded = this.createLoadedDocument(
106
106
  sourceTableId,
@@ -120,7 +120,7 @@ export class SourceRecordStoreV3 implements SourceRecordStore {
120
120
  sourceTableId: bson.ObjectId,
121
121
  limit: number
122
122
  ): Promise<LoadedSourceRecord[]> {
123
- const cursor = this.db.sourceRecordsV3(this.groupId, sourceTableId).find(
123
+ const cursor = this.db.sourceRecords(this.replicationStreamId, sourceTableId).find(
124
124
  {
125
125
  pending_delete: { $exists: false }
126
126
  },
@@ -145,7 +145,7 @@ export class SourceRecordStoreV3 implements SourceRecordStore {
145
145
  // ones have dirty deletes in source_tables.
146
146
 
147
147
  const dirtySourceTables = await this.db
148
- .sourceTablesV3(this.groupId)
148
+ .sourceTables(this.replicationStreamId)
149
149
  .find(
150
150
  {
151
151
  latest_pending_delete: { $exists: true }
@@ -159,7 +159,7 @@ export class SourceRecordStoreV3 implements SourceRecordStore {
159
159
  let deletedCount = 0;
160
160
  const sourceTableUpdates: mongo.AnyBulkWriteOperation<SourceTableDocumentV3>[] = [];
161
161
  for (const sourceTable of dirtySourceTables) {
162
- const collection = this.db.sourceRecordsV3(this.groupId, sourceTable._id);
162
+ const collection = this.db.sourceRecords(this.replicationStreamId, sourceTable._id);
163
163
  const result = await this.deletePendingDeletes(collection, sourceTable._id, lastCheckpoint, logger);
164
164
  deletedCount += result.deletedCount;
165
165
 
@@ -182,7 +182,7 @@ export class SourceRecordStoreV3 implements SourceRecordStore {
182
182
  }
183
183
 
184
184
  if (sourceTableUpdates.length > 0) {
185
- await this.db.sourceTablesV3(this.groupId).bulkWrite(sourceTableUpdates, { ordered: false });
185
+ await this.db.sourceTables(this.replicationStreamId).bulkWrite(sourceTableUpdates, { ordered: false });
186
186
  }
187
187
  if (deletedCount > 0) {
188
188
  logger.info(`Cleaned up ${deletedCount} pending delete current_data records for checkpoint ${lastCheckpoint}`);
@@ -12,53 +12,80 @@ import {
12
12
  } from './models.js';
13
13
 
14
14
  export class VersionedPowerSyncMongoV3 extends BaseVersionedPowerSyncMongo {
15
+ constructor(
16
+ upstream: ConstructorParameters<typeof BaseVersionedPowerSyncMongo>[0],
17
+ storageConfig: ConstructorParameters<typeof BaseVersionedPowerSyncMongo>[1]
18
+ ) {
19
+ super(upstream, storageConfig);
20
+ }
21
+
15
22
  get syncConfigDefinitions() {
16
23
  return this.db.collection<SyncConfigDefinition>('sync_config');
17
24
  }
18
25
 
19
- sourceRecordsV3(replicationStreamId: number, sourceTableId: mongo.ObjectId): mongo.Collection<CurrentDataDocumentV3> {
26
+ sourceRecords(replicationStreamId: number, sourceTableId: mongo.ObjectId): mongo.Collection<CurrentDataDocumentV3> {
20
27
  const collectionName = this.sourceRecordsCollectionName(replicationStreamId, sourceTableId);
21
28
  return this.db.collection<CurrentDataDocumentV3>(collectionName);
22
29
  }
23
30
 
24
- async listSourceRecordCollectionsV3(replicationStreamId: number): Promise<mongo.Collection<CurrentDataDocumentV3>[]> {
31
+ async listSourceRecordCollections(replicationStreamId: number): Promise<mongo.Collection<CurrentDataDocumentV3>[]> {
25
32
  return this.listCollectionsByPrefix<CurrentDataDocumentV3>(`source_records_${replicationStreamId}_`);
26
33
  }
27
34
 
28
- async initializeSourceRecordsCollection(replicationStreamId: number, sourceTableId: mongo.ObjectId) {
29
- await this.sourceRecordsV3(replicationStreamId, sourceTableId).createIndex(
35
+ async initializeSourceRecordsCollection(
36
+ replicationStreamId: number,
37
+ sourceTableId: mongo.ObjectId,
38
+ session?: mongo.ClientSession
39
+ ) {
40
+ await this.sourceRecords(replicationStreamId, sourceTableId).createIndex(
30
41
  {
31
42
  pending_delete: 1
32
43
  },
33
44
  {
34
45
  partialFilterExpression: { pending_delete: { $exists: true } },
35
- name: 'pending_delete'
46
+ name: 'pending_delete',
47
+ session
36
48
  }
37
49
  );
38
50
  }
39
51
 
40
52
  commonSourceTables(replicationStreamId: number): mongo.Collection<CommonSourceTableDocument> {
41
- return this.sourceTablesV3(replicationStreamId) as mongo.Collection<CommonSourceTableDocument>;
53
+ return this.sourceTables(replicationStreamId) as any as mongo.Collection<CommonSourceTableDocument>;
42
54
  }
43
55
 
44
- bucketStateV3(replicationStreamId: number): mongo.Collection<BucketStateDocumentV3> {
56
+ bucketState(replicationStreamId: number): mongo.Collection<BucketStateDocumentV3> {
45
57
  return this.db.collection(`bucket_state_${replicationStreamId}`);
46
58
  }
47
59
 
48
- parameterIndexV3(
49
- replicationStreamId: number,
50
- indexId: ParameterIndexId
51
- ): mongo.Collection<BucketParameterDocumentV3> {
60
+ parameterIndex(replicationStreamId: number, indexId: ParameterIndexId): mongo.Collection<BucketParameterDocumentV3> {
52
61
  return this.db.collection(`parameter_index_${replicationStreamId}_${indexId}`);
53
62
  }
54
63
 
55
- sourceTablesV3(replicationStreamId: number): mongo.Collection<SourceTableDocumentV3> {
64
+ sourceTables(replicationStreamId: number): mongo.Collection<SourceTableDocumentV3> {
56
65
  return this.db.collection<SourceTableDocumentV3>(this.sourceTableCollectionName(replicationStreamId));
57
66
  }
58
67
 
68
+ bucketData(replicationStreamId: number, definitionId: BucketDefinitionId): mongo.Collection<BucketDataDocumentV3> {
69
+ return this.db.collection<BucketDataDocumentV3>(`bucket_data_${replicationStreamId}_${definitionId}`);
70
+ }
71
+
72
+ listBucketDataCollections(replicationStreamId: number) {
73
+ return this.listCollectionsByPrefix(`bucket_data_${replicationStreamId}_`);
74
+ }
75
+
76
+ async listParameterIndexCollections(replicationStreamId: number) {
77
+ const prefix = `parameter_index_${replicationStreamId}_`;
78
+ const collections = await this.listCollectionsByPrefix(prefix);
79
+
80
+ return collections.map((collection) => ({
81
+ collection,
82
+ indexId: collection.collectionName.slice(prefix.length)
83
+ }));
84
+ }
85
+
59
86
  async initializeStreamStorage(replicationStreamId: number) {
60
- const sourceTables = this.sourceTablesV3(replicationStreamId);
61
- const bucketState = this.bucketStateV3(replicationStreamId);
87
+ const sourceTables = this.sourceTables(replicationStreamId);
88
+ const bucketState = this.bucketState(replicationStreamId);
62
89
  await sourceTables.createIndex(
63
90
  {
64
91
  connection_id: 1,
@@ -92,26 +119,4 @@ export class VersionedPowerSyncMongoV3 extends BaseVersionedPowerSyncMongo {
92
119
  { name: 'dirty_count' }
93
120
  );
94
121
  }
95
-
96
- bucketDataV3(replicationStreamId: number, definitionId: BucketDefinitionId) {
97
- return this.db.collection<BucketDataDocumentV3>(`bucket_data_${replicationStreamId}_${definitionId}`);
98
- }
99
-
100
- listBucketDataCollectionsV3(replicationStreamId: number) {
101
- return this.upstream.listBucketDataCollectionsV3(replicationStreamId);
102
- }
103
-
104
- async listParameterIndexCollectionsV3(
105
- replicationStreamId: number
106
- ): Promise<{ collection: mongo.Collection<BucketParameterDocumentV3>; indexId: ParameterIndexId }[]> {
107
- const prefix = `parameter_index_${replicationStreamId}_`;
108
- const collections = await this.db.listCollections({ name: new RegExp(`^${prefix}`) }, { nameOnly: true }).toArray();
109
-
110
- return collections
111
- .filter((collection) => collection.name.startsWith(prefix))
112
- .map((collection) => ({
113
- collection: this.db.collection<BucketParameterDocumentV3>(collection.name),
114
- indexId: collection.name.slice(prefix.length)
115
- }));
116
- }
117
122
  }
@@ -0,0 +1,70 @@
1
+ import { BucketDataDoc, BucketKey } from '../common/BucketDataDoc.js';
2
+ import { BucketDataDocumentV3, BucketOperation } from './models.js';
3
+
4
+ export function serializeBucketData(bucket: string, operations: BucketDataDoc[]): BucketDataDocumentV3 {
5
+ const minOp = operations[0].o;
6
+ const maxOp = operations[operations.length - 1].o;
7
+
8
+ let totalChecksum = 0n;
9
+ let totalSize = 0;
10
+ let maxTargetOp: bigint | null = null;
11
+
12
+ const ops: BucketOperation[] = operations.map((op) => {
13
+ totalChecksum += op.checksum;
14
+ totalSize += op.data?.length ?? 0;
15
+
16
+ if (op.target_op != null && (maxTargetOp == null || op.target_op > maxTargetOp)) {
17
+ maxTargetOp = op.target_op;
18
+ }
19
+
20
+ return {
21
+ o: op.o,
22
+ op: op.op,
23
+ source_table: op.source_table,
24
+ source_key: op.source_key,
25
+ table: op.table,
26
+ row_id: op.row_id,
27
+ checksum: op.checksum,
28
+ data: op.data
29
+ };
30
+ });
31
+
32
+ return {
33
+ _id: {
34
+ b: bucket,
35
+ o: maxOp
36
+ },
37
+ min_op: minOp,
38
+ checksum: totalChecksum,
39
+ count: operations.length,
40
+ size: totalSize,
41
+ target_op: maxTargetOp,
42
+ ops
43
+ };
44
+ }
45
+
46
+ export function* loadBucketDataDocument(
47
+ context: Pick<BucketKey, 'replicationStreamId' | 'definitionId'>,
48
+ doc: BucketDataDocumentV3
49
+ ): Generator<BucketDataDoc> {
50
+ const { _id, ops } = doc;
51
+ const bucketKey = {
52
+ ...context,
53
+ bucket: _id.b
54
+ };
55
+
56
+ for (const op of ops) {
57
+ yield {
58
+ bucketKey,
59
+ o: op.o,
60
+ op: op.op,
61
+ source_table: op.source_table,
62
+ source_key: op.source_key,
63
+ table: op.table,
64
+ row_id: op.row_id,
65
+ checksum: op.checksum,
66
+ data: op.data,
67
+ target_op: doc.target_op ?? null
68
+ };
69
+ }
70
+ }
@@ -0,0 +1,38 @@
1
+ import { BucketDataDoc } from '../common/BucketDataDoc.js';
2
+
3
+ export const DEFAULT_MAX_DOC_SIZE_BYTES = 1024 * 1024; // 1MB
4
+
5
+ /**
6
+ * Split an array of bucket data operations into chunks where each chunk's
7
+ * total data size stays close to `maxDocSizeBytes`.
8
+ *
9
+ * The threshold is a target, not a hard ceiling — a single operation whose
10
+ * `data` field exceeds the limit is placed in its own chunk and is NOT split.
11
+ */
12
+ export function chunkBucketData(
13
+ operations: BucketDataDoc[],
14
+ maxDocSizeBytes = DEFAULT_MAX_DOC_SIZE_BYTES
15
+ ): BucketDataDoc[][] {
16
+ const chunks: BucketDataDoc[][] = [];
17
+ let currentChunk: BucketDataDoc[] = [];
18
+ let currentSize = 0;
19
+
20
+ for (const op of operations) {
21
+ const opSize = op.data?.length ?? 0;
22
+
23
+ if (currentSize + opSize > maxDocSizeBytes && currentChunk.length > 0) {
24
+ chunks.push(currentChunk);
25
+ currentChunk = [];
26
+ currentSize = 0;
27
+ }
28
+
29
+ currentChunk.push(op);
30
+ currentSize += opSize;
31
+ }
32
+
33
+ if (currentChunk.length > 0) {
34
+ chunks.push(currentChunk);
35
+ }
36
+
37
+ return chunks;
38
+ }