From 4664b344e339107af43bb5936e88a3dab0e61c58 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Fri, 15 Aug 2025 16:24:10 +0800 Subject: [PATCH 01/20] feat: enhance locale handling with AsyncLocalStorage support for server-side requests --- .../request-handler/user-response.ts | 7 ++- packages/qwik/src/core/use/use-locale.ts | 45 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/packages/qwik-router/src/middleware/request-handler/user-response.ts b/packages/qwik-router/src/middleware/request-handler/user-response.ts index f37ca8a8c17..a2445a560a9 100644 --- a/packages/qwik-router/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-router/src/middleware/request-handler/user-response.ts @@ -13,6 +13,7 @@ import { } from './request-event'; import { encoder } from './resolve-request-handlers'; import type { QwikSerializer, ServerRequestEvent, StatusCodes } from './types'; +import { withLocale } from '@qwik.dev/core'; // Import separately to avoid duplicate imports in the vite dev server import { AbortMessage, @@ -64,8 +65,10 @@ export function runQwikRouter( response: responsePromise, requestEv, completion: asyncStore - ? asyncStore.run(requestEv, runNext, requestEv, rebuildRouteInfo, resolve!) - : runNext(requestEv, rebuildRouteInfo, resolve!), + ? asyncStore.run(requestEv, () => + withLocale(requestEv.locale(), () => runNext(requestEv, rebuildRouteInfo, resolve!)) + ) + : withLocale(requestEv.locale(), () => runNext(requestEv, rebuildRouteInfo, resolve!)), }; } diff --git a/packages/qwik/src/core/use/use-locale.ts b/packages/qwik/src/core/use/use-locale.ts index 5e3efdddabb..90432ff61bf 100644 --- a/packages/qwik/src/core/use/use-locale.ts +++ b/packages/qwik/src/core/use/use-locale.ts @@ -11,6 +11,21 @@ let _locale: string | undefined = undefined; * @public */ export function getLocale(defaultLocale?: string): string { + // Prefer per-request locale from AsyncLocalStorage if available (server-side) + try { + const asyncStore = (globalThis as any).qcAsyncRequestStore; + const ev = asyncStore?.getStore?.(); + const evLocale = ev && typeof ev.locale === 'function' ? ev.locale : undefined; + if (evLocale) { + const l = evLocale(); + if (l) { + return l; + } + } + } catch { + // ignore and fallback + } + if (_locale === undefined) { const ctx = tryGetInvokeContext(); if (ctx && ctx.$locale$) { @@ -30,6 +45,24 @@ export function getLocale(defaultLocale?: string): string { * @public */ export function withLocale(locale: string, fn: () => T): T { + // If running on the server with AsyncLocalStorage, set locale on the current request + try { + const asyncStore = (globalThis as any).qcAsyncRequestStore; + const ev = asyncStore?.getStore?.(); + const evLocale = ev && typeof ev.locale === 'function' ? ev.locale : undefined; + if (evLocale) { + const previous = evLocale(); + try { + evLocale(locale); + return fn(); + } finally { + evLocale(previous); + } + } + } catch { + // ignore and fallback + } + const previousLang = _locale; try { _locale = locale; @@ -48,5 +81,17 @@ export function withLocale(locale: string, fn: () => T): T { * @public */ export function setLocale(locale: string): void { + // On the server, prefer setting the locale on the per-request store + try { + const asyncStore = (globalThis as any).qcAsyncRequestStore; + const ev = asyncStore?.getStore?.(); + const evLocale = ev && typeof ev.locale === 'function' ? ev.locale : undefined; + if (evLocale) { + evLocale(locale); + return; + } + } catch { + // ignore and fallback + } _locale = locale; } From 90890c916349b35cf6988fec6089a2eff939da11 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Fri, 15 Aug 2025 16:43:52 +0800 Subject: [PATCH 02/20] add changeset --- .changeset/pretty-parents-draw.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/pretty-parents-draw.md diff --git a/.changeset/pretty-parents-draw.md b/.changeset/pretty-parents-draw.md new file mode 100644 index 00000000000..5dc3372ef3f --- /dev/null +++ b/.changeset/pretty-parents-draw.md @@ -0,0 +1,6 @@ +--- +'@qwik.dev/router': patch +'@qwik.dev/core': patch +--- + +enhance locale handling with AsyncLocalStorage support for server-side requests From 25d16481246dfcfdf4c421d290a8b58ed87deda6 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Fri, 15 Aug 2025 17:00:50 +0800 Subject: [PATCH 03/20] fix: improve locale handling in resolveHead function with AsyncLocalStorage support --- packages/qwik-router/src/runtime/src/head.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/qwik-router/src/runtime/src/head.ts b/packages/qwik-router/src/runtime/src/head.ts index 6e2136cd43f..39d31bb6ce3 100644 --- a/packages/qwik-router/src/runtime/src/head.ts +++ b/packages/qwik-router/src/runtime/src/head.ts @@ -37,9 +37,10 @@ export const resolveHead = ( } return data; }) as any as ResolveSyncValue; + const storeEv = (globalThis as any).qcAsyncRequestStore?.getStore?.(); const headProps: DocumentHeadProps = { head, - withLocale: (fn) => withLocale(locale, fn), + withLocale: (fn) => (storeEv ? fn() : withLocale(locale, fn)), resolveValue: getData, ...routeLocation, }; @@ -50,7 +51,9 @@ export const resolveHead = ( if (typeof contentModuleHead === 'function') { resolveDocumentHead( head, - withLocale(locale, () => contentModuleHead(headProps)) + storeEv + ? contentModuleHead(headProps) + : withLocale(locale, () => contentModuleHead(headProps)) ); } else if (typeof contentModuleHead === 'object') { resolveDocumentHead(head, contentModuleHead); From 0abc9d8e41a8a69d3642ea8eb12d2fc37c5a208a Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Sat, 16 Aug 2025 09:23:21 +0800 Subject: [PATCH 04/20] refactor: streamline locale handling with AsyncLocalStorage for server-side context --- .../request-handler/user-response.ts | 10 +-- packages/qwik/src/core/use/use-locale.ts | 62 +++++++++++-------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/packages/qwik-router/src/middleware/request-handler/user-response.ts b/packages/qwik-router/src/middleware/request-handler/user-response.ts index a2445a560a9..70c7b954980 100644 --- a/packages/qwik-router/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-router/src/middleware/request-handler/user-response.ts @@ -64,11 +64,11 @@ export function runQwikRouter( return { response: responsePromise, requestEv, - completion: asyncStore - ? asyncStore.run(requestEv, () => - withLocale(requestEv.locale(), () => runNext(requestEv, rebuildRouteInfo, resolve!)) - ) - : withLocale(requestEv.locale(), () => runNext(requestEv, rebuildRouteInfo, resolve!)), + completion: withLocale(requestEv.locale(), () => + asyncStore + ? asyncStore.run(requestEv, () => runNext(requestEv, rebuildRouteInfo, resolve!)) + : runNext(requestEv, rebuildRouteInfo, resolve!) + ), }; } diff --git a/packages/qwik/src/core/use/use-locale.ts b/packages/qwik/src/core/use/use-locale.ts index 90432ff61bf..ca711abfd06 100644 --- a/packages/qwik/src/core/use/use-locale.ts +++ b/packages/qwik/src/core/use/use-locale.ts @@ -1,7 +1,30 @@ import { tryGetInvokeContext } from './use-core'; +import { isServer } from '@qwik.dev/core/build'; let _locale: string | undefined = undefined; +type LocaleStore = { locale: string | undefined }; + +type LocaleAsyncStore = import('node:async_hooks').AsyncLocalStorage; + +let localAsyncStore: LocaleAsyncStore | undefined; + +if (isServer) { + try { + // Lazy import to avoid bundling for non-Node targets + import('node:async_hooks') + .then((module) => { + const AsyncLocalStorage = module.AsyncLocalStorage as unknown as new () => LocaleAsyncStore; + localAsyncStore = new AsyncLocalStorage(); + }) + .catch(() => { + // ignore if AsyncLocalStorage is not available + }); + } catch { + // ignore and fallback + } +} + /** * Retrieve the current locale. * @@ -11,16 +34,12 @@ let _locale: string | undefined = undefined; * @public */ export function getLocale(defaultLocale?: string): string { - // Prefer per-request locale from AsyncLocalStorage if available (server-side) + // Prefer per-request locale from local AsyncLocalStorage if available (server-side) try { - const asyncStore = (globalThis as any).qcAsyncRequestStore; - const ev = asyncStore?.getStore?.(); - const evLocale = ev && typeof ev.locale === 'function' ? ev.locale : undefined; - if (evLocale) { - const l = evLocale(); - if (l) { - return l; - } + const store = localAsyncStore?.getStore?.(); + const l = store?.locale; + if (l) { + return l; } } catch { // ignore and fallback @@ -45,19 +64,10 @@ export function getLocale(defaultLocale?: string): string { * @public */ export function withLocale(locale: string, fn: () => T): T { - // If running on the server with AsyncLocalStorage, set locale on the current request + // If running on the server with AsyncLocalStorage, set locale for this async context try { - const asyncStore = (globalThis as any).qcAsyncRequestStore; - const ev = asyncStore?.getStore?.(); - const evLocale = ev && typeof ev.locale === 'function' ? ev.locale : undefined; - if (evLocale) { - const previous = evLocale(); - try { - evLocale(locale); - return fn(); - } finally { - evLocale(previous); - } + if (localAsyncStore?.run) { + return localAsyncStore.run({ locale }, fn); } } catch { // ignore and fallback @@ -81,13 +91,11 @@ export function withLocale(locale: string, fn: () => T): T { * @public */ export function setLocale(locale: string): void { - // On the server, prefer setting the locale on the per-request store + // On the server, prefer setting the locale on the local per-request store try { - const asyncStore = (globalThis as any).qcAsyncRequestStore; - const ev = asyncStore?.getStore?.(); - const evLocale = ev && typeof ev.locale === 'function' ? ev.locale : undefined; - if (evLocale) { - evLocale(locale); + const store = localAsyncStore?.getStore?.(); + if (store) { + store.locale = locale; return; } } catch { From 27dd79508e7322ab1dea943b79db373795e2c369 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Mon, 18 Aug 2025 09:40:45 +0800 Subject: [PATCH 05/20] refactor: remove unnecessary comment and simplify store retrieval in head.ts --- packages/qwik-router/src/runtime/src/head.ts | 2 +- packages/qwik/src/core/use/use-locale.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/qwik-router/src/runtime/src/head.ts b/packages/qwik-router/src/runtime/src/head.ts index 39d31bb6ce3..a347c43e396 100644 --- a/packages/qwik-router/src/runtime/src/head.ts +++ b/packages/qwik-router/src/runtime/src/head.ts @@ -37,7 +37,7 @@ export const resolveHead = ( } return data; }) as any as ResolveSyncValue; - const storeEv = (globalThis as any).qcAsyncRequestStore?.getStore?.(); + const storeEv = (globalThis as any).qcAsyncRequestStore; const headProps: DocumentHeadProps = { head, withLocale: (fn) => (storeEv ? fn() : withLocale(locale, fn)), diff --git a/packages/qwik/src/core/use/use-locale.ts b/packages/qwik/src/core/use/use-locale.ts index ca711abfd06..4aaf785795e 100644 --- a/packages/qwik/src/core/use/use-locale.ts +++ b/packages/qwik/src/core/use/use-locale.ts @@ -11,7 +11,6 @@ let localAsyncStore: LocaleAsyncStore | undefined; if (isServer) { try { - // Lazy import to avoid bundling for non-Node targets import('node:async_hooks') .then((module) => { const AsyncLocalStorage = module.AsyncLocalStorage as unknown as new () => LocaleAsyncStore; From 1c341e44bd4053581733234eaffd345f21d1bd09 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Tue, 19 Aug 2025 17:02:13 +0800 Subject: [PATCH 06/20] refactor: simplify AsyncLocalStorage usage and improve locale retrieval in use-locale.ts --- .../request-handler/user-response.ts | 7 ++--- packages/qwik-router/src/runtime/src/head.ts | 2 +- packages/qwik/src/core/use/use-locale.ts | 27 ++++++++----------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/qwik-router/src/middleware/request-handler/user-response.ts b/packages/qwik-router/src/middleware/request-handler/user-response.ts index 70c7b954980..c3bb6e22012 100644 --- a/packages/qwik-router/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-router/src/middleware/request-handler/user-response.ts @@ -64,10 +64,11 @@ export function runQwikRouter( return { response: responsePromise, requestEv, - completion: withLocale(requestEv.locale(), () => + completion: withLocale( + requestEv.locale(), asyncStore - ? asyncStore.run(requestEv, () => runNext(requestEv, rebuildRouteInfo, resolve!)) - : runNext(requestEv, rebuildRouteInfo, resolve!) + ? () => asyncStore!.run(requestEv, runNext, requestEv, rebuildRouteInfo, resolve!) + : () => runNext(requestEv, rebuildRouteInfo, resolve!) ), }; } diff --git a/packages/qwik-router/src/runtime/src/head.ts b/packages/qwik-router/src/runtime/src/head.ts index a347c43e396..a712bc9110d 100644 --- a/packages/qwik-router/src/runtime/src/head.ts +++ b/packages/qwik-router/src/runtime/src/head.ts @@ -40,7 +40,7 @@ export const resolveHead = ( const storeEv = (globalThis as any).qcAsyncRequestStore; const headProps: DocumentHeadProps = { head, - withLocale: (fn) => (storeEv ? fn() : withLocale(locale, fn)), + withLocale: storeEv ? (fn) => fn() : (fn) => withLocale(locale, fn), resolveValue: getData, ...routeLocation, }; diff --git a/packages/qwik/src/core/use/use-locale.ts b/packages/qwik/src/core/use/use-locale.ts index 4aaf785795e..589529ad789 100644 --- a/packages/qwik/src/core/use/use-locale.ts +++ b/packages/qwik/src/core/use/use-locale.ts @@ -10,18 +10,14 @@ type LocaleAsyncStore = import('node:async_hooks').AsyncLocalStorage { - const AsyncLocalStorage = module.AsyncLocalStorage as unknown as new () => LocaleAsyncStore; - localAsyncStore = new AsyncLocalStorage(); - }) - .catch(() => { - // ignore if AsyncLocalStorage is not available - }); - } catch { - // ignore and fallback - } + import('node:async_hooks') + .then((module) => { + const AsyncLocalStorage = module.AsyncLocalStorage as unknown as new () => LocaleAsyncStore; + localAsyncStore = new AsyncLocalStorage(); + }) + .catch(() => { + // ignore if AsyncLocalStorage is not available + }); } /** @@ -35,10 +31,9 @@ if (isServer) { export function getLocale(defaultLocale?: string): string { // Prefer per-request locale from local AsyncLocalStorage if available (server-side) try { - const store = localAsyncStore?.getStore?.(); - const l = store?.locale; - if (l) { - return l; + const locale = localAsyncStore?.getStore?.()?.locale; + if (locale) { + return locale; } } catch { // ignore and fallback From 92d67daa6d61d3e5b3b8e8e49663e1c7988eba72 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Mon, 25 Aug 2025 13:42:18 +0800 Subject: [PATCH 07/20] refactor: unify AsyncLocalStorage usage across router middleware and improve request handling --- packages/qwik-router/global.d.ts | 1 - .../request-handler/async-request-store.ts | 11 ++++++++ .../request-handler/request-event.ts | 3 ++- .../request-handler/user-response.ts | 4 ++- packages/qwik-router/src/runtime/src/head.ts | 8 +++--- .../src/runtime/src/server-functions.ts | 3 ++- packages/qwik/src/core/use/use-locale.ts | 25 ++++++++----------- 7 files changed, 34 insertions(+), 21 deletions(-) create mode 100644 packages/qwik-router/src/middleware/request-handler/async-request-store.ts diff --git a/packages/qwik-router/global.d.ts b/packages/qwik-router/global.d.ts index 2f382a9ddcf..12b823070f8 100644 --- a/packages/qwik-router/global.d.ts +++ b/packages/qwik-router/global.d.ts @@ -6,7 +6,6 @@ type RequestEventInternal = type AsyncStore = import('node:async_hooks').AsyncLocalStorage; type SerializationStrategy = import('@qwik.dev/core/internal').SerializationStrategy; -declare var qcAsyncRequestStore: AsyncStore | undefined; declare var _qwikActionsMap: Map | undefined; /** @deprecated Will be removed in v3 */ diff --git a/packages/qwik-router/src/middleware/request-handler/async-request-store.ts b/packages/qwik-router/src/middleware/request-handler/async-request-store.ts new file mode 100644 index 00000000000..fd34f6ae826 --- /dev/null +++ b/packages/qwik-router/src/middleware/request-handler/async-request-store.ts @@ -0,0 +1,11 @@ +import type { RequestEventInternal } from './request-event'; +import type { AsyncLocalStorage } from 'node:async_hooks'; + +export type AsyncStore = AsyncLocalStorage; + +// Qwik Core will also be using the async store if this is present +export let asyncRequestStore: AsyncStore | undefined; + +export const setAsyncRequestStore = (store: AsyncStore | undefined) => { + asyncRequestStore = store; +}; diff --git a/packages/qwik-router/src/middleware/request-handler/request-event.ts b/packages/qwik-router/src/middleware/request-handler/request-event.ts index 9cdadfe9055..42096286bd7 100644 --- a/packages/qwik-router/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-router/src/middleware/request-handler/request-event.ts @@ -20,6 +20,7 @@ import { RewriteMessage, } from '@qwik.dev/router/middleware/request-handler'; import { encoder, getRouteLoaderPromise } from './resolve-request-handlers'; +import { asyncRequestStore } from './async-request-store'; import type { CacheControl, CacheControlTarget, @@ -83,7 +84,7 @@ export function createRequestEvent( while (routeModuleIndex < requestHandlers.length) { const moduleRequestHandler = requestHandlers[routeModuleIndex]; - const asyncStore = globalThis.qcAsyncRequestStore; + const asyncStore = asyncRequestStore; const result = asyncStore?.run ? asyncStore.run(requestEv, moduleRequestHandler, requestEv) : moduleRequestHandler(requestEv); diff --git a/packages/qwik-router/src/middleware/request-handler/user-response.ts b/packages/qwik-router/src/middleware/request-handler/user-response.ts index c3bb6e22012..f94450d61af 100644 --- a/packages/qwik-router/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-router/src/middleware/request-handler/user-response.ts @@ -14,6 +14,8 @@ import { import { encoder } from './resolve-request-handlers'; import type { QwikSerializer, ServerRequestEvent, StatusCodes } from './types'; import { withLocale } from '@qwik.dev/core'; +import { setAsyncRequestStore } from './async-request-store'; +import type { AsyncStore } from './async-request-store'; // Import separately to avoid duplicate imports in the vite dev server import { AbortMessage, @@ -33,7 +35,7 @@ import('node:async_hooks') .then((module) => { const AsyncLocalStorage = module.AsyncLocalStorage; asyncStore = new AsyncLocalStorage(); - globalThis.qcAsyncRequestStore = asyncStore; + setAsyncRequestStore(asyncStore); }) .catch((err) => { console.warn( diff --git a/packages/qwik-router/src/runtime/src/head.ts b/packages/qwik-router/src/runtime/src/head.ts index a712bc9110d..ab4d7d696aa 100644 --- a/packages/qwik-router/src/runtime/src/head.ts +++ b/packages/qwik-router/src/runtime/src/head.ts @@ -13,6 +13,7 @@ import type { ActionInternal, } from './types'; import { isPromise } from './utils'; +import { asyncRequestStore } from '../../middleware/request-handler/async-request-store'; export const resolveHead = ( endpoint: EndpointResponse | ClientPageData, @@ -37,10 +38,11 @@ export const resolveHead = ( } return data; }) as any as ResolveSyncValue; - const storeEv = (globalThis as any).qcAsyncRequestStore; + // Qwik Core will also be using the async store if this is present + const hasAsyncStore = asyncRequestStore; const headProps: DocumentHeadProps = { head, - withLocale: storeEv ? (fn) => fn() : (fn) => withLocale(locale, fn), + withLocale: hasAsyncStore ? (fn) => fn() : (fn) => withLocale(locale, fn), resolveValue: getData, ...routeLocation, }; @@ -51,7 +53,7 @@ export const resolveHead = ( if (typeof contentModuleHead === 'function') { resolveDocumentHead( head, - storeEv + hasAsyncStore ? contentModuleHead(headProps) : withLocale(locale, () => contentModuleHead(headProps)) ); diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index fafddd70ab2..3ba57ada274 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -65,6 +65,7 @@ import { useAction, useLocation, useQwikRouterEnv } from './use-functions'; import type { FormSubmitCompletedDetail } from './form-component'; import { deepFreeze } from './utils'; +import { asyncRequestStore } from '../../middleware/request-handler/async-request-store'; /** @internal */ export const routeActionQrl = (( @@ -420,7 +421,7 @@ export const serverQrl = ( if (isServer) { // Running during SSR, we can call the function directly - let requestEvent = globalThis.qcAsyncRequestStore?.getStore() as RequestEvent | undefined; + let requestEvent = asyncRequestStore?.getStore() as RequestEvent | undefined; if (!requestEvent) { const contexts = [useQwikRouterEnv()?.ev, this, _getContextEvent()] as RequestEvent[]; diff --git a/packages/qwik/src/core/use/use-locale.ts b/packages/qwik/src/core/use/use-locale.ts index 589529ad789..81a3cd6f106 100644 --- a/packages/qwik/src/core/use/use-locale.ts +++ b/packages/qwik/src/core/use/use-locale.ts @@ -1,18 +1,17 @@ import { tryGetInvokeContext } from './use-core'; import { isServer } from '@qwik.dev/core/build'; +import type { AsyncLocalStorage } from 'node:async_hooks'; let _locale: string | undefined = undefined; -type LocaleStore = { locale: string | undefined }; - -type LocaleAsyncStore = import('node:async_hooks').AsyncLocalStorage; - -let localAsyncStore: LocaleAsyncStore | undefined; +let localAsyncStore: AsyncLocalStorage<{ locale?: string }> | undefined; if (isServer) { import('node:async_hooks') .then((module) => { - const AsyncLocalStorage = module.AsyncLocalStorage as unknown as new () => LocaleAsyncStore; + const AsyncLocalStorage = module.AsyncLocalStorage as unknown as new () => AsyncLocalStorage<{ + locale?: string; + }>; localAsyncStore = new AsyncLocalStorage(); }) .catch(() => { @@ -30,13 +29,11 @@ if (isServer) { */ export function getLocale(defaultLocale?: string): string { // Prefer per-request locale from local AsyncLocalStorage if available (server-side) - try { - const locale = localAsyncStore?.getStore?.()?.locale; + if (localAsyncStore) { + const locale = localAsyncStore.getStore()?.locale; if (locale) { return locale; } - } catch { - // ignore and fallback } if (_locale === undefined) { @@ -60,7 +57,7 @@ export function getLocale(defaultLocale?: string): string { export function withLocale(locale: string, fn: () => T): T { // If running on the server with AsyncLocalStorage, set locale for this async context try { - if (localAsyncStore?.run) { + if (localAsyncStore) { return localAsyncStore.run({ locale }, fn); } } catch { @@ -87,9 +84,9 @@ export function withLocale(locale: string, fn: () => T): T { export function setLocale(locale: string): void { // On the server, prefer setting the locale on the local per-request store try { - const store = localAsyncStore?.getStore?.(); - if (store) { - store.locale = locale; + if (localAsyncStore && localAsyncStore.getStore) { + const store = localAsyncStore.getStore(); + store!.locale = locale; return; } } catch { From 559b9f381f4bb7196ced25407677dcea940cf942 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Tue, 26 Aug 2025 10:24:03 +0800 Subject: [PATCH 08/20] refactor: streamline AsyncLocalStorage usage in locale functions and improve code clarity --- packages/qwik-router/src/runtime/src/head.ts | 3 +-- packages/qwik/src/core/use/use-locale.ts | 22 ++++++-------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/packages/qwik-router/src/runtime/src/head.ts b/packages/qwik-router/src/runtime/src/head.ts index ab4d7d696aa..dd8ec4c5661 100644 --- a/packages/qwik-router/src/runtime/src/head.ts +++ b/packages/qwik-router/src/runtime/src/head.ts @@ -13,7 +13,7 @@ import type { ActionInternal, } from './types'; import { isPromise } from './utils'; -import { asyncRequestStore } from '../../middleware/request-handler/async-request-store'; +import { asyncRequestStore as hasAsyncStore } from '../../middleware/request-handler/async-request-store'; export const resolveHead = ( endpoint: EndpointResponse | ClientPageData, @@ -39,7 +39,6 @@ export const resolveHead = ( return data; }) as any as ResolveSyncValue; // Qwik Core will also be using the async store if this is present - const hasAsyncStore = asyncRequestStore; const headProps: DocumentHeadProps = { head, withLocale: hasAsyncStore ? (fn) => fn() : (fn) => withLocale(locale, fn), diff --git a/packages/qwik/src/core/use/use-locale.ts b/packages/qwik/src/core/use/use-locale.ts index 81a3cd6f106..83d545f8f3f 100644 --- a/packages/qwik/src/core/use/use-locale.ts +++ b/packages/qwik/src/core/use/use-locale.ts @@ -55,13 +55,8 @@ export function getLocale(defaultLocale?: string): string { * @public */ export function withLocale(locale: string, fn: () => T): T { - // If running on the server with AsyncLocalStorage, set locale for this async context - try { - if (localAsyncStore) { - return localAsyncStore.run({ locale }, fn); - } - } catch { - // ignore and fallback + if (localAsyncStore) { + return localAsyncStore.run({ locale }, fn); } const previousLang = _locale; @@ -82,15 +77,10 @@ export function withLocale(locale: string, fn: () => T): T { * @public */ export function setLocale(locale: string): void { - // On the server, prefer setting the locale on the local per-request store - try { - if (localAsyncStore && localAsyncStore.getStore) { - const store = localAsyncStore.getStore(); - store!.locale = locale; - return; - } - } catch { - // ignore and fallback + if (localAsyncStore && localAsyncStore.getStore) { + const store = localAsyncStore.getStore(); + store!.locale = locale; + return; } _locale = locale; } From 0e148b711fc8dc3e45b8c284478e502084e7e158 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Tue, 26 Aug 2025 14:21:01 +0800 Subject: [PATCH 09/20] refactor: simplify AsyncLocalStorage usage and improve locale retrieval in use-locale.ts --- packages/qwik-router/global.d.ts | 2 ++ .../middleware/request-handler/async-request-store.ts | 4 ++-- .../src/middleware/request-handler/user-response.ts | 11 +++++------ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/qwik-router/global.d.ts b/packages/qwik-router/global.d.ts index 12b823070f8..ac5e1996356 100644 --- a/packages/qwik-router/global.d.ts +++ b/packages/qwik-router/global.d.ts @@ -6,6 +6,8 @@ type RequestEventInternal = type AsyncStore = import('node:async_hooks').AsyncLocalStorage; type SerializationStrategy = import('@qwik.dev/core/internal').SerializationStrategy; +declare var qcAsyncRequestStore: AsyncStore | undefined; + declare var _qwikActionsMap: Map | undefined; /** @deprecated Will be removed in v3 */ diff --git a/packages/qwik-router/src/middleware/request-handler/async-request-store.ts b/packages/qwik-router/src/middleware/request-handler/async-request-store.ts index fd34f6ae826..72b87db8203 100644 --- a/packages/qwik-router/src/middleware/request-handler/async-request-store.ts +++ b/packages/qwik-router/src/middleware/request-handler/async-request-store.ts @@ -4,8 +4,8 @@ import type { AsyncLocalStorage } from 'node:async_hooks'; export type AsyncStore = AsyncLocalStorage; // Qwik Core will also be using the async store if this is present -export let asyncRequestStore: AsyncStore | undefined; +export const asyncRequestStore: AsyncStore | undefined = globalThis.qcAsyncRequestStore; export const setAsyncRequestStore = (store: AsyncStore | undefined) => { - asyncRequestStore = store; + globalThis.qcAsyncRequestStore = store; }; diff --git a/packages/qwik-router/src/middleware/request-handler/user-response.ts b/packages/qwik-router/src/middleware/request-handler/user-response.ts index f94450d61af..d7c5c311207 100644 --- a/packages/qwik-router/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-router/src/middleware/request-handler/user-response.ts @@ -14,8 +14,7 @@ import { import { encoder } from './resolve-request-handlers'; import type { QwikSerializer, ServerRequestEvent, StatusCodes } from './types'; import { withLocale } from '@qwik.dev/core'; -import { setAsyncRequestStore } from './async-request-store'; -import type { AsyncStore } from './async-request-store'; +import { setAsyncRequestStore, asyncRequestStore } from './async-request-store'; // Import separately to avoid duplicate imports in the vite dev server import { AbortMessage, @@ -30,11 +29,11 @@ export interface QwikRouterRun { completion: Promise; } -let asyncStore: AsyncStore | undefined; import('node:async_hooks') .then((module) => { const AsyncLocalStorage = module.AsyncLocalStorage; - asyncStore = new AsyncLocalStorage(); + const asyncStore = new AsyncLocalStorage(); + console.log('AsyncLocalStorage', asyncStore); setAsyncRequestStore(asyncStore); }) .catch((err) => { @@ -68,8 +67,8 @@ export function runQwikRouter( requestEv, completion: withLocale( requestEv.locale(), - asyncStore - ? () => asyncStore!.run(requestEv, runNext, requestEv, rebuildRouteInfo, resolve!) + asyncRequestStore + ? () => asyncRequestStore!.run(requestEv, runNext, requestEv, rebuildRouteInfo, resolve!) : () => runNext(requestEv, rebuildRouteInfo, resolve!) ), }; From c1c07c65fde33ddc4c4bf4eb72381dd2afeb0938 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Tue, 26 Aug 2025 14:24:56 +0800 Subject: [PATCH 10/20] refactor: remove debug log for AsyncLocalStorage in user-response.ts --- .../qwik-router/src/middleware/request-handler/user-response.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/qwik-router/src/middleware/request-handler/user-response.ts b/packages/qwik-router/src/middleware/request-handler/user-response.ts index d7c5c311207..39100b1e469 100644 --- a/packages/qwik-router/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-router/src/middleware/request-handler/user-response.ts @@ -33,7 +33,6 @@ import('node:async_hooks') .then((module) => { const AsyncLocalStorage = module.AsyncLocalStorage; const asyncStore = new AsyncLocalStorage(); - console.log('AsyncLocalStorage', asyncStore); setAsyncRequestStore(asyncStore); }) .catch((err) => { From 888e4df54f10791b91f8b16e581469fd653f42c6 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Tue, 9 Sep 2025 16:08:14 +0800 Subject: [PATCH 11/20] refactor: enhance AsyncLocalStorage integration in request handling and locale management --- .../request-handler/async-request-store.ts | 13 +++++++++++++ .../middleware/request-handler/user-response.ts | 15 +-------------- packages/qwik/src/core/use/use-locale.ts | 16 ++++++---------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/packages/qwik-router/src/middleware/request-handler/async-request-store.ts b/packages/qwik-router/src/middleware/request-handler/async-request-store.ts index 72b87db8203..f286631064f 100644 --- a/packages/qwik-router/src/middleware/request-handler/async-request-store.ts +++ b/packages/qwik-router/src/middleware/request-handler/async-request-store.ts @@ -9,3 +9,16 @@ export const asyncRequestStore: AsyncStore | undefined = globalThis.qcAsyncReque export const setAsyncRequestStore = (store: AsyncStore | undefined) => { globalThis.qcAsyncRequestStore = store; }; + +import('node:async_hooks') + .then((module) => { + const AsyncLocalStorage = module.AsyncLocalStorage; + const asyncStore = new AsyncLocalStorage(); + setAsyncRequestStore(asyncStore); + }) + .catch((err) => { + console.warn( + 'AsyncLocalStorage not available, continuing without it. This might impact concurrent server calls.', + err + ); + }); diff --git a/packages/qwik-router/src/middleware/request-handler/user-response.ts b/packages/qwik-router/src/middleware/request-handler/user-response.ts index 39100b1e469..b431aa97319 100644 --- a/packages/qwik-router/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-router/src/middleware/request-handler/user-response.ts @@ -14,7 +14,7 @@ import { import { encoder } from './resolve-request-handlers'; import type { QwikSerializer, ServerRequestEvent, StatusCodes } from './types'; import { withLocale } from '@qwik.dev/core'; -import { setAsyncRequestStore, asyncRequestStore } from './async-request-store'; +import { asyncRequestStore } from './async-request-store'; // Import separately to avoid duplicate imports in the vite dev server import { AbortMessage, @@ -29,19 +29,6 @@ export interface QwikRouterRun { completion: Promise; } -import('node:async_hooks') - .then((module) => { - const AsyncLocalStorage = module.AsyncLocalStorage; - const asyncStore = new AsyncLocalStorage(); - setAsyncRequestStore(asyncStore); - }) - .catch((err) => { - console.warn( - 'AsyncLocalStorage not available, continuing without it. This might impact concurrent server calls.', - err - ); - }); - export function runQwikRouter( serverRequestEv: ServerRequestEvent, loadedRoute: LoadedRoute | null, diff --git a/packages/qwik/src/core/use/use-locale.ts b/packages/qwik/src/core/use/use-locale.ts index 83d545f8f3f..ab7ec99318e 100644 --- a/packages/qwik/src/core/use/use-locale.ts +++ b/packages/qwik/src/core/use/use-locale.ts @@ -4,15 +4,12 @@ import type { AsyncLocalStorage } from 'node:async_hooks'; let _locale: string | undefined = undefined; -let localAsyncStore: AsyncLocalStorage<{ locale?: string }> | undefined; +let localAsyncStore: AsyncLocalStorage | undefined; if (isServer) { import('node:async_hooks') .then((module) => { - const AsyncLocalStorage = module.AsyncLocalStorage as unknown as new () => AsyncLocalStorage<{ - locale?: string; - }>; - localAsyncStore = new AsyncLocalStorage(); + localAsyncStore = new module.AsyncLocalStorage(); }) .catch(() => { // ignore if AsyncLocalStorage is not available @@ -30,7 +27,7 @@ if (isServer) { export function getLocale(defaultLocale?: string): string { // Prefer per-request locale from local AsyncLocalStorage if available (server-side) if (localAsyncStore) { - const locale = localAsyncStore.getStore()?.locale; + const locale = localAsyncStore.getStore(); if (locale) { return locale; } @@ -56,7 +53,7 @@ export function getLocale(defaultLocale?: string): string { */ export function withLocale(locale: string, fn: () => T): T { if (localAsyncStore) { - return localAsyncStore.run({ locale }, fn); + return localAsyncStore.run(locale, fn); } const previousLang = _locale; @@ -77,9 +74,8 @@ export function withLocale(locale: string, fn: () => T): T { * @public */ export function setLocale(locale: string): void { - if (localAsyncStore && localAsyncStore.getStore) { - const store = localAsyncStore.getStore(); - store!.locale = locale; + if (isServer) { + // ignore on server; use withLocale() within a request scope instead return; } _locale = locale; From 1609ffd5b30e997030f1a5e8e6d362f8621e2fbb Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Wed, 10 Sep 2025 18:43:12 +0800 Subject: [PATCH 12/20] refactor: remove server-side check in setLocale and update asyncRequestStore initialization --- packages/qwik-router/global.d.ts | 2 -- .../request-handler/async-request-store.ts | 13 ++++--------- packages/qwik/src/core/use/use-locale.ts | 4 ---- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/qwik-router/global.d.ts b/packages/qwik-router/global.d.ts index ac5e1996356..12b823070f8 100644 --- a/packages/qwik-router/global.d.ts +++ b/packages/qwik-router/global.d.ts @@ -6,8 +6,6 @@ type RequestEventInternal = type AsyncStore = import('node:async_hooks').AsyncLocalStorage; type SerializationStrategy = import('@qwik.dev/core/internal').SerializationStrategy; -declare var qcAsyncRequestStore: AsyncStore | undefined; - declare var _qwikActionsMap: Map | undefined; /** @deprecated Will be removed in v3 */ diff --git a/packages/qwik-router/src/middleware/request-handler/async-request-store.ts b/packages/qwik-router/src/middleware/request-handler/async-request-store.ts index f286631064f..1f0d5b38e87 100644 --- a/packages/qwik-router/src/middleware/request-handler/async-request-store.ts +++ b/packages/qwik-router/src/middleware/request-handler/async-request-store.ts @@ -3,18 +3,10 @@ import type { AsyncLocalStorage } from 'node:async_hooks'; export type AsyncStore = AsyncLocalStorage; -// Qwik Core will also be using the async store if this is present -export const asyncRequestStore: AsyncStore | undefined = globalThis.qcAsyncRequestStore; - -export const setAsyncRequestStore = (store: AsyncStore | undefined) => { - globalThis.qcAsyncRequestStore = store; -}; - import('node:async_hooks') .then((module) => { const AsyncLocalStorage = module.AsyncLocalStorage; - const asyncStore = new AsyncLocalStorage(); - setAsyncRequestStore(asyncStore); + asyncRequestStore = new AsyncLocalStorage(); }) .catch((err) => { console.warn( @@ -22,3 +14,6 @@ import('node:async_hooks') err ); }); + +// Qwik Core will also be using the async store if this is present +export let asyncRequestStore: AsyncStore | undefined = undefined; diff --git a/packages/qwik/src/core/use/use-locale.ts b/packages/qwik/src/core/use/use-locale.ts index ab7ec99318e..da62788cc33 100644 --- a/packages/qwik/src/core/use/use-locale.ts +++ b/packages/qwik/src/core/use/use-locale.ts @@ -74,9 +74,5 @@ export function withLocale(locale: string, fn: () => T): T { * @public */ export function setLocale(locale: string): void { - if (isServer) { - // ignore on server; use withLocale() within a request scope instead - return; - } _locale = locale; } From ef7579846f77adb499e1c9b1c9a6221ad93470b7 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Wed, 10 Sep 2025 18:56:45 +0800 Subject: [PATCH 13/20] refactor: optimize AsyncLocalStorage handling in locale functions and enhance code organization --- .../src/routes/locale-concurrent/index.tsx | 67 +++++++++++++++++++ .../e2e/qwikrouter/locale-concurrent.e2e.ts | 37 ++++++++++ 2 files changed, 104 insertions(+) create mode 100644 starters/apps/qwikrouter-test/src/routes/locale-concurrent/index.tsx create mode 100644 starters/e2e/qwikrouter/locale-concurrent.e2e.ts diff --git a/starters/apps/qwikrouter-test/src/routes/locale-concurrent/index.tsx b/starters/apps/qwikrouter-test/src/routes/locale-concurrent/index.tsx new file mode 100644 index 00000000000..e0d9c011ce0 --- /dev/null +++ b/starters/apps/qwikrouter-test/src/routes/locale-concurrent/index.tsx @@ -0,0 +1,67 @@ +import { component$, Resource, getLocale } from "@qwik.dev/core"; +import type { RequestHandler } from "@qwik.dev/router"; +import { routeLoader$ } from "@qwik.dev/router"; + +// Simple in-memory barrier to coordinate two concurrent requests in tests. +type Barrier = { + waiters: Set; + promise?: Promise; + resolve?: () => void; +}; + +const barriers = new Map(); + +function getBarrier(group: string): Barrier { + let b = barriers.get(group); + if (!b) { + b = { waiters: new Set() }; + barriers.set(group, b); + } + return b; +} + +function waitForBoth(group: string, id: string) { + const barrier = getBarrier(group); + if (!barrier.promise) { + barrier.promise = new Promise( + (resolve) => (barrier.resolve = resolve), + ); + } + barrier.waiters.add(id); + if (barrier.waiters.size >= 2) { + barrier.resolve?.(); + } + return barrier.promise!; +} + +export const onRequest: RequestHandler = ({ url, locale }) => { + const qpLocale = url.searchParams.get("locale"); + if (qpLocale) { + locale(qpLocale); + } +}; + +export const useBarrier = routeLoader$(({ url }) => { + const group = url.searchParams.get("group") || "default"; + const id = url.searchParams.get("id") || Math.random().toString(36).slice(2); + return waitForBoth(group, id).then(() => ({ done: true })); +}); + +export default component$(() => { + const barrier = useBarrier(); + return ( +
+

