Skip to content

Commit baff619

Browse files
gnoffAndyPengc12
authored andcommitted
[Float][Flight] Flight support for Float (facebook#26502)
Stacked on facebook#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
1 parent f59d5aa commit baff619

File tree

56 files changed

+1119
-188
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1119
-188
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ import type {
1818
SSRManifest,
1919
} from './ReactFlightClientConfig';
2020

21+
import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
22+
2123
import {
2224
resolveClientReference,
2325
preloadModule,
2426
requireModule,
2527
parseModel,
28+
dispatchHint,
2629
} from './ReactFlightClientConfig';
2730

2831
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
@@ -778,6 +781,15 @@ export function resolveErrorDev(
778781
}
779782
}
780783

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+
781793
export function close(response: Response): void {
782794
// In case there are any remaining unresolved chunks, they won't
783795
// be resolved now. So we need to issue an error to those.

packages/react-client/src/ReactFlightClientStream.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
resolveModel,
1717
resolveErrorProd,
1818
resolveErrorDev,
19+
resolveHint,
1920
createResponse as createResponseBase,
2021
parseModelString,
2122
parseModelTuple,
@@ -46,6 +47,11 @@ function processFullRow(response: Response, row: string): void {
4647
resolveModule(response, id, row.slice(colon + 2));
4748
return;
4849
}
50+
case 'H': {
51+
const code = row[colon + 2];
52+
resolveHint(response, code, row.slice(colon + 3));
53+
return;
54+
}
4955
case 'E': {
5056
const errorInfo = JSON.parse(row.slice(colon + 2));
5157
if (__DEV__) {

packages/react-client/src/forks/ReactFlightClientConfig.custom.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const resolveClientReference = $$$config.resolveClientReference;
3535
export const resolveServerReference = $$$config.resolveServerReference;
3636
export const preloadModule = $$$config.preloadModule;
3737
export const requireModule = $$$config.requireModule;
38+
export const dispatchHint = $$$config.dispatchHint;
3839

3940
export opaque type Source = mixed;
4041

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* @flow
88
*/
99

10+
import type {HostDispatcher} from 'react-dom/src/ReactDOMDispatcher';
1011
import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities';
1112
import type {DOMEventName} from '../events/DOMEventNames';
1213
import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
@@ -1917,10 +1918,6 @@ export function clearSingleton(instance: Instance): void {
19171918

19181919
export const supportsResources = true;
19191920

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-
19241921
type HoistableTagType = 'link' | 'meta' | 'title';
19251922
type TResource<
19261923
T: 'stylesheet' | 'style' | 'script' | 'void',
@@ -2011,7 +2008,7 @@ function getDocumentFromRoot(root: HoistableRoot): Document {
20112008
// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
20122009
// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
20132010
// from Internals -> ReactDOM -> HostConfig -> Internals so this doesn't introduce a new one.
2014-
export const ReactDOMClientDispatcher = {
2011+
export const ReactDOMClientDispatcher: HostDispatcher = {
20152012
prefetchDNS,
20162013
preconnect,
20172014
preload,
@@ -2085,7 +2082,10 @@ function prefetchDNS(href: string, options?: mixed) {
20852082
preconnectAs('dns-prefetch', null, href);
20862083
}
20872084
2088-
function preconnect(href: string, options?: {crossOrigin?: string}) {
2085+
function preconnect(href: string, options: ?{crossOrigin?: string}) {
2086+
if (!enableFloat) {
2087+
return;
2088+
}
20892089
if (__DEV__) {
20902090
if (typeof href !== 'string' || !href) {
20912091
console.error(
@@ -2113,9 +2113,8 @@ function preconnect(href: string, options?: {crossOrigin?: string}) {
21132113
preconnectAs('preconnect', crossOrigin, href);
21142114
}
21152115
2116-
type PreloadAs = ResourceType;
21172116
type PreloadOptions = {
2118-
as: PreloadAs,
2117+
as: string,
21192118
crossOrigin?: string,
21202119
integrity?: string,
21212120
type?: string,
@@ -2164,7 +2163,7 @@ function preload(href: string, options: PreloadOptions) {
21642163
21652164
function preloadPropsFromPreloadOptions(
21662165
href: string,
2167-
as: ResourceType,
2166+
as: string,
21682167
options: PreloadOptions,
21692168
): PreloadProps {
21702169
return {
@@ -2177,9 +2176,8 @@ function preloadPropsFromPreloadOptions(
21772176
};
21782177
}
21792178
2180-
type PreinitAs = 'style' | 'script';
21812179
type PreinitOptions = {
2182-
as: PreinitAs,
2180+
as: string,
21832181
precedence?: string,
21842182
crossOrigin?: string,
21852183
integrity?: string,
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ export function scheduleWork(callback: () => void) {
2121

2222
export function flushBuffered(destination: Destination) {}
2323

24-
export const supportsRequestStorage = false;
25-
export const requestStorage: AsyncLocalStorage<any> = (null: any);
26-
2724
export function beginWriting(destination: Destination) {}
2825

2926
export function writeChunk(

0 commit comments

Comments
 (0)