@esportsplus/reactivity 0.39.2 → 0.41.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/README.md +12 -6
- package/bench/compiler.bench.ts +115 -0
- package/bench/kairo.bench.ts +1 -1
- package/bench/reactive/array.bench.ts +1 -0
- package/bench/reactive/reactive-object.bench.ts +3 -2
- package/build/compiler/array.js +28 -25
- package/build/compiler/bindings.d.ts +3 -1
- package/build/compiler/bindings.js +32 -9
- package/build/compiler/index.d.ts +3 -2
- package/build/compiler/index.js +21 -13
- package/build/compiler/object.js +12 -13
- package/build/compiler/plugins/vite.d.ts +13 -4
- package/build/compiler/primitives.js +9 -9
- package/build/constants.d.ts +2 -1
- package/build/constants.js +2 -1
- package/build/index.d.ts +1 -0
- package/build/reactive/array.d.ts +40 -1
- package/build/reactive/array.js +82 -16
- package/build/reactive/index.d.ts +1 -1
- package/build/reactive/object.d.ts +3 -3
- package/build/reactive/object.js +1 -1
- package/build/system.d.ts +3 -2
- package/build/system.js +10 -5
- package/oxlintrc.json +3 -0
- package/package.json +15 -7
- package/pnpm-workspace.yaml +4 -7
- package/src/compiler/array.ts +30 -27
- package/src/compiler/bindings.ts +49 -10
- package/src/compiler/index.ts +37 -20
- package/src/compiler/object.ts +14 -16
- package/src/compiler/primitives.ts +13 -10
- package/src/constants.ts +4 -1
- package/src/index.ts +1 -0
- package/src/reactive/array.ts +133 -22
- package/src/reactive/index.ts +3 -3
- package/src/reactive/object.ts +4 -4
- package/src/system.ts +20 -5
- package/test/compiler/__snapshots__/output.test.ts.snap +157 -0
- package/test/compiler/compiler.test.ts +51 -9
- package/test/compiler/composition.test.ts +110 -0
- package/test/compiler/coverage.test.ts +22 -22
- package/test/compiler/origins.test.ts +156 -0
- package/test/compiler/output.test.ts +116 -0
- package/test/ownership.test.ts +100 -1
- package/test/reactive/array.test.ts +189 -8
- package/test/reactive/reactive.test.ts +4 -4
- package/test/system.test.ts +3 -4
- package/test/untrack.test.ts +1 -1
- package/tsconfig.build.json +3 -0
- package/tsconfig.json +2 -2
- package/vitest.config.ts +2 -2
- package/bench/tsconfig.json +0 -16
- package/test/tsconfig.json +0 -16
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import vite from '~/compiler/plugins/vite';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// Whole compiled modules, pinned byte for byte: every lowering form the compiler emits, each one
|
|
9
|
+
// alone in its range so no output depends on how nested rewrites compose
|
|
10
|
+
const FILES: Record<string, string> = {
|
|
11
|
+
'app.ts': [
|
|
12
|
+
"export { reactive as make } from '@esportsplus/reactivity';",
|
|
13
|
+
"export * from './state';",
|
|
14
|
+
"export { default as total } from './state';"
|
|
15
|
+
].join('\n'),
|
|
16
|
+
'consumer.ts': [
|
|
17
|
+
"import { count as renamed, later, make, total } from './app';",
|
|
18
|
+
"import * as app from './app';",
|
|
19
|
+
'export let a = make(0);',
|
|
20
|
+
'export let b = () => renamed + total + later;',
|
|
21
|
+
'export let c = () => app.count + app.list.length;',
|
|
22
|
+
"export let d = () => app['count'];",
|
|
23
|
+
'export let e = () => { app.list[0] = 1; };'
|
|
24
|
+
].join('\n'),
|
|
25
|
+
'forms.ts': [
|
|
26
|
+
"import { reactive } from '@esportsplus/reactivity';",
|
|
27
|
+
'declare function total(): number;',
|
|
28
|
+
'declare let list: number[];',
|
|
29
|
+
'declare let n: number;',
|
|
30
|
+
'let count = reactive(0);',
|
|
31
|
+
'let doubled = reactive(() => count * 2);',
|
|
32
|
+
'let typed = reactive(n);',
|
|
33
|
+
'let fn = reactive(total);',
|
|
34
|
+
'let runtime = reactive(list);',
|
|
35
|
+
'let obj = reactive({ a: 1, b: \'x\', items: [1, 2], sum: () => 3, neg: -1, source: list, flag: true });',
|
|
36
|
+
'let shorthand = reactive({ list });',
|
|
37
|
+
'let hinted = reactive<{ a: number; items: number[] }>({ a: 1, items: [] });',
|
|
38
|
+
'let spread = reactive({ ...{ a: 1 }, b: 2 });',
|
|
39
|
+
'let method = reactive({ m() { return 1; } });',
|
|
40
|
+
'let arr = reactive([1, 2, 3]);',
|
|
41
|
+
'let empty = reactive([] as string[]);',
|
|
42
|
+
'let declared: number[] = reactive([4]);',
|
|
43
|
+
'count = 5;',
|
|
44
|
+
'count += 2;',
|
|
45
|
+
'count ||= 3;',
|
|
46
|
+
'count++;',
|
|
47
|
+
'--count;',
|
|
48
|
+
'let p = count++;',
|
|
49
|
+
'let q = ++count;',
|
|
50
|
+
'let o = { count };',
|
|
51
|
+
'let t = typeof count;',
|
|
52
|
+
'arr[0] = 4;',
|
|
53
|
+
'arr[1] += 2;',
|
|
54
|
+
'arr[2]++;',
|
|
55
|
+
'let r = ++arr[0];',
|
|
56
|
+
'let size = arr.length;',
|
|
57
|
+
'arr.length = 1;',
|
|
58
|
+
'arr.length += 1;',
|
|
59
|
+
'arr.length++;',
|
|
60
|
+
'arr = [5, 6];',
|
|
61
|
+
'arr = [];',
|
|
62
|
+
'export { count, doubled, typed, fn, runtime, obj, shorthand, hinted, spread, method, empty, declared, p, q, o, t, r, size };',
|
|
63
|
+
'export default reactive(0);'
|
|
64
|
+
].join('\n'),
|
|
65
|
+
'state.ts': [
|
|
66
|
+
"import { reactive } from '@esportsplus/reactivity';",
|
|
67
|
+
'export let count = reactive(0);',
|
|
68
|
+
'export let list = reactive([1, 2]);',
|
|
69
|
+
'export let later: number;',
|
|
70
|
+
'later = reactive(1);',
|
|
71
|
+
'export default reactive(() => 2);'
|
|
72
|
+
].join('\n')
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
let fixtures: string[] = [];
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
function compile(name: string): string | null {
|
|
80
|
+
let directory = mkdtempSync(join(process.cwd(), '.fixture-output-')).replace(/\\/g, '/');
|
|
81
|
+
|
|
82
|
+
fixtures.push(directory);
|
|
83
|
+
|
|
84
|
+
for (let [file, code] of Object.entries(FILES)) {
|
|
85
|
+
writeFileSync(join(directory, file), code);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
writeFileSync(join(directory, 'tsconfig.json'), JSON.stringify({
|
|
89
|
+
compilerOptions: { module: 'esnext', moduleResolution: 'bundler', strict: true, target: 'esnext', types: [] },
|
|
90
|
+
files: Object.keys(FILES).map(file => './' + file)
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
let plugin = vite({ root: directory });
|
|
94
|
+
|
|
95
|
+
plugin.configResolved({ command: 'build', root: directory });
|
|
96
|
+
|
|
97
|
+
return plugin.transform.handler(FILES[name], directory + '/' + name)?.code ?? null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
afterEach(() => {
|
|
102
|
+
for (let i = 0, n = fixtures.length; i < n; i++) {
|
|
103
|
+
rmSync(fixtures[i], { force: true, recursive: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fixtures = [];
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
describe('compiled output', () => {
|
|
111
|
+
for (let name of Object.keys(FILES)) {
|
|
112
|
+
it(name, () => {
|
|
113
|
+
expect(compile(name)).toMatchSnapshot();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
package/test/ownership.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { computed, dispose, effect, flush, hasOwner, onCleanup, read, root, signal, untrack, write } from '~/system';
|
|
2
|
+
import { computed, owner as current, dispose, effect, flush, hasOwner, onCleanup, read, root, signal, untrack, write } from '~/system';
|
|
3
3
|
import type { Computed } from '~/system';
|
|
4
4
|
import { captureUncaught } from './lib/uncaught';
|
|
5
5
|
|
|
@@ -584,3 +584,102 @@ describe('hasOwner', () => {
|
|
|
584
584
|
});
|
|
585
585
|
});
|
|
586
586
|
|
|
587
|
+
describe('owner', () => {
|
|
588
|
+
it('is the running computation, else the running root scope, else null', () => {
|
|
589
|
+
let running: Computed<unknown> | null = null,
|
|
590
|
+
scope: Computed<unknown> | null = null,
|
|
591
|
+
seen: unknown[] = [];
|
|
592
|
+
|
|
593
|
+
seen.push(current());
|
|
594
|
+
|
|
595
|
+
let stop = root((dispose) => {
|
|
596
|
+
scope = current();
|
|
597
|
+
|
|
598
|
+
effect(() => {
|
|
599
|
+
running = current();
|
|
600
|
+
seen.push(untrack(() => current()) === running);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
return dispose;
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
seen.push(current());
|
|
607
|
+
|
|
608
|
+
expect(scope).not.toBeNull();
|
|
609
|
+
expect(running).not.toBe(scope);
|
|
610
|
+
// An effect is owned once its first run returns
|
|
611
|
+
expect(running!.owner).toBe(scope);
|
|
612
|
+
|
|
613
|
+
stop();
|
|
614
|
+
|
|
615
|
+
expect(seen).toEqual([null, true, null]);
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
describe('root with an owner', () => {
|
|
620
|
+
it('runs fn owned by the given node, its reads untracked', async () => {
|
|
621
|
+
let cleaned = 0,
|
|
622
|
+
s = signal(0),
|
|
623
|
+
node: Computed<unknown> | null = null,
|
|
624
|
+
runs = 0,
|
|
625
|
+
stop = root((dispose) => {
|
|
626
|
+
node = current();
|
|
627
|
+
|
|
628
|
+
return dispose;
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
root(() => {
|
|
632
|
+
runs++;
|
|
633
|
+
read(s);
|
|
634
|
+
onCleanup(() => cleaned++);
|
|
635
|
+
effect(() => { read(s); });
|
|
636
|
+
}, node);
|
|
637
|
+
|
|
638
|
+
expect(hasOwner()).toBe(false);
|
|
639
|
+
expect(registrations(node!)).toBe(2);
|
|
640
|
+
|
|
641
|
+
write(s, 1);
|
|
642
|
+
await Promise.resolve();
|
|
643
|
+
|
|
644
|
+
expect(runs).toBe(1);
|
|
645
|
+
|
|
646
|
+
stop();
|
|
647
|
+
|
|
648
|
+
expect(cleaned).toBe(1);
|
|
649
|
+
expect(registrations(node!)).toBe(0);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('creates a child root of the given node instead of whatever is running', () => {
|
|
653
|
+
let a: Computed<unknown> | null = null,
|
|
654
|
+
b: Computed<unknown> | null = null,
|
|
655
|
+
child: Computed<unknown> | null = null,
|
|
656
|
+
stopA = root((dispose) => { a = current(); return dispose; }),
|
|
657
|
+
stopB = root((dispose) => { b = current(); return dispose; });
|
|
658
|
+
|
|
659
|
+
root(() => {
|
|
660
|
+
root((dispose) => {
|
|
661
|
+
child = current();
|
|
662
|
+
|
|
663
|
+
return dispose;
|
|
664
|
+
}, a);
|
|
665
|
+
}, b);
|
|
666
|
+
|
|
667
|
+
expect(child!.owner).toBe(a);
|
|
668
|
+
expect(registrations(b!)).toBe(0);
|
|
669
|
+
|
|
670
|
+
stopA();
|
|
671
|
+
|
|
672
|
+
expect(child!.owner).toBe(null);
|
|
673
|
+
|
|
674
|
+
stopB();
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('throws on a disposed owner', () => {
|
|
678
|
+
let node: Computed<unknown> | null = null,
|
|
679
|
+
stop = root((dispose) => { node = current(); return dispose; });
|
|
680
|
+
|
|
681
|
+
stop();
|
|
682
|
+
|
|
683
|
+
expect(() => root(() => {}, node)).toThrow('@esportsplus/reactivity: root() was given a disposed owner');
|
|
684
|
+
});
|
|
685
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { effect, read, signal, write } from '~/system';
|
|
2
|
+
import { effect, flush, owner, read, root, signal, write } from '~/system';
|
|
3
3
|
import { ReactiveArray } from '~/reactive/array';
|
|
4
4
|
import { ReactiveObject } from '~/reactive/object';
|
|
5
5
|
import reactive from '~/reactive/index';
|
|
@@ -54,7 +54,7 @@ describe('ReactiveArray', () => {
|
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it('does not overflow the stack for a large array', () => {
|
|
57
|
-
let arr = new ReactiveArray(
|
|
57
|
+
let arr = new ReactiveArray(Array.from({ length: 500000 }, () => 0));
|
|
58
58
|
|
|
59
59
|
expect(arr.length).toBe(500000);
|
|
60
60
|
expect(arr.$length).toBe(500000);
|
|
@@ -463,7 +463,7 @@ describe('ReactiveArray', () => {
|
|
|
463
463
|
arr.on('splice', (e) => { events.push(e); });
|
|
464
464
|
arr.splice(1, 1, 20, 30);
|
|
465
465
|
|
|
466
|
-
expect(events).toEqual([{
|
|
466
|
+
expect(events).toEqual([{ deleteCount: 1, items: [20, 30], start: 1 }]);
|
|
467
467
|
});
|
|
468
468
|
|
|
469
469
|
it('no event when nothing changes', () => {
|
|
@@ -499,7 +499,7 @@ describe('ReactiveArray', () => {
|
|
|
499
499
|
|
|
500
500
|
expect([...removed]).toEqual([4]);
|
|
501
501
|
expect([...arr]).toEqual([1, 2, 3, 5]);
|
|
502
|
-
expect(events).toEqual([{
|
|
502
|
+
expect(events).toEqual([{ deleteCount: 1, items: [], start: 3 }]);
|
|
503
503
|
});
|
|
504
504
|
|
|
505
505
|
it('splice dispatches the count actually removed, not the requested count', () => {
|
|
@@ -510,7 +510,7 @@ describe('ReactiveArray', () => {
|
|
|
510
510
|
arr.splice(1, 100);
|
|
511
511
|
|
|
512
512
|
expect([...arr]).toEqual([1]);
|
|
513
|
-
expect(events).toEqual([{
|
|
513
|
+
expect(events).toEqual([{ deleteCount: 2, items: [], start: 1 }]);
|
|
514
514
|
});
|
|
515
515
|
|
|
516
516
|
it('splice clamps an out-of-range or fractional start the way Array.prototype.splice does', () => {
|
|
@@ -524,9 +524,9 @@ describe('ReactiveArray', () => {
|
|
|
524
524
|
|
|
525
525
|
expect([...arr]).toEqual([0, 9, 1, 2, 3, 4]);
|
|
526
526
|
expect(events).toEqual([
|
|
527
|
-
{
|
|
528
|
-
{
|
|
529
|
-
{
|
|
527
|
+
{ deleteCount: 0, items: [4], start: 3 },
|
|
528
|
+
{ deleteCount: 0, items: [0], start: 0 },
|
|
529
|
+
{ deleteCount: 0, items: [9], start: 1 }
|
|
530
530
|
]);
|
|
531
531
|
});
|
|
532
532
|
});
|
|
@@ -652,6 +652,7 @@ describe('ReactiveArray', () => {
|
|
|
652
652
|
arr.sort((a, b) => a - b);
|
|
653
653
|
|
|
654
654
|
expect([...arr]).toEqual([1, 2, 2]);
|
|
655
|
+
expect(order).toEqual([1, 0, 2]);
|
|
655
656
|
});
|
|
656
657
|
|
|
657
658
|
it('order maps duplicate values stably (first stays first)', () => {
|
|
@@ -1226,3 +1227,183 @@ describe('ReactiveArray', () => {
|
|
|
1226
1227
|
});
|
|
1227
1228
|
});
|
|
1228
1229
|
});
|
|
1230
|
+
|
|
1231
|
+
describe('ReactiveArray batch', () => {
|
|
1232
|
+
// Disposed links go back to a shared pool; a node disposed mid-run then walks pooled links
|
|
1233
|
+
let pool = () => {
|
|
1234
|
+
let a = signal(0),
|
|
1235
|
+
b = signal(0),
|
|
1236
|
+
c = signal(0);
|
|
1237
|
+
|
|
1238
|
+
root(() => effect(() => read(a) + read(b) + read(c)))();
|
|
1239
|
+
};
|
|
1240
|
+
|
|
1241
|
+
it('delivers every mutation of a task once, in order, in the pass that follows', async () => {
|
|
1242
|
+
let arr = new ReactiveArray([1, 2, 3]),
|
|
1243
|
+
batches: unknown[][] = [];
|
|
1244
|
+
|
|
1245
|
+
arr.on('batch', (ops) => batches.push(ops));
|
|
1246
|
+
arr.push(4);
|
|
1247
|
+
arr.$set(0, 9);
|
|
1248
|
+
arr.splice(1, 1, 7, 8);
|
|
1249
|
+
arr.pop();
|
|
1250
|
+
|
|
1251
|
+
expect(batches).toEqual([]);
|
|
1252
|
+
|
|
1253
|
+
await Promise.resolve();
|
|
1254
|
+
|
|
1255
|
+
expect(batches).toEqual([[
|
|
1256
|
+
{ items: [4], op: 'push' },
|
|
1257
|
+
{ index: 0, item: 9, op: 'set' },
|
|
1258
|
+
{ deleteCount: 1, items: [7, 8], op: 'splice', start: 1 },
|
|
1259
|
+
{ item: 4, op: 'pop' }
|
|
1260
|
+
]]);
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
it('settles through flush()', () => {
|
|
1264
|
+
let arr = new ReactiveArray<number>(),
|
|
1265
|
+
batches: unknown[][] = [];
|
|
1266
|
+
|
|
1267
|
+
arr.on('batch', (ops) => batches.push(ops));
|
|
1268
|
+
arr.push(1, 2);
|
|
1269
|
+
flush();
|
|
1270
|
+
|
|
1271
|
+
expect(batches).toEqual([[{ items: [1, 2], op: 'push' }]]);
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
it('drops the mutations before a clear', () => {
|
|
1275
|
+
let arr = new ReactiveArray([1]),
|
|
1276
|
+
batches: unknown[][] = [];
|
|
1277
|
+
|
|
1278
|
+
arr.on('batch', (ops) => batches.push(ops));
|
|
1279
|
+
arr.push(2);
|
|
1280
|
+
arr.clear();
|
|
1281
|
+
arr.unshift(5);
|
|
1282
|
+
flush();
|
|
1283
|
+
|
|
1284
|
+
expect(batches).toEqual([[{ op: 'clear' }, { items: [5], op: 'unshift' }]]);
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
it('reports the order of a sort, and reverse, shift and concat', () => {
|
|
1288
|
+
let arr = new ReactiveArray([3, 1, 2]),
|
|
1289
|
+
batches: unknown[][] = [];
|
|
1290
|
+
|
|
1291
|
+
arr.on('batch', (ops) => batches.push(ops));
|
|
1292
|
+
arr.sort((a, b) => a - b);
|
|
1293
|
+
arr.reverse();
|
|
1294
|
+
arr.shift();
|
|
1295
|
+
arr.concat([9]);
|
|
1296
|
+
flush();
|
|
1297
|
+
|
|
1298
|
+
expect(batches).toEqual([[
|
|
1299
|
+
{ op: 'sort', order: [1, 2, 0] },
|
|
1300
|
+
{ op: 'reverse' },
|
|
1301
|
+
{ item: 3, op: 'shift' },
|
|
1302
|
+
{ items: [9], op: 'concat' }
|
|
1303
|
+
]]);
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
it('runs listeners outside any owner, and stops delivering once the last one leaves', () => {
|
|
1307
|
+
let arr = new ReactiveArray<number>(),
|
|
1308
|
+
seen: unknown[] = [],
|
|
1309
|
+
stop = arr.on('batch', () => seen.push(owner()));
|
|
1310
|
+
|
|
1311
|
+
arr.push(1);
|
|
1312
|
+
flush();
|
|
1313
|
+
stop();
|
|
1314
|
+
arr.push(2);
|
|
1315
|
+
flush();
|
|
1316
|
+
|
|
1317
|
+
expect(seen).toEqual([null]);
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
it('delivers mutations made inside an effect within the same pass', () => {
|
|
1321
|
+
let arr = new ReactiveArray<number>(),
|
|
1322
|
+
batches: unknown[][] = [],
|
|
1323
|
+
s = signal(0);
|
|
1324
|
+
|
|
1325
|
+
arr.on('batch', (ops) => batches.push(ops));
|
|
1326
|
+
|
|
1327
|
+
let stop = root(() => effect(() => {
|
|
1328
|
+
if (read(s)) {
|
|
1329
|
+
arr.push(read(s));
|
|
1330
|
+
}
|
|
1331
|
+
}));
|
|
1332
|
+
|
|
1333
|
+
write(s, 5);
|
|
1334
|
+
flush();
|
|
1335
|
+
stop();
|
|
1336
|
+
|
|
1337
|
+
expect(batches).toEqual([[{ items: [5], op: 'push' }]]);
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
it('keeps delivering to later listeners after the last one throws and is dropped', async () => {
|
|
1341
|
+
pool();
|
|
1342
|
+
|
|
1343
|
+
let arr = new ReactiveArray<number>(),
|
|
1344
|
+
batches: unknown[][] = [],
|
|
1345
|
+
lengths: number[] = [],
|
|
1346
|
+
stop = root(() => effect(() => { lengths.push(arr.$length); }));
|
|
1347
|
+
|
|
1348
|
+
let captured = await captureUncaught(() => {
|
|
1349
|
+
arr.on('batch', () => { throw new Error('reactivity: batch'); });
|
|
1350
|
+
arr.push(1);
|
|
1351
|
+
flush();
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
arr.on('batch', (ops) => batches.push(ops));
|
|
1355
|
+
arr.push(2);
|
|
1356
|
+
flush();
|
|
1357
|
+
stop();
|
|
1358
|
+
|
|
1359
|
+
expect(captured.map((e) => (e as Error).message)).toEqual(['reactivity: batch']);
|
|
1360
|
+
expect(batches).toEqual([[{ items: [2], op: 'push' }]]);
|
|
1361
|
+
expect(lengths).toEqual([0, 1, 2]);
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
it('keeps delivering to later listeners after the last one unsubscribes itself during dispatch', async () => {
|
|
1365
|
+
pool();
|
|
1366
|
+
|
|
1367
|
+
let arr = new ReactiveArray<number>(),
|
|
1368
|
+
batches: unknown[][] = [],
|
|
1369
|
+
first: unknown[][] = [],
|
|
1370
|
+
lengths: number[] = [],
|
|
1371
|
+
stop = root(() => effect(() => { lengths.push(arr.$length); })),
|
|
1372
|
+
unsubscribe = arr.on('batch', (ops) => {
|
|
1373
|
+
first.push(ops);
|
|
1374
|
+
unsubscribe();
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
arr.push(1);
|
|
1378
|
+
flush();
|
|
1379
|
+
await Promise.resolve();
|
|
1380
|
+
|
|
1381
|
+
arr.push(2);
|
|
1382
|
+
flush();
|
|
1383
|
+
|
|
1384
|
+
arr.on('batch', (ops) => batches.push(ops));
|
|
1385
|
+
arr.push(3);
|
|
1386
|
+
flush();
|
|
1387
|
+
stop();
|
|
1388
|
+
|
|
1389
|
+
expect(first).toEqual([[{ items: [1], op: 'push' }]]);
|
|
1390
|
+
expect(batches).toEqual([[{ items: [3], op: 'push' }]]);
|
|
1391
|
+
expect(lengths).toEqual([0, 1, 2, 3]);
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
it('keeps the delivering effect when a listener joins before the stop runs', () => {
|
|
1395
|
+
let arr = new ReactiveArray<number>(),
|
|
1396
|
+
batches: unknown[][] = [],
|
|
1397
|
+
unsubscribe = arr.on('batch', () => {
|
|
1398
|
+
unsubscribe();
|
|
1399
|
+
arr.on('batch', (ops) => batches.push(ops));
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
arr.push(1);
|
|
1403
|
+
flush();
|
|
1404
|
+
arr.push(2);
|
|
1405
|
+
flush();
|
|
1406
|
+
|
|
1407
|
+
expect(batches).toEqual([[{ items: [2], op: 'push' }]]);
|
|
1408
|
+
});
|
|
1409
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { computed, effect, onCleanup, read,
|
|
2
|
+
import { computed, effect, onCleanup, read, signal, write } from '~/system';
|
|
3
3
|
import { SIGNAL } from '~/constants';
|
|
4
|
-
import {
|
|
4
|
+
import { isReactiveObject, ReactiveObject } from '~/reactive/object';
|
|
5
5
|
import reactive from '~/reactive/index';
|
|
6
6
|
import { ReactiveArray } from '~/reactive/array';
|
|
7
7
|
import { tick, waitFor } from '../lib/wait-for';
|
|
@@ -348,7 +348,7 @@ describe('isReactiveObject', () => {
|
|
|
348
348
|
describe('reactive()', () => {
|
|
349
349
|
describe('objects', () => {
|
|
350
350
|
it('creates reactive object', () => {
|
|
351
|
-
let obj = reactive({ name: 'John'
|
|
351
|
+
let obj = reactive({ age: 25, name: 'John' });
|
|
352
352
|
|
|
353
353
|
expect((obj as any).name).toBe('John');
|
|
354
354
|
expect((obj as any).age).toBe(25);
|
|
@@ -515,7 +515,7 @@ describe('integration', () => {
|
|
|
515
515
|
|
|
516
516
|
it('large dependency chain', async () => {
|
|
517
517
|
let s = signal(0),
|
|
518
|
-
chain: ReturnType<typeof computed
|
|
518
|
+
chain: ReturnType<typeof computed<number>>[] = [computed(() => read(s))];
|
|
519
519
|
|
|
520
520
|
for (let i = 1; i < 50; i++) {
|
|
521
521
|
let prev = chain[i - 1];
|
package/test/system.test.ts
CHANGED
|
@@ -584,8 +584,7 @@ describe('root', () => {
|
|
|
584
584
|
stop = dispose;
|
|
585
585
|
|
|
586
586
|
try {
|
|
587
|
-
root((
|
|
588
|
-
inner;
|
|
587
|
+
root((_inner) => {
|
|
589
588
|
throw new Error('reactivity: inner boom');
|
|
590
589
|
});
|
|
591
590
|
}
|
|
@@ -1224,7 +1223,7 @@ describe('edge cases', () => {
|
|
|
1224
1223
|
|
|
1225
1224
|
it('heap auto-resizes for computed chain deeper than 64', async () => {
|
|
1226
1225
|
let s = signal(0),
|
|
1227
|
-
chain: ReturnType<typeof computed
|
|
1226
|
+
chain: ReturnType<typeof computed<number>>[] = [computed(() => read(s) + 1)];
|
|
1228
1227
|
|
|
1229
1228
|
for (let i = 1; i < 80; i++) {
|
|
1230
1229
|
let prev = chain[i - 1];
|
|
@@ -1416,7 +1415,7 @@ describe('edge cases', () => {
|
|
|
1416
1415
|
});
|
|
1417
1416
|
|
|
1418
1417
|
it('link pool handles >1000 dependencies with disposal and reuse', async () => {
|
|
1419
|
-
let signals: ReturnType<typeof signal
|
|
1418
|
+
let signals: ReturnType<typeof signal<number>>[] = [],
|
|
1420
1419
|
stops: (() => void)[] = [];
|
|
1421
1420
|
|
|
1422
1421
|
for (let i = 0; i < 1100; i++) {
|
package/test/untrack.test.ts
CHANGED
package/tsconfig.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
{
|
|
2
|
-
"extends": "@esportsplus/typescript/tsconfig.package.json"
|
|
3
|
-
}
|
|
2
|
+
"extends": "@esportsplus/typescript/tsconfig.package.dev.json"
|
|
3
|
+
}
|
package/vitest.config.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { resolve } from 'path';
|
|
|
5
5
|
export default defineConfig({
|
|
6
6
|
resolve: {
|
|
7
7
|
alias: {
|
|
8
|
-
'
|
|
9
|
-
'
|
|
8
|
+
'@esportsplus/reactivity': resolve(import.meta.dirname, 'src/index.ts'),
|
|
9
|
+
'~': resolve(import.meta.dirname, 'src')
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
12
|
test: {
|
package/bench/tsconfig.json
DELETED