@esportsplus/reactivity 0.32.0 → 0.34.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 +30 -0
- package/build/compiler/array.d.ts +1 -1
- package/build/compiler/array.js +2 -2
- package/build/compiler/index.js +2 -2
- package/build/compiler/object.d.ts +1 -1
- package/build/compiler/object.js +5 -5
- package/build/compiler/plugins/vite.d.ts +4 -2
- package/build/compiler/primitives.d.ts +1 -1
- package/build/compiler/primitives.js +4 -4
- package/build/reactive/array.d.ts +3 -2
- package/build/reactive/array.js +36 -28
- package/build/reactive/object.d.ts +1 -1
- package/build/system.d.ts +14 -14
- package/build/system.js +26 -9
- package/build/types.d.ts +7 -1
- package/package.json +6 -6
- package/pnpm-workspace.yaml +2 -0
- package/src/compiler/array.ts +5 -5
- package/src/compiler/index.ts +6 -6
- package/src/compiler/object.ts +8 -8
- package/src/compiler/primitives.ts +4 -4
- package/src/reactive/array.ts +51 -39
- package/src/system.ts +35 -12
- package/src/types.ts +9 -0
- package/test/async-computed.test.ts +87 -1
- package/test/compiler/compiler.test.ts +2 -1
- package/test/effects.test.ts +62 -0
- package/test/errors.test.ts +6 -6
- package/test/system.test.ts +10 -10
- package/.claude/CHANGELOG.md +0 -43
package/src/compiler/object.ts
CHANGED
|
@@ -28,7 +28,7 @@ interface ReactiveObjectCall {
|
|
|
28
28
|
interface VisitContext {
|
|
29
29
|
bindings: Bindings;
|
|
30
30
|
calls: ReactiveObjectCall[];
|
|
31
|
-
checker: ts.
|
|
31
|
+
checker: ts.Checker | undefined;
|
|
32
32
|
sourceFile: ts.SourceFile;
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -51,11 +51,11 @@ function analyzeProperty(prop: ts.ObjectLiteralElementLike, sourceFile: ts.Sourc
|
|
|
51
51
|
value = unwrapped,
|
|
52
52
|
valueText = value.getText(sourceFile);
|
|
53
53
|
|
|
54
|
-
while (ts.isAsExpression(unwrapped) || ts.
|
|
54
|
+
while (ts.isAsExpression(unwrapped) || ts.isTypeAssertion(unwrapped) || ts.isParenthesizedExpression(unwrapped)) {
|
|
55
55
|
unwrapped = unwrapped.expression;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
if (ts.isAsExpression(value) || ts.
|
|
58
|
+
if (ts.isAsExpression(value) || ts.isTypeAssertion(value)) {
|
|
59
59
|
let type = (value as ts.AsExpression).type;
|
|
60
60
|
|
|
61
61
|
if (
|
|
@@ -216,7 +216,7 @@ function isStaticValue(node: ts.Node): boolean {
|
|
|
216
216
|
(ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand));
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
function isReactiveCall(checker: ts.
|
|
219
|
+
function isReactiveCall(checker: ts.Checker | undefined, node: ts.Node): node is ts.CallExpression {
|
|
220
220
|
if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) {
|
|
221
221
|
return false;
|
|
222
222
|
}
|
|
@@ -250,14 +250,14 @@ function visit(ctx: VisitContext, node: ts.Node): void {
|
|
|
250
250
|
let prop = props[i];
|
|
251
251
|
|
|
252
252
|
if (ts.isSpreadAssignment(prop)) {
|
|
253
|
-
|
|
253
|
+
node.forEachChild(n => visit(ctx, n));
|
|
254
254
|
return;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
let analyzed = analyzeProperty(prop, ctx.sourceFile);
|
|
258
258
|
|
|
259
259
|
if (!analyzed) {
|
|
260
|
-
|
|
260
|
+
node.forEachChild(n => visit(ctx, n));
|
|
261
261
|
return;
|
|
262
262
|
}
|
|
263
263
|
|
|
@@ -278,11 +278,11 @@ function visit(ctx: VisitContext, node: ts.Node): void {
|
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
|
|
281
|
+
node.forEachChild(n => visit(ctx, n));
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
|
|
285
|
-
export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.
|
|
285
|
+
export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.Checker): ObjectTransformResult => {
|
|
286
286
|
let ctx: VisitContext = {
|
|
287
287
|
bindings,
|
|
288
288
|
calls: [],
|
|
@@ -67,7 +67,7 @@ function visit(ctx: TransformContext, node: ts.Node): void {
|
|
|
67
67
|
else {
|
|
68
68
|
let unwrapped = arg;
|
|
69
69
|
|
|
70
|
-
while (ts.isAsExpression(unwrapped) || ts.isParenthesizedExpression(unwrapped) || ts.
|
|
70
|
+
while (ts.isAsExpression(unwrapped) || ts.isParenthesizedExpression(unwrapped) || ts.isTypeAssertion(unwrapped)) {
|
|
71
71
|
unwrapped = unwrapped.expression;
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -80,7 +80,7 @@ function visit(ctx: TransformContext, node: ts.Node): void {
|
|
|
80
80
|
generate: () => `${NAMESPACE}.reactive`,
|
|
81
81
|
node: call.expression
|
|
82
82
|
});
|
|
83
|
-
|
|
83
|
+
node.forEachChild(n => visit(ctx, n));
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -148,7 +148,7 @@ function visit(ctx: TransformContext, node: ts.Node): void {
|
|
|
148
148
|
!(ts.isVariableDeclaration(node.parent) && node.parent.name === node)
|
|
149
149
|
) {
|
|
150
150
|
if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
|
|
151
|
-
|
|
151
|
+
node.forEachChild(n => visit(ctx, n));
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -251,7 +251,7 @@ function visit(ctx: TransformContext, node: ts.Node): void {
|
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
|
|
254
|
+
node.forEachChild(n => visit(ctx, n));
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
|
package/src/reactive/array.ts
CHANGED
|
@@ -54,7 +54,10 @@ function dispose(value: unknown) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
// Derived arrays (splice removals, map/filter/slice) must be plain Arrays: species-creating a
|
|
58
|
+
// ReactiveArray runs the reactive constructor per call and seeds _length from the length argument.
|
|
57
59
|
class ReactiveArray<T> extends Array<T> {
|
|
60
|
+
|
|
58
61
|
private _length: Signal<number>;
|
|
59
62
|
|
|
60
63
|
listeners: Listeners<T> = {};
|
|
@@ -66,6 +69,24 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
|
|
72
|
+
static get [Symbol.species]() {
|
|
73
|
+
return Array;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
get $length() {
|
|
78
|
+
return read(this._length);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
set $length(value: number) {
|
|
82
|
+
if (value > this.length) {
|
|
83
|
+
throw Error(`@esportsplus/reactivity: cannot set length to a value larger than the current length, use splice instead.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.splice(value, this.length);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
69
90
|
$set(i: number, value: T) {
|
|
70
91
|
let prev = this[i];
|
|
71
92
|
|
|
@@ -204,7 +225,6 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
204
225
|
if (item !== undefined) {
|
|
205
226
|
dispose(item);
|
|
206
227
|
write(this._length, this.length);
|
|
207
|
-
|
|
208
228
|
this.dispatch('pop', { item });
|
|
209
229
|
}
|
|
210
230
|
|
|
@@ -219,6 +239,7 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
219
239
|
let length = super.push(...items);
|
|
220
240
|
|
|
221
241
|
write(this._length, length);
|
|
242
|
+
|
|
222
243
|
this.dispatch('push', { items });
|
|
223
244
|
|
|
224
245
|
return length;
|
|
@@ -237,7 +258,6 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
237
258
|
if (item !== undefined) {
|
|
238
259
|
dispose(item);
|
|
239
260
|
write(this._length, this.length);
|
|
240
|
-
|
|
241
261
|
this.dispatch('shift', { item });
|
|
242
262
|
}
|
|
243
263
|
|
|
@@ -245,41 +265,46 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
245
265
|
}
|
|
246
266
|
|
|
247
267
|
sort(fn?: (a: T, b: T) => number) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
for (let i = 0; i < n; i++) {
|
|
252
|
-
before[i] = this[i];
|
|
268
|
+
if (this.listeners.sort === undefined) {
|
|
269
|
+
super.sort(fn);
|
|
253
270
|
}
|
|
271
|
+
else {
|
|
272
|
+
let n = this.length,
|
|
273
|
+
before = new Array(n) as T[];
|
|
254
274
|
|
|
255
|
-
|
|
275
|
+
for (let i = 0; i < n; i++) {
|
|
276
|
+
before[i] = this[i];
|
|
277
|
+
}
|
|
256
278
|
|
|
257
|
-
|
|
258
|
-
order = new Array(n);
|
|
279
|
+
super.sort(fn);
|
|
259
280
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
list = buckets.get(value);
|
|
281
|
+
let buckets = new Map<T, number[]>(),
|
|
282
|
+
order = new Array(n);
|
|
263
283
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
list
|
|
284
|
+
for (let i = 0; i < n; i++) {
|
|
285
|
+
let value = before[i],
|
|
286
|
+
list = buckets.get(value);
|
|
287
|
+
|
|
288
|
+
if (!list) {
|
|
289
|
+
buckets.set(value, [i]);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
list.push(i);
|
|
293
|
+
}
|
|
269
294
|
}
|
|
270
|
-
}
|
|
271
295
|
|
|
272
|
-
|
|
273
|
-
|
|
296
|
+
for (let i = 0; i < n; i++) {
|
|
297
|
+
let list = buckets.get(this[i])!;
|
|
274
298
|
|
|
275
|
-
|
|
299
|
+
order[i] = list.length === 1 ? list[0] : list[list.length - 1];
|
|
276
300
|
|
|
277
|
-
|
|
278
|
-
|
|
301
|
+
if (list.length > 1) {
|
|
302
|
+
list.pop();
|
|
303
|
+
}
|
|
279
304
|
}
|
|
280
|
-
}
|
|
281
305
|
|
|
282
|
-
|
|
306
|
+
this.dispatch('sort', { order });
|
|
307
|
+
}
|
|
283
308
|
|
|
284
309
|
return this;
|
|
285
310
|
}
|
|
@@ -312,19 +337,6 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
312
337
|
|
|
313
338
|
return length;
|
|
314
339
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
get $length() {
|
|
318
|
-
return read(this._length);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
set $length(value: number) {
|
|
322
|
-
if (value > this.length) {
|
|
323
|
-
throw Error(`@esportsplus/reactivity: cannot set length to a value larger than the current length, use splice instead.`);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
this.splice(value, this.length);
|
|
327
|
-
}
|
|
328
340
|
}
|
|
329
341
|
|
|
330
342
|
Object.defineProperty(ReactiveArray.prototype, REACTIVE_ARRAY, { value: true });
|
package/src/system.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
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
|
-
import { Computed, Link, SelectorSignal, Settled, Signal } from './types';
|
|
7
|
+
import { Computed, ComputedResult, Link, SelectorSignal, Settled, Signal } from './types';
|
|
8
8
|
import { isObject, isPromise } from '@esportsplus/utilities';
|
|
9
9
|
|
|
10
10
|
|
|
@@ -613,12 +613,14 @@ function update<T>(root: Computed<T>): void {
|
|
|
613
613
|
function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> | T>): Computed<T | undefined> {
|
|
614
614
|
let error = signal<unknown>(undefined),
|
|
615
615
|
node = signal<T | undefined>(undefined),
|
|
616
|
+
pending = signal(false),
|
|
616
617
|
v = 0;
|
|
617
618
|
|
|
618
619
|
let stop = effect(() => {
|
|
619
620
|
let fail = (e: unknown) => {
|
|
620
621
|
if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
|
|
621
622
|
write(error, e === undefined ? new Error('reactivity: async computed rejected with undefined') : e);
|
|
623
|
+
write(pending, false);
|
|
622
624
|
}
|
|
623
625
|
},
|
|
624
626
|
id = ++v,
|
|
@@ -626,11 +628,16 @@ function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> |
|
|
|
626
628
|
result = read(factory);
|
|
627
629
|
|
|
628
630
|
if (isPromise(result)) {
|
|
631
|
+
if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
|
|
632
|
+
write(pending, true);
|
|
633
|
+
}
|
|
634
|
+
|
|
629
635
|
(result as Promise<T>).then(
|
|
630
636
|
(value) => {
|
|
631
637
|
if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
|
|
632
638
|
write(error, undefined);
|
|
633
639
|
write(node, value);
|
|
640
|
+
write(pending, false);
|
|
634
641
|
}
|
|
635
642
|
},
|
|
636
643
|
fail
|
|
@@ -651,15 +658,24 @@ function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> |
|
|
|
651
658
|
if (!r.done) {
|
|
652
659
|
write(error, undefined);
|
|
653
660
|
write(node, r.value);
|
|
661
|
+
write(pending, false);
|
|
654
662
|
it.next().then(step, fail);
|
|
655
663
|
}
|
|
664
|
+
else {
|
|
665
|
+
write(pending, false);
|
|
666
|
+
}
|
|
656
667
|
};
|
|
657
668
|
|
|
669
|
+
if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
|
|
670
|
+
write(pending, true);
|
|
671
|
+
}
|
|
672
|
+
|
|
658
673
|
untrack(() => it.next()).then(step, fail);
|
|
659
674
|
}
|
|
660
675
|
else {
|
|
661
676
|
write(error, undefined);
|
|
662
677
|
write(node, result as T);
|
|
678
|
+
write(pending, false);
|
|
663
679
|
}
|
|
664
680
|
});
|
|
665
681
|
|
|
@@ -673,6 +689,8 @@ function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> |
|
|
|
673
689
|
return read(node);
|
|
674
690
|
});
|
|
675
691
|
|
|
692
|
+
(wrapper as Computed<T | undefined> & { pending: Signal<boolean> }).pending = pending;
|
|
693
|
+
|
|
676
694
|
asyncMeta.set(wrapper as Computed<unknown>, { factory: factory as Computed<unknown> });
|
|
677
695
|
wrapper.disposal = stop;
|
|
678
696
|
|
|
@@ -753,7 +771,7 @@ const batch = <T>(fn: () => T): T => {
|
|
|
753
771
|
|
|
754
772
|
// A fn returning a Promise or AsyncIterable transparently becomes an async computed: the first run is
|
|
755
773
|
// the probe, reused as the factory (no duplicate dispatch). A plain fn returns the node directly.
|
|
756
|
-
const computed = <T>(fn: Computed<T>['fn'], equals: ((a: Settled<T>, b: Settled<T>) => boolean) | null = null):
|
|
774
|
+
const computed = <T>(fn: Computed<T>['fn'], equals: ((a: Settled<T>, b: Settled<T>) => boolean) | null = null): ComputedResult<T> => {
|
|
757
775
|
// eager probe so self.value carries fn's return even when this is a non-first tracked op — the
|
|
758
776
|
// detection below cannot depend on the deferred branch, which never runs fn synchronously.
|
|
759
777
|
let o = observer,
|
|
@@ -774,7 +792,7 @@ const computed = <T>(fn: Computed<T>['fn'], equals: ((a: Settled<T>, b: Settled<
|
|
|
774
792
|
|
|
775
793
|
self.equals = equals as ((a: unknown, b: unknown) => boolean) | null;
|
|
776
794
|
|
|
777
|
-
return self as
|
|
795
|
+
return self as unknown as ComputedResult<T>;
|
|
778
796
|
};
|
|
779
797
|
|
|
780
798
|
// Forces a re-derivation without the dummy-signal-dependency hack. writes++ FIRST so a gv-stamped
|
|
@@ -803,7 +821,6 @@ const dispose = <T>(computed: Computed<T>): void => {
|
|
|
803
821
|
// processed inline (no worklist node) and the pool is touched only for re-entrant deep cascades.
|
|
804
822
|
if (draining) {
|
|
805
823
|
disposeHead = walkPush(computed as Computed<unknown>, null, disposeHead);
|
|
806
|
-
|
|
807
824
|
return;
|
|
808
825
|
}
|
|
809
826
|
|
|
@@ -847,16 +864,18 @@ const dispose = <T>(computed: Computed<T>): void => {
|
|
|
847
864
|
}
|
|
848
865
|
};
|
|
849
866
|
|
|
850
|
-
const effect = <T>(fn: Computed<T>['fn'],
|
|
867
|
+
const effect = <T>(fn: Computed<T>['fn'], apply?: (value: T, prev: T | undefined) => void) => {
|
|
868
|
+
let prev: T | undefined;
|
|
869
|
+
|
|
851
870
|
let c = makeComputed<T | undefined>(
|
|
852
|
-
|
|
871
|
+
apply
|
|
853
872
|
? (o) => {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
873
|
+
let v = fn(o);
|
|
874
|
+
|
|
875
|
+
untrack(() => apply(v, prev));
|
|
876
|
+
prev = v;
|
|
877
|
+
|
|
878
|
+
return v;
|
|
860
879
|
}
|
|
861
880
|
: fn
|
|
862
881
|
);
|
|
@@ -1001,9 +1020,12 @@ root.disposables = 0;
|
|
|
1001
1020
|
const signal = <T>(value: T, equals: ((a: T, b: T) => boolean) | null = null): Signal<T> => {
|
|
1002
1021
|
return {
|
|
1003
1022
|
equals: equals as ((a: unknown, b: unknown) => boolean) | null,
|
|
1023
|
+
key: undefined,
|
|
1004
1024
|
keys: null,
|
|
1005
1025
|
nextPending: null,
|
|
1026
|
+
parent: undefined,
|
|
1006
1027
|
rv: 0,
|
|
1028
|
+
state: 0,
|
|
1007
1029
|
subs: null,
|
|
1008
1030
|
subsTail: null,
|
|
1009
1031
|
type: SIGNAL,
|
|
@@ -1029,6 +1051,7 @@ signal.selector = <T>(node: Signal<T>, key: T): boolean => {
|
|
|
1029
1051
|
nextPending: null,
|
|
1030
1052
|
parent: node,
|
|
1031
1053
|
rv: 0,
|
|
1054
|
+
state: 0,
|
|
1032
1055
|
subs: null,
|
|
1033
1056
|
subsTail: null,
|
|
1034
1057
|
type: SIGNAL,
|
package/src/types.ts
CHANGED
|
@@ -22,6 +22,11 @@ interface Computed<T> {
|
|
|
22
22
|
value: T;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
type ComputedResult<T> =
|
|
26
|
+
T extends Promise<any> | AsyncIterable<any>
|
|
27
|
+
? Computed<Settled<T>> & { pending: Signal<boolean> }
|
|
28
|
+
: Computed<Settled<T>>;
|
|
29
|
+
|
|
25
30
|
interface Link {
|
|
26
31
|
dep: Signal<unknown> | Computed<unknown>;
|
|
27
32
|
nextDep: Link | null;
|
|
@@ -56,9 +61,12 @@ type Settled<T> =
|
|
|
56
61
|
|
|
57
62
|
type Signal<T> = {
|
|
58
63
|
equals: ((a: unknown, b: unknown) => boolean) | null;
|
|
64
|
+
key: unknown;
|
|
59
65
|
keys: Map<T, SelectorSignal<T>> | null;
|
|
60
66
|
nextPending: Signal<unknown> | null;
|
|
67
|
+
parent: Signal<unknown> | undefined;
|
|
61
68
|
rv: number;
|
|
69
|
+
state: number;
|
|
62
70
|
subs: Link | null;
|
|
63
71
|
subsTail: Link | null;
|
|
64
72
|
type: typeof SIGNAL;
|
|
@@ -74,6 +82,7 @@ interface TransformResult {
|
|
|
74
82
|
|
|
75
83
|
export type {
|
|
76
84
|
Computed,
|
|
85
|
+
ComputedResult,
|
|
77
86
|
Link,
|
|
78
87
|
Reactive,
|
|
79
88
|
SelectorSignal,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { computed, dispose, effect, isComputed, isSignal, read, root, signal, write } from '~/system';
|
|
3
3
|
import { tick, waitFor } from './lib/wait-for';
|
|
4
|
-
import type { Computed } from '~/system';
|
|
4
|
+
import type { Computed, Signal } from '~/system';
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
describe('asyncComputed', () => {
|
|
@@ -295,6 +295,92 @@ describe('asyncComputed', () => {
|
|
|
295
295
|
expect(read(node)).toBe(3);
|
|
296
296
|
});
|
|
297
297
|
|
|
298
|
+
it('pending toggles true then false across a resolving promise', async () => {
|
|
299
|
+
let node!: Computed<number | undefined> & { pending: Signal<boolean> },
|
|
300
|
+
resolve!: (v: number) => void;
|
|
301
|
+
|
|
302
|
+
root(() => {
|
|
303
|
+
node = computed(() => new Promise<number>((r) => {
|
|
304
|
+
resolve = r;
|
|
305
|
+
}));
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// The polling effect dispatches synchronously at creation — in flight now
|
|
309
|
+
expect(read(node.pending)).toBe(true);
|
|
310
|
+
expect(read(node)).toBeUndefined();
|
|
311
|
+
|
|
312
|
+
resolve(42);
|
|
313
|
+
await waitFor(() => read(node) === 42, 'node resolves to 42');
|
|
314
|
+
|
|
315
|
+
expect(read(node)).toBe(42);
|
|
316
|
+
expect(read(node.pending)).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('pending rises on refetch while the value stays stale', async () => {
|
|
320
|
+
let node!: Computed<number | undefined> & { pending: Signal<boolean> },
|
|
321
|
+
resolvers: ((v: number) => void)[] = [],
|
|
322
|
+
s = signal(1);
|
|
323
|
+
|
|
324
|
+
root(() => {
|
|
325
|
+
node = computed(() => {
|
|
326
|
+
read(s);
|
|
327
|
+
|
|
328
|
+
return new Promise<number>((r) => {
|
|
329
|
+
resolvers.push(r);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
expect(read(node.pending)).toBe(true);
|
|
335
|
+
|
|
336
|
+
resolvers[0](10);
|
|
337
|
+
await waitFor(() => read(node) === 10, 'node settles to 10');
|
|
338
|
+
|
|
339
|
+
expect(read(node)).toBe(10);
|
|
340
|
+
expect(read(node.pending)).toBe(false);
|
|
341
|
+
|
|
342
|
+
// Refetch — dependency change re-dispatches the factory
|
|
343
|
+
write(s, 2);
|
|
344
|
+
await waitFor(() => resolvers.length === 2, 'refetch dispatched');
|
|
345
|
+
|
|
346
|
+
// stale-while-revalidate: value stays 10, but pending rises to signal the in-flight fetch
|
|
347
|
+
expect(read(node)).toBe(10);
|
|
348
|
+
expect(read(node.pending)).toBe(true);
|
|
349
|
+
|
|
350
|
+
resolvers[1](20);
|
|
351
|
+
await waitFor(() => read(node) === 20, 'node settles to 20');
|
|
352
|
+
|
|
353
|
+
expect(read(node)).toBe(20);
|
|
354
|
+
expect(read(node.pending)).toBe(false);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('pending clears on rejection', async () => {
|
|
358
|
+
let node!: Computed<number | undefined> & { pending: Signal<boolean> },
|
|
359
|
+
reject!: (e: Error) => void;
|
|
360
|
+
|
|
361
|
+
root(() => {
|
|
362
|
+
node = computed(() => new Promise<number>((_, r) => {
|
|
363
|
+
reject = r;
|
|
364
|
+
}));
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
expect(read(node.pending)).toBe(true);
|
|
368
|
+
|
|
369
|
+
reject(new Error('boom'));
|
|
370
|
+
await waitFor(() => read(node.pending) === false, 'pending clears after rejection');
|
|
371
|
+
|
|
372
|
+
expect(read(node.pending)).toBe(false);
|
|
373
|
+
expect(() => read(node)).toThrow('boom');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('sync computed has no pending', () => {
|
|
377
|
+
root(() => {
|
|
378
|
+
let node = computed(() => 42);
|
|
379
|
+
|
|
380
|
+
expect((node as { pending?: unknown }).pending).toBeUndefined();
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
298
384
|
it('disposing the returned computed stops the polling effect and the factory', async () => {
|
|
299
385
|
let calls = 0,
|
|
300
386
|
s = signal(1);
|
|
@@ -1,5 +1,6 @@
|
|
|
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
5
|
import { NAMESPACE } from '~/compiler/constants';
|
|
5
6
|
import type { Bindings } from '~/compiler/types';
|
|
@@ -30,7 +31,7 @@ function isReactiveCall(node: ts.Node): boolean {
|
|
|
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 } {
|
package/test/effects.test.ts
CHANGED
|
@@ -269,3 +269,65 @@ describe('effect patterns', () => {
|
|
|
269
269
|
});
|
|
270
270
|
});
|
|
271
271
|
});
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
describe('effect apply phase', () => {
|
|
275
|
+
it('apply receives (value, prev) after each run', async () => {
|
|
276
|
+
let calls: [number, number | undefined][] = [],
|
|
277
|
+
s = signal(1);
|
|
278
|
+
|
|
279
|
+
effect(
|
|
280
|
+
() => read(s),
|
|
281
|
+
(value, prev) => {
|
|
282
|
+
calls.push([value, prev]);
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
expect(calls).toEqual([[1, undefined]]);
|
|
287
|
+
|
|
288
|
+
write(s, 2);
|
|
289
|
+
await Promise.resolve();
|
|
290
|
+
|
|
291
|
+
expect(calls).toEqual([[1, undefined], [2, 1]]);
|
|
292
|
+
|
|
293
|
+
write(s, 3);
|
|
294
|
+
await Promise.resolve();
|
|
295
|
+
|
|
296
|
+
expect(calls).toEqual([[1, undefined], [2, 1], [3, 2]]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('apply runs untracked — its reads do not subscribe', async () => {
|
|
300
|
+
let applied: number[] = [],
|
|
301
|
+
runs = 0,
|
|
302
|
+
s = signal(1),
|
|
303
|
+
t = signal(100);
|
|
304
|
+
|
|
305
|
+
effect(
|
|
306
|
+
() => {
|
|
307
|
+
runs++;
|
|
308
|
+
|
|
309
|
+
return read(s);
|
|
310
|
+
},
|
|
311
|
+
(value) => {
|
|
312
|
+
applied.push(value + read(t));
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
expect(runs).toBe(1);
|
|
317
|
+
expect(applied).toEqual([101]);
|
|
318
|
+
|
|
319
|
+
// apply read t untracked, so writing t must NOT re-run the effect
|
|
320
|
+
write(t, 200);
|
|
321
|
+
await Promise.resolve();
|
|
322
|
+
|
|
323
|
+
expect(runs).toBe(1);
|
|
324
|
+
expect(applied).toEqual([101]);
|
|
325
|
+
|
|
326
|
+
// writing s re-runs fn; apply then reads the current t (200)
|
|
327
|
+
write(s, 2);
|
|
328
|
+
await Promise.resolve();
|
|
329
|
+
|
|
330
|
+
expect(runs).toBe(2);
|
|
331
|
+
expect(applied).toEqual([101, 202]);
|
|
332
|
+
});
|
|
333
|
+
});
|
package/test/errors.test.ts
CHANGED
|
@@ -174,19 +174,19 @@ describe('effect error contract', () => {
|
|
|
174
174
|
stop();
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
it('effect
|
|
177
|
+
it('effect catching its own error internally rethrows nothing', async () => {
|
|
178
178
|
let errors: unknown[] = [],
|
|
179
179
|
s = signal(0),
|
|
180
|
-
stop = effect(
|
|
181
|
-
|
|
180
|
+
stop = effect(() => {
|
|
181
|
+
try {
|
|
182
182
|
if (read(s) === 1) {
|
|
183
183
|
throw new Error('handled');
|
|
184
184
|
}
|
|
185
|
-
}
|
|
186
|
-
(e)
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
187
|
errors.push(e);
|
|
188
188
|
}
|
|
189
|
-
);
|
|
189
|
+
});
|
|
190
190
|
|
|
191
191
|
let captured = await captureUncaught(() => {
|
|
192
192
|
write(s, 1);
|
package/test/system.test.ts
CHANGED
|
@@ -1031,14 +1031,14 @@ describe('edge cases', () => {
|
|
|
1031
1031
|
return val * 10;
|
|
1032
1032
|
});
|
|
1033
1033
|
|
|
1034
|
-
effect(
|
|
1035
|
-
|
|
1034
|
+
effect(() => {
|
|
1035
|
+
try {
|
|
1036
1036
|
effectValues.push(read(c));
|
|
1037
|
-
}
|
|
1038
|
-
(e)
|
|
1037
|
+
}
|
|
1038
|
+
catch (e) {
|
|
1039
1039
|
effectErrors.push(e);
|
|
1040
1040
|
}
|
|
1041
|
-
);
|
|
1041
|
+
});
|
|
1042
1042
|
|
|
1043
1043
|
expect(effectValues).toEqual([0]);
|
|
1044
1044
|
|
|
@@ -1072,14 +1072,14 @@ describe('edge cases', () => {
|
|
|
1072
1072
|
return val;
|
|
1073
1073
|
});
|
|
1074
1074
|
|
|
1075
|
-
effect(
|
|
1076
|
-
|
|
1075
|
+
effect(() => {
|
|
1076
|
+
try {
|
|
1077
1077
|
effectValues.push(read(c));
|
|
1078
|
-
}
|
|
1079
|
-
(e)
|
|
1078
|
+
}
|
|
1079
|
+
catch (e) {
|
|
1080
1080
|
effectErrors.push(e);
|
|
1081
1081
|
}
|
|
1082
|
-
);
|
|
1082
|
+
});
|
|
1083
1083
|
|
|
1084
1084
|
expect(effectValues).toEqual([0]);
|
|
1085
1085
|
|