@powersync/service-module-mongodb-storage 0.21.0 → 0.23.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 (130) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +10 -1
  3. package/dist/storage/MongoBucketStorage.js +37 -7
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +24 -10
  6. package/dist/storage/implementation/MongoBucketBatch.js +134 -89
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoCompactor.d.ts +2 -0
  9. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  10. package/dist/storage/implementation/MongoIdSequence.d.ts +10 -1
  11. package/dist/storage/implementation/MongoIdSequence.js +14 -1
  12. package/dist/storage/implementation/MongoIdSequence.js.map +1 -1
  13. package/dist/storage/implementation/MongoOpIdAllocator.d.ts +27 -0
  14. package/dist/storage/implementation/MongoOpIdAllocator.js +97 -0
  15. package/dist/storage/implementation/MongoOpIdAllocator.js.map +1 -0
  16. package/dist/storage/implementation/MongoStorageProvider.js +4 -2
  17. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  18. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +5 -0
  19. package/dist/storage/implementation/MongoSyncBucketStorage.js +17 -0
  20. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  21. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +26 -2
  22. package/dist/storage/implementation/MongoSyncRulesLock.js +38 -1
  23. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  24. package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
  25. package/dist/storage/implementation/MongoWriteBatch.js +149 -0
  26. package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
  27. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +2 -2
  28. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  29. package/dist/storage/implementation/common/PersistedBatch.d.ts +5 -4
  30. package/dist/storage/implementation/common/PersistedBatch.js +6 -4
  31. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  32. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
  33. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
  34. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  35. package/dist/storage/implementation/db.d.ts +5 -0
  36. package/dist/storage/implementation/db.js +14 -0
  37. package/dist/storage/implementation/db.js.map +1 -1
  38. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +5 -0
  39. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +149 -139
  40. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  41. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +2 -1
  42. package/dist/storage/implementation/v1/MongoCompactorV1.js +23 -17
  43. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  44. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +1 -1
  45. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  46. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
  47. package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
  48. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
  49. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +6 -1
  50. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +148 -162
  51. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  52. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +10 -4
  53. package/dist/storage/implementation/v3/MongoCompactorV3.js +175 -76
  54. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  55. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +12 -8
  56. package/dist/storage/implementation/v3/PersistedBatchV3.js +25 -32
  57. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  58. package/dist/storage/implementation/v3/compact-utils.d.ts +3 -2
  59. package/dist/storage/implementation/v3/compact-utils.js +12 -5
  60. package/dist/storage/implementation/v3/compact-utils.js.map +1 -1
  61. package/dist/storage/implementation/v3/models.d.ts +1 -1
  62. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -1
  63. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
  64. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -3
  65. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +7 -9
  66. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
  67. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +3 -2
  68. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +4 -7
  69. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -1
  70. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +21 -3
  71. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
  72. package/dist/types/types.d.ts +5 -0
  73. package/dist/types/types.js +12 -0
  74. package/dist/types/types.js.map +1 -1
  75. package/dist/utils/test-utils.d.ts +2 -1
  76. package/dist/utils/test-utils.js +5 -3
  77. package/dist/utils/test-utils.js.map +1 -1
  78. package/package.json +7 -7
  79. package/src/storage/MongoBucketStorage.ts +47 -8
  80. package/src/storage/implementation/MongoBucketBatch.ts +205 -111
  81. package/src/storage/implementation/MongoCompactor.ts +2 -0
  82. package/src/storage/implementation/MongoIdSequence.ts +21 -1
  83. package/src/storage/implementation/MongoOpIdAllocator.ts +105 -0
  84. package/src/storage/implementation/MongoStorageProvider.ts +12 -2
  85. package/src/storage/implementation/MongoSyncBucketStorage.ts +19 -0
  86. package/src/storage/implementation/MongoSyncRulesLock.ts +56 -3
  87. package/src/storage/implementation/MongoWriteBatch.ts +172 -0
  88. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -2
  89. package/src/storage/implementation/common/PersistedBatch.ts +12 -8
  90. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
  91. package/src/storage/implementation/db.ts +15 -0
  92. package/src/storage/implementation/models.ts +1 -0
  93. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +167 -160
  94. package/src/storage/implementation/v1/MongoCompactorV1.ts +29 -26
  95. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +1 -1
  96. package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
  97. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +217 -242
  98. package/src/storage/implementation/v3/MongoCompactorV3.ts +284 -108
  99. package/src/storage/implementation/v3/PersistedBatchV3.ts +36 -36
  100. package/src/storage/implementation/v3/compact-utils.ts +16 -7
  101. package/src/storage/implementation/v3/models.ts +1 -1
  102. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +8 -1
  103. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +11 -10
  104. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +8 -9
  105. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +23 -3
  106. package/src/types/types.ts +18 -0
  107. package/src/utils/test-utils.ts +6 -3
  108. package/test/src/__snapshots__/storage.test.ts.snap +102 -2
  109. package/test/src/__snapshots__/storage_compacting.test.ts.snap +2 -2
  110. package/test/src/__snapshots__/storage_sync.test.ts.snap +12 -12
  111. package/test/src/chunk_compaction_config.test.ts +38 -0
  112. package/test/src/cleanup-stopped-sync-configs.test.ts +19 -13
  113. package/test/src/compact-utils.test.ts +33 -8
  114. package/test/src/concurrent_writers.test.ts +599 -0
  115. package/test/src/helpers/concurrentWriter.mjs +29 -0
  116. package/test/src/helpers/s3TestFactory.ts +12 -12
  117. package/test/src/object_storage_usage.test.ts +18 -5
  118. package/test/src/parameter_compacting_v1.test.ts +2 -2
  119. package/test/src/parameter_compacting_v3.test.ts +2 -2
  120. package/test/src/parameter_compaction_fence.test.ts +3 -3
  121. package/test/src/storage.test.ts +32 -33
  122. package/test/src/storage_compacting.test.ts +402 -24
  123. package/test/src/storage_object_storage_inline_threshold.test.ts +3 -3
  124. package/test/src/storage_s3_checksums.test.ts +4 -4
  125. package/test/src/storage_s3_compaction_lifecycle.test.ts +8 -8
  126. package/test/src/storage_s3_reading.test.ts +51 -5
  127. package/test/src/storage_s3_writing.test.ts +3 -3
  128. package/test/src/storage_sync.test.ts +52 -46
  129. package/test/src/storeCurrentData.test.ts +5 -5
  130. package/tsconfig.tsbuildinfo +1 -1
