@@ -31,15 +31,22 @@ export type SingleFetchRedirectResult = {
3131 replace : boolean ;
3232} ;
3333
34+ // Shared/serializable type used by both turbo-stream and RSC implementations
35+ type AgnosticSingleFetchResults =
36+ | { routes : { [ key : string ] : SingleFetchResult } }
37+ | { redirect : SingleFetchRedirectResult } ;
38+
39+ // This and SingleFetchResults are only used over the wire, and are converted to
40+ // AgnosticSingleFetchResults in `fethAndDecode`. This way turbo-stream/RSC
41+ // can use the same `unwrapSingleFetchResult` implementation
3442export type SingleFetchResult =
3543 | { data : unknown }
3644 | { error : unknown }
3745 | SingleFetchRedirectResult ;
3846
39- export type SingleFetchResults = {
40- [ key : string ] : SingleFetchResult ;
41- [ SingleFetchRedirectSymbol ] ?: SingleFetchRedirectResult ;
42- } ;
47+ export type SingleFetchResults =
48+ | { [ key : string ] : SingleFetchResult }
49+ | { [ SingleFetchRedirectSymbol ] : SingleFetchRedirectResult } ;
4350
4451interface StreamTransferProps {
4552 context : EntryContext ;
@@ -249,12 +256,13 @@ async function singleFetchActionStrategy(
249256 let result = await handler ( async ( ) => {
250257 let url = singleFetchUrl ( request . url , basename ) ;
251258 let init = await createRequestInit ( request ) ;
252- let { data, status } = await fetchAndDecode ( url , init ) ;
253- actionStatus = status ;
254- return unwrapSingleFetchResult (
255- data as SingleFetchResult ,
259+ let { data, status } = await fetchAndDecode (
260+ url ,
261+ init ,
256262 actionMatch ! . route . id
257263 ) ;
264+ actionStatus = status ;
265+ return unwrapSingleFetchResult ( data , actionMatch ! . route . id ) ;
258266 } ) ;
259267 return result ;
260268 } ) ;
@@ -325,7 +333,7 @@ async function singleFetchLoaderNavigationStrategy(
325333 let routeDfds = matches . map ( ( ) => createDeferred < void > ( ) ) ;
326334
327335 // Deferred we'll use for the singleular call to the server
328- let singleFetchDfd = createDeferred < SingleFetchResults > ( ) ;
336+ let singleFetchDfd = createDeferred < AgnosticSingleFetchResults > ( ) ;
329337
330338 // Base URL and RequestInit for calls to the server
331339 let url = stripIndexParam ( singleFetchUrl ( request . url , basename ) ) ;
@@ -395,7 +403,7 @@ async function singleFetchLoaderNavigationStrategy(
395403 try {
396404 let result = await handler ( async ( ) => {
397405 let data = await singleFetchDfd . promise ;
398- return unwrapSingleFetchResults ( data , m . route . id ) ;
406+ return unwrapSingleFetchResult ( data , m . route . id ) ;
399407 } ) ;
400408 results [ m . route . id ] = {
401409 type : "data" ,
@@ -436,7 +444,7 @@ async function singleFetchLoaderNavigationStrategy(
436444
437445 try {
438446 let data = await fetchAndDecode ( url , init ) ;
439- singleFetchDfd . resolve ( data . data as SingleFetchResults ) ;
447+ singleFetchDfd . resolve ( data . data ) ;
440448 } catch ( e ) {
441449 singleFetchDfd . reject ( e ) ;
442450 }
@@ -475,7 +483,7 @@ function fetchSingleLoader(
475483 let singleLoaderUrl = new URL ( url ) ;
476484 singleLoaderUrl . searchParams . set ( "_routes" , routeId ) ;
477485 let { data } = await fetchAndDecode ( singleLoaderUrl , init ) ;
478- return unwrapSingleFetchResults ( data as SingleFetchResults , routeId ) ;
486+ return unwrapSingleFetchResult ( data , routeId ) ;
479487 } ) ;
480488}
481489
@@ -524,8 +532,9 @@ export function singleFetchUrl(
524532
525533async function fetchAndDecode (
526534 url : URL ,
527- init : RequestInit
528- ) : Promise < { status : number ; data : unknown } > {
535+ init : RequestInit ,
536+ routeId ?: string
537+ ) : Promise < { status : number ; data : AgnosticSingleFetchResults } > {
529538 let res = await fetch ( url , init ) ;
530539
531540 // If this 404'd without hitting the running server (most likely in a
@@ -540,21 +549,38 @@ async function fetchAndDecode(
540549 // with the cached body content.
541550 const NO_BODY_STATUS_CODES = new Set ( [ 100 , 101 , 204 , 205 ] ) ;
542551 if ( NO_BODY_STATUS_CODES . has ( res . status ) ) {
543- if ( ! init . method || init . method === "GET" ) {
544- // SingleFetchResults can just have no routeId keys which will result
545- // in no data for all routes
546- return { status : res . status , data : { } } ;
547- } else {
548- // SingleFetchResult is for a singular route and can specify no data
549- return { status : res . status , data : { data : undefined } } ;
552+ let routes : { [ key : string ] : SingleFetchResult } = { } ;
553+ if ( routeId ) {
554+ routes [ routeId ] = { data : undefined } ;
550555 }
556+ return {
557+ status : res . status ,
558+ data : { routes } ,
559+ } ;
551560 }
552561
553562 invariant ( res . body , "No response body to decode" ) ;
554563
555564 try {
556565 let decoded = await decodeViaTurboStream ( res . body , window ) ;
557- return { status : res . status , data : decoded . value } ;
566+ let data : AgnosticSingleFetchResults ;
567+ if ( ! init . method || init . method === "GET" ) {
568+ let typed = decoded . value as SingleFetchResults ;
569+ if ( SingleFetchRedirectSymbol in typed ) {
570+ data = { redirect : typed [ SingleFetchRedirectSymbol ] } ;
571+ } else {
572+ data = { routes : typed } ;
573+ }
574+ } else {
575+ let typed = decoded . value as SingleFetchResult ;
576+ invariant ( routeId , "No routeId found for single fetch call decoding" ) ;
577+ if ( "redirect" in typed ) {
578+ data = { redirect : typed } ;
579+ } else {
580+ data = { routes : { [ routeId ] : typed } } ;
581+ }
582+ }
583+ return { status : res . status , data } ;
558584 } catch ( e ) {
559585 // Can't clone after consuming the body via turbo-stream so we can't
560586 // include the body here. In an ideal world we'd look for a turbo-stream
@@ -621,43 +647,39 @@ export function decodeViaTurboStream(
621647 } ) ;
622648}
623649
624- function unwrapSingleFetchResults (
625- results : SingleFetchResults ,
650+ function unwrapSingleFetchResult (
651+ result : AgnosticSingleFetchResults ,
626652 routeId : string
627653) {
628- let redirect = results [ SingleFetchRedirectSymbol ] ;
629- if ( redirect ) {
630- return unwrapSingleFetchResult ( redirect , routeId ) ;
654+ if ( "redirect" in result ) {
655+ let {
656+ redirect : location ,
657+ revalidate,
658+ reload,
659+ replace,
660+ status,
661+ } = result . redirect ;
662+ throw redirect ( location , {
663+ status,
664+ headers : {
665+ // Three R's of redirecting (lol Veep)
666+ ...( revalidate ? { "X-Remix-Revalidate" : "yes" } : null ) ,
667+ ...( reload ? { "X-Remix-Reload-Document" : "yes" } : null ) ,
668+ ...( replace ? { "X-Remix-Replace" : "yes" } : null ) ,
669+ } ,
670+ } ) ;
631671 }
632672
633- return results [ routeId ] !== undefined
634- ? unwrapSingleFetchResult ( results [ routeId ] , routeId )
635- : null ;
636- }
637-
638- function unwrapSingleFetchResult ( result : SingleFetchResult , routeId : string ) {
639- if ( "error" in result ) {
640- throw result . error ;
641- } else if ( "redirect" in result ) {
642- let headers : Record < string , string > = { } ;
643- if ( result . revalidate ) {
644- headers [ "X-Remix-Revalidate" ] = "yes" ;
645- }
646- if ( result . reload ) {
647- headers [ "X-Remix-Reload-Document" ] = "yes" ;
648- }
649- if ( result . replace ) {
650- headers [ "X-Remix-Replace" ] = "yes" ;
651- }
652- throw redirect ( result . redirect , { status : result . status , headers } ) ;
653- } else if ( "data" in result ) {
654- return result . data ;
673+ let routeResult = result . routes [ routeId ] ;
674+ if ( "error" in routeResult ) {
675+ throw routeResult . error ;
676+ } else if ( "data" in routeResult ) {
677+ return routeResult . data ;
655678 } else {
656679 throw new Error ( `No response found for routeId "${ routeId } "` ) ;
657680 }
658681}
659682
660- type Deferred = ReturnType < typeof createDeferred > ;
661683function createDeferred < T = unknown > ( ) {
662684 let resolve : ( val ?: any ) => Promise < void > ;
663685 let reject : ( error ?: unknown ) => Promise < void > ;
0 commit comments