Skip to content
GitHubRSS

src/cache/chained-cache.ts — several declared cache layers, in order

When more than one plugin contributes a cache layer, resolveCache (plugin-host.ts) chains them in declaration order instead of picking one.

export type LayerErrorReport = (layer: number, method: string, err: unknown) => void
export class ChainedCache implements CacheLayer {
constructor(
readonly layers: readonly CacheLayer[], // at least two, or it throws
onLayerError?: LayerErrorReport, // told of a failure the chain went past
)
readonly hasRemote: boolean // any layer's
get local(): Cache | undefined // the first layer's
}

Every CacheLayer method is implemented by delegation under the rules below; key goes to the first layer, like the run index.

  • Lookup walks the layers (get / has / prefetch) until one answers; the answering layer is remembered per hash, and get asks a remembered layer first. Layers can share one local store, and an earlier layer found a copy a later one had prefetched there and reported that remote’s hit as local (item 889).
  • Save reaches every layer, in order — but a layer whose local handle an EARLIER layer already saved to gets skipLocalWrite: the shared artifact is packed and written once, and the later layer does only its remote upload (two remote plugins over one local handle would otherwise pack every miss twice).
  • A layer that throws is passed, not obeyed (item 1020). A throw in get / has / prefetch / remoteHasMany is a miss (or no answer) in that layer and the walk goes on; a throw in save skips that layer and the rest still save. Each is reported to onLayerError; resolveCache turns that into one warning per layer and method, naming the plugin. A save that fails in every layer still throws. Before, a raw plugin layer’s throw ended the walk above the local floor: its get failed the task, its save kept every entry out of the local store.
  • Restore goes to the layer that answered (restoreOutputs, outputsPath) — an entry’s artifact lives wherever it was found.
  • The first layer owns the run index (recordRunBundle, stats, prune, ingest, hashFile, isOutputsCurrent), so a run is recorded once.
  • hasRemote is true when any layer has a remote. remoteHasMany marks each ANSWERING layer’s own complement absent (its own truth) and returns the union only when every remote layer answered — a partial union is null, because the caller treats a non-null answer as authoritative for the whole chain and would poison a layer that cannot batch with a sibling’s negatives (its later lazy get would then skip a real remote hit). markRemoteAbsent / drainUploads / close reach every layer.

A layer exposes local when it wraps the host’s local handle (LayeredCache.local). resolveCache appends the host’s local store to the tail of every chain, then drops it again when a declared layer already wraps it — so [remote()] resolves to the remote plugin’s layered cache alone instead of writing the local store twice, with no edit to the remote plugin.

tests/chained-cache.test.ts; two declared cache plugins: a run saves into BOTH stores in tests/plugin-capabilities.test.ts.