@powersync/service-module-mongodb-storage 0.0.0-dev-20260602073133 → 0.0.0-dev-20260909133214
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.
- package/CHANGELOG.md +177 -5
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.d.ts +3 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js +41 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js.map +1 -0
- package/dist/storage/MongoBucketStorage.d.ts +71 -18
- package/dist/storage/MongoBucketStorage.js +516 -202
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +71 -29
- package/dist/storage/implementation/MongoBucketBatch.js +140 -66
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoChecksums.d.ts +41 -13
- package/dist/storage/implementation/MongoChecksums.js +36 -121
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +28 -90
- package/dist/storage/implementation/MongoCompactor.js +87 -481
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoHydrationState.d.ts +11 -0
- package/dist/storage/implementation/MongoHydrationState.js +49 -0
- package/dist/storage/implementation/MongoHydrationState.js.map +1 -0
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +82 -8
- package/dist/storage/implementation/MongoParameterCompactor.js +312 -76
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +21 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js +93 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -0
- package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +36 -0
- package/dist/storage/implementation/MongoPersistedReplicationStream.js +90 -0
- package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -0
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +25 -0
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +125 -0
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
- package/dist/storage/implementation/MongoStorageProvider.js +41 -4
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +81 -37
- package/dist/storage/implementation/MongoSyncBucketStorage.js +159 -86
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js +23 -11
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +11 -8
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +159 -33
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
- package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
- package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +28 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js +2 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js.map +1 -0
- package/dist/storage/implementation/common/PersistedBatch.d.ts +34 -2
- package/dist/storage/implementation/common/PersistedBatch.js +63 -10
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +1 -1
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
- package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
- package/dist/storage/implementation/createMongoSyncBucketStorage.js +4 -4
- package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +4 -1
- package/dist/storage/implementation/db.js +31 -16
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +55 -13
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +15 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +135 -107
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +4 -4
- package/dist/storage/implementation/v1/MongoChecksumsV1.js +119 -7
- package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +47 -7
- package/dist/storage/implementation/v1/MongoCompactorV1.js +478 -8
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +33 -27
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +146 -73
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +2 -1
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
- package/dist/storage/implementation/v1/models.d.ts +10 -1
- package/dist/storage/implementation/v1/models.js +1 -0
- package/dist/storage/implementation/v1/models.js.map +1 -1
- package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
- package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
- package/dist/storage/implementation/v3/CompactionLease.js +131 -0
- package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +16 -5
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +393 -262
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +30 -9
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +161 -20
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +108 -18
- package/dist/storage/implementation/v3/MongoCompactorV3.js +1115 -47
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +12 -3
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +13 -8
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +54 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +319 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +61 -36
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +353 -161
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +8 -3
- package/dist/storage/implementation/v3/PersistedBatchV3.js +131 -29
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +3 -3
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js +13 -11
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +26 -14
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +73 -29
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.d.ts +13 -0
- package/dist/storage/implementation/v3/bucket-format.js +79 -0
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -0
- package/dist/storage/implementation/v3/chunking.d.ts +10 -0
- package/dist/storage/implementation/v3/chunking.js +30 -0
- package/dist/storage/implementation/v3/chunking.js.map +1 -0
- package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
- package/dist/storage/implementation/v3/compact-utils.js +216 -0
- package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
- package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
- package/dist/storage/implementation/v3/compaction-constants.js +6 -0
- package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +187 -23
- package/dist/storage/implementation/v3/models.js +11 -28
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +20 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +48 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +33 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +68 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +154 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +37 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +167 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +67 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +439 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/source-table-utils.d.ts +63 -0
- package/dist/storage/implementation/v3/source-table-utils.js +206 -0
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
- package/dist/storage/storage-index.d.ts +5 -4
- package/dist/storage/storage-index.js +5 -4
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/types/types.d.ts +71 -3
- package/dist/types/types.js +58 -2
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.d.ts +1 -2
- package/dist/utils/test-utils.js +7 -1
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.d.ts +9 -2
- package/dist/utils/util.js +131 -5
- package/dist/utils/util.js.map +1 -1
- package/package.json +13 -19
- package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
- package/src/storage/MongoBucketStorage.ts +728 -242
- package/src/storage/implementation/MongoBucketBatch.ts +196 -91
- package/src/storage/implementation/MongoChecksums.ts +93 -153
- package/src/storage/implementation/MongoCompactor.ts +106 -631
- package/src/storage/implementation/MongoHydrationState.ts +61 -0
- package/src/storage/implementation/MongoParameterCompactor.ts +392 -87
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +122 -0
- package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +156 -0
- package/src/storage/implementation/MongoStorageProvider.ts +48 -4
- package/src/storage/implementation/MongoSyncBucketStorage.ts +226 -118
- package/src/storage/implementation/MongoSyncRulesLock.ts +28 -12
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +194 -52
- package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +31 -0
- package/src/storage/implementation/common/PersistedBatch.ts +83 -15
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
- package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
- package/src/storage/implementation/db.ts +40 -22
- package/src/storage/implementation/models.ts +58 -14
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +174 -125
- package/src/storage/implementation/v1/MongoChecksumsV1.ts +154 -12
- package/src/storage/implementation/v1/MongoCompactorV1.ts +594 -25
- package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +226 -129
- package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
- package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +11 -1
- package/src/storage/implementation/v1/models.ts +12 -1
- package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
- package/src/storage/implementation/v3/CompactionLease.ts +152 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +502 -324
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +250 -32
- package/src/storage/implementation/v3/MongoCompactorV3.ts +1439 -70
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +19 -9
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +473 -0
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +453 -211
- package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +169 -45
- package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +99 -38
- package/src/storage/implementation/v3/bucket-format.ts +99 -0
- package/src/storage/implementation/v3/chunking.ts +40 -0
- package/src/storage/implementation/v3/compact-utils.ts +320 -0
- package/src/storage/implementation/v3/compaction-constants.ts +5 -0
- package/src/storage/implementation/v3/models.ts +228 -58
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +62 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +44 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +182 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +230 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +573 -0
- package/src/storage/implementation/v3/source-table-utils.ts +398 -0
- package/src/storage/storage-index.ts +14 -4
- package/src/types/types.ts +76 -2
- package/src/utils/test-utils.ts +8 -3
- package/src/utils/util.ts +188 -7
- package/test/src/__snapshots__/storage.test.ts.snap +4 -54
- package/test/src/__snapshots__/storage_sync.test.ts.snap +652 -645
- package/test/src/cleanup-stopped-sync-configs.test.ts +698 -0
- package/test/src/compact-utils.test.ts +226 -0
- package/test/src/helpers/MemoryObjectStorage.ts +61 -0
- package/test/src/helpers/s3TestFactory.ts +69 -0
- package/test/src/object_storage_lifecycle.test.ts +59 -0
- package/test/src/object_storage_usage.test.ts +255 -0
- package/test/src/parameter_compacting_v1.test.ts +120 -0
- package/test/src/parameter_compacting_v3.test.ts +192 -0
- package/test/src/parameter_compaction_fence.test.ts +276 -0
- package/test/src/setup.ts +6 -1
- package/test/src/storage.test.ts +478 -13
- package/test/src/storage_compacting.test.ts +2516 -107
- package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
- package/test/src/storage_s3_checksums.test.ts +190 -0
- package/test/src/storage_s3_compaction_lifecycle.test.ts +543 -0
- package/test/src/storage_s3_reading.test.ts +768 -0
- package/test/src/storage_s3_writing.test.ts +136 -0
- package/test/src/storage_sync.test.ts +1507 -76
- package/test/src/storeCurrentData.test.ts +227 -0
- package/test/src/util.ts +3 -2
- package/tsconfig.json +1 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +0 -15
- package/dist/storage/implementation/BucketDefinitionMapping.js +0 -58
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +0 -1
- package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -14
- package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -67
- package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -29
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -76
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +0 -13
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +0 -2
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +0 -1
- package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
- package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
- package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
- package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +0 -4
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js +0 -9
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +0 -1
- package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -48
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
- package/src/storage/implementation/BucketDefinitionMapping.ts +0 -75
- package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -82
- package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -101
- package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +0 -15
- package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
- package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -11
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -68
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyCompactionDelta,
|
|
3
|
+
applyStatsReplacement,
|
|
4
|
+
bucketStats,
|
|
5
|
+
chooseCompactionKind,
|
|
6
|
+
combineAdjacentStats,
|
|
7
|
+
combineChunkStats,
|
|
8
|
+
CompactIntervalConfig,
|
|
9
|
+
CompactionKind,
|
|
10
|
+
emptyBucketStats,
|
|
11
|
+
forcedCompactionKind,
|
|
12
|
+
fullCompactionCheckAt,
|
|
13
|
+
statsForDocument,
|
|
14
|
+
statsForDocuments
|
|
15
|
+
} from '@module/storage/implementation/v3/compact-utils.js';
|
|
16
|
+
import { BucketStateDocumentV3 } from '@module/storage/implementation/v3/models.js';
|
|
17
|
+
import { describe, expect, test } from 'vitest';
|
|
18
|
+
|
|
19
|
+
const INTERVAL_CONFIG: CompactIntervalConfig = {
|
|
20
|
+
minCompactChunkIntervalMs: 1_000,
|
|
21
|
+
minCompactFullIntervalMs: 10_000,
|
|
22
|
+
maxCompactFullIntervalMs: 100_000
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function bucketState(overrides: Partial<BucketStateDocumentV3> = {}): BucketStateDocumentV3 {
|
|
26
|
+
return {
|
|
27
|
+
_id: { d: 'definition', b: 'bucket[]' },
|
|
28
|
+
last_op: 10n,
|
|
29
|
+
next_compact_check: new Date(0),
|
|
30
|
+
first_uncompacted_write: new Date(0),
|
|
31
|
+
bucket_stats: { count: 10, bytes: 100n, chunks: 1 },
|
|
32
|
+
...overrides
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function compactedState(overrides: Partial<NonNullable<BucketStateDocumentV3['compacted_state']>> = {}) {
|
|
37
|
+
return {
|
|
38
|
+
op_id: 5n,
|
|
39
|
+
checksum: 50n,
|
|
40
|
+
count: 5,
|
|
41
|
+
bytes: 50n,
|
|
42
|
+
chunks: 1,
|
|
43
|
+
at: new Date(0),
|
|
44
|
+
...overrides
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('V3 compact utilities', () => {
|
|
49
|
+
test.each([
|
|
50
|
+
{
|
|
51
|
+
name: 'uses the minimum interval before the first full compact',
|
|
52
|
+
state: bucketState(),
|
|
53
|
+
expected: new Date(10_000)
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'scales the interval by the uncompacted row ratio',
|
|
57
|
+
state: bucketState({
|
|
58
|
+
bucket_stats: { count: 100, bytes: 100n, chunks: 1 },
|
|
59
|
+
last_full_compact: { op_id: 90n, count: 90, puts: 90, at: new Date(0) }
|
|
60
|
+
}),
|
|
61
|
+
expected: new Date(100_000)
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'uses the maximum interval when there are no uncompacted rows',
|
|
65
|
+
state: bucketState({
|
|
66
|
+
bucket_stats: { count: 100, bytes: 100n, chunks: 1 },
|
|
67
|
+
last_full_compact: { op_id: 10n, count: 100, puts: 100, at: new Date(0) }
|
|
68
|
+
}),
|
|
69
|
+
expected: new Date(100_000)
|
|
70
|
+
}
|
|
71
|
+
])('$name', ({ state, expected }) => {
|
|
72
|
+
expect(fullCompactionCheckAt(state, INTERVAL_CONFIG)).toEqual(expected);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('rejects a scheduled bucket without an uncompacted write', () => {
|
|
76
|
+
expect(() => fullCompactionCheckAt(bucketState({ first_uncompacted_write: undefined }), INTERVAL_CONFIG)).toThrow(
|
|
77
|
+
'Scheduled V3 bucket bucket[] has no first uncompacted write'
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('chooses a full compact when its check is due', () => {
|
|
82
|
+
const decision = chooseCompactionKind(
|
|
83
|
+
bucketState({ bucket_stats: { count: 10, bytes: 100n, chunks: 10 } }),
|
|
84
|
+
new Date(10_000),
|
|
85
|
+
INTERVAL_CONFIG
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(decision.kind).toBe(CompactionKind.Full);
|
|
89
|
+
expect(decision.nextCompactCheck).toEqual({
|
|
90
|
+
$min: [new Date(70_000), { $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: 1_000 } }]
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('chooses a chunk compact after enough chunks and the minimum interval', () => {
|
|
95
|
+
const decision = chooseCompactionKind(
|
|
96
|
+
bucketState({
|
|
97
|
+
bucket_stats: { count: 10, bytes: 100n, chunks: 9 },
|
|
98
|
+
compacted_state: compactedState({ chunks: 1 })
|
|
99
|
+
}),
|
|
100
|
+
new Date(5_000),
|
|
101
|
+
INTERVAL_CONFIG
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(decision.kind).toBe(CompactionKind.Chunks);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('waits when the chunk interval has not elapsed', () => {
|
|
108
|
+
const decision = chooseCompactionKind(
|
|
109
|
+
bucketState({
|
|
110
|
+
bucket_stats: { count: 10, bytes: 100n, chunks: 9 },
|
|
111
|
+
compacted_state: compactedState({ chunks: 1, at: new Date(4_500) })
|
|
112
|
+
}),
|
|
113
|
+
new Date(5_000),
|
|
114
|
+
INTERVAL_CONFIG
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(decision.kind).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('schedules only the full check when too few chunks were added', () => {
|
|
121
|
+
const decision = chooseCompactionKind(
|
|
122
|
+
bucketState({ bucket_stats: { count: 7, bytes: 70n, chunks: 7 } }),
|
|
123
|
+
new Date(5_000),
|
|
124
|
+
INTERVAL_CONFIG
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(decision).toEqual({ kind: null, nextCompactCheck: new Date(70_000) });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test.each([
|
|
131
|
+
{
|
|
132
|
+
name: 'does not force an unspecified kind',
|
|
133
|
+
forceKind: undefined,
|
|
134
|
+
compactedOpId: undefined,
|
|
135
|
+
maxOpIdCap: undefined,
|
|
136
|
+
expected: null
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'forces work without previous compacted state',
|
|
140
|
+
forceKind: CompactionKind.Chunks,
|
|
141
|
+
compactedOpId: undefined,
|
|
142
|
+
maxOpIdCap: undefined,
|
|
143
|
+
expected: CompactionKind.Chunks
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'skips work already compacted through the cap',
|
|
147
|
+
forceKind: CompactionKind.Chunks,
|
|
148
|
+
compactedOpId: 5n,
|
|
149
|
+
maxOpIdCap: 5n,
|
|
150
|
+
expected: null
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'skips work already compacted through the bucket head',
|
|
154
|
+
forceKind: CompactionKind.Chunks,
|
|
155
|
+
compactedOpId: 10n,
|
|
156
|
+
maxOpIdCap: undefined,
|
|
157
|
+
expected: null
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'forces work beyond the compacted state',
|
|
161
|
+
forceKind: CompactionKind.Chunks,
|
|
162
|
+
compactedOpId: 5n,
|
|
163
|
+
maxOpIdCap: 6n,
|
|
164
|
+
expected: CompactionKind.Chunks
|
|
165
|
+
}
|
|
166
|
+
])('$name', ({ forceKind, compactedOpId, maxOpIdCap, expected }) => {
|
|
167
|
+
const state = bucketState({
|
|
168
|
+
compacted_state: compactedOpId == null ? undefined : compactedState({ op_id: compactedOpId })
|
|
169
|
+
});
|
|
170
|
+
expect(forcedCompactionKind(state, forceKind, { maxOpIdCap })).toBe(expected);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('derives and combines bucket statistics', () => {
|
|
174
|
+
const state = bucketState({ bucket_stats: { count: 12, bytes: 120n, chunks: 3 } });
|
|
175
|
+
expect(bucketStats(state)).toEqual({ count: 12, bytes: 120n, chunks: 3 });
|
|
176
|
+
expect(emptyBucketStats()).toEqual({ count: 0, bytes: 0n, chunks: 0, checksum: 0 });
|
|
177
|
+
expect(statsForDocument({ count: 2, size: 20, checksum: 10n })).toEqual({
|
|
178
|
+
count: 2,
|
|
179
|
+
bytes: 20n,
|
|
180
|
+
chunks: 1,
|
|
181
|
+
checksum: 10
|
|
182
|
+
});
|
|
183
|
+
expect(
|
|
184
|
+
statsForDocuments([
|
|
185
|
+
{ count: 2, size: 20, checksum: 10n },
|
|
186
|
+
{ count: 3, size: 30, checksum: 20n }
|
|
187
|
+
])
|
|
188
|
+
).toEqual({ count: 5, bytes: 50n, chunks: 2, checksum: 30 });
|
|
189
|
+
expect(
|
|
190
|
+
combineAdjacentStats(
|
|
191
|
+
{ count: 2, bytes: 20n, chunks: 1, checksum: 10 },
|
|
192
|
+
{ count: 3, bytes: 30n, chunks: 2, checksum: 20 }
|
|
193
|
+
)
|
|
194
|
+
).toEqual({ count: 5, bytes: 50n, chunks: 3, checksum: 30 });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('replaces an overlapping chunk in cached statistics', () => {
|
|
198
|
+
expect(
|
|
199
|
+
combineChunkStats(
|
|
200
|
+
compactedState({ count: 10, bytes: 100n, chunks: 4, checksum: 100n }),
|
|
201
|
+
{ count: 6, bytes: 60n, chunks: 2, checksum: 50 },
|
|
202
|
+
{ count: 3, bytes: 30n, chunks: 1, checksum: 30 }
|
|
203
|
+
)
|
|
204
|
+
).toEqual({ count: 13, bytes: 130n, chunks: 5, checksum: 120 });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('applies compacted statistics as a delta to total bucket statistics', () => {
|
|
208
|
+
expect(
|
|
209
|
+
applyCompactionDelta(
|
|
210
|
+
{ count: 20, bytes: 200n, chunks: 8 },
|
|
211
|
+
{ count: 5, bytes: 50n, chunks: 3, checksum: 10 },
|
|
212
|
+
{ count: 3, bytes: 30n, chunks: 1, checksum: 10 }
|
|
213
|
+
)
|
|
214
|
+
).toEqual({ count: 18, bytes: 180n, chunks: 6 });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('applies a stored range replacement to statistics including its checksum', () => {
|
|
218
|
+
expect(
|
|
219
|
+
applyStatsReplacement(
|
|
220
|
+
{ count: 20, bytes: 200n, chunks: 8, checksum: 100 },
|
|
221
|
+
{ count: 5, bytes: 50n, chunks: 3, checksum: 40 },
|
|
222
|
+
{ count: 3, bytes: 30n, chunks: 1, checksum: 20 }
|
|
223
|
+
)
|
|
224
|
+
).toEqual({ count: 18, bytes: 180n, chunks: 6, checksum: 80 });
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ObjectStorage,
|
|
3
|
+
ObjectStorageOperationOptions,
|
|
4
|
+
ObjectStoragePutMetadata
|
|
5
|
+
} from '@module/storage/implementation/v3/object-storage/ObjectStorage.js';
|
|
6
|
+
|
|
7
|
+
export class MemoryObjectStorage implements ObjectStorage {
|
|
8
|
+
/**
|
|
9
|
+
* Public for testing purposes.
|
|
10
|
+
*/
|
|
11
|
+
public readonly store = new Map<string, { data: Uint8Array; metadata: ObjectStoragePutMetadata }>();
|
|
12
|
+
|
|
13
|
+
async put(
|
|
14
|
+
path: string,
|
|
15
|
+
data: Uint8Array,
|
|
16
|
+
metadata: ObjectStoragePutMetadata,
|
|
17
|
+
options?: ObjectStorageOperationOptions
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
options?.signal?.throwIfAborted();
|
|
20
|
+
this.store.set(path, { data, metadata: metadata });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async get(
|
|
24
|
+
path: string,
|
|
25
|
+
options?: ObjectStorageOperationOptions
|
|
26
|
+
): Promise<{ data: Uint8Array; metadata: ObjectStoragePutMetadata }> {
|
|
27
|
+
const signal = options?.signal;
|
|
28
|
+
signal?.throwIfAborted();
|
|
29
|
+
const data = this.store.get(path);
|
|
30
|
+
if (!data) {
|
|
31
|
+
throw new Error(`NotFound: ${path}`);
|
|
32
|
+
}
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async *list(prefix: string, options?: ObjectStorageOperationOptions): AsyncIterable<string> {
|
|
37
|
+
const signal = options?.signal;
|
|
38
|
+
for (const path of this.store.keys()) {
|
|
39
|
+
signal?.throwIfAborted();
|
|
40
|
+
if (path.startsWith(prefix)) {
|
|
41
|
+
yield path;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async delete(paths: string[], options?: ObjectStorageOperationOptions): Promise<void> {
|
|
47
|
+
options?.signal?.throwIfAborted();
|
|
48
|
+
for (const p of paths) {
|
|
49
|
+
this.store.delete(p);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async deletePrefix(prefix: string, options?: ObjectStorageOperationOptions): Promise<{ objectCount: number }> {
|
|
54
|
+
const paths: string[] = [];
|
|
55
|
+
for await (const path of this.list(prefix, options)) {
|
|
56
|
+
paths.push(path);
|
|
57
|
+
}
|
|
58
|
+
await this.delete(paths, options);
|
|
59
|
+
return { objectCount: paths.length };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ObjectStorage } from '@module/storage/implementation/v3/object-storage/ObjectStorage.js';
|
|
2
|
+
import { S3ObjectStorage } from '@module/storage/implementation/v3/object-storage/S3ObjectStorage.js';
|
|
3
|
+
import { mongoTestStorageFactoryGenerator } from '@module/utils/test-utils.js';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { MemoryObjectStorage } from './MemoryObjectStorage.js';
|
|
6
|
+
|
|
7
|
+
const minioStorages: S3ObjectStorage[] = [];
|
|
8
|
+
|
|
9
|
+
export interface S3TestFactoryOptions {
|
|
10
|
+
url: string;
|
|
11
|
+
isCI: boolean;
|
|
12
|
+
inlineThresholdBytes?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createTestStorageSuite(options: S3TestFactoryOptions, objectStorage: ObjectStorage) {
|
|
16
|
+
return {
|
|
17
|
+
objectStorage,
|
|
18
|
+
factoryGen: mongoTestStorageFactoryGenerator({
|
|
19
|
+
url: options.url,
|
|
20
|
+
isCI: options.isCI,
|
|
21
|
+
objectStorage,
|
|
22
|
+
inlineThresholdBytes: options.inlineThresholdBytes ?? 0
|
|
23
|
+
})
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates an ObjectStorage instance for S3 tests.
|
|
29
|
+
* Set MINIO_ENDPOINT to switch all S3 tests from MemoryObjectStorage
|
|
30
|
+
* to a real MinIO/S3 endpoint.
|
|
31
|
+
* MINIO_ENDPOINT=http://localhost:9000
|
|
32
|
+
*/
|
|
33
|
+
export function createS3TestStorageSuite(options: S3TestFactoryOptions) {
|
|
34
|
+
const minioEndpoint = process.env.MINIO_ENDPOINT;
|
|
35
|
+
let objectStorage: ObjectStorage;
|
|
36
|
+
if (minioEndpoint) {
|
|
37
|
+
const s3 = new S3ObjectStorage({
|
|
38
|
+
bucket: 'powersync-s3-test',
|
|
39
|
+
region: 'us-east-1',
|
|
40
|
+
prefix: `test-${process.pid}-${randomUUID()}`,
|
|
41
|
+
endpoint: minioEndpoint,
|
|
42
|
+
forcePathStyle: true,
|
|
43
|
+
accessKeyId: process.env.MINIO_ACCESS_KEY ?? 'minioadmin',
|
|
44
|
+
secretAccessKey: process.env.MINIO_SECRET_KEY ?? 'minioadmin'
|
|
45
|
+
});
|
|
46
|
+
minioStorages.push(s3);
|
|
47
|
+
objectStorage = s3;
|
|
48
|
+
} else {
|
|
49
|
+
objectStorage = new MemoryObjectStorage();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return createTestStorageSuite(options, objectStorage);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Remove all objects created by MinIO-backed suites in the current test. */
|
|
56
|
+
export async function cleanupS3TestStorage(): Promise<void> {
|
|
57
|
+
for (const storage of minioStorages.splice(0)) {
|
|
58
|
+
await storage.deletePrefix('bucket-data/');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Creates an explicitly memory-backed suite for tests that inspect stored objects. */
|
|
63
|
+
export function createMemoryS3TestStorageSuite(options: S3TestFactoryOptions) {
|
|
64
|
+
const objectStorage = new MemoryObjectStorage();
|
|
65
|
+
return {
|
|
66
|
+
...createTestStorageSuite(options, objectStorage),
|
|
67
|
+
objectStorage
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { ObjectStorageLifecycle } from '../../src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js';
|
|
4
|
+
import { MemoryObjectStorage } from './helpers/MemoryObjectStorage.js';
|
|
5
|
+
|
|
6
|
+
describe('ObjectStorageLifecycle', () => {
|
|
7
|
+
test('escapes bucket names as path segments', () => {
|
|
8
|
+
const lifecycle = new ObjectStorageLifecycle(null as any, 12, null as any);
|
|
9
|
+
|
|
10
|
+
const path = lifecycle.allocatePath('definition', 'bucket/with spaces?and%escapes', 1n, 2n);
|
|
11
|
+
|
|
12
|
+
expect(path).toMatch(/^bucket-data\/12\/definition\/bucket%2Fwith%20spaces%3Fand%25escapes\/1-2-[0-9a-f-]+\.bson$/);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('hashes bucket path segments longer than 128 bytes', () => {
|
|
16
|
+
const lifecycle = new ObjectStorageLifecycle(null as any, 12, null as any);
|
|
17
|
+
const bucket = '💾/long bucket parameter/'.repeat(20);
|
|
18
|
+
const hash = createHash('sha256').update(bucket).digest('base64url');
|
|
19
|
+
|
|
20
|
+
const path = lifecycle.allocatePath('definition', bucket, 1n, 2n);
|
|
21
|
+
|
|
22
|
+
expect(path).toMatch(new RegExp(`^bucket-data/12/definition/sha256-${hash}/1-2-[0-9a-f-]+\\.bson$`));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('deletePrefix removes only objects under the exact prefix', async () => {
|
|
26
|
+
const objectStorage = new MemoryObjectStorage();
|
|
27
|
+
const lifecycle = new ObjectStorageLifecycle(null as any, 1, objectStorage);
|
|
28
|
+
for (const path of [
|
|
29
|
+
'bucket-data/1/definition/object-1.bson',
|
|
30
|
+
'bucket-data/12/definition/object-2.bson',
|
|
31
|
+
'bucket-data/1-other/definition/object-3.bson'
|
|
32
|
+
]) {
|
|
33
|
+
await objectStorage.put(path, new Uint8Array(), {
|
|
34
|
+
contentType: 'application/bson',
|
|
35
|
+
contentEncoding: null
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await expect(lifecycle.deletePrefix(lifecycle.streamPrefix())).resolves.toEqual({ objectCount: 1 });
|
|
40
|
+
expect([...objectStorage.store.keys()]).toEqual([
|
|
41
|
+
'bucket-data/12/definition/object-2.bson',
|
|
42
|
+
'bucket-data/1-other/definition/object-3.bson'
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('definition prefixes do not match ids with the same string prefix', async () => {
|
|
47
|
+
const objectStorage = new MemoryObjectStorage();
|
|
48
|
+
const lifecycle = new ObjectStorageLifecycle(null as any, 7, objectStorage);
|
|
49
|
+
for (const path of ['bucket-data/7/1/object-1.bson', 'bucket-data/7/12/object-2.bson']) {
|
|
50
|
+
await objectStorage.put(path, new Uint8Array(), {
|
|
51
|
+
contentType: 'application/bson',
|
|
52
|
+
contentEncoding: null
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await lifecycle.deletePrefix(lifecycle.definitionPrefix('1'));
|
|
57
|
+
expect([...objectStorage.store.keys()]).toEqual(['bucket-data/7/12/object-2.bson']);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { MongoSyncBucketStorage } from '@module/storage/implementation/createMongoSyncBucketStorage.js';
|
|
2
|
+
import { BucketDataDocumentV3, ObjectStorageUsageDocument } from '@module/storage/implementation/v3/models.js';
|
|
3
|
+
import {
|
|
4
|
+
OBJECT_STORAGE_USAGE_BASE_WRITER_ID,
|
|
5
|
+
ObjectStorageUsage
|
|
6
|
+
} from '@module/storage/implementation/v3/object-storage/ObjectStorageUsage.js';
|
|
7
|
+
import { VersionedPowerSyncMongoV3 } from '@module/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
|
|
8
|
+
import * as lib_mongo from '@powersync/lib-service-mongodb';
|
|
9
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
10
|
+
import { updateSyncRulesFromYaml } from '@powersync/service-core';
|
|
11
|
+
import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
12
|
+
import { describe, expect, test } from 'vitest';
|
|
13
|
+
import { INITIALIZED_MONGO_STORAGE_FACTORY } from './util.js';
|
|
14
|
+
|
|
15
|
+
const SYNC_RULES_YAML = `
|
|
16
|
+
bucket_definitions:
|
|
17
|
+
global:
|
|
18
|
+
data:
|
|
19
|
+
- SELECT id FROM items
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
type UsageContext = {
|
|
23
|
+
db: VersionedPowerSyncMongoV3;
|
|
24
|
+
bucketStorage: MongoSyncBucketStorage;
|
|
25
|
+
definitionId: BucketDefinitionId;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type UsageEntry = {
|
|
29
|
+
definition_id: BucketDefinitionId;
|
|
30
|
+
active_bytes: bigint;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
async function withUsageContext<T>(callback: (context: UsageContext) => Promise<T>): Promise<T> {
|
|
34
|
+
await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
|
|
35
|
+
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(SYNC_RULES_YAML, { storageVersion: 3 }));
|
|
36
|
+
const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
|
|
37
|
+
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
38
|
+
const definitionId = syncRules.syncConfigContent[0].mapping.allBucketDefinitionIds()[0];
|
|
39
|
+
return callback({ db, bucketStorage, definitionId });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function withTransaction<T>(
|
|
43
|
+
db: VersionedPowerSyncMongoV3,
|
|
44
|
+
callback: (session: mongo.ClientSession) => Promise<T>
|
|
45
|
+
): Promise<T> {
|
|
46
|
+
const session = db.client.startSession();
|
|
47
|
+
try {
|
|
48
|
+
return await session.withTransaction(() => callback(session));
|
|
49
|
+
} finally {
|
|
50
|
+
await session.endSession();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function readUsageEntries(
|
|
55
|
+
db: VersionedPowerSyncMongoV3,
|
|
56
|
+
replicationStreamId: number,
|
|
57
|
+
session?: mongo.ClientSession
|
|
58
|
+
): Promise<UsageEntry[]> {
|
|
59
|
+
const entries = await db.objectStorageUsage
|
|
60
|
+
.aggregate<{ _id: BucketDefinitionId; active_bytes: bigint }>(
|
|
61
|
+
[
|
|
62
|
+
{ $match: { '_id.g': replicationStreamId } },
|
|
63
|
+
{ $project: { definitions: { $objectToArray: '$definitions' } } },
|
|
64
|
+
{ $unwind: '$definitions' },
|
|
65
|
+
{ $group: { _id: '$definitions.k', active_bytes: { $sum: '$definitions.v' } } }
|
|
66
|
+
],
|
|
67
|
+
{ session, readConcern: 'snapshot' }
|
|
68
|
+
)
|
|
69
|
+
.toArray()
|
|
70
|
+
.catch((error) => {
|
|
71
|
+
if (lib_mongo.isMongoNamespaceNotFoundError(error)) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return entries.map((entry) => ({
|
|
78
|
+
definition_id: entry._id,
|
|
79
|
+
active_bytes: BigInt(entry.active_bytes ?? 0)
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function usageDocument(
|
|
84
|
+
replicationStreamId: number,
|
|
85
|
+
writerId: string,
|
|
86
|
+
definitions: Record<string, bigint>,
|
|
87
|
+
updatedAt = new Date(0)
|
|
88
|
+
): ObjectStorageUsageDocument {
|
|
89
|
+
return {
|
|
90
|
+
_id: { g: replicationStreamId, w: writerId },
|
|
91
|
+
updated_at: updatedAt,
|
|
92
|
+
definitions
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function bucketDataDocument(
|
|
97
|
+
replicationStreamId: number,
|
|
98
|
+
definitionId: BucketDefinitionId,
|
|
99
|
+
fileSize: number
|
|
100
|
+
): BucketDataDocumentV3 {
|
|
101
|
+
return {
|
|
102
|
+
_id: { b: `${replicationStreamId}-${definitionId}`, o: 1n },
|
|
103
|
+
min_op: 1n,
|
|
104
|
+
checksum: 1n,
|
|
105
|
+
count: 1,
|
|
106
|
+
size: fileSize,
|
|
107
|
+
storage_ref: {
|
|
108
|
+
path: `bucket-data/${replicationStreamId}/${definitionId}/1.bson`,
|
|
109
|
+
file_size: fileSize
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
describe('ObjectStorageUsage', () => {
|
|
115
|
+
test('counts a committed reference exactly once after an aborted transaction is retried', async () => {
|
|
116
|
+
await withUsageContext(async ({ db, bucketStorage, definitionId }) => {
|
|
117
|
+
const replicationStreamId = bucketStorage.replicationStreamId;
|
|
118
|
+
const writerId = 'replication-writer';
|
|
119
|
+
const usage = new ObjectStorageUsage(db, replicationStreamId, writerId);
|
|
120
|
+
const bucketData = db.bucketData(replicationStreamId, definitionId);
|
|
121
|
+
const document = bucketDataDocument(replicationStreamId, definitionId, 123);
|
|
122
|
+
|
|
123
|
+
await expect(
|
|
124
|
+
withTransaction(db, async (session) => {
|
|
125
|
+
await bucketData.insertOne(document, { session });
|
|
126
|
+
await usage.applyDelta(definitionId, 123n, session);
|
|
127
|
+
throw new Error('abort this attempt');
|
|
128
|
+
})
|
|
129
|
+
).rejects.toThrow('abort this attempt');
|
|
130
|
+
|
|
131
|
+
await withTransaction(db, async (session) => {
|
|
132
|
+
await bucketData.insertOne(document, { session });
|
|
133
|
+
await usage.applyDelta(definitionId, 123n, session);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(await bucketData.countDocuments({ storage_ref: { $exists: true } })).toBe(1);
|
|
137
|
+
await expect(readUsageEntries(db, replicationStreamId)).resolves.toEqual([
|
|
138
|
+
{ definition_id: definitionId, active_bytes: 123n }
|
|
139
|
+
]);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('snapshot usage reads observe a complete pre-fold view', async () => {
|
|
144
|
+
await withUsageContext(async ({ db, bucketStorage, definitionId }) => {
|
|
145
|
+
const replicationStreamId = bucketStorage.replicationStreamId;
|
|
146
|
+
const usage = new ObjectStorageUsage(db, replicationStreamId);
|
|
147
|
+
await db.objectStorageUsage.insertMany([
|
|
148
|
+
usageDocument(replicationStreamId, OBJECT_STORAGE_USAGE_BASE_WRITER_ID, { [definitionId]: 100n }),
|
|
149
|
+
usageDocument(replicationStreamId, 'replication', { [definitionId]: 20n }),
|
|
150
|
+
usageDocument(replicationStreamId, 'compactor', { [definitionId]: -5n })
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
const readSession = db.client.startSession();
|
|
154
|
+
try {
|
|
155
|
+
readSession.startTransaction({ readConcern: { level: 'snapshot' } });
|
|
156
|
+
await db.objectStorageUsage.findOne(
|
|
157
|
+
{ _id: { g: replicationStreamId, w: OBJECT_STORAGE_USAGE_BASE_WRITER_ID } },
|
|
158
|
+
{ session: readSession }
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
await usage.foldStaleWriterDeltas({ staleWriterMs: 0 });
|
|
162
|
+
const entries = await readUsageEntries(db, replicationStreamId, readSession);
|
|
163
|
+
|
|
164
|
+
await readSession.commitTransaction();
|
|
165
|
+
expect(entries).toEqual([{ definition_id: definitionId, active_bytes: 115n }]);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
await readSession.abortTransaction().catch(() => undefined);
|
|
168
|
+
throw error;
|
|
169
|
+
} finally {
|
|
170
|
+
await readSession.endSession();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await expect(readUsageEntries(db, replicationStreamId)).resolves.toEqual([
|
|
174
|
+
{ definition_id: definitionId, active_bytes: 115n }
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('folding preserves totals across both writer/folder commit orders', async () => {
|
|
180
|
+
await withUsageContext(async ({ db, bucketStorage, definitionId }) => {
|
|
181
|
+
const replicationStreamId = bucketStorage.replicationStreamId;
|
|
182
|
+
const writerId = 'reused-writer';
|
|
183
|
+
const usage = new ObjectStorageUsage(db, replicationStreamId, writerId);
|
|
184
|
+
|
|
185
|
+
await withTransaction(db, (session) => usage.applyDelta(definitionId, 10n, session));
|
|
186
|
+
await db.objectStorageUsage.updateOne(
|
|
187
|
+
{ _id: { g: replicationStreamId, w: writerId } },
|
|
188
|
+
{ $set: { updated_at: new Date(0) } }
|
|
189
|
+
);
|
|
190
|
+
await usage.foldStaleWriterDeltas({ staleWriterMs: 0 });
|
|
191
|
+
|
|
192
|
+
// The folder committed first and deleted the writer shard. The writer's
|
|
193
|
+
// next transaction must recreate it through the upsert.
|
|
194
|
+
await withTransaction(db, (session) => usage.applyDelta(definitionId, 4n, session));
|
|
195
|
+
expect((await readUsageEntries(db, replicationStreamId))[0].active_bytes).toBe(14n);
|
|
196
|
+
|
|
197
|
+
await db.objectStorageUsage.updateOne(
|
|
198
|
+
{ _id: { g: replicationStreamId, w: writerId } },
|
|
199
|
+
{ $set: { updated_at: new Date(0) } }
|
|
200
|
+
);
|
|
201
|
+
await usage.foldStaleWriterDeltas({ staleWriterMs: 0 });
|
|
202
|
+
|
|
203
|
+
// The writer committed before the folder. Folding the recreated shard
|
|
204
|
+
// must preserve the same visible total.
|
|
205
|
+
expect((await readUsageEntries(db, replicationStreamId))[0].active_bytes).toBe(14n);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('definition cleanup racing a fold removes only that definition, and stream clear is isolated', async () => {
|
|
210
|
+
await withUsageContext(async ({ db, bucketStorage, definitionId }) => {
|
|
211
|
+
const replicationStreamId = bucketStorage.replicationStreamId;
|
|
212
|
+
const otherStreamId = replicationStreamId + 1;
|
|
213
|
+
const otherDefinitionId = 'other_definition';
|
|
214
|
+
const usage = new ObjectStorageUsage(db, replicationStreamId);
|
|
215
|
+
|
|
216
|
+
await db.objectStorageUsage.insertMany([
|
|
217
|
+
usageDocument(replicationStreamId, OBJECT_STORAGE_USAGE_BASE_WRITER_ID, {
|
|
218
|
+
[definitionId]: 3n,
|
|
219
|
+
[otherDefinitionId]: 7n
|
|
220
|
+
}),
|
|
221
|
+
usageDocument(replicationStreamId, 'writer', { [definitionId]: 20n, [otherDefinitionId]: 11n }),
|
|
222
|
+
usageDocument(otherStreamId, 'other-writer', { [definitionId]: 50n })
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
await Promise.all([
|
|
226
|
+
usage.foldStaleWriterDeltas({ staleWriterMs: 0 }),
|
|
227
|
+
withTransaction(db, (session) => usage.removeDefinition(definitionId, session))
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
await expect(readUsageEntries(db, replicationStreamId)).resolves.toEqual([
|
|
231
|
+
{ definition_id: otherDefinitionId, active_bytes: 18n }
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
await withTransaction(db, async (session) => {
|
|
235
|
+
await new ObjectStorageUsage(db, otherStreamId).removeStream(session);
|
|
236
|
+
});
|
|
237
|
+
await db.objectStorageUsage.insertOne(usageDocument(otherStreamId, 'other-writer', { [definitionId]: 50n }));
|
|
238
|
+
|
|
239
|
+
await withTransaction(db, async (session) => {
|
|
240
|
+
await usage.removeStream(session);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await expect(ObjectStorageUsage.readAllDefinitionUsage(db)).resolves.toEqual([
|
|
244
|
+
{ replication_stream_id: otherStreamId, definition_id: definitionId, active_bytes: 50n }
|
|
245
|
+
]);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('returns no entries for an absent usage collection', async () => {
|
|
250
|
+
await withUsageContext(async ({ db, bucketStorage }) => {
|
|
251
|
+
await db.objectStorageUsage.drop().catch(() => undefined);
|
|
252
|
+
await expect(readUsageEntries(db, bucketStorage.replicationStreamId)).resolves.toEqual([]);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|