@dariushstony/smart-storage 0.1.0
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/LICENSE +21 -0
- package/README.md +764 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +301 -0
- package/dist/index.d.ts +301 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +91 -0
package/README.md
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
# @dariushstony/smart-storage
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@dariushstony/smart-storage)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
A **robust, SSR-safe, and production-ready wrapper** around Web Storage (`localStorage` / `sessionStorage` / `in-memory`) with:
|
|
8
|
+
|
|
9
|
+
- ๐๏ธ Three storage types: `'local'`, `'session'`, `'in-memory'`
|
|
10
|
+
- โฑ๏ธ TTL-based expiration
|
|
11
|
+
- โก Debounced writes
|
|
12
|
+
- ๐งน Automatic cleanup
|
|
13
|
+
- ๐ SSR-safe with automatic fallback
|
|
14
|
+
- ๐ก๏ธ Strong safety guarantees and detailed diagnostics
|
|
15
|
+
- ๐ฆ Dual package: ESM and CommonJS support
|
|
16
|
+
|
|
17
|
+
Designed for **real-world frontend applications** where correctness, performance, and edge-case handling matter.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## ๐ฆ Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @dariushstony/smart-storage
|
|
25
|
+
# or
|
|
26
|
+
yarn add @dariushstony/smart-storage
|
|
27
|
+
# or
|
|
28
|
+
pnpm add @dariushstony/smart-storage
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Works with:**
|
|
32
|
+
|
|
33
|
+
- โ
ESM (`import`)
|
|
34
|
+
- โ
CommonJS (`require`)
|
|
35
|
+
- โ
TypeScript
|
|
36
|
+
- โ
Node.js 18+
|
|
37
|
+
- โ
Modern browsers
|
|
38
|
+
- โ
SSR frameworks (Next.js, Nuxt, etc.)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## โ ๏ธ Security Warning (Read This First)
|
|
43
|
+
|
|
44
|
+
**Web Storage is NOT secure.**
|
|
45
|
+
|
|
46
|
+
- Data is fully accessible via JavaScript
|
|
47
|
+
- Vulnerable to XSS
|
|
48
|
+
- Easily inspectable by users
|
|
49
|
+
|
|
50
|
+
โ **Do NOT store**:
|
|
51
|
+
|
|
52
|
+
- Auth tokens
|
|
53
|
+
- Passwords
|
|
54
|
+
- Sensitive user data
|
|
55
|
+
|
|
56
|
+
โ
**Use instead**:
|
|
57
|
+
|
|
58
|
+
- `httpOnly` cookies
|
|
59
|
+
- Secure server-side sessions
|
|
60
|
+
|
|
61
|
+
Treat **all stored data as potentially compromised** and validate on read.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## โจ Features
|
|
66
|
+
|
|
67
|
+
- โ
Three storage types: `'local'`, `'session'`, `'in-memory'`
|
|
68
|
+
- โ
Safe SSR support (no `window` access on server)
|
|
69
|
+
- โ
TTL (time-to-live) expiration
|
|
70
|
+
- โ
Automatic expired-item cleanup
|
|
71
|
+
- โ
Debounced writes (default: 100ms)
|
|
72
|
+
- โ
Read-after-write consistency
|
|
73
|
+
- โ
QuotaExceeded recovery logic
|
|
74
|
+
- โ
Prototype-pollution protection
|
|
75
|
+
- โ
Singleton per storage slice + type
|
|
76
|
+
- โ
Detailed stats & diagnostics
|
|
77
|
+
- โ
Zero dependencies
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## ๐ค Exports
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// Main exports
|
|
85
|
+
import {
|
|
86
|
+
getStorageSlice, // Create custom storage slices
|
|
87
|
+
disposeStorageSlice, // Clean up temporary slices
|
|
88
|
+
StorageVault, // Class for advanced usage
|
|
89
|
+
} from '@dariushstony/smart-storage';
|
|
90
|
+
|
|
91
|
+
// Type exports
|
|
92
|
+
import type {
|
|
93
|
+
StorageLogger,
|
|
94
|
+
StorageType, // 'local' | 'session' | 'in-memory'
|
|
95
|
+
StorageVaultOptions,
|
|
96
|
+
StorageStats,
|
|
97
|
+
StorageTransform,
|
|
98
|
+
StoredData,
|
|
99
|
+
DataRecord,
|
|
100
|
+
} from '@dariushstony/smart-storage';
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## ๐๏ธ Storage Types
|
|
106
|
+
|
|
107
|
+
StorageVault supports three storage backends:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import { getStorageSlice } from '@dariushstony/smart-storage';
|
|
111
|
+
|
|
112
|
+
// 'local' - localStorage (persists across browser sessions)
|
|
113
|
+
const persistent = getStorageSlice('USER_DATA', {
|
|
114
|
+
storageType: 'local', // Default
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// 'session' - sessionStorage (cleared when tab closes)
|
|
118
|
+
const temporary = getStorageSlice('WIZARD_STATE', {
|
|
119
|
+
storageType: 'session',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// 'in-memory' - Map (cleared on page reload, great for testing)
|
|
123
|
+
const testing = getStorageSlice('TEST_DATA', {
|
|
124
|
+
storageType: 'in-memory',
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### When to use each type
|
|
129
|
+
|
|
130
|
+
| Type | Persistence | Use Case |
|
|
131
|
+
| ----------------- | ----------------- | --------------------------------------- |
|
|
132
|
+
| **`'local'`** | Across sessions | User preferences, cart, long-term cache |
|
|
133
|
+
| **`'session'`** | Until tab closes | Wizard flows, temporary form data |
|
|
134
|
+
| **`'in-memory'`** | Until page reload | Testing, SSR fallback, temporary data |
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## ๐ Quick Start
|
|
139
|
+
|
|
140
|
+
### ESM (Modern JavaScript)
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { getStorageSlice } from '@dariushstony/smart-storage';
|
|
144
|
+
|
|
145
|
+
// Create a storage slice (localStorage by default)
|
|
146
|
+
const storage = getStorageSlice('MY_APP');
|
|
147
|
+
|
|
148
|
+
// Store data
|
|
149
|
+
storage.setItem('theme', 'dark');
|
|
150
|
+
|
|
151
|
+
// Retrieve data
|
|
152
|
+
const theme = storage.getItem<string>('theme');
|
|
153
|
+
console.log(theme); // โ "dark"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### CommonJS (Node.js)
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
const { getStorageSlice } = require('@dariushstony/smart-storage');
|
|
160
|
+
|
|
161
|
+
// Create a storage slice
|
|
162
|
+
const storage = getStorageSlice('MY_APP');
|
|
163
|
+
|
|
164
|
+
// Works the same way!
|
|
165
|
+
storage.setItem('user', { name: 'dariush', id: 123 });
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### TypeScript
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { getStorageSlice } from '@dariushstony/smart-storage';
|
|
172
|
+
|
|
173
|
+
interface User {
|
|
174
|
+
name: string;
|
|
175
|
+
id: number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const storage = getStorageSlice('MY_APP');
|
|
179
|
+
|
|
180
|
+
// Store with TTL (auto-expiry)
|
|
181
|
+
storage.setItem<User>('user', { name: 'dariush', id: 123 });
|
|
182
|
+
|
|
183
|
+
// Retrieve with type safety
|
|
184
|
+
const user = storage.getItem<User>('user');
|
|
185
|
+
if (user) {
|
|
186
|
+
console.log(user.name); // TypeScript knows the shape!
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## ๐งฉ Storage Slices (Recommended)
|
|
193
|
+
|
|
194
|
+
All data is stored as one JSON blob per slice. Slices help reduce re-serialization costs and isolate concerns.
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { getStorageSlice } from '@dariushstony/smart-storage';
|
|
198
|
+
|
|
199
|
+
// Persistent user preferences
|
|
200
|
+
const userPrefs = getStorageSlice('USER_PREFERENCES', {
|
|
201
|
+
storageType: 'local', // Default
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Temporary session cache
|
|
205
|
+
const tempCache = getStorageSlice('TEMP_CACHE', {
|
|
206
|
+
storageType: 'session',
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Testing data
|
|
210
|
+
const testData = getStorageSlice('TEST_DATA', {
|
|
211
|
+
storageType: 'in-memory',
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
userPrefs.setItem('theme', 'dark');
|
|
215
|
+
tempCache.setItem('data', { value: 123 }, 5 * 60 * 1000); // 5 min TTL
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### When to use slices
|
|
219
|
+
|
|
220
|
+
- โ
Split data by update frequency
|
|
221
|
+
- โ
Isolate large or experimental data
|
|
222
|
+
- โ
Avoid rewriting unrelated data on each update
|
|
223
|
+
|
|
224
|
+
### Examples
|
|
225
|
+
|
|
226
|
+
โ **Bad** - Too granular:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
getStorageSlice('USER_NAME');
|
|
230
|
+
getStorageSlice('USER_EMAIL');
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
โ
**Good** - Grouped logically:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
getStorageSlice('USER_DATA');
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## โฑ TTL (Time-to-Live)
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { getStorageSlice } from '@dariushstony/smart-storage';
|
|
245
|
+
|
|
246
|
+
const storage = getStorageSlice('MY_APP');
|
|
247
|
+
|
|
248
|
+
// Expires in 1 hour
|
|
249
|
+
storage.setItem('token', 'abc123', 60 * 60 * 1000);
|
|
250
|
+
|
|
251
|
+
// Never expires
|
|
252
|
+
storage.setItem('config', { theme: 'dark' });
|
|
253
|
+
|
|
254
|
+
// Immediately deletes (TTL = 0)
|
|
255
|
+
storage.setItem('temp', 'data', 0);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Get remaining TTL
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const remainingMs = storage.getRemainingTTL('token');
|
|
262
|
+
if (remainingMs) {
|
|
263
|
+
console.log(`Expires in ${Math.floor(remainingMs / 1000)} seconds`);
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Update value without changing TTL
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
storage.setItem('counter', 0, 60000); // Expires in 1 minute
|
|
271
|
+
storage.updateItem('counter', 5); // Updates value, keeps same expiry
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Extend TTL
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
storage.extendTTL('token', 30 * 60 * 1000); // +30 minutes
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Note:** If the item had no expiry, `extendTTL` will add one starting from now.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## ๐งน Cleanup
|
|
285
|
+
|
|
286
|
+
### Automatic
|
|
287
|
+
|
|
288
|
+
- Expired items are removed on read
|
|
289
|
+
- Cleanup runs automatically on quota errors
|
|
290
|
+
|
|
291
|
+
### Manual
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
const storage = getStorageSlice('MY_APP');
|
|
295
|
+
const removedCount = storage.cleanupExpiredItems();
|
|
296
|
+
console.log(`Removed ${removedCount} expired items`);
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## โก Debounced Writes (Performance)
|
|
302
|
+
|
|
303
|
+
Writes are debounced (default: 100ms) to batch rapid updates.
|
|
304
|
+
|
|
305
|
+
- **Reads always see pending writes** (read-after-write consistency)
|
|
306
|
+
- **Pending writes flush automatically on page unload** (no data loss)
|
|
307
|
+
|
|
308
|
+
### Force immediate persistence
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
const storage = getStorageSlice('MY_APP');
|
|
312
|
+
storage.flush();
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Disable debouncing
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
const criticalStorage = getStorageSlice('CRITICAL_DATA', { debounceMs: 0 });
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Custom debounce timing
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
const analyticsStorage = getStorageSlice('ANALYTICS', { debounceMs: 500 });
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## ๐ฅ SSR Behavior
|
|
330
|
+
|
|
331
|
+
- **Server:** Automatically uses in-memory storage (Map)
|
|
332
|
+
- **Client:** Uses Web Storage (localStorage/sessionStorage based on `storageType`)
|
|
333
|
+
- **Important:** Server data is NOT hydrated automatically
|
|
334
|
+
|
|
335
|
+
### Recommended pattern
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
import { getStorageSlice } from '@dariushstony/smart-storage';
|
|
339
|
+
|
|
340
|
+
// Server โ pass initial data via props
|
|
341
|
+
// Client โ re-store in useEffect or client-side code
|
|
342
|
+
|
|
343
|
+
const storage = getStorageSlice('MY_APP');
|
|
344
|
+
|
|
345
|
+
// In your client-side initialization:
|
|
346
|
+
storage.setItem('data', initialData);
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Explicit in-memory for testing
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
const testStorage = getStorageSlice('TEST', {
|
|
353
|
+
storageType: 'in-memory', // No real storage, perfect for tests
|
|
354
|
+
debounceMs: 0, // Immediate writes for predictable tests
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## ๐ง Serialization Rules (JSON)
|
|
361
|
+
|
|
362
|
+
Uses `JSON.stringify()` internally.
|
|
363
|
+
|
|
364
|
+
### Limitations
|
|
365
|
+
|
|
366
|
+
- โ `Functions`, `undefined`, `Symbol` โ silently dropped
|
|
367
|
+
- โ Circular references โ throws error
|
|
368
|
+
- โ ๏ธ `Date` โ becomes string (must convert back manually)
|
|
369
|
+
- โ ๏ธ `Map`, `Set`, class instances โ lose type information
|
|
370
|
+
|
|
371
|
+
๐ **Serialize complex types manually before storing.**
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## ๐ Stats & Debugging
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
import { getStorageSlice } from '@dariushstony/smart-storage';
|
|
379
|
+
|
|
380
|
+
const storage = getStorageSlice('MY_APP');
|
|
381
|
+
const stats = storage.getStats();
|
|
382
|
+
console.log(stats);
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Returns
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
{
|
|
389
|
+
itemCount: number;
|
|
390
|
+
sizeBytes: number;
|
|
391
|
+
stringLength: number;
|
|
392
|
+
maxSizeBytes: number;
|
|
393
|
+
quotaPercentage: number;
|
|
394
|
+
storageType: 'localStorage' | 'sessionStorage' | 'memory' | 'unavailable';
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Example usage
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
const storage = getStorageSlice('MY_APP');
|
|
402
|
+
const stats = storage.getStats();
|
|
403
|
+
console.log(`Using ${stats.itemCount} items`);
|
|
404
|
+
console.log(`Size: ${(stats.sizeBytes / 1024).toFixed(2)} KB`);
|
|
405
|
+
console.log(`Quota: ${stats.quotaPercentage.toFixed(1)}%`);
|
|
406
|
+
|
|
407
|
+
if (stats.quotaPercentage > 80) {
|
|
408
|
+
console.warn('Storage is over 80% full!');
|
|
409
|
+
storage.cleanupExpiredItems();
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## ๐ Clearing Data
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
import { getStorageSlice } from '@dariushstony/smart-storage';
|
|
419
|
+
|
|
420
|
+
const storage = getStorageSlice('MY_APP');
|
|
421
|
+
storage.clear(); // Clears only this slice
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Note:** Other slices are unaffected.
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## โป๏ธ Disposing Slices
|
|
429
|
+
|
|
430
|
+
Useful for temporary or short-lived slices. Removes the instance from the singleton cache and cleans up event listeners.
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
import {
|
|
434
|
+
getStorageSlice,
|
|
435
|
+
disposeStorageSlice,
|
|
436
|
+
} from '@dariushstony/smart-storage';
|
|
437
|
+
|
|
438
|
+
const tempStorage = getStorageSlice('TEMP_SESSION', {
|
|
439
|
+
storageType: 'session',
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// ... use storage ...
|
|
443
|
+
|
|
444
|
+
// Clean up when done (must match storageType used when creating)
|
|
445
|
+
disposeStorageSlice('TEMP_SESSION', {
|
|
446
|
+
storageType: 'session',
|
|
447
|
+
});
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Important:** The options passed to `disposeStorageSlice` must match exactly with those used in `getStorageSlice`.
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## ๐งช Testing Utilities
|
|
455
|
+
|
|
456
|
+
### Clear all instances
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
import { StorageVault } from '@dariushstony/smart-storage';
|
|
460
|
+
|
|
461
|
+
// In test teardown:
|
|
462
|
+
afterEach(() => {
|
|
463
|
+
StorageVault.clearAllInstances();
|
|
464
|
+
});
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Use in-memory storage for tests
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
import { getStorageSlice } from '@dariushstony/smart-storage';
|
|
471
|
+
|
|
472
|
+
describe('My tests', () => {
|
|
473
|
+
const testVault = getStorageSlice('TEST_DATA', {
|
|
474
|
+
storageType: 'in-memory', // Isolated, no real storage
|
|
475
|
+
debounceMs: 0, // Immediate writes
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
afterEach(() => {
|
|
479
|
+
testVault.clear(); // Clean up after each test
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('should store data', () => {
|
|
483
|
+
testVault.setItem('key', 'value');
|
|
484
|
+
expect(testVault.getItem('key')).toBe('value');
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
Flushes, cleans up, and removes all vault instances.
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## ๐งฑ Error Handling & Logging
|
|
494
|
+
|
|
495
|
+
Inject your own logger (e.g., Sentry):
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
import { getStorageSlice } from '@dariushstony/smart-storage';
|
|
499
|
+
import type { StorageLogger } from '@dariushstony/smart-storage';
|
|
500
|
+
|
|
501
|
+
const customLogger: StorageLogger = {
|
|
502
|
+
log: (message, error) => {
|
|
503
|
+
console.error('[Storage]', message, error);
|
|
504
|
+
// Send to Sentry or your logging service
|
|
505
|
+
// Sentry.captureException(error, { extra: { message } });
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const vault = getStorageSlice('APP_DATA', {
|
|
510
|
+
storageType: 'local',
|
|
511
|
+
logger: customLogger,
|
|
512
|
+
});
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## ๐ง Advanced Configuration
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
const customStorage = getStorageSlice('CUSTOM', {
|
|
521
|
+
storageType: 'session', // 'local' | 'session' | 'in-memory'
|
|
522
|
+
debounceMs: 200, // Custom debounce delay
|
|
523
|
+
maxSizeBytes: 10_000_000, // 10MB quota warning threshold
|
|
524
|
+
maxItemsInMemory: 2000, // Max items for in-memory fallback
|
|
525
|
+
logger: customLogger, // Custom logger integration
|
|
526
|
+
});
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Configuration Options
|
|
530
|
+
|
|
531
|
+
| Option | Type | Default | Description |
|
|
532
|
+
| ------------------ | ------------------------------------- | ------------ | -------------------------------------- |
|
|
533
|
+
| `storageType` | `'local' \| 'session' \| 'in-memory'` | `'local'` | Storage backend to use |
|
|
534
|
+
| `storageKey` | `string` | `'APP_DATA'` | Key under which to store data |
|
|
535
|
+
| `debounceMs` | `number` | `100` | Write debouncing delay (0 = immediate) |
|
|
536
|
+
| `maxSizeBytes` | `number` | `4_000_000` | Quota warning threshold (~4MB) |
|
|
537
|
+
| `maxItemsInMemory` | `number` | `1000` | Max items for in-memory storage |
|
|
538
|
+
| `logger` | `StorageLogger` | `undefined` | Custom error logger |
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
## ๐ API Reference
|
|
543
|
+
|
|
544
|
+
### Write Operations
|
|
545
|
+
|
|
546
|
+
| Method | Returns | Description |
|
|
547
|
+
| ------------------------------- | --------- | -------------------------------------- |
|
|
548
|
+
| `setItem(key, value, ttl?)` | `boolean` | Stores a value with optional TTL |
|
|
549
|
+
| `updateItem(key, newValue)` | `boolean` | Updates value without changing TTL |
|
|
550
|
+
| `removeItem(key)` | `boolean` | Removes an item |
|
|
551
|
+
| `clear()` | `boolean` | Clears all data for this slice |
|
|
552
|
+
| `extendTTL(key, additionalTTL)` | `boolean` | Extends TTL or adds one if none exists |
|
|
553
|
+
|
|
554
|
+
### Read Operations
|
|
555
|
+
|
|
556
|
+
| Method | Returns | Description |
|
|
557
|
+
| ---------------------- | ------------------------- | ---------------------------------------- |
|
|
558
|
+
| `getItem<T>(key)` | `T \| null` | Retrieves a value |
|
|
559
|
+
| `hasItem(key)` | `boolean` | Checks if item exists and is not expired |
|
|
560
|
+
| `getRemainingTTL(key)` | `number \| null` | Returns remaining TTL in milliseconds |
|
|
561
|
+
| `getAllKeys()` | `string[]` | Returns all valid keys |
|
|
562
|
+
| `getAll()` | `Record<string, unknown>` | Returns all valid items |
|
|
563
|
+
|
|
564
|
+
### Maintenance Operations
|
|
565
|
+
|
|
566
|
+
| Method | Returns | Description |
|
|
567
|
+
| ----------------------- | -------- | ------------------------------------ |
|
|
568
|
+
| `cleanupExpiredItems()` | `number` | Removes expired items, returns count |
|
|
569
|
+
| `flush()` | `void` | Flushes pending debounced writes |
|
|
570
|
+
| `getCurrentSize()` | `number` | Returns storage size in bytes |
|
|
571
|
+
| `getStats()` | `object` | Returns detailed storage statistics |
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
## ๐ Transform Pipeline
|
|
576
|
+
|
|
577
|
+
You can chain multiple transforms (compression, encryption, encoding) to process data before storage:
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
import { getStorageSlice } from '@dariushstony/smart-storage';
|
|
581
|
+
import type { StorageTransform } from '@dariushstony/smart-storage';
|
|
582
|
+
|
|
583
|
+
// Example: Compression transform (requires lz-string package)
|
|
584
|
+
const compressionTransform: StorageTransform = {
|
|
585
|
+
serialize: (data: string) => LZString.compress(data),
|
|
586
|
+
deserialize: (data: string) => LZString.decompress(data) || '',
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const vault = getStorageSlice('LARGE_DATA', {
|
|
590
|
+
transforms: [compressionTransform],
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// Data is automatically compressed before storage and decompressed on read
|
|
594
|
+
vault.setItem('bigObject', {
|
|
595
|
+
/* large data */
|
|
596
|
+
});
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
Transforms are applied in order during writes and reversed during reads.
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
## ๐ Setup with Custom Logger
|
|
604
|
+
|
|
605
|
+
If you want to integrate with your logging service (Sentry, LogRocket, etc.):
|
|
606
|
+
|
|
607
|
+
```typescript
|
|
608
|
+
import { getStorageSlice } from '@dariushstony/smart-storage';
|
|
609
|
+
import type { StorageLogger } from '@dariushstony/smart-storage';
|
|
610
|
+
|
|
611
|
+
const customLogger: StorageLogger = {
|
|
612
|
+
log: (message, error) => {
|
|
613
|
+
console.error('[Storage]', message, error);
|
|
614
|
+
// Send to your logging service
|
|
615
|
+
// Sentry.captureException(error, { extra: { message } });
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const vault = getStorageSlice('APP_DATA', {
|
|
620
|
+
storageType: 'local',
|
|
621
|
+
logger: customLogger,
|
|
622
|
+
});
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## ๐ก Common Patterns
|
|
628
|
+
|
|
629
|
+
### Feature Flags with Auto-Expiry
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
const featureFlags = getStorageSlice('FEATURE_FLAGS', {
|
|
633
|
+
storageType: 'local',
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
function enableFeature(name: string, durationMs = 24 * 60 * 60 * 1000) {
|
|
637
|
+
featureFlags.setItem(`feature:${name}`, true, durationMs);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function isFeatureEnabled(name: string): boolean {
|
|
641
|
+
return featureFlags.hasItem(`feature:${name}`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
enableFeature('new-checkout', 7 * 24 * 60 * 60 * 1000); // 7 days
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Rate Limiting
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
const rateLimiter = getStorageSlice('RATE_LIMIT', {
|
|
651
|
+
storageType: 'local',
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
function canPerformAction(action: string, limitMs = 60000): boolean {
|
|
655
|
+
const key = `action:${action}`;
|
|
656
|
+
if (rateLimiter.hasItem(key)) {
|
|
657
|
+
return false; // Rate-limited
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
rateLimiter.setItem(key, true, limitMs);
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (canPerformAction('send-email', 5 * 60 * 1000)) {
|
|
665
|
+
console.log('Sending email...');
|
|
666
|
+
}
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### Form Draft Auto-Save
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
const draftStorage = getStorageSlice('FORM_DRAFTS', {
|
|
673
|
+
storageType: 'local',
|
|
674
|
+
debounceMs: 1000, // Save 1 second after typing stops
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
function saveDraft(formId: string, data: Record<string, unknown>) {
|
|
678
|
+
draftStorage.setItem(`draft:${formId}`, data, 24 * 60 * 60 * 1000);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function loadDraft(formId: string) {
|
|
682
|
+
return draftStorage.getItem<Record<string, unknown>>(`draft:${formId}`);
|
|
683
|
+
}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### Temporary Wizard Flow
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
const wizardStorage = getStorageSlice('CHECKOUT_WIZARD', {
|
|
690
|
+
storageType: 'session', // Auto-cleared when tab closes
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
function saveWizardStep(step: number, data: any) {
|
|
694
|
+
wizardStorage.setItem('currentStep', step);
|
|
695
|
+
wizardStorage.setItem('formData', data);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Data automatically cleared when user closes tab - no cleanup needed!
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
---
|
|
702
|
+
|
|
703
|
+
## ๐ Design Philosophy
|
|
704
|
+
|
|
705
|
+
- **Prefer correctness over cleverness**
|
|
706
|
+
- **Defensive by default**
|
|
707
|
+
- **Explicit trade-offs**
|
|
708
|
+
- **Optimized for real production constraints**
|
|
709
|
+
|
|
710
|
+
This is infrastructure code, not a toy utility.
|
|
711
|
+
|
|
712
|
+
---
|
|
713
|
+
|
|
714
|
+
## ๐ Additional Documentation
|
|
715
|
+
|
|
716
|
+
- **Architecture**: `docs/ARCHITECTURE.md` - System design and technical decisions
|
|
717
|
+
- **Storage Architecture**: `docs/STORAGE_ARCHITECTURE.md` - Deep dive into storage mechanisms
|
|
718
|
+
- **How to Use**: `docs/HOW_TO_USE_STORAGE.md` - Simple examples and patterns
|
|
719
|
+
- **Project Structure**: `docs/STRUCTURE.md` - Codebase organization
|
|
720
|
+
|
|
721
|
+
## ๐ค Contributing
|
|
722
|
+
|
|
723
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
724
|
+
|
|
725
|
+
1. Fork the repository
|
|
726
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
727
|
+
3. Commit your changes (`git commit -m 'feat: add amazing feature'`)
|
|
728
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
729
|
+
5. Open a Pull Request
|
|
730
|
+
|
|
731
|
+
## ๐ License
|
|
732
|
+
|
|
733
|
+
This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
|
|
734
|
+
|
|
735
|
+
## ๐ค Author
|
|
736
|
+
|
|
737
|
+
**Dariush Hadipour**
|
|
738
|
+
|
|
739
|
+
- GitHub: [@DariushStony](https://github.com/DariushStony)
|
|
740
|
+
- Package: [@dariushstony/smart-storage](https://www.npmjs.com/package/@dariushstony/smart-storage)
|
|
741
|
+
|
|
742
|
+
---
|
|
743
|
+
|
|
744
|
+
## ๐ Quick Reference
|
|
745
|
+
|
|
746
|
+
### Storage Types
|
|
747
|
+
|
|
748
|
+
```typescript
|
|
749
|
+
'local'; // localStorage - persists across sessions
|
|
750
|
+
'session'; // sessionStorage - cleared on tab close
|
|
751
|
+
'in-memory'; // Map - cleared on reload, great for tests
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### Common Use Cases
|
|
755
|
+
|
|
756
|
+
| Use Case | Storage Type | TTL | Debounce |
|
|
757
|
+
| ---------------- | ------------------------ | -------- | --------------- |
|
|
758
|
+
| User preferences | `'local'` | None | 0ms (immediate) |
|
|
759
|
+
| Shopping cart | `'local'` | None | 100ms |
|
|
760
|
+
| API cache | `'local'` | 5-10 min | 200ms |
|
|
761
|
+
| Feature flags | `'local'` | 7 days | 100ms |
|
|
762
|
+
| Wizard flow | `'session'` | None | 100ms |
|
|
763
|
+
| Form drafts | `'local'` or `'session'` | 24 hours | 1000ms |
|
|
764
|
+
| Testing | `'in-memory'` | Varies | 0ms |
|