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

@nitida/asset-uploader-web

v0.2.1

Published

nitida browser upload task — resumable multipart to R2 with IndexedDB persistence, so a reload does not restart a 2 GB upload.

Readme

@nitida/asset-uploader-web

Browser uploader for the nitida asset platform. Single-part and multipart uploads to R2, incrementally SHA-256 hashed with bounded memory, and IndexedDB persistence so a reload does not restart a 2 GB upload. Mirrors the API of @nitida/asset-uploader-expo.

Install

npm install @nitida/asset-uploader-web @nitida/asset-client

@nitida/asset-client is a peer dependency.

Usage

import { UploadTask } from "@nitida/asset-uploader-web";

const task = new UploadTask({
  file,
  tenantCode: "acme",
  endpoint: "/api/am",       // same-origin BFF prefix, or the absolute API URL
  presets: ["responsive"],   // see below — omitting this is not neutral
});

task.on("progress", ({ ratio, bytesUploaded, totalBytes }) => setPct(ratio));
task.on("done", ({ assetId, deduped }) => console.log(assetId, deduped));

const result = await task.start();   // resumes automatically if state exists
// task.pause() · task.resume() · task.abort()

Events: progress, partCompleted, ready, compressed, error, done.

Three things that bite

Omitting presets silently removes the variants. The field is threaded into /assets/upload-url and /assets/multipart/initiate + /complete. With no presets the server generates only original — the responsive ladder and the video AI pipeline just do not happen, and nothing reports an error. The upload succeeds; the images you expected downstream never exist.

assetId is undefined on two paths, by server contract. On the multipart path, /assets/multipart/complete enqueues /assets/process and the asset row is created asynchronously. On single-part videos, /process returns { ok, kind: "video", dispatch } with no id at all. Neither is an error. The sha256 in the result is the universal correlation key: resolve the row with GET /assets/by-hash/<sha256>. Only the dedup paths, single-part images and non-media files return an id inline.

Compression is injected, never imported. There is no compress: true — you hand the compressor function in, so compressorjs and heic2any stay out of your bundle when you do not use them:

import { compressImage } from "@nitida/asset-compressor-web";

new UploadTask({ file, tenantCode, endpoint, compress: { compressImage } });

When compression runs, the compressed bytes are what reach R2 and what the server-side sha256 describes. The original hash is not lost: the compressed event carries originalSha256, compressedSha256 and the ratio.

Hashing a 2 GB file without a 2 GB ArrayBuffer

SubtleCrypto.digest() is one-shot: it wants the entire input as a single BufferSource, which for a multi-GB video means a multi-GB allocation in the JS heap. WebKit caps a single ArrayBuffer at 4 GB — a hard JSC limit, so every iOS browser inherits it — and mobile Safari jetsams tabs well below that. This package instead feeds hash-wasm's incremental createSHA256() with lazy Blob.slice() chunks: slice() reads no bytes, so peak memory stays at one 8 MiB chunk regardless of file size, and the loop yields between chunks so even on the main thread nothing janks longer than a single chunk's hash.

Resumability, and what is stored to make it work

Upload state (upload id, completed parts, their ETags, the part size) is written to IndexedDB as parts land, so start() on a task for the same file picks up where it stopped — across a reload or a crash, not just a pause(). Persistence is an optimisation, never a gate: an IndexedDB failure disables resume and logs, it does not fail an upload whose bytes are moving fine. Pass your own persistence implementation to store state elsewhere.

The stored session includes presigned R2 PUT URLs, and a presigned URL is a bearer credential. They are persisted because that is what resume-after-reload means; the bounds are worth knowing rather than discovering. They are write-only and scoped to the parts of one upload the user started, the server signs them for 60 minutes and refuses a stale row (the task then asks for fresh URLs), IndexedDB is origin-scoped, and the row is deleted on both complete and abort.

Your authToken is deliberately not among them: it lives only in memory on the UploadTask instance, UploadSession has no field for it, and a dump of this database never yields one. A reload loses it and you supply it again. authToken is for service-to-service calls anyway — in BFF mode leave it out and cookies ride along with the same-origin request.

License

MIT