synstate 4.0.0 → 5.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.
Files changed (77) hide show
  1. package/README.md +58 -1
  2. package/dist/core/class/child-observable-class.d.mts +4 -7
  3. package/dist/core/class/child-observable-class.d.mts.map +1 -1
  4. package/dist/core/class/child-observable-class.mjs +8 -25
  5. package/dist/core/class/child-observable-class.mjs.map +1 -1
  6. package/dist/core/class/create-child-observable.d.mts +8 -5
  7. package/dist/core/class/create-child-observable.d.mts.map +1 -1
  8. package/dist/core/class/create-child-observable.mjs.map +1 -1
  9. package/dist/core/class/manager-observable-class.d.mts +4 -15
  10. package/dist/core/class/manager-observable-class.d.mts.map +1 -1
  11. package/dist/core/class/manager-observable-class.mjs +5 -91
  12. package/dist/core/class/manager-observable-class.mjs.map +1 -1
  13. package/dist/core/class/observable-base-class.d.mts +9 -2
  14. package/dist/core/class/observable-base-class.d.mts.map +1 -1
  15. package/dist/core/class/observable-base-class.mjs +17 -0
  16. package/dist/core/class/observable-base-class.mjs.map +1 -1
  17. package/dist/core/class/observable-node.d.mts +4 -8
  18. package/dist/core/class/observable-node.d.mts.map +1 -1
  19. package/dist/core/class/observable-node.mjs.map +1 -1
  20. package/dist/core/class/propagation-state.d.mts +26 -0
  21. package/dist/core/class/propagation-state.d.mts.map +1 -1
  22. package/dist/core/class/propagation-state.mjs +190 -0
  23. package/dist/core/class/propagation-state.mjs.map +1 -1
  24. package/dist/core/create/empty.d.mts +32 -0
  25. package/dist/core/create/empty.d.mts.map +1 -0
  26. package/dist/core/create/empty.mjs +39 -0
  27. package/dist/core/create/empty.mjs.map +1 -0
  28. package/dist/core/create/index.d.mts +1 -0
  29. package/dist/core/create/index.d.mts.map +1 -1
  30. package/dist/core/create/index.mjs +1 -0
  31. package/dist/core/create/index.mjs.map +1 -1
  32. package/dist/core/operators/index.d.mts +1 -0
  33. package/dist/core/operators/index.d.mts.map +1 -1
  34. package/dist/core/operators/index.mjs +1 -0
  35. package/dist/core/operators/index.mjs.map +1 -1
  36. package/dist/core/operators/keep-equal-parts.d.mts +85 -0
  37. package/dist/core/operators/keep-equal-parts.d.mts.map +1 -0
  38. package/dist/core/operators/keep-equal-parts.mjs +129 -0
  39. package/dist/core/operators/keep-equal-parts.mjs.map +1 -0
  40. package/dist/core/types/id.d.mts +6 -2
  41. package/dist/core/types/id.d.mts.map +1 -1
  42. package/dist/core/types/observable-family.d.mts +1 -0
  43. package/dist/core/types/observable-family.d.mts.map +1 -1
  44. package/dist/core/utils/id-maker.d.mts +0 -8
  45. package/dist/core/utils/id-maker.d.mts.map +1 -1
  46. package/dist/core/utils/id-maker.mjs +1 -11
  47. package/dist/core/utils/id-maker.mjs.map +1 -1
  48. package/dist/utils/create-reducer.d.mts +30 -1
  49. package/dist/utils/create-reducer.d.mts.map +1 -1
  50. package/dist/utils/create-reducer.mjs +8 -1
  51. package/dist/utils/create-reducer.mjs.map +1 -1
  52. package/dist/utils/create-state.d.mts +53 -1
  53. package/dist/utils/create-state.d.mts.map +1 -1
  54. package/dist/utils/create-state.mjs +52 -0
  55. package/dist/utils/create-state.mjs.map +1 -1
  56. package/package.json +10 -10
  57. package/src/core/class/child-observable-class.mts +15 -49
  58. package/src/core/class/create-child-observable.mts +8 -5
  59. package/src/core/class/manager-observable-class.mts +15 -134
  60. package/src/core/class/observable-base-class.mts +23 -2
  61. package/src/core/class/observable-node.mts +5 -11
  62. package/src/core/class/propagation-order.test.mts +107 -0
  63. package/src/core/class/propagation-state.mts +262 -0
  64. package/src/core/class/remove-completed.test.mts +2 -2
  65. package/src/core/class/update-loop.test.mts +104 -0
  66. package/src/core/create/empty.mts +42 -0
  67. package/src/core/create/empty.test.mts +65 -0
  68. package/src/core/create/index.mts +1 -0
  69. package/src/core/operators/index.mts +1 -0
  70. package/src/core/operators/keep-equal-parts.mts +167 -0
  71. package/src/core/operators/keep-equal-parts.test.mts +132 -0
  72. package/src/core/types/id.mts +6 -2
  73. package/src/core/types/observable-family.mts +5 -0
  74. package/src/core/utils/id-maker.mts +1 -12
  75. package/src/utils/create-reducer.mts +42 -2
  76. package/src/utils/create-state.mts +53 -1
  77. package/src/utils/create-state.test.mts +88 -0
