dev-react-microstore 4.0.0 → 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) => {
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
+ }
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
+ };
24
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
 
@@ -38,14 +116,12 @@ export function createStoreState<T extends object>(
38
116
 
39
117
  if (updatedKeys.length === 0) return;
40
118
 
41
-
42
119
  for (const key of updatedKeys) {
43
-
44
- if (typeof debounceDelay === 'number') {
120
+ if (debounceDelay !== false) {
45
121
  if (!debouncedNotifiers.has(key)) {
46
122
  debouncedNotifiers.set(key, debounce(() => {
47
123
  keyListeners.get(key)?.forEach(listener => listener());
48
- }, debounceDelay));
124
+ }, typeof debounceDelay === 'number' ? debounceDelay : 0));
49
125
  }
50
126
  debouncedNotifiers.get(key)!();
51
127
  } else {
@@ -54,6 +130,33 @@ export function createStoreState<T extends object>(
54
130
  }
55
131
  };
56
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
+
57
160
  const subscribe = (keys: (keyof T)[], listener: StoreListener): (() => void) => {
58
161
  for (const key of keys) {
59
162
  if (!keyListeners.has(key)) {
@@ -78,7 +181,7 @@ export function createStoreState<T extends object>(
78
181
  return result;
79
182
  };
80
183
 
81
- return { get, set, subscribe, select };
184
+ return { get, set, subscribe, select, addMiddleware };
82
185
  }
83
186
 
84
187
  type StoreType<T extends object> = ReturnType<typeof createStoreState<T>>;
@@ -111,6 +214,25 @@ function shallowEqualSelector<T extends object>(
111
214
  return a.length === b.length && a.every((item, i) => item === b[i]);
112
215
  }
113
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
+ */
114
236
  export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
115
237
  store: StoreType<T>,
116
238
  selector: S
@@ -122,6 +244,19 @@ export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
122
244
  const isFirstRunRef = useRef(true);
123
245
  const lastValues = useRef<Partial<T>>({});
124
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
+ }
125
260
 
126
261
  if (!prevSelector.current || !shallowEqualSelector(prevSelector.current, selector)) {
127
262
  const normalized: NormalizedSelector<T>[] = [];
@@ -172,7 +307,7 @@ export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
172
307
  for (const { key, compare } of normalized) {
173
308
  const prevVal = lastValues.current[key];
174
309
  const nextVal = current[key];
175
- if (prevVal === undefined ? true : (compare?.(prevVal, nextVal) ?? !Object.is(prevVal, nextVal))) {
310
+ if (prevVal === undefined ? true : (compare ? !compare(prevVal, nextVal) : !Object.is(prevVal, nextVal))) {
176
311
  return true;
177
312
  }
178
313
  }
@@ -212,10 +347,110 @@ export function useStoreSelector<T extends object, S extends SelectorInput<T>>(
212
347
  return result as Picked<T, S>;
213
348
  }, [keys]);
214
349
 
215
- if (!subscribeRef.current) {
350
+ if (!subscribeRef.current || storeChanged) {
216
351
  subscribeRef.current = (onStoreChange: () => void) =>
217
352
  store.subscribe(keys, onStoreChange);
218
353
  }
219
354
 
220
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;
221
456
  }