@powersync/service-module-mongodb-storage 0.18.2 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.d.ts +3 -0
  3. package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js +41 -0
  4. package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js.map +1 -0
  5. package/dist/storage/MongoBucketStorage.d.ts +5 -1
  6. package/dist/storage/MongoBucketStorage.js +5 -1
  7. package/dist/storage/MongoBucketStorage.js.map +1 -1
  8. package/dist/storage/implementation/MongoBucketBatch.d.ts +3 -0
  9. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  10. package/dist/storage/implementation/MongoChecksums.d.ts +2 -0
  11. package/dist/storage/implementation/MongoChecksums.js +3 -2
  12. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  13. package/dist/storage/implementation/MongoCompactor.d.ts +15 -19
  14. package/dist/storage/implementation/MongoCompactor.js +68 -260
  15. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  16. package/dist/storage/implementation/MongoStorageProvider.js +26 -3
  17. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  18. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +12 -5
  19. package/dist/storage/implementation/MongoSyncBucketStorage.js +25 -14
  20. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  21. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +2 -1
  22. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +146 -34
  23. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  24. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +2 -0
  25. package/dist/storage/implementation/common/PersistedBatch.d.ts +6 -0
  26. package/dist/storage/implementation/common/PersistedBatch.js +15 -8
  27. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  28. package/dist/storage/implementation/models.d.ts +16 -0
  29. package/dist/storage/implementation/models.js.map +1 -1
  30. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +11 -2
  31. package/dist/storage/implementation/v1/MongoCompactorV1.js +250 -1
  32. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  33. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +20 -11
  34. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  35. package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
  36. package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
  37. package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
  38. package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
  39. package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
  40. package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
  41. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +3 -1
  42. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  43. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +1 -0
  44. package/dist/storage/implementation/v3/MongoChecksumsV3.js +59 -103
  45. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  46. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -2
  47. package/dist/storage/implementation/v3/MongoCompactorV3.js +354 -219
  48. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  49. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +5 -0
  50. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +12 -11
  51. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
  52. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +5 -3
  53. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +109 -106
  54. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  55. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +5 -2
  56. package/dist/storage/implementation/v3/PersistedBatchV3.js +53 -15
  57. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  58. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +2 -1
  59. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +3 -0
  60. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  61. package/dist/storage/implementation/v3/bucket-format.d.ts +10 -1
  62. package/dist/storage/implementation/v3/bucket-format.js +28 -9
  63. package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
  64. package/dist/storage/implementation/v3/chunking.js +3 -1
  65. package/dist/storage/implementation/v3/chunking.js.map +1 -1
  66. package/dist/storage/implementation/v3/models.d.ts +38 -1
  67. package/dist/storage/implementation/v3/models.js.map +1 -1
  68. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +21 -0
  69. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +44 -0
  70. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
  71. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +30 -0
  72. package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
  73. package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
  74. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +70 -0
  75. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +146 -0
  76. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
  77. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +42 -0
  78. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +285 -0
  79. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
  80. package/dist/types/types.d.ts +35 -0
  81. package/dist/types/types.js +35 -1
  82. package/dist/types/types.js.map +1 -1
  83. package/dist/utils/test-utils.js +3 -1
  84. package/dist/utils/test-utils.js.map +1 -1
  85. package/dist/utils/util.d.ts +8 -1
  86. package/dist/utils/util.js +125 -2
  87. package/dist/utils/util.js.map +1 -1
  88. package/package.json +10 -7
  89. package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
  90. package/src/storage/MongoBucketStorage.ts +10 -2
  91. package/src/storage/implementation/MongoBucketBatch.ts +4 -0
  92. package/src/storage/implementation/MongoChecksums.ts +5 -2
  93. package/src/storage/implementation/MongoCompactor.ts +90 -314
  94. package/src/storage/implementation/MongoStorageProvider.ts +32 -3
  95. package/src/storage/implementation/MongoSyncBucketStorage.ts +34 -24
  96. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +164 -37
  97. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +2 -0
  98. package/src/storage/implementation/common/PersistedBatch.ts +22 -10
  99. package/src/storage/implementation/models.ts +16 -0
  100. package/src/storage/implementation/v1/MongoCompactorV1.ts +292 -8
  101. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +54 -40
  102. package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
  103. package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
  104. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +3 -1
  105. package/src/storage/implementation/v3/MongoChecksumsV3.ts +75 -105
  106. package/src/storage/implementation/v3/MongoCompactorV3.ts +456 -258
  107. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +21 -19
  108. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +123 -126
  109. package/src/storage/implementation/v3/PersistedBatchV3.ts +69 -16
  110. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +5 -0
  111. package/src/storage/implementation/v3/bucket-format.ts +36 -9
  112. package/src/storage/implementation/v3/chunking.ts +3 -1
  113. package/src/storage/implementation/v3/models.ts +40 -1
  114. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +54 -0
  115. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +29 -0
  116. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +171 -0
  117. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +343 -0
  118. package/src/types/types.ts +42 -1
  119. package/src/utils/test-utils.ts +3 -1
  120. package/src/utils/util.ts +182 -4
  121. package/test/src/__snapshots__/storage_sync.test.ts.snap +1 -583
  122. package/test/src/cleanup-stopped-sync-configs.test.ts +24 -1
  123. package/test/src/helpers/MemoryObjectStorage.ts +53 -0
  124. package/test/src/helpers/s3TestFactory.ts +69 -0
  125. package/test/src/object_storage_lifecycle.test.ts +59 -0
  126. package/test/src/setup.ts +6 -1
  127. package/test/src/storage.test.ts +216 -2
  128. package/test/src/storage_compacting.test.ts +176 -187
  129. package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
  130. package/test/src/storage_s3_checksums.test.ts +191 -0
  131. package/test/src/storage_s3_compaction_lifecycle.test.ts +417 -0
  132. package/test/src/storage_s3_reading.test.ts +526 -0
  133. package/test/src/storage_s3_writing.test.ts +135 -0
  134. package/test/src/storage_sync.test.ts +95 -5
  135. package/tsconfig.tsbuildinfo +1 -1
  136. package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
  137. package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
  138. package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
  139. package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
  140. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -84
  141. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
  142. package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
  143. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -108
