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
@@ -2,5 +2,6 @@ export * from './id.mjs';
2
2
  export * from './observable-family.mjs';
3
3
  export * from './observable-kind.mjs';
4
4
  export * from './observable.mjs';
5
+ export * from './scope.mjs';
5
6
  export * from './timer.mjs';
6
7
  export * from './types.mjs';
@@ -20,7 +20,11 @@ import type { NonEmptyUnknownList } from './types.mjs';
20
20
  // RootObservable
21
21
 
22
22
  export type InitializedSourceObservable<A> = Readonly<{
23
- next: (value: A) => void;
23
+ /**
24
+ * Passes `value` on and returns it. Once completed, passes nothing on and
25
+ * returns the value held.
26
+ */
27
+ next: (value: A) => A;
24
28
  }> &
25
29
  InitializedRootObservable<A>;
26
30
 
@@ -206,6 +210,8 @@ export type PairwiseOperatorObservable<A> = SyncChildObservable<
206
210
  readonly [A]
207
211
  >;
208
212
 
213
+ export type TakeOperatorObservable<A> = SyncChildObservable<A, readonly [A]>;
214
+
209
215
  export type TakeWhileOperatorObservable<A> = SyncChildObservable<
210
216
  A,
211
217
  readonly [A]
@@ -1,6 +1,13 @@
1
- import { expectType, type Optional, type Some } from 'ts-data-forge';
1
+ import {
2
+ expectType,
3
+ hasKey,
4
+ isRecord,
5
+ type Optional,
6
+ type Some,
7
+ } from 'ts-data-forge';
2
8
  import type { FixedLengthTuple, StrictOmit } from 'ts-type-forge';
3
- import type { ObservableId, UpdateToken } from './id.mjs';
9
+ import { observableBrand } from '../utils/index.mjs';
10
+ import type { UpdateToken } from './id.mjs';
4
11
  import type { ObservableKind } from './observable-kind.mjs';
5
12
  import type { NonEmptyUnknownList, Subscription } from './types.mjs';
6
13
 
@@ -19,42 +26,37 @@ import type { NonEmptyUnknownList, Subscription } from './types.mjs';
19
26
  */
20
27
 
