Don't close the queue while a watcher swap is in flight
A rebuild spliced the old cleanups out of `cleanupWatchers` and only pushed the new generation back after awaiting them. If stdin closed inside that window, shutdown saw an empty list, resolved immediately and closed the queue — so the files the old generation flushed on its way out were pushed into a closed queue and ignored, and the process exited 0 with stale CSS. Register the new generation before awaiting the old one, so the list is never empty, and track the in-flight swap so shutdown waits for it before closing. The ordering now lives in `shutdownWatchMode` so it can be asserted directly: the queue must still be open while a swap is flushing. Against the previous ordering that assertion fails — the queue is already closed and the flush has not landed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lcj4iQ3fBxMwAu2rf4zLbC
This commit is contained in:
parent
d346922c11
commit
0bbf270efc
2 changed files with 58 additions and 11 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { expect, it } from 'vitest'
|
||||
import { serializeBatches } from '../../utils/serial-batches'
|
||||
import { createWatchers, filterChangedFiles } from './index'
|
||||
import { createWatchers, filterChangedFiles, shutdownWatchMode } from './index'
|
||||
|
||||
type WatchEvent = { type: 'create' | 'update' | 'delete'; path: string }
|
||||
type WatchCallback = (error: Error | null, events: WatchEvent[]) => Promise<void>
|
||||
|
|
@ -121,3 +121,29 @@ it('writes the newest change last when an earlier rebuild is slower', async () =
|
|||
|
||||
expect(written).toEqual(['older-change', 'newer-change'])
|
||||
})
|
||||
|
||||
it('does not close the queue while a watcher swap is still flushing', async () => {
|
||||
// A rebuild swaps the watcher generation, and the old generation flushes what
|
||||
// it collected as it is torn down. If shutdown closes the queue first, those
|
||||
// files land in a closed queue and the process exits with stale CSS.
|
||||
let closed = false
|
||||
let flushed: string[] = []
|
||||
let finishSwap!: () => void
|
||||
let swap = new Promise<void>((resolve) => (finishSwap = resolve)).then(() => {
|
||||
flushed.push('collected-during-swap')
|
||||
})
|
||||
|
||||
let shutdown = shutdownWatchMode(swap, [], {
|
||||
async close() {
|
||||
closed = true
|
||||
},
|
||||
})
|
||||
await nextTask()
|
||||
expect(closed).toBe(false)
|
||||
|
||||
finishSwap()
|
||||
await shutdown
|
||||
|
||||
expect(flushed).toEqual(['collected-during-swap'])
|
||||
expect(closed).toBe(true)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -327,6 +327,10 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
|
||||
let [compiler, scanner] = await handleError(() => createCompiler(input, I))
|
||||
let cleanupWatchers: (() => Promise<void>)[] = []
|
||||
// A rebuild swaps the watcher generation. Shutdown must not close the queue
|
||||
// while that swap is in flight, or the files the old generation flushes are
|
||||
// pushed into a closed queue and silently dropped.
|
||||
let watcherSwap: Promise<unknown> = Promise.resolve()
|
||||
let finishInitialBuild!: () => void
|
||||
let initialBuildFinished = new Promise<void>((resolve) => (finishInitialBuild = resolve))
|
||||
let eventBatches: SerialBatches<string> | null = null
|
||||
|
|
@ -409,12 +413,14 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
)
|
||||
DEBUG && I.end('Setup new watchers')
|
||||
|
||||
// Clear old watchers
|
||||
// Clear old watchers. Register the new generation *before* awaiting
|
||||
// the old one, so shutdown never observes an empty cleanup list.
|
||||
DEBUG && I.start('Cleanup old watchers')
|
||||
await Promise.all(cleanupWatchers.splice(0).map((cleanup) => cleanup()))
|
||||
DEBUG && I.end('Cleanup old watchers')
|
||||
|
||||
let previousCleanups = cleanupWatchers.splice(0)
|
||||
cleanupWatchers.push(newWatchers.cleanup)
|
||||
watcherSwap = Promise.all(previousCleanups.map((cleanup) => cleanup()))
|
||||
await watcherSwap
|
||||
DEBUG && I.end('Cleanup old watchers')
|
||||
|
||||
// Re-compile the CSS
|
||||
DEBUG && I.start('Build CSS')
|
||||
|
|
@ -509,12 +515,10 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
// disable this behavior with `--watch=always`.
|
||||
if (args['--watch'] !== 'always') {
|
||||
process.stdin.on('end', () => {
|
||||
Promise.all(cleanupWatchers.map((fn) => fn()))
|
||||
.then(() => eventBatches?.close())
|
||||
.then(
|
||||
() => process.exit(0),
|
||||
() => process.exit(1),
|
||||
)
|
||||
shutdownWatchMode(watcherSwap, cleanupWatchers, eventBatches).then(
|
||||
() => process.exit(0),
|
||||
() => process.exit(1),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -706,6 +710,23 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
|
|||
// Load `@parcel/watcher` lazily so a missing or broken native binding only
|
||||
// affects `--watch` (without `--poll`), instead of crashing one-off builds and
|
||||
// polling mode as well.
|
||||
/// Shut watch mode down in an order that cannot drop collected files.
|
||||
///
|
||||
/// A rebuild swaps the watcher generation, and the old generation flushes what it
|
||||
/// collected as it is torn down. Closing the queue before that flush lands means the
|
||||
/// files are pushed into a closed queue and ignored, so the process exits successfully
|
||||
/// with stale CSS. Wait for an in-flight swap first, then the current generation, and
|
||||
/// only then close.
|
||||
export async function shutdownWatchMode(
|
||||
watcherSwap: Promise<unknown>,
|
||||
cleanups: (() => Promise<void>)[],
|
||||
batches: { close(): Promise<void> } | null,
|
||||
) {
|
||||
await watcherSwap
|
||||
await Promise.all(cleanups.map((cleanup) => cleanup()))
|
||||
await batches?.close()
|
||||
}
|
||||
|
||||
async function loadWatcher(): Promise<typeof import('@parcel/watcher')> {
|
||||
try {
|
||||
return (await import('@parcel/watcher')).default
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue