1- use notify:: { Config as NotifyConfig , RecommendedWatcher , RecursiveMode , Watcher } ;
1+ use notify:: { event :: ModifyKind , Config as NotifyConfig , EventKind , RecommendedWatcher , RecursiveMode , Watcher } ;
22use serde_json:: Value ;
33use std:: {
44 path:: { Path , PathBuf } ,
55 sync:: { Arc , RwLock } ,
6+ time:: Duration ,
67} ;
7- use tokio:: { sync:: mpsc, task:: JoinHandle } ;
8+ use tokio:: { sync:: mpsc, task:: JoinHandle , time } ;
89
910#[ derive( Debug , thiserror:: Error ) ]
1011pub enum WalletFeatureFlagsError {
@@ -28,6 +29,14 @@ pub struct WalletFeatureFlagsService {
2829}
2930
3031impl WalletFeatureFlagsService {
32+ fn is_reload_event_kind ( kind : & EventKind ) -> bool {
33+ match kind {
34+ EventKind :: Create ( _) | EventKind :: Modify ( ModifyKind :: Name ( _) ) => true ,
35+ EventKind :: Modify ( modify_kind) => !matches ! ( modify_kind, ModifyKind :: Metadata ( _) | ModifyKind :: Other ) ,
36+ _ => false ,
37+ }
38+ }
39+
3140 pub fn new ( file_path : impl Into < PathBuf > ) -> Result < Self , WalletFeatureFlagsError > {
3241 let file_path = file_path. into ( ) ;
3342
@@ -50,23 +59,63 @@ impl WalletFeatureFlagsService {
5059 watcher. watch ( parent_dir, RecursiveMode :: NonRecursive ) ?;
5160
5261 let wallet_feature_flags_clone = wallet_feature_flags. clone ( ) ;
62+ let watched_file_name = file_path. file_name ( ) . map ( |n| n. to_os_string ( ) ) ;
63+ let debounce_duration = Duration :: from_millis ( 250 ) ;
5364
5465 let watch_task = tokio:: spawn ( async move {
55- while let Some ( result) = rx. recv ( ) . await {
56- match result {
57- Ok ( event) => {
58- // This ensures Create, Rename, and Modify events triggered by atomic saves are caught.
59- let should_reload = event. paths . iter ( ) . any ( |p| p. file_name ( ) == file_path. file_name ( ) ) ;
60-
61- if !should_reload {
62- continue ;
66+ // Atomic file saves commonly emit bursts of events (Create/Rename/Modify).
67+ // We debounce these bursts to avoid reloading (and logging) repeatedly.
68+ let mut reload_pending = false ;
69+ let reload_sleep = time:: sleep ( debounce_duration) ;
70+ tokio:: pin!( reload_sleep) ;
71+
72+ loop {
73+ tokio:: select! {
74+ biased;
75+ maybe_result = rx. recv( ) => {
76+ let Some ( result) = maybe_result else {
77+ break ;
78+ } ;
79+
80+ match result {
81+ Ok ( event) => {
82+ if !Self :: is_reload_event_kind( & event. kind) {
83+ continue ;
84+ }
85+
86+ let should_reload = watched_file_name
87+ . as_deref( )
88+ . map( |name| event. paths. iter( ) . any( |p| p. file_name( ) == Some ( name) ) )
89+ . unwrap_or( false ) ;
90+
91+ if !should_reload {
92+ continue ;
93+ }
94+
95+ reload_pending = true ;
96+ reload_sleep. as_mut( ) . reset( time:: Instant :: now( ) + debounce_duration) ;
97+ }
98+ Err ( err) => {
99+ tracing:: error!( "Wallet feature flags watcher error: {}" , err) ;
100+ }
63101 }
102+ }
103+ _ = & mut reload_sleep, if reload_pending => {
104+ reload_pending = false ;
64105
65106 match Self :: read_flags_from_file_async( & file_path) . await {
66107 Ok ( updated_flags) => {
67108 if let Ok ( mut write_guard) = wallet_feature_flags_clone. write( ) {
109+ if * write_guard == updated_flags {
110+ // Avoid noisy log spam when events are emitted without content changes.
111+ continue ;
112+ }
113+
68114 * write_guard = updated_flags;
69- tracing:: info!( "Wallet feature flags reloaded from {}" , file_path. display( ) ) ;
115+ tracing:: info!(
116+ "Wallet feature flags reloaded from {}" ,
117+ file_path. display( )
118+ ) ;
70119 }
71120 }
72121 Err ( err) => {
@@ -78,9 +127,6 @@ impl WalletFeatureFlagsService {
78127 }
79128 }
80129 }
81- Err ( err) => {
82- tracing:: error!( "Wallet feature flags watcher error: {}" , err) ;
83- }
84130 }
85131 }
86132 } ) ;
0 commit comments