@ersbeth/picoflow 1.0.1 → 1.1.1

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 (183) hide show
  1. package/.cursor/plans/unifier-flowresource-avec-flowderivation-c9506e24.plan.md +372 -0
  2. package/README.md +17 -1
  3. package/biome.json +4 -1
  4. package/dist/picoflow.js +1155 -582
  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 +2 -4
  54. package/dist/types/index.d.ts.map +1 -1
  55. package/dist/types/solid/converters.d.ts +34 -45
  56. package/dist/types/solid/converters.d.ts.map +1 -1
  57. package/dist/types/solid/index.d.ts +2 -2
  58. package/dist/types/solid/index.d.ts.map +1 -1
  59. package/dist/types/solid/primitives.d.ts +1 -0
  60. package/dist/types/solid/primitives.d.ts.map +1 -1
  61. package/docs/.vitepress/config.mts +1 -1
  62. package/docs/api/typedoc-sidebar.json +81 -1
  63. package/package.json +60 -58
  64. package/src/flow/base/flowDisposable.ts +71 -0
  65. package/src/flow/base/flowEffect.ts +171 -0
  66. package/src/flow/base/flowGraph.ts +288 -0
  67. package/src/flow/base/flowSignal.ts +207 -0
  68. package/src/flow/base/flowTracker.ts +17 -0
  69. package/src/flow/base/index.ts +6 -0
  70. package/src/flow/base/utils.ts +19 -0
  71. package/src/flow/collections/flowArray.ts +409 -0
  72. package/src/flow/collections/flowMap.ts +398 -0
  73. package/src/flow/collections/index.ts +2 -0
  74. package/src/flow/index.ts +3 -0
  75. package/src/flow/nodes/async/flowConstantAsync.ts +142 -0
  76. package/src/flow/nodes/async/flowDerivationAsync.ts +143 -0
  77. package/src/flow/nodes/async/flowNodeAsync.ts +474 -0
  78. package/src/flow/nodes/async/flowReadonlyAsync.ts +81 -0
  79. package/src/flow/nodes/async/flowStateAsync.ts +116 -0
  80. package/src/flow/nodes/async/index.ts +5 -0
  81. package/src/flow/nodes/await/advanced/index.ts +5 -0
  82. package/src/{advanced → flow/nodes/await/advanced}/resource.ts +37 -3
  83. package/src/{advanced → flow/nodes/await/advanced}/resourceAsync.ts +35 -3
  84. package/src/{advanced → flow/nodes/await/advanced}/stream.ts +40 -2
  85. package/src/{advanced → flow/nodes/await/advanced}/streamAsync.ts +38 -3
  86. package/src/flow/nodes/await/flowConstantAwait.ts +154 -0
  87. package/src/flow/nodes/await/flowDerivationAwait.ts +154 -0
  88. package/src/flow/nodes/await/flowNodeAwait.ts +508 -0
  89. package/src/flow/nodes/await/flowReadonlyAwait.ts +89 -0
  90. package/src/flow/nodes/await/flowStateAwait.ts +130 -0
  91. package/src/flow/nodes/await/index.ts +5 -0
  92. package/src/flow/nodes/index.ts +3 -0
  93. package/src/flow/nodes/sync/flowConstant.ts +111 -0
  94. package/src/flow/nodes/sync/flowDerivation.ts +105 -0
  95. package/src/flow/nodes/sync/flowNode.ts +439 -0
  96. package/src/flow/nodes/sync/flowReadonly.ts +57 -0
  97. package/src/flow/nodes/sync/flowState.ts +101 -0
  98. package/src/flow/nodes/sync/index.ts +5 -0
  99. package/src/index.ts +2 -47
  100. package/src/solid/converters.ts +60 -199
  101. package/src/solid/index.ts +2 -8
  102. package/src/solid/primitives.ts +4 -0
  103. package/test/base/flowEffect.test.ts +108 -0
  104. package/test/base/flowGraph.test.ts +485 -0
  105. package/test/base/flowSignal.test.ts +372 -0
  106. package/test/collections/flowArray.asyncStates.test.ts +1553 -0
  107. package/test/collections/flowArray.scalars.test.ts +1129 -0
  108. package/test/collections/flowArray.states.test.ts +1365 -0
  109. package/test/collections/flowMap.asyncStates.test.ts +1105 -0
  110. package/test/collections/flowMap.scalars.test.ts +877 -0
  111. package/test/collections/flowMap.states.test.ts +1097 -0
  112. package/test/nodes/async/flowConstantAsync.test.ts +860 -0
  113. package/test/nodes/async/flowDerivationAsync.test.ts +1517 -0
  114. package/test/nodes/async/flowStateAsync.test.ts +1387 -0
  115. package/test/{resource.test.ts → nodes/await/advanced/resource.test.ts} +21 -19
  116. package/test/{resourceAsync.test.ts → nodes/await/advanced/resourceAsync.test.ts} +3 -1
  117. package/test/{stream.test.ts → nodes/await/advanced/stream.test.ts} +30 -28
  118. package/test/{streamAsync.test.ts → nodes/await/advanced/streamAsync.test.ts} +16 -14
  119. package/test/nodes/await/flowConstantAwait.test.ts +643 -0
  120. package/test/nodes/await/flowDerivationAwait.test.ts +1583 -0
  121. package/test/nodes/await/flowStateAwait.test.ts +999 -0
  122. package/test/nodes/mixed/derivation.test.ts +1527 -0
  123. package/test/nodes/sync/flowConstant.test.ts +620 -0
  124. package/test/nodes/sync/flowDerivation.test.ts +1373 -0
  125. package/test/nodes/sync/flowState.test.ts +945 -0
  126. package/test/solid/converters.test.ts +721 -0
  127. package/test/solid/primitives.test.ts +1031 -0
  128. package/tsconfig.json +2 -1
  129. package/vitest.config.ts +7 -1
  130. package/IMPLEMENTATION_GUIDE.md +0 -1578
  131. package/dist/types/advanced/array.d.ts.map +0 -1
  132. package/dist/types/advanced/index.d.ts +0 -9
  133. package/dist/types/advanced/index.d.ts.map +0 -1
  134. package/dist/types/advanced/map.d.ts +0 -166
  135. package/dist/types/advanced/map.d.ts.map +0 -1
  136. package/dist/types/advanced/resource.d.ts +0 -78
  137. package/dist/types/advanced/resource.d.ts.map +0 -1
  138. package/dist/types/advanced/resourceAsync.d.ts +0 -56
  139. package/dist/types/advanced/resourceAsync.d.ts.map +0 -1
  140. package/dist/types/advanced/stream.d.ts +0 -117
  141. package/dist/types/advanced/stream.d.ts.map +0 -1
  142. package/dist/types/advanced/streamAsync.d.ts +0 -97
  143. package/dist/types/advanced/streamAsync.d.ts.map +0 -1
  144. package/dist/types/basic/constant.d.ts +0 -60
  145. package/dist/types/basic/constant.d.ts.map +0 -1
  146. package/dist/types/basic/derivation.d.ts +0 -89
  147. package/dist/types/basic/derivation.d.ts.map +0 -1
  148. package/dist/types/basic/disposable.d.ts +0 -82
  149. package/dist/types/basic/disposable.d.ts.map +0 -1
  150. package/dist/types/basic/effect.d.ts +0 -67
  151. package/dist/types/basic/effect.d.ts.map +0 -1
  152. package/dist/types/basic/index.d.ts +0 -10
  153. package/dist/types/basic/index.d.ts.map +0 -1
  154. package/dist/types/basic/observable.d.ts +0 -83
  155. package/dist/types/basic/observable.d.ts.map +0 -1
  156. package/dist/types/basic/signal.d.ts +0 -69
  157. package/dist/types/basic/signal.d.ts.map +0 -1
  158. package/dist/types/basic/state.d.ts +0 -47
  159. package/dist/types/basic/state.d.ts.map +0 -1
  160. package/dist/types/basic/trackingContext.d.ts +0 -33
  161. package/dist/types/basic/trackingContext.d.ts.map +0 -1
  162. package/dist/types/creators.d.ts +0 -340
  163. package/dist/types/creators.d.ts.map +0 -1
  164. package/src/advanced/array.ts +0 -222
  165. package/src/advanced/index.ts +0 -12
  166. package/src/advanced/map.ts +0 -193
  167. package/src/basic/constant.ts +0 -97
  168. package/src/basic/derivation.ts +0 -147
  169. package/src/basic/disposable.ts +0 -86
  170. package/src/basic/effect.ts +0 -104
  171. package/src/basic/index.ts +0 -9
  172. package/src/basic/observable.ts +0 -109
  173. package/src/basic/signal.ts +0 -145
  174. package/src/basic/state.ts +0 -60
  175. package/src/basic/trackingContext.ts +0 -45
  176. package/src/creators.ts +0 -395
  177. package/test/array.test.ts +0 -600
  178. package/test/constant.test.ts +0 -44
  179. package/test/derivation.test.ts +0 -539
  180. package/test/effect.test.ts +0 -29
  181. package/test/map.test.ts +0 -240
  182. package/test/signal.test.ts +0 -72
  183. package/test/state.test.ts +0 -212