@@ -7,6 +7,7 @@ import { CompactionLease } from '@module/storage/implementation/v3/CompactionLea
7
7
  import { BucketDataDocumentV3 } from '@module/storage/implementation/v3/models.js';
8
8
  import { ObjectStorageError } from '@module/storage/implementation/v3/object-storage/ObjectStorage.js';
9
9
  import { VersionedPowerSyncMongoV3 } from '@module/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
10
+ import { mongoTestStorageFactoryGenerator } from '@module/utils/test-utils.js';
10
11
  import { replicaIdToSubkey } from '@module/utils/util.js';
11
12
  import { logger as defaultLogger } from '@powersync/lib-services-framework';
12
13
  import {
@@ -16,9 +17,11 @@ import {
16
17
  SyncRulesBucketStorage,
17
18
  updateSyncRulesFromYaml
18
19
  } from '@powersync/service-core';
19
- import { bucketRequest, compactActive, register, test_utils } from '@powersync/service-core-tests';
20
+ import { bucketRequest, compactActive, getTestStorage, register, test_utils } from '@powersync/service-core-tests';
20
21
  import * as bson from 'bson';
21
22
  import { describe, expect, test, vi } from 'vitest';
23
+ import { env } from './env.js';
24
+ import { MemoryObjectStorage } from './helpers/MemoryObjectStorage.js';
22
25
  import { INITIALIZED_MONGO_STORAGE_FACTORY, TEST_STORAGE_VERSIONS } from './util.js';
23
26
 
24
27
  function makeOp(
@@ -103,7 +106,7 @@ bucket_definitions:
103
106
  { storageVersion }
104
107
  )
105
108
  );
106
- const bucketStorage = factory.getInstance(syncRules);
109
+ const bucketStorage = await getTestStorage(factory, syncRules);
107
110
  const syncRulesContent = syncRules.syncConfigContent[0];
108
111
  const { checkpoint } = await populate(bucketStorage, 1);
109
112
 
@@ -161,15 +164,16 @@ bucket_definitions:
161
164
  { storageVersion }
162
165
  )
163
166
  );
164
- const bucketStorage = factory.getInstance(syncRules);
167
+ const bucketStorage = await getTestStorage(factory, syncRules);
165
168
  const syncRulesContent = syncRules.syncConfigContent[0];
166
169
 
167
170
  await populate(bucketStorage, 2);
168
171
  const { checkpoint } = await bucketStorage.getCheckpoint();
169
172
 
170
- // V3's initial lite pass processes every bucket with no prior compact state.
173
+ // Force V3's initial lite pass to process these small buckets for cache verification.
171
174
  // Earlier storage versions use the default minimum-change threshold.
172
175
  const result0 = await bucketStorage.compactInitialReplication({
176
+ forceChunkCompaction: true,
173
177
  maxOpId: checkpoint
174
178
  });
175
179
  expect(result0.buckets).toEqual(storageVersion >= storage.STORAGE_VERSION_3 ? 2 : 0);
@@ -177,6 +181,7 @@ bucket_definitions:
177
181
  // For V1/V2, lower the threshold to populate the checksum cache. V3 has
178
182
  // already updated its compacted state, so another initial pass is a no-op.
179
183
  const result1 = await bucketStorage.compactInitialReplication({
184
+ forceChunkCompaction: true,
180
185
  maxOpId: checkpoint,
181
186
  minBucketChanges: 1
182
187
  });
@@ -184,6 +189,7 @@ bucket_definitions:
184
189
 
185
190
  // Repeating it stays a no-op.
186
191
  const result2 = await bucketStorage.compactInitialReplication({
192
+ forceChunkCompaction: true,
187
193
  maxOpId: checkpoint,
188
194
  minBucketChanges: 1
189
195
  });
@@ -211,6 +217,7 @@ bucket_definitions:
211
217
  .createMongoCompactor({
212
218
  maxOpId: checkpoint,
213
219
  compactChunksOnly: true,
220
+ forceChunkCompaction: true,
214
221
  minCompactChunkIntervalMs: 1
215
222
  })
216
223
  .compact();
@@ -220,7 +227,10 @@ bucket_definitions:
220
227
 
