synstate 3.3.0 → 4.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 (307) hide show
  1. package/README.md +14 -9
  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 +0 -3
  46. package/dist/core/combine/merge.d.mts.map +1 -1
  47. package/dist/core/combine/merge.mjs +24 -15
  48. package/dist/core/combine/merge.mjs.map +1 -1
  49. package/dist/core/combine/zip.d.mts +4 -2
  50. package/dist/core/combine/zip.d.mts.map +1 -1
  51. package/dist/core/combine/zip.mjs +54 -18
  52. package/dist/core/combine/zip.mjs.map +1 -1
  53. package/dist/core/create/counter.d.mts +9 -3
  54. package/dist/core/create/counter.d.mts.map +1 -1
  55. package/dist/core/create/counter.mjs +69 -35
  56. package/dist/core/create/counter.mjs.map +1 -1
  57. package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
  58. package/dist/core/create/from-abortable-promise.mjs +26 -18
  59. package/dist/core/create/from-abortable-promise.mjs.map +1 -1
  60. package/dist/core/create/from-promise.d.mts.map +1 -1
  61. package/dist/core/create/from-promise.mjs +30 -21
  62. package/dist/core/create/from-promise.mjs.map +1 -1
  63. package/dist/core/create/from-subscribable.d.mts.map +1 -1
  64. package/dist/core/create/from-subscribable.mjs +48 -12
  65. package/dist/core/create/from-subscribable.mjs.map +1 -1
  66. package/dist/core/create/just.d.mts.map +1 -1
  67. package/dist/core/create/just.mjs +4 -2
  68. package/dist/core/create/just.mjs.map +1 -1
  69. package/dist/core/create/source.d.mts +7 -0
  70. package/dist/core/create/source.d.mts.map +1 -1
  71. package/dist/core/create/source.mjs +27 -13
  72. package/dist/core/create/source.mjs.map +1 -1
  73. package/dist/core/create/timer.d.mts.map +1 -1
  74. package/dist/core/create/timer.mjs +40 -25
  75. package/dist/core/create/timer.mjs.map +1 -1
  76. package/dist/core/index.d.mts +2 -0
  77. package/dist/core/index.d.mts.map +1 -1
  78. package/dist/core/index.mjs +2 -0
  79. package/dist/core/index.mjs.map +1 -1
  80. package/dist/core/operators/audit.d.mts +3 -0
  81. package/dist/core/operators/audit.d.mts.map +1 -1
  82. package/dist/core/operators/audit.mjs +56 -28
  83. package/dist/core/operators/audit.mjs.map +1 -1
  84. package/dist/core/operators/debounce.d.mts +3 -0
  85. package/dist/core/operators/debounce.d.mts.map +1 -1
  86. package/dist/core/operators/debounce.mjs +57 -26
  87. package/dist/core/operators/debounce.mjs.map +1 -1
  88. package/dist/core/operators/filter.d.mts.map +1 -1
  89. package/dist/core/operators/filter.mjs +35 -21
  90. package/dist/core/operators/filter.mjs.map +1 -1
  91. package/dist/core/operators/index.d.mts +1 -0
  92. package/dist/core/operators/index.d.mts.map +1 -1
  93. package/dist/core/operators/index.mjs +1 -0
  94. package/dist/core/operators/index.mjs.map +1 -1
  95. package/dist/core/operators/map.d.mts.map +1 -1
  96. package/dist/core/operators/map.mjs +31 -17
  97. package/dist/core/operators/map.mjs.map +1 -1
  98. package/dist/core/operators/merge-map.d.mts +5 -3
  99. package/dist/core/operators/merge-map.d.mts.map +1 -1
  100. package/dist/core/operators/merge-map.mjs +49 -26
  101. package/dist/core/operators/merge-map.mjs.map +1 -1
  102. package/dist/core/operators/pairwise.d.mts.map +1 -1
  103. package/dist/core/operators/pairwise.mjs +31 -20
  104. package/dist/core/operators/pairwise.mjs.map +1 -1
  105. package/dist/core/operators/scan.d.mts.map +1 -1
  106. package/dist/core/operators/scan.mjs +27 -14
  107. package/dist/core/operators/scan.mjs.map +1 -1
  108. package/dist/core/operators/skip-if-no-change.d.mts.map +1 -1
  109. package/dist/core/operators/skip-if-no-change.mjs +33 -19
  110. package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
  111. package/dist/core/operators/skip-until.d.mts.map +1 -1
  112. package/dist/core/operators/skip-until.mjs +43 -20
  113. package/dist/core/operators/skip-until.mjs.map +1 -1
  114. package/dist/core/operators/skip-while.d.mts.map +1 -1
  115. package/dist/core/operators/skip-while.mjs +35 -20
  116. package/dist/core/operators/skip-while.mjs.map +1 -1
  117. package/dist/core/operators/switch-map.d.mts +5 -3
  118. package/dist/core/operators/switch-map.d.mts.map +1 -1
  119. package/dist/core/operators/switch-map.mjs +53 -25
  120. package/dist/core/operators/switch-map.mjs.map +1 -1
  121. package/dist/core/operators/take-until.d.mts.map +1 -1
  122. package/dist/core/operators/take-until.mjs +38 -19
  123. package/dist/core/operators/take-until.mjs.map +1 -1
  124. package/dist/core/operators/take-while.d.mts.map +1 -1
  125. package/dist/core/operators/take-while.mjs +36 -22
  126. package/dist/core/operators/take-while.mjs.map +1 -1
  127. package/dist/core/{predefined/operators → operators}/take.d.mts +7 -3
  128. package/dist/core/operators/take.d.mts.map +1 -0
  129. package/dist/core/operators/take.mjs +102 -0
  130. package/dist/core/operators/take.mjs.map +1 -0
  131. package/dist/core/operators/throttle.d.mts.map +1 -1
  132. package/dist/core/operators/throttle.mjs +41 -26
  133. package/dist/core/operators/throttle.mjs.map +1 -1
  134. package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
  135. package/dist/core/operators/with-buffered-from.mjs +44 -31
  136. package/dist/core/operators/with-buffered-from.mjs.map +1 -1
  137. package/dist/core/operators/with-current-value-from.d.mts.map +1 -1
  138. package/dist/core/operators/with-current-value-from.mjs +36 -21
  139. package/dist/core/operators/with-current-value-from.mjs.map +1 -1
  140. package/dist/core/operators/with-initial-value.d.mts.map +1 -1
  141. package/dist/core/operators/with-initial-value.mjs +24 -12
  142. package/dist/core/operators/with-initial-value.mjs.map +1 -1
  143. package/dist/core/predefined/operators/index.d.mts +0 -1
  144. package/dist/core/predefined/operators/index.d.mts.map +1 -1
  145. package/dist/core/predefined/operators/index.mjs +0 -1
  146. package/dist/core/predefined/operators/index.mjs.map +1 -1
  147. package/dist/core/scope/index.d.mts +2 -0
  148. package/dist/core/scope/index.d.mts.map +1 -0
  149. package/dist/core/scope/index.mjs +2 -0
  150. package/dist/core/scope/index.mjs.map +1 -0
  151. package/dist/core/scope/scope.d.mts +51 -0
  152. package/dist/core/scope/scope.d.mts.map +1 -0
  153. package/dist/core/scope/scope.mjs +194 -0
  154. package/dist/core/scope/scope.mjs.map +1 -0
  155. package/dist/core/types/id.d.mts +0 -2
  156. package/dist/core/types/id.d.mts.map +1 -1
  157. package/dist/core/types/index.d.mts +1 -0
  158. package/dist/core/types/index.d.mts.map +1 -1
  159. package/dist/core/types/index.mjs +1 -0
  160. package/dist/core/types/index.mjs.map +1 -1
  161. package/dist/core/types/observable-family.d.mts +6 -1
  162. package/dist/core/types/observable-family.d.mts.map +1 -1
  163. package/dist/core/types/observable-family.mjs.map +1 -1
  164. package/dist/core/types/observable.d.mts +40 -28
  165. package/dist/core/types/observable.d.mts.map +1 -1
  166. package/dist/core/types/observable.mjs +24 -0
  167. package/dist/core/types/observable.mjs.map +1 -1
  168. package/dist/core/types/scope.d.mts +27 -0
  169. package/dist/core/types/scope.d.mts.map +1 -0
  170. package/dist/core/types/scope.mjs +2 -0
  171. package/dist/core/types/scope.mjs.map +1 -0
  172. package/dist/core/utils/current-scope.d.mts +30 -0
  173. package/dist/core/utils/current-scope.d.mts.map +1 -0
  174. package/dist/core/utils/current-scope.mjs +26 -0
  175. package/dist/core/utils/current-scope.mjs.map +1 -0
  176. package/dist/core/utils/id-maker.d.mts +14 -3
  177. package/dist/core/utils/id-maker.d.mts.map +1 -1
  178. package/dist/core/utils/id-maker.mjs +18 -14
  179. package/dist/core/utils/id-maker.mjs.map +1 -1
  180. package/dist/core/utils/index.d.mts +2 -1
  181. package/dist/core/utils/index.d.mts.map +1 -1
  182. package/dist/core/utils/index.mjs +2 -1
  183. package/dist/core/utils/index.mjs.map +1 -1
  184. package/dist/core/utils/max-depth.d.mts +1 -0
  185. package/dist/core/utils/max-depth.d.mts.map +1 -1
  186. package/dist/core/utils/max-depth.mjs +9 -7
  187. package/dist/core/utils/max-depth.mjs.map +1 -1
  188. package/dist/core/utils/observable-brand.d.mts +13 -0
  189. package/dist/core/utils/observable-brand.d.mts.map +1 -0
  190. package/dist/core/utils/observable-brand.mjs +13 -0
  191. package/dist/core/utils/observable-brand.mjs.map +1 -0
  192. package/dist/utils/create-boolean-state.d.mts +2 -2
  193. package/dist/utils/create-event-emitter.d.mts +2 -2
  194. package/dist/utils/create-reducer.d.mts +17 -8
  195. package/dist/utils/create-reducer.d.mts.map +1 -1
  196. package/dist/utils/create-reducer.mjs +14 -6
  197. package/dist/utils/create-reducer.mjs.map +1 -1
  198. package/dist/utils/create-state.d.mts +9 -2
  199. package/dist/utils/create-state.d.mts.map +1 -1
  200. package/dist/utils/create-state.mjs +7 -0
  201. package/dist/utils/create-state.mjs.map +1 -1
  202. package/package.json +16 -16
  203. package/src/core/authoring.mts +19 -0
  204. package/src/core/class/child-observable-class.mts +374 -0
  205. package/src/core/{base → class}/circular-dependency-comparison.test.mts +46 -38
  206. package/src/core/{base → class}/circular-dependency.test.mts +38 -2
  207. package/src/core/class/complete-propagation.test.mts +264 -0
  208. package/src/core/class/create-child-observable.mts +370 -0
  209. package/src/core/class/create-root-observable.mts +107 -0
  210. package/src/core/class/index.mts +8 -0
  211. package/src/core/class/manager-observable-class.mts +166 -0
  212. package/src/core/class/observable-base-class.mts +315 -0
  213. package/src/core/{base → class}/observable-base.test.mts +1 -1
  214. package/src/core/class/observable-node.mts +54 -0
  215. package/src/core/class/propagation-order.test.mts +92 -0
  216. package/src/core/class/propagation-state.mts +80 -0
  217. package/src/core/class/remove-completed.test.mts +369 -0
  218. package/src/core/class/root-observable-class.mts +85 -0
  219. package/src/core/class/write-during-propagation.test.mts +234 -0
  220. package/src/core/combine/combine.mts +67 -55
  221. package/src/core/combine/merge.mts +33 -26
  222. package/src/core/combine/zip.mts +103 -58
  223. package/src/core/create/counter.mts +105 -54
  224. package/src/core/create/counter.test.mts +82 -0
  225. package/src/core/create/from-abortable-promise.mts +54 -42
  226. package/src/core/create/from-promise.mts +38 -29
  227. package/src/core/create/from-subscribable.mts +68 -23
  228. package/src/core/create/from-subscribable.test.mts +134 -0
  229. package/src/core/create/just.mts +4 -5
  230. package/src/core/create/source.mts +37 -13
  231. package/src/core/create/source.test.mts +41 -0
  232. package/src/core/create/timer.mts +51 -32
  233. package/src/core/index.mts +2 -0
  234. package/src/core/operators/audit.mts +79 -45
  235. package/src/core/operators/debounce-audit-complete.test.mts +121 -0
  236. package/src/core/operators/debounce.mts +81 -40
  237. package/src/core/operators/filter.mts +48 -31
  238. package/src/core/operators/index.mts +1 -0
  239. package/src/core/operators/inner-ownership.test.mts +280 -0
  240. package/src/core/operators/map.mts +49 -32
  241. package/src/core/operators/merge-map.mts +81 -46
  242. package/src/core/operators/pairwise.mts +40 -30
  243. package/src/core/operators/scan.mts +41 -24
  244. package/src/core/operators/skip-if-no-change.mts +45 -31
  245. package/src/core/operators/skip-until.mts +59 -33
  246. package/src/core/operators/skip-while.mts +49 -31
  247. package/src/core/operators/switch-map.mts +74 -38
  248. package/src/core/operators/take-until-skip-until-teardown.test.mts +133 -0
  249. package/src/core/operators/take-until.mts +49 -31
  250. package/src/core/operators/take-while.mts +50 -33
  251. package/src/core/operators/take.mts +138 -0
  252. package/src/core/operators/throttle.mts +55 -38
  253. package/src/core/operators/with-buffered-from.mts +45 -35
  254. package/src/core/operators/with-buffered-from.test.mts +57 -0
  255. package/src/core/operators/with-current-value-from.mts +39 -25
  256. package/src/core/operators/with-initial-value.mts +30 -21
  257. package/src/core/predefined/operators/index.mts +0 -1
  258. package/src/core/scope/index.mts +1 -0
  259. package/src/core/scope/scope.mts +260 -0
  260. package/src/core/scope/scope.test.mts +462 -0
  261. package/src/core/types/id.mts +0 -4
  262. package/src/core/types/index.mts +1 -0
  263. package/src/core/types/observable-family.mts +7 -1
  264. package/src/core/types/observable.mts +77 -42
  265. package/src/core/types/scope.mts +27 -0
  266. package/src/core/utils/current-scope.mts +61 -0
  267. package/src/core/utils/id-maker.mts +20 -30
  268. package/src/core/utils/index.mts +2 -1
  269. package/src/core/utils/max-depth.mts +12 -9
  270. package/src/core/utils/observable-brand.mts +12 -0
  271. package/src/utils/create-boolean-state.mts +2 -2
  272. package/src/utils/create-boolean-state.test.mts +18 -4
  273. package/src/utils/create-event-emitter.mts +2 -2
  274. package/src/utils/create-reducer.mts +22 -16
  275. package/src/utils/create-reducer.test.mts +43 -14
  276. package/src/utils/create-state.mts +9 -2
  277. package/src/utils/create-state.test.mts +32 -14
  278. package/dist/core/base/create-child-observable.d.mts +0 -65
  279. package/dist/core/base/create-child-observable.d.mts.map +0 -1
  280. package/dist/core/base/create-child-observable.mjs +0 -267
  281. package/dist/core/base/create-child-observable.mjs.map +0 -1
  282. package/dist/core/base/create-observable-base.d.mts +0 -147
  283. package/dist/core/base/create-observable-base.d.mts.map +0 -1
  284. package/dist/core/base/create-observable-base.mjs +0 -255
  285. package/dist/core/base/create-observable-base.mjs.map +0 -1
  286. package/dist/core/base/create-root-observable.d.mts +0 -37
  287. package/dist/core/base/create-root-observable.d.mts.map +0 -1
  288. package/dist/core/base/create-root-observable.mjs +0 -57
  289. package/dist/core/base/create-root-observable.mjs.map +0 -1
  290. package/dist/core/base/index.d.mts +0 -3
  291. package/dist/core/base/index.d.mts.map +0 -1
  292. package/dist/core/base/index.mjs +0 -3
  293. package/dist/core/base/index.mjs.map +0 -1
  294. package/dist/core/predefined/operators/take.d.mts.map +0 -1
  295. package/dist/core/predefined/operators/take.mjs +0 -44
  296. package/dist/core/predefined/operators/take.mjs.map +0 -1
  297. package/dist/core/utils/utils.d.mts +0 -6
  298. package/dist/core/utils/utils.d.mts.map +0 -1
  299. package/dist/core/utils/utils.mjs +0 -37
  300. package/dist/core/utils/utils.mjs.map +0 -1
  301. package/src/core/base/create-child-observable.mts +0 -457
  302. package/src/core/base/create-observable-base.mts +0 -548
  303. package/src/core/base/create-root-observable.mts +0 -113
  304. package/src/core/base/index.mts +0 -2
  305. package/src/core/predefined/operators/take.mts +0 -48
  306. package/src/core/utils/utils.mts +0 -56
  307. package/src/core/utils/utils.test.mts +0 -88
