synstate 2.0.2 → 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 +15 -16
  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,358 @@
1
+ import { Arr, Optional } from 'ts-data-forge';
2
+ import {
3
+ type MutableMap,
4
+ type MutableSet,
5
+ type ReadonlyRecord,
6
+ } from 'ts-type-forge';
7
+ import {
8
+ type ChildObservable,
9
+ type InitializedObservable,
10
+ type Observable,
11
+ type ObservableBase,
12
+ type ObservableId,
13
+ type ObservableKind,
14
+ type Operator,
15
+ type Subscriber,
16
+ type SubscriberId,
17
+ type Subscription,
18
+ type UpdateToken,
19
+ type WithInitialValueOperator,
20
+ } from '../types/index.mjs';
21
+ import {
22
+ binarySearch,
23
+ issueObservableId,
24
+ issueSubscriberId,
25
+ issueUpdateToken,
26
+ toSubscriber,
27
+ } from '../utils/index.mjs';
28
+
29
+ /**
30
+ * The mutable core shared by every observable, expressed as closures instead of
31
+ * a base class.
32
+ *
33
+ * The returned handle is the "protected" surface of the former
34
+ * `ObservableBaseClass`: the layer factories (`createRootObservable`,
35
+ * `createSyncChildObservable`, `createAsyncChildObservable`) build the public
36
+ * object from it via {@link assembleObservable}, and leaf factories receive
37
+ * only the narrow tools they need. Neither the handle nor its type is exported
38
+ * from the package (`core/base/**` is excluded from index generation), so
39
+ * `setNext` and friends stay internal, as `protected` kept them before.
40
+ */
41
+ export type ObservableBaseHandle<A> = Readonly<{
42
+ id: ObservableId;
43
+
44
+ addChild: <B>(child: ChildObservable<B>) => void;
45
+
46
+ getSnapshot: () => Optional<A>;
47
+
48
+ isCompleted: () => boolean;
49
+
50
+ updateToken: () => UpdateToken;
51
+
52
+ hasSubscriber: () => boolean;
53
+
54
+ hasChild: () => boolean;
55
+
56
+ hasActiveChild: () => boolean;
57
+
58
+ subscribe: (onNext: (v: A) => void, onComplete?: () => void) => Subscription;
59
+
60
+ /** Stores `nextValue` as the current value and notifies subscribers. */
61
+ setNext: (nextValue: A, updateToken: UpdateToken) => void;
62
+
63
+ /**
64
+ * The innermost part of `complete()` (what `ObservableBaseClass.complete`
65
+ * used to do): marks this observable completed exactly once, notifies and
66
+ * removes all subscribers, and propagates `tryComplete` to children. Layer
67
+ * factories wrap this with leaf teardown and propagation to parents.
68
+ */
69
+ completeBase: () => void;
70
+ }>;
71
+
72
+ export const createObservableBaseHandle = <A,>(
73
+ initialValue: Optional<A>,
74
+ ): ObservableBaseHandle<A> => {
75
+ const id = issueObservableId();
76
+
77
+ let mut_children: readonly ChildObservable<unknown>[] = [];
78
+
79
+ const mut_subscribers: MutableMap<SubscriberId, Subscriber<A>> = new Map();
80
+
81
+ let mut_currentValue: Optional<A> = initialValue;
82
+
83
+ let mut_isCompleted = false;
84
+
85
+ let mut_updateToken: UpdateToken = issueUpdateToken();
86
+
87
+ const addSubscriber = (s: Subscriber<A>): SubscriberId => {
88
+ // return the id of added subscriber
89
+ const subscriberId = issueSubscriberId();
90
+
91
+ mut_subscribers.set(subscriberId, s);
92
+
93
+ return subscriberId;
94
+ };
95
+
96
+ const removeSubscriber = (subscriberId: SubscriberId): void => {
97
+ mut_subscribers.delete(subscriberId);
98
+ };
99
+
100
+ const addChild = <B,>(child: ChildObservable<B>): void => {
101
+ mut_children = Arr.toPushed(
102
+ mut_children,
103
+
104
+ child as ChildObservable<unknown>,
105
+ );
106
+ };
107
+
108
+ const getSnapshot = (): Optional<A> => mut_currentValue;
109
+
110
+ const isCompleted = (): boolean => mut_isCompleted;
111
+
112
+ const updateToken = (): UpdateToken => mut_updateToken;
113
+
114
+ const hasSubscriber = (): boolean => mut_subscribers.size > 0;
115
+
116
+ const hasChild = (): boolean => Arr.isNonEmpty(mut_children);
117
+
118
+ const hasActiveChild = (): boolean =>
119
+ mut_children.some((c) => !c.isCompleted);
120
+
121
+ const setNext = (nextValue: A, nextUpdateToken: UpdateToken): void => {
122
+ mut_updateToken = nextUpdateToken;
123
+
124
+ mut_currentValue = Optional.some(nextValue);
125
+
126
+ for (const s of mut_subscribers.values()) {
127
+ s.onNext(nextValue);
128
+ }
129
+ };
130
+
131
+ const completeBase = (): void => {
132
+ if (mut_isCompleted) return; // terminate only once
133
+
134
+ // change state
135
+ mut_isCompleted = true;
136
+
137
+ // run subscribers for the current value
138
+ for (const s of mut_subscribers.values()) {
139
+ s.onComplete();
140
+ }
141
+
142
+ // remove all subscribers
143
+ mut_subscribers.clear();
144
+
145
+ // propagate to children
146
+ for (const o of mut_children) {
147
+ o.tryComplete();
148
+ }
149
+ };
150
+
151
+ const subscribe = (
152
+ onNext: (v: A) => void,
153
+ onComplete?: () => void,
154
+ ): Subscription => {
155
+ // first emit
156
+ const curr = getSnapshot();
157
+
158
+ if (Optional.isSome(curr)) {
159
+ onNext(curr.value);
160
+ }
161
+
162
+ if (mut_isCompleted) {
163
+ if (onComplete !== undefined) {
164
+ onComplete();
165
+ }
166
+
167
+ return { unsubscribe: () => {} };
168
+ }
169
+
170
+ const subscriberId: SubscriberId = addSubscriber(
171
+ toSubscriber(onNext, onComplete),
172
+ );
173
+
174
+ return {
175
+ unsubscribe: () => {
176
+ removeSubscriber(subscriberId);
177
+ },
178
+ };
179
+ };
180
+
181
+ return {
182
+ id,
183
+ addChild,
184
+ getSnapshot,
185
+ isCompleted,
186
+ updateToken,
187
+ hasSubscriber,
188
+ hasChild,
189
+ hasActiveChild,
190
+ subscribe,
191
+ setNext,
192
+ completeBase,
193
+ };
194
+ };
195
+
196
+ /**
197
+ * The extra members every manager observable — a root or an async child — owns:
198
+ * the depth-ordered descendant list and the update propagation that walks it.
199
+ */
200
+ export type ManagerObservableParts<A> = Readonly<{
201
+ addDescendant: <B>(child: ChildObservable<B>) => void;
202
+
203
+ /** Emits `nextValue` and propagates the update to all descendants. */
204
+ startUpdate: (nextValue: A) => void;
205
+ }>;
206
+
207
+ /**
208
+ * Builds the manager half of a root or async-child observable. Both start their
209
+ * own update propagation, so both keep a descendant set and a depth-ordered
210
+ * propagation list; this is the single implementation of that pair (the class
211
+ * version duplicated it across `RootObservableClass` and
212
+ * `AsyncChildObservableClass`).
213
+ */
214
+ export const createManagerObservableParts = <A,>(
215
+ handle: ObservableBaseHandle<A>,
216
+ ): ManagerObservableParts<A> => {
217
+ let mut_propagationOrder: readonly ChildObservable<unknown>[] = [];
218
+
219
+ const mut_descendantsIdSet: MutableSet<ObservableId> = new Set();
220
+
221
+ const addDescendant = <B,>(child: ChildObservable<B>): void => {
222
+ if (mut_descendantsIdSet.has(child.id)) return;
223
+
224
+ mut_descendantsIdSet.add(child.id);
225
+
226
+ const insertPos = binarySearch(
227
+ mut_propagationOrder.map((a) => a.depth),
228
+ child.depth,
229
+ );
230
+
231
+ mut_propagationOrder = Arr.toInserted(
232
+ mut_propagationOrder,
233
+ insertPos,
234
+ child,
235
+ );
236
+ };
237
+
238
+ const startUpdate = (nextValue: A): void => {
239
+ const updateToken = issueUpdateToken();
240
+
241
+ handle.setNext(nextValue, updateToken);
242
+
243
+ for (const p of mut_propagationOrder) {
244
+ p.tryUpdate(updateToken);
245
+ }
246
+ };
247
+
248
+ return { addDescendant, startUpdate };
249
+ };
250
+
251
+ /** Default `tryUpdate` for observables that never receive parent updates. */
252
+ export const createTryUpdateNotImplemented = (): ((
253
+ _updateToken: UpdateToken,
254
+ ) => void) => tryUpdateNotImplemented;
255
+
256
+ const tryUpdateNotImplemented = (_updateToken: UpdateToken): void => {
257
+ throw new Error('not implemented');
258
+ };
259
+
260
+ type AssembleObservableArgs<
261
+ A,
262
+ Kind extends ObservableKind,
263
+ Extra extends ReadonlyRecord<string, unknown>,
264
+ > = Readonly<{
265
+ kind: Kind;
266
+ depth: number;
267
+ handle: ObservableBaseHandle<A>;
268
+ tryUpdate: (updateToken: UpdateToken) => void;
269
+ tryComplete: () => void;
270
+ complete: () => void;
271
+ /**
272
+ * Kind-specific public members (`parents`, `addDescendant`) and leaf
273
+ * extensions (`next`, `start`). They are merged here, in the one place the
274
+ * final object literal is created, because the public getters below must not
275
+ * be copied by a spread (spreading would freeze their current values).
276
+ */
277
+ extra: Extra;
278
+ }>;
279
+
280
+ /**
281
+ * Builds the public observable object from a base handle. This replaces the
282
+ * implicit assembly a `class` performs: state stays in the handle's closures,
283
+ * `isCompleted` / `updateToken` / `hasSubscriber` / `hasChild` become getters
284
+ * delegating to the handle, and the kind-specific behavior (`tryUpdate`,
285
+ * `tryComplete`, `complete`) is passed in already composed.
286
+ */
287
+ export const assembleObservable = <
288
+ A,
289
+ Kind extends ObservableKind,
290
+ Extra extends ReadonlyRecord<string, unknown>,
291
+ >({
292
+ kind,
293
+ depth,
294
+ handle,
295
+ tryUpdate,
296
+ tryComplete,
297
+ complete,
298
+ extra,
299
+ }: AssembleObservableArgs<A, Kind, Extra>): Extra &
300
+ ObservableBase<A> &
301
+ Readonly<{ kind: Kind }> => {
302
+ function pipe<B>(
303
+ operator: WithInitialValueOperator<A, B>,
304
+ ): InitializedObservable<B>;
305
+
306
+ function pipe<B>(operator: Operator<A, B>): Observable<B>;
307
+
308
+ function pipe<B>(operator: Operator<A, B>): Observable<B> {
309
+ return operator(
310
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
311
+ observable as unknown as InitializedObservable<A>,
312
+ );
313
+ }
314
+
315
+ const observable = {
316
+ ...extra,
317
+
318
+ id: handle.id,
319
+
320
+ kind,
321
+
322
+ depth,
323
+
324
+ addChild: handle.addChild,
325
+
326
+ getSnapshot: handle.getSnapshot,
327
+
328
+ get isCompleted(): boolean {
329
+ return handle.isCompleted();
330
+ },
331
+
332
+ get updateToken(): UpdateToken {
333
+ return handle.updateToken();
334
+ },
335
+
336
+ get hasSubscriber(): boolean {
337
+ return handle.hasSubscriber();
338
+ },
339
+
340
+ get hasChild(): boolean {
341
+ return handle.hasChild();
342
+ },
343
+
344
+ hasActiveChild: handle.hasActiveChild,
345
+
346
+ tryUpdate,
347
+
348
+ tryComplete,
349
+
350
+ complete,
351
+
352
+ subscribe: handle.subscribe,
353
+
354
+ pipe,
355
+ } as const;
356
+
357
+ return observable;
358
+ };
@@ -0,0 +1,102 @@
1
+ import { Optional } from 'ts-data-forge';
2
+ import { type ReadonlyRecord } from 'ts-type-forge';
3
+ import { isRootObservable, type RootObservable } from '../types/index.mjs';
4
+ import {
5
+ assembleObservable,
6
+ createManagerObservableParts,
7
+ createObservableBaseHandle,
8
+ createTryUpdateNotImplemented,
9
+ } from './create-observable-base.mjs';
10
+
11
+ /**
12
+ * The internal tools a root leaf factory (`source`, `timer`, `fromPromise`, …)
13
+ * receives — the replacement for the `protected` methods a subclass of
14
+ * `RootObservableClass` used to inherit.
15
+ */
16
+ export type RootObservableTools<A> = Readonly<{
17
+ /** Emits `nextValue` and propagates the update to all descendants. */
18
+ startUpdate: (nextValue: A) => void;
19
+
20
+ isCompleted: () => boolean;
21
+
22
+ /** Completes this observable (leaf teardown included). */
23
+ complete: () => void;
24
+ }>;
25
+
26
+ export type RootObservableConfig<A> = Readonly<{
27
+ initialValue: Optional<A>;
28
+
29
+ /**
30
+ * Leaf teardown (clear timers, abort requests, …), run at the beginning of
31
+ * `complete()` — the closure replacement for
32
+ * `override complete() { teardown(); super.complete(); }`.
33
+ */
34
+ onComplete?: () => void;
35
+ }>;
36
+
37
+ /**
38
+ * Creates a root observable — the classless replacement for extending
39
+ * `RootObservableClass`.
40
+ *
41
+ * The optional `init` callback receives the internal tools and returns extra
42
+ * public members to merge into the observable (e.g. `next` for `source`,
43
+ * `start` for `timer`). `init` runs during construction; it may register
44
+ * asynchronous callbacks that use the tools, and may call `complete`
45
+ * synchronously, but the observable object itself does not exist yet at that
46
+ * point.
47
+ */
48
+ export const createRootObservable = <
49
+ A,
50
+ Extension extends ReadonlyRecord<string, unknown> = ReadonlyRecord<
51
+ never,
52
+ never
53
+ >,
54
+ >(
55
+ { initialValue, onComplete }: RootObservableConfig<A>,
56
+ init: (tools: RootObservableTools<A>) => Extension,
57
+ ): Extension & RootObservable<A> => {
58
+ const handle = createObservableBaseHandle<A>(initialValue);
59
+
60
+ const { addDescendant, startUpdate } = createManagerObservableParts(handle);
61
+
62
+ const complete = (): void => {
63
+ onComplete?.();
64
+
65
+ handle.completeBase();
66
+ };
67
+
68
+ const tryComplete = (): void => {
69
+ if (!handle.hasSubscriber() && !handle.hasActiveChild()) {
70
+ complete();
71
+ }
72
+ };
73
+
74
+ const extension: Extension = init({
75
+ startUpdate,
76
+ isCompleted: handle.isCompleted,
77
+ complete,
78
+ });
79
+
80
+ return assembleObservable({
81
+ kind: 'root',
82
+ depth: 0,
83
+ handle,
84
+ tryUpdate: createTryUpdateNotImplemented,
85
+ tryComplete,
86
+ complete,
87
+ extra: {
88
+ ...extension,
89
+ addDescendant,
90
+ },
91
+ });
92
+ };
93
+
94
+ if (import.meta.vitest !== undefined) {
95
+ test('isRootObservable', () => {
96
+ assert.isTrue(
97
+ isRootObservable(
98
+ createRootObservable({ initialValue: Optional.some(0) }, () => ({})),
99
+ ),
100
+ );
101
+ });
102
+ }
@@ -0,0 +1,2 @@
1
+ export * from './create-child-observable.mjs';
2
+ export * from './create-root-observable.mjs';
@@ -8,10 +8,11 @@ import {
8
8
  type SyncChildObservable,
9
9
  } from '../types/index.mjs';
