@powersync/service-module-mongodb-storage 0.0.0-dev-20260803144812 → 0.0.0-dev-20260929095716

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 (213) hide show
  1. package/CHANGELOG.md +129 -7
  2. package/dist/storage/MongoBucketStorage.d.ts +42 -5
  3. package/dist/storage/MongoBucketStorage.js +404 -215
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +39 -14
  6. package/dist/storage/implementation/MongoBucketBatch.js +169 -98
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoCompactor.d.ts +21 -79
  9. package/dist/storage/implementation/MongoCompactor.js +27 -238
  10. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  11. package/dist/storage/implementation/MongoIdSequence.d.ts +10 -1
  12. package/dist/storage/implementation/MongoIdSequence.js +14 -1
  13. package/dist/storage/implementation/MongoIdSequence.js.map +1 -1
  14. package/dist/storage/implementation/MongoOpIdAllocator.d.ts +27 -0
  15. package/dist/storage/implementation/MongoOpIdAllocator.js +97 -0
  16. package/dist/storage/implementation/MongoOpIdAllocator.js.map +1 -0
  17. package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
  18. package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
  19. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  20. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
  21. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +35 -2
  22. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
  23. package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +2 -1
  24. package/dist/storage/implementation/MongoPersistedReplicationStream.js +7 -4
  25. package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -1
  26. package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +3 -3
  27. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +10 -8
  28. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
  29. package/dist/storage/implementation/MongoStorageProvider.js +8 -4
  30. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  31. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +32 -11
  32. package/dist/storage/implementation/MongoSyncBucketStorage.js +95 -31
  33. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  34. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +26 -2
  35. package/dist/storage/implementation/MongoSyncRulesLock.js +59 -10
  36. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  37. package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
  38. package/dist/storage/implementation/MongoWriteBatch.js +149 -0
  39. package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
  40. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
  41. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +9 -30
  42. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  43. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
  44. package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
  45. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
  46. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
  47. package/dist/storage/implementation/common/PersistedBatch.d.ts +32 -4
  48. package/dist/storage/implementation/common/PersistedBatch.js +57 -14
  49. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  50. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
  51. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
  52. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  53. package/dist/storage/implementation/db.d.ts +9 -1
  54. package/dist/storage/implementation/db.js +28 -1
  55. package/dist/storage/implementation/db.js.map +1 -1
  56. package/dist/storage/implementation/models.d.ts +35 -0
  57. package/dist/storage/implementation/models.js.map +1 -1
  58. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +11 -0
  59. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +227 -221
  60. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  61. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +38 -6
  62. package/dist/storage/implementation/v1/MongoCompactorV1.js +247 -20
  63. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  64. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
  65. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
  66. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
  67. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
  68. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +66 -17
  69. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  70. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
  71. package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
  72. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
  73. package/dist/storage/implementation/v1/models.js +1 -0
  74. package/dist/storage/implementation/v1/models.js.map +1 -1
  75. package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
  76. package/dist/storage/implementation/v3/CompactionLease.js +131 -0
  77. package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
  78. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +14 -2
  79. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +261 -194
  80. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  81. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +83 -25
  82. package/dist/storage/implementation/v3/MongoCompactorV3.js +702 -159
  83. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  84. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
  85. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
  86. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  87. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
  88. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
  89. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
  90. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
  91. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +92 -32
  92. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  93. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
  94. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
  95. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
  96. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +14 -8
  97. package/dist/storage/implementation/v3/PersistedBatchV3.js +77 -45
  98. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  99. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
  100. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
  101. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  102. package/dist/storage/implementation/v3/bucket-format.js +2 -0
  103. package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
  104. package/dist/storage/implementation/v3/compact-utils.d.ts +108 -0
  105. package/dist/storage/implementation/v3/compact-utils.js +223 -0
  106. package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
  107. package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
  108. package/dist/storage/implementation/v3/compaction-constants.js +6 -0
  109. package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
  110. package/dist/storage/implementation/v3/models.d.ts +90 -6
  111. package/dist/storage/implementation/v3/models.js.map +1 -1
  112. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
  113. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +16 -5
  114. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
  115. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
  116. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +6 -8
  117. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +17 -11
  118. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
  119. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +38 -0
  120. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +164 -0
  121. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
  122. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
  123. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +197 -25
  124. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
  125. package/dist/storage/implementation/v3/source-table-utils.d.ts +15 -7
  126. package/dist/storage/implementation/v3/source-table-utils.js +49 -57
  127. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
  128. package/dist/storage/storage-index.d.ts +2 -1
  129. package/dist/storage/storage-index.js +1 -0
  130. package/dist/storage/storage-index.js.map +1 -1
  131. package/dist/types/types.d.ts +28 -1
  132. package/dist/types/types.js +28 -7
  133. package/dist/types/types.js.map +1 -1
  134. package/dist/utils/test-utils.d.ts +4 -2
  135. package/dist/utils/test-utils.js +7 -4
  136. package/dist/utils/test-utils.js.map +1 -1
  137. package/dist/utils/util.js +3 -1
  138. package/dist/utils/util.js.map +1 -1
  139. package/package.json +10 -9
  140. package/src/storage/MongoBucketStorage.ts +545 -277
  141. package/src/storage/implementation/MongoBucketBatch.ts +258 -124
  142. package/src/storage/implementation/MongoCompactor.ts +37 -346
  143. package/src/storage/implementation/MongoIdSequence.ts +21 -1
  144. package/src/storage/implementation/MongoOpIdAllocator.ts +105 -0
  145. package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
  146. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +46 -1
  147. package/src/storage/implementation/MongoPersistedReplicationStream.ts +12 -4
  148. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +48 -29
  149. package/src/storage/implementation/MongoStorageProvider.ts +16 -4
  150. package/src/storage/implementation/MongoSyncBucketStorage.ts +133 -40
  151. package/src/storage/implementation/MongoSyncRulesLock.ts +78 -12
  152. package/src/storage/implementation/MongoWriteBatch.ts +172 -0
  153. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +16 -46
  154. package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
  155. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
  156. package/src/storage/implementation/common/PersistedBatch.ts +76 -20
  157. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
  158. package/src/storage/implementation/db.ts +36 -2
  159. package/src/storage/implementation/models.ts +39 -0
  160. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +272 -254
  161. package/src/storage/implementation/v1/MongoCompactorV1.ts +327 -40
  162. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
  163. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +88 -28
  164. package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
  165. package/src/storage/implementation/v1/models.ts +1 -0
  166. package/src/storage/implementation/v3/CompactionLease.ts +152 -0
  167. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +339 -247
  168. package/src/storage/implementation/v3/MongoCompactorV3.ts +955 -225
  169. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
  170. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
  171. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +112 -42
  172. package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
  173. package/src/storage/implementation/v3/PersistedBatchV3.ts +94 -51
  174. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
  175. package/src/storage/implementation/v3/bucket-format.ts +2 -0
  176. package/src/storage/implementation/v3/compact-utils.ts +329 -0
  177. package/src/storage/implementation/v3/compaction-constants.ts +5 -0
  178. package/src/storage/implementation/v3/models.ts +97 -4
  179. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +24 -9
  180. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
  181. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +26 -14
  182. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +229 -0
  183. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +279 -29
  184. package/src/storage/implementation/v3/source-table-utils.ts +82 -76
  185. package/src/storage/storage-index.ts +2 -0
  186. package/src/types/types.ts +42 -7
  187. package/src/utils/test-utils.ts +9 -5
  188. package/src/utils/util.ts +3 -1
  189. package/test/src/__snapshots__/storage.test.ts.snap +25 -0
  190. package/test/src/__snapshots__/storage_sync.test.ts.snap +794 -205
  191. package/test/src/chunk_compaction_config.test.ts +38 -0
  192. package/test/src/cleanup-stopped-sync-configs.test.ts +59 -26
  193. package/test/src/compact-utils.test.ts +251 -0
  194. package/test/src/concurrent_writers.test.ts +599 -0
  195. package/test/src/connection_config.test.ts +159 -0
  196. package/test/src/helpers/MemoryObjectStorage.ts +14 -6
  197. package/test/src/helpers/concurrentWriter.mjs +29 -0
  198. package/test/src/helpers/s3TestFactory.ts +12 -12
  199. package/test/src/object_storage_usage.test.ts +268 -0
  200. package/test/src/parameter_compacting_v1.test.ts +120 -0
  201. package/test/src/parameter_compacting_v3.test.ts +192 -0
  202. package/test/src/parameter_compaction_fence.test.ts +276 -0
  203. package/test/src/storage.test.ts +280 -24
  204. package/test/src/storage_compacting.test.ts +1097 -253
  205. package/test/src/storage_object_storage_inline_threshold.test.ts +3 -3
  206. package/test/src/storage_s3_checksums.test.ts +6 -7
  207. package/test/src/storage_s3_compaction_lifecycle.test.ts +143 -17
  208. package/test/src/storage_s3_reading.test.ts +294 -6
  209. package/test/src/storage_s3_writing.test.ts +4 -3
  210. package/test/src/storage_sync.test.ts +412 -80
  211. package/test/src/storeCurrentData.test.ts +5 -5
  212. package/test/src/util.ts +1 -1
  213. package/tsconfig.tsbuildinfo +1 -1
