synstate 4.1.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 (66) 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/operators/index.d.mts +1 -0
  25. package/dist/core/operators/index.d.mts.map +1 -1
  26. package/dist/core/operators/index.mjs +1 -0
  27. package/dist/core/operators/index.mjs.map +1 -1
  28. package/dist/core/operators/keep-equal-parts.d.mts +85 -0
  29. package/dist/core/operators/keep-equal-parts.d.mts.map +1 -0
  30. package/dist/core/operators/keep-equal-parts.mjs +129 -0
  31. package/dist/core/operators/keep-equal-parts.mjs.map +1 -0
  32. package/dist/core/types/id.d.mts +6 -2
  33. package/dist/core/types/id.d.mts.map +1 -1
  34. package/dist/core/types/observable-family.d.mts +1 -0
  35. package/dist/core/types/observable-family.d.mts.map +1 -1
  36. package/dist/core/utils/id-maker.d.mts +0 -8
  37. package/dist/core/utils/id-maker.d.mts.map +1 -1
  38. package/dist/core/utils/id-maker.mjs +1 -11
  39. package/dist/core/utils/id-maker.mjs.map +1 -1
  40. package/dist/utils/create-reducer.d.mts +30 -1
  41. package/dist/utils/create-reducer.d.mts.map +1 -1
  42. package/dist/utils/create-reducer.mjs +8 -1
  43. package/dist/utils/create-reducer.mjs.map +1 -1
  44. package/dist/utils/create-state.d.mts +53 -1
  45. package/dist/utils/create-state.d.mts.map +1 -1
  46. package/dist/utils/create-state.mjs +52 -0
  47. package/dist/utils/create-state.mjs.map +1 -1
  48. package/package.json +9 -9
  49. package/src/core/class/child-observable-class.mts +15 -49
  50. package/src/core/class/create-child-observable.mts +8 -5
  51. package/src/core/class/manager-observable-class.mts +15 -134
  52. package/src/core/class/observable-base-class.mts +23 -2
  53. package/src/core/class/observable-node.mts +5 -11
  54. package/src/core/class/propagation-order.test.mts +107 -0
  55. package/src/core/class/propagation-state.mts +262 -0
  56. package/src/core/class/remove-completed.test.mts +2 -2
  57. package/src/core/class/update-loop.test.mts +104 -0
  58. package/src/core/operators/index.mts +1 -0
  59. package/src/core/operators/keep-equal-parts.mts +167 -0
  60. package/src/core/operators/keep-equal-parts.test.mts +132 -0
  61. package/src/core/types/id.mts +6 -2
  62. package/src/core/types/observable-family.mts +5 -0
  63. package/src/core/utils/id-maker.mts +1 -12
  64. package/src/utils/create-reducer.mts +42 -2
  65. package/src/utils/create-state.mts +53 -1
  66. package/src/utils/create-state.test.mts +88 -0
@@ -1,3 +1,4 @@
1
+ import { type ReplaceEqualDeepOptions } from 'ts-data-forge';
1
2
  import type { Reducer } from 'ts-type-forge';
2
3
  import { type InitializedObservable } from '../core/index.mjs';
3
4
  /**
@@ -9,7 +10,8 @@ import { type InitializedObservable } from '../core/index.mjs';
9
10
  * @param reducer - A pure function that takes current state and action, returns new state
10
11
  * @param initialState - The initial value of the state
11
12
  * @param options - {@link StateOptions}: `equals` (default `Object.is`),
12
- * to pass on only the dispatches that change the state
13
+ * to pass on only the dispatches that change the state, and
14
+ * `keepEqualParts`, to keep the parts of the state a dispatch did not change
13
15
  * @returns A 3-element tuple: `[state, dispatch, { getSnapshot, initialState }]`.
14
16
  * `dispatch` returns the state it leaves; once `state` is completed, that
15
17
  * is the state held.
@@ -102,5 +104,32 @@ export type StateOptions<S> = Readonly<{
102
104
  * @param next - The state the update would set
103
105
  */
104
106
  equals?: (current: S, next: S) => boolean;
