synstate 6.1.1 → 7.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 (189) hide show
  1. package/README.md +3 -3
  2. package/dist/core/authoring.d.mts +3 -3
  3. package/dist/core/class/child-observable-class.d.mts +40 -27
  4. package/dist/core/class/child-observable-class.d.mts.map +1 -1
  5. package/dist/core/class/child-observable-class.mjs +78 -46
  6. package/dist/core/class/child-observable-class.mjs.map +1 -1
  7. package/dist/core/class/create-child-observable.d.mts +28 -30
  8. package/dist/core/class/create-child-observable.d.mts.map +1 -1
  9. package/dist/core/class/create-child-observable.mjs +6 -8
  10. package/dist/core/class/create-child-observable.mjs.map +1 -1
  11. package/dist/core/class/create-root-observable.d.mts +12 -12
  12. package/dist/core/class/create-root-observable.d.mts.map +1 -1
  13. package/dist/core/class/create-root-observable.mjs +1 -5
  14. package/dist/core/class/create-root-observable.mjs.map +1 -1
  15. package/dist/core/class/manager-observable-class.d.mts +6 -6
  16. package/dist/core/class/manager-observable-class.mjs.map +1 -1
  17. package/dist/core/class/observable-base-class.d.mts +29 -31
  18. package/dist/core/class/observable-base-class.d.mts.map +1 -1
  19. package/dist/core/class/observable-base-class.mjs +0 -2
  20. package/dist/core/class/observable-base-class.mjs.map +1 -1
  21. package/dist/core/class/observable-node.d.mts +3 -4
  22. package/dist/core/class/observable-node.d.mts.map +1 -1
  23. package/dist/core/class/observable-node.mjs +0 -1
  24. package/dist/core/class/observable-node.mjs.map +1 -1
  25. package/dist/core/class/propagation-state.d.mts +6 -8
  26. package/dist/core/class/propagation-state.d.mts.map +1 -1
  27. package/dist/core/class/propagation-state.mjs +1 -3
  28. package/dist/core/class/propagation-state.mjs.map +1 -1
  29. package/dist/core/class/root-observable-class.d.mts +4 -4
  30. package/dist/core/combine/derive.d.mts +7 -8
  31. package/dist/core/combine/derive.d.mts.map +1 -1
  32. package/dist/core/combine/derive.mjs +2 -2
  33. package/dist/core/combine/derive.mjs.map +1 -1
  34. package/dist/core/combine/merge.mjs.map +1 -1
  35. package/dist/core/combine/zip.d.mts +3 -3
  36. package/dist/core/combine/zip.d.mts.map +1 -1
  37. package/dist/core/combine/zip.mjs +5 -2
  38. package/dist/core/combine/zip.mjs.map +1 -1
  39. package/dist/core/create/counter.d.mts +3 -3
  40. package/dist/core/create/counter.mjs +2 -2
  41. package/dist/core/create/counter.mjs.map +1 -1
  42. package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
  43. package/dist/core/create/from-abortable-promise.mjs +10 -3
  44. package/dist/core/create/from-abortable-promise.mjs.map +1 -1
  45. package/dist/core/create/from-promise.d.mts.map +1 -1
  46. package/dist/core/create/from-promise.mjs +10 -3
  47. package/dist/core/create/from-promise.mjs.map +1 -1
  48. package/dist/core/create/from-subscribable.mjs.map +1 -1
  49. package/dist/core/create/source.d.mts.map +1 -1
  50. package/dist/core/create/source.mjs +2 -1
  51. package/dist/core/create/source.mjs.map +1 -1
  52. package/dist/core/create/timer.mjs.map +1 -1
  53. package/dist/core/operators/audit.mjs.map +1 -1
  54. package/dist/core/operators/debounce.mjs.map +1 -1
  55. package/dist/core/operators/filter.d.mts +1 -1
  56. package/dist/core/operators/filter.d.mts.map +1 -1
  57. package/dist/core/operators/filter.mjs +2 -3
  58. package/dist/core/operators/filter.mjs.map +1 -1
  59. package/dist/core/operators/keep-equal-parts.d.mts +14 -15
  60. package/dist/core/operators/keep-equal-parts.d.mts.map +1 -1
  61. package/dist/core/operators/keep-equal-parts.mjs +0 -1
  62. package/dist/core/operators/keep-equal-parts.mjs.map +1 -1
  63. package/dist/core/operators/map.mjs.map +1 -1
  64. package/dist/core/operators/merge-map.d.mts +7 -3
  65. package/dist/core/operators/merge-map.d.mts.map +1 -1
  66. package/dist/core/operators/merge-map.mjs +47 -16
  67. package/dist/core/operators/merge-map.mjs.map +1 -1
  68. package/dist/core/operators/pairwise.mjs.map +1 -1
  69. package/dist/core/operators/scan.mjs.map +1 -1
  70. package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
  71. package/dist/core/operators/skip-until.mjs.map +1 -1
  72. package/dist/core/operators/skip-while.d.mts +1 -1
  73. package/dist/core/operators/skip-while.d.mts.map +1 -1
  74. package/dist/core/operators/skip-while.mjs +9 -6
  75. package/dist/core/operators/skip-while.mjs.map +1 -1
  76. package/dist/core/operators/switch-map.d.mts +4 -0
  77. package/dist/core/operators/switch-map.d.mts.map +1 -1
  78. package/dist/core/operators/switch-map.mjs +16 -6
  79. package/dist/core/operators/switch-map.mjs.map +1 -1
  80. package/dist/core/operators/take-until.mjs.map +1 -1
  81. package/dist/core/operators/take-while.d.mts +2 -2
  82. package/dist/core/operators/take-while.d.mts.map +1 -1
  83. package/dist/core/operators/take-while.mjs +10 -8
  84. package/dist/core/operators/take-while.mjs.map +1 -1
  85. package/dist/core/operators/take.mjs.map +1 -1
  86. package/dist/core/operators/throttle.mjs +3 -2
  87. package/dist/core/operators/throttle.mjs.map +1 -1
  88. package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
  89. package/dist/core/operators/with-buffered-from.mjs +13 -3
  90. package/dist/core/operators/with-buffered-from.mjs.map +1 -1
  91. package/dist/core/operators/with-current-value-from.mjs.map +1 -1
  92. package/dist/core/operators/with-initial-value.mjs.map +1 -1
  93. package/dist/core/predefined/operators/attach-index.d.mts +1 -1
  94. package/dist/core/predefined/operators/attach-index.d.mts.map +1 -1
  95. package/dist/core/predefined/operators/attach-index.mjs +2 -3
  96. package/dist/core/predefined/operators/attach-index.mjs.map +1 -1
  97. package/dist/core/predefined/operators/map-to.d.mts +3 -4
  98. package/dist/core/predefined/operators/map-to.d.mts.map +1 -1
  99. package/dist/core/predefined/operators/map-to.mjs +0 -1
  100. package/dist/core/predefined/operators/map-to.mjs.map +1 -1
  101. package/dist/core/predefined/operators/skip.d.mts.map +1 -1
  102. package/dist/core/predefined/operators/skip.mjs +2 -2
  103. package/dist/core/predefined/operators/skip.mjs.map +1 -1
  104. package/dist/core/scope/scope.d.mts +4 -4
  105. package/dist/core/types/observable-family.d.mts.map +1 -1
  106. package/dist/core/types/observable-family.mjs.map +1 -1
  107. package/dist/core/types/observable.d.mts +5 -5
  108. package/dist/core/types/observable.mjs.map +1 -1
  109. package/dist/core/utils/current-scope.d.mts +6 -6
  110. package/dist/core/utils/id-maker.d.mts +2 -2
  111. package/dist/core/utils/index.d.mts +1 -0
  112. package/dist/core/utils/index.d.mts.map +1 -1
  113. package/dist/core/utils/index.mjs +1 -0
  114. package/dist/core/utils/index.mjs.map +1 -1
  115. package/dist/core/utils/observable-brand.d.mts +5 -5
  116. package/dist/core/utils/rethrow-out-of-promise.d.mts +10 -0
  117. package/dist/core/utils/rethrow-out-of-promise.d.mts.map +1 -0
  118. package/dist/core/utils/rethrow-out-of-promise.mjs +17 -0
  119. package/dist/core/utils/rethrow-out-of-promise.mjs.map +1 -0
  120. package/dist/utils/create-boolean-state.d.mts +2 -2
  121. package/dist/utils/create-reducer.d.mts +18 -18
  122. package/dist/utils/create-state.d.mts +2 -2
  123. package/dist/utils/create-state.mjs.map +1 -1
  124. package/dist/utils/state-source.d.mts +3 -3
  125. package/package.json +12 -11
  126. package/src/core/authoring.mts +3 -3
  127. package/src/core/class/child-observable-class.mts +131 -90
  128. package/src/core/class/circular-dependency-comparison.test.mts +2 -2
  129. package/src/core/class/create-child-observable.mts +35 -34
  130. package/src/core/class/create-root-observable.mts +13 -12
  131. package/src/core/class/manager-observable-class.mts +12 -12
  132. package/src/core/class/observable-base-class.mts +33 -35
  133. package/src/core/class/observable-node.mts +3 -4
  134. package/src/core/class/propagation-state.mts +27 -29
  135. package/src/core/class/remove-completed.test.mts +2 -2
  136. package/src/core/class/root-observable-class.mts +9 -9
  137. package/src/core/class/unobserved-write.test.mts +4 -4
  138. package/src/core/class/write-during-propagation.test.mts +75 -4
  139. package/src/core/combine/derive.mts +12 -12
  140. package/src/core/combine/merge.mts +2 -2
  141. package/src/core/combine/zip.mts +12 -9
  142. package/src/core/create/counter.mts +7 -7
  143. package/src/core/create/from-abortable-promise.mts +35 -25
  144. package/src/core/create/from-promise.mts +30 -20
  145. package/src/core/create/from-promise.test.mts +97 -0
  146. package/src/core/create/from-subscribable.mts +2 -2
  147. package/src/core/create/source.mts +7 -6
  148. package/src/core/create/timer.mts +2 -2
  149. package/src/core/operators/audit.mts +2 -2
  150. package/src/core/operators/debounce.mts +2 -2
  151. package/src/core/operators/filter.mts +6 -7
  152. package/src/core/operators/flat-map-initial-value.test.mts +269 -0
  153. package/src/core/operators/flat-map-sync-inner.test.mts +4 -2
  154. package/src/core/operators/inner-ownership.test.mts +145 -2
  155. package/src/core/operators/keep-equal-parts.mts +16 -17
  156. package/src/core/operators/map.mts +2 -2
  157. package/src/core/operators/merge-map.mts +59 -29
  158. package/src/core/operators/pairwise.mts +4 -4
  159. package/src/core/operators/scan.mts +2 -2
  160. package/src/core/operators/skip-if-no-change.mts +4 -4
  161. package/src/core/operators/skip-until.mts +4 -4
  162. package/src/core/operators/skip-while.mts +14 -10
  163. package/src/core/operators/skip-while.test.mts +59 -0
  164. package/src/core/operators/switch-map.mts +26 -15
  165. package/src/core/operators/take-until.mts +2 -2
  166. package/src/core/operators/take-while.mts +17 -19
  167. package/src/core/operators/take-while.test.mts +51 -0
  168. package/src/core/operators/take.mts +2 -2
  169. package/src/core/operators/throttle.mts +6 -5
  170. package/src/core/operators/with-buffered-from.mts +18 -5
  171. package/src/core/operators/with-buffered-from.test.mts +27 -0
  172. package/src/core/operators/with-current-value-from.mts +2 -2
  173. package/src/core/operators/with-initial-value.mts +2 -2
  174. package/src/core/predefined/operators/attach-index.mts +4 -5
  175. package/src/core/predefined/operators/map-to.mts +3 -4
  176. package/src/core/predefined/operators/skip.mts +2 -2
  177. package/src/core/scope/scope.mts +6 -6
  178. package/src/core/types/observable-family.mts +4 -5
  179. package/src/core/types/observable.mts +7 -7
  180. package/src/core/utils/current-scope.mts +8 -8
  181. package/src/core/utils/id-maker.mts +2 -2
  182. package/src/core/utils/index.mts +1 -0
  183. package/src/core/utils/observable-brand.mts +5 -5
  184. package/src/core/utils/rethrow-out-of-promise.mts +16 -0
  185. package/src/utils/create-boolean-state.mts +2 -2
  186. package/src/utils/create-reducer.mts +18 -18
  187. package/src/utils/create-state.mts +4 -4
  188. package/src/utils/create-state.test.mts +2 -2
  189. package/src/utils/state-source.mts +3 -3
