@@ -30,12 +30,23 @@ const getLocalStorageSize = (storage = window.localStorage, encoder = new TextEn
30
30
return Object . entries ( storage ) . reduce ( ( acc , [ key , value ] ) => acc + encoder . encode ( key ) . length + encoder . encode ( value ) . length , 0 ) ;
31
31
} ;
32
32
33
+ export type StorageAreaWrapper = {
34
+ name : string ;
35
+ getBytesInUse : ( ) => Promise < number > ;
36
+ getAll : < T > ( regex ?: string | RegExp ) => Promise < T > ;
37
+ get : < T > ( key : string ) => Promise < T > ;
38
+ set : < T > ( key : string , value : T ) => Promise < void > ;
39
+ remove : ( key : string ) => Promise < void > ;
40
+ removeAll : ( regex : string | RegExp ) => Promise < void > ;
41
+ clear : ( ) => Promise < void > ;
42
+ } ;
43
+
33
44
/**
34
45
* This function is used to wrap the storage areas to provide type inference and a more convenient interface.
35
46
* @param area The storage area to wrap.
36
47
* @param name The name of the storage area.
37
48
*/
38
- export const storageWrapper = ( area : chrome . storage . StorageArea , name : string ) => {
49
+ export const storageWrapper = ( area : chrome . storage . StorageArea , name : string ) : StorageAreaWrapper => {
39
50
if ( ! globalThis ?. chrome ?. storage ) {
40
51
console . warn ( 'Storage API is not available, using local storage instead.' ) ;
41
52
@@ -62,6 +73,7 @@ export const storageWrapper = (area: chrome.storage.StorageArea, name: string) =
62
73
63
74
window . trakt = { ...window . trakt , [ name ] : storage } ;
64
75
return {
76
+ name,
65
77
getBytesInUse : async ( ) : Promise < number > => getLocalStorageSize ( window . localStorage ) ,
66
78
getAll : async < T > ( regex ?: string | RegExp ) : Promise < T > => ( regex ? filterObject ( storage . values , regex ) : storage . values ) as T ,
67
79
get : async < T > ( key : string ) : Promise < T > => storage . values [ key ] as T ,
@@ -74,6 +86,7 @@ export const storageWrapper = (area: chrome.storage.StorageArea, name: string) =
74
86
} ;
75
87
}
76
88
return {
89
+ name,
77
90
getBytesInUse : ( ) : Promise < number > => area . getBytesInUse ( ) ,
78
91
getAll : < T > ( regex ?: string | RegExp ) : Promise < T > => area . get ( ) . then ( data => ( regex ? filterObject ( data , regex ) : data ) as T ) ,
79
92
get : < T > ( key : string ) : Promise < T > => area . get ( key ) . then ( ( { [ key ] : value } ) => value ) ,
@@ -104,32 +117,76 @@ export const storage = {
104
117
session : storageWrapper ( sessionStorage , 'session' ) ,
105
118
} ;
106
119
107
- export const defaultMaxLocalStorageSize = 10485760 ;
120
+ /**
121
+ * Determines whether an error is a QuotaExceededError.
122
+ *
123
+ * Browsers love throwing slightly different variations of QuotaExceededError
124
+ * (this is especially true for old browsers/versions), so we need to check
125
+ * different fields and values to ensure we cover every edge-case.
126
+ *
127
+ * @param err - The error to check
128
+ * @return Is the error a QuotaExceededError?
129
+ */
130
+ export const isQuotaExceededError = ( err : unknown ) : boolean => {
131
+ if ( ! ( err instanceof DOMException ) ) return false ;
132
+ if ( err . name === 'QuotaExceededError' ) return true ;
133
+ if ( err . name === 'NS_ERROR_DOM_QUOTA_REACHED' ) return true ; // Firefox
134
+ if ( err . code === 22 ) return true ;
135
+ return err . code === 1014 ; // Firefox
136
+ } ;
108
137
109
- export const localCache : < T > ( key : string , value : T , regex ?: string | RegExp ) => Promise < void > = async ( key , value , regex ) => {
110
- let inUse = await storage . local . getBytesInUse ( ) ;
138
+ /**
139
+ * The default maximum size of the local/session storage.
140
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria
141
+ */
142
+ export const defaultMaxLocalStorageSize = 10485760 / 2 ;
143
+
144
+ /**
145
+ * Wrapper around the storage set method with additional checks for the size of the storage and storage eviction.
146
+ * @param key The key to store the value under.
147
+ * @param value The payload to store.
148
+ * @param regex An optional regex to filter the keys to remove when the storage is full.
149
+ * @param area The storage area to use.
150
+ */
151
+ export const setStorageWrapper : < T > ( key : string , value : T , regex ?: string | RegExp , area ?: StorageAreaWrapper ) => Promise < void > = async (
152
+ key ,
153
+ value ,
154
+ regex ,
155
+ area = storage . local ,
156
+ ) => {
157
+ let inUse = await area . getBytesInUse ( ) ;
111
158
112
159
const max = globalThis ?. chrome ?. storage ?. local . QUOTA_BYTES ?? defaultMaxLocalStorageSize ;
113
160
114
161
const encoder = new TextEncoder ( ) ;
115
162
const payload = encoder . encode ( JSON . stringify ( value ) ) . length ;
116
163
117
164
if ( payload > max ) {
118
- console . warn ( 'Payload is too large to store in local storage.' , { payload, max, inUse } ) ;
165
+ console . warn ( 'Payload is too large to store in local storage.' , { payload, max, inUse, regex , area : area . name } ) ;
119
166
return Promise . resolve ( ) ;
120
167
}
121
168
122
169
if ( inUse + payload >= max ) {
123
- console . warn ( 'Local storage is full, clearing cache.' , { payload, max, inUse } ) ;
124
- if ( regex ) await storage . local . removeAll ( regex ) ;
125
- else await storage . local . clear ( ) ;
170
+ console . warn ( 'Local storage is full, clearing cache.' , { payload, max, inUse, regex , area : area . name } ) ;
171
+ if ( regex ) await area . removeAll ( regex ) ;
172
+ else await area . clear ( ) ;
126
173
}
127
174
128
- inUse = await storage . local . getBytesInUse ( ) ;
175
+ inUse = await area . getBytesInUse ( ) ;
129
176
if ( inUse + payload >= max ) {
130
- console . warn ( 'Local storage is still full, skipping cache.' , { payload, max, inUse } ) ;
177
+ console . warn ( 'Local storage is still full, skipping cache.' , { payload, max, inUse, regex , area : area . name } ) ;
131
178
return Promise . resolve ( ) ;
132
179
}
133
180
134
- return storage . local . set ( key , value ) ;
181
+ try {
182
+ return area . set ( key , value ) ;
183
+ } catch ( error ) {
184
+ if ( isQuotaExceededError ( error ) ) {
185
+ console . warn ( 'Local storage is full, clearing cache.' , { payload, max, inUse, regex, area : area . name } ) ;
186
+ if ( regex ) await area . removeAll ( regex ) ;
187
+ else await area . clear ( ) ;
188
+ return area . set ( key , value ) ;
189
+ }
190
+ throw error ;
191
+ }
135
192
} ;
0 commit comments