dev-react-microstore 4.0.1 → 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
@@ -2,67 +2,254 @@ import { useMemo, useRef, useSyncExternalStore } from 'react'
2
2
 
3
3
  type StoreListener = () => void;
4
4
 
5
- function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
6
- let timeoutId: ReturnType<typeof setTimeout> | undefined;
7
- return ((...args: Parameters<T>) => {
8
- clearTimeout(timeoutId);
9
- timeoutId = setTimeout(() => fn(...args), delay);
10
- }) as T;
11
- }
12
-
5
+ type MiddlewareFunction<T extends object> = (
6
+ currentState: T,
7
+ update: Partial<T>,
8
+ next: (modifiedUpdate?: Partial<T>) => void
9
+ ) => void;
10
+
11
+ /**
12
+ * Creates a new reactive store with fine-grained subscriptions and middleware support.
13
+ *
14
+ * @param initialState - The initial state object for the store
15
+ * @returns Store object with methods: get, set, subscribe, select, addMiddleware, onChange
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const store = createStoreState({ count: 0, name: 'John' });
20
+ * store.set({ count: 1 }); // Update state
21
+ * const { count } = useStoreSelector(store, ['count']); // Subscribe in React
22
+ * ```
23
+ */
13
24
  export function createStoreState<T extends object>(
14
- initialState: T,
25
+ initialState: T
15
26
  ) {
27
+ const _initialState = { ...initialState };
16
28
  let state = initialState;
17
- const keyListeners = new Map<keyof T, Set<StoreListener>>();
18
- const debouncedNotifiers = new Map<keyof T, () => void>();
29
+ const keyListeners: Record<string, Set<StoreListener> | undefined> = Object.create(null);
30
+
31
+ // Middleware storage
32
+ const middleware: Array<{ callback: MiddlewareFunction<T>; keys: (keyof T)[] | null }> = [];
33
+
34
+ // Per-key equality registry
35
+ const equalityRegistry: Record<string, ((prev: any, next: any) => boolean) | undefined> = Object.create(null);
36
+
37
+ // Batching
38
+ let batchedKeys: Set<keyof T> | null = null;
19
39
 
20
40
  const get = () => state;
21
41
 
22
- const set = (next: Partial<T>, debounceDelay: number | boolean = false) => {
23
- if (!next) return;
42
+ const getKey = <K extends keyof T>(key: K): T[K] => state[key];
24
43
 
25
- const updatedKeys: (keyof T)[] = [];
26
- for (const key in next) {
27
- const typedKey = key as keyof T;
28
- const currentValue = state[typedKey];
29
- const nextValue = next[typedKey];
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
+ };
30
51
 
31
- if (currentValue === nextValue) continue;
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
+ };
32
69
 
33
- if (!Object.is(currentValue, nextValue)) {
34
- state[typedKey] = nextValue!;
35
- updatedKeys.push(typedKey);
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];
36
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>) => {
120
+ if (!update) return;
121
+
122
+ if (middleware.length === 0) {
123
+ applyUpdate(update);
124
+ return;
37
125
  }
38
126
 
39
- if (updatedKeys.length === 0) return;
127
+ // Run middleware chain
128
+ let currentUpdate = update;
129
+ let middlewareIndex = 0;
130
+ let blocked = false;
40
131
 
41
- for (const key of updatedKeys) {
42
- if (debounceDelay !== false) {
43
- if (!debouncedNotifiers.has(key)) {
44
- debouncedNotifiers.set(key, debounce(() => {
45
- keyListeners.get(key)?.forEach(listener => listener());
46
- }, typeof debounceDelay === 'number' ? debounceDelay : 0));
132
+ const runMiddleware = (modifiedUpdate?: Partial<T>) => {
133
+ if (modifiedUpdate !== undefined) {
134
+ currentUpdate = modifiedUpdate;
135
+ }
136
+
137
+ if (middlewareIndex >= middleware.length) {
138
+ // All middleware processed, apply the update
139
+ if (!blocked) {
140
+ applyUpdate(currentUpdate);
141
+ }
142
+ return;
143
+ }
144
+
145
+ const currentMiddleware = middleware[middlewareIndex++];
146
+
147
+ // Check if middleware applies to these keys
148
+ if (!currentMiddleware.keys || currentMiddleware.keys.some(key => key in currentUpdate)) {
149
+ let nextCalled = false;
150
+
151
+ const next = (modifiedUpdate?: Partial<T>) => {
152
+ if (nextCalled) return; // Prevent multiple calls
153
+ nextCalled = true;
154
+ runMiddleware(modifiedUpdate);
155
+ };
156
+
157
+ try {
158
+ currentMiddleware.callback(state, currentUpdate, next);
159
+ } catch (error) {
160
+ blocked = true;
161
+ console.error('Middleware error:', error);
162
+ return;
163
+ }
164
+
165
+ // If next() wasn't called, the middleware blocked the update
166
+ if (!nextCalled) {
167
+ blocked = true;
168
+ return;
47
169
  }
48
- debouncedNotifiers.get(key)!();
49
170
  } else {
50
- keyListeners.get(key)?.forEach(listener => listener());
171
+ // Skip this middleware
172
+ runMiddleware();
173
+ }
174
+ };
175
+
176
+ runMiddleware();
177
+ };
178
+
179
+ const applyUpdate = (processedUpdate: Partial<T>) => {
180
+ const changedKeys: string[] = [];
181
+
182
+ // Phase 1: update all state before notifying
183
+ for (const key in processedUpdate) {
184
+ const typedKey = key as keyof T;
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
+ }
191
+
192
+ if (changedKeys.length === 0) return;
193
+
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);
198
+ }
199
+ return;
200
+ }
201
+
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
+ });
51
214
  }
