Skip to content

Commit 808d26b

Browse files
committed
new dual key strategy
1 parent f117627 commit 808d26b

2 files changed

Lines changed: 400 additions & 70 deletions

File tree

src/index.mjs

Lines changed: 131 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,41 @@
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
*/
66
export 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

Comments
 (0)