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,107 @@
1
+ // @sumi-expect-error-file classes/no-class
2
+ // @sumi-expect-error-file banned-syntax/no-this
3
+ // @sumi-expect-error-file functions/prefer-arrow-function
4
+ // @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
5
+ import { Arr, type Optional } from 'ts-data-forge';
6
+ import type { ReadonlyRecord } from 'ts-type-forge';
7
+ import type { RootObservable } from '../types/index.mjs';
8
+ import { RootObservableClass } from './root-observable-class.mjs';
9
+
10
+ /**
11
+ * The tools a custom root's `init` receives: the `protected` methods a
12
+ * subclass of `RootObservableClass` inherits.
13
+ */
14
+ export type RootObservableTools<A> = Readonly<{
15
+ /** Emits `nextValue` and propagates the update to all descendants. */
16
+ startUpdate: (nextValue: A) => void;
17
+
18
+ getSnapshot: () => Optional<A>;
19
+
20
+ isCompleted: () => boolean;
21
+
22
+ /** Completes this observable (leaf teardown included). */
23
+ complete: () => void;
24
+
25
+ /**
26
+ * Registers a teardown (clear timers, abort requests, remove listeners, …)
27
+ * to run at the beginning of `complete()`, in the order registered. Called
28
+ * from `init`, it lets the teardown close over what `init` creates.
29
+ * Registered once the observable has completed — `init` may complete it
30
+ * synchronously first — the teardown runs at once.
31
+ */
32
+ onComplete: (teardown: () => void) => void;
33
+ }>;
34
+
35
+ export type RootObservableConfig<A> = Readonly<{
36
+ initialValue: Optional<A>;
37
+ }>;
38
+
39
+ /**
40
+ * Creates a root observable, for a custom creator written without a class
41
+ * of its own.
42
+ *
43
+ * `init` runs during construction, receives the tools, and returns extra
44
+ * public members to merge into the observable (e.g. `next` for a source,
45
+ * `start` for a timer). It may register asynchronous callbacks that use the
46
+ * tools, and may call `complete` synchronously.
47
+ */
48
+ export const createRootObservable = <
49
+ A,
50
+ // The default is deliberately empty: a root without `init` adds no members.
51
+ // eslint-disable-next-line @typescript-eslint/no-generated-empty-object-type
52
+ Extension extends ReadonlyRecord<string, unknown> = ReadonlyRecord<
53
+ never,
54
+ never
55
+ >,
56
+ >(
57
+ { initialValue }: RootObservableConfig<A>,
58
+ init: (tools: RootObservableTools<A>) => Extension,
59
+ ): Extension & RootObservable<A> =>
60
+ // The members `init` returned are assigned to the observable.
61
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
62
+ new CustomRootObservableClass(initialValue, init) as unknown as Extension &
63
+ RootObservable<A>;
64
+
65
+ /* implementation */
66
+
67
+ class CustomRootObservableClass<A> extends RootObservableClass<A> {
68
+ #mut_teardowns: readonly (() => void)[] | undefined;
69
+
70
+ constructor(
71
+ initialValue: Optional<A>,
72
+ init: (tools: RootObservableTools<A>) => ReadonlyRecord<string, unknown>,
73
+ ) {
74
+ super({ initialValue });
75
+
76
+ this.#mut_teardowns = undefined;
77
+
78
+ const extension = init({
79
+ startUpdate: (nextValue) => {
80
+ this.startUpdate(nextValue);
81
+ },
82
+ getSnapshot: () => this.getSnapshot(),
83
+ isCompleted: () => this.isCompleted,
84
+ complete: () => {
85
+ this.complete();
86
+ },
87
+ onComplete: (teardown) => {
88
+ if (this.isCompleted) {
89
+ teardown();
90
+ } else {
91
+ this.#mut_teardowns = Arr.toPushed(
92
+ this.#mut_teardowns ?? [],
93
+ teardown,
94
+ );
95
+ }
96
+ },
97
+ });
98
+
99
+ Object.assign(this, extension);
100
+ }
101
+
102
+ protected override teardown(): void {
103
+ for (const teardown of this.#mut_teardowns ?? []) {
104
+ teardown();
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,8 @@
1
+ export * from './child-observable-class.mjs';
2
+ export * from './create-child-observable.mjs';
3
+ export * from './create-root-observable.mjs';
4
+ export * from './manager-observable-class.mjs';
5
+ export * from './observable-base-class.mjs';
6
+ export * from './observable-node.mjs';
7
+ export * from './propagation-state.mjs';
8
+ export * from './root-observable-class.mjs';
@@ -0,0 +1,166 @@
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 type { Optional } from 'ts-data-forge';
6
+ import type { MutableSet } from 'ts-type-forge';
7
+ import type { ManagerObservableKind, UpdateToken } from '../types/index.mjs';
8
+ import { issueUpdateToken } from '../utils/index.mjs';
9
+ import { ObservableBaseClass } from './observable-base-class.mjs';
10
+ import type { ChildNode, ManagerNode } from './observable-node.mjs';
11
+ import { beginPropagation, endPropagation } from './propagation-state.mjs';
12
+
13
+ /**
14
+ * The base of the observables that start updates of their own: a root, and
15
+ * an async child (`debounce`, `switchMap`), which emits later than its
16
+ * parents do. Every descendant registers to the managers it is reached from,
17
+ * and an update reaches them in ascending depth, so that each sees its
18
+ * parents' values of that update (no glitch).
19
+ */
20
+ export class ManagerObservableClass<
21
+ A,
22
+ Kind extends ManagerObservableKind,
23
+ Depth extends number,
24
+ > extends ObservableBaseClass<A, Kind, Depth> {
25
+ /**
26
+ * The descendants, by depth: the set at index `i` holds those at
27
+ * `depth + 1 + i`, in the order they registered. Depths below a live node
28
+ * never skip one (every live node has a live parent one level up), so the
29
+ * array has few holes; a hole is `undefined`, never a gap left for V8 to
30
+ * treat as a holey array.
31
+ *
32
+ * An update walks this array and its sets in place. Adding or removing a
33
+ * node is O(1) and there is no flat order to rebuild; only the first node
34
+ * of a depth replaces the array with a longer copy. A node deleted during
35
+ * the walk is skipped by the set's iterator; a node registered during it is
36
+ * also put in `#mut_joinedDuringUpdate` and skipped, so an update visits the
37
+ * nodes that existed when it began.
38
+ */
39
+ #mut_propagationOrder: readonly (MutableSet<ChildNode> | undefined)[];
40
+
41
+ /** Whether this node is propagating an update of its own. */
42
+ #mut_isPropagating: boolean;
43
+
44
+ /**
45
+ * The descendants that registered while this node propagated the update
46
+ * under way, which that update does not visit; made when the first one
47
+ * arrives. Kept on the manager rather than in a module, so that it also
48
+ * holds for a node another copy of synstate registered.
49
+ */
50
+ #mut_joinedDuringUpdate: MutableSet<ChildNode> | undefined;
51
+
52
+ /** The managers this node's descendants register to: itself. */
53
+ readonly managers: readonly ManagerNode[];
54
+
55
+ constructor(
56
+ config: Readonly<{
57
+ kind: Kind;
58
+ depth: Depth;
59
+ initialValue: Optional<A>;
60
+ }>,
61
+ ) {
62
+ super(config);
63
+
64
+ this.managers = [this];
65
+
66
+ this.#mut_propagationOrder = [];
67
+
68
+ this.#mut_isPropagating = false;
69
+
70
+ this.#mut_joinedDuringUpdate = undefined;
71
+ }
72
+
73
+ /**
74
+ * Runs `tryUpdate` on the descendants, shallowest first, skipping those
75
+ * registered after this update began.
76
+ */
77
+ #propagate(updateToken: UpdateToken): void {
78
+ for (const bucket of this.#mut_propagationOrder) {
79
+ if (bucket !== undefined) {
80
+ for (const c of bucket) {
81
+ // Read for each node, since the set is made when the first joins.
82
+ if (this.#mut_joinedDuringUpdate?.has(c) !== true) {
83
+ c.tryUpdate(updateToken);
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ addDescendant(child: ChildNode): void {
91
+ const descendants = this.#mut_propagationOrder;
92
+
93
+ const index = child.depth - this.depth - 1;
94
+
95
+ const mut_bucket = descendants[index];
96
+
97
+ if (mut_bucket === undefined) {
98
+ // The first node of its depth: rare, so the array is copied rather than
99
+ // written, which also leaves a walk already under way on the old one.
100
+ this.#mut_propagationOrder = Array.from(
101
+ { length: Math.max(descendants.length, index + 1) },
102
+ (_, i) => (i === index ? new Set([child]) : descendants[i]),
103
+ );
104
+ } else {
105
+ mut_bucket.add(child);
106
+ }
107
+
108
+ if (!this.#mut_isPropagating) {
109
+ return;
110
+ }
111
+
112
+ this.#mut_joinedDuringUpdate ??= new Set();
113
+
114
+ this.#mut_joinedDuringUpdate.add(child);
115
+ }
116
+
117
+ deleteDescendant(child: ChildNode): void {
118
+ // An emptied set stays, to be reused by the next node of its depth.
119
+ this.#mut_propagationOrder[child.depth - this.depth - 1]?.delete(child);
120
+ }
121
+
122
+ /** Emits `nextValue` and propagates it to the descendants. */
123
+ protected startUpdate(nextValue: A): void {
124
+ const updateToken = issueUpdateToken();
125
+
126
+ // A write to this same manager from inside its own update starts an inner
127
+ // one; what joins during it joined during the outer one too.
128
+ const wasPropagating = this.#mut_isPropagating;
129
+
130
+ const joinedBefore = this.#mut_joinedDuringUpdate;
131
+
132
+ this.#mut_isPropagating = true;
133
+
134
+ this.#mut_joinedDuringUpdate = undefined;
135
+
136
+ beginPropagation(updateToken);
137
+
138
+ // A subscriber or projection may throw, and the depth has to come back
139
+ // down regardless, or every later write would warn. The exception itself
140
+ // is not caught: it reaches the caller of `next` as it always has.
141
+ // @sumi-expect-error exceptions/no-try
142
+ try {
143
+ this.setNext(nextValue, updateToken);
144
+
145
+ this.#propagate(updateToken);
146
+ } finally {
147
+ // Widened: TypeScript would take the write above for the value.
148
+ const joinedNow = this.#mut_joinedDuringUpdate as
149
+ MutableSet<ChildNode> | undefined;
150
+
151
+ this.#mut_isPropagating = wasPropagating;
152
+
153
+ this.#mut_joinedDuringUpdate = joinedBefore;
154
+
155
+ if (wasPropagating && joinedNow !== undefined) {
156
+ this.#mut_joinedDuringUpdate ??= new Set();
157
+
158
+ for (const c of joinedNow) {
159
+ this.#mut_joinedDuringUpdate.add(c);
160
+ }
161
+ }
162
+
163
+ endPropagation();
164
+ }
165
+ }
166
+ }
@@ -0,0 +1,315 @@
1
+ // Sumi bans classes, `this`, method syntax and writes to `this` outside a
2
+ // `mut_` binding. The observables are classes (why is on
3
+ // `ObservableBaseClass` below), so those four 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
8
+ import { Optional } from 'ts-data-forge';
9
+ import { unreachable } from 'ts-std-forge';
10
+ import type { MutableSet } from 'ts-type-forge';
11
+ import type {
12
+ DropInitialValueOperator,
13
+ InitializedObservable,
14
+ KeepInitialValueOperator,
15
+ Observable,
16
+ ObservableBase,
17
+ ObservableKind,
18
+ Operator,
19
+ Subscriber,
20
+ Subscription,
21
+ UpdateToken,
22
+ WithInitialValueOperator,
23
+ } from '../types/index.mjs';
24
+ import {
25
+ getCurrentScopeInternals,
26
+ notUpdatedYet,
27
+ observableBrand,
28
+ toSubscriber,
29
+ type ScopeInternals,
30
+ } from '../utils/index.mjs';
31
+ import type { ChildNode } from './observable-node.mjs';
32
+
33
+ /**
34
+ * The base of every observable, and the reason an observable is a class.
35
+ *
36
+ * A node is created for every derived value of a UI, so creating one has to
37
+ * be cheap, and most of its cost was its methods: each observable used to be
38
+ * a set of closures made for it, a dozen or more per node (#2421). Sharing
39
+ * the methods among all nodes removes that cost, and a shared method has to
40
+ * read the node it is called on from `this`. Once `this` is unavoidable, a
41
+ * class is the plainest way to say it: the methods live once on the
42
+ * prototype, `this` is typed, a creator or operator is a subclass that
43
+ * overrides `tryUpdate` (and `flush` / `teardown` where it holds something),
44
+ * and assigning every field in the constructor gives the nodes of one class
45
+ * one shape for V8. The collections (`children`, subscribers) are made when
46
+ * the first one arrives.
47
+ *
48
+ * The methods read the observable they are called on, so taking one off it
49
+ * (`const { subscribe } = obs$`) is not supported.
50
+ *
51
+ * A node reaches another node only through its methods and its public
52
+ * fields, never through a `#private` one: the other node may be an instance
53
+ * of another copy of synstate in the same program, whose classes are not
54
+ * these.
55
+ */
56
+ export class ObservableBaseClass<
57
+ A,
58
+ Kind extends ObservableKind,
59
+ Depth extends number,
60
+ > implements ObservableBase<A> {
61
+ #mut_snapshot: Optional<A>;
62
+ #mut_isCompleted: boolean;
63
+ #mut_updateToken: UpdateToken;
64
+ #mut_children: MutableSet<ChildNode> | undefined;
65
+ #mut_subscribers: MutableSet<Subscriber<A>> | undefined;
66
+ #mut_scope: ScopeInternals | undefined;
67
+ readonly kind: Kind;
68
+ readonly depth: Depth;
69
+ declare readonly [observableBrand]: true;
70
+
71
+ constructor({
72
+ kind,
73
+ depth,
74
+ initialValue,
75
+ }: Readonly<{
76
+ kind: Kind;
77
+ depth: Depth;
78
+ initialValue: Optional<A>;
79
+ }>) {
80
+ this.kind = kind;
81
+
82
+ this.depth = depth;
83
+
84
+ this.#mut_snapshot = initialValue;
85
+
86
+ this.#mut_isCompleted = false;
87
+
88
+ this.#mut_updateToken = notUpdatedYet;
89
+
90
+ this.#mut_children = undefined;
91
+
92
+ this.#mut_subscribers = undefined;
93
+
94
+ this.#mut_scope = undefined;
95
+
96
+ this[observableBrand] = true;
97
+ }
98
+
99
+ getSnapshot(): Optional<A> {
100
+ return this.#mut_snapshot;
101
+ }
102
+
103
+ get isCompleted(): boolean {
104
+ return this.#mut_isCompleted;
105
+ }
106
+
107
+ get updateToken(): UpdateToken {
108
+ return this.#mut_updateToken;
109
+ }
110
+
111
+ get hasSubscriber(): boolean {
112
+ return (
113
+ this.#mut_subscribers !== undefined && this.#mut_subscribers.size > 0
114
+ );
115
+ }
116
+
117
+ get hasChild(): boolean {
118
+ return this.#mut_children !== undefined && this.#mut_children.size > 0;
119
+ }
120
+
121
+ hasActiveChild(): boolean {
122
+ if (this.#mut_children !== undefined) {
123
+ for (const c of this.#mut_children) {
124
+ if (!c.isCompleted) {
125
+ return true;
126
+ }
127
+ }
128
+ }
129
+
130
+ return false;
131
+ }
132
+
133
+ addChild(child: ChildNode): void {
134
+ // A set, not an array: a completed child removes itself, and that has to
135
+ // be O(1) for a UI that creates and completes derived nodes on every mount.
136
+ if (this.#mut_children === undefined) {
137
+ this.#mut_children = new Set([child]);
138
+ } else {
139
+ this.#mut_children.add(child);
140
+ }
141
+ }
142
+
143
+ deleteChild(child: ChildNode): void {
144
+ this.#mut_children?.delete(child);
145
+ }
146
+
147
+ /** Stores `nextValue` as the current value and notifies subscribers. */
148
+ protected setNext(nextValue: A, updateToken: UpdateToken): void {
149
+ this.#mut_updateToken = updateToken;
150
+
151
+ this.#mut_snapshot = Optional.some(nextValue);
152
+
153
+ if (this.#mut_subscribers !== undefined) {
154
+ for (const s of this.#mut_subscribers) {
155
+ s.onNext(nextValue);
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Called with the token of every update that reaches this observable; a
162
+ * parent that did not emit in that update has a different `updateToken`.
163
+ * Every observable that can receive a parent update overrides it, and an
164
+ * observable with no parents never gets one, so reaching this is the
165
+ * invariant breaking (Sumi D-48).
166
+ */
167
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
168
+ tryUpdate(_updateToken: UpdateToken): void {
169
+ unreachable();
170
+ }
171
+
172
+ /**
173
+ * Called by a parent that has completed. A root has no parents, and a child
174
+ * completing never completes it (#2133), so only its own `complete()` ends
175
+ * it.
176
+ */
177
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
178
+ tryComplete(): void {}
179
+
180
+ /**
181
+ * Completes the observable: an async child first passes on what it still
182
+ * holds (`flush`), then it is disposed. Completing never reaches the
183
+ * parents: an upstream observable may be shared, and only whoever owns it
184
+ * decides when it ends (#2133).
185
+ */
186
+ complete(): void {
187
+ if (this.#mut_isCompleted) {
188
+ return;
189
+ }
190
+
191
+ this.flush();
192
+
193
+ this.dispose();
194
+ }
195
+
196
+ /**
197
+ * Completes the observable without passing on anything it still holds,
198
+ * runs its teardown, and takes it out of the graph.
199
+ */
200
+ dispose(): void {
201
+ // A completed node is reached again through every parent that completes
202
+ // after it; running again would repeat the teardown and the unregistering.
203
+ if (this.#mut_isCompleted) {
204
+ return;
205
+ }
206
+
207
+ this.teardown();
208
+
209
+ this.#mut_isCompleted = true;
210
+
211
+ if (this.#mut_scope !== undefined) {
212
+ this.#mut_scope.release();
213
+
214
+ this.#mut_scope = undefined;
215
+ }
216
+
217
+ if (this.#mut_subscribers !== undefined) {
218
+ for (const s of this.#mut_subscribers) {
219
+ s.onComplete();
220
+ }
221
+
222
+ this.#mut_subscribers.clear();
223
+ }
224
+
225
+ // A child that completes removes itself from `children` during this loop,
226
+ // which iterating a `Set` tolerates.
227
+ if (this.#mut_children !== undefined) {
228
+ for (const c of this.#mut_children) {
229
+ c.tryComplete();
230
+ }
231
+ }
232
+
233
+ this.detach();
234
+ }
235
+
236
+ /**
237
+ * Passes on what the observable still holds, at the beginning of
238
+ * `complete()` and never in `dispose()` (`debounce`'s pending value).
239
+ */
240
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
241
+ protected flush(): void {}
242
+
243
+ /**
244
+ * Releases what the observable holds (timers, subscriptions, inner
245
+ * observables), once, when it completes or is disposed, before its
246
+ * subscribers hear of it.
247
+ */
248
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
249
+ protected teardown(): void {}
250
+
251
+ /** Takes a completed child out of its parents' and managers' lists. */
252
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
253
+ protected detach(): void {}
254
+
255
+ /**
256
+ * Joins the scope whose `run` is executing (`createScope`), if any. A node
257
+ * that completed while it was built (`just`, a child of completed parents)
258
+ * has nothing left to release.
259
+ */
260
+ protected adoptIntoCurrentScope(): void {
261
+ const scope = getCurrentScopeInternals();
262
+
263
+ if (scope === undefined || this.#mut_isCompleted) {
264
+ return;
265
+ }
266
+
267
+ scope.adopt(this);
268
+
269
+ this.#mut_scope = scope;
270
+ }
271
+
272
+ // A `WithInitialValueOperator` is also an `Operator`: nothing at run time
273
+ // tells the operators apart, and the overloads only refine the return type
274
+ // (#1952).
275
+ pipe<B>(operator: WithInitialValueOperator<A, B>): InitializedObservable<B>;
276
+
277
+ pipe<B>(
278
+ operator: DropInitialValueOperator<A, B> | KeepInitialValueOperator<A, B>,
279
+ ): Observable<B>;
280
+
281
+ pipe<B>(operator: Operator<A, B>): Observable<B> {
282
+ return operator(
283
+ // The node is the observable; the public types are only a view of it.
284
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
285
+ this as unknown as InitializedObservable<A>,
286
+ );
287
+ }
288
+
289
+ subscribe(onNext: (v: A) => void, onComplete?: () => void): Subscription {
290
+ // first emit
291
+ if (Optional.isSome(this.#mut_snapshot)) {
292
+ onNext(this.#mut_snapshot.value);
293
+ }
294
+
295
+ if (this.#mut_isCompleted) {
296
+ onComplete?.();
297
+
298
+ return noopSubscription;
299
+ }
300
+
301
+ const subscriber = toSubscriber(onNext, onComplete);
302
+
303
+ this.#mut_subscribers ??= new Set();
304
+
305
+ this.#mut_subscribers.add(subscriber);
306
+
307
+ return {
308
+ unsubscribe: () => {
309
+ this.#mut_subscribers?.delete(subscriber);
310
+ },
311
+ };
312
+ }
313
+ }
314
+
315
+ const noopSubscription: Subscription = { unsubscribe: () => {} } as const;
@@ -10,8 +10,8 @@ import type {
10
10
  import {
11
11
  createAsyncChildObservable,
12
12
  createSyncChildObservable,
13
+ createTryUpdateNotImplemented,
13
14
  } from './create-child-observable.mjs';
14
- import { createTryUpdateNotImplemented } from './create-observable-base.mjs';
15
15
  import { createRootObservable } from './create-root-observable.mjs';
16
16
 
17
17
  /**
@@ -0,0 +1,54 @@
1
+ import type {
2
+ Observable,
3
+ ObservableBase,
4
+ UpdateToken,
5
+ } from '../types/index.mjs';
6
+
7
+ /**
8
+ * The members the nodes of the graph call on one another, which the public
9
+ * types leave out (#2117). Every observable is an instance of
10
+ * `ObservableBaseClass` — of this copy of synstate or of another — and has
11
+ * them.
12
+ */
13
+ type NodeInternals = Readonly<{
14
+ hasSubscriber: boolean;
15
+ hasChild: boolean;
16
+ hasActiveChild: () => boolean;
17
+ tryUpdate: (updateToken: UpdateToken) => void;
18
+ tryComplete: () => void;
19
+ addChild: (child: ChildNode) => void;
20
+ deleteChild: (child: ChildNode) => void;
21
+
22
+ /**
23
+ * The managers this node's own descendants register to: itself for a
24
+ * manager, its parents' for a sync child, shared with its parent when it
25
+ * has one. Cached so that creating a child does not walk its ancestors.
26
+ */
27
+ managers: readonly ManagerNode[];
28
+ }>;
29
+
30
+ /** An observable, as the other nodes of the graph see it. */
31
+ export type ObservableNode<A = unknown> = NodeInternals & Observable<A>;
32
+
33
+ export type ChildNode = NodeInternals &
34
+ ObservableBase<unknown> &
35
+ Readonly<{
36
+ parents: readonly Observable<unknown>[];
37
+ }>;
38
+
39
+ export type ManagerNode = NodeInternals &
40
+ ObservableBase<unknown> &
41
+ Readonly<{
42
+ addDescendant: (child: ChildNode) => void;
43
+ deleteDescendant: (child: ChildNode) => void;
44
+ }>;
45
+
46
+ /**
47
+ * The one place a public observable is seen as the node it is. Every
48
+ * observable is built by a synstate class, and the public types carry a
49
+ * brand an object literal cannot supply, so this holds for every value of
50
+ * those types.
51
+ */
52
+ export const toNode = <A,>(observable: Observable<A>): ObservableNode<A> =>
53
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
54
+ observable as ObservableNode<A>;