From 57ca924ef2e368628ef925d32bee09962b09c47f Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 23 Oct 2023 13:58:16 -0700 Subject: [PATCH] [Fizz] Support onHeadersReady option for `renderTo...` functions During a render it can be useful to emit headers to start the process of preloading without waiting for the shell to complete. If provided an `onHeadersReady` callback option Fizz will call this callback with some headers. Currently this is implemented as a set of preload link headers that are generated during the first performWork pass. After this pass preloads are handled as they were preious to this change and emitted as tags. Since headers are not streamed typically we are choosing an API that provides headers once. Because of this we need to choose a point at which to provide headers once even if we could in theory wait longer to accumulate more. It is possible that a better API is to allow the caller to tell Fizz when it wants whatever headers have accumulated however in practice this would require having some secondary header outside of react that is blocking sending headers and that is considered not a likely ocurrence In this commit we add the option to pass `onHeadersReady` to static and dynamic render entrypoints. This is not yet wired up to anything so it will never be called. This will be added in a subsequent commit. --- .../src/server/ReactDOMFizzServerBrowser.js | 16 +++++++++++++++- .../src/server/ReactDOMFizzServerBun.js | 12 ++++++++++++ .../src/server/ReactDOMFizzServerEdge.js | 17 ++++++++++++++++- .../src/server/ReactDOMFizzServerNode.js | 8 +++++++- .../src/server/ReactDOMFizzStaticBrowser.js | 16 +++++++++++++++- .../src/server/ReactDOMFizzStaticEdge.js | 15 ++++++++++++++- .../src/server/ReactDOMFizzStaticNode.js | 7 ++++++- packages/react-server/src/ReactFizzServer.js | 8 ++++++++ 8 files changed, 93 insertions(+), 6 deletions(-) diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js index 48b3df777ee99..bd94b66ef6239 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js @@ -7,7 +7,10 @@ * @flow */ -import type {PostponedState} from 'react-server/src/ReactFizzServer'; +import type { + PostponedState, + HeadersDescriptor, +} from 'react-server/src/ReactFizzServer'; import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes'; import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; import type {ImportMap} from '../shared/ReactDOMTypes'; @@ -44,6 +47,7 @@ type Options = { unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, formState?: ReactFormState | null, + onHeaders?: (headers: Headers) => void, }; type ResumeOptions = { @@ -97,6 +101,15 @@ function renderToReadableStream( allReady.catch(() => {}); reject(error); } + + const onHeaders = options ? options.onHeaders : undefined; + let onHeadersImpl; + if (onHeaders) { + onHeadersImpl = (headersDescriptor: HeadersDescriptor) => { + onHeaders(new Headers(headersDescriptor)); + }; + } + const resumableState = createResumableState( options ? options.identifierPrefix : undefined, options ? options.unstable_externalRuntimeSrc : undefined, @@ -122,6 +135,7 @@ function renderToReadableStream( onFatalError, options ? options.onPostpone : undefined, options ? options.formState : undefined, + onHeadersImpl, ); if (options && options.signal) { const signal = options.signal; diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBun.js b/packages/react-dom/src/server/ReactDOMFizzServerBun.js index c9271d3444344..dddef60ef020c 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBun.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBun.js @@ -7,6 +7,7 @@ * @flow */ +import type {HeadersDescriptor} from 'react-server/src/ReactFizzServer'; import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes'; import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; import type {ImportMap} from '../shared/ReactDOMTypes'; @@ -41,6 +42,7 @@ type Options = { unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, formState?: ReactFormState | null, + onHeaders?: (headers: Headers) => void, }; // TODO: Move to sub-classing ReadableStream. @@ -87,6 +89,15 @@ function renderToReadableStream( allReady.catch(() => {}); reject(error); } + + const onHeaders = options ? options.onHeaders : undefined; + let onHeadersImpl; + if (onHeaders) { + onHeadersImpl = (headersDescriptor: HeadersDescriptor) => { + onHeaders(new Headers(headersDescriptor)); + }; + } + const resumableState = createResumableState( options ? options.identifierPrefix : undefined, options ? options.unstable_externalRuntimeSrc : undefined, @@ -112,6 +123,7 @@ function renderToReadableStream( onFatalError, options ? options.onPostpone : undefined, options ? options.formState : undefined, + onHeadersImpl, ); if (options && options.signal) { const signal = options.signal; diff --git a/packages/react-dom/src/server/ReactDOMFizzServerEdge.js b/packages/react-dom/src/server/ReactDOMFizzServerEdge.js index 48b3df777ee99..7ee96eb9c2fe2 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerEdge.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerEdge.js @@ -7,7 +7,10 @@ * @flow */ -import type {PostponedState} from 'react-server/src/ReactFizzServer'; +import type { + PostponedState, + HeadersDescriptor, +} from 'react-server/src/ReactFizzServer'; import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes'; import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; import type {ImportMap} from '../shared/ReactDOMTypes'; @@ -44,6 +47,7 @@ type Options = { unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, formState?: ReactFormState | null, + onHeaders?: (headers: Headers) => void, }; type ResumeOptions = { @@ -97,6 +101,15 @@ function renderToReadableStream( allReady.catch(() => {}); reject(error); } + + const onHeaders = options ? options.onHeaders : undefined; + let onHeadersImpl; + if (onHeaders) { + onHeadersImpl = (headersDescriptor: HeadersDescriptor) => { + onHeaders(new Headers(headersDescriptor)); + }; + } + const resumableState = createResumableState( options ? options.identifierPrefix : undefined, options ? options.unstable_externalRuntimeSrc : undefined, @@ -122,6 +135,7 @@ function renderToReadableStream( onFatalError, options ? options.onPostpone : undefined, options ? options.formState : undefined, + onHeadersImpl, ); if (options && options.signal) { const signal = options.signal; @@ -190,6 +204,7 @@ function resume( onShellReady, onShellError, onFatalError, + undefined, options ? options.onPostpone : undefined, ); if (options && options.signal) { diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js index db77734a6af71..eaa98e9e6a04d 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js @@ -7,7 +7,11 @@ * @flow */ -import type {Request, PostponedState} from 'react-server/src/ReactFizzServer'; +import type { + Request, + PostponedState, + HeadersDescriptor, +} from 'react-server/src/ReactFizzServer'; import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes'; import type {Writable} from 'stream'; import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; @@ -60,6 +64,7 @@ type Options = { unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, formState?: ReactFormState | null, + onHeaders?: (headers: HeadersDescriptor) => void, }; type ResumeOptions = { @@ -104,6 +109,7 @@ function createRequestImpl(children: ReactNodeList, options: void | Options) { undefined, options ? options.onPostpone : undefined, options ? options.formState : undefined, + options ? options.onHeaders : undefined, ); } diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js index b3db085738b2c..089cfd0cb1ea9 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js @@ -9,7 +9,10 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; -import type {PostponedState} from 'react-server/src/ReactFizzServer'; +import type { + PostponedState, + HeadersDescriptor, +} from 'react-server/src/ReactFizzServer'; import type {ImportMap} from '../shared/ReactDOMTypes'; import ReactVersion from 'shared/ReactVersion'; @@ -41,6 +44,7 @@ type Options = { onPostpone?: (reason: string) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, + onHeaders?: (headers: Headers) => void, }; type StaticResult = { @@ -77,6 +81,15 @@ function prerender( }; resolve(result); } + + const onHeaders = options ? options.onHeaders : undefined; + let onHeadersImpl; + if (onHeaders) { + onHeadersImpl = (headersDescriptor: HeadersDescriptor) => { + onHeaders(new Headers(headersDescriptor)); + }; + } + const resources = createResumableState( options ? options.identifierPrefix : undefined, options ? options.unstable_externalRuntimeSrc : undefined, @@ -101,6 +114,7 @@ function prerender( undefined, onFatalError, options ? options.onPostpone : undefined, + onHeadersImpl, ); if (options && options.signal) { const signal = options.signal; diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js b/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js index b3db085738b2c..7ea4695fafd04 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticEdge.js @@ -9,7 +9,10 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; -import type {PostponedState} from 'react-server/src/ReactFizzServer'; +import type { + PostponedState, + HeadersDescriptor, +} from 'react-server/src/ReactFizzServer'; import type {ImportMap} from '../shared/ReactDOMTypes'; import ReactVersion from 'shared/ReactVersion'; @@ -41,6 +44,7 @@ type Options = { onPostpone?: (reason: string) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, + onHeaders?: (headers: Headers) => void, }; type StaticResult = { @@ -77,6 +81,14 @@ function prerender( }; resolve(result); } + + const onHeaders = options ? options.onHeaders : undefined; + let onHeadersImpl; + if (onHeaders) { + onHeadersImpl = (headersDescriptor: HeadersDescriptor) => { + onHeaders(new Headers(headersDescriptor)); + }; + } const resources = createResumableState( options ? options.identifierPrefix : undefined, options ? options.unstable_externalRuntimeSrc : undefined, @@ -101,6 +113,7 @@ function prerender( undefined, onFatalError, options ? options.onPostpone : undefined, + onHeadersImpl, ); if (options && options.signal) { const signal = options.signal; diff --git a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js index 4ac8afd9ce0d7..3a3c8619bdbe2 100644 --- a/packages/react-dom/src/server/ReactDOMFizzStaticNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzStaticNode.js @@ -9,7 +9,10 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import type {BootstrapScriptDescriptor} from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; -import type {PostponedState} from 'react-server/src/ReactFizzServer'; +import type { + PostponedState, + HeadersDescriptor, +} from 'react-server/src/ReactFizzServer'; import type {ImportMap} from '../shared/ReactDOMTypes'; import {Writable, Readable} from 'stream'; @@ -42,6 +45,7 @@ type Options = { onPostpone?: (reason: string) => void, unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor, importMap?: ImportMap, + onHeaders?: (headers: HeadersDescriptor) => void, }; type StaticResult = { @@ -110,6 +114,7 @@ function prerenderToNodeStream( undefined, onFatalError, options ? options.onPostpone : undefined, + options ? options.onHeaders : undefined, ); if (options && options.signal) { const signal = options.signal; diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 34091855daec3..fb629fdca3e5d 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -353,6 +353,10 @@ function defaultErrorHandler(error: mixed) { function noop(): void {} +export type HeadersDescriptor = { + Link: string, +}; + export function createRequest( children: ReactNodeList, resumableState: ResumableState, @@ -366,6 +370,7 @@ export function createRequest( onFatalError: void | ((error: mixed) => void), onPostpone: void | ((reason: string) => void), formState: void | null | ReactFormState, + onHeaders: void | ((headers: HeadersDescriptor) => void), ): Request { prepareHostDispatcher(); const pingedTasks: Array = []; @@ -442,6 +447,7 @@ export function createPrerenderRequest( onShellError: void | ((error: mixed) => void), onFatalError: void | ((error: mixed) => void), onPostpone: void | ((reason: string) => void), + onHeaders: void | ((headers: HeadersDescriptor) => void), ): Request { const request = createRequest( children, @@ -455,6 +461,8 @@ export function createPrerenderRequest( onShellError, onFatalError, onPostpone, + undefined, + onHeaders, ); // Start tracking postponed holes during this render. request.trackedPostpones = {