107
+ /**
108
+ * Whether an update keeps the parts of the state it did not change. With
109
+ * `true`, the next state is compared with the current one before it is
110
+ * written, and every part of it deeply equal to the same part of the
111
+ * current state is replaced by that part (`replaceEqualDeep` of
112
+ * ts-data-forge): a reader of an unchanged part keeps the object it has, so
113
+ * `skipIfNoChange()` with `Object.is` stops it. When the whole next state is
114
+ * deeply equal, the current state is kept and nothing is passed on, so
115
+ * `equals` can stay `Object.is`.
116
+ *
117
+ * This is for a state written whole from a source that answers with a new
118
+ * object every time, a server polled for a report, say. Array items are
119
+ * matched by index; pass `{ key }` to match them by key, an item for which
120
+ * `key` returns `undefined` being matched by index. Only plain objects and
121
+ * arrays are walked into; a `Map`, a `Set`, a `Date` or a class instance is
122
+ * compared and kept whole.
123
+ *
124
+ * Prefer this over `keepEqualParts()` downstream: keeping the parts where
125
+ * the state is written means no update with nothing in it is passed on at
126
+ * all, and nothing downstream has anything to undo.
127
+ *
128
+ * Defaults to `false`: the next state is written as given. With it on,
129
+ * each update compares deeply every part of the next state that is not
130
+ * the very object of the current one; a state whose updates already keep
131
+ * what they do not change, as `{ ...prev, x }` does, gains nothing from it.
132
+ */
133
+ keepEqualParts?: boolean | ReplaceEqualDeepOptions;
105
134
  }>;
106
135
  //# sourceMappingURL=create-reducer.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-reducer.d.mts","sourceRoot":"","sources":["../../src/utils/create-reducer.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAU,KAAK,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2EG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,EAAE,CAAC,WACvB,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,gBACR,CAAC,YACL,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KACjC,SAAS,CACV,KAAK,EAAE,qBAAqB,CAAC,CAAC,CAAC,EAC/B,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,EAC1B,KAAK,EAAE,QAAQ,CAAC;IACd,WAAW,EAAE,MAAM,CAAC,CAAC;IACrB,YAAY,EAAE,CAAC,CAAC;CACjB,CAAC,CAoBH,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,QAAQ,CAAC;IACrC;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC;CAC3C,CAAC,CAAC"}
