@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.
@@ -28,7 +28,7 @@ interface ReactiveObjectCall {
28
28
  interface VisitContext {
29
29
  bindings: Bindings;
30
30
  calls: ReactiveObjectCall[];
31
- checker: ts.TypeChecker | undefined;
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.isTypeAssertionExpression(unwrapped) || ts.isParenthesizedExpression(unwrapped)) {
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.isTypeAssertionExpression(value)) {
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.TypeChecker | undefined, node: ts.Node): node is ts.CallExpression {
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
- ts.forEachChild(node, n => visit(ctx, n));
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
- ts.forEachChild(node, n => visit(ctx, n));
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
- ts.forEachChild(node, n => visit(ctx, n));
281
+ node.forEachChild(n => visit(ctx, n));
282
282
  }
283
283
 
284
284
 
285
- export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker): ObjectTransformResult => {
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.isTypeAssertionExpression(unwrapped)) {
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
- ts.forEachChild(node, n => visit(ctx, n));
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
- ts.forEachChild(node, n => visit(ctx, n));
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
- ts.forEachChild(node, n => visit(ctx, n));
254
+ node.forEachChild(n => visit(ctx, n));
255
255
  }
256
256
 
257
257
 
@@ -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
- let n = this.length,
249
- before = new Array(n) as T[];
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
- super.sort(fn);
275
+ for (let i = 0; i < n; i++) {
276
+ before[i] = this[i];
277
+ }
256
278
 
257
- let buckets = new Map<T, number[]>(),
258
- order = new Array(n);
279
+ super.sort(fn);
259
280
 
260
- for (let i = 0; i < n; i++) {
261
- let value = before[i],
262
- list = buckets.get(value);
281
+ let buckets = new Map<T, number[]>(),
282
+ order = new Array(n);
263
283
 
264
- if (!list) {
265
- buckets.set(value, [i]);
266
- }
267
- else {
268
- list.push(i);
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
- for (let i = 0; i < n; i++) {
273
- let list = buckets.get(this[i])!;
296
+ for (let i = 0; i < n; i++) {
297
+ let list = buckets.get(this[i])!;
274
298
 
275
- order[i] = list.length === 1 ? list[0] : list[list.length - 1];
299
+ order[i] = list.length === 1 ? list[0] : list[list.length - 1];
276
300
 
277
- if (list.length > 1) {
278
- list.pop();
301
+ if (list.length > 1) {
302
+ list.pop();
303
+ }
279
304
  }
280
- }
281
305
 
282
- this.dispatch('sort', { order });
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): Computed<Settled<T>> => {
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 Computed<Settled<T>>;
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'], onError?: (e: unknown) => void) => {
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
- onError
871
+ apply
853
872
  ? (o) => {
854
- try {
855
- return fn(o);
856
- }
857
- catch (e) {
858
- onError(e);
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 ts.createSourceFile('test.ts', code, ts.ScriptTarget.Latest, true);
34
+ return languageService.parse(process.cwd() + '/test.ts', code);
34
35
  }
35
36
 
36
37
  function transformPrimitives(code: string): { bindings: Bindings; output: string } {
@@ -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
+ });
@@ -174,19 +174,19 @@ describe('effect error contract', () => {
174
174
  stop();
175
175
  });
176
176
 
177
- it('effect onError receives the error and nothing rethrows', async () => {
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);
@@ -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