diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index ec1e5d34e7c7c..e8034d257e790 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -18,11 +18,14 @@ import type { SSRManifest, } from './ReactFlightClientConfig'; +import type {HintModel} from 'react-server/src/ReactFlightServerConfig'; + import { resolveClientReference, preloadModule, requireModule, parseModel, + dispatchHint, } from './ReactFlightClientConfig'; import {knownServerReferences} from './ReactFlightServerReferenceRegistry'; @@ -778,6 +781,15 @@ export function resolveErrorDev( } } +export function resolveHint( + response: Response, + code: string, + model: UninitializedModel, +): void { + const hintModel = parseModel(response, model); + dispatchHint(code, hintModel); +} + export function close(response: Response): void { // In case there are any remaining unresolved chunks, they won't // be resolved now. So we need to issue an error to those. diff --git a/packages/react-client/src/ReactFlightClientStream.js b/packages/react-client/src/ReactFlightClientStream.js index d261da8b88b21..2c4c29919db6c 100644 --- a/packages/react-client/src/ReactFlightClientStream.js +++ b/packages/react-client/src/ReactFlightClientStream.js @@ -16,6 +16,7 @@ import { resolveModel, resolveErrorProd, resolveErrorDev, + resolveHint, createResponse as createResponseBase, parseModelString, parseModelTuple, @@ -46,6 +47,11 @@ function processFullRow(response: Response, row: string): void { resolveModule(response, id, row.slice(colon + 2)); return; } + case 'H': { + const code = row[colon + 2]; + resolveHint(response, code, row.slice(colon + 3)); + return; + } case 'E': { const errorInfo = JSON.parse(row.slice(colon + 2)); if (__DEV__) { diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js index 4990e84e7cb09..d8a49b459a0d8 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js @@ -35,6 +35,7 @@ export const resolveClientReference = $$$config.resolveClientReference; export const resolveServerReference = $$$config.resolveServerReference; export const preloadModule = $$$config.preloadModule; export const requireModule = $$$config.requireModule; +export const dispatchHint = $$$config.dispatchHint; export opaque type Source = mixed; diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 8c991026bc2e1..b2c7d6153f0ec 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -7,6 +7,7 @@ * @flow */ +import type {HostDispatcher} from 'react-dom/src/ReactDOMDispatcher'; import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities'; import type {DOMEventName} from '../events/DOMEventNames'; import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; @@ -1868,10 +1869,6 @@ export function clearSingleton(instance: Instance): void { export const supportsResources = true; -// The resource types we support. currently they match the form for the as argument. -// In the future this may need to change, especially when modules / scripts are supported -type ResourceType = 'style' | 'font' | 'script'; - type HoistableTagType = 'link' | 'meta' | 'title'; type TResource< T: 'stylesheet' | 'style' | 'script' | 'void', @@ -1962,7 +1959,7 @@ function getDocumentFromRoot(root: HoistableRoot): Document { // We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate // internals in Module scope. Instead we export it and Internals will import it. There is already a cycle // from Internals -> ReactDOM -> HostConfig -> Internals so this doesn't introduce a new one. -export const ReactDOMClientDispatcher = { +export const ReactDOMClientDispatcher: HostDispatcher = { prefetchDNS, preconnect, preload, @@ -2036,7 +2033,10 @@ function prefetchDNS(href: string, options?: mixed) { preconnectAs('dns-prefetch', null, href); } -function preconnect(href: string, options?: {crossOrigin?: string}) { +function preconnect(href: string, options: ?{crossOrigin?: string}) { + if (!enableFloat) { + return; + } if (__DEV__) { if (typeof href !== 'string' || !href) { console.error( @@ -2064,9 +2064,8 @@ function preconnect(href: string, options?: {crossOrigin?: string}) { preconnectAs('preconnect', crossOrigin, href); } -type PreloadAs = ResourceType; type PreloadOptions = { - as: PreloadAs, + as: string, crossOrigin?: string, integrity?: string, type?: string, @@ -2115,7 +2114,7 @@ function preload(href: string, options: PreloadOptions) { function preloadPropsFromPreloadOptions( href: string, - as: ResourceType, + as: string, options: PreloadOptions, ): PreloadProps { return { @@ -2128,9 +2127,8 @@ function preloadPropsFromPreloadOptions( }; } -type PreinitAs = 'style' | 'script'; type PreinitOptions = { - as: PreinitAs, + as: string, precedence?: string, crossOrigin?: string, integrity?: string, diff --git a/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js b/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js new file mode 100644 index 0000000000000..f8c9becaf5e4f --- /dev/null +++ b/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js @@ -0,0 +1,118 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + HostDispatcher, + PrefetchDNSOptions, + PreconnectOptions, + PreloadOptions, + PreinitOptions, +} from 'react-dom/src/ReactDOMDispatcher'; + +import {enableFloat} from 'shared/ReactFeatureFlags'; + +import { + emitHint, + getHints, + resolveRequest, +} from 'react-server/src/ReactFlightServer'; + +export const ReactDOMFlightServerDispatcher: HostDispatcher = { + prefetchDNS, + preconnect, + preload, + preinit, +}; + +function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) { + if (enableFloat) { + if (typeof href === 'string') { + const request = resolveRequest(); + if (request) { + const hints = getHints(request); + const key = 'D' + href; + if (hints.has(key)) { + // duplicate hint + return; + } + hints.add(key); + if (options) { + emitHint(request, 'D', [href, options]); + } else { + emitHint(request, 'D', href); + } + } + } + } +} + +function preconnect(href: string, options: ?PreconnectOptions) { + if (enableFloat) { + if (typeof href === 'string') { + const request = resolveRequest(); + if (request) { + const hints = getHints(request); + const crossOrigin = + options == null || typeof options.crossOrigin !== 'string' + ? null + : options.crossOrigin === 'use-credentials' + ? 'use-credentials' + : ''; + + const key = `C${crossOrigin === null ? 'null' : crossOrigin}|${href}`; + if (hints.has(key)) { + // duplicate hint + return; + } + hints.add(key); + if (options) { + emitHint(request, 'C', [href, options]); + } else { + emitHint(request, 'C', href); + } + } + } + } +} + +function preload(href: string, options: PreloadOptions) { + if (enableFloat) { + if (typeof href === 'string') { + const request = resolveRequest(); + if (request) { + const hints = getHints(request); + const key = 'L' + href; + if (hints.has(key)) { + // duplicate hint + return; + } + hints.add(key); + emitHint(request, 'L', [href, options]); + } + } + } +} + +function preinit(href: string, options: PreinitOptions) { + if (enableFloat) { + if (typeof href === 'string') { + const request = resolveRequest(); + if (request) { + const hints = getHints(request); + const key = 'I' + href; + if (hints.has(key)) { + // duplicate hint + return; + } + hints.add(key); + emitHint(request, 'I', [href, options]); + } + } + } +} diff --git a/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js b/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js index f5a5356312fbe..242c3a79f382c 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js @@ -21,9 +21,6 @@ export function scheduleWork(callback: () => void) { export function flushBuffered(destination: Destination) {} -export const supportsRequestStorage = false; -export const requestStorage: AsyncLocalStorage = (null: any); - export function beginWriting(destination: Destination) {} export function writeChunk( diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 25d296dbd0d60..1b03652ef8ce7 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -38,6 +38,11 @@ import { stringToPrecomputedChunk, clonePrecomputedChunk, } from 'react-server/src/ReactServerStreamConfig'; +import { + resolveRequest, + getResources, + flushResources, +} from 'react-server/src/ReactFizzServer'; import isAttributeNameSafe from '../shared/isAttributeNameSafe'; import isUnitlessNumber from '../shared/isUnitlessNumber'; @@ -79,30 +84,15 @@ import { import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher; -const ReactDOMServerDispatcher = enableFloat - ? { - prefetchDNS, - preconnect, - preload, - preinit, - } - : {}; - -let currentResources: null | Resources = null; -const currentResourcesStack = []; - -export function prepareToRender(resources: Resources): mixed { - currentResourcesStack.push(currentResources); - currentResources = resources; +const ReactDOMServerDispatcher = { + prefetchDNS, + preconnect, + preload, + preinit, +}; - const previousHostDispatcher = ReactDOMCurrentDispatcher.current; +export function prepareHostDispatcher() { ReactDOMCurrentDispatcher.current = ReactDOMServerDispatcher; - return previousHostDispatcher; -} - -export function cleanupAfterRender(previousDispatcher: mixed) { - currentResources = currentResourcesStack.pop(); - ReactDOMCurrentDispatcher.current = previousDispatcher; } // Used to distinguish these contexts from ones used in other renderers. @@ -4030,7 +4020,7 @@ export function writePreamble( // (User code could choose to send this even earlier by calling // preinit(...), if they know they will suspend). const {src, integrity} = responseState.externalRuntimeConfig; - preinitImpl(resources, src, {as: 'script', integrity}); + internalPreinitScript(resources, src, integrity); } const htmlChunks = responseState.htmlChunks; @@ -4804,16 +4794,19 @@ function getResourceKey(as: string, href: string): string { } export function prefetchDNS(href: string, options?: mixed) { - if (!currentResources) { - // While we expect that preconnect calls are primarily going to be observed - // during render because effects and events don't run on the server it is - // still possible that these get called in module scope. This is valid on - // the client since there is still a document to interact with but on the - // server we need a request to associate the call to. Because of this we - // simply return and do not warn. + if (!enableFloat) { + return; + } + const request = resolveRequest(); + if (!request) { + // In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also + // possibly get them from the stack if we are not in an async context. Since we were not able to resolve + // the resources for this call in either case we opt to do nothing. We can consider making this a warning + // but there may be times where calling a function outside of render is intentional (i.e. to warm up data + // fetching) and we don't want to warn in those cases. return; } - const resources = currentResources; + const resources = getResources(request); if (__DEV__) { if (typeof href !== 'string' || !href) { console.error( @@ -4855,20 +4848,24 @@ export function prefetchDNS(href: string, options?: mixed) { ); } resources.preconnects.add(resource); + flushResources(request); } } -export function preconnect(href: string, options?: {crossOrigin?: string}) { - if (!currentResources) { - // While we expect that preconnect calls are primarily going to be observed - // during render because effects and events don't run on the server it is - // still possible that these get called in module scope. This is valid on - // the client since there is still a document to interact with but on the - // server we need a request to associate the call to. Because of this we - // simply return and do not warn. +export function preconnect(href: string, options?: ?{crossOrigin?: string}) { + if (!enableFloat) { + return; + } + const request = resolveRequest(); + if (!request) { + // In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also + // possibly get them from the stack if we are not in an async context. Since we were not able to resolve + // the resources for this call in either case we opt to do nothing. We can consider making this a warning + // but there may be times where calling a function outside of render is intentional (i.e. to warm up data + // fetching) and we don't want to warn in those cases. return; } - const resources = currentResources; + const resources = getResources(request); if (__DEV__) { if (typeof href !== 'string' || !href) { console.error( @@ -4914,27 +4911,30 @@ export function preconnect(href: string, options?: {crossOrigin?: string}) { ); } resources.preconnects.add(resource); + flushResources(request); } } -type PreloadAs = 'style' | 'font' | 'script'; type PreloadOptions = { - as: PreloadAs, + as: string, crossOrigin?: string, integrity?: string, type?: string, }; export function preload(href: string, options: PreloadOptions) { - if (!currentResources) { - // While we expect that preload calls are primarily going to be observed - // during render because effects and events don't run on the server it is - // still possible that these get called in module scope. This is valid on - // the client since there is still a document to interact with but on the - // server we need a request to associate the call to. Because of this we - // simply return and do not warn. + if (!enableFloat) { return; } - const resources = currentResources; + const request = resolveRequest(); + if (!request) { + // In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also + // possibly get them from the stack if we are not in an async context. Since we were not able to resolve + // the resources for this call in either case we opt to do nothing. We can consider making this a warning + // but there may be times where calling a function outside of render is intentional (i.e. to warm up data + // fetching) and we don't want to warn in those cases. + return; + } + const resources = getResources(request); if (__DEV__) { if (typeof href !== 'string' || !href) { console.error( @@ -5055,37 +5055,30 @@ export function preload(href: string, options: PreloadOptions) { resources.explicitOtherPreloads.add(resource); } } + flushResources(request); } } -type PreinitAs = 'style' | 'script'; type PreinitOptions = { - as: PreinitAs, + as: string, precedence?: string, crossOrigin?: string, integrity?: string, }; -export function preinit(href: string, options: PreinitOptions): void { - if (!currentResources) { - // While we expect that preinit calls are primarily going to be observed - // during render because effects and events don't run on the server it is - // still possible that these get called in module scope. This is valid on - // the client since there is still a document to interact with but on the - // server we need a request to associate the call to. Because of this we - // simply return and do not warn. +function preinit(href: string, options: PreinitOptions): void { + if (!enableFloat) { return; } - preinitImpl(currentResources, href, options); -} - -// On the server, preinit may be called outside of render when sending an -// external SSR runtime as part of the initial resources payload. Since this -// is an internal React call, we do not need to use the resources stack. -function preinitImpl( - resources: Resources, - href: string, - options: PreinitOptions, -): void { + const request = resolveRequest(); + if (!request) { + // In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also + // possibly get them from the stack if we are not in an async context. Since we were not able to resolve + // the resources for this call in either case we opt to do nothing. We can consider making this a warning + // but there may be times where calling a function outside of render is intentional (i.e. to warm up data + // fetching) and we don't want to warn in those cases. + return; + } + const resources = getResources(request); if (__DEV__) { if (typeof href !== 'string' || !href) { console.error( @@ -5214,6 +5207,7 @@ function preinitImpl( resources.stylePrecedences.set(precedence, emptyStyleResource); } precedenceSet.add(resource); + flushResources(request); } return; } @@ -5288,6 +5282,7 @@ function preinitImpl( } resources.scripts.add(resource); pushScriptImpl(resource.chunks, resourceProps); + flushResources(request); } return; } @@ -5295,9 +5290,37 @@ function preinitImpl( } } +// This method is trusted. It must only be called from within this codebase and it assumes the arguments +// conform to the types because no user input is being passed in. It also assumes that it is being called as +// part of a work or flush loop and therefore does not need to request Fizz to flush Resources. +function internalPreinitScript( + resources: Resources, + src: string, + integrity: ?string, +): void { + const key = getResourceKey('script', src); + let resource = resources.scriptsMap.get(key); + if (!resource) { + resource = { + type: 'script', + chunks: [], + state: NoState, + props: null, + }; + resources.scriptsMap.set(key, resource); + resources.scripts.add(resource); + pushScriptImpl(resource.chunks, { + async: true, + src, + integrity, + }); + } + return; +} + function preloadPropsFromPreloadOptions( href: string, - as: PreloadAs, + as: string, options: PreloadOptions, ): PreloadProps { return { diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js index 1707b4d2c13e3..4feafb782dae6 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js @@ -137,8 +137,7 @@ export { writePostamble, hoistResources, setCurrentlyRenderingBoundaryResourcesTarget, - prepareToRender, - cleanupAfterRender, + prepareHostDispatcher, } from './ReactFizzConfigDOM'; import {stringToChunk} from 'react-server/src/ReactServerStreamConfig'; diff --git a/packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js index e2daf606c8ebd..c7ce2e767ef98 100644 --- a/packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js @@ -7,6 +7,35 @@ * @flow */ +import type { + PrefetchDNSOptions, + PreconnectOptions, + PreloadOptions, + PreinitOptions, +} from 'react-dom/src/ReactDOMDispatcher'; + +import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; +const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher; + +import {ReactDOMFlightServerDispatcher} from './ReactDOMFlightServerHostDispatcher'; + +export function prepareHostDispatcher(): void { + ReactDOMCurrentDispatcher.current = ReactDOMFlightServerDispatcher; +} + // Used to distinguish these contexts from ones used in other renderers. // E.g. this can be used to distinguish legacy renderers from this modern one. export const isPrimaryRenderer = true; + +export type HintModel = + | string + | [ + string, + PrefetchDNSOptions | PreconnectOptions | PreloadOptions | PreinitOptions, + ]; + +export type Hints = Set; + +export function createHints(): Hints { + return new Set(); +} diff --git a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js index d92d7d50ea060..3f1e0a1b66f67 100644 --- a/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js +++ b/packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js @@ -10,4 +10,44 @@ // This client file is in the shared folder because it applies to both SSR and browser contexts. // It is the configuraiton of the FlightClient behavior which can run in either environment. -// In a future update this is where we will implement `dispatchDirective` such as for Float methods +import type {HintModel} from '../server/ReactFlightServerConfigDOM'; + +import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; +const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher; + +export function dispatchHint(code: string, model: HintModel): void { + const dispatcher = ReactDOMCurrentDispatcher.current; + if (dispatcher) { + let href, options; + if (typeof model === 'string') { + href = model; + } else { + href = model[0]; + options = model[1]; + } + switch (code) { + case 'D': { + // $FlowFixMe[prop-missing] options are not refined to their types by code + dispatcher.prefetchDNS(href, options); + return; + } + case 'C': { + // $FlowFixMe[prop-missing] options are not refined to their types by code + dispatcher.preconnect(href, options); + return; + } + case 'L': { + // $FlowFixMe[prop-missing] options are not refined to their types by code + // $FlowFixMe[incompatible-call] options are not refined to their types by code + dispatcher.preload(href, options); + return; + } + case 'I': { + // $FlowFixMe[prop-missing] options are not refined to their types by code + // $FlowFixMe[incompatible-call] options are not refined to their types by code + dispatcher.preinit(href, options); + return; + } + } + } +} diff --git a/packages/react-dom/src/ReactDOMDispatcher.js b/packages/react-dom/src/ReactDOMDispatcher.js new file mode 100644 index 0000000000000..fdead10cd0948 --- /dev/null +++ b/packages/react-dom/src/ReactDOMDispatcher.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type PrefetchDNSOptions = {}; +export type PreconnectOptions = {crossOrigin?: string}; +export type PreloadOptions = { + as: string, + crossOrigin?: string, + integrity?: string, + type?: string, +}; +export type PreinitOptions = { + as: string, + precedence?: string, + crossOrigin?: string, + integrity?: string, +}; + +export type HostDispatcher = { + prefetchDNS: (href: string, options?: ?PrefetchDNSOptions) => void, + preconnect: (href: string, options: ?PreconnectOptions) => void, + preload: (href: string, options: PreloadOptions) => void, + preinit: (href: string, options: PreinitOptions) => void, +}; diff --git a/packages/react-dom/src/ReactDOMFloat.js b/packages/react-dom/src/ReactDOMFloat.js index 99a867286d361..25a03fcd092e9 100644 --- a/packages/react-dom/src/ReactDOMFloat.js +++ b/packages/react-dom/src/ReactDOMFloat.js @@ -1,39 +1,72 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ +import type { + PreconnectOptions, + PreloadOptions, + PreinitOptions, +} from './ReactDOMDispatcher'; + import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; +const Dispatcher = ReactDOMSharedInternals.Dispatcher; -export function prefetchDNS() { - const dispatcher = ReactDOMSharedInternals.Dispatcher.current; +export function prefetchDNS(href: string) { + let passedOptionArg: any; + if (__DEV__) { + if (arguments[1] !== undefined) { + passedOptionArg = arguments[1]; + } + } + const dispatcher = Dispatcher.current; if (dispatcher) { - dispatcher.prefetchDNS.apply(this, arguments); + if (__DEV__) { + if (passedOptionArg !== undefined) { + // prefetchDNS will warn if you pass reserved options arg. We pass it along in Dev only to + // elicit the warning. In prod we do not forward since it is not a part of the interface. + // @TODO move all arg validation into this file. It needs to be universal anyway so may as well lock down the interace here and + // let the rest of the codebase trust the types + dispatcher.prefetchDNS(href, passedOptionArg); + } else { + dispatcher.prefetchDNS(href); + } + } else { + dispatcher.prefetchDNS(href); + } } // We don't error because preconnect needs to be resilient to being called in a variety of scopes // and the runtime may not be capable of responding. The function is optimistic and not critical // so we favor silent bailout over warning or erroring. } -export function preconnect() { - const dispatcher = ReactDOMSharedInternals.Dispatcher.current; +export function preconnect(href: string, options?: ?PreconnectOptions) { + const dispatcher = Dispatcher.current; if (dispatcher) { - dispatcher.preconnect.apply(this, arguments); + dispatcher.preconnect(href, options); } // We don't error because preconnect needs to be resilient to being called in a variety of scopes // and the runtime may not be capable of responding. The function is optimistic and not critical // so we favor silent bailout over warning or erroring. } -export function preload() { - const dispatcher = ReactDOMSharedInternals.Dispatcher.current; +export function preload(href: string, options: PreloadOptions) { + const dispatcher = Dispatcher.current; if (dispatcher) { - dispatcher.preload.apply(this, arguments); + dispatcher.preload(href, options); } // We don't error because preload needs to be resilient to being called in a variety of scopes // and the runtime may not be capable of responding. The function is optimistic and not critical // so we favor silent bailout over warning or erroring. } -export function preinit() { - const dispatcher = ReactDOMSharedInternals.Dispatcher.current; +export function preinit(href: string, options: PreinitOptions) { + const dispatcher = Dispatcher.current; if (dispatcher) { - dispatcher.preinit.apply(this, arguments); + dispatcher.preinit(href, options); } // We don't error because preinit needs to be resilient to being called in a variety of scopes // and the runtime may not be capable of responding. The function is optimistic and not critical diff --git a/packages/react-dom/src/ReactDOMSharedInternals.js b/packages/react-dom/src/ReactDOMSharedInternals.js index a9e0407b006b2..cf2909ffac2ff 100644 --- a/packages/react-dom/src/ReactDOMSharedInternals.js +++ b/packages/react-dom/src/ReactDOMSharedInternals.js @@ -7,11 +7,13 @@ * @flow */ +import type {HostDispatcher} from './ReactDOMDispatcher'; + type InternalsType = { usingClientEntryPoint: boolean, Events: [any, any, any, any, any, any], Dispatcher: { - current: mixed, + current: null | HostDispatcher, }, }; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index aa9222adb0766..1f8746c0e96cc 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -3701,7 +3701,7 @@ describe('ReactDOMFizzServer', () => { Array.from(document.head.getElementsByTagName('script')).map( n => n.outerHTML, ), - ).toEqual(['']); + ).toEqual(['']); expect(getVisibleChildren(document)).toEqual( diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index e93af6fad421d..0aeef3357bd83 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -3981,7 +3981,7 @@ body { }); // @gate enableFloat - it('creates a preload resource when ReactDOM.preinit(..., {as: "script" }) is called outside of render on the client', async () => { + it('creates a script resource when ReactDOM.preinit(..., {as: "script" }) is called outside of render on the client', async () => { function App() { React.useEffect(() => { ReactDOM.preinit('foo', {as: 'script'}); diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js index e79fa16452bda..2167175a90b1b 100644 --- a/packages/react-dom/src/client/ReactDOMRoot.js +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -13,8 +13,6 @@ import type { TransitionTracingCallbacks, } from 'react-reconciler/src/ReactInternalTypes'; -import ReactDOMSharedInternals from '../ReactDOMSharedInternals'; -const {Dispatcher} = ReactDOMSharedInternals; import {ReactDOMClientDispatcher} from 'react-dom-bindings/src/client/ReactFiberConfigDOM'; import {queueExplicitHydrationTarget} from 'react-dom-bindings/src/events/ReactDOMEventReplaying'; import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; @@ -25,13 +23,19 @@ import { disableCommentsAsDOMContainers, } from 'shared/ReactFeatureFlags'; +import ReactDOMSharedInternals from '../ReactDOMSharedInternals'; +const {Dispatcher} = ReactDOMSharedInternals; +if (enableFloat && typeof document !== 'undefined') { + // Set the default dispatcher to the client dispatcher + Dispatcher.current = ReactDOMClientDispatcher; +} + export type RootType = { render(children: ReactNodeList): void, unmount(): void, _internalRoot: FiberRoot | null, ... }; - export type CreateRootOptions = { unstable_strictMode?: boolean, unstable_concurrentUpdatesByDefault?: boolean, diff --git a/packages/react-native-renderer/src/server/ReactFizzConfigNative.js b/packages/react-native-renderer/src/server/ReactFizzConfigNative.js index 54650e851f614..4e348d1e8ac20 100644 --- a/packages/react-native-renderer/src/server/ReactFizzConfigNative.js +++ b/packages/react-native-renderer/src/server/ReactFizzConfigNative.js @@ -339,8 +339,7 @@ export function hoistResources( boundaryResources: BoundaryResources, ) {} -export function prepareToRender(resources: Resources) {} -export function cleanupAfterRender(previousDispatcher: mixed) {} +export function prepareHostDispatcher() {} export function createResources() {} export function createBoundaryResources() {} export function setCurrentlyRenderingBoundaryResourcesTarget( diff --git a/packages/react-native-renderer/src/server/ReactFlightServerConfigNative.js b/packages/react-native-renderer/src/server/ReactFlightServerConfigNative.js index cc92e90d8b342..e13d4c5ab7205 100644 --- a/packages/react-native-renderer/src/server/ReactFlightServerConfigNative.js +++ b/packages/react-native-renderer/src/server/ReactFlightServerConfigNative.js @@ -8,3 +8,12 @@ */ export const isPrimaryRenderer = true; + +export type Hints = null; +export type HintModel = ''; + +export function createHints(): null { + return null; +} + +export function prepareHostDispatcher() {} diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js index 52794a6221c3f..32b3aa2c4f21e 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightServer.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js @@ -63,6 +63,7 @@ const ReactNoopFlightServer = ReactFlightServer({ ) { return saveModule(reference.value); }, + prepareHostDispatcher() {}, }); type Options = { diff --git a/packages/react-noop-renderer/src/ReactNoopServer.js b/packages/react-noop-renderer/src/ReactNoopServer.js index cd5edd94d29d6..c8f4fcd2ca4d7 100644 --- a/packages/react-noop-renderer/src/ReactNoopServer.js +++ b/packages/react-noop-renderer/src/ReactNoopServer.js @@ -279,8 +279,7 @@ const ReactNoopServer = ReactFizzServer({ setCurrentlyRenderingBoundaryResourcesTarget(resources: BoundaryResources) {}, - prepareToRender() {}, - cleanupAfterRender() {}, + prepareHostDispatcher() {}, }); type Options = { diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js index 43d24623e53bb..4268993e76faf 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js @@ -17,6 +17,7 @@ import { resolveModule, resolveErrorDev, resolveErrorProd, + resolveHint, close, getRoot, } from 'react-client/src/ReactFlightClient'; @@ -30,10 +31,14 @@ export function resolveRow(response: Response, chunk: RowEncoding): void { } else if (chunk[0] === 'I') { // $FlowFixMe[incompatible-call] unable to refine on array indices resolveModule(response, chunk[1], chunk[2]); + } else if (chunk[0] === 'H') { + // $FlowFixMe[incompatible-call] unable to refine on array indices + resolveHint(response, chunk[1], chunk[2]); } else { if (__DEV__) { resolveErrorDev( response, + // $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples. chunk[1], // $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples. // $FlowFixMe[prop-missing] diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js index 73b793f0d715c..5e7ae5ae8784c 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js @@ -7,6 +7,7 @@ * @flow */ +import type {HintModel} from 'react-server/src/ReactFlightServerConfig'; import type {ClientReferenceMetadata} from 'ReactFlightDOMRelayServerIntegration'; export type JSONValue = @@ -20,6 +21,7 @@ export type JSONValue = export type RowEncoding = | ['O', number, JSONValue] | ['I', number, ClientReferenceMetadata] + | ['H', string, HintModel] | ['P', number, string] | ['S', number, string] | [ diff --git a/packages/react-server-dom-relay/src/ReactFlightServerConfigDOMRelay.js b/packages/react-server-dom-relay/src/ReactFlightServerConfigDOMRelay.js index 0f709b7dbcc24..e270e543ab95b 100644 --- a/packages/react-server-dom-relay/src/ReactFlightServerConfigDOMRelay.js +++ b/packages/react-server-dom-relay/src/ReactFlightServerConfigDOMRelay.js @@ -7,6 +7,7 @@ * @flow */ +import type {HintModel} from 'react-server/src/ReactFlightServerConfig'; import type {RowEncoding, JSONValue} from './ReactFlightDOMRelayProtocol'; import type { @@ -191,6 +192,16 @@ export function processImportChunk( return ['I', id, clientReferenceMetadata]; } +export function processHintChunk( + request: Request, + id: number, + code: string, + model: HintModel, +): Chunk { + // The hint is already a JSON serializable value. + return ['H', code, model]; +} + export function scheduleWork(callback: () => void) { callback(); } @@ -198,8 +209,7 @@ export function scheduleWork(callback: () => void) { export function flushBuffered(destination: Destination) {} export const supportsRequestStorage = false; -export const requestStorage: AsyncLocalStorage> = - (null: any); +export const requestStorage: AsyncLocalStorage = (null: any); export function beginWriting(destination: Destination) {} diff --git a/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js b/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js index bc550d63ea7ea..88d7d3c52ae40 100644 --- a/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js +++ b/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js @@ -24,8 +24,7 @@ export function scheduleWork(callback: () => void) { export function flushBuffered(destination: Destination) {} export const supportsRequestStorage = false; -export const requestStorage: AsyncLocalStorage> = - (null: any); +export const requestStorage: AsyncLocalStorage = (null: any); export function beginWriting(destination: Destination) {} diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index cb30243215fc5..fb99875425279 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -25,9 +25,11 @@ let clientModuleError; let webpackMap; let Stream; let React; +let ReactDOM; let ReactDOMClient; let ReactServerDOMServer; let ReactServerDOMClient; +let ReactDOMFizzServer; let Suspense; let ErrorBoundary; @@ -42,6 +44,8 @@ describe('ReactFlightDOM', () => { Stream = require('stream'); React = require('react'); + ReactDOM = require('react-dom'); + ReactDOMFizzServer = require('react-dom/server.node'); use = React.use; Suspense = React.Suspense; ReactDOMClient = require('react-dom/client'); @@ -1153,4 +1157,292 @@ describe('ReactFlightDOM', () => { ); expect(reportedErrors).toEqual([theError]); }); + + // @gate enableUseHook + it('should support ReactDOM.preload when rendering in Fiber', async () => { + function Component() { + return

hello world

; + } + + const ClientComponent = clientExports(Component); + + async function ServerComponent() { + ReactDOM.preload('before', {as: 'style'}); + await 1; + ReactDOM.preload('after', {as: 'style'}); + return ; + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + + let response = null; + function getResponse() { + if (response === null) { + response = ReactServerDOMClient.createFromReadableStream(readable); + } + return response; + } + + function App() { + return getResponse(); + } + + // We pause to allow the float call after the await point to process before the + // HostDispatcher gets set for Fiber by createRoot. This is only needed in testing + // because the module graphs are not different and the HostDispatcher is shared. + // In a real environment the Fiber and Flight code would each have their own independent + // dispatcher. + // @TODO consider what happens when Server-Components-On-The-Client exist. we probably + // want to use the Fiber HostDispatcher there too since it is more about the host than the runtime + // but we need to make sure that actually makes sense + await 1; + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(document.head.innerHTML).toBe( + '' + + '', + ); + expect(container.innerHTML).toBe('

hello world

'); + }); + + // @gate enableUseHook + it('should support ReactDOM.preload when rendering in Fizz', async () => { + function Component() { + return

hello world

; + } + + const ClientComponent = clientExports(Component); + + async function ServerComponent() { + ReactDOM.preload('before', {as: 'style'}); + await 1; + ReactDOM.preload('after', {as: 'style'}); + return ; + } + + const {writable: flightWritable, readable: flightReadable} = + getTestStream(); + const {writable: fizzWritable, readable: fizzReadable} = getTestStream(); + + // In a real environment you would want to call the render during the Fizz render. + // The reason we cannot do this in our test is because we don't actually have two separate + // module graphs and we are contriving the sequencing to work in a way where + // the right HostDispatcher is in scope during the Flight Server Float calls and the + // Flight Client hint dispatches + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(flightWritable); + + let response = null; + function getResponse() { + if (response === null) { + response = + ReactServerDOMClient.createFromReadableStream(flightReadable); + } + return response; + } + + function App() { + return ( + + {getResponse()} + + ); + } + + await act(async () => { + ReactDOMFizzServer.renderToPipeableStream().pipe(fizzWritable); + }); + + const decoder = new TextDecoder(); + const reader = fizzReadable.getReader(); + let content = ''; + while (true) { + const {done, value} = await reader.read(); + if (done) { + content += decoder.decode(); + break; + } + content += decoder.decode(value, {stream: true}); + } + + expect(content).toEqual( + '' + + '