1
+ {"version":3,"file":"create-reducer.d.mts","sourceRoot":"","sources":["../../src/utils/create-reducer.mts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAC/E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAU,KAAK,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4EG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,EAAE,CAAC,WACvB,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,gBACR,CAAC,YACL,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KACjC,SAAS,CACV,KAAK,EAAE,qBAAqB,CAAC,CAAC,CAAC,EAC/B,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,EAC1B,KAAK,EAAE,QAAQ,CAAC;IACd,WAAW,EAAE,MAAM,CAAC,CAAC;IACrB,YAAY,EAAE,CAAC,CAAC;CACjB,CAAC,CA8BH,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,QAAQ,CAAC;IACrC;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC;IAE1C;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,cAAc,CAAC,EAAE,OAAO,GAAG,uBAAuB,CAAC;CACpD,CAAC,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { replaceEqualDeep } from 'ts-data-forge';
1
2
  import { source } from '../core/index.mjs';
2
3
 
3
4
 
@@ -73,14 +74,20 @@ import { source } from '../core/index.mjs';
73
74
 
74
75
 
75
76
 
77
+
76
78
 
77
79
 
78
80
  export const createReducer = (reducer, initialState, options) => {
79
81
  const state = source(initialState);
80
82
  const equals = options?.equals ?? Object.is;
83
+ const keepEqualParts = options?.keepEqualParts ?? false;
84
+ const keepOptions = typeof keepEqualParts === 'boolean' ? undefined : keepEqualParts;
81
85
  const dispatch = (action) => {
82
86
  const currentState = state.getSnapshot().value;
83
- const nextState = reducer(currentState, action);
87
+ const reduced = reducer(currentState, action);
88
+ const nextState = keepEqualParts === false
89
+ ? reduced
90
+ : replaceEqualDeep(currentState, reduced, keepOptions);
84
91
 
85
92
  return equals(currentState, nextState)
86
93
  ? currentState
@@ -1 +1 @@
1
- {"version":3,"file":"create-reducer.mjs","sourceRoot":"","sources":["../../src/utils/create-reducer.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAA8B,MAAM,mBAAmB,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2EG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,OAAsB,EACtB,YAAe,EACf,OAAkC,EAQlC,EAAE;IACF,MAAM,KAAK,GAAG,MAAM,CAAI,YAAY,CAAC,CAAC;IAEtC,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,IAAI,MAAM,CAAC,EAAE,CAAC;IAE5C,MAAM,QAAQ,GAAG,CAAC,MAAS,EAAK,EAAE;QAChC,MAAM,YAAY,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC;QAE/C,MAAM,SAAS,GAAG,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;QAEhD,8DAA8D;QAC9D,OAAO,MAAM,CAAC,YAAY,EAAE,SAAS,CAAC;YACpC,CAAC,CAAC,YAAY;YACd,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC5B,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,GAAM,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC;IAEvD,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,WAAW,EAAE,YAAY,EAAE,CAAU,CAAC;AACnE,CAAC,CAAC"}
1
+ {"version":3,"file":"create-reducer.mjs","sourceRoot":"","sources":["../../src/utils/create-reducer.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAgC,MAAM,eAAe,CAAC;AAE/E,OAAO,EAAE,MAAM,EAA8B,MAAM,mBAAmB,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4EG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,OAAsB,EACtB,YAAe,EACf,OAAkC,EAQlC,EAAE;IACF,MAAM,KAAK,GAAG,MAAM,CAAI,YAAY,CAAC,CAAC;IAEtC,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,IAAI,MAAM,CAAC,EAAE,CAAC;IAE5C,MAAM,cAAc,GAAG,OAAO,EAAE,cAAc,IAAI,KAAK,CAAC;IAExD,MAAM,WAAW,GACf,OAAO,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC;IAEnE,MAAM,QAAQ,GAAG,CAAC,MAAS,EAAK,EAAE;QAChC,MAAM,YAAY,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC;QAE/C,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;QAE9C,MAAM,SAAS,GACb,cAAc,KAAK,KAAK;YACtB,CAAC,CAAC,OAAO;YACT,CAAC,CAAC,gBAAgB,CAAC,YAAY,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;QAE3D,8DAA8D;QAC9D,OAAO,MAAM,CAAC,YAAY,EAAE,SAAS,CAAC;YACpC,CAAC,CAAC,YAAY;YACd,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC5B,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,GAAM,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC;IAEvD,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,WAAW,EAAE,YAAY,EAAE,CAAU,CAAC;AACnE,CAAC,CAAC"}
@@ -7,7 +7,8 @@ import { type StateOptions } from './create-reducer.mjs';
7
7
  * @template S - The type of the state
8
8
  * @param initialState - The initial value of the state
9
9
  * @param options - {@link StateOptions}: `equals` (default `Object.is`),
10
- * to pass on only the updates that change the state
10
+ * to pass on only the updates that change the state, and `keepEqualParts`,
11
+ * to keep the parts of the state an update did not change
11
12
  * @returns A 3-element tuple: `[state, setState, { updateState, resetState, getSnapshot, initialState }]`
12
13
  *
13
14
  * @example
@@ -67,6 +68,57 @@ import { type StateOptions } from './create-reducer.mjs';
67
68
  * ]);
68
69
  * ```
69
70
  *
71
+ * @example
72
+ * ```ts
73
+ * type PullReport = DeepReadonly<{
74
+ * summary: { open: number };
75
+ * pulls: { number: number; title: string }[];
76
+ * }>;
77
+ *
78
+ * const [report, setReport] = createState<PullReport>(
79
+ * { summary: { open: 0 }, pulls: [] },
80
+ * { keepEqualParts: true },
81
+ * );
82
+ *
83
+ * const summary = report.pipe(map((r) => r.summary)).pipe(skipIfNoChange());
84
+ *
85
+ * const summaryHistory: unknown[] = [];
86
+ *
87
+ * summary.subscribe((value) => {
88
+ * summaryHistory.push(value);
89
+ * });
90
+ *
91
+ * // A server answers each poll with the whole report, a new object every
92
+ * // time. What did not change keeps the object already held.
93
+ * setReport({
94
+ * summary: { open: 1 },
95
+ * pulls: [{ number: 1, title: 'one' }],
96
+ * });
97
+ *
98
+ * const held = report.getSnapshot().value;
99
+ *
100
+ * // An answer that changes nothing is not passed on at all.
101
+ * setReport({
102
+ * summary: { open: 1 },
103
+ * pulls: [{ number: 1, title: 'one' }],
104
+ * });
105
+ *
106
+ * assert.strictEqual(report.getSnapshot().value, held);
107
+ *
108
+ * // An edited title: a new report and a new array, while the summary is
109
+ * // the one held, so `summary` is not told of it.
110
+ * setReport({
111
+ * summary: { open: 1 },
112
+ * pulls: [{ number: 1, title: 'one (edited)' }],
113
+ * });
114
+ *
115
+ * assert.notStrictEqual(report.getSnapshot().value, held);
116
+ *
117
+ * assert.strictEqual(report.getSnapshot().value.summary, held.summary);
118
+ *
119
+ * assert.deepStrictEqual(summaryHistory, [{ open: 0 }, { open: 1 }]);
120
+ * ```
121
+ *
70
122
  * @note Do not call `setState`, `updateState` or `resetState` from a subscriber or a projection while another
71
123
  * update is propagating: the write propagates immediately, so subscribers can
72
124
  * see an intermediate state, and it logs a `console.warn` (once per
@@ -1 +1 @@
1
- {"version":3,"file":"create-state.d.mts","sourceRoot":"","sources":["../../src/utils/create-state.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAuBxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,gBACb,CAAC,YACL,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KACjC,SAAS,CACV,KAAK,EAAE,qBAAqB,CAAC,CAAC,CAAC,EAC/B,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EACrB,KAAK,EAAE,QAAQ,CAAC;IACd,WAAW,EAAE,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC7C,UAAU,EAAE,MAAM,CAAC,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,CAAC;IACrB,YAAY,EAAE,CAAC,CAAC;CACjB,CAAC,CA0BH,CAAC"}
1
+ {"version":3,"file":"create-state.d.mts","sourceRoot":"","sources":["../../src/utils/create-state.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAuBxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6HG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,gBACb,CAAC,YACL,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KACjC,SAAS,CACV,KAAK,EAAE,qBAAqB,CAAC,CAAC,CAAC,EAC/B,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EACrB,KAAK,EAAE,QAAQ,CAAC;IACd,WAAW,EAAE,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC7C,UAAU,EAAE,MAAM,CAAC,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,CAAC;IACrB,YAAY,EAAE,CAAC,CAAC;CACjB,CAAC,CA0BH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"create-state.mjs","sourceRoot":"","sources":["../../src/utils/create-state.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAqB,MAAM,sBAAsB,CAAC;AAaxE,MAAM,OAAO,GAAG,CAAK,KAAQ,EAAE,MAAiB,EAAK,EAAE;IACrD,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,KAAK;YACR,OAAO,MAAM,CAAC,SAAS,CAAC;QAE1B,KAAK,QAAQ;YACX,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,YAAe,EACf,OAAkC,EAUlC,EAAE;IACF,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,WAAW,EAAE,CAAC,GAAG,aAAa,CACtD,OAAO,EACP,YAAY,EACZ,OAAO,CACR,CAAC;IAEF,MAAM,WAAW,GAAG,CAAC,QAAwB,EAAK,EAAE,CAClD,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;IAEzC,MAAM,QAAQ,GAAG,CAAC,SAAY,EAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAE3E,MAAM,UAAU,GAAG,GAAM,EAAE,CACzB,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC,CAAC;IAErD,OAAO;QACL,KAAK;QACL,QAAQ;QACR;YACE,WAAW;YACX,UAAU;YACV,WAAW;YACX,YAAY;SACb;KACO,CAAC;AACb,CAAC,CAAC"}
1
+ {"version":3,"file":"create-state.mjs","sourceRoot":"","sources":["../../src/utils/create-state.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAqB,MAAM,sBAAsB,CAAC;AAaxE,MAAM,OAAO,GAAG,CAAK,KAAQ,EAAE,MAAiB,EAAK,EAAE;IACrD,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,KAAK;YACR,OAAO,MAAM,CAAC,SAAS,CAAC;QAE1B,KAAK,QAAQ;YACX,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6HG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,YAAe,EACf,OAAkC,EAUlC,EAAE;IACF,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,WAAW,EAAE,CAAC,GAAG,aAAa,CACtD,OAAO,EACP,YAAY,EACZ,OAAO,CACR,CAAC;IAEF,MAAM,WAAW,GAAG,CAAC,QAAwB,EAAK,EAAE,CAClD,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;IAEzC,MAAM,QAAQ,GAAG,CAAC,SAAY,EAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAE3E,MAAM,UAAU,GAAG,GAAM,EAAE,CACzB,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC,CAAC;IAErD,OAAO;QACL,KAAK;QACL,QAAQ;QACR;YACE,WAAW;YACX,UAAU;YACV,WAAW;YACX,YAAY;SACb;KACO,CAAC;AACb,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synstate",
3
- "version": "4.1.0",
3
+ "version": "5.0.0",
4
4
  "description": "Type-safe State Management Library for TypeScript/JavaScript",
5
5
  "repository": {
6
6
  "type": "git",
@@ -29,20 +29,20 @@
29
29
  "LICENSE"
30
30
  ],
31
31
  "dependencies": {
32
- "ts-data-forge": "^14.9.0",
32
+ "ts-data-forge": "^14.10.0",
33
33
  "ts-std-forge": "^0.5.2",
34
34
  "ts-type-forge": "^9.2.2"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@reduxjs/toolkit": "2.13.0",
38
- "@sumi-lang/cli": "0.0.7",
38
+ "@sumi-lang/cli": "0.1.0",
39
39
  "@types/react": "19.3.0",
40
40
  "@vitest/browser-playwright": "5.0.3",
41
41
  "argparse": "3.0.2",
42
- "eslint-config-typed": "5.19.0",
43
- "eslint-plugin-ts-data-forge": "0.10.0",
44
- "eslint-plugin-ts-fortress": "1.3.5",
45
- "eslint-plugin-ts-type-forge": "0.8.10",
42
+ "eslint-config-typed": "5.19.1",
43
+ "eslint-plugin-ts-data-forge": "0.10.2",
44
+ "eslint-plugin-ts-fortress": "1.3.7",
45
+ "eslint-plugin-ts-type-forge": "0.8.12",
46
46
  "jotai": "3.0.1",
47
47
  "mobx": "7.0.6",
48
48
  "playwright": "1.64.0",
@@ -51,8 +51,8 @@
51
51
  "prettier-plugin-packagejson": "3.0.2",
52
52
  "react": "19.3.0",
53
53
  "rxjs": "7.8.2",
54
- "synstate-react-hooks": "4.1.0",
55
- "synstate-react-hooks-compat": "4.1.0",
54
+ "synstate-react-hooks": "5.0.0",
55
+ "synstate-react-hooks-compat": "5.0.0",
56
56
  "ts-repo-utils": "10.7.1",
57
57
  "valtio": "2.3.2",
58
58
  "zustand": "5.0.15"
@@ -2,7 +2,7 @@
2
2
  // @sumi-expect-error-file banned-syntax/no-this
3
3
  // @sumi-expect-error-file functions/prefer-arrow-function
4
4
  // @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
5
- import { Arr, hasKey, isRecord, type Optional, type Some } from 'ts-data-forge';
5
+ import { hasKey, isRecord, type Optional, type Some } from 'ts-data-forge';
6
6
  import { panic } from 'ts-std-forge';
7
7
  import type { MutableSet } from 'ts-type-forge';
8
8
  import type {
@@ -21,11 +21,7 @@ import type {
21
21
  import { maxDepth } from '../utils/index.mjs';
22
22
  import { ManagerObservableClass } from './manager-observable-class.mjs';
23
23
  import { ObservableBaseClass } from './observable-base-class.mjs';
24
- import {
25
- toNode,
26
- type ChildNode,
27
- type ManagerNode,
28
- } from './observable-node.mjs';
24
+ import { toNode, type ChildNode } from './observable-node.mjs';
29
25
  import { runAfterPropagation } from './propagation-state.mjs';
30
26
 
31
27
  type ChildObservableConfig<A, P extends NonEmptyUnknownList> = Readonly<{
@@ -57,7 +53,6 @@ export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
57
53
  {
58
54
  #mut_isAttached: boolean;
59
55
  readonly parents: Wrap<P>;
60
- readonly managers: readonly ManagerNode[];
61
56
 
62
57
  constructor({
63
58
  parents,
@@ -72,17 +67,14 @@ export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
72
67
 
73
68
  this.parents = parents;
74
69
 
75
- this.managers = managersOf(parents);
76
-
77
70
  this.#mut_isAttached = false;
78
71
  }
79
72
 
80
73
  /**
81
- * Joins the current scope and registers this observable to its parents
82
- * and the managers it is reached from. Completed parents never update
83
- * again, so a child created under them is complete from the start
84
- * (#2248), as is one its constructor completed; registering it would only
85
- * leave a reference in each parent.
74
+ * Joins the current scope and registers this observable to its parents.
75
+ * Completed parents never update again, so a child created under them is
76
+ * complete from the start (#2248), as is one its constructor completed;
77
+ * registering it would only leave a reference in each parent.
86
78
  */
87
79
  protected attach(): void {
88
80
  this.adoptIntoCurrentScope();
@@ -90,7 +82,7 @@ export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
90
82
  if (this.isCompleted || this.parents.every((p) => p.isCompleted)) {
91
83
  this.complete();
92
84
  } else {
93
- registerChild(this, this.managers);
85
+ registerChild(this);
94
86
 
95
87
  this.#mut_isAttached = true;
96
88
  }
@@ -103,7 +95,7 @@ export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
103
95
 
104
96
  this.#mut_isAttached = false;
105
97
 
106
- unregisterChild(this, this.managers);
98
+ unregisterChild(this);
107
99
  }
108
100
 
109
101
  override tryComplete(): void {
@@ -170,7 +162,7 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
170
162
  if (this.isCompleted || this.parents.every((p) => p.isCompleted)) {
171
163
  this.complete();
172
164
  } else {
173
- registerChild(this, managersOf(this.parents));
165
+ registerChild(this);
174
166
 
175
167
  this.#mut_isAttached = true;
176
168
  }
@@ -183,7 +175,7 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
183
175
 
184
176
  this.#mut_isAttached = false;
185
177
 
186
- unregisterChild(this, managersOf(this.parents));
178
+ unregisterChild(this);
187
179
  }
188
180
 
189
181
  override tryComplete(): void {
@@ -247,11 +239,8 @@ const shouldComplete = (
247
239
  !child.isCompleted &&
248
240
  (child.parents.every((p) => p.isCompleted) || child.canEmitNoMore());
249
241
 
250
- /** Registers a child to its parents and to `managers`. */
251
- const registerChild = (
252
- child: ChildNode,
253
- managers: readonly ManagerNode[],
254
- ): void => {
242
+ /** Registers a child to its parents. */
243
+ const registerChild = (child: ChildNode): void => {
255
244
  if (checksCircularDependency) {
256
245
  detectCircularDependency(child);
257
246
  }
@@ -259,42 +248,19 @@ const registerChild = (
259
248
  for (const p of child.parents) {
260
249
  toNode(p).addChild(child);
261
250
  }
262
-
263
- for (const m of managers) {
264
- m.addDescendant(child);
265
- }
266
251
  };
267
252
 
268
253
  /**
269
254
  * Undoes {@link registerChild} for a completed child: its parents forget it,
270
- * and every manager it registered to stops propagating updates to it.
271
- * Without this a completed node stayed reachable for as long as its root
272
- * lived, and ran its `tryUpdate` on every update (#2134).
255
+ * so that no update puts it in its queue again. Without this a completed
256
+ * node stayed reachable for as long as its root lived (#2134).
273
257
  */
274
- const unregisterChild = (
275
- child: ChildNode,
276
- managers: readonly ManagerNode[],
277
- ): void => {
258
+ const unregisterChild = (child: ChildNode): void => {
278
259
  for (const p of child.parents) {
279
260
  toNode(p).deleteChild(child);
280
261
  }
281
-
282
- for (const m of managers) {
283
- m.deleteDescendant(child);
284
- }
285
262
  };
286
263
 
287
- /**
288
- * The managers the descendants of `parents` register to, shared with the
289
- * parent when there is one.
290
- */
291
- const managersOf = (
292
- parents: readonly Observable<unknown>[],
293
- ): readonly ManagerNode[] =>
294
- Arr.isFixedLengthArray(1, parents)
295
- ? toNode(parents[0]).managers
296
- : Array.from(new Set(parents.flatMap((p) => toNode(p).managers)));
297
-
298
264
  const isDevelopmentProcess = (): boolean => {
299
265
  const proc: unknown = Reflect.get(globalThis, 'process');
300
266
 
@@ -61,7 +61,10 @@ export type AsyncChildObservableTools<A> = Readonly<{
61
61
  * hooks the factory calls, which read the state `init` creates.
62
62
  */
63
63
  export type SyncChildObservableHooks = Readonly<{
64
- /** Called with the token of every update that reaches this observable. */
64
+ /**
65
+ * Called with the token of every update in which a parent of this
66
+ * observable emitted.
67
+ */
65
68
  tryUpdate: (updateToken: UpdateToken) => void;
66
69
 
67
70
  /**
@@ -124,10 +127,10 @@ export type InitializedChildObservableConfig<
124
127
  * other synchronous operators are built with it.
125
128
  *
126
129
  * `init` runs during construction and receives the tools. It returns
127
- * `tryUpdate`, which is called with the token of every update that reaches
128
- * this observable — a parent that did not emit in that update has a different
129
- * `updateToken`; to emit, call `setNext` with the token received — or an
130
- * object holding `tryUpdate` and the other hooks
130
+ * `tryUpdate`, which is called with the token of every update in which some
131
+ * parent of this observable emitted — a parent that did not emit in that
132
+ * update has a different `updateToken`; to emit, call `setNext` with the
133
+ * token received — or an object holding `tryUpdate` and the other hooks
131
134
  * ({@link SyncChildObservableHooks}). State the hooks share can live inside
132
135
  * `init`, next to the teardown it registers with the `onComplete` tool.
133
136
  * `init` may complete the observable; it is then never registered to its
@@ -1,137 +1,32 @@
1
1
  // @sumi-expect-error-file classes/no-class
2
2
  // @sumi-expect-error-file banned-syntax/no-this
3
3
  // @sumi-expect-error-file functions/prefer-arrow-function
4
- // @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
5
- import type { Optional } from 'ts-data-forge';
6
- import type { MutableSet } from 'ts-type-forge';
7
- import type { ManagerObservableKind, UpdateToken } from '../types/index.mjs';
8
- import { issueUpdateToken } from '../utils/index.mjs';
4
+ import type { ManagerObservableKind } from '../types/index.mjs';
9
5
  import { ObservableBaseClass } from './observable-base-class.mjs';
10
- import type { ChildNode, ManagerNode } from './observable-node.mjs';
11
- import { beginPropagation, endPropagation } from './propagation-state.mjs';
6
+ import {
7
+ beginPropagation,
8
+ closeUpdate,
9
+ drainUpdate,
10
+ endPropagation,
11
+ openUpdate,
12
+ } from './propagation-state.mjs';
12
13
 
13
14
  /**
14
15
  * The base of the observables that start updates of their own: a root, and
15
16
  * an async child (`debounce`, `switchMap`), which emits later than its
16
- * parents do. Every descendant registers to the managers it is reached from,
17
- * and an update reaches them in ascending depth, so that each sees its
18
- * parents' values of that update (no glitch).
17
+ * parents do. Its update visits, shallowest first, the nodes its value
18
+ * reaches: each node that emits puts its children in the update's queue
19
+ * (`propagation-state.mts`), so that each sees its parents' values of that
20
+ * update (no glitch) and a node no value reached is not visited.
19
21
  */
20
22
  export class ManagerObservableClass<
21
23
  A,
22
24
  Kind extends ManagerObservableKind,
23
25
  Depth extends number,
24
26
  > extends ObservableBaseClass<A, Kind, Depth> {
25
- /**
26
- * The descendants, by depth: the set at index `i` holds those at
27
- * `depth + 1 + i`, in the order they registered. Depths below a live node
28
- * never skip one (every live node has a live parent one level up), so the
29
- * array has few holes; a hole is `undefined`, never a gap left for V8 to
30
- * treat as a holey array.
31
- *
32
- * An update walks this array and its sets in place. Adding or removing a
33
- * node is O(1) and there is no flat order to rebuild; only the first node
34
- * of a depth replaces the array with a longer copy. A node deleted during
35
- * the walk is skipped by the set's iterator; a node registered during it is
36
- * also put in `#mut_joinedDuringUpdate` and skipped, so an update visits the
37
- * nodes that existed when it began.
38
- */
39
- #mut_propagationOrder: readonly (MutableSet<ChildNode> | undefined)[];
40
-
41
- /** Whether this node is propagating an update of its own. */
42
- #mut_isPropagating: boolean;
43
-
44
- /**
45
- * The descendants that registered while this node propagated the update
46
- * under way, which that update does not visit; made when the first one
47
- * arrives. Kept on the manager rather than in a module, so that it also
48
- * holds for a node another copy of synstate registered.
49
- */
50
- #mut_joinedDuringUpdate: MutableSet<ChildNode> | undefined;
51
-
52
- /** The managers this node's descendants register to: itself. */
53
- readonly managers: readonly ManagerNode[];
54
-
55
- constructor(
56
- config: Readonly<{
57
- kind: Kind;
58
- depth: Depth;
59
- initialValue: Optional<A>;
60
- }>,
61
- ) {
62
- super(config);
63
-
64
- this.managers = [this];
65
-
66
- this.#mut_propagationOrder = [];
67
-
68
- this.#mut_isPropagating = false;
69
-
70
- this.#mut_joinedDuringUpdate = undefined;
71
- }
72
-
73
- /**
74
- * Runs `tryUpdate` on the descendants, shallowest first, skipping those
75
- * registered after this update began.
76
- */
77
- #propagate(updateToken: UpdateToken): void {
78
- for (const bucket of this.#mut_propagationOrder) {
79
- if (bucket !== undefined) {
80
- for (const c of bucket) {
81
- // Read for each node, since the set is made when the first joins.
82
- if (this.#mut_joinedDuringUpdate?.has(c) !== true) {
83
- c.tryUpdate(updateToken);
84
- }
85
- }
86
- }
87
- }
88
- }
89
-
90
- addDescendant(child: ChildNode): void {
91
- const descendants = this.#mut_propagationOrder;
92
-
93
- const index = child.depth - this.depth - 1;
94
-
95
- const mut_bucket = descendants[index];
96
-
97
- if (mut_bucket === undefined) {
98
- // The first node of its depth: rare, so the array is copied rather than
99
- // written, which also leaves a walk already under way on the old one.
100
- this.#mut_propagationOrder = Array.from(
101
- { length: Math.max(descendants.length, index + 1) },
102
- (_, i) => (i === index ? new Set([child]) : descendants[i]),
103
- );
104
- } else {
105
- mut_bucket.add(child);
106
- }
107
-
108
- if (!this.#mut_isPropagating) {
109
- return;
110
- }
111
-
112
- this.#mut_joinedDuringUpdate ??= new Set();
113
-
114
- this.#mut_joinedDuringUpdate.add(child);
115
- }
116
-
117
- deleteDescendant(child: ChildNode): void {
118
- // An emptied set stays, to be reused by the next node of its depth.
119
- this.#mut_propagationOrder[child.depth - this.depth - 1]?.delete(child);
120
- }
121
-
122
27
  /** Emits `nextValue` and propagates it to the descendants. */
123
28
  protected startUpdate(nextValue: A): void {
124
- const updateToken = issueUpdateToken();
125
-
126
- // A write to this same manager from inside its own update starts an inner
127
- // one; what joins during it joined during the outer one too.
128
- const wasPropagating = this.#mut_isPropagating;
129
-
130
- const joinedBefore = this.#mut_joinedDuringUpdate;
131
-
132
- this.#mut_isPropagating = true;
133
-
134
- this.#mut_joinedDuringUpdate = undefined;
29
+ const updateToken = openUpdate(this.depth);
135
30
 
136
31
  beginPropagation(updateToken);
137
32
 
@@ -142,23 +37,9 @@ export class ManagerObservableClass<
142
37
  try {
143
38
  this.setNext(nextValue, updateToken);
144
39
 
145
- this.#propagate(updateToken);
40
+ drainUpdate(updateToken);
146
41
  } finally {
147
- // Widened: TypeScript would take the write above for the value.
148
- const joinedNow = this.#mut_joinedDuringUpdate as
149
- MutableSet<ChildNode> | undefined;
150
-
151
- this.#mut_isPropagating = wasPropagating;
152
-
153
- this.#mut_joinedDuringUpdate = joinedBefore;
154
-
155
- if (wasPropagating && joinedNow !== undefined) {
156
- this.#mut_joinedDuringUpdate ??= new Set();
157
-
158
- for (const c of joinedNow) {
159
- this.#mut_joinedDuringUpdate.add(c);
160
- }
161
- }
42
+ closeUpdate(updateToken);
162
43
 
163
44
  endPropagation();
164
45
  }