synstate 3.3.0 → 4.0.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 +14 -9
- package/dist/core/authoring.d.mts +7 -0
- package/dist/core/authoring.d.mts.map +1 -0
- package/dist/core/authoring.mjs +7 -0
- package/dist/core/authoring.mjs.map +1 -0
- package/dist/core/class/child-observable-class.d.mts +93 -0
- package/dist/core/class/child-observable-class.d.mts.map +1 -0
- package/dist/core/class/child-observable-class.mjs +248 -0
- package/dist/core/class/child-observable-class.mjs.map +1 -0
- package/dist/core/class/create-child-observable.d.mts +159 -0
- package/dist/core/class/create-child-observable.d.mts.map +1 -0
- package/dist/core/class/create-child-observable.mjs +183 -0
- package/dist/core/class/create-child-observable.mjs.map +1 -0
- package/dist/core/class/create-root-observable.d.mts +37 -0
- package/dist/core/class/create-root-observable.d.mts.map +1 -0
- package/dist/core/class/create-root-observable.mjs +52 -0
- package/dist/core/class/create-root-observable.mjs.map +1 -0
- package/dist/core/class/index.d.mts +9 -0
- package/dist/core/class/index.d.mts.map +1 -0
- package/dist/core/class/index.mjs +9 -0
- package/dist/core/class/index.mjs.map +1 -0
- package/dist/core/class/manager-observable-class.d.mts +26 -0
- package/dist/core/class/manager-observable-class.d.mts.map +1 -0
- package/dist/core/class/manager-observable-class.mjs +116 -0
- package/dist/core/class/manager-observable-class.mjs.map +1 -0
- package/dist/core/class/observable-base-class.d.mts +97 -0
- package/dist/core/class/observable-base-class.d.mts.map +1 -0
- package/dist/core/class/observable-base-class.mjs +219 -0
- package/dist/core/class/observable-base-class.mjs.map +1 -0
- package/dist/core/class/observable-node.d.mts +40 -0
- package/dist/core/class/observable-node.d.mts.map +1 -0
- package/dist/core/class/observable-node.mjs +10 -0
- package/dist/core/class/observable-node.mjs.map +1 -0
- package/dist/core/class/propagation-state.d.mts +24 -0
- package/dist/core/class/propagation-state.d.mts.map +1 -0
- package/dist/core/class/propagation-state.mjs +66 -0
- package/dist/core/class/propagation-state.mjs.map +1 -0
- package/dist/core/class/root-observable-class.d.mts +23 -0
- package/dist/core/class/root-observable-class.d.mts.map +1 -0
- package/dist/core/class/root-observable-class.mjs +58 -0
- package/dist/core/class/root-observable-class.mjs.map +1 -0
- package/dist/core/combine/combine.d.mts.map +1 -1
- package/dist/core/combine/combine.mjs +25 -14
- package/dist/core/combine/combine.mjs.map +1 -1
- package/dist/core/combine/merge.d.mts +0 -3
- package/dist/core/combine/merge.d.mts.map +1 -1
- package/dist/core/combine/merge.mjs +24 -15
- package/dist/core/combine/merge.mjs.map +1 -1
- package/dist/core/combine/zip.d.mts +4 -2
- package/dist/core/combine/zip.d.mts.map +1 -1
- package/dist/core/combine/zip.mjs +54 -18
- package/dist/core/combine/zip.mjs.map +1 -1
- package/dist/core/create/counter.d.mts +9 -3
- package/dist/core/create/counter.d.mts.map +1 -1
- package/dist/core/create/counter.mjs +69 -35
- package/dist/core/create/counter.mjs.map +1 -1
- package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
- package/dist/core/create/from-abortable-promise.mjs +26 -18
- package/dist/core/create/from-abortable-promise.mjs.map +1 -1
- package/dist/core/create/from-promise.d.mts.map +1 -1
- package/dist/core/create/from-promise.mjs +30 -21
- package/dist/core/create/from-promise.mjs.map +1 -1
- package/dist/core/create/from-subscribable.d.mts.map +1 -1
- package/dist/core/create/from-subscribable.mjs +48 -12
- package/dist/core/create/from-subscribable.mjs.map +1 -1
- package/dist/core/create/just.d.mts.map +1 -1
- package/dist/core/create/just.mjs +4 -2
- package/dist/core/create/just.mjs.map +1 -1
- package/dist/core/create/source.d.mts +7 -0
- package/dist/core/create/source.d.mts.map +1 -1
- package/dist/core/create/source.mjs +27 -13
- package/dist/core/create/source.mjs.map +1 -1
- package/dist/core/create/timer.d.mts.map +1 -1
- package/dist/core/create/timer.mjs +40 -25
- package/dist/core/create/timer.mjs.map +1 -1
- package/dist/core/index.d.mts +2 -0
- package/dist/core/index.d.mts.map +1 -1
- package/dist/core/index.mjs +2 -0
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/operators/audit.d.mts +3 -0
- package/dist/core/operators/audit.d.mts.map +1 -1
- package/dist/core/operators/audit.mjs +56 -28
- package/dist/core/operators/audit.mjs.map +1 -1
- package/dist/core/operators/debounce.d.mts +3 -0
- package/dist/core/operators/debounce.d.mts.map +1 -1
- package/dist/core/operators/debounce.mjs +57 -26
- package/dist/core/operators/debounce.mjs.map +1 -1
- package/dist/core/operators/filter.d.mts.map +1 -1
- package/dist/core/operators/filter.mjs +35 -21
- package/dist/core/operators/filter.mjs.map +1 -1
- package/dist/core/operators/index.d.mts +1 -0
- package/dist/core/operators/index.d.mts.map +1 -1
- package/dist/core/operators/index.mjs +1 -0
- package/dist/core/operators/index.mjs.map +1 -1
- package/dist/core/operators/map.d.mts.map +1 -1
- package/dist/core/operators/map.mjs +31 -17
- package/dist/core/operators/map.mjs.map +1 -1
- package/dist/core/operators/merge-map.d.mts +5 -3
- package/dist/core/operators/merge-map.d.mts.map +1 -1
- package/dist/core/operators/merge-map.mjs +49 -26
- package/dist/core/operators/merge-map.mjs.map +1 -1
- package/dist/core/operators/pairwise.d.mts.map +1 -1
- package/dist/core/operators/pairwise.mjs +31 -20
- package/dist/core/operators/pairwise.mjs.map +1 -1
- package/dist/core/operators/scan.d.mts.map +1 -1
- package/dist/core/operators/scan.mjs +27 -14
- package/dist/core/operators/scan.mjs.map +1 -1
- package/dist/core/operators/skip-if-no-change.d.mts.map +1 -1
- package/dist/core/operators/skip-if-no-change.mjs +33 -19
- package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
- package/dist/core/operators/skip-until.d.mts.map +1 -1
- package/dist/core/operators/skip-until.mjs +43 -20
- package/dist/core/operators/skip-until.mjs.map +1 -1
- package/dist/core/operators/skip-while.d.mts.map +1 -1
- package/dist/core/operators/skip-while.mjs +35 -20
- package/dist/core/operators/skip-while.mjs.map +1 -1
- package/dist/core/operators/switch-map.d.mts +5 -3
- package/dist/core/operators/switch-map.d.mts.map +1 -1
- package/dist/core/operators/switch-map.mjs +53 -25
- package/dist/core/operators/switch-map.mjs.map +1 -1
- package/dist/core/operators/take-until.d.mts.map +1 -1
- package/dist/core/operators/take-until.mjs +38 -19
- package/dist/core/operators/take-until.mjs.map +1 -1
- package/dist/core/operators/take-while.d.mts.map +1 -1
- package/dist/core/operators/take-while.mjs +36 -22
- package/dist/core/operators/take-while.mjs.map +1 -1
- package/dist/core/{predefined/operators → operators}/take.d.mts +7 -3
- package/dist/core/operators/take.d.mts.map +1 -0
- package/dist/core/operators/take.mjs +102 -0
- package/dist/core/operators/take.mjs.map +1 -0
- package/dist/core/operators/throttle.d.mts.map +1 -1
- package/dist/core/operators/throttle.mjs +41 -26
- package/dist/core/operators/throttle.mjs.map +1 -1
- package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
- package/dist/core/operators/with-buffered-from.mjs +44 -31
- package/dist/core/operators/with-buffered-from.mjs.map +1 -1
- package/dist/core/operators/with-current-value-from.d.mts.map +1 -1
- package/dist/core/operators/with-current-value-from.mjs +36 -21
- package/dist/core/operators/with-current-value-from.mjs.map +1 -1
- package/dist/core/operators/with-initial-value.d.mts.map +1 -1
- package/dist/core/operators/with-initial-value.mjs +24 -12
- package/dist/core/operators/with-initial-value.mjs.map +1 -1
- package/dist/core/predefined/operators/index.d.mts +0 -1
- package/dist/core/predefined/operators/index.d.mts.map +1 -1
- package/dist/core/predefined/operators/index.mjs +0 -1
- package/dist/core/predefined/operators/index.mjs.map +1 -1
- package/dist/core/scope/index.d.mts +2 -0
- package/dist/core/scope/index.d.mts.map +1 -0
- package/dist/core/scope/index.mjs +2 -0
- package/dist/core/scope/index.mjs.map +1 -0
- package/dist/core/scope/scope.d.mts +51 -0
- package/dist/core/scope/scope.d.mts.map +1 -0
- package/dist/core/scope/scope.mjs +194 -0
- package/dist/core/scope/scope.mjs.map +1 -0
- package/dist/core/types/id.d.mts +0 -2
- package/dist/core/types/id.d.mts.map +1 -1
- package/dist/core/types/index.d.mts +1 -0
- package/dist/core/types/index.d.mts.map +1 -1
- package/dist/core/types/index.mjs +1 -0
- package/dist/core/types/index.mjs.map +1 -1
- package/dist/core/types/observable-family.d.mts +6 -1
- package/dist/core/types/observable-family.d.mts.map +1 -1
- package/dist/core/types/observable-family.mjs.map +1 -1
- package/dist/core/types/observable.d.mts +40 -28
- package/dist/core/types/observable.d.mts.map +1 -1
- package/dist/core/types/observable.mjs +24 -0
- package/dist/core/types/observable.mjs.map +1 -1
- package/dist/core/types/scope.d.mts +27 -0
- package/dist/core/types/scope.d.mts.map +1 -0
- package/dist/core/types/scope.mjs +2 -0
- package/dist/core/types/scope.mjs.map +1 -0
- package/dist/core/utils/current-scope.d.mts +30 -0
- package/dist/core/utils/current-scope.d.mts.map +1 -0
- package/dist/core/utils/current-scope.mjs +26 -0
- package/dist/core/utils/current-scope.mjs.map +1 -0
- package/dist/core/utils/id-maker.d.mts +14 -3
- package/dist/core/utils/id-maker.d.mts.map +1 -1
- package/dist/core/utils/id-maker.mjs +18 -14
- package/dist/core/utils/id-maker.mjs.map +1 -1
- package/dist/core/utils/index.d.mts +2 -1
- package/dist/core/utils/index.d.mts.map +1 -1
- package/dist/core/utils/index.mjs +2 -1
- package/dist/core/utils/index.mjs.map +1 -1
- package/dist/core/utils/max-depth.d.mts +1 -0
- package/dist/core/utils/max-depth.d.mts.map +1 -1
- package/dist/core/utils/max-depth.mjs +9 -7
- package/dist/core/utils/max-depth.mjs.map +1 -1
- package/dist/core/utils/observable-brand.d.mts +13 -0
- package/dist/core/utils/observable-brand.d.mts.map +1 -0
- package/dist/core/utils/observable-brand.mjs +13 -0
- package/dist/core/utils/observable-brand.mjs.map +1 -0
- package/dist/utils/create-boolean-state.d.mts +2 -2
- package/dist/utils/create-event-emitter.d.mts +2 -2
- package/dist/utils/create-reducer.d.mts +17 -8
- package/dist/utils/create-reducer.d.mts.map +1 -1
- package/dist/utils/create-reducer.mjs +14 -6
- package/dist/utils/create-reducer.mjs.map +1 -1
- package/dist/utils/create-state.d.mts +9 -2
- package/dist/utils/create-state.d.mts.map +1 -1
- package/dist/utils/create-state.mjs +7 -0
- package/dist/utils/create-state.mjs.map +1 -1
- package/package.json +16 -16
- package/src/core/authoring.mts +19 -0
- package/src/core/class/child-observable-class.mts +374 -0
- package/src/core/{base → class}/circular-dependency-comparison.test.mts +46 -38
- package/src/core/{base → class}/circular-dependency.test.mts +38 -2
- package/src/core/class/complete-propagation.test.mts +264 -0
- package/src/core/class/create-child-observable.mts +370 -0
- package/src/core/class/create-root-observable.mts +107 -0
- package/src/core/class/index.mts +8 -0
- package/src/core/class/manager-observable-class.mts +166 -0
- package/src/core/class/observable-base-class.mts +315 -0
- package/src/core/{base → class}/observable-base.test.mts +1 -1
- package/src/core/class/observable-node.mts +54 -0
- package/src/core/class/propagation-order.test.mts +92 -0
- package/src/core/class/propagation-state.mts +80 -0
- package/src/core/class/remove-completed.test.mts +369 -0
- package/src/core/class/root-observable-class.mts +85 -0
- package/src/core/class/write-during-propagation.test.mts +234 -0
- package/src/core/combine/combine.mts +67 -55
- package/src/core/combine/merge.mts +33 -26
- package/src/core/combine/zip.mts +103 -58
- package/src/core/create/counter.mts +105 -54
- package/src/core/create/counter.test.mts +82 -0
- package/src/core/create/from-abortable-promise.mts +54 -42
- package/src/core/create/from-promise.mts +38 -29
- package/src/core/create/from-subscribable.mts +68 -23
- package/src/core/create/from-subscribable.test.mts +134 -0
- package/src/core/create/just.mts +4 -5
- package/src/core/create/source.mts +37 -13
- package/src/core/create/source.test.mts +41 -0
- package/src/core/create/timer.mts +51 -32
- package/src/core/index.mts +2 -0
- package/src/core/operators/audit.mts +79 -45
- package/src/core/operators/debounce-audit-complete.test.mts +121 -0
- package/src/core/operators/debounce.mts +81 -40
- package/src/core/operators/filter.mts +48 -31
- package/src/core/operators/index.mts +1 -0
- package/src/core/operators/inner-ownership.test.mts +280 -0
- package/src/core/operators/map.mts +49 -32
- package/src/core/operators/merge-map.mts +81 -46
- package/src/core/operators/pairwise.mts +40 -30
- package/src/core/operators/scan.mts +41 -24
- package/src/core/operators/skip-if-no-change.mts +45 -31
- package/src/core/operators/skip-until.mts +59 -33
- package/src/core/operators/skip-while.mts +49 -31
- package/src/core/operators/switch-map.mts +74 -38
- package/src/core/operators/take-until-skip-until-teardown.test.mts +133 -0
- package/src/core/operators/take-until.mts +49 -31
- package/src/core/operators/take-while.mts +50 -33
- package/src/core/operators/take.mts +138 -0
- package/src/core/operators/throttle.mts +55 -38
- package/src/core/operators/with-buffered-from.mts +45 -35
- package/src/core/operators/with-buffered-from.test.mts +57 -0
- package/src/core/operators/with-current-value-from.mts +39 -25
- package/src/core/operators/with-initial-value.mts +30 -21
- package/src/core/predefined/operators/index.mts +0 -1
- package/src/core/scope/index.mts +1 -0
- package/src/core/scope/scope.mts +260 -0
- package/src/core/scope/scope.test.mts +462 -0
- package/src/core/types/id.mts +0 -4
- package/src/core/types/index.mts +1 -0
- package/src/core/types/observable-family.mts +7 -1
- package/src/core/types/observable.mts +77 -42
- package/src/core/types/scope.mts +27 -0
- package/src/core/utils/current-scope.mts +61 -0
- package/src/core/utils/id-maker.mts +20 -30
- package/src/core/utils/index.mts +2 -1
- package/src/core/utils/max-depth.mts +12 -9
- package/src/core/utils/observable-brand.mts +12 -0
- package/src/utils/create-boolean-state.mts +2 -2
- package/src/utils/create-boolean-state.test.mts +18 -4
- package/src/utils/create-event-emitter.mts +2 -2
- package/src/utils/create-reducer.mts +22 -16
- package/src/utils/create-reducer.test.mts +43 -14
- package/src/utils/create-state.mts +9 -2
- package/src/utils/create-state.test.mts +32 -14
- package/dist/core/base/create-child-observable.d.mts +0 -65
- package/dist/core/base/create-child-observable.d.mts.map +0 -1
- package/dist/core/base/create-child-observable.mjs +0 -267
- package/dist/core/base/create-child-observable.mjs.map +0 -1
- package/dist/core/base/create-observable-base.d.mts +0 -147
- package/dist/core/base/create-observable-base.d.mts.map +0 -1
- package/dist/core/base/create-observable-base.mjs +0 -255
- package/dist/core/base/create-observable-base.mjs.map +0 -1
- package/dist/core/base/create-root-observable.d.mts +0 -37
- package/dist/core/base/create-root-observable.d.mts.map +0 -1
- package/dist/core/base/create-root-observable.mjs +0 -57
- package/dist/core/base/create-root-observable.mjs.map +0 -1
- package/dist/core/base/index.d.mts +0 -3
- package/dist/core/base/index.d.mts.map +0 -1
- package/dist/core/base/index.mjs +0 -3
- package/dist/core/base/index.mjs.map +0 -1
- package/dist/core/predefined/operators/take.d.mts.map +0 -1
- package/dist/core/predefined/operators/take.mjs +0 -44
- package/dist/core/predefined/operators/take.mjs.map +0 -1
- package/dist/core/utils/utils.d.mts +0 -6
- package/dist/core/utils/utils.d.mts.map +0 -1
- package/dist/core/utils/utils.mjs +0 -37
- package/dist/core/utils/utils.mjs.map +0 -1
- package/src/core/base/create-child-observable.mts +0 -457
- package/src/core/base/create-observable-base.mts +0 -548
- package/src/core/base/create-root-observable.mts +0 -113
- package/src/core/base/index.mts +0 -2
- package/src/core/predefined/operators/take.mts +0 -48
- package/src/core/utils/utils.mts +0 -56
- package/src/core/utils/utils.test.mts +0 -88
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createState } from '../../utils/index.mjs';
|
|
2
|
+
import { combine } from '../combine/index.mjs';
|
|
3
|
+
import { map } from '../operators/index.mjs';
|
|
4
|
+
|
|
5
|
+
describe('the nodes an update visits', () => {
|
|
6
|
+
test('a node created while an update propagates is not visited by it', () => {
|
|
7
|
+
const [count$, setCount] = createState(0);
|
|
8
|
+
|
|
9
|
+
let mut_calls = 0;
|
|
10
|
+
|
|
11
|
+
const countCall = (x: number): number => {
|
|
12
|
+
mut_calls += 1;
|
|
13
|
+
|
|
14
|
+
return x;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let mut_created = false;
|
|
18
|
+
|
|
19
|
+
count$.subscribe((x) => {
|
|
20
|
+
if (x !== 1 || mut_created) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
mut_created = true;
|
|
25
|
+
|
|
26
|
+
count$.pipe(map(countCall)).subscribe(() => {});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
count$.pipe(map((x) => x)).subscribe(() => {});
|
|
30
|
+
|
|
31
|
+
setCount(1);
|
|
32
|
+
|
|
33
|
+
// Once for the initial value; the update that created it does not run it
|
|
34
|
+
// again.
|
|
35
|
+
expect(mut_calls).toBe(1);
|
|
36
|
+
|
|
37
|
+
setCount(2);
|
|
38
|
+
|
|
39
|
+
expect(mut_calls).toBe(2);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('a node completed while an update propagates is not visited by it', () => {
|
|
43
|
+
const [count$, setCount] = createState(0);
|
|
44
|
+
|
|
45
|
+
let mut_calls = 0;
|
|
46
|
+
|
|
47
|
+
const late$ = count$.pipe(map((x) => x + 1)).pipe(
|
|
48
|
+
map((x) => {
|
|
49
|
+
mut_calls += 1;
|
|
50
|
+
|
|
51
|
+
return x;
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
late$.subscribe(() => {});
|
|
56
|
+
|
|
57
|
+
count$.subscribe((x) => {
|
|
58
|
+
if (x === 1) {
|
|
59
|
+
late$.complete();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
mut_calls = 0;
|
|
64
|
+
|
|
65
|
+
setCount(1);
|
|
66
|
+
|
|
67
|
+
expect(mut_calls).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('a manager whose descendants skip depths updates them in order', () => {
|
|
71
|
+
const [a$, setA] = createState(0);
|
|
72
|
+
|
|
73
|
+
const [b$] = createState(10);
|
|
74
|
+
|
|
75
|
+
// `c$` is at depth 3 and the only descendant of `a$`.
|
|
76
|
+
const c$ = combine([a$, b$.pipe(map((x) => x)).pipe(map((x) => x))]);
|
|
77
|
+
|
|
78
|
+
const d$ = c$.pipe(map(([x, y]) => x + y));
|
|
79
|
+
|
|
80
|
+
const mut_values: number[] = [];
|
|
81
|
+
|
|
82
|
+
d$.subscribe((v) => {
|
|
83
|
+
mut_values.push(v);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
setA(1);
|
|
87
|
+
|
|
88
|
+
setA(2);
|
|
89
|
+
|
|
90
|
+
assert.deepStrictEqual(mut_values, [10, 11, 12]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The state of the update propagating now, shared by every observable.
|
|
3
|
+
*
|
|
4
|
+
* It is module state rather than a field of a class, so that nothing outside
|
|
5
|
+
* this file can reach it but through the functions below: an exported class
|
|
6
|
+
* would hand its static fields to every importer, and a private static field
|
|
7
|
+
* costs about twice as much per access as a module binding, on a path every
|
|
8
|
+
* update takes.
|
|
9
|
+
*/
|
|
10
|
+
import { Arr } from 'ts-data-forge';
|
|
11
|
+
import type { UpdateToken } from '../types/index.mjs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* How many `startUpdate` calls are running, across every observable, and
|
|
15
|
+
* the token of the outermost one.
|
|
16
|
+
*/
|
|
17
|
+
const mut_propagation: {
|
|
18
|
+
depth: number;
|
|
19
|
+
outermostToken: UpdateToken | undefined;
|
|
20
|
+
} = { depth: 0, outermostToken: undefined };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Work deferred by {@link runAfterPropagation}, run once the outermost
|
|
24
|
+
* `startUpdate` has reached every descendant.
|
|
25
|
+
*/
|
|
26
|
+
const mut_afterPropagation: (() => void)[] = [];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether some observable's update is propagating right now. A root written
|
|
30
|
+
* to while one is, is written to from inside a subscriber or a projection
|
|
31
|
+
* (#2336).
|
|
32
|
+
*/
|
|
33
|
+
export const isPropagating = (): boolean => mut_propagation.depth > 0;
|
|
34
|
+
|
|
35
|
+
/** The token of the outermost update propagating now, if any. */
|
|
36
|
+
export const outermostUpdateToken = (): UpdateToken | undefined =>
|
|
37
|
+
mut_propagation.outermostToken;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Runs `task` once the update now propagating has reached every descendant, or
|
|
41
|
+
* at once when none is. A child that completes itself in its `tryUpdate`
|
|
42
|
+
* completes its children with it, before they have seen the value it has just
|
|
43
|
+
* passed on (`take`).
|
|
44
|
+
*/
|
|
45
|
+
export const runAfterPropagation = (task: () => void): void => {
|
|
46
|
+
if (isPropagating()) {
|
|
47
|
+
mut_afterPropagation.push(task);
|
|
48
|
+
} else {
|
|
49
|
+
task();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Brackets one `startUpdate`; see {@link endPropagation}. */
|
|
54
|
+
export const beginPropagation = (updateToken: UpdateToken): void => {
|
|
55
|
+
if (mut_propagation.depth === 0) {
|
|
56
|
+
mut_propagation.outermostToken = updateToken;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
mut_propagation.depth += 1;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Ends one `startUpdate`, and runs the deferred work once the outermost one
|
|
64
|
+
* has ended.
|
|
65
|
+
*/
|
|
66
|
+
export const endPropagation = (): void => {
|
|
67
|
+
mut_propagation.depth -= 1;
|
|
68
|
+
|
|
69
|
+
if (mut_propagation.depth > 0) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
mut_propagation.outermostToken = undefined;
|
|
74
|
+
|
|
75
|
+
// A deferred task may start an update of its own, which flushes what that
|
|
76
|
+
// update defers before this loop moves on.
|
|
77
|
+
while (Arr.isNonEmpty(mut_afterPropagation)) {
|
|
78
|
+
mut_afterPropagation.shift()?.();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { Optional } from 'ts-data-forge';
|
|
2
|
+
import type { FixedLengthTuple } from 'ts-type-forge';
|
|
3
|
+
import { createState } from '../../utils/index.mjs';
|
|
4
|
+
import { combine } from '../combine/index.mjs';
|
|
5
|
+
import { source } from '../create/index.mjs';
|
|
6
|
+
import { map, switchMap, takeWhile } from '../operators/index.mjs';
|
|
7
|
+
import { toNode } from './observable-node.mjs';
|
|
8
|
+
|
|
9
|
+
describe('a completed observable leaves the propagation graph', () => {
|
|
10
|
+
test('completed derived observables stop computing (#2134)', () => {
|
|
11
|
+
const [count$, setCount] = createState(0);
|
|
12
|
+
|
|
13
|
+
// A subscriber, so that the root has a reason to stay alive whatever the
|
|
14
|
+
// completion rules for an unused root are.
|
|
15
|
+
count$.subscribe(() => {});
|
|
16
|
+
|
|
17
|
+
let mut_calls = 0;
|
|
18
|
+
|
|
19
|
+
const countCall = (x: number): number => {
|
|
20
|
+
mut_calls += 1;
|
|
21
|
+
|
|
22
|
+
return x;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
for (const _ of Array.from({ length: 1000 })) {
|
|
26
|
+
const d = count$.pipe(map(countCall));
|
|
27
|
+
|
|
28
|
+
d.complete();
|
|
29
|
+
|
|
30
|
+
assert.isTrue(d.isCompleted);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
mut_calls = 0;
|
|
34
|
+
|
|
35
|
+
setCount(1);
|
|
36
|
+
|
|
37
|
+
expect(mut_calls).toBe(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('a completed descendant several levels down stops computing', () => {
|
|
41
|
+
const [count$, setCount] = createState(0);
|
|
42
|
+
|
|
43
|
+
count$.subscribe(() => {});
|
|
44
|
+
|
|
45
|
+
let mut_calls = 0;
|
|
46
|
+
|
|
47
|
+
const d1 = count$.pipe(map((x) => x + 1));
|
|
48
|
+
|
|
49
|
+
const d2 = d1.pipe(
|
|
50
|
+
map((x) => {
|
|
51
|
+
mut_calls += 1;
|
|
52
|
+
|
|
53
|
+
return x;
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
d1.subscribe(() => {});
|
|
58
|
+
|
|
59
|
+
d2.complete();
|
|
60
|
+
|
|
61
|
+
mut_calls = 0;
|
|
62
|
+
|
|
63
|
+
setCount(1);
|
|
64
|
+
|
|
65
|
+
expect(mut_calls).toBe(0);
|
|
66
|
+
|
|
67
|
+
assert.deepStrictEqual(d1.getSnapshot(), Optional.some(2));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('completing a node in the middle of a chain removes everything below it', () => {
|
|
71
|
+
const s = source<number>(0);
|
|
72
|
+
|
|
73
|
+
s.subscribe(() => {});
|
|
74
|
+
|
|
75
|
+
const middle = s.pipe(map((x) => x + 1));
|
|
76
|
+
|
|
77
|
+
let mut_calls = 0;
|
|
78
|
+
|
|
79
|
+
const leaf = middle.pipe(
|
|
80
|
+
map((x) => {
|
|
81
|
+
mut_calls += 1;
|
|
82
|
+
|
|
83
|
+
return x;
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const mut_got: number[] = [];
|
|
88
|
+
|
|
89
|
+
leaf.subscribe((v) => {
|
|
90
|
+
mut_got.push(v);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
middle.complete();
|
|
94
|
+
|
|
95
|
+
assert.isTrue(leaf.isCompleted);
|
|
96
|
+
|
|
97
|
+
assert.isFalse(toNode(s).hasChild);
|
|
98
|
+
|
|
99
|
+
mut_calls = 0;
|
|
100
|
+
|
|
101
|
+
s.next(1);
|
|
102
|
+
|
|
103
|
+
// The leaf is not reached through the completed node, nor directly.
|
|
104
|
+
expect(mut_calls).toBe(0);
|
|
105
|
+
|
|
106
|
+
assert.deepStrictEqual(mut_got, [1]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('completing the root of a long chain takes time linear in its length', () => {
|
|
110
|
+
const s = source<number>(0);
|
|
111
|
+
|
|
112
|
+
let mut_tail = s.pipe(map((x) => x));
|
|
113
|
+
|
|
114
|
+
for (const _ of Array.from({ length: 499 })) {
|
|
115
|
+
mut_tail = mut_tail.pipe(map((x) => x));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const leaf = mut_tail;
|
|
119
|
+
|
|
120
|
+
leaf.subscribe(() => {});
|
|
121
|
+
|
|
122
|
+
const start = performance.now();
|
|
123
|
+
|
|
124
|
+
s.complete();
|
|
125
|
+
|
|
126
|
+
const elapsed = performance.now() - start;
|
|
127
|
+
|
|
128
|
+
assert.isTrue(leaf.isCompleted);
|
|
129
|
+
|
|
130
|
+
// Each node used to re-run the completion of every ancestor, each run
|
|
131
|
+
// walking the ancestors again: cubic in the length, over 2 s here.
|
|
132
|
+
expect(elapsed).toBeLessThan(1000);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('the parent no longer reports the completed child', () => {
|
|
136
|
+
const s = source<number>(0);
|
|
137
|
+
|
|
138
|
+
s.subscribe(() => {});
|
|
139
|
+
|
|
140
|
+
const d = s.pipe(map((x) => x));
|
|
141
|
+
|
|
142
|
+
assert.isTrue(toNode(s).hasChild);
|
|
143
|
+
|
|
144
|
+
d.complete();
|
|
145
|
+
|
|
146
|
+
assert.isFalse(toNode(s).hasChild);
|
|
147
|
+
|
|
148
|
+
assert.isFalse(toNode(s).hasActiveChild());
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('a node with another live parent keeps updating', () => {
|
|
152
|
+
const a = source<number>(0);
|
|
153
|
+
|
|
154
|
+
const b = source<number>(0);
|
|
155
|
+
|
|
156
|
+
const a2 = a.pipe(map((x) => x * 10));
|
|
157
|
+
|
|
158
|
+
const c = combine([a2, b]);
|
|
159
|
+
|
|
160
|
+
const mut_got: FixedLengthTuple<2, number>[] = [];
|
|
161
|
+
|
|
162
|
+
c.subscribe((v) => {
|
|
163
|
+
mut_got.push(v);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
b.complete();
|
|
167
|
+
|
|
168
|
+
a.next(1);
|
|
169
|
+
|
|
170
|
+
assert.deepStrictEqual(mut_got, [
|
|
171
|
+
[0, 0],
|
|
172
|
+
[10, 0],
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('a sibling completing during propagation does not disturb the others', () => {
|
|
177
|
+
const s = source<number>(0);
|
|
178
|
+
|
|
179
|
+
const stopper = s.pipe(takeWhile((x) => x < 1));
|
|
180
|
+
|
|
181
|
+
stopper.subscribe(() => {});
|
|
182
|
+
|
|
183
|
+
const mut_got: number[] = [];
|
|
184
|
+
|
|
185
|
+
s.pipe(map((x) => x * 2)).subscribe((v) => {
|
|
186
|
+
mut_got.push(v);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
s.next(1);
|
|
190
|
+
|
|
191
|
+
s.next(2);
|
|
192
|
+
|
|
193
|
+
assert.isTrue(stopper.isCompleted);
|
|
194
|
+
|
|
195
|
+
assert.deepStrictEqual(mut_got, [0, 2, 4]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('a node created during propagation is not updated by that same update', () => {
|
|
199
|
+
const s = source<number>(0);
|
|
200
|
+
|
|
201
|
+
let mut_calls = 0;
|
|
202
|
+
|
|
203
|
+
const mut_got: number[] = [];
|
|
204
|
+
|
|
205
|
+
// Subscribed on a child, so the node is created while `s` is already
|
|
206
|
+
// walking its propagation order.
|
|
207
|
+
s.pipe(map((x) => x)).subscribe((x) => {
|
|
208
|
+
if (x === 1) {
|
|
209
|
+
s.pipe(
|
|
210
|
+
map((y) => {
|
|
211
|
+
mut_calls += 1;
|
|
212
|
+
|
|
213
|
+
return y;
|
|
214
|
+
}),
|
|
215
|
+
).subscribe((y) => {
|
|
216
|
+
mut_got.push(y);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
s.next(1);
|
|
222
|
+
|
|
223
|
+
// Only the initial value computed when the node was created.
|
|
224
|
+
expect(mut_calls).toBe(1);
|
|
225
|
+
|
|
226
|
+
assert.deepStrictEqual(mut_got, [1]);
|
|
227
|
+
|
|
228
|
+
s.next(2);
|
|
229
|
+
|
|
230
|
+
assert.deepStrictEqual(mut_got, [1, 2]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('inner observables switched away from stop computing', () => {
|
|
234
|
+
const [outer$, setOuter] = createState(0);
|
|
235
|
+
|
|
236
|
+
const [inner$, setInner] = createState(0);
|
|
237
|
+
|
|
238
|
+
inner$.subscribe(() => {});
|
|
239
|
+
|
|
240
|
+
let mut_calls = 0;
|
|
241
|
+
|
|
242
|
+
const result$ = outer$.pipe(
|
|
243
|
+
switchMap((o) =>
|
|
244
|
+
inner$.pipe(
|
|
245
|
+
map((i) => {
|
|
246
|
+
mut_calls += 1;
|
|
247
|
+
|
|
248
|
+
return o + i;
|
|
249
|
+
}),
|
|
250
|
+
),
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
result$.subscribe(() => {});
|
|
255
|
+
|
|
256
|
+
for (const o of [1, 2, 3, 4, 5]) {
|
|
257
|
+
setOuter(o);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
mut_calls = 0;
|
|
261
|
+
|
|
262
|
+
setInner(1);
|
|
263
|
+
|
|
264
|
+
expect(mut_calls).toBe(1);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('a completed observable keeps its last value', () => {
|
|
269
|
+
test('getSnapshot returns the value the observable completed with', () => {
|
|
270
|
+
const s = source<number>(0);
|
|
271
|
+
|
|
272
|
+
s.next(5);
|
|
273
|
+
|
|
274
|
+
s.complete();
|
|
275
|
+
|
|
276
|
+
assert.isTrue(s.isCompleted);
|
|
277
|
+
|
|
278
|
+
assert.deepStrictEqual(s.getSnapshot(), Optional.some(5));
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('combine keeps reading the last value of a parent that completed after it changed', () => {
|
|
282
|
+
const a = source<number>(0);
|
|
283
|
+
|
|
284
|
+
const b = source<number>(0);
|
|
285
|
+
|
|
286
|
+
const c = combine([a, b]);
|
|
287
|
+
|
|
288
|
+
const mut_got: FixedLengthTuple<2, number>[] = [];
|
|
289
|
+
|
|
290
|
+
c.subscribe((v) => {
|
|
291
|
+
mut_got.push(v);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
b.next(5);
|
|
295
|
+
|
|
296
|
+
b.complete();
|
|
297
|
+
|
|
298
|
+
a.next(1);
|
|
299
|
+
|
|
300
|
+
assert.isFalse(c.isCompleted);
|
|
301
|
+
|
|
302
|
+
assert.deepStrictEqual(mut_got, [
|
|
303
|
+
[0, 0],
|
|
304
|
+
[0, 5],
|
|
305
|
+
[1, 5],
|
|
306
|
+
]);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('a derived observable completed by its parent keeps its last value for combine', () => {
|
|
310
|
+
const a = source<number>(0);
|
|
311
|
+
|
|
312
|
+
const s = source<number>(1);
|
|
313
|
+
|
|
314
|
+
const m = s.pipe(map((x) => x * 2));
|
|
315
|
+
|
|
316
|
+
const c = combine([a, m]);
|
|
317
|
+
|
|
318
|
+
const mut_got: FixedLengthTuple<2, number>[] = [];
|
|
319
|
+
|
|
320
|
+
c.subscribe((v) => {
|
|
321
|
+
mut_got.push(v);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
s.next(3);
|
|
325
|
+
|
|
326
|
+
s.complete();
|
|
327
|
+
|
|
328
|
+
assert.isTrue(m.isCompleted);
|
|
329
|
+
|
|
330
|
+
assert.deepStrictEqual(m.getSnapshot(), Optional.some(6));
|
|
331
|
+
|
|
332
|
+
a.next(1);
|
|
333
|
+
|
|
334
|
+
assert.isFalse(c.isCompleted);
|
|
335
|
+
|
|
336
|
+
assert.deepStrictEqual(mut_got, [
|
|
337
|
+
[0, 2],
|
|
338
|
+
[0, 6],
|
|
339
|
+
[1, 6],
|
|
340
|
+
]);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test('combine created after a parent completed starts from its last value', () => {
|
|
344
|
+
const a = source<number>(0);
|
|
345
|
+
|
|
346
|
+
const b = source<number>(0);
|
|
347
|
+
|
|
348
|
+
b.next(5);
|
|
349
|
+
|
|
350
|
+
b.complete();
|
|
351
|
+
|
|
352
|
+
const c = combine([a, b]);
|
|
353
|
+
|
|
354
|
+
assert.deepStrictEqual(c.getSnapshot(), Optional.some([0, 5] as const));
|
|
355
|
+
|
|
356
|
+
const mut_got: FixedLengthTuple<2, number>[] = [];
|
|
357
|
+
|
|
358
|
+
c.subscribe((v) => {
|
|
359
|
+
mut_got.push(v);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
a.next(1);
|
|
363
|
+
|
|
364
|
+
assert.deepStrictEqual(mut_got, [
|
|
365
|
+
[0, 5],
|
|
366
|
+
[1, 5],
|
|
367
|
+
]);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// @sumi-expect-error-file classes/no-class
|
|
2
|
+
// @sumi-expect-error-file banned-syntax/no-this
|
|
3
|
+
// @sumi-expect-error-file functions/prefer-arrow-function
|
|
4
|
+
// @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
|
|
5
|
+
import { Optional } from 'ts-data-forge';
|
|
6
|
+
import {
|
|
7
|
+
isRootObservable,
|
|
8
|
+
type RootObservable,
|
|
9
|
+
type UpdateToken,
|
|
10
|
+
} from '../types/index.mjs';
|
|
11
|
+
import { ManagerObservableClass } from './manager-observable-class.mjs';
|
|
12
|
+
import { isPropagating, outermostUpdateToken } from './propagation-state.mjs';
|
|
13
|
+
|
|
14
|
+
export class RootObservableClass<A>
|
|
15
|
+
extends ManagerObservableClass<A, 'root', 0>
|
|
16
|
+
implements RootObservable<A>
|
|
17
|
+
{
|
|
18
|
+
/**
|
|
19
|
+
* The outermost update propagating when this root was created. A root
|
|
20
|
+
* created while an update propagates (inside a `switchMap` projection,
|
|
21
|
+
* say) may emit while it is created, as `fromSubscribable` does for a
|
|
22
|
+
* synchronous source. Nothing is derived from it yet, so that is its
|
|
23
|
+
* initial value rather than a write (#2336).
|
|
24
|
+
*/
|
|
25
|
+
readonly #createdDuring: UpdateToken | undefined;
|
|
26
|
+
#mut_hasWarned: boolean;
|
|
27
|
+
|
|
28
|
+
constructor({
|
|
29
|
+
initialValue,
|
|
30
|
+
}: Readonly<{
|
|
31
|
+
initialValue: Optional<A>;
|
|
32
|
+
}>) {
|
|
33
|
+
super({
|
|
34
|
+
kind: 'root',
|
|
35
|
+
depth: 0,
|
|
36
|
+
initialValue,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
this.#createdDuring = outermostUpdateToken();
|
|
40
|
+
|
|
41
|
+
this.#mut_hasWarned = false;
|
|
42
|
+
|
|
43
|
+
this.adoptIntoCurrentScope();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Emits `nextValue` and propagates the update to all descendants, warning
|
|
48
|
+
* once when it is a write from inside another update.
|
|
49
|
+
*/
|
|
50
|
+
protected override startUpdate(nextValue: A): void {
|
|
51
|
+
if (
|
|
52
|
+
!this.#mut_hasWarned &&
|
|
53
|
+
isPropagating() &&
|
|
54
|
+
outermostUpdateToken() !== this.#createdDuring
|
|
55
|
+
) {
|
|
56
|
+
this.#mut_hasWarned = true;
|
|
57
|
+
|
|
58
|
+
console.warn(writeDuringPropagationWarning);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
super.startUpdate(nextValue);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Shown once per root observable, the first time it is written to (`next`,
|
|
67
|
+
* `setState`, `dispatch`, …) while another update is still propagating —
|
|
68
|
+
* from a subscriber or a projection. The write still propagates immediately,
|
|
69
|
+
* as it always has; the warning is there because the pattern is wrong, not
|
|
70
|
+
* because anything was refused.
|
|
71
|
+
*/
|
|
72
|
+
export const writeDuringPropagationWarning =
|
|
73
|
+
'synstate: an observable was written to (next / setState / dispatch) while an update was propagating, so subscribers can see intermediate states. Derive the value with map / combine instead of writing from a subscriber or a projection.';
|
|
74
|
+
|
|
75
|
+
if (import.meta.vitest !== undefined) {
|
|
76
|
+
test('isRootObservable', () => {
|
|
77
|
+
assert.isTrue(
|
|
78
|
+
isRootObservable(
|
|
79
|
+
new RootObservableClass({
|
|
80
|
+
initialValue: Optional.some(0),
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
}
|