Stagehand

Cache Stores

fileStore and memoryStore, the CacheStore interface, and the types that describe a cache slot's options and value.

Narrative coverage of caching (what gets stored, stale, the shape guard, reaching the cache from a step) is in Caching. This page is the flat reference for the store-level exports and types.

fileStore(filePath)

function fileStore(filePath: string): CacheStore

Backs every slot passed to it with one JSON file at filePath (resolved to an absolute path). A missing, unreadable, or malformed file all read as no cache at all — deleting the file is always a valid way to reset every slot it held. Values must survive JSON.stringify; anything that does not (a Date, a class instance) comes back as its JSON shape on the next read, not as the original type.

Writes go through a temp file and an atomic rename where the platform allows it (falling back to an in-place write if the rename is blocked by an antivirus, editor, or file watcher holding the destination open). Concurrent writes to the same resolved path are serialized through an in-process queue, so two fileStore(path) calls on the same file (even spelled differently, e.g. with a redundant .. segment) share one writer and never race each other. This serialization is in-process only: two separate node processes writing the same file can still interleave.

memoryStore()

function memoryStore(): CacheStore

Backs every slot with a Map that lives for the process. Useful in tests, or for a long-lived process that re-runs the same script and wants hits to persist between runs without touching disk.

CacheStore

interface CacheStore {
  read(slot: string): Awaitable<CachedEntry | undefined>;
  write(slot: string, entry: CachedEntry): Awaitable<void>;
  clear(slot: string): Awaitable<void>;
}

The interface a custom store implements: three methods, each free to be sync or async (Awaitable<T> is T | Promise<T>). slot is derived from a phase’s or step’s name and passed in by the script; a store never generates or interprets it.

CachedEntry

interface CachedEntry {
  value: unknown;
  savedAt: number; // Date.now() at the moment it was written
}

One stored result, exactly as a CacheStore reads and writes it.

CacheSource<In, Ctx, Value>

type CacheSource<In, Ctx, Value> = CacheStore | CacheOptions<In, Ctx, Value>;

What cache accepts on a phase or a step: a bare CacheStore (shorthand for { store }), or the fuller CacheOptions.

CacheOptions<In, Ctx, Value>

interface CacheOptions<In, Ctx, Value> {
  store: CacheStore;
  stale?: (context: StaleContext<In, Ctx>) => Awaitable<boolean>;
  schema?: StandardSchemaV1<unknown, Value>;
}

interface StaleContext<In, Ctx> {
  value: unknown;
  input: In;
  ctx: Ctx;       // the live context as it stands when the phase/step is reached
  savedAt: number;
  ageMs: number;
}

stale returning true treats a stored entry as a miss — the work runs again and the entry is overwritten. schema, when given, is what the stored value is validated against on every read; a value that fails is also a miss, not a thrown error.

CacheMode

type CacheMode = "on" | "off" | "refresh" | "read-only";

Passed as run(input, { cache }) to control caching for one run without editing the script. "on" is the default.

CacheHandle<Slots>

interface CacheHandle<Slots> {
  read<K extends keyof Slots & string>(
    slot: K,
    options?: { raw?: boolean },
  ): Promise<Slots[K] | undefined>;
  write<K extends keyof Slots & string>(
    slot: K,
    value: unknown,
    options?: { keepAge?: boolean },
  ): Promise<void>;
  clear(slot: keyof Slots & string): Promise<void>;
  ageOf(slot: keyof Slots & string): Promise<number | undefined>;
}

The cache property on both StepContext and RollbackContext, typed to the cached phases and steps declared before the current one; see Caching. read wraps the result in a guard that throws CacheShapeError on a field the stored value lacks, unless the slot declared a schema or { raw: true } was passed.

SlotValue<Schema, Delta>

type SlotValue<Schema, Delta> = unknown extends Schema ? Delta : Schema;

The type-level rule for what a slot reads back as: the schema’s output type when one was declared, otherwise the phase’s (or step’s) own inferred delta. Not something called directly; it is what drives CacheHandle<Slots>’s inference as phases are declared.