synstate 2.0.1 → 3.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 (161) hide show
  1. package/README.md +2 -2
  2. package/dist/core/base/create-child-observable.d.mts +65 -0
  3. package/dist/core/base/create-child-observable.d.mts.map +1 -0
  4. package/dist/core/base/create-child-observable.mjs +157 -0
  5. package/dist/core/base/create-child-observable.mjs.map +1 -0
  6. package/dist/core/base/create-observable-base.d.mts +82 -0
  7. package/dist/core/base/create-observable-base.d.mts.map +1 -0
  8. package/dist/core/base/create-observable-base.mjs +160 -0
  9. package/dist/core/base/create-observable-base.mjs.map +1 -0
  10. package/dist/core/base/create-root-observable.d.mts +37 -0
  11. package/dist/core/base/create-root-observable.d.mts.map +1 -0
  12. package/dist/core/base/create-root-observable.mjs +36 -0
  13. package/dist/core/base/create-root-observable.mjs.map +1 -0
  14. package/dist/core/base/index.d.mts +3 -0
  15. package/dist/core/base/index.d.mts.map +1 -0
  16. package/dist/core/base/index.mjs +3 -0
  17. package/dist/core/{class → base}/index.mjs.map +1 -1
  18. package/dist/core/combine/combine.d.mts.map +1 -1
  19. package/dist/core/combine/combine.mjs +28 -27
  20. package/dist/core/combine/combine.mjs.map +1 -1
  21. package/dist/core/combine/merge.d.mts.map +1 -1
  22. package/dist/core/combine/merge.mjs +11 -13
  23. package/dist/core/combine/merge.mjs.map +1 -1
  24. package/dist/core/combine/zip.d.mts.map +1 -1
  25. package/dist/core/combine/zip.mjs +32 -31
  26. package/dist/core/combine/zip.mjs.map +1 -1
  27. package/dist/core/create/counter.d.mts.map +1 -1
  28. package/dist/core/create/counter.mjs +39 -50
  29. package/dist/core/create/counter.mjs.map +1 -1
  30. package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
  31. package/dist/core/create/from-abortable-promise.mjs +33 -32
  32. package/dist/core/create/from-abortable-promise.mjs.map +1 -1
  33. package/dist/core/create/from-promise.d.mts.map +1 -1
  34. package/dist/core/create/from-promise.mjs +12 -12
  35. package/dist/core/create/from-promise.mjs.map +1 -1
  36. package/dist/core/create/from-subscribable.d.mts.map +1 -1
  37. package/dist/core/create/from-subscribable.mjs +10 -10
  38. package/dist/core/create/from-subscribable.mjs.map +1 -1
  39. package/dist/core/create/just.d.mts.map +1 -1
  40. package/dist/core/create/just.mjs +5 -4
  41. package/dist/core/create/just.mjs.map +1 -1
  42. package/dist/core/create/source.mjs +11 -12
  43. package/dist/core/create/source.mjs.map +1 -1
  44. package/dist/core/create/timer.d.mts.map +1 -1
  45. package/dist/core/create/timer.mjs +32 -38
  46. package/dist/core/create/timer.mjs.map +1 -1
  47. package/dist/core/index.d.mts +0 -1
  48. package/dist/core/index.d.mts.map +1 -1
  49. package/dist/core/index.mjs +0 -3
  50. package/dist/core/index.mjs.map +1 -1
  51. package/dist/core/operators/audit.d.mts.map +1 -1
  52. package/dist/core/operators/audit.mjs +28 -36
  53. package/dist/core/operators/audit.mjs.map +1 -1
  54. package/dist/core/operators/debounce.d.mts.map +1 -1
  55. package/dist/core/operators/debounce.mjs +26 -33
  56. package/dist/core/operators/debounce.mjs.map +1 -1
  57. package/dist/core/operators/filter.d.mts.map +1 -1
  58. package/dist/core/operators/filter.mjs +23 -27
  59. package/dist/core/operators/filter.mjs.map +1 -1
  60. package/dist/core/operators/map.d.mts.map +1 -1
  61. package/dist/core/operators/map.mjs +21 -28
  62. package/dist/core/operators/map.mjs.map +1 -1
  63. package/dist/core/operators/merge-map.d.mts.map +1 -1
  64. package/dist/core/operators/merge-map.mjs +30 -39
  65. package/dist/core/operators/merge-map.mjs.map +1 -1
  66. package/dist/core/operators/pairwise.d.mts.map +1 -1
  67. package/dist/core/operators/pairwise.mjs +22 -24
  68. package/dist/core/operators/pairwise.mjs.map +1 -1
  69. package/dist/core/operators/scan.d.mts.map +1 -1
  70. package/dist/core/operators/scan.mjs +13 -18
  71. package/dist/core/operators/scan.mjs.map +1 -1
  72. package/dist/core/operators/skip-if-no-change.d.mts.map +1 -1
  73. package/dist/core/operators/skip-if-no-change.mjs +22 -26
  74. package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
  75. package/dist/core/operators/skip-until.d.mts.map +1 -1
  76. package/dist/core/operators/skip-until.mjs +23 -24
  77. package/dist/core/operators/skip-until.mjs.map +1 -1
  78. package/dist/core/operators/skip-while.d.mts.map +1 -1
  79. package/dist/core/operators/skip-while.mjs +27 -32
  80. package/dist/core/operators/skip-while.mjs.map +1 -1
  81. package/dist/core/operators/switch-map.d.mts.map +1 -1
  82. package/dist/core/operators/switch-map.mjs +28 -34
  83. package/dist/core/operators/switch-map.mjs.map +1 -1
  84. package/dist/core/operators/take-until.d.mts.map +1 -1
  85. package/dist/core/operators/take-until.mjs +22 -22
  86. package/dist/core/operators/take-until.mjs.map +1 -1
  87. package/dist/core/operators/take-while.d.mts.map +1 -1
  88. package/dist/core/operators/take-while.mjs +25 -29
  89. package/dist/core/operators/take-while.mjs.map +1 -1
  90. package/dist/core/operators/throttle.d.mts.map +1 -1
  91. package/dist/core/operators/throttle.mjs +27 -35
  92. package/dist/core/operators/throttle.mjs.map +1 -1
  93. package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
  94. package/dist/core/operators/with-buffered-from.mjs +24 -24
  95. package/dist/core/operators/with-buffered-from.mjs.map +1 -1
  96. package/dist/core/operators/with-current-value-from.d.mts.map +1 -1
  97. package/dist/core/operators/with-current-value-from.mjs +20 -25
  98. package/dist/core/operators/with-current-value-from.mjs.map +1 -1
  99. package/dist/core/operators/with-initial-value.d.mts.map +1 -1
  100. package/dist/core/operators/with-initial-value.mjs +12 -15
  101. package/dist/core/operators/with-initial-value.mjs.map +1 -1
  102. package/dist/entry-point.mjs +0 -3
  103. package/dist/entry-point.mjs.map +1 -1
  104. package/dist/index.mjs +0 -3
  105. package/dist/index.mjs.map +1 -1
  106. package/package.json +25 -18
  107. package/src/core/{class → base}/circular-dependency-comparison.test.mts +28 -18
  108. package/src/core/base/circular-dependency.test.mts +322 -0
  109. package/src/core/base/create-child-observable.mts +340 -0
  110. package/src/core/base/create-observable-base.mts +358 -0
  111. package/src/core/base/create-root-observable.mts +102 -0
  112. package/src/core/base/index.mts +2 -0
  113. package/src/core/{class/observable.class.test.mts → base/observable-base.test.mts} +25 -17
  114. package/src/core/combine/combine.mts +51 -53
  115. package/src/core/combine/merge.mts +21 -23
  116. package/src/core/combine/zip.mts +54 -58
  117. package/src/core/create/counter.mts +42 -59
  118. package/src/core/create/from-abortable-promise.mts +38 -45
  119. package/src/core/create/from-promise.mts +25 -28
  120. package/src/core/create/from-subscribable.mts +23 -27
  121. package/src/core/create/just.mts +5 -4
  122. package/src/core/create/source.mts +11 -17
  123. package/src/core/create/timer.mts +32 -47
  124. package/src/core/index.mts +0 -1
  125. package/src/core/operators/audit.mts +43 -57
  126. package/src/core/operators/debounce.mts +35 -45
  127. package/src/core/operators/filter.mts +31 -39
  128. package/src/core/operators/map.mts +32 -40
  129. package/src/core/operators/merge-map.mts +44 -59
  130. package/src/core/operators/pairwise.mts +30 -33
  131. package/src/core/operators/scan.mts +24 -35
  132. package/src/core/operators/skip-if-no-change.mts +31 -36
  133. package/src/core/operators/skip-until.mts +32 -35
  134. package/src/core/operators/skip-while.mts +31 -40
  135. package/src/core/operators/switch-map.mts +36 -48
  136. package/src/core/operators/take-until.mts +31 -29
  137. package/src/core/operators/take-while.mts +33 -41
  138. package/src/core/operators/throttle.mts +37 -51
  139. package/src/core/operators/with-buffered-from.mts +35 -30
  140. package/src/core/operators/with-current-value-from.mts +23 -28
  141. package/src/core/operators/with-initial-value.mts +21 -22
  142. package/dist/core/class/child-observable-class.d.mts +0 -39
  143. package/dist/core/class/child-observable-class.d.mts.map +0 -1
  144. package/dist/core/class/child-observable-class.mjs +0 -173
  145. package/dist/core/class/child-observable-class.mjs.map +0 -1
  146. package/dist/core/class/index.d.mts +0 -4
  147. package/dist/core/class/index.d.mts.map +0 -1
  148. package/dist/core/class/index.mjs +0 -4
  149. package/dist/core/class/observable-base-class.d.mts +0 -28
  150. package/dist/core/class/observable-base-class.d.mts.map +0 -1
  151. package/dist/core/class/observable-base-class.mjs +0 -120
  152. package/dist/core/class/observable-base-class.mjs.map +0 -1
  153. package/dist/core/class/root-observable-class.d.mts +0 -14
  154. package/dist/core/class/root-observable-class.d.mts.map +0 -1
  155. package/dist/core/class/root-observable-class.mjs +0 -43
  156. package/dist/core/class/root-observable-class.mjs.map +0 -1
  157. package/src/core/class/child-observable-class.mts +0 -286
  158. package/src/core/class/circular-dependency.test.mts +0 -251
  159. package/src/core/class/index.mts +0 -3
  160. package/src/core/class/observable-base-class.mts +0 -187
  161. package/src/core/class/root-observable-class.mts +0 -73
