Skip to content

Commit af748b1

Browse files
committed
fix: debounce onSuccess in watch mode to prevent duplicate execution
closes #763
1 parent d30c59b commit af748b1

File tree

5 files changed

+31
-4
lines changed

5 files changed

+31
-4
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ docs/.vitepress/cache
1717

1818
docs/reference/api
1919
docs/zh-CN/reference/api
20+
21+
reproduction

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default sxzz(
99
},
1010
},
1111
{
12-
ignores: ['skills/**/*.md'],
12+
ignores: ['skills/**/*.md', 'reproduction/**'],
1313
},
1414
{
1515
files: ['templates/**'],

pnpm-lock.yaml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/build.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
type RolldownChunk,
3030
type TsdownBundle,
3131
} from './utils/chunks.ts'
32-
import { typeAssert } from './utils/general.ts'
32+
import { debounce, typeAssert } from './utils/general.ts'
3333
import { globalLogger } from './utils/logger.ts'
3434

3535
const asyncDispose: typeof Symbol.asyncDispose =
@@ -146,12 +146,16 @@ async function buildSingle(
146146
const chunks: RolldownChunk[] = []
147147
let watcher: RolldownWatcher | undefined
148148
let ab: AbortController | undefined
149+
const debouncedPostBuild = debounce(() => {
150+
postBuild().catch((error) => logger.error(error))
151+
}, 100)
149152

150153
let updated = false
151154
const bundle: TsdownBundle = {
152155
chunks,
153156
config,
154157
async [asyncDispose]() {
158+
debouncedPostBuild.cancel()
155159
ab?.abort()
156160
await watcher?.close()
157161
},
@@ -185,6 +189,12 @@ async function buildSingle(
185189
watcher.on('change', (id, event) => {
186190
if (event.event === 'update') {
187191
changedFile.push(id)
192+
// Cancel pending postBuild immediately on file change,
193+
// before the new build cycle starts. This prevents duplicate
194+
// onSuccess execution when rapid file changes (e.g. VS Code
195+
// auto-save) trigger multiple build cycles.
196+
debouncedPostBuild.cancel()
197+
ab?.abort()
188198
}
189199
if (configFiles.includes(id) || endsWithConfig.test(id)) {
190200
globalLogger.info(`Reload config: ${id}, restarting...`)
@@ -195,6 +205,8 @@ async function buildSingle(
195205
watcher.on('event', async (event) => {
196206
switch (event.code) {
197207
case 'START': {
208+
debouncedPostBuild.cancel()
209+
198210
if (config.clean.length) {
199211
await cleanChunks(config.outDir, chunks)
200212
}
@@ -206,7 +218,7 @@ async function buildSingle(
206218

207219
case 'END': {
208220
if (!hasError) {
209-
await postBuild()
221+
debouncedPostBuild()
210222
}
211223
break
212224
}

src/utils/general.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,16 @@ export function promiseWithResolvers<T>(): {
9696
export function typeAssert<T>(
9797
value: T,
9898
): asserts value is Exclude<T, false | null | undefined> {}
99+
100+
export function debounce<T extends (...args: any[]) => any>(
101+
fn: T,
102+
delay: number,
103+
): ((...args: Parameters<T>) => void) & { cancel: () => void } {
104+
let timer: ReturnType<typeof setTimeout> | undefined
105+
const debounced = (...args: Parameters<T>) => {
106+
clearTimeout(timer)
107+
timer = setTimeout(() => fn(...args), delay)
108+
}
109+
debounced.cancel = () => clearTimeout(timer)
110+
return debounced
111+
}

0 commit comments

Comments
 (0)