@ahoo-wang/fetcher-storage 2.8.8 → 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 CHANGED
@@ -8,18 +8,18 @@
8
8
  [![npm bundle size](https://img.shields.io/bundlephobia/minzip/%40ahoo-wang%2Ffetcher-storage)](https://www.npmjs.com/package/@ahoo-wang/fetcher-storage)
9
9
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Ahoo-Wang/fetcher)
10
10
 
11
- A lightweight, cross-environment storage library with change event listening capabilities. Provides consistent API for
12
- browser localStorage/sessionStorage and in-memory storage with change notifications.
11
+ A lightweight, cross-environment storage library with key-based storage and automatic environment detection. Provides
12
+ consistent API for browser localStorage and in-memory storage with change notifications.
13
13
 
14
14
  ## Features
15
15
 
16
16
  - 🌐 Cross-environment support (Browser & Node.js)
17
17
  - 📦 Ultra-lightweight (~1KB gzip)
18
18
  - 🔔 Storage change event listening
19
- - 🔄 Automatic environment detection
20
- - 🛠️ Key-based storage with caching
19
+ - 🔄 Automatic environment detection with fallback
20
+ - 🛠️ Key-based storage with caching and serialization
21
21
  - 🔧 Custom serialization support
22
- - 📝 TypeScript support
22
+ - 📝 Full TypeScript support
23
23
 
24
24
  ## Installation
25
25
 
@@ -29,25 +29,20 @@ npm install @ahoo-wang/fetcher-storage
29
29
 
30
30
  ## Usage
31
31
 
32
- ### Basic Usage
32
+ ### Environment Detection and Storage Selection
33
33
 
34
34
  ```typescript
35
- import { createListenableStorage } from '@ahoo-wang/fetcher-storage';
35
+ import { getStorage, isBrowser } from '@ahoo-wang/fetcher-storage';
36
36
 
37
- // Automatically selects the appropriate storage implementation
38
- const storage = createListenableStorage();
37
+ // Check if running in browser
38
+ console.log('Is browser:', isBrowser());
39
39
 
40
- // Use like regular Storage API
40
+ // Get appropriate storage for current environment
41
+ const storage = getStorage(); // localStorage in browser, InMemoryStorage in Node.js
42
+
43
+ // Use like standard Storage API
41
44
  storage.setItem('key', 'value');
42
45
  const value = storage.getItem('key');
43
-
44
- // Listen for storage changes
45
- const removeListener = storage.addListener(event => {
46
- console.log('Storage changed:', event);
47
- });
48
-
49
- // Remove listener when no longer needed
50
- removeListener();
51
46
  ```
52
47
 
53
48
  ### Key-based Storage with Caching
@@ -55,19 +50,22 @@ removeListener();
55
50
  ```typescript
56
51
  import { KeyStorage } from '@ahoo-wang/fetcher-storage';
57
52
 
58
- // Create a storage for a specific key
53
+ // Create typed storage for a specific key
59
54
  const userStorage = new KeyStorage<{ name: string; age: number }>({
60
55
  key: 'user',
61
56
  });
62
57
 
63
- // Set and get values
58
+ // Set and get values with automatic caching
64
59
  userStorage.set({ name: 'John', age: 30 });
65
60
  const user = userStorage.get(); // {name: 'John', age: 30}
66
61
 
67
62
  // Listen for changes to this specific key
68
63
  const removeListener = userStorage.addListener(event => {
69
- console.log('User changed:', event.newValue);
64
+ console.log('User changed:', event.newValue, 'from:', event.oldValue);
70
65
  });
66
+
67
+ // Clean up when done
68
+ removeListener();
71
69
  ```
72
70
 
73
71
  ### Custom Serialization
@@ -75,58 +73,1035 @@ const removeListener = userStorage.addListener(event => {
75
73
  ```typescript
76
74
  import { KeyStorage, JsonSerializer } from '@ahoo-wang/fetcher-storage';
77
75
 
76
+ // Use JSON serialization (default)
78
77
  const jsonStorage = new KeyStorage<any>({
79
78
  key: 'data',
80
79
  serializer: new JsonSerializer(),
81
80
  });
82
81
 
83
- jsonStorage.set({ message: 'Hello World' });
84
- const data = jsonStorage.get(); // {message: 'Hello World'}
82
+ jsonStorage.set({ message: 'Hello World', timestamp: Date.now() });
83
+ const data = jsonStorage.get(); // {message: 'Hello World', timestamp: 1234567890}
85
84
  ```
86
85
 
87
- ### Environment-specific Storage
86
+ ### In-Memory Storage
88
87
 
89
88
  ```typescript
90
- import {
91
- BrowserListenableStorage,
92
- InMemoryListenableStorage,
93
- } from '@ahoo-wang/fetcher-storage';
89
+ import { InMemoryStorage } from '@ahoo-wang/fetcher-storage';
90
+
91
+ // Create in-memory storage (works in any environment)
92
+ const memoryStorage = new InMemoryStorage();
93
+
94
+ // Use like standard Storage API
95
+ memoryStorage.setItem('temp', 'data');
96
+ console.log(memoryStorage.getItem('temp')); // 'data'
97
+ console.log(memoryStorage.length); // 1
98
+ ```
99
+
100
+ ### Advanced Configuration
101
+
102
+ ```typescript
103
+ import { KeyStorage, InMemoryStorage } from '@ahoo-wang/fetcher-storage';
104
+
105
+ // Custom storage and event bus
106
+ const customStorage = new KeyStorage<string>({
107
+ key: 'custom',
108
+ storage: new InMemoryStorage(), // Use in-memory instead of localStorage
109
+ // eventBus: customEventBus, // Custom event bus for notifications
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
+ }
94
339
 
95
- // Browser storage (wraps localStorage or sessionStorage)
96
- const browserStorage = new BrowserListenableStorage(localStorage);
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
+ ];
97
369
 
98
- // In-memory storage (works in any environment)
99
- const memoryStorage = new InMemoryListenableStorage();
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();
100
386
  ```
101
387
 
102
- ## API
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
+ });
103
413
 
104
- ### createListenableStorage()
414
+ // Listen for storage changes
415
+ this.storage.subscribe(messages => {
416
+ if (!messages) return;
105
417
 
106
- Factory function that automatically returns the appropriate storage implementation based on the environment:
418
+ // Process new messages
419
+ messages.forEach(message => {
420
+ if (message.sourceTab !== this.tabId) {
421
+ this.notifyListeners(message.type, message);
422
+ }
423
+ });
424
+ });
107
425
 
108
- - Browser environment: `BrowserListenableStorage` wrapping `localStorage`
109
- - Non-browser environment: `InMemoryListenableStorage`
426
+ // Initialize storage if empty
427
+ if (!this.storage.get()) {
428
+ this.storage.set([]);
429
+ }
430
+ }
110
431
 
