dev-react-microstore 4.0.1 → 5.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,6 +2,12 @@ import { useMemo, useRef, useSyncExternalStore } from 'react'
2
2
 
3
3
  type StoreListener = () => void;
4
4
 
5
+ type MiddlewareFunction<T extends object> = (
6
+ currentState: T,
7
+ update: Partial<T>,
8
+ next: (modifiedUpdate?: Partial<T>) => void
9
+ ) => void;
10
+
5
11
  function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
6
12
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
7
13
  return ((...args: Parameters<T>) => {
@@ -10,23 +16,95 @@ function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
10
16
  }) as T;
11
17
  }
12
18
 
19
+ /**
20
+ * Creates a new reactive store with fine-grained subscriptions and middleware support.
21
+ *
22
+ * @param initialState - The initial state object for the store
23
+ * @returns Store object with methods: get, set, subscribe, select, addMiddleware
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const store = createStoreState({ count: 0, name: 'John' });
28
+ * store.set({ count: 1 }); // Update state
29
+ * const { count } = useStoreSelector(store, ['count']); // Subscribe in React
30
+ * ```
31
+ */
13
32
  export function createStoreState<T extends object>(
14
- initialState: T,
33
+ initialState: T
15
34
  ) {
16
35
  let state = initialState;
17
36
  const keyListeners = new Map<keyof T, Set<StoreListener>>();
18
37
  const debouncedNotifiers = new Map<keyof T, () => void>();
38
+
39
+ // Middleware storage
40
+ const middleware: Array<{ callback: MiddlewareFunction<T>; keys: (keyof T)[] | null }> = [];
41
+
42
+
19
43
 
20
44
  const get = () => state;
21
45
 
22
- const set = (next: Partial<T>, debounceDelay: number | boolean = false) => {
23
- if (!next) return;
46
+ const set = (update: Partial<T>, debounceDelay: number | boolean = false) => {
47
+ if (!update) return;
48
+
49
+ // Run middleware chain
50
+ let currentUpdate = update;
51
+ let middlewareIndex = 0;
52
+ let blocked = false;
53
+
54
+ const runMiddleware = (modifiedUpdate?: Partial<T>) => {
55
+ if (modifiedUpdate !== undefined) {
56
+ currentUpdate = modifiedUpdate;
57
+ }
58
+
59
+ if (middlewareIndex >= middleware.length) {
60
+ // All middleware processed, apply the update
61
+ if (!blocked) {
62
+ applyUpdate(currentUpdate, debounceDelay);
63
+ }
64
+ return;
65
+ }
66
+
67
+ const currentMiddleware = middleware[middlewareIndex++];
68
+
69
+ // Check if middleware applies to these keys
70
+ if (!currentMiddleware.keys || currentMiddleware.keys.some(key => key in currentUpdate)) {
71
+ let nextCalled = false;
72
+
73
+ const next = (modifiedUpdate?: Partial<T>) => {
74
+ if (nextCalled) return; // Prevent multiple calls
75
+ nextCalled = true;
76
+ runMiddleware(modifiedUpdate);
77
+ };
78
+
79
+ try {
80
+ currentMiddleware.callback(state, currentUpdate, next);
81
+ } catch (error) {
82
+ blocked = true;
83
+ console.error('Middleware error:', error);
84
+ return;
85
+ }
24
86
 
87
+ // If next() wasn't called, the middleware blocked the update
88
+ if (!nextCalled) {
89
+ blocked = true;
90
+ return;
91
+ }
92
+ } else {
93
+ // Skip this middleware
94
+ runMiddleware();
95
+ }
96
+ };
97
+
98
+ runMiddleware();
99
+ };
100
+
101
+ const applyUpdate = (processedUpdate: Partial<T>, debounceDelay: number | boolean) => {
25
102
  const updatedKeys: (keyof T)[] = [];
26
- for (const key in next) {
103
+
104
+ for (const key in processedUpdate) {
27
105
  const typedKey = key as keyof T;
28
106
  const currentValue = state[typedKey];
29
- const nextValue = next[typedKey];
107
+ const nextValue = processedUpdate[typedKey];
30
108
 
31
109
  if (currentValue === nextValue) continue;
32
110
 
@@ -52,6 +130,33 @@ export function createStoreState<T extends object>(
52
130
  }
53
131
  };
54
132
 
133
+ const addMiddleware = (
134
+ callbackOrTuple: MiddlewareFunction<T> | [MiddlewareFunction<T>, (keyof T)[]],
135
+ affectedKeys: (keyof T)[] | null = null
136
+ ) => {
137
+ let callback: MiddlewareFunction<T>;
138
+ let keys: (keyof T)[] | null;
139
+
140
+ if (Array.isArray(callbackOrTuple)) {
141
+ [callback, keys] = callbackOrTuple;
142
+ } else {
143
+ callback = callbackOrTuple;
144
+ keys = affectedKeys;
145
+ }
146
+
147
+ const middlewareItem = { callback, keys };
148
+ middleware.push(middlewareItem);
149
+
150
+ return () => {
151
+ const index = middleware.indexOf(middlewareItem);
152
+ if (index > -1) {
153
+ middleware.splice(index, 1);
154
+ }
155
+ };
156
+ };
157
+
158
+
159
+
55
160
  const subscribe = (keys: (keyof T)[], listener: StoreListener): (() => void) => {
56
161
  for (const key of keys) {
57
162
  if (!keyListeners.has(key)) {
@@ -76,7 +181,7 @@ export function createStoreState<T extends object>(
76
181
  return result;
77
182
  };
78
183
 
79
- return { get, set, subscribe, select };
184
+ return { get, set, subscribe, select, addMiddleware };
80
185
  }
81
186
 
82
187
  type StoreType<T extends object> = ReturnType<typeof createStoreState<T>>;
@@ -109,6 +214,25 @@ function shallowEqualSelector<T extends object>(
109
214
  return a.length === b.length && a.every((item, i) => item === b[i]);
110
215
  }
111
216
 
217
+ /**
218
+ * React hook that subscribes to specific keys in a store with fine-grained re-renders.
219
+ * Only re-renders when the selected keys actually change (using Object.is comparison).
220
+ *
221
+ * @param store - The store created with createStoreState
222
+ * @param selector - Array of keys to subscribe to, or objects with custom compare functions
223
+ * @returns Selected state values from the store
224
+ *
225
+ * @example
226
+ * ```ts
227
+ * // Subscribe to specific keys
228
+ * const { count, name } = useStoreSelector(store, ['count', 'name']);
229
+ *
230
+ * // Custom comparison for complex objects
231
+ * const { tasks } = useStoreSelector(store, [
232
+ * { tasks: (prev, next) => prev.length === next.length }
233
+ * ]);
234
+ * ```
235
+ */
112
236
  export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
113
237
  store: StoreType<T>,
114
238
  selector: S
@@ -120,6 +244,19 @@ export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
120
244
  const isFirstRunRef = useRef(true);
121
245
  const lastValues = useRef<Partial<T>>({});
122
246
  const subscribeRef = useRef<((onStoreChange: () => void) => () => void) | null>(null);
247
+ const storeRef = useRef(store);
248
+
249
+ const storeChanged = storeRef.current !== store;
250
+ 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;
259
+ }
123
260
 
124
261
  if (!prevSelector.current || !shallowEqualSelector(prevSelector.current, selector)) {
125
262
  const normalized: NormalizedSelector<T>[] = [];
@@ -170,7 +307,7 @@ export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
170
307
  for (const { key, compare } of normalized) {
171
308
  const prevVal = lastValues.current[key];
172
309
  const nextVal = current[key];
173
- if (prevVal === undefined ? true : (compare?.(prevVal, nextVal) ?? !Object.is(prevVal, nextVal))) {
310
+ if (prevVal === undefined ? true : (compare ? !compare(prevVal, nextVal) : !Object.is(prevVal, nextVal))) {
174
311
  return true;
175
312
  }
176
313
  }
@@ -210,10 +347,110 @@ export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
210
347
  return result as Picked<T, S>;
211
348
  }, [keys]);
212
349
 
213
- if (!subscribeRef.current) {
350
+ if (!subscribeRef.current || storeChanged) {
214
351
  subscribeRef.current = (onStoreChange: () => void) =>
215
352
  store.subscribe(keys, onStoreChange);
216
353
  }
217
354
 
218
355
  return useSyncExternalStore(subscribeRef.current, getSnapshot, () => staticSnapshot);
356
+ }
357
+
358
+
359
+ /**
360
+ * Interface for storage objects compatible with persistence middleware.
361
+ * Includes localStorage, sessionStorage, AsyncStorage, or any custom storage.
362
+ */
363
+ export interface StorageSupportingInterface {
364
+ getItem(key: string): string | null;
365
+ setItem(key: string, value: string): void;
366
+ }
367
+
368
+ /**
369
+ * Creates a persistence middleware that saves individual keys to storage.
370
+ * Only writes when the specified keys actually change, using per-key storage.
371
+ * Storage format: `${persistKey}:${keyName}` for each persisted key.
372
+ *
373
+ * @param storage - Storage interface (localStorage, sessionStorage, AsyncStorage, etc.)
374
+ * @param persistKey - Base key prefix for storage (e.g., 'myapp' creates 'myapp:theme')
375
+ * @param keys - Array of state keys to persist
376
+ * @returns Tuple of [middleware function, affected keys] for use with addMiddleware
377
+ *
378
+ * @example
379
+ * ```ts
380
+ * // Add persistence for theme and user settings
381
+ * store.addMiddleware(
382
+ * createPersistenceMiddleware(localStorage, 'myapp', ['theme', 'isLoggedIn'])
383
+ * );
384
+ * ```
385
+ */
386
+ export function createPersistenceMiddleware<T extends object>(
387
+ storage: Storage | StorageSupportingInterface,
388
+ persistKey: string,
389
+ keys: (keyof T)[]
390
+ ): [MiddlewareFunction<T>, (keyof T)[]] {
391
+ const middlewareFunction: MiddlewareFunction<T> = (_, update, next) => {
392
+ // Check if any of the persisted keys are being updated
393
+ const changedKeys = keys.filter(key => key in update);
394
+ if (changedKeys.length === 0) {
395
+ return next();
396
+ }
397
+
398
+ // Save each changed key individually
399
+ for (const key of changedKeys) {
400
+ try {
401
+ const value = update[key];
402
+ const storageKey = `${persistKey}:${String(key)}`;
403
+ storage.setItem(storageKey, JSON.stringify(value));
404
+ } catch (error) {
405
+ console.warn(`Failed to persist key ${String(key)}:`, error);
406
+ }
407
+ }
408
+
409
+ next();
410
+ };
411
+
412
+ return [middlewareFunction, keys];
413
+ }
414
+
415
+ /**
416
+ * Loads persisted state from individual key storage during store initialization.
417
+ * Reads keys saved by createPersistenceMiddleware and returns them as partial state.
418
+ *
419
+ * @param storage - Storage interface to read from (same as used in middleware)
420
+ * @param persistKey - Base key prefix used for storage (same as used in middleware)
421
+ * @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
423
+ *
424
+ * @example
425
+ * ```ts
426
+ * // Load persisted state before creating store
427
+ * const persistedState = loadPersistedState(localStorage, 'myapp', ['theme', 'isLoggedIn']);
428
+ *
429
+ * const store = createStoreState({
430
+ * theme: 'light',
431
+ * isLoggedIn: false,
432
+ * ...persistedState // Apply persisted values
433
+ * });
434
+ * ```
435
+ */
436
+ export function loadPersistedState<T extends object>(
437
+ storage: Storage | StorageSupportingInterface,
438
+ persistKey: string,
439
+ keys: (keyof T)[]
440
+ ): Partial<T> {
441
+ const result: Partial<T> = {};
442
+
443
+ for (const key of keys) {
444
+ try {
445
+ const storageKey = `${persistKey}:${String(key)}`;
446
+ const stored = storage.getItem(storageKey);
447
+ if (stored !== null) {
448
+ result[key] = JSON.parse(stored);
449
+ }
450
+ } catch (error) {
451
+ console.warn(`Failed to load persisted key ${String(key)}:`, error);
452
+ }
453
+ }
454
+
455
+ return result;
219
456
  }