Skip to content

Commit

Permalink
Flight support for Float
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gnoff committed Apr 6, 2023
1 parent ed97c69 commit 2487dd5
Show file tree
Hide file tree
Showing 56 changed files with 927 additions and 105 deletions.
10 changes: 10 additions & 0 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
preloadModule,
requireModule,
parseModel,
dispatchDirective,
} from './ReactFlightClientHostConfig';

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

export function resolveDirective(
response: Response,
id: number,
model: string,
): void {
const payload = JSON.parse(model);
dispatchDirective(payload);
}

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
5 changes: 5 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,
resolveDirective,
createResponse as createResponseBase,
parseModelString,
parseModelTuple,
Expand Down Expand Up @@ -46,6 +47,10 @@ function processFullRow(response: Response, row: string): void {
resolveModule(response, id, row.substring(colon + 2));
return;
}
case 'D': {
resolveDirective(response, id, row.substring(colon + 2));
return;
}
case 'E': {
const errorInfo = JSON.parse(row.substring(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 = $$$hostConfig.resolveClientReference;
export const resolveServerReference = $$$hostConfig.resolveServerReference;
export const preloadModule = $$$hostConfig.preloadModule;
export const requireModule = $$$hostConfig.requireModule;
export const dispatchDirective = $$$hostConfig.dispatchDirective;

export opaque type Source = mixed;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig';
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig';

export type Response = any;
export opaque type SSRManifest = mixed;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig';
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig';
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
export * from 'react-client/src/ReactFlightClientHostConfigNode';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig';
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
export * from 'react-client/src/ReactFlightClientHostConfigNode';
export * from 'react-client/src/ReactFlightClientHostConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientNodeBundlerConfig';
export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig';
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@

export * from 'react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig';
export * from '../ReactFlightClientHostConfigNoStream';
export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig';
17 changes: 6 additions & 11 deletions packages/react-dom-bindings/src/client/ReactDOMHostConfig.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 @@ -1865,10 +1866,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 @@ -1959,7 +1956,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 @@ -2033,7 +2030,7 @@ 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;
}
Expand Down Expand Up @@ -2064,9 +2061,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 @@ -2115,7 +2111,7 @@ function preload(href: string, options: PreloadOptions) {
function preloadPropsFromPreloadOptions(
href: string,
as: ResourceType,
as: string,
options: PreloadOptions,
): PreloadProps {
return {
Expand All @@ -2128,9 +2124,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,92 @@
/**
* 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 {enableFloat} from 'shared/ReactFeatureFlags';

import {resolveDirectives} from 'react-server/src/ReactFlightDirectives';

import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;

const ReactDOMFlightServerDispatcher = {
prefetchDNS,
preconnect,
preload,
preinit,
};

export function prepareHostDispatcher(): void {
ReactDOMCurrentDispatcher.current = ReactDOMFlightServerDispatcher;
}

// Used to distinguish these contexts from ones used in other renderers.
// E.g. this can be used to distinguish legacy renderers from this modern one.
export const isPrimaryRenderer = true;

let didWarnAsyncEnvironmentDev = false;

export function prefetchDNS(href: string, options?: mixed) {
if (enableFloat) {
pushDirective('prefetchDNS', href, options);
}
}

export function preconnect(href: string, options?: ?{crossOrigin?: string}) {
if (enableFloat) {
pushDirective('preconnect', href, options);
}
}

type PreloadOptions = {
as: string,
crossOrigin?: string,
integrity?: string,
type?: string,
};

export function preload(href: string, options: PreloadOptions) {
if (enableFloat) {
pushDirective('preload', href, options);
}
}

type PreinitOptions = {
as: string,
precedence?: string,
crossOrigin?: string,
integrity?: string,
};
export function preinit(href: string, options: PreinitOptions): void {
if (enableFloat) {
pushDirective('preinit', href, options);
}
}

function pushDirective(
method: 'prefetchDNS' | 'preconnect' | 'preload' | 'preinit',
href: string,
options: mixed,
): void {
const directives = resolveDirectives();
if (directives === null) {
if (__DEV__) {
if (!didWarnAsyncEnvironmentDev) {
didWarnAsyncEnvironmentDev = true;
console.error(
'ReactDOM.%s(): React expected to be able to associate this call to a specific Request but cannot. It is possible that this call was invoked outside of a React component. If you are calling it from within a React component that is an async function after the first `await` then you are in an environment which does not support AsyncLocalStorage. In this kind of environment ReactDOM.%s() does not do anything when called in an async manner. Try moving this function call above the first `await` within the component or remove this call. In environments that support AsyncLocalStorage such as Node.js you can call this method anywhere in a React component even after `await` operator.',
method,
method,
);
}
}
return;
}
// @TODO need to escape
directives.push(JSON.stringify({method, args: [href, options]}));
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function flushBuffered(destination: Destination) {}

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

export function beginWriting(destination: Destination) {}

Expand Down
Loading

0 comments on commit 2487dd5

Please sign in to comment.