@@ -6,7 +6,7 @@ import {
6
6
  storage,
7
7
  updateSyncRulesFromYaml
8
8
  } from '@powersync/service-core';
9
- import { bucketRequest, register, test_utils } from '@powersync/service-core-tests';
9
+ import { bucketRequest, getTestStorage, register, test_utils } from '@powersync/service-core-tests';
10
10
  import { RequestParameters, ScopedParameterLookup, SqlSyncRules } from '@powersync/service-sync-rules';
11
11
  import * as bson from 'bson';
12
12
  import { describe, expect, test } from 'vitest';
@@ -16,6 +16,7 @@ import { MongoPersistedSyncConfigContentV3 } from '../../src/storage/implementat
16
16
  import { BucketDataDoc, BucketKey } from '../../src/storage/implementation/common/BucketDataDoc.js';
17
17
  import { MongoSyncBucketStorage } from '../../src/storage/implementation/createMongoSyncBucketStorage.js';
18
18
  import { getMongoStorageConfig } from '../../src/storage/implementation/models.js';
19
+ import { loadBucketDataDocumentV1, serializeBucketDataV1 } from '../../src/storage/implementation/v1/models.js';
19
20
  import { SourceRecordStoreV3 } from '../../src/storage/implementation/v3/SourceRecordStoreV3.js';
20
21
  import type { VersionedPowerSyncMongoV3 } from '../../src/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
21
22
  import { serializeBucketData } from '../../src/storage/implementation/v3/bucket-format.js';
@@ -98,7 +99,9 @@ async function getMongoSyncConfigContents(factory: storage.BucketStorageFactory,
98
99
  })
99
100
  .toArray();
100
101
 
101
- return syncConfigDocs.map((config) => new MongoPersistedSyncConfigContentV3(mongoFactory.db, doc, config));
102
+ return syncConfigDocs.map(
103
+ (config) => new MongoPersistedSyncConfigContentV3(mongoFactory.db, doc, config, mongoFactory.syncConfigParser)
104
+ );
102
105
  }
103
106
 
