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/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ type MiddlewareFunction<T extends object> = (currentState: T, update: Partial<T>
4
4
  * Creates a new reactive store with fine-grained subscriptions and middleware support.
5
5
  *
6
6
  * @param initialState - The initial state object for the store
7
- * @returns Store object with methods: get, set, subscribe, select, addMiddleware
7
+ * @returns Store object with methods: get, set, subscribe, select, addMiddleware, onChange
8
8
  *
9
9
  * @example
10
10
  * ```ts
@@ -15,10 +15,20 @@ type MiddlewareFunction<T extends object> = (currentState: T, update: Partial<T>
15
15
  */
16
16
  declare function createStoreState<T extends object>(initialState: T): {
17
17
  get: () => T;
18
- set: (update: Partial<T>, debounceDelay?: number | boolean) => void;
18
+ getKey: <K extends keyof T>(key: K) => T[K];
19
+ set: (update: Partial<T>) => void;
20
+ setKey: <K extends keyof T>(key: K, value: T[K]) => void;
21
+ merge: <K extends keyof T>(key: K, value: T[K] extends object ? Partial<T[K]> : never) => T[K];
22
+ mergeSet: <K extends keyof T>(key: K, value: T[K] extends object ? Partial<T[K]> : never) => void;
23
+ reset: (keys?: (keyof T)[]) => void;
24
+ batch: (fn: () => void) => void;
19
25
  subscribe: (keys: (keyof T)[], listener: StoreListener) => (() => void);
20
26
  select: <K extends keyof T>(keys: K[]) => Pick<T, K>;
21
27
  addMiddleware: (callbackOrTuple: MiddlewareFunction<T> | [MiddlewareFunction<T>, (keyof T)[]], affectedKeys?: (keyof T)[] | null) => () => void;
28
+ onChange: <K extends keyof T>(keys: K[], callback: (values: Pick<T, K>, prev: Pick<T, K>) => void) => (() => void);
29
+ skipSetWhen: <K extends keyof T>(key: K, fn: (prev: T[K], next: T[K]) => boolean) => void;
30
+ removeSkipSetWhen: (key: keyof T) => void;
31
+ _eqReg: Record<string, ((prev: any, next: any) => boolean) | undefined>;
22
32
  };
23
33
  type StoreType<T extends object> = ReturnType<typeof createStoreState<T>>;
24
34
  type PrimitiveKey<T extends object> = keyof T;
@@ -53,18 +63,45 @@ type Picked<T extends object, S extends SelectorInput<T>> = ExtractSelectorKeys<
53
63
  */
54
64
  declare function useStoreSelector<T extends object, S extends SelectorInput<T>>(store: StoreType<T>, selector: S): Picked<T, S>;
55
65
  /**
56
- * Interface for storage objects compatible with persistence middleware.
57
- * Includes localStorage, sessionStorage, AsyncStorage, or any custom storage.
66
+ * Creates a pre-bound selector hook for a specific store instance.
67
+ * Infers the state type from the store — no manual generics needed.
68
+ *
69
+ * @param store - The store created with createStoreState
70
+ * @returns A React hook with the same API as useStoreSelector, but with the store already bound
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * const useMyStore = createSelectorHook(myStore);
75
+ *
76
+ * // In a component:
77
+ * const { count, name } = useMyStore(['count', 'name']);
78
+ * ```
79
+ */
80
+ declare function createSelectorHook<T extends object>(store: StoreType<T>): <S extends SelectorInput<T>>(selector: S) => Picked<T, S>;
81
+ /**
82
+ * Interface for synchronous storage (localStorage, sessionStorage, etc.).
58
83
  */
59
84
  interface StorageSupportingInterface {
60
85
  getItem(key: string): string | null;
61
86
  setItem(key: string, value: string): void;
62
87
  }
88
+ /**
89
+ * Interface for asynchronous storage (React Native AsyncStorage, etc.).
90
+ */
91
+ interface AsyncStorageSupportingInterface {
92
+ getItem(key: string): Promise<string | null>;
93
+ setItem(key: string, value: string): Promise<void>;
94
+ }
95
+ type AnyStorage = Storage | StorageSupportingInterface | AsyncStorageSupportingInterface;
63
96
  /**
64
97
  * Creates a persistence middleware that saves individual keys to storage.
65
98
  * Only writes when the specified keys actually change, using per-key storage.
66
99
  * Storage format: `${persistKey}:${keyName}` for each persisted key.
67
100
  *
101
+ * Works with both synchronous storage (localStorage) and asynchronous storage
102
+ * (React Native AsyncStorage). Async writes are fire-and-forget — the state
103
+ * update is never blocked by a slow write.
104
+ *
68
105
  * @param storage - Storage interface (localStorage, sessionStorage, AsyncStorage, etc.)
69
106
  * @param persistKey - Base key prefix for storage (e.g., 'myapp' creates 'myapp:theme')
70
107
  * @param keys - Array of state keys to persist
@@ -72,34 +109,41 @@ interface StorageSupportingInterface {
72
109
  *
73
110
  * @example
74
111
  * ```ts
75
- * // Add persistence for theme and user settings
112
+ * // Sync localStorage
76
113
  * store.addMiddleware(
77
114
  * createPersistenceMiddleware(localStorage, 'myapp', ['theme', 'isLoggedIn'])
78
115
  * );
116
+ *
117
+ * // Async — React Native AsyncStorage
118
+ * store.addMiddleware(
119
+ * createPersistenceMiddleware(AsyncStorage, 'myapp', ['theme', 'isLoggedIn'])
120
+ * );
79
121
  * ```
80
122
  */
81
- declare function createPersistenceMiddleware<T extends object>(storage: Storage | StorageSupportingInterface, persistKey: string, keys: (keyof T)[]): [MiddlewareFunction<T>, (keyof T)[]];
123
+ declare function createPersistenceMiddleware<T extends object>(storage: AnyStorage, persistKey: string, keys: (keyof T)[]): [MiddlewareFunction<T>, (keyof T)[]];
82
124
  /**
83
125
  * Loads persisted state from individual key storage during store initialization.
84
126
  * Reads keys saved by createPersistenceMiddleware and returns them as partial state.
85
127
  *
128
+ * Returns synchronously for sync storage and a Promise for async storage.
129
+ *
86
130
  * @param storage - Storage interface to read from (same as used in middleware)
87
131
  * @param persistKey - Base key prefix used for storage (same as used in middleware)
88
132
  * @param keys - Array of keys to restore (should match middleware keys)
89
- * @returns Partial state object with persisted values, or empty object if loading fails
133
+ * @returns Partial state object (sync) or Promise of partial state (async)
90
134
  *
91
135
  * @example
92
136
  * ```ts
93
- * // Load persisted state before creating store
94
- * const persistedState = loadPersistedState(localStorage, 'myapp', ['theme', 'isLoggedIn']);
95
- *
96
- * const store = createStoreState({
97
- * theme: 'light',
98
- * isLoggedIn: false,
99
- * ...persistedState // Apply persisted values
100
- * });
137
+ * // Sync localStorage
138
+ * const persisted = loadPersistedState(localStorage, 'myapp', ['theme']);
139
+ * const store = createStoreState({ theme: 'light', ...persisted });
140
+ *
141
+ * // Async — React Native AsyncStorage
142
+ * const persisted = await loadPersistedState(AsyncStorage, 'myapp', ['theme']);
143
+ * const store = createStoreState({ theme: 'light', ...persisted });
101
144
  * ```
102
145
  */
103
146
  declare function loadPersistedState<T extends object>(storage: Storage | StorageSupportingInterface, persistKey: string, keys: (keyof T)[]): Partial<T>;
147
+ declare function loadPersistedState<T extends object>(storage: AsyncStorageSupportingInterface, persistKey: string, keys: (keyof T)[]): Promise<Partial<T>>;
104
148
 
105
- export { type StorageSupportingInterface, createPersistenceMiddleware, createStoreState, loadPersistedState, useStoreSelector };
149
+ export { type AsyncStorageSupportingInterface, type StorageSupportingInterface, createPersistenceMiddleware, createSelectorHook, createStoreState, loadPersistedState, useStoreSelector };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var w=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var F=Object.prototype.hasOwnProperty;var K=(n,e)=>{for(var t in e)w(n,t,{get:e[t],enumerable:!0})},j=(n,e,t,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of I(e))!F.call(n,i)&&i!==t&&w(n,i,{get:()=>e[i],enumerable:!(a=v(e,i))||a.enumerable});return n};var M=n=>j(w({},"__esModule",{value:!0}),n);var $={};K($,{createPersistenceMiddleware:()=>E,createStoreState:()=>R,loadPersistedState:()=>N,useStoreSelector:()=>z});module.exports=M($);var S=require("react");function C(n,e){let t;return(...a)=>{clearTimeout(t),t=setTimeout(()=>n(...a),e)}}function R(n){let e=n,t=new Map,a=new Map,i=[],k=()=>e,g=(l,f=!1)=>{if(!l)return;let s=l,T=0,r=!1,o=y=>{if(y!==void 0&&(s=y),T>=i.length){r||x(s,f);return}let u=i[T++];if(!u.keys||u.keys.some(c=>c in s)){let c=!1,p=d=>{c||(c=!0,o(d))};try{u.callback(e,s,p)}catch(d){r=!0,console.error("Middleware error:",d);return}if(!c){r=!0;return}}else o()};o()},x=(l,f)=>{var T;let s=[];for(let r in l){let o=r,y=e[o],u=l[o];y!==u&&(Object.is(y,u)||(e[o]=u,s.push(o)))}if(s.length!==0)for(let r of s)f!==!1?(a.has(r)||a.set(r,C(()=>{var o;(o=t.get(r))==null||o.forEach(y=>y())},typeof f=="number"?f:0)),a.get(r)()):(T=t.get(r))==null||T.forEach(o=>o())};return{get:k,set:g,subscribe:(l,f)=>{for(let s of l)t.has(s)||t.set(s,new Set),t.get(s).add(f);return()=>{var s;for(let T of l)(s=t.get(T))==null||s.delete(f)}},select:l=>{let f={},s=e;for(let T of l)f[T]=s[T];return f},addMiddleware:(l,f=null)=>{let s,T;Array.isArray(l)?[s,T]=l:(s=l,T=f);let r={callback:s,keys:T};return i.push(r),()=>{let o=i.indexOf(r);o>-1&&i.splice(o,1)}}}}function V(n,e){return n.length===e.length&&n.every((t,a)=>t===e[a])}function z(n,e){let t=(0,S.useRef)({}),a=(0,S.useRef)(null),i=(0,S.useRef)(null),k=(0,S.useRef)(null),g=(0,S.useRef)(!0),x=(0,S.useRef)({}),m=(0,S.useRef)(null),b=(0,S.useRef)(n),P=b.current!==n;if(P&&(b.current=n,t.current={},a.current=null,i.current=null,k.current=null,g.current=!0,x.current={},m.current=null),!a.current||!V(a.current,e)){let r=[],o=[];for(let y of e)if(typeof y=="string"){let u=y;r.push({key:u}),o.push(u)}else{let u=y;for(let c in u){let p=u[c],d=c;r.push({key:d,compare:p}),o.push(d)}}i.current=r,k.current=o,a.current=e,m.current=null}let l=i.current,f=k.current,s=()=>{let r=n.get();if(g.current){g.current=!1;let c={};for(let{key:p}of l){let d=r[p];x.current[p]=d,c[p]=d}return t.current=c,c}if(!(()=>{for(let{key:c,compare:p}of l){let d=x.current[c],h=r[c];if(d===void 0||(p?!p(d,h):!Object.is(d,h)))return!0}return!1})())return t.current;let u={};for(let{key:c,compare:p}of l){let d=x.current[c],h=r[c];d===void 0||(p?!p(d,h):!Object.is(d,h))?(x.current[c]=h,u[c]=h):u[c]=d}return t.current=u,u},T=(0,S.useMemo)(()=>{let r=n.get(),o={};for(let y of f)o[y]=r[y];return o},[f]);return(!m.current||P)&&(m.current=r=>n.subscribe(f,r)),(0,S.useSyncExternalStore)(m.current,s,()=>T)}function E(n,e,t){return[(i,k,g)=>{let x=t.filter(m=>m in k);if(x.length===0)return g();for(let m of x)try{let b=k[m],P=`${e}:${String(m)}`;n.setItem(P,JSON.stringify(b))}catch(b){console.warn(`Failed to persist key ${String(m)}:`,b)}g()},t]}function N(n,e,t){let a={};for(let i of t)try{let k=`${e}:${String(i)}`,g=n.getItem(k);g!==null&&(a[i]=JSON.parse(g))}catch(k){console.warn(`Failed to load persisted key ${String(i)}:`,k)}return a}0&&(module.exports={createPersistenceMiddleware,createStoreState,loadPersistedState,useStoreSelector});
1
+ "use strict";var F=Object.defineProperty;var $=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames,R=Object.getOwnPropertySymbols;var A=Object.prototype.hasOwnProperty,L=Object.prototype.propertyIsEnumerable;var V=(o,i,r)=>i in o?F(o,i,{enumerable:!0,configurable:!0,writable:!0,value:r}):o[i]=r,h=(o,i)=>{for(var r in i||(i={}))A.call(i,r)&&V(o,r,i[r]);if(R)for(var r of R(i))L.call(i,r)&&V(o,r,i[r]);return o};var N=(o,i)=>{for(var r in i)F(o,r,{get:i[r],enumerable:!0})},W=(o,i,r,t)=>{if(i&&typeof i=="object"||typeof i=="function")for(let l of q(i))!A.call(o,l)&&l!==r&&F(o,l,{get:()=>i[l],enumerable:!(t=$(i,l))||t.enumerable});return o};var _=o=>W(F({},"__esModule",{value:!0}),o);var Q={};N(Q,{createPersistenceMiddleware:()=>D,createSelectorHook:()=>B,createStoreState:()=>J,loadPersistedState:()=>G,useStoreSelector:()=>z});module.exports=_(Q);var I=require("react");function J(o){let i=h({},o),r=o,t=Object.create(null),l=[],f=Object.create(null),S=null,m=()=>r,k=e=>r[e],p=e=>{var n;if(S){S.add(e);return}(n=t[e])==null||n.forEach(s=>s())},u=(e,n)=>{if(Object.is(r[e],n))return;let s=f[e];s&&s(r[e],n)||(r[e]=n,p(e))},d=(e,n)=>{if(l.length===0){u(e,n);return}let s={};s[e]=n,b(s)},y=(e,n)=>h(h({},r[e]),n),K=(e,n)=>{if(l.length===0){u(e,h(h({},r[e]),n));return}let s={};s[e]=h(h({},r[e]),n),b(s)},P=e=>{if(!e)b(h({},i));else{let n={};for(let s of e)n[s]=i[s];b(n)}},x=e=>{var n;if(S){e();return}S=new Set;try{e()}finally{let s=S;S=null;let c=new Set;for(let T of s)(n=t[T])==null||n.forEach(a=>{c.has(a)||(c.add(a),a())})}},b=e=>{if(!e)return;if(l.length===0){v(e);return}let n=e,s=0,c=!1,T=a=>{if(a!==void 0&&(n=a),s>=l.length){c||v(n);return}let g=l[s++];if(!g.keys||g.keys.some(w=>w in n)){let w=!1,j=M=>{w||(w=!0,T(M))};try{g.callback(r,n,j)}catch(M){c=!0,console.error("Middleware error:",M);return}if(!w){c=!0;return}}else T()};T()},v=e=>{var s,c;let n=[];for(let T in e){let a=T;if(Object.is(r[a],e[a]))continue;let g=f[T];g&&g(r[a],e[a])||(r[a]=e[a],n.push(T))}if(n.length!==0){if(S){for(let T of n)S.add(T);return}if(n.length===1)(s=t[n[0]])==null||s.forEach(T=>T());else{let T=new Set;for(let a of n)(c=t[a])==null||c.forEach(g=>{T.has(g)||(T.add(g),g())})}}},O=(e,n=null)=>{let s,c;Array.isArray(e)?[s,c]=e:(s=e,c=n);let T={callback:s,keys:c};return l.push(T),()=>{let a=l.indexOf(T);a>-1&&l.splice(a,1)}},C=(e,n)=>{for(let s of e){let c=s;t[c]||(t[c]=new Set),t[c].add(n)}return()=>{var s;for(let c of e)(s=t[c])==null||s.delete(n)}};return{get:m,getKey:k,set:b,setKey:d,merge:y,mergeSet:K,reset:P,batch:x,subscribe:C,select:e=>{let n={},s=r;for(let c of e)n[c]=s[c];return n},addMiddleware:O,onChange:(e,n)=>{let s={};for(let a of e)s[a]=r[a];let c=!1;return C(e,()=>{c||(c=!0,queueMicrotask(()=>{c=!1;let a=!1;for(let j of e)if(!Object.is(r[j],s[j])){a=!0;break}if(!a)return;let g={};for(let j of e)g[j]=r[j];let w=s;s=g,n(g,w)}))})},skipSetWhen:(e,n)=>{f[e]=n},removeSkipSetWhen:e=>{delete f[e]},_eqReg:f}}function H(o,i){return o.length===i.length&&o.every((r,t)=>r===i[t])}function z(o,i){let t=(0,I.useRef)({lastSelected:{},prevSelector:null,normalized:null,keys:null,isFirstRun:!0,lastValues:{},subscribe:null,store:o}).current,l=t.store!==o;if(l&&(t.store=o,t.lastSelected={},t.prevSelector=null,t.normalized=null,t.keys=null,t.isFirstRun=!0,t.lastValues={},t.subscribe=null),!t.prevSelector||!H(t.prevSelector,i)){let p=[],u=[];for(let d of i)if(typeof d=="string"){let y=d;p.push({key:y}),u.push(y)}else{let y=d;for(let K in y){let P=y[K],x=K;p.push({key:x,compare:P}),u.push(x)}}t.normalized=p,t.keys=u,t.prevSelector=i,t.subscribe=null}let f=t.normalized,S=t.keys,m=()=>{let p=o.get();if(t.isFirstRun){t.isFirstRun=!1;let d={};for(let{key:y}of f){let K=p[y];t.lastValues[y]=K,d[y]=K}return t.lastSelected=d,d}let u=null;for(let d=0;d<f.length;d++){let{key:y,compare:K}=f[d],P=t.lastValues[y],x=p[y];if(!Object.is(P,x)){let b=K||o._eqReg[y];if(!b||!b(P,x)){if(!u){u={};for(let v=0;v<d;v++)u[f[v].key]=t.lastValues[f[v].key]}t.lastValues[y]=x,u[y]=x;continue}}u&&(u[y]=P)}return u?(t.lastSelected=u,u):t.lastSelected},k=(0,I.useMemo)(()=>{let p=o.get(),u={};for(let d of S)u[d]=p[d];return u},[S]);return(!t.subscribe||l)&&(t.subscribe=p=>o.subscribe(S,p)),(0,I.useSyncExternalStore)(t.subscribe,m,()=>k)}function B(o){return function(i){return z(o,i)}}function E(o){return o!=null&&typeof o.then=="function"}function D(o,i,r){return[(l,f,S)=>{let m=r.filter(k=>k in f);if(m.length===0)return S();for(let k of m)try{let p=f[k],u=`${i}:${String(k)}`,d=o.setItem(u,JSON.stringify(p));E(d)&&d.catch(y=>{console.warn(`Failed to persist key ${String(k)}:`,y)})}catch(p){console.warn(`Failed to persist key ${String(k)}:`,p)}S()},r]}function G(o,i,r){let t={},l=[];for(let f of r){let S=`${i}:${String(f)}`;try{let m=o.getItem(S);E(m)?l.push(m.then(k=>{k!==null&&(t[f]=JSON.parse(k))}).catch(k=>{console.warn(`Failed to load persisted key ${String(f)}:`,k)})):m!==null&&(t[f]=JSON.parse(m))}catch(m){console.warn(`Failed to load persisted key ${String(f)}:`,m)}}return l.length>0?Promise.all(l).then(()=>t):t}0&&(module.exports={createPersistenceMiddleware,createSelectorHook,createStoreState,loadPersistedState,useStoreSelector});
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{useMemo as w,useRef as x,useSyncExternalStore as v}from"react";function I(l,s){let n;return(...d)=>{clearTimeout(n),n=setTimeout(()=>l(...d),s)}}function C(l){let s=l,n=new Map,d=new Map,y=[],S=()=>s,m=(c,u=!1)=>{if(!c)return;let r=c,f=0,e=!1,t=T=>{if(T!==void 0&&(r=T),f>=y.length){e||g(r,u);return}let i=y[f++];if(!i.keys||i.keys.some(o=>o in r)){let o=!1,p=a=>{o||(o=!0,t(a))};try{i.callback(s,r,p)}catch(a){e=!0,console.error("Middleware error:",a);return}if(!o){e=!0;return}}else t()};t()},g=(c,u)=>{var f;let r=[];for(let e in c){let t=e,T=s[t],i=c[t];T!==i&&(Object.is(T,i)||(s[t]=i,r.push(t)))}if(r.length!==0)for(let e of r)u!==!1?(d.has(e)||d.set(e,I(()=>{var t;(t=n.get(e))==null||t.forEach(T=>T())},typeof u=="number"?u:0)),d.get(e)()):(f=n.get(e))==null||f.forEach(t=>t())};return{get:S,set:m,subscribe:(c,u)=>{for(let r of c)n.has(r)||n.set(r,new Set),n.get(r).add(u);return()=>{var r;for(let f of c)(r=n.get(f))==null||r.delete(u)}},select:c=>{let u={},r=s;for(let f of c)u[f]=r[f];return u},addMiddleware:(c,u=null)=>{let r,f;Array.isArray(c)?[r,f]=c:(r=c,f=u);let e={callback:r,keys:f};return y.push(e),()=>{let t=y.indexOf(e);t>-1&&y.splice(t,1)}}}}function F(l,s){return l.length===s.length&&l.every((n,d)=>n===s[d])}function R(l,s){let n=x({}),d=x(null),y=x(null),S=x(null),m=x(!0),g=x({}),k=x(null),b=x(l),P=b.current!==l;if(P&&(b.current=l,n.current={},d.current=null,y.current=null,S.current=null,m.current=!0,g.current={},k.current=null),!d.current||!F(d.current,s)){let e=[],t=[];for(let T of s)if(typeof T=="string"){let i=T;e.push({key:i}),t.push(i)}else{let i=T;for(let o in i){let p=i[o],a=o;e.push({key:a,compare:p}),t.push(a)}}y.current=e,S.current=t,d.current=s,k.current=null}let c=y.current,u=S.current,r=()=>{let e=l.get();if(m.current){m.current=!1;let o={};for(let{key:p}of c){let a=e[p];g.current[p]=a,o[p]=a}return n.current=o,o}if(!(()=>{for(let{key:o,compare:p}of c){let a=g.current[o],h=e[o];if(a===void 0||(p?!p(a,h):!Object.is(a,h)))return!0}return!1})())return n.current;let i={};for(let{key:o,compare:p}of c){let a=g.current[o],h=e[o];a===void 0||(p?!p(a,h):!Object.is(a,h))?(g.current[o]=h,i[o]=h):i[o]=a}return n.current=i,i},f=w(()=>{let e=l.get(),t={};for(let T of u)t[T]=e[T];return t},[u]);return(!k.current||P)&&(k.current=e=>l.subscribe(u,e)),v(k.current,r,()=>f)}function V(l,s,n){return[(y,S,m)=>{let g=n.filter(k=>k in S);if(g.length===0)return m();for(let k of g)try{let b=S[k],P=`${s}:${String(k)}`;l.setItem(P,JSON.stringify(b))}catch(b){console.warn(`Failed to persist key ${String(k)}:`,b)}m()},n]}function z(l,s,n){let d={};for(let y of n)try{let S=`${s}:${String(y)}`,m=l.getItem(S);m!==null&&(d[y]=JSON.parse(m))}catch(S){console.warn(`Failed to load persisted key ${String(y)}:`,S)}return d}export{V as createPersistenceMiddleware,C as createStoreState,z as loadPersistedState,R as useStoreSelector};
1
+ var A=Object.defineProperty;var M=Object.getOwnPropertySymbols;var z=Object.prototype.hasOwnProperty,E=Object.prototype.propertyIsEnumerable;var C=(s,i,o)=>i in s?A(s,i,{enumerable:!0,configurable:!0,writable:!0,value:o}):s[i]=o,h=(s,i)=>{for(var o in i||(i={}))z.call(i,o)&&C(s,o,i[o]);if(M)for(var o of M(i))E.call(i,o)&&C(s,o,i[o]);return s};import{useMemo as O,useRef as $,useSyncExternalStore as q}from"react";function G(s){let i=h({},s),o=s,t=Object.create(null),S=[],u=Object.create(null),T=null,m=()=>o,k=e=>o[e],p=e=>{var n;if(T){T.add(e);return}(n=t[e])==null||n.forEach(r=>r())},l=(e,n)=>{if(Object.is(o[e],n))return;let r=u[e];r&&r(o[e],n)||(o[e]=n,p(e))},f=(e,n)=>{if(S.length===0){l(e,n);return}let r={};r[e]=n,b(r)},d=(e,n)=>h(h({},o[e]),n),K=(e,n)=>{if(S.length===0){l(e,h(h({},o[e]),n));return}let r={};r[e]=h(h({},o[e]),n),b(r)},P=e=>{if(!e)b(h({},i));else{let n={};for(let r of e)n[r]=i[r];b(n)}},x=e=>{var n;if(T){e();return}T=new Set;try{e()}finally{let r=T;T=null;let c=new Set;for(let y of r)(n=t[y])==null||n.forEach(a=>{c.has(a)||(c.add(a),a())})}},b=e=>{if(!e)return;if(S.length===0){v(e);return}let n=e,r=0,c=!1,y=a=>{if(a!==void 0&&(n=a),r>=S.length){c||v(n);return}let g=S[r++];if(!g.keys||g.keys.some(w=>w in n)){let w=!1,j=I=>{w||(w=!0,y(I))};try{g.callback(o,n,j)}catch(I){c=!0,console.error("Middleware error:",I);return}if(!w){c=!0;return}}else y()};y()},v=e=>{var r,c;let n=[];for(let y in e){let a=y;if(Object.is(o[a],e[a]))continue;let g=u[y];g&&g(o[a],e[a])||(o[a]=e[a],n.push(y))}if(n.length!==0){if(T){for(let y of n)T.add(y);return}if(n.length===1)(r=t[n[0]])==null||r.forEach(y=>y());else{let y=new Set;for(let a of n)(c=t[a])==null||c.forEach(g=>{y.has(g)||(y.add(g),g())})}}},V=(e,n=null)=>{let r,c;Array.isArray(e)?[r,c]=e:(r=e,c=n);let y={callback:r,keys:c};return S.push(y),()=>{let a=S.indexOf(y);a>-1&&S.splice(a,1)}},F=(e,n)=>{for(let r of e){let c=r;t[c]||(t[c]=new Set),t[c].add(n)}return()=>{var r;for(let c of e)(r=t[c])==null||r.delete(n)}};return{get:m,getKey:k,set:b,setKey:f,merge:d,mergeSet:K,reset:P,batch:x,subscribe:F,select:e=>{let n={},r=o;for(let c of e)n[c]=r[c];return n},addMiddleware:V,onChange:(e,n)=>{let r={};for(let a of e)r[a]=o[a];let c=!1;return F(e,()=>{c||(c=!0,queueMicrotask(()=>{c=!1;let a=!1;for(let j of e)if(!Object.is(o[j],r[j])){a=!0;break}if(!a)return;let g={};for(let j of e)g[j]=o[j];let w=r;r=g,n(g,w)}))})},skipSetWhen:(e,n)=>{u[e]=n},removeSkipSetWhen:e=>{delete u[e]},_eqReg:u}}function L(s,i){return s.length===i.length&&s.every((o,t)=>o===i[t])}function N(s,i){let t=$({lastSelected:{},prevSelector:null,normalized:null,keys:null,isFirstRun:!0,lastValues:{},subscribe:null,store:s}).current,S=t.store!==s;if(S&&(t.store=s,t.lastSelected={},t.prevSelector=null,t.normalized=null,t.keys=null,t.isFirstRun=!0,t.lastValues={},t.subscribe=null),!t.prevSelector||!L(t.prevSelector,i)){let p=[],l=[];for(let f of i)if(typeof f=="string"){let d=f;p.push({key:d}),l.push(d)}else{let d=f;for(let K in d){let P=d[K],x=K;p.push({key:x,compare:P}),l.push(x)}}t.normalized=p,t.keys=l,t.prevSelector=i,t.subscribe=null}let u=t.normalized,T=t.keys,m=()=>{let p=s.get();if(t.isFirstRun){t.isFirstRun=!1;let f={};for(let{key:d}of u){let K=p[d];t.lastValues[d]=K,f[d]=K}return t.lastSelected=f,f}let l=null;for(let f=0;f<u.length;f++){let{key:d,compare:K}=u[f],P=t.lastValues[d],x=p[d];if(!Object.is(P,x)){let b=K||s._eqReg[d];if(!b||!b(P,x)){if(!l){l={};for(let v=0;v<f;v++)l[u[v].key]=t.lastValues[u[v].key]}t.lastValues[d]=x,l[d]=x;continue}}l&&(l[d]=P)}return l?(t.lastSelected=l,l):t.lastSelected},k=O(()=>{let p=s.get(),l={};for(let f of T)l[f]=p[f];return l},[T]);return(!t.subscribe||S)&&(t.subscribe=p=>s.subscribe(T,p)),q(t.subscribe,m,()=>k)}function Q(s){return function(i){return N(s,i)}}function R(s){return s!=null&&typeof s.then=="function"}function X(s,i,o){return[(S,u,T)=>{let m=o.filter(k=>k in u);if(m.length===0)return T();for(let k of m)try{let p=u[k],l=`${i}:${String(k)}`,f=s.setItem(l,JSON.stringify(p));R(f)&&f.catch(d=>{console.warn(`Failed to persist key ${String(k)}:`,d)})}catch(p){console.warn(`Failed to persist key ${String(k)}:`,p)}T()},o]}function Y(s,i,o){let t={},S=[];for(let u of o){let T=`${i}:${String(u)}`;try{let m=s.getItem(T);R(m)?S.push(m.then(k=>{k!==null&&(t[u]=JSON.parse(k))}).catch(k=>{console.warn(`Failed to load persisted key ${String(u)}:`,k)})):m!==null&&(t[u]=JSON.parse(m))}catch(m){console.warn(`Failed to load persisted key ${String(u)}:`,m)}}return S.length>0?Promise.all(S).then(()=>t):t}export{X as createPersistenceMiddleware,Q as createSelectorHook,G as createStoreState,Y as loadPersistedState,N as useStoreSelector};
package/package.json CHANGED
@@ -1,19 +1,22 @@
1
1
  {
2
2
  "name": "dev-react-microstore",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "A minimal global state manager for React with fine-grained subscriptions.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/OhadBaehr/react-microstore.git"
9
+ "url": "https://github.com/OhadBaehr/dev-react-microstore.git"
10
10
  },
