deepspace 0.11.0 → 0.13.0

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/dist/index.d.ts CHANGED
@@ -3636,6 +3636,12 @@ declare function toConnectionStatus(queryStatus: string): ConnectionStatus;
3636
3636
  *
3637
3637
  * Provides upload, download, list, and delete for files in R2.
3638
3638
  *
3639
+ * Uploads pick their own transport. A small file goes in one request; a large
3640
+ * one is chunked into parts with `Blob.slice`, so peak memory is bounded by
3641
+ * the PART SIZE rather than by the file — a 1 GiB video costs one part, not a
3642
+ * gibibyte — and the file ceiling becomes a product decision rather than a
3643
+ * request-size accident. Callers do nothing differently either way.
3644
+ *
3639
3645
  * Two scopes are supported via the `scope` option:
3640
3646
  * - `'self'` (default): per-user storage. Reads require auth.
3641
3647
  * - `'app'`: app-wide storage. Reads are public — the upload URL works
@@ -3668,10 +3674,29 @@ interface R2FileInfo {
3668
3674
  originalName?: string;
3669
3675
  uploadedBy?: string;
3670
3676
  }
3677
+ /** Told after each part lands, so a long upload can drive a progress bar. */
3678
+ type UploadProgress = (sent: number, total: number) => void;
3679
+ interface UploadOptions {
3680
+ onProgress?: UploadProgress;
3681
+ }
3671
3682
  interface UseR2FilesReturn {
3672
- /** Upload a File or Blob. Optionally provide a display name. */
3673
- upload: (file: File | Blob, name?: string) => Promise<R2UploadResult>;
3674
- /** Upload raw base64 data. */
3683
+ /**
3684
+ * Upload a File or Blob. Optionally provide a display name.
3685
+ *
3686
+ * A file above the single-request bound is chunked automatically and sent as
3687
+ * parts — nothing to opt into, and nothing held in memory beyond one part.
3688
+ * Pass `onProgress` to follow a large one.
3689
+ */
3690
+ upload: (file: File | Blob, name?: string, options?: UploadOptions) => Promise<R2UploadResult>;
3691
+ /**
3692
+ * Upload raw base64 data.
3693
+ *
3694
+ * Single-request only: base64 has to be materialized whole to be decoded, so
3695
+ * this path keeps the one-request bound rather than the file ceiling — and
3696
+ * because the request carries the ENCODED string, its real budget is
3697
+ * `MAX_BASE64_UPLOAD_BYTES` (~18.75 MiB decoded), not the full 25 MiB. Send a
3698
+ * Blob through `upload` for anything larger; it chunks.
3699
+ */
3675
3700
  uploadBase64: (base64Data: string, name: string, mimeType?: string) => Promise<R2UploadResult>;
3676
3701
  /**
3677
3702
  * Delete a file. Accepts an R2FileInfo from `list()` or a raw key string.
package/dist/index.js CHANGED
@@ -3519,14 +3519,128 @@ function toConnectionStatus(queryStatus) {
3519
3519
 
3520
3520
  // src/client/storage/useR2Files.ts
3521
3521
  import { useCallback as useCallback15, useState as useState13, useMemo as useMemo11 } from "react";
3522
+
3523
+ // src/shared/app-files.ts
3524
+ var MAX_APP_FILE_BYTES = 1024 * 1024 * 1024;
3525
+ var MAX_UPLOAD_REQUEST_BYTES = 25 * 1024 * 1024;
3526
+ var UPLOAD_PART_BYTES = 20 * 1024 * 1024;
3527
+ var MAX_UPLOAD_PARTS = Math.ceil(MAX_APP_FILE_BYTES / UPLOAD_PART_BYTES);
3528
+ var MAX_BASE64_UPLOAD_BYTES = Math.floor(MAX_UPLOAD_REQUEST_BYTES * 3 / 4);
3529
+ function planUploadParts(totalBytes, partSize) {
3530
+ const parts = [];
3531
+ for (let start = 0; start < totalBytes; start += partSize) {
3532
+ parts.push({
3533
+ partNumber: parts.length + 1,
3534
+ start,
3535
+ end: Math.min(start + partSize, totalBytes)
3536
+ });
3537
+ }
3538
+ return parts;
3539
+ }
3540
+ var MAX_DEPLOY_ASSET_FILE_BYTES = 25 * 1024 * 1024;
3541
+ function formatBytes(bytes) {
3542
+ if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GiB`;
3543
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
3544
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)} KiB`;
3545
+ return `${bytes} B`;
3546
+ }
3547
+ function encodeKeyPath(key) {
3548
+ return key.split("/").map(encodeURIComponent).join("/");
3549
+ }
3550
+ function oversizeMessage(bytes, limit = MAX_APP_FILE_BYTES) {
3551
+ return `That file is ${formatBytes(bytes)}; the limit is ${formatBytes(limit)} per file.`;
3552
+ }
3553
+ function describeFilesFailure(status, body) {
3554
+ const trimmed = body.trim();
3555
+ if (trimmed.startsWith("{")) {
3556
+ try {
3557
+ const parsed = JSON.parse(trimmed);
3558
+ if (parsed.error) return parsed.error;
3559
+ } catch {
3560
+ }
3561
+ }
3562
+ if (status === 413) {
3563
+ return `The upload is too large. Files are limited to ${formatBytes(MAX_APP_FILE_BYTES)}, and one request may carry ${formatBytes(MAX_UPLOAD_REQUEST_BYTES)} \u2014 a larger request is rejected by the network before the app receives it.`;
3564
+ }
3565
+ if (status === 411) return "The upload did not declare its length.";
3566
+ if (status === 401 || status === 403) return "Not authorized to write these files.";
3567
+ if (status === 415) return "That file type is not accepted.";
3568
+ if (status >= 500) return `The files service failed (HTTP ${status}). Retry shortly.`;
3569
+ return `Upload failed (HTTP ${status}).`;
3570
+ }
3571
+
3572
+ // src/client/storage/useR2Files.ts
3573
+ function oversize(bytes, limit = MAX_APP_FILE_BYTES) {
3574
+ return bytes > limit ? { success: false, error: oversizeMessage(bytes, limit) } : null;
3575
+ }
3576
+ async function readUploadResult(response) {
3577
+ const text = await response.text();
3578
+ if (response.ok) {
3579
+ try {
3580
+ return JSON.parse(text);
3581
+ } catch {
3582
+ return { success: false, error: "The files service returned a malformed response." };
3583
+ }
3584
+ }
3585
+ return { success: false, error: describeFilesFailure(response.status, text) };
3586
+ }
3522
3587
  function resolveFile(fileOrKey) {
3523
3588
  if (typeof fileOrKey === "string") {
3524
3589
  return { key: fileOrKey, name: void 0 };
3525
3590
  }
3526
3591
  return { key: fileOrKey.key, name: fileOrKey.originalName };
3527
3592
  }
3528
- function encodeKeyPath(key) {
3529
- return key.split("/").map(encodeURIComponent).join("/");
3593
+ async function uploadInParts(file, name, scopeParams, authHeaders, onProgress) {
3594
+ const headers = await authHeaders();
3595
+ const json = { ...headers, "Content-Type": "application/json" };
3596
+ const initResponse = await fetch(`/api/files/multipart?${scopeParams}`, {
3597
+ method: "POST",
3598
+ headers: json,
3599
+ body: JSON.stringify({ name, mimeType: file.type || "application/octet-stream", size: file.size })
3600
+ });
3601
+ if (!initResponse.ok) return await readUploadResult(initResponse);
3602
+ const session = await initResponse.json();
3603
+ if (!Number.isSafeInteger(session.partSize) || session.partSize < 1) {
3604
+ return {
3605
+ success: false,
3606
+ error: `The files service advertised an unusable part size (${session.partSize}).`
3607
+ };
3608
+ }
3609
+ const query = `${scopeParams}&uploadKey=${encodeKeyPath(session.uploadKey)}&uploadId=${encodeURIComponent(session.uploadId)}`;
3610
+ const plan = planUploadParts(file.size, session.partSize);
3611
+ const parts = [];
3612
+ let assembled = false;
3613
+ try {
3614
+ for (const part of plan) {
3615
+ const send = () => fetch(`/api/files/multipart/part?${query}&partNumber=${part.partNumber}`, {
3616
+ method: "PUT",
3617
+ headers,
3618
+ body: file.slice(part.start, part.end)
3619
+ });
3620
+ let response = await send().catch(() => null);
3621
+ if (!response || response.status === 429 || response.status >= 500) {
3622
+ const retried = await send().catch(() => null);
3623
+ if (retried) response = retried;
3624
+ }
3625
+ if (!response) return { success: false, error: "The files service could not be reached." };
3626
+ if (!response.ok) return await readUploadResult(response);
3627
+ parts.push(await response.json());
3628
+ onProgress?.(part.end, file.size);
3629
+ }
3630
+ const completed = await fetch(`/api/files/multipart/complete?${query}`, {
3631
+ method: "POST",
3632
+ headers: json,
3633
+ body: JSON.stringify({ parts })
3634
+ });
3635
+ assembled = completed.ok;
3636
+ return await readUploadResult(completed);
3637
+ } finally {
3638
+ if (!assembled) {
3639
+ await fetch(`/api/files/multipart?${query}`, { method: "DELETE", headers }).catch(
3640
+ () => void 0
3641
+ );
3642
+ }
3643
+ }
3530
3644
  }
3531
3645
  function useR2Files(options) {
3532
3646
  const [isUploading, setIsUploading] = useState13(false);
@@ -3544,9 +3658,15 @@ function useR2Files(options) {
3544
3658
  return {};
3545
3659
  }, []);
3546
3660
  const upload = useCallback15(
3547
- async (file, name) => {
3661
+ async (file, name, options2) => {
3548
3662
  setIsUploading(true);
3549
3663
  try {
3664
+ const tooBig = oversize(file.size);
3665
+ if (tooBig) return tooBig;
3666
+ const fileName = name ?? (file instanceof File ? file.name : "upload");
3667
+ if (file.size > UPLOAD_PART_BYTES) {
3668
+ return await uploadInParts(file, fileName, scopeParams, authHeaders, options2?.onProgress);
3669
+ }
3550
3670
  const headers = await authHeaders();
3551
3671
  const formData = new FormData();
3552
3672
  formData.append("file", file);
@@ -3556,7 +3676,8 @@ function useR2Files(options) {
3556
3676
  headers,
3557
3677
  body: formData
3558
3678
  });
3559
- return await response.json();
3679
+ options2?.onProgress?.(file.size, file.size);
3680
+ return await readUploadResult(response);
3560
3681
  } catch (err) {
3561
3682
  return { success: false, error: err instanceof Error ? err.message : "Upload failed" };
3562
3683
  } finally {
@@ -3569,13 +3690,18 @@ function useR2Files(options) {
3569
3690
  async (base64Data, name, mimeType) => {
3570
3691
  setIsUploading(true);
3571
3692
  try {
3693
+ const tooBig = oversize(
3694
+ Math.floor(base64Data.length * 3 / 4),
3695
+ MAX_BASE64_UPLOAD_BYTES
3696
+ );
3697
+ if (tooBig) return tooBig;
3572
3698
  const headers = await authHeaders();
3573
3699
  const response = await fetch(`/api/files/upload?${scopeParams}`, {
3574
3700
  method: "POST",
3575
3701
  headers: { ...headers, "Content-Type": "application/json" },
3576
3702
  body: JSON.stringify({ data: base64Data, name, mimeType })
3577
3703
  });
3578
- return await response.json();
3704
+ return await readUploadResult(response);
3579
3705
  } catch (err) {
3580
3706
  return { success: false, error: err instanceof Error ? err.message : "Upload failed" };
3581
3707
  } finally {