Skip to content

Commit

Permalink
wip: rsc runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
jacob-ebey committed May 8, 2024
1 parent 9800c95 commit 1897d73
Show file tree
Hide file tree
Showing 18 changed files with 520 additions and 61 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
},
"overrides": {
"react": "0.0.0-experimental-96c584661-20240412",
"react-dom": "0.0.0-experimental-96c584661-20240412"
"react-dom": "0.0.0-experimental-96c584661-20240412"
}
}
}
2 changes: 2 additions & 0 deletions packages/react-router/lib/dom/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export type WindowRemixContext = {
isSpaMode: boolean;
stream: ReadableStream<Uint8Array> | undefined;
streamController: ReadableStreamDefaultController<Uint8Array>;
streamAction?: ReadableStream<Uint8Array> | undefined;
streamControllerAction?: ReadableStreamDefaultController<Uint8Array>;
// The number of active deferred keys rendered on the server
a?: number;
dev?: {
Expand Down
59 changes: 50 additions & 9 deletions packages/react-router/lib/dom/ssr/browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,17 +217,58 @@ function createHydratedRouter(): RemixRouter {
// then only get past here and create the `router` one time
if (!ssrInfo.stateDecodingPromise) {
let stream = ssrInfo.context.stream;
let streamAction = ssrInfo.context.streamAction;
invariant(stream, "No stream found for single fetch decoding");
ssrInfo.context.stream = undefined;
ssrInfo.stateDecodingPromise = decodeViaTurboStream(stream, window)
.then((value) => {
ssrInfo!.context.state =
value.value as typeof localSsrInfo.context.state;
localSsrInfo.stateDecodingPromise!.value = true;
})
.catch((e) => {
localSsrInfo.stateDecodingPromise!.error = e;
});
if (ssrInfo.context.future.unstable_serverComponents) {
ssrInfo.stateDecodingPromise = Promise.all([
// @ts-expect-error - TODO: Get this from somewhere else
window.createFromReadableStream(stream),
streamAction
? // @ts-expect-error - TODO: Get this from somewhere else
window.createFromReadableStream(streamAction)
: undefined,
])
.then(([loaderPayload, actionPayload]) => {
let state: NonNullable<typeof ssrInfo>["context"]["state"] = {};
for (let routeId of Object.keys(ssrInfo!.routeModules)) {
if ("error" in loaderPayload[routeId]) {
state.errors = state.errors || {};
state.errors[routeId] = loaderPayload[routeId].error;
} else if ("data" in loaderPayload[routeId]) {
state.loaderData = state.loaderData || {};
state.loaderData[routeId] = loaderPayload[routeId].data;
}
}
if (actionPayload) {
// @ts-expect-error - TODO: Fix types and don't get it off the window directly
const actionId = window.__remixContext.serverHandoffActionId;

if ("error" in actionPayload) {
state.errors = state.errors || {};
state.errors[actionId] = actionPayload.error;
} else if ("data" in actionPayload) {
state.actionData = state.actionData || {};
state.actionData[actionId] = actionPayload.data;
}
}
ssrInfo!.context.state = state;
localSsrInfo.stateDecodingPromise!.value = true;
})
.catch((e) => {
localSsrInfo.stateDecodingPromise!.error = e;
});
} else {
ssrInfo.stateDecodingPromise = decodeViaTurboStream(stream, window)
.then((value) => {
ssrInfo!.context.state =
value.value as typeof localSsrInfo.context.state;
localSsrInfo.stateDecodingPromise!.value = true;
})
.catch((e) => {
localSsrInfo.stateDecodingPromise!.error = e;
});
}
}
if (ssrInfo.stateDecodingPromise.error) {
throw ssrInfo.stateDecodingPromise.error;
Expand Down
11 changes: 10 additions & 1 deletion packages/react-router/lib/dom/ssr/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ export type ScriptProps = Omit<
* @see https://remix.run/components/scripts
*/
export function Scripts(props: ScriptProps) {
let { manifest, serverHandoffString, isSpaMode, renderMeta } =
let { manifest, serverHandoffString, isSpaMode, renderMeta, future } =
useRemixContext();
let { router, static: isStatic, staticContext } = useDataRouterContext();
let { matches: routerMatches } = useDataRouterStateContext();
Expand All @@ -599,6 +599,15 @@ export function Scripts(props: ScriptProps) {
"}" +
"}).pipeThrough(new TextEncoderStream());";

if (future.unstable_serverComponents && staticContext?.actionData) {
streamScript +=
"window.__remixContext.streamAction = new ReadableStream({" +
"start(controller){" +
"window.__remixContext.streamControllerAction = controller;" +
"}" +
"}).pipeThrough(new TextEncoderStream());";
}

let contextScript = staticContext
? `window.__remixContext = ${serverHandoffString};${streamScript}`
: " ";
Expand Down
1 change: 1 addition & 0 deletions packages/react-router/lib/dom/ssr/create-remix-stub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export function createRemixStub(
future: {
v3_fetcherPersist: future?.v3_fetcherPersist === true,
v3_relativeSplatPath: future?.v3_relativeSplatPath === true,
unstable_serverComponents: future?.unstable_serverComponents === true,
},
manifest: {
routes: {},
Expand Down
12 changes: 12 additions & 0 deletions packages/react-router/lib/dom/ssr/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ export interface RemixContextObject {
error?: unknown;
}
>;
streamCacheAction?: Record<
number,
Promise<void> & {
result?: {
done: boolean;
value: string;
};
error?: unknown;
}
>;
};
}

Expand All @@ -38,11 +48,13 @@ export interface RemixContextObject {
export interface EntryContext extends RemixContextObject {
staticHandlerContext: StaticHandlerContext;
serverHandoffStream?: ReadableStream<Uint8Array>;
serverHandoffStreamAction?: ReadableStream<Uint8Array>;
}

export interface FutureConfig {
v3_fetcherPersist: boolean;
v3_relativeSplatPath: boolean;
unstable_serverComponents: boolean;
}

export interface AssetsManifest {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router/lib/dom/ssr/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export function createServerRoutes(
// has a loader/clientLoader, but it won't ever be called during the static
// render, so just give it a no-op function so we can render down to the
// proper fallback
loader: route.hasLoader || route.hasClientLoader ? () => null : undefined,
loader: route.hasLoader || route.hasClientLoader ? true : undefined,
// We don't need action/shouldRevalidate on these routes since they're
// for a static render
};
Expand Down
12 changes: 12 additions & 0 deletions packages/react-router/lib/dom/ssr/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,25 @@ export function RemixServer({
/>
</RemixErrorBoundary>
</RemixContext.Provider>
{context.serverHandoffStreamAction ? (
<React.Suspense>
<StreamTransfer
context={context}
identifier={0}
reader={context.serverHandoffStreamAction.getReader()}
textDecoder={new TextDecoder()}
isAction={true}
/>
</React.Suspense>
) : null}
{context.serverHandoffStream ? (
<React.Suspense>
<StreamTransfer
context={context}
identifier={0}
reader={context.serverHandoffStream.getReader()}
textDecoder={new TextDecoder()}
isAction={false}
/>
</React.Suspense>
) : null}
Expand Down
34 changes: 29 additions & 5 deletions packages/react-router/lib/dom/ssr/single-fetch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface StreamTransferProps {
identifier: number;
reader: ReadableStreamDefaultReader<Uint8Array>;
textDecoder: TextDecoder;
isAction: boolean;
}

// StreamTransfer recursively renders down chunks of the `serverHandoffStream`
Expand All @@ -50,6 +51,7 @@ export function StreamTransfer({
identifier,
reader,
textDecoder,
isAction,
}: StreamTransferProps) {
// If the user didn't render the <Scripts> component then we don't have to
// bother streaming anything in
Expand All @@ -60,7 +62,19 @@ export function StreamTransfer({
if (!context.renderMeta.streamCache) {
context.renderMeta.streamCache = {};
}
let { streamCache } = context.renderMeta;
let streamCache = isAction
? context.renderMeta.streamCacheAction
: context.renderMeta.streamCache;
if (!streamCache) {
if (isAction) {
context.renderMeta.streamCacheAction = {};
streamCache = context.renderMeta.streamCacheAction;
} else {
context.renderMeta.streamCache = {};
streamCache = context.renderMeta.streamCache;
}
}

let promise = streamCache[identifier];
if (!promise) {
promise = streamCache[identifier] = reader
Expand All @@ -87,9 +101,9 @@ export function StreamTransfer({
let scriptTag = value ? (
<script
dangerouslySetInnerHTML={{
__html: `window.__remixContext.streamController.enqueue(${escapeHtml(
JSON.stringify(value)
)});`,
__html: `window.__remixContext.streamController${
isAction ? "Action" : ""
}.enqueue(${escapeHtml(JSON.stringify(value))});`,
}}
/>
) : null;
Expand All @@ -100,7 +114,9 @@ export function StreamTransfer({
{scriptTag}
<script
dangerouslySetInnerHTML={{
__html: `window.__remixContext.streamController.close();`,
__html: `window.__remixContext.streamController${
isAction ? "Action" : ""
}.close();`,
}}
/>
</>
Expand All @@ -115,6 +131,7 @@ export function StreamTransfer({
identifier={identifier + 1}
reader={reader}
textDecoder={textDecoder}
isAction={isAction}
/>
</React.Suspense>
</>
Expand Down Expand Up @@ -307,6 +324,13 @@ async function fetchAndDecode(url: URL, init?: RequestInit) {
return { status: res.status, data: decoded.value };
}

if (res.headers.get("Content-Type")?.includes("text/x-component")) {
invariant(res.body, "No response body to decode");
// @ts-expect-error - TODO: Figure out where this comes from
let decoded = await window.createFromReadableStream(res.body);
return { status: res.status, data: decoded };
}

// If we didn't get back a turbo-stream response, then we never reached the
// Remix server and likely this is a network error - just expose up the
// response body as an Error
Expand Down
1 change: 1 addition & 0 deletions packages/remix-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = (_config) => {
})
.join("\n")}
export const future = ${JSON.stringify(ctx.reactRouterConfig.future)};
export const basename = ${JSON.stringify(ctx.reactRouterConfig.basename)};
export const entry = { module: entryServer };
export const routes = {
${Object.keys(routes)
Expand Down
22 changes: 22 additions & 0 deletions packages/remix-server-runtime/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,26 @@ export interface ServerEntryModule {
handleDataRequest?: HandleDataRequestFunction;
handleError?: HandleErrorFunction;
streamTimeout?: number;
createFromReadableStream?: CreateFromReadableStreamFunction;
}

export interface RenderToReadableStreamFunction {
(data: unknown): ReadableStream<Uint8Array>;
}

export interface CreateFromReadableStreamFunction {
(body: ReadableStream<Uint8Array>): Promise<unknown>;
}

export interface ReactServerEntryModule {
renderToReadableStream: RenderToReadableStreamFunction;
}

export interface ReactServerBuild {
entry: {
module: ReactServerEntryModule;
};
routes: ServerRouteManifest;
future: FutureConfig;
basename: string;
}
3 changes: 3 additions & 0 deletions packages/remix-server-runtime/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export interface EntryContext {
criticalCss?: string;
serverHandoffString?: string;
serverHandoffStream?: ReadableStream<Uint8Array>;
serverHandoffActionId?: string;
serverHandoffStreamAction?: ReadableStream<Uint8Array>;
renderMeta?: {
didRenderScripts?: boolean;
streamCache?: Record<
Expand All @@ -33,6 +35,7 @@ export interface FutureConfig {
v3_fetcherPersist: boolean;
v3_relativeSplatPath: boolean;
v3_throwAbortReason: boolean;
unstable_serverComponents: boolean;
}

export interface AssetsManifest {
Expand Down
5 changes: 5 additions & 0 deletions packages/remix-server-runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export {
} from "./formData";
export { defer, json, redirect, redirectDocument } from "./responses";
export { createRequestHandler } from "./server";
export { createReactServerRequestHandler } from "./server-react";
export {
createSession,
createSessionStorageFactory,
Expand Down Expand Up @@ -41,6 +42,7 @@ export type {
CookieParseOptions,
CookieSerializeOptions,
CookieSignatureOptions,
CreateFromReadableStreamFunction,
DataFunctionArgs,
EntryContext,
ErrorResponse,
Expand All @@ -58,6 +60,9 @@ export type {
MemoryUploadHandlerOptions,
HandleErrorFunction,
PageLinkDescriptor,
ReactServerBuild,
ReactServerEntryModule,
RenderToReadableStreamFunction,
RequestHandler,
SerializeFrom,
ServerBuild,
Expand Down
4 changes: 4 additions & 0 deletions packages/remix-server-runtime/reexport.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
export type { ErrorResponse } from "react-router";

export type {
CreateFromReadableStreamFunction,
HandleDataRequestFunction,
HandleDocumentRequestFunction,
HandleErrorFunction,
ReactServerBuild,
ReactServerEntryModule,
RenderToReadableStreamFunction,
ServerBuild,
ServerEntryModule,
} from "./build";
Expand Down

0 comments on commit 1897d73

Please sign in to comment.