@ersbeth/picoflow 1.0.0 → 1.1.0

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 (180) hide show
  1. package/.cursor/plans/unifier-flowresource-avec-flowderivation-c9506e24.plan.md +372 -0
  2. package/README.md +25 -171
  3. package/biome.json +4 -1
  4. package/dist/picoflow.js +1129 -661
  5. package/dist/types/flow/base/flowDisposable.d.ts +67 -0
  6. package/dist/types/flow/base/flowDisposable.d.ts.map +1 -0
  7. package/dist/types/flow/base/flowEffect.d.ts +127 -0
  8. package/dist/types/flow/base/flowEffect.d.ts.map +1 -0
  9. package/dist/types/flow/base/flowGraph.d.ts +97 -0
  10. package/dist/types/flow/base/flowGraph.d.ts.map +1 -0
  11. package/dist/types/flow/base/flowSignal.d.ts +134 -0
  12. package/dist/types/flow/base/flowSignal.d.ts.map +1 -0
  13. package/dist/types/flow/base/flowTracker.d.ts +15 -0
  14. package/dist/types/flow/base/flowTracker.d.ts.map +1 -0
  15. package/dist/types/flow/base/index.d.ts +7 -0
  16. package/dist/types/flow/base/index.d.ts.map +1 -0
  17. package/dist/types/flow/base/utils.d.ts +20 -0
  18. package/dist/types/flow/base/utils.d.ts.map +1 -0
  19. package/dist/types/{advanced/array.d.ts → flow/collections/flowArray.d.ts} +50 -12
  20. package/dist/types/flow/collections/flowArray.d.ts.map +1 -0
  21. package/dist/types/flow/collections/flowMap.d.ts +224 -0
  22. package/dist/types/flow/collections/flowMap.d.ts.map +1 -0
  23. package/dist/types/flow/collections/index.d.ts +3 -0
  24. package/dist/types/flow/collections/index.d.ts.map +1 -0
  25. package/dist/types/flow/index.d.ts +4 -0
  26. package/dist/types/flow/index.d.ts.map +1 -0
  27. package/dist/types/flow/nodes/async/flowConstantAsync.d.ts +137 -0
  28. package/dist/types/flow/nodes/async/flowConstantAsync.d.ts.map +1 -0
  29. package/dist/types/flow/nodes/async/flowDerivationAsync.d.ts +137 -0
  30. package/dist/types/flow/nodes/async/flowDerivationAsync.d.ts.map +1 -0
  31. package/dist/types/flow/nodes/async/flowNodeAsync.d.ts +343 -0
  32. package/dist/types/flow/nodes/async/flowNodeAsync.d.ts.map +1 -0
  33. package/dist/types/flow/nodes/async/flowReadonlyAsync.d.ts +81 -0
  34. package/dist/types/flow/nodes/async/flowReadonlyAsync.d.ts.map +1 -0
  35. package/dist/types/flow/nodes/async/flowStateAsync.d.ts +111 -0
  36. package/dist/types/flow/nodes/async/flowStateAsync.d.ts.map +1 -0
  37. package/dist/types/flow/nodes/async/index.d.ts +6 -0
  38. package/dist/types/flow/nodes/async/index.d.ts.map +1 -0
  39. package/dist/types/flow/nodes/index.d.ts +3 -0
  40. package/dist/types/flow/nodes/index.d.ts.map +1 -0
  41. package/dist/types/flow/nodes/sync/flowConstant.d.ts +108 -0
  42. package/dist/types/flow/nodes/sync/flowConstant.d.ts.map +1 -0
  43. package/dist/types/flow/nodes/sync/flowDerivation.d.ts +100 -0
  44. package/dist/types/flow/nodes/sync/flowDerivation.d.ts.map +1 -0
  45. package/dist/types/flow/nodes/sync/flowNode.d.ts +314 -0
  46. package/dist/types/flow/nodes/sync/flowNode.d.ts.map +1 -0
  47. package/dist/types/flow/nodes/sync/flowReadonly.d.ts +57 -0
  48. package/dist/types/flow/nodes/sync/flowReadonly.d.ts.map +1 -0
  49. package/dist/types/flow/nodes/sync/flowState.d.ts +96 -0
  50. package/dist/types/flow/nodes/sync/flowState.d.ts.map +1 -0
  51. package/dist/types/flow/nodes/sync/index.d.ts +6 -0
  52. package/dist/types/flow/nodes/sync/index.d.ts.map +1 -0
  53. package/dist/types/index.d.ts +1 -4
  54. package/dist/types/index.d.ts.map +1 -1
  55. package/dist/types/solid/converters.d.ts +34 -44
  56. package/dist/types/solid/converters.d.ts.map +1 -1
  57. package/dist/types/solid/primitives.d.ts +1 -0
  58. package/dist/types/solid/primitives.d.ts.map +1 -1
  59. package/docs/.vitepress/config.mts +1 -1
  60. package/docs/api/typedoc-sidebar.json +81 -1
  61. package/package.json +60 -58
  62. package/src/flow/base/flowDisposable.ts +71 -0
  63. package/src/flow/base/flowEffect.ts +171 -0
  64. package/src/flow/base/flowGraph.ts +288 -0
  65. package/src/flow/base/flowSignal.ts +207 -0
  66. package/src/flow/base/flowTracker.ts +17 -0
  67. package/src/flow/base/index.ts +6 -0
  68. package/src/flow/base/utils.ts +19 -0
  69. package/src/flow/collections/flowArray.ts +409 -0
  70. package/src/flow/collections/flowMap.ts +398 -0
  71. package/src/flow/collections/index.ts +2 -0
  72. package/src/flow/index.ts +3 -0
  73. package/src/flow/nodes/async/flowConstantAsync.ts +142 -0
  74. package/src/flow/nodes/async/flowDerivationAsync.ts +143 -0
  75. package/src/flow/nodes/async/flowNodeAsync.ts +474 -0
  76. package/src/flow/nodes/async/flowReadonlyAsync.ts +81 -0
  77. package/src/flow/nodes/async/flowStateAsync.ts +116 -0
  78. package/src/flow/nodes/async/index.ts +5 -0
  79. package/src/flow/nodes/await/advanced/index.ts +5 -0
  80. package/src/{advanced → flow/nodes/await/advanced}/resource.ts +37 -3
  81. package/src/{advanced → flow/nodes/await/advanced}/resourceAsync.ts +35 -3
  82. package/src/{advanced → flow/nodes/await/advanced}/stream.ts +40 -2
  83. package/src/{advanced → flow/nodes/await/advanced}/streamAsync.ts +38 -3
  84. package/src/flow/nodes/await/flowConstantAwait.ts +154 -0
  85. package/src/flow/nodes/await/flowDerivationAwait.ts +154 -0
  86. package/src/flow/nodes/await/flowNodeAwait.ts +508 -0
  87. package/src/flow/nodes/await/flowReadonlyAwait.ts +89 -0
  88. package/src/flow/nodes/await/flowStateAwait.ts +130 -0
  89. package/src/flow/nodes/await/index.ts +5 -0
  90. package/src/flow/nodes/index.ts +3 -0
  91. package/src/flow/nodes/sync/flowConstant.ts +111 -0
  92. package/src/flow/nodes/sync/flowDerivation.ts +105 -0
  93. package/src/flow/nodes/sync/flowNode.ts +439 -0
  94. package/src/flow/nodes/sync/flowReadonly.ts +57 -0
  95. package/src/flow/nodes/sync/flowState.ts +101 -0
  96. package/src/flow/nodes/sync/index.ts +5 -0
  97. package/src/index.ts +1 -47
  98. package/src/solid/converters.ts +59 -198
  99. package/src/solid/primitives.ts +4 -0
  100. package/test/base/flowEffect.test.ts +108 -0
  101. package/test/base/flowGraph.test.ts +485 -0
  102. package/test/base/flowSignal.test.ts +372 -0
  103. package/test/collections/flowArray.asyncStates.test.ts +1553 -0
  104. package/test/collections/flowArray.scalars.test.ts +1129 -0
  105. package/test/collections/flowArray.states.test.ts +1365 -0
  106. package/test/collections/flowMap.asyncStates.test.ts +1105 -0
  107. package/test/collections/flowMap.scalars.test.ts +877 -0
  108. package/test/collections/flowMap.states.test.ts +1097 -0
  109. package/test/nodes/async/flowConstantAsync.test.ts +860 -0
  110. package/test/nodes/async/flowDerivationAsync.test.ts +1517 -0
  111. package/test/nodes/async/flowStateAsync.test.ts +1387 -0
  112. package/test/{resource.test.ts → nodes/await/advanced/resource.test.ts} +21 -19
  113. package/test/{resourceAsync.test.ts → nodes/await/advanced/resourceAsync.test.ts} +3 -1
  114. package/test/{stream.test.ts → nodes/await/advanced/stream.test.ts} +30 -28
  115. package/test/{streamAsync.test.ts → nodes/await/advanced/streamAsync.test.ts} +16 -14
  116. package/test/nodes/await/flowConstantAwait.test.ts +643 -0
  117. package/test/nodes/await/flowDerivationAwait.test.ts +1583 -0
  118. package/test/nodes/await/flowStateAwait.test.ts +999 -0
  119. package/test/nodes/mixed/derivation.test.ts +1527 -0
  120. package/test/nodes/sync/flowConstant.test.ts +620 -0
  121. package/test/nodes/sync/flowDerivation.test.ts +1373 -0
  122. package/test/nodes/sync/flowState.test.ts +945 -0
  123. package/test/solid/converters.test.ts +721 -0
  124. package/test/solid/primitives.test.ts +1031 -0
  125. package/tsconfig.json +2 -1
  126. package/vitest.config.ts +7 -1
  127. package/IMPLEMENTATION_GUIDE.md +0 -1578
  128. package/dist/types/advanced/array.d.ts.map +0 -1
  129. package/dist/types/advanced/index.d.ts +0 -9
  130. package/dist/types/advanced/index.d.ts.map +0 -1
  131. package/dist/types/advanced/map.d.ts +0 -166
  132. package/dist/types/advanced/map.d.ts.map +0 -1
  133. package/dist/types/advanced/resource.d.ts +0 -78
  134. package/dist/types/advanced/resource.d.ts.map +0 -1
  135. package/dist/types/advanced/resourceAsync.d.ts +0 -56
  136. package/dist/types/advanced/resourceAsync.d.ts.map +0 -1
  137. package/dist/types/advanced/stream.d.ts +0 -117
  138. package/dist/types/advanced/stream.d.ts.map +0 -1
  139. package/dist/types/advanced/streamAsync.d.ts +0 -97
  140. package/dist/types/advanced/streamAsync.d.ts.map +0 -1
  141. package/dist/types/basic/constant.d.ts +0 -60
  142. package/dist/types/basic/constant.d.ts.map +0 -1
  143. package/dist/types/basic/derivation.d.ts +0 -89
  144. package/dist/types/basic/derivation.d.ts.map +0 -1
  145. package/dist/types/basic/disposable.d.ts +0 -82
  146. package/dist/types/basic/disposable.d.ts.map +0 -1
  147. package/dist/types/basic/effect.d.ts +0 -67
  148. package/dist/types/basic/effect.d.ts.map +0 -1
  149. package/dist/types/basic/index.d.ts +0 -10
  150. package/dist/types/basic/index.d.ts.map +0 -1
  151. package/dist/types/basic/observable.d.ts +0 -83
  152. package/dist/types/basic/observable.d.ts.map +0 -1
  153. package/dist/types/basic/signal.d.ts +0 -69
  154. package/dist/types/basic/signal.d.ts.map +0 -1
  155. package/dist/types/basic/state.d.ts +0 -47
  156. package/dist/types/basic/state.d.ts.map +0 -1
  157. package/dist/types/basic/trackingContext.d.ts +0 -33
  158. package/dist/types/basic/trackingContext.d.ts.map +0 -1
  159. package/dist/types/creators.d.ts +0 -340
  160. package/dist/types/creators.d.ts.map +0 -1
  161. package/src/advanced/array.ts +0 -222
  162. package/src/advanced/index.ts +0 -12
  163. package/src/advanced/map.ts +0 -193
  164. package/src/basic/constant.ts +0 -97
  165. package/src/basic/derivation.ts +0 -147
  166. package/src/basic/disposable.ts +0 -86
  167. package/src/basic/effect.ts +0 -104
  168. package/src/basic/index.ts +0 -9
  169. package/src/basic/observable.ts +0 -109
  170. package/src/basic/signal.ts +0 -145
  171. package/src/basic/state.ts +0 -60
  172. package/src/basic/trackingContext.ts +0 -45
  173. package/src/creators.ts +0 -395
  174. package/test/array.test.ts +0 -600
  175. package/test/constant.test.ts +0 -44
  176. package/test/derivation.test.ts +0 -539
  177. package/test/effect.test.ts +0 -29
  178. package/test/map.test.ts +0 -240
  179. package/test/signal.test.ts +0 -72
  180. package/test/state.test.ts +0 -212
