@esportsplus/reactivity 0.33.0 → 0.36.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 +9 -7
- package/bench/reactive/array.bench.ts +17 -17
- package/build/compiler/array.d.ts +2 -2
- package/build/compiler/array.js +12 -54
- package/build/compiler/constants.d.ts +4 -4
- package/build/compiler/constants.js +20 -4
- package/build/compiler/index.js +25 -38
- package/build/compiler/object.d.ts +2 -2
- package/build/compiler/object.js +10 -21
- package/build/compiler/plugins/vite.d.ts +4 -2
- package/build/compiler/primitives.d.ts +6 -2
- package/build/compiler/primitives.js +35 -42
- package/build/compiler/types.d.ts +3 -1
- package/build/constants.d.ts +2 -2
- package/build/constants.js +2 -2
- package/build/reactive/array.d.ts +1 -1
- package/build/reactive/array.js +26 -15
- package/build/reactive/index.js +5 -14
- package/build/reactive/object.js +1 -1
- package/build/system.d.ts +10 -13
- package/build/system.js +105 -83
- package/build/types.d.ts +2 -7
- package/package.json +5 -5
- package/pnpm-workspace.yaml +3 -0
- package/src/compiler/array.ts +14 -64
- package/src/compiler/constants.ts +21 -5
- package/src/compiler/index.ts +39 -70
- package/src/compiler/object.ts +12 -29
- package/src/compiler/primitives.ts +55 -53
- package/src/compiler/types.ts +4 -1
- package/src/constants.ts +4 -4
- package/src/reactive/array.ts +33 -17
- package/src/reactive/index.ts +5 -17
- package/src/reactive/object.ts +1 -1
- package/src/system.ts +138 -101
- package/src/types.ts +2 -9
- package/test/async-computed.test.ts +2 -2
- package/test/compiler/compiler.test.ts +40 -23
- package/test/effects.test.ts +37 -1
- package/test/lib/uncaught.ts +26 -0
- package/test/reactive/array.test.ts +169 -99
- package/test/reactive/nested.test.ts +14 -14
- package/test/reactive/objects.test.ts +1 -1
- package/test/reactive/reactive.test.ts +49 -0
- package/test/system.test.ts +130 -22
package/src/system.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PACKAGE_NAME,
|
|
3
3
|
SIGNAL,
|
|
4
|
-
STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED,
|
|
4
|
+
STABILIZER_DEFERRED, STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED,
|
|
5
5
|
STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NOTIFY_MASK, STATE_RECOMPUTING
|
|
6
6
|
} from './constants';
|
|
7
7
|
import { Computed, ComputedResult, Link, SelectorSignal, Settled, Signal } from './types';
|
|
@@ -28,6 +28,8 @@ let asyncMeta = new WeakMap<Computed<unknown>, { factory: Computed<unknown> }>()
|
|
|
28
28
|
notified = false,
|
|
29
29
|
observer: Computed<unknown> | null = null,
|
|
30
30
|
pendingHead: Signal<unknown> | null = null,
|
|
31
|
+
pulled: Computed<unknown> | null = null,
|
|
32
|
+
puller: Computed<unknown> | null = null,
|
|
31
33
|
scope: Computed<unknown> | null = null,
|
|
32
34
|
stabilizer = STABILIZER_IDLE,
|
|
33
35
|
version = 0,
|
|
@@ -35,6 +37,9 @@ let asyncMeta = new WeakMap<Computed<unknown>, { factory: Computed<unknown> }>()
|
|
|
35
37
|
writes = 0;
|
|
36
38
|
|
|
37
39
|
|
|
40
|
+
function noop() {
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
function walkPop(walk: Walk): Walk | null {
|
|
39
44
|
let prev = walk.prev;
|
|
40
45
|
|
|
@@ -129,8 +134,8 @@ function deleteFromHeap<T>(computed: Computed<T>) {
|
|
|
129
134
|
computed.prevHeap = computed;
|
|
130
135
|
}
|
|
131
136
|
|
|
132
|
-
//
|
|
133
|
-
//
|
|
137
|
+
// N writes to one signal queue it once, so each subscriber is heap-inserted once. A self-linked
|
|
138
|
+
// nextPending marks the tail.
|
|
134
139
|
function drainPending() {
|
|
135
140
|
let node = pendingHead;
|
|
136
141
|
|
|
@@ -180,6 +185,10 @@ function insertIntoHeap<T>(computed: Computed<T>) {
|
|
|
180
185
|
heap.length = Math.max(height + 1, Math.ceil(heap.length * 2));
|
|
181
186
|
}
|
|
182
187
|
}
|
|
188
|
+
// heap_i is only non-zero inside a pass: a bucket it already scanned needs another pass
|
|
189
|
+
else if (height < heap_i) {
|
|
190
|
+
stabilizer = STABILIZER_RESCHEDULE;
|
|
191
|
+
}
|
|
183
192
|
}
|
|
184
193
|
|
|
185
194
|
// https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L52
|
|
@@ -305,7 +314,8 @@ function notify<T>(computed: Computed<T>, newState: number) {
|
|
|
305
314
|
}
|
|
306
315
|
|
|
307
316
|
// Shared by read()'s tracked pull and peek()'s untracked pull. observer is nulled around update()
|
|
308
|
-
// so a recompute triggered here tracks into the node's own scope, never the caller's.
|
|
317
|
+
// so a recompute triggered here tracks into the node's own scope, never the caller's. pulled/puller
|
|
318
|
+
// let propagate() skip re-queueing the caller: it receives the fresh value when this returns.
|
|
309
319
|
function pull<T>(node: Computed<T>): void {
|
|
310
320
|
if (!notified) {
|
|
311
321
|
notified = true;
|
|
@@ -319,16 +329,33 @@ function pull<T>(node: Computed<T>): void {
|
|
|
319
329
|
|
|
320
330
|
let o = observer;
|
|
321
331
|
|
|
332
|
+
// Cleared rather than restored: an outer pull then merely fails to skip, which is only a re-run
|
|
322
333
|
observer = null;
|
|
334
|
+
pulled = node as Computed<unknown>;
|
|
335
|
+
puller = o;
|
|
323
336
|
update(node);
|
|
324
337
|
observer = o;
|
|
338
|
+
pulled = null;
|
|
339
|
+
puller = null;
|
|
325
340
|
}
|
|
326
341
|
|
|
327
342
|
function propagate<T>(computed: Computed<T>) {
|
|
328
|
-
|
|
343
|
+
let c = computed.subs;
|
|
344
|
+
|
|
345
|
+
if (c === null) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let skip = (computed as Computed<unknown>) === pulled ? puller : null;
|
|
350
|
+
|
|
351
|
+
for (; c; c = c.nextSub) {
|
|
329
352
|
let s = c.sub,
|
|
330
353
|
state = s.state;
|
|
331
354
|
|
|
355
|
+
if (s === skip) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
332
359
|
if (state & STATE_CHECK) {
|
|
333
360
|
s.state = state | STATE_DIRTY;
|
|
334
361
|
}
|
|
@@ -339,14 +366,10 @@ function propagate<T>(computed: Computed<T>) {
|
|
|
339
366
|
schedule();
|
|
340
367
|
}
|
|
341
368
|
|
|
342
|
-
function recompute<T>(computed: Computed<T
|
|
343
|
-
if (
|
|
369
|
+
function recompute<T>(computed: Computed<T>) {
|
|
370
|
+
if (computed.state & STATE_IN_HEAP) {
|
|
344
371
|
deleteFromHeap(computed);
|
|
345
372
|
}
|
|
346
|
-
else {
|
|
347
|
-
computed.nextHeap = undefined;
|
|
348
|
-
computed.prevHeap = computed;
|
|
349
|
-
}
|
|
350
373
|
|
|
351
374
|
if (computed.cleanup) {
|
|
352
375
|
// A failing PREVIOUS generation's teardown must not poison this recompute or the stabilize pass
|
|
@@ -387,7 +410,8 @@ function recompute<T>(computed: Computed<T>, del: boolean) {
|
|
|
387
410
|
// Fresh version so rv/link stamps from this run (incl. nested creations) go stale — false negatives only
|
|
388
411
|
version++;
|
|
389
412
|
observer = o;
|
|
390
|
-
|
|
413
|
+
// fn may have re-queued this node (a tracked read drains pending writes), so heap membership survives
|
|
414
|
+
computed.state = STATE_COMPUTED | flags | (computed.state & STATE_IN_HEAP);
|
|
391
415
|
// Entry snapshot, not current writes: a node whose fn wrote mid-run must stay gv < writes and validate normally
|
|
392
416
|
computed.gv = w;
|
|
393
417
|
|
|
@@ -437,61 +461,62 @@ function recompute<T>(computed: Computed<T>, del: boolean) {
|
|
|
437
461
|
propagate(computed);
|
|
438
462
|
}
|
|
439
463
|
}
|
|
464
|
+
|
|
465
|
+
// A schedule() requested while depth > 0 is parked as DEFERRED and queued once the outermost
|
|
466
|
+
// recompute returns
|
|
467
|
+
if (!depth && stabilizer === STABILIZER_DEFERRED) {
|
|
468
|
+
stabilizer = STABILIZER_SCHEDULED;
|
|
469
|
+
microtask(stabilize);
|
|
470
|
+
}
|
|
440
471
|
}
|
|
441
472
|
|
|
442
473
|
function schedule() {
|
|
443
|
-
if (stabilizer
|
|
474
|
+
if (stabilizer !== STABILIZER_IDLE) {
|
|
444
475
|
return;
|
|
445
476
|
}
|
|
446
477
|
|
|
447
|
-
if (
|
|
478
|
+
if (depth) {
|
|
479
|
+
stabilizer = STABILIZER_DEFERRED;
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
448
482
|
stabilizer = STABILIZER_SCHEDULED;
|
|
449
483
|
microtask(stabilize);
|
|
450
484
|
}
|
|
451
|
-
else if (stabilizer === STABILIZER_RUNNING) {
|
|
452
|
-
stabilizer = STABILIZER_RESCHEDULE;
|
|
453
|
-
}
|
|
454
485
|
}
|
|
455
486
|
|
|
487
|
+
// Buckets are popped from the head so a node queued at the current height mid-pass (a write drained
|
|
488
|
+
// by a tracked read) settles in this pass, and the heap stays intact for deleteFromHeap() when a
|
|
489
|
+
// recompute pulls a same-height sibling. A write that lands below heap_i flags RESCHEDULE.
|
|
456
490
|
function stabilize() {
|
|
457
491
|
let o = observer;
|
|
458
492
|
|
|
459
493
|
observer = null;
|
|
460
|
-
stabilizer = STABILIZER_RUNNING;
|
|
461
|
-
|
|
462
|
-
for (heap_i = 0; heap_i <= heap_n; heap_i++) {
|
|
463
|
-
// Drain before scanning each height so writes emitted by a lower level's recompute
|
|
464
|
-
// land their subscribers in this same pass, matching main's eager same-pass pickup
|
|
465
|
-
if (pendingHead !== null) {
|
|
466
|
-
drainPending();
|
|
467
|
-
}
|
|
468
494
|
|
|
469
|
-
|
|
495
|
+
do {
|
|
496
|
+
stabilizer = STABILIZER_RUNNING;
|
|
470
497
|
|
|
471
|
-
|
|
498
|
+
for (heap_i = 0; heap_i <= heap_n; heap_i++) {
|
|
499
|
+
if (pendingHead !== null) {
|
|
500
|
+
drainPending();
|
|
501
|
+
}
|
|
472
502
|
|
|
473
|
-
|
|
474
|
-
let next = computed.nextHeap;
|
|
503
|
+
let computed;
|
|
475
504
|
|
|
476
|
-
|
|
505
|
+
while ((computed = heap[heap_i]) !== undefined) {
|
|
506
|
+
recompute(computed);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
477
509
|
|
|
478
|
-
|
|
510
|
+
while (heap_n > 0 && heap[heap_n] === undefined) {
|
|
511
|
+
heap_n--;
|
|
479
512
|
}
|
|
480
|
-
}
|
|
481
513
|
|
|
482
|
-
|
|
483
|
-
heap_n--;
|
|
514
|
+
heap_i = 0;
|
|
484
515
|
}
|
|
516
|
+
while (pendingHead !== null || stabilizer === STABILIZER_RESCHEDULE);
|
|
485
517
|
|
|
518
|
+
stabilizer = STABILIZER_IDLE;
|
|
486
519
|
observer = o;
|
|
487
|
-
|
|
488
|
-
if (stabilizer === STABILIZER_RESCHEDULE) {
|
|
489
|
-
stabilizer = STABILIZER_SCHEDULED;
|
|
490
|
-
microtask(stabilize);
|
|
491
|
-
}
|
|
492
|
-
else {
|
|
493
|
-
stabilizer = STABILIZER_IDLE;
|
|
494
|
-
}
|
|
495
520
|
}
|
|
496
521
|
|
|
497
522
|
// https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L100
|
|
@@ -555,7 +580,7 @@ function update<T>(root: Computed<T>): void {
|
|
|
555
580
|
resuming = true;
|
|
556
581
|
}
|
|
557
582
|
else if (node.state & STATE_DIRTY) {
|
|
558
|
-
recompute(node
|
|
583
|
+
recompute(node);
|
|
559
584
|
node.state &= ~STATE_NOTIFY_MASK;
|
|
560
585
|
}
|
|
561
586
|
else {
|
|
@@ -585,7 +610,7 @@ function update<T>(root: Computed<T>): void {
|
|
|
585
610
|
}
|
|
586
611
|
|
|
587
612
|
if (node.state & STATE_DIRTY) {
|
|
588
|
-
recompute(node
|
|
613
|
+
recompute(node);
|
|
589
614
|
}
|
|
590
615
|
else {
|
|
591
616
|
node.gv = w;
|
|
@@ -617,24 +642,33 @@ function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> |
|
|
|
617
642
|
v = 0;
|
|
618
643
|
|
|
619
644
|
let stop = effect(() => {
|
|
620
|
-
let
|
|
621
|
-
|
|
645
|
+
let id = ++v,
|
|
646
|
+
// A settle is stale once a newer dispatch exists or the factory has a re-run queued. Pending
|
|
647
|
+
// writes are drained first so a write that dirtied the factory but has not been settled yet
|
|
648
|
+
// still counts, whatever the microtask order.
|
|
649
|
+
stale = () => {
|
|
650
|
+
if (pendingHead !== null) {
|
|
651
|
+
drainPending();
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return id !== v || (factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK)) !== 0;
|
|
655
|
+
},
|
|
656
|
+
fail = (e: unknown) => {
|
|
657
|
+
if (!stale()) {
|
|
622
658
|
write(error, e === undefined ? new Error('reactivity: async computed rejected with undefined') : e);
|
|
623
659
|
write(pending, false);
|
|
624
660
|
}
|
|
625
661
|
},
|
|
626
|
-
id = ++v,
|
|
627
|
-
// Heap membership (a write's eager insert) marks a pending re-run the notify mask alone misses.
|
|
628
662
|
result = read(factory);
|
|
629
663
|
|
|
630
664
|
if (isPromise(result)) {
|
|
631
|
-
if (
|
|
665
|
+
if (!stale()) {
|
|
632
666
|
write(pending, true);
|
|
633
667
|
}
|
|
634
668
|
|
|
635
669
|
(result as Promise<T>).then(
|
|
636
670
|
(value) => {
|
|
637
|
-
if (
|
|
671
|
+
if (!stale()) {
|
|
638
672
|
write(error, undefined);
|
|
639
673
|
write(node, value);
|
|
640
674
|
write(pending, false);
|
|
@@ -651,7 +685,7 @@ function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> |
|
|
|
651
685
|
});
|
|
652
686
|
|
|
653
687
|
let step = (r: IteratorResult<T>) => {
|
|
654
|
-
if (
|
|
688
|
+
if (stale()) {
|
|
655
689
|
return;
|
|
656
690
|
}
|
|
657
691
|
|
|
@@ -666,7 +700,7 @@ function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> |
|
|
|
666
700
|
}
|
|
667
701
|
};
|
|
668
702
|
|
|
669
|
-
if (
|
|
703
|
+
if (!stale()) {
|
|
670
704
|
write(pending, true);
|
|
671
705
|
}
|
|
672
706
|
|
|
@@ -689,7 +723,7 @@ function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> |
|
|
|
689
723
|
return read(node);
|
|
690
724
|
});
|
|
691
725
|
|
|
692
|
-
|
|
726
|
+
wrapper.pending = pending;
|
|
693
727
|
|
|
694
728
|
asyncMeta.set(wrapper as Computed<unknown>, { factory: factory as Computed<unknown> });
|
|
695
729
|
wrapper.disposal = stop;
|
|
@@ -698,38 +732,19 @@ function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> |
|
|
|
698
732
|
}
|
|
699
733
|
|
|
700
734
|
function makeComputed<T>(fn: Computed<T>['fn'], eager: boolean = false): Computed<T> {
|
|
701
|
-
let self
|
|
702
|
-
cleanup: null,
|
|
703
|
-
deps: null,
|
|
704
|
-
depsTail: null,
|
|
705
|
-
disposal: null,
|
|
706
|
-
equals: null,
|
|
707
|
-
error: null,
|
|
708
|
-
fn: fn,
|
|
709
|
-
gv: 0,
|
|
710
|
-
height: 0,
|
|
711
|
-
nextHeap: undefined,
|
|
712
|
-
prevHeap: null as unknown as Computed<unknown>,
|
|
713
|
-
rv: 0,
|
|
714
|
-
state: STATE_COMPUTED,
|
|
715
|
-
subs: null,
|
|
716
|
-
subsTail: null,
|
|
717
|
-
value: undefined as T,
|
|
718
|
-
};
|
|
719
|
-
|
|
720
|
-
self.prevHeap = self;
|
|
735
|
+
let self = makeNode(fn);
|
|
721
736
|
|
|
722
737
|
if (observer) {
|
|
723
738
|
if (observer.depsTail === null) {
|
|
724
739
|
self.height = observer.height;
|
|
725
|
-
recompute(self
|
|
740
|
+
recompute(self);
|
|
726
741
|
}
|
|
727
742
|
else if (eager) {
|
|
728
743
|
// computed() must know fn's return type to pick sync vs async. This probe runs BEFORE
|
|
729
744
|
// link() below, so self has no subs yet — recompute's propagate is a no-op and cannot
|
|
730
745
|
// re-run the parent. Deferring here (as effect() still does) would leave value unset.
|
|
731
746
|
self.height = observer.height + 1;
|
|
732
|
-
recompute(self
|
|
747
|
+
recompute(self);
|
|
733
748
|
}
|
|
734
749
|
else {
|
|
735
750
|
self.height = observer.height + 1;
|
|
@@ -741,8 +756,7 @@ function makeComputed<T>(fn: Computed<T>['fn'], eager: boolean = false): Compute
|
|
|
741
756
|
onCleanup(() => dispose(self));
|
|
742
757
|
}
|
|
743
758
|
else {
|
|
744
|
-
recompute(self
|
|
745
|
-
root.disposables++;
|
|
759
|
+
recompute(self);
|
|
746
760
|
|
|
747
761
|
if (scope) {
|
|
748
762
|
onCleanup(() => dispose(self));
|
|
@@ -752,6 +766,34 @@ function makeComputed<T>(fn: Computed<T>['fn'], eager: boolean = false): Compute
|
|
|
752
766
|
return self;
|
|
753
767
|
}
|
|
754
768
|
|
|
769
|
+
// Every node, including root()'s cleanup-only scope, is built here so dispose() and onCleanup()
|
|
770
|
+
// only ever see one hidden class.
|
|
771
|
+
function makeNode<T>(fn: Computed<T>['fn']): Computed<T> {
|
|
772
|
+
let self: Computed<T> = {
|
|
773
|
+
cleanup: null,
|
|
774
|
+
deps: null,
|
|
775
|
+
depsTail: null,
|
|
776
|
+
disposal: null,
|
|
777
|
+
equals: null,
|
|
778
|
+
error: null,
|
|
779
|
+
fn,
|
|
780
|
+
gv: 0,
|
|
781
|
+
height: 0,
|
|
782
|
+
nextHeap: undefined,
|
|
783
|
+
pending: null,
|
|
784
|
+
prevHeap: null as unknown as Computed<unknown>,
|
|
785
|
+
rv: 0,
|
|
786
|
+
state: STATE_COMPUTED,
|
|
787
|
+
subs: null,
|
|
788
|
+
subsTail: null,
|
|
789
|
+
value: undefined as T,
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
self.prevHeap = self;
|
|
793
|
+
|
|
794
|
+
return self;
|
|
795
|
+
}
|
|
796
|
+
|
|
755
797
|
// Reuses the same recompute-nesting counter schedule() consults, so writes inside fn defer
|
|
756
798
|
// scheduling until fn returns; pair with flush() for a synchronous transaction.
|
|
757
799
|
const batch = <T>(fn: () => T): T => {
|
|
@@ -763,8 +805,9 @@ const batch = <T>(fn: () => T): T => {
|
|
|
763
805
|
finally {
|
|
764
806
|
depth--;
|
|
765
807
|
|
|
766
|
-
if (!depth) {
|
|
767
|
-
|
|
808
|
+
if (!depth && stabilizer === STABILIZER_DEFERRED) {
|
|
809
|
+
stabilizer = STABILIZER_SCHEDULED;
|
|
810
|
+
microtask(stabilize);
|
|
768
811
|
}
|
|
769
812
|
}
|
|
770
813
|
};
|
|
@@ -896,13 +939,9 @@ const effect = <T>(fn: Computed<T>['fn'], apply?: (value: T, prev: T | undefined
|
|
|
896
939
|
};
|
|
897
940
|
};
|
|
898
941
|
|
|
899
|
-
//
|
|
900
|
-
// re-entering stabilize here would corrupt heap_i, so this is a deliberate no-op.
|
|
901
|
-
// Loops (not a single call): a write during a pass can target a height stabilize()'s current
|
|
902
|
-
// pass already scanned past, which only flips stabilizer to RESCHEDULE for the *next* microtask
|
|
903
|
-
// rather than draining in-pass — looping here is what actually settles that tail synchronously.
|
|
942
|
+
// A no-op while a pass is running: re-entering stabilize() would corrupt heap_i.
|
|
904
943
|
const flush = (): void => {
|
|
905
|
-
|
|
944
|
+
if (stabilizer === STABILIZER_SCHEDULED) {
|
|
906
945
|
stabilize();
|
|
907
946
|
}
|
|
908
947
|
};
|
|
@@ -958,8 +997,8 @@ const read = <T>(node: Signal<T> | Computed<T>): T => {
|
|
|
958
997
|
link(node, observer);
|
|
959
998
|
|
|
960
999
|
if ((node as Computed<unknown>).state & STATE_COMPUTED) {
|
|
961
|
-
//
|
|
962
|
-
//
|
|
1000
|
+
// A tracked read must see pending writes: drain so the heap and this node's notify
|
|
1001
|
+
// bits reflect them before the pull gate below is evaluated
|
|
963
1002
|
if (pendingHead !== null) {
|
|
964
1003
|
drainPending();
|
|
965
1004
|
}
|
|
@@ -985,7 +1024,6 @@ const read = <T>(node: Signal<T> | Computed<T>): T => {
|
|
|
985
1024
|
|
|
986
1025
|
const root = <T>(fn: ((dispose: VoidFunction) => T) | (() => T)) => {
|
|
987
1026
|
let c,
|
|
988
|
-
d = root.disposables,
|
|
989
1027
|
o = observer,
|
|
990
1028
|
s = scope,
|
|
991
1029
|
self: Computed<unknown> | null = null,
|
|
@@ -993,21 +1031,22 @@ const root = <T>(fn: ((dispose: VoidFunction) => T) | (() => T)) => {
|
|
|
993
1031
|
value: T;
|
|
994
1032
|
|
|
995
1033
|
observer = null;
|
|
996
|
-
root.disposables = 0;
|
|
997
1034
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1035
|
+
try {
|
|
1036
|
+
if (tracking) {
|
|
1037
|
+
scope = self = makeNode(noop);
|
|
1038
|
+
value = (fn as (dispose: VoidFunction) => T)(c = () => dispose(self!));
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
scope = null;
|
|
1042
|
+
value = (fn as () => T)();
|
|
1043
|
+
}
|
|
1001
1044
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1045
|
+
finally {
|
|
1046
|
+
observer = o;
|
|
1047
|
+
scope = s;
|
|
1005
1048
|
}
|
|
1006
1049
|
|
|
1007
|
-
observer = o;
|
|
1008
|
-
root.disposables = d;
|
|
1009
|
-
scope = s;
|
|
1010
|
-
|
|
1011
1050
|
if (c) {
|
|
1012
1051
|
onCleanup(c);
|
|
1013
1052
|
}
|
|
@@ -1015,8 +1054,6 @@ const root = <T>(fn: ((dispose: VoidFunction) => T) | (() => T)) => {
|
|
|
1015
1054
|
return value;
|
|
1016
1055
|
};
|
|
1017
1056
|
|
|
1018
|
-
root.disposables = 0;
|
|
1019
|
-
|
|
1020
1057
|
const signal = <T>(value: T, equals: ((a: T, b: T) => boolean) | null = null): Signal<T> => {
|
|
1021
1058
|
return {
|
|
1022
1059
|
equals: equals as ((a: unknown, b: unknown) => boolean) | null,
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { ts } from '@esportsplus/typescript';
|
|
2
1
|
import { SIGNAL } from './constants';
|
|
3
2
|
import { ReactiveArray } from './reactive';
|
|
4
3
|
|
|
@@ -14,6 +13,7 @@ interface Computed<T> {
|
|
|
14
13
|
gv: number;
|
|
15
14
|
height: number;
|
|
16
15
|
nextHeap: Computed<unknown> | undefined;
|
|
16
|
+
pending: Signal<boolean> | null;
|
|
17
17
|
prevHeap: Computed<unknown>;
|
|
18
18
|
rv: number;
|
|
19
19
|
state: number;
|
|
@@ -73,12 +73,6 @@ type Signal<T> = {
|
|
|
73
73
|
value: T;
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
-
interface TransformResult {
|
|
77
|
-
changed: boolean;
|
|
78
|
-
code: string;
|
|
79
|
-
sourceFile: ts.SourceFile;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
76
|
|
|
83
77
|
export type {
|
|
84
78
|
Computed,
|
|
@@ -87,6 +81,5 @@ export type {
|
|
|
87
81
|
Reactive,
|
|
88
82
|
SelectorSignal,
|
|
89
83
|
Settled,
|
|
90
|
-
Signal
|
|
91
|
-
TransformResult
|
|
84
|
+
Signal
|
|
92
85
|
};
|
|
@@ -373,11 +373,11 @@ describe('asyncComputed', () => {
|
|
|
373
373
|
expect(() => read(node)).toThrow('boom');
|
|
374
374
|
});
|
|
375
375
|
|
|
376
|
-
it('sync computed has
|
|
376
|
+
it('sync computed has a null pending', () => {
|
|
377
377
|
root(() => {
|
|
378
378
|
let node = computed(() => 42);
|
|
379
379
|
|
|
380
|
-
expect(
|
|
380
|
+
expect(node.pending).toBeNull();
|
|
381
381
|
});
|
|
382
382
|
});
|
|
383
383
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { ts } from '@esportsplus/typescript';
|
|
3
|
+
import { languageService } from '@esportsplus/typescript/compiler';
|
|
3
4
|
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
4
|
-
import { NAMESPACE } from '~/compiler/constants';
|
|
5
|
+
import { NAMESPACE, TYPES } from '~/compiler/constants';
|
|
5
6
|
import type { Bindings } from '~/compiler/types';
|
|
6
7
|
import array from '~/compiler/array';
|
|
7
8
|
import object from '~/compiler/object';
|
|
@@ -25,26 +26,26 @@ function applyIntents(code: string, sourceFile: ts.SourceFile, intents: Replacem
|
|
|
25
26
|
return code;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
function isReactiveCall(node: ts.Node):
|
|
29
|
+
function isReactiveCall(node: ts.Node): node is ts.CallExpression {
|
|
29
30
|
return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'reactive';
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
function parse(code: string): ts.SourceFile {
|
|
33
|
-
return
|
|
34
|
+
return languageService.parse(process.cwd() + '/test.ts', code);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function transformPrimitives(code: string): { bindings: Bindings; output: string } {
|
|
37
38
|
let bindings: Bindings = new Map(),
|
|
38
39
|
sourceFile = parse(code),
|
|
39
|
-
|
|
40
|
+
{ replacements } = primitives(sourceFile, bindings, isReactiveCall);
|
|
40
41
|
|
|
41
|
-
return { bindings, output: applyIntents(code, sourceFile,
|
|
42
|
+
return { bindings, output: applyIntents(code, sourceFile, replacements) };
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
function transformArray(code: string, bindings?: Bindings): { bindings: Bindings; output: string } {
|
|
45
46
|
let b: Bindings = bindings ?? new Map(),
|
|
46
47
|
sourceFile = parse(code),
|
|
47
|
-
intents = array(sourceFile, b,
|
|
48
|
+
intents = array(sourceFile, b, isReactiveCall);
|
|
48
49
|
|
|
49
50
|
return { bindings: b, output: applyIntents(code, sourceFile, intents) };
|
|
50
51
|
}
|
|
@@ -52,7 +53,7 @@ function transformArray(code: string, bindings?: Bindings): { bindings: Bindings
|
|
|
52
53
|
function transformObject(code: string): { bindings: Bindings; output: string; prepend: string[] } {
|
|
53
54
|
let bindings: Bindings = new Map(),
|
|
54
55
|
sourceFile = parse(code),
|
|
55
|
-
result = object(sourceFile, bindings,
|
|
56
|
+
result = object(sourceFile, bindings, isReactiveCall);
|
|
56
57
|
|
|
57
58
|
return {
|
|
58
59
|
bindings,
|
|
@@ -127,15 +128,28 @@ describe('primitives transform', () => {
|
|
|
127
128
|
it('tracks bindings for signal type', () => {
|
|
128
129
|
let { bindings } = transformPrimitives('let x = reactive(0);');
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
expect(bindings.get('x')).toBe(3);
|
|
131
|
+
expect(bindings.get('x')).toBe(TYPES.Signal);
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
it('tracks bindings for computed type', () => {
|
|
135
135
|
let { bindings } = transformPrimitives('let x = reactive(0); let d = reactive(() => x * 2);');
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
expect(bindings.get('d')).toBe(TYPES.Computed);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('leaves a same-named plain variable in a sibling function untouched', () => {
|
|
141
|
+
let { output } = transformPrimitives('function a() { let x = reactive(0); return x; }\nfunction b() { let x = 1; return x; }');
|
|
142
|
+
|
|
143
|
+
expect(output).toContain(`function a() { let x = ${NAMESPACE}.signal(0); return ${NAMESPACE}.read(x); }`);
|
|
144
|
+
expect(output).toContain('function b() { let x = 1; return x; }');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('resolves a shadowing inner binding to the innermost declaration', () => {
|
|
148
|
+
let { output } = transformPrimitives('let x = reactive(0); function f() { let x = reactive(() => 1); x = 2; }');
|
|
149
|
+
|
|
150
|
+
expect(output).toContain(`let x = ${NAMESPACE}.signal(0)`);
|
|
151
|
+
expect(output).toContain(`let x = ${NAMESPACE}.computed(() => 1)`);
|
|
152
|
+
expect(output).not.toContain(`${NAMESPACE}.write(x, 2)`);
|
|
139
153
|
});
|
|
140
154
|
|
|
141
155
|
it('transforms prefix --x in statement', () => {
|
|
@@ -205,18 +219,10 @@ describe('object transform', () => {
|
|
|
205
219
|
expect(output).toContain('<MyType>');
|
|
206
220
|
});
|
|
207
221
|
|
|
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
222
|
it('tracks nested array bindings', () => {
|
|
216
223
|
let { bindings } = transformObject('let obj = reactive({ items: [1, 2, 3] });');
|
|
217
224
|
|
|
218
|
-
|
|
219
|
-
expect(bindings.get('obj.items')).toBe(0);
|
|
225
|
+
expect(bindings.get('obj.items')).toBe(TYPES.Array);
|
|
220
226
|
});
|
|
221
227
|
});
|
|
222
228
|
|
|
@@ -226,7 +232,7 @@ describe('array transform', () => {
|
|
|
226
232
|
let { output } = transformArray('let arr = reactive([1, 2, 3]);');
|
|
227
233
|
|
|
228
234
|
expect(output).toContain(`new ${NAMESPACE}.ReactiveArray`);
|
|
229
|
-
expect(output).toContain('
|
|
235
|
+
expect(output).toContain('([1, 2, 3])');
|
|
230
236
|
});
|
|
231
237
|
|
|
232
238
|
it('transforms reactive([] as Type[]) to typed ReactiveArray', () => {
|
|
@@ -262,6 +268,18 @@ describe('array transform', () => {
|
|
|
262
268
|
expect(output).toContain('arr.$length = arr.length + 3');
|
|
263
269
|
});
|
|
264
270
|
|
|
271
|
+
it('transforms every compound operator on arr.length with its own token', () => {
|
|
272
|
+
let bindings: Bindings = new Map();
|
|
273
|
+
|
|
274
|
+
bindings.set('arr', 0);
|
|
275
|
+
|
|
276
|
+
for (let op of ['<<', '>>', '>>>', '??', '||', '&&', '-', '*', '/', '%', '**', '&', '|', '^']) {
|
|
277
|
+
let { output } = transformArray(`arr.length ${op}= 2;`, bindings);
|
|
278
|
+
|
|
279
|
+
expect(output).toContain(`arr.$length = arr.length ${op} 2`);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
265
283
|
it('transforms arr[i] = value to arr.$set(i, value)', () => {
|
|
266
284
|
let bindings: Bindings = new Map();
|
|
267
285
|
|
|
@@ -275,8 +293,7 @@ describe('array transform', () => {
|
|
|
275
293
|
it('tracks reactive array binding from reactive call', () => {
|
|
276
294
|
let { bindings } = transformArray('let arr = reactive([1, 2, 3]);');
|
|
277
295
|
|
|
278
|
-
|
|
279
|
-
expect(bindings.get('arr')).toBe(0);
|
|
296
|
+
expect(bindings.get('arr')).toBe(TYPES.Array);
|
|
280
297
|
});
|
|
281
298
|
|
|
282
299
|
it('tracks alias binding from reactive array', () => {
|