synstate 3.4.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 (294) hide show
  1. package/README.md +13 -8
  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.map +1 -1
  46. package/dist/core/combine/merge.mjs +26 -14
  47. package/dist/core/combine/merge.mjs.map +1 -1
  48. package/dist/core/combine/zip.d.mts.map +1 -1
  49. package/dist/core/combine/zip.mjs +43 -25
  50. package/dist/core/combine/zip.mjs.map +1 -1
  51. package/dist/core/create/counter.d.mts.map +1 -1
  52. package/dist/core/create/counter.mjs +65 -44
  53. package/dist/core/create/counter.mjs.map +1 -1
  54. package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
  55. package/dist/core/create/from-abortable-promise.mjs +26 -18
  56. package/dist/core/create/from-abortable-promise.mjs.map +1 -1
  57. package/dist/core/create/from-promise.d.mts.map +1 -1
  58. package/dist/core/create/from-promise.mjs +30 -21
  59. package/dist/core/create/from-promise.mjs.map +1 -1
  60. package/dist/core/create/from-subscribable.d.mts.map +1 -1
  61. package/dist/core/create/from-subscribable.mjs +37 -24
  62. package/dist/core/create/from-subscribable.mjs.map +1 -1
  63. package/dist/core/create/just.d.mts.map +1 -1
  64. package/dist/core/create/just.mjs +4 -2
  65. package/dist/core/create/just.mjs.map +1 -1
  66. package/dist/core/create/source.d.mts.map +1 -1
  67. package/dist/core/create/source.mjs +27 -17
  68. package/dist/core/create/source.mjs.map +1 -1
  69. package/dist/core/create/timer.d.mts.map +1 -1
  70. package/dist/core/create/timer.mjs +40 -25
  71. package/dist/core/create/timer.mjs.map +1 -1
  72. package/dist/core/index.d.mts +2 -0
  73. package/dist/core/index.d.mts.map +1 -1
  74. package/dist/core/index.mjs +2 -0
  75. package/dist/core/index.mjs.map +1 -1
  76. package/dist/core/operators/audit.d.mts +3 -0
  77. package/dist/core/operators/audit.d.mts.map +1 -1
  78. package/dist/core/operators/audit.mjs +56 -28
  79. package/dist/core/operators/audit.mjs.map +1 -1
  80. package/dist/core/operators/debounce.d.mts +3 -0
  81. package/dist/core/operators/debounce.d.mts.map +1 -1
  82. package/dist/core/operators/debounce.mjs +57 -26
  83. package/dist/core/operators/debounce.mjs.map +1 -1
  84. package/dist/core/operators/filter.d.mts.map +1 -1
  85. package/dist/core/operators/filter.mjs +35 -21
  86. package/dist/core/operators/filter.mjs.map +1 -1
  87. package/dist/core/operators/index.d.mts +1 -0
  88. package/dist/core/operators/index.d.mts.map +1 -1
  89. package/dist/core/operators/index.mjs +1 -0
  90. package/dist/core/operators/index.mjs.map +1 -1
  91. package/dist/core/operators/map.d.mts.map +1 -1
  92. package/dist/core/operators/map.mjs +31 -17
  93. package/dist/core/operators/map.mjs.map +1 -1
  94. package/dist/core/operators/merge-map.d.mts +5 -0
  95. package/dist/core/operators/merge-map.d.mts.map +1 -1
  96. package/dist/core/operators/merge-map.mjs +52 -26
  97. package/dist/core/operators/merge-map.mjs.map +1 -1
  98. package/dist/core/operators/pairwise.d.mts.map +1 -1
  99. package/dist/core/operators/pairwise.mjs +31 -20
  100. package/dist/core/operators/pairwise.mjs.map +1 -1
  101. package/dist/core/operators/scan.d.mts.map +1 -1
  102. package/dist/core/operators/scan.mjs +27 -14
  103. package/dist/core/operators/scan.mjs.map +1 -1
  104. package/dist/core/operators/skip-if-no-change.d.mts.map +1 -1
  105. package/dist/core/operators/skip-if-no-change.mjs +33 -19
  106. package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
  107. package/dist/core/operators/skip-until.d.mts.map +1 -1
  108. package/dist/core/operators/skip-until.mjs +40 -26
  109. package/dist/core/operators/skip-until.mjs.map +1 -1
  110. package/dist/core/operators/skip-while.d.mts.map +1 -1
  111. package/dist/core/operators/skip-while.mjs +35 -20
  112. package/dist/core/operators/skip-while.mjs.map +1 -1
  113. package/dist/core/operators/switch-map.d.mts +5 -0
  114. package/dist/core/operators/switch-map.d.mts.map +1 -1
  115. package/dist/core/operators/switch-map.mjs +56 -25
  116. package/dist/core/operators/switch-map.mjs.map +1 -1
  117. package/dist/core/operators/take-until.d.mts.map +1 -1
  118. package/dist/core/operators/take-until.mjs +36 -29
  119. package/dist/core/operators/take-until.mjs.map +1 -1
  120. package/dist/core/operators/take-while.d.mts.map +1 -1
  121. package/dist/core/operators/take-while.mjs +36 -22
  122. package/dist/core/operators/take-while.mjs.map +1 -1
  123. package/dist/core/{predefined/operators → operators}/take.d.mts +7 -3
  124. package/dist/core/operators/take.d.mts.map +1 -0
  125. package/dist/core/operators/take.mjs +102 -0
  126. package/dist/core/operators/take.mjs.map +1 -0
  127. package/dist/core/operators/throttle.d.mts.map +1 -1
  128. package/dist/core/operators/throttle.mjs +41 -26
  129. package/dist/core/operators/throttle.mjs.map +1 -1
  130. package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
  131. package/dist/core/operators/with-buffered-from.mjs +43 -39
  132. package/dist/core/operators/with-buffered-from.mjs.map +1 -1
  133. package/dist/core/operators/with-current-value-from.d.mts.map +1 -1
  134. package/dist/core/operators/with-current-value-from.mjs +36 -21
  135. package/dist/core/operators/with-current-value-from.mjs.map +1 -1
  136. package/dist/core/operators/with-initial-value.d.mts.map +1 -1
  137. package/dist/core/operators/with-initial-value.mjs +24 -12
  138. package/dist/core/operators/with-initial-value.mjs.map +1 -1
  139. package/dist/core/predefined/operators/index.d.mts +0 -1
  140. package/dist/core/predefined/operators/index.d.mts.map +1 -1
  141. package/dist/core/predefined/operators/index.mjs +0 -1
  142. package/dist/core/predefined/operators/index.mjs.map +1 -1
  143. package/dist/core/scope/index.d.mts +2 -0
  144. package/dist/core/scope/index.d.mts.map +1 -0
  145. package/dist/core/scope/index.mjs +2 -0
  146. package/dist/core/scope/index.mjs.map +1 -0
  147. package/dist/core/scope/scope.d.mts +51 -0
  148. package/dist/core/scope/scope.d.mts.map +1 -0
  149. package/dist/core/scope/scope.mjs +194 -0
  150. package/dist/core/scope/scope.mjs.map +1 -0
  151. package/dist/core/types/id.d.mts +0 -2
  152. package/dist/core/types/id.d.mts.map +1 -1
  153. package/dist/core/types/index.d.mts +1 -0
  154. package/dist/core/types/index.d.mts.map +1 -1
  155. package/dist/core/types/index.mjs +1 -0
  156. package/dist/core/types/index.mjs.map +1 -1
  157. package/dist/core/types/observable-family.d.mts +1 -0
  158. package/dist/core/types/observable-family.d.mts.map +1 -1
  159. package/dist/core/types/observable.d.mts +40 -28
  160. package/dist/core/types/observable.d.mts.map +1 -1
  161. package/dist/core/types/observable.mjs +24 -0
  162. package/dist/core/types/observable.mjs.map +1 -1
  163. package/dist/core/types/scope.d.mts +27 -0
  164. package/dist/core/types/scope.d.mts.map +1 -0
  165. package/dist/core/types/scope.mjs +2 -0
  166. package/dist/core/types/scope.mjs.map +1 -0
  167. package/dist/core/utils/current-scope.d.mts +30 -0
  168. package/dist/core/utils/current-scope.d.mts.map +1 -0
  169. package/dist/core/utils/current-scope.mjs +26 -0
  170. package/dist/core/utils/current-scope.mjs.map +1 -0
  171. package/dist/core/utils/id-maker.d.mts +14 -3
  172. package/dist/core/utils/id-maker.d.mts.map +1 -1
  173. package/dist/core/utils/id-maker.mjs +18 -14
  174. package/dist/core/utils/id-maker.mjs.map +1 -1
  175. package/dist/core/utils/index.d.mts +2 -1
  176. package/dist/core/utils/index.d.mts.map +1 -1
  177. package/dist/core/utils/index.mjs +2 -1
  178. package/dist/core/utils/index.mjs.map +1 -1
  179. package/dist/core/utils/max-depth.d.mts +1 -0
  180. package/dist/core/utils/max-depth.d.mts.map +1 -1
  181. package/dist/core/utils/max-depth.mjs +9 -7
  182. package/dist/core/utils/max-depth.mjs.map +1 -1
  183. package/dist/core/utils/observable-brand.d.mts +13 -0
  184. package/dist/core/utils/observable-brand.d.mts.map +1 -0
  185. package/dist/core/utils/observable-brand.mjs +13 -0
  186. package/dist/core/utils/observable-brand.mjs.map +1 -0
  187. package/dist/utils/create-boolean-state.d.mts +2 -2
  188. package/dist/utils/create-reducer.d.mts +7 -7
  189. package/dist/utils/create-reducer.mjs +2 -2
  190. package/dist/utils/create-reducer.mjs.map +1 -1
  191. package/dist/utils/create-state.d.mts +2 -2
  192. package/package.json +16 -16
  193. package/src/core/authoring.mts +19 -0
  194. package/src/core/class/child-observable-class.mts +374 -0
  195. package/src/core/{base → class}/circular-dependency-comparison.test.mts +46 -38
  196. package/src/core/{base → class}/circular-dependency.test.mts +38 -2
  197. package/src/core/class/complete-propagation.test.mts +264 -0
  198. package/src/core/class/create-child-observable.mts +370 -0
  199. package/src/core/class/create-root-observable.mts +107 -0
  200. package/src/core/class/index.mts +8 -0
  201. package/src/core/class/manager-observable-class.mts +166 -0
  202. package/src/core/class/observable-base-class.mts +315 -0
  203. package/src/core/{base → class}/observable-base.test.mts +1 -1
  204. package/src/core/class/observable-node.mts +54 -0
  205. package/src/core/class/propagation-order.test.mts +92 -0
  206. package/src/core/class/propagation-state.mts +80 -0
  207. package/src/core/class/remove-completed.test.mts +369 -0
  208. package/src/core/class/root-observable-class.mts +85 -0
  209. package/src/core/{base → class}/write-during-propagation.test.mts +3 -3
  210. package/src/core/combine/combine.mts +67 -55
  211. package/src/core/combine/merge.mts +33 -23
  212. package/src/core/combine/zip.mts +95 -81
  213. package/src/core/create/counter.mts +95 -65
  214. package/src/core/create/from-abortable-promise.mts +54 -42
  215. package/src/core/create/from-promise.mts +38 -29
  216. package/src/core/create/from-subscribable.mts +62 -45
  217. package/src/core/create/from-subscribable.test.mts +8 -3
  218. package/src/core/create/just.mts +4 -5
  219. package/src/core/create/source.mts +29 -20
  220. package/src/core/create/timer.mts +51 -32
  221. package/src/core/index.mts +2 -0
  222. package/src/core/operators/audit.mts +79 -45
  223. package/src/core/operators/debounce-audit-complete.test.mts +121 -0
  224. package/src/core/operators/debounce.mts +81 -40
  225. package/src/core/operators/filter.mts +48 -31
  226. package/src/core/operators/index.mts +1 -0
  227. package/src/core/operators/inner-ownership.test.mts +280 -0
  228. package/src/core/operators/map.mts +49 -32
  229. package/src/core/operators/merge-map.mts +81 -43
  230. package/src/core/operators/pairwise.mts +40 -30
  231. package/src/core/operators/scan.mts +41 -24
  232. package/src/core/operators/skip-if-no-change.mts +45 -31
  233. package/src/core/operators/skip-until.mts +55 -41
  234. package/src/core/operators/skip-while.mts +49 -31
  235. package/src/core/operators/switch-map.mts +74 -35
  236. package/src/core/operators/take-until-skip-until-teardown.test.mts +18 -12
  237. package/src/core/operators/take-until.mts +45 -41
  238. package/src/core/operators/take-while.mts +50 -33
  239. package/src/core/operators/take.mts +138 -0
  240. package/src/core/operators/throttle.mts +55 -38
  241. package/src/core/operators/with-buffered-from.mts +42 -44
  242. package/src/core/operators/with-buffered-from.test.mts +4 -3
  243. package/src/core/operators/with-current-value-from.mts +39 -25
  244. package/src/core/operators/with-initial-value.mts +30 -21
  245. package/src/core/predefined/operators/index.mts +0 -1
  246. package/src/core/scope/index.mts +1 -0
  247. package/src/core/scope/scope.mts +260 -0
  248. package/src/core/scope/scope.test.mts +462 -0
  249. package/src/core/types/id.mts +0 -4
  250. package/src/core/types/index.mts +1 -0
  251. package/src/core/types/observable-family.mts +2 -0
  252. package/src/core/types/observable.mts +77 -42
  253. package/src/core/types/scope.mts +27 -0
  254. package/src/core/utils/current-scope.mts +61 -0
  255. package/src/core/utils/id-maker.mts +20 -30
  256. package/src/core/utils/index.mts +2 -1
  257. package/src/core/utils/max-depth.mts +12 -9
  258. package/src/core/utils/observable-brand.mts +12 -0
  259. package/src/utils/create-boolean-state.mts +2 -2
  260. package/src/utils/create-boolean-state.test.mts +18 -4
  261. package/src/utils/create-reducer.mts +9 -9
  262. package/src/utils/create-reducer.test.mts +31 -14
  263. package/src/utils/create-state.mts +2 -2
  264. package/src/utils/create-state.test.mts +12 -14
  265. package/dist/core/base/create-child-observable.d.mts +0 -72
  266. package/dist/core/base/create-child-observable.d.mts.map +0 -1
  267. package/dist/core/base/create-child-observable.mjs +0 -285
  268. package/dist/core/base/create-child-observable.mjs.map +0 -1
  269. package/dist/core/base/create-observable-base.d.mts +0 -149
  270. package/dist/core/base/create-observable-base.d.mts.map +0 -1
  271. package/dist/core/base/create-observable-base.mjs +0 -273
  272. package/dist/core/base/create-observable-base.mjs.map +0 -1
  273. package/dist/core/base/create-root-observable.d.mts +0 -46
  274. package/dist/core/base/create-root-observable.d.mts.map +0 -1
  275. package/dist/core/base/create-root-observable.mjs +0 -80
  276. package/dist/core/base/create-root-observable.mjs.map +0 -1
  277. package/dist/core/base/index.d.mts +0 -3
  278. package/dist/core/base/index.d.mts.map +0 -1
  279. package/dist/core/base/index.mjs +0 -3
  280. package/dist/core/base/index.mjs.map +0 -1
  281. package/dist/core/predefined/operators/take.d.mts.map +0 -1
  282. package/dist/core/predefined/operators/take.mjs +0 -44
  283. package/dist/core/predefined/operators/take.mjs.map +0 -1
  284. package/dist/core/utils/utils.d.mts +0 -6
  285. package/dist/core/utils/utils.d.mts.map +0 -1
  286. package/dist/core/utils/utils.mjs +0 -37
  287. package/dist/core/utils/utils.mjs.map +0 -1
  288. package/src/core/base/create-child-observable.mts +0 -485
  289. package/src/core/base/create-observable-base.mts +0 -568
  290. package/src/core/base/create-root-observable.mts +0 -147
  291. package/src/core/base/index.mts +0 -2
  292. package/src/core/predefined/operators/take.mts +0 -48
  293. package/src/core/utils/utils.mts +0 -56
  294. package/src/core/utils/utils.test.mts +0 -88
