@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.
- package/CHANGELOG.md +18 -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,291 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { batch } from "../signal/batch";
|
|
4
|
-
import { SUBS_SET_THRESHOLD } from "../signal/effect";
|
|
5
|
-
import { effect } from "../signal/effect";
|
|
6
|
-
import { signal, addSub, removeSub, type SubList } from "../signal/signal";
|
|
7
|
-
import type { Effect } from "../signal/effect";
|
|
8
|
-
|
|
9
|
-
describe("signal", () => {
|
|
10
|
-
it("returns initial value", () => {
|
|
11
|
-
const s = signal(42);
|
|
12
|
-
expect(s()).toBe(42);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("updates value with set", () => {
|
|
16
|
-
const s = signal(0);
|
|
17
|
-
s.set(10);
|
|
18
|
-
expect(s()).toBe(10);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("updates value with update fn", () => {
|
|
22
|
-
const s = signal(5);
|
|
23
|
-
s.update((v) => v * 2);
|
|
24
|
-
expect(s()).toBe(10);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("skips update when value is identical", () => {
|
|
28
|
-
const s = signal(1);
|
|
29
|
-
const calls: number[] = [];
|
|
30
|
-
s.subscribe((v) => calls.push(v));
|
|
31
|
-
const before = calls.length;
|
|
32
|
-
s.set(1); // same value
|
|
33
|
-
expect(calls.length).toBe(before);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("subscribe fires immediately with current value", () => {
|
|
37
|
-
const s = signal("hello");
|
|
38
|
-
const received: string[] = [];
|
|
39
|
-
s.subscribe((v) => received.push(v));
|
|
40
|
-
expect(received).toEqual(["hello"]);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("subscribe fires on subsequent updates", () => {
|
|
44
|
-
const s = signal(0);
|
|
45
|
-
const received: number[] = [];
|
|
46
|
-
s.subscribe((v) => received.push(v));
|
|
47
|
-
s.set(1);
|
|
48
|
-
s.set(2);
|
|
49
|
-
expect(received).toEqual([0, 1, 2]);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("unsubscribe stops receiving updates", () => {
|
|
53
|
-
const s = signal(0);
|
|
54
|
-
const received: number[] = [];
|
|
55
|
-
const unsub = s.subscribe((v) => received.push(v));
|
|
56
|
-
unsub();
|
|
57
|
-
s.set(99);
|
|
58
|
-
expect(received).toEqual([0]);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("marks __isSignal = true", () => {
|
|
62
|
-
const s = signal(0);
|
|
63
|
-
expect(s.__isSignal).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("supports object values", () => {
|
|
67
|
-
const s = signal<{ name: string }>({ name: "Alice" });
|
|
68
|
-
s.set({ name: "Bob" });
|
|
69
|
-
expect(s().name).toBe("Bob");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("supports null and undefined", () => {
|
|
73
|
-
const s = signal<null | number>(null);
|
|
74
|
-
expect(s()).toBeNull();
|
|
75
|
-
s.set(5);
|
|
76
|
-
expect(s()).toBe(5);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("NaN is considered equal to NaN — no notification", () => {
|
|
80
|
-
const s = signal(NaN);
|
|
81
|
-
const calls: unknown[] = [];
|
|
82
|
-
s.subscribe((v) => calls.push(v));
|
|
83
|
-
const before = calls.length;
|
|
84
|
-
s.set(NaN);
|
|
85
|
-
expect(calls.length).toBe(before);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("-0 and +0 are considered different — notification fires", () => {
|
|
89
|
-
const s = signal<number>(-0);
|
|
90
|
-
const calls: number[] = [];
|
|
91
|
-
s.subscribe((v) => calls.push(v));
|
|
92
|
-
const before = calls.length;
|
|
93
|
-
s.set(+0);
|
|
94
|
-
expect(calls.length).toBe(before + 1);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("update() fn that throws does not update the value", () => {
|
|
98
|
-
const s = signal(5);
|
|
99
|
-
expect(() => s.update(() => { throw new Error("fn error"); })).toThrow("fn error");
|
|
100
|
-
expect(s()).toBe(5);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("multiple subscribers all receive the update", () => {
|
|
104
|
-
const s = signal(0);
|
|
105
|
-
const a: number[] = [];
|
|
106
|
-
const b: number[] = [];
|
|
107
|
-
s.subscribe((v) => a.push(v));
|
|
108
|
-
s.subscribe((v) => b.push(v));
|
|
109
|
-
s.set(7);
|
|
110
|
-
expect(a).toContain(7);
|
|
111
|
-
expect(b).toContain(7);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("subscriber B still fires when subscriber A throws", () => {
|
|
115
|
-
const s = signal(0);
|
|
116
|
-
const received: number[] = [];
|
|
117
|
-
s.subscribe((v) => { if (v !== 0) throw new Error("sub A throws"); });
|
|
118
|
-
s.subscribe((v) => received.push(v));
|
|
119
|
-
expect(() => s.set(1)).toThrow("sub A throws");
|
|
120
|
-
expect(received).toContain(1);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("unsubscribe during notification does not crash", () => {
|
|
124
|
-
const s = signal(0);
|
|
125
|
-
let unsub: (() => void) | undefined;
|
|
126
|
-
unsub = s.subscribe(() => {
|
|
127
|
-
unsub?.();
|
|
128
|
-
});
|
|
129
|
-
expect(() => s.set(1)).not.toThrow();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("unsubscribing one of multiple subscribers removes only that subscriber", () => {
|
|
133
|
-
const s = signal(0);
|
|
134
|
-
const a: number[] = [];
|
|
135
|
-
const b: number[] = [];
|
|
136
|
-
const unsubA = s.subscribe((v) => a.push(v));
|
|
137
|
-
s.subscribe((v) => b.push(v));
|
|
138
|
-
// subs is now an array — hits removeSub else branch
|
|
139
|
-
unsubA();
|
|
140
|
-
s.set(1);
|
|
141
|
-
expect(a).toEqual([0]); // A stopped after initial fire
|
|
142
|
-
expect(b).toEqual([0, 1]); // B still receives
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("unsubscribing a non-existent subscriber from array subs is a no-op", () => {
|
|
146
|
-
const s = signal(0);
|
|
147
|
-
const a: number[] = [];
|
|
148
|
-
const b: number[] = [];
|
|
149
|
-
s.subscribe((v) => a.push(v));
|
|
150
|
-
const unsubB = s.subscribe((v) => b.push(v));
|
|
151
|
-
unsubB(); // remove B, subs becomes array without B
|
|
152
|
-
unsubB(); // call again — idx will be -1, splice not called
|
|
153
|
-
expect(() => s.set(1)).not.toThrow();
|
|
154
|
-
expect(a).toContain(1);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("batch defers notifications for multiple array subscribers", () => {
|
|
158
|
-
const s = signal(0);
|
|
159
|
-
const a: number[] = [];
|
|
160
|
-
const b: number[] = [];
|
|
161
|
-
s.subscribe((v) => a.push(v));
|
|
162
|
-
s.subscribe((v) => b.push(v));
|
|
163
|
-
// subs is an array; inside batch, notifySubs hits the batching=true array path
|
|
164
|
-
batch(() => {
|
|
165
|
-
s.set(1);
|
|
166
|
-
expect(a).toEqual([0]); // not yet fired
|
|
167
|
-
expect(b).toEqual([0]);
|
|
168
|
-
});
|
|
169
|
-
expect(a).toContain(1);
|
|
170
|
-
expect(b).toContain(1);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("removeSub with non-matching single-function subscriber is a no-op", () => {
|
|
174
|
-
// When subs is a single function and we try to remove a different function,
|
|
175
|
-
// the holder must remain unchanged (subs !== eff → false branch).
|
|
176
|
-
const holder: { subs: SubList } = { subs: null };
|
|
177
|
-
const fn1: Effect = () => {};
|
|
178
|
-
const fn2: Effect = () => {};
|
|
179
|
-
addSub(holder, fn1); // subs = fn1 (single function)
|
|
180
|
-
removeSub(holder, fn2); // fn2 !== fn1 → no-op
|
|
181
|
-
expect(holder.subs).toBe(fn1);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("addSub does not add a duplicate subscriber to an array holder", () => {
|
|
185
|
-
const fn1: Effect = () => {};
|
|
186
|
-
const fn2: Effect = () => {};
|
|
187
|
-
const holder: { subs: SubList } = { subs: [fn1, fn2] };
|
|
188
|
-
|
|
189
|
-
addSub(holder, fn2);
|
|
190
|
-
|
|
191
|
-
expect(holder.subs).toEqual([fn1, fn2]);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("removeSub normalizes an emptied array holder back to null", () => {
|
|
195
|
-
const fn: Effect = () => {};
|
|
196
|
-
const holder: { subs: SubList } = { subs: [fn] };
|
|
197
|
-
|
|
198
|
-
removeSub(holder, fn);
|
|
199
|
-
|
|
200
|
-
expect(holder.subs).toBeNull();
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("set() with mutated object reference (same ref) does NOT notify — Object.is semantics", () => {
|
|
204
|
-
const obj = { count: 0 };
|
|
205
|
-
const s = signal(obj);
|
|
206
|
-
const calls: unknown[] = [];
|
|
207
|
-
s.subscribe((v) => calls.push(v));
|
|
208
|
-
const before = calls.length;
|
|
209
|
-
// Mutate the object in-place and set the same reference
|
|
210
|
-
obj.count = 99;
|
|
211
|
-
s.set(obj);
|
|
212
|
-
// Object.is(obj, obj) === true, so no notification
|
|
213
|
-
expect(calls.length).toBe(before);
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
describe("large subscriber lists", () => {
|
|
218
|
-
function fns(count: number): Array<() => void> {
|
|
219
|
-
return Array.from({ length: count }, () => () => {});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
it("keeps an array up to the threshold and promotes to a Set past it, preserving order", () => {
|
|
223
|
-
const holder: { subs: SubList } = { subs: null };
|
|
224
|
-
const list = fns(SUBS_SET_THRESHOLD);
|
|
225
|
-
for (const fn of list) addSub(holder, fn);
|
|
226
|
-
expect(Array.isArray(holder.subs)).toBe(true);
|
|
227
|
-
|
|
228
|
-
const extra = () => {};
|
|
229
|
-
addSub(holder, extra);
|
|
230
|
-
expect(holder.subs).toBeInstanceOf(Set);
|
|
231
|
-
expect([...(holder.subs as Set<() => void>)]).toEqual([...list, extra]);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("addSub on a Set holder adds new subscribers and ignores duplicates", () => {
|
|
235
|
-
const list = fns(SUBS_SET_THRESHOLD + 1);
|
|
236
|
-
const holder: { subs: SubList } = { subs: new Set(list) };
|
|
237
|
-
addSub(holder, list[0]);
|
|
238
|
-
expect((holder.subs as Set<() => void>).size).toBe(list.length);
|
|
239
|
-
const extra = () => {};
|
|
240
|
-
addSub(holder, extra);
|
|
241
|
-
expect((holder.subs as Set<() => void>).size).toBe(list.length + 1);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it("removeSub on a Set holder removes one subscriber, ignores unknown ones, and normalizes to null when emptied", () => {
|
|
245
|
-
const [a, b, stranger] = fns(3);
|
|
246
|
-
const holder: { subs: SubList } = { subs: new Set([a, b]) };
|
|
247
|
-
removeSub(holder, stranger);
|
|
248
|
-
expect((holder.subs as Set<() => void>).size).toBe(2);
|
|
249
|
-
removeSub(holder, a);
|
|
250
|
-
expect([...(holder.subs as Set<() => void>)]).toEqual([b]);
|
|
251
|
-
removeSub(holder, b);
|
|
252
|
-
expect(holder.subs).toBeNull();
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("many effects on one signal all re-run once per change, in subscription order", () => {
|
|
256
|
-
const count = SUBS_SET_THRESHOLD * 3;
|
|
257
|
-
const s = signal(0);
|
|
258
|
-
const order: number[] = [];
|
|
259
|
-
const stops = Array.from({ length: count }, (_, i) =>
|
|
260
|
-
effect(() => {
|
|
261
|
-
void s();
|
|
262
|
-
order.push(i);
|
|
263
|
-
}),
|
|
264
|
-
);
|
|
265
|
-
order.length = 0;
|
|
266
|
-
s.set(1);
|
|
267
|
-
expect(order).toEqual(Array.from({ length: count }, (_, i) => i));
|
|
268
|
-
order.length = 0;
|
|
269
|
-
s.set(2);
|
|
270
|
-
expect(order).toEqual(Array.from({ length: count }, (_, i) => i));
|
|
271
|
-
stops.forEach((stop) => { stop(); });
|
|
272
|
-
order.length = 0;
|
|
273
|
-
s.set(3);
|
|
274
|
-
expect(order).toEqual([]);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it("an effect stopped while the signal holds a Set no longer runs, the others still do", () => {
|
|
278
|
-
const s = signal(0);
|
|
279
|
-
const runs = new Map<number, number>();
|
|
280
|
-
const stops = Array.from({ length: SUBS_SET_THRESHOLD + 4 }, (_, i) =>
|
|
281
|
-
effect(() => {
|
|
282
|
-
void s();
|
|
283
|
-
runs.set(i, (runs.get(i) ?? 0) + 1);
|
|
284
|
-
}),
|
|
285
|
-
);
|
|
286
|
-
stops[2]();
|
|
287
|
-
s.set(1);
|
|
288
|
-
expect(runs.get(2)).toBe(1);
|
|
289
|
-
expect(runs.get(3)).toBe(2);
|
|
290
|
-
});
|
|
291
|
-
});
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { syncedSignal } from "../signal/synced";
|
|
4
|
-
import { effect } from "../signal/effect";
|
|
5
|
-
|
|
6
|
-
// BroadcastChannel mock
|
|
7
|
-
class MockBroadcastChannel {
|
|
8
|
-
static instances: MockBroadcastChannel[] = [];
|
|
9
|
-
static byChannel: Map<string, MockBroadcastChannel[]> = new Map();
|
|
10
|
-
|
|
11
|
-
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
12
|
-
readonly name: string;
|
|
13
|
-
closed = false;
|
|
14
|
-
|
|
15
|
-
constructor(name: string) {
|
|
16
|
-
this.name = name;
|
|
17
|
-
MockBroadcastChannel.instances.push(this);
|
|
18
|
-
if (!MockBroadcastChannel.byChannel.has(name)) {
|
|
19
|
-
MockBroadcastChannel.byChannel.set(name, []);
|
|
20
|
-
}
|
|
21
|
-
MockBroadcastChannel.byChannel.get(name)!.push(this);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
postMessage(data: unknown) {
|
|
25
|
-
// Deliver to all OTHER instances on same channel
|
|
26
|
-
MockBroadcastChannel.byChannel.get(this.name)?.forEach((ch) => {
|
|
27
|
-
if (ch !== this && ch.onmessage) {
|
|
28
|
-
ch.onmessage(new MessageEvent("message", { data }));
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
close() {
|
|
34
|
-
this.closed = true;
|
|
35
|
-
const list = MockBroadcastChannel.byChannel.get(this.name);
|
|
36
|
-
if (list) {
|
|
37
|
-
const idx = list.indexOf(this);
|
|
38
|
-
if (idx !== -1) list.splice(idx, 1);
|
|
39
|
-
}
|
|
40
|
-
MockBroadcastChannel.instances.splice(
|
|
41
|
-
MockBroadcastChannel.instances.indexOf(this),
|
|
42
|
-
1,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
beforeEach(() => {
|
|
48
|
-
MockBroadcastChannel.instances = [];
|
|
49
|
-
MockBroadcastChannel.byChannel = new Map();
|
|
50
|
-
vi.stubGlobal("BroadcastChannel", MockBroadcastChannel);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe("syncedSignal", () => {
|
|
54
|
-
it("reads initial value", () => {
|
|
55
|
-
const s = syncedSignal("ch", 42);
|
|
56
|
-
expect(s()).toBe(42);
|
|
57
|
-
s.close();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("set() updates the signal value", () => {
|
|
61
|
-
const s = syncedSignal("ch", 0);
|
|
62
|
-
s.set(10);
|
|
63
|
-
expect(s()).toBe(10);
|
|
64
|
-
s.close();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("update() applies a transform", () => {
|
|
68
|
-
const s = syncedSignal("ch", 5);
|
|
69
|
-
s.update((v) => v * 2);
|
|
70
|
-
expect(s()).toBe(10);
|
|
71
|
-
s.close();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("set() posts a JSON message to the channel", () => {
|
|
75
|
-
const s = syncedSignal("ch", 0);
|
|
76
|
-
const postSpy = vi.spyOn(
|
|
77
|
-
MockBroadcastChannel.instances[0],
|
|
78
|
-
"postMessage",
|
|
79
|
-
);
|
|
80
|
-
s.set(99);
|
|
81
|
-
expect(postSpy).toHaveBeenCalledWith(JSON.stringify(99));
|
|
82
|
-
s.close();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("receiving a message from another tab updates the signal", () => {
|
|
86
|
-
const s = syncedSignal("ch", 0);
|
|
87
|
-
const channel = MockBroadcastChannel.instances[0];
|
|
88
|
-
|
|
89
|
-
channel.onmessage!(new MessageEvent("message", { data: JSON.stringify(7) }));
|
|
90
|
-
expect(s()).toBe(7);
|
|
91
|
-
s.close();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("does not echo own set() back to itself", () => {
|
|
95
|
-
const s = syncedSignal("ch", 0);
|
|
96
|
-
|
|
97
|
-
// Simulate two instances on the same channel
|
|
98
|
-
const s2 = syncedSignal("ch", 0);
|
|
99
|
-
const received: number[] = [];
|
|
100
|
-
effect(() => { received.push(s2()); });
|
|
101
|
-
|
|
102
|
-
s.set(5);
|
|
103
|
-
// s2 should receive the message because it came from a different instance
|
|
104
|
-
expect(s2()).toBe(5);
|
|
105
|
-
|
|
106
|
-
// s itself should NOT trigger its own onmessage handler (no echo)
|
|
107
|
-
const channel = MockBroadcastChannel.instances[0];
|
|
108
|
-
const receivedBySelf: number[] = [];
|
|
109
|
-
effect(() => { receivedBySelf.push(s()); });
|
|
110
|
-
const beforeCount = receivedBySelf.length;
|
|
111
|
-
// Manually simulate echo — should be ignored because posting=false after set
|
|
112
|
-
channel.onmessage!(new MessageEvent("message", { data: JSON.stringify(5) }));
|
|
113
|
-
// The value stays 5 (no spurious extra notification tested via subscribe count)
|
|
114
|
-
expect(s()).toBe(5);
|
|
115
|
-
expect(receivedBySelf.length).toBe(beforeCount); // no extra effect run (Object.is equality)
|
|
116
|
-
|
|
117
|
-
s.close();
|
|
118
|
-
s2.close();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("subscribe fires immediately then on each remote change", () => {
|
|
122
|
-
const s = syncedSignal("ch", 0);
|
|
123
|
-
const channel = MockBroadcastChannel.instances[0];
|
|
124
|
-
const received: number[] = [];
|
|
125
|
-
s.subscribe((v) => received.push(v));
|
|
126
|
-
|
|
127
|
-
expect(received).toEqual([0]);
|
|
128
|
-
|
|
129
|
-
channel.onmessage!(new MessageEvent("message", { data: JSON.stringify(3) }));
|
|
130
|
-
expect(received).toEqual([0, 3]);
|
|
131
|
-
s.close();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("close() shuts down the BroadcastChannel", () => {
|
|
135
|
-
const s = syncedSignal("ch", 0);
|
|
136
|
-
const channel = MockBroadcastChannel.instances[0];
|
|
137
|
-
expect(channel.closed).toBe(false);
|
|
138
|
-
s.close();
|
|
139
|
-
expect(channel.closed).toBe(true);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("has __isSignal marker", () => {
|
|
143
|
-
const s = syncedSignal("ch", 0);
|
|
144
|
-
expect(s.__isSignal).toBe(true);
|
|
145
|
-
s.close();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("does not update value when onmessage fires while posting (posting guard)", () => {
|
|
149
|
-
// Use a self-delivering channel that echoes to itself during postMessage.
|
|
150
|
-
// This covers the `if (posting) return;` branch in synced.ts.
|
|
151
|
-
class SelfEchoChannel {
|
|
152
|
-
onmessage: ((e: MessageEvent) => void) | null = null;
|
|
153
|
-
postMessage(data: unknown) {
|
|
154
|
-
this.onmessage?.(new MessageEvent("message", { data }));
|
|
155
|
-
}
|
|
156
|
-
close() {}
|
|
157
|
-
}
|
|
158
|
-
vi.stubGlobal("BroadcastChannel", SelfEchoChannel);
|
|
159
|
-
|
|
160
|
-
const s = syncedSignal("echo-ch", 0);
|
|
161
|
-
const received: number[] = [];
|
|
162
|
-
s.subscribe((v) => received.push(v));
|
|
163
|
-
|
|
164
|
-
// set() triggers postMessage → onmessage fires → posting=true → early return
|
|
165
|
-
// Inner value should not be double-set
|
|
166
|
-
s.set(42);
|
|
167
|
-
expect(s()).toBe(42);
|
|
168
|
-
// Only two notifications: initial (0) + the set (42), no echo re-set
|
|
169
|
-
expect(received).toEqual([0, 42]);
|
|
170
|
-
|
|
171
|
-
s.close();
|
|
172
|
-
});
|
|
173
|
-
});
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { track, runEffect, activeEffect } from "../signal/effect";
|
|
4
|
-
import { signal } from "../signal/signal";
|
|
5
|
-
|
|
6
|
-
describe("track", () => {
|
|
7
|
-
it("runs the effect immediately", () => {
|
|
8
|
-
const fn = vi.fn();
|
|
9
|
-
track(fn);
|
|
10
|
-
expect(fn).toHaveBeenCalledOnce();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("sets activeEffect during execution, resets after", () => {
|
|
14
|
-
let during: unknown = "not-set";
|
|
15
|
-
track(() => {
|
|
16
|
-
during = activeEffect;
|
|
17
|
-
});
|
|
18
|
-
expect(during).toBeTypeOf("function");
|
|
19
|
-
expect(activeEffect).toBeNull();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("supports nested track calls — restores outer effect", () => {
|
|
23
|
-
const outer = vi.fn();
|
|
24
|
-
const inner = vi.fn();
|
|
25
|
-
let outerActive: unknown;
|
|
26
|
-
let innerActive: unknown;
|
|
27
|
-
|
|
28
|
-
track(() => {
|
|
29
|
-
outer();
|
|
30
|
-
outerActive = activeEffect;
|
|
31
|
-
track(() => {
|
|
32
|
-
inner();
|
|
33
|
-
innerActive = activeEffect;
|
|
34
|
-
});
|
|
35
|
-
// after inner track, activeEffect should be back to outer
|
|
36
|
-
expect(activeEffect).toBe(outerActive);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
expect(outer).toHaveBeenCalledOnce();
|
|
40
|
-
expect(inner).toHaveBeenCalledOnce();
|
|
41
|
-
expect(innerActive).not.toBe(outerActive);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("subscribes the tracked effect to signals read inside it", () => {
|
|
45
|
-
const s = signal(1);
|
|
46
|
-
const values: number[] = [];
|
|
47
|
-
track(() => values.push(s()));
|
|
48
|
-
s.set(2);
|
|
49
|
-
expect(values).toEqual([1, 2]);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe("runEffect", () => {
|
|
54
|
-
it("sets activeEffect to the given value", () => {
|
|
55
|
-
const fn = () => {};
|
|
56
|
-
runEffect(fn);
|
|
57
|
-
expect(activeEffect).toBe(fn);
|
|
58
|
-
runEffect(null);
|
|
59
|
-
expect(activeEffect).toBeNull();
|
|
60
|
-
});
|
|
61
|
-
});
|