hello world

', + ); + }); + + it('supports Float hints from concurrent Flight -> Fizz renders', async () => { + function Component() { + return

hello world

; + } + + const ClientComponent = clientExports(Component); + + async function ServerComponent1() { + ReactDOM.preload('before1', {as: 'style'}); + await 1; + ReactDOM.preload('after1', {as: 'style'}); + return ; + } + + async function ServerComponent2() { + ReactDOM.preload('before2', {as: 'style'}); + await 1; + ReactDOM.preload('after2', {as: 'style'}); + return ; + } + + const {writable: flightWritable1, readable: flightReadable1} = + getTestStream(); + const {writable: flightWritable2, readable: flightReadable2} = + getTestStream(); + + ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ).pipe(flightWritable1); + + ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ).pipe(flightWritable2); + + const responses = new Map(); + function getResponse(stream) { + let response = responses.get(stream); + if (!response) { + response = ReactServerDOMClient.createFromReadableStream(stream); + responses.set(stream, response); + } + return response; + } + + function App({stream}) { + return ( + + {getResponse(stream)} + + ); + } + + // pausing to let Flight runtime tick. This is a test only artifact of the fact that + // we aren't operating separate module graphs for flight and fiber. In a real app + // each would have their own dispatcher and there would be no cross dispatching. + await 1; + + const {writable: fizzWritable1, readable: fizzReadable1} = getTestStream(); + const {writable: fizzWritable2, readable: fizzReadable2} = getTestStream(); + await act(async () => { + ReactDOMFizzServer.renderToPipeableStream( + , + ).pipe(fizzWritable1); + ReactDOMFizzServer.renderToPipeableStream( + , + ).pipe(fizzWritable2); + }); + + async function read(stream) { + const decoder = new TextDecoder(); + const reader = stream.getReader(); + let buffer = ''; + while (true) { + const {done, value} = await reader.read(); + if (done) { + buffer += decoder.decode(); + break; + } + buffer += decoder.decode(value, {stream: true}); + } + return buffer; + } + + const [content1, content2] = await Promise.all([ + read(fizzReadable1), + read(fizzReadable2), + ]); + + expect(content1).toEqual( + '' + + '

