UNPKG

next

Version:

The React Framework

148 lines (147 loc) 7.31 kB
import { Worker } from 'next/dist/compiled/jest-worker'; import { setDevValidationWorker } from '../app-render/dev-validation-worker-globals'; import { onCacheInvalidation } from './require-cache'; import { getFormattedNodeOptionsWithoutInspect } from '../lib/utils'; import { needsExperimentalReact } from '../../lib/needs-experimental-react'; /** * Wire up the dev-server's validation worker: register the HMR teardown * listener and install the hook that `runDevValidationInBackground` calls once * a render has settled. The worker thread is spawned lazily, so nothing is * created until the first navigation actually validates. */ export function installDevValidationWorker(options) { const { distDir, buildId, deploymentId, nextConfig } = options; // A single worker, not a pool. Validation for one navigation runs its depth // loop sequentially, and a newer navigation supersedes the previous one // (aborting it mid-run) rather than running concurrently, so there's no // per-request fan-out to parallelize (unlike the `'use cache'` probe, where // one request fans out into concurrent probes). One worker already frees the // main thread, which is the whole point; it also keeps each request's CLI // marker block contiguous in the piped output. Torn down on HMR (stale user // modules) and on crash. // // TODO(dev-validation-worker): raise `numWorkers` if concurrent navigations // across independent requests (e.g. multiple tabs) show a validation-latency // tail. let pool; const getPool = ()=>{ if (pool) { return pool; } // Strip `--inspect` from any inherited `NODE_OPTIONS` so the worker doesn't // fight the parent for the same debug port. const workerNodeOptions = getFormattedNodeOptionsWithoutInspect(); // The worker is shipped as four pre-bundled dev-only artifacts // ({webpack,turbopack} × {stable,experimental}), one per combination of the // user's bundler and vendored React channel. Pick the matching artifact // from runtime env so the worker stays in lockstep with the user's app // bundle. `needsExperimentalReact` is the same predicate `define-env.ts` // uses to wire `__NEXT_EXPERIMENTAL_REACT`. const turbo = process.env.TURBOPACK ? '-turbo' : ''; const channel = needsExperimentalReact(nextConfig) ? '-experimental' : ''; const workerPath = require.resolve(`next/dist/compiled/next-server/dev-validation-worker${turbo}${channel}.runtime.dev.js`); const worker = new Worker(workerPath, { maxRetries: 0, numWorkers: 1, // Always worker-threads, regardless of `experimental.workerThreads`. // Unlike the `'use cache'` probe (which follows the flag), validation has // no reason to prefer a child process: it doesn't need process-level // isolation (a worker thread already has its own V8 heap and module // registry, so the reloaded route is isolated from the main thread), and // threads let a superseded validation be aborted mid-run through a shared // `SharedArrayBuffer`, which a separate process can't receive. Threads // also carry the transported Flight bytes as typed arrays via structured // clone, with no JSON round-trip to corrupt them. enableWorkerThreads: true, // Listing the method explicitly tells jest-worker to skip the discovery // `require()` it would otherwise do in the parent process to enumerate // the module's exports. This worker's top-level imports (`require-hook`, // `node-environment`) run runtime setup meant only for the isolated // worker thread, so they must not be evaluated in the parent. exposedMethods: [ 'runDevValidation' ], forkOptions: { env: { ...process.env, NODE_OPTIONS: workerNodeOptions } } }); worker.getStdout().pipe(process.stdout); worker.getStderr().pipe(process.stderr); pool = worker; return worker; }; const tearDownPool = async ()=>{ const current = pool; if (!current) { return; } pool = undefined; await current.end().catch(()=>{ // The worker thread exits on its own once its work settles; a failed // `.end()` here just means we couldn't wait for it cleanly. }); }; const runValidation = async (snapshot, validationAbortSignal)=>{ let activePool; try { activePool = getPool(); } catch { return null; } const message = { ...snapshot, distDir, buildId, deploymentId, nextConfigSerializable: { httpAgentOptions: nextConfig.httpAgentOptions, cacheLifeProfiles: nextConfig.cacheLife, useCacheTimeout: nextConfig.experimental.useCacheTimeout, staticPageGenerationTimeout: nextConfig.staticPageGenerationTimeout } }; // The worker runs as a thread (see `enableWorkerThreads` above), so an // abort reaches it through a one-slot shared flag rather than the abort // signal directly (a signal can't cross the thread boundary). Mirror an // abort of `validationAbortSignal` into the buffer and wake the worker's // `Atomics.waitAsync` on it, so it aborts the in-flight run at its next // depth boundary. const abortBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT); const abortFlag = new Int32Array(abortBuffer); const propagateAbort = ()=>{ Atomics.store(abortFlag, 0, 1); Atomics.notify(abortFlag, 0); }; if (validationAbortSignal.aborted) { propagateAbort(); } else { validationAbortSignal.addEventListener('abort', propagateAbort, { once: true }); } try { return await activePool.runDevValidation(message, abortBuffer); } catch { // Worker crash or IPC error: tear down so the next validation starts // fresh. The main thread treats a missing result as "nothing to deliver." await tearDownPool(); return null; } finally{ // `once` only auto-removes the listener if it fired, so remove it // explicitly to bound its lifetime to this run when validation completed // without being superseded. validationAbortSignal.removeEventListener('abort', propagateAbort); } }; // The dev server can't reach into the worker to clear its `require.cache` or // manifest caches, so we drop the worker whenever the parent's caches are // invalidated (HMR, route recompile). The next validation lazy-spawns a fresh // worker with empty caches. onCacheInvalidation(()=>{ void tearDownPool(); }); setDevValidationWorker(runValidation); } //# sourceMappingURL=dev-validation-worker-pool.js.map