+ Before barrier locale: {getLocale()} +

+ ( +

+ After barrier locale: {getLocale()} +

+ )} + /> +
+ ); +}); diff --git a/starters/e2e/qwikrouter/locale-concurrent.e2e.ts b/starters/e2e/qwikrouter/locale-concurrent.e2e.ts new file mode 100644 index 00000000000..199ee98f42a --- /dev/null +++ b/starters/e2e/qwikrouter/locale-concurrent.e2e.ts @@ -0,0 +1,37 @@ +import { expect, test } from "@playwright/test"; + +// This test ensures asyncRequestStore locale isolation across concurrent requests. +// It triggers two concurrent server renders to the same route with different locales, +// and uses a server-side barrier so the page reveals the locale only after both renders started. + +test.describe("Qwik Router concurrent locale", () => { + test("should isolate locale per concurrent request", async ({ browser }) => { + const ctx1 = await browser.newContext(); + const ctx2 = await browser.newContext(); + + const page1 = await ctx1.newPage(); + const page2 = await ctx2.newPage(); + + const url1 = + "/qwikrouter-test/locale-concurrent?group=g&id=one&locale=en-US"; + const url2 = + "/qwikrouter-test/locale-concurrent?group=g&id=two&locale=fr-FR"; + + // Start both navigations without waiting them to finish + const nav1 = page1.goto(url1); + const nav2 = page2.goto(url2); + + await Promise.all([nav1, nav2]); + + // Before barrier render, locale is already set and visible in first block + await expect(page1.locator(".locale-before")).toHaveText("en-US"); + await expect(page2.locator(".locale-before")).toHaveText("fr-FR"); + + // After barrier releases, the bottom content renders and must preserve each locale + await expect(page1.locator(".locale")).toHaveText("en-US"); + await expect(page2.locator(".locale")).toHaveText("fr-FR"); + + await ctx1.close(); + await ctx2.close(); + }); +}); From 1293991437efdfd8038ceabea3158293ce4e20e9 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Thu, 25 Sep 2025 17:04:29 +0800 Subject: [PATCH 14/20] refactor: add server-side check for AsyncLocalStorage initialization in async-request-store --- .../request-handler/async-request-store.ts | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/qwik-router/src/middleware/request-handler/async-request-store.ts b/packages/qwik-router/src/middleware/request-handler/async-request-store.ts index 1f0d5b38e87..e72ff3b538f 100644 --- a/packages/qwik-router/src/middleware/request-handler/async-request-store.ts +++ b/packages/qwik-router/src/middleware/request-handler/async-request-store.ts @@ -1,19 +1,22 @@ +import { isServer } from 'packages/qwik/dist'; import type { RequestEventInternal } from './request-event'; import type { AsyncLocalStorage } from 'node:async_hooks'; export type AsyncStore = AsyncLocalStorage; -import('node:async_hooks') - .then((module) => { - const AsyncLocalStorage = module.AsyncLocalStorage; - asyncRequestStore = new AsyncLocalStorage(); - }) - .catch((err) => { - console.warn( - 'AsyncLocalStorage not available, continuing without it. This might impact concurrent server calls.', - err - ); - }); +if (isServer) { + import('node:async_hooks') + .then((module) => { + const AsyncLocalStorage = module.AsyncLocalStorage; + asyncRequestStore = new AsyncLocalStorage(); + }) + .catch((err) => { + console.warn( + 'AsyncLocalStorage not available, continuing without it. This might impact concurrent server calls.', + err + ); + }); +} // Qwik Core will also be using the async store if this is present export let asyncRequestStore: AsyncStore | undefined = undefined; From e97bc4066b2ff58635fc56f6ae1525854a7614a9 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Fri, 26 Sep 2025 14:50:52 +0800 Subject: [PATCH 15/20] refactor: update import paths for core modules in async-request-store and server-functions --- .../src/middleware/request-handler/async-request-store.ts | 2 +- packages/qwik-router/src/runtime/src/server-functions.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/qwik-router/src/middleware/request-handler/async-request-store.ts b/packages/qwik-router/src/middleware/request-handler/async-request-store.ts index e72ff3b538f..bf05e191813 100644 --- a/packages/qwik-router/src/middleware/request-handler/async-request-store.ts +++ b/packages/qwik-router/src/middleware/request-handler/async-request-store.ts @@ -1,4 +1,4 @@ -import { isServer } from 'packages/qwik/dist'; +import { isServer } from '@qwik.dev/core'; import type { RequestEventInternal } from './request-event'; import type { AsyncLocalStorage } from 'node:async_hooks'; diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index 3ba57ada274..32cf842d7e5 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -64,8 +64,8 @@ import type { import { useAction, useLocation, useQwikRouterEnv } from './use-functions'; import type { FormSubmitCompletedDetail } from './form-component'; -import { deepFreeze } from './utils'; import { asyncRequestStore } from '../../middleware/request-handler/async-request-store'; +import { deepFreeze } from './deepFreeze'; /** @internal */ export const routeActionQrl = (( From e1bd82c51faeabf4a00d77c43c676f7a1193f55e Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Fri, 26 Sep 2025 15:30:57 +0800 Subject: [PATCH 16/20] refactor(router): improve plugin retrieval logic in Vite adapter --- packages/qwik-router/src/adapters/shared/vite/index.ts | 5 ++--- packages/qwik-router/src/buildtime/vite/plugin.ts | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/qwik-router/src/adapters/shared/vite/index.ts b/packages/qwik-router/src/adapters/shared/vite/index.ts index 708e54926b4..c6f1dc62991 100644 --- a/packages/qwik-router/src/adapters/shared/vite/index.ts +++ b/packages/qwik-router/src/adapters/shared/vite/index.ts @@ -48,9 +48,8 @@ export function viteAdapter(opts: ViteAdapterPluginOptions) { if (!qwikRouterPlugin) { throw new Error('Missing vite-plugin-qwik-router'); } - qwikVitePlugin = config.plugins.find( - (p) => p.name === 'vite-plugin-qwik' - ) as QwikVitePlugin; + const foundQwikPlugin = config.plugins.find((p) => p.name === 'vite-plugin-qwik'); + qwikVitePlugin = foundQwikPlugin as QwikVitePlugin; if (!qwikVitePlugin) { throw new Error('Missing vite-plugin-qwik'); } diff --git a/packages/qwik-router/src/buildtime/vite/plugin.ts b/packages/qwik-router/src/buildtime/vite/plugin.ts index 19d2babf251..4569d74bfc2 100644 --- a/packages/qwik-router/src/buildtime/vite/plugin.ts +++ b/packages/qwik-router/src/buildtime/vite/plugin.ts @@ -134,7 +134,8 @@ function qwikRouterPlugin(userOpts?: QwikRouterVitePluginOptions): any { mdxTransform = await createMdxTransformer(ctx); - qwikPlugin = config.plugins.find((p) => p.name === 'vite-plugin-qwik') as QwikVitePlugin; + const foundPlugin = config.plugins.find((p) => p.name === 'vite-plugin-qwik'); + qwikPlugin = foundPlugin as QwikVitePlugin; if (!qwikPlugin) { throw new Error('Missing vite-plugin-qwik'); } From 412a0e3168162bd8d7920e8635d967ed61dfb93d Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Fri, 26 Sep 2025 15:41:20 +0800 Subject: [PATCH 17/20] refactor(router): unify plugin retrieval logic in Vite adapter and buildtime plugin --- packages/qwik-router/src/adapters/shared/vite/index.ts | 5 +++-- packages/qwik-router/src/buildtime/vite/plugin.ts | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/qwik-router/src/adapters/shared/vite/index.ts b/packages/qwik-router/src/adapters/shared/vite/index.ts index c6f1dc62991..e2ff36a3b70 100644 --- a/packages/qwik-router/src/adapters/shared/vite/index.ts +++ b/packages/qwik-router/src/adapters/shared/vite/index.ts @@ -48,8 +48,9 @@ export function viteAdapter(opts: ViteAdapterPluginOptions) { if (!qwikRouterPlugin) { throw new Error('Missing vite-plugin-qwik-router'); } - const foundQwikPlugin = config.plugins.find((p) => p.name === 'vite-plugin-qwik'); - qwikVitePlugin = foundQwikPlugin as QwikVitePlugin; + qwikVitePlugin = config.plugins.find( + (p) => p.name === 'vite-plugin-qwik' + ) as any as QwikVitePlugin; if (!qwikVitePlugin) { throw new Error('Missing vite-plugin-qwik'); } diff --git a/packages/qwik-router/src/buildtime/vite/plugin.ts b/packages/qwik-router/src/buildtime/vite/plugin.ts index 4569d74bfc2..3c5e106c388 100644 --- a/packages/qwik-router/src/buildtime/vite/plugin.ts +++ b/packages/qwik-router/src/buildtime/vite/plugin.ts @@ -133,9 +133,9 @@ function qwikRouterPlugin(userOpts?: QwikRouterVitePluginOptions): any { await validatePlugin(ctx.opts); mdxTransform = await createMdxTransformer(ctx); - - const foundPlugin = config.plugins.find((p) => p.name === 'vite-plugin-qwik'); - qwikPlugin = foundPlugin as QwikVitePlugin; + qwikPlugin = config.plugins.find( + (p) => p.name === 'vite-plugin-qwik' + ) as any as QwikVitePlugin; if (!qwikPlugin) { throw new Error('Missing vite-plugin-qwik'); } From bd3925a41b8041baeca6a88d1b2a4aa317217e0b Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Fri, 26 Sep 2025 15:54:45 +0800 Subject: [PATCH 18/20] refactor(router): add double type assertion to avoid TypeScript stack depth error in plugin retrieval --- packages/qwik-router/src/adapters/shared/vite/index.ts | 2 ++ packages/qwik-router/src/buildtime/vite/plugin.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/qwik-router/src/adapters/shared/vite/index.ts b/packages/qwik-router/src/adapters/shared/vite/index.ts index e2ff36a3b70..8debe3c08b0 100644 --- a/packages/qwik-router/src/adapters/shared/vite/index.ts +++ b/packages/qwik-router/src/adapters/shared/vite/index.ts @@ -48,6 +48,8 @@ export function viteAdapter(opts: ViteAdapterPluginOptions) { if (!qwikRouterPlugin) { throw new Error('Missing vite-plugin-qwik-router'); } + // Use double type assertion to avoid TS "Excessive stack depth comparing types" error + // when comparing QwikVitePlugin with Plugin types qwikVitePlugin = config.plugins.find( (p) => p.name === 'vite-plugin-qwik' ) as any as QwikVitePlugin; diff --git a/packages/qwik-router/src/buildtime/vite/plugin.ts b/packages/qwik-router/src/buildtime/vite/plugin.ts index 3c5e106c388..79a23097022 100644 --- a/packages/qwik-router/src/buildtime/vite/plugin.ts +++ b/packages/qwik-router/src/buildtime/vite/plugin.ts @@ -133,6 +133,8 @@ function qwikRouterPlugin(userOpts?: QwikRouterVitePluginOptions): any { await validatePlugin(ctx.opts); mdxTransform = await createMdxTransformer(ctx); + // Use double type assertion to avoid TS "Excessive stack depth comparing types" error + // when comparing QwikVitePlugin with Plugin types qwikPlugin = config.plugins.find( (p) => p.name === 'vite-plugin-qwik' ) as any as QwikVitePlugin; From f815a3279eec6a079cf075faa65a9002cd4e7b51 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Fri, 26 Sep 2025 16:41:12 +0800 Subject: [PATCH 19/20] refactor(router): enhance head resolution logic to support async request store --- packages/qwik-router/src/runtime/src/head.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/qwik-router/src/runtime/src/head.ts b/packages/qwik-router/src/runtime/src/head.ts index fdb00342d15..ec3edf63565 100644 --- a/packages/qwik-router/src/runtime/src/head.ts +++ b/packages/qwik-router/src/runtime/src/head.ts @@ -14,6 +14,7 @@ import type { ContentModuleHead, } from './types'; import { isPromise } from './utils'; +import { asyncRequestStore } from '../../middleware/request-handler/async-request-store'; export const resolveHead = ( endpoint: EndpointResponse | ClientPageData, @@ -53,18 +54,25 @@ export const resolveHead = ( } } if (fns.length) { + const hasAsyncStore = !!asyncRequestStore; const headProps: DocumentHeadProps = { head, - withLocale: (fn) => withLocale(locale, fn), + withLocale: hasAsyncStore ? (fn) => fn() : (fn) => withLocale(locale, fn), resolveValue: getData, ...routeLocation, }; - withLocale(locale, () => { + if (hasAsyncStore) { for (const fn of fns) { resolveDocumentHead(head, fn(headProps)); } - }); + } else { + withLocale(locale, () => { + for (const fn of fns) { + resolveDocumentHead(head, fn(headProps)); + } + }); + } } return head; From a50d819bb324f652ff8573320e16ae1a7250ca31 Mon Sep 17 00:00:00 2001 From: Jerry_Wu <409187100@qq.com> Date: Sun, 28 Sep 2025 15:27:23 +0800 Subject: [PATCH 20/20] refactor(router): replace asyncRequestStore with global qcAsyncRequestStore for improved async handling --- packages/qwik-router/global.d.ts | 2 +- .../request-handler/async-request-store.ts | 22 ------------------- .../request-handler/request-event.ts | 4 ++-- .../request-handler/user-response.ts | 18 ++++++++++++--- packages/qwik-router/src/runtime/src/head.ts | 14 +++--------- .../src/runtime/src/server-functions.ts | 3 +-- 6 files changed, 22 insertions(+), 41 deletions(-) delete mode 100644 packages/qwik-router/src/middleware/request-handler/async-request-store.ts diff --git a/packages/qwik-router/global.d.ts b/packages/qwik-router/global.d.ts index c94b51763b8..cc81dcec280 100644 --- a/packages/qwik-router/global.d.ts +++ b/packages/qwik-router/global.d.ts @@ -5,7 +5,7 @@ type RequestEventInternal = import('./middleware/request-handler/request-event').RequestEventInternal; type AsyncStore = import('node:async_hooks').AsyncLocalStorage; type SerializationStrategy = import('@qwik.dev/core/internal').SerializationStrategy; - +declare var qcAsyncRequestStore: AsyncStore | undefined; declare var _qwikActionsMap: Map | undefined; type ExperimentalFeatures = import('@qwik.dev/core/optimizer').ExperimentalFeatures; diff --git a/packages/qwik-router/src/middleware/request-handler/async-request-store.ts b/packages/qwik-router/src/middleware/request-handler/async-request-store.ts deleted file mode 100644 index bf05e191813..00000000000 --- a/packages/qwik-router/src/middleware/request-handler/async-request-store.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { isServer } from '@qwik.dev/core'; -import type { RequestEventInternal } from './request-event'; -import type { AsyncLocalStorage } from 'node:async_hooks'; - -export type AsyncStore = AsyncLocalStorage; - -if (isServer) { - import('node:async_hooks') - .then((module) => { - const AsyncLocalStorage = module.AsyncLocalStorage; - asyncRequestStore = new AsyncLocalStorage(); - }) - .catch((err) => { - console.warn( - 'AsyncLocalStorage not available, continuing without it. This might impact concurrent server calls.', - err - ); - }); -} - -// Qwik Core will also be using the async store if this is present -export let asyncRequestStore: AsyncStore | undefined = undefined; diff --git a/packages/qwik-router/src/middleware/request-handler/request-event.ts b/packages/qwik-router/src/middleware/request-handler/request-event.ts index 00a79bfd30e..8b36afd4a92 100644 --- a/packages/qwik-router/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-router/src/middleware/request-handler/request-event.ts @@ -20,7 +20,7 @@ import { RewriteMessage, } from '@qwik.dev/router/middleware/request-handler'; import { encoder, getRouteLoaderPromise } from './resolve-request-handlers'; -import { asyncRequestStore } from './async-request-store'; + import type { CacheControl, CacheControlTarget, @@ -81,7 +81,7 @@ export function createRequestEvent( while (routeModuleIndex < requestHandlers.length) { const moduleRequestHandler = requestHandlers[routeModuleIndex]; - const asyncStore = asyncRequestStore; + const asyncStore = globalThis.qcAsyncRequestStore; const result = asyncStore?.run ? asyncStore.run(requestEv, moduleRequestHandler, requestEv) : moduleRequestHandler(requestEv); diff --git a/packages/qwik-router/src/middleware/request-handler/user-response.ts b/packages/qwik-router/src/middleware/request-handler/user-response.ts index 11d031f5529..f43f0e6564e 100644 --- a/packages/qwik-router/src/middleware/request-handler/user-response.ts +++ b/packages/qwik-router/src/middleware/request-handler/user-response.ts @@ -9,7 +9,6 @@ import { getErrorHtml } from './error-handler'; import { createRequestEvent, getRequestMode, type RequestEventInternal } from './request-event'; import { encoder } from './resolve-request-handlers'; import { withLocale } from '@qwik.dev/core'; -import { asyncRequestStore } from './async-request-store'; import type { ServerRequestEvent, StatusCodes } from './types'; // Import separately to avoid duplicate imports in the vite dev server import { @@ -19,6 +18,19 @@ import { ServerError, } from '@qwik.dev/router/middleware/request-handler'; +let asyncStore: AsyncStore | undefined; +import('node:async_hooks') + .then((module) => { + const AsyncLocalStorage = module.AsyncLocalStorage; + asyncStore = new AsyncLocalStorage(); + globalThis.qcAsyncRequestStore = asyncStore; + }) + .catch((err) => { + console.warn( + 'AsyncLocalStorage not available, continuing without it. This might impact concurrent server calls.', + err + ); + }); export interface QwikRouterRun { response: Promise; requestEv: RequestEvent; @@ -47,8 +59,8 @@ export function runQwikRouter( requestEv, completion: withLocale( requestEv.locale(), - asyncRequestStore - ? () => asyncRequestStore!.run(requestEv, runNext, requestEv, rebuildRouteInfo, resolve!) + asyncStore + ? () => asyncStore!.run(requestEv, runNext, requestEv, rebuildRouteInfo, resolve!) : () => runNext(requestEv, rebuildRouteInfo, resolve!) ), }; diff --git a/packages/qwik-router/src/runtime/src/head.ts b/packages/qwik-router/src/runtime/src/head.ts index ec3edf63565..fdb00342d15 100644 --- a/packages/qwik-router/src/runtime/src/head.ts +++ b/packages/qwik-router/src/runtime/src/head.ts @@ -14,7 +14,6 @@ import type { ContentModuleHead, } from './types'; import { isPromise } from './utils'; -import { asyncRequestStore } from '../../middleware/request-handler/async-request-store'; export const resolveHead = ( endpoint: EndpointResponse | ClientPageData, @@ -54,25 +53,18 @@ export const resolveHead = ( } } if (fns.length) { - const hasAsyncStore = !!asyncRequestStore; const headProps: DocumentHeadProps = { head, - withLocale: hasAsyncStore ? (fn) => fn() : (fn) => withLocale(locale, fn), + withLocale: (fn) => withLocale(locale, fn), resolveValue: getData, ...routeLocation, }; - if (hasAsyncStore) { + withLocale(locale, () => { for (const fn of fns) { resolveDocumentHead(head, fn(headProps)); } - } else { - withLocale(locale, () => { - for (const fn of fns) { - resolveDocumentHead(head, fn(headProps)); - } - }); - } + }); } return head; diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index 32cf842d7e5..323486d30a1 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -64,7 +64,6 @@ import type { import { useAction, useLocation, useQwikRouterEnv } from './use-functions'; import type { FormSubmitCompletedDetail } from './form-component'; -import { asyncRequestStore } from '../../middleware/request-handler/async-request-store'; import { deepFreeze } from './deepFreeze'; /** @internal */ @@ -421,7 +420,7 @@ export const serverQrl = ( if (isServer) { // Running during SSR, we can call the function directly - let requestEvent = asyncRequestStore?.getStore() as RequestEvent | undefined; + let requestEvent = globalThis.qcAsyncRequestStore?.getStore() as RequestEvent | undefined; if (!requestEvent) { const contexts = [useQwikRouterEnv()?.ev, this, _getContextEvent()] as RequestEvent[];