@@ -1,5 +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
- import { createRootObservable } from '../base/index.mjs';
9
+ import type { PositiveSafeIntWithSmallInt } from 'ts-type-forge';
10
+ import { RootObservableClass } from '../class/index.mjs';
3
11
  import type { CounterObservable, TimerId } from '../types/index.mjs';
4
12
 
5
13
  /**
@@ -15,6 +23,10 @@ import type { CounterObservable, TimerId } from '../types/index.mjs';
15
23
  * @param options.emitInitial - If false, skips the 0 emitted right after
16
24
  * `start()`, so the first value is 1, one interval later — like RxJS
17
25
  * `interval` rather than `timer(0, ms)` (default: true)
26
+ * @param options.times - If set, completes right after the `times`-th
27
+ * emission and clears its timers. Completion does not travel upstream, so
28
+ * `counter(ms).pipe(take(n))` leaves the counter running; pass `times` to
29
+ * the counter instead (default: unlimited)
18
30
  * @returns An observable that emits sequential numbers
19
31
  *
20
32
  * @example
@@ -45,11 +57,11 @@ import type { CounterObservable, TimerId } from '../types/index.mjs';
45
57
  *
46
58
  * assert.isTrue(Arr.isMinLengthArray(3, valueHistory));
47
59
  *
48
- * assert.deepStrictEqual(valueHistory[0], 0);
60
+ * assert.strictEqual(valueHistory[0], 0);
49
61
  *
50
- * assert.deepStrictEqual(valueHistory[1], 1);
62
+ * assert.strictEqual(valueHistory[1], 1);
51
63
  *
52
- * assert.deepStrictEqual(valueHistory[2], 2);
64
+ * assert.strictEqual(valueHistory[2], 2);
53
65
  * ```
54
66
  */
