synstate 0.1.2 → 1.0.1

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 (323) hide show
  1. package/README.md +200 -147
  2. package/assets/old/synstate-icon-square.png +0 -0
  3. package/assets/synstate-logo.png +0 -0
  4. package/dist/core/class/child-observable-class.d.mts +2 -0
  5. package/dist/core/class/child-observable-class.d.mts.map +1 -1
  6. package/dist/core/class/child-observable-class.mjs +44 -13
  7. package/dist/core/class/child-observable-class.mjs.map +1 -1
  8. package/dist/core/class/observable-base-class.d.mts +4 -4
  9. package/dist/core/class/observable-base-class.d.mts.map +1 -1
  10. package/dist/core/class/observable-base-class.mjs +8 -8
  11. package/dist/core/class/observable-base-class.mjs.map +1 -1
  12. package/dist/core/class/root-observable-class.d.mts +3 -1
  13. package/dist/core/class/root-observable-class.d.mts.map +1 -1
  14. package/dist/core/class/root-observable-class.mjs +9 -9
  15. package/dist/core/class/root-observable-class.mjs.map +1 -1
  16. package/dist/core/combine/combine.d.mts +7 -6
  17. package/dist/core/combine/combine.d.mts.map +1 -1
  18. package/dist/core/combine/combine.mjs +11 -12
  19. package/dist/core/combine/combine.mjs.map +1 -1
  20. package/dist/core/combine/merge.d.mts +7 -6
  21. package/dist/core/combine/merge.d.mts.map +1 -1
  22. package/dist/core/combine/merge.mjs +9 -9
  23. package/dist/core/combine/merge.mjs.map +1 -1
  24. package/dist/core/combine/zip.d.mts +21 -19
  25. package/dist/core/combine/zip.d.mts.map +1 -1
  26. package/dist/core/combine/zip.mjs +22 -21
  27. package/dist/core/combine/zip.mjs.map +1 -1
  28. package/dist/core/create/{interval.d.mts → counter.d.mts} +14 -12
  29. package/dist/core/create/counter.d.mts.map +1 -0
  30. package/dist/core/create/{interval.mjs → counter.mjs} +21 -23
  31. package/dist/core/create/counter.mjs.map +1 -0
  32. package/dist/core/create/from-abortable-promise.d.mts +29 -0
  33. package/dist/core/create/from-abortable-promise.d.mts.map +1 -0
  34. package/dist/core/create/from-abortable-promise.mjs +70 -0
  35. package/dist/core/create/from-abortable-promise.mjs.map +1 -0
  36. package/dist/core/create/from-promise.d.mts +9 -6
  37. package/dist/core/create/from-promise.d.mts.map +1 -1
  38. package/dist/core/create/from-promise.mjs +8 -5
  39. package/dist/core/create/from-promise.mjs.map +1 -1
  40. package/dist/core/create/from-subscribable.d.mts +4 -4
  41. package/dist/core/create/from-subscribable.mjs +4 -4
  42. package/dist/core/create/index.d.mts +3 -3
  43. package/dist/core/create/index.d.mts.map +1 -1
  44. package/dist/core/create/index.mjs +4 -4
  45. package/dist/core/create/just.d.mts +32 -0
  46. package/dist/core/create/just.d.mts.map +1 -0
  47. package/dist/core/create/just.mjs +44 -0
  48. package/dist/core/create/just.mjs.map +1 -0
  49. package/dist/core/create/source.d.mts +7 -12
  50. package/dist/core/create/source.d.mts.map +1 -1
  51. package/dist/core/create/source.mjs +1 -6
  52. package/dist/core/create/source.mjs.map +1 -1
  53. package/dist/core/create/timer.d.mts +6 -4
  54. package/dist/core/create/timer.d.mts.map +1 -1
  55. package/dist/core/create/timer.mjs +6 -7
  56. package/dist/core/create/timer.mjs.map +1 -1
  57. package/dist/core/index.d.mts +0 -1
  58. package/dist/core/index.d.mts.map +1 -1
  59. package/dist/core/index.mjs +8 -13
  60. package/dist/core/index.mjs.map +1 -1
  61. package/dist/core/operators/audit.d.mts +97 -0
  62. package/dist/core/operators/audit.d.mts.map +1 -0
  63. package/dist/core/operators/audit.mjs +144 -0
  64. package/dist/core/operators/audit.mjs.map +1 -0
  65. package/dist/core/operators/debounce.d.mts +88 -0
  66. package/dist/core/operators/debounce.d.mts.map +1 -0
  67. package/dist/core/operators/debounce.mjs +130 -0
  68. package/dist/core/operators/debounce.mjs.map +1 -0
  69. package/dist/core/operators/filter.d.mts +6 -5
  70. package/dist/core/operators/filter.d.mts.map +1 -1
  71. package/dist/core/operators/filter.mjs +3 -3
  72. package/dist/core/operators/filter.mjs.map +1 -1
  73. package/dist/core/operators/index.d.mts +4 -4
  74. package/dist/core/operators/index.d.mts.map +1 -1
  75. package/dist/core/operators/index.mjs +4 -4
  76. package/dist/core/operators/{map-with-index.d.mts → map.d.mts} +12 -11
  77. package/dist/core/operators/map.d.mts.map +1 -0
  78. package/dist/core/operators/{map-with-index.mjs → map.mjs} +17 -17
  79. package/dist/core/operators/map.mjs.map +1 -0
  80. package/dist/core/operators/merge-map.d.mts +56 -29
  81. package/dist/core/operators/merge-map.d.mts.map +1 -1
  82. package/dist/core/operators/merge-map.mjs +58 -31
  83. package/dist/core/operators/merge-map.mjs.map +1 -1
  84. package/dist/core/operators/pairwise.d.mts +6 -6
  85. package/dist/core/operators/pairwise.mjs +9 -9
  86. package/dist/core/operators/pairwise.mjs.map +1 -1
  87. package/dist/core/operators/scan.d.mts +6 -6
  88. package/dist/core/operators/scan.mjs +9 -9
  89. package/dist/core/operators/scan.mjs.map +1 -1
  90. package/dist/core/operators/skip-if-no-change.d.mts +20 -8
  91. package/dist/core/operators/skip-if-no-change.d.mts.map +1 -1
  92. package/dist/core/operators/skip-if-no-change.mjs +23 -11
  93. package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
  94. package/dist/core/operators/skip-until.d.mts +5 -5
  95. package/dist/core/operators/skip-until.mjs +8 -8
  96. package/dist/core/operators/skip-until.mjs.map +1 -1
  97. package/dist/core/operators/skip-while.d.mts +19 -8
  98. package/dist/core/operators/skip-while.d.mts.map +1 -1
  99. package/dist/core/operators/skip-while.mjs +26 -11
  100. package/dist/core/operators/skip-while.mjs.map +1 -1
  101. package/dist/core/operators/switch-map.d.mts +57 -26
  102. package/dist/core/operators/switch-map.d.mts.map +1 -1
  103. package/dist/core/operators/switch-map.mjs +59 -28
  104. package/dist/core/operators/switch-map.mjs.map +1 -1
  105. package/dist/core/operators/take-until.d.mts +5 -5
  106. package/dist/core/operators/take-until.mjs +8 -8
  107. package/dist/core/operators/take-until.mjs.map +1 -1
  108. package/dist/core/operators/take-while.d.mts +16 -7
  109. package/dist/core/operators/take-while.d.mts.map +1 -1
  110. package/dist/core/operators/take-while.mjs +18 -10
  111. package/dist/core/operators/take-while.mjs.map +1 -1
  112. package/dist/core/operators/throttle.d.mts +81 -0
  113. package/dist/core/operators/throttle.d.mts.map +1 -0
  114. package/dist/core/operators/throttle.mjs +126 -0
  115. package/dist/core/operators/throttle.mjs.map +1 -0
  116. package/dist/core/operators/with-buffered-from.d.mts +9 -9
  117. package/dist/core/operators/with-buffered-from.mjs +12 -12
  118. package/dist/core/operators/with-buffered-from.mjs.map +1 -1
  119. package/dist/core/operators/with-current-value-from.d.mts +10 -9
  120. package/dist/core/operators/with-current-value-from.d.mts.map +1 -1
  121. package/dist/core/operators/with-current-value-from.mjs +13 -12
  122. package/dist/core/operators/with-current-value-from.mjs.map +1 -1
  123. package/dist/core/operators/with-initial-value.d.mts +5 -5
  124. package/dist/core/operators/with-initial-value.mjs +8 -8
  125. package/dist/core/operators/with-initial-value.mjs.map +1 -1
  126. package/dist/core/predefined/index.mjs +0 -1
  127. package/dist/core/predefined/index.mjs.map +1 -1
  128. package/dist/core/predefined/operators/attach-index.d.mts +50 -0
  129. package/dist/core/predefined/operators/attach-index.d.mts.map +1 -1
  130. package/dist/core/predefined/operators/attach-index.mjs +51 -2
  131. package/dist/core/predefined/operators/attach-index.mjs.map +1 -1
  132. package/dist/core/predefined/operators/index.d.mts +0 -1
  133. package/dist/core/predefined/operators/index.d.mts.map +1 -1
  134. package/dist/core/predefined/operators/index.mjs +0 -1
  135. package/dist/core/predefined/operators/index.mjs.map +1 -1
  136. package/dist/core/predefined/operators/map-optional.d.mts +48 -1
  137. package/dist/core/predefined/operators/map-optional.d.mts.map +1 -1
  138. package/dist/core/predefined/operators/map-optional.mjs +49 -1
  139. package/dist/core/predefined/operators/map-optional.mjs.map +1 -1
  140. package/dist/core/predefined/operators/map-result-err.d.mts +48 -1
  141. package/dist/core/predefined/operators/map-result-err.d.mts.map +1 -1
  142. package/dist/core/predefined/operators/map-result-err.mjs +49 -1
  143. package/dist/core/predefined/operators/map-result-err.mjs.map +1 -1
  144. package/dist/core/predefined/operators/map-result-ok.d.mts +48 -1
  145. package/dist/core/predefined/operators/map-result-ok.d.mts.map +1 -1
  146. package/dist/core/predefined/operators/map-result-ok.mjs +49 -1
  147. package/dist/core/predefined/operators/map-result-ok.mjs.map +1 -1
  148. package/dist/core/predefined/operators/map-to.d.mts +40 -0
  149. package/dist/core/predefined/operators/map-to.d.mts.map +1 -1
  150. package/dist/core/predefined/operators/map-to.mjs +43 -1
  151. package/dist/core/predefined/operators/map-to.mjs.map +1 -1
  152. package/dist/core/predefined/operators/pluck.d.mts +39 -0
  153. package/dist/core/predefined/operators/pluck.d.mts.map +1 -1
  154. package/dist/core/predefined/operators/pluck.mjs +42 -1
  155. package/dist/core/predefined/operators/pluck.mjs.map +1 -1
  156. package/dist/core/predefined/operators/skip.d.mts +48 -0
  157. package/dist/core/predefined/operators/skip.d.mts.map +1 -1
  158. package/dist/core/predefined/operators/skip.mjs +47 -0
  159. package/dist/core/predefined/operators/skip.mjs.map +1 -1
  160. package/dist/core/predefined/operators/take.d.mts +42 -0
  161. package/dist/core/predefined/operators/take.d.mts.map +1 -1
  162. package/dist/core/predefined/operators/take.mjs +41 -0
  163. package/dist/core/predefined/operators/take.mjs.map +1 -1
  164. package/dist/core/predefined/operators/unwrap-optional.d.mts +41 -1
  165. package/dist/core/predefined/operators/unwrap-optional.d.mts.map +1 -1
  166. package/dist/core/predefined/operators/unwrap-optional.mjs +42 -1
  167. package/dist/core/predefined/operators/unwrap-optional.mjs.map +1 -1
  168. package/dist/core/predefined/operators/unwrap-result-err.d.mts +41 -1
  169. package/dist/core/predefined/operators/unwrap-result-err.d.mts.map +1 -1
  170. package/dist/core/predefined/operators/unwrap-result-err.mjs +42 -1
  171. package/dist/core/predefined/operators/unwrap-result-err.mjs.map +1 -1
  172. package/dist/core/predefined/operators/unwrap-result-ok.d.mts +41 -1
  173. package/dist/core/predefined/operators/unwrap-result-ok.d.mts.map +1 -1
  174. package/dist/core/predefined/operators/unwrap-result-ok.mjs +42 -1
  175. package/dist/core/predefined/operators/unwrap-result-ok.mjs.map +1 -1
  176. package/dist/core/types/id.d.mts +2 -1
  177. package/dist/core/types/id.d.mts.map +1 -1
  178. package/dist/core/types/index.d.mts +1 -0
  179. package/dist/core/types/index.d.mts.map +1 -1
  180. package/dist/core/types/observable-family.d.mts +10 -14
  181. package/dist/core/types/observable-family.d.mts.map +1 -1
  182. package/dist/core/types/observable-kind.d.mts +1 -0
  183. package/dist/core/types/observable-kind.d.mts.map +1 -1
  184. package/dist/core/types/observable.d.mts +5 -3
  185. package/dist/core/types/observable.d.mts.map +1 -1
  186. package/dist/core/types/observable.mjs.map +1 -1
  187. package/dist/core/types/timer.d.mts +2 -0
  188. package/dist/core/types/timer.d.mts.map +1 -0
  189. package/dist/core/types/timer.mjs +2 -0
  190. package/dist/core/types/timer.mjs.map +1 -0
  191. package/dist/core/utils/id-maker.d.mts +2 -2
  192. package/dist/core/utils/id-maker.d.mts.map +1 -1
  193. package/dist/core/utils/id-maker.mjs +3 -3
  194. package/dist/core/utils/id-maker.mjs.map +1 -1
  195. package/dist/core/utils/index.mjs +1 -1
  196. package/dist/core/utils/utils.d.mts +2 -0
  197. package/dist/core/utils/utils.d.mts.map +1 -1
  198. package/dist/core/utils/utils.mjs.map +1 -1
  199. package/dist/entry-point.mjs +11 -14
  200. package/dist/entry-point.mjs.map +1 -1
  201. package/dist/index.mjs +11 -14
  202. package/dist/index.mjs.map +1 -1
  203. package/dist/types.d.mts +1 -2
  204. package/dist/utils/collect-to-array.d.mts +3 -0
  205. package/dist/utils/collect-to-array.d.mts.map +1 -0
  206. package/dist/utils/collect-to-array.mjs +11 -0
  207. package/dist/utils/collect-to-array.mjs.map +1 -0
  208. package/dist/utils/create-boolean-state.d.mts +40 -0
  209. package/dist/utils/create-boolean-state.d.mts.map +1 -0
  210. package/dist/utils/create-boolean-state.mjs +53 -0
  211. package/dist/utils/create-boolean-state.mjs.map +1 -0
  212. package/dist/utils/create-event-emitter.d.mts +4 -4
  213. package/dist/utils/create-event-emitter.mjs +4 -4
  214. package/dist/utils/create-reducer.d.mts +11 -7
  215. package/dist/utils/create-reducer.d.mts.map +1 -1
  216. package/dist/utils/create-reducer.mjs +7 -7
  217. package/dist/utils/create-reducer.mjs.map +1 -1
  218. package/dist/utils/create-state.d.mts +8 -48
  219. package/dist/utils/create-state.d.mts.map +1 -1
  220. package/dist/utils/create-state.mjs +10 -60
  221. package/dist/utils/create-state.mjs.map +1 -1
  222. package/dist/utils/index.d.mts +2 -0
  223. package/dist/utils/index.d.mts.map +1 -1
  224. package/dist/utils/index.mjs +3 -1
  225. package/dist/utils/index.mjs.map +1 -1
  226. package/package.json +21 -14
  227. package/src/core/class/child-observable-class.mts +68 -14
  228. package/src/core/class/circular-dependency-comparison.test.mts +142 -0
  229. package/src/core/class/circular-dependency.test.mts +251 -0
  230. package/src/core/class/observable-base-class.mts +10 -9
  231. package/src/core/class/root-observable-class.mts +15 -10
  232. package/src/core/combine/combine.mts +14 -13
  233. package/src/core/combine/merge.mts +14 -14
  234. package/src/core/combine/zip.mts +27 -25
  235. package/src/core/create/{interval.mts → counter.mts} +32 -30
  236. package/src/core/create/from-abortable-promise.mts +83 -0
  237. package/src/core/create/from-promise.mts +10 -7
  238. package/src/core/create/from-subscribable.mts +4 -4
  239. package/src/core/create/index.mts +3 -3
  240. package/src/core/create/just.mts +43 -0
  241. package/src/core/create/source.mts +10 -14
  242. package/src/core/create/timer.mts +12 -11
  243. package/src/core/index.mts +0 -1
  244. package/src/core/operators/audit.mts +172 -0
  245. package/src/core/operators/debounce.mts +154 -0
  246. package/src/core/operators/filter.mts +9 -9
  247. package/src/core/operators/index.mts +4 -4
  248. package/src/core/operators/{map-with-index.mts → map.mts} +20 -20
  249. package/src/core/operators/merge-map.mts +59 -32
  250. package/src/core/operators/pairwise.mts +10 -10
  251. package/src/core/operators/scan.mts +10 -10
  252. package/src/core/operators/skip-if-no-change.mts +24 -12
  253. package/src/core/operators/skip-until.mts +9 -9
  254. package/src/core/operators/skip-while.mts +29 -12
  255. package/src/core/operators/switch-map.mts +60 -29
  256. package/src/core/operators/take-until.mts +9 -9
  257. package/src/core/operators/take-while.mts +19 -11
  258. package/src/core/operators/{throttle-time.mts → throttle.mts} +58 -38
  259. package/src/core/operators/with-buffered-from.mts +13 -13
  260. package/src/core/operators/with-current-value-from.mts +14 -13
  261. package/src/core/operators/with-initial-value.mts +9 -9
  262. package/src/core/predefined/operators/attach-index.mts +52 -2
  263. package/src/core/predefined/operators/index.mts +0 -1
  264. package/src/core/predefined/operators/map-optional.mts +49 -2
  265. package/src/core/predefined/operators/map-result-err.mts +49 -2
  266. package/src/core/predefined/operators/map-result-ok.mts +49 -2
  267. package/src/core/predefined/operators/map-to.mts +41 -1
  268. package/src/core/predefined/operators/pluck.mts +40 -1
  269. package/src/core/predefined/operators/skip.mts +48 -0
  270. package/src/core/predefined/operators/take.mts +42 -0
  271. package/src/core/predefined/operators/unwrap-optional.mts +43 -2
  272. package/src/core/predefined/operators/unwrap-result-err.mts +43 -2
  273. package/src/core/predefined/operators/unwrap-result-ok.mts +43 -2
  274. package/src/core/types/id.mts +3 -1
  275. package/src/core/types/index.mts +1 -0
  276. package/src/core/types/observable-family.mts +13 -24
  277. package/src/core/types/observable-kind.mts +2 -0
  278. package/src/core/types/observable.mts +5 -4
  279. package/src/core/types/timer.mts +2 -0
  280. package/src/core/utils/id-maker.mts +4 -4
  281. package/src/core/utils/utils.mts +1 -0
  282. package/src/utils/collect-to-array.mts +17 -0
  283. package/src/utils/create-boolean-state.mts +68 -0
  284. package/src/utils/create-event-emitter.mts +4 -4
  285. package/src/utils/create-reducer.mts +12 -8
  286. package/src/utils/create-state.mts +10 -75
  287. package/src/utils/index.mts +2 -0
  288. package/dist/core/create/from-array.d.mts +0 -39
  289. package/dist/core/create/from-array.d.mts.map +0 -1
  290. package/dist/core/create/from-array.mjs +0 -65
  291. package/dist/core/create/from-array.mjs.map +0 -1
  292. package/dist/core/create/interval.d.mts.map +0 -1
  293. package/dist/core/create/interval.mjs.map +0 -1
  294. package/dist/core/create/of.d.mts +0 -39
  295. package/dist/core/create/of.d.mts.map +0 -1
  296. package/dist/core/create/of.mjs +0 -63
  297. package/dist/core/create/of.mjs.map +0 -1
  298. package/dist/core/operators/audit-time.d.mts +0 -62
  299. package/dist/core/operators/audit-time.d.mts.map +0 -1
  300. package/dist/core/operators/audit-time.mjs +0 -109
  301. package/dist/core/operators/audit-time.mjs.map +0 -1
  302. package/dist/core/operators/debounce-time.d.mts +0 -51
  303. package/dist/core/operators/debounce-time.d.mts.map +0 -1
  304. package/dist/core/operators/debounce-time.mjs +0 -93
  305. package/dist/core/operators/debounce-time.mjs.map +0 -1
  306. package/dist/core/operators/map-with-index.d.mts.map +0 -1
  307. package/dist/core/operators/map-with-index.mjs.map +0 -1
  308. package/dist/core/operators/throttle-time.d.mts +0 -62
  309. package/dist/core/operators/throttle-time.d.mts.map +0 -1
  310. package/dist/core/operators/throttle-time.mjs +0 -107
  311. package/dist/core/operators/throttle-time.mjs.map +0 -1
  312. package/dist/core/predefined/operators/map.d.mts +0 -3
  313. package/dist/core/predefined/operators/map.d.mts.map +0 -1
  314. package/dist/core/predefined/operators/map.mjs +0 -8
  315. package/dist/core/predefined/operators/map.mjs.map +0 -1
  316. package/dist/globals.d.mts +0 -4
  317. package/src/core/create/from-array.mts +0 -76
  318. package/src/core/create/of.mts +0 -73
  319. package/src/core/operators/audit-time.mts +0 -136
  320. package/src/core/operators/debounce-time.mts +0 -116
  321. package/src/core/predefined/operators/map.mts +0 -5
  322. package/src/globals.d.mts +0 -4
  323. /package/assets/{synstate-icon.png → old/synstate-icon.png} +0 -0
