Skip to content

Commit 8f33065

Browse files
committed
Lift turbo-stream-specific deciding into fetchAndDecode
1 parent 12857fd commit 8f33065

File tree

2 files changed

+79
-52
lines changed

2 files changed

+79
-52
lines changed

packages/react-router/lib/dom/ssr/single-fetch.tsx

Lines changed: 72 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -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
3442
export 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

4451
interface 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

525533
async 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>;
661683
function createDeferred<T = unknown>() {
662684
let resolve: (val?: any) => Promise<void>;
663685
let reject: (error?: unknown) => Promise<void>;

packages/react-router/lib/server-runtime/routes.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import type {
1212
SingleFetchResult,
1313
SingleFetchResults,
1414
} from "../dom/ssr/single-fetch";
15-
import { decodeViaTurboStream } from "../dom/ssr/single-fetch";
15+
import {
16+
SingleFetchRedirectSymbol,
17+
decodeViaTurboStream,
18+
} from "../dom/ssr/single-fetch";
1619
import invariant from "./invariant";
1720
import type { ServerRouteModule } from "../dom/ssr/routeModules";
1821

@@ -100,7 +103,9 @@ export function createStaticHandlerDataRoutes(
100103
let decoded = await decodeViaTurboStream(stream, global);
101104
let data = decoded.value as SingleFetchResults;
102105
invariant(
103-
data && route.id in data,
106+
data &&
107+
!(SingleFetchRedirectSymbol in data) &&
108+
route.id in data,
104109
"Unable to decode prerendered data"
105110
);
106111
let result = data[route.id] as SingleFetchResult;

0 commit comments

Comments
 (0)