synstate 6.1.1 → 6.2.1
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.
- package/README.md +2 -2
- package/dist/core/authoring.d.mts +3 -3
- package/dist/core/class/child-observable-class.d.mts +24 -25
- package/dist/core/class/child-observable-class.d.mts.map +1 -1
- package/dist/core/class/child-observable-class.mjs +41 -42
- package/dist/core/class/child-observable-class.mjs.map +1 -1
- package/dist/core/class/create-child-observable.d.mts +27 -29
- package/dist/core/class/create-child-observable.d.mts.map +1 -1
- package/dist/core/class/create-child-observable.mjs.map +1 -1
- package/dist/core/class/create-root-observable.d.mts +11 -11
- package/dist/core/class/manager-observable-class.d.mts +6 -6
- package/dist/core/class/manager-observable-class.mjs.map +1 -1
- package/dist/core/class/observable-base-class.d.mts +29 -31
- package/dist/core/class/observable-base-class.d.mts.map +1 -1
- package/dist/core/class/observable-base-class.mjs +0 -2
- package/dist/core/class/observable-base-class.mjs.map +1 -1
- package/dist/core/class/observable-node.d.mts +3 -4
- package/dist/core/class/observable-node.d.mts.map +1 -1
- package/dist/core/class/observable-node.mjs +0 -1
- package/dist/core/class/observable-node.mjs.map +1 -1
- package/dist/core/class/propagation-state.d.mts +6 -8
- package/dist/core/class/propagation-state.d.mts.map +1 -1
- package/dist/core/class/propagation-state.mjs +0 -2
- package/dist/core/class/propagation-state.mjs.map +1 -1
- package/dist/core/class/root-observable-class.d.mts +4 -4
- package/dist/core/combine/derive.d.mts +7 -8
- package/dist/core/combine/derive.d.mts.map +1 -1
- package/dist/core/combine/derive.mjs +0 -1
- package/dist/core/combine/derive.mjs.map +1 -1
- package/dist/core/combine/merge.mjs.map +1 -1
- package/dist/core/combine/zip.d.mts +3 -3
- package/dist/core/combine/zip.mjs.map +1 -1
- package/dist/core/create/counter.d.mts +3 -3
- package/dist/core/create/counter.mjs.map +1 -1
- package/dist/core/create/from-abortable-promise.mjs.map +1 -1
- package/dist/core/create/from-promise.mjs.map +1 -1
- package/dist/core/create/from-subscribable.mjs.map +1 -1
- package/dist/core/create/source.mjs.map +1 -1
- package/dist/core/create/timer.mjs.map +1 -1
- package/dist/core/operators/audit.mjs.map +1 -1
- package/dist/core/operators/debounce.mjs.map +1 -1
- package/dist/core/operators/filter.mjs.map +1 -1
- package/dist/core/operators/keep-equal-parts.d.mts +14 -15
- package/dist/core/operators/keep-equal-parts.d.mts.map +1 -1
- package/dist/core/operators/keep-equal-parts.mjs +0 -1
- package/dist/core/operators/keep-equal-parts.mjs.map +1 -1
- package/dist/core/operators/map.mjs.map +1 -1
- package/dist/core/operators/merge-map.d.mts +3 -3
- package/dist/core/operators/merge-map.mjs.map +1 -1
- package/dist/core/operators/pairwise.mjs.map +1 -1
- package/dist/core/operators/scan.mjs.map +1 -1
- package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
- package/dist/core/operators/skip-until.mjs.map +1 -1
- package/dist/core/operators/skip-while.mjs.map +1 -1
- package/dist/core/operators/switch-map.mjs.map +1 -1
- package/dist/core/operators/take-until.mjs.map +1 -1
- package/dist/core/operators/take-while.mjs.map +1 -1
- package/dist/core/operators/take.mjs.map +1 -1
- package/dist/core/operators/throttle.mjs.map +1 -1
- package/dist/core/operators/with-buffered-from.mjs.map +1 -1
- package/dist/core/operators/with-current-value-from.mjs.map +1 -1
- package/dist/core/operators/with-initial-value.mjs.map +1 -1
- package/dist/core/predefined/operators/attach-index.mjs.map +1 -1
- package/dist/core/predefined/operators/map-to.d.mts +3 -4
- package/dist/core/predefined/operators/map-to.d.mts.map +1 -1
- package/dist/core/predefined/operators/map-to.mjs +0 -1
- package/dist/core/predefined/operators/map-to.mjs.map +1 -1
- package/dist/core/scope/scope.d.mts +4 -4
- package/dist/core/types/observable-family.d.mts.map +1 -1
- package/dist/core/types/observable-family.mjs.map +1 -1
- package/dist/core/types/observable.d.mts +5 -5
- package/dist/core/types/observable.mjs.map +1 -1
- package/dist/core/utils/current-scope.d.mts +6 -6
- package/dist/core/utils/id-maker.d.mts +2 -2
- package/dist/core/utils/observable-brand.d.mts +5 -5
- package/dist/utils/create-boolean-state.d.mts +2 -2
- package/dist/utils/create-reducer.d.mts +18 -18
- package/dist/utils/create-state.d.mts +2 -2
- package/dist/utils/create-state.mjs.map +1 -1
- package/dist/utils/state-source.d.mts +3 -3
- package/package.json +9 -9
- package/src/core/authoring.mts +3 -3
- package/src/core/class/child-observable-class.mts +86 -87
- package/src/core/class/circular-dependency-comparison.test.mts +2 -2
- package/src/core/class/create-child-observable.mts +27 -29
- package/src/core/class/create-root-observable.mts +11 -11
- package/src/core/class/manager-observable-class.mts +12 -12
- package/src/core/class/observable-base-class.mts +33 -35
- package/src/core/class/observable-node.mts +3 -4
- package/src/core/class/propagation-state.mts +26 -28
- package/src/core/class/remove-completed.test.mts +2 -2
- package/src/core/class/root-observable-class.mts +9 -9
- package/src/core/class/unobserved-write.test.mts +4 -4
- package/src/core/combine/derive.mts +10 -11
- package/src/core/combine/merge.mts +2 -2
- package/src/core/combine/zip.mts +7 -7
- package/src/core/create/counter.mts +5 -5
- package/src/core/create/from-abortable-promise.mts +2 -2
- package/src/core/create/from-promise.mts +2 -2
- package/src/core/create/from-subscribable.mts +2 -2
- package/src/core/create/source.mts +5 -5
- package/src/core/create/timer.mts +2 -2
- package/src/core/operators/audit.mts +2 -2
- package/src/core/operators/debounce.mts +2 -2
- package/src/core/operators/filter.mts +4 -4
- package/src/core/operators/keep-equal-parts.mts +16 -17
- package/src/core/operators/map.mts +2 -2
- package/src/core/operators/merge-map.mts +5 -5
- package/src/core/operators/pairwise.mts +4 -4
- package/src/core/operators/scan.mts +2 -2
- package/src/core/operators/skip-if-no-change.mts +4 -4
- package/src/core/operators/skip-until.mts +4 -4
- package/src/core/operators/skip-while.mts +2 -2
- package/src/core/operators/switch-map.mts +5 -5
- package/src/core/operators/take-until.mts +2 -2
- package/src/core/operators/take-while.mts +2 -2
- package/src/core/operators/take.mts +2 -2
- package/src/core/operators/throttle.mts +2 -2
- package/src/core/operators/with-buffered-from.mts +2 -2
- package/src/core/operators/with-current-value-from.mts +2 -2
- package/src/core/operators/with-initial-value.mts +2 -2
- package/src/core/predefined/operators/attach-index.mts +2 -2
- package/src/core/predefined/operators/map-to.mts +3 -4
- package/src/core/scope/scope.mts +6 -6
- package/src/core/types/observable-family.mts +4 -5
- package/src/core/types/observable.mts +7 -7
- package/src/core/utils/current-scope.mts +8 -8
- package/src/core/utils/id-maker.mts +2 -2
- package/src/core/utils/observable-brand.mts +5 -5
- package/src/utils/create-boolean-state.mts +2 -2
- package/src/utils/create-reducer.mts +18 -18
- package/src/utils/create-state.mts +4 -4
- package/src/utils/create-state.test.mts +2 -2
- package/src/utils/state-source.mts +3 -3
|
@@ -30,11 +30,11 @@ type ChildObservableConfig<A, P extends NonEmptyUnknownList> = Readonly<{
|
|
|
30
30
|
parents: Wrap<P>;
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* Defaults to one more than the deepest parent. An update reaches
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* Defaults to one more than the deepest parent. An update reaches observables
|
|
34
|
+
* in ascending depth, so an observable `tryUpdate` reads with `getSnapshot`
|
|
35
|
+
* without being its child (`withCurrentValueFrom`'s second source) has to be
|
|
36
|
+
* shallower than this one too, or this one reads its value from before the
|
|
37
|
+
* update: pass `1 + Math.max(...)` over both then.
|
|
38
38
|
*/
|
|
39
39
|
depth?: number;
|
|
40
40
|
|
|
@@ -45,9 +45,9 @@ type ChildObservableConfig<A, P extends NonEmptyUnknownList> = Readonly<{
|
|
|
45
45
|
* An observable derived from `parents` that emits, if at all, in the same
|
|
46
46
|
* update as they do (`map`, `filter`, `combine`, …).
|
|
47
47
|
*
|
|
48
|
-
* A subclass overrides `tryUpdate`, and, when it holds something to
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* A subclass overrides `tryUpdate`, and, when it holds something to release,
|
|
49
|
+
* `teardown`; its constructor ends with `this.attach()`, once everything
|
|
50
|
+
* `tryUpdate` and `teardown` read is in place.
|
|
51
51
|
*/
|
|
52
52
|
export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
53
53
|
extends ObservableBaseClass<A, 'sync child', number>
|
|
@@ -107,10 +107,9 @@ export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
* Whether this observable can emit nothing more although some parent is
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* empty).
|
|
110
|
+
* Whether this observable can emit nothing more although some parent is still
|
|
111
|
+
* live, asked whenever a parent completes. `true` completes it then, as all
|
|
112
|
+
* parents completing does (`zip` once a completed source's queue is empty).
|
|
114
113
|
*/
|
|
115
114
|
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
116
115
|
canEmitNoMore(): boolean {
|
|
@@ -118,8 +117,8 @@ export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
118
117
|
}
|
|
119
118
|
|
|
120
119
|
/**
|
|
121
|
-
* Completes this observable once the update now propagating has reached
|
|
122
|
-
*
|
|
120
|
+
* Completes this observable once the update now propagating has reached every
|
|
121
|
+
* descendant, so that they see the value just passed on first.
|
|
123
122
|
*/
|
|
124
123
|
protected completeAfterPropagation(): void {
|
|
125
124
|
runAfterPropagation(() => {
|
|
@@ -129,8 +128,8 @@ export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
129
128
|
}
|
|
130
129
|
|
|
131
130
|
/**
|
|
132
|
-
* An observable derived from `parents` that may emit later, in an update of
|
|
133
|
-
*
|
|
131
|
+
* An observable derived from `parents` that may emit later, in an update of its
|
|
132
|
+
* own, through `startUpdate` (`debounce`, `switchMap`). It may also emit
|
|
134
133
|
* synchronously with `setNext`, as a sync child does. A subclass may also
|
|
135
134
|
* override `flush`, to pass on at completion what it still holds.
|
|
136
135
|
*/
|
|
@@ -204,13 +203,13 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
204
203
|
* `mergeMap` returned.
|
|
205
204
|
*
|
|
206
205
|
* If the inner observable already holds a value (`just(x)`, a state), it
|
|
207
|
-
* emits that value synchronously while being subscribed. That first value
|
|
208
|
-
*
|
|
209
|
-
* projection, so it joins the propagation in progress like a sync
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
206
|
+
* emits that value synchronously while being subscribed. That first value is
|
|
207
|
+
* emitted with `updateToken`, the token of the update that ran the
|
|
208
|
+
* projection, so it joins the propagation in progress like a sync operator's
|
|
209
|
+
* value would. Starting an update of its own instead would reach the nodes
|
|
210
|
+
* below before the current update reached this observable's siblings: a node
|
|
211
|
+
* reading both would see the new inner value beside stale sibling values (a
|
|
212
|
+
* glitch), and would emit twice.
|
|
214
213
|
*
|
|
215
214
|
* Values the inner observable emits later start updates of their own.
|
|
216
215
|
*/
|
|
@@ -236,8 +235,8 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
|
|
|
236
235
|
|
|
237
236
|
/**
|
|
238
237
|
* A sync child that always holds a value: a subclass passes a `Some` as its
|
|
239
|
-
* `initialValue`. It differs from its base only in the types of
|
|
240
|
-
*
|
|
238
|
+
* `initialValue`. It differs from its base only in the types of `getSnapshot`
|
|
239
|
+
* and `pipe`.
|
|
241
240
|
*/
|
|
242
241
|
export class InitializedSyncChildObservableClass<
|
|
243
242
|
A,
|
|
@@ -266,8 +265,8 @@ export class InitializedSyncChildObservableClass<
|
|
|
266
265
|
/* helpers */
|
|
267
266
|
|
|
268
267
|
/**
|
|
269
|
-
* Called by a parent that has completed: whether no live parent is left, or
|
|
270
|
-
*
|
|
268
|
+
* Called by a parent that has completed: whether no live parent is left, or no
|
|
269
|
+
* value can come from the live ones.
|
|
271
270
|
*/
|
|
272
271
|
const shouldComplete = (
|
|
273
272
|
child: ChildNode & Readonly<{ canEmitNoMore: () => boolean }>,
|
|
@@ -275,6 +274,63 @@ const shouldComplete = (
|
|
|
275
274
|
!child.isCompleted &&
|
|
276
275
|
(child.parents.every((p) => p.isCompleted) || child.canEmitNoMore());
|
|
277
276
|
|
|
277
|
+
const hasCircularDependencyFrom = (
|
|
278
|
+
node: Observable<unknown>,
|
|
279
|
+
mut_visited: MutableSet<Observable<unknown>>,
|
|
280
|
+
mut_inPath: MutableSet<Observable<unknown>>,
|
|
281
|
+
): boolean => {
|
|
282
|
+
if (mut_inPath.has(node)) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (mut_visited.has(node)) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
mut_visited.add(node);
|
|
291
|
+
|
|
292
|
+
mut_inPath.add(node);
|
|
293
|
+
|
|
294
|
+
if (node.kind !== 'root') {
|
|
295
|
+
for (const parent of node.parents) {
|
|
296
|
+
if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
mut_inPath.delete(node);
|
|
303
|
+
|
|
304
|
+
return false;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Detects circular dependencies by walking the full ancestor chain of the
|
|
309
|
+
* child's parents and checking whether the child already appears among them.
|
|
310
|
+
*
|
|
311
|
+
* @throws {Error} if a circular dependency is detected
|
|
312
|
+
*/
|
|
313
|
+
const detectCircularDependency = (child: ChildNode): void => {
|
|
314
|
+
const mut_visited = new Set<Observable<unknown>>();
|
|
315
|
+
|
|
316
|
+
const mut_inPath = new Set<Observable<unknown>>([
|
|
317
|
+
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
318
|
+
child as Observable<unknown>,
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
for (const parent of child.parents) {
|
|
322
|
+
if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
|
|
323
|
+
// A programming error (invariant violation), not a recoverable failure.
|
|
324
|
+
// `panic` marks the error it throws, so the `Result` / `AsyncResult`
|
|
325
|
+
// boundaries rethrow it rather than turning an invariant violation into
|
|
326
|
+
// an `Err` a caller might handle (Sumi D-48 / D-53).
|
|
327
|
+
panic(
|
|
328
|
+
'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
278
334
|
/** Registers a child to its parents. */
|
|
279
335
|
const registerChild = (child: ChildNode): void => {
|
|
280
336
|
if (checksCircularDependency) {
|
|
@@ -287,9 +343,9 @@ const registerChild = (child: ChildNode): void => {
|
|
|
287
343
|
};
|
|
288
344
|
|
|
289
345
|
/**
|
|
290
|
-
* Undoes {@link registerChild} for a completed child: its parents forget it,
|
|
291
|
-
*
|
|
292
|
-
*
|
|
346
|
+
* Undoes {@link registerChild} for a completed child: its parents forget it, so
|
|
347
|
+
* that no update puts it in its queue again. Without this a completed node
|
|
348
|
+
* stayed reachable for as long as its root lived (#2134).
|
|
293
349
|
*/
|
|
294
350
|
const unregisterChild = (child: ChildNode): void => {
|
|
295
351
|
for (const p of child.parents) {
|
|
@@ -320,60 +376,3 @@ const isDevelopmentProcess = (): boolean => {
|
|
|
320
376
|
* bundle).
|
|
321
377
|
*/
|
|
322
378
|
export const checksCircularDependency: boolean = isDevelopmentProcess();
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Detects circular dependencies by walking the full ancestor chain of the
|
|
326
|
-
* child's parents and checking whether the child already appears among them.
|
|
327
|
-
*
|
|
328
|
-
* @throws {Error} if a circular dependency is detected
|
|
329
|
-
*/
|
|
330
|
-
const detectCircularDependency = (child: ChildNode): void => {
|
|
331
|
-
const mut_visited = new Set<Observable<unknown>>();
|
|
332
|
-
|
|
333
|
-
const mut_inPath = new Set<Observable<unknown>>([
|
|
334
|
-
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
335
|
-
child as Observable<unknown>,
|
|
336
|
-
]);
|
|
337
|
-
|
|
338
|
-
for (const parent of child.parents) {
|
|
339
|
-
if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
|
|
340
|
-
// A programming error (invariant violation), not a recoverable
|
|
341
|
-
// failure. `panic` marks the error it throws, so the `Result` /
|
|
342
|
-
// `AsyncResult` boundaries rethrow it rather than turning an invariant
|
|
343
|
-
// violation into an `Err` a caller might handle (Sumi D-48 / D-53).
|
|
344
|
-
panic(
|
|
345
|
-
'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
const hasCircularDependencyFrom = (
|
|
352
|
-
node: Observable<unknown>,
|
|
353
|
-
mut_visited: MutableSet<Observable<unknown>>,
|
|
354
|
-
mut_inPath: MutableSet<Observable<unknown>>,
|
|
355
|
-
): boolean => {
|
|
356
|
-
if (mut_inPath.has(node)) {
|
|
357
|
-
return true;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (mut_visited.has(node)) {
|
|
361
|
-
return false;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
mut_visited.add(node);
|
|
365
|
-
|
|
366
|
-
mut_inPath.add(node);
|
|
367
|
-
|
|
368
|
-
if (node.kind !== 'root') {
|
|
369
|
-
for (const parent of node.parents) {
|
|
370
|
-
if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
|
|
371
|
-
return true;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
mut_inPath.delete(node);
|
|
377
|
-
|
|
378
|
-
return false;
|
|
379
|
-
};
|
|
@@ -49,8 +49,8 @@ describe('circular dependency comparison', () => {
|
|
|
49
49
|
);
|
|
50
50
|
|
|
51
51
|
// Simulate a cycle: mut_childA → childB → mut_childA
|
|
52
|
-
// In normal usage this is impossible — the check runs in every
|
|
53
|
-
//
|
|
52
|
+
// In normal usage this is impossible — the check runs in every child
|
|
53
|
+
// constructor before the reference becomes available.
|
|
54
54
|
Object.defineProperty(mut_childA, 'parents', {
|
|
55
55
|
value: [childB],
|
|
56
56
|
writable: false,
|
|
@@ -31,17 +31,16 @@ export type SyncChildObservableTools<A> = Readonly<{
|
|
|
31
31
|
complete: () => void;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Completes this observable once the update now propagating has reached
|
|
35
|
-
*
|
|
34
|
+
* Completes this observable once the update now propagating has reached every
|
|
35
|
+
* descendant, so that they see the value just passed on first.
|
|
36
36
|
*/
|
|
37
37
|
completeAfterPropagation: () => void;
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Registers a teardown (clear timers, unsubscribe inner observables, …) to
|
|
41
|
-
* run when this observable completes or is disposed, in the order
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* teardown runs at once.
|
|
41
|
+
* run when this observable completes or is disposed, in the order registered.
|
|
42
|
+
* Called from `init`, it lets the teardown close over what `init` creates.
|
|
43
|
+
* Registered once the observable has completed, the teardown runs at once.
|
|
45
44
|
*/
|
|
46
45
|
onComplete: (teardown: () => void) => void;
|
|
47
46
|
}>;
|
|
@@ -57,21 +56,20 @@ export type AsyncChildObservableTools<A> = Readonly<{
|
|
|
57
56
|
SyncChildObservableTools<A>;
|
|
58
57
|
|
|
59
58
|
/**
|
|
60
|
-
* What a sync child's `init` may return instead of `tryUpdate` alone: the
|
|
61
|
-
*
|
|
59
|
+
* What a sync child's `init` may return instead of `tryUpdate` alone: the hooks
|
|
60
|
+
* the factory calls, which read the state `init` creates.
|
|
62
61
|
*/
|
|
63
62
|
export type SyncChildObservableHooks = Readonly<{
|
|
64
63
|
/**
|
|
65
|
-
* Called with the token of every update in which a parent of this
|
|
66
|
-
*
|
|
64
|
+
* Called with the token of every update in which a parent of this observable
|
|
65
|
+
* emitted.
|
|
67
66
|
*/
|
|
68
67
|
tryUpdate: (updateToken: UpdateToken) => void;
|
|
69
68
|
|
|
70
69
|
/**
|
|
71
|
-
* Whether this observable can emit nothing more although some parent is
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* empty).
|
|
70
|
+
* Whether this observable can emit nothing more although some parent is still
|
|
71
|
+
* live, asked whenever a parent completes. `true` completes it then, as all
|
|
72
|
+
* parents completing does (`zip` once a completed source's queue is empty).
|
|
75
73
|
*/
|
|
76
74
|
canEmitNoMore?: () => boolean;
|
|
77
75
|
}>;
|
|
@@ -84,8 +82,8 @@ export type SyncChildObservableHooks = Readonly<{
|
|
|
84
82
|
export type AsyncChildObservableHooks = SyncChildObservableHooks &
|
|
85
83
|
Readonly<{
|
|
86
84
|
/**
|
|
87
|
-
* Run at the beginning of `complete()`, before the teardowns, and never
|
|
88
|
-
*
|
|
85
|
+
* Run at the beginning of `complete()`, before the teardowns, and never by
|
|
86
|
+
* `dispose()`: passes on what this observable still holds through
|
|
89
87
|
* `startUpdate`, so that its subscribers see it before they see the
|
|
90
88
|
* completion. A disposed observable has no one left to tell.
|
|
91
89
|
*/
|
|
@@ -96,11 +94,11 @@ type ChildObservableConfigBase<P extends NonEmptyUnknownList> = Readonly<{
|
|
|
96
94
|
parents: Wrap<P>;
|
|
97
95
|
|
|
98
96
|
/**
|
|
99
|
-
* Defaults to one more than the deepest parent. An update reaches
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
97
|
+
* Defaults to one more than the deepest parent. An update reaches observables
|
|
98
|
+
* in ascending depth, so an observable `tryUpdate` reads with `getSnapshot`
|
|
99
|
+
* without being its child (`withCurrentValueFrom`'s second source) has to be
|
|
100
|
+
* shallower than this one too, or this one reads its value from before the
|
|
101
|
+
* update: pass `1 + Math.max(...)` over both then.
|
|
104
102
|
*/
|
|
105
103
|
depth?: number;
|
|
106
104
|
}>;
|
|
@@ -122,15 +120,15 @@ export type InitializedChildObservableConfig<
|
|
|
122
120
|
}>;
|
|
123
121
|
|
|
124
122
|
/**
|
|
125
|
-
* Creates a sync-child observable: one derived from `parents` that emits, if
|
|
126
|
-
*
|
|
127
|
-
*
|
|
123
|
+
* Creates a sync-child observable: one derived from `parents` that emits, if at
|
|
124
|
+
* all, in the same update as they do. `map`, `filter`, `combine` and the other
|
|
125
|
+
* synchronous operators are built with it.
|
|
128
126
|
*
|
|
129
127
|
* `init` runs during construction and receives the tools. It returns
|
|
130
128
|
* `tryUpdate`, which is called with the token of every update in which some
|
|
131
|
-
* parent of this observable emitted — a parent that did not emit in that
|
|
132
|
-
*
|
|
133
|
-
*
|
|
129
|
+
* parent of this observable emitted — a parent that did not emit in that update
|
|
130
|
+
* has a different `updateToken`; to emit, call `setNext` with the token
|
|
131
|
+
* received — or an object holding `tryUpdate` and the other hooks
|
|
134
132
|
* ({@link SyncChildObservableHooks}). State the hooks share can live inside
|
|
135
133
|
* `init`, next to the teardown it registers with the `onComplete` tool.
|
|
136
134
|
* `init` may complete the observable; it is then never registered to its
|
|
@@ -211,8 +209,8 @@ export const createInitializedSyncChildObservable = <
|
|
|
211
209
|
* Creates an async-child observable: one derived from `parents` that may emit
|
|
212
210
|
* later, in an update of its own, through `startUpdate` (`debounce`,
|
|
213
211
|
* `switchMap`). It may also emit synchronously with `setNext`, as a sync child
|
|
214
|
-
* does. See {@link createSyncChildObservable} for the `init` contract; an
|
|
215
|
-
*
|
|
212
|
+
* does. See {@link createSyncChildObservable} for the `init` contract; an async
|
|
213
|
+
* child's `init` may also return a `flush` hook
|
|
216
214
|
* ({@link AsyncChildObservableHooks}).
|
|
217
215
|
*/
|
|
218
216
|
export const createAsyncChildObservable = <A, P extends NonEmptyUnknownList>(
|
|
@@ -8,8 +8,8 @@ import type { RootObservable } from '../types/index.mjs';
|
|
|
8
8
|
import { RootObservableClass } from './root-observable-class.mjs';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* The tools a custom root's `init` receives: the `protected` methods a
|
|
12
|
-
*
|
|
11
|
+
* The tools a custom root's `init` receives: the `protected` methods a subclass
|
|
12
|
+
* of `RootObservableClass` inherits.
|
|
13
13
|
*/
|
|
14
14
|
export type RootObservableTools<A> = Readonly<{
|
|
15
15
|
/** Emits `nextValue` and propagates the update to all descendants. */
|
|
@@ -23,9 +23,9 @@ export type RootObservableTools<A> = Readonly<{
|
|
|
23
23
|
complete: () => void;
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* Registers a teardown (clear timers, abort requests, remove listeners, …)
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Registers a teardown (clear timers, abort requests, remove listeners, …) to
|
|
27
|
+
* run at the beginning of `complete()`, in the order registered. Called from
|
|
28
|
+
* `init`, it lets the teardown close over what `init` creates.
|
|
29
29
|
* Registered once the observable has completed — `init` may complete it
|
|
30
30
|
* synchronously first — the teardown runs at once.
|
|
31
31
|
*/
|
|
@@ -37,13 +37,13 @@ export type RootObservableConfig<A> = Readonly<{
|
|
|
37
37
|
}>;
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Creates a root observable, for a custom creator written without a class
|
|
41
|
-
*
|
|
40
|
+
* Creates a root observable, for a custom creator written without a class of
|
|
41
|
+
* its own.
|
|
42
42
|
*
|
|
43
|
-
* `init` runs during construction, receives the tools, and returns extra
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
43
|
+
* `init` runs during construction, receives the tools, and returns extra public
|
|
44
|
+
* members to merge into the observable (e.g. `next` for a source, `start` for a
|
|
45
|
+
* timer). It may register asynchronous callbacks that use the tools, and may
|
|
46
|
+
* call `complete` synchronously.
|
|
47
47
|
*/
|
|
48
48
|
export const createRootObservable = <
|
|
49
49
|
A,
|
|
@@ -12,12 +12,12 @@ import {
|
|
|
12
12
|
} from './propagation-state.mjs';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* The base of the observables that start updates of their own: a root, and
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
15
|
+
* The base of the observables that start updates of their own: a root, and an
|
|
16
|
+
* async child (`debounce`, `switchMap`), which emits later than its parents do.
|
|
17
|
+
* Its update visits, shallowest first, the nodes its value reaches: each node
|
|
18
|
+
* that emits puts its children in the update's queue (`propagation-state.mts`),
|
|
19
|
+
* so that each sees its parents' values of that update (no glitch) and a node
|
|
20
|
+
* no value reached is not visited.
|
|
21
21
|
*/
|
|
22
22
|
export class ManagerObservableClass<
|
|
23
23
|
A,
|
|
@@ -26,9 +26,9 @@ export class ManagerObservableClass<
|
|
|
26
26
|
> extends ObservableBaseClass<A, Kind, Depth> {
|
|
27
27
|
/** Emits `nextValue` and propagates it to the descendants. */
|
|
28
28
|
protected startUpdate(nextValue: A): void {
|
|
29
|
-
// Nothing to propagate to (a branch no `switchMap` follows, a state
|
|
30
|
-
//
|
|
31
|
-
//
|
|
29
|
+
// Nothing to propagate to (a branch no `switchMap` follows, a state written
|
|
30
|
+
// before anything subscribes): the update is the value alone, and opening
|
|
31
|
+
// one would cost more than storing it.
|
|
32
32
|
if (this.setNextIfUnobserved(nextValue)) {
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
@@ -37,9 +37,9 @@ export class ManagerObservableClass<
|
|
|
37
37
|
|
|
38
38
|
beginPropagation(updateToken);
|
|
39
39
|
|
|
40
|
-
// A subscriber or projection may throw, and the depth has to come back
|
|
41
|
-
//
|
|
42
|
-
//
|
|
40
|
+
// A subscriber or projection may throw, and the depth has to come back down
|
|
41
|
+
// regardless, or every later write would warn. The exception itself is not
|
|
42
|
+
// caught: it reaches the caller of `next` as it always has.
|
|
43
43
|
// @sumi-expect-error exceptions/no-try
|
|
44
44
|
try {
|
|
45
45
|
this.setNext(nextValue, updateToken);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Sumi bans classes, `this`, method syntax and writes to `this` outside a
|
|
2
|
-
// `mut_` binding. The observables are classes (why is on
|
|
3
|
-
//
|
|
2
|
+
// `mut_` binding. The observables are classes (why is on `ObservableBaseClass`
|
|
3
|
+
// below), so those four rules are waived here (D-63).
|
|
4
4
|
// @sumi-expect-error-file classes/no-class
|
|
5
5
|
// @sumi-expect-error-file banned-syntax/no-this
|
|
6
6
|
// @sumi-expect-error-file functions/prefer-arrow-function
|
|
@@ -35,25 +35,23 @@ import { enqueueChildren } from './propagation-state.mjs';
|
|
|
35
35
|
/**
|
|
36
36
|
* The base of every observable, and the reason an observable is a class.
|
|
37
37
|
*
|
|
38
|
-
* A node is created for every derived value of a UI, so creating one has to
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* the first one arrives.
|
|
38
|
+
* A node is created for every derived value of a UI, so creating one has to be
|
|
39
|
+
* cheap, and most of its cost was its methods: each observable used to be a set
|
|
40
|
+
* of closures made for it, a dozen or more per node (#2421). Sharing the
|
|
41
|
+
* methods among all nodes removes that cost, and a shared method has to read
|
|
42
|
+
* the node it is called on from `this`. Once `this` is unavoidable, a class is
|
|
43
|
+
* the plainest way to say it: the methods live once on the prototype, `this` is
|
|
44
|
+
* typed, a creator or operator is a subclass that overrides `tryUpdate` (and
|
|
45
|
+
* `flush` / `teardown` where it holds something), and assigning every field in
|
|
46
|
+
* the constructor gives the nodes of one class one shape for V8. The
|
|
47
|
+
* collections (`children`, subscribers) are made when the first one arrives.
|
|
49
48
|
*
|
|
50
49
|
* The methods read the observable they are called on, so taking one off it
|
|
51
50
|
* (`const { subscribe } = obs$`) is not supported.
|
|
52
51
|
*
|
|
53
|
-
* A node reaches another node only through its methods and its public
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* these.
|
|
52
|
+
* A node reaches another node only through its methods and its public fields,
|
|
53
|
+
* never through a `#private` one: the other node may be an instance of another
|
|
54
|
+
* copy of synstate in the same program, whose classes are not these.
|
|
57
55
|
*/
|
|
58
56
|
export class ObservableBaseClass<
|
|
59
57
|
A,
|
|
@@ -144,8 +142,8 @@ export class ObservableBaseClass<
|
|
|
144
142
|
}
|
|
145
143
|
|
|
146
144
|
addChild(child: ChildNode): void {
|
|
147
|
-
// A set, not an array: a completed child removes itself, and that has to
|
|
148
|
-
//
|
|
145
|
+
// A set, not an array: a completed child removes itself, and that has to be
|
|
146
|
+
// O(1) for a UI that creates and completes derived nodes on every mount.
|
|
149
147
|
if (this.#mut_children === undefined) {
|
|
150
148
|
this.#mut_children = new Set<ChildNode>([child]);
|
|
151
149
|
} else {
|
|
@@ -158,10 +156,10 @@ export class ObservableBaseClass<
|
|
|
158
156
|
}
|
|
159
157
|
|
|
160
158
|
/**
|
|
161
|
-
* Stores `nextValue` as the current value, queues the children for the
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
159
|
+
* Stores `nextValue` as the current value, queues the children for the update
|
|
160
|
+
* `updateToken`, and notifies subscribers. The children are queued first, so
|
|
161
|
+
* that a child a subscriber creates, which starts from this value, is not
|
|
162
|
+
* visited for it again.
|
|
165
163
|
*/
|
|
166
164
|
protected setNext(nextValue: A, updateToken: UpdateToken): void {
|
|
167
165
|
this.#mut_updateToken = updateToken;
|
|
@@ -180,11 +178,11 @@ export class ObservableBaseClass<
|
|
|
180
178
|
}
|
|
181
179
|
|
|
182
180
|
/**
|
|
183
|
-
* Stores `nextValue` and returns `true` when nothing observes this
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
181
|
+
* Stores `nextValue` and returns `true` when nothing observes this observable
|
|
182
|
+
* — no child to queue and no subscriber to call — so that the caller need not
|
|
183
|
+
* open an update; returns `false`, storing nothing, otherwise. The value
|
|
184
|
+
* still gets a token of its own, so that whoever stored the last one sees
|
|
185
|
+
* that it emitted.
|
|
188
186
|
*/
|
|
189
187
|
protected setNextIfUnobserved(nextValue: A): boolean {
|
|
190
188
|
if (this.hasChild || this.hasSubscriber) {
|
|
@@ -220,9 +218,9 @@ export class ObservableBaseClass<
|
|
|
220
218
|
|
|
221
219
|
/**
|
|
222
220
|
* Completes the observable: an async child first passes on what it still
|
|
223
|
-
* holds (`flush`), then it is disposed. Completing never reaches the
|
|
224
|
-
*
|
|
225
|
-
*
|
|
221
|
+
* holds (`flush`), then it is disposed. Completing never reaches the parents:
|
|
222
|
+
* an upstream observable may be shared, and only whoever owns it decides when
|
|
223
|
+
* it ends (#2133).
|
|
226
224
|
*/
|
|
227
225
|
complete(): void {
|
|
228
226
|
if (this.#mut_isCompleted) {
|
|
@@ -235,8 +233,8 @@ export class ObservableBaseClass<
|
|
|
235
233
|
}
|
|
236
234
|
|
|
237
235
|
/**
|
|
238
|
-
* Completes the observable without passing on anything it still holds,
|
|
239
|
-
*
|
|
236
|
+
* Completes the observable without passing on anything it still holds, runs
|
|
237
|
+
* its teardown, and takes it out of the graph.
|
|
240
238
|
*/
|
|
241
239
|
dispose(): void {
|
|
242
240
|
// A completed node is reached again through every parent that completes
|
|
@@ -275,8 +273,8 @@ export class ObservableBaseClass<
|
|
|
275
273
|
}
|
|
276
274
|
|
|
277
275
|
/**
|
|
278
|
-
* Passes on what the observable still holds, at the beginning of
|
|
279
|
-
*
|
|
276
|
+
* Passes on what the observable still holds, at the beginning of `complete()`
|
|
277
|
+
* and never in `dispose()` (`debounce`'s pending value).
|
|
280
278
|
*/
|
|
281
279
|
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
282
280
|
protected flush(): void {}
|
|
@@ -38,10 +38,9 @@ export type ChildNode = NodeInternals &
|
|
|
38
38
|
}>;
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* The one place a public observable is seen as the node it is. Every
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* those types.
|
|
41
|
+
* The one place a public observable is seen as the node it is. Every observable
|
|
42
|
+
* is built by a synstate class, and the public types carry a brand an object
|
|
43
|
+
* literal cannot supply, so this holds for every value of those types.
|
|
45
44
|
*/
|
|
46
45
|
export const toNode = <A,>(observable: Observable<A>): ObservableNode<A> =>
|
|
47
46
|
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|