@ahoo-wang/fetcher-storage 2.9.0 → 2.9.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 +921 -1
- package/dist/index.es.js +134 -135
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ console.log(memoryStorage.length); // 1
|
|
|
100
100
|
### Advanced Configuration
|
|
101
101
|
|
|
102
102
|
```typescript
|
|
103
|
-
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
103
|
+
import { KeyStorage, InMemoryStorage } from '@ahoo-wang/fetcher-storage';
|
|
104
104
|
|
|
105
105
|
// Custom storage and event bus
|
|
106
106
|
const customStorage = new KeyStorage<string>({
|
|
@@ -108,6 +108,614 @@ const customStorage = new KeyStorage<string>({
|
|
|
108
108
|
storage: new InMemoryStorage(), // Use in-memory instead of localStorage
|
|
109
109
|
// eventBus: customEventBus, // Custom event bus for notifications
|
|
110
110
|
});
|
|
111
|
+
|
|
112
|
+
// Custom serializer for complex data types
|
|
113
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
114
|
+
|
|
115
|
+
class DateSerializer {
|
|
116
|
+
serialize(value: any): string {
|
|
117
|
+
return JSON.stringify(value, (key, val) =>
|
|
118
|
+
val instanceof Date ? { __type: 'Date', value: val.toISOString() } : val,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
deserialize(value: string): any {
|
|
123
|
+
return JSON.parse(value, (key, val) =>
|
|
124
|
+
val && typeof val === 'object' && val.__type === 'Date'
|
|
125
|
+
? new Date(val.value)
|
|
126
|
+
: val,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const dateStorage = new KeyStorage<{ createdAt: Date; data: string }>({
|
|
132
|
+
key: 'date-data',
|
|
133
|
+
serializer: new DateSerializer(),
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## 🚀 Advanced Usage Examples
|
|
138
|
+
|
|
139
|
+
### Reactive Storage with RxJS Integration
|
|
140
|
+
|
|
141
|
+
Create reactive storage that integrates with RxJS observables:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
145
|
+
import { BehaviorSubject, Observable } from 'rxjs';
|
|
146
|
+
import { map, distinctUntilChanged } from 'rxjs/operators';
|
|
147
|
+
|
|
148
|
+
class ReactiveKeyStorage<T> extends KeyStorage<T> {
|
|
149
|
+
private subject: BehaviorSubject<T | null>;
|
|
150
|
+
|
|
151
|
+
constructor(options: any) {
|
|
152
|
+
super(options);
|
|
153
|
+
this.subject = new BehaviorSubject<T | null>(this.get());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Override set to emit changes
|
|
157
|
+
set(value: T): void {
|
|
158
|
+
super.set(value);
|
|
159
|
+
this.subject.next(value);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Get observable for reactive updates
|
|
163
|
+
asObservable(): Observable<T | null> {
|
|
164
|
+
return this.subject.asObservable();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Get observable for specific property
|
|
168
|
+
select<R>(selector: (value: T | null) => R): Observable<R> {
|
|
169
|
+
return this.subject.pipe(map(selector), distinctUntilChanged());
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Usage
|
|
174
|
+
const userStorage = new ReactiveKeyStorage<{ name: string; theme: string }>({
|
|
175
|
+
key: 'user-preferences',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// React to all changes
|
|
179
|
+
userStorage.asObservable().subscribe(preferences => {
|
|
180
|
+
console.log('User preferences changed:', preferences);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// React to specific property changes
|
|
184
|
+
userStorage
|
|
185
|
+
.select(prefs => prefs?.theme)
|
|
186
|
+
.subscribe(theme => {
|
|
187
|
+
document.body.className = `theme-${theme}`;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Update storage (will trigger observers)
|
|
191
|
+
userStorage.set({ name: 'John', theme: 'dark' });
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Encrypted Storage with Web Crypto API
|
|
195
|
+
|
|
196
|
+
Implement secure encrypted storage for sensitive data:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
200
|
+
|
|
201
|
+
class EncryptedSerializer {
|
|
202
|
+
private keyPromise: Promise<CryptoKey>;
|
|
203
|
+
|
|
204
|
+
constructor(password: string) {
|
|
205
|
+
this.keyPromise = this.deriveKey(password);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async deriveKey(password: string): Promise<CryptoKey> {
|
|
209
|
+
const encoder = new TextEncoder();
|
|
210
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
211
|
+
'raw',
|
|
212
|
+
encoder.encode(password),
|
|
213
|
+
'PBKDF2',
|
|
214
|
+
false,
|
|
215
|
+
['deriveBits', 'deriveKey'],
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
return crypto.subtle.deriveKey(
|
|
219
|
+
{
|
|
220
|
+
name: 'PBKDF2',
|
|
221
|
+
salt: encoder.encode('fetcher-storage-salt'),
|
|
222
|
+
iterations: 100000,
|
|
223
|
+
hash: 'SHA-256',
|
|
224
|
+
},
|
|
225
|
+
keyMaterial,
|
|
226
|
+
{ name: 'AES-GCM', length: 256 },
|
|
227
|
+
false,
|
|
228
|
+
['encrypt', 'decrypt'],
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async serialize(value: any): Promise<string> {
|
|
233
|
+
const key = await this.keyPromise;
|
|
234
|
+
const encoder = new TextEncoder();
|
|
235
|
+
const data = encoder.encode(JSON.stringify(value));
|
|
236
|
+
|
|
237
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
238
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
239
|
+
{ name: 'AES-GCM', iv },
|
|
240
|
+
key,
|
|
241
|
+
data,
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Combine IV and encrypted data
|
|
245
|
+
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
|
246
|
+
combined.set(iv);
|
|
247
|
+
combined.set(new Uint8Array(encrypted), iv.length);
|
|
248
|
+
|
|
249
|
+
return btoa(String.fromCharCode(...combined));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async deserialize(value: string): Promise<any> {
|
|
253
|
+
const key = await this.keyPromise;
|
|
254
|
+
const combined = new Uint8Array(
|
|
255
|
+
atob(value)
|
|
256
|
+
.split('')
|
|
257
|
+
.map(c => c.charCodeAt(0)),
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const iv = combined.slice(0, 12);
|
|
261
|
+
const encrypted = combined.slice(12);
|
|
262
|
+
|
|
263
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
264
|
+
{ name: 'AES-GCM', iv },
|
|
265
|
+
key,
|
|
266
|
+
encrypted,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const decoder = new TextDecoder();
|
|
270
|
+
return JSON.parse(decoder.decode(decrypted));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Usage (only works in secure contexts - HTTPS)
|
|
275
|
+
const secureStorage = new KeyStorage<any>({
|
|
276
|
+
key: 'sensitive-data',
|
|
277
|
+
serializer: new EncryptedSerializer('user-password'),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Store encrypted data
|
|
281
|
+
secureStorage.set({ apiKey: 'secret-key', tokens: ['token1', 'token2'] });
|
|
282
|
+
|
|
283
|
+
// Retrieve decrypted data
|
|
284
|
+
const data = secureStorage.get();
|
|
285
|
+
console.log(data); // { apiKey: 'secret-key', tokens: [...] }
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Storage Migration and Versioning
|
|
289
|
+
|
|
290
|
+
Handle storage schema migrations across app versions:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
294
|
+
|
|
295
|
+
interface StorageVersion {
|
|
296
|
+
version: number;
|
|
297
|
+
migrate: (data: any) => any;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
class VersionedKeyStorage<T> extends KeyStorage<T> {
|
|
301
|
+
private migrations: StorageVersion[] = [];
|
|
302
|
+
|
|
303
|
+
constructor(options: any, migrations: StorageVersion[] = []) {
|
|
304
|
+
super(options);
|
|
305
|
+
this.migrations = migrations.sort((a, b) => a.version - b.version);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
get(): T | null {
|
|
309
|
+
const rawData = super.get();
|
|
310
|
+
if (!rawData) return null;
|
|
311
|
+
|
|
312
|
+
return this.migrateData(rawData);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private migrateData(data: any): T {
|
|
316
|
+
const currentVersion = data.__version || 0;
|
|
317
|
+
let migratedData = { ...data };
|
|
318
|
+
|
|
319
|
+
// Remove version marker for clean data
|
|
320
|
+
delete migratedData.__version;
|
|
321
|
+
|
|
322
|
+
// Apply migrations in order
|
|
323
|
+
for (const migration of this.migrations) {
|
|
324
|
+
if (currentVersion < migration.version) {
|
|
325
|
+
migratedData = migration.migrate(migratedData);
|
|
326
|
+
migratedData.__version = migration.version;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Save migrated data
|
|
331
|
+
if (migratedData.__version !== currentVersion) {
|
|
332
|
+
super.set(migratedData);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
delete migratedData.__version;
|
|
336
|
+
return migratedData;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Define migrations
|
|
341
|
+
const migrations: StorageVersion[] = [
|
|
342
|
+
{
|
|
343
|
+
version: 1,
|
|
344
|
+
migrate: data => ({
|
|
345
|
+
...data,
|
|
346
|
+
// Add default theme if missing
|
|
347
|
+
theme: data.theme || 'light',
|
|
348
|
+
}),
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
version: 2,
|
|
352
|
+
migrate: data => ({
|
|
353
|
+
...data,
|
|
354
|
+
// Rename property
|
|
355
|
+
preferences: data.settings || {},
|
|
356
|
+
settings: undefined,
|
|
357
|
+
}),
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
version: 3,
|
|
361
|
+
migrate: data => ({
|
|
362
|
+
...data,
|
|
363
|
+
// Add timestamps
|
|
364
|
+
createdAt: data.createdAt || new Date().toISOString(),
|
|
365
|
+
updatedAt: new Date().toISOString(),
|
|
366
|
+
}),
|
|
367
|
+
},
|
|
368
|
+
];
|
|
369
|
+
|
|
370
|
+
// Usage
|
|
371
|
+
const userPrefsStorage = new VersionedKeyStorage<{
|
|
372
|
+
name: string;
|
|
373
|
+
theme: string;
|
|
374
|
+
preferences: Record<string, any>;
|
|
375
|
+
createdAt: string;
|
|
376
|
+
updatedAt: string;
|
|
377
|
+
}>(
|
|
378
|
+
{
|
|
379
|
+
key: 'user-preferences',
|
|
380
|
+
},
|
|
381
|
+
migrations,
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// Data will be automatically migrated when accessed
|
|
385
|
+
const prefs = userPrefsStorage.get();
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Cross-Tab Communication with Shared Storage
|
|
389
|
+
|
|
390
|
+
Implement cross-tab communication using storage events:
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
394
|
+
|
|
395
|
+
interface TabMessage {
|
|
396
|
+
id: string;
|
|
397
|
+
type: string;
|
|
398
|
+
payload: any;
|
|
399
|
+
timestamp: number;
|
|
400
|
+
sourceTab: string;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
class CrossTabMessenger {
|
|
404
|
+
private storage: KeyStorage<TabMessage[]>;
|
|
405
|
+
private tabId: string;
|
|
406
|
+
private listeners: Map<string, (message: TabMessage) => void> = new Map();
|
|
407
|
+
|
|
408
|
+
constructor(channelName: string = 'cross-tab-messages') {
|
|
409
|
+
this.tabId = `tab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
410
|
+
this.storage = new KeyStorage<TabMessage[]>({
|
|
411
|
+
key: channelName,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Listen for storage changes
|
|
415
|
+
this.storage.subscribe(messages => {
|
|
416
|
+
if (!messages) return;
|
|
417
|
+
|
|
418
|
+
// Process new messages
|
|
419
|
+
messages.forEach(message => {
|
|
420
|
+
if (message.sourceTab !== this.tabId) {
|
|
421
|
+
this.notifyListeners(message.type, message);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Initialize storage if empty
|
|
427
|
+
if (!this.storage.get()) {
|
|
428
|
+
this.storage.set([]);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Send message to other tabs
|
|
433
|
+
broadcast(type: string, payload: any) {
|
|
434
|
+
const messages = this.storage.get() || [];
|
|
435
|
+
const message: TabMessage = {
|
|
436
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
437
|
+
type,
|
|
438
|
+
payload,
|
|
439
|
+
timestamp: Date.now(),
|
|
440
|
+
sourceTab: this.tabId,
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// Add message and keep only recent messages
|
|
444
|
+
const updatedMessages = [...messages, message].slice(-50);
|
|
445
|
+
this.storage.set(updatedMessages);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Listen for messages
|
|
449
|
+
on(type: string, callback: (message: TabMessage) => void) {
|
|
450
|
+
this.listeners.set(type, callback);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Remove listener
|
|
454
|
+
off(type: string) {
|
|
455
|
+
this.listeners.delete(type);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private notifyListeners(type: string, message: TabMessage) {
|
|
459
|
+
const listener = this.listeners.get(type);
|
|
460
|
+
if (listener) {
|
|
461
|
+
listener(message);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Get current tab ID
|
|
466
|
+
getTabId(): string {
|
|
467
|
+
return this.tabId;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Usage
|
|
472
|
+
const messenger = new CrossTabMessenger('app-messages');
|
|
473
|
+
|
|
474
|
+
// Listen for user login events
|
|
475
|
+
messenger.on('user-logged-in', message => {
|
|
476
|
+
console.log('User logged in from another tab:', message.payload);
|
|
477
|
+
// Update current tab's state
|
|
478
|
+
updateUserState(message.payload.user);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Broadcast user actions
|
|
482
|
+
function onUserLogin(user: any) {
|
|
483
|
+
messenger.broadcast('user-logged-in', { user, tabId: messenger.getTabId() });
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Performance Monitoring and Analytics
|
|
488
|
+
|
|
489
|
+
Add performance tracking to storage operations:
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
493
|
+
|
|
494
|
+
interface PerformanceMetrics {
|
|
495
|
+
operation: string;
|
|
496
|
+
duration: number;
|
|
497
|
+
timestamp: number;
|
|
498
|
+
success: boolean;
|
|
499
|
+
error?: string;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
class MonitoredKeyStorage<T> extends KeyStorage<T> {
|
|
503
|
+
private metrics: PerformanceMetrics[] = [];
|
|
504
|
+
private readonly maxMetrics = 100;
|
|
505
|
+
|
|
506
|
+
constructor(options: any) {
|
|
507
|
+
super(options);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
set(value: T): void {
|
|
511
|
+
const startTime = performance.now();
|
|
512
|
+
try {
|
|
513
|
+
super.set(value);
|
|
514
|
+
this.recordMetric('set', performance.now() - startTime, true);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
this.recordMetric(
|
|
517
|
+
'set',
|
|
518
|
+
performance.now() - startTime,
|
|
519
|
+
false,
|
|
520
|
+
String(error),
|
|
521
|
+
);
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
get(): T | null {
|
|
527
|
+
const startTime = performance.now();
|
|
528
|
+
try {
|
|
529
|
+
const result = super.get();
|
|
530
|
+
this.recordMetric('get', performance.now() - startTime, true);
|
|
531
|
+
return result;
|
|
532
|
+
} catch (error) {
|
|
533
|
+
this.recordMetric(
|
|
534
|
+
'get',
|
|
535
|
+
performance.now() - startTime,
|
|
536
|
+
false,
|
|
537
|
+
String(error),
|
|
538
|
+
);
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private recordMetric(
|
|
544
|
+
operation: string,
|
|
545
|
+
duration: number,
|
|
546
|
+
success: boolean,
|
|
547
|
+
error?: string,
|
|
548
|
+
) {
|
|
549
|
+
this.metrics.push({
|
|
550
|
+
operation,
|
|
551
|
+
duration,
|
|
552
|
+
timestamp: Date.now(),
|
|
553
|
+
success,
|
|
554
|
+
error,
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// Keep only recent metrics
|
|
558
|
+
if (this.metrics.length > this.maxMetrics) {
|
|
559
|
+
this.metrics = this.metrics.slice(-this.maxMetrics);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Get performance statistics
|
|
564
|
+
getPerformanceStats() {
|
|
565
|
+
const total = this.metrics.length;
|
|
566
|
+
const successful = this.metrics.filter(m => m.success).length;
|
|
567
|
+
const failed = total - successful;
|
|
568
|
+
|
|
569
|
+
const avgDuration =
|
|
570
|
+
this.metrics.reduce((sum, m) => sum + m.duration, 0) / total;
|
|
571
|
+
const maxDuration = Math.max(...this.metrics.map(m => m.duration));
|
|
572
|
+
const minDuration = Math.min(...this.metrics.map(m => m.duration));
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
total,
|
|
576
|
+
successful,
|
|
577
|
+
failed,
|
|
578
|
+
successRate: successful / total,
|
|
579
|
+
avgDuration,
|
|
580
|
+
maxDuration,
|
|
581
|
+
minDuration,
|
|
582
|
+
recentErrors: this.metrics
|
|
583
|
+
.filter(m => !m.success)
|
|
584
|
+
.slice(-5)
|
|
585
|
+
.map(m => ({
|
|
586
|
+
operation: m.operation,
|
|
587
|
+
error: m.error,
|
|
588
|
+
timestamp: m.timestamp,
|
|
589
|
+
})),
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Export metrics for analysis
|
|
594
|
+
exportMetrics(): PerformanceMetrics[] {
|
|
595
|
+
return [...this.metrics];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Clear metrics
|
|
599
|
+
clearMetrics() {
|
|
600
|
+
this.metrics = [];
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Usage
|
|
605
|
+
const monitoredStorage = new MonitoredKeyStorage<any>({
|
|
606
|
+
key: 'app-data',
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// Use normally
|
|
610
|
+
monitoredStorage.set({ user: 'john', settings: {} });
|
|
611
|
+
const data = monitoredStorage.get();
|
|
612
|
+
|
|
613
|
+
// Get performance insights
|
|
614
|
+
const stats = monitoredStorage.getPerformanceStats();
|
|
615
|
+
console.log('Storage Performance:', stats);
|
|
616
|
+
|
|
617
|
+
// Export for further analysis
|
|
618
|
+
const metrics = monitoredStorage.exportMetrics();
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### Integration with State Management Libraries
|
|
622
|
+
|
|
623
|
+
Examples of integrating storage with popular state management solutions:
|
|
624
|
+
|
|
625
|
+
#### With Redux
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
import { createStore, combineReducers } from 'redux';
|
|
629
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
630
|
+
|
|
631
|
+
// Storage-backed reducer
|
|
632
|
+
function createPersistentReducer(reducer: any, storageKey: string) {
|
|
633
|
+
const storage = new KeyStorage({
|
|
634
|
+
key: storageKey,
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// Load initial state from storage
|
|
638
|
+
const initialState = storage.get() || reducer(undefined, { type: '@@INIT' });
|
|
639
|
+
|
|
640
|
+
return (state = initialState, action: any) => {
|
|
641
|
+
const newState = reducer(state, action);
|
|
642
|
+
|
|
643
|
+
// Persist state changes (debounced)
|
|
644
|
+
if (action.type !== '@@INIT') {
|
|
645
|
+
setTimeout(() => storage.set(newState), 100);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return newState;
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Usage
|
|
653
|
+
const userReducer = (state = { name: '', loggedIn: false }, action: any) => {
|
|
654
|
+
switch (action.type) {
|
|
655
|
+
case 'LOGIN':
|
|
656
|
+
return { ...state, name: action.payload.name, loggedIn: true };
|
|
657
|
+
case 'LOGOUT':
|
|
658
|
+
return { ...state, name: '', loggedIn: false };
|
|
659
|
+
default:
|
|
660
|
+
return state;
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const rootReducer = combineReducers({
|
|
665
|
+
user: createPersistentReducer(userReducer, 'redux-user-state'),
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
const store = createStore(rootReducer);
|
|
669
|
+
|
|
670
|
+
// State is automatically persisted and restored
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
#### With Zustand
|
|
674
|
+
|
|
675
|
+
```typescript
|
|
676
|
+
import { create } from 'zustand';
|
|
677
|
+
import { subscribeWithSelector } from 'zustand/middleware';
|
|
678
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
679
|
+
|
|
680
|
+
interface AppState {
|
|
681
|
+
user: { name: string; email: string } | null;
|
|
682
|
+
theme: 'light' | 'dark';
|
|
683
|
+
login: (user: { name: string; email: string }) => void;
|
|
684
|
+
logout: () => void;
|
|
685
|
+
setTheme: (theme: 'light' | 'dark') => void;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const storage = new KeyStorage<AppState['user']>({
|
|
689
|
+
key: 'zustand-user',
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
export const useAppStore = create<AppState>()(
|
|
693
|
+
subscribeWithSelector((set, get) => ({
|
|
694
|
+
user: storage.get(),
|
|
695
|
+
theme: 'light',
|
|
696
|
+
|
|
697
|
+
login: user => {
|
|
698
|
+
set({ user });
|
|
699
|
+
storage.set(user);
|
|
700
|
+
},
|
|
701
|
+
|
|
702
|
+
logout: () => {
|
|
703
|
+
set({ user: null });
|
|
704
|
+
storage.set(null);
|
|
705
|
+
},
|
|
706
|
+
|
|
707
|
+
setTheme: theme => set({ theme }),
|
|
708
|
+
})),
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
// Auto-persist theme changes
|
|
712
|
+
useAppStore.subscribe(
|
|
713
|
+
state => state.theme,
|
|
714
|
+
theme => {
|
|
715
|
+
const themeStorage = new KeyStorage({ key: 'app-theme' });
|
|
716
|
+
themeStorage.set(theme);
|
|
717
|
+
},
|
|
718
|
+
);
|
|
111
719
|
```
|
|
112
720
|
|
|
113
721
|
## API Reference
|
|
@@ -170,6 +778,212 @@ Serializes values to/from JSON strings.
|
|
|
170
778
|
|
|
171
779
|
Identity serializer that passes values through unchanged.
|
|
172
780
|
|
|
781
|
+
## Real-World Examples
|
|
782
|
+
|
|
783
|
+
### User Session Management
|
|
784
|
+
|
|
785
|
+
```typescript
|
|
786
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
787
|
+
|
|
788
|
+
interface UserSession {
|
|
789
|
+
userId: string;
|
|
790
|
+
token: string;
|
|
791
|
+
expiresAt: Date;
|
|
792
|
+
preferences: Record<string, any>;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
class SessionManager {
|
|
796
|
+
private sessionStorage = new KeyStorage<UserSession>({
|
|
797
|
+
key: 'user-session',
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
async login(credentials: LoginCredentials): Promise<UserSession> {
|
|
801
|
+
const response = await fetch('/api/login', {
|
|
802
|
+
method: 'POST',
|
|
803
|
+
body: JSON.stringify(credentials),
|
|
804
|
+
});
|
|
805
|
+
const session = await response.json();
|
|
806
|
+
|
|
807
|
+
this.sessionStorage.set(session);
|
|
808
|
+
return session;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
getCurrentSession(): UserSession | null {
|
|
812
|
+
const session = this.sessionStorage.get();
|
|
813
|
+
if (!session) return null;
|
|
814
|
+
|
|
815
|
+
// Check if session is expired
|
|
816
|
+
if (new Date(session.expiresAt) < new Date()) {
|
|
817
|
+
this.logout();
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return session;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
logout(): void {
|
|
825
|
+
this.sessionStorage.remove();
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
updatePreferences(preferences: Record<string, any>): void {
|
|
829
|
+
const session = this.getCurrentSession();
|
|
830
|
+
if (session) {
|
|
831
|
+
this.sessionStorage.set({
|
|
832
|
+
...session,
|
|
833
|
+
preferences: { ...session.preferences, ...preferences },
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
### Cross-Tab Application State
|
|
841
|
+
|
|
842
|
+
```typescript
|
|
843
|
+
import {
|
|
844
|
+
KeyStorage,
|
|
845
|
+
BroadcastTypedEventBus,
|
|
846
|
+
SerialTypedEventBus,
|
|
847
|
+
} from '@ahoo-wang/fetcher-storage';
|
|
848
|
+
|
|
849
|
+
interface AppState {
|
|
850
|
+
theme: 'light' | 'dark';
|
|
851
|
+
language: string;
|
|
852
|
+
sidebarCollapsed: boolean;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
class AppStateManager {
|
|
856
|
+
private stateStorage: KeyStorage<AppState>;
|
|
857
|
+
|
|
858
|
+
constructor() {
|
|
859
|
+
// Use broadcast event bus for cross-tab synchronization
|
|
860
|
+
const eventBus = new BroadcastTypedEventBus(
|
|
861
|
+
new SerialTypedEventBus('app-state'),
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
this.stateStorage = new KeyStorage<AppState>({
|
|
865
|
+
key: 'app-state',
|
|
866
|
+
eventBus,
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
// Listen for state changes from other tabs
|
|
870
|
+
this.stateStorage.addListener(event => {
|
|
871
|
+
if (event.newValue) {
|
|
872
|
+
this.applyStateToUI(event.newValue);
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
getState(): AppState {
|
|
878
|
+
return (
|
|
879
|
+
this.stateStorage.get() || {
|
|
880
|
+
theme: 'light',
|
|
881
|
+
language: 'en',
|
|
882
|
+
sidebarCollapsed: false,
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
updateState(updates: Partial<AppState>): void {
|
|
888
|
+
const currentState = this.getState();
|
|
889
|
+
const newState = { ...currentState, ...updates };
|
|
890
|
+
this.stateStorage.set(newState);
|
|
891
|
+
this.applyStateToUI(newState);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
private applyStateToUI(state: AppState): void {
|
|
895
|
+
document.documentElement.setAttribute('data-theme', state.theme);
|
|
896
|
+
// Update UI components based on state
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
### Form Auto-Save
|
|
902
|
+
|
|
903
|
+
```typescript
|
|
904
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
905
|
+
import { useEffect, useState } from 'react';
|
|
906
|
+
|
|
907
|
+
interface FormData {
|
|
908
|
+
title: string;
|
|
909
|
+
content: string;
|
|
910
|
+
tags: string[];
|
|
911
|
+
lastSaved: Date;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function useAutoSaveForm(formId: string) {
|
|
915
|
+
const [formData, setFormData] = useState<Partial<FormData>>({});
|
|
916
|
+
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
|
917
|
+
|
|
918
|
+
const formStorage = new KeyStorage<Partial<FormData>>({
|
|
919
|
+
key: `form-autosave-${formId}`
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// Load saved data on mount
|
|
923
|
+
useEffect(() => {
|
|
924
|
+
const saved = formStorage.get();
|
|
925
|
+
if (saved) {
|
|
926
|
+
setFormData(saved);
|
|
927
|
+
setLastSaved(saved.lastSaved || null);
|
|
928
|
+
}
|
|
929
|
+
}, [formStorage]);
|
|
930
|
+
|
|
931
|
+
// Auto-save on changes
|
|
932
|
+
useEffect(() => {
|
|
933
|
+
if (Object.keys(formData).length > 0) {
|
|
934
|
+
const dataToSave = {
|
|
935
|
+
...formData,
|
|
936
|
+
lastSaved: new Date(),
|
|
937
|
+
};
|
|
938
|
+
formStorage.set(dataToSave);
|
|
939
|
+
setLastSaved(dataToSave.lastSaved);
|
|
940
|
+
}
|
|
941
|
+
}, [formData, formStorage]);
|
|
942
|
+
|
|
943
|
+
const clearAutoSave = () => {
|
|
944
|
+
formStorage.remove();
|
|
945
|
+
setFormData({});
|
|
946
|
+
setLastSaved(null);
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
return {
|
|
950
|
+
formData,
|
|
951
|
+
setFormData,
|
|
952
|
+
lastSaved,
|
|
953
|
+
clearAutoSave,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Usage in component
|
|
958
|
+
function ArticleEditor({ articleId }: { articleId: string }) {
|
|
959
|
+
const { formData, setFormData, lastSaved, clearAutoSave } = useAutoSaveForm(articleId);
|
|
960
|
+
|
|
961
|
+
return (
|
|
962
|
+
<div>
|
|
963
|
+
{lastSaved && (
|
|
964
|
+
<div className="autosave-indicator">
|
|
965
|
+
Auto-saved at {lastSaved.toLocaleTimeString()}
|
|
966
|
+
</div>
|
|
967
|
+
)}
|
|
968
|
+
|
|
969
|
+
<input
|
|
970
|
+
value={formData.title || ''}
|
|
971
|
+
onChange={e => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
|
972
|
+
placeholder="Article title"
|
|
973
|
+
/>
|
|
974
|
+
|
|
975
|
+
<textarea
|
|
976
|
+
value={formData.content || ''}
|
|
977
|
+
onChange={e => setFormData(prev => ({ ...prev, content: e.target.value }))}
|
|
978
|
+
placeholder="Article content"
|
|
979
|
+
/>
|
|
980
|
+
|
|
981
|
+
<button onClick={clearAutoSave}>Clear Auto-save</button>
|
|
982
|
+
</div>
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
```
|
|
986
|
+
|
|
173
987
|
## TypeScript Support
|
|
174
988
|
|
|
175
989
|
Full TypeScript support with generics and type inference:
|
|
@@ -183,6 +997,112 @@ userStorage.set({ id: 1, name: 'John' });
|
|
|
183
997
|
const user = userStorage.get(); // User | null
|
|
184
998
|
```
|
|
185
999
|
|
|
1000
|
+
## Troubleshooting
|
|
1001
|
+
|
|
1002
|
+
### Common Issues
|
|
1003
|
+
|
|
1004
|
+
#### Storage Quota Exceeded
|
|
1005
|
+
|
|
1006
|
+
```typescript
|
|
1007
|
+
// Handle storage quota errors
|
|
1008
|
+
try {
|
|
1009
|
+
userStorage.set(largeData);
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
if (error.name === 'QuotaExceededError') {
|
|
1012
|
+
// Fallback to in-memory storage or compress data
|
|
1013
|
+
console.warn('Storage quota exceeded, using fallback');
|
|
1014
|
+
// Implement fallback logic
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
#### Cross-Tab Synchronization Not Working
|
|
1020
|
+
|
|
1021
|
+
```typescript
|
|
1022
|
+
// Ensure BroadcastChannel is supported
|
|
1023
|
+
if ('BroadcastChannel' in window) {
|
|
1024
|
+
const eventBus = new BroadcastTypedEventBus(
|
|
1025
|
+
new SerialTypedEventBus('my-app'),
|
|
1026
|
+
);
|
|
1027
|
+
// Use with KeyStorage
|
|
1028
|
+
} else {
|
|
1029
|
+
console.warn(
|
|
1030
|
+
'BroadcastChannel not supported, falling back to local-only storage',
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
#### Serialization Errors
|
|
1036
|
+
|
|
1037
|
+
```typescript
|
|
1038
|
+
// Handle circular references and complex objects
|
|
1039
|
+
class SafeJsonSerializer implements Serializer<string, any> {
|
|
1040
|
+
serialize(value: any): string {
|
|
1041
|
+
// Remove circular references or handle special cases
|
|
1042
|
+
const safeValue = this.makeSerializable(value);
|
|
1043
|
+
return JSON.stringify(safeValue);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
deserialize(value: string): any {
|
|
1047
|
+
return JSON.parse(value);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
private makeSerializable(obj: any, seen = new WeakSet()): any {
|
|
1051
|
+
if (obj === null || typeof obj !== 'object') return obj;
|
|
1052
|
+
if (seen.has(obj)) return '[Circular]';
|
|
1053
|
+
|
|
1054
|
+
seen.add(obj);
|
|
1055
|
+
const result: any = Array.isArray(obj) ? [] : {};
|
|
1056
|
+
|
|
1057
|
+
for (const key in obj) {
|
|
1058
|
+
if (obj.hasOwnProperty(key)) {
|
|
1059
|
+
result[key] = this.makeSerializable(obj[key], seen);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
seen.delete(obj);
|
|
1064
|
+
return result;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
#### Memory Leaks
|
|
1070
|
+
|
|
1071
|
+
```typescript
|
|
1072
|
+
// Always clean up listeners
|
|
1073
|
+
class ComponentWithStorage {
|
|
1074
|
+
private storage: KeyStorage<any>;
|
|
1075
|
+
private removeListener: () => void;
|
|
1076
|
+
|
|
1077
|
+
constructor() {
|
|
1078
|
+
this.storage = new KeyStorage({ key: 'component-data' });
|
|
1079
|
+
this.removeListener = this.storage.addListener(event => {
|
|
1080
|
+
// Handle changes
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
destroy() {
|
|
1085
|
+
// Clean up when component is destroyed
|
|
1086
|
+
this.removeListener();
|
|
1087
|
+
this.storage.destroy?.(); // If available
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
### Performance Tips
|
|
1093
|
+
|
|
1094
|
+
- **Use appropriate serializers**: JSON for simple objects, custom serializers for complex data
|
|
1095
|
+
- **Batch operations**: Group multiple storage operations when possible
|
|
1096
|
+
- **Monitor storage size**: Implement size limits and cleanup strategies
|
|
1097
|
+
- **Use memory storage for temporary data**: Avoid persisting unnecessary data
|
|
1098
|
+
- **Debounce frequent updates**: Prevent excessive storage writes
|
|
1099
|
+
|
|
1100
|
+
### Browser Compatibility
|
|
1101
|
+
|
|
1102
|
+
- **localStorage**: IE 8+, all modern browsers
|
|
1103
|
+
- **BroadcastChannel**: Chrome 54+, Firefox 38+, Safari 15.4+
|
|
1104
|
+
- **Fallback handling**: Always provide fallbacks for unsupported features
|
|
1105
|
+
|
|
186
1106
|
## License
|
|
187
1107
|
|
|
188
1108
|
[Apache 2.0](https://github.com/Ahoo-Wang/fetcher/blob/master/LICENSE)
|