-
Notifications
You must be signed in to change notification settings - Fork 329
feat(dev): detect 'use cache' module-scope deadlocks early in dev (#1126) #1157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
6103e8c
f0ba0b9
2c7dde8
4b2c506
5662e1a
b4886d1
b543d3a
c2e053f
445a0d0
41895ed
fe5ac07
9dc5254
bc998e6
0b10e4f
3223445
f0a6c58
f15c467
73d4f15
86678d8
242b27e
42fb5cd
fe0d953
9d71da4
fea883d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,6 +16,7 @@ import { handleApiRoute } from "./server/api-handler.js"; | |||||||||||||||
| import { installSocketErrorBackstop } from "./server/socket-error-backstop.js"; | ||||||||||||||||
| import { shouldInvalidateAppRouteFile } from "./server/dev-route-files.js"; | ||||||||||||||||
| import { createDirectRunner } from "./server/dev-module-runner.js"; | ||||||||||||||||
| import { initUseCacheProbePool, tearDownUseCacheProbePool } from "./server/use-cache-probe-pool.js"; | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: static import loads probe pool at plugin registration time. Non-blocking — Vite plugins are dev-time artifacts and the module loading cost is trivial. But if bundle size of the plugin itself ever matters, this is low-hanging fruit. |
||||||||||||||||
| import { generateRscEntry } from "./entries/app-rsc-entry.js"; | ||||||||||||||||
| import { generateSsrEntry } from "./entries/app-ssr-entry.js"; | ||||||||||||||||
| import { generateBrowserEntry } from "./entries/app-browser-entry.js"; | ||||||||||||||||
|
|
@@ -2041,6 +2042,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { | |||||||||||||||
| invalidateAppRouteCache(); | ||||||||||||||||
| invalidateRscEntryModule(); | ||||||||||||||||
| invalidateRootParamsModule(); | ||||||||||||||||
| // Tear down the use-cache probe pool so the next probe starts with | ||||||||||||||||
| // fresh code after HMR invalidation. | ||||||||||||||||
| tearDownUseCacheProbePool(); | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: probe permanently disabled after first HMR. Either re-initialize here:
Suggested change
Or restructure
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The teardown+reinit pattern looks correct now. One thing to watch: if |
||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Node throws on unhandled 'error' events on sockets. When a browser | ||||||||||||||||
|
|
@@ -2134,6 +2138,14 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { | |||||||||||||||
| // it is flushed to the client. | ||||||||||||||||
| // 2. Logs the full request after res finishes, using those timings. | ||||||||||||||||
| if (hasAppDir) { | ||||||||||||||||
| // Initialize the "use cache" deadlock probe pool for the App | ||||||||||||||||
| // Router dev server. We bind to the RSC environment because | ||||||||||||||||
| // "use cache" functions run inside the RSC module graph. | ||||||||||||||||
| const rscEnv = server.environments["rsc"]; | ||||||||||||||||
| if (rscEnv) { | ||||||||||||||||
| initUseCacheProbePool(rscEnv); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| server.middlewares.use((req, res, next) => { | ||||||||||||||||
| const url = req.url ?? "/"; | ||||||||||||||||
| // Skip Vite internals, HMR, and static assets. | ||||||||||||||||
|
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,7 +65,7 @@ import type { DevEnvironment } from "vite"; | |
| * environment types — including Cloudflare's custom environments that don't | ||
| * support the hot-channel-based transport. | ||
| */ | ||
| type DevEnvironmentLike = { | ||
| export type DevEnvironmentLike = { | ||
| fetchModule: ( | ||
| id: string, | ||
| importer?: string, | ||
|
|
@@ -129,3 +129,51 @@ export function createDirectRunner(environment: DevEnvironmentLike | DevEnvironm | |
| new ESModulesEvaluator(), | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Create a fresh ModuleRunner with its own isolated module graph. | ||
| * | ||
| * Used by the "use cache" deadlock probe to re-import a cache function | ||
| * with zero shared module-scope state from the main request pipeline. | ||
| * Each probe gets a brand-new `EvaluatedModules` instance so top-level | ||
| * `Map`s, `Promise`s, etc. are recreated from scratch. | ||
| * | ||
| * The transport delegates to the same `environment.fetchModule()`, so | ||
| * Vite transforms and HMR still work, but the *evaluated* module cache | ||
| * is completely separate. | ||
| */ | ||
| export function createProbeRunner(environment: DevEnvironmentLike | DevEnvironment): ModuleRunner { | ||
| return new ModuleRunner( | ||
| { | ||
| transport: { | ||
| // oxlint-disable-next-line @typescript-eslint/no-explicit-any | ||
| invoke: async (payload: any) => { | ||
| const { name, data: args } = payload.data; | ||
| if (name === "fetchModule") { | ||
| const [id, importer, options] = args as [ | ||
| string, | ||
| string | undefined, | ||
| { cached?: boolean; startOffset?: number } | undefined, | ||
| ]; | ||
| return { | ||
| result: await environment.fetchModule(id, importer, options), | ||
| }; | ||
| } | ||
| if (name === "getBuiltins") { | ||
| return { result: [] }; | ||
| } | ||
| return { | ||
| error: { | ||
| name: "Error", | ||
| message: `[vinext] Unexpected ModuleRunner invoke: ${name}`, | ||
| }, | ||
| }; | ||
| }, | ||
| }, | ||
| createImportMeta: createNodeImportMeta, | ||
| sourcemapInterceptor: false, | ||
| hmr: false, | ||
| }, | ||
| new ESModulesEvaluator(), | ||
| ); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moderate: exact duplicate of Just reuse |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -601,7 +601,7 @@ export function filterInternalHeaders(headers: Headers): Headers { | |
| return filtered; | ||
| } | ||
|
|
||
| function getRequestCf(request: Request): unknown | undefined { | ||
| function getRequestCf(request: Request): unknown { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This type change ( |
||
| const cf = Reflect.get(request, "cf"); | ||
| return cf === undefined ? undefined : cf; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,215 @@ | ||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * use-cache-probe-pool.ts | ||||||||||||||||||||||||
| * | ||||||||||||||||||||||||
| * Manages a pool of isolated ModuleRunners for "use cache" deadlock probes. | ||||||||||||||||||||||||
| * | ||||||||||||||||||||||||
| * In dev mode, when a cache fill appears stuck, we re-run the same cache | ||||||||||||||||||||||||
| * function in a fresh module graph. If it completes there but the main fill | ||||||||||||||||||||||||
| * is still hung, the hang is attributable to module-scope shared state | ||||||||||||||||||||||||
| * (e.g. a top-level Map used to dedupe fetches) from the outer render. | ||||||||||||||||||||||||
| * | ||||||||||||||||||||||||
| * Unlike Next.js (which uses jest-worker with real OS processes), vinext | ||||||||||||||||||||||||
| * creates a fresh Vite ModuleRunner per probe. Each runner has its own | ||||||||||||||||||||||||
| * EvaluatedModules instance, so top-level module state is recreated from | ||||||||||||||||||||||||
| * scratch while still using the same Vite transform pipeline. | ||||||||||||||||||||||||
| * | ||||||||||||||||||||||||
| * The pool is torn down on HMR / file invalidation so the next probe | ||||||||||||||||||||||||
| * starts with fresh transformed code. | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| import type { DevEnvironment } from "vite"; | ||||||||||||||||||||||||
| import { createProbeRunner, type DevEnvironmentLike } from "./dev-module-runner.js"; | ||||||||||||||||||||||||
| import { ModuleRunner } from "vite/module-runner"; | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||
| import { setUseCacheProbe } from "vinext/shims/use-cache-probe-globals"; | ||||||||||||||||||||||||
| import { UseCacheTimeoutError } from "vinext/shims/use-cache-errors"; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| let _activeProbeRunners: ModuleRunner[] | null = null; | ||||||||||||||||||||||||
| let _environment: DevEnvironmentLike | DevEnvironment | null = null; | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dead store: |
||||||||||||||||||||||||
| const MAX_RUNNERS = 4; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function getProbeRunner(): ModuleRunner { | ||||||||||||||||||||||||
| if (!_activeProbeRunners || _activeProbeRunners.length === 0) { | ||||||||||||||||||||||||
| throw new Error("[vinext] use cache probe pool not initialized"); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| // Round-robin across runners for basic load distribution. | ||||||||||||||||||||||||
| const runner = _activeProbeRunners.shift()!; | ||||||||||||||||||||||||
| _activeProbeRunners.push(runner); | ||||||||||||||||||||||||
| return runner; | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stale module state on reuse: The pool round-robins across 4 pre-created runners, but This means the first probe for a given function works correctly, but subsequent probes on the same runner reuse stale module state and may not detect the deadlock. Either:
|
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * Initialize the probe pool with a set of fresh ModuleRunners bound to the | ||||||||||||||||||||||||
| * given Vite dev environment. | ||||||||||||||||||||||||
| * | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: JSDoc says "Called once during configureServer()" but the function is also called on every HMR file change (via
Suggested change
|
||||||||||||||||||||||||
| * Called once during configureServer() when the App Router dev server starts. | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| export function initUseCacheProbePool(environment: DevEnvironmentLike | DevEnvironment): void { | ||||||||||||||||||||||||
| if (_activeProbeRunners) { | ||||||||||||||||||||||||
| // Already initialized — no-op. The environment is the same for the | ||||||||||||||||||||||||
| // lifetime of the dev server. | ||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||
|
Comment on lines
+36
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Misleading comment. This says "Already initialized — no-op" and "the environment is the same for the lifetime of the dev server", but
Suggested change
|
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| _environment = environment; | ||||||||||||||||||||||||
| _activeProbeRunners = []; | ||||||||||||||||||||||||
| for (let i = 0; i < MAX_RUNNERS; i++) { | ||||||||||||||||||||||||
| _activeProbeRunners.push(createProbeRunner(environment)); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| setUseCacheProbe(async (msg) => { | ||||||||||||||||||||||||
| const runner = getProbeRunner(); | ||||||||||||||||||||||||
| const { id, kind, encodedArguments, request, timeoutMs } = msg; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Internal timeout so the probe aborts before the outer render timeout. | ||||||||||||||||||||||||
| const deadline = Date.now() + timeoutMs; | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: The
Suggested change
Then line 121 becomes |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||
| // Import the cache-runtime shim in the isolated runner. | ||||||||||||||||||||||||
| // The shim's registerCachedFunction will create fresh module-scope state. | ||||||||||||||||||||||||
| const cacheRuntime = (await runner.import("vinext/shims/cache-runtime")) as Record< | ||||||||||||||||||||||||
| string, | ||||||||||||||||||||||||
| unknown | ||||||||||||||||||||||||
| >; | ||||||||||||||||||||||||
|
Comment on lines
+60
to
+66
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: The This works correctly because the |
||||||||||||||||||||||||
| const registerCachedFunction = cacheRuntime.registerCachedFunction as | ||||||||||||||||||||||||
| | (<T extends (...args: unknown[]) => Promise<unknown>>( | ||||||||||||||||||||||||
| fn: T, | ||||||||||||||||||||||||
| id: string, | ||||||||||||||||||||||||
| variant?: string, | ||||||||||||||||||||||||
| ) => T) | ||||||||||||||||||||||||
| | undefined; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (!registerCachedFunction) { | ||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // We need to locate the original cached function module in the isolated | ||||||||||||||||||||||||
| // runner. The function id is "<modulePath>:<exportName>". We split it | ||||||||||||||||||||||||
| // to find the module and the export. | ||||||||||||||||||||||||
| const lastColon = id.lastIndexOf(":"); | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Edge case: function IDs containing colons in the module path. This is fine for now — just flagging it as a known limitation. A comment noting the assumption ("export names don't contain colons") would be helpful.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The The comment on line 81 documents the assumption that export names don't contain colons. Worth noting that on Windows, module IDs like
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Windows path edge case. On Windows, Vite module IDs use forward slashes internally, so Not blocking — the assumption is reasonable for file-system modules. But consider adding a brief comment noting this relies on the ID format being |
||||||||||||||||||||||||
| const modulePath = lastColon >= 0 ? id.slice(0, lastColon) : id; | ||||||||||||||||||||||||
| const exportName = lastColon >= 0 ? id.slice(lastColon + 1) : "default"; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Import the module containing the original "use cache" function. | ||||||||||||||||||||||||
| const mod = (await runner.import(modulePath)) as Record<string, unknown>; | ||||||||||||||||||||||||
| const originalFn = mod[exportName]; | ||||||||||||||||||||||||
| if (typeof originalFn !== "function") { | ||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Wrap it with registerCachedFunction so the probe runs through the | ||||||||||||||||||||||||
| // same cache-runtime path (fresh ALS, no shared state). | ||||||||||||||||||||||||
| const variant = kind === "private" ? "private" : ""; | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dead code. This should either be removed (simplify to |
||||||||||||||||||||||||
| const wrapped = registerCachedFunction( | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When If the If that reordering isn't done, consider adding a brief comment here noting the double-timeout is expected and harmless (outer pool timeout preempts inner). |
||||||||||||||||||||||||
| originalFn as (...args: unknown[]) => Promise<unknown>, | ||||||||||||||||||||||||
| id, | ||||||||||||||||||||||||
| variant, | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Double dev-timeout in probe. Next.js avoids this: when Consider either:
Not blocking since the outer probe pool timeout preempts the inner one, but it's wasteful. |
||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Decode the arguments (simple JSON fallback; RSC encodeReply is | ||||||||||||||||||||||||
| // not available in the probe because we lack the client environment). | ||||||||||||||||||||||||
| // For deadlock detection, the exact argument values matter less than | ||||||||||||||||||||||||
| // the fact that the function body executes with a fresh module scope. | ||||||||||||||||||||||||
| let args: unknown[] = []; | ||||||||||||||||||||||||
| if (typeof encodedArguments === "string") { | ||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||
| args = JSON.parse(encodedArguments); | ||||||||||||||||||||||||
| if (!Array.isArray(args)) args = [args]; | ||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||
| args = []; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Run the function with a reconstructed request store so private caches | ||||||||||||||||||||||||
| // that read cookies()/headers()/draftMode() see the same values. | ||||||||||||||||||||||||
| const result = await runWithProbeRequestStore(request, async () => wrapped(...args)); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Wait for the result, but enforce the internal timeout. | ||||||||||||||||||||||||
| const remaining = deadline - Date.now(); | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the suggestion to use
Suggested change
Or more simply: const remaining = deadline - performance.now();since the |
||||||||||||||||||||||||
| if (remaining <= 0) { | ||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // If we got here, the probe completed. | ||||||||||||||||||||||||
| await Promise.race([ | ||||||||||||||||||||||||
| Promise.resolve(result), | ||||||||||||||||||||||||
| new Promise<never>((_, reject) => { | ||||||||||||||||||||||||
| const t = setTimeout(() => reject(new UseCacheTimeoutError()), remaining); | ||||||||||||||||||||||||
| // Ensure timer is cleaned up on success via unref if available. | ||||||||||||||||||||||||
| if (typeof (t as NodeJS.Timeout).unref === "function") { | ||||||||||||||||||||||||
| (t as NodeJS.Timeout).unref(); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||
|
Comment on lines
+110
to
+128
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant await + race: The timeout should wrap the un-awaited call: const result = await Promise.race([
runWithProbeRequestStore(request, async () => wrapped(...args)),
new Promise<never>((_, reject) => {
const t = setTimeout(() => reject(new UseCacheTimeoutError()), remaining);
if (typeof t.unref === 'function') t.unref();
}),
]);
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor timer leak inside the probe. When The
Suggested change
Then in the if (probeTimeoutTimer !== undefined) clearTimeout(probeTimeoutTimer); |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||
| // Probe failure is inconclusive — the function might genuinely hang | ||||||||||||||||||||||||
| // even in isolation, or the module import failed. Fall back to the | ||||||||||||||||||||||||
| // regular timeout. | ||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A function that throws is not deadlocked. The
Consider distinguishing these:
Suggested change
|
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * Tear down the probe pool. Called on HMR / file invalidation so the next | ||||||||||||||||||||||||
| * probe starts with fresh code. | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| export function tearDownUseCacheProbePool(): void { | ||||||||||||||||||||||||
| if (_activeProbeRunners) { | ||||||||||||||||||||||||
| for (const runner of _activeProbeRunners) { | ||||||||||||||||||||||||
| runner.close().catch(() => {}); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| _activeProbeRunners = null; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| _environment = null; | ||||||||||||||||||||||||
| setUseCacheProbe(undefined); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * Reconstruct a minimal request store in the probe runner so that | ||||||||||||||||||||||||
| * cookies(), headers(), and draftMode() behave correctly. | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| async function runWithProbeRequestStore<T>( | ||||||||||||||||||||||||
| requestSnapshot: { | ||||||||||||||||||||||||
| headers: [string, string][]; | ||||||||||||||||||||||||
| cookieHeader: string | undefined; | ||||||||||||||||||||||||
| urlPathname: string; | ||||||||||||||||||||||||
| urlSearch: string; | ||||||||||||||||||||||||
| rootParams: Record<string, unknown>; | ||||||||||||||||||||||||
| isDraftMode: boolean; | ||||||||||||||||||||||||
| isHmrRefresh: boolean; | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| fn: () => Promise<T>, | ||||||||||||||||||||||||
| ): Promise<T> { | ||||||||||||||||||||||||
| // Import the ALS-backed request-context modules in the isolated runner. | ||||||||||||||||||||||||
| const unifiedCtx = (async () => { | ||||||||||||||||||||||||
| // These imports run inside the probe runner's module graph. | ||||||||||||||||||||||||
| // We dynamic-import them because the probe runner doesn't share | ||||||||||||||||||||||||
| // module state with the main runner. | ||||||||||||||||||||||||
| const { createRequestContext, runWithRequestContext } = | ||||||||||||||||||||||||
| (await import("vinext/shims/unified-request-context")) as typeof import("vinext/shims/unified-request-context"); | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: These To actually run in the probe's isolated graph, these would need to go through As-is, the ALS context is set on the main runner's modules, so |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const { headersContextFromRequest } = | ||||||||||||||||||||||||
| (await import("vinext/shims/headers")) as typeof import("vinext/shims/headers"); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Build a Request from the snapshot so headersContextFromRequest works. | ||||||||||||||||||||||||
| const url = new URL( | ||||||||||||||||||||||||
| requestSnapshot.urlPathname + requestSnapshot.urlSearch, | ||||||||||||||||||||||||
| "http://localhost", | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| const request = new Request(url, { | ||||||||||||||||||||||||
| headers: new Headers(requestSnapshot.headers), | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const headersContext = headersContextFromRequest(request); | ||||||||||||||||||||||||
| const ctx = createRequestContext({ | ||||||||||||||||||||||||
| headersContext, | ||||||||||||||||||||||||
| executionContext: null, | ||||||||||||||||||||||||
| rootParams: requestSnapshot.rootParams as Record<string, string | string[]>, | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return runWithRequestContext(ctx, fn); | ||||||||||||||||||||||||
| })(); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return unifiedCtx; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is unrelated to the deadlock probe feature. If the benchmarks tsconfig needs a
vinextpath mapping, that should be a separate commit or PR.