7676
return <span>hi</span>;
7677
}
7678
7679
- // Recursively render a component tree deep enough to trigger stack overflow.
7680
- // Don't make this too short to not hit the limit but also not too deep to slow
7681
- // down the test.
7679
+ // Recursively render a component tree deep enough to trigger stack overflow
7680
+ // more than once. The first overflow is recovered by the renderNode
7681
+ // trampoline; deeper trees must also recover when the retried task
7682
+ // overflows again. Don't make this too deep to slow down the test.
7683
await act(() => {
7684
const {pipe} = renderToPipeableStream(
7685
<div>
7685
- <Recursive n={1000} />
7686
+ <Recursive n={1200} />
7687
</div>,
7688
);
7689
pipe(writable);
7730
expect(caughtError.message).toBe('Maximum call stack size exceeded');
7731
});
7732
7733
+ it('can recover from very deep trees during resume to avoid stack overflow', async () => {
7734
+ const promise = new Promise(() => {});
7735
+
7736
+ let prerendering = true;
7737
+
7738
+ // Deep wrappers above the postponed boundary. On resume, replaying this
7739
+ // path goes through retryReplayTask → retryNode (no trampoline), so a
7740
+ // tree deep enough to overflow must recover there — not only on the
7741
+ // ordinary render retry path.
7742
+ function Deep({n, children}) {
7743
+ if (n > 0) {
7744
+ return <Deep n={n - 1}>{children}</Deep>;
7745
+ }
7746
+ return children;
7747
+ }
7748
+
7749
+ function Content() {
7750
+ if (prerendering) {
7751
+ return React.use(promise);
7752
+ }
7753
+ return <span>hi</span>;
7754
+ }
7755
+
7756
+ function App() {
7757
+ return (
7758
+ <div>
7759
+ <Deep n={1200}>
7760
+ <Suspense fallback="Loading...">
7761
+ <Content />
7762
+ </Suspense>
7763
+ </Deep>
7764
+ </div>
7765
+ );
7766
+ }
7767
+
7768
+ const controller = new AbortController();
7769
+ const errors = [];
7770
+ let pendingPrerender;
7771
+ await act(() => {
7772
+ pendingPrerender = ReactDOMFizzStatic.prerenderToNodeStream(<App />, {
7773
+ signal: controller.signal,
7774
+ onError(error) {
7775
+ errors.push(error);
7776
+ },
7777
+ });
7778
+ });
7779
+ controller.abort('abort');
7780
+
7781
+ const prerendered = await pendingPrerender;
7782
+ expect(errors).toEqual(['abort']);
7783
+ expect(prerendered.postponed).not.toBe(null);
7784
+
7785
+ const preludeWritable = new Stream.PassThrough();
7786
+ preludeWritable.setEncoding('utf8');
7787
+ preludeWritable.on('data', chunk => {
7788
+ writable.write(chunk);
7789
+ });
7790
+
7791
+ await act(() => {
7792
+ prerendered.prelude.pipe(preludeWritable);
7793
+ });
7794
+ expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
7795
+
7796
+ prerendering = false;
7797
+ errors.length = 0;
7798
+
7799
+ const resumed = await ReactDOMFizzServer.resumeToPipeableStream(
7800
+ <App />,
7801
+ JSON.parse(JSON.stringify(prerendered.postponed)),
7802
+ {
7803
+ onError(error) {
7804
+ errors.push(error);
7805
+ },
7806
+ },
7807
+ );
7808
+
7809
+ await act(() => {
7810
+ resumed.pipe(writable);
7811
+ });
7812
+
7813
+ expect(errors).toEqual([]);
7814
+ expect(getVisibleChildren(container)).toEqual(
7815
+ <div>
7816
+ <span>hi</span>
7817
+ </div>,
7818
+ );
7819
+ });
7820
+
7821
it('client renders incomplete Suspense boundaries when the document is no longer loading when hydration begins', async () => {
7822
let resolve;
7823
const promise = new Promise(r => {