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
@@ -0,0 +1,264 @@
1
+ import { Optional } from 'ts-data-forge';
2
+ import { createState } from '../../utils/index.mjs';
3
+ import { combine } from '../combine/index.mjs';
4
+ import { counter, source } from '../create/index.mjs';
5
+ import { debounce, map } from '../operators/index.mjs';
6
+ import { toNode } from './observable-node.mjs';
7
+
8
+ describe('completing a derived observable', () => {
9
+ test('does not complete an upstream state (#2133)', () => {
10
+ const [count$, setCount] = createState(0);
11
+
12
+ const doubled$ = count$.pipe(map((x) => x * 2));
13
+
14
+ doubled$.complete();
15
+
16
+ assert.isFalse(count$.isCompleted);
17
+
18
+ setCount(5);
19
+
20
+ assert.deepStrictEqual(count$.getSnapshot(), Optional.some(5));
21
+
22
+ const mut_got: number[] = [];
23
+
24
+ count$.subscribe((v) => {
25
+ mut_got.push(v);
26
+ });
27
+
28
+ setCount(6);
29
+
30
+ assert.deepStrictEqual(mut_got, [5, 6]);
31
+ });
32
+
33
+ test('does not complete a shared derived observable', () => {
34
+ const s = source<number>(0);
35
+
36
+ const shared$ = s.pipe(map((x) => x + 1));
37
+
38
+ const local$ = shared$.pipe(map((x) => x * 2));
39
+
40
+ local$.complete();
41
+
42
+ assert.isFalse(shared$.isCompleted);
43
+
44
+ assert.isFalse(s.isCompleted);
45
+
46
+ const mut_got: number[] = [];
47
+
48
+ shared$.subscribe((v) => {
49
+ mut_got.push(v);
50
+ });
51
+
52
+ s.next(1);
53
+
54
+ assert.deepStrictEqual(mut_got, [1, 2]);
55
+ });
56
+
57
+ test('does not complete an unused root with a teardown', () => {
58
+ const counter$ = counter(10, { startManually: true });
59
+
60
+ const derived$ = counter$.pipe(map((x) => x * 2));
61
+
62
+ const leaf$ = derived$.pipe(map((x) => x + 1));
63
+
64
+ leaf$.complete();
65
+
66
+ assert.isFalse(derived$.isCompleted);
67
+
68
+ assert.isFalse(counter$.isCompleted);
69
+
70
+ counter$.complete();
71
+
72
+ assert.isTrue(derived$.isCompleted);
73
+ });
74
+
75
+ test('does not complete an unused operator with a teardown', () => {
76
+ const s = source<number>(0);
77
+
78
+ const debounced$ = s.pipe(debounce(10));
79
+
80
+ const leaf$ = debounced$.pipe(map((x) => x));
81
+
82
+ leaf$.complete();
83
+
84
+ assert.isFalse(debounced$.isCompleted);
85
+
86
+ assert.isFalse(s.isCompleted);
87
+
88
+ debounced$.complete();
89
+ });
90
+
91
+ test('does not stop a root with a teardown that is still used', () => {
92
+ const counter$ = counter(10, { startManually: true });
93
+
94
+ counter$.subscribe(() => {});
95
+
96
+ counter$.pipe(map((x) => x)).complete();
97
+
98
+ assert.isFalse(counter$.isCompleted);
99
+
100
+ counter$.complete();
101
+ });
102
+ });
103
+
104
+ describe('dispose', () => {
105
+ test('completes the observable without completing its parents', () => {
106
+ const counter$ = counter(10, { startManually: true });
107
+
108
+ const derived$ = counter$.pipe(map((x) => x * 2));
109
+
110
+ derived$.dispose();
111
+
112
+ assert.isTrue(derived$.isCompleted);
113
+
114
+ assert.isFalse(counter$.isCompleted);
115
+
116
+ counter$.complete();
117
+ });
118
+
119
+ test('takes the observable out of the propagation graph', () => {
120
+ const s = source<number>(0);
121
+
122
+ s.subscribe(() => {});
123
+
124
+ let mut_calls = 0;
125
+
126
+ const derived$ = s.pipe(
127
+ map((x) => {
128
+ mut_calls += 1;
129
+
130
+ return x;
131
+ }),
132
+ );
133
+
134
+ derived$.dispose();
135
+
136
+ assert.isFalse(toNode(s).hasChild);
137
+
138
+ mut_calls = 0;
139
+
140
+ s.next(1);
141
+
142
+ expect(mut_calls).toBe(0);
143
+ });
144
+
145
+ test('completes the descendants that have no other live parent', () => {
146
+ const s = source<number>(0);
147
+
148
+ const t = source<number>(0);
149
+
150
+ const derived$ = s.pipe(map((x) => x));
151
+
152
+ const onlyChild$ = derived$.pipe(map((x) => x));
153
+
154
+ const s2 = t.pipe(map((x) => x));
155
+
156
+ const mut_got: string[] = [];
157
+
158
+ onlyChild$.subscribe(
159
+ () => {},
160
+ () => {
161
+ mut_got.push('onlyChild completed');
162
+ },
163
+ );
164
+
165
+ derived$.dispose();
166
+
167
+ assert.isTrue(onlyChild$.isCompleted);
168
+
169
+ assert.isFalse(s2.isCompleted);
170
+
171
+ assert.deepStrictEqual(mut_got, ['onlyChild completed']);
172
+ });
173
+
174
+ test('runs the teardown of the observable it is called on', async () => {
175
+ const s = source<number>(0);
176
+
177
+ const debounced$ = s.pipe(debounce(10));
178
+
179
+ const mut_got: number[] = [];
180
+
181
+ debounced$.subscribe((v) => {
182
+ mut_got.push(v);
183
+ });
184
+
185
+ s.next(1);
186
+
187
+ debounced$.dispose();
188
+
189
+ await new Promise((resolve) => {
190
+ setTimeout(resolve, 50);
191
+ });
192
+
193
+ // Only the initial value: the pending 1 was cancelled.
194
+ assert.deepStrictEqual(mut_got, [0]);
195
+ });
196
+ });
197
+
198
+ describe('a child created under completed parents (#2248)', () => {
199
+ test('completes at creation and keeps the value computed from the final parent value', () => {
200
+ const s = source<number>(0);
201
+
202
+ s.next(5);
203
+
204
+ s.complete();
205
+
206
+ const d = s.pipe(map((x) => x * 100));
207
+
208
+ assert.isTrue(d.isCompleted);
209
+
210
+ assert.deepStrictEqual(d.getSnapshot(), Optional.some(500));
211
+
212
+ const mut_got: string[] = [];
213
+
214
+ d.subscribe(
215
+ (v) => {
216
+ mut_got.push(`next ${v}`);
217
+ },
218
+ () => {
219
+ mut_got.push('complete');
220
+ },
221
+ );
222
+
223
+ // A completed observable emits its final value, then completes.
224
+ assert.deepStrictEqual(mut_got, ['next 500', 'complete']);
225
+ });
226
+
227
+ test('is not registered to the completed parent', () => {
228
+ const s = source<number>(0);
229
+
230
+ s.complete();
231
+
232
+ s.pipe(map((x) => x + 1));
233
+
234
+ assert.isFalse(toNode(s).hasChild);
235
+ });
236
+
237
+ test('completes an async child at creation', () => {
238
+ const s = source<number>(0);
239
+
240
+ s.complete();
241
+
242
+ const d = s.pipe(debounce(10));
243
+
244
+ assert.isTrue(d.isCompleted);
245
+
246
+ assert.isFalse(toNode(s).hasChild);
247
+ });
248
+
249
+ test('does not complete a child that still has a live parent', () => {
250
+ const live$ = source<number>(1);
251
+
252
+ const completed$ = source<number>(2);
253
+
254
+ completed$.complete();
255
+
256
+ const sum$ = combine([live$, completed$]).pipe(map(([a, b]) => a + b));
257
+
258
+ assert.isFalse(sum$.isCompleted);
259
+
260
+ live$.next(10);
261
+
262
+ assert.deepStrictEqual(sum$.getSnapshot(), Optional.some(12));
263
+ });
264
+ });
@@ -0,0 +1,370 @@
1
+ // @sumi-expect-error-file classes/no-class
2
+ // @sumi-expect-error-file banned-syntax/no-this
3
+ // @sumi-expect-error-file functions/prefer-arrow-function
4
+ // @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
5
+ import { Arr, type Optional, type Some } from 'ts-data-forge';
6
+ import { unreachable } from 'ts-std-forge';
7
+ import type {
8
+ AsyncChildObservable,
9
+ InitializedSyncChildObservable,
10
+ NonEmptyUnknownList,
11
+ SyncChildObservable,
12
+ UpdateToken,
13
+ Wrap,
14
+ } from '../types/index.mjs';
15
+ import {
16
+ AsyncChildObservableClass,
17
+ SyncChildObservableClass,
18
+ } from './child-observable-class.mjs';
19
+
20
+ /**
21
+ * The tools a sync-child factory (`map`, `filter`, `zip`, …) receives: what
22
+ * only the observable being created may do to itself.
23
+ */
24
+ export type SyncChildObservableTools<A> = Readonly<{
25
+ /** Emits `nextValue` under `updateToken` and notifies subscribers. */
26
+ setNext: (nextValue: A, updateToken: UpdateToken) => void;
27
+
28
+ getSnapshot: () => Optional<A>;
29
+
30
+ /** Completes this observable (leaf teardown included). */
31
+ complete: () => void;
32
+
33
+ /**
34
+ * Completes this observable once the update now propagating has reached
35
+ * every descendant, so that they see the value just passed on first.
36
+ */
37
+ completeAfterPropagation: () => void;
38
+
39
+ /**
40
+ * 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.
45
+ */
46
+ onComplete: (teardown: () => void) => void;
47
+ }>;
48
+
49
+ /**
50
+ * The tools an async-child factory (`debounce`, `switchMap`, …) receives.
51
+ * Async children additionally start their own update propagation.
52
+ */
53
+ export type AsyncChildObservableTools<A> = Readonly<{
54
+ /** Emits `nextValue` and propagates the update to all descendants. */
55
+ startUpdate: (nextValue: A) => void;
56
+ }> &
57
+ SyncChildObservableTools<A>;
58
+
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.
62
+ */
63
+ export type SyncChildObservableHooks = Readonly<{
64
+ /** Called with the token of every update that reaches this observable. */
65
+ tryUpdate: (updateToken: UpdateToken) => void;
66
+
67
+ /**
68
+ * Whether this observable can emit nothing more although some parent is
69
+ * still live, asked whenever a parent completes. `true` completes it then,
70
+ * as all parents completing does (`zip` once a completed source's queue is
71
+ * empty).
72
+ */
73
+ canEmitNoMore?: () => boolean;
74
+ }>;
75
+
76
+ /**
77
+ * What an async child's `init` may return instead of `tryUpdate` alone. An
78
+ * async child can also hold a value it has not passed on yet (`debounce`'s
79
+ * pending value), which completing should not lose.
80
+ */
81
+ export type AsyncChildObservableHooks = SyncChildObservableHooks &
82
+ Readonly<{
83
+ /**
84
+ * Run at the beginning of `complete()`, before the teardowns, and never
85
+ * by `dispose()`: passes on what this observable still holds through
86
+ * `startUpdate`, so that its subscribers see it before they see the
87
+ * completion. A disposed observable has no one left to tell.
88
+ */
89
+ flush?: () => void;
90
+ }>;
91
+
92
+ type ChildObservableConfigBase<P extends NonEmptyUnknownList> = Readonly<{
93
+ parents: Wrap<P>;
94
+
95
+ /**
96
+ * Defaults to one more than the deepest parent. An update reaches
97
+ * observables in ascending depth, so an observable `tryUpdate` reads with
98
+ * `getSnapshot` without being its child (`withCurrentValueFrom`'s second
99
+ * source) has to be shallower than this one too, or this one reads its value
100
+ * from before the update: pass `1 + Math.max(...)` over both then.
101
+ */
102
+ depth?: number;
103
+ }>;
104
+
105
+ export type ChildObservableConfig<
106
+ A,
107
+ P extends NonEmptyUnknownList,
108
+ > = ChildObservableConfigBase<P> &
109
+ Readonly<{
110
+ initialValue: Optional<A>;
111
+ }>;
112
+
113
+ export type InitializedChildObservableConfig<
114
+ A,
115
+ P extends NonEmptyUnknownList,
116
+ > = ChildObservableConfigBase<P> &
117
+ Readonly<{
118
+ initialValue: Some<A>;
119
+ }>;
120
+
121
+ /**
122
+ * Creates a sync-child observable: one derived from `parents` that emits, if
123
+ * at all, in the same update as they do. `map`, `filter`, `combine` and the
124
+ * other synchronous operators are built with it.
125
+ *
126
+ * `init` runs during construction and receives the tools. It returns
127
+ * `tryUpdate`, which is called with the token of every update that reaches
128
+ * this observable — a parent that did not emit in that update has a different
129
+ * `updateToken`; to emit, call `setNext` with the token received — or an
130
+ * object holding `tryUpdate` and the other hooks
131
+ * ({@link SyncChildObservableHooks}). State the hooks share can live inside
132
+ * `init`, next to the teardown it registers with the `onComplete` tool.
133
+ * `init` may complete the observable; it is then never registered to its
134
+ * parents.
135
+ *
136
+ * @example
137
+ *
138
+ * ```ts
139
+ * // A custom operator, written as `map((x) => x * 2)` is.
140
+ * const double = (src: Observable<number>): Observable<number> =>
141
+ * createSyncChildObservable(
142
+ * { parents: [src], initialValue: Optional.none },
143
+ * ({ setNext }) =>
144
+ * (updateToken) => {
145
+ * const sn = src.getSnapshot();
146
+ *
147
+ * if (src.updateToken !== updateToken || Optional.isNone(sn)) {
148
+ * return; // `src` did not emit in this update
149
+ * }
150
+ *
151
+ * setNext(sn.value * 2, updateToken);
152
+ * },
153
+ * );
154
+ *
155
+ * const num$ = source<number>();
156
+ *
157
+ * const pair$ = combine([num$, double(num$)]);
158
+ *
159
+ * const valueHistory: FixedLengthTuple<2, number>[] = [];
160
+ *
161
+ * pair$.subscribe((pair) => {
162
+ * valueHistory.push(pair);
163
+ * });
164
+ *
165
+ * num$.next(1);
166
+ *
167
+ * num$.next(2);
168
+ *
169
+ * // `double` joins the same update as `num$`, so `combine` sees no [2, 2].
170
+ * assert.deepStrictEqual(valueHistory, [
171
+ * [1, 2],
172
+ * [2, 4],
173
+ * ]);
174
+ * ```
175
+ */
176
+ export const createSyncChildObservable = <A, P extends NonEmptyUnknownList>(
177
+ config: ChildObservableConfig<A, P>,
178
+ init: (
179
+ tools: SyncChildObservableTools<A>,
180
+ ) => ((updateToken: UpdateToken) => void) | SyncChildObservableHooks,
181
+ ): SyncChildObservable<A, P> =>
182
+ new CustomSyncChildObservableClass(config, init);
183
+
184
+ /**
185
+ * Creates a sync-child observable that always holds a value.
186
+ *
187
+ * Runtime-wise this is exactly `createSyncChildObservable`; only the types of
188
+ * `getSnapshot` and `pipe` are narrowed.
189
+ */
190
+ export const createInitializedSyncChildObservable = <
191
+ A,
192
+ P extends NonEmptyUnknownList,
193
+ >(
194
+ config: InitializedChildObservableConfig<A, P>,
195
+ init: (
196
+ tools: SyncChildObservableTools<A>,
197
+ ) => ((updateToken: UpdateToken) => void) | SyncChildObservableHooks,
198
+ ): InitializedSyncChildObservable<A, P> =>
199
+ // `getSnapshot` always returns `Some` here: `initialValue` is a `Some` and
200
+ // `setNext` only ever stores a `Some`.
201
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
202
+ new CustomSyncChildObservableClass<A, P>(
203
+ config,
204
+ init,
205
+ ) as unknown as InitializedSyncChildObservable<A, P>;
206
+
207
+ /**
208
+ * Creates an async-child observable: one derived from `parents` that may emit
209
+ * later, in an update of its own, through `startUpdate` (`debounce`,
210
+ * `switchMap`). It may also emit synchronously with `setNext`, as a sync child
211
+ * does. See {@link createSyncChildObservable} for the `init` contract; an
212
+ * async child's `init` may also return a `flush` hook
213
+ * ({@link AsyncChildObservableHooks}).
214
+ */
215
+ export const createAsyncChildObservable = <A, P extends NonEmptyUnknownList>(
216
+ config: ChildObservableConfig<A, P>,
217
+ init: (
218
+ tools: AsyncChildObservableTools<A>,
219
+ ) => ((updateToken: UpdateToken) => void) | AsyncChildObservableHooks,
220
+ ): AsyncChildObservable<A, P> =>
221
+ new CustomAsyncChildObservableClass(config, init);
222
+
223
+ /**
224
+ * An `init` for a child that never receives a parent update; reaching its
225
+ * `tryUpdate` is the invariant breaking (Sumi D-48).
226
+ */
227
+ export const createTryUpdateNotImplemented = (): ((
228
+ updateToken: UpdateToken,
229
+ ) => void) => tryUpdateNotImplemented;
230
+
231
+ const tryUpdateNotImplemented = (): void => {
232
+ unreachable();
233
+ };
234
+
235
+ /* implementation */
236
+
237
+ class CustomSyncChildObservableClass<
238
+ A,
239
+ const P extends NonEmptyUnknownList,
240
+ > extends SyncChildObservableClass<A, P> {
241
+ #mut_teardowns: readonly (() => void)[] | undefined;
242
+ readonly #hooks: SyncChildObservableHooks;
243
+
244
+ constructor(
245
+ config: ChildObservableConfig<A, P>,
246
+ init: (
247
+ tools: SyncChildObservableTools<A>,
248
+ ) => ((updateToken: UpdateToken) => void) | SyncChildObservableHooks,
249
+ ) {
250
+ super(config);
251
+
252
+ this.#mut_teardowns = undefined;
253
+
254
+ this.#hooks = toHooks(
255
+ init({
256
+ setNext: (nextValue, updateToken) => {
257
+ this.setNext(nextValue, updateToken);
258
+ },
259
+ getSnapshot: () => this.getSnapshot(),
260
+ complete: () => {
261
+ this.complete();
262
+ },
263
+ completeAfterPropagation: () => {
264
+ this.completeAfterPropagation();
265
+ },
266
+ onComplete: (teardown) => {
267
+ if (this.isCompleted) {
268
+ teardown();
269
+ } else {
270
+ this.#mut_teardowns = Arr.toPushed(
271
+ this.#mut_teardowns ?? [],
272
+ teardown,
273
+ );
274
+ }
275
+ },
276
+ }),
277
+ );
278
+
279
+ this.attach();
280
+ }
281
+
282
+ override tryUpdate(updateToken: UpdateToken): void {
283
+ this.#hooks.tryUpdate(updateToken);
284
+ }
285
+
286
+ override canEmitNoMore(): boolean {
287
+ return this.#hooks.canEmitNoMore?.() === true;
288
+ }
289
+
290
+ protected override teardown(): void {
291
+ for (const teardown of this.#mut_teardowns ?? []) {
292
+ teardown();
293
+ }
294
+ }
295
+ }
296
+
297
+ class CustomAsyncChildObservableClass<
298
+ A,
299
+ const P extends NonEmptyUnknownList,
300
+ > extends AsyncChildObservableClass<A, P> {
301
+ #mut_teardowns: readonly (() => void)[] | undefined;
302
+ readonly #hooks: AsyncChildObservableHooks;
303
+
304
+ constructor(
305
+ config: ChildObservableConfig<A, P>,
306
+ init: (
307
+ tools: AsyncChildObservableTools<A>,
308
+ ) => ((updateToken: UpdateToken) => void) | AsyncChildObservableHooks,
309
+ ) {
310
+ super(config);
311
+
312
+ this.#mut_teardowns = undefined;
313
+
314
+ this.#hooks = toHooks(
315
+ init({
316
+ setNext: (nextValue, updateToken) => {
317
+ this.setNext(nextValue, updateToken);
318
+ },
319
+ startUpdate: (nextValue) => {
320
+ this.startUpdate(nextValue);
321
+ },
322
+ getSnapshot: () => this.getSnapshot(),
323
+ complete: () => {
324
+ this.complete();
325
+ },
326
+ completeAfterPropagation: () => {
327
+ this.completeAfterPropagation();
328
+ },
329
+ onComplete: (teardown) => {
330
+ if (this.isCompleted) {
331
+ teardown();
332
+ } else {
333
+ this.#mut_teardowns = Arr.toPushed(
334
+ this.#mut_teardowns ?? [],
335
+ teardown,
336
+ );
337
+ }
338
+ },
339
+ }),
340
+ );
341
+
342
+ this.attach();
343
+ }
344
+
345
+ override tryUpdate(updateToken: UpdateToken): void {
346
+ this.#hooks.tryUpdate(updateToken);
347
+ }
348
+
349
+ override canEmitNoMore(): boolean {
350
+ return this.#hooks.canEmitNoMore?.() === true;
351
+ }
352
+
353
+ protected override flush(): void {
354
+ this.#hooks.flush?.();
355
+ }
356
+
357
+ protected override teardown(): void {
358
+ for (const teardown of this.#mut_teardowns ?? []) {
359
+ teardown();
360
+ }
361
+ }
362
+ }
363
+
364
+ /** `init` may return `tryUpdate` alone, or it with the other hooks. */
365
+ const toHooks = (
366
+ returned: ((updateToken: UpdateToken) => void) | AsyncChildObservableHooks,
367
+ ): AsyncChildObservableHooks =>
368
+ typeof returned === 'function'
369
+ ? ({ tryUpdate: returned } as const)
370
+ : returned;