mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
We use the stack of a Promise as the start of the I/O instead of the actual I/O since that can symbolize the start of the operation even if the actual I/O is batched, deduped or pooled. It can also group multiple I/O operations into one. We want the deepest possible Promise since otherwise it would just be the Component's Promise. However, we don't really need deeper than the boundary between first party and third party. We can't just take the outer most that has third party things on the stack though because third party can have callbacks into first party and then we want the inner one. So we take the inner most Promise that depends on I/O that has a first party stack on it. The realization is that for the purposes of determining whether we have a first party stack we need to ignore async stack frames. They can appear on the stack when we resume third party code inside a resumption frame of a first party stack. <img width="832" alt="Screenshot 2025-07-08 at 6 34 25 PM" src="https://github.com/user-attachments/assets/1636f980-be4c-4340-ad49-8d2b31953436" /> --------- Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
241 lines
5.8 KiB
JavaScript
241 lines
5.8 KiB
JavaScript
import * as React from 'react';
|
|
import {renderToReadableStream} from 'react-server-dom-webpack/server';
|
|
import {createFromReadableStream} from 'react-server-dom-webpack/client';
|
|
import {PassThrough, Readable} from 'stream';
|
|
|
|
import Container from './Container.js';
|
|
|
|
import {Counter} from './Counter.js';
|
|
import {Counter as Counter2} from './Counter2.js';
|
|
import AsyncModule from './cjs/Counter3.js';
|
|
const Counter3 = await(AsyncModule);
|
|
|
|
import ShowMore from './ShowMore.js';
|
|
import Button from './Button.js';
|
|
import Form from './Form.js';
|
|
import {Dynamic} from './Dynamic.js';
|
|
import {Client} from './Client.js';
|
|
import {Navigate} from './Navigate.js';
|
|
|
|
import {Note} from './cjs/Note.js';
|
|
|
|
import {GenerateImage} from './GenerateImage.js';
|
|
|
|
import {like, greet, increment} from './actions.js';
|
|
|
|
import {getServerState} from './ServerState.js';
|
|
import {sdkMethod} from './library.js';
|
|
|
|
const promisedText = new Promise(resolve =>
|
|
setTimeout(() => resolve('deferred text'), 50)
|
|
);
|
|
|
|
function Foo({children}) {
|
|
return <div>{children}</div>;
|
|
}
|
|
|
|
async function delayedError(text, ms) {
|
|
return new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error(text)), ms)
|
|
);
|
|
}
|
|
|
|
async function delay(text, ms) {
|
|
return new Promise(resolve => setTimeout(() => resolve(text), ms));
|
|
}
|
|
|
|
async function delayTwice() {
|
|
try {
|
|
await delayedError('Delayed exception', 20);
|
|
} catch (x) {
|
|
// Ignored
|
|
}
|
|
await delay('', 10);
|
|
}
|
|
|
|
async function delayTrice() {
|
|
const p = delayTwice();
|
|
await delay('', 40);
|
|
return p;
|
|
}
|
|
|
|
async function Bar({children}) {
|
|
await delayTrice();
|
|
return <div>{children}</div>;
|
|
}
|
|
|
|
async function ThirdPartyComponent() {
|
|
return await delay('hello from a 3rd party', 30);
|
|
}
|
|
|
|
let cachedThirdPartyStream;
|
|
|
|
// We create the Component outside of AsyncLocalStorage so that it has no owner.
|
|
// That way it gets the owner from the call to createFromNodeStream.
|
|
const thirdPartyComponent = <ThirdPartyComponent />;
|
|
|
|
function simulateFetch(cb, latencyMs) {
|
|
return new Promise(resolve => {
|
|
// Request latency
|
|
setTimeout(() => {
|
|
const result = cb();
|
|
// Response latency
|
|
setTimeout(() => {
|
|
resolve(result);
|
|
}, latencyMs);
|
|
}, latencyMs);
|
|
});
|
|
}
|
|
|
|
async function fetchThirdParty(noCache) {
|
|
// We're using the Web Streams APIs for tee'ing convenience.
|
|
let stream;
|
|
if (cachedThirdPartyStream && !noCache) {
|
|
stream = cachedThirdPartyStream;
|
|
} else {
|
|
stream = await simulateFetch(
|
|
() =>
|
|
renderToReadableStream(
|
|
thirdPartyComponent,
|
|
{},
|
|
{environmentName: 'third-party'}
|
|
),
|
|
25
|
|
);
|
|
}
|
|
|
|
const [stream1, stream2] = stream.tee();
|
|
cachedThirdPartyStream = stream1;
|
|
|
|
return createFromReadableStream(stream2, {
|
|
serverConsumerManifest: {
|
|
moduleMap: {},
|
|
serverModuleMap: null,
|
|
moduleLoading: null,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function ServerComponent({noCache}) {
|
|
await delay('deferred text', 50);
|
|
return await fetchThirdParty(noCache);
|
|
}
|
|
|
|
let veryDeepObject = [
|
|
{
|
|
bar: {
|
|
baz: {
|
|
a: {},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
bar: {
|
|
baz: {
|
|
a: {},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
bar: {
|
|
baz: {
|
|
a: {},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
bar: {
|
|
baz: {
|
|
a: {
|
|
b: {
|
|
c: {
|
|
d: {
|
|
e: {
|
|
f: {
|
|
g: {
|
|
h: {
|
|
i: {
|
|
j: {
|
|
k: {
|
|
l: {
|
|
m: {
|
|
yay: 'You reached the end',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
export default async function App({prerender, noCache}) {
|
|
const res = await fetch('http://localhost:3001/todos');
|
|
const todos = await res.json();
|
|
await sdkMethod('http://localhost:3001/todos');
|
|
|
|
console.log('Expand me:', veryDeepObject);
|
|
|
|
const dedupedChild = <ServerComponent noCache={noCache} />;
|
|
const message = getServerState();
|
|
return (
|
|
<html lang="en">
|
|
<head>
|
|
<meta charSet="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Flight</title>
|
|
</head>
|
|
<body>
|
|
<Container>
|
|
{prerender ? (
|
|
<meta data-testid="prerendered" name="prerendered" content="true" />
|
|
) : (
|
|
<meta content="when not prerendering we render this meta tag. When prerendering you will expect to see this tag and the one with data-testid=prerendered because we SSR one and hydrate the other" />
|
|
)}
|
|
<h1>{message}</h1>
|
|
<React.Suspense fallback={null}>
|
|
<div data-testid="promise-as-a-child-test">
|
|
Promise as a child hydrates without errors: {promisedText}
|
|
</div>
|
|
</React.Suspense>
|
|
<Counter incrementAction={increment} />
|
|
<Counter2 incrementAction={increment} />
|
|
<Counter3 incrementAction={increment} />
|
|
<ul>
|
|
{todos.map(todo => (
|
|
<li key={todo.id}>{todo.text}</li>
|
|
))}
|
|
</ul>
|
|
<ShowMore>
|
|
<p>Lorem ipsum</p>
|
|
</ShowMore>
|
|
<Form action={greet} />
|
|
<div>
|
|
<Button action={like}>Like</Button>
|
|
</div>
|
|
<div>
|
|
loaded statically: <Dynamic />
|
|
</div>
|
|
<div>
|
|
<GenerateImage message={message} />
|
|
</div>
|
|
<Client />
|
|
<Note />
|
|
<Foo>{dedupedChild}</Foo>
|
|
<Bar>{Promise.resolve([dedupedChild])}</Bar>
|
|
<Navigate />
|
|
</Container>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|