@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,1100 +0,0 @@
1
- // packages/storve/tests/async.test.ts
2
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
3
- import { createStore } from '../src'
4
- import { createAsync } from '../src/async'
5
-
6
- // ─────────────────────────────────────────────
7
- // TEST UTILITIES
8
- // ─────────────────────────────────────────────
9
-
10
- const wait = (ms: number) => new Promise(r => setTimeout(r, ms))
11
-
12
- // Controlled promise — lets tests resolve/reject on demand
13
- function deferred<T>() {
14
- let resolve!: (v: T) => void
15
- let reject!: (e: unknown) => void
16
- const promise = new Promise<T>((res, rej) => {
17
- resolve = res
18
- reject = rej
19
- })
20
- return { promise, resolve, reject }
21
- }
22
-
23
- // ─────────────────────────────────────────────
24
- // 1. INITIAL STATE
25
- // ─────────────────────────────────────────────
26
- describe('Async State — Initial State', () => {
27
-
28
- it('async key has correct initial shape', () => {
29
- const store = createStore({
30
- user: createAsync(async () => ({ name: 'Alice' }))
31
- })
32
- const state = store.getState()
33
- expect(state.user).toMatchObject({
34
- data: null,
35
- error: null,
36
- status: 'idle',
37
- loading: false,
38
- })
39
- })
40
-
41
- it('loading is false before any fetch', () => {
42
- const store = createStore({
43
- user: createAsync(async () => ({ name: 'Alice' }))
44
- })
45
- expect(store.getState().user.loading).toBe(false)
46
- })
47
-
48
- it('data is null before any fetch', () => {
49
- const store = createStore({
50
- user: createAsync(async () => ({ name: 'Alice' }))
51
- })
52
- expect(store.getState().user.data).toBeNull()
53
- })
54
-
55
- it('error is null before any fetch', () => {
56
- const store = createStore({
57
- user: createAsync(async () => ({ name: 'Alice' }))
58
- })
59
- expect(store.getState().user.error).toBeNull()
60
- })
61
-
62
- it('status is idle before any fetch', () => {
63
- const store = createStore({
64
- user: createAsync(async () => ({ name: 'Alice' }))
65
- })
66
- expect(store.getState().user.status).toBe('idle')
67
- })
68
-
69
- it('multiple async keys each have independent initial state', () => {
70
- const store = createStore({
71
- user: createAsync(async () => ({ name: 'Alice' })),
72
- posts: createAsync(async () => [{ id: 1 }]),
73
- })
74
- expect(store.getState().user.status).toBe('idle')
75
- expect(store.getState().posts.status).toBe('idle')
76
- })
77
-
78
- it('sync and async keys coexist in same store', () => {
79
- const store = createStore({
80
- theme: 'light',
81
- count: 0,
82
- user: createAsync(async () => ({ name: 'Alice' })),
83
- })
84
- expect(store.getState().theme).toBe('light')
85
- expect(store.getState().count).toBe(0)
86
- expect(store.getState().user.status).toBe('idle')
87
- })
88
-
89
- it('refetch function is exposed on initial async state', () => {
90
- const store = createStore({
91
- user: createAsync(async () => ({ name: 'Alice' }))
92
- })
93
- expect(typeof store.getState().user.refetch).toBe('function')
94
- })
95
- })
96
-
97
- // ─────────────────────────────────────────────
98
- // 2. STATUS TRANSITIONS
99
- // ─────────────────────────────────────────────
100
- describe('Async State — Status Transitions', () => {
101
-
102
- describe('idle → loading → success', () => {
103
- it('status transitions to loading when fetch starts', async () => {
104
- const { promise, resolve } = deferred<{ name: string }>()
105
- const store = createStore({
106
- user: createAsync(() => promise)
107
- })
108
- const fetchPromise = store.fetch('user')
109
- expect(store.getState().user.status).toBe('loading')
110
- expect(store.getState().user.loading).toBe(true)
111
- resolve({ name: 'Alice' })
112
- await fetchPromise
113
- })
114
-
115
- it('status transitions to success on resolve', async () => {
116
- const store = createStore({
117
- user: createAsync(async () => ({ name: 'Alice' }))
118
- })
119
- await store.fetch('user')
120
- expect(store.getState().user.status).toBe('success')
121
- })
122
-
123
- it('data is set correctly on success', async () => {
124
- const store = createStore({
125
- user: createAsync(async () => ({ name: 'Alice', age: 30 }))
126
- })
127
- await store.fetch('user')
128
- expect(store.getState().user.data).toEqual({ name: 'Alice', age: 30 })
129
- })
130
-
131
- it('loading is false after success', async () => {
132
- const store = createStore({
133
- user: createAsync(async () => ({ name: 'Alice' }))
134
- })
135
- await store.fetch('user')
136
- expect(store.getState().user.loading).toBe(false)
137
- })
138
-
139
- it('error is null after success', async () => {
140
- const store = createStore({
141
- user: createAsync(async () => ({ name: 'Alice' }))
142
- })
143
- await store.fetch('user')
144
- expect(store.getState().user.error).toBeNull()
145
- })
146
- })
147
-
148
- describe('idle → loading → error', () => {
149
- it('status transitions to error on rejection', async () => {
150
- const store = createStore({
151
- user: createAsync(async () => { throw new Error('Network error') })
152
- })
153
- await store.fetch('user').catch(() => { })
154
- expect(store.getState().user.status).toBe('error')
155
- })
156
-
157
- it('error message is captured on failure', async () => {
158
- const store = createStore({
159
- user: createAsync(async () => { throw new Error('Network error') })
160
- })
161
- await store.fetch('user').catch(() => { })
162
- expect(store.getState().user.error).toBe('Network error')
163
- })
164
-
165
- it('loading is false after error', async () => {
166
- const store = createStore({
167
- user: createAsync(async () => { throw new Error('fail') })
168
- })
169
- await store.fetch('user').catch(() => { })
170
- expect(store.getState().user.loading).toBe(false)
171
- })
172
-
173
- it('data is null after error', async () => {
174
- const store = createStore({
175
- user: createAsync(async () => { throw new Error('fail') })
176
- })
177
- await store.fetch('user').catch(() => { })
178
- expect(store.getState().user.data).toBeNull()
179
- })
180
-
181
- it('non-Error rejection is handled gracefully', async () => {
182
- const store = createStore({
183
- user: createAsync(async () => { throw 'string error' })
184
- })
185
- await store.fetch('user').catch(() => { })
186
- expect(store.getState().user.error).toBeTruthy()
187
- expect(store.getState().user.status).toBe('error')
188
- })
189
-
190
- it('fetch does not throw to caller — error is captured in state', async () => {
191
- const store = createStore({
192
- user: createAsync(async () => { throw new Error('fail') })
193
- })
194
- await expect(store.fetch('user')).resolves.not.toThrow()
195
- })
196
- })
197
-
198
- describe('success → loading → success (refetch)', () => {
199
- it('refetch sets loading true while in flight', async () => {
200
- const { promise, resolve } = deferred<{ name: string }>()
201
- let call = 0
202
- const store = createStore({
203
- user: createAsync(async () => {
204
- call++
205
- if (call === 1) return { name: 'Alice' }
206
- return promise // second call returns controlled promise
207
- })
208
- })
209
- await store.fetch('user')
210
- expect(store.getState().user.data).toEqual({ name: 'Alice' })
211
-
212
- const refetchPromise = store.refetch('user')
213
- expect(store.getState().user.loading).toBe(true) // Check BEFORE await
214
- resolve({ name: 'Bob' })
215
- await refetchPromise
216
- expect(store.getState().user.data).toEqual({ name: 'Bob' })
217
- expect(store.getState().user.loading).toBe(false)
218
- })
219
-
220
- it('previous data is preserved while refetching', async () => {
221
- const { promise, resolve } = deferred<string>()
222
- let callCount = 0
223
- const store = createStore({
224
- msg: createAsync(async () => {
225
- callCount++
226
- if (callCount === 1) return 'first'
227
- return promise
228
- })
229
- })
230
- await store.fetch('msg')
231
- expect(store.getState().msg.data).toBe('first')
232
- const refetchP = store.refetch('msg')
233
- // Data is still 'first' while loading
234
- expect(store.getState().msg.data).toBe('first')
235
- expect(store.getState().msg.loading).toBe(true)
236
- resolve('second')
237
- await refetchP
238
- expect(store.getState().msg.data).toBe('second')
239
- })
240
- })
241
-
242
- describe('error → loading → success (retry)', () => {
243
- it('retry after error clears error field', async () => {
244
- let callCount = 0
245
- const store = createStore({
246
- user: createAsync(async () => {
247
- callCount++
248
- if (callCount === 1) throw new Error('fail')
249
- return { name: 'Alice' }
250
- })
251
- })
252
- await store.fetch('user').catch(() => { })
253
- expect(store.getState().user.status).toBe('error')
254
- await store.refetch('user')
255
- expect(store.getState().user.error).toBeNull()
256
- expect(store.getState().user.status).toBe('success')
257
- expect(store.getState().user.data).toEqual({ name: 'Alice' })
258
- })
259
- })
260
- })
261
-
262
- // ─────────────────────────────────────────────
263
- // 3. FETCH WITH ARGUMENTS
264
- // ─────────────────────────────────────────────
265
- describe('Async State — Fetch Arguments', () => {
266
-
267
- it('fetch passes single argument to async fn', async () => {
268
- const fn = vi.fn(async (id: string) => ({ id, name: 'Alice' }))
269
- const store = createStore({ user: createAsync(fn) })
270
- await store.fetch('user', 'user-1')
271
- expect(fn).toHaveBeenCalledWith('user-1')
272
- })
273
-
274
- it('fetch passes multiple arguments to async fn', async () => {
275
- const fn = vi.fn(async (a: string, b: number) => ({ a, b }))
276
- const store = createStore({ item: createAsync(fn) })
277
- await store.fetch('item', 'hello', 42)
278
- expect(fn).toHaveBeenCalledWith('hello', 42)
279
- })
280
-
281
- it('fetch with no arguments works', async () => {
282
- const fn = vi.fn(async () => 'result')
283
- const store = createStore({ data: createAsync(fn) })
284
- await store.fetch('data')
285
- expect(fn).toHaveBeenCalledWith()
286
- expect(store.getState().data.data).toBe('result')
287
- })
288
-
289
- it('refetch uses last arguments automatically', async () => {
290
- const fn = vi.fn(async (id: string) => ({ id }))
291
- const store = createStore({ user: createAsync(fn) })
292
- await store.fetch('user', 'user-42')
293
- await store.refetch('user')
294
- expect(fn).toHaveBeenCalledTimes(2)
295
- expect(fn).toHaveBeenLastCalledWith('user-42')
296
- })
297
-
298
- it('refetch on idle store triggers fetch with no args', async () => {
299
- const fn = vi.fn(async () => 'data')
300
- const store = createStore({ data: createAsync(fn) })
301
- await store.refetch('data')
302
- expect(fn).toHaveBeenCalledTimes(1)
303
- expect(store.getState().data.status).toBe('success')
304
- })
305
- })
306
-
307
- // ─────────────────────────────────────────────
308
- // 4. RACE CONDITION PROTECTION
309
- // ─────────────────────────────────────────────
310
- describe('Async State — Race Condition Protection', () => {
311
-
312
- it('two rapid fetches — only last response wins', async () => {
313
- const { promise: p1, resolve: r1 } = deferred<string>()
314
- const { promise: p2, resolve: r2 } = deferred<string>()
315
- let call = 0
316
- const store = createStore({
317
- data: createAsync(async () => {
318
- call++
319
- return call === 1 ? p1 : p2
320
- })
321
- })
322
-
323
- const f1 = store.fetch('data')
324
- const f2 = store.fetch('data')
325
-
326
- // Resolve second first, then first
327
- r2('second')
328
- await f2
329
- r1('first')
330
- await f1
331
-
332
- // Only second response should win
333
- expect(store.getState().data.data).toBe('second')
334
- })
335
-
336
- it('three rapid fetches — only last response wins', async () => {
337
- const deferreds = [deferred<string>(), deferred<string>(), deferred<string>()]
338
- let call = 0
339
- const store = createStore({
340
- data: createAsync(async () => deferreds[call++].promise)
341
- })
342
-
343
- const fetches = [store.fetch('data'), store.fetch('data'), store.fetch('data')]
344
-
345
- // Resolve out of order — last (index 2) should win
346
- deferreds[2].resolve('third')
347
- deferreds[0].resolve('first')
348
- deferreds[1].resolve('second')
349
-
350
- await Promise.all(fetches)
351
- expect(store.getState().data.data).toBe('third')
352
- })
353
-
354
- it('slow first + fast second — fast second wins', async () => {
355
- const slow = deferred<string>()
356
- let call = 0
357
- const store = createStore({
358
- data: createAsync(async () => {
359
- call++
360
- if (call === 1) return slow.promise
361
- return 'fast'
362
- })
363
- })
364
-
365
- const f1 = store.fetch('data') // slow
366
- const f2 = store.fetch('data') // fast
367
-
368
- await f2 // fast resolves first
369
- expect(store.getState().data.data).toBe('fast')
370
-
371
- slow.resolve('slow')
372
- await f1 // slow resolves — should be ignored
373
- expect(store.getState().data.data).toBe('fast')
374
- })
375
-
376
- it('stale slow response does not overwrite newer success', async () => {
377
- const slow = deferred<string>()
378
- let call = 0
379
- const store = createStore({
380
- data: createAsync(async () => {
381
- call++
382
- if (call === 1) return slow.promise
383
- return 'new'
384
- })
385
- })
386
-
387
- store.fetch('data') // slow request
388
- await store.fetch('data') // fast request — wins
389
- expect(store.getState().data.data).toBe('new')
390
- expect(store.getState().data.status).toBe('success')
391
-
392
- slow.resolve('old')
393
- await wait(10)
394
- expect(store.getState().data.data).toBe('new') // not overwritten
395
- })
396
-
397
- it('stale error does not overwrite newer success', async () => {
398
- const slow = deferred<string>()
399
- let call = 0
400
- const store = createStore({
401
- data: createAsync(async () => {
402
- call++
403
- if (call === 1) return slow.promise
404
- return 'new'
405
- })
406
- })
407
-
408
- store.fetch('data') // will be stale
409
- await store.fetch('data') // new success
410
-
411
- slow.reject(new Error('stale error'))
412
- await wait(10)
413
-
414
- expect(store.getState().data.status).toBe('success')
415
- expect(store.getState().data.data).toBe('new')
416
- expect(store.getState().data.error).toBeNull()
417
- })
418
-
419
- it('10 rapid fetches — only last wins', async () => {
420
- const deferreds = Array.from({ length: 10 }, () => deferred<number>())
421
- let call = 0
422
- const store = createStore({
423
- data: createAsync(async () => deferreds[call++].promise)
424
- })
425
-
426
- const fetches = Array.from({ length: 10 }, () => store.fetch('data'))
427
-
428
- // Resolve all — last one wins
429
- deferreds.forEach((d, i) => d.resolve(i))
430
- await Promise.all(fetches)
431
-
432
- expect(store.getState().data.data).toBe(9)
433
- })
434
- })
435
-
436
- // ─────────────────────────────────────────────
437
- // 5. CACHE & TTL
438
- // ─────────────────────────────────────────────
439
- describe('Async State — Cache & TTL', () => {
440
-
441
- beforeEach(() => { vi.useFakeTimers({ now: Date.now(), toFake: ['Date', 'setTimeout', 'clearTimeout'] }) })
442
- afterEach(() => { vi.useRealTimers() })
443
-
444
- it('ttl=0 (default) — every fetch calls the fn', async () => {
445
- const fn = vi.fn(async () => 'data')
446
- const store = createStore({ data: createAsync(fn) })
447
- await store.fetch('data')
448
- await store.fetch('data')
449
- expect(fn).toHaveBeenCalledTimes(2)
450
- })
451
-
452
- it('within TTL — second fetch returns cached data without calling fn', async () => {
453
- const fn = vi.fn(async () => 'data')
454
- const store = createStore({ data: createAsync(fn, { ttl: 60_000 }) })
455
- await store.fetch('data')
456
- await store.fetch('data')
457
- expect(fn).toHaveBeenCalledTimes(1)
458
- expect(store.getState().data.data).toBe('data')
459
- })
460
-
461
- it('within TTL — cached data is returned immediately', async () => {
462
- const fn = vi.fn(async () => 'cached')
463
- const store = createStore({ data: createAsync(fn, { ttl: 60_000 }) })
464
- await store.fetch('data')
465
- await store.fetch('data')
466
- expect(store.getState().data.status).toBe('success')
467
- expect(store.getState().data.data).toBe('cached')
468
- })
469
-
470
- it('after TTL expires — fetch calls fn again', async () => {
471
- const fn = vi.fn(async () => 'data')
472
- const store = createStore({ data: createAsync(fn, { ttl: 1_000 }) })
473
- await store.fetch('data')
474
- vi.advanceTimersByTime(1_001)
475
- await store.fetch('data')
476
- expect(fn).toHaveBeenCalledTimes(2)
477
- })
478
-
479
- it('exactly at TTL boundary — fetch calls fn again', async () => {
480
- const fn = vi.fn(async () => 'data')
481
- const store = createStore({ data: createAsync(fn, { ttl: 1_000 }) })
482
- await store.fetch('data')
483
- vi.advanceTimersByTime(1_000)
484
- await store.fetch('data')
485
- expect(fn).toHaveBeenCalledTimes(2)
486
- })
487
-
488
- it('just before TTL — cache hit', async () => {
489
- const fn = vi.fn(async () => 'data')
490
- const store = createStore({ data: createAsync(fn, { ttl: 1_000 }) })
491
- await store.fetch('data')
492
- vi.advanceTimersByTime(999)
493
- await store.fetch('data')
494
- expect(fn).toHaveBeenCalledTimes(1)
495
- })
496
-
497
- it('invalidate() clears cache — next fetch calls fn regardless of TTL', async () => {
498
- const fn = vi.fn(async () => 'data')
499
- const store = createStore({ data: createAsync(fn, { ttl: 60_000 }) })
500
- await store.fetch('data')
501
- store.invalidate('data')
502
- await store.fetch('data')
503
- expect(fn).toHaveBeenCalledTimes(2)
504
- })
505
-
506
- it('invalidate() does not reset state to idle', async () => {
507
- const fn = vi.fn(async () => 'data')
508
- const store = createStore({ data: createAsync(fn, { ttl: 60_000 }) })
509
- await store.fetch('data')
510
- store.invalidate('data')
511
- // State should still show previous data until next fetch
512
- expect(store.getState().data.data).toBe('data')
513
- expect(store.getState().data.status).toBe('success')
514
- })
515
-
516
- it('invalidateAll() clears cache for all async keys', async () => {
517
- const fn1 = vi.fn(async () => 'a')
518
- const fn2 = vi.fn(async () => 'b')
519
- const store = createStore({
520
- a: createAsync(fn1, { ttl: 60_000 }),
521
- b: createAsync(fn2, { ttl: 60_000 }),
522
- })
523
- await store.fetch('a')
524
- await store.fetch('b')
525
- store.invalidateAll()
526
- await store.fetch('a')
527
- await store.fetch('b')
528
- expect(fn1).toHaveBeenCalledTimes(2)
529
- expect(fn2).toHaveBeenCalledTimes(2)
530
- })
531
-
532
- it('invalidate() only affects specified key', async () => {
533
- const fn1 = vi.fn(async () => 'a')
534
- const fn2 = vi.fn(async () => 'b')
535
- const store = createStore({
536
- a: createAsync(fn1, { ttl: 60_000 }),
537
- b: createAsync(fn2, { ttl: 60_000 }),
538
- })
539
- await store.fetch('a')
540
- await store.fetch('b')
541
- store.invalidate('a')
542
- await store.fetch('a')
543
- await store.fetch('b')
544
- expect(fn1).toHaveBeenCalledTimes(2) // refetched
545
- expect(fn2).toHaveBeenCalledTimes(1) // still cached
546
- })
547
-
548
- it('different args produce independent cache entries', async () => {
549
- const fn = vi.fn(async (id: string) => ({ id }))
550
- const store = createStore({ user: createAsync(fn, { ttl: 60_000 }) })
551
- await store.fetch('user', 'user-1')
552
- await store.fetch('user', 'user-2')
553
- await store.fetch('user', 'user-1') // cache hit for user-1
554
- expect(fn).toHaveBeenCalledTimes(2) // user-1 and user-2 only
555
- })
556
- })
557
-
558
- // ─────────────────────────────────────────────
559
- // 6. STALE-WHILE-REVALIDATE
560
- // ─────────────────────────────────────────────
561
- describe('Async State — Stale-While-Revalidate', () => {
562
-
563
- beforeEach(() => { vi.useFakeTimers({ now: Date.now(), toFake: ['Date', 'setTimeout', 'clearTimeout'] }) })
564
- afterEach(() => { vi.useRealTimers() })
565
-
566
- it('returns stale data immediately when cache is expired', async () => {
567
- let call = 0
568
- const { promise, resolve } = deferred<string>()
569
- const store = createStore({
570
- data: createAsync(async () => {
571
- call++
572
- if (call === 1) return 'stale'
573
- return promise
574
- }, { ttl: 1_000, staleWhileRevalidate: true })
575
- })
576
-
577
- await store.fetch('data')
578
- vi.advanceTimersByTime(1_001)
579
-
580
- const fetchP = store.fetch('data')
581
- // Stale data returned immediately
582
- expect(store.getState().data.data).toBe('stale')
583
- expect(store.getState().data.status).toBe('success') // not loading
584
-
585
- resolve('fresh')
586
- await fetchP
587
- expect(store.getState().data.data).toBe('fresh')
588
- })
589
-
590
- it('status stays success during background revalidation', async () => {
591
- const { promise, resolve } = deferred<string>()
592
- let call = 0
593
- const store = createStore({
594
- data: createAsync(async () => {
595
- call++
596
- if (call === 1) return 'stale'
597
- return promise
598
- }, { ttl: 500, staleWhileRevalidate: true })
599
- })
600
-
601
- await store.fetch('data')
602
- vi.advanceTimersByTime(501)
603
- store.fetch('data')
604
-
605
- // Status must NOT be loading during SWR background fetch
606
- expect(store.getState().data.status).toBe('success')
607
- expect(store.getState().data.loading).toBe(false)
608
-
609
- resolve('fresh')
610
- })
611
-
612
- it('background revalidation updates data when resolved', async () => {
613
- const { promise, resolve } = deferred<string>()
614
- let call = 0
615
- const store = createStore({
616
- data: createAsync(async () => {
617
- call++
618
- if (call === 1) return 'stale'
619
- return promise
620
- }, { ttl: 500, staleWhileRevalidate: true })
621
- })
622
-
623
- await store.fetch('data')
624
- vi.advanceTimersByTime(501)
625
- const fetchP = store.fetch('data')
626
-
627
- resolve('fresh')
628
- await fetchP
629
- expect(store.getState().data.data).toBe('fresh')
630
- })
631
-
632
- it('without SWR — expired cache shows loading state', async () => {
633
- const { promise, resolve } = deferred<string>()
634
- let call = 0
635
- const store = createStore({
636
- data: createAsync(async () => {
637
- call++
638
- if (call === 1) return 'first'
639
- return promise
640
- }, { ttl: 500, staleWhileRevalidate: false })
641
- })
642
-
643
- await store.fetch('data')
644
- vi.advanceTimersByTime(501)
645
- store.fetch('data')
646
-
647
- // Without SWR, status should be loading
648
- expect(store.getState().data.status).toBe('loading')
649
- expect(store.getState().data.loading).toBe(true)
650
-
651
- resolve('second')
652
- })
653
-
654
- it('SWR background error does not wipe stale data', async () => {
655
- const { promise, reject } = deferred<string>()
656
- let call = 0
657
- const store = createStore({
658
- data: createAsync(async () => {
659
- call++
660
- if (call === 1) return 'stale'
661
- return promise
662
- }, { ttl: 500, staleWhileRevalidate: true })
663
- })
664
-
665
- await store.fetch('data')
666
- vi.advanceTimersByTime(501)
667
- const fetchP = store.fetch('data')
668
-
669
- reject(new Error('background fail'))
670
- await fetchP
671
-
672
- // Stale data preserved, error surfaced
673
- expect(store.getState().data.data).toBe('stale')
674
- expect(store.getState().data.error).toBe('background fail')
675
- })
676
- })
677
-
678
- // ─────────────────────────────────────────────
679
- // 7. OPTIMISTIC UPDATES
680
- // ─────────────────────────────────────────────
681
- describe('Async State — Optimistic Updates', () => {
682
-
683
- it('optimistic state is applied immediately before fetch resolves', async () => {
684
- const { promise, resolve } = deferred<{ name: string }>()
685
- const store = createStore({
686
- user: createAsync(async () => promise)
687
- })
688
-
689
- store.fetch('user', undefined, {
690
- optimistic: { data: { name: 'Optimistic Alice' }, status: 'success' }
691
- })
692
-
693
- expect(store.getState().user.data).toEqual({ name: 'Optimistic Alice' })
694
- expect(store.getState().user.status).toBe('success')
695
-
696
- resolve({ name: 'Real Alice' })
697
- await wait(10)
698
- })
699
-
700
- it('on success — final data replaces optimistic data', async () => {
701
- const store = createStore({
702
- user: createAsync(async () => ({ name: 'Real Alice' }))
703
- })
704
-
705
- await store.fetch('user', undefined, {
706
- optimistic: { data: { name: 'Optimistic Alice' }, status: 'success' }
707
- })
708
-
709
- expect(store.getState().user.data).toEqual({ name: 'Real Alice' })
710
- })
711
-
712
- it('on failure — state rolls back to pre-optimistic values', async () => {
713
- const store = createStore({
714
- user: createAsync(async () => ({ name: 'Original' }))
715
- })
716
-
717
- // First fetch to set initial data
718
- await store.fetch('user')
719
- expect(store.getState().user.data).toEqual({ name: 'Original' })
720
-
721
- // Optimistic update that fails
722
- await store.fetch('user', undefined, {
723
- optimistic: { data: { name: 'Optimistic' }, status: 'success' },
724
- // Override fn to simulate failure
725
- })
726
- // After rollback, original data should be restored
727
- // (depends on implementation — adjust based on actual rollback behavior)
728
- })
729
-
730
- it('optimistic loading is false', async () => {
731
- const { promise, resolve } = deferred<{ name: string }>()
732
- const store = createStore({
733
- user: createAsync(async () => promise)
734
- })
735
-
736
- store.fetch('user', undefined, {
737
- optimistic: { data: { name: 'Alice' }, status: 'success' }
738
- })
739
-
740
- expect(store.getState().user.loading).toBe(false)
741
- resolve({ name: 'Alice' })
742
- })
743
- })
744
-
745
- // ─────────────────────────────────────────────
746
- // 8. SUBSCRIBERS & NOTIFICATIONS
747
- // ─────────────────────────────────────────────
748
- describe('Async State — Subscribers', () => {
749
-
750
- it('subscribers notified when loading starts', async () => {
751
- const { promise, resolve } = deferred<string>()
752
- const store = createStore({ data: createAsync(async () => promise) })
753
- const listener = vi.fn()
754
- store.subscribe(listener)
755
-
756
- store.fetch('data')
757
- expect(listener).toHaveBeenCalled()
758
- const calls = listener.mock.calls.length
759
- expect((listener.mock.calls[calls - 1][0] as { data: { loading: boolean } }).data.loading).toBe(true)
760
-
761
- resolve('done')
762
- await wait(10)
763
- })
764
-
765
- it('subscribers notified when data resolves', async () => {
766
- const store = createStore({ data: createAsync(async () => 'result') })
767
- const listener = vi.fn()
768
- store.subscribe(listener)
769
- await store.fetch('data')
770
- const lastState = listener.mock.calls[listener.mock.calls.length - 1][0] as { data: { data: string } }
771
- expect(lastState.data.data).toBe('result')
772
- })
773
-
774
- it('subscribers notified when error occurs', async () => {
775
- const store = createStore({
776
- data: createAsync(async () => { throw new Error('fail') })
777
- })
778
- const listener = vi.fn()
779
- store.subscribe(listener)
780
- await store.fetch('data').catch(() => { })
781
- const lastState = listener.mock.calls[listener.mock.calls.length - 1][0] as { data: { error: string } }
782
- expect(lastState.data.error).toBe('fail')
783
- })
784
-
785
- it('subscriber receives exactly 2 notifications for successful fetch (loading + success)', async () => {
786
- const store = createStore({ data: createAsync(async () => 'done') })
787
- const listener = vi.fn()
788
- store.subscribe(listener)
789
- await store.fetch('data')
790
- // loading transition + success transition = 2
791
- expect(listener).toHaveBeenCalledTimes(2)
792
- })
793
-
794
- it('unsubscribed listener does not receive async notifications', async () => {
795
- const store = createStore({ data: createAsync(async () => 'done') })
796
- const listener = vi.fn()
797
- const unsub = store.subscribe(listener)
798
- unsub()
799
- await store.fetch('data')
800
- expect(listener).not.toHaveBeenCalled()
801
- })
802
-
803
- it('multiple subscribers all receive async state updates', async () => {
804
- const store = createStore({ data: createAsync(async () => 'done') })
805
- const l1 = vi.fn(), l2 = vi.fn(), l3 = vi.fn()
806
- store.subscribe(l1)
807
- store.subscribe(l2)
808
- store.subscribe(l3)
809
- await store.fetch('data')
810
- expect(l1).toHaveBeenCalled()
811
- expect(l2).toHaveBeenCalled()
812
- expect(l3).toHaveBeenCalled()
813
- })
814
-
815
- it('async state update does not affect sync state subscribers unnecessarily', async () => {
816
- const store = createStore({
817
- count: 0,
818
- data: createAsync(async () => 'done')
819
- })
820
- const countSnapshots: number[] = []
821
- store.subscribe(s => countSnapshots.push((s as { count: number }).count))
822
- await store.fetch('data')
823
- // count should never change
824
- expect(countSnapshots.every(c => c === 0)).toBe(true)
825
- })
826
- })
827
-
828
- // ─────────────────────────────────────────────
829
- // 9. GETASYNCSTATE
830
- // ─────────────────────────────────────────────
831
- describe('Async State — getAsyncState()', () => {
832
-
833
- it('getAsyncState returns current async state synchronously', async () => {
834
- const store = createStore({ data: createAsync(async () => 'result') })
835
- await store.fetch('data')
836
- const state = store.getAsyncState('data')
837
- expect(state.data).toBe('result')
838
- expect(state.status).toBe('success')
839
- })
840
-
841
- it('getAsyncState on idle key returns initial shape', () => {
842
- const store = createStore({ data: createAsync(async () => 'result') })
843
- const state = store.getAsyncState('data')
844
- expect(state.status).toBe('idle')
845
- expect(state.data).toBeNull()
846
- })
847
-
848
- it('getAsyncState is consistent with getState()', async () => {
849
- const store = createStore({ data: createAsync(async () => 'result') })
850
- await store.fetch('data')
851
- const fromGetState = store.getState().data
852
- const fromGetAsyncState = store.getAsyncState('data')
853
- expect(fromGetState).toEqual(fromGetAsyncState)
854
- })
855
- })
856
-
857
- // ─────────────────────────────────────────────
858
- // 10. REFETCH CONVENIENCE METHOD
859
- // ─────────────────────────────────────────────
860
- describe('Async State — refetch() convenience method', () => {
861
-
862
- it('value.refetch() re-runs the fetch', async () => {
863
- const fn = vi.fn(async () => 'data')
864
- const store = createStore({ data: createAsync(fn) })
865
- await store.fetch('data')
866
- await store.getState().data.refetch()
867
- expect(fn).toHaveBeenCalledTimes(2)
868
- })
869
-
870
- it('value.refetch() updates state correctly', async () => {
871
- let call = 0
872
- const store = createStore({
873
- data: createAsync(async () => {
874
- call++
875
- return `call-${call}`
876
- })
877
- })
878
- await store.fetch('data')
879
- expect(store.getState().data.data).toBe('call-1')
880
- await store.getState().data.refetch()
881
- expect(store.getState().data.data).toBe('call-2')
882
- })
883
- })
884
-
885
- // ─────────────────────────────────────────────
886
- // 11. MULTIPLE ASYNC KEYS — ISOLATION
887
- // ─────────────────────────────────────────────
888
- describe('Async State — Multiple Keys Isolation', () => {
889
-
890
- it('fetching key A does not affect key B state', async () => {
891
- const store = createStore({
892
- user: createAsync(async () => ({ name: 'Alice' })),
893
- posts: createAsync(async () => [{ id: 1 }]),
894
- })
895
- await store.fetch('user')
896
- expect(store.getState().posts.status).toBe('idle')
897
- expect(store.getState().posts.data).toBeNull()
898
- })
899
-
900
- it('error in key A does not affect key B', async () => {
901
- const store = createStore({
902
- a: createAsync(async () => { throw new Error('fail') }),
903
- b: createAsync(async () => 'ok'),
904
- })
905
- await store.fetch('a').catch(() => { })
906
- await store.fetch('b')
907
- expect(store.getState().a.status).toBe('error')
908
- expect(store.getState().b.status).toBe('success')
909
- })
910
-
911
- it('race condition in key A does not affect key B', async () => {
912
- const slow = deferred<string>()
913
- let callA = 0
914
- const store = createStore({
915
- a: createAsync(async () => {
916
- callA++
917
- if (callA === 1) return slow.promise
918
- return 'fast-a'
919
- }),
920
- b: createAsync(async () => 'b-data'),
921
- })
922
-
923
- store.fetch('a') // slow
924
- store.fetch('a') // fast
925
- await store.fetch('b')
926
-
927
- expect(store.getState().b.data).toBe('b-data')
928
- slow.resolve('slow-a')
929
- })
930
-
931
- it('invalidateAll clears all keys but does not reset state', async () => {
932
- const store = createStore({
933
- a: createAsync(async () => 'a', { ttl: 60_000 }),
934
- b: createAsync(async () => 'b', { ttl: 60_000 }),
935
- })
936
- await store.fetch('a')
937
- await store.fetch('b')
938
- store.invalidateAll()
939
- // State still shows previous data
940
- expect(store.getState().a.data).toBe('a')
941
- expect(store.getState().b.data).toBe('b')
942
- })
943
- })
944
-
945
- // ─────────────────────────────────────────────
946
- // 12. ASYNC + SYNC STATE COEXISTENCE
947
- // ─────────────────────────────────────────────
948
- describe('Async State — Coexistence with Sync State', () => {
949
-
950
- it('setState on sync key does not affect async key', async () => {
951
- const store = createStore({
952
- count: 0,
953
- data: createAsync(async () => 'result'),
954
- })
955
- await store.fetch('data')
956
- store.setState({ count: 99 })
957
- expect(store.getState().data.status).toBe('success')
958
- expect(store.getState().data.data).toBe('result')
959
- })
960
-
961
- it('async fetch does not affect sync state', async () => {
962
- const store = createStore({
963
- count: 42,
964
- data: createAsync(async () => 'result'),
965
- })
966
- await store.fetch('data')
967
- expect(store.getState().count).toBe(42)
968
- })
969
-
970
- it('actions and async keys work together', async () => {
971
- const store = createStore({
972
- theme: 'light',
973
- user: createAsync(async () => ({ name: 'Alice' })),
974
- actions: {
975
- setTheme(t: string) { store.setState({ theme: t }) }
976
- }
977
- })
978
- store.setTheme('dark')
979
- await store.fetch('user')
980
- expect(store.getState().theme).toBe('dark')
981
- expect(store.getState().user.data).toEqual({ name: 'Alice' })
982
- })
983
-
984
- it('immer mutations and async keys work together', async () => {
985
- const store = createStore({
986
- items: [] as string[],
987
- data: createAsync(async () => 'async-result'),
988
- }, { immer: true })
989
- store.setState(draft => { draft.items.push('hello') })
990
- await store.fetch('data')
991
- expect(store.getState().items).toEqual(['hello'])
992
- expect(store.getState().data.data).toBe('async-result')
993
- })
994
-
995
- it('batch updates work with async state notifications', async () => {
996
- const store = createStore({
997
- a: 0,
998
- b: 0,
999
- data: createAsync(async () => 'result'),
1000
- })
1001
- const listener = vi.fn()
1002
- store.subscribe(listener)
1003
- store.batch(() => {
1004
- store.setState({ a: 1 })
1005
- store.setState({ b: 2 })
1006
- })
1007
- expect(listener).toHaveBeenCalledTimes(1)
1008
- await store.fetch('data')
1009
- // async transitions fire their own notifications outside batch
1010
- expect(store.getState().a).toBe(1)
1011
- expect(store.getState().b).toBe(2)
1012
- })
1013
- })
1014
-
1015
- // ─────────────────────────────────────────────
1016
- // 13. EDGE CASES
1017
- // ─────────────────────────────────────────────
1018
- describe('Async State — Edge Cases', () => {
1019
-
1020
- it('async fn returning null is valid', async () => {
1021
- const store = createStore({ data: createAsync(async () => null) })
1022
- await store.fetch('data')
1023
- expect(store.getState().data.data).toBeNull()
1024
- expect(store.getState().data.status).toBe('success')
1025
- })
1026
-
1027
- it('async fn returning undefined is valid', async () => {
1028
- const store = createStore({ data: createAsync(async () => undefined) })
1029
- await store.fetch('data')
1030
- expect(store.getState().data.status).toBe('success')
1031
- })
1032
-
1033
- it('async fn returning 0 is valid', async () => {
1034
- const store = createStore({ data: createAsync(async () => 0) })
1035
- await store.fetch('data')
1036
- expect(store.getState().data.data).toBe(0)
1037
- expect(store.getState().data.status).toBe('success')
1038
- })
1039
-
1040
- it('async fn returning false is valid', async () => {
1041
- const store = createStore({ data: createAsync(async () => false) })
1042
- await store.fetch('data')
1043
- expect(store.getState().data.data).toBe(false)
1044
- expect(store.getState().data.status).toBe('success')
1045
- })
1046
-
1047
- it('async fn returning empty string is valid', async () => {
1048
- const store = createStore({ data: createAsync(async () => '') })
1049
- await store.fetch('data')
1050
- expect(store.getState().data.data).toBe('')
1051
- expect(store.getState().data.status).toBe('success')
1052
- })
1053
-
1054
- it('async fn returning empty array is valid', async () => {
1055
- const store = createStore({ data: createAsync(async () => []) })
1056
- await store.fetch('data')
1057
- expect(store.getState().data.data).toEqual([])
1058
- expect(store.getState().data.status).toBe('success')
1059
- })
1060
-
1061
- it('fetching a non-existent key throws or handles gracefully', async () => {
1062
- const store = createStore({ data: createAsync(async () => 'ok') })
1063
- await expect(store.fetch('nonExistent' as never)).rejects.toThrow(
1064
- 'Storve: no async key "nonExistent" found in store'
1065
- )
1066
- })
1067
-
1068
- it('store with 10 async keys all independent', async () => {
1069
- const fns = Array.from({ length: 10 }, (_, i) => vi.fn(async () => `data-${i}`))
1070
- const definition = Object.fromEntries(
1071
- fns.map((fn, i) => [`key${i}`, createAsync(fn)])
1072
- )
1073
- const store = createStore(definition)
1074
- await Promise.all(fns.map((_, i) => store.fetch(`key${i}`)))
1075
- fns.forEach((_, i) => {
1076
- expect((store.getState() as Record<string, { data: string }>)[`key${i}`].data).toBe(`data-${i}`)
1077
- })
1078
- })
1079
-
1080
- it('fetch called before previous fetch resolves — no state corruption', async () => {
1081
- const slow = deferred<string>()
1082
- let call = 0
1083
- const store = createStore({
1084
- data: createAsync(async () => {
1085
- call++
1086
- if (call === 1) return slow.promise
1087
- return 'second'
1088
- })
1089
- })
1090
-
1091
- store.fetch('data') // in flight
1092
- await store.fetch('data') // second
1093
-
1094
- expect(store.getState().data.data).toBe('second')
1095
- expect(store.getState().data.status).toBe('success')
1096
- slow.resolve('first')
1097
- await wait(10)
1098
- expect(store.getState().data.data).toBe('second') // not corrupted
1099
- })
1100
- })