55
67
  export const counter = (
@@ -57,65 +69,104 @@ export const counter = (
57
69
  options?: Readonly<{
58
70
  startManually?: boolean;
59
71
  emitInitial?: boolean;
72
+ times?: PositiveSafeIntWithSmallInt;
60
73
  }>,
61
74
  ): CounterObservable => {
62
- 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;
100
+
101
+ constructor(
102
+ intervalMilliSeconds: number,
103
+ emitInitial: boolean,
104
+ times: PositiveSafeIntWithSmallInt | undefined,
105
+ ) {
106
+ super({ initialValue: Optional.none });
107
+
108
+ this.#intervalMilliSeconds = intervalMilliSeconds;
109
+
110
+ this.#emitInitial = emitInitial;
111
+
112
+ this.#times = times;
113
+
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
+ }
63
124
 
64
- let mut_timerId0: TimerId | undefined = undefined;
125
+ #emit(value: SafeUint): void {
126
+ this.startUpdate(value);
65
127
 
66
- let mut_timerId: TimerId | undefined = undefined;
128
+ this.#mut_emittedCount += 1;
67
129
 
68
- let mut_isStarted = false;
130
+ if (this.#times !== undefined && this.#mut_emittedCount >= this.#times) {
131
+ this.complete();
132
+ }
133
+ }
69
134
 
70
- const resetTimer = (): void => {
71
- if (mut_timerId0 !== undefined) {
72
- clearTimeout(mut_timerId0);
135
+ start(): void {
136
+ if (this.#mut_isStarted) {
137
+ console.warn('cannot start twice');
138
+
139
+ return;
73
140
  }
74
141
 
75
- if (mut_timerId !== undefined) {
76
- clearInterval(mut_timerId);
142
+ this.#mut_isStarted = true;
143
+
144
+ if (this.isCompleted) {
145
+ console.warn('cannot restart stopped CounterObservable');
146
+
147
+ return;
77
148
  }
78
- };
79
-
80
- const observable = createRootObservable<
81
- SafeUint,
82
- Readonly<{ start: () => void }>
83
- >(
84
- { initialValue: Optional.none, onComplete: resetTimer },
85
- ({ startUpdate, isCompleted }) => ({
86
- start: () => {
87
- if (mut_isStarted) {
88
- console.warn('cannot start twice');
89
-
90
- return;
91
- }
92
-
93
- mut_isStarted = true;
94
-
95
- if (isCompleted()) {
96
- console.warn('cannot restart stopped CounterObservable');
97
-
98
- return;
99
- }
100
-
101
- if (options?.emitInitial ?? true) {
102
- mut_timerId0 = setTimeout(() => {
103
- startUpdate(mut_counter);
104
- }, 0);
105
- }
106
-
107
- mut_timerId = setInterval(() => {
108
- mut_counter = SafeUint.add(1, mut_counter);
109
-
110
- startUpdate(mut_counter);
111
- }, intervalMilliSeconds);
112
- },
113
- }),
114
- );
115
149
 
116
- if (!(options?.startManually ?? false)) {
117
- observable.start();
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);
118
161
  }
119
162
 
120
- return observable;
121
- };
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
+ }
@@ -1,4 +1,5 @@
1
1
  import { Arr } from 'ts-data-forge';
2
+ import { map } from '../operators/index.mjs';
2
3
  import { counter } from './counter.mjs';
3
4
 
4
5
  describe(counter, () => {
@@ -191,4 +192,85 @@ describe(counter, () => {
191
192
 
192
193
  vi.useRealTimers();
193
194
  });
195
+
196
+ test.each([
197
+ { emitInitial: true, expected: [0, 1, 2] },
198
+ { emitInitial: false, expected: [1, 2, 3] },
199
+ ] as const)(
200
+ 'times: 3, emitInitial: $emitInitial completes right after the third emission',
201
+ ({ emitInitial, expected }) => {
202
+ vi.useFakeTimers();
203
+
204
+ const counter$ = counter(100, { times: 3, emitInitial });
205
+
206
+ const mut_history: number[] = [];
207
+
208
+ counter$.subscribe((n) => {
209
+ mut_history.push(n);
210
+ });
211
+
212
+ vi.advanceTimersToNextTimer().advanceTimersToNextTimer();
213
+
214
+ assert.deepStrictEqual<readonly number[]>(
215
+ mut_history,
216
+ Arr.take(expected, 2),
217
+ );
218
+
219
+ assert.isFalse(counter$.isCompleted);
220
+
221
+ vi.advanceTimersToNextTimer();
222
+
223
+ assert.deepStrictEqual<readonly number[]>(mut_history, expected);
224
+
225
+ assert.isTrue(counter$.isCompleted);
226
+
227
+ assert.strictEqual(vi.getTimerCount(), 0);
228
+
229
+ vi.useRealTimers();
230
+ },
231
+ );
232
+
233
+ test('times: 1 emits only the 0 and clears the interval', () => {
234
+ vi.useFakeTimers();
235
+
236
+ const counter$ = counter(100, { times: 1 });
237
+
238
+ const mut_history: number[] = [];
239
+
240
+ counter$.subscribe((n) => {
241
+ mut_history.push(n);
242
+ });
243
+
244
+ vi.advanceTimersToNextTimer();
245
+
246
+ assert.deepStrictEqual(mut_history, [0]);
247
+
248
+ assert.isTrue(counter$.isCompleted);
249
+
250
+ assert.strictEqual(vi.getTimerCount(), 0);
251
+
252
+ vi.useRealTimers();
253
+ });
254
+
255
+ test('times completes the downstream after the last value reaches it', () => {
256
+ vi.useFakeTimers();
257
+
258
+ const counter$ = counter(100, { times: 2 });
259
+
260
+ const doubled$ = counter$.pipe(map((n) => n * 2));
261
+
262
+ const mut_history: number[] = [];
263
+
264
+ doubled$.subscribe((n) => {
265
+ mut_history.push(n);
266
+ });
267
+
268
+ vi.advanceTimersToNextTimer().advanceTimersToNextTimer();
269
+
270
+ assert.deepStrictEqual(mut_history, [0, 2]);
271
+
272
+ assert.isTrue(doubled$.isCompleted);
273
+
274
+ vi.useRealTimers();
275
+ });
194
276
  });
@@ -1,5 +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
7
+ // @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
1
8
  import { Optional, Result } from 'ts-data-forge';
2
- import { createRootObservable } from '../base/index.mjs';
9
+ import { RootObservableClass } from '../class/index.mjs';
3
10
  import type { FromPromiseObservable } from '../types/index.mjs';
4
11
 
5
12
  /**
@@ -72,50 +79,55 @@ import type { FromPromiseObservable } from '../types/index.mjs';
72
79
  export const fromAbortablePromise = <A, E = unknown>(
73
80
  // @sumi-expect-error readonly/require-readonly-parameter
74
81
  factory: (signal: AbortSignal) => Promise<A>,
75
- ): FromPromiseObservable<A, E> => {
76
- const abortController = new AbortController();
82
+ ): FromPromiseObservable<A, E> =>
83
+ new FromAbortablePromiseObservableClass(factory);
77
84
 
78
- return createRootObservable<Result<A, E>>(
79
- {
80
- initialValue: Optional.none,
81
- onComplete: () => {
82
- abortController.abort();
83
- },
84
- },
85
- ({ startUpdate, isCompleted, complete }) => {
86
- const promise = factory(abortController.signal);
85
+ class FromAbortablePromiseObservableClass<A, E = unknown>
86
+ extends RootObservableClass<Result<A, E>>
87
+ implements FromPromiseObservable<A, E>
88
+ {
89
+ readonly #abortController: AbortController;
87
90
 
88
- promise
89
- .then((value) => {
90
- if (isCompleted()) {
91
- return;
92
- }
91
+ // @sumi-expect-error readonly/require-readonly-parameter
92
+ constructor(factory: (signal: AbortSignal) => Promise<A>) {
93
+ super({ initialValue: Optional.none });
94
+
95
+ this.#abortController = new AbortController();
96
+
97
+ const promise = factory(this.#abortController.signal);
98
+
99
+ promise
100
+ .then((value) => {
101
+ if (this.isCompleted) {
102
+ return;
103
+ }
93
104
 
94
- startUpdate(Result.ok(value));
95
- })
96
- .catch((error: unknown) => {
97
- if (isCompleted()) {
98
- return;
99
- }
105
+ this.startUpdate(Result.ok(value));
106
+ })
107
+ .catch((error: unknown) => {
108
+ if (this.isCompleted) {
109
+ return;
110
+ }
100
111
 
101
- // Silently ignore AbortError — it means the observable was
102
- // intentionally completed (e.g., by switchMap).
103
- if (error instanceof DOMException && error.name === 'AbortError') {
104
- return;
105
- }
112
+ // Silently ignore AbortError — it means the observable was
113
+ // intentionally completed (e.g., by switchMap).
114
+ if (error instanceof DOMException && error.name === 'AbortError') {
115
+ return;
116
+ }
106
117
 
107
- startUpdate(
108
- Result.err(
109
- // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
110
- error as E,
111
- ),
112
- );
113
- })
114
- .finally(() => {
115
- complete();
116
- });
118
+ this.startUpdate(
119
+ Result.err(
120
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
121
+ error as E,
122
+ ),
123
+ );
124
+ })
125
+ .finally(() => {
126
+ this.complete();
127
+ });
128
+ }
117
129
 
118
- return {};
119
- },
120
- );
121
- };
130
+ protected override teardown(): void {
131
+ this.#abortController.abort();
132
+ }
133
+ }
@@ -1,5 +1,11 @@
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, Result } from 'ts-data-forge';
2
- import { createRootObservable } from '../base/index.mjs';
8
+ import { RootObservableClass } from '../class/index.mjs';
3
9
  import type { FromPromiseObservable } from '../types/index.mjs';
4
10
 
5
11
  /**
@@ -51,34 +57,37 @@ import type { FromPromiseObservable } from '../types/index.mjs';
51
57
  */
52
58
  export const fromPromise = <A, E = unknown>(
53
59
  promise: Promise<A>,
54
- ): FromPromiseObservable<A, E> =>
55
- createRootObservable<Result<A, E>>(
56
- { initialValue: Optional.none },
57
- ({ startUpdate, isCompleted, complete }) => {
58
- promise
59
- .then((value) => {
60
- if (isCompleted()) {
61
- return;
62
- }
60
+ ): FromPromiseObservable<A, E> => new FromPromiseObservableClass(promise);
63
61
 
64
- startUpdate(Result.ok(value));
65
- })
66
- .catch((error: unknown) => {
67
- if (isCompleted()) {
68
- return;
69
- }
62
+ class FromPromiseObservableClass<A, E = unknown>
63
+ extends RootObservableClass<Result<A, E>>
64
+ implements FromPromiseObservable<A, E>
65
+ {
66
+ constructor(promise: Promise<A>) {
67
+ super({ initialValue: Optional.none });
70
68
 
71
- startUpdate(
72
- Result.err(
73
- // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
74
- error as E,
75
- ),
76
- );
77
- })
78
- .finally(() => {
79
- complete();
80
- });
69
+ promise
70
+ .then((value) => {
71
+ if (this.isCompleted) {
72
+ return;
73
+ }
81
74
 
82
- return {};
83
- },
84
- );
75
+ this.startUpdate(Result.ok(value));
76
+ })
77
+ .catch((error: unknown) => {
78
+ if (this.isCompleted) {
79
+ return;
80
+ }
81
+
82
+ this.startUpdate(
83
+ Result.err(
84
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
85
+ error as E,
86
+ ),
87
+ );
88
+ })
89
+ .finally(() => {
90
+ this.complete();
91
+ });
92
+ }
93
+ }
@@ -1,8 +1,16 @@
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 { Optional, Result } from 'ts-data-forge';
2
- import { createRootObservable } from '../base/index.mjs';
9
+ import { RootObservableClass } from '../class/index.mjs';
3
10
  import type {
4
11
  FromSubscribableObservable,
5
12
  Subscribable,
13
+ Subscription,
6
14
  } from '../types/index.mjs';
