rask-ui 0.29.3 → 0.29.4
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/dist/render.d.ts.map +1 -1
- package/dist/render.js +21 -17
- package/package.json +1 -1
- package/swc-plugin/target/wasm32-wasip1/release/swc_plugin_rask_component.wasm +0 -0
- package/dist/compiler.d.ts +0 -4
- package/dist/compiler.d.ts.map +0 -1
- package/dist/compiler.js +0 -7
- package/dist/createAsync.d.ts +0 -39
- package/dist/createAsync.d.ts.map +0 -1
- package/dist/createAsync.js +0 -47
- package/dist/createComputed.d.ts +0 -4
- package/dist/createComputed.d.ts.map +0 -1
- package/dist/createComputed.js +0 -69
- package/dist/createEffect.d.ts +0 -2
- package/dist/createEffect.d.ts.map +0 -1
- package/dist/createEffect.js +0 -29
- package/dist/createMutation.d.ts +0 -43
- package/dist/createMutation.d.ts.map +0 -1
- package/dist/createMutation.js +0 -76
- package/dist/createQuery.d.ts +0 -42
- package/dist/createQuery.d.ts.map +0 -1
- package/dist/createQuery.js +0 -80
- package/dist/createRouter.d.ts +0 -8
- package/dist/createRouter.d.ts.map +0 -1
- package/dist/createRouter.js +0 -27
- package/dist/createState.d.ts +0 -28
- package/dist/createState.d.ts.map +0 -1
- package/dist/createState.js +0 -129
- package/dist/createTask.d.ts +0 -31
- package/dist/createTask.d.ts.map +0 -1
- package/dist/createTask.js +0 -79
- package/dist/createView.d.ts +0 -28
- package/dist/createView.d.ts.map +0 -1
- package/dist/createView.js +0 -77
- package/dist/error.d.ts +0 -5
- package/dist/error.d.ts.map +0 -1
- package/dist/error.js +0 -16
- package/dist/jsx.d.ts +0 -11
- package/dist/patchInferno.d.ts +0 -6
- package/dist/patchInferno.d.ts.map +0 -1
- package/dist/patchInferno.js +0 -53
- package/dist/tests/batch.test.d.ts +0 -2
- package/dist/tests/batch.test.d.ts.map +0 -1
- package/dist/tests/batch.test.js +0 -434
- package/dist/tests/createComputed.test.d.ts +0 -2
- package/dist/tests/createComputed.test.d.ts.map +0 -1
- package/dist/tests/createComputed.test.js +0 -257
- package/dist/tests/createContext.test.d.ts +0 -2
- package/dist/tests/createContext.test.d.ts.map +0 -1
- package/dist/tests/createContext.test.js +0 -149
- package/dist/tests/createEffect.test.d.ts +0 -2
- package/dist/tests/createEffect.test.d.ts.map +0 -1
- package/dist/tests/createEffect.test.js +0 -467
- package/dist/tests/createState.test.d.ts +0 -2
- package/dist/tests/createState.test.d.ts.map +0 -1
- package/dist/tests/createState.test.js +0 -144
- package/dist/tests/createTask.test.d.ts +0 -2
- package/dist/tests/createTask.test.d.ts.map +0 -1
- package/dist/tests/createTask.test.js +0 -322
- package/dist/tests/createView.test.d.ts +0 -2
- package/dist/tests/createView.test.d.ts.map +0 -1
- package/dist/tests/createView.test.js +0 -203
- package/dist/tests/error.test.d.ts +0 -2
- package/dist/tests/error.test.d.ts.map +0 -1
- package/dist/tests/error.test.js +0 -181
- package/dist/tests/observation.test.d.ts +0 -2
- package/dist/tests/observation.test.d.ts.map +0 -1
- package/dist/tests/observation.test.js +0 -341
- package/dist/tests/renderCount.test.d.ts +0 -2
- package/dist/tests/renderCount.test.d.ts.map +0 -1
- package/dist/tests/renderCount.test.js +0 -95
- package/dist/tests/scopeEnforcement.test.d.ts +0 -2
- package/dist/tests/scopeEnforcement.test.d.ts.map +0 -1
- package/dist/tests/scopeEnforcement.test.js +0 -157
- package/dist/tests/useAction.test.d.ts +0 -2
- package/dist/tests/useAction.test.d.ts.map +0 -1
- package/dist/tests/useAction.test.js +0 -132
- package/dist/tests/useAsync.test.d.ts +0 -2
- package/dist/tests/useAsync.test.d.ts.map +0 -1
- package/dist/tests/useAsync.test.js +0 -499
- package/dist/tests/useDerived.test.d.ts +0 -2
- package/dist/tests/useDerived.test.d.ts.map +0 -1
- package/dist/tests/useDerived.test.js +0 -407
- package/dist/tests/useEffect.test.d.ts +0 -2
- package/dist/tests/useEffect.test.d.ts.map +0 -1
- package/dist/tests/useEffect.test.js +0 -600
- package/dist/tests/useLookup.test.d.ts +0 -2
- package/dist/tests/useLookup.test.d.ts.map +0 -1
- package/dist/tests/useLookup.test.js +0 -299
- package/dist/tests/useRef.test.d.ts +0 -2
- package/dist/tests/useRef.test.d.ts.map +0 -1
- package/dist/tests/useRef.test.js +0 -189
- package/dist/tests/useState.test.d.ts +0 -2
- package/dist/tests/useState.test.d.ts.map +0 -1
- package/dist/tests/useState.test.js +0 -178
- package/dist/tests/useSuspend.test.d.ts +0 -2
- package/dist/tests/useSuspend.test.d.ts.map +0 -1
- package/dist/tests/useSuspend.test.js +0 -752
- package/dist/tests/useView.test.d.ts +0 -2
- package/dist/tests/useView.test.d.ts.map +0 -1
- package/dist/tests/useView.test.js +0 -305
- package/dist/useComputed.d.ts +0 -5
- package/dist/useComputed.d.ts.map +0 -1
- package/dist/useComputed.js +0 -69
- package/dist/useQuery.d.ts +0 -25
- package/dist/useQuery.d.ts.map +0 -1
- package/dist/useQuery.js +0 -25
- package/dist/useSuspendAsync.d.ts +0 -18
- package/dist/useSuspendAsync.d.ts.map +0 -1
- package/dist/useSuspendAsync.js +0 -37
- package/dist/useTask.d.ts +0 -25
- package/dist/useTask.d.ts.map +0 -1
- package/dist/useTask.js +0 -70
package/dist/tests/batch.test.js
DELETED
|
@@ -1,434 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { syncBatch } from "../batch";
|
|
3
|
-
import { useState } from "../useState";
|
|
4
|
-
import { Observer } from "../observation";
|
|
5
|
-
describe("syncBatch", () => {
|
|
6
|
-
it("should batch multiple state changes into a single notification", () => {
|
|
7
|
-
const state = useState({ count: 0, name: "Alice" });
|
|
8
|
-
let notifyCount = 0;
|
|
9
|
-
const observer = new Observer(() => {
|
|
10
|
-
notifyCount++;
|
|
11
|
-
});
|
|
12
|
-
const dispose = observer.observe();
|
|
13
|
-
state.count; // Track count
|
|
14
|
-
state.name; // Track name
|
|
15
|
-
dispose();
|
|
16
|
-
// Make multiple changes in a batch
|
|
17
|
-
syncBatch(() => {
|
|
18
|
-
state.count = 1;
|
|
19
|
-
state.name = "Bob";
|
|
20
|
-
state.count = 2;
|
|
21
|
-
});
|
|
22
|
-
// Should only notify once despite multiple changes, and synchronously
|
|
23
|
-
expect(notifyCount).toBe(1);
|
|
24
|
-
expect(state.count).toBe(2);
|
|
25
|
-
expect(state.name).toBe("Bob");
|
|
26
|
-
observer.dispose();
|
|
27
|
-
});
|
|
28
|
-
it("should handle nested batches correctly", () => {
|
|
29
|
-
const state = useState({ count: 0 });
|
|
30
|
-
let notifyCount = 0;
|
|
31
|
-
const observer = new Observer(() => {
|
|
32
|
-
notifyCount++;
|
|
33
|
-
});
|
|
34
|
-
const dispose = observer.observe();
|
|
35
|
-
state.count; // Track
|
|
36
|
-
dispose();
|
|
37
|
-
syncBatch(() => {
|
|
38
|
-
state.count = 1;
|
|
39
|
-
syncBatch(() => {
|
|
40
|
-
state.count = 2;
|
|
41
|
-
});
|
|
42
|
-
state.count = 3;
|
|
43
|
-
});
|
|
44
|
-
// Should still only notify once for nested batches
|
|
45
|
-
expect(notifyCount).toBe(1);
|
|
46
|
-
expect(state.count).toBe(3);
|
|
47
|
-
observer.dispose();
|
|
48
|
-
});
|
|
49
|
-
it("should handle multiple observers with syncBatch", () => {
|
|
50
|
-
const state = useState({ count: 0 });
|
|
51
|
-
let notifyCount1 = 0;
|
|
52
|
-
let notifyCount2 = 0;
|
|
53
|
-
const observer1 = new Observer(() => {
|
|
54
|
-
notifyCount1++;
|
|
55
|
-
});
|
|
56
|
-
const observer2 = new Observer(() => {
|
|
57
|
-
notifyCount2++;
|
|
58
|
-
});
|
|
59
|
-
const dispose1 = observer1.observe();
|
|
60
|
-
state.count; // Track in observer1
|
|
61
|
-
dispose1();
|
|
62
|
-
const dispose2 = observer2.observe();
|
|
63
|
-
state.count; // Track in observer2
|
|
64
|
-
dispose2();
|
|
65
|
-
syncBatch(() => {
|
|
66
|
-
state.count = 1;
|
|
67
|
-
state.count = 2;
|
|
68
|
-
state.count = 3;
|
|
69
|
-
});
|
|
70
|
-
// Both observers should be notified exactly once
|
|
71
|
-
expect(notifyCount1).toBe(1);
|
|
72
|
-
expect(notifyCount2).toBe(1);
|
|
73
|
-
observer1.dispose();
|
|
74
|
-
observer2.dispose();
|
|
75
|
-
});
|
|
76
|
-
it("should maintain correct state values after syncBatch", () => {
|
|
77
|
-
const state = useState({
|
|
78
|
-
count: 0,
|
|
79
|
-
name: "Alice",
|
|
80
|
-
items: [1, 2, 3],
|
|
81
|
-
});
|
|
82
|
-
syncBatch(() => {
|
|
83
|
-
state.count = 10;
|
|
84
|
-
state.name = "Bob";
|
|
85
|
-
state.items.push(4);
|
|
86
|
-
state.items[0] = 100;
|
|
87
|
-
});
|
|
88
|
-
expect(state.count).toBe(10);
|
|
89
|
-
expect(state.name).toBe("Bob");
|
|
90
|
-
expect(state.items).toEqual([100, 2, 3, 4]);
|
|
91
|
-
});
|
|
92
|
-
it("should not flush if exception thrown within syncBatch", () => {
|
|
93
|
-
const state = useState({ count: 0 });
|
|
94
|
-
let notifyCount = 0;
|
|
95
|
-
const observer = new Observer(() => {
|
|
96
|
-
notifyCount++;
|
|
97
|
-
});
|
|
98
|
-
const dispose = observer.observe();
|
|
99
|
-
state.count; // Track
|
|
100
|
-
dispose();
|
|
101
|
-
try {
|
|
102
|
-
syncBatch(() => {
|
|
103
|
-
state.count = 1;
|
|
104
|
-
throw new Error("Test error");
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
catch (e) {
|
|
108
|
-
// Expected error
|
|
109
|
-
}
|
|
110
|
-
// Should NOT have flushed since the batch was interrupted
|
|
111
|
-
expect(notifyCount).toBe(0);
|
|
112
|
-
// But state change still occurred
|
|
113
|
-
expect(state.count).toBe(1);
|
|
114
|
-
observer.dispose();
|
|
115
|
-
});
|
|
116
|
-
it("should deduplicate notifications for the same observer", () => {
|
|
117
|
-
const state = useState({ count: 0, name: "Alice" });
|
|
118
|
-
let notifyCount = 0;
|
|
119
|
-
const observer = new Observer(() => {
|
|
120
|
-
notifyCount++;
|
|
121
|
-
});
|
|
122
|
-
const dispose = observer.observe();
|
|
123
|
-
state.count; // Track
|
|
124
|
-
state.name; // Track
|
|
125
|
-
dispose();
|
|
126
|
-
syncBatch(() => {
|
|
127
|
-
state.count = 1; // Triggers observer
|
|
128
|
-
state.name = "Bob"; // Triggers same observer again
|
|
129
|
-
state.count = 2; // Triggers observer yet again
|
|
130
|
-
});
|
|
131
|
-
// Should deduplicate and only notify once
|
|
132
|
-
expect(notifyCount).toBe(1);
|
|
133
|
-
observer.dispose();
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
describe("queue (async batching)", () => {
|
|
137
|
-
it("should queue updates and flush on microtask", async () => {
|
|
138
|
-
const state = useState({ count: 0 });
|
|
139
|
-
let notifyCount = 0;
|
|
140
|
-
const observer = new Observer(() => {
|
|
141
|
-
notifyCount++;
|
|
142
|
-
});
|
|
143
|
-
const dispose = observer.observe();
|
|
144
|
-
state.count; // Track
|
|
145
|
-
dispose();
|
|
146
|
-
// Make changes that will be queued
|
|
147
|
-
state.count = 1;
|
|
148
|
-
state.count = 2;
|
|
149
|
-
state.count = 3;
|
|
150
|
-
// Not yet notified (queued)
|
|
151
|
-
expect(notifyCount).toBe(0);
|
|
152
|
-
// Wait for microtask flush
|
|
153
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
154
|
-
// Should have notified once after flush
|
|
155
|
-
expect(notifyCount).toBe(1);
|
|
156
|
-
expect(state.count).toBe(3);
|
|
157
|
-
observer.dispose();
|
|
158
|
-
});
|
|
159
|
-
it("should batch multiple async updates into one notification", async () => {
|
|
160
|
-
const state = useState({ count: 0, name: "Alice" });
|
|
161
|
-
let notifyCount = 0;
|
|
162
|
-
const observer = new Observer(() => {
|
|
163
|
-
notifyCount++;
|
|
164
|
-
});
|
|
165
|
-
const dispose = observer.observe();
|
|
166
|
-
state.count;
|
|
167
|
-
state.name;
|
|
168
|
-
dispose();
|
|
169
|
-
state.count = 1;
|
|
170
|
-
state.name = "Bob";
|
|
171
|
-
state.count = 2;
|
|
172
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
173
|
-
// Should batch all updates into single notification
|
|
174
|
-
expect(notifyCount).toBe(1);
|
|
175
|
-
observer.dispose();
|
|
176
|
-
});
|
|
177
|
-
it("should handle separate async batches", async () => {
|
|
178
|
-
const state = useState({ count: 0 });
|
|
179
|
-
let notifyCount = 0;
|
|
180
|
-
const observer = new Observer(() => {
|
|
181
|
-
notifyCount++;
|
|
182
|
-
});
|
|
183
|
-
const dispose = observer.observe();
|
|
184
|
-
state.count;
|
|
185
|
-
dispose();
|
|
186
|
-
state.count = 1;
|
|
187
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
188
|
-
const afterFirst = notifyCount;
|
|
189
|
-
state.count = 2;
|
|
190
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
191
|
-
const afterSecond = notifyCount;
|
|
192
|
-
expect(afterFirst).toBe(1);
|
|
193
|
-
expect(afterSecond).toBe(2);
|
|
194
|
-
observer.dispose();
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
describe("syncBatch with nested async updates", () => {
|
|
198
|
-
it("should handle syncBatch inside async context", async () => {
|
|
199
|
-
const state = useState({ count: 0 });
|
|
200
|
-
let notifyCount = 0;
|
|
201
|
-
const observer = new Observer(() => {
|
|
202
|
-
notifyCount++;
|
|
203
|
-
});
|
|
204
|
-
const dispose = observer.observe();
|
|
205
|
-
state.count;
|
|
206
|
-
dispose();
|
|
207
|
-
// Async update
|
|
208
|
-
state.count = 1;
|
|
209
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
210
|
-
expect(notifyCount).toBe(1);
|
|
211
|
-
// Sync batch after async
|
|
212
|
-
syncBatch(() => {
|
|
213
|
-
state.count = 2;
|
|
214
|
-
state.count = 3;
|
|
215
|
-
});
|
|
216
|
-
expect(notifyCount).toBe(2); // +1 from sync batch
|
|
217
|
-
observer.dispose();
|
|
218
|
-
});
|
|
219
|
-
it("should handle async updates inside syncBatch callback", async () => {
|
|
220
|
-
const state = useState({ count: 0 });
|
|
221
|
-
let notifyCount = 0;
|
|
222
|
-
const observer = new Observer(() => {
|
|
223
|
-
notifyCount++;
|
|
224
|
-
});
|
|
225
|
-
const dispose = observer.observe();
|
|
226
|
-
state.count;
|
|
227
|
-
dispose();
|
|
228
|
-
syncBatch(() => {
|
|
229
|
-
state.count = 1;
|
|
230
|
-
// Trigger an async update from within syncBatch
|
|
231
|
-
setTimeout(() => {
|
|
232
|
-
state.count = 2;
|
|
233
|
-
}, 0);
|
|
234
|
-
});
|
|
235
|
-
// Sync batch should flush immediately
|
|
236
|
-
expect(notifyCount).toBe(1);
|
|
237
|
-
expect(state.count).toBe(1);
|
|
238
|
-
// Wait for async update
|
|
239
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
240
|
-
expect(notifyCount).toBe(2);
|
|
241
|
-
expect(state.count).toBe(2);
|
|
242
|
-
observer.dispose();
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
describe("syncBatch with cascading updates", () => {
|
|
246
|
-
it("should handle cascading observer notifications within the same batch", () => {
|
|
247
|
-
const state = useState({ count: 0 });
|
|
248
|
-
const derived = useState({ doubled: 0 });
|
|
249
|
-
let stateNotifyCount = 0;
|
|
250
|
-
let derivedNotifyCount = 0;
|
|
251
|
-
let componentNotifyCount = 0;
|
|
252
|
-
// Observer 1: Watches state, updates derived (simulates useDerived)
|
|
253
|
-
const derivedObserver = new Observer(() => {
|
|
254
|
-
stateNotifyCount++;
|
|
255
|
-
// When state changes, update derived synchronously
|
|
256
|
-
derived.doubled = state.count * 2;
|
|
257
|
-
});
|
|
258
|
-
const dispose1 = derivedObserver.observe();
|
|
259
|
-
state.count; // Track state
|
|
260
|
-
dispose1();
|
|
261
|
-
// Observer 2: Watches derived (simulates component)
|
|
262
|
-
const componentObserver = new Observer(() => {
|
|
263
|
-
derivedNotifyCount++;
|
|
264
|
-
});
|
|
265
|
-
const dispose2 = componentObserver.observe();
|
|
266
|
-
derived.doubled; // Track derived
|
|
267
|
-
dispose2();
|
|
268
|
-
// Observer 3: Also watches derived (another component)
|
|
269
|
-
const component2Observer = new Observer(() => {
|
|
270
|
-
componentNotifyCount++;
|
|
271
|
-
});
|
|
272
|
-
const dispose3 = component2Observer.observe();
|
|
273
|
-
derived.doubled; // Track derived
|
|
274
|
-
dispose3();
|
|
275
|
-
// Make a change in a batch
|
|
276
|
-
syncBatch(() => {
|
|
277
|
-
state.count = 5;
|
|
278
|
-
});
|
|
279
|
-
// All observers should have been notified exactly once
|
|
280
|
-
expect(stateNotifyCount).toBe(1);
|
|
281
|
-
expect(derivedNotifyCount).toBe(1);
|
|
282
|
-
expect(componentNotifyCount).toBe(1);
|
|
283
|
-
expect(state.count).toBe(5);
|
|
284
|
-
expect(derived.doubled).toBe(10);
|
|
285
|
-
derivedObserver.dispose();
|
|
286
|
-
componentObserver.dispose();
|
|
287
|
-
component2Observer.dispose();
|
|
288
|
-
});
|
|
289
|
-
it("should handle multi-level cascading updates", () => {
|
|
290
|
-
const state = useState({ value: 0 });
|
|
291
|
-
const derived1 = useState({ level1: 0 });
|
|
292
|
-
const derived2 = useState({ level2: 0 });
|
|
293
|
-
const derived3 = useState({ level3: 0 });
|
|
294
|
-
const notifyCounts = [0, 0, 0, 0];
|
|
295
|
-
// Level 1: state -> derived1
|
|
296
|
-
const observer1 = new Observer(() => {
|
|
297
|
-
notifyCounts[0]++;
|
|
298
|
-
derived1.level1 = state.value + 1;
|
|
299
|
-
});
|
|
300
|
-
const dispose1 = observer1.observe();
|
|
301
|
-
state.value;
|
|
302
|
-
dispose1();
|
|
303
|
-
// Level 2: derived1 -> derived2
|
|
304
|
-
const observer2 = new Observer(() => {
|
|
305
|
-
notifyCounts[1]++;
|
|
306
|
-
derived2.level2 = derived1.level1 + 1;
|
|
307
|
-
});
|
|
308
|
-
const dispose2 = observer2.observe();
|
|
309
|
-
derived1.level1;
|
|
310
|
-
dispose2();
|
|
311
|
-
// Level 3: derived2 -> derived3
|
|
312
|
-
const observer3 = new Observer(() => {
|
|
313
|
-
notifyCounts[2]++;
|
|
314
|
-
derived3.level3 = derived2.level2 + 1;
|
|
315
|
-
});
|
|
316
|
-
const dispose3 = observer3.observe();
|
|
317
|
-
derived2.level2;
|
|
318
|
-
dispose3();
|
|
319
|
-
// Final observer: watches derived3
|
|
320
|
-
const observer4 = new Observer(() => {
|
|
321
|
-
notifyCounts[3]++;
|
|
322
|
-
});
|
|
323
|
-
const dispose4 = observer4.observe();
|
|
324
|
-
derived3.level3;
|
|
325
|
-
dispose4();
|
|
326
|
-
// Update state in a batch
|
|
327
|
-
syncBatch(() => {
|
|
328
|
-
state.value = 10;
|
|
329
|
-
});
|
|
330
|
-
// All levels should have cascaded and each observer notified exactly once
|
|
331
|
-
expect(notifyCounts).toEqual([1, 1, 1, 1]);
|
|
332
|
-
expect(state.value).toBe(10);
|
|
333
|
-
expect(derived1.level1).toBe(11);
|
|
334
|
-
expect(derived2.level2).toBe(12);
|
|
335
|
-
expect(derived3.level3).toBe(13);
|
|
336
|
-
observer1.dispose();
|
|
337
|
-
observer2.dispose();
|
|
338
|
-
observer3.dispose();
|
|
339
|
-
observer4.dispose();
|
|
340
|
-
});
|
|
341
|
-
it("should handle diamond dependency pattern", () => {
|
|
342
|
-
// Diamond: state -> [derived1, derived2] -> derived3
|
|
343
|
-
const state = useState({ value: 0 });
|
|
344
|
-
const derived1 = useState({ path1: 0 });
|
|
345
|
-
const derived2 = useState({ path2: 0 });
|
|
346
|
-
const derived3 = useState({ combined: 0 });
|
|
347
|
-
let derived3NotifyCount = 0;
|
|
348
|
-
// State -> derived1
|
|
349
|
-
const obs1 = new Observer(() => {
|
|
350
|
-
derived1.path1 = state.value * 2;
|
|
351
|
-
});
|
|
352
|
-
const d1 = obs1.observe();
|
|
353
|
-
state.value;
|
|
354
|
-
d1();
|
|
355
|
-
// State -> derived2
|
|
356
|
-
const obs2 = new Observer(() => {
|
|
357
|
-
derived2.path2 = state.value * 3;
|
|
358
|
-
});
|
|
359
|
-
const d2 = obs2.observe();
|
|
360
|
-
state.value;
|
|
361
|
-
d2();
|
|
362
|
-
// [derived1, derived2] -> derived3
|
|
363
|
-
const obs3 = new Observer(() => {
|
|
364
|
-
derived3.combined = derived1.path1 + derived2.path2;
|
|
365
|
-
});
|
|
366
|
-
const d3 = obs3.observe();
|
|
367
|
-
derived1.path1;
|
|
368
|
-
derived2.path2;
|
|
369
|
-
d3();
|
|
370
|
-
// Watch derived3
|
|
371
|
-
const obs4 = new Observer(() => {
|
|
372
|
-
derived3NotifyCount++;
|
|
373
|
-
});
|
|
374
|
-
const d4 = obs4.observe();
|
|
375
|
-
derived3.combined;
|
|
376
|
-
d4();
|
|
377
|
-
syncBatch(() => {
|
|
378
|
-
state.value = 5;
|
|
379
|
-
});
|
|
380
|
-
// derived3 should only be notified once despite two paths updating
|
|
381
|
-
expect(derived3NotifyCount).toBe(1);
|
|
382
|
-
expect(derived1.path1).toBe(10);
|
|
383
|
-
expect(derived2.path2).toBe(15);
|
|
384
|
-
expect(derived3.combined).toBe(25);
|
|
385
|
-
obs1.dispose();
|
|
386
|
-
obs2.dispose();
|
|
387
|
-
obs3.dispose();
|
|
388
|
-
obs4.dispose();
|
|
389
|
-
});
|
|
390
|
-
it("should not create infinite loops with circular dependencies", () => {
|
|
391
|
-
const state1 = useState({ value: 0 });
|
|
392
|
-
const state2 = useState({ value: 0 });
|
|
393
|
-
let notify1Count = 0;
|
|
394
|
-
let notify2Count = 0;
|
|
395
|
-
// Observer 1: watches state1, updates state2
|
|
396
|
-
const obs1 = new Observer(() => {
|
|
397
|
-
notify1Count++;
|
|
398
|
-
if (notify1Count > 10) {
|
|
399
|
-
throw new Error("Infinite loop detected");
|
|
400
|
-
}
|
|
401
|
-
// Only update if different to break the cycle
|
|
402
|
-
if (state2.value !== state1.value + 1) {
|
|
403
|
-
state2.value = state1.value + 1;
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
const d1 = obs1.observe();
|
|
407
|
-
state1.value;
|
|
408
|
-
d1();
|
|
409
|
-
// Observer 2: watches state2, updates state1
|
|
410
|
-
const obs2 = new Observer(() => {
|
|
411
|
-
notify2Count++;
|
|
412
|
-
if (notify2Count > 10) {
|
|
413
|
-
throw new Error("Infinite loop detected");
|
|
414
|
-
}
|
|
415
|
-
// Only update if different to break the cycle
|
|
416
|
-
if (state1.value !== state2.value - 1) {
|
|
417
|
-
state1.value = state2.value - 1;
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
const d2 = obs2.observe();
|
|
421
|
-
state2.value;
|
|
422
|
-
d2();
|
|
423
|
-
syncBatch(() => {
|
|
424
|
-
state1.value = 5;
|
|
425
|
-
});
|
|
426
|
-
// Should stabilize without infinite loop
|
|
427
|
-
expect(notify1Count).toBeLessThan(10);
|
|
428
|
-
expect(notify2Count).toBeLessThan(10);
|
|
429
|
-
expect(state1.value).toBe(5);
|
|
430
|
-
expect(state2.value).toBe(6);
|
|
431
|
-
obs1.dispose();
|
|
432
|
-
obs2.dispose();
|
|
433
|
-
});
|
|
434
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"createComputed.test.d.ts","sourceRoot":"","sources":["../../src/tests/createComputed.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { createComputed } from "../createComputed";
|
|
3
|
-
import { createState } from "../createState";
|
|
4
|
-
import { Observer } from "../observation";
|
|
5
|
-
describe("createComputed", () => {
|
|
6
|
-
it("should compute values lazily", () => {
|
|
7
|
-
const state = createState({ count: 5 });
|
|
8
|
-
const computeFn = vi.fn(() => state.count * 2);
|
|
9
|
-
const computed = createComputed({
|
|
10
|
-
doubled: computeFn,
|
|
11
|
-
});
|
|
12
|
-
// Should not compute until accessed
|
|
13
|
-
expect(computeFn).not.toHaveBeenCalled();
|
|
14
|
-
const result = computed.doubled;
|
|
15
|
-
expect(computeFn).toHaveBeenCalledTimes(1);
|
|
16
|
-
expect(result).toBe(10);
|
|
17
|
-
});
|
|
18
|
-
it("should cache computed values", () => {
|
|
19
|
-
const state = createState({ count: 5 });
|
|
20
|
-
const computeFn = vi.fn(() => state.count * 2);
|
|
21
|
-
const computed = createComputed({
|
|
22
|
-
doubled: computeFn,
|
|
23
|
-
});
|
|
24
|
-
// Access multiple times
|
|
25
|
-
computed.doubled;
|
|
26
|
-
computed.doubled;
|
|
27
|
-
computed.doubled;
|
|
28
|
-
// Should only compute once due to caching
|
|
29
|
-
expect(computeFn).toHaveBeenCalledTimes(1);
|
|
30
|
-
});
|
|
31
|
-
it("should invalidate cache when dependencies change", async () => {
|
|
32
|
-
const state = createState({ count: 5 });
|
|
33
|
-
const computeFn = vi.fn(() => state.count * 2);
|
|
34
|
-
const computed = createComputed({
|
|
35
|
-
doubled: computeFn,
|
|
36
|
-
});
|
|
37
|
-
expect(computed.doubled).toBe(10);
|
|
38
|
-
expect(computeFn).toHaveBeenCalledTimes(1);
|
|
39
|
-
// Change dependency
|
|
40
|
-
state.count = 10;
|
|
41
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
42
|
-
// Should recompute on next access
|
|
43
|
-
expect(computed.doubled).toBe(20);
|
|
44
|
-
expect(computeFn).toHaveBeenCalledTimes(2);
|
|
45
|
-
});
|
|
46
|
-
it("should handle multiple computed properties", async () => {
|
|
47
|
-
const state = createState({ width: 10, height: 5 });
|
|
48
|
-
const computed = createComputed({
|
|
49
|
-
area: () => state.width * state.height,
|
|
50
|
-
perimeter: () => 2 * (state.width + state.height),
|
|
51
|
-
});
|
|
52
|
-
expect(computed.area).toBe(50);
|
|
53
|
-
expect(computed.perimeter).toBe(30);
|
|
54
|
-
state.width = 20;
|
|
55
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
56
|
-
expect(computed.area).toBe(100);
|
|
57
|
-
expect(computed.perimeter).toBe(50);
|
|
58
|
-
});
|
|
59
|
-
it("should support computed properties depending on other computed properties", async () => {
|
|
60
|
-
const state = createState({ count: 5 });
|
|
61
|
-
const computed = createComputed({
|
|
62
|
-
doubled: () => state.count * 2,
|
|
63
|
-
quadrupled: () => computed.doubled * 2,
|
|
64
|
-
});
|
|
65
|
-
expect(computed.doubled).toBe(10);
|
|
66
|
-
expect(computed.quadrupled).toBe(20);
|
|
67
|
-
state.count = 10;
|
|
68
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
69
|
-
expect(computed.doubled).toBe(20);
|
|
70
|
-
expect(computed.quadrupled).toBe(40);
|
|
71
|
-
});
|
|
72
|
-
it("should be reactive when observed", async () => {
|
|
73
|
-
const state = createState({ count: 5 });
|
|
74
|
-
const computed = createComputed({
|
|
75
|
-
doubled: () => state.count * 2,
|
|
76
|
-
});
|
|
77
|
-
let observedValue = null;
|
|
78
|
-
const observer = new Observer(() => {
|
|
79
|
-
observedValue = computed.doubled;
|
|
80
|
-
});
|
|
81
|
-
const dispose = observer.observe();
|
|
82
|
-
computed.doubled; // Track the computed
|
|
83
|
-
dispose();
|
|
84
|
-
expect(observedValue).toBe(null);
|
|
85
|
-
// Change state
|
|
86
|
-
state.count = 10;
|
|
87
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
88
|
-
expect(observedValue).toBe(20);
|
|
89
|
-
observer.dispose();
|
|
90
|
-
});
|
|
91
|
-
it("should only recompute when actual dependencies change", () => {
|
|
92
|
-
const state = createState({ a: 1, b: 2 });
|
|
93
|
-
const computeFn = vi.fn(() => state.a * 2);
|
|
94
|
-
const computed = createComputed({
|
|
95
|
-
result: computeFn,
|
|
96
|
-
});
|
|
97
|
-
expect(computed.result).toBe(2);
|
|
98
|
-
expect(computeFn).toHaveBeenCalledTimes(1);
|
|
99
|
-
// Change unrelated property
|
|
100
|
-
state.b = 100;
|
|
101
|
-
// Should still return cached value
|
|
102
|
-
expect(computed.result).toBe(2);
|
|
103
|
-
expect(computeFn).toHaveBeenCalledTimes(1);
|
|
104
|
-
});
|
|
105
|
-
it("should handle complex dependency chains", async () => {
|
|
106
|
-
const state = createState({
|
|
107
|
-
items: [1, 2, 3, 4, 5],
|
|
108
|
-
multiplier: 2,
|
|
109
|
-
});
|
|
110
|
-
const computed = createComputed({
|
|
111
|
-
total: () => state.items.reduce((sum, item) => sum + item, 0),
|
|
112
|
-
multipliedTotal: () => computed.total * state.multiplier,
|
|
113
|
-
average: () => computed.total / state.items.length,
|
|
114
|
-
});
|
|
115
|
-
expect(computed.total).toBe(15);
|
|
116
|
-
expect(computed.multipliedTotal).toBe(30);
|
|
117
|
-
expect(computed.average).toBe(3);
|
|
118
|
-
state.items.push(6);
|
|
119
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
120
|
-
expect(computed.total).toBe(21);
|
|
121
|
-
expect(computed.multipliedTotal).toBe(42);
|
|
122
|
-
expect(computed.average).toBe(3.5);
|
|
123
|
-
state.multiplier = 3;
|
|
124
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
125
|
-
expect(computed.multipliedTotal).toBe(63);
|
|
126
|
-
});
|
|
127
|
-
it("should handle array operations", async () => {
|
|
128
|
-
const state = createState({ items: [1, 2, 3] });
|
|
129
|
-
const computed = createComputed({
|
|
130
|
-
sum: () => state.items.reduce((sum, item) => sum + item, 0),
|
|
131
|
-
count: () => state.items.length,
|
|
132
|
-
});
|
|
133
|
-
expect(computed.sum).toBe(6);
|
|
134
|
-
expect(computed.count).toBe(3);
|
|
135
|
-
state.items.push(4);
|
|
136
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
137
|
-
expect(computed.sum).toBe(10);
|
|
138
|
-
expect(computed.count).toBe(4);
|
|
139
|
-
state.items.pop();
|
|
140
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
141
|
-
expect(computed.sum).toBe(6);
|
|
142
|
-
expect(computed.count).toBe(3);
|
|
143
|
-
});
|
|
144
|
-
it("should handle deeply nested state", async () => {
|
|
145
|
-
const state = createState({
|
|
146
|
-
user: {
|
|
147
|
-
profile: {
|
|
148
|
-
name: "Alice",
|
|
149
|
-
age: 30,
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
});
|
|
153
|
-
const computed = createComputed({
|
|
154
|
-
displayName: () => `${state.user.profile.name} (${state.user.profile.age})`,
|
|
155
|
-
});
|
|
156
|
-
expect(computed.displayName).toBe("Alice (30)");
|
|
157
|
-
state.user.profile.name = "Bob";
|
|
158
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
159
|
-
expect(computed.displayName).toBe("Bob (30)");
|
|
160
|
-
state.user.profile.age = 25;
|
|
161
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
162
|
-
expect(computed.displayName).toBe("Bob (25)");
|
|
163
|
-
});
|
|
164
|
-
it("should not recompute unnecessarily with nested computed", async () => {
|
|
165
|
-
const state = createState({ count: 5 });
|
|
166
|
-
const innerFn = vi.fn(() => state.count * 2);
|
|
167
|
-
const outerFn = vi.fn(() => computed.inner + 10);
|
|
168
|
-
const computed = createComputed({
|
|
169
|
-
inner: innerFn,
|
|
170
|
-
outer: outerFn,
|
|
171
|
-
});
|
|
172
|
-
// Access outer (should compute both)
|
|
173
|
-
expect(computed.outer).toBe(20);
|
|
174
|
-
expect(innerFn).toHaveBeenCalledTimes(1);
|
|
175
|
-
expect(outerFn).toHaveBeenCalledTimes(1);
|
|
176
|
-
// Access outer again (should use cache)
|
|
177
|
-
expect(computed.outer).toBe(20);
|
|
178
|
-
expect(innerFn).toHaveBeenCalledTimes(1);
|
|
179
|
-
expect(outerFn).toHaveBeenCalledTimes(1);
|
|
180
|
-
// Change state
|
|
181
|
-
state.count = 10;
|
|
182
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
183
|
-
// Access outer (should recompute both)
|
|
184
|
-
expect(computed.outer).toBe(30);
|
|
185
|
-
expect(innerFn).toHaveBeenCalledTimes(2);
|
|
186
|
-
expect(outerFn).toHaveBeenCalledTimes(2);
|
|
187
|
-
});
|
|
188
|
-
it("should handle conditional dependencies", async () => {
|
|
189
|
-
const state = createState({ useA: true, a: 10, b: 20 });
|
|
190
|
-
const computeFn = vi.fn(() => (state.useA ? state.a : state.b));
|
|
191
|
-
const computed = createComputed({
|
|
192
|
-
value: computeFn,
|
|
193
|
-
});
|
|
194
|
-
expect(computed.value).toBe(10);
|
|
195
|
-
expect(computeFn).toHaveBeenCalledTimes(1);
|
|
196
|
-
// Change b (not currently tracked)
|
|
197
|
-
state.b = 30;
|
|
198
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
199
|
-
expect(computed.value).toBe(10); // Should not recompute
|
|
200
|
-
expect(computeFn).toHaveBeenCalledTimes(1);
|
|
201
|
-
// Change a (currently tracked)
|
|
202
|
-
state.a = 15;
|
|
203
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
204
|
-
expect(computed.value).toBe(15); // Should recompute
|
|
205
|
-
expect(computeFn).toHaveBeenCalledTimes(2);
|
|
206
|
-
// Switch to using b
|
|
207
|
-
state.useA = false;
|
|
208
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
209
|
-
expect(computed.value).toBe(30); // Should recompute and now track b
|
|
210
|
-
expect(computeFn).toHaveBeenCalledTimes(3);
|
|
211
|
-
// Change a (no longer tracked)
|
|
212
|
-
state.a = 100;
|
|
213
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
214
|
-
expect(computed.value).toBe(30); // Should not recompute
|
|
215
|
-
expect(computeFn).toHaveBeenCalledTimes(3);
|
|
216
|
-
// Change b (now tracked)
|
|
217
|
-
state.b = 40;
|
|
218
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
219
|
-
expect(computed.value).toBe(40); // Should recompute
|
|
220
|
-
expect(computeFn).toHaveBeenCalledTimes(4);
|
|
221
|
-
});
|
|
222
|
-
it("should return consistent values during same synchronous execution", () => {
|
|
223
|
-
const state = createState({ count: 5 });
|
|
224
|
-
const computed = createComputed({
|
|
225
|
-
doubled: () => state.count * 2,
|
|
226
|
-
});
|
|
227
|
-
const first = computed.doubled;
|
|
228
|
-
const second = computed.doubled;
|
|
229
|
-
const third = computed.doubled;
|
|
230
|
-
expect(first).toBe(10);
|
|
231
|
-
expect(second).toBe(10);
|
|
232
|
-
expect(third).toBe(10);
|
|
233
|
-
});
|
|
234
|
-
it("should handle empty computed object", () => {
|
|
235
|
-
const computed = createComputed({});
|
|
236
|
-
expect(Object.keys(computed).length).toBe(0);
|
|
237
|
-
});
|
|
238
|
-
it("should properly track changes in computed used by observers", async () => {
|
|
239
|
-
const state = createState({ x: 1, y: 2 });
|
|
240
|
-
const computed = createComputed({
|
|
241
|
-
sum: () => state.x + state.y,
|
|
242
|
-
});
|
|
243
|
-
const results = [];
|
|
244
|
-
const observer = new Observer(() => {
|
|
245
|
-
results.push(computed.sum);
|
|
246
|
-
});
|
|
247
|
-
const dispose = observer.observe();
|
|
248
|
-
computed.sum; // Track
|
|
249
|
-
dispose();
|
|
250
|
-
state.x = 10;
|
|
251
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
252
|
-
state.y = 20;
|
|
253
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
254
|
-
expect(results).toEqual([12, 30]);
|
|
255
|
-
observer.dispose();
|
|
256
|
-
});
|
|
257
|
-
});
|