10
10
  import {
11
- AsyncChildObservableClass,
12
- SyncChildObservableClass,
13
- } from './child-observable-class.mjs';
14
- import { RootObservableClass } from './root-observable-class.mjs';
11
+ createAsyncChildObservable,
12
+ createSyncChildObservable,
13
+ } from './create-child-observable.mjs';
14
+ import { createTryUpdateNotImplemented } from './create-observable-base.mjs';
15
+ import { createRootObservable } from './create-root-observable.mjs';
15
16
 
16
17
  /**
17
18
  * Inheritance
@@ -60,30 +61,37 @@ expectType<Observable<1>, Observable<number>>('<=');
60
61
 
61
62
  expectType<Observable<number>, Observable<1>>('!<=');
62
63
 
63
- const root = new RootObservableClass({
64
- initialValue: Optional.some(0),
65
- });
64
+ const root = createRootObservable(
65
+ { initialValue: Optional.some(0) },
66
+ () => ({}),
67
+ );
66
68
 
67
69
  expectType<typeof root, RootObservable<number>>('<=');
68
70
 
69
- const syncChild = new SyncChildObservableClass({
70
- parents: [root],
71
- initialValue: Optional.some(0),
72
- });
71
+ const syncChild = createSyncChildObservable(
72
+ {
73
+ parents: [root],
74
+ initialValue: Optional.some(0),
75
+ },
76
+ createTryUpdateNotImplemented,
77
+ );
73
78
 
74
79
  expectType<typeof syncChild, SyncChildObservable<number>>('<=');
75
80
 
76
- const asyncChild = new AsyncChildObservableClass({
77
- parents: [root],
78
- initialValue: Optional.some(0),
79
- });
81
+ const asyncChild = createAsyncChildObservable(
82
+ {
83
+ parents: [root],
84
+ initialValue: Optional.some(0),
85
+ },
86
+ createTryUpdateNotImplemented,
87
+ );
80
88
 
81
89
  expectType<typeof asyncChild, AsyncChildObservable<number>>('<=');
82
90
 
83
- test('SyncChildObservableClass', () => {
91
+ test('createSyncChildObservable', () => {
84
92
  expect(syncChild.depth).toBe(1);
85
93
  });
86
94
 
87
- test('AsyncChildObservableClass', () => {
95
+ test('createAsyncChildObservable', () => {
88
96
  expect(asyncChild.depth).toBe(1);
89
97
  });
@@ -1,6 +1,6 @@
1
1
  import { Arr, expectType, Optional } from 'ts-data-forge';
2
2
  import { type NonEmptyTuple, type Tuple } from 'ts-type-forge';
3
- import { SyncChildObservableClass } from '../class/index.mjs';
3
+ import { createSyncChildObservable } from '../base/index.mjs';
4
4
  import { source } from '../create/index.mjs';
5
5
  import { withInitialValue } from '../operators/index.mjs';
6
6
  import {
@@ -11,7 +11,6 @@ import {
11
11
  type NonEmptyUnknownList,
12
12
  type Observable,
13
13
  type SyncChildObservable,
14
- type UpdateToken,
15
14
  type Wrap,
16
15
  } from '../types/index.mjs';
17
16
 
@@ -76,9 +75,7 @@ export const combine = <const OS extends NonEmptyTuple<Observable<unknown>>>(
76
75
  parents: OS,
77
76
  ): CombineObservableRefined<OS> =>
78
77
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
79
- new CombineObservableClass(
80
- parents,
81
- ) as unknown as CombineObservableRefined<OS>;
78
+ createCombineObservable(parents) as unknown as CombineObservableRefined<OS>;
82
79
 
83
80
  /**
84
81
  * Alias for `combine`.
@@ -86,56 +83,57 @@ export const combine = <const OS extends NonEmptyTuple<Observable<unknown>>>(
86
83
  */