7
15
 
8
16
  /**
@@ -66,26 +74,63 @@ import type {
66
74
  export const fromSubscribable = <A, E = unknown>(
67
75
  subscribable: Subscribable<A>,
68
76
  ): FromSubscribableObservable<A, E> =>
69
- createRootObservable<Result<A, E>>(
70
- { initialValue: Optional.none },
71
- ({ startUpdate, complete }) => {
72
- subscribable.subscribe(
73
- (nextValue) => {
74
- startUpdate(Result.ok(nextValue));
75
- },
76
- (error?: unknown) => {
77
- startUpdate(
78
- Result.err(
79
- // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
80
- error as E,
81
- ),
82
- );
83
- },
84
- () => {
85
- complete();
86
- },
87
- );
77
+ new FromSubscribableObservableClass(subscribable);
88
78
 
89
- return {};
90
- },
91
- );
79
+ class FromSubscribableObservableClass<A, E = unknown>
80
+ extends RootObservableClass<Result<A, E>>
81
+ implements FromSubscribableObservable<A, E>
82
+ {
83
+ readonly #mut_subscription: Subscription | undefined;
84
+
85
+ /**
86
+ * Set once the subscribable has completed: its subscription is over, so
87
+ * completing this observable must not unsubscribe from it again.
88
+ */
89
+ #mut_isSourceCompleted: boolean;
90
+
91
+ constructor(subscribable: Subscribable<A>) {
92
+ super({ initialValue: Optional.none });
93
+
94
+ this.#mut_subscription = undefined;
95
+
96
+ this.#mut_isSourceCompleted = false;
97
+
98
+ const subscription = subscribable.subscribe(
99
+ (nextValue) => {
100
+ this.startUpdate(Result.ok(nextValue));
101
+ },
102
+ (error?: unknown) => {
103
+ this.startUpdate(
104
+ Result.err(
105
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
106
+ error as E,
107
+ ),
108
+ );
109
+ },
110
+ () => {
111
+ this.#mut_isSourceCompleted = true;
112
+
113
+ this.complete();
114
+ },
115
+ );
116
+
117
+ // `subscribe` may have completed this observable already.
118
+ if (this.isCompleted) {
119
+ this.#unsubscribe(subscription);
120
+ } else {
121
+ this.#mut_subscription = subscription;
122
+ }
123
+ }
124
+
125
+ #unsubscribe(subscription: Subscription): void {
126
+ if (!this.#mut_isSourceCompleted) {
127
+ subscription.unsubscribe();
128
+ }
129
+ }
130
+
131
+ protected override teardown(): void {
132
+ if (this.#mut_subscription !== undefined) {
133
+ this.#unsubscribe(this.#mut_subscription);
134
+ }
135
+ }
136
+ }