@powersync/service-module-mongodb-storage 0.21.0 → 0.22.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 (81) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +5 -0
  3. package/dist/storage/MongoBucketStorage.js +7 -1
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +2 -1
  6. package/dist/storage/implementation/MongoBucketBatch.js +9 -8
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoStorageProvider.js +4 -2
  9. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  10. package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
  11. package/dist/storage/implementation/MongoWriteBatch.js +149 -0
  12. package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
  13. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +2 -2
  14. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  15. package/dist/storage/implementation/common/PersistedBatch.d.ts +5 -4
  16. package/dist/storage/implementation/common/PersistedBatch.js +6 -4
  17. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  18. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
  19. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
  20. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  21. package/dist/storage/implementation/db.d.ts +5 -0
  22. package/dist/storage/implementation/db.js +13 -0
  23. package/dist/storage/implementation/db.js.map +1 -1
  24. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +13 -6
  25. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  26. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +2 -1
  27. package/dist/storage/implementation/v1/MongoCompactorV1.js +23 -17
  28. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  29. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
  30. package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
  31. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
  32. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +2 -1
  33. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +17 -14
  34. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  35. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +6 -1
  36. package/dist/storage/implementation/v3/MongoCompactorV3.js +164 -69
  37. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  38. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +12 -8
  39. package/dist/storage/implementation/v3/PersistedBatchV3.js +25 -32
  40. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  41. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -1
  42. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
  43. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -3
  44. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +7 -9
  45. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
  46. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +3 -2
  47. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +4 -7
  48. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -1
  49. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +21 -3
  50. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
  51. package/dist/types/types.d.ts +5 -0
  52. package/dist/types/types.js +12 -0
  53. package/dist/types/types.js.map +1 -1
  54. package/dist/utils/test-utils.js +1 -0
  55. package/dist/utils/test-utils.js.map +1 -1
  56. package/package.json +6 -6
  57. package/src/storage/MongoBucketStorage.ts +12 -1
  58. package/src/storage/implementation/MongoBucketBatch.ts +13 -10
  59. package/src/storage/implementation/MongoStorageProvider.ts +12 -2
  60. package/src/storage/implementation/MongoWriteBatch.ts +172 -0
  61. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -2
  62. package/src/storage/implementation/common/PersistedBatch.ts +12 -8
  63. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
  64. package/src/storage/implementation/db.ts +14 -0
  65. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +17 -8
  66. package/src/storage/implementation/v1/MongoCompactorV1.ts +29 -26
  67. package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
  68. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +51 -49
  69. package/src/storage/implementation/v3/MongoCompactorV3.ts +276 -97
  70. package/src/storage/implementation/v3/PersistedBatchV3.ts +36 -36
  71. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +8 -1
  72. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +11 -10
  73. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +8 -9
  74. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +23 -3
  75. package/src/types/types.ts +18 -0
  76. package/src/utils/test-utils.ts +1 -0
  77. package/test/src/chunk_compaction_config.test.ts +38 -0
  78. package/test/src/object_storage_usage.test.ts +16 -4
  79. package/test/src/storage_compacting.test.ts +257 -5
  80. package/test/src/storage_s3_reading.test.ts +47 -1
  81. 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 {
@@ -19,6 +20,8 @@ import {
19
20
  import { bucketRequest, compactActive, 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(
@@ -460,7 +463,7 @@ bucket_definitions:
460
463
 
461
464
  test('aborting scheduled compaction does not reschedule the remaining batch', async () => {
462
465
  const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3Storage();
463
- const buckets = ['first[]', 'second[]', 'third[]'];
466
+ const buckets = Array.from({ length: 8 }, (_, index) => `bucket${index}[]`);
464
467
  const documents = buckets.map((bucket) =>
465
468
  serializeBucketData(bucket, [makeOp(2, bucket, bucket, { ...ctx, bucket }, sourceTableId)])
466
469
  );
@@ -499,8 +502,11 @@ bucket_definitions:
499
502
  const states = await bucketStateCollection.find({ '_id.b': { $in: buckets } }).toArray();
500
503
  const completed = states.filter((state) => state.compacted_state != null);
501
504
  const remaining = states.filter((state) => state.compacted_state == null);
502
- expect(completed).toHaveLength(1);
503
- expect(remaining).toHaveLength(2);
505
+ // Already-started buckets may finish after abort, but the queued remainder
506
+ // must not be claimed or rescheduled.
507
+ expect(completed.length).toBeGreaterThanOrEqual(1);
508
+ expect(completed.length).toBeLessThanOrEqual(2);
509
+ expect(remaining.length).toBeGreaterThanOrEqual(6);
504
510
  for (const state of remaining) {
505
511
  expect(state.next_compact_check).toEqual(new Date(0));
506
512
  expect(state.compact_lease).toBeUndefined();
@@ -1637,8 +1643,8 @@ bucket_definitions:
1637
1643
  describe('Streaming compactor', () => {
1638
1644
  const BUCKET = 'global[]';
1639
1645
 
1640
- async function setupV3() {
1641
- await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
1646
+ async function setupV3(factoryGenerator = INITIALIZED_MONGO_STORAGE_FACTORY) {
1647
+ await using factory = await factoryGenerator.factory();
1642
1648
  const syncRules = await factory.updateSyncRules(
1643
1649
  updateSyncRulesFromYaml(
1644
1650
  `
@@ -1769,6 +1775,252 @@ bucket_definitions:
1769
1775
  });
1770
1776
  });
1771
1777
 
1778
+ test.each([
1779
+ { payloadSize: 100, workers: undefined, abort: false },
1780
+ { payloadSize: 450_000, workers: undefined, abort: false },
1781
+ { payloadSize: 450_000, workers: 4, abort: false },
1782
+ { payloadSize: 100, workers: 1, abort: false },
1783
+ { payloadSize: 450_000, workers: 4, abort: true }
1784
+ ])('chunk compaction shares configured workers across runs: %o', async ({ payloadSize, workers, abort }) => {
1785
+ const concurrency = workers ?? 4;
1786
+ const objectStorage = new MemoryObjectStorage();
1787
+ const generator = mongoTestStorageFactoryGenerator({
1788
+ url: env.MONGO_TEST_URL,
1789
+ isCI: env.CI,
1790
+ objectStorage,
1791
+ chunkCompactionConcurrency: workers,
1792
+ inlineThresholdBytes: 0
1793
+ });
1794
+ const { bucketStorage, syncRules, collection, bucketStateCollection, ctx, sourceTableId, db } =
1795
+ await setupV3(generator);
1796
+ const bucketCount = 12;
1797
+ for (let i = 0; i < bucketCount; i++) {
1798
+ const bucket = `global[${i}]`;
1799
+ const documents = [1, 2].map((id) =>
1800
+ serializeBucketData(bucket, [makeOp(id, String(id), 'x'.repeat(payloadSize), ctx, sourceTableId)])
1801
+ );
1802
+ await collection.insertMany(documents);
1803
+ await bucketStateCollection.insertOne({
1804
+ _id: { d: ctx.definitionId, b: bucket },
1805
+ last_op: 2n,
1806
+ next_compact_check: new Date(0),
1807
+ first_uncompacted_write: new Date(0),
1808
+ bucket_stats: {
1809
+ count: 2,
1810
+ bytes: BigInt(documents.reduce((sum, doc) => sum + doc.size, 0)),
1811
+ chunks: 2
1812
+ }
1813
+ });
1814
+ }
1815
+
1816
+ const gate = Promise.withResolvers<void>();
1817
+ const originalPut = objectStorage.put.bind(objectStorage);
1818
+ let uploads = 0;
1819
+ const put = vi.spyOn(objectStorage, 'put').mockImplementation(async (...args) => {
1820
+ uploads++;
1821
+ await gate.promise;
1822
+ await originalPut(...args);
1823
+ });
1824
+ const controller = new AbortController();
1825
+ let settled = false;
1826
+ const runs = Promise.allSettled(
1827
+ Array.from({ length: 2 }, () =>
1828
+ bucketStorage.factory
1829
+ .getInstance(syncRules)
1830
+ .compactInitialReplication({ maxOpId: 2n, signal: controller.signal })
1831
+ )
1832
+ ).finally(() => {
1833
+ settled = true;
1834
+ });
1835
+ try {
1836
+ await vi.waitFor(() => expect(uploads).toBe(concurrency));
1837
+ // Let other ready workers reach admission; the held uploads must prevent
1838
+ // any additional payloads from reaching S3, across both compactor instances.
1839
+ await new Promise((resolve) => setTimeout(resolve, 50));
1840
+ expect(uploads).toBe(concurrency);
1841
+ expect(await bucketStateCollection.countDocuments({ compact_lease: { $exists: true } })).toBeLessThanOrEqual(
1842
+ concurrency
1843
+ );
1844
+ expect(await collection.countDocuments()).toBe(bucketCount * 2);
1845
+ if (abort) {
1846
+ controller.abort();
1847
+ await new Promise((resolve) => setTimeout(resolve, 50));
1848
+ expect(settled).toBe(false);
1849
+ }
1850
+ } finally {
1851
+ gate.resolve();
1852
+ await runs;
1853
+ put.mockRestore();
1854
+ }
1855
+
1856
+ const results = await runs;
1857
+ if (abort) {
1858
+ expect(results.every((result) => result.status === 'rejected')).toBe(true);
1859
+ expect(await bucketStateCollection.countDocuments({ compact_lease: { $exists: true } })).toBe(0);
1860
+ // Cancellation must return all worker slots.
1861
+ expect(await bucketStorage.compactInitialReplication({ maxOpId: 2n })).toEqual({ buckets: bucketCount });
1862
+ } else {
1863
+ expect(results.every((result) => result.status === 'fulfilled')).toBe(true);
1864
+ expect(results.reduce((sum, result) => sum + (result.status === 'fulfilled' ? result.value.buckets : 0), 0)).toBe(
1865
+ bucketCount
1866
+ );
1867
+ }
1868
+
1869
+ const documents = await collection.find().toArray();
1870
+ expect(documents).toHaveLength(bucketCount);
1871
+ expect(documents.every((doc) => doc.count === 2 && doc.checksum === 21n && doc.storage_ref != null)).toBe(true);
1872
+ expect(
1873
+ await bucketStateCollection.countDocuments({ 'compacted_state.op_id': 2n, compact_lease: { $exists: false } })
1874
+ ).toBe(bucketCount);
1875
+ const usage = await db.objectStorageUsage.find({ '_id.g': ctx.replicationStreamId }).toArray();
1876
+ const activeBytes = usage.reduce((sum, doc) => sum + BigInt(doc.definitions[ctx.definitionId] ?? 0), 0n);
1877
+ expect(activeBytes).toBe(documents.reduce((sum, doc) => sum + BigInt(doc.storage_ref!.file_size), 0n));
1878
+ });
1879
+
1880
+ test.each([2, 3])('failed chunk worker stops queued work and drains siblings (%s workers)', async (workerCount) => {
1881
+ const { bucketStorage } = await setupV3();
1882
+ const compactor = bucketStorage.createMongoCompactor({}) as any;
1883
+ const failure = new Error('worker failed');
1884
+ const failing = Promise.withResolvers<void>();
1885
+ const draining = Promise.withResolvers<void>();
1886
+ const started: number[] = [];
1887
+ let settled = false;
1888
+ // The factory has two slots. With three workers, one also waits for a slot
1889
+ // when its sibling fails; it must release that slot without starting work.
1890
+ const run = compactor
1891
+ .runChunkCompactionWorkers(
1892
+ [0, 1, 2, 3, 4],
1893
+ Array.from({ length: workerCount }, () => compactor.objectStorageUsage),
1894
+ async (bucket: number) => {
1895
+ started.push(bucket);
1896
+ if (bucket === 0) await failing.promise;
1897
+ if (bucket === 1) await draining.promise;
1898
+ }
1899
+ )
1900
+ .then(
1901
+ () => undefined,
1902
+ (error: unknown) => error
1903
+ )
1904
+ .finally(() => {
1905
+ settled = true;
1906
+ });
1907
+ try {
1908
+ await vi.waitFor(() => expect(started).toEqual([0, 1]));
1909
+ failing.reject(failure);
1910
+ await new Promise<void>((resolve) => setImmediate(resolve));
1911
+ expect(started).toEqual([0, 1]);
1912
+ expect(settled).toBe(false);
1913
+ } finally {
1914
+ failing.resolve();
1915
+ draining.resolve();
1916
+ await run;
1917
+ }
1918
+ expect(await run).toBe(failure);
1919
+ expect(started).toEqual([0, 1]);
1920
+ expect(bucketStorage.factory.chunkCompactionSlots.getValue()).toBe(2);
1921
+ });
1922
+
1923
+ test('unforced chunk workers drain before full compaction and reclassify aged buckets on the next scan', async () => {
1924
+ const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
1925
+ const buckets = ['chunk1[]', 'chunk2[]', 'promoted[]', 'full1[]', 'full2[]'];
1926
+ for (const bucket of buckets) {
1927
+ const documents = Array.from({ length: 8 }, (_, index) =>
1928
+ serializeBucketData(bucket, [makeOp(index + 1, String(index), 'data', { ...ctx, bucket }, sourceTableId)])
1929
+ );
1930
+ await collection.insertMany(documents);
1931
+ await bucketStateCollection.insertOne({
1932
+ _id: { d: ctx.definitionId, b: bucket },
1933
+ last_op: 8n,
1934
+ next_compact_check: new Date(0),
1935
+ first_uncompacted_write: bucket.startsWith('full') ? new Date(0) : new Date(),
1936
+ bucket_stats: { count: 8, bytes: BigInt(documents.reduce((sum, doc) => sum + doc.size, 0)), chunks: 8 }
1937
+ });
1938
+ }
1939
+
1940
+ const originalClaim = CompactionLease.claim.bind(CompactionLease);
1941
+ const jobStartedAt = new Date();
1942
+ let serverTime = jobStartedAt;
1943
+ let promoted = false;
1944
+ const claim = vi.spyOn(CompactionLease, 'claim').mockImplementation(async (...args) => {
1945
+ if (!promoted && (args[1]._id as { b: string })?.b === 'promoted[]') {
1946
+ promoted = true;
1947
+ // Advance the server clock past the full-compaction interval without
1948
+ // changing bucket state. A fixed scan timestamp would retry forever.
1949
+ serverTime = new Date(jobStartedAt.getTime() + 3 * 60 * 60 * 1000);
1950
+ }
1951
+ const lease = await originalClaim(...args);
1952
+ if (lease != null) {
1953
+ Object.defineProperty(lease, 'startedAt', { value: serverTime });
1954
+ }
1955
+ return lease;
1956
+ });
1957
+
1958
+ const compactor = bucketStorage.createMongoCompactor({ maxOpId: 8n });
1959
+ const internal = compactor as any;
1960
+ let timeReads = 0;
1961
+ const clock = vi.spyOn(internal, 'readCompactionTime').mockImplementation(async () => {
1962
+ if (++timeReads > 5) {
1963
+ throw new Error('Compaction did not advance after the bucket aged');
1964
+ }
1965
+ return serverTime;
1966
+ });
1967
+ const scan = vi.spyOn(internal, 'findScheduledBucketBatch');
1968
+ const originalChunks = internal.compactSingleBucketChunks.bind(compactor);
1969
+ const originalFull = internal.compactSingleBucketFully.bind(compactor);
1970
+ const gate = Promise.withResolvers<void>();
1971
+ let activeChunks = 0;
1972
+ let activeFull = 0;
1973
+ let maxActiveFull = 0;
1974
+ const fullBuckets: string[] = [];
1975
+ const chunks = vi.spyOn(internal, 'compactSingleBucketChunks').mockImplementation(async (...args) => {
1976
+ activeChunks++;
1977
+ try {
1978
+ await gate.promise;
1979
+ return await originalChunks(...args);
1980
+ } finally {
1981
+ activeChunks--;
1982
+ }
1983
+ });
1984
+ const full = vi.spyOn(internal, 'compactSingleBucketFully').mockImplementation(async (...args) => {
1985
+ expect(activeChunks).toBe(0);
1986
+ activeFull++;
1987
+ maxActiveFull = Math.max(maxActiveFull, activeFull);
1988
+ fullBuckets.push((args[0] as any).state._id.b);
1989
+ try {
1990
+ return await originalFull(...args);
1991
+ } finally {
1992
+ activeFull--;
1993
+ }
1994
+ });
1995
+ const run = compactor.compact();
1996
+ try {
1997
+ await vi.waitFor(() => expect(activeChunks).toBe(2));
1998
+ expect(full).not.toHaveBeenCalled();
1999
+ } finally {
2000
+ gate.resolve();
2001
+ try {
2002
+ await run;
2003
+ expect(timeReads).toBe(3); // Job start, first batch, and promoted bucket's batch.
2004
+ expect(scan).toHaveBeenCalledTimes(3); // Includes the final empty scan.
2005
+ for (const [cutoff] of scan.mock.calls) {
2006
+ expect(cutoff).toEqual(jobStartedAt);
2007
+ }
2008
+ } finally {
2009
+ clock.mockRestore();
2010
+ scan.mockRestore();
2011
+ claim.mockRestore();
2012
+ chunks.mockRestore();
2013
+ full.mockRestore();
2014
+ }
2015
+ }
2016
+ expect(await run).toBe(5);
2017
+ expect(promoted).toBe(true);
2018
+ expect(maxActiveFull).toBe(1);
2019
+ expect(fullBuckets.sort()).toEqual(['full1[]', 'full2[]', 'promoted[]']);
2020
+ expect(await bucketStateCollection.countDocuments({ compact_lease: { $exists: true } })).toBe(0);
2021
+ expect(await collection.countDocuments()).toBe(5);
2022
+ });
2023
+
1772
2024
  test('capped chunk compaction repairs stale bucket stats after a committed first merge', async () => {
1773
2025
  const { bucketStorage, collection, bucketStateCollection, ctx, sourceTableId } = await setupV3();
1774
2026
  const operations = [
@@ -2,7 +2,7 @@ import { DeleteObjectsCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
2
2
  import { storage, updateSyncRulesFromYaml } from '@powersync/service-core';
3
3
  import { bucketRequest, test_utils } from '@powersync/service-core-tests';
4
4
  import * as bson from 'bson';
5
- import { describe, expect, test } from 'vitest';
5
+ import { describe, expect, test, vi } from 'vitest';
6
6
  import { MongoSyncBucketStorage } from '../../src/storage/implementation/createMongoSyncBucketStorage.js';
7
7
  import { VersionedPowerSyncMongoV3 } from '../../src/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
8
8
  import { hydrateBucketDataDocuments } from '../../src/storage/implementation/v3/object-storage/BucketDataObjectStorage.js';
@@ -576,6 +576,52 @@ describe('S3 object storage reads', () => {
576
576
  expect(maxActiveOperations).toBe(4);
577
577
  });
578
578
 
579
+ test('drains sibling downloads before reporting a hydration failure', async () => {
580
+ const objectStorage = new MemoryObjectStorage();
581
+ const gate = Promise.withResolvers<void>();
582
+ const failure = new Error('download failed');
583
+ const documents = ['failed', 'pending'].map((path, index) => ({
584
+ _id: { b: 'bucket', o: BigInt(index + 1) },
585
+ min_op: BigInt(index + 1),
586
+ checksum: 0n,
587
+ count: 0,
588
+ size: 1,
589
+ storage_ref: { path, file_size: 1 }
590
+ }));
591
+ const get = vi.spyOn(objectStorage, 'get').mockImplementation(async (path) => {
592
+ if (path === 'failed') {
593
+ throw failure;
594
+ }
595
+ await gate.promise;
596
+ return {
597
+ data: bson.serialize({ ops: [] }),
598
+ metadata: { contentType: 'application/bson', contentEncoding: null }
599
+ };
600
+ });
601
+ let settled = false;
602
+ const result = hydrateBucketDataDocuments(documents, objectStorage, {}).then(
603
+ () => {
604
+ settled = true;
605
+ return undefined;
606
+ },
607
+ (error) => {
608
+ settled = true;
609
+ return error;
610
+ }
611
+ );
612
+ try {
613
+ await vi.waitFor(() => expect(get).toHaveBeenCalledTimes(2));
614
+ await new Promise<void>((resolve) => setImmediate(resolve));
615
+ expect(settled).toBe(false);
616
+ } finally {
617
+ gate.resolve();
618
+ await result;
619
+ get.mockRestore();
620
+ }
621
+ expect(await result).toBe(failure);
622
+ expect(documents[1]).toHaveProperty('ops', []);
623
+ });
624
+
579
625
  test('aborts active object downloads', async () => {
580
626
  const objectStorage = new MemoryObjectStorage();
581
627
  const controller = new AbortController();