Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Flight] Track Owner on AsyncLocalStorage When Available #28807

Merged
merged 3 commits into from May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -21,6 +21,8 @@ if (typeof Blob === 'undefined') {
if (typeof File === 'undefined') {
global.File = require('buffer').File;
}
// Patch for Edge environments for global scope
global.AsyncLocalStorage = require('async_hooks').AsyncLocalStorage;

// Don't wait before processing work on the server.
// TODO: we can replace this with FlightServer.act().
Expand All @@ -32,6 +34,7 @@ let webpackMap;
let webpackModules;
let webpackModuleLoading;
let React;
let ReactServer;
let ReactDOMServer;
let ReactServerDOMServer;
let ReactServerDOMClient;
Expand All @@ -55,6 +58,7 @@ describe('ReactFlightDOMEdge', () => {
webpackModules = WebpackMock.webpackModules;
webpackModuleLoading = WebpackMock.moduleLoading;

ReactServer = require('react');
ReactServerDOMServer = require('react-server-dom-webpack/server');

jest.resetModules();
Expand Down Expand Up @@ -692,4 +696,71 @@ describe('ReactFlightDOMEdge', () => {
),
);
});

it('supports async server component debug info as the element owner in DEV', async () => {
function Container({children}) {
return children;
}

const promise = Promise.resolve(true);
async function Greeting({firstName}) {
// We can't use JSX here because it'll use the Client React.
const child = ReactServer.createElement(
'span',
null,
'Hello, ' + firstName,
);
// Yield the synchronous pass
await promise;
// We should still be able to track owner using AsyncLocalStorage.
return ReactServer.createElement(Container, null, child);
}

const model = {
greeting: ReactServer.createElement(Greeting, {firstName: 'Seb'}),
};

const stream = ReactServerDOMServer.renderToReadableStream(
model,
webpackMap,
);

const rootModel = await ReactServerDOMClient.createFromReadableStream(
stream,
{
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);

const ssrStream = await ReactDOMServer.renderToReadableStream(
rootModel.greeting,
);
const result = await readResult(ssrStream);
expect(result).toEqual('<span>Hello, Seb</span>');

// Resolve the React Lazy wrapper which must have resolved by now.
const lazyWrapper = rootModel.greeting;
const greeting = lazyWrapper._init(lazyWrapper._payload);

// We've rendered down to the span.
expect(greeting.type).toBe('span');
if (__DEV__) {
const greetInfo = {name: 'Greeting', env: 'Server', owner: null};
expect(lazyWrapper._debugInfo).toEqual([
greetInfo,
{name: 'Container', env: 'Server', owner: greetInfo},
]);
// The owner that created the span was the outer server component.
// We expect the debug info to be referentially equal to the owner.
expect(greeting._owner).toBe(lazyWrapper._debugInfo[0]);
} else {
expect(lazyWrapper._debugInfo).toBe(undefined);
expect(greeting._owner).toBe(
gate(flags => flags.disableStringRefs) ? undefined : null,
);
}
});
});
39 changes: 24 additions & 15 deletions packages/react-server/src/ReactFlightServer.js
Expand Up @@ -73,6 +73,8 @@ import {
isServerReference,
supportsRequestStorage,
requestStorage,
supportsComponentStorage,
componentStorage,
createHints,
initAsyncDebugInfo,
} from './ReactFlightServerConfig';
Expand All @@ -89,11 +91,9 @@ import {
getThenableStateAfterSuspending,
resetHooksForRequest,
} from './ReactFlightHooks';
import {
DefaultAsyncDispatcher,
currentOwner,
setCurrentOwner,
} from './flight/ReactFlightAsyncDispatcher';
import {DefaultAsyncDispatcher} from './flight/ReactFlightAsyncDispatcher';

import {resolveOwner, setCurrentOwner} from './flight/ReactFlightCurrentOwner';