52
215
  }
53
216
  };
54
217
 
218
+ const addMiddleware = (
219
+ callbackOrTuple: MiddlewareFunction<T> | [MiddlewareFunction<T>, (keyof T)[]],
220
+ affectedKeys: (keyof T)[] | null = null
221
+ ) => {
222
+ let callback: MiddlewareFunction<T>;
223
+ let keys: (keyof T)[] | null;
224
+
225
+ if (Array.isArray(callbackOrTuple)) {
226
+ [callback, keys] = callbackOrTuple;
227
+ } else {
228
+ callback = callbackOrTuple;
229
+ keys = affectedKeys;
230
+ }
231
+
232
+ const middlewareItem = { callback, keys };
233
+ middleware.push(middlewareItem);
234
+
235
+ return () => {
236
+ const index = middleware.indexOf(middlewareItem);
237
+ if (index > -1) {
238
+ middleware.splice(index, 1);
239
+ }
240
+ };
241
+ };
242
+
55
243
  const subscribe = (keys: (keyof T)[], listener: StoreListener): (() => void) => {
56
244
  for (const key of keys) {
57
- if (!keyListeners.has(key)) {
58
- keyListeners.set(key, new Set());
59
- }
60
- keyListeners.get(key)!.add(listener);
245
+ const k = key as string;
246
+ if (!keyListeners[k]) keyListeners[k] = new Set();
247
+ keyListeners[k]!.add(listener);
61
248
  }
62
249
 
63
250
  return () => {
64
251
  for (const key of keys) {
65
- keyListeners.get(key)?.delete(listener);
252
+ keyListeners[key as string]?.delete(listener);
66
253
  }
67
254
  };
68
255
  };
@@ -76,7 +263,52 @@ export function createStoreState<T extends object>(
76
263
  return result;
77
264
  };
78
265
 
79
- return { get, set, subscribe, select };
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 };
80
312
  }
81
313
 
82
314
  type StoreType<T extends object> = ReturnType<typeof createStoreState<T>>;
@@ -109,19 +341,54 @@ function shallowEqualSelector<T extends object>(
109
341
  return a.length === b.length && a.every((item, i) => item === b[i]);
110
342
  }
111
343
 
344
+ /**
345
+ * React hook that subscribes to specific keys in a store with fine-grained re-renders.
346
+ * Only re-renders when the selected keys actually change (using Object.is comparison).
347
+ *
348
+ * @param store - The store created with createStoreState
349
+ * @param selector - Array of keys to subscribe to, or objects with custom compare functions
350
+ * @returns Selected state values from the store
351
+ *
352
+ * @example
353
+ * ```ts
354
+ * // Subscribe to specific keys
355
+ * const { count, name } = useStoreSelector(store, ['count', 'name']);
356
+ *
357
+ * // Custom comparison for complex objects
358
+ * const { tasks } = useStoreSelector(store, [
359
+ * { tasks: (prev, next) => prev.length === next.length }
360
+ * ]);
361
+ * ```
362
+ */
112
363
  export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
