1
- import { _deserialize } from '@qwik.dev/core/internal' ;
1
+ import { _deserialize , isDev } from '@qwik.dev/core/internal' ;
2
2
import type { QData } from '../../middleware/request-handler/handlers/qdata-handler' ;
3
3
import { preloadRouteBundles } from './client-navigate' ;
4
4
import { QACTION_KEY } from './constants' ;
5
5
import type { ClientPageData , RouteActionValue } from './types' ;
6
6
7
+ class ShouldRedirect < T > {
8
+ constructor (
9
+ public location : string ,
10
+ public data : T
11
+ ) { }
12
+ }
13
+
7
14
interface LoaderDataResponse {
8
15
id : string ;
9
16
route : string ;
10
17
}
11
18
19
+ interface RedirectContext {
20
+ promise : Promise < unknown > | undefined ;
21
+ }
22
+
12
23
export const loadClientLoaderData = async ( url : URL , loaderId : string , manifestHash : string ) => {
13
24
const pagePathname = url . pathname . endsWith ( '/' ) ? url . pathname : url . pathname + '/' ;
14
- return fetchLoader ( loaderId , pagePathname , manifestHash ) ;
25
+ const abortController = new AbortController ( ) ;
26
+ return fetchLoader ( loaderId , pagePathname , manifestHash , abortController , { promise : undefined } ) ;
15
27
} ;
16
28
17
29
export const loadClientData = async (
@@ -20,6 +32,7 @@ export const loadClientData = async (
20
32
opts ?: {
21
33
action ?: RouteActionValue ;
22
34
loaderIds ?: string [ ] ;
35
+ redirectData ?: ShouldRedirect < LoaderDataResponse [ ] > ;
23
36
clearCache ?: boolean ;
24
37
preloadRouteBundles ?: boolean ;
25
38
isPrefetch ?: boolean ;
@@ -34,33 +47,69 @@ export const loadClientData = async (
34
47
let resolveFn : ( ) => void | undefined ;
35
48
let actionData : unknown ;
36
49
if ( opts ?. action ) {
37
- const actionResult = await fetchActionData ( opts . action , pagePathname , url . searchParams ) ;
38
- actionData = actionResult . data ;
39
- resolveFn = ( ) => {
40
- opts . action ! . resolve ! ( { status : actionResult . status , result : actionData } ) ;
41
- } ;
50
+ try {
51
+ const actionResult = await fetchActionData ( opts . action , pagePathname , url . searchParams ) ;
52
+ actionData = actionResult . data ;
53
+ resolveFn = ( ) => {
54
+ opts . action ! . resolve ! ( { status : actionResult . status , result : actionData } ) ;
55
+ } ;
56
+ } catch ( e ) {
57
+ if ( e instanceof ShouldRedirect ) {
58
+ const newUrl = new URL ( e . location , url ) ;
59
+ const newOpts = {
60
+ ...opts ,
61
+ action : undefined ,
62
+ loaderIds : undefined ,
63
+ redirectData : e ,
64
+ } ;
65
+ return loadClientData ( newUrl , manifestHash , newOpts ) ;
66
+ } else {
67
+ throw e ;
68
+ }
69
+ }
42
70
} else {
43
71
let loaderData : LoaderDataResponse [ ] = [ ] ;
44
- if ( ! opts ?. loaderIds ) {
45
- // we need to load all the loaders
46
- // first we need to get the loader urls
47
- loaderData = ( await fetchLoaderData ( pagePathname , manifestHash ) ) . loaderData ;
48
- } else {
72
+ if ( opts && opts . loaderIds ) {
49
73
loaderData = opts . loaderIds . map ( ( loaderId ) => {
50
74
return {
51
75
id : loaderId ,
52
76
route : pagePathname ,
53
77
} ;
54
78
} ) ;
79
+ } else if ( opts ?. redirectData ?. data ) {
80
+ loaderData = opts . redirectData . data ;
81
+ } else {
82
+ // we need to load all the loaders
83
+ // first we need to get the loader urls
84
+ loaderData = ( await fetchLoaderData ( pagePathname , manifestHash ) ) . loaderData ;
55
85
}
56
86
if ( loaderData . length > 0 ) {
57
87
// load specific loaders
58
- const loaderPromises = loaderData . map ( ( loader ) =>
59
- fetchLoader ( loader . id , loader . route , manifestHash )
60
- ) ;
61
- const loaderResults = await Promise . all ( loaderPromises ) ;
62
- for ( let i = 0 ; i < loaderData . length ; i ++ ) {
63
- loaders [ loaderData [ i ] . id ] = loaderResults [ i ] ;
88
+ const abortController = new AbortController ( ) ;
89
+ const redirectContext : RedirectContext = { promise : undefined } ;
90
+ try {
91
+ const loaderPromises = loaderData . map ( ( loader ) =>
92
+ fetchLoader ( loader . id , loader . route , manifestHash , abortController , redirectContext )
93
+ ) ;
94
+ const loaderResults = await Promise . all ( loaderPromises ) ;
95
+ for ( let i = 0 ; i < loaderData . length ; i ++ ) {
96
+ loaders [ loaderData [ i ] . id ] = loaderResults [ i ] ;
97
+ }
98
+ } catch ( e ) {
99
+ if ( e instanceof ShouldRedirect ) {
100
+ const newUrl = new URL ( e . location , url ) ;
101
+ const newOpts = {
102
+ ...opts ,
103
+ action : undefined ,
104
+ loaderIds : undefined ,
105
+ redirectData : e ,
106
+ } ;
107
+ return loadClientData ( newUrl , manifestHash , newOpts ) ;
108
+ } else if ( e instanceof Error && e . name === 'AbortError' ) {
109
+ // Expected, do nothing
110
+ } else {
111
+ throw e ;
112
+ }
64
113
}
65
114
}
66
115
}
@@ -118,24 +167,57 @@ export async function fetchLoaderData(
118
167
) : Promise < { loaderData : LoaderDataResponse [ ] } > {
119
168
const url = `${ routePath } q-loader-data.${ manifestHash } .json` ;
120
169
const response = await fetch ( url ) ;
170
+
171
+ if ( ! response . ok ) {
172
+ if ( isDev ) {
173
+ throw new Error ( `Failed to load loader data for ${ routePath } : ${ response . status } ` ) ;
174
+ }
175
+ return { loaderData : [ ] } ;
176
+ }
121
177
return response . json ( ) ;
122
178
}
123
179
124
180
export async function fetchLoader (
125
181
loaderId : string ,
126
182
routePath : string ,
127
- manifestHash : string
183
+ manifestHash : string ,
184
+ abortController : AbortController ,
185
+ redirectContext : RedirectContext
128
186
) : Promise < unknown > {
129
187
const url = `${ routePath } q-loader-${ loaderId } .${ manifestHash } .json` ;
130
188
131
- const response = await fetch ( url ) ;
189
+ const response = await fetch ( url , {
190
+ signal : abortController . signal ,
191
+ } ) ;
192
+
193
+ if ( response . redirected ) {
194
+ if ( ! redirectContext . promise ) {
195
+ redirectContext . promise = response . json ( ) ;
196
+
197
+ abortController . abort ( ) ;
198
+
199
+ const data = await redirectContext . promise ;
200
+ // remove the q-loader-XY.json from the url and keep the rest of the url
201
+ // the url is like this: https://localhost:3000/q-loader-XY.json
202
+ // we need to remove the q-loader-XY.json and keep the rest of the url
203
+ // the new url is like this: https://localhost:3000/
204
+ const newUrl = new URL ( response . url ) ;
205
+ newUrl . pathname = newUrl . pathname . replace ( `q-loader-data.${ manifestHash } .json` , '' ) ;
206
+ throw new ShouldRedirect (
207
+ newUrl . pathname ,
208
+ ( data as { loaderData : LoaderDataResponse [ ] } ) . loaderData
209
+ ) ;
210
+ }
211
+ }
132
212
if ( ! response . ok ) {
133
- throw new Error ( `Failed to load ${ loaderId } : ${ response . status } ` ) ;
213
+ if ( isDev ) {
214
+ throw new Error ( `Failed to load ${ loaderId } : ${ response . status } ` ) ;
215
+ }
216
+ return undefined ;
134
217
}
135
218
136
219
const text = await response . text ( ) ;
137
220
const [ data ] = _deserialize ( text , document . documentElement ) as [ Record < string , unknown > ] ;
138
-
139
221
return data ;
140
222
}
141
223
@@ -159,6 +241,12 @@ export async function fetchActionData(
159
241
action . data = undefined ;
160
242
const response = await fetch ( url , fetchOptions ) ;
161
243
244
+ if ( response . redirected ) {
245
+ const newUrl = new URL ( response . url ) ;
246
+ newUrl . pathname = newUrl . pathname . replace ( 'q-data.json' , '' ) ;
247
+ throw new ShouldRedirect ( newUrl . pathname , undefined ) ;
248
+ }
249
+
162
250
const text = await response . text ( ) ;
163
251
const [ data ] = _deserialize ( text , document . documentElement ) as [ Record < string , unknown > ] ;
164
252
0 commit comments