@@ -0,0 +1,322 @@
1
+ /* eslint-disable functional/immutable-data */
2
+ import { Optional } from 'ts-data-forge';
3
+ import { combine, merge } from '../combine/index.mjs';
4
+ import { source } from '../create/index.mjs';
5
+ import { map } from '../operators/index.mjs';
6
+ import {
7
+ createAsyncChildObservable,
8
+ createSyncChildObservable,
9
+ } from './create-child-observable.mjs';
10
+ import { createTryUpdateNotImplemented } from './create-observable-base.mjs';
11
+ import { createRootObservable } from './create-root-observable.mjs';
12
+
13
+ describe('circular dependency detection', () => {
14
+ describe('cycle in ancestor graph', () => {
15
+ test('should throw when parent chain contains a cycle (A -> B -> A)', () => {
16
+ const root = createRootObservable(
17
+ { initialValue: Optional.some(0) },
18
+ () => ({}),
19
+ );
20
+
21
+ const childA = createSyncChildObservable(
22
+ {
23
+ parents: [root],
24
+ initialValue: Optional.some(0),
25
+ },
26
+ createTryUpdateNotImplemented,
27
+ );
28
+
29
+ const childB = createSyncChildObservable(
30
+ {
31
+ parents: [childA],
32
+ initialValue: Optional.some(0),
33
+ },
34
+ createTryUpdateNotImplemented,
35
+ );
36
+
37
+ // Mutate childA.parents to create a cycle: childA -> childB -> childA
38
+ Object.defineProperty(childA, 'parents', {
39
+ value: [childB],
40
+ writable: false,
41
+ configurable: true,
42
+ });
43
+
44
+ expect(() => {
45
+ createSyncChildObservable(
46
+ {
47
+ parents: [childA],
48
+ initialValue: Optional.some(0),
49
+ },
50
+ createTryUpdateNotImplemented,
51
+ );
52
+ }).toThrow(
53
+ 'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
54
+ );
55
+ });
56
+
57
+ test('should throw when AsyncChildObservable parent chain contains a cycle', () => {
58
+ const root = createRootObservable(
59
+ { initialValue: Optional.some(0) },
60
+ () => ({}),
61
+ );
62
+
63
+ const childA = createAsyncChildObservable(
64
+ {
65
+ parents: [root],
66
+ initialValue: Optional.some(0),
67
+ },
68
+ createTryUpdateNotImplemented,
69
+ );
70
+
71
+ const childB = createSyncChildObservable(
72
+ {
73
+ parents: [childA],
74
+ initialValue: Optional.some(0),
75
+ },
76
+ createTryUpdateNotImplemented,
77
+ );
78
+
79
+ // Create cycle: childA -> childB -> childA
80
+ Object.defineProperty(childA, 'parents', {
81
+ value: [childB],
82
+ writable: false,
83
+ configurable: true,
84
+ });
85
+
86
+ expect(() => {
87
+ createAsyncChildObservable(
88
+ {
89
+ parents: [childA],
90
+ initialValue: Optional.some(0),
91
+ },
92
+ createTryUpdateNotImplemented,
93
+ );
94
+ }).toThrow(
95
+ 'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
96
+ );
97
+ });
98
+
99
+ test('should throw when a cycle exists through a chain of 3 observables', () => {
100
+ const root = createRootObservable(
101
+ { initialValue: Optional.some(0) },
102
+ () => ({}),
103
+ );
104
+
105
+ const childA = createSyncChildObservable(
106
+ {
107
+ parents: [root],
108
+ initialValue: Optional.some(0),
109
+ },
110
+ createTryUpdateNotImplemented,
111
+ );
112
+
113
+ const childB = createSyncChildObservable(
114
+ {
115
+ parents: [childA],
116
+ initialValue: Optional.some(0),
117
+ },
118
+ createTryUpdateNotImplemented,
119
+ );
120
+
121
+ const childC = createSyncChildObservable(
122
+ {
123
+ parents: [childB],
124
+ initialValue: Optional.some(0),
125
+ },
126
+ createTryUpdateNotImplemented,
127
+ );
128
+
129
+ // Create cycle: childA -> childC -> childB -> childA
130
+ Object.defineProperty(childA, 'parents', {
131
+ value: [childC],
132
+ writable: false,
133
+ configurable: true,
134
+ });
135
+
136
+ expect(() => {
137
+ createSyncChildObservable(
138
+ {
139
+ parents: [childA],
140
+ initialValue: Optional.some(0),
141
+ },
142
+ createTryUpdateNotImplemented,
143
+ );
144
+ }).toThrow(
145
+ 'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
146
+ );
147
+ });
148
+
149
+ test('should throw when the new child itself appears as an ancestor', () => {
150
+ const root = createRootObservable(
151
+ { initialValue: Optional.some(0) },
152
+ () => ({}),
153
+ );
154
+
155
+ const childA = createSyncChildObservable(
156
+ {
157
+ parents: [root],
158
+ initialValue: Optional.some(0),
159
+ },
160
+ createTryUpdateNotImplemented,
161
+ );
162
+
163
+ // Make childA's parents reference childA itself (self-loop)
164
+ Object.defineProperty(childA, 'parents', {
165
+ value: [childA],
166
+ writable: false,
167
+ configurable: true,
168
+ });
169
+
170
+ expect(() => {
171
+ createSyncChildObservable(
172
+ {
173
+ parents: [childA],
174
+ initialValue: Optional.some(0),
175
+ },
176
+ createTryUpdateNotImplemented,
177
+ );
178
+ }).toThrow(
179
+ 'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
180
+ );
181
+ });
182
+ });
183
+
184
+ describe('valid DAG patterns should not throw', () => {
185
+ test('diamond dependency is not a cycle', () => {
186
+ const root = createRootObservable(
187
+ { initialValue: Optional.some(0) },
188
+ () => ({}),
189
+ );
190
+
191
+ const left = createSyncChildObservable(
192
+ {
193
+ parents: [root],
194
+ initialValue: Optional.some(0),
195
+ },
196
+ createTryUpdateNotImplemented,
197
+ );
198
+
199
+ const right = createSyncChildObservable(
200
+ {
201
+ parents: [root],
202
+ initialValue: Optional.some(0),
203
+ },
204
+ createTryUpdateNotImplemented,
205
+ );
206
+
207
+ // Diamond: root -> left -> combined, root -> right -> combined
208
+ expect(() => {
209
+ createSyncChildObservable(
210
+ {
211
+ parents: [left, right],
212
+ initialValue: Optional.some(0),
213
+ },
214
+ createTryUpdateNotImplemented,
215
+ );
216
+ }).not.toThrow();
217
+ });
218
+
219
+ test('linear chain is not a cycle', () => {
220
+ const root = createRootObservable(
221
+ { initialValue: Optional.some(0) },
222
+ () => ({}),
223
+ );
224
+
225
+ const a = createSyncChildObservable(
226
+ {
227
+ parents: [root],
228
+ initialValue: Optional.some(0),
229
+ },
230
+ createTryUpdateNotImplemented,
231
+ );
232
+
233
+ const b = createSyncChildObservable(
234
+ {
235
+ parents: [a],
236
+ initialValue: Optional.some(0),
237
+ },
238
+ createTryUpdateNotImplemented,
239
+ );
240
+
241
+ expect(() => {
242
+ createSyncChildObservable(
243
+ {
244
+ parents: [b],
245
+ initialValue: Optional.some(0),
246
+ },
247
+ createTryUpdateNotImplemented,
248
+ );
249
+ }).not.toThrow();
250
+ });
251
+
252
+ test('multiple roots converging is not a cycle', () => {
253
+ const root1 = createRootObservable(
254
+ { initialValue: Optional.some(1) },
255
+ () => ({}),
256
+ );
257
+
258
+ const root2 = createRootObservable(
259
+ { initialValue: Optional.some(2) },
260
+ () => ({}),
261
+ );
262
+
263
+ const child1 = createSyncChildObservable(
264
+ {
265
+ parents: [root1],
266
+ initialValue: Optional.some(0),
267
+ },
268
+ createTryUpdateNotImplemented,
269
+ );
270
+
271
+ const child2 = createSyncChildObservable(
272
+ {
273
+ parents: [root2],
274
+ initialValue: Optional.some(0),
275
+ },
276
+ createTryUpdateNotImplemented,
277
+ );
278
+
279
+ expect(() => {
280
+ createSyncChildObservable(
281
+ {
282
+ parents: [child1, child2],
283
+ initialValue: Optional.some(0),
284
+ },
285
+ createTryUpdateNotImplemented,
286
+ );
287
+ }).not.toThrow();
288
+ });
289
+
290
+ test('combine with source observables works', () => {
291
+ const a$ = source<number>();
292
+
293
+ const b$ = source<string>();
294
+
295
+ expect(() => {
296
+ combine([a$, b$]);
297
+ }).not.toThrow();
298
+ });
299
+
300
+ test('combine with derived observables works', () => {
301
+ const a$ = source<number>();
302
+
303
+ const b$ = source<number>();
304
+
305
+ const mapped$ = a$.pipe(map((x) => x * 2));
306
+
307
+ expect(() => {
308
+ combine([mapped$, b$]);
309
+ }).not.toThrow();
310
+ });
311
+
312
+ test('merge with source observables works', () => {
313
+ const a$ = source<number>();
314
+
315
+ const b$ = source<number>();
316
+
317
+ expect(() => {
318
+ merge([a$, b$]);
319
+ }).not.toThrow();
320
+ });
321
+ });
322
+ });
@@ -0,0 +1,340 @@
1
+ import { Arr, type Optional, type Some } from 'ts-data-forge';
2
+ import { type MutableSet } from 'ts-type-forge';
3
+ import {
4
+ isChildObservable,
5
+ isManagerObservable,
6
+ type AsyncChildObservable,
7
+ type ChildObservable,
8
+ type InitializedSyncChildObservable,
9
+ type NonEmptyUnknownList,
10
+ type Observable,
11
+ type ObservableId,
12
+ type SyncChildObservable,
13
+ type UpdateToken,
14
+ type Wrap,
15
+ } from '../types/index.mjs';
16
+ import { maxDepth } from '../utils/index.mjs';
17
+ import {
18
+ assembleObservable,
19
+ createManagerObservableParts,
20
+ createObservableBaseHandle,
21
+ } from './create-observable-base.mjs';
22
+
23
+ /**
24
+ * The internal tools a sync-child leaf factory (`map`, `filter`, `zip`, …)
25
+ * receives — the replacement for the `protected` methods a subclass of
26
+ * `SyncChildObservableClass` used to inherit.
27
+ */
28
+ export type SyncChildObservableTools<A> = Readonly<{
29
+ /** Emits `nextValue` under `updateToken` and notifies subscribers. */
30
+ setNext: (nextValue: A, updateToken: UpdateToken) => void;
31
+
32
+ getSnapshot: () => Optional<A>;
33
+
34
+ /** Completes this observable (leaf teardown included). */
35
+ complete: () => void;
36
+ }>;
37
+
38
+ /**
39
+ * The internal tools an async-child leaf factory (`debounce`, `switchMap`, …)
40
+ * receives. Async children additionally start their own update propagation.
41
+ */
42
+ export type AsyncChildObservableTools<A> = Readonly<{
43
+ /** Emits `nextValue` and propagates the update to all descendants. */
44
+ startUpdate: (nextValue: A) => void;
45
+ }> &
46
+ SyncChildObservableTools<A>;
47
+
48
+ type ChildObservableConfigBase<P extends NonEmptyUnknownList> = Readonly<{
49
+ parents: Wrap<P>;
50
+
51
+ depth?: number;
52
+
53
+ /**
54
+ * Leaf teardown (clear timers, unsubscribe inner observables, …), run at
55
+ * the beginning of `complete()` — the closure replacement for
56
+ * `override complete() { teardown(); super.complete(); }`.
57
+ */
58
+ onComplete?: () => void;
59
+ }>;
60
+
61
+ export type ChildObservableConfig<
62
+ A,
63
+ P extends NonEmptyUnknownList,
64
+ > = ChildObservableConfigBase<P> &
65
+ Readonly<{
66
+ initialValue: Optional<A>;
67
+ }>;
68
+
69
+ export type InitializedChildObservableConfig<
70
+ A,
71
+ P extends NonEmptyUnknownList,
72
+ > = ChildObservableConfigBase<P> &
73
+ Readonly<{
74
+ initialValue: Some<A>;
75
+ }>;
76
+
77
+ /**
78
+ * Creates a sync-child observable — the classless replacement for extending
79
+ * `SyncChildObservableClass`.
80
+ *
81
+ * `createTryUpdate` receives the internal tools and returns the `tryUpdate`
82
+ * implementation (the former `override tryUpdate`). It runs during
83
+ * construction, before the observable is registered to its parents; leaf
84
+ * state can live either in the enclosing factory scope or inside the
85
+ * `createTryUpdate` closure.
86
+ */
87
+ export const createSyncChildObservable = <A, P extends NonEmptyUnknownList>(
88
+ {
89
+ parents,
90
+ depth = 1 + maxDepth(parents),
91
+ initialValue,
92
+ onComplete,
93
+ }: ChildObservableConfig<A, P>,
94
+ createTryUpdate: (
95
+ tools: SyncChildObservableTools<A>,
96
+ ) => (updateToken: UpdateToken) => void,
97
+ ): SyncChildObservable<A, P> => {
98
+ const handle = createObservableBaseHandle<A>(initialValue);
99
+
100
+ const complete = (): void => {
101
+ onComplete?.();
102
+
103
+ handle.completeBase();
104
+
105
+ // propagate to parents
106
+ for (const par of parents) {
107
+ par.tryComplete();
108
+ }
109
+ };
110
+
111
+ const tryComplete = (): void => {
112
+ tryCompleteChild({
113
+ hasSubscriber: handle.hasSubscriber(),
114
+ hasActiveChild: handle.hasActiveChild(),
115
+ parents,
116
+ complete,
117
+ });
118
+ };
119
+
120
+ const tryUpdate = createTryUpdate({
121
+ setNext: handle.setNext,
122
+ getSnapshot: handle.getSnapshot,
123
+ complete,
124
+ });
125
+
126
+ const observable: SyncChildObservable<A, P> = assembleObservable({
127
+ kind: 'sync child',
128
+ depth,
129
+ handle,
130
+ tryUpdate,
131
+ tryComplete,
132
+ complete,
133
+ extra: { parents },
134
+ });
135
+
136
+ registerChild(observable, parents);
137
+
138
+ return observable;
139
+ };
140
+
141
+ /**
142
+ * Creates a sync-child observable that always holds a value.
143
+ *
144
+ * Runtime-wise this is exactly `createSyncChildObservable`; only the types of
145
+ * `getSnapshot` and `pipe` are narrowed, which is what the
146
+ * `InitializedSyncChildObservableClass` overrides existed for.
147
+ */
148
+ export const createInitializedSyncChildObservable = <
149
+ A,
150
+ P extends NonEmptyUnknownList,
151
+ >(
152
+ config: InitializedChildObservableConfig<A, P>,
153
+ createTryUpdate: (
154
+ tools: SyncChildObservableTools<A>,
155
+ ) => (updateToken: UpdateToken) => void,
156
+ ): InitializedSyncChildObservable<A, P> => {
157
+ const observable = createSyncChildObservable<A, P>(config, createTryUpdate);
158
+
159
+ return (
160
+ // `getSnapshot` always returns `Some` here: `initialValue` is a `Some` and
161
+ // `setNext` only ever stores a `Some`.
162
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
163
+ observable as InitializedSyncChildObservable<A, P>
164
+ );
165
+ };
166
+
167
+ /**
168
+ * Creates an async-child observable — the classless replacement for extending
169
+ * `AsyncChildObservableClass`. See {@link createSyncChildObservable} for the
170
+ * `createTryUpdate` contract.
171
+ */
172
+ export const createAsyncChildObservable = <A, P extends NonEmptyUnknownList>(
173
+ {
174
+ parents,
175
+ depth = 1 + maxDepth(parents),
176
+ initialValue,
177
+ onComplete,
178
+ }: ChildObservableConfig<A, P>,
179
+ createTryUpdate: (
180
+ tools: AsyncChildObservableTools<A>,
181
+ ) => (updateToken: UpdateToken) => void,
182
+ ): AsyncChildObservable<A, P> => {
183
+ const handle = createObservableBaseHandle<A>(initialValue);
184
+
185
+ const { addDescendant, startUpdate } = createManagerObservableParts(handle);
186
+
187
+ const complete = (): void => {
188
+ onComplete?.();
189
+
190
+ handle.completeBase();
191
+
192
+ // propagate to parents
193
+ for (const par of parents) {
194
+ par.tryComplete();
195
+ }
196
+ };
197
+
198
+ const tryComplete = (): void => {
199
+ tryCompleteChild({
200
+ hasSubscriber: handle.hasSubscriber(),
201
+ hasActiveChild: handle.hasActiveChild(),
202
+ parents,
203
+ complete,
204
+ });
205
+ };
206
+
207
+ const tryUpdate = createTryUpdate({
208
+ setNext: handle.setNext,
209
+ getSnapshot: handle.getSnapshot,
210
+ complete,
211
+ startUpdate,
212
+ });
213
+
214
+ const observable: AsyncChildObservable<A, P> = assembleObservable({
215
+ kind: 'async child',
216
+ depth,
217
+ handle,
218
+ tryUpdate,
219
+ tryComplete,
220
+ complete,
221
+ extra: { parents, addDescendant },
222
+ });
223
+
224
+ registerChild(observable, parents);
225
+
226
+ return observable;
227
+ };
228
+
229
+ /* helpers */
230
+
231
+ /**
232
+ * Detects circular dependencies by walking the full ancestor chain of the
233
+ * given parents and checking whether `child` already appears among them.
234
+ *
235
+ * @throws {Error} if a circular dependency is detected
236
+ */
237
+ const hasCircularDependencyFrom = (
238
+ node: Observable<unknown>,
239
+ mut_visited: MutableSet<ObservableId>,
240
+ mut_inPath: MutableSet<ObservableId>,
241
+ ): boolean => {
242
+ if (mut_inPath.has(node.id)) return true;
243
+
244
+ if (mut_visited.has(node.id)) return false;
245
+
246
+ mut_visited.add(node.id);
247
+
248
+ mut_inPath.add(node.id);
249
+
250
+ if (isChildObservable(node)) {
251
+ for (const parent of node.parents) {
252
+ if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
253
+ return true;
254
+ }
255
+ }
256
+ }
257
+
258
+ mut_inPath.delete(node.id);
259
+
260
+ return false;
261
+ };
262
+
263
+ const detectCircularDependency = (
264
+ child: ChildObservable<unknown>,
265
+ parents: readonly Observable<unknown>[],
266
+ ): void => {
267
+ const mut_visited = new Set<ObservableId>();
268
+
269
+ const mut_inPath = new Set<ObservableId>([child.id]);
270
+
271
+ for (const parent of parents) {
272
+ if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
273
+ throw new Error(
274
+ 'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
275
+ );
276
+ }
277
+ }
278
+ };
279
+
280
+ /**
281
+ * Registers `child` to its parents and to all reachable manager observables.
282
+ * Runs after the child object is fully assembled, so parents only ever hold a
283
+ * reference to the final composed object (the class version registered `this`
284
+ * from inside the constructor instead).
285
+ */
286
+ const registerChild = <A,>(
287
+ child: ChildObservable<A>,
288
+ parents: ChildObservable<A>['parents'],
289
+ ): void => {
290
+ detectCircularDependency(child, parents);
291
+
292
+ for (const p of parents) {
293
+ p.addChild(child);
294
+ }
295
+
296
+ // register child to all reachable ManagerObservables
297
+ const mut_rest = Array.from(parents);
298
+
299
+ while (Arr.isNonEmpty(mut_rest)) {
300
+ const p = mut_rest.pop();
301
+
302
+ if (p === undefined) break;
303
+
304
+ if (isManagerObservable(p)) {
305
+ p.addDescendant(child);
306
+ } else {
307
+ // trace back dependency graph
308
+ mut_rest.push(...p.parents);
309
+ }
310
+ }
311
+ };
312
+
313
+ const tryCompleteChild = <A,>({
314
+ hasSubscriber,
315
+ hasActiveChild,
316
+ parents,
317
+ complete,
318
+ }: Readonly<{
319
+ hasSubscriber: boolean;
320
+ hasActiveChild: boolean;
321
+ parents: ChildObservable<A>['parents'];
322
+ complete: () => void;
323
+ }>): void => {
324
+ // If there is no working parent node
325
+ if (parents.every((r) => r.isCompleted)) {
326
+ complete();
327
+
328
+ return;
329
+ }
330
+
331
+ // If there are no active child node
332
+ if (!hasSubscriber && !hasActiveChild) {
333
+ complete();
334
+ }
335
+
336
+ // propagate to parents
337
+ for (const par of parents) {
338
+ par.tryComplete();
339
+ }
340
+ };