@@ -1,5 +1,7 @@
1
- import { Arr } from 'ts-data-forge';
1
+ import { Arr, type Some } from 'ts-data-forge';
2
+ import { type MutableSet } from 'ts-type-forge';
2
3
  import {
4
+ isChildObservable,
3
5
  isManagerObservable,
4
6
  type AsyncChildObservable,
5
7
  type ChildObservable,
@@ -14,13 +16,64 @@ import {
14
16
  type WithInitialValueOperator,
15
17
  type Wrap,
16
18
  } from '../types/index.mjs';
17
- import { binarySearch, issueUpdaterSymbol, maxDepth } from '../utils/index.mjs';
19
+ import { binarySearch, issueUpdateToken, maxDepth } from '../utils/index.mjs';
18
20
  import { ObservableBaseClass } from './observable-base-class.mjs';
19
21
 
22
+ /**
23
+ * Detects circular dependencies by walking the full ancestor chain of the
24
+ * given parents and checking whether `child` already appears among them.
25
+ *
26
+ * @throws {Error} if a circular dependency is detected
27
+ */
28
+ const hasCircularDependencyFrom = (
29
+ node: Observable<unknown>,
30
+ mut_visited: MutableSet<ObservableId>,
31
+ mut_inPath: MutableSet<ObservableId>,
32
+ ): boolean => {
33
+ if (mut_inPath.has(node.id)) return true;
34
+
35
+ if (mut_visited.has(node.id)) return false;
36
+
37
+ mut_visited.add(node.id);
38
+
39
+ mut_inPath.add(node.id);
40
+
41
+ if (isChildObservable(node)) {
42
+ for (const parent of node.parents) {
43
+ if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
44
+ return true;
45
+ }
46
+ }
47
+ }
48
+
49
+ mut_inPath.delete(node.id);
50
+
51
+ return false;
52
+ };
53
+
54
+ const detectCircularDependency = (
55
+ child: ChildObservable<unknown>,
56
+ parents: readonly Observable<unknown>[],
57
+ ): void => {
58
+ const mut_visited = new Set<ObservableId>();
59
+
60
+ const mut_inPath = new Set<ObservableId>([child.id]);
61
+
62
+ for (const parent of parents) {
63
+ if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
64
+ throw new Error(
65
+ 'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
66
+ );
67
+ }
68
+ }
69
+ };
70
+
20
71
  const registerChild = <A,>(
21
72
  child: ChildObservable<A>,
22
73
  parents: ChildObservable<A>['parents'],
23
74
  ): void => {
75
+ detectCircularDependency(child, parents);
76
+
24
77
  for (const p of parents) {
25
78
  p.addChild(child);
26
79
  }
@@ -76,7 +129,7 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
76
129
  implements AsyncChildObservable<A, P>
77
130
  {
78
131
  readonly parents;
79
- #mut_procedure: readonly ChildObservable<unknown>[];
132
+ #mut_propagationOrder: readonly ChildObservable<unknown>[];
80
133
  protected readonly descendantsIdSet: MutableSet<ObservableId>;
81
134
 
82
135
  constructor({
@@ -96,7 +149,7 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
96
149
 
97
150
  this.parents = parents;
98
151
 
99
- this.#mut_procedure = [];
152
+ this.#mut_propagationOrder = [];
100
153
 
101
154
  this.descendantsIdSet = new Set<ObservableId>();
102
155
 
@@ -110,20 +163,24 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
110
163
  this.descendantsIdSet.add(child.id);
111
164
 
112
165
  const insertPos = binarySearch(
113
- this.#mut_procedure.map((a) => a.depth),
166
+ this.#mut_propagationOrder.map((a) => a.depth),
114
167
  child.depth,
115
168
  );
116
169
 
117
- this.#mut_procedure = Arr.toInserted(this.#mut_procedure, insertPos, child);
170
+ this.#mut_propagationOrder = Arr.toInserted(
171
+ this.#mut_propagationOrder,
172
+ insertPos,
173
+ child,
174
+ );
118
175
  }
119
176
 
120
177
  startUpdate(nextValue: A): void {
121
- const updaterSymbol = issueUpdaterSymbol();
178
+ const updateToken = issueUpdateToken();
122
179
 
123
- this.setNext(nextValue, updaterSymbol);
180
+ this.setNext(nextValue, updateToken);
124
181
 
125
- for (const p of this.#mut_procedure) {
126
- p.tryUpdate(updaterSymbol);
182
+ for (const p of this.#mut_propagationOrder) {
183
+ p.tryUpdate(updateToken);
127
184
  }
128
185
  }
129
186
 
@@ -224,9 +281,6 @@ export class InitializedSyncChildObservableClass<
224
281
 
225
282
  override pipe<B>(operator: Operator<A, B>): Observable<B>;
226
283
  override pipe<B>(operator: Operator<A, B>): Observable<B> {
227
- return operator(
228
- // eslint-disable-next-line total-functions/no-unsafe-type-assertion
229
- this as unknown as InitializedObservable<A>,
230
- );
284
+ return operator(this);
231
285
  }
232
286
  }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Comparison tests: how SynState, RxJS, and Jotai handle circular dependencies.
3
+ *
4
+ * - SynState: detects cycles at construction time and throws a clear error.
5
+ * - RxJS: pipe() always returns a new Observable, so static cycles cannot be
6
+ * expressed through the public API. No detection is needed or provided.
7
+ * - Jotai: circular atom definitions are silently accepted. A cycle manifests
8
+ * only at read time as a `Maximum call stack size exceeded` error.
9
+ */
10
+ /* eslint-disable functional/immutable-data */
11
+ /* eslint-disable no-new */
12
+ import { type Atom, atom, createStore } from 'jotai';
13
+ import { BehaviorSubject, combineLatest, map as rxMap } from 'rxjs';
14
+ import { Optional } from 'ts-data-forge';
15
+ import { combine } from '../combine/index.mjs';
16
+ import { source } from '../create/index.mjs';
17
+ import { map } from '../operators/index.mjs';
18
+ import { SyncChildObservableClass } from './child-observable-class.mjs';
19
+ import { RootObservableClass } from './root-observable-class.mjs';
20
+
21
+ describe('circular dependency comparison', () => {
22
+ describe('SynState', () => {
23
+ test('detects cycle at construction time with a clear error message', () => {
24
+ const root = new RootObservableClass({
25
+ initialValue: Optional.some(0),
26
+ });
27
+
28
+ const childA = new SyncChildObservableClass({
29
+ parents: [root],
30
+ initialValue: Optional.some(0),
31
+ });
32
+
33
+ const childB = new SyncChildObservableClass({
34
+ parents: [childA],
35
+ initialValue: Optional.some(0),
36
+ });
37
+
38
+ // Simulate a cycle: childA → childB → childA
39
+ // In normal usage this is impossible — the check runs in every
40
+ // child constructor before the reference becomes available.
41
+ Object.defineProperty(childA, 'parents', {
42
+ value: [childB],
43
+ writable: false,
44
+ configurable: true,
45
+ });
46
+
47
+ expect(() => {
48
+ new SyncChildObservableClass({
49
+ parents: [childA],
50
+ initialValue: Optional.some(0),
51
+ });
52
+ }).toThrow(
53
+ 'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
54
+ );
55
+ });
56
+
57
+ test('accepts valid DAG (diamond dependency)', () => {
58
+ const a$ = source(0);
59
+
60
+ const b$ = a$.pipe(map((x) => x * 10));
61
+
62
+ const c$ = a$.pipe(map((x) => x * 1000));
63
+
64
+ expect(() => {
65
+ combine([b$, c$]);
66
+ }).not.toThrow();
67
+ });
68
+ });
69
+
70
+ describe('RxJS', () => {
71
+ test('cannot express static graph cycles — pipe() always creates new instances', () => {
72
+ // RxJS pipe() returns a brand-new Observable each time, so there is no
73
+ // way to construct a cycle through the public API. This test documents
74
+ // that behavior rather than asserting error detection.
75
+ const a$ = new BehaviorSubject(0);
76
+
77
+ const b$ = a$.pipe(rxMap((x) => x + 1));
78
+
79
+ // b$ is a wholly separate object; there is no "parents" link back to a$.
80
+ expect(a$).not.toBe(b$);
81
+
82
+ // A diamond dependency works but produces glitches (intermediate states).
83
+ const left$ = a$.pipe(rxMap((x) => x * 10));
84
+
85
+ const right$ = a$.pipe(rxMap((x) => x * 1000));
86
+
87
+ const combined$ = combineLatest([left$, right$]).pipe(
88
+ rxMap(([l, r]) => l + r),
89
+ );
90
+
91
+ const mut_values: number[] = [];
92
+
93
+ combined$.subscribe((v) => {
94
+ mut_values.push(v);
95
+ });
96
+
97
+ a$.next(1);
98
+
99
+ a$.next(2);
100
+
101
+ // Glitch values (10, 1020) appear between the correct values.
102
+ assert.deepStrictEqual(mut_values, [0, 10, 1010, 1020, 2020]);
103
+ });
104
+ });
105
+
106
+ describe('Jotai', () => {
107
+ test('does not detect circular atom definitions — crashes on read', () => {
108
+ const store = createStore();
109
+
110
+ // Two atoms that depend on each other — an obvious cycle.
111
+ // Explicit type annotations are required because TypeScript cannot infer
112
+ // the type of mutually-recursive initializers.
113
+ const atomA: Atom<number> = atom((get) => get(atomB) + 1);
114
+
115
+ const atomB: Atom<number> = atom((get) => get(atomA) + 1);
116
+
117
+ // Definition succeeds without any error.
118
+ // Reading triggers infinite recursion.
119
+ expect(() => {
120
+ store.get(atomA);
121
+ }).toThrow('Maximum call stack size exceeded');
122
+ });
123
+
124
+ test('handles valid DAG (diamond dependency) correctly', () => {
125
+ const store = createStore();
126
+
127
+ const base = atom(1);
128
+
129
+ const left = atom((get) => get(base) * 10);
130
+
131
+ const right = atom((get) => get(base) * 1000);
132
+
133
+ const combined = atom((get) => get(left) + get(right));
134
+
135
+ expect(store.get(combined)).toBe(1010);
136
+
137
+ store.set(base, 2);
138
+
139
+ expect(store.get(combined)).toBe(2020);
140
+ });
141
+ });
142
+ });
@@ -0,0 +1,251 @@
1
+ /* eslint-disable functional/immutable-data */
2
+ /* eslint-disable no-new */
3
+ import { Optional } from 'ts-data-forge';
4
+ import { combine, merge } from '../combine/index.mjs';
5
+ import { source } from '../create/index.mjs';
6
+ import { map } from '../operators/index.mjs';
7
+ import {
8
+ AsyncChildObservableClass,
9
+ SyncChildObservableClass,
10
+ } from './child-observable-class.mjs';
11
+ import { RootObservableClass } from './root-observable-class.mjs';
12
+
13
+ describe('circular dependency detection', () => {
14
+ describe('cycle in ancestor graph', () => {
15
+ test('should throw when parent chain contains a cycle (A -> B -> A)', () => {
16
+ const root = new RootObservableClass({
17
+ initialValue: Optional.some(0),
18
+ });
19
+
20
+ const childA = new SyncChildObservableClass({
21
+ parents: [root],
22
+ initialValue: Optional.some(0),
23
+ });
24
+
25
+ const childB = new SyncChildObservableClass({
26
+ parents: [childA],
27
+ initialValue: Optional.some(0),
28
+ });
29
+
30
+ // Mutate childA.parents to create a cycle: childA -> childB -> childA
31
+ Object.defineProperty(childA, 'parents', {
32
+ value: [childB],
33
+ writable: false,
34
+ configurable: true,
35
+ });
36
+
37
+ expect(() => {
38
+ new SyncChildObservableClass({
39
+ parents: [childA],
40
+ initialValue: Optional.some(0),
41
+ });
42
+ }).toThrow(
43
+ 'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
44
+ );
45
+ });
46
+
47
+ test('should throw when AsyncChildObservable parent chain contains a cycle', () => {
48
+ const root = new RootObservableClass({
49
+ initialValue: Optional.some(0),
50
+ });
51
+
52
+ const childA = new AsyncChildObservableClass({
53
+ parents: [root],
54
+ initialValue: Optional.some(0),
55
+ });
56
+
57
+ const childB = new SyncChildObservableClass({
58
+ parents: [childA],
59
+ initialValue: Optional.some(0),
60
+ });
61
+
62
+ // Create cycle: childA -> childB -> childA
63
+ Object.defineProperty(childA, 'parents', {
64
+ value: [childB],
65
+ writable: false,
66
+ configurable: true,
67
+ });
68
+
69
+ expect(() => {
70
+ new AsyncChildObservableClass({
71
+ parents: [childA],
72
+ initialValue: Optional.some(0),
73
+ });
74
+ }).toThrow(
75
+ 'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
76
+ );
77
+ });
78
+
79
+ test('should throw when a cycle exists through a chain of 3 observables', () => {
80
+ const root = new RootObservableClass({
81
+ initialValue: Optional.some(0),
82
+ });
83
+
84
+ const childA = new SyncChildObservableClass({
85
+ parents: [root],
86
+ initialValue: Optional.some(0),
87
+ });
88
+
89
+ const childB = new SyncChildObservableClass({
90
+ parents: [childA],
91
+ initialValue: Optional.some(0),
92
+ });
93
+
94
+ const childC = new SyncChildObservableClass({
95
+ parents: [childB],
96
+ initialValue: Optional.some(0),
97
+ });
98
+
99
+ // Create cycle: childA -> childC -> childB -> childA
100
+ Object.defineProperty(childA, 'parents', {
101
+ value: [childC],
102
+ writable: false,
103
+ configurable: true,
104
+ });
105
+
106
+ expect(() => {
107
+ new SyncChildObservableClass({
108
+ parents: [childA],
109
+ initialValue: Optional.some(0),
110
+ });
111
+ }).toThrow(
112
+ 'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
113
+ );
114
+ });
115
+
116
+ test('should throw when the new child itself appears as an ancestor', () => {
117
+ const root = new RootObservableClass({
118
+ initialValue: Optional.some(0),
119
+ });
120
+
121
+ const childA = new SyncChildObservableClass({
122
+ parents: [root],
123
+ initialValue: Optional.some(0),
124
+ });
125
+
126
+ // Make childA's parents reference childA itself (self-loop)
127
+ Object.defineProperty(childA, 'parents', {
128
+ value: [childA],
129
+ writable: false,
130
+ configurable: true,
131
+ });
132
+
133
+ expect(() => {
134
+ new SyncChildObservableClass({
135
+ parents: [childA],
136
+ initialValue: Optional.some(0),
137
+ });
138
+ }).toThrow(
139
+ 'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
140
+ );
141
+ });
142
+ });
143
+
144
+ describe('valid DAG patterns should not throw', () => {
145
+ test('diamond dependency is not a cycle', () => {
146
+ const root = new RootObservableClass({
147
+ initialValue: Optional.some(0),
148
+ });
149
+
150
+ const left = new SyncChildObservableClass({
151
+ parents: [root],
152
+ initialValue: Optional.some(0),
153
+ });
154
+
155
+ const right = new SyncChildObservableClass({
156
+ parents: [root],
157
+ initialValue: Optional.some(0),
158
+ });
159
+
160
+ // Diamond: root -> left -> combined, root -> right -> combined
161
+ expect(() => {
162
+ new SyncChildObservableClass({
163
+ parents: [left, right],
164
+ initialValue: Optional.some(0),
165
+ });
166
+ }).not.toThrow();
167
+ });
168
+
169
+ test('linear chain is not a cycle', () => {
170
+ const root = new RootObservableClass({
171
+ initialValue: Optional.some(0),
172
+ });
173
+
174
+ const a = new SyncChildObservableClass({
175
+ parents: [root],
176
+ initialValue: Optional.some(0),
177
+ });
178
+
179
+ const b = new SyncChildObservableClass({
180
+ parents: [a],
181
+ initialValue: Optional.some(0),
182
+ });
183
+
184
+ expect(() => {
185
+ new SyncChildObservableClass({
186
+ parents: [b],
187
+ initialValue: Optional.some(0),
188
+ });
189
+ }).not.toThrow();
190
+ });
191
+
192
+ test('multiple roots converging is not a cycle', () => {
193
+ const root1 = new RootObservableClass({
194
+ initialValue: Optional.some(1),
195
+ });
196
+
197
+ const root2 = new RootObservableClass({
198
+ initialValue: Optional.some(2),
199
+ });
200
+
201
+ const child1 = new SyncChildObservableClass({
202
+ parents: [root1],
203
+ initialValue: Optional.some(0),
204
+ });
205
+
206
+ const child2 = new SyncChildObservableClass({
207
+ parents: [root2],
208
+ initialValue: Optional.some(0),
209
+ });
210
+
211
+ expect(() => {
212
+ new SyncChildObservableClass({
213
+ parents: [child1, child2],
214
+ initialValue: Optional.some(0),
215
+ });
216
+ }).not.toThrow();
217
+ });
218
+
219
+ test('combine with source observables works', () => {
220
+ const a$ = source<number>();
221
+
222
+ const b$ = source<string>();
223
+
224
+ expect(() => {
225
+ combine([a$, b$]);
226
+ }).not.toThrow();
227
+ });
228
+
229
+ test('combine with derived observables works', () => {
230
+ const a$ = source<number>();
231
+
232
+ const b$ = source<number>();
233
+
234
+ const mapped$ = a$.pipe(map((x) => x * 2));
235
+
236
+ expect(() => {
237
+ combine([mapped$, b$]);
238
+ }).not.toThrow();
239
+ });
240
+
241
+ test('merge with source observables works', () => {
242
+ const a$ = source<number>();
243
+
244
+ const b$ = source<number>();
245
+
246
+ expect(() => {
247
+ merge([a$, b$]);
248
+ }).not.toThrow();
249
+ });
250
+ });
251
+ });
@@ -1,4 +1,5 @@
1
1
  import { Arr, Optional } from 'ts-data-forge';
2
+ import { type MutableMap } from 'ts-type-forge';
2
3
  import {
3
4
  type ChildObservable,
4
5
  type InitializedObservable,
@@ -8,13 +9,13 @@ import {
8
9
  type Subscriber,
9
10
  type SubscriberId,
10
11
  type Subscription,
11
- type UpdaterSymbol,
12
+ type UpdateToken,
12
13
  type WithInitialValueOperator,
13
14
  } from '../types/index.mjs';
14
15
  import {
15
16
  issueObservableId,
16
17
  issueSubscriberId,
17
- issueUpdaterSymbol,
18
+ issueUpdateToken,
18
19
  toSubscriber,
19
20
  } from '../utils/index.mjs';
20
21
 
@@ -30,7 +31,7 @@ export class ObservableBaseClass<
30
31
  readonly #subscribers: MutableMap<SubscriberId, Subscriber<A>>;
31
32
  #mut_currentValue: ReturnType<ObservableBase<A>['getSnapshot']>;
32
33
  #mut_isCompleted: ObservableBase<A>['isCompleted'];
33
- #mut_updaterSymbol: ObservableBase<A>['updaterSymbol'];
34
+ #mut_updateToken: ObservableBase<A>['updateToken'];
34
35
 
35
36
  constructor({
36
37
  kind,
@@ -55,7 +56,7 @@ export class ObservableBaseClass<
55
56
 
56
57
  this.#mut_isCompleted = false;
57
58
 
58
- this.#mut_updaterSymbol = issueUpdaterSymbol();
59
+ this.#mut_updateToken = issueUpdateToken();
59
60
  }
60
61
 
61
62
  addChild<B>(child: ChildObservable<B>): void {
@@ -78,8 +79,8 @@ export class ObservableBaseClass<
78
79
  return this.#mut_isCompleted;
79
80
  }
80
81
 
81
- get updaterSymbol(): UpdaterSymbol {
82
- return this.#mut_updaterSymbol;
82
+ get updateToken(): UpdateToken {
83
+ return this.#mut_updateToken;
83
84
  }
84
85
 
85
86
  get hasSubscriber(): boolean {
@@ -94,8 +95,8 @@ export class ObservableBaseClass<
94
95
  return this.#mut_children.some((c) => !c.isCompleted);
95
96
  }
96
97
 
97
- protected setNext(nextValue: A, updaterSymbol: UpdaterSymbol): void {
98
- this.#mut_updaterSymbol = updaterSymbol;
98
+ protected setNext(nextValue: A, updateToken: UpdateToken): void {
99
+ this.#mut_updateToken = updateToken;
99
100
 
100
101
  this.#mut_currentValue = Optional.some(nextValue);
101
102
 
@@ -105,7 +106,7 @@ export class ObservableBaseClass<
105
106
  }
106
107
 
107
108
  // eslint-disable-next-line @typescript-eslint/class-methods-use-this
108
- tryUpdate(_updaterSymbol: UpdaterSymbol): void {
109
+ tryUpdate(_updateToken: UpdateToken): void {
109
110
  throw new Error('not implemented');
110
111
  }
111
112
 
@@ -1,24 +1,25 @@
1
1
  import { Arr, Optional } from 'ts-data-forge';
2
+ import { type MutableSet } from 'ts-type-forge';
2
3
  import {
3
4
  isRootObservable,
4
5
  type ChildObservable,
5
6
  type ObservableId,
6
7
  type RootObservable,
7
8
  } from '../types/index.mjs';
8
- import { binarySearch, issueUpdaterSymbol } from '../utils/index.mjs';
9
+ import { binarySearch, issueUpdateToken } from '../utils/index.mjs';
9
10
  import { ObservableBaseClass } from './observable-base-class.mjs';
10
11
 
11
12
  export class RootObservableClass<A>
12
13
  extends ObservableBaseClass<A, 'root', 0>
13
14
  implements RootObservable<A>
14
15
  {
15
- #mut_procedure: readonly ChildObservable<unknown>[];
16
+ #mut_propagationOrder: readonly ChildObservable<unknown>[];
16
17
  protected readonly _descendantsIdSet: MutableSet<ObservableId>;
17
18
 
18
19
  constructor({
19
20
  initialValue,
20
21
  }: Readonly<{
21
- initialValue: ReturnType<RootObservable<A>['getSnapshot']>;
22
+ initialValue: Optional<A>;
22
23
  }>) {
23
24
  super({
24
25
  kind: 'root',
@@ -26,7 +27,7 @@ export class RootObservableClass<A>
26
27
  initialValue,
27
28
  });
28
29
 
29
- this.#mut_procedure = [];
30
+ this.#mut_propagationOrder = [];
30
31
 
31
32
  this._descendantsIdSet = new Set<ObservableId>();
32
33
  }
@@ -37,20 +38,24 @@ export class RootObservableClass<A>
37
38
  this._descendantsIdSet.add(child.id);
38
39
 
39
40
  const insertPos = binarySearch(
40
- this.#mut_procedure.map((a) => a.depth),
41
+ this.#mut_propagationOrder.map((a) => a.depth),
41
42
  child.depth,
42
43
  );
43
44
 
44
- this.#mut_procedure = Arr.toInserted(this.#mut_procedure, insertPos, child);
45
+ this.#mut_propagationOrder = Arr.toInserted(
46
+ this.#mut_propagationOrder,
47
+ insertPos,
48
+ child,
49
+ );
45
50
  }
46
51
 
47
52
  startUpdate(nextValue: A): void {
48
- const updaterSymbol = issueUpdaterSymbol();
53
+ const updateToken = issueUpdateToken();
49
54
 
50
- this.setNext(nextValue, updaterSymbol);
55
+ this.setNext(nextValue, updateToken);
51
56
 
52
- for (const p of this.#mut_procedure) {
53
- p.tryUpdate(updaterSymbol);
57
+ for (const p of this.#mut_propagationOrder) {
58
+ p.tryUpdate(updateToken);
54
59
  }
55
60
  }
56
61
  }