11/**
2- * @param {Storage } originalStorage - The storage object (localStorage or sessionStorage)
3- * @param {{ expiresInSeconds?: number } } [options={}] - Configuration options
2+ * @param {Storage } originalStorage
3+ * @param {{ expiresInSeconds?: number } } options
44 * @returns {Storage }
55 */
66export function wrapStorage ( originalStorage , { expiresInSeconds } = { } ) {
7- const VERSION = "1" ;
7+ const EXPIRY_PREFIX = "__exp_" ;
8+ const EXPIRY_VERSION = "v1" ;
9+ const pendingExpiries = new Map ( ) ;
810
911 /**
10- * @param {any } parsed
11- * @returns {boolean }
12+ * @param {string } key
1213 */
13- function isWrappedValue ( parsed ) {
14- return typeof parsed === "object" && parsed !== null && "__vr" in parsed ;
14+ function getExpiryKey ( key ) {
15+ return EXPIRY_PREFIX + key ;
1516 }
1617
1718 /**
18- * @param {string } value
19- * @returns {object }
19+ * @param {() => void } task
2020 */
21- function createWrappedItem ( value ) {
22- /** @type { { v: string, __vr: string, ed?: number } } */
23- const item = { v : value , __vr : VERSION } ;
24- if ( expiresInSeconds !== undefined ) {
25- item . ed = Date . now ( ) + expiresInSeconds * 1000 ;
21+ function scheduleTask ( task ) {
22+ if ( typeof requestIdleCallback !== "undefined" ) {
23+ requestIdleCallback ( task ) ;
24+ } else {
25+ task ( ) ;
2626 }
27- return item ;
2827 }
2928
3029 /**
3130 * @param {string } key
32- * @param {string } value
33- * @returns {string }
3431 */
35- function autoWrapIfNeeded ( key , value ) {
36- if ( expiresInSeconds !== undefined ) {
37- originalStorage . setItem ( key , JSON . stringify ( createWrappedItem ( value ) ) ) ;
38- }
39- return value ;
32+ function cleanupOrphanedExpiry ( key ) {
33+ scheduleTask ( ( ) => {
34+ const expiryKey = getExpiryKey ( key ) ;
35+ if ( originalStorage . getItem ( expiryKey ) && ! originalStorage . getItem ( key ) ) {
36+ originalStorage . removeItem ( expiryKey ) ;
37+ }
38+ } ) ;
4039 }
4140
4241 return {
@@ -45,26 +44,60 @@ export function wrapStorage(originalStorage, { expiresInSeconds } = {}) {
4544 * @returns {string | null }
4645 */
4746 getItem ( key ) {
47+ if ( key . startsWith ( EXPIRY_PREFIX ) ) {
48+ return originalStorage . getItem ( key ) ;
49+ }
50+
4851 const value = originalStorage . getItem ( key ) ;
49- if ( ! value ) return value ;
52+ if ( value === null ) {
53+ cleanupOrphanedExpiry ( key ) ;
54+ return null ;
55+ }
5056
51- try {
52- const parsed = JSON . parse ( value ) ;
57+ const expiryKey = getExpiryKey ( key ) ;
58+ let expiryData =
59+ originalStorage . getItem ( expiryKey ) ||
60+ ( pendingExpiries . has ( key )
61+ ? JSON . stringify ( pendingExpiries . get ( key ) )
62+ : null ) ;
5363
54- if ( isWrappedValue ( parsed ) ) {
55- if ( parsed . ed && parsed . ed < Date . now ( ) ) {
56- originalStorage . removeItem ( key ) ;
57- return null ;
64+ if ( ! expiryData && expiresInSeconds !== undefined ) {
65+ const newExpiry = {
66+ e : Date . now ( ) + expiresInSeconds * 1000 ,
67+ v : EXPIRY_VERSION ,
68+ } ;
69+ pendingExpiries . set ( key , newExpiry ) ;
70+ scheduleTask ( ( ) => {
71+ if ( pendingExpiries . has ( key ) ) {
72+ originalStorage . setItem (
73+ expiryKey ,
74+ JSON . stringify ( pendingExpiries . get ( key ) )
75+ ) ;
76+ pendingExpiries . delete ( key ) ;
5877 }
59- return parsed . v ;
60- }
78+ } ) ;
79+ return value ;
80+ }
81+
82+ if ( ! expiryData ) return value ;
6183
62- // Auto-wrap existing non-wrapped values
63- return autoWrapIfNeeded ( key , value ) ;
84+ let expiryTime ;
85+ try {
86+ expiryTime = JSON . parse ( expiryData ) . e ;
6487 } catch ( e ) {
65- // Handle non-JSON values
66- return autoWrapIfNeeded ( key , value ) ;
88+ originalStorage . removeItem ( expiryKey ) ;
89+ pendingExpiries . delete ( key ) ;
90+ return value ;
6791 }
92+
93+ if ( Date . now ( ) > expiryTime ) {
94+ originalStorage . removeItem ( key ) ;
95+ originalStorage . removeItem ( expiryKey ) ;
96+ pendingExpiries . delete ( key ) ;
97+ return null ;
98+ }
99+
100+ return value ;
68101 } ,
69102
70103 /**
@@ -78,18 +111,34 @@ export function wrapStorage(originalStorage, { expiresInSeconds } = {}) {
78111 ) ;
79112 }
80113
81- const stringValue = String ( value ) ;
82- originalStorage . setItem (
83- key ,
84- JSON . stringify ( createWrappedItem ( stringValue ) )
85- ) ;
114+ originalStorage . setItem ( key , String ( value ) ) ;
115+
116+ if ( expiresInSeconds !== undefined ) {
117+ const expiryData = {
118+ e : Date . now ( ) + expiresInSeconds * 1000 ,
119+ v : EXPIRY_VERSION ,
120+ } ;
121+ pendingExpiries . set ( key , expiryData ) ;
122+
123+ scheduleTask ( ( ) => {
124+ if ( pendingExpiries . has ( key ) ) {
125+ originalStorage . setItem (
126+ getExpiryKey ( key ) ,
127+ JSON . stringify ( pendingExpiries . get ( key ) )
128+ ) ;
129+ pendingExpiries . delete ( key ) ;
130+ }
131+ } ) ;
132+ }
86133 } ,
87134
88135 /**
89136 * @param {string } key
90137 */
91138 removeItem ( key ) {
92139 originalStorage . removeItem ( key ) ;
140+ originalStorage . removeItem ( getExpiryKey ( key ) ) ;
141+ pendingExpiries . delete ( key ) ;
93142 } ,
94143
95144 /**
@@ -102,6 +151,48 @@ export function wrapStorage(originalStorage, { expiresInSeconds } = {}) {
102151
103152 clear ( ) {
104153 originalStorage . clear ( ) ;
154+ pendingExpiries . clear ( ) ;
155+ } ,
156+
157+ cleanup ( ) {
158+ const keysToRemove = [ ] ;
159+
160+ for ( let i = 0 ; i < originalStorage . length ; i ++ ) {
161+ const key = originalStorage . key ( i ) ;
162+
163+ if ( key ?. startsWith ( EXPIRY_PREFIX ) ) {
164+ const dataKey = key . slice ( EXPIRY_PREFIX . length ) ;
165+ const expiryData = originalStorage . getItem ( key ) ;
166+
167+ let expiryTime = 0 ;
168+ if ( expiryData ) {
169+ try {
170+ expiryTime = JSON . parse ( expiryData ) . e ;
171+ } catch ( e ) {
172+ expiryTime = 0 ;
173+ }
174+ }
175+
176+ if (
177+ ! originalStorage . getItem ( dataKey ) ||
178+ ( expiryTime && Date . now ( ) > expiryTime )
179+ ) {
180+ keysToRemove . push ( key ) ;
181+ if ( originalStorage . getItem ( dataKey ) ) {
182+ keysToRemove . push ( dataKey ) ;
183+ }
184+ }
185+ }
186+ }
187+
188+ keysToRemove . forEach ( ( key ) => {
189+ originalStorage . removeItem ( key ) ;
190+ if ( key . startsWith ( EXPIRY_PREFIX ) ) {
191+ pendingExpiries . delete ( key . slice ( EXPIRY_PREFIX . length ) ) ;
192+ } else {
193+ pendingExpiries . delete ( key ) ;
194+ }
195+ } ) ;
105196 } ,
106197
107198 get length ( ) {
0 commit comments