import type { Adapter, FilesPlugin } from "../index.js"; /** Which of the two stores an object lives in (or should be written to). */ export type Tier = "hot" | "cold"; /** * The inputs {@link TieringOptions.route} decides a {@link Tier} from. It's * called once per logical operation, on the **caller-facing** key: * * - on `upload`, with `size` set to the body's declared byte length when it's * known up front (string / `Blob` / `ArrayBuffer` / typed array); * - on every other decision (reads, `delete`, the destination of `copy` / * `move`, `signedUploadUrl`, and locating an object), with `size` **omitted** * — those inputs have no body to measure. * * Write a function whose result is stable for a given key (e.g. routing by key * prefix) and reads land on the right tier first try. Route by `size` — which * reads can't recompute — and enable {@link TieringOptions.fallback} so a read * that misses the guessed tier transparently checks the other. */ export interface TierContext { /** The caller-facing object key the decision is for. */ key: string; /** * Declared body length in bytes, present only on `upload` and only when the * body's length is known without consuming it. `undefined` for streaming * uploads and on every read / locate decision. */ size?: number; } /** Decide which {@link Tier} an operation routes to. See {@link TierContext}. */ export type TierRouter = (context: TierContext) => Tier; export interface TieringOptions { /** * The cold tier — a second {@link Adapter} the plugin drives directly (wrapped * in its own internal {@link Files}, so cold-tier calls get the same retry, * capability gating, and `StoredFile` normalization the hot tier does). The * hot tier is the instance's own adapter, reached through the rest of the * onion. Configure the cold adapter with its **own** bucket / container; it * receives caller-facing keys (the instance `prefix` is **not** applied to * it). */ cold: Adapter; /** * Decide which {@link Tier} each operation routes to. Required — there's no * sensible default for "hot vs cold". See {@link TierContext} for what it's * handed and when. */ route: TierRouter; /** * When `true`, treat an object's tier as **discoverable** rather than fixed: * a read that misses the routed tier retries the other; `delete` removes the * key from both tiers; and an `upload` evicts the key from the other tier so * exactly one copy ever exists. Turn this on for `size`-based routing or when * you move objects between tiers with {@link TieringApi.tier} (e.g. age-based * transitions) — anything where the tier isn't a pure function of the key. * * Defaults to `false`: routing is deterministic, every op touches exactly the * one tier {@link TieringOptions.route} names, and there's no extra round-trip * — the right choice for prefix / key-based routing. */ fallback?: boolean; } /** * The methods {@link tiering} 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 TieringApi = { /** * Report which {@link Tier} currently holds `key`, or `undefined` when neither * tier does. Checks the routed tier first, then the other (regardless of * {@link TieringOptions.fallback}, so it always gives a definitive answer). */ tierOf(key: string): Promise; /** * Move `key` to `target`, streaming the object across adapters and removing * the source copy. A no-op when it's already there. Throws `NotFound` when * neither tier holds the key. This is the lever for age-based transitions: * list, check `lastModified`, and `tier(key, "cold")` what's gone cold. Pair * it with `fallback: true` so reads still find what you've moved. */ tier(key: string, target: Tier): Promise; }; /** * Route by size / prefix / age to a hot and a cold {@link Adapter}, so an * `upload` lands in the right store and every read transparently finds it again. * The **hot** tier is the instance's own adapter (reached through the rest of * the onion); the **cold** tier is a second adapter passed in * {@link TieringOptions.cold}. {@link TieringOptions.route} decides per * operation. * * What each verb does: * - **`upload`** routes by `route({ key, size })` (`size` is the body's declared * length when known). With `fallback`, it then evicts the key from the other * tier so a re-upload that flips tiers leaves exactly one copy. * - **`download` / `head` / `url` / `exists`** consult the routed tier; with * `fallback` they fall through to the other tier on a miss, so `size`-routed * and hand-moved objects are still found. * - **`delete`** removes the routed tier's copy; with `fallback`, both tiers'. * - **`copy` / `move`** locate the source, route the destination by key, and use * a native same-tier op or stream the bytes across when the tiers differ. * - **`list`** merges a page from each tier (keys sorted within the page), * paginating the two independently via a composite cursor. * - **`signedUploadUrl`** signs against the tier `route({ key })` picks; the * resulting direct upload bypasses the plugin, so it can't be size-routed or * deduplicated. * * It's **body-transparent** — it never buffers or transforms bytes (a cross-tier * copy streams) — and adds two methods via `extend`: {@link TieringApi.tierOf} * and {@link TieringApi.tier}. Use {@link createFiles} to surface them on the * type. * * Placement and prefixes: * - Place it **last** (innermost) so body-transforming plugins * (`encryption()`, `compression()`) wrap it and apply to **both** tiers. * - Address objects by caller-facing keys: the cold adapter does **not** receive * the instance `prefix` (configure its own bucket / container), while the hot * tier — including {@link TieringApi.tierOf} / {@link TieringApi.tier} — does. * * @param options `{ cold, route, fallback? }` — see {@link TieringOptions}. * @example * ```ts * import { createFiles } from "files-sdk"; * import { s3 } from "files-sdk/s3"; * import { tiering } from "files-sdk/tiering"; * * const files = createFiles({ * adapter: s3({ bucket: "hot" }), // hot tier * plugins: [ * tiering({ * cold: s3({ bucket: "cold" }), * // archives go cold; everything else stays hot * route: ({ key }) => (key.startsWith("archive/") ? "cold" : "hot"), * }), * ], * }); * * await files.upload("photo.jpg", body); // → hot * await files.upload("archive/2019.zip", zip); // → cold * await files.download("archive/2019.zip"); // transparently read from cold * await files.tier("photo.jpg", "cold"); // age it down (needs fallback: true) * ``` */ export declare const tiering: (options: TieringOptions) => FilesPlugin; //# sourceMappingURL=index.d.ts.map