Skip to content

Commit

Permalink
Use AsyncLocalStorage to extend the scope of the cache to micro tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage committed Oct 23, 2022
1 parent db586ec commit 4a99766
Show file tree
Hide file tree
Showing 9 changed files with 56 additions and 17 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,5 +276,6 @@ module.exports = {
gate: 'readonly',
trustedTypes: 'readonly',
IS_REACT_ACT_ENVIRONMENT: 'readonly',
AsyncLocalStorage: 'readonly',
},
};
28 changes: 19 additions & 9 deletions packages/react-server/src/ReactFlightCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Function, mixed> {
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
Expand All @@ -27,11 +41,7 @@ export const DefaultCacheDispatcher: CacheDispatcher = {
return entry;
},
getCacheForType<T>(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?
Expand Down
6 changes: 5 additions & 1 deletion packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion packages/react-server/src/ReactServerStreamConfigBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Function, mixed>,
> = supportsRequestStorage ? new AsyncLocalStorage() : (null: any);

const VIEW_SIZE = 512;
let currentView = null;
Expand Down
4 changes: 3 additions & 1 deletion packages/react-server/src/ReactServerStreamConfigNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export function flushBuffered(destination: Destination) {
}

export const supportsRequestStorage = true;
export const requestStorage = new AsyncLocalStorage();
export const requestStorage: AsyncLocalStorage<
Map<Function, mixed>,
> = new AsyncLocalStorage();

const VIEW_SIZE = 2048;
let currentView = null;
Expand Down
8 changes: 6 additions & 2 deletions packages/react/src/__tests__/ReactFetch-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
});

Expand Down
2 changes: 1 addition & 1 deletion scripts/error-codes/codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
16 changes: 16 additions & 0 deletions scripts/flow/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,19 @@ declare module 'pg/lib/utils' {
prepareValue(val: any): mixed,
};
}

declare class AsyncLocalStorage<T> {
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<T> {
disable(): void;
getStore(): T | void;
run(store: T, callback: (...args: any[]) => void, ...args: any[]): void;
enterWith(store: T): void;
}
}
4 changes: 2 additions & 2 deletions scripts/rollup/bundles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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] : [],
Expand Down Expand Up @@ -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 *******/
Expand Down

0 comments on commit 4a99766

Please sign in to comment.