synstate 3.4.0 → 4.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 (305) hide show
  1. package/README.md +13 -8
  2. package/dist/core/authoring.d.mts +7 -0
  3. package/dist/core/authoring.d.mts.map +1 -0
  4. package/dist/core/authoring.mjs +7 -0
  5. package/dist/core/authoring.mjs.map +1 -0
  6. package/dist/core/class/child-observable-class.d.mts +93 -0
  7. package/dist/core/class/child-observable-class.d.mts.map +1 -0
  8. package/dist/core/class/child-observable-class.mjs +248 -0
  9. package/dist/core/class/child-observable-class.mjs.map +1 -0
  10. package/dist/core/class/create-child-observable.d.mts +159 -0
  11. package/dist/core/class/create-child-observable.d.mts.map +1 -0
  12. package/dist/core/class/create-child-observable.mjs +183 -0
  13. package/dist/core/class/create-child-observable.mjs.map +1 -0
  14. package/dist/core/class/create-root-observable.d.mts +37 -0
  15. package/dist/core/class/create-root-observable.d.mts.map +1 -0
  16. package/dist/core/class/create-root-observable.mjs +52 -0
  17. package/dist/core/class/create-root-observable.mjs.map +1 -0
  18. package/dist/core/class/index.d.mts +9 -0
  19. package/dist/core/class/index.d.mts.map +1 -0
  20. package/dist/core/class/index.mjs +9 -0
  21. package/dist/core/class/index.mjs.map +1 -0
  22. package/dist/core/class/manager-observable-class.d.mts +26 -0
  23. package/dist/core/class/manager-observable-class.d.mts.map +1 -0
  24. package/dist/core/class/manager-observable-class.mjs +116 -0
  25. package/dist/core/class/manager-observable-class.mjs.map +1 -0
  26. package/dist/core/class/observable-base-class.d.mts +97 -0
  27. package/dist/core/class/observable-base-class.d.mts.map +1 -0
  28. package/dist/core/class/observable-base-class.mjs +219 -0
  29. package/dist/core/class/observable-base-class.mjs.map +1 -0
  30. package/dist/core/class/observable-node.d.mts +40 -0
  31. package/dist/core/class/observable-node.d.mts.map +1 -0
  32. package/dist/core/class/observable-node.mjs +10 -0
  33. package/dist/core/class/observable-node.mjs.map +1 -0
  34. package/dist/core/class/propagation-state.d.mts +24 -0
  35. package/dist/core/class/propagation-state.d.mts.map +1 -0
  36. package/dist/core/class/propagation-state.mjs +66 -0
  37. package/dist/core/class/propagation-state.mjs.map +1 -0
  38. package/dist/core/class/root-observable-class.d.mts +23 -0
  39. package/dist/core/class/root-observable-class.d.mts.map +1 -0
  40. package/dist/core/class/root-observable-class.mjs +58 -0
  41. package/dist/core/class/root-observable-class.mjs.map +1 -0
  42. package/dist/core/combine/combine.d.mts.map +1 -1
  43. package/dist/core/combine/combine.mjs +25 -14
  44. package/dist/core/combine/combine.mjs.map +1 -1
  45. package/dist/core/combine/merge.d.mts.map +1 -1
  46. package/dist/core/combine/merge.mjs +26 -14
  47. package/dist/core/combine/merge.mjs.map +1 -1
  48. package/dist/core/combine/zip.d.mts.map +1 -1
  49. package/dist/core/combine/zip.mjs +43 -25
  50. package/dist/core/combine/zip.mjs.map +1 -1
  51. package/dist/core/create/counter.d.mts.map +1 -1
  52. package/dist/core/create/counter.mjs +65 -44
  53. package/dist/core/create/counter.mjs.map +1 -1
  54. package/dist/core/create/empty.d.mts +32 -0
  55. package/dist/core/create/empty.d.mts.map +1 -0
  56. package/dist/core/create/empty.mjs +39 -0
  57. package/dist/core/create/empty.mjs.map +1 -0
  58. package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
  59. package/dist/core/create/from-abortable-promise.mjs +26 -18
  60. package/dist/core/create/from-abortable-promise.mjs.map +1 -1
  61. package/dist/core/create/from-promise.d.mts.map +1 -1
  62. package/dist/core/create/from-promise.mjs +30 -21
  63. package/dist/core/create/from-promise.mjs.map +1 -1
  64. package/dist/core/create/from-subscribable.d.mts.map +1 -1
  65. package/dist/core/create/from-subscribable.mjs +37 -24
  66. package/dist/core/create/from-subscribable.mjs.map +1 -1
  67. package/dist/core/create/index.d.mts +1 -0
  68. package/dist/core/create/index.d.mts.map +1 -1
  69. package/dist/core/create/index.mjs +1 -0
  70. package/dist/core/create/index.mjs.map +1 -1
  71. package/dist/core/create/just.d.mts.map +1 -1
  72. package/dist/core/create/just.mjs +4 -2
  73. package/dist/core/create/just.mjs.map +1 -1
  74. package/dist/core/create/source.d.mts.map +1 -1
  75. package/dist/core/create/source.mjs +27 -17
  76. package/dist/core/create/source.mjs.map +1 -1
  77. package/dist/core/create/timer.d.mts.map +1 -1
  78. package/dist/core/create/timer.mjs +40 -25
  79. package/dist/core/create/timer.mjs.map +1 -1
  80. package/dist/core/index.d.mts +2 -0
  81. package/dist/core/index.d.mts.map +1 -1
  82. package/dist/core/index.mjs +2 -0
  83. package/dist/core/index.mjs.map +1 -1
  84. package/dist/core/operators/audit.d.mts +3 -0
  85. package/dist/core/operators/audit.d.mts.map +1 -1
  86. package/dist/core/operators/audit.mjs +56 -28
  87. package/dist/core/operators/audit.mjs.map +1 -1
  88. package/dist/core/operators/debounce.d.mts +3 -0
  89. package/dist/core/operators/debounce.d.mts.map +1 -1
  90. package/dist/core/operators/debounce.mjs +57 -26
  91. package/dist/core/operators/debounce.mjs.map +1 -1
  92. package/dist/core/operators/filter.d.mts.map +1 -1
  93. package/dist/core/operators/filter.mjs +35 -21
  94. package/dist/core/operators/filter.mjs.map +1 -1
  95. package/dist/core/operators/index.d.mts +1 -0
  96. package/dist/core/operators/index.d.mts.map +1 -1
  97. package/dist/core/operators/index.mjs +1 -0
  98. package/dist/core/operators/index.mjs.map +1 -1
  99. package/dist/core/operators/map.d.mts.map +1 -1
  100. package/dist/core/operators/map.mjs +31 -17
  101. package/dist/core/operators/map.mjs.map +1 -1
  102. package/dist/core/operators/merge-map.d.mts +5 -0
  103. package/dist/core/operators/merge-map.d.mts.map +1 -1
  104. package/dist/core/operators/merge-map.mjs +52 -26
  105. package/dist/core/operators/merge-map.mjs.map +1 -1
  106. package/dist/core/operators/pairwise.d.mts.map +1 -1
  107. package/dist/core/operators/pairwise.mjs +31 -20
  108. package/dist/core/operators/pairwise.mjs.map +1 -1
  109. package/dist/core/operators/scan.d.mts.map +1 -1
  110. package/dist/core/operators/scan.mjs +27 -14
  111. package/dist/core/operators/scan.mjs.map +1 -1
  112. package/dist/core/operators/skip-if-no-change.d.mts.map +1 -1
  113. package/dist/core/operators/skip-if-no-change.mjs +33 -19
  114. package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
  115. package/dist/core/operators/skip-until.d.mts.map +1 -1
  116. package/dist/core/operators/skip-until.mjs +40 -26
  117. package/dist/core/operators/skip-until.mjs.map +1 -1
  118. package/dist/core/operators/skip-while.d.mts.map +1 -1
  119. package/dist/core/operators/skip-while.mjs +35 -20
  120. package/dist/core/operators/skip-while.mjs.map +1 -1
  121. package/dist/core/operators/switch-map.d.mts +5 -0
  122. package/dist/core/operators/switch-map.d.mts.map +1 -1
  123. package/dist/core/operators/switch-map.mjs +56 -25
  124. package/dist/core/operators/switch-map.mjs.map +1 -1
  125. package/dist/core/operators/take-until.d.mts.map +1 -1
  126. package/dist/core/operators/take-until.mjs +36 -29
  127. package/dist/core/operators/take-until.mjs.map +1 -1
  128. package/dist/core/operators/take-while.d.mts.map +1 -1
  129. package/dist/core/operators/take-while.mjs +36 -22
  130. package/dist/core/operators/take-while.mjs.map +1 -1
  131. package/dist/core/{predefined/operators → operators}/take.d.mts +7 -3
  132. package/dist/core/operators/take.d.mts.map +1 -0
  133. package/dist/core/operators/take.mjs +102 -0
  134. package/dist/core/operators/take.mjs.map +1 -0
  135. package/dist/core/operators/throttle.d.mts.map +1 -1
  136. package/dist/core/operators/throttle.mjs +41 -26
  137. package/dist/core/operators/throttle.mjs.map +1 -1
  138. package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
  139. package/dist/core/operators/with-buffered-from.mjs +43 -39
  140. package/dist/core/operators/with-buffered-from.mjs.map +1 -1
  141. package/dist/core/operators/with-current-value-from.d.mts.map +1 -1
  142. package/dist/core/operators/with-current-value-from.mjs +36 -21
  143. package/dist/core/operators/with-current-value-from.mjs.map +1 -1
  144. package/dist/core/operators/with-initial-value.d.mts.map +1 -1
  145. package/dist/core/operators/with-initial-value.mjs +24 -12
  146. package/dist/core/operators/with-initial-value.mjs.map +1 -1
  147. package/dist/core/predefined/operators/index.d.mts +0 -1
  148. package/dist/core/predefined/operators/index.d.mts.map +1 -1
  149. package/dist/core/predefined/operators/index.mjs +0 -1
  150. package/dist/core/predefined/operators/index.mjs.map +1 -1
  151. package/dist/core/scope/index.d.mts +2 -0
  152. package/dist/core/scope/index.d.mts.map +1 -0
  153. package/dist/core/scope/index.mjs +2 -0
  154. package/dist/core/scope/index.mjs.map +1 -0
  155. package/dist/core/scope/scope.d.mts +51 -0
  156. package/dist/core/scope/scope.d.mts.map +1 -0
  157. package/dist/core/scope/scope.mjs +194 -0
  158. package/dist/core/scope/scope.mjs.map +1 -0
  159. package/dist/core/types/id.d.mts +0 -2
  160. package/dist/core/types/id.d.mts.map +1 -1
  161. package/dist/core/types/index.d.mts +1 -0
  162. package/dist/core/types/index.d.mts.map +1 -1
  163. package/dist/core/types/index.mjs +1 -0
  164. package/dist/core/types/index.mjs.map +1 -1
  165. package/dist/core/types/observable-family.d.mts +1 -0
  166. package/dist/core/types/observable-family.d.mts.map +1 -1
  167. package/dist/core/types/observable.d.mts +40 -28
  168. package/dist/core/types/observable.d.mts.map +1 -1
  169. package/dist/core/types/observable.mjs +24 -0
  170. package/dist/core/types/observable.mjs.map +1 -1
  171. package/dist/core/types/scope.d.mts +27 -0
  172. package/dist/core/types/scope.d.mts.map +1 -0
  173. package/dist/core/types/scope.mjs +2 -0
  174. package/dist/core/types/scope.mjs.map +1 -0
  175. package/dist/core/utils/current-scope.d.mts +30 -0
  176. package/dist/core/utils/current-scope.d.mts.map +1 -0
  177. package/dist/core/utils/current-scope.mjs +26 -0
  178. package/dist/core/utils/current-scope.mjs.map +1 -0
  179. package/dist/core/utils/id-maker.d.mts +14 -3
  180. package/dist/core/utils/id-maker.d.mts.map +1 -1
  181. package/dist/core/utils/id-maker.mjs +18 -14
  182. package/dist/core/utils/id-maker.mjs.map +1 -1
  183. package/dist/core/utils/index.d.mts +2 -1
  184. package/dist/core/utils/index.d.mts.map +1 -1
  185. package/dist/core/utils/index.mjs +2 -1
  186. package/dist/core/utils/index.mjs.map +1 -1
  187. package/dist/core/utils/max-depth.d.mts +1 -0
  188. package/dist/core/utils/max-depth.d.mts.map +1 -1
  189. package/dist/core/utils/max-depth.mjs +9 -7
  190. package/dist/core/utils/max-depth.mjs.map +1 -1
  191. package/dist/core/utils/observable-brand.d.mts +13 -0
  192. package/dist/core/utils/observable-brand.d.mts.map +1 -0
  193. package/dist/core/utils/observable-brand.mjs +13 -0
  194. package/dist/core/utils/observable-brand.mjs.map +1 -0
  195. package/dist/utils/create-boolean-state.d.mts +2 -2
  196. package/dist/utils/create-reducer.d.mts +7 -7
  197. package/dist/utils/create-reducer.mjs +2 -2
  198. package/dist/utils/create-reducer.mjs.map +1 -1
  199. package/dist/utils/create-state.d.mts +2 -2
  200. package/package.json +17 -17
  201. package/src/core/authoring.mts +19 -0
  202. package/src/core/class/child-observable-class.mts +374 -0
  203. package/src/core/{base → class}/circular-dependency-comparison.test.mts +46 -38
  204. package/src/core/{base → class}/circular-dependency.test.mts +38 -2
  205. package/src/core/class/complete-propagation.test.mts +264 -0
  206. package/src/core/class/create-child-observable.mts +370 -0
  207. package/src/core/class/create-root-observable.mts +107 -0
  208. package/src/core/class/index.mts +8 -0
  209. package/src/core/class/manager-observable-class.mts +166 -0
  210. package/src/core/class/observable-base-class.mts +315 -0
  211. package/src/core/{base → class}/observable-base.test.mts +1 -1
  212. package/src/core/class/observable-node.mts +54 -0
  213. package/src/core/class/propagation-order.test.mts +92 -0
  214. package/src/core/class/propagation-state.mts +80 -0
  215. package/src/core/class/remove-completed.test.mts +369 -0
  216. package/src/core/class/root-observable-class.mts +85 -0
  217. package/src/core/{base → class}/write-during-propagation.test.mts +3 -3
  218. package/src/core/combine/combine.mts +67 -55
  219. package/src/core/combine/merge.mts +33 -23
  220. package/src/core/combine/zip.mts +95 -81
  221. package/src/core/create/counter.mts +95 -65
  222. package/src/core/create/empty.mts +42 -0
  223. package/src/core/create/empty.test.mts +65 -0
  224. package/src/core/create/from-abortable-promise.mts +54 -42
  225. package/src/core/create/from-promise.mts +38 -29
  226. package/src/core/create/from-subscribable.mts +62 -45
  227. package/src/core/create/from-subscribable.test.mts +8 -3
  228. package/src/core/create/index.mts +1 -0
  229. package/src/core/create/just.mts +4 -5
  230. package/src/core/create/source.mts +29 -20
  231. package/src/core/create/timer.mts +51 -32
  232. package/src/core/index.mts +2 -0
  233. package/src/core/operators/audit.mts +79 -45
  234. package/src/core/operators/debounce-audit-complete.test.mts +121 -0
  235. package/src/core/operators/debounce.mts +81 -40
  236. package/src/core/operators/filter.mts +48 -31
  237. package/src/core/operators/index.mts +1 -0
  238. package/src/core/operators/inner-ownership.test.mts +280 -0
  239. package/src/core/operators/map.mts +49 -32
  240. package/src/core/operators/merge-map.mts +81 -43
  241. package/src/core/operators/pairwise.mts +40 -30
  242. package/src/core/operators/scan.mts +41 -24
  243. package/src/core/operators/skip-if-no-change.mts +45 -31
  244. package/src/core/operators/skip-until.mts +55 -41
  245. package/src/core/operators/skip-while.mts +49 -31
  246. package/src/core/operators/switch-map.mts +74 -35
  247. package/src/core/operators/take-until-skip-until-teardown.test.mts +18 -12
  248. package/src/core/operators/take-until.mts +45 -41
  249. package/src/core/operators/take-while.mts +50 -33
  250. package/src/core/operators/take.mts +138 -0
  251. package/src/core/operators/throttle.mts +55 -38
  252. package/src/core/operators/with-buffered-from.mts +42 -44
  253. package/src/core/operators/with-buffered-from.test.mts +4 -3
  254. package/src/core/operators/with-current-value-from.mts +39 -25
  255. package/src/core/operators/with-initial-value.mts +30 -21
  256. package/src/core/predefined/operators/index.mts +0 -1
  257. package/src/core/scope/index.mts +1 -0
  258. package/src/core/scope/scope.mts +260 -0
  259. package/src/core/scope/scope.test.mts +462 -0
  260. package/src/core/types/id.mts +0 -4
  261. package/src/core/types/index.mts +1 -0
  262. package/src/core/types/observable-family.mts +2 -0
  263. package/src/core/types/observable.mts +77 -42
  264. package/src/core/types/scope.mts +27 -0
  265. package/src/core/utils/current-scope.mts +61 -0
  266. package/src/core/utils/id-maker.mts +20 -30
  267. package/src/core/utils/index.mts +2 -1
  268. package/src/core/utils/max-depth.mts +12 -9
  269. package/src/core/utils/observable-brand.mts +12 -0
  270. package/src/utils/create-boolean-state.mts +2 -2
  271. package/src/utils/create-boolean-state.test.mts +18 -4
  272. package/src/utils/create-reducer.mts +9 -9
  273. package/src/utils/create-reducer.test.mts +31 -14
  274. package/src/utils/create-state.mts +2 -2
  275. package/src/utils/create-state.test.mts +12 -14
  276. package/dist/core/base/create-child-observable.d.mts +0 -72
  277. package/dist/core/base/create-child-observable.d.mts.map +0 -1
  278. package/dist/core/base/create-child-observable.mjs +0 -285
  279. package/dist/core/base/create-child-observable.mjs.map +0 -1
  280. package/dist/core/base/create-observable-base.d.mts +0 -149
  281. package/dist/core/base/create-observable-base.d.mts.map +0 -1
  282. package/dist/core/base/create-observable-base.mjs +0 -273
  283. package/dist/core/base/create-observable-base.mjs.map +0 -1
  284. package/dist/core/base/create-root-observable.d.mts +0 -46
  285. package/dist/core/base/create-root-observable.d.mts.map +0 -1
  286. package/dist/core/base/create-root-observable.mjs +0 -80
  287. package/dist/core/base/create-root-observable.mjs.map +0 -1
  288. package/dist/core/base/index.d.mts +0 -3
  289. package/dist/core/base/index.d.mts.map +0 -1
  290. package/dist/core/base/index.mjs +0 -3
  291. package/dist/core/base/index.mjs.map +0 -1
  292. package/dist/core/predefined/operators/take.d.mts.map +0 -1
  293. package/dist/core/predefined/operators/take.mjs +0 -44
  294. package/dist/core/predefined/operators/take.mjs.map +0 -1
  295. package/dist/core/utils/utils.d.mts +0 -6
  296. package/dist/core/utils/utils.d.mts.map +0 -1
  297. package/dist/core/utils/utils.mjs +0 -37
  298. package/dist/core/utils/utils.mjs.map +0 -1
  299. package/src/core/base/create-child-observable.mts +0 -485
  300. package/src/core/base/create-observable-base.mts +0 -568
  301. package/src/core/base/create-root-observable.mts +0 -147
  302. package/src/core/base/index.mts +0 -2
  303. package/src/core/predefined/operators/take.mts +0 -48
  304. package/src/core/utils/utils.mts +0 -56
  305. package/src/core/utils/utils.test.mts +0 -88
