synstate 3.0.6 → 3.1.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.
Files changed (172) hide show
  1. package/dist/core/base/create-observable-base.d.mts +2 -2
  2. package/dist/core/base/create-observable-base.d.mts.map +1 -1
  3. package/dist/core/base/create-observable-base.mjs.map +1 -1
  4. package/dist/core/base/create-root-observable.d.mts +1 -1
  5. package/dist/core/base/create-root-observable.d.mts.map +1 -1
  6. package/dist/core/combine/combine.d.mts +2 -2
  7. package/dist/core/combine/combine.d.mts.map +1 -1
  8. package/dist/core/combine/merge.d.mts +2 -2
  9. package/dist/core/combine/merge.d.mts.map +1 -1
  10. package/dist/core/combine/zip.d.mts +2 -2
  11. package/dist/core/combine/zip.d.mts.map +1 -1
  12. package/dist/core/create/counter.d.mts +1 -1
  13. package/dist/core/create/counter.d.mts.map +1 -1
  14. package/dist/core/create/from-abortable-promise.d.mts +1 -1
  15. package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
  16. package/dist/core/create/from-promise.d.mts +1 -1
  17. package/dist/core/create/from-promise.d.mts.map +1 -1
  18. package/dist/core/create/from-subscribable.d.mts +1 -1
  19. package/dist/core/create/from-subscribable.d.mts.map +1 -1
  20. package/dist/core/create/just.d.mts +1 -1
  21. package/dist/core/create/just.d.mts.map +1 -1
  22. package/dist/core/create/source.d.mts +1 -1
  23. package/dist/core/create/source.d.mts.map +1 -1
  24. package/dist/core/create/timer.d.mts +1 -1
  25. package/dist/core/create/timer.d.mts.map +1 -1
  26. package/dist/core/operators/audit.d.mts +1 -1
  27. package/dist/core/operators/audit.d.mts.map +1 -1
  28. package/dist/core/operators/debounce.d.mts +1 -1
  29. package/dist/core/operators/debounce.d.mts.map +1 -1
  30. package/dist/core/operators/filter.d.mts +1 -1
  31. package/dist/core/operators/filter.d.mts.map +1 -1
  32. package/dist/core/operators/map.d.mts +1 -1
  33. package/dist/core/operators/map.d.mts.map +1 -1
  34. package/dist/core/operators/merge-map.d.mts +1 -1
  35. package/dist/core/operators/merge-map.d.mts.map +1 -1
  36. package/dist/core/operators/pairwise.d.mts +2 -2
  37. package/dist/core/operators/pairwise.d.mts.map +1 -1
  38. package/dist/core/operators/scan.d.mts +1 -1
  39. package/dist/core/operators/scan.d.mts.map +1 -1
  40. package/dist/core/operators/skip-if-no-change.d.mts +1 -1
  41. package/dist/core/operators/skip-if-no-change.d.mts.map +1 -1
  42. package/dist/core/operators/skip-until.d.mts +1 -1
  43. package/dist/core/operators/skip-until.d.mts.map +1 -1
  44. package/dist/core/operators/skip-while.d.mts +1 -1
  45. package/dist/core/operators/skip-while.d.mts.map +1 -1
  46. package/dist/core/operators/switch-map.d.mts +1 -1
  47. package/dist/core/operators/switch-map.d.mts.map +1 -1
  48. package/dist/core/operators/take-until.d.mts +1 -1
  49. package/dist/core/operators/take-until.d.mts.map +1 -1
  50. package/dist/core/operators/take-while.d.mts +1 -1
  51. package/dist/core/operators/take-while.d.mts.map +1 -1
  52. package/dist/core/operators/throttle.d.mts +1 -1
  53. package/dist/core/operators/throttle.d.mts.map +1 -1
  54. package/dist/core/operators/with-buffered-from.d.mts +1 -1
  55. package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
  56. package/dist/core/operators/with-current-value-from.d.mts +1 -1
  57. package/dist/core/operators/with-current-value-from.d.mts.map +1 -1
  58. package/dist/core/operators/with-initial-value.d.mts +1 -1
  59. package/dist/core/operators/with-initial-value.d.mts.map +1 -1
  60. package/dist/core/predefined/operators/attach-index.d.mts +2 -2
  61. package/dist/core/predefined/operators/attach-index.d.mts.map +1 -1
  62. package/dist/core/predefined/operators/map-optional.d.mts +1 -1
  63. package/dist/core/predefined/operators/map-optional.d.mts.map +1 -1
  64. package/dist/core/predefined/operators/map-result-err.d.mts +1 -1
  65. package/dist/core/predefined/operators/map-result-err.d.mts.map +1 -1
  66. package/dist/core/predefined/operators/map-result-ok.d.mts +1 -1
  67. package/dist/core/predefined/operators/map-result-ok.d.mts.map +1 -1
  68. package/dist/core/predefined/operators/map-to.d.mts +1 -1
  69. package/dist/core/predefined/operators/map-to.d.mts.map +1 -1
  70. package/dist/core/predefined/operators/pluck.d.mts +1 -1
  71. package/dist/core/predefined/operators/pluck.d.mts.map +1 -1
  72. package/dist/core/predefined/operators/skip.d.mts +2 -2
  73. package/dist/core/predefined/operators/skip.d.mts.map +1 -1
  74. package/dist/core/predefined/operators/take.d.mts +2 -2
  75. package/dist/core/predefined/operators/take.d.mts.map +1 -1
  76. package/dist/core/predefined/operators/unwrap-optional.d.mts +1 -1
  77. package/dist/core/predefined/operators/unwrap-optional.d.mts.map +1 -1
  78. package/dist/core/predefined/operators/unwrap-result-err.d.mts +1 -1
  79. package/dist/core/predefined/operators/unwrap-result-err.d.mts.map +1 -1
  80. package/dist/core/predefined/operators/unwrap-result-ok.d.mts +1 -1
  81. package/dist/core/predefined/operators/unwrap-result-ok.d.mts.map +1 -1
  82. package/dist/core/types/id.d.mts +1 -1
  83. package/dist/core/types/id.d.mts.map +1 -1
  84. package/dist/core/types/observable-family.d.mts +3 -3
  85. package/dist/core/types/observable-family.d.mts.map +1 -1
  86. package/dist/core/types/observable-kind.d.mts +1 -1
  87. package/dist/core/types/observable-kind.d.mts.map +1 -1
  88. package/dist/core/types/observable.d.mts +4 -4
  89. package/dist/core/types/observable.d.mts.map +1 -1
  90. package/dist/core/types/types.d.mts +1 -1
  91. package/dist/core/types/types.d.mts.map +1 -1
  92. package/dist/core/utils/id-maker.d.mts +1 -1
  93. package/dist/core/utils/id-maker.d.mts.map +1 -1
  94. package/dist/core/utils/max-depth.d.mts +1 -1
  95. package/dist/core/utils/max-depth.d.mts.map +1 -1
  96. package/dist/core/utils/observable-utils.d.mts +1 -1
  97. package/dist/core/utils/observable-utils.d.mts.map +1 -1
  98. package/dist/core/utils/utils.d.mts +1 -1
  99. package/dist/core/utils/utils.d.mts.map +1 -1
  100. package/dist/utils/collect-to-array.d.mts +1 -1
  101. package/dist/utils/collect-to-array.d.mts.map +1 -1
  102. package/dist/utils/create-boolean-state.d.mts +17 -14
  103. package/dist/utils/create-boolean-state.d.mts.map +1 -1
  104. package/dist/utils/create-boolean-state.mjs +4 -2
  105. package/dist/utils/create-boolean-state.mjs.map +1 -1
  106. package/dist/utils/create-reducer.d.mts +49 -2
  107. package/dist/utils/create-reducer.d.mts.map +1 -1
  108. package/dist/utils/create-reducer.mjs +31 -2
  109. package/dist/utils/create-reducer.mjs.map +1 -1
  110. package/dist/utils/create-state.d.mts +37 -2
  111. package/dist/utils/create-state.d.mts.map +1 -1
  112. package/dist/utils/create-state.mjs +36 -2
  113. package/dist/utils/create-state.mjs.map +1 -1
  114. package/package.json +11 -11
  115. package/src/core/base/create-child-observable.mts +1 -1
  116. package/src/core/base/create-observable-base.mts +14 -18
  117. package/src/core/base/create-root-observable.mts +1 -1
  118. package/src/core/base/observable-base.test.mts +7 -7
  119. package/src/core/combine/combine.mts +10 -10
  120. package/src/core/combine/merge.mts +8 -8
  121. package/src/core/combine/zip.mts +11 -11
  122. package/src/core/create/counter.mts +1 -1
  123. package/src/core/create/from-abortable-promise.mts +1 -1
  124. package/src/core/create/from-promise.mts +1 -1
  125. package/src/core/create/from-subscribable.mts +3 -3
  126. package/src/core/create/just.mts +1 -1
  127. package/src/core/create/source.mts +3 -3
  128. package/src/core/create/timer.mts +1 -1
  129. package/src/core/operators/audit.mts +5 -5
  130. package/src/core/operators/debounce.mts +5 -5
  131. package/src/core/operators/filter.mts +5 -5
  132. package/src/core/operators/map.mts +5 -5
  133. package/src/core/operators/merge-map.mts +5 -5
  134. package/src/core/operators/pairwise.mts +5 -5
  135. package/src/core/operators/scan.mts +4 -4
  136. package/src/core/operators/skip-if-no-change.mts +4 -4
  137. package/src/core/operators/skip-until.mts +4 -4
  138. package/src/core/operators/skip-while.mts +4 -4
  139. package/src/core/operators/switch-map.mts +5 -5
  140. package/src/core/operators/take-until.mts +4 -4
  141. package/src/core/operators/take-while.mts +5 -5
  142. package/src/core/operators/throttle.mts +5 -5
  143. package/src/core/operators/with-buffered-from.mts +4 -4
  144. package/src/core/operators/with-current-value-from.mts +4 -4
  145. package/src/core/operators/with-initial-value.mts +5 -5
  146. package/src/core/predefined/operators/attach-index.mts +2 -2
  147. package/src/core/predefined/operators/map-optional.mts +1 -1
  148. package/src/core/predefined/operators/map-result-err.mts +1 -1
  149. package/src/core/predefined/operators/map-result-ok.mts +1 -1
  150. package/src/core/predefined/operators/map-to.mts +1 -1
  151. package/src/core/predefined/operators/pluck.mts +1 -1
  152. package/src/core/predefined/operators/skip.mts +2 -2
  153. package/src/core/predefined/operators/take.mts +2 -2
  154. package/src/core/predefined/operators/unwrap-optional.mts +2 -2
  155. package/src/core/predefined/operators/unwrap-result-err.mts +2 -2
  156. package/src/core/predefined/operators/unwrap-result-ok.mts +2 -2
  157. package/src/core/types/id.mts +1 -1
  158. package/src/core/types/observable-family.mts +15 -15
  159. package/src/core/types/observable-kind.mts +1 -1
  160. package/src/core/types/observable.mts +4 -4
  161. package/src/core/types/types.mts +1 -1
  162. package/src/core/utils/id-maker.mts +4 -4
  163. package/src/core/utils/max-depth.mts +1 -1
  164. package/src/core/utils/observable-utils.mts +1 -1
  165. package/src/core/utils/utils.mts +1 -1
  166. package/src/utils/collect-to-array.mts +1 -1
  167. package/src/utils/create-boolean-state.mts +18 -14
  168. package/src/utils/create-boolean-state.test.mts +23 -0
  169. package/src/utils/create-reducer.mts +59 -2
  170. package/src/utils/create-reducer.test.mts +87 -0
  171. package/src/utils/create-state.mts +38 -2
  172. package/src/utils/create-state.test.mts +72 -0
