44 * @returns {Storage }
55 */
66export function wrapStorage ( originalStorage , { expiresInSeconds } = { } ) {
7- const VERSION = "1" ;
7+ const EXPIRY_PREFIX = "__exp_" ;
8+ const EXPIRY_VERSION = "v1" ;
89
910 /**
10- * @param {any } parsed
11- * @returns {boolean }
12- */
13- function isWrappedValue ( parsed ) {
14- return typeof parsed === "object" && parsed !== null && "__vr" in parsed ;
15- }
16-
17- /**
18- * @param {string } value
19- * @returns {object }
11+ * @param {string } key
12+ * @returns {string }
2013 */
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 ;
26- }
27- return item ;
14+ function getExpiryKey ( key ) {
15+ return EXPIRY_PREFIX + key ;
2816 }
2917
3018 /**
3119 * @param {string } key
32- * @param {string } value
33- * @returns {string }
3420 */
35- function autoWrapIfNeeded ( key , value ) {
36- if ( expiresInSeconds !== undefined ) {
37- originalStorage . setItem ( key , JSON . stringify ( createWrappedItem ( value ) ) ) ;
21+ function cleanupOrphanedExpiry ( key ) {
22+ const cleanup = ( ) => {
23+ const expiryKey = getExpiryKey ( key ) ;
24+ if ( originalStorage . getItem ( expiryKey ) && ! originalStorage . getItem ( key ) ) {
25+ originalStorage . removeItem ( expiryKey ) ;
26+ }
27+ } ;
28+
29+ if ( typeof requestIdleCallback !== "undefined" ) {
30+ requestIdleCallback ( cleanup ) ;
31+ } else {
32+ cleanup ( ) ;
3833 }
39- return value ;
4034 }
4135
4236 return {
@@ -45,26 +39,54 @@ export function wrapStorage(originalStorage, { expiresInSeconds } = {}) {
4539 * @returns {string | null }
4640 */
4741 getItem ( key ) {
42+ // Don't process expiry keys directly
43+ if ( key . startsWith ( EXPIRY_PREFIX ) ) {
44+ return originalStorage . getItem ( key ) ;
45+ }
46+
4847 const value = originalStorage . getItem ( key ) ;
49- if ( ! value ) return value ;
48+ if ( value === null ) {
49+ cleanupOrphanedExpiry ( key ) ;
50+ return value ;
51+ }
5052
51- try {
52- const parsed = JSON . parse ( value ) ;
53+ const expiryKey = getExpiryKey ( key ) ;
54+ const expiryData = originalStorage . getItem ( expiryKey ) ;
5355
54- if ( isWrappedValue ( parsed ) ) {
55- if ( parsed . ed && parsed . ed < Date . now ( ) ) {
56- originalStorage . removeItem ( key ) ;
57- return null ;
58- }
59- return parsed . v ;
60- }
56+ // If no expiry data but we have expiration configured, auto-wrap it
57+ if ( ! expiryData && expiresInSeconds !== undefined ) {
58+ const newExpiryTime = Date . now ( ) + expiresInSeconds * 1000 ;
59+ const expiryDataObj = JSON . stringify ( {
60+ e : newExpiryTime ,
61+ v : EXPIRY_VERSION ,
62+ } ) ;
63+ originalStorage . setItem ( expiryKey , expiryDataObj ) ;
64+ return value ;
65+ }
66+
67+ // If no expiry data, return the value as-is
68+ if ( ! expiryData ) {
69+ return value ;
70+ }
6171
62- // Auto-wrap existing non-wrapped values
63- return autoWrapIfNeeded ( key , value ) ;
72+ let expiryTime ;
73+ try {
74+ const parsed = JSON . parse ( expiryData ) ;
75+ expiryTime = parsed . e ;
6476 } catch ( e ) {
65- // Handle non-JSON values
66- return autoWrapIfNeeded ( key , value ) ;
77+ // Invalid expiry data, clean up and return value
78+ originalStorage . removeItem ( expiryKey ) ;
79+ return value ;
6780 }
81+
82+ // Check if expired
83+ if ( Date . now ( ) > expiryTime ) {
84+ originalStorage . removeItem ( key ) ;
85+ originalStorage . removeItem ( expiryKey ) ;
86+ return null ;
87+ }
88+
89+ return value ;
6890 } ,
6991
7092 /**
@@ -79,17 +101,21 @@ export function wrapStorage(originalStorage, { expiresInSeconds } = {}) {
79101 }
80102
81103 const stringValue = String ( value ) ;
82- originalStorage . setItem (
83- key ,
84- JSON . stringify ( createWrappedItem ( stringValue ) )
85- ) ;
104+ originalStorage . setItem ( key , stringValue ) ;
105+
106+ if ( expiresInSeconds !== undefined ) {
107+ const expiryTime = Date . now ( ) + expiresInSeconds * 1000 ;
108+ const expiryData = JSON . stringify ( { e : expiryTime , v : EXPIRY_VERSION } ) ;
109+ originalStorage . setItem ( getExpiryKey ( key ) , expiryData ) ;
110+ }
86111 } ,
87112
88113 /**
89114 * @param {string } key
90115 */
91116 removeItem ( key ) {
92117 originalStorage . removeItem ( key ) ;
118+ originalStorage . removeItem ( getExpiryKey ( key ) ) ;
93119 } ,
94120
95121 /**
@@ -104,6 +130,44 @@ export function wrapStorage(originalStorage, { expiresInSeconds } = {}) {
104130 originalStorage . clear ( ) ;
105131 } ,
106132
133+ cleanup ( ) {
134+ const keysToRemove = [ ] ;
135+
136+ for ( let i = 0 ; i < originalStorage . length ; i ++ ) {
137+ const key = originalStorage . key ( i ) ;
138+
139+ if ( key ?. startsWith ( EXPIRY_PREFIX ) ) {
140+ const dataKey = key . slice ( EXPIRY_PREFIX . length ) ;
141+ const expiryData = originalStorage . getItem ( key ) ;
142+
143+ let expiryTime ;
144+ if ( expiryData ) {
145+ try {
146+ const parsed = JSON . parse ( expiryData ) ;
147+ expiryTime = parsed . e ;
148+ } catch ( e ) {
149+ expiryTime = 0 ; // Invalid data, will be cleaned up
150+ }
151+ } else {
152+ expiryTime = 0 ; // No expiry data, will be cleaned up
153+ }
154+
155+ // Remove if orphaned or expired
156+ if (
157+ ! originalStorage . getItem ( dataKey ) ||
158+ ( expiryTime && Date . now ( ) > expiryTime )
159+ ) {
160+ keysToRemove . push ( key ) ;
161+ if ( originalStorage . getItem ( dataKey ) ) {
162+ keysToRemove . push ( dataKey ) ;
163+ }
164+ }
165+ }
166+ }
167+
168+ keysToRemove . forEach ( ( key ) => originalStorage . removeItem ( key ) ) ;
169+ } ,
170+
107171 get length ( ) {
108172 return originalStorage . length ;
109173 } ,
0 commit comments