104
107
  /**
@@ -114,11 +117,65 @@ function bucketDefinitionId(bucket: string) {
114
117
  return parseInt(match[1], 16);
115
118
  }
116
119
 
120
+ test('V1 bucket data retains an optional persisted subkey', () => {
121
+ const sourceTable = new bson.ObjectId();
122
+ const document = serializeBucketDataV1({
123
+ bucketKey: {
124
+ replicationStreamId: 1,
125
+ definitionId: '0',
126
+ bucket: 'global[]'
127
+ },
128
+ o: 1n,
129
+ op: 'PUT',
130
+ source_table: sourceTable,
131
+ source_key: 'source-key',
132
+ subkey: 'persisted-subkey',
133
+ table: 'items',
134
+ row_id: 'item-1',
135
+ checksum: 1n,
136
+ data: '{"id":"item-1"}'
137
+ });
138
+
139
+ expect(loadBucketDataDocumentV1(document).subkey).toBe('persisted-subkey');
140
+
141
+ const { subkey: _subkey, ...legacyDocument } = document;
142
+ expect(loadBucketDataDocumentV1(legacyDocument).subkey).toBeUndefined();
143
+ });
144
+
117
145
  function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, storageVersion: number) {
118
146
  register.registerSyncTests(storageConfig.factory, {
119
147
  storageVersion,
120
148
  tableIdStrings: storageConfig.tableIdStrings
121
149
  });
150
+
151
+ test('updates source metadata on an existing resolved table', async () => {
152
+ await using factory = await storageConfig.factory();
153
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(MINIMAL_SYNC_RULES, { storageVersion }));
154
+ const bucketStorage = await getTestStorage(factory, syncRules);
155
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
156
+ const source = sourceDescriptor('test');
157
+
158
+ const initial = await writer.resolveTables({ connection_id: 1, source });
159
+ expect(initial.tables[0].sourceMetadata).toBeNull();
160
+
161
+ const sourceMetadata = { captureTableObjectId: 42 };
162
+ const updated = await writer.resolveTables({
163
+ connection_id: 1,
164
+ source,
165
+ reconcileSourceTables: ({ candidates }) => ({
166
+ compatibleTables: candidates.map((candidate) => candidate.withSourceMetadata(sourceMetadata)),
167
+ incompatibleTables: [],
168
+ newTableValues: { sourceMetadata }
169
+ })
170
+ });
171
+
172
+ expect(updated.tables.map((table) => table.id.toString())).toEqual(
173
+ initial.tables.map((table) => table.id.toString())
174
+ );
175
+ expect(updated.tables[0].sourceMetadata).toEqual(sourceMetadata);
176
+ expect((await writer.getSourceTableStatus(updated.tables[0]))?.sourceMetadata).toEqual(sourceMetadata);
177
+ });
178
+
122
179
  // The split of returned results can vary depending on storage drivers
123
180
  test('large batch (2)', async () => {
124
181
  // Test syncing a batch of data that is small in count,
@@ -136,7 +193,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
136
193
  { storageVersion }
137
194
  )
138
195
  );
139
- const bucketStorage = factory.getInstance(syncRules);
196
+ const bucketStorage = await getTestStorage(factory, syncRules);
140
197
  const syncRulesContent = syncRules.syncConfigContent[0];
141
198
 
142
199
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
@@ -283,7 +340,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
283
340
  { storageVersion }
284
341
  )
285
342
  );
286
- const bucketStorage = factory.getInstance(syncRules);
343
+ const bucketStorage = await getTestStorage(factory, syncRules);
287
344
 
288
345
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
289
346
  const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], INITIALIZED_MONGO_STORAGE_FACTORY);
@@ -308,7 +365,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
308
365
  { storageVersion }
309
366
  )
310
367
  );
311
- const bucketStorage = factory.getInstance(syncRules);
368
+ const bucketStorage = await getTestStorage(factory, syncRules);
312
369
 
313
370
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
314
371
  const before = await writer.resolveTables({
@@ -346,7 +403,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
346
403
  { storageVersion }
347
404
  )
348
405
  );
349
- const bucketStorage = factory.getInstance(syncRules);
406
+ const bucketStorage = await getTestStorage(factory, syncRules);
350
407
 
351
408
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
352
409
  const before = await writer.resolveTables({
@@ -382,7 +439,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
382
439
  { storageVersion }
383
440
  )
384
441
  );
385
- const bucketStorage = factory.getInstance(syncRules);
442
+ const bucketStorage = await getTestStorage(factory, syncRules);
386
443
 
387
444
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
388
445
  const before = await writer.resolveTables({
@@ -426,7 +483,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
426
483
  { storageVersion }
427
484
  )
428
485
  );
429
- const bucketStorage = factory.getInstance(syncRules);
486
+ const bucketStorage = await getTestStorage(factory, syncRules);
430
487
 
431
488
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
432
489
  const resolved = await writer.resolveTables({
@@ -495,7 +552,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
495
552
  // Here we're persisting one sync config, then resolving tables with others.
496
553
  // We're also using the default hydration state for them all.
497
554
  const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(fullRulesYaml, { storageVersion }));
498
- const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
555
+ const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
499
556
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
500
557
  const fullRules = parsedSyncConfigSetFor(fullRulesYaml, storageVersion);
501
558
  const dataOnlyRules = parsedSyncConfigSetFor(dataOnlyRulesYaml, storageVersion);
@@ -504,6 +561,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
504
561
  const source = sourceDescriptor('memberships', { objectId: 'memberships-relation' });
505
562
  const dataOnlyTableId = new bson.ObjectId('6544e3899293153fa7b38348');
506
563
  const addedParameterTableId = new bson.ObjectId('6544e3899293153fa7b38349');
564
+ const addedEventTableId = new bson.ObjectId('6544e3899293153fa7b3834a');
507
565
 
508
566
  const dataOnly = await writer.resolveTables({
509
567
  connection_id: 1,
@@ -561,15 +619,13 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
561
619
  const eventOnly = await writer.resolveTables({
562
620
  connection_id: 1,
563
621
  source,
564
- idGenerator: () => {
565
- throw new Error('resolve should reuse existing v3 source table');
566
- },
622
+ idGenerator: () => addedEventTableId,
567
623
  parsedSyncConfig: eventOnlyRules
568
624
  });
569
625
 
570
- // Event-only table can re-use any existing table.
626
+ // A newly-added event gets its own source table so that its existing rows are snapshotted.
571
627
  expect(eventOnly.tables).toHaveLength(1);
572
- expect([dataOnlyTableId.toString(), addedParameterTableId.toString()]).toContain(eventOnly.tables[0].id.toString());
628
+ expect(eventOnly.tables[0].id).toEqual(addedEventTableId);
573
629
  expect(eventOnly.dropTables.map((table) => table.id)).toEqual([]);
574
630
  expect(eventOnly.tables[0].bucketDataSources).toHaveLength(0);
575
631
  expect(eventOnly.tables[0].parameterLookupSources).toHaveLength(0);
@@ -578,13 +634,12 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
578
634
  expect(eventOnly.tables[0].syncEvent).toBe(true);
579
635
  });
580
636
 
581
- test.runIf(storageVersion >= 3)(
582
- 'resolveTables designates a single event carrier across split source tables',
583
- async () => {
584
- // When memberships are split over multiple SourceTables for the same ref, a row change
585
- // is saved once per table. Only one table may fire events, otherwise the same event
586
- // would fire once per table for every row change.
587
- const dataOnlyEventYaml = `
637
+ test.runIf(storageVersion >= 3)('resolveTables assigns an event definition to one split source table', async () => {
638
+ // When memberships are split over multiple SourceTables for the same ref, a row change
639
+ // is saved once per table. Each event-definition id must belong to only one of those tables,
640
+ // otherwise that definition would be evaluated more than once for the same row change.
641
+ // Other tables may independently own and evaluate different event definitions.
642
+ const dataOnlyEventYaml = `
588
643
  bucket_definitions:
589
644
  by_owner:
590
645
  parameters:
@@ -597,7 +652,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
597
652
  payloads:
598
653
  - SELECT id, owner_id FROM memberships
599
654
  `;
600
- const fullEventYaml = `
655
+ const fullEventYaml = `
601
656
  bucket_definitions:
602
657
  by_owner:
603
658
  parameters:
@@ -611,42 +666,201 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
611
666
  - SELECT id, owner_id FROM memberships
612
667
  `;
613
668
 
669
+ await using factory = await storageConfig.factory();
670
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(dataOnlyEventYaml, { storageVersion }));
671
+ const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
672
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
673
+ const dataOnlyRules = parsedSyncConfigSetFor(dataOnlyEventYaml, storageVersion);
674
+ const fullRules = parsedSyncConfigSetFor(fullEventYaml, storageVersion);
675
+ const source = sourceDescriptor('memberships', { objectId: 'memberships-relation' });
676
+
677
+ const initial = await writer.resolveTables({
678
+ connection_id: 1,
679
+ source,
680
+ idGenerator: objectIdGenerator('6544e3899293153fa7b3834b'),
681
+ parsedSyncConfig: dataOnlyRules
682
+ });
683
+ expect(initial.tables).toHaveLength(1);
684
+ expect(initial.tables[0].syncEvent).toBe(true);
685
+
686
+ // Adding the table-based parameter lookup creates a second SourceTable for the same ref.
687
+ const split = await writer.resolveTables({
688
+ connection_id: 1,
689
+ source,
690
+ idGenerator: objectIdGenerator('6544e3899293153fa7b3834c'),
691
+ parsedSyncConfig: fullRules
692
+ });
693
+ expect(split.tables).toHaveLength(2);
694
+ // Both tables match the event by ref, but this event id belongs to only one of them.
695
+ const eventOwners = split.tables.filter((table) => table.syncEvent);
696
+ expect(eventOwners).toHaveLength(1);
697
+
698
+ // getSourceTableStatus rehydrates the persisted event memberships rather than recomputing
699
+ // them from the ref, so refreshing the other table does not assign this event to it.
700
+ const nonOwner = split.tables.find((table) => !table.syncEvent)!;
701
+ const refreshed = await writer.getSourceTableStatus(nonOwner);
702
+ expect(refreshed!.syncEvent).toBe(false);
703
+ });
704
+
705
+ test.runIf(storageVersion >= 3)(
706
+ 'reuses an unchanged event definition without resnapshotting or duplicate evaluation',
707
+ async () => {
708
+ const firstYaml = `
709
+ config:
710
+ edition: 3
711
+
712
+ streams:
713
+ by_owner:
714
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
715
+
716
+ event_definitions:
717
+ write_checkpoints:
718
+ payloads:
719
+ - SELECT user_id, checkpoint FROM checkpoints WHERE active = true AND checkpoint > 0
720
+ `;
721
+ const secondYaml = `
722
+ config:
723
+ edition: 3
724
+
725
+ streams:
726
+ by_owner:
727
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
728
+ by_project:
729
+ query: SELECT * FROM todos WHERE project_id = subscription.parameter('project_id')
730
+
731
+ event_definitions:
732
+ write_checkpoints:
733
+ payloads:
734
+ - select "user_id", "checkpoint" from "checkpoints" where "active" = true and "checkpoint" > 0
735
+ `;
736
+
614
737
  await using factory = await storageConfig.factory();
615
- const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(dataOnlyEventYaml, { storageVersion }));
616
- const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
617
- await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
618
- const dataOnlyRules = parsedSyncConfigSetFor(dataOnlyEventYaml, storageVersion);
619
- const fullRules = parsedSyncConfigSetFor(fullEventYaml, storageVersion);
620
- const source = sourceDescriptor('memberships', { objectId: 'memberships-relation' });
738
+ const emittedEvents: { name: string; eventId: string | undefined }[] = [];
739
+ const disposeListener = factory.registerListener({
740
+ replicationEvent: ({ event, event_id }) => emittedEvents.push({ name: event.name, eventId: event_id })
741
+ });
742
+ try {
743
+ const first = await factory.updateSyncRules(updateSyncRulesFromYaml(firstYaml, { storageVersion }));
744
+ const firstStorage = (await getTestStorage(factory, first)) as MongoSyncBucketStorage;
745
+ await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
746
+ const source = sourceDescriptor('checkpoints', { objectId: 'checkpoints-relation' });
747
+ const firstResolved = await firstWriter.resolveTables({
748
+ connection_id: 1,
749
+ source,
750
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38351')
751
+ });
752
+ const eventId = [...firstResolved.tables[0].eventDefinitionIds!][0];
753
+ expect(eventId).toBeDefined();
754
+ await firstWriter.markTableSnapshotDone(firstResolved.tables, '1/1');
755
+ await firstWriter.markAllSnapshotDone('1/1');
756
+ await firstWriter.commit('1/1');
757
+
758
+ const second = await factory.updateSyncRules(updateSyncRulesFromYaml(secondYaml, { storageVersion }));
759
+ expect(second.replicationStreamId).toBe(first.replicationStreamId);
760
+ const secondStorage = (await getTestStorage(factory, second)) as MongoSyncBucketStorage;
761
+ await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
762
+ const resolved = await secondWriter.resolveTables({
763
+ connection_id: 1,
764
+ source,
765
+ idGenerator: () => {
766
+ throw new Error('unchanged event definition should reuse the snapshotted source table');
767
+ }
768
+ });
621
769
 
622
- const initial = await writer.resolveTables({
770
+ expect(resolved.tables).toHaveLength(1);
771
+ expect(resolved.tables[0].id).toEqual(firstResolved.tables[0].id);
772
+ expect(resolved.tables[0].snapshotComplete).toBe(true);
773
+ expect([...resolved.tables[0].eventDefinitionIds!]).toEqual([eventId]);
774
+
775
+ emittedEvents.length = 0;
776
+ await secondWriter.save({
777
+ sourceTable: resolved.tables[0],
778
+ tag: storage.SaveOperationTag.INSERT,
779
+ after: { id: 'checkpoint-1', user_id: 'user-1', checkpoint: 1n, active: 1 },
780
+ afterReplicaId: test_utils.rid('checkpoint-1')
781
+ });
782
+ expect(emittedEvents).toEqual([{ name: 'write_checkpoints', eventId }]);
783
+ } finally {
784
+ disposeListener();
785
+ }
786
+ }
787
+ );
788
+
789
+ test.runIf(storageVersion >= 3)('creates snapshot work for a changed event definition', async () => {
790
+ const yaml = (active: boolean) => `
791
+ config:
792
+ edition: 3
793
+
794
+ streams:
795
+ by_owner:
796
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
797
+
798
+ event_definitions:
799
+ write_checkpoints:
800
+ payloads:
801
+ - SELECT user_id, checkpoint FROM checkpoints WHERE active = ${active}
802
+ `;
803
+
804
+ await using factory = await storageConfig.factory();
805
+ const emittedEvents: { name: string; eventId: string | undefined }[] = [];
806
+ const disposeListener = factory.registerListener({
807
+ replicationEvent: ({ event, event_id }) => emittedEvents.push({ name: event.name, eventId: event_id })
808
+ });
809
+ try {
810
+ const first = await factory.updateSyncRules(updateSyncRulesFromYaml(yaml(true), { storageVersion }));
811
+ const firstStorage = (await getTestStorage(factory, first)) as MongoSyncBucketStorage;
812
+ await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
813
+ const source = sourceDescriptor('checkpoints', { objectId: 'checkpoints-relation' });
814
+ const firstResolved = await firstWriter.resolveTables({
623
815
  connection_id: 1,
624
816
  source,
625
- idGenerator: objectIdGenerator('6544e3899293153fa7b3834b'),
626
- parsedSyncConfig: dataOnlyRules
817
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38352')
627
818
  });
628
- expect(initial.tables).toHaveLength(1);
629
- expect(initial.tables[0].syncEvent).toBe(true);
819
+ const oldEventId = [...firstResolved.tables[0].eventDefinitionIds!][0];
820
+ await firstWriter.markTableSnapshotDone(firstResolved.tables, '1/1');
821
+ await firstWriter.markAllSnapshotDone('1/1');
822
+ await firstWriter.commit('1/1');
630
823
 
631
- // Adding the table-based parameter lookup creates a second SourceTable for the same ref.
632
- const split = await writer.resolveTables({
824
+ const second = await factory.updateSyncRules(updateSyncRulesFromYaml(yaml(false), { storageVersion }));
825
+ expect(second.replicationStreamId).toBe(first.replicationStreamId);
826
+ const secondStorage = (await getTestStorage(factory, second)) as MongoSyncBucketStorage;
827
+ await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
828
+ const resolved = await secondWriter.resolveTables({
633
829
  connection_id: 1,
634
830
  source,
635
- idGenerator: objectIdGenerator('6544e3899293153fa7b3834c'),
636
- parsedSyncConfig: fullRules
831
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38353')
637
832
  });
638
- expect(split.tables).toHaveLength(2);
639
- // Both tables match the event by ref, but only one may carry it.
640
- const carriers = split.tables.filter((table) => table.syncEvent);
641
- expect(carriers).toHaveLength(1);
642
-
643
- // getSourceTableStatus preserves the carrier designation rather than recomputing it
644
- // from the ref, so refreshing a non-carrier table does not make it fire events.
645
- const nonCarrier = split.tables.find((table) => !table.syncEvent)!;
646
- const refreshed = await writer.getSourceTableStatus(nonCarrier);
647
- expect(refreshed!.syncEvent).toBe(false);
833
+
834
+ expect(resolved.tables).toHaveLength(2);
835
+ const oldTable = resolved.tables.find((table) => table.eventDefinitionIds?.has(oldEventId))!;
836
+ const newTable = resolved.tables.find((table) => !table.eventDefinitionIds?.has(oldEventId))!;
837
+ const newEventId = [...newTable.eventDefinitionIds!][0];
838
+ expect(newEventId).toBeDefined();
839
+ expect(newEventId).not.toBe(oldEventId);
840
+ expect(oldTable.snapshotComplete).toBe(true);
841
+ expect(newTable.snapshotComplete).toBe(false);
842
+ expect(newTable.bucketDataSources).toEqual([]);
843
+ expect(newTable.parameterLookupSources).toEqual([]);
844
+
845
+ await expect(secondWriter.markSnapshotDone('2/1')).rejects.toThrow(
846
+ 'Cannot mark snapshot done while source tables still require snapshotting'
847
+ );
848
+
849
+ emittedEvents.length = 0;
850
+ await secondWriter.save({
851
+ sourceTable: newTable,
852
+ tag: storage.SaveOperationTag.INSERT,
853
+ after: { id: 'checkpoint-2', user_id: 'user-2', checkpoint: 2n, active: 0 },
854
+ afterReplicaId: test_utils.rid('checkpoint-2')
855
+ });
856
+ expect(emittedEvents).toEqual([{ name: 'write_checkpoints', eventId: newEventId }]);
857
+
858
+ await secondWriter.markTableSnapshotDone([newTable], '2/1');
859
+ await secondWriter.markSnapshotDone('2/1');
860
+ } finally {
861
+ disposeListener();
648
862
  }
649
- );
863
+ });
650
864
 
651
865
  test.runIf(storageVersion >= 3)('uses v3 mongodb model shapes', async () => {
652
866
  await using factory = await storageConfig.factory();
@@ -663,7 +877,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
663
877
  { storageVersion }
664
878
  )
665
879
  );
666
- const bucketStorage = factory.getInstance(syncRules);
880
+ const bucketStorage = await getTestStorage(factory, syncRules);
667
881
  const syncRulesContent = syncRules.syncConfigContent[0];
668
882
  const sync_rules = syncRulesContent.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig;
669
883
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
@@ -776,7 +990,7 @@ streams:
776
990
  expect(parsed.syncConfigs).toHaveLength(1);
777
991
  expect(parsed.hydratedSyncConfig.bucketDataSources).toHaveLength(1);
778
992
 
779
- const bucketStorage = factory.getInstance(replicatingStreams[0]);
993
+ const bucketStorage = await getTestStorage(factory, replicatingStreams[0]);
780
994
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
781
995
  const resolved = await writer.resolveTables({
782
996
  connection_id: 1,
@@ -803,7 +1017,7 @@ streams:
803
1017
  { storageVersion }
804
1018
  )
805
1019
  );
806
- const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
1020
+ const firstStorage = (await getTestStorage(factory, first)) as MongoSyncBucketStorage;
807
1021
  await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
808
1022
  await firstWriter.markAllSnapshotDone('1/1');
809
1023
  await firstWriter.commit('1/1');
@@ -823,7 +1037,7 @@ streams:
823
1037
  );
824
1038
  expect(second.replicationStreamId).toEqual(first.replicationStreamId);
825
1039
 
826
- const bucketStorage = factory.getInstance(second);
1040
+ const bucketStorage = await getTestStorage(factory, second);
827
1041
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
828
1042
  const source = sourceDescriptor('todos', { objectId: 'todos-relation' });
829
1043
 
@@ -838,7 +1052,10 @@ streams:
838
1052
  await writer.markAllSnapshotDone('2/1');
839
1053
  await writer.commit('2/1');
840
1054
 
841
- const activeStorage = (await factory.getActiveSyncConfig())?.storage as MongoSyncBucketStorage;
1055
+ const activeStorage = (await getTestStorage(
1056
+ factory,
1057
+ (await factory.getActiveSyncConfig())!.replicationStream
1058
+ )) as MongoSyncBucketStorage;
842
1059
  await using activeWriter = await activeStorage.createWriter(test_utils.BATCH_OPTIONS);
843
1060
  const activeStatus = await activeWriter.getSourceTableStatus(resolved.tables[0]);
844
1061
  expect(activeStatus?.bucketDataSources).toHaveLength(1);
@@ -862,7 +1079,7 @@ streams:
862
1079
  { storageVersion }
863
1080
  )
864
1081
  );
865
- const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
1082
+ const firstStorage = (await getTestStorage(factory, first)) as MongoSyncBucketStorage;
866
1083
  await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
867
1084
  const source = sourceDescriptor('todos', { objectId: 'todos-relation' });
868
1085
  const firstResolved = await firstWriter.resolveTables({
@@ -891,7 +1108,7 @@ streams:
891
1108
 
892
1109
  const deploying = await factory.getDeployingSyncConfig();
893
1110
  expect(deploying?.replicationStream.replicationStreamId).toBe(first.replicationStreamId);
894
- const bucketStorage = deploying!.storage;
1111
+ const bucketStorage = await getTestStorage(factory, deploying!.replicationStream);
895
1112
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
896
1113
  const resolved = await writer.resolveTables({
897
1114
  connection_id: 1,
@@ -937,7 +1154,7 @@ streams:
937
1154
  `;
938
1155
 
939
1156
  const first = await factory.updateSyncRules(updateSyncRulesFromYaml(ownerRules, { storageVersion }));
940
- const firstStorage = factory.getInstance(first);
1157
+ const firstStorage = await getTestStorage(factory, first);
941
1158
  await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
942
1159
  await firstWriter.markAllSnapshotDone('1/1');
943
1160
  await firstWriter.commit('1/1');
@@ -989,7 +1206,7 @@ streams:
989
1206
  { storageVersion }
990
1207
  )
991
1208
  );
992
- const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
1209
+ const firstStorage = (await getTestStorage(factory, first)) as MongoSyncBucketStorage;
993
1210
  await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
994
1211
  await firstWriter.markAllSnapshotDone('1/1');
995
1212
  await firstWriter.commit('1/1');
@@ -1011,7 +1228,7 @@ streams:
1011
1228
  );
1012
1229
  expect(second.replicationStreamId).toBe(first.replicationStreamId);
1013
1230
 
1014
- const bucketStorage = factory.getInstance(second);
1231
+ const bucketStorage = await getTestStorage(factory, second);
1015
1232
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1016
1233
  const resolved = await writer.resolveTables({
1017
1234
  connection_id: 1,
@@ -1062,7 +1279,7 @@ streams:
1062
1279
  { storageVersion }
1063
1280
  )
1064
1281
  );
1065
- const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
1282
+ const firstStorage = (await getTestStorage(factory, first)) as MongoSyncBucketStorage;
1066
1283
  await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
1067
1284
  await firstWriter.markAllSnapshotDone('1/1');
1068
1285
  await firstWriter.commit('1/1');
@@ -1105,7 +1322,7 @@ streams:
1105
1322
  expect(replicatingStreams[0].replicationJobId).toContain(config.syncConfigId);
1106
1323
  }
1107
1324
 
1108
- const secondStorage = factory.getInstance(replicatingStreams[0]) as MongoSyncBucketStorage;
1325
+ const secondStorage = (await getTestStorage(factory, replicatingStreams[0])) as MongoSyncBucketStorage;
1109
1326
  await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
1110
1327
  await secondWriter.markAllSnapshotDone('2/1');
1111
1328
  await secondWriter.commit('2/1');
@@ -1130,6 +1347,100 @@ streams:
1130
1347
  }
1131
1348
  );
1132
1349
 
1350
+ test.runIf(storageVersion >= 3)(
1351
+ 'restart creates a replacement stream instead of appending to the active stream',
1352
+ async () => {
1353
+ await using factory = await storageConfig.factory();
1354
+
1355
+ const active = await factory.updateSyncRules(
1356
+ updateSyncRulesFromYaml(
1357
+ `
1358
+ config:
1359
+ edition: 3
1360
+
1361
+ streams:
1362
+ by_owner:
1363
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
1364
+ `,
1365
+ { storageVersion }
1366
+ )
1367
+ );
1368
+ const activeStorage = (await getTestStorage(factory, active)) as MongoSyncBucketStorage;
1369
+ await using activeWriter = await activeStorage.createWriter(test_utils.BATCH_OPTIONS);
1370
+ await activeWriter.markAllSnapshotDone('1/1');
1371
+ await activeWriter.commit('1/1');
1372
+
1373
+ await factory.restartReplication(active.replicationStreamId);
1374
+
1375
+ const replicatingStreams = await factory.getReplicatingReplicationStreams();
1376
+ expect(replicatingStreams).toHaveLength(1);
1377
+ expect(replicatingStreams[0].replicationStreamId).not.toEqual(active.replicationStreamId);
1378
+ expect(replicatingStreams[0].state).toBe(storage.SyncRuleState.PROCESSING);
1379
+
1380
+ const oldStream = (await (factory as MongoBucketStorage).db.sync_rules.findOne({
1381
+ _id: active.replicationStreamId
1382
+ })) as ReplicationStreamDocumentV3;
1383
+ expect(oldStream.state).toBe(storage.SyncRuleState.ERRORED);
1384
+ expect(oldStream.sync_configs.map((config) => config.state)).toEqual([storage.SyncRuleState.ERRORED]);
1385
+ }
1386
+ );
1387
+
1388
+ test.runIf(storageVersion >= 3)('restart errors an active stream with an embedded deploying config', async () => {
1389
+ await using factory = await storageConfig.factory();
1390
+
1391
+ const active = await factory.updateSyncRules(
1392
+ updateSyncRulesFromYaml(
1393
+ `
1394
+ config:
1395
+ edition: 3
1396
+
1397
+ streams:
1398
+ by_owner:
1399
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
1400
+ `,
1401
+ { storageVersion }
1402
+ )
1403
+ );
1404
+ const activeStorage = (await getTestStorage(factory, active)) as MongoSyncBucketStorage;
1405
+ await using activeWriter = await activeStorage.createWriter(test_utils.BATCH_OPTIONS);
1406
+ await activeWriter.markAllSnapshotDone('1/1');
1407
+ await activeWriter.commit('1/1');
1408
+
1409
+ const deploying = await factory.updateSyncRules(
1410
+ updateSyncRulesFromYaml(
1411
+ `
1412
+ config:
1413
+ edition: 3
1414
+
1415
+ streams:
1416
+ by_project:
1417
+ query: SELECT * FROM todos WHERE project_id = subscription.parameter('project_id')
1418
+ `,
1419
+ { storageVersion }
1420
+ )
1421
+ );
1422
+ expect(deploying.replicationStreamId).toEqual(active.replicationStreamId);
1423
+
1424
+ await factory.restartReplication(active.replicationStreamId);
1425
+
1426
+ const replicatingStreams = await factory.getReplicatingReplicationStreams();
1427
+ expect(replicatingStreams).toHaveLength(1);
1428
+ expect(replicatingStreams[0].replicationStreamId).not.toEqual(active.replicationStreamId);
1429
+
1430
+ const oldStream = (await (factory as MongoBucketStorage).db.sync_rules.findOne({
1431
+ _id: active.replicationStreamId
1432
+ })) as ReplicationStreamDocumentV3;
1433
+ expect(oldStream.state).toBe(storage.SyncRuleState.ERRORED);
1434
+ expect(oldStream.sync_configs.map((config) => config.state)).toEqual([
1435
+ storage.SyncRuleState.ERRORED,
1436
+ storage.SyncRuleState.STOP
1437
+ ]);
1438
+ const oldActive = await factory.getActiveSyncConfig();
1439
+ expect(oldActive).not.toBeNull();
1440
+ expect(oldActive!.replicationStream.syncConfigContent).toHaveLength(1);
1441
+ await expect(oldActive!.storage.getCheckpoint()).resolves.toBeDefined();
1442
+ });
1443
+
1133
1444
  test.runIf(storageVersion >= 3)(
1134
1445
  'appended compatible config adopts the stream checkpoint instead of regressing to 0',
1135
1446
  async () => {
@@ -1149,7 +1460,7 @@ streams:
1149
1460
  { storageVersion }
1150
1461
  )
1151
1462
  );
1152
- const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
1463
+ const firstStorage = (await getTestStorage(factory, first)) as MongoSyncBucketStorage;
1153
1464
  await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
1154
1465
  const sourceTable = await test_utils.resolveTestTable(
1155
1466
  firstWriter,
@@ -1199,7 +1510,7 @@ streams:
1199
1510
 
1200
1511
  const replicatingStreams = await factory.getReplicatingReplicationStreams();
1201
1512
  expect(replicatingStreams).toHaveLength(1);
1202
- const secondStorage = factory.getInstance(replicatingStreams[0]) as MongoSyncBucketStorage;
1513
+ const secondStorage = (await getTestStorage(factory, replicatingStreams[0])) as MongoSyncBucketStorage;
1203
1514
  await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
1204
1515
  // No new data replicated - just complete the snapshot and commit.
1205
1516
  await secondWriter.markAllSnapshotDone('2/1');
@@ -1218,7 +1529,10 @@ streams:
1218
1529
  expect(head).toBeGreaterThanOrEqual(firstCheckpoint);
1219
1530
 
1220
1531
  // After activation, the active config's checkpoint does not regress.
1221
- const activeStorage = (await factory.getActiveSyncConfig())?.storage as MongoSyncBucketStorage;
1532
+ const activeStorage = (await getTestStorage(
1533
+ factory,
1534
+ (await factory.getActiveSyncConfig())!.replicationStream
1535
+ )) as MongoSyncBucketStorage;
1222
1536
  const activeCheckpoint = (await activeStorage.getCheckpoint()).checkpoint;
1223
1537
  expect(activeCheckpoint).toBeGreaterThanOrEqual(firstCheckpoint);
1224
1538
  }
@@ -1240,7 +1554,7 @@ streams:
1240
1554
  { storageVersion }
1241
1555
  )
1242
1556
  );
1243
- const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
1557
+ const firstStorage = (await getTestStorage(factory, first)) as MongoSyncBucketStorage;
1244
1558
  await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
1245
1559
  await firstWriter.markAllSnapshotDone('1/1');
1246
1560
  await firstWriter.commit('1/1');
@@ -1287,7 +1601,7 @@ streams:
1287
1601
  { storageVersion }
1288
1602
  )
1289
1603
  );
1290
- const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
1604
+ const firstStorage = (await getTestStorage(factory, first)) as MongoSyncBucketStorage;
1291
1605
  await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
1292
1606
  await firstWriter.markAllSnapshotDone('1/1');
1293
1607
  await firstWriter.commit('1/1');
@@ -1302,7 +1616,7 @@ streams:
1302
1616
  await using factory = await storageConfig.factory();
1303
1617
 
1304
1618
  const first = await factory.updateSyncRules(updateSyncRulesFromYaml(MINIMAL_SYNC_RULES, { storageVersion }));
1305
- const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
1619
+ const firstStorage = (await getTestStorage(factory, first)) as MongoSyncBucketStorage;
1306
1620
  await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
1307
1621
  await firstWriter.markAllSnapshotDone('1/1');
1308
1622
  await firstWriter.commit('1/1');
@@ -1375,7 +1689,7 @@ streams:
1375
1689
  const first = await factory.updateSyncRules(updateSyncRulesFromYaml(firstRules, { storageVersion, lock: true }));
1376
1690
  expect(first.current_lock?.sync_rules_id).toBe(first.replicationStreamId);
1377
1691
  try {
1378
- const firstStorage = factory.getInstance(first);
1692
+ const firstStorage = factory.getInstance(first, { replicationLock: first.current_lock! });
1379
1693
  await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
1380
1694
  await firstWriter.markAllSnapshotDone('1/1');
1381
1695
  await firstWriter.commit('1/1');
@@ -1403,7 +1717,7 @@ streams:
1403
1717
  { storageVersion }
1404
1718
  )
1405
1719
  );
1406
- const bucketStorage = factory.getInstance(syncRules);
1720
+ const bucketStorage = await getTestStorage(factory, syncRules);
1407
1721
 
1408
1722
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1409
1723
  const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], INITIALIZED_MONGO_STORAGE_FACTORY);
@@ -1442,7 +1756,7 @@ streams:
1442
1756
  { storageVersion }
1443
1757
  )
1444
1758
  );
1445
- const bucketStorage = factory.getInstance(syncRules);
1759
+ const bucketStorage = await getTestStorage(factory, syncRules);
1446
1760
 
1447
1761
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1448
1762
  const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], INITIALIZED_MONGO_STORAGE_FACTORY);
@@ -1476,7 +1790,7 @@ streams:
1476
1790
  storageVersion
1477
1791
  })
1478
1792
  );
1479
- const bucketStorage = factory.getInstance(syncRules);
1793
+ const bucketStorage = await getTestStorage(factory, syncRules);
1480
1794
  const mongoFactory = factory as MongoBucketStorage;
1481
1795
  const sourceTableId = new bson.ObjectId();
1482
1796
  const documents = Array.from({ length: 10_002 }, (_, index) => ({
@@ -1519,7 +1833,7 @@ streams:
1519
1833
  storageVersion
1520
1834
  })
1521
1835
  );
1522
- const bucketStorage = factory.getInstance(syncRules);
1836
+ const bucketStorage = await getTestStorage(factory, syncRules);
1523
1837
  const mongoFactory = factory as MongoBucketStorage;
1524
1838
  const sourceTableId = new bson.ObjectId();
1525
1839
  const documents = Array.from({ length: 10_002 }, (_, index) => ({
@@ -1570,7 +1884,7 @@ streams:
1570
1884
  { storageVersion }
1571
1885
  )
1572
1886
  );
1573
- const bucketStorage = factory.getInstance(syncRules);
1887
+ const bucketStorage = await getTestStorage(factory, syncRules);
1574
1888
  const metricsBefore = await factory.getStorageMetrics();
1575
1889
 
1576
1890
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
@@ -1595,6 +1909,22 @@ streams:
1595
1909
  expect(metricsAfter.replication_size_bytes).toBeGreaterThan(metricsBefore.replication_size_bytes);
1596
1910
  });
1597
1911
 
1912
+ test.runIf(storageVersion >= 3)('storage metrics include the sync config version label', async () => {
1913
+ await using factory = await storageConfig.factory();
1914
+ const syncRules = await factory.updateSyncRules(
1915
+ updateSyncRulesFromYaml(MINIMAL_SYNC_RULES, { storageVersion, version_label: 'metrics-test' })
1916
+ );
1917
+
1918
+ const metrics = await factory.getStorageMetrics();
1919
+
1920
+ expect(metrics.sync_config_metrics).toContainEqual(
1921
+ expect.objectContaining({
1922
+ sync_config_id: syncRules.syncConfigContent[0].syncConfigId,
1923
+ version_label: 'metrics-test'
1924
+ })
1925
+ );
1926
+ });
1927
+
1598
1928
  test.runIf(storageVersion >= 3)(
1599
1929
  'loads parameter checkpoint changes across all v3 parameter index collections',
1600
1930
  async () => {
@@ -1617,7 +1947,7 @@ streams:
1617
1947
  { storageVersion }
1618
1948
  )
1619
1949
  );
1620
- const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
1950
+ const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
1621
1951
  const previousCheckpoint = await bucketStorage.getCheckpoint();
1622
1952
 
1623
1953
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
@@ -1647,7 +1977,7 @@ streams:
1647
1977
  }
1648
1978
  );
1649
1979
 
1650
- test.runIf(storageVersion == 3)('cleans pending deletes only for tracked v3 source tables', async () => {
1980
+ test.runIf(storageVersion >= 3)('cleans pending deletes only for tracked v3 source tables', async () => {
1651
1981
  await using factory = await storageConfig.factory();
1652
1982
  const syncRules = await factory.updateSyncRules(
1653
1983
  updateSyncRulesFromYaml(
@@ -1662,7 +1992,7 @@ streams:
1662
1992
  );
1663
1993
 
1664
1994
  const mongoFactory = factory as MongoBucketStorage;
1665
- const bucketStorage = mongoFactory.getInstance(syncRules);
1995
+ const bucketStorage = await getTestStorage(mongoFactory, syncRules);
1666
1996
  const db = bucketStorage.db as VersionedPowerSyncMongoV3;
1667
1997
  await db.initializeStreamStorage(syncRules.replicationStreamId);
1668
1998
 
@@ -1680,6 +2010,7 @@ streams:
1680
2010
  snapshot_status: undefined,
1681
2011
  bucket_data_source_ids: [],
1682
2012
  parameter_lookup_source_ids: [],
2013
+ event_definition_ids: [],
1683
2014
  latest_pending_delete: 9n
1684
2015
  },
1685
2016
  {
@@ -1693,6 +2024,7 @@ streams:
1693
2024
  snapshot_status: undefined,
1694
2025
  bucket_data_source_ids: [],
1695
2026
  parameter_lookup_source_ids: [],
2027
+ event_definition_ids: [],
1696
2028
  latest_pending_delete: 12n
1697
2029
  }
1698
2030
  ]);
@@ -1755,7 +2087,7 @@ describe('sync - mongodb', () => {
1755
2087
  const legacySyncRules = await factory.updateSyncRules(
1756
2088
  updateSyncRulesFromYaml(MINIMAL_SYNC_RULES, { storageVersion: storage.LEGACY_STORAGE_VERSION })
1757
2089
  );
1758
- const legacyStorage = factory.getInstance(legacySyncRules);
2090
+ const legacyStorage = await getTestStorage(factory, legacySyncRules);
1759
2091
  await using legacyWriter = await legacyStorage.createWriter(test_utils.BATCH_OPTIONS);
1760
2092
  await legacyWriter.markAllSnapshotDone('1/1');
1761
2093
  await legacyWriter.commit('1/1');
@@ -1767,7 +2099,7 @@ describe('sync - mongodb', () => {
1767
2099
  const v3SyncRules = await factory.updateSyncRules(
1768
2100
  updateSyncRulesFromYaml(MINIMAL_SYNC_RULES, { storageVersion: storage.STORAGE_VERSION_3 })
1769
2101
  );
1770
- const v3Storage = factory.getInstance(v3SyncRules);
2102
+ const v3Storage = await getTestStorage(factory, v3SyncRules);
1771
2103
  await using v3Writer = await v3Storage.createWriter(test_utils.BATCH_OPTIONS);
1772
2104
  await v3Writer.markAllSnapshotDone('2/1');
1773
2105
  await v3Writer.commit('2/1');
@@ -1781,7 +2113,7 @@ describe('sync - mongodb', () => {
1781
2113
  describe(`storage v${storageVersion}`, () => {
1782
2114
  registerSyncStorageTests(INITIALIZED_MONGO_STORAGE_FACTORY, storageVersion);
1783
2115
 
1784
- describe.runIf(storageVersion == 3)('V3 read filtering boundaries', () => {
2116
+ describe.runIf(storageVersion >= 3)('V3 read filtering boundaries', () => {
1785
2117
  async function setupFilteringTest() {
1786
2118
  await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
1787
2119
  const syncRules = await factory.updateSyncRules(
@@ -1795,7 +2127,7 @@ describe('sync - mongodb', () => {
1795
2127
  { storageVersion }
1796
2128
  )
1797
2129
  );
1798
- const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
2130
+ const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
1799
2131
  const db = bucketStorage.db as VersionedPowerSyncMongoV3;
1800
2132
  const mapping = syncRules.syncConfigContent[0].mapping;
1801
2133
 
@@ -1927,7 +2259,7 @@ describe('sync - mongodb', () => {
1927
2259
  { storageVersion }
1928
2260
  )
1929
2261
  );
1930
- const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorageV3;
2262
+ const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorageV3;
1931
2263
  const db = bucketStorage.db as VersionedPowerSyncMongoV3;
1932
2264
 
1933
2265
  const start = 5n;