dev-react-microstore 5.0.0 → 6.0.0

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/src/index.ts CHANGED
@@ -8,19 +8,11 @@ type MiddlewareFunction<T extends object> = (
8
8
  next: (modifiedUpdate?: Partial<T>) => void
9
9
  ) => void;
10
10
 
11
- function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
12
- let timeoutId: ReturnType<typeof setTimeout> | undefined;
13
- return ((...args: Parameters<T>) => {
14
- clearTimeout(timeoutId);
15
- timeoutId = setTimeout(() => fn(...args), delay);
16
- }) as T;
17
- }
18
-
19
11
  /**
20
12
  * Creates a new reactive store with fine-grained subscriptions and middleware support.
21
13
  *
22
14
  * @param initialState - The initial state object for the store
23
- * @returns Store object with methods: get, set, subscribe, select, addMiddleware
15
+ * @returns Store object with methods: get, set, subscribe, select, addMiddleware, onChange
24
16
  *
25
17
  * @example
26
18
  * ```ts
@@ -32,20 +24,106 @@ function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
32
24
  export function createStoreState<T extends object>(
33
25
  initialState: T
34
26
  ) {
27
+ const _initialState = { ...initialState };
35
28
  let state = initialState;
36
- const keyListeners = new Map<keyof T, Set<StoreListener>>();
37
- const debouncedNotifiers = new Map<keyof T, () => void>();
29
+ const keyListeners: Record<string, Set<StoreListener> | undefined> = Object.create(null);
38
30
 
39
31
  // Middleware storage
40
32
  const middleware: Array<{ callback: MiddlewareFunction<T>; keys: (keyof T)[] | null }> = [];
41
33
 
34
+ // Per-key equality registry
35
+ const equalityRegistry: Record<string, ((prev: any, next: any) => boolean) | undefined> = Object.create(null);
42
36
 
37
+ // Batching
38
+ let batchedKeys: Set<keyof T> | null = null;
43
39
 
44
40
  const get = () => state;
45
41
 
46
- const set = (update: Partial<T>, debounceDelay: number | boolean = false) => {
42
+ const getKey = <K extends keyof T>(key: K): T[K] => state[key];
43
+
44
+ const notifyKey = (key: keyof T) => {
45
+ if (batchedKeys) {
46
+ batchedKeys.add(key);
47
+ return;
48
+ }
49
+ keyListeners[key as string]?.forEach(listener => listener());
50
+ };
51
+
52
+ const applySingleKey = <K extends keyof T>(key: K, value: T[K]) => {
53
+ if (Object.is(state[key], value)) return;
54
+ const eq = equalityRegistry[key as string];
55
+ if (eq && eq(state[key], value)) return;
56
+ state[key] = value;
57
+ notifyKey(key);
58
+ };
59
+
60
+ const setKey = <K extends keyof T>(key: K, value: T[K]) => {
61
+ if (middleware.length === 0) {
62
+ applySingleKey(key, value);
63
+ return;
64
+ }
65
+ const partial = {} as Partial<T>;
66
+ partial[key] = value;
67
+ set(partial);
68
+ };
69
+
70
+ const merge = <K extends keyof T>(key: K, value: T[K] extends object ? Partial<T[K]> : never): T[K] => {
71
+ return { ...state[key], ...value };
72
+ };
73
+
74
+ const mergeSet = <K extends keyof T>(key: K, value: T[K] extends object ? Partial<T[K]> : never) => {
75
+ if (middleware.length === 0) {
76
+ applySingleKey(key, { ...state[key], ...value } as T[K]);
77
+ return;
78
+ }
79
+ const partial = {} as Partial<T>;
80
+ partial[key] = { ...state[key], ...value } as T[K];
81
+ set(partial);
82
+ };
83
+
84
+ const reset = (keys?: (keyof T)[]) => {
85
+ if (!keys) {
86
+ set({ ..._initialState });
87
+ } else {
88
+ const partial = {} as Partial<T>;
89
+ for (const key of keys) {
90
+ partial[key] = _initialState[key];
91
+ }
92
+ set(partial);
93
+ }
94
+ };
95
+
96
+ const batch = (fn: () => void) => {
97
+ if (batchedKeys) {
98
+ fn();
99
+ return;
100
+ }
101
+ batchedKeys = new Set();
102
+ try {
103
+ fn();
104
+ } finally {
105
+ const keys = batchedKeys;
106
+ batchedKeys = null;
107
+ const notified = new Set<StoreListener>();
108
+ for (const key of keys) {
109
+ keyListeners[key as string]?.forEach(listener => {
110
+ if (!notified.has(listener)) {
111
+ notified.add(listener);
112
+ listener();
113
+ }
114
+ });
115
+ }
116
+ }
117
+ };
118
+
119
+ const set = (update: Partial<T>) => {
47
120
  if (!update) return;
48
121
 
122
+ if (middleware.length === 0) {
123
+ applyUpdate(update);
124
+ return;
125
+ }
126
+
49
127
  // Run middleware chain
50
128
  let currentUpdate = update;
51
129
  let middlewareIndex = 0;
@@ -59,7 +137,7 @@ export function createStoreState<T extends object>(
59
137
  if (middlewareIndex >= middleware.length) {
60
138
  // All middleware processed, apply the update
61
139
  if (!blocked) {
62
- applyUpdate(currentUpdate, debounceDelay);
140
+ applyUpdate(currentUpdate);
63
141
  }
64
142
  return;
65
143
  }
@@ -98,34 +176,41 @@ export function createStoreState<T extends object>(
98
176
  runMiddleware();
99
177
  };
100
178
 
101
- const applyUpdate = (processedUpdate: Partial<T>, debounceDelay: number | boolean) => {
102
- const updatedKeys: (keyof T)[] = [];
179
+ const applyUpdate = (processedUpdate: Partial<T>) => {
180
+ const changedKeys: string[] = [];
103
181
 
182
+ // Phase 1: update all state before notifying
104
183
  for (const key in processedUpdate) {
105
184
  const typedKey = key as keyof T;
106
- const currentValue = state[typedKey];
107
- const nextValue = processedUpdate[typedKey];
185
+ if (Object.is(state[typedKey], processedUpdate[typedKey])) continue;
186
+ const eq = equalityRegistry[key];
187
+ if (eq && eq(state[typedKey], processedUpdate[typedKey])) continue;
188
+ state[typedKey] = processedUpdate[typedKey]!;
189
+ changedKeys.push(key);
190
+ }
108
191
 
109
- if (currentValue === nextValue) continue;
192
+ if (changedKeys.length === 0) return;
110
193
 
111
- if (!Object.is(currentValue, nextValue)) {
112
- state[typedKey] = nextValue!;
113
- updatedKeys.push(typedKey);
194
+ // Phase 2: notify after state is fully consistent
195
+ if (batchedKeys) {
196
+ for (const key of changedKeys) {
197
+ batchedKeys.add(key as keyof T);
114
198
  }
199
+ return;
115
200
  }
116
201
 
117
- if (updatedKeys.length === 0) return;
118
-
119
- for (const key of updatedKeys) {
120
- if (debounceDelay !== false) {
121
- if (!debouncedNotifiers.has(key)) {
122
- debouncedNotifiers.set(key, debounce(() => {
123
- keyListeners.get(key)?.forEach(listener => listener());
124
- }, typeof debounceDelay === 'number' ? debounceDelay : 0));
125
- }
126
- debouncedNotifiers.get(key)!();
127
- } else {
128
- keyListeners.get(key)?.forEach(listener => listener());
202
+ // Synchronous: deduplicate listeners across keys
203
+ if (changedKeys.length === 1) {
204
+ keyListeners[changedKeys[0]]?.forEach(listener => listener());
205
+ } else {
206
+ const notified = new Set<StoreListener>();
207
+ for (const key of changedKeys) {
208
+ keyListeners[key]?.forEach(listener => {
209
+ if (!notified.has(listener)) {
210
+ notified.add(listener);
211
+ listener();
212
+ }
213
+ });
129
214
  }
130
215
  }
131
216
  };
@@ -155,19 +240,16 @@ export function createStoreState<T extends object>(
155
240
  };
156
241
  };
157
242
 
158
-
159
-
160
243
  const subscribe = (keys: (keyof T)[], listener: StoreListener): (() => void) => {
161
244
  for (const key of keys) {
162
- if (!keyListeners.has(key)) {
163
- keyListeners.set(key, new Set());
164
- }
165
- keyListeners.get(key)!.add(listener);
245
+ const k = key as string;
246
+ if (!keyListeners[k]) keyListeners[k] = new Set();
247
+ keyListeners[k]!.add(listener);
166
248
  }
167
249
 
168
250
  return () => {
169
251
  for (const key of keys) {
170
- keyListeners.get(key)?.delete(listener);
252
+ keyListeners[key as string]?.delete(listener);
171
253
  }
172
254
  };
173
255
  };
@@ -181,7 +263,52 @@ export function createStoreState<T extends object>(
181
263
  return result;
182
264
  };
183
265
 
184
- return { get, set, subscribe, select, addMiddleware };
266
+ const onChange = <K extends keyof T>(
267
+ keys: K[],
268
+ callback: (values: Pick<T, K>, prev: Pick<T, K>) => void
269
+ ): (() => void) => {
270
+ let prev = {} as Pick<T, K>;
271
+ for (const key of keys) {
272
+ prev[key] = state[key];
273
+ }
274
+
275
+ let scheduled = false;
276
+
277
+ const listener = () => {
278
+ if (scheduled) return;
279
+ scheduled = true;
280
+ queueMicrotask(() => {
281
+ scheduled = false;
282
+ let hasChanges = false;
283
+ for (const key of keys) {
284
+ if (!Object.is(state[key], prev[key])) {
285
+ hasChanges = true;
286
+ break;
287
+ }
288
+ }
289
+ if (!hasChanges) return;
290
+ const next = {} as Pick<T, K>;
291
+ for (const key of keys) {
292
+ next[key] = state[key];
293
+ }
294
+ const snapshot = prev;
295
+ prev = next;
296
+ callback(next, snapshot);
297
+ });
298
+ };
299
+
300
+ return subscribe(keys, listener);
301
+ };
302
+
303
+ const skipSetWhen = <K extends keyof T>(key: K, fn: (prev: T[K], next: T[K]) => boolean) => {
304
+ equalityRegistry[key as string] = fn;
305
+ };
306
+
307
+ const removeSkipSetWhen = (key: keyof T) => {
308
+ delete equalityRegistry[key as string];
309
+ };
310
+
311
+ return { get, getKey, set, setKey, merge, mergeSet, reset, batch, subscribe, select, addMiddleware, onChange, skipSetWhen, removeSkipSetWhen, _eqReg: equalityRegistry };
185
312
  }
186
313
 
187
314
  type StoreType<T extends object> = ReturnType<typeof createStoreState<T>>;
@@ -237,28 +364,31 @@ export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
237
364
  store: StoreType<T>,
238
365
  selector: S
239
366
  ): Picked<T, S> {
240
- const lastSelected = useRef<Partial<T>>({});
241
- const prevSelector = useRef<SelectorInput<T> | null>(null);
242
- const normalizedRef = useRef<NormalizedSelector<T>[] | null>(null);
243
- const keysRef = useRef<(keyof T)[] | null>(null);
244
- const isFirstRunRef = useRef(true);
245
- const lastValues = useRef<Partial<T>>({});
246
- const subscribeRef = useRef<((onStoreChange: () => void) => () => void) | null>(null);
247
- const storeRef = useRef(store);
248
-
249
- const storeChanged = storeRef.current !== store;
367
+ const ref = useRef({
368
+ lastSelected: {} as Partial<T>,
369
+ prevSelector: null as SelectorInput<T> | null,
370
+ normalized: null as NormalizedSelector<T>[] | null,
371
+ keys: null as (keyof T)[] | null,
372
+ isFirstRun: true,
373
+ lastValues: {} as Partial<T>,
374
+ subscribe: null as ((onStoreChange: () => void) => () => void) | null,
375
+ store,
376
+ });
377
+
378
+ const r = ref.current;
379
+ const storeChanged = r.store !== store;
250
380
  if (storeChanged) {
251
- storeRef.current = store;
252
- lastSelected.current = {};
253
- prevSelector.current = null;
254
- normalizedRef.current = null;
255
- keysRef.current = null;
256
- isFirstRunRef.current = true;
257
- lastValues.current = {};
258
- subscribeRef.current = null;
381
+ r.store = store;
382
+ r.lastSelected = {};
383
+ r.prevSelector = null;
384
+ r.normalized = null;
385
+ r.keys = null;
386
+ r.isFirstRun = true;
387
+ r.lastValues = {};
388
+ r.subscribe = null;
259
389
  }
260
390
 
261
- if (!prevSelector.current || !shallowEqualSelector(prevSelector.current, selector)) {
391
+ if (!r.prevSelector || !shallowEqualSelector(r.prevSelector, selector)) {
262
392
  const normalized: NormalizedSelector<T>[] = [];
263
393
  const keys: (keyof T)[] = [];
264
394
 
@@ -278,63 +408,54 @@ export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
278
408
  }
279
409
  }
280
410
 
281
- normalizedRef.current = normalized;
282
- keysRef.current = keys;
283
- prevSelector.current = selector;
284
- subscribeRef.current = null;
411
+ r.normalized = normalized;
412
+ r.keys = keys;
413
+ r.prevSelector = selector;
414
+ r.subscribe = null;
285
415
  }
286
416
 
287
- const normalized = normalizedRef.current!;
288
- const keys = keysRef.current!;
417
+ const normalized = r.normalized!;
418
+ const keys = r.keys!;
289
419
 
290
420
  const getSnapshot = () => {
291
421
  const current = store.get();
292
- const isFirstRun = isFirstRunRef.current;
293
422
 
294
- if (isFirstRun) {
295
- isFirstRunRef.current = false;
423
+ if (r.isFirstRun) {
424
+ r.isFirstRun = false;
296
425
  const result = {} as Partial<T>;
297
426
  for (const { key } of normalized) {
298
427
  const value = current[key];
299
- lastValues.current[key] = value;
428
+ r.lastValues[key] = value;
300
429
  result[key] = value;
301
430
  }
302
- lastSelected.current = result;
431
+ r.lastSelected = result;
303
432
  return result as Picked<T, S>;
304
433
  }
305
434
 
306
- const hasChanges = () => {
307
- for (const { key, compare } of normalized) {
308
- const prevVal = lastValues.current[key];
309
- const nextVal = current[key];
310
- if (prevVal === undefined ? true : (compare ? !compare(prevVal, nextVal) : !Object.is(prevVal, nextVal))) {
311
- return true;
435
+ // Single pass lazy allocation on first change
436
+ let result: Partial<T> | null = null;
437
+ for (let i = 0; i < normalized.length; i++) {
438
+ const { key, compare } = normalized[i];
439
+ const prevVal = r.lastValues[key]!;
440
+ const nextVal = current[key];
441
+ if (!Object.is(prevVal, nextVal)) {
442
+ const cmp = compare || store._eqReg[key as string];
443
+ if (!cmp || !cmp(prevVal, nextVal)) {
444
+ if (!result) {
445
+ result = {} as Partial<T>;
446
+ for (let j = 0; j < i; j++) result[normalized[j].key] = r.lastValues[normalized[j].key];
447
+ }
448
+ r.lastValues[key] = nextVal;
449
+ result[key] = nextVal;
450
+ continue;
312
451
  }
313
452
  }
314
- return false;
315
- };
316
-
317
- if (!hasChanges()) {
318
- return lastSelected.current as Picked<T, S>;
453
+ if (result) result[key] = prevVal;
319
454
  }
320
455
 
321
- const result = {} as Partial<T>;
322
- for (const { key, compare } of normalized) {
323
- const prevVal = lastValues.current[key];
324
- const nextVal = current[key];
456
+ if (!result) return r.lastSelected as Picked<T, S>;
325
457
 
326
- const isFirstTime = prevVal === undefined;
327
- const changed = isFirstTime || (compare ? !compare(prevVal, nextVal) : !Object.is(prevVal, nextVal));
328
-
329
- if (changed) {
330
- lastValues.current[key] = nextVal;
331
- result[key] = nextVal;
332
- } else {
333
- result[key] = prevVal;
334
- }
335
- }
336
-
337
- lastSelected.current = result;
458
+ r.lastSelected = result;
338
459
  return result as Picked<T, S>;
339
460
  };
340
461
 
@@ -347,29 +468,66 @@ export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
347
468
  return result as Picked<T, S>;
348
469
  }, [keys]);
349
470
 
350
- if (!subscribeRef.current || storeChanged) {
351
- subscribeRef.current = (onStoreChange: () => void) =>
471
+ if (!r.subscribe || storeChanged) {
472
+ r.subscribe = (onStoreChange: () => void) =>
352
473
  store.subscribe(keys, onStoreChange);
353
474
  }
354
475
 
355
- return useSyncExternalStore(subscribeRef.current, getSnapshot, () => staticSnapshot);
476
+ return useSyncExternalStore(r.subscribe, getSnapshot, () => staticSnapshot);
356
477
  }
357
478
 
479
+ /**
480
+ * Creates a pre-bound selector hook for a specific store instance.
481
+ * Infers the state type from the store — no manual generics needed.
482
+ *
483
+ * @param store - The store created with createStoreState
484
+ * @returns A React hook with the same API as useStoreSelector, but with the store already bound
485
+ *
486
+ * @example
487
+ * ```ts
488
+ * const useMyStore = createSelectorHook(myStore);
489
+ *
490
+ * // In a component:
491
+ * const { count, name } = useMyStore(['count', 'name']);
492
+ * ```
493
+ */
494
+ export function createSelectorHook<T extends object>(store: StoreType<T>) {
495
+ return function <S extends SelectorInput<T>>(selector: S): Picked<T, S> {
496
+ return useStoreSelector(store, selector);
497
+ };
498
+ }
358
499
 
