@ahoo-wang/fetcher-storage 2.9.6 → 2.9.8

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 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
  }
@@ -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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"}
@@ -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"}
@@ -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
@@ -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,EAEZ,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;IAC1C,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,CAKhC;IAEF;;;OAGG;gBACS,OAAO,EAAE,iBAAiB,CAAC,YAAY,CAAC;IAYpD,WAAW,CACT,QAAQ,EAAE,YAAY,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC,GACjD,qBAAqB;IAKxB,GAAG,IAAI,YAAY,GAAG,IAAI;IAY1B,GAAG,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI;IAW9B,MAAM,IAAI,IAAI;IAUd,OAAO;CAGR"}
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.6",
3
+ "version": "2.9.8",
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",