@howells/stow-client
Advanced tools
+78
-19
@@ -0,1 +1,8 @@ | ||
| /** Error thrown when an SDK request fails. */ | ||
| declare class StowError extends Error { | ||
| readonly status: number; | ||
| readonly code?: string; | ||
| constructor(message: string, status: number, code?: string); | ||
| } | ||
| /** | ||
@@ -28,9 +35,13 @@ * Stow Client SDK | ||
| */ | ||
| /** Error thrown when an SDK request fails. */ | ||
| declare class StowError extends Error { | ||
| readonly status: number; | ||
| readonly code?: string; | ||
| constructor(message: string, status: number, code?: string); | ||
| } | ||
| /** Configuration for creating a {@link StowClient} instance. */ | ||
| /** | ||
| * Configuration for creating a {@link StowClient} instance. | ||
| * | ||
| * `endpoint` must point at your own application endpoint, not Stow directly. | ||
| * That endpoint is expected to wrap `@howells/stow-server` and expose: | ||
| * - `POST {endpoint}/presign` | ||
| * - `POST {endpoint}/confirm` | ||
| * | ||
| * The browser package never handles API keys. | ||
| */ | ||
| interface StowClientConfig { | ||
@@ -43,3 +54,8 @@ /** | ||
| } | ||
| /** Result returned after a successful upload or dedupe short-circuit. */ | ||
| /** | ||
| * Result returned after a successful upload or dedupe short-circuit. | ||
| * | ||
| * When `deduped` is `true`, no bytes were uploaded because the server matched | ||
| * the file to an existing object using content-hash deduplication. | ||
| */ | ||
| interface UploadResult { | ||
@@ -53,3 +69,9 @@ contentType: string; | ||
| } | ||
| /** Progress payload emitted during `uploadFile`. */ | ||
| /** | ||
| * Progress payload emitted during `uploadFile()` and `uploadFiles()`. | ||
| * | ||
| * `phase` is useful for UI state because uploads have three distinct stages: | ||
| * presigning on your app server, uploading to object storage, and confirming | ||
| * the completed upload back through your app. | ||
| */ | ||
| interface UploadProgress { | ||
@@ -65,12 +87,36 @@ /** Bytes uploaded so far */ | ||
| } | ||
| /** Optional behavior for a single upload operation. */ | ||
| /** | ||
| * Optional behavior for a single upload operation. | ||
| * | ||
| * This object is reused by `uploadFile()` and `uploadFiles()`. | ||
| */ | ||
| interface UploadOptions { | ||
| /** Custom metadata to attach to the file */ | ||
| /** Custom metadata to persist on the Stow file record during confirm. */ | ||
| metadata?: Record<string, string>; | ||
| /** Progress callback */ | ||
| /** Progress callback for presign, upload, and confirm phases. */ | ||
| onProgress?: (progress: UploadProgress) => void; | ||
| /** Optional upload route (server-side configured route/path policy). */ | ||
| /** Optional route or folder hint forwarded to your server's presign endpoint. */ | ||
| route?: string; | ||
| } | ||
| /** Browser SDK for direct-to-R2 uploads via your server's presign/confirm endpoints. */ | ||
| /** | ||
| * Browser SDK for direct-to-storage uploads via your app's presign/confirm endpoints. | ||
| * | ||
| * This package is intentionally narrow: it only handles the browser side of the | ||
| * upload handshake. Pair it with `@howells/stow-next` if you want ready-made | ||
| * Next.js route handlers, or with `@howells/stow-server` if you are building | ||
| * your own backend endpoints. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const stow = new StowClient("/api/stow"); | ||
| * | ||
| * const result = await stow.uploadFile(file, { | ||
| * route: "products/featured", | ||
| * metadata: { sku: "SKU-123" }, | ||
| * onProgress: ({ phase, percent }) => { | ||
| * console.log(phase, percent); | ||
| * }, | ||
| * }); | ||
| * ``` | ||
| */ | ||
| declare class StowClient { | ||
@@ -84,2 +130,4 @@ private readonly endpoint; | ||
| * - `POST {endpoint}/confirm` (or a `confirmUrl` returned from presign) | ||
| * | ||
| * Both `"/api/stow"` and `{ endpoint: "/api/stow" }` are accepted. | ||
| */ | ||
@@ -98,10 +146,9 @@ constructor(config: StowClientConfig | string); | ||
| * confirm entirely, returning the existing file metadata with `deduped: true`. | ||
| * | ||
| * This method rejects with {@link StowError} for presign, network, R2, and | ||
| * confirm failures. | ||
| */ | ||
| uploadFile(file: File, options?: UploadOptions): Promise<UploadResult>; | ||
| /** | ||
| * Upload file directly to R2 using presigned URL with progress tracking | ||
| */ | ||
| private uploadToR2; | ||
| /** | ||
| * Upload multiple files with optional concurrency | ||
| * Upload multiple files with bounded concurrency. | ||
| * | ||
@@ -111,2 +158,14 @@ * - `concurrency=1` preserves strict sequential ordering. | ||
| * after the first error. | ||
| * | ||
| * The returned array preserves the original input order. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const results = await stow.uploadFiles(input.files, { | ||
| * concurrency: 4, | ||
| * onFileComplete: (result, index) => { | ||
| * console.log("uploaded", index, result.key); | ||
| * }, | ||
| * }); | ||
| * ``` | ||
| */ | ||
@@ -113,0 +172,0 @@ uploadFiles(files: FileList | File[], options?: UploadOptions & { |
+78
-19
@@ -0,1 +1,8 @@ | ||
| /** Error thrown when an SDK request fails. */ | ||
| declare class StowError extends Error { | ||
| readonly status: number; | ||
| readonly code?: string; | ||
| constructor(message: string, status: number, code?: string); | ||
| } | ||
| /** | ||
@@ -28,9 +35,13 @@ * Stow Client SDK | ||
| */ | ||
| /** Error thrown when an SDK request fails. */ | ||
| declare class StowError extends Error { | ||
| readonly status: number; | ||
| readonly code?: string; | ||
| constructor(message: string, status: number, code?: string); | ||
| } | ||
| /** Configuration for creating a {@link StowClient} instance. */ | ||
| /** | ||
| * Configuration for creating a {@link StowClient} instance. | ||
| * | ||
| * `endpoint` must point at your own application endpoint, not Stow directly. | ||
| * That endpoint is expected to wrap `@howells/stow-server` and expose: | ||
| * - `POST {endpoint}/presign` | ||
| * - `POST {endpoint}/confirm` | ||
| * | ||
| * The browser package never handles API keys. | ||
| */ | ||
| interface StowClientConfig { | ||
@@ -43,3 +54,8 @@ /** | ||
| } | ||
| /** Result returned after a successful upload or dedupe short-circuit. */ | ||
| /** | ||
| * Result returned after a successful upload or dedupe short-circuit. | ||
| * | ||
| * When `deduped` is `true`, no bytes were uploaded because the server matched | ||
| * the file to an existing object using content-hash deduplication. | ||
| */ | ||
| interface UploadResult { | ||
@@ -53,3 +69,9 @@ contentType: string; | ||
| } | ||
| /** Progress payload emitted during `uploadFile`. */ | ||
| /** | ||
| * Progress payload emitted during `uploadFile()` and `uploadFiles()`. | ||
| * | ||
| * `phase` is useful for UI state because uploads have three distinct stages: | ||
| * presigning on your app server, uploading to object storage, and confirming | ||
| * the completed upload back through your app. | ||
| */ | ||
| interface UploadProgress { | ||
@@ -65,12 +87,36 @@ /** Bytes uploaded so far */ | ||
| } | ||
| /** Optional behavior for a single upload operation. */ | ||
| /** | ||
| * Optional behavior for a single upload operation. | ||
| * | ||
| * This object is reused by `uploadFile()` and `uploadFiles()`. | ||
| */ | ||
| interface UploadOptions { | ||
| /** Custom metadata to attach to the file */ | ||
| /** Custom metadata to persist on the Stow file record during confirm. */ | ||
| metadata?: Record<string, string>; | ||
| /** Progress callback */ | ||
| /** Progress callback for presign, upload, and confirm phases. */ | ||
| onProgress?: (progress: UploadProgress) => void; | ||
| /** Optional upload route (server-side configured route/path policy). */ | ||
| /** Optional route or folder hint forwarded to your server's presign endpoint. */ | ||
| route?: string; | ||
| } | ||
| /** Browser SDK for direct-to-R2 uploads via your server's presign/confirm endpoints. */ | ||
| /** | ||
| * Browser SDK for direct-to-storage uploads via your app's presign/confirm endpoints. | ||
| * | ||
| * This package is intentionally narrow: it only handles the browser side of the | ||
| * upload handshake. Pair it with `@howells/stow-next` if you want ready-made | ||
| * Next.js route handlers, or with `@howells/stow-server` if you are building | ||
| * your own backend endpoints. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const stow = new StowClient("/api/stow"); | ||
| * | ||
| * const result = await stow.uploadFile(file, { | ||
| * route: "products/featured", | ||
| * metadata: { sku: "SKU-123" }, | ||
| * onProgress: ({ phase, percent }) => { | ||
| * console.log(phase, percent); | ||
| * }, | ||
| * }); | ||
| * ``` | ||
| */ | ||
| declare class StowClient { | ||
@@ -84,2 +130,4 @@ private readonly endpoint; | ||
| * - `POST {endpoint}/confirm` (or a `confirmUrl` returned from presign) | ||
| * | ||
| * Both `"/api/stow"` and `{ endpoint: "/api/stow" }` are accepted. | ||
| */ | ||
@@ -98,10 +146,9 @@ constructor(config: StowClientConfig | string); | ||
| * confirm entirely, returning the existing file metadata with `deduped: true`. | ||
| * | ||
| * This method rejects with {@link StowError} for presign, network, R2, and | ||
| * confirm failures. | ||
| */ | ||
| uploadFile(file: File, options?: UploadOptions): Promise<UploadResult>; | ||
| /** | ||
| * Upload file directly to R2 using presigned URL with progress tracking | ||
| */ | ||
| private uploadToR2; | ||
| /** | ||
| * Upload multiple files with optional concurrency | ||
| * Upload multiple files with bounded concurrency. | ||
| * | ||
@@ -111,2 +158,14 @@ * - `concurrency=1` preserves strict sequential ordering. | ||
| * after the first error. | ||
| * | ||
| * The returned array preserves the original input order. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const results = await stow.uploadFiles(input.files, { | ||
| * concurrency: 4, | ||
| * onFileComplete: (result, index) => { | ||
| * console.log("uploaded", index, result.key); | ||
| * }, | ||
| * }); | ||
| * ``` | ||
| */ | ||
@@ -113,0 +172,0 @@ uploadFiles(files: FileList | File[], options?: UploadOptions & { |
+82
-79
@@ -27,2 +27,4 @@ "use strict"; | ||
| module.exports = __toCommonJS(index_exports); | ||
| // src/stow-error.ts | ||
| var StowError = class extends Error { | ||
@@ -38,2 +40,30 @@ status; | ||
| }; | ||
| // src/index.ts | ||
| function uploadToR2(uploadUrl, file, onProgress) { | ||
| return new Promise((resolve, reject) => { | ||
| const xhr = new XMLHttpRequest(); | ||
| xhr.upload.addEventListener("progress", (event) => { | ||
| if (event.lengthComputable && onProgress) { | ||
| onProgress(event.loaded); | ||
| } | ||
| }); | ||
| xhr.addEventListener("load", () => { | ||
| if (xhr.status >= 200 && xhr.status < 300) { | ||
| resolve(void 0); | ||
| return; | ||
| } | ||
| reject(new StowError(`R2 upload failed with status ${xhr.status}`, xhr.status)); | ||
| }); | ||
| xhr.addEventListener("error", () => { | ||
| reject(new StowError("Network error during R2 upload", 0)); | ||
| }); | ||
| xhr.addEventListener("abort", () => { | ||
| reject(new StowError("R2 upload aborted", 0, "ABORTED")); | ||
| }); | ||
| xhr.open("PUT", uploadUrl); | ||
| xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream"); | ||
| xhr.send(file); | ||
| }); | ||
| } | ||
| var StowClient = class { | ||
@@ -47,9 +77,7 @@ endpoint; | ||
| * - `POST {endpoint}/confirm` (or a `confirmUrl` returned from presign) | ||
| * | ||
| * Both `"/api/stow"` and `{ endpoint: "/api/stow" }` are accepted. | ||
| */ | ||
| constructor(config) { | ||
| if (typeof config === "string") { | ||
| this.endpoint = config.replace(/\/+$/, ""); | ||
| } else { | ||
| this.endpoint = config.endpoint.replace(/\/+$/, ""); | ||
| } | ||
| this.endpoint = typeof config === "string" ? config.replace(/\/+$/, "") : config.endpoint.replace(/\/+$/, ""); | ||
| } | ||
@@ -80,2 +108,5 @@ endpointUrl(path) { | ||
| * confirm entirely, returning the existing file metadata with `deduped: true`. | ||
| * | ||
| * This method rejects with {@link StowError} for presign, network, R2, and | ||
| * confirm failures. | ||
| */ | ||
@@ -103,7 +134,3 @@ async uploadFile(file, options) { | ||
| const error = await presignResponse.json().catch(() => ({ error: "Presign failed" })); | ||
| throw new StowError( | ||
| error.error || "Presign failed", | ||
| presignResponse.status, | ||
| error.code | ||
| ); | ||
| throw new StowError(error.error || "Presign failed", presignResponse.status, error.code); | ||
| } | ||
@@ -132,3 +159,3 @@ const presign = await presignResponse.json(); | ||
| }); | ||
| await this.uploadToR2(presign.uploadUrl, file, (loaded) => { | ||
| await uploadToR2(presign.uploadUrl, file, (loaded) => { | ||
| onProgress?.({ | ||
@@ -147,22 +174,15 @@ loaded, | ||
| }); | ||
| const confirmResponse = await fetch( | ||
| this.resolveConfirmUrl(presign.confirmUrl), | ||
| { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| fileKey: presign.fileKey, | ||
| size: file.size, | ||
| contentType: file.type || "application/octet-stream", | ||
| ...metadata ? { metadata } : {} | ||
| }) | ||
| } | ||
| ); | ||
| const confirmResponse = await fetch(this.resolveConfirmUrl(presign.confirmUrl), { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| fileKey: presign.fileKey, | ||
| size: file.size, | ||
| contentType: file.type || "application/octet-stream", | ||
| ...metadata ? { metadata } : {} | ||
| }) | ||
| }); | ||
| if (!confirmResponse.ok) { | ||
| const error = await confirmResponse.json().catch(() => ({ error: "Confirm failed" })); | ||
| throw new StowError( | ||
| error.error || "Confirm failed", | ||
| confirmResponse.status, | ||
| error.code | ||
| ); | ||
| throw new StowError(error.error || "Confirm failed", confirmResponse.status, error.code); | ||
| } | ||
@@ -178,40 +198,3 @@ const result = await confirmResponse.json(); | ||
| /** | ||
| * Upload file directly to R2 using presigned URL with progress tracking | ||
| */ | ||
| uploadToR2(uploadUrl, file, onProgress) { | ||
| return new Promise((resolve, reject) => { | ||
| const xhr = new XMLHttpRequest(); | ||
| xhr.upload.addEventListener("progress", (event) => { | ||
| if (event.lengthComputable && onProgress) { | ||
| onProgress(event.loaded); | ||
| } | ||
| }); | ||
| xhr.addEventListener("load", () => { | ||
| if (xhr.status >= 200 && xhr.status < 300) { | ||
| resolve(); | ||
| } else { | ||
| reject( | ||
| new StowError( | ||
| `R2 upload failed with status ${xhr.status}`, | ||
| xhr.status | ||
| ) | ||
| ); | ||
| } | ||
| }); | ||
| xhr.addEventListener("error", () => { | ||
| reject(new StowError("Network error during R2 upload", 0)); | ||
| }); | ||
| xhr.addEventListener("abort", () => { | ||
| reject(new StowError("R2 upload aborted", 0, "ABORTED")); | ||
| }); | ||
| xhr.open("PUT", uploadUrl); | ||
| xhr.setRequestHeader( | ||
| "Content-Type", | ||
| file.type || "application/octet-stream" | ||
| ); | ||
| xhr.send(file); | ||
| }); | ||
| } | ||
| /** | ||
| * Upload multiple files with optional concurrency | ||
| * Upload multiple files with bounded concurrency. | ||
| * | ||
@@ -221,2 +204,14 @@ * - `concurrency=1` preserves strict sequential ordering. | ||
| * after the first error. | ||
| * | ||
| * The returned array preserves the original input order. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const results = await stow.uploadFiles(input.files, { | ||
| * concurrency: 4, | ||
| * onFileComplete: (result, index) => { | ||
| * console.log("uploaded", index, result.key); | ||
| * }, | ||
| * }); | ||
| * ``` | ||
| */ | ||
@@ -228,5 +223,9 @@ async uploadFiles(files, options) { | ||
| const results2 = []; | ||
| for (let i = 0; i < fileArray.length; i++) { | ||
| for (let i = 0; i < fileArray.length; i += 1) { | ||
| const file = fileArray[i]; | ||
| if (!file) { | ||
| continue; | ||
| } | ||
| try { | ||
| const result = await this.uploadFile(fileArray[i], options); | ||
| const result = await this.uploadFile(file, options); | ||
| results2.push(result); | ||
@@ -237,3 +236,3 @@ options?.onFileComplete?.(result, i); | ||
| error instanceof Error ? error : new Error("Upload failed"), | ||
| fileArray[i], | ||
| file, | ||
| i | ||
@@ -246,3 +245,5 @@ ); | ||
| } | ||
| const results = new Array(fileArray.length); | ||
| const results = Array.from({ | ||
| length: fileArray.length | ||
| }); | ||
| let nextIndex = 0; | ||
@@ -252,5 +253,10 @@ let firstError = null; | ||
| while (nextIndex < fileArray.length && !firstError) { | ||
| const i = nextIndex++; | ||
| const i = nextIndex; | ||
| nextIndex += 1; | ||
| const file = fileArray[i]; | ||
| if (!file) { | ||
| continue; | ||
| } | ||
| try { | ||
| const result = await this.uploadFile(fileArray[i], options); | ||
| const result = await this.uploadFile(file, options); | ||
| results[i] = result; | ||
@@ -260,5 +266,5 @@ options?.onFileComplete?.(result, i); | ||
| const err = error instanceof Error ? error : new Error("Upload failed"); | ||
| options?.onFileError?.(err, fileArray[i], i); | ||
| options?.onFileError?.(err, file, i); | ||
| if (!firstError) { | ||
| firstError = { error: err, file: fileArray[i], index: i }; | ||
| firstError = { error: err, file, index: i }; | ||
| } | ||
@@ -268,6 +274,3 @@ } | ||
| }; | ||
| const workers = Array.from( | ||
| { length: Math.min(concurrency, fileArray.length) }, | ||
| () => worker() | ||
| ); | ||
| const workers = Array.from({ length: Math.min(concurrency, fileArray.length) }, () => worker()); | ||
| await Promise.all(workers); | ||
@@ -274,0 +277,0 @@ if (firstError !== null) { |
+81
-80
@@ -1,2 +0,2 @@ | ||
| // src/index.ts | ||
| // src/stow-error.ts | ||
| var StowError = class extends Error { | ||
@@ -12,2 +12,30 @@ status; | ||
| }; | ||
| // src/index.ts | ||
| function uploadToR2(uploadUrl, file, onProgress) { | ||
| return new Promise((resolve, reject) => { | ||
| const xhr = new XMLHttpRequest(); | ||
| xhr.upload.addEventListener("progress", (event) => { | ||
| if (event.lengthComputable && onProgress) { | ||
| onProgress(event.loaded); | ||
| } | ||
| }); | ||
| xhr.addEventListener("load", () => { | ||
| if (xhr.status >= 200 && xhr.status < 300) { | ||
| resolve(void 0); | ||
| return; | ||
| } | ||
| reject(new StowError(`R2 upload failed with status ${xhr.status}`, xhr.status)); | ||
| }); | ||
| xhr.addEventListener("error", () => { | ||
| reject(new StowError("Network error during R2 upload", 0)); | ||
| }); | ||
| xhr.addEventListener("abort", () => { | ||
| reject(new StowError("R2 upload aborted", 0, "ABORTED")); | ||
| }); | ||
| xhr.open("PUT", uploadUrl); | ||
| xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream"); | ||
| xhr.send(file); | ||
| }); | ||
| } | ||
| var StowClient = class { | ||
@@ -21,9 +49,7 @@ endpoint; | ||
| * - `POST {endpoint}/confirm` (or a `confirmUrl` returned from presign) | ||
| * | ||
| * Both `"/api/stow"` and `{ endpoint: "/api/stow" }` are accepted. | ||
| */ | ||
| constructor(config) { | ||
| if (typeof config === "string") { | ||
| this.endpoint = config.replace(/\/+$/, ""); | ||
| } else { | ||
| this.endpoint = config.endpoint.replace(/\/+$/, ""); | ||
| } | ||
| this.endpoint = typeof config === "string" ? config.replace(/\/+$/, "") : config.endpoint.replace(/\/+$/, ""); | ||
| } | ||
@@ -54,2 +80,5 @@ endpointUrl(path) { | ||
| * confirm entirely, returning the existing file metadata with `deduped: true`. | ||
| * | ||
| * This method rejects with {@link StowError} for presign, network, R2, and | ||
| * confirm failures. | ||
| */ | ||
@@ -77,7 +106,3 @@ async uploadFile(file, options) { | ||
| const error = await presignResponse.json().catch(() => ({ error: "Presign failed" })); | ||
| throw new StowError( | ||
| error.error || "Presign failed", | ||
| presignResponse.status, | ||
| error.code | ||
| ); | ||
| throw new StowError(error.error || "Presign failed", presignResponse.status, error.code); | ||
| } | ||
@@ -106,3 +131,3 @@ const presign = await presignResponse.json(); | ||
| }); | ||
| await this.uploadToR2(presign.uploadUrl, file, (loaded) => { | ||
| await uploadToR2(presign.uploadUrl, file, (loaded) => { | ||
| onProgress?.({ | ||
@@ -121,22 +146,15 @@ loaded, | ||
| }); | ||
| const confirmResponse = await fetch( | ||
| this.resolveConfirmUrl(presign.confirmUrl), | ||
| { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| fileKey: presign.fileKey, | ||
| size: file.size, | ||
| contentType: file.type || "application/octet-stream", | ||
| ...metadata ? { metadata } : {} | ||
| }) | ||
| } | ||
| ); | ||
| const confirmResponse = await fetch(this.resolveConfirmUrl(presign.confirmUrl), { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| fileKey: presign.fileKey, | ||
| size: file.size, | ||
| contentType: file.type || "application/octet-stream", | ||
| ...metadata ? { metadata } : {} | ||
| }) | ||
| }); | ||
| if (!confirmResponse.ok) { | ||
| const error = await confirmResponse.json().catch(() => ({ error: "Confirm failed" })); | ||
| throw new StowError( | ||
| error.error || "Confirm failed", | ||
| confirmResponse.status, | ||
| error.code | ||
| ); | ||
| throw new StowError(error.error || "Confirm failed", confirmResponse.status, error.code); | ||
| } | ||
@@ -152,40 +170,3 @@ const result = await confirmResponse.json(); | ||
| /** | ||
| * Upload file directly to R2 using presigned URL with progress tracking | ||
| */ | ||
| uploadToR2(uploadUrl, file, onProgress) { | ||
| return new Promise((resolve, reject) => { | ||
| const xhr = new XMLHttpRequest(); | ||
| xhr.upload.addEventListener("progress", (event) => { | ||
| if (event.lengthComputable && onProgress) { | ||
| onProgress(event.loaded); | ||
| } | ||
| }); | ||
| xhr.addEventListener("load", () => { | ||
| if (xhr.status >= 200 && xhr.status < 300) { | ||
| resolve(); | ||
| } else { | ||
| reject( | ||
| new StowError( | ||
| `R2 upload failed with status ${xhr.status}`, | ||
| xhr.status | ||
| ) | ||
| ); | ||
| } | ||
| }); | ||
| xhr.addEventListener("error", () => { | ||
| reject(new StowError("Network error during R2 upload", 0)); | ||
| }); | ||
| xhr.addEventListener("abort", () => { | ||
| reject(new StowError("R2 upload aborted", 0, "ABORTED")); | ||
| }); | ||
| xhr.open("PUT", uploadUrl); | ||
| xhr.setRequestHeader( | ||
| "Content-Type", | ||
| file.type || "application/octet-stream" | ||
| ); | ||
| xhr.send(file); | ||
| }); | ||
| } | ||
| /** | ||
| * Upload multiple files with optional concurrency | ||
| * Upload multiple files with bounded concurrency. | ||
| * | ||
@@ -195,2 +176,14 @@ * - `concurrency=1` preserves strict sequential ordering. | ||
| * after the first error. | ||
| * | ||
| * The returned array preserves the original input order. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const results = await stow.uploadFiles(input.files, { | ||
| * concurrency: 4, | ||
| * onFileComplete: (result, index) => { | ||
| * console.log("uploaded", index, result.key); | ||
| * }, | ||
| * }); | ||
| * ``` | ||
| */ | ||
@@ -202,5 +195,9 @@ async uploadFiles(files, options) { | ||
| const results2 = []; | ||
| for (let i = 0; i < fileArray.length; i++) { | ||
| for (let i = 0; i < fileArray.length; i += 1) { | ||
| const file = fileArray[i]; | ||
| if (!file) { | ||
| continue; | ||
| } | ||
| try { | ||
| const result = await this.uploadFile(fileArray[i], options); | ||
| const result = await this.uploadFile(file, options); | ||
| results2.push(result); | ||
@@ -211,3 +208,3 @@ options?.onFileComplete?.(result, i); | ||
| error instanceof Error ? error : new Error("Upload failed"), | ||
| fileArray[i], | ||
| file, | ||
| i | ||
@@ -220,3 +217,5 @@ ); | ||
| } | ||
| const results = new Array(fileArray.length); | ||
| const results = Array.from({ | ||
| length: fileArray.length | ||
| }); | ||
| let nextIndex = 0; | ||
@@ -226,5 +225,10 @@ let firstError = null; | ||
| while (nextIndex < fileArray.length && !firstError) { | ||
| const i = nextIndex++; | ||
| const i = nextIndex; | ||
| nextIndex += 1; | ||
| const file = fileArray[i]; | ||
| if (!file) { | ||
| continue; | ||
| } | ||
| try { | ||
| const result = await this.uploadFile(fileArray[i], options); | ||
| const result = await this.uploadFile(file, options); | ||
| results[i] = result; | ||
@@ -234,5 +238,5 @@ options?.onFileComplete?.(result, i); | ||
| const err = error instanceof Error ? error : new Error("Upload failed"); | ||
| options?.onFileError?.(err, fileArray[i], i); | ||
| options?.onFileError?.(err, file, i); | ||
| if (!firstError) { | ||
| firstError = { error: err, file: fileArray[i], index: i }; | ||
| firstError = { error: err, file, index: i }; | ||
| } | ||
@@ -242,6 +246,3 @@ } | ||
| }; | ||
| const workers = Array.from( | ||
| { length: Math.min(concurrency, fileArray.length) }, | ||
| () => worker() | ||
| ); | ||
| const workers = Array.from({ length: Math.min(concurrency, fileArray.length) }, () => worker()); | ||
| await Promise.all(workers); | ||
@@ -248,0 +249,0 @@ if (firstError !== null) { |
+14
-14
| { | ||
| "name": "@howells/stow-client", | ||
| "version": "2.2.0", | ||
| "version": "2.4.0", | ||
| "description": "Client-side SDK for Stow file storage", | ||
| "keywords": [ | ||
| "file-storage", | ||
| "presigned-url", | ||
| "sdk", | ||
| "stow", | ||
| "upload" | ||
| ], | ||
| "homepage": "https://stow.sh", | ||
| "license": "MIT", | ||
@@ -11,9 +19,4 @@ "repository": { | ||
| }, | ||
| "homepage": "https://stow.sh", | ||
| "keywords": [ | ||
| "stow", | ||
| "file-storage", | ||
| "upload", | ||
| "presigned-url", | ||
| "sdk" | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
@@ -30,5 +33,2 @@ "main": "./dist/index.js", | ||
| }, | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "devDependencies": { | ||
@@ -38,3 +38,3 @@ "jsdom": "^29.0.0", | ||
| "typescript": "^5.9.3", | ||
| "vitest": "^4.1.0", | ||
| "vite-plus": "latest", | ||
| "@stow/typescript-config": "0.0.0" | ||
@@ -45,5 +45,5 @@ }, | ||
| "dev": "tsup src/index.ts --format cjs,esm --dts --watch", | ||
| "test": "vitest run", | ||
| "test:watch": "vitest" | ||
| "test": "vp test run", | ||
| "test:watch": "vp test" | ||
| } | ||
| } |
32703
14.72%684
9.27%