Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Flight] Add getCacheForType() to the dispatcher #20315

Merged
merged 13 commits into from
Dec 3, 2020
21 changes: 18 additions & 3 deletions fixtures/flight/server/cli.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,28 @@ const app = express();
// Application
app.get('/', function(req, res) {
if (process.env.NODE_ENV === 'development') {
for (var key in require.cache) {
delete require.cache[key];
}
// This doesn't work in ESM mode.
// for (var key in require.cache) {
// delete require.cache[key];
// }
}
require('./handler.server.js')(req, res);
});

app.get('/todos', function(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.json([
{
id: 1,
text: 'Shave yaks',
},
{
id: 2,
text: 'Eat kale',
},
]);
});

app.listen(3001, () => {
console.log('Flight Server listening on port 3001...');
});
Expand Down
7 changes: 7 additions & 0 deletions fixtures/flight/src/App.server.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import {fetch} from 'react-fetch';

import Container from './Container.js';

Expand All @@ -8,11 +9,17 @@ import {Counter as Counter2} from './Counter2.client.js';
import ShowMore from './ShowMore.client.js';

export default function App() {
const todos = fetch('http://localhost:3001/todos').json();
return (
<Container>
<h1>Hello, world</h1>
<Counter />
<Counter2 />
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
<ShowMore>
<p>Lorem ipsum</p>
</ShowMore>
Expand Down
6 changes: 6 additions & 0 deletions packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig';
import {NoMode} from 'react-reconciler/src/ReactTypeOfMode';

import ErrorStackParser from 'error-stack-parser';
import invariant from 'shared/invariant';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols';
import {
Expand Down Expand Up @@ -100,6 +101,10 @@ function nextHook(): null | Hook {
return hook;
}

function getCacheForType<T>(resourceType: () => T): T {
invariant(false, 'Not implemented.');
}

function readContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
Expand Down Expand Up @@ -298,6 +303,7 @@ function useOpaqueIdentifier(): OpaqueIDType | void {
}

const Dispatcher: DispatcherType = {
getCacheForType,
readContext,
useCallback,
useContext,
Expand Down
9 changes: 9 additions & 0 deletions packages/react-dom/src/server/ReactPartialRendererHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type PartialRenderer from './ReactPartialRenderer';
import {validateContextBounds} from './ReactPartialRendererContext';

import invariant from 'shared/invariant';
import {enableCache} from 'shared/ReactFeatureFlags';
import is from 'shared/objectIs';

type BasicStateAction<S> = (S => S) | S;
Expand Down Expand Up @@ -214,6 +215,10 @@ export function resetHooksState(): void {
workInProgressHook = null;
}

function getCacheForType<T>(resourceType: () => T): T {
invariant(false, 'Not implemented.');
}

function readContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
Expand Down Expand Up @@ -512,3 +517,7 @@ export const Dispatcher: DispatcherType = {
// Subscriptions are not setup in a server environment.
useMutableSource,
};

if (enableCache) {
Dispatcher.getCacheForType = getCacheForType;
}
21 changes: 9 additions & 12 deletions packages/react-fetch/src/ReactFetchBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import type {Wakeable} from 'shared/ReactTypes';

import {readCache} from 'react/unstable-cache';
import {unstable_getCacheForType} from 'react';

const Pending = 0;
const Resolved = 1;
Expand All @@ -34,16 +34,13 @@ type Result = PendingResult | ResolvedResult | RejectedResult;

// TODO: this is a browser-only version. Add a separate Node entry point.
const nativeFetch = window.fetch;
const fetchKey = {};

function readResultMap(): Map<string, Result> {
const resources = readCache().resources;
let map = resources.get(fetchKey);
if (map === undefined) {
map = new Map();
resources.set(fetchKey, map);
}
return map;

function getResultMap(): Map<string, Result> {
return unstable_getCacheForType(createResultMap);
}

function createResultMap(): Map<string, Result> {
return new Map();
}

function toResult(thenable): Result {
Expand Down Expand Up @@ -120,7 +117,7 @@ Response.prototype = {
};

function preloadResult(url: string, options: mixed): Result {
const map = readResultMap();
const map = getResultMap();
let entry = map.get(url);
if (!entry) {
if (options) {
Expand Down
19 changes: 7 additions & 12 deletions packages/react-fetch/src/ReactFetchNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import type {Wakeable} from 'shared/ReactTypes';

import * as http from 'http';
import * as https from 'https';

import {readCache} from 'react/unstable-cache';
import {unstable_getCacheForType} from 'react';

type FetchResponse = {|
// Properties
Expand Down Expand Up @@ -75,16 +74,12 @@ type RejectedResult = {|

type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;

const fetchKey = {};
function getResultMap(): Map<string, Result<FetchResponse>> {
return unstable_getCacheForType(createResultMap);
}

function readResultMap(): Map<string, Result<FetchResponse>> {
const resources = readCache().resources;
let map = resources.get(fetchKey);
if (map === undefined) {
map = new Map();
resources.set(fetchKey, map);
}
return map;
function createResultMap(): Map<string, Result<FetchResponse>> {
return new Map();
}

function readResult<T>(result: Result<T>): T {
Expand Down Expand Up @@ -166,7 +161,7 @@ Response.prototype = {
};

function preloadResult(url: string, options: mixed): Result<FetchResponse> {
const map = readResultMap();
const map = getResultMap();
let entry = map.get(url);
if (!entry) {
if (options) {
Expand Down
120 changes: 73 additions & 47 deletions packages/react-fetch/src/__tests__/ReactFetchNode-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,86 +10,112 @@
'use strict';

describe('ReactFetchNode', () => {
let ReactCache;
let ReactFetchNode;
let http;
let fetch;
let waitForSuspense;
let server;
let serverEndpoint;
let serverImpl;

beforeEach(done => {
jest.resetModules();
if (__EXPERIMENTAL__) {
ReactCache = require('react/unstable-cache');
// TODO: A way to pass load context.
ReactCache.CacheProvider._context._currentValue = ReactCache.createCache();
ReactFetchNode = require('react-fetch');
fetch = ReactFetchNode.fetch;
}

fetch = require('react-fetch').fetch;
http = require('http');
waitForSuspense = require('react-suspense-test-utils').waitForSuspense;

server = http.createServer((req, res) => {
serverImpl(req, res);
});
server.listen(done);
serverEndpoint = `http://localhost:${server.address().port}/`;
serverEndpoint = null;
server.listen(() => {
serverEndpoint = `http://localhost:${server.address().port}/`;
done();
});
});

afterEach(done => {
server.close(done);
server = null;
});

async function waitForSuspense(fn) {
while (true) {
try {
return fn();
} catch (promise) {
if (typeof promise.then === 'function') {
await promise;
} else {
throw promise;
}
}
}
}
// @gate experimental
it('can fetch text from a server component', async () => {
serverImpl = (req, res) => {
res.write('mango');
res.end();
};
const text = await waitForSuspense(() => {
return fetch(serverEndpoint).text();
});
expect(text).toEqual('mango');
});

// @gate experimental
it('can read text', async () => {
it('can fetch json from a server component', async () => {
serverImpl = (req, res) => {
res.write('ok');
res.write(JSON.stringify({name: 'Sema'}));
res.end();
};
await waitForSuspense(() => {
const response = fetch(serverEndpoint);
expect(response.status).toBe(200);
expect(response.statusText).toBe('OK');
expect(response.ok).toBe(true);
expect(response.text()).toEqual('ok');
// Can read again:
expect(response.text()).toEqual('ok');
const json = await waitForSuspense(() => {
return fetch(serverEndpoint).json();
});
expect(json).toEqual({name: 'Sema'});
});

// @gate experimental
it('can read json', async () => {
it('provides response status', async () => {
serverImpl = (req, res) => {
res.write(JSON.stringify({name: 'Sema'}));
res.end();
};
await waitForSuspense(() => {
const response = fetch(serverEndpoint);
expect(response.status).toBe(200);
expect(response.statusText).toBe('OK');
expect(response.ok).toBe(true);
expect(response.json()).toEqual({
name: 'Sema',
});
// Can read again:
expect(response.json()).toEqual({
name: 'Sema',
});
const response = await waitForSuspense(() => {
return fetch(serverEndpoint);
});
expect(response).toMatchObject({
status: 200,
statusText: 'OK',
ok: true,
});
});

// @gate experimental
it('handles different paths', async () => {
serverImpl = (req, res) => {
switch (req.url) {
case '/banana':
res.write('banana');
break;
case '/mango':
res.write('mango');
break;
case '/orange':
res.write('orange');
break;
}
res.end();
};
const outputs = await waitForSuspense(() => {
return [
fetch(serverEndpoint + 'banana').text(),
fetch(serverEndpoint + 'mango').text(),
fetch(serverEndpoint + 'orange').text(),
];
});
expect(outputs).toMatchObject(['banana', 'mango', 'orange']);
});

// @gate experimental
it('can produce an error', async () => {
serverImpl = (req, res) => {};

expect.assertions(1);
try {
await waitForSuspense(() => {
return fetch('BOOM');
});
} catch (err) {
expect(err.message).toEqual('Invalid URL: BOOM');
}
});
});
Loading