import {
getIteratorFn,
Expand Down Expand Up @@ -162,7 +162,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
// We don't currently use this id for anything but we emit it so that we can later
// refer to previous logs in debug info to associate them with a component.
const id = request.nextChunkId++;
const owner: null | ReactComponentInfo = currentOwner;
const owner: null | ReactComponentInfo = resolveOwner();
emitConsoleChunk(request, id, methodName, owner, stack, arguments);
}
// $FlowFixMe[prop-missing]
Expand Down Expand Up @@ -824,7 +824,11 @@ function renderFunctionComponent<Props>(
const prevThenableState = task.thenableState;
task.thenableState = null;

let componentDebugInfo: null | ReactComponentInfo = null;
// The secondArg is always undefined in Server Components since refs error early.
const secondArg = undefined;
let result;

let componentDebugInfo: ReactComponentInfo;
if (__DEV__) {
if (debugID === null) {
// We don't have a chunk to assign debug info. We need to outline this
Expand Down Expand Up @@ -853,20 +857,25 @@ function renderFunctionComponent<Props>(
outlineModel(request, componentDebugInfo);
emitDebugChunk(request, componentDebugID, componentDebugInfo);
}
}

prepareToUseHooksForComponent(prevThenableState, componentDebugInfo);
// The secondArg is always undefined in Server Components since refs error early.
const secondArg = undefined;
let result;
if (__DEV__) {
prepareToUseHooksForComponent(prevThenableState, componentDebugInfo);
setCurrentOwner(componentDebugInfo);
try {
result = Component(props, secondArg);
if (supportsComponentStorage) {
// Run the component in an Async Context that tracks the current owner.
result = componentStorage.run(
componentDebugInfo,
Component,
props,
secondArg,
);
} else {
result = Component(props, secondArg);
}
} finally {
setCurrentOwner(null);
}
} else {
prepareToUseHooksForComponent(prevThenableState, null);
result = Component(props, secondArg);
}
if (typeof result === 'object' && result !== null) {
Expand Down
12 changes: 3 additions & 9 deletions packages/react-server/src/flight/ReactFlightAsyncDispatcher.js
Expand Up @@ -15,6 +15,8 @@ import {resolveRequest, getCache} from '../ReactFlightServer';

import {disableStringRefs} from 'shared/ReactFeatureFlags';

import {resolveOwner} from './ReactFlightCurrentOwner';

function resolveCache(): Map<Function, mixed> {
const request = resolveRequest();
if (request) {
Expand All @@ -36,19 +38,11 @@ export const DefaultAsyncDispatcher: AsyncDispatcher = ({
},
}: any);

export let currentOwner: ReactComponentInfo | null = null;

if (__DEV__) {
DefaultAsyncDispatcher.getOwner = (): null | ReactComponentInfo => {
return currentOwner;
};
DefaultAsyncDispatcher.getOwner = resolveOwner;
} else if (!disableStringRefs) {
// Server Components never use string refs but the JSX runtime looks for it.
DefaultAsyncDispatcher.getOwner = (): null | ReactComponentInfo => {
return null;
};
}

export function setCurrentOwner(componentInfo: null | ReactComponentInfo) {
currentOwner = componentInfo;
}
30 changes: 30 additions & 0 deletions packages/react-server/src/flight/ReactFlightCurrentOwner.js
@@ -0,0 +1,30 @@
/**
* 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 {ReactComponentInfo} from 'shared/ReactTypes';

import {
supportsComponentStorage,
componentStorage,
} from '../ReactFlightServerConfig';

let currentOwner: ReactComponentInfo | null = null;

export function setCurrentOwner(componentInfo: null | ReactComponentInfo) {
currentOwner = componentInfo;
}

export function resolveOwner(): null | ReactComponentInfo {
if (currentOwner) return currentOwner;
if (supportsComponentStorage) {
const owner = componentStorage.getStore();
if (owner) return owner;
}
return null;
}
Expand Up @@ -8,6 +8,7 @@
*/

import type {Request} from 'react-server/src/ReactFlightServer';
import type {ReactComponentInfo} from 'shared/ReactTypes';

export * from '../ReactFlightServerConfigBundlerCustom';

Expand All @@ -23,6 +24,10 @@ export const isPrimaryRenderer = false;
export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage<Request | void> = (null: any);

export const supportsComponentStorage = false;
export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
(null: any);

export function createHints(): any {
return null;
}
Expand Up @@ -6,15 +6,18 @@
*
* @flow
*/
import {AsyncLocalStorage} from 'async_hooks';

import type {Request} from 'react-server/src/ReactFlightServer';
import type {ReactComponentInfo} from 'shared/ReactTypes';

export * from 'react-server-dom-esm/src/ReactFlightServerConfigESMBundler';
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';

export const supportsRequestStorage = true;
export const requestStorage: AsyncLocalStorage<Request | void> =
new AsyncLocalStorage();
export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage<Request | void> = (null: any);

export const supportsComponentStorage = false;
export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
(null: any);

export * from '../ReactFlightServerConfigDebugNoop';
Expand Up @@ -8,11 +8,16 @@
*/

import type {Request} from 'react-server/src/ReactFlightServer';
import type {ReactComponentInfo} from 'shared/ReactTypes';

export * from 'react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler';
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';

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

export const supportsComponentStorage = false;
export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
(null: any);

export * from '../ReactFlightServerConfigDebugNoop';
Expand Up @@ -8,11 +8,16 @@
*/

import type {Request} from 'react-server/src/ReactFlightServer';
import type {ReactComponentInfo} from 'shared/ReactTypes';

export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';

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

export const supportsComponentStorage = false;
export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
(null: any);

export * from '../ReactFlightServerConfigDebugNoop';
Expand Up @@ -8,11 +8,16 @@
*/

import type {Request} from 'react-server/src/ReactFlightServer';
import type {ReactComponentInfo} from 'shared/ReactTypes';

export * from '../ReactFlightServerConfigBundlerCustom';
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';

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

export const supportsComponentStorage = false;
export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
(null: any);

export * from '../ReactFlightServerConfigDebugNoop';
Expand Up @@ -7,6 +7,7 @@
* @flow
*/
import type {Request} from 'react-server/src/ReactFlightServer';
import type {ReactComponentInfo} from 'shared/ReactTypes';

export * from 'react-server-dom-turbopack/src/ReactFlightServerConfigTurbopackBundler';
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
Expand All @@ -16,6 +17,11 @@ export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
export const requestStorage: AsyncLocalStorage<Request | void> =
supportsRequestStorage ? new AsyncLocalStorage() : (null: any);

export const supportsComponentStorage: boolean =
__DEV__ && supportsRequestStorage;
export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
supportsComponentStorage ? new AsyncLocalStorage() : (null: any);

// We use the Node version but get access to async_hooks from a global.
import type {HookCallbacks, AsyncHook} from 'async_hooks';
export const createAsyncHook: HookCallbacks => AsyncHook =
Expand Down
Expand Up @@ -6,7 +6,9 @@
*
* @flow
*/

import type {Request} from 'react-server/src/ReactFlightServer';
import type {ReactComponentInfo} from 'shared/ReactTypes';

export * from 'react-server-dom-webpack/src/ReactFlightServerConfigWebpackBundler';
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
Expand All @@ -16,6 +18,11 @@ export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
export const requestStorage: AsyncLocalStorage<Request | void> =
supportsRequestStorage ? new AsyncLocalStorage() : (null: any);

export const supportsComponentStorage: boolean =
__DEV__ && supportsRequestStorage;
export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
supportsComponentStorage ? new AsyncLocalStorage() : (null: any);

// We use the Node version but get access to async_hooks from a global.
import type {HookCallbacks, AsyncHook} from 'async_hooks';
export const createAsyncHook: HookCallbacks => AsyncHook =
Expand Down
Expand Up @@ -8,11 +8,16 @@
*/

import type {Request} from 'react-server/src/ReactFlightServer';
import type {ReactComponentInfo} from 'shared/ReactTypes';

export * from '../ReactFlightServerConfigBundlerCustom';
export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';

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

export const supportsComponentStorage = false;
export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
(null: any);

export * from '../ReactFlightServerConfigDebugNoop';