From 00459a1d78a657e7372b2c92a8604cef8af1c511 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Thu, 11 Dec 2025 20:23:05 -0700 Subject: [PATCH] fix: race condition when importing `AsyncLocalStorage` (#17350) * fix: race condition when importing `AsyncLocalStorage` * types: * tweak * remove a line * comment * dumb esbuild * final final pt2 final * ugh --- .changeset/early-showers-marry.md | 5 ++++ .../src/internal/server/render-context.js | 28 +++++++++++++------ packages/svelte/tests/runtime-browser/test.ts | 1 + 3 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 .changeset/early-showers-marry.md diff --git a/.changeset/early-showers-marry.md b/.changeset/early-showers-marry.md new file mode 100644 index 0000000000..a599cefe64 --- /dev/null +++ b/.changeset/early-showers-marry.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: race condition when importing `AsyncLocalStorage` diff --git a/packages/svelte/src/internal/server/render-context.js b/packages/svelte/src/internal/server/render-context.js index a33fff69d3..7a014a1b15 100644 --- a/packages/svelte/src/internal/server/render-context.js +++ b/packages/svelte/src/internal/server/render-context.js @@ -2,7 +2,7 @@ /** @import { AsyncLocalStorage } from 'node:async_hooks' */ /** @import { RenderContext } from '#server' */ -import { deferred } from '../shared/utils.js'; +import { deferred, noop } from '../shared/utils.js'; import * as e from './errors.js'; /** @type {Promise | null} */ @@ -56,14 +56,26 @@ export async function with_render_context(fn) { /** @type {AsyncLocalStorage | null} */ let als = null; +/** @type {Promise | null} */ +let als_import = null; -export async function init_render_context() { - if (als !== null) return; - try { - // @ts-ignore -- we don't include node types in the production build - const { AsyncLocalStorage } = await import('node:async_hooks'); - als = new AsyncLocalStorage(); - } catch {} +/** + * + * @returns {Promise} + */ +export function init_render_context() { + // It's important the right side of this assignment can run a maximum of one time + // otherwise it's possible for a very, very well-timed race condition to assign to `als` + // at the beginning of a render, and then another render to assign to it again, which causes + // the first render's second half to use a new instance of `als` which doesn't have its + // context anymore. + // @ts-ignore -- we don't include node types in the production build + als_import ??= import('node:async_hooks') + .then((hooks) => { + als = new hooks.AsyncLocalStorage(); + }) + .then(noop, noop); + return als_import; } // this has to be a function because rollup won't treeshake it if it's a constant diff --git a/packages/svelte/tests/runtime-browser/test.ts b/packages/svelte/tests/runtime-browser/test.ts index 63e601b115..597b2909dc 100644 --- a/packages/svelte/tests/runtime-browser/test.ts +++ b/packages/svelte/tests/runtime-browser/test.ts @@ -164,6 +164,7 @@ async function run_test( } ], bundle: true, + platform: 'node', format: 'iife', globalName: 'test_ssr' });