@praxisjs/core 2.2.0 → 2.3.1
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 +24 -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/signal/batch.d.ts +1 -3
- package/dist/signal/batch.d.ts.map +1 -1
- package/dist/signal/batch.js +4 -30
- package/dist/signal/batch.js.map +1 -1
- package/dist/signal/computed.d.ts.map +1 -1
- package/dist/signal/computed.js +13 -66
- package/dist/signal/computed.js.map +1 -1
- package/dist/signal/effect.d.ts +6 -14
- package/dist/signal/effect.d.ts.map +1 -1
- package/dist/signal/effect.js +11 -83
- 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 +1 -0
- 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/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 -70
- package/dist/signal/signal.js.map +1 -1
- package/package.json +13 -4
- package/src/component/internals.ts +35 -10
- package/src/internal.ts +26 -0
- package/src/signal/batch.ts +5 -33
- package/src/signal/computed.ts +13 -66
- package/src/signal/effect.ts +12 -92
- package/src/signal/graph.ts +562 -0
- package/src/signal/index.ts +1 -0
- package/src/signal/peek.ts +2 -8
- package/src/signal/selector.ts +140 -0
- package/src/signal/signal.ts +11 -74
- 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 -124
- 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 -289
- 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 -200
- 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 -252
- 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 -299
- 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 -254
- 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 -138
- package/src/__tests__/component.test.ts +0 -165
- package/src/__tests__/computed.test.ts +0 -333
- package/src/__tests__/effect.test.ts +0 -241
- package/src/__tests__/peek.test.ts +0 -31
- package/src/__tests__/persisted-signal.test.ts +0 -305
- package/src/__tests__/reactive.test.ts +0 -343
- 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 -291
- package/src/__tests__/synced.test.ts +0 -173
- package/src/__tests__/track.test.ts +0 -61
- 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
|
-
});
|