From df075242e926c0a13b6b0e2204d74cdb796bf12a Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sat, 22 Oct 2022 23:49:22 -0400 Subject: [PATCH] Use AsyncLocalStorage to extend the scope of the cache to micro tasks --- .eslintrc.js | 1 + .../ReactDOMLegacyServerStreamConfig.js | 3 ++ .../ReactFlightDOMRelayServerHostConfig.js | 5 ++++ .../src/ReactServerStreamConfigFB.js | 5 ++++ .../ReactFlightNativeRelayServerHostConfig.js | 5 ++++ packages/react-server/src/ReactFlightCache.js | 28 +++++++++++++------ .../react-server/src/ReactFlightServer.js | 6 +++- .../src/ReactServerStreamConfigBrowser.js | 4 ++- .../src/ReactServerStreamConfigNode.js | 4 ++- .../forks/ReactServerStreamConfig.custom.js | 2 ++ .../react/src/__tests__/ReactFetch-test.js | 8 ++++-- scripts/error-codes/codes.json | 2 +- scripts/flow/environment.js | 16 +++++++++++ scripts/rollup/bundles.js | 11 ++++---- scripts/rollup/validate/eslintrc.rn.js | 2 ++ 15 files changed, 82 insertions(+), 20 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index c272cfcd4a4ad..9445bacebe835 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -276,5 +276,6 @@ module.exports = { gate: 'readonly', trustedTypes: 'readonly', IS_REACT_ACT_ENVIRONMENT: 'readonly', + AsyncLocalStorage: 'readonly', }, }; diff --git a/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js b/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js index 1988170779e4c..d1b68be1b350e 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js @@ -21,6 +21,9 @@ export function scheduleWork(callback: () => void) { export function flushBuffered(destination: Destination) {} +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage = (null: any); + export function beginWriting(destination: Destination) {} export function writeChunk( diff --git a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js index 03b5134c7757d..50d31ea1316ca 100644 --- a/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js +++ b/packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js @@ -191,6 +191,11 @@ export function scheduleWork(callback: () => void) { export function flushBuffered(destination: Destination) {} +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage< + Map, +> = (null: any); + export function beginWriting(destination: Destination) {} export function writeChunk(destination: Destination, chunk: Chunk): void { diff --git a/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js b/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js index ee6b151ead541..a1874ce362386 100644 --- a/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js +++ b/packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js @@ -23,6 +23,11 @@ export function scheduleWork(callback: () => void) { export function flushBuffered(destination: Destination) {} +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage< + Map, +> = (null: any); + export function beginWriting(destination: Destination) {} export function writeChunk( diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js index 621bc8890d33f..814773b3f128a 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js @@ -186,6 +186,11 @@ export function scheduleWork(callback: () => void) { export function flushBuffered(destination: Destination) {} +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage< + Map, +> = (null: any); + export function beginWriting(destination: Destination) {} export function writeChunk(destination: Destination, chunk: Chunk): void { diff --git a/packages/react-server/src/ReactFlightCache.js b/packages/react-server/src/ReactFlightCache.js index 54a34990e0114..e69f84afb3704 100644 --- a/packages/react-server/src/ReactFlightCache.js +++ b/packages/react-server/src/ReactFlightCache.js @@ -9,16 +9,30 @@ import type {CacheDispatcher} from 'react-reconciler/src/ReactInternalTypes'; +import { + supportsRequestStorage, + requestStorage, +} from './ReactFlightServerConfig'; + function createSignal(): AbortSignal { return new AbortController().signal; } +function resolveCache(): Map { + if (currentCache) return currentCache; + if (supportsRequestStorage) { + const cache = requestStorage.getStore(); + if (cache) return cache; + } + // Since we override the dispatcher all the time, we're effectively always + // active and so to support cache() and fetch() outside of render, we yield + // an empty Map. + return new Map(); +} + export const DefaultCacheDispatcher: CacheDispatcher = { getCacheSignal(): AbortSignal { - if (!currentCache) { - throw new Error('Reading the cache is only supported while rendering.'); - } - let entry: AbortSignal | void = (currentCache.get(createSignal): any); + let entry: AbortSignal | void = (resolveCache().get(createSignal): any); if (entry === undefined) { entry = createSignal(); // $FlowFixMe[incompatible-use] found when upgrading Flow @@ -27,11 +41,7 @@ export const DefaultCacheDispatcher: CacheDispatcher = { return entry; }, getCacheForType(resourceType: () => T): T { - if (!currentCache) { - throw new Error('Reading the cache is only supported while rendering.'); - } - - let entry: T | void = (currentCache.get(resourceType): any); + let entry: T | void = (resolveCache().get(resourceType): any); if (entry === undefined) { entry = resourceType(); // TODO: Warn if undefined? diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 5e4232a6cd984..dc82d78d0d40d 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -1263,7 +1263,11 @@ function flushCompletedChunks( } export function startWork(request: Request): void { - scheduleWork(() => performWork(request)); + if (supportsRequestStorage) { + scheduleWork(() => requestStorage.run(request.cache, performWork, request)); + } else { + scheduleWork(() => performWork(request)); + } } export function startFlowing(request: Request, destination: Destination): void { diff --git a/packages/react-server/src/ReactServerStreamConfigBrowser.js b/packages/react-server/src/ReactServerStreamConfigBrowser.js index dd7108a6752a2..082e0edbaf46c 100644 --- a/packages/react-server/src/ReactServerStreamConfigBrowser.js +++ b/packages/react-server/src/ReactServerStreamConfigBrowser.js @@ -24,7 +24,9 @@ export function flushBuffered(destination: Destination) { // For now we support AsyncLocalStorage as a global for the "browser" builds // TODO: Move this to some special WinterCG build. export const supportsRequestStorage = typeof AsyncLocalStorage === 'function'; -export const requestStorage = new AsyncLocalStorage(); +export const requestStorage: AsyncLocalStorage< + Map, +> = supportsRequestStorage ? new AsyncLocalStorage() : (null: any); const VIEW_SIZE = 512; let currentView = null; diff --git a/packages/react-server/src/ReactServerStreamConfigNode.js b/packages/react-server/src/ReactServerStreamConfigNode.js index 9ebfe7f7a41e6..5790682d301c9 100644 --- a/packages/react-server/src/ReactServerStreamConfigNode.js +++ b/packages/react-server/src/ReactServerStreamConfigNode.js @@ -35,7 +35,9 @@ export function flushBuffered(destination: Destination) { } export const supportsRequestStorage = true; -export const requestStorage = new AsyncLocalStorage(); +export const requestStorage: AsyncLocalStorage< + Map, +> = new AsyncLocalStorage(); const VIEW_SIZE = 2048; let currentView = null; diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.custom.js b/packages/react-server/src/forks/ReactServerStreamConfig.custom.js index 7fa07365b8559..8a5fd3173c96b 100644 --- a/packages/react-server/src/forks/ReactServerStreamConfig.custom.js +++ b/packages/react-server/src/forks/ReactServerStreamConfig.custom.js @@ -35,6 +35,8 @@ export const writeChunk = $$$hostConfig.writeChunk; export const writeChunkAndReturn = $$$hostConfig.writeChunkAndReturn; export const completeWriting = $$$hostConfig.completeWriting; export const flushBuffered = $$$hostConfig.flushBuffered; +export const supportsRequestStorage = $$$hostConfig.supportsRequestStorage; +export const requestStorage = $$$hostConfig.requestStorage; export const close = $$$hostConfig.close; export const closeWithError = $$$hostConfig.closeWithError; export const stringToChunk = $$$hostConfig.stringToChunk; diff --git a/packages/react/src/__tests__/ReactFetch-test.js b/packages/react/src/__tests__/ReactFetch-test.js index c86748432030a..24520c69d8cc5 100644 --- a/packages/react/src/__tests__/ReactFetch-test.js +++ b/packages/react/src/__tests__/ReactFetch-test.js @@ -16,6 +16,8 @@ global.TextDecoder = require('util').TextDecoder; global.Headers = require('node-fetch').Headers; global.Request = require('node-fetch').Request; global.Response = require('node-fetch').Response; +// Patch for Browser environments to be able to polyfill AsyncLocalStorage +global.AsyncLocalStorage = require('async_hooks').AsyncLocalStorage; let fetchCount = 0; async function fetchMock(resource, options) { @@ -81,14 +83,16 @@ describe('ReactFetch', () => { async function getData() { const r1 = await fetch('hello'); const t1 = await r1.text(); - const r2 = await fetch('hello'); + const r2 = await fetch('world'); const t2 = await r2.text(); return t1 + ' ' + t2; } function Component() { return use(getData()); } - expect(await render(Component)).toMatchInlineSnapshot(`"GET world []"`); + expect(await render(Component)).toMatchInlineSnapshot( + `"GET hello [] GET world []"`, + ); expect(fetchCount).toBe(2); }); diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 412b53791cd60..9400521ca3c8f 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -443,5 +443,5 @@ "455": "This CacheSignal was requested outside React which means that it is immediately aborted.", "456": "Calling Offscreen.detach before instance handle has been set.", "457": "acquireHeadResource encountered a resource type it did not expect: \"%s\". This is a bug in React.", - "458": "Currently React only supports one RSC renderer at a time" + "458": "Currently React only supports one RSC renderer at a time." } diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index 0d4d0d33c216a..ffb3728c17729 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -157,3 +157,19 @@ declare module 'pg/lib/utils' { prepareValue(val: any): mixed, }; } + +declare class AsyncLocalStorage { + disable(): void; + getStore(): T | void; + run(store: T, callback: (...args: any[]) => void, ...args: any[]): void; + enterWith(store: T): void; +} + +declare module 'async_hooks' { + declare class AsyncLocalStorage { + disable(): void; + getStore(): T | void; + run(store: T, callback: (...args: any[]) => void, ...args: any[]): void; + enterWith(store: T): void; + } +} diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index deba31cbf1232..7325bb41b3124 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -320,7 +320,7 @@ const bundles = [ global: 'ReactDOMServer', minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, - externals: ['react', 'util', 'async-hooks', 'react-dom'], + externals: ['react', 'util', 'async_hooks', 'react-dom'], }, { bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [], @@ -350,7 +350,7 @@ const bundles = [ global: 'ReactDOMStatic', minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, - externals: ['react', 'util', 'stream', 'react-dom'], + externals: ['react', 'util', 'async_hooks', 'stream', 'react-dom'], }, /******* React DOM Fizz Server External Runtime *******/ @@ -394,7 +394,7 @@ const bundles = [ global: 'ReactServerDOMServer', minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, - externals: ['react', 'util', 'async-hooks', 'react-dom'], + externals: ['react', 'util', 'async_hooks', 'react-dom'], }, /******* React Server DOM Webpack Client *******/ @@ -462,7 +462,7 @@ const bundles = [ bundleTypes: [FB_WWW_DEV, FB_WWW_PROD], moduleType: RENDERER, entry: 'react-server-dom-relay', - global: 'ReactFlightDOMRelayClient', // TODO: Rename to Reader + global: 'ReactFlightDOMRelayClient', minifyWithProdErrorCodes: true, wrapWithModuleBoundaries: false, externals: [ @@ -477,7 +477,7 @@ const bundles = [ bundleTypes: [RN_FB_DEV, RN_FB_PROD], moduleType: RENDERER, entry: 'react-server-native-relay/server', - global: 'ReactFlightNativeRelayServer', // TODO: Rename to Writer + global: 'ReactFlightNativeRelayServer', minifyWithProdErrorCodes: false, wrapWithModuleBoundaries: false, externals: [ @@ -486,6 +486,7 @@ const bundles = [ 'JSResourceReferenceImpl', 'ReactNativeInternalFeatureFlags', 'util', + 'async_hooks', ], }, diff --git a/scripts/rollup/validate/eslintrc.rn.js b/scripts/rollup/validate/eslintrc.rn.js index 21d8397487ff2..940346cdc5da1 100644 --- a/scripts/rollup/validate/eslintrc.rn.js +++ b/scripts/rollup/validate/eslintrc.rn.js @@ -33,6 +33,8 @@ module.exports = { TaskController: 'readonly', reportError: 'readonly', + AsyncLocalStorage: 'readonly', + // jest jest: 'readonly',