Skip to content

Commit c0c1e79

Browse files
Merge pull request #7 from robin-drexler/rd/cleanup
Rd/cleanup
2 parents f117627 + 22225fc commit c0c1e79

7 files changed

Lines changed: 989 additions & 3 deletions

File tree

README.md

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ A storage wrapper that adds expiration functionality to `localStorage` or any ot
1010
npm install expiring-storage
1111
```
1212

13-
## Usage
13+
## Examples
14+
15+
### Basic Usage
1416

1517
```javascript
1618
import { wrapStorage } from "expirix";
@@ -25,6 +27,31 @@ storage.setItem("key", "value");
2527
const value = storage.getItem("key");
2628
```
2729

30+
### Creating a reusable storage module
31+
32+
```javascript
33+
// app-storage.js
34+
import { wrapStorage } from "expirix";
35+
36+
const wrappedLocalStorage = wrapStorage(localStorage, {
37+
expiresInSeconds: 3600,
38+
});
39+
40+
export default wrappedLocalStorage;
41+
```
42+
43+
```javascript
44+
// Using in your app
45+
import storage from "./app-storage.js";
46+
47+
// Store user preferences that expire in 1 hour
48+
storage.setItem("theme", "dark");
49+
storage.setItem("language", "en");
50+
51+
// Later...
52+
const theme = storage.getItem("theme"); // null if expired
53+
```
54+
2855
## API
2956

3057
### `wrapStorage(originalStorage, options?)`
@@ -39,6 +66,85 @@ Wraps a Storage object to add expiration functionality.
3966

4067
**Returns:** A Storage-compatible object with expiration support
4168

69+
### `cleanupFactory(originalStorage, options?)`
70+
71+
Creates a cleanup function to remove expired values and optionally wrap unwrapped values so that they can be cleaned up later on as well.
72+
Useful to cleanup keys that might not be requested by the app anymore.
73+
74+
**Parameters:**
75+
76+
- `originalStorage` (Storage): The storage object (localStorage or sessionStorage)
77+
- `options` (object, optional):
78+
- `expiresInSeconds` (number, optional): Time in seconds after which stored items expire
79+
- `runWhenBrowserIsIdle` (boolean, optional): Whether to run cleanup when browser is idle (default: true)
80+
- `wrapUnwrappedItems` (boolean, optional): Whether to wrap non-wrapped items with expiration metadata (default: false)
81+
If turned on, this will wrap existing values in a JSON structure. Only use it if you are sure that whatever code reads these values can handle this.
82+
83+
**Returns:** An object with a `runCleanup()` method
84+
85+
**Example:**
86+
87+
```javascript
88+
import { cleanupFactory } from "expirix";
89+
90+
// Create cleanup that runs when browser is idle (default behavior)
91+
const cleanup = cleanupFactory(localStorage, {
92+
expiresInSeconds: 3600, // 1 hour
93+
});
94+
95+
// Run cleanup (will use requestIdleCallback if available, setTimeout as fallback)
96+
cleanup.runCleanup();
97+
98+
// To run cleanup immediately instead:
99+
const immediateCleanup = cleanupFactory(localStorage, {
100+
expiresInSeconds: 3600,
101+
runWhenBrowserIsIdle: false,
102+
});
103+
```
104+
105+
#### Wrapping Behavior Control
106+
107+
By default, cleanup operations only remove expired items but do not modify existing unwrapped values. You can control whether cleanup should wrap existing unwrapped items with the `wrapUnwrappedItems` option.
108+
109+
```javascript
110+
// Only clean up expired items, leave unwrapped items as-is (default)
111+
const cleanupOnly = cleanupFactory(localStorage, {
112+
expiresInSeconds: 3600,
113+
});
114+
115+
// Clean up expired items AND wrap unwrapped items
116+
const cleanupAndWrap = cleanupFactory(localStorage, {
117+
expiresInSeconds: 3600,
118+
wrapUnwrappedItems: true,
119+
});
120+
```
121+
122+
This gives you fine-grained control over when existing data gets wrapped with expiration metadata.
123+
124+
#### Browser Idle Cleanup
125+
126+
By default, the library runs cleanup operations when the browser is idle, using the `requestIdleCallback` API when available, with a simple polyfill fallback for older browsers.
127+
128+
```javascript
129+
// Cleanup will run when browser is idle (default behavior)
130+
const idleCleanup = cleanupFactory(localStorage, {
131+
expiresInSeconds: 3600,
132+
});
133+
134+
idleCleanup.runCleanup(); // Schedules cleanup for when browser is idle
135+
136+
// To run cleanup immediately instead:
137+
const immediateCleanup = cleanupFactory(localStorage, {
138+
expiresInSeconds: 3600,
139+
runWhenBrowserIsIdle: false,
140+
});
141+
```
142+
143+
**requestIdleCallback Polyfill:**
144+
145+
- Uses native `requestIdleCallback` when available
146+
- Falls back to `setTimeout` with 1ms delay in older browsers
147+
42148
## Features
43149

44150
-**Drop-in replacement** for localStorage/sessionStorage

playground/main.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,42 @@
1-
import { wrapStorage } from "../src/index.mjs";
1+
import { wrapStorage, cleanupFactory } from "../src/index.mjs";
22

33
// Create wrapped storage with 60 seconds expiry and make it globally available
44
window.storage = wrapStorage(localStorage, { expiresInSeconds: 60 });
55

66
// Also expose the wrapStorage function globally for custom configurations
77
window.wrapStorage = wrapStorage;
88

9-
// Log to console that everything is ready
9+
// Demonstrate runWhenBrowserIsIdle feature
1010
console.log("🚀 Expirix loaded!");
1111
console.log("Available globals:");
1212
console.log(" storage - Pre-configured localStorage with 60s expiry");
1313
console.log(" wrapStorage - Function to create custom storage instances");
1414
console.log("");
15+
16+
// Demonstrate idle cleanup
17+
console.log("🛠️ Demonstrating idle cleanup feature:");
18+
console.log("Creating cleanup with runWhenBrowserIsIdle=true...");
19+
20+
const idleCleanup = cleanupFactory(localStorage, {
21+
expiresInSeconds: 60,
22+
runWhenBrowserIsIdle: true,
23+
});
24+
25+
// Add some test data
26+
localStorage.setItem("demo-key1", "demo-value1");
27+
localStorage.setItem("demo-key2", "demo-value2");
28+
29+
console.log("Added test data to localStorage");
30+
console.log("Running cleanup (will use requestIdleCallback if available)...");
31+
32+
idleCleanup.runCleanup();
33+
34+
console.log("Cleanup scheduled! Check localStorage to see wrapped values.");
35+
console.log("");
1536
console.log("Try it out:");
1637
console.log(' storage.setItem("test", "hello world")');
1738
console.log(' storage.getItem("test")');
39+
console.log(" idleCleanup.runCleanup() // Run cleanup when browser is idle");
40+
41+
// Make cleanup available globally for experimentation
42+
window.idleCleanup = idleCleanup;

src/cleanup.mjs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { isWrappedValue, createWrappedItem, isExpired } from "./utils.mjs";
2+
3+
/**
4+
* Simple polyfill for requestIdleCallback
5+
* @param {Function} callback - The callback to run when idle
6+
* @param {{ timeout?: number }} [options] - Options object
7+
* @returns {number} - The callback ID
8+
*/
9+
function requestIdleCallbackPolyfill(callback, options = {}) {
10+
const timeout = options.timeout || 0;
11+
const start = Date.now();
12+
13+
// @ts-ignore - setTimeout returns different types in different environments
14+
return setTimeout(() => {
15+
const deadline = {
16+
didTimeout: timeout > 0 && Date.now() - start >= timeout,
17+
timeRemaining() {
18+
return Math.max(0, 50 - (Date.now() - start));
19+
},
20+
};
21+
callback(deadline);
22+
}, 1);
23+
}
24+
25+
/**
26+
* Get requestIdleCallback with polyfill fallback
27+
* @returns {Function} - The requestIdleCallback function
28+
*/
29+
function getRequestIdleCallback() {
30+
if (typeof window !== "undefined" && window.requestIdleCallback) {
31+
return window.requestIdleCallback.bind(window);
32+
}
33+
return requestIdleCallbackPolyfill;
34+
}
35+
36+
/**
37+
* @param {Storage} originalStorage - The storage object (localStorage or sessionStorage)
38+
* @param {{ expiresInSeconds?: number, runWhenBrowserIsIdle?: boolean, wrapUnwrappedItems?: boolean }} [options={}] - Configuration options
39+
* @returns {{ runCleanup: () => void }}
40+
*/
41+
export function cleanupFactory(
42+
originalStorage,
43+
{
44+
expiresInSeconds,
45+
runWhenBrowserIsIdle = true,
46+
wrapUnwrappedItems = false,
47+
} = {}
48+
) {
49+
/**
50+
* @returns {void}
51+
*/
52+
function runCleanup() {
53+
const actualCleanup = () => {
54+
// Iterate backwards through storage to handle removals safely in a single pass
55+
for (let i = originalStorage.length - 1; i >= 0; i--) {
56+
const key = originalStorage.key(i);
57+
if (key === null) continue;
58+
59+
const value = originalStorage.getItem(key);
60+
if (!value) continue;
61+
62+
try {
63+
const parsed = JSON.parse(value);
64+
65+
if (isWrappedValue(parsed)) {
66+
// Check if it should be deleted (expired)
67+
if (isExpired(parsed)) {
68+
originalStorage.removeItem(key);
69+
}
70+
// If not expired, leave it as is
71+
} else if (wrapUnwrappedItems) {
72+
// Not a wrapped value yet, wrap it only if wrapUnwrappedItems is true
73+
originalStorage.setItem(
74+
key,
75+
JSON.stringify(createWrappedItem(value, expiresInSeconds))
76+
);
77+
}
78+
// If wrapUnwrappedItems is false, leave unwrapped items as-is
79+
} catch (e) {
80+
// Handle non-JSON values - treat as plain string and wrap only if wrapUnwrappedItems is true
81+
if (wrapUnwrappedItems) {
82+
originalStorage.setItem(
83+
key,
84+
JSON.stringify(createWrappedItem(value, expiresInSeconds))
85+
);
86+
}
87+
}
88+
}
89+
};
90+
91+
if (runWhenBrowserIsIdle) {
92+
const requestIdleCallback = getRequestIdleCallback();
93+
requestIdleCallback(actualCleanup);
94+
} else {
95+
actualCleanup();
96+
}
97+
}
98+
99+
return {
100+
runCleanup,
101+
};
102+
}

0 commit comments

Comments
 (0)