Skip to content

Commit 4a99766

Browse files
committed
Use AsyncLocalStorage to extend the scope of the cache to micro tasks
1 parent db586ec commit 4a99766

9 files changed

Lines changed: 56 additions & 17 deletions

File tree

.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-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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1263,7 +1263,11 @@ function flushCompletedChunks(
12631263
}
12641264

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

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

packages/react-server/src/ReactServerStreamConfigBrowser.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ export function flushBuffered(destination: Destination) {
2424
// For now we support AsyncLocalStorage as a global for the "browser" builds
2525
// TODO: Move this to some special WinterCG build.
2626
export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
27-
export const requestStorage = new AsyncLocalStorage();
27+
export const requestStorage: AsyncLocalStorage<
28+
Map<Function, mixed>,
29+
> = supportsRequestStorage ? new AsyncLocalStorage() : (null: any);
2830

2931
const VIEW_SIZE = 512;
3032
let currentView = null;

packages/react-server/src/ReactServerStreamConfigNode.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ export function flushBuffered(destination: Destination) {
3535
}
3636

3737
export const supportsRequestStorage = true;
38-
export const requestStorage = new AsyncLocalStorage();
38+
export const requestStorage: AsyncLocalStorage<
39+
Map<Function, mixed>,
40+
> = new AsyncLocalStorage();
3941

4042
const VIEW_SIZE = 2048;
4143
let currentView = null;

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

Lines changed: 6 additions & 2 deletions
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) {
@@ -81,14 +83,16 @@ describe('ReactFetch', () => {
8183
async function getData() {
8284
const r1 = await fetch('hello');
8385
const t1 = await r1.text();
84-
const r2 = await fetch('hello');
86+
const r2 = await fetch('world');
8587
const t2 = await r2.text();
8688
return t1 + ' ' + t2;
8789
}
8890
function Component() {
8991
return use(getData());
9092
}
91-
expect(await render(Component)).toMatchInlineSnapshot(`"GET world []"`);
93+
expect(await render(Component)).toMatchInlineSnapshot(
94+
`"GET hello [] GET world []"`,
95+
);
9296
expect(fetchCount).toBe(2);
9397
});
9498

scripts/error-codes/codes.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,5 +443,5 @@
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.",
445445
"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"
446+
"458": "Currently React only supports one RSC renderer at a time."
447447
}

scripts/flow/environment.js

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

Lines changed: 2 additions & 2 deletions
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', 'async-hooks', 'react-dom'],
323+
externals: ['react', 'util', 'async_hooks', 'react-dom'],
324324
},
325325
{
326326
bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [],
@@ -394,7 +394,7 @@ const bundles = [
394394
global: 'ReactServerDOMServer',
395395
minifyWithProdErrorCodes: false,
396396
wrapWithModuleBoundaries: false,
397-
externals: ['react', 'util', 'async-hooks', 'react-dom'],
397+
externals: ['react', 'util', 'async_hooks', 'react-dom'],
398398
},
399399

400400
/******* React Server DOM Webpack Client *******/

0 commit comments

Comments
 (0)