/** * @copyright Sister Software * @license AGPL-3.0 * @author Teffen Ellis, et al. * @file An on-disk `axios-cache-interceptor` storage adapter, so a client gets a durable HTTP cache by * CONFIGURATION rather than by hand-rolling one. * * NODE ONLY, and deliberately NOT re-exported from `./index.ts`: `core/api` reaches a browser bundle * (`docs`'s `DashboardMap` → `@mailwoman/cartographer` → `tiles/api.ts` → `@mailwoman/core/api`), * and webpack refuses to resolve `node:fs/promises` for the web target. Import this through its own * `@mailwoman/core/api/disk-storage` subpath. * * Two rules here are load-bearing, both carried over from the bespoke cache this replaces * (`98c4dda1:filer/sdk/sec-client.ts`), both learned the hard way: * * 1. VALIDATE BEFORE WRITING. A response that can't be read back — an unparseable body, a * non-finite TTL — must never reach disk. A permanently-cached entry has no self-healing path * short of hand-deleting a hash-named file. * 2. ATOMIC WRITE, UNIQUE TEMP NAME. Write-then-rename, with a temp name unique per write. A * DETERMINISTIC temp name (`${final}.building`) made two clients writing one URL collide: the * first `rename()` moved the shared temp file away and the second got a raw `ENOENT` for a * response that had already succeeded (reproduced 6/6), and at multi-MB bodies the two writers' * bytes interleaved into a corrupt-but-parseable entry. */ import { type AxiosStorage, type NotEmptyStorageValue } from "axios-cache-interceptor"; import { type IRuntimeLogger } from "../logging/index.ts"; /** * Options for {@linkcode buildDiskStorage}. */ export interface DiskStorageOptions { /** * The directory cache entries live in. Created on first write (recursively). */ directory: string; /** * An additional, domain-specific gate run against every entry BEFORE it is written. Return `false` (or throw) to drop * the write; the entry is removed rather than persisted, so the next request re-fetches. * * This is the seam for "a 200 whose body isn't what this API is supposed to return". Some upstreams (SEC EDGAR among * them) serve an HTML error page with a 200 status; persisting one under a permanent TTL poisons that URL forever. * The structural checks below (serializable, finite `createdAt`/`ttl`) always run regardless. */ validate?: (value: NotEmptyStorageValue) => boolean; /** * Where rejected writes and unreadable entries are reported. Defaults to a `disk-storage`-prefixed console logger. */ logger?: IRuntimeLogger; } /** * Create an on-disk {@linkcode AxiosStorage}, keyed by the SHA-256 of the interceptor's cache key (which already folds * in method, URL, params and body), so a filename is always a fixed-length, filesystem-safe hex digest. * * An in-process overlay Map sits in front of the files, and it is load-bearing for two reasons: * * 1. `loading` markers live there INSTEAD of on disk. That keeps the interceptor's stampede guard working (a concurrent * second request for the same key sees `loading` and waits on the first) without a file write per request, and * without an interrupted process leaving a `loading` marker on disk forever. * 2. A value being WRITTEN stays there until its `rename` lands. Without that, `set()` clearing the `loading` marker * before the file exists opens a window where the key is in neither place, and a concurrent reader gets `empty` for * a response that is already in hand — measured as 3 dispatches for 3 concurrent requests to one URL, i.e. the * stampede guard fully defeated. */ export declare function buildDiskStorage(options: DiskStorageOptions): AxiosStorage; //# sourceMappingURL=disk-storage.d.ts.map