UNPKG

next

Version:

The React Framework

119 lines (118 loc) 5.12 kB
import { getUseCacheProbe } from './use-cache-probe-globals'; const PROBE_THRESHOLD_MS = 10000; const MIN_PROBE_BUDGET_MS = 3000; /** * Schedule an idle-deadline probe over a cache fill stream (dev-only). * * Fires the probe when the stream has been idle for `PROBE_THRESHOLD_MS`. The * probe re-runs the cache function in a fresh V8 isolate so module-scoped state * from the outer render — e.g. a top-level `Map<string, Promise>` deduping * fetches — can't poison the body. If the function completes in isolation, the * hang is attributable to that shared state and the caller surfaces a specific * error instead of waiting out the generic timeout. * * Returns the input stream unchanged when the scheduler should be skipped: no * probe hook installed, or the remaining time until `fillDeadlineAt` is too * short to leave room for both the idle threshold and a minimum probe budget. */ export function setupProbeScheduler(opts) { const { workStore, outerRequestStore, cacheContext, encodedArguments, fillDeadlineAt, stream, abortSignal, onProbeCompleted } = opts; if (fillDeadlineAt - performance.now() < PROBE_THRESHOLD_MS + MIN_PROBE_BUDGET_MS) { return stream; } const probe = getUseCacheProbe(); if (!probe) { return stream; } let lastChunkAt = performance.now(); let idleTimer; const startProbe = ()=>{ if (abortSignal.aborted) { return; } const probeStartedAtChunk = lastChunkAt; // Computed when the probe is about to run so it gets the actual remaining // budget, not a stale value baked in at scheduler setup. Reserves a 1s // buffer so the probe's internal timeout fires before the outer render // timeout. const probeInternalTimeoutMs = fillDeadlineAt - performance.now() - 1000; probe({ page: workStore.page, route: workStore.route, id: cacheContext.functionId, kind: cacheContext.handlerKind, encodedArguments, // Built lazily because most fills complete before the idle timer fires; // only worth assembling once we know the probe is running. request: { headers: Array.from(outerRequestStore.headers.entries()), cookieHeader: outerRequestStore.headers.get('cookie') ?? undefined, urlPathname: outerRequestStore.url.pathname, urlSearch: outerRequestStore.url.search, rootParams: outerRequestStore.rootParams ?? {}, isDraftMode: workStore.isDraftMode ?? false, isHmrRefresh: outerRequestStore.isHmrRefresh ?? false, hmrRefreshHash: outerRequestStore.hmrRefreshHash }, timeoutMs: probeInternalTimeoutMs }).then((completed)=>{ // Mid-probe recovery: chunks arrived while the probe was running, so // the main stream is making progress. Discard the probe's result rather // than reporting a deadlock that no longer holds. if (lastChunkAt > probeStartedAtChunk) { return; } if (completed && !abortSignal.aborted) { onProbeCompleted(); } }, ()=>{ // Probe failures are inconclusive; fall back to the regular cache-fill // timeout. }); }; const scheduleAfterIdle = ()=>{ if (idleTimer !== undefined || abortSignal.aborted) { return; } const now = performance.now(); const idleFor = now - lastChunkAt; const wait = Math.max(0, PROBE_THRESHOLD_MS - idleFor); // Skip scheduling if the outer fill timeout will fire before the probe // could even start running with at least a minimum useful budget. Without // this check, a chunk arriving late in the fill could reschedule a probe // that the outer timeout would then abort — wasted worker spawn for a probe // that can't meaningfully complete. if (fillDeadlineAt - now < wait + MIN_PROBE_BUDGET_MS) { return; } idleTimer = setTimeout(()=>{ idleTimer = undefined; if (abortSignal.aborted) { return; } const idleNow = performance.now() - lastChunkAt; if (idleNow < PROBE_THRESHOLD_MS) { // A chunk arrived since we set this timer; reschedule. scheduleAfterIdle(); return; } startProbe(); }, wait); }; abortSignal.addEventListener('abort', ()=>{ if (idleTimer !== undefined) { clearTimeout(idleTimer); idleTimer = undefined; } }, { once: true }); scheduleAfterIdle(); return stream.pipeThrough(new TransformStream({ transform (chunk, controller) { lastChunkAt = performance.now(); scheduleAfterIdle(); controller.enqueue(chunk); } })); } //# sourceMappingURL=use-cache-probe-scheduler.js.map