Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/nuxt-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"exports": {
".": "./dist/index.mjs",
"./cli": "./bin/nuxi.mjs",
"./runtime/dev-close-sockets": "./runtime/dev-close-sockets.mjs",
"./runtime/dev-request-context": "./runtime/dev-request-context.mjs"
},
"types": "./dist/index.d.ts",
Expand Down
22 changes: 22 additions & 0 deletions packages/nuxt-cli/runtime/dev-close-sockets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { subscribe, unsubscribe } from 'node:diagnostics_channel'

/**
* Destroy sockets left open in the Nitro dev worker on shutdown, including
* upgraded ones that `server.closeAllConnections()` skips.
*
* TODO: remove once https://github.com/nitrojs/nitro/pull/4671 is released.
*/
export default function (nitroApp) {
const sockets = new Set()
const track = ({ socket }) => {
sockets.add(socket)
socket.once('close', () => sockets.delete(socket))
}
subscribe('net.server.socket', track)
nitroApp.hooks.hook('close', () => {
unsubscribe('net.server.socket', track)
for (const socket of sockets) {
socket.destroy()
}
})
}
29 changes: 19 additions & 10 deletions packages/nuxt-cli/src/dev/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ function registerRequestContextPlugin(nitro: NitroConfigForHook, cwd: string): v
}
}

function registerCloseSocketsPlugin(nitro: NitroConfigForHook): void {
try {
nitro.plugins ||= []
nitro.plugins.push(fileURLToPath(import.meta.resolve('@nuxt/cli/runtime/dev-close-sockets')))
}
catch (error) {
debug('Could not resolve the close sockets plugin:', error)
}
}

/**
* The `consola` the app itself logs through, which is the one
* `@nuxt/nitro-server` wraps `console` with: its own, not the CLI's. Reporting
Expand Down Expand Up @@ -889,17 +899,16 @@ export class NuxtDevServer extends EventEmitter<DevServerEventMap> {
clearScreen: this.options.clear,
...this.options.overrides.vite,
},
...captureUIEvents
? {
hooks: {
...this.options.overrides.hooks,
'nitro:config': (nitro) => {
registerRequestContextPlugin(nitro, this.options.cwd)
return this.options.overrides.hooks?.['nitro:config']?.(nitro)
},
} satisfies NuxtConfig['hooks'],
hooks: {
...this.options.overrides.hooks,
'nitro:config': (nitro) => {
registerCloseSocketsPlugin(nitro)
if (captureUIEvents) {
registerRequestContextPlugin(nitro, this.options.cwd)
}
: {},
return this.options.overrides.hooks?.['nitro:config']?.(nitro)
},
} satisfies NuxtConfig['hooks'],
},
}

Expand Down
55 changes: 55 additions & 0 deletions packages/nuxt-cli/test/e2e/dev-websocket-shutdown.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { spawn } from 'node:child_process'
import { once } from 'node:events'
import { mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { getPort } from 'get-port-please'
import { describe, expect, it } from 'vitest'
import { createDevFixture } from '../utils'

const nuxi = fileURLToPath(new URL('../../bin/nuxi.mjs', import.meta.url))
const fixtureDir = await createDevFixture('dev-websocket-shutdown')
await writeFile(join(fixtureDir, 'nuxt.config.ts'), 'export default defineNuxtConfig({ nitro: { experimental: { websocket: true } } })\n')
await mkdir(join(fixtureDir, 'server/routes'), { recursive: true })
await writeFile(join(fixtureDir, 'server/routes/_ws.ts'), 'export default defineWebSocketHandler({ message(peer, message) { peer.send(message.text()) } })\n')

describe('dev server shutdown', () => {
it('should exit promptly with an open websocket connection', { timeout: 120_000 }, async () => {
const host = '127.0.0.1'
const port = await getPort({ host, port: 3060 })
// nitropack skips its graceful worker shutdown under test and CI
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !['CI', 'TEST', 'VITEST', 'NODE_ENV', 'GITHUB_ACTIONS'].includes(key)))
const child = spawn(process.execPath, [nuxi, 'dev', '--no-fork', '--host', host, '--port', String(port)], {
cwd: fixtureDir,
env: { ...env, NO_COLOR: '1' },
stdio: ['ignore', 'pipe', 'pipe'],
})
let output = ''
const append = (data: string) => {
output += data
}
child.stdout.on('data', append)
child.stderr.on('data', append)

try {
await expect.poll(() => output, { timeout: 90_000, interval: 250 }).toContain('Ready in')

const ws = new WebSocket(`ws://${host}:${port}/_ws`)
const echo = await new Promise<string>((resolve, reject) => {
ws.addEventListener('open', () => ws.send('ping'))
ws.addEventListener('message', event => resolve(String(event.data)))
ws.addEventListener('error', reject)
})
expect(echo).toBe('ping')

const start = performance.now()
child.kill('SIGINT')
await once(child, 'exit')
expect(performance.now() - start).toBeLessThan(5000)
}
finally {
child.kill('SIGKILL')
}
})
})
Loading