11
- "homepage": "https://github.com/OhadBaehr/react-microstore",
11
+ "homepage": "https://github.com/OhadBaehr/dev-react-microstore",
12
12
  "bugs": {
13
- "url": "https://github.com/OhadBaehr/react-microstore/issues"
13
+ "url": "https://github.com/OhadBaehr/dev-react-microstore/issues"
14
14
  },
15
15
  "scripts": {
16
16
  "build": "tsup src/index.ts --format esm,cjs --dts --minify",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
19
+ "typecheck": "tsc --noEmit --skipLibCheck",
17
20
  "prepare": "npm run build"
18
21
  },
19
22
  "keywords": [
@@ -32,8 +35,15 @@
32
35
  "react": ">=17.0.0"
33
36
  },
34
37
  "devDependencies": {
38
+ "@testing-library/jest-dom": "^6.9.1",
39
+ "@testing-library/react": "^16.3.2",
35
40
  "@types/react": "^19.1.2",
41
+ "@types/react-dom": "^19.2.3",
42
+ "jsdom": "^28.1.0",
43
+ "react": "^19.2.4",
44
+ "react-dom": "^19.2.4",
36
45
  "tsup": "^8.4.0",
37
- "typescript": "^5.8.3"
46
+ "typescript": "^5.8.3",
47
+ "vitest": "^4.0.18"
38
48
  }
