1
1
import { Signal } from '@segment/analytics-signals-runtime'
2
- import { openDB , DBSchema , IDBPDatabase } from 'idb'
2
+ import { openDB , DBSchema , IDBPDatabase , IDBPObjectStore } from 'idb'
3
3
import { logger } from '../../lib/logger'
4
+ import { WebStorage } from '../../lib/storage/web-storage'
4
5
5
6
interface SignalDatabase extends DBSchema {
6
7
signals : {
@@ -15,77 +16,147 @@ export interface SignalPersistentStorage {
15
16
clear ( ) : void
16
17
}
17
18
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 {
19
32
static readonly DB_NAME = 'Segment Signals Buffer'
20
33
static readonly STORE_NAME = 'signals'
21
- private signalStore : Promise < IDBPDatabase < SignalDatabase > >
22
- private signalCount = 0
34
+ private db : Promise < IDBPDatabaseSignals >
23
35
private maxBufferSize : number
24
-
25
- public length ( ) {
26
- return this . signalCount
27
- }
28
-
36
+ private sessionKeyStorage = new WebStorage ( window . sessionStorage )
29
37
static deleteDatabase ( ) {
30
- return indexedDB . deleteDatabase ( SignalStore . DB_NAME )
38
+ return indexedDB . deleteDatabase ( SignalStoreIndexDB . DB_NAME )
31
39
}
32
40
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
37
50
}
38
51
39
- private getStore ( ) {
40
- return this . signalStore
52
+ constructor ( settings : StoreSettings ) {
53
+ this . maxBufferSize = settings . maxBufferSize
54
+ this . db = this . initSignalDB ( )
41
55
}
42
56
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 , {
45
59
upgrade ( db ) {
46
- db . createObjectStore ( SignalStore . STORE_NAME , { autoIncrement : true } )
60
+ db . createObjectStore ( SignalStoreIndexDB . STORE_NAME , {
61
+ autoIncrement : true ,
62
+ } )
47
63
} ,
48
64
} )
49
65
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
50
71
return db
51
72
}
52
73
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
+ }
59
82
}
60
83
61
84
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
+ }
72
114
}
73
115
}
74
- await store . add ( SignalStore . STORE_NAME , signal )
75
- this . signalCount ++
76
116
}
77
117
78
118
/**
79
119
* Get list of signals from the store, with the newest signals first.
80
120
*/
81
121
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 ( )
84
126
}
85
127
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 ) : [ ]
89
160
}
90
161
}
91
162
@@ -125,14 +196,33 @@ export class SignalBuffer<
125
196
export interface SignalBufferSettingsConfig <
126
197
T extends SignalPersistentStorage = SignalPersistentStorage
127
198
> {
199
+ /**
200
+ * Maximum number of signals to store. Only applies if no custom storage implementation is provided.
201
+ */
128
202
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
+ */
129
212
signalStorage ?: T
130
213
}
131
214
export const getSignalBuffer = <
132
215
T extends SignalPersistentStorage = SignalPersistentStorage
133
216
> (
134
217
settings : SignalBufferSettingsConfig < T >
135
218
) => {
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 )
137
227
return new SignalBuffer ( store )
138
228
}
0 commit comments