@veryfront/ext-blob-s3 0.1.1184

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.
@@ -0,0 +1,159 @@
1
+ import { Upload } from "@aws-sdk/lib-storage";
2
+ import { Readable } from "node:stream";
3
+ import { API_ERROR, INVALID_ARGUMENT } from "veryfront/errors";
4
+ export const MIN_MULTIPART_PART_SIZE = 5 * 1024 * 1024;
5
+ export const DEFAULT_MULTIPART_PART_SIZE = 8 * 1024 * 1024;
6
+ export const MAX_MULTIPART_PART_SIZE = 512 * 1024 * 1024;
7
+ export const DEFAULT_MULTIPART_QUEUE_SIZE = 2;
8
+ export const MAX_MULTIPART_QUEUE_SIZE = 16;
9
+ function invalidByteStream(detail, cause) {
10
+ return INVALID_ARGUMENT.create({ detail, cause });
11
+ }
12
+ async function cancelReader(reader, reason) {
13
+ try {
14
+ await reader.cancel(reason);
15
+ }
16
+ catch {
17
+ // The failure that stopped the upload remains authoritative.
18
+ }
19
+ }
20
+ async function uploadStream(client, input) {
21
+ input.signal?.throwIfAborted();
22
+ let reader;
23
+ try {
24
+ reader = input.stream.getReader();
25
+ }
26
+ catch (cause) {
27
+ throw invalidByteStream("S3BlobStorage: upload stream must be unlocked", cause);
28
+ }
29
+ let bytesRead = 0;
30
+ let readerFinished = false;
31
+ let readerCancelled = false;
32
+ let cancellationReason;
33
+ const cancelSource = async (reason) => {
34
+ if (readerFinished || readerCancelled)
35
+ return;
36
+ readerCancelled = true;
37
+ await cancelReader(reader, reason);
38
+ };
39
+ async function* bytes() {
40
+ try {
41
+ while (true) {
42
+ input.signal?.throwIfAborted();
43
+ const result = await reader.read();
44
+ input.signal?.throwIfAborted();
45
+ if (result.done) {
46
+ readerFinished = true;
47
+ return;
48
+ }
49
+ if (!(result.value instanceof Uint8Array)) {
50
+ throw invalidByteStream("S3BlobStorage: ReadableStream chunks must be Uint8Array values");
51
+ }
52
+ bytesRead += result.value.byteLength;
53
+ if (!Number.isSafeInteger(bytesRead)) {
54
+ throw invalidByteStream("S3BlobStorage: upload stream exceeds the supported size range");
55
+ }
56
+ yield result.value;
57
+ }
58
+ }
59
+ catch (error) {
60
+ cancellationReason = error;
61
+ await cancelSource(error);
62
+ throw error;
63
+ }
64
+ finally {
65
+ await cancelSource(cancellationReason);
66
+ reader.releaseLock();
67
+ }
68
+ }
69
+ const body = Readable.from(bytes(), { objectMode: false });
70
+ const abortController = new AbortController();
71
+ let upload;
72
+ let uploadAbortPromise;
73
+ let uploadAbortFailed = false;
74
+ let uploadAbortFailure;
75
+ const requestUploadAbort = () => {
76
+ if (!upload || uploadAbortPromise)
77
+ return;
78
+ const currentUpload = upload;
79
+ uploadAbortPromise = Promise.resolve()
80
+ .then(() => currentUpload.abort())
81
+ .catch((error) => {
82
+ uploadAbortFailed = true;
83
+ uploadAbortFailure = error;
84
+ });
85
+ };
86
+ let abortCancellation;
87
+ let rejectAbort;
88
+ const aborted = new Promise((_resolve, reject) => {
89
+ rejectAbort = reject;
90
+ });
91
+ const onAbort = () => {
92
+ const reason = input.signal?.reason;
93
+ cancellationReason = reason;
94
+ abortController.abort(reason);
95
+ abortCancellation ??= cancelSource(reason);
96
+ body.destroy();
97
+ requestUploadAbort();
98
+ rejectAbort?.(reason);
99
+ };
100
+ if (input.signal?.aborted)
101
+ onAbort();
102
+ else
103
+ input.signal?.addEventListener("abort", onAbort, { once: true });
104
+ let completed = false;
105
+ let done;
106
+ try {
107
+ const params = {
108
+ Bucket: input.bucket,
109
+ Key: input.key,
110
+ Body: body,
111
+ ContentType: input.mimeType,
112
+ Expires: input.expiresAt,
113
+ Metadata: input.metadata,
114
+ };
115
+ upload = new Upload({
116
+ client,
117
+ params,
118
+ abortController,
119
+ leavePartsOnError: false,
120
+ partSize: input.partSize,
121
+ queueSize: input.queueSize,
122
+ });
123
+ done = upload.done();
124
+ await Promise.race([done, aborted]);
125
+ if (!readerFinished) {
126
+ throw API_ERROR.create({
127
+ detail: "S3BlobStorage: multipart uploader did not consume the source stream",
128
+ });
129
+ }
130
+ completed = true;
131
+ return bytesRead;
132
+ }
133
+ catch (error) {
134
+ cancellationReason = error;
135
+ abortController.abort(error);
136
+ await cancelSource(error);
137
+ body.destroy();
138
+ requestUploadAbort();
139
+ await uploadAbortPromise;
140
+ if (uploadAbortFailed) {
141
+ throw new AggregateError([error, uploadAbortFailure], "S3BlobStorage: upload failed and multipart cleanup also failed");
142
+ }
143
+ throw error;
144
+ }
145
+ finally {
146
+ input.signal?.removeEventListener("abort", onAbort);
147
+ rejectAbort = undefined;
148
+ await abortCancellation;
149
+ await uploadAbortPromise;
150
+ if (!completed)
151
+ void done?.catch(() => undefined);
152
+ }
153
+ }
154
+ /** Create the official AWS multipart adapter for unknown-length Web streams. */
155
+ export function createS3BlobStorageStreamUploader(client) {
156
+ return {
157
+ upload: (input) => uploadStream(client, input),
158
+ };
159
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,82 @@
1
+ import { CreateBucketCommand, DeleteObjectCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
2
+ import { type BlobRef, type BlobStorage, type StoreBlobOptions } from "veryfront/workflow/blob";
3
+ import { type S3BlobStorageStreamUploader } from "./multipart-upload.js";
4
+ /** AWS commands issued through the injectable client boundary. */
5
+ export type S3BlobStorageCommand = CreateBucketCommand | DeleteObjectCommand | GetObjectCommand | HeadBucketCommand | HeadObjectCommand | PutObjectCommand;
6
+ /** Provider request options required by the storage implementation. */
7
+ export interface S3BlobStorageSendOptions {
8
+ abortSignal?: AbortSignal;
9
+ }
10
+ /** Minimal client boundary used by the storage implementation and deterministic tests. */
11
+ export interface S3BlobStorageClient {
12
+ send(command: S3BlobStorageCommand, options?: S3BlobStorageSendOptions): Promise<unknown>;
13
+ destroy?(): void;
14
+ }
15
+ /** Injectable provider seams for deterministic tests and custom transports. */
16
+ export interface S3BlobStorageDependencies {
17
+ /** S3-compatible command client. */
18
+ client?: S3BlobStorageClient;
19
+ /** Required for stream uploads when a custom client is supplied. */
20
+ streamUploader?: S3BlobStorageStreamUploader;
21
+ }
22
+ /** Explicit S3 connection, transport, and object-layout configuration. */
23
+ export interface S3BlobStorageConfig {
24
+ /** AWS region used by the client and bucket creation. */
25
+ region: string;
26
+ /** S3 bucket name. */
27
+ bucket: string;
28
+ /** AWS access-key id. */
29
+ accessKeyId: string;
30
+ /** AWS secret access key. */
31
+ secretAccessKey: string;
32
+ /** Optional temporary-credential session token. */
33
+ sessionToken?: string;
34
+ /** Optional S3-compatible endpoint. */
35
+ endpoint?: string;
36
+ /** Force path-style requests for S3-compatible services. */
37
+ forcePathStyle?: boolean;
38
+ /** Maximum attempts, including the initial request. Defaults to three. */
39
+ maxAttempts?: number;
40
+ /** Explicit SDK retry algorithm. Defaults to standard. */
41
+ retryMode?: "standard" | "adaptive";
42
+ /** Use AWS dual-stack endpoints. Defaults to false. */
43
+ useDualstackEndpoint?: boolean;
44
+ /** Use AWS FIPS endpoints. Defaults to false. */
45
+ useFipsEndpoint?: boolean;
46
+ /** Resolve an ARN's region instead of the configured region. Defaults to false. */
47
+ useArnRegion?: boolean;
48
+ /** Request-checksum policy. Defaults to the SDK's deterministic supported policy. */
49
+ requestChecksumCalculation?: "WHEN_SUPPORTED" | "WHEN_REQUIRED";
50
+ /** Response-checksum policy. Defaults to the SDK's deterministic supported policy. */
51
+ responseChecksumValidation?: "WHEN_SUPPORTED" | "WHEN_REQUIRED";
52
+ /** Disable S3 Express session authentication. Defaults to false. */
53
+ disableS3ExpressSessionAuth?: boolean;
54
+ /** Multipart part size for unknown-length streams. Defaults to 8 MiB. */
55
+ multipartPartSize?: number;
56
+ /** Concurrent multipart requests for unknown-length streams. Defaults to two. */
57
+ multipartQueueSize?: number;
58
+ /** Object-key prefix applied verbatim before each blob id. */
59
+ prefix?: string;
60
+ /** Public base URL used only to populate `BlobRef.url`. */
61
+ baseUrl?: string;
62
+ /** Default TTL in seconds. Zero means no expiry. */
63
+ defaultTtl?: number;
64
+ /** Check for and create a missing bucket before the first upload. */
65
+ autoCreateBucket?: boolean;
66
+ /** Cancellation signal applied to provider requests. */
67
+ signal?: AbortSignal;
68
+ }
69
+ /** AWS S3 implementation of the framework-owned `BlobStorage` interface. */
70
+ export declare class S3BlobStorage implements BlobStorage {
71
+ #private;
72
+ constructor(config: S3BlobStorageConfig, dependencies?: S3BlobStorageDependencies);
73
+ close(): void;
74
+ put(data: string | Uint8Array | Blob | ReadableStream, options?: StoreBlobOptions): Promise<BlobRef>;
75
+ getStream(id: string): Promise<ReadableStream | null>;
76
+ getText(id: string): Promise<string | null>;
77
+ getBytes(id: string): Promise<Uint8Array | null>;
78
+ delete(id: string): Promise<void>;
79
+ exists(id: string): Promise<boolean>;
80
+ stat(id: string): Promise<BlobRef | null>;
81
+ }
82
+ //# sourceMappingURL=s3-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3-storage.d.ts","sourceRoot":"","sources":["../src/s3-storage.ts"],"names":[],"mappings":"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"}