359
500
  /**
360
- * Interface for storage objects compatible with persistence middleware.
361
- * Includes localStorage, sessionStorage, AsyncStorage, or any custom storage.
501
+ * Interface for synchronous storage (localStorage, sessionStorage, etc.).
362
502
  */
363
503
  export interface StorageSupportingInterface {
364
504
  getItem(key: string): string | null;
365
505
  setItem(key: string, value: string): void;
366
506
  }
367
507
 
508
+ /**
509
+ * Interface for asynchronous storage (React Native AsyncStorage, etc.).
510
+ */
511
+ export interface AsyncStorageSupportingInterface {
512
+ getItem(key: string): Promise<string | null>;
513
+ setItem(key: string, value: string): Promise<void>;
514
+ }
515
+
516
+ type AnyStorage = Storage | StorageSupportingInterface | AsyncStorageSupportingInterface;
517
+
518
+ function isThenable(value: unknown): value is Promise<unknown> {
519
+ return value != null && typeof (value as any).then === 'function';
520
+ }
521
+
368
522
  /**
369
523
  * Creates a persistence middleware that saves individual keys to storage.
370
524
  * Only writes when the specified keys actually change, using per-key storage.
371
525
  * Storage format: `${persistKey}:${keyName}` for each persisted key.
372
526
  *
527
+ * Works with both synchronous storage (localStorage) and asynchronous storage
528
+ * (React Native AsyncStorage). Async writes are fire-and-forget — the state
529
+ * update is never blocked by a slow write.
530
+ *
373
531
  * @param storage - Storage interface (localStorage, sessionStorage, AsyncStorage, etc.)
374
532
  * @param persistKey - Base key prefix for storage (e.g., 'myapp' creates 'myapp:theme')
375
533
  * @param keys - Array of state keys to persist
@@ -377,30 +535,38 @@ export interface StorageSupportingInterface {
377
535
  *
378
536
  * @example
379
537
  * ```ts
380
- * // Add persistence for theme and user settings
538
+ * // Sync localStorage
381
539
  * store.addMiddleware(
382
540
  * createPersistenceMiddleware(localStorage, 'myapp', ['theme', 'isLoggedIn'])
383
541
  * );
542
+ *
543
+ * // Async — React Native AsyncStorage
544
+ * store.addMiddleware(
545
+ * createPersistenceMiddleware(AsyncStorage, 'myapp', ['theme', 'isLoggedIn'])
546
+ * );
384
547
  * ```
385
548
  */
