UNPKG

next

Version:

The React Framework

232 lines (231 loc) 11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 0 && (module.exports = { dropDevValidationWorker: null, installDevValidationWorker: null, mirrorModuleStateToDevValidationWorker: null }); function _export(target, all) { for(var name in all)Object.defineProperty(target, name, { enumerable: true, get: all[name] }); } _export(exports, { dropDevValidationWorker: function() { return dropDevValidationWorker; }, installDevValidationWorker: function() { return installDevValidationWorker; }, mirrorModuleStateToDevValidationWorker: function() { return mirrorModuleStateToDevValidationWorker; } }); const _jestworker = require("next/dist/compiled/jest-worker"); const _devvalidationworkerglobals = require("../app-render/dev-validation-worker-globals"); const _requirecache = require("./require-cache"); const _utils = require("../lib/utils"); const _needsexperimentalreact = require("../../lib/needs-experimental-react"); /** * Replays a change the dev server made to its own module state in the * validation worker. Installed alongside the worker, so it is absent when no * worker runs (`experimental.devValidationWorker: false`, or Webpack). */ let mirrorModuleState; /** * Drops the validation worker. Called when the dev server cannot repair its own * module state in place and re-evaluates every module from disk, which the * worker matches by starting over: the next validation spawns a worker that * loads the current build output. * * A worker dropped on its own, by a failed replay or a crash, is the one case * where the two can diverge. The dev server keeps the modules it evaluated from * earlier updates, and a worker spawned afterwards has no way to obtain those * scripts, so frames naming them stay unresolved until those modules change * again. The validation itself is unaffected, because the worker loads the * current code from disk. */ let dropWorker; function mirrorModuleStateToDevValidationWorker(change) { mirrorModuleState == null ? void 0 : mirrorModuleState(change); } function dropDevValidationWorker() { dropWorker == null ? void 0 : dropWorker(); } 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. Raising it means `mirrorChange` has to reach every worker instead of // whichever one the pool hands the call to, and the ordering it relies on // holds per worker rather than across them. let pool; const mirrorChange = (change)=>{ const current = pool; if (!current) { // No worker to keep current, and nothing to replay into a later one: the // compilation that produced this change also wrote the updated chunk to // disk, so a worker spawning after it reads the current code through // `loadComponents`. Evaluating the update is what an isolate that is // already running needs, not what a new one does. return; } // The worker runs one call at a time, in the order the calls were made // (see `numWorkers` below), so this is replayed before any validation // requested after it, and never in the middle of one. That ordering is by // call time, which is why the call is made here, as the change arrives, // rather than deferred onto a queue of our own. A validation already // queued runs first, which is what its render needs: it was produced // before this change. The dev server does not hold its own updates back // for a validation running in process either. const replayed = change.type === 'invalidate' ? current.invalidateCaches(change.filePaths, change.evictModules) : current.applyHmrUpdate(change.update).then(async (outcome)=>{ if (outcome === 'failed') { await tearDownPool(); } }); void replayed.catch(async ()=>{ // A replay that failed leaves the worker's state unknown, so it is // dropped rather than trusted. await tearDownPool(); }); }; 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 = (0, _utils.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 = (0, _needsexperimentalreact.needsExperimentalReact)(nextConfig) ? '-experimental' : ''; const workerPath = require.resolve(`next/dist/compiled/next-server/dev-validation-worker${turbo}${channel}.runtime.dev.js`); const worker = new _jestworker.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', 'applyHmrUpdate', 'invalidateCaches' ], 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 just cleared these paths from its own `require.cache` and // manifest caches. The worker clears them from its copies. (0, _requirecache.onCacheInvalidation)((filePaths)=>{ mirrorChange({ type: 'invalidate', filePaths, evictModules: true }); }); mirrorModuleState = mirrorChange; dropWorker = ()=>{ void tearDownPool(); }; (0, _devvalidationworkerglobals.setDevValidationWorker)(runValidation); } //# sourceMappingURL=dev-validation-worker-pool.js.map