From 37b82c37e25bbcfe3ac1f2aac88692ba4a8aa7f2 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Mon, 17 Nov 2025 08:58:54 +0600 Subject: [PATCH 1/2] [FSSDK-12028] use save time for event expiration in event store --- lib/event_processor/event_store.spec.ts | 28 ++++++++++++++----------- lib/event_processor/event_store.ts | 17 ++++++++++----- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/lib/event_processor/event_store.spec.ts b/lib/event_processor/event_store.spec.ts index e1effb74f..4f907aa47 100644 --- a/lib/event_processor/event_store.spec.ts +++ b/lib/event_processor/event_store.spec.ts @@ -201,7 +201,7 @@ describe('EventStore', () => { expect(await store.getKeys()).toEqual([]); }); - it('should resave events without expiresAt on get', async () => { + it('should resave events without time information on get', async () => { const ttl = 120_000; const { mockStore, store } = getEventStore({ ttl }); @@ -218,12 +218,12 @@ describe('EventStore', () => { return originalSet(key, value); } - // Simulate old stored event without expiresAt - const eventWithoutExpiresAt: StoredEvent = { + // Simulate old stored event without time info + const eventWithoutTime: StoredEvent = { id: value.id, event: value.event, }; - return originalSet(key, eventWithoutExpiresAt); + return originalSet(key, eventWithoutTime); }); await store.set('test', event); @@ -237,8 +237,10 @@ describe('EventStore', () => { const secondCall = setSpy.mock.calls[1]; - expect(secondCall[1].expiresAt).toBeDefined(); - expect(secondCall[1].expiresAt!).toBeGreaterThanOrEqual(Date.now() + ttl - 10); + expect(secondCall[1]._time).toBeDefined(); + expect(secondCall[1]._time?.storedAt).toBeLessThanOrEqual(Date.now()); + expect(secondCall[1]._time?.storedAt).toBeGreaterThanOrEqual(Date.now() - 10); + expect(secondCall[1]._time?.ttl).toBe(ttl); }); it('should store event when key expires after store being full', async () => { @@ -317,7 +319,7 @@ describe('EventStore', () => { await expect(store.getKeys()).resolves.toEqual(['key-2']); }); - it('should resave events without expiresAt during getBatched', async () => { + it('should resave events without time information during getBatched', async () => { const ttl = 120_000; const { mockStore, store } = getEventStore({ ttl }); const event: EventWithId = { id: '1', event: createImpressionEvent('test') }; @@ -330,12 +332,12 @@ describe('EventStore', () => { return originalSet(key, value); } - // Simulate old stored event without expiresAt - const eventWithoutExpiresAt: StoredEvent = { + // Simulate old stored event without time information + const eventWithoutTime: StoredEvent = { id: value.id, event: value.event, }; - return originalSet(key, eventWithoutExpiresAt); + return originalSet(key, eventWithoutTime); }); await store.set('key-1', event); @@ -352,8 +354,10 @@ describe('EventStore', () => { const secondCall = setSpy.mock.calls[1]; - expect(secondCall[1].expiresAt).toBeDefined(); - expect(secondCall[1].expiresAt!).toBeGreaterThanOrEqual(Date.now() + ttl - 10); + expect(secondCall[1]._time).toBeDefined(); + expect(secondCall[1]._time?.storedAt).toBeLessThanOrEqual(Date.now()); + expect(secondCall[1]._time?.storedAt).toBeGreaterThanOrEqual(Date.now() - 10); + expect(secondCall[1]._time?.ttl).toBe(ttl); }); it('should store event when keys expire during getBatched after store being full', async () => { diff --git a/lib/event_processor/event_store.ts b/lib/event_processor/event_store.ts index d8390020e..bfbf7a5a5 100644 --- a/lib/event_processor/event_store.ts +++ b/lib/event_processor/event_store.ts @@ -14,7 +14,10 @@ import { Maybe } from "../utils/type"; import { EventWithId } from "./batch_event_processor"; export type StoredEvent = EventWithId & { - expiresAt?: number; + _time?: { + storedAt: number; + ttl: number; + } }; const identity = (v: T): T => v; @@ -97,7 +100,7 @@ export class EventStore extends AsyncStoreWithBatchedGet implements // still keep the stored event count below maxSize (it will underfill the store). // next getKeys() should fix the discrepency. this.keys?.add(key); - return this.store.set(key, { ...event, expiresAt: Date.now() + this.ttl }); + return this.store.set(key, { ...event, _time: { storedAt: Date.now(), ttl: this.ttl } }); } private processStoredEvent(key: string, value: StoredEvent | undefined): Maybe { @@ -107,13 +110,17 @@ export class EventStore extends AsyncStoreWithBatchedGet implements // they will not have the storedAt time, update them with the current time // before returning - if (value.expiresAt === undefined) { - value.expiresAt = Date.now() + this.ttl; + if (value._time === undefined) { + value._time = { storedAt: Date.now(), ttl: this.ttl }; this.set(key, value).catch(() => {}); return value; } - if (value.expiresAt <= Date.now()) { + // use the ttl of the current store even if the stored event has a different ttl + // this ensures that if the store ttl is reduced, old events will also expire sooner + // and if the store ttl is increased, old events will stay longer + // the ttl at the time of save is still stored with the event for potential future use + if (value._time.storedAt + this.ttl <= Date.now()) { this.remove(key).catch(() => {}); return undefined; } From bcb2af3b0632a41ce5f762c9e2593b198c552e54 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Mon, 17 Nov 2025 18:17:07 +0600 Subject: [PATCH 2/2] semicolon --- lib/event_processor/event_store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/event_processor/event_store.ts b/lib/event_processor/event_store.ts index bfbf7a5a5..b55520a19 100644 --- a/lib/event_processor/event_store.ts +++ b/lib/event_processor/event_store.ts @@ -17,7 +17,7 @@ export type StoredEvent = EventWithId & { _time?: { storedAt: number; ttl: number; - } + }; }; const identity = (v: T): T => v;