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,234 @@
1
+ import { Result } from 'ts-data-forge';
2
+ import { panic } from 'ts-std-forge';
3
+ import type { FixedLengthTuple } from 'ts-type-forge';
4
+ import { createState } from '../../utils/index.mjs';
5
+ import { combine } from '../combine/index.mjs';
6
+ import { fromSubscribable, source } from '../create/index.mjs';
7
+ import { map, switchMap } from '../operators/index.mjs';
8
+ import { writeDuringPropagationWarning } from './root-observable-class.mjs';
9
+
10
+ describe('a write to a root observable while an update is propagating (#2336)', () => {
11
+ test('warns when a subscriber writes to another state, and still propagates the write immediately', async () => {
12
+ const [a$, setA] = createState(0);
13
+
14
+ const [b$, setB] = createState(0);
15
+
16
+ const doubled$ = a$.pipe(map((a) => a * 10));
17
+
18
+ a$.subscribe((a) => {
19
+ setB(a * 2);
20
+ });
21
+
22
+ const mut_history: FixedLengthTuple<2, number>[] = [];
23
+
24
+ combine([doubled$, b$]).subscribe((v) => {
25
+ mut_history.push(v);
26
+ });
27
+
28
+ const warn = await captureWarnings(() => {
29
+ setA(1);
30
+ });
31
+
32
+ assert.deepStrictEqual(warn, [writeDuringPropagationWarning]);
33
+
34
+ // The behavior is unchanged: b$ propagates before doubled$ is updated, so
35
+ // the intermediate state [0, 2] is observed.
36
+ assert.deepStrictEqual(mut_history, [
37
+ [0, 0],
38
+ [0, 2],
39
+ [10, 2],
40
+ ]);
41
+
42
+ assert.strictEqual(b$.getSnapshot().value, 2);
43
+ });
44
+
45
+ test('warns when a projection writes to a state', async () => {
46
+ const a$ = source<number>(0);
47
+
48
+ const [, setB] = createState(0);
49
+
50
+ a$.pipe(
51
+ map((a) => {
52
+ setB(a);
53
+
54
+ return a;
55
+ }),
56
+ ).subscribe(() => {});
57
+
58
+ const warn = await captureWarnings(() => {
59
+ a$.next(1);
60
+ });
61
+
62
+ assert.deepStrictEqual(warn, [writeDuringPropagationWarning]);
63
+ });
64
+
65
+ test('warns when a subscriber of an asynchronous child writes to a state', async () => {
66
+ const outer$ = source<number>(0);
67
+
68
+ const inner$ = source<number>(0);
69
+
70
+ const [, setB] = createState(0);
71
+
72
+ outer$.pipe(switchMap(() => inner$)).subscribe((v) => {
73
+ if (v > 0) {
74
+ setB(v);
75
+ }
76
+ });
77
+
78
+ const warnOnSwitch = await captureWarnings(() => {
79
+ outer$.next(1);
80
+ });
81
+
82
+ assert.deepStrictEqual(warnOnSwitch, []);
83
+
84
+ const warn = await captureWarnings(() => {
85
+ inner$.next(1);
86
+ });
87
+
88
+ assert.deepStrictEqual(warn, [writeDuringPropagationWarning]);
89
+ });
90
+
91
+ test('warns once per observable', async () => {
92
+ const a$ = source<number>(0);
93
+
94
+ const [, setB] = createState(0);
95
+
96
+ const [, setC] = createState(0);
97
+
98
+ a$.subscribe((a) => {
99
+ if (a <= 0) {
100
+ return;
101
+ }
102
+
103
+ setB(a);
104
+
105
+ setB(a + 1);
106
+
107
+ setC(a);
108
+ });
109
+
110
+ const warn = await captureWarnings(() => {
111
+ a$.next(1);
112
+
113
+ a$.next(2);
114
+ });
115
+
116
+ assert.deepStrictEqual(warn, [
117
+ writeDuringPropagationWarning,
118
+ writeDuringPropagationWarning,
119
+ ]);
120
+ });
121
+
122
+ test('does not warn for writes outside propagation', async () => {
123
+ const [a$, setA] = createState(0);
124
+
125
+ const [b$, setB] = createState(0);
126
+
127
+ a$.subscribe((a) => {
128
+ queueMicrotask(() => {
129
+ setB(a);
130
+ });
131
+ });
132
+
133
+ const warn = await captureWarnings(async () => {
134
+ setA(1);
135
+
136
+ await Promise.resolve();
137
+ });
138
+
139
+ assert.deepStrictEqual(warn, []);
140
+
141
+ assert.strictEqual(b$.getSnapshot().value, 1);
142
+ });
143
+
144
+ test('does not warn when an operator emits an inner observable synchronously', async () => {
145
+ const outer$ = source<number>(0);
146
+
147
+ const mut_history: number[] = [];
148
+
149
+ outer$.pipe(switchMap((x) => source(x * 10))).subscribe((v) => {
150
+ mut_history.push(v);
151
+ });
152
+
153
+ const warn = await captureWarnings(() => {
154
+ outer$.next(1);
155
+ });
156
+
157
+ assert.deepStrictEqual(warn, []);
158
+
159
+ assert.deepStrictEqual(mut_history, [10]);
160
+ });
161
+
162
+ test('does not warn when a root created during propagation emits while it is created', async () => {
163
+ const outer$ = source<number>(0);
164
+
165
+ const mut_history: number[] = [];
166
+
167
+ outer$
168
+ .pipe(
169
+ switchMap((x) =>
170
+ fromSubscribable<number>({
171
+ subscribe: (onNext) => {
172
+ onNext(x * 10);
173
+
174
+ return { unsubscribe: () => {} };
175
+ },
176
+ }),
177
+ ),
178
+ )
179
+ .subscribe((r) => {
180
+ if (Result.isOk(r)) {
181
+ mut_history.push(r.value);
182
+ }
183
+ });
184
+
185
+ const warn = await captureWarnings(() => {
186
+ outer$.next(1);
187
+ });
188
+
189
+ assert.deepStrictEqual(warn, []);
190
+
191
+ assert.deepStrictEqual(mut_history, [10]);
192
+ });
193
+
194
+ test('a subscriber that throws does not leave the propagation flag set', async () => {
195
+ const a$ = source<number>(0);
196
+
197
+ const [, setB] = createState(0);
198
+
199
+ a$.subscribe((a) => {
200
+ if (a > 0) {
201
+ panic('boom');
202
+ }
203
+ });
204
+
205
+ const warn = await captureWarnings(() => {
206
+ expect(() => {
207
+ a$.next(1);
208
+ }).toThrow('boom');
209
+
210
+ setB(1);
211
+ });
212
+
213
+ assert.deepStrictEqual(warn, []);
214
+ });
215
+ });
216
+
217
+ /**
218
+ * Runs `run` with `console.warn` replaced by a mock, and returns the messages
219
+ * it was called with.
220
+ */
221
+ const captureWarnings = async (
222
+ run: () => Promise<void> | void,
223
+ ): Promise<readonly unknown[]> => {
224
+ const warn = vi.fn();
225
+
226
+ vi.stubGlobal('console', { ...console, warn });
227
+
228
+ await run();
229
+
230
+ // cspell:disable-next-line
231
+ vi.unstubAllGlobals();
232
+
233
+ return warn.mock.calls.map((args: readonly unknown[]) => args[0]);
234
+ };
@@ -1,6 +1,12 @@
1
+ // Sumi bans classes, `this`, method syntax and writes to `this` outside a
2
+ // `mut_` binding; an observable is a class (`ObservableBaseClass` says why),
3
+ // so those rules are waived here (D-63).
4
+ // @sumi-expect-error-file classes/no-class
5
+ // @sumi-expect-error-file banned-syntax/no-this
6
+ // @sumi-expect-error-file functions/prefer-arrow-function
1
7
  import { Arr, expectType, Optional } from 'ts-data-forge';