@@ -0,0 +1,462 @@
1
+ // cspell:ignore unstub
2
+ import { expectType } from 'ts-data-forge';
3
+ import type { Mock } from 'vitest';
4
+ import { toNode } from '../class/index.mjs';
5
+ import { source } from '../create/index.mjs';
6
+ import { map } from '../operators/index.mjs';
7
+ import type { Scope } from '../types/index.mjs';
8
+ import {
9
+ createScope,
10
+ getCurrentScope,
11
+ onScopeDispose,
12
+ untracked,
13
+ } from './scope.mjs';
14
+
15
+ describe(createScope, () => {
16
+ test('types', () => {
17
+ expectType<ReturnType<typeof createScope>, Scope>('=');
18
+
19
+ expectType<ReturnType<typeof getCurrentScope>, Scope | undefined>('=');
20
+
21
+ expectType<ReturnType<typeof untracked<number>>, number>('=');
22
+ });
23
+
24
+ test('run returns what fn returns and makes the scope current only while it runs', () => {
25
+ const scope = createScope();
26
+
27
+ assert.isUndefined(getCurrentScope());
28
+
29
+ const result = scope.run(() => {
30
+ assert.strictEqual(getCurrentScope(), scope);
31
+
32
+ return 42;
33
+ });
34
+
35
+ assert.strictEqual(result, 42);
36
+
37
+ assert.isUndefined(getCurrentScope());
38
+ });
39
+
40
+ test('the outer scope is current again after a nested run, and after fn throws', () => {
41
+ const outer = createScope();
42
+
43
+ const inner = createScope(outer);
44
+
45
+ outer.run(() => {
46
+ inner.run(() => {
47
+ assert.strictEqual(getCurrentScope(), inner);
48
+ });
49
+
50
+ assert.strictEqual(getCurrentScope(), outer);
51
+
52
+ assert.throws(() => {
53
+ // throws a SyntaxError
54
+ inner.run(() => JSON.parse('{'));
55
+ }, SyntaxError);
56
+
57
+ assert.strictEqual(getCurrentScope(), outer);
58
+ });
59
+
60
+ assert.isUndefined(getCurrentScope());
61
+ });
62
+
63
+ test('an observable created inside run is disposed with the scope; a shared parent outside it is not', () => {
64
+ const store$ = source<number>();
65
+
66
+ const scope = createScope();
67
+
68
+ const doubled$ = scope.run(() => store$.pipe(map((x) => x * 2)));
69
+
70
+ const mut_history: number[] = [];
71
+
72
+ doubled$.subscribe((x) => {
73
+ mut_history.push(x);
74
+ });
75
+
76
+ store$.next(1);
77
+
78
+ assert.deepStrictEqual(mut_history, [2]);
79
+
80
+ scope.dispose();
81
+
82
+ assert.isTrue(doubled$.isCompleted);
83
+
84
+ assert.isFalse(store$.isCompleted);
85
+
86
+ assert.isFalse(toNode(store$).hasChild);
87
+
88
+ store$.next(2);
89
+
90
+ assert.deepStrictEqual(mut_history, [2]);
91
+ });
92
+
93
+ test('an observable created outside any scope belongs to nothing', () => {
94
+ const store$ = source<number>();
95
+
96
+ const doubled$ = store$.pipe(map((x) => x * 2));
97
+
98
+ const scope = createScope();
99
+
100
+ scope.run(() => {
101
+ // a derived node created elsewhere is not adopted by being used here
102
+ doubled$.subscribe(() => {});
103
+ });
104
+
105
+ scope.dispose();
106
+
107
+ assert.isFalse(doubled$.isCompleted);
108
+ });
109
+
110
+ test('nodes go to the innermost scope', () => {
111
+ const store$ = source<number>();
112
+
113
+ const outer = createScope();
114
+
115
+ const inner = createScope(outer);
116
+
117
+ const [a$, b$] = outer.run(() => {
118
+ const a = store$.pipe(map((x) => x + 1));
119
+
120
+ const b = inner.run(() => store$.pipe(map((x) => x + 2)));
121
+
122
+ return [a, b] as const;
123
+ });
124
+
125
+ inner.dispose();
126
+
127
+ assert.isFalse(a$.isCompleted);
128
+
129
+ assert.isTrue(b$.isCompleted);
130
+
131
+ outer.dispose();
132
+
133
+ assert.isTrue(a$.isCompleted);
134
+ });
135
+
136
+ test('dispose runs child scopes, then cleanups in reverse, then owned nodes in reverse creation order', () => {
137
+ const store$ = source<number>();
138
+
139
+ const parent = createScope();
140
+
141
+ const child1 = createScope(parent);
142
+
143
+ const child2 = createScope(parent);
144
+
145
+ const mut_order: string[] = [];
146
+
147
+ child1.run(() => {
148
+ onScopeDispose(() => {
149
+ mut_order.push('child1');
150
+ });
151
+ });
152
+
153
+ child2.run(() => {
154
+ onScopeDispose(() => {
155
+ mut_order.push('child2');
156
+ });
157
+ });
158
+
159
+ parent.run(() => {
160
+ const first$ = store$.pipe(map((x) => x));
161
+
162
+ first$.subscribe(
163
+ () => {},
164
+ () => {
165
+ mut_order.push('node first');
166
+ },
167
+ );
168
+
169
+ onScopeDispose(() => {
170
+ mut_order.push('cleanup 1');
171
+ });
172
+
173
+ const second$ = store$.pipe(map((x) => x));
174
+
175
+ second$.subscribe(
176
+ () => {},
177
+ () => {
178
+ mut_order.push('node second');
179
+ },
180
+ );
181
+
182
+ onScopeDispose(() => {
183
+ mut_order.push('cleanup 2');
184
+ });
185
+ });
186
+
187
+ parent.dispose();
188
+
189
+ assert.deepStrictEqual(mut_order, [
190
+ 'child2',
191
+ 'child1',
192
+ 'cleanup 2',
193
+ 'cleanup 1',
194
+ 'node second',
195
+ 'node first',
196
+ ]);
197
+ });
198
+
199
+ test('dispose is idempotent', () => {
200
+ const scope = createScope();
201
+
202
+ const mut_calls: number[] = [];
203
+
204
+ scope.run(() => {
205
+ onScopeDispose(() => {
206
+ mut_calls.push(1);
207
+ });
208
+ });
209
+
210
+ scope.dispose();
211
+
212
+ scope.dispose();
213
+
214
+ assert.deepStrictEqual(mut_calls, [1]);
215
+ });
216
+
217
+ test('a disposed child scope leaves its parent, so the parent does not dispose it again', () => {
218
+ const parent = createScope();
219
+
220
+ const child = createScope(parent);
221
+
222
+ const mut_calls: string[] = [];
223
+
224
+ child.run(() => {
225
+ onScopeDispose(() => {
226
+ mut_calls.push('child');
227
+ });
228
+ });
229
+
230
+ child.dispose();
231
+
232
+ parent.dispose();
233
+
234
+ assert.deepStrictEqual(mut_calls, ['child']);
235
+ });
236
+
237
+ test('a node that completes on its own is released by its scope', () => {
238
+ const store$ = source<number>();
239
+
240
+ const scope = createScope();
241
+
242
+ const mut_completions: number[] = [];
243
+
244
+ const doubled$ = scope.run(() => store$.pipe(map((x) => x * 2)));
245
+
246
+ doubled$.subscribe(
247
+ () => {},
248
+ () => {
249
+ mut_completions.push(1);
250
+ },
251
+ );
252
+
253
+ doubled$.complete();
254
+
255
+ scope.dispose();
256
+
257
+ assert.deepStrictEqual(mut_completions, [1]);
258
+ });
259
+
260
+ test('after many child scopes are disposed on their own, the parent disposes the rest once, newest first', () => {
261
+ const parent = createScope();
262
+
263
+ const mut_order: number[] = [];
264
+
265
+ const children = Array.from({ length: 100 }, (_, i) => {
266
+ const child = createScope(parent);
267
+
268
+ child.run(() => {
269
+ onScopeDispose(() => {
270
+ mut_order.push(i);
271
+ });
272
+ });
273
+
274
+ return child;
275
+ });
276
+
277
+ // Every child whose index is not a multiple of 3 goes on its own: 66 of
278
+ // them, enough for the parent to rebuild its list of children.
279
+ for (const [i, child] of children.entries()) {
280
+ if (i % 3 !== 0) {
281
+ child.dispose();
282
+ }
283
+ }
284
+
285
+ const disposedAlone = mut_order.length;
286
+
287
+ parent.dispose();
288
+
289
+ assert.strictEqual(disposedAlone, 66);
290
+
291
+ assert.deepStrictEqual(
292
+ mut_order.slice(disposedAlone),
293
+ Array.from({ length: 34 }, (_, k) => (33 - k) * 3),
294
+ );
295
+ });
296
+
297
+ test('after many owned nodes complete on their own, the scope disposes the rest once, newest first', () => {
298
+ const store$ = source<number>();
299
+
300
+ const scope = createScope();
301
+
302
+ const mut_completions: number[] = [];
303
+
304
+ const nodes = scope.run(() =>
305
+ Array.from({ length: 100 }, (_, i) => {
306
+ const node$ = store$.pipe(map((x) => x + i));
307
+
308
+ node$.subscribe(
309
+ () => {},
310
+ () => {
311
+ mut_completions.push(i);
312
+ },
313
+ );
314
+
315
+ return node$;
316
+ }),
317
+ );
318
+
319
+ for (const [i, node$] of nodes.entries()) {
320
+ if (i % 3 !== 0) {
321
+ node$.complete();
322
+ }
323
+ }
324
+
325
+ const completedAlone = mut_completions.length;
326
+
327
+ scope.dispose();
328
+
329
+ assert.strictEqual(completedAlone, 66);
330
+
331
+ assert.deepStrictEqual(
332
+ mut_completions.slice(completedAlone),
333
+ Array.from({ length: 34 }, (_, k) => (33 - k) * 3),
334
+ );
335
+ });
336
+
337
+ test('nodes created inside untracked belong to no scope', () => {
338
+ const store$ = source<number>();
339
+
340
+ const scope = createScope();
341
+
342
+ const [owned$, free$] = scope.run(() => {
343
+ const owned = store$.pipe(map((x) => x));
344
+
345
+ const free = untracked(() => {
346
+ assert.isUndefined(getCurrentScope());
347
+
348
+ return store$.pipe(map((x) => x));
349
+ });
350
+
351
+ assert.strictEqual(getCurrentScope(), scope);
352
+
353
+ return [owned, free] as const;
354
+ });
355
+
356
+ scope.dispose();
357
+
358
+ assert.isTrue(owned$.isCompleted);
359
+
360
+ assert.isFalse(free$.isCompleted);
361
+ });
362
+
363
+ test('a node created after await belongs to no scope', async () => {
364
+ const store$ = source<number>();
365
+
366
+ const scope = createScope();
367
+
368
+ const promise = scope.run(async () => {
369
+ await Promise.resolve();
370
+
371
+ assert.isUndefined(getCurrentScope());
372
+
373
+ return store$.pipe(map((x) => x));
374
+ });
375
+
376
+ const late$ = await promise;
377
+
378
+ scope.dispose();
379
+
380
+ assert.isFalse(late$.isCompleted);
381
+ });
382
+
383
+ test('run on a disposed scope warns, still runs fn, and owns nothing it creates', () => {
384
+ const warn = stubConsoleWarn();
385
+
386
+ const store$ = source<number>();
387
+
388
+ const scope = createScope();
389
+
390
+ scope.dispose();
391
+
392
+ const free$ = scope.run(() => {
393
+ assert.isUndefined(getCurrentScope());
394
+
395
+ return store$.pipe(map((x) => x));
396
+ });
397
+
398
+ assert.isFalse(free$.isCompleted);
399
+
400
+ expect(warn).toHaveBeenCalledExactlyOnceWith(
401
+ expect.stringContaining('disposed scope'),
402
+ );
403
+
404
+ vi.unstubAllGlobals();
405
+ });
406
+
407
+ test('onScopeDispose outside any scope warns and registers nothing', () => {
408
+ const warn = stubConsoleWarn();
409
+
410
+ onScopeDispose(() => {});
411
+
412
+ expect(warn).toHaveBeenCalledExactlyOnceWith(
413
+ expect.stringContaining('outside any scope'),
414
+ );
415
+
416
+ vi.unstubAllGlobals();
417
+ });
418
+
419
+ test('a scope created under a disposed parent is born disposed', () => {
420
+ const warn = stubConsoleWarn();
421
+
422
+ const parent = createScope();
423
+
424
+ parent.dispose();
425
+
426
+ const child = createScope(parent);
427
+
428
+ expect(warn).toHaveBeenCalledExactlyOnceWith(
429
+ expect.stringContaining('disposed parent'),
430
+ );
431
+
432
+ child.run(() => {
433
+ assert.isUndefined(getCurrentScope());
434
+ });
435
+
436
+ expect(warn).toHaveBeenCalledTimes(2);
437
+
438
+ vi.unstubAllGlobals();
439
+ });
440
+
441
+ test('disposing a parent disposes nodes of its child scopes', () => {
442
+ const store$ = source<number>();
443
+
444
+ const parent = createScope();
445
+
446
+ const child = createScope(parent);
447
+
448
+ const owned$ = child.run(() => store$.pipe(map((x) => x)));
449
+
450
+ parent.dispose();
451
+
452
+ assert.isTrue(owned$.isCompleted);
453
+ });
454
+ });
455
+
456
+ const stubConsoleWarn = (): Mock => {
457
+ const warn = vi.fn();
458
+
459
+ vi.stubGlobal('console', { ...console, warn });
460
+
461
+ return warn;
462
+ };
@@ -1,7 +1,3 @@
1
1
  import type { Brand } from 'ts-type-forge';
2
2
 
3
- export type ObservableId = Brand<symbol, 'observable-id'>;
4
-
5
- export type SubscriberId = Brand<symbol, 'subscriber-id'>;
6
-
7
3
  export type UpdateToken = Brand<symbol, 'update-token'>;
@@ -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';
@@ -210,6 +210,8 @@ export type PairwiseOperatorObservable<A> = SyncChildObservable<
210
210
  readonly [A]
211
211
  >;
212
212
 
213
+ export type TakeOperatorObservable<A> = SyncChildObservable<A, readonly [A]>;
214
+
213
215
  export type TakeWhileOperatorObservable<A> = SyncChildObservable<
214
216
  A,
215
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
+ }>;