提交 a27c2352 编写于 作者: J JJ Kasper 提交者: Tim Neutkens

Update to share HMR and ondemand SSE connection (#7084)

We also close the connection when the window is in the background and re-connect when it is brought to the foreground. This prevents us from using up too many connections. 
上级 08937807
let hotDevCallback
function EventSourceWrapper (options) {
var source
var lastActivity = new Date()
......@@ -31,6 +33,9 @@ function EventSourceWrapper (options) {
for (var i = 0; i < listeners.length; i++) {
listeners[i](event)
}
if (hotDevCallback && event.data.indexOf('action') !== -1) {
hotDevCallback(event)
}
}
function handleDisconnect () {
......@@ -40,6 +45,10 @@ function EventSourceWrapper (options) {
}
return {
close: () => {
clearTimeout(timer)
source.close()
},
addMessageListener: function (fn) {
listeners.push(fn)
}
......@@ -47,13 +56,12 @@ function EventSourceWrapper (options) {
}
export function getEventSourceWrapper (options) {
if (!window.__whmEventSourceWrapper) {
window.__whmEventSourceWrapper = {}
}
if (!window.__whmEventSourceWrapper[options.path]) {
// cache the wrapper for other entries loaded on
// the same page with the same options.path
window.__whmEventSourceWrapper[options.path] = EventSourceWrapper(options)
if (!options.ondemand) {
return {
addMessageListener: cb => {
hotDevCallback = cb
}
}
}
return window.__whmEventSourceWrapper[options.path]
return EventSourceWrapper(options)
}
......@@ -5,10 +5,6 @@ import { setupPing, currentPage, closePing } from './on-demand-entries-utils'
export default async ({ assetPrefix }) => {
Router.ready(() => {
Router.events.on(
'routeChangeStart',
() => closePing()
)
Router.events.on(
'routeChangeComplete',
setupPing.bind(this, assetPrefix, () => Router.pathname)
......@@ -16,4 +12,13 @@ export default async ({ assetPrefix }) => {
})
setupPing(assetPrefix, () => Router.pathname, currentPage)
document.addEventListener('visibilitychange', event => {
const state = document.visibilityState
if (state === 'visible') {
setupPing(assetPrefix, () => Router.pathname, true)
} else {
closePing()
}
})
}
/* global window, location */
import fetch from 'unfetch'
import { getEventSourceWrapper } from './dev-error-overlay/eventsource'
let evtSource
export let currentPage
let retryTimeout
const retryWait = 5000
export function closePing () {
if (evtSource) evtSource.close()
evtSource = null
}
export function setupPing (assetPrefix, pathnameFn, retry) {
......@@ -20,21 +20,11 @@ export function setupPing (assetPrefix, pathnameFn, retry) {
// close current EventSource connection
closePing()
const url = `${assetPrefix}/_next/on-demand-entries-ping?page=${currentPage}`
evtSource = new window.EventSource(url)
const url = `${assetPrefix}/_next/webpack-hmr?page=${currentPage}`
evtSource = getEventSourceWrapper({ path: url, timeout: 5000, ondemand: 1 })
evtSource.onerror = () => {
retryTimeout = setTimeout(
() => setupPing(assetPrefix, pathnameFn, true),
retryWait
)
}
evtSource.onopen = () => {
clearTimeout(retryTimeout)
}
evtSource.onmessage = event => {
evtSource.addMessageListener(event => {
if (event.data.indexOf('{') === -1) return
try {
const payload = JSON.parse(event.data)
if (payload.invalid) {
......@@ -51,5 +41,5 @@ export function setupPing (assetPrefix, pathnameFn, retry) {
} catch (err) {
console.error('on-demand-entries failed to parse response', err)
}
}
})
}
......@@ -229,9 +229,10 @@ export default class HotReloader {
this.onDemandEntries = onDemandEntries
this.middlewares = [
webpackDevMiddleware,
// must come before hotMiddleware
onDemandEntries.middleware(),
webpackHotMiddleware,
errorOverlayMiddleware({ dir: this.dir }),
onDemandEntries.middleware()
errorOverlayMiddleware({ dir: this.dir })
]
}
......
......@@ -38,16 +38,6 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
serverRuntimeConfig
}) {
const pagesDir = join(dir, 'pages')
const clients = new Map()
const evtSourceHeaders = {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/event-stream;charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
// While behind nginx, event stream should not be buffered:
// http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
'X-Accel-Buffering': 'no',
'Connection': 'keep-alive'
}
const { compilers } = multiCompiler
const invalidator = new Invalidator(devMiddleware, multiCompiler)
let entries = {}
......@@ -158,7 +148,6 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
disposeHandler.unref()
function stop () {
clients.forEach((id, client) => client.end())
clearInterval(disposeHandler)
stopped = true
doneCallbacks = null
......@@ -307,35 +296,12 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
res.end('302')
})
} else {
if (!/^\/_next\/on-demand-entries-ping/.test(req.url)) return next()
if (!/^\/_next\/webpack-hmr/.test(req.url)) return next()
const { query } = parse(req.url, true)
const page = query.page
if (!page) return next()
// Upgrade request to EventSource
req.socket.setKeepAlive(true)
res.writeHead(200, evtSourceHeaders)
res.write('\n')
const startId = req.headers['user-agent'] + req.connection.remoteAddress
let clientId = startId
let numSameClient = 0
while (clients.has(clientId)) {
numSameClient++
clientId = startId + numSameClient
}
if (numSameClient > 1) {
// If the user has too many tabs with Next.js open in the same browser,
// they might be exceeding the max number of concurrent request.
// This varies per browser so we can only guess if this is the cause of
// a slow request and show a warning that this might be why
console.warn(`\nWarn: You are opening multiple tabs of the same site in the same browser, this could cause requests to stall. https://err.sh/zeit/next.js/multi-tabs`)
}
clients.set(clientId, res)
const runPing = () => {
const data = handlePing(query.page)
if (!data) return
......@@ -344,11 +310,11 @@ export default function onDemandEntryHandler (devMiddleware, multiCompiler, {
const pingInterval = setInterval(() => runPing(), 5000)
req.on('close', () => {
clients.delete(clientId)
clearInterval(pingInterval)
})
// Do initial ping right after EventSource is finished being set up
runPing()
setImmediate(() => runPing())
next()
}
}
}
......
......@@ -20,7 +20,7 @@ const context = {}
const doPing = page => {
const controller = new AbortController()
const signal = controller.signal
return fetchViaHTTP(context.appPort, '/_next/on-demand-entries-ping', { page }, { signal })
return fetchViaHTTP(context.appPort, '/_next/webpack-hmr', { page }, { signal })
.then(res => {
res.body.on('data', chunk => {
try {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册