1- import { describe , expect , it , vi } from "vitest" ;
1+ import { afterEach , describe , expect , it , vi } from "vitest" ;
22import { create } from "zustand" ;
33import { persist } from "zustand/middleware" ;
4+ import { WRITE_DEBOUNCE_MS } from "./rendererStorage" ;
45
56type RendererStorageModule = typeof import ( "./rendererStorage" ) ;
67
@@ -9,6 +10,15 @@ async function importFreshRendererStorage(): Promise<RendererStorageModule> {
910 return await import ( "./rendererStorage" ) ;
1011}
1112
13+ /** Fresh module with fake timers and a backend already registered. */
14+ async function setupRegisteredBackend ( data : Record < string , string > = { } ) {
15+ vi . useFakeTimers ( ) ;
16+ const module = await importFreshRendererStorage ( ) ;
17+ const backend = fakeBackend ( data ) ;
18+ module . registerRendererStateStorage ( backend ) ;
19+ return { module, storage : jsonStorageOf ( module ) , backend } ;
20+ }
21+
1222function fakeBackend ( data : Record < string , string > ) {
1323 return {
1424 getItem : vi . fn ( async ( key : string ) => data [ key ] ?? null ) ,
@@ -30,6 +40,10 @@ function jsonStorageOf(module: RendererStorageModule) {
3040}
3141
3242describe ( "rendererStorage" , ( ) => {
43+ afterEach ( ( ) => {
44+ vi . useRealTimers ( ) ;
45+ } ) ;
46+
3347 it ( "serves reads issued before the host registers its storage" , async ( ) => {
3448 const module = await importFreshRendererStorage ( ) ;
3549 const storage = jsonStorageOf ( module ) ;
@@ -51,6 +65,7 @@ describe("rendererStorage", () => {
5165 } ) ;
5266
5367 it ( "drops writes racing the initial read, then writes through" , async ( ) => {
68+ vi . useFakeTimers ( ) ;
5469 const module = await importFreshRendererStorage ( ) ;
5570 const storage = jsonStorageOf ( module ) ;
5671 const backend = fakeBackend ( {
@@ -65,31 +80,35 @@ describe("rendererStorage", () => {
6580
6681 module . registerRendererStateStorage ( backend ) ;
6782 await Promise . all ( [ read , racingWrite ] ) ;
83+ await vi . advanceTimersByTimeAsync ( WRITE_DEBOUNCE_MS ) ;
6884 expect ( backend . setItem ) . not . toHaveBeenCalled ( ) ;
6985
7086 await storage . setItem ( "settings-storage" , {
7187 state : { mode : "changed" } ,
7288 version : 0 ,
7389 } ) ;
90+ await vi . advanceTimersByTimeAsync ( WRITE_DEBOUNCE_MS ) ;
7491 expect ( backend . setItem ) . toHaveBeenCalledTimes ( 1 ) ;
7592 } ) ;
7693
7794 it ( "passes writes through for keys that were never read" , async ( ) => {
95+ vi . useFakeTimers ( ) ;
7896 const module = await importFreshRendererStorage ( ) ;
7997 const storage = jsonStorageOf ( module ) ;
8098 const backend = fakeBackend ( { } ) ;
8199
82- const write = storage . setItem ( "write-only" , {
100+ await storage . setItem ( "write-only" , {
83101 state : { value : 1 } ,
84102 version : 0 ,
85103 } ) ;
86104 module . registerRendererStateStorage ( backend ) ;
87- await write ;
105+ await vi . advanceTimersByTimeAsync ( WRITE_DEBOUNCE_MS ) ;
88106
89107 expect ( backend . setItem ) . toHaveBeenCalledTimes ( 1 ) ;
90108 } ) ;
91109
92110 it ( "settles concurrent initial reads of the same key once" , async ( ) => {
111+ vi . useFakeTimers ( ) ;
93112 const module = await importFreshRendererStorage ( ) ;
94113 const storage = jsonStorageOf ( module ) ;
95114 const backend = fakeBackend ( {
@@ -110,10 +129,12 @@ describe("rendererStorage", () => {
110129 state : { mode : "changed" } ,
111130 version : 0 ,
112131 } ) ;
132+ await vi . advanceTimersByTimeAsync ( WRITE_DEBOUNCE_MS ) ;
113133 expect ( backend . setItem ) . toHaveBeenCalledTimes ( 1 ) ;
114134 } ) ;
115135
116136 it ( "marks a key settled when the initial read rejects so later writes pass" , async ( ) => {
137+ vi . useFakeTimers ( ) ;
117138 const module = await importFreshRendererStorage ( ) ;
118139 const storage = jsonStorageOf ( module ) ;
119140 const backend = fakeBackend ( { } ) ;
@@ -127,6 +148,7 @@ describe("rendererStorage", () => {
127148 state : { mode : "changed" } ,
128149 version : 0 ,
129150 } ) ;
151+ await vi . advanceTimersByTimeAsync ( WRITE_DEBOUNCE_MS ) ;
130152 expect ( backend . setItem ) . toHaveBeenCalledTimes ( 1 ) ;
131153 } ) ;
132154
@@ -196,12 +218,84 @@ describe("rendererStorage", () => {
196218 } ) ;
197219
198220 useStore . setState ( { defaultInitialTaskMode : "plan" } ) ;
199- await vi . waitFor ( ( ) => {
221+ await vi . waitFor ( async ( ) => {
222+ await module . flushRendererStateWrites ( ) ;
200223 expect ( backend . setItem ) . toHaveBeenCalled ( ) ;
201224 } ) ;
202225 const persisted = JSON . parse (
203226 backend . setItem . mock . calls [ backend . setItem . mock . calls . length - 1 ] [ 1 ] ,
204227 ) ;
205228 expect ( persisted . state . defaultInitialTaskMode ) . toBe ( "plan" ) ;
206229 } ) ;
230+
231+ it ( "coalesces a burst of writes into one backend write with the latest value" , async ( ) => {
232+ const { storage, backend } = await setupRegisteredBackend ( ) ;
233+
234+ await storage . setItem ( "drafts" , { state : { value : 1 } , version : 0 } ) ;
235+ await storage . setItem ( "drafts" , { state : { value : 2 } , version : 0 } ) ;
236+ await storage . setItem ( "drafts" , { state : { value : 3 } , version : 0 } ) ;
237+
238+ expect ( backend . setItem ) . not . toHaveBeenCalled ( ) ;
239+ await vi . advanceTimersByTimeAsync ( WRITE_DEBOUNCE_MS ) ;
240+ expect ( backend . setItem ) . toHaveBeenCalledTimes ( 1 ) ;
241+ const persisted = JSON . parse ( backend . setItem . mock . calls [ 0 ] [ 1 ] ) ;
242+ expect ( persisted . state . value ) . toBe ( 3 ) ;
243+ } ) ;
244+
245+ it ( "flushes at the max-wait bound during sustained writes" , async ( ) => {
246+ const { storage, backend } = await setupRegisteredBackend ( ) ;
247+
248+ // Write every 500ms for 6s: a plain trailing debounce would never fire,
249+ // the max-wait bound forces a flush mid-burst.
250+ for ( let i = 0 ; i < 12 ; i ++ ) {
251+ await storage . setItem ( "drafts" , { state : { value : i } , version : 0 } ) ;
252+ await vi . advanceTimersByTimeAsync ( 500 ) ;
253+ }
254+ expect ( backend . setItem . mock . calls . length ) . toBeGreaterThanOrEqual ( 1 ) ;
255+
256+ await vi . advanceTimersByTimeAsync ( WRITE_DEBOUNCE_MS ) ;
257+ const last = backend . setItem . mock . calls . at ( - 1 ) ;
258+ expect ( JSON . parse ( last ?. [ 1 ] ?? "{}" ) . state . value ) . toBe ( 11 ) ;
259+ } ) ;
260+
261+ it ( "lands the pending coalesced write before serving a read" , async ( ) => {
262+ const { storage, backend } = await setupRegisteredBackend ( {
263+ drafts : JSON . stringify ( { state : { value : "stale" } , version : 0 } ) ,
264+ } ) ;
265+
266+ await storage . setItem ( "drafts" , { state : { value : "fresh" } , version : 0 } ) ;
267+
268+ await expect ( storage . getItem ( "drafts" ) ) . resolves . toMatchObject ( {
269+ state : { value : "fresh" } ,
270+ } ) ;
271+ expect ( backend . setItem ) . toHaveBeenCalledTimes ( 1 ) ;
272+
273+ // The read consumed the pending write; the debounce timer must not fire
274+ // a duplicate.
275+ await vi . advanceTimersByTimeAsync ( WRITE_DEBOUNCE_MS ) ;
276+ expect ( backend . setItem ) . toHaveBeenCalledTimes ( 1 ) ;
277+ } ) ;
278+
279+ it ( "cancels a pending write when the key is removed" , async ( ) => {
280+ const { storage, backend } = await setupRegisteredBackend ( ) ;
281+
282+ await storage . setItem ( "drafts" , { state : { value : 1 } , version : 0 } ) ;
283+ await storage . removeItem ( "drafts" ) ;
284+ await vi . advanceTimersByTimeAsync ( WRITE_DEBOUNCE_MS ) ;
285+
286+ expect ( backend . setItem ) . not . toHaveBeenCalled ( ) ;
287+ expect ( backend . removeItem ) . toHaveBeenCalledTimes ( 1 ) ;
288+ } ) ;
289+
290+ it ( "flushRendererStateWrites persists pending state immediately" , async ( ) => {
291+ const { module, storage, backend } = await setupRegisteredBackend ( ) ;
292+
293+ await storage . setItem ( "drafts" , { state : { value : 1 } , version : 0 } ) ;
294+ await module . flushRendererStateWrites ( ) ;
295+ expect ( backend . setItem ) . toHaveBeenCalledTimes ( 1 ) ;
296+
297+ // The debounce timer was cancelled by the flush; no duplicate write.
298+ await vi . advanceTimersByTimeAsync ( WRITE_DEBOUNCE_MS ) ;
299+ expect ( backend . setItem ) . toHaveBeenCalledTimes ( 1 ) ;
300+ } ) ;
207301} ) ;
0 commit comments