From eccebce32b13a2910fa6f5863af1ee996a507c64 Mon Sep 17 00:00:00 2001 From: Varixo Date: Fri, 28 Feb 2025 17:23:56 +0100 Subject: [PATCH 01/12] feat: add QLOADER_KEY constant and implement loadersMiddleware --- package.json | 1 + .../resolve-request-handlers.ts | 126 ++++++++++++------ .../resolve-request-handlers.unit.ts | 3 + .../qwik-router/src/runtime/src/constants.ts | 2 + 4 files changed, 90 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 87efcc892e3..9ca2a288707 100644 --- a/package.json +++ b/package.json @@ -177,6 +177,7 @@ "build.cli": "tsx --require ./scripts/runBefore.ts scripts/index.ts --cli --dev", "build.cli.prod": "tsx --require ./scripts/runBefore.ts scripts/index.ts --cli", "build.core": "tsx --require ./scripts/runBefore.ts scripts/index.ts --tsc --qwik --insights --qwikrouter --api --platform-binding", + "build.router": "tsx --require ./scripts/runBefore.ts scripts/index.ts --tsc --qwikrouter --api", "build.eslint": "tsx --require ./scripts/runBefore.ts scripts/index.ts --eslint", "build.full": "tsx --require ./scripts/runBefore.ts scripts/index.ts --tsc --tsc-docs --qwik --insights --supabaseauthhelpers --api --eslint --qwikrouter --qwikworker --qwikreact --cli --platform-binding --wasm", "build.local": "tsx --require ./scripts/runBefore.ts scripts/index.ts --tsc --tsc-docs --qwik --insights --supabaseauthhelpers --api --eslint --qwikrouter --qwikworker --qwikreact --cli --platform-binding-wasm-copy", diff --git a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts index 75eca7e4dbe..a961d2006dc 100644 --- a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts @@ -1,6 +1,6 @@ import type { QRL } from '@qwik.dev/core'; import type { Render, RenderToStringResult } from '@qwik.dev/core/server'; -import { QACTION_KEY, QFN_KEY } from '../../runtime/src/constants'; +import { QACTION_KEY, QFN_KEY, QLOADER_KEY } from '../../runtime/src/constants'; import type { ActionInternal, ClientPageData, @@ -89,7 +89,8 @@ export const resolveRequestHandlers = ( // Set the current route name ev.sharedMap.set(RequestRouteName, routeName); }); - requestHandlers.push(actionsMiddleware(routeActions, routeLoaders) as any); + requestHandlers.push(actionsMiddleware(routeActions)); + requestHandlers.push(loadersMiddleware(routeLoaders)); requestHandlers.push(renderHandler); } } @@ -167,8 +168,9 @@ export const checkBrand = (obj: any, brand: string) => { return obj && typeof obj === 'function' && obj.__brand === brand; }; -export function actionsMiddleware(routeActions: ActionInternal[], routeLoaders: LoaderInternal[]) { - return async (requestEv: RequestEventInternal) => { +export function actionsMiddleware(routeActions: ActionInternal[]): RequestHandler { + return async (requestEvent: RequestEvent) => { + const requestEv = requestEvent as RequestEventInternal; if (requestEv.headersSent) { requestEv.exit(); return; @@ -218,51 +220,91 @@ export function actionsMiddleware(routeActions: ActionInternal[], routeLoaders: } } } + }; +} +export function loadersMiddleware(routeLoaders: LoaderInternal[]): RequestHandler { + return async (requestEvent: RequestEvent) => { + const requestEv = requestEvent as RequestEventInternal; + if (requestEv.headersSent) { + requestEv.exit(); + return; + } + const loaders = getRequestLoaders(requestEv); + const isDev = getRequestMode(requestEv) === 'dev'; + const qwikSerializer = requestEv[RequestEvQwikSerializer]; if (routeLoaders.length > 0) { - const resolvedLoadersPromises = routeLoaders.map((loader) => { - const loaderId = loader.__id; - loaders[loaderId] = runValidators( - requestEv, - loader.__validators, - undefined, // data - isDev - ) - .then((res) => { - if (res.success) { - if (isDev) { - return measure>( - requestEv, - loader.__qrl.getSymbol().split('_', 1)[0], - () => loader.__qrl.call(requestEv, requestEv) - ); - } else { - return loader.__qrl.call(requestEv, requestEv); - } - } else { - return requestEv.fail(res.status ?? 500, res.error); - } - }) - .then((resolvedLoader) => { - if (typeof resolvedLoader === 'function') { - loaders[loaderId] = resolvedLoader(); - } else { - if (isDev) { - verifySerializable(qwikSerializer, resolvedLoader, loader.__qrl); - } - loaders[loaderId] = resolvedLoader; - } - return resolvedLoader; - }); - - return loaders[loaderId]; - }); + let currentLoaders: LoaderInternal[] = []; + if (requestEv.query.has(QLOADER_KEY)) { + const selectedLoaderIds = requestEv.query.getAll(QLOADER_KEY); + const skippedLoaders: LoaderInternal[] = []; + for (const loader of routeLoaders) { + if (selectedLoaderIds.includes(loader.__id)) { + currentLoaders.push(loader); + } else { + skippedLoaders.push(loader); + } + } + // mark skipped loaders as null + for (const skippedLoader of skippedLoaders) { + loaders[skippedLoader.__id] = null; + } + } else { + currentLoaders = routeLoaders; + } + const resolvedLoadersPromises = currentLoaders.map((loader) => + getRouteLoaderPromise(loader, loaders, requestEv, isDev, qwikSerializer) + ); await Promise.all(resolvedLoadersPromises); } }; } +async function getRouteLoaderPromise( + loader: LoaderInternal, + loaders: Record, + requestEv: RequestEventInternal, + isDev: boolean, + qwikSerializer: QwikSerializer +) { + const loaderId = loader.__id; + loaders[loaderId] = runValidators( + requestEv, + loader.__validators, + undefined, // data + isDev + ) + .then((res) => { + if (res.success) { + if (isDev) { + return measure>( + requestEv, + loader.__qrl.getSymbol().split('_', 1)[0], + () => loader.__qrl.call(requestEv, requestEv) + ); + } else { + return loader.__qrl.call(requestEv, requestEv); + } + } else { + return requestEv.fail(res.status ?? 500, res.error); + } + }) + .then((resolvedLoader) => { + if (typeof resolvedLoader === 'function') { + loaders[loaderId] = resolvedLoader(); + } else { + if (isDev) { + verifySerializable(qwikSerializer, resolvedLoader, loader.__qrl); + } + loaders[loaderId] = resolvedLoader; + } + return resolvedLoader; + }); + + return loaders[loaderId]; +} + async function runValidators( requestEv: RequestEvent, validators: DataValidator[] | undefined, @@ -413,7 +455,7 @@ export function getPathname(url: URL, trailingSlash: boolean | undefined) { } } // strip internal search params - const search = url.search.slice(1).replaceAll(/&?q(action|data|func)=[^&]+/g, ''); + const search = url.search.slice(1).replaceAll(/&?q(action|data|func|loader)=[^&]+/g, ''); return `${url.pathname}${search ? `?${search}` : ''}${url.hash}`; } diff --git a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.unit.ts b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.unit.ts index fd6e49bfaf6..5306ade84ce 100644 --- a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.unit.ts +++ b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.unit.ts @@ -28,6 +28,9 @@ describe('resolve-request-handler', () => { expect(getPathname(new URL('http://server/path?foo=1&qfunc=f&bar=2'), false)).toBe( '/path?foo=1&bar=2' ); + expect(getPathname(new URL('http://server/path?foo=1&qloader=f&bar=2'), false)).toBe( + '/path?foo=1&bar=2' + ); }); }); diff --git a/packages/qwik-router/src/runtime/src/constants.ts b/packages/qwik-router/src/runtime/src/constants.ts index f5d9842f2cc..552a1063396 100644 --- a/packages/qwik-router/src/runtime/src/constants.ts +++ b/packages/qwik-router/src/runtime/src/constants.ts @@ -6,6 +6,8 @@ export const CLIENT_DATA_CACHE = new Map Date: Fri, 28 Feb 2025 17:51:22 +0100 Subject: [PATCH 02/12] fix: replace string literals with constants for server timing and route data properties --- .../src/buildtime/vite/dev-server.ts | 3 +- .../request-handler/request-event.ts | 11 +++++- .../resolve-request-handlers.ts | 37 ++++++++++--------- .../qwik-router/src/runtime/src/routing.ts | 31 +++++++++------- packages/qwik-router/src/runtime/src/types.ts | 28 +++++++++----- packages/qwik-router/src/static/not-found.ts | 5 ++- .../qwik-router/src/static/worker-thread.ts | 3 +- 7 files changed, 72 insertions(+), 46 deletions(-) diff --git a/packages/qwik-router/src/buildtime/vite/dev-server.ts b/packages/qwik-router/src/buildtime/vite/dev-server.ts index cf95cd6c1a7..c9a1b655e09 100644 --- a/packages/qwik-router/src/buildtime/vite/dev-server.ts +++ b/packages/qwik-router/src/buildtime/vite/dev-server.ts @@ -33,6 +33,7 @@ import { getExtension, normalizePath } from '../../utils/fs'; import { updateBuildContext } from '../build'; import type { BuildContext, BuildRoute } from '../types'; import { formatError } from './format-error'; +import { RequestEvShareServerTiming } from '../../middleware/request-handler/request-event'; export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { const matchRouteRequest = (pathname: string) => { @@ -188,7 +189,7 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { res.setHeader('Set-Cookie', cookieHeaders); } - const serverTiming = requestEv.sharedMap.get('@serverTiming') as + const serverTiming = requestEv.sharedMap.get(RequestEvShareServerTiming) as | [string, number][] | undefined; if (serverTiming) { 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 23cc56c821c..a3879120faa 100644 --- a/packages/qwik-router/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-router/src/middleware/request-handler/request-event.ts @@ -36,6 +36,8 @@ export const RequestRouteName = '@routeName'; export const RequestEvSharedActionId = '@actionId'; export const RequestEvSharedActionFormData = '@actionFormData'; export const RequestEvSharedNonce = '@nonce'; +export const RequestEvShareServerTiming = '@serverTiming'; +export const RequestEvShareQData = 'qData'; export function createRequestEvent( serverRequestEv: ServerRequestEvent, @@ -264,9 +266,14 @@ export function createRequestEvent( getWritableStream: () => { if (writableStream === null) { if (serverRequestEv.mode === 'dev') { - const serverTiming = sharedMap.get('@serverTiming') as [string, number][] | undefined; + const serverTiming = sharedMap.get(RequestEvShareServerTiming) as + | [string, number][] + | undefined; if (serverTiming) { - headers.set('Server-Timing', serverTiming.map((a) => `${a[0]};dur=${a[1]}`).join(',')); + headers.set( + 'Server-Timing', + serverTiming.map(([name, duration]) => `${name};dur=${duration}`).join(',') + ); } } writableStream = serverRequestEv.getWritableStream( diff --git a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts index a961d2006dc..0804a794cc7 100644 --- a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts @@ -1,16 +1,17 @@ import type { QRL } from '@qwik.dev/core'; import type { Render, RenderToStringResult } from '@qwik.dev/core/server'; import { QACTION_KEY, QFN_KEY, QLOADER_KEY } from '../../runtime/src/constants'; -import type { - ActionInternal, - ClientPageData, - DataValidator, - JSONObject, - LoadedRoute, - LoaderInternal, - PageModule, - RouteModule, - ValidatorReturn, +import { + type ActionInternal, + type ClientPageData, + type DataValidator, + type JSONObject, + type LoadedRoute, + LoadedRouteProp, + type LoaderInternal, + type PageModule, + type RouteModule, + type ValidatorReturn, } from '../../runtime/src/types'; import { HttpStatus } from './http-status-codes'; import { RedirectMessage } from './redirect-handler'; @@ -22,6 +23,8 @@ import { getRequestMode, getRequestTrailingSlash, type RequestEventInternal, + RequestEvShareServerTiming, + RequestEvShareQData, } from './request-event'; import { getQwikRouterServerData } from './response-page'; import type { @@ -45,7 +48,7 @@ export const resolveRequestHandlers = ( const routeActions: ActionInternal[] = []; const requestHandlers: RequestHandler[] = []; - const isPageRoute = !!(route && isLastModulePageRoute(route[2])); + const isPageRoute = !!(route && isLastModulePageRoute(route[LoadedRouteProp.Mods])); if (serverPlugins) { _resolveRequestHandlers( routeLoaders, @@ -58,7 +61,7 @@ export const resolveRequestHandlers = ( } if (route) { - const routeName = route[0]; + const routeName = route[LoadedRouteProp.RouteName]; if ( checkOrigin && (method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE') @@ -74,7 +77,7 @@ export const resolveRequestHandlers = ( requestHandlers.push(fixTrailingSlash); requestHandlers.push(renderQData); } - const routeModules = route[2]; + const routeModules = route[LoadedRouteProp.Mods]; requestHandlers.push(handleRedirect); _resolveRequestHandlers( routeLoaders, @@ -527,7 +530,7 @@ export function renderQwikMiddleware(render: Render) { // write the already completed html to the stream await stream.write((result as any as RenderToStringResult).html); } - requestEv.sharedMap.set('qData', qData); + requestEv.sharedMap.set(RequestEvShareQData, qData); } finally { await stream.ready; await stream.close(); @@ -601,7 +604,7 @@ export async function renderQData(requestEv: RequestEvent) { // write just the page json data to the response body const data = await qwikSerializer._serialize([qData]); writer.write(encoder.encode(data)); - requestEv.sharedMap.set('qData', qData); + requestEv.sharedMap.set(RequestEvShareQData, qData); writer.close(); } @@ -632,9 +635,9 @@ export async function measure( return await fn(); } finally { const duration = now() - start; - let measurements = requestEv.sharedMap.get('@serverTiming'); + let measurements = requestEv.sharedMap.get(RequestEvShareServerTiming); if (!measurements) { - requestEv.sharedMap.set('@serverTiming', (measurements = [])); + requestEv.sharedMap.set(RequestEvShareServerTiming, (measurements = [])); } measurements.push([name, duration]); } diff --git a/packages/qwik-router/src/runtime/src/routing.ts b/packages/qwik-router/src/runtime/src/routing.ts index 2fbe62ba5e3..0412bb8ed95 100644 --- a/packages/qwik-router/src/runtime/src/routing.ts +++ b/packages/qwik-router/src/runtime/src/routing.ts @@ -1,17 +1,18 @@ import { MODULE_CACHE } from './constants'; import { matchRoute } from './route-matcher'; -import type { - ContentMenu, - LoadedRoute, - MenuData, - MenuModule, - ModuleLoader, - RouteData, - RouteModule, +import { + type ContentMenu, + type LoadedRoute, + type MenuData, + MenuDataProp, + type MenuModule, + type ModuleLoader, + type RouteData, + RouteDataProp, + type RouteModule, } from './types'; import { deepFreeze } from './utils'; -export const CACHE = new Map>(); /** LoadRoute() runs in both client and server. */ export const loadRoute = async ( routes: RouteData[] | undefined, @@ -23,13 +24,13 @@ export const loadRoute = async ( return null; } for (const routeData of routes) { - const routeName = routeData[0]; + const routeName = routeData[RouteDataProp.RouteName]; const params = matchRoute(routeName, pathname); if (!params) { continue; } - const loaders = routeData[1]; - const routeBundleNames = routeData[3]; + const loaders = routeData[RouteDataProp.Loaders]; + const routeBundleNames = routeData[RouteDataProp.RouteBundleNames]; const modules: RouteModule[] = new Array(loaders.length); const pendingLoads: Promise[] = []; @@ -93,10 +94,12 @@ export const getMenuLoader = (menus: MenuData[] | undefined, pathname: string) = if (menus) { pathname = pathname.endsWith('/') ? pathname : pathname + '/'; const menu = menus.find( - (m) => m[0] === pathname || pathname.startsWith(m[0] + (pathname.endsWith('/') ? '' : '/')) + (m) => + m[MenuDataProp.Pathname] === pathname || + pathname.startsWith(m[MenuDataProp.Pathname] + (pathname.endsWith('/') ? '' : '/')) ); if (menu) { - return menu[1]; + return menu[MenuDataProp.MenuLoader]; } } }; diff --git a/packages/qwik-router/src/runtime/src/types.ts b/packages/qwik-router/src/runtime/src/types.ts index 522673f081e..6998b32fb07 100644 --- a/packages/qwik-router/src/runtime/src/types.ts +++ b/packages/qwik-router/src/runtime/src/types.ts @@ -258,9 +258,21 @@ export type RouteData = routeBundleNames: string[], ]; +export const enum RouteDataProp { + RouteName, + Loaders, + OriginalPathname, + RouteBundleNames, +} + /** @public */ export type MenuData = [pathname: string, menuLoader: MenuModuleLoader]; +export const enum MenuDataProp { + Pathname, + MenuLoader, +} + /** * @deprecated Use `QwikRouterConfig` instead. Will be removed in V3. * @public @@ -292,16 +304,14 @@ export type LoadedRoute = [ routeBundleNames: string[] | undefined, ]; -export interface LoadedContent extends LoadedRoute { - pageModule: PageModule; +export const enum LoadedRouteProp { + RouteName, + Params, + Mods, + Menu, + RouteBundleNames, } -export type RequestHandlerBody = BODY | string | number | boolean | undefined | null | void; - -export type RequestHandlerBodyFunction = () => - | RequestHandlerBody - | Promise>; - export interface EndpointResponse { status: number; loaders: Record; @@ -665,8 +675,6 @@ export type LoaderConstructorQRL = { ): Loader>>>; }; -export type LoaderStateHolder = Record>; - /** @public */ export type ActionReturn = { readonly status?: number; diff --git a/packages/qwik-router/src/static/not-found.ts b/packages/qwik-router/src/static/not-found.ts index 4584aeb5e56..d29a7dc2ee1 100644 --- a/packages/qwik-router/src/static/not-found.ts +++ b/packages/qwik-router/src/static/not-found.ts @@ -1,6 +1,7 @@ import type { RouteData } from '@qwik.dev/router'; import { getErrorHtml } from '@qwik.dev/router/middleware/request-handler'; import type { StaticGenerateOptions, System } from './types'; +import { RouteDataProp } from '../runtime/src/types'; export async function generateNotFoundPages( sys: System, @@ -11,7 +12,9 @@ export async function generateNotFoundPages( const basePathname = opts.basePathname || '/'; const rootNotFoundPathname = basePathname + '404.html'; - const hasRootNotFound = routes.some((r) => r[2] === rootNotFoundPathname); + const hasRootNotFound = routes.some( + (r) => r[RouteDataProp.OriginalPathname] === rootNotFoundPathname + ); if (!hasRootNotFound) { const filePath = sys.getRouteFilePath(rootNotFoundPathname, true); diff --git a/packages/qwik-router/src/static/worker-thread.ts b/packages/qwik-router/src/static/worker-thread.ts index 82da1350300..841ce5c2064 100644 --- a/packages/qwik-router/src/static/worker-thread.ts +++ b/packages/qwik-router/src/static/worker-thread.ts @@ -12,6 +12,7 @@ import type { StaticWorkerRenderResult, System, } from './types'; +import { RequestEvShareQData } from '../middleware/request-handler/request-event'; export async function workerThread(sys: System) { const ssgOpts = sys.getOptions(); @@ -178,7 +179,7 @@ async function workerRender( try { if (writeQDataEnabled) { - const qData: ClientPageData = requestEv.sharedMap.get('qData'); + const qData: ClientPageData = requestEv.sharedMap.get(RequestEvShareQData); if (qData && !is404ErrorPage) { // write q-data.json file when enabled and qData is set const qDataFilePath = sys.getDataFilePath(url.pathname); From 6e711f46050a5bb3f9eeacd2c2a06119a8085258 Mon Sep 17 00:00:00 2001 From: Varixo Date: Fri, 28 Feb 2025 19:03:17 +0100 Subject: [PATCH 03/12] feat: add new test place --- .../routes/loaders-serialization/index.tsx | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx diff --git a/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx new file mode 100644 index 00000000000..6ff615b886b --- /dev/null +++ b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx @@ -0,0 +1,25 @@ +import { component$, useSignal } from "@qwik.dev/core"; +import { routeLoader$ } from "@qwik.dev/router"; + +export const useTestLoader = routeLoader$(async () => { + return { test: "test" }; +}); + +export default component$(() => { + const testSignal = useTestLoader(); + const toggle = useSignal(false); + return ( + <> + {testSignal.value.test} + + {toggle.value && } + + ); +}); + +export const Child = component$(() => { + const testSignal = useTestLoader(); + return
{testSignal.value.test}
; +}); From b2c74f188e56f1c9affd5a7ea852883c290bf5ac Mon Sep 17 00:00:00 2001 From: Varixo Date: Sat, 1 Mar 2025 08:17:48 +0100 Subject: [PATCH 04/12] feat: don't use q:seq for routeLoader$ and allow loading client data if missing --- .../src/runtime/src/link-component.tsx | 2 +- .../src/runtime/src/server-functions.ts | 25 +++++++++++++------ packages/qwik/src/core/internal.ts | 1 + packages/qwik/src/core/qwik.core.api.md | 5 ++++ packages/qwik/src/core/use/use-core.ts | 2 +- .../routes/loaders-serialization/index.tsx | 2 +- 6 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/qwik-router/src/runtime/src/link-component.tsx b/packages/qwik-router/src/runtime/src/link-component.tsx index 979528eadf2..28c34dd4546 100644 --- a/packages/qwik-router/src/runtime/src/link-component.tsx +++ b/packages/qwik-router/src/runtime/src/link-component.tsx @@ -61,7 +61,7 @@ export const Link = component$((props) => { }) : undefined; const preventDefault = clientNavPath - ? sync$((event: MouseEvent, target: HTMLAnchorElement) => { + ? sync$((event: MouseEvent) => { if (!(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) { event.preventDefault(); } diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index d065522cd54..a087d76ea83 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -2,10 +2,11 @@ import { $, implicit$FirstArg, noSerialize, - useContext, useStore, type QRL, type ValueOrPromise, + untrack, + isBrowser, } from '@qwik.dev/core'; import { _deserialize, @@ -19,7 +20,7 @@ import * as v from 'valibot'; import { z } from 'zod'; import type { RequestEventLoader } from '../../middleware/request-handler/types'; import { QACTION_KEY, QDATA_KEY, QFN_KEY } from './constants'; -import { RouteStateContext } from './contexts'; +import { RouteLocationContext, RouteStateContext } from './contexts'; import type { ActionConstructor, ActionConstructorQRL, @@ -53,9 +54,11 @@ import type { import { useAction, useLocation, useQwikRouterEnv } from './use-functions'; import { isDev, isServer } from '@qwik.dev/core'; +import { _useInvokeContext } from '@qwik.dev/core/internal'; import type { FormSubmitCompletedDetail } from './form-component'; import { deepFreeze } from './utils'; +import { loadClientData } from './use-endpoint'; /** @internal */ export const routeActionQrl = (( @@ -193,17 +196,23 @@ export const routeLoaderQrl = (( ): LoaderInternal => { const { id, validators } = getValidators(rest, loaderQrl); function loader() { - return useContext(RouteStateContext, (state) => { - if (!(id in state)) { - throw new Error(`routeLoader$ "${loaderQrl.getSymbol()}" was invoked in a route where it was not declared. + const iCtx = _useInvokeContext(); + const state = iCtx.$container$.resolveContext(iCtx.$hostElement$, RouteStateContext)!; + const location = iCtx.$container$.resolveContext(iCtx.$hostElement$, RouteLocationContext)!; + + if (!(id in state)) { + throw new Error(`routeLoader$ "${loaderQrl.getSymbol()}" was invoked in a route where it was not declared. This is because the routeLoader$ was not exported in a 'layout.tsx' or 'index.tsx' file of the existing route. For more information check: https://qwik.dev/docs/route-loader/ If your are managing reusable logic or a library it is essential that this function is re-exported from within 'layout.tsx' or 'index.tsx file of the existing route otherwise it will not run or throw exception. For more information check: https://qwik.dev/docs/re-exporting-loaders/`); - } - return _wrapStore(state, id); - }); + } + const data = untrack(() => state[id]); + if (!data && isBrowser) { + throw loadClientData(location.url, iCtx.$hostElement$); + } + return _wrapStore(state, id); } loader.__brand = 'server_loader' as const; loader.__qrl = loaderQrl; diff --git a/packages/qwik/src/core/internal.ts b/packages/qwik/src/core/internal.ts index 766b94aa3c1..a9326449dd5 100644 --- a/packages/qwik/src/core/internal.ts +++ b/packages/qwik/src/core/internal.ts @@ -51,6 +51,7 @@ export { _getContextElement, _getContextEvent, _jsxBranch, + useInvokeContext as _useInvokeContext, _waitUntilRendered, } from './use/use-core'; export { scheduleTask as _task } from './use/use-task'; diff --git a/packages/qwik/src/core/qwik.core.api.md b/packages/qwik/src/core/qwik.core.api.md index 2df8dbce463..80332e8c770 100644 --- a/packages/qwik/src/core/qwik.core.api.md +++ b/packages/qwik/src/core/qwik.core.api.md @@ -1645,6 +1645,11 @@ export const useErrorBoundary: () => ErrorBoundaryStore; // @public (undocumented) export const useId: () => string; +// Warning: (ae-forgotten-export) The symbol "RenderInvokeContext" needs to be exported by the entry point index.d.ts +// +// @internal (undocumented) +export const _useInvokeContext: () => RenderInvokeContext; + // Warning: (ae-internal-missing-underscore) The name "useLexicalScope" should be prefixed with an underscore because the declaration is marked as @internal // // @internal diff --git a/packages/qwik/src/core/use/use-core.ts b/packages/qwik/src/core/use/use-core.ts index bcb1721ef9e..81fb4cd9a5f 100644 --- a/packages/qwik/src/core/use/use-core.ts +++ b/packages/qwik/src/core/use/use-core.ts @@ -66,7 +66,6 @@ export interface InvokeContext { let _context: InvokeContext | undefined; -/** @public */ export const tryGetInvokeContext = (): InvokeContext | undefined => { if (!_context) { const context = typeof document !== 'undefined' && document && document.__q_context__; @@ -89,6 +88,7 @@ export const getInvokeContext = (): InvokeContext => { return ctx; }; +/** @internal */ export const useInvokeContext = (): RenderInvokeContext => { const ctx = tryGetInvokeContext(); if (!ctx || ctx.$event$ !== RenderEvent) { diff --git a/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx index 6ff615b886b..53a37da632c 100644 --- a/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx +++ b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx @@ -2,7 +2,7 @@ import { component$, useSignal } from "@qwik.dev/core"; import { routeLoader$ } from "@qwik.dev/router"; export const useTestLoader = routeLoader$(async () => { - return { test: "test" }; + return { test: "should not serialize this" }; }); export default component$(() => { From db7cee51c29b6bc6c06f0aeb9d20134e23b7739c Mon Sep 17 00:00:00 2001 From: Varixo Date: Sat, 1 Mar 2025 08:49:34 +0100 Subject: [PATCH 05/12] feat: implement WeakObject serialization --- .../src/runtime/src/qwik-router-component.tsx | 2 +- .../src/runtime/src/server-functions.ts | 5 +++- .../src/core/shared/shared-serialization.ts | 30 ++++++++++++++----- .../core/shared/shared-serialization.unit.ts | 8 ++--- .../src/core/shared/utils/serialize-utils.ts | 10 ++++++- .../routes/loaders-serialization/index.tsx | 2 +- 6 files changed, 41 insertions(+), 16 deletions(-) diff --git a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx index 29f888b564c..fe9e78e013f 100644 --- a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx +++ b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx @@ -146,7 +146,7 @@ export const QwikRouterProvider = component$((props) => { { deep: false } ); const navResolver: { r?: () => void } = {}; - const loaderState = _weakSerialize(useStore(env.response.loaders, { deep: false })); + const loaderState = useStore(_weakSerialize(env.response.loaders), { deep: false }); const routeInternal = useSignal({ type: 'initial', dest: url, diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index a087d76ea83..53c5f2c166e 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -210,7 +210,10 @@ export const routeLoaderQrl = (( } const data = untrack(() => state[id]); if (!data && isBrowser) { - throw loadClientData(location.url, iCtx.$hostElement$); + // TODO: fetch only loader with current id + throw loadClientData(location.url, iCtx.$hostElement$).then( + (data) => (state[id] = data?.loaders[id]) + ); } return _wrapStore(state, id); } diff --git a/packages/qwik/src/core/shared/shared-serialization.ts b/packages/qwik/src/core/shared/shared-serialization.ts index 3909d563550..72b590d8b0f 100644 --- a/packages/qwik/src/core/shared/shared-serialization.ts +++ b/packages/qwik/src/core/shared/shared-serialization.ts @@ -40,7 +40,7 @@ import { isElement, isNode } from './utils/element'; import { EMPTY_ARRAY, EMPTY_OBJ } from './utils/flyweight'; import { ELEMENT_ID } from './utils/markers'; import { isPromise } from './utils/promises'; -import { SerializerSymbol, fastSkipSerialize } from './utils/serialize-utils'; +import { SerializerSymbol, fastSkipSerialize, fastWeakSerialize } from './utils/serialize-utils'; import { _EFFECT_BACK_REF, EffectSubscriptionProp, @@ -387,12 +387,22 @@ const inflate = ( propsProxy[_VAR_PROPS] = data === 0 ? {} : (data as any)[0]; propsProxy[_CONST_PROPS] = (data as any)[1]; break; - case TypeIds.EffectData: { + case TypeIds.SubscriptionData: { const effectData = target as SubscriptionData; effectData.data.$scopedStyleIdPrefix$ = (data as any[])[0]; effectData.data.$isConst$ = (data as any[])[1]; break; } + case TypeIds.WeakObject: { + const objectKeys = data as string[]; + target = Object.fromEntries( + objectKeys.map((v) => + // initialize values with null + [v, null] + ) + ); + break; + } default: throw qError(QError.serializeErrorNotImplemented, [typeId]); } @@ -460,6 +470,7 @@ const allocate = (container: DeserializeContainer, typeId: number, value: unknow case TypeIds.Array: return wrapDeserializerProxy(container as any, value as any[]); case TypeIds.Object: + case TypeIds.WeakObject: return {}; case TypeIds.QRL: case TypeIds.PreloadQRL: @@ -546,9 +557,8 @@ const allocate = (container: DeserializeContainer, typeId: number, value: unknow } else { throw qError(QError.serializeErrorExpectedVNode, [typeof vNode]); } - case TypeIds.EffectData: + case TypeIds.SubscriptionData: return new SubscriptionData({} as NodePropData); - default: throw qError(QError.serializeErrorCannotAllocate, [typeId]); } @@ -848,7 +858,7 @@ const discoverValuesForVNodeData = (vnodeData: VNodeData, callback: (value: unkn if (isSsrAttrs(value)) { for (let i = 1; i < value.length; i += 2) { const attrValue = value[i]; - if (typeof attrValue === 'string') { + if (attrValue == null || typeof attrValue === 'string') { continue; } callback(attrValue); @@ -1065,7 +1075,7 @@ async function serialize(serializationContext: SerializationContext): Promise { it.todo(title(TypeIds.FormData)); it.todo(title(TypeIds.JSXNode)); it.todo(title(TypeIds.PropsProxy)); - it(title(TypeIds.EffectData), async () => { + it(title(TypeIds.SubscriptionData), async () => { expect(await dump(new SubscriptionData({ $isConst$: true, $scopedStyleIdPrefix$: null }))) .toMatchInlineSnapshot(` " @@ -751,7 +751,7 @@ describe('shared-serialization', () => { it.todo(title(TypeIds.ComputedSignal)); it.todo(title(TypeIds.SerializerSignal)); // this requires a domcontainer - it.skip(title(TypeIds.Store), async () => { + it(title(TypeIds.Store), async () => { const objs = await serialize(createStore(null, { a: { b: true } }, StoreFlags.RECURSIVE)); const store = deserialize(objs)[0] as any; expect(store).toHaveProperty('a'); @@ -761,7 +761,7 @@ describe('shared-serialization', () => { it.todo(title(TypeIds.FormData)); it.todo(title(TypeIds.JSXNode)); it.todo(title(TypeIds.PropsProxy)); - it(title(TypeIds.EffectData), async () => { + it(title(TypeIds.SubscriptionData), async () => { const objs = await serialize( new SubscriptionData({ $isConst$: true, $scopedStyleIdPrefix$: null }) ); diff --git a/packages/qwik/src/core/shared/utils/serialize-utils.ts b/packages/qwik/src/core/shared/utils/serialize-utils.ts index dbb309431a1..d54227b98f8 100644 --- a/packages/qwik/src/core/shared/utils/serialize-utils.ts +++ b/packages/qwik/src/core/shared/utils/serialize-utils.ts @@ -145,7 +145,15 @@ export const noSerialize = (input: T): NoSerialize /** @internal */ export const _weakSerialize = (input: T): Partial => { weakSerializeSet.add(input); - return input as any; + if (isObject(input)) { + for (const key in input) { + const value = input[key]; + if (isObject(value)) { + noSerializeSet.add(value); + } + } + } + return input; }; /** diff --git a/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx index 53a37da632c..4a7f035c431 100644 --- a/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx +++ b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx @@ -2,7 +2,7 @@ import { component$, useSignal } from "@qwik.dev/core"; import { routeLoader$ } from "@qwik.dev/router"; export const useTestLoader = routeLoader$(async () => { - return { test: "should not serialize this" }; + return { test: "some test value", notUsed: "should not serialize this" }; }); export default component$(() => { From ee73214e7cb5b776d8ad09dab12592d2e86592c6 Mon Sep 17 00:00:00 2001 From: Varixo Date: Sat, 1 Mar 2025 21:26:01 +0100 Subject: [PATCH 06/12] feat: get data for single loader --- .../resolve-request-handlers.ts | 11 +++++++- .../src/runtime/src/server-functions.ts | 7 +++-- .../src/runtime/src/use-endpoint.ts | 6 ++++- packages/qwik-router/src/runtime/src/utils.ts | 27 ++++++++++++++----- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts index 0804a794cc7..1ea2d59b13e 100644 --- a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts @@ -592,8 +592,17 @@ export async function renderQData(requestEv: RequestEvent) { requestEv.request.headers.forEach((value, key) => (requestHeaders[key] = value)); requestEv.headers.set('Content-Type', 'application/json; charset=utf-8'); + const allLoaders = getRequestLoaders(requestEv); + const loaders: Record = {}; + for (const loaderId in allLoaders) { + const loader = allLoaders[loaderId]; + if (loader) { + loaders[loaderId] = loader; + } + } + const qData: ClientPageData = { - loaders: getRequestLoaders(requestEv), + loaders, action: requestEv.sharedMap.get(RequestEvSharedActionId), status: status !== 200 ? status : 200, href: getPathname(requestEv.url, trailingSlash), diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index 53c5f2c166e..2de9c1a1195 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -210,10 +210,9 @@ export const routeLoaderQrl = (( } const data = untrack(() => state[id]); if (!data && isBrowser) { - // TODO: fetch only loader with current id - throw loadClientData(location.url, iCtx.$hostElement$).then( - (data) => (state[id] = data?.loaders[id]) - ); + throw loadClientData(location.url, iCtx.$hostElement$, { + loaderIds: [id], + }).then((data) => (state[id] = data?.loaders[id])); } return _wrapStore(state, id); } diff --git a/packages/qwik-router/src/runtime/src/use-endpoint.ts b/packages/qwik-router/src/runtime/src/use-endpoint.ts index cd0d26632a7..a429e1b9527 100644 --- a/packages/qwik-router/src/runtime/src/use-endpoint.ts +++ b/packages/qwik-router/src/runtime/src/use-endpoint.ts @@ -9,6 +9,7 @@ export const loadClientData = async ( element: unknown, opts?: { action?: RouteActionValue; + loaderIds?: string[]; clearCache?: boolean; prefetchSymbols?: boolean; isPrefetch?: boolean; @@ -16,7 +17,10 @@ export const loadClientData = async ( ) => { const pagePathname = url.pathname; const pageSearch = url.search; - const clientDataPath = getClientDataPath(pagePathname, pageSearch, opts?.action); + const clientDataPath = getClientDataPath(pagePathname, pageSearch, { + actionId: opts?.action?.id, + loaderIds: opts?.loaderIds, + }); let qData: Promise | undefined; if (!opts?.action) { qData = CLIENT_DATA_CACHE.get(clientDataPath); diff --git a/packages/qwik-router/src/runtime/src/utils.ts b/packages/qwik-router/src/runtime/src/utils.ts index c540b61e136..21f27e82304 100644 --- a/packages/qwik-router/src/runtime/src/utils.ts +++ b/packages/qwik-router/src/runtime/src/utils.ts @@ -1,6 +1,6 @@ -import type { RouteActionValue, SimpleURL } from './types'; +import type { SimpleURL } from './types'; -import { QACTION_KEY } from './constants'; +import { QACTION_KEY, QLOADER_KEY } from './constants'; /** Gets an absolute url path string (url.pathname + url.search + url.hash) */ export const toPath = (url: URL) => url.pathname + url.search + url.hash; @@ -31,13 +31,26 @@ export const isSameOriginDifferentPathname = (a: SimpleURL, b: SimpleURL) => export const getClientDataPath = ( pathname: string, pageSearch?: string, - action?: RouteActionValue + options?: { + actionId?: string; + loaderIds?: string[]; + } ) => { - let search = pageSearch ?? ''; - if (action) { - search += (search ? '&' : '?') + QACTION_KEY + '=' + encodeURIComponent(action.id); + const search = new URLSearchParams(pageSearch); + if (options?.actionId) { + search.set(QACTION_KEY, options.actionId); + } else if (options?.loaderIds) { + for (const id of options.loaderIds) { + search.append(QLOADER_KEY, id); + } } - return pathname + (pathname.endsWith('/') ? '' : '/') + 'q-data.json' + search; + const searchString = search.toString(); + return ( + pathname + + (pathname.endsWith('/') ? '' : '/') + + 'q-data.json' + + (searchString.length ? '?' + searchString : '') + ); }; export const getClientNavPath = (props: Record, baseUrl: { url: URL }) => { From 918c88cf78121168144d222499d561d985cd3338 Mon Sep 17 00:00:00 2001 From: Varixo Date: Sun, 2 Mar 2025 17:32:43 +0100 Subject: [PATCH 07/12] fix: loaders serialization --- .../request-handler/resolve-request-handlers.ts | 3 ++- packages/qwik-router/src/runtime/src/server-functions.ts | 7 +++++-- packages/qwik/src/core/qwik.core.api.md | 3 +++ packages/qwik/src/core/shared/shared-serialization.ts | 9 +++++++-- packages/qwik/src/core/shared/utils/constants.ts | 3 +++ scripts/qwik-router.ts | 2 +- starters/e2e/qwikrouter/nav.e2e.ts | 2 +- 7 files changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts index 1ea2d59b13e..61309842e8d 100644 --- a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts @@ -36,6 +36,7 @@ import type { } from './types'; import { IsQData, QDATA_JSON } from './user-response'; import { ServerError } from './error-handler'; +import { _UNINITIALIZED } from '@qwik.dev/core/internal'; export const resolveRequestHandlers = ( serverPlugins: RouteModule[] | undefined, @@ -596,7 +597,7 @@ export async function renderQData(requestEv: RequestEvent) { const loaders: Record = {}; for (const loaderId in allLoaders) { const loader = allLoaders[loaderId]; - if (loader) { + if (loader !== _UNINITIALIZED) { loaders[loaderId] = loader; } } diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index 2de9c1a1195..ba516cb4e73 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -14,6 +14,7 @@ import { _getContextEvent, _serialize, _wrapStore, + _UNINITIALIZED, } from '@qwik.dev/core/internal'; import * as v from 'valibot'; @@ -209,10 +210,12 @@ export const routeLoaderQrl = (( For more information check: https://qwik.dev/docs/re-exporting-loaders/`); } const data = untrack(() => state[id]); - if (!data && isBrowser) { + if (data === _UNINITIALIZED && isBrowser) { throw loadClientData(location.url, iCtx.$hostElement$, { loaderIds: [id], - }).then((data) => (state[id] = data?.loaders[id])); + }).then((data) => { + state[id] = data?.loaders[id]; + }); } return _wrapStore(state, id); } diff --git a/packages/qwik/src/core/qwik.core.api.md b/packages/qwik/src/core/qwik.core.api.md index 80332e8c770..2821b1f8d15 100644 --- a/packages/qwik/src/core/qwik.core.api.md +++ b/packages/qwik/src/core/qwik.core.api.md @@ -1614,6 +1614,9 @@ export interface Tracker { (obj: T, prop: P): T[P]; } +// @internal (undocumented) +export const _UNINITIALIZED: unique symbol; + // @public export const untrack: (fn: () => T) => T; diff --git a/packages/qwik/src/core/shared/shared-serialization.ts b/packages/qwik/src/core/shared/shared-serialization.ts index 72b590d8b0f..87f09c9395d 100644 --- a/packages/qwik/src/core/shared/shared-serialization.ts +++ b/packages/qwik/src/core/shared/shared-serialization.ts @@ -35,7 +35,7 @@ import { isQrl, isSyncQrl } from './qrl/qrl-utils'; import type { QRL } from './qrl/qrl.public'; import { ChoreType } from './util-chore-type'; import type { DeserializeContainer, HostElement, ObjToProxyMap } from './types'; -import { _CONST_PROPS, _VAR_PROPS } from './utils/constants'; +import { _CONST_PROPS, _UNINITIALIZED, _VAR_PROPS } from './utils/constants'; import { isElement, isNode } from './utils/element'; import { EMPTY_ARRAY, EMPTY_OBJ } from './utils/flyweight'; import { ELEMENT_ID } from './utils/markers'; @@ -398,7 +398,7 @@ const inflate = ( target = Object.fromEntries( objectKeys.map((v) => // initialize values with null - [v, null] + [v, _UNINITIALIZED] ) ); break; @@ -419,6 +419,7 @@ export const _constants = [ EMPTY_OBJ, NEEDS_COMPUTATION, STORE_ALL_PROPS, + _UNINITIALIZED, Slot, Fragment, NaN, @@ -438,6 +439,7 @@ const _constantNames = [ 'EMPTY_OBJ', 'NEEDS_COMPUTATION', 'STORE_ALL_PROPS', + '_UNINITIALIZED', 'Slot', 'Fragment', 'NaN', @@ -1033,6 +1035,8 @@ async function serialize(serializationContext: SerializationContext): Promise { +test.describe("nav", () => { test.describe("mpa", () => { test.use({ javaScriptEnabled: false }); tests(); From 02c9caaff3f91b375591b6ec0724e0678cd700f6 Mon Sep 17 00:00:00 2001 From: Varixo Date: Sat, 29 Mar 2025 19:17:56 +0100 Subject: [PATCH 08/12] feat: use custom serialization --- .../request-handler/request-event.ts | 15 ++++++------ .../resolve-request-handlers.ts | 7 ++++-- .../request-handler/response-page.ts | 22 ++++++++++++++++-- .../qwik-router/src/runtime/src/constants.ts | 2 ++ .../src/runtime/src/qwik-router-component.tsx | 7 +++--- .../src/runtime/src/server-functions.ts | 6 ++--- packages/qwik-router/src/runtime/src/utils.ts | 19 ++++++--------- packages/qwik/src/core/internal.ts | 7 ++---- packages/qwik/src/core/qwik.core.api.md | 3 --- .../src/core/shared/shared-serialization.ts | 17 +------------- .../core/shared/shared-serialization.unit.ts | 23 ++++++++++--------- .../src/core/shared/utils/serialize-utils.ts | 19 --------------- .../routes/loaders-serialization/index.tsx | 9 ++++++-- 13 files changed, 70 insertions(+), 86 deletions(-) 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 a3879120faa..c71f67a97c9 100644 --- a/packages/qwik-router/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-router/src/middleware/request-handler/request-event.ts @@ -1,11 +1,12 @@ import type { ValueOrPromise } from '@qwik.dev/core'; import { QDATA_KEY } from '../../runtime/src/constants'; -import type { - ActionInternal, - FailReturn, - JSONValue, - LoadedRoute, - LoaderInternal, +import { + LoadedRouteProp, + type ActionInternal, + type FailReturn, + type JSONValue, + type LoadedRoute, + type LoaderInternal, } from '../../runtime/src/types'; import { isPromise } from '../../runtime/src/utils'; import { createCacheControl } from './cache-control'; @@ -142,7 +143,7 @@ export function createRequestEvent( env, method: request.method, signal: request.signal, - params: loadedRoute?.[1] ?? {}, + params: loadedRoute?.[LoadedRouteProp.Params] ?? {}, pathname: url.pathname, platform, query: url.searchParams, diff --git a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts index 61309842e8d..0ca9aa2ab29 100644 --- a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts @@ -1,4 +1,5 @@ -import type { QRL } from '@qwik.dev/core'; +import { type QRL } from '@qwik.dev/core'; +import { SerializerSymbol, _UNINITIALIZED } from '@qwik.dev/core/internal'; import type { Render, RenderToStringResult } from '@qwik.dev/core/server'; import { QACTION_KEY, QFN_KEY, QLOADER_KEY } from '../../runtime/src/constants'; import { @@ -36,7 +37,6 @@ import type { } from './types'; import { IsQData, QDATA_JSON } from './user-response'; import { ServerError } from './error-handler'; -import { _UNINITIALIZED } from '@qwik.dev/core/internal'; export const resolveRequestHandlers = ( serverPlugins: RouteModule[] | undefined, @@ -597,6 +597,9 @@ export async function renderQData(requestEv: RequestEvent) { const loaders: Record = {}; for (const loaderId in allLoaders) { const loader = allLoaders[loaderId]; + if (typeof loader === 'object' && loader !== null && SerializerSymbol in loader) { + delete (loader as any)[SerializerSymbol]; + } if (loader !== _UNINITIALIZED) { loaders[loaderId] = loader; } diff --git a/packages/qwik-router/src/middleware/request-handler/response-page.ts b/packages/qwik-router/src/middleware/request-handler/response-page.ts index 7bc79f51a18..dca8f042167 100644 --- a/packages/qwik-router/src/middleware/request-handler/response-page.ts +++ b/packages/qwik-router/src/middleware/request-handler/response-page.ts @@ -1,3 +1,5 @@ +import { SerializerSymbol } from '@qwik.dev/core'; +import { _UNINITIALIZED } from '@qwik.dev/core/internal'; import type { QwikRouterEnvData } from '../../runtime/src/types'; import { getRequestLoaders, @@ -8,6 +10,7 @@ import { RequestRouteName, } from './request-event'; import type { RequestEvent } from './types'; +import { Q_ROUTE } from '../../runtime/src/constants'; export function getQwikRouterServerData(requestEv: RequestEvent) { const { url, params, request, status, locale } = requestEv; @@ -30,13 +33,28 @@ export function getQwikRouterServerData(requestEv: RequestEvent) { reconstructedUrl.protocol = protocol; } + const loaders = getRequestLoaders(requestEv); + + // shallow serialize loaders data + (loaders as any)[SerializerSymbol] = (loaders: Record) => { + const result: Record = {}; + for (const key in loaders) { + const loader = loaders[key]; + if (typeof loader === 'object' && loader !== null) { + (loader as any)[SerializerSymbol] = () => _UNINITIALIZED; + } + result[key] = _UNINITIALIZED; + } + return result; + }; + return { url: reconstructedUrl.href, requestHeaders, locale: locale(), nonce, containerAttributes: { - 'q:route': routeName, + [Q_ROUTE]: routeName, }, qwikrouter: { routeName, @@ -45,7 +63,7 @@ export function getQwikRouterServerData(requestEv: RequestEvent) { loadedRoute: getRequestRoute(requestEv), response: { status: status(), - loaders: getRequestLoaders(requestEv), + loaders, action, formData, }, diff --git a/packages/qwik-router/src/runtime/src/constants.ts b/packages/qwik-router/src/runtime/src/constants.ts index 552a1063396..f7dfedd8ad3 100644 --- a/packages/qwik-router/src/runtime/src/constants.ts +++ b/packages/qwik-router/src/runtime/src/constants.ts @@ -11,3 +11,5 @@ export const QLOADER_KEY = 'qloader'; export const QFN_KEY = 'qfunc'; export const QDATA_KEY = 'qdata'; + +export const Q_ROUTE = 'q:route'; diff --git a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx index fe9e78e013f..e93b03c44fc 100644 --- a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx +++ b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx @@ -20,11 +20,10 @@ import { _getContextElement, _getQContainerElement, _waitUntilRendered, - _weakSerialize, type _ElementVNode, } from '@qwik.dev/core/internal'; import { clientNavigate } from './client-navigate'; -import { CLIENT_DATA_CACHE } from './constants'; +import { CLIENT_DATA_CACHE, Q_ROUTE } from './constants'; import { ContentContext, ContentInternalContext, @@ -146,7 +145,7 @@ export const QwikRouterProvider = component$((props) => { { deep: false } ); const navResolver: { r?: () => void } = {}; - const loaderState = useStore(_weakSerialize(env.response.loaders), { deep: false }); + const loaderState = useStore(env.response.loaders, { deep: false }); const routeInternal = useSignal({ type: 'initial', dest: url, @@ -641,7 +640,7 @@ export const QwikRouterProvider = component$((props) => { clientNavigate(window, navType, prevUrl, trackUrl, replaceState); _waitUntilRendered(elm as Element).then(() => { const container = _getQContainerElement(elm as _ElementVNode)!; - container.setAttribute('q:route', routeName); + container.setAttribute(Q_ROUTE, routeName); const scrollState = currentScrollState(scroller); saveScrollHistory(scrollState); win._qRouterScrollEnabled = true; diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index ba516cb4e73..fd3ba627bf1 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -7,6 +7,8 @@ import { type ValueOrPromise, untrack, isBrowser, + isDev, + isServer, } from '@qwik.dev/core'; import { _deserialize, @@ -14,6 +16,7 @@ import { _getContextEvent, _serialize, _wrapStore, + _useInvokeContext, _UNINITIALIZED, } from '@qwik.dev/core/internal'; @@ -54,9 +57,6 @@ import type { } from './types'; import { useAction, useLocation, useQwikRouterEnv } from './use-functions'; -import { isDev, isServer } from '@qwik.dev/core'; -import { _useInvokeContext } from '@qwik.dev/core/internal'; - import type { FormSubmitCompletedDetail } from './form-component'; import { deepFreeze } from './utils'; import { loadClientData } from './use-endpoint'; diff --git a/packages/qwik-router/src/runtime/src/utils.ts b/packages/qwik-router/src/runtime/src/utils.ts index 21f27e82304..33c727ff875 100644 --- a/packages/qwik-router/src/runtime/src/utils.ts +++ b/packages/qwik-router/src/runtime/src/utils.ts @@ -36,21 +36,16 @@ export const getClientDataPath = ( loaderIds?: string[]; } ) => { - const search = new URLSearchParams(pageSearch); + let search = pageSearch ?? ''; if (options?.actionId) { - search.set(QACTION_KEY, options.actionId); - } else if (options?.loaderIds) { - for (const id of options.loaderIds) { - search.append(QLOADER_KEY, id); + search += (search ? '&' : '?') + QACTION_KEY + '=' + encodeURIComponent(options.actionId); + } + if (options?.loaderIds) { + for (const loaderId of options.loaderIds) { + search += (search ? '&' : '?') + QLOADER_KEY + '=' + encodeURIComponent(loaderId); } } - const searchString = search.toString(); - return ( - pathname + - (pathname.endsWith('/') ? '' : '/') + - 'q-data.json' + - (searchString.length ? '?' + searchString : '') - ); + return pathname + (pathname.endsWith('/') ? '' : '/') + 'q-data.json' + search; }; export const getClientNavPath = (props: Record, baseUrl: { url: URL }) => { diff --git a/packages/qwik/src/core/internal.ts b/packages/qwik/src/core/internal.ts index a9326449dd5..942caf8ed97 100644 --- a/packages/qwik/src/core/internal.ts +++ b/packages/qwik/src/core/internal.ts @@ -39,13 +39,10 @@ export { preprocessState as _preprocessState, _serialize, } from './shared/shared-serialization'; -export { _CONST_PROPS, _IMMUTABLE, _VAR_PROPS } from './shared/utils/constants'; +export { _CONST_PROPS, _IMMUTABLE, _VAR_PROPS, _UNINITIALIZED } from './shared/utils/constants'; export { EMPTY_ARRAY as _EMPTY_ARRAY } from './shared/utils/flyweight'; export { _restProps } from './shared/utils/prop'; -export { - verifySerializable as _verifySerializable, - _weakSerialize, -} from './shared/utils/serialize-utils'; +export { verifySerializable as _verifySerializable } from './shared/utils/serialize-utils'; export { _walkJSX } from './ssr/ssr-render-jsx'; export { _getContextElement, diff --git a/packages/qwik/src/core/qwik.core.api.md b/packages/qwik/src/core/qwik.core.api.md index 2821b1f8d15..1a838cfc549 100644 --- a/packages/qwik/src/core/qwik.core.api.md +++ b/packages/qwik/src/core/qwik.core.api.md @@ -1835,9 +1835,6 @@ export function _walkJSX(ssr: SSRContainer, value: JSXOutput, options: { parentComponentFrame: ISsrComponentFrame | null; }): Promise; -// @internal (undocumented) -export const _weakSerialize: (input: T) => Partial; - // @public export function withLocale(locale: string, fn: () => T): T; diff --git a/packages/qwik/src/core/shared/shared-serialization.ts b/packages/qwik/src/core/shared/shared-serialization.ts index 87f09c9395d..43eb6f89b1f 100644 --- a/packages/qwik/src/core/shared/shared-serialization.ts +++ b/packages/qwik/src/core/shared/shared-serialization.ts @@ -40,7 +40,7 @@ import { isElement, isNode } from './utils/element'; import { EMPTY_ARRAY, EMPTY_OBJ } from './utils/flyweight'; import { ELEMENT_ID } from './utils/markers'; import { isPromise } from './utils/promises'; -import { SerializerSymbol, fastSkipSerialize, fastWeakSerialize } from './utils/serialize-utils'; +import { SerializerSymbol, fastSkipSerialize } from './utils/serialize-utils'; import { _EFFECT_BACK_REF, EffectSubscriptionProp, @@ -393,16 +393,6 @@ const inflate = ( effectData.data.$isConst$ = (data as any[])[1]; break; } - case TypeIds.WeakObject: { - const objectKeys = data as string[]; - target = Object.fromEntries( - objectKeys.map((v) => - // initialize values with null - [v, _UNINITIALIZED] - ) - ); - break; - } default: throw qError(QError.serializeErrorNotImplemented, [typeId]); } @@ -472,7 +462,6 @@ const allocate = (container: DeserializeContainer, typeId: number, value: unknow case TypeIds.Array: return wrapDeserializerProxy(container as any, value as any[]); case TypeIds.Object: - case TypeIds.WeakObject: return {}; case TypeIds.QRL: case TypeIds.PreloadQRL: @@ -1131,8 +1120,6 @@ async function serialize(serializationContext: SerializationContext): Promise { 6 Constant EMPTY_OBJ 7 Constant NEEDS_COMPUTATION 8 Constant STORE_ALL_PROPS - 9 Constant Slot - 10 Constant Fragment - 11 Constant NaN - 12 Constant Infinity - 13 Constant -Infinity - 14 Constant MAX_SAFE_INTEGER - 15 Constant MAX_SAFE_INTEGER-1 - 16 Constant MIN_SAFE_INTEGER - (76 chars)" + 9 Constant _UNINITIALIZED + 10 Constant Slot + 11 Constant Fragment + 12 Constant NaN + 13 Constant Infinity + 14 Constant -Infinity + 15 Constant MAX_SAFE_INTEGER + 16 Constant MAX_SAFE_INTEGER-1 + 17 Constant MIN_SAFE_INTEGER + (81 chars)" `); }); it(title(TypeIds.Number), async () => { @@ -550,7 +551,7 @@ describe('shared-serialization', () => { expect(await dump(new SubscriptionData({ $isConst$: true, $scopedStyleIdPrefix$: null }))) .toMatchInlineSnapshot(` " - 0 EffectData [ + 0 SubscriptionData [ Constant null Constant true ] diff --git a/packages/qwik/src/core/shared/utils/serialize-utils.ts b/packages/qwik/src/core/shared/utils/serialize-utils.ts index d54227b98f8..e47dcd01718 100644 --- a/packages/qwik/src/core/shared/utils/serialize-utils.ts +++ b/packages/qwik/src/core/shared/utils/serialize-utils.ts @@ -90,7 +90,6 @@ const _verifySerializable = ( return value; }; const noSerializeSet = /*#__PURE__*/ new WeakSet(); -const weakSerializeSet = /*#__PURE__*/ new WeakSet(); export const shouldSerialize = (obj: unknown): boolean => { if (isObject(obj) || isFunction(obj)) { @@ -103,10 +102,6 @@ export const fastSkipSerialize = (obj: object): boolean => { return typeof obj === 'object' && obj && (NoSerializeSymbol in obj || noSerializeSet.has(obj)); }; -export const fastWeakSerialize = (obj: object): boolean => { - return weakSerializeSet.has(obj); -}; - /** * Returned type of the `noSerialize()` function. It will be TYPE or undefined. * @@ -142,20 +137,6 @@ export const noSerialize = (input: T): NoSerialize return input as any; }; -/** @internal */ -export const _weakSerialize = (input: T): Partial => { - weakSerializeSet.add(input); - if (isObject(input)) { - for (const key in input) { - const value = input[key]; - if (isObject(value)) { - noSerializeSet.add(value); - } - } - } - return input; -}; - /** * If an object has this property, it will not be serialized. Use this on prototypes to avoid having * to call `noSerialize()` on every object. diff --git a/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx index 4a7f035c431..6c51d241e94 100644 --- a/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx +++ b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx @@ -2,7 +2,7 @@ import { component$, useSignal } from "@qwik.dev/core"; import { routeLoader$ } from "@qwik.dev/router"; export const useTestLoader = routeLoader$(async () => { - return { test: "some test value", notUsed: "should not serialize this" }; + return { test: "some test value", abcd: "should not serialize this" }; }); export default component$(() => { @@ -21,5 +21,10 @@ export default component$(() => { export const Child = component$(() => { const testSignal = useTestLoader(); - return
{testSignal.value.test}
; + return ( + <> +
{testSignal.value.test}
+
{testSignal.value.abcd}
+ + ); }); From 3b9b4f436a5c3050735cfbe4da20bc95501ead66 Mon Sep 17 00:00:00 2001 From: Varixo Date: Thu, 1 May 2025 09:13:41 +0200 Subject: [PATCH 09/12] assign undefined value on loader instead of removing the SerializerSymbol --- .../middleware/request-handler/resolve-request-handlers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts index 0ca9aa2ab29..1ba61d5a203 100644 --- a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts @@ -252,7 +252,7 @@ export function loadersMiddleware(routeLoaders: LoaderInternal[]): RequestHandle // mark skipped loaders as null for (const skippedLoader of skippedLoaders) { - loaders[skippedLoader.__id] = null; + loaders[skippedLoader.__id] = _UNINITIALIZED; } } else { currentLoaders = routeLoaders; @@ -598,7 +598,7 @@ export async function renderQData(requestEv: RequestEvent) { for (const loaderId in allLoaders) { const loader = allLoaders[loaderId]; if (typeof loader === 'object' && loader !== null && SerializerSymbol in loader) { - delete (loader as any)[SerializerSymbol]; + (loader as any)[SerializerSymbol] = undefined; } if (loader !== _UNINITIALIZED) { loaders[loaderId] = loader; From 993c902f74ad1732d3b1d4dd9caebf6bf2a7eec7 Mon Sep 17 00:00:00 2001 From: Varixo Date: Thu, 1 May 2025 12:29:53 +0200 Subject: [PATCH 10/12] use forward refs for shallow objects --- .../src/middleware/request-handler/index.ts | 1 + .../middleware.request-handler.api.md | 485 +++++++++--------- .../request-handler/request-event.ts | 1 + .../resolve-request-handlers.ts | 10 +- .../qwik-router/src/static/worker-thread.ts | 3 +- .../core/reactive-primitives/impl/store.ts | 5 +- .../src/core/reactive-primitives/utils.ts | 5 +- packages/qwik/src/core/shared/error/error.ts | 3 +- .../src/core/shared/shared-serialization.ts | 44 +- .../core/shared/shared-serialization.unit.ts | 78 ++- .../src/core/shared/utils/serialize-utils.ts | 2 +- packages/qwik/src/core/shared/utils/types.ts | 2 +- packages/qwik/src/core/use/use-core.ts | 4 +- 13 files changed, 372 insertions(+), 271 deletions(-) diff --git a/packages/qwik-router/src/middleware/request-handler/index.ts b/packages/qwik-router/src/middleware/request-handler/index.ts index a5d10b3195a..931c4562200 100644 --- a/packages/qwik-router/src/middleware/request-handler/index.ts +++ b/packages/qwik-router/src/middleware/request-handler/index.ts @@ -2,6 +2,7 @@ export { getErrorHtml, ServerError } from './error-handler'; export { mergeHeadersCookies } from './cookie'; export { AbortMessage, RedirectMessage } from './redirect-handler'; export { requestHandler } from './request-handler'; +export { RequestEvShareQData } from './request-event'; export { _TextEncoderStream_polyfill } from './polyfill'; export type { CacheControl, diff --git a/packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md b/packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md index 4198462608e..69fa7f0fd9f 100644 --- a/packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md +++ b/packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md @@ -1,240 +1,245 @@ -## API Report File for "@qwik.dev/router" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import type { Action } from '@qwik.dev/router'; -import type { _deserialize } from '@qwik.dev/core/internal'; -import type { EnvGetter as EnvGetter_2 } from '@qwik.dev/router/middleware/request-handler'; -import type { FailReturn } from '@qwik.dev/router'; -import type { Loader as Loader_2 } from '@qwik.dev/router'; -import type { QwikCityPlan } from '@qwik.dev/router'; -import type { QwikIntrinsicElements } from '@qwik.dev/core'; -import type { QwikRouterConfig } from '@qwik.dev/router'; -import type { Render } from '@qwik.dev/core/server'; -import type { RenderOptions } from '@qwik.dev/core/server'; -import { RequestEvent as RequestEvent_2 } from '@qwik.dev/router/middleware/request-handler'; -import type { RequestHandler as RequestHandler_2 } from '@qwik.dev/router/middleware/request-handler'; -import type { ResolveSyncValue as ResolveSyncValue_2 } from '@qwik.dev/router/middleware/request-handler'; -import type { _serialize } from '@qwik.dev/core/internal'; -import type { ValueOrPromise } from '@qwik.dev/core'; -import type { _verifySerializable } from '@qwik.dev/core/internal'; - -// @public (undocumented) -export class AbortMessage { -} - -// Warning: (ae-forgotten-export) The symbol "CacheControlOptions" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export type CacheControl = CacheControlOptions | number | 'day' | 'week' | 'month' | 'year' | 'no-cache' | 'immutable' | 'private'; - -// @public (undocumented) -export interface ClientConn { - // (undocumented) - country?: string; - // (undocumented) - ip?: string; -} - -// @public (undocumented) -export interface Cookie { - append(name: string, value: string | number | Record, options?: CookieOptions): void; - delete(name: string, options?: Pick): void; - get(name: string): CookieValue | null; - getAll(): Record; - has(name: string): boolean; - headers(): string[]; - set(name: string, value: string | number | Record, options?: CookieOptions): void; -} - -// @public -export interface CookieOptions { - domain?: string; - expires?: Date | string; - httpOnly?: boolean; - maxAge?: number | [number, 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks']; - path?: string; - sameSite?: 'strict' | 'lax' | 'none' | 'Strict' | 'Lax' | 'None' | boolean; - secure?: boolean; -} - -// @public (undocumented) -export interface CookieValue { - // (undocumented) - json: () => T; - // (undocumented) - number: () => number; - // (undocumented) - value: string; -} - -// @public (undocumented) -export type DeferReturn = () => Promise; - -// @public (undocumented) -export interface EnvGetter { - // (undocumented) - get(key: string): string | undefined; -} - -// @public (undocumented) -export function getErrorHtml(status: number, e: any): string; - -// @public (undocumented) -export const mergeHeadersCookies: (headers: Headers, cookies: Cookie) => Headers; - -// @public (undocumented) -export class RedirectMessage extends AbortMessage { -} - -// @public (undocumented) -export interface RequestEvent extends RequestEventCommon { - readonly exited: boolean; - readonly getWritableStream: () => WritableStream; - readonly headersSent: boolean; - readonly next: () => Promise; -} - -// @public (undocumented) -export interface RequestEventAction extends RequestEventCommon { - // (undocumented) - fail: >(status: number, returnData: T) => FailReturn; -} - -// @public (undocumented) -export interface RequestEventBase { - readonly basePathname: string; - // Warning: (ae-forgotten-export) The symbol "CacheControlTarget" needs to be exported by the entry point index.d.ts - readonly cacheControl: (cacheControl: CacheControl, target?: CacheControlTarget) => void; - readonly clientConn: ClientConn; - readonly cookie: Cookie; - readonly env: EnvGetter; - readonly headers: Headers; - readonly method: string; - readonly params: Readonly>; - readonly parseBody: () => Promise; - readonly pathname: string; - readonly platform: PLATFORM; - readonly query: URLSearchParams; - readonly request: Request; - readonly sharedMap: Map; - readonly signal: AbortSignal; - readonly url: URL; -} - -// @public (undocumented) -export interface RequestEventCommon extends RequestEventBase { - // Warning: (ae-forgotten-export) The symbol "ErrorCodes" needs to be exported by the entry point index.d.ts - readonly error: (statusCode: ErrorCodes, message: T) => ServerError; - // (undocumented) - readonly exit: () => AbortMessage; - readonly html: (statusCode: StatusCodes, html: string) => AbortMessage; - readonly json: (statusCode: StatusCodes, data: any) => AbortMessage; - readonly locale: (local?: string) => string; - // Warning: (ae-forgotten-export) The symbol "RedirectCode" needs to be exported by the entry point index.d.ts - readonly redirect: (statusCode: RedirectCode, url: string) => RedirectMessage; - // Warning: (ae-forgotten-export) The symbol "SendMethod" needs to be exported by the entry point index.d.ts - readonly send: SendMethod; - // Warning: (ae-forgotten-export) The symbol "StatusCodes" needs to be exported by the entry point index.d.ts - readonly status: (statusCode?: StatusCodes) => number; - readonly text: (statusCode: StatusCodes, text: string) => AbortMessage; -} - -// @public (undocumented) -export interface RequestEventLoader extends RequestEventAction { - // (undocumented) - defer: (returnData: Promise | (() => Promise)) => DeferReturn; - // (undocumented) - resolveValue: ResolveValue; -} - -// @public (undocumented) -export type RequestHandler = (ev: RequestEvent) => Promise | void; - -// Warning: (ae-forgotten-export) The symbol "QwikSerializer" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "QwikRouterRun" needs to be exported by the entry point index.d.ts -// -// @public -export function requestHandler(serverRequestEv: ServerRequestEvent, opts: ServerRenderOptions, qwikSerializer: QwikSerializer): Promise | null>; - -// @public (undocumented) -export interface ResolveSyncValue { - // (undocumented) - (loader: Loader_2): Awaited extends () => any ? never : Awaited; - // (undocumented) - (action: Action): Awaited | undefined; -} - -// @public (undocumented) -export interface ResolveValue { - // (undocumented) - (loader: Loader_2): Awaited extends () => any ? never : Promise; - // (undocumented) - (action: Action): Promise; -} - -// @public (undocumented) -export class ServerError extends Error { - constructor(status: number, data: T); - // (undocumented) - data: T; - // (undocumented) - status: number; -} - -// @public (undocumented) -export interface ServerRenderOptions extends RenderOptions { - checkOrigin?: boolean; - // @deprecated (undocumented) - qwikCityPlan?: QwikCityPlan; - // (undocumented) - qwikRouterConfig?: QwikRouterConfig; - // (undocumented) - render: Render; -} - -// @public -export interface ServerRequestEvent { - // (undocumented) - env: EnvGetter; - // (undocumented) - getClientConn: () => ClientConn; - // (undocumented) - getWritableStream: ServerResponseHandler; - // (undocumented) - locale: string | undefined; - // (undocumented) - mode: ServerRequestMode; - // (undocumented) - platform: QwikRouterPlatform; - // (undocumented) - request: Request; - // (undocumented) - url: URL; -} - -// @public (undocumented) -export type ServerRequestMode = 'dev' | 'static' | 'server'; - -// Warning: (ae-forgotten-export) The symbol "RequestEventInternal" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export type ServerResponseHandler = (status: number, headers: Headers, cookies: Cookie, resolve: (response: T) => void, requestEv: RequestEventInternal) => WritableStream; - -// @internal (undocumented) -export class _TextEncoderStream_polyfill { - // (undocumented) - get [Symbol.toStringTag](): string; - // (undocumented) - get encoding(): string; - // (undocumented) - get readable(): ReadableStream>; - // (undocumented) - get writable(): WritableStream; -} - -// (No @packageDocumentation comment for this package) - -``` +## API Report File for "@qwik.dev/router" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { Action } from '@qwik.dev/router'; +import type { _deserialize } from '@qwik.dev/core/internal'; +import type { EnvGetter as EnvGetter_2 } from '@qwik.dev/router/middleware/request-handler'; +import type { FailReturn } from '@qwik.dev/router'; +import type { Loader as Loader_2 } from '@qwik.dev/router'; +import type { QwikCityPlan } from '@qwik.dev/router'; +import type { QwikIntrinsicElements } from '@qwik.dev/core'; +import type { QwikRouterConfig } from '@qwik.dev/router'; +import type { Render } from '@qwik.dev/core/server'; +import type { RenderOptions } from '@qwik.dev/core/server'; +import { RequestEvent as RequestEvent_2 } from '@qwik.dev/router/middleware/request-handler'; +import type { RequestHandler as RequestHandler_2 } from '@qwik.dev/router/middleware/request-handler'; +import type { ResolveSyncValue as ResolveSyncValue_2 } from '@qwik.dev/router/middleware/request-handler'; +import type { _serialize } from '@qwik.dev/core/internal'; +import type { ValueOrPromise } from '@qwik.dev/core'; +import type { _verifySerializable } from '@qwik.dev/core/internal'; + +// @public (undocumented) +export class AbortMessage { +} + +// Warning: (ae-forgotten-export) The symbol "CacheControlOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type CacheControl = CacheControlOptions | number | 'day' | 'week' | 'month' | 'year' | 'no-cache' | 'immutable' | 'private'; + +// @public (undocumented) +export interface ClientConn { + // (undocumented) + country?: string; + // (undocumented) + ip?: string; +} + +// @public (undocumented) +export interface Cookie { + append(name: string, value: string | number | Record, options?: CookieOptions): void; + delete(name: string, options?: Pick): void; + get(name: string): CookieValue | null; + getAll(): Record; + has(name: string): boolean; + headers(): string[]; + set(name: string, value: string | number | Record, options?: CookieOptions): void; +} + +// @public +export interface CookieOptions { + domain?: string; + expires?: Date | string; + httpOnly?: boolean; + maxAge?: number | [number, 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks']; + path?: string; + sameSite?: 'strict' | 'lax' | 'none' | 'Strict' | 'Lax' | 'None' | boolean; + secure?: boolean; +} + +// @public (undocumented) +export interface CookieValue { + // (undocumented) + json: () => T; + // (undocumented) + number: () => number; + // (undocumented) + value: string; +} + +// @public (undocumented) +export type DeferReturn = () => Promise; + +// @public (undocumented) +export interface EnvGetter { + // (undocumented) + get(key: string): string | undefined; +} + +// @public (undocumented) +export function getErrorHtml(status: number, e: any): string; + +// @public (undocumented) +export const mergeHeadersCookies: (headers: Headers, cookies: Cookie) => Headers; + +// @public (undocumented) +export class RedirectMessage extends AbortMessage { +} + +// @public (undocumented) +export interface RequestEvent extends RequestEventCommon { + readonly exited: boolean; + readonly getWritableStream: () => WritableStream; + readonly headersSent: boolean; + readonly next: () => Promise; +} + +// @public (undocumented) +export interface RequestEventAction extends RequestEventCommon { + // (undocumented) + fail: >(status: number, returnData: T) => FailReturn; +} + +// @public (undocumented) +export interface RequestEventBase { + readonly basePathname: string; + // Warning: (ae-forgotten-export) The symbol "CacheControlTarget" needs to be exported by the entry point index.d.ts + readonly cacheControl: (cacheControl: CacheControl, target?: CacheControlTarget) => void; + readonly clientConn: ClientConn; + readonly cookie: Cookie; + readonly env: EnvGetter; + readonly headers: Headers; + readonly method: string; + readonly params: Readonly>; + readonly parseBody: () => Promise; + readonly pathname: string; + readonly platform: PLATFORM; + readonly query: URLSearchParams; + readonly request: Request; + readonly sharedMap: Map; + readonly signal: AbortSignal; + readonly url: URL; +} + +// @public (undocumented) +export interface RequestEventCommon extends RequestEventBase { + // Warning: (ae-forgotten-export) The symbol "ErrorCodes" needs to be exported by the entry point index.d.ts + readonly error: (statusCode: ErrorCodes, message: T) => ServerError; + // (undocumented) + readonly exit: () => AbortMessage; + readonly html: (statusCode: StatusCodes, html: string) => AbortMessage; + readonly json: (statusCode: StatusCodes, data: any) => AbortMessage; + readonly locale: (local?: string) => string; + // Warning: (ae-forgotten-export) The symbol "RedirectCode" needs to be exported by the entry point index.d.ts + readonly redirect: (statusCode: RedirectCode, url: string) => RedirectMessage; + // Warning: (ae-forgotten-export) The symbol "SendMethod" needs to be exported by the entry point index.d.ts + readonly send: SendMethod; + // Warning: (ae-forgotten-export) The symbol "StatusCodes" needs to be exported by the entry point index.d.ts + readonly status: (statusCode?: StatusCodes) => number; + readonly text: (statusCode: StatusCodes, text: string) => AbortMessage; +} + +// @public (undocumented) +export interface RequestEventLoader extends RequestEventAction { + // (undocumented) + defer: (returnData: Promise | (() => Promise)) => DeferReturn; + // (undocumented) + resolveValue: ResolveValue; +} + +// Warning: (ae-internal-missing-underscore) The name "RequestEvShareQData" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const RequestEvShareQData = "qData"; + +// @public (undocumented) +export type RequestHandler = (ev: RequestEvent) => Promise | void; + +// Warning: (ae-forgotten-export) The symbol "QwikSerializer" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "QwikRouterRun" needs to be exported by the entry point index.d.ts +// +// @public +export function requestHandler(serverRequestEv: ServerRequestEvent, opts: ServerRenderOptions, qwikSerializer: QwikSerializer): Promise | null>; + +// @public (undocumented) +export interface ResolveSyncValue { + // (undocumented) + (loader: Loader_2): Awaited extends () => any ? never : Awaited; + // (undocumented) + (action: Action): Awaited | undefined; +} + +// @public (undocumented) +export interface ResolveValue { + // (undocumented) + (loader: Loader_2): Awaited extends () => any ? never : Promise; + // (undocumented) + (action: Action): Promise; +} + +// @public (undocumented) +export class ServerError extends Error { + constructor(status: number, data: T); + // (undocumented) + data: T; + // (undocumented) + status: number; +} + +// @public (undocumented) +export interface ServerRenderOptions extends RenderOptions { + checkOrigin?: boolean; + // @deprecated (undocumented) + qwikCityPlan?: QwikCityPlan; + // (undocumented) + qwikRouterConfig?: QwikRouterConfig; + // (undocumented) + render: Render; +} + +// @public +export interface ServerRequestEvent { + // (undocumented) + env: EnvGetter; + // (undocumented) + getClientConn: () => ClientConn; + // (undocumented) + getWritableStream: ServerResponseHandler; + // (undocumented) + locale: string | undefined; + // (undocumented) + mode: ServerRequestMode; + // (undocumented) + platform: QwikRouterPlatform; + // (undocumented) + request: Request; + // (undocumented) + url: URL; +} + +// @public (undocumented) +export type ServerRequestMode = 'dev' | 'static' | 'server'; + +// Warning: (ae-forgotten-export) The symbol "RequestEventInternal" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type ServerResponseHandler = (status: number, headers: Headers, cookies: Cookie, resolve: (response: T) => void, requestEv: RequestEventInternal) => WritableStream; + +// @internal (undocumented) +export class _TextEncoderStream_polyfill { + // (undocumented) + get [Symbol.toStringTag](): string; + // (undocumented) + get encoding(): string; + // (undocumented) + get readable(): ReadableStream>; + // (undocumented) + get writable(): WritableStream; +} + +// (No @packageDocumentation comment for this package) + +``` 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 c71f67a97c9..086a37cc417 100644 --- a/packages/qwik-router/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-router/src/middleware/request-handler/request-event.ts @@ -38,6 +38,7 @@ export const RequestEvSharedActionId = '@actionId'; export const RequestEvSharedActionFormData = '@actionFormData'; export const RequestEvSharedNonce = '@nonce'; export const RequestEvShareServerTiming = '@serverTiming'; +/** @internal */ export const RequestEvShareQData = 'qData'; export function createRequestEvent( diff --git a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts index 1ba61d5a203..0a9e5a7168a 100644 --- a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts @@ -252,7 +252,7 @@ export function loadersMiddleware(routeLoaders: LoaderInternal[]): RequestHandle // mark skipped loaders as null for (const skippedLoader of skippedLoaders) { - loaders[skippedLoader.__id] = _UNINITIALIZED; + loaders[skippedLoader.__id] = null; } } else { currentLoaders = routeLoaders; @@ -597,10 +597,10 @@ export async function renderQData(requestEv: RequestEvent) { const loaders: Record = {}; for (const loaderId in allLoaders) { const loader = allLoaders[loaderId]; - if (typeof loader === 'object' && loader !== null && SerializerSymbol in loader) { - (loader as any)[SerializerSymbol] = undefined; - } - if (loader !== _UNINITIALIZED) { + if (loader !== null) { + if (typeof loader === 'object' && SerializerSymbol in loader) { + (loader as any)[SerializerSymbol] = undefined; + } loaders[loaderId] = loader; } } diff --git a/packages/qwik-router/src/static/worker-thread.ts b/packages/qwik-router/src/static/worker-thread.ts index 841ce5c2064..497ea2dfb53 100644 --- a/packages/qwik-router/src/static/worker-thread.ts +++ b/packages/qwik-router/src/static/worker-thread.ts @@ -1,6 +1,6 @@ import { _deserialize, _serialize, _verifySerializable } from '@qwik.dev/core/internal'; import type { ServerRequestEvent } from '@qwik.dev/router/middleware/request-handler'; -import { requestHandler } from '@qwik.dev/router/middleware/request-handler'; +import { requestHandler, RequestEvShareQData } from '@qwik.dev/router/middleware/request-handler'; import { WritableStream } from 'node:stream/web'; import { pathToFileURL } from 'node:url'; import type { QwikSerializer } from '../middleware/request-handler/types'; @@ -12,7 +12,6 @@ import type { StaticWorkerRenderResult, System, } from './types'; -import { RequestEvShareQData } from '../middleware/request-handler/request-event'; export async function workerThread(sys: System) { const ssgOpts = sys.getOptions(); diff --git a/packages/qwik/src/core/reactive-primitives/impl/store.ts b/packages/qwik/src/core/reactive-primitives/impl/store.ts index f5e7202ee38..6b5dbaf64bd 100644 --- a/packages/qwik/src/core/reactive-primitives/impl/store.ts +++ b/packages/qwik/src/core/reactive-primitives/impl/store.ts @@ -1,7 +1,7 @@ import { pad, qwikDebugToString } from '../../debug'; import { assertTrue } from '../../shared/error/assert'; import { tryGetInvokeContext } from '../../use/use-core'; -import { isSerializableObject } from '../../shared/utils/types'; +import { isObject, isSerializableObject } from '../../shared/utils/types'; import type { Container } from '../../shared/types'; import { addQrlToSerializationCtx, @@ -124,8 +124,7 @@ export class StoreHandler implements ProxyHandler { const flags = this.$flags$; if ( flags & StoreFlags.RECURSIVE && - typeof value === 'object' && - value !== null && + isObject(value) && !Object.isFrozen(value) && !isStore(value) && !Object.isFrozen(target) diff --git a/packages/qwik/src/core/reactive-primitives/utils.ts b/packages/qwik/src/core/reactive-primitives/utils.ts index fe898ce337c..b93dd0c2f5a 100644 --- a/packages/qwik/src/core/reactive-primitives/utils.ts +++ b/packages/qwik/src/core/reactive-primitives/utils.ts @@ -9,6 +9,7 @@ import type { Container, HostElement } from '../shared/types'; import { ChoreType } from '../shared/util-chore-type'; import { ELEMENT_PROPS, OnRenderProp } from '../shared/utils/markers'; import { SerializerSymbol } from '../shared/utils/serialize-utils'; +import { isObject } from '../shared/utils/types'; import type { ISsrNode, SSRContainer } from '../ssr/ssr-types'; import { TaskFlags, isTask } from '../use/use-task'; import { ComputedSignalImpl } from './impl/computed-signal-impl'; @@ -147,7 +148,5 @@ export const triggerEffects = ( export const isSerializerObj = any }, S>( obj: unknown ): obj is CustomSerializable => { - return ( - typeof obj === 'object' && obj !== null && typeof (obj as any)[SerializerSymbol] === 'function' - ); + return isObject(obj) && typeof (obj as any)[SerializerSymbol] === 'function'; }; diff --git a/packages/qwik/src/core/shared/error/error.ts b/packages/qwik/src/core/shared/error/error.ts index 52386099afa..5f8eb848ff4 100644 --- a/packages/qwik/src/core/shared/error/error.ts +++ b/packages/qwik/src/core/shared/error/error.ts @@ -1,5 +1,6 @@ import { logErrorAndStop } from '../utils/log'; import { qDev } from '../utils/qdev'; +import { isObject } from '../utils/types'; export const codeToText = (code: number, ...parts: any[]): string => { if (qDev) { @@ -61,7 +62,7 @@ export const codeToText = (code: number, ...parts: any[]): string => { if (parts.length) { text = text.replaceAll(/{{(\d+)}}/g, (_, index) => { let v = parts[index]; - if (v && typeof v === 'object' && v.constructor === Object) { + if (v && isObject(v) && v.constructor === Object) { v = JSON.stringify(v).slice(0, 50); } return v; diff --git a/packages/qwik/src/core/shared/shared-serialization.ts b/packages/qwik/src/core/shared/shared-serialization.ts index 43eb6f89b1f..241f8d0cb89 100644 --- a/packages/qwik/src/core/shared/shared-serialization.ts +++ b/packages/qwik/src/core/shared/shared-serialization.ts @@ -56,13 +56,14 @@ import { SignalImpl } from '../reactive-primitives/impl/signal-impl'; import { ComputedSignalImpl } from '../reactive-primitives/impl/computed-signal-impl'; import { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl'; import { SerializerSignalImpl } from '../reactive-primitives/impl/serializer-signal-impl'; +import { isObject } from './utils/types'; const deserializedProxyMap = new WeakMap(); type DeserializerProxy = T & { [SERIALIZER_PROXY_UNWRAP]: object }; export const isDeserializerProxy = (value: unknown): value is DeserializerProxy => { - return typeof value === 'object' && value !== null && SERIALIZER_PROXY_UNWRAP in value; + return isObject(value) && SERIALIZER_PROXY_UNWRAP in value; }; export const SERIALIZER_PROXY_UNWRAP = Symbol('UNWRAP'); @@ -887,6 +888,7 @@ async function serialize(serializationContext: SerializationContext): Promise> = new Set(); const preloadQrls = new Set(); + const uninitializedRefs = new Map(); let parent: unknown = null; const isRootObject = () => depth === 0; @@ -960,7 +962,7 @@ async function serialize(serializationContext: SerializationContext): Promise { - const qrl = qrlToString(serializationContext, value); + const qrl = qrlToString(serializationContext, value, uninitializedRefs); const type = preloadQrls.has(value) ? TypeIds.PreloadQRL : TypeIds.QRL; if (isRootObject()) { output(type, qrl); @@ -1112,6 +1114,10 @@ async function serialize(serializationContext: SerializationContext): Promise ) { let symbol = value.$symbol$; let chunk = value.$chunk$; @@ -1464,8 +1479,17 @@ export function qrlToString( if (i > 0) { serializedReferences += ' '; } + const captureRef = value.$captureRef$[i]; + if ( + isObject(captureRef) && + uninitializedRefs && + uninitializedRefs.has(captureRef) && + SerializerSymbol in captureRef + ) { + captureRef[SerializerSymbol] = undefined; + } // We refer by id so every capture needs to be a root - serializedReferences += serializationContext.$addRoot$(value.$captureRef$[i]); + serializedReferences += serializationContext.$addRoot$(captureRef); } qrlStringInline += `[${serializedReferences}]`; } else if (value.$capture$ && value.$capture$.length > 0) { @@ -1693,7 +1717,7 @@ function shouldTrackObj(obj: unknown) { return ( // THINK: Not sure if we need to keep track of functions (QRLs) Let's skip them for now. // and see if we have a test case which requires them. - (typeof obj === 'object' && obj !== null) || + isObject(obj) || /** * We track all strings greater than 1 character, because those take at least 6 bytes to encode * and even with 999 root objects it saves one byte per reference. Tracking more objects makes @@ -1726,9 +1750,7 @@ function isResource(value: object): value is ResourceReturnInternal const frameworkType = (obj: any) => { return ( - (typeof obj === 'object' && - obj !== null && - (obj instanceof SignalImpl || obj instanceof Task || isJSXNode(obj))) || + (isObject(obj) && (obj instanceof SignalImpl || obj instanceof Task || isJSXNode(obj))) || isQrl(obj) ); }; @@ -1914,8 +1936,8 @@ const circularProofJson = (obj: unknown, indent?: string | number) => { const seen = new WeakSet(); return JSON.stringify( obj, - (key, value) => { - if (typeof value === 'object' && value !== null) { + (_, value) => { + if (isObject(value)) { if (seen.has(value)) { return `[Circular ${value.constructor.name}]`; } @@ -1960,7 +1982,7 @@ export const dumpState = ( if (key === undefined) { hasRaw = true; out.push( - `${RED}[raw${typeof value === 'object' && value ? ` ${value.constructor.name}` : ''}]${RESET} ${printRaw(value, `${prefix} `)}` + `${RED}[raw${isObject(value) ? ` ${value.constructor.name}` : ''}]${RESET} ${printRaw(value, `${prefix} `)}` ); } else { if (key === TypeIds.Constant) { diff --git a/packages/qwik/src/core/shared/shared-serialization.unit.ts b/packages/qwik/src/core/shared/shared-serialization.unit.ts index 450a625536b..47c5aff9c75 100644 --- a/packages/qwik/src/core/shared/shared-serialization.unit.ts +++ b/packages/qwik/src/core/shared/shared-serialization.unit.ts @@ -1,6 +1,6 @@ import { $, componentQrl, noSerialize } from '@qwik.dev/core'; import { describe, expect, it, vi } from 'vitest'; -import { _fnSignal, _wrapProp } from '../internal'; +import { _fnSignal, _UNINITIALIZED, _wrapProp } from '../internal'; import { type SignalImpl } from '../reactive-primitives/impl/signal-impl'; import { createComputedQrl, @@ -25,7 +25,7 @@ import { EMPTY_ARRAY, EMPTY_OBJ } from './utils/flyweight'; import { isQrl } from './qrl/qrl-utils'; import { NoSerializeSymbol, SerializerSymbol } from './utils/serialize-utils'; import { SubscriptionData } from '../reactive-primitives/subscription-data'; -import { StoreFlags } from '../reactive-primitives/types'; +import { StoreFlags, type CustomSerializable } from '../reactive-primitives/types'; const DEBUG = false; @@ -77,6 +77,47 @@ describe('shared-serialization', () => { (81 chars)" `); }); + describe('UNINITIALIZED', () => { + it(title(TypeIds.Constant) + ' - UNINITIALIZED, not serialized object', async () => { + const uninitializedObject = { + shouldNot: 'serialize', + }; + (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { + return _UNINITIALIZED; + }; + expect(await dump(uninitializedObject)).toMatchInlineSnapshot(` + " + 0 ForwardRef 0 + 1 Constant _UNINITIALIZED + 2 ForwardRefs [ + 1 + ] + (15 chars)" + `); + }); + it(title(TypeIds.Constant) + ' - UNINITIALIZED, serialized object', async () => { + const uninitializedObject = { + should: 'serialize', + }; + (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { + return _UNINITIALIZED; + }; + const qrl = inlinedQrl(() => uninitializedObject.should, 'dump_qrl', [uninitializedObject]); + expect(await dump(uninitializedObject, qrl)).toMatchInlineSnapshot(` + " + 0 ForwardRef 0 + 1 QRL "mock-chunk#dump_qrl[0]" + 2 Object [ + String "should" + String "serialize" + ] + 3 ForwardRefs [ + 2 + ] + (69 chars)" + `); + }); + }); it(title(TypeIds.Number), async () => { expect(await dump(123)).toMatchInlineSnapshot(` " @@ -770,6 +811,39 @@ describe('shared-serialization', () => { expect(effect).toBeInstanceOf(SubscriptionData); expect(effect.data).toEqual({ $isConst$: true, $scopedStyleIdPrefix$: null }); }); + + describe('UNINITIALIZED', () => { + it(title(TypeIds.Constant) + ' - UNINITIALIZED, not serialized object', async () => { + const uninitializedObject = { + shouldNot: 'serialize', + }; + (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { + return _UNINITIALIZED; + }; + + const objs = await serialize(uninitializedObject); + const effect = deserialize(objs)[0] as any; + expect(effect).toBe(_UNINITIALIZED); + }); + it(title(TypeIds.Constant) + ' - UNINITIALIZED, serialized object', async () => { + const uninitializedObject = { + should: 'serialize', + }; + (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { + return _UNINITIALIZED; + }; + const qrl = inlinedQrl(() => uninitializedObject.should, 'dump_qrl', [uninitializedObject]); + const objs = await serialize(uninitializedObject, qrl); + const state = deserialize(objs); + delete (uninitializedObject as any)[SerializerSymbol]; + const deserializedObject = state[0]; + expect(deserializedObject).toEqual(uninitializedObject); + + const deserializedQrl = state[1] as QRLInternal; + expect(isQrl(deserializedQrl)).toBeTruthy(); + expect(await (deserializedQrl.getFn() as any)()).toBe(uninitializedObject.should); + }); + }); }); describe('special cases', () => { diff --git a/packages/qwik/src/core/shared/utils/serialize-utils.ts b/packages/qwik/src/core/shared/utils/serialize-utils.ts index e47dcd01718..621a25b8d4a 100644 --- a/packages/qwik/src/core/shared/utils/serialize-utils.ts +++ b/packages/qwik/src/core/shared/utils/serialize-utils.ts @@ -99,7 +99,7 @@ export const shouldSerialize = (obj: unknown): boolean => { }; export const fastSkipSerialize = (obj: object): boolean => { - return typeof obj === 'object' && obj && (NoSerializeSymbol in obj || noSerializeSet.has(obj)); + return obj && isObject(obj) && (NoSerializeSymbol in obj || noSerializeSet.has(obj)); }; /** diff --git a/packages/qwik/src/core/shared/utils/types.ts b/packages/qwik/src/core/shared/utils/types.ts index 20567d85822..c3114840e9a 100644 --- a/packages/qwik/src/core/shared/utils/types.ts +++ b/packages/qwik/src/core/shared/utils/types.ts @@ -9,7 +9,7 @@ export const isSerializableObject = (v: unknown): v is Record = }; export const isObject = (v: unknown): v is object => { - return !!v && typeof v === 'object'; + return typeof v === 'object' && v !== null; }; export const isArray = (v: unknown): v is unknown[] => { diff --git a/packages/qwik/src/core/use/use-core.ts b/packages/qwik/src/core/use/use-core.ts index 81fb4cd9a5f..bba4d8af006 100644 --- a/packages/qwik/src/core/use/use-core.ts +++ b/packages/qwik/src/core/use/use-core.ts @@ -5,7 +5,7 @@ import type { QRLInternal } from '../shared/qrl/qrl-class'; import type { QRL } from '../shared/qrl/qrl.public'; import { ComputedEvent, RenderEvent, ResourceEvent, TaskEvent } from '../shared/utils/markers'; import { seal } from '../shared/utils/qdev'; -import { isArray } from '../shared/utils/types'; +import { isArray, isObject } from '../shared/utils/types'; import { setLocale } from './use-locale'; import type { Container, HostElement } from '../shared/types'; import { vnode_getNode, vnode_isElementVNode, vnode_isVNode, vnode_locate } from '../client/vnode'; @@ -159,7 +159,7 @@ export const newInvokeContext = ( ): InvokeContext => { // ServerRequestEvent has .locale, but it's not always defined. const $locale$ = - locale || (typeof event === 'object' && event && 'locale' in event ? event.locale : undefined); + locale || (event && isObject(event) && 'locale' in event ? event.locale : undefined); const ctx: InvokeContext = { $url$: url, $i$: 0, From 564057cd93173fed69f0c9092d44cef9c6da821b Mon Sep 17 00:00:00 2001 From: Varixo Date: Fri, 2 May 2025 14:42:51 +0200 Subject: [PATCH 11/12] feat: serialize UNINITIALIZED if is inside capture ref --- .../request-handler/response-page.ts | 15 +- .../src/core/shared/shared-serialization.ts | 267 +++++++++++----- .../core/shared/shared-serialization.unit.ts | 300 +++++++++++++++++- 3 files changed, 490 insertions(+), 92 deletions(-) diff --git a/packages/qwik-router/src/middleware/request-handler/response-page.ts b/packages/qwik-router/src/middleware/request-handler/response-page.ts index dca8f042167..74eef361e4f 100644 --- a/packages/qwik-router/src/middleware/request-handler/response-page.ts +++ b/packages/qwik-router/src/middleware/request-handler/response-page.ts @@ -36,17 +36,12 @@ export function getQwikRouterServerData(requestEv: RequestEvent) { const loaders = getRequestLoaders(requestEv); // shallow serialize loaders data - (loaders as any)[SerializerSymbol] = (loaders: Record) => { - const result: Record = {}; - for (const key in loaders) { - const loader = loaders[key]; - if (typeof loader === 'object' && loader !== null) { - (loader as any)[SerializerSymbol] = () => _UNINITIALIZED; - } - result[key] = _UNINITIALIZED; + for (const key in loaders) { + const loader = loaders[key]; + if (typeof loader === 'object' && loader !== null) { + (loader as any)[SerializerSymbol] = () => _UNINITIALIZED; } - return result; - }; + } return { url: reconstructedUrl.href, diff --git a/packages/qwik/src/core/shared/shared-serialization.ts b/packages/qwik/src/core/shared/shared-serialization.ts index 241f8d0cb89..29c02dd38b0 100644 --- a/packages/qwik/src/core/shared/shared-serialization.ts +++ b/packages/qwik/src/core/shared/shared-serialization.ts @@ -615,8 +615,13 @@ type DomRef = { }; type SeenRef = { - $parent$: unknown | null; + // Parent used for RootRef path calculation (null for roots) + $refParent$: unknown | null; + // The actual parent in the object graph, roots can also have a structural parent + $structuralParents$: (unknown | null)[]; + // Index within $structuralParent$ $index$: number; + // Index in the roots array (-1 if not a root) $rootIndex$: number; }; @@ -651,20 +656,13 @@ export interface SerializationContext { */ $addRoot$: (obj: unknown, parent?: unknown) => number; - /** - * Get root path of the object without creating a new root. - * - * This is used during serialization, as new roots can't be created during serialization. - * - * The function throws if the root was not found. - */ - $addRootPath$: (obj: any) => string | number; + $getOwningSerializationRoots$: (obj: unknown) => unknown[]; $seen$: (obj: unknown, parent: unknown | null, index: number) => void; $roots$: unknown[]; - $pathMap$: Map; - + $objectPathStringCache$: Map; + $outputRootRefs$: boolean; $addSyncFn$($funcStr$: string | null, argsCount: number, fn: Function): number; $isSsrNode$: (obj: unknown) => obj is SsrNode; @@ -714,39 +712,90 @@ export const createSerializationContext = ( } as StreamWriter; } const seenObjsMap = new Map(); - const rootsPathMap = new Map(); + const objectPathStringCache = new Map(); const syncFnMap = new Map(); const syncFns: string[] = []; const roots: unknown[] = []; const $wasSeen$ = (obj: unknown) => seenObjsMap.get(obj); const $seen$ = (obj: unknown, parent: unknown | null, index: number) => { - return seenObjsMap.set(obj, { $parent$: parent, $index$: index, $rootIndex$: -1 }); + return seenObjsMap.set(obj, { + $refParent$: parent, + $structuralParents$: parent ? [parent] : [], // Initialize as array + $index$: index, + $rootIndex$: -1, + }); }; - const $addRootPath$ = (obj: unknown) => { - const rootPath = rootsPathMap.get(obj); - if (rootPath) { - return rootPath; - } - const seen = seenObjsMap.get(obj); - if (!seen) { - throw qError(QError.serializeErrorMissingRootId, [obj]); + const $getOwningSerializationRoots$ = (obj: unknown): unknown[] => { + const rootsFound: unknown[] = []; + const queue: unknown[] = [obj]; + const visited = new Set(); + + while (queue.length > 0) { + const currentObj = queue.shift(); + if (!currentObj || visited.has(currentObj)) { + continue; + } + visited.add(currentObj); + + const seen = seenObjsMap.get(currentObj); + + if (!seen) { + continue; + } + + // Only add actual root objects (those added via $addRoot$ with no parent or seen before $addRoot$) + // Ensure it's actually in the roots array by checking $rootIndex$ != -1 + const rootPath = objectPathStringCache.get(currentObj); + if ((seen.$rootIndex$ !== -1 || rootPath) && !rootsFound.includes(currentObj)) { + rootsFound.push(currentObj); + } + + // Add its structural parent to the queue to continue traversal + if (seen.$structuralParents$) { + for (const structuralParent of seen.$structuralParents$) { + if (structuralParent) { + queue.push(structuralParent); + } + } + } } + return rootsFound; + }; + + const $getObjectIndexPath$ = ( + seen: SeenRef, + getParent: (seen: SeenRef) => unknown | null + ): number[] => { const path = []; let current: typeof seen | undefined = seen; // Traverse up through parent references to build a path while (current && current.$index$ >= 0) { path.unshift(current.$index$); - if (typeof current.$parent$ !== 'object' || current.$parent$ === null) { + const parent = getParent(current); + if (typeof parent !== 'object' || parent === null) { break; } - current = seenObjsMap.get(current.$parent$); + current = seenObjsMap.get(parent); } + return path; + }; + + const $getObjectPathString$ = (obj: unknown) => { + const rootPath = objectPathStringCache.get(obj); + if (rootPath) { + return rootPath; + } + const seen = seenObjsMap.get(obj); + if (!seen) { + throw qError(QError.serializeErrorMissingRootId, [obj]); + } + const path = $getObjectIndexPath$(seen, (seen) => seen.$refParent$); const pathStr = path.length > 1 ? path.join(' ') : path.length ? path[0] : seen.$index$; - rootsPathMap.set(obj, pathStr); + objectPathStringCache.set(obj, pathStr); return pathStr; }; @@ -754,14 +803,23 @@ export const createSerializationContext = ( let seen = seenObjsMap.get(obj); if (!seen) { const rootIndex = roots.length; - seen = { $parent$: parent, $index$: rootIndex, $rootIndex$: rootIndex }; + seen = { + $refParent$: parent, + $structuralParents$: parent ? [parent] : [], + $index$: rootIndex, + $rootIndex$: rootIndex, + }; seenObjsMap.set(obj, seen); roots.push(obj); } else if (seen.$rootIndex$ === -1) { seen.$rootIndex$ = roots.length; roots.push(obj); } - $addRootPath$(obj); + // If a parent is provided and not already in the list, add it. + if (parent !== null && !seen.$structuralParents$.includes(parent)) { + seen.$structuralParents$.push(parent); + } + $getObjectPathString$(obj); return seen.$rootIndex$; }; @@ -781,13 +839,15 @@ export const createSerializationContext = ( $symbolToChunkResolver$: symbolToChunkResolver, $wasSeen$, $roots$: roots, + $objectPathStringCache$: objectPathStringCache, + $outputRootRefs$: true, $seen$, $hasRootId$: (obj: any) => { const id = seenObjsMap.get(obj); - return id?.$parent$ === null ? id.$index$ : undefined; + return id?.$refParent$ === null ? id.$index$ : undefined; }, $addRoot$, - $addRootPath$, + $getOwningSerializationRoots$, $syncFns$: syncFns, $addSyncFn$: (funcStr: string | null, argCount: number, fn: Function) => { const isFullFn = funcStr == null; @@ -819,7 +879,6 @@ export const createSerializationContext = ( $getProp$: getProp, $setProp$: setProp, $prepVNodeData$: prepVNodeData, - $pathMap$: rootsPathMap, }; }; @@ -881,14 +940,22 @@ class PromiseResult { * - Therefore root indexes need to be doubled to get the actual index. */ async function serialize(serializationContext: SerializationContext): Promise { - const { $writer$, $isSsrNode$, $isDomRef$, $storeProxyMap$, $addRoot$, $pathMap$, $wasSeen$ } = - serializationContext; + const { + $writer$, + $isSsrNode$, + $isDomRef$, + $storeProxyMap$, + $addRoot$, + $objectPathStringCache$, + $wasSeen$, + } = serializationContext; let depth = 0; const forwardRefs: number[] = []; let forwardRefsId = 0; const promises: Set> = new Set(); const preloadQrls = new Set(); const uninitializedRefs = new Map(); + const captureRefs = new Set(); let parent: unknown = null; const isRootObject = () => depth === 0; @@ -933,18 +1000,31 @@ async function serialize(serializationContext: SerializationContext): Promise { preloadQrls.add(qrl); - serializationContext.$addRoot$(qrl, null); + serializationContext.$addRoot$(qrl); }; - const outputRootRef = (value: unknown, elseCallback: () => void) => { + const outputRootRef = (value: unknown, rootDepth = 0): boolean => { + if (!serializationContext.$outputRootRefs$) { + return false; + } const seen = $wasSeen$(value); - const rootRefPath = $pathMap$.get(value); - if (isRootObject() && seen && seen.$parent$ !== null && rootRefPath) { + const rootRefPath = $objectPathStringCache$.get(value); + + // Objects are the only way to create circular dependencies. + // So the first thing to to is to see if we have a circular dependency. + // (NOTE: For root objects we need to serialize them regardless if we have seen + // them before, otherwise the root object reference will point to itself.) + // Also note that depth will be 1 for objects in root + if (rootDepth === depth && seen && seen.$refParent$ !== null && rootRefPath) { output(TypeIds.RootRef, rootRefPath); - } else if (depth > 0 && seen && seen.$rootIndex$ !== -1) { + return true; + } else if (depth > rootDepth && seen && seen.$rootIndex$ !== -1) { + // We have seen this object before, so we can serialize it as a reference. + // Otherwise serialize as normal output(TypeIds.RootRef, seen.$rootIndex$); + return true; } else { - elseCallback(); + return false; } }; @@ -961,8 +1041,8 @@ async function serialize(serializationContext: SerializationContext): Promise { - const qrl = qrlToString(serializationContext, value, uninitializedRefs); + if (!outputRootRef(value)) { + const qrl = qrlToString(serializationContext, value, uninitializedRefs, captureRefs); const type = preloadQrls.has(value) ? TypeIds.PreloadQRL : TypeIds.QRL; if (isRootObject()) { output(type, qrl); @@ -970,7 +1050,7 @@ async function serialize(serializationContext: SerializationContext): Promise { + if (!outputRootRef(value)) { output(TypeIds.String, value); - }); + } } } else if (typeof value === 'undefined') { output(TypeIds.Constant, Constants.Undefined); @@ -1038,27 +1118,8 @@ async function serialize(serializationContext: SerializationContext): Promise 1) { - const seen = $wasSeen$(value); - if (seen && seen.$rootIndex$ !== -1) { - // We have seen this object before, so we can serialize it as a reference. - // Otherwise serialize as normal - output(TypeIds.RootRef, seen.$rootIndex$); - return; - } + if (outputRootRef(value, 1)) { + return; } if (isPropsProxy(value)) { const varProps = value[_VAR_PROPS]; @@ -1108,16 +1169,54 @@ async function serialize(serializationContext: SerializationContext): Promise { return new PromiseResult(TypeIds.SerializerSignal, resolved, resolvedValue, null, null); }); output(TypeIds.ForwardRef, forwardRef); - } else if (result === _UNINITIALIZED && !uninitializedRefs.has(value)) { - const forwardRefId = forwardRefsId++; - uninitializedRefs.set(value, forwardRefId); - output(TypeIds.ForwardRef, forwardRefId); } else { depth--; writeValue(result); @@ -1356,10 +1455,18 @@ async function serialize(serializationContext: SerializationContext): Promise + uninitializedRefs?: Map, + captureRefs?: Set ) { let symbol = value.$symbol$; let chunk = value.$chunk$; @@ -1480,13 +1588,16 @@ export function qrlToString( serializedReferences += ' '; } const captureRef = value.$captureRef$[i]; - if ( - isObject(captureRef) && - uninitializedRefs && - uninitializedRefs.has(captureRef) && - SerializerSymbol in captureRef - ) { - captureRef[SerializerSymbol] = undefined; + if (isObject(captureRef) && uninitializedRefs) { + if (SerializerSymbol in captureRef && uninitializedRefs.has(captureRef)) { + // we already seen shallow object before the qrl + captureRef[SerializerSymbol] = undefined; + } + if (captureRefs) { + // we didn't see shallow object before the qrl, so it might be shallow + // object if SerializerSymbol value is resolved to the UNINITIALIZED symbol + captureRefs.add(captureRef); + } } // We refer by id so every capture needs to be a root serializedReferences += serializationContext.$addRoot$(captureRef); @@ -2013,6 +2124,6 @@ export const typeIdToName = (code: TypeIds) => { return _typeIdNames[code] || `Unknown(${code})`; }; -const constantToName = (code: Constants) => { +export const constantToName = (code: Constants) => { return _constantNames[code] || `Unknown(${code})`; }; diff --git a/packages/qwik/src/core/shared/shared-serialization.unit.ts b/packages/qwik/src/core/shared/shared-serialization.unit.ts index 47c5aff9c75..8e10a0e4f7c 100644 --- a/packages/qwik/src/core/shared/shared-serialization.unit.ts +++ b/packages/qwik/src/core/shared/shared-serialization.unit.ts @@ -1,4 +1,4 @@ -import { $, componentQrl, noSerialize } from '@qwik.dev/core'; +import { $, componentQrl, noSerialize, useLexicalScope } from '@qwik.dev/core'; import { describe, expect, it, vi } from 'vitest'; import { _fnSignal, _UNINITIALIZED, _wrapProp } from '../internal'; import { type SignalImpl } from '../reactive-primitives/impl/signal-impl'; @@ -14,10 +14,12 @@ import { Task } from '../use/use-task'; import { inlinedQrl } from './qrl/qrl'; import { createQRL, type QRLInternal } from './qrl/qrl-class'; import { + Constants, TypeIds, _constants, _createDeserializeContainer, _typeIdNames, + constantToName, createSerializationContext, dumpState, } from './shared-serialization'; @@ -26,6 +28,7 @@ import { isQrl } from './qrl/qrl-utils'; import { NoSerializeSymbol, SerializerSymbol } from './utils/serialize-utils'; import { SubscriptionData } from '../reactive-primitives/subscription-data'; import { StoreFlags, type CustomSerializable } from '../reactive-primitives/types'; +import { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl'; const DEBUG = false; @@ -77,8 +80,8 @@ describe('shared-serialization', () => { (81 chars)" `); }); - describe('UNINITIALIZED', () => { - it(title(TypeIds.Constant) + ' - UNINITIALIZED, not serialized object', async () => { + describe(constantToName(Constants.UNINITIALIZED), () => { + it('should not serialize object', async () => { const uninitializedObject = { shouldNot: 'serialize', }; @@ -95,7 +98,7 @@ describe('shared-serialization', () => { (15 chars)" `); }); - it(title(TypeIds.Constant) + ' - UNINITIALIZED, serialized object', async () => { + it('should serialize object before qrl', async () => { const uninitializedObject = { should: 'serialize', }; @@ -117,6 +120,295 @@ describe('shared-serialization', () => { (69 chars)" `); }); + it('should serialize object after qrl', async () => { + const uninitializedObject = { + should: 'serialize', + }; + (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { + return _UNINITIALIZED; + }; + const qrl = inlinedQrl(() => uninitializedObject.should, 'dump_qrl', [uninitializedObject]); + expect(await dump(qrl, uninitializedObject)).toMatchInlineSnapshot(` + " + 0 QRL "mock-chunk#dump_qrl[1]" + 1 Object [ + String "should" + String "serialize" + ] + (59 chars)" + `); + }); + it('should not serialize nested object', async () => { + const uninitializedObject = { + shouldNot: 'serialize', + }; + (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { + return _UNINITIALIZED; + }; + const parent = { + child: uninitializedObject, + }; + expect(await dump(parent)).toMatchInlineSnapshot(` + " + 0 Object [ + String "child" + ForwardRef 0 + ] + 1 Constant _UNINITIALIZED + 2 ForwardRefs [ + 1 + ] + (30 chars)" + `); + }); + it('should serialize nested object before qrl', async () => { + const uninitializedObject = { + should: 'serialize', + }; + (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { + return _UNINITIALIZED; + }; + + const parent = { + child: uninitializedObject, + }; + const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent]); + expect(await dump(parent, qrl)).toMatchInlineSnapshot(` + " + 0 Object [ + String "child" + ForwardRef 0 + ] + 1 QRL "mock-chunk#dump_qrl[0]" + 2 Object [ + String "should" + String "serialize" + ] + 3 ForwardRefs [ + 2 + ] + (84 chars)" + `); + }); + it('should serialize nested object after qrl', async () => { + const uninitializedObject = { + should: 'serialize', + }; + (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { + return _UNINITIALIZED; + }; + + const parent = { + child: uninitializedObject, + }; + const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent]); + expect(await dump(qrl, parent)).toMatchInlineSnapshot(` + " + 0 QRL "mock-chunk#dump_qrl[1]" + 1 Object [ + String "child" + Object [ + String "should" + String "serialize" + ] + ] + (74 chars)" + `); + }); + it('should serialize nested root ref object before qrl', async () => { + const uninitializedObject = { + should: 'serialize', + }; + (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { + return _UNINITIALIZED; + }; + + const parent = { + child: uninitializedObject, + }; + const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent]); + expect(await dump(parent, uninitializedObject, qrl)).toMatchInlineSnapshot(` + " + 0 Object [ + String "child" + RootRef 1 + ] + 1 ForwardRef 0 + 2 QRL "mock-chunk#dump_qrl[0]" + 3 Object [ + String "should" + String "serialize" + ] + 4 ForwardRefs [ + 3 + ] + (88 chars)" + `); + }); + it('should serialize nested root ref object after qrl', async () => { + const uninitializedObject = { + should: 'serialize', + }; + (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { + return _UNINITIALIZED; + }; + + const parent = { + child: uninitializedObject, + }; + const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent]); + expect(await dump(qrl, parent, uninitializedObject)).toMatchInlineSnapshot(` + " + 0 QRL "mock-chunk#dump_qrl[1]" + 1 Object [ + String "child" + RootRef 2 + ] + 2 Object [ + String "should" + String "serialize" + ] + (78 chars)" + `); + }); + it('should serialize nested object inside wrapped signal before qrl', async () => { + const uninitializedObject = { + should: 'serialize', + }; + (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { + return _UNINITIALIZED; + }; + + const parent = { + child: uninitializedObject, + }; + const wrappedSignal = new WrappedSignalImpl( + null, + (obj: any) => obj.child.should, + [parent], + null + ); + const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent]); + expect(await dump(wrappedSignal, qrl)).toMatchInlineSnapshot(` + " + 0 WrappedSignal [ + Number 0 + Array [ + Object [ + String "child" + ForwardRef 0 + ] + ] + Constant null + Number 3 + Constant null + ] + 1 QRL "mock-chunk#dump_qrl[2]" + 2 RootRef "0 1 0" + 3 Object [ + String "should" + String "serialize" + ] + 4 ForwardRefs [ + 3 + ] + (119 chars)" + `); + }); + it('should serialize nested object inside wrapped signal after qrl', async () => { + const uninitializedObject = { + should: 'serialize', + }; + (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { + return _UNINITIALIZED; + }; + + const parent = { + child: uninitializedObject, + }; + const wrappedSignal = new WrappedSignalImpl( + null, + (obj: any) => obj.child.should, + [parent], + null + ); + const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent]); + expect(await dump(qrl, wrappedSignal)).toMatchInlineSnapshot(` + " + 0 QRL "mock-chunk#dump_qrl[2]" + 1 WrappedSignal [ + Number 0 + Array [ + RootRef 2 + ] + Constant null + Number 3 + Constant null + ] + 2 Object [ + String "child" + Object [ + String "should" + String "serialize" + ] + ] + (103 chars)" + `); + }); + it('should serialize nested object inside wrapped signal after qrl', async () => { + const uninitializedObject = { + should: 'serialize', + }; + (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { + return _UNINITIALIZED; + }; + + const parent = { + child: uninitializedObject, + }; + const anotherParent = { + child: uninitializedObject, + }; + const wrappedSignal = new WrappedSignalImpl(null, (obj: any) => obj.child, [parent], null); + const qrl = inlinedQrl( + () => { + const [signal] = useLexicalScope(); + return signal.value.should; + }, + 'dump_qrl', + [wrappedSignal] + ); + expect(await dump(anotherParent, wrappedSignal, qrl)).toMatchInlineSnapshot(` + " + 0 Object [ + String "child" + ForwardRef 0 + ] + 1 WrappedSignal [ + Number 0 + Array [ + Object [ + RootRef 3 + RootRef 4 + ] + ] + Constant null + Number 3 + Constant null + ] + 2 QRL "mock-chunk#dump_qrl[1]" + 3 RootRef "0 0" + 4 RootRef "0 1" + 5 Object [ + String "should" + String "serialize" + ] + 6 ForwardRefs [ + 5 + ] + (138 chars)" + `); + }); }); it(title(TypeIds.Number), async () => { expect(await dump(123)).toMatchInlineSnapshot(` From c091b65ce799f4596c77c571fd2e97c517c3bf0b Mon Sep 17 00:00:00 2001 From: Varixo Date: Thu, 8 May 2025 14:17:45 +0200 Subject: [PATCH 12/12] WIP: serialization weak ref Co-authored-by: Wout Mertens --- eslint.config.mjs | 2 +- packages/docs/src/routes/api/qwik/api.json | 32 +- packages/docs/src/routes/api/qwik/index.mdx | 24 +- .../resolve-request-handlers.ts | 4 - .../request-handler/response-page.ts | 10 - .../qwik-router/src/runtime/src/contexts.ts | 3 +- .../src/runtime/src/qwik-router-component.tsx | 34 +- .../src/runtime/src/server-functions.ts | 13 +- packages/qwik-router/src/runtime/src/types.ts | 1 + packages/qwik/src/core/index.ts | 2 - packages/qwik/src/core/internal.ts | 3 +- packages/qwik/src/core/qwik.core.api.md | 22 +- .../core/reactive-primitives/internal-api.ts | 18 +- .../src/core/shared/shared-serialization.ts | 183 +++------ .../core/shared/shared-serialization.unit.ts | 362 ++++-------------- 15 files changed, 181 insertions(+), 532 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index dcc4bcb7897..238e944172d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -109,7 +109,7 @@ export default tseslint.config( }, }, { - files: ['packages/docs/**/*.{ts,tsx}'], + files: ['packages/docs/demo/**/*.{ts,tsx}'], rules: { 'no-console': 'off', }, diff --git a/packages/docs/src/routes/api/qwik/api.json b/packages/docs/src/routes/api/qwik/api.json index 3ac5286798e..28ae3ec1c85 100644 --- a/packages/docs/src/routes/api/qwik/api.json +++ b/packages/docs/src/routes/api/qwik/api.json @@ -672,20 +672,6 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/jsx-runtime.ts", "mdFile": "core.h.md" }, - { - "name": "HTMLElementAttrs", - "id": "htmlelementattrs", - "hierarchy": [ - { - "name": "HTMLElementAttrs", - "id": "htmlelementattrs" - } - ], - "kind": "Interface", - "content": "```typescript\nexport interface HTMLElementAttrs extends HTMLAttributesBase, FilterBase \n```\n**Extends:** HTMLAttributesBase, FilterBase<HTMLElement>", - "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts", - "mdFile": "core.htmlelementattrs.md" - }, { "name": "implicit$FirstArg", "id": "implicit_firstarg", @@ -1318,7 +1304,7 @@ } ], "kind": "TypeAlias", - "content": "The DOM props without plain handlers, for use inside functions\n\n\n```typescript\nexport type QwikHTMLElements = {\n [tag in keyof HTMLElementTagNameMap]: Augmented & HTMLElementAttrs & QwikAttributes;\n};\n```\n**References:** [HTMLElementAttrs](#htmlelementattrs), [QwikAttributes](#qwikattributes)", + "content": "The DOM props without plain handlers, for use inside functions\n\n\n```typescript\nexport type QwikHTMLElements = {\n [tag in keyof HTMLElementTagNameMap]: Augmented & HTMLElementAttrs & QwikAttributes;\n};\n```\n**References:** [QwikAttributes](#qwikattributes)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts", "mdFile": "core.qwikhtmlelements.md" }, @@ -1458,7 +1444,7 @@ } ], "kind": "TypeAlias", - "content": "The SVG props without plain handlers, for use inside functions\n\n\n```typescript\nexport type QwikSVGElements = {\n [K in keyof Omit]: SVGProps;\n};\n```\n**References:** [SVGProps](#svgprops)", + "content": "The SVG props without plain handlers, for use inside functions\n\n\n```typescript\nexport type QwikSVGElements = {\n [K in keyof Omit]: SVGProps;\n};\n```", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts", "mdFile": "core.qwiksvgelements.md" }, @@ -2008,20 +1994,6 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts", "mdFile": "core.svgattributes.md" }, - { - "name": "SVGProps", - "id": "svgprops", - "hierarchy": [ - { - "name": "SVGProps", - "id": "svgprops" - } - ], - "kind": "Interface", - "content": "```typescript\nexport interface SVGProps extends SVGAttributes, QwikAttributes \n```\n**Extends:** [SVGAttributes](#svgattributes), [QwikAttributes](#qwikattributes)<T>", - "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts", - "mdFile": "core.svgprops.md" - }, { "name": "sync$", "id": "sync_", diff --git a/packages/docs/src/routes/api/qwik/index.mdx b/packages/docs/src/routes/api/qwik/index.mdx index bb1de806426..04abe6e5ef4 100644 --- a/packages/docs/src/routes/api/qwik/index.mdx +++ b/packages/docs/src/routes/api/qwik/index.mdx @@ -1333,16 +1333,6 @@ any[] [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/jsx-runtime.ts) -## HTMLElementAttrs - -```typescript -export interface HTMLElementAttrs extends HTMLAttributesBase, FilterBase -``` - -**Extends:** HTMLAttributesBase, FilterBase<HTMLElement> - -[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts) - ## implicit$FirstArg Create a `____$(...)` convenience method from `___(...)`. @@ -2586,7 +2576,7 @@ export type QwikHTMLElements = { }; ``` -**References:** [HTMLElementAttrs](#htmlelementattrs), [QwikAttributes](#qwikattributes) +**References:** [QwikAttributes](#qwikattributes) [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts) @@ -2784,8 +2774,6 @@ export type QwikSVGElements = { }; ``` -**References:** [SVGProps](#svgprops) - [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts) ## QwikSymbolEvent @@ -8121,16 +8109,6 @@ _(Optional)_ [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts) -## SVGProps - -```typescript -export interface SVGProps extends SVGAttributes, QwikAttributes -``` - -**Extends:** [SVGAttributes](#svgattributes), [QwikAttributes](#qwikattributes)<T> - -[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts) - ## sync$ Extract function into a synchronously loadable QRL. diff --git a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts index 0a9e5a7168a..69a604d72be 100644 --- a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts @@ -1,5 +1,4 @@ import { type QRL } from '@qwik.dev/core'; -import { SerializerSymbol, _UNINITIALIZED } from '@qwik.dev/core/internal'; import type { Render, RenderToStringResult } from '@qwik.dev/core/server'; import { QACTION_KEY, QFN_KEY, QLOADER_KEY } from '../../runtime/src/constants'; import { @@ -598,9 +597,6 @@ export async function renderQData(requestEv: RequestEvent) { for (const loaderId in allLoaders) { const loader = allLoaders[loaderId]; if (loader !== null) { - if (typeof loader === 'object' && SerializerSymbol in loader) { - (loader as any)[SerializerSymbol] = undefined; - } loaders[loaderId] = loader; } } diff --git a/packages/qwik-router/src/middleware/request-handler/response-page.ts b/packages/qwik-router/src/middleware/request-handler/response-page.ts index 74eef361e4f..0319962a3e1 100644 --- a/packages/qwik-router/src/middleware/request-handler/response-page.ts +++ b/packages/qwik-router/src/middleware/request-handler/response-page.ts @@ -1,5 +1,3 @@ -import { SerializerSymbol } from '@qwik.dev/core'; -import { _UNINITIALIZED } from '@qwik.dev/core/internal'; import type { QwikRouterEnvData } from '../../runtime/src/types'; import { getRequestLoaders, @@ -35,14 +33,6 @@ export function getQwikRouterServerData(requestEv: RequestEvent) { const loaders = getRequestLoaders(requestEv); - // shallow serialize loaders data - for (const key in loaders) { - const loader = loaders[key]; - if (typeof loader === 'object' && loader !== null) { - (loader as any)[SerializerSymbol] = () => _UNINITIALIZED; - } - } - return { url: reconstructedUrl.href, requestHeaders, diff --git a/packages/qwik-router/src/runtime/src/contexts.ts b/packages/qwik-router/src/runtime/src/contexts.ts index 20881a21344..5f16b512ea8 100644 --- a/packages/qwik-router/src/runtime/src/contexts.ts +++ b/packages/qwik-router/src/runtime/src/contexts.ts @@ -10,7 +10,8 @@ import type { RouteStateInternal, } from './types'; -export const RouteStateContext = /*#__PURE__*/ createContextId>('qc-s'); +export const RouteStateContext = + /*#__PURE__*/ createContextId>>('qc-s'); export const ContentContext = /*#__PURE__*/ createContextId('qc-c'); export const ContentInternalContext = diff --git a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx index e93b03c44fc..15f1a5604b3 100644 --- a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx +++ b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx @@ -19,8 +19,13 @@ import { import { _getContextElement, _getQContainerElement, + _UNINITIALIZED, _waitUntilRendered, + SerializerSymbol, + _serializationWeakRef, type _ElementVNode, + createSignal, + type Signal, } from '@qwik.dev/core/internal'; import { clientNavigate } from './client-navigate'; import { CLIENT_DATA_CACHE, Q_ROUTE } from './constants'; @@ -145,7 +150,21 @@ export const QwikRouterProvider = component$((props) => { { deep: false } ); const navResolver: { r?: () => void } = {}; - const loaderState = useStore(env.response.loaders, { deep: false }); + const loaderState = Object.fromEntries( + Object.entries(env.response.loaders).map(([k, v]) => { + const value = createSignal(v); + return [k, value]; + }) + ); + + (loaderState as any)[SerializerSymbol] = async (o: Record) => { + const resultPs = Object.entries(o).map(async ([k, val]) => { + const v = await val; + return [k, _serializationWeakRef(v)]; + }); + return Object.fromEntries(await Promise.all(resultPs)); + }; + const routeInternal = useSignal({ type: 'initial', dest: url, @@ -278,7 +297,7 @@ export const QwikRouterProvider = component$((props) => { let scroller = document.getElementById(QWIK_ROUTER_SCROLLER); if (!scroller) { scroller = document.getElementById(QWIK_CITY_SCROLLER); - if (scroller) { + if (scroller && isDev) { console.warn( `Please update your scroller ID to "${QWIK_ROUTER_SCROLLER}" as "${QWIK_CITY_SCROLLER}" is deprecated and will be removed in V3` ); @@ -460,12 +479,17 @@ export const QwikRouterProvider = component$((props) => { } const loaders = clientPageData?.loaders; - const win = window as ClientSPAWindow; if (loaders) { - Object.assign(loaderState, loaders); + for (const [key, value] of Object.entries(loaders)) { + const signal = loaderState[key] as Signal | typeof _UNINITIALIZED; + if (signal && signal !== _UNINITIALIZED) { + signal.value = value; + } + } } CLIENT_DATA_CACHE.clear(); + const win = window as ClientSPAWindow; if (!win._qRouterSPA) { // only add event listener once win._qRouterSPA = true; @@ -698,7 +722,7 @@ export const QwikRouterMockProvider = component$((props) => { deep: false } ); - const loaderState = useSignal({}); + const loaderState = {}; const routeInternal = useSignal({ type: 'initial', dest: url }); const goto: RouteNavigate = diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index fd3ba627bf1..ee976c2b324 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -15,7 +15,6 @@ import { _getContextElement, _getContextEvent, _serialize, - _wrapStore, _useInvokeContext, _UNINITIALIZED, } from '@qwik.dev/core/internal'; @@ -209,15 +208,17 @@ export const routeLoaderQrl = (( If your are managing reusable logic or a library it is essential that this function is re-exported from within 'layout.tsx' or 'index.tsx file of the existing route otherwise it will not run or throw exception. For more information check: https://qwik.dev/docs/re-exporting-loaders/`); } - const data = untrack(() => state[id]); - if (data === _UNINITIALIZED && isBrowser) { + const loaderData = untrack(() => state[id].value); + if (loaderData === _UNINITIALIZED && isBrowser) { + // Request the loader data from the server and throw the Promise + // so the client can load it synchronously. throw loadClientData(location.url, iCtx.$hostElement$, { loaderIds: [id], - }).then((data) => { - state[id] = data?.loaders[id]; + }).then((clientData) => { + state[id].value = clientData?.loaders[id]; }); } - return _wrapStore(state, id); + return state[id]; } loader.__brand = 'server_loader' as const; loader.__qrl = loaderQrl; diff --git a/packages/qwik-router/src/runtime/src/types.ts b/packages/qwik-router/src/runtime/src/types.ts index 6998b32fb07..8ad79a346ec 100644 --- a/packages/qwik-router/src/runtime/src/types.ts +++ b/packages/qwik-router/src/runtime/src/types.ts @@ -401,6 +401,7 @@ export type GetValidatorType = export interface CommonLoaderActionOptions { readonly id?: string; readonly validation?: DataValidator[]; + readonly persist?: boolean; } /** @public */ diff --git a/packages/qwik/src/core/index.ts b/packages/qwik/src/core/index.ts index 921188abe73..50c3e232e4f 100644 --- a/packages/qwik/src/core/index.ts +++ b/packages/qwik/src/core/index.ts @@ -85,8 +85,6 @@ export type { QwikHTMLElements, QwikSVGElements, SVGAttributes, - HTMLElementAttrs, - SVGProps, } from './shared/jsx/types/jsx-generated'; export { render } from './client/dom-render'; export { getDomContainer, _getQContainerElement } from './client/dom-container'; diff --git a/packages/qwik/src/core/internal.ts b/packages/qwik/src/core/internal.ts index 942caf8ed97..6825f4b34dd 100644 --- a/packages/qwik/src/core/internal.ts +++ b/packages/qwik/src/core/internal.ts @@ -16,7 +16,7 @@ export type { VNodeFlags as _VNodeFlags, } from './client/types'; export { vnode_toString as _vnode_toString } from './client/vnode'; -export { _wrapProp, _wrapSignal, _wrapStore } from './reactive-primitives/internal-api'; +export { _wrapProp, _wrapSignal } from './reactive-primitives/internal-api'; export { SubscriptionData as _SubscriptionData } from './reactive-primitives/subscription-data'; export { _EFFECT_BACK_REF } from './reactive-primitives/types'; export { @@ -37,6 +37,7 @@ export { _deserialize, dumpState as _dumpState, preprocessState as _preprocessState, + _serializationWeakRef, _serialize, } from './shared/shared-serialization'; export { _CONST_PROPS, _IMMUTABLE, _VAR_PROPS, _UNINITIALIZED } from './shared/utils/constants'; diff --git a/packages/qwik/src/core/qwik.core.api.md b/packages/qwik/src/core/qwik.core.api.md index 1a838cfc549..c45d6d6f7c9 100644 --- a/packages/qwik/src/core/qwik.core.api.md +++ b/packages/qwik/src/core/qwik.core.api.md @@ -336,13 +336,6 @@ function h, PROPS extends {} = {} export { h as createElement } export { h } -// Warning: (ae-forgotten-export) The symbol "HTMLAttributesBase" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "FilterBase" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export interface HTMLElementAttrs extends HTMLAttributesBase, FilterBase { -} - // @internal @deprecated (undocumented) export const _IMMUTABLE: unique symbol; @@ -637,6 +630,7 @@ export type QwikFocusEvent = NativeFocusEvent; // Warning: (ae-forgotten-export) The symbol "Augmented" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SpecialAttrs" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "HTMLElementAttrs" needs to be exported by the entry point index.d.ts // // @public export type QwikHTMLElements = { @@ -693,6 +687,8 @@ export type QwikPointerEvent = NativePointerEvent; // @public @deprecated (undocumented) export type QwikSubmitEvent = SubmitEvent; +// Warning: (ae-forgotten-export) The symbol "SVGProps" needs to be exported by the entry point index.d.ts +// // @public export type QwikSVGElements = { [K in keyof Omit]: SVGProps; @@ -842,6 +838,11 @@ export const _restProps: (props: PropsProxy, omit: string[], target?: Props) => // @internal export const _run: (...args: unknown[]) => ValueOrPromise_2; +// Warning: (ae-forgotten-export) The symbol "SerializationWeakRef" needs to be exported by the entry point index.d.ts +// +// @internal (undocumented) +export const _serializationWeakRef: (obj: unknown) => SerializationWeakRef; + // @internal export function _serialize(data: unknown[]): Promise; @@ -1559,10 +1560,6 @@ export interface SVGAttributes extends AriaAttribut zoomAndPan?: string | undefined; } -// @public (undocumented) -export interface SVGProps extends SVGAttributes, QwikAttributes { -} - // @public export const sync$: (fn: T) => SyncQRL; @@ -1844,9 +1841,6 @@ export const _wrapProp: , P extends keyof T>(...args: // @internal @deprecated (undocumented) export const _wrapSignal: , P extends keyof T>(obj: T, prop: P) => any; -// @internal (undocumented) -export const _wrapStore: , P extends keyof T>(obj: T, prop: P) => Signal; - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/qwik/src/core/reactive-primitives/internal-api.ts b/packages/qwik/src/core/reactive-primitives/internal-api.ts index 17c3bdae903..cf3523aecb8 100644 --- a/packages/qwik/src/core/reactive-primitives/internal-api.ts +++ b/packages/qwik/src/core/reactive-primitives/internal-api.ts @@ -1,10 +1,10 @@ import { _CONST_PROPS, _IMMUTABLE } from '../shared/utils/constants'; import { assertEqual } from '../shared/error/assert'; import { isObject } from '../shared/utils/types'; -import { isSignal, type Signal } from './signal.public'; +import { isSignal } from './signal.public'; import { getStoreTarget } from './impl/store'; import { isPropsProxy } from '../shared/jsx/jsx-runtime'; -import { SignalFlags, WrappedSignalFlags } from './types'; +import { WrappedSignalFlags } from './types'; import { WrappedSignalImpl } from './impl/wrapped-signal-impl'; // Keep these properties named like this so they're the same as from wrapSignal @@ -60,20 +60,6 @@ export const _wrapProp = , P extends keyof T>(...args return getWrapped(args); }; -/** @internal */ -export const _wrapStore = , P extends keyof T>( - obj: T, - prop: P -): Signal => { - const target = getStoreTarget(obj)!; - const value = target[prop]; - if (isSignal(value)) { - return value; - } else { - return new WrappedSignalImpl(null, getProp, [obj, prop], null, SignalFlags.INVALID); - } -}; - /** @internal @deprecated v1 compat */ export const _wrapSignal = , P extends keyof T>( obj: T, diff --git a/packages/qwik/src/core/shared/shared-serialization.ts b/packages/qwik/src/core/shared/shared-serialization.ts index 29c02dd38b0..9065b36d415 100644 --- a/packages/qwik/src/core/shared/shared-serialization.ts +++ b/packages/qwik/src/core/shared/shared-serialization.ts @@ -453,7 +453,12 @@ const allocate = (container: DeserializeContainer, typeId: number, value: unknow if (!container.$forwardRefs$) { throw qError(QError.serializeErrorCannotAllocate, ['forward ref']); } - return container.$getObjectById$(container.$forwardRefs$[value as number]); + const rootRef = container.$forwardRefs$[value as number]; + if (rootRef === -1) { + return _UNINITIALIZED; + } else { + return container.$getObjectById$(rootRef); + } case TypeIds.ForwardRefs: return value; case TypeIds.Constant: @@ -656,8 +661,6 @@ export interface SerializationContext { */ $addRoot$: (obj: unknown, parent?: unknown) => number; - $getOwningSerializationRoots$: (obj: unknown) => unknown[]; - $seen$: (obj: unknown, parent: unknown | null, index: number) => void; $roots$: unknown[]; @@ -727,43 +730,6 @@ export const createSerializationContext = ( }); }; - const $getOwningSerializationRoots$ = (obj: unknown): unknown[] => { - const rootsFound: unknown[] = []; - const queue: unknown[] = [obj]; - const visited = new Set(); - - while (queue.length > 0) { - const currentObj = queue.shift(); - if (!currentObj || visited.has(currentObj)) { - continue; - } - visited.add(currentObj); - - const seen = seenObjsMap.get(currentObj); - - if (!seen) { - continue; - } - - // Only add actual root objects (those added via $addRoot$ with no parent or seen before $addRoot$) - // Ensure it's actually in the roots array by checking $rootIndex$ != -1 - const rootPath = objectPathStringCache.get(currentObj); - if ((seen.$rootIndex$ !== -1 || rootPath) && !rootsFound.includes(currentObj)) { - rootsFound.push(currentObj); - } - - // Add its structural parent to the queue to continue traversal - if (seen.$structuralParents$) { - for (const structuralParent of seen.$structuralParents$) { - if (structuralParent) { - queue.push(structuralParent); - } - } - } - } - return rootsFound; - }; - const $getObjectIndexPath$ = ( seen: SeenRef, getParent: (seen: SeenRef) => unknown | null @@ -847,7 +813,6 @@ export const createSerializationContext = ( return id?.$refParent$ === null ? id.$index$ : undefined; }, $addRoot$, - $getOwningSerializationRoots$, $syncFns$: syncFns, $addSyncFn$: (funcStr: string | null, argCount: number, fn: Function) => { const isFullFn = funcStr == null; @@ -930,6 +895,14 @@ class PromiseResult { public $qrl$: QRLInternal | null = null ) {} } + +class SerializationWeakRef { + constructor(public $obj$: unknown) {} +} + +/** @internal */ +export const _serializationWeakRef = (obj: unknown) => new SerializationWeakRef(obj); + /** * Format: * @@ -950,12 +923,12 @@ async function serialize(serializationContext: SerializationContext): Promise> = new Set(); const preloadQrls = new Set(); - const uninitializedRefs = new Map(); - const captureRefs = new Set(); + const s11nWeakRefs = new Map(); let parent: unknown = null; const isRootObject = () => depth === 0; @@ -1003,10 +976,7 @@ async function serialize(serializationContext: SerializationContext): Promise { - if (!serializationContext.$outputRootRefs$) { - return false; - } + const outputAsRootRef = (value: unknown, rootDepth = 0): boolean => { const seen = $wasSeen$(value); const rootRefPath = $objectPathStringCache$.get(value); @@ -1023,9 +993,22 @@ async function serialize(serializationContext: SerializationContext): Promise { @@ -1041,8 +1024,8 @@ async function serialize(serializationContext: SerializationContext): Promise { return new PromiseResult(TypeIds.SerializerSignal, resolved, resolvedValue, null, null); @@ -1394,6 +1338,11 @@ async function serialize(serializationContext: SerializationContext): Promise { $writer$.write('['); - let lastRootsLength = 0; let rootsLength = serializationContext.$roots$.length; - while (lastRootsLength < rootsLength || promises.size) { - if (lastRootsLength !== 0) { + while (rootIdx < rootsLength || promises.size) { + if (rootIdx !== 0) { $writer$.write(','); } let separator = false; - for (let i = lastRootsLength; i < rootsLength; i++) { + for (; rootIdx < rootsLength; rootIdx++) { if (separator) { $writer$.write(','); } else { separator = true; } - writeValue(serializationContext.$roots$[i]); + writeValue(serializationContext.$roots$[rootIdx]); } if (promises.size) { @@ -1450,26 +1398,9 @@ async function serialize(serializationContext: SerializationContext): Promise, - captureRefs?: Set + value: QRLInternal | SyncQRLInternal ) { let symbol = value.$symbol$; let chunk = value.$chunk$; @@ -1587,20 +1516,8 @@ export function qrlToString( if (i > 0) { serializedReferences += ' '; } - const captureRef = value.$captureRef$[i]; - if (isObject(captureRef) && uninitializedRefs) { - if (SerializerSymbol in captureRef && uninitializedRefs.has(captureRef)) { - // we already seen shallow object before the qrl - captureRef[SerializerSymbol] = undefined; - } - if (captureRefs) { - // we didn't see shallow object before the qrl, so it might be shallow - // object if SerializerSymbol value is resolved to the UNINITIALIZED symbol - captureRefs.add(captureRef); - } - } // We refer by id so every capture needs to be a root - serializedReferences += serializationContext.$addRoot$(captureRef); + serializedReferences += serializationContext.$addRoot$(value.$captureRef$[i]); } qrlStringInline += `[${serializedReferences}]`; } else if (value.$capture$ && value.$capture$.length > 0) { diff --git a/packages/qwik/src/core/shared/shared-serialization.unit.ts b/packages/qwik/src/core/shared/shared-serialization.unit.ts index 8e10a0e4f7c..7be7972b00c 100644 --- a/packages/qwik/src/core/shared/shared-serialization.unit.ts +++ b/packages/qwik/src/core/shared/shared-serialization.unit.ts @@ -1,6 +1,6 @@ -import { $, componentQrl, noSerialize, useLexicalScope } from '@qwik.dev/core'; +import { $, componentQrl, noSerialize } from '@qwik.dev/core'; import { describe, expect, it, vi } from 'vitest'; -import { _fnSignal, _UNINITIALIZED, _wrapProp } from '../internal'; +import { _fnSignal, _serializationWeakRef, _UNINITIALIZED, _wrapProp } from '../internal'; import { type SignalImpl } from '../reactive-primitives/impl/signal-impl'; import { createComputedQrl, @@ -28,7 +28,6 @@ import { isQrl } from './qrl/qrl-utils'; import { NoSerializeSymbol, SerializerSymbol } from './utils/serialize-utils'; import { SubscriptionData } from '../reactive-primitives/subscription-data'; import { StoreFlags, type CustomSerializable } from '../reactive-primitives/types'; -import { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl'; const DEBUG = false; @@ -82,104 +81,43 @@ describe('shared-serialization', () => { }); describe(constantToName(Constants.UNINITIALIZED), () => { it('should not serialize object', async () => { - const uninitializedObject = { - shouldNot: 'serialize', - }; - (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { - return _UNINITIALIZED; - }; - expect(await dump(uninitializedObject)).toMatchInlineSnapshot(` - " - 0 ForwardRef 0 - 1 Constant _UNINITIALIZED - 2 ForwardRefs [ - 1 - ] - (15 chars)" - `); - }); - it('should serialize object before qrl', async () => { - const uninitializedObject = { - should: 'serialize', - }; - (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { - return _UNINITIALIZED; - }; - const qrl = inlinedQrl(() => uninitializedObject.should, 'dump_qrl', [uninitializedObject]); - expect(await dump(uninitializedObject, qrl)).toMatchInlineSnapshot(` - " - 0 ForwardRef 0 - 1 QRL "mock-chunk#dump_qrl[0]" - 2 Object [ - String "should" - String "serialize" - ] - 3 ForwardRefs [ - 2 - ] - (69 chars)" - `); - }); - it('should serialize object after qrl', async () => { - const uninitializedObject = { - should: 'serialize', - }; - (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { - return _UNINITIALIZED; - }; - const qrl = inlinedQrl(() => uninitializedObject.should, 'dump_qrl', [uninitializedObject]); - expect(await dump(qrl, uninitializedObject)).toMatchInlineSnapshot(` - " - 0 QRL "mock-chunk#dump_qrl[1]" - 1 Object [ - String "should" - String "serialize" - ] - (59 chars)" - `); - }); - it('should not serialize nested object', async () => { - const uninitializedObject = { - shouldNot: 'serialize', - }; - (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { - return _UNINITIALIZED; - }; const parent = { - child: uninitializedObject, + child: { should: 'serialize' }, }; + + (parent as any)[SerializerSymbol] = () => ({ + child: _serializationWeakRef(parent.child), + }); + expect(await dump(parent)).toMatchInlineSnapshot(` " 0 Object [ String "child" ForwardRef 0 ] - 1 Constant _UNINITIALIZED - 2 ForwardRefs [ - 1 + 1 ForwardRefs [ + -1 ] - (30 chars)" + (27 chars)" `); }); - it('should serialize nested object before qrl', async () => { - const uninitializedObject = { - should: 'serialize', - }; - (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { - return _UNINITIALIZED; - }; - + it('should serialize object before qrl', async () => { const parent = { - child: uninitializedObject, + child: { should: 'serialize' }, }; - const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent]); + + (parent as any)[SerializerSymbol] = () => ({ + child: _serializationWeakRef(parent.child), + }); + + const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent.child]); expect(await dump(parent, qrl)).toMatchInlineSnapshot(` " 0 Object [ String "child" ForwardRef 0 ] - 1 QRL "mock-chunk#dump_qrl[0]" + 1 QRL "mock-chunk#dump_qrl[2]" 2 Object [ String "should" String "serialize" @@ -190,225 +128,78 @@ describe('shared-serialization', () => { (84 chars)" `); }); - it('should serialize nested object after qrl', async () => { - const uninitializedObject = { - should: 'serialize', - }; - (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { - return _UNINITIALIZED; - }; - + it('should serialize object after qrl', async () => { const parent = { - child: uninitializedObject, - }; - const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent]); - expect(await dump(qrl, parent)).toMatchInlineSnapshot(` - " - 0 QRL "mock-chunk#dump_qrl[1]" - 1 Object [ - String "child" - Object [ - String "should" - String "serialize" - ] - ] - (74 chars)" - `); - }); - it('should serialize nested root ref object before qrl', async () => { - const uninitializedObject = { - should: 'serialize', - }; - (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { - return _UNINITIALIZED; + child: { should: 'serialize' }, }; - const parent = { - child: uninitializedObject, - }; - const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent]); - expect(await dump(parent, uninitializedObject, qrl)).toMatchInlineSnapshot(` - " - 0 Object [ - String "child" - RootRef 1 - ] - 1 ForwardRef 0 - 2 QRL "mock-chunk#dump_qrl[0]" - 3 Object [ - String "should" - String "serialize" - ] - 4 ForwardRefs [ - 3 - ] - (88 chars)" - `); - }); - it('should serialize nested root ref object after qrl', async () => { - const uninitializedObject = { - should: 'serialize', - }; - (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { - return _UNINITIALIZED; - }; + (parent as any)[SerializerSymbol] = () => ({ + child: _serializationWeakRef(parent.child), + }); - const parent = { - child: uninitializedObject, - }; - const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent]); - expect(await dump(qrl, parent, uninitializedObject)).toMatchInlineSnapshot(` + const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent.child]); + expect(await dump(qrl, parent)).toMatchInlineSnapshot(` " - 0 QRL "mock-chunk#dump_qrl[1]" + 0 QRL "mock-chunk#dump_qrl[2]" 1 Object [ String "child" - RootRef 2 + ForwardRef 0 ] 2 Object [ String "should" String "serialize" ] - (78 chars)" - `); - }); - it('should serialize nested object inside wrapped signal before qrl', async () => { - const uninitializedObject = { - should: 'serialize', - }; - (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { - return _UNINITIALIZED; - }; - - const parent = { - child: uninitializedObject, - }; - const wrappedSignal = new WrappedSignalImpl( - null, - (obj: any) => obj.child.should, - [parent], - null - ); - const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent]); - expect(await dump(wrappedSignal, qrl)).toMatchInlineSnapshot(` - " - 0 WrappedSignal [ - Number 0 - Array [ - Object [ - String "child" - ForwardRef 0 - ] - ] - Constant null - Number 3 - Constant null - ] - 1 QRL "mock-chunk#dump_qrl[2]" - 2 RootRef "0 1 0" - 3 Object [ - String "should" - String "serialize" - ] - 4 ForwardRefs [ - 3 + 3 ForwardRefs [ + 2 ] - (119 chars)" + (84 chars)" `); }); - it('should serialize nested object inside wrapped signal after qrl', async () => { - const uninitializedObject = { - should: 'serialize', - }; - (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { - return _UNINITIALIZED; - }; - const parent = { - child: uninitializedObject, - }; - const wrappedSignal = new WrappedSignalImpl( - null, - (obj: any) => obj.child.should, - [parent], - null - ); - const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent]); - expect(await dump(qrl, wrappedSignal)).toMatchInlineSnapshot(` - " - 0 QRL "mock-chunk#dump_qrl[2]" - 1 WrappedSignal [ - Number 0 - Array [ - RootRef 2 - ] - Constant null - Number 3 - Constant null - ] - 2 Object [ - String "child" - Object [ - String "should" - String "serialize" - ] - ] - (103 chars)" - `); - }); - it('should serialize nested object inside wrapped signal after qrl', async () => { - const uninitializedObject = { - should: 'serialize', - }; - (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { - return _UNINITIALIZED; - }; + // it.only('should serialize wrapped signal args', async () => { + // const parent = createStore( + // null, + // { + // child: { should: 'serialize' }, + // }, + // StoreFlags.RECURSIVE + // ); - const parent = { - child: uninitializedObject, - }; - const anotherParent = { - child: uninitializedObject, - }; - const wrappedSignal = new WrappedSignalImpl(null, (obj: any) => obj.child, [parent], null); - const qrl = inlinedQrl( - () => { - const [signal] = useLexicalScope(); - return signal.value.should; - }, - 'dump_qrl', - [wrappedSignal] - ); - expect(await dump(anotherParent, wrappedSignal, qrl)).toMatchInlineSnapshot(` - " - 0 Object [ - String "child" - ForwardRef 0 - ] - 1 WrappedSignal [ - Number 0 - Array [ - Object [ - RootRef 3 - RootRef 4 - ] - ] - Constant null - Number 3 - Constant null - ] - 2 QRL "mock-chunk#dump_qrl[1]" - 3 RootRef "0 0" - 4 RootRef "0 1" - 5 Object [ - String "should" - String "serialize" - ] - 6 ForwardRefs [ - 5 - ] - (138 chars)" - `); - }); + // (parent as any)[SerializerSymbol] = () => ({ + // child: _serializationWeakRef(parent.child), + // }); + + // const wrappedSignal = _wrapStore(parent, 'child'); + // expect(await dump(parent, wrappedSignal)).toMatchInlineSnapshot(` + // " + // 0 Store [ + // Object [ + // String "child" + // ForwardRef 0 + // ] + // Number 1 + // ] + // 1 WrappedSignal [ + // Number 0 + // Array [ + // RootRef 0 + // RootRef 2 + // ] + // Constant null + // Number 1 + // Constant null + // ] + // 2 String "child" + // 3 Object [ + // String "should" + // String "serialize" + // ] + // 4 ForwardRefs [ + // 3 + // ] + // (79 chars)" + // `); + // }); }); it(title(TypeIds.Number), async () => { expect(await dump(123)).toMatchInlineSnapshot(` @@ -964,7 +755,7 @@ describe('shared-serialization', () => { it.todo(title(TypeIds.VNode)); it(title(TypeIds.BigInt), async () => { const objs = await serialize(BigInt('12345678901234567890')); - const bi = deserialize(objs)[0] as BigInt; + const bi = deserialize(objs)[0] as bigint; expect(bi).toBeTypeOf('bigint'); expect(bi.toString()).toBe('12345678901234567890'); }); @@ -1435,7 +1226,6 @@ async function serialize(...roots: any[]): Promise { } await sCtx.$serialize$(); const objs = JSON.parse(sCtx.$writer$.toString()); - // eslint-disable-next-line no-console DEBUG && console.log(objs); return objs; }