113
364
  store: StoreType<T>,
114
365
  selector: S
115
366
  ): Picked<T, S> {
116
- const lastSelected = useRef<Partial<T>>({});
117
- const prevSelector = useRef<SelectorInput<T> | null>(null);
118
- const normalizedRef = useRef<NormalizedSelector<T>[] | null>(null);
119
- const keysRef = useRef<(keyof T)[] | null>(null);
120
- const isFirstRunRef = useRef(true);
121
- const lastValues = useRef<Partial<T>>({});
122
- const subscribeRef = useRef<((onStoreChange: () => void) => () => void) | null>(null);
123
-
124
- if (!prevSelector.current || !shallowEqualSelector(prevSelector.current, selector)) {
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;
380
+ if (storeChanged) {
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;
389
+ }
390
+
391
+ if (!r.prevSelector || !shallowEqualSelector(r.prevSelector, selector)) {
125
392
  const normalized: NormalizedSelector<T>[] = [];
126
393
  const keys: (keyof T)[] = [];
127
394
 
@@ -141,63 +408,54 @@ export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
141
408
  }
142
409
  }
143
410
 
144
- normalizedRef.current = normalized;
145
- keysRef.current = keys;
146
- prevSelector.current = selector;
147
- subscribeRef.current = null;
411
+ r.normalized = normalized;
412
+ r.keys = keys;
413
+ r.prevSelector = selector;
414
+ r.subscribe = null;
148
415
  }
149
416
 
150
- const normalized = normalizedRef.current!;
151
- const keys = keysRef.current!;
417
+ const normalized = r.normalized!;
418
+ const keys = r.keys!;
152
419
 
153
420
  const getSnapshot = () => {
154
421
  const current = store.get();
155
- const isFirstRun = isFirstRunRef.current;
156
422
 
157
- if (isFirstRun) {
158
- isFirstRunRef.current = false;
423
+ if (r.isFirstRun) {
424
+ r.isFirstRun = false;
159
425
  const result = {} as Partial<T>;
160
426
  for (const { key } of normalized) {
161
427
  const value = current[key];
162
- lastValues.current[key] = value;
428
+ r.lastValues[key] = value;
163
429
  result[key] = value;
164
430
  }
165
- lastSelected.current = result;
431
+ r.lastSelected = result;
166
432
  return result as Picked<T, S>;
167
433
  }
168
434
 
169
- const hasChanges = () => {
170
- for (const { key, compare } of normalized) {
171
- const prevVal = lastValues.current[key];
172
- const nextVal = current[key];
173
- if (prevVal === undefined ? true : (compare?.(prevVal, nextVal) ?? !Object.is(prevVal, nextVal))) {
174
- 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;
175
451
  }
176
452
  }
177
- return false;
178
- };
179
-
180
- if (!hasChanges()) {
181
- return lastSelected.current as Picked<T, S>;
453
+ if (result) result[key] = prevVal;
182
454
  }
183
455
 
184
- const result = {} as Partial<T>;
185
- for (const { key, compare } of normalized) {
186
- const prevVal = lastValues.current[key];
187
- const nextVal = current[key];
188
-
189
- const isFirstTime = prevVal === undefined;
190
- const changed = isFirstTime || (compare ? !compare(prevVal, nextVal) : !Object.is(prevVal, nextVal));
456
+ if (!result) return r.lastSelected as Picked<T, S>;
191
457
 
192
- if (changed) {
193
- lastValues.current[key] = nextVal;
194
- result[key] = nextVal;
195
- } else {
196
- result[key] = prevVal;
197
- }
198
- }
199
-
200
- lastSelected.current = result;
458
+ r.lastSelected = result;
201
459
  return result as Picked<T, S>;
202
460
  };
203
461
 
@@ -210,10 +468,179 @@ export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
210
468
  return result as Picked<T, S>;
211
469
  }, [keys]);
212
470
 
