Skip to content

Commit cce18e3

Browse files
authored
[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 caa84c8 commit cce18e3

21 files changed

+137
-19
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 19 additions & 9 deletions
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

Lines changed: 17 additions & 4 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 7 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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;

0 commit comments

Comments
 (0)