hello world

', + ); + expect(content2).toEqual( + '' + + '

hello world

', + ); + }); + + it('supports deduping hints by Float key', async () => { + function Component() { + return

hello world

; + } + + const ClientComponent = clientExports(Component); + + async function ServerComponent() { + ReactDOM.prefetchDNS('dns'); + ReactDOM.preconnect('preconnect'); + ReactDOM.preload('load', {as: 'style'}); + ReactDOM.preinit('init', {as: 'script'}); + // again but vary preconnect to demonstrate crossOrigin participates in the key + ReactDOM.prefetchDNS('dns'); + ReactDOM.preconnect('preconnect', {crossOrigin: 'anonymous'}); + ReactDOM.preload('load', {as: 'style'}); + ReactDOM.preinit('init', {as: 'script'}); + await 1; + // after an async point + ReactDOM.prefetchDNS('dns'); + ReactDOM.preconnect('preconnect', {crossOrigin: 'use-credentials'}); + ReactDOM.preload('load', {as: 'style'}); + ReactDOM.preinit('init', {as: 'script'}); + return ; + } + + const {writable, readable} = getTestStream(); + + ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ).pipe(writable); + + const hintRows = []; + async function collectHints(stream) { + const decoder = new TextDecoder(); + const reader = stream.getReader(); + let buffer = ''; + while (true) { + const {done, value} = await reader.read(); + if (done) { + buffer += decoder.decode(); + if (buffer.includes(':H')) { + hintRows.push(buffer); + } + break; + } + buffer += decoder.decode(value, {stream: true}); + let line; + while ((line = buffer.indexOf('\n')) > -1) { + const row = buffer.slice(0, line); + buffer = buffer.slice(line + 1); + if (row.includes(':H')) { + hintRows.push(row); + } + } + } + } + + await collectHints(readable); + expect(hintRows.length).toEqual(6); + }); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index 2847801d9499c..c846493de8430 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -21,7 +21,9 @@ let webpackMap; let webpackServerMap; let act; let React; +let ReactDOM; let ReactDOMClient; +let ReactDOMFizzServer; let ReactServerDOMServer; let ReactServerDOMClient; let Suspense; @@ -37,7 +39,9 @@ describe('ReactFlightDOMBrowser', () => { webpackMap = WebpackMock.webpackMap; webpackServerMap = WebpackMock.webpackServerMap; React = require('react'); + ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); + ReactDOMFizzServer = require('react-dom/server.browser'); ReactServerDOMServer = require('react-server-dom-webpack/server.browser'); ReactServerDOMClient = require('react-server-dom-webpack/client'); Suspense = React.Suspense; @@ -1062,4 +1066,118 @@ describe('ReactFlightDOMBrowser', () => { expect(thrownError.digest).toBe('test-error-digest'); } }); + + it('supports Float hints before the first await in server components in Fiber', async () => { + function Component() { + return

hello world

; + } + + const ClientComponent = clientExports(Component); + + async function ServerComponent() { + ReactDOM.preload('before', {as: 'style'}); + await 1; + ReactDOM.preload('after', {as: 'style'}); + return ; + } + + const stream = ReactServerDOMServer.renderToReadableStream( + , + webpackMap, + ); + + let response = null; + function getResponse() { + if (response === null) { + response = ReactServerDOMClient.createFromReadableStream(stream); + } + return response; + } + + function App() { + return getResponse(); + } + + // pausing to let Flight runtime tick. This is a test only artifact of the fact that + // we aren't operating separate module graphs for flight and fiber. In a real app + // each would have their own dispatcher and there would be no cross dispatching. + await 1; + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(document.head.innerHTML).toBe( + '', + ); + expect(container.innerHTML).toBe('