213
- if (!subscribeRef.current) {
214
- subscribeRef.current = (onStoreChange: () => void) =>
471
+ if (!r.subscribe || storeChanged) {
472
+ r.subscribe = (onStoreChange: () => void) =>
215
473
  store.subscribe(keys, onStoreChange);
216
474
  }
217
475
 
218
- return useSyncExternalStore(subscribeRef.current, getSnapshot, () => staticSnapshot);
476
+ return useSyncExternalStore(r.subscribe, getSnapshot, () => staticSnapshot);
477
+ }
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
+ }
499
+
500
+ /**
501
+ * Interface for synchronous storage (localStorage, sessionStorage, etc.).
502
+ */
503
+ export interface StorageSupportingInterface {
504
+ getItem(key: string): string | null;
505
+ setItem(key: string, value: string): void;
506
+ }
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
+
522
+ /**
523
+ * Creates a persistence middleware that saves individual keys to storage.
524
+ * Only writes when the specified keys actually change, using per-key storage.
525
+ * Storage format: `${persistKey}:${keyName}` for each persisted key.
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
+ *
531
+ * @param storage - Storage interface (localStorage, sessionStorage, AsyncStorage, etc.)
532
+ * @param persistKey - Base key prefix for storage (e.g., 'myapp' creates 'myapp:theme')
533
+ * @param keys - Array of state keys to persist
534
+ * @returns Tuple of [middleware function, affected keys] for use with addMiddleware
535
+ *
536
+ * @example
537
+ * ```ts
538
+ * // Sync — localStorage
539
+ * store.addMiddleware(
540
+ * createPersistenceMiddleware(localStorage, 'myapp', ['theme', 'isLoggedIn'])
541
+ * );
542
+ *
543
+ * // Async — React Native AsyncStorage
544
+ * store.addMiddleware(
545
+ * createPersistenceMiddleware(AsyncStorage, 'myapp', ['theme', 'isLoggedIn'])
546
+ * );
547
+ * ```
548
+ */
549
+ export function createPersistenceMiddleware<T extends object>(
550
+ storage: AnyStorage,
551
+ persistKey: string,
552
+ keys: (keyof T)[]
553
+ ): [MiddlewareFunction<T>, (keyof T)[]] {
554
+ const middlewareFunction: MiddlewareFunction<T> = (_, update, next) => {
555
+ const changedKeys = keys.filter(key => key in update);
556
+ if (changedKeys.length === 0) {
557
+ return next();
558
+ }
559
+
560
+ for (const key of changedKeys) {
561
+ try {
562
+ const value = update[key];
563
+ const storageKey = `${persistKey}:${String(key)}`;
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
+ }
570
+ } catch (error) {
571
+ console.warn(`Failed to persist key ${String(key)}:`, error);
572
+ }
573
+ }
574
+
575
+ next();
576
+ };
577
+
578
+ return [middlewareFunction, keys];
579
+ }
580
+
581
+ /**
582
+ * Loads persisted state from individual key storage during store initialization.
583
+ * Reads keys saved by createPersistenceMiddleware and returns them as partial state.
584
+ *
585
+ * Returns synchronously for sync storage and a Promise for async storage.
586
+ *
587
+ * @param storage - Storage interface to read from (same as used in middleware)
588
+ * @param persistKey - Base key prefix used for storage (same as used in middleware)
589
+ * @param keys - Array of keys to restore (should match middleware keys)
590
+ * @returns Partial state object (sync) or Promise of partial state (async)
591
+ *
592
+ * @example
593
+ * ```ts
594
+ * // Sync — localStorage
595
+ * const persisted = loadPersistedState(localStorage, 'myapp', ['theme']);
596
+ * const store = createStoreState({ theme: 'light', ...persisted });
597
+ *
598
+ * // Async — React Native AsyncStorage
599
+ * const persisted = await loadPersistedState(AsyncStorage, 'myapp', ['theme']);
600
+ * const store = createStoreState({ theme: 'light', ...persisted });
601
+ * ```
602
+ */
603
+ export function loadPersistedState<T extends object>(
604
+ storage: Storage | StorageSupportingInterface,
605
+ persistKey: string,
606
+ keys: (keyof 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>> {
618
+ const result: Partial<T> = {};
619
+ const pending: Promise<void>[] = [];
620
+
621
+ for (const key of keys) {
622
+ const storageKey = `${persistKey}:${String(key)}`;
623
+ try {
624
+ const stored = storage.getItem(storageKey);
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);
635
+ }
636
+ } catch (error) {
637
+ console.warn(`Failed to load persisted key ${String(key)}:`, error);
638
+ }
639
+ }
640
+
641
+ if (pending.length > 0) {
642
+ return Promise.all(pending).then(() => result);
643
+ }
644
+
645
+ return result;
219
646
  }