Skip to content

Commit a3c9dfd

Browse files
committed
new dual key strategy
1 parent f117627 commit a3c9dfd

2 files changed

Lines changed: 427 additions & 72 deletions

File tree

src/index.mjs

Lines changed: 106 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,33 @@
44
* @returns {Storage}
55
*/
66
export 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

Comments
 (0)