386
549
  export function createPersistenceMiddleware<T extends object>(
387
- storage: Storage | StorageSupportingInterface,
550
+ storage: AnyStorage,
388
551
  persistKey: string,
389
552
  keys: (keyof T)[]
390
553
  ): [MiddlewareFunction<T>, (keyof T)[]] {
391
554
  const middlewareFunction: MiddlewareFunction<T> = (_, update, next) => {
392
- // Check if any of the persisted keys are being updated
393
555
  const changedKeys = keys.filter(key => key in update);
394
556
  if (changedKeys.length === 0) {
395
557
  return next();
396
558
  }
397
559
 
398
- // Save each changed key individually
399
560
  for (const key of changedKeys) {
400
561
  try {
401
562
  const value = update[key];
402
563
  const storageKey = `${persistKey}:${String(key)}`;
403
- storage.setItem(storageKey, JSON.stringify(value));
564
+ const result = storage.setItem(storageKey, JSON.stringify(value));
565
+ if (isThenable(result)) {
566
+ result.catch(error => {
567
+ console.warn(`Failed to persist key ${String(key)}:`, error);
568
+ });
569
+ }
404
570
  } catch (error) {
405
571
  console.warn(`Failed to persist key ${String(key)}:`, error);
406
572
  }
@@ -416,41 +582,65 @@ export function createPersistenceMiddleware<T extends object>(
416
582
  * Loads persisted state from individual key storage during store initialization.
417
583
  * Reads keys saved by createPersistenceMiddleware and returns them as partial state.
418
584
  *
585
+ * Returns synchronously for sync storage and a Promise for async storage.
586
+ *
419
587
  * @param storage - Storage interface to read from (same as used in middleware)
420
588
  * @param persistKey - Base key prefix used for storage (same as used in middleware)
421
589
  * @param keys - Array of keys to restore (should match middleware keys)
422
- * @returns Partial state object with persisted values, or empty object if loading fails
590
+ * @returns Partial state object (sync) or Promise of partial state (async)
423
591
  *
424
592
  * @example
425
593
  * ```ts
426
- * // Load persisted state before creating store
427
- * const persistedState = loadPersistedState(localStorage, 'myapp', ['theme', 'isLoggedIn']);
594
+ * // Sync localStorage
595
+ * const persisted = loadPersistedState(localStorage, 'myapp', ['theme']);
596
+ * const store = createStoreState({ theme: 'light', ...persisted });
428
597
  *
429
- * const store = createStoreState({
430
- * theme: 'light',
431
- * isLoggedIn: false,
432
- * ...persistedState // Apply persisted values
433
- * });
598
+ * // Async React Native AsyncStorage
599
+ * const persisted = await loadPersistedState(AsyncStorage, 'myapp', ['theme']);
600
+ * const store = createStoreState({ theme: 'light', ...persisted });
434
601
  * ```
435
602
  */
436
603
  export function loadPersistedState<T extends object>(
437
604
  storage: Storage | StorageSupportingInterface,
438
605
  persistKey: string,
439
606
  keys: (keyof T)[]
440
- ): Partial<T> {
607
+ ): Partial<T>;
608
+ export function loadPersistedState<T extends object>(
609
+ storage: AsyncStorageSupportingInterface,
610
+ persistKey: string,
611
+ keys: (keyof T)[]
612
+ ): Promise<Partial<T>>;
613
+ export function loadPersistedState<T extends object>(
614
+ storage: AnyStorage,
615
+ persistKey: string,
616
+ keys: (keyof T)[]
617
+ ): Partial<T> | Promise<Partial<T>> {
441
618
  const result: Partial<T> = {};
442
-
619
+ const pending: Promise<void>[] = [];
620
+
443
621
  for (const key of keys) {
622
+ const storageKey = `${persistKey}:${String(key)}`;
444
623
  try {
445
- const storageKey = `${persistKey}:${String(key)}`;
446
624
  const stored = storage.getItem(storageKey);
447
- if (stored !== null) {
448
- result[key] = JSON.parse(stored);
625
+ if (isThenable(stored)) {
626
+ pending.push(
627
+ stored.then(value => {
628
+ if (value !== null) result[key] = JSON.parse(value as string);
629
+ }).catch(error => {
630
+ console.warn(`Failed to load persisted key ${String(key)}:`, error);
631
+ })
632
+ );
633
+ } else if (stored !== null) {
634
+ result[key] = JSON.parse(stored as string);
449
635
  }
450
636
  } catch (error) {
451
637
  console.warn(`Failed to load persisted key ${String(key)}:`, error);
452
638
  }
453
639
  }
454
-
640
+
641
+ if (pending.length > 0) {
642
+ return Promise.all(pending).then(() => result);
643
+ }
644
+
455
645
  return result;
456
646
  }