2
8
  import type { NonEmptyTuple, Tuple } from 'ts-type-forge';
3
- import { createSyncChildObservable } from '../base/index.mjs';
9
+ import { SyncChildObservableClass } from '../class/index.mjs';
4
10
  import { source } from '../create/index.mjs';
5
11
  import { withInitialValue } from '../operators/index.mjs';
6
12
  import type {
@@ -11,6 +17,7 @@ import type {
11
17
  NonEmptyUnknownList,
12
18
  Observable,
13
19
  SyncChildObservable,
20
+ UpdateToken,
14
21
  Wrap,
15
22
  } from '../types/index.mjs';
16
23
 
@@ -75,7 +82,9 @@ export const combine = <const OS extends NonEmptyTuple<Observable<unknown>>>(
75
82
  parents: OS,
76
83
  ): CombineObservableRefined<OS> =>
77
84
  // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
78
- createCombineObservable(parents) as unknown as CombineObservableRefined<OS>;
85
+ new CombineObservableClass(
86
+ parents,
87
+ ) as unknown as CombineObservableRefined<OS>;
79
88
 
80
89
  /**
81
90
  * Alias for `combine`.
@@ -83,61 +92,64 @@ export const combine = <const OS extends NonEmptyTuple<Observable<unknown>>>(
83
92
  */
84
93
  export const combineLatest = combine;
85
94
 
86
- const createCombineObservable = <const A extends NonEmptyUnknownList>(
87
- parents: Wrap<A>,
88
- ): CombineObservable<A> => {
89
- const parentsValues: Tuple.MapTo<Optional<A[number]>, A> = Arr.map(
90
- parents,
91
- (p) => p.getSnapshot(),
92
- );
93
-
94
- // The annotation on `initialValue` is what replaces the type assertion.
95
- // `Arr.map` carries a single result element type, so it reports `A[number]`
96
- // at every position, and `Tuple.MapTo<A[number], A>` — the uniform mapping
97
- // — is *not* assignable back to `A`. The positional
98
- // `Readonly<{ [K in keyof A]: A[K] }>` is, and TypeScript accepts the
99
- // uniform result into it because the two mapped types share `keyof A`. So
100
- // it is the annotation, not an assertion, that records the position-wise
101
- // fact the checker cannot derive on its own.
102
- const initialValue: Optional<Readonly<{ [K in keyof A]: A[K] }>> = Arr.every(
103
- parentsValues,
104
- Optional.isSome,
105
- )
106
- ? Optional.some(Arr.map(parentsValues, (c) => c.value))
107
- : Optional.none;
108
-
109
- return createSyncChildObservable<A, A>(
110
- {
95
+ class CombineObservableClass<const A extends NonEmptyUnknownList>
96
+ extends SyncChildObservableClass<A, A>
97
+ implements CombineObservable<A>
98
+ {
99
+ constructor(parents: Wrap<A>) {
100
+ const parentsValues: Tuple.MapTo<Optional<A[number]>, A> = Arr.map(
101
+ parents,
102
+ (p) => p.getSnapshot(),
103
+ );
104
+
105
+ // The annotation on `initialValue` is what replaces the type assertion.
106
+ // `Arr.map` carries a single result element type, so it reports `A[number]`
107
+ // at every position, and `Tuple.MapTo<A[number], A>` — the uniform mapping
108
+ // — is *not* assignable back to `A`. The positional
109
+ // `Readonly<{ [K in keyof A]: A[K] }>` is, and TypeScript accepts the
110
+ // uniform result into it because the two mapped types share `keyof A`. So
111
+ // it is the annotation, not an assertion, that records the position-wise
112
+ // fact the checker cannot derive on its own.
113
+ const initialValue: Optional<Readonly<{ [K in keyof A]: A[K] }>> =
114
+ Arr.every(parentsValues, Optional.isSome)
115
+ ? Optional.some(Arr.map(parentsValues, (c) => c.value))
116
+ : Optional.none;
117
+
118
+ super({
111
119
  parents,
112
120
  initialValue,
113
- },
114
- ({ setNext }) =>
115
- (updateToken) => {
116
- if (parents.every((o) => o.updateToken !== updateToken)) {
117
- return; // all parents are skipped
118
- }
119
-
120
- // Same shape-preserving chain as the initial value: `Arr.map` (not the
121
- // native one, which drops the tuple shape) into the uniform
122
- // `Tuple.MapTo`, then a positional annotation to get back to `A`.
123
- const parentValues: Tuple.MapTo<Optional<A[number]>, A> = Arr.map(
124
- parents,
125
- (a) => a.getSnapshot(),
126
- );
127
-
128
- if (!Arr.every(parentValues, Optional.isSome)) {
129
- return;
130
- }
131
-
132
- const nextValue: Readonly<{ [K in keyof A]: A[K] }> = Arr.map(
133
- parentValues,
134
- (a) => a.value,
135
- );
136
-
137
- setNext(nextValue, updateToken);
138
- },
139
- );
140
- };
121
+ });
122
+
123
+ this.attach();
124
+ }
125
+
126
+ override tryUpdate(updateToken: UpdateToken): void {
127
+ const parents = this.parents;
128
+
129
+ if (parents.every((o) => o.updateToken !== updateToken)) {
130
+ return; // all parents are skipped
131
+ }
132
+
133
+ // Same shape-preserving chain as the initial value: `Arr.map` (not the
134
+ // native one, which drops the tuple shape) into the uniform
135
+ // `Tuple.MapTo`, then a positional annotation to get back to `A`.
136
+ const parentValues: Tuple.MapTo<Optional<A[number]>, A> = Arr.map(
137
+ parents,
138
+ (a) => a.getSnapshot(),
139
+ );
140
+
141
+ if (!Arr.every(parentValues, Optional.isSome)) {
142
+ return;
143
+ }
144
+
145
+ const nextValue: Readonly<{ [K in keyof A]: A[K] }> = Arr.map(
146
+ parentValues,
147
+ (a) => a.value,
148
+ );
149
+
150
+ this.setNext(nextValue, updateToken);
151
+ }
152
+ }
141
153
 
142
154
  if (import.meta.vitest !== undefined) {
143
155
  test('type test', () => {
@@ -1,6 +1,12 @@
1
+ // Sumi bans classes, `this`, method syntax and writes to `this` outside a
2
+ // `mut_` binding; an observable is a class (`ObservableBaseClass` says why),
3
+ // so those rules are waived here (D-63).
4
+ // @sumi-expect-error-file classes/no-class
5
+ // @sumi-expect-error-file banned-syntax/no-this
6
+ // @sumi-expect-error-file functions/prefer-arrow-function
1
7
  import { Optional, expectType } from 'ts-data-forge';
2
8
  import type { ArrayElement, NonEmptyTuple } from 'ts-type-forge';
3
- import { createSyncChildObservable } from '../base/index.mjs';
9
+ import { SyncChildObservableClass } from '../class/index.mjs';
4
10
  import { source } from '../create/index.mjs';
5
11
  import type {
6
12
  MergeObservable,
@@ -8,6 +14,7 @@ import type {
8
14
  NonEmptyUnknownList,
9
15
  Observable,
10
16
  SyncChildObservable,
17
+ UpdateToken,
11
18
  Wrap,
12
19
  } from '../types/index.mjs';
13
20
 
@@ -58,42 +65,42 @@ import type {
58
65
  *
59
66
  * assert.deepStrictEqual(valueHistory, ['c1', 'k1', 'c2', 'k2']);
60
67
  * ```
61
- *
62
- * @note To improve code readability, consider using `createState` instead of `merge`,
63
- * subscribing to `parents` and calling `setState` within it.
64
68
  */
65
69
  export const merge = <const OS extends NonEmptyTuple<Observable<unknown>>>(
66
70
  parents: OS,
67
71
  ): MergeObservableRefined<OS> =>
68
72
  // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
69
- createMergeObservable(parents) as MergeObservableRefined<OS>;
73
+ new MergeObservableClass(parents) as MergeObservableRefined<OS>;
70
74
 
71
- const createMergeObservable = <const P extends NonEmptyUnknownList>(
72
- parents: Wrap<P>,
73
- ): MergeObservable<P> =>
74
- createSyncChildObservable<ArrayElement<P>, P>(
75
- {
75
+ class MergeObservableClass<const P extends NonEmptyUnknownList>
76
+ extends SyncChildObservableClass<ArrayElement<P>, P>
77
+ implements MergeObservable<P>
78
+ {
79
+ constructor(parents: Wrap<P>) {
80
+ super({
76
81
  parents,
77
82
  initialValue: Optional.none,
78
- },
79
- ({ setNext }) =>
80
- (updateToken) => {
81
- const parentToUse = parents.find(
82
- (o) =>
83
- o.updateToken === updateToken && Optional.isSome(o.getSnapshot()),
84
- );
83
+ });
84
+
85
+ this.attach();
86
+ }
85
87
 
86
- if (parentToUse === undefined) {
87
- return;
88
- }
88
+ override tryUpdate(updateToken: UpdateToken): void {
89
+ const parentToUse = this.parents.find(
90
+ (o) => o.updateToken === updateToken && Optional.isSome(o.getSnapshot()),
91
+ );
89
92
 
90
- const nextValue =
91
- // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
92
- Optional.unwrap(parentToUse.getSnapshot()) as ArrayElement<P>;
93
+ if (parentToUse === undefined) {
94
+ return;
95
+ }
93
96
 
94
- setNext(nextValue, updateToken);
95
- },
96
- );
97
+ const nextValue =
98
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
99
+ Optional.unwrap(parentToUse.getSnapshot()) as ArrayElement<P>;
100
+
101
+ this.setNext(nextValue, updateToken);
102
+ }
103
+ }
97
104
 
98
105
  if (import.meta.vitest !== undefined) {
99
106
  test('type test', () => {
@@ -1,6 +1,13 @@
1
+ // Sumi bans classes, `this`, method syntax and writes to `this` outside a
2
+ // `mut_` binding; an observable is a class (`ObservableBaseClass` says why),
3
+ // so those rules are waived here (D-63).
4
+ // @sumi-expect-error-file classes/no-class
5
+ // @sumi-expect-error-file banned-syntax/no-this
6
+ // @sumi-expect-error-file functions/prefer-arrow-function
7
+ // @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
1
8
  import { Arr, Optional, createQueue, expectType } from 'ts-data-forge';
2
9
  import type { NonEmptyTuple, Tuple } from 'ts-type-forge';
3
- import { createSyncChildObservable } from '../base/index.mjs';
10
+ import { SyncChildObservableClass } from '../class/index.mjs';
4
11
  import { source } from '../create/index.mjs';
5
12
  import { withInitialValue } from '../operators/index.mjs';
6
13
  import type {
@@ -10,6 +17,7 @@ import type {
10
17
  Observable,
11
18
  SyncChildObservable,
12
19
  TupleToQueueTuple,
20
+ UpdateToken,
13
21
  Wrap,
14
22
  ZipObservable,
15
23
  ZipObservableRefined,
@@ -18,7 +26,9 @@ import type {
18
26
  /**
19
27
  * Combines multiple observables by pairing their emissions by index.
20
28
  * Waits for all sources to emit their nth value before emitting the nth tuple.
21
- * Completes when any source completes.
29
+ * Completes once a completed source has no value left to pair, as RxJS's
30
+ * `zip` does: at once if it completes with nothing queued, otherwise after
31
+ * the tuple that takes its last queued value.
22
32
  *
23
33
  * @template OS - Tuple type of source observables
24
34
  * @param parents - Array of observables to zip
@@ -35,7 +45,7 @@ import type {
35
45
  * // Explanation:
36
46
  * // - zip pairs values by their index from multiple sources
37
47
  * // - Waits for all sources to emit at the same index
38
- * // - Completes when any source completes
48
+ * // - Completes once a completed source has no value left to pair
39
49
  *
40
50
  * const [letters$, setLetter] = createState<string>('A');
41
51
  *
@@ -68,62 +78,97 @@ export const zip = <const OS extends NonEmptyTuple<Observable<unknown>>>(
68
78
  parents: OS,
69
79
  ): ZipObservableRefined<OS> =>
70
80
  // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
71
- createZipObservable(parents) as unknown as ZipObservableRefined<OS>;
72
-
73
- const createZipObservable = <const A extends NonEmptyUnknownList>(
74
- parents: Wrap<A>,
75
- ): ZipObservable<A> => {
76
- const parentsValues: Tuple.MapTo<Optional<A[number]>, A> = Arr.map(
77
- parents,
78
- (p) => p.getSnapshot(),
79
- );
80
-
81
- // The annotation on `initialValue` is what replaces the type assertion.
82
- // `Arr.map` carries a single result element type, so it reports `A[number]`
83
- // at every position, and `Tuple.MapTo<A[number], A>` — the uniform mapping
84
- // — is *not* assignable back to `A`. The positional
85
- // `Readonly<{ [K in keyof A]: A[K] }>` is, and TypeScript accepts the
86
- // uniform result into it because the two mapped types share `keyof A`. So
87
- // it is the annotation, not an assertion, that records the position-wise
88
- // fact the checker cannot derive on its own.
89
- const initialValue: Optional<Readonly<{ [K in keyof A]: A[K] }>> = Arr.every(
90
- parentsValues,
91
- Optional.isSome,
92
- )
93
- ? Optional.some(Arr.map(parentsValues, (c) => c.value))
94
- : Optional.none;
95
-
96
- const queues: TupleToQueueTuple<A> = Arr.map(parents, () => createQueue());
97
-
98
- return createSyncChildObservable<A, A>(
99
- {
81
+ new ZipObservableClass(parents) as unknown as ZipObservableRefined<OS>;
82
+
83
+ class ZipObservableClass<const A extends NonEmptyUnknownList>
84
+ extends SyncChildObservableClass<A, A>
85
+ implements ZipObservable<A>
86
+ {
87
+ readonly #queues: TupleToQueueTuple<A>;
88
+
89
+ /**
90
+ * The token of each parent's latest value that zip has taken in, so that a
91
+ * value a parent emits in the update it completes in — its `tryComplete`
92
+ * reaches zip before its `tryUpdate` does — still counts as queued.
93
+ * Written in place, one slot per parent, on every update that reaches zip.
94
+ */
95
+ // @sumi-expect-error readonly/require-readonly-type
96
+ readonly #mut_takenTokens: UpdateToken[];
97
+
98
+ constructor(parents: Wrap<A>) {
99
+ const parentsValues: Tuple.MapTo<Optional<A[number]>, A> = Arr.map(
100
100
  parents,
101
- initialValue,
102
- },
103
- ({ setNext }) =>
104
- (updateToken) => {
105
- for (const [index, par] of parents.entries()) {
106
- const sn = par.getSnapshot();
107
-
108
- if (par.updateToken === updateToken && Optional.isSome(sn)) {
109
- queues[index]?.enqueue(sn.value);
110
- }
111
- }
112
-
113
- if (queues.some((list) => list.isEmpty)) {
114
- return;
115
- }
116
-
117
- const nextValue =
118
- // `Arr.map` has a single result element type, so it reports
119
- // `A[number] | undefined` per position rather than `A[P]`.
120
- // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
121
- Arr.map(queues, (q) => Optional.unwrap(q.dequeue())) as A;
122
-
123
- setNext(nextValue, updateToken);
124
- },
125
- );
126
- };
101
+ (p) => p.getSnapshot(),
102
+ );
103
+
104
+ // The annotation on `initialValue` is what replaces the type assertion.
105
+ // `Arr.map` carries a single result element type, so it reports `A[number]`
106
+ // at every position, and `Tuple.MapTo<A[number], A>` — the uniform mapping
107
+ // — is *not* assignable back to `A`. The positional
108
+ // `Readonly<{ [K in keyof A]: A[K] }>` is, and TypeScript accepts the
109
+ // uniform result into it because the two mapped types share `keyof A`. So
110
+ // it is the annotation, not an assertion, that records the position-wise
111
+ // fact the checker cannot derive on its own.
112
+ const initialValue: Optional<Readonly<{ [K in keyof A]: A[K] }>> =
113
+ Arr.every(parentsValues, Optional.isSome)
114
+ ? Optional.some(Arr.map(parentsValues, (c) => c.value))
115
+ : Optional.none;
116
+
117
+ super({ parents, initialValue });
118
+
119
+ this.#queues = Arr.map(parents, () => createQueue());
120
+
121
+ this.#mut_takenTokens = Array.from(parents, (p) => p.updateToken);
122
+
123
+ this.attach();
124
+ }
125
+
126
+ override tryUpdate(updateToken: UpdateToken): void {
127
+ const queues = this.#queues;
128
+
129
+ for (const [index, par] of this.parents.entries()) {
130
+ const sn = par.getSnapshot();
131
+
132
+ if (!(par.updateToken === updateToken && Optional.isSome(sn))) {
133
+ continue;
134
+ }
135
+
136
+ queues[index]?.enqueue(sn.value);
137
+
138
+ this.#mut_takenTokens[index] = updateToken;
139
+ }
140
+
141
+ if (queues.some((list) => list.isEmpty)) {
142
+ return;
143
+ }
144
+
145
+ const nextValue =
146
+ // `Arr.map` has a single result element type, so it reports
147
+ // `A[number] | undefined` per position rather than `A[P]`.
148
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
149
+ Arr.map(queues, (q) => Optional.unwrap(q.dequeue())) as A;
150
+
151
+ this.setNext(nextValue, updateToken);
152
+
153
+ if (this.canEmitNoMore()) {
154
+ this.complete();
155
+ }
156
+ }
157
+
158
+ /**
159
+ * No tuple can be completed any more once a completed source has nothing
160
+ * left to pair (#2343).
161
+ */
162
+ override canEmitNoMore(): boolean {
163
+ return this.parents.some(
164
+ (par, index) =>
165
+ par.isCompleted &&
166
+ this.#queues[index]?.isEmpty === true &&
167
+ (par.updateToken === this.#mut_takenTokens[index] ||
168
+ Optional.isNone(par.getSnapshot())),
169
+ );
170
+ }
171
+ }
127
172
 
128
173
  if (import.meta.vitest !== undefined) {
129
174
  test('type test', () => {