@@ -0,0 +1,116 @@
1
+ import { mongoTestStorageFactoryGenerator } from '@module/utils/test-utils.js';
2
+ import { storage, updateSyncRulesFromYaml } from '@powersync/service-core';
3
+ import { bucketRequest, test_utils } from '@powersync/service-core-tests';
4
+ import { describe, expect, test } from 'vitest';
5
+ import { DEFAULT_INLINE_THRESHOLD_BYTES } from '../../src/storage/implementation/common/PersistedBatch.js';
6
+ import { MongoSyncBucketStorage } from '../../src/storage/implementation/createMongoSyncBucketStorage.js';
7
+ import { VersionedPowerSyncMongoV3 } from '../../src/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
8
+ import { env } from './env.js';
9
+ import { MemoryObjectStorage } from './helpers/MemoryObjectStorage.js';
10
+
11
+ const SYNC_RULES_YAML = `
12
+ bucket_definitions:
13
+ global:
14
+ data:
15
+ - SELECT id, description FROM items
16
+ `;
17
+
18
+ function s3Factory(threshold?: number) {
19
+ const memoryStorage = new MemoryObjectStorage();
20
+ const factoryGen = mongoTestStorageFactoryGenerator({
21
+ url: env.MONGO_TEST_URL,
22
+ isCI: env.CI,
23
+ objectStorage: memoryStorage,
24
+ inlineThresholdBytes: threshold
25
+ });
26
+ return { memoryStorage, factoryGen };
27
+ }
28
+
29
+ describe('Object storage inline threshold', () => {
30
+ test('stores small documents inline using the default threshold', async () => {
31
+ const { memoryStorage, factoryGen } = s3Factory();
32
+ await using factory = await factoryGen.factory();
33
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
34
+ const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
35
+ expect(bucketStorage.inlineThresholdBytes).toBe(DEFAULT_INLINE_THRESHOLD_BYTES);
36
+
37
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
38
+ const sourceTable = await test_utils.resolveTestTable(writer, 'items', ['id'], factoryGen, 1);
39
+ await writer.markAllSnapshotDone('1/1');
40
+
41
+ await writer.save({
42
+ sourceTable,
43
+ tag: storage.SaveOperationTag.INSERT,
44
+ after: { id: 'small1', description: 'tiny' },
45
+ afterReplicaId: test_utils.rid('small1')
46
+ });
47
+ await writer.save({
48
+ sourceTable,
49
+ tag: storage.SaveOperationTag.INSERT,
50
+ after: { id: 'small2', description: 'tiny' },
51
+ afterReplicaId: test_utils.rid('small2')
52
+ });
53
+ await writer.commit('1/1');
54
+ const checkpoint = await bucketStorage.getCheckpoint();
55
+ const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);
56
+
57
+ const db = bucketStorage.db as VersionedPowerSyncMongoV3;
58
+ const definitionId = syncRules.syncConfigContent[0].mapping.allBucketDefinitionIds()[0];
59
+ const documents = await db
60
+ .bucketData(bucketStorage.replicationStreamId, definitionId)
61
+ .find({ '_id.b': request.bucket })
62
+ .toArray();
63
+ expect(documents).not.toHaveLength(0);
64
+ for (const document of documents) {
65
+ expect(document.size).toBeLessThanOrEqual(DEFAULT_INLINE_THRESHOLD_BYTES);
66
+ expect(document.ops).toBeDefined();
67
+ expect(document.storage_ref).toBeUndefined();
68
+ }
69
+ expect(memoryStorage.store.size).toBe(0);
70
+
71
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
72
+ const data = test_utils.getBatchData(batch);
73
+ expect(data.map((op) => op.object_id)).toEqual(['small1', 'small2']);
74
+ });
75
+
76
+ test('stores documents above a configured threshold in object storage', async () => {
77
+ const { memoryStorage, factoryGen } = s3Factory(256);
78
+ await using factory = await factoryGen.factory();
79
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
80
+ const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
81
+ expect(bucketStorage.inlineThresholdBytes).toBe(256);
82
+
83
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
84
+ const sourceTable = await test_utils.resolveTestTable(writer, 'items', ['id'], factoryGen, 1);
85
+ await writer.markAllSnapshotDone('1/1');
86
+
87
+ await writer.save({
88
+ sourceTable,
89
+ tag: storage.SaveOperationTag.INSERT,
90
+ after: { id: 'large', description: 'value'.repeat(500) },
91
+ afterReplicaId: test_utils.rid('large')
92
+ });
93
+ await writer.commit('1/1');
94
+ const checkpoint = await bucketStorage.getCheckpoint();
95
+ const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);
96
+
97
+ const db = bucketStorage.db as VersionedPowerSyncMongoV3;
98
+ const definitionId = syncRules.syncConfigContent[0].mapping.allBucketDefinitionIds()[0];
99
+ const documents = await db
100
+ .bucketData(bucketStorage.replicationStreamId, definitionId)
101
+ .find({ '_id.b': request.bucket })
102
+ .toArray();
103
+ expect(documents).not.toHaveLength(0);
104
+ for (const document of documents) {
105
+ expect(document.size).toBeGreaterThan(256);
106
+ expect(document.ops).toBeUndefined();
107
+ expect(document.storage_ref).toBeDefined();
108
+ }
109
+ expect(new Set(memoryStorage.store.keys())).toEqual(
110
+ new Set(documents.map((document) => document.storage_ref!.path))
111
+ );
112
+
113
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
114
+ expect(test_utils.getBatchData(batch).map((op) => op.object_id)).toEqual(['large']);
115
+ });
116
+ });
@@ -0,0 +1,191 @@
1
+ import {
2
+ addChecksums,
3
+ CheckpointChecksumInvalidatedError,
4
+ storage,
5
+ updateSyncRulesFromYaml
6
+ } from '@powersync/service-core';
7
+ import { bucketRequest, test_utils } from '@powersync/service-core-tests';
8
+ import { describe, expect, test } from 'vitest';
9
+ import { MongoSyncBucketStorage } from '../../src/storage/implementation/createMongoSyncBucketStorage.js';
10
+ import { VersionedPowerSyncMongoV3 } from '../../src/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
11
+ import { env } from './env.js';
12
+ import { createS3TestStorageSuite } from './helpers/s3TestFactory.js';
13
+
14
+ const SYNC_RULES_YAML = `
15
+ bucket_definitions:
16
+ global:
17
+ data:
18
+ - SELECT id, description FROM items
19
+ `;
20
+
21
+ function s3Factory() {
22
+ const { objectStorage, factoryGen } = createS3TestStorageSuite({ url: env.MONGO_TEST_URL, isCI: env.CI });
23
+ return { memoryStorage: objectStorage, factoryGen };
24
+ }
25
+
26
+ describe('V3 checksums with S3 object storage', () => {
27
+ test('partial checksum with start straddling S3-backed document', async () => {
28
+ const { factoryGen } = s3Factory();
29
+ await using factory = await factoryGen.factory();
30
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
31
+ const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
32
+ const db = bucketStorage.db as VersionedPowerSyncMongoV3;
33
+
34
+ const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);
35
+ const bucket = request.bucket;
36
+ const definitionId = syncRules.syncConfigContent[0].mapping.allBucketDefinitionIds()[0];
37
+
38
+ // Write 6 ops through S3 writer — all land in one S3-backed doc
39
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
40
+ const sourceTable = await test_utils.resolveTestTable(writer, 'items', ['id'], factoryGen, 1);
41
+ await writer.markAllSnapshotDone('1/1');
42
+ for (const id of ['A', 'B', 'C', 'D', 'E', 'F']) {
43
+ await writer.save({
44
+ sourceTable,
45
+ tag: storage.SaveOperationTag.INSERT,
46
+ after: { id, description: id },
47
+ afterReplicaId: test_utils.rid(id)
48
+ });
49
+ }
50
+ await writer.commit('1/1');
51
+ const checkpoint = await bucketStorage.getCheckpoint();
52
+
53
+ // Baseline: full checksum with no compacted_state
54
+ const full = (await bucketStorage.getChecksums(checkpoint, [request])).get(bucket)!;
55
+
56
+ // Set compacted_state.op_id = 3 to create a partial range starting after op 3.
57
+ // The doc has min_op=1, _id.o=6 so is_fully_included=false for the range (3, 6].
58
+ await db.bucketState(bucketStorage.replicationStreamId).updateOne(
59
+ { _id: { d: definitionId, b: bucket } },
60
+ {
61
+ $set: {
62
+ last_op: 3n,
63
+ compacted_state: { op_id: 3n, count: 0, checksum: 0n, bytes: null },
64
+ estimate_since_compact: { count: 3, bytes: 100 }
65
+ }
66
+ },
67
+ { upsert: true }
68
+ );
69
+
70
+ const partial = (await bucketStorage.getChecksums(checkpoint, [request])).get(bucket)!;
71
+ expect(partial.checksum).toBe(full.checksum);
72
+ expect(partial.count).toBe(full.count);
73
+ });
74
+
75
+ test('partial checksum with end straddling S3-backed document invalidates the checkpoint', async () => {
76
+ const { factoryGen } = s3Factory();
77
+ await using factory = await factoryGen.factory();
78
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
79
+ const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
80
+ const db = bucketStorage.db as VersionedPowerSyncMongoV3;
81
+
82
+ const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);
83
+ const definitionId = syncRules.syncConfigContent[0].mapping.allBucketDefinitionIds()[0];
84
+
85
+ // Write several operations into one S3-backed document that straddles the
86
+ // requested checkpoint.
87
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
88
+ const sourceTable = await test_utils.resolveTestTable(writer, 'items', ['id'], factoryGen, 1);
89
+ await writer.markAllSnapshotDone('1/1');
90
+ for (let i = 1; i <= 12; i++) {
91
+ const id = `row${i}`;
92
+ await writer.save({
93
+ sourceTable,
94
+ tag: storage.SaveOperationTag.INSERT,
95
+ after: { id, description: `value${i}`.repeat(50) },
96
+ afterReplicaId: test_utils.rid(id)
97
+ });
98
+ }
99
+ await writer.commit('1/1');
100
+ const documents = await db
101
+ .bucketData(bucketStorage.replicationStreamId, definitionId)
102
+ .find({ '_id.b': request.bucket })
103
+ .toArray();
104
+ expect(documents).toHaveLength(1);
105
+ expect(documents[0].storage_ref).toBeDefined();
106
+ expect(documents[0].ops).toBeUndefined();
107
+
108
+ // The document starts before op 7 and ends after it.
109
+ await expect(bucketStorage.getChecksums(test_utils.testCheckpoint(7n), [request])).rejects.toBeInstanceOf(
110
+ CheckpointChecksumInvalidatedError
111
+ );
112
+ });
113
+
114
+ test('checksum preserved after CLEAR-producing S3 compaction', async () => {
115
+ const { factoryGen } = s3Factory();
116
+ await using factory = await factoryGen.factory();
117
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
118
+ const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
119
+ const db = bucketStorage.db as VersionedPowerSyncMongoV3;
120
+
121
+ const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);
122
+ const definitionId = syncRules.syncConfigContent[0].mapping.allBucketDefinitionIds()[0];
123
+
124
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
125
+ const sourceTable = await test_utils.resolveTestTable(writer, 'items', ['id'], factoryGen, 1);
126
+ await writer.markAllSnapshotDone('1/1');
127
+
128
+ // A@1, A@2 get superseded by A@4 (same replica_id for dedup). B@3 independent.
129
+ await writer.save({
130
+ sourceTable,
131
+ tag: storage.SaveOperationTag.INSERT,
132
+ after: { id: 'A', description: 'v1' },
133
+ afterReplicaId: test_utils.rid('A')
134
+ });
135
+ await writer.save({
136
+ sourceTable,
137
+ tag: storage.SaveOperationTag.UPDATE,
138
+ after: { id: 'A', description: 'v2' },
139
+ afterReplicaId: test_utils.rid('A')
140
+ });
141
+ await writer.save({
142
+ sourceTable,
143
+ tag: storage.SaveOperationTag.INSERT,
144
+ after: { id: 'B', description: 'beta' },
145
+ afterReplicaId: test_utils.rid('B')
146
+ });
147
+ await writer.save({
148
+ sourceTable,
149
+ tag: storage.SaveOperationTag.UPDATE,
150
+ after: { id: 'A', description: 'v4' },
151
+ afterReplicaId: test_utils.rid('A')
152
+ });
153
+ await writer.commit('1/1');
154
+ const checkpoint = await bucketStorage.getCheckpoint();
155
+
156
+ // Compact — produces CLEAR doc from collapsed MOVEs
157
+ await bucketStorage.compact({
158
+ maxOpId: checkpoint.checkpoint,
159
+ compactBuckets: [request.bucket],
160
+ clearBatchLimit: 200,
161
+ moveBatchLimit: 10,
162
+ moveBatchQueryLimit: 10,
163
+ moveBatchByteLimit: 1024,
164
+ minBucketChanges: 1,
165
+ minChangeRatio: 0
166
+ });
167
+
168
+ // Shift compacted_state.op_id back to before the CLEAR doc so getChecksums
169
+ // queries the pipeline (not just reads compacted_state directly).
170
+ await db
171
+ .bucketState(bucketStorage.replicationStreamId)
172
+ .updateOne({ _id: { d: definitionId, b: request.bucket } }, { $set: { 'compacted_state.op_id': 0n } });
173
+
174
+ // Ground truth: sum of doc-level checksum fields
175
+ const docs = await db
176
+ .bucketData(bucketStorage.replicationStreamId, definitionId)
177
+ .find({ '_id.b': request.bucket })
178
+ .toArray();
179
+ const groundTruth = docs.reduce((sum: number, d: any) => addChecksums(sum, Number(d.checksum)), 0);
180
+ expect(docs.some((doc) => doc.has_clear_op)).toBe(true);
181
+ expect(docs.every((doc) => doc.storage_ref != null && doc.ops == null)).toBe(true);
182
+
183
+ const checksum = (await bucketStorage.getChecksums(checkpoint, [request])).get(request.bucket)!;
184
+ expect(checksum.checksum).toBe(groundTruth);
185
+
186
+ // The two superseded A operations collapse into CLEAR.
187
+ const batchAfter = await test_utils.getBatchArray(bucketStorage.getBucketDataBatch(checkpoint, [request]));
188
+ const dataAfter = batchAfter.flatMap((chunk) => chunk.chunkData.data);
189
+ expect(dataAfter).toMatchObject([{ op: 'CLEAR' }, { object_id: 'B', op: 'PUT' }, { object_id: 'A', op: 'PUT' }]);
190
+ });
191
+ });