11import { Signal } from '@segment/analytics-signals-runtime'
2- import { openDB , DBSchema , IDBPDatabase } from 'idb'
2+ import { openDB , DBSchema , IDBPDatabase , IDBPObjectStore } from 'idb'
33import { logger } from '../../lib/logger'
4+ import { WebStorage } from '../../lib/storage/web-storage'
45
56interface SignalDatabase extends DBSchema {
67 signals : {
@@ -15,77 +16,147 @@ export interface SignalPersistentStorage {
1516 clear ( ) : void
1617}
1718
18- export class SignalStore implements SignalPersistentStorage {
19+ interface IDBPDatabaseSignals extends IDBPDatabase < SignalDatabase > { }
20+ interface IDBPObjectStoreSignals
21+ extends IDBPObjectStore <
22+ SignalDatabase ,
23+ [ 'signals' ] ,
24+ 'signals' ,
25+ 'readonly' | 'readwrite' | 'versionchange'
26+ > { }
27+
28+ interface StoreSettings {
29+ maxBufferSize : number
30+ }
31+ export class SignalStoreIndexDB implements SignalPersistentStorage {
1932 static readonly DB_NAME = 'Segment Signals Buffer'
2033 static readonly STORE_NAME = 'signals'
21- private signalStore : Promise < IDBPDatabase < SignalDatabase > >
22- private signalCount = 0
34+ private db : Promise < IDBPDatabaseSignals >
2335 private maxBufferSize : number
24-
25- public length ( ) {
26- return this . signalCount
27- }
28-
36+ private sessionKeyStorage = new WebStorage ( window . sessionStorage )
2937 static deleteDatabase ( ) {
30- return indexedDB . deleteDatabase ( SignalStore . DB_NAME )
38+ return indexedDB . deleteDatabase ( SignalStoreIndexDB . DB_NAME )
3139 }
3240
33- constructor ( settings : { maxBufferSize ?: number } = { } ) {
34- this . maxBufferSize = settings . maxBufferSize ?? 50
35- this . signalStore = this . createSignalStore ( )
36- void this . initializeSignalCount ( )
41+ async getStore (
42+ permission : IDBTransactionMode ,
43+ database ?: IDBPDatabaseSignals
44+ ) : Promise < IDBPObjectStoreSignals > {
45+ const db = database ?? ( await this . db )
46+ const store = db
47+ . transaction ( SignalStoreIndexDB . STORE_NAME , permission )
48+ . objectStore ( SignalStoreIndexDB . STORE_NAME )
49+ return store
3750 }
3851
39- private getStore ( ) {
40- return this . signalStore
52+ constructor ( settings : StoreSettings ) {
53+ this . maxBufferSize = settings . maxBufferSize
54+ this . db = this . initSignalDB ( )
4155 }
4256
43- private async createSignalStore ( ) {
44- const db = await openDB < SignalDatabase > ( SignalStore . DB_NAME , 1 , {
57+ private async initSignalDB ( ) : Promise < IDBPDatabaseSignals > {
58+ const db = await openDB < SignalDatabase > ( SignalStoreIndexDB . DB_NAME , 1 , {
4559 upgrade ( db ) {
46- db . createObjectStore ( SignalStore . STORE_NAME , { autoIncrement : true } )
60+ db . createObjectStore ( SignalStoreIndexDB . STORE_NAME , {
61+ autoIncrement : true ,
62+ } )
4763 } ,
4864 } )
4965 logger . debug ( 'Signals Buffer (indexDB) initialized' )
66+ // if the signal buffer is too large, delete the oldest signals (e.g, the settings have changed)
67+ const store = await this . getStore ( 'readwrite' , db )
68+ await this . clearStoreIfNeeded ( store )
69+ await this . countAndDeleteOldestIfNeeded ( store , true )
70+ await store . transaction . done
5071 return db
5172 }
5273
53- private async initializeSignalCount ( ) {
54- const store = await this . signalStore
55- this . signalCount = await store . count ( SignalStore . STORE_NAME )
56- logger . debug (
57- `Signal count initialized with ${ this . signalCount } signals (max: ${ this . maxBufferSize } )`
58- )
74+ private async clearStoreIfNeeded ( store : IDBPObjectStoreSignals ) {
75+ // prevent the signals buffer from persisting across sessions (e.g, user closes tab and reopens)
76+ const sessionKey = 'segment_signals_db_session_key'
77+ if ( ! sessionStorage . getItem ( sessionKey ) ) {
78+ this . sessionKeyStorage . setItem ( sessionKey , true )
79+ await store . clear ! ( )
80+ logger . debug ( 'New Session, so signals buffer cleared' )
81+ }
5982 }
6083
6184 async add ( signal : Signal ) : Promise < void > {
62- const store = await this . signalStore
63- if ( this . signalCount >= this . maxBufferSize ) {
64- // Get the key of the oldest signal and delete it
65- const oldestKey = await store
66- . transaction ( SignalStore . STORE_NAME )
67- . store . getKey ( IDBKeyRange . lowerBound ( 0 ) )
68- if ( oldestKey !== undefined ) {
69- await store . delete ( SignalStore . STORE_NAME , oldestKey )
70- } else {
71- this . signalCount --
85+ const store = await this . getStore ( 'readwrite' )
86+ await store . add ! ( signal )
87+ await this . countAndDeleteOldestIfNeeded ( store )
88+ return store . transaction . done
89+ }
90+
91+ private async countAndDeleteOldestIfNeeded (
92+ store : IDBPObjectStoreSignals ,
93+ deleteMultiple = false
94+ ) : Promise < void > {
95+ let count = await store . count ( )
96+ if ( count > this . maxBufferSize ) {
97+ const cursor = await store . openCursor ( )
98+ if ( cursor ) {
99+ // delete up to maxItems
100+ if ( deleteMultiple ) {
101+ while ( count > this . maxBufferSize ) {
102+ await cursor . delete ! ( )
103+ await cursor . continue ( )
104+ count --
105+ }
106+ logger . debug (
107+ `Signals Buffer: Purged signals to max buffer size of ${ this . maxBufferSize } `
108+ )
109+ } else {
110+ // just delete the oldest item
111+ await cursor . delete ! ( )
112+ count --
113+ }
72114 }
73115 }
74- await store . add ( SignalStore . STORE_NAME , signal )
75- this . signalCount ++
76116 }
77117
78118 /**
79119 * Get list of signals from the store, with the newest signals first.
80120 */
81121 async getAll ( ) : Promise < Signal [ ] > {
82- const store = await this . getStore ( )
83- return ( await store . getAll ( SignalStore . STORE_NAME ) ) . reverse ( )
122+ const store = await this . getStore ( 'readonly' )
123+ const signals = await store . getAll ( )
124+ await store . transaction . done
125+ return signals . reverse ( )
84126 }
85127
86- async clear ( ) {
87- const store = await this . getStore ( )
88- return store . clear ( SignalStore . STORE_NAME )
128+ async clear ( ) : Promise < void > {
129+ const store = await this . getStore ( 'readwrite' )
130+ await store . clear ! ( )
131+ await store . transaction . done
132+ }
133+ }
134+
135+ export class SignalStoreSessionStorage implements SignalPersistentStorage {
136+ private readonly storageKey = 'segment_signals_buffer'
137+ private maxBufferSize : number
138+
139+ constructor ( settings : StoreSettings ) {
140+ this . maxBufferSize = settings . maxBufferSize
141+ }
142+
143+ add ( signal : Signal ) : void {
144+ const signals = this . getAll ( )
145+ signals . unshift ( signal )
146+ if ( signals . length > this . maxBufferSize ) {
147+ // delete the last one
148+ signals . splice ( - 1 )
149+ }
150+ sessionStorage . setItem ( this . storageKey , JSON . stringify ( signals ) )
151+ }
152+
153+ clear ( ) : void {
154+ sessionStorage . removeItem ( this . storageKey )
155+ }
156+
157+ getAll ( ) : Signal [ ] {
158+ const signals = sessionStorage . getItem ( this . storageKey )
159+ return signals ? JSON . parse ( signals ) : [ ]
89160 }
90161}
91162
@@ -125,14 +196,33 @@ export class SignalBuffer<
125196export interface SignalBufferSettingsConfig <
126197 T extends SignalPersistentStorage = SignalPersistentStorage
127198> {
199+ /**
200+ * Maximum number of signals to store. Only applies if no custom storage implementation is provided.
201+ */
128202 maxBufferSize ?: number
203+ /**
204+ * Choose between sessionStorage and indexDB. Only applies if no custom storage implementation is provided.
205+ * @default 'indexDB'
206+ */
207+ storageType ?: 'session' | 'indexDB'
208+ /**
209+ * Custom storage implementation
210+ * @default SignalStoreIndexDB
211+ */
129212 signalStorage ?: T
130213}
131214export const getSignalBuffer = <
132215 T extends SignalPersistentStorage = SignalPersistentStorage
133216> (
134217 settings : SignalBufferSettingsConfig < T >
135218) => {
136- const store = settings . signalStorage ?? new SignalStore ( settings )
219+ const settingsWithDefaults : StoreSettings = {
220+ maxBufferSize : 50 ,
221+ ...settings ,
222+ }
223+ const store =
224+ settings . signalStorage ?? settings . storageType === 'session'
225+ ? new SignalStoreSessionStorage ( settingsWithDefaults )
226+ : new SignalStoreIndexDB ( settingsWithDefaults )
137227 return new SignalBuffer ( store )
138228}
0 commit comments