Skip to content

Commit ac47170

Browse files
committed
add cleanup
1 parent f117627 commit ac47170

8 files changed

Lines changed: 808 additions & 2 deletions

File tree

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,54 @@ Wraps a Storage object to add expiration functionality.
3939

4040
**Returns:** A Storage-compatible object with expiration support
4141

42+
### `cleanupFactory(originalStorage, options?)`
43+
44+
Creates a cleanup function to wrap existing values and remove expired ones.
45+
46+
**Parameters:**
47+
48+
- `originalStorage` (Storage): The storage object (localStorage or sessionStorage)
49+
- `options` (object, optional):
50+
- `expiresInSeconds` (number, optional): Time in seconds after which stored items expire
51+
- `runWhenBrowserIsIdle` (boolean, optional): Whether to run cleanup when browser is idle (default: false)
52+
53+
**Returns:** An object with a `runCleanup()` method
54+
55+
**Example:**
56+
57+
```javascript
58+
import { cleanupFactory } from "expirix";
59+
60+
// Create cleanup that runs when browser is idle
61+
const cleanup = cleanupFactory(localStorage, {
62+
expiresInSeconds: 3600, // 1 hour
63+
runWhenBrowserIsIdle: true,
64+
});
65+
66+
// Run cleanup (will use requestIdleCallback if available, setTimeout as fallback)
67+
cleanup.runCleanup();
68+
```
69+
70+
## Browser Idle Cleanup
71+
72+
The library includes support for running cleanup operations when the browser is idle, using the `requestIdleCallback` API when available, with a simple polyfill fallback for older browsers.
73+
74+
```javascript
75+
// Cleanup will run when browser is idle
76+
const idleCleanup = cleanupFactory(localStorage, {
77+
expiresInSeconds: 3600,
78+
runWhenBrowserIsIdle: true,
79+
});
80+
81+
idleCleanup.runCleanup(); // Schedules cleanup for when browser is idle
82+
```
83+
84+
**requestIdleCallback Polyfill:**
85+
86+
- Uses native `requestIdleCallback` when available
87+
- Falls back to `setTimeout` with 1ms delay in older browsers
88+
- Provides deadline object with `didTimeout` and `timeRemaining()` methods
89+
4290
## Features
4391

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

