@ahoo-wang/fetcher-storage 2.9.6 → 2.9.9
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.zh-CN.md +939 -1
- package/dist/index.es.js +78 -0
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/keyStorage.d.ts +78 -0
- package/dist/keyStorage.d.ts.map +1 -1
- package/package.json +1 -1
package/README.zh-CN.md
CHANGED
|
@@ -107,8 +107,926 @@ const customStorage = new KeyStorage<string>({
|
|
|
107
107
|
storage: new InMemoryStorage(), // 使用内存存储而不是 localStorage
|
|
108
108
|
// eventBus: customEventBus, // 自定义事件总线用于通知
|
|
109
109
|
});
|
|
110
|
+
|
|
111
|
+
// 自定义序列化器处理复杂数据类型
|
|
112
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
113
|
+
|
|
114
|
+
class DateSerializer {
|
|
115
|
+
serialize(value: any): string {
|
|
116
|
+
return JSON.stringify(value, (key, val) =>
|
|
117
|
+
val instanceof Date ? { __type: 'Date', value: val.toISOString() } : val,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
deserialize(value: string): any {
|
|
122
|
+
return JSON.parse(value, (key, val) =>
|
|
123
|
+
val && typeof val === 'object' && val.__type === 'Date'
|
|
124
|
+
? new Date(val.value)
|
|
125
|
+
: val,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const dateStorage = new KeyStorage<{ createdAt: Date; data: string }>({
|
|
131
|
+
key: 'date-data',
|
|
132
|
+
serializer: new DateSerializer(),
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## 🚀 高级用法示例
|
|
137
|
+
|
|
138
|
+
### 与 RxJS 集成的响应式存储
|
|
139
|
+
|
|
140
|
+
创建与 RxJS 可观察对象集成的响应式存储:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
144
|
+
import { BehaviorSubject, Observable } from 'rxjs';
|
|
145
|
+
import { map, distinctUntilChanged } from 'rxjs/operators';
|
|
146
|
+
|
|
147
|
+
class ReactiveKeyStorage<T> extends KeyStorage<T> {
|
|
148
|
+
private subject: BehaviorSubject<T | null>;
|
|
149
|
+
|
|
150
|
+
constructor(options: any) {
|
|
151
|
+
super(options);
|
|
152
|
+
this.subject = new BehaviorSubject<T | null>(this.get());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 重写 set 以发出变更
|
|
156
|
+
set(value: T): void {
|
|
157
|
+
super.set(value);
|
|
158
|
+
this.subject.next(value);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 获取可观察对象以进行响应式更新
|
|
162
|
+
asObservable(): Observable<T | null> {
|
|
163
|
+
return this.subject.asObservable();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 获取特定属性的可观察对象
|
|
167
|
+
select<R>(selector: (value: T | null) => R): Observable<R> {
|
|
168
|
+
return this.subject.pipe(map(selector), distinctUntilChanged());
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 使用
|
|
173
|
+
const userStorage = new ReactiveKeyStorage<{ name: string; theme: string }>({
|
|
174
|
+
key: 'user-preferences',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// 对所有变更做出响应
|
|
178
|
+
userStorage.asObservable().subscribe(preferences => {
|
|
179
|
+
console.log('用户偏好已变更:', preferences);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// 对特定属性变更做出响应
|
|
183
|
+
userStorage
|
|
184
|
+
.select(prefs => prefs?.theme)
|
|
185
|
+
.subscribe(theme => {
|
|
186
|
+
document.body.className = `theme-${theme}`;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// 更新存储(将触发观察者)
|
|
190
|
+
userStorage.set({ name: 'John', theme: 'dark' });
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 使用 Web Crypto API 的加密存储
|
|
194
|
+
|
|
195
|
+
为敏感数据实现安全的加密存储:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
199
|
+
|
|
200
|
+
class EncryptedSerializer {
|
|
201
|
+
private keyPromise: Promise<CryptoKey>;
|
|
202
|
+
|
|
203
|
+
constructor(password: string) {
|
|
204
|
+
this.keyPromise = this.deriveKey(password);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private async deriveKey(password: string): Promise<CryptoKey> {
|
|
208
|
+
const encoder = new TextEncoder();
|
|
209
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
210
|
+
'raw',
|
|
211
|
+
encoder.encode(password),
|
|
212
|
+
'PBKDF2',
|
|
213
|
+
false,
|
|
214
|
+
['deriveBits', 'deriveKey'],
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
return crypto.subtle.deriveKey(
|
|
218
|
+
{
|
|
219
|
+
name: 'PBKDF2',
|
|
220
|
+
salt: encoder.encode('fetcher-storage-salt'),
|
|
221
|
+
iterations: 100000,
|
|
222
|
+
hash: 'SHA-256',
|
|
223
|
+
},
|
|
224
|
+
keyMaterial,
|
|
225
|
+
{ name: 'AES-GCM', length: 256 },
|
|
226
|
+
false,
|
|
227
|
+
['encrypt', 'decrypt'],
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async serialize(value: any): Promise<string> {
|
|
232
|
+
const key = await this.keyPromise;
|
|
233
|
+
const encoder = new TextEncoder();
|
|
234
|
+
const data = encoder.encode(JSON.stringify(value));
|
|
235
|
+
|
|
236
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
237
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
238
|
+
{ name: 'AES-GCM', iv },
|
|
239
|
+
key,
|
|
240
|
+
data,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// 合并 IV 和加密数据
|
|
244
|
+
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
|
245
|
+
combined.set(iv);
|
|
246
|
+
combined.set(new Uint8Array(encrypted), iv.length);
|
|
247
|
+
|
|
248
|
+
return btoa(String.fromCharCode(...combined));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async deserialize(value: string): Promise<any> {
|
|
252
|
+
const key = await this.keyPromise;
|
|
253
|
+
const combined = new Uint8Array(
|
|
254
|
+
atob(value)
|
|
255
|
+
.split('')
|
|
256
|
+
.map(c => c.charCodeAt(0)),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const iv = combined.slice(0, 12);
|
|
260
|
+
const encrypted = combined.slice(12);
|
|
261
|
+
|
|
262
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
263
|
+
{ name: 'AES-GCM', iv },
|
|
264
|
+
key,
|
|
265
|
+
encrypted,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const decoder = new TextDecoder();
|
|
269
|
+
return JSON.parse(decoder.decode(decrypted));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 使用(仅在安全上下文 - HTTPS 中工作)
|
|
274
|
+
const secureStorage = new KeyStorage<any>({
|
|
275
|
+
key: 'sensitive-data',
|
|
276
|
+
serializer: new EncryptedSerializer('user-password'),
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// 存储加密数据
|
|
280
|
+
secureStorage.set({ apiKey: 'secret-key', tokens: ['token1', 'token2'] });
|
|
281
|
+
|
|
282
|
+
// 检索解密数据
|
|
283
|
+
const data = secureStorage.get();
|
|
284
|
+
console.log(data); // { apiKey: 'secret-key', tokens: [...] }
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### 存储迁移和版本控制
|
|
288
|
+
|
|
289
|
+
跨应用版本处理存储模式迁移:
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
293
|
+
|
|
294
|
+
interface StorageVersion {
|
|
295
|
+
version: number;
|
|
296
|
+
migrate: (data: any) => any;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
class VersionedKeyStorage<T> extends KeyStorage<T> {
|
|
300
|
+
private migrations: StorageVersion[] = [];
|
|
301
|
+
|
|
302
|
+
constructor(options: any, migrations: StorageVersion[] = []) {
|
|
303
|
+
super(options);
|
|
304
|
+
this.migrations = migrations.sort((a, b) => a.version - b.version);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
get(): T | null {
|
|
308
|
+
const rawData = super.get();
|
|
309
|
+
if (!rawData) return null;
|
|
310
|
+
|
|
311
|
+
return this.migrateData(rawData);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private migrateData(data: any): T {
|
|
315
|
+
const currentVersion = data.__version || 0;
|
|
316
|
+
let migratedData = { ...data };
|
|
317
|
+
|
|
318
|
+
// 为干净数据移除版本标记
|
|
319
|
+
delete migratedData.__version;
|
|
320
|
+
|
|
321
|
+
// 按顺序应用迁移
|
|
322
|
+
for (const migration of this.migrations) {
|
|
323
|
+
if (currentVersion < migration.version) {
|
|
324
|
+
migratedData = migration.migrate(migratedData);
|
|
325
|
+
migratedData.__version = migration.version;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 保存迁移数据
|
|
330
|
+
if (migratedData.__version !== currentVersion) {
|
|
331
|
+
super.set(migratedData);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
delete migratedData.__version;
|
|
335
|
+
return migratedData;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 定义迁移
|
|
340
|
+
const migrations: StorageVersion[] = [
|
|
341
|
+
{
|
|
342
|
+
version: 1,
|
|
343
|
+
migrate: data => ({
|
|
344
|
+
...data,
|
|
345
|
+
// 如果缺失则添加默认主题
|
|
346
|
+
theme: data.theme || 'light',
|
|
347
|
+
}),
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
version: 2,
|
|
351
|
+
migrate: data => ({
|
|
352
|
+
...data,
|
|
353
|
+
// 重命名属性
|
|
354
|
+
preferences: data.settings || {},
|
|
355
|
+
settings: undefined,
|
|
356
|
+
}),
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
version: 3,
|
|
360
|
+
migrate: data => ({
|
|
361
|
+
...data,
|
|
362
|
+
// 添加时间戳
|
|
363
|
+
createdAt: data.createdAt || new Date().toISOString(),
|
|
364
|
+
updatedAt: new Date().toISOString(),
|
|
365
|
+
}),
|
|
366
|
+
},
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
// 使用
|
|
370
|
+
const userPrefsStorage = new VersionedKeyStorage<{
|
|
371
|
+
name: string;
|
|
372
|
+
theme: string;
|
|
373
|
+
preferences: Record<string, any>;
|
|
374
|
+
createdAt: string;
|
|
375
|
+
updatedAt: string;
|
|
376
|
+
}>(
|
|
377
|
+
{
|
|
378
|
+
key: 'user-preferences',
|
|
379
|
+
},
|
|
380
|
+
migrations,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// 数据将被自动迁移时访问
|
|
384
|
+
const prefs = userPrefsStorage.get();
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### 使用存储事件的跨标签页通信
|
|
388
|
+
|
|
389
|
+
使用存储事件实现跨标签页通信:
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
393
|
+
|
|
394
|
+
interface TabMessage {
|
|
395
|
+
id: string;
|
|
396
|
+
type: string;
|
|
397
|
+
payload: any;
|
|
398
|
+
timestamp: number;
|
|
399
|
+
sourceTab: string;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
class CrossTabMessenger {
|
|
403
|
+
private storage: KeyStorage<TabMessage[]>;
|
|
404
|
+
private tabId: string;
|
|
405
|
+
private listeners: Map<string, (message: TabMessage) => void> = new Map();
|
|
406
|
+
|
|
407
|
+
constructor(channelName: string = 'cross-tab-messages') {
|
|
408
|
+
this.tabId = `tab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
409
|
+
this.storage = new KeyStorage<TabMessage[]>({
|
|
410
|
+
key: channelName,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// 监听存储变更
|
|
414
|
+
this.storage.addListener(messages => {
|
|
415
|
+
if (!messages) return;
|
|
416
|
+
|
|
417
|
+
// 处理新消息
|
|
418
|
+
messages.forEach(message => {
|
|
419
|
+
if (message.sourceTab !== this.tabId) {
|
|
420
|
+
this.notifyListeners(message.type, message);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// 如果为空则初始化存储
|
|
426
|
+
if (!this.storage.get()) {
|
|
427
|
+
this.storage.set([]);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// 向其他标签页广播消息
|
|
432
|
+
broadcast(type: string, payload: any) {
|
|
433
|
+
const messages = this.storage.get() || [];
|
|
434
|
+
const message: TabMessage = {
|
|
435
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
436
|
+
type,
|
|
437
|
+
payload,
|
|
438
|
+
timestamp: Date.now(),
|
|
439
|
+
sourceTab: this.tabId,
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// 添加消息并保留最近消息
|
|
443
|
+
const updatedMessages = [...messages, message].slice(-50);
|
|
444
|
+
this.storage.set(updatedMessages);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 监听消息
|
|
448
|
+
on(type: string, callback: (message: TabMessage) => void) {
|
|
449
|
+
this.listeners.set(type, callback);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 移除监听器
|
|
453
|
+
off(type: string) {
|
|
454
|
+
this.listeners.delete(type);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private notifyListeners(type: string, message: TabMessage) {
|
|
458
|
+
const listener = this.listeners.get(type);
|
|
459
|
+
if (listener) {
|
|
460
|
+
listener(message);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// 获取当前标签页 ID
|
|
465
|
+
getTabId(): string {
|
|
466
|
+
return this.tabId;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 使用
|
|
471
|
+
const messenger = new CrossTabMessenger('app-messages');
|
|
472
|
+
|
|
473
|
+
// 监听来自其他标签页的用户登录事件
|
|
474
|
+
messenger.on('user-logged-in', message => {
|
|
475
|
+
console.log('用户从另一个标签页登录:', message.payload);
|
|
476
|
+
// 更新当前标签页的状态
|
|
477
|
+
updateUserState(message.payload.user);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// 广播用户操作
|
|
481
|
+
function onUserLogin(user: any) {
|
|
482
|
+
messenger.broadcast('user-logged-in', { user, tabId: messenger.getTabId() });
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### 性能监控和分析
|
|
487
|
+
|
|
488
|
+
为存储操作添加性能跟踪:
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
492
|
+
|
|
493
|
+
interface PerformanceMetrics {
|
|
494
|
+
operation: string;
|
|
495
|
+
duration: number;
|
|
496
|
+
timestamp: number;
|
|
497
|
+
success: boolean;
|
|
498
|
+
error?: string;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
class MonitoredKeyStorage<T> extends KeyStorage<T> {
|
|
502
|
+
private metrics: PerformanceMetrics[] = [];
|
|
503
|
+
private readonly maxMetrics = 100;
|
|
504
|
+
|
|
505
|
+
constructor(options: any) {
|
|
506
|
+
super(options);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
set(value: T): void {
|
|
510
|
+
const startTime = performance.now();
|
|
511
|
+
try {
|
|
512
|
+
super.set(value);
|
|
513
|
+
this.recordMetric('set', performance.now() - startTime, true);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
this.recordMetric(
|
|
516
|
+
'set',
|
|
517
|
+
performance.now() - startTime,
|
|
518
|
+
false,
|
|
519
|
+
String(error),
|
|
520
|
+
);
|
|
521
|
+
throw error;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
get(): T | null {
|
|
526
|
+
const startTime = performance.now();
|
|
527
|
+
try {
|
|
528
|
+
const result = super.get();
|
|
529
|
+
this.recordMetric('get', performance.now() - startTime, true);
|
|
530
|
+
return result;
|
|
531
|
+
} catch (error) {
|
|
532
|
+
this.recordMetric(
|
|
533
|
+
'get',
|
|
534
|
+
performance.now() - startTime,
|
|
535
|
+
false,
|
|
536
|
+
String(error),
|
|
537
|
+
);
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private recordMetric(
|
|
543
|
+
operation: string,
|
|
544
|
+
duration: number,
|
|
545
|
+
success: boolean,
|
|
546
|
+
error?: string,
|
|
547
|
+
) {
|
|
548
|
+
this.metrics.push({
|
|
549
|
+
operation,
|
|
550
|
+
duration,
|
|
551
|
+
timestamp: Date.now(),
|
|
552
|
+
success,
|
|
553
|
+
error,
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// 保留最近的指标
|
|
557
|
+
if (this.metrics.length > this.maxMetrics) {
|
|
558
|
+
this.metrics = this.metrics.slice(-this.maxMetrics);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// 获取性能统计
|
|
563
|
+
getPerformanceStats() {
|
|
564
|
+
const total = this.metrics.length;
|
|
565
|
+
const successful = this.metrics.filter(m => m.success).length;
|
|
566
|
+
const failed = total - successful;
|
|
567
|
+
|
|
568
|
+
const avgDuration =
|
|
569
|
+
this.metrics.reduce((sum, m) => sum + m.duration, 0) / total;
|
|
570
|
+
const maxDuration = Math.max(...this.metrics.map(m => m.duration));
|
|
571
|
+
const minDuration = Math.min(...this.metrics.map(m => m.duration));
|
|
572
|
+
|
|
573
|
+
return {
|
|
574
|
+
total,
|
|
575
|
+
successful,
|
|
576
|
+
failed,
|
|
577
|
+
successRate: successful / total,
|
|
578
|
+
avgDuration,
|
|
579
|
+
maxDuration,
|
|
580
|
+
minDuration,
|
|
581
|
+
recentErrors: this.metrics
|
|
582
|
+
.filter(m => !m.success)
|
|
583
|
+
.slice(-5)
|
|
584
|
+
.map(m => ({
|
|
585
|
+
operation: m.operation,
|
|
586
|
+
error: m.error,
|
|
587
|
+
timestamp: m.timestamp,
|
|
588
|
+
})),
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// 导出指标以进行分析
|
|
593
|
+
exportMetrics(): PerformanceMetrics[] {
|
|
594
|
+
return [...this.metrics];
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// 清除指标
|
|
598
|
+
clearMetrics() {
|
|
599
|
+
this.metrics = [];
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// 使用
|
|
604
|
+
const monitoredStorage = new MonitoredKeyStorage<any>({
|
|
605
|
+
key: 'app-data',
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// 正常使用
|
|
609
|
+
monitoredStorage.set({ user: 'john', settings: {} });
|
|
610
|
+
const data = monitoredStorage.get();
|
|
611
|
+
|
|
612
|
+
// 获取性能洞察
|
|
613
|
+
const stats = monitoredStorage.getPerformanceStats();
|
|
614
|
+
console.log('存储性能:', stats);
|
|
615
|
+
|
|
616
|
+
// 导出以进行进一步分析
|
|
617
|
+
const metrics = monitoredStorage.exportMetrics();
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### 与状态管理库集成
|
|
621
|
+
|
|
622
|
+
集成存储与流行状态管理解决方案的示例:
|
|
623
|
+
|
|
624
|
+
#### 与 Redux 集成
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
import { createStore, combineReducers } from 'redux';
|
|
628
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
629
|
+
|
|
630
|
+
// 存储支持的 reducer
|
|
631
|
+
function createPersistentReducer(reducer: any, storageKey: string) {
|
|
632
|
+
const storage = new KeyStorage({
|
|
633
|
+
key: storageKey,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// 从存储加载初始状态
|
|
637
|
+
const initialState = storage.get() || reducer(undefined, { type: '@@INIT' });
|
|
638
|
+
|
|
639
|
+
return (state = initialState, action: any) => {
|
|
640
|
+
const newState = reducer(state, action);
|
|
641
|
+
|
|
642
|
+
// 持久化状态变更(防抖)
|
|
643
|
+
if (action.type !== '@@INIT') {
|
|
644
|
+
setTimeout(() => storage.set(newState), 100);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return newState;
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// 使用
|
|
652
|
+
const userReducer = (state = { name: '', loggedIn: false }, action: any) => {
|
|
653
|
+
switch (action.type) {
|
|
654
|
+
case 'LOGIN':
|
|
655
|
+
return { ...state, name: action.payload.name, loggedIn: true };
|
|
656
|
+
case 'LOGOUT':
|
|
657
|
+
return { ...state, name: '', loggedIn: false };
|
|
658
|
+
default:
|
|
659
|
+
return state;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const rootReducer = combineReducers({
|
|
664
|
+
user: createPersistentReducer(userReducer, 'redux-user-state'),
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
const store = createStore(rootReducer);
|
|
668
|
+
|
|
669
|
+
// 状态将自动持久化和恢复
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
#### 与 Zustand 集成
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
import { create } from 'zustand';
|
|
676
|
+
import { subscribeWithSelector } from 'zustand/middleware';
|
|
677
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
678
|
+
|
|
679
|
+
interface AppState {
|
|
680
|
+
user: { name: string; email: string } | null;
|
|
681
|
+
theme: 'light' | 'dark';
|
|
682
|
+
login: (user: { name: string; email: string }) => void;
|
|
683
|
+
logout: () => void;
|
|
684
|
+
setTheme: (theme: 'light' | 'dark') => void;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const storage = new KeyStorage<AppState['user']>({
|
|
688
|
+
key: 'zustand-user',
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
export const useAppStore = create<AppState>()(
|
|
692
|
+
subscribeWithSelector((set, get) => ({
|
|
693
|
+
user: storage.get(),
|
|
694
|
+
theme: 'light',
|
|
695
|
+
|
|
696
|
+
login: user => {
|
|
697
|
+
set({ user });
|
|
698
|
+
storage.set(user);
|
|
699
|
+
},
|
|
700
|
+
|
|
701
|
+
logout: () => {
|
|
702
|
+
set({ user: null });
|
|
703
|
+
storage.set(null);
|
|
704
|
+
},
|
|
705
|
+
|
|
706
|
+
setTheme: theme => set({ theme }),
|
|
707
|
+
})),
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
// 自动持久化主题变更
|
|
711
|
+
useAppStore.subscribe(
|
|
712
|
+
state => state.theme,
|
|
713
|
+
theme => {
|
|
714
|
+
const themeStorage = new KeyStorage({ key: 'app-theme' });
|
|
715
|
+
themeStorage.set(theme);
|
|
716
|
+
},
|
|
717
|
+
);
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
## 实际示例
|
|
721
|
+
|
|
722
|
+
### 用户会话管理
|
|
723
|
+
|
|
724
|
+
```typescript
|
|
725
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
726
|
+
|
|
727
|
+
interface UserSession {
|
|
728
|
+
userId: string;
|
|
729
|
+
token: string;
|
|
730
|
+
expiresAt: Date;
|
|
731
|
+
preferences: Record<string, any>;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
class SessionManager {
|
|
735
|
+
private sessionStorage = new KeyStorage<UserSession>({
|
|
736
|
+
key: 'user-session',
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
async login(credentials: LoginCredentials): Promise<UserSession> {
|
|
740
|
+
const response = await fetch('/api/login', {
|
|
741
|
+
method: 'POST',
|
|
742
|
+
body: JSON.stringify(credentials),
|
|
743
|
+
});
|
|
744
|
+
const session = await response.json();
|
|
745
|
+
|
|
746
|
+
this.sessionStorage.set(session);
|
|
747
|
+
return session;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
getCurrentSession(): UserSession | null {
|
|
751
|
+
const session = this.sessionStorage.get();
|
|
752
|
+
if (!session) return null;
|
|
753
|
+
|
|
754
|
+
// 检查会话是否过期
|
|
755
|
+
if (new Date(session.expiresAt) < new Date()) {
|
|
756
|
+
this.logout();
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return session;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
logout(): void {
|
|
764
|
+
this.sessionStorage.remove();
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
updatePreferences(preferences: Record<string, any>): void {
|
|
768
|
+
const session = this.getCurrentSession();
|
|
769
|
+
if (session) {
|
|
770
|
+
this.sessionStorage.set({
|
|
771
|
+
...session,
|
|
772
|
+
preferences: { ...session.preferences, ...preferences },
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### 跨标签页应用状态
|
|
780
|
+
|
|
781
|
+
```typescript
|
|
782
|
+
import {
|
|
783
|
+
KeyStorage,
|
|
784
|
+
BroadcastTypedEventBus,
|
|
785
|
+
SerialTypedEventBus,
|
|
786
|
+
} from '@ahoo-wang/fetcher-storage';
|
|
787
|
+
|
|
788
|
+
interface AppState {
|
|
789
|
+
theme: 'light' | 'dark';
|
|
790
|
+
language: string;
|
|
791
|
+
sidebarCollapsed: boolean;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
class AppStateManager {
|
|
795
|
+
private stateStorage: KeyStorage<AppState>;
|
|
796
|
+
|
|
797
|
+
constructor() {
|
|
798
|
+
// 使用广播事件总线进行跨标签页同步
|
|
799
|
+
const eventBus = new BroadcastTypedEventBus(
|
|
800
|
+
new SerialTypedEventBus('app-state'),
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
this.stateStorage = new KeyStorage<AppState>({
|
|
804
|
+
key: 'app-state',
|
|
805
|
+
eventBus,
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// 监听来自其他标签页的状态变更
|
|
809
|
+
this.stateStorage.addListener(event => {
|
|
810
|
+
if (event.newValue) {
|
|
811
|
+
this.applyStateToUI(event.newValue);
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
getState(): AppState {
|
|
817
|
+
return (
|
|
818
|
+
this.stateStorage.get() || {
|
|
819
|
+
theme: 'light',
|
|
820
|
+
language: 'en',
|
|
821
|
+
sidebarCollapsed: false,
|
|
822
|
+
}
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
updateState(updates: Partial<AppState>): void {
|
|
827
|
+
const currentState = this.getState();
|
|
828
|
+
const newState = { ...currentState, ...updates };
|
|
829
|
+
this.stateStorage.set(newState);
|
|
830
|
+
this.applyStateToUI(newState);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
private applyStateToUI(state: AppState): void {
|
|
834
|
+
document.documentElement.setAttribute('data-theme', state.theme);
|
|
835
|
+
// 根据状态更新 UI 组件
|
|
836
|
+
}
|
|
837
|
+
}
|
|
110
838
|
```
|
|
111
839
|
|
|
840
|
+
### 表单自动保存
|
|
841
|
+
|
|
842
|
+
```typescript
|
|
843
|
+
import { KeyStorage } from '@ahoo-wang/fetcher-storage';
|
|
844
|
+
import { useEffect, useState } from 'react';
|
|
845
|
+
|
|
846
|
+
interface FormData {
|
|
847
|
+
title: string;
|
|
848
|
+
content: string;
|
|
849
|
+
tags: string[];
|
|
850
|
+
lastSaved: Date;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function useAutoSaveForm(formId: string) {
|
|
854
|
+
const [formData, setFormData] = useState<Partial<FormData>>({});
|
|
855
|
+
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
|
856
|
+
|
|
857
|
+
const formStorage = new KeyStorage<Partial<FormData>>({
|
|
858
|
+
key: `form-autosave-${formId}`
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
// 挂载时加载保存的数据
|
|
862
|
+
useEffect(() => {
|
|
863
|
+
const saved = formStorage.get();
|
|
864
|
+
if (saved) {
|
|
865
|
+
setFormData(saved);
|
|
866
|
+
setLastSaved(saved.lastSaved || null);
|
|
867
|
+
}
|
|
868
|
+
}, [formStorage]);
|
|
869
|
+
|
|
870
|
+
// 变更时自动保存
|
|
871
|
+
useEffect(() => {
|
|
872
|
+
if (Object.keys(formData).length > 0) {
|
|
873
|
+
const dataToSave = {
|
|
874
|
+
...formData,
|
|
875
|
+
lastSaved: new Date(),
|
|
876
|
+
};
|
|
877
|
+
formStorage.set(dataToSave);
|
|
878
|
+
setLastSaved(dataToSave.lastSaved);
|
|
879
|
+
}
|
|
880
|
+
}, [formData, formStorage]);
|
|
881
|
+
|
|
882
|
+
const clearAutoSave = () => {
|
|
883
|
+
formStorage.remove();
|
|
884
|
+
setFormData({});
|
|
885
|
+
setLastSaved(null);
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
return {
|
|
889
|
+
formData,
|
|
890
|
+
setFormData,
|
|
891
|
+
lastSaved,
|
|
892
|
+
clearAutoSave,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// 在组件中使用
|
|
897
|
+
function ArticleEditor({ articleId }: { articleId: string }) {
|
|
898
|
+
const { formData, setFormData, lastSaved, clearAutoSave } = useAutoSaveForm(articleId);
|
|
899
|
+
|
|
900
|
+
return (
|
|
901
|
+
<div>
|
|
902
|
+
{lastSaved && (
|
|
903
|
+
<div className="autosave-indicator">
|
|
904
|
+
自动保存于 {lastSaved.toLocaleTimeString()}
|
|
905
|
+
</div>
|
|
906
|
+
)}
|
|
907
|
+
|
|
908
|
+
<input
|
|
909
|
+
value={formData.title || ''}
|
|
910
|
+
onChange={e => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
|
911
|
+
placeholder="文章标题"
|
|
912
|
+
/>
|
|
913
|
+
|
|
914
|
+
<textarea
|
|
915
|
+
value={formData.content || ''}
|
|
916
|
+
onChange={e => setFormData(prev => ({ ...prev, content: e.target.value }))}
|
|
917
|
+
placeholder="文章内容"
|
|
918
|
+
/>
|
|
919
|
+
|
|
920
|
+
<button onClick={clearAutoSave}>清除自动保存</button>
|
|
921
|
+
</div>
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
## 故障排除
|
|
927
|
+
|
|
928
|
+
### 常见问题
|
|
929
|
+
|
|
930
|
+
#### 存储配额超出
|
|
931
|
+
|
|
932
|
+
```typescript
|
|
933
|
+
// 处理存储配额错误
|
|
934
|
+
try {
|
|
935
|
+
userStorage.set(largeData);
|
|
936
|
+
} catch (error) {
|
|
937
|
+
if (error.name === 'QuotaExceededError') {
|
|
938
|
+
// 降级到内存存储或压缩数据
|
|
939
|
+
console.warn('存储配额超出,使用降级方案');
|
|
940
|
+
// 实现降级逻辑
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
#### 跨标签页同步不工作
|
|
946
|
+
|
|
947
|
+
```typescript
|
|
948
|
+
// 确保 BroadcastChannel 被支持
|
|
949
|
+
if ('BroadcastChannel' in window) {
|
|
950
|
+
const eventBus = new BroadcastTypedEventBus(
|
|
951
|
+
new SerialTypedEventBus('my-app'),
|
|
952
|
+
);
|
|
953
|
+
// 与 KeyStorage 一起使用
|
|
954
|
+
} else {
|
|
955
|
+
console.warn('BroadcastChannel 不被支持,降级到仅本地存储');
|
|
956
|
+
}
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
#### 序列化错误
|
|
960
|
+
|
|
961
|
+
```typescript
|
|
962
|
+
// 处理循环引用和复杂对象
|
|
963
|
+
class SafeJsonSerializer implements Serializer<string, any> {
|
|
964
|
+
serialize(value: any): string {
|
|
965
|
+
// 移除循环引用或处理特殊情况
|
|
966
|
+
const safeValue = this.makeSerializable(value);
|
|
967
|
+
return JSON.stringify(safeValue);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
deserialize(value: string): any {
|
|
971
|
+
return JSON.parse(value);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
private makeSerializable(obj: any, seen = new WeakSet()): any {
|
|
975
|
+
if (obj === null || typeof obj !== 'object') return obj;
|
|
976
|
+
if (seen.has(obj)) return '[Circular]';
|
|
977
|
+
|
|
978
|
+
seen.add(obj);
|
|
979
|
+
const result: any = Array.isArray(obj) ? [] : {};
|
|
980
|
+
|
|
981
|
+
for (const key in obj) {
|
|
982
|
+
if (obj.hasOwnProperty(key)) {
|
|
983
|
+
result[key] = this.makeSerializable(obj[key], seen);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
seen.delete(obj);
|
|
988
|
+
return result;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
#### 内存泄漏
|
|
994
|
+
|
|
995
|
+
```typescript
|
|
996
|
+
// 始终清理监听器
|
|
997
|
+
class ComponentWithStorage {
|
|
998
|
+
private storage: KeyStorage<any>;
|
|
999
|
+
private removeListener: () => void;
|
|
1000
|
+
|
|
1001
|
+
constructor() {
|
|
1002
|
+
this.storage = new KeyStorage({ key: 'component-data' });
|
|
1003
|
+
this.removeListener = this.storage.addListener(event => {
|
|
1004
|
+
// 处理变更
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
destroy() {
|
|
1009
|
+
// 组件销毁时清理
|
|
1010
|
+
this.removeListener();
|
|
1011
|
+
this.storage.destroy?.(); // 如果可用
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
### 性能提示
|
|
1017
|
+
|
|
1018
|
+
- **使用合适的序列化器**:简单对象使用 JSON,复杂数据使用自定义序列化器
|
|
1019
|
+
- **批量操作**:尽可能将多个存储操作分组
|
|
1020
|
+
- **监控存储大小**:实施大小限制和清理策略
|
|
1021
|
+
- **临时数据使用内存存储**:避免持久化不必要的数据
|
|
1022
|
+
- **防抖频繁更新**:防止过度存储写入
|
|
1023
|
+
|
|
1024
|
+
### 浏览器兼容性
|
|
1025
|
+
|
|
1026
|
+
- **localStorage**:IE 8+,所有现代浏览器
|
|
1027
|
+
- **BroadcastChannel**:Chrome 54+,Firefox 38+,Safari 15.4+
|
|
1028
|
+
- **降级处理**:始终为不支持的功能提供降级方案
|
|
1029
|
+
|
|
112
1030
|
## API 参考
|
|
113
1031
|
|
|
114
1032
|
### 环境工具
|
|
@@ -166,6 +1084,20 @@ new InMemoryStorage();
|
|
|
166
1084
|
|
|
167
1085
|
恒等序列化器,直接传递值而不修改。
|
|
168
1086
|
|
|
1087
|
+
## 🧪 测试
|
|
1088
|
+
|
|
1089
|
+
```bash
|
|
1090
|
+
# 运行测试
|
|
1091
|
+
pnpm test
|
|
1092
|
+
|
|
1093
|
+
# 运行带覆盖率的测试
|
|
1094
|
+
pnpm test --coverage
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
## 🤝 贡献
|
|
1098
|
+
|
|
1099
|
+
欢迎贡献!请查看 [贡献指南](https://github.com/Ahoo-Wang/fetcher/blob/main/CONTRIBUTING.md) 了解更多详情。
|
|
1100
|
+
|
|
169
1101
|
## TypeScript 支持
|
|
170
1102
|
|
|
171
1103
|
完整的 TypeScript 支持,包括泛型和类型推断:
|
|
@@ -179,6 +1111,12 @@ userStorage.set({ id: 1, name: 'John' });
|
|
|
179
1111
|
const user = userStorage.get(); // User | null
|
|
180
1112
|
```
|
|
181
1113
|
|
|
182
|
-
## 许可证
|
|
1114
|
+
## 📄 许可证
|
|
183
1115
|
|
|
184
1116
|
[Apache 2.0](https://github.com/Ahoo-Wang/fetcher/blob/master/LICENSE)
|
|
1117
|
+
|
|
1118
|
+
---
|
|
1119
|
+
|
|
1120
|
+
<p align="center">
|
|
1121
|
+
Fetcher 生态系统的一部分
|
|
1122
|
+
</p>
|
package/dist/index.es.js
CHANGED
|
@@ -107,15 +107,67 @@ class m {
|
|
|
107
107
|
`KeyStorage:${this.key}`
|
|
108
108
|
), this.eventBus.on(this.keyStorageHandler);
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Adds a listener for storage changes.
|
|
112
|
+
*
|
|
113
|
+
* The listener will be called whenever the storage value changes,
|
|
114
|
+
* either locally or from other tabs/windows.
|
|
115
|
+
*
|
|
116
|
+
* @param listener - The event handler to be called when storage changes
|
|
117
|
+
* @returns A function that can be called to remove the listener
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* const storage = new KeyStorage<string>({ key: 'userName' });
|
|
122
|
+
* const removeListener = storage.addListener({
|
|
123
|
+
* name: 'userNameChange',
|
|
124
|
+
* handle: (event) => {
|
|
125
|
+
* console.log('User name changed:', event.newValue);
|
|
126
|
+
* }
|
|
127
|
+
* });
|
|
128
|
+
*
|
|
129
|
+
* // Later, to remove the listener
|
|
130
|
+
* removeListener();
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
110
133
|
addListener(e) {
|
|
111
134
|
return this.eventBus.on(e), () => this.eventBus.off(e.name);
|
|
112
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Retrieves the current value from storage.
|
|
138
|
+
*
|
|
139
|
+
* Uses caching to avoid repeated deserialization. If the value is not in cache,
|
|
140
|
+
* it retrieves it from the underlying storage and deserializes it.
|
|
141
|
+
*
|
|
142
|
+
* @returns The deserialized value, or null if no value exists in storage
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* const storage = new KeyStorage<string>({ key: 'userName' });
|
|
147
|
+
* const userName = storage.get();
|
|
148
|
+
* console.log(userName); // 'John Doe' or null
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
113
151
|
get() {
|
|
114
152
|
if (this.cacheValue)
|
|
115
153
|
return this.cacheValue;
|
|
116
154
|
const e = this.storage.getItem(this.key);
|
|
117
155
|
return e ? (this.cacheValue = this.serializer.deserialize(e), this.cacheValue) : null;
|
|
118
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Stores a value in storage and notifies all listeners.
|
|
159
|
+
*
|
|
160
|
+
* Serializes the value, stores it in the underlying storage, updates the cache,
|
|
161
|
+
* and emits a change event to all registered listeners.
|
|
162
|
+
*
|
|
163
|
+
* @param value - The value to store (will be serialized before storage)
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* const storage = new KeyStorage<string>({ key: 'userName' });
|
|
168
|
+
* storage.set('John Doe');
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
119
171
|
set(e) {
|
|
120
172
|
const t = this.get(), s = this.serializer.serialize(e);
|
|
121
173
|
this.storage.setItem(this.key, s), this.cacheValue = e, this.eventBus.emit({
|
|
@@ -123,6 +175,18 @@ class m {
|
|
|
123
175
|
oldValue: t
|
|
124
176
|
});
|
|
125
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Removes the value from storage and notifies all listeners.
|
|
180
|
+
*
|
|
181
|
+
* Removes the item from the underlying storage, clears the cache,
|
|
182
|
+
* and emits a change event indicating the value was removed.
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```typescript
|
|
186
|
+
* const storage = new KeyStorage<string>({ key: 'userName' });
|
|
187
|
+
* storage.remove(); // Removes the stored value
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
126
190
|
remove() {
|
|
127
191
|
const e = this.get();
|
|
128
192
|
this.storage.removeItem(this.key), this.cacheValue = null, this.eventBus.emit({
|
|
@@ -130,6 +194,20 @@ class m {
|
|
|
130
194
|
newValue: null
|
|
131
195
|
});
|
|
132
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Cleans up resources used by the KeyStorage instance.
|
|
199
|
+
*
|
|
200
|
+
* Removes the internal event handler from the event bus.
|
|
201
|
+
* Should be called when the KeyStorage instance is no longer needed
|
|
202
|
+
* to prevent memory leaks.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* const storage = new KeyStorage<string>({ key: 'userName' });
|
|
207
|
+
* // ... use storage ...
|
|
208
|
+
* storage.destroy(); // Clean up resources
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
133
211
|
destroy() {
|
|
134
212
|
this.eventBus.off(this.keyStorageHandler.name);
|
|
135
213
|
}
|
package/dist/index.es.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.es.js","sources":["../src/inMemoryStorage.ts","../src/env.ts","../src/serializer.ts","../src/keyStorage.ts"],"sourcesContent":["/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport class InMemoryStorage implements Storage {\n private readonly store: Map<string, string> = new Map();\n\n /**\n * Gets the number of items stored in the storage.\n */\n get length(): number {\n return this.store.size;\n }\n\n /**\n * Clears all items from the storage.\n */\n clear(): void {\n this.store.clear();\n }\n\n /**\n * Gets an item from the storage.\n * @param key - The key of the item to retrieve\n * @returns The value of the item, or null if the item does not exist\n */\n getItem(key: string): string | null {\n const value = this.store.get(key);\n return value !== undefined ? value : null;\n }\n\n /**\n * Gets the key at the specified index.\n * @param index - The index of the key to retrieve\n * @returns The key at the specified index, or null if the index is out of bounds\n */\n key(index: number): string | null {\n const keys = Array.from(this.store.keys());\n return keys[index] || null;\n }\n\n /**\n * Removes an item from the storage.\n * @param key - The key of the item to remove\n */\n removeItem(key: string): void {\n this.store.delete(key);\n }\n\n /**\n * Sets an item in the storage.\n * @param key - The key of the item to set\n * @param value - The value to set\n */\n setItem(key: string, value: string): void {\n this.store.set(key, value);\n }\n}","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { InMemoryStorage } from './inMemoryStorage';\n\n/**\n * Checks if the current environment is a browser.\n * @returns True if running in a browser environment, false otherwise\n */\nexport function isBrowser(): boolean {\n return typeof window !== 'undefined';\n}\n\n/**\n * Gets the appropriate storage implementation based on the environment.\n * Returns localStorage in browser environments if available, InMemoryStorage otherwise.\n * @returns A Storage-compatible object\n */\nexport const getStorage = (): Storage => {\n if (isBrowser()) {\n return window.localStorage;\n }\n return new InMemoryStorage();\n};\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Interface for serializing and deserializing values\n * @template Serialized The type of the serialized value\n * @template Deserialized The type of the deserialized value\n */\nexport interface Serializer<Serialized, Deserialized> {\n /**\n * Serializes a value to the specified format\n * @param value The value to serialize\n * @returns The serialized value\n */\n serialize(value: any): Serialized;\n\n /**\n * Deserializes a value from the specified format\n * @param value The value to deserialize\n * @returns The deserialized value\n */\n deserialize(value: Serialized): Deserialized;\n}\n\n/**\n * Implementation of Serializer that uses JSON for serialization\n */\nexport class JsonSerializer implements Serializer<string, any> {\n /**\n * Serializes a value to a JSON string\n * @param value The value to serialize\n * @returns The JSON string representation of the value\n */\n serialize(value: any): string {\n return JSON.stringify(value);\n }\n\n /**\n * Deserializes a JSON string to a value\n * @param value The JSON string to deserialize\n * @returns The deserialized value\n */\n deserialize(value: string): any {\n return JSON.parse(value);\n }\n}\n\n/**\n * Implementation of Serializer that performs no actual serialization\n * @template T The type of the value to pass through\n */\nexport class IdentitySerializer<T> implements Serializer<T, T> {\n /**\n * Returns the value as-is without serialization\n * @param value The value to pass through\n * @returns The same value that was passed in\n */\n serialize(value: T): T {\n return value;\n }\n\n /**\n * Returns the value as-is without deserialization\n * @param value The value to pass through\n * @returns The same value that was passed in\n */\n deserialize(value: T): T {\n return value;\n }\n}\n\n/**\n * Global instance of JsonSerializer\n */\nexport const jsonSerializer = new JsonSerializer();\n/**\n * Global instance of IdentitySerializer\n */\nexport const identitySerializer = new IdentitySerializer<any>();\n\nexport function typedIdentitySerializer<T>(): IdentitySerializer<T> {\n return identitySerializer as IdentitySerializer<T>;\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Serializer, typedIdentitySerializer } from './serializer';\nimport {\n EventHandler, nameGenerator,\n SerialTypedEventBus,\n TypedEventBus,\n} from '@ahoo-wang/fetcher-eventbus';\nimport { getStorage } from './env';\n\nexport interface StorageEvent<Deserialized> {\n newValue?: Deserialized | null;\n oldValue?: Deserialized | null;\n}\n\n/**\n * A function that removes a storage listener when called.\n */\nexport type RemoveStorageListener = () => void;\n\nexport interface StorageListenable<Deserialized> {\n /**\n * Adds a listener for storage changes.\n * @param listener - The listener function to be called when storage changes\n * @returns A function that can be called to remove the listener\n */\n addListener(\n listener: EventHandler<StorageEvent<Deserialized>>,\n ): RemoveStorageListener;\n}\n\n/**\n * Options for configuring KeyStorage\n */\nexport interface KeyStorageOptions<Deserialized> {\n /**\n * The key used to store and retrieve values from storage\n */\n key: string;\n\n /**\n * Optional serializer for converting values to and from storage format\n * Defaults to IdentitySerializer if not provided\n */\n serializer?: Serializer<string, Deserialized>;\n\n /**\n * Optional storage instance. Defaults to localStorage\n */\n storage?: Storage;\n\n /**\n * Optional event bus for cross-tab communication. Defaults to SerialTypedEventBus\n */\n eventBus?: TypedEventBus<StorageEvent<Deserialized>>;\n}\n\n/**\n * A storage wrapper that manages a single value associated with a specific key\n * Provides caching and automatic cache invalidation when the storage value changes\n * @template Deserialized The type of the value being stored\n */\nexport class KeyStorage<Deserialized>\n implements StorageListenable<Deserialized> {\n private readonly key: string;\n private readonly serializer: Serializer<string, Deserialized>;\n private readonly storage: Storage;\n private readonly eventBus: TypedEventBus<StorageEvent<Deserialized>>;\n private cacheValue: Deserialized | null = null;\n private readonly keyStorageHandler: EventHandler<StorageEvent<Deserialized>> = {\n name: nameGenerator.generate('KeyStorage'),\n handle: (event: StorageEvent<Deserialized>) => {\n this.cacheValue = event.newValue ?? null;\n },\n };\n\n /**\n * Creates a new KeyStorage instance\n * @param options Configuration options for the storage\n */\n constructor(options: KeyStorageOptions<Deserialized>) {\n this.key = options.key;\n this.serializer = options.serializer ?? typedIdentitySerializer();\n this.storage = options.storage ?? getStorage();\n this.eventBus =\n options.eventBus ??\n new SerialTypedEventBus<StorageEvent<Deserialized>>(\n `KeyStorage:${this.key}`,\n );\n this.eventBus.on(this.keyStorageHandler);\n }\n\n addListener(\n listener: EventHandler<StorageEvent<Deserialized>>,\n ): RemoveStorageListener {\n this.eventBus.on(listener);\n return () => this.eventBus.off(listener.name);\n }\n\n get(): Deserialized | null {\n if (this.cacheValue) {\n return this.cacheValue;\n }\n const value = this.storage.getItem(this.key);\n if (!value) {\n return null;\n }\n this.cacheValue = this.serializer.deserialize(value);\n return this.cacheValue;\n }\n\n set(value: Deserialized): void {\n const oldValue = this.get();\n const serialized = this.serializer.serialize(value);\n this.storage.setItem(this.key, serialized);\n this.cacheValue = value;\n this.eventBus.emit({\n newValue: value,\n oldValue: oldValue,\n });\n }\n\n remove(): void {\n const oldValue = this.get();\n this.storage.removeItem(this.key);\n this.cacheValue = null;\n this.eventBus.emit({\n oldValue: oldValue,\n newValue: null,\n });\n }\n\n destroy() {\n this.eventBus.off(this.keyStorageHandler.name);\n }\n}\n"],"names":["InMemoryStorage","key","value","index","isBrowser","getStorage","JsonSerializer","IdentitySerializer","jsonSerializer","identitySerializer","typedIdentitySerializer","KeyStorage","options","nameGenerator","event","SerialTypedEventBus","listener","oldValue","serialized"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.es.js","sources":["../src/inMemoryStorage.ts","../src/env.ts","../src/serializer.ts","../src/keyStorage.ts"],"sourcesContent":["/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport class InMemoryStorage implements Storage {\n private readonly store: Map<string, string> = new Map();\n\n /**\n * Gets the number of items stored in the storage.\n */\n get length(): number {\n return this.store.size;\n }\n\n /**\n * Clears all items from the storage.\n */\n clear(): void {\n this.store.clear();\n }\n\n /**\n * Gets an item from the storage.\n * @param key - The key of the item to retrieve\n * @returns The value of the item, or null if the item does not exist\n */\n getItem(key: string): string | null {\n const value = this.store.get(key);\n return value !== undefined ? value : null;\n }\n\n /**\n * Gets the key at the specified index.\n * @param index - The index of the key to retrieve\n * @returns The key at the specified index, or null if the index is out of bounds\n */\n key(index: number): string | null {\n const keys = Array.from(this.store.keys());\n return keys[index] || null;\n }\n\n /**\n * Removes an item from the storage.\n * @param key - The key of the item to remove\n */\n removeItem(key: string): void {\n this.store.delete(key);\n }\n\n /**\n * Sets an item in the storage.\n * @param key - The key of the item to set\n * @param value - The value to set\n */\n setItem(key: string, value: string): void {\n this.store.set(key, value);\n }\n}","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { InMemoryStorage } from './inMemoryStorage';\n\n/**\n * Checks if the current environment is a browser.\n * @returns True if running in a browser environment, false otherwise\n */\nexport function isBrowser(): boolean {\n return typeof window !== 'undefined';\n}\n\n/**\n * Gets the appropriate storage implementation based on the environment.\n * Returns localStorage in browser environments if available, InMemoryStorage otherwise.\n * @returns A Storage-compatible object\n */\nexport const getStorage = (): Storage => {\n if (isBrowser()) {\n return window.localStorage;\n }\n return new InMemoryStorage();\n};\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Interface for serializing and deserializing values\n * @template Serialized The type of the serialized value\n * @template Deserialized The type of the deserialized value\n */\nexport interface Serializer<Serialized, Deserialized> {\n /**\n * Serializes a value to the specified format\n * @param value The value to serialize\n * @returns The serialized value\n */\n serialize(value: any): Serialized;\n\n /**\n * Deserializes a value from the specified format\n * @param value The value to deserialize\n * @returns The deserialized value\n */\n deserialize(value: Serialized): Deserialized;\n}\n\n/**\n * Implementation of Serializer that uses JSON for serialization\n */\nexport class JsonSerializer implements Serializer<string, any> {\n /**\n * Serializes a value to a JSON string\n * @param value The value to serialize\n * @returns The JSON string representation of the value\n */\n serialize(value: any): string {\n return JSON.stringify(value);\n }\n\n /**\n * Deserializes a JSON string to a value\n * @param value The JSON string to deserialize\n * @returns The deserialized value\n */\n deserialize(value: string): any {\n return JSON.parse(value);\n }\n}\n\n/**\n * Implementation of Serializer that performs no actual serialization\n * @template T The type of the value to pass through\n */\nexport class IdentitySerializer<T> implements Serializer<T, T> {\n /**\n * Returns the value as-is without serialization\n * @param value The value to pass through\n * @returns The same value that was passed in\n */\n serialize(value: T): T {\n return value;\n }\n\n /**\n * Returns the value as-is without deserialization\n * @param value The value to pass through\n * @returns The same value that was passed in\n */\n deserialize(value: T): T {\n return value;\n }\n}\n\n/**\n * Global instance of JsonSerializer\n */\nexport const jsonSerializer = new JsonSerializer();\n/**\n * Global instance of IdentitySerializer\n */\nexport const identitySerializer = new IdentitySerializer<any>();\n\nexport function typedIdentitySerializer<T>(): IdentitySerializer<T> {\n return identitySerializer as IdentitySerializer<T>;\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Serializer, typedIdentitySerializer } from './serializer';\nimport {\n EventHandler,\n nameGenerator,\n SerialTypedEventBus,\n TypedEventBus,\n} from '@ahoo-wang/fetcher-eventbus';\nimport { getStorage } from './env';\n\nexport interface StorageEvent<Deserialized> {\n newValue?: Deserialized | null;\n oldValue?: Deserialized | null;\n}\n\n/**\n * A function that removes a storage listener when called.\n */\nexport type RemoveStorageListener = () => void;\n\nexport interface StorageListenable<Deserialized> {\n /**\n * Adds a listener for storage changes.\n * @param listener - The listener function to be called when storage changes\n * @returns A function that can be called to remove the listener\n */\n addListener(\n listener: EventHandler<StorageEvent<Deserialized>>,\n ): RemoveStorageListener;\n}\n\n/**\n * Options for configuring KeyStorage\n */\nexport interface KeyStorageOptions<Deserialized> {\n /**\n * The key used to store and retrieve values from storage\n */\n key: string;\n\n /**\n * Optional serializer for converting values to and from storage format\n * Defaults to IdentitySerializer if not provided\n */\n serializer?: Serializer<string, Deserialized>;\n\n /**\n * Optional storage instance. Defaults to localStorage\n */\n storage?: Storage;\n\n /**\n * Optional event bus for cross-tab communication. Defaults to SerialTypedEventBus\n */\n eventBus?: TypedEventBus<StorageEvent<Deserialized>>;\n}\n\n/**\n * A storage wrapper that manages a single value associated with a specific key\n * Provides caching and automatic cache invalidation when the storage value changes\n * @template Deserialized The type of the value being stored\n */\nexport class KeyStorage<Deserialized>\n implements StorageListenable<Deserialized>\n{\n private readonly key: string;\n private readonly serializer: Serializer<string, Deserialized>;\n private readonly storage: Storage;\n private readonly eventBus: TypedEventBus<StorageEvent<Deserialized>>;\n private cacheValue: Deserialized | null = null;\n private readonly keyStorageHandler: EventHandler<StorageEvent<Deserialized>> =\n {\n name: nameGenerator.generate('KeyStorage'),\n handle: (event: StorageEvent<Deserialized>) => {\n this.cacheValue = event.newValue ?? null;\n },\n };\n\n /**\n * Creates a new KeyStorage instance\n * @param options Configuration options for the storage\n */\n constructor(options: KeyStorageOptions<Deserialized>) {\n this.key = options.key;\n this.serializer = options.serializer ?? typedIdentitySerializer();\n this.storage = options.storage ?? getStorage();\n this.eventBus =\n options.eventBus ??\n new SerialTypedEventBus<StorageEvent<Deserialized>>(\n `KeyStorage:${this.key}`,\n );\n this.eventBus.on(this.keyStorageHandler);\n }\n\n /**\n * Adds a listener for storage changes.\n *\n * The listener will be called whenever the storage value changes,\n * either locally or from other tabs/windows.\n *\n * @param listener - The event handler to be called when storage changes\n * @returns A function that can be called to remove the listener\n *\n * @example\n * ```typescript\n * const storage = new KeyStorage<string>({ key: 'userName' });\n * const removeListener = storage.addListener({\n * name: 'userNameChange',\n * handle: (event) => {\n * console.log('User name changed:', event.newValue);\n * }\n * });\n *\n * // Later, to remove the listener\n * removeListener();\n * ```\n */\n addListener(\n listener: EventHandler<StorageEvent<Deserialized>>,\n ): RemoveStorageListener {\n this.eventBus.on(listener);\n return () => this.eventBus.off(listener.name);\n }\n\n /**\n * Retrieves the current value from storage.\n *\n * Uses caching to avoid repeated deserialization. If the value is not in cache,\n * it retrieves it from the underlying storage and deserializes it.\n *\n * @returns The deserialized value, or null if no value exists in storage\n *\n * @example\n * ```typescript\n * const storage = new KeyStorage<string>({ key: 'userName' });\n * const userName = storage.get();\n * console.log(userName); // 'John Doe' or null\n * ```\n */\n get(): Deserialized | null {\n if (this.cacheValue) {\n return this.cacheValue;\n }\n const value = this.storage.getItem(this.key);\n if (!value) {\n return null;\n }\n this.cacheValue = this.serializer.deserialize(value);\n return this.cacheValue;\n }\n\n /**\n * Stores a value in storage and notifies all listeners.\n *\n * Serializes the value, stores it in the underlying storage, updates the cache,\n * and emits a change event to all registered listeners.\n *\n * @param value - The value to store (will be serialized before storage)\n *\n * @example\n * ```typescript\n * const storage = new KeyStorage<string>({ key: 'userName' });\n * storage.set('John Doe');\n * ```\n */\n set(value: Deserialized): void {\n const oldValue = this.get();\n const serialized = this.serializer.serialize(value);\n this.storage.setItem(this.key, serialized);\n this.cacheValue = value;\n this.eventBus.emit({\n newValue: value,\n oldValue: oldValue,\n });\n }\n\n /**\n * Removes the value from storage and notifies all listeners.\n *\n * Removes the item from the underlying storage, clears the cache,\n * and emits a change event indicating the value was removed.\n *\n * @example\n * ```typescript\n * const storage = new KeyStorage<string>({ key: 'userName' });\n * storage.remove(); // Removes the stored value\n * ```\n */\n remove(): void {\n const oldValue = this.get();\n this.storage.removeItem(this.key);\n this.cacheValue = null;\n this.eventBus.emit({\n oldValue: oldValue,\n newValue: null,\n });\n }\n\n /**\n * Cleans up resources used by the KeyStorage instance.\n *\n * Removes the internal event handler from the event bus.\n * Should be called when the KeyStorage instance is no longer needed\n * to prevent memory leaks.\n *\n * @example\n * ```typescript\n * const storage = new KeyStorage<string>({ key: 'userName' });\n * // ... use storage ...\n * storage.destroy(); // Clean up resources\n * ```\n */\n destroy() {\n this.eventBus.off(this.keyStorageHandler.name);\n }\n}\n"],"names":["InMemoryStorage","key","value","index","isBrowser","getStorage","JsonSerializer","IdentitySerializer","jsonSerializer","identitySerializer","typedIdentitySerializer","KeyStorage","options","nameGenerator","event","SerialTypedEventBus","listener","oldValue","serialized"],"mappings":"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"}
|
package/dist/index.umd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.umd.js","sources":["../src/inMemoryStorage.ts","../src/env.ts","../src/serializer.ts","../src/keyStorage.ts"],"sourcesContent":["/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport class InMemoryStorage implements Storage {\n private readonly store: Map<string, string> = new Map();\n\n /**\n * Gets the number of items stored in the storage.\n */\n get length(): number {\n return this.store.size;\n }\n\n /**\n * Clears all items from the storage.\n */\n clear(): void {\n this.store.clear();\n }\n\n /**\n * Gets an item from the storage.\n * @param key - The key of the item to retrieve\n * @returns The value of the item, or null if the item does not exist\n */\n getItem(key: string): string | null {\n const value = this.store.get(key);\n return value !== undefined ? value : null;\n }\n\n /**\n * Gets the key at the specified index.\n * @param index - The index of the key to retrieve\n * @returns The key at the specified index, or null if the index is out of bounds\n */\n key(index: number): string | null {\n const keys = Array.from(this.store.keys());\n return keys[index] || null;\n }\n\n /**\n * Removes an item from the storage.\n * @param key - The key of the item to remove\n */\n removeItem(key: string): void {\n this.store.delete(key);\n }\n\n /**\n * Sets an item in the storage.\n * @param key - The key of the item to set\n * @param value - The value to set\n */\n setItem(key: string, value: string): void {\n this.store.set(key, value);\n }\n}","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { InMemoryStorage } from './inMemoryStorage';\n\n/**\n * Checks if the current environment is a browser.\n * @returns True if running in a browser environment, false otherwise\n */\nexport function isBrowser(): boolean {\n return typeof window !== 'undefined';\n}\n\n/**\n * Gets the appropriate storage implementation based on the environment.\n * Returns localStorage in browser environments if available, InMemoryStorage otherwise.\n * @returns A Storage-compatible object\n */\nexport const getStorage = (): Storage => {\n if (isBrowser()) {\n return window.localStorage;\n }\n return new InMemoryStorage();\n};\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Interface for serializing and deserializing values\n * @template Serialized The type of the serialized value\n * @template Deserialized The type of the deserialized value\n */\nexport interface Serializer<Serialized, Deserialized> {\n /**\n * Serializes a value to the specified format\n * @param value The value to serialize\n * @returns The serialized value\n */\n serialize(value: any): Serialized;\n\n /**\n * Deserializes a value from the specified format\n * @param value The value to deserialize\n * @returns The deserialized value\n */\n deserialize(value: Serialized): Deserialized;\n}\n\n/**\n * Implementation of Serializer that uses JSON for serialization\n */\nexport class JsonSerializer implements Serializer<string, any> {\n /**\n * Serializes a value to a JSON string\n * @param value The value to serialize\n * @returns The JSON string representation of the value\n */\n serialize(value: any): string {\n return JSON.stringify(value);\n }\n\n /**\n * Deserializes a JSON string to a value\n * @param value The JSON string to deserialize\n * @returns The deserialized value\n */\n deserialize(value: string): any {\n return JSON.parse(value);\n }\n}\n\n/**\n * Implementation of Serializer that performs no actual serialization\n * @template T The type of the value to pass through\n */\nexport class IdentitySerializer<T> implements Serializer<T, T> {\n /**\n * Returns the value as-is without serialization\n * @param value The value to pass through\n * @returns The same value that was passed in\n */\n serialize(value: T): T {\n return value;\n }\n\n /**\n * Returns the value as-is without deserialization\n * @param value The value to pass through\n * @returns The same value that was passed in\n */\n deserialize(value: T): T {\n return value;\n }\n}\n\n/**\n * Global instance of JsonSerializer\n */\nexport const jsonSerializer = new JsonSerializer();\n/**\n * Global instance of IdentitySerializer\n */\nexport const identitySerializer = new IdentitySerializer<any>();\n\nexport function typedIdentitySerializer<T>(): IdentitySerializer<T> {\n return identitySerializer as IdentitySerializer<T>;\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Serializer, typedIdentitySerializer } from './serializer';\nimport {\n EventHandler, nameGenerator,\n SerialTypedEventBus,\n TypedEventBus,\n} from '@ahoo-wang/fetcher-eventbus';\nimport { getStorage } from './env';\n\nexport interface StorageEvent<Deserialized> {\n newValue?: Deserialized | null;\n oldValue?: Deserialized | null;\n}\n\n/**\n * A function that removes a storage listener when called.\n */\nexport type RemoveStorageListener = () => void;\n\nexport interface StorageListenable<Deserialized> {\n /**\n * Adds a listener for storage changes.\n * @param listener - The listener function to be called when storage changes\n * @returns A function that can be called to remove the listener\n */\n addListener(\n listener: EventHandler<StorageEvent<Deserialized>>,\n ): RemoveStorageListener;\n}\n\n/**\n * Options for configuring KeyStorage\n */\nexport interface KeyStorageOptions<Deserialized> {\n /**\n * The key used to store and retrieve values from storage\n */\n key: string;\n\n /**\n * Optional serializer for converting values to and from storage format\n * Defaults to IdentitySerializer if not provided\n */\n serializer?: Serializer<string, Deserialized>;\n\n /**\n * Optional storage instance. Defaults to localStorage\n */\n storage?: Storage;\n\n /**\n * Optional event bus for cross-tab communication. Defaults to SerialTypedEventBus\n */\n eventBus?: TypedEventBus<StorageEvent<Deserialized>>;\n}\n\n/**\n * A storage wrapper that manages a single value associated with a specific key\n * Provides caching and automatic cache invalidation when the storage value changes\n * @template Deserialized The type of the value being stored\n */\nexport class KeyStorage<Deserialized>\n implements StorageListenable<Deserialized> {\n private readonly key: string;\n private readonly serializer: Serializer<string, Deserialized>;\n private readonly storage: Storage;\n private readonly eventBus: TypedEventBus<StorageEvent<Deserialized>>;\n private cacheValue: Deserialized | null = null;\n private readonly keyStorageHandler: EventHandler<StorageEvent<Deserialized>> = {\n name: nameGenerator.generate('KeyStorage'),\n handle: (event: StorageEvent<Deserialized>) => {\n this.cacheValue = event.newValue ?? null;\n },\n };\n\n /**\n * Creates a new KeyStorage instance\n * @param options Configuration options for the storage\n */\n constructor(options: KeyStorageOptions<Deserialized>) {\n this.key = options.key;\n this.serializer = options.serializer ?? typedIdentitySerializer();\n this.storage = options.storage ?? getStorage();\n this.eventBus =\n options.eventBus ??\n new SerialTypedEventBus<StorageEvent<Deserialized>>(\n `KeyStorage:${this.key}`,\n );\n this.eventBus.on(this.keyStorageHandler);\n }\n\n addListener(\n listener: EventHandler<StorageEvent<Deserialized>>,\n ): RemoveStorageListener {\n this.eventBus.on(listener);\n return () => this.eventBus.off(listener.name);\n }\n\n get(): Deserialized | null {\n if (this.cacheValue) {\n return this.cacheValue;\n }\n const value = this.storage.getItem(this.key);\n if (!value) {\n return null;\n }\n this.cacheValue = this.serializer.deserialize(value);\n return this.cacheValue;\n }\n\n set(value: Deserialized): void {\n const oldValue = this.get();\n const serialized = this.serializer.serialize(value);\n this.storage.setItem(this.key, serialized);\n this.cacheValue = value;\n this.eventBus.emit({\n newValue: value,\n oldValue: oldValue,\n });\n }\n\n remove(): void {\n const oldValue = this.get();\n this.storage.removeItem(this.key);\n this.cacheValue = null;\n this.eventBus.emit({\n oldValue: oldValue,\n newValue: null,\n });\n }\n\n destroy() {\n this.eventBus.off(this.keyStorageHandler.name);\n }\n}\n"],"names":["InMemoryStorage","key","value","index","isBrowser","getStorage","JsonSerializer","IdentitySerializer","jsonSerializer","identitySerializer","typedIdentitySerializer","KeyStorage","options","nameGenerator","event","SerialTypedEventBus","listener","oldValue","serialized"],"mappings":"yTAaO,MAAMA,CAAmC,CAAzC,aAAA,CACL,KAAiB,UAAiC,GAAI,CAKtD,IAAI,QAAiB,CACnB,OAAO,KAAK,MAAM,IACpB,CAKA,OAAc,CACZ,KAAK,MAAM,MAAA,CACb,CAOA,QAAQC,EAA4B,CAClC,MAAMC,EAAQ,KAAK,MAAM,IAAID,CAAG,EAChC,OAAOC,IAAU,OAAYA,EAAQ,IACvC,CAOA,IAAIC,EAA8B,CAEhC,OADa,MAAM,KAAK,KAAK,MAAM,MAAM,EAC7BA,CAAK,GAAK,IACxB,CAMA,WAAWF,EAAmB,CAC5B,KAAK,MAAM,OAAOA,CAAG,CACvB,CAOA,QAAQA,EAAaC,EAAqB,CACxC,KAAK,MAAM,IAAID,EAAKC,CAAK,CAC3B,CACF,CC/CO,SAASE,GAAqB,CACnC,OAAO,OAAO,OAAW,GAC3B,CAOO,MAAMC,EAAa,IACpBD,IACK,OAAO,aAET,IAAIJ,ECKN,MAAMM,CAAkD,CAM7D,UAAUJ,EAAoB,CAC5B,OAAO,KAAK,UAAUA,CAAK,CAC7B,CAOA,YAAYA,EAAoB,CAC9B,OAAO,KAAK,MAAMA,CAAK,CACzB,CACF,CAMO,MAAMK,CAAkD,CAM7D,UAAUL,EAAa,CACrB,OAAOA,CACT,CAOA,YAAYA,EAAa,CACvB,OAAOA,CACT,CACF,CAKO,MAAMM,EAAiB,IAAIF,EAIrBG,EAAqB,IAAIF,EAE/B,SAASG,GAAoD,CAClE,OAAOD,CACT,CCnBO,MAAME,CACgC,CAiB3C,YAAYC,EAA0C,CAZtD,KAAQ,WAAkC,KAC1C,KAAiB,kBAA8D,CAC7E,KAAMC,EAAAA,cAAc,SAAS,YAAY,EACzC,OAASC,GAAsC,CAC7C,KAAK,WAAaA,EAAM,UAAY,IACtC,CAAA,EAQA,KAAK,IAAMF,EAAQ,IACnB,KAAK,WAAaA,EAAQ,YAAcF,EAAA,EACxC,KAAK,QAAUE,EAAQ,SAAWP,EAAA,EAClC,KAAK,SACHO,EAAQ,UACR,IAAIG,EAAAA,oBACF,cAAc,KAAK,GAAG,EAAA,EAE1B,KAAK,SAAS,GAAG,KAAK,iBAAiB,CACzC,CAEA,YACEC,EACuB,CACvB,YAAK,SAAS,GAAGA,CAAQ,EAClB,IAAM,KAAK,SAAS,IAAIA,EAAS,IAAI,CAC9C,CAEA,KAA2B,CACzB,GAAI,KAAK,WACP,OAAO,KAAK,WAEd,MAAMd,EAAQ,KAAK,QAAQ,QAAQ,KAAK,GAAG,EAC3C,OAAKA,GAGL,KAAK,WAAa,KAAK,WAAW,YAAYA,CAAK,EAC5C,KAAK,YAHH,IAIX,CAEA,IAAIA,EAA2B,CAC7B,MAAMe,EAAW,KAAK,IAAA,EAChBC,EAAa,KAAK,WAAW,UAAUhB,CAAK,EAClD,KAAK,QAAQ,QAAQ,KAAK,IAAKgB,CAAU,EACzC,KAAK,WAAahB,EAClB,KAAK,SAAS,KAAK,CACjB,SAAUA,EACV,SAAAe,CAAA,CACD,CACH,CAEA,QAAe,CACb,MAAMA,EAAW,KAAK,IAAA,EACtB,KAAK,QAAQ,WAAW,KAAK,GAAG,EAChC,KAAK,WAAa,KAClB,KAAK,SAAS,KAAK,CACjB,SAAAA,EACA,SAAU,IAAA,CACX,CACH,CAEA,SAAU,CACR,KAAK,SAAS,IAAI,KAAK,kBAAkB,IAAI,CAC/C,CACF"}
|
|
1
|
+
{"version":3,"file":"index.umd.js","sources":["../src/inMemoryStorage.ts","../src/env.ts","../src/serializer.ts","../src/keyStorage.ts"],"sourcesContent":["/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport class InMemoryStorage implements Storage {\n private readonly store: Map<string, string> = new Map();\n\n /**\n * Gets the number of items stored in the storage.\n */\n get length(): number {\n return this.store.size;\n }\n\n /**\n * Clears all items from the storage.\n */\n clear(): void {\n this.store.clear();\n }\n\n /**\n * Gets an item from the storage.\n * @param key - The key of the item to retrieve\n * @returns The value of the item, or null if the item does not exist\n */\n getItem(key: string): string | null {\n const value = this.store.get(key);\n return value !== undefined ? value : null;\n }\n\n /**\n * Gets the key at the specified index.\n * @param index - The index of the key to retrieve\n * @returns The key at the specified index, or null if the index is out of bounds\n */\n key(index: number): string | null {\n const keys = Array.from(this.store.keys());\n return keys[index] || null;\n }\n\n /**\n * Removes an item from the storage.\n * @param key - The key of the item to remove\n */\n removeItem(key: string): void {\n this.store.delete(key);\n }\n\n /**\n * Sets an item in the storage.\n * @param key - The key of the item to set\n * @param value - The value to set\n */\n setItem(key: string, value: string): void {\n this.store.set(key, value);\n }\n}","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { InMemoryStorage } from './inMemoryStorage';\n\n/**\n * Checks if the current environment is a browser.\n * @returns True if running in a browser environment, false otherwise\n */\nexport function isBrowser(): boolean {\n return typeof window !== 'undefined';\n}\n\n/**\n * Gets the appropriate storage implementation based on the environment.\n * Returns localStorage in browser environments if available, InMemoryStorage otherwise.\n * @returns A Storage-compatible object\n */\nexport const getStorage = (): Storage => {\n if (isBrowser()) {\n return window.localStorage;\n }\n return new InMemoryStorage();\n};\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Interface for serializing and deserializing values\n * @template Serialized The type of the serialized value\n * @template Deserialized The type of the deserialized value\n */\nexport interface Serializer<Serialized, Deserialized> {\n /**\n * Serializes a value to the specified format\n * @param value The value to serialize\n * @returns The serialized value\n */\n serialize(value: any): Serialized;\n\n /**\n * Deserializes a value from the specified format\n * @param value The value to deserialize\n * @returns The deserialized value\n */\n deserialize(value: Serialized): Deserialized;\n}\n\n/**\n * Implementation of Serializer that uses JSON for serialization\n */\nexport class JsonSerializer implements Serializer<string, any> {\n /**\n * Serializes a value to a JSON string\n * @param value The value to serialize\n * @returns The JSON string representation of the value\n */\n serialize(value: any): string {\n return JSON.stringify(value);\n }\n\n /**\n * Deserializes a JSON string to a value\n * @param value The JSON string to deserialize\n * @returns The deserialized value\n */\n deserialize(value: string): any {\n return JSON.parse(value);\n }\n}\n\n/**\n * Implementation of Serializer that performs no actual serialization\n * @template T The type of the value to pass through\n */\nexport class IdentitySerializer<T> implements Serializer<T, T> {\n /**\n * Returns the value as-is without serialization\n * @param value The value to pass through\n * @returns The same value that was passed in\n */\n serialize(value: T): T {\n return value;\n }\n\n /**\n * Returns the value as-is without deserialization\n * @param value The value to pass through\n * @returns The same value that was passed in\n */\n deserialize(value: T): T {\n return value;\n }\n}\n\n/**\n * Global instance of JsonSerializer\n */\nexport const jsonSerializer = new JsonSerializer();\n/**\n * Global instance of IdentitySerializer\n */\nexport const identitySerializer = new IdentitySerializer<any>();\n\nexport function typedIdentitySerializer<T>(): IdentitySerializer<T> {\n return identitySerializer as IdentitySerializer<T>;\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Serializer, typedIdentitySerializer } from './serializer';\nimport {\n EventHandler,\n nameGenerator,\n SerialTypedEventBus,\n TypedEventBus,\n} from '@ahoo-wang/fetcher-eventbus';\nimport { getStorage } from './env';\n\nexport interface StorageEvent<Deserialized> {\n newValue?: Deserialized | null;\n oldValue?: Deserialized | null;\n}\n\n/**\n * A function that removes a storage listener when called.\n */\nexport type RemoveStorageListener = () => void;\n\nexport interface StorageListenable<Deserialized> {\n /**\n * Adds a listener for storage changes.\n * @param listener - The listener function to be called when storage changes\n * @returns A function that can be called to remove the listener\n */\n addListener(\n listener: EventHandler<StorageEvent<Deserialized>>,\n ): RemoveStorageListener;\n}\n\n/**\n * Options for configuring KeyStorage\n */\nexport interface KeyStorageOptions<Deserialized> {\n /**\n * The key used to store and retrieve values from storage\n */\n key: string;\n\n /**\n * Optional serializer for converting values to and from storage format\n * Defaults to IdentitySerializer if not provided\n */\n serializer?: Serializer<string, Deserialized>;\n\n /**\n * Optional storage instance. Defaults to localStorage\n */\n storage?: Storage;\n\n /**\n * Optional event bus for cross-tab communication. Defaults to SerialTypedEventBus\n */\n eventBus?: TypedEventBus<StorageEvent<Deserialized>>;\n}\n\n/**\n * A storage wrapper that manages a single value associated with a specific key\n * Provides caching and automatic cache invalidation when the storage value changes\n * @template Deserialized The type of the value being stored\n */\nexport class KeyStorage<Deserialized>\n implements StorageListenable<Deserialized>\n{\n private readonly key: string;\n private readonly serializer: Serializer<string, Deserialized>;\n private readonly storage: Storage;\n private readonly eventBus: TypedEventBus<StorageEvent<Deserialized>>;\n private cacheValue: Deserialized | null = null;\n private readonly keyStorageHandler: EventHandler<StorageEvent<Deserialized>> =\n {\n name: nameGenerator.generate('KeyStorage'),\n handle: (event: StorageEvent<Deserialized>) => {\n this.cacheValue = event.newValue ?? null;\n },\n };\n\n /**\n * Creates a new KeyStorage instance\n * @param options Configuration options for the storage\n */\n constructor(options: KeyStorageOptions<Deserialized>) {\n this.key = options.key;\n this.serializer = options.serializer ?? typedIdentitySerializer();\n this.storage = options.storage ?? getStorage();\n this.eventBus =\n options.eventBus ??\n new SerialTypedEventBus<StorageEvent<Deserialized>>(\n `KeyStorage:${this.key}`,\n );\n this.eventBus.on(this.keyStorageHandler);\n }\n\n /**\n * Adds a listener for storage changes.\n *\n * The listener will be called whenever the storage value changes,\n * either locally or from other tabs/windows.\n *\n * @param listener - The event handler to be called when storage changes\n * @returns A function that can be called to remove the listener\n *\n * @example\n * ```typescript\n * const storage = new KeyStorage<string>({ key: 'userName' });\n * const removeListener = storage.addListener({\n * name: 'userNameChange',\n * handle: (event) => {\n * console.log('User name changed:', event.newValue);\n * }\n * });\n *\n * // Later, to remove the listener\n * removeListener();\n * ```\n */\n addListener(\n listener: EventHandler<StorageEvent<Deserialized>>,\n ): RemoveStorageListener {\n this.eventBus.on(listener);\n return () => this.eventBus.off(listener.name);\n }\n\n /**\n * Retrieves the current value from storage.\n *\n * Uses caching to avoid repeated deserialization. If the value is not in cache,\n * it retrieves it from the underlying storage and deserializes it.\n *\n * @returns The deserialized value, or null if no value exists in storage\n *\n * @example\n * ```typescript\n * const storage = new KeyStorage<string>({ key: 'userName' });\n * const userName = storage.get();\n * console.log(userName); // 'John Doe' or null\n * ```\n */\n get(): Deserialized | null {\n if (this.cacheValue) {\n return this.cacheValue;\n }\n const value = this.storage.getItem(this.key);\n if (!value) {\n return null;\n }\n this.cacheValue = this.serializer.deserialize(value);\n return this.cacheValue;\n }\n\n /**\n * Stores a value in storage and notifies all listeners.\n *\n * Serializes the value, stores it in the underlying storage, updates the cache,\n * and emits a change event to all registered listeners.\n *\n * @param value - The value to store (will be serialized before storage)\n *\n * @example\n * ```typescript\n * const storage = new KeyStorage<string>({ key: 'userName' });\n * storage.set('John Doe');\n * ```\n */\n set(value: Deserialized): void {\n const oldValue = this.get();\n const serialized = this.serializer.serialize(value);\n this.storage.setItem(this.key, serialized);\n this.cacheValue = value;\n this.eventBus.emit({\n newValue: value,\n oldValue: oldValue,\n });\n }\n\n /**\n * Removes the value from storage and notifies all listeners.\n *\n * Removes the item from the underlying storage, clears the cache,\n * and emits a change event indicating the value was removed.\n *\n * @example\n * ```typescript\n * const storage = new KeyStorage<string>({ key: 'userName' });\n * storage.remove(); // Removes the stored value\n * ```\n */\n remove(): void {\n const oldValue = this.get();\n this.storage.removeItem(this.key);\n this.cacheValue = null;\n this.eventBus.emit({\n oldValue: oldValue,\n newValue: null,\n });\n }\n\n /**\n * Cleans up resources used by the KeyStorage instance.\n *\n * Removes the internal event handler from the event bus.\n * Should be called when the KeyStorage instance is no longer needed\n * to prevent memory leaks.\n *\n * @example\n * ```typescript\n * const storage = new KeyStorage<string>({ key: 'userName' });\n * // ... use storage ...\n * storage.destroy(); // Clean up resources\n * ```\n */\n destroy() {\n this.eventBus.off(this.keyStorageHandler.name);\n }\n}\n"],"names":["InMemoryStorage","key","value","index","isBrowser","getStorage","JsonSerializer","IdentitySerializer","jsonSerializer","identitySerializer","typedIdentitySerializer","KeyStorage","options","nameGenerator","event","SerialTypedEventBus","listener","oldValue","serialized"],"mappings":"yTAaO,MAAMA,CAAmC,CAAzC,aAAA,CACL,KAAiB,UAAiC,GAAI,CAKtD,IAAI,QAAiB,CACnB,OAAO,KAAK,MAAM,IACpB,CAKA,OAAc,CACZ,KAAK,MAAM,MAAA,CACb,CAOA,QAAQC,EAA4B,CAClC,MAAMC,EAAQ,KAAK,MAAM,IAAID,CAAG,EAChC,OAAOC,IAAU,OAAYA,EAAQ,IACvC,CAOA,IAAIC,EAA8B,CAEhC,OADa,MAAM,KAAK,KAAK,MAAM,MAAM,EAC7BA,CAAK,GAAK,IACxB,CAMA,WAAWF,EAAmB,CAC5B,KAAK,MAAM,OAAOA,CAAG,CACvB,CAOA,QAAQA,EAAaC,EAAqB,CACxC,KAAK,MAAM,IAAID,EAAKC,CAAK,CAC3B,CACF,CC/CO,SAASE,GAAqB,CACnC,OAAO,OAAO,OAAW,GAC3B,CAOO,MAAMC,EAAa,IACpBD,IACK,OAAO,aAET,IAAIJ,ECKN,MAAMM,CAAkD,CAM7D,UAAUJ,EAAoB,CAC5B,OAAO,KAAK,UAAUA,CAAK,CAC7B,CAOA,YAAYA,EAAoB,CAC9B,OAAO,KAAK,MAAMA,CAAK,CACzB,CACF,CAMO,MAAMK,CAAkD,CAM7D,UAAUL,EAAa,CACrB,OAAOA,CACT,CAOA,YAAYA,EAAa,CACvB,OAAOA,CACT,CACF,CAKO,MAAMM,EAAiB,IAAIF,EAIrBG,EAAqB,IAAIF,EAE/B,SAASG,GAAoD,CAClE,OAAOD,CACT,CClBO,MAAME,CAEb,CAkBE,YAAYC,EAA0C,CAbtD,KAAQ,WAAkC,KAC1C,KAAiB,kBACf,CACE,KAAMC,EAAAA,cAAc,SAAS,YAAY,EACzC,OAASC,GAAsC,CAC7C,KAAK,WAAaA,EAAM,UAAY,IACtC,CAAA,EAQF,KAAK,IAAMF,EAAQ,IACnB,KAAK,WAAaA,EAAQ,YAAcF,EAAA,EACxC,KAAK,QAAUE,EAAQ,SAAWP,EAAA,EAClC,KAAK,SACHO,EAAQ,UACR,IAAIG,EAAAA,oBACF,cAAc,KAAK,GAAG,EAAA,EAE1B,KAAK,SAAS,GAAG,KAAK,iBAAiB,CACzC,CAyBA,YACEC,EACuB,CACvB,YAAK,SAAS,GAAGA,CAAQ,EAClB,IAAM,KAAK,SAAS,IAAIA,EAAS,IAAI,CAC9C,CAiBA,KAA2B,CACzB,GAAI,KAAK,WACP,OAAO,KAAK,WAEd,MAAMd,EAAQ,KAAK,QAAQ,QAAQ,KAAK,GAAG,EAC3C,OAAKA,GAGL,KAAK,WAAa,KAAK,WAAW,YAAYA,CAAK,EAC5C,KAAK,YAHH,IAIX,CAgBA,IAAIA,EAA2B,CAC7B,MAAMe,EAAW,KAAK,IAAA,EAChBC,EAAa,KAAK,WAAW,UAAUhB,CAAK,EAClD,KAAK,QAAQ,QAAQ,KAAK,IAAKgB,CAAU,EACzC,KAAK,WAAahB,EAClB,KAAK,SAAS,KAAK,CACjB,SAAUA,EACV,SAAAe,CAAA,CACD,CACH,CAcA,QAAe,CACb,MAAMA,EAAW,KAAK,IAAA,EACtB,KAAK,QAAQ,WAAW,KAAK,GAAG,EAChC,KAAK,WAAa,KAClB,KAAK,SAAS,KAAK,CACjB,SAAAA,EACA,SAAU,IAAA,CACX,CACH,CAgBA,SAAU,CACR,KAAK,SAAS,IAAI,KAAK,kBAAkB,IAAI,CAC/C,CACF"}
|
package/dist/keyStorage.d.ts
CHANGED
|
@@ -55,10 +55,88 @@ export declare class KeyStorage<Deserialized> implements StorageListenable<Deser
|
|
|
55
55
|
* @param options Configuration options for the storage
|
|
56
56
|
*/
|
|
57
57
|
constructor(options: KeyStorageOptions<Deserialized>);
|
|
58
|
+
/**
|
|
59
|
+
* Adds a listener for storage changes.
|
|
60
|
+
*
|
|
61
|
+
* The listener will be called whenever the storage value changes,
|
|
62
|
+
* either locally or from other tabs/windows.
|
|
63
|
+
*
|
|
64
|
+
* @param listener - The event handler to be called when storage changes
|
|
65
|
+
* @returns A function that can be called to remove the listener
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* const storage = new KeyStorage<string>({ key: 'userName' });
|
|
70
|
+
* const removeListener = storage.addListener({
|
|
71
|
+
* name: 'userNameChange',
|
|
72
|
+
* handle: (event) => {
|
|
73
|
+
* console.log('User name changed:', event.newValue);
|
|
74
|
+
* }
|
|
75
|
+
* });
|
|
76
|
+
*
|
|
77
|
+
* // Later, to remove the listener
|
|
78
|
+
* removeListener();
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
58
81
|
addListener(listener: EventHandler<StorageEvent<Deserialized>>): RemoveStorageListener;
|
|
82
|
+
/**
|
|
83
|
+
* Retrieves the current value from storage.
|
|
84
|
+
*
|
|
85
|
+
* Uses caching to avoid repeated deserialization. If the value is not in cache,
|
|
86
|
+
* it retrieves it from the underlying storage and deserializes it.
|
|
87
|
+
*
|
|
88
|
+
* @returns The deserialized value, or null if no value exists in storage
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* const storage = new KeyStorage<string>({ key: 'userName' });
|
|
93
|
+
* const userName = storage.get();
|
|
94
|
+
* console.log(userName); // 'John Doe' or null
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
59
97
|
get(): Deserialized | null;
|
|
98
|
+
/**
|
|
99
|
+
* Stores a value in storage and notifies all listeners.
|
|
100
|
+
*
|
|
101
|
+
* Serializes the value, stores it in the underlying storage, updates the cache,
|
|
102
|
+
* and emits a change event to all registered listeners.
|
|
103
|
+
*
|
|
104
|
+
* @param value - The value to store (will be serialized before storage)
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const storage = new KeyStorage<string>({ key: 'userName' });
|
|
109
|
+
* storage.set('John Doe');
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
60
112
|
set(value: Deserialized): void;
|
|
113
|
+
/**
|
|
114
|
+
* Removes the value from storage and notifies all listeners.
|
|
115
|
+
*
|
|
116
|
+
* Removes the item from the underlying storage, clears the cache,
|
|
117
|
+
* and emits a change event indicating the value was removed.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* const storage = new KeyStorage<string>({ key: 'userName' });
|
|
122
|
+
* storage.remove(); // Removes the stored value
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
61
125
|
remove(): void;
|
|
126
|
+
/**
|
|
127
|
+
* Cleans up resources used by the KeyStorage instance.
|
|
128
|
+
*
|
|
129
|
+
* Removes the internal event handler from the event bus.
|
|
130
|
+
* Should be called when the KeyStorage instance is no longer needed
|
|
131
|
+
* to prevent memory leaks.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* const storage = new KeyStorage<string>({ key: 'userName' });
|
|
136
|
+
* // ... use storage ...
|
|
137
|
+
* storage.destroy(); // Clean up resources
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
62
140
|
destroy(): void;
|
|
63
141
|
}
|
|
64
142
|
//# sourceMappingURL=keyStorage.d.ts.map
|
package/dist/keyStorage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keyStorage.d.ts","sourceRoot":"","sources":["../src/keyStorage.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,UAAU,EAA2B,MAAM,cAAc,CAAC;AACnE,OAAO,EACL,YAAY,
|
|
1
|
+
{"version":3,"file":"keyStorage.d.ts","sourceRoot":"","sources":["../src/keyStorage.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,UAAU,EAA2B,MAAM,cAAc,CAAC;AACnE,OAAO,EACL,YAAY,EAGZ,aAAa,EACd,MAAM,6BAA6B,CAAC;AAGrC,MAAM,WAAW,YAAY,CAAC,YAAY;IACxC,QAAQ,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,IAAI,CAAC;AAE/C,MAAM,WAAW,iBAAiB,CAAC,YAAY;IAC7C;;;;OAIG;IACH,WAAW,CACT,QAAQ,EAAE,YAAY,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC,GACjD,qBAAqB,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB,CAAC,YAAY;IAC7C;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;OAGG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAE9C;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;OAEG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC;CACtD;AAED;;;;GAIG;AACH,qBAAa,UAAU,CAAC,YAAY,CAClC,YAAW,iBAAiB,CAAC,YAAY,CAAC;IAE1C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmC;IAC9D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4C;IACrE,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAM9B;IAEJ;;;OAGG;gBACS,OAAO,EAAE,iBAAiB,CAAC,YAAY,CAAC;IAYpD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,WAAW,CACT,QAAQ,EAAE,YAAY,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC,GACjD,qBAAqB;IAKxB;;;;;;;;;;;;;;OAcG;IACH,GAAG,IAAI,YAAY,GAAG,IAAI;IAY1B;;;;;;;;;;;;;OAaG;IACH,GAAG,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI;IAW9B;;;;;;;;;;;OAWG;IACH,MAAM,IAAI,IAAI;IAUd;;;;;;;;;;;;;OAaG;IACH,OAAO;CAGR"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ahoo-wang/fetcher-storage",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.9",
|
|
4
4
|
"description": "A lightweight, cross-environment storage library with key-based storage and automatic environment detection. Provides consistent API for browser localStorage and in-memory storage with change notifications.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"storage",
|