@storve/core 1.0.1 → 1.0.3
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/LICENSE +21 -0
- package/README.md +993 -26
- package/dist/adapters/indexedDB.cjs +0 -1
- package/dist/adapters/indexedDB.mjs +0 -1
- package/dist/adapters/localStorage.cjs +0 -1
- package/dist/adapters/localStorage.mjs +0 -1
- package/dist/adapters/memory.cjs +0 -1
- package/dist/adapters/memory.mjs +0 -1
- package/dist/adapters/sessionStorage.cjs +0 -1
- package/dist/adapters/sessionStorage.mjs +0 -1
- package/dist/async-entry.d.ts +0 -1
- package/dist/async.cjs +0 -1
- package/dist/async.d.ts +0 -1
- package/dist/async.mjs +0 -1
- package/dist/batch.d.ts +0 -1
- package/dist/compose.d.ts +0 -1
- package/dist/computed-entry.d.ts +0 -1
- package/dist/computed.cjs +0 -1
- package/dist/computed.d.ts +0 -1
- package/dist/computed.mjs +0 -1
- package/dist/devtools/history.d.ts +0 -1
- package/dist/devtools/index.d.ts +0 -1
- package/dist/devtools/redux-bridge.d.ts +0 -1
- package/dist/devtools/snapshots.d.ts +0 -1
- package/dist/devtools/withDevtools.d.ts +0 -1
- package/dist/devtools.cjs +0 -1
- package/dist/devtools.mjs +0 -1
- package/dist/extensions/noop.d.ts +0 -1
- package/dist/index.cjs +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +0 -1
- package/dist/persist/adapters/indexedDB.d.ts +0 -1
- package/dist/persist/adapters/localStorage.d.ts +0 -1
- package/dist/persist/adapters/memory.d.ts +0 -1
- package/dist/persist/adapters/sessionStorage.d.ts +0 -1
- package/dist/persist/debounce.d.ts +0 -1
- package/dist/persist/hydrate.d.ts +0 -1
- package/dist/persist/index.d.ts +0 -1
- package/dist/persist/serialize.d.ts +0 -1
- package/dist/persist.cjs +0 -1
- package/dist/persist.mjs +0 -1
- package/dist/proxy.d.ts +0 -1
- package/dist/registry-qtr1UpFU.js +0 -1
- package/dist/registry-zaKZ1P-s.js +0 -1
- package/dist/registry.d.ts +0 -1
- package/dist/signals/createSignal.d.ts +0 -1
- package/dist/signals/index.d.ts +0 -1
- package/dist/signals/useSignal.d.ts +0 -1
- package/dist/signals.cjs +0 -1
- package/dist/signals.mjs +0 -1
- package/dist/store.d.ts +0 -1
- package/dist/sync/channel.d.ts +0 -1
- package/dist/sync/index.d.ts +0 -1
- package/dist/sync/protocol.d.ts +0 -1
- package/dist/sync/withSync.d.ts +0 -1
- package/dist/sync.cjs +0 -1
- package/dist/sync.mjs +0 -1
- package/dist/types.d.ts +0 -1
- package/package.json +9 -3
- package/CHANGELOG.md +0 -151
- package/benchmarks/run.ts +0 -102
- package/benchmarks/week2.md +0 -9
- package/benchmarks/week2.ts +0 -64
- package/benchmarks/week4.md +0 -13
- package/benchmarks/week4.ts +0 -178
- package/benchmarks/week5.md +0 -15
- package/benchmarks/week5.ts +0 -184
- package/coverage/coverage-summary.json +0 -31
- package/dist/adapters/indexedDB.cjs.map +0 -1
- package/dist/adapters/indexedDB.mjs.map +0 -1
- package/dist/adapters/localStorage.cjs.map +0 -1
- package/dist/adapters/localStorage.mjs.map +0 -1
- package/dist/adapters/memory.cjs.map +0 -1
- package/dist/adapters/memory.mjs.map +0 -1
- package/dist/adapters/sessionStorage.cjs.map +0 -1
- package/dist/adapters/sessionStorage.mjs.map +0 -1
- package/dist/async-entry.d.ts.map +0 -1
- package/dist/async.cjs.map +0 -1
- package/dist/async.d.ts.map +0 -1
- package/dist/async.mjs.map +0 -1
- package/dist/batch.d.ts.map +0 -1
- package/dist/compose.d.ts.map +0 -1
- package/dist/computed-entry.d.ts.map +0 -1
- package/dist/computed.cjs.map +0 -1
- package/dist/computed.d.ts.map +0 -1
- package/dist/computed.mjs.map +0 -1
- package/dist/devtools/history.d.ts.map +0 -1
- package/dist/devtools/index.d.ts.map +0 -1
- package/dist/devtools/redux-bridge.d.ts.map +0 -1
- package/dist/devtools/snapshots.d.ts.map +0 -1
- package/dist/devtools/withDevtools.d.ts.map +0 -1
- package/dist/devtools.cjs.map +0 -1
- package/dist/devtools.mjs.map +0 -1
- package/dist/extensions/noop.d.ts.map +0 -1
- package/dist/index.cjs.js +0 -118
- package/dist/index.cjs.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.esm.js +0 -116
- package/dist/index.esm.js.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/persist/adapters/indexedDB.d.ts.map +0 -1
- package/dist/persist/adapters/localStorage.d.ts.map +0 -1
- package/dist/persist/adapters/memory.d.ts.map +0 -1
- package/dist/persist/adapters/sessionStorage.d.ts.map +0 -1
- package/dist/persist/debounce.d.ts.map +0 -1
- package/dist/persist/hydrate.d.ts.map +0 -1
- package/dist/persist/index.d.ts.map +0 -1
- package/dist/persist/serialize.d.ts.map +0 -1
- package/dist/persist.cjs.map +0 -1
- package/dist/persist.mjs.map +0 -1
- package/dist/proxy.d.ts.map +0 -1
- package/dist/registry-D3X0HSbl.js +0 -26
- package/dist/registry-D3X0HSbl.js.map +0 -1
- package/dist/registry-RDjbeJdx.js +0 -29
- package/dist/registry-RDjbeJdx.js.map +0 -1
- package/dist/registry-qtr1UpFU.js.map +0 -1
- package/dist/registry-zaKZ1P-s.js.map +0 -1
- package/dist/registry.d.ts.map +0 -1
- package/dist/signals/createSignal.d.ts.map +0 -1
- package/dist/signals/index.d.ts.map +0 -1
- package/dist/signals/useSignal.d.ts.map +0 -1
- package/dist/signals.cjs.map +0 -1
- package/dist/signals.mjs.map +0 -1
- package/dist/stats.html +0 -4949
- package/dist/store.d.ts.map +0 -1
- package/dist/sync/channel.d.ts.map +0 -1
- package/dist/sync/index.d.ts.map +0 -1
- package/dist/sync/protocol.d.ts.map +0 -1
- package/dist/sync/withSync.d.ts.map +0 -1
- package/dist/sync.cjs.map +0 -1
- package/dist/sync.mjs.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/rollup.config.mjs +0 -44
- package/src/async-entry.ts +0 -6
- package/src/async.ts +0 -240
- package/src/batch.ts +0 -33
- package/src/compose.ts +0 -50
- package/src/computed-entry.ts +0 -6
- package/src/computed.ts +0 -187
- package/src/devtools/history.ts +0 -103
- package/src/devtools/index.ts +0 -5
- package/src/devtools/redux-bridge.ts +0 -70
- package/src/devtools/snapshots.ts +0 -54
- package/src/devtools/withDevtools.ts +0 -196
- package/src/extensions/noop.ts +0 -12
- package/src/index.ts +0 -4
- package/src/persist/adapters/indexedDB.ts +0 -114
- package/src/persist/adapters/localStorage.ts +0 -28
- package/src/persist/adapters/memory.ts +0 -26
- package/src/persist/adapters/sessionStorage.ts +0 -28
- package/src/persist/debounce.ts +0 -28
- package/src/persist/hydrate.ts +0 -60
- package/src/persist/index.ts +0 -141
- package/src/persist/serialize.ts +0 -60
- package/src/proxy.ts +0 -87
- package/src/registry.ts +0 -67
- package/src/signals/createSignal.ts +0 -81
- package/src/signals/index.ts +0 -20
- package/src/signals/useSignal.ts +0 -18
- package/src/store.ts +0 -250
- package/src/sync/channel.ts +0 -15
- package/src/sync/index.ts +0 -3
- package/src/sync/protocol.ts +0 -18
- package/src/sync/withSync.ts +0 -147
- package/src/types.ts +0 -159
- package/tests/async.test.ts +0 -1100
- package/tests/batch.test.ts +0 -41
- package/tests/compose.test.ts +0 -209
- package/tests/computed.test.ts +0 -867
- package/tests/devtools.test.ts +0 -1039
- package/tests/integration/persist.integration.test.ts +0 -258
- package/tests/integration/signals.integration.test.ts +0 -309
- package/tests/integration.test.ts +0 -278
- package/tests/persist/adapters/indexedDB.adapter.test.ts +0 -185
- package/tests/persist/adapters/localStorage.adapter.test.ts +0 -105
- package/tests/persist/adapters/memory.adapter.test.ts +0 -112
- package/tests/persist/adapters/sessionStorage.adapter.test.ts +0 -128
- package/tests/persist/debounce.test.ts +0 -121
- package/tests/persist/hydrate.test.ts +0 -120
- package/tests/persist/migrate.test.ts +0 -208
- package/tests/persist/persist.test.ts +0 -357
- package/tests/persist/serialize.test.ts +0 -128
- package/tests/proxy.test.ts +0 -473
- package/tests/registry.test.ts +0 -67
- package/tests/signals/derived.test.ts +0 -244
- package/tests/signals/inference.test.ts +0 -108
- package/tests/signals/signal.test.ts +0 -348
- package/tests/signals/useSignal.test.tsx +0 -275
- package/tests/store.test.ts +0 -482
- package/tests/stress.test.ts +0 -268
- package/tests/sync.test.ts +0 -576
- package/tests/types.test.ts +0 -32
- package/tests/v0.3.test.ts +0 -813
- package/tree-shake-test.js +0 -1
- package/tsconfig.json +0 -15
- package/vitest.config.ts +0 -22
- package/vitest_play.ts +0 -7
package/tests/computed.test.ts
DELETED
|
@@ -1,867 +0,0 @@
|
|
|
1
|
-
// packages/storve/tests/computed.test.ts
|
|
2
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
3
|
-
import { createStore } from '../src'
|
|
4
|
-
import { computed } from '../src/computed'
|
|
5
|
-
import { createAsync } from '../src/async'
|
|
6
|
-
|
|
7
|
-
// ─────────────────────────────────────────────
|
|
8
|
-
// SHARED TYPES
|
|
9
|
-
// ─────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
type AnyStore = Record<string, unknown>
|
|
12
|
-
|
|
13
|
-
// ─────────────────────────────────────────────
|
|
14
|
-
// 1. INITIALISATION
|
|
15
|
-
// ─────────────────────────────────────────────
|
|
16
|
-
describe('Computed — Initialisation', () => {
|
|
17
|
-
|
|
18
|
-
it('computed value is present in getState() on creation', () => {
|
|
19
|
-
const store = createStore({
|
|
20
|
-
count: 0,
|
|
21
|
-
doubled: computed((s: { count: number }) => s.count * 2)
|
|
22
|
-
})
|
|
23
|
-
expect('doubled' in store.getState()).toBe(true)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('computed value is correctly evaluated on init', () => {
|
|
27
|
-
const store = createStore({
|
|
28
|
-
count: 5,
|
|
29
|
-
doubled: computed((s: { count: number }) => s.count * 2)
|
|
30
|
-
})
|
|
31
|
-
expect(store.getState().doubled).toBe(10)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('computed marker is not exposed in getState()', () => {
|
|
35
|
-
const store = createStore({
|
|
36
|
-
count: 0,
|
|
37
|
-
doubled: computed((s: { count: number }) => s.count * 2)
|
|
38
|
-
})
|
|
39
|
-
const state = store.getState() as AnyStore
|
|
40
|
-
expect((state.doubled as AnyStore).__rf_computed).toBeUndefined()
|
|
41
|
-
expect(typeof state.doubled).toBe('number')
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('multiple computed keys all initialise correctly', () => {
|
|
45
|
-
const store = createStore({
|
|
46
|
-
a: 2,
|
|
47
|
-
b: 3,
|
|
48
|
-
sum: computed((s: { a: number; b: number }) => s.a + s.b),
|
|
49
|
-
product: computed((s: { a: number; b: number }) => s.a * s.b),
|
|
50
|
-
diff: computed((s: { a: number; b: number }) => s.a - s.b),
|
|
51
|
-
})
|
|
52
|
-
expect(store.getState().sum).toBe(5)
|
|
53
|
-
expect(store.getState().product).toBe(6)
|
|
54
|
-
expect(store.getState().diff).toBe(-1)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('computed and regular state coexist in getState()', () => {
|
|
58
|
-
const store = createStore({
|
|
59
|
-
count: 10,
|
|
60
|
-
label: 'hello',
|
|
61
|
-
doubled: computed((s: { count: number }) => s.count * 2)
|
|
62
|
-
})
|
|
63
|
-
const state = store.getState()
|
|
64
|
-
expect(state.count).toBe(10)
|
|
65
|
-
expect(state.label).toBe('hello')
|
|
66
|
-
expect(state.doubled).toBe(20)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('computed with string return type initialises correctly', () => {
|
|
70
|
-
const store = createStore({
|
|
71
|
-
first: 'John',
|
|
72
|
-
last: 'Doe',
|
|
73
|
-
full: computed((s: { first: string; last: string }) => `${s.first} ${s.last}`)
|
|
74
|
-
})
|
|
75
|
-
expect(store.getState().full).toBe('John Doe')
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('computed with boolean return type initialises correctly', () => {
|
|
79
|
-
const store = createStore({
|
|
80
|
-
count: 0,
|
|
81
|
-
isEmpty: computed((s: { count: number }) => s.count === 0)
|
|
82
|
-
})
|
|
83
|
-
expect(store.getState().isEmpty).toBe(true)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('computed with array return type initialises correctly', () => {
|
|
87
|
-
const store = createStore({
|
|
88
|
-
items: [1, 2, 3],
|
|
89
|
-
doubled: computed((s: { items: number[] }) => s.items.map(x => x * 2))
|
|
90
|
-
})
|
|
91
|
-
expect(store.getState().doubled).toEqual([2, 4, 6])
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it('computed with object return type initialises correctly', () => {
|
|
95
|
-
const store = createStore({
|
|
96
|
-
x: 1,
|
|
97
|
-
y: 2,
|
|
98
|
-
point: computed((s: { x: number; y: number }) => ({ x: s.x, y: s.y }))
|
|
99
|
-
})
|
|
100
|
-
expect(store.getState().point).toEqual({ x: 1, y: 2 })
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('store with only computed values and no base state initialises', () => {
|
|
104
|
-
const store = createStore({
|
|
105
|
-
constant: computed(() => 42)
|
|
106
|
-
})
|
|
107
|
-
expect(store.getState().constant).toBe(42)
|
|
108
|
-
})
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
// ─────────────────────────────────────────────
|
|
112
|
-
// 2. REACTIVITY
|
|
113
|
-
// ─────────────────────────────────────────────
|
|
114
|
-
describe('Computed — Reactivity', () => {
|
|
115
|
-
|
|
116
|
-
it('computed updates when its dependency changes', () => {
|
|
117
|
-
const store = createStore({
|
|
118
|
-
count: 0,
|
|
119
|
-
doubled: computed((s: { count: number }) => s.count * 2)
|
|
120
|
-
})
|
|
121
|
-
store.setState({ count: 5 })
|
|
122
|
-
expect(store.getState().doubled).toBe(10)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('computed does NOT update when an unrelated key changes', () => {
|
|
126
|
-
const fn = vi.fn((s: { count: number }) => s.count * 2)
|
|
127
|
-
const store = createStore({
|
|
128
|
-
count: 0,
|
|
129
|
-
name: 'Alice',
|
|
130
|
-
doubled: computed(fn)
|
|
131
|
-
})
|
|
132
|
-
const callsBefore = fn.mock.calls.length
|
|
133
|
-
store.setState({ name: 'Bob' })
|
|
134
|
-
expect(fn.mock.calls.length).toBe(callsBefore)
|
|
135
|
-
expect(store.getState().doubled).toBe(0)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('computed updates when one of multiple dependencies changes', () => {
|
|
139
|
-
const store = createStore({
|
|
140
|
-
a: 1,
|
|
141
|
-
b: 2,
|
|
142
|
-
sum: computed((s: { a: number; b: number }) => s.a + s.b)
|
|
143
|
-
})
|
|
144
|
-
store.setState({ a: 10 })
|
|
145
|
-
expect(store.getState().sum).toBe(12)
|
|
146
|
-
store.setState({ b: 20 })
|
|
147
|
-
expect(store.getState().sum).toBe(30)
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('computed updates when ALL dependencies change simultaneously', () => {
|
|
151
|
-
const store = createStore({
|
|
152
|
-
a: 1,
|
|
153
|
-
b: 2,
|
|
154
|
-
sum: computed((s: { a: number; b: number }) => s.a + s.b)
|
|
155
|
-
})
|
|
156
|
-
store.setState({ a: 10, b: 20 })
|
|
157
|
-
expect(store.getState().sum).toBe(30)
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
it('computed updates correctly across multiple sequential setStates', () => {
|
|
161
|
-
const store = createStore({
|
|
162
|
-
count: 0,
|
|
163
|
-
doubled: computed((s: { count: number }) => s.count * 2)
|
|
164
|
-
})
|
|
165
|
-
for (let i = 1; i <= 10; i++) {
|
|
166
|
-
store.setState({ count: i })
|
|
167
|
-
expect(store.getState().doubled).toBe(i * 2)
|
|
168
|
-
}
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('computed depending on object key updates when object reference changes', () => {
|
|
172
|
-
const store = createStore({
|
|
173
|
-
user: { name: 'Alice', age: 30 },
|
|
174
|
-
greeting: computed((s: { user: { name: string } }) => `Hello ${s.user.name}`)
|
|
175
|
-
})
|
|
176
|
-
store.setState({ user: { name: 'Bob', age: 25 } })
|
|
177
|
-
expect(store.getState().greeting).toBe('Hello Bob')
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
it('computed depending on array updates when array reference changes', () => {
|
|
181
|
-
const store = createStore({
|
|
182
|
-
items: [1, 2, 3],
|
|
183
|
-
total: computed((s: { items: number[] }) =>
|
|
184
|
-
s.items.reduce((acc, x) => acc + x, 0)
|
|
185
|
-
)
|
|
186
|
-
})
|
|
187
|
-
store.setState({ items: [10, 20, 30] })
|
|
188
|
-
expect(store.getState().total).toBe(60)
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
it('multiple computed values each track their own dependencies independently', () => {
|
|
192
|
-
const fnA = vi.fn((s: { a: number }) => s.a * 2)
|
|
193
|
-
const fnB = vi.fn((s: { b: number }) => s.b * 2)
|
|
194
|
-
const store = createStore({
|
|
195
|
-
a: 1,
|
|
196
|
-
b: 2,
|
|
197
|
-
doubledA: computed(fnA),
|
|
198
|
-
doubledB: computed(fnB),
|
|
199
|
-
})
|
|
200
|
-
const callsA = fnA.mock.calls.length
|
|
201
|
-
const callsB = fnB.mock.calls.length
|
|
202
|
-
|
|
203
|
-
store.setState({ a: 10 })
|
|
204
|
-
expect(fnA.mock.calls.length).toBe(callsA + 1)
|
|
205
|
-
expect(fnB.mock.calls.length).toBe(callsB)
|
|
206
|
-
|
|
207
|
-
store.setState({ b: 20 })
|
|
208
|
-
expect(fnA.mock.calls.length).toBe(callsA + 1)
|
|
209
|
-
expect(fnB.mock.calls.length).toBe(callsB + 1)
|
|
210
|
-
})
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
// ─────────────────────────────────────────────
|
|
214
|
-
// 3. MEMOIZATION
|
|
215
|
-
// ─────────────────────────────────────────────
|
|
216
|
-
describe('Computed — Memoization', () => {
|
|
217
|
-
|
|
218
|
-
it('computed fn is NOT called when unrelated key changes', () => {
|
|
219
|
-
const fn = vi.fn((s: { count: number }) => s.count * 2)
|
|
220
|
-
const store = createStore({ count: 0, name: 'Alice', doubled: computed(fn) })
|
|
221
|
-
const before = fn.mock.calls.length
|
|
222
|
-
store.setState({ name: 'Bob' })
|
|
223
|
-
store.setState({ name: 'Charlie' })
|
|
224
|
-
store.setState({ name: 'Dave' })
|
|
225
|
-
expect(fn.mock.calls.length).toBe(before)
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
it('computed fn is called exactly once per dependency change', () => {
|
|
229
|
-
const fn = vi.fn((s: { count: number }) => s.count * 2)
|
|
230
|
-
const store = createStore({ count: 0, doubled: computed(fn) })
|
|
231
|
-
const before = fn.mock.calls.length
|
|
232
|
-
store.setState({ count: 1 })
|
|
233
|
-
expect(fn.mock.calls.length).toBe(before + 1)
|
|
234
|
-
store.setState({ count: 2 })
|
|
235
|
-
expect(fn.mock.calls.length).toBe(before + 2)
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
it('computed fn is called once even when multiple deps change in one setState', () => {
|
|
239
|
-
const fn = vi.fn((s: { a: number; b: number }) => s.a + s.b)
|
|
240
|
-
const store = createStore({ a: 0, b: 0, sum: computed(fn) })
|
|
241
|
-
const before = fn.mock.calls.length
|
|
242
|
-
store.setState({ a: 1, b: 2 })
|
|
243
|
-
expect(fn.mock.calls.length).toBe(before + 1)
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
it('computed fn is called once inside batch regardless of setState count', () => {
|
|
247
|
-
const fn = vi.fn((s: { a: number; b: number }) => s.a + s.b)
|
|
248
|
-
const store = createStore({ a: 0, b: 0, sum: computed(fn) })
|
|
249
|
-
const before = fn.mock.calls.length
|
|
250
|
-
store.batch(() => {
|
|
251
|
-
store.setState({ a: 1 })
|
|
252
|
-
store.setState({ b: 2 })
|
|
253
|
-
store.setState({ a: 3 })
|
|
254
|
-
})
|
|
255
|
-
expect(store.getState().sum).toBe(5)
|
|
256
|
-
expect(fn.mock.calls.length).toBe(before + 1)
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
it('computed with no dependencies is evaluated once and never recomputed', () => {
|
|
260
|
-
const fn = vi.fn(() => 42)
|
|
261
|
-
const store = createStore({ count: 0, constant: computed(fn) })
|
|
262
|
-
const before = fn.mock.calls.length
|
|
263
|
-
store.setState({ count: 1 })
|
|
264
|
-
store.setState({ count: 2 })
|
|
265
|
-
store.setState({ count: 3 })
|
|
266
|
-
expect(fn.mock.calls.length).toBe(before)
|
|
267
|
-
expect(store.getState().constant).toBe(42)
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
it('computed result is cached — fn not called again on repeated reads', () => {
|
|
271
|
-
const fn = vi.fn((s: { count: number }) => s.count * 2)
|
|
272
|
-
const store = createStore({ count: 5, doubled: computed(fn) })
|
|
273
|
-
const before = fn.mock.calls.length
|
|
274
|
-
store.getState().doubled
|
|
275
|
-
store.getState().doubled
|
|
276
|
-
store.getState().doubled
|
|
277
|
-
expect(fn.mock.calls.length).toBe(before)
|
|
278
|
-
})
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
// ─────────────────────────────────────────────
|
|
282
|
-
// 4. CHAINED COMPUTED
|
|
283
|
-
// ─────────────────────────────────────────────
|
|
284
|
-
describe('Computed — Chaining', () => {
|
|
285
|
-
|
|
286
|
-
it('computed depending on computed updates correctly', () => {
|
|
287
|
-
const store = createStore({
|
|
288
|
-
count: 2,
|
|
289
|
-
doubled: computed((s: { count: number }) => s.count * 2),
|
|
290
|
-
quadrupled: computed((s: { doubled: number }) => s.doubled * 2),
|
|
291
|
-
})
|
|
292
|
-
expect(store.getState().doubled).toBe(4)
|
|
293
|
-
expect(store.getState().quadrupled).toBe(8)
|
|
294
|
-
store.setState({ count: 5 })
|
|
295
|
-
expect(store.getState().doubled).toBe(10)
|
|
296
|
-
expect(store.getState().quadrupled).toBe(20)
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
it('three-level chain all update when base changes', () => {
|
|
300
|
-
const store = createStore({
|
|
301
|
-
count: 1,
|
|
302
|
-
a: computed((s: { count: number }) => s.count * 2),
|
|
303
|
-
b: computed((s: { a: number }) => s.a * 2),
|
|
304
|
-
c: computed((s: { b: number }) => s.b * 2),
|
|
305
|
-
})
|
|
306
|
-
store.setState({ count: 3 })
|
|
307
|
-
expect(store.getState().a).toBe(6)
|
|
308
|
-
expect(store.getState().b).toBe(12)
|
|
309
|
-
expect(store.getState().c).toBe(24)
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
it('five-level deep chain propagates correctly', () => {
|
|
313
|
-
const store = createStore({
|
|
314
|
-
base: 1,
|
|
315
|
-
l1: computed((s: { base: number }) => s.base + 1),
|
|
316
|
-
l2: computed((s: { l1: number }) => s.l1 + 1),
|
|
317
|
-
l3: computed((s: { l2: number }) => s.l2 + 1),
|
|
318
|
-
l4: computed((s: { l3: number }) => s.l3 + 1),
|
|
319
|
-
l5: computed((s: { l4: number }) => s.l4 + 1),
|
|
320
|
-
})
|
|
321
|
-
store.setState({ base: 10 })
|
|
322
|
-
expect(store.getState().l1).toBe(11)
|
|
323
|
-
expect(store.getState().l2).toBe(12)
|
|
324
|
-
expect(store.getState().l3).toBe(13)
|
|
325
|
-
expect(store.getState().l4).toBe(14)
|
|
326
|
-
expect(store.getState().l5).toBe(15)
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
it('diamond dependency — two computeds share a base, third depends on both', () => {
|
|
330
|
-
const store = createStore({
|
|
331
|
-
base: 2,
|
|
332
|
-
a: computed((s: { base: number }) => s.base * 2),
|
|
333
|
-
b: computed((s: { base: number }) => s.base * 3),
|
|
334
|
-
c: computed((s: { a: number; b: number }) => s.a + s.b),
|
|
335
|
-
})
|
|
336
|
-
expect(store.getState().c).toBe(10)
|
|
337
|
-
store.setState({ base: 5 })
|
|
338
|
-
expect(store.getState().a).toBe(10)
|
|
339
|
-
expect(store.getState().b).toBe(15)
|
|
340
|
-
expect(store.getState().c).toBe(25)
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
it('unrelated computed in chain is not recomputed', () => {
|
|
344
|
-
const fnX = vi.fn((s: { x: number }) => s.x * 2)
|
|
345
|
-
const fnY = vi.fn((s: { y: number }) => s.y * 2)
|
|
346
|
-
const store = createStore({
|
|
347
|
-
x: 1, y: 1,
|
|
348
|
-
doubledX: computed(fnX),
|
|
349
|
-
doubledY: computed(fnY),
|
|
350
|
-
})
|
|
351
|
-
const beforeY = fnY.mock.calls.length
|
|
352
|
-
store.setState({ x: 10 })
|
|
353
|
-
expect(fnY.mock.calls.length).toBe(beforeY)
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
it('chained computed fn call counts are correct', () => {
|
|
357
|
-
const fnA = vi.fn((s: { count: number }) => s.count * 2)
|
|
358
|
-
const fnB = vi.fn((s: { a: number }) => s.a * 2)
|
|
359
|
-
const store = createStore({ count: 0, a: computed(fnA), b: computed(fnB) })
|
|
360
|
-
const beforeA = fnA.mock.calls.length
|
|
361
|
-
const beforeB = fnB.mock.calls.length
|
|
362
|
-
store.setState({ count: 5 })
|
|
363
|
-
expect(fnA.mock.calls.length).toBe(beforeA + 1)
|
|
364
|
-
expect(fnB.mock.calls.length).toBe(beforeB + 1)
|
|
365
|
-
})
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
// ─────────────────────────────────────────────
|
|
369
|
-
// 5. CIRCULAR DEPENDENCY DETECTION
|
|
370
|
-
// ─────────────────────────────────────────────
|
|
371
|
-
describe('Computed — Circular Dependency Detection', () => {
|
|
372
|
-
|
|
373
|
-
it('two-node cycle throws on createStore', () => {
|
|
374
|
-
expect(() => createStore({
|
|
375
|
-
a: computed((s: { b: number }) => s.b + 1),
|
|
376
|
-
b: computed((s: { a: number }) => s.a + 1),
|
|
377
|
-
})).toThrow()
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
it('error message contains "circular" or "cycle"', () => {
|
|
381
|
-
expect(() => createStore({
|
|
382
|
-
a: computed((s: { b: number }) => s.b + 1),
|
|
383
|
-
b: computed((s: { a: number }) => s.a + 1),
|
|
384
|
-
})).toThrow(/circular|cycle/i)
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
it('error message contains the cycle path', () => {
|
|
388
|
-
expect(() => createStore({
|
|
389
|
-
a: computed((s: { b: number }) => s.b + 1),
|
|
390
|
-
b: computed((s: { a: number }) => s.a + 1),
|
|
391
|
-
})).toThrow(/a.*b|b.*a/i)
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
it('three-node cycle throws on createStore', () => {
|
|
395
|
-
expect(() => createStore({
|
|
396
|
-
a: computed((s: { b: number }) => s.b + 1),
|
|
397
|
-
b: computed((s: { c: number }) => s.c + 1),
|
|
398
|
-
c: computed((s: { a: number }) => s.a + 1),
|
|
399
|
-
})).toThrow(/circular|cycle/i)
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
it('self-referencing computed throws on createStore', () => {
|
|
403
|
-
expect(() => createStore({
|
|
404
|
-
a: computed((s: { a: number }) => s.a + 1),
|
|
405
|
-
})).toThrow(/circular|cycle/i)
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
it('non-circular computed does not throw', () => {
|
|
409
|
-
expect(() => createStore({
|
|
410
|
-
count: 0,
|
|
411
|
-
doubled: computed((s: { count: number }) => s.count * 2),
|
|
412
|
-
quadrupled: computed((s: { doubled: number }) => s.doubled * 2),
|
|
413
|
-
})).not.toThrow()
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
it('long valid chain does not throw', () => {
|
|
417
|
-
expect(() => createStore({
|
|
418
|
-
base: 0,
|
|
419
|
-
l1: computed((s: { base: number }) => s.base + 1),
|
|
420
|
-
l2: computed((s: { l1: number }) => s.l1 + 1),
|
|
421
|
-
l3: computed((s: { l2: number }) => s.l2 + 1),
|
|
422
|
-
l4: computed((s: { l3: number }) => s.l3 + 1),
|
|
423
|
-
l5: computed((s: { l4: number }) => s.l4 + 1),
|
|
424
|
-
})).not.toThrow()
|
|
425
|
-
})
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
// ─────────────────────────────────────────────
|
|
429
|
-
// 6. READ-ONLY ENFORCEMENT
|
|
430
|
-
// ─────────────────────────────────────────────
|
|
431
|
-
describe('Computed — Read-Only Enforcement', () => {
|
|
432
|
-
|
|
433
|
-
it('setState on computed key is silently ignored', () => {
|
|
434
|
-
const store = createStore({
|
|
435
|
-
count: 2,
|
|
436
|
-
doubled: computed((s: { count: number }) => s.count * 2)
|
|
437
|
-
})
|
|
438
|
-
store.setState({ doubled: 999 } as Parameters<typeof store.setState>[0])
|
|
439
|
-
expect(store.getState().doubled).toBe(4)
|
|
440
|
-
})
|
|
441
|
-
|
|
442
|
-
it('computed value is not overwritten by setState partial object', () => {
|
|
443
|
-
const store = createStore({
|
|
444
|
-
count: 3,
|
|
445
|
-
doubled: computed((s: { count: number }) => s.count * 2)
|
|
446
|
-
})
|
|
447
|
-
store.setState({ doubled: 0, count: 5 } as Parameters<typeof store.setState>[0])
|
|
448
|
-
expect(store.getState().doubled).toBe(10)
|
|
449
|
-
})
|
|
450
|
-
|
|
451
|
-
it('computed value stays correct after attempted overwrite + dependency change', () => {
|
|
452
|
-
const store = createStore({
|
|
453
|
-
count: 1,
|
|
454
|
-
doubled: computed((s: { count: number }) => s.count * 2)
|
|
455
|
-
})
|
|
456
|
-
store.setState({ doubled: 9999 } as Parameters<typeof store.setState>[0])
|
|
457
|
-
store.setState({ count: 7 })
|
|
458
|
-
expect(store.getState().doubled).toBe(14)
|
|
459
|
-
})
|
|
460
|
-
})
|
|
461
|
-
|
|
462
|
-
// ─────────────────────────────────────────────
|
|
463
|
-
// 7. SUBSCRIBER NOTIFICATIONS
|
|
464
|
-
// ─────────────────────────────────────────────
|
|
465
|
-
describe('Computed — Subscribers', () => {
|
|
466
|
-
|
|
467
|
-
it('subscriber receives updated computed value after setState', () => {
|
|
468
|
-
const store = createStore({
|
|
469
|
-
count: 0,
|
|
470
|
-
doubled: computed((s: { count: number }) => s.count * 2)
|
|
471
|
-
})
|
|
472
|
-
const listener = vi.fn()
|
|
473
|
-
store.subscribe(listener)
|
|
474
|
-
store.setState({ count: 5 })
|
|
475
|
-
const lastState = listener.mock.calls[listener.mock.calls.length - 1][0] as { doubled: number }
|
|
476
|
-
expect(lastState.doubled).toBe(10)
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
it('subscriber receives correct computed value in full state', () => {
|
|
480
|
-
const store = createStore({
|
|
481
|
-
a: 1, b: 2,
|
|
482
|
-
sum: computed((s: { a: number; b: number }) => s.a + s.b)
|
|
483
|
-
})
|
|
484
|
-
const snapshots: number[] = []
|
|
485
|
-
store.subscribe(s => snapshots.push((s as { sum: number }).sum))
|
|
486
|
-
store.setState({ a: 10 })
|
|
487
|
-
store.setState({ b: 20 })
|
|
488
|
-
expect(snapshots).toEqual([12, 30])
|
|
489
|
-
})
|
|
490
|
-
|
|
491
|
-
it('subscriber receives computed value in chained update', () => {
|
|
492
|
-
const store = createStore({
|
|
493
|
-
count: 1,
|
|
494
|
-
doubled: computed((s: { count: number }) => s.count * 2),
|
|
495
|
-
quadrupled: computed((s: { doubled: number }) => s.doubled * 2),
|
|
496
|
-
})
|
|
497
|
-
const listener = vi.fn()
|
|
498
|
-
store.subscribe(listener)
|
|
499
|
-
store.setState({ count: 3 })
|
|
500
|
-
const lastState = listener.mock.calls[listener.mock.calls.length - 1][0] as {
|
|
501
|
-
doubled: number
|
|
502
|
-
quadrupled: number
|
|
503
|
-
}
|
|
504
|
-
expect(lastState.doubled).toBe(6)
|
|
505
|
-
expect(lastState.quadrupled).toBe(12)
|
|
506
|
-
})
|
|
507
|
-
|
|
508
|
-
it('unsubscribed listener does not receive computed updates', () => {
|
|
509
|
-
const store = createStore({
|
|
510
|
-
count: 0,
|
|
511
|
-
doubled: computed((s: { count: number }) => s.count * 2)
|
|
512
|
-
})
|
|
513
|
-
const listener = vi.fn()
|
|
514
|
-
const unsub = store.subscribe(listener)
|
|
515
|
-
unsub()
|
|
516
|
-
store.setState({ count: 5 })
|
|
517
|
-
expect(listener).not.toHaveBeenCalled()
|
|
518
|
-
})
|
|
519
|
-
|
|
520
|
-
it('batch fires one subscriber notification with correct computed value', () => {
|
|
521
|
-
const store = createStore({
|
|
522
|
-
a: 0, b: 0,
|
|
523
|
-
sum: computed((s: { a: number; b: number }) => s.a + s.b)
|
|
524
|
-
})
|
|
525
|
-
const listener = vi.fn()
|
|
526
|
-
store.subscribe(listener)
|
|
527
|
-
store.batch(() => {
|
|
528
|
-
store.setState({ a: 3 })
|
|
529
|
-
store.setState({ b: 7 })
|
|
530
|
-
})
|
|
531
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
532
|
-
const lastState = listener.mock.calls[0][0] as { sum: number }
|
|
533
|
-
expect(lastState.sum).toBe(10)
|
|
534
|
-
})
|
|
535
|
-
})
|
|
536
|
-
|
|
537
|
-
// ─────────────────────────────────────────────
|
|
538
|
-
// 8. COEXISTENCE WITH ASYNC STATE
|
|
539
|
-
// ─────────────────────────────────────────────
|
|
540
|
-
describe('Computed — Coexistence with Async State', () => {
|
|
541
|
-
|
|
542
|
-
it('computed and async keys coexist in same store', () => {
|
|
543
|
-
const store = createStore({
|
|
544
|
-
count: 0,
|
|
545
|
-
doubled: computed((s: { count: number }) => s.count * 2),
|
|
546
|
-
data: createAsync(async () => 'result'),
|
|
547
|
-
})
|
|
548
|
-
expect(store.getState().doubled).toBe(0)
|
|
549
|
-
expect(store.getState().data.status).toBe('idle')
|
|
550
|
-
})
|
|
551
|
-
|
|
552
|
-
it('async fetch does not affect computed value', async () => {
|
|
553
|
-
const store = createStore({
|
|
554
|
-
count: 5,
|
|
555
|
-
doubled: computed((s: { count: number }) => s.count * 2),
|
|
556
|
-
data: createAsync(async () => 'result'),
|
|
557
|
-
})
|
|
558
|
-
await store.fetch('data')
|
|
559
|
-
expect(store.getState().doubled).toBe(10)
|
|
560
|
-
})
|
|
561
|
-
|
|
562
|
-
it('setState on sync key after async fetch still updates computed', async () => {
|
|
563
|
-
const store = createStore({
|
|
564
|
-
count: 0,
|
|
565
|
-
doubled: computed((s: { count: number }) => s.count * 2),
|
|
566
|
-
data: createAsync(async () => 'result'),
|
|
567
|
-
})
|
|
568
|
-
await store.fetch('data')
|
|
569
|
-
store.setState({ count: 7 })
|
|
570
|
-
expect(store.getState().doubled).toBe(14)
|
|
571
|
-
})
|
|
572
|
-
})
|
|
573
|
-
|
|
574
|
-
// ─────────────────────────────────────────────
|
|
575
|
-
// 9. COEXISTENCE WITH IMMER
|
|
576
|
-
// ─────────────────────────────────────────────
|
|
577
|
-
describe('Computed — Coexistence with Immer', () => {
|
|
578
|
-
|
|
579
|
-
it('computed updates correctly after Immer draft mutation', () => {
|
|
580
|
-
const store = createStore({
|
|
581
|
-
count: 0,
|
|
582
|
-
doubled: computed((s: { count: number }) => s.count * 2)
|
|
583
|
-
}, { immer: true })
|
|
584
|
-
store.setState(draft => { draft.count = 5 })
|
|
585
|
-
expect(store.getState().doubled).toBe(10)
|
|
586
|
-
})
|
|
587
|
-
|
|
588
|
-
it('computed updates after Immer array push', () => {
|
|
589
|
-
const store = createStore({
|
|
590
|
-
items: [] as number[],
|
|
591
|
-
total: computed((s: { items: number[] }) =>
|
|
592
|
-
s.items.reduce((acc, x) => acc + x, 0)
|
|
593
|
-
)
|
|
594
|
-
}, { immer: true })
|
|
595
|
-
store.setState(draft => { draft.items.push(10) })
|
|
596
|
-
store.setState(draft => { draft.items.push(20) })
|
|
597
|
-
expect(store.getState().total).toBe(30)
|
|
598
|
-
})
|
|
599
|
-
|
|
600
|
-
it('computed updates after Immer nested object mutation', () => {
|
|
601
|
-
const store = createStore({
|
|
602
|
-
user: { name: 'Alice' },
|
|
603
|
-
greeting: computed((s: { user: { name: string } }) => `Hello ${s.user.name}`)
|
|
604
|
-
}, { immer: true })
|
|
605
|
-
store.setState(draft => { draft.user.name = 'Bob' })
|
|
606
|
-
expect(store.getState().greeting).toBe('Hello Bob')
|
|
607
|
-
})
|
|
608
|
-
})
|
|
609
|
-
|
|
610
|
-
// ─────────────────────────────────────────────
|
|
611
|
-
// 10. EDGE CASES
|
|
612
|
-
// ─────────────────────────────────────────────
|
|
613
|
-
describe('Computed — Edge Cases', () => {
|
|
614
|
-
|
|
615
|
-
it('computed returning null is valid', () => {
|
|
616
|
-
const store = createStore({
|
|
617
|
-
flag: false,
|
|
618
|
-
result: computed((s: { flag: boolean }) => s.flag ? 'yes' : null)
|
|
619
|
-
})
|
|
620
|
-
expect(store.getState().result).toBeNull()
|
|
621
|
-
})
|
|
622
|
-
|
|
623
|
-
it('computed returning 0 is valid and not falsy-treated as empty', () => {
|
|
624
|
-
const store = createStore({
|
|
625
|
-
count: 0,
|
|
626
|
-
doubled: computed((s: { count: number }) => s.count * 2)
|
|
627
|
-
})
|
|
628
|
-
expect(store.getState().doubled).toBe(0)
|
|
629
|
-
store.setState({ count: 5 })
|
|
630
|
-
expect(store.getState().doubled).toBe(10)
|
|
631
|
-
})
|
|
632
|
-
|
|
633
|
-
it('computed returning false is valid', () => {
|
|
634
|
-
const store = createStore({
|
|
635
|
-
count: 1,
|
|
636
|
-
isZero: computed((s: { count: number }) => s.count === 0)
|
|
637
|
-
})
|
|
638
|
-
expect(store.getState().isZero).toBe(false)
|
|
639
|
-
store.setState({ count: 0 })
|
|
640
|
-
expect(store.getState().isZero).toBe(true)
|
|
641
|
-
})
|
|
642
|
-
|
|
643
|
-
it('computed returning empty string is valid', () => {
|
|
644
|
-
const store = createStore({
|
|
645
|
-
name: '',
|
|
646
|
-
greeting: computed((s: { name: string }) => s.name ? `Hello ${s.name}` : '')
|
|
647
|
-
})
|
|
648
|
-
expect(store.getState().greeting).toBe('')
|
|
649
|
-
store.setState({ name: 'Alice' })
|
|
650
|
-
expect(store.getState().greeting).toBe('Hello Alice')
|
|
651
|
-
})
|
|
652
|
-
|
|
653
|
-
it('computed returning empty array is valid', () => {
|
|
654
|
-
const store = createStore({
|
|
655
|
-
filter: 'none',
|
|
656
|
-
items: computed((s: { filter: string }) => s.filter === 'none' ? [] : [1, 2, 3])
|
|
657
|
-
})
|
|
658
|
-
expect(store.getState().items).toEqual([])
|
|
659
|
-
})
|
|
660
|
-
|
|
661
|
-
it('computed returning undefined is valid', () => {
|
|
662
|
-
const store = createStore({
|
|
663
|
-
value: null as string | null,
|
|
664
|
-
upper: computed((s: { value: string | null }) => s.value?.toUpperCase())
|
|
665
|
-
})
|
|
666
|
-
expect(store.getState().upper).toBeUndefined()
|
|
667
|
-
store.setState({ value: 'hello' })
|
|
668
|
-
expect(store.getState().upper).toBe('HELLO')
|
|
669
|
-
})
|
|
670
|
-
|
|
671
|
-
it('computed with no dependencies evaluates once and is constant', () => {
|
|
672
|
-
const fn = vi.fn(() => 99)
|
|
673
|
-
const store = createStore({ count: 0, constant: computed(fn) })
|
|
674
|
-
const callsAfterInit = fn.mock.calls.length
|
|
675
|
-
store.setState({ count: 1 })
|
|
676
|
-
store.setState({ count: 2 })
|
|
677
|
-
store.setState({ count: 3 })
|
|
678
|
-
expect(fn.mock.calls.length).toBe(callsAfterInit)
|
|
679
|
-
expect(store.getState().constant).toBe(99)
|
|
680
|
-
})
|
|
681
|
-
|
|
682
|
-
it('computed returning a new object reference documents expected behaviour', () => {
|
|
683
|
-
const store = createStore({
|
|
684
|
-
count: 1,
|
|
685
|
-
obj: computed((s: { count: number }) => ({ value: s.count }))
|
|
686
|
-
})
|
|
687
|
-
store.setState({ count: 1 })
|
|
688
|
-
expect(store.getState().obj.value).toBe(1)
|
|
689
|
-
})
|
|
690
|
-
|
|
691
|
-
it('large number of computed values all initialise correctly', () => {
|
|
692
|
-
type DynState = { base: number } & Record<string, number>
|
|
693
|
-
const definition: DynState = { base: 1 }
|
|
694
|
-
for (let i = 0; i < 50; i++) {
|
|
695
|
-
definition[`comp${i}`] = computed((s: DynState) => s.base + i) as unknown as number
|
|
696
|
-
}
|
|
697
|
-
const store = createStore(definition)
|
|
698
|
-
for (let i = 0; i < 50; i++) {
|
|
699
|
-
expect((store.getState() as DynState)[`comp${i}`]).toBe(1 + i)
|
|
700
|
-
}
|
|
701
|
-
})
|
|
702
|
-
|
|
703
|
-
it('large number of computed values all update correctly', () => {
|
|
704
|
-
type DynState = { base: number } & Record<string, number>
|
|
705
|
-
const definition: DynState = { base: 1 }
|
|
706
|
-
for (let i = 0; i < 50; i++) {
|
|
707
|
-
definition[`comp${i}`] = computed((s: DynState) => s.base + i) as unknown as number
|
|
708
|
-
}
|
|
709
|
-
const store = createStore(definition)
|
|
710
|
-
store.setState({ base: 10 })
|
|
711
|
-
for (let i = 0; i < 50; i++) {
|
|
712
|
-
expect((store.getState() as DynState)[`comp${i}`]).toBe(10 + i)
|
|
713
|
-
}
|
|
714
|
-
})
|
|
715
|
-
|
|
716
|
-
it('computed depending on multiple keys — only relevant key change triggers recompute', () => {
|
|
717
|
-
const fn = vi.fn((s: { first: string; last: string }) => `${s.first} ${s.last}`)
|
|
718
|
-
const store = createStore({
|
|
719
|
-
first: 'John', last: 'Doe', unrelated: 0,
|
|
720
|
-
full: computed(fn)
|
|
721
|
-
})
|
|
722
|
-
const before = fn.mock.calls.length
|
|
723
|
-
store.setState({ unrelated: 99 })
|
|
724
|
-
expect(fn.mock.calls.length).toBe(before)
|
|
725
|
-
store.setState({ first: 'Jane' })
|
|
726
|
-
expect(fn.mock.calls.length).toBe(before + 1)
|
|
727
|
-
expect(store.getState().full).toBe('Jane Doe')
|
|
728
|
-
})
|
|
729
|
-
})
|
|
730
|
-
|
|
731
|
-
// ─────────────────────────────────────────────
|
|
732
|
-
// 11. ACTIONS + COMPUTED
|
|
733
|
-
// ─────────────────────────────────────────────
|
|
734
|
-
describe('Computed — Coexistence with Actions', () => {
|
|
735
|
-
|
|
736
|
-
it('action can read computed value via getState()', () => {
|
|
737
|
-
const store = createStore({
|
|
738
|
-
count: 5,
|
|
739
|
-
doubled: computed((s: { count: number }) => s.count * 2),
|
|
740
|
-
actions: {
|
|
741
|
-
getDoubled() { return store.getState().doubled }
|
|
742
|
-
}
|
|
743
|
-
})
|
|
744
|
-
expect(store.getDoubled()).toBe(10)
|
|
745
|
-
})
|
|
746
|
-
|
|
747
|
-
it('action that updates dependency causes computed to update', () => {
|
|
748
|
-
const store = createStore({
|
|
749
|
-
count: 0,
|
|
750
|
-
doubled: computed((s: { count: number }) => s.count * 2),
|
|
751
|
-
actions: {
|
|
752
|
-
increment() { store.setState(s => ({ count: s.count + 1 })) }
|
|
753
|
-
}
|
|
754
|
-
})
|
|
755
|
-
store.increment()
|
|
756
|
-
store.increment()
|
|
757
|
-
expect(store.getState().doubled).toBe(4)
|
|
758
|
-
})
|
|
759
|
-
|
|
760
|
-
it('computed value is correct after multiple action calls', () => {
|
|
761
|
-
const store = createStore({
|
|
762
|
-
items: [] as string[],
|
|
763
|
-
count: computed((s: { items: string[] }) => s.items.length),
|
|
764
|
-
actions: {
|
|
765
|
-
add(item: string) {
|
|
766
|
-
store.setState(s => ({ items: [...s.items, item] }))
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
})
|
|
770
|
-
store.add('a')
|
|
771
|
-
store.add('b')
|
|
772
|
-
store.add('c')
|
|
773
|
-
expect(store.getState().count).toBe(3)
|
|
774
|
-
})
|
|
775
|
-
})
|
|
776
|
-
|
|
777
|
-
// ─────────────────────────────────────────────
|
|
778
|
-
// 12. STRESS TESTS
|
|
779
|
-
// ─────────────────────────────────────────────
|
|
780
|
-
describe('Computed — Stress Tests', () => {
|
|
781
|
-
|
|
782
|
-
it('10,000 setState calls with computed — all correct', () => {
|
|
783
|
-
const store = createStore({
|
|
784
|
-
count: 0,
|
|
785
|
-
doubled: computed((s: { count: number }) => s.count * 2)
|
|
786
|
-
})
|
|
787
|
-
for (let i = 1; i <= 10_000; i++) {
|
|
788
|
-
store.setState({ count: i })
|
|
789
|
-
}
|
|
790
|
-
expect(store.getState().doubled).toBe(20_000)
|
|
791
|
-
})
|
|
792
|
-
|
|
793
|
-
it('rapid setState calls — computed always reflects latest state', () => {
|
|
794
|
-
const store = createStore({
|
|
795
|
-
value: 0,
|
|
796
|
-
squared: computed((s: { value: number }) => s.value * s.value)
|
|
797
|
-
})
|
|
798
|
-
for (let i = 0; i < 1000; i++) {
|
|
799
|
-
store.setState({ value: i })
|
|
800
|
-
expect(store.getState().squared).toBe(i * i)
|
|
801
|
-
}
|
|
802
|
-
})
|
|
803
|
-
|
|
804
|
-
it('100 subscribers all receive correct computed value', () => {
|
|
805
|
-
const store = createStore({
|
|
806
|
-
count: 0,
|
|
807
|
-
doubled: computed((s: { count: number }) => s.count * 2)
|
|
808
|
-
})
|
|
809
|
-
const listeners = Array.from({ length: 100 }, () => vi.fn())
|
|
810
|
-
listeners.forEach(l => store.subscribe(l))
|
|
811
|
-
store.setState({ count: 7 })
|
|
812
|
-
listeners.forEach(l => {
|
|
813
|
-
const lastState = l.mock.calls[l.mock.calls.length - 1][0] as { doubled: number }
|
|
814
|
-
expect(lastState.doubled).toBe(14)
|
|
815
|
-
})
|
|
816
|
-
})
|
|
817
|
-
|
|
818
|
-
it('deep 10-level chain propagates correctly under stress', () => {
|
|
819
|
-
type LevelState = { base: number } & Record<string, number>
|
|
820
|
-
const definition: LevelState = { base: 0 }
|
|
821
|
-
for (let i = 1; i <= 10; i++) {
|
|
822
|
-
const prev = i === 1 ? 'base' : `l${i - 1}`
|
|
823
|
-
definition[`l${i}`] = computed((s: LevelState) => s[prev] + 1) as unknown as number
|
|
824
|
-
}
|
|
825
|
-
const store = createStore(definition)
|
|
826
|
-
for (let run = 1; run <= 100; run++) {
|
|
827
|
-
store.setState({ base: run })
|
|
828
|
-
expect((store.getState() as LevelState).l10).toBe(run + 10)
|
|
829
|
-
}
|
|
830
|
-
})
|
|
831
|
-
|
|
832
|
-
it('batch with 100 setStates fires computed once and subscribers once', () => {
|
|
833
|
-
const fn = vi.fn((s: { count: number }) => s.count * 2)
|
|
834
|
-
const store = createStore({ count: 0, doubled: computed(fn) })
|
|
835
|
-
const listener = vi.fn()
|
|
836
|
-
store.subscribe(listener)
|
|
837
|
-
const callsBefore = fn.mock.calls.length
|
|
838
|
-
store.batch(() => {
|
|
839
|
-
for (let i = 1; i <= 100; i++) {
|
|
840
|
-
store.setState({ count: i })
|
|
841
|
-
}
|
|
842
|
-
})
|
|
843
|
-
expect(fn.mock.calls.length - callsBefore).toBe(1)
|
|
844
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
845
|
-
expect(store.getState().doubled).toBe(200)
|
|
846
|
-
})
|
|
847
|
-
|
|
848
|
-
it('50 computed keys — only changed dependency triggers correct recomputes', () => {
|
|
849
|
-
type BigState = Record<string, number>
|
|
850
|
-
const fns = Array.from({ length: 50 }, (_, i) =>
|
|
851
|
-
vi.fn((s: BigState) => s[`key${i}`] * 2)
|
|
852
|
-
)
|
|
853
|
-
const definition: BigState = {}
|
|
854
|
-
for (let i = 0; i < 50; i++) {
|
|
855
|
-
definition[`key${i}`] = i
|
|
856
|
-
definition[`doubled${i}`] = computed(fns[i]) as unknown as number
|
|
857
|
-
}
|
|
858
|
-
const store = createStore(definition)
|
|
859
|
-
const callCounts = fns.map(fn => fn.mock.calls.length)
|
|
860
|
-
store.setState({ key0: 99 })
|
|
861
|
-
expect(fns[0].mock.calls.length).toBe(callCounts[0] + 1)
|
|
862
|
-
for (let i = 1; i < 50; i++) {
|
|
863
|
-
expect(fns[i].mock.calls.length).toBe(callCounts[i])
|
|
864
|
-
}
|
|
865
|
-
expect((store.getState() as BigState).doubled0).toBe(198)
|
|
866
|
-
})
|
|
867
|
-
})
|