use-idb-storage 0.0.8 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,26 @@ A modern, developer-friendly IndexedDB library for React and vanilla JavaScript
15
15
  - 🔧 **TypeScript**: Full type safety
16
16
  - 🎯 **Promise-based**: Modern async/await support
17
17
 
18
+ ## Performance
19
+
20
+ useIDBStorage offers near-native performance with minimal overhead. Benchmark results show only 1.5ms difference compared to useState for 400 forced unbatched updates:
21
+
22
+ ```json
23
+ {
24
+ "useState": {
25
+ "renders": 400,
26
+ "time": 3317.9
27
+ },
28
+ "useIDBStorage": {
29
+ "renders": 400,
30
+ "time": 3316.4
31
+ },
32
+ "differenceMs": 1.5
33
+ }
34
+ ```
35
+
36
+ For detailed performance testing guide, see [PERFORMANCE.md](./PERFORMANCE.md).
37
+
18
38
  ## Installation
19
39
 
20
40
  ```bash
package/dist/index.d.ts CHANGED
@@ -31,7 +31,7 @@ interface IDBConfigProps extends Partial<IDBConfigValues> {
31
31
  /**
32
32
  * Return type of the useIDBStorage hook
33
33
  */
34
- type UseIDBStorageReturn<T> = [T, (value: T | ((prevState: T) => T)) => Promise<void>, () => Promise<void>];
34
+ type UseIDBStorageReturn<T> = [T, (value: T | ((prevState: T) => T)) => void, () => void];
35
35
  //#endregion
36
36
  //#region src/idb-storage.d.ts
37
37
  /**
@@ -121,9 +121,20 @@ declare class IDBStorage {
121
121
  close(): void;
122
122
  }
123
123
  //#endregion
124
+ //#region src/config.d.ts
125
+ /**
126
+ * Configure global defaults for IDBStorage.
127
+ */
128
+ declare function configureIDBStorage(config: Partial<IDBConfigValues>): void;
129
+ /**
130
+ * Get the current global configuration.
131
+ */
132
+ declare function getGlobalConfig(): IDBConfigValues;
133
+ //#endregion
124
134
  //#region src/hook.d.ts
125
135
  /**
126
136
  * Hook to persist state in IndexedDB with a clean object-based API.
137
+ * Optimized for performance - synchronous updates like useState with background IDB persistence.
127
138
  *
128
139
  * @param options - Configuration object containing key, defaultValue, and optional database/store names
129
140
  * @returns A tuple of the stored value, an async updater function, and a remove function.
@@ -183,16 +194,6 @@ declare function IDBConfig({
183
194
  ...conf
184
195
  }: IDBConfigProps): react_jsx_runtime0.JSX.Element;
185
196
  //#endregion
186
- //#region src/config.d.ts
187
- /**
188
- * Configure global defaults for IDBStorage.
189
- */
190
- declare function configureIDBStorage(config: Partial<IDBConfigValues>): void;
191
- /**
192
- * Get the current global configuration.
193
- */
194
- declare function getGlobalConfig(): IDBConfigValues;
195
- //#endregion
196
197
  //#region src/index.d.ts
197
198
  declare const idb: IDBStorage;
198
199
  //#endregion
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import*as e from"react";import{jsx as t}from"react/jsx-runtime";const n=new Map;function r(){return typeof indexedDB<`u`}function i(e,t,i=1,o){if(!r())throw Error(`IndexedDB is not available in this environment`);let s=`${e}:${i}:${t}`;if(n.has(s))return n.get(s);let c=a(e,t,i,o);return n.set(s,c),c}function a(e,t,r,i){return new Promise((o,s)=>{let c=indexedDB.open(e,r);c.onerror=()=>{n.delete(`${e}:${r}:${t}`),s(c.error)},c.onupgradeneeded=e=>{let n=e.target.result;n.objectStoreNames.contains(t)||n.createObjectStore(t)},c.onsuccess=()=>{let s=c.result;if(!s.objectStoreNames.contains(t)){s.close(),n.delete(`${e}:${r}:${t}`),o(a(e,t,r+1,i));return}s.onversionchange=()=>{s.close();for(let[t]of n)t.startsWith(`${e}:`)&&n.delete(t);i?.()},o(s)}})}function o(e,t,n){return new Promise((r,i)=>{try{let a=e.transaction([t],`readonly`).objectStore(t).get(n);a.onerror=()=>i(a.error),a.onsuccess=()=>r(a.result)}catch(e){i(e)}})}function s(e,t,n,r){return new Promise((i,a)=>{try{let o=e.transaction([t],`readwrite`).objectStore(t).put(r,n);o.onerror=()=>a(o.error),o.onsuccess=()=>i()}catch(e){a(e)}})}function c(e,t,n){return new Promise((r,i)=>{try{let a=e.transaction([t],`readwrite`).objectStore(t).delete(n);a.onerror=()=>i(a.error),a.onsuccess=()=>r()}catch(e){i(e)}})}var l=class{db;storeName;constructor(e,t){this.db=e,this.storeName=t}async get(e){try{return await o(this.db,this.storeName,e)}catch(t){throw console.error(`Failed to get value for key "${e}":`,t),t}}async set(e,t){try{await s(this.db,this.storeName,e,t)}catch(t){throw console.error(`Failed to set value for key "${e}":`,t),t}}async delete(e){try{await c(this.db,this.storeName,e)}catch(t){throw console.error(`Failed to delete key "${e}":`,t),t}}async getMany(e){try{let t=e.map(e=>this.get(e));return await Promise.all(t)}catch(e){throw console.error(`Failed to get multiple values:`,e),e}}async setMany(e){try{let t=this.db.transaction([this.storeName],`readwrite`).objectStore(this.storeName),n=e.map(([e,n])=>new Promise((r,i)=>{let a=t.put(n,e);a.onerror=()=>i(a.error),a.onsuccess=()=>r()}));await Promise.all(n)}catch(e){throw console.error(`Failed to set multiple values:`,e),e}}async deleteMany(e){try{let t=this.db.transaction([this.storeName],`readwrite`).objectStore(this.storeName),n=e.map(e=>new Promise((n,r)=>{let i=t.delete(e);i.onerror=()=>r(i.error),i.onsuccess=()=>n()}));await Promise.all(n)}catch(e){throw console.error(`Failed to delete multiple keys:`,e),e}}async update(e,t){try{let n=t(await this.get(e));await this.set(e,n)}catch(t){throw console.error(`Failed to update value for key "${e}":`,t),t}}async clear(){try{let e=this.db.transaction([this.storeName],`readwrite`).objectStore(this.storeName).clear();await new Promise((t,n)=>{e.onerror=()=>n(e.error),e.onsuccess=()=>t()})}catch(e){throw console.error(`Failed to clear store:`,e),e}}async keys(){try{let e=this.db.transaction([this.storeName],`readonly`).objectStore(this.storeName).getAllKeys();return await new Promise((t,n)=>{e.onerror=()=>n(e.error),e.onsuccess=()=>t(Array.from(e.result))})}catch(e){throw console.error(`Failed to get keys:`,e),e}}async values(){try{let e=this.db.transaction([this.storeName],`readonly`).objectStore(this.storeName).getAll();return await new Promise((t,n)=>{e.onerror=()=>n(e.error),e.onsuccess=()=>t(e.result)})}catch(e){throw console.error(`Failed to get values:`,e),e}}async entries(){try{let e=this.db.transaction([this.storeName],`readonly`).objectStore(this.storeName).openCursor(),t=[];return new Promise((n,r)=>{e.onerror=()=>r(e.error),e.onsuccess=()=>{let r=e.result;r?(t.push([r.key,r.value]),r.continue()):n(t)}})}catch(e){throw console.error(`Failed to get entries:`,e),e}}},u=class{config;db=null;dbPromise=null;constructor(e){this.config={database:`sohanemon-idb`,version:1,store:`default`,...e},this.config.version=Math.max(1,Math.floor(this.config.version))}async getDB(){return this.db?this.db:this.dbPromise?this.dbPromise:(this.dbPromise=i(this.config.database,this.config.store,this.config.version,()=>{this.db=null,this.dbPromise=null}),this.db=await this.dbPromise,this.dbPromise=null,this.db)}async get(e){return new l(await this.getDB(),e)}get store(){return this.get(this.config.store)}async drop(e){await(await this.get(e)).clear()}close(){this.db&&=(this.db.close(),null)}};let d={database:`sohanemon-idb`,version:1,store:`default`};function f(e){d={...d,...e}}function p(){return{...d}}const m=e.createContext(null);function h(){return e.useContext(m)||p()}function g({children:e,...n}){let r={database:n.database||`sohanemon-idb`,version:n.version||1,store:n.store||`default`};return t(m.Provider,{value:r,children:e})}function _(t){let{key:n,defaultValue:i,...a}=t,o={...h(),...a};o.version=Math.max(1,Math.floor(o.version||1));let[s,c]=e.useState(i),[l,d]=e.useState(null),[f,p]=e.useState(!1),m=e.useRef(null),g=e.useRef(null),_=e.useRef([]);return e.useEffect(()=>{if(typeof window>`u`)return;let e=!0;return(async()=>{try{if(!r()){console.warn(`IndexedDB is not available, using default values only`);return}d(null),m.current&&(m.current.close(),m.current=null,g.current=null);let t=new u(o),i=await t.get(o.store);if(!e)return;m.current=t,g.current=i;let a=await i.get(n);if(e&&a!==void 0&&c(a),e){p(!0);let e=_.current;_.current=[];for(let t of e)t().catch(e=>{console.error(`Failed to process pending update:`,e)})}}catch(t){e&&(d(t instanceof Error?t:Error(`Failed to load from IndexedDB`)),console.error(`Failed to initialize IDBStorage:`,t))}})(),()=>{e=!1,m.current&&(m.current.close(),m.current=null,g.current=null)}},[o.database,o.version,o.store,n]),[s,e.useCallback(async e=>{if(l){console.warn(`Cannot update value due to previous error:`,l);return}let t=typeof e==`function`?e(s):e;c(t);let r=async()=>{if(g.current)await g.current.set(n,t);else throw Error(`Store not initialized`)};if(f)try{await r()}catch(e){throw c(s),d(e instanceof Error?e:Error(`Failed to save to IndexedDB`)),console.error(`Failed to save value to IndexedDB:`,e),e}else _.current.push(r)},[s,n,l,f]),e.useCallback(async()=>{if(l){console.warn(`Cannot remove value due to previous error:`,l);return}c(i);let e=async()=>{if(g.current)await g.current.delete(n);else throw Error(`Store not initialized`)};if(f)try{await e()}catch(e){throw c(s),d(e instanceof Error?e:Error(`Failed to remove from IndexedDB`)),console.error(`Failed to remove value from IndexedDB:`,e),e}else _.current.push(e)},[s,n,i,l,f])]}const v=new u;export{g as IDBConfig,u as IDBStorage,l as IDBStore,f as configureIDBStorage,p as getGlobalConfig,v as idb,h as useIDBConfig,_ as useIDBStorage};
1
+ import*as e from"react";import{jsx as t}from"react/jsx-runtime";const n=new Map;function r(){return typeof indexedDB<`u`}function i(e,t,i=1,o){if(!r())throw Error(`IndexedDB is not available in this environment`);let s=`${e}:${i}:${t}`;if(n.has(s))return n.get(s);let c=a(e,t,i,o);return n.set(s,c),c}function a(e,t,r,i){return new Promise((o,s)=>{let c=indexedDB.open(e,r);c.onerror=()=>{let l=`${e}:${r??`current`}:${t}`;if(n.delete(l),c.error?.name===`VersionError`&&c.error.message.includes(`less than`)){o(a(e,t,void 0,i));return}s(c.error)},c.onupgradeneeded=e=>{let n=e.target.result;n.objectStoreNames.contains(t)||n.createObjectStore(t)},c.onsuccess=()=>{let s=c.result,l=s.version;if(!s.objectStoreNames.contains(t)){s.close();let c=`${e}:${r??`current`}:${t}`;n.delete(c),o(a(e,t,l+1,i));return}s.onversionchange=()=>{s.close();for(let[t]of n)t.startsWith(`${e}:`)&&n.delete(t);i?.()},o(s)}})}function o(e,t,n){return new Promise((r,i)=>{try{let a=e.transaction([t],`readonly`).objectStore(t).get(n);a.onerror=()=>i(a.error),a.onsuccess=()=>r(a.result)}catch(e){i(e)}})}function s(e,t,n,r){return new Promise((i,a)=>{try{let o=e.transaction([t],`readwrite`).objectStore(t).put(r,n);o.onerror=()=>a(o.error),o.onsuccess=()=>i()}catch(e){a(e)}})}function c(e,t,n){return new Promise((r,i)=>{try{let a=e.transaction([t],`readwrite`).objectStore(t).delete(n);a.onerror=()=>i(a.error),a.onsuccess=()=>r()}catch(e){i(e)}})}var l=class{db;storeName;constructor(e,t){this.db=e,this.storeName=t}async get(e){try{return await o(this.db,this.storeName,e)}catch(t){throw console.error(`Failed to get value for key "${e}":`,t),t}}async set(e,t){try{await s(this.db,this.storeName,e,t)}catch(t){throw console.error(`Failed to set value for key "${e}":`,t),t}}async delete(e){try{await c(this.db,this.storeName,e)}catch(t){throw console.error(`Failed to delete key "${e}":`,t),t}}async getMany(e){try{let t=e.map(e=>this.get(e));return await Promise.all(t)}catch(e){throw console.error(`Failed to get multiple values:`,e),e}}async setMany(e){try{let t=this.db.transaction([this.storeName],`readwrite`).objectStore(this.storeName),n=e.map(([e,n])=>new Promise((r,i)=>{let a=t.put(n,e);a.onerror=()=>i(a.error),a.onsuccess=()=>r()}));await Promise.all(n)}catch(e){throw console.error(`Failed to set multiple values:`,e),e}}async deleteMany(e){try{let t=this.db.transaction([this.storeName],`readwrite`).objectStore(this.storeName),n=e.map(e=>new Promise((n,r)=>{let i=t.delete(e);i.onerror=()=>r(i.error),i.onsuccess=()=>n()}));await Promise.all(n)}catch(e){throw console.error(`Failed to delete multiple keys:`,e),e}}async update(e,t){try{let n=t(await this.get(e));await this.set(e,n)}catch(t){throw console.error(`Failed to update value for key "${e}":`,t),t}}async clear(){try{let e=this.db.transaction([this.storeName],`readwrite`).objectStore(this.storeName).clear();await new Promise((t,n)=>{e.onerror=()=>n(e.error),e.onsuccess=()=>t()})}catch(e){throw console.error(`Failed to clear store:`,e),e}}async keys(){try{let e=this.db.transaction([this.storeName],`readonly`).objectStore(this.storeName).getAllKeys();return await new Promise((t,n)=>{e.onerror=()=>n(e.error),e.onsuccess=()=>t(Array.from(e.result))})}catch(e){throw console.error(`Failed to get keys:`,e),e}}async values(){try{let e=this.db.transaction([this.storeName],`readonly`).objectStore(this.storeName).getAll();return await new Promise((t,n)=>{e.onerror=()=>n(e.error),e.onsuccess=()=>t(e.result)})}catch(e){throw console.error(`Failed to get values:`,e),e}}async entries(){try{let e=this.db.transaction([this.storeName],`readonly`).objectStore(this.storeName).openCursor(),t=[];return new Promise((n,r)=>{e.onerror=()=>r(e.error),e.onsuccess=()=>{let r=e.result;r?(t.push([r.key,r.value]),r.continue()):n(t)}})}catch(e){throw console.error(`Failed to get entries:`,e),e}}},u=class{config;db=null;dbPromise=null;constructor(e){this.config={database:`sohanemon-idb`,version:1,store:`default`,...e},this.config.version=Math.max(1,Math.floor(this.config.version))}async getDB(){return this.db?this.db:this.dbPromise?this.dbPromise:(this.dbPromise=i(this.config.database,this.config.store,this.config.version,()=>{this.db=null,this.dbPromise=null}),this.db=await this.dbPromise,this.dbPromise=null,this.db)}async get(e){return new l(await this.getDB(),e)}get store(){return this.get(this.config.store)}async drop(e){await(await this.get(e)).clear()}close(){this.db&&=(this.db.close(),null)}};let d={database:`sohanemon-idb`,version:1,store:`default`};function f(e){d={...d,...e}}function p(){return{...d}}const m=e.createContext(null);function h(){return e.useContext(m)||p()}function g({children:e,...n}){let r={database:n.database||`sohanemon-idb`,version:n.version||1,store:n.store||`default`};return t(m.Provider,{value:r,children:e})}function _(t){let{key:n,defaultValue:i,...a}=t,o=h(),s=a.database||o.database,c=Math.max(1,Math.floor(a.version||o.version||1)),l=a.store||o.store,[d,f]=e.useState(i),p=e.useRef(!1),m=e.useRef(null),g=e.useRef(null),_=e.useRef(null),v=e.useRef(null),y=e.useRef(!1);e.useEffect(()=>{if(typeof window>`u`)return;let e=!0;return(async()=>{try{if(!r()){console.warn(`IndexedDB is not available, using default values only`),p.current=!0,y.current=!0;return}m.current&&m.current.close();let t=new u({database:s,version:c,store:l}),i=await t.get(l);if(!e)return;m.current=t,g.current=i;let a=await i.get(n);e&&a!==void 0&&f(a),p.current=!0,y.current=!0}catch(e){console.error(`Failed to initialize IDBStorage:`,e),p.current=!0,y.current=!0}})(),()=>{e=!1,_.current&&(clearTimeout(_.current),v.current!==null&&g.current&&g.current.set(n,v.current).catch(console.error)),m.current&&(m.current.close(),m.current=null,g.current=null)}},[s,c,l,n]);let b=e.useCallback(e=>{!p.current||!g.current||!y.current||(v.current=e,_.current&&clearTimeout(_.current),_.current=setTimeout(()=>{let e=v.current;v.current=null,e!==null&&g.current&&g.current.set(n,e).catch(e=>{console.error(`Failed to save value to IndexedDB:`,e)})},0))},[n]);return[d,e.useCallback(e=>{f(t=>{let n=typeof e==`function`?e(t):e;return b(n),n})},[b]),e.useCallback(()=>{f(i),b(i)},[i,b])]}const v=new u;export{g as IDBConfig,u as IDBStorage,l as IDBStore,f as configureIDBStorage,p as getGlobalConfig,v as idb,h as useIDBConfig,_ as useIDBStorage};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "use-idb-storage",
3
- "version": "0.0.8",
3
+ "version": "0.1.1",
4
4
  "author": "Sohan Emon <sohanemon@outlook.com>",
5
5
  "description": "",
6
6
  "type": "module",
@@ -19,6 +19,7 @@
19
19
  "test:list": "vitest list",
20
20
  "test:log": "vitest run --reporter verbose",
21
21
  "test:ui": "vitest --ui",
22
+ "bench": "vitest bench",
22
23
  "build:playground": "vite build --config playground/vite.config.ts",
23
24
  "playground": "tsdown && vite build --config playground/vite.config.ts && vite --config playground/vite.config.ts",
24
25
  "check": "biome check .",
@@ -57,6 +58,7 @@
57
58
  "@vitest/ui": "^4.0.14",
58
59
  "fake-indexeddb": "^6.2.5",
59
60
  "jsdom": "^27.2.0",
61
+ "react-scan": "^0.4.3",
60
62
  "tsdown": "^0.16.4",
61
63
  "typescript": "^5.9.3",
62
64
  "vite": "npm:rolldown-vite@^7.2.5",