synstate 3.0.6 → 3.2.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 +59 -1
- package/dist/core/base/create-child-observable.d.mts.map +1 -1
- package/dist/core/base/create-child-observable.mjs +0 -1
- package/dist/core/base/create-child-observable.mjs.map +1 -1
- package/dist/core/base/create-observable-base.d.mts +2 -2
- package/dist/core/base/create-observable-base.d.mts.map +1 -1
- package/dist/core/base/create-observable-base.mjs +0 -1
- package/dist/core/base/create-observable-base.mjs.map +1 -1
- package/dist/core/base/create-root-observable.d.mts +1 -1
- package/dist/core/base/create-root-observable.d.mts.map +1 -1
- package/dist/core/combine/combine.d.mts +2 -2
- package/dist/core/combine/combine.d.mts.map +1 -1
- package/dist/core/combine/combine.mjs +0 -1
- package/dist/core/combine/combine.mjs.map +1 -1
- package/dist/core/combine/merge.d.mts +2 -2
- package/dist/core/combine/merge.d.mts.map +1 -1
- package/dist/core/combine/merge.mjs +0 -2
- package/dist/core/combine/merge.mjs.map +1 -1
- package/dist/core/combine/zip.d.mts +2 -2
- package/dist/core/combine/zip.d.mts.map +1 -1
- package/dist/core/combine/zip.mjs +0 -2
- package/dist/core/combine/zip.mjs.map +1 -1
- package/dist/core/create/counter.d.mts +11 -3
- package/dist/core/create/counter.d.mts.map +1 -1
- package/dist/core/create/counter.mjs +17 -7
- package/dist/core/create/counter.mjs.map +1 -1
- package/dist/core/create/from-abortable-promise.d.mts +1 -1
- package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
- package/dist/core/create/from-abortable-promise.mjs +0 -2
- package/dist/core/create/from-abortable-promise.mjs.map +1 -1
- package/dist/core/create/from-promise.d.mts +1 -1
- package/dist/core/create/from-promise.d.mts.map +1 -1
- package/dist/core/create/from-promise.mjs +0 -1
- package/dist/core/create/from-promise.mjs.map +1 -1
- package/dist/core/create/from-subscribable.d.mts +1 -1
- package/dist/core/create/from-subscribable.d.mts.map +1 -1
- package/dist/core/create/from-subscribable.mjs +0 -1
- package/dist/core/create/from-subscribable.mjs.map +1 -1
- package/dist/core/create/just.d.mts +1 -1
- package/dist/core/create/just.d.mts.map +1 -1
- package/dist/core/create/just.mjs +0 -1
- package/dist/core/create/just.mjs.map +1 -1
- package/dist/core/create/source.d.mts +1 -1
- package/dist/core/create/source.d.mts.map +1 -1
- package/dist/core/create/timer.d.mts +1 -1
- package/dist/core/create/timer.d.mts.map +1 -1
- package/dist/core/operators/audit.d.mts +1 -1
- package/dist/core/operators/audit.d.mts.map +1 -1
- package/dist/core/operators/audit.mjs +0 -1
- package/dist/core/operators/audit.mjs.map +1 -1
- package/dist/core/operators/debounce.d.mts +1 -1
- package/dist/core/operators/debounce.d.mts.map +1 -1
- package/dist/core/operators/debounce.mjs +0 -1
- package/dist/core/operators/debounce.mjs.map +1 -1
- package/dist/core/operators/filter.d.mts +1 -1
- package/dist/core/operators/filter.d.mts.map +1 -1
- package/dist/core/operators/filter.mjs +19 -0
- package/dist/core/operators/filter.mjs.map +1 -1
- package/dist/core/operators/map.d.mts +1 -1
- package/dist/core/operators/map.d.mts.map +1 -1
- package/dist/core/operators/map.mjs +0 -1
- package/dist/core/operators/map.mjs.map +1 -1
- package/dist/core/operators/merge-map.d.mts +1 -1
- package/dist/core/operators/merge-map.d.mts.map +1 -1
- package/dist/core/operators/pairwise.d.mts +2 -2
- package/dist/core/operators/pairwise.d.mts.map +1 -1
- package/dist/core/operators/scan.d.mts +1 -1
- package/dist/core/operators/scan.d.mts.map +1 -1
- package/dist/core/operators/skip-if-no-change.d.mts +1 -1
- package/dist/core/operators/skip-if-no-change.d.mts.map +1 -1
- package/dist/core/operators/skip-if-no-change.mjs +0 -1
- package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
- package/dist/core/operators/skip-until.d.mts +1 -1
- package/dist/core/operators/skip-until.d.mts.map +1 -1
- package/dist/core/operators/skip-while.d.mts +1 -1
- package/dist/core/operators/skip-while.d.mts.map +1 -1
- package/dist/core/operators/switch-map.d.mts +1 -1
- package/dist/core/operators/switch-map.d.mts.map +1 -1
- package/dist/core/operators/take-until.d.mts +1 -1
- package/dist/core/operators/take-until.d.mts.map +1 -1
- package/dist/core/operators/take-until.mjs +0 -1
- package/dist/core/operators/take-until.mjs.map +1 -1
- package/dist/core/operators/take-while.d.mts +1 -1
- package/dist/core/operators/take-while.d.mts.map +1 -1
- package/dist/core/operators/throttle.d.mts +1 -1
- package/dist/core/operators/throttle.d.mts.map +1 -1
- package/dist/core/operators/throttle.mjs +0 -1
- package/dist/core/operators/throttle.mjs.map +1 -1
- package/dist/core/operators/with-buffered-from.d.mts +1 -1
- package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
- package/dist/core/operators/with-buffered-from.mjs +0 -1
- package/dist/core/operators/with-buffered-from.mjs.map +1 -1
- package/dist/core/operators/with-current-value-from.d.mts +1 -1
- package/dist/core/operators/with-current-value-from.d.mts.map +1 -1
- package/dist/core/operators/with-initial-value.d.mts +1 -1
- package/dist/core/operators/with-initial-value.d.mts.map +1 -1
- package/dist/core/predefined/operators/attach-index.d.mts +2 -2
- package/dist/core/predefined/operators/attach-index.d.mts.map +1 -1
- package/dist/core/predefined/operators/map-optional.d.mts +1 -1
- package/dist/core/predefined/operators/map-optional.d.mts.map +1 -1
- package/dist/core/predefined/operators/map-result-err.d.mts +1 -1
- package/dist/core/predefined/operators/map-result-err.d.mts.map +1 -1
- package/dist/core/predefined/operators/map-result-ok.d.mts +1 -1
- package/dist/core/predefined/operators/map-result-ok.d.mts.map +1 -1
- package/dist/core/predefined/operators/map-to.d.mts +1 -1
- package/dist/core/predefined/operators/map-to.d.mts.map +1 -1
- package/dist/core/predefined/operators/pluck.d.mts +1 -1
- package/dist/core/predefined/operators/pluck.d.mts.map +1 -1
- package/dist/core/predefined/operators/skip.d.mts +2 -2
- package/dist/core/predefined/operators/skip.d.mts.map +1 -1
- package/dist/core/predefined/operators/take.d.mts +2 -2
- package/dist/core/predefined/operators/take.d.mts.map +1 -1
- package/dist/core/predefined/operators/unwrap-optional.d.mts +1 -1
- package/dist/core/predefined/operators/unwrap-optional.d.mts.map +1 -1
- package/dist/core/predefined/operators/unwrap-optional.mjs +0 -1
- package/dist/core/predefined/operators/unwrap-optional.mjs.map +1 -1
- package/dist/core/predefined/operators/unwrap-result-err.d.mts +1 -1
- package/dist/core/predefined/operators/unwrap-result-err.d.mts.map +1 -1
- package/dist/core/predefined/operators/unwrap-result-ok.d.mts +1 -1
- package/dist/core/predefined/operators/unwrap-result-ok.d.mts.map +1 -1
- package/dist/core/predefined/operators/unwrap-result-ok.mjs +0 -1
- package/dist/core/predefined/operators/unwrap-result-ok.mjs.map +1 -1
- package/dist/core/types/id.d.mts +1 -1
- package/dist/core/types/id.d.mts.map +1 -1
- package/dist/core/types/observable-family.d.mts +3 -3
- package/dist/core/types/observable-family.d.mts.map +1 -1
- package/dist/core/types/observable-kind.d.mts +1 -1
- package/dist/core/types/observable-kind.d.mts.map +1 -1
- package/dist/core/types/observable.d.mts +4 -4
- package/dist/core/types/observable.d.mts.map +1 -1
- package/dist/core/types/types.d.mts +1 -1
- package/dist/core/types/types.d.mts.map +1 -1
- package/dist/core/utils/id-maker.d.mts +1 -1
- package/dist/core/utils/id-maker.d.mts.map +1 -1
- package/dist/core/utils/id-maker.mjs +0 -1
- package/dist/core/utils/id-maker.mjs.map +1 -1
- package/dist/core/utils/max-depth.d.mts +1 -1
- package/dist/core/utils/max-depth.d.mts.map +1 -1
- package/dist/core/utils/observable-utils.d.mts +1 -1
- package/dist/core/utils/observable-utils.d.mts.map +1 -1
- package/dist/core/utils/utils.d.mts +1 -1
- package/dist/core/utils/utils.d.mts.map +1 -1
- package/dist/core/utils/utils.mjs +0 -1
- package/dist/core/utils/utils.mjs.map +1 -1
- package/dist/utils/collect-to-array.d.mts +1 -1
- package/dist/utils/collect-to-array.d.mts.map +1 -1
- package/dist/utils/create-boolean-state.d.mts +17 -14
- package/dist/utils/create-boolean-state.d.mts.map +1 -1
- package/dist/utils/create-boolean-state.mjs +4 -2
- package/dist/utils/create-boolean-state.mjs.map +1 -1
- package/dist/utils/create-reducer.d.mts +49 -2
- package/dist/utils/create-reducer.d.mts.map +1 -1
- package/dist/utils/create-reducer.mjs +31 -2
- package/dist/utils/create-reducer.mjs.map +1 -1
- package/dist/utils/create-state.d.mts +37 -2
- package/dist/utils/create-state.d.mts.map +1 -1
- package/dist/utils/create-state.mjs +36 -2
- package/dist/utils/create-state.mjs.map +1 -1
- package/package.json +13 -13
- package/src/core/base/create-child-observable.mts +1 -2
- package/src/core/base/create-observable-base.mts +17 -19
- package/src/core/base/create-root-observable.mts +1 -1
- package/src/core/base/observable-base.test.mts +7 -7
- package/src/core/combine/combine.mts +10 -11
- package/src/core/combine/merge.mts +8 -10
- package/src/core/combine/zip.mts +11 -13
- package/src/core/create/counter.mts +21 -11
- package/src/core/create/counter.test.mts +183 -0
- package/src/core/create/from-abortable-promise.mts +1 -3
- package/src/core/create/from-promise.mts +1 -2
- package/src/core/create/from-subscribable.mts +3 -4
- package/src/core/create/just.mts +1 -2
- package/src/core/create/source.mts +3 -3
- package/src/core/create/timer.mts +1 -1
- package/src/core/operators/audit.mts +5 -6
- package/src/core/operators/debounce.mts +5 -6
- package/src/core/operators/filter.mts +46 -5
- package/src/core/operators/map.mts +5 -6
- package/src/core/operators/merge-map.mts +5 -5
- package/src/core/operators/pairwise.mts +5 -5
- package/src/core/operators/scan.mts +4 -4
- package/src/core/operators/skip-if-no-change.mts +4 -5
- package/src/core/operators/skip-until.mts +4 -4
- package/src/core/operators/skip-while.mts +4 -4
- package/src/core/operators/switch-map.mts +5 -5
- package/src/core/operators/take-until.mts +4 -5
- package/src/core/operators/take-while.mts +5 -5
- package/src/core/operators/throttle.mts +5 -6
- package/src/core/operators/with-buffered-from.mts +4 -5
- package/src/core/operators/with-current-value-from.mts +4 -4
- package/src/core/operators/with-initial-value.mts +5 -5
- package/src/core/predefined/operators/attach-index.mts +2 -2
- package/src/core/predefined/operators/map-optional.mts +1 -1
- package/src/core/predefined/operators/map-result-err.mts +1 -1
- package/src/core/predefined/operators/map-result-ok.mts +1 -1
- package/src/core/predefined/operators/map-to.mts +1 -1
- package/src/core/predefined/operators/pluck.mts +1 -1
- package/src/core/predefined/operators/skip.mts +2 -2
- package/src/core/predefined/operators/take.mts +2 -2
- package/src/core/predefined/operators/unwrap-optional.mts +2 -3
- package/src/core/predefined/operators/unwrap-result-err.mts +2 -2
- package/src/core/predefined/operators/unwrap-result-ok.mts +2 -3
- package/src/core/types/id.mts +1 -1
- package/src/core/types/observable-family.mts +15 -15
- package/src/core/types/observable-kind.mts +1 -1
- package/src/core/types/observable.mts +4 -4
- package/src/core/types/types.mts +1 -1
- package/src/core/utils/id-maker.mts +4 -5
- package/src/core/utils/max-depth.mts +1 -1
- package/src/core/utils/observable-utils.mts +1 -1
- package/src/core/utils/utils.mts +1 -2
- package/src/utils/collect-to-array.mts +1 -1
- package/src/utils/create-boolean-state.mts +18 -14
- package/src/utils/create-boolean-state.test.mts +23 -0
- package/src/utils/create-reducer.mts +59 -2
- package/src/utils/create-reducer.test.mts +87 -0
- package/src/utils/create-state.mts +38 -2
- package/src/utils/create-state.test.mts +72 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { InitializedObservable } from '../core/index.mjs';
|
|
2
|
+
import type { StateOptions } from './create-reducer.mjs';
|
|
2
3
|
import { createState } from './create-state.mjs';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -6,6 +7,8 @@ import { createState } from './create-state.mjs';
|
|
|
6
7
|
* Extends `createState` with boolean-specific helpers like `toggle`, `setTrue`, and `setFalse`.
|
|
7
8
|
*
|
|
8
9
|
* @param initialState - The initial boolean value
|
|
10
|
+
* @param options - {@link StateOptions}: `equals`, to pass on only the
|
|
11
|
+
* updates that change the state
|
|
9
12
|
* @returns A 2-element tuple: `[state, { setTrue, setFalse, toggle, setState, updateState, resetState, getSnapshot, initialState }]`
|
|
10
13
|
*
|
|
11
14
|
* @example
|
|
@@ -14,24 +17,25 @@ import { createState } from './create-state.mjs';
|
|
|
14
17
|
* import { createBooleanState } from 'synstate';
|
|
15
18
|
* import { useObservableValue } from 'synstate-react-hooks';
|
|
16
19
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* // Menu drawer open/close state.
|
|
21
|
+
* // setTrue and setFalse can be passed directly as callbacks
|
|
22
|
+
* // — no need to create wrapper functions like `() => setState(true)`.
|
|
23
|
+
* const [menuOpen$, { setTrue: openMenu, setFalse: closeMenu }] =
|
|
24
|
+
* createBooleanState(false);
|
|
22
25
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
26
|
+
* const SampleComponent = (): React.JSX.Element => (
|
|
27
|
+
* <MenuDrawer
|
|
28
|
+
* open={useObservableValue(menuOpen$)}
|
|
29
|
+
* onClose={closeMenu}
|
|
30
|
+
* onOpen={openMenu}
|
|
31
|
+
* />
|
|
32
|
+
* );
|
|
30
33
|
* ```
|
|
31
34
|
*/
|
|
32
35
|
|
|
33
36
|
export const createBooleanState = (
|
|
34
37
|
initialState: boolean,
|
|
38
|
+
options?: StateOptions<boolean>,
|
|
35
39
|
): readonly [
|
|
36
40
|
state: InitializedObservable<boolean>,
|
|
37
41
|
utils: Readonly<{
|
|
@@ -46,7 +50,7 @@ export const createBooleanState = (
|
|
|
46
50
|
}>,
|
|
47
51
|
] => {
|
|
48
52
|
const [state, setState, { updateState, resetState, getSnapshot }] =
|
|
49
|
-
createState(initialState);
|
|
53
|
+
createState(initialState, options);
|
|
50
54
|
|
|
51
55
|
return [
|
|
52
56
|
state,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createBooleanState } from './create-boolean-state.mjs';
|
|
2
|
+
|
|
3
|
+
describe(createBooleanState, () => {
|
|
4
|
+
test('with `equals`, setting what it already holds passes nothing on', () => {
|
|
5
|
+
const [state, { setTrue, setFalse }] = createBooleanState(false, {
|
|
6
|
+
equals: Object.is,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const mut_seen: boolean[] = [];
|
|
10
|
+
|
|
11
|
+
state.subscribe((value) => {
|
|
12
|
+
mut_seen.push(value);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
setFalse();
|
|
16
|
+
|
|
17
|
+
setTrue();
|
|
18
|
+
|
|
19
|
+
setTrue();
|
|
20
|
+
|
|
21
|
+
assert.deepStrictEqual(mut_seen, [false, true]);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Reducer } from 'ts-type-forge';
|
|
2
2
|
import { source, type InitializedObservable } from '../core/index.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -9,6 +9,8 @@ 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`, to pass on only the
|
|
13
|
+
* dispatches that change the state
|
|
12
14
|
* @returns A 3-element tuple: `[state, dispatch, { getSnapshot, initialState }]`
|
|
13
15
|
*
|
|
14
16
|
* @example
|
|
@@ -43,10 +45,33 @@ import { source, type InitializedObservable } from '../core/index.mjs';
|
|
|
43
45
|
*
|
|
44
46
|
* assert.deepStrictEqual(stateHistory, [0, 1, 2, 1]);
|
|
45
47
|
* ```
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* const [count, dispatch] = createReducer(
|
|
52
|
+
* (s, action: Readonly<{ type: 'add'; amount: number }>) =>
|
|
53
|
+
* s + action.amount,
|
|
54
|
+
* 0,
|
|
55
|
+
* { equals: Object.is },
|
|
56
|
+
* );
|
|
57
|
+
*
|
|
58
|
+
* const countHistory: number[] = [];
|
|
59
|
+
*
|
|
60
|
+
* count.subscribe((value: number) => {
|
|
61
|
+
* countHistory.push(value);
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* dispatch({ type: 'add', amount: 0 }); // 0 again: nothing is passed on
|
|
65
|
+
*
|
|
66
|
+
* dispatch({ type: 'add', amount: 2 });
|
|
67
|
+
*
|
|
68
|
+
* assert.deepStrictEqual(countHistory, [0, 2]);
|
|
69
|
+
* ```
|
|
46
70
|
*/
|
|
47
71
|
export const createReducer = <S, A>(
|
|
48
72
|
reducer: Reducer<S, A>,
|
|
49
73
|
initialState: S,
|
|
74
|
+
options?: StateOptions<NoInfer<S>>,
|
|
50
75
|
): readonly [
|
|
51
76
|
state: InitializedObservable<S>,
|
|
52
77
|
dispatch: (action: A) => S,
|
|
@@ -57,8 +82,16 @@ export const createReducer = <S, A>(
|
|
|
57
82
|
] => {
|
|
58
83
|
const state = source<S>(initialState);
|
|
59
84
|
|
|
85
|
+
const equals = options?.equals;
|
|
86
|
+
|
|
60
87
|
const dispatch = (action: A): S => {
|
|
61
|
-
const
|
|
88
|
+
const currentState = state.getSnapshot().value;
|
|
89
|
+
|
|
90
|
+
const nextState = reducer(currentState, action);
|
|
91
|
+
|
|
92
|
+
if (equals?.(currentState, nextState) === true) {
|
|
93
|
+
return currentState;
|
|
94
|
+
}
|
|
62
95
|
|
|
63
96
|
state.next(nextState);
|
|
64
97
|
|
|
@@ -69,3 +102,27 @@ export const createReducer = <S, A>(
|
|
|
69
102
|
|
|
70
103
|
return [state, dispatch, { getSnapshot, initialState }] as const;
|
|
71
104
|
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Options of {@link createReducer}, `createState` and `createBooleanState`.
|
|
108
|
+
*/
|
|
109
|
+
export type StateOptions<S> = Readonly<{
|
|
110
|
+
/**
|
|
111
|
+
* Whether the next state is the same as the current one. When it says so,
|
|
112
|
+
* the update is not passed on — no subscriber, and nothing derived from
|
|
113
|
+
* the state, is told of it — and the current state is kept, along with its
|
|
114
|
+
* identity: `setState` and the like return the current state rather than
|
|
115
|
+
* the next.
|
|
116
|
+
*
|
|
117
|
+
* Decide here, where the state is held, rather than downstream with
|
|
118
|
+
* `skipIfNoChange`: an update passed on here reaches whatever reads the
|
|
119
|
+
* state directly before any operator can thin it out.
|
|
120
|
+
*
|
|
121
|
+
* When omitted, every update is passed on, equal or not. That default is
|
|
122
|
+
* stocked in `NEXT_MAJOR.md` to become `Object.is`.
|
|
123
|
+
*
|
|
124
|
+
* @param current - The state held now
|
|
125
|
+
* @param next - The state the update would set
|
|
126
|
+
*/
|
|
127
|
+
equals?: (current: S, next: S) => boolean;
|
|
128
|
+
}>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { FixedLengthTuple } from 'ts-type-forge';
|
|
2
|
+
import { createReducer } from './create-reducer.mjs';
|
|
3
|
+
|
|
4
|
+
describe(createReducer, () => {
|
|
5
|
+
test('passes on every dispatch when no `equals` is given, equal or not', () => {
|
|
6
|
+
const [state, dispatch] = createReducer(keepOrAdd, 0);
|
|
7
|
+
|
|
8
|
+
const seen = record(state);
|
|
9
|
+
|
|
10
|
+
dispatch({ type: 'keep' });
|
|
11
|
+
|
|
12
|
+
dispatch({ type: 'add', amount: 1 });
|
|
13
|
+
|
|
14
|
+
assert.deepStrictEqual(seen, [0, 0, 1]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('with `equals`, does not pass on a dispatch whose next state is equal to the current one', () => {
|
|
18
|
+
const [state, dispatch, { getSnapshot }] = createReducer(keepOrAdd, 0, {
|
|
19
|
+
equals: Object.is,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const seen = record(state);
|
|
23
|
+
|
|
24
|
+
assert.strictEqual(dispatch({ type: 'keep' }), 0);
|
|
25
|
+
|
|
26
|
+
assert.strictEqual(dispatch({ type: 'add', amount: 1 }), 1);
|
|
27
|
+
|
|
28
|
+
assert.strictEqual(dispatch({ type: 'add', amount: 0 }), 1);
|
|
29
|
+
|
|
30
|
+
assert.deepStrictEqual(seen, [0, 1]);
|
|
31
|
+
|
|
32
|
+
assert.strictEqual(getSnapshot(), 1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('keeps the current object when `equals` finds the next one equal to it', () => {
|
|
36
|
+
const initial = { count: 0 } as const;
|
|
37
|
+
|
|
38
|
+
const [state, dispatch, { getSnapshot }] = createReducer(
|
|
39
|
+
(_current: Count, next: Count) => next,
|
|
40
|
+
initial,
|
|
41
|
+
{ equals: (current, next) => current.count === next.count },
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const seen = record(state);
|
|
45
|
+
|
|
46
|
+
assert.strictEqual(dispatch({ count: 0 }), initial);
|
|
47
|
+
|
|
48
|
+
assert.strictEqual(getSnapshot(), initial);
|
|
49
|
+
|
|
50
|
+
assert.deepStrictEqual(seen, [initial]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('asks `equals` with the current state first and the next one second', () => {
|
|
54
|
+
const mut_asked: FixedLengthTuple<2, number>[] = [];
|
|
55
|
+
|
|
56
|
+
const [, dispatch] = createReducer(keepOrAdd, 0, {
|
|
57
|
+
equals: (current, next) => {
|
|
58
|
+
mut_asked.push([current, next]);
|
|
59
|
+
|
|
60
|
+
return false;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
dispatch({ type: 'add', amount: 5 });
|
|
65
|
+
|
|
66
|
+
assert.deepStrictEqual(mut_asked, [[0, 5]]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
type Count = Readonly<{ count: number }>;
|
|
71
|
+
|
|
72
|
+
type KeepOrAdd = Readonly<{ type: 'keep' } | { type: 'add'; amount: number }>;
|
|
73
|
+
|
|
74
|
+
const keepOrAdd = (current: number, action: KeepOrAdd): number =>
|
|
75
|
+
action.type === 'keep' ? current : current + action.amount;
|
|
76
|
+
|
|
77
|
+
const record = <A,>(
|
|
78
|
+
state: Readonly<{ subscribe: (onNext: (value: A) => void) => unknown }>,
|
|
79
|
+
): readonly A[] => {
|
|
80
|
+
const mut_seen: A[] = [];
|
|
81
|
+
|
|
82
|
+
state.subscribe((value) => {
|
|
83
|
+
mut_seen.push(value);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return mut_seen;
|
|
87
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createReducer } from './create-reducer.mjs';
|
|
1
|
+
import type { InitializedObservable } from '../core/index.mjs';
|
|
2
|
+
import { createReducer, type StateOptions } from './create-reducer.mjs';
|
|
3
3
|
|
|
4
4
|
type Action<S> = Readonly<
|
|
5
5
|
| {
|
|
@@ -28,6 +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`, to pass on only the
|
|
32
|
+
* updates that change the state
|
|
31
33
|
* @returns A 3-element tuple: `[state, setState, { updateState, resetState, getSnapshot, initialState }]`
|
|
32
34
|
*
|
|
33
35
|
* @example
|
|
@@ -54,9 +56,42 @@ const reducer = <S,>(state: S, action: Action<S>): S => {
|
|
|
54
56
|
*
|
|
55
57
|
* assert.deepStrictEqual(stateHistory, [0, 10, 11, 0]);
|
|
56
58
|
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* const [filters, setFilters] = createState(
|
|
63
|
+
* { label: 'bug', page: 1 },
|
|
64
|
+
* { equals: fastDeepEqual },
|
|
65
|
+
* );
|
|
66
|
+
*
|
|
67
|
+
* const before = filters.getSnapshot().value;
|
|
68
|
+
*
|
|
69
|
+
* // Derived state is recomputed only when the state really changes.
|
|
70
|
+
* const query = filters.pipe(
|
|
71
|
+
* map(({ label, page }) => `label:${label} page:${page}`),
|
|
72
|
+
* );
|
|
73
|
+
*
|
|
74
|
+
* const queryHistory: string[] = [];
|
|
75
|
+
*
|
|
76
|
+
* query.subscribe((value: string) => {
|
|
77
|
+
* queryHistory.push(value);
|
|
78
|
+
* });
|
|
79
|
+
*
|
|
80
|
+
* setFilters({ label: 'bug', page: 1 }); // equal: nothing is passed on
|
|
81
|
+
*
|
|
82
|
+
* assert.strictEqual(filters.getSnapshot().value, before);
|
|
83
|
+
*
|
|
84
|
+
* setFilters({ label: 'bug', page: 2 });
|
|
85
|
+
*
|
|
86
|
+
* assert.deepStrictEqual(queryHistory, [
|
|
87
|
+
* 'label:bug page:1',
|
|
88
|
+
* 'label:bug page:2',
|
|
89
|
+
* ]);
|
|
90
|
+
* ```
|
|
57
91
|
*/
|
|
58
92
|
export const createState = <S,>(
|
|
59
93
|
initialState: S,
|
|
94
|
+
options?: StateOptions<NoInfer<S>>,
|
|
60
95
|
): readonly [
|
|
61
96
|
state: InitializedObservable<S>,
|
|
62
97
|
setState: (v: S) => S,
|
|
@@ -70,6 +105,7 @@ export const createState = <S,>(
|
|
|
70
105
|
const [state, dispatch, { getSnapshot }] = createReducer<S, Action<S>>(
|
|
71
106
|
reducer,
|
|
72
107
|
initialState,
|
|
108
|
+
options,
|
|
73
109
|
);
|
|
74
110
|
|
|
75
111
|
const updateState = (updateFn: (prev: S) => S): S =>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createState } from './create-state.mjs';
|
|
2
|
+
|
|
3
|
+
describe(createState, () => {
|
|
4
|
+
test('passes on every update when no `equals` is given, equal or not', () => {
|
|
5
|
+
const [state, setState] = createState(0);
|
|
6
|
+
|
|
7
|
+
const seen = record(state);
|
|
8
|
+
|
|
9
|
+
setState(0);
|
|
10
|
+
|
|
11
|
+
assert.deepStrictEqual(seen, [0, 0]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('with `equals`, setState, updateState and resetState do not pass on a value equal to the current one', () => {
|
|
15
|
+
const [state, setState, { updateState, resetState }] = createState(0, {
|
|
16
|
+
equals: Object.is,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const seen = record(state);
|
|
20
|
+
|
|
21
|
+
assert.strictEqual(setState(0), 0);
|
|
22
|
+
|
|
23
|
+
assert.strictEqual(
|
|
24
|
+
updateState((current) => current),
|
|
25
|
+
0,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
assert.strictEqual(resetState(), 0);
|
|
29
|
+
|
|
30
|
+
setState(1);
|
|
31
|
+
|
|
32
|
+
assert.strictEqual(setState(1), 1);
|
|
33
|
+
|
|
34
|
+
assert.deepStrictEqual(seen, [0, 1]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// What `equals` is for: whatever reads the state, directly or through
|
|
38
|
+
// anything derived from it, is told only of a real change, and an equal
|
|
39
|
+
// value written over the current one leaves the object that is there.
|
|
40
|
+
test('keeps the current object when `equals` finds the next one equal to it', () => {
|
|
41
|
+
const [state, setState, { getSnapshot }] = createState<readonly number[]>(
|
|
42
|
+
[1, 2],
|
|
43
|
+
{
|
|
44
|
+
equals: (current, next) =>
|
|
45
|
+
current.length === next.length &&
|
|
46
|
+
current.every((value, index) => value === next[index]),
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const held = getSnapshot();
|
|
51
|
+
|
|
52
|
+
const seen = record(state);
|
|
53
|
+
|
|
54
|
+
assert.strictEqual(setState([1, 2]), held);
|
|
55
|
+
|
|
56
|
+
assert.strictEqual(getSnapshot(), held);
|
|
57
|
+
|
|
58
|
+
assert.deepStrictEqual(seen, [held]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const record = <A,>(
|
|
63
|
+
state: Readonly<{ subscribe: (onNext: (value: A) => void) => unknown }>,
|
|
64
|
+
): readonly A[] => {
|
|
65
|
+
const mut_seen: A[] = [];
|
|
66
|
+
|
|
67
|
+
state.subscribe((value) => {
|
|
68
|
+
mut_seen.push(value);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return mut_seen;
|
|
72
|
+
};
|