@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 +1022 -47
- package/README.zh-CN.md +98 -46
- package/dist/env.d.ts +10 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/{inMemoryListenableStorage.d.ts → inMemoryStorage.d.ts} +2 -19
- package/dist/inMemoryStorage.d.ts.map +1 -0
- package/dist/index.d.ts +1 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.es.js +1323 -146
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/keyStorage.d.ts +28 -30
- package/dist/keyStorage.d.ts.map +1 -1
- package/dist/serializer.d.ts +1 -33
- package/dist/serializer.d.ts.map +1 -1
- package/package.json +11 -4
- package/dist/browserListenableStorage.d.ts +0 -51
- package/dist/browserListenableStorage.d.ts.map +0 -1
- package/dist/inMemoryListenableStorage.d.ts.map +0 -1
- package/dist/listenableStorage.d.ts +0 -33
- package/dist/listenableStorage.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -8,18 +8,18 @@
|
|
|
8
8
|
[](https://www.npmjs.com/package/@ahoo-wang/fetcher-storage)
|
|
9
9
|
[](https://deepwiki.com/Ahoo-Wang/fetcher)
|
|
10
10
|
|
|
11
|
-
A lightweight, cross-environment storage library with
|
|
12
|
-
browser localStorage
|
|
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
|
-
###
|
|
32
|
+
### Environment Detection and Storage Selection
|
|
33
33
|
|
|
34
34
|
```typescript
|
|
35
|
-
import {
|
|
35
|
+
import { getStorage, isBrowser } from '@ahoo-wang/fetcher-storage';
|
|
36
36
|
|
|
37
|
-
//
|
|
38
|
-
|
|
37
|
+
// Check if running in browser
|
|
38
|
+
console.log('Is browser:', isBrowser());
|
|
39
39
|
|
|
40
|
-
//
|
|
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
|
|
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
|
-
###
|
|
86
|
+
### In-Memory Storage
|
|
88
87
|
|
|
89
88
|
```typescript
|
|
90
|
-
import {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
//
|
|
96
|
-
const
|
|
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
|
-
//
|
|
99
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
414
|
+
// Listen for storage changes
|
|
415
|
+
this.storage.subscribe(messages => {
|
|
416
|
+
if (!messages) return;
|
|
105
417
|
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
426
|
+
// Initialize storage if empty
|
|
427
|
+
if (!this.storage.get()) {
|
|
428
|
+
this.storage.set([]);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
110
431
|
|
|
111
|
-
|
|
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
|
-
|
|
631
|
+
// Storage-backed reducer
|
|
632
|
+
function createPersistentReducer(reducer: any, storageKey: string) {
|
|
633
|
+
const storage = new KeyStorage({
|
|
634
|
+
key: storageKey,
|
|
635
|
+
});
|
|
114
636
|
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|