221
228
  test('v3 repeated initial chunk compaction reschedules overdue full work beyond the current run', async () => {
222
229
  const { bucketStorage, checkpoint } = await setup(storage.STORAGE_VERSION_3);
223
- const firstResult = await bucketStorage.compactInitialReplication({ maxOpId: checkpoint });
230
+ const firstResult = await bucketStorage.compactInitialReplication({
231
+ forceChunkCompaction: true,
232
+ maxOpId: checkpoint
233
+ });
224
234
  expect(firstResult.buckets).toBe(2);
225
235
 
226
236
  const bucketStateCollection = (bucketStorage.db as VersionedPowerSyncMongoV3).bucketState(
@@ -240,6 +250,7 @@ bucket_definitions:
240
250
  .aggregate<{ now: Date }>([{ $documents: [{}] }, { $project: { _id: 0, now: '$$NOW' } }])
241
251
  .toArray();
242
252
  const result = await bucketStorage.compactInitialReplication({
253
+ forceChunkCompaction: true,
243
254
  maxOpId: checkpoint,
244
255
  signal: AbortSignal.timeout(2_000)
245
256
  });
@@ -286,7 +297,11 @@ bucket_definitions:
286
297
  });
287
298
 
288
299
  describe('Mongo Sync Parameter Storage Compact', () => {
289
- register.registerParameterCompactTests(INITIALIZED_MONGO_STORAGE_FACTORY);
300
+ for (const storageVersion of TEST_STORAGE_VERSIONS) {
301
+ describe(`storage v${storageVersion}`, () => {
302
+ register.registerParameterCompactTests({ ...INITIALIZED_MONGO_STORAGE_FACTORY, storageVersion });
303
+ });
304
+ }
290
305
  });
291
306
 
292
307
  /**
@@ -345,7 +360,7 @@ bucket_definitions:
345
360
  { storageVersion: 3 }
346
361
  )
347
362
  );
348
- const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
363
+ const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
349
364
  const db = bucketStorage.db as VersionedPowerSyncMongoV3;
350
365
  const mapping = syncRules.syncConfigContent[0].mapping;
351
366
  const definitionId = mapping.allBucketDefinitionIds()[0];
@@ -385,6 +400,91 @@ bucket_definitions:
385
400
  });
386
401
  }
387
402
 
403
+ test.each([1, 7])(
404
+ 'initial compaction bulk-reschedules buckets with %s chunks without reading data',
405
+ async (chunks) => {
406
+ const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId, db } = await setupV3Storage();
407
+ const documents = Array.from({ length: chunks }, (_, index) =>
408
+ serializeBucketData(BUCKET, [makeOp(index + 1, String(index), 'data', ctx, sourceTableId)])
409
+ );
410
+ await insertDocs(collection, documents);
411
+ await bucketStateCollection.insertOne({
412
+ _id: { d: ctx.definitionId, b: BUCKET },
413
+ last_op: BigInt(chunks),
414
+ next_compact_check: new Date(0),
415
+ // Even overdue full compaction must not force a small initial chunk merge.
416
+ first_uncompacted_write: new Date(0),
417
+ bucket_stats: { count: chunks, bytes: BigInt(documents.reduce((sum, doc) => sum + doc.size, 0)), chunks }
418
+ });
419
+ const claim = vi.spyOn(CompactionLease, 'claim');
420
+ const data = vi.spyOn(db, 'bucketData');
421
+ try {
422
+ expect(await bucketStorage.compactInitialReplication({ maxOpId: BigInt(chunks) })).toEqual({ buckets: 0 });
423
+ expect(claim).not.toHaveBeenCalled();
424
+ expect(data).not.toHaveBeenCalled();
425
+ const state = await bucketStateCollection.findOne({ _id: { d: ctx.definitionId, b: BUCKET } });
426
+ expect(state?.compacted_state).toBeUndefined();
427
+ expect(state?.compact_lease).toBeUndefined();
428
+ expect(state?.last_full_compact).toBeUndefined();
429
+ expect(state!.next_compact_check!.getTime()).toBeGreaterThan(Date.now());
430
+ expect(await collection.find().toArray()).toEqual(documents);
431
+ } finally {
432
+ claim.mockRestore();
433
+ data.mockRestore();
434
+ }
435
+ }
436
+ );
437
+
438
+ test('initial compaction rechecks the chunk threshold after claiming', async () => {
439
+ const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId, db } = await setupV3Storage();
440
+ const documents = Array.from({ length: 8 }, (_, index) =>
441
+ serializeBucketData(BUCKET, [makeOp(index + 1, String(index), 'data', ctx, sourceTableId)])
442
+ );
443
+ await insertDocs(collection, documents);
444
+ const bytes = BigInt(documents.reduce((sum, doc) => sum + doc.size, 0));
445
+ await bucketStateCollection.insertOne({
446
+ _id: { d: ctx.definitionId, b: BUCKET },
447
+ last_op: 8n,
448
+ next_compact_check: new Date(0),
449
+ first_uncompacted_write: new Date(),
450
+ bucket_stats: { count: 8, bytes, chunks: 8 }
451
+ });
452
+ const originalClaim = CompactionLease.claim.bind(CompactionLease);
453
+ const claim = vi.spyOn(CompactionLease, 'claim').mockImplementationOnce(async (...args) => {
454
+ // Another compactor finishes before we acquire the lease, leaving one
455
+ // new chunk beyond its cached prefix instead of the eight we selected.
456
+ await bucketStateCollection.updateOne(
457
+ { _id: { d: ctx.definitionId, b: BUCKET } },
458
+ {
459
+ $set: {
460
+ compacted_state: {
461
+ op_id: 7n,
462
+ checksum: 196n,
463
+ count: 7,
464
+ bytes: bytes - BigInt(documents[7].size),
465
+ chunks: 7,
466
+ at: new Date()
467
+ }
468
+ }
469
+ }
470
+ );
471
+ return originalClaim(...args);
472
+ });
473
+ const data = vi.spyOn(db, 'bucketData');
474
+ try {
475
+ expect(await bucketStorage.compactInitialReplication({ maxOpId: 8n })).toEqual({ buckets: 0 });
476
+ expect(claim).toHaveBeenCalledTimes(1);
477
+ expect(data).not.toHaveBeenCalled();
478
+ const state = await bucketStateCollection.findOne({ _id: { d: ctx.definitionId, b: BUCKET } });
479
+ expect(state?.compacted_state?.op_id).toBe(7n);
480
+ expect(state?.compact_lease).toBeUndefined();
481
+ expect(state!.next_compact_check!.getTime()).toBeGreaterThan(Date.now());
482
+ } finally {
483
+ claim.mockRestore();
484
+ data.mockRestore();
485
+ }
486
+ });
487
+
388
488
  test('a successful lease renewal clears a transient renewal error', async () => {
389
489
  const { bucketStateCollection, ctx } = await setupV3Storage();
390
490
  await insertBucketState(bucketStateCollection, ctx.definitionId, 1n);
@@ -446,7 +546,7 @@ bucket_definitions:
446
546
  );
447
547
 
448
548
  await (bucketStorage as MongoSyncBucketStorage)
449
- .createMongoCompactor({ maxOpId: 2n, compactChunksOnly: true })
549
+ .createMongoCompactor({ maxOpId: 2n, compactChunksOnly: true, forceChunkCompaction: true })
450
550
  .compact();
451
551
 
452
552
  const [badState, goodState] = await Promise.all([
@@ -460,7 +560,7 @@ bucket_definitions:
460
560
 
461
561
  test('aborting scheduled compaction does not reschedule the remaining batch', async () => {
462
562
  const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3Storage();
463
- const buckets = ['first[]', 'second[]', 'third[]'];
563
+ const buckets = Array.from({ length: 8 }, (_, index) => `bucket${index}[]`);
464
564
  const documents = buckets.map((bucket) =>
465
565
  serializeBucketData(bucket, [makeOp(2, bucket, bucket, { ...ctx, bucket }, sourceTableId)])
466
566
  );
@@ -490,6 +590,7 @@ bucket_definitions:
490
590
  .createMongoCompactor({
491
591
  maxOpId: 2n,
492
592
  compactChunksOnly: true,
593
+ forceChunkCompaction: true,
493
594
  signal: abortController.signal,
494
595
  logger: testLogger
495
596
  })
@@ -499,8 +600,11 @@ bucket_definitions:
499
600
  const states = await bucketStateCollection.find({ '_id.b': { $in: buckets } }).toArray();
500
601
  const completed = states.filter((state) => state.compacted_state != null);
501
602
  const remaining = states.filter((state) => state.compacted_state == null);
502
- expect(completed).toHaveLength(1);
503
- expect(remaining).toHaveLength(2);
603
+ // Already-started buckets may finish after abort, but the queued remainder
604
+ // must not be claimed or rescheduled.
605
+ expect(completed.length).toBeGreaterThanOrEqual(1);
606
+ expect(completed.length).toBeLessThanOrEqual(2);
607
+ expect(remaining.length).toBeGreaterThanOrEqual(6);
504
608
  for (const state of remaining) {
505
609
  expect(state.next_compact_check).toEqual(new Date(0));
506
610
  expect(state.compact_lease).toBeUndefined();
@@ -890,7 +994,7 @@ bucket_definitions:
890
994
  { storageVersion: 3 }
891
995
  )
892
996
  );
893
- const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
997
+ const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
894
998
  const db = bucketStorage.db as VersionedPowerSyncMongoV3;
895
999
  const mapping = syncRules.syncConfigContent[0].mapping;
896
1000
  const definitionId = mapping.allBucketDefinitionIds()[0];
@@ -1067,7 +1171,7 @@ bucket_definitions:
1067
1171
  { storageVersion: 3 }
1068
1172
  )
1069
1173
  );
1070
- const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
1174
+ const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
1071
1175
  const db = bucketStorage.db as VersionedPowerSyncMongoV3;
1072
1176
  const mapping = syncRules.syncConfigContent[0].mapping;
1073
1177
  const definitionId = mapping.allBucketDefinitionIds()[0];
@@ -1460,7 +1564,7 @@ bucket_definitions:
1460
1564
  { storageVersion: 3 }
1461
1565
  )
1462
1566
  );
1463
- const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
1567
+ const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
1464
1568
  const db = bucketStorage.db as VersionedPowerSyncMongoV3;
1465
1569
  const mapping = syncRules.syncConfigContent[0].mapping;
1466
1570
  const definitionId = mapping.allBucketDefinitionIds()[0];
@@ -1637,8 +1741,8 @@ bucket_definitions:
1637
1741
  describe('Streaming compactor', () => {
1638
1742
  const BUCKET = 'global[]';
1639
1743
 
1640
- async function setupV3() {
1641
- await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
1744
+ async function setupV3(factoryGenerator = INITIALIZED_MONGO_STORAGE_FACTORY) {
1745
+ await using factory = await factoryGenerator.factory();
1642
1746
  const syncRules = await factory.updateSyncRules(
1643
1747
  updateSyncRulesFromYaml(
1644
1748
  `
@@ -1649,7 +1753,7 @@ bucket_definitions:
1649
1753
  { storageVersion: 3 }
1650
1754
  )
1651
1755
  );
1652
- const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
1756
+ const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
1653
1757
  const db = bucketStorage.db as VersionedPowerSyncMongoV3;
1654
1758
  const definitionId = bucketStorage.storageIds.bucketDefinitionIds[0];
1655
1759
  const collection = db.bucketData(bucketStorage.replicationStreamId, definitionId);
@@ -1746,6 +1850,29 @@ bucket_definitions:
1746
1850
  expect(state?.bucket_stats).toEqual({ count: 4, bytes, chunks: documents.length });
1747
1851
  }
1748
1852
 
1853
+ test('initial compaction merges eight chunks immediately without full compaction', async () => {
1854
+ const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
1855
+ const inputs = Array.from({ length: 8 }, (_, index) =>
1856
+ serializeBucketData(BUCKET, [makeOp(index + 1, 'A', 'data', ctx, sourceTableId)])
1857
+ );
1858
+ await insertDocs(collection, inputs);
1859
+ await bucketStateCollection.insertOne({
1860
+ _id: { d: ctx.definitionId, b: BUCKET },
1861
+ last_op: 8n,
1862
+ next_compact_check: new Date(),
1863
+ first_uncompacted_write: new Date(0),
1864
+ bucket_stats: { count: 8, bytes: BigInt(inputs.reduce((sum, doc) => sum + doc.size, 0)), chunks: 8 }
1865
+ });
1866
+
1867
+ expect(await bucketStorage.compactInitialReplication({ maxOpId: 8n })).toEqual({ buckets: 1 });
1868
+ const documents = await collection.find({ '_id.b': BUCKET }).toArray();
1869
+ expect(documents).toHaveLength(1);
1870
+ expect(documents[0].ops!.map((op) => op.o)).toEqual([1n, 2n, 3n, 4n, 5n, 6n, 7n, 8n]);
1871
+ const state = await bucketStateCollection.findOne({ _id: { d: ctx.definitionId, b: BUCKET } });
1872
+ expect(state?.compacted_state).toMatchObject({ op_id: 8n, count: 8, checksum: 252n });
1873
+ expect(state?.last_full_compact).toBeUndefined();
1874
+ });
1875
+
1749
1876
  test('initial compaction merges small chunks and refreshes bucket metadata', async () => {
1750
1877
  const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
1751
1878
  await insertDocs(collection, [
@@ -1754,7 +1881,7 @@ bucket_definitions:
1754
1881
  ]);
1755
1882
  await insertBucketState(bucketStateCollection, ctx.definitionId, 4n);
1756
1883
 
1757
- const result = await bucketStorage.compactInitialReplication({ maxOpId: 4n });
1884
+ const result = await bucketStorage.compactInitialReplication({ forceChunkCompaction: true, maxOpId: 4n });
1758
1885
 
1759
1886
  expect(result).toEqual({ buckets: 1 });
1760
1887
  const documents = await collection.find({ '_id.b': BUCKET }).sort({ '_id.o': 1 }).toArray();
@@ -1769,6 +1896,254 @@ bucket_definitions:
1769
1896
  });
1770
1897
  });
1771
1898
 
1899
+ test.each([
1900
+ { payloadSize: 100, workers: undefined, abort: false },
1901
+ { payloadSize: 450_000, workers: undefined, abort: false },
1902
+ { payloadSize: 450_000, workers: 4, abort: false },
1903
+ { payloadSize: 100, workers: 1, abort: false },
1904
+ { payloadSize: 450_000, workers: 4, abort: true }
1905
+ ])('chunk compaction shares configured workers across runs: %o', async ({ payloadSize, workers, abort }) => {
1906
+ const concurrency = workers ?? 4;
1907
+ const objectStorage = new MemoryObjectStorage();
1908
+ const generator = mongoTestStorageFactoryGenerator({
1909
+ url: env.MONGO_TEST_URL,
1910
+ isCI: env.CI,
1911
+ objectStorage,
1912
+ chunkCompactionConcurrency: workers,
1913
+ inlineThresholdBytes: 0
1914
+ });
1915
+ const { bucketStorage, syncRules, collection, bucketStateCollection, ctx, sourceTableId, db } =
1916
+ await setupV3(generator);
1917
+ const bucketCount = 12;
1918
+ for (let i = 0; i < bucketCount; i++) {
1919
+ const bucket = `global[${i}]`;
1920
+ const documents = [1, 2].map((id) =>
1921
+ serializeBucketData(bucket, [makeOp(id, String(id), 'x'.repeat(payloadSize), ctx, sourceTableId)])
1922
+ );
1923
+ await collection.insertMany(documents);
1924
+ await bucketStateCollection.insertOne({
1925
+ _id: { d: ctx.definitionId, b: bucket },
1926
+ last_op: 2n,
1927
+ next_compact_check: new Date(0),
1928
+ first_uncompacted_write: new Date(0),
1929
+ bucket_stats: {
1930
+ count: 2,
1931
+ bytes: BigInt(documents.reduce((sum, doc) => sum + doc.size, 0)),
1932
+ chunks: 2
1933
+ }
1934
+ });
1935
+ }
1936
+
1937
+ const gate = Promise.withResolvers<void>();
1938
+ const originalPut = objectStorage.put.bind(objectStorage);
1939
+ let uploads = 0;
1940
+ const put = vi.spyOn(objectStorage, 'put').mockImplementation(async (...args) => {
1941
+ uploads++;
1942
+ await gate.promise;
1943
+ await originalPut(...args);
1944
+ });
1945
+ const controller = new AbortController();
1946
+ let settled = false;
1947
+ const runs = Promise.allSettled(
1948
+ Array.from({ length: 2 }, () =>
1949
+ bucketStorage.factory
1950
+ .getInstance(syncRules)
1951
+ .compactInitialReplication({ forceChunkCompaction: true, maxOpId: 2n, signal: controller.signal })
1952
+ )
1953
+ ).finally(() => {
1954
+ settled = true;
1955
+ });
1956
+ try {
1957
+ await vi.waitFor(() => expect(uploads).toBe(concurrency));
1958
+ // Let other ready workers reach admission; the held uploads must prevent
1959
+ // any additional payloads from reaching S3, across both compactor instances.
1960
+ await new Promise((resolve) => setTimeout(resolve, 50));
1961
+ expect(uploads).toBe(concurrency);
1962
+ expect(await bucketStateCollection.countDocuments({ compact_lease: { $exists: true } })).toBeLessThanOrEqual(
1963
+ concurrency
1964
+ );
1965
+ expect(await collection.countDocuments()).toBe(bucketCount * 2);
1966
+ if (abort) {
1967
+ controller.abort();
1968
+ await new Promise((resolve) => setTimeout(resolve, 50));
1969
+ expect(settled).toBe(false);
1970
+ }
1971
+ } finally {
1972
+ gate.resolve();
1973
+ await runs;
1974
+ put.mockRestore();
1975
+ }
1976
+
1977
+ const results = await runs;
1978
+ if (abort) {
1979
+ expect(results.every((result) => result.status === 'rejected')).toBe(true);
1980
+ expect(await bucketStateCollection.countDocuments({ compact_lease: { $exists: true } })).toBe(0);
1981
+ // Cancellation must return all worker slots.
1982
+ expect(await bucketStorage.compactInitialReplication({ forceChunkCompaction: true, maxOpId: 2n })).toEqual({
1983
+ buckets: bucketCount
1984
+ });
1985
+ } else {
1986
+ expect(results.every((result) => result.status === 'fulfilled')).toBe(true);
1987
+ expect(results.reduce((sum, result) => sum + (result.status === 'fulfilled' ? result.value.buckets : 0), 0)).toBe(
1988
+ bucketCount
1989
+ );
1990
+ }
1991
+
1992
+ const documents = await collection.find().toArray();
1993
+ expect(documents).toHaveLength(bucketCount);
1994
+ expect(documents.every((doc) => doc.count === 2 && doc.checksum === 21n && doc.storage_ref != null)).toBe(true);
1995
+ expect(
1996
+ await bucketStateCollection.countDocuments({ 'compacted_state.op_id': 2n, compact_lease: { $exists: false } })
1997
+ ).toBe(bucketCount);
1998
+ const usage = await db.objectStorageUsage.find({ '_id.g': ctx.replicationStreamId }).toArray();
1999
+ const activeBytes = usage.reduce((sum, doc) => sum + BigInt(doc.definitions[ctx.definitionId] ?? 0), 0n);
2000
+ expect(activeBytes).toBe(documents.reduce((sum, doc) => sum + BigInt(doc.storage_ref!.file_size), 0n));
2001
+ });
2002
+
2003
+ test.each([2, 3])('failed chunk worker stops queued work and drains siblings (%s workers)', async (workerCount) => {
2004
+ const { bucketStorage } = await setupV3();
2005
+ const compactor = bucketStorage.createMongoCompactor({}) as any;
2006
+ const failure = new Error('worker failed');
2007
+ const failing = Promise.withResolvers<void>();
2008
+ const draining = Promise.withResolvers<void>();
2009
+ const started: number[] = [];
2010
+ let settled = false;
2011
+ // The factory has two slots. With three workers, one also waits for a slot
2012
+ // when its sibling fails; it must release that slot without starting work.
2013
+ const run = compactor
2014
+ .runChunkCompactionWorkers(
2015
+ [0, 1, 2, 3, 4],
2016
+ Array.from({ length: workerCount }, () => compactor.objectStorageUsage),
2017
+ async (bucket: number) => {
2018
+ started.push(bucket);
2019
+ if (bucket === 0) await failing.promise;
2020
+ if (bucket === 1) await draining.promise;
2021
+ }
2022
+ )
2023
+ .then(
2024
+ () => undefined,
2025
+ (error: unknown) => error
2026
+ )
2027
+ .finally(() => {
2028
+ settled = true;
2029
+ });
2030
+ try {
2031
+ await vi.waitFor(() => expect(started).toEqual([0, 1]));
2032
+ failing.reject(failure);
2033
+ await new Promise<void>((resolve) => setImmediate(resolve));
2034
+ expect(started).toEqual([0, 1]);
2035
+ expect(settled).toBe(false);
2036
+ } finally {
2037
+ failing.resolve();
2038
+ draining.resolve();
2039
+ await run;
2040
+ }
2041
+ expect(await run).toBe(failure);
2042
+ expect(started).toEqual([0, 1]);
2043
+ expect(bucketStorage.factory.chunkCompactionSlots.getValue()).toBe(2);
2044
+ });
2045
+
2046
+ test('unforced chunk workers drain before full compaction and reclassify aged buckets on the next scan', async () => {
2047
+ const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
2048
+ const buckets = ['chunk1[]', 'chunk2[]', 'promoted[]', 'full1[]', 'full2[]'];
2049
+ for (const bucket of buckets) {
2050
+ const documents = Array.from({ length: 8 }, (_, index) =>
2051
+ serializeBucketData(bucket, [makeOp(index + 1, String(index), 'data', { ...ctx, bucket }, sourceTableId)])
2052
+ );
2053
+ await collection.insertMany(documents);
2054
+ await bucketStateCollection.insertOne({
2055
+ _id: { d: ctx.definitionId, b: bucket },
2056
+ last_op: 8n,
2057
+ next_compact_check: new Date(0),
2058
+ first_uncompacted_write: bucket.startsWith('full') ? new Date(0) : new Date(),
2059
+ bucket_stats: { count: 8, bytes: BigInt(documents.reduce((sum, doc) => sum + doc.size, 0)), chunks: 8 }
2060
+ });
2061
+ }
2062
+
2063
+ const originalClaim = CompactionLease.claim.bind(CompactionLease);
2064
+ const jobStartedAt = new Date();
2065
+ let serverTime = jobStartedAt;
2066
+ let promoted = false;
2067
+ const claim = vi.spyOn(CompactionLease, 'claim').mockImplementation(async (...args) => {
2068
+ if (!promoted && (args[1]._id as { b: string })?.b === 'promoted[]') {
2069
+ promoted = true;
2070
+ // Advance the server clock past the full-compaction interval without
2071
+ // changing bucket state. A fixed scan timestamp would retry forever.
2072
+ serverTime = new Date(jobStartedAt.getTime() + 3 * 60 * 60 * 1000);
2073
+ }
2074
+ const lease = await originalClaim(...args);
2075
+ if (lease != null) {
2076
+ Object.defineProperty(lease, 'startedAt', { value: serverTime });
2077
+ }
2078
+ return lease;
2079
+ });
2080
+
2081
+ const compactor = bucketStorage.createMongoCompactor({ maxOpId: 8n });
2082
+ const internal = compactor as any;
2083
+ let timeReads = 0;
2084
+ const clock = vi.spyOn(internal, 'readCompactionTime').mockImplementation(async () => {
2085
+ if (++timeReads > 5) {
2086
+ throw new Error('Compaction did not advance after the bucket aged');
2087
+ }
2088
+ return serverTime;
2089
+ });
2090
+ const scan = vi.spyOn(internal, 'findScheduledBucketBatch');
2091
+ const originalChunks = internal.compactSingleBucketChunks.bind(compactor);
2092
+ const originalFull = internal.compactSingleBucketFully.bind(compactor);
2093
+ const gate = Promise.withResolvers<void>();
2094
+ let activeChunks = 0;
2095
+ let activeFull = 0;
2096
+ let maxActiveFull = 0;
2097
+ const fullBuckets: string[] = [];
2098
+ const chunks = vi.spyOn(internal, 'compactSingleBucketChunks').mockImplementation(async (...args) => {
2099
+ activeChunks++;
2100
+ try {
2101
+ await gate.promise;
2102
+ return await originalChunks(...args);
2103
+ } finally {
2104
+ activeChunks--;
2105
+ }
2106
+ });
2107
+ const full = vi.spyOn(internal, 'compactSingleBucketFully').mockImplementation(async (...args) => {
2108
+ expect(activeChunks).toBe(0);
2109
+ activeFull++;
2110
+ maxActiveFull = Math.max(maxActiveFull, activeFull);
2111
+ fullBuckets.push((args[0] as any).state._id.b);
2112
+ try {
2113
+ return await originalFull(...args);
2114
+ } finally {
2115
+ activeFull--;
2116
+ }
2117
+ });
2118
+ const run = compactor.compact();
2119
+ try {
2120
+ await vi.waitFor(() => expect(activeChunks).toBe(2));
2121
+ expect(full).not.toHaveBeenCalled();
2122
+ } finally {
2123
+ gate.resolve();
2124
+ try {
2125
+ await run;
2126
+ expect(timeReads).toBe(3); // Job start, first batch, and promoted bucket's batch.
2127
+ expect(scan).toHaveBeenCalledTimes(3); // Includes the final empty scan.
2128
+ for (const [cutoff] of scan.mock.calls) {
2129
+ expect(cutoff).toEqual(jobStartedAt);
2130
+ }
2131
+ } finally {
2132
+ clock.mockRestore();
2133
+ scan.mockRestore();
2134
+ claim.mockRestore();
2135
+ chunks.mockRestore();
2136
+ full.mockRestore();
2137
+ }
2138
+ }
2139
+ expect(await run).toBe(5);
2140
+ expect(promoted).toBe(true);
2141
+ expect(maxActiveFull).toBe(1);
2142
+ expect(fullBuckets.sort()).toEqual(['full1[]', 'full2[]', 'promoted[]']);
2143
+ expect(await bucketStateCollection.countDocuments({ compact_lease: { $exists: true } })).toBe(0);
2144
+ expect(await collection.countDocuments()).toBe(5);
2145
+ });
2146
+
1772
2147
  test('capped chunk compaction repairs stale bucket stats after a committed first merge', async () => {
1773
2148
  const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
1774
2149
  const operations = [
@@ -1796,7 +2171,7 @@ bucket_definitions:
1796
2171
  }
1797
2172
  });
1798
2173
 
1799
- const result = await bucketStorage.compactInitialReplication({ maxOpId: 4n });
2174
+ const result = await bucketStorage.compactInitialReplication({ forceChunkCompaction: true, maxOpId: 4n });
1800
2175
 
1801
2176
  expect(result).toEqual({ buckets: 1 });
1802
2177
  const expectedStats = {
@@ -1915,9 +2290,9 @@ bucket_definitions:
1915
2290
  bucket_stats: { count: 4, bytes: 200n, chunks: 4 }
1916
2291
  });
1917
2292
 
1918
- await expect(bucketStorage.createMongoCompactor({ maxOpId: 4n, compactChunksOnly: true }).compact()).resolves.toBe(
1919
- 1
1920
- );
2293
+ await expect(
2294
+ bucketStorage.createMongoCompactor({ maxOpId: 4n, compactChunksOnly: true, forceChunkCompaction: true }).compact()
2295
+ ).resolves.toBe(1);
1921
2296
 
1922
2297
  const currentDocuments = await collection.find({ '_id.b': BUCKET }).sort({ '_id.o': 1 }).toArray();
1923
2298
  expect(currentDocuments.flatMap((document) => document.ops!.map((op) => op.o))).toEqual([1n, 3n, 4n]);
@@ -1937,7 +2312,8 @@ bucket_definitions:
1937
2312
  const { bucketStorage, collection, bucketStateCollection, ctx } = await setupCompactedTail();
1938
2313
  const failedCompactor = bucketStorage.createMongoCompactor({
1939
2314
  maxOpId: 4n,
1940
- compactChunksOnly: true
2315
+ compactChunksOnly: true,
2316
+ forceChunkCompaction: true
1941
2317
  });
1942
2318
  const originalFlush = (failedCompactor as any).flushCompactionGroup.bind(failedCompactor);
1943
2319
  const flushSpy = vi
@@ -1958,7 +2334,9 @@ bucket_definitions:
1958
2334
  { $set: { next_compact_check: new Date(0) } }
1959
2335
  );
1960
2336
 
1961
- const result = await bucketStorage.createMongoCompactor({ maxOpId: 4n, compactChunksOnly: true }).compact();
2337
+ const result = await bucketStorage
2338
+ .createMongoCompactor({ maxOpId: 4n, compactChunksOnly: true, forceChunkCompaction: true })
2339
+ .compact();
1962
2340
 
1963
2341
  expect(result).toBe(1);
1964
2342
  await expectRecoveredCompactedTail(collection, bucketStateCollection, ctx.definitionId);
@@ -1,6 +1,6 @@
1
1
  import { mongoTestStorageFactoryGenerator } from '@module/utils/test-utils.js';
2
2
  import { storage, updateSyncRulesFromYaml } from '@powersync/service-core';
3
- import { bucketRequest, test_utils } from '@powersync/service-core-tests';
3
+ import { bucketRequest, getTestStorage, test_utils } from '@powersync/service-core-tests';
4
4
  import { describe, expect, test } from 'vitest';
5
5
  import { DEFAULT_INLINE_THRESHOLD_BYTES } from '../../src/storage/implementation/common/PersistedBatch.js';
6
6
  import { MongoSyncBucketStorage } from '../../src/storage/implementation/createMongoSyncBucketStorage.js';
@@ -31,7 +31,7 @@ describe('Object storage inline threshold', () => {
31
31
  const { memoryStorage, factoryGen } = s3Factory();
32
32
  await using factory = await factoryGen.factory();
33
33
  const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
34
- const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
34
+ const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
35
35
  expect(bucketStorage.inlineThresholdBytes).toBe(DEFAULT_INLINE_THRESHOLD_BYTES);
36
36
 
37
37
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
@@ -77,7 +77,7 @@ describe('Object storage inline threshold', () => {
77
77
  const { memoryStorage, factoryGen } = s3Factory(256);
78
78
  await using factory = await factoryGen.factory();
79
79
  const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
80
- const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
80
+ const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
81
81
  expect(bucketStorage.inlineThresholdBytes).toBe(256);
82
82
 
83
83
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
@@ -4,7 +4,7 @@ import {
4
4
  storage,
5
5
  updateSyncRulesFromYaml
6
6
  } from '@powersync/service-core';
7
- import { bucketRequest, compactActive, test_utils } from '@powersync/service-core-tests';
7
+ import { bucketRequest, compactActive, getTestStorage, test_utils } from '@powersync/service-core-tests';
8
8
  import { describe, expect, test } from 'vitest';
9
9
  import { MongoSyncBucketStorage } from '../../src/storage/implementation/createMongoSyncBucketStorage.js';
10
10
  import { VersionedPowerSyncMongoV3 } from '../../src/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
@@ -28,7 +28,7 @@ describe('V3 checksums with S3 object storage', () => {
28
28
  const { factoryGen } = s3Factory();
29
29
  await using factory = await factoryGen.factory();
30
30
  const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
31
- const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
31
+ const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
32
32
  const db = bucketStorage.db as VersionedPowerSyncMongoV3;
33
33
 
34
34
  const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);
@@ -75,7 +75,7 @@ describe('V3 checksums with S3 object storage', () => {
75
75
  const { factoryGen } = s3Factory();
76
76
  await using factory = await factoryGen.factory();
77
77
  const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
78
- const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
78
+ const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
79
79
  const db = bucketStorage.db as VersionedPowerSyncMongoV3;
80
80
 
81
81
  const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);
@@ -114,7 +114,7 @@ describe('V3 checksums with S3 object storage', () => {
114
114
  const { factoryGen } = s3Factory();
115
115
  await using factory = await factoryGen.factory();
116
116
  const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
117
- const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
117
+ const bucketStorage = (await getTestStorage(factory, syncRules)) as MongoSyncBucketStorage;
118
118
  const db = bucketStorage.db as VersionedPowerSyncMongoV3;
119
119
 
120
120
  const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);