Skip to content

v0.8.0

Latest

Choose a tag to compare

@hdae hdae released this 07 Sep 05:10

This release adds range reads on cached entries: openCachedUrl (and openHfFile on the HuggingFace layer) opens an entry that is already in the cache and reads just the byte range you ask for — one row out of a multi-hundred-MB table instead of the whole thing — without ever touching the network. The API is additive — no function, type, default or error-message prefix changes and no migration is needed.

✨ Features

  • openCachedUrl(url, options?) — open a cached entry and read ranges from it. Returns { read, strategy } for a cached entry and undefined when there is none; it never goes to the network, so warming the entry stays the caller's job (fetchBytes / prefetchUrl). read(offset, length, { signal? }) resolves to a new Uint8Array of exactly length bytes; a range past the end of the entry, or a body that ends early, throws instead of returning a short array, and a negative or non-integer offset / length throws before anything is read. fetchBytes is unchanged and still reads a cache hit in full.
  • Two read strategies, chosen per runtime. A browser Blob is a lazy handle to the stored entry, so "blob" (response.blob() once when opening, then blob.slice(...) per read) is constant-time there; Deno's blob() loads the whole entry into memory, so under Deno the default is "stream", which re-reads the body from the cache on every read and skips forward to the offset — a cost proportional to the offset. The default is "stream" when globalThis.Deno exists and "blob" otherwise; strategy: "blob" | "stream" forces one (any other value throws rather than silently falling back), and strategy on the returned entry tells you which one is in use. options.signal on read aborts a "stream" read at the next chunk boundary and rejects with signal.reason; the "blob" strategy checks the signal once before slicing.
  • Verification is the recorded hash, as a string. With sha256, the entry opens only when its recorded x-fetch-cache-sha256 matches — the same string comparison a fetchBytes cache hit makes; the bytes themselves are never hashed, since a range read cannot compute the hash of the whole entry. A mismatch self-heals: the entry is evicted and undefined comes back. An entry with no recorded hash also returns undefined but is not evicted — read it once through fetchBytes with the same sha256 and the record is backfilled, after which it opens. Without sha256 the entry opens as is; a malformed sha256 (anything but 64 lowercase hex digits) throws before the cache is touched, as it does on fetchBytes. There is deliberately no validate / decode / recheck / into: a range read only ever sees the stored raw form, and full-entry verification stays with fetchBytes.
  • Failures while opening degrade, failures while reading throw. A cache I/O failure when opening (caches.open, cache.match, or blob() under the "blob" strategy) is treated like a miss — undefined — and reported through onCacheError (a blob() failure is reported as op: "match", a failed self-heal delete as op: "delete"), whose default handler says the entry is treated as absent rather than that the call falls back to the network. A failure during read throws, because there is nothing to fall back to. On a runtime without caches at all, both openCachedUrl and openHfFile return undefined rather than throwing (the openHfFile check for a missing sha256 still runs first). An open handle is not a snapshot: a "stream" handle re-reads the cache on every call, so it throws once the entry has been evicted (evictUrl / evict / a self-heal; after clearCache it depends on the runtime: under Deno 2.9 the Cache object the handle holds stops matching once the namespace is deleted, so it throws there too, while browsers are unmeasured) — and, when opened with sha256, it re-checks the recorded hash on every read and throws if another write has replaced the entry under it (a self-heal refetch with a different sha256, or any write that lands on the same key after the entry was evicted). A "blob" handle keeps the Blob it opened and can still be read after the entry is gone.
  • openHfFile(ref, spec, options?) on the HF layer does the same for a file whose sha256 is declared: that is the content key, which does not contain the revision, so nothing is resolved and nothing is fetched — fetchHfFile / prefetchHfFile warm the same key. A spec without sha256 throws (its key is the revision-pinned resolve URL, and resolving that would need the network). The spec's format checks still run (a negative or non-integer expectedBytes throws with the same message as on fetchHfFile), even though expectedBytes / validate / decode / into play no part in a range read. strategy, onCacheError and caches are passed through to the cache layer.
  • HfResolveOptions — the options type of resolveHfRevision (fetch / init / retry / onRetry), now exported from ./hf so a wrapper can take the same options without copying the declaration. No change in behavior.
  • New exported types: CachedEntry and OpenCachedOptions from ., CachedEntry, HfOpenOptions and HfResolveOptions from ./hf.

📝 Docs

  • README gains a Reading a range out of a cached entry section (and a "Range reads on cached entries" feature bullet). The design — why a separate open/read API instead of a fetchBytes option, why two strategies, why verification is the recorded hash only, and the measured numbers behind the runtime default — is recorded in ADR 0012; the by-design constraints (no verification of the bytes read, no network and no single-flight, offset-proportional cost under Deno, handles are not snapshots, aborts land on chunk boundaries) are in limitations; that "stream" needs a body it can stream is noted in ADR 0012 and on OpenCachedOptions.strategy.

Full Changelog: v0.7.0...v0.8.0