111
- ### ListenableStorage
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';
112
630
 
113
- Extends the native `Storage` interface with event listening capabilities:
631
+ // Storage-backed reducer
632
+ function createPersistentReducer(reducer: any, storageKey: string) {
633
+ const storage = new KeyStorage({
634
+ key: storageKey,
635
+ });
114
636
 
115
- - `addListener(listener: StorageListener): RemoveStorageListener`
116
- - All standard `Storage` methods (`getItem`, `setItem`, `removeItem`, etc.)
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
+ );
719
+ ```
720
+
721
+ ## API Reference
722
+
723
+ ### Environment Utilities
724
+
725
+ #### `isBrowser(): boolean`
726
+
727
+ Checks if the current environment is a browser.
728
+
729
+ #### `getStorage(): Storage`
730
+
731
+ Returns the appropriate storage implementation:
732
+
733
+ - Browser: `window.localStorage` (with availability check)
734
+ - Non-browser: `InMemoryStorage` instance
117
735
 
118
736
  ### KeyStorage
119
737
 
120
- A storage wrapper for managing a single value associated with a specific key:
738
+ A storage wrapper for managing typed values with caching and change notifications.
739
+
740
+ ```typescript
741
+ new KeyStorage<T>(options
742
+ :
743
+ KeyStorageOptions<T>
744
+ )
745
+ ```
121
746
 
122
- - Automatic caching with cache invalidation
123
- - Key-specific event listening
124
- - Custom serialization support
747
+ #### Options
748
+
749
+ - `key: string` - Storage key
750
+ - `serializer?: Serializer<string, T>` - Custom serializer (default: JsonSerializer)
751
+ - `storage?: Storage` - Custom storage (default: getStorage())
752
+ - `eventBus?: TypedEventBus<StorageEvent<T>>` - Custom event bus
753
+
754
+ #### Methods
755
+
756
+ - `get(): T | null` - Get cached value
757
+ - `set(value: T): void` - Set value with caching and notification
758
+ - `remove(): void` - Remove value and clear cache
759
+ - `addListener(handler: EventHandler<StorageEvent<T>>): RemoveStorageListener` - Add change listener
760
+
761
+ ### InMemoryStorage
762
+
763
+ In-memory implementation of the Storage interface.
764
+
765
+ ```typescript
766
+ new InMemoryStorage();
767
+ ```
768
+
769
+ Implements all standard Storage methods with Map-based storage.
125
770
 
126
771
  ### Serializers
127
772
 
128
- - `JsonSerializer`: Serializes values to/from JSON strings
129
- - `IdentitySerializer`: Passes values through without modification
773
+ #### `JsonSerializer`
774
+
775
+ Serializes values to/from JSON strings.
776
+
777
+ #### `typedIdentitySerializer<T>()`
778
+
779
+ Identity serializer that passes values through unchanged.
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
+
987
+ ## TypeScript Support
988
+
989
+ Full TypeScript support with generics and type inference:
990
+
991
+ ```typescript
992
+ // Typed storage
993
+ const userStorage = new KeyStorage<User>({ key: 'user' });
994
+
995
+ // Type-safe operations
996
+ userStorage.set({ id: 1, name: 'John' });
997
+ const user = userStorage.get(); // User | null
998
+ ```
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
130
1105
 
131
1106
  ## License
132
1107