Skip to content

Commit

Permalink
[Float][Flight] Flight support for Float (facebook#26502)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
gnoff authored and AndyPengc12 committed Apr 15, 2024
1 parent f59d5aa commit baff619
Show file tree
Hide file tree
Showing 56 changed files with 1,119 additions and 188 deletions.
12 changes: 12 additions & 0 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ import type {
SSRManifest,
} from './ReactFlightClientConfig';

import type {HintModel} from 'react-server/src/ReactFlightServerConfig';

import {
resolveClientReference,
preloadModule,
requireModule,
parseModel,
dispatchHint,
} from './ReactFlightClientConfig';

import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
Expand Down Expand Up @@ -778,6 +781,15 @@ export function resolveErrorDev(
}
}

export function resolveHint(
response: Response,
code: string,
model: UninitializedModel,
): void {
const hintModel = parseModel<HintModel>(response, model);
dispatchHint(code, hintModel);
}

export function close(response: Response): void {
// In case there are any remaining unresolved chunks, they won't
// be resolved now. So we need to issue an error to those.
Expand Down
6 changes: 6 additions & 0 deletions packages/react-client/src/ReactFlightClientStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
resolveModel,
resolveErrorProd,
resolveErrorDev,
resolveHint,
createResponse as createResponseBase,
parseModelString,
parseModelTuple,
Expand Down Expand Up @@ -46,6 +47,11 @@ function processFullRow(response: Response, row: string): void {
resolveModule(response, id, row.slice(colon + 2));
return;
}
case 'H': {
const code = row[colon + 2];
resolveHint(response, code, row.slice(colon + 3));
return;
}
case 'E': {
const errorInfo = JSON.parse(row.slice(colon + 2));
if (__DEV__) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const resolveClientReference = $$$config.resolveClientReference;
export const resolveServerReference = $$$config.resolveServerReference;
export const preloadModule = $$$config.preloadModule;
export const requireModule = $$$config.requireModule;
export const dispatchHint = $$$config.dispatchHint;

export opaque type Source = mixed;

Expand Down
20 changes: 9 additions & 11 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* @flow
*/

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

export const supportsResources = true;

// The resource types we support. currently they match the form for the as argument.
// In the future this may need to change, especially when modules / scripts are supported
type ResourceType = 'style' | 'font' | 'script';

type HoistableTagType = 'link' | 'meta' | 'title';
type TResource<
T: 'stylesheet' | 'style' | 'script' | 'void',
Expand Down Expand Up @@ -2011,7 +2008,7 @@ function getDocumentFromRoot(root: HoistableRoot): Document {
// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
// from Internals -> ReactDOM -> HostConfig -> Internals so this doesn't introduce a new one.
export const ReactDOMClientDispatcher = {
export const ReactDOMClientDispatcher: HostDispatcher = {
prefetchDNS,
preconnect,
preload,
Expand Down Expand Up @@ -2085,7 +2082,10 @@ function prefetchDNS(href: string, options?: mixed) {
preconnectAs('dns-prefetch', null, href);
}
function preconnect(href: string, options?: {crossOrigin?: string}) {
function preconnect(href: string, options: ?{crossOrigin?: string}) {
if (!enableFloat) {
return;
}
if (__DEV__) {
if (typeof href !== 'string' || !href) {
console.error(
Expand Down Expand Up @@ -2113,9 +2113,8 @@ function preconnect(href: string, options?: {crossOrigin?: string}) {
preconnectAs('preconnect', crossOrigin, href);
}
type PreloadAs = ResourceType;
type PreloadOptions = {
as: PreloadAs,
as: string,
crossOrigin?: string,
integrity?: string,
type?: string,
Expand Down Expand Up @@ -2164,7 +2163,7 @@ function preload(href: string, options: PreloadOptions) {
function preloadPropsFromPreloadOptions(
href: string,
as: ResourceType,
as: string,
options: PreloadOptions,
): PreloadProps {
return {
Expand All @@ -2177,9 +2176,8 @@ function preloadPropsFromPreloadOptions(
};
}
type PreinitAs = 'style' | 'script';
type PreinitOptions = {
as: PreinitAs,
as: string,
precedence?: string,
crossOrigin?: string,
integrity?: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import type {
HostDispatcher,
PrefetchDNSOptions,
PreconnectOptions,
PreloadOptions,
PreinitOptions,
} from 'react-dom/src/ReactDOMDispatcher';

import {enableFloat} from 'shared/ReactFeatureFlags';

import {
emitHint,
getHints,
resolveRequest,
} from 'react-server/src/ReactFlightServer';

export const ReactDOMFlightServerDispatcher: HostDispatcher = {
prefetchDNS,
preconnect,
preload,
preinit,
};

function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) {
if (enableFloat) {
if (typeof href === 'string') {
const request = resolveRequest();
if (request) {
const hints = getHints(request);
const key = 'D' + href;
if (hints.has(key)) {
// duplicate hint
return;
}
hints.add(key);
if (options) {
emitHint(request, 'D', [href, options]);
} else {
emitHint(request, 'D', href);
}
}
}
}
}

function preconnect(href: string, options: ?PreconnectOptions) {
if (enableFloat) {
if (typeof href === 'string') {
const request = resolveRequest();
if (request) {
const hints = getHints(request);
const crossOrigin =
options == null || typeof options.crossOrigin !== 'string'
? null
: options.crossOrigin === 'use-credentials'
? 'use-credentials'
: '';

const key = `C${crossOrigin === null ? 'null' : crossOrigin}|${href}`;
if (hints.has(key)) {
// duplicate hint
return;
}
hints.add(key);
if (options) {
emitHint(request, 'C', [href, options]);
} else {
emitHint(request, 'C', href);
}
}
}
}
}

function preload(href: string, options: PreloadOptions) {
if (enableFloat) {
if (typeof href === 'string') {
const request = resolveRequest();
if (request) {
const hints = getHints(request);
const key = 'L' + href;
if (hints.has(key)) {
// duplicate hint
return;
}
hints.add(key);
emitHint(request, 'L', [href, options]);
}
}
}
}

function preinit(href: string, options: PreinitOptions) {
if (enableFloat) {
if (typeof href === 'string') {
const request = resolveRequest();
if (request) {
const hints = getHints(request);
const key = 'I' + href;
if (hints.has(key)) {
// duplicate hint
return;
}
hints.add(key);
emitHint(request, 'I', [href, options]);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ export function scheduleWork(callback: () => void) {

export function flushBuffered(destination: Destination) {}

export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage<any> = (null: any);

export function beginWriting(destination: Destination) {}

export function writeChunk(
Expand Down
Loading

0 comments on commit baff619

Please sign in to comment.