use-idb-storage 0.1.1 → 0.1.3
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 +320 -100
- package/dist/index.d.ts +55 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,39 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# useIDBStorage
|
|
2
|
+
|
|
2
3
|

|
|
3
4
|

|
|
4
5
|

|
|
5
6
|

|
|
6
7
|
|
|
7
|
-
A
|
|
8
|
-
|
|
9
|
-
## Features
|
|
10
|
-
|
|
11
|
-
- 🚀 **Simple API**: Easy-to-use hooks and classes
|
|
12
|
-
- 🔄 **Reactive**: React hooks with automatic re-renders
|
|
13
|
-
- 🏪 **Multi-store**: Support for multiple object stores
|
|
14
|
-
- 📦 **Batch operations**: Efficient bulk operations
|
|
15
|
-
- 🔧 **TypeScript**: Full type safety
|
|
16
|
-
- 🎯 **Promise-based**: Modern async/await support
|
|
8
|
+
A React hook for IndexedDB state management with automatic persistence, similar to `useState` but with data persistence across sessions.
|
|
17
9
|
|
|
18
10
|
## Performance
|
|
19
11
|
|
|
20
|
-
|
|
12
|
+
Near-native performance with minimal overhead. Benchmark shows only 1.5ms difference vs `useState` for 400 forced updates:
|
|
21
13
|
|
|
22
14
|
```json
|
|
23
15
|
{
|
|
24
|
-
"useState": {
|
|
25
|
-
|
|
26
|
-
"time": 3317.9
|
|
27
|
-
},
|
|
28
|
-
"useIDBStorage": {
|
|
29
|
-
"renders": 400,
|
|
30
|
-
"time": 3316.4
|
|
31
|
-
},
|
|
16
|
+
"useState": { "renders": 400, "time": 3317.9 },
|
|
17
|
+
"useIDBStorage": { "renders": 400, "time": 3316.4 },
|
|
32
18
|
"differenceMs": 1.5
|
|
33
19
|
}
|
|
34
20
|
```
|
|
35
21
|
|
|
36
|
-
|
|
22
|
+
See [PERFORMANCE.md](./PERFORMANCE.md) for detailed benchmarks.
|
|
37
23
|
|
|
38
24
|
## Installation
|
|
39
25
|
|
|
@@ -43,32 +29,10 @@ npm install use-idb-storage
|
|
|
43
29
|
|
|
44
30
|
## Quick Start
|
|
45
31
|
|
|
46
|
-
###
|
|
47
|
-
|
|
48
|
-
```typescript
|
|
49
|
-
import { idb } from 'use-idb-storage';
|
|
50
|
-
|
|
51
|
-
// Use default store immediately
|
|
52
|
-
await idb.store.set('user', { name: 'John' });
|
|
53
|
-
const user = await idb.store.get('user');
|
|
54
|
-
|
|
55
|
-
// Use custom store
|
|
56
|
-
const customStore = await idb.get('settings');
|
|
57
|
-
await customStore.set('theme', 'dark');
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### React Hook (Recommended for React apps)
|
|
32
|
+
### Basic Usage
|
|
61
33
|
|
|
62
34
|
```tsx
|
|
63
|
-
import { useIDBStorage
|
|
64
|
-
|
|
65
|
-
function App() {
|
|
66
|
-
return (
|
|
67
|
-
<IDBConfig database="my-app" version={1} store="data">
|
|
68
|
-
<MyComponent />
|
|
69
|
-
</IDBConfig>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
35
|
+
import { useIDBStorage } from 'use-idb-storage';
|
|
72
36
|
|
|
73
37
|
function MyComponent() {
|
|
74
38
|
const [user, setUser, removeUser] = useIDBStorage({
|
|
@@ -83,41 +47,84 @@ function MyComponent() {
|
|
|
83
47
|
onChange={e => setUser({ ...user, name: e.target.value })}
|
|
84
48
|
/>
|
|
85
49
|
<button onClick={() => removeUser()}>Clear</button>
|
|
50
|
+
<p>Data persists across browser sessions.</p>
|
|
86
51
|
</div>
|
|
87
52
|
);
|
|
88
53
|
}
|
|
89
54
|
```
|
|
90
55
|
|
|
91
|
-
###
|
|
56
|
+
### Advanced Usage
|
|
57
|
+
|
|
58
|
+
Use object destructuring for additional features:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
function AdvancedComponent() {
|
|
62
|
+
const {
|
|
63
|
+
data: user,
|
|
64
|
+
update: setUser,
|
|
65
|
+
reset: clearUser,
|
|
66
|
+
loading,
|
|
67
|
+
persisted,
|
|
68
|
+
error,
|
|
69
|
+
lastUpdated,
|
|
70
|
+
refresh
|
|
71
|
+
} = useIDBStorage({
|
|
72
|
+
key: 'user-profile',
|
|
73
|
+
defaultValue: { name: '', email: '', avatar: null }
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (loading) return <div>Loading...</div>;
|
|
77
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div>
|
|
81
|
+
<h2>{user.name || 'Anonymous'}</h2>
|
|
82
|
+
<button onClick={() => setUser(prev => ({
|
|
83
|
+
...prev,
|
|
84
|
+
loginCount: (prev.loginCount || 0) + 1
|
|
85
|
+
}))}>
|
|
86
|
+
Increment Login Count
|
|
87
|
+
</button>
|
|
88
|
+
<button onClick={() => setUser({ ...user, name: 'John' })}>
|
|
89
|
+
Set Name
|
|
90
|
+
</button>
|
|
91
|
+
<button onClick={clearUser}>Reset</button>
|
|
92
|
+
<button onClick={() => refresh()}>Refresh from DB</button>
|
|
93
|
+
<div>
|
|
94
|
+
Status: {persisted ? 'Saved' : 'Saving...'}
|
|
95
|
+
{lastUpdated && (
|
|
96
|
+
<span>Last saved: {lastUpdated.toLocaleTimeString()}</span>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Class-based API
|
|
92
105
|
|
|
93
106
|
```tsx
|
|
94
107
|
import { IDBStorage } from 'use-idb-storage';
|
|
95
108
|
|
|
96
|
-
// Create storage instance
|
|
97
109
|
const db = new IDBStorage({
|
|
98
110
|
database: 'my-app',
|
|
99
111
|
version: 1,
|
|
100
112
|
store: 'data'
|
|
101
113
|
});
|
|
102
114
|
|
|
103
|
-
// Get a store
|
|
104
115
|
const store = await db.get('my-store');
|
|
105
|
-
|
|
106
|
-
const defaultStore = await db.store; // Convenience getter
|
|
116
|
+
const defaultStore = await db.store;
|
|
107
117
|
|
|
108
|
-
// Basic operations
|
|
109
118
|
await store.set('user', { name: 'John', age: 30 });
|
|
110
119
|
const user = await store.get('user');
|
|
111
120
|
await store.delete('user');
|
|
112
121
|
|
|
113
|
-
// Batch operations
|
|
114
122
|
await store.setMany([
|
|
115
123
|
['key1', 'value1'],
|
|
116
124
|
['key2', 'value2']
|
|
117
125
|
]);
|
|
118
126
|
const values = await store.getMany(['key1', 'key2']);
|
|
119
127
|
|
|
120
|
-
// Advanced operations
|
|
121
128
|
await store.update('counter', (val) => (val || 0) + 1);
|
|
122
129
|
const allKeys = await store.keys();
|
|
123
130
|
await store.clear();
|
|
@@ -184,29 +191,71 @@ Provides operations for a specific object store.
|
|
|
184
191
|
|
|
185
192
|
### useIDBStorage Hook
|
|
186
193
|
|
|
187
|
-
|
|
194
|
+
Supports both simple `useState`-style usage and advanced object destructuring.
|
|
188
195
|
|
|
189
|
-
|
|
190
|
-
|
|
196
|
+
#### Basic Usage
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
const [value, setValue, removeValue] = useIDBStorage({
|
|
200
|
+
key: 'my-key',
|
|
201
|
+
defaultValue: 'default value'
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
setValue('new value');
|
|
205
|
+
setValue(prev => prev + ' updated');
|
|
206
|
+
removeValue();
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### Advanced Usage
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
const {
|
|
213
|
+
data,
|
|
214
|
+
update,
|
|
215
|
+
reset,
|
|
216
|
+
loading,
|
|
217
|
+
persisted,
|
|
218
|
+
error,
|
|
219
|
+
lastUpdated,
|
|
220
|
+
refresh
|
|
221
|
+
} = useIDBStorage({
|
|
222
|
+
key: 'my-key',
|
|
223
|
+
defaultValue: 'default value'
|
|
224
|
+
});
|
|
191
225
|
```
|
|
192
226
|
|
|
193
227
|
#### Parameters
|
|
194
228
|
|
|
195
229
|
```typescript
|
|
196
230
|
interface IDBStorageOptions<T> {
|
|
197
|
-
key: string;
|
|
198
|
-
defaultValue: T;
|
|
199
|
-
database?: string;
|
|
200
|
-
version?: number;
|
|
201
|
-
store?: string;
|
|
231
|
+
key: string; // Unique key for the stored value
|
|
232
|
+
defaultValue: T; // Default value if none exists in IndexedDB
|
|
233
|
+
database?: string; // Database name (uses context default)
|
|
234
|
+
version?: number; // Database version (uses context default)
|
|
235
|
+
store?: string; // Object store name (uses context default)
|
|
202
236
|
}
|
|
203
237
|
```
|
|
204
238
|
|
|
205
|
-
####
|
|
239
|
+
#### Return Types
|
|
206
240
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
241
|
+
**Tuple Destructuring** (useState-compatible):
|
|
242
|
+
```typescript
|
|
243
|
+
[value: T, setValue: (value: T | ((prev: T) => T)) => void, removeValue: () => void]
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Object Destructuring** (Full-featured):
|
|
247
|
+
```typescript
|
|
248
|
+
{
|
|
249
|
+
data: T, // The stored data
|
|
250
|
+
update: (value: T | ((prev: T) => T)) => void, // Update function
|
|
251
|
+
reset: () => void, // Reset to default
|
|
252
|
+
loading: boolean, // Loading state
|
|
253
|
+
persisted: boolean, // Persistence status
|
|
254
|
+
error: Error | null, // Error state
|
|
255
|
+
lastUpdated: Date | null, // Last update timestamp
|
|
256
|
+
refresh: () => Promise<void> // Force refresh from DB
|
|
257
|
+
}
|
|
258
|
+
```
|
|
210
259
|
|
|
211
260
|
### IDBConfig Provider
|
|
212
261
|
|
|
@@ -228,72 +277,243 @@ interface IDBConfigValues {
|
|
|
228
277
|
}
|
|
229
278
|
```
|
|
230
279
|
|
|
231
|
-
##
|
|
280
|
+
## Examples
|
|
232
281
|
|
|
233
|
-
###
|
|
282
|
+
### User Authentication
|
|
234
283
|
|
|
235
|
-
```
|
|
236
|
-
|
|
284
|
+
```tsx
|
|
285
|
+
function AuthProvider({ children }) {
|
|
286
|
+
const { data: user, update: setUser, reset: logout, loading } = useIDBStorage({
|
|
287
|
+
key: 'auth-user',
|
|
288
|
+
defaultValue: null
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const login = async (credentials) => {
|
|
292
|
+
try {
|
|
293
|
+
const userData = await api.login(credentials);
|
|
294
|
+
setUser(userData);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.error('Login failed:', error);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
237
299
|
|
|
238
|
-
|
|
239
|
-
const users = await db.get('users');
|
|
240
|
-
const settings = await db.get('settings');
|
|
300
|
+
if (loading) return <div>Loading...</div>;
|
|
241
301
|
|
|
242
|
-
|
|
243
|
-
|
|
302
|
+
return (
|
|
303
|
+
<AuthContext.Provider value={{ user, login, logout }}>
|
|
304
|
+
{children}
|
|
305
|
+
</AuthContext.Provider>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
244
308
|
```
|
|
245
309
|
|
|
246
|
-
###
|
|
310
|
+
### Shopping Cart
|
|
247
311
|
|
|
248
|
-
```
|
|
249
|
-
|
|
312
|
+
```tsx
|
|
313
|
+
function ShoppingCart() {
|
|
314
|
+
const {
|
|
315
|
+
data: cart,
|
|
316
|
+
update: updateCart,
|
|
317
|
+
reset: clearCart,
|
|
318
|
+
persisted,
|
|
319
|
+
lastUpdated
|
|
320
|
+
} = useIDBStorage({
|
|
321
|
+
key: 'shopping-cart',
|
|
322
|
+
defaultValue: { items: [], total: 0 }
|
|
323
|
+
});
|
|
250
324
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
325
|
+
const addItem = (item) => {
|
|
326
|
+
updateCart(prev => ({
|
|
327
|
+
items: [...prev.items, item],
|
|
328
|
+
total: prev.total + item.price
|
|
329
|
+
}));
|
|
330
|
+
};
|
|
257
331
|
|
|
258
|
-
|
|
259
|
-
|
|
332
|
+
return (
|
|
333
|
+
<div>
|
|
334
|
+
<h2>Cart ({cart.items.length} items)</h2>
|
|
335
|
+
{cart.items.map(item => (
|
|
336
|
+
<div key={item.id}>{item.name} - ${item.price}</div>
|
|
337
|
+
))}
|
|
338
|
+
<p>Total: ${cart.total}</p>
|
|
339
|
+
<button onClick={clearCart}>Clear Cart</button>
|
|
340
|
+
<div>
|
|
341
|
+
Status: {persisted ? 'Saved' : 'Saving...'}
|
|
342
|
+
{lastUpdated && <span> • Saved {lastUpdated.toLocaleTimeString()}</span>}
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
260
347
|
```
|
|
261
348
|
|
|
262
|
-
###
|
|
349
|
+
### Auto-Save Form
|
|
263
350
|
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
351
|
+
```tsx
|
|
352
|
+
function AutoSaveForm() {
|
|
353
|
+
const {
|
|
354
|
+
data: formData,
|
|
355
|
+
update: updateForm,
|
|
356
|
+
loading,
|
|
357
|
+
error,
|
|
358
|
+
lastUpdated
|
|
359
|
+
} = useIDBStorage({
|
|
360
|
+
key: 'draft-form',
|
|
361
|
+
defaultValue: { title: '', content: '', tags: [] }
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
React.useEffect(() => {
|
|
365
|
+
const timer = setTimeout(() => {
|
|
366
|
+
console.log('Auto-saved at', new Date().toLocaleTimeString());
|
|
367
|
+
}, 1000);
|
|
368
|
+
|
|
369
|
+
return () => clearTimeout(timer);
|
|
370
|
+
}, [formData]);
|
|
269
371
|
|
|
270
|
-
|
|
271
|
-
|
|
372
|
+
if (loading) return <div>Loading draft...</div>;
|
|
373
|
+
if (error) return <div>Failed to load draft: {error.message}</div>;
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<form>
|
|
377
|
+
<input
|
|
378
|
+
value={formData.title}
|
|
379
|
+
onChange={e => updateForm(prev => ({ ...prev, title: e.target.value }))}
|
|
380
|
+
placeholder="Title"
|
|
381
|
+
/>
|
|
382
|
+
<textarea
|
|
383
|
+
value={formData.content}
|
|
384
|
+
onChange={e => updateForm(prev => ({ ...prev, content: e.target.value }))}
|
|
385
|
+
placeholder="Content"
|
|
386
|
+
/>
|
|
387
|
+
<div>
|
|
388
|
+
{lastUpdated && (
|
|
389
|
+
<small>Last saved: {lastUpdated.toLocaleTimeString()}</small>
|
|
390
|
+
)}
|
|
391
|
+
</div>
|
|
392
|
+
</form>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
272
395
|
```
|
|
273
396
|
|
|
274
|
-
###
|
|
397
|
+
### Error Handling
|
|
275
398
|
|
|
276
|
-
|
|
399
|
+
```tsx
|
|
400
|
+
function RobustComponent() {
|
|
401
|
+
const {
|
|
402
|
+
data: settings,
|
|
403
|
+
update: updateSettings,
|
|
404
|
+
reset: resetSettings,
|
|
405
|
+
error,
|
|
406
|
+
refresh,
|
|
407
|
+
loading
|
|
408
|
+
} = useIDBStorage({
|
|
409
|
+
key: 'user-settings',
|
|
410
|
+
defaultValue: { theme: 'light', notifications: true }
|
|
411
|
+
});
|
|
277
412
|
|
|
278
|
-
|
|
279
|
-
|
|
413
|
+
if (error) {
|
|
414
|
+
return (
|
|
415
|
+
<div>
|
|
416
|
+
<p>Failed to load settings: {error.message}</p>
|
|
417
|
+
<button onClick={() => refresh()}>Retry</button>
|
|
418
|
+
<button onClick={resetSettings}>Reset to Defaults</button>
|
|
419
|
+
</div>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (loading) return <div>Loading settings...</div>;
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
<div>
|
|
427
|
+
<label>
|
|
428
|
+
Theme:
|
|
429
|
+
<select
|
|
430
|
+
value={settings.theme}
|
|
431
|
+
onChange={e => updateSettings(prev => ({
|
|
432
|
+
...prev,
|
|
433
|
+
theme: e.target.value
|
|
434
|
+
}))}
|
|
435
|
+
>
|
|
436
|
+
<option value="light">Light</option>
|
|
437
|
+
<option value="dark">Dark</option>
|
|
438
|
+
</select>
|
|
439
|
+
</label>
|
|
440
|
+
</div>
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Advanced Usage
|
|
446
|
+
|
|
447
|
+
### Custom Configuration
|
|
448
|
+
|
|
449
|
+
```tsx
|
|
450
|
+
import { IDBConfig } from 'use-idb-storage';
|
|
451
|
+
|
|
452
|
+
function App() {
|
|
453
|
+
return (
|
|
454
|
+
<IDBConfig database="my-app" version={2} store="main">
|
|
455
|
+
<MyComponents />
|
|
456
|
+
</IDBConfig>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function MyComponents() {
|
|
461
|
+
const [user] = useIDBStorage({
|
|
462
|
+
key: 'user',
|
|
463
|
+
defaultValue: { name: '' }
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const [cache] = useIDBStorage({
|
|
467
|
+
key: 'api-cache',
|
|
468
|
+
defaultValue: {},
|
|
469
|
+
store: 'cache'
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
return <div>...</div>;
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Migration
|
|
477
|
+
|
|
478
|
+
Increment version for schema changes:
|
|
479
|
+
|
|
480
|
+
```tsx
|
|
280
481
|
<IDBConfig database="my-app" version={1} store="data">
|
|
281
482
|
<App />
|
|
282
483
|
</IDBConfig>
|
|
283
484
|
|
|
284
|
-
// Version 2 (with new features)
|
|
285
485
|
<IDBConfig database="my-app" version={2} store="data">
|
|
286
486
|
<App />
|
|
287
487
|
</IDBConfig>
|
|
288
488
|
```
|
|
289
489
|
|
|
290
|
-
|
|
490
|
+
### Class-based API
|
|
491
|
+
|
|
492
|
+
```tsx
|
|
493
|
+
import { IDBStorage } from 'use-idb-storage';
|
|
494
|
+
|
|
495
|
+
const db = new IDBStorage({ database: 'my-app' });
|
|
496
|
+
const store = await db.get('analytics');
|
|
497
|
+
|
|
498
|
+
await store.setMany([
|
|
499
|
+
['pageviews', 1234],
|
|
500
|
+
['sessions', 89],
|
|
501
|
+
['bounce-rate', 0.45]
|
|
502
|
+
]);
|
|
503
|
+
|
|
504
|
+
const allKeys = await store.keys();
|
|
505
|
+
const allData = await store.values();
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
## 🌐 Browser Support
|
|
291
511
|
|
|
292
512
|
- Chrome 24+
|
|
293
513
|
- Firefox 16+
|
|
294
514
|
- Safari 10+
|
|
295
515
|
- Edge 12+
|
|
296
516
|
|
|
297
|
-
## License
|
|
517
|
+
## 📄 License
|
|
298
518
|
|
|
299
519
|
ISC
|
package/dist/index.d.ts
CHANGED
|
@@ -30,8 +30,37 @@ interface IDBConfigProps extends Partial<IDBConfigValues> {
|
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* Return type of the useIDBStorage hook
|
|
33
|
+
* Supports both tuple destructuring: const [value, setValue, removeValue] = useIDBStorage()
|
|
34
|
+
* and object destructuring: const { value, update, reset, loading, persisted, ... } = useIDBStorage()
|
|
33
35
|
*/
|
|
34
|
-
type UseIDBStorageReturn<T> =
|
|
36
|
+
type UseIDBStorageReturn<T> = {
|
|
37
|
+
/** The current stored value */
|
|
38
|
+
0: T;
|
|
39
|
+
/** Function to update the stored value */
|
|
40
|
+
1: (value: T | ((prevState: T) => T)) => void;
|
|
41
|
+
/** Function to remove the stored value and reset to default */
|
|
42
|
+
2: () => void;
|
|
43
|
+
/** The current stored data (alias for index 0) */
|
|
44
|
+
data: T;
|
|
45
|
+
/** Function to update the stored value (alias for index 1) */
|
|
46
|
+
update: (value: T | ((prevState: T) => T)) => void;
|
|
47
|
+
/** Function to remove the stored value and reset to default (alias for index 2) */
|
|
48
|
+
reset: () => void;
|
|
49
|
+
/** Whether the hook is still loading initial value from IndexedDB */
|
|
50
|
+
loading: boolean;
|
|
51
|
+
/** Whether the current value has been loaded/persisted to IndexedDB */
|
|
52
|
+
persisted: boolean;
|
|
53
|
+
/** Error that occurred during loading/persistence, if any */
|
|
54
|
+
error: Error | null;
|
|
55
|
+
/** Timestamp of last successful persistence */
|
|
56
|
+
lastUpdated: Date | null;
|
|
57
|
+
/** Force refresh value from IndexedDB */
|
|
58
|
+
refresh: () => Promise<void>;
|
|
59
|
+
/** Length for array-like behavior */
|
|
60
|
+
length: 3;
|
|
61
|
+
/** Iterator for array destructuring */
|
|
62
|
+
[Symbol.iterator](): Iterator<T | ((value: T | ((prevState: T) => T)) => void) | (() => void)>;
|
|
63
|
+
};
|
|
35
64
|
//#endregion
|
|
36
65
|
//#region src/idb-storage.d.ts
|
|
37
66
|
/**
|
|
@@ -137,7 +166,9 @@ declare function getGlobalConfig(): IDBConfigValues;
|
|
|
137
166
|
* Optimized for performance - synchronous updates like useState with background IDB persistence.
|
|
138
167
|
*
|
|
139
168
|
* @param options - Configuration object containing key, defaultValue, and optional database/store names
|
|
140
|
-
* @returns
|
|
169
|
+
* @returns An object that supports both tuple and object destructuring:
|
|
170
|
+
* - Tuple: const [value, setValue, removeValue] = useIDBStorage()
|
|
171
|
+
* - Object: const { data, update, reset, loading, persisted, error, lastUpdated, refresh } = useIDBStorage()
|
|
141
172
|
*
|
|
142
173
|
* @example
|
|
143
174
|
* ```tsx
|
|
@@ -149,6 +180,7 @@ declare function getGlobalConfig(): IDBConfigValues;
|
|
|
149
180
|
* // Option 2: Global config
|
|
150
181
|
* configureIDBStorage({ database: 'myApp', version: 2, store: 'data' });
|
|
151
182
|
*
|
|
183
|
+
* // Tuple destructuring (like useState)
|
|
152
184
|
* const [userData, setUserData, removeUserData] = useIDBStorage({
|
|
153
185
|
* key: 'currentUser',
|
|
154
186
|
* defaultValue: { name: '', email: '' },
|
|
@@ -157,6 +189,27 @@ declare function getGlobalConfig(): IDBConfigValues;
|
|
|
157
189
|
* // store: 'users' // optional, uses context or global default
|
|
158
190
|
* });
|
|
159
191
|
*
|
|
192
|
+
* // Object destructuring (with powerful features)
|
|
193
|
+
* const {
|
|
194
|
+
* data: userData,
|
|
195
|
+
* update: updateUserData,
|
|
196
|
+
* reset: resetUserData,
|
|
197
|
+
* loading,
|
|
198
|
+
* persisted,
|
|
199
|
+
* error,
|
|
200
|
+
* lastUpdated,
|
|
201
|
+
* refresh
|
|
202
|
+
* } = useIDBStorage({
|
|
203
|
+
* key: 'currentUser',
|
|
204
|
+
* defaultValue: { name: '', email: '' },
|
|
205
|
+
* });
|
|
206
|
+
*
|
|
207
|
+
* // Use powerful features
|
|
208
|
+
* if (error) console.error('Storage error:', error);
|
|
209
|
+
* await refresh(); // Force reload from IndexedDB
|
|
210
|
+
* updateUserData({ name: 'John', email: 'john@example.com' }); // Direct update
|
|
211
|
+
* updateUserData(prev => ({ ...prev, lastLogin: new Date() })); // Functional update
|
|
212
|
+
*
|
|
160
213
|
* // Update data
|
|
161
214
|
* await setUserData({ name: 'John', email: 'john@example.com' });
|
|
162
215
|
*
|
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=()=>{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),
|
|
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,m]=e.useState(null),[g,_]=e.useState(null),v=e.useRef(!1),y=e.useRef(null),b=e.useRef(null),x=e.useRef(null),S=e.useRef(null),C=e.useRef(!1),w=e.useRef(null);e.useEffect(()=>{if(typeof window>`u`)return;let e=!0;return(async()=>{try{if(m(null),!r()){console.warn(`IndexedDB is not available, using default values only`),w.current=i,v.current=!0,C.current=!0;return}y.current&&y.current.close();let t=new u({database:s,version:c,store:l}),a=await t.get(l);if(!e)return;y.current=t,b.current=a;let o=await a.get(n);e&&(o===void 0?w.current=i:(f(o),w.current=o,_(new Date))),v.current=!0,C.current=!0}catch(e){console.error(`Failed to initialize IDBStorage:`,e),m(e instanceof Error?e:Error(String(e))),w.current=i,v.current=!0,C.current=!0}})(),()=>{e=!1,x.current&&(clearTimeout(x.current),S.current!==null&&b.current&&b.current.set(n,S.current).catch(console.error)),y.current&&(y.current.close(),y.current=null,b.current=null)}},[s,c,l,n]);let T=e.useCallback(e=>{!v.current||!b.current||!C.current||(S.current=e,x.current&&clearTimeout(x.current),x.current=setTimeout(()=>{let e=S.current;S.current=null,e!==null&&b.current&&b.current.set(n,e).then(()=>{_(new Date),w.current=e}).catch(e=>{console.error(`Failed to save value to IndexedDB:`,e),m(e instanceof Error?e:Error(String(e)))})},0))},[n]),E=e.useCallback(e=>{f(t=>{let n=typeof e==`function`?e(t):e;return T(n),n})},[T]),D=e.useCallback(()=>{f(i),_(null),w.current=i,T(i)},[i,T]),O=e.useCallback(async()=>{if(!(!r()||!b.current))try{m(null);let e=await b.current.get(n);e===void 0?(f(i),w.current=i,_(null)):(f(e),w.current=e,_(new Date))}catch(e){m(e instanceof Error?e:Error(String(e)))}},[n,i]),k=e.useCallback(()=>{D()},[D]),A=e.useCallback(e=>{E(e)},[E]);return Object.assign({0:d,1:E,2:D,data:d,update:A,reset:k,loading:!C.current,persisted:C.current,error:p,lastUpdated:g,refresh:O,length:3},{[Symbol.iterator]:function*(){yield d,yield E,yield D}})}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};
|