@plasius/react-state 1.0.1

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.
@@ -0,0 +1,87 @@
1
+ import { createContext, useContext, useRef, useSyncExternalStore } from "react";
2
+ import type { IState, IAction, Store } from "./store.js";
3
+ import { createStore } from "./store.js";
4
+
5
+ function shallowEqual(a: any, b: any) {
6
+ if (Object.is(a, b)) return true;
7
+ if (
8
+ typeof a !== "object" ||
9
+ a === null ||
10
+ typeof b !== "object" ||
11
+ b === null
12
+ )
13
+ return false;
14
+ const ak = Object.keys(a),
15
+ bk = Object.keys(b);
16
+ if (ak.length !== bk.length) return false;
17
+ for (let i = 0; i < ak.length; i++) {
18
+ const k = ak[i] as string;
19
+ if (
20
+ !Object.prototype.hasOwnProperty.call(b, k) ||
21
+ !Object.is((a as any)[k], (b as any)[k])
22
+ )
23
+ return false;
24
+ }
25
+ return true;
26
+ }
27
+
28
+ export function createScopedStoreContext<S extends IState, A extends IAction>(
29
+ reducer: (state: S, action: A) => S,
30
+ initialState: S
31
+ ) {
32
+ const Context = createContext<Store<S, A> | null>(null);
33
+
34
+ const store = createStore(reducer, initialState);
35
+
36
+ const Provider = ({ children }: { children: React.ReactNode }) => (
37
+ <Context.Provider value={store}>{children}</Context.Provider>
38
+ );
39
+
40
+ const useStore = (): S => {
41
+ const ctx = useContext(Context);
42
+ if (!ctx) throw new Error("Store not found in context");
43
+ return useSyncExternalStore(ctx.subscribe, ctx.getState, ctx.getState);
44
+ };
45
+
46
+ const useDispatch = (): ((action: A) => void) => {
47
+ const ctx = useContext(Context);
48
+ if (!ctx) throw new Error("Dispatch not found in context");
49
+ return (action: A) => ctx.dispatch(action);
50
+ };
51
+
52
+ function useSelector<T>(
53
+ selector: (state: S) => T,
54
+ isEqual: (a: T, b: T) => boolean = shallowEqual
55
+ ): T {
56
+ const ctx = useContext(Context);
57
+ if (!ctx) throw new Error("Store not found in context");
58
+
59
+ // Subscribe to the raw state snapshot (stable reference until a dispatch)
60
+ const state = useSyncExternalStore(
61
+ ctx.subscribe,
62
+ ctx.getState,
63
+ ctx.getState
64
+ );
65
+
66
+ // Cache the selected slice per state snapshot to avoid returning fresh objects during render
67
+ const lastRef = useRef<{ state: S; selected: T } | null>(null);
68
+ const last = lastRef.current;
69
+ const nextSelected = selector(state);
70
+
71
+ if (last && last.state === state && isEqual(last.selected, nextSelected)) {
72
+ return last.selected; // return cached reference to satisfy getSnapshot caching
73
+ }
74
+
75
+ lastRef.current = { state, selected: nextSelected };
76
+ return nextSelected;
77
+ }
78
+
79
+ return {
80
+ store,
81
+ Context,
82
+ Provider,
83
+ useStore,
84
+ useDispatch,
85
+ useSelector,
86
+ };
87
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./types.js";
2
+ export * from "./create-scoped-store.js";
3
+ export * from "./store.js";
4
+ export * from "./provider.js";
5
+ export * from "./metadata-store.js";
@@ -0,0 +1,24 @@
1
+ // metadata-store.ts
2
+ export class MetadataStore<T extends object, Meta extends object> {
3
+ private readonly symbol: symbol;
4
+
5
+ constructor(description: string) {
6
+ this.symbol = Symbol(description);
7
+ }
8
+
9
+ set(target: T, meta: Meta) {
10
+ Object.defineProperty(target, this.symbol as PropertyKey, {
11
+ value: meta,
12
+ writable: false,
13
+ enumerable: false,
14
+ });
15
+ }
16
+
17
+ get(target: T): Meta | undefined {
18
+ return (target as Record<PropertyKey, Meta>)[this.symbol as PropertyKey];
19
+ }
20
+
21
+ has(target: T): boolean {
22
+ return (this.symbol as PropertyKey) in target;
23
+ }
24
+ }
@@ -0,0 +1,52 @@
1
+ import React, { createContext, useContext, useEffect, useState } from "react";
2
+ import type { ReactNode } from "react";
3
+ import type { Store, IState, IAction } from "./store.js";
4
+
5
+ const StoreContext = createContext<Store<IState, IAction> | undefined>(undefined);
6
+
7
+ function useStoreInstance<S extends IState, A extends IAction>(): Store<S, A> {
8
+ const store = useContext(StoreContext) as Store<S, A> | undefined;
9
+ if (!store) {
10
+ throw new Error(
11
+ "StoreProvider is missing in the React tree. Wrap your app with <StoreProvider store={...}>."
12
+ );
13
+ }
14
+ return store;
15
+ }
16
+
17
+ interface StoreProviderProps<S extends IState, A extends IAction> {
18
+ store: Store<S, A>;
19
+ children: ReactNode;
20
+ }
21
+
22
+ export function StoreProvider<S extends IState, A extends IAction>({
23
+ store,
24
+ children,
25
+ }: StoreProviderProps<S, A>) {
26
+ return (
27
+ <StoreContext.Provider value={store as unknown as Store<IState, IAction>}>
28
+ {children}
29
+ </StoreContext.Provider>
30
+ );
31
+ }
32
+
33
+ export function useStore<S extends IState>(): S {
34
+ const store = useStoreInstance<S, IAction>();
35
+ const [state, setState] = useState<S>(() => store.getState());
36
+
37
+ useEffect(() => {
38
+ // Subscribe to store changes and update local state.
39
+ const unsubscribe = store.subscribe(() => {
40
+ setState(store.getState());
41
+ });
42
+ return unsubscribe;
43
+ }, [store]);
44
+
45
+ return state;
46
+ }
47
+
48
+ export function useDispatch<A extends IAction>(): Store<IState, A>["dispatch"] {
49
+ const store = useStoreInstance<IState, A>();
50
+ // Return the store's dispatch directly; consumers can call dispatch(action).
51
+ return store.dispatch as Store<IState, A>["dispatch"];
52
+ }
package/src/store.ts ADDED
@@ -0,0 +1,131 @@
1
+ import type { Reducer, Listener } from "./types.js";
2
+
3
+ // Allow narrower parameter types for callbacks without fighting variance
4
+ type BivariantListener<T> = {
5
+ bivarianceHack(value: T): void;
6
+ }["bivarianceHack"];
7
+
8
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
9
+ export interface IState {}
10
+ export interface IAction {
11
+ type: string;
12
+ }
13
+
14
+ export interface Store<S extends IState, A extends IAction> {
15
+ getState(): S;
16
+ dispatch(action: A): void;
17
+ /**
18
+ * Subscribe to all state changes.
19
+ */
20
+ subscribe(listener: Listener): () => void;
21
+ /**
22
+ * Subscribe to changes of a specific key in the state.
23
+ */
24
+ subscribeToKey<K extends keyof S>(
25
+ key: K,
26
+ listener: (value: S[K]) => void
27
+ ): () => void;
28
+ /**
29
+ * Subscribe to changes in a selected value from the state.
30
+ */
31
+ subscribeWithSelector<T>(
32
+ selector: (state: S) => T,
33
+ listener: (selected: T) => void
34
+ ): () => void;
35
+ }
36
+
37
+ export function createStore<S extends IState, A extends IAction>(
38
+ reducer: Reducer<S, A>,
39
+ initialState: S
40
+ ): Store<S, A> {
41
+ let state = initialState;
42
+ const listeners = new Set<Listener>();
43
+ const keyListeners = new Map<keyof S, Set<BivariantListener<S[keyof S]>>>();
44
+
45
+ interface SelectorEntry<T> {
46
+ selector: (state: S) => T;
47
+ listener: BivariantListener<T>;
48
+ lastValue: T;
49
+ }
50
+ const selectorListeners = new Set<SelectorEntry<unknown>>();
51
+
52
+ const getState = () => state;
53
+
54
+ const dispatch = (action: A) => {
55
+ const prevState = state;
56
+ const nextState = reducer(state, action);
57
+
58
+ // Distinct-until-changed: if the reducer returns the same reference,
59
+ // skip all notifications (prevents unnecessary re-renders).
60
+ if (Object.is(prevState, nextState)) {
61
+ state = nextState; // keep any identity guarantees from reducer
62
+ return;
63
+ }
64
+
65
+ state = nextState;
66
+
67
+ // Notify global listeners (iterate over a snapshot so unsubscribe during
68
+ // notify does not skip the next listener)
69
+ for (const listener of [...listeners]) listener();
70
+
71
+ // Notify key listeners only when that key actually changed (Object.is)
72
+ for (const [key, set] of keyListeners.entries()) {
73
+ if (!Object.is(prevState[key], state[key])) {
74
+ for (const listener of [...set]) listener(state[key]);
75
+ }
76
+ }
77
+
78
+ // Notify selector listeners only when selected value changed (Object.is)
79
+ selectorListeners.forEach((entry) => {
80
+ const nextValue = (entry.selector as (s: S) => unknown)(state);
81
+ if (!Object.is(entry.lastValue, nextValue)) {
82
+ entry.lastValue = nextValue as unknown;
83
+ (entry.listener as (v: unknown) => void)(nextValue);
84
+ }
85
+ });
86
+ };
87
+
88
+ const subscribe = (listener: Listener) => {
89
+ listeners.add(listener);
90
+ return () => {
91
+ listeners.delete(listener);
92
+ };
93
+ };
94
+
95
+ const subscribeToKey = <K extends keyof S>(
96
+ key: K,
97
+ listener: (value: S[K]) => void
98
+ ) => {
99
+ const set =
100
+ keyListeners.get(key) ?? new Set<BivariantListener<S[keyof S]>>();
101
+ set.add(listener as unknown as BivariantListener<S[keyof S]>);
102
+ keyListeners.set(key, set);
103
+ return () => {
104
+ set.delete(listener as unknown as BivariantListener<S[keyof S]>);
105
+ if (set.size === 0) keyListeners.delete(key);
106
+ };
107
+ };
108
+
109
+ const subscribeWithSelector = <T>(
110
+ selector: (state: S) => T,
111
+ listener: (selected: T) => void
112
+ ) => {
113
+ const entry: SelectorEntry<T> = {
114
+ selector,
115
+ listener: listener as BivariantListener<T>,
116
+ lastValue: selector(state),
117
+ };
118
+ selectorListeners.add(entry as unknown as SelectorEntry<unknown>);
119
+ return () => {
120
+ selectorListeners.delete(entry as unknown as SelectorEntry<unknown>);
121
+ };
122
+ };
123
+
124
+ return {
125
+ getState,
126
+ dispatch,
127
+ subscribe,
128
+ subscribeToKey,
129
+ subscribeWithSelector,
130
+ };
131
+ }
package/src/types.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type Reducer<S, A> = (state: S, action: A) => S;
2
+ export type Listener = () => void;
3
+ export type Unsubscribe = () => void;
@@ -0,0 +1,75 @@
1
+ import React from "react"; // tests/setup.ts
2
+ import "@testing-library/jest-dom";
3
+ import { render, fireEvent } from "@testing-library/react";
4
+ import { describe, it, expect } from "vitest";
5
+ import { createScopedStoreContext } from "../src";
6
+
7
+ describe("scoped store", () => {
8
+ const initialState = { count: 0 };
9
+ const reducer = (
10
+ state: typeof initialState,
11
+ action: { type: string; payload?: number }
12
+ ) => {
13
+ switch (action.type) {
14
+ case "inc":
15
+ return { count: state.count + 1 };
16
+ case "dec":
17
+ return { count: state.count - 1 };
18
+ case "set":
19
+ return { count: action.payload ?? state.count };
20
+ default:
21
+ return state;
22
+ }
23
+ };
24
+
25
+ const store = createScopedStoreContext(reducer, initialState);
26
+
27
+ const Counter = () => {
28
+ const state = store.useStore();
29
+ const dispatch = store.useDispatch();
30
+
31
+ return (
32
+ <div>
33
+ <button id="counter-inc" onClick={() => dispatch({ type: "inc" })}>
34
+ +
35
+ </button>
36
+ <button id="counter-dec" onClick={() => dispatch({ type: "dec" })}>
37
+ -
38
+ </button>
39
+ <input
40
+ aria-label="Counter value"
41
+ title=""
42
+ id="counter-set"
43
+ type="number"
44
+ value={state.count}
45
+ onChange={(e) =>
46
+ dispatch({ type: "set", payload: Number(e.target.value) })
47
+ }
48
+ />
49
+ </div>
50
+ );
51
+ };
52
+
53
+ it("increments, decrements, and sets the counter correctly", () => {
54
+ const { getByText, getByLabelText } = render(
55
+ <store.Provider>
56
+ <Counter />
57
+ </store.Provider>
58
+ );
59
+
60
+ const incButton = getByText("+");
61
+ const decButton = getByText("-");
62
+ const input = getByLabelText("Counter value") as HTMLInputElement;
63
+
64
+ expect(input.value).toBe("0");
65
+
66
+ fireEvent.click(incButton);
67
+ expect(input.value).toBe("1");
68
+
69
+ fireEvent.click(decButton);
70
+ expect(input.value).toBe("0");
71
+
72
+ fireEvent.change(input, { target: { value: "5" } });
73
+ expect(input.value).toBe("5");
74
+ });
75
+ });
@@ -0,0 +1,295 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import {
3
+ createStore,
4
+ type Store,
5
+ type IState,
6
+ type IAction,
7
+ } from "../src/store";
8
+
9
+ type S = { count: number; meta?: { tag: string } };
10
+ type A =
11
+ | { type: "inc" }
12
+ | { type: "set"; value: number }
13
+ | { type: "setMeta"; tag: string }
14
+ | { type: "noop" };
15
+
16
+ const reducer = (s: S, a: A): S => {
17
+ switch (a.type) {
18
+ case "inc":
19
+ return { ...s, count: s.count + 1 };
20
+ case "set":
21
+ return { ...s, count: a.value };
22
+ case "setMeta":
23
+ return { ...s, meta: { tag: a.tag } };
24
+ case "noop":
25
+ return s; // return same reference to simulate no-change dispatch
26
+ default:
27
+ return s;
28
+ }
29
+ };
30
+
31
+ const initial: S = { count: 0, meta: { tag: "a" } };
32
+
33
+ describe("createStore – basics", () => {
34
+ it("getState returns initial; dispatch updates", () => {
35
+ const store = createStore<S, A>(reducer, initial);
36
+ expect(store.getState()).toEqual(initial);
37
+ store.dispatch({ type: "inc" });
38
+ expect(store.getState().count).toBe(1);
39
+ });
40
+ });
41
+
42
+ describe("subscribe (global)", () => {
43
+ it("fires only when state reference changes (distinct-until-changed)", () => {
44
+ const store = createStore<S, A>(reducer, initial);
45
+ const cb = vi.fn();
46
+ const un = store.subscribe(cb);
47
+
48
+ store.dispatch({ type: "noop" }); // same reference → no notify
49
+ store.dispatch({ type: "inc" }); // changed → notify once
50
+
51
+ expect(cb).toHaveBeenCalledTimes(1);
52
+ un();
53
+ store.dispatch({ type: "inc" });
54
+ expect(cb).toHaveBeenCalledTimes(1);
55
+ });
56
+ });
57
+
58
+ describe("subscribeToKey (per-key)", () => {
59
+ it("notifies only when that key changes", () => {
60
+ const store = createStore<S, A>(reducer, initial);
61
+ const cb = vi.fn();
62
+ const un = store.subscribeToKey("count", cb);
63
+
64
+ store.dispatch({ type: "setMeta", tag: "b" }); // different key
65
+ expect(cb).not.toHaveBeenCalled();
66
+
67
+ store.dispatch({ type: "inc" });
68
+ expect(cb).toHaveBeenCalledTimes(1);
69
+ expect(cb).toHaveBeenLastCalledWith(1);
70
+
71
+ un();
72
+ store.dispatch({ type: "inc" });
73
+ expect(cb).toHaveBeenCalledTimes(1);
74
+ });
75
+
76
+ it("removes empty key sets when last listener unsubscribes", () => {
77
+ const store = createStore<S, A>(reducer, initial);
78
+ const cb1 = vi.fn();
79
+ const cb2 = vi.fn();
80
+
81
+ const un1 = store.subscribeToKey("count", cb1);
82
+ const un2 = store.subscribeToKey("count", cb2);
83
+
84
+ un1();
85
+ un2();
86
+ // We can't directly inspect keyListeners map, but we can assert no leaks by re-subscribing and ensuring it still works.
87
+ const cb3 = vi.fn();
88
+ const un3 = store.subscribeToKey("count", cb3);
89
+ store.dispatch({ type: "inc" });
90
+ expect(cb3).toHaveBeenCalledTimes(1);
91
+ un3();
92
+ });
93
+ });
94
+
95
+ describe("subscribeWithSelector (derived changes)", () => {
96
+ it("does not call immediately; calls when selected value changes", () => {
97
+ const store = createStore<S, A>(reducer, initial);
98
+ const sel = (s: S) => s.count;
99
+ const cb = vi.fn();
100
+
101
+ const un = store.subscribeWithSelector(sel, cb);
102
+ expect(cb).not.toHaveBeenCalled();
103
+
104
+ store.dispatch({ type: "inc" });
105
+ expect(cb).toHaveBeenCalledTimes(1);
106
+ expect(cb).toHaveBeenLastCalledWith(1);
107
+
108
+ un();
109
+ });
110
+
111
+ it("does not call if selector result is referentially equal", () => {
112
+ const store = createStore<S, A>(reducer, { count: 0, meta: { tag: "x" } });
113
+ const sel = (s: S) => s.meta?.tag; // primitive equality
114
+ const cb = vi.fn();
115
+
116
+ const un = store.subscribeWithSelector(sel, cb);
117
+ store.dispatch({ type: "setMeta", tag: "x" }); // new object, same tag
118
+ expect(cb).not.toHaveBeenCalled();
119
+
120
+ store.dispatch({ type: "setMeta", tag: "y" });
121
+ expect(cb).toHaveBeenCalledTimes(1);
122
+ expect(cb).toHaveBeenLastCalledWith("y");
123
+ un();
124
+ });
125
+ });
126
+
127
+ describe("notification order", () => {
128
+ it("global → key → selector", () => {
129
+ const store = createStore<S, A>(reducer, initial);
130
+ const calls: string[] = [];
131
+
132
+ const unG = store.subscribe(() => calls.push("global"));
133
+ const unK = store.subscribeToKey("count", () => calls.push("key"));
134
+ const unS = store.subscribeWithSelector(
135
+ (s) => s.count,
136
+ () => calls.push("selector")
137
+ );
138
+
139
+ store.dispatch({ type: "inc" });
140
+
141
+ expect(calls).toEqual(["global", "key", "selector"]);
142
+ unG();
143
+ unK();
144
+ unS();
145
+ });
146
+ });
147
+
148
+ describe("mutation during iteration (edge case)", () => {
149
+ it("unsubscribing within a global listener may skip the next listener (document current behavior)", () => {
150
+ const store = createStore<S, A>(reducer, initial);
151
+ const log: string[] = [];
152
+
153
+ let un1: () => void = () => {};
154
+ const l1 = () => {
155
+ log.push("l1");
156
+ un1();
157
+ }; // removes itself during dispatch
158
+ const l2 = () => {
159
+ log.push("l2");
160
+ };
161
+
162
+ un1 = store.subscribe(l1);
163
+ store.subscribe(l2);
164
+
165
+ store.dispatch({ type: "inc" });
166
+
167
+ // Depending on Array#forEach semantics with splice, l2 might be skipped.
168
+ // Document current behavior so a future change (copy-before-iterate) can flip this expectation.
169
+ expect(log.length === 1 || log.length === 2).toBe(true);
170
+ });
171
+ });
172
+
173
+ describe("no-change dispatch side-effects", () => {
174
+ it("global does not fire; key/selector do not", () => {
175
+ const store = createStore<S, A>(reducer, initial);
176
+ const g = vi.fn();
177
+ const k = vi.fn();
178
+ const s = vi.fn();
179
+
180
+ store.subscribe(g);
181
+ store.subscribeToKey("count", k);
182
+ store.subscribeWithSelector((st) => st.count, s);
183
+
184
+ store.dispatch({ type: "noop" }); // same reference returned → no notifications
185
+
186
+ expect(g).toHaveBeenCalledTimes(0);
187
+ expect(k).not.toHaveBeenCalled();
188
+ expect(s).not.toHaveBeenCalled();
189
+ });
190
+ });
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Additional coverage for core store semantics
194
+
195
+ describe("state identity & no-op behavior", () => {
196
+ it("returns the same state reference after a no-op and a new reference after a change", () => {
197
+ const store = createStore<S, A>(reducer, initial);
198
+ const s1 = store.getState();
199
+ store.dispatch({ type: "noop" });
200
+ const s2 = store.getState();
201
+ expect(s2).toBe(s1); // identity equal on no-op
202
+
203
+ store.dispatch({ type: "inc" });
204
+ const s3 = store.getState();
205
+ expect(s3).not.toBe(s2); // new object on change
206
+ });
207
+ });
208
+
209
+ describe("unsubscribe semantics", () => {
210
+ it("global unsubscribe stops further notifications", () => {
211
+ const store = createStore<S, A>(reducer, initial);
212
+ const cb = vi.fn();
213
+ const un = store.subscribe(cb);
214
+
215
+ store.dispatch({ type: "inc" });
216
+ expect(cb).toHaveBeenCalledTimes(1);
217
+ un();
218
+ store.dispatch({ type: "inc" });
219
+ expect(cb).toHaveBeenCalledTimes(1);
220
+ });
221
+
222
+ it("per-key unsubscribe stops further notifications", () => {
223
+ const store = createStore<S, A>(reducer, initial);
224
+ const cb = vi.fn();
225
+ const un = store.subscribeToKey("count", cb);
226
+
227
+ store.dispatch({ type: "inc" });
228
+ expect(cb).toHaveBeenCalledTimes(1);
229
+ un();
230
+ store.dispatch({ type: "inc" });
231
+ expect(cb).toHaveBeenCalledTimes(1);
232
+ });
233
+
234
+ it("selector unsubscribe stops further notifications", () => {
235
+ const store = createStore<S, A>(reducer, initial);
236
+ const cb = vi.fn();
237
+ const un = store.subscribeWithSelector((s) => s.count, cb);
238
+
239
+ store.dispatch({ type: "inc" });
240
+ expect(cb).toHaveBeenCalledTimes(1);
241
+ un();
242
+ store.dispatch({ type: "inc" });
243
+ expect(cb).toHaveBeenCalledTimes(1);
244
+ });
245
+ });
246
+
247
+ describe("multiple subscribers", () => {
248
+ it("notifies all global subscribers once per change", () => {
249
+ const store = createStore<S, A>(reducer, initial);
250
+ const a = vi.fn();
251
+ const b = vi.fn();
252
+ store.subscribe(a);
253
+ store.subscribe(b);
254
+
255
+ store.dispatch({ type: "inc" });
256
+
257
+ expect(a).toHaveBeenCalledTimes(1);
258
+ expect(b).toHaveBeenCalledTimes(1);
259
+ });
260
+ });
261
+
262
+ describe("mutation during iteration – per-key/selector", () => {
263
+ it("unsubscribing within a per-key listener may skip the next listener (document current behavior)", () => {
264
+ const store = createStore<S, A>(reducer, initial);
265
+ const log: string[] = [];
266
+
267
+ let un1: () => void = () => {};
268
+ const l1 = () => { log.push("k1"); un1(); };
269
+ const l2 = () => { log.push("k2"); };
270
+
271
+ un1 = store.subscribeToKey("count", l1);
272
+ store.subscribeToKey("count", l2);
273
+
274
+ store.dispatch({ type: "inc" });
275
+
276
+ // Depending on iteration strategy, l2 may be skipped when l1 unsubscribes.
277
+ expect(log.length === 1 || log.length === 2).toBe(true);
278
+ });
279
+
280
+ it("unsubscribing within a selector listener may skip the next listener (document current behavior)", () => {
281
+ const store = createStore<S, A>(reducer, initial);
282
+ const log: string[] = [];
283
+
284
+ let un1: () => void = () => {};
285
+ const l1 = () => { log.push("s1"); un1(); };
286
+ const l2 = () => { log.push("s2"); };
287
+
288
+ un1 = store.subscribeWithSelector((s) => s.count, l1);
289
+ store.subscribeWithSelector((s) => s.count, l2);
290
+
291
+ store.dispatch({ type: "inc" });
292
+
293
+ expect(log.length === 1 || log.length === 2).toBe(true);
294
+ });
295
+ });
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "NodeNext",
5
+ "lib": ["ESNext", "DOM"],
6
+ "moduleResolution": "NodeNext",
7
+ "jsx": "react-jsx",
8
+ "types": ["node", "vitest"],
9
+ "declaration": true,
10
+ "sourceMap": true,
11
+ "strict": true,
12
+ "noUncheckedIndexedAccess": true,
13
+ "noImplicitOverride": true,
14
+ "noFallthroughCasesInSwitch": true,
15
+ "resolveJsonModule": true,
16
+ "outDir": "dist",
17
+ "forceConsistentCasingInFileNames": true
18
+ },
19
+ "include": ["src"]
20
+ }