Skip to content

Commit

Permalink
[Flight] Implement prerender
Browse files Browse the repository at this point in the history
Prerendering in flight is similar to prerendering in Fizz. Instead of receiving a result (the stream) immediately a promise is returned which resolves to the stream when the prerender is complete. The promise will reject if the flight render fatally errors otherwise it will resolve when the render is completed or is aborted.
  • Loading branch information
gnoff committed Aug 14, 2024
1 parent 2a54019 commit 47c1a7e
Show file tree
Hide file tree
Showing 9 changed files with 546 additions and 4 deletions.
73 changes: 73 additions & 0 deletions packages/react-server-dom-esm/src/ReactFlightDOMServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
import type {Writable} from 'stream';
import type {Thenable} from 'shared/ReactTypes';

import {Readable} from 'stream';

import {
createRequest,
startWork,
Expand Down Expand Up @@ -123,6 +125,76 @@ function renderToPipeableStream(
},
};
}
function createFakeWritable(readable: any): Writable {
// The current host config expects a Writable so we create
// a fake writable for now to push into the Readable.
return ({
write(chunk) {
return readable.push(chunk);
},
end() {
readable.push(null);
},
destroy(error) {
readable.destroy(error);
},
}: any);
}

type PrerenderOptions = {
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
onError?: (error: mixed) => void,
onPostpone?: (reason: string) => void,
identifierPrefix?: string,
temporaryReferences?: TemporaryReferenceSet,
signal?: AbortSignal,
};

function prerenderToNodeStream(
model: ReactClientValue,
moduleBasePath: ClientManifest,
options?: PrerenderOptions,
): Promise<Readable> {
return new Promise((resolve, reject) => {
const onFatalError = reject;
function onAllReady() {
const readable: Readable = new Readable({
read() {
startFlowing(request, writable);
},
});
const writable = createFakeWritable(readable);
resolve(readable);
}

const request = createRequest(
model,
moduleBasePath,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
onAllReady,
onFatalError,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
} else {
const listener = () => {
abort(request, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
startWork(request);
});
}

function decodeReplyFromBusboy<T>(
busboyStream: Busboy,
Expand Down Expand Up @@ -207,6 +279,7 @@ function decodeReply<T>(

export {
renderToPipeableStream,
prerenderToNodeStream,
decodeReplyFromBusboy,
decodeReply,
decodeAction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,60 @@ function renderToReadableStream(
return stream;
}

function prerender(
model: ReactClientValue,
turbopackMap: ClientManifest,
options?: Options,
): Promise<ReadableStream> {
return new Promise((resolve, reject) => {
const onFatalError = reject;
function onAllReady() {
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
startWork(request);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request, reason);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
);
resolve(stream);
}
const request = createRequest(
model,
turbopackMap,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
onAllReady,
onFatalError,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
} else {
const listener = () => {
abort(request, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
});
}

function decodeReply<T>(
body: string | FormData,
turbopackMap: ServerManifest,
Expand All @@ -121,4 +175,10 @@ function decodeReply<T>(
return root;
}

export {renderToReadableStream, decodeReply, decodeAction, decodeFormState};
export {
renderToReadableStream,
prerender,
decodeReply,
decodeAction,
decodeFormState,
};
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,60 @@ function renderToReadableStream(
return stream;
}

function prerender(
model: ReactClientValue,
turbopackMap: ClientManifest,
options?: Options,
): Promise<ReadableStream> {
return new Promise((resolve, reject) => {
const onFatalError = reject;
function onAllReady() {
const stream = new ReadableStream(
{
type: 'bytes',
start: (controller): ?Promise<void> => {
startWork(request);
},
pull: (controller): ?Promise<void> => {
startFlowing(request, controller);
},
cancel: (reason): ?Promise<void> => {
stopFlowing(request);
abort(request, reason);
},
},
// $FlowFixMe[prop-missing] size() methods are not allowed on byte streams.
{highWaterMark: 0},
);
resolve(stream);
}
const request = createRequest(
model,
turbopackMap,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
onAllReady,
onFatalError,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
} else {
const listener = () => {
abort(request, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
});
}

function decodeReply<T>(
body: string | FormData,
turbopackMap: ServerManifest,
Expand All @@ -121,4 +175,10 @@ function decodeReply<T>(
return root;
}

export {renderToReadableStream, decodeReply, decodeAction, decodeFormState};
export {
renderToReadableStream,
prerender,
decodeReply,
decodeAction,
decodeFormState,
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import type {Busboy} from 'busboy';
import type {Writable} from 'stream';
import type {Thenable} from 'shared/ReactTypes';

import {Readable} from 'stream';

import {
createRequest,
startWork,
Expand Down Expand Up @@ -125,6 +127,77 @@ function renderToPipeableStream(
};
}

function createFakeWritable(readable: any): Writable {
// The current host config expects a Writable so we create
// a fake writable for now to push into the Readable.
return ({
write(chunk) {
return readable.push(chunk);
},
end() {
readable.push(null);
},
destroy(error) {
readable.destroy(error);
},
}: any);
}

type PrerenderOptions = {
environmentName?: string | (() => string),
filterStackFrame?: (url: string, functionName: string) => boolean,
onError?: (error: mixed) => void,
onPostpone?: (reason: string) => void,
identifierPrefix?: string,
temporaryReferences?: TemporaryReferenceSet,
signal?: AbortSignal,
};

function prerenderToNodeStream(
model: ReactClientValue,
turbopackMap: ClientManifest,
options?: PrerenderOptions,
): Promise<Readable> {
return new Promise((resolve, reject) => {
const onFatalError = reject;
function onAllReady() {
const readable: Readable = new Readable({
read() {
startFlowing(request, writable);
},
});
const writable = createFakeWritable(readable);
resolve(readable);
}

const request = createRequest(
model,
turbopackMap,
options ? options.onError : undefined,
options ? options.identifierPrefix : undefined,
options ? options.onPostpone : undefined,
options ? options.temporaryReferences : undefined,
__DEV__ && options ? options.environmentName : undefined,
__DEV__ && options ? options.filterStackFrame : undefined,
onAllReady,
onFatalError,
);
if (options && options.signal) {
const signal = options.signal;
if (signal.aborted) {
abort(request, (signal: any).reason);
} else {
const listener = () => {
abort(request, (signal: any).reason);
signal.removeEventListener('abort', listener);
};
signal.addEventListener('abort', listener);
}
}
startWork(request);
});
}

function decodeReplyFromBusboy<T>(
busboyStream: Busboy,
turbopackMap: ServerManifest,
Expand Down Expand Up @@ -208,6 +281,7 @@ function decodeReply<T>(

export {
renderToPipeableStream,
prerenderToNodeStream,
decodeReplyFromBusboy,
decodeReply,
decodeAction,
Expand Down
Loading

0 comments on commit 47c1a7e

Please sign in to comment.