@@ -1,6 +1,12 @@
1
+ // Sumi bans classes, `this`, method syntax and writes to `this` outside a
2
+ // `mut_` binding; an observable is a class (`ObservableBaseClass` says why),
3
+ // so those rules are waived here (D-63).
4
+ // @sumi-expect-error-file classes/no-class
5
+ // @sumi-expect-error-file banned-syntax/no-this
6
+ // @sumi-expect-error-file functions/prefer-arrow-function
1
7
  import { Arr, expectType, Optional } from 'ts-data-forge';
2
8
  import type { NonEmptyTuple, Tuple } from 'ts-type-forge';
3
- import { createSyncChildObservable } from '../base/index.mjs';
9
+ import { SyncChildObservableClass } from '../class/index.mjs';
4
10
  import { source } from '../create/index.mjs';
5
11
  import { withInitialValue } from '../operators/index.mjs';
6
12
  import type {
@@ -11,6 +17,7 @@ import type {
11
17
  NonEmptyUnknownList,
12
18
  Observable,
13
19
  SyncChildObservable,
20
+ UpdateToken,
14
21
  Wrap,
15
22
  } from '../types/index.mjs';
16
23
 
@@ -75,7 +82,9 @@ export const combine = <const OS extends NonEmptyTuple<Observable<unknown>>>(
75
82
  parents: OS,
76
83
  ): CombineObservableRefined<OS> =>
77
84
  // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
78
- createCombineObservable(parents) as unknown as CombineObservableRefined<OS>;
85
+ new CombineObservableClass(
86
+ parents,
87
+ ) as unknown as CombineObservableRefined<OS>;
79
88
 
80
89
  /**
81
90
  * Alias for `combine`.
@@ -83,61 +92,64 @@ export const combine = <const OS extends NonEmptyTuple<Observable<unknown>>>(
83
92
  */
84
93
  export const combineLatest = combine;
85
94
 
86
- const createCombineObservable = <const A extends NonEmptyUnknownList>(
87
- parents: Wrap<A>,
88
- ): CombineObservable<A> => {
89
- const parentsValues: Tuple.MapTo<Optional<A[number]>, A> = Arr.map(
90
- parents,
91
- (p) => p.getSnapshot(),
92
- );
93
-
94
- // The annotation on `initialValue` is what replaces the type assertion.
95
- // `Arr.map` carries a single result element type, so it reports `A[number]`
96
- // at every position, and `Tuple.MapTo<A[number], A>` — the uniform mapping
97
- // — is *not* assignable back to `A`. The positional
98
- // `Readonly<{ [K in keyof A]: A[K] }>` is, and TypeScript accepts the
99
- // uniform result into it because the two mapped types share `keyof A`. So
100
- // it is the annotation, not an assertion, that records the position-wise
101
- // fact the checker cannot derive on its own.
102
- const initialValue: Optional<Readonly<{ [K in keyof A]: A[K] }>> = Arr.every(
103
- parentsValues,
104
- Optional.isSome,
105
- )
106
- ? Optional.some(Arr.map(parentsValues, (c) => c.value))
107
- : Optional.none;
108
-
109
- return createSyncChildObservable<A, A>(
110
- {
95
+ class CombineObservableClass<const A extends NonEmptyUnknownList>
96
+ extends SyncChildObservableClass<A, A>
97
+ implements CombineObservable<A>
98
+ {
99
+ constructor(parents: Wrap<A>) {
100
+ const parentsValues: Tuple.MapTo<Optional<A[number]>, A> = Arr.map(
101
+ parents,
102
+ (p) => p.getSnapshot(),
103
+ );
104
+
105
+ // The annotation on `initialValue` is what replaces the type assertion.
106
+ // `Arr.map` carries a single result element type, so it reports `A[number]`
107
+ // at every position, and `Tuple.MapTo<A[number], A>` — the uniform mapping
108
+ // — is *not* assignable back to `A`. The positional
109
+ // `Readonly<{ [K in keyof A]: A[K] }>` is, and TypeScript accepts the
110
+ // uniform result into it because the two mapped types share `keyof A`. So
111
+ // it is the annotation, not an assertion, that records the position-wise
112
+ // fact the checker cannot derive on its own.
113
+ const initialValue: Optional<Readonly<{ [K in keyof A]: A[K] }>> =
114
+ Arr.every(parentsValues, Optional.isSome)
115
+ ? Optional.some(Arr.map(parentsValues, (c) => c.value))
116
+ : Optional.none;
117
+
118
+ super({
111
119
  parents,
112
120
  initialValue,
113
- },
114
- ({ setNext }) =>
115
- (updateToken) => {
116
- if (parents.every((o) => o.updateToken !== updateToken)) {
117
- return; // all parents are skipped
118
- }
119
-
120
- // Same shape-preserving chain as the initial value: `Arr.map` (not the
121
- // native one, which drops the tuple shape) into the uniform
122
- // `Tuple.MapTo`, then a positional annotation to get back to `A`.
123
- const parentValues: Tuple.MapTo<Optional<A[number]>, A> = Arr.map(
124
- parents,
125
- (a) => a.getSnapshot(),
126
- );
127
-
128
- if (!Arr.every(parentValues, Optional.isSome)) {
129
- return;
130
- }
131
-
132
- const nextValue: Readonly<{ [K in keyof A]: A[K] }> = Arr.map(
133
- parentValues,
134
- (a) => a.value,
135
- );
136
-
137
- setNext(nextValue, updateToken);
138
- },
139
- );
140
- };
121
+ });
122
+
123
+ this.attach();
124
+ }
125
+
126
+ override tryUpdate(updateToken: UpdateToken): void {
127
+ const parents = this.parents;
128
+
129
+ if (parents.every((o) => o.updateToken !== updateToken)) {
130
+ return; // all parents are skipped
131
+ }
132
+
133
+ // Same shape-preserving chain as the initial value: `Arr.map` (not the
134
+ // native one, which drops the tuple shape) into the uniform
135
+ // `Tuple.MapTo`, then a positional annotation to get back to `A`.
136
+ const parentValues: Tuple.MapTo<Optional<A[number]>, A> = Arr.map(
137
+ parents,
138
+ (a) => a.getSnapshot(),
139
+ );
140
+
141
+ if (!Arr.every(parentValues, Optional.isSome)) {
142
+ return;
143
+ }
144
+
145
+ const nextValue: Readonly<{ [K in keyof A]: A[K] }> = Arr.map(
146
+ parentValues,
147
+ (a) => a.value,
148
+ );
149
+
150
+ this.setNext(nextValue, updateToken);
151
+ }
152
+ }
141
153
 
142
154
  if (import.meta.vitest !== undefined) {
143
155
  test('type test', () => {
@@ -1,6 +1,12 @@
1
+ // Sumi bans classes, `this`, method syntax and writes to `this` outside a
2
+ // `mut_` binding; an observable is a class (`ObservableBaseClass` says why),
3
+ // so those rules are waived here (D-63).
4
+ // @sumi-expect-error-file classes/no-class
5
+ // @sumi-expect-error-file banned-syntax/no-this
6
+ // @sumi-expect-error-file functions/prefer-arrow-function
1
7
  import { Optional, expectType } from 'ts-data-forge';
2
8
  import type { ArrayElement, NonEmptyTuple } from 'ts-type-forge';
3
- import { createSyncChildObservable } from '../base/index.mjs';
9
+ import { SyncChildObservableClass } from '../class/index.mjs';
4
10
  import { source } from '../create/index.mjs';
5
11
  import type {
6
12
  MergeObservable,
@@ -8,6 +14,7 @@ import type {
8
14
  NonEmptyUnknownList,
9
15
  Observable,
10
16
  SyncChildObservable,
17
+ UpdateToken,
11
18
  Wrap,
12
19
  } from '../types/index.mjs';
13
20
 
@@ -63,34 +70,37 @@ export const merge = <const OS extends NonEmptyTuple<Observable<unknown>>>(
63
70
  parents: OS,
64
71
  ): MergeObservableRefined<OS> =>
65
72
  // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
66
- createMergeObservable(parents) as MergeObservableRefined<OS>;
73
+ new MergeObservableClass(parents) as MergeObservableRefined<OS>;
67
74
 
68
- const createMergeObservable = <const P extends NonEmptyUnknownList>(
69
- parents: Wrap<P>,
70
- ): MergeObservable<P> =>
71
- createSyncChildObservable<ArrayElement<P>, P>(
72
- {
75
+ class MergeObservableClass<const P extends NonEmptyUnknownList>
76
+ extends SyncChildObservableClass<ArrayElement<P>, P>
77
+ implements MergeObservable<P>
78
+ {
79
+ constructor(parents: Wrap<P>) {
80
+ super({
73
81
  parents,
74
82
  initialValue: Optional.none,
75
- },
76
- ({ setNext }) =>
77
- (updateToken) => {
78
- const parentToUse = parents.find(
79
- (o) =>
80
- o.updateToken === updateToken && Optional.isSome(o.getSnapshot()),
81
- );
83
+ });
82
84
 
83
- if (parentToUse === undefined) {
84
- return;
85
- }
85
+ this.attach();
86
+ }
86
87
 
87
- const nextValue =
88
- // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
89
- Optional.unwrap(parentToUse.getSnapshot()) as ArrayElement<P>;
88
+ override tryUpdate(updateToken: UpdateToken): void {
89
+ const parentToUse = this.parents.find(
90
+ (o) => o.updateToken === updateToken && Optional.isSome(o.getSnapshot()),
91
+ );
90
92
 
91
- setNext(nextValue, updateToken);
92
- },
93
- );
93
+ if (parentToUse === undefined) {
94
+ return;
95
+ }
96
+
97
+ const nextValue =
98
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
99
+ Optional.unwrap(parentToUse.getSnapshot()) as ArrayElement<P>;
100
+
101
+ this.setNext(nextValue, updateToken);
102
+ }
103
+ }
94
104
 
95
105
  if (import.meta.vitest !== undefined) {
96
106
  test('type test', () => {
@@ -1,6 +1,13 @@
1
+ // Sumi bans classes, `this`, method syntax and writes to `this` outside a
2
+ // `mut_` binding; an observable is a class (`ObservableBaseClass` says why),
3
+ // so those rules are waived here (D-63).
4
+ // @sumi-expect-error-file classes/no-class
5
+ // @sumi-expect-error-file banned-syntax/no-this
6
+ // @sumi-expect-error-file functions/prefer-arrow-function
7
+ // @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
1
8
  import { Arr, Optional, createQueue, expectType } from 'ts-data-forge';
2
9
  import type { NonEmptyTuple, Tuple } from 'ts-type-forge';
3
- import { createSyncChildObservable } from '../base/index.mjs';
10
+ import { SyncChildObservableClass } from '../class/index.mjs';
4
11
  import { source } from '../create/index.mjs';
5
12
  import { withInitialValue } from '../operators/index.mjs';
6
13
  import type {
@@ -71,90 +78,97 @@ export const zip = <const OS extends NonEmptyTuple<Observable<unknown>>>(
71
78
  parents: OS,
72
79
  ): ZipObservableRefined<OS> =>
73
80
  // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
74
- createZipObservable(parents) as unknown as ZipObservableRefined<OS>;
75
-
76
- const createZipObservable = <const A extends NonEmptyUnknownList>(
77
- parents: Wrap<A>,
78
- ): ZipObservable<A> => {
79
- const parentsValues: Tuple.MapTo<Optional<A[number]>, A> = Arr.map(
80
- parents,
81
- (p) => p.getSnapshot(),
82
- );
83
-
84
- // The annotation on `initialValue` is what replaces the type assertion.
85
- // `Arr.map` carries a single result element type, so it reports `A[number]`
86
- // at every position, and `Tuple.MapTo<A[number], A>` — the uniform mapping
87
- // — is *not* assignable back to `A`. The positional
88
- // `Readonly<{ [K in keyof A]: A[K] }>` is, and TypeScript accepts the
89
- // uniform result into it because the two mapped types share `keyof A`. So
90
- // it is the annotation, not an assertion, that records the position-wise
91
- // fact the checker cannot derive on its own.
92
- const initialValue: Optional<Readonly<{ [K in keyof A]: A[K] }>> = Arr.every(
93
- parentsValues,
94
- Optional.isSome,
95
- )
96
- ? Optional.some(Arr.map(parentsValues, (c) => c.value))
97
- : Optional.none;
98
-
99
- const queues: TupleToQueueTuple<A> = Arr.map(parents, () => createQueue());
100
-
101
- // The token of each parent's latest value that zip has taken in, so that a
102
- // value a parent emits in the update it completes in — its `tryComplete`
103
- // reaches zip before its `tryUpdate` does — still counts as queued.
104
- const mut_takenTokens: UpdateToken[] = Array.from(
105
- parents,
106
- (p) => p.updateToken,
107
- );
108
-
109
- // No tuple can be completed any more once a completed source has nothing
110
- // left to pair (#2343).
111
- const canEmitNoMore = (): boolean =>
112
- parents.some(
81
+ new ZipObservableClass(parents) as unknown as ZipObservableRefined<OS>;
82
+
83
+ class ZipObservableClass<const A extends NonEmptyUnknownList>
84
+ extends SyncChildObservableClass<A, A>
85
+ implements ZipObservable<A>
86
+ {
87
+ readonly #queues: TupleToQueueTuple<A>;
88
+
89
+ /**
90
+ * The token of each parent's latest value that zip has taken in, so that a
91
+ * value a parent emits in the update it completes in — its `tryComplete`
92
+ * reaches zip before its `tryUpdate` does — still counts as queued.
93
+ * Written in place, one slot per parent, on every update that reaches zip.
94
+ */
95
+ // @sumi-expect-error readonly/require-readonly-type
96
+ readonly #mut_takenTokens: UpdateToken[];
97
+
98
+ constructor(parents: Wrap<A>) {
99
+ const parentsValues: Tuple.MapTo<Optional<A[number]>, A> = Arr.map(
100
+ parents,
101
+ (p) => p.getSnapshot(),
102
+ );
103
+
104
+ // The annotation on `initialValue` is what replaces the type assertion.
105
+ // `Arr.map` carries a single result element type, so it reports `A[number]`
106
+ // at every position, and `Tuple.MapTo<A[number], A>` — the uniform mapping
107
+ // — is *not* assignable back to `A`. The positional
108
+ // `Readonly<{ [K in keyof A]: A[K] }>` is, and TypeScript accepts the
109
+ // uniform result into it because the two mapped types share `keyof A`. So
110
+ // it is the annotation, not an assertion, that records the position-wise
111
+ // fact the checker cannot derive on its own.
112
+ const initialValue: Optional<Readonly<{ [K in keyof A]: A[K] }>> =
113
+ Arr.every(parentsValues, Optional.isSome)
114
+ ? Optional.some(Arr.map(parentsValues, (c) => c.value))
115
+ : Optional.none;
116
+
117
+ super({ parents, initialValue });
118
+
119
+ this.#queues = Arr.map(parents, () => createQueue());
120
+
121
+ this.#mut_takenTokens = Array.from(parents, (p) => p.updateToken);
122
+
123
+ this.attach();
124
+ }
125
+
126
+ override tryUpdate(updateToken: UpdateToken): void {
127
+ const queues = this.#queues;
128
+
129
+ for (const [index, par] of this.parents.entries()) {
130
+ const sn = par.getSnapshot();
131
+
132
+ if (!(par.updateToken === updateToken && Optional.isSome(sn))) {
133
+ continue;
134
+ }
135
+
136
+ queues[index]?.enqueue(sn.value);
137
+
138
+ this.#mut_takenTokens[index] = updateToken;
139
+ }
140
+
141
+ if (queues.some((list) => list.isEmpty)) {
142
+ return;
143
+ }
144
+
145
+ const nextValue =
146
+ // `Arr.map` has a single result element type, so it reports
147
+ // `A[number] | undefined` per position rather than `A[P]`.
148
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
149
+ Arr.map(queues, (q) => Optional.unwrap(q.dequeue())) as A;
150
+
151
+ this.setNext(nextValue, updateToken);
152
+
153
+ if (this.canEmitNoMore()) {
154
+ this.complete();
155
+ }
156
+ }
157
+
158
+ /**
159
+ * No tuple can be completed any more once a completed source has nothing
160
+ * left to pair (#2343).
161
+ */
162
+ override canEmitNoMore(): boolean {
163
+ return this.parents.some(
113
164
  (par, index) =>
114
165
  par.isCompleted &&
115
- queues[index]?.isEmpty === true &&
116
- (par.updateToken === mut_takenTokens[index] ||
166
+ this.#queues[index]?.isEmpty === true &&
167
+ (par.updateToken === this.#mut_takenTokens[index] ||
117
168
  Optional.isNone(par.getSnapshot())),
118
169
  );
119
-
120
- return createSyncChildObservable<A, A>(
121
- {
122
- parents,
123
- initialValue,
124
- canEmitNoMore,
125
- },
126
- ({ setNext, complete }) =>
127
- (updateToken) => {
128
- for (const [index, par] of parents.entries()) {
129
- const sn = par.getSnapshot();
130
-
131
- if (!(par.updateToken === updateToken && Optional.isSome(sn))) {
132
- continue;
133
- }
134
-
135
- queues[index]?.enqueue(sn.value);
136
-
137
- mut_takenTokens[index] = updateToken;
138
- }
139
-
140
- if (queues.some((list) => list.isEmpty)) {
141
- return;
142
- }
143
-
144
- const nextValue =
145
- // `Arr.map` has a single result element type, so it reports
146
- // `A[number] | undefined` per position rather than `A[P]`.
147
- // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
148
- Arr.map(queues, (q) => Optional.unwrap(q.dequeue())) as A;
149
-
150
- setNext(nextValue, updateToken);
151
-
152
- if (canEmitNoMore()) {
153
- complete();
154
- }
155
- },
156
- );
157
- };
170
+ }
171
+ }
158
172
 
159
173
  if (import.meta.vitest !== undefined) {
160
174
  test('type test', () => {
@@ -1,6 +1,13 @@
1
+ // Sumi bans classes, `this`, method syntax and writes to `this` outside a
2
+ // `mut_` binding; an observable is a class (`ObservableBaseClass` says why),
3
+ // so those rules are waived here (D-63).
4
+ // @sumi-expect-error-file classes/no-class
5
+ // @sumi-expect-error-file banned-syntax/no-this
6
+ // @sumi-expect-error-file functions/prefer-arrow-function
7
+ // @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
1
8
  import { asSafeUint, Optional, SafeUint } from 'ts-data-forge';
2
9
  import type { PositiveSafeIntWithSmallInt } from 'ts-type-forge';
3
- import { createRootObservable } from '../base/index.mjs';
10
+ import { RootObservableClass } from '../class/index.mjs';
4
11
  import type { CounterObservable, TimerId } from '../types/index.mjs';
5
12
 
6
13
  /**
@@ -65,78 +72,101 @@ export const counter = (
65
72
  times?: PositiveSafeIntWithSmallInt;
66
73
  }>,
67
74
  ): CounterObservable => {
68
- let mut_counter: SafeUint = asSafeUint(0);
75
+ const observable = new CounterObservableClass(
76
+ intervalMilliSeconds,
77
+ options?.emitInitial ?? true,
78
+ options?.times,
79
+ );
80
+
81
+ if (!(options?.startManually ?? false)) {
82
+ observable.start();
83
+ }
84
+
85
+ return observable;
86
+ };
87
+
88
+ class CounterObservableClass
89
+ extends RootObservableClass<SafeUint>
90
+ implements CounterObservable
91
+ {
92
+ readonly #intervalMilliSeconds: number;
93
+ readonly #emitInitial: boolean;
94
+ readonly #times: PositiveSafeIntWithSmallInt | undefined;
95
+ #mut_counter: SafeUint;
96
+ #mut_emittedCount: number;
97
+ #mut_timerId0: TimerId | undefined;
98
+ #mut_timerId: TimerId | undefined;
99
+ #mut_isStarted: boolean;
69
100
 
70
- let mut_timerId0: TimerId | undefined = undefined;
101
+ constructor(
102
+ intervalMilliSeconds: number,
103
+ emitInitial: boolean,
104
+ times: PositiveSafeIntWithSmallInt | undefined,
105
+ ) {
106
+ super({ initialValue: Optional.none });
71
107
 
72
- let mut_timerId: TimerId | undefined = undefined;
108
+ this.#intervalMilliSeconds = intervalMilliSeconds;
73
109
 
74
- let mut_isStarted = false;
110
+ this.#emitInitial = emitInitial;
75
111
 
76
- let mut_emittedCount = 0;
112
+ this.#times = times;
77
113
 
78
- const resetTimer = (): void => {
79
- if (mut_timerId0 !== undefined) {
80
- clearTimeout(mut_timerId0);
114
+ this.#mut_counter = asSafeUint(0);
115
+
116
+ this.#mut_emittedCount = 0;
117
+
118
+ this.#mut_timerId0 = undefined;
119
+
120
+ this.#mut_timerId = undefined;
121
+
122
+ this.#mut_isStarted = false;
123
+ }
124
+
125
+ #emit(value: SafeUint): void {
126
+ this.startUpdate(value);
127
+
128
+ this.#mut_emittedCount += 1;
129
+
130
+ if (this.#times !== undefined && this.#mut_emittedCount >= this.#times) {
131
+ this.complete();
81
132
  }
133
+ }
82
134
 
83
- if (mut_timerId !== undefined) {
84
- clearInterval(mut_timerId);
135
+ start(): void {
136
+ if (this.#mut_isStarted) {
137
+ console.warn('cannot start twice');
138
+
139
+ return;
85
140
  }
86
- };
87
-
88
- const observable = createRootObservable<
89
- SafeUint,
90
- Readonly<{ start: () => void }>
91
- >(
92
- { initialValue: Optional.none, onComplete: resetTimer },
93
- ({ startUpdate, isCompleted, complete }) => ({
94
- start: () => {
95
- if (mut_isStarted) {
96
- console.warn('cannot start twice');
97
-
98
- return;
99
- }
100
-
101
- mut_isStarted = true;
102
-
103
- if (isCompleted()) {
104
- console.warn('cannot restart stopped CounterObservable');
105
-
106
- return;
107
- }
108
-
109
- const emit = (value: SafeUint): void => {
110
- startUpdate(value);
111
-
112
- mut_emittedCount += 1;
113
-
114
- if (
115
- options?.times !== undefined &&
116
- mut_emittedCount >= options.times
117
- ) {
118
- complete();
119
- }
120
- };
121
-
122
- if (options?.emitInitial ?? true) {
123
- mut_timerId0 = setTimeout(() => {
124
- emit(mut_counter);
125
- }, 0);
126
- }
127
-
128
- mut_timerId = setInterval(() => {
129
- mut_counter = SafeUint.add(1, mut_counter);
130
-
131
- emit(mut_counter);
132
- }, intervalMilliSeconds);
133
- },
134
- }),
135
- );
136
141
 
137
- if (!(options?.startManually ?? false)) {
138
- observable.start();
142
+ this.#mut_isStarted = true;
143
+
144
+ if (this.isCompleted) {
145
+ console.warn('cannot restart stopped CounterObservable');
146
+
147
+ return;
148
+ }
149
+
150
+ if (this.#emitInitial) {
151
+ this.#mut_timerId0 = setTimeout(() => {
152
+ this.#emit(this.#mut_counter);
153
+ }, 0);
154
+ }
155
+
156
+ this.#mut_timerId = setInterval(() => {
157
+ this.#mut_counter = SafeUint.add(1, this.#mut_counter);
158
+
159
+ this.#emit(this.#mut_counter);
160
+ }, this.#intervalMilliSeconds);
139
161
  }
140
162
 
141
- return observable;
142
- };
163
+ protected override teardown(): void {
164
+ if (this.#mut_timerId0 !== undefined) {
165
+ clearTimeout(this.#mut_timerId0);
166
+ }
167
+
168
+ if (this.#mut_timerId !== undefined) {
169
+ clearInterval(this.#mut_timerId);
170
+ }
171
+ }
172
+ }
@@ -0,0 +1,42 @@
1
+ import { Optional } from 'ts-data-forge';
2
+ import { RootObservableClass } from '../class/index.mjs';
3
+ import type { RootObservable } from '../types/index.mjs';
4
+
5
+ /**
6
+ * Creates an Observable that holds no value and is already completed, so it
7
+ * never passes anything on (almost equivalent to RxJS `EMPTY`). Useful inside
8
+ * `switchMap` for the branch that should follow nothing, where a `source()`
9
+ * would offer a `next` that nothing calls.
10
+ *
11
+ * @template A - The type of the values it would carry
12
+ * @returns An Observable with no value that is already completed
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const nothing$ = empty<number>();
17
+ *
18
+ * const valueHistory: number[] = [];
19
+ *
20
+ * nothing$.subscribe(
21
+ * (value) => {
22
+ * valueHistory.push(value);
23
+ * },
24
+ * () => {
25
+ * // onComplete — called immediately since empty() completes right away
26
+ * },
27
+ * );
28
+ *
29
+ * assert.deepStrictEqual(valueHistory, []);
30
+ *
31
+ * assert.strictEqual(nothing$.isCompleted, true);
32
+ * ```
33
+ */
34
+ export const empty = <A,>(): RootObservable<A> => {
35
+ const obs = new RootObservableClass<A>({
36
+ initialValue: Optional.none,
37
+ });
38
+
39
+ obs.complete();
40
+
41
+ return obs;
42
+ };