@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.
Files changed (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +993 -26
  3. package/dist/adapters/indexedDB.cjs +0 -1
  4. package/dist/adapters/indexedDB.mjs +0 -1
  5. package/dist/adapters/localStorage.cjs +0 -1
  6. package/dist/adapters/localStorage.mjs +0 -1
  7. package/dist/adapters/memory.cjs +0 -1
  8. package/dist/adapters/memory.mjs +0 -1
  9. package/dist/adapters/sessionStorage.cjs +0 -1
  10. package/dist/adapters/sessionStorage.mjs +0 -1
  11. package/dist/async-entry.d.ts +0 -1
  12. package/dist/async.cjs +0 -1
  13. package/dist/async.d.ts +0 -1
  14. package/dist/async.mjs +0 -1
  15. package/dist/batch.d.ts +0 -1
  16. package/dist/compose.d.ts +0 -1
  17. package/dist/computed-entry.d.ts +0 -1
  18. package/dist/computed.cjs +0 -1
  19. package/dist/computed.d.ts +0 -1
  20. package/dist/computed.mjs +0 -1
  21. package/dist/devtools/history.d.ts +0 -1
  22. package/dist/devtools/index.d.ts +0 -1
  23. package/dist/devtools/redux-bridge.d.ts +0 -1
  24. package/dist/devtools/snapshots.d.ts +0 -1
  25. package/dist/devtools/withDevtools.d.ts +0 -1
  26. package/dist/devtools.cjs +0 -1
  27. package/dist/devtools.mjs +0 -1
  28. package/dist/extensions/noop.d.ts +0 -1
  29. package/dist/index.cjs +0 -1
  30. package/dist/index.d.ts +0 -1
  31. package/dist/index.mjs +0 -1
  32. package/dist/persist/adapters/indexedDB.d.ts +0 -1
  33. package/dist/persist/adapters/localStorage.d.ts +0 -1
  34. package/dist/persist/adapters/memory.d.ts +0 -1
  35. package/dist/persist/adapters/sessionStorage.d.ts +0 -1
  36. package/dist/persist/debounce.d.ts +0 -1
  37. package/dist/persist/hydrate.d.ts +0 -1
  38. package/dist/persist/index.d.ts +0 -1
  39. package/dist/persist/serialize.d.ts +0 -1
  40. package/dist/persist.cjs +0 -1
  41. package/dist/persist.mjs +0 -1
  42. package/dist/proxy.d.ts +0 -1
  43. package/dist/registry-qtr1UpFU.js +0 -1
  44. package/dist/registry-zaKZ1P-s.js +0 -1
  45. package/dist/registry.d.ts +0 -1
  46. package/dist/signals/createSignal.d.ts +0 -1
  47. package/dist/signals/index.d.ts +0 -1
  48. package/dist/signals/useSignal.d.ts +0 -1
  49. package/dist/signals.cjs +0 -1
  50. package/dist/signals.mjs +0 -1
  51. package/dist/store.d.ts +0 -1
  52. package/dist/sync/channel.d.ts +0 -1
  53. package/dist/sync/index.d.ts +0 -1
  54. package/dist/sync/protocol.d.ts +0 -1
  55. package/dist/sync/withSync.d.ts +0 -1
  56. package/dist/sync.cjs +0 -1
  57. package/dist/sync.mjs +0 -1
  58. package/dist/types.d.ts +0 -1
  59. package/package.json +9 -3
  60. package/CHANGELOG.md +0 -151
  61. package/benchmarks/run.ts +0 -102
  62. package/benchmarks/week2.md +0 -9
  63. package/benchmarks/week2.ts +0 -64
  64. package/benchmarks/week4.md +0 -13
  65. package/benchmarks/week4.ts +0 -178
  66. package/benchmarks/week5.md +0 -15
  67. package/benchmarks/week5.ts +0 -184
  68. package/coverage/coverage-summary.json +0 -31
  69. package/dist/adapters/indexedDB.cjs.map +0 -1
  70. package/dist/adapters/indexedDB.mjs.map +0 -1
  71. package/dist/adapters/localStorage.cjs.map +0 -1
  72. package/dist/adapters/localStorage.mjs.map +0 -1
  73. package/dist/adapters/memory.cjs.map +0 -1
  74. package/dist/adapters/memory.mjs.map +0 -1
  75. package/dist/adapters/sessionStorage.cjs.map +0 -1
  76. package/dist/adapters/sessionStorage.mjs.map +0 -1
  77. package/dist/async-entry.d.ts.map +0 -1
  78. package/dist/async.cjs.map +0 -1
  79. package/dist/async.d.ts.map +0 -1
  80. package/dist/async.mjs.map +0 -1
  81. package/dist/batch.d.ts.map +0 -1
  82. package/dist/compose.d.ts.map +0 -1
  83. package/dist/computed-entry.d.ts.map +0 -1
  84. package/dist/computed.cjs.map +0 -1
  85. package/dist/computed.d.ts.map +0 -1
  86. package/dist/computed.mjs.map +0 -1
  87. package/dist/devtools/history.d.ts.map +0 -1
  88. package/dist/devtools/index.d.ts.map +0 -1
  89. package/dist/devtools/redux-bridge.d.ts.map +0 -1
  90. package/dist/devtools/snapshots.d.ts.map +0 -1
  91. package/dist/devtools/withDevtools.d.ts.map +0 -1
  92. package/dist/devtools.cjs.map +0 -1
  93. package/dist/devtools.mjs.map +0 -1
  94. package/dist/extensions/noop.d.ts.map +0 -1
  95. package/dist/index.cjs.js +0 -118
  96. package/dist/index.cjs.js.map +0 -1
  97. package/dist/index.cjs.map +0 -1
  98. package/dist/index.d.ts.map +0 -1
  99. package/dist/index.esm.js +0 -116
  100. package/dist/index.esm.js.map +0 -1
  101. package/dist/index.mjs.map +0 -1
  102. package/dist/persist/adapters/indexedDB.d.ts.map +0 -1
  103. package/dist/persist/adapters/localStorage.d.ts.map +0 -1
  104. package/dist/persist/adapters/memory.d.ts.map +0 -1
  105. package/dist/persist/adapters/sessionStorage.d.ts.map +0 -1
  106. package/dist/persist/debounce.d.ts.map +0 -1
  107. package/dist/persist/hydrate.d.ts.map +0 -1
  108. package/dist/persist/index.d.ts.map +0 -1
  109. package/dist/persist/serialize.d.ts.map +0 -1
  110. package/dist/persist.cjs.map +0 -1
  111. package/dist/persist.mjs.map +0 -1
  112. package/dist/proxy.d.ts.map +0 -1
  113. package/dist/registry-D3X0HSbl.js +0 -26
  114. package/dist/registry-D3X0HSbl.js.map +0 -1
  115. package/dist/registry-RDjbeJdx.js +0 -29
  116. package/dist/registry-RDjbeJdx.js.map +0 -1
  117. package/dist/registry-qtr1UpFU.js.map +0 -1
  118. package/dist/registry-zaKZ1P-s.js.map +0 -1
  119. package/dist/registry.d.ts.map +0 -1
  120. package/dist/signals/createSignal.d.ts.map +0 -1
  121. package/dist/signals/index.d.ts.map +0 -1
  122. package/dist/signals/useSignal.d.ts.map +0 -1
  123. package/dist/signals.cjs.map +0 -1
  124. package/dist/signals.mjs.map +0 -1
  125. package/dist/stats.html +0 -4949
  126. package/dist/store.d.ts.map +0 -1
  127. package/dist/sync/channel.d.ts.map +0 -1
  128. package/dist/sync/index.d.ts.map +0 -1
  129. package/dist/sync/protocol.d.ts.map +0 -1
  130. package/dist/sync/withSync.d.ts.map +0 -1
  131. package/dist/sync.cjs.map +0 -1
  132. package/dist/sync.mjs.map +0 -1
  133. package/dist/types.d.ts.map +0 -1
  134. package/rollup.config.mjs +0 -44
  135. package/src/async-entry.ts +0 -6
  136. package/src/async.ts +0 -240
  137. package/src/batch.ts +0 -33
  138. package/src/compose.ts +0 -50
  139. package/src/computed-entry.ts +0 -6
  140. package/src/computed.ts +0 -187
  141. package/src/devtools/history.ts +0 -103
  142. package/src/devtools/index.ts +0 -5
  143. package/src/devtools/redux-bridge.ts +0 -70
  144. package/src/devtools/snapshots.ts +0 -54
  145. package/src/devtools/withDevtools.ts +0 -196
  146. package/src/extensions/noop.ts +0 -12
  147. package/src/index.ts +0 -4
  148. package/src/persist/adapters/indexedDB.ts +0 -114
  149. package/src/persist/adapters/localStorage.ts +0 -28
  150. package/src/persist/adapters/memory.ts +0 -26
  151. package/src/persist/adapters/sessionStorage.ts +0 -28
  152. package/src/persist/debounce.ts +0 -28
  153. package/src/persist/hydrate.ts +0 -60
  154. package/src/persist/index.ts +0 -141
  155. package/src/persist/serialize.ts +0 -60
  156. package/src/proxy.ts +0 -87
  157. package/src/registry.ts +0 -67
  158. package/src/signals/createSignal.ts +0 -81
  159. package/src/signals/index.ts +0 -20
  160. package/src/signals/useSignal.ts +0 -18
  161. package/src/store.ts +0 -250
  162. package/src/sync/channel.ts +0 -15
  163. package/src/sync/index.ts +0 -3
  164. package/src/sync/protocol.ts +0 -18
  165. package/src/sync/withSync.ts +0 -147
  166. package/src/types.ts +0 -159
  167. package/tests/async.test.ts +0 -1100
  168. package/tests/batch.test.ts +0 -41
  169. package/tests/compose.test.ts +0 -209
  170. package/tests/computed.test.ts +0 -867
  171. package/tests/devtools.test.ts +0 -1039
  172. package/tests/integration/persist.integration.test.ts +0 -258
  173. package/tests/integration/signals.integration.test.ts +0 -309
  174. package/tests/integration.test.ts +0 -278
  175. package/tests/persist/adapters/indexedDB.adapter.test.ts +0 -185
  176. package/tests/persist/adapters/localStorage.adapter.test.ts +0 -105
  177. package/tests/persist/adapters/memory.adapter.test.ts +0 -112
  178. package/tests/persist/adapters/sessionStorage.adapter.test.ts +0 -128
  179. package/tests/persist/debounce.test.ts +0 -121
  180. package/tests/persist/hydrate.test.ts +0 -120
  181. package/tests/persist/migrate.test.ts +0 -208
  182. package/tests/persist/persist.test.ts +0 -357
  183. package/tests/persist/serialize.test.ts +0 -128
  184. package/tests/proxy.test.ts +0 -473
  185. package/tests/registry.test.ts +0 -67
  186. package/tests/signals/derived.test.ts +0 -244
  187. package/tests/signals/inference.test.ts +0 -108
  188. package/tests/signals/signal.test.ts +0 -348
  189. package/tests/signals/useSignal.test.tsx +0 -275
  190. package/tests/store.test.ts +0 -482
  191. package/tests/stress.test.ts +0 -268
  192. package/tests/sync.test.ts +0 -576
  193. package/tests/types.test.ts +0 -32
  194. package/tests/v0.3.test.ts +0 -813
  195. package/tree-shake-test.js +0 -1
  196. package/tsconfig.json +0 -15
  197. package/vitest.config.ts +0 -22
  198. package/vitest_play.ts +0 -7
@@ -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
- })