@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.
Files changed (126) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/component/internals.d.ts +12 -2
  3. package/dist/component/internals.d.ts.map +1 -1
  4. package/dist/component/internals.js +25 -7
  5. package/dist/component/internals.js.map +1 -1
  6. package/dist/internal.d.ts +3 -2
  7. package/dist/internal.d.ts.map +1 -1
  8. package/dist/internal.js +3 -2
  9. package/dist/internal.js.map +1 -1
  10. package/dist/reactive/history.d.ts +2 -0
  11. package/dist/reactive/history.d.ts.map +1 -1
  12. package/dist/reactive/history.js +2 -1
  13. package/dist/reactive/history.js.map +1 -1
  14. package/dist/signal/batch.d.ts +1 -3
  15. package/dist/signal/batch.d.ts.map +1 -1
  16. package/dist/signal/batch.js +4 -16
  17. package/dist/signal/batch.js.map +1 -1
  18. package/dist/signal/computed.d.ts.map +1 -1
  19. package/dist/signal/computed.js +19 -61
  20. package/dist/signal/computed.js.map +1 -1
  21. package/dist/signal/effect.d.ts +6 -12
  22. package/dist/signal/effect.d.ts.map +1 -1
  23. package/dist/signal/effect.js +11 -74
  24. package/dist/signal/effect.js.map +1 -1
  25. package/dist/signal/graph.d.ts +111 -0
  26. package/dist/signal/graph.d.ts.map +1 -0
  27. package/dist/signal/graph.js +513 -0
  28. package/dist/signal/graph.js.map +1 -0
  29. package/dist/signal/index.d.ts +2 -1
  30. package/dist/signal/index.d.ts.map +1 -1
  31. package/dist/signal/index.js +1 -0
  32. package/dist/signal/index.js.map +1 -1
  33. package/dist/signal/peek.d.ts.map +1 -1
  34. package/dist/signal/peek.js +2 -9
  35. package/dist/signal/peek.js.map +1 -1
  36. package/dist/signal/persisted.d.ts +11 -1
  37. package/dist/signal/persisted.d.ts.map +1 -1
  38. package/dist/signal/persisted.js +39 -5
  39. package/dist/signal/persisted.js.map +1 -1
  40. package/dist/signal/selector.d.ts +5 -0
  41. package/dist/signal/selector.d.ts.map +1 -0
  42. package/dist/signal/selector.js +128 -0
  43. package/dist/signal/selector.js.map +1 -0
  44. package/dist/signal/signal.d.ts +0 -5
  45. package/dist/signal/signal.d.ts.map +1 -1
  46. package/dist/signal/signal.js +11 -81
  47. package/dist/signal/signal.js.map +1 -1
  48. package/package.json +13 -4
  49. package/src/component/internals.ts +35 -10
  50. package/src/internal.ts +27 -0
  51. package/src/reactive/history.ts +4 -1
  52. package/src/signal/batch.ts +5 -19
  53. package/src/signal/computed.ts +20 -63
  54. package/src/signal/effect.ts +12 -83
  55. package/src/signal/graph.ts +562 -0
  56. package/src/signal/index.ts +2 -1
  57. package/src/signal/peek.ts +2 -8
  58. package/src/signal/persisted.ts +54 -5
  59. package/src/signal/selector.ts +140 -0
  60. package/src/signal/signal.ts +11 -79
  61. package/dist/__tests__/batch.test.d.ts +0 -2
  62. package/dist/__tests__/batch.test.d.ts.map +0 -1
  63. package/dist/__tests__/batch.test.js +0 -73
  64. package/dist/__tests__/batch.test.js.map +0 -1
  65. package/dist/__tests__/component.test.d.ts +0 -2
  66. package/dist/__tests__/component.test.d.ts.map +0 -1
  67. package/dist/__tests__/component.test.js +0 -124
  68. package/dist/__tests__/component.test.js.map +0 -1
  69. package/dist/__tests__/computed.test.d.ts +0 -2
  70. package/dist/__tests__/computed.test.d.ts.map +0 -1
  71. package/dist/__tests__/computed.test.js +0 -252
  72. package/dist/__tests__/computed.test.js.map +0 -1
  73. package/dist/__tests__/effect.test.d.ts +0 -2
  74. package/dist/__tests__/effect.test.d.ts.map +0 -1
  75. package/dist/__tests__/effect.test.js +0 -189
  76. package/dist/__tests__/effect.test.js.map +0 -1
  77. package/dist/__tests__/peek.test.d.ts +0 -2
  78. package/dist/__tests__/peek.test.d.ts.map +0 -1
  79. package/dist/__tests__/peek.test.js +0 -26
  80. package/dist/__tests__/peek.test.js.map +0 -1
  81. package/dist/__tests__/persisted-signal.test.d.ts +0 -2
  82. package/dist/__tests__/persisted-signal.test.d.ts.map +0 -1
  83. package/dist/__tests__/persisted-signal.test.js +0 -156
  84. package/dist/__tests__/persisted-signal.test.js.map +0 -1
  85. package/dist/__tests__/reactive.test.d.ts +0 -2
  86. package/dist/__tests__/reactive.test.d.ts.map +0 -1
  87. package/dist/__tests__/reactive.test.js +0 -264
  88. package/dist/__tests__/reactive.test.js.map +0 -1
  89. package/dist/__tests__/resource-focus.test.d.ts +0 -2
  90. package/dist/__tests__/resource-focus.test.d.ts.map +0 -1
  91. package/dist/__tests__/resource-focus.test.js +0 -50
  92. package/dist/__tests__/resource-focus.test.js.map +0 -1
  93. package/dist/__tests__/resource-server-pass.test.d.ts +0 -2
  94. package/dist/__tests__/resource-server-pass.test.d.ts.map +0 -1
  95. package/dist/__tests__/resource-server-pass.test.js +0 -78
  96. package/dist/__tests__/resource-server-pass.test.js.map +0 -1
  97. package/dist/__tests__/resource.test.d.ts +0 -2
  98. package/dist/__tests__/resource.test.d.ts.map +0 -1
  99. package/dist/__tests__/resource.test.js +0 -336
  100. package/dist/__tests__/resource.test.js.map +0 -1
  101. package/dist/__tests__/signal.test.d.ts +0 -2
  102. package/dist/__tests__/signal.test.d.ts.map +0 -1
  103. package/dist/__tests__/signal.test.js +0 -186
  104. package/dist/__tests__/signal.test.js.map +0 -1
  105. package/dist/__tests__/synced.test.d.ts +0 -2
  106. package/dist/__tests__/synced.test.d.ts.map +0 -1
  107. package/dist/__tests__/synced.test.js +0 -142
  108. package/dist/__tests__/synced.test.js.map +0 -1
  109. package/dist/__tests__/track.test.d.ts +0 -2
  110. package/dist/__tests__/track.test.d.ts.map +0 -1
  111. package/dist/__tests__/track.test.js +0 -54
  112. package/dist/__tests__/track.test.js.map +0 -1
  113. package/src/__tests__/batch.test.ts +0 -87
  114. package/src/__tests__/component.test.ts +0 -165
  115. package/src/__tests__/computed.test.ts +0 -290
  116. package/src/__tests__/effect.test.ts +0 -226
  117. package/src/__tests__/peek.test.ts +0 -31
  118. package/src/__tests__/persisted-signal.test.ts +0 -194
  119. package/src/__tests__/reactive.test.ts +0 -305
  120. package/src/__tests__/resource-focus.test.ts +0 -61
  121. package/src/__tests__/resource-server-pass.test.ts +0 -90
  122. package/src/__tests__/resource.test.ts +0 -421
  123. package/src/__tests__/signal.test.ts +0 -213
  124. package/src/__tests__/synced.test.ts +0 -173
  125. package/src/__tests__/track.test.ts +0 -61
  126. 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
- });