@@ -1,21 +1,21 @@
1
1
  import { expectType, type Result } from 'ts-data-forge';
2
- import {
3
- type ArrayElement,
4
- type FixedLengthTuple,
5
- type NonEmptyTuple,
6
- type SafeUint,
2
+ import type {
3
+ ArrayElement,
4
+ FixedLengthTuple,
5
+ NonEmptyTuple,
6
+ SafeUint,
7
7
  } from 'ts-type-forge';
8
- import {
9
- type AsyncChildObservable,
10
- type InitializedObservable,
11
- type InitializedRootObservable,
12
- type InitializedSyncChildObservable,
13
- type Observable,
14
- type RootObservable,
15
- type SyncChildObservable,
16
- type Unwrap,
8
+ import type {
9
+ AsyncChildObservable,
10
+ InitializedObservable,
11
+ InitializedRootObservable,
12
+ InitializedSyncChildObservable,
13
+ Observable,
14
+ RootObservable,
15
+ SyncChildObservable,
16
+ Unwrap,
17
17
  } from './observable.mjs';
18
- import { type NonEmptyUnknownList } from './types.mjs';
18
+ import type { NonEmptyUnknownList } from './types.mjs';
19
19
 
20
20
  // RootObservable
21
21
 
@@ -1,4 +1,4 @@
1
- import { type StrictExclude } from 'ts-type-forge';
1
+ import type { StrictExclude } from 'ts-type-forge';
2
2
 
3
3
  export type ObservableKind = 'root' | 'sync child' | 'async child';
4
4
 
@@ -1,8 +1,8 @@
1
1
  import { expectType, type Optional, type Some } from 'ts-data-forge';
2
- import { type FixedLengthTuple, type StrictOmit } from 'ts-type-forge';
3
- import { type ObservableId, type UpdateToken } from './id.mjs';
4
- import { type ObservableKind } from './observable-kind.mjs';
5
- import { type NonEmptyUnknownList, type Subscription } from './types.mjs';
2
+ import type { FixedLengthTuple, StrictOmit } from 'ts-type-forge';
3
+ import type { ObservableId, UpdateToken } from './id.mjs';
4
+ import type { ObservableKind } from './observable-kind.mjs';
5
+ import type { NonEmptyUnknownList, Subscription } from './types.mjs';
6
6
 
7
7
  /**
8
8
  * Inheritance
@@ -1,5 +1,5 @@
1
1
  import { expectType, type Queue } from 'ts-data-forge';
2
- import { type NonEmptyTuple } from 'ts-type-forge';
2
+ import type { NonEmptyTuple } from 'ts-type-forge';
3
3
 
4
4
  export type TupleToQueueTuple<T extends readonly unknown[]> = Readonly<{
5
5
  [P in keyof T]: Queue<T[P]>;
@@ -1,7 +1,7 @@
1
- import {
2
- type ObservableId,
3
- type SubscriberId,
4
- type UpdateToken,
1
+ import type {
2
+ ObservableId,
3
+ SubscriberId,
4
+ UpdateToken,
5
5
  } from '../types/index.mjs';
6
6
 
7
7
  function* idMaker<T extends symbol>(): Generator<T, T, T> {
@@ -1,5 +1,5 @@
1
1
  import { Arr, Optional, pipe } from 'ts-data-forge';
2
- import { type Observable } from '../types/index.mjs';
2
+ import type { Observable } from '../types/index.mjs';
3
3
 
4
4
  export const maxDepth = (parents: readonly Observable<unknown>[]): number =>
5
5
  pipe(Arr.maxBy(parents, (p) => p.depth))
@@ -1,4 +1,4 @@
1
- import { type Subscriber } from '../types/index.mjs';
1
+ import type { Subscriber } from '../types/index.mjs';
2
2
 
3
3
  export const toSubscriber = <A,>(
4
4
  onNext: (v: A) => void,
@@ -1,5 +1,5 @@
1
1
  import { Arr, asSafeInt, asUint32, SafeInt } from 'ts-data-forge';
2
- import { type NegativeInt32, type Uint32 } from 'ts-type-forge';
2
+ import type { NegativeInt32, Uint32 } from 'ts-type-forge';
3
3
 
4
4
  export const halfInt = (x: SafeInt): SafeInt =>
5
5
  x % 2 === 0
@@ -1,4 +1,4 @@
1
- import { type Observable } from '../core/index.mjs';
1
+ import type { Observable } from '../core/index.mjs';
2
2
 
3
3
  export const collectToArray = <A,>(
4
4
  observable: Observable<A>,
@@ -1,4 +1,5 @@
1
- import { type InitializedObservable } from '../core/index.mjs';
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
- * // Menu drawer open/close state.
18
- * // setTrue and setFalse can be passed directly as callbacks
19
- * // — no need to create wrapper functions like `() => setState(true)`.
20
- * const [menuOpen$, { setTrue: openMenu, setFalse: closeMenu }] =
21
- * createBooleanState(false);
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
- * const SampleComponent = (): React.JSX.Element => (
24
- * <MenuDrawer
25
- * open={useObservableValue(menuOpen$)}
26
- * onClose={closeMenu}
27
- * onOpen={openMenu}
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 { type Reducer } from 'ts-type-forge';
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 nextState = reducer(state.getSnapshot().value, action);
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 { type InitializedObservable } from '../core/index.mjs';
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
+ };