@esportsplus/reactivity 0.30.2 → 0.30.3
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/build/reactive/array.js +24 -22
- package/build/reactive/object.js +3 -2
- package/build/system.js +4 -3
- package/package.json +3 -2
- package/src/reactive/array.ts +27 -26
- package/src/reactive/object.ts +5 -2
- package/src/system.ts +5 -3
- package/tests/array.ts +657 -0
- package/tests/bench/array.ts +162 -0
- package/tests/bench/system.ts +230 -0
- package/tests/effects.ts +211 -0
- package/tests/nested.ts +293 -0
- package/tests/objects.ts +185 -0
- package/tests/primitives.ts +280 -0
- package/tests/reactive.ts +324 -0
- package/tests/system.ts +655 -0
- package/vitest.config.ts +18 -0
- package/test/arrays.ts +0 -146
- package/test/debug.ts +0 -7
- package/test/effects.ts +0 -168
- package/test/index.ts +0 -8
- package/test/nested.ts +0 -201
- package/test/objects.ts +0 -106
- package/test/primitives.ts +0 -87
- package/test/range.ts +0 -45
- package/test/vite.config.ts +0 -41
package/tests/nested.ts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { computed, effect, read, signal, write } from '~/system';
|
|
3
|
+
import { ReactiveArray } from '~/reactive/array';
|
|
4
|
+
import { ReactiveObject } from '~/reactive/object';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// These tests validate nested/cross-object reactive patterns from the compiler
|
|
8
|
+
// integration tests. Self-referential computed properties require the compiler;
|
|
9
|
+
// here we use external signals or direct property access.
|
|
10
|
+
|
|
11
|
+
describe('nested reactive patterns', () => {
|
|
12
|
+
describe('cross-object computed', () => {
|
|
13
|
+
it('object reads properties from other objects', async () => {
|
|
14
|
+
let config = new ReactiveObject({ debug: true, theme: 'dark' }) as any,
|
|
15
|
+
settings = new ReactiveObject({ notifications: true, volume: 80 }) as any,
|
|
16
|
+
themeValues: string[] = [],
|
|
17
|
+
volumeValues: number[] = [];
|
|
18
|
+
|
|
19
|
+
effect(() => { themeValues.push(config.theme); });
|
|
20
|
+
effect(() => { volumeValues.push(settings.volume); });
|
|
21
|
+
|
|
22
|
+
expect(themeValues).toEqual(['dark']);
|
|
23
|
+
expect(volumeValues).toEqual([80]);
|
|
24
|
+
|
|
25
|
+
config.theme = 'light';
|
|
26
|
+
await Promise.resolve();
|
|
27
|
+
|
|
28
|
+
expect(themeValues).toEqual(['dark', 'light']);
|
|
29
|
+
|
|
30
|
+
settings.volume = 50;
|
|
31
|
+
await Promise.resolve();
|
|
32
|
+
|
|
33
|
+
expect(volumeValues).toEqual([80, 50]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('computed depends on properties from multiple objects', async () => {
|
|
37
|
+
let obj1 = new ReactiveObject({ value: 10 }) as any,
|
|
38
|
+
obj2 = new ReactiveObject({ value: 20 }) as any,
|
|
39
|
+
combined = computed(() => obj1.value + obj2.value),
|
|
40
|
+
values: number[] = [];
|
|
41
|
+
|
|
42
|
+
effect(() => {
|
|
43
|
+
values.push(read(combined));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(values).toEqual([30]);
|
|
47
|
+
|
|
48
|
+
obj1.value = 100;
|
|
49
|
+
await Promise.resolve();
|
|
50
|
+
|
|
51
|
+
expect(values).toEqual([30, 120]);
|
|
52
|
+
|
|
53
|
+
obj2.value = 200;
|
|
54
|
+
await Promise.resolve();
|
|
55
|
+
|
|
56
|
+
expect(values).toEqual([30, 120, 300]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
describe('array of reactive objects', () => {
|
|
62
|
+
it('todo list pattern with filter', async () => {
|
|
63
|
+
let todo1 = new ReactiveObject({ done: false, text: 'Learn reactivity' }) as any,
|
|
64
|
+
todo2 = new ReactiveObject({ done: true, text: 'Build app' }) as any,
|
|
65
|
+
todo3 = new ReactiveObject({ done: false, text: 'Test everything' }) as any,
|
|
66
|
+
todos = new ReactiveArray(todo1, todo2, todo3),
|
|
67
|
+
completedCounts: number[] = [];
|
|
68
|
+
|
|
69
|
+
effect(() => {
|
|
70
|
+
let count = 0;
|
|
71
|
+
|
|
72
|
+
for (let i = 0, n = todos.length; i < n; i++) {
|
|
73
|
+
if (todos[i].done) {
|
|
74
|
+
count++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
completedCounts.push(count);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(completedCounts).toEqual([1]);
|
|
82
|
+
|
|
83
|
+
todo1.done = true;
|
|
84
|
+
await Promise.resolve();
|
|
85
|
+
|
|
86
|
+
expect(completedCounts).toEqual([1, 2]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('push new reactive object to array', () => {
|
|
90
|
+
let todo1 = new ReactiveObject({ done: false, text: 'First' }) as any,
|
|
91
|
+
todos = new ReactiveArray(todo1);
|
|
92
|
+
|
|
93
|
+
expect(todos.length).toBe(1);
|
|
94
|
+
|
|
95
|
+
let todo2 = new ReactiveObject({ done: false, text: 'Second' }) as any;
|
|
96
|
+
|
|
97
|
+
todos.push(todo2);
|
|
98
|
+
|
|
99
|
+
expect(todos.length).toBe(2);
|
|
100
|
+
expect(todos[1].text).toBe('Second');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
describe('matrix pattern', () => {
|
|
106
|
+
it('array of arrays', () => {
|
|
107
|
+
let row1 = new ReactiveArray(1, 2, 3),
|
|
108
|
+
row2 = new ReactiveArray(4, 5, 6),
|
|
109
|
+
row3 = new ReactiveArray(7, 8, 9),
|
|
110
|
+
matrix = new ReactiveArray(row1, row2, row3);
|
|
111
|
+
|
|
112
|
+
expect(matrix.length).toBe(3);
|
|
113
|
+
expect([...matrix[0]]).toEqual([1, 2, 3]);
|
|
114
|
+
expect([...matrix[1]]).toEqual([4, 5, 6]);
|
|
115
|
+
expect([...matrix[2]]).toEqual([7, 8, 9]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('modifying inner array', () => {
|
|
119
|
+
let row1 = new ReactiveArray(1, 2, 3),
|
|
120
|
+
row2 = new ReactiveArray(4, 5, 6),
|
|
121
|
+
matrix = new ReactiveArray(row1, row2);
|
|
122
|
+
|
|
123
|
+
row1.$set(0, 100);
|
|
124
|
+
row2.push(60);
|
|
125
|
+
|
|
126
|
+
expect(matrix[0][0]).toBe(100);
|
|
127
|
+
expect([...matrix[1]]).toEqual([4, 5, 6, 60]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('push new row to matrix', () => {
|
|
131
|
+
let row1 = new ReactiveArray(1, 2, 3),
|
|
132
|
+
matrix = new ReactiveArray(row1);
|
|
133
|
+
|
|
134
|
+
let row2 = new ReactiveArray(4, 5, 6);
|
|
135
|
+
|
|
136
|
+
matrix.push(row2);
|
|
137
|
+
|
|
138
|
+
expect(matrix.length).toBe(2);
|
|
139
|
+
expect([...matrix[1]]).toEqual([4, 5, 6]);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
describe('cross-object array + multiplier', () => {
|
|
145
|
+
it('computed sum reacts to multiplier changes', async () => {
|
|
146
|
+
let items = new ReactiveArray(10, 20, 30),
|
|
147
|
+
multiplier = signal(2),
|
|
148
|
+
sum = computed(() => {
|
|
149
|
+
let total = 0;
|
|
150
|
+
|
|
151
|
+
for (let i = 0, n = items.length; i < n; i++) {
|
|
152
|
+
total += items[i];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return total;
|
|
156
|
+
}),
|
|
157
|
+
total = computed(() => read(sum) * read(multiplier)),
|
|
158
|
+
values: number[] = [];
|
|
159
|
+
|
|
160
|
+
effect(() => {
|
|
161
|
+
values.push(read(total));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(read(sum)).toBe(60);
|
|
165
|
+
expect(values).toEqual([120]);
|
|
166
|
+
|
|
167
|
+
write(multiplier, 3);
|
|
168
|
+
await Promise.resolve();
|
|
169
|
+
|
|
170
|
+
expect(values).toEqual([120, 180]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('computed sum reacts to reactive length changes', async () => {
|
|
174
|
+
let items = new ReactiveArray(10, 20, 30),
|
|
175
|
+
multiplier = signal(2),
|
|
176
|
+
sum = computed(() => {
|
|
177
|
+
let length = items.$length,
|
|
178
|
+
total = 0;
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < length; i++) {
|
|
181
|
+
total += items[i];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return total;
|
|
185
|
+
}),
|
|
186
|
+
total = computed(() => read(sum) * read(multiplier)),
|
|
187
|
+
values: number[] = [];
|
|
188
|
+
|
|
189
|
+
effect(() => {
|
|
190
|
+
values.push(read(total));
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(values).toEqual([120]);
|
|
194
|
+
|
|
195
|
+
items.push(40);
|
|
196
|
+
await Promise.resolve();
|
|
197
|
+
|
|
198
|
+
expect(read(sum)).toBe(100);
|
|
199
|
+
expect(values).toEqual([120, 200]);
|
|
200
|
+
|
|
201
|
+
write(multiplier, 3);
|
|
202
|
+
await Promise.resolve();
|
|
203
|
+
|
|
204
|
+
expect(values).toEqual([120, 200, 300]);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
describe('effects tracking multiple objects', () => {
|
|
210
|
+
it('effect reacts to changes in any tracked object', async () => {
|
|
211
|
+
let obj1 = new ReactiveObject({ value: 1 }) as any,
|
|
212
|
+
obj2 = new ReactiveObject({ value: 2 }) as any,
|
|
213
|
+
combined = computed(() => obj1.value + obj2.value),
|
|
214
|
+
runs = 0,
|
|
215
|
+
results: number[] = [];
|
|
216
|
+
|
|
217
|
+
effect(() => {
|
|
218
|
+
runs++;
|
|
219
|
+
results.push(read(combined));
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(results).toEqual([3]);
|
|
223
|
+
|
|
224
|
+
obj1.value = 10;
|
|
225
|
+
await Promise.resolve();
|
|
226
|
+
|
|
227
|
+
expect(results).toEqual([3, 12]);
|
|
228
|
+
|
|
229
|
+
obj2.value = 20;
|
|
230
|
+
await Promise.resolve();
|
|
231
|
+
|
|
232
|
+
expect(results).toEqual([3, 12, 30]);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
describe('primitive derived from object property', () => {
|
|
238
|
+
it('computed reads object property', async () => {
|
|
239
|
+
let obj = new ReactiveObject({ base: 10 }) as any,
|
|
240
|
+
derived = computed(() => obj.base * 2),
|
|
241
|
+
values: number[] = [];
|
|
242
|
+
|
|
243
|
+
effect(() => {
|
|
244
|
+
values.push(read(derived));
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(values).toEqual([20]);
|
|
248
|
+
|
|
249
|
+
obj.base = 20;
|
|
250
|
+
await Promise.resolve();
|
|
251
|
+
|
|
252
|
+
expect(values).toEqual([20, 40]);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
describe('individual object disposal', () => {
|
|
258
|
+
it('disposes parent without affecting independent child', async () => {
|
|
259
|
+
let parent = new ReactiveObject({ value: 42 }) as any,
|
|
260
|
+
child = new ReactiveObject({ ownValue: 10 }) as any;
|
|
261
|
+
|
|
262
|
+
expect(parent.value).toBe(42);
|
|
263
|
+
expect(child.ownValue).toBe(10);
|
|
264
|
+
|
|
265
|
+
parent.dispose();
|
|
266
|
+
|
|
267
|
+
// Child still works
|
|
268
|
+
expect(child.ownValue).toBe(10);
|
|
269
|
+
|
|
270
|
+
child.ownValue = 20;
|
|
271
|
+
await Promise.resolve();
|
|
272
|
+
|
|
273
|
+
expect(child.ownValue).toBe(20);
|
|
274
|
+
|
|
275
|
+
child.dispose();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('disposes object with computed that depends on external signal', async () => {
|
|
279
|
+
let s = signal(5),
|
|
280
|
+
obj = new ReactiveObject({
|
|
281
|
+
doubled: () => read(s) * 2
|
|
282
|
+
}) as any;
|
|
283
|
+
|
|
284
|
+
expect(obj.doubled).toBe(10);
|
|
285
|
+
|
|
286
|
+
obj.dispose();
|
|
287
|
+
|
|
288
|
+
// Signal still works independently
|
|
289
|
+
write(s, 100);
|
|
290
|
+
expect(read(s)).toBe(100);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
});
|
package/tests/objects.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { computed, effect, read, signal, write } from '~/system';
|
|
3
|
+
import { ReactiveObject } from '~/reactive/object';
|
|
4
|
+
import { ReactiveArray } from '~/reactive/array';
|
|
5
|
+
import reactive from '~/reactive/index';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// These tests validate reactive object patterns from the compiler integration
|
|
9
|
+
// tests. Self-referential computed properties (e.g., `doubled: () => obj.count * 2`)
|
|
10
|
+
// require the compiler transform; here we use external signals instead.
|
|
11
|
+
|
|
12
|
+
describe('reactive object patterns', () => {
|
|
13
|
+
describe('multi-property objects', () => {
|
|
14
|
+
it('creates object with multiple typed properties', () => {
|
|
15
|
+
let obj = new ReactiveObject({ age: 25, email: 'test@example.com', name: 'John' }) as any;
|
|
16
|
+
|
|
17
|
+
expect(obj.age).toBe(25);
|
|
18
|
+
expect(obj.email).toBe('test@example.com');
|
|
19
|
+
expect(obj.name).toBe('John');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('updates multiple properties independently', async () => {
|
|
23
|
+
let obj = new ReactiveObject({ age: 25, name: 'John' }) as any,
|
|
24
|
+
ages: number[] = [],
|
|
25
|
+
names: string[] = [];
|
|
26
|
+
|
|
27
|
+
effect(() => { ages.push(obj.age); });
|
|
28
|
+
effect(() => { names.push(obj.name); });
|
|
29
|
+
|
|
30
|
+
obj.age = 26;
|
|
31
|
+
await Promise.resolve();
|
|
32
|
+
|
|
33
|
+
obj.name = 'Jane';
|
|
34
|
+
await Promise.resolve();
|
|
35
|
+
|
|
36
|
+
expect(ages).toEqual([25, 26]);
|
|
37
|
+
expect(names).toEqual(['John', 'Jane']);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
describe('computed properties', () => {
|
|
43
|
+
it('string template computed', () => {
|
|
44
|
+
let s = signal(5),
|
|
45
|
+
obj = new ReactiveObject({
|
|
46
|
+
message: () => `Count is ${read(s)}`
|
|
47
|
+
}) as any;
|
|
48
|
+
|
|
49
|
+
expect(obj.message).toBe('Count is 5');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('multiple computed from same dependency', async () => {
|
|
53
|
+
let s = signal(5),
|
|
54
|
+
obj = new ReactiveObject({
|
|
55
|
+
doubled: () => read(s) * 2,
|
|
56
|
+
message: () => `Count is ${read(s)}`
|
|
57
|
+
}) as any;
|
|
58
|
+
|
|
59
|
+
expect(obj.doubled).toBe(10);
|
|
60
|
+
expect(obj.message).toBe('Count is 5');
|
|
61
|
+
|
|
62
|
+
write(s, 10);
|
|
63
|
+
await Promise.resolve();
|
|
64
|
+
|
|
65
|
+
expect(obj.doubled).toBe(20);
|
|
66
|
+
expect(obj.message).toBe('Count is 10');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('effect tracks computed property changes', async () => {
|
|
70
|
+
let s = signal(0),
|
|
71
|
+
obj = new ReactiveObject({
|
|
72
|
+
doubled: () => read(s) * 2
|
|
73
|
+
}) as any,
|
|
74
|
+
values: number[] = [];
|
|
75
|
+
|
|
76
|
+
effect(() => {
|
|
77
|
+
values.push(obj.doubled);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(values).toEqual([0]);
|
|
81
|
+
|
|
82
|
+
write(s, 5);
|
|
83
|
+
await Promise.resolve();
|
|
84
|
+
|
|
85
|
+
expect(values).toEqual([0, 10]);
|
|
86
|
+
|
|
87
|
+
write(s, 10);
|
|
88
|
+
await Promise.resolve();
|
|
89
|
+
|
|
90
|
+
expect(values).toEqual([0, 10, 20]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
describe('object with arrays', () => {
|
|
96
|
+
it('array property is ReactiveArray', () => {
|
|
97
|
+
let obj = new ReactiveObject({ items: [1, 2, 3] }) as any;
|
|
98
|
+
|
|
99
|
+
expect(obj.items).toBeInstanceOf(ReactiveArray);
|
|
100
|
+
expect([...obj.items]).toEqual([1, 2, 3]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('array push updates reactive length', async () => {
|
|
104
|
+
let obj = new ReactiveObject({ items: [1, 2, 3] }) as any,
|
|
105
|
+
lengths: number[] = [];
|
|
106
|
+
|
|
107
|
+
effect(() => {
|
|
108
|
+
lengths.push(obj.items.$length);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
obj.items.push(4, 5);
|
|
112
|
+
await Promise.resolve();
|
|
113
|
+
|
|
114
|
+
expect(lengths).toEqual([3, 5]);
|
|
115
|
+
expect([...obj.items]).toEqual([1, 2, 3, 4, 5]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('array computed using reduce via external signal', async () => {
|
|
119
|
+
let items = new ReactiveArray(1, 2, 3),
|
|
120
|
+
total = computed(() => {
|
|
121
|
+
let sum = 0;
|
|
122
|
+
|
|
123
|
+
for (let i = 0, n = items.length; i < n; i++) {
|
|
124
|
+
sum += items[i];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return sum;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(read(total)).toBe(6);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
describe('effects with objects', () => {
|
|
136
|
+
it('effect tracks sum of two properties', async () => {
|
|
137
|
+
let a = signal(1),
|
|
138
|
+
b = signal(2),
|
|
139
|
+
obj = new ReactiveObject({
|
|
140
|
+
sum: () => read(a) + read(b)
|
|
141
|
+
}) as any,
|
|
142
|
+
results: number[] = [];
|
|
143
|
+
|
|
144
|
+
effect(() => {
|
|
145
|
+
results.push(obj.sum);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(results).toEqual([3]);
|
|
149
|
+
|
|
150
|
+
write(a, 10);
|
|
151
|
+
await Promise.resolve();
|
|
152
|
+
|
|
153
|
+
expect(results).toEqual([3, 12]);
|
|
154
|
+
|
|
155
|
+
write(b, 20);
|
|
156
|
+
await Promise.resolve();
|
|
157
|
+
|
|
158
|
+
expect(results).toEqual([3, 12, 30]);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
describe('dispose', () => {
|
|
164
|
+
it('disposes object with computed properties', () => {
|
|
165
|
+
let s = signal(42),
|
|
166
|
+
obj = new ReactiveObject({
|
|
167
|
+
computed: () => read(s) * 2,
|
|
168
|
+
value: 42
|
|
169
|
+
}) as any;
|
|
170
|
+
|
|
171
|
+
expect(obj.value).toBe(42);
|
|
172
|
+
expect(obj.computed).toBe(84);
|
|
173
|
+
|
|
174
|
+
obj.dispose();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('disposes object created via reactive()', () => {
|
|
178
|
+
let obj = reactive({ value: 42 }) as any;
|
|
179
|
+
|
|
180
|
+
expect(obj.value).toBe(42);
|
|
181
|
+
|
|
182
|
+
obj.dispose();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|