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,92 @@
1
+ import { createState } from '../../utils/index.mjs';
2
+ import { combine } from '../combine/index.mjs';
3
+ import { map } from '../operators/index.mjs';
4
+
5
+ describe('the nodes an update visits', () => {
6
+ test('a node created while an update propagates is not visited by it', () => {
7
+ const [count$, setCount] = createState(0);
8
+
9
+ let mut_calls = 0;
10
+
11
+ const countCall = (x: number): number => {
12
+ mut_calls += 1;
13
+
14
+ return x;
15
+ };
16
+
17
+ let mut_created = false;
18
+
19
+ count$.subscribe((x) => {
20
+ if (x !== 1 || mut_created) {
21
+ return;
22
+ }
23
+
24
+ mut_created = true;
25
+
26
+ count$.pipe(map(countCall)).subscribe(() => {});
27
+ });
28
+
29
+ count$.pipe(map((x) => x)).subscribe(() => {});
30
+
31
+ setCount(1);
32
+
33
+ // Once for the initial value; the update that created it does not run it
34
+ // again.
35
+ expect(mut_calls).toBe(1);
36
+
37
+ setCount(2);
38
+
39
+ expect(mut_calls).toBe(2);
40
+ });
41
+
42
+ test('a node completed while an update propagates is not visited by it', () => {
43
+ const [count$, setCount] = createState(0);
44
+
45
+ let mut_calls = 0;
46
+
47
+ const late$ = count$.pipe(map((x) => x + 1)).pipe(
48
+ map((x) => {
49
+ mut_calls += 1;
50
+
51
+ return x;
52
+ }),
53
+ );
54
+
55
+ late$.subscribe(() => {});
56
+
57
+ count$.subscribe((x) => {
58
+ if (x === 1) {
59
+ late$.complete();
60
+ }
61
+ });
62
+
63
+ mut_calls = 0;
64
+
65
+ setCount(1);
66
+
67
+ expect(mut_calls).toBe(0);
68
+ });
69
+
70
+ test('a manager whose descendants skip depths updates them in order', () => {
71
+ const [a$, setA] = createState(0);
72
+
73
+ const [b$] = createState(10);
74
+
75
+ // `c$` is at depth 3 and the only descendant of `a$`.
76
+ const c$ = combine([a$, b$.pipe(map((x) => x)).pipe(map((x) => x))]);
77
+
78
+ const d$ = c$.pipe(map(([x, y]) => x + y));
79
+
80
+ const mut_values: number[] = [];
81
+
82
+ d$.subscribe((v) => {
83
+ mut_values.push(v);
84
+ });
85
+
86
+ setA(1);
87
+
88
+ setA(2);
89
+
90
+ assert.deepStrictEqual(mut_values, [10, 11, 12]);
91
+ });
92
+ });
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The state of the update propagating now, shared by every observable.
3
+ *
4
+ * It is module state rather than a field of a class, so that nothing outside
5
+ * this file can reach it but through the functions below: an exported class
6
+ * would hand its static fields to every importer, and a private static field
7
+ * costs about twice as much per access as a module binding, on a path every
8
+ * update takes.
9
+ */
10
+ import { Arr } from 'ts-data-forge';
11
+ import type { UpdateToken } from '../types/index.mjs';
12
+
13
+ /**
14
+ * How many `startUpdate` calls are running, across every observable, and
15
+ * the token of the outermost one.
16
+ */
17
+ const mut_propagation: {
18
+ depth: number;
19
+ outermostToken: UpdateToken | undefined;
20
+ } = { depth: 0, outermostToken: undefined };
21
+
22
+ /**
23
+ * Work deferred by {@link runAfterPropagation}, run once the outermost
24
+ * `startUpdate` has reached every descendant.
25
+ */
26
+ const mut_afterPropagation: (() => void)[] = [];
27
+
28
+ /**
29
+ * Whether some observable's update is propagating right now. A root written
30
+ * to while one is, is written to from inside a subscriber or a projection
31
+ * (#2336).
32
+ */
33
+ export const isPropagating = (): boolean => mut_propagation.depth > 0;
34
+
35
+ /** The token of the outermost update propagating now, if any. */
36
+ export const outermostUpdateToken = (): UpdateToken | undefined =>
37
+ mut_propagation.outermostToken;
38
+
39
+ /**
40
+ * Runs `task` once the update now propagating has reached every descendant, or
41
+ * at once when none is. A child that completes itself in its `tryUpdate`
42
+ * completes its children with it, before they have seen the value it has just
43
+ * passed on (`take`).
44
+ */
45
+ export const runAfterPropagation = (task: () => void): void => {
46
+ if (isPropagating()) {
47
+ mut_afterPropagation.push(task);
48
+ } else {
49
+ task();
50
+ }
51
+ };
52
+
53
+ /** Brackets one `startUpdate`; see {@link endPropagation}. */
54
+ export const beginPropagation = (updateToken: UpdateToken): void => {
55
+ if (mut_propagation.depth === 0) {
56
+ mut_propagation.outermostToken = updateToken;
57
+ }
58
+
59
+ mut_propagation.depth += 1;
60
+ };
61
+
62
+ /**
63
+ * Ends one `startUpdate`, and runs the deferred work once the outermost one
64
+ * has ended.
65
+ */
66
+ export const endPropagation = (): void => {
67
+ mut_propagation.depth -= 1;
68
+
69
+ if (mut_propagation.depth > 0) {
70
+ return;
71
+ }
72
+
73
+ mut_propagation.outermostToken = undefined;
74
+
75
+ // A deferred task may start an update of its own, which flushes what that
76
+ // update defers before this loop moves on.
77
+ while (Arr.isNonEmpty(mut_afterPropagation)) {
78
+ mut_afterPropagation.shift()?.();
79
+ }
80
+ };
@@ -0,0 +1,369 @@
1
+ import { Optional } from 'ts-data-forge';
2
+ import type { FixedLengthTuple } from 'ts-type-forge';
3
+ import { createState } from '../../utils/index.mjs';
4
+ import { combine } from '../combine/index.mjs';
5
+ import { source } from '../create/index.mjs';
6
+ import { map, switchMap, takeWhile } from '../operators/index.mjs';
7
+ import { toNode } from './observable-node.mjs';
8
+
9
+ describe('a completed observable leaves the propagation graph', () => {
10
+ test('completed derived observables stop computing (#2134)', () => {
11
+ const [count$, setCount] = createState(0);
12
+
13
+ // A subscriber, so that the root has a reason to stay alive whatever the
14
+ // completion rules for an unused root are.
15
+ count$.subscribe(() => {});
16
+
17
+ let mut_calls = 0;
18
+
19
+ const countCall = (x: number): number => {
20
+ mut_calls += 1;
21
+
22
+ return x;
23
+ };
24
+
25
+ for (const _ of Array.from({ length: 1000 })) {
26
+ const d = count$.pipe(map(countCall));
27
+
28
+ d.complete();
29
+
30
+ assert.isTrue(d.isCompleted);
31
+ }
32
+
33
+ mut_calls = 0;
34
+
35
+ setCount(1);
36
+
37
+ expect(mut_calls).toBe(0);
38
+ });
39
+
40
+ test('a completed descendant several levels down stops computing', () => {
41
+ const [count$, setCount] = createState(0);
42
+
43
+ count$.subscribe(() => {});
44
+
45
+ let mut_calls = 0;
46
+
47
+ const d1 = count$.pipe(map((x) => x + 1));
48
+
49
+ const d2 = d1.pipe(
50
+ map((x) => {
51
+ mut_calls += 1;
52
+
53
+ return x;
54
+ }),
55
+ );
56
+
57
+ d1.subscribe(() => {});
58
+
59
+ d2.complete();
60
+
61
+ mut_calls = 0;
62
+
63
+ setCount(1);
64
+
65
+ expect(mut_calls).toBe(0);
66
+
67
+ assert.deepStrictEqual(d1.getSnapshot(), Optional.some(2));
68
+ });
69
+
70
+ test('completing a node in the middle of a chain removes everything below it', () => {
71
+ const s = source<number>(0);
72
+
73
+ s.subscribe(() => {});
74
+
75
+ const middle = s.pipe(map((x) => x + 1));
76
+
77
+ let mut_calls = 0;
78
+
79
+ const leaf = middle.pipe(
80
+ map((x) => {
81
+ mut_calls += 1;
82
+
83
+ return x;
84
+ }),
85
+ );
86
+
87
+ const mut_got: number[] = [];
88
+
89
+ leaf.subscribe((v) => {
90
+ mut_got.push(v);
91
+ });
92
+
93
+ middle.complete();
94
+
95
+ assert.isTrue(leaf.isCompleted);
96
+
97
+ assert.isFalse(toNode(s).hasChild);
98
+
99
+ mut_calls = 0;
100
+
101
+ s.next(1);
102
+
103
+ // The leaf is not reached through the completed node, nor directly.
104
+ expect(mut_calls).toBe(0);
105
+
106
+ assert.deepStrictEqual(mut_got, [1]);
107
+ });
108
+
109
+ test('completing the root of a long chain takes time linear in its length', () => {
110
+ const s = source<number>(0);
111
+
112
+ let mut_tail = s.pipe(map((x) => x));
113
+
114
+ for (const _ of Array.from({ length: 499 })) {
115
+ mut_tail = mut_tail.pipe(map((x) => x));
116
+ }
117
+
118
+ const leaf = mut_tail;
119
+
120
+ leaf.subscribe(() => {});
121
+
122
+ const start = performance.now();
123
+
124
+ s.complete();
125
+
126
+ const elapsed = performance.now() - start;
127
+
128
+ assert.isTrue(leaf.isCompleted);
129
+
130
+ // Each node used to re-run the completion of every ancestor, each run
131
+ // walking the ancestors again: cubic in the length, over 2 s here.
132
+ expect(elapsed).toBeLessThan(1000);
133
+ });
134
+
135
+ test('the parent no longer reports the completed child', () => {
136
+ const s = source<number>(0);
137
+
138
+ s.subscribe(() => {});
139
+
140
+ const d = s.pipe(map((x) => x));
141
+
142
+ assert.isTrue(toNode(s).hasChild);
143
+
144
+ d.complete();
145
+
146
+ assert.isFalse(toNode(s).hasChild);
147
+
148
+ assert.isFalse(toNode(s).hasActiveChild());
149
+ });
150
+
151
+ test('a node with another live parent keeps updating', () => {
152
+ const a = source<number>(0);
153
+
154
+ const b = source<number>(0);
155
+
156
+ const a2 = a.pipe(map((x) => x * 10));
157
+
158
+ const c = combine([a2, b]);
159
+
160
+ const mut_got: FixedLengthTuple<2, number>[] = [];
161
+
162
+ c.subscribe((v) => {
163
+ mut_got.push(v);
164
+ });
165
+
166
+ b.complete();
167
+
168
+ a.next(1);
169
+
170
+ assert.deepStrictEqual(mut_got, [
171
+ [0, 0],
172
+ [10, 0],
173
+ ]);
174
+ });
175
+
176
+ test('a sibling completing during propagation does not disturb the others', () => {
177
+ const s = source<number>(0);
178
+
179
+ const stopper = s.pipe(takeWhile((x) => x < 1));
180
+
181
+ stopper.subscribe(() => {});
182
+
183
+ const mut_got: number[] = [];
184
+
185
+ s.pipe(map((x) => x * 2)).subscribe((v) => {
186
+ mut_got.push(v);
187
+ });
188
+
189
+ s.next(1);
190
+
191
+ s.next(2);
192
+
193
+ assert.isTrue(stopper.isCompleted);
194
+
195
+ assert.deepStrictEqual(mut_got, [0, 2, 4]);
196
+ });
197
+
198
+ test('a node created during propagation is not updated by that same update', () => {
199
+ const s = source<number>(0);
200
+
201
+ let mut_calls = 0;
202
+
203
+ const mut_got: number[] = [];
204
+
205
+ // Subscribed on a child, so the node is created while `s` is already
206
+ // walking its propagation order.
207
+ s.pipe(map((x) => x)).subscribe((x) => {
208
+ if (x === 1) {
209
+ s.pipe(
210
+ map((y) => {
211
+ mut_calls += 1;
212
+
213
+ return y;
214
+ }),
215
+ ).subscribe((y) => {
216
+ mut_got.push(y);
217
+ });
218
+ }
219
+ });
220
+
221
+ s.next(1);
222
+
223
+ // Only the initial value computed when the node was created.
224
+ expect(mut_calls).toBe(1);
225
+
226
+ assert.deepStrictEqual(mut_got, [1]);
227
+
228
+ s.next(2);
229
+
230
+ assert.deepStrictEqual(mut_got, [1, 2]);
231
+ });
232
+
233
+ test('inner observables switched away from stop computing', () => {
234
+ const [outer$, setOuter] = createState(0);
235
+
236
+ const [inner$, setInner] = createState(0);
237
+
238
+ inner$.subscribe(() => {});
239
+
240
+ let mut_calls = 0;
241
+
242
+ const result$ = outer$.pipe(
243
+ switchMap((o) =>
244
+ inner$.pipe(
245
+ map((i) => {
246
+ mut_calls += 1;
247
+
248
+ return o + i;
249
+ }),
250
+ ),
251
+ ),
252
+ );
253
+
254
+ result$.subscribe(() => {});
255
+
256
+ for (const o of [1, 2, 3, 4, 5]) {
257
+ setOuter(o);
258
+ }
259
+
260
+ mut_calls = 0;
261
+
262
+ setInner(1);
263
+
264
+ expect(mut_calls).toBe(1);
265
+ });
266
+ });
267
+
268
+ describe('a completed observable keeps its last value', () => {
269
+ test('getSnapshot returns the value the observable completed with', () => {
270
+ const s = source<number>(0);
271
+
272
+ s.next(5);
273
+
274
+ s.complete();
275
+
276
+ assert.isTrue(s.isCompleted);
277
+
278
+ assert.deepStrictEqual(s.getSnapshot(), Optional.some(5));
279
+ });
280
+
281
+ test('combine keeps reading the last value of a parent that completed after it changed', () => {
282
+ const a = source<number>(0);
283
+
284
+ const b = source<number>(0);
285
+
286
+ const c = combine([a, b]);
287
+
288
+ const mut_got: FixedLengthTuple<2, number>[] = [];
289
+
290
+ c.subscribe((v) => {
291
+ mut_got.push(v);
292
+ });
293
+
294
+ b.next(5);
295
+
296
+ b.complete();
297
+
298
+ a.next(1);
299
+
300
+ assert.isFalse(c.isCompleted);
301
+
302
+ assert.deepStrictEqual(mut_got, [
303
+ [0, 0],
304
+ [0, 5],
305
+ [1, 5],
306
+ ]);
307
+ });
308
+
309
+ test('a derived observable completed by its parent keeps its last value for combine', () => {
310
+ const a = source<number>(0);
311
+
312
+ const s = source<number>(1);
313
+
314
+ const m = s.pipe(map((x) => x * 2));
315
+
316
+ const c = combine([a, m]);
317
+
318
+ const mut_got: FixedLengthTuple<2, number>[] = [];
319
+
320
+ c.subscribe((v) => {
321
+ mut_got.push(v);
322
+ });
323
+
324
+ s.next(3);
325
+
326
+ s.complete();
327
+
328
+ assert.isTrue(m.isCompleted);
329
+
330
+ assert.deepStrictEqual(m.getSnapshot(), Optional.some(6));
331
+
332
+ a.next(1);
333
+
334
+ assert.isFalse(c.isCompleted);
335
+
336
+ assert.deepStrictEqual(mut_got, [
337
+ [0, 2],
338
+ [0, 6],
339
+ [1, 6],
340
+ ]);
341
+ });
342
+
343
+ test('combine created after a parent completed starts from its last value', () => {
344
+ const a = source<number>(0);
345
+
346
+ const b = source<number>(0);
347
+
348
+ b.next(5);
349
+
350
+ b.complete();
351
+
352
+ const c = combine([a, b]);
353
+
354
+ assert.deepStrictEqual(c.getSnapshot(), Optional.some([0, 5] as const));
355
+
356
+ const mut_got: FixedLengthTuple<2, number>[] = [];
357
+
358
+ c.subscribe((v) => {
359
+ mut_got.push(v);
360
+ });
361
+
362
+ a.next(1);
363
+
364
+ assert.deepStrictEqual(mut_got, [
365
+ [0, 5],
366
+ [1, 5],
367
+ ]);
368
+ });
369
+ });
@@ -0,0 +1,85 @@
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 { Optional } from 'ts-data-forge';
6
+ import {
7
+ isRootObservable,
8
+ type RootObservable,
9
+ type UpdateToken,
10
+ } from '../types/index.mjs';
11
+ import { ManagerObservableClass } from './manager-observable-class.mjs';
12
+ import { isPropagating, outermostUpdateToken } from './propagation-state.mjs';
13
+
14
+ export class RootObservableClass<A>
15
+ extends ManagerObservableClass<A, 'root', 0>
16
+ implements RootObservable<A>
17
+ {
18
+ /**
19
+ * The outermost update propagating when this root was created. A root
20
+ * created while an update propagates (inside a `switchMap` projection,
21
+ * say) may emit while it is created, as `fromSubscribable` does for a
22
+ * synchronous source. Nothing is derived from it yet, so that is its
23
+ * initial value rather than a write (#2336).
24
+ */
25
+ readonly #createdDuring: UpdateToken | undefined;
26
+ #mut_hasWarned: boolean;
27
+
28
+ constructor({
29
+ initialValue,
30
+ }: Readonly<{
31
+ initialValue: Optional<A>;
32
+ }>) {
33
+ super({
34
+ kind: 'root',
35
+ depth: 0,
36
+ initialValue,
37
+ });
38
+
39
+ this.#createdDuring = outermostUpdateToken();
40
+
41
+ this.#mut_hasWarned = false;
42
+
43
+ this.adoptIntoCurrentScope();
44
+ }
45
+
46
+ /**
47
+ * Emits `nextValue` and propagates the update to all descendants, warning
48
+ * once when it is a write from inside another update.
49
+ */
50
+ protected override startUpdate(nextValue: A): void {
51
+ if (
52
+ !this.#mut_hasWarned &&
53
+ isPropagating() &&
54
+ outermostUpdateToken() !== this.#createdDuring
55
+ ) {
56
+ this.#mut_hasWarned = true;
57
+
58
+ console.warn(writeDuringPropagationWarning);
59
+ }
60
+
61
+ super.startUpdate(nextValue);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Shown once per root observable, the first time it is written to (`next`,
67
+ * `setState`, `dispatch`, …) while another update is still propagating —
68
+ * from a subscriber or a projection. The write still propagates immediately,
69
+ * as it always has; the warning is there because the pattern is wrong, not
70
+ * because anything was refused.
71
+ */
72
+ export const writeDuringPropagationWarning =
73
+ 'synstate: an observable was written to (next / setState / dispatch) while an update was propagating, so subscribers can see intermediate states. Derive the value with map / combine instead of writing from a subscriber or a projection.';
74
+
75
+ if (import.meta.vitest !== undefined) {
76
+ test('isRootObservable', () => {
77
+ assert.isTrue(
78
+ isRootObservable(
79
+ new RootObservableClass({
80
+ initialValue: Optional.some(0),
81
+ }),
82
+ ),
83
+ );
84
+ });
85
+ }