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,134 @@
1
+ import { Result } from 'ts-data-forge';
2
+ import { take } from '../operators/index.mjs';
3
+ import type { Subscribable } from '../types/index.mjs';
4
+ import { fromSubscribable } from './from-subscribable.mjs';
5
+
6
+ describe(fromSubscribable, () => {
7
+ test('complete() unsubscribes from the subscribable once', () => {
8
+ const { subscribable, emit, unsubscribe } = createManualSubscribable();
9
+
10
+ const observable$ = fromSubscribable(subscribable);
11
+
12
+ observable$.complete();
13
+
14
+ observable$.complete();
15
+
16
+ expect(unsubscribe).toHaveBeenCalledExactlyOnceWith();
17
+
18
+ emit(1);
19
+
20
+ assert.isTrue(observable$.isCompleted);
21
+ });
22
+
23
+ test('dispose() unsubscribes from the subscribable', () => {
24
+ const { subscribable, unsubscribe } = createManualSubscribable();
25
+
26
+ fromSubscribable(subscribable).dispose();
27
+
28
+ expect(unsubscribe).toHaveBeenCalledExactlyOnceWith();
29
+ });
30
+
31
+ test('does not unsubscribe when the subscribable completes first', () => {
32
+ const { subscribable, emit, completeSource, unsubscribe } =
33
+ createManualSubscribable();
34
+
35
+ const observable$ = fromSubscribable(subscribable);
36
+
37
+ const mut_history: number[] = [];
38
+
39
+ observable$.subscribe((result) => {
40
+ if (Result.isOk(result)) {
41
+ mut_history.push(result.value);
42
+ }
43
+ });
44
+
45
+ emit(1);
46
+
47
+ completeSource();
48
+
49
+ assert.isTrue(observable$.isCompleted);
50
+
51
+ observable$.complete();
52
+
53
+ expect(unsubscribe).not.toHaveBeenCalled();
54
+
55
+ assert.deepStrictEqual(mut_history, [1]);
56
+ });
57
+
58
+ test('does not unsubscribe when the subscribable completes synchronously during subscribe', () => {
59
+ const unsubscribe = vi.fn<() => void>();
60
+
61
+ const observable$ = fromSubscribable<number>({
62
+ subscribe: (_onNext, _onError, onComplete) => {
63
+ onComplete?.();
64
+
65
+ return { unsubscribe };
66
+ },
67
+ });
68
+
69
+ assert.isTrue(observable$.isCompleted);
70
+
71
+ expect(unsubscribe).not.toHaveBeenCalled();
72
+ });
73
+
74
+ test('a child that completes leaves the subscribable subscribed until the observable is completed', () => {
75
+ const { subscribable, emit, unsubscribe } = createManualSubscribable();
76
+
77
+ const observable$ = fromSubscribable(subscribable);
78
+
79
+ const taken$ = observable$.pipe(take(1));
80
+
81
+ taken$.subscribe(() => {});
82
+
83
+ emit(1);
84
+
85
+ emit(2);
86
+
87
+ assert.isTrue(taken$.isCompleted);
88
+
89
+ // Completion flows only downstream.
90
+ assert.isFalse(observable$.isCompleted);
91
+
92
+ expect(unsubscribe).not.toHaveBeenCalled();
93
+
94
+ observable$.complete();
95
+
96
+ expect(unsubscribe).toHaveBeenCalledExactlyOnceWith();
97
+ });
98
+ });
99
+
100
+ const createManualSubscribable = (): Readonly<{
101
+ subscribable: Subscribable<number>;
102
+ emit: (value: number) => void;
103
+ completeSource: () => void;
104
+ unsubscribe: () => void;
105
+ }> => {
106
+ let mut_onNext: ((value: number) => void) | undefined = undefined;
107
+
108
+ let mut_onComplete: (() => void) | undefined = undefined;
109
+
110
+ const unsubscribe = vi.fn<() => void>(() => {
111
+ mut_onNext = undefined;
112
+
113
+ mut_onComplete = undefined;
114
+ });
115
+
116
+ return {
117
+ subscribable: {
118
+ subscribe: (onNext, _onError, onComplete) => {
119
+ mut_onNext = onNext;
120
+
121
+ mut_onComplete = onComplete;
122
+
123
+ return { unsubscribe };
124
+ },
125
+ },
126
+ emit: (value) => {
127
+ mut_onNext?.(value);
128
+ },
129
+ completeSource: () => {
130
+ mut_onComplete?.();
131
+ },
132
+ unsubscribe,
133
+ };
134
+ };
@@ -1,5 +1,5 @@
1
1
  import { Optional } from 'ts-data-forge';
