npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@holdthis/upload

v0.1.0

Published

Browser upload SDK for holdthis media infrastructure

Readme

@holdthis/upload

Browser upload SDK for holdthis media infrastructure. Uploads files directly from the browser to B2 via presigned URLs -- zero bytes through your server. Multipart, concurrent, with progress tracking, pause/resume, retry, and offline detection.

  • Tiny footprint (ships unminified source; your bundler tree-shakes and minifies)
  • Zero dependencies
  • Works with any frontend framework
  • TypeScript declarations included

Installation

npm install @holdthis/upload

Quick start

Proxy mode (recommended)

Your backend proxies to the holdthis API. The SDK handles init, upload, and finalization automatically.

import { createUpload } from "@holdthis/upload";

const upload = createUpload({
  file: fileInput.files[0],
  initUrl: "/api/upload/init",
  doneUrl: "/api/upload/done",
  headers: { "X-CSRF-Token": csrfToken },

  onProgress({ percent, bytesUploaded, bytesTotal }) {
    progressBar.style.width = `${percent}%`;
  },
  onSuccess({ uploadId, parts }) {
    console.log("Upload complete:", uploadId);
  },
  onError(err) {
    console.error("Upload failed:", err.message);
  },
});

upload.start();

Your backend's /api/upload/init proxies to the holdthis POST /api/v1/upload/init endpoint (adding your API key), and returns the response ({ uploadId, parts: [{ start, end, url }] }). Same for /api/upload/done.

Direct mode

If you fetch presigned URLs yourself, pass them directly.

import { createUpload } from "@holdthis/upload";

const file = fileInput.files[0];

// Your backend fetches these from holdthis /api/v1/upload/init.
const { uploadId, parts } = await fetchPresignedUrls(file);

const upload = createUpload({
  file,
  uploadId,
  parts, // [{ start, end, url }, ...]
  onProgress({ percent }) {
    console.log(`${percent}%`);
  },
  onSuccess({ uploadId, parts }) {
    // POST parts (with ETags) to your backend to finalize.
    finalizeUpload(uploadId, parts);
  },
  onError(err) {
    console.error(err);
  },
});

upload.start();

API

createUpload(options): Upload

Creates an upload instance. Throws if the file fails validation or options are invalid. The upload does not start until upload.start() is called.

Options (proxy mode):

| Option | Type | Required | Description | |--------|------|----------|-------------| | file | File | Yes | The file to upload | | initUrl | string | Yes | Your backend's init endpoint | | doneUrl | string | Yes | Your backend's done endpoint | | headers | Record<string, string> | No | Extra headers for init/done (CSRF, auth). Content-Type is always set to application/json by the SDK |

Options (direct mode):

| Option | Type | Required | Description | |--------|------|----------|-------------| | file | File | Yes | The file to upload | | uploadId | string | Yes | Upload ID from your backend | | parts | { start, end, url }[] | Yes | Presigned URLs with byte ranges. start and end are byte offsets forming a half-open interval [start, end) -- end is exclusive |

Shared options:

| Option | Type | Default | Description | |--------|------|---------|-------------| | concurrency | number | 3 | Concurrent part uploads | | maxAttempts | number | 5 | Max attempts per part | | retryBaseDelay | number | 1000 | Initial retry backoff (ms) | | maxSize | number | - | Max file size in bytes | | allowedTypes | string[] | - | Allowed MIME patterns ("audio/*", "video/mp4") |

Callbacks:

| Callback | Payload | Description | |----------|---------|-------------| | onProgress | { bytesUploaded, bytesTotal, percent } | Aggregate upload progress | | onPartComplete | { partNumber, etag } | A part finished uploading | | onSuccess | { uploadId, parts: [{ partNumber, etag }] } | All parts uploaded (and done called in proxy mode) | | onError | Error | Fatal error. Check err.name === "AbortError" for user-initiated abort. Always provide this -- start() does not return a promise, so without onError failures surface as unhandled promise rejections | | onOffline | - | Network lost, upload auto-paused | | onOnline | - | Network restored, upload auto-resumed |

Upload

| Method | Description | |--------|-------------| | start() | Begin uploading. No-op if already started | | pause() | Cancel in-flight requests. Completed parts are preserved | | resume() | Restart from where pause left off | | abort() | Cancel permanently. Fires onError with AbortError |

validateFile(file, options?): Error | null

Validates a file against size and type constraints without creating an upload. Returns null if valid, an Error if not.

import { validateFile } from "@holdthis/upload";

const err = validateFile(file, {
  maxSize: 500 * 1024 * 1024,
  allowedTypes: ["audio/*", "video/*"],
});
if (err) {
  alert(err.message);
}

Pause and resume

pauseButton.onclick = () => upload.pause();
resumeButton.onclick = () => upload.resume();

Completed parts are preserved across pause/resume -- only incomplete parts are restarted.

Network awareness

The SDK listens for online/offline events and automatically pauses when the network drops and resumes when it returns. The onOffline and onOnline callbacks fire so you can update your UI.

Manual pauses are respected -- an online event will not auto-resume an upload that was paused by the user.

Retry

Failed parts retry automatically with exponential backoff (1s, 2s, 4s, 8s cap). Network errors, 5xx, 408, and 429 responses are retried. Other 4xx errors fail immediately.

Integration guide

Backend proxy pattern

Your platform backend sits between the browser and holdthis. The browser never sees your holdthis API key.

Browser                    Your Backend                     holdthis
  |                            |                               |
  |-- POST /api/upload/init -->|-- POST /api/v1/upload/init -->|
  |<-- { uploadId, parts } ----|<-- { uploadId, parts } -------|
  |                            |                               |
  |-- PUT parts directly to B2 (presigned URLs) ------------->|
  |                            |                               |
  |-- POST /api/upload/done -->|-- POST /api/v1/upload/done -->|
  |<-- success ----------------|<-- success -------------------|

Your init endpoint receives the file metadata from the SDK:

{ "filename": "episode.mp3", "size": 52428800, "contentType": "audio/mpeg" }

Forward it to holdthis with your API key, return the response as-is.

CORS

B2 buckets must allow browser PUT requests. Required CORS configuration:

  • Allowed origins: * (or your specific origins)
  • Allowed methods: PUT
  • Allowed headers: Content-Type, X-Request-ID
  • Exposed headers: ETag, Last-Modified

Without exposing ETag, the browser cannot read ETags and the upload cannot be finalized. Last-Modified is optional but useful for post-upload verification.

License

Proprietary. Copyright holdthis.