@howells/stow-client
Advanced tools
+4
-0
@@ -146,2 +146,6 @@ /** Error thrown when an SDK request fails. */ | ||
| uploadFile(file: File, options?: UploadOptions): Promise<UploadResult>; | ||
| /** Request a presigned upload from your server, returning either a fresh presign or a dedupe hit. */ | ||
| private fetchPresign; | ||
| /** Confirm a completed R2 upload through your server and return the stored file metadata. */ | ||
| private confirmUpload; | ||
| /** | ||
@@ -148,0 +152,0 @@ * Upload multiple files with bounded concurrency. |
+4
-0
@@ -146,2 +146,6 @@ /** Error thrown when an SDK request fails. */ | ||
| uploadFile(file: File, options?: UploadOptions): Promise<UploadResult>; | ||
| /** Request a presigned upload from your server, returning either a fresh presign or a dedupe hit. */ | ||
| private fetchPresign; | ||
| /** Confirm a completed R2 upload through your server and return the stored file metadata. */ | ||
| private confirmUpload; | ||
| /** | ||
@@ -148,0 +152,0 @@ * Upload multiple files with bounded concurrency. |
+78
-40
@@ -41,5 +41,23 @@ "use strict"; | ||
| // src/index.ts | ||
| var uploadToR2 = (uploadUrl, file, onProgress) => ( | ||
| // eslint-disable-next-line eslint-plugin-promise/avoid-new | ||
| new Promise((resolve, reject) => { | ||
| var DEFAULT_CONTENT_TYPE = "application/octet-stream"; | ||
| var isRecord = (value) => typeof value === "object" && value !== null; | ||
| var readErrorBody = async (response) => { | ||
| try { | ||
| const body = await response.json(); | ||
| if (!isRecord(body)) { | ||
| return {}; | ||
| } | ||
| return { | ||
| code: typeof body.code === "string" ? body.code : void 0, | ||
| error: typeof body.error === "string" && body.error !== "" ? body.error : void 0 | ||
| }; | ||
| } catch { | ||
| return {}; | ||
| } | ||
| }; | ||
| var isPresignResult = (value) => isRecord(value); | ||
| var isPresignDedupeResponse = (value) => isRecord(value) && value.dedupe === true; | ||
| var isConfirmResponse = (value) => isRecord(value); | ||
| var uploadToR2 = async (uploadUrl, file, onProgress) => { | ||
| await new Promise((resolve, reject) => { | ||
| const xhr = new XMLHttpRequest(); | ||
@@ -65,6 +83,6 @@ xhr.upload.addEventListener("progress", (event) => { | ||
| xhr.open("PUT", uploadUrl); | ||
| xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream"); | ||
| xhr.setRequestHeader("Content-Type", file.type || DEFAULT_CONTENT_TYPE); | ||
| xhr.send(file); | ||
| }) | ||
| ); | ||
| }); | ||
| }; | ||
| var StowClient = class { | ||
@@ -91,3 +109,3 @@ endpoint; | ||
| resolveConfirmUrl(confirmUrl) { | ||
| if (!confirmUrl) { | ||
| if (confirmUrl === void 0 || confirmUrl === "") { | ||
| return this.endpointUrl("/confirm"); | ||
@@ -114,3 +132,3 @@ } | ||
| async uploadFile(file, options) { | ||
| const { route, metadata, onProgress } = options || {}; | ||
| const { route, metadata, onProgress } = options ?? {}; | ||
| onProgress?.({ | ||
@@ -122,19 +140,4 @@ loaded: 0, | ||
| }); | ||
| const presignResponse = await fetch(this.endpointUrl("/presign"), { | ||
| body: JSON.stringify({ | ||
| contentType: file.type || "application/octet-stream", | ||
| filename: file.name, | ||
| route, | ||
| size: file.size, | ||
| ...metadata ? { metadata } : {} | ||
| }), | ||
| headers: { "Content-Type": "application/json" }, | ||
| method: "POST" | ||
| }); | ||
| if (!presignResponse.ok) { | ||
| const error = await presignResponse.json().catch(() => ({ error: "Presign failed" })); | ||
| throw new StowError(error.error || "Presign failed", presignResponse.status, error.code); | ||
| } | ||
| const presign = await presignResponse.json(); | ||
| if ("dedupe" in presign && presign.dedupe) { | ||
| const presign = await this.fetchPresign(file, route, metadata); | ||
| if (isPresignDedupeResponse(presign)) { | ||
| onProgress?.({ | ||
@@ -174,5 +177,32 @@ loaded: file.size, | ||
| }); | ||
| return await this.confirmUpload(presign, file, metadata); | ||
| } | ||
| /** Request a presigned upload from your server, returning either a fresh presign or a dedupe hit. */ | ||
| async fetchPresign(file, route, metadata) { | ||
| const presignResponse = await fetch(this.endpointUrl("/presign"), { | ||
| body: JSON.stringify({ | ||
| contentType: file.type || DEFAULT_CONTENT_TYPE, | ||
| filename: file.name, | ||
| route, | ||
| size: file.size, | ||
| ...metadata ? { metadata } : {} | ||
| }), | ||
| headers: { "Content-Type": "application/json" }, | ||
| method: "POST" | ||
| }); | ||
| if (!presignResponse.ok) { | ||
| const body = await readErrorBody(presignResponse); | ||
| throw new StowError(body.error ?? "Presign failed", presignResponse.status, body.code); | ||
| } | ||
| const presignBody = await presignResponse.json(); | ||
| if (!isPresignResult(presignBody)) { | ||
| throw new StowError("Invalid presign response from server", presignResponse.status); | ||
| } | ||
| return presignBody; | ||
| } | ||
| /** Confirm a completed R2 upload through your server and return the stored file metadata. */ | ||
| async confirmUpload(presign, file, metadata) { | ||
| const confirmResponse = await fetch(this.resolveConfirmUrl(presign.confirmUrl), { | ||
| body: JSON.stringify({ | ||
| contentType: file.type || "application/octet-stream", | ||
| contentType: file.type || DEFAULT_CONTENT_TYPE, | ||
| fileKey: presign.fileKey, | ||
@@ -186,11 +216,14 @@ size: file.size, | ||
| if (!confirmResponse.ok) { | ||
| const error = await confirmResponse.json().catch(() => ({ error: "Confirm failed" })); | ||
| throw new StowError(error.error || "Confirm failed", confirmResponse.status, error.code); | ||
| const body = await readErrorBody(confirmResponse); | ||
| throw new StowError(body.error ?? "Confirm failed", confirmResponse.status, body.code); | ||
| } | ||
| const result = await confirmResponse.json(); | ||
| const confirmBody = await confirmResponse.json(); | ||
| if (!isConfirmResponse(confirmBody)) { | ||
| throw new StowError("Invalid confirm response from server", confirmResponse.status); | ||
| } | ||
| return { | ||
| contentType: result.contentType, | ||
| key: result.key, | ||
| size: result.size, | ||
| url: result.url | ||
| contentType: confirmBody.contentType, | ||
| key: confirmBody.key, | ||
| size: confirmBody.size, | ||
| url: confirmBody.url | ||
| }; | ||
@@ -246,5 +279,5 @@ } | ||
| let nextIndex = 0; | ||
| let firstError = null; | ||
| const errors = []; | ||
| const worker = async () => { | ||
| while (nextIndex < fileArray.length && !firstError) { | ||
| while (nextIndex < fileArray.length && errors.length === 0) { | ||
| const i = nextIndex; | ||
@@ -263,4 +296,4 @@ nextIndex += 1; | ||
| options?.onFileError?.(err, file, i); | ||
| if (!firstError) { | ||
| firstError = { error: err, file, index: i }; | ||
| if (errors.length === 0) { | ||
| errors.push({ error: err, file, index: i }); | ||
| } | ||
@@ -270,8 +303,13 @@ } | ||
| }; | ||
| const workers = Array.from({ length: Math.min(concurrency, fileArray.length) }, () => worker()); | ||
| const workerCount = Math.min(concurrency, fileArray.length); | ||
| const workers = []; | ||
| for (let w = 0; w < workerCount; w += 1) { | ||
| workers.push(worker()); | ||
| } | ||
| await Promise.all(workers); | ||
| if (firstError !== null) { | ||
| const [firstError] = errors; | ||
| if (firstError !== void 0) { | ||
| throw firstError.error; | ||
| } | ||
| return results; | ||
| return results.filter((result) => result !== void 0); | ||
| } | ||
@@ -278,0 +316,0 @@ }; |
+78
-40
@@ -14,5 +14,23 @@ // src/stow-error.ts | ||
| // src/index.ts | ||
| var uploadToR2 = (uploadUrl, file, onProgress) => ( | ||
| // eslint-disable-next-line eslint-plugin-promise/avoid-new | ||
| new Promise((resolve, reject) => { | ||
| var DEFAULT_CONTENT_TYPE = "application/octet-stream"; | ||
| var isRecord = (value) => typeof value === "object" && value !== null; | ||
| var readErrorBody = async (response) => { | ||
| try { | ||
| const body = await response.json(); | ||
| if (!isRecord(body)) { | ||
| return {}; | ||
| } | ||
| return { | ||
| code: typeof body.code === "string" ? body.code : void 0, | ||
| error: typeof body.error === "string" && body.error !== "" ? body.error : void 0 | ||
| }; | ||
| } catch { | ||
| return {}; | ||
| } | ||
| }; | ||
| var isPresignResult = (value) => isRecord(value); | ||
| var isPresignDedupeResponse = (value) => isRecord(value) && value.dedupe === true; | ||
| var isConfirmResponse = (value) => isRecord(value); | ||
| var uploadToR2 = async (uploadUrl, file, onProgress) => { | ||
| await new Promise((resolve, reject) => { | ||
| const xhr = new XMLHttpRequest(); | ||
@@ -38,6 +56,6 @@ xhr.upload.addEventListener("progress", (event) => { | ||
| xhr.open("PUT", uploadUrl); | ||
| xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream"); | ||
| xhr.setRequestHeader("Content-Type", file.type || DEFAULT_CONTENT_TYPE); | ||
| xhr.send(file); | ||
| }) | ||
| ); | ||
| }); | ||
| }; | ||
| var StowClient = class { | ||
@@ -64,3 +82,3 @@ endpoint; | ||
| resolveConfirmUrl(confirmUrl) { | ||
| if (!confirmUrl) { | ||
| if (confirmUrl === void 0 || confirmUrl === "") { | ||
| return this.endpointUrl("/confirm"); | ||
@@ -87,3 +105,3 @@ } | ||
| async uploadFile(file, options) { | ||
| const { route, metadata, onProgress } = options || {}; | ||
| const { route, metadata, onProgress } = options ?? {}; | ||
| onProgress?.({ | ||
@@ -95,19 +113,4 @@ loaded: 0, | ||
| }); | ||
| const presignResponse = await fetch(this.endpointUrl("/presign"), { | ||
| body: JSON.stringify({ | ||
| contentType: file.type || "application/octet-stream", | ||
| filename: file.name, | ||
| route, | ||
| size: file.size, | ||
| ...metadata ? { metadata } : {} | ||
| }), | ||
| headers: { "Content-Type": "application/json" }, | ||
| method: "POST" | ||
| }); | ||
| if (!presignResponse.ok) { | ||
| const error = await presignResponse.json().catch(() => ({ error: "Presign failed" })); | ||
| throw new StowError(error.error || "Presign failed", presignResponse.status, error.code); | ||
| } | ||
| const presign = await presignResponse.json(); | ||
| if ("dedupe" in presign && presign.dedupe) { | ||
| const presign = await this.fetchPresign(file, route, metadata); | ||
| if (isPresignDedupeResponse(presign)) { | ||
| onProgress?.({ | ||
@@ -147,5 +150,32 @@ loaded: file.size, | ||
| }); | ||
| return await this.confirmUpload(presign, file, metadata); | ||
| } | ||
| /** Request a presigned upload from your server, returning either a fresh presign or a dedupe hit. */ | ||
| async fetchPresign(file, route, metadata) { | ||
| const presignResponse = await fetch(this.endpointUrl("/presign"), { | ||
| body: JSON.stringify({ | ||
| contentType: file.type || DEFAULT_CONTENT_TYPE, | ||
| filename: file.name, | ||
| route, | ||
| size: file.size, | ||
| ...metadata ? { metadata } : {} | ||
| }), | ||
| headers: { "Content-Type": "application/json" }, | ||
| method: "POST" | ||
| }); | ||
| if (!presignResponse.ok) { | ||
| const body = await readErrorBody(presignResponse); | ||
| throw new StowError(body.error ?? "Presign failed", presignResponse.status, body.code); | ||
| } | ||
| const presignBody = await presignResponse.json(); | ||
| if (!isPresignResult(presignBody)) { | ||
| throw new StowError("Invalid presign response from server", presignResponse.status); | ||
| } | ||
| return presignBody; | ||
| } | ||
| /** Confirm a completed R2 upload through your server and return the stored file metadata. */ | ||
| async confirmUpload(presign, file, metadata) { | ||
| const confirmResponse = await fetch(this.resolveConfirmUrl(presign.confirmUrl), { | ||
| body: JSON.stringify({ | ||
| contentType: file.type || "application/octet-stream", | ||
| contentType: file.type || DEFAULT_CONTENT_TYPE, | ||
| fileKey: presign.fileKey, | ||
@@ -159,11 +189,14 @@ size: file.size, | ||
| if (!confirmResponse.ok) { | ||
| const error = await confirmResponse.json().catch(() => ({ error: "Confirm failed" })); | ||
| throw new StowError(error.error || "Confirm failed", confirmResponse.status, error.code); | ||
| const body = await readErrorBody(confirmResponse); | ||
| throw new StowError(body.error ?? "Confirm failed", confirmResponse.status, body.code); | ||
| } | ||
| const result = await confirmResponse.json(); | ||
| const confirmBody = await confirmResponse.json(); | ||
| if (!isConfirmResponse(confirmBody)) { | ||
| throw new StowError("Invalid confirm response from server", confirmResponse.status); | ||
| } | ||
| return { | ||
| contentType: result.contentType, | ||
| key: result.key, | ||
| size: result.size, | ||
| url: result.url | ||
| contentType: confirmBody.contentType, | ||
| key: confirmBody.key, | ||
| size: confirmBody.size, | ||
| url: confirmBody.url | ||
| }; | ||
@@ -219,5 +252,5 @@ } | ||
| let nextIndex = 0; | ||
| let firstError = null; | ||
| const errors = []; | ||
| const worker = async () => { | ||
| while (nextIndex < fileArray.length && !firstError) { | ||
| while (nextIndex < fileArray.length && errors.length === 0) { | ||
| const i = nextIndex; | ||
@@ -236,4 +269,4 @@ nextIndex += 1; | ||
| options?.onFileError?.(err, file, i); | ||
| if (!firstError) { | ||
| firstError = { error: err, file, index: i }; | ||
| if (errors.length === 0) { | ||
| errors.push({ error: err, file, index: i }); | ||
| } | ||
@@ -243,8 +276,13 @@ } | ||
| }; | ||
| const workers = Array.from({ length: Math.min(concurrency, fileArray.length) }, () => worker()); | ||
| const workerCount = Math.min(concurrency, fileArray.length); | ||
| const workers = []; | ||
| for (let w = 0; w < workerCount; w += 1) { | ||
| workers.push(worker()); | ||
| } | ||
| await Promise.all(workers); | ||
| if (firstError !== null) { | ||
| const [firstError] = errors; | ||
| if (firstError !== void 0) { | ||
| throw firstError.error; | ||
| } | ||
| return results; | ||
| return results.filter((result) => result !== void 0); | ||
| } | ||
@@ -251,0 +289,0 @@ }; |
+1
-1
| { | ||
| "name": "@howells/stow-client", | ||
| "version": "3.0.0", | ||
| "version": "3.1.0", | ||
| "description": "Client-side SDK for Stow visual media infrastructure", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
36346
10.43%766
11.66%