1+ class LocalStorageManager {
2+ /**
3+ * @param {{ maxSize?: number, overflowStrategy?: 'LRU' | 'EXPIRE_FIRST' } } options
4+ */
5+ constructor ( options = { } ) {
6+ this . maxSize = options . maxSize || 4 * 1024 * 1024 ;
7+ this . overflowStrategy = options . overflowStrategy || 'EXPIRE_FIRST' ;
8+ }
9+
10+ /**
11+ * @param {string } key
12+ * @param {any } value
13+ * @param {number | null } ttl
14+ */
15+ set ( key , value , ttl = null ) {
16+ try {
17+ const item = {
18+ value,
19+ meta : {
20+ expire : ttl ? Date . now ( ) + ttl : null ,
21+ lastAccess : Date . now ( )
22+ }
23+ } ;
24+
25+ const cost = this . _calculateItemCost ( key , JSON . stringify ( item ) ) ;
26+
27+ if ( cost > this . maxSize ) throw new Error ( 'Item exceeds maximum storage size' ) ;
28+
29+ if ( this . _getTotalSize ( ) + cost > this . maxSize ) {
30+ this . _performCleanup ( cost ) ;
31+ }
32+
33+ if ( this . _getTotalSize ( ) + cost > this . maxSize ) throw new Error ( 'Item exceeds maximum storage size' ) ;
34+
35+ localStorage . setItem ( key , JSON . stringify ( item ) ) ;
36+ this . _updateSizeCache ( cost ) ;
37+ } catch ( /** @type {any } */ error ) {
38+ console . error ( 'Storage Error:' , error ) ;
39+ }
40+ }
41+
42+ /**
43+ * @param {string } key
44+ * @returns {any }
45+ */
46+ get ( key ) {
47+ const raw = localStorage . getItem ( key ) ;
48+ if ( ! raw ) return null ;
49+
50+ const item = JSON . parse ( raw ) ;
51+ if ( this . _isExpired ( item ) ) {
52+ this . remove ( key ) ;
53+ return null ;
54+ }
55+
56+ item . meta . lastAccess = Date . now ( ) ;
57+ localStorage . setItem ( key , JSON . stringify ( item ) ) ;
58+ return item . value ;
59+ }
60+
61+ /**
62+ * @param {string } key
63+ */
64+ remove ( key ) {
65+ const raw = localStorage . getItem ( key ) ;
66+ localStorage . removeItem ( key ) ;
67+ if ( raw ) this . _updateSizeCache ( - this . _calculateItemCost ( key , raw ) ) ;
68+ }
69+
70+ clear ( ) {
71+ localStorage . clear ( ) ;
72+ this . _sizeCache = 0 ;
73+ }
74+
75+ /**
76+ * @param {any } item
77+ * @returns {boolean }
78+ */
79+ _isExpired ( item ) {
80+ return item && item . meta && item . meta . expire && item . meta . expire < Date . now ( ) ;
81+ }
82+
83+ /**
84+ * @param {string } key
85+ * @param {string } valueString
86+ * @returns {number }
87+ */
88+ _calculateItemCost ( key , valueString ) {
89+ const encoder = new TextEncoder ( ) ;
90+ return encoder . encode ( key ) . length + encoder . encode ( valueString ) . length ;
91+ }
92+
93+ _getTotalSize ( ) {
94+ if ( ! this . _sizeCache ) this . _rebuildSizeCache ( ) ;
95+ return this . _sizeCache ;
96+ }
97+
98+ _rebuildSizeCache ( ) {
99+ this . _sizeCache = Array . from ( { length : localStorage . length } )
100+ . reduce ( ( total , _ , i ) => {
101+ const key = localStorage . key ( i ) ;
102+ const item = key ? localStorage . getItem ( key ) : null ;
103+ return total + ( key && item ? this . _calculateItemCost ( key , item ) : 0 ) ;
104+ } , 0 ) ;
105+ }
106+
107+ /**
108+ * @param {number } delta
109+ */
110+ _updateSizeCache ( delta ) {
111+ this . _sizeCache = ( this . _sizeCache || 0 ) + delta ;
112+ }
113+
114+ /**
115+ * @param {number } requiredSpace
116+ */
117+ _performCleanup ( requiredSpace ) {
118+ const /** @type {any[] } */ candidates = [ ] ;
119+
120+ Array . from ( { length : localStorage . length } ) . forEach ( ( _ , i ) => {
121+ const key = localStorage . key ( i ) ;
122+ const raw = key ? localStorage . getItem ( key ) : null ;
123+ if ( ! key || ! raw ) {
124+ return ;
125+ }
126+ const item = JSON . parse ( raw ) ;
127+ if ( item && item . meta ) {
128+ candidates . push ( {
129+ key,
130+ size : this . _calculateItemCost ( key , raw ) ,
131+ expire : item . meta . expire || Infinity ,
132+ lastAccess : item . meta . lastAccess
133+ } ) ;
134+ }
135+ } ) ;
136+
137+ switch ( this . overflowStrategy ) {
138+ case 'EXPIRE_FIRST' :
139+ candidates . sort ( ( a , b ) => a . expire - b . expire ) ;
140+ break ;
141+ case 'LRU' :
142+ candidates . sort ( ( a , b ) => a . lastAccess - b . lastAccess ) ;
143+ break ;
144+ }
145+
146+ let freedSpace = 0 ;
147+ while ( freedSpace < requiredSpace && candidates . length > 0 ) {
148+ const target = candidates . shift ( ) ;
149+ this . remove ( target . key ) ;
150+ freedSpace += target . size ;
151+ }
152+ }
153+ }
154+
155+ export default LocalStorageManager ;
0 commit comments