@powersync/service-module-mongodb-storage 0.0.0-dev-20260602073133 → 0.0.0-dev-20260803144812
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 +101 -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 +41 -16
- package/dist/storage/MongoBucketStorage.js +226 -71
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +56 -25
- package/dist/storage/implementation/MongoBucketBatch.js +110 -62
- 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 +17 -19
- package/dist/storage/implementation/MongoCompactor.js +80 -263
- 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 +7 -6
- package/dist/storage/implementation/MongoParameterCompactor.js +25 -2
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +13 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js +64 -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 +123 -0
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
- package/dist/storage/implementation/MongoStorageProvider.js +39 -4
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +57 -29
- package/dist/storage/implementation/MongoSyncBucketStorage.js +87 -61
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js +3 -3
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +2 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +171 -24
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +8 -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 +7 -2
- package/dist/storage/implementation/common/PersistedBatch.js +12 -0
- 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.js +17 -15
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +20 -13
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +9 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +61 -29
- 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 +11 -2
- package/dist/storage/implementation/v1/MongoCompactorV1.js +251 -2
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -19
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +82 -58
- 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/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/MongoBucketBatchV3.d.ts +8 -4
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +309 -259
- 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 +40 -2
- package/dist/storage/implementation/v3/MongoCompactorV3.js +647 -23
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +67 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +351 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +45 -30
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +272 -140
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +6 -3
- package/dist/storage/implementation/v3/PersistedBatchV3.js +84 -21
- 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 +15 -13
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +32 -27
- 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 +77 -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/models.d.ts +101 -21
- 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 +21 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +44 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +30 -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 +70 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +146 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +42 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +285 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/source-table-utils.d.ts +55 -0
- package/dist/storage/implementation/v3/source-table-utils.js +214 -0
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
- package/dist/storage/storage-index.d.ts +4 -4
- package/dist/storage/storage-index.js +4 -4
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/types/types.d.ts +49 -3
- package/dist/types/types.js +48 -1
- 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 +128 -4
- package/dist/utils/util.js.map +1 -1
- package/package.json +12 -19
- package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
- package/src/storage/MongoBucketStorage.ts +347 -88
- package/src/storage/implementation/MongoBucketBatch.ts +147 -82
- package/src/storage/implementation/MongoChecksums.ts +93 -153
- package/src/storage/implementation/MongoCompactor.ts +102 -316
- package/src/storage/implementation/MongoHydrationState.ts +61 -0
- package/src/storage/implementation/MongoParameterCompactor.ts +25 -6
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +88 -0
- package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +154 -0
- package/src/storage/implementation/MongoStorageProvider.ts +46 -4
- package/src/storage/implementation/MongoSyncBucketStorage.ts +121 -87
- package/src/storage/implementation/MongoSyncRulesLock.ts +7 -4
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +202 -28
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +9 -0
- package/src/storage/implementation/common/PersistedBatch.ts +19 -3
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
- package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
- package/src/storage/implementation/db.ts +19 -20
- package/src/storage/implementation/models.ts +20 -14
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +75 -37
- package/src/storage/implementation/v1/MongoChecksumsV1.ts +154 -12
- package/src/storage/implementation/v1/MongoCompactorV1.ts +294 -9
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +142 -105
- 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 +11 -1
- package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +382 -321
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +250 -32
- package/src/storage/implementation/v3/MongoCompactorV3.ts +847 -32
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +482 -0
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +359 -187
- package/src/storage/implementation/v3/PersistedBatchV3.ts +118 -37
- package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +46 -36
- package/src/storage/implementation/v3/bucket-format.ts +97 -0
- package/src/storage/implementation/v3/chunking.ts +40 -0
- package/src/storage/implementation/v3/models.ts +132 -55
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +54 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +29 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +171 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +343 -0
- package/src/storage/implementation/v3/source-table-utils.ts +392 -0
- package/src/storage/storage-index.ts +12 -4
- package/src/types/types.ts +58 -1
- package/src/utils/test-utils.ts +8 -3
- package/src/utils/util.ts +185 -6
- package/test/src/__snapshots__/storage.test.ts.snap +0 -75
- package/test/src/__snapshots__/storage_sync.test.ts.snap +35 -617
- package/test/src/cleanup-stopped-sync-configs.test.ts +671 -0
- package/test/src/helpers/MemoryObjectStorage.ts +53 -0
- package/test/src/helpers/s3TestFactory.ts +69 -0
- package/test/src/object_storage_lifecycle.test.ts +59 -0
- package/test/src/setup.ts +6 -1
- package/test/src/storage.test.ts +227 -13
- package/test/src/storage_compacting.test.ts +1951 -12
- package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
- package/test/src/storage_s3_checksums.test.ts +191 -0
- package/test/src/storage_s3_compaction_lifecycle.test.ts +417 -0
- package/test/src/storage_s3_reading.test.ts +526 -0
- package/test/src/storage_s3_writing.test.ts +135 -0
- package/test/src/storage_sync.test.ts +1180 -73
- package/test/src/storeCurrentData.test.ts +227 -0
- package/test/src/util.ts +2 -1
- 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/MongoParameterCompactorV3.d.ts +0 -9
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +0 -18
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.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/MongoParameterCompactorV3.ts +0 -24
- package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -11
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -68
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DeleteObjectsCommand,
|
|
3
|
+
GetObjectCommand,
|
|
4
|
+
ListObjectsV2Command,
|
|
5
|
+
ListObjectsV2CommandOutput,
|
|
6
|
+
PutObjectCommand,
|
|
7
|
+
S3Client
|
|
8
|
+
} from '@aws-sdk/client-s3';
|
|
9
|
+
import { acquireSemaphoreAbortable, isAbortError } from '@powersync/service-core';
|
|
10
|
+
import { isThrottlingError, isTransientError } from '@smithy/core/retry';
|
|
11
|
+
import { Semaphore, SemaphoreInterface } from 'async-mutex';
|
|
12
|
+
import { ObjectStorageError, type ObjectStorage, type ObjectStoragePutMetadata } from './ObjectStorage.js';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_S3_OPERATION_CONCURRENCY = 16;
|
|
15
|
+
const S3_DELETE_PREFIX_BATCH_SIZE = 1000;
|
|
16
|
+
/**
|
|
17
|
+
* Slightly smaller than DEFAULT_S3_OPERATION_CONCURRENCY.
|
|
18
|
+
*/
|
|
19
|
+
const S3_DELETE_PREFIX_CONCURRENCY = 12;
|
|
20
|
+
const MAX_S3_PREFIX_BYTES = 256;
|
|
21
|
+
const SAFE_S3_KEY_BYTES = 896;
|
|
22
|
+
|
|
23
|
+
export interface S3ObjectStorageOptions {
|
|
24
|
+
bucket: string;
|
|
25
|
+
region?: string;
|
|
26
|
+
prefix?: string;
|
|
27
|
+
endpoint?: string;
|
|
28
|
+
forcePathStyle?: boolean;
|
|
29
|
+
accessKeyId?: string;
|
|
30
|
+
secretAccessKey?: string;
|
|
31
|
+
concurrencyLimit?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class S3ObjectStorage implements ObjectStorage {
|
|
35
|
+
/**
|
|
36
|
+
* Public for tests only.
|
|
37
|
+
*/
|
|
38
|
+
public readonly client: S3Client;
|
|
39
|
+
private bucket: string;
|
|
40
|
+
private prefix: string;
|
|
41
|
+
private readonly operationSemaphore: SemaphoreInterface;
|
|
42
|
+
|
|
43
|
+
constructor(options: S3ObjectStorageOptions) {
|
|
44
|
+
const concurrencyLimit = options.concurrencyLimit ?? DEFAULT_S3_OPERATION_CONCURRENCY;
|
|
45
|
+
if (!Number.isInteger(concurrencyLimit) || concurrencyLimit <= 0) {
|
|
46
|
+
throw new Error('S3 object storage concurrencyLimit must be a positive integer');
|
|
47
|
+
}
|
|
48
|
+
if ((options.accessKeyId == null) !== (options.secretAccessKey == null)) {
|
|
49
|
+
throw new Error('S3 object storage accessKeyId and secretAccessKey must be configured together');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const prefix = options.prefix ?? '';
|
|
53
|
+
if (Buffer.byteLength(prefix, 'utf8') > MAX_S3_PREFIX_BYTES) {
|
|
54
|
+
throw new Error(`S3 object storage prefix must be at most ${MAX_S3_PREFIX_BYTES} UTF-8 bytes`);
|
|
55
|
+
}
|
|
56
|
+
if (prefix.endsWith('/')) {
|
|
57
|
+
throw new Error('S3 object storage prefix must not end with "/"');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.bucket = options.bucket;
|
|
61
|
+
this.prefix = prefix;
|
|
62
|
+
this.operationSemaphore = new Semaphore(concurrencyLimit);
|
|
63
|
+
this.client = new S3Client({
|
|
64
|
+
region: options.region,
|
|
65
|
+
endpoint: options.endpoint,
|
|
66
|
+
forcePathStyle: options.forcePathStyle,
|
|
67
|
+
credentials:
|
|
68
|
+
options.accessKeyId && options.secretAccessKey
|
|
69
|
+
? { accessKeyId: options.accessKeyId, secretAccessKey: options.secretAccessKey }
|
|
70
|
+
: undefined
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async put(path: string, data: Uint8Array, metadata: ObjectStoragePutMetadata): Promise<void> {
|
|
75
|
+
const fullPath = this.fullPath(path);
|
|
76
|
+
await using _ = await this.withOperation();
|
|
77
|
+
try {
|
|
78
|
+
await this.client.send(
|
|
79
|
+
new PutObjectCommand({
|
|
80
|
+
Bucket: this.bucket,
|
|
81
|
+
Key: fullPath,
|
|
82
|
+
Body: data,
|
|
83
|
+
ContentType: metadata.contentType,
|
|
84
|
+
ContentEncoding: metadata.contentEncoding ?? undefined
|
|
85
|
+
})
|
|
86
|
+
);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw s3OperationError('upload', fullPath, error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async get(
|
|
93
|
+
path: string,
|
|
94
|
+
options?: { signal?: AbortSignal }
|
|
95
|
+
): Promise<{ data: Uint8Array; metadata: ObjectStoragePutMetadata }> {
|
|
96
|
+
const fullPath = this.fullPath(path);
|
|
97
|
+
const signal = options?.signal;
|
|
98
|
+
await using _ = await this.withOperation(signal);
|
|
99
|
+
try {
|
|
100
|
+
const response = await this.client.send(
|
|
101
|
+
new GetObjectCommand({
|
|
102
|
+
Bucket: this.bucket,
|
|
103
|
+
Key: fullPath
|
|
104
|
+
}),
|
|
105
|
+
{ abortSignal: signal }
|
|
106
|
+
);
|
|
107
|
+
const contentLength = response.ContentLength;
|
|
108
|
+
if (contentLength == null) {
|
|
109
|
+
throw new Error(`S3 download response for ${fullPath} is missing ContentLength`);
|
|
110
|
+
}
|
|
111
|
+
if (!Number.isSafeInteger(contentLength) || contentLength < 0) {
|
|
112
|
+
throw new Error(`S3 download response for ${fullPath} has invalid ContentLength ${contentLength}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const data = new Uint8Array(contentLength);
|
|
116
|
+
let offset = 0;
|
|
117
|
+
const stream = response.Body as AsyncIterable<Uint8Array>;
|
|
118
|
+
for await (const chunk of stream) {
|
|
119
|
+
signal?.throwIfAborted();
|
|
120
|
+
const nextOffset = offset + chunk.byteLength;
|
|
121
|
+
if (nextOffset > contentLength) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`S3 download ContentLength mismatch for ${fullPath}: expected ${contentLength} bytes, received at least ${nextOffset}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
data.set(chunk, offset);
|
|
127
|
+
offset = nextOffset;
|
|
128
|
+
}
|
|
129
|
+
if (offset !== contentLength) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`S3 download ContentLength mismatch for ${fullPath}: expected ${contentLength} bytes, received ${offset}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
data,
|
|
137
|
+
metadata: {
|
|
138
|
+
contentType: response.ContentType ?? 'application/octet-stream',
|
|
139
|
+
contentEncoding: response.ContentEncoding ?? null
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
} catch (err: any) {
|
|
143
|
+
if (err.name === 'NoSuchKey' || err.Code === 'NoSuchKey') {
|
|
144
|
+
throw new ObjectStorageError(`S3 object not found: ${fullPath}`, { cause: err, retryable: false });
|
|
145
|
+
}
|
|
146
|
+
throw s3OperationError('download', fullPath, err);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async *list(prefix: string, options?: { signal?: AbortSignal }): AsyncIterable<string> {
|
|
151
|
+
const fullPrefix = this.fullPath(prefix);
|
|
152
|
+
let continuationToken: string | undefined;
|
|
153
|
+
const signal = options?.signal;
|
|
154
|
+
|
|
155
|
+
do {
|
|
156
|
+
signal?.throwIfAborted();
|
|
157
|
+
const response = await this.listPage(fullPrefix, continuationToken, signal);
|
|
158
|
+
for (const object of response.Contents ?? []) {
|
|
159
|
+
if (object.Key == null) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
yield this.prefix ? object.Key.slice(this.prefix.length + 1) : object.Key;
|
|
163
|
+
}
|
|
164
|
+
continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined;
|
|
165
|
+
if (response.IsTruncated && continuationToken == null) {
|
|
166
|
+
throw new Error(`S3 listing for ${fullPrefix} was truncated without a continuation token`);
|
|
167
|
+
}
|
|
168
|
+
} while (continuationToken != null);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async delete(paths: string[]): Promise<void> {
|
|
172
|
+
const fullPaths = paths.map((path) => ({ Key: this.fullPath(path) }));
|
|
173
|
+
await this.deleteFullPaths(fullPaths);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async deletePrefix(prefix: string, options?: { signal?: AbortSignal }): Promise<{ objectCount: number }> {
|
|
177
|
+
const fullPrefix = this.fullPath(prefix);
|
|
178
|
+
const signal = options?.signal;
|
|
179
|
+
let continuationToken: string | undefined;
|
|
180
|
+
let objectCount = 0;
|
|
181
|
+
const deleting = new Set<Promise<void>>();
|
|
182
|
+
let deleteError: { error: unknown } | undefined;
|
|
183
|
+
|
|
184
|
+
const scheduleDelete = (fullPaths: { Key: string }[]) => {
|
|
185
|
+
let tracked: Promise<void>;
|
|
186
|
+
tracked = this.deleteFullPaths(fullPaths, signal)
|
|
187
|
+
.catch((error) => {
|
|
188
|
+
deleteError ??= { error };
|
|
189
|
+
})
|
|
190
|
+
.finally(() => deleting.delete(tracked));
|
|
191
|
+
deleting.add(tracked);
|
|
192
|
+
};
|
|
193
|
+
const throwDeleteError = () => {
|
|
194
|
+
if (deleteError != null) {
|
|
195
|
+
throw deleteError.error;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
let operationError: { error: unknown } | undefined;
|
|
200
|
+
try {
|
|
201
|
+
do {
|
|
202
|
+
signal?.throwIfAborted();
|
|
203
|
+
const response = await this.listPage(fullPrefix, continuationToken, signal);
|
|
204
|
+
throwDeleteError();
|
|
205
|
+
const fullPaths: { Key: string }[] = [];
|
|
206
|
+
for (const object of response.Contents ?? []) {
|
|
207
|
+
if (object.Key != null) {
|
|
208
|
+
fullPaths.push({ Key: object.Key });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
objectCount += fullPaths.length;
|
|
212
|
+
if (fullPaths.length !== 0) {
|
|
213
|
+
while (deleting.size >= S3_DELETE_PREFIX_CONCURRENCY) {
|
|
214
|
+
await Promise.race(deleting);
|
|
215
|
+
throwDeleteError();
|
|
216
|
+
}
|
|
217
|
+
scheduleDelete(fullPaths);
|
|
218
|
+
}
|
|
219
|
+
continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined;
|
|
220
|
+
if (response.IsTruncated && continuationToken == null) {
|
|
221
|
+
throw new Error(`S3 listing for ${fullPrefix} was truncated without a continuation token`);
|
|
222
|
+
}
|
|
223
|
+
} while (continuationToken != null);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
operationError = { error };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await Promise.all(deleting);
|
|
229
|
+
if (operationError != null) {
|
|
230
|
+
throw operationError.error;
|
|
231
|
+
}
|
|
232
|
+
throwDeleteError();
|
|
233
|
+
return { objectCount };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private async listPage(
|
|
237
|
+
fullPrefix: string,
|
|
238
|
+
continuationToken: string | undefined,
|
|
239
|
+
signal?: AbortSignal
|
|
240
|
+
): Promise<ListObjectsV2CommandOutput> {
|
|
241
|
+
try {
|
|
242
|
+
await using _ = await this.withOperation(signal);
|
|
243
|
+
return await this.client.send(
|
|
244
|
+
new ListObjectsV2Command({
|
|
245
|
+
Bucket: this.bucket,
|
|
246
|
+
Prefix: fullPrefix,
|
|
247
|
+
ContinuationToken: continuationToken,
|
|
248
|
+
MaxKeys: S3_DELETE_PREFIX_BATCH_SIZE
|
|
249
|
+
}),
|
|
250
|
+
{ abortSignal: signal }
|
|
251
|
+
);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
throw s3OperationError('list', fullPrefix, error);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private async deleteFullPaths(fullPaths: { Key: string }[], signal?: AbortSignal): Promise<void> {
|
|
258
|
+
if (fullPaths.length === 0) return;
|
|
259
|
+
await using _ = await this.withOperation(signal);
|
|
260
|
+
let response;
|
|
261
|
+
try {
|
|
262
|
+
response = await this.client.send(
|
|
263
|
+
new DeleteObjectsCommand({
|
|
264
|
+
Bucket: this.bucket,
|
|
265
|
+
Delete: { Objects: fullPaths, Quiet: true }
|
|
266
|
+
}),
|
|
267
|
+
{ abortSignal: signal }
|
|
268
|
+
);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
throw s3OperationError('delete', `${fullPaths.length} objects`, error);
|
|
271
|
+
}
|
|
272
|
+
if (response.Errors?.length) {
|
|
273
|
+
const errors = response.Errors.map((error) => `${error.Key}: ${error.Code}`).join(', ');
|
|
274
|
+
throw new ObjectStorageError(`Failed to delete S3 objects: ${errors}`, {
|
|
275
|
+
cause: response.Errors,
|
|
276
|
+
retryable: response.Errors.some((error) => isTransientS3Error({ name: error.Code }))
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private fullPath(path: string): string {
|
|
282
|
+
const fullPath = this.prefix ? `${this.prefix}/${path}` : path;
|
|
283
|
+
const size = Buffer.byteLength(fullPath, 'utf8');
|
|
284
|
+
if (size > SAFE_S3_KEY_BYTES) {
|
|
285
|
+
throw new Error(`S3 object key is ${size} bytes, exceeding the safe limit of ${SAFE_S3_KEY_BYTES} bytes`);
|
|
286
|
+
}
|
|
287
|
+
return fullPath;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private async withOperation(signal?: AbortSignal): Promise<AsyncDisposable> {
|
|
291
|
+
signal?.throwIfAborted();
|
|
292
|
+
const acquired = signal
|
|
293
|
+
? await acquireSemaphoreAbortable(this.operationSemaphore, signal)
|
|
294
|
+
: await this.operationSemaphore.acquire();
|
|
295
|
+
if (acquired === 'aborted') {
|
|
296
|
+
signal!.throwIfAborted();
|
|
297
|
+
throw new Error('S3 operation aborted while waiting for a concurrency slot');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const [, release] = acquired;
|
|
301
|
+
if (signal?.aborted) {
|
|
302
|
+
release();
|
|
303
|
+
signal.throwIfAborted();
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
[Symbol.asyncDispose]: async () => release()
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function s3OperationError(operation: string, target: string, error: unknown): Error {
|
|
312
|
+
if (error instanceof ObjectStorageError) {
|
|
313
|
+
return error;
|
|
314
|
+
}
|
|
315
|
+
// Preserve abort errors so callers can distinguish cancellation from an S3
|
|
316
|
+
// failure and avoid retrying work after shutdown.
|
|
317
|
+
if (isAbortError(error)) {
|
|
318
|
+
return error as Error;
|
|
319
|
+
}
|
|
320
|
+
const detail = error instanceof Error ? `: ${error.message}` : '';
|
|
321
|
+
return new ObjectStorageError(`S3 ${operation} failed for ${target}${detail}`, {
|
|
322
|
+
cause: error,
|
|
323
|
+
retryable: isTransientS3Error(error)
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function isTransientS3Error(error: unknown): boolean {
|
|
328
|
+
if (typeof error != 'object' || error == null) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
if (isAbortError(error)) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const sdkError = error as Parameters<typeof isTransientError>[0];
|
|
336
|
+
// Use the same classification as the AWS SDK retry middleware. The SDK
|
|
337
|
+
// returns the final error after its own attempt/quota limits are exhausted;
|
|
338
|
+
// this tells the compactor whether restarting the larger bucket operation is
|
|
339
|
+
// still appropriate.
|
|
340
|
+
// One exception is clock skew errors: We don't treat that as retryable by us,
|
|
341
|
+
// although the SDK may internally retry them.
|
|
342
|
+
return isThrottlingError(sdkError) || isTransientError(sdkError);
|
|
343
|
+
}
|