src/cache/chained-cache.ts — several declared cache layers, in order
Purpose
Section titled “Purpose”When more than one plugin contributes a cache layer, resolveCache
(plugin-host.ts) chains them in declaration order instead of picking one.
Public surface
Section titled “Public surface”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, andgetasks 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
localhandle an EARLIER layer already saved to getsskipLocalWrite: 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/remoteHasManyis a miss (or no answer) in that layer and the walk goes on; a throw insaveskips that layer and the rest still save. Each is reported toonLayerError;resolveCacheturns 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: itsgetfailed the task, itssavekept 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. hasRemoteis true when any layer has a remote.remoteHasManymarks each ANSWERING layer’s own complement absent (its own truth) and returns the union only when every remote layer answered — a partial union isnull, 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 lazygetwould then skip a real remote hit).markRemoteAbsent/drainUploads/closereach every layer.
The subsume rule
Section titled “The subsume rule”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.