@esportsplus/reactivity 0.30.3 → 0.31.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.
@@ -102,6 +102,18 @@ describe('ReactiveArray sort', () => {
102
102
 
103
103
  arr.sort((a, b) => a - b);
104
104
  });
105
+
106
+ bench('sort 1000 items', () => {
107
+ let items = [];
108
+
109
+ for (let i = 1000; i > 0; i--) {
110
+ items.push(i);
111
+ }
112
+
113
+ let arr = new ReactiveArray(...items);
114
+
115
+ arr.sort((a, b) => a - b);
116
+ });
105
117
  });
106
118
 
107
119
 
@@ -149,6 +161,71 @@ describe('ReactiveArray events', () => {
149
161
  });
150
162
 
151
163
 
164
+ describe('ReactiveArray dispose/clear', () => {
165
+ bench('dispose with 100 items', () => {
166
+ let items = [];
167
+
168
+ for (let i = 0; i < 100; i++) {
169
+ items.push(i);
170
+ }
171
+
172
+ let arr = new ReactiveArray(...items);
173
+
174
+ arr.dispose();
175
+ });
176
+
177
+ bench('clear with 100 items', () => {
178
+ let items = [];
179
+
180
+ for (let i = 0; i < 100; i++) {
181
+ items.push(i);
182
+ }
183
+
184
+ let arr = new ReactiveArray(...items);
185
+
186
+ arr.clear();
187
+ });
188
+ });
189
+
190
+
191
+ describe('ReactiveArray concat/unshift/shift/reverse', () => {
192
+ bench('concat 100 items', () => {
193
+ let arr = new ReactiveArray<number>(),
194
+ items = [];
195
+
196
+ for (let i = 0; i < 100; i++) {
197
+ items.push(i);
198
+ }
199
+
200
+ arr.concat(items);
201
+ });
202
+
203
+ bench('unshift 10 items', () => {
204
+ let arr = new ReactiveArray(1, 2, 3, 4, 5);
205
+
206
+ arr.unshift(10, 20, 30, 40, 50, 60, 70, 80, 90, 100);
207
+ });
208
+
209
+ bench('shift', () => {
210
+ let arr = new ReactiveArray(1, 2, 3, 4, 5);
211
+
212
+ arr.shift();
213
+ });
214
+
215
+ bench('reverse 100 items', () => {
216
+ let items = [];
217
+
218
+ for (let i = 0; i < 100; i++) {
219
+ items.push(i);
220
+ }
221
+
222
+ let arr = new ReactiveArray(...items);
223
+
224
+ arr.reverse();
225
+ });
226
+ });
227
+
228
+
152
229
  describe('ReactiveArray reactive length', () => {
153
230
  bench('read $length in effect', () => {
154
231
  let arr = new ReactiveArray(1, 2, 3);
@@ -0,0 +1,59 @@
1
+ import { bench, describe } from 'vitest';
2
+ import { ReactiveObject } from '~/reactive/object';
3
+
4
+
5
+ describe('ReactiveObject creation', () => {
6
+ bench('create with 5 signal properties', () => {
7
+ new ReactiveObject({ a: 1, b: 2, c: 3, d: 4, e: 5 });
8
+ });
9
+
10
+ bench('create with computed properties', () => {
11
+ new ReactiveObject({
12
+ a: 1,
13
+ b: 2,
14
+ sum: () => 0
15
+ });
16
+ });
17
+
18
+ bench('create with async computed property', () => {
19
+ new ReactiveObject({
20
+ value: 1,
21
+ async: () => Promise.resolve(0)
22
+ });
23
+ });
24
+ });
25
+
26
+
27
+ describe('ReactiveObject read/write', () => {
28
+ bench('read property (signal-backed)', () => {
29
+ let obj = new ReactiveObject({ a: 1, b: 2, c: 3, d: 4, e: 5 });
30
+
31
+ (obj as any).a;
32
+ });
33
+
34
+ bench('write property (signal-backed)', () => {
35
+ let obj = new ReactiveObject({ a: 1, b: 2, c: 3, d: 4, e: 5 }),
36
+ i = 0;
37
+
38
+ (obj as any).a = ++i;
39
+ });
40
+ });
41
+
42
+
43
+ describe('ReactiveObject dispose', () => {
44
+ bench('dispose with 5 properties', () => {
45
+ let obj = new ReactiveObject({ a: 1, b: 2, c: 3, d: 4, e: 5 });
46
+
47
+ obj.dispose();
48
+ });
49
+
50
+ bench('dispose with arrays + computeds', () => {
51
+ let obj = new ReactiveObject({
52
+ items: [1, 2, 3],
53
+ name: 'test',
54
+ total: () => 0
55
+ });
56
+
57
+ obj.dispose();
58
+ });
59
+ });
@@ -1,5 +1,5 @@
1
1
  import { bench, describe } from 'vitest';
2
- import { computed, dispose, effect, read, root, signal, write } from '~/system';
2
+ import { asyncComputed, computed, dispose, effect, onCleanup, read, root, signal, write } from '~/system';
3
3
 
4
4
 
5
5
  describe('signal', () => {
@@ -76,6 +76,19 @@ describe('computed', () => {
76
76
  });
77
77
 
78
78
 
79
+ describe('asyncComputed', () => {
80
+ bench('create asyncComputed', () => {
81
+ asyncComputed(() => Promise.resolve(0));
82
+ });
83
+
84
+ bench('create asyncComputed from signal', () => {
85
+ let s = signal(0);
86
+
87
+ asyncComputed(() => Promise.resolve(read(s)));
88
+ });
89
+ });
90
+
91
+
79
92
  describe('effect', () => {
80
93
  bench('create + dispose effect', () => {
81
94
  let stop = effect(() => {});
@@ -228,3 +241,131 @@ describe('memory', () => {
228
241
  });
229
242
  });
230
243
  });
244
+
245
+
246
+ describe('effect stress', () => {
247
+ bench('create + dispose 1000 effects (pool recycling)', () => {
248
+ for (let i = 0; i < 1000; i++) {
249
+ let stop = effect(() => {});
250
+
251
+ stop();
252
+ }
253
+ });
254
+ });
255
+
256
+
257
+ describe('deep propagation', () => {
258
+ bench('deep chain (50 computeds)', () => {
259
+ let s = signal(0),
260
+ chain: ReturnType<typeof computed>[] = [],
261
+ i = 0;
262
+
263
+ chain[0] = computed(() => read(s) + 1);
264
+
265
+ for (let j = 1; j < 50; j++) {
266
+ let prev = chain[j - 1];
267
+
268
+ chain[j] = computed(() => read(prev) + 1);
269
+ }
270
+
271
+ effect(() => {
272
+ read(chain[49]);
273
+ });
274
+
275
+ write(s, ++i);
276
+ });
277
+
278
+ bench('deep chain (100 computeds)', () => {
279
+ let s = signal(0),
280
+ chain: ReturnType<typeof computed>[] = [],
281
+ i = 0;
282
+
283
+ chain[0] = computed(() => read(s) + 1);
284
+
285
+ for (let j = 1; j < 100; j++) {
286
+ let prev = chain[j - 1];
287
+
288
+ chain[j] = computed(() => read(prev) + 1);
289
+ }
290
+
291
+ effect(() => {
292
+ read(chain[99]);
293
+ });
294
+
295
+ write(s, ++i);
296
+ });
297
+ });
298
+
299
+
300
+ describe('stabilization', () => {
301
+ bench('write during stabilization (reschedule path)', () => {
302
+ let a = signal(0),
303
+ b = signal(0),
304
+ i = 0;
305
+
306
+ effect(() => {
307
+ let val = read(a);
308
+
309
+ if (val > 0) {
310
+ write(b, val * 10);
311
+ }
312
+ });
313
+
314
+ effect(() => {
315
+ read(b);
316
+ });
317
+
318
+ write(a, ++i);
319
+ });
320
+
321
+ bench('read computed during stabilization', () => {
322
+ let a = signal(0),
323
+ b = signal(0),
324
+ c = computed(() => read(b) * 2),
325
+ i = 0;
326
+
327
+ effect(() => {
328
+ let val = read(a);
329
+
330
+ if (val > 0) {
331
+ write(b, val * 10);
332
+ }
333
+ });
334
+
335
+ effect(() => {
336
+ read(c);
337
+ });
338
+
339
+ write(a, ++i);
340
+ });
341
+ });
342
+
343
+
344
+ describe('root', () => {
345
+ bench('root scope creation + disposal', () => {
346
+ root((dispose) => {
347
+ dispose();
348
+ });
349
+ });
350
+ });
351
+
352
+
353
+ describe('onCleanup', () => {
354
+ bench('register 1 cleanup', () => {
355
+ root(() => {
356
+ effect(() => {
357
+ onCleanup(() => {});
358
+ });
359
+ });
360
+ });
361
+
362
+ bench('register 10 cleanups', () => {
363
+ root(() => {
364
+ effect(() => {
365
+ for (let i = 0; i < 10; i++) {
366
+ onCleanup(() => {});
367
+ }
368
+ });
369
+ });
370
+ });
371
+ });
@@ -0,0 +1,326 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ts } from '@esportsplus/typescript';
3
+ import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
4
+ import { NAMESPACE } from '~/compiler/constants';
5
+ import type { Bindings } from '~/compiler/types';
6
+ import array from '~/compiler/array';
7
+ import object from '~/compiler/object';
8
+ import primitives from '~/compiler/primitives';
9
+ import pipeline from '~/compiler/index';
10
+ import tscPlugin from '~/compiler/plugins/tsc';
11
+ import vitePlugin from '~/compiler/plugins/vite';
12
+
13
+
14
+ function applyIntents(code: string, sourceFile: ts.SourceFile, intents: ReplacementIntent[]): string {
15
+ let sorted = [...intents].sort((a, b) => b.node.getStart(sourceFile) - a.node.getStart(sourceFile));
16
+
17
+ for (let i = 0, n = sorted.length; i < n; i++) {
18
+ let intent = sorted[i],
19
+ end = intent.node.getEnd(),
20
+ start = intent.node.getStart(sourceFile);
21
+
22
+ code = code.slice(0, start) + intent.generate(sourceFile) + code.slice(end);
23
+ }
24
+
25
+ return code;
26
+ }
27
+
28
+ function isReactiveCall(node: ts.Node): boolean {
29
+ return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'reactive';
30
+ }
31
+
32
+ function parse(code: string): ts.SourceFile {
33
+ return ts.createSourceFile('test.ts', code, ts.ScriptTarget.Latest, true);
34
+ }
35
+
36
+ function transformPrimitives(code: string): { bindings: Bindings; output: string } {
37
+ let bindings: Bindings = new Map(),
38
+ sourceFile = parse(code),
39
+ intents = primitives(sourceFile, bindings, isReactiveCall);
40
+
41
+ return { bindings, output: applyIntents(code, sourceFile, intents) };
42
+ }
43
+
44
+ function transformArray(code: string, bindings?: Bindings): { bindings: Bindings; output: string } {
45
+ let b: Bindings = bindings ?? new Map(),
46
+ sourceFile = parse(code),
47
+ intents = array(sourceFile, b, undefined);
48
+
49
+ return { bindings: b, output: applyIntents(code, sourceFile, intents) };
50
+ }
51
+
52
+ function transformObject(code: string): { bindings: Bindings; output: string; prepend: string[] } {
53
+ let bindings: Bindings = new Map(),
54
+ sourceFile = parse(code),
55
+ result = object(sourceFile, bindings, undefined);
56
+
57
+ return {
58
+ bindings,
59
+ output: applyIntents(code, sourceFile, result.replacements),
60
+ prepend: result.prepend
61
+ };
62
+ }
63
+
64
+
65
+ describe('primitives transform', () => {
66
+ it('transforms reactive(0) to signal', () => {
67
+ let { output } = transformPrimitives('let x = reactive(0);');
68
+
69
+ expect(output).toContain(`${NAMESPACE}.signal(0)`);
70
+ });
71
+
72
+ it('transforms reactive(() => expr) to computed', () => {
73
+ let { output } = transformPrimitives('let x = reactive(0); let d = reactive(() => x * 2);');
74
+
75
+ expect(output).toContain(`${NAMESPACE}.computed(() =>`);
76
+ });
77
+
78
+ it('transforms reads to namespace read', () => {
79
+ let { output } = transformPrimitives('let x = reactive(0); console.log(x);');
80
+
81
+ expect(output).toContain(`${NAMESPACE}.read(x)`);
82
+ });
83
+
84
+ it('transforms simple assignment to write', () => {
85
+ let { output } = transformPrimitives('let x = reactive(0); x = 5;');
86
+
87
+ expect(output).toContain(`${NAMESPACE}.write(x, 5)`);
88
+ });
89
+
90
+ it('transforms compound assignment += to write', () => {
91
+ let { output } = transformPrimitives('let x = reactive(0); x += 5;');
92
+
93
+ expect(output).toContain(`${NAMESPACE}.write(x, x.value + 5)`);
94
+ });
95
+
96
+ it('transforms postfix x++ in statement to write', () => {
97
+ let { output } = transformPrimitives('let x = reactive(0); x++;');
98
+
99
+ expect(output).toContain(`${NAMESPACE}.write(x, x.value + 1)`);
100
+ });
101
+
102
+ it('transforms prefix ++x in expression', () => {
103
+ let { output } = transformPrimitives('let x = reactive(0); let y = ++x;');
104
+
105
+ expect(output).toContain(`(${NAMESPACE}.write(x, x.value + 1), x.value)`);
106
+ });
107
+
108
+ it('transforms postfix x++ in expression with temp variable', () => {
109
+ let { output } = transformPrimitives('let x = reactive(0); let y = x++;');
110
+
111
+ expect(output).toContain(`((_t0) => (${NAMESPACE}.write(x, _t0 + 1), _t0))(x.value)`);
112
+ });
113
+
114
+ it('transforms reads in nested functions within scope', () => {
115
+ let { output } = transformPrimitives('let x = reactive(0); function fn() { return x; }');
116
+
117
+ // The x inside fn IS within the reactive binding scope, so it gets transformed
118
+ expect(output).toContain(`${NAMESPACE}.read(x)`);
119
+ });
120
+
121
+ it('transforms dynamic expression to namespace reactive', () => {
122
+ let { output } = transformPrimitives('let x = reactive(someCall());');
123
+
124
+ expect(output).toContain(`${NAMESPACE}.reactive(someCall())`);
125
+ });
126
+
127
+ it('tracks bindings for signal type', () => {
128
+ let { bindings } = transformPrimitives('let x = reactive(0);');
129
+
130
+ // TYPES.Signal = 3
131
+ expect(bindings.get('x')).toBe(3);
132
+ });
133
+
134
+ it('tracks bindings for computed type', () => {
135
+ let { bindings } = transformPrimitives('let x = reactive(0); let d = reactive(() => x * 2);');
136
+
137
+ // TYPES.Computed = 1
138
+ expect(bindings.get('d')).toBe(1);
139
+ });
140
+
141
+ it('transforms prefix --x in statement', () => {
142
+ let { output } = transformPrimitives('let x = reactive(0); --x;');
143
+
144
+ expect(output).toContain(`${NAMESPACE}.write(x, x.value - 1)`);
145
+ });
146
+
147
+ it('transforms compound assignment -= to write', () => {
148
+ let { output } = transformPrimitives('let x = reactive(0); x -= 3;');
149
+
150
+ expect(output).toContain(`${NAMESPACE}.write(x, x.value - 3)`);
151
+ });
152
+ });
153
+
154
+
155
+ describe('object transform', () => {
156
+ it('transforms reactive object with signal field', () => {
157
+ let { output, prepend } = transformObject('let obj = reactive({ count: 0 });');
158
+
159
+ expect(prepend.length).toBe(1);
160
+ expect(prepend[0]).toContain(`extends ${NAMESPACE}.ReactiveObject`);
161
+ expect(prepend[0]).toContain(`${NAMESPACE}.read(this.#count)`);
162
+ expect(prepend[0]).toContain(`${NAMESPACE}.write(this.#count`);
163
+ expect(output).toContain('new ');
164
+ expect(output).not.toContain('reactive(');
165
+ });
166
+
167
+ it('transforms reactive object with array field', () => {
168
+ let { prepend } = transformObject('let obj = reactive({ items: [1, 2, 3] });');
169
+
170
+ expect(prepend.length).toBe(1);
171
+ expect(prepend[0]).toContain(`${NAMESPACE}.REACTIVE_ARRAY`);
172
+ expect(prepend[0]).toContain('get items()');
173
+ });
174
+
175
+ it('transforms reactive object with computed field', () => {
176
+ let { prepend } = transformObject('let obj = reactive({ doubled: () => 2 });');
177
+
178
+ expect(prepend.length).toBe(1);
179
+ expect(prepend[0]).toContain(`${NAMESPACE}.COMPUTED`);
180
+ expect(prepend[0]).toContain(`${NAMESPACE}.read(this.#doubled)`);
181
+ });
182
+
183
+ it('transforms reactive object with mixed properties', () => {
184
+ let { output, prepend } = transformObject(
185
+ 'let obj = reactive({ count: 0, items: [1], doubled: () => 2 });'
186
+ );
187
+
188
+ expect(prepend.length).toBe(1);
189
+ expect(prepend[0]).toContain(`${NAMESPACE}.SIGNAL`);
190
+ expect(prepend[0]).toContain(`${NAMESPACE}.REACTIVE_ARRAY`);
191
+ expect(prepend[0]).toContain(`${NAMESPACE}.COMPUTED`);
192
+ expect(output).toContain('new ');
193
+ });
194
+
195
+ it('does not transform object with spread assignment', () => {
196
+ let { output, prepend } = transformObject('let obj = reactive({ ...base, count: 0 });');
197
+
198
+ expect(prepend.length).toBe(0);
199
+ expect(output).toContain('reactive(');
200
+ });
201
+
202
+ it('preserves type parameter', () => {
203
+ let { output } = transformObject('let obj = reactive<MyType>({ count: 0 });');
204
+
205
+ expect(output).toContain('<MyType>');
206
+ });
207
+
208
+ it('tracks object binding', () => {
209
+ let { bindings } = transformObject('let obj = reactive({ count: 0 });');
210
+
211
+ // TYPES.Object = 2
212
+ expect(bindings.get('obj')).toBe(2);
213
+ });
214
+
215
+ it('tracks nested array bindings', () => {
216
+ let { bindings } = transformObject('let obj = reactive({ items: [1, 2, 3] });');
217
+
218
+ // TYPES.Array = 0
219
+ expect(bindings.get('obj.items')).toBe(0);
220
+ });
221
+ });
222
+
223
+
224
+ describe('array transform', () => {
225
+ it('transforms reactive([1,2,3]) to ReactiveArray', () => {
226
+ let { output } = transformArray('let arr = reactive([1, 2, 3]);');
227
+
228
+ expect(output).toContain(`new ${NAMESPACE}.ReactiveArray`);
229
+ expect(output).toContain('...[1, 2, 3]');
230
+ });
231
+
232
+ it('transforms reactive([] as Type[]) to typed ReactiveArray', () => {
233
+ let { output } = transformArray('let arr = reactive([] as number[]);');
234
+
235
+ expect(output).toContain(`new ${NAMESPACE}.ReactiveArray<number>()`);
236
+ });
237
+
238
+ it('transforms arr.length read to arr.$length', () => {
239
+ let bindings: Bindings = new Map();
240
+
241
+ bindings.set('arr', 0); // TYPES.Array = 0
242
+ let { output } = transformArray('let x = arr.length;', bindings);
243
+
244
+ expect(output).toContain('arr.$length');
245
+ });
246
+
247
+ it('transforms arr.length = n to arr.$length = n', () => {
248
+ let bindings: Bindings = new Map();
249
+
250
+ bindings.set('arr', 0);
251
+ let { output } = transformArray('arr.length = 5;', bindings);
252
+
253
+ expect(output).toContain('arr.$length = 5');
254
+ });
255
+
256
+ it('transforms arr.length += n to arr.$length = arr.length + n', () => {
257
+ let bindings: Bindings = new Map();
258
+
259
+ bindings.set('arr', 0);
260
+ let { output } = transformArray('arr.length += 3;', bindings);
261
+
262
+ expect(output).toContain('arr.$length = arr.length + 3');
263
+ });
264
+
265
+ it('transforms arr[i] = value to arr.$set(i, value)', () => {
266
+ let bindings: Bindings = new Map();
267
+
268
+ bindings.set('arr', 0);
269
+ let { output } = transformArray('arr[0] = 42;', bindings);
270
+
271
+ expect(output).toContain('arr.$set(');
272
+ expect(output).toContain('42');
273
+ });
274
+
275
+ it('tracks reactive array binding from reactive call', () => {
276
+ let { bindings } = transformArray('let arr = reactive([1, 2, 3]);');
277
+
278
+ // TYPES.Array = 0
279
+ expect(bindings.get('arr')).toBe(0);
280
+ });
281
+
282
+ it('tracks alias binding from reactive array', () => {
283
+ let bindings: Bindings = new Map();
284
+
285
+ bindings.set('a', 0);
286
+ transformArray('let b = a;', bindings);
287
+
288
+ expect(bindings.get('b')).toBe(0);
289
+ });
290
+
291
+ it('tracks typed parameter as ReactiveArray', () => {
292
+ let bindings: Bindings = new Map();
293
+
294
+ transformArray('function fn(arr: ReactiveArray) { return arr; }', bindings);
295
+
296
+ expect(bindings.get('arr')).toBe(0);
297
+ });
298
+
299
+ it('transforms empty array', () => {
300
+ let { output } = transformArray('let arr = reactive([] as string[]);');
301
+
302
+ expect(output).toContain(`new ${NAMESPACE}.ReactiveArray<string>()`);
303
+ });
304
+ });
305
+
306
+
307
+ describe('index transform', () => {
308
+ it('exports patterns array', () => {
309
+ expect(pipeline.patterns).toEqual(['reactive(', 'reactive<']);
310
+ });
311
+
312
+ it('has transform function', () => {
313
+ expect(typeof pipeline.transform).toBe('function');
314
+ });
315
+ });
316
+
317
+
318
+ describe('plugins', () => {
319
+ it('tsc plugin is defined', () => {
320
+ expect(tscPlugin).toBeDefined();
321
+ });
322
+
323
+ it('vite plugin is defined', () => {
324
+ expect(vitePlugin).toBeDefined();
325
+ });
326
+ });