@praxisjs/core 2.2.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 (116) hide show
  1. package/CHANGELOG.md +18 -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/signal/batch.d.ts +1 -3
  11. package/dist/signal/batch.d.ts.map +1 -1
  12. package/dist/signal/batch.js +4 -30
  13. package/dist/signal/batch.js.map +1 -1
  14. package/dist/signal/computed.d.ts.map +1 -1
  15. package/dist/signal/computed.js +13 -66
  16. package/dist/signal/computed.js.map +1 -1
  17. package/dist/signal/effect.d.ts +6 -14
  18. package/dist/signal/effect.d.ts.map +1 -1
  19. package/dist/signal/effect.js +11 -83
  20. package/dist/signal/effect.js.map +1 -1
  21. package/dist/signal/graph.d.ts +111 -0
  22. package/dist/signal/graph.d.ts.map +1 -0
  23. package/dist/signal/graph.js +513 -0
  24. package/dist/signal/graph.js.map +1 -0
  25. package/dist/signal/index.d.ts +1 -0
  26. package/dist/signal/index.d.ts.map +1 -1
  27. package/dist/signal/index.js +1 -0
  28. package/dist/signal/index.js.map +1 -1
  29. package/dist/signal/peek.d.ts.map +1 -1
  30. package/dist/signal/peek.js +2 -9
  31. package/dist/signal/peek.js.map +1 -1
  32. package/dist/signal/selector.d.ts +5 -0
  33. package/dist/signal/selector.d.ts.map +1 -0
  34. package/dist/signal/selector.js +128 -0
  35. package/dist/signal/selector.js.map +1 -0
  36. package/dist/signal/signal.d.ts +0 -5
  37. package/dist/signal/signal.d.ts.map +1 -1
  38. package/dist/signal/signal.js +11 -70
  39. package/dist/signal/signal.js.map +1 -1
  40. package/package.json +13 -4
  41. package/src/component/internals.ts +35 -10
  42. package/src/internal.ts +26 -0
  43. package/src/signal/batch.ts +5 -33
  44. package/src/signal/computed.ts +13 -66
  45. package/src/signal/effect.ts +12 -92
  46. package/src/signal/graph.ts +562 -0
  47. package/src/signal/index.ts +1 -0
  48. package/src/signal/peek.ts +2 -8
  49. package/src/signal/selector.ts +140 -0
  50. package/src/signal/signal.ts +11 -74
  51. package/dist/__tests__/batch.test.d.ts +0 -2
  52. package/dist/__tests__/batch.test.d.ts.map +0 -1
  53. package/dist/__tests__/batch.test.js +0 -124
  54. package/dist/__tests__/batch.test.js.map +0 -1
  55. package/dist/__tests__/component.test.d.ts +0 -2
  56. package/dist/__tests__/component.test.d.ts.map +0 -1
  57. package/dist/__tests__/component.test.js +0 -124
  58. package/dist/__tests__/component.test.js.map +0 -1
  59. package/dist/__tests__/computed.test.d.ts +0 -2
  60. package/dist/__tests__/computed.test.d.ts.map +0 -1
  61. package/dist/__tests__/computed.test.js +0 -289
  62. package/dist/__tests__/computed.test.js.map +0 -1
  63. package/dist/__tests__/effect.test.d.ts +0 -2
  64. package/dist/__tests__/effect.test.d.ts.map +0 -1
  65. package/dist/__tests__/effect.test.js +0 -200
  66. package/dist/__tests__/effect.test.js.map +0 -1
  67. package/dist/__tests__/peek.test.d.ts +0 -2
  68. package/dist/__tests__/peek.test.d.ts.map +0 -1
  69. package/dist/__tests__/peek.test.js +0 -26
  70. package/dist/__tests__/peek.test.js.map +0 -1
  71. package/dist/__tests__/persisted-signal.test.d.ts +0 -2
  72. package/dist/__tests__/persisted-signal.test.d.ts.map +0 -1
  73. package/dist/__tests__/persisted-signal.test.js +0 -252
  74. package/dist/__tests__/persisted-signal.test.js.map +0 -1
  75. package/dist/__tests__/reactive.test.d.ts +0 -2
  76. package/dist/__tests__/reactive.test.d.ts.map +0 -1
  77. package/dist/__tests__/reactive.test.js +0 -299
  78. package/dist/__tests__/reactive.test.js.map +0 -1
  79. package/dist/__tests__/resource-focus.test.d.ts +0 -2
  80. package/dist/__tests__/resource-focus.test.d.ts.map +0 -1
  81. package/dist/__tests__/resource-focus.test.js +0 -50
  82. package/dist/__tests__/resource-focus.test.js.map +0 -1
  83. package/dist/__tests__/resource-server-pass.test.d.ts +0 -2
  84. package/dist/__tests__/resource-server-pass.test.d.ts.map +0 -1
  85. package/dist/__tests__/resource-server-pass.test.js +0 -78
  86. package/dist/__tests__/resource-server-pass.test.js.map +0 -1
  87. package/dist/__tests__/resource.test.d.ts +0 -2
  88. package/dist/__tests__/resource.test.d.ts.map +0 -1
  89. package/dist/__tests__/resource.test.js +0 -336
  90. package/dist/__tests__/resource.test.js.map +0 -1
  91. package/dist/__tests__/signal.test.d.ts +0 -2
  92. package/dist/__tests__/signal.test.d.ts.map +0 -1
  93. package/dist/__tests__/signal.test.js +0 -254
  94. package/dist/__tests__/signal.test.js.map +0 -1
  95. package/dist/__tests__/synced.test.d.ts +0 -2
  96. package/dist/__tests__/synced.test.d.ts.map +0 -1
  97. package/dist/__tests__/synced.test.js +0 -142
  98. package/dist/__tests__/synced.test.js.map +0 -1
  99. package/dist/__tests__/track.test.d.ts +0 -2
  100. package/dist/__tests__/track.test.d.ts.map +0 -1
  101. package/dist/__tests__/track.test.js +0 -54
  102. package/dist/__tests__/track.test.js.map +0 -1
  103. package/src/__tests__/batch.test.ts +0 -138
  104. package/src/__tests__/component.test.ts +0 -165
  105. package/src/__tests__/computed.test.ts +0 -333
  106. package/src/__tests__/effect.test.ts +0 -241
  107. package/src/__tests__/peek.test.ts +0 -31
  108. package/src/__tests__/persisted-signal.test.ts +0 -305
  109. package/src/__tests__/reactive.test.ts +0 -343
  110. package/src/__tests__/resource-focus.test.ts +0 -61
  111. package/src/__tests__/resource-server-pass.test.ts +0 -90
  112. package/src/__tests__/resource.test.ts +0 -421
  113. package/src/__tests__/signal.test.ts +0 -291
  114. package/src/__tests__/synced.test.ts +0 -173
  115. package/src/__tests__/track.test.ts +0 -61
  116. package/tsconfig.json +0 -8
