Skip to content

Commit 461527b

Browse files
committed
feat(watch): expose debounce related options
1 parent ec246ba commit 461527b

File tree

9 files changed

+87
-29
lines changed

9 files changed

+87
-29
lines changed

crates/rolldown_binding/src/options/binding_input_options/binding_watch_option.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ pub struct BindingWatchOption {
1717
pub use_polling: Option<bool>,
1818
pub poll_interval: Option<u32>,
1919
pub compare_contents_for_polling: Option<bool>,
20+
pub use_debounce: Option<bool>,
21+
pub debounce_delay: Option<u32>,
22+
pub debounce_tick_rate: Option<u32>,
2023
#[napi(ts_type = "((id: string) => void) | undefined")]
2124
#[debug(skip)]
2225
pub on_invalidate: Option<JsCallback<FnArgs<(String,)>>>,
@@ -32,6 +35,9 @@ impl From<BindingWatchOption> for rolldown_common::WatchOption {
3235
use_polling: value.use_polling.unwrap_or_default(),
3336
poll_interval: value.poll_interval.map(u64::from),
3437
compare_contents_for_polling: value.compare_contents_for_polling.unwrap_or_default(),
38+
use_debounce: value.use_debounce.unwrap_or_default(),
39+
debounce_delay: value.debounce_delay.map(u64::from),
40+
debounce_tick_rate: value.debounce_tick_rate.map(u64::from),
3541
on_invalidate: value.on_invalidate.map(|js_callback| {
3642
OnInvalidate::new(Arc::new(move |path| {
3743
let f = Arc::clone(&js_callback);

crates/rolldown_binding/src/watcher.rs

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -66,33 +66,16 @@ impl BindingWatcher {
6666
.map(create_bundler_config_from_binding_options)
6767
.collect::<Result<Vec<_>, _>>()?;
6868

69-
// Forward the largest build_delay from configs to the watcher's debounce.
70-
let build_delay =
71-
configs.iter().filter_map(|c| c.options.watch.as_ref().and_then(|w| w.build_delay)).max();
72-
73-
// Extract use_polling / poll_interval / compare_contents_for_polling from the first config that specifies them.
74-
let use_polling = configs
75-
.iter()
76-
.find_map(|c| c.options.watch.as_ref().filter(|w| w.use_polling).map(|w| w.use_polling))
77-
.unwrap_or(false);
78-
let poll_interval =
79-
configs.iter().find_map(|c| c.options.watch.as_ref().and_then(|w| w.poll_interval));
80-
let compare_contents_for_polling = configs
81-
.iter()
82-
.find_map(|c| {
83-
c.options
84-
.watch
85-
.as_ref()
86-
.filter(|w| w.compare_contents_for_polling)
87-
.map(|w| w.compare_contents_for_polling)
88-
})
89-
.unwrap_or(false);
90-
69+
// Extract watcher config from the first config's watch options.
70+
let watch = configs.first().and_then(|c| c.options.watch.as_ref());
9171
let watcher_config = WatcherConfig {
92-
debounce: build_delay.map(|ms| Duration::from_millis(u64::from(ms))),
93-
use_polling,
94-
poll_interval,
95-
compare_contents_for_polling,
72+
debounce: watch.and_then(|w| w.build_delay).map(|ms| Duration::from_millis(u64::from(ms))),
73+
use_polling: watch.is_some_and(|w| w.use_polling),
74+
poll_interval: watch.and_then(|w| w.poll_interval),
75+
compare_contents_for_polling: watch.is_some_and(|w| w.compare_contents_for_polling),
76+
use_debounce: watch.is_some_and(|w| w.use_debounce),
77+
debounce_delay: watch.and_then(|w| w.debounce_delay),
78+
debounce_tick_rate: watch.and_then(|w| w.debounce_tick_rate),
9679
};
9780

9881
let handler = NapiWatcherEventHandler { listener: Arc::new(listener) };

crates/rolldown_common/src/inner_bundler_options/types/watch_option.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@ use serde::{Deserialize, Deserializer};
1414
derive(Deserialize, JsonSchema),
1515
serde(rename_all = "camelCase", deny_unknown_fields)
1616
)]
17+
#[expect(clippy::struct_excessive_bools)]
1718
pub struct WatchOption {
1819
pub skip_write: bool,
1920
pub build_delay: Option<u32>,
2021
pub use_polling: bool,
2122
pub poll_interval: Option<u64>,
2223
pub compare_contents_for_polling: bool,
24+
pub use_debounce: bool,
25+
pub debounce_delay: Option<u64>,
26+
pub debounce_tick_rate: Option<u64>,
2327
#[cfg_attr(
2428
feature = "deserialize_bundler_options",
2529
serde(default, deserialize_with = "deserialize_string_or_regex"),

crates/rolldown_testing/_config.schema.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1458,6 +1458,25 @@
14581458
"compareContentsForPolling": {
14591459
"type": "boolean"
14601460
},
1461+
"useDebounce": {
1462+
"type": "boolean"
1463+
},
1464+
"debounceDelay": {
1465+
"type": [
1466+
"integer",
1467+
"null"
1468+
],
1469+
"format": "uint64",
1470+
"minimum": 0
1471+
},
1472+
"debounceTickRate": {
1473+
"type": [
1474+
"integer",
1475+
"null"
1476+
],
1477+
"format": "uint64",
1478+
"minimum": 0
1479+
},
14611480
"include": {
14621481
"type": [
14631482
"array",
@@ -1481,7 +1500,8 @@
14811500
"required": [
14821501
"skipWrite",
14831502
"usePolling",
1484-
"compareContentsForPolling"
1503+
"compareContentsForPolling",
1504+
"useDebounce"
14851505
]
14861506
},
14871507
"LegalComments": {

crates/rolldown_watcher/src/watcher.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ pub struct WatcherConfig {
3131
pub poll_interval: Option<u64>,
3232
/// Whether to compare file contents for poll-based watchers (only used when `use_polling` is true)
3333
pub compare_contents_for_polling: bool,
34+
/// Whether to use debounced event delivery at the filesystem level
35+
pub use_debounce: bool,
36+
/// Debounce delay in milliseconds for fs-level debounced watchers (only used when `use_debounce` is true)
37+
pub debounce_delay: Option<u64>,
38+
/// Tick rate in milliseconds for the debouncer's internal polling (only used when `use_debounce` is true)
39+
pub debounce_tick_rate: Option<u64>,
3440
}
3541

3642
impl WatcherConfig {
@@ -45,8 +51,11 @@ impl WatcherConfig {
4551
}
4652
config.compare_contents_for_polling = self.compare_contents_for_polling;
4753
config.use_polling = self.use_polling;
48-
// rolldown_watcher doesn't support debounce currently
49-
config.use_debounce = false;
54+
config.use_debounce = self.use_debounce;
55+
if let Some(debounce_delay) = self.debounce_delay {
56+
config.debounce_delay = debounce_delay;
57+
}
58+
config.debounce_tick_rate = self.debounce_tick_rate;
5059
config
5160
}
5261
}

packages/rolldown/src/binding.d.cts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2780,6 +2780,9 @@ export interface BindingWatchOption {
27802780
usePolling?: boolean
27812781
pollInterval?: number
27822782
compareContentsForPolling?: boolean
2783+
useDebounce?: boolean
2784+
debounceDelay?: number
2785+
debounceTickRate?: number
27832786
onInvalidate?: ((id: string) => void) | undefined
27842787
}
27852788

packages/rolldown/src/options/input-options.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,24 @@ export interface WatcherFileWatcherOptions {
8181
* @default false
8282
*/
8383
compareContentsForPolling?: boolean;
84+
/**
85+
* Whether to use debounced event delivery at the filesystem level.
86+
* This coalesces rapid filesystem events before they reach the build coordinator.
87+
* @default false
88+
*/
89+
useDebounce?: boolean;
90+
/**
91+
* Debounce delay in milliseconds for fs-level debounced watchers.
92+
* Only used when {@linkcode useDebounce} is `true`.
93+
* @default 10
94+
*/
95+
debounceDelay?: number;
96+
/**
97+
* Tick rate in milliseconds for the debouncer's internal polling.
98+
* Only used when {@linkcode useDebounce} is `true`.
99+
* When undefined, auto-selects 1/4 of debounceDelay.
100+
*/
101+
debounceTickRate?: number;
84102
}
85103

86104
export interface WatcherOptions {

packages/rolldown/src/utils/bindingify-input-options.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,9 @@ function bindingifyWatch(watch: InputOptions['watch']): BindingInputOptions['wat
309309
usePolling: watcher.usePolling,
310310
pollInterval: watcher.pollInterval,
311311
compareContentsForPolling: watcher.compareContentsForPolling,
312+
useDebounce: watcher.useDebounce,
313+
debounceDelay: watcher.debounceDelay,
314+
debounceTickRate: watcher.debounceTickRate,
312315
include: normalizedStringOrRegex(watch.include),
313316
exclude: normalizedStringOrRegex(watch.exclude),
314317
onInvalidate: (...args) => watch.onInvalidate?.(...args),

packages/rolldown/src/utils/validator.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,18 @@ const WatcherFileWatcherOptionsSchema = v.strictObject({
274274
'Compare file contents for poll-based watchers (only used when usePolling is true)',
275275
),
276276
),
277+
useDebounce: v.pipe(
278+
v.optional(v.boolean()),
279+
v.description('Use debounced event delivery at the filesystem level'),
280+
),
281+
debounceDelay: v.pipe(
282+
v.optional(v.number()),
283+
v.description('Debounce delay in milliseconds (only used when useDebounce is true)'),
284+
),
285+
debounceTickRate: v.pipe(
286+
v.optional(v.number()),
287+
v.description('Tick rate in milliseconds for debouncer (only used when useDebounce is true)'),
288+
),
277289
});
278290

279291
const WatcherOptionsSchema = v.strictObject({

0 commit comments

Comments
 (0)