@ahoo-wang/fetcher-storage 2.9.0 → 2.9.2

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
@@ -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)