@johnhenry/hf-cache
v0.1.2
Published
Resolve and cache Hugging Face Hub files: ~/.cache/huggingface layout on Node/Bun, Cache API/OPFS in browsers
Maintainers
Readme
@johnhenry/hf-cache
Resolve and cache Hugging Face Hub files from JS.
- Node/Bun/Deno: exactly the
huggingface_hubcache layout (~/.cache/huggingface/hub/models--{org}--{name}/{blobs,snapshots,refs}). Files Python'shf download/hf_hub_download/snapshot_downloadalready fetched are reused offline, and Python reuses what this package downloads. - Browsers: the same functions backed by the Cache API (picked
automatically through the
browserexport condition, or import@johnhenry/hf-cache/browser).
No dependencies: plain fetch against https://huggingface.co/{repo}/resolve/{revision}/{file}
and /api/models/{repo}/revision/{revision}.
Install
npm install @johnhenry/hf-cache
bun add @johnhenry/hf-cache
deno add jsr:@johnhenry/hf-cacheNode ≥ 24, Bun ≥ 1.2 and Deno use the huggingface_hub disk cache; browsers get the Cache API build through the browser export condition. No dependencies.
import { resolveFile, snapshot, fetchFile } from "@johnhenry/hf-cache";
import { openSafetensors } from "@johnhenry/math-plus-safetensors";
// Node: absolute path inside the cache (downloads if missing)
const path = await resolveFile("aac6fef/laya-mlx", "model.safetensors", {
onProgress: ({ loaded, total }) => console.log(loaded, total),
});
const model = await openSafetensors(path); // header only; tensors on demand
// Several files, one pinned commit
const { dir, files, commit } = await snapshot("aac6fef/laya-mlx", {
allowPatterns: ["*.json", "tokenizer/"],
});
// Content: a lazy disk-backed Blob in Node (fs.openAsBlob), a cached Blob in browsers
const cfg = JSON.parse(await (await fetchFile("aac6fef/laya-mlx", "mlx_config.json")).text());API
| Export | Node/Bun | Browser |
|---|---|---|
| resolveFile(repo, file, opts?) → Promise<string> | absolute snapshot path | cache key URL {endpoint}/{repo}/resolve/{commit}/{file} |
| snapshot(repo, opts?) → { dir, files, commit } | dir = …/snapshots/{commit} | dir = {endpoint}/{repo}/resolve/{commit}/ |
| fetchFile(repo, file, opts?) → Blob (or ArrayBuffer with { as: "arraybuffer" }) | fs.openAsBlob (lazy) | from the Cache API |
| tryToLoadFromCache(repo, file, opts?) → path | null (cached 404) | undefined | ✓ (huggingface_hub.try_to_load_from_cache) | — |
| hubCacheDir(opts?), hfHome(), repoFolderName(repo, type?) | ✓ | — |
| HfCacheError (code: EntryNotFound, RepoNotFound, RevisionNotFound, GatedRepo, Unauthorized, OfflineCacheMiss, InvalidFilename, HttpError), filterFiles, globToRegExp | ✓ | ✓ |
Options (HubOptions): revision (default "main"; branch, tag or commit),
repoType ("model" | "dataset" | "space"), token, cacheDir
(Node: hub cache dir; browser: Cache API cache name, default "hf-cache"),
endpoint, fetch, onProgress({ file, loaded, total }), offline,
signal. snapshot adds files (exact list) or allowPatterns /
ignorePatterns (fnmatch, as huggingface_hub; "dir/" = whole folder) and
concurrency (default 4). Browser functions also take caches (inject a
CacheStorage, e.g. in tests or workers).
Behaviour (Node/Bun)
- Cache dir:
opts.cacheDir, else$HF_HUB_CACHE, else$HUGGINGFACE_HUB_CACHE, else$HF_HOME/hub, else$XDG_CACHE_HOME/huggingface/hub, else~/.cache/huggingface/hub. - Offline:
opts.offlineor$HF_HUB_OFFLINE(1/ON/YES/TRUE):refs/{revision}→snapshots/{commit}/{file}; misses throwOfflineCacheMiss, cached 404s (.no_exist/) throwEntryNotFound. When the Hub is unreachable (transport error), online calls fall back to the cache the same way.snapshotoffline lists the files present (using huggingface_hub 1.x'strees/{commit}.jsonlisting when available). - Online resolution mirrors
hf_hub_download: a HEAD on the resolve URL (following same-origin redirects, stopping at the CDN redirect) yieldsX-Repo-Commitand the blob id (X-Linked-Etag= sha256 for LFS/Xet files, else the git blob sha1) →refs/{revision}is updated, an existing blob is reused (a moved branch costs HEAD requests only), otherwise the file is streamed toblobs/{etag}.{random}.incomplete, size-checked and renamed into place, andsnapshots/{commit}/{file}becomes a relative symlink to it (copied where symlinks are not permitted). A commit-hash revision whose file is cached makes no request at all. - Tokens:
opts.token, else$HF_TOKEN,$HUGGING_FACE_HUB_TOKEN, else the token filehf auth loginwrites ($HF_TOKEN_PATHor$HF_HOME/token). Passtoken: ""to send none. Redirects are followed manually and the token is never sent to another origin (the CDN). - 404 on a file with a known commit writes
.no_exist/{commit}/{file}(as huggingface_hub does).
Limitations
- No inter-process locking (huggingface_hub's
.locks/): concurrent downloads of the same blob both download; atomic renames keep the cache consistent, but bandwidth is wasted. - Downloads are not resumable (a failed download is discarded) and are not hash-verified beyond the byte count.
- Writes the classic per-repo
blobs/layout, not huggingface_hub 1.x's cache-wide Xet shared-blob store ({cache}/blobs/xx/{xet_hash}); both layouts are read transparently (symlinks are followed). Does not writetrees/{commit}.json. snapshotonline asks/api/{type}s/{repo}/revision/{rev}for the file list, so it needs one API call even when everything is cached (except a commit-hash revision with an explicitfileslist).- Browser store: Cache API only (no OPFS); a downloaded file is buffered
into a Blob before being stored (large files need the memory, or the
browser's disk-backed Blob support); an offline browser
snapshotneeds an explicitfileslist (Cache API has no listing). No.no_existcaching in the browser. - Buckets and the Hub's
local_dirdownload mode are not supported.
Tests
npm test -w @johnhenry/hf-cache and npm run test:bun -w @johnhenry/hf-cache.
offline.test.tsresolvesaac6fef/laya-mlx,laya-multilingual-mlxandlaya-typed-decisions-mlxfrom the real local cache (offline; skipped per repo when absent) and cross-checkshuggingface_hub.try_to_load_from_cache(skipped without a Python that hashuggingface_hub;$MATH_PLUS_ORACLE_PYTHONorpython3).layout.test.tsdrives a mocked Hub into a temp cache: exact files and symlinks, refs updates, blob reuse,.no_exist, failed/short downloads, token handling, network fallback, snapshot filters, and Python reading what we wrote.network.test.tsdownloads small JSON files from huggingface.co into a temp cache and checks the commit, blob names and bytes equal whathf downloadstored (skipped when the Hub is unreachable).browser.test.tsruns the Cache API store against an in-memory CacheStorage.
Family
Part of laya-js, Laya typed decisions in JavaScript on MLX, WebGPU and CPU — see its package map and results.
@johnhenry/laya'sload()resolves checkpoints through it (disk cache on Node/Bun, Cache API in browsers).resolveFilereturns a path (Node) andfetchFilea Blob;@johnhenry/math-plus-safetensors'sopenSafetensorsreads either lazily (header first, tensors on demand).
License
MIT.
