@@ -41,7 +41,7 @@ describe('PouchDbSyncStorage', () => {
4141 storage [ Symbol . dispose ] ( ) ;
4242 } ) ;
4343
44- it ( " surfaces a remote-only write via a scheduled pull, without needing a continuous pull connection" , async ( ) => {
44+ it ( ' surfaces a remote-only write via a scheduled pull, without needing a continuous pull connection' , async ( ) => {
4545 const storage = new PouchDbSyncStorage ( {
4646 localDb : { name_or_instance : localDb } ,
4747 remoteDb : { name_or_instance : remoteDb } ,
@@ -93,6 +93,70 @@ describe('PouchDbSyncStorage', () => {
9393 } ) ;
9494 } ) ;
9595
96+ describe ( 'pull.mode "visible-interval"' , ( ) => {
97+ const setVisibility = ( state : DocumentVisibilityState ) => {
98+ Object . defineProperty ( document , 'visibilityState' , { value : state , configurable : true } ) ;
99+ document . dispatchEvent ( new Event ( 'visibilitychange' ) ) ;
100+ } ;
101+
102+ afterEach ( ( ) => {
103+ // Other describe blocks in this file assume a visible tab by default.
104+ setVisibility ( 'visible' ) ;
105+ } ) ;
106+
107+ it ( 'skips interval ticks while hidden, then pulls exactly once on returning to visible' , async ( ) => {
108+ // Captures each replication's registered handlers so the test can drive 'complete'
109+ // itself - deterministic, instead of racing real PouchDB I/O against the interval timer.
110+ const fakeReplications : Array < { complete : ( ) => void } > = [ ] ;
111+ const replicateFrom = vi . spyOn ( localDb . replicate , 'from' ) . mockImplementation ( ( ) => {
112+ const handlers : Record < string , Array < ( ) => void > > = { } ;
113+ const replication = {
114+ on : vi . fn ( ( event : string , handler : ( ) => void ) => {
115+ if ( ! handlers [ event ] ) handlers [ event ] = [ ] ;
116+ handlers [ event ] . push ( handler ) ;
117+ } ) ,
118+ removeListener : vi . fn ( ) ,
119+ // biome-ignore lint/suspicious/noThenProperty: mocking PouchDB's Replication, which is genuinely thenable.
120+ then : vi . fn ( ) ,
121+ cancel : vi . fn ( ) ,
122+ } ;
123+ fakeReplications . push ( {
124+ complete : ( ) => {
125+ handlers . complete ?. forEach ( ( handler ) => {
126+ handler ( ) ;
127+ } ) ;
128+ } ,
129+ } ) ;
130+ return replication as unknown as ReturnType < typeof localDb . replicate . from > ;
131+ } ) ;
132+
133+ setVisibility ( 'hidden' ) ;
134+ const storage = new PouchDbSyncStorage ( {
135+ localDb : { name_or_instance : localDb } ,
136+ remoteDb : { name_or_instance : remoteDb } ,
137+ syncOptions : { } ,
138+ pull : { mode : 'visible-interval' , intervalMs : 20 , refreshOnFocus : true } ,
139+ } ) ;
140+
141+ await storage . initialize ( ) ;
142+ expect ( replicateFrom ) . toHaveBeenCalledTimes ( 1 ) ; // the always-runs initial pull
143+
144+ // Several interval ticks pass while hidden - the schedule should skip every one of them.
145+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
146+ expect ( replicateFrom ) . toHaveBeenCalledTimes ( 1 ) ;
147+
148+ // Release the initial pull, so the upcoming focus trigger isn't skipped as already in flight.
149+ fakeReplications [ 0 ] . complete ( ) ;
150+
151+ setVisibility ( 'visible' ) ;
152+ // Returning to visible triggers exactly one catch-up pull, not one per missed tick.
153+ expect ( replicateFrom ) . toHaveBeenCalledTimes ( 2 ) ;
154+
155+ replicateFrom . mockRestore ( ) ;
156+ storage [ Symbol . dispose ] ( ) ;
157+ } ) ;
158+ } ) ;
159+
96160 describe ( 'pull watchdog' , ( ) => {
97161 it ( 'cancels a hung pull replication and releases it for the next scheduled pull' , async ( ) => {
98162 vi . useFakeTimers ( ) ;
0 commit comments