From d084296ae97f4207fab867aca35974a716796c52 Mon Sep 17 00:00:00 2001 From: shriramthebeast Date: Sat, 25 Oct 2025 19:57:07 +0530 Subject: [PATCH 1/4] Hactoberfest 1st pr --- .../Debounce and Throttle/README.md | 41 +++++++++++ .../Debounce and Throttle/combined-usage.js | 69 ++++++++++++++++++ .../Debounce and Throttle/debounce.js | 37 ++++++++++ .../Debounce and Throttle/throttle.js | 73 +++++++++++++++++++ 4 files changed, 220 insertions(+) create mode 100644 Modern Development/Debounce and Throttle/README.md create mode 100644 Modern Development/Debounce and Throttle/combined-usage.js create mode 100644 Modern Development/Debounce and Throttle/debounce.js create mode 100644 Modern Development/Debounce and Throttle/throttle.js diff --git a/Modern Development/Debounce and Throttle/README.md b/Modern Development/Debounce and Throttle/README.md new file mode 100644 index 0000000000..c80b6be23d --- /dev/null +++ b/Modern Development/Debounce and Throttle/README.md @@ -0,0 +1,41 @@ +# Debounce and Throttle + +Utility functions to control how often a function executes during frequent events. + +## Overview + +**Debounce** - Waits until user stops action before executing (e.g., search input, auto-save) +**Throttle** - Limits execution to once per time interval (e.g., scroll, resize) + +## Files + +- `debounce.js` - 3 debounce variations +- `throttle.js` - 4 throttle variations +- `combined-usage.js` - Real-world examples + +## Quick Examples + +### Debounce +```javascript +const handleSearch = debounce((query) => { + // API call after user stops typing + fetch(`/api/search?q=${query}`); +}, 300); + +inputElement.addEventListener('input', e => handleSearch(e.target.value)); +``` + +### Throttle +```javascript +const handleScroll = throttle(() => { + // Update every 300ms during scroll + updateScrollBar(); +}, 300); + +window.addEventListener('scroll', handleScroll); +``` + +## Use Cases + +**Debounce:** search input, auto-save, form validation +**Throttle:** scroll events, window resize, animations diff --git a/Modern Development/Debounce and Throttle/combined-usage.js b/Modern Development/Debounce and Throttle/combined-usage.js new file mode 100644 index 0000000000..12e0f59abf --- /dev/null +++ b/Modern Development/Debounce and Throttle/combined-usage.js @@ -0,0 +1,69 @@ +// Search with debounce - waits for user to stop typing +class SearchBox { + constructor(inputSelector, onSearch) { + this.input = document.querySelector(inputSelector); + this.onSearch = onSearch; + this.debouncedSearch = debounce((query) => { + if (query.length > 2) this.onSearch(query); + }, 300); + this.input.addEventListener('input', e => this.debouncedSearch(e.target.value)); + } +} + +// Auto-save form after user stops typing +class AutoSaveForm { + constructor(formSelector, saveUrl) { + this.form = document.querySelector(formSelector); + this.saveUrl = saveUrl; + this.debouncedSave = debounce(() => this.save(), 1000); + this.form.addEventListener('input', () => this.debouncedSave()); + } + + async save() { + const data = new FormData(this.form); + try { + await fetch(this.saveUrl, { + method: 'POST', + body: new URLSearchParams(data) + }); + console.log('Saved'); + } catch (e) { + console.error('Save failed', e); + } + } +} + +// Handle window resize with throttle +class ResponsiveLayout { + constructor() { + this.throttledResize = throttle(() => this.handleResize(), 500); + window.addEventListener('resize', this.throttledResize); + } + + handleResize() { + const width = window.innerWidth; + if (width < 768) { + document.body.classList.add('mobile'); + } else { + document.body.classList.remove('mobile'); + } + } +} + +// Scroll detection with throttle +class ScrollHandler { + constructor() { + this.throttledScroll = throttle(() => { + const pos = window.scrollY; + const docHeight = document.documentElement.scrollHeight; + if (pos + window.innerHeight >= docHeight - 100) { + this.loadMore(); + } + }, 300); + window.addEventListener('scroll', this.throttledScroll); + } + + loadMore() { + console.log('Load more content'); + } +} diff --git a/Modern Development/Debounce and Throttle/debounce.js b/Modern Development/Debounce and Throttle/debounce.js new file mode 100644 index 0000000000..7d5a19ebd3 --- /dev/null +++ b/Modern Development/Debounce and Throttle/debounce.js @@ -0,0 +1,37 @@ +// Basic debounce - delays function execution until after user stops action +function debounce(func, wait) { + let timeout; + return function(...args) { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; +} + +// Debounce with cancel method +function debounceWithCancel(func, wait) { + let timeout; + const debounced = function(...args) { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; + debounced.cancel = () => clearTimeout(timeout); + return debounced; +} + +// Debounce with flush - execute immediately +function debounceWithFlush(func, wait) { + let timeout, lastArgs, lastThis; + const debounced = function(...args) { + lastArgs = args; + lastThis = this; + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(lastThis, lastArgs), wait); + }; + debounced.flush = () => { + if (timeout) { + clearTimeout(timeout); + func.apply(lastThis, lastArgs); + } + }; + return debounced; +} diff --git a/Modern Development/Debounce and Throttle/throttle.js b/Modern Development/Debounce and Throttle/throttle.js new file mode 100644 index 0000000000..dba497dd64 --- /dev/null +++ b/Modern Development/Debounce and Throttle/throttle.js @@ -0,0 +1,73 @@ +// Basic throttle - limit function execution to once per interval +function throttle(func, limit) { + let inThrottle; + return function(...args) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; +} + +// Throttle with timestamp - more precise timing +function throttleWithTimestamp(func, limit) { + let lastRan; + return function(...args) { + if (!lastRan) { + func.apply(this, args); + lastRan = Date.now(); + } else { + const remaining = limit - (Date.now() - lastRan); + if (remaining <= 0) { + func.apply(this, args); + lastRan = Date.now(); + } + } + }; +} + +// Throttle with leading and trailing options +function throttleAdvanced(func, wait, options = {}) { + const { leading = true, trailing = false } = options; + let timeout, previous = 0, lastArgs, lastThis; + + const throttled = function(...args) { + const now = Date.now(); + if (!previous && !leading) previous = now; + + const remaining = wait - (now - previous); + lastArgs = args; + lastThis = this; + + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + func.apply(this, args); + } else if (!timeout && trailing) { + timeout = setTimeout(() => { + previous = leading ? Date.now() : 0; + timeout = null; + func.apply(lastThis, lastArgs); + }, remaining); + } + }; + throttled.cancel = () => { + clearTimeout(timeout); + previous = 0; + }; + return throttled; +} + +// Throttle using requestAnimationFrame for smooth animation +function throttleRAF(func) { + let frameId = null; + return function(...args) { + if (frameId) return; + frameId = requestAnimationFrame(() => { + func.apply(this, args); + frameId = null; + }); + }; +} From 8f28c5608816adf02bb1c53c90936ed5cb98f0e0 Mon Sep 17 00:00:00 2001 From: shriramthebeast Date: Sat, 25 Oct 2025 20:04:12 +0530 Subject: [PATCH 2/4] debounce and throttle --- .../Debounce and Throttle/README.md | 41 +++++++++++ .../Debounce and Throttle/combined-usage.js | 69 ++++++++++++++++++ .../Debounce and Throttle/debounce.js | 37 ++++++++++ .../Debounce and Throttle/throttle.js | 73 +++++++++++++++++++ 4 files changed, 220 insertions(+) create mode 100644 Modern Development/ECMASCript 2021/Debounce and Throttle/README.md create mode 100644 Modern Development/ECMASCript 2021/Debounce and Throttle/combined-usage.js create mode 100644 Modern Development/ECMASCript 2021/Debounce and Throttle/debounce.js create mode 100644 Modern Development/ECMASCript 2021/Debounce and Throttle/throttle.js diff --git a/Modern Development/ECMASCript 2021/Debounce and Throttle/README.md b/Modern Development/ECMASCript 2021/Debounce and Throttle/README.md new file mode 100644 index 0000000000..c80b6be23d --- /dev/null +++ b/Modern Development/ECMASCript 2021/Debounce and Throttle/README.md @@ -0,0 +1,41 @@ +# Debounce and Throttle + +Utility functions to control how often a function executes during frequent events. + +## Overview + +**Debounce** - Waits until user stops action before executing (e.g., search input, auto-save) +**Throttle** - Limits execution to once per time interval (e.g., scroll, resize) + +## Files + +- `debounce.js` - 3 debounce variations +- `throttle.js` - 4 throttle variations +- `combined-usage.js` - Real-world examples + +## Quick Examples + +### Debounce +```javascript +const handleSearch = debounce((query) => { + // API call after user stops typing + fetch(`/api/search?q=${query}`); +}, 300); + +inputElement.addEventListener('input', e => handleSearch(e.target.value)); +``` + +### Throttle +```javascript +const handleScroll = throttle(() => { + // Update every 300ms during scroll + updateScrollBar(); +}, 300); + +window.addEventListener('scroll', handleScroll); +``` + +## Use Cases + +**Debounce:** search input, auto-save, form validation +**Throttle:** scroll events, window resize, animations diff --git a/Modern Development/ECMASCript 2021/Debounce and Throttle/combined-usage.js b/Modern Development/ECMASCript 2021/Debounce and Throttle/combined-usage.js new file mode 100644 index 0000000000..12e0f59abf --- /dev/null +++ b/Modern Development/ECMASCript 2021/Debounce and Throttle/combined-usage.js @@ -0,0 +1,69 @@ +// Search with debounce - waits for user to stop typing +class SearchBox { + constructor(inputSelector, onSearch) { + this.input = document.querySelector(inputSelector); + this.onSearch = onSearch; + this.debouncedSearch = debounce((query) => { + if (query.length > 2) this.onSearch(query); + }, 300); + this.input.addEventListener('input', e => this.debouncedSearch(e.target.value)); + } +} + +// Auto-save form after user stops typing +class AutoSaveForm { + constructor(formSelector, saveUrl) { + this.form = document.querySelector(formSelector); + this.saveUrl = saveUrl; + this.debouncedSave = debounce(() => this.save(), 1000); + this.form.addEventListener('input', () => this.debouncedSave()); + } + + async save() { + const data = new FormData(this.form); + try { + await fetch(this.saveUrl, { + method: 'POST', + body: new URLSearchParams(data) + }); + console.log('Saved'); + } catch (e) { + console.error('Save failed', e); + } + } +} + +// Handle window resize with throttle +class ResponsiveLayout { + constructor() { + this.throttledResize = throttle(() => this.handleResize(), 500); + window.addEventListener('resize', this.throttledResize); + } + + handleResize() { + const width = window.innerWidth; + if (width < 768) { + document.body.classList.add('mobile'); + } else { + document.body.classList.remove('mobile'); + } + } +} + +// Scroll detection with throttle +class ScrollHandler { + constructor() { + this.throttledScroll = throttle(() => { + const pos = window.scrollY; + const docHeight = document.documentElement.scrollHeight; + if (pos + window.innerHeight >= docHeight - 100) { + this.loadMore(); + } + }, 300); + window.addEventListener('scroll', this.throttledScroll); + } + + loadMore() { + console.log('Load more content'); + } +} diff --git a/Modern Development/ECMASCript 2021/Debounce and Throttle/debounce.js b/Modern Development/ECMASCript 2021/Debounce and Throttle/debounce.js new file mode 100644 index 0000000000..7d5a19ebd3 --- /dev/null +++ b/Modern Development/ECMASCript 2021/Debounce and Throttle/debounce.js @@ -0,0 +1,37 @@ +// Basic debounce - delays function execution until after user stops action +function debounce(func, wait) { + let timeout; + return function(...args) { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; +} + +// Debounce with cancel method +function debounceWithCancel(func, wait) { + let timeout; + const debounced = function(...args) { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; + debounced.cancel = () => clearTimeout(timeout); + return debounced; +} + +// Debounce with flush - execute immediately +function debounceWithFlush(func, wait) { + let timeout, lastArgs, lastThis; + const debounced = function(...args) { + lastArgs = args; + lastThis = this; + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(lastThis, lastArgs), wait); + }; + debounced.flush = () => { + if (timeout) { + clearTimeout(timeout); + func.apply(lastThis, lastArgs); + } + }; + return debounced; +} diff --git a/Modern Development/ECMASCript 2021/Debounce and Throttle/throttle.js b/Modern Development/ECMASCript 2021/Debounce and Throttle/throttle.js new file mode 100644 index 0000000000..dba497dd64 --- /dev/null +++ b/Modern Development/ECMASCript 2021/Debounce and Throttle/throttle.js @@ -0,0 +1,73 @@ +// Basic throttle - limit function execution to once per interval +function throttle(func, limit) { + let inThrottle; + return function(...args) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; +} + +// Throttle with timestamp - more precise timing +function throttleWithTimestamp(func, limit) { + let lastRan; + return function(...args) { + if (!lastRan) { + func.apply(this, args); + lastRan = Date.now(); + } else { + const remaining = limit - (Date.now() - lastRan); + if (remaining <= 0) { + func.apply(this, args); + lastRan = Date.now(); + } + } + }; +} + +// Throttle with leading and trailing options +function throttleAdvanced(func, wait, options = {}) { + const { leading = true, trailing = false } = options; + let timeout, previous = 0, lastArgs, lastThis; + + const throttled = function(...args) { + const now = Date.now(); + if (!previous && !leading) previous = now; + + const remaining = wait - (now - previous); + lastArgs = args; + lastThis = this; + + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + func.apply(this, args); + } else if (!timeout && trailing) { + timeout = setTimeout(() => { + previous = leading ? Date.now() : 0; + timeout = null; + func.apply(lastThis, lastArgs); + }, remaining); + } + }; + throttled.cancel = () => { + clearTimeout(timeout); + previous = 0; + }; + return throttled; +} + +// Throttle using requestAnimationFrame for smooth animation +function throttleRAF(func) { + let frameId = null; + return function(...args) { + if (frameId) return; + frameId = requestAnimationFrame(() => { + func.apply(this, args); + frameId = null; + }); + }; +} From 04f9996f96cb0b1616e6c9a60db783fc440011e9 Mon Sep 17 00:00:00 2001 From: shriramthebeast Date: Sat, 25 Oct 2025 20:10:36 +0530 Subject: [PATCH 3/4] Revert "Hactoberfest 1st pr" This reverts commit d084296ae97f4207fab867aca35974a716796c52. --- .../Debounce and Throttle/README.md | 41 ----------- .../Debounce and Throttle/combined-usage.js | 69 ------------------ .../Debounce and Throttle/debounce.js | 37 ---------- .../Debounce and Throttle/throttle.js | 73 ------------------- 4 files changed, 220 deletions(-) delete mode 100644 Modern Development/Debounce and Throttle/README.md delete mode 100644 Modern Development/Debounce and Throttle/combined-usage.js delete mode 100644 Modern Development/Debounce and Throttle/debounce.js delete mode 100644 Modern Development/Debounce and Throttle/throttle.js diff --git a/Modern Development/Debounce and Throttle/README.md b/Modern Development/Debounce and Throttle/README.md deleted file mode 100644 index c80b6be23d..0000000000 --- a/Modern Development/Debounce and Throttle/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Debounce and Throttle - -Utility functions to control how often a function executes during frequent events. - -## Overview - -**Debounce** - Waits until user stops action before executing (e.g., search input, auto-save) -**Throttle** - Limits execution to once per time interval (e.g., scroll, resize) - -## Files - -- `debounce.js` - 3 debounce variations -- `throttle.js` - 4 throttle variations -- `combined-usage.js` - Real-world examples - -## Quick Examples - -### Debounce -```javascript -const handleSearch = debounce((query) => { - // API call after user stops typing - fetch(`/api/search?q=${query}`); -}, 300); - -inputElement.addEventListener('input', e => handleSearch(e.target.value)); -``` - -### Throttle -```javascript -const handleScroll = throttle(() => { - // Update every 300ms during scroll - updateScrollBar(); -}, 300); - -window.addEventListener('scroll', handleScroll); -``` - -## Use Cases - -**Debounce:** search input, auto-save, form validation -**Throttle:** scroll events, window resize, animations diff --git a/Modern Development/Debounce and Throttle/combined-usage.js b/Modern Development/Debounce and Throttle/combined-usage.js deleted file mode 100644 index 12e0f59abf..0000000000 --- a/Modern Development/Debounce and Throttle/combined-usage.js +++ /dev/null @@ -1,69 +0,0 @@ -// Search with debounce - waits for user to stop typing -class SearchBox { - constructor(inputSelector, onSearch) { - this.input = document.querySelector(inputSelector); - this.onSearch = onSearch; - this.debouncedSearch = debounce((query) => { - if (query.length > 2) this.onSearch(query); - }, 300); - this.input.addEventListener('input', e => this.debouncedSearch(e.target.value)); - } -} - -// Auto-save form after user stops typing -class AutoSaveForm { - constructor(formSelector, saveUrl) { - this.form = document.querySelector(formSelector); - this.saveUrl = saveUrl; - this.debouncedSave = debounce(() => this.save(), 1000); - this.form.addEventListener('input', () => this.debouncedSave()); - } - - async save() { - const data = new FormData(this.form); - try { - await fetch(this.saveUrl, { - method: 'POST', - body: new URLSearchParams(data) - }); - console.log('Saved'); - } catch (e) { - console.error('Save failed', e); - } - } -} - -// Handle window resize with throttle -class ResponsiveLayout { - constructor() { - this.throttledResize = throttle(() => this.handleResize(), 500); - window.addEventListener('resize', this.throttledResize); - } - - handleResize() { - const width = window.innerWidth; - if (width < 768) { - document.body.classList.add('mobile'); - } else { - document.body.classList.remove('mobile'); - } - } -} - -// Scroll detection with throttle -class ScrollHandler { - constructor() { - this.throttledScroll = throttle(() => { - const pos = window.scrollY; - const docHeight = document.documentElement.scrollHeight; - if (pos + window.innerHeight >= docHeight - 100) { - this.loadMore(); - } - }, 300); - window.addEventListener('scroll', this.throttledScroll); - } - - loadMore() { - console.log('Load more content'); - } -} diff --git a/Modern Development/Debounce and Throttle/debounce.js b/Modern Development/Debounce and Throttle/debounce.js deleted file mode 100644 index 7d5a19ebd3..0000000000 --- a/Modern Development/Debounce and Throttle/debounce.js +++ /dev/null @@ -1,37 +0,0 @@ -// Basic debounce - delays function execution until after user stops action -function debounce(func, wait) { - let timeout; - return function(...args) { - clearTimeout(timeout); - timeout = setTimeout(() => func.apply(this, args), wait); - }; -} - -// Debounce with cancel method -function debounceWithCancel(func, wait) { - let timeout; - const debounced = function(...args) { - clearTimeout(timeout); - timeout = setTimeout(() => func.apply(this, args), wait); - }; - debounced.cancel = () => clearTimeout(timeout); - return debounced; -} - -// Debounce with flush - execute immediately -function debounceWithFlush(func, wait) { - let timeout, lastArgs, lastThis; - const debounced = function(...args) { - lastArgs = args; - lastThis = this; - clearTimeout(timeout); - timeout = setTimeout(() => func.apply(lastThis, lastArgs), wait); - }; - debounced.flush = () => { - if (timeout) { - clearTimeout(timeout); - func.apply(lastThis, lastArgs); - } - }; - return debounced; -} diff --git a/Modern Development/Debounce and Throttle/throttle.js b/Modern Development/Debounce and Throttle/throttle.js deleted file mode 100644 index dba497dd64..0000000000 --- a/Modern Development/Debounce and Throttle/throttle.js +++ /dev/null @@ -1,73 +0,0 @@ -// Basic throttle - limit function execution to once per interval -function throttle(func, limit) { - let inThrottle; - return function(...args) { - if (!inThrottle) { - func.apply(this, args); - inThrottle = true; - setTimeout(() => inThrottle = false, limit); - } - }; -} - -// Throttle with timestamp - more precise timing -function throttleWithTimestamp(func, limit) { - let lastRan; - return function(...args) { - if (!lastRan) { - func.apply(this, args); - lastRan = Date.now(); - } else { - const remaining = limit - (Date.now() - lastRan); - if (remaining <= 0) { - func.apply(this, args); - lastRan = Date.now(); - } - } - }; -} - -// Throttle with leading and trailing options -function throttleAdvanced(func, wait, options = {}) { - const { leading = true, trailing = false } = options; - let timeout, previous = 0, lastArgs, lastThis; - - const throttled = function(...args) { - const now = Date.now(); - if (!previous && !leading) previous = now; - - const remaining = wait - (now - previous); - lastArgs = args; - lastThis = this; - - if (remaining <= 0) { - clearTimeout(timeout); - timeout = null; - previous = now; - func.apply(this, args); - } else if (!timeout && trailing) { - timeout = setTimeout(() => { - previous = leading ? Date.now() : 0; - timeout = null; - func.apply(lastThis, lastArgs); - }, remaining); - } - }; - throttled.cancel = () => { - clearTimeout(timeout); - previous = 0; - }; - return throttled; -} - -// Throttle using requestAnimationFrame for smooth animation -function throttleRAF(func) { - let frameId = null; - return function(...args) { - if (frameId) return; - frameId = requestAnimationFrame(() => { - func.apply(this, args); - frameId = null; - }); - }; -} From 51280493ef3f4fa6ea3172f19fdc34e30ad256f9 Mon Sep 17 00:00:00 2001 From: shriramthebeast Date: Sat, 25 Oct 2025 20:38:57 +0530 Subject: [PATCH 4/4] Added LocalStorage and SessionStorage Utilities Added storage.js --- .../Storage Utilities/README.md | 8 +++++ .../Storage Utilities/storage-examples.js | 14 ++++++++ .../Storage Utilities/storage.js | 36 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 Modern Development/ECMASCript 2021/Storage Utilities/README.md create mode 100644 Modern Development/ECMASCript 2021/Storage Utilities/storage-examples.js create mode 100644 Modern Development/ECMASCript 2021/Storage Utilities/storage.js diff --git a/Modern Development/ECMASCript 2021/Storage Utilities/README.md b/Modern Development/ECMASCript 2021/Storage Utilities/README.md new file mode 100644 index 0000000000..2fe273298f --- /dev/null +++ b/Modern Development/ECMASCript 2021/Storage Utilities/README.md @@ -0,0 +1,8 @@ +# Storage Utilities + +Tiny helpers for LocalStorage and SessionStorage. + +- `storage.js` - lightweight wrapper: set/get/remove/clear/keys +- `storage-examples.js` - quick usage + +Use a prefix to avoid colliding with other data. diff --git a/Modern Development/ECMASCript 2021/Storage Utilities/storage-examples.js b/Modern Development/ECMASCript 2021/Storage Utilities/storage-examples.js new file mode 100644 index 0000000000..d904db1729 --- /dev/null +++ b/Modern Development/ECMASCript 2021/Storage Utilities/storage-examples.js @@ -0,0 +1,14 @@ +// Small usage examples +// local store with prefix +const appStore = localStore('myapp:'); +appStore.set('user', { id: 1, name: 'Alex' }); +console.log(appStore.get('user')); // {id:1, name:'Alex'} + +// session store +const sess = sessionStore('sess:'); +sess.set('temp', { foo: 'bar' }); +console.log(sess.get('temp')); + +// remove and keys +appStore.remove('user'); +console.log(sess.keys()); diff --git a/Modern Development/ECMASCript 2021/Storage Utilities/storage.js b/Modern Development/ECMASCript 2021/Storage Utilities/storage.js new file mode 100644 index 0000000000..e06b790d8b --- /dev/null +++ b/Modern Development/ECMASCript 2021/Storage Utilities/storage.js @@ -0,0 +1,36 @@ +// Minimal storage helpers for localStorage / sessionStorage +function createStorage(storage, prefix = '') { + const key = k => prefix + k; + return { + set(k, v) { + try { storage.setItem(key(k), JSON.stringify(v)); } catch (e) { /* ignore */ } + }, + get(k) { + try { const v = storage.getItem(key(k)); return v ? JSON.parse(v) : null; } catch (e) { return null; } + }, + remove(k) { + try { storage.removeItem(key(k)); } catch (e) { } + }, + clear() { + try { if (!prefix) storage.clear(); else { + // remove keys that start with prefix + for (let i = storage.length - 1; i >= 0; i--) { + const k = storage.key(i); if (k && k.indexOf(prefix) === 0) storage.removeItem(k); + } + }} catch (e) {} + }, + keys() { + const out = []; + for (let i = 0; i < storage.length; i++) { + const k = storage.key(i); if (k && (!prefix || k.indexOf(prefix) === 0)) out.push(prefix ? k.slice(prefix.length) : k); + } + return out; + } + }; +} + +const localStore = (prefix) => createStorage(window.localStorage, prefix); +const sessionStore = (prefix) => createStorage(window.sessionStorage, prefix); + +// Export for Node/test environments (if needed) +if (typeof module !== 'undefined' && module.exports) module.exports = { createStorage, localStore, sessionStore };