39
- }
49
+ }
@@ -0,0 +1,271 @@
1
+ import React from 'react'
2
+ import { describe, it, expect, vi } from 'vitest'
3
+ import { renderHook, act } from '@testing-library/react'
4
+ import {
5
+ createStoreState,
6
+ useStoreSelector,
7
+ createSelectorHook,
8
+ } from './index'
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // useStoreSelector
12
+ // ---------------------------------------------------------------------------
13
+ describe('useStoreSelector', () => {
14
+ it('returns selected keys from the store', () => {
15
+ const store = createStoreState({ a: 1, b: 2, c: 3 })
16
+ const { result } = renderHook(() => useStoreSelector(store, ['a', 'c']))
17
+
18
+ expect(result.current).toEqual({ a: 1, c: 3 })
19
+ })
20
+
21
+ it('re-renders when a subscribed key changes', () => {
22
+ const store = createStoreState({ count: 0 })
23
+ const { result } = renderHook(() => useStoreSelector(store, ['count']))
24
+
25
+ expect(result.current.count).toBe(0)
26
+
27
+ act(() => store.set({ count: 5 }))
28
+
29
+ expect(result.current.count).toBe(5)
30
+ })
31
+
32
+ it('does not re-render when an unrelated key changes', () => {
33
+ const store = createStoreState({ a: 1, b: 2 })
34
+ const renderCount = vi.fn()
35
+
36
+ renderHook(() => {
37
+ renderCount()
38
+ return useStoreSelector(store, ['a'])
39
+ })
40
+
41
+ const callsAfterMount = renderCount.mock.calls.length
42
+
43
+ act(() => store.set({ b: 99 }))
44
+
45
+ expect(renderCount.mock.calls.length).toBe(callsAfterMount)
46
+ })
47
+
48
+ it('does not re-render when set to the same value', () => {
49
+ const store = createStoreState({ count: 0 })
50
+ const renderCount = vi.fn()
51
+
52
+ renderHook(() => {
53
+ renderCount()
54
+ return useStoreSelector(store, ['count'])
55
+ })
56
+
57
+ const callsAfterMount = renderCount.mock.calls.length
58
+
59
+ act(() => store.set({ count: 0 }))
60
+
61
+ expect(renderCount.mock.calls.length).toBe(callsAfterMount)
62
+ })
63
+
64
+ it('handles multiple keys and only re-renders for actual changes', () => {
65
+ const store = createStoreState({ x: 1, y: 2, z: 3 })
66
+ const { result } = renderHook(() => useStoreSelector(store, ['x', 'y']))
67
+
68
+ act(() => store.set({ x: 10 }))
69
+ expect(result.current).toEqual({ x: 10, y: 2 })
70
+
71
+ act(() => store.set({ y: 20 }))
72
+ expect(result.current).toEqual({ x: 10, y: 20 })
73
+ })
74
+
75
+ it('works with custom comparison functions', () => {
76
+ const store = createStoreState({
77
+ items: [1, 2, 3],
78
+ })
79
+ const renderCount = vi.fn()
80
+
81
+ const { result } = renderHook(() => {
82
+ renderCount()
83
+ return useStoreSelector(store, [
84
+ { items: (prev: number[], next: number[]) => prev.length === next.length },
85
+ ])
86
+ })
87
+
88
+ const callsAfterMount = renderCount.mock.calls.length
89
+
90
+ // Same length array — custom compare returns true (equal), so no re-render
91
+ act(() => store.set({ items: [4, 5, 6] }))
92
+ expect(renderCount.mock.calls.length).toBe(callsAfterMount)
93
+
94
+ // Different length — custom compare returns false (not equal), triggers re-render
95
+ act(() => store.set({ items: [1, 2, 3, 4] }))
96
+ expect(result.current.items).toEqual([1, 2, 3, 4])
97
+ })
98
+
99
+ it('returns referentially stable result when nothing changed', () => {
100
+ const store = createStoreState({ a: 1, b: 2 })
101
+ const { result, rerender } = renderHook(() => useStoreSelector(store, ['a']))
102
+
103
+ const first = result.current
104
+ rerender()
105
+ const second = result.current
106
+
107
+ expect(first).toBe(second)
108
+ })
109
+
110
+ it('handles rapid sequential updates correctly', () => {
111
+ const store = createStoreState({ v: 0 })
112
+ const { result } = renderHook(() => useStoreSelector(store, ['v']))
113
+
114
+ act(() => {
115
+ store.set({ v: 1 })
116
+ store.set({ v: 2 })
117
+ store.set({ v: 3 })
118
+ })
119
+
120
+ expect(result.current.v).toBe(3)
121
+ })
122
+
123
+ it('works with object values', () => {
124
+ const store = createStoreState({ user: { name: 'Alice', age: 30 } })
125
+ const { result } = renderHook(() => useStoreSelector(store, ['user']))
126
+
127
+ expect(result.current.user).toEqual({ name: 'Alice', age: 30 })
128
+
129
+ const newUser = { name: 'Bob', age: 25 }
130
+ act(() => store.set({ user: newUser }))
131
+ expect(result.current.user).toBe(newUser)
132
+ })
133
+
134
+ it('works with null and undefined values', () => {
135
+ const store = createStoreState<{ v: string | null }>({ v: null })
136
+ const { result } = renderHook(() => useStoreSelector(store, ['v']))
137
+
138
+ expect(result.current.v).toBeNull()
139
+
140
+ act(() => store.set({ v: 'hello' }))
141
+ expect(result.current.v).toBe('hello')
142
+
143
+ act(() => store.set({ v: null }))
144
+ expect(result.current.v).toBeNull()
145
+ })
146
+ })
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // createSelectorHook
150
+ // ---------------------------------------------------------------------------
151
+ describe('createSelectorHook', () => {
152
+ it('returns a hook that works like useStoreSelector', () => {
153
+ const store = createStoreState({ count: 0, name: 'Alice' })
154
+ const useStore = createSelectorHook(store)
155
+
156
+ const { result } = renderHook(() => useStore(['count', 'name']))
157
+ expect(result.current).toEqual({ count: 0, name: 'Alice' })
158
+ })
159
+
160
+ it('re-renders on subscribed key change', () => {
161
+ const store = createStoreState({ count: 0 })
162
+ const useStore = createSelectorHook(store)
163
+
164
+ const { result } = renderHook(() => useStore(['count']))
165
+
166
+ act(() => store.set({ count: 42 }))
167
+ expect(result.current.count).toBe(42)
168
+ })
169
+
170
+ it('does not re-render on unrelated key change', () => {
171
+ const store = createStoreState({ a: 1, b: 2 })
172
+ const useStore = createSelectorHook(store)
173
+ const renderCount = vi.fn()
174
+
175
+ renderHook(() => {
176
+ renderCount()
177
+ return useStore(['a'])
178
+ })
179
+
180
+ const callsAfterMount = renderCount.mock.calls.length
181
+
182
+ act(() => store.set({ b: 99 }))
183
+
184
+ expect(renderCount.mock.calls.length).toBe(callsAfterMount)
185
+ })
186
+
187
+ it('supports custom comparison functions', () => {
188
+ const store = createStoreState({ data: [1, 2] })
189
+ const useStore = createSelectorHook(store)
190
+ const renderCount = vi.fn()
191
+
192
+ renderHook(() => {
193
+ renderCount()
194
+ return useStore([
195
+ { data: (prev: number[], next: number[]) => prev.length === next.length },
196
+ ])
197
+ })
198
+
199
+ const callsAfterMount = renderCount.mock.calls.length
200
+
201
+ act(() => store.set({ data: [3, 4] })) // same length
202
+ expect(renderCount.mock.calls.length).toBe(callsAfterMount)
203
+
204
+ act(() => store.set({ data: [1] })) // different length
205
+ expect(renderCount.mock.calls.length).toBeGreaterThan(callsAfterMount)
206
+ })
207
+
208
+ it('multiple hooks on the same store work independently', () => {
209
+ const store = createStoreState({ a: 1, b: 2 })
210
+ const useStore = createSelectorHook(store)
211
+
212
+ const { result: r1 } = renderHook(() => useStore(['a']))
213
+ const { result: r2 } = renderHook(() => useStore(['b']))
214
+
215
+ act(() => store.set({ a: 10 }))
216
+
217
+ expect(r1.current.a).toBe(10)
218
+ expect(r2.current.b).toBe(2)
219
+ })
220
+ })
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Edge cases
224
+ // ---------------------------------------------------------------------------
225
+ describe('edge cases', () => {
226
+ it('useStoreSelector with middleware that blocks — hook reflects blocked state', () => {
227
+ const store = createStoreState({ count: 0 })
228
+ store.addMiddleware((_state, update, next) => {
229
+ if (update.count !== undefined && update.count < 0) return // block negatives
230
+ next()
231
+ })
232
+
233
+ const { result } = renderHook(() => useStoreSelector(store, ['count']))
234
+
235
+ act(() => store.set({ count: 5 }))
236
+ expect(result.current.count).toBe(5)
237
+
238
+ act(() => store.set({ count: -1 })) // blocked
239
+ expect(result.current.count).toBe(5)
240
+ })
241
+
242
+ it('useStoreSelector with middleware that transforms', () => {
243
+ const store = createStoreState({ name: '' })
244
+ store.addMiddleware((_state, update, next) => {
245
+ if (update.name) next({ name: update.name.trim() })
246
+ else next()
247
+ })
248
+
249
+ const { result } = renderHook(() => useStoreSelector(store, ['name']))
250
+
251
+ act(() => store.set({ name: ' hello ' }))
252
+ expect(result.current.name).toBe('hello')
253
+ })
254
+
255
+ it('store works correctly after many subscribe/unsubscribe cycles', () => {
256
+ const store = createStoreState({ v: 0 })
257
+ const unsubs: (() => void)[] = []
258
+
259
+ for (let i = 0; i < 100; i++) {
260
+ unsubs.push(store.subscribe(['v'], () => {}))
261
+ }
262
+ for (const unsub of unsubs) {
263
+ unsub()
264
+ }
265
+
266
+ const listener = vi.fn()
267
+ store.subscribe(['v'], listener)
268
+ store.set({ v: 1 })
269
+ expect(listener).toHaveBeenCalledTimes(1)
270
+ })
271
+ })