1
+ import { setTimeout } from 'node:timers/promises'
1
2
import { describe , expect , it , vi } from 'vitest'
2
- import { DebouncedPromiseQueue } from './debounce'
3
+ import type { UpdateUrlFunction } from '../../adapters/lib/defs'
4
+ import { DebounceController , DebouncedPromiseQueue } from './debounce'
5
+ import { ThrottledQueue , type UpdateQueueAdapterContext } from './throttle'
3
6
4
- describe ( 'queues: DebouncedPromiseQueue' , ( ) => {
5
- it ( 'creates a queue for a given key' , ( ) => {
7
+ async function passThrough < T > ( value : T ) : Promise < T > {
8
+ return value
9
+ }
10
+
11
+ describe ( 'debounce: DebouncedPromiseQueue' , ( ) => {
12
+ it ( 'calls the callback after the timer expired' , ( ) => {
6
13
vi . useFakeTimers ( )
7
14
const spy = vi . fn ( ) . mockResolvedValue ( 'output' )
8
15
const queue = new DebouncedPromiseQueue ( spy )
@@ -22,12 +29,12 @@ describe('queues: DebouncedPromiseQueue', () => {
22
29
} )
23
30
it ( 'returns a stable promise to the next time the callback is called' , async ( ) => {
24
31
vi . useFakeTimers ( )
25
- const queue = new DebouncedPromiseQueue ( ( ) => Promise . resolve ( 'output' ) )
26
- const p1 = queue . push ( 'value ' , 100 )
27
- const p2 = queue . push ( 'value ' , 100 )
32
+ const queue = new DebouncedPromiseQueue ( passThrough )
33
+ const p1 = queue . push ( 'a ' , 100 )
34
+ const p2 = queue . push ( 'b ' , 100 )
28
35
expect ( p1 ) . toBe ( p2 )
29
36
vi . advanceTimersToNextTimer ( )
30
- await expect ( p1 ) . resolves . toBe ( 'output ' )
37
+ await expect ( p1 ) . resolves . toBe ( 'b ' )
31
38
} )
32
39
it ( 'returns a new Promise once the callback is called' , async ( ) => {
33
40
vi . useFakeTimers ( )
@@ -41,4 +48,144 @@ describe('queues: DebouncedPromiseQueue', () => {
41
48
vi . advanceTimersToNextTimer ( )
42
49
await expect ( p2 ) . resolves . toBe ( 1 )
43
50
} )
51
+ it ( 'keeps a record of the last queued value' , async ( ) => {
52
+ vi . useFakeTimers ( )
53
+ const queue = new DebouncedPromiseQueue ( passThrough )
54
+ const p = queue . push ( 'a' , 100 )
55
+ expect ( queue . queuedValue ) . toBe ( 'a' )
56
+ vi . advanceTimersToNextTimer ( )
57
+ await expect ( p ) . resolves . toBe ( 'a' )
58
+ expect ( queue . queuedValue ) . toBeUndefined ( )
59
+ } )
60
+ it ( 'clears the queued value when the callback returns its promise (not when it resolves)' , ( ) => {
61
+ vi . useFakeTimers ( )
62
+ const queue = new DebouncedPromiseQueue ( async input => {
63
+ await setTimeout ( 100 )
64
+ return input
65
+ } )
66
+ queue . push ( 'a' , 100 )
67
+ vi . advanceTimersByTime ( 100 )
68
+ expect ( queue . queuedValue ) . toBeUndefined ( )
69
+ } )
70
+ it ( 'clears the queued value when the callback throws an error synchronously' , async ( ) => {
71
+ vi . useFakeTimers ( )
72
+ const queue = new DebouncedPromiseQueue ( ( ) => {
73
+ throw new Error ( 'error' )
74
+ } )
75
+ const p = queue . push ( 'a' , 100 )
76
+ vi . advanceTimersToNextTimer ( )
77
+ expect ( queue . queuedValue ) . toBeUndefined ( )
78
+ await expect ( p ) . rejects . toThrowError ( 'error' )
79
+ } )
80
+ it ( 'clears the queued value when the callback rejects' , async ( ) => {
81
+ vi . useFakeTimers ( )
82
+ const queue = new DebouncedPromiseQueue ( ( ) =>
83
+ Promise . reject ( new Error ( 'error' ) )
84
+ )
85
+ const p = queue . push ( 'a' , 100 )
86
+ vi . advanceTimersToNextTimer ( )
87
+ expect ( queue . queuedValue ) . toBeUndefined ( )
88
+ await expect ( p ) . rejects . toThrowError ( 'error' )
89
+ } )
90
+ } )
91
+
92
+ describe . only ( 'debounce: DebounceController' , ( ) => {
93
+ it ( 'schedules an update and calls the adapter with it' , async ( ) => {
94
+ vi . useFakeTimers ( )
95
+ const fakeAdapter : UpdateQueueAdapterContext = {
96
+ updateUrl : vi . fn < UpdateUrlFunction > ( ) ,
97
+ getSearchParamsSnapshot ( ) {
98
+ return new URLSearchParams ( )
99
+ }
100
+ }
101
+ const controller = new DebounceController ( )
102
+ const promise = controller . push (
103
+ {
104
+ key : 'key' ,
105
+ query : 'value' ,
106
+ options : { }
107
+ } ,
108
+ 100 ,
109
+ fakeAdapter
110
+ )
111
+ const queue = controller . queues . get ( 'key' )
112
+ expect ( queue ) . toBeInstanceOf ( DebouncedPromiseQueue )
113
+ vi . runAllTimers ( )
114
+ await expect ( promise ) . resolves . toEqual ( new URLSearchParams ( '?key=value' ) )
115
+ expect ( fakeAdapter . updateUrl ) . toHaveBeenCalledExactlyOnceWith (
116
+ new URLSearchParams ( '?key=value' ) ,
117
+ {
118
+ history : 'replace' ,
119
+ scroll : false ,
120
+ shallow : true
121
+ }
122
+ )
123
+ } )
124
+ it ( 'isolates debounce queues per key' , async ( ) => {
125
+ vi . useFakeTimers ( )
126
+ const fakeAdapter : UpdateQueueAdapterContext = {
127
+ updateUrl : vi . fn < UpdateUrlFunction > ( ) ,
128
+ getSearchParamsSnapshot ( ) {
129
+ return new URLSearchParams ( )
130
+ }
131
+ }
132
+ const controller = new DebounceController ( )
133
+ const promise1 = controller . push (
134
+ {
135
+ key : 'a' ,
136
+ query : 'a' ,
137
+ options : { }
138
+ } ,
139
+ 100 ,
140
+ fakeAdapter
141
+ )
142
+ const promise2 = controller . push (
143
+ {
144
+ key : 'b' ,
145
+ query : 'b' ,
146
+ options : { }
147
+ } ,
148
+ 200 ,
149
+ fakeAdapter
150
+ )
151
+ expect ( promise1 ) . not . toBe ( promise2 )
152
+ vi . runAllTimers ( )
153
+ await expect ( promise1 ) . resolves . toEqual ( new URLSearchParams ( '?a=a' ) )
154
+ // Our snapshot always returns an empty search params object, so there is no
155
+ // merging of keys here.
156
+ await expect ( promise2 ) . resolves . toEqual ( new URLSearchParams ( '?b=b' ) )
157
+ expect ( fakeAdapter . updateUrl ) . toHaveBeenCalledTimes ( 2 )
158
+ } )
159
+ it ( 'keeps a record of pending updates' , async ( ) => {
160
+ vi . useFakeTimers ( )
161
+ const fakeAdapter : UpdateQueueAdapterContext = {
162
+ updateUrl : vi . fn < UpdateUrlFunction > ( ) ,
163
+ getSearchParamsSnapshot ( ) {
164
+ return new URLSearchParams ( )
165
+ }
166
+ }
167
+ const controller = new DebounceController ( )
168
+ controller . push (
169
+ {
170
+ key : 'key' ,
171
+ query : 'value' ,
172
+ options : { }
173
+ } ,
174
+ 100 ,
175
+ fakeAdapter
176
+ )
177
+ expect ( controller . getQueuedQuery ( 'key' ) ) . toEqual ( 'value' )
178
+ vi . runAllTimers ( )
179
+ expect ( controller . getQueuedQuery ( 'key' ) ) . toBeUndefined ( )
180
+ } )
181
+ it ( 'falls back to the throttle queue pending values if nothing is debounced' , ( ) => {
182
+ const throttleQueue = new ThrottledQueue ( )
183
+ throttleQueue . push ( {
184
+ key : 'key' ,
185
+ query : 'value' ,
186
+ options : { }
187
+ } )
188
+ const controller = new DebounceController ( throttleQueue )
189
+ expect ( controller . getQueuedQuery ( 'key' ) ) . toEqual ( 'value' )
190
+ } )
44
191
} )
0 commit comments