@@ -0,0 +1,1583 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { derivationAwait, effect, signal, state, stateAwait } from "#package";
3
+
4
+ describe("flowDerivationAwait", () => {
5
+ describe("unit", () => {
6
+ describe("initialization", () => {
7
+ it("should not call compute function on creation", () => {
8
+ const computeFn = vi.fn(async () => Promise.resolve(42));
9
+ derivationAwait(computeFn);
10
+
11
+ expect(computeFn).not.toHaveBeenCalled();
12
+ });
13
+
14
+ it("should call compute function on first access", async () => {
15
+ const computeFn = vi.fn(async () => Promise.resolve(100));
16
+ const $state = stateAwait(Promise.resolve(1));
17
+ const $derivation = derivationAwait(async (t) => {
18
+ computeFn();
19
+ const val = $state.get(t);
20
+ return Promise.resolve((val ?? 0) * 2);
21
+ });
22
+
23
+ expect(computeFn).not.toHaveBeenCalled();
24
+ await $derivation.pick();
25
+ expect(computeFn).toHaveBeenCalledTimes(1);
26
+ });
27
+
28
+ it("should call compute function again when dependency changes", async () => {
29
+ const $state = stateAwait(Promise.resolve(1));
30
+ const computeFn = vi.fn(async (t) => {
31
+ const val = $state.get(t);
32
+ return Promise.resolve((val ?? 0) * 2);
33
+ });
34
+ const $derivation = derivationAwait(computeFn);
35
+
36
+ await $derivation.pick();
37
+ expect(computeFn).toHaveBeenCalledTimes(1);
38
+
39
+ await $state.set(Promise.resolve(2));
40
+ await $derivation.pick();
41
+ expect(computeFn).toHaveBeenCalledTimes(2);
42
+ });
43
+
44
+ it("should handle number return values", async () => {
45
+ const $state = stateAwait(Promise.resolve(5));
46
+ const $derivation = derivationAwait(async (t) => {
47
+ const val = $state.get(t);
48
+ return Promise.resolve((val ?? 0) * 2);
49
+ });
50
+ expect(await $derivation.pick()).toBe(10);
51
+ });
52
+
53
+ it("should handle string return values", async () => {
54
+ const $state = stateAwait(Promise.resolve("hello"));
55
+ const $derivation = derivationAwait(async (t) => {
56
+ const val = $state.get(t);
57
+ return Promise.resolve(val?.toUpperCase() ?? "");
58
+ });
59
+ expect(await $derivation.pick()).toBe("HELLO");
60
+ });
61
+
62
+ it("should handle object return values", async () => {
63
+ const $state = stateAwait(Promise.resolve({ a: 1 }));
64
+ const $derivation = derivationAwait(async (t) => {
65
+ const val = $state.get(t);
66
+ return Promise.resolve({
67
+ ...(val ?? {}),
68
+ b: 2,
69
+ });
70
+ });
71
+ const value = await $derivation.pick();
72
+ expect(value).toEqual({ a: 1, b: 2 });
73
+ });
74
+
75
+ it("should handle array return values", async () => {
76
+ const $state = stateAwait(Promise.resolve([1, 2]));
77
+ const $derivation = derivationAwait(async (t) => {
78
+ const val = $state.get(t);
79
+ return Promise.resolve([...(val ?? []), 3]);
80
+ });
81
+ const value = await $derivation.pick();
82
+ expect(value).toEqual([1, 2, 3]);
83
+ });
84
+
85
+ it("should handle null return values", async () => {
86
+ const $state = stateAwait(Promise.resolve(true));
87
+ const $derivation = derivationAwait(async (t) => {
88
+ const val = $state.get(t);
89
+ return Promise.resolve(val ? null : "not null");
90
+ });
91
+ expect(await $derivation.pick()).toBeNull();
92
+ });
93
+
94
+ it("should handle undefined return values", async () => {
95
+ const $state = stateAwait(Promise.resolve(true));
96
+ const $derivation = derivationAwait(async (t) => {
97
+ const val = $state.get(t);
98
+ return Promise.resolve(val ? undefined : "defined");
99
+ });
100
+ expect(await $derivation.pick()).toBeUndefined();
101
+ });
102
+ });
103
+
104
+ describe("get", () => {
105
+ it("should return computed value with tracking context", async () => {
106
+ const $state = stateAwait(Promise.resolve(5));
107
+ const $derivation = derivationAwait(async (t) => {
108
+ const val = $state.get(t);
109
+ return Promise.resolve((val ?? 0) * 2);
110
+ });
111
+ const $tracker = state(0);
112
+
113
+ // Wait for Promise to resolve first
114
+ await $derivation.pick();
115
+
116
+ const value = $derivation.get($tracker);
117
+ expect(value).toBe(10);
118
+ });
119
+
120
+ it("should return undefined if Promise not yet resolved", () => {
121
+ // Create a Promise that resolves after a delay
122
+ const $state = stateAwait(
123
+ new Promise<number>((resolve) => setTimeout(() => resolve(10), 100)),
124
+ );
125
+ const $derivation = derivationAwait(async (t) => {
126
+ const val = $state.get(t);
127
+ return Promise.resolve((val ?? 0) * 2);
128
+ });
129
+ const $tracker = state(0);
130
+
131
+ // Before Promise resolves, get() should return undefined
132
+ const value = $derivation.get($tracker);
133
+ expect(value).toBeUndefined();
134
+ });
135
+
136
+ it("should compute lazy value on first get call", async () => {
137
+ const $state = stateAwait(Promise.resolve(10));
138
+ const computeFn = vi.fn(async (t) => {
139
+ const val = $state.get(t);
140
+ return (val ?? 0) * 2;
141
+ });
142
+ const $derivation = derivationAwait(async (t) => {
143
+ return computeFn(t);
144
+ });
145
+ const $tracker = state(0);
146
+
147
+ expect(computeFn).not.toHaveBeenCalled();
148
+
149
+ // First get() may return undefined, but triggers computation
150
+ const value = $derivation.get($tracker);
151
+ await vi.waitFor(() => expect(computeFn).toHaveBeenCalledTimes(1));
152
+ expect(value).toBeUndefined();
153
+
154
+ // Wait for Promise to resolve
155
+ await $derivation.pick();
156
+ const value2 = $derivation.get($tracker);
157
+ await vi.waitFor(() => expect(computeFn).toHaveBeenCalledTimes(2));
158
+ expect(value2).toBe(20);
159
+ });
160
+
161
+ it("should recompute when dependency changes", async () => {
162
+ const $state = stateAwait(Promise.resolve(1));
163
+ const $derivation = derivationAwait(async (t) => {
164
+ const val = $state.get(t);
165
+ return Promise.resolve((val ?? 0) * 2);
166
+ });
167
+ const $tracker = state(0);
168
+
169
+ // Wait for initial computation
170
+ await $derivation.pick();
171
+ expect($derivation.get($tracker)).toBe(2);
172
+
173
+ await $state.set(Promise.resolve(2));
174
+ // Wait for recomputation
175
+ await $derivation.pick();
176
+ expect($derivation.get($tracker)).toBe(4);
177
+ });
178
+
179
+ it("should throw error when get is called after disposal", async () => {
180
+ const $state = stateAwait(Promise.resolve(1));
181
+ const $derivation = derivationAwait(async (t) => {
182
+ const val = $state.get(t);
183
+ return Promise.resolve((val ?? 0) * 2);
184
+ });
185
+ const $tracker = state(0);
186
+
187
+ await $derivation.pick();
188
+ $derivation.get($tracker);
189
+ $derivation.dispose();
190
+
191
+ expect(() => $derivation.get($tracker)).toThrow(
192
+ "[PicoFlow] Primitive is disposed",
193
+ );
194
+ });
195
+ });
196
+
197
+ describe("pick", () => {
198
+ it("should return computed value without tracking", async () => {
199
+ const $state = stateAwait(Promise.resolve(15));
200
+ const $derivation = derivationAwait(async (t) => {
201
+ const val = $state.get(t);
202
+ return Promise.resolve((val ?? 0) * 2);
203
+ });
204
+ const value = await $derivation.pick();
205
+ expect(value).toBe(30);
206
+ });
207
+
208
+ it("should compute lazy value on first pick call", async () => {
209
+ const computeFn = vi.fn(async (t) => {
210
+ const $s = stateAwait(Promise.resolve(25));
211
+ const val = $s.get(t);
212
+ return Promise.resolve((val ?? 0) * 2);
213
+ });
214
+ const $state = stateAwait(Promise.resolve(25));
215
+ const $derivation = derivationAwait(async (t) => {
216
+ computeFn(t);
217
+ const val = $state.get(t);
218
+ return Promise.resolve((val ?? 0) * 2);
219
+ });
220
+
221
+ expect(computeFn).not.toHaveBeenCalled();
222
+ const value = await $derivation.pick();
223
+ expect(value).toBe(50);
224
+ expect(computeFn).toHaveBeenCalledTimes(1);
225
+ });
226
+
227
+ it("should recompute when dependency changes", async () => {
228
+ const $state = stateAwait(Promise.resolve(1));
229
+ const $derivation = derivationAwait(async (t) => {
230
+ const val = $state.get(t);
231
+ return Promise.resolve((val ?? 0) * 2);
232
+ });
233
+
234
+ expect(await $derivation.pick()).toBe(2);
235
+
236
+ await $state.set(Promise.resolve(2));
237
+ expect(await $derivation.pick()).toBe(4);
238
+ });
239
+
240
+ it("should await Promise to get value", async () => {
241
+ const $state = stateAwait(Promise.resolve(35));
242
+ const $derivation = derivationAwait(async (t) => {
243
+ const val = $state.get(t);
244
+ return Promise.resolve((val ?? 0) * 2);
245
+ });
246
+ const value = await $derivation.pick();
247
+ expect(value).toBe(70);
248
+ });
249
+
250
+ it("should throw error when pick is called after disposal", async () => {
251
+ const $state = stateAwait(Promise.resolve(1));
252
+ const $derivation = derivationAwait(async (t) => {
253
+ const val = $state.get(t);
254
+ return Promise.resolve((val ?? 0) * 2);
255
+ });
256
+
257
+ expect(await $derivation.pick()).toBe(2);
258
+
259
+ $derivation.dispose();
260
+
261
+ await expect($derivation.pick()).rejects.toThrow(
262
+ "[PicoFlow] Primitive is disposed",
263
+ );
264
+ });
265
+ });
266
+
267
+ describe("refresh", () => {
268
+ it("should force recomputation", async () => {
269
+ const $state = stateAwait(Promise.resolve(1));
270
+ const computeFn = vi.fn(async (t) => {
271
+ const val = $state.get(t);
272
+ return Promise.resolve((val ?? 0) * 2);
273
+ });
274
+ const $derivation = derivationAwait(computeFn);
275
+
276
+ await $derivation.pick();
277
+ expect(computeFn).toHaveBeenCalledTimes(1);
278
+
279
+ await $derivation.refresh();
280
+ expect(computeFn).toHaveBeenCalledTimes(2);
281
+ });
282
+
283
+ it("should return Promise<void>", async () => {
284
+ const $state = stateAwait(Promise.resolve(1));
285
+ const $derivation = derivationAwait(async (t) => {
286
+ const val = $state.get(t);
287
+ return Promise.resolve((val ?? 0) * 2);
288
+ });
289
+ const result = await $derivation.refresh();
290
+ expect(result).toBeUndefined();
291
+ });
292
+
293
+ it("should not notify when refresh returns same value", async () => {
294
+ const $state = stateAwait(Promise.resolve(5));
295
+ const $derivation = derivationAwait(async (t) => {
296
+ const val = $state.get(t);
297
+ return (val ?? 0) * 2;
298
+ });
299
+ const listener = vi.fn();
300
+ $derivation.subscribe(listener);
301
+
302
+ await vi.waitFor(() => expect(listener).toHaveBeenCalledTimes(2));
303
+ expect(listener).toHaveBeenNthCalledWith(1, undefined);
304
+ expect(listener).toHaveBeenNthCalledWith(2, 10);
305
+
306
+ // Value is 10, refresh will compute same value
307
+ await $derivation.refresh();
308
+
309
+ // Wait to ensure no additional calls
310
+ await new Promise((resolve) => setTimeout(resolve, 50));
311
+ await vi.waitFor(() => expect(listener).toHaveBeenCalledTimes(2));
312
+ });
313
+
314
+ it("should notify when refresh returns different value", async () => {
315
+ let multiplier = 2;
316
+ const $state = stateAwait(Promise.resolve(5));
317
+ const $derivation = derivationAwait(async (t) => {
318
+ const val = $state.get(t);
319
+ return (val ?? 0) * multiplier;
320
+ });
321
+ const listener = vi.fn();
322
+ $derivation.subscribe(listener);
323
+
324
+ await vi.waitFor(() => expect(listener).toHaveBeenCalledTimes(2));
325
+ expect(listener).toHaveBeenNthCalledWith(1, undefined);
326
+ expect(listener).toHaveBeenNthCalledWith(2, 10);
327
+
328
+ multiplier = 3;
329
+ await $derivation.refresh();
330
+ await vi.waitFor(() => expect(listener).toHaveBeenCalledTimes(3));
331
+ expect(listener).toHaveBeenNthCalledWith(3, 15);
332
+ });
333
+
334
+ it("should throw error when refresh is called after disposal", async () => {
335
+ const $state = stateAwait(Promise.resolve(1));
336
+ const $derivation = derivationAwait(async (t) => {
337
+ const val = $state.get(t);
338
+ return Promise.resolve((val ?? 0) * 2);
339
+ });
340
+
341
+ $derivation.dispose();
342
+
343
+ await expect($derivation.refresh()).rejects.toThrow(
344
+ "[PicoFlow] Primitive is disposed",
345
+ );
346
+ });
347
+
348
+ it("should recompute even if not dirty", async () => {
349
+ const $state = stateAwait(Promise.resolve(1));
350
+ const computeFn = vi.fn(async (t) => {
351
+ const val = $state.get(t);
352
+ return Promise.resolve((val ?? 0) * 2);
353
+ });
354
+ const $derivation = derivationAwait(computeFn);
355
+
356
+ await $derivation.pick();
357
+ expect(computeFn).toHaveBeenCalledTimes(1);
358
+
359
+ // Not dirty, but refresh forces recomputation
360
+ await $derivation.refresh();
361
+ expect(computeFn).toHaveBeenCalledTimes(2);
362
+ });
363
+ });
364
+
365
+ describe("watch", () => {
366
+ it("should register dependency when watch is called", () => {
367
+ const $state = stateAwait(Promise.resolve(35));
368
+ const $derivation = derivationAwait(async (t) => {
369
+ const val = $state.get(t);
370
+ return Promise.resolve((val ?? 0) * 2);
371
+ });
372
+ const $tracker = state(0);
373
+
374
+ expect(() => $derivation.watch($tracker)).not.toThrow();
375
+ });
376
+
377
+ it("should compute lazy value when watch is called", async () => {
378
+ const computeFn = vi.fn(async (t) => {
379
+ const $s = stateAwait(Promise.resolve(40));
380
+ const val = $s.get(t);
381
+ return Promise.resolve((val ?? 0) * 2);
382
+ });
383
+ const $state = stateAwait(Promise.resolve(40));
384
+ const $derivation = derivationAwait(async (t) => {
385
+ computeFn(t);
386
+ const val = $state.get(t);
387
+ return Promise.resolve((val ?? 0) * 2);
388
+ });
389
+ const $tracker = state(0);
390
+
391
+ expect(computeFn).not.toHaveBeenCalled();
392
+ $derivation.watch($tracker);
393
+ // Wait a bit for the computation to complete
394
+ await new Promise((resolve) => setTimeout(resolve, 10));
395
+ expect(computeFn).toHaveBeenCalledTimes(1);
396
+ });
397
+
398
+ it("should throw error when watch is called after disposal", () => {
399
+ const $state = stateAwait(Promise.resolve(1));
400
+ const $derivation = derivationAwait(async (t) => {
401
+ const val = $state.get(t);
402
+ return Promise.resolve((val ?? 0) * 2);
403
+ });
404
+ const $tracker = state(0);
405
+
406
+ $derivation.watch($tracker);
407
+ $derivation.dispose();
408
+
409
+ expect(() => $derivation.watch($tracker)).toThrow(
410
+ "[PicoFlow] Primitive is disposed",
411
+ );
412
+ });
413
+ });
414
+
415
+ describe("subscribe", () => {
416
+ it("should return a disposer function", () => {
417
+ const $state = stateAwait(Promise.resolve(50));
418
+ const $derivation = derivationAwait(async (t) => {
419
+ const val = $state.get(t);
420
+ return Promise.resolve((val ?? 0) * 2);
421
+ });
422
+ const listener = vi.fn();
423
+ const disposer = $derivation.subscribe(listener);
424
+
425
+ expect(typeof disposer).toBe("function");
426
+ });
427
+
428
+ it("should call listener immediately with computed value (T | undefined)", async () => {
429
+ const $state = stateAwait(Promise.resolve(55));
430
+ const $derivation = derivationAwait(async (t) => {
431
+ const val = $state.get(t);
432
+ return (val ?? 0) * 2;
433
+ });
434
+ const listener = vi.fn();
435
+
436
+ $derivation.subscribe(listener);
437
+
438
+ await vi.waitFor(() => expect(listener).toHaveBeenCalledTimes(2));
439
+ expect(listener).toHaveBeenNthCalledWith(1, undefined);
440
+ expect(listener).toHaveBeenNthCalledWith(2, 110);
441
+ });
442
+
443
+ it("should call listener when value changes", async () => {
444
+ const $state = stateAwait(Promise.resolve(60));
445
+ const $derivation = derivationAwait(async (t) => {
446
+ const val = $state.get(t);
447
+ return (val ?? 0) * 2;
448
+ });
449
+ const listener = vi.fn();
450
+
451
+ $derivation.subscribe(listener);
452
+ await vi.waitFor(() => expect(listener).toHaveBeenCalledTimes(2));
453
+ expect(listener).toHaveBeenNthCalledWith(1, undefined);
454
+ expect(listener).toHaveBeenNthCalledWith(2, 120);
455
+
456
+ await $state.set(70);
457
+ await vi.waitFor(() => expect(listener).toHaveBeenCalledTimes(4));
458
+ expect(listener).toHaveBeenNthCalledWith(3, 120);
459
+ expect(listener).toHaveBeenNthCalledWith(4, 140);
460
+
461
+ await $state.set(Promise.resolve(80));
462
+ await vi.waitFor(() => expect(listener).toHaveBeenCalledTimes(6));
463
+ expect(listener).toHaveBeenNthCalledWith(5, 140);
464
+ expect(listener).toHaveBeenNthCalledWith(6, 160);
465
+ });
466
+
467
+ it("should not call listener when value does not change", async () => {
468
+ const $state = stateAwait(Promise.resolve(75));
469
+ const $derivation = derivationAwait(async (t) => {
470
+ const val = $state.get(t);
471
+ return (val ?? 0) * 2;
472
+ });
473
+ const listener = vi.fn();
474
+
475
+ $derivation.subscribe(listener);
476
+ await vi.waitFor(() => expect(listener).toHaveBeenCalledTimes(2));
477
+ expect(listener).toHaveBeenNthCalledWith(1, undefined);
478
+ expect(listener).toHaveBeenNthCalledWith(2, 150);
479
+
480
+ await $state.set(Promise.resolve(75));
481
+ await new Promise((resolve) => setTimeout(resolve, 50));
482
+ expect(listener).toHaveBeenCalledTimes(2);
483
+ });
484
+
485
+ it("should support multiple subscriptions", async () => {
486
+ const $state = stateAwait(Promise.resolve(80));
487
+ const $derivation = derivationAwait(async (t) => {
488
+ const val = $state.get(t);
489
+ return Promise.resolve((val ?? 0) * 2);
490
+ });
491
+ const listener1 = vi.fn();
492
+ const listener2 = vi.fn();
493
+ const listener3 = vi.fn();
494
+
495
+ $derivation.subscribe(listener1);
496
+ $derivation.subscribe(listener2);
497
+ $derivation.subscribe(listener3);
498
+
499
+ await vi.waitFor(() => {
500
+ expect(listener1).toHaveBeenCalledTimes(2);
501
+ expect(listener2).toHaveBeenCalledTimes(2);
502
+ expect(listener3).toHaveBeenCalledTimes(2);
503
+ });
504
+
505
+ expect(listener1).toHaveBeenNthCalledWith(1, undefined);
506
+ expect(listener1).toHaveBeenNthCalledWith(2, 160);
507
+ expect(listener2).toHaveBeenNthCalledWith(1, undefined);
508
+ expect(listener2).toHaveBeenNthCalledWith(2, 160);
509
+ expect(listener3).toHaveBeenNthCalledWith(1, undefined);
510
+ expect(listener3).toHaveBeenNthCalledWith(2, 160);
511
+ });
512
+
513
+ it("should throw error when subscribe is called after disposal", () => {
514
+ const $state = stateAwait(Promise.resolve(1));
515
+ const $derivation = derivationAwait(async (t) => {
516
+ const val = $state.get(t);
517
+ return Promise.resolve((val ?? 0) * 2);
518
+ });
519
+ const listener = vi.fn();
520
+
521
+ $derivation.dispose();
522
+
523
+ expect(() => $derivation.subscribe(listener)).toThrow(
524
+ "[PicoFlow] Primitive is disposed",
525
+ );
526
+ });
527
+
528
+ it("should dispose effect when disposer is called", async () => {
529
+ const $state = stateAwait(Promise.resolve(85));
530
+ const $derivation = derivationAwait(async (t) => {
531
+ const val = $state.get(t);
532
+ return (val ?? 0) * 2;
533
+ });
534
+ const listener = vi.fn();
535
+
536
+ const unsubscribe = $derivation.subscribe(listener);
537
+ await vi.waitFor(() => expect(listener).toHaveBeenCalledTimes(2));
538
+ expect(listener).toHaveBeenNthCalledWith(1, undefined);
539
+ expect(listener).toHaveBeenNthCalledWith(2, 170);
540
+
541
+ await $state.set(Promise.resolve(90));
542
+ await vi.waitFor(() => expect(listener).toHaveBeenCalledTimes(4));
543
+ expect(listener).toHaveBeenNthCalledWith(3, 170);
544
+ expect(listener).toHaveBeenNthCalledWith(4, 180);
545
+
546
+ unsubscribe();
547
+
548
+ await $state.set(Promise.resolve(95));
549
+ await new Promise((resolve) => setTimeout(resolve, 50));
550
+ expect(listener).toHaveBeenCalledTimes(4);
551
+ });
552
+ });
553
+
554
+ describe("trigger", () => {
555
+ it("should return a Promise", () => {
556
+ const $state = stateAwait(Promise.resolve(1));
557
+ const $derivation = derivationAwait(async (t) => {
558
+ const val = $state.get(t);
559
+ return Promise.resolve((val ?? 0) * 2);
560
+ });
561
+ const result = $derivation.trigger();
562
+ expect(result).toBeInstanceOf(Promise);
563
+ });
564
+
565
+ it("should throw error when trigger is called after disposal", async () => {
566
+ const $state = stateAwait(Promise.resolve(1));
567
+ const $derivation = derivationAwait(async (t) => {
568
+ const val = $state.get(t);
569
+ return Promise.resolve((val ?? 0) * 2);
570
+ });
571
+ $derivation.dispose();
572
+ await expect($derivation.trigger()).rejects.toThrow(
573
+ "[PicoFlow] Primitive is disposed",
574
+ );
575
+ });
576
+
577
+ it("should allow multiple triggers", async () => {
578
+ const $state = stateAwait(Promise.resolve(1));
579
+ const $derivation = derivationAwait(async (t) => {
580
+ const val = $state.get(t);
581
+ return Promise.resolve((val ?? 0) * 2);
582
+ });
583
+ const promise1 = $derivation.trigger();
584
+ const promise2 = $derivation.trigger();
585
+ const promise3 = $derivation.trigger();
586
+
587
+ expect(promise1).toBeInstanceOf(Promise);
588
+ expect(promise2).toBeInstanceOf(Promise);
589
+ expect(promise3).toBeInstanceOf(Promise);
590
+
591
+ await Promise.all([promise1, promise2, promise3]);
592
+ });
593
+ });
594
+
595
+ describe("disposal", () => {
596
+ it("should have disposed property set to false initially", () => {
597
+ const $state = stateAwait(Promise.resolve(1));
598
+ const $derivation = derivationAwait(async (t) => {
599
+ const val = $state.get(t);
600
+ return Promise.resolve((val ?? 0) * 2);
601
+ });
602
+ expect($derivation.disposed).toBe(false);
603
+ });
604
+
605
+ it("should have disposed property set to true after disposal", () => {
606
+ const $state = stateAwait(Promise.resolve(1));
607
+ const $derivation = derivationAwait(async (t) => {
608
+ const val = $state.get(t);
609
+ return Promise.resolve((val ?? 0) * 2);
610
+ });
611
+ $derivation.dispose();
612
+ expect($derivation.disposed).toBe(true);
613
+ });
614
+
615
+ it("should throw error when disposed twice", () => {
616
+ const $state = stateAwait(Promise.resolve(1));
617
+ const $derivation = derivationAwait(async (t) => {
618
+ const val = $state.get(t);
619
+ return Promise.resolve((val ?? 0) * 2);
620
+ });
621
+ $derivation.dispose();
622
+ expect(() => $derivation.dispose()).toThrow(
623
+ "[PicoFlow] Primitive is disposed",
624
+ );
625
+ });
626
+
627
+ it("should accept self option without throwing", () => {
628
+ const $state = stateAwait(Promise.resolve(1));
629
+ const $derivation = derivationAwait(async (t) => {
630
+ const val = $state.get(t);
631
+ return Promise.resolve((val ?? 0) * 2);
632
+ });
633
+ expect(() => $derivation.dispose({ self: true })).not.toThrow();
634
+ expect($derivation.disposed).toBe(true);
635
+ });
636
+ });
637
+
638
+ describe("special cases", () => {
639
+ it("should handle dynamic dependencies - dependencies change between computations", async () => {
640
+ const $state1 = stateAwait(Promise.resolve(1));
641
+ const $state2 = stateAwait(Promise.resolve(10));
642
+ const $cond = stateAwait(Promise.resolve(true));
643
+ const $derivation = derivationAwait(async (t) => {
644
+ const cond = $cond.get(t);
645
+ if (cond) {
646
+ const val = $state1.get(t);
647
+ return Promise.resolve((val ?? 0) * 2);
648
+ }
649
+ const val = $state2.get(t);
650
+ return Promise.resolve((val ?? 0) * 2);
651
+ });
652
+
653
+ // Initially depends on state1
654
+ expect(await $derivation.pick()).toBe(2);
655
+
656
+ // Change state1 - should recompute
657
+ await $state1.set(Promise.resolve(2));
658
+ expect(await $derivation.pick()).toBe(4);
659
+
660
+ // Switch to state2 dependency
661
+ await $cond.set(Promise.resolve(false));
662
+ expect(await $derivation.pick()).toBe(20);
663
+
664
+ // Change state2 - should recompute
665
+ await $state2.set(Promise.resolve(20));
666
+ expect(await $derivation.pick()).toBe(40);
667
+
668
+ // Change state1 - should NOT recompute (no longer a dependency)
669
+ await $state1.set(Promise.resolve(100));
670
+ expect(await $derivation.pick()).toBe(40);
671
+ });
672
+
673
+ it("should unregister dependencies that are no longer used", async () => {
674
+ const $state1 = stateAwait(Promise.resolve(1));
675
+ const $state2 = stateAwait(Promise.resolve(10));
676
+ const $cond = stateAwait(Promise.resolve(true));
677
+ const $derivation = derivationAwait(async (t) => {
678
+ const cond = $cond.get(t);
679
+ if (cond) {
680
+ const val = $state1.get(t);
681
+ return Promise.resolve(val ?? 0);
682
+ }
683
+ const val = $state2.get(t);
684
+ return Promise.resolve(val ?? 0);
685
+ });
686
+
687
+ await $derivation.pick();
688
+ // Initially depends on state1
689
+
690
+ await $cond.set(Promise.resolve(false));
691
+ await $derivation.pick();
692
+ // Now depends on state2, state1 should be unregistered
693
+ });
694
+
695
+ it("should mark as dirty when dependency changes", async () => {
696
+ const $state = stateAwait(Promise.resolve(1));
697
+ const computeFn = vi.fn(async (t) => {
698
+ const val = $state.get(t);
699
+ return Promise.resolve((val ?? 0) * 2);
700
+ });
701
+ const $derivation = derivationAwait(computeFn);
702
+
703
+ await $derivation.pick();
704
+ expect(computeFn).toHaveBeenCalledTimes(1);
705
+
706
+ // Change dependency - marks as dirty but doesn't recompute yet
707
+ await $state.set(Promise.resolve(2));
708
+
709
+ // Next access triggers recomputation
710
+ await $derivation.pick();
711
+ expect(computeFn).toHaveBeenCalledTimes(2);
712
+ });
713
+
714
+ it("should recompute only on next access when dirty", async () => {
715
+ const $state = stateAwait(Promise.resolve(1));
716
+ const computeFn = vi.fn(async (t) => {
717
+ const val = $state.get(t);
718
+ return Promise.resolve((val ?? 0) * 2);
719
+ });
720
+ const $derivation = derivationAwait(computeFn);
721
+
722
+ await $derivation.pick();
723
+ expect(computeFn).toHaveBeenCalledTimes(1);
724
+
725
+ // Multiple dependency changes
726
+ await $state.set(Promise.resolve(2));
727
+ await $state.set(Promise.resolve(3));
728
+ await $state.set(Promise.resolve(4));
729
+
730
+ // Still only called once more (lazy recomputation)
731
+ await $derivation.pick();
732
+ expect(computeFn).toHaveBeenCalledTimes(2);
733
+ });
734
+
735
+ it("should handle nested derivations", async () => {
736
+ const $state = stateAwait(Promise.resolve(1));
737
+ const $derivation1 = derivationAwait(async (t) => {
738
+ const val = $state.get(t);
739
+ return (val ?? 0) * 2;
740
+ });
741
+ const $derivation2 = derivationAwait(async (t) => {
742
+ const val = $derivation1.get(t);
743
+ return (val ?? 0) * 2;
744
+ });
745
+
746
+ const effectFn = vi.fn();
747
+ effect((t) => {
748
+ effectFn($derivation2.get(t));
749
+ });
750
+
751
+ const value = await $derivation2.pick();
752
+ expect(value).toBe(4);
753
+
754
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(4));
755
+ expect(effectFn).toHaveBeenNthCalledWith(1, undefined);
756
+ expect(effectFn).toHaveBeenNthCalledWith(2, 0);
757
+ expect(effectFn).toHaveBeenNthCalledWith(3, 4);
758
+ expect(effectFn).toHaveBeenNthCalledWith(4, 4);
759
+
760
+ await $state.set(Promise.resolve(2));
761
+ const value2 = await $derivation2.pick();
762
+ expect(value2).toBe(8);
763
+ });
764
+
765
+ it("should handle compute function that returns undefined", async () => {
766
+ const $state = stateAwait(Promise.resolve(true));
767
+ const $derivation = derivationAwait(async (t) => {
768
+ const val = $state.get(t);
769
+ return Promise.resolve(val ? undefined : "defined");
770
+ });
771
+ expect(await $derivation.pick()).toBeUndefined();
772
+ });
773
+
774
+ // TODO: fix this test
775
+ it("should handle compute function that throws error", async () => {
776
+ const error = new Error("Compute error");
777
+ const $state = stateAwait(Promise.resolve(1));
778
+ const $derivation = derivationAwait(async (t) => {
779
+ const val = $state.get(t);
780
+ if (val === 1) {
781
+ throw error;
782
+ }
783
+ return Promise.resolve((val ?? 0) * 2);
784
+ });
785
+
786
+ await expect($derivation.pick()).rejects.toThrow("Compute error");
787
+ });
788
+ });
789
+ });
790
+
791
+ describe("integration", () => {
792
+ describe("with effects - get", () => {
793
+ it("should create reactive dependency when get is used in effect", async () => {
794
+ const $state = stateAwait(Promise.resolve(100));
795
+ const $derivation = derivationAwait(async (t) => {
796
+ const val = $state.get(t);
797
+ return (val ?? 0) * 2;
798
+ });
799
+ const effectFn = vi.fn();
800
+
801
+ effect((t) => {
802
+ effectFn($derivation.get(t));
803
+ });
804
+
805
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
806
+ expect(effectFn).toHaveBeenNthCalledWith(1, undefined);
807
+ expect(effectFn).toHaveBeenNthCalledWith(2, 200);
808
+ });
809
+
810
+ it("should call effect with initial computed value", async () => {
811
+ const $state = stateAwait(Promise.resolve(1));
812
+ const $derivation = derivationAwait(async (t) => {
813
+ const val = $state.get(t);
814
+ return (val ?? 0) * 2;
815
+ });
816
+ const effectFn = vi.fn();
817
+
818
+ effect((t) => {
819
+ effectFn($derivation.get(t));
820
+ });
821
+
822
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
823
+ expect(effectFn).toHaveBeenNthCalledWith(1, undefined);
824
+ expect(effectFn).toHaveBeenNthCalledWith(2, 2);
825
+ });
826
+
827
+ it("should call effect when dependency changes", async () => {
828
+ const $state = stateAwait(Promise.resolve(1));
829
+ const $derivation = derivationAwait(async (t) => {
830
+ const val = $state.get(t);
831
+ return (val ?? 0) * 2;
832
+ });
833
+ const effectFn = vi.fn();
834
+
835
+ effect((t) => {
836
+ effectFn($derivation.get(t));
837
+ });
838
+
839
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
840
+ expect(effectFn).toHaveBeenNthCalledWith(1, undefined);
841
+ expect(effectFn).toHaveBeenNthCalledWith(2, 2);
842
+
843
+ await $state.set(Promise.resolve(2));
844
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(4));
845
+ expect(effectFn).toHaveBeenNthCalledWith(3, 2);
846
+ expect(effectFn).toHaveBeenNthCalledWith(4, 4);
847
+ });
848
+
849
+ it("should not call effect when value does not change", async () => {
850
+ const $state = stateAwait(Promise.resolve(1));
851
+ const $derivation = derivationAwait(async (t) => {
852
+ const val = $state.get(t);
853
+ return (val ?? 0) * 2;
854
+ });
855
+ const effectFn = vi.fn();
856
+
857
+ effect((t) => {
858
+ effectFn($derivation.get(t));
859
+ });
860
+
861
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
862
+ expect(effectFn).toHaveBeenNthCalledWith(1, undefined);
863
+ expect(effectFn).toHaveBeenNthCalledWith(2, 2);
864
+
865
+ await $state.set(Promise.resolve(1));
866
+ await new Promise((resolve) => setTimeout(resolve, 50));
867
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
868
+ });
869
+
870
+ it("should not call effect after effect is disposed", async () => {
871
+ const $state = stateAwait(Promise.resolve(1));
872
+ const $derivation = derivationAwait(async (t) => {
873
+ const val = $state.get(t);
874
+ return (val ?? 0) * 2;
875
+ });
876
+ const effectFn = vi.fn();
877
+ const $effect = effect((t) => {
878
+ effectFn($derivation.get(t));
879
+ });
880
+
881
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
882
+ expect(effectFn).toHaveBeenNthCalledWith(1, undefined);
883
+ expect(effectFn).toHaveBeenNthCalledWith(2, 2);
884
+
885
+ await $state.set(Promise.resolve(2));
886
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(4));
887
+ expect(effectFn).toHaveBeenNthCalledWith(3, 2);
888
+ expect(effectFn).toHaveBeenNthCalledWith(4, 4);
889
+
890
+ $effect.dispose();
891
+
892
+ await $state.set(Promise.resolve(3));
893
+ await new Promise((resolve) => setTimeout(resolve, 50));
894
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(4));
895
+ });
896
+
897
+ it("should support multiple effects depending on same derivation", async () => {
898
+ const $state = stateAwait(Promise.resolve(200));
899
+ const $derivation = derivationAwait(async (t) => {
900
+ const val = $state.get(t);
901
+ return (val ?? 0) * 2;
902
+ });
903
+ const effectFn1 = vi.fn();
904
+ const effectFn2 = vi.fn();
905
+ const effectFn3 = vi.fn();
906
+
907
+ effect((t) => {
908
+ effectFn1($derivation.get(t));
909
+ });
910
+
911
+ effect((t) => {
912
+ effectFn2($derivation.get(t));
913
+ });
914
+
915
+ effect((t) => {
916
+ effectFn3($derivation.get(t));
917
+ });
918
+
919
+ await vi.waitFor(() => {
920
+ expect(effectFn1).toHaveBeenCalledTimes(2);
921
+ expect(effectFn2).toHaveBeenCalledTimes(2);
922
+ expect(effectFn3).toHaveBeenCalledTimes(2);
923
+ });
924
+
925
+ expect(effectFn1).toHaveBeenLastCalledWith(400);
926
+ expect(effectFn2).toHaveBeenLastCalledWith(400);
927
+ expect(effectFn3).toHaveBeenLastCalledWith(400);
928
+
929
+ await $state.set(Promise.resolve(300));
930
+ await vi.waitFor(() => {
931
+ expect(effectFn1).toHaveBeenCalledTimes(4);
932
+ expect(effectFn2).toHaveBeenCalledTimes(4);
933
+ expect(effectFn3).toHaveBeenCalledTimes(4);
934
+ });
935
+
936
+ expect(effectFn1).toHaveBeenLastCalledWith(600);
937
+ expect(effectFn2).toHaveBeenLastCalledWith(600);
938
+ expect(effectFn3).toHaveBeenLastCalledWith(600);
939
+ });
940
+
941
+ it("should support effect depending on multiple derivations", async () => {
942
+ const $stateA = stateAwait(Promise.resolve(5));
943
+ const $stateB = stateAwait(Promise.resolve(10));
944
+ const $derivationA = derivationAwait(async (t) => {
945
+ const val = $stateA.get(t);
946
+ return (val ?? 0) * 2;
947
+ });
948
+ const $derivationB = derivationAwait(async (t) => {
949
+ const val = $stateB.get(t);
950
+ return (val ?? 0) * 2;
951
+ });
952
+ const effectFn = vi.fn();
953
+
954
+ effect((t) => {
955
+ const a = $derivationA.get(t);
956
+ const b = $derivationB.get(t);
957
+ if (a !== undefined && b !== undefined) {
958
+ effectFn(a, b);
959
+ }
960
+ });
961
+
962
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
963
+ expect(effectFn).toHaveBeenNthCalledWith(1, 10, 20);
964
+ expect(effectFn).toHaveBeenNthCalledWith(2, 10, 20);
965
+
966
+ await $stateA.set(Promise.resolve(6));
967
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(4));
968
+ expect(effectFn).toHaveBeenNthCalledWith(3, 10, 20);
969
+ expect(effectFn).toHaveBeenNthCalledWith(4, 12, 20);
970
+
971
+ await $stateB.set(Promise.resolve(11));
972
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(6));
973
+ expect(effectFn).toHaveBeenNthCalledWith(5, 12, 20);
974
+ expect(effectFn).toHaveBeenNthCalledWith(6, 12, 22);
975
+ });
976
+ });
977
+
978
+ describe("with effects - watch", () => {
979
+ it("should register dependency when watch is used in effect", async () => {
980
+ const $state = stateAwait(Promise.resolve(400));
981
+ const $derivation = derivationAwait(async (t) => {
982
+ const val = $state.get(t);
983
+ return (val ?? 0) * 2;
984
+ });
985
+ const effectFn = vi.fn();
986
+
987
+ effect((t) => {
988
+ $derivation.watch(t);
989
+ effectFn();
990
+ });
991
+
992
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
993
+ });
994
+
995
+ it("should trigger re-runs when derivation changes after watch", async () => {
996
+ const $state = stateAwait(Promise.resolve(500));
997
+ const $derivation = derivationAwait(async (t) => {
998
+ const val = $state.get(t);
999
+ return (val ?? 0) * 2;
1000
+ });
1001
+ const effectFn = vi.fn();
1002
+
1003
+ effect((t) => {
1004
+ $derivation.watch(t);
1005
+ effectFn();
1006
+ });
1007
+
1008
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
1009
+
1010
+ await $state.set(Promise.resolve(600));
1011
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(4));
1012
+ });
1013
+ });
1014
+
1015
+ describe("with effects - subscribe", () => {
1016
+ it("should call listener immediately when subscribe is used", async () => {
1017
+ const $state = stateAwait(Promise.resolve(700));
1018
+ const $derivation = derivationAwait(async (t) => {
1019
+ const val = $state.get(t);
1020
+ return Promise.resolve((val ?? 0) * 2);
1021
+ });
1022
+ const listener = vi.fn();
1023
+
1024
+ $derivation.subscribe(listener);
1025
+
1026
+ await vi.waitFor(() => expect(listener).toHaveBeenCalledTimes(2));
1027
+ expect(listener).toHaveBeenLastCalledWith(1400);
1028
+ });
1029
+
1030
+ it("should support multiple subscriptions with effects", async () => {
1031
+ const $state = stateAwait(Promise.resolve(800));
1032
+ const $derivation = derivationAwait(async (t) => {
1033
+ const val = $state.get(t);
1034
+ return Promise.resolve((val ?? 0) * 2);
1035
+ });
1036
+ const listener1 = vi.fn();
1037
+ const listener2 = vi.fn();
1038
+
1039
+ $derivation.subscribe(listener1);
1040
+ $derivation.subscribe(listener2);
1041
+
1042
+ await vi.waitFor(() => {
1043
+ expect(listener1).toHaveBeenCalledTimes(2);
1044
+ expect(listener2).toHaveBeenCalledTimes(2);
1045
+ });
1046
+
1047
+ await $state.set(Promise.resolve(900));
1048
+ await vi.waitFor(() => {
1049
+ expect(listener1).toHaveBeenCalledTimes(4);
1050
+ expect(listener2).toHaveBeenCalledTimes(4);
1051
+ });
1052
+ });
1053
+
1054
+ it("should dispose effect when disposer is called", async () => {
1055
+ const $state = stateAwait(Promise.resolve(1000));
1056
+ const $derivation = derivationAwait(async (t) => {
1057
+ const val = $state.get(t);
1058
+ return Promise.resolve((val ?? 0) * 2);
1059
+ });
1060
+ const listener = vi.fn();
1061
+
1062
+ const unsubscribe = $derivation.subscribe(listener);
1063
+ await vi.waitFor(() => expect(listener).toHaveBeenCalledTimes(2));
1064
+
1065
+ await $state.set(Promise.resolve(1100));
1066
+ await vi.waitFor(() => expect(listener).toHaveBeenCalledTimes(4));
1067
+
1068
+ unsubscribe();
1069
+
1070
+ await $state.set(Promise.resolve(1200));
1071
+ await new Promise((resolve) => setTimeout(resolve, 50));
1072
+ expect(listener).toHaveBeenCalledTimes(4);
1073
+ });
1074
+ });
1075
+
1076
+ describe("with effects - disposal", () => {
1077
+ it("should dispose effects when derivation is disposed", async () => {
1078
+ const $state = stateAwait(Promise.resolve(1300));
1079
+ const $derivation = derivationAwait(async (t) => {
1080
+ const val = $state.get(t);
1081
+ return Promise.resolve((val ?? 0) * 2);
1082
+ });
1083
+ const effectFn = vi.fn();
1084
+ const $effect = effect((t) => {
1085
+ $derivation.get(t);
1086
+ effectFn();
1087
+ });
1088
+
1089
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
1090
+ expect($effect.disposed).toBe(false);
1091
+
1092
+ $derivation.dispose();
1093
+
1094
+ expect($effect.disposed).toBe(true);
1095
+ });
1096
+
1097
+ it("should not dispose effects when derivation is disposed with self option", async () => {
1098
+ const $state = stateAwait(Promise.resolve(1400));
1099
+ const $derivation = derivationAwait(async (t) => {
1100
+ const val = $state.get(t);
1101
+ return (val ?? 0) * 2;
1102
+ });
1103
+ const effectFn = vi.fn();
1104
+ const $effect = effect((t) => {
1105
+ $derivation.get(t);
1106
+ effectFn();
1107
+ });
1108
+
1109
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
1110
+ expect($effect.disposed).toBe(false);
1111
+
1112
+ $derivation.dispose({ self: true });
1113
+
1114
+ expect($effect.disposed).toBe(false);
1115
+ expect($derivation.disposed).toBe(true);
1116
+ });
1117
+
1118
+ it("should unregister effects when derivation is disposed with self option", async () => {
1119
+ const $state = stateAwait(Promise.resolve(1500));
1120
+ const $derivation = derivationAwait(async (t) => {
1121
+ const val = $state.get(t);
1122
+ return (val ?? 0) * 2;
1123
+ });
1124
+ const effectFn = vi.fn();
1125
+ const $effect = effect((t) => {
1126
+ $derivation.get(t);
1127
+ effectFn();
1128
+ });
1129
+
1130
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
1131
+
1132
+ $derivation.dispose({ self: true });
1133
+
1134
+ // Effect should still be active but not notified
1135
+ expect($effect.disposed).toBe(false);
1136
+
1137
+ // But derivation is disposed so operations should fail
1138
+ const $tracker = state(0);
1139
+ expect(() => $derivation.get($tracker)).toThrow(
1140
+ "[PicoFlow] Primitive is disposed",
1141
+ );
1142
+ });
1143
+ });
1144
+
1145
+ describe("dependencies", () => {
1146
+ it("should handle chained dependencies", async () => {
1147
+ const $state = stateAwait(Promise.resolve(1));
1148
+ const $derivation1 = derivationAwait(async (t) => {
1149
+ const val = $state.get(t);
1150
+ return (val ?? 0) * 2;
1151
+ });
1152
+ const $derivation2 = derivationAwait(async (t) => {
1153
+ const val = $derivation1.get(t);
1154
+ return (val ?? 0) * 2;
1155
+ });
1156
+ const effectFn = vi.fn();
1157
+
1158
+ effect((t) => {
1159
+ const val = $derivation2.get(t);
1160
+ if (val !== undefined) {
1161
+ effectFn(val);
1162
+ }
1163
+ });
1164
+
1165
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(3));
1166
+ expect(effectFn).toHaveBeenNthCalledWith(1, 0);
1167
+ expect(effectFn).toHaveBeenNthCalledWith(2, 4);
1168
+ expect(effectFn).toHaveBeenNthCalledWith(3, 4);
1169
+
1170
+ await $state.set(Promise.resolve(2));
1171
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(6));
1172
+ expect(effectFn).toHaveBeenNthCalledWith(4, 4);
1173
+ expect(effectFn).toHaveBeenNthCalledWith(5, 4);
1174
+ expect(effectFn).toHaveBeenNthCalledWith(6, 8);
1175
+ });
1176
+
1177
+ it("should handle multiple dependencies", async () => {
1178
+ const $state1 = stateAwait(Promise.resolve(1));
1179
+ const $state2 = stateAwait(Promise.resolve(2));
1180
+ const $derivation = derivationAwait(async (t) => {
1181
+ const val1 = $state1.get(t);
1182
+ const val2 = $state2.get(t);
1183
+ return (val1 ?? 0) + (val2 ?? 0);
1184
+ });
1185
+ const effectFn = vi.fn();
1186
+
1187
+ effect((t) => {
1188
+ const val = $derivation.get(t);
1189
+ if (val !== undefined) {
1190
+ effectFn(val);
1191
+ }
1192
+ });
1193
+
1194
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(1));
1195
+ expect(effectFn).toHaveBeenLastCalledWith(3);
1196
+
1197
+ await $state1.set(Promise.resolve(2));
1198
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(3));
1199
+ expect(effectFn).toHaveBeenLastCalledWith(4);
1200
+
1201
+ await $state2.set(Promise.resolve(3));
1202
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(5));
1203
+ expect(effectFn).toHaveBeenLastCalledWith(5);
1204
+ });
1205
+
1206
+ it("should handle multiple dependants", async () => {
1207
+ const $state = stateAwait(Promise.resolve(1));
1208
+ const $derivation1 = derivationAwait(async (t) => {
1209
+ const val = $state.get(t);
1210
+ return (val ?? 0) * 2;
1211
+ });
1212
+ const $derivation2 = derivationAwait(async (t) => {
1213
+ const val = $state.get(t);
1214
+ return (val ?? 0) * 3;
1215
+ });
1216
+ const effect1Fn = vi.fn();
1217
+ const effect2Fn = vi.fn();
1218
+
1219
+ effect((t) => {
1220
+ const val = $derivation1.get(t);
1221
+ if (val !== undefined) {
1222
+ effect1Fn(val);
1223
+ }
1224
+ });
1225
+
1226
+ effect((t) => {
1227
+ const val = $derivation2.get(t);
1228
+ if (val !== undefined) {
1229
+ effect2Fn(val);
1230
+ }
1231
+ });
1232
+
1233
+ await vi.waitFor(() => {
1234
+ expect(effect1Fn).toHaveBeenCalledTimes(1);
1235
+ expect(effect2Fn).toHaveBeenCalledTimes(1);
1236
+ });
1237
+ expect(effect1Fn).toHaveBeenNthCalledWith(1, 2);
1238
+ expect(effect2Fn).toHaveBeenNthCalledWith(1, 3);
1239
+
1240
+ await $state.set(Promise.resolve(2));
1241
+ await vi.waitFor(() => {
1242
+ expect(effect1Fn).toHaveBeenCalledTimes(3);
1243
+ expect(effect2Fn).toHaveBeenCalledTimes(3);
1244
+ });
1245
+ expect(effect1Fn).toHaveBeenLastCalledWith(4);
1246
+ expect(effect2Fn).toHaveBeenLastCalledWith(6);
1247
+ });
1248
+
1249
+ it("should handle derivation depending on state and signal", async () => {
1250
+ const $signal = signal();
1251
+ const $state = stateAwait(Promise.resolve(1));
1252
+ const $derivation = derivationAwait(async (t) => {
1253
+ $signal.watch(t);
1254
+ const val = $state.get(t);
1255
+ return (val ?? 0) * 2;
1256
+ });
1257
+ const effectFn = vi.fn();
1258
+
1259
+ effect((t) => {
1260
+ const val = $derivation.get(t);
1261
+ if (val !== undefined) {
1262
+ effectFn(val);
1263
+ }
1264
+ });
1265
+
1266
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(1));
1267
+ expect(effectFn).toHaveBeenNthCalledWith(1, 2);
1268
+
1269
+ $signal.trigger();
1270
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
1271
+ expect(effectFn).toHaveBeenNthCalledWith(2, 2);
1272
+
1273
+ await $state.set(Promise.resolve(2));
1274
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(4));
1275
+ expect(effectFn).toHaveBeenNthCalledWith(4, 4);
1276
+ });
1277
+
1278
+ it("should handle derivation depending on multiple states", async () => {
1279
+ const $state1 = stateAwait(Promise.resolve(5));
1280
+ const $state2 = stateAwait(Promise.resolve(10));
1281
+ const $state3 = stateAwait(Promise.resolve(15));
1282
+ const $derivation = derivationAwait(async (t) => {
1283
+ const val1 = $state1.get(t);
1284
+ const val2 = $state2.get(t);
1285
+ const val3 = $state3.get(t);
1286
+ return (val1 ?? 0) + (val2 ?? 0) + (val3 ?? 0);
1287
+ });
1288
+ const effectFn = vi.fn();
1289
+
1290
+ effect((t) => {
1291
+ const val = $derivation.get(t);
1292
+ if (val !== undefined) {
1293
+ effectFn(val);
1294
+ }
1295
+ });
1296
+
1297
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(1));
1298
+ expect(effectFn).toHaveBeenNthCalledWith(1, 30);
1299
+
1300
+ await $state1.set(Promise.resolve(6));
1301
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(3));
1302
+ expect(effectFn).toHaveBeenLastCalledWith(31);
1303
+ });
1304
+
1305
+ it("should handle derivation depending on multiple signals", async () => {
1306
+ const $signal1 = signal();
1307
+ const $signal2 = signal();
1308
+ const $derivation = derivationAwait(async (t) => {
1309
+ $signal1.watch(t);
1310
+ $signal2.watch(t);
1311
+ return Promise.resolve(undefined);
1312
+ });
1313
+ const effectFn = vi.fn();
1314
+
1315
+ effect((t) => {
1316
+ $derivation.watch(t);
1317
+ effectFn();
1318
+ });
1319
+
1320
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(1));
1321
+
1322
+ $signal1.trigger();
1323
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
1324
+
1325
+ $signal2.trigger();
1326
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(3));
1327
+ });
1328
+
1329
+ it("should handle derivation depending on state and other derivation", async () => {
1330
+ const $state = stateAwait(Promise.resolve(1));
1331
+ const $derivation1 = derivationAwait(async (t) => {
1332
+ const val = $state.get(t);
1333
+ return (val ?? 0) * 2;
1334
+ });
1335
+ const $derivation2 = derivationAwait(async (t) => {
1336
+ const val1 = $state.get(t);
1337
+ const val2 = $derivation1.get(t);
1338
+ return (val1 ?? 0) + (val2 ?? 0);
1339
+ });
1340
+ const effectFn = vi.fn();
1341
+
1342
+ effect((t) => {
1343
+ const val = $derivation2.get(t);
1344
+ if (val !== undefined) {
1345
+ effectFn(val);
1346
+ }
1347
+ });
1348
+
1349
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(3));
1350
+ expect(effectFn).toHaveBeenLastCalledWith(3);
1351
+
1352
+ await $state.set(Promise.resolve(2));
1353
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(7));
1354
+ expect(effectFn).toHaveBeenLastCalledWith(6);
1355
+ });
1356
+ });
1357
+
1358
+ describe("patterns", () => {
1359
+ it("should handle diamond pattern", async () => {
1360
+ const $stateA = stateAwait(Promise.resolve(1));
1361
+ const $aMult3 = derivationAwait(async (t) => {
1362
+ const val = $stateA.get(t);
1363
+ return (val ?? 0) * 3;
1364
+ });
1365
+ const $aMult2 = derivationAwait(async (t) => {
1366
+ const val = $stateA.get(t);
1367
+ return (val ?? 0) * 2;
1368
+ });
1369
+ const $addAmult3Amult2 = derivationAwait(async (t) => {
1370
+ const val1 = $aMult3.get(t);
1371
+ const val2 = $aMult2.get(t);
1372
+ return (val1 ?? 0) + (val2 ?? 0);
1373
+ });
1374
+
1375
+ const effectFn = vi.fn();
1376
+
1377
+ effect((t) => {
1378
+ const val = $addAmult3Amult2.get(t);
1379
+ if (val !== undefined) {
1380
+ effectFn(val);
1381
+ }
1382
+ });
1383
+
1384
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(4));
1385
+ expect(effectFn).toHaveBeenLastCalledWith(5);
1386
+
1387
+ await $stateA.set(Promise.resolve(2));
1388
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(8));
1389
+ expect(effectFn).toHaveBeenLastCalledWith(10);
1390
+
1391
+ await $stateA.set(Promise.resolve(3));
1392
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(12));
1393
+ expect(effectFn).toHaveBeenLastCalledWith(15);
1394
+ });
1395
+
1396
+ it("should handle multi diamond pattern", async () => {
1397
+ const $stateA = stateAwait(Promise.resolve(1));
1398
+ const $stateB = stateAwait(Promise.resolve(2));
1399
+ const $addAB = derivationAwait(async (t) => {
1400
+ const a = $stateA.get(t);
1401
+ const b = $stateB.get(t);
1402
+ return (a ?? 0) + (b ?? 0);
1403
+ });
1404
+
1405
+ const $multiplyAB = derivationAwait(async (t) => {
1406
+ const a = $stateA.get(t);
1407
+ const b = $stateB.get(t);
1408
+ return (a ?? 0) * (b ?? 0);
1409
+ });
1410
+
1411
+ const $addAndMultiply = derivationAwait(async (t) => {
1412
+ const add = $addAB.get(t);
1413
+ const multiply = $multiplyAB.get(t);
1414
+ return (add ?? 0) * (multiply ?? 0);
1415
+ });
1416
+
1417
+ const effectFn = vi.fn();
1418
+
1419
+ effect((t) => {
1420
+ const val = $addAndMultiply.get(t);
1421
+ if (val !== undefined) {
1422
+ effectFn(val);
1423
+ }
1424
+ });
1425
+
1426
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(1));
1427
+ expect(effectFn).toHaveBeenNthCalledWith(1, 6);
1428
+
1429
+ await $stateA.set(Promise.resolve(2));
1430
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
1431
+ expect(effectFn).toHaveBeenNthCalledWith(2, 16);
1432
+
1433
+ await $stateB.set(Promise.resolve(3));
1434
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(3));
1435
+ expect(effectFn).toHaveBeenNthCalledWith(3, 30);
1436
+ });
1437
+
1438
+ it("should handle derivation with conditional dependencies", async () => {
1439
+ const obj = {
1440
+ cond: stateAwait(Promise.resolve(false)),
1441
+ b: stateAwait(Promise.resolve(2)),
1442
+ };
1443
+
1444
+ const $state = stateAwait(Promise.resolve(obj));
1445
+ const $derivation = derivationAwait(async (t) => {
1446
+ const stateVal = $state.get(t);
1447
+ if (stateVal) {
1448
+ const cond = stateVal.cond.get(t);
1449
+ if (cond) {
1450
+ const b = stateVal.b.get(t);
1451
+ return (b ?? 0) * 2;
1452
+ }
1453
+ }
1454
+ return 0;
1455
+ });
1456
+
1457
+ const effectFn = vi.fn();
1458
+
1459
+ effect((t) => {
1460
+ const val = $derivation.get(t);
1461
+ if (val !== undefined) {
1462
+ effectFn(val);
1463
+ }
1464
+ });
1465
+
1466
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(1));
1467
+ expect(effectFn).toHaveBeenNthCalledWith(1, 0);
1468
+
1469
+ const stateVal = await $state.pick();
1470
+ await stateVal.cond.set(Promise.resolve(true));
1471
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
1472
+ expect(effectFn).toHaveBeenNthCalledWith(2, 4);
1473
+
1474
+ await $state.set(
1475
+ Promise.resolve({
1476
+ cond: stateAwait(Promise.resolve(false)),
1477
+ b: stateAwait(Promise.resolve(3)),
1478
+ }),
1479
+ );
1480
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(3));
1481
+ expect(effectFn).toHaveBeenNthCalledWith(3, 0);
1482
+
1483
+ const stateVal2 = await $state.pick();
1484
+ await stateVal2.cond.set(Promise.resolve(true));
1485
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(4));
1486
+ expect(effectFn).toHaveBeenNthCalledWith(4, 6);
1487
+ });
1488
+
1489
+ it("should handle derivation with dynamic dependencies", async () => {
1490
+ const $state1 = stateAwait(Promise.resolve(1));
1491
+ const $state2 = stateAwait(Promise.resolve(10));
1492
+ const $cond = stateAwait(Promise.resolve(true));
1493
+ const $derivation = derivationAwait(async (t) => {
1494
+ const cond = $cond.get(t);
1495
+ if (cond) {
1496
+ const val = $state1.get(t);
1497
+ return (val ?? 0) * 2;
1498
+ }
1499
+ const val = $state2.get(t);
1500
+ return (val ?? 0) * 2;
1501
+ });
1502
+
1503
+ const effectFn = vi.fn();
1504
+
1505
+ effect((t) => {
1506
+ const val = $derivation.get(t);
1507
+ if (val !== undefined) {
1508
+ effectFn(val);
1509
+ }
1510
+ });
1511
+
1512
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(1));
1513
+ expect(effectFn).toHaveBeenNthCalledWith(1, 2);
1514
+
1515
+ await $state1.set(Promise.resolve(2));
1516
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
1517
+ expect(effectFn).toHaveBeenNthCalledWith(2, 4);
1518
+
1519
+ await $cond.set(Promise.resolve(false));
1520
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(3));
1521
+ expect(effectFn).toHaveBeenNthCalledWith(3, 20);
1522
+
1523
+ await $state2.set(Promise.resolve(20));
1524
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(4));
1525
+ expect(effectFn).toHaveBeenNthCalledWith(4, 40);
1526
+ });
1527
+ });
1528
+
1529
+ describe("with refresh", () => {
1530
+ it("should force recomputation in effect", async () => {
1531
+ let multiplier = 2;
1532
+ const $state = stateAwait(Promise.resolve(5));
1533
+ const $derivation = derivationAwait(async (t) => {
1534
+ const val = $state.get(t);
1535
+ return (val ?? 0) * multiplier;
1536
+ });
1537
+ const effectFn = vi.fn();
1538
+
1539
+ effect((t) => {
1540
+ const val = $derivation.get(t);
1541
+ if (val !== undefined) {
1542
+ effectFn(val);
1543
+ }
1544
+ });
1545
+
1546
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(1));
1547
+ expect(effectFn).toHaveBeenNthCalledWith(1, 10);
1548
+
1549
+ multiplier = 3;
1550
+ await $derivation.refresh();
1551
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
1552
+ expect(effectFn).toHaveBeenNthCalledWith(2, 15);
1553
+ });
1554
+
1555
+ it("shouldn't trigger effects even if dependencies have not changed", async () => {
1556
+ const $state = stateAwait(Promise.resolve(1));
1557
+ const computeFn = vi.fn(async (t) => {
1558
+ const val = $state.get(t);
1559
+ return Promise.resolve((val ?? 0) * 2);
1560
+ });
1561
+ const $derivation = derivationAwait(computeFn);
1562
+ const effectFn = vi.fn();
1563
+
1564
+ effect((t) => {
1565
+ const val = $derivation.get(t);
1566
+ if (val !== undefined) {
1567
+ effectFn(val);
1568
+ }
1569
+ });
1570
+
1571
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(1));
1572
+ expect(computeFn).toHaveBeenCalledTimes(1);
1573
+
1574
+ // Refresh forces recomputation even though state hasn't changed
1575
+ await $derivation.refresh();
1576
+ await vi.waitFor(() => expect(computeFn).toHaveBeenCalledTimes(2));
1577
+ // Effect should not be called again because value hasn't changed
1578
+ await new Promise((resolve) => setTimeout(resolve, 50));
1579
+ expect(effectFn).toHaveBeenCalledTimes(1);
1580
+ });
1581
+ });
1582
+ });
1583
+ });