import type { FilesPlugin, StoredFileMeta } 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: StoredFileMeta; 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}. Keyed by the **caller-facing** object key * (never the internal prefixed path), each entry is the whole {@link CacheRecord} * for that key — so a write invalidates every cached verb in one `delete`. * 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. */ store?: CacheStore; /** * 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. */ 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 metadata only. A hit returns a {@link StoredFile} * whose body still lazy-fetches on access (the same contract an uncached * `head` has), so nothing buffers. * - **`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. 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. Writes the plugin can't observe — a presigned-URL upload, or a change * made straight against the provider — 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. * * 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. * * It uses `extend` (for `invalidateCache()` / `cacheStats()` / `resetCacheStats()`), * so reach for {@link createFiles} to surface those on the type. * * @param options optional `{ store, ttl, operations, maxBytes, maxEntries, clock }`. * @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