@@ -29,7 +29,8 @@ const reducer = <S,>(state: S, action: Action<S>): S => {
29
29
  * @template S - The type of the state
30
30
  * @param initialState - The initial value of the state
31
31
  * @param options - {@link StateOptions}: `equals` (default `Object.is`),
32
- * to pass on only the updates that change the state
32
+ * to pass on only the updates that change the state, and `keepEqualParts`,
33
+ * to keep the parts of the state an update did not change
33
34
  * @returns A 3-element tuple: `[state, setState, { updateState, resetState, getSnapshot, initialState }]`
34
35
  *
35
36
  * @example
@@ -89,6 +90,57 @@ const reducer = <S,>(state: S, action: Action<S>): S => {
89
90
  * ]);
90
91
  * ```
91
92
  *
93
+ * @example
94
+ * ```ts
95
+ * type PullReport = DeepReadonly<{
96
+ * summary: { open: number };
97
+ * pulls: { number: number; title: string }[];
98
+ * }>;
99
+ *
100
+ * const [report, setReport] = createState<PullReport>(
101
+ * { summary: { open: 0 }, pulls: [] },
102
+ * { keepEqualParts: true },
103
+ * );
104
+ *
105
+ * const summary = report.pipe(map((r) => r.summary)).pipe(skipIfNoChange());
106
+ *
107
+ * const summaryHistory: unknown[] = [];
108
+ *
109
+ * summary.subscribe((value) => {
110
+ * summaryHistory.push(value);
111
+ * });
112
+ *
113
+ * // A server answers each poll with the whole report, a new object every
114
+ * // time. What did not change keeps the object already held.
115
+ * setReport({
116
+ * summary: { open: 1 },
117
+ * pulls: [{ number: 1, title: 'one' }],
118
+ * });
119
+ *
120
+ * const held = report.getSnapshot().value;
121
+ *
122
+ * // An answer that changes nothing is not passed on at all.
123
+ * setReport({
124
+ * summary: { open: 1 },
125
+ * pulls: [{ number: 1, title: 'one' }],
126
+ * });
127
+ *
128
+ * assert.strictEqual(report.getSnapshot().value, held);
129
+ *
130
+ * // An edited title: a new report and a new array, while the summary is
131
+ * // the one held, so `summary` is not told of it.
132
+ * setReport({
133
+ * summary: { open: 1 },
134
+ * pulls: [{ number: 1, title: 'one (edited)' }],
135
+ * });
136
+ *
137
+ * assert.notStrictEqual(report.getSnapshot().value, held);
138
+ *
139
+ * assert.strictEqual(report.getSnapshot().value.summary, held.summary);
140
+ *
141
+ * assert.deepStrictEqual(summaryHistory, [{ open: 0 }, { open: 1 }]);
142
+ * ```
143
+ *
92
144
  * @note Do not call `setState`, `updateState` or `resetState` from a subscriber or a projection while another
93
145
  * update is propagating: the write propagates immediately, so subscribers can
94
146
  * see an intermediate state, and it logs a `console.warn` (once per
@@ -1,3 +1,4 @@
1
+ import { hasKey, isRecord } from 'ts-data-forge';
1
2
  import { createState } from './create-state.mjs';
2
3
 
3
4
  describe(createState, () => {
@@ -75,6 +76,93 @@ describe(createState, () => {
75
76
 
76
77
  assert.deepStrictEqual(seen, [held]);
77
78
  });
79
+
80
+ describe('keepEqualParts', () => {
81
+ type Report = Readonly<{
82
+ summary: Readonly<{ open: number }>;
83
+ entries: readonly Readonly<{ number: number; title: string }>[];
84
+ }>;
85
+
86
+ const initial: Report = {
87
+ summary: { open: 1 },
88
+ entries: [{ number: 1, title: 'a' }],
89
+ } as const;
90
+
91
+ test('passes nothing on, and keeps the current object, when the next state is deeply equal', () => {
92
+ const [state, setState, { getSnapshot }] = createState(initial, {
93
+ keepEqualParts: true,
94
+ });
95
+
96
+ const seen = record(state);
97
+
98
+ assert.strictEqual(
99
+ setState({
100
+ summary: { open: 1 },
101
+ entries: [{ number: 1, title: 'a' }],
102
+ }),
103
+ initial,
104
+ );
105
+
106
+ assert.strictEqual(getSnapshot(), initial);
107
+
108
+ assert.deepStrictEqual(seen, [initial]);
109
+ });
110
+
111
+ test('writes a new state whose unchanged branches are the current ones', () => {
112
+ const [state, setState] = createState(initial, { keepEqualParts: true });
113
+
114
+ const seen = record(state);
115
+
116
+ const written = setState({
117
+ summary: { open: 1 },
118
+ entries: [
119
+ { number: 1, title: 'a' },
120
+ { number: 2, title: 'b' },
121
+ ],
122
+ });
123
+
124
+ assert.deepStrictEqual(seen, [initial, written]);
125
+
126
+ assert.strictEqual(written.summary, initial.summary);
127
+
128
+ assert.strictEqual(written.entries[0], initial.entries[0]);
129
+ });
130
+
131
+ test('applies to `updateState` and matches items by `key`', () => {
132
+ const [state, , { updateState }] = createState(initial, {
133
+ keepEqualParts: {
134
+ key: (item) =>
135
+ isRecord(item) && hasKey(item, 'number') ? item.number : undefined,
136
+ },
137
+ });
138
+
139
+ // A whole new object, as a server answer would be.
140
+ const written = updateState(() => ({
141
+ summary: { open: 1 },
142
+ entries: [
143
+ { number: 0, title: 'new' },
144
+ { number: 1, title: 'a' },
145
+ ],
146
+ }));
147
+
148
+ assert.strictEqual(written.summary, initial.summary);
149
+
150
+ assert.strictEqual(written.entries[1], initial.entries[0]);
151
+
152
+ assert.strictEqual(state.getSnapshot().value, written);
153
+ });
154
+
155
+ test('is off by default', () => {
156
+ const [, setState] = createState(initial);
157
+
158
+ const next: Report = {
159
+ summary: { open: 1 },
160
+ entries: [{ number: 1, title: 'a' }],
161
+ } as const;
162
+
163
+ assert.strictEqual(setState(next), next);
164
+ });
165
+ });
78
166
  });
79
167
 
80
168
  const record = <A,>(