Skip to content

Commit 042e959

Browse files
committed
WIP
1 parent a3048bd commit 042e959

33 files changed

Lines changed: 759 additions & 526 deletions

.changeset/green-experts-turn.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
'@qwik.dev/router': minor
2+
3+
Refactor route loaders to be backed by shared async signals across SSR, client refresh, and action invalidation.

packages/docs/src/routes/api/qwik-router/api.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,7 @@
698698
}
699699
],
700700
"kind": "Interface",
701-
"content": "```typescript\nexport interface QwikRouterEnvData \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nev\n\n\n</td><td>\n\n\n</td><td>\n\nRequestEvent\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nloadedRoute\n\n\n</td><td>\n\n\n</td><td>\n\nLoadedRoute\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nparams\n\n\n</td><td>\n\n\n</td><td>\n\n[PathParams](#pathparams)\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nresponse\n\n\n</td><td>\n\n\n</td><td>\n\nEndpointResponse\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nrouteName\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>",
701+
"content": "```typescript\nexport interface QwikRouterEnvData \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nev\n\n\n</td><td>\n\n\n</td><td>\n\nRequestEvent\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nloadedRoute\n\n\n</td><td>\n\n\n</td><td>\n\nLoadedRoute\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nloaderState\n\n\n</td><td>\n\n\n</td><td>\n\nRecord&lt;string, import('@qwik.dev/core/internal').AsyncSignal&lt;unknown&gt;&gt;\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nparams\n\n\n</td><td>\n\n\n</td><td>\n\n[PathParams](#pathparams)\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nresponse\n\n\n</td><td>\n\n\n</td><td>\n\nEndpointResponse\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nrouteLoaderCtx\n\n\n</td><td>\n\n\n</td><td>\n\nimport('./route-loaders').RouteLoaderCtx\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nrouteName\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>",
702702
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts",
703703
"mdFile": "router.qwikrouterenvdata.md"
704704
},
@@ -895,7 +895,7 @@
895895
],
896896
"kind": "Variable",
897897
"content": "```typescript\nrouteLoader$: LoaderConstructor\n```",
898-
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/server-functions.ts",
898+
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/route-loaders.ts",
899899
"mdFile": "router.routeloader_.md"
900900
},
901901
{

packages/docs/src/routes/api/qwik-router/index.mdx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1606,6 +1606,19 @@ LoadedRoute
16061606
</td></tr>
16071607
<tr><td>
16081608

1609+
loaderState
1610+
1611+
</td><td>
1612+
1613+
</td><td>
1614+
1615+
Record&lt;string, import('@qwik.dev/core/internal').AsyncSignal&lt;unknown&gt;&gt;
1616+
1617+
</td><td>
1618+
1619+
</td></tr>
1620+
<tr><td>
1621+
16091622
params
16101623

16111624
</td><td>
@@ -1632,6 +1645,19 @@ EndpointResponse
16321645
</td></tr>
16331646
<tr><td>
16341647

1648+
routeLoaderCtx
1649+
1650+
</td><td>
1651+
1652+
</td><td>
1653+
1654+
import('./route-loaders').RouteLoaderCtx
1655+
1656+
</td><td>
1657+
1658+
</td></tr>
1659+
<tr><td>
1660+
16351661
routeName
16361662

16371663
</td><td>
@@ -2312,7 +2338,7 @@ _(Optional)_ Array of routeLoader$ hashes for this node's loaders
23122338
routeLoader$: LoaderConstructor;
23132339
```
23142340

2315-
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/server-functions.ts)
2341+
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/route-loaders.ts)
23162342

23172343
<h2 id="routelocation">RouteLocation</h2>
23182344

packages/qwik-router/src/buildtime/runtime-generation/generate-routes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,12 @@ function serializeBuildTrie(
324324
const routeFiles = node._files
325325
.filter((f) => f.type === 'route' || f.type === 'layout')
326326
.map((f) => f.filePath);
327+
// Include server plugin files at the root trie node (they apply to all routes)
328+
if (node === ctx.routeTrie) {
329+
for (const plugin of ctx.serverPlugins) {
330+
routeFiles.push(plugin.filePath);
331+
}
332+
}
327333
if (routeFiles.length > 0) {
328334
const nodeLoaderHashes: string[] = [];
329335
if (loadersByFile) {

packages/qwik-router/src/middleware/request-handler/handlers/action-handler.ts

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
1-
import { _serialize, _UNINITIALIZED, _verifySerializable, isDev } from '@qwik.dev/core/internal';
1+
import { _serialize, _verifySerializable, isDev } from '@qwik.dev/core/internal';
22
import type {
33
ActionInternal,
44
DataValidator,
55
JSONObject,
6-
LoaderInternal,
76
RequestEvent,
87
RequestHandler,
98
ValidatorReturn,
109
} from '../../../runtime/src/types';
11-
import {
12-
getRequestLoaders,
13-
getRequestLoaderSerializationStrategyMap,
14-
type RequestEventInternal,
15-
} from '../request-event-core';
16-
import { getRouteLoaderPromise } from '../request-loader';
10+
import { type RequestEventInternal } from '../request-event-core';
1711
import { IsQAction, QActionId } from '../request-path';
1812
import type { QRL } from '@qwik.dev/core';
1913
import type { RequestEventBase } from '../types';
@@ -26,7 +20,7 @@ import type { RequestEventBase } from '../types';
2620
*/
2721
export function actionHandler(
2822
routeActions: ActionInternal[],
29-
routeLoaders: LoaderInternal[]
23+
routeLoaderHashes: string[]
3024
): RequestHandler {
3125
return async (requestEvent: RequestEvent) => {
3226
const requestEv = requestEvent as RequestEventInternal;
@@ -60,8 +54,6 @@ export function actionHandler(
6054
return;
6155
}
6256

63-
const loaders = getRequestLoaders(requestEv);
64-
6557
// Find the action
6658
let action: ActionInternal | undefined;
6759
for (const routeAction of routeActions) {
@@ -89,9 +81,10 @@ export function actionHandler(
8981
throw new Error(`Expected request data for the action id ${actionId} to be an object`);
9082
}
9183

84+
let actionResult: unknown;
9285
const result = await runValidators(requestEv, action.__validators, data, devMode);
9386
if (!result.success) {
94-
loaders[actionId] = requestEv.fail(result.status ?? 500, result.error);
87+
actionResult = requestEv.fail(result.status ?? 500, result.error);
9588
} else {
9689
const actionResolved = devMode
9790
? await measure(requestEv, action.__qrl.getHash(), () =>
@@ -101,22 +94,15 @@ export function actionHandler(
10194
if (devMode) {
10295
verifySerializable(actionResolved, action.__qrl);
10396
}
104-
loaders[actionId] = actionResolved;
105-
}
106-
107-
// Run all route loaders after the action so they can see the action result
108-
// via resolveValue() (loaders may depend on action results)
109-
const loadersSerializationStrategy = getRequestLoaderSerializationStrategyMap(requestEv);
110-
if (routeLoaders.length > 0) {
111-
const loaderPromises = routeLoaders.map((loader) =>
112-
getRouteLoaderPromise(loader, loaders, loadersSerializationStrategy, requestEv)
113-
);
114-
await Promise.all(loaderPromises);
97+
actionResult = actionResolved;
11598
}
99+
requestEv.sharedMap.set('@actionResult', actionResult);
116100

117-
// Return action result + all loader results as JSON
118-
// The client uses this to update all loader signals after an action
119-
const serialized = await _serialize(loaders);
101+
const serialized = await _serialize({
102+
result: actionResult,
103+
// TODO get via `invalidate: [useUser, useEtc]` option
104+
loaderHashes: routeLoaderHashes,
105+
});
120106
requestEv.headers.set('Content-Type', 'application/json; charset=utf-8');
121107
requestEv.send(200, serialized);
122108
};

packages/qwik-router/src/middleware/request-handler/handlers/loader-handler.ts

Lines changed: 26 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,25 @@
1-
import { _serialize, _UNINITIALIZED, type ValueOrPromise } from '@qwik.dev/core/internal';
1+
import { _serialize } from '@qwik.dev/core/internal';
22
import type {
33
ActionInternal,
44
LoaderInternal,
55
RequestEvent,
66
RequestHandler,
77
} from '../../../runtime/src/types';
8-
import {
9-
getRequestLoaders,
10-
getRequestLoaderSerializationStrategyMap,
11-
type RequestEventInternal,
12-
} from '../request-event-core';
13-
import { getRouteLoaderPromise } from '../request-loader';
8+
import { type RequestEventInternal } from '../request-event-core';
149
import { IsQLoader, QLoaderId } from '../request-path';
15-
16-
/**
17-
* Middleware that executes ALL route loaders (used during SSR page rendering). This is the same as
18-
* the existing loadersMiddleware but extracted for clarity.
19-
*/
20-
export function loadersMiddleware(routeLoaders: LoaderInternal[]): RequestHandler {
21-
return async (requestEvent: RequestEvent) => {
22-
const requestEv = requestEvent as RequestEventInternal;
23-
if (requestEv.headersSent) {
24-
requestEv.exit();
25-
return;
26-
}
27-
const loaders = getRequestLoaders(requestEv);
28-
const loadersSerializationStrategy = getRequestLoaderSerializationStrategyMap(requestEv);
29-
if (routeLoaders.length > 0) {
30-
const resolvedLoadersPromises = routeLoaders.map((loader) =>
31-
getRouteLoaderPromise(loader, loaders, loadersSerializationStrategy, requestEv)
32-
);
33-
await Promise.all(resolvedLoadersPromises);
34-
}
35-
};
36-
}
10+
import {
11+
getRouteLoaderData,
12+
resolveRouteLoaderByHash,
13+
LOADER_URL_HEADER,
14+
} from '../../../runtime/src/route-loaders';
3715

3816
/**
3917
* Handler for individual loader fetch requests (`/path/q-loader-{id}.{hash}.json`). Runs only the
4018
* requested loader and returns its serialized result as JSON.
4119
*/
4220
export function loaderHandler(
4321
routeLoaders: LoaderInternal[],
44-
routeActions: ActionInternal[]
22+
_routeActions: ActionInternal[]
4523
): RequestHandler {
4624
return async (requestEvent: RequestEvent) => {
4725
const requestEv = requestEvent as RequestEventInternal;
@@ -55,30 +33,31 @@ export function loaderHandler(
5533
}
5634

5735
const loaderId = requestEv.sharedMap.get(QLoaderId) as string;
58-
const loaders = getRequestLoaders(requestEv);
59-
const loadersSerializationStrategy = getRequestLoaderSerializationStrategyMap(requestEv);
60-
61-
// Find the requested loader
62-
let loader: LoaderInternal | undefined;
63-
for (const routeLoader of routeLoaders) {
64-
if (routeLoader.__id === loaderId) {
65-
loader = routeLoader;
66-
} else if (!loaders[routeLoader.__id]) {
67-
// Other loaders set to _UNINITIALIZED so resolveValue() can trigger them on demand
68-
loaders[routeLoader.__id] = _UNINITIALIZED as unknown as ValueOrPromise<unknown>;
69-
}
70-
}
36+
const loader = resolveRouteLoaderByHash(routeLoaders, loaderId);
7137

7238
if (!loader) {
7339
requestEv.json(404, { error: 'Loader not found' });
7440
return;
7541
}
7642

77-
// Execute the loader
78-
await getRouteLoaderPromise(loader, loaders, loadersSerializationStrategy, requestEv);
43+
// Use the X-Qwik-Loader-URL header to reconstruct the actual page URL.
44+
// The physical request goes to /path/q-loader-{id}.{hash}.json but the loader
45+
// function should see the real page URL (with search params, etc.)
46+
const loaderUrl = requestEv.request.headers.get(LOADER_URL_HEADER);
47+
if (loaderUrl) {
48+
try {
49+
const pageUrl = new URL(loaderUrl, requestEv.url.origin);
50+
// Override URL properties so the loader sees the real page URL
51+
requestEv.url.pathname = pageUrl.pathname;
52+
requestEv.url.search = pageUrl.search;
53+
requestEv.url.hash = pageUrl.hash;
54+
} catch {
55+
// Invalid URL header — ignore and use the trimmed URL
56+
}
57+
}
7958

80-
// Serialize and return just this loader's result
81-
const data = await _serialize([loaders[loaderId]]);
59+
const result = await getRouteLoaderData(loader.__qrl, loader.__validators, requestEv);
60+
const data = await _serialize([result]);
8261
requestEv.headers.set('Content-Type', 'application/json; charset=utf-8');
8362

8463
// Set cache headers based on loader's expires option

packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import type { RenderOptions } from '@qwik.dev/core/server';
1616
import { RequestEvent as RequestEvent_2 } from '@qwik.dev/router/middleware/request-handler';
1717
import type { RequestHandler as RequestHandler_2 } from '@qwik.dev/router/middleware/request-handler';
1818
import type { ResolveSyncValue as ResolveSyncValue_2 } from '@qwik.dev/router/middleware/request-handler';
19-
import { SerializationStrategy } from '@qwik.dev/core/internal';
2019
import type { ValueOrPromise } from '@qwik.dev/core';
2120

2221
// @public (undocumented)

packages/qwik-router/src/middleware/request-handler/request-event-core.ts

Lines changed: 14 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
import type { ValueOrPromise } from '@qwik.dev/core';
2-
import {
3-
_deserialize,
4-
_UNINITIALIZED,
5-
isDev,
6-
type SerializationStrategy,
7-
} from '@qwik.dev/core/internal';
1+
import { _deserialize, isDev } from '@qwik.dev/core/internal';
82
import type {
93
ActionInternal,
104
FailReturn,
115
JSONValue,
126
LoadedRoute,
137
LoaderInternal,
148
} from '../../runtime/src/types';
9+
import {
10+
ensureRouteLoaderSignal,
11+
getRouteLoaderState,
12+
getRouteLoaderCtx,
13+
} from '../../runtime/src/route-loaders';
1514
import type { AbortMessage, RedirectMessage } from './redirect-handler';
1615
import type { RewriteMessage } from './rewrite-handler';
1716
import type { ServerError } from './server-error';
@@ -37,7 +36,6 @@ interface RequestEventDeps {
3736
RedirectMessage: new () => RedirectMessage;
3837
RewriteMessage: new (pathname: string) => RewriteMessage;
3938
ServerError: new <T = any>(status: number, data: T) => ServerError<T>;
40-
getRouteLoaderPromise: typeof import('./request-loader').getRouteLoaderPromise;
4139
recognizeRequest: typeof import('./request-path').recognizeRequest;
4240
IsQLoader: string;
4341
IsQAction: string;
@@ -48,12 +46,8 @@ interface RequestEventDeps {
4846
getContentType: typeof import('./request-utils').getContentType;
4947
}
5048

51-
const RequestEvLoaders = Symbol('RequestEvLoaders');
5249
const RequestEvMode = Symbol('RequestEvMode');
5350
const RequestEvRoute = Symbol('RequestEvRoute');
54-
export const RequestEvLoaderSerializationStrategyMap = Symbol(
55-
'RequestEvLoaderSerializationStrategyMap'
56-
);
5751
export const RequestRouteName = '@routeName';
5852
export const RequestEvSharedActionId = '@actionId';
5953
export const RequestEvSharedActionFormData = '@actionFormData';
@@ -181,10 +175,7 @@ export function createRequestEventWithDeps(
181175
return message;
182176
};
183177

184-
const loaders: Record<string, ValueOrPromise<unknown> | undefined> = {};
185178
const requestEv: RequestEventInternal = {
186-
[RequestEvLoaders]: loaders,
187-
[RequestEvLoaderSerializationStrategyMap]: new Map(),
188179
[RequestEvMode]: serverRequestEv.mode,
189180
get [RequestEvRoute]() {
190181
return loadedRoute;
@@ -231,25 +222,18 @@ export function createRequestEventWithDeps(
231222
},
232223

233224
resolveValue: (async (loaderOrAction: LoaderInternal | ActionInternal) => {
234-
// create user request event, which is a narrowed down request context
235-
const id = loaderOrAction.__id;
236225
if (loaderOrAction.__brand === 'server_loader') {
237-
if (!(id in loaders)) {
238-
throw new Error(
239-
'You can not get the returned data of a loader that has not been executed for this request.'
240-
);
241-
}
242-
if (loaders[id] === _UNINITIALIZED) {
243-
await deps.getRouteLoaderPromise(
244-
loaderOrAction,
245-
loaders,
246-
requestEv[RequestEvLoaderSerializationStrategyMap],
247-
requestEv
248-
);
226+
const loaderState = getRouteLoaderState(requestEv);
227+
const routeLoaderCtx = getRouteLoaderCtx(requestEv);
228+
const signal = ensureRouteLoaderSignal(loaderOrAction, loaderState, routeLoaderCtx);
229+
if (signal.loading) {
230+
await signal.promise();
249231
}
232+
return signal.value;
250233
}
251234

252-
return loaders[id];
235+
// Action result
236+
return requestEv.sharedMap.get('@actionResult');
253237
}) as ResolveValue,
254238

255239
status: (statusCode?: number) => {
@@ -379,8 +363,6 @@ export function createRequestEventWithDeps(
379363
}
380364

381365
export interface RequestEventInternal extends Readonly<RequestEvent>, Readonly<RequestEventLoader> {
382-
readonly [RequestEvLoaders]: Record<string, ValueOrPromise<unknown> | undefined>;
383-
readonly [RequestEvLoaderSerializationStrategyMap]: Map<string, SerializationStrategy>;
384366
readonly [RequestEvMode]: ServerRequestMode;
385367
readonly [RequestEvRoute]: LoadedRoute;
386368

@@ -405,14 +387,6 @@ export interface RequestEventInternal extends Readonly<RequestEvent>, Readonly<R
405387
): void;
406388
}
407389

408-
export function getRequestLoaders(requestEv: RequestEventCommon) {
409-
return (requestEv as RequestEventInternal)[RequestEvLoaders];
410-
}
411-
412-
export function getRequestLoaderSerializationStrategyMap(requestEv: RequestEventCommon) {
413-
return (requestEv as RequestEventInternal)[RequestEvLoaderSerializationStrategyMap];
414-
}
415-
416390
export function getRequestRoute(requestEv: RequestEventCommon) {
417391
return (requestEv as RequestEventInternal)[RequestEvRoute];
418392
}

0 commit comments

Comments
 (0)