1
- import type { EvaluationDetails , Hook , HookContext } from '@openfeature/web-sdk ' ;
1
+ import type { EvaluationDetails , BaseHook , HookContext } from '@openfeature/core ' ;
2
2
import { DebounceHook } from './debounce-hook' ;
3
+ import type { Hook as WebSdkHook } from '@openfeature/web-sdk' ;
4
+ import type { Hook as ServerSdkHook } from '@openfeature/server-sdk' ;
3
5
4
6
describe ( 'DebounceHook' , ( ) => {
5
7
describe ( 'caching' , ( ) => {
6
8
afterAll ( ( ) => {
7
9
jest . resetAllMocks ( ) ;
8
10
} ) ;
9
11
10
- const innerHook : Hook = {
12
+ const innerHook : BaseHook < string , void , void > = {
11
13
before : jest . fn ( ) ,
12
14
after : jest . fn ( ) ,
13
15
error : jest . fn ( ) ,
@@ -40,10 +42,10 @@ describe('DebounceHook', () => {
40
42
calledTimesTotal : 2 , // should not have been incremented, same cache key
41
43
} ,
42
44
] ) ( 'should cache each stage based on supplier' , ( { flagKey, calledTimesTotal } ) => {
43
- hook . before ( { flagKey, context } as HookContext , hints ) ;
44
- hook . after ( { flagKey, context } as HookContext , evaluationDetails , hints ) ;
45
- hook . error ( { flagKey, context } as HookContext , err , hints ) ;
46
- hook . finally ( { flagKey, context } as HookContext , evaluationDetails , hints ) ;
45
+ hook . before ( { flagKey, context } as HookContext < string > , hints ) ;
46
+ hook . after ( { flagKey, context } as HookContext < string > , evaluationDetails , hints ) ;
47
+ hook . error ( { flagKey, context } as HookContext < string > , err , hints ) ;
48
+ hook . finally ( { flagKey, context } as HookContext < string > , evaluationDetails , hints ) ;
47
49
48
50
expect ( innerHook . before ) . toHaveBeenNthCalledWith ( calledTimesTotal , expect . objectContaining ( { context } ) , hints ) ;
49
51
expect ( innerHook . after ) . toHaveBeenNthCalledWith (
@@ -67,7 +69,7 @@ describe('DebounceHook', () => {
67
69
} ) ;
68
70
69
71
it ( 'stages should be cached independently' , ( ) => {
70
- const innerHook : Hook = {
72
+ const innerHook : BaseHook < boolean , void , void > = {
71
73
before : jest . fn ( ) ,
72
74
after : jest . fn ( ) ,
73
75
} ;
@@ -79,8 +81,8 @@ describe('DebounceHook', () => {
79
81
80
82
const flagKey = 'my-flag' ;
81
83
82
- hook . before ( { flagKey } as HookContext , { } ) ;
83
- hook . after ( { flagKey } as HookContext , {
84
+ hook . before ( { flagKey } as HookContext < boolean > , { } ) ;
85
+ hook . after ( { flagKey } as HookContext < boolean > , {
84
86
flagKey,
85
87
flagMetadata : { } ,
86
88
value : true ,
@@ -98,7 +100,7 @@ describe('DebounceHook', () => {
98
100
} ) ;
99
101
100
102
it ( 'maxCacheItems should limit size' , ( ) => {
101
- const innerHook : Hook = {
103
+ const innerHook : BaseHook < string , void , void > = {
102
104
before : jest . fn ( ) ,
103
105
} ;
104
106
@@ -107,57 +109,59 @@ describe('DebounceHook', () => {
107
109
maxCacheItems : 1 ,
108
110
} ) ;
109
111
110
- hook . before ( { flagKey : 'flag1' } as HookContext , { } ) ;
111
- hook . before ( { flagKey : 'flag2' } as HookContext , { } ) ;
112
- hook . before ( { flagKey : 'flag1' } as HookContext , { } ) ;
112
+ hook . before ( { flagKey : 'flag1' } as HookContext < string > , { } ) ;
113
+ hook . before ( { flagKey : 'flag2' } as HookContext < string > , { } ) ;
114
+ hook . before ( { flagKey : 'flag1' } as HookContext < string > , { } ) ;
113
115
114
116
// every invocation should have run since we have only maxCacheItems: 1
115
117
expect ( innerHook . before ) . toHaveBeenCalledTimes ( 3 ) ;
116
118
} ) ;
117
119
118
120
it ( 'should rerun inner hook only after debounce time' , async ( ) => {
119
- const innerHook : Hook = {
121
+ const innerHook : BaseHook < string , void , void > = {
120
122
before : jest . fn ( ) ,
121
123
} ;
122
124
123
125
const flagKey = 'some-flag' ;
124
126
125
- const hook = new DebounceHook < string > ( innerHook , {
127
+ const hook = new DebounceHook ( innerHook , {
126
128
debounceTime : 500 ,
127
129
maxCacheItems : 1 ,
128
130
} ) ;
129
131
130
- hook . before ( { flagKey } as HookContext , { } ) ;
131
- hook . before ( { flagKey } as HookContext , { } ) ;
132
- hook . before ( { flagKey } as HookContext , { } ) ;
132
+ hook . before ( { flagKey } as HookContext < string > , { } ) ;
133
+ hook . before ( { flagKey } as HookContext < string > , { } ) ;
134
+ hook . before ( { flagKey } as HookContext < string > , { } ) ;
133
135
134
136
await new Promise ( ( r ) => setTimeout ( r , 1000 ) ) ;
135
137
136
- hook . before ( { flagKey } as HookContext , { } ) ;
138
+ hook . before ( { flagKey } as HookContext < string > , { } ) ;
137
139
138
140
// only the first and last should have invoked the inner hook
139
141
expect ( innerHook . before ) . toHaveBeenCalledTimes ( 2 ) ;
140
142
} ) ;
141
143
142
144
it ( 'use custom supplier' , ( ) => {
143
- const innerHook : Hook = {
145
+ const innerHook : BaseHook < number , void , void > = {
144
146
before : jest . fn ( ) ,
145
147
after : jest . fn ( ) ,
146
148
error : jest . fn ( ) ,
147
149
finally : jest . fn ( ) ,
148
150
} ;
149
151
150
- const context = { } ;
152
+ const context = {
153
+ targetingKey : 'user123' ,
154
+ } ;
151
155
const hints = { } ;
152
156
153
- const hook = new DebounceHook < string > ( innerHook , {
154
- cacheKeySupplier : ( ) => 'a-silly-const-key' , // a constant key means all invocations are cached; just to test that the custom supplier is used
157
+ const hook = new DebounceHook < number > ( innerHook , {
158
+ cacheKeySupplier : ( _ , context ) => context . targetingKey , // we are caching purely based on the targetingKey in the context, so we will only ever cache one entry
155
159
debounceTime : 60_000 ,
156
160
maxCacheItems : 100 ,
157
161
} ) ;
158
162
159
- hook . before ( { flagKey : 'flag1' , context } as HookContext , hints ) ;
160
- hook . before ( { flagKey : 'flag2' , context } as HookContext , hints ) ;
163
+ hook . before ( { flagKey : 'flag1' , context } as HookContext < number > , hints ) ;
164
+ hook . before ( { flagKey : 'flag2' , context } as HookContext < number > , hints ) ;
161
165
162
166
// since we used a constant key, the second invocation should have been cached even though the flagKey was different
163
167
expect ( innerHook . before ) . toHaveBeenCalledTimes ( 1 ) ;
@@ -173,7 +177,7 @@ describe('DebounceHook', () => {
173
177
timesCalled : 1 , // should be called once since we cached the error
174
178
} ,
175
179
] ) ( 'should cache errors if cacheErrors set' , ( { cacheErrors, timesCalled } ) => {
176
- const innerErrorHook : Hook = {
180
+ const innerErrorHook : BaseHook < string [ ] , void , void > = {
177
181
before : jest . fn ( ( ) => {
178
182
// throw an error
179
183
throw new Error ( 'fake!' ) ;
@@ -184,16 +188,141 @@ describe('DebounceHook', () => {
184
188
const context = { } ;
185
189
186
190
// this hook caches error invocations
187
- const hook = new DebounceHook < string > ( innerErrorHook , {
191
+ const hook = new DebounceHook < string [ ] > ( innerErrorHook , {
188
192
maxCacheItems : 100 ,
189
193
debounceTime : 60_000 ,
190
194
cacheErrors,
191
195
} ) ;
192
196
193
- expect ( ( ) => hook . before ( { flagKey, context } as HookContext ) ) . toThrow ( ) ;
194
- expect ( ( ) => hook . before ( { flagKey, context } as HookContext ) ) . toThrow ( ) ;
197
+ expect ( ( ) => hook . before ( { flagKey, context } as HookContext < string [ ] > ) ) . toThrow ( ) ;
198
+ expect ( ( ) => hook . before ( { flagKey, context } as HookContext < string [ ] > ) ) . toThrow ( ) ;
195
199
196
200
expect ( innerErrorHook . before ) . toHaveBeenCalledTimes ( timesCalled ) ;
197
201
} ) ;
198
202
} ) ;
203
+
204
+ describe ( 'SDK compatibility' , ( ) => {
205
+ describe ( 'web-sdk hooks' , ( ) => {
206
+ it ( 'should debounce synchronous hooks' , ( ) => {
207
+ const innerWebSdkHook : WebSdkHook = {
208
+ before : jest . fn ( ) ,
209
+ after : jest . fn ( ) ,
210
+ error : jest . fn ( ) ,
211
+ finally : jest . fn ( ) ,
212
+ } ;
213
+
214
+ const hook = new DebounceHook < string > ( innerWebSdkHook , {
215
+ debounceTime : 60_000 ,
216
+ maxCacheItems : 100 ,
217
+ } ) ;
218
+
219
+ const evaluationDetails : EvaluationDetails < string > = {
220
+ value : 'testValue' ,
221
+ } as EvaluationDetails < string > ;
222
+ const err : Error = new Error ( 'fake error!' ) ;
223
+ const context = { } ;
224
+ const hints = { } ;
225
+ const flagKey = 'flag1' ;
226
+
227
+ for ( let i = 0 ; i < 2 ; i ++ ) {
228
+ hook . before ( { flagKey, context } as HookContext < string > , hints ) ;
229
+ hook . after ( { flagKey, context } as HookContext < string > , evaluationDetails , hints ) ;
230
+ hook . error ( { flagKey, context } as HookContext < string > , err , hints ) ;
231
+ hook . finally ( { flagKey, context } as HookContext < string > , evaluationDetails , hints ) ;
232
+ }
233
+
234
+ expect ( innerWebSdkHook . before ) . toHaveBeenCalledTimes ( 1 ) ;
235
+ } ) ;
236
+ } ) ;
237
+
238
+ describe ( 'server-sdk hooks' , ( ) => {
239
+ const contextKey = 'key' ;
240
+ const contextValue = 'value' ;
241
+ const evaluationContext = { [ contextKey ] : contextValue } ;
242
+ it ( 'should debounce synchronous hooks' , ( ) => {
243
+ const innerServerSdkHook : ServerSdkHook = {
244
+ before : jest . fn ( ( ) => {
245
+ return evaluationContext ;
246
+ } ) ,
247
+ after : jest . fn ( ) ,
248
+ error : jest . fn ( ) ,
249
+ finally : jest . fn ( ) ,
250
+ } ;
251
+
252
+ const hook = new DebounceHook < number > ( innerServerSdkHook , {
253
+ debounceTime : 60_000 ,
254
+ maxCacheItems : 100 ,
255
+ } ) ;
256
+
257
+ const evaluationDetails : EvaluationDetails < number > = {
258
+ value : 1337 ,
259
+ } as EvaluationDetails < number > ;
260
+ const err : Error = new Error ( 'fake error!' ) ;
261
+ const context = { } ;
262
+ const hints = { } ;
263
+ const flagKey = 'flag1' ;
264
+
265
+ for ( let i = 0 ; i < 2 ; i ++ ) {
266
+ const returnedContext = hook . before ( { flagKey, context } as HookContext < number > , hints ) ;
267
+ // make sure we return the expected context each time
268
+ expect ( returnedContext ) . toEqual ( expect . objectContaining ( evaluationContext ) ) ;
269
+ hook . after ( { flagKey, context } as HookContext < number > , evaluationDetails , hints ) ;
270
+ hook . error ( { flagKey, context } as HookContext < number > , err , hints ) ;
271
+ hook . finally ( { flagKey, context } as HookContext < number > , evaluationDetails , hints ) ;
272
+ }
273
+
274
+ // all stages should have been called only once
275
+ expect ( innerServerSdkHook . before ) . toHaveBeenCalledTimes ( 1 ) ;
276
+ expect ( innerServerSdkHook . after ) . toHaveBeenCalledTimes ( 1 ) ;
277
+ expect ( innerServerSdkHook . error ) . toHaveBeenCalledTimes ( 1 ) ;
278
+ expect ( innerServerSdkHook . finally ) . toHaveBeenCalledTimes ( 1 ) ;
279
+ } ) ;
280
+
281
+ it ( 'should debounce asynchronous hooks' , async ( ) => {
282
+ const delayMs = 100 ;
283
+ const innerServerSdkHook : ServerSdkHook = {
284
+ before : jest . fn ( ( ) => {
285
+ return new Promise ( ( resolve ) => setTimeout ( ( ) => resolve ( evaluationContext ) , delayMs ) ) ;
286
+ } ) ,
287
+ after : jest . fn ( ( ) => {
288
+ return new Promise ( ( resolve ) => setTimeout ( ( ) => resolve ( ) , delayMs ) ) ;
289
+ } ) ,
290
+ error : jest . fn ( ( ) => {
291
+ return new Promise ( ( resolve ) => setTimeout ( ( ) => resolve ( ) , delayMs ) ) ;
292
+ } ) ,
293
+ finally : jest . fn ( ( ) => {
294
+ return new Promise ( ( resolve ) => setTimeout ( ( ) => resolve ( ) , delayMs ) ) ;
295
+ } ) ,
296
+ } ;
297
+
298
+ const hook = new DebounceHook < number > ( innerServerSdkHook , {
299
+ debounceTime : 60_000 ,
300
+ maxCacheItems : 100 ,
301
+ } ) ;
302
+
303
+ const evaluationDetails : EvaluationDetails < number > = {
304
+ value : 1337 ,
305
+ } as EvaluationDetails < number > ;
306
+ const err : Error = new Error ( 'fake error!' ) ;
307
+ const context = { } ;
308
+ const hints = { } ;
309
+ const flagKey = 'flag1' ;
310
+
311
+ for ( let i = 0 ; i < 2 ; i ++ ) {
312
+ const returnedContext = await hook . before ( { flagKey, context } as HookContext < number > , hints ) ;
313
+ // make sure we return the expected context each time
314
+ expect ( returnedContext ) . toEqual ( expect . objectContaining ( evaluationContext ) ) ;
315
+ await hook . after ( { flagKey, context } as HookContext < number > , evaluationDetails , hints ) ;
316
+ await hook . error ( { flagKey, context } as HookContext < number > , err , hints ) ;
317
+ await hook . finally ( { flagKey, context } as HookContext < number > , evaluationDetails , hints ) ;
318
+ }
319
+
320
+ // each stage should have been called only once
321
+ expect ( innerServerSdkHook . before ) . toHaveBeenCalledTimes ( 1 ) ;
322
+ expect ( innerServerSdkHook . after ) . toHaveBeenCalledTimes ( 1 ) ;
323
+ expect ( innerServerSdkHook . error ) . toHaveBeenCalledTimes ( 1 ) ;
324
+ expect ( innerServerSdkHook . finally ) . toHaveBeenCalledTimes ( 1 ) ;
325
+ } ) ;
326
+ } ) ;
327
+ } ) ;
199
328
} ) ;
0 commit comments