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. For environments that support AsyncLocalStorage we could use that to provide the right Request context when dispatching the directives as they stream in however it would but ineffective in the environments without this feature. Instead we stash a "Store" on the Flight client request object and then set it just before dispatching and unset it when the dispatch is complete.

This commit also adds support for AsyncLocalStorage in Fizz however this is more about supporting Float functions after an await point given we expect to offer support for async function components in this runtime soon.

For Flight, if AsyncLocalStorage is available Float methods can be called after await points. If AsyncLocalStorage is not available float methods after await points are noops and you get a warning (in Dev).
  • Loading branch information
gnoff committed Apr 4, 2023
1 parent 4a1cc2d commit 8622483
Show file tree
Hide file tree
Showing 56 changed files with 1,129 additions and 153 deletions.
11 changes: 11 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,16 @@ export function resolveErrorDev(
}
}

export function resolveDirective(
response: Response,
id: number,
model: UninitializedModel,
): void {
const store = response._store;
const payload = JSON.parse(model);
dispatchDirective(store, 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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type Response = ResponseBase & {
_partialRow: string,
_fromJSON: (key: string, value: JSONValue) => any,
_stringDecoder: StringDecoder,
_store: mixed,
};

export type UninitializedModel = string;
Expand Down
9 changes: 9 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 All @@ -26,6 +27,7 @@ import {
readFinalStringChunk,
supportsBinaryStreams,
createStringDecoder,
resolveStore,
} from './ReactFlightClientHostConfig';

export type {Response};
Expand All @@ -46,6 +48,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 Expand Up @@ -134,6 +140,9 @@ export function createResponse(
}
// Don't inline this call because it causes closure to outline the call above.
response._fromJSON = createFromJSONCallback(response);
// If the Host provides a store stash it on the response so we can provide it back
// to the dispatcher when resolving directives
response._store = resolveStore();
return response;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const resolveClientReference = $$$hostConfig.resolveClientReference;
export const resolveServerReference = $$$hostConfig.resolveServerReference;
export const preloadModule = $$$hostConfig.preloadModule;
export const requireModule = $$$hostConfig.requireModule;
export const resolveStore = $$$hostConfig.resolveStore;
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';
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@
* @flow
*/

export * from 'react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig';
export * from 'react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig';
export * from '../ReactFlightClientHostConfigNoStream';
export * from 'react-native-renderer/src/shared/ReactNativeFlightClientHostConfig';
88 changes: 26 additions & 62 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-bindings/src/shared/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 @@ -1940,23 +1941,14 @@ export function prepareToCommitHoistables() {
// appropriate for resources that don't really have a strict tie to the document itself for example
// preloads
let lastCurrentDocument: ?Document = null;
let previousDispatcher = null;
export function prepareRendererToRender(rootContainer: Container) {
if (enableFloat) {
const rootNode = getHoistableRoot(rootContainer);
lastCurrentDocument = getDocumentFromRoot(rootNode);

previousDispatcher = Dispatcher.current;
Dispatcher.current = ReactDOMClientDispatcher;
}
}

export function resetRendererAfterRender() {
if (enableFloat) {
Dispatcher.current = previousDispatcher;
previousDispatcher = null;
}
}
export function resetRendererAfterRender() {}

// global collections of Resources
const preloadPropsMap: Map<string, PreloadProps> = new Map();
Expand All @@ -1979,13 +1971,7 @@ function getCurrentResourceRoot(): null | HoistableRoot {
return currentContainer ? getHoistableRoot(currentContainer) : null;
}

// Preloads are somewhat special. Even if we don't have the Document
// used by the root that is rendering a component trying to insert a preload
// we can still seed the file cache by doing the preload on any document we have
// access to. We prefer the currentDocument if it exists, we also prefer the
// lastCurrentDocument if that exists. As a fallback we will use the window.document
// if available.
function getDocumentForPreloads(): ?Document {
function getBestEffortDocument(): ?Document {
const root = getCurrentResourceRoot();
if (root) {
return root.ownerDocument || root;
Expand All @@ -1998,14 +1984,27 @@ function getDocumentForPreloads(): ?Document {
}
}

function getBestEffortRoot(): ?HoistableRoot {
const root = getCurrentResourceRoot();
if (root) {
return root;
} else {
try {
return lastCurrentDocument || window.document;
} catch (error) {
return null;
}
}
}

function getDocumentFromRoot(root: HoistableRoot): Document {
return root.ownerDocument || root;
}

// 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 All @@ -2017,7 +2016,7 @@ function preconnectAs(
crossOrigin: null | '' | 'use-credentials',
href: string,
) {
const ownerDocument = getDocumentForPreloads();
const ownerDocument = getBestEffortDocument();
if (typeof href === 'string' && href && ownerDocument) {
const limitedEscapedHref =
escapeSelectorAttributeValueInsideDoubleQuotes(href);
Expand All @@ -2039,7 +2038,7 @@ function preconnectAs(
}
}
function prefetchDNS(href: string, options?: mixed) {
function prefetchDNS(href: string, options?: ?mixed) {
if (__DEV__) {
if (typeof href !== 'string' || !href) {
console.error(
Expand All @@ -2066,7 +2065,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 (__DEV__) {
if (typeof href !== 'string' || !href) {
console.error(
Expand Down Expand Up @@ -2096,7 +2095,7 @@ function preconnect(href: string, options?: {crossOrigin?: string}) {
type PreloadAs = ResourceType;
type PreloadOptions = {
as: PreloadAs,
as: string,
crossOrigin?: string,
integrity?: string,
type?: string,
Expand All @@ -2105,7 +2104,7 @@ function preload(href: string, options: PreloadOptions) {
if (__DEV__) {
validatePreloadArguments(href, options);
}
const ownerDocument = getDocumentForPreloads();
const ownerDocument = getBestEffortDocument();
if (
typeof href === 'string' &&
href &&
Expand Down Expand Up @@ -2142,7 +2141,7 @@ function preload(href: string, options: PreloadOptions) {
function preloadPropsFromPreloadOptions(
href: string,
as: ResourceType,
as: string,
options: PreloadOptions,
): PreloadProps {
return {
Expand All @@ -2157,7 +2156,7 @@ function preloadPropsFromPreloadOptions(
type PreinitAs = 'style' | 'script';
type PreinitOptions = {
as: PreinitAs,
as: string,
precedence?: string,
crossOrigin?: string,
integrity?: string,
Expand All @@ -2173,45 +2172,10 @@ function preinit(href: string, options: PreinitOptions) {
typeof options === 'object' &&
options !== null
) {
const resourceRoot = getCurrentResourceRoot();
const resourceRoot = getBestEffortRoot();
const as = options.as;
if (!resourceRoot) {
if (as === 'style' || as === 'script') {
// We are going to emit a preload as a best effort fallback since this preinit
// was called outside of a render. Given the passive nature of this fallback
// we do not warn in dev when props disagree if there happens to already be a
// matching preload with this href
const preloadDocument = getDocumentForPreloads();
if (preloadDocument) {
const limitedEscapedHref =
escapeSelectorAttributeValueInsideDoubleQuotes(href);
const preloadKey = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
let key = preloadKey;
switch (as) {
case 'style':
key = getStyleKey(href);
break;
case 'script':
key = getScriptKey(href);
break;
}
if (!preloadPropsMap.has(key)) {
const preloadProps = preloadPropsFromPreinitOptions(
href,
as,
options,
);
preloadPropsMap.set(key, preloadProps);

if (null === preloadDocument.querySelector(preloadKey)) {
const instance = preloadDocument.createElement('link');
setInitialProperties(instance, 'link', preloadProps);
markNodeAsHoistable(instance);
(preloadDocument.head: any).appendChild(instance);
}
}
}
}
// If we don't have a root to preinit into we just do nothing.
return;
}
Expand Down
Loading

0 comments on commit 8622483

Please sign in to comment.