2
- import { createRootObservable } from '../base/index.mjs';
2
+ import { RootObservableClass } from '../class/index.mjs';
3
3
  import type { InitializedRootObservable } from '../types/index.mjs';
4
4
 
5
5
  /**
@@ -32,10 +32,9 @@ import type { InitializedRootObservable } from '../types/index.mjs';
32
32
  * ```
33
33
  */
34
34
  export const just = <const A,>(value: A): InitializedRootObservable<A> => {
35
- const obs = createRootObservable<A>(
36
- { initialValue: Optional.some(value) },
37
- () => ({}),
38
- );
35
+ const obs = new RootObservableClass<A>({
36
+ initialValue: Optional.some(value),
37
+ });
39
38
 
40
39
  obs.complete();
41
40
 
@@ -1,5 +1,11 @@
1
+ // Sumi bans classes, `this`, method syntax and writes to `this` outside a
2
+ // `mut_` binding; an observable is a class (`ObservableBaseClass` says why),
3
+ // so those rules are waived here (D-63).
4
+ // @sumi-expect-error-file classes/no-class
5
+ // @sumi-expect-error-file banned-syntax/no-this
6
+ // @sumi-expect-error-file functions/prefer-arrow-function
1
7
  import { Arr, Optional } from 'ts-data-forge';
2
- import { createRootObservable } from '../base/index.mjs';
8
+ import { RootObservableClass } from '../class/index.mjs';
3
9
  import type {
4
10
  InitializedSourceObservable,
5
11
  SourceObservable,
@@ -43,6 +49,13 @@ import type {
43
49
  *
44
50
  * assert.deepStrictEqual(valueHistory, [1, 2, 3]);
45
51
  * ```
52
+ *
53
+ * @note Do not call `next` from a subscriber or a projection while another
54
+ * update is propagating: the write propagates immediately, so subscribers can
55
+ * see an intermediate state, and it logs a `console.warn` (once per
56
+ * observable). Derive the value from its source instead —
57
+ * `const b$ = a$.pipe(map((a) => a * 2))` rather than
58
+ * `a$.subscribe((a) => { setB(a * 2); })`.
46
59
  */
47
60
  export function source<const A>(
48
61
  initialValue: A,
@@ -51,20 +64,31 @@ export function source<const A>(
51
64
  export function source<const A>(): SourceObservable<A>;
52
65
 
53
66
  export function source<const A>(...args: readonly A[]): SourceObservable<A> {
54
- return createRootObservable<A, Readonly<{ next: (nextValue: A) => void }>>(
55
- {
67
+ return new SourceObservableClass<A>(...args);
68
+ }
69
+
70
+ class SourceObservableClass<A>
71
+ extends RootObservableClass<A>
72
+ implements SourceObservable<A>
73
+ {
74
+ constructor(...args: readonly A[]) {
75
+ super({
56
76
  initialValue: Arr.isNonEmpty(args)
57
77
  ? Optional.some(args[0])
58
78
  : Optional.none,
59
- },
60
- ({ startUpdate, isCompleted }) => ({
61
- next: (nextValue: A) => {
62
- if (isCompleted()) {
63
- return;
64
- }
79
+ });
80
+ }
81
+
82
+ // Returns the value held after the call: `nextValue`, or, once completed,
83
+ // the value already there — `undefined` only on a source that never had
84
+ // one, whose `next` is typed `void`.
85
+ next(nextValue: A): A | undefined {
86
+ if (this.isCompleted) {
87
+ return Optional.unwrap(this.getSnapshot());
88
+ }
89
+
90
+ this.startUpdate(nextValue);
65
91
 
66
- startUpdate(nextValue);
67
- },
68
- }),
69
- );
92
+ return nextValue;
93
+ }
70
94
  }
@@ -0,0 +1,41 @@
1
+ import { expectType } from 'ts-data-forge';
2
+ import type { SourceObservable } from '../types/index.mjs';
3
+ import { source } from './source.mjs';
4
+
5
+ describe(source, () => {
6
+ test('`next` returns the value it passed on', () => {
7
+ const count$ = source<number>(0);
8
+
9
+ assert.strictEqual(count$.next(1), 1);
10
+
11
+ assert.strictEqual(count$.getSnapshot().value, 1);
12
+ });
13
+
14
+ test('once completed, `next` passes nothing on and returns the value held', () => {
15
+ const count$ = source<number>(0);
16
+
17
+ const mut_seen: number[] = [];
18
+
19
+ count$.subscribe((value) => {
20
+ mut_seen.push(value);
21
+ });
22
+
23
+ count$.next(1);
24
+
25
+ count$.complete();
26
+
27
+ assert.strictEqual(count$.next(2), 1);
28
+
29
+ assert.deepStrictEqual(mut_seen, [0, 1]);
30
+ });
31
+
32
+ test('`next` returns the value only on a source with an initial value', () => {
33
+ const passed = source<number>(0).next(1);
34
+
35
+ expectType<typeof passed, number>('=');
36
+
37
+ assert.strictEqual(passed, 1);
38
+
39
+ expectType<SourceObservable<number>['next'], (value: number) => void>('=');
40
+ });
41
+ });
@@ -1,5 +1,12 @@
1
+ // Sumi bans classes, `this`, method syntax and writes to `this` outside a
2
+ // `mut_` binding; an observable is a class (`ObservableBaseClass` says why),
3
+ // so those rules are waived here (D-63).
4
+ // @sumi-expect-error-file classes/no-class
5
+ // @sumi-expect-error-file banned-syntax/no-this
6
+ // @sumi-expect-error-file functions/prefer-arrow-function
7
+ // @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
1
8
  import { Optional } from 'ts-data-forge';
2
- import { createRootObservable } from '../base/index.mjs';
9
+ import { RootObservableClass } from '../class/index.mjs';
3
10
  import type { TimerId, TimerObservable } from '../types/index.mjs';
4
11
 
5
12
  /**
@@ -44,46 +51,58 @@ export const timer = (
44
51
  startManually?: boolean;
45
52
  }>,
46
53
  ): TimerObservable => {
47
- let mut_timerId: TimerId | undefined = undefined;
54
+ const observable = new TimerObservableClass(milliSeconds);
48
55
 
49
- let mut_isStarted = false;
56
+ if (!(options?.startManually ?? false)) {
57
+ observable.start();
58
+ }
50
59
 
51
- const resetTimer = (): void => {
52
- if (mut_timerId !== undefined) {
53
- clearTimeout(mut_timerId);
54
- }
55
- };
60
+ return observable;
61
+ };
56
62
 
57
- const observable = createRootObservable<0, Readonly<{ start: () => void }>>(
58
- { initialValue: Optional.none, onComplete: resetTimer },
59
- ({ startUpdate, isCompleted, complete }) => ({
60
- start: () => {
61
- if (mut_isStarted) {
62
- console.warn('cannot start twice');
63
+ class TimerObservableClass
64
+ extends RootObservableClass<0>
65
+ implements TimerObservable
66
+ {
67
+ readonly #milliSeconds: number;
68
+ #mut_timerId: TimerId | undefined;
69
+ #mut_isStarted: boolean;
63
70
 
64
- return;
65
- }
71
+ constructor(milliSeconds: number) {
72
+ super({ initialValue: Optional.none });
66
73
 
67
- mut_isStarted = true;
74
+ this.#milliSeconds = milliSeconds;
68
75
 
69
- if (isCompleted()) {
70
- console.warn('cannot restart stopped TimerObservable');
76
+ this.#mut_timerId = undefined;
71
77
 
72
- return;
73
- }
78
+ this.#mut_isStarted = false;
79
+ }
74
80
 
75
- mut_timerId = setTimeout(() => {
76
- startUpdate(0);
81
+ start(): void {
82
+ if (this.#mut_isStarted) {
83
+ console.warn('cannot start twice');
77
84
 
78
- complete();
79
- }, milliSeconds);
80
- },
81
- }),
82
- );
85
+ return;
86
+ }
83
87
 
84
- if (!(options?.startManually ?? false)) {
85
- observable.start();
88
+ this.#mut_isStarted = true;
89
+
90
+ if (this.isCompleted) {
91
+ console.warn('cannot restart stopped TimerObservable');
92
+
93
+ return;
94
+ }
95
+
96
+ this.#mut_timerId = setTimeout(() => {
97
+ this.startUpdate(0);
98
+
99
+ this.complete();
100
+ }, this.#milliSeconds);
86
101
  }
87
102
 
88
- return observable;
89
- };
103
+ protected override teardown(): void {
104
+ if (this.#mut_timerId !== undefined) {
105
+ clearTimeout(this.#mut_timerId);
106
+ }
107
+ }
108
+ }
@@ -1,5 +1,7 @@
1
+ export * from './authoring.mjs';
1
2
  export * from './combine/index.mjs';
2
3
  export * from './create/index.mjs';
3
4
  export * from './operators/index.mjs';
4
5
  export * from './predefined/index.mjs';
6
+ export * from './scope/index.mjs';
5
7
  export * from './types/index.mjs';
@@ -1,10 +1,18 @@
1
+ // Sumi bans classes, `this`, method syntax and writes to `this` outside a
2
+ // `mut_` binding; an observable is a class (`ObservableBaseClass` says why),
3
+ // so those rules are waived here (D-63).
4
+ // @sumi-expect-error-file classes/no-class
5
+ // @sumi-expect-error-file banned-syntax/no-this
6
+ // @sumi-expect-error-file functions/prefer-arrow-function
7
+ // @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
1
8
  import { Optional } from 'ts-data-forge';
2
- import { createAsyncChildObservable } from '../base/index.mjs';
9
+ import { AsyncChildObservableClass } from '../class/index.mjs';
3
10
  import type {
4
11
  AuditOperatorObservable,
5
12
  KeepInitialValueOperator,
6
13
  Observable,
7
14
  TimerId,
15
+ UpdateToken,
8
16
  } from '../types/index.mjs';
9
17
 
10
18
  /**
@@ -12,6 +20,9 @@ import type {
12
20
  *
13
21
  * Unlike `throttle` which emits the first value, `audit` emits the last value.
14
22
  *
23
+ * Completing it, or its source completing, passes on the value its timer is
24
+ * still holding before the completion; `dispose()` drops that value.
25
+ *
15
26
  * @template A - The type of values from the source
16
27
  * @param milliSeconds - The audit time window in milliseconds
17
28
  * @returns An operator that audits emissions from the observable
@@ -106,55 +117,78 @@ export const audit = <A,>(
106
117
  ): KeepInitialValueOperator<A, A> =>
107
118
  // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
108
119
  ((parentObservable) =>
109
- createAuditObservable(
120
+ new AuditObservableClass(
110
121
  parentObservable,
111
122
  milliSeconds,
112
123
  )) as KeepInitialValueOperator<A, A>;
113
124
 
114
- const createAuditObservable = <A,>(
115
- parentObservable: Observable<A>,
116
- milliSeconds: number,
117
- ): AuditOperatorObservable<A> => {
118
- let mut_timerId: TimerId | undefined = undefined;
119
-
120
- let mut_isSkipping = false;
125
+ class AuditObservableClass<A>
126
+ extends AsyncChildObservableClass<A, readonly [A]>
127
+ implements AuditOperatorObservable<A>
128
+ {
129
+ readonly #milliSeconds: number;
121
130
 
122
- const resetTimer = (): void => {
123
- if (mut_timerId !== undefined) {
124
- clearTimeout(mut_timerId);
125
- }
126
- };
131
+ /** Set while a window is open; its end passes on the latest value. */
132
+ #mut_timerId: TimerId | undefined;
127
133
 
128
- return createAsyncChildObservable(
129
- {
134
+ constructor(parentObservable: Observable<A>, milliSeconds: number) {
135
+ super({
130
136
  parents: [parentObservable],
131
137
  initialValue: parentObservable.getSnapshot(),
132
- onComplete: resetTimer,
133
- },
134
- ({ startUpdate }) =>
135
- (updateToken) => {
136
- if (
137
- mut_isSkipping ||
138
- parentObservable.updateToken !== updateToken ||
139
- Optional.isNone(parentObservable.getSnapshot())
140
- ) {
141
- return; // skip update
142
- }
143
-
144
- // set timer
145
- mut_isSkipping = true;
146
-
147
- mut_timerId = setTimeout(() => {
148
- const sn = parentObservable.getSnapshot();
149
-
150
- if (Optional.isNone(sn)) {
151
- return;
152
- }
153
-
154
- startUpdate(sn.value);
155
-
156
- mut_isSkipping = false;
157
- }, milliSeconds);
158
- },
159
- );
160
- };
138
+ });
139
+
140
+ this.#mut_timerId = undefined;
141
+
142
+ this.#milliSeconds = milliSeconds;
143
+
144
+ this.attach();
145
+ }
146
+
147
+ /** Ends the open window, passing on the latest value of the source. */
148
+ #emitLatest(): void {
149
+ this.#mut_timerId = undefined;
150
+
151
+ const sn = this.parents[0].getSnapshot();
152
+
153
+ if (Optional.isSome(sn)) {
154
+ this.startUpdate(sn.value);
155
+ }
156
+ }
157
+
158
+ override tryUpdate(updateToken: UpdateToken): void {
159
+ const par = this.parents[0];
160
+
161
+ if (
162
+ this.#mut_timerId !== undefined ||
163
+ par.updateToken !== updateToken ||
164
+ Optional.isNone(par.getSnapshot())
165
+ ) {
166
+ return; // skip update
167
+ }
168
+
169
+ // set timer
170
+ this.#mut_timerId = setTimeout(() => {
171
+ this.#emitLatest();
172
+ }, this.#milliSeconds);
173
+ }
174
+
175
+ protected override flush(): void {
176
+ if (this.#mut_timerId === undefined) {
177
+ return;
178
+ }
179
+
180
+ clearTimeout(this.#mut_timerId);
181
+
182
+ this.#emitLatest();
183
+ }
184
+
185
+ protected override teardown(): void {
186
+ if (this.#mut_timerId === undefined) {
187
+ return;
188
+ }
189
+
190
+ clearTimeout(this.#mut_timerId);
191
+
192
+ this.#mut_timerId = undefined;
193
+ }
194
+ }
@@ -0,0 +1,121 @@
1
+ import { source } from '../create/index.mjs';
2
+ import type {
3
+ KeepInitialValueOperator,
4
+ Observable,
5
+ SourceObservable,
6
+ } from '../types/index.mjs';
7
+ import { audit } from './audit.mjs';
8
+ import { debounce } from './debounce.mjs';
9
+
10
+ const operators: readonly (readonly [
11
+ string,
12
+ (milliSeconds: number) => KeepInitialValueOperator<number, number>,
13
+ ])[] = [
14
+ ['debounce', debounce],
15
+ ['audit', audit],
16
+ ] as const;
17
+
18
+ describe.each(operators)('%s', (_name, operator) => {
19
+ const setup = (): Readonly<{
20
+ source$: SourceObservable<number>;
21
+ operated$: Observable<number>;
22
+ history: readonly string[];
23
+ }> => {
24
+ const source$ = source<number>();
25
+
26
+ const operated$ = source$.pipe(operator(100));
27
+
28
+ const mut_history: string[] = [];
29
+
30
+ operated$.subscribe(
31
+ (v) => {
32
+ mut_history.push(`next ${v}`);
33
+ },
34
+ () => {
35
+ mut_history.push('complete');
36
+ },
37
+ );
38
+
39
+ return { source$, operated$, history: mut_history };
40
+ };
41
+
42
+ test('passes on the pending value, then completes, when the source completes within the window', () => {
43
+ vi.useFakeTimers();
44
+
45
+ const { source$, operated$, history } = setup();
46
+
47
+ source$.next(1);
48
+
49
+ source$.next(2);
50
+
51
+ source$.complete();
52
+
53
+ assert.deepStrictEqual(history, ['next 2', 'complete']);
54
+
55
+ assert.isTrue(operated$.isCompleted);
56
+
57
+ vi.runAllTimers();
58
+
59
+ assert.deepStrictEqual(history, ['next 2', 'complete']);
60
+
61
+ vi.useRealTimers();
62
+ });
63
+
64
+ test('passes on the pending value, then completes, when it is completed itself', () => {
65
+ vi.useFakeTimers();
66
+
67
+ const { source$, operated$, history } = setup();
68
+
69
+ source$.next(1);
70
+
71
+ operated$.complete();
72
+
73
+ assert.deepStrictEqual(history, ['next 1', 'complete']);
74
+
75
+ vi.runAllTimers();
76
+
77
+ assert.deepStrictEqual(history, ['next 1', 'complete']);
78
+
79
+ vi.useRealTimers();
80
+ });
81
+
82
+ test('completes without emitting when nothing is pending', () => {
83
+ vi.useFakeTimers();
84
+
85
+ const { source$, operated$, history } = setup();
86
+
87
+ source$.next(1);
88
+
89
+ vi.runAllTimers();
90
+
91
+ assert.deepStrictEqual(history, ['next 1']);
92
+
93
+ operated$.complete();
94
+
95
+ assert.deepStrictEqual(history, ['next 1', 'complete']);
96
+
97
+ vi.useRealTimers();
98
+ });
99
+
100
+ test('drops the pending value when it is disposed', () => {
101
+ vi.useFakeTimers();
102
+
103
+ const { source$, operated$, history } = setup();
104
+
105
+ source$.next(1);
106
+
107
+ operated$.dispose();
108
+
109
+ assert.deepStrictEqual(history, ['complete']);
110
+
111
+ assert.isTrue(operated$.isCompleted);
112
+
113
+ vi.runAllTimers();
114
+
115
+ assert.deepStrictEqual(history, ['complete']);
116
+
117
+ assert.isFalse(source$.isCompleted);
118
+
119
+ vi.useRealTimers();
120
+ });
121
+ });