{"version":3,"sources":["../../../../src/server/use-cache/tiered-cache-handler.ts"],"sourcesContent":["import type { CacheEntry, CacheHandler } from '../lib/cache-handlers/types'\nimport { workAsyncStorage } from '../app-render/work-async-storage.external'\nimport { cloneCacheEntry } from './clone-cache-entry'\n\n/**\n * The subset of the cache-handler interface used on the per-invocation\n * read/write path in the \"use cache\" wrapper. Tag operations are intentionally\n * excluded: they are applied to the front and backing handlers individually by\n * the registry iterators (`getCacheHandlers` / `getCacheHandlerEntries`), never\n * through this composite, so the composite is never registered.\n */\nexport type CacheReadWriteHandler = Pick<CacheHandler, 'get' | 'set'>\n\n/**\n * Development-only. Puts a fast built-in in-memory `front` handler in front of\n * a slower or persistent user-configured `backing` handler. Its only job is to\n * guarantee that cache hits resolve in a microtask (so they aren't counted as\n * cache misses at a staged-render boundary, which would otherwise surface a\n * cold cache indicator), while keeping the front in sync with the backing.\n *\n * It implements only `get` and `set` because that is all the wrapper calls on\n * its handler. Regeneration is never done here; this only reads, mirrors, and\n * writes through.\n *\n * The handler is a per-kind singleton (the front and backing are both shared),\n * so its in-flight map can serialize background front syncs for a key across\n * concurrent reads, running them one at a time instead of in parallel.\n */\nexport function createTieredCacheHandler(\n  front: CacheHandler,\n  backing: CacheHandler\n): CacheReadWriteHandler {\n  // Holds the in-flight (or chained) background sync per key, so a sync for a\n  // key runs after any earlier one for that key rather than in parallel.\n  const inFlightSyncs = new Map<string, Promise<void>>()\n\n  function scheduleBackgroundSync(\n    cacheKey: string,\n    sync: () => Promise<void>\n  ): void {\n    // Serialize syncs per key: chain this one after any in-flight sync rather\n    // than running a second in parallel. The trailing sync still re-reads the\n    // backing, so the front converges to the latest state; a later read is\n    // never dropped in favor of an earlier, possibly stale, in-flight read.\n    const previous = inFlightSyncs.get(cacheKey)\n\n    let pending: Promise<void>\n    if (previous) {\n      pending = previous.then(sync)\n    } else {\n      pending = sync()\n    }\n\n    pending = pending.finally(() => {\n      if (inFlightSyncs.get(cacheKey) === pending) {\n        inFlightSyncs.delete(cacheKey)\n      }\n    })\n\n    inFlightSyncs.set(cacheKey, pending)\n\n    // Register the sync on the current request's revalidation writes so it is\n    // awaited rather than left untracked. Reading the work store here (rather\n    // than capturing it at construction) is what lets the handler be a shared\n    // singleton; `get` always runs within the request's async context, so the\n    // store is present.\n    const workStore = workAsyncStorage.getStore()\n    if (workStore) {\n      workStore.pendingRevalidateWrites ??= []\n      workStore.pendingRevalidateWrites.push(pending)\n    }\n  }\n\n  return {\n    async get(cacheKey, softTags) {\n      const frontEntry = await front.get(cacheKey, softTags)\n\n      if (frontEntry) {\n        // Cache hit: serve immediately (in a microtask). A background reconcile\n        // keeps the front in sync with the backing for the next read;\n        // reconciles for the same key are serialized, so concurrent cache hits\n        // don't hit the backing in parallel.\n        scheduleBackgroundSync(cacheKey, () =>\n          reconcileFrontFromBacking(\n            front,\n            backing,\n            cacheKey,\n            softTags,\n            frontEntry\n          )\n        )\n\n        return frontEntry\n      }\n\n      // Cold or evicted front entry: we pay the backing latency here (this is a\n      // read that may legitimately surface a cold cache indicator). A miss\n      // returns undefined and the \"use cache\" wrapper generates the entry and\n      // writes it through both tiers via `set`.\n      const backingEntry = await backing.get(cacheKey, softTags)\n\n      if (!backingEntry) {\n        return undefined\n      }\n\n      // Mirror this freshly read backing entry into the front so the next read\n      // hits it. The mirror is serialized per key: if a sync is already\n      // running, this chains after it, so the front converges to this read even\n      // if the backing changed since that sync started.\n      const [servedEntry, mirroredEntry] = cloneCacheEntry(backingEntry)\n      scheduleBackgroundSync(cacheKey, () =>\n        mirrorIntoFront(front, cacheKey, mirroredEntry)\n      )\n\n      return servedEntry\n    },\n\n    async set(cacheKey, pendingEntry) {\n      // Write through to both tiers. The entry's value stream is single-use, so\n      // tee it into one entry per tier.\n      const entry = await pendingEntry\n      const [frontEntry, backingEntry] = cloneCacheEntry(entry)\n\n      await Promise.all([\n        front.set(cacheKey, Promise.resolve(frontEntry)),\n        backing.set(cacheKey, Promise.resolve(backingEntry)),\n      ])\n    },\n  }\n}\n\n/**\n * After serving a cache hit from the front, consult the backing and mirror a\n * newer entry into the front for the next read. Runs in the background;\n * failures are non-fatal.\n */\nasync function reconcileFrontFromBacking(\n  front: CacheHandler,\n  backing: CacheHandler,\n  cacheKey: string,\n  softTags: string[],\n  frontEntry: CacheEntry\n): Promise<void> {\n  try {\n    const backingEntry = await backing.get(cacheKey, softTags)\n\n    if (!backingEntry) {\n      // The backing no longer has this entry (it was purged out-of-band). The\n      // cache-handler interface has no per-key delete, so evict the front entry\n      // by overwriting it with an already-expired copy: the next read sees a\n      // front miss, falls through to the (also empty) backing, and the wrapper\n      // regenerates. The entry we just served was the last stale read.\n      await front.set(cacheKey, Promise.resolve(toExpiredEntry(frontEntry)))\n\n      return\n    }\n\n    if (backingEntry.timestamp > frontEntry.timestamp) {\n      await front.set(cacheKey, Promise.resolve(backingEntry))\n    } else {\n      // The front is already up to date, so the backing entry goes unused.\n      // Release its stream without awaiting: a teed stream's `cancel()` only\n      // settles once the sibling branch (retained by the backing handler) is\n      // also cancelled, so awaiting it here would hang the reconcile.\n      void backingEntry.value.cancel()\n    }\n  } catch {\n    // Background warming; failures are non-fatal.\n  }\n}\n\n/**\n * Mirror a backing entry into the front.\n */\nasync function mirrorIntoFront(\n  front: CacheHandler,\n  cacheKey: string,\n  entry: CacheEntry\n): Promise<void> {\n  try {\n    await front.set(cacheKey, Promise.resolve(entry))\n  } catch {\n    // Background warming; failures are non-fatal.\n  }\n}\n\n/**\n * Build an already-expired copy of an entry, used to evict it from the front\n * handler (which has no per-key delete) once the backing no longer has it. The\n * default handler treats a negative `expire` as an eviction sentinel and\n * reports the entry as missing on the next read. A negative `expire` is used\n * rather than `0` because the dev front handler enforces a minimum retention,\n * so a `0` `expire` would be kept alive by that minimum instead of evicted. The\n * value is never read once the entry is evicted, but it must carry at least one\n * byte because the built-in LRU cache refuses to store size-0 entries.\n */\nfunction toExpiredEntry(entry: CacheEntry): CacheEntry {\n  return {\n    ...entry,\n    expire: -1,\n    value: new ReadableStream({\n      start(controller) {\n        controller.enqueue(new Uint8Array(1))\n        controller.close()\n      },\n    }),\n  }\n}\n"],"names":["workAsyncStorage","cloneCacheEntry","createTieredCacheHandler","front","backing","inFlightSyncs","Map","scheduleBackgroundSync","cacheKey","sync","previous","get","pending","then","finally","delete","set","workStore","getStore","pendingRevalidateWrites","push","softTags","frontEntry","reconcileFrontFromBacking","backingEntry","undefined","servedEntry","mirroredEntry","mirrorIntoFront","pendingEntry","entry","Promise","all","resolve","toExpiredEntry","timestamp","value","cancel","expire","ReadableStream","start","controller","enqueue","Uint8Array","close"],"mappings":"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","ignoreList":[0]}