examples/idle-cleanup.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* Example: Using runWhenBrowserIsIdle for cleanup operations
3+
*
4+
* This example demonstrates how to use the cleanupFactory with the
5+
* runWhenBrowserIsIdle option to perform cleanup operations when
6+
* the browser is idle, improving performance by not blocking the
7+
* main thread during busy periods.
8+
*/
9+
10+
import { cleanupFactory } from "../src/index.mjs";
11+
12+
// Example 1: Basic idle cleanup
13+
console.log("🔄 Example 1: Basic idle cleanup");
14+
15+
// Create cleanup that runs when browser is idle
16+
const idleCleanup = cleanupFactory(localStorage, {
17+
expiresInSeconds: 3600, // 1 hour expiration
18+
runWhenBrowserIsIdle: true,
19+
});
20+
21+
// Add some test data
22+
localStorage.setItem("user-preference", "dark-mode");
23+
localStorage.setItem(
24+
"session-data",
25+
JSON.stringify({ userId: 123, token: "abc" })
26+
);
27+
localStorage.setItem("temp-cache", "some-cached-data");
28+
29+
console.log("Added test data to localStorage");
30+
console.log("Running idle cleanup...");
31+
32+
// This will use requestIdleCallback if available, setTimeout as fallback
33+
idleCleanup.runCleanup();
34+
35+
console.log("Cleanup scheduled for when browser is idle");
36+
console.log("Check developer tools to see the wrapped values in localStorage");
37+
38+
// Example 2: Comparison with immediate cleanup
39+
console.log("\n🚀 Example 2: Immediate vs Idle cleanup comparison");
40+
41+
const immediateCleanup = cleanupFactory(localStorage, {
42+
expiresInSeconds: 7200, // 2 hours
43+
runWhenBrowserIsIdle: false, // Run immediately
44+
});
45+
46+
// Simulate adding data during heavy computation
47+
console.log("Simulating heavy computation...");
48+
const start = performance.now();
49+
50+
// Add data
51+
localStorage.setItem("immediate-test", "test-data");
52+
53+
// Run immediate cleanup
54+
immediateCleanup.runCleanup();
55+
56+
const immediateTime = performance.now() - start;
57+
console.log(`Immediate cleanup took: ${immediateTime}ms`);
58+
59+
// Now with idle cleanup
60+
localStorage.setItem("idle-test", "test-data");
61+
62+
const idleStart = performance.now();
63+
const idleCleanup2 = cleanupFactory(localStorage, {
64+
expiresInSeconds: 7200,
65+
runWhenBrowserIsIdle: true,
66+
});
67+
68+
idleCleanup2.runCleanup();
69+
const idleScheduleTime = performance.now() - idleStart;
70+
71+
console.log(`Idle cleanup scheduling took: ${idleScheduleTime}ms`);
72+
console.log("Actual cleanup will run when browser is idle");
73+
74+
// Example 3: Periodic idle cleanup
75+
console.log("\n⏰ Example 3: Periodic idle cleanup");
76+
77+
function setupPeriodicIdleCleanup() {
78+
const periodicCleanup = cleanupFactory(localStorage, {
79+
expiresInSeconds: 1800, // 30 minutes
80+
runWhenBrowserIsIdle: true,
81+
});
82+
83+
// Schedule cleanup every 5 minutes, but only when browser is idle
84+
setInterval(() => {
85+
console.log("Running periodic idle cleanup...");
86+
periodicCleanup.runCleanup();
87+
}, 5 * 60 * 1000); // 5 minutes
88+
}
89+
90+
// setupPeriodicIdleCleanup();
91+
console.log("Periodic cleanup example ready (uncomment to activate)");
92+
93+
// Example 4: Feature detection
94+
console.log("\n🔍 Example 4: Feature detection");
95+
96+
if (typeof window !== "undefined" && window.requestIdleCallback) {
97+
console.log("✅ Native requestIdleCallback is available");
98+
} else {
99+
console.log("📦 Using polyfill fallback for requestIdleCallback");
100+
}
101+
102+
console.log(
103+
"\n✨ All examples complete! Check localStorage in developer tools."
104+
);

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: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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 }} [options={}] - Configuration options
39+
* @returns {{ runCleanup: () => void }}
40+
*/
41+
export function cleanupFactory(
42+
originalStorage,
43+
{ expiresInSeconds, runWhenBrowserIsIdle = false } = {}
44+
) {
45+
/**
46+
* @returns {void}
47+
*/
48+
function runCleanup() {
49+
const actualCleanup = () => {
50+
// Collect all keys first to avoid issues with storage mutation during iteration
51+
const keys = [];
52+
for (let i = 0; i < originalStorage.length; i++) {
53+
const key = originalStorage.key(i);
54+
if (key !== null) {
55+
keys.push(key);
56+
}
57+
}
58+
59+
// Process each key
60+
keys.forEach((key) => {
61+
const value = originalStorage.getItem(key);
62+
if (!value) return;
63+
64+
try {
65+
const parsed = JSON.parse(value);
66+
67+
if (isWrappedValue(parsed)) {
68+
// Check if it should be deleted (expired)
69+
if (isExpired(parsed)) {
70+
originalStorage.removeItem(key);
71+
}
72+
// If not expired, leave it as is
73+
} else {
74+
// Not a wrapped value yet, wrap it
75+
originalStorage.setItem(
76+
key,
77+
JSON.stringify(createWrappedItem(value, expiresInSeconds))
78+
);
79+
}
80+
} catch (e) {
81+
// Handle non-JSON values - treat as plain string and wrap
82+
originalStorage.setItem(
83+
key,
84+
JSON.stringify(createWrappedItem(value, expiresInSeconds))
85+
);
86+
}
87+
});
88+
};
89+
90+
if (runWhenBrowserIsIdle) {
91+
const requestIdleCallback = getRequestIdleCallback();
92+
requestIdleCallback(actualCleanup);
93+
} else {
94+
actualCleanup();
95+
}
96+
}
97+
98+
return {
99+
runCleanup,
100+
};
101+
}

0 commit comments

Comments
 (0)