Skip to content

Commit 40f0320

Browse files
sebmarkbagerickhanlonii
authored andcommitted
[Flight] Use AsyncLocalStorage to extend the scope of the cache to micro tasks (#25542)
This extends the scope of the cache and fetch instrumentation using AsyncLocalStorage for microtasks. This is an intermediate step. It sets up the dispatcher only once. This is unique to RSC because it uses the react.shared-subset module for its shared state. Ideally we should support multiple renderers. We should also have this take over from an outer SSR's instrumented fetch. We should also be able to have a fallback to global state per request where AsyncLocalStorage doesn't exist and then the whole client-side solutions. I'm still figuring out the right wiring for that so this is a temporary hack.
1 parent 99b9b30 commit 40f0320

21 files changed

+137
-19
lines changed

.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -276,5 +276,6 @@ module.exports = {
276276
gate: 'readonly',
277277
trustedTypes: 'readonly',
278278
IS_REACT_ACT_ENVIRONMENT: 'readonly',
279+
AsyncLocalStorage: 'readonly',
279280
},
280281
};

packages/react-dom-bindings/src/server/ReactDOMLegacyServerStreamConfig.js

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export function scheduleWork(callback: () => void) {
2121

2222
export function flushBuffered(destination: Destination) {}
2323

24+
export const supportsRequestStorage = false;
25+
export const requestStorage: AsyncLocalStorage<any> = (null: any);
26+
2427
export function beginWriting(destination: Destination) {}
2528

2629
export function writeChunk(

packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js

+5
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ export function scheduleWork(callback: () => void) {
191191

192192
export function flushBuffered(destination: Destination) {}
193193

194+
export const supportsRequestStorage = false;
195+
export const requestStorage: AsyncLocalStorage<
196+
Map<Function, mixed>,
197+
> = (null: any);
198+
194199
export function beginWriting(destination: Destination) {}
195200

196201
export function writeChunk(destination: Destination, chunk: Chunk): void {

packages/react-server-dom-relay/src/ReactServerStreamConfigFB.js

+5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export function scheduleWork(callback: () => void) {
2323

2424
export function flushBuffered(destination: Destination) {}
2525

26+
export const supportsRequestStorage = false;
27+
export const requestStorage: AsyncLocalStorage<
28+
Map<Function, mixed>,
29+
> = (null: any);
30+
2631
export function beginWriting(destination: Destination) {}
2732

2833
export function writeChunk(

packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js

+5
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,11 @@ export function scheduleWork(callback: () => void) {
186186

187187
export function flushBuffered(destination: Destination) {}
188188

189+
export const supportsRequestStorage = false;
190+
export const requestStorage: AsyncLocalStorage<
191+
Map<Function, mixed>,
192+
> = (null: any);
193+
189194
export function beginWriting(destination: Destination) {}
190195

191196
export function writeChunk(destination: Destination, chunk: Chunk): void {

packages/react-server/src/ReactFlightCache.js

+19-9
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,30 @@
99

1010
import type {CacheDispatcher} from 'react-reconciler/src/ReactInternalTypes';
1111

12+
import {
13+
supportsRequestStorage,
14+
requestStorage,
15+
} from './ReactFlightServerConfig';
16+
1217
function createSignal(): AbortSignal {
1318
return new AbortController().signal;
1419
}
1520

21+
function resolveCache(): Map<Function, mixed> {
22+
if (currentCache) return currentCache;
23+
if (supportsRequestStorage) {
24+
const cache = requestStorage.getStore();
25+
if (cache) return cache;
26+
}
27+
// Since we override the dispatcher all the time, we're effectively always
28+
// active and so to support cache() and fetch() outside of render, we yield
29+
// an empty Map.
30+
return new Map();
31+
}
32+
1633
export const DefaultCacheDispatcher: CacheDispatcher = {
1734
getCacheSignal(): AbortSignal {
18-
if (!currentCache) {
19-
throw new Error('Reading the cache is only supported while rendering.');
20-
}
21-
let entry: AbortSignal | void = (currentCache.get(createSignal): any);
35+
let entry: AbortSignal | void = (resolveCache().get(createSignal): any);
2236
if (entry === undefined) {
2337
entry = createSignal();
2438
// $FlowFixMe[incompatible-use] found when upgrading Flow
@@ -27,11 +41,7 @@ export const DefaultCacheDispatcher: CacheDispatcher = {
2741
return entry;
2842
},
2943
getCacheForType<T>(resourceType: () => T): T {
30-
if (!currentCache) {
31-
throw new Error('Reading the cache is only supported while rendering.');
32-
}
33-
34-
let entry: T | void = (currentCache.get(resourceType): any);
44+
let entry: T | void = (resolveCache().get(resourceType): any);
3545
if (entry === undefined) {
3646
entry = resourceType();
3747
// TODO: Warn if undefined?

packages/react-server/src/ReactFlightServer.js

+17-4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import {
4343
resolveModuleMetaData,
4444
getModuleKey,
4545
isModuleReference,
46+
supportsRequestStorage,
47+
requestStorage,
4648
} from './ReactFlightServerConfig';
4749

4850
import {
@@ -157,6 +159,16 @@ export function createRequest(
157159
context?: Array<[string, ServerContextJSONValue]>,
158160
identifierPrefix?: string,
159161
): Request {
162+
if (
163+
ReactCurrentCache.current !== null &&
164+
ReactCurrentCache.current !== DefaultCacheDispatcher
165+
) {
166+
throw new Error(
167+
'Currently React only supports one RSC renderer at a time.',
168+
);
169+
}
170+
ReactCurrentCache.current = DefaultCacheDispatcher;
171+
160172
const abortSet: Set<Task> = new Set();
161173
const pingedTasks = [];
162174
const request = {
@@ -1155,10 +1167,8 @@ function retryTask(request: Request, task: Task): void {
11551167

11561168
function performWork(request: Request): void {
11571169
const prevDispatcher = ReactCurrentDispatcher.current;
1158-
const prevCacheDispatcher = ReactCurrentCache.current;
11591170
const prevCache = getCurrentCache();
11601171
ReactCurrentDispatcher.current = HooksDispatcher;
1161-
ReactCurrentCache.current = DefaultCacheDispatcher;
11621172
setCurrentCache(request.cache);
11631173
prepareToUseHooksForRequest(request);
11641174

@@ -1177,7 +1187,6 @@ function performWork(request: Request): void {
11771187
fatalError(request, error);
11781188
} finally {
11791189
ReactCurrentDispatcher.current = prevDispatcher;
1180-
ReactCurrentCache.current = prevCacheDispatcher;
11811190
setCurrentCache(prevCache);
11821191
resetHooksForRequest();
11831192
}
@@ -1254,7 +1263,11 @@ function flushCompletedChunks(
12541263
}
12551264

12561265
export function startWork(request: Request): void {
1257-
scheduleWork(() => performWork(request));
1266+
if (supportsRequestStorage) {
1267+
scheduleWork(() => requestStorage.run(request.cache, performWork, request));
1268+
} else {
1269+
scheduleWork(() => performWork(request));
1270+
}
12581271
}
12591272

12601273
export function startFlowing(request: Request, destination: Destination): void {

packages/react-server/src/ReactFlightServerConfigStream.js

+5
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ import type {Chunk} from './ReactServerStreamConfig';
7272

7373
export type {Destination, Chunk} from './ReactServerStreamConfig';
7474

75+
export {
76+
supportsRequestStorage,
77+
requestStorage,
78+
} from './ReactServerStreamConfig';
79+
7580
const stringify = JSON.stringify;
7681

7782
function serializeRowHeader(tag: string, id: number) {

packages/react-server/src/ReactServerStreamConfigBrowser.js

+7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ export function flushBuffered(destination: Destination) {
2121
// transform streams. https://github.com/whatwg/streams/issues/960
2222
}
2323

24+
// For now we support AsyncLocalStorage as a global for the "browser" builds
25+
// TODO: Move this to some special WinterCG build.
26+
export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
27+
export const requestStorage: AsyncLocalStorage<
28+
Map<Function, mixed>,
29+
> = supportsRequestStorage ? new AsyncLocalStorage() : (null: any);
30+
2431
const VIEW_SIZE = 512;
2532
let currentView = null;
2633
let writtenBytes = 0;

packages/react-server/src/ReactServerStreamConfigNode.js

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import type {Writable} from 'stream';
1111
import {TextEncoder} from 'util';
12+
import {AsyncLocalStorage} from 'async_hooks';
1213

1314
interface MightBeFlushable {
1415
flush?: () => void;
@@ -33,6 +34,11 @@ export function flushBuffered(destination: Destination) {
3334
}
3435
}
3536

37+
export const supportsRequestStorage = true;
38+
export const requestStorage: AsyncLocalStorage<
39+
Map<Function, mixed>,
40+
> = new AsyncLocalStorage();
41+
3642
const VIEW_SIZE = 2048;
3743
let currentView = null;
3844
let writtenBytes = 0;

packages/react-server/src/forks/ReactServerStreamConfig.custom.js

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export const writeChunk = $$$hostConfig.writeChunk;
3535
export const writeChunkAndReturn = $$$hostConfig.writeChunkAndReturn;
3636
export const completeWriting = $$$hostConfig.completeWriting;
3737
export const flushBuffered = $$$hostConfig.flushBuffered;
38+
export const supportsRequestStorage = $$$hostConfig.supportsRequestStorage;
39+
export const requestStorage = $$$hostConfig.requestStorage;
3840
export const close = $$$hostConfig.close;
3941
export const closeWithError = $$$hostConfig.closeWithError;
4042
export const stringToChunk = $$$hostConfig.stringToChunk;

packages/react/src/__tests__/ReactFetch-test.js

+20
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ global.TextDecoder = require('util').TextDecoder;
1616
global.Headers = require('node-fetch').Headers;
1717
global.Request = require('node-fetch').Request;
1818
global.Response = require('node-fetch').Response;
19+
// Patch for Browser environments to be able to polyfill AsyncLocalStorage
20+
global.AsyncLocalStorage = require('async_hooks').AsyncLocalStorage;
1921

2022
let fetchCount = 0;
2123
async function fetchMock(resource, options) {
@@ -76,6 +78,24 @@ describe('ReactFetch', () => {
7678
expect(fetchCount).toBe(1);
7779
});
7880

81+
// @gate enableFetchInstrumentation && enableCache
82+
it('can dedupe fetches in micro tasks', async () => {
83+
async function getData() {
84+
const r1 = await fetch('hello');
85+
const t1 = await r1.text();
86+
const r2 = await fetch('world');
87+
const t2 = await r2.text();
88+
return t1 + ' ' + t2;
89+
}
90+
function Component() {
91+
return use(getData());
92+
}
93+
expect(await render(Component)).toMatchInlineSnapshot(
94+
`"GET hello [] GET world []"`,
95+
);
96+
expect(fetchCount).toBe(2);
97+
});
98+
7999
// @gate enableFetchInstrumentation && enableCache
80100
it('can dedupe fetches using Request and not', async () => {
81101
function Component() {

scripts/error-codes/codes.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -442,5 +442,6 @@
442442
"454": "React expected a <body> element (document.body) to exist in the Document but one was not found. React never removes the body for any Document it renders into so the cause is likely in some other script running on this page.",
443443
"455": "This CacheSignal was requested outside React which means that it is immediately aborted.",
444444
"456": "Calling Offscreen.detach before instance handle has been set.",
445-
"457": "acquireHeadResource encountered a resource type it did not expect: \"%s\". This is a bug in React."
445+
"457": "acquireHeadResource encountered a resource type it did not expect: \"%s\". This is a bug in React.",
446+
"458": "Currently React only supports one RSC renderer at a time."
446447
}

scripts/flow/environment.js

+16
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,19 @@ declare module 'pg/lib/utils' {
157157
prepareValue(val: any): mixed,
158158
};
159159
}
160+
161+
declare class AsyncLocalStorage<T> {
162+
disable(): void;
163+
getStore(): T | void;
164+
run(store: T, callback: (...args: any[]) => void, ...args: any[]): void;
165+
enterWith(store: T): void;
166+
}
167+
168+
declare module 'async_hooks' {
169+
declare class AsyncLocalStorage<T> {
170+
disable(): void;
171+
getStore(): T | void;
172+
run(store: T, callback: (...args: any[]) => void, ...args: any[]): void;
173+
enterWith(store: T): void;
174+
}
175+
}

scripts/rollup/bundles.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ const bundles = [
320320
global: 'ReactDOMServer',
321321
minifyWithProdErrorCodes: false,
322322
wrapWithModuleBoundaries: false,
323-
externals: ['react', 'util', 'react-dom'],
323+
externals: ['react', 'util', 'async_hooks', 'react-dom'],
324324
},
325325
{
326326
bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [],
@@ -350,7 +350,7 @@ const bundles = [
350350
global: 'ReactDOMStatic',
351351
minifyWithProdErrorCodes: false,
352352
wrapWithModuleBoundaries: false,
353-
externals: ['react', 'util', 'stream', 'react-dom'],
353+
externals: ['react', 'util', 'async_hooks', 'stream', 'react-dom'],
354354
},
355355

356356
/******* React DOM Fizz Server External Runtime *******/
@@ -394,7 +394,7 @@ const bundles = [
394394
global: 'ReactServerDOMServer',
395395
minifyWithProdErrorCodes: false,
396396
wrapWithModuleBoundaries: false,
397-
externals: ['react', 'util', 'react-dom'],
397+
externals: ['react', 'util', 'async_hooks', 'react-dom'],
398398
},
399399

400400
/******* React Server DOM Webpack Client *******/
@@ -462,7 +462,7 @@ const bundles = [
462462
bundleTypes: [FB_WWW_DEV, FB_WWW_PROD],
463463
moduleType: RENDERER,
464464
entry: 'react-server-dom-relay',
465-
global: 'ReactFlightDOMRelayClient', // TODO: Rename to Reader
465+
global: 'ReactFlightDOMRelayClient',
466466
minifyWithProdErrorCodes: true,
467467
wrapWithModuleBoundaries: false,
468468
externals: [
@@ -477,7 +477,7 @@ const bundles = [
477477
bundleTypes: [RN_FB_DEV, RN_FB_PROD],
478478
moduleType: RENDERER,
479479
entry: 'react-server-native-relay/server',
480-
global: 'ReactFlightNativeRelayServer', // TODO: Rename to Writer
480+
global: 'ReactFlightNativeRelayServer',
481481
minifyWithProdErrorCodes: false,
482482
wrapWithModuleBoundaries: false,
483483
externals: [
@@ -486,6 +486,7 @@ const bundles = [
486486
'JSResourceReferenceImpl',
487487
'ReactNativeInternalFeatureFlags',
488488
'util',
489+
'async_hooks',
489490
],
490491
},
491492

scripts/rollup/validate/eslintrc.cjs.js

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ module.exports = {
3838
Uint8Array: 'readonly',
3939
Promise: 'readonly',
4040

41+
// Temp
42+
AsyncLocalStorage: 'readonly',
43+
4144
// Flight Webpack
4245
__webpack_chunk_load__: 'readonly',
4346
__webpack_require__: 'readonly',

scripts/rollup/validate/eslintrc.cjs2015.js

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ module.exports = {
3737
Uint8Array: 'readonly',
3838
Promise: 'readonly',
3939

40+
// Temp
41+
AsyncLocalStorage: 'readonly',
42+
4043
// Flight Webpack
4144
__webpack_chunk_load__: 'readonly',
4245
__webpack_require__: 'readonly',

scripts/rollup/validate/eslintrc.esm.js

+3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ module.exports = {
3636
Uint8Array: 'readonly',
3737
Promise: 'readonly',
3838

39+
// Temp
40+
AsyncLocalStorage: 'readonly',
41+
3942
// Flight Webpack
4043
__webpack_chunk_load__: 'readonly',
4144
__webpack_require__: 'readonly',

scripts/rollup/validate/eslintrc.fb.js

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ module.exports = {
3737
Uint8Array: 'readonly',
3838
Promise: 'readonly',
3939

40+
// Temp
41+
AsyncLocalStorage: 'readonly',
42+
4043
// jest
4144
jest: 'readonly',
4245

scripts/rollup/validate/eslintrc.rn.js

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ module.exports = {
3333
TaskController: 'readonly',
3434
reportError: 'readonly',
3535

36+
// Temp
37+
AsyncLocalStorage: 'readonly',
38+
3639
// jest
3740
jest: 'readonly',
3841

scripts/rollup/validate/eslintrc.umd.js

+3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ module.exports = {
4242
Uint8Array: 'readonly',
4343
Promise: 'readonly',
4444

45+
// Temp
46+
AsyncLocalStorage: 'readonly',
47+
4548
// Flight Webpack
4649
__webpack_chunk_load__: 'readonly',
4750
__webpack_require__: 'readonly',

0 commit comments

Comments
 (0)