@@ -87,18 +87,18 @@ export declare const createReducer: <S, A>(reducer: Reducer<S, A>, initialState:
87
87
  */
88
88
  export type StateOptions<S> = Readonly<{
89
89
  /**
90
- * Whether the next state is the same as the current one. When it says so,
91
- * the update is not passed on — no subscriber, and nothing derived from
92
- * the state, is told of it — and the current state is kept, along with its
93
- * identity: `setState` and the like return the current state rather than
94
- * the next.
90
+ * Whether the next state is the same as the current one. When it says so, the
91
+ * update is not passed on — no subscriber, and nothing derived from the
92
+ * state, is told of it — and the current state is kept, along with its
93
+ * identity: `setState` and the like return the current state rather than the
94
+ * next.
95
95
  *
96
96
  * Decide here, where the state is held, rather than downstream with
97
- * `skipIfNoChange`: an update passed on here reaches whatever reads the
98
- * state directly before any operator can thin it out.
97
+ * `skipIfNoChange`: an update passed on here reaches whatever reads the state
98
+ * directly before any operator can thin it out.
99
99
  *
100
- * Defaults to `Object.is`, so writing the value already held passes
101
- * nothing on. Pass `() => false` to pass on every update, equal or not.
100
+ * Defaults to `Object.is`, so writing the value already held passes nothing
101
+ * on. Pass `() => false` to pass on every update, equal or not.
102
102
  *
103
103
  * @param current - The state held now
104
104
  * @param next - The state the update would set
@@ -107,8 +107,8 @@ export type StateOptions<S> = Readonly<{
107
107
  /**
108
108
  * Whether an update keeps the parts of the state it did not change. With
109
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
110
+ * written, and every part of it deeply equal to the same part of the current
111
+ * state is replaced by that part (`replaceEqualDeep` of
112
112
  * ts-data-forge): a reader of an unchanged part keeps the object it has, so
113
113
  * `map`, comparing with `Object.is`, stops there. When the whole next state is
114
114
  * deeply equal, the current state is kept and nothing is passed on, so
@@ -121,14 +121,14 @@ export type StateOptions<S> = Readonly<{
121
121
  * arrays are walked into; a `Map`, a `Set`, a `Date` or a class instance is
122
122
  * compared and kept whole.
123
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.
124
+ * Prefer this over `keepEqualParts()` downstream: keeping the parts where the
125
+ * state is written means no update with nothing in it is passed on at all,
126
+ * and nothing downstream has anything to undo.
127
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.
128
+ * Defaults to `false`: the next state is written as given. With it on, each
129
+ * update compares deeply every part of the next state that is not the very
130
+ * object of the current one; a state whose updates already keep what they do
131
+ * not change, as `{ ...prev, x }` does, gains nothing from it.
132
132
  */
133
133
  keepEqualParts?: boolean | ReplaceEqualDeepOptions;
134
134
  }>;
@@ -105,8 +105,8 @@ import type { StateOptions } from './create-reducer.mjs';
105
105
  *
106
106
  * assert.strictEqual(report.getSnapshot().value, held);
107
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.
108
+ * // An edited title: a new report and a new array, while the summary is the
109
+ * // one held, so `summary` is not told of it.
110
110
  * setReport({
111
111
  * summary: { open: 1 },
112
112
  * pulls: [{ number: 1, title: 'one (edited)' }],
@@ -1 +1 @@
1
- {"version":3,"file":"create-state.mjs","sourceRoot":"","sources":["../../src/utils/create-state.mts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6HG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,YAAe,EACf,OAAkC,EAUlC,EAAE;IACF,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,iBAAiB,CACtD,YAAY,EACZ,OAAO,CACR,CAAC;IAEF,qEAAqE;IACrE,uCAAuC;IACvC,MAAM,WAAW,GAAG,CAAC,QAAwB,EAAK,EAAE;QAClD,MAAM,YAAY,GAAG,WAAW,EAAE,CAAC;QAEnC,OAAO,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;IACtD,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,CAAC,SAAY,EAAK,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,SAAS,CAAC,CAAC;IAEvE,MAAM,UAAU,GAAG,GAAM,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,YAAY,CAAC,CAAC;IAEhE,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":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6HG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,YAAe,EACf,OAAkC,EAUlC,EAAE;IACF,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,iBAAiB,CACtD,YAAY,EACZ,OAAO,CACR,CAAC;IAEF,2EAA2E;IAC3E,iCAAiC;IACjC,MAAM,WAAW,GAAG,CAAC,QAAwB,EAAK,EAAE;QAClD,MAAM,YAAY,GAAG,WAAW,EAAE,CAAC;QAEnC,OAAO,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;IACtD,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,CAAC,SAAY,EAAK,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,SAAS,CAAC,CAAC;IAEvE,MAAM,UAAU,GAAG,GAAM,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,YAAY,CAAC,CAAC;IAEhE,OAAO;QACL,KAAK;QACL,QAAQ;QACR;YACE,WAAW;YACX,UAAU;YACV,WAAW;YACX,YAAY;SACb;KACO,CAAC;AACb,CAAC,CAAC"}
@@ -1,9 +1,9 @@
1
1
  import { type InitializedSourceObservable } from '../core/index.mjs';
2
2
  import type { StateOptions } from './create-reducer.mjs';
3
3
  /**
4
- * What `createState` and `createReducer` are both built on: the source
5
- * holding the state, its current value, and `commit`, the one way either
6
- * writes it. `commit` writes `reduced` unless `equals` finds it the same as
4
+ * What `createState` and `createReducer` are both built on: the source holding
5
+ * the state, its current value, and `commit`, the one way either writes it.
6
+ * `commit` writes `reduced` unless `equals` finds it the same as
7
7
  * `currentState`, after keeping the parts of `currentState` it did not change
8
8
  * when `keepEqualParts` asks for that, and returns the state written, or
9
9
  * `currentState` when nothing was.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synstate",
3
- "version": "6.1.1",
3
+ "version": "7.0.0",
4
4
  "description": "Type-safe State Management Library for TypeScript/JavaScript",
5
5
  "repository": {
6
6
  "type": "git",
@@ -29,21 +29,22 @@
29
29
  "LICENSE"
30
30
  ],
31
31
  "dependencies": {
32
- "ts-data-forge": "^14.10.0",
33
- "ts-std-forge": "^0.5.2",
32
+ "ts-data-forge": "^14.11.0",
33
+ "ts-std-forge": "^0.5.3",
34
34
  "ts-type-forge": "^9.2.2"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@preact/signals-core": "1.14.4",
38
38
  "@reduxjs/toolkit": "2.13.0",
39
- "@sumi-lang/cli": "0.1.3",
39
+ "@sumi-lang/cli": "0.1.6",
40
40
  "@types/react": "19.3.0",
41
41
  "@vitest/browser-playwright": "5.0.5",
42
42
  "argparse": "3.0.3",
43
- "eslint-config-typed": "7.1.0",
44
- "eslint-plugin-ts-data-forge": "0.10.5",
45
- "eslint-plugin-ts-fortress": "1.3.10",
46
- "eslint-plugin-ts-type-forge": "0.8.15",
43
+ "eslint": "10.13.0",
44
+ "eslint-config-typed": "7.4.0",
45
+ "eslint-plugin-ts-data-forge": "0.10.9",
46
+ "eslint-plugin-ts-fortress": "1.3.13",
47
+ "eslint-plugin-ts-type-forge": "0.8.18",
47
48
  "jotai": "3.0.1",
48
49
  "mobx": "7.1.0",
49
50
  "playwright": "1.64.1",
@@ -53,9 +54,9 @@
53
54
  "react": "19.3.1",
54
55
  "rxjs": "7.8.2",
55
56
  "solid-js": "1.9.17",
56
- "synstate-react-hooks": "6.1.1",
57
- "synstate-react-hooks-compat": "6.1.1",
58
- "ts-repo-utils": "10.7.1",
57
+ "synstate-react-hooks": "7.0.0",
58
+ "synstate-react-hooks-compat": "7.0.0",
59
+ "ts-repo-utils": "10.7.2",
59
60
  "valtio": "2.3.2",
60
61
  "zustand": "5.0.15"
61
62
  },
@@ -1,7 +1,7 @@
1
1
  /**
2
- * What custom creators and operators are written with: the factories every
3
- * built-in observable is made by (#2117). The rest of `core/base` stays
4
- * internal.
2
+ * What custom creators and operators are written with: factories that wrap the
3
+ * base classes the built-in observables extend (#2117). The rest of `core/base`
4
+ * stays internal.
5
5
  */
6
6
  export {
7
7
  createAsyncChildObservable,
@@ -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 { hasKey, isRecord, type Optional, type Some } from 'ts-data-forge';
5
+ import { hasKey, isRecord, 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 {
@@ -20,7 +20,7 @@ import type {
20
20
  WithInitialValueOperator,
21
21
  Wrap,
22
22
  } from '../types/index.mjs';
23
- import { maxDepth } from '../utils/index.mjs';
23
+ import { issueUnqueuedToken, maxDepth } from '../utils/index.mjs';
24
24
  import { ManagerObservableClass } from './manager-observable-class.mjs';
25
25
  import { ObservableBaseClass } from './observable-base-class.mjs';
26
26
  import { toNode, type ChildNode } from './observable-node.mjs';
@@ -30,11 +30,11 @@ type ChildObservableConfig<A, P extends NonEmptyUnknownList> = Readonly<{
30
30
  parents: Wrap<P>;
31
31
 
32
32
  /**
33
- * Defaults to one more than the deepest parent. An update reaches
34
- * observables in ascending depth, so an observable `tryUpdate` reads with
35
- * `getSnapshot` without being its child (`withCurrentValueFrom`'s second
36
- * source) has to be shallower than this one too, or this one reads its value
37
- * from before the update: pass `1 + Math.max(...)` over both then.
33
+ * Defaults to one more than the deepest parent. An update reaches observables
34
+ * in ascending depth, so an observable `tryUpdate` reads with `getSnapshot`
35
+ * without being its child (`withCurrentValueFrom`'s second source) has to be
36
+ * shallower than this one too, or this one reads its value from before the
37
+ * update: pass `1 + Math.max(...)` over both then.
38
38
  */
39
39
  depth?: number;
40
40
 
@@ -45,9 +45,9 @@ type ChildObservableConfig<A, P extends NonEmptyUnknownList> = Readonly<{
45
45
  * An observable derived from `parents` that emits, if at all, in the same
46
46
  * update as they do (`map`, `filter`, `combine`, …).
47
47
  *
48
- * A subclass overrides `tryUpdate`, and, when it holds something to
49
- * release, `teardown`; its constructor ends with `this.attach()`, once
50
- * everything `tryUpdate` and `teardown` read is in place.
48
+ * A subclass overrides `tryUpdate`, and, when it holds something to release,
49
+ * `teardown`; its constructor ends with `this.attach()`, once everything
50
+ * `tryUpdate` and `teardown` read is in place.
51
51
  */
52
52
  export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
53
53
  extends ObservableBaseClass<A, 'sync child', number>
@@ -107,10 +107,9 @@ export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
107
107
  }
108
108
 
109
109
  /**
110
- * Whether this observable can emit nothing more although some parent is
111
- * still live, asked whenever a parent completes. `true` completes it then,
112
- * as all parents completing does (`zip` once a completed source's queue is
113
- * empty).
110
+ * Whether this observable can emit nothing more although some parent is still
111
+ * live, asked whenever a parent completes. `true` completes it then, as all
112
+ * parents completing does (`zip` once a completed source's queue is empty).
114
113
  */
115
114
  // eslint-disable-next-line @typescript-eslint/class-methods-use-this
116
115
  canEmitNoMore(): boolean {
@@ -118,8 +117,8 @@ export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
118
117
  }
119
118
 
120
119
  /**
121
- * Completes this observable once the update now propagating has reached
122
- * every descendant, so that they see the value just passed on first.
120
+ * Completes this observable once the update now propagating has reached every
121
+ * descendant, so that they see the value just passed on first.
123
122
  */
124
123
  protected completeAfterPropagation(): void {
125
124
  runAfterPropagation(() => {
@@ -129,8 +128,8 @@ export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
129
128
  }
130
129
 
131
130
  /**
132
- * An observable derived from `parents` that may emit later, in an update of
133
- * its own, through `startUpdate` (`debounce`, `switchMap`). It may also emit
131
+ * An observable derived from `parents` that may emit later, in an update of its
132
+ * own, through `startUpdate` (`debounce`, `switchMap`). It may also emit
134
133
  * synchronously with `setNext`, as a sync child does. A subclass may also
135
134
  * override `flush`, to pass on at completion what it still holds.
136
135
  */
@@ -199,24 +198,66 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
199
198
  });
200
199
  }
201
200
 
201
+ /**
202
+ * `attach`, then projects the value `parent` holds when `switchMap` /
203
+ * `mergeMap` is created, as `map` and `filter` start from it; a completed
204
+ * parent is not projected. Nothing observes the operator yet, so a value the
205
+ * inner observable emits while it is subscribed is only stored: the token is
206
+ * one no update is queued with.
207
+ *
208
+ * If the projection throws, the operator is disposed, which disposes what the
209
+ * projection made and takes the operator off its parent, and the exception
210
+ * goes on to the caller of `pipe`.
211
+ */
212
+ protected attachProjectingHeldValue<V>(
213
+ parent: Observable<V>,
214
+ project: (value: V, updateToken: UpdateToken) => void,
215
+ ): void {
216
+ this.attach();
217
+
218
+ const sn = parent.getSnapshot();
219
+
220
+ if (this.isCompleted || Optional.isNone(sn)) {
221
+ return;
222
+ }
223
+
224
+ // A flag and `finally` rather than `catch` and rethrow: Sumi bans
225
+ // `try..catch`, and the exception is never caught on its way to `pipe`.
226
+ let mut_projected = false;
227
+
228
+ // @sumi-expect-error exceptions/no-try
229
+ try {
230
+ project(sn.value, issueUnqueuedToken());
231
+
232
+ mut_projected = true;
233
+ } finally {
234
+ if (!mut_projected) {
235
+ this.dispose();
236
+ }
237
+ }
238
+ }
239
+
202
240
  /**
203
241
  * Subscribes to the inner observable that the projection of `switchMap` /
204
242
  * `mergeMap` returned.
205
243
  *
206
244
  * If the inner observable already holds a value (`just(x)`, a state), it
207
- * emits that value synchronously while being subscribed. That first value
208
- * is emitted with `updateToken`, the token of the update that ran the
209
- * projection, so it joins the propagation in progress like a sync
210
- * operator's value would. Starting an update of its own instead would
211
- * reach the nodes below before the current update reached this
212
- * observable's siblings: a node reading both would see the new inner value
213
- * beside stale sibling values (a glitch), and would emit twice.
245
+ * emits that value synchronously while being subscribed. That first value is
246
+ * emitted with `updateToken`, the token of the update that ran the
247
+ * projection, so it joins the propagation in progress like a sync operator's
248
+ * value would. Starting an update of its own instead would reach the nodes
249
+ * below before the current update reached this observable's siblings: a node
250
+ * reading both would see the new inner value beside stale sibling values (a
251
+ * glitch), and would emit twice.
214
252
  *
215
253
  * Values the inner observable emits later start updates of their own.
254
+ * `onComplete` is called when it completes, which may be while it is being
255
+ * subscribed, before this returns.
216
256
  */
217
257
  protected subscribeInner(
218
258
  inner: Observable<A>,
219
259
  updateToken: UpdateToken,
260
+ onComplete?: () => void,
220
261
  ): Subscription {
221
262
  let mut_isSubscribing = true;
222
263
 
@@ -226,7 +267,7 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
226
267
  } else {
227
268
  this.startUpdate(value);
228
269
  }
229
- });
270
+ }, onComplete);
230
271
 
231
272
  mut_isSubscribing = false;
232
273
 
@@ -236,8 +277,8 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
236
277
 
237
278
  /**
238
279
  * A sync child that always holds a value: a subclass passes a `Some` as its
239
- * `initialValue`. It differs from its base only in the types of
240
- * `getSnapshot` and `pipe`.
280
+ * `initialValue`. It differs from its base only in the types of `getSnapshot`
281
+ * and `pipe`.
241
282
  */
242
283
  export class InitializedSyncChildObservableClass<
243
284
  A,
@@ -266,8 +307,8 @@ export class InitializedSyncChildObservableClass<
266
307
  /* helpers */
267
308
 
268
309
  /**
269
- * Called by a parent that has completed: whether no live parent is left, or
270
- * no value can come from the live ones.
310
+ * Called by a parent that has completed: whether no live parent is left, or no
311
+ * value can come from the live ones.
271
312
  */
272
313
  const shouldComplete = (
273
314
  child: ChildNode & Readonly<{ canEmitNoMore: () => boolean }>,
@@ -275,6 +316,63 @@ const shouldComplete = (
275
316
  !child.isCompleted &&
276
317
  (child.parents.every((p) => p.isCompleted) || child.canEmitNoMore());
277
318
 
319
+ const hasCircularDependencyFrom = (
320
+ node: Observable<unknown>,
321
+ mut_visited: MutableSet<Observable<unknown>>,
322
+ mut_inPath: MutableSet<Observable<unknown>>,
323
+ ): boolean => {
324
+ if (mut_inPath.has(node)) {
325
+ return true;
326
+ }
327
+
328
+ if (mut_visited.has(node)) {
329
+ return false;
330
+ }
331
+
332
+ mut_visited.add(node);
333
+
334
+ mut_inPath.add(node);
335
+
336
+ if (node.kind !== 'root') {
337
+ for (const parent of node.parents) {
338
+ if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
339
+ return true;
340
+ }
341
+ }
342
+ }
343
+
344
+ mut_inPath.delete(node);
345
+
346
+ return false;
347
+ };
348
+
349
+ /**
350
+ * Detects circular dependencies by walking the full ancestor chain of the
351
+ * child's parents and checking whether the child already appears among them.
352
+ *
353
+ * @throws {Error} if a circular dependency is detected
354
+ */
355
+ const detectCircularDependency = (child: ChildNode): void => {
356
+ const mut_visited = new Set<Observable<unknown>>();
357
+
358
+ const mut_inPath = new Set<Observable<unknown>>([
359
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
360
+ child as Observable<unknown>,
361
+ ]);
362
+
363
+ for (const parent of child.parents) {
364
+ if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
365
+ // A programming error (invariant violation), not a recoverable failure.
366
+ // `panic` marks the error it throws, so the `Result` / `AsyncResult`
367
+ // boundaries rethrow it rather than turning an invariant violation into
368
+ // an `Err` a caller might handle (Sumi D-48 / D-53).
369
+ panic(
370
+ 'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
371
+ );
372
+ }
373
+ }
374
+ };
375
+
278
376
  /** Registers a child to its parents. */
279
377
  const registerChild = (child: ChildNode): void => {
280
378
  if (checksCircularDependency) {
@@ -287,9 +385,9 @@ const registerChild = (child: ChildNode): void => {
287
385
  };
288
386
 
289
387
  /**
290
- * Undoes {@link registerChild} for a completed child: its parents forget it,
291
- * so that no update puts it in its queue again. Without this a completed
292
- * node stayed reachable for as long as its root lived (#2134).
388
+ * Undoes {@link registerChild} for a completed child: its parents forget it, so
389
+ * that no update puts it in its queue again. Without this a completed node
390
+ * stayed reachable for as long as its root lived (#2134).
293
391
  */
294
392
  const unregisterChild = (child: ChildNode): void => {
295
393
  for (const p of child.parents) {
@@ -320,60 +418,3 @@ const isDevelopmentProcess = (): boolean => {
320
418
  * bundle).
321
419
  */
322
420
  export const checksCircularDependency: boolean = isDevelopmentProcess();
323
-
324
- /**
325
- * Detects circular dependencies by walking the full ancestor chain of the
326
- * child's parents and checking whether the child already appears among them.
327
- *
328
- * @throws {Error} if a circular dependency is detected
329
- */
330
- const detectCircularDependency = (child: ChildNode): void => {
331
- const mut_visited = new Set<Observable<unknown>>();
332
-
333
- const mut_inPath = new Set<Observable<unknown>>([
334
- // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
335
- child as Observable<unknown>,
336
- ]);
337
-
338
- for (const parent of child.parents) {
339
- if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
340
- // A programming error (invariant violation), not a recoverable
341
- // failure. `panic` marks the error it throws, so the `Result` /
342
- // `AsyncResult` boundaries rethrow it rather than turning an invariant
343
- // violation into an `Err` a caller might handle (Sumi D-48 / D-53).
344
- panic(
345
- 'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
346
- );
347
- }
348
- }
349
- };
350
-
351
- const hasCircularDependencyFrom = (
352
- node: Observable<unknown>,
353
- mut_visited: MutableSet<Observable<unknown>>,
354
- mut_inPath: MutableSet<Observable<unknown>>,
355
- ): boolean => {
356
- if (mut_inPath.has(node)) {
357
- return true;
358
- }
359
-
360
- if (mut_visited.has(node)) {
361
- return false;
362
- }
363
-
364
- mut_visited.add(node);
365
-
366
- mut_inPath.add(node);
367
-
368
- if (node.kind !== 'root') {
369
- for (const parent of node.parents) {
370
- if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
371
- return true;
372
- }
373
- }
374
- }
375
-
376
- mut_inPath.delete(node);
377
-
378
- return false;
379
- };
@@ -49,8 +49,8 @@ describe('circular dependency comparison', () => {
49
49
  );
50
50
 
51
51
  // Simulate a cycle: mut_childA → childB → mut_childA
52
- // In normal usage this is impossible — the check runs in every
53
- // child constructor before the reference becomes available.
52
+ // In normal usage this is impossible — the check runs in every child
53
+ // constructor before the reference becomes available.
54
54
  Object.defineProperty(mut_childA, 'parents', {
55
55
  value: [childB],
56
56
  writable: false,
@@ -2,7 +2,8 @@
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, type Optional, type Some } from 'ts-data-forge';
5
+ import type { Optional, Some } from 'ts-data-forge';
6
+ import * as Arr from 'ts-data-forge/array';
6
7
  import { unreachable } from 'ts-std-forge';
7
8
  import type {
8
9
  AsyncChildObservable,
@@ -31,17 +32,16 @@ export type SyncChildObservableTools<A> = Readonly<{
31
32
  complete: () => void;
32
33
 
33
34
  /**
34
- * Completes this observable once the update now propagating has reached
35
- * every descendant, so that they see the value just passed on first.
35
+ * Completes this observable once the update now propagating has reached every
36
+ * descendant, so that they see the value just passed on first.
36
37
  */
37
38
  completeAfterPropagation: () => void;
38
39
 
39
40
  /**
40
41
  * Registers a teardown (clear timers, unsubscribe inner observables, …) to
41
- * run when this observable completes or is disposed, in the order
42
- * registered. Called from `init`, it lets the teardown close over what
43
- * `init` creates. Registered once the observable has completed, the
44
- * teardown runs at once.
42
+ * run when this observable completes or is disposed, in the order registered.
43
+ * Called from `init`, it lets the teardown close over what `init` creates.
44
+ * Registered once the observable has completed, the teardown runs at once.
45
45
  */
46
46
  onComplete: (teardown: () => void) => void;
47
47
  }>;
@@ -57,21 +57,20 @@ export type AsyncChildObservableTools<A> = Readonly<{
57
57
  SyncChildObservableTools<A>;
58
58
 
59
59
  /**
60
- * What a sync child's `init` may return instead of `tryUpdate` alone: the
61
- * hooks the factory calls, which read the state `init` creates.
60
+ * What a sync child's `init` may return instead of `tryUpdate` alone: the hooks
61
+ * the factory calls, which read the state `init` creates.
62
62
  */
63
63
  export type SyncChildObservableHooks = Readonly<{
64
64
  /**
65
- * Called with the token of every update in which a parent of this
66
- * observable emitted.
65
+ * Called with the token of every update in which a parent of this observable
66
+ * emitted.
67
67
  */
68
68
  tryUpdate: (updateToken: UpdateToken) => void;
69
69
 
70
70
  /**
71
- * Whether this observable can emit nothing more although some parent is
72
- * still live, asked whenever a parent completes. `true` completes it then,
73
- * as all parents completing does (`zip` once a completed source's queue is
74
- * empty).
71
+ * Whether this observable can emit nothing more although some parent is still
72
+ * live, asked whenever a parent completes. `true` completes it then, as all
73
+ * parents completing does (`zip` once a completed source's queue is empty).
75
74
  */
76
75
  canEmitNoMore?: () => boolean;
77
76
  }>;
@@ -84,8 +83,8 @@ export type SyncChildObservableHooks = Readonly<{
84
83
  export type AsyncChildObservableHooks = SyncChildObservableHooks &
85
84
  Readonly<{
86
85
  /**
87
- * Run at the beginning of `complete()`, before the teardowns, and never
88
- * by `dispose()`: passes on what this observable still holds through
86
+ * Run at the beginning of `complete()`, before the teardowns, and never by
87
+ * `dispose()`: passes on what this observable still holds through
89
88
  * `startUpdate`, so that its subscribers see it before they see the
90
89
  * completion. A disposed observable has no one left to tell.
91
90
  */
@@ -96,11 +95,11 @@ type ChildObservableConfigBase<P extends NonEmptyUnknownList> = Readonly<{
96
95
  parents: Wrap<P>;
97
96
 
98
97
  /**
99
- * Defaults to one more than the deepest parent. An update reaches
100
- * observables in ascending depth, so an observable `tryUpdate` reads with
101
- * `getSnapshot` without being its child (`withCurrentValueFrom`'s second
102
- * source) has to be shallower than this one too, or this one reads its value
103
- * from before the update: pass `1 + Math.max(...)` over both then.
98
+ * Defaults to one more than the deepest parent. An update reaches observables
99
+ * in ascending depth, so an observable `tryUpdate` reads with `getSnapshot`
100
+ * without being its child (`withCurrentValueFrom`'s second source) has to be
101
+ * shallower than this one too, or this one reads its value from before the
102
+ * update: pass `1 + Math.max(...)` over both then.
104
103
  */
105
104
  depth?: number;
106
105
  }>;
@@ -122,15 +121,15 @@ export type InitializedChildObservableConfig<
122
121
  }>;
123
122
 
124
123
  /**
125
- * Creates a sync-child observable: one derived from `parents` that emits, if
126
- * at all, in the same update as they do. `map`, `filter`, `combine` and the
127
- * other synchronous operators are built with it.
124
+ * Creates a sync-child observable: one derived from `parents` that emits, if at
125
+ * all, in the same update as they do. `map`, `filter`, `combine` and the other
126
+ * synchronous operators are built with it.
128
127
  *
129
128
  * `init` runs during construction and receives the tools. It returns
130
129
  * `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
130
+ * parent of this observable emitted — a parent that did not emit in that update
131
+ * has a different `updateToken`; to emit, call `setNext` with the token
132
+ * received — or an object holding `tryUpdate` and the other hooks
134
133
  * ({@link SyncChildObservableHooks}). State the hooks share can live inside
135
134
  * `init`, next to the teardown it registers with the `onComplete` tool.
136
135
  * `init` may complete the observable; it is then never registered to its
@@ -211,8 +210,8 @@ export const createInitializedSyncChildObservable = <
211
210
  * Creates an async-child observable: one derived from `parents` that may emit
212
211
  * later, in an update of its own, through `startUpdate` (`debounce`,
213
212
  * `switchMap`). It may also emit synchronously with `setNext`, as a sync child
214
- * does. See {@link createSyncChildObservable} for the `init` contract; an
215
- * async child's `init` may also return a `flush` hook
213
+ * does. See {@link createSyncChildObservable} for the `init` contract; an async
214
+ * child's `init` may also return a `flush` hook
216
215
  * ({@link AsyncChildObservableHooks}).
217
216
  */
218
217
  export const createAsyncChildObservable = <A, P extends NonEmptyUnknownList>(
@@ -302,7 +301,9 @@ class CustomAsyncChildObservableClass<
302
301
  const P extends NonEmptyUnknownList,
303
302
  > extends AsyncChildObservableClass<A, P> {
304
303
  #mut_teardowns: readonly (() => void)[] | undefined;
305
- readonly #hooks: AsyncChildObservableHooks;
304
+ // Undefined until `init` returns, which may complete the observable first;
305
+ // `complete` then calls `flush` before there is a hook to call.
306
+ readonly #hooks: AsyncChildObservableHooks | undefined;
306
307
 
307
308
  constructor(
308
309
  config: ChildObservableConfig<A, P>,
@@ -346,15 +347,15 @@ class CustomAsyncChildObservableClass<
346
347
  }
347
348
 
348
349
  override tryUpdate(updateToken: UpdateToken): void {
349
- this.#hooks.tryUpdate(updateToken);
350
+ this.#hooks?.tryUpdate(updateToken);
350
351
  }
351
352
 
352
353
  override canEmitNoMore(): boolean {
353
- return this.#hooks.canEmitNoMore?.() === true;
354
+ return this.#hooks?.canEmitNoMore?.() === true;
354
355
  }
355
356
 
356
357
  protected override flush(): void {
357
- this.#hooks.flush?.();
358
+ this.#hooks?.flush?.();
358
359
  }
359
360
 
360
361
  protected override teardown(): void {