@plasius/react-state 1.0.10 → 1.0.11
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.cjs +219 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +59 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +187 -0
- package/dist/index.js.map +1 -0
- package/package.json +18 -6
- package/.eslintrc.cjs +0 -7
- package/.github/workflows/cd.yml +0 -186
- package/.github/workflows/ci.yml +0 -16
- package/.nvmrc +0 -1
- package/.vscode/launch.json +0 -15
- package/CHANGELOG.md +0 -72
- package/CODE_OF_CONDUCT.md +0 -79
- package/CONTRIBUTING.md +0 -201
- package/CONTRIBUTORS.md +0 -27
- package/SECURITY.md +0 -17
- package/docs/adrs/adr-0001: React store.md +0 -42
- package/docs/adrs/adr-template.md +0 -65
- package/legal/CLA-REGISTRY.csv +0 -2
- package/legal/CLA.md +0 -22
- package/legal/CORPORATE_CLA.md +0 -57
- package/legal/INDIVIDUAL_CLA.md +0 -91
- package/sbom.cdx.json +0 -66
- package/src/create-scoped-store.tsx +0 -87
- package/src/index.ts +0 -5
- package/src/metadata-store.ts +0 -24
- package/src/provider.tsx +0 -52
- package/src/store.ts +0 -131
- package/src/types.ts +0 -3
- package/tests/scoped.store.test.tsx +0 -75
- package/tests/store.core.test.ts +0 -295
- package/tsconfig.build.json +0 -20
- package/tsconfig.json +0 -7
- package/tsup.config.ts +0 -10
- package/vitest.config.js +0 -20
|
@@ -1,75 +0,0 @@
|
|
|
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
|
-
});
|
package/tests/store.core.test.ts
DELETED
|
@@ -1,295 +0,0 @@
|
|
|
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
|
-
});
|
package/tsconfig.build.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
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
|
-
}
|
package/tsconfig.json
DELETED
package/tsup.config.ts
DELETED
package/vitest.config.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "vitest/config";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
environment: "jsdom",
|
|
6
|
-
globals: true,
|
|
7
|
-
include: ["tests/**/*.test.{ts,tsx}"],
|
|
8
|
-
coverage: {
|
|
9
|
-
provider: "v8",
|
|
10
|
-
reporter: ["text", "lcov"],
|
|
11
|
-
reportsDirectory: "./coverage",
|
|
12
|
-
exclude: [
|
|
13
|
-
"tests/**",
|
|
14
|
-
"dist/**",
|
|
15
|
-
"**/*.config.{js,ts}",
|
|
16
|
-
"**/.eslintrc.{js,cjs}",
|
|
17
|
-
],
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
});
|