1583
}
1584
});
1585
1586
+ it('should use late-arriving I/O debug info from rejected server promises to enhance component and owner stacks when aborting a prerender', async () => {
1587
+ let rejectHangingPromise;
1588
+
1589
+ async function makeHangingPromise() {
1590
+ return new Promise((resolve, reject) => {
1591
+ rejectHangingPromise = reject;
1592
+ });
1593
+ }
1594
+
1595
+ async function getRoot() {
1596
+ return {promise: makeHangingPromise()};
1597
+ }
1598
+
1599
+ let staticEndTime = -1;
1600
+ const staticChunks = [];
1601
+ const dynamicChunks = [];
1602
+
1603
+ const serverAbortController = new AbortController();
1604
+ await new Promise(resolve => {
1605
+ setTimeout(async () => {
1606
+ const stream = ReactServerDOMServer.renderToPipeableStream(
1607
+ getRoot(),
1608
+ webpackMap,
1609
+ {
1610
+ filterStackFrame,
1611
+ onError(err) {
1612
+ if (serverAbortController.signal.aborted) {
1613
+ return;
1614
+ }
1615
+ console.error(err);
1616
+ },
1617
+ },
1618
+ );
1619
+ serverAbortController.signal.addEventListener(
1620
+ 'abort',
1621
+ () => {
1622
+ stream.abort(serverAbortController.signal.reason);
1623
+
1624
+ // Only reject the promise after the render is aborted
1625
+ // so that it's no longer observable
1626
+ rejectHangingPromise(
1627
+ new Error(
1628
+ 'Hanging promise was rejected after the prerender finished',
1629
+ ),
1630
+ );
1631
+ },
1632
+ {once: true},
1633
+ );
1634
+
1635
+ const passThrough = new Stream.PassThrough(streamOptions);
1636
+ stream.pipe(passThrough);
1637
+
1638
+ passThrough.on('data', chunk => {
1639
+ if (staticEndTime < 0) {
1640
+ staticChunks.push(chunk);
1641
+ } else {
1642
+ dynamicChunks.push(chunk);
1643
+ }
1644
+ });
1645
+
1646
+ passThrough.on('end', resolve);
1647
+ });
1648
+ setTimeout(() => {
1649
+ staticEndTime = performance.now() + performance.timeOrigin;
1650
+ serverAbortController.abort();
1651
+ });
1652
+ });
1653
+
1654
+ const clientAbortController = new AbortController();
1655
+
1656
+ const serverStream = createReadableWithLateRelease(
1657
+ staticChunks,
1658
+ dynamicChunks,
1659
+ clientAbortController.signal,
1660
+ );
1661
+
1662
+ const response = await ReactServerDOMClient.createFromNodeStream(
1663
+ serverStream,
1664
+ {
1665
+ serverConsumerManifest: {
1666
+ moduleMap: null,
1667
+ moduleLoading: null,
1668
+ },
1669
+ },
1670
+ {
1671
+ // Debug info arriving after this end time will be ignored, e.g. the
1672
+ // I/O info for the second dynamic data.
1673
+ endTime: staticEndTime,
1674
+ },
1675
+ );
1676
+
1677
+ const resolvedPromise = Promise.resolve('hello');
1678
+ function ClientDynamic() {
1679
+ use(resolvedPromise);
1680
+ use(response.promise); // unresolved ReactPromise (becomes rejected when we abort)
1681
+ }
1682
+
1683
+ function ClientRoot() {
1684
+ return React.createElement(
1685
+ 'html',
1686
+ null,
1687
+ React.createElement(
1688
+ 'body',
1689
+ null,
1690
+ React.createElement(
1691
+ React.Suspense,
1692
+ {fallback: 'Loading...'},
1693
+ React.createElement(ClientDynamic),
1694
+ ),
1695
+ ),
1696
+ );
1697
+ }
1698
+
1699
+ let ownerStack;
1700
+ let componentStack;
1701
+
1702
+ const {prelude} = await new Promise(resolve => {
1703
+ let result;
1704
+
1705
+ setTimeout(() => {
1706
+ result = ReactDOMFizzStatic.prerenderToNodeStream(
1707
+ React.createElement(ClientRoot),
1708
+ {
1709
+ signal: clientAbortController.signal,
1710
+ onError(error, errorInfo) {
1711
+ componentStack = errorInfo.componentStack;
1712
+ ownerStack = React.captureOwnerStack
1713
+ ? React.captureOwnerStack()
1714
+ : null;
1715
+ },
1716
+ },
1717
+ );
1718
+ });
1719
+
1720
+ setTimeout(() => {
1721
+ clientAbortController.abort();
1722
+ resolve(result);
1723
+ });
1724
+ });
1725
+
1726
+ const prerenderHTML = await readResult(prelude);
1727
+
1728
+ expect(prerenderHTML).toContain('Loading...');
1729
+
1730
+ if (__DEV__) {
1731
+ expect(
1732
+ normalizeCodeLocInfo(componentStack, {preserveLocation: true}),
1733
+ ).toBe(
1734
+ '\n' +
1735
+ ' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n' +
1736
+ ' in Suspense\n' +
1737
+ ' in body\n' +
1738
+ ' in html\n' +
1739
+ ' in ClientRoot',
1740
+ );
1741
+ } else {
1742
+ expect(
1743
+ normalizeCodeLocInfo(componentStack, {preserveLocation: true}),
1744
+ ).toBe(
1745
+ '\n' +
1746
+ ' in ClientDynamic (ReactFlightDOMNode-test.js:1679:9)\n' +
1747
+ ' in Suspense\n' +
1748
+ ' in body\n' +
1749
+ ' in html\n' +
1750
+ ' in ClientRoot',
1751
+ );
1752
+ }
1753
+
1754
+ if (__DEV__) {
1755
+ expect(ignoreListStack(ownerStack)).toBe(
1756
+ '\n' +
1757
+ gate(flags =>
1758
+ flags.enableAsyncDebugInfo
1759
+ ? ' at ClientDynamic (./ReactFlightDOMNode-test.js:1680:9)\n'
1760
+ : '',
1761
+ ) +
1762
+ ' at ClientRoot (./ReactFlightDOMNode-test.js:1693:21)',
1763
+ );
1764
+ } else {
1765
+ expect(ownerStack).toBeNull();
1766
+ }
1767
+ });
1768
+
1769
function createReadableWithLateRelease(initialChunks, lateChunks, signal) {
1770
// Create a new Readable and push all initial chunks immediately.
1771
const readable = new Stream.Readable({...streamOptions, read() {}});