hello world

'); + }); + + it('Does not support Float hints in server components anywhere in Fizz', async () => { + // In environments that do not support AsyncLocalStorage the Flight client has no ability + // to scope hint dispatching to a specific Request. In Fiber this isn't a problem because + // the Browser scope acts like a singleton and we can dispatch away. But in Fizz we need to have + // a reference to Resources and this is only possible during render unless you support AsyncLocalStorage. + function Component() { + return

hello world

; + } + + const ClientComponent = clientExports(Component); + + async function ServerComponent() { + ReactDOM.preload('before', {as: 'style'}); + await 1; + ReactDOM.preload('after', {as: 'style'}); + return ; + } + + const stream = ReactServerDOMServer.renderToReadableStream( + , + webpackMap, + ); + + let response = null; + function getResponse() { + if (response === null) { + response = ReactServerDOMClient.createFromReadableStream(stream); + } + return response; + } + + function App() { + return ( + + {getResponse()} + + ); + } + + // pausing to let Flight runtime tick. This is a test only artifact of the fact that + // we aren't operating separate module graphs for flight and fiber. In a real app + // each would have their own dispatcher and there would be no cross dispatching. + await 1; + + let fizzStream; + await act(async () => { + fizzStream = await ReactDOMFizzServer.renderToReadableStream(); + }); + + const decoder = new TextDecoder(); + const reader = fizzStream.getReader(); + let content = ''; + while (true) { + const {done, value} = await reader.read(); + if (done) { + content += decoder.decode(); + break; + } + content += decoder.decode(value, {stream: true}); + } + + expect(content).toEqual( + '' + + '

hello world

', + ); + }); }); diff --git a/packages/react-server-native-relay/src/ReactFlightClientConfigNativeRelay.js b/packages/react-server-native-relay/src/ReactFlightClientConfigNativeRelay.js index e21df4d13e80c..7cdd99d1db2ce 100644 --- a/packages/react-server-native-relay/src/ReactFlightClientConfigNativeRelay.js +++ b/packages/react-server-native-relay/src/ReactFlightClientConfigNativeRelay.js @@ -95,3 +95,5 @@ const dummy = {}; export function parseModel(response: Response, json: UninitializedModel): T { return (parseModelRecursively(response, dummy, '', json): any); } + +export function dispatchHint(code: string, model: mixed) {} diff --git a/packages/react-server-native-relay/src/ReactFlightServerConfigNativeRelay.js b/packages/react-server-native-relay/src/ReactFlightServerConfigNativeRelay.js index ab815ae2f0144..7feb8628f2486 100644 --- a/packages/react-server-native-relay/src/ReactFlightServerConfigNativeRelay.js +++ b/packages/react-server-native-relay/src/ReactFlightServerConfigNativeRelay.js @@ -187,6 +187,17 @@ export function processImportChunk( return ['I', id, clientReferenceMetadata]; } +export function processHintChunk( + request: Request, + id: number, + code: string, + model: JSONValue, +): Chunk { + throw new Error( + 'React Internal Error: processHintChunk is not implemented for Native-Relay. The fact that this method was called means there is a bug in React.', + ); +} + export function scheduleWork(callback: () => void) { callback(); } @@ -194,8 +205,7 @@ export function scheduleWork(callback: () => void) { export function flushBuffered(destination: Destination) {} export const supportsRequestStorage = false; -export const requestStorage: AsyncLocalStorage> = - (null: any); +export const requestStorage: AsyncLocalStorage = (null: any); export function beginWriting(destination: Destination) {} diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index eab8c6c50e366..489c440ee78d5 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -71,11 +71,12 @@ import { writeHoistables, writePostamble, hoistResources, - prepareToRender, - cleanupAfterRender, setCurrentlyRenderingBoundaryResourcesTarget, createResources, createBoundaryResources, + prepareHostDispatcher, + supportsRequestStorage, + requestStorage, } from './ReactFizzConfig'; import { constructClassInstance, @@ -210,6 +211,7 @@ const CLOSED = 2; export opaque type Request = { destination: null | Destination, + flushScheduled: boolean, +responseState: ResponseState, +progressiveChunkSize: number, status: 0 | 1 | 2, @@ -277,11 +279,13 @@ export function createRequest( onShellError: void | ((error: mixed) => void), onFatalError: void | ((error: mixed) => void), ): Request { + prepareHostDispatcher(); const pingedTasks: Array = []; const abortSet: Set = new Set(); const resources: Resources = createResources(); const request: Request = { destination: null, + flushScheduled: false, responseState, progressiveChunkSize: progressiveChunkSize === undefined @@ -332,10 +336,22 @@ export function createRequest( return request; } +let currentRequest: null | Request = null; + +export function resolveRequest(): null | Request { + if (currentRequest) return currentRequest; + if (supportsRequestStorage) { + const store = requestStorage.getStore(); + if (store) return store; + } + return null; +} + function pingTask(request: Request, task: Task): void { const pingedTasks = request.pingedTasks; pingedTasks.push(task); - if (pingedTasks.length === 1) { + if (request.pingedTasks.length === 1) { + request.flushScheduled = request.destination !== null; scheduleWork(() => performWork(request)); } } @@ -1947,7 +1963,9 @@ export function performWork(request: Request): void { ReactCurrentCache.current = DefaultCacheDispatcher; } - const previousHostDispatcher = prepareToRender(request.resources); + const prevRequest = currentRequest; + currentRequest = request; + let prevGetCurrentStackImpl; if (__DEV__) { prevGetCurrentStackImpl = ReactDebugCurrentFrame.getCurrentStack; @@ -1975,7 +1993,6 @@ export function performWork(request: Request): void { if (enableCache) { ReactCurrentCache.current = prevCacheDispatcher; } - cleanupAfterRender(previousHostDispatcher); if (__DEV__) { ReactDebugCurrentFrame.getCurrentStack = prevGetCurrentStackImpl; @@ -1990,6 +2007,7 @@ export function performWork(request: Request): void { // we'll to restore the context to what it was before returning. switchContext(prevContext); } + currentRequest = prevRequest; } } @@ -2389,6 +2407,7 @@ function flushCompletedQueues( // We don't need to check any partially completed segments because // either they have pending task or they're complete. ) { + request.flushScheduled = false; if (enableFloat) { writePostamble(destination, request.responseState); } @@ -2411,7 +2430,27 @@ function flushCompletedQueues( } export function startWork(request: Request): void { - scheduleWork(() => performWork(request)); + request.flushScheduled = request.destination !== null; + if (supportsRequestStorage) { + scheduleWork(() => requestStorage.run(request, performWork, request)); + } else { + scheduleWork(() => performWork(request)); + } +} + +function enqueueFlush(request: Request): void { + if ( + request.flushScheduled === false && + // If there are pinged tasks we are going to flush anyway after work completes + request.pingedTasks.length === 0 && + // If there is no destination there is nothing we can flush to. A flush will + // happen when we start flowing again + request.destination !== null + ) { + const destination = request.destination; + request.flushScheduled = true; + scheduleWork(() => flushCompletedQueues(request, destination)); + } } export function startFlowing(request: Request, destination: Destination): void { @@ -2456,3 +2495,11 @@ export function abort(request: Request, reason: mixed): void { fatalError(request, error); } } + +export function flushResources(request: Request): void { + enqueueFlush(request); +} + +export function getResources(request: Request): Resources { + return request.resources; +} diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index dc37f750ae277..a4048e9924b41 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -16,6 +16,8 @@ import type { ClientReferenceKey, ServerReference, ServerReferenceId, + Hints, + HintModel, } from './ReactFlightServerConfig'; import type {ContextSnapshot} from './ReactFlightNewContext'; import type {ThenableState} from './ReactFlightThenable'; @@ -44,6 +46,7 @@ import { processErrorChunkProd, processErrorChunkDev, processReferenceChunk, + processHintChunk, resolveClientReferenceMetadata, getServerReferenceId, getServerReferenceBoundArguments, @@ -52,6 +55,8 @@ import { isServerReference, supportsRequestStorage, requestStorage, + prepareHostDispatcher, + createHints, } from './ReactFlightServerConfig'; import { @@ -61,11 +66,7 @@ import { getThenableStateAfterSuspending, resetHooksForRequest, } from './ReactFlightHooks'; -import { - DefaultCacheDispatcher, - getCurrentCache, - setCurrentCache, -} from './ReactFlightCache'; +import {DefaultCacheDispatcher} from './flight/ReactFlightServerCache'; import { pushProvider, popProvider, @@ -148,15 +149,18 @@ type Task = { export type Request = { status: 0 | 1 | 2, + flushScheduled: boolean, fatalError: mixed, destination: null | Destination, bundlerConfig: ClientManifest, cache: Map, nextChunkId: number, pendingChunks: number, + hints: Hints, abortableTasks: Set, pingedTasks: Array, completedImportChunks: Array, + completedHintChunks: Array, completedJSONChunks: Array, completedErrorChunks: Array, writtenSymbols: Map, @@ -196,21 +200,26 @@ export function createRequest( 'Currently React only supports one RSC renderer at a time.', ); } + prepareHostDispatcher(); ReactCurrentCache.current = DefaultCacheDispatcher; const abortSet: Set = new Set(); const pingedTasks: Array = []; + const hints = createHints(); const request: Request = { status: OPEN, + flushScheduled: false, fatalError: null, destination: null, bundlerConfig, cache: new Map(), nextChunkId: 0, pendingChunks: 0, + hints, abortableTasks: abortSet, pingedTasks: pingedTasks, completedImportChunks: ([]: Array), + completedHintChunks: ([]: Array), completedJSONChunks: ([]: Array), completedErrorChunks: ([]: Array), writtenSymbols: new Map(), @@ -232,6 +241,17 @@ export function createRequest( return request; } +let currentRequest: null | Request = null; + +export function resolveRequest(): null | Request { + if (currentRequest) return currentRequest; + if (supportsRequestStorage) { + const store = requestStorage.getStore(); + if (store) return store; + } + return null; +} + function createRootContext( reqContext?: Array<[string, ServerContextJSONValue]>, ) { @@ -320,6 +340,23 @@ function serializeThenable(request: Request, thenable: Thenable): number { return newTask.id; } +export function emitHint( + request: Request, + code: string, + model: HintModel, +): void { + emitHintChunk(request, code, model); + enqueueFlush(request); +} + +export function getHints(request: Request): Hints { + return request.hints; +} + +export function getCache(request: Request): Map { + return request.cache; +} + function readThenable(thenable: Thenable): T { if (thenable.status === 'fulfilled') { return thenable.value; @@ -502,6 +539,7 @@ function pingTask(request: Request, task: Task): void { const pingedTasks = request.pingedTasks; pingedTasks.push(task); if (pingedTasks.length === 1) { + request.flushScheduled = request.destination !== null; scheduleWork(() => performWork(request)); } } @@ -1082,6 +1120,16 @@ function emitImportChunk( request.completedImportChunks.push(processedChunk); } +function emitHintChunk(request: Request, code: string, model: HintModel): void { + const processedChunk = processHintChunk( + request, + request.nextChunkId++, + code, + model, + ); + request.completedHintChunks.push(processedChunk); +} + function emitSymbolChunk(request: Request, id: number, name: string): void { const symbolReference = serializeSymbolReference(name); const processedChunk = processReferenceChunk(request, id, symbolReference); @@ -1195,9 +1243,9 @@ function retryTask(request: Request, task: Task): void { function performWork(request: Request): void { const prevDispatcher = ReactCurrentDispatcher.current; - const prevCache = getCurrentCache(); ReactCurrentDispatcher.current = HooksDispatcher; - setCurrentCache(request.cache); + const prevRequest = currentRequest; + currentRequest = request; prepareToUseHooksForRequest(request); try { @@ -1215,8 +1263,8 @@ function performWork(request: Request): void { fatalError(request, error); } finally { ReactCurrentDispatcher.current = prevDispatcher; - setCurrentCache(prevCache); resetHooksForRequest(); + currentRequest = prevRequest; } } @@ -1250,6 +1298,21 @@ function flushCompletedChunks( } } importsChunks.splice(0, i); + + // Next comes hints. + const hintChunks = request.completedHintChunks; + i = 0; + for (; i < hintChunks.length; i++) { + const chunk = hintChunks[i]; + const keepWriting: boolean = writeChunkAndReturn(destination, chunk); + if (!keepWriting) { + request.destination = null; + i++; + break; + } + } + hintChunks.splice(0, i); + // Next comes model data. const jsonChunks = request.completedJSONChunks; i = 0; @@ -1264,6 +1327,7 @@ function flushCompletedChunks( } } jsonChunks.splice(0, i); + // Finally, errors are sent. The idea is that it's ok to delay // any error messages and prioritize display of other parts of // the page. @@ -1281,6 +1345,7 @@ function flushCompletedChunks( } errorChunks.splice(0, i); } finally { + request.flushScheduled = false; completeWriting(destination); } flushBuffered(destination); @@ -1291,13 +1356,29 @@ function flushCompletedChunks( } export function startWork(request: Request): void { + request.flushScheduled = request.destination !== null; if (supportsRequestStorage) { - scheduleWork(() => requestStorage.run(request.cache, performWork, request)); + scheduleWork(() => requestStorage.run(request, performWork, request)); } else { scheduleWork(() => performWork(request)); } } +function enqueueFlush(request: Request): void { + if ( + request.flushScheduled === false && + // If there are pinged tasks we are going to flush anyway after work completes + request.pingedTasks.length === 0 && + // If there is no destination there is nothing we can flush to. A flush will + // happen when we start flowing again + request.destination !== null + ) { + const destination = request.destination; + request.flushScheduled = true; + scheduleWork(() => flushCompletedChunks(request, destination)); + } +} + export function startFlowing(request: Request, destination: Destination): void { if (request.status === CLOSING) { request.status = CLOSED; diff --git a/packages/react-server/src/ReactFlightServerConfigBundlerCustom.js b/packages/react-server/src/ReactFlightServerConfigBundlerCustom.js index e11c154d05f32..b1fbc93ee61b1 100644 --- a/packages/react-server/src/ReactFlightServerConfigBundlerCustom.js +++ b/packages/react-server/src/ReactFlightServerConfigBundlerCustom.js @@ -23,3 +23,4 @@ export const resolveClientReferenceMetadata = export const getServerReferenceId = $$$config.getServerReferenceId; export const getServerReferenceBoundArguments = $$$config.getServerReferenceBoundArguments; +export const prepareHostDispatcher = $$$config.prepareHostDispatcher; diff --git a/packages/react-server/src/ReactFlightServerConfigStream.js b/packages/react-server/src/ReactFlightServerConfigStream.js index 4377a313b374a..2180228f6bff2 100644 --- a/packages/react-server/src/ReactFlightServerConfigStream.js +++ b/packages/react-server/src/ReactFlightServerConfigStream.js @@ -75,11 +75,6 @@ import type {Chunk} from './ReactServerStreamConfig'; export type {Destination, Chunk} from './ReactServerStreamConfig'; -export { - supportsRequestStorage, - requestStorage, -} from './ReactServerStreamConfig'; - const stringify = JSON.stringify; function serializeRowHeader(tag: string, id: number) { @@ -156,6 +151,17 @@ export function processImportChunk( return stringToChunk(row); } +export function processHintChunk( + request: Request, + id: number, + code: string, + model: JSONValue, +): Chunk { + const json: string = stringify(model); + const row = serializeRowHeader('H' + code, id) + json + '\n'; + return stringToChunk(row); +} + export { scheduleWork, flushBuffered, diff --git a/packages/react-server/src/ReactServerStreamConfigBrowser.js b/packages/react-server/src/ReactServerStreamConfigBrowser.js index aa9cac7b2c373..28c16a92ede25 100644 --- a/packages/react-server/src/ReactServerStreamConfigBrowser.js +++ b/packages/react-server/src/ReactServerStreamConfigBrowser.js @@ -21,10 +21,6 @@ export function flushBuffered(destination: Destination) { // transform streams. https://github.com/whatwg/streams/issues/960 } -export const supportsRequestStorage = false; -export const requestStorage: AsyncLocalStorage> = - (null: any); - const VIEW_SIZE = 512; let currentView = null; let writtenBytes = 0; diff --git a/packages/react-server/src/ReactServerStreamConfigBun.js b/packages/react-server/src/ReactServerStreamConfigBun.js index 9cc88c4086475..b71b6542f36eb 100644 --- a/packages/react-server/src/ReactServerStreamConfigBun.js +++ b/packages/react-server/src/ReactServerStreamConfigBun.js @@ -26,10 +26,6 @@ export function flushBuffered(destination: Destination) { // transform streams. https://github.com/whatwg/streams/issues/960 } -// AsyncLocalStorage is not available in bun -export const supportsRequestStorage = false; -export const requestStorage = (null: any); - export function beginWriting(destination: Destination) {} export function writeChunk( diff --git a/packages/react-server/src/ReactServerStreamConfigEdge.js b/packages/react-server/src/ReactServerStreamConfigEdge.js index db6bfb14fee8b..e41bf7940134b 100644 --- a/packages/react-server/src/ReactServerStreamConfigEdge.js +++ b/packages/react-server/src/ReactServerStreamConfigEdge.js @@ -21,11 +21,6 @@ export function flushBuffered(destination: Destination) { // transform streams. https://github.com/whatwg/streams/issues/960 } -// For now, we get this from the global scope, but this will likely move to a module. -export const supportsRequestStorage = typeof AsyncLocalStorage === 'function'; -export const requestStorage: AsyncLocalStorage> = - supportsRequestStorage ? new AsyncLocalStorage() : (null: any); - const VIEW_SIZE = 512; let currentView = null; let writtenBytes = 0; diff --git a/packages/react-server/src/ReactServerStreamConfigNode.js b/packages/react-server/src/ReactServerStreamConfigNode.js index 5d33ce7d6576d..0313daf307a12 100644 --- a/packages/react-server/src/ReactServerStreamConfigNode.js +++ b/packages/react-server/src/ReactServerStreamConfigNode.js @@ -8,8 +8,8 @@ */ import type {Writable} from 'stream'; + import {TextEncoder} from 'util'; -import {AsyncLocalStorage} from 'async_hooks'; interface MightBeFlushable { flush?: () => void; @@ -34,10 +34,6 @@ export function flushBuffered(destination: Destination) { } } -export const supportsRequestStorage = true; -export const requestStorage: AsyncLocalStorage> = - new AsyncLocalStorage(); - const VIEW_SIZE = 2048; let currentView = null; let writtenBytes = 0; diff --git a/packages/react-server/src/ReactFlightCache.js b/packages/react-server/src/flight/ReactFlightServerCache.js similarity index 60% rename from packages/react-server/src/ReactFlightCache.js rename to packages/react-server/src/flight/ReactFlightServerCache.js index 7ac8aaa66222f..d386dbb9f2bb4 100644 --- a/packages/react-server/src/ReactFlightCache.js +++ b/packages/react-server/src/flight/ReactFlightServerCache.js @@ -9,24 +9,17 @@ import type {CacheDispatcher} from 'react-reconciler/src/ReactInternalTypes'; -import { - supportsRequestStorage, - requestStorage, -} from './ReactFlightServerConfig'; +import {resolveRequest, getCache} from '../ReactFlightServer'; function createSignal(): AbortSignal { return new AbortController().signal; } function resolveCache(): Map { - if (currentCache) return currentCache; - if (supportsRequestStorage) { - const cache = requestStorage.getStore(); - if (cache) return cache; + const request = resolveRequest(); + if (request) { + return getCache(request); } - // Since we override the dispatcher all the time, we're effectively always - // active and so to support cache() and fetch() outside of render, we yield - // an empty Map. return new Map(); } @@ -51,16 +44,3 @@ export const DefaultCacheDispatcher: CacheDispatcher = { return entry; }, }; - -let currentCache: Map | null = null; - -export function setCurrentCache( - cache: Map | null, -): Map | null { - currentCache = cache; - return currentCache; -} - -export function getCurrentCache(): Map | null { - return currentCache; -} diff --git a/packages/react-server/src/forks/ReactFizzConfig.custom.js b/packages/react-server/src/forks/ReactFizzConfig.custom.js index f761eb392f3c0..4b44462d9d412 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.custom.js +++ b/packages/react-server/src/forks/ReactFizzConfig.custom.js @@ -23,6 +23,8 @@ // So `$$$config` looks like a global variable, but it's // really an argument to a top-level wrapping function. +import type {Request} from 'react-server/src/ReactFizzServer'; + declare var $$$config: any; export opaque type Destination = mixed; // eslint-disable-line no-undef export opaque type ResponseState = mixed; @@ -33,6 +35,9 @@ export opaque type SuspenseBoundaryID = mixed; export const isPrimaryRenderer = false; +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); + export const getChildFormatContext = $$$config.getChildFormatContext; export const UNINITIALIZED_SUSPENSE_BOUNDARY_ID = $$$config.UNINITIALIZED_SUSPENSE_BOUNDARY_ID; @@ -68,8 +73,7 @@ export const writeCompletedBoundaryInstruction = $$$config.writeCompletedBoundaryInstruction; export const writeClientRenderBoundaryInstruction = $$$config.writeClientRenderBoundaryInstruction; -export const prepareToRender = $$$config.prepareToRender; -export const cleanupAfterRender = $$$config.cleanupAfterRender; +export const prepareHostDispatcher = $$$config.prepareHostDispatcher; // ------------------------- // Resources diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-browser.js b/packages/react-server/src/forks/ReactFizzConfig.dom-browser.js index f2cf57ab5942f..5f887770d211e 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-browser.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-browser.js @@ -6,5 +6,9 @@ * * @flow */ +import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-bun.js b/packages/react-server/src/forks/ReactFizzConfig.dom-bun.js index f2cf57ab5942f..5f887770d211e 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-bun.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-bun.js @@ -6,5 +6,9 @@ * * @flow */ +import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-edge-webpack.js b/packages/react-server/src/forks/ReactFizzConfig.dom-edge-webpack.js index f2cf57ab5942f..67c8d7c13a78c 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-edge-webpack.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-edge-webpack.js @@ -6,5 +6,12 @@ * * @flow */ +import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; + +// For now, we get this from the global scope, but this will likely move to a module. +export const supportsRequestStorage = typeof AsyncLocalStorage === 'function'; +export const requestStorage: AsyncLocalStorage = supportsRequestStorage + ? new AsyncLocalStorage() + : (null: any); diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js b/packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js index 4760bb843ea89..903250ce22db2 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js @@ -6,5 +6,9 @@ * * @flow */ +import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-node-webpack.js b/packages/react-server/src/forks/ReactFizzConfig.dom-node-webpack.js index f2cf57ab5942f..71c6ab5a5586c 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-node-webpack.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-node-webpack.js @@ -6,5 +6,12 @@ * * @flow */ +import {AsyncLocalStorage} from 'async_hooks'; + +import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; + +export const supportsRequestStorage = true; +export const requestStorage: AsyncLocalStorage = + new AsyncLocalStorage(); diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-node.js b/packages/react-server/src/forks/ReactFizzConfig.dom-node.js index f2cf57ab5942f..99d0d74a7b76a 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-node.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-node.js @@ -7,4 +7,12 @@ * @flow */ +import {AsyncLocalStorage} from 'async_hooks'; + +import type {Request} from 'react-server/src/ReactFizzServer'; + export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; + +export const supportsRequestStorage = true; +export const requestStorage: AsyncLocalStorage = + new AsyncLocalStorage(); diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-relay.js b/packages/react-server/src/forks/ReactFizzConfig.dom-relay.js index f2cf57ab5942f..5f887770d211e 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.dom-relay.js +++ b/packages/react-server/src/forks/ReactFizzConfig.dom-relay.js @@ -6,5 +6,9 @@ * * @flow */ +import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFizzConfig.native-relay.js b/packages/react-server/src/forks/ReactFizzConfig.native-relay.js index c4981f9edf140..df90c935a9c92 100644 --- a/packages/react-server/src/forks/ReactFizzConfig.native-relay.js +++ b/packages/react-server/src/forks/ReactFizzConfig.native-relay.js @@ -6,5 +6,9 @@ * * @flow */ +import type {Request} from 'react-server/src/ReactFizzServer'; export * from 'react-native-renderer/src/server/ReactFizzConfigNative'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.custom.js b/packages/react-server/src/forks/ReactFlightServerConfig.custom.js index 28977b2357cc6..4590689e5f440 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.custom.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.custom.js @@ -6,8 +6,21 @@ * * @flow */ +import type {Request} from 'react-server/src/ReactFlightServer'; export * from '../ReactFlightServerConfigStream'; export * from '../ReactFlightServerConfigBundlerCustom'; +export type Hints = null; +export type HintModel = ''; + export const isPrimaryRenderer = false; + +export const prepareHostDispatcher = () => {}; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); + +export function createHints(): null { + return null; +} diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js index 5304ae8c21af8..a532e31b6c206 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js @@ -7,6 +7,11 @@ * @flow */ +import type {Request} from 'react-server/src/ReactFlightServer'; + export * from '../ReactFlightServerConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler'; export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js index 3778ad89ee89c..31db7c12414eb 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js @@ -7,6 +7,11 @@ * @flow */ +import type {Request} from 'react-server/src/ReactFlightServer'; + export * from '../ReactFlightServerConfigStream'; export * from '../ReactFlightServerConfigBundlerCustom'; export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-webpack.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-webpack.js index 5304ae8c21af8..036c0b7dc3a38 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-webpack.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-webpack.js @@ -6,7 +6,14 @@ * * @flow */ +import type {Request} from 'react-server/src/ReactFlightServer'; export * from '../ReactFlightServerConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler'; export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +// For now, we get this from the global scope, but this will likely move to a module. +export const supportsRequestStorage = typeof AsyncLocalStorage === 'function'; +export const requestStorage: AsyncLocalStorage = supportsRequestStorage + ? new AsyncLocalStorage() + : (null: any); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js index 5304ae8c21af8..a532e31b6c206 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js @@ -7,6 +7,11 @@ * @flow */ +import type {Request} from 'react-server/src/ReactFlightServer'; + export * from '../ReactFlightServerConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler'; export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-webpack.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-webpack.js index 5304ae8c21af8..62a70db4abe6f 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-webpack.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-webpack.js @@ -6,7 +6,14 @@ * * @flow */ +import {AsyncLocalStorage} from 'async_hooks'; + +import type {Request} from 'react-server/src/ReactFlightServer'; export * from '../ReactFlightServerConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler'; export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = true; +export const requestStorage: AsyncLocalStorage = + new AsyncLocalStorage(); diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js index 5304ae8c21af8..ccbacc8c1c3a0 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js @@ -7,6 +7,14 @@ * @flow */ +import {AsyncLocalStorage} from 'async_hooks'; + +import type {Request} from 'react-server/src/ReactFlightServer'; + export * from '../ReactFlightServerConfigStream'; export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler'; export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM'; + +export const supportsRequestStorage = true; +export const requestStorage: AsyncLocalStorage = + new AsyncLocalStorage(); diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.custom.js b/packages/react-server/src/forks/ReactServerStreamConfig.custom.js index ed00afa434e7e..f62c2a54035ba 100644 --- a/packages/react-server/src/forks/ReactServerStreamConfig.custom.js +++ b/packages/react-server/src/forks/ReactServerStreamConfig.custom.js @@ -35,8 +35,6 @@ export const writeChunk = $$$config.writeChunk; export const writeChunkAndReturn = $$$config.writeChunkAndReturn; export const completeWriting = $$$config.completeWriting; export const flushBuffered = $$$config.flushBuffered; -export const supportsRequestStorage = $$$config.supportsRequestStorage; -export const requestStorage = $$$config.requestStorage; export const close = $$$config.close; export const closeWithError = $$$config.closeWithError; export const stringToChunk = $$$config.stringToChunk; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index a2080dc38ed8f..50579ff47a039 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -461,5 +461,6 @@ "473": "React doesn't accept base64 encoded file uploads because we don't except form data passed from a browser to ever encode data that way. If that's the wrong assumption, we can easily fix it.", "474": "Suspense Exception: This is not a real error, and should not leak into userspace. If you're seeing this, it's likely a bug in React.", "475": "Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.", - "476": "Expected the form instance to be a HostComponent. This is a bug in React." + "476": "Expected the form instance to be a HostComponent. This is a bug in React.", + "477": "React Internal Error: processHintChunk is not implemented for Native-Relay. The fact that this method was called means there is a bug in React." } diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index f29fe5e17a433..23b779ddf8eb6 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -386,7 +386,7 @@ const bundles = [ global: 'ReactServerDOMClient', minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, - externals: ['react'], + externals: ['react', 'react-dom'], }, { bundleTypes: [NODE_DEV, NODE_PROD], @@ -395,7 +395,7 @@ const bundles = [ global: 'ReactServerDOMClient', minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, - externals: ['react', 'util'], + externals: ['react', 'react-dom', 'util'], }, { bundleTypes: [NODE_DEV, NODE_PROD], @@ -404,7 +404,7 @@ const bundles = [ global: 'ReactServerDOMClient', minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, - externals: ['react', 'util'], + externals: ['react', 'react-dom', 'util'], }, { bundleTypes: [NODE_DEV, NODE_PROD], @@ -413,7 +413,7 @@ const bundles = [ global: 'ReactServerDOMClient', minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, - externals: ['react'], + externals: ['react', 'react-dom'], }, /******* React Server DOM Webpack Plugin *******/