@@ -0,0 +1,485 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { effect, FlowGraph } from "#package";
3
+
4
+ describe("FlowGraph", () => {
5
+ beforeEach(() => {
6
+ FlowGraph.clear();
7
+ });
8
+
9
+ describe("unit", () => {
10
+ describe("clear", () => {
11
+ it("should reset all internal queues and processing state", () => {
12
+ // Use FlowGraph methods to populate queues
13
+ const notify = vi.fn();
14
+ FlowGraph.requestTrigger(notify);
15
+ FlowGraph.requestRead(() => 42);
16
+
17
+ // Clear should reset everything
18
+ FlowGraph.clear();
19
+
20
+ // After clear, new operations should work without interference
21
+ const newNotify = vi.fn();
22
+ const promise = FlowGraph.requestTrigger(newNotify);
23
+ expect(promise).toBeInstanceOf(Promise);
24
+ });
25
+
26
+ it("should allow multiple clear calls without errors", () => {
27
+ expect(() => {
28
+ FlowGraph.clear();
29
+ FlowGraph.clear();
30
+ FlowGraph.clear();
31
+ }).not.toThrow();
32
+ });
33
+ });
34
+ });
35
+
36
+ describe("integration", () => {
37
+ describe("requestRead", () => {
38
+ it("should process read operations synchronously", async () => {
39
+ const readFn = vi.fn(() => 42);
40
+ const promise = FlowGraph.requestRead(readFn);
41
+
42
+ expect(readFn).toHaveBeenCalled();
43
+ const result = await promise;
44
+ expect(result).toBe(42);
45
+ });
46
+
47
+ it("should process read operations asynchronously", async () => {
48
+ const readFn = vi.fn(async () => {
49
+ await new Promise((resolve) => setTimeout(resolve, 10));
50
+ return 100;
51
+ });
52
+ const promise = FlowGraph.requestRead(readFn);
53
+
54
+ const result = await promise;
55
+ expect(result).toBe(100);
56
+ });
57
+
58
+ it("should resolve promise with read value", async () => {
59
+ const value = "test-value";
60
+ const promise = FlowGraph.requestRead(() => value);
61
+
62
+ await expect(promise).resolves.toBe(value);
63
+ });
64
+
65
+ it("should process read operations in order", async () => {
66
+ const executionOrder: number[] = [];
67
+
68
+ const promise1 = FlowGraph.requestRead(() => {
69
+ executionOrder.push(1);
70
+ return 1;
71
+ });
72
+
73
+ const promise2 = FlowGraph.requestRead(() => {
74
+ executionOrder.push(2);
75
+ return 2;
76
+ });
77
+
78
+ const promise3 = FlowGraph.requestRead(() => {
79
+ executionOrder.push(3);
80
+ return 3;
81
+ });
82
+
83
+ await Promise.all([promise1, promise2, promise3]);
84
+
85
+ expect(executionOrder).toEqual([1, 2, 3]);
86
+ });
87
+
88
+ it("should stop processing after read operation", async () => {
89
+ const effectFn = vi.fn();
90
+ const $effect = effect(() => {
91
+ effectFn();
92
+ });
93
+
94
+ FlowGraph.pushEffects([$effect]);
95
+
96
+ // Read should stop processing, so effects shouldn't execute
97
+ await FlowGraph.requestRead(() => 42);
98
+
99
+ // Wait a bit to ensure effects don't execute
100
+ await new Promise((resolve) => setTimeout(resolve, 50));
101
+
102
+ // Effect should not have been executed by the read operation
103
+ expect(effectFn).toHaveBeenCalledTimes(1); // Only initial execution
104
+ });
105
+ });
106
+
107
+ describe("requestTrigger", () => {
108
+ it("should process trigger operations", async () => {
109
+ const notify = vi.fn();
110
+ const promise = FlowGraph.requestTrigger(notify);
111
+
112
+ await promise;
113
+ expect(notify).toHaveBeenCalled();
114
+ });
115
+
116
+ it("should call notify function when processing trigger", async () => {
117
+ const notify = vi.fn();
118
+ await FlowGraph.requestTrigger(notify);
119
+
120
+ expect(notify).toHaveBeenCalledTimes(1);
121
+ });
122
+
123
+ it("should resolve promise after trigger is processed", async () => {
124
+ const notify = vi.fn();
125
+ const promise = FlowGraph.requestTrigger(notify);
126
+
127
+ await expect(promise).resolves.toBeUndefined();
128
+ expect(notify).toHaveBeenCalled();
129
+ });
130
+
131
+ it("should process multiple triggers in order", async () => {
132
+ const executionOrder: string[] = [];
133
+ const notify1 = vi.fn(() => executionOrder.push("trigger1"));
134
+ const notify2 = vi.fn(() => executionOrder.push("trigger2"));
135
+ const notify3 = vi.fn(() => executionOrder.push("trigger3"));
136
+
137
+ const promise1 = FlowGraph.requestTrigger(notify1);
138
+ const promise2 = FlowGraph.requestTrigger(notify2);
139
+ const promise3 = FlowGraph.requestTrigger(notify3);
140
+
141
+ await Promise.all([promise1, promise2, promise3]);
142
+
143
+ expect(executionOrder).toEqual(["trigger1", "trigger2", "trigger3"]);
144
+ });
145
+
146
+ it("should execute effects after trigger notification", async () => {
147
+ const notify = vi.fn();
148
+ const effectFn = vi.fn();
149
+ const $effect = effect(() => {
150
+ effectFn();
151
+ });
152
+
153
+ FlowGraph.pushEffects([$effect]);
154
+
155
+ await FlowGraph.requestTrigger(notify);
156
+
157
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2)); // Initial + after trigger
158
+ });
159
+ });
160
+
161
+ describe("requestWrite", () => {
162
+ it("should process write operations with sync update", async () => {
163
+ const notify = vi.fn();
164
+ const update = vi.fn(() => true);
165
+ const promise = FlowGraph.requestWrite(notify, update);
166
+
167
+ await promise;
168
+ expect(update).toHaveBeenCalled();
169
+ expect(notify).toHaveBeenCalled();
170
+ });
171
+
172
+ it("should process write operations with async update", async () => {
173
+ const notify = vi.fn();
174
+ const update = vi.fn(async () => {
175
+ await new Promise((resolve) => setTimeout(resolve, 10));
176
+ return true;
177
+ });
178
+ const promise = FlowGraph.requestWrite(notify, update);
179
+
180
+ await promise;
181
+ expect(update).toHaveBeenCalled();
182
+ expect(notify).toHaveBeenCalled();
183
+ });
184
+
185
+ it("should call notify only when update returns true", async () => {
186
+ const notify1 = vi.fn();
187
+ const notify2 = vi.fn();
188
+ const notify3 = vi.fn();
189
+
190
+ await FlowGraph.requestWrite(notify1, () => true);
191
+ await FlowGraph.requestWrite(notify2, () => false);
192
+ await FlowGraph.requestWrite(notify3, () => true);
193
+
194
+ expect(notify1).toHaveBeenCalledTimes(1);
195
+ expect(notify2).not.toHaveBeenCalled();
196
+ expect(notify3).toHaveBeenCalledTimes(1);
197
+ });
198
+
199
+ it("should not call notify when update returns false", async () => {
200
+ const notify = vi.fn();
201
+ await FlowGraph.requestWrite(notify, () => false);
202
+
203
+ expect(notify).not.toHaveBeenCalled();
204
+ });
205
+
206
+ it("should resolve promise after write is processed", async () => {
207
+ const notify = vi.fn();
208
+ const update = vi.fn(() => true);
209
+ const promise = FlowGraph.requestWrite(notify, update);
210
+
211
+ await expect(promise).resolves.toBeUndefined();
212
+ });
213
+
214
+ it("should execute effects after write notification", async () => {
215
+ const notify = vi.fn();
216
+ const effectFn = vi.fn();
217
+ const $effect = effect(() => {
218
+ effectFn();
219
+ });
220
+
221
+ FlowGraph.pushEffects([$effect]);
222
+
223
+ await FlowGraph.requestWrite(notify, () => true);
224
+
225
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2)); // Initial + after write
226
+ });
227
+
228
+ it("should not execute effects when update returns false", async () => {
229
+ const notify = vi.fn();
230
+ const effectFn = vi.fn();
231
+ const $effect = effect(() => {
232
+ effectFn();
233
+ });
234
+
235
+ FlowGraph.pushEffects([$effect]);
236
+
237
+ await FlowGraph.requestWrite(notify, () => false);
238
+
239
+ // Wait to ensure effects don't execute
240
+ await new Promise((resolve) => setTimeout(resolve, 50));
241
+
242
+ expect(effectFn).toHaveBeenCalledTimes(1); // Only initial execution
243
+ });
244
+ });
245
+
246
+ describe("pushEffects", () => {
247
+ it("should queue effects for execution", async () => {
248
+ const effectFn1 = vi.fn();
249
+ const effectFn2 = vi.fn();
250
+ const $effect1 = effect(() => {
251
+ effectFn1();
252
+ });
253
+ const $effect2 = effect(() => {
254
+ effectFn2();
255
+ });
256
+
257
+ FlowGraph.pushEffects([$effect1, $effect2]);
258
+
259
+ const notify = vi.fn();
260
+ await FlowGraph.requestTrigger(notify);
261
+
262
+ await vi.waitFor(() => {
263
+ expect(effectFn1).toHaveBeenCalledTimes(2); // Initial + queued
264
+ expect(effectFn2).toHaveBeenCalledTimes(2); // Initial + queued
265
+ });
266
+ });
267
+
268
+ it("should execute queued effects after current operation", async () => {
269
+ const effectFn = vi.fn();
270
+ const $effect = effect(() => {
271
+ effectFn();
272
+ });
273
+
274
+ FlowGraph.pushEffects([$effect]);
275
+
276
+ const notify = vi.fn();
277
+ await FlowGraph.requestTrigger(notify);
278
+
279
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
280
+ });
281
+
282
+ it("should execute multiple effects in order", async () => {
283
+ const executionOrder: string[] = [];
284
+ const $effect1 = effect(() => {
285
+ executionOrder.push("effect1");
286
+ });
287
+ const $effect2 = effect(() => {
288
+ executionOrder.push("effect2");
289
+ });
290
+ const $effect3 = effect(() => {
291
+ executionOrder.push("effect3");
292
+ });
293
+
294
+ FlowGraph.pushEffects([$effect1, $effect2, $effect3]);
295
+
296
+ const notify = vi.fn();
297
+ await FlowGraph.requestTrigger(notify);
298
+
299
+ await vi.waitFor(() =>
300
+ expect(executionOrder.length).toBeGreaterThan(3),
301
+ );
302
+
303
+ // Check that effects execute in order (after initial executions)
304
+ const lastThree = executionOrder.slice(-3);
305
+ expect(lastThree).toEqual(["effect1", "effect2", "effect3"]);
306
+ });
307
+
308
+ it("should clear effects queue after execution", async () => {
309
+ const effectFn1 = vi.fn();
310
+ const effectFn2 = vi.fn();
311
+ const $effect1 = effect(() => {
312
+ effectFn1();
313
+ });
314
+ const $effect2 = effect(() => {
315
+ effectFn2();
316
+ });
317
+
318
+ FlowGraph.pushEffects([$effect1, $effect2]);
319
+
320
+ const notify1 = vi.fn();
321
+ await FlowGraph.requestTrigger(notify1);
322
+
323
+ await vi.waitFor(() => {
324
+ expect(effectFn1).toHaveBeenCalledTimes(2);
325
+ expect(effectFn2).toHaveBeenCalledTimes(2);
326
+ });
327
+
328
+ // Push effects again and trigger - should execute again
329
+ FlowGraph.pushEffects([$effect1, $effect2]);
330
+
331
+ const notify2 = vi.fn();
332
+ await FlowGraph.requestTrigger(notify2);
333
+
334
+ await vi.waitFor(() => {
335
+ expect(effectFn1).toHaveBeenCalledTimes(3);
336
+ expect(effectFn2).toHaveBeenCalledTimes(3);
337
+ });
338
+ });
339
+
340
+ it("should handle async effects", async () => {
341
+ const executionOrder: string[] = [];
342
+ const $effect = effect(async () => {
343
+ executionOrder.push("effect-start");
344
+ await new Promise((resolve) => setTimeout(resolve, 10));
345
+ executionOrder.push("effect-end");
346
+ });
347
+
348
+ FlowGraph.pushEffects([$effect]);
349
+
350
+ const notify = vi.fn();
351
+ const promise = FlowGraph.requestTrigger(notify);
352
+
353
+ executionOrder.push("trigger-called");
354
+
355
+ await promise;
356
+ executionOrder.push("trigger-resolved");
357
+
358
+ await vi.waitFor(() => expect(executionOrder).toContain("effect-end"));
359
+
360
+ expect(executionOrder).toContain("effect-start");
361
+ expect(executionOrder).toContain("effect-end");
362
+ });
363
+ });
364
+
365
+ describe("operation ordering", () => {
366
+ it("should process operations in correct order (read stops, write/trigger continue)", async () => {
367
+ const executionOrder: string[] = [];
368
+ const effectFn = vi.fn(() => executionOrder.push("effect"));
369
+ const $effect = effect(() => {
370
+ effectFn();
371
+ });
372
+
373
+ FlowGraph.pushEffects([$effect]);
374
+
375
+ // Read should stop processing
376
+ await FlowGraph.requestRead(() => {
377
+ executionOrder.push("read");
378
+ return 1;
379
+ });
380
+
381
+ executionOrder.push("after-read");
382
+
383
+ // Write should continue and execute effects
384
+ await FlowGraph.requestWrite(
385
+ () => executionOrder.push("write-notify"),
386
+ () => {
387
+ executionOrder.push("write-update");
388
+ return true;
389
+ },
390
+ );
391
+
392
+ executionOrder.push("after-write");
393
+
394
+ await vi.waitFor(() =>
395
+ expect(executionOrder.length).toBeGreaterThan(5),
396
+ );
397
+
398
+ // Read should not trigger effects
399
+ expect(
400
+ executionOrder.filter((x) => x === "effect").length,
401
+ ).toBeGreaterThan(1); // At least initial + after write
402
+ });
403
+
404
+ it("should handle mixed operation types in queue", async () => {
405
+ const executionOrder: string[] = [];
406
+
407
+ const read1 = FlowGraph.requestRead(() => {
408
+ executionOrder.push("read1");
409
+ return 1;
410
+ });
411
+
412
+ const trigger1 = FlowGraph.requestTrigger(() => {
413
+ executionOrder.push("trigger1");
414
+ });
415
+
416
+ const write1 = FlowGraph.requestWrite(
417
+ () => executionOrder.push("write1-notify"),
418
+ () => {
419
+ executionOrder.push("write1-update");
420
+ return true;
421
+ },
422
+ );
423
+
424
+ const read2 = FlowGraph.requestRead(() => {
425
+ executionOrder.push("read2");
426
+ return 2;
427
+ });
428
+
429
+ await Promise.all([read1, trigger1, write1, read2]);
430
+
431
+ // Reads should execute first (stopping processing), then trigger/write
432
+ expect(executionOrder).toContain("read1");
433
+ expect(executionOrder).toContain("read2");
434
+ expect(executionOrder).toContain("trigger1");
435
+ expect(executionOrder).toContain("write1-update");
436
+ });
437
+
438
+ it("should prevent concurrent queue processing", async () => {
439
+ const executionOrder: string[] = [];
440
+
441
+ // Start multiple operations rapidly
442
+ const promises = [];
443
+ for (let i = 0; i < 5; i++) {
444
+ promises.push(
445
+ FlowGraph.requestTrigger(() => {
446
+ executionOrder.push(`trigger-${i}`);
447
+ }),
448
+ );
449
+ }
450
+
451
+ await Promise.all(promises);
452
+
453
+ // All triggers should have been processed sequentially
454
+ expect(executionOrder.length).toBe(5);
455
+ for (let i = 0; i < 5; i++) {
456
+ expect(executionOrder).toContain(`trigger-${i}`);
457
+ }
458
+ });
459
+
460
+ it("should execute effects after each write/trigger but not after read", async () => {
461
+ const effectFn = vi.fn();
462
+ const $effect = effect(() => {
463
+ effectFn();
464
+ });
465
+
466
+ // Initial execution
467
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(1));
468
+
469
+ // Read should not trigger effects
470
+ FlowGraph.pushEffects([$effect]);
471
+ await FlowGraph.requestRead(() => 1);
472
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(1));
473
+
474
+ // Write should trigger effects
475
+ await FlowGraph.requestWrite(vi.fn(), () => true);
476
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(2));
477
+
478
+ // Trigger should trigger effects
479
+ FlowGraph.pushEffects([$effect]);
480
+ await FlowGraph.requestTrigger(vi.fn());
481
+ await vi.waitFor(() => expect(effectFn).toHaveBeenCalledTimes(3));
482
+ });
483
+ });
484
+ });
485
+ });