Ensure timed out prefetches are cleaned up correctly (#28899)
This applies the fix from the awesome investigation done in https://github.com/vercel/next.js/issues/28797 by @jayphelps and adds a test to ensure this is working as expected. It seems that the `route-loader` has a race condition while prefetching and if a script is executed before we have created a current "future" entry to resolve the entry stays in a pending state causing routes to hang so this handles the condition by ensuring pending/errored entries do not stay around. ## Bug - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [x] Errors have helpful link attached, see `contributing.md` Fixes: https://github.com/vercel/next.js/issues/28797 Fixes: https://github.com/vercel/next.js/issues/27783
This commit is contained in:
parent
4f8d883acd
commit
b71df190e5
4 changed files with 63 additions and 18 deletions
|
@ -60,8 +60,13 @@ function withFuture<T>(
|
||||||
})
|
})
|
||||||
map.set(key, (entry = { resolve: resolver!, future: prom }))
|
map.set(key, (entry = { resolve: resolver!, future: prom }))
|
||||||
return generator
|
return generator
|
||||||
? // eslint-disable-next-line no-sequences
|
? generator()
|
||||||
generator().then((value) => (resolver(value), value))
|
// eslint-disable-next-line no-sequences
|
||||||
|
.then((value) => (resolver(value), value))
|
||||||
|
.catch((err) => {
|
||||||
|
map.delete(key)
|
||||||
|
throw err
|
||||||
|
})
|
||||||
: prom
|
: prom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export default () => <p>Hello world</p>
|
export default () => <p id="another">Hello world</p>
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
/* eslint-env jest */
|
/* eslint-env jest */
|
||||||
|
|
||||||
import {
|
import {
|
||||||
nextServer,
|
findPort,
|
||||||
runNextCommand,
|
killApp,
|
||||||
startApp,
|
nextBuild,
|
||||||
stopApp,
|
nextStart,
|
||||||
waitFor,
|
waitFor,
|
||||||
} from 'next-test-utils'
|
} from 'next-test-utils'
|
||||||
|
import http from 'http'
|
||||||
|
import httpProxy from 'http-proxy'
|
||||||
import webdriver from 'next-webdriver'
|
import webdriver from 'next-webdriver'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { readFile } from 'fs-extra'
|
import { readFile } from 'fs-extra'
|
||||||
|
@ -14,24 +16,62 @@ import { readFile } from 'fs-extra'
|
||||||
jest.setTimeout(1000 * 60 * 5)
|
jest.setTimeout(1000 * 60 * 5)
|
||||||
|
|
||||||
const appDir = join(__dirname, '../')
|
const appDir = join(__dirname, '../')
|
||||||
let appPort
|
|
||||||
let server
|
|
||||||
let app
|
let app
|
||||||
|
let appPort
|
||||||
|
let stallJs
|
||||||
|
let proxyServer
|
||||||
|
|
||||||
describe('Prefetching Links in viewport', () => {
|
describe('Prefetching Links in viewport', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await runNextCommand(['build', appDir])
|
await nextBuild(appDir)
|
||||||
|
const port = await findPort()
|
||||||
|
app = await nextStart(appDir, port)
|
||||||
|
appPort = await findPort()
|
||||||
|
|
||||||
app = nextServer({
|
const proxy = httpProxy.createProxyServer({
|
||||||
dir: join(__dirname, '../'),
|
target: `http://localhost:${port}`,
|
||||||
dev: false,
|
|
||||||
quiet: true,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
server = await startApp(app)
|
proxyServer = http.createServer(async (req, res) => {
|
||||||
appPort = server.address().port
|
if (stallJs && req.url.includes('chunks/pages/another')) {
|
||||||
|
console.log('stalling request for', req.url)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5 * 1000))
|
||||||
|
}
|
||||||
|
proxy.web(req, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
proxy.on('error', (err) => {
|
||||||
|
console.warn('Failed to proxy', err)
|
||||||
|
})
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
proxyServer.listen(appPort, () => resolve())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
afterAll(async () => {
|
||||||
|
await killApp(app)
|
||||||
|
proxyServer.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle timed out prefetch correctly', async () => {
|
||||||
|
try {
|
||||||
|
stallJs = true
|
||||||
|
const browser = await webdriver(appPort, '/')
|
||||||
|
|
||||||
|
await browser.elementByCss('#scroll-to-another').click()
|
||||||
|
// wait for preload to timeout
|
||||||
|
await waitFor(6 * 1000)
|
||||||
|
|
||||||
|
await browser
|
||||||
|
.elementByCss('#link-another')
|
||||||
|
.click()
|
||||||
|
.waitForElementByCss('#another')
|
||||||
|
|
||||||
|
expect(await browser.elementByCss('#another').text()).toBe('Hello world')
|
||||||
|
} finally {
|
||||||
|
stallJs = false
|
||||||
|
}
|
||||||
})
|
})
|
||||||
afterAll(() => stopApp(server))
|
|
||||||
|
|
||||||
it('should prefetch with link in viewport onload', async () => {
|
it('should prefetch with link in viewport onload', async () => {
|
||||||
let browser
|
let browser
|
||||||
|
@ -280,6 +320,7 @@ describe('Prefetching Links in viewport', () => {
|
||||||
|
|
||||||
expect(await browser.eval('window.hadUnhandledReject')).toBeFalsy()
|
expect(await browser.eval('window.hadUnhandledReject')).toBeFalsy()
|
||||||
|
|
||||||
|
await browser.waitForElementByCss('#invalid-link')
|
||||||
await browser.elementByCss('#invalid-link').moveTo()
|
await browser.elementByCss('#invalid-link').moveTo()
|
||||||
expect(await browser.eval('window.hadUnhandledReject')).toBeFalsy()
|
expect(await browser.eval('window.hadUnhandledReject')).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,7 +20,6 @@ const appDir = join(__dirname, '../')
|
||||||
function runTests() {
|
function runTests() {
|
||||||
it('should cancel slow page loads on re-navigation', async () => {
|
it('should cancel slow page loads on re-navigation', async () => {
|
||||||
const browser = await webdriver(appPort, '/')
|
const browser = await webdriver(appPort, '/')
|
||||||
await waitFor(5000)
|
|
||||||
|
|
||||||
await browser.elementByCss('#link-1').click()
|
await browser.elementByCss('#link-1').click()
|
||||||
await waitFor(3000)
|
await waitFor(3000)
|
||||||
|
|
Loading…
Reference in a new issue