@@ -1,165 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
-
3
- import { RootComponent } from "../component/base";
4
- import {
5
- getComponentAnchor,
6
- getComponentDefault,
7
- getComponentDefaults,
8
- getComponentProps,
9
- getComponentRawProp,
10
- isComponentMounted,
11
- isStateDirty,
12
- markStateDirty,
13
- setComponentAnchor,
14
- setComponentDefault,
15
- setComponentMounted,
16
- setComponentProps,
17
- setStateDirty,
18
- } from "../component/internals";
19
- import { StatefulComponent } from "../component/stateful";
20
- import { StatelessComponent } from "../component/stateless";
21
-
22
- class ConcreteRoot extends RootComponent<{ name: string }> {
23
- render() {
24
- return null;
25
- }
26
- }
27
-
28
- class ConcreteStateful extends StatefulComponent {
29
- render() {
30
- return null;
31
- }
32
- }
33
-
34
- describe("RootComponent", () => {
35
- it("stores props in component internals", () => {
36
- const c = new ConcreteRoot({ name: "Alice" });
37
- expect((getComponentProps(c) as { name: string }).name).toBe("Alice");
38
- });
39
-
40
- it("does not expose old underscore internals as own properties", () => {
41
- const c = new ConcreteRoot({ name: "Alice" });
42
- expect(Object.prototype.hasOwnProperty.call(c, "_rawProps")).toBe(false);
43
- expect(Object.prototype.hasOwnProperty.call(c, "_mounted")).toBe(false);
44
- expect(Object.prototype.hasOwnProperty.call(c, "_anchor")).toBe(false);
45
- expect(Object.prototype.hasOwnProperty.call(c, "_setProps")).toBe(false);
46
- });
47
-
48
- it("mounted starts as false", () => {
49
- const c = new ConcreteRoot({ name: "" });
50
- expect(isComponentMounted(c)).toBe(false);
51
- });
52
-
53
- it("anchor starts as undefined", () => {
54
- const c = new ConcreteRoot({ name: "" });
55
- expect(getComponentAnchor(c)).toBeUndefined();
56
- });
57
-
58
- it("defaults to empty object when no props given", () => {
59
- const c = new ConcreteRoot();
60
- expect(getComponentProps(c)).toEqual({});
61
- });
62
- });
63
-
64
- describe("StatefulComponent", () => {
65
- it("defaults start as empty object", () => {
66
- const c = new ConcreteStateful();
67
- expect(getComponentDefaults(c)).toEqual({});
68
- });
69
-
70
- it("stateDirty starts as false", () => {
71
- const c = new ConcreteStateful();
72
- expect(isStateDirty(c)).toBe(false);
73
- });
74
-
75
- it("setComponentProps replaces raw props contents", () => {
76
- const c = new ConcreteStateful({ x: 1 });
77
- setComponentProps(c, { y: 2 });
78
- const props = getComponentProps(c) as Record<string, unknown>;
79
- expect(props.x).toBeUndefined();
80
- expect(props.y).toBe(2);
81
- });
82
-
83
- it("updates anchor, mounted, defaults, and dirty state through helpers", () => {
84
- const c = new ConcreteStateful();
85
- const anchor = {} as Comment;
86
-
87
- setComponentAnchor(c, anchor);
88
- setComponentMounted(c, true);
89
- setComponentDefault(c, "label", "Save");
90
- setStateDirty(c, true);
91
-
92
- expect(getComponentAnchor(c)).toBe(anchor);
93
- expect(isComponentMounted(c)).toBe(true);
94
- expect(getComponentDefaults(c).label).toBe("Save");
95
- expect(isStateDirty(c)).toBe(true);
96
- });
97
-
98
- it("reads raw props and defaults through single-value helpers", () => {
99
- const c = new ConcreteStateful({ label: "From parent" });
100
- setComponentDefault(c, "label", "Default");
101
-
102
- expect(getComponentRawProp(c, "label")).toBe("From parent");
103
- expect(getComponentDefault(c, "label")).toBe("Default");
104
- });
105
-
106
- it("throws when component internals are read from a non-component", () => {
107
- expect(() => getComponentProps({})).toThrow(TypeError);
108
- expect(() => getComponentProps({})).toThrow(
109
- "Expected a PraxisJS component instance.",
110
- );
111
- });
112
-
113
- it("ignores dirty-state writes on non-component hosts", () => {
114
- const host = {};
115
-
116
- expect(() => markStateDirty(host)).not.toThrow();
117
- expect(() => setStateDirty(host, true)).not.toThrow();
118
- expect(isStateDirty(host)).toBe(false);
119
- });
120
- });
121
-
122
- class ConcreteStateless extends StatelessComponent<{ name: string }> {
123
- render() {
124
- return null;
125
- }
126
- }
127
-
128
- describe("StatelessComponent", () => {
129
- it("stores props in component internals", () => {
130
- const c = new ConcreteStateless({ name: "Alice" });
131
- expect((getComponentProps(c) as { name: string }).name).toBe("Alice");
132
- });
133
-
134
- it("setComponentProps replaces raw props contents", () => {
135
- const c = new ConcreteStateless({ name: "Alice" });
136
- setComponentProps(c, { name: "Bob" });
137
- expect((getComponentProps(c) as { name: string }).name).toBe("Bob");
138
- });
139
-
140
- it("setComponentProps removes old keys", () => {
141
- const c = new ConcreteStateless({ name: "Alice" });
142
- setComponentProps(c, {});
143
- expect((getComponentProps(c) as Record<string, unknown>).name).toBeUndefined();
144
- });
145
-
146
- it("accepts and exposes children via props.children", () => {
147
- const c = new ConcreteStateless({ name: "x", children: "hello" });
148
- expect(c.props.children).toBe("hello");
149
- });
150
-
151
- it("children is optional and defaults to undefined", () => {
152
- const c = new ConcreteStateless({ name: "x" });
153
- expect(c.props.children).toBeUndefined();
154
- });
155
-
156
- it("works with default generic (no explicit T)", () => {
157
- class Bare extends StatelessComponent {
158
- render() {
159
- return null;
160
- }
161
- }
162
- const c = new Bare({ children: "hi" });
163
- expect(c.props.children).toBe("hi");
164
- });
165
- });
@@ -1,333 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
-
3
- import { computed, writableComputed } from "../signal/computed";
4
- import { effect } from "../signal/effect";
5
- import { signal } from "../signal/signal";
6
-
7
- describe("computed", () => {
8
- it("derives value from signal", () => {
9
- const s = signal(4);
10
- const doubled = computed(() => s() * 2);
11
- expect(doubled()).toBe(8);
12
- });
13
-
14
- it("updates when source signal changes", () => {
15
- const s = signal(3);
16
- const triple = computed(() => s() * 3);
17
- s.set(5);
18
- expect(triple()).toBe(15);
19
- });
20
-
21
- it("is lazy — does not recompute until accessed", () => {
22
- const s = signal(1);
23
- const fn = vi.fn(() => s() + 1);
24
- const c = computed(fn);
25
- expect(fn).not.toHaveBeenCalled();
26
- c();
27
- expect(fn).toHaveBeenCalledOnce();
28
- });
29
-
30
- it("caches value between reads when source has not changed", () => {
31
- const s = signal(2);
32
- const fn = vi.fn(() => s() * 10);
33
- const c = computed(fn);
34
- c();
35
- c();
36
- expect(fn).toHaveBeenCalledOnce();
37
- });
38
-
39
- it("recomputes after source change", () => {
40
- const s = signal(1);
41
- const fn = vi.fn(() => s() + 100);
42
- const c = computed(fn);
43
- c();
44
- s.set(2);
45
- c();
46
- expect(fn).toHaveBeenCalledTimes(2);
47
- });
48
-
49
- it("marks __isComputed = true", () => {
50
- const c = computed(() => 42);
51
- expect(c.__isComputed).toBe(true);
52
- });
53
-
54
- it("chained computed values update correctly", () => {
55
- const s = signal(1);
56
- const doubled = computed(() => s() * 2);
57
- const quadrupled = computed(() => doubled() * 2);
58
- expect(quadrupled()).toBe(4);
59
- s.set(3);
60
- expect(quadrupled()).toBe(12);
61
- });
62
-
63
- it("coalesces subscriber notifications when multiple dependencies change in the same tick", async () => {
64
- const first = signal("John");
65
- const last = signal("Doe");
66
- const fullName = computed(() => `${first()} ${last()}`);
67
-
68
- const calls: string[] = [];
69
- fullName.subscribe((v) => calls.push(v));
70
- expect(calls).toEqual(["John Doe"]); // immediate on subscribe
71
-
72
- first.set("Jane");
73
- last.set("Smith");
74
- await Promise.resolve();
75
-
76
- // Both changes in the same tick → single notification with final value
77
- expect(calls).toEqual(["John Doe", "Jane Smith"]);
78
- });
79
-
80
- it("subscribe fires immediately with computed value", () => {
81
- const s = signal(5);
82
- const c = computed(() => s() + 1);
83
- const received: number[] = [];
84
- c.subscribe((v) => received.push(v));
85
- expect(received).toEqual([6]);
86
- });
87
-
88
- it("subscribe fires when source changes", async () => {
89
- const s = signal(0);
90
- const c = computed(() => s() * 3);
91
- const received: number[] = [];
92
- c.subscribe((v) => received.push(v));
93
- s.set(2);
94
- await Promise.resolve();
95
- s.set(4);
96
- await Promise.resolve();
97
- expect(received).toEqual([0, 6, 12]);
98
- });
99
-
100
- it("unsubscribe stops receiving updates", () => {
101
- const s = signal(0);
102
- const c = computed(() => s() + 1);
103
- const received: number[] = [];
104
- const unsub = c.subscribe((v) => received.push(v));
105
- unsub();
106
- s.set(10);
107
- expect(received).toEqual([1]);
108
- });
109
-
110
- it("computeFn that throws propagates the error on read", () => {
111
- const s = signal(true);
112
- const c = computed(() => {
113
- if (s()) throw new Error("compute error");
114
- return 0;
115
- });
116
- expect(() => c()).toThrow("compute error");
117
- });
118
-
119
- it("dynamic dependency: switches tracked signal based on condition", () => {
120
- const toggle = signal(true);
121
- const a = signal(10);
122
- const b = signal(20);
123
- const c = computed(() => (toggle() ? a() : b()));
124
-
125
- expect(c()).toBe(10);
126
- a.set(11);
127
- expect(c()).toBe(11);
128
-
129
- toggle.set(false);
130
- expect(c()).toBe(20);
131
- b.set(21);
132
- expect(c()).toBe(21);
133
- });
134
-
135
- it("dynamic dependency: stale branch changes do not dirty the computed", () => {
136
- const toggle = signal(true);
137
- const a = signal(10);
138
- const b = signal(20);
139
- const fn = vi.fn(() => (toggle() ? a() : b()));
140
- const c = computed(fn);
141
-
142
- expect(c()).toBe(10);
143
- toggle.set(false);
144
- expect(c()).toBe(20);
145
- expect(fn).toHaveBeenCalledTimes(2);
146
-
147
- a.set(11);
148
- expect(c()).toBe(20);
149
- expect(fn).toHaveBeenCalledTimes(2);
150
-
151
- b.set(21);
152
- expect(c()).toBe(21);
153
- expect(fn).toHaveBeenCalledTimes(3);
154
- });
155
-
156
- it("unsubscribe then re-subscribe works correctly", async () => {
157
- const s = signal(1);
158
- const c = computed(() => s() * 3);
159
- const received: number[] = [];
160
-
161
- const unsub = c.subscribe((v) => received.push(v));
162
- unsub();
163
-
164
- // Re-subscribe
165
- c.subscribe((v) => received.push(v));
166
- s.set(2);
167
- await Promise.resolve();
168
- expect(received).toContain(6);
169
- });
170
-
171
- it("dirty remains true after computeFn throws — next read recomputes instead of returning stale cache", () => {
172
- const s = signal(true);
173
- const fn = vi.fn(() => {
174
- if (s()) throw new Error("compute error");
175
- return 42;
176
- });
177
- const c = computed(fn);
178
- // First read — throws, dirty stays true
179
- expect(() => c()).toThrow("compute error");
180
- expect(fn).toHaveBeenCalledTimes(1);
181
- // Second read while still throwing — must recompute, not return stale
182
- expect(() => c()).toThrow("compute error");
183
- expect(fn).toHaveBeenCalledTimes(2);
184
- // Fix the source and read — should now succeed
185
- s.set(false);
186
- expect(c()).toBe(42);
187
- expect(fn).toHaveBeenCalledTimes(3);
188
- });
189
-
190
- it("circular dependency between two computed values throws or does not hang", () => {
191
- // This test documents behavior: circular deps must not cause infinite loops
192
- // We use a signal to break potential infinite recursion via dirty flag
193
- const s = signal(0);
194
- let aVal = 0;
195
- let bVal = 0;
196
- const a: ReturnType<typeof computed<number>> = computed(() => {
197
- s(); // track signal to make dirty
198
- return bVal + 1;
199
- });
200
- const b: ReturnType<typeof computed<number>> = computed(() => {
201
- s(); // track signal to make dirty
202
- return aVal + 1;
203
- });
204
- // Manually reading without cross-dependency reads to avoid true circularity
205
- aVal = a();
206
- bVal = b();
207
- expect(aVal).toBeGreaterThanOrEqual(1);
208
- expect(bVal).toBeGreaterThanOrEqual(1);
209
- });
210
-
211
- it("microtask guard: skips notifySubs when leaf subscriber unsubscribes before microtask fires", async () => {
212
- const s = signal(0);
213
- const c = computed(() => s() * 2);
214
- const results: number[] = [];
215
- const unsub = c.subscribe((v) => results.push(v));
216
- // results = [0]
217
-
218
- s.set(1); // markDirty: queues microtask (scheduled = true)
219
- unsub(); // removes leaf subscriber BEFORE microtask fires → leafHolder.subs = null
220
-
221
- await Promise.resolve(); // microtask: if (leafHolder.subs !== null) → FALSE → no notify
222
- expect(results).toEqual([0]); // no update received after unsubscribe
223
- });
224
-
225
- it("skips re-scheduling leaf notification when already scheduled (!scheduled false branch)", async () => {
226
- const s = signal(0);
227
- const c = computed(() => s() * 2);
228
- const results: number[] = [];
229
- c.subscribe((v) => results.push(v));
230
- // results = [0] after initial subscribe
231
-
232
- s.set(1); // markDirty: scheduled = true, microtask queued
233
- c(); // re-computes: dirty = false (scheduled still true)
234
- s.set(2); // markDirty: dirty=false → proceeds, but !scheduled is false → no second microtask
235
-
236
- await Promise.resolve(); // microtask runs, subscriber notified with current value
237
- expect(results).toContain(4); // 2 * 2
238
- });
239
-
240
- it("double unsubscribe on computed does not crash", () => {
241
- const s = signal(1);
242
- const c = computed(() => s() + 1);
243
- const unsub = c.subscribe(() => {});
244
- expect(() => {
245
- unsub();
246
- unsub();
247
- }).not.toThrow();
248
- });
249
- });
250
-
251
- describe("writableComputed", () => {
252
- it("read returns the getter value", () => {
253
- const s = signal(5);
254
- const wc = writableComputed(() => s() * 2, (_v) => { /* no-op */ });
255
- expect(wc()).toBe(10);
256
- });
257
-
258
- it("set calls the setter", () => {
259
- const s = signal(0);
260
- const wc = writableComputed(() => s(), (v) => { s.set(v); });
261
- wc.set(42);
262
- expect(s()).toBe(42);
263
- expect(wc()).toBe(42);
264
- });
265
-
266
- it("update applies the updater and calls the setter", () => {
267
- const s = signal(10);
268
- const wc = writableComputed(() => s(), (v) => { s.set(v); });
269
- wc.update((current) => current + 5);
270
- expect(s()).toBe(15);
271
- expect(wc()).toBe(15);
272
- });
273
-
274
- it("marks __isWritableComputed = true", () => {
275
- const wc = writableComputed(() => 1, (_v) => { /* no-op */ });
276
- expect(wc.__isWritableComputed).toBe(true);
277
- });
278
-
279
- it("inherits __isComputed = true from computed", () => {
280
- const wc = writableComputed(() => 1, (_v) => { /* no-op */ });
281
- expect(wc.__isComputed).toBe(true);
282
- });
283
-
284
- it("recomputes when underlying signal changes", () => {
285
- const s = signal(3);
286
- const wc = writableComputed(() => s() * 2, (_v) => { /* no-op */ });
287
- expect(wc()).toBe(6);
288
- s.set(7);
289
- expect(wc()).toBe(14);
290
- });
291
- });
292
-
293
- describe("computed.dispose", () => {
294
- type Disposable<T> = (() => T) & { dispose(): void };
295
-
296
- it("does nothing while an effect still depends on it", async () => {
297
- const s = signal(1);
298
- const c = computed(() => s() * 2) as unknown as Disposable<number>;
299
- let seen = 0;
300
- effect(() => {
301
- seen = c();
302
- });
303
- c.dispose();
304
- s.set(2);
305
- await Promise.resolve();
306
- expect(seen).toBe(4);
307
- });
308
-
309
- it("does nothing while another computed depends on it", () => {
310
- const s = signal(1);
311
- const inner = computed(() => s() * 2) as unknown as Disposable<number>;
312
- const outer = computed(() => inner() + 1);
313
- expect(outer()).toBe(3);
314
- inner.dispose();
315
- s.set(2);
316
- expect(outer()).toBe(5);
317
- });
318
-
319
- it("recomputes from scratch when read again after disposal", () => {
320
- const getter = vi.fn(() => 42);
321
- const c = computed(getter) as unknown as Disposable<number>;
322
- c();
323
- c.dispose();
324
- expect(c()).toBe(42);
325
- expect(getter).toHaveBeenCalledTimes(2);
326
- });
327
-
328
- it("is safe on a computed that was never read", () => {
329
- const c = computed(() => 1) as unknown as Disposable<number>;
330
- expect(() => { c.dispose(); }).not.toThrow();
331
- expect(c()).toBe(1);
332
- });
333
- });