21
28
  type CreateObservableType<A, Kind extends ObservableKind> = Readonly<{
22
- id: ObservableId;
29
+ [observableBrand]: true;
30
+
23
31
  kind: Kind;
24
32
 
25
- // reactive dependency tree structure
33
+ /**
34
+ * One more than the deepest observable this one is derived from; 0 for a
35
+ * root. An update reaches observables in ascending depth, so every parent
36
+ * has emitted before its child reads it.
37
+ */
26
38
  depth: Kind extends 'root' ? 0 : number;
27
- addChild: <B>(child: ChildObservable<B>) => void;
28
- deleteChild: <B>(child: ChildObservable<B>) => void;
29
39
 
30
- // state
31
40
  getSnapshot: () => Optional<A>;
32
41
  isCompleted: boolean;
33
- updateToken: UpdateToken;
34
- hasSubscriber: boolean;
35
- hasChild: boolean;
36
- hasActiveChild: () => boolean;
37
42
 
38
43
  /**
39
- * Whether this observable completes itself once nothing uses it any more —
40
- * once it has neither subscribers nor live children when one of its children
41
- * completes. True when completing it would release something: it has a
42
- * teardown of its own (a timer, an inner subscription) or a parent with
43
- * `autoComplete`. A `source` or `createState` and everything derived from
44
- * it alone are never completed this way, so completing a derived observable
45
- * cannot end a state that something else may still read (#2133).
44
+ * The token of the update in which this observable last emitted. A child's
45
+ * `tryUpdate` receives the token of the update now propagating; a parent
46
+ * whose token differs did not emit in it. The token is opaque: compare it,
47
+ * store it, or pass it on to `setNext`.
46
48
  */
47
- autoComplete: boolean;
49
+ updateToken: UpdateToken;
48
50
 
49
- tryUpdate: (updateToken: UpdateToken) => void;
50
- tryComplete: () => void;
51
51
  complete: () => void;
52
52
 
53
53
  /**
54
- * Completes this observable and takes it out of the graph, without telling
55
- * its parents: unlike `complete()`, it never completes an upstream
56
- * observable, whatever its `autoComplete`. Its descendants are completed as
57
- * by `complete()` once none of their parents is live.
54
+ * Completes this observable and takes it out of the graph, for whoever owns
55
+ * it (a scope) to release it. Neither this nor `complete()` reaches upstream:
56
+ * a child completing never completes its parents, which may be shared
57
+ * (#2133). Its descendants complete once none of their parents is live.
58
+ * Unlike `complete()`, it passes on nothing the observable still holds: a
59
+ * `debounce` or `audit` drops its pending value instead of emitting it.
58
60
  */
59
61
  dispose: () => void;
60
62
  subscribe: (onNext: (v: A) => void, onComplete?: () => void) => Subscription;
@@ -97,15 +99,6 @@ namespace ObservableTypeConverter {
97
99
  Readonly<{
98
100
  parents: Wrap<P>;
99
101
  }>;
100
-
101
- export type ToManager<
102
- A,
103
- O extends CreateObservableType<A, 'async child' | 'root'>,
104
- > = O &
105
- Readonly<{
106
- addDescendant: <B>(child: ChildObservable<B>) => void;
107
- deleteDescendant: <B>(child: ChildObservable<B>) => void;
108
- }>;
109
102
  }
110
103
 
111
104
  export type SyncChildObservable<
@@ -125,9 +118,10 @@ export type InitializedSyncChildObservable<
125
118
  export type AsyncChildObservable<
126
119
  A,
127
120
  P extends NonEmptyUnknownList = NonEmptyUnknownList,
128
- > = ObservableTypeConverter.ToManager<
121
+ > = ObservableTypeConverter.ToChild<
129
122
  A,
130
- ObservableTypeConverter.ToChild<A, CreateObservableType<A, 'async child'>, P>
123
+ CreateObservableType<A, 'async child'>,
124
+ P
131
125
  >;
132
126
 
133
127
  export type InitializedAsyncChildObservable<
@@ -140,10 +134,7 @@ export type ChildObservable<
140
134
  P extends NonEmptyUnknownList = NonEmptyUnknownList,
141
135
  > = AsyncChildObservable<A, P> | SyncChildObservable<A, P>;
142
136
 
143
- export type RootObservable<A> = ObservableTypeConverter.ToManager<
144
- A,
145
- CreateObservableType<A, 'root'>
146
- >;
137
+ export type RootObservable<A> = CreateObservableType<A, 'root'>;
147
138
 
148
139
  export type InitializedRootObservable<A> =
149
140
  ObservableTypeConverter.ToInitialized<A, RootObservable<A>>;
@@ -180,6 +171,27 @@ export type Operator<A, B> =
180
171
  | DropInitialValueOperator<A, B>
181
172
  | KeepInitialValueOperator<A, B>;
182
173
 
174
+ /**
175
+ * Tells an observable made by synstate from any other value, by the brand
176
+ * every observable carries. Checking for a `subscribe` method instead would
177
+ * also accept plain values that happen to have one.
178
+ *
179
+ * @example
180
+ *
181
+ * ```ts
182
+ * const [count$] = createState(0);
183
+ *
184
+ * assert.isTrue(isObservable(count$));
185
+ *
186
+ * // A `subscribe` method alone does not make an observable.
187
+ * assert.isFalse(isObservable({ subscribe: () => {} }));
188
+ *
189
+ * assert.isFalse(isObservable(0));
190
+ * ```
191
+ */
192
+ export const isObservable = (value: unknown): value is Observable<unknown> =>
193
+ isRecord(value) && hasKey(value, observableBrand);
194
+
183
195
  export const isManagerObservable = <A,>(
184
196
  obs: Observable<A>,
185
197
  ): obs is ManagerObservable<A> => obs.kind !== 'sync child';
@@ -210,9 +222,32 @@ export type WrapInitialized<A extends readonly unknown[]> = Readonly<{
210
222
  // type GetDestTypeOfOperator<Op extends Operator<unknown, unknown>> =
211
223
  // Op extends Operator<unknown, infer B> ? B : never;
212
224
 
213
- expectType<keyof AsyncChildObservable<unknown>, 'addDescendant'>('>=');
225
+ // Only the members meant for users are public; the rest live on the
226
+ // internal node types in `core/class` (#2117).
227
+ expectType<
228
+ keyof Observable<unknown>,
229
+ | typeof observableBrand
230
+ | 'kind'
231
+ | 'depth'
232
+ | 'getSnapshot'
233
+ | 'isCompleted'
234
+ | 'updateToken'
235
+ | 'complete'
236
+ | 'dispose'
237
+ | 'subscribe'
238
+ | 'pipe'
239
+ >('=');
214
240
 
215
- expectType<keyof RootObservable<unknown>, 'addDescendant'>('>=');
241
+ expectType<
242
+ keyof ChildObservable<unknown>,
243
+ keyof Observable<unknown> | 'parents'
244
+ >('=');
245
+
246
+ // Everything but the brand is not enough to pass for an observable.
247
+ expectType<
248
+ StrictOmit<RootObservable<unknown>, typeof observableBrand>,
249
+ Observable<unknown>
250
+ >('!<=');
216
251
 
217
252
  expectType<RootObservable<unknown>['depth'], 0>('=');
218
253
 
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A unit of ownership: the observables created while {@link Scope.run} is
3
+ * running belong to the scope, and {@link Scope.dispose} releases them all at
4
+ * once. Create one with `createScope`.
5
+ *
6
+ * For authors of libraries built on synstate (a UI binding mounts and unmounts
7
+ * by scope); application code that defines its state at module level does not
8
+ * need it.
9
+ */
10
+ export type Scope = Readonly<{
11
+ /**
12
+ * Calls `body` with this scope as the current one and returns what it
13
+ * returns. Only what `body` creates synchronously joins the scope: by the time
14
+ * code after an `await` or in a callback runs, `run` has already returned.
15
+ *
16
+ * On a disposed scope it warns, still calls `body`, and nothing created there
17
+ * belongs to any scope.
18
+ */
19
+ run: <R>(body: () => R) => R;
20
+
21
+ /**
22
+ * Disposes the child scopes (newest first), calls the functions registered
23
+ * with `onScopeDispose` (newest first), disposes the owned observables
24
+ * (newest first) and leaves the parent scope. Calling it again does nothing.
25
+ */
26
+ dispose: () => void;
27
+ }>;
@@ -0,0 +1,61 @@
1
+ import type { Scope } from '../types/index.mjs';
2
+
3
+ /** What a scope owns: an observable, as far as the scope needs to know it. */
4
+ export type ScopeMember = Readonly<{
5
+ isCompleted: boolean;
6
+ dispose: () => void;
7
+ }>;
8
+
9
+ /**
10
+ * The side of a scope the rest of the core talks to, on top of its public
11
+ * face {@link Scope}. Kept out of the public API (`core/utils/**` is excluded
12
+ * from index generation), so that only `createScope` and its siblings decide
13
+ * what a scope owns.
14
+ */
15
+ export type ScopeInternals = Scope &
16
+ Readonly<{
17
+ /** Takes `node` into the scope. */
18
+ adopt: (node: ScopeMember) => void;
19
+
20
+ /**
21
+ * Tells the scope that one of the nodes it adopted completed on its own,
22
+ * so that the scope can drop it and keep no reference to it. The node is
23
+ * completed (`isCompleted`) by the time it calls this.
24
+ */
25
+ release: () => void;
26
+
27
+ addCleanup: (cleanup: () => void) => void;
28
+ }>;
29
+
30
+ /**
31
+ * The scope whose `run` is executing. A field rather than a module-level
32
+ * `let`, so that the one place writing it is visibly this object.
33
+ */
34
+ const mut_slot: { current: ScopeInternals | undefined } = {
35
+ current: undefined,
36
+ };
37
+
38
+ export const getCurrentScopeInternals = (): ScopeInternals | undefined =>
39
+ mut_slot.current;
40
+
41
+ /**
42
+ * Calls `body` with `scope` as the current scope (`undefined`: none) and
43
+ * restores the previous one afterwards, also when `body` throws.
44
+ */
45
+ export const runWithScope = <R,>(
46
+ scope: ScopeInternals | undefined,
47
+ body: () => R,
48
+ ): R => {
49
+ const prev = mut_slot.current;
50
+
51
+ mut_slot.current = scope;
52
+
53
+ // `finally` is the point: an exception from `body` must not leave its scope
54
+ // current for whatever runs next. Nothing is caught.
55
+ // @sumi-expect-error exceptions/no-try
56
+ try {
57
+ return body();
58
+ } finally {
59
+ mut_slot.current = prev;
60
+ }
61
+ };
@@ -1,31 +1,21 @@
1
- import type {
2
- ObservableId,
3
- SubscriberId,
4
- UpdateToken,
5
- } from '../types/index.mjs';
6
-
7
- function* idMaker<T extends symbol>(): Generator<T, T, T> {
8
- let mut_i = 0;
9
-
10
- while (true) {
11
- // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
12
- yield Symbol(mut_i.toString()) as T;
13
-
14
- mut_i += 1;
15
- }
16
- }
17
-
18
- const observableIdMaker = idMaker<ObservableId>();
19
-
20
- const subscriberIdMaker = idMaker<SubscriberId>();
21
-
22
- const updateTokenMaker = idMaker<UpdateToken>();
23
-
24
- export const issueObservableId = (): ObservableId =>
25
- observableIdMaker.next().value;
26
-
27
- export const issueSubscriberId = (): SubscriberId =>
28
- subscriberIdMaker.next().value;
29
-
1
+ import type { UpdateToken } from '../types/index.mjs';
2
+
3
+ /**
4
+ * The token of a new update, which the update's root or async child passes to
5
+ * every node it reaches. A `Symbol` with no description, issued once per
6
+ * update and never per node, so its cost is spread over every node the
7
+ * update reaches. Unique without any counter, so the tokens of two copies of
8
+ * synstate in one program never coincide, and nothing is kept in a global.
9
+ */
30
10
  export const issueUpdateToken = (): UpdateToken =>
31
- updateTokenMaker.next().value;
11
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
12
+ Symbol() as UpdateToken;
13
+
14
+ /**
15
+ * The token a node holds before its first update: one value shared by every
16
+ * node of this copy, since no update is ever issued it, so creating a node
17
+ * issues nothing.
18
+ */
19
+ export const notUpdatedYet: UpdateToken =
20
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
21
+ Symbol('synstate/not-updated-yet') as UpdateToken;
@@ -1,4 +1,5 @@
1
+ export * from './current-scope.mjs';
1
2
  export * from './id-maker.mjs';
2
3
  export * from './max-depth.mjs';
4
+ export * from './observable-brand.mjs';
3
5
  export * from './observable-utils.mjs';
4
- export * from './utils.mjs';
@@ -1,11 +1,14 @@
1
- import { Arr, Optional, pipe } from 'ts-data-forge';
2
1
  import type { Observable } from '../types/index.mjs';
3
2
 
4
- export const maxDepth = (parents: readonly Observable<unknown>[]): number =>
5
- pipe(Arr.maxBy(parents, (p) => p.depth))
6
- // Explicit return type + non-curried `Optional.unwrapOr` call: TypeScript
7
- // 7 (native)'s inference for this chained generic call resolves to
8
- // `unknown` without them, even though TypeScript <= 6.0's checker
9
- // resolves this fine.
10
- .mapOptional((o): number => o.depth)
11
- .map(Optional.unwrapOr(0)).value;
3
+ /** The greatest `depth` among `parents`, or 0 when there are none. */
4
+ export const maxDepth = (parents: readonly Observable<unknown>[]): number => {
5
+ let mut_max = 0;
6
+
7
+ for (const p of parents) {
8
+ if (p.depth > mut_max) {
9
+ mut_max = p.depth;
10
+ }
11
+ }
12
+
13
+ return mut_max;
14
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The key every observable carries, set to `true` by `assembleObservable`. It
3
+ * lets `isObservable` tell an observable from any other value at run time —
4
+ * a `subscribe` method is not enough, since plain values may have one — and,
5
+ * as a type, makes the public observable types impossible to satisfy with an
6
+ * object literal: an imitation passed to an operator would lack the internal
7
+ * members propagation reads and fail at run time instead of at compile time.
8
+ *
9
+ * `Symbol.for`, so that an observable made by another copy of synstate in the
10
+ * same program is still recognized.
11
+ */
12
+ export const observableBrand: unique symbol = Symbol.for('synstate/observable');
@@ -7,8 +7,8 @@ import { createState } from './create-state.mjs';
7
7
  * Extends `createState` with boolean-specific helpers like `toggle`, `setTrue`, and `setFalse`.
8
8
  *
9
9
  * @param initialState - The initial boolean value
10
- * @param options - {@link StateOptions}: `equals`, to pass on only the
11
- * updates that change the state
10
+ * @param options - {@link StateOptions}: `equals` (default `Object.is`),
11
+ * to pass on only the updates that change the state
12
12
  * @returns A 2-element tuple: `[state, { setTrue, setFalse, toggle, setState, updateState, resetState, getSnapshot, initialState }]`
13
13
  *
14
14
  * @example
@@ -1,10 +1,8 @@
1
1
  import { createBooleanState } from './create-boolean-state.mjs';
2
2
 
3
3
  describe(createBooleanState, () => {
4
- test('with `equals`, setting what it already holds passes nothing on', () => {
5
- const [state, { setTrue, setFalse }] = createBooleanState(false, {
6
- equals: Object.is,
7
- });
4
+ test('setting what it already holds passes nothing on when no `equals` is given', () => {
5
+ const [state, { setTrue, setFalse }] = createBooleanState(false);
8
6
 
9
7
  const mut_seen: boolean[] = [];
10
8
 
@@ -20,4 +18,20 @@ describe(createBooleanState, () => {
20
18
 
21
19
  assert.deepStrictEqual(mut_seen, [false, true]);
22
20
  });
21
+
22
+ test('passes on every update, equal or not, with `equals: () => false`', () => {
23
+ const [state, { setTrue }] = createBooleanState(true, {
24
+ equals: () => false,
25
+ });
26
+
27
+ const mut_seen: boolean[] = [];
28
+
29
+ state.subscribe((value) => {
30
+ mut_seen.push(value);
31
+ });
32
+
33
+ setTrue();
34
+
35
+ assert.deepStrictEqual(mut_seen, [true, true]);
36
+ });
23
37
  });
@@ -18,13 +18,13 @@ import { source, type Observable } from '../core/index.mjs';
18
18
  *
19
19
  * emitClick(); // logs: Clicked!
20
20
  *
21
- * assert.deepStrictEqual(mut_clickCount.value, 1);
21
+ * assert.strictEqual(mut_clickCount.value, 1);
22
22
  *
23
23
  * emitClick();
24
24
  *
25
25
  * emitClick();
26
26
  *
27
- * assert.deepStrictEqual(mut_clickCount.value, 3);
27
+ * assert.strictEqual(mut_clickCount.value, 3);
28
28
  * ```
29
29
  */
30
30
  export const createEventEmitter = (): readonly [
@@ -9,9 +9,11 @@ import { source, type InitializedObservable } from '../core/index.mjs';
9
9
  * @template A - The type of actions
10
10
  * @param reducer - A pure function that takes current state and action, returns new state
11
11
  * @param initialState - The initial value of the state
12
- * @param options - {@link StateOptions}: `equals`, to pass on only the
13
- * dispatches that change the state
14
- * @returns A 3-element tuple: `[state, dispatch, { getSnapshot, initialState }]`
12
+ * @param options - {@link StateOptions}: `equals` (default `Object.is`),
13
+ * to pass on only the dispatches that change the state
14
+ * @returns A 3-element tuple: `[state, dispatch, { getSnapshot, initialState }]`.
15
+ * `dispatch` returns the state it leaves; once `state` is completed, that
16
+ * is the state held.
15
17
  *
16
18
  * @example
17
19
  * ```ts
@@ -52,7 +54,7 @@ import { source, type InitializedObservable } from '../core/index.mjs';
52
54
  * (s, action: Readonly<{ type: 'add'; amount: number }>) =>
53
55
  * s + action.amount,
54
56
  * 0,
55
- * { equals: Object.is },
57
+ * { equals: () => false },
56
58
  * );
57
59
  *
58
60
  * const countHistory: number[] = [];
@@ -61,12 +63,19 @@ import { source, type InitializedObservable } from '../core/index.mjs';
61
63
  * countHistory.push(value);
62
64
  * });
63
65
  *
64
- * dispatch({ type: 'add', amount: 0 }); // 0 again: nothing is passed on
66
+ * dispatch({ type: 'add', amount: 0 }); // 0 again, passed on all the same
65
67
  *
66
68
  * dispatch({ type: 'add', amount: 2 });
67
69
  *
68
- * assert.deepStrictEqual(countHistory, [0, 2]);
70
+ * assert.deepStrictEqual(countHistory, [0, 0, 2]);
69
71
  * ```
72
+ *
73
+ * @note Do not call `dispatch` from a subscriber or a projection while another
74
+ * update is propagating: the write propagates immediately, so subscribers can
75
+ * see an intermediate state, and it logs a `console.warn` (once per
76
+ * observable). Derive the value from its source instead —
77
+ * `const b$ = a$.pipe(map((a) => a * 2))` rather than
78
+ * `a$.subscribe((a) => { setB(a * 2); })`.
70
79
  */
71
80
  export const createReducer = <S, A>(
72
81
  reducer: Reducer<S, A>,
@@ -82,20 +91,17 @@ export const createReducer = <S, A>(
82
91
  ] => {
83
92
  const state = source<S>(initialState);
84
93
 
85
- const equals = options?.equals;
94
+ const equals = options?.equals ?? Object.is;
86
95
 
87
96
  const dispatch = (action: A): S => {
88
97
  const currentState = state.getSnapshot().value;
89
98
 
90
99
  const nextState = reducer(currentState, action);
91
100
 
92
- if (equals?.(currentState, nextState) === true) {
93
- return currentState;
94
- }
95
-
96
- state.next(nextState);
97
-
98
- return nextState;
101
+ // Once completed, `next` keeps the state held and returns it.
102
+ return equals(currentState, nextState)
103
+ ? currentState
104
+ : state.next(nextState);
99
105
  };
100
106
 
101
107
  const getSnapshot = (): S => state.getSnapshot().value;
@@ -118,8 +124,8 @@ export type StateOptions<S> = Readonly<{
118
124
  * `skipIfNoChange`: an update passed on here reaches whatever reads the
119
125
  * state directly before any operator can thin it out.
120
126
  *
121
- * When omitted, every update is passed on, equal or not. That default is
122
- * stocked in `NEXT_MAJOR.md` to become `Object.is`.
127
+ * Defaults to `Object.is`, so writing the value already held passes
128
+ * nothing on. Pass `() => false` to pass on every update, equal or not.
123
129
  *
124
130
  * @param current - The state held now
125
131
  * @param next - The state the update would set
@@ -2,34 +2,51 @@ import type { FixedLengthTuple } from 'ts-type-forge';
2
2
  import { createReducer } from './create-reducer.mjs';
3
3
 
4
4
  describe(createReducer, () => {
5
- test('passes on every dispatch when no `equals` is given, equal or not', () => {
6
- const [state, dispatch] = createReducer(keepOrAdd, 0);
5
+ test('does not pass on a dispatch whose next state is `Object.is`-equal to the current one when no `equals` is given', () => {
6
+ const [state, dispatch, { getSnapshot }] = createReducer(keepOrAdd, 0);
7
7
 
8
8
  const seen = record(state);
9
9
 
10
- dispatch({ type: 'keep' });
10
+ assert.strictEqual(dispatch({ type: 'keep' }), 0);
11
11
 
12
- dispatch({ type: 'add', amount: 1 });
12
+ assert.strictEqual(dispatch({ type: 'add', amount: 1 }), 1);
13
13
 
14
- assert.deepStrictEqual(seen, [0, 0, 1]);
14
+ assert.strictEqual(dispatch({ type: 'add', amount: 0 }), 1);
15
+
16
+ assert.deepStrictEqual(seen, [0, 1]);
17
+
18
+ assert.strictEqual(getSnapshot(), 1);
15
19
  });
16
20
 
17
- test('with `equals`, does not pass on a dispatch whose next state is equal to the current one', () => {
18
- const [state, dispatch, { getSnapshot }] = createReducer(keepOrAdd, 0, {
19
- equals: Object.is,
20
- });
21
+ test('compares with `Object.is` by default, so `NaN` equals itself and `-0` differs from `0`', () => {
22
+ const [state, dispatch] = createReducer(
23
+ (_current: number, next: number) => next,
24
+ 0,
25
+ );
21
26
 
22
27
  const seen = record(state);
23
28
 
24
- assert.strictEqual(dispatch({ type: 'keep' }), 0);
29
+ dispatch(-0);
25
30
 
26
- assert.strictEqual(dispatch({ type: 'add', amount: 1 }), 1);
31
+ dispatch(Number.NaN);
27
32
 
28
- assert.strictEqual(dispatch({ type: 'add', amount: 0 }), 1);
33
+ dispatch(Number.NaN);
29
34
 
30
- assert.deepStrictEqual(seen, [0, 1]);
35
+ assert.deepStrictEqual(seen, [0, -0, Number.NaN]);
36
+ });
31
37
 
32
- assert.strictEqual(getSnapshot(), 1);
38
+ test('passes on every dispatch, equal or not, with `equals: () => false`', () => {
39
+ const [state, dispatch] = createReducer(keepOrAdd, 0, {
40
+ equals: () => false,
41
+ });
42
+
43
+ const seen = record(state);
44
+
45
+ dispatch({ type: 'keep' });
46
+
47
+ dispatch({ type: 'add', amount: 1 });
48
+
49
+ assert.deepStrictEqual(seen, [0, 0, 1]);
33
50
  });
34
51
 
35
52
  test('keeps the current object when `equals` finds the next one equal to it', () => {
@@ -65,6 +82,18 @@ describe(createReducer, () => {
65
82
 
66
83
  assert.deepStrictEqual(mut_asked, [[0, 5]]);
67
84
  });
85
+
86
+ test('once the state is completed, a dispatch returns the current state', () => {
87
+ const [state, dispatch, { getSnapshot }] = createReducer(keepOrAdd, 0);
88
+
89
+ dispatch({ type: 'add', amount: 1 });
90
+
91
+ state.complete();
92
+
93
+ assert.strictEqual(dispatch({ type: 'add', amount: 2 }), 1);
94
+
95
+ assert.strictEqual(getSnapshot(), 1);
96
+ });
68
97
  });
69
98
 
70
99
  type Count = Readonly<{ count: number }>;
@@ -28,8 +28,8 @@ const reducer = <S,>(state: S, action: Action<S>): S => {
28
28
  *
29
29
  * @template S - The type of the state
30
30
  * @param initialState - The initial value of the state
31
- * @param options - {@link StateOptions}: `equals`, to pass on only the
32
- * updates that change the state
31
+ * @param options - {@link StateOptions}: `equals` (default `Object.is`),
32
+ * to pass on only the updates that change the state
33
33
  * @returns A 3-element tuple: `[state, setState, { updateState, resetState, getSnapshot, initialState }]`
34
34
  *
35
35
  * @example
@@ -88,6 +88,13 @@ const reducer = <S,>(state: S, action: Action<S>): S => {
88
88
  * 'label:bug page:2',
89
89
  * ]);
90
90
  * ```
91
+ *
92
+ * @note Do not call `setState`, `updateState` or `resetState` from a subscriber or a projection while another
93
+ * update is propagating: the write propagates immediately, so subscribers can
94
+ * see an intermediate state, and it logs a `console.warn` (once per
95
+ * observable). Derive the value from its source instead —
96
+ * `const b$ = a$.pipe(map((a) => a * 2))` rather than
97
+ * `a$.subscribe((a) => { setB(a * 2); })`.
91
98
  */
92
99
  export const createState = <S,>(
93
100
  initialState: S,