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