@@ -18,9 +18,7 @@ import {
18
18
import { createRequestInit } from "./data" ;
19
19
import type { AssetsManifest , EntryContext } from "./entry" ;
20
20
import { escapeHtml } from "./markup" ;
21
- import type { RouteModule , RouteModules } from "./routeModules" ;
22
21
import invariant from "./invariant" ;
23
- import type { EntryRoute } from "./routes" ;
24
22
25
23
export const SingleFetchRedirectSymbol = Symbol ( "SingleFetchRedirect" ) ;
26
24
@@ -32,15 +30,22 @@ export type SingleFetchRedirectResult = {
32
30
replace : boolean ;
33
31
} ;
34
32
33
+ // Shared/serializable type used by both turbo-stream and RSC implementations
34
+ type DecodedSingleFetchResults =
35
+ | { routes : { [ key : string ] : SingleFetchResult } }
36
+ | { redirect : SingleFetchRedirectResult } ;
37
+
38
+ // This and SingleFetchResults are only used over the wire, and are converted to
39
+ // DecodedSingleFetchResults in `fetchAndDecode`. This way turbo-stream/RSC
40
+ // can use the same `unwrapSingleFetchResult` implementation.
35
41
export type SingleFetchResult =
36
42
| { data : unknown }
37
43
| { error : unknown }
38
44
| SingleFetchRedirectResult ;
39
45
40
- export type SingleFetchResults = {
41
- [ key : string ] : SingleFetchResult ;
42
- [ SingleFetchRedirectSymbol ] ?: SingleFetchRedirectResult ;
43
- } ;
46
+ export type SingleFetchResults =
47
+ | { [ key : string ] : SingleFetchResult }
48
+ | { [ SingleFetchRedirectSymbol ] : SingleFetchRedirectResult } ;
44
49
45
50
interface StreamTransferProps {
46
51
context : EntryContext ;
@@ -50,6 +55,16 @@ interface StreamTransferProps {
50
55
nonce ?: string ;
51
56
}
52
57
58
+ // Some status codes are not permitted to have bodies, so we want to just
59
+ // treat those as "no data" instead of throwing an exception:
60
+ // https://datatracker.ietf.org/doc/html/rfc9110#name-informational-1xx
61
+ // https://datatracker.ietf.org/doc/html/rfc9110#name-204-no-content
62
+ // https://datatracker.ietf.org/doc/html/rfc9110#name-205-reset-content
63
+ //
64
+ // Note: 304 is not included here because the browser should fill those responses
65
+ // with the cached body content.
66
+ export const NO_BODY_STATUS_CODES = new Set ( [ 100 , 101 , 204 , 205 ] ) ;
67
+
53
68
// StreamTransfer recursively renders down chunks of the `serverHandoffStream`
54
69
// into the client-side `streamController`
55
70
export function StreamTransfer ( {
@@ -245,12 +260,13 @@ async function singleFetchActionStrategy(
245
260
let result = await handler ( async ( ) => {
246
261
let url = singleFetchUrl ( request . url , basename ) ;
247
262
let init = await createRequestInit ( request ) ;
248
- let { data, status } = await fetchAndDecode ( url , init ) ;
249
- actionStatus = status ;
250
- return unwrapSingleFetchResult (
251
- data as SingleFetchResult ,
263
+ let { data, status } = await fetchAndDecode (
264
+ url ,
265
+ init ,
252
266
actionMatch ! . route . id
253
267
) ;
268
+ actionStatus = status ;
269
+ return unwrapSingleFetchResult ( data , actionMatch ! . route . id ) ;
254
270
} ) ;
255
271
return result ;
256
272
} ) ;
@@ -308,23 +324,17 @@ async function singleFetchLoaderNavigationStrategy(
308
324
router : DataRouter ,
309
325
basename : string | undefined
310
326
) {
311
- // Track which routes need a server load - in case we need to tack on a
312
- // `_routes` param
327
+ // Track which routes need a server load for use in a `_routes` param
313
328
let routesParams = new Set < string > ( ) ;
314
329
315
- // We only add `_routes` when one or more routes opts out of a load via
316
- // `shouldRevalidate` or `clientLoader`
330
+ // Only add `_routes` when at least 1 route opts out via `shouldRevalidate`/`clientLoader`
317
331
let foundOptOutRoute = false ;
318
332
319
- // Deferreds for each route so we can be sure they've all loaded via
320
- // `match.resolve()`, and a singular promise that can tell us all routes
321
- // have been resolved
333
+ // Deferreds per-route so we can be sure they've all loaded via `match.resolve()`
322
334
let routeDfds = matches . map ( ( ) => createDeferred < void > ( ) ) ;
323
- let routesLoadedPromise = Promise . all ( routeDfds . map ( ( d ) => d . promise ) ) ;
324
335
325
- // Deferred that we'll use for the call to the server that each match can
326
- // await and parse out it's specific result
327
- let singleFetchDfd = createDeferred < SingleFetchResults > ( ) ;
336
+ // Deferred we'll use for the singleular call to the server
337
+ let singleFetchDfd = createDeferred < DecodedSingleFetchResults > ( ) ;
328
338
329
339
// Base URL and RequestInit for calls to the server
330
340
let url = stripIndexParam ( singleFetchUrl ( request . url , basename ) ) ;
@@ -339,6 +349,7 @@ async function singleFetchLoaderNavigationStrategy(
339
349
routeDfds [ i ] . resolve ( ) ;
340
350
341
351
let manifestRoute = manifest . routes [ m . route . id ] ;
352
+ invariant ( manifestRoute , "No manifest route found for dataStrategy" ) ;
342
353
343
354
let defaultShouldRevalidate =
344
355
! m . unstable_shouldRevalidateArgs ||
@@ -347,8 +358,7 @@ async function singleFetchLoaderNavigationStrategy(
347
358
let shouldCall = m . unstable_shouldCallHandler ( defaultShouldRevalidate ) ;
348
359
349
360
if ( ! shouldCall ) {
350
- // If this route opted out of revalidation, we don't want to include
351
- // it in the single fetch .data request
361
+ // If this route opted out, don't include in the .data request
352
362
foundOptOutRoute ||=
353
363
m . unstable_shouldRevalidateArgs != null && // This is a revalidation,
354
364
manifestRoute ?. hasLoader === true && // for a route with a server loader,
@@ -358,7 +368,7 @@ async function singleFetchLoaderNavigationStrategy(
358
368
359
369
// When a route has a client loader, it opts out of the singular call and
360
370
// calls it's server loader via `serverLoader()` using a `?_routes` param
361
- if ( manifestRoute && manifestRoute . hasClientLoader ) {
371
+ if ( manifestRoute . hasClientLoader ) {
362
372
if ( manifestRoute . hasLoader ) {
363
373
foundOptOutRoute = true ;
364
374
}
@@ -385,7 +395,7 @@ async function singleFetchLoaderNavigationStrategy(
385
395
try {
386
396
let result = await handler ( async ( ) => {
387
397
let data = await singleFetchDfd . promise ;
388
- return unwrapSingleFetchResults ( data , m . route . id ) ;
398
+ return unwrapSingleFetchResult ( data , m . route . id ) ;
389
399
} ) ;
390
400
results [ m . route . id ] = {
391
401
type : "data" ,
@@ -402,7 +412,7 @@ async function singleFetchLoaderNavigationStrategy(
402
412
) ;
403
413
404
414
// Wait for all routes to resolve above before we make the HTTP call
405
- await routesLoadedPromise ;
415
+ await Promise . all ( routeDfds . map ( ( d ) => d . promise ) ) ;
406
416
407
417
// We can skip the server call:
408
418
// - On initial hydration - only clientLoaders can pass through via `clientLoader.hydrate`
@@ -417,24 +427,18 @@ async function singleFetchLoaderNavigationStrategy(
417
427
) {
418
428
singleFetchDfd . resolve ( { } ) ;
419
429
} else {
420
- try {
421
- // When one or more routes have opted out, we add a _routes param to
422
- // limit the loaders to those that have a server loader and did not
423
- // opt out
424
- if ( ssr && foundOptOutRoute && routesParams . size > 0 ) {
425
- url . searchParams . set (
426
- "_routes" ,
427
- matches
428
- . filter ( ( m ) => routesParams . has ( m . route . id ) )
429
- . map ( ( m ) => m . route . id )
430
- . join ( "," )
431
- ) ;
432
- }
430
+ // When routes have opted out, add a `_routes` param to filter server loaders
431
+ // Skipped in `ssr:false` because we expect to be loading static `.data` files
432
+ if ( ssr && foundOptOutRoute && routesParams . size > 0 ) {
433
+ let routes = [ ...routesParams . keys ( ) ] . join ( "," ) ;
434
+ url . searchParams . set ( "_routes" , routes ) ;
435
+ }
433
436
437
+ try {
434
438
let data = await fetchAndDecode ( url , init ) ;
435
- singleFetchDfd . resolve ( data . data as SingleFetchResults ) ;
439
+ singleFetchDfd . resolve ( data . data ) ;
436
440
} catch ( e ) {
437
- singleFetchDfd . reject ( e as Error ) ;
441
+ singleFetchDfd . reject ( e ) ;
438
442
}
439
443
}
440
444
@@ -471,7 +475,7 @@ function fetchSingleLoader(
471
475
let singleLoaderUrl = new URL ( url ) ;
472
476
singleLoaderUrl . searchParams . set ( "_routes" , routeId ) ;
473
477
let { data } = await fetchAndDecode ( singleLoaderUrl , init ) ;
474
- return unwrapSingleFetchResults ( data as SingleFetchResults , routeId ) ;
478
+ return unwrapSingleFetchResult ( data , routeId ) ;
475
479
} ) ;
476
480
}
477
481
@@ -520,8 +524,9 @@ export function singleFetchUrl(
520
524
521
525
async function fetchAndDecode (
522
526
url : URL ,
523
- init : RequestInit
524
- ) : Promise < { status : number ; data : unknown } > {
527
+ init : RequestInit ,
528
+ routeId ?: string
529
+ ) : Promise < { status : number ; data : DecodedSingleFetchResults } > {
525
530
let res = await fetch ( url , init ) ;
526
531
527
532
// If this 404'd without hitting the running server (most likely in a
@@ -530,27 +535,39 @@ async function fetchAndDecode(
530
535
throw new ErrorResponseImpl ( 404 , "Not Found" , true ) ;
531
536
}
532
537
533
- // some status codes are not permitted to have bodies, so we want to just
534
- // treat those as "no data" instead of throwing an exception.
535
- // 304 is not included here because the browser should fill those responses
536
- // with the cached body content.
537
- const NO_BODY_STATUS_CODES = new Set ( [ 100 , 101 , 204 , 205 ] ) ;
538
538
if ( NO_BODY_STATUS_CODES . has ( res . status ) ) {
539
- if ( ! init . method || init . method === "GET" ) {
540
- // SingleFetchResults can just have no routeId keys which will result
541
- // in no data for all routes
542
- return { status : res . status , data : { } } ;
543
- } else {
544
- // SingleFetchResult is for a singular route and can specify no data
545
- return { status : res . status , data : { data : undefined } } ;
539
+ let routes : { [ key : string ] : SingleFetchResult } = { } ;
540
+ if ( routeId ) {
541
+ routes [ routeId ] = { data : undefined } ;
546
542
}
543
+ return {
544
+ status : res . status ,
545
+ data : { routes } ,
546
+ } ;
547
547
}
548
548
549
549
invariant ( res . body , "No response body to decode" ) ;
550
550
551
551
try {
552
552
let decoded = await decodeViaTurboStream ( res . body , window ) ;
553
- return { status : res . status , data : decoded . value } ;
553
+ let data : DecodedSingleFetchResults ;
554
+ if ( ! init . method || init . method === "GET" ) {
555
+ let typed = decoded . value as SingleFetchResults ;
556
+ if ( SingleFetchRedirectSymbol in typed ) {
557
+ data = { redirect : typed [ SingleFetchRedirectSymbol ] } ;
558
+ } else {
559
+ data = { routes : typed } ;
560
+ }
561
+ } else {
562
+ let typed = decoded . value as SingleFetchResult ;
563
+ invariant ( routeId , "No routeId found for single fetch call decoding" ) ;
564
+ if ( "redirect" in typed ) {
565
+ data = { redirect : typed } ;
566
+ } else {
567
+ data = { routes : { [ routeId ] : typed } } ;
568
+ }
569
+ }
570
+ return { status : res . status , data } ;
554
571
} catch ( e ) {
555
572
// Can't clone after consuming the body via turbo-stream so we can't
556
573
// include the body here. In an ideal world we'd look for a turbo-stream
@@ -617,53 +634,50 @@ export function decodeViaTurboStream(
617
634
} ) ;
618
635
}
619
636
620
- function unwrapSingleFetchResults (
621
- results : SingleFetchResults ,
637
+ function unwrapSingleFetchResult (
638
+ result : DecodedSingleFetchResults ,
622
639
routeId : string
623
640
) {
624
- let redirect = results [ SingleFetchRedirectSymbol ] ;
625
- if ( redirect ) {
626
- return unwrapSingleFetchResult ( redirect , routeId ) ;
641
+ if ( "redirect" in result ) {
642
+ let {
643
+ redirect : location ,
644
+ revalidate,
645
+ reload,
646
+ replace,
647
+ status,
648
+ } = result . redirect ;
649
+ throw redirect ( location , {
650
+ status,
651
+ headers : {
652
+ // Three R's of redirecting (lol Veep)
653
+ ...( revalidate ? { "X-Remix-Revalidate" : "yes" } : null ) ,
654
+ ...( reload ? { "X-Remix-Reload-Document" : "yes" } : null ) ,
655
+ ...( replace ? { "X-Remix-Replace" : "yes" } : null ) ,
656
+ } ,
657
+ } ) ;
627
658
}
628
659
629
- return results [ routeId ] !== undefined
630
- ? unwrapSingleFetchResult ( results [ routeId ] , routeId )
631
- : null ;
632
- }
633
-
634
- function unwrapSingleFetchResult ( result : SingleFetchResult , routeId : string ) {
635
- if ( "error" in result ) {
636
- throw result . error ;
637
- } else if ( "redirect" in result ) {
638
- let headers : Record < string , string > = { } ;
639
- if ( result . revalidate ) {
640
- headers [ "X-Remix-Revalidate" ] = "yes" ;
641
- }
642
- if ( result . reload ) {
643
- headers [ "X-Remix-Reload-Document" ] = "yes" ;
644
- }
645
- if ( result . replace ) {
646
- headers [ "X-Remix-Replace" ] = "yes" ;
647
- }
648
- throw redirect ( result . redirect , { status : result . status , headers } ) ;
649
- } else if ( "data" in result ) {
650
- return result . data ;
660
+ let routeResult = result . routes [ routeId ] ;
661
+ if ( "error" in routeResult ) {
662
+ throw routeResult . error ;
663
+ } else if ( "data" in routeResult ) {
664
+ return routeResult . data ;
651
665
} else {
652
666
throw new Error ( `No response found for routeId "${ routeId } "` ) ;
653
667
}
654
668
}
655
669
656
670
function createDeferred < T = unknown > ( ) {
657
671
let resolve : ( val ?: any ) => Promise < void > ;
658
- let reject : ( error ?: Error ) => Promise < void > ;
672
+ let reject : ( error ?: unknown ) => Promise < void > ;
659
673
let promise = new Promise < T > ( ( res , rej ) => {
660
674
resolve = async ( val : T ) => {
661
675
res ( val ) ;
662
676
try {
663
677
await promise ;
664
678
} catch ( e ) { }
665
679
} ;
666
- reject = async ( error ?: Error ) => {
680
+ reject = async ( error ?: unknown ) => {
667
681
rej ( error ) ;
668
682
try {
669
683
await promise ;
0 commit comments