import type { FileInfo, FilesPlugin } from "../index.js"; /** The read verbs {@link cache} can serve from its store. */ export type CacheableOperation = "head" | "url" | "download"; /** * One key's cached state, as held by a {@link CacheStore}. Treat it as * **opaque** — the in-memory store keeps it as-is; a remote/KV store must * (de)serialize it (it's JSON-able apart from `downloads[*].bytes`, a * `Uint8Array` of the cached body). Keeping every verb for a key under one * record is what makes invalidation a single `delete(key)`. */ export interface CacheRecord { /** Cached `head` metadata and when it goes stale (ms epoch). */ head?: { meta: FileInfo; expiresAt: number; }; /** Cached `url` strings, keyed by their url-options signature. */ urls?: Record; /** Cached `download` bodies, keyed by their byte-range signature. */ downloads?: Record; } /** * The backing store for {@link cache}. Each entry is the whole * {@link CacheRecord} for one object — so a write invalidates every cached verb * in one `delete`. Store keys are opaque strings that scope the * **caller-facing** object key by the {@link CacheOptions.namespace} and the * instance `prefix` (`//`, the first two * percent-encoded), so instances sharing one store never read each other's * entries unless they share both. Defaults to a bounded in-memory LRU; pass your * own to share a cache across instances or processes (e.g. a Redis-backed store * that serializes the record). * * Methods may be sync or async; the plugin awaits them either way. A distributed * store has an inherent read-modify-write race when two different verbs for the * same key are first cached at the exact same moment — harmless, it just costs a * re-fetch next time. */ export interface CacheStore { /** Read a key's record, or `undefined` on a miss. */ get: (key: string) => CacheRecord | undefined | Promise; /** Write (replace) a key's record. */ set: (key: string, record: CacheRecord) => void | Promise; /** Drop a key's record — the unit of invalidation. */ delete: (key: string) => void | Promise; /** Drop every record. */ clear: () => void | Promise; } export interface CacheOptions { /** * Where cached records live. Defaults to a bounded in-memory LRU keyed by * object key (see {@link CacheOptions.maxEntries}). Pass a {@link CacheStore} * to back the cache with your own KV — shared across instances/processes. * A custom store requires a {@link CacheOptions.namespace}. */ store?: CacheStore; /** * The name that scopes this cache's entries inside a shared * {@link CacheOptions.store}, alongside the instance `prefix`. Required with a * custom `store` (`cache()` throws `Invalid` without one): the plugin can't * tell which bucket an adapter points at, so it can't keep two buckets' * entries apart on its own. Every `cache()` that shares a store **and** a * namespace (and the same `prefix`) serves the others' cached bytes, URLs, * and metadata, so give each bucket its own (e.g. `"uploads-prod"`), and * reuse one only across instances or processes that address the same bucket. * Instances with different `prefix`es stay apart even under one namespace. */ namespace?: string; /** * Time-to-live for cached entries, in milliseconds. Defaults to `60_000` * (60s). `0` or negative disables time-based expiry (entries live until * evicted or invalidated). A cached `url` is **additionally** capped at its * own `expiresIn`, so a presigned URL is never served past its signature — * but keep `ttl` comfortably below your URL expiry so reads stay fresh. */ ttl?: number; /** * Which read verbs to cache. Defaults to `["head", "url"]` — the cheap, * body-free ones. Add `"download"` to also cache **small** bodies (gated by * {@link CacheOptions.maxBytes}); larger or unknown-length downloads stream * through uncached so streaming is never broken. */ operations?: readonly CacheableOperation[]; /** * Largest `download` body eligible for caching, in bytes. Defaults to * `1_048_576` (1 MiB). A response larger than this — or one whose length the * adapter doesn't report — is returned untouched and not buffered, so * streaming and large objects keep working. Only consulted when `"download"` * is in {@link CacheOptions.operations}. */ maxBytes?: number; /** * Maximum number of distinct keys the **default** in-memory store retains * before evicting the least-recently-used. Defaults to `1000`. Ignored when a * custom {@link CacheOptions.store} is supplied. Note the worst-case memory is * roughly `maxEntries * maxBytes` once `download` caching is on. */ maxEntries?: number; /** * Clock backing TTL and expiry, defaulting to `Date.now`. Inject a fake for * deterministic expiry in tests. */ clock?: () => number; /** * The signature lifetime (in seconds) assumed for a `url()` call that omits * `expiresIn`. The adapter signs such calls with its own default, which the * plugin can't see — so cached entries are capped at this value to keep the * "never served past its signature" guarantee. Defaults to `3600` (the * SDK-wide default URL expiry); set it to match your adapter when you've * configured a different `defaultUrlExpiresIn` there. */ defaultUrlExpiresIn?: number; } /** * The methods {@link cache} grafts onto a {@link Files} instance. A `type` * rather than an `interface` so it satisfies the `Record` * constraint on {@link FilesPlugin}'s extension parameter — an interface has no * implicit index signature and wouldn't be assignable. */ export type CacheApi = { /** * Drop the cached entries for one key — or the **entire** cache when `key` is * omitted. Reach for this after a change the plugin couldn't see (a write * through a presigned URL, or directly against the provider), to stop serving * stale reads. With no `key` it calls the store's `clear()`, so on a shared * {@link CacheOptions.store} it drops every namespace's entries, not only * this instance's. */ invalidateCache: (key?: string) => Promise; /** A fresh snapshot of cache hit/miss counts since construction (or last reset). */ cacheStats: () => CacheStats; /** Zero the hit/miss counters, starting a fresh accounting window. */ resetCacheStats: () => void; }; /** Point-in-time hit/miss tally from {@link CacheApi.cacheStats}. */ export interface CacheStats { /** Reads served from the cache. */ hits: number; /** Reads that fell through to the provider. */ misses: number; } /** * An LRU/KV cache in front of the cheap read verbs — `head`, `url`, and * (opt-in) small `download` bodies. A repeat read of an unchanged key is served * from memory instead of round-tripping to the provider; any write through the * instance (`upload`, `delete`, `copy`, `move`) invalidates the affected key so * the next read re-fetches. * * What's cached, and how it stays correct: * - **`head`** caches the {@link FileInfo} it returns — metadata only, no * body — and a hit hands out a fresh copy of it. * - **`url`** caches the returned string per url-options signature, and **caps * each entry at its own `expiresIn`** so a presigned URL is never handed out * past its signature. A call without `expiresIn` is capped at * {@link CacheOptions.defaultUrlExpiresIn} (default `3600`, the SDK-wide * default) — lower it to match an adapter configured with a shorter * `defaultUrlExpiresIn`. When the URL states its own lifetime * (`X-Amz-Expires`, `X-Goog-Expires`) and it's shorter, that wins. Keep * {@link CacheOptions.ttl} well below your URL expiry. * - **`download`** is **off by default** — add `"download"` to * {@link CacheOptions.operations}. Even then only **known-length bodies at or * under {@link CacheOptions.maxBytes}** are buffered and cached; anything * larger or of unknown length streams through untouched, so streaming and * range downloads keep working. A cached small body is re-served as a fresh, * re-readable `StoredFile`. * * Invalidation is by **caller-facing key** (never the internal prefixed path): * `upload`/`delete` drop that key, `copy` drops the destination, `move` drops * both — before the write and again after it settles, failed writes included * (a timed-out write may still have landed). A read miss that overlaps a * write to its key returns what it fetched but never caches it, so a slow * read can't re-populate the bytes a write just replaced. A provider storage event from `files-sdk/events` (an upload through a * presigned URL, a write by another process) drops its key too, when the * instance has `events()` wired to a feed. Writes the plugin can't observe at * all won't invalidate; call `files.invalidateCache(key)` (or * `invalidateCache()` to clear all) when that happens, and treat a cache as * eventually-consistent. It writes **no object metadata** and has **no native * dependencies**, so it works on any adapter. * * Entries are scoped to the instance: the store key carries the instance * `prefix` (and, with a shared {@link CacheOptions.store}, the required * {@link CacheOptions.namespace}), so tenants that differ by `prefix` never see * each other's entries. One `cache()` serves one {@link Files} instance (and * its {@link Files.readonly} views); passing the same `cache()` to an instance * with another adapter or `prefix` throws `Invalid` at construction. To share * entries between instances, give each its own `cache()` with the same `store` * and `namespace`. * * Plugins run **outside** retries, so a cache hit skips the retry loop entirely * and a populated entry reflects one logical, post-retry result. Place `cache()` * **first** (outermost) so it short-circuits before the rest of the pipeline * does any work; place it after a body-transforming plugin (`encryption()`, * `compression()`) only if you intend to cache the transformed bytes. The one * plugin that belongs **before** it is `signedUrlPolicy()`: plugins run in * array order, so a policy placed after `cache()` rewrites `url()` options the * cache never sees — it would key and cap the entry by the caller's * `expiresIn` rather than the shorter one actually signed (only S3-style and * GCS URLs, which state their own lifetime, are caught regardless). Use * `plugins: [signedUrlPolicy({ maxExpiresIn }), cache()]`. * * It uses `extend` (for `invalidateCache()` / `cacheStats()` / `resetCacheStats()`), * so reach for {@link createFiles} to surface those on the type. * * @param options optional `{ store, namespace, ttl, operations, maxBytes, * maxEntries, clock, defaultUrlExpiresIn }`. * @example * ```ts * import { createFiles } from "files-sdk"; * import { s3 } from "files-sdk/s3"; * import { cache } from "files-sdk/cache"; * * const files = createFiles({ * adapter: s3({ bucket: "uploads" }), * plugins: [cache({ ttl: 30_000, operations: ["head", "url", "download"] })], * }); * * await files.head("a.png"); // miss → provider * await files.head("a.png"); // hit → memory * await files.upload("a.png", body); // invalidates "a.png" * await files.head("a.png"); // miss → provider again * files.cacheStats(); // { hits: 1, misses: 2 } * ``` */ export declare const cache: (options?: CacheOptions) => FilesPlugin; //# sourceMappingURL=index.d.ts.map