1- import type { EvaluationDetails , Hook , HookContext } from '@openfeature/web-sdk ' ;
1+ import type { EvaluationDetails , BaseHook , HookContext } from '@openfeature/core ' ;
22import { DebounceHook } from './debounce-hook' ;
3+ import type { Hook as WebSdkHook } from '@openfeature/web-sdk' ;
4+ import type { Hook as ServerSdkHook } from '@openfeature/server-sdk' ;
35
46describe ( 'DebounceHook' , ( ) => {
57 describe ( 'caching' , ( ) => {
68 afterAll ( ( ) => {
79 jest . resetAllMocks ( ) ;
810 } ) ;
911
10- const innerHook : Hook = {
12+ const innerHook : BaseHook < string , void , void > = {
1113 before : jest . fn ( ) ,
1214 after : jest . fn ( ) ,
1315 error : jest . fn ( ) ,
@@ -40,10 +42,10 @@ describe('DebounceHook', () => {
4042 calledTimesTotal : 2 , // should not have been incremented, same cache key
4143 } ,
4244 ] ) ( '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 ) ;
4749
4850 expect ( innerHook . before ) . toHaveBeenNthCalledWith ( calledTimesTotal , expect . objectContaining ( { context } ) , hints ) ;
4951 expect ( innerHook . after ) . toHaveBeenNthCalledWith (
@@ -67,7 +69,7 @@ describe('DebounceHook', () => {
6769 } ) ;
6870
6971 it ( 'stages should be cached independently' , ( ) => {
70- const innerHook : Hook = {
72+ const innerHook : BaseHook < boolean , void , void > = {
7173 before : jest . fn ( ) ,
7274 after : jest . fn ( ) ,
7375 } ;
@@ -79,8 +81,8 @@ describe('DebounceHook', () => {
7981
8082 const flagKey = 'my-flag' ;
8183
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 > , {
8486 flagKey,
8587 flagMetadata : { } ,
8688 value : true ,
@@ -98,7 +100,7 @@ describe('DebounceHook', () => {
98100 } ) ;
99101
100102 it ( 'maxCacheItems should limit size' , ( ) => {
101- const innerHook : Hook = {
103+ const innerHook : BaseHook < string , void , void > = {
102104 before : jest . fn ( ) ,
103105 } ;
104106
@@ -107,57 +109,59 @@ describe('DebounceHook', () => {
107109 maxCacheItems : 1 ,
108110 } ) ;
109111
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 > , { } ) ;
113115
114116 // every invocation should have run since we have only maxCacheItems: 1
115117 expect ( innerHook . before ) . toHaveBeenCalledTimes ( 3 ) ;
116118 } ) ;
117119
118120 it ( 'should rerun inner hook only after debounce time' , async ( ) => {
119- const innerHook : Hook = {
121+ const innerHook : BaseHook < string , void , void > = {
120122 before : jest . fn ( ) ,
121123 } ;
122124
123125 const flagKey = 'some-flag' ;
124126
125- const hook = new DebounceHook < string > ( innerHook , {
127+ const hook = new DebounceHook ( innerHook , {
126128 debounceTime : 500 ,
127129 maxCacheItems : 1 ,
128130 } ) ;
129131
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 > , { } ) ;
133135
134136 await new Promise ( ( r ) => setTimeout ( r , 1000 ) ) ;
135137
136- hook . before ( { flagKey } as HookContext , { } ) ;
138+ hook . before ( { flagKey } as HookContext < string > , { } ) ;
137139
138140 // only the first and last should have invoked the inner hook
139141 expect ( innerHook . before ) . toHaveBeenCalledTimes ( 2 ) ;
140142 } ) ;
141143
142144 it ( 'use custom supplier' , ( ) => {
143- const innerHook : Hook = {
145+ const innerHook : BaseHook < number , void , void > = {
144146 before : jest . fn ( ) ,
145147 after : jest . fn ( ) ,
146148 error : jest . fn ( ) ,
147149 finally : jest . fn ( ) ,
148150 } ;
149151
150- const context = { } ;
152+ const context = {
153+ targetingKey : 'user123' ,
154+ } ;
151155 const hints = { } ;
152156
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
155159 debounceTime : 60_000 ,
156160 maxCacheItems : 100 ,
157161 } ) ;
158162
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 ) ;
161165
162166 // since we used a constant key, the second invocation should have been cached even though the flagKey was different
163167 expect ( innerHook . before ) . toHaveBeenCalledTimes ( 1 ) ;
@@ -173,7 +177,7 @@ describe('DebounceHook', () => {
173177 timesCalled : 1 , // should be called once since we cached the error
174178 } ,
175179 ] ) ( 'should cache errors if cacheErrors set' , ( { cacheErrors, timesCalled } ) => {
176- const innerErrorHook : Hook = {
180+ const innerErrorHook : BaseHook < string [ ] , void , void > = {
177181 before : jest . fn ( ( ) => {
178182 // throw an error
179183 throw new Error ( 'fake!' ) ;
@@ -184,16 +188,141 @@ describe('DebounceHook', () => {
184188 const context = { } ;
185189
186190 // this hook caches error invocations
187- const hook = new DebounceHook < string > ( innerErrorHook , {
191+ const hook = new DebounceHook < string [ ] > ( innerErrorHook , {
188192 maxCacheItems : 100 ,
189193 debounceTime : 60_000 ,
190194 cacheErrors,
191195 } ) ;
192196
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 ( ) ;
195199
196200 expect ( innerErrorHook . before ) . toHaveBeenCalledTimes ( timesCalled ) ;
197201 } ) ;
198202 } ) ;
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+ } ) ;
199328} ) ;
0 commit comments