1313 * See the License for the specific language governing permissions and
1414 * limitations under the License.
1515 */
16- import { vi , describe , it , expect , beforeEach } from 'vitest' ;
17- import { getMockAsyncCache , getMockSyncCache } from '../tests/mock/mock_cache' ;
18- import { SyncStore } from '../utils/cache/store' ;
16+ import { vi , describe , it , expect } from 'vitest' ;
17+ import { getMockAsyncCache } from '../tests/mock/mock_cache' ;
1918import { EventWithId } from './batch_event_processor' ;
2019import { EventStore , StoredEvent } from './event_store' ;
2120import { createImpressionEvent } from '../tests/mock/create_event' ;
2221
2322import { DEFAULT_MAX_EVENTS_IN_STORE } from './event_store' ;
2423import { exhaustMicrotasks } from '../tests/testUtils' ;
24+ import { EVENT_STORE_FULL } from '../message/log_message' ;
25+ import { OptimizelyError } from '../error/optimizly_error' ;
2526
2627type TestStoreConfig = {
2728 maxSize ?: number ;
@@ -68,7 +69,7 @@ describe('EventStore', () => {
6869 expect ( saved2 ) . toEqual ( expect . objectContaining ( event ) ) ;
6970 } ) ;
7071
71- it ( 'should return all keys from getKeys() ' , async ( ) => {
72+ it ( 'should return all keys from getKeys' , async ( ) => {
7273 const { store } = getEventStore ( ) ;
7374 const event : EventWithId = {
7475 id : '1' ,
@@ -88,7 +89,7 @@ describe('EventStore', () => {
8889 expect ( savedKeys ) . toEqual ( keys ) ;
8990 } ) ;
9091
91- it ( 'should limit the number of saved keys' , async ( ) => {
92+ it ( 'should limit the number of saved keys when set is called concurrently ' , async ( ) => {
9293 const { store } = getEventStore ( ) ;
9394
9495 const event : EventWithId = {
@@ -120,6 +121,24 @@ describe('EventStore', () => {
120121 expect ( savedKeys ) . toEqual ( keys . slice ( 0 , DEFAULT_MAX_EVENTS_IN_STORE ) ) ;
121122 } ) ;
122123
124+ it ( 'should limit the number of saved keys when set is called serially' , async ( ) => {
125+ const { store } = getEventStore ( { maxSize : 2 } ) ;
126+
127+ const event : EventWithId = {
128+ id : '1' ,
129+ event : createImpressionEvent ( 'test' ) ,
130+ }
131+
132+ await expect ( store . set ( 'event-1' , event ) ) . resolves . not . toThrow ( ) ;
133+ await expect ( store . set ( 'event-2' , event ) ) . resolves . not . toThrow ( ) ;
134+ await expect ( store . set ( 'event-3' , event ) ) . rejects . toThrow ( new OptimizelyError ( EVENT_STORE_FULL , event . id ) ) ;
135+
136+ const savedKeys = await store . getKeys ( ) ;
137+ savedKeys . sort ( ) ;
138+
139+ expect ( savedKeys ) . toEqual ( [ 'event-1' , 'event-2' ] ) ;
140+ } ) ;
141+
123142 it ( 'should save keys again when the number of stored events drops below maxSize' , async ( ) => {
124143 const { store } = getEventStore ( ) ;
125144
@@ -182,7 +201,7 @@ describe('EventStore', () => {
182201 expect ( await store . getKeys ( ) ) . toEqual ( [ ] ) ;
183202 } ) ;
184203
185- it ( 'should resave events without expireAt on get' , async ( ) => {
204+ it ( 'should resave events without expiresAt on get' , async ( ) => {
186205 const ttl = 120_000 ;
187206 const { mockStore, store } = getEventStore ( { ttl } ) ;
188207
@@ -191,7 +210,6 @@ describe('EventStore', () => {
191210 event : createImpressionEvent ( 'test' ) ,
192211 }
193212
194-
195213 const originalSet = mockStore . set . bind ( mockStore ) ;
196214
197215 let call = 0 ;
@@ -200,12 +218,12 @@ describe('EventStore', () => {
200218 return originalSet ( key , value ) ;
201219 }
202220
203- // Simulate old stored event without expireAt
204- const eventWithoutExpireAt : StoredEvent = {
221+ // Simulate old stored event without expiresAt
222+ const eventWithoutExpiresAt : StoredEvent = {
205223 id : value . id ,
206224 event : value . event ,
207225 } ;
208- return originalSet ( key , eventWithoutExpireAt ) ;
226+ return originalSet ( key , eventWithoutExpiresAt ) ;
209227 } ) ;
210228
211229 await store . set ( 'test' , event ) ;
@@ -222,4 +240,179 @@ describe('EventStore', () => {
222240 expect ( secondCall [ 1 ] . expiresAt ) . toBeDefined ( ) ;
223241 expect ( secondCall [ 1 ] . expiresAt ! ) . toBeGreaterThanOrEqual ( Date . now ( ) + ttl - 10 ) ;
224242 } ) ;
243+
244+ it ( 'should store event when key expires after store being full' , async ( ) => {
245+ const ttl = 100 ;
246+ const { store } = getEventStore ( { ttl, maxSize : 2 } ) ;
247+
248+ const event : EventWithId = {
249+ id : '1' ,
250+ event : createImpressionEvent ( 'test' ) ,
251+ }
252+
253+ await store . set ( 'event-1' , event ) ;
254+ await store . set ( 'event-2' , event ) ;
255+ await expect ( store . set ( 'event-3' , event ) ) . rejects . toThrow ( ) ;
256+
257+ expect ( await store . getKeys ( ) . then ( keys => keys . sort ( ) ) ) . toEqual ( [ 'event-1' , 'event-2' ] ) ;
258+
259+ // wait for ttl to expire
260+ await new Promise ( resolve => setTimeout ( resolve , ttl + 50 ) ) ;
261+
262+ // both events should be expired now
263+ expect ( await store . get ( 'event-1' ) ) . toBeUndefined ( ) ;
264+ expect ( await store . get ( 'event-2' ) ) . toBeUndefined ( ) ;
265+
266+ // should be able to add new events now
267+ await expect ( store . set ( 'event-3' , event ) ) . resolves . not . toThrow ( ) ;
268+ await expect ( store . set ( 'event-4' , event ) ) . resolves . not . toThrow ( ) ;
269+
270+ const savedEvent3 = await store . get ( 'event-3' ) ;
271+ expect ( savedEvent3 ) . toEqual ( expect . objectContaining ( event ) ) ;
272+ const savedEvent4 = await store . get ( 'event-4' ) ;
273+ expect ( savedEvent4 ) . toEqual ( expect . objectContaining ( event ) ) ;
274+ } ) ;
275+
276+ it ( 'should return all requested events correctly from getBatched' , async ( ) => {
277+ const { store } = getEventStore ( ) ;
278+ const event1 : EventWithId = { id : '1' , event : createImpressionEvent ( 'test-1' ) } ;
279+ const event2 : EventWithId = { id : '2' , event : createImpressionEvent ( 'test-2' ) } ;
280+ const event3 : EventWithId = { id : '3' , event : createImpressionEvent ( 'test-3' ) } ;
281+
282+ await store . set ( 'key-1' , event1 ) ;
283+ await store . set ( 'key-2' , event2 ) ;
284+ await store . set ( 'key-3' , event3 ) ;
285+
286+ const results = await store . getBatched ( [ 'key-3' , 'key-1' , 'key-2' , 'key-4' ] ) ;
287+
288+ expect ( results ) . toHaveLength ( 4 ) ;
289+ expect ( results [ 0 ] ) . toEqual ( expect . objectContaining ( event3 ) ) ;
290+ expect ( results [ 1 ] ) . toEqual ( expect . objectContaining ( event1 ) ) ;
291+ expect ( results [ 2 ] ) . toEqual ( expect . objectContaining ( event2 ) ) ;
292+ expect ( results [ 3 ] ) . toBeUndefined ( ) ;
293+ } ) ;
294+
295+
296+ it ( 'should handle expired events in getBatched' , async ( ) => {
297+ const ttl = 100 ;
298+ const { store } = getEventStore ( { ttl } ) ;
299+ const event1 : EventWithId = { id : '1' , event : createImpressionEvent ( 'test-1' ) } ;
300+ const event2 : EventWithId = { id : '2' , event : createImpressionEvent ( 'test-2' ) } ;
301+
302+ await store . set ( 'key-1' , event1 ) ;
303+ await new Promise ( resolve => setTimeout ( resolve , 70 ) ) ;
304+ await store . set ( 'key-2' , event2 ) ;
305+
306+ // wait for first key to expire but not the second
307+ await new Promise ( resolve => setTimeout ( resolve , 50 ) ) ;
308+
309+ const results = await store . getBatched ( [ 'key-1' , 'key-2' ] ) ;
310+
311+ expect ( results ) . toHaveLength ( 2 ) ;
312+ expect ( results [ 0 ] ) . toBeUndefined ( ) ;
313+ expect ( results [ 1 ] ) . toEqual ( expect . objectContaining ( event2 ) ) ;
314+
315+ await expect ( store . getKeys ( ) ) . resolves . toEqual ( [ 'key-2' ] ) ;
316+ } ) ;
317+
318+ it ( 'should resave events without expiresAt during getBatched' , async ( ) => {
319+ const ttl = 120_000 ;
320+ const { mockStore, store } = getEventStore ( { ttl } ) ;
321+ const event : EventWithId = { id : '1' , event : createImpressionEvent ( 'test' ) } ;
322+
323+ const originalSet = mockStore . set . bind ( mockStore ) ;
324+
325+ let call = 0 ;
326+ const setSpy = vi . spyOn ( mockStore , 'set' ) . mockImplementation ( async ( key : string , value : StoredEvent ) => {
327+ if ( call ++ > 0 ) {
328+ return originalSet ( key , value ) ;
329+ }
330+
331+ // Simulate old stored event without expiresAt
332+ const eventWithoutExpiresAt : StoredEvent = {
333+ id : value . id ,
334+ event : value . event ,
335+ } ;
336+ return originalSet ( key , eventWithoutExpiresAt ) ;
337+ } ) ;
338+
339+ await store . set ( 'key-1' , event ) ;
340+ await store . set ( 'key-2' , event ) ;
341+
342+ const results = await store . getBatched ( [ 'key-1' , 'key-2' ] ) ;
343+
344+ expect ( results ) . toHaveLength ( 2 ) ;
345+ expect ( results [ 0 ] ) . toEqual ( expect . objectContaining ( event ) ) ;
346+ expect ( results [ 1 ] ) . toEqual ( expect . objectContaining ( event ) ) ;
347+
348+ await exhaustMicrotasks ( ) ;
349+ expect ( setSpy ) . toHaveBeenCalledTimes ( 3 ) ;
350+
351+ const secondCall = setSpy . mock . calls [ 1 ] ;
352+
353+ expect ( secondCall [ 1 ] . expiresAt ) . toBeDefined ( ) ;
354+ expect ( secondCall [ 1 ] . expiresAt ! ) . toBeGreaterThanOrEqual ( Date . now ( ) + ttl - 10 ) ;
355+ } ) ;
356+
357+ it ( 'should store event when keys expire during getBatched after store being full' , async ( ) => {
358+ const ttl = 100 ;
359+ const { store } = getEventStore ( { ttl, maxSize : 2 } ) ;
360+
361+ const event : EventWithId = {
362+ id : '1' ,
363+ event : createImpressionEvent ( 'test' ) ,
364+ }
365+
366+ await store . set ( 'event-1' , event ) ;
367+ await new Promise ( resolve => setTimeout ( resolve , 70 ) ) ;
368+ await store . set ( 'event-2' , event ) ;
369+ await expect ( store . set ( 'event-3' , event ) ) . rejects . toThrow ( ) ;
370+
371+ expect ( await store . getKeys ( ) . then ( keys => keys . sort ( ) ) ) . toEqual ( [ 'event-1' , 'event-2' ] ) ;
372+
373+ // wait for the first event to expire
374+ await new Promise ( resolve => setTimeout ( resolve , 50 ) ) ;
375+
376+ const results = await store . getBatched ( [ 'event-1' , 'event-2' ] ) ;
377+
378+ expect ( results ) . toHaveLength ( 2 ) ;
379+ expect ( results [ 0 ] ) . toBeUndefined ( ) ;
380+ expect ( results [ 1 ] ) . toEqual ( expect . objectContaining ( event ) ) ;
381+
382+ await new Promise ( resolve => setTimeout ( resolve ) ) ;
383+
384+ // should be able to add new event now
385+ await expect ( store . set ( 'event-3' , event ) ) . resolves . not . toThrow ( ) ;
386+ const savedEvent = await store . get ( 'event-3' ) ;
387+ expect ( savedEvent ) . toEqual ( expect . objectContaining ( event ) ) ;
388+ expect ( await store . getKeys ( ) . then ( keys => keys . sort ( ) ) ) . toEqual ( [ 'event-2' , 'event-3' ] ) ;
389+ } ) ;
390+
391+ it ( 'should restore in-memory key consistency after getKeys is called' , async ( ) => {
392+ const { mockStore, store } = getEventStore ( { maxSize : 2 } ) ;
393+ const event : EventWithId = { id : '1' , event : createImpressionEvent ( 'test' ) } ;
394+
395+ const originalSet = mockStore . set . bind ( mockStore ) ;
396+
397+ let call = 0 ;
398+ vi . spyOn ( mockStore , 'set' ) . mockImplementation ( async ( key : string , value : StoredEvent ) => {
399+ // only the seconde set call should fail
400+ if ( call ++ != 1 ) return originalSet ( key , value ) ;
401+ return Promise . reject ( new Error ( 'Simulated set failure' ) ) ;
402+ } ) ;
403+
404+ await expect ( store . set ( 'key-1' , event ) ) . resolves . not . toThrow ( ) ;
405+ // this should fail, but in memory key list will become full
406+ await expect ( store . set ( 'key-2' , event ) ) . rejects . toThrow ( 'Simulated set failure' ) ;
407+ await expect ( store . set ( 'key-3' , event ) ) . rejects . toThrow ( new OptimizelyError ( EVENT_STORE_FULL , event . id ) ) ;
408+
409+
410+ let keys = await store . getKeys ( ) ;
411+ expect ( keys . sort ( ) ) . toEqual ( [ 'key-1' ] ) ;
412+
413+ await expect ( store . set ( 'key-3' , event ) ) . resolves . not . toThrow ( ) ;
414+
415+ keys = await store . getKeys ( ) ;
416+ expect ( keys . sort ( ) ) . toEqual ( [ 'key-1' , 'key-3' ] ) ;
417+ } ) ;
225418} ) ;
0 commit comments