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
package/src/core/types/index.mts
CHANGED
|
@@ -20,7 +20,11 @@ import type { NonEmptyUnknownList } from './types.mjs';
|
|
|
20
20
|
// RootObservable
|
|
21
21
|
|
|
22
22
|
export type InitializedSourceObservable<A> = Readonly<{
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Passes `value` on and returns it. Once completed, passes nothing on and
|
|
25
|
+
* returns the value held.
|
|
26
|
+
*/
|
|
27
|
+
next: (value: A) => A;
|
|
24
28
|
}> &
|
|
25
29
|
InitializedRootObservable<A>;
|
|
26
30
|
|
|
@@ -206,6 +210,8 @@ export type PairwiseOperatorObservable<A> = SyncChildObservable<
|
|
|
206
210
|
readonly [A]
|
|
207
211
|
>;
|
|
208
212
|
|
|
213
|
+
export type TakeOperatorObservable<A> = SyncChildObservable<A, readonly [A]>;
|
|
214
|
+
|
|
209
215
|
export type TakeWhileOperatorObservable<A> = SyncChildObservable<
|
|
210
216
|
A,
|
|
211
217
|
readonly [A]
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
expectType,
|
|
3
|
+
hasKey,
|
|
4
|
+
isRecord,
|
|
5
|
+
type Optional,
|
|
6
|
+
type Some,
|
|
7
|
+
} from 'ts-data-forge';
|
|
2
8
|
import type { FixedLengthTuple, StrictOmit } from 'ts-type-forge';
|
|
3
|
-
import
|
|
9
|
+
import { observableBrand } from '../utils/index.mjs';
|
|
10
|
+
import type { UpdateToken } from './id.mjs';
|
|
4
11
|
import type { ObservableKind } from './observable-kind.mjs';
|
|
5
12
|
import type { NonEmptyUnknownList, Subscription } from './types.mjs';
|
|
6
13
|
|
|
@@ -19,42 +26,37 @@ import type { NonEmptyUnknownList, Subscription } from './types.mjs';
|
|
|
19
26
|
*/
|
|
20
27
|
|
|
21
28
|
type CreateObservableType<A, Kind extends ObservableKind> = Readonly<{
|
|
22
|
-
|
|
29
|
+
[observableBrand]: true;
|
|
30
|
+
|
|
23
31
|
kind: Kind;
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
/**
|
|
34
|
+
* One more than the deepest observable this one is derived from; 0 for a
|
|
35
|
+
* root. An update reaches observables in ascending depth, so every parent
|
|
36
|
+
* has emitted before its child reads it.
|
|
37
|
+
*/
|
|
26
38
|
depth: Kind extends 'root' ? 0 : number;
|
|
27
|
-
addChild: <B>(child: ChildObservable<B>) => void;
|
|
28
|
-
deleteChild: <B>(child: ChildObservable<B>) => void;
|
|
29
39
|
|
|
30
|
-
// state
|
|
31
40
|
getSnapshot: () => Optional<A>;
|
|
32
41
|
isCompleted: boolean;
|
|
33
|
-
updateToken: UpdateToken;
|
|
34
|
-
hasSubscriber: boolean;
|
|
35
|
-
hasChild: boolean;
|
|
36
|
-
hasActiveChild: () => boolean;
|
|
37
42
|
|
|
38
43
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* `autoComplete`. A `source` or `createState` and everything derived from
|
|
44
|
-
* it alone are never completed this way, so completing a derived observable
|
|
45
|
-
* cannot end a state that something else may still read (#2133).
|
|
44
|
+
* The token of the update in which this observable last emitted. A child's
|
|
45
|
+
* `tryUpdate` receives the token of the update now propagating; a parent
|
|
46
|
+
* whose token differs did not emit in it. The token is opaque: compare it,
|
|
47
|
+
* store it, or pass it on to `setNext`.
|
|
46
48
|
*/
|
|
47
|
-
|
|
49
|
+
updateToken: UpdateToken;
|
|
48
50
|
|
|
49
|
-
tryUpdate: (updateToken: UpdateToken) => void;
|
|
50
|
-
tryComplete: () => void;
|
|
51
51
|
complete: () => void;
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
* Completes this observable and takes it out of the graph,
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
54
|
+
* Completes this observable and takes it out of the graph, for whoever owns
|
|
55
|
+
* it (a scope) to release it. Neither this nor `complete()` reaches upstream:
|
|
56
|
+
* a child completing never completes its parents, which may be shared
|
|
57
|
+
* (#2133). Its descendants complete once none of their parents is live.
|
|
58
|
+
* Unlike `complete()`, it passes on nothing the observable still holds: a
|
|
59
|
+
* `debounce` or `audit` drops its pending value instead of emitting it.
|
|
58
60
|
*/
|
|
59
61
|
dispose: () => void;
|
|
60
62
|
subscribe: (onNext: (v: A) => void, onComplete?: () => void) => Subscription;
|
|
@@ -97,15 +99,6 @@ namespace ObservableTypeConverter {
|
|
|
97
99
|
Readonly<{
|
|
98
100
|
parents: Wrap<P>;
|
|
99
101
|
}>;
|
|
100
|
-
|
|
101
|
-
export type ToManager<
|
|
102
|
-
A,
|
|
103
|
-
O extends CreateObservableType<A, 'async child' | 'root'>,
|
|
104
|
-
> = O &
|
|
105
|
-
Readonly<{
|
|
106
|
-
addDescendant: <B>(child: ChildObservable<B>) => void;
|
|
107
|
-
deleteDescendant: <B>(child: ChildObservable<B>) => void;
|
|
108
|
-
}>;
|
|
109
102
|
}
|
|
110
103
|
|
|
111
104
|
export type SyncChildObservable<
|
|
@@ -125,9 +118,10 @@ export type InitializedSyncChildObservable<
|
|
|
125
118
|
export type AsyncChildObservable<
|
|
126
119
|
A,
|
|
127
120
|
P extends NonEmptyUnknownList = NonEmptyUnknownList,
|
|
128
|
-
> = ObservableTypeConverter.
|
|
121
|
+
> = ObservableTypeConverter.ToChild<
|
|
129
122
|
A,
|
|
130
|
-
|
|
123
|
+
CreateObservableType<A, 'async child'>,
|
|
124
|
+
P
|
|
131
125
|
>;
|
|
132
126
|
|
|
133
127
|
export type InitializedAsyncChildObservable<
|
|
@@ -140,10 +134,7 @@ export type ChildObservable<
|
|
|
140
134
|
P extends NonEmptyUnknownList = NonEmptyUnknownList,
|
|
141
135
|
> = AsyncChildObservable<A, P> | SyncChildObservable<A, P>;
|
|
142
136
|
|
|
143
|
-
export type RootObservable<A> =
|
|
144
|
-
A,
|
|
145
|
-
CreateObservableType<A, 'root'>
|
|
146
|
-
>;
|
|
137
|
+
export type RootObservable<A> = CreateObservableType<A, 'root'>;
|
|
147
138
|
|
|
148
139
|
export type InitializedRootObservable<A> =
|
|
149
140
|
ObservableTypeConverter.ToInitialized<A, RootObservable<A>>;
|
|
@@ -180,6 +171,27 @@ export type Operator<A, B> =
|
|
|
180
171
|
| DropInitialValueOperator<A, B>
|
|
181
172
|
| KeepInitialValueOperator<A, B>;
|
|
182
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Tells an observable made by synstate from any other value, by the brand
|
|
176
|
+
* every observable carries. Checking for a `subscribe` method instead would
|
|
177
|
+
* also accept plain values that happen to have one.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
*
|
|
181
|
+
* ```ts
|
|
182
|
+
* const [count$] = createState(0);
|
|
183
|
+
*
|
|
184
|
+
* assert.isTrue(isObservable(count$));
|
|
185
|
+
*
|
|
186
|
+
* // A `subscribe` method alone does not make an observable.
|
|
187
|
+
* assert.isFalse(isObservable({ subscribe: () => {} }));
|
|
188
|
+
*
|
|
189
|
+
* assert.isFalse(isObservable(0));
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
export const isObservable = (value: unknown): value is Observable<unknown> =>
|
|
193
|
+
isRecord(value) && hasKey(value, observableBrand);
|
|
194
|
+
|
|
183
195
|
export const isManagerObservable = <A,>(
|
|
184
196
|
obs: Observable<A>,
|
|
185
197
|
): obs is ManagerObservable<A> => obs.kind !== 'sync child';
|
|
@@ -210,9 +222,32 @@ export type WrapInitialized<A extends readonly unknown[]> = Readonly<{
|
|
|
210
222
|
// type GetDestTypeOfOperator<Op extends Operator<unknown, unknown>> =
|
|
211
223
|
// Op extends Operator<unknown, infer B> ? B : never;
|
|
212
224
|
|
|
213
|
-
|
|
225
|
+
// Only the members meant for users are public; the rest live on the
|
|
226
|
+
// internal node types in `core/class` (#2117).
|
|
227
|
+
expectType<
|
|
228
|
+
keyof Observable<unknown>,
|
|
229
|
+
| typeof observableBrand
|
|
230
|
+
| 'kind'
|
|
231
|
+
| 'depth'
|
|
232
|
+
| 'getSnapshot'
|
|
233
|
+
| 'isCompleted'
|
|
234
|
+
| 'updateToken'
|
|
235
|
+
| 'complete'
|
|
236
|
+
| 'dispose'
|
|
237
|
+
| 'subscribe'
|
|
238
|
+
| 'pipe'
|
|
239
|
+
>('=');
|
|
214
240
|
|
|
215
|
-
expectType<
|
|
241
|
+
expectType<
|
|
242
|
+
keyof ChildObservable<unknown>,
|
|
243
|
+
keyof Observable<unknown> | 'parents'
|
|
244
|
+
>('=');
|
|
245
|
+
|
|
246
|
+
// Everything but the brand is not enough to pass for an observable.
|
|
247
|
+
expectType<
|
|
248
|
+
StrictOmit<RootObservable<unknown>, typeof observableBrand>,
|
|
249
|
+
Observable<unknown>
|
|
250
|
+
>('!<=');
|
|
216
251
|
|
|
217
252
|
expectType<RootObservable<unknown>['depth'], 0>('=');
|
|
218
253
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A unit of ownership: the observables created while {@link Scope.run} is
|
|
3
|
+
* running belong to the scope, and {@link Scope.dispose} releases them all at
|
|
4
|
+
* once. Create one with `createScope`.
|
|
5
|
+
*
|
|
6
|
+
* For authors of libraries built on synstate (a UI binding mounts and unmounts
|
|
7
|
+
* by scope); application code that defines its state at module level does not
|
|
8
|
+
* need it.
|
|
9
|
+
*/
|
|
10
|
+
export type Scope = Readonly<{
|
|
11
|
+
/**
|
|
12
|
+
* Calls `body` with this scope as the current one and returns what it
|
|
13
|
+
* returns. Only what `body` creates synchronously joins the scope: by the time
|
|
14
|
+
* code after an `await` or in a callback runs, `run` has already returned.
|
|
15
|
+
*
|
|
16
|
+
* On a disposed scope it warns, still calls `body`, and nothing created there
|
|
17
|
+
* belongs to any scope.
|
|
18
|
+
*/
|
|
19
|
+
run: <R>(body: () => R) => R;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Disposes the child scopes (newest first), calls the functions registered
|
|
23
|
+
* with `onScopeDispose` (newest first), disposes the owned observables
|
|
24
|
+
* (newest first) and leaves the parent scope. Calling it again does nothing.
|
|
25
|
+
*/
|
|
26
|
+
dispose: () => void;
|
|
27
|
+
}>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Scope } from '../types/index.mjs';
|
|
2
|
+
|
|
3
|
+
/** What a scope owns: an observable, as far as the scope needs to know it. */
|
|
4
|
+
export type ScopeMember = Readonly<{
|
|
5
|
+
isCompleted: boolean;
|
|
6
|
+
dispose: () => void;
|
|
7
|
+
}>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The side of a scope the rest of the core talks to, on top of its public
|
|
11
|
+
* face {@link Scope}. Kept out of the public API (`core/utils/**` is excluded
|
|
12
|
+
* from index generation), so that only `createScope` and its siblings decide
|
|
13
|
+
* what a scope owns.
|
|
14
|
+
*/
|
|
15
|
+
export type ScopeInternals = Scope &
|
|
16
|
+
Readonly<{
|
|
17
|
+
/** Takes `node` into the scope. */
|
|
18
|
+
adopt: (node: ScopeMember) => void;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Tells the scope that one of the nodes it adopted completed on its own,
|
|
22
|
+
* so that the scope can drop it and keep no reference to it. The node is
|
|
23
|
+
* completed (`isCompleted`) by the time it calls this.
|
|
24
|
+
*/
|
|
25
|
+
release: () => void;
|
|
26
|
+
|
|
27
|
+
addCleanup: (cleanup: () => void) => void;
|
|
28
|
+
}>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The scope whose `run` is executing. A field rather than a module-level
|
|
32
|
+
* `let`, so that the one place writing it is visibly this object.
|
|
33
|
+
*/
|
|
34
|
+
const mut_slot: { current: ScopeInternals | undefined } = {
|
|
35
|
+
current: undefined,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const getCurrentScopeInternals = (): ScopeInternals | undefined =>
|
|
39
|
+
mut_slot.current;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Calls `body` with `scope` as the current scope (`undefined`: none) and
|
|
43
|
+
* restores the previous one afterwards, also when `body` throws.
|
|
44
|
+
*/
|
|
45
|
+
export const runWithScope = <R,>(
|
|
46
|
+
scope: ScopeInternals | undefined,
|
|
47
|
+
body: () => R,
|
|
48
|
+
): R => {
|
|
49
|
+
const prev = mut_slot.current;
|
|
50
|
+
|
|
51
|
+
mut_slot.current = scope;
|
|
52
|
+
|
|
53
|
+
// `finally` is the point: an exception from `body` must not leave its scope
|
|
54
|
+
// current for whatever runs next. Nothing is caught.
|
|
55
|
+
// @sumi-expect-error exceptions/no-try
|
|
56
|
+
try {
|
|
57
|
+
return body();
|
|
58
|
+
} finally {
|
|
59
|
+
mut_slot.current = prev;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
@@ -1,31 +1,21 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
while (true) {
|
|
11
|
-
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
12
|
-
yield Symbol(mut_i.toString()) as T;
|
|
13
|
-
|
|
14
|
-
mut_i += 1;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const observableIdMaker = idMaker<ObservableId>();
|
|
19
|
-
|
|
20
|
-
const subscriberIdMaker = idMaker<SubscriberId>();
|
|
21
|
-
|
|
22
|
-
const updateTokenMaker = idMaker<UpdateToken>();
|
|
23
|
-
|
|
24
|
-
export const issueObservableId = (): ObservableId =>
|
|
25
|
-
observableIdMaker.next().value;
|
|
26
|
-
|
|
27
|
-
export const issueSubscriberId = (): SubscriberId =>
|
|
28
|
-
subscriberIdMaker.next().value;
|
|
29
|
-
|
|
1
|
+
import type { UpdateToken } from '../types/index.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The token of a new update, which the update's root or async child passes to
|
|
5
|
+
* every node it reaches. A `Symbol` with no description, issued once per
|
|
6
|
+
* update and never per node, so its cost is spread over every node the
|
|
7
|
+
* update reaches. Unique without any counter, so the tokens of two copies of
|
|
8
|
+
* synstate in one program never coincide, and nothing is kept in a global.
|
|
9
|
+
*/
|
|
30
10
|
export const issueUpdateToken = (): UpdateToken =>
|
|
31
|
-
|
|
11
|
+
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
12
|
+
Symbol() as UpdateToken;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The token a node holds before its first update: one value shared by every
|
|
16
|
+
* node of this copy, since no update is ever issued it, so creating a node
|
|
17
|
+
* issues nothing.
|
|
18
|
+
*/
|
|
19
|
+
export const notUpdatedYet: UpdateToken =
|
|
20
|
+
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
21
|
+
Symbol('synstate/not-updated-yet') as UpdateToken;
|
package/src/core/utils/index.mts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { Arr, Optional, pipe } from 'ts-data-forge';
|
|
2
1
|
import type { Observable } from '../types/index.mjs';
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
/** The greatest `depth` among `parents`, or 0 when there are none. */
|
|
4
|
+
export const maxDepth = (parents: readonly Observable<unknown>[]): number => {
|
|
5
|
+
let mut_max = 0;
|
|
6
|
+
|
|
7
|
+
for (const p of parents) {
|
|
8
|
+
if (p.depth > mut_max) {
|
|
9
|
+
mut_max = p.depth;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return mut_max;
|
|
14
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The key every observable carries, set to `true` by `assembleObservable`. It
|
|
3
|
+
* lets `isObservable` tell an observable from any other value at run time —
|
|
4
|
+
* a `subscribe` method is not enough, since plain values may have one — and,
|
|
5
|
+
* as a type, makes the public observable types impossible to satisfy with an
|
|
6
|
+
* object literal: an imitation passed to an operator would lack the internal
|
|
7
|
+
* members propagation reads and fail at run time instead of at compile time.
|
|
8
|
+
*
|
|
9
|
+
* `Symbol.for`, so that an observable made by another copy of synstate in the
|
|
10
|
+
* same program is still recognized.
|
|
11
|
+
*/
|
|
12
|
+
export const observableBrand: unique symbol = Symbol.for('synstate/observable');
|
|
@@ -7,8 +7,8 @@ import { createState } from './create-state.mjs';
|
|
|
7
7
|
* Extends `createState` with boolean-specific helpers like `toggle`, `setTrue`, and `setFalse`.
|
|
8
8
|
*
|
|
9
9
|
* @param initialState - The initial boolean value
|
|
10
|
-
* @param options - {@link StateOptions}: `equals
|
|
11
|
-
* updates that change the state
|
|
10
|
+
* @param options - {@link StateOptions}: `equals` (default `Object.is`),
|
|
11
|
+
* to pass on only the updates that change the state
|
|
12
12
|
* @returns A 2-element tuple: `[state, { setTrue, setFalse, toggle, setState, updateState, resetState, getSnapshot, initialState }]`
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { createBooleanState } from './create-boolean-state.mjs';
|
|
2
2
|
|
|
3
3
|
describe(createBooleanState, () => {
|
|
4
|
-
test('
|
|
5
|
-
const [state, { setTrue, setFalse }] = createBooleanState(false
|
|
6
|
-
equals: Object.is,
|
|
7
|
-
});
|
|
4
|
+
test('setting what it already holds passes nothing on when no `equals` is given', () => {
|
|
5
|
+
const [state, { setTrue, setFalse }] = createBooleanState(false);
|
|
8
6
|
|
|
9
7
|
const mut_seen: boolean[] = [];
|
|
10
8
|
|
|
@@ -20,4 +18,20 @@ describe(createBooleanState, () => {
|
|
|
20
18
|
|
|
21
19
|
assert.deepStrictEqual(mut_seen, [false, true]);
|
|
22
20
|
});
|
|
21
|
+
|
|
22
|
+
test('passes on every update, equal or not, with `equals: () => false`', () => {
|
|
23
|
+
const [state, { setTrue }] = createBooleanState(true, {
|
|
24
|
+
equals: () => false,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const mut_seen: boolean[] = [];
|
|
28
|
+
|
|
29
|
+
state.subscribe((value) => {
|
|
30
|
+
mut_seen.push(value);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
setTrue();
|
|
34
|
+
|
|
35
|
+
assert.deepStrictEqual(mut_seen, [true, true]);
|
|
36
|
+
});
|
|
23
37
|
});
|
|
@@ -18,13 +18,13 @@ import { source, type Observable } from '../core/index.mjs';
|
|
|
18
18
|
*
|
|
19
19
|
* emitClick(); // logs: Clicked!
|
|
20
20
|
*
|
|
21
|
-
* assert.
|
|
21
|
+
* assert.strictEqual(mut_clickCount.value, 1);
|
|
22
22
|
*
|
|
23
23
|
* emitClick();
|
|
24
24
|
*
|
|
25
25
|
* emitClick();
|
|
26
26
|
*
|
|
27
|
-
* assert.
|
|
27
|
+
* assert.strictEqual(mut_clickCount.value, 3);
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
30
|
export const createEventEmitter = (): readonly [
|
|
@@ -9,9 +9,11 @@ import { source, type InitializedObservable } from '../core/index.mjs';
|
|
|
9
9
|
* @template A - The type of actions
|
|
10
10
|
* @param reducer - A pure function that takes current state and action, returns new state
|
|
11
11
|
* @param initialState - The initial value of the state
|
|
12
|
-
* @param options - {@link StateOptions}: `equals
|
|
13
|
-
* dispatches that change the state
|
|
14
|
-
* @returns A 3-element tuple: `[state, dispatch, { getSnapshot, initialState }]
|
|
12
|
+
* @param options - {@link StateOptions}: `equals` (default `Object.is`),
|
|
13
|
+
* to pass on only the dispatches that change the state
|
|
14
|
+
* @returns A 3-element tuple: `[state, dispatch, { getSnapshot, initialState }]`.
|
|
15
|
+
* `dispatch` returns the state it leaves; once `state` is completed, that
|
|
16
|
+
* is the state held.
|
|
15
17
|
*
|
|
16
18
|
* @example
|
|
17
19
|
* ```ts
|
|
@@ -52,7 +54,7 @@ import { source, type InitializedObservable } from '../core/index.mjs';
|
|
|
52
54
|
* (s, action: Readonly<{ type: 'add'; amount: number }>) =>
|
|
53
55
|
* s + action.amount,
|
|
54
56
|
* 0,
|
|
55
|
-
* { equals:
|
|
57
|
+
* { equals: () => false },
|
|
56
58
|
* );
|
|
57
59
|
*
|
|
58
60
|
* const countHistory: number[] = [];
|
|
@@ -61,12 +63,19 @@ import { source, type InitializedObservable } from '../core/index.mjs';
|
|
|
61
63
|
* countHistory.push(value);
|
|
62
64
|
* });
|
|
63
65
|
*
|
|
64
|
-
* dispatch({ type: 'add', amount: 0 }); // 0 again
|
|
66
|
+
* dispatch({ type: 'add', amount: 0 }); // 0 again, passed on all the same
|
|
65
67
|
*
|
|
66
68
|
* dispatch({ type: 'add', amount: 2 });
|
|
67
69
|
*
|
|
68
|
-
* assert.deepStrictEqual(countHistory, [0, 2]);
|
|
70
|
+
* assert.deepStrictEqual(countHistory, [0, 0, 2]);
|
|
69
71
|
* ```
|
|
72
|
+
*
|
|
73
|
+
* @note Do not call `dispatch` from a subscriber or a projection while another
|
|
74
|
+
* update is propagating: the write propagates immediately, so subscribers can
|
|
75
|
+
* see an intermediate state, and it logs a `console.warn` (once per
|
|
76
|
+
* observable). Derive the value from its source instead —
|
|
77
|
+
* `const b$ = a$.pipe(map((a) => a * 2))` rather than
|
|
78
|
+
* `a$.subscribe((a) => { setB(a * 2); })`.
|
|
70
79
|
*/
|
|
71
80
|
export const createReducer = <S, A>(
|
|
72
81
|
reducer: Reducer<S, A>,
|
|
@@ -82,20 +91,17 @@ export const createReducer = <S, A>(
|
|
|
82
91
|
] => {
|
|
83
92
|
const state = source<S>(initialState);
|
|
84
93
|
|
|
85
|
-
const equals = options?.equals;
|
|
94
|
+
const equals = options?.equals ?? Object.is;
|
|
86
95
|
|
|
87
96
|
const dispatch = (action: A): S => {
|
|
88
97
|
const currentState = state.getSnapshot().value;
|
|
89
98
|
|
|
90
99
|
const nextState = reducer(currentState, action);
|
|
91
100
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
state.next(nextState);
|
|
97
|
-
|
|
98
|
-
return nextState;
|
|
101
|
+
// Once completed, `next` keeps the state held and returns it.
|
|
102
|
+
return equals(currentState, nextState)
|
|
103
|
+
? currentState
|
|
104
|
+
: state.next(nextState);
|
|
99
105
|
};
|
|
100
106
|
|
|
101
107
|
const getSnapshot = (): S => state.getSnapshot().value;
|
|
@@ -118,8 +124,8 @@ export type StateOptions<S> = Readonly<{
|
|
|
118
124
|
* `skipIfNoChange`: an update passed on here reaches whatever reads the
|
|
119
125
|
* state directly before any operator can thin it out.
|
|
120
126
|
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
127
|
+
* Defaults to `Object.is`, so writing the value already held passes
|
|
128
|
+
* nothing on. Pass `() => false` to pass on every update, equal or not.
|
|
123
129
|
*
|
|
124
130
|
* @param current - The state held now
|
|
125
131
|
* @param next - The state the update would set
|
|
@@ -2,34 +2,51 @@ import type { FixedLengthTuple } from 'ts-type-forge';
|
|
|
2
2
|
import { createReducer } from './create-reducer.mjs';
|
|
3
3
|
|
|
4
4
|
describe(createReducer, () => {
|
|
5
|
-
test('
|
|
6
|
-
const [state, dispatch] = createReducer(keepOrAdd, 0);
|
|
5
|
+
test('does not pass on a dispatch whose next state is `Object.is`-equal to the current one when no `equals` is given', () => {
|
|
6
|
+
const [state, dispatch, { getSnapshot }] = createReducer(keepOrAdd, 0);
|
|
7
7
|
|
|
8
8
|
const seen = record(state);
|
|
9
9
|
|
|
10
|
-
dispatch({ type: 'keep' });
|
|
10
|
+
assert.strictEqual(dispatch({ type: 'keep' }), 0);
|
|
11
11
|
|
|
12
|
-
dispatch({ type: 'add', amount: 1 });
|
|
12
|
+
assert.strictEqual(dispatch({ type: 'add', amount: 1 }), 1);
|
|
13
13
|
|
|
14
|
-
assert.
|
|
14
|
+
assert.strictEqual(dispatch({ type: 'add', amount: 0 }), 1);
|
|
15
|
+
|
|
16
|
+
assert.deepStrictEqual(seen, [0, 1]);
|
|
17
|
+
|
|
18
|
+
assert.strictEqual(getSnapshot(), 1);
|
|
15
19
|
});
|
|
16
20
|
|
|
17
|
-
test('with `
|
|
18
|
-
const [state, dispatch
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
test('compares with `Object.is` by default, so `NaN` equals itself and `-0` differs from `0`', () => {
|
|
22
|
+
const [state, dispatch] = createReducer(
|
|
23
|
+
(_current: number, next: number) => next,
|
|
24
|
+
0,
|
|
25
|
+
);
|
|
21
26
|
|
|
22
27
|
const seen = record(state);
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
dispatch(-0);
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
dispatch(Number.NaN);
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
dispatch(Number.NaN);
|
|
29
34
|
|
|
30
|
-
assert.deepStrictEqual(seen, [0,
|
|
35
|
+
assert.deepStrictEqual(seen, [0, -0, Number.NaN]);
|
|
36
|
+
});
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
test('passes on every dispatch, equal or not, with `equals: () => false`', () => {
|
|
39
|
+
const [state, dispatch] = createReducer(keepOrAdd, 0, {
|
|
40
|
+
equals: () => false,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const seen = record(state);
|
|
44
|
+
|
|
45
|
+
dispatch({ type: 'keep' });
|
|
46
|
+
|
|
47
|
+
dispatch({ type: 'add', amount: 1 });
|
|
48
|
+
|
|
49
|
+
assert.deepStrictEqual(seen, [0, 0, 1]);
|
|
33
50
|
});
|
|
34
51
|
|
|
35
52
|
test('keeps the current object when `equals` finds the next one equal to it', () => {
|
|
@@ -65,6 +82,18 @@ describe(createReducer, () => {
|
|
|
65
82
|
|
|
66
83
|
assert.deepStrictEqual(mut_asked, [[0, 5]]);
|
|
67
84
|
});
|
|
85
|
+
|
|
86
|
+
test('once the state is completed, a dispatch returns the current state', () => {
|
|
87
|
+
const [state, dispatch, { getSnapshot }] = createReducer(keepOrAdd, 0);
|
|
88
|
+
|
|
89
|
+
dispatch({ type: 'add', amount: 1 });
|
|
90
|
+
|
|
91
|
+
state.complete();
|
|
92
|
+
|
|
93
|
+
assert.strictEqual(dispatch({ type: 'add', amount: 2 }), 1);
|
|
94
|
+
|
|
95
|
+
assert.strictEqual(getSnapshot(), 1);
|
|
96
|
+
});
|
|
68
97
|
});
|
|
69
98
|
|
|
70
99
|
type Count = Readonly<{ count: number }>;
|
|
@@ -28,8 +28,8 @@ const reducer = <S,>(state: S, action: Action<S>): S => {
|
|
|
28
28
|
*
|
|
29
29
|
* @template S - The type of the state
|
|
30
30
|
* @param initialState - The initial value of the state
|
|
31
|
-
* @param options - {@link StateOptions}: `equals
|
|
32
|
-
* updates that change the state
|
|
31
|
+
* @param options - {@link StateOptions}: `equals` (default `Object.is`),
|
|
32
|
+
* to pass on only the updates that change the state
|
|
33
33
|
* @returns A 3-element tuple: `[state, setState, { updateState, resetState, getSnapshot, initialState }]`
|
|
34
34
|
*
|
|
35
35
|
* @example
|
|
@@ -88,6 +88,13 @@ const reducer = <S,>(state: S, action: Action<S>): S => {
|
|
|
88
88
|
* 'label:bug page:2',
|
|
89
89
|
* ]);
|
|
90
90
|
* ```
|
|
91
|
+
*
|
|
92
|
+
* @note Do not call `setState`, `updateState` or `resetState` from a subscriber or a projection while another
|
|
93
|
+
* update is propagating: the write propagates immediately, so subscribers can
|
|
94
|
+
* see an intermediate state, and it logs a `console.warn` (once per
|
|
95
|
+
* observable). Derive the value from its source instead —
|
|
96
|
+
* `const b$ = a$.pipe(map((a) => a * 2))` rather than
|
|
97
|
+
* `a$.subscribe((a) => { setB(a * 2); })`.
|
|
91
98
|
*/
|
|
92
99
|
export const createState = <S,>(
|
|
93
100
|
initialState: S,
|