@praxisjs/core 2.1.0 → 2.3.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/CHANGELOG.md +30 -0
- package/dist/component/internals.d.ts +12 -2
- package/dist/component/internals.d.ts.map +1 -1
- package/dist/component/internals.js +25 -7
- package/dist/component/internals.js.map +1 -1
- package/dist/internal.d.ts +3 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +3 -2
- package/dist/internal.js.map +1 -1
- package/dist/reactive/history.d.ts +2 -0
- package/dist/reactive/history.d.ts.map +1 -1
- package/dist/reactive/history.js +2 -1
- package/dist/reactive/history.js.map +1 -1
- package/dist/signal/batch.d.ts +1 -3
- package/dist/signal/batch.d.ts.map +1 -1
- package/dist/signal/batch.js +4 -16
- package/dist/signal/batch.js.map +1 -1
- package/dist/signal/computed.d.ts.map +1 -1
- package/dist/signal/computed.js +19 -61
- package/dist/signal/computed.js.map +1 -1
- package/dist/signal/effect.d.ts +6 -12
- package/dist/signal/effect.d.ts.map +1 -1
- package/dist/signal/effect.js +11 -74
- package/dist/signal/effect.js.map +1 -1
- package/dist/signal/graph.d.ts +111 -0
- package/dist/signal/graph.d.ts.map +1 -0
- package/dist/signal/graph.js +513 -0
- package/dist/signal/graph.js.map +1 -0
- package/dist/signal/index.d.ts +2 -1
- package/dist/signal/index.d.ts.map +1 -1
- package/dist/signal/index.js +1 -0
- package/dist/signal/index.js.map +1 -1
- package/dist/signal/peek.d.ts.map +1 -1
- package/dist/signal/peek.js +2 -9
- package/dist/signal/peek.js.map +1 -1
- package/dist/signal/persisted.d.ts +11 -1
- package/dist/signal/persisted.d.ts.map +1 -1
- package/dist/signal/persisted.js +39 -5
- package/dist/signal/persisted.js.map +1 -1
- package/dist/signal/selector.d.ts +5 -0
- package/dist/signal/selector.d.ts.map +1 -0
- package/dist/signal/selector.js +128 -0
- package/dist/signal/selector.js.map +1 -0
- package/dist/signal/signal.d.ts +0 -5
- package/dist/signal/signal.d.ts.map +1 -1
- package/dist/signal/signal.js +11 -81
- package/dist/signal/signal.js.map +1 -1
- package/package.json +13 -4
- package/src/component/internals.ts +35 -10
- package/src/internal.ts +27 -0
- package/src/reactive/history.ts +4 -1
- package/src/signal/batch.ts +5 -19
- package/src/signal/computed.ts +20 -63
- package/src/signal/effect.ts +12 -83
- package/src/signal/graph.ts +562 -0
- package/src/signal/index.ts +2 -1
- package/src/signal/peek.ts +2 -8
- package/src/signal/persisted.ts +54 -5
- package/src/signal/selector.ts +140 -0
- package/src/signal/signal.ts +11 -79
- 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 -73
- package/dist/__tests__/batch.test.js.map +0 -1
- package/dist/__tests__/component.test.d.ts +0 -2
- package/dist/__tests__/component.test.d.ts.map +0 -1
- package/dist/__tests__/component.test.js +0 -124
- package/dist/__tests__/component.test.js.map +0 -1
- package/dist/__tests__/computed.test.d.ts +0 -2
- package/dist/__tests__/computed.test.d.ts.map +0 -1
- package/dist/__tests__/computed.test.js +0 -252
- package/dist/__tests__/computed.test.js.map +0 -1
- package/dist/__tests__/effect.test.d.ts +0 -2
- package/dist/__tests__/effect.test.d.ts.map +0 -1
- package/dist/__tests__/effect.test.js +0 -189
- package/dist/__tests__/effect.test.js.map +0 -1
- package/dist/__tests__/peek.test.d.ts +0 -2
- package/dist/__tests__/peek.test.d.ts.map +0 -1
- package/dist/__tests__/peek.test.js +0 -26
- package/dist/__tests__/peek.test.js.map +0 -1
- package/dist/__tests__/persisted-signal.test.d.ts +0 -2
- package/dist/__tests__/persisted-signal.test.d.ts.map +0 -1
- package/dist/__tests__/persisted-signal.test.js +0 -156
- package/dist/__tests__/persisted-signal.test.js.map +0 -1
- package/dist/__tests__/reactive.test.d.ts +0 -2
- package/dist/__tests__/reactive.test.d.ts.map +0 -1
- package/dist/__tests__/reactive.test.js +0 -264
- package/dist/__tests__/reactive.test.js.map +0 -1
- package/dist/__tests__/resource-focus.test.d.ts +0 -2
- package/dist/__tests__/resource-focus.test.d.ts.map +0 -1
- package/dist/__tests__/resource-focus.test.js +0 -50
- package/dist/__tests__/resource-focus.test.js.map +0 -1
- package/dist/__tests__/resource-server-pass.test.d.ts +0 -2
- package/dist/__tests__/resource-server-pass.test.d.ts.map +0 -1
- package/dist/__tests__/resource-server-pass.test.js +0 -78
- package/dist/__tests__/resource-server-pass.test.js.map +0 -1
- package/dist/__tests__/resource.test.d.ts +0 -2
- package/dist/__tests__/resource.test.d.ts.map +0 -1
- package/dist/__tests__/resource.test.js +0 -336
- package/dist/__tests__/resource.test.js.map +0 -1
- package/dist/__tests__/signal.test.d.ts +0 -2
- package/dist/__tests__/signal.test.d.ts.map +0 -1
- package/dist/__tests__/signal.test.js +0 -186
- package/dist/__tests__/signal.test.js.map +0 -1
- package/dist/__tests__/synced.test.d.ts +0 -2
- package/dist/__tests__/synced.test.d.ts.map +0 -1
- package/dist/__tests__/synced.test.js +0 -142
- package/dist/__tests__/synced.test.js.map +0 -1
- package/dist/__tests__/track.test.d.ts +0 -2
- package/dist/__tests__/track.test.d.ts.map +0 -1
- package/dist/__tests__/track.test.js +0 -54
- package/dist/__tests__/track.test.js.map +0 -1
- package/src/__tests__/batch.test.ts +0 -87
- package/src/__tests__/component.test.ts +0 -165
- package/src/__tests__/computed.test.ts +0 -290
- package/src/__tests__/effect.test.ts +0 -226
- package/src/__tests__/peek.test.ts +0 -31
- package/src/__tests__/persisted-signal.test.ts +0 -194
- package/src/__tests__/reactive.test.ts +0 -305
- package/src/__tests__/resource-focus.test.ts +0 -61
- package/src/__tests__/resource-server-pass.test.ts +0 -90
- package/src/__tests__/resource.test.ts +0 -421
- package/src/__tests__/signal.test.ts +0 -213
- package/src/__tests__/synced.test.ts +0 -173
- package/src/__tests__/track.test.ts +0 -61
- package/tsconfig.json +0 -8
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { computed, writableComputed } from "../signal/computed";
|
|
4
|
-
import { signal } from "../signal/signal";
|
|
5
|
-
|
|
6
|
-
describe("computed", () => {
|
|
7
|
-
it("derives value from signal", () => {
|
|
8
|
-
const s = signal(4);
|
|
9
|
-
const doubled = computed(() => s() * 2);
|
|
10
|
-
expect(doubled()).toBe(8);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("updates when source signal changes", () => {
|
|
14
|
-
const s = signal(3);
|
|
15
|
-
const triple = computed(() => s() * 3);
|
|
16
|
-
s.set(5);
|
|
17
|
-
expect(triple()).toBe(15);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("is lazy — does not recompute until accessed", () => {
|
|
21
|
-
const s = signal(1);
|
|
22
|
-
const fn = vi.fn(() => s() + 1);
|
|
23
|
-
const c = computed(fn);
|
|
24
|
-
expect(fn).not.toHaveBeenCalled();
|
|
25
|
-
c();
|
|
26
|
-
expect(fn).toHaveBeenCalledOnce();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("caches value between reads when source has not changed", () => {
|
|
30
|
-
const s = signal(2);
|
|
31
|
-
const fn = vi.fn(() => s() * 10);
|
|
32
|
-
const c = computed(fn);
|
|
33
|
-
c();
|
|
34
|
-
c();
|
|
35
|
-
expect(fn).toHaveBeenCalledOnce();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("recomputes after source change", () => {
|
|
39
|
-
const s = signal(1);
|
|
40
|
-
const fn = vi.fn(() => s() + 100);
|
|
41
|
-
const c = computed(fn);
|
|
42
|
-
c();
|
|
43
|
-
s.set(2);
|
|
44
|
-
c();
|
|
45
|
-
expect(fn).toHaveBeenCalledTimes(2);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("marks __isComputed = true", () => {
|
|
49
|
-
const c = computed(() => 42);
|
|
50
|
-
expect(c.__isComputed).toBe(true);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("chained computed values update correctly", () => {
|
|
54
|
-
const s = signal(1);
|
|
55
|
-
const doubled = computed(() => s() * 2);
|
|
56
|
-
const quadrupled = computed(() => doubled() * 2);
|
|
57
|
-
expect(quadrupled()).toBe(4);
|
|
58
|
-
s.set(3);
|
|
59
|
-
expect(quadrupled()).toBe(12);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("coalesces subscriber notifications when multiple dependencies change in the same tick", async () => {
|
|
63
|
-
const first = signal("John");
|
|
64
|
-
const last = signal("Doe");
|
|
65
|
-
const fullName = computed(() => `${first()} ${last()}`);
|
|
66
|
-
|
|
67
|
-
const calls: string[] = [];
|
|
68
|
-
fullName.subscribe((v) => calls.push(v));
|
|
69
|
-
expect(calls).toEqual(["John Doe"]); // immediate on subscribe
|
|
70
|
-
|
|
71
|
-
first.set("Jane");
|
|
72
|
-
last.set("Smith");
|
|
73
|
-
await Promise.resolve();
|
|
74
|
-
|
|
75
|
-
// Both changes in the same tick → single notification with final value
|
|
76
|
-
expect(calls).toEqual(["John Doe", "Jane Smith"]);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("subscribe fires immediately with computed value", () => {
|
|
80
|
-
const s = signal(5);
|
|
81
|
-
const c = computed(() => s() + 1);
|
|
82
|
-
const received: number[] = [];
|
|
83
|
-
c.subscribe((v) => received.push(v));
|
|
84
|
-
expect(received).toEqual([6]);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("subscribe fires when source changes", async () => {
|
|
88
|
-
const s = signal(0);
|
|
89
|
-
const c = computed(() => s() * 3);
|
|
90
|
-
const received: number[] = [];
|
|
91
|
-
c.subscribe((v) => received.push(v));
|
|
92
|
-
s.set(2);
|
|
93
|
-
await Promise.resolve();
|
|
94
|
-
s.set(4);
|
|
95
|
-
await Promise.resolve();
|
|
96
|
-
expect(received).toEqual([0, 6, 12]);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("unsubscribe stops receiving updates", () => {
|
|
100
|
-
const s = signal(0);
|
|
101
|
-
const c = computed(() => s() + 1);
|
|
102
|
-
const received: number[] = [];
|
|
103
|
-
const unsub = c.subscribe((v) => received.push(v));
|
|
104
|
-
unsub();
|
|
105
|
-
s.set(10);
|
|
106
|
-
expect(received).toEqual([1]);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("computeFn that throws propagates the error on read", () => {
|
|
110
|
-
const s = signal(true);
|
|
111
|
-
const c = computed(() => {
|
|
112
|
-
if (s()) throw new Error("compute error");
|
|
113
|
-
return 0;
|
|
114
|
-
});
|
|
115
|
-
expect(() => c()).toThrow("compute error");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("dynamic dependency: switches tracked signal based on condition", () => {
|
|
119
|
-
const toggle = signal(true);
|
|
120
|
-
const a = signal(10);
|
|
121
|
-
const b = signal(20);
|
|
122
|
-
const c = computed(() => (toggle() ? a() : b()));
|
|
123
|
-
|
|
124
|
-
expect(c()).toBe(10);
|
|
125
|
-
a.set(11);
|
|
126
|
-
expect(c()).toBe(11);
|
|
127
|
-
|
|
128
|
-
toggle.set(false);
|
|
129
|
-
expect(c()).toBe(20);
|
|
130
|
-
b.set(21);
|
|
131
|
-
expect(c()).toBe(21);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("dynamic dependency: stale branch changes do not dirty the computed", () => {
|
|
135
|
-
const toggle = signal(true);
|
|
136
|
-
const a = signal(10);
|
|
137
|
-
const b = signal(20);
|
|
138
|
-
const fn = vi.fn(() => (toggle() ? a() : b()));
|
|
139
|
-
const c = computed(fn);
|
|
140
|
-
|
|
141
|
-
expect(c()).toBe(10);
|
|
142
|
-
toggle.set(false);
|
|
143
|
-
expect(c()).toBe(20);
|
|
144
|
-
expect(fn).toHaveBeenCalledTimes(2);
|
|
145
|
-
|
|
146
|
-
a.set(11);
|
|
147
|
-
expect(c()).toBe(20);
|
|
148
|
-
expect(fn).toHaveBeenCalledTimes(2);
|
|
149
|
-
|
|
150
|
-
b.set(21);
|
|
151
|
-
expect(c()).toBe(21);
|
|
152
|
-
expect(fn).toHaveBeenCalledTimes(3);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("unsubscribe then re-subscribe works correctly", async () => {
|
|
156
|
-
const s = signal(1);
|
|
157
|
-
const c = computed(() => s() * 3);
|
|
158
|
-
const received: number[] = [];
|
|
159
|
-
|
|
160
|
-
const unsub = c.subscribe((v) => received.push(v));
|
|
161
|
-
unsub();
|
|
162
|
-
|
|
163
|
-
// Re-subscribe
|
|
164
|
-
c.subscribe((v) => received.push(v));
|
|
165
|
-
s.set(2);
|
|
166
|
-
await Promise.resolve();
|
|
167
|
-
expect(received).toContain(6);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("dirty remains true after computeFn throws — next read recomputes instead of returning stale cache", () => {
|
|
171
|
-
const s = signal(true);
|
|
172
|
-
const fn = vi.fn(() => {
|
|
173
|
-
if (s()) throw new Error("compute error");
|
|
174
|
-
return 42;
|
|
175
|
-
});
|
|
176
|
-
const c = computed(fn);
|
|
177
|
-
// First read — throws, dirty stays true
|
|
178
|
-
expect(() => c()).toThrow("compute error");
|
|
179
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
180
|
-
// Second read while still throwing — must recompute, not return stale
|
|
181
|
-
expect(() => c()).toThrow("compute error");
|
|
182
|
-
expect(fn).toHaveBeenCalledTimes(2);
|
|
183
|
-
// Fix the source and read — should now succeed
|
|
184
|
-
s.set(false);
|
|
185
|
-
expect(c()).toBe(42);
|
|
186
|
-
expect(fn).toHaveBeenCalledTimes(3);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("circular dependency between two computed values throws or does not hang", () => {
|
|
190
|
-
// This test documents behavior: circular deps must not cause infinite loops
|
|
191
|
-
// We use a signal to break potential infinite recursion via dirty flag
|
|
192
|
-
const s = signal(0);
|
|
193
|
-
let aVal = 0;
|
|
194
|
-
let bVal = 0;
|
|
195
|
-
const a: ReturnType<typeof computed<number>> = computed(() => {
|
|
196
|
-
s(); // track signal to make dirty
|
|
197
|
-
return bVal + 1;
|
|
198
|
-
});
|
|
199
|
-
const b: ReturnType<typeof computed<number>> = computed(() => {
|
|
200
|
-
s(); // track signal to make dirty
|
|
201
|
-
return aVal + 1;
|
|
202
|
-
});
|
|
203
|
-
// Manually reading without cross-dependency reads to avoid true circularity
|
|
204
|
-
aVal = a();
|
|
205
|
-
bVal = b();
|
|
206
|
-
expect(aVal).toBeGreaterThanOrEqual(1);
|
|
207
|
-
expect(bVal).toBeGreaterThanOrEqual(1);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("microtask guard: skips notifySubs when leaf subscriber unsubscribes before microtask fires", async () => {
|
|
211
|
-
const s = signal(0);
|
|
212
|
-
const c = computed(() => s() * 2);
|
|
213
|
-
const results: number[] = [];
|
|
214
|
-
const unsub = c.subscribe((v) => results.push(v));
|
|
215
|
-
// results = [0]
|
|
216
|
-
|
|
217
|
-
s.set(1); // markDirty: queues microtask (scheduled = true)
|
|
218
|
-
unsub(); // removes leaf subscriber BEFORE microtask fires → leafHolder.subs = null
|
|
219
|
-
|
|
220
|
-
await Promise.resolve(); // microtask: if (leafHolder.subs !== null) → FALSE → no notify
|
|
221
|
-
expect(results).toEqual([0]); // no update received after unsubscribe
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it("skips re-scheduling leaf notification when already scheduled (!scheduled false branch)", async () => {
|
|
225
|
-
const s = signal(0);
|
|
226
|
-
const c = computed(() => s() * 2);
|
|
227
|
-
const results: number[] = [];
|
|
228
|
-
c.subscribe((v) => results.push(v));
|
|
229
|
-
// results = [0] after initial subscribe
|
|
230
|
-
|
|
231
|
-
s.set(1); // markDirty: scheduled = true, microtask queued
|
|
232
|
-
c(); // re-computes: dirty = false (scheduled still true)
|
|
233
|
-
s.set(2); // markDirty: dirty=false → proceeds, but !scheduled is false → no second microtask
|
|
234
|
-
|
|
235
|
-
await Promise.resolve(); // microtask runs, subscriber notified with current value
|
|
236
|
-
expect(results).toContain(4); // 2 * 2
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("double unsubscribe on computed does not crash", () => {
|
|
240
|
-
const s = signal(1);
|
|
241
|
-
const c = computed(() => s() + 1);
|
|
242
|
-
const unsub = c.subscribe(() => {});
|
|
243
|
-
expect(() => {
|
|
244
|
-
unsub();
|
|
245
|
-
unsub();
|
|
246
|
-
}).not.toThrow();
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
describe("writableComputed", () => {
|
|
251
|
-
it("read returns the getter value", () => {
|
|
252
|
-
const s = signal(5);
|
|
253
|
-
const wc = writableComputed(() => s() * 2, (_v) => { /* no-op */ });
|
|
254
|
-
expect(wc()).toBe(10);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it("set calls the setter", () => {
|
|
258
|
-
const s = signal(0);
|
|
259
|
-
const wc = writableComputed(() => s(), (v) => { s.set(v); });
|
|
260
|
-
wc.set(42);
|
|
261
|
-
expect(s()).toBe(42);
|
|
262
|
-
expect(wc()).toBe(42);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("update applies the updater and calls the setter", () => {
|
|
266
|
-
const s = signal(10);
|
|
267
|
-
const wc = writableComputed(() => s(), (v) => { s.set(v); });
|
|
268
|
-
wc.update((current) => current + 5);
|
|
269
|
-
expect(s()).toBe(15);
|
|
270
|
-
expect(wc()).toBe(15);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("marks __isWritableComputed = true", () => {
|
|
274
|
-
const wc = writableComputed(() => 1, (_v) => { /* no-op */ });
|
|
275
|
-
expect(wc.__isWritableComputed).toBe(true);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it("inherits __isComputed = true from computed", () => {
|
|
279
|
-
const wc = writableComputed(() => 1, (_v) => { /* no-op */ });
|
|
280
|
-
expect(wc.__isComputed).toBe(true);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it("recomputes when underlying signal changes", () => {
|
|
284
|
-
const s = signal(3);
|
|
285
|
-
const wc = writableComputed(() => s() * 2, (_v) => { /* no-op */ });
|
|
286
|
-
expect(wc()).toBe(6);
|
|
287
|
-
s.set(7);
|
|
288
|
-
expect(wc()).toBe(14);
|
|
289
|
-
});
|
|
290
|
-
});
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
effect,
|
|
5
|
-
track,
|
|
6
|
-
activeEffect,
|
|
7
|
-
cleanupEffectDeps,
|
|
8
|
-
recordDependency,
|
|
9
|
-
type SubscriberHolder,
|
|
10
|
-
} from "../signal/effect";
|
|
11
|
-
import { batch } from "../signal/batch";
|
|
12
|
-
import { signal } from "../signal/signal";
|
|
13
|
-
|
|
14
|
-
describe("effect", () => {
|
|
15
|
-
it("runs immediately on creation", () => {
|
|
16
|
-
const ran = vi.fn();
|
|
17
|
-
effect(ran);
|
|
18
|
-
expect(ran).toHaveBeenCalledOnce();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("re-runs when a tracked signal changes", () => {
|
|
22
|
-
const s = signal(1);
|
|
23
|
-
const values: number[] = [];
|
|
24
|
-
effect(() => {
|
|
25
|
-
values.push(s());
|
|
26
|
-
});
|
|
27
|
-
s.set(2);
|
|
28
|
-
s.set(3);
|
|
29
|
-
expect(values).toEqual([1, 2, 3]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("stop() calls the cleanup function", () => {
|
|
33
|
-
const cleanupFn = vi.fn();
|
|
34
|
-
const stop = effect(() => cleanupFn);
|
|
35
|
-
stop();
|
|
36
|
-
expect(cleanupFn).toHaveBeenCalledOnce();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("calls cleanup function before re-run", () => {
|
|
40
|
-
const s = signal(0);
|
|
41
|
-
const order: string[] = [];
|
|
42
|
-
effect(() => {
|
|
43
|
-
const v = s();
|
|
44
|
-
order.push(`run:${v}`);
|
|
45
|
-
return () => order.push(`cleanup`);
|
|
46
|
-
});
|
|
47
|
-
s.set(1);
|
|
48
|
-
expect(order).toEqual(["run:0", "cleanup", "run:1"]);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("tracks multiple signals", () => {
|
|
52
|
-
const a = signal(1);
|
|
53
|
-
const b = signal(10);
|
|
54
|
-
const sums: number[] = [];
|
|
55
|
-
effect(() => {
|
|
56
|
-
sums.push(a() + b());
|
|
57
|
-
});
|
|
58
|
-
a.set(2);
|
|
59
|
-
b.set(20);
|
|
60
|
-
expect(sums).toEqual([11, 12, 22]);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("stop() prevents future re-runs", () => {
|
|
64
|
-
const s = signal(0);
|
|
65
|
-
const values: number[] = [];
|
|
66
|
-
const stop = effect(() => {
|
|
67
|
-
values.push(s());
|
|
68
|
-
});
|
|
69
|
-
stop();
|
|
70
|
-
s.set(99);
|
|
71
|
-
expect(values).toEqual([0]); // only initial run
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("drops dependencies that are no longer read", () => {
|
|
75
|
-
const useA = signal(true);
|
|
76
|
-
const a = signal(1);
|
|
77
|
-
const b = signal(10);
|
|
78
|
-
const values: number[] = [];
|
|
79
|
-
|
|
80
|
-
effect(() => {
|
|
81
|
-
values.push(useA() ? a() : b());
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
useA.set(false);
|
|
85
|
-
expect(values).toEqual([1, 10]);
|
|
86
|
-
|
|
87
|
-
a.set(2);
|
|
88
|
-
expect(values).toEqual([1, 10]);
|
|
89
|
-
|
|
90
|
-
b.set(20);
|
|
91
|
-
expect(values).toEqual([1, 10, 20]);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("does not skip other subscribers when one effect changes subscriptions while notifying", () => {
|
|
95
|
-
const useOther = signal(false);
|
|
96
|
-
const source = signal(0);
|
|
97
|
-
const other = signal(100);
|
|
98
|
-
const first = vi.fn();
|
|
99
|
-
const second = vi.fn();
|
|
100
|
-
|
|
101
|
-
effect(() => {
|
|
102
|
-
first(useOther() ? other() : source());
|
|
103
|
-
});
|
|
104
|
-
effect(() => {
|
|
105
|
-
second(source());
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
useOther.set(true);
|
|
109
|
-
first.mockClear();
|
|
110
|
-
second.mockClear();
|
|
111
|
-
|
|
112
|
-
source.set(1);
|
|
113
|
-
|
|
114
|
-
expect(first).not.toHaveBeenCalled();
|
|
115
|
-
expect(second).toHaveBeenCalledWith(1);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("effect with no reactive dependencies runs once only", () => {
|
|
119
|
-
const ran = vi.fn();
|
|
120
|
-
effect(() => {
|
|
121
|
-
ran(); // no signal reads
|
|
122
|
-
});
|
|
123
|
-
expect(ran).toHaveBeenCalledTimes(1);
|
|
124
|
-
// No signal changes possible — just verify it doesn't re-run spontaneously
|
|
125
|
-
expect(ran).toHaveBeenCalledTimes(1);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("stop() with no cleanup function does not throw", () => {
|
|
129
|
-
const stop = effect(() => { /* no return value */ });
|
|
130
|
-
expect(() => stop()).not.toThrow();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("cleanup is re-invoked before each re-run", () => {
|
|
134
|
-
const s = signal(0);
|
|
135
|
-
const cleanupCount = { n: 0 };
|
|
136
|
-
effect(() => {
|
|
137
|
-
s(); // track
|
|
138
|
-
return () => { cleanupCount.n++; };
|
|
139
|
-
});
|
|
140
|
-
s.set(1);
|
|
141
|
-
s.set(2);
|
|
142
|
-
// cleanup called once before second run, once before third run
|
|
143
|
-
expect(cleanupCount.n).toBe(2);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("stop() called multiple times does not crash or double-clean", () => {
|
|
147
|
-
const cleanupFn = vi.fn();
|
|
148
|
-
const stop = effect(() => cleanupFn);
|
|
149
|
-
expect(() => {
|
|
150
|
-
stop();
|
|
151
|
-
stop();
|
|
152
|
-
stop();
|
|
153
|
-
}).not.toThrow();
|
|
154
|
-
// cleanup is only called once (on first stop)
|
|
155
|
-
expect(cleanupFn).toHaveBeenCalledTimes(1);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("ignores a queued run after the effect was stopped", () => {
|
|
159
|
-
const s = signal(0);
|
|
160
|
-
const values: number[] = [];
|
|
161
|
-
const stop = effect(() => {
|
|
162
|
-
values.push(s());
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
batch(() => {
|
|
166
|
-
s.set(1);
|
|
167
|
-
stop();
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
expect(values).toEqual([0]);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("cleanupEffectDeps handles empty, single, and array dependency holders", () => {
|
|
174
|
-
const tracked = vi.fn();
|
|
175
|
-
const other = vi.fn();
|
|
176
|
-
const emptyHolder: SubscriberHolder = { subs: null };
|
|
177
|
-
const singleHolder: SubscriberHolder = { subs: tracked };
|
|
178
|
-
const arrayHolder: SubscriberHolder = { subs: [tracked, other] };
|
|
179
|
-
const unmatchedSingleHolder: SubscriberHolder = { subs: other };
|
|
180
|
-
const unmatchedArrayHolder: SubscriberHolder = { subs: [other] };
|
|
181
|
-
|
|
182
|
-
recordDependency(tracked, emptyHolder);
|
|
183
|
-
recordDependency(tracked, singleHolder);
|
|
184
|
-
recordDependency(tracked, arrayHolder);
|
|
185
|
-
recordDependency(tracked, unmatchedSingleHolder);
|
|
186
|
-
recordDependency(tracked, unmatchedArrayHolder);
|
|
187
|
-
|
|
188
|
-
cleanupEffectDeps(tracked);
|
|
189
|
-
|
|
190
|
-
expect(emptyHolder.subs).toBeNull();
|
|
191
|
-
expect(singleHolder.subs).toBeNull();
|
|
192
|
-
expect(arrayHolder.subs).toEqual([other]);
|
|
193
|
-
expect(unmatchedSingleHolder.subs).toBe(other);
|
|
194
|
-
expect(unmatchedArrayHolder.subs).toEqual([other]);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("exception inside nested track() restores outer activeEffect", () => {
|
|
198
|
-
const s = signal(0);
|
|
199
|
-
const outer = vi.fn();
|
|
200
|
-
let outerEffectRef: unknown;
|
|
201
|
-
|
|
202
|
-
effect(() => {
|
|
203
|
-
outer();
|
|
204
|
-
s(); // track
|
|
205
|
-
outerEffectRef = activeEffect;
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// Now do a track that throws — outer activeEffect must be restored
|
|
209
|
-
try {
|
|
210
|
-
track(() => { throw new Error("inner throws"); });
|
|
211
|
-
} catch {
|
|
212
|
-
// expected
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
expect(activeEffect).toBeNull();
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it("activeEffect is null after a track() that throws", () => {
|
|
219
|
-
try {
|
|
220
|
-
track(() => { throw new Error("boom"); });
|
|
221
|
-
} catch {
|
|
222
|
-
// expected
|
|
223
|
-
}
|
|
224
|
-
expect(activeEffect).toBeNull();
|
|
225
|
-
});
|
|
226
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { computed } from "../signal/computed";
|
|
4
|
-
import { effect } from "../signal/effect";
|
|
5
|
-
import { peek } from "../signal/peek";
|
|
6
|
-
import { signal } from "../signal/signal";
|
|
7
|
-
|
|
8
|
-
describe("peek", () => {
|
|
9
|
-
it("returns the current value without tracking", () => {
|
|
10
|
-
const s = signal(42);
|
|
11
|
-
expect(peek(s)).toBe(42);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("does not subscribe the active effect when peeked", () => {
|
|
15
|
-
const s = signal(1);
|
|
16
|
-
const runs: number[] = [];
|
|
17
|
-
|
|
18
|
-
effect(() => {
|
|
19
|
-
runs.push(peek(s)); // peek — should not subscribe
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
s.set(99); // should NOT trigger re-run
|
|
23
|
-
expect(runs).toEqual([1]);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("works with computed values", () => {
|
|
27
|
-
const s = signal(5);
|
|
28
|
-
const c = computed(() => s() * 2);
|
|
29
|
-
expect(peek(c)).toBe(10);
|
|
30
|
-
});
|
|
31
|
-
});
|