87
84
  export const combineLatest = combine;
88
85
 
89
- class CombineObservableClass<const A extends NonEmptyUnknownList>
90
- extends SyncChildObservableClass<A, A>
91
- implements CombineObservable<A>
92
- {
93
- constructor(parents: Wrap<A>) {
94
- const parentsValues: Tuple.MapTo<Optional<A[number]>, A> = Arr.map(
95
- parents,
96
- (p) => p.getSnapshot(),
97
- );
98
-
99
- // The annotation on `initialValue` is what replaces the type assertion.
100
- // `Arr.map` carries a single result element type, so it reports `A[number]`
101
- // at every position, and `Tuple.MapTo<A[number], A>` — the uniform mapping
102
- // — is *not* assignable back to `A`. The positional
103
- // `Readonly<{ [K in keyof A]: A[K] }>` is, and TypeScript accepts the
104
- // uniform result into it because the two mapped types share `keyof A`. So
105
- // it is the annotation, not an assertion, that records the position-wise
106
- // fact the checker cannot derive on its own.
107
- const initialValue: Optional<Readonly<{ [K in keyof A]: A[K] }>> =
108
- Arr.every(parentsValues, Optional.isSome)
109
- ? Optional.some(Arr.map(parentsValues, (c) => c.value))
110
- : Optional.none;
111
-
112
- super({
86
+ const createCombineObservable = <const A extends NonEmptyUnknownList>(
87
+ parents: Wrap<A>,
88
+ ): CombineObservable<A> => {
89
+ const parentsValues: Tuple.MapTo<Optional<A[number]>, A> = Arr.map(
90
+ parents,
91
+ (p) => p.getSnapshot(),
92
+ );
93
+
94
+ // The annotation on `initialValue` is what replaces the type assertion.
95
+ // `Arr.map` carries a single result element type, so it reports `A[number]`
96
+ // at every position, and `Tuple.MapTo<A[number], A>` — the uniform mapping
97
+ // — is *not* assignable back to `A`. The positional
98
+ // `Readonly<{ [K in keyof A]: A[K] }>` is, and TypeScript accepts the
99
+ // uniform result into it because the two mapped types share `keyof A`. So
100
+ // it is the annotation, not an assertion, that records the position-wise
101
+ // fact the checker cannot derive on its own.
102
+ const initialValue: Optional<Readonly<{ [K in keyof A]: A[K] }>> = Arr.every(
103
+ parentsValues,
104
+ Optional.isSome,
105
+ )
106
+ ? Optional.some(Arr.map(parentsValues, (c) => c.value))
107
+ : Optional.none;
108
+
109
+ return createSyncChildObservable<A, A>(
110
+ {
113
111
  parents,
114
112
  initialValue,
115
- });
116
- }
117
-
118
- override tryUpdate(updateToken: UpdateToken): void {
119
- if (this.parents.every((o) => o.updateToken !== updateToken)) return; // all parents are skipped
120
-
121
- // Same shape-preserving chain as the constructor: `Arr.map` (not the
122
- // native one, which drops the tuple shape) into the uniform
123
- // `Tuple.MapTo`, then a positional annotation to get back to `A`.
124
- const parentValues: Tuple.MapTo<Optional<A[number]>, A> = Arr.map(
125
- this.parents,
126
- (a) => a.getSnapshot(),
127
- );
128
-
129
- if (Arr.every(parentValues, Optional.isSome)) {
130
- const nextValue: Readonly<{ [K in keyof A]: A[K] }> = Arr.map(
131
- parentValues,
132
- (a) => a.value,
133
- );
134
-
135
- this.setNext(nextValue, updateToken);
136
- }
137
- }
138
- }
113
+ },
114
+ ({ setNext }) =>
115
+ (updateToken) => {
116
+ if (parents.every((o) => o.updateToken !== updateToken)) return; // all parents are skipped
117
+
118
+ // Same shape-preserving chain as the initial value: `Arr.map` (not the
119
+ // native one, which drops the tuple shape) into the uniform
120
+ // `Tuple.MapTo`, then a positional annotation to get back to `A`.
121
+ const parentValues: Tuple.MapTo<Optional<A[number]>, A> = Arr.map(
122
+ parents,
123
+ (a) => a.getSnapshot(),
124
+ );
125
+
126
+ if (Arr.every(parentValues, Optional.isSome)) {
127
+ const nextValue: Readonly<{ [K in keyof A]: A[K] }> = Arr.map(
128
+ parentValues,
129
+ (a) => a.value,
130
+ );
131
+
132
+ setNext(nextValue, updateToken);
133
+ }
134
+ },
135
+ );
136
+ };
139
137
 
140
138
  if (import.meta.vitest !== undefined) {
141
139
  test('type test', () => {