@@ -74,7 +74,37 @@ class CommandStore {
7474 /// Drain any in-flight async work so all queued writes finish before we
7575 /// return. Safe to call from any thread.
7676 func flushSync( ) {
77- queue. sync { /* serial fence */ }
77+ queue. sync { drainPendingSaves ( ) }
78+ }
79+
80+ // MARK: - Debounced persistence (call only from within queue)
81+
82+ /// Directories with unsaved changes and the exact snapshot to persist.
83+ /// Holding the data (not just the dirty flag) protects against the LRU
84+ /// cache evicting a directory before its debounced save lands.
85+ private var pendingSaves : [ String : [ StoredCommand ] ] = [ : ]
86+ private var saveTimer : DispatchSourceTimer ?
87+
88+ /// Coalesces the full-file JSON rewrite that used to run on every Enter.
89+ /// Safe against quits: the willTerminate/willResignActive observers call
90+ /// flushSync(), which drains pending saves synchronously.
91+ private func scheduleSave( _ commands: [ StoredCommand ] , for directory: String ) {
92+ pendingSaves [ directory] = commands
93+ guard saveTimer == nil else { return }
94+ let timer = DispatchSource . makeTimerSource ( queue: queue)
95+ timer. schedule ( deadline: . now( ) + 1.0 )
96+ timer. setEventHandler { [ weak self] in self ? . drainPendingSaves ( ) }
97+ timer. resume ( )
98+ saveTimer = timer
99+ }
100+
101+ private func drainPendingSaves( ) {
102+ saveTimer? . cancel ( )
103+ saveTimer = nil
104+ for (directory, commands) in pendingSaves {
105+ saveCommands ( commands, for: directory)
106+ }
107+ pendingSaves. removeAll ( )
78108 }
79109
80110 // MARK: - Public API
@@ -103,12 +133,7 @@ class CommandStore {
103133 cmds = Array ( cmds. prefix ( Self . maxCommandsPerDirectory) )
104134 }
105135 self . updateCache ( directory: directory, commands: cmds)
106- // Persist immediately on the serial queue. The previous version
107- // debounced writes by 2s and relied on app-lifecycle notifications
108- // to flush — those notifications never fired because the
109- // selector-based observer required NSObject inheritance, so a
110- // burst of commands followed by a quit lost all of them.
111- self . saveCommands ( cmds, for: directory)
136+ self . scheduleSave ( cmds, for: directory)
112137 }
113138 }
114139
@@ -118,7 +143,7 @@ class CommandStore {
118143 var cmds = self . _commands ( for: directory)
119144 cmds. removeAll { $0. text == command }
120145 self . updateCache ( directory: directory, commands: cmds)
121- self . saveCommands ( cmds, for: directory)
146+ self . scheduleSave ( cmds, for: directory)
122147 }
123148 }
124149
@@ -186,6 +211,11 @@ class CommandStore {
186211 touchCache ( directory)
187212 return cached
188213 }
214+ // A pending debounced save is fresher than the file on disk.
215+ if let pending = pendingSaves [ directory] {
216+ updateCache ( directory: directory, commands: pending)
217+ return pending
218+ }
189219 let loaded = loadCommands ( for: directory)
190220 updateCache ( directory: directory, commands: loaded)
191221 return loaded
0 commit comments