UNPKG

next

Version:

The React Framework

145 lines (144 loc) 6.94 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "createTieredCacheHandler", { enumerable: true, get: function() { return createTieredCacheHandler; } }); const _workasyncstorageexternal = require("../app-render/work-async-storage.external"); const _clonecacheentry = require("./clone-cache-entry"); function createTieredCacheHandler(front, backing) { // Holds the in-flight (or chained) background sync per key, so a sync for a // key runs after any earlier one for that key rather than in parallel. const inFlightSyncs = new Map(); function scheduleBackgroundSync(cacheKey, sync) { // Serialize syncs per key: chain this one after any in-flight sync rather // than running a second in parallel. The trailing sync still re-reads the // backing, so the front converges to the latest state; a later read is // never dropped in favor of an earlier, possibly stale, in-flight read. const previous = inFlightSyncs.get(cacheKey); let pending; if (previous) { pending = previous.then(sync); } else { pending = sync(); } pending = pending.finally(()=>{ if (inFlightSyncs.get(cacheKey) === pending) { inFlightSyncs.delete(cacheKey); } }); inFlightSyncs.set(cacheKey, pending); // Register the sync on the current request's revalidation writes so it is // awaited rather than left untracked. Reading the work store here (rather // than capturing it at construction) is what lets the handler be a shared // singleton; `get` always runs within the request's async context, so the // store is present. const workStore = _workasyncstorageexternal.workAsyncStorage.getStore(); if (workStore) { workStore.pendingRevalidateWrites ??= []; workStore.pendingRevalidateWrites.push(pending); } } return { async get (cacheKey, softTags) { const frontEntry = await front.get(cacheKey, softTags); if (frontEntry) { // Cache hit: serve immediately (in a microtask). A background reconcile // keeps the front in sync with the backing for the next read; // reconciles for the same key are serialized, so concurrent cache hits // don't hit the backing in parallel. scheduleBackgroundSync(cacheKey, ()=>reconcileFrontFromBacking(front, backing, cacheKey, softTags, frontEntry)); return frontEntry; } // Cold or evicted front entry: we pay the backing latency here (this is a // read that may legitimately surface a cold cache indicator). A miss // returns undefined and the "use cache" wrapper generates the entry and // writes it through both tiers via `set`. const backingEntry = await backing.get(cacheKey, softTags); if (!backingEntry) { return undefined; } // Mirror this freshly read backing entry into the front so the next read // hits it. The mirror is serialized per key: if a sync is already // running, this chains after it, so the front converges to this read even // if the backing changed since that sync started. const [servedEntry, mirroredEntry] = (0, _clonecacheentry.cloneCacheEntry)(backingEntry); scheduleBackgroundSync(cacheKey, ()=>mirrorIntoFront(front, cacheKey, mirroredEntry)); return servedEntry; }, async set (cacheKey, pendingEntry) { // Write through to both tiers. The entry's value stream is single-use, so // tee it into one entry per tier. const entry = await pendingEntry; const [frontEntry, backingEntry] = (0, _clonecacheentry.cloneCacheEntry)(entry); await Promise.all([ front.set(cacheKey, Promise.resolve(frontEntry)), backing.set(cacheKey, Promise.resolve(backingEntry)) ]); } }; } /** * After serving a cache hit from the front, consult the backing and mirror a * newer entry into the front for the next read. Runs in the background; * failures are non-fatal. */ async function reconcileFrontFromBacking(front, backing, cacheKey, softTags, frontEntry) { try { const backingEntry = await backing.get(cacheKey, softTags); if (!backingEntry) { // The backing no longer has this entry (it was purged out-of-band). The // cache-handler interface has no per-key delete, so evict the front entry // by overwriting it with an already-expired copy: the next read sees a // front miss, falls through to the (also empty) backing, and the wrapper // regenerates. The entry we just served was the last stale read. await front.set(cacheKey, Promise.resolve(toExpiredEntry(frontEntry))); return; } if (backingEntry.timestamp > frontEntry.timestamp) { await front.set(cacheKey, Promise.resolve(backingEntry)); } else { // The front is already up to date, so the backing entry goes unused. // Release its stream without awaiting: a teed stream's `cancel()` only // settles once the sibling branch (retained by the backing handler) is // also cancelled, so awaiting it here would hang the reconcile. void backingEntry.value.cancel(); } } catch { // Background warming; failures are non-fatal. } } /** * Mirror a backing entry into the front. */ async function mirrorIntoFront(front, cacheKey, entry) { try { await front.set(cacheKey, Promise.resolve(entry)); } catch { // Background warming; failures are non-fatal. } } /** * Build an already-expired copy of an entry, used to evict it from the front * handler (which has no per-key delete) once the backing no longer has it. The * default handler treats a negative `expire` as an eviction sentinel and * reports the entry as missing on the next read. A negative `expire` is used * rather than `0` because the dev front handler enforces a minimum retention, * so a `0` `expire` would be kept alive by that minimum instead of evicted. The * value is never read once the entry is evicted, but it must carry at least one * byte because the built-in LRU cache refuses to store size-0 entries. */ function toExpiredEntry(entry) { return { ...entry, expire: -1, value: new ReadableStream({ start (controller) { controller.enqueue(new Uint8Array(1)); controller.close(); } }) }; } //# sourceMappingURL=tiered-cache-handler.js.map