@@ -120,23 +120,57 @@ describe('createBoardRealtimeController', () => {
120120 } )
121121
122122 it ( 'refreshes board when matching board mutation event arrives' , async ( ) => {
123+ vi . useFakeTimers ( )
123124 const fetchBoard = vi . fn ( async ( ) => undefined )
124125 const controller = createBoardRealtimeController ( { fetchBoard } )
125126
126127 await controller . start ( 'board-1' )
127- await callbacks . boardMutation ?.( { boardId : 'board-1' } )
128+ callbacks . boardMutation ?.( { boardId : 'board-1' } )
129+
130+ // The handler debounces the refresh — advance past the debounce window.
131+ await vi . advanceTimersByTimeAsync ( 300 )
128132
129133 expect ( fetchBoard ) . toHaveBeenCalledWith ( 'board-1' )
134+ vi . useRealTimers ( )
130135 } )
131136
132137 it ( 'ignores mutation events for other boards' , async ( ) => {
138+ vi . useFakeTimers ( )
139+ const fetchBoard = vi . fn ( async ( ) => undefined )
140+ const controller = createBoardRealtimeController ( { fetchBoard } )
141+
142+ await controller . start ( 'board-1' )
143+ callbacks . boardMutation ?.( { boardId : 'board-2' } )
144+
145+ // Advance past the debounce window to confirm nothing fires.
146+ await vi . advanceTimersByTimeAsync ( 300 )
147+
148+ expect ( fetchBoard ) . not . toHaveBeenCalled ( )
149+ vi . useRealTimers ( )
150+ } )
151+
152+ it ( 'coalesces rapid burst mutation events into a single fetchBoard call' , async ( ) => {
153+ vi . useFakeTimers ( )
133154 const fetchBoard = vi . fn ( async ( ) => undefined )
134155 const controller = createBoardRealtimeController ( { fetchBoard } )
135156
136157 await controller . start ( 'board-1' )
137- await callbacks . boardMutation ?.( { boardId : 'board-2' } )
138158
159+ // Fire three mutation events in rapid succession (within the debounce window).
160+ callbacks . boardMutation ?.( { boardId : 'board-1' } )
161+ callbacks . boardMutation ?.( { boardId : 'board-1' } )
162+ callbacks . boardMutation ?.( { boardId : 'board-1' } )
163+
164+ // Before the debounce window closes, fetchBoard should not have been called.
139165 expect ( fetchBoard ) . not . toHaveBeenCalled ( )
166+
167+ // Advance past the debounce — only one fetch should fire for the burst.
168+ await vi . advanceTimersByTimeAsync ( 300 )
169+ expect ( fetchBoard ) . toHaveBeenCalledTimes ( 1 )
170+ expect ( fetchBoard ) . toHaveBeenCalledWith ( 'board-1' )
171+
172+ vi . useRealTimers ( )
173+ await controller . stop ( )
140174 } )
141175
142176 it ( 'emits presence snapshots for the currently subscribed board' , async ( ) => {
@@ -175,6 +209,30 @@ describe('createBoardRealtimeController', () => {
175209 expect ( mockConnection . invoke ) . toHaveBeenCalledWith ( 'JoinBoard' , 'board-2' )
176210 } )
177211
212+ it ( 'cancels a pending debounce timer when switching boards' , async ( ) => {
213+ // Regression: a board-A mutation event with a debounce timer pending must
214+ // not fire fetchBoard after subscribedBoardId has advanced to board-B.
215+ vi . useFakeTimers ( )
216+ const fetchBoard = vi . fn ( async ( ) => undefined )
217+ const controller = createBoardRealtimeController ( { fetchBoard } )
218+
219+ await controller . start ( 'board-1' )
220+
221+ // A mutation event for board-1 starts the 300 ms debounce timer.
222+ callbacks . boardMutation ?.( { boardId : 'board-1' } )
223+
224+ // Switch to board-2 before the timer fires — must discard the pending timer.
225+ await controller . switchBoard ( 'board-2' )
226+
227+ // Advance past the original debounce window.
228+ await vi . advanceTimersByTimeAsync ( 300 )
229+
230+ // fetchBoard must not have been called at all; the stale timer was cancelled.
231+ expect ( fetchBoard ) . not . toHaveBeenCalled ( )
232+
233+ await controller . stop ( )
234+ } )
235+
178236 it ( 'falls back to polling when websocket connection cannot start' , async ( ) => {
179237 vi . useFakeTimers ( )
180238 const fetchBoard = vi . fn ( async ( ) => undefined )
@@ -183,7 +241,7 @@ describe('createBoardRealtimeController', () => {
183241 const controller = createBoardRealtimeController ( { fetchBoard } )
184242 await controller . start ( 'board-1' )
185243
186- await vi . advanceTimersByTimeAsync ( 15000 )
244+ await vi . advanceTimersByTimeAsync ( 30000 )
187245 expect ( fetchBoard ) . toHaveBeenCalledWith ( 'board-1' )
188246
189247 await controller . stop ( )
@@ -207,15 +265,15 @@ describe('createBoardRealtimeController', () => {
207265 await controller . start ( 'board-1' )
208266 await controller . setEditingCard ( 'card-1' )
209267 await callbacks . reconnecting ?.( )
210- await vi . advanceTimersByTimeAsync ( 15000 )
268+ await vi . advanceTimersByTimeAsync ( 30000 )
211269 expect ( fetchBoard ) . toHaveBeenCalledWith ( 'board-1' )
212270
213271 fetchBoard . mockClear ( )
214272 await callbacks . reconnected ?.( )
215273 expect ( mockConnection . invoke ) . toHaveBeenCalledWith ( 'JoinBoard' , 'board-1' )
216274 expect ( mockConnection . invoke ) . toHaveBeenCalledWith ( 'SetEditingCard' , 'board-1' , 'card-1' )
217275
218- await vi . advanceTimersByTimeAsync ( 15000 )
276+ await vi . advanceTimersByTimeAsync ( 30000 )
219277 expect ( fetchBoard ) . not . toHaveBeenCalled ( )
220278
221279 await controller . stop ( )
0 commit comments