@samitouri / QOS-React / commits / 36e4cbe2e9

[Float][Flight] Flight support for Float (#26502)

Stacked on #26557 Supporting Float methods such as ReactDOM.preload() are challenging for flight because it does not have an easy means to convey direct executions in other environments. Because the flight wire format is a JSON-like serialization that is expected to be rendered it currently only describes renderable elements. We need a way to convey a function invocation that gets run in the context of the client environment whether that is Fizz or Fiber. Fiber is somewhat straightforward because the HostDispatcher is always active and we can just have the FlightClient dispatch the serialized directive. Fizz is much more challenging becaue the dispatcher is always scoped but the specific request the dispatch belongs to is not readily available. Environments that support AsyncLocalStorage (or in the future AsyncContext) we will use this to be able to resolve directives in Fizz to the appropriate Request. For other environments directives will be elided. Right now this is pragmatic and non-breaking because all directives are opportunistic and non-critical. If this changes in the future we will need to reconsider how widespread support for async context tracking is. For Flight, if AsyncLocalStorage is available Float methods can be called before and after await points and be expected to work. If AsyncLocalStorage is not available float methods called in the sync phase of a component render will be captured but anything after an await point will be a noop. If a float call is dropped in this manner a DEV warning should help you realize your code may need to be modified. This PR also introduces a way for resources (Fizz) and hints (Flight) to flush even if there is not active task being worked on. This will help when Float methods are called in between async points within a function execution but the task is blocked on the entire function finishing. This PR also introduces deduping of Hints in Flight using the same resource keys used in Fizz. This will help shrink payload sizes when the same hint is attempted to emit over and over again

Josh Story committed Apr 21, 2023 at 20:45 UTC 36e4cbe2e918ec9c8a7abbfda28898c835361fb2
56 files changed +1119 -188
packages/react-client/src/ReactFlightClient.js
+12
@@ -18,11 +18,14 @@ import type {
18 SSRManifest,
19 } from './ReactFlightClientConfig';
20
21 +import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
22 +
23 import {
24 resolveClientReference,
25 preloadModule,
26 requireModule,
27 parseModel,
28 + dispatchHint,
29 } from './ReactFlightClientConfig';
30
31 import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
@@ -778,6 +781,15 @@ export function resolveErrorDev(
781 }
782 }
783
784 +export function resolveHint(
785 + response: Response,
786 + code: string,
787 + model: UninitializedModel,
788 +): void {
789 + const hintModel = parseModel<HintModel>(response, model);
790 + dispatchHint(code, hintModel);
791 +}
792 +
793 export function close(response: Response): void {
794 // In case there are any remaining unresolved chunks, they won't
795 // be resolved now. So we need to issue an error to those.
packages/react-client/src/ReactFlightClientStream.js
+6
@@ -16,6 +16,7 @@ import {
16 resolveModel,
17 resolveErrorProd,
18 resolveErrorDev,
19 + resolveHint,
20 createResponse as createResponseBase,
21 parseModelString,
22 parseModelTuple,
@@ -46,6 +47,11 @@ function processFullRow(response: Response, row: string): void {
47 resolveModule(response, id, row.slice(colon + 2));
48 return;
49 }
50 + case 'H': {
51 + const code = row[colon + 2];
52 + resolveHint(response, code, row.slice(colon + 3));
53 + return;
54 + }
55 case 'E': {
56 const errorInfo = JSON.parse(row.slice(colon + 2));
57 if (__DEV__) {
packages/react-client/src/forks/ReactFlightClientConfig.custom.js
+1
@@ -35,6 +35,7 @@ export const resolveClientReference = $$$config.resolveClientReference;
35 export const resolveServerReference = $$$config.resolveServerReference;
36 export const preloadModule = $$$config.preloadModule;
37 export const requireModule = $$$config.requireModule;
38 +export const dispatchHint = $$$config.dispatchHint;
39
40 export opaque type Source = mixed;
41
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+9 -11
@@ -7,6 +7,7 @@
7 * @flow
8 */
9
10 +import type {HostDispatcher} from 'react-dom/src/ReactDOMDispatcher';
11 import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities';
12 import type {DOMEventName} from '../events/DOMEventNames';
13 import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
@@ -1917,10 +1918,6 @@ export function clearSingleton(instance: Instance): void {
1918
1919 export const supportsResources = true;
1920
1920 -// The resource types we support. currently they match the form for the as argument.
1921 -// In the future this may need to change, especially when modules / scripts are supported
1922 -type ResourceType = 'style' | 'font' | 'script';
1923 -
1921 type HoistableTagType = 'link' | 'meta' | 'title';
1922 type TResource<
1923 T: 'stylesheet' | 'style' | 'script' | 'void',
@@ -2011,7 +2008,7 @@ function getDocumentFromRoot(root: HoistableRoot): Document {
2008 // We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
2009 // internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
2010 // from Internals -> ReactDOM -> HostConfig -> Internals so this doesn't introduce a new one.
2014 -export const ReactDOMClientDispatcher = {
2011 +export const ReactDOMClientDispatcher: HostDispatcher = {
2012 prefetchDNS,
2013 preconnect,
2014 preload,
@@ -2085,7 +2082,10 @@ function prefetchDNS(href: string, options?: mixed) {
2082 preconnectAs('dns-prefetch', null, href);
2083 }
2084
2088 -function preconnect(href: string, options?: {crossOrigin?: string}) {
2085 +function preconnect(href: string, options: ?{crossOrigin?: string}) {
2086 + if (!enableFloat) {
2087 + return;
2088 + }
2089 if (__DEV__) {
2090 if (typeof href !== 'string' || !href) {
2091 console.error(
@@ -2113,9 +2113,8 @@ function preconnect(href: string, options?: {crossOrigin?: string}) {
2113 preconnectAs('preconnect', crossOrigin, href);
2114 }
2115
2116 -type PreloadAs = ResourceType;
2116 type PreloadOptions = {
2118 - as: PreloadAs,
2117 + as: string,
2118 crossOrigin?: string,
2119 integrity?: string,
2120 type?: string,
@@ -2164,7 +2163,7 @@ function preload(href: string, options: PreloadOptions) {
2163
2164 function preloadPropsFromPreloadOptions(
2165 href: string,
2167 - as: ResourceType,
2166 + as: string,
2167 options: PreloadOptions,
2168 ): PreloadProps {
2169 return {
@@ -2177,9 +2176,8 @@ function preloadPropsFromPreloadOptions(
2176 };
2177 }
2178
2180 -type PreinitAs = 'style' | 'script';
2179 type PreinitOptions = {
2182 - as: PreinitAs,
2180 + as: string,
2181 precedence?: string,
2182 crossOrigin?: string,
2183 integrity?: string,
packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js new
+118
@@ -0,0 +1,118 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {
11 + HostDispatcher,
12 + PrefetchDNSOptions,
13 + PreconnectOptions,
14 + PreloadOptions,
15 + PreinitOptions,
16 +} from 'react-dom/src/ReactDOMDispatcher';
17 +
18 +import {enableFloat} from 'shared/ReactFeatureFlags';
19 +
20 +import {
21 + emitHint,
22 + getHints,
23 + resolveRequest,
24 +} from 'react-server/src/ReactFlightServer';
25 +
26 +export const ReactDOMFlightServerDispatcher: HostDispatcher = {
27 + prefetchDNS,
28 + preconnect,
29 + preload,
30 + preinit,
31 +};
32 +
33 +function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) {
34 + if (enableFloat) {
35 + if (typeof href === 'string') {
36 + const request = resolveRequest();
37 + if (request) {
38 + const hints = getHints(request);
39 + const key = 'D' + href;
40 + if (hints.has(key)) {
41 + // duplicate hint
42 + return;
43 + }
44 + hints.add(key);
45 + if (options) {
46 + emitHint(request, 'D', [href, options]);
47 + } else {
48 + emitHint(request, 'D', href);
49 + }
50 + }
51 + }
52 + }
53 +}
54 +
55 +function preconnect(href: string, options: ?PreconnectOptions) {
56 + if (enableFloat) {
57 + if (typeof href === 'string') {
58 + const request = resolveRequest();
59 + if (request) {
60 + const hints = getHints(request);
61 + const crossOrigin =
62 + options == null || typeof options.crossOrigin !== 'string'
63 + ? null
64 + : options.crossOrigin === 'use-credentials'
65 + ? 'use-credentials'
66 + : '';
67 +
68 + const key = `C${crossOrigin === null ? 'null' : crossOrigin}|${href}`;
69 + if (hints.has(key)) {
70 + // duplicate hint
71 + return;
72 + }
73 + hints.add(key);
74 + if (options) {
75 + emitHint(request, 'C', [href, options]);
76 + } else {
77 + emitHint(request, 'C', href);
78 + }
79 + }
80 + }
81 + }
82 +}
83 +
84 +function preload(href: string, options: PreloadOptions) {
85 + if (enableFloat) {
86 + if (typeof href === 'string') {
87 + const request = resolveRequest();
88 + if (request) {
89 + const hints = getHints(request);
90 + const key = 'L' + href;
91 + if (hints.has(key)) {
92 + // duplicate hint
93 + return;
94 + }
95 + hints.add(key);
96 + emitHint(request, 'L', [href, options]);
97 + }
98 + }
99 + }
100 +}
101 +
102 +function preinit(href: string, options: PreinitOptions) {
103 + if (enableFloat) {
104 + if (typeof href === 'string') {
105 + const request = resolveRequest();
106 + if (request) {
107 + const hints = getHints(request);
108 + const key = 'I' + href;
109 + if (hints.has(key)) {
110 + // duplicate hint
111 + return;
112 + }
113 + hints.add(key);
114 + emitHint(request, 'I', [href, options]);
115 + }
116 + }
117 + }
118 +}
packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js
-3
@@ -21,9 +21,6 @@ export function scheduleWork(callback: () => void) {
21
22 export function flushBuffered(destination: Destination) {}
23
24 -export const supportsRequestStorage = false;
25 -export const requestStorage: AsyncLocalStorage<any> = (null: any);
26 -
24 export function beginWriting(destination: Destination) {}
25
26 export function writeChunk(
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+95 -72
@@ -38,6 +38,11 @@ import {
38 stringToPrecomputedChunk,
39 clonePrecomputedChunk,
40 } from 'react-server/src/ReactServerStreamConfig';
41 +import {
42 + resolveRequest,
43 + getResources,
44 + flushResources,
45 +} from 'react-server/src/ReactFizzServer';
46
47 import isAttributeNameSafe from '../shared/isAttributeNameSafe';
48 import isUnitlessNumber from '../shared/isUnitlessNumber';
@@ -79,30 +84,15 @@ import {
84 import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
85 const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
86
82 -const ReactDOMServerDispatcher = enableFloat
83 - ? {
84 - prefetchDNS,
85 - preconnect,
86 - preload,
87 - preinit,
88 - }
89 - : {};
90 -
91 -let currentResources: null | Resources = null;
92 -const currentResourcesStack = [];
93 -
94 -export function prepareToRender(resources: Resources): mixed {
95 - currentResourcesStack.push(currentResources);
96 - currentResources = resources;
87 +const ReactDOMServerDispatcher = {
88 + prefetchDNS,
89 + preconnect,
90 + preload,
91 + preinit,
92 +};
93
98 - const previousHostDispatcher = ReactDOMCurrentDispatcher.current;
94 +export function prepareHostDispatcher() {
95 ReactDOMCurrentDispatcher.current = ReactDOMServerDispatcher;
100 - return previousHostDispatcher;
101 -}
102 -
103 -export function cleanupAfterRender(previousDispatcher: mixed) {
104 - currentResources = currentResourcesStack.pop();
105 - ReactDOMCurrentDispatcher.current = previousDispatcher;
96 }
97
98 // Used to distinguish these contexts from ones used in other renderers.
@@ -4030,7 +4020,7 @@ export function writePreamble(
4020 // (User code could choose to send this even earlier by calling
4021 // preinit(...), if they know they will suspend).
4022 const {src, integrity} = responseState.externalRuntimeConfig;
4033 - preinitImpl(resources, src, {as: 'script', integrity});
4023 + internalPreinitScript(resources, src, integrity);
4024 }
4025
4026 const htmlChunks = responseState.htmlChunks;
@@ -4804,16 +4794,19 @@ function getResourceKey(as: string, href: string): string {
4794 }
4795
4796 export function prefetchDNS(href: string, options?: mixed) {
4807 - if (!currentResources) {
4808 - // While we expect that preconnect calls are primarily going to be observed
4809 - // during render because effects and events don't run on the server it is
4810 - // still possible that these get called in module scope. This is valid on
4811 - // the client since there is still a document to interact with but on the
4812 - // server we need a request to associate the call to. Because of this we
4813 - // simply return and do not warn.
4797 + if (!enableFloat) {
4798 + return;
4799 + }
4800 + const request = resolveRequest();
4801 + if (!request) {
4802 + // In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
4803 + // possibly get them from the stack if we are not in an async context. Since we were not able to resolve
4804 + // the resources for this call in either case we opt to do nothing. We can consider making this a warning
4805 + // but there may be times where calling a function outside of render is intentional (i.e. to warm up data
4806 + // fetching) and we don't want to warn in those cases.
4807 return;
4808 }
4816 - const resources = currentResources;
4809 + const resources = getResources(request);
4810 if (__DEV__) {
4811 if (typeof href !== 'string' || !href) {
4812 console.error(
@@ -4855,20 +4848,24 @@ export function prefetchDNS(href: string, options?: mixed) {
4848 );
4849 }
4850 resources.preconnects.add(resource);
4851 + flushResources(request);
4852 }
4853 }
4854
4861 -export function preconnect(href: string, options?: {crossOrigin?: string}) {
4862 - if (!currentResources) {
4863 - // While we expect that preconnect calls are primarily going to be observed
4864 - // during render because effects and events don't run on the server it is
4865 - // still possible that these get called in module scope. This is valid on
4866 - // the client since there is still a document to interact with but on the
4867 - // server we need a request to associate the call to. Because of this we
4868 - // simply return and do not warn.
4855 +export function preconnect(href: string, options?: ?{crossOrigin?: string}) {
4856 + if (!enableFloat) {
4857 + return;
4858 + }
4859 + const request = resolveRequest();
4860 + if (!request) {
4861 + // In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
4862 + // possibly get them from the stack if we are not in an async context. Since we were not able to resolve
4863 + // the resources for this call in either case we opt to do nothing. We can consider making this a warning
4864 + // but there may be times where calling a function outside of render is intentional (i.e. to warm up data
4865 + // fetching) and we don't want to warn in those cases.
4866 return;
4867 }
4871 - const resources = currentResources;
4868 + const resources = getResources(request);
4869 if (__DEV__) {
4870 if (typeof href !== 'string' || !href) {
4871 console.error(
@@ -4914,27 +4911,30 @@ export function preconnect(href: string, options?: {crossOrigin?: string}) {
4911 );
4912 }
4913 resources.preconnects.add(resource);
4914 + flushResources(request);
4915 }
4916 }
4917
4920 -type PreloadAs = 'style' | 'font' | 'script';
4918 type PreloadOptions = {
4922 - as: PreloadAs,
4919 + as: string,
4920 crossOrigin?: string,
4921 integrity?: string,
4922 type?: string,
4923 };
4924 export function preload(href: string, options: PreloadOptions) {
4928 - if (!currentResources) {
4929 - // While we expect that preload calls are primarily going to be observed
4930 - // during render because effects and events don't run on the server it is
4931 - // still possible that these get called in module scope. This is valid on
4932 - // the client since there is still a document to interact with but on the
4933 - // server we need a request to associate the call to. Because of this we
4934 - // simply return and do not warn.
4925 + if (!enableFloat) {
4926 return;
4927 }
4937 - const resources = currentResources;
4928 + const request = resolveRequest();
4929 + if (!request) {
4930 + // In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
4931 + // possibly get them from the stack if we are not in an async context. Since we were not able to resolve
4932 + // the resources for this call in either case we opt to do nothing. We can consider making this a warning
4933 + // but there may be times where calling a function outside of render is intentional (i.e. to warm up data
4934 + // fetching) and we don't want to warn in those cases.
4935 + return;
4936 + }
4937 + const resources = getResources(request);
4938 if (__DEV__) {
4939 if (typeof href !== 'string' || !href) {
4940 console.error(
@@ -5055,37 +5055,30 @@ export function preload(href: string, options: PreloadOptions) {
5055 resources.explicitOtherPreloads.add(resource);
5056 }
5057 }
5058 + flushResources(request);
5059 }
5060 }
5061
5061 -type PreinitAs = 'style' | 'script';
5062 type PreinitOptions = {
5063 - as: PreinitAs,
5063 + as: string,
5064 precedence?: string,
5065 crossOrigin?: string,
5066 integrity?: string,
5067 };
5068 -export function preinit(href: string, options: PreinitOptions): void {
5069 - if (!currentResources) {
5070 - // While we expect that preinit calls are primarily going to be observed
5071 - // during render because effects and events don't run on the server it is
5072 - // still possible that these get called in module scope. This is valid on
5073 - // the client since there is still a document to interact with but on the
5074 - // server we need a request to associate the call to. Because of this we
5075 - // simply return and do not warn.
5068 +function preinit(href: string, options: PreinitOptions): void {
5069 + if (!enableFloat) {
5070 return;
5071 }
5078 - preinitImpl(currentResources, href, options);
5079 -}
5080 -
5081 -// On the server, preinit may be called outside of render when sending an
5082 -// external SSR runtime as part of the initial resources payload. Since this
5083 -// is an internal React call, we do not need to use the resources stack.
5084 -function preinitImpl(
5085 - resources: Resources,
5086 - href: string,
5087 - options: PreinitOptions,
5088 -): void {
5072 + const request = resolveRequest();
5073 + if (!request) {
5074 + // In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
5075 + // possibly get them from the stack if we are not in an async context. Since we were not able to resolve
5076 + // the resources for this call in either case we opt to do nothing. We can consider making this a warning
5077 + // but there may be times where calling a function outside of render is intentional (i.e. to warm up data
5078 + // fetching) and we don't want to warn in those cases.
5079 + return;
5080 + }
5081 + const resources = getResources(request);
5082 if (__DEV__) {
5083 if (typeof href !== 'string' || !href) {
5084 console.error(
@@ -5214,6 +5207,7 @@ function preinitImpl(
5207 resources.stylePrecedences.set(precedence, emptyStyleResource);
5208 }
5209 precedenceSet.add(resource);
5210 + flushResources(request);
5211 }
5212 return;
5213 }
@@ -5288,6 +5282,7 @@ function preinitImpl(
5282 }
5283 resources.scripts.add(resource);
5284 pushScriptImpl(resource.chunks, resourceProps);
5285 + flushResources(request);
5286 }
5287 return;
5288 }
@@ -5295,9 +5290,37 @@ function preinitImpl(
5290 }
5291 }
5292
5293 +// This method is trusted. It must only be called from within this codebase and it assumes the arguments
5294 +// conform to the types because no user input is being passed in. It also assumes that it is being called as
5295 +// part of a work or flush loop and therefore does not need to request Fizz to flush Resources.
5296 +function internalPreinitScript(
5297 + resources: Resources,
5298 + src: string,
5299 + integrity: ?string,
5300 +): void {
5301 + const key = getResourceKey('script', src);
5302 + let resource = resources.scriptsMap.get(key);
5303 + if (!resource) {
5304 + resource = {
5305 + type: 'script',
5306 + chunks: [],
5307 + state: NoState,
5308 + props: null,
5309 + };
5310 + resources.scriptsMap.set(key, resource);
5311 + resources.scripts.add(resource);
5312 + pushScriptImpl(resource.chunks, {
5313 + async: true,
5314 + src,
5315 + integrity,
5316 + });
5317 + }
5318 + return;
5319 +}
5320 +
5321 function preloadPropsFromPreloadOptions(
5322 href: string,
5300 - as: PreloadAs,
5323 + as: string,
5324 options: PreloadOptions,
5325 ): PreloadProps {
5326 return {
packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
+1 -2
@@ -137,8 +137,7 @@ export {
137 writePostamble,
138 hoistResources,
139 setCurrentlyRenderingBoundaryResourcesTarget,
140 - prepareToRender,
141 - cleanupAfterRender,
140 + prepareHostDispatcher,
141 } from './ReactFizzConfigDOM';
142
143 import {stringToChunk} from 'react-server/src/ReactServerStreamConfig';
packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js
+29
@@ -7,6 +7,35 @@
7 * @flow
8 */
9
10 +import type {
11 + PrefetchDNSOptions,
12 + PreconnectOptions,
13 + PreloadOptions,
14 + PreinitOptions,
15 +} from 'react-dom/src/ReactDOMDispatcher';
16 +
17 +import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
18 +const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
19 +
20 +import {ReactDOMFlightServerDispatcher} from './ReactDOMFlightServerHostDispatcher';
21 +
22 +export function prepareHostDispatcher(): void {
23 + ReactDOMCurrentDispatcher.current = ReactDOMFlightServerDispatcher;
24 +}
25 +
26 // Used to distinguish these contexts from ones used in other renderers.
27 // E.g. this can be used to distinguish legacy renderers from this modern one.
28 export const isPrimaryRenderer = true;
29 +
30 +export type HintModel =
31 + | string
32 + | [
33 + string,
34 + PrefetchDNSOptions | PreconnectOptions | PreloadOptions | PreinitOptions,
35 + ];
36 +
37 +export type Hints = Set<string>;
38 +
39 +export function createHints(): Hints {
40 + return new Set();
41 +}
packages/react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js
+41 -1
@@ -10,4 +10,44 @@
10 // This client file is in the shared folder because it applies to both SSR and browser contexts.
11 // It is the configuraiton of the FlightClient behavior which can run in either environment.
12
13 -// In a future update this is where we will implement `dispatchDirective` such as for Float methods
13 +import type {HintModel} from '../server/ReactFlightServerConfigDOM';
14 +
15 +import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
16 +const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
17 +
18 +export function dispatchHint(code: string, model: HintModel): void {
19 + const dispatcher = ReactDOMCurrentDispatcher.current;
20 + if (dispatcher) {
21 + let href, options;
22 + if (typeof model === 'string') {
23 + href = model;
24 + } else {
25 + href = model[0];
26 + options = model[1];
27 + }
28 + switch (code) {
29 + case 'D': {
30 + // $FlowFixMe[prop-missing] options are not refined to their types by code
31 + dispatcher.prefetchDNS(href, options);
32 + return;
33 + }
34 + case 'C': {
35 + // $FlowFixMe[prop-missing] options are not refined to their types by code
36 + dispatcher.preconnect(href, options);
37 + return;
38 + }
39 + case 'L': {
40 + // $FlowFixMe[prop-missing] options are not refined to their types by code
41 + // $FlowFixMe[incompatible-call] options are not refined to their types by code
42 + dispatcher.preload(href, options);
43 + return;
44 + }
45 + case 'I': {
46 + // $FlowFixMe[prop-missing] options are not refined to their types by code
47 + // $FlowFixMe[incompatible-call] options are not refined to their types by code
48 + dispatcher.preinit(href, options);
49 + return;
50 + }
51 + }
52 + }
53 +}
packages/react-dom/src/ReactDOMDispatcher.js new
+30
@@ -0,0 +1,30 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export type PrefetchDNSOptions = {};
11 +export type PreconnectOptions = {crossOrigin?: string};
12 +export type PreloadOptions = {
13 + as: string,
14 + crossOrigin?: string,
15 + integrity?: string,
16 + type?: string,
17 +};
18 +export type PreinitOptions = {
19 + as: string,
20 + precedence?: string,
21 + crossOrigin?: string,
22 + integrity?: string,
23 +};
24 +
25 +export type HostDispatcher = {
26 + prefetchDNS: (href: string, options?: ?PrefetchDNSOptions) => void,
27 + preconnect: (href: string, options: ?PreconnectOptions) => void,
28 + preload: (href: string, options: PreloadOptions) => void,
29 + preinit: (href: string, options: PreinitOptions) => void,
30 +};
packages/react-dom/src/ReactDOMFloat.js
+45 -12
@@ -1,39 +1,72 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +import type {
10 + PreconnectOptions,
11 + PreloadOptions,
12 + PreinitOptions,
13 +} from './ReactDOMDispatcher';
14 +
15 import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
16 +const Dispatcher = ReactDOMSharedInternals.Dispatcher;
17
3 -export function prefetchDNS() {
4 - const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
18 +export function prefetchDNS(href: string) {
19 + let passedOptionArg: any;
20 + if (__DEV__) {
21 + if (arguments[1] !== undefined) {
22 + passedOptionArg = arguments[1];
23 + }
24 + }
25 + const dispatcher = Dispatcher.current;
26 if (dispatcher) {
6 - dispatcher.prefetchDNS.apply(this, arguments);
27 + if (__DEV__) {
28 + if (passedOptionArg !== undefined) {
29 + // prefetchDNS will warn if you pass reserved options arg. We pass it along in Dev only to
30 + // elicit the warning. In prod we do not forward since it is not a part of the interface.
31 + // @TODO move all arg validation into this file. It needs to be universal anyway so may as well lock down the interace here and
32 + // let the rest of the codebase trust the types
33 + dispatcher.prefetchDNS(href, passedOptionArg);
34 + } else {
35 + dispatcher.prefetchDNS(href);
36 + }
37 + } else {
38 + dispatcher.prefetchDNS(href);
39 + }
40 }
41 // We don't error because preconnect needs to be resilient to being called in a variety of scopes
42 // and the runtime may not be capable of responding. The function is optimistic and not critical
43 // so we favor silent bailout over warning or erroring.
44 }
45
13 -export function preconnect() {
14 - const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
46 +export function preconnect(href: string, options?: ?PreconnectOptions) {
47 + const dispatcher = Dispatcher.current;
48 if (dispatcher) {
16 - dispatcher.preconnect.apply(this, arguments);
49 + dispatcher.preconnect(href, options);
50 }
51 // We don't error because preconnect needs to be resilient to being called in a variety of scopes
52 // and the runtime may not be capable of responding. The function is optimistic and not critical
53 // so we favor silent bailout over warning or erroring.
54 }
55
23 -export function preload() {
24 - const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
56 +export function preload(href: string, options: PreloadOptions) {
57 + const dispatcher = Dispatcher.current;
58 if (dispatcher) {
26 - dispatcher.preload.apply(this, arguments);
59 + dispatcher.preload(href, options);
60 }
61 // We don't error because preload needs to be resilient to being called in a variety of scopes
62 // and the runtime may not be capable of responding. The function is optimistic and not critical
63 // so we favor silent bailout over warning or erroring.
64 }
65
33 -export function preinit() {
34 - const dispatcher = ReactDOMSharedInternals.Dispatcher.current;
66 +export function preinit(href: string, options: PreinitOptions) {
67 + const dispatcher = Dispatcher.current;
68 if (dispatcher) {
36 - dispatcher.preinit.apply(this, arguments);
69 + dispatcher.preinit(href, options);
70 }
71 // We don't error because preinit needs to be resilient to being called in a variety of scopes
72 // and the runtime may not be capable of responding. The function is optimistic and not critical
packages/react-dom/src/ReactDOMSharedInternals.js
+3 -1
@@ -7,11 +7,13 @@
7 * @flow
8 */
9
10 +import type {HostDispatcher} from './ReactDOMDispatcher';
11 +
12 type InternalsType = {
13 usingClientEntryPoint: boolean,
14 Events: [any, any, any, any, any, any],
15 Dispatcher: {
14 - current: mixed,
16 + current: null | HostDispatcher,
17 },
18 };
19
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+1 -1
@@ -3701,7 +3701,7 @@ describe('ReactDOMFizzServer', () => {
3701 Array.from(document.head.getElementsByTagName('script')).map(
3702 n => n.outerHTML,
3703 ),
3704 - ).toEqual(['<script src="src-of-external-runtime" async=""></script>']);
3704 + ).toEqual(['<script async="" src="src-of-external-runtime"></script>']);
3705
3706 expect(getVisibleChildren(document)).toEqual(
3707 <html>
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+1 -1
@@ -3981,7 +3981,7 @@ body {
3981 });
3982
3983 // @gate enableFloat
3984 - it('creates a preload resource when ReactDOM.preinit(..., {as: "script" }) is called outside of render on the client', async () => {
3984 + it('creates a script resource when ReactDOM.preinit(..., {as: "script" }) is called outside of render on the client', async () => {
3985 function App() {
3986 React.useEffect(() => {
3987 ReactDOM.preinit('foo', {as: 'script'});
packages/react-dom/src/client/ReactDOMRoot.js
+7 -3
@@ -13,8 +13,6 @@ import type {
13 TransitionTracingCallbacks,
14 } from 'react-reconciler/src/ReactInternalTypes';
15
16 -import ReactDOMSharedInternals from '../ReactDOMSharedInternals';
17 -const {Dispatcher} = ReactDOMSharedInternals;
16 import {ReactDOMClientDispatcher} from 'react-dom-bindings/src/client/ReactFiberConfigDOM';
17 import {queueExplicitHydrationTarget} from 'react-dom-bindings/src/events/ReactDOMEventReplaying';
18 import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
@@ -25,13 +23,19 @@ import {
23 disableCommentsAsDOMContainers,
24 } from 'shared/ReactFeatureFlags';
25
26 +import ReactDOMSharedInternals from '../ReactDOMSharedInternals';
27 +const {Dispatcher} = ReactDOMSharedInternals;
28 +if (enableFloat && typeof document !== 'undefined') {
29 + // Set the default dispatcher to the client dispatcher
30 + Dispatcher.current = ReactDOMClientDispatcher;
31 +}
32 +
33 export type RootType = {
34 render(children: ReactNodeList): void,
35 unmount(): void,
36 _internalRoot: FiberRoot | null,
37 ...
38 };
34 -
39 export type CreateRootOptions = {
40 unstable_strictMode?: boolean,
41 unstable_concurrentUpdatesByDefault?: boolean,
packages/react-native-renderer/src/server/ReactFizzConfigNative.js
+1 -2
@@ -339,8 +339,7 @@ export function hoistResources(
339 boundaryResources: BoundaryResources,
340 ) {}
341
342 -export function prepareToRender(resources: Resources) {}
343 -export function cleanupAfterRender(previousDispatcher: mixed) {}
342 +export function prepareHostDispatcher() {}
343 export function createResources() {}
344 export function createBoundaryResources() {}
345 export function setCurrentlyRenderingBoundaryResourcesTarget(
packages/react-native-renderer/src/server/ReactFlightServerConfigNative.js
+9
@@ -8,3 +8,12 @@
8 */
9
10 export const isPrimaryRenderer = true;
11 +
12 +export type Hints = null;
13 +export type HintModel = '';
14 +
15 +export function createHints(): null {
16 + return null;
17 +}
18 +
19 +export function prepareHostDispatcher() {}
packages/react-noop-renderer/src/ReactNoopFlightServer.js
+1
@@ -63,6 +63,7 @@ const ReactNoopFlightServer = ReactFlightServer({
63 ) {
64 return saveModule(reference.value);
65 },
66 + prepareHostDispatcher() {},
67 });
68
69 type Options = {
packages/react-noop-renderer/src/ReactNoopServer.js
+1 -2
@@ -279,8 +279,7 @@ const ReactNoopServer = ReactFizzServer({
279
280 setCurrentlyRenderingBoundaryResourcesTarget(resources: BoundaryResources) {},
281
282 - prepareToRender() {},
283 - cleanupAfterRender() {},
282 + prepareHostDispatcher() {},
283 });
284
285 type Options = {
packages/react-server-dom-relay/src/ReactFlightDOMRelayClient.js
+5
@@ -17,6 +17,7 @@ import {
17 resolveModule,
18 resolveErrorDev,
19 resolveErrorProd,
20 + resolveHint,
21 close,
22 getRoot,
23 } from 'react-client/src/ReactFlightClient';
@@ -30,10 +31,14 @@ export function resolveRow(response: Response, chunk: RowEncoding): void {
31 } else if (chunk[0] === 'I') {
32 // $FlowFixMe[incompatible-call] unable to refine on array indices
33 resolveModule(response, chunk[1], chunk[2]);
34 + } else if (chunk[0] === 'H') {
35 + // $FlowFixMe[incompatible-call] unable to refine on array indices
36 + resolveHint(response, chunk[1], chunk[2]);
37 } else {
38 if (__DEV__) {
39 resolveErrorDev(
40 response,
41 + // $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples.
42 chunk[1],
43 // $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples.
44 // $FlowFixMe[prop-missing]
packages/react-server-dom-relay/src/ReactFlightDOMRelayProtocol.js
+2
@@ -7,6 +7,7 @@
7 * @flow
8 */
9
10 +import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
11 import type {ClientReferenceMetadata} from 'ReactFlightDOMRelayServerIntegration';
12
13 export type JSONValue =
@@ -20,6 +21,7 @@ export type JSONValue =
21 export type RowEncoding =
22 | ['O', number, JSONValue]
23 | ['I', number, ClientReferenceMetadata]
24 + | ['H', string, HintModel]
25 | ['P', number, string]
26 | ['S', number, string]
27 | [
packages/react-server-dom-relay/src/ReactFlightServerConfigDOMRelay.js
+12 -2
@@ -7,6 +7,7 @@
7 * @flow
8 */
9
10 +import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
11 import type {RowEncoding, JSONValue} from './ReactFlightDOMRelayProtocol';
12
13 import type {
@@ -191,6 +192,16 @@ export function processImportChunk(
192 return ['I', id, clientReferenceMetadata];
193 }
194
195 +export function processHintChunk(
196 + request: Request,
197 + id: number,
198 + code: string,
199 + model: HintModel,
200 +): Chunk {
201 + // The hint is already a JSON serializable value.
202 + return ['H', code, model];
203 +}
204 +
205 export function scheduleWork(callback: () => void) {
206 callback();
207 }
@@ -198,8 +209,7 @@ export function scheduleWork(callback: () => void) {
209 export function flushBuffered(destination: Destination) {}
210
211 export const supportsRequestStorage = false;
201 -export const requestStorage: AsyncLocalStorage<Map<Function, mixed>> =
202 - (null: any);
212 +export const requestStorage: AsyncLocalStorage<Request> = (null: any);
213
214 export function beginWriting(destination: Destination) {}
215
packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js
+1 -2
@@ -24,8 +24,7 @@ export function scheduleWork(callback: () => void) {
24 export function flushBuffered(destination: Destination) {}
25
26 export const supportsRequestStorage = false;
27 -export const requestStorage: AsyncLocalStorage<Map<Function, mixed>> =
28 - (null: any);
27 +export const requestStorage: AsyncLocalStorage<Request> = (null: any);
28
29 export function beginWriting(destination: Destination) {}
30
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+292
@@ -25,9 +25,11 @@ let clientModuleError;
25 let webpackMap;
26 let Stream;
27 let React;
28 +let ReactDOM;
29 let ReactDOMClient;
30 let ReactServerDOMServer;
31 let ReactServerDOMClient;
32 +let ReactDOMFizzServer;
33 let Suspense;
34 let ErrorBoundary;
35
@@ -42,6 +44,8 @@ describe('ReactFlightDOM', () => {
44
45 Stream = require('stream');
46 React = require('react');
47 + ReactDOM = require('react-dom');
48 + ReactDOMFizzServer = require('react-dom/server.node');
49 use = React.use;
50 Suspense = React.Suspense;
51 ReactDOMClient = require('react-dom/client');
@@ -1153,4 +1157,292 @@ describe('ReactFlightDOM', () => {
1157 );
1158 expect(reportedErrors).toEqual([theError]);
1159 });
1160 +
1161 + // @gate enableUseHook
1162 + it('should support ReactDOM.preload when rendering in Fiber', async () => {
1163 + function Component() {
1164 + return <p>hello world</p>;
1165 + }
1166 +
1167 + const ClientComponent = clientExports(Component);
1168 +
1169 + async function ServerComponent() {
1170 + ReactDOM.preload('before', {as: 'style'});
1171 + await 1;
1172 + ReactDOM.preload('after', {as: 'style'});
1173 + return <ClientComponent />;
1174 + }
1175 +
1176 + const {writable, readable} = getTestStream();
1177 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1178 + <ServerComponent />,
1179 + webpackMap,
1180 + );
1181 + pipe(writable);
1182 +
1183 + let response = null;
1184 + function getResponse() {
1185 + if (response === null) {
1186 + response = ReactServerDOMClient.createFromReadableStream(readable);
1187 + }
1188 + return response;
1189 + }
1190 +
1191 + function App() {
1192 + return getResponse();
1193 + }
1194 +
1195 + // We pause to allow the float call after the await point to process before the
1196 + // HostDispatcher gets set for Fiber by createRoot. This is only needed in testing
1197 + // because the module graphs are not different and the HostDispatcher is shared.
1198 + // In a real environment the Fiber and Flight code would each have their own independent
1199 + // dispatcher.
1200 + // @TODO consider what happens when Server-Components-On-The-Client exist. we probably
1201 + // want to use the Fiber HostDispatcher there too since it is more about the host than the runtime
1202 + // but we need to make sure that actually makes sense
1203 + await 1;
1204 +
1205 + const container = document.createElement('div');
1206 + const root = ReactDOMClient.createRoot(container);
1207 + await act(() => {
1208 + root.render(<App />);
1209 + });
1210 + expect(document.head.innerHTML).toBe(
1211 + '<link href="before" rel="preload" as="style">' +
1212 + '<link href="after" rel="preload" as="style">',
1213 + );
1214 + expect(container.innerHTML).toBe('<p>hello world</p>');
1215 + });
1216 +
1217 + // @gate enableUseHook
1218 + it('should support ReactDOM.preload when rendering in Fizz', async () => {
1219 + function Component() {
1220 + return <p>hello world</p>;
1221 + }
1222 +
1223 + const ClientComponent = clientExports(Component);
1224 +
1225 + async function ServerComponent() {
1226 + ReactDOM.preload('before', {as: 'style'});
1227 + await 1;
1228 + ReactDOM.preload('after', {as: 'style'});
1229 + return <ClientComponent />;
1230 + }
1231 +
1232 + const {writable: flightWritable, readable: flightReadable} =
1233 + getTestStream();
1234 + const {writable: fizzWritable, readable: fizzReadable} = getTestStream();
1235 +
1236 + // In a real environment you would want to call the render during the Fizz render.
1237 + // The reason we cannot do this in our test is because we don't actually have two separate
1238 + // module graphs and we are contriving the sequencing to work in a way where
1239 + // the right HostDispatcher is in scope during the Flight Server Float calls and the
1240 + // Flight Client hint dispatches
1241 + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
1242 + <ServerComponent />,
1243 + webpackMap,
1244 + );
1245 + pipe(flightWritable);
1246 +
1247 + let response = null;
1248 + function getResponse() {
1249 + if (response === null) {
1250 + response =
1251 + ReactServerDOMClient.createFromReadableStream(flightReadable);
1252 + }
1253 + return response;
1254 + }
1255 +
1256 + function App() {
1257 + return (
1258 + <html>
1259 + <body>{getResponse()}</body>
1260 + </html>
1261 + );
1262 + }
1263 +
1264 + await act(async () => {
1265 + ReactDOMFizzServer.renderToPipeableStream(<App />).pipe(fizzWritable);
1266 + });
1267 +
1268 + const decoder = new TextDecoder();
1269 + const reader = fizzReadable.getReader();
1270 + let content = '';
1271 + while (true) {
1272 + const {done, value} = await reader.read();
1273 + if (done) {
1274 + content += decoder.decode();
1275 + break;
1276 + }
1277 + content += decoder.decode(value, {stream: true});
1278 + }
1279 +
1280 + expect(content).toEqual(
1281 + '<!DOCTYPE html><html><head><link rel="preload" as="style" href="before"/>' +
1282 + '<link rel="preload" as="style" href="after"/></head><body><p>hello world</p></body></html>',
1283 + );
1284 + });
1285 +
1286 + it('supports Float hints from concurrent Flight -> Fizz renders', async () => {
1287 + function Component() {
1288 + return <p>hello world</p>;
1289 + }
1290 +
1291 + const ClientComponent = clientExports(Component);
1292 +
1293 + async function ServerComponent1() {
1294 + ReactDOM.preload('before1', {as: 'style'});
1295 + await 1;
1296 + ReactDOM.preload('after1', {as: 'style'});
1297 + return <ClientComponent />;
1298 + }
1299 +
1300 + async function ServerComponent2() {
1301 + ReactDOM.preload('before2', {as: 'style'});
1302 + await 1;
1303 + ReactDOM.preload('after2', {as: 'style'});
1304 + return <ClientComponent />;
1305 + }
1306 +
1307 + const {writable: flightWritable1, readable: flightReadable1} =
1308 + getTestStream();
1309 + const {writable: flightWritable2, readable: flightReadable2} =
1310 + getTestStream();
1311 +
1312 + ReactServerDOMServer.renderToPipeableStream(
1313 + <ServerComponent1 />,
1314 + webpackMap,
1315 + ).pipe(flightWritable1);
1316 +
1317 + ReactServerDOMServer.renderToPipeableStream(
1318 + <ServerComponent2 />,
1319 + webpackMap,
1320 + ).pipe(flightWritable2);
1321 +
1322 + const responses = new Map();
1323 + function getResponse(stream) {
1324 + let response = responses.get(stream);
1325 + if (!response) {
1326 + response = ReactServerDOMClient.createFromReadableStream(stream);
1327 + responses.set(stream, response);
1328 + }
1329 + return response;
1330 + }
1331 +
1332 + function App({stream}) {
1333 + return (
1334 + <html>
1335 + <body>{getResponse(stream)}</body>
1336 + </html>
1337 + );
1338 + }
1339 +
1340 + // pausing to let Flight runtime tick. This is a test only artifact of the fact that
1341 + // we aren't operating separate module graphs for flight and fiber. In a real app
1342 + // each would have their own dispatcher and there would be no cross dispatching.
1343 + await 1;
1344 +
1345 + const {writable: fizzWritable1, readable: fizzReadable1} = getTestStream();
1346 + const {writable: fizzWritable2, readable: fizzReadable2} = getTestStream();
1347 + await act(async () => {
1348 + ReactDOMFizzServer.renderToPipeableStream(
1349 + <App stream={flightReadable1} />,
1350 + ).pipe(fizzWritable1);
1351 + ReactDOMFizzServer.renderToPipeableStream(
1352 + <App stream={flightReadable2} />,
1353 + ).pipe(fizzWritable2);
1354 + });
1355 +
1356 + async function read(stream) {
1357 + const decoder = new TextDecoder();
1358 + const reader = stream.getReader();
1359 + let buffer = '';
1360 + while (true) {
1361 + const {done, value} = await reader.read();
1362 + if (done) {
1363 + buffer += decoder.decode();
1364 + break;
1365 + }
1366 + buffer += decoder.decode(value, {stream: true});
1367 + }
1368 + return buffer;
1369 + }
1370 +
1371 + const [content1, content2] = await Promise.all([
1372 + read(fizzReadable1),
1373 + read(fizzReadable2),
1374 + ]);
1375 +
1376 + expect(content1).toEqual(
1377 + '<!DOCTYPE html><html><head><link rel="preload" as="style" href="before1"/>' +
1378 + '<link rel="preload" as="style" href="after1"/></head><body><p>hello world</p></body></html>',
1379 + );
1380 + expect(content2).toEqual(
1381 + '<!DOCTYPE html><html><head><link rel="preload" as="style" href="before2"/>' +
1382 + '<link rel="preload" as="style" href="after2"/></head><body><p>hello world</p></body></html>',
1383 + );
1384 + });
1385 +
1386 + it('supports deduping hints by Float key', async () => {
1387 + function Component() {
1388 + return <p>hello world</p>;
1389 + }
1390 +
1391 + const ClientComponent = clientExports(Component);
1392 +
1393 + async function ServerComponent() {
1394 + ReactDOM.prefetchDNS('dns');
1395 + ReactDOM.preconnect('preconnect');
1396 + ReactDOM.preload('load', {as: 'style'});
1397 + ReactDOM.preinit('init', {as: 'script'});
1398 + // again but vary preconnect to demonstrate crossOrigin participates in the key
1399 + ReactDOM.prefetchDNS('dns');
1400 + ReactDOM.preconnect('preconnect', {crossOrigin: 'anonymous'});
1401 + ReactDOM.preload('load', {as: 'style'});
1402 + ReactDOM.preinit('init', {as: 'script'});
1403 + await 1;
1404 + // after an async point
1405 + ReactDOM.prefetchDNS('dns');
1406 + ReactDOM.preconnect('preconnect', {crossOrigin: 'use-credentials'});
1407 + ReactDOM.preload('load', {as: 'style'});
1408 + ReactDOM.preinit('init', {as: 'script'});
1409 + return <ClientComponent />;
1410 + }
1411 +
1412 + const {writable, readable} = getTestStream();
1413 +
1414 + ReactServerDOMServer.renderToPipeableStream(
1415 + <ServerComponent />,
1416 + webpackMap,
1417 + ).pipe(writable);
1418 +
1419 + const hintRows = [];
1420 + async function collectHints(stream) {
1421 + const decoder = new TextDecoder();
1422 + const reader = stream.getReader();
1423 + let buffer = '';
1424 + while (true) {
1425 + const {done, value} = await reader.read();
1426 + if (done) {
1427 + buffer += decoder.decode();
1428 + if (buffer.includes(':H')) {
1429 + hintRows.push(buffer);
1430 + }
1431 + break;
1432 + }
1433 + buffer += decoder.decode(value, {stream: true});
1434 + let line;
1435 + while ((line = buffer.indexOf('\n')) > -1) {
1436 + const row = buffer.slice(0, line);
1437 + buffer = buffer.slice(line + 1);
1438 + if (row.includes(':H')) {
1439 + hintRows.push(row);
1440 + }
1441 + }
1442 + }
1443 + }
1444 +
1445 + await collectHints(readable);
1446 + expect(hintRows.length).toEqual(6);
1447 + });
1448 });
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+118
@@ -21,7 +21,9 @@ let webpackMap;
21 let webpackServerMap;
22 let act;
23 let React;
24 +let ReactDOM;
25 let ReactDOMClient;
26 +let ReactDOMFizzServer;
27 let ReactServerDOMServer;
28 let ReactServerDOMClient;
29 let Suspense;
@@ -37,7 +39,9 @@ describe('ReactFlightDOMBrowser', () => {
39 webpackMap = WebpackMock.webpackMap;
40 webpackServerMap = WebpackMock.webpackServerMap;
41 React = require('react');
42 + ReactDOM = require('react-dom');
43 ReactDOMClient = require('react-dom/client');
44 + ReactDOMFizzServer = require('react-dom/server.browser');
45 ReactServerDOMServer = require('react-server-dom-webpack/server.browser');
46 ReactServerDOMClient = require('react-server-dom-webpack/client');
47 Suspense = React.Suspense;
@@ -1062,4 +1066,118 @@ describe('ReactFlightDOMBrowser', () => {
1066 expect(thrownError.digest).toBe('test-error-digest');
1067 }
1068 });
1069 +
1070 + it('supports Float hints before the first await in server components in Fiber', async () => {
1071 + function Component() {
1072 + return <p>hello world</p>;
1073 + }
1074 +
1075 + const ClientComponent = clientExports(Component);
1076 +
1077 + async function ServerComponent() {
1078 + ReactDOM.preload('before', {as: 'style'});
1079 + await 1;
1080 + ReactDOM.preload('after', {as: 'style'});
1081 + return <ClientComponent />;
1082 + }
1083 +
1084 + const stream = ReactServerDOMServer.renderToReadableStream(
1085 + <ServerComponent />,
1086 + webpackMap,
1087 + );
1088 +
1089 + let response = null;
1090 + function getResponse() {
1091 + if (response === null) {
1092 + response = ReactServerDOMClient.createFromReadableStream(stream);
1093 + }
1094 + return response;
1095 + }
1096 +
1097 + function App() {
1098 + return getResponse();
1099 + }
1100 +
1101 + // pausing to let Flight runtime tick. This is a test only artifact of the fact that
1102 + // we aren't operating separate module graphs for flight and fiber. In a real app
1103 + // each would have their own dispatcher and there would be no cross dispatching.
1104 + await 1;
1105 +
1106 + const container = document.createElement('div');
1107 + const root = ReactDOMClient.createRoot(container);
1108 + await act(() => {
1109 + root.render(<App />);
1110 + });
1111 + expect(document.head.innerHTML).toBe(
1112 + '<link href="before" rel="preload" as="style">',
1113 + );
1114 + expect(container.innerHTML).toBe('<p>hello world</p>');
1115 + });
1116 +
1117 + it('Does not support Float hints in server components anywhere in Fizz', async () => {
1118 + // In environments that do not support AsyncLocalStorage the Flight client has no ability
1119 + // to scope hint dispatching to a specific Request. In Fiber this isn't a problem because
1120 + // the Browser scope acts like a singleton and we can dispatch away. But in Fizz we need to have
1121 + // a reference to Resources and this is only possible during render unless you support AsyncLocalStorage.
1122 + function Component() {
1123 + return <p>hello world</p>;
1124 + }
1125 +
1126 + const ClientComponent = clientExports(Component);
1127 +
1128 + async function ServerComponent() {
1129 + ReactDOM.preload('before', {as: 'style'});
1130 + await 1;
1131 + ReactDOM.preload('after', {as: 'style'});
1132 + return <ClientComponent />;
1133 + }
1134 +
1135 + const stream = ReactServerDOMServer.renderToReadableStream(
1136 + <ServerComponent />,
1137 + webpackMap,
1138 + );
1139 +
1140 + let response = null;
1141 + function getResponse() {
1142 + if (response === null) {
1143 + response = ReactServerDOMClient.createFromReadableStream(stream);
1144 + }
1145 + return response;
1146 + }
1147 +
1148 + function App() {
1149 + return (
1150 + <html>
1151 + <body>{getResponse()}</body>
1152 + </html>
1153 + );
1154 + }
1155 +
1156 + // pausing to let Flight runtime tick. This is a test only artifact of the fact that
1157 + // we aren't operating separate module graphs for flight and fiber. In a real app
1158 + // each would have their own dispatcher and there would be no cross dispatching.
1159 + await 1;
1160 +
1161 + let fizzStream;
1162 + await act(async () => {
1163 + fizzStream = await ReactDOMFizzServer.renderToReadableStream(<App />);
1164 + });
1165 +
1166 + const decoder = new TextDecoder();
1167 + const reader = fizzStream.getReader();
1168 + let content = '';
1169 + while (true) {
1170 + const {done, value} = await reader.read();
1171 + if (done) {
1172 + content += decoder.decode();
1173 + break;
1174 + }
1175 + content += decoder.decode(value, {stream: true});
1176 + }
1177 +
1178 + expect(content).toEqual(
1179 + '<!DOCTYPE html><html><head>' +
1180 + '</head><body><p>hello world</p></body></html>',
1181 + );
1182 + });
1183 });
packages/react-server-native-relay/src/ReactFlightClientConfigNativeRelay.js
+2
@@ -95,3 +95,5 @@ const dummy = {};
95 export function parseModel<T>(response: Response, json: UninitializedModel): T {
96 return (parseModelRecursively(response, dummy, '', json): any);
97 }
98 +
99 +export function dispatchHint(code: string, model: mixed) {}
packages/react-server-native-relay/src/ReactFlightServerConfigNativeRelay.js
+12 -2
@@ -187,6 +187,17 @@ export function processImportChunk(
187 return ['I', id, clientReferenceMetadata];
188 }
189
190 +export function processHintChunk(
191 + request: Request,
192 + id: number,
193 + code: string,
194 + model: JSONValue,
195 +): Chunk {
196 + throw new Error(
197 + 'React Internal Error: processHintChunk is not implemented for Native-Relay. The fact that this method was called means there is a bug in React.',
198 + );
199 +}
200 +
201 export function scheduleWork(callback: () => void) {
202 callback();
203 }
@@ -194,8 +205,7 @@ export function scheduleWork(callback: () => void) {
205 export function flushBuffered(destination: Destination) {}
206
207 export const supportsRequestStorage = false;
197 -export const requestStorage: AsyncLocalStorage<Map<Function, mixed>> =
198 - (null: any);
208 +export const requestStorage: AsyncLocalStorage<Request> = (null: any);
209
210 export function beginWriting(destination: Destination) {}
211
packages/react-server/src/ReactFizzServer.js
+53 -6
@@ -71,11 +71,12 @@ import {
71 writeHoistables,
72 writePostamble,
73 hoistResources,
74 - prepareToRender,
75 - cleanupAfterRender,
74 setCurrentlyRenderingBoundaryResourcesTarget,
75 createResources,
76 createBoundaryResources,
77 + prepareHostDispatcher,
78 + supportsRequestStorage,
79 + requestStorage,
80 } from './ReactFizzConfig';
81 import {
82 constructClassInstance,
@@ -210,6 +211,7 @@ const CLOSED = 2;
211
212 export opaque type Request = {
213 destination: null | Destination,
214 + flushScheduled: boolean,
215 +responseState: ResponseState,
216 +progressiveChunkSize: number,
217 status: 0 | 1 | 2,
@@ -277,11 +279,13 @@ export function createRequest(
279 onShellError: void | ((error: mixed) => void),
280 onFatalError: void | ((error: mixed) => void),
281 ): Request {
282 + prepareHostDispatcher();
283 const pingedTasks: Array<Task> = [];
284 const abortSet: Set<Task> = new Set();
285 const resources: Resources = createResources();
286 const request: Request = {
287 destination: null,
288 + flushScheduled: false,
289 responseState,
290 progressiveChunkSize:
291 progressiveChunkSize === undefined
@@ -332,10 +336,22 @@ export function createRequest(
336 return request;
337 }
338
339 +let currentRequest: null | Request = null;
340 +
341 +export function resolveRequest(): null | Request {
342 + if (currentRequest) return currentRequest;
343 + if (supportsRequestStorage) {
344 + const store = requestStorage.getStore();
345 + if (store) return store;
346 + }
347 + return null;
348 +}
349 +
350 function pingTask(request: Request, task: Task): void {
351 const pingedTasks = request.pingedTasks;
352 pingedTasks.push(task);
338 - if (pingedTasks.length === 1) {
353 + if (request.pingedTasks.length === 1) {
354 + request.flushScheduled = request.destination !== null;
355 scheduleWork(() => performWork(request));
356 }
357 }
@@ -1947,7 +1963,9 @@ export function performWork(request: Request): void {
1963 ReactCurrentCache.current = DefaultCacheDispatcher;
1964 }
1965
1950 - const previousHostDispatcher = prepareToRender(request.resources);
1966 + const prevRequest = currentRequest;
1967 + currentRequest = request;
1968 +
1969 let prevGetCurrentStackImpl;
1970 if (__DEV__) {
1971 prevGetCurrentStackImpl = ReactDebugCurrentFrame.getCurrentStack;
@@ -1975,7 +1993,6 @@ export function performWork(request: Request): void {
1993 if (enableCache) {
1994 ReactCurrentCache.current = prevCacheDispatcher;
1995 }
1978 - cleanupAfterRender(previousHostDispatcher);
1996
1997 if (__DEV__) {
1998 ReactDebugCurrentFrame.getCurrentStack = prevGetCurrentStackImpl;
@@ -1990,6 +2007,7 @@ export function performWork(request: Request): void {
2007 // we'll to restore the context to what it was before returning.
2008 switchContext(prevContext);
2009 }
2010 + currentRequest = prevRequest;
2011 }
2012 }
2013
@@ -2389,6 +2407,7 @@ function flushCompletedQueues(
2407 // We don't need to check any partially completed segments because
2408 // either they have pending task or they're complete.
2409 ) {
2410 + request.flushScheduled = false;
2411 if (enableFloat) {
2412 writePostamble(destination, request.responseState);
2413 }
@@ -2411,7 +2430,27 @@ function flushCompletedQueues(
2430 }
2431
2432 export function startWork(request: Request): void {
2414 - scheduleWork(() => performWork(request));
2433 + request.flushScheduled = request.destination !== null;
2434 + if (supportsRequestStorage) {
2435 + scheduleWork(() => requestStorage.run(request, performWork, request));
2436 + } else {
2437 + scheduleWork(() => performWork(request));
2438 + }
2439 +}
2440 +
2441 +function enqueueFlush(request: Request): void {
2442 + if (
2443 + request.flushScheduled === false &&
2444 + // If there are pinged tasks we are going to flush anyway after work completes
2445 + request.pingedTasks.length === 0 &&
2446 + // If there is no destination there is nothing we can flush to. A flush will
2447 + // happen when we start flowing again
2448 + request.destination !== null
2449 + ) {
2450 + const destination = request.destination;
2451 + request.flushScheduled = true;
2452 + scheduleWork(() => flushCompletedQueues(request, destination));
2453 + }
2454 }
2455
2456 export function startFlowing(request: Request, destination: Destination): void {
@@ -2456,3 +2495,11 @@ export function abort(request: Request, reason: mixed): void {
2495 fatalError(request, error);
2496 }
2497 }
2498 +
2499 +export function flushResources(request: Request): void {
2500 + enqueueFlush(request);
2501 +}
2502 +
2503 +export function getResources(request: Request): Resources {
2504 + return request.resources;
2505 +}
packages/react-server/src/ReactFlightServer.js
+90 -9
@@ -16,6 +16,8 @@ import type {
16 ClientReferenceKey,
17 ServerReference,
18 ServerReferenceId,
19 + Hints,
20 + HintModel,
21 } from './ReactFlightServerConfig';
22 import type {ContextSnapshot} from './ReactFlightNewContext';
23 import type {ThenableState} from './ReactFlightThenable';
@@ -44,6 +46,7 @@ import {
46 processErrorChunkProd,
47 processErrorChunkDev,
48 processReferenceChunk,
49 + processHintChunk,
50 resolveClientReferenceMetadata,
51 getServerReferenceId,
52 getServerReferenceBoundArguments,
@@ -52,6 +55,8 @@ import {
55 isServerReference,
56 supportsRequestStorage,
57 requestStorage,
58 + prepareHostDispatcher,
59 + createHints,
60 } from './ReactFlightServerConfig';
61
62 import {
@@ -61,11 +66,7 @@ import {
66 getThenableStateAfterSuspending,
67 resetHooksForRequest,
68 } from './ReactFlightHooks';
64 -import {
65 - DefaultCacheDispatcher,
66 - getCurrentCache,
67 - setCurrentCache,
68 -} from './ReactFlightCache';
69 +import {DefaultCacheDispatcher} from './flight/ReactFlightServerCache';
70 import {
71 pushProvider,
72 popProvider,
@@ -148,15 +149,18 @@ type Task = {
149
150 export type Request = {
151 status: 0 | 1 | 2,
152 + flushScheduled: boolean,
153 fatalError: mixed,
154 destination: null | Destination,
155 bundlerConfig: ClientManifest,
156 cache: Map<Function, mixed>,
157 nextChunkId: number,
158 pendingChunks: number,
159 + hints: Hints,
160 abortableTasks: Set<Task>,
161 pingedTasks: Array<Task>,
162 completedImportChunks: Array<Chunk>,
163 + completedHintChunks: Array<Chunk>,
164 completedJSONChunks: Array<Chunk>,
165 completedErrorChunks: Array<Chunk>,
166 writtenSymbols: Map<symbol, number>,
@@ -196,21 +200,26 @@ export function createRequest(
200 'Currently React only supports one RSC renderer at a time.',
201 );
202 }
203 + prepareHostDispatcher();
204 ReactCurrentCache.current = DefaultCacheDispatcher;
205
206 const abortSet: Set<Task> = new Set();
207 const pingedTasks: Array<Task> = [];
208 + const hints = createHints();
209 const request: Request = {
210 status: OPEN,
211 + flushScheduled: false,
212 fatalError: null,
213 destination: null,
214 bundlerConfig,
215 cache: new Map(),
216 nextChunkId: 0,
217 pendingChunks: 0,
218 + hints,
219 abortableTasks: abortSet,
220 pingedTasks: pingedTasks,
221 completedImportChunks: ([]: Array<Chunk>),
222 + completedHintChunks: ([]: Array<Chunk>),
223 completedJSONChunks: ([]: Array<Chunk>),
224 completedErrorChunks: ([]: Array<Chunk>),
225 writtenSymbols: new Map(),
@@ -232,6 +241,17 @@ export function createRequest(
241 return request;
242 }
243
244 +let currentRequest: null | Request = null;
245 +
246 +export function resolveRequest(): null | Request {
247 + if (currentRequest) return currentRequest;
248 + if (supportsRequestStorage) {
249 + const store = requestStorage.getStore();
250 + if (store) return store;
251 + }
252 + return null;
253 +}
254 +
255 function createRootContext(
256 reqContext?: Array<[string, ServerContextJSONValue]>,
257 ) {
@@ -320,6 +340,23 @@ function serializeThenable(request: Request, thenable: Thenable<any>): number {
340 return newTask.id;
341 }
342
343 +export function emitHint(
344 + request: Request,
345 + code: string,
346 + model: HintModel,
347 +): void {
348 + emitHintChunk(request, code, model);
349 + enqueueFlush(request);
350 +}
351 +
352 +export function getHints(request: Request): Hints {
353 + return request.hints;
354 +}
355 +
356 +export function getCache(request: Request): Map<Function, mixed> {
357 + return request.cache;
358 +}
359 +
360 function readThenable<T>(thenable: Thenable<T>): T {
361 if (thenable.status === 'fulfilled') {
362 return thenable.value;
@@ -502,6 +539,7 @@ function pingTask(request: Request, task: Task): void {
539 const pingedTasks = request.pingedTasks;
540 pingedTasks.push(task);
541 if (pingedTasks.length === 1) {
542 + request.flushScheduled = request.destination !== null;
543 scheduleWork(() => performWork(request));
544 }
545 }
@@ -1082,6 +1120,16 @@ function emitImportChunk(
1120 request.completedImportChunks.push(processedChunk);
1121 }
1122
1123 +function emitHintChunk(request: Request, code: string, model: HintModel): void {
1124 + const processedChunk = processHintChunk(
1125 + request,
1126 + request.nextChunkId++,
1127 + code,
1128 + model,
1129 + );
1130 + request.completedHintChunks.push(processedChunk);
1131 +}
1132 +
1133 function emitSymbolChunk(request: Request, id: number, name: string): void {
1134 const symbolReference = serializeSymbolReference(name);
1135 const processedChunk = processReferenceChunk(request, id, symbolReference);
@@ -1195,9 +1243,9 @@ function retryTask(request: Request, task: Task): void {
1243
1244 function performWork(request: Request): void {
1245 const prevDispatcher = ReactCurrentDispatcher.current;
1198 - const prevCache = getCurrentCache();
1246 ReactCurrentDispatcher.current = HooksDispatcher;
1200 - setCurrentCache(request.cache);
1247 + const prevRequest = currentRequest;
1248 + currentRequest = request;
1249 prepareToUseHooksForRequest(request);
1250
1251 try {
@@ -1215,8 +1263,8 @@ function performWork(request: Request): void {
1263 fatalError(request, error);
1264 } finally {
1265 ReactCurrentDispatcher.current = prevDispatcher;
1218 - setCurrentCache(prevCache);
1266 resetHooksForRequest();
1267 + currentRequest = prevRequest;
1268 }
1269 }
1270
@@ -1250,6 +1298,21 @@ function flushCompletedChunks(
1298 }
1299 }
1300 importsChunks.splice(0, i);
1301 +
1302 + // Next comes hints.
1303 + const hintChunks = request.completedHintChunks;
1304 + i = 0;
1305 + for (; i < hintChunks.length; i++) {
1306 + const chunk = hintChunks[i];
1307 + const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
1308 + if (!keepWriting) {
1309 + request.destination = null;
1310 + i++;
1311 + break;
1312 + }
1313 + }
1314 + hintChunks.splice(0, i);
1315 +
1316 // Next comes model data.
1317 const jsonChunks = request.completedJSONChunks;
1318 i = 0;
@@ -1264,6 +1327,7 @@ function flushCompletedChunks(
1327 }
1328 }
1329 jsonChunks.splice(0, i);
1330 +
1331 // Finally, errors are sent. The idea is that it's ok to delay
1332 // any error messages and prioritize display of other parts of
1333 // the page.
@@ -1281,6 +1345,7 @@ function flushCompletedChunks(
1345 }
1346 errorChunks.splice(0, i);
1347 } finally {
1348 + request.flushScheduled = false;
1349 completeWriting(destination);
1350 }
1351 flushBuffered(destination);
@@ -1291,13 +1356,29 @@ function flushCompletedChunks(
1356 }
1357
1358 export function startWork(request: Request): void {
1359 + request.flushScheduled = request.destination !== null;
1360 if (supportsRequestStorage) {
1295 - scheduleWork(() => requestStorage.run(request.cache, performWork, request));
1361 + scheduleWork(() => requestStorage.run(request, performWork, request));
1362 } else {
1363 scheduleWork(() => performWork(request));
1364 }
1365 }
1366
1367 +function enqueueFlush(request: Request): void {
1368 + if (
1369 + request.flushScheduled === false &&
1370 + // If there are pinged tasks we are going to flush anyway after work completes
1371 + request.pingedTasks.length === 0 &&
1372 + // If there is no destination there is nothing we can flush to. A flush will
1373 + // happen when we start flowing again
1374 + request.destination !== null
1375 + ) {
1376 + const destination = request.destination;
1377 + request.flushScheduled = true;
1378 + scheduleWork(() => flushCompletedChunks(request, destination));
1379 + }
1380 +}
1381 +
1382 export function startFlowing(request: Request, destination: Destination): void {
1383 if (request.status === CLOSING) {
1384 request.status = CLOSED;
packages/react-server/src/ReactFlightServerConfigBundlerCustom.js
+1
@@ -23,3 +23,4 @@ export const resolveClientReferenceMetadata =
23 export const getServerReferenceId = $$$config.getServerReferenceId;
24 export const getServerReferenceBoundArguments =
25 $$$config.getServerReferenceBoundArguments;
26 +export const prepareHostDispatcher = $$$config.prepareHostDispatcher;
packages/react-server/src/ReactFlightServerConfigStream.js
+11 -5
@@ -75,11 +75,6 @@ import type {Chunk} from './ReactServerStreamConfig';
75
76 export type {Destination, Chunk} from './ReactServerStreamConfig';
77
78 -export {
79 - supportsRequestStorage,
80 - requestStorage,
81 -} from './ReactServerStreamConfig';
82 -
78 const stringify = JSON.stringify;
79
80 function serializeRowHeader(tag: string, id: number) {
@@ -156,6 +151,17 @@ export function processImportChunk(
151 return stringToChunk(row);
152 }
153
154 +export function processHintChunk(
155 + request: Request,
156 + id: number,
157 + code: string,
158 + model: JSONValue,
159 +): Chunk {
160 + const json: string = stringify(model);
161 + const row = serializeRowHeader('H' + code, id) + json + '\n';
162 + return stringToChunk(row);
163 +}
164 +
165 export {
166 scheduleWork,
167 flushBuffered,
packages/react-server/src/ReactServerStreamConfigBrowser.js
-4
@@ -21,10 +21,6 @@ export function flushBuffered(destination: Destination) {
21 // transform streams. https://github.com/whatwg/streams/issues/960
22 }
23
24 -export const supportsRequestStorage = false;
25 -export const requestStorage: AsyncLocalStorage<Map<Function, mixed>> =
26 - (null: any);
27 -
24 const VIEW_SIZE = 512;
25 let currentView = null;
26 let writtenBytes = 0;
packages/react-server/src/ReactServerStreamConfigBun.js
-4
@@ -26,10 +26,6 @@ export function flushBuffered(destination: Destination) {
26 // transform streams. https://github.com/whatwg/streams/issues/960
27 }
28
29 -// AsyncLocalStorage is not available in bun
30 -export const supportsRequestStorage = false;
31 -export const requestStorage = (null: any);
32 -
29 export function beginWriting(destination: Destination) {}
30
31 export function writeChunk(
packages/react-server/src/ReactServerStreamConfigEdge.js
-5
@@ -21,11 +21,6 @@ export function flushBuffered(destination: Destination) {
21 // transform streams. https://github.com/whatwg/streams/issues/960
22 }
23
24 -// For now, we get this from the global scope, but this will likely move to a module.
25 -export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
26 -export const requestStorage: AsyncLocalStorage<Map<Function, mixed>> =
27 - supportsRequestStorage ? new AsyncLocalStorage() : (null: any);
28 -
24 const VIEW_SIZE = 512;
25 let currentView = null;
26 let writtenBytes = 0;
packages/react-server/src/ReactServerStreamConfigNode.js
+1 -5
@@ -8,8 +8,8 @@
8 */
9
10 import type {Writable} from 'stream';
11 +
12 import {TextEncoder} from 'util';
12 -import {AsyncLocalStorage} from 'async_hooks';
13
14 interface MightBeFlushable {
15 flush?: () => void;
@@ -34,10 +34,6 @@ export function flushBuffered(destination: Destination) {
34 }
35 }
36
37 -export const supportsRequestStorage = true;
38 -export const requestStorage: AsyncLocalStorage<Map<Function, mixed>> =
39 - new AsyncLocalStorage();
40 -
37 const VIEW_SIZE = 2048;
38 let currentView = null;
39 let writtenBytes = 0;
packages/react-server/src/flight/ReactFlightServerCache.js renamed
+4 -24
@@ -9,24 +9,17 @@
9
10 import type {CacheDispatcher} from 'react-reconciler/src/ReactInternalTypes';
11
12 -import {
13 - supportsRequestStorage,
14 - requestStorage,
15 -} from './ReactFlightServerConfig';
12 +import {resolveRequest, getCache} from '../ReactFlightServer';
13
14 function createSignal(): AbortSignal {
15 return new AbortController().signal;
16 }
17
18 function resolveCache(): Map<Function, mixed> {
22 - if (currentCache) return currentCache;
23 - if (supportsRequestStorage) {
24 - const cache = requestStorage.getStore();
25 - if (cache) return cache;
19 + const request = resolveRequest();
20 + if (request) {
21 + return getCache(request);
22 }
27 - // Since we override the dispatcher all the time, we're effectively always
28 - // active and so to support cache() and fetch() outside of render, we yield
29 - // an empty Map.
23 return new Map();
24 }
25
@@ -51,16 +44,3 @@ export const DefaultCacheDispatcher: CacheDispatcher = {
44 return entry;
45 },
46 };
54 -
55 -let currentCache: Map<Function, mixed> | null = null;
56 -
57 -export function setCurrentCache(
58 - cache: Map<Function, mixed> | null,
59 -): Map<Function, mixed> | null {
60 - currentCache = cache;
61 - return currentCache;
62 -}
63 -
64 -export function getCurrentCache(): Map<Function, mixed> | null {
65 - return currentCache;
66 -}
packages/react-server/src/forks/ReactFizzConfig.custom.js
+6 -2
@@ -23,6 +23,8 @@
23 // So `$$$config` looks like a global variable, but it's
24 // really an argument to a top-level wrapping function.
25
26 +import type {Request} from 'react-server/src/ReactFizzServer';
27 +
28 declare var $$$config: any;
29 export opaque type Destination = mixed; // eslint-disable-line no-undef
30 export opaque type ResponseState = mixed;
@@ -33,6 +35,9 @@ export opaque type SuspenseBoundaryID = mixed;
35
36 export const isPrimaryRenderer = false;
37
38 +export const supportsRequestStorage = false;
39 +export const requestStorage: AsyncLocalStorage<Request> = (null: any);
40 +
41 export const getChildFormatContext = $$$config.getChildFormatContext;
42 export const UNINITIALIZED_SUSPENSE_BOUNDARY_ID =
43 $$$config.UNINITIALIZED_SUSPENSE_BOUNDARY_ID;
@@ -68,8 +73,7 @@ export const writeCompletedBoundaryInstruction =
73 $$$config.writeCompletedBoundaryInstruction;
74 export const writeClientRenderBoundaryInstruction =
75 $$$config.writeClientRenderBoundaryInstruction;
71 -export const prepareToRender = $$$config.prepareToRender;
72 -export const cleanupAfterRender = $$$config.cleanupAfterRender;
76 +export const prepareHostDispatcher = $$$config.prepareHostDispatcher;
77
78 // -------------------------
79 // Resources
packages/react-server/src/forks/ReactFizzConfig.dom-browser.js
+4
@@ -6,5 +6,9 @@
6 *
7 * @flow
8 */
9 +import type {Request} from 'react-server/src/ReactFizzServer';
10
11 export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
12 +
13 +export const supportsRequestStorage = false;
14 +export const requestStorage: AsyncLocalStorage<Request> = (null: any);
packages/react-server/src/forks/ReactFizzConfig.dom-bun.js
+4
@@ -6,5 +6,9 @@
6 *
7 * @flow
8 */
9 +import type {Request} from 'react-server/src/ReactFizzServer';
10
11 export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
12 +
13 +export const supportsRequestStorage = false;
14 +export const requestStorage: AsyncLocalStorage<Request> = (null: any);
packages/react-server/src/forks/ReactFizzConfig.dom-edge-webpack.js
+7
@@ -6,5 +6,12 @@
6 *
7 * @flow
8 */
9 +import type {Request} from 'react-server/src/ReactFizzServer';
10
11 export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
12 +
13 +// For now, we get this from the global scope, but this will likely move to a module.
14 +export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
15 +export const requestStorage: AsyncLocalStorage<Request> = supportsRequestStorage
16 + ? new AsyncLocalStorage()
17 + : (null: any);
packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js
+4
@@ -6,5 +6,9 @@
6 *
7 * @flow
8 */
9 +import type {Request} from 'react-server/src/ReactFizzServer';
10
11 export * from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy';
12 +
13 +export const supportsRequestStorage = false;
14 +export const requestStorage: AsyncLocalStorage<Request> = (null: any);
packages/react-server/src/forks/ReactFizzConfig.dom-node-webpack.js
+7
@@ -6,5 +6,12 @@
6 *
7 * @flow
8 */
9 +import {AsyncLocalStorage} from 'async_hooks';
10 +
11 +import type {Request} from 'react-server/src/ReactFizzServer';
12
13 export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
14 +
15 +export const supportsRequestStorage = true;
16 +export const requestStorage: AsyncLocalStorage<Request> =
17 + new AsyncLocalStorage();
packages/react-server/src/forks/ReactFizzConfig.dom-node.js
+8
@@ -7,4 +7,12 @@
7 * @flow
8 */
9
10 +import {AsyncLocalStorage} from 'async_hooks';
11 +
12 +import type {Request} from 'react-server/src/ReactFizzServer';
13 +
14 export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
15 +
16 +export const supportsRequestStorage = true;
17 +export const requestStorage: AsyncLocalStorage<Request> =
18 + new AsyncLocalStorage();
packages/react-server/src/forks/ReactFizzConfig.dom-relay.js
+4
@@ -6,5 +6,9 @@
6 *
7 * @flow
8 */
9 +import type {Request} from 'react-server/src/ReactFizzServer';
10
11 export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
12 +
13 +export const supportsRequestStorage = false;
14 +export const requestStorage: AsyncLocalStorage<Request> = (null: any);
packages/react-server/src/forks/ReactFizzConfig.native-relay.js
+4
@@ -6,5 +6,9 @@
6 *
7 * @flow
8 */
9 +import type {Request} from 'react-server/src/ReactFizzServer';
10
11 export * from 'react-native-renderer/src/server/ReactFizzConfigNative';
12 +
13 +export const supportsRequestStorage = false;
14 +export const requestStorage: AsyncLocalStorage<Request> = (null: any);
packages/react-server/src/forks/ReactFlightServerConfig.custom.js
+13
@@ -6,8 +6,21 @@
6 *
7 * @flow
8 */
9 +import type {Request} from 'react-server/src/ReactFlightServer';
10
11 export * from '../ReactFlightServerConfigStream';
12 export * from '../ReactFlightServerConfigBundlerCustom';
13
14 +export type Hints = null;
15 +export type HintModel = '';
16 +
17 export const isPrimaryRenderer = false;
18 +
19 +export const prepareHostDispatcher = () => {};
20 +
21 +export const supportsRequestStorage = false;
22 +export const requestStorage: AsyncLocalStorage<Request> = (null: any);
23 +
24 +export function createHints(): null {
25 + return null;
26 +}
packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js
+5
@@ -7,6 +7,11 @@
7 * @flow
8 */
9
10 +import type {Request} from 'react-server/src/ReactFlightServer';
11 +
12 export * from '../ReactFlightServerConfigStream';
13 export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
14 export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
15 +
16 +export const supportsRequestStorage = false;
17 +export const requestStorage: AsyncLocalStorage<Request> = (null: any);
packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js
+5
@@ -7,6 +7,11 @@
7 * @flow
8 */
9
10 +import type {Request} from 'react-server/src/ReactFlightServer';
11 +
12 export * from '../ReactFlightServerConfigStream';
13 export * from '../ReactFlightServerConfigBundlerCustom';
14 export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
15 +
16 +export const supportsRequestStorage = false;
17 +export const requestStorage: AsyncLocalStorage<Request> = (null: any);
packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-webpack.js
+7
@@ -6,7 +6,14 @@
6 *
7 * @flow
8 */
9 +import type {Request} from 'react-server/src/ReactFlightServer';
10
11 export * from '../ReactFlightServerConfigStream';
12 export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
13 export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
14 +
15 +// For now, we get this from the global scope, but this will likely move to a module.
16 +export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
17 +export const requestStorage: AsyncLocalStorage<Request> = supportsRequestStorage
18 + ? new AsyncLocalStorage()
19 + : (null: any);
packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js
+5
@@ -7,6 +7,11 @@
7 * @flow
8 */
9
10 +import type {Request} from 'react-server/src/ReactFlightServer';
11 +
12 export * from '../ReactFlightServerConfigStream';
13 export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
14 export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
15 +
16 +export const supportsRequestStorage = false;
17 +export const requestStorage: AsyncLocalStorage<Request> = (null: any);
packages/react-server/src/forks/ReactFlightServerConfig.dom-node-webpack.js
+7
@@ -6,7 +6,14 @@
6 *
7 * @flow
8 */
9 +import {AsyncLocalStorage} from 'async_hooks';
10 +
11 +import type {Request} from 'react-server/src/ReactFlightServer';
12
13 export * from '../ReactFlightServerConfigStream';
14 export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
15 export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
16 +
17 +export const supportsRequestStorage = true;
18 +export const requestStorage: AsyncLocalStorage<Request> =
19 + new AsyncLocalStorage();
packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js
+8
@@ -7,6 +7,14 @@
7 * @flow
8 */
9
10 +import {AsyncLocalStorage} from 'async_hooks';
11 +
12 +import type {Request} from 'react-server/src/ReactFlightServer';
13 +
14 export * from '../ReactFlightServerConfigStream';
15 export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
16 export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
17 +
18 +export const supportsRequestStorage = true;
19 +export const requestStorage: AsyncLocalStorage<Request> =
20 + new AsyncLocalStorage();
packages/react-server/src/forks/ReactServerStreamConfig.custom.js
-2
@@ -35,8 +35,6 @@ export const writeChunk = $$$config.writeChunk;
35 export const writeChunkAndReturn = $$$config.writeChunkAndReturn;
36 export const completeWriting = $$$config.completeWriting;
37 export const flushBuffered = $$$config.flushBuffered;
38 -export const supportsRequestStorage = $$$config.supportsRequestStorage;
39 -export const requestStorage = $$$config.requestStorage;
38 export const close = $$$config.close;
39 export const closeWithError = $$$config.closeWithError;
40 export const stringToChunk = $$$config.stringToChunk;
scripts/error-codes/codes.json
+2 -1
@@ -461,5 +461,6 @@
461 "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.",
462 "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.",
463 "475": "Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.",
464 - "476": "Expected the form instance to be a HostComponent. This is a bug in React."
464 + "476": "Expected the form instance to be a HostComponent. This is a bug in React.",
465 + "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."
466 }
scripts/rollup/bundles.js
+4 -4
@@ -386,7 +386,7 @@ const bundles = [
386 global: 'ReactServerDOMClient',
387 minifyWithProdErrorCodes: false,
388 wrapWithModuleBoundaries: false,
389 - externals: ['react'],
389 + externals: ['react', 'react-dom'],
390 },
391 {
392 bundleTypes: [NODE_DEV, NODE_PROD],
@@ -395,7 +395,7 @@ const bundles = [
395 global: 'ReactServerDOMClient',
396 minifyWithProdErrorCodes: false,
397 wrapWithModuleBoundaries: false,
398 - externals: ['react', 'util'],
398 + externals: ['react', 'react-dom', 'util'],
399 },
400 {
401 bundleTypes: [NODE_DEV, NODE_PROD],
@@ -404,7 +404,7 @@ const bundles = [
404 global: 'ReactServerDOMClient',
405 minifyWithProdErrorCodes: false,
406 wrapWithModuleBoundaries: false,
407 - externals: ['react', 'util'],
407 + externals: ['react', 'react-dom', 'util'],
408 },
409 {
410 bundleTypes: [NODE_DEV, NODE_PROD],
@@ -413,7 +413,7 @@ const bundles = [
413 global: 'ReactServerDOMClient',
414 minifyWithProdErrorCodes: false,
415 wrapWithModuleBoundaries: false,
416 - externals: ['react'],
416 + externals: ['react', 'react-dom'],
417 },
418
419 /******* React Server DOM Webpack Plugin *******/