synstate 6.1.1 → 7.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.
- package/README.md +3 -3
- package/dist/core/authoring.d.mts +3 -3
- package/dist/core/class/child-observable-class.d.mts +40 -27
- package/dist/core/class/child-observable-class.d.mts.map +1 -1
- package/dist/core/class/child-observable-class.mjs +78 -46
- package/dist/core/class/child-observable-class.mjs.map +1 -1
- package/dist/core/class/create-child-observable.d.mts +28 -30
- package/dist/core/class/create-child-observable.d.mts.map +1 -1
- package/dist/core/class/create-child-observable.mjs +6 -8
- package/dist/core/class/create-child-observable.mjs.map +1 -1
- package/dist/core/class/create-root-observable.d.mts +12 -12
- package/dist/core/class/create-root-observable.d.mts.map +1 -1
- package/dist/core/class/create-root-observable.mjs +1 -5
- package/dist/core/class/create-root-observable.mjs.map +1 -1
- 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 +1 -3
- 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 +2 -2
- 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.d.mts.map +1 -1
- package/dist/core/combine/zip.mjs +5 -2
- package/dist/core/combine/zip.mjs.map +1 -1
- package/dist/core/create/counter.d.mts +3 -3
- package/dist/core/create/counter.mjs +2 -2
- package/dist/core/create/counter.mjs.map +1 -1
- package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
- package/dist/core/create/from-abortable-promise.mjs +10 -3
- package/dist/core/create/from-abortable-promise.mjs.map +1 -1
- package/dist/core/create/from-promise.d.mts.map +1 -1
- package/dist/core/create/from-promise.mjs +10 -3
- 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.d.mts.map +1 -1
- package/dist/core/create/source.mjs +2 -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.d.mts +1 -1
- package/dist/core/operators/filter.d.mts.map +1 -1
- package/dist/core/operators/filter.mjs +2 -3
- 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 +7 -3
- package/dist/core/operators/merge-map.d.mts.map +1 -1
- package/dist/core/operators/merge-map.mjs +47 -16
- 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.d.mts +1 -1
- package/dist/core/operators/skip-while.d.mts.map +1 -1
- package/dist/core/operators/skip-while.mjs +9 -6
- package/dist/core/operators/skip-while.mjs.map +1 -1
- package/dist/core/operators/switch-map.d.mts +4 -0
- package/dist/core/operators/switch-map.d.mts.map +1 -1
- package/dist/core/operators/switch-map.mjs +16 -6
- 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.d.mts +2 -2
- package/dist/core/operators/take-while.d.mts.map +1 -1
- package/dist/core/operators/take-while.mjs +10 -8
- 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 +3 -2
- package/dist/core/operators/throttle.mjs.map +1 -1
- package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
- package/dist/core/operators/with-buffered-from.mjs +13 -3
- 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.d.mts +1 -1
- package/dist/core/predefined/operators/attach-index.d.mts.map +1 -1
- package/dist/core/predefined/operators/attach-index.mjs +2 -3
- 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/predefined/operators/skip.d.mts.map +1 -1
- package/dist/core/predefined/operators/skip.mjs +2 -2
- package/dist/core/predefined/operators/skip.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/index.d.mts +1 -0
- package/dist/core/utils/index.d.mts.map +1 -1
- package/dist/core/utils/index.mjs +1 -0
- package/dist/core/utils/index.mjs.map +1 -1
- package/dist/core/utils/observable-brand.d.mts +5 -5
- package/dist/core/utils/rethrow-out-of-promise.d.mts +10 -0
- package/dist/core/utils/rethrow-out-of-promise.d.mts.map +1 -0
- package/dist/core/utils/rethrow-out-of-promise.mjs +17 -0
- package/dist/core/utils/rethrow-out-of-promise.mjs.map +1 -0
- 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 +12 -11
- package/src/core/authoring.mts +3 -3
- package/src/core/class/child-observable-class.mts +131 -90
- package/src/core/class/circular-dependency-comparison.test.mts +2 -2
- package/src/core/class/create-child-observable.mts +35 -34
- package/src/core/class/create-root-observable.mts +13 -12
- 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 +27 -29
- 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/class/write-during-propagation.test.mts +75 -4
- package/src/core/combine/derive.mts +12 -12
- package/src/core/combine/merge.mts +2 -2
- package/src/core/combine/zip.mts +12 -9
- package/src/core/create/counter.mts +7 -7
- package/src/core/create/from-abortable-promise.mts +35 -25
- package/src/core/create/from-promise.mts +30 -20
- package/src/core/create/from-promise.test.mts +97 -0
- package/src/core/create/from-subscribable.mts +2 -2
- package/src/core/create/source.mts +7 -6
- 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 +6 -7
- package/src/core/operators/flat-map-initial-value.test.mts +269 -0
- package/src/core/operators/flat-map-sync-inner.test.mts +4 -2
- package/src/core/operators/inner-ownership.test.mts +145 -2
- 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 +59 -29
- 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 +14 -10
- package/src/core/operators/skip-while.test.mts +59 -0
- package/src/core/operators/switch-map.mts +26 -15
- package/src/core/operators/take-until.mts +2 -2
- package/src/core/operators/take-while.mts +17 -19
- package/src/core/operators/take-while.test.mts +51 -0
- package/src/core/operators/take.mts +2 -2
- package/src/core/operators/throttle.mts +6 -5
- package/src/core/operators/with-buffered-from.mts +18 -5
- package/src/core/operators/with-buffered-from.test.mts +27 -0
- 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 +4 -5
- package/src/core/predefined/operators/map-to.mts +3 -4
- package/src/core/predefined/operators/skip.mts +2 -2
- 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/index.mts +1 -0
- package/src/core/utils/observable-brand.mts +5 -5
- package/src/core/utils/rethrow-out-of-promise.mts +16 -0
- 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
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
// @sumi-expect-error-file banned-syntax/no-this
|
|
3
3
|
// @sumi-expect-error-file functions/prefer-arrow-function
|
|
4
4
|
// @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
|
|
5
|
-
import {
|
|
5
|
+
import type { Optional } from 'ts-data-forge';
|
|
6
|
+
import * as Arr from 'ts-data-forge/array';
|
|
6
7
|
import type { ReadonlyRecord } from 'ts-type-forge';
|
|
7
8
|
import type { RootObservable } from '../types/index.mjs';
|
|
8
9
|
import { RootObservableClass } from './root-observable-class.mjs';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
* The tools a custom root's `init` receives: the `protected` methods a
|
|
12
|
-
*
|
|
12
|
+
* The tools a custom root's `init` receives: the `protected` methods a subclass
|
|
13
|
+
* of `RootObservableClass` inherits.
|
|
13
14
|
*/
|
|
14
15
|
export type RootObservableTools<A> = Readonly<{
|
|
15
16
|
/** Emits `nextValue` and propagates the update to all descendants. */
|
|
@@ -23,9 +24,9 @@ export type RootObservableTools<A> = Readonly<{
|
|
|
23
24
|
complete: () => void;
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
|
-
* Registers a teardown (clear timers, abort requests, remove listeners, …)
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* Registers a teardown (clear timers, abort requests, remove listeners, …) to
|
|
28
|
+
* run at the beginning of `complete()`, in the order registered. Called from
|
|
29
|
+
* `init`, it lets the teardown close over what `init` creates.
|
|
29
30
|
* Registered once the observable has completed — `init` may complete it
|
|
30
31
|
* synchronously first — the teardown runs at once.
|
|
31
32
|
*/
|
|
@@ -37,13 +38,13 @@ export type RootObservableConfig<A> = Readonly<{
|
|
|
37
38
|
}>;
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
|
-
* Creates a root observable, for a custom creator written without a class
|
|
41
|
-
*
|
|
41
|
+
* Creates a root observable, for a custom creator written without a class of
|
|
42
|
+
* its own.
|
|
42
43
|
*
|
|
43
|
-
* `init` runs during construction, receives the tools, and returns extra
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* `init` runs during construction, receives the tools, and returns extra public
|
|
45
|
+
* members to merge into the observable (e.g. `next` for a source, `start` for a
|
|
46
|
+
* timer). It may register asynchronous callbacks that use the tools, and may
|
|
47
|
+
* call `complete` synchronously.
|
|
47
48
|
*/
|
|
48
49
|
export const createRootObservable = <
|
|
49
50
|
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
|
|
@@ -11,15 +11,15 @@
|
|
|
11
11
|
* costs about twice as much per access as a module binding, on a path every
|
|
12
12
|
* update takes.
|
|
13
13
|
*/
|
|
14
|
-
import
|
|
14
|
+
import * as Arr from 'ts-data-forge/array';
|
|
15
15
|
import { panic } from 'ts-std-forge';
|
|
16
16
|
import type { MutableSet } from 'ts-type-forge';
|
|
17
17
|
import type { UpdateToken } from '../types/index.mjs';
|
|
18
18
|
import type { ChildNode } from './observable-node.mjs';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* How many updates are propagating, across every observable, and the token
|
|
22
|
-
*
|
|
21
|
+
* How many updates are propagating, across every observable, and the token of
|
|
22
|
+
* the outermost one. A write nothing observes opens none
|
|
23
23
|
* (`setNextIfUnobserved`).
|
|
24
24
|
*/
|
|
25
25
|
const mut_propagation: {
|
|
@@ -28,15 +28,14 @@ const mut_propagation: {
|
|
|
28
28
|
} = { depth: 0, outermostToken: undefined };
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Work deferred by {@link runAfterPropagation}, run once the outermost
|
|
32
|
-
*
|
|
31
|
+
* Work deferred by {@link runAfterPropagation}, run once the outermost update
|
|
32
|
+
* has reached every descendant.
|
|
33
33
|
*/
|
|
34
34
|
const mut_afterPropagation: (() => void)[] = [];
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Whether some observable's update is propagating right now. A root written
|
|
38
|
-
*
|
|
39
|
-
* (#2336).
|
|
37
|
+
* Whether some observable's update is propagating right now. A root written to
|
|
38
|
+
* while one is, is written to from inside a subscriber or a projection (#2336).
|
|
40
39
|
*/
|
|
41
40
|
export const isPropagating = (): boolean => mut_propagation.depth > 0;
|
|
42
41
|
|
|
@@ -71,8 +70,7 @@ export const beginPropagation = (updateToken: UpdateToken): void => {
|
|
|
71
70
|
};
|
|
72
71
|
|
|
73
72
|
/**
|
|
74
|
-
* Ends one update, and runs the deferred work once the outermost one has
|
|
75
|
-
* ended.
|
|
73
|
+
* Ends one update, and runs the deferred work once the outermost one has ended.
|
|
76
74
|
*/
|
|
77
75
|
export const endPropagation = (): void => {
|
|
78
76
|
mut_propagation.depth -= 1;
|
|
@@ -91,11 +89,11 @@ export const endPropagation = (): void => {
|
|
|
91
89
|
};
|
|
92
90
|
|
|
93
91
|
/**
|
|
94
|
-
* The buckets of this copy's open updates, one set per nesting level: a
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* update
|
|
98
|
-
*
|
|
92
|
+
* The buckets of this copy's open updates, one set per nesting level: a bucket
|
|
93
|
+
* per depth below the node that started the update, holding the queued nodes
|
|
94
|
+
* and how many of its slots are in use. Kept from update to update, so that an
|
|
95
|
+
* update allocates nothing but its token; emptying a bucket is setting its
|
|
96
|
+
* length to 0.
|
|
99
97
|
*/
|
|
100
98
|
const mut_bucketSets: Bucket[][] = [];
|
|
101
99
|
|
|
@@ -125,9 +123,9 @@ export const openUpdate = (depth: number): UpdateToken => {
|
|
|
125
123
|
|
|
126
124
|
/**
|
|
127
125
|
* How many updates of one copy may be open at once, one inside another. An
|
|
128
|
-
* update opens inside another only when a subscriber or projection writes to
|
|
129
|
-
*
|
|
130
|
-
*
|
|
126
|
+
* update opens inside another only when a subscriber or projection writes to a
|
|
127
|
+
* root while the outer one is propagating; a chain of such writes this deep is
|
|
128
|
+
* a loop that would otherwise run until the stack overflows.
|
|
131
129
|
*/
|
|
132
130
|
export const maxUpdateNesting = 100;
|
|
133
131
|
|
|
@@ -173,10 +171,10 @@ type Bucket = {
|
|
|
173
171
|
*
|
|
174
172
|
* A queued node is marked with the token of the update that queued it
|
|
175
173
|
* (`queuedFor`), which is how an update queues a node once. The mark is not
|
|
176
|
-
* cleared after the visit: a later update has another token. An update does
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
174
|
+
* cleared after the visit: a later update has another token. An update does not
|
|
175
|
+
* overwrite the mark of an update that is still open — one it is nested in, or
|
|
176
|
+
* one of another copy of synstate — and remembers the nodes it queues despite
|
|
177
|
+
* such a mark in a set of its own instead.
|
|
180
178
|
*/
|
|
181
179
|
class UpdateQueue {
|
|
182
180
|
readonly #base: number;
|
|
@@ -252,8 +250,8 @@ class UpdateQueue {
|
|
|
252
250
|
}
|
|
253
251
|
|
|
254
252
|
if (mark !== undefined && toQueue(mark).isOpen) {
|
|
255
|
-
// Another open update has queued `child` and may not have visited
|
|
256
|
-
//
|
|
253
|
+
// Another open update has queued `child` and may not have visited it
|
|
254
|
+
// yet: leave its mark alone.
|
|
257
255
|
if (this.#mut_queuedDespiteMark === undefined) {
|
|
258
256
|
this.#mut_queuedDespiteMark = new Set<ChildNode>();
|
|
259
257
|
} else if (this.#mut_queuedDespiteMark.has(child)) {
|
|
@@ -274,8 +272,8 @@ class UpdateQueue {
|
|
|
274
272
|
|
|
275
273
|
const mut_buckets = this.#mut_buckets;
|
|
276
274
|
|
|
277
|
-
// `size` grows while the buckets are visited, always past `index`, which
|
|
278
|
-
//
|
|
275
|
+
// `size` grows while the buckets are visited, always past `index`, which a
|
|
276
|
+
// `range` built at the start would miss.
|
|
279
277
|
// eslint-disable-next-line ts-data-forge/prefer-range-for-loop
|
|
280
278
|
for (let mut_index = 0; mut_index < this.#mut_size; mut_index += 1) {
|
|
281
279
|
const mut_bucket = mut_buckets[mut_index];
|
|
@@ -284,9 +282,9 @@ class UpdateQueue {
|
|
|
284
282
|
continue;
|
|
285
283
|
}
|
|
286
284
|
|
|
287
|
-
// A node visited here queues only deeper nodes, so this bucket does
|
|
288
|
-
//
|
|
289
|
-
//
|
|
285
|
+
// A node visited here queues only deeper nodes, so this bucket does not
|
|
286
|
+
// grow while it is visited. Its length is kept until the end, so that
|
|
287
|
+
// `close` can clear what an exception leaves.
|
|
290
288
|
const { nodes: mut_nodes, length } = mut_bucket;
|
|
291
289
|
|
|
292
290
|
// eslint-disable-next-line ts-data-forge/prefer-range-for-loop
|
|
@@ -202,8 +202,8 @@ describe('a completed observable leaves the propagation graph', () => {
|
|
|
202
202
|
|
|
203
203
|
const mut_got: number[] = [];
|
|
204
204
|
|
|
205
|
-
// Subscribed on a child, so the node is created while the update of `s`
|
|
206
|
-
//
|
|
205
|
+
// Subscribed on a child, so the node is created while the update of `s` is
|
|
206
|
+
// under way.
|
|
207
207
|
s.pipe(map((x) => x)).subscribe((x) => {
|
|
208
208
|
if (x === 1) {
|
|
209
209
|
s.pipe(
|
|
@@ -16,11 +16,11 @@ export class RootObservableClass<A>
|
|
|
16
16
|
implements RootObservable<A>
|
|
17
17
|
{
|
|
18
18
|
/**
|
|
19
|
-
* The outermost update propagating when this root was created. A root
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* The outermost update propagating when this root was created. A root created
|
|
20
|
+
* while an update propagates (inside a `switchMap` projection, say) may emit
|
|
21
|
+
* while it is created, as `fromSubscribable` does for a synchronous source.
|
|
22
|
+
* Nothing is derived from it yet, so that is its initial value rather than a
|
|
23
|
+
* write (#2336).
|
|
24
24
|
*/
|
|
25
25
|
readonly #createdDuring: UpdateToken | undefined;
|
|
26
26
|
#mut_hasWarned: boolean;
|
|
@@ -64,10 +64,10 @@ export class RootObservableClass<A>
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Shown once per root observable, the first time it is written to (`next`,
|
|
67
|
-
* `setState`, `dispatch`, …) while another update is still propagating —
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
67
|
+
* `setState`, `dispatch`, …) while another update is still propagating — from a
|
|
68
|
+
* subscriber or a projection. The write still propagates immediately, as it
|
|
69
|
+
* always has; the warning is there because the pattern is wrong, not because
|
|
70
|
+
* anything was refused.
|
|
71
71
|
*/
|
|
72
72
|
export const writeDuringPropagationWarning =
|
|
73
73
|
'synstate: an observable was written to (next / setState / dispatch) while an update was propagating, so subscribers can see intermediate states. Derive the value with map / combine instead of writing from a subscriber or a projection.';
|
|
@@ -6,8 +6,8 @@ import { debounce, map, switchMap } from '../operators/index.mjs';
|
|
|
6
6
|
import { writeDuringPropagationWarning } from './root-observable-class.mjs';
|
|
7
7
|
|
|
8
8
|
// A write to an observable with no child and no subscriber stores the value
|
|
9
|
-
// without opening an update (`ManagerObservableClass.startUpdate`). These
|
|
10
|
-
//
|
|
9
|
+
// without opening an update (`ManagerObservableClass.startUpdate`). These pin
|
|
10
|
+
// what that must not change.
|
|
11
11
|
describe('a write nothing observes', () => {
|
|
12
12
|
test('is held, and a later subscriber receives the last one', () => {
|
|
13
13
|
const s$ = source<number>(0);
|
|
@@ -98,8 +98,8 @@ describe('a write nothing observes', () => {
|
|
|
98
98
|
|
|
99
99
|
a$.next(6);
|
|
100
100
|
|
|
101
|
-
// merge passes on the parents that emitted in an update: only b$, then
|
|
102
|
-
//
|
|
101
|
+
// merge passes on the parents that emitted in an update: only b$, then only
|
|
102
|
+
// a$, never the value a$ was given while nothing observed it.
|
|
103
103
|
assert.deepStrictEqual(mut_merged, [1, 6]);
|
|
104
104
|
|
|
105
105
|
assert.deepStrictEqual(mut_derived, [
|
|
@@ -4,7 +4,12 @@ import type { FixedLengthTuple } from 'ts-type-forge';
|
|
|
4
4
|
import { createState } from '../../utils/index.mjs';
|
|
5
5
|
import { combine } from '../combine/index.mjs';
|
|
6
6
|
import { fromSubscribable, source } from '../create/index.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
map,
|
|
9
|
+
switchMap,
|
|
10
|
+
throttle,
|
|
11
|
+
withBufferedFrom,
|
|
12
|
+
} from '../operators/index.mjs';
|
|
8
13
|
import { writeDuringPropagationWarning } from './root-observable-class.mjs';
|
|
9
14
|
|
|
10
15
|
describe('a write to a root observable while an update is propagating (#2336)', () => {
|
|
@@ -156,7 +161,8 @@ describe('a write to a root observable while an update is propagating (#2336)',
|
|
|
156
161
|
|
|
157
162
|
assert.deepStrictEqual(warn, []);
|
|
158
163
|
|
|
159
|
-
|
|
164
|
+
// 0 from the value `outer$` held when `switchMap` was created.
|
|
165
|
+
assert.deepStrictEqual(mut_history, [0, 10]);
|
|
160
166
|
});
|
|
161
167
|
|
|
162
168
|
test('does not warn when a root created during propagation emits while it is created', async () => {
|
|
@@ -188,7 +194,8 @@ describe('a write to a root observable while an update is propagating (#2336)',
|
|
|
188
194
|
|
|
189
195
|
assert.deepStrictEqual(warn, []);
|
|
190
196
|
|
|
191
|
-
|
|
197
|
+
// 0 from the value `outer$` held when `switchMap` was created.
|
|
198
|
+
assert.deepStrictEqual(mut_history, [0, 10]);
|
|
192
199
|
});
|
|
193
200
|
|
|
194
201
|
test('a subscriber that throws does not leave the propagation flag set', async () => {
|
|
@@ -214,12 +221,76 @@ describe('a write to a root observable while an update is propagating (#2336)',
|
|
|
214
221
|
});
|
|
215
222
|
});
|
|
216
223
|
|
|
224
|
+
describe('a stateful operator updates its state before setNext, which a write from a subscriber re-enters', () => {
|
|
225
|
+
test('withBufferedFrom keeps a value written to the buffered observable for the next batch', async () => {
|
|
226
|
+
const [data$, setData] = createState('a');
|
|
227
|
+
|
|
228
|
+
const [trigger$, setTrigger] = createState(0);
|
|
229
|
+
|
|
230
|
+
const result$ = trigger$.pipe(withBufferedFrom(data$));
|
|
231
|
+
|
|
232
|
+
const mut_history: (readonly [number, readonly string[]])[] = [];
|
|
233
|
+
|
|
234
|
+
result$.subscribe((v) => {
|
|
235
|
+
mut_history.push(v);
|
|
236
|
+
|
|
237
|
+
if (v[0] === 1) {
|
|
238
|
+
setData('b');
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await captureWarnings(() => {
|
|
243
|
+
setTrigger(1);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
setTrigger(2);
|
|
247
|
+
|
|
248
|
+
assert.deepStrictEqual(mut_history, [
|
|
249
|
+
[0, ['a']],
|
|
250
|
+
[1, []],
|
|
251
|
+
[2, ['b']],
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('throttle drops a value a subscriber writes to the parent inside the window, and keeps one timer', async () => {
|
|
256
|
+
vi.useFakeTimers();
|
|
257
|
+
|
|
258
|
+
onTestFinished(() => {
|
|
259
|
+
vi.useRealTimers();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const [src$, setSrc] = createState(0);
|
|
263
|
+
|
|
264
|
+
const throttled$ = src$.pipe(throttle(1000));
|
|
265
|
+
|
|
266
|
+
const mut_history: number[] = [];
|
|
267
|
+
|
|
268
|
+
throttled$.subscribe((v) => {
|
|
269
|
+
mut_history.push(v);
|
|
270
|
+
|
|
271
|
+
if (v === 1) {
|
|
272
|
+
setSrc(2);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await captureWarnings(() => {
|
|
277
|
+
setSrc(1);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
assert.deepStrictEqual(mut_history, [0, 1]);
|
|
281
|
+
|
|
282
|
+
throttled$.complete();
|
|
283
|
+
|
|
284
|
+
assert.strictEqual(vi.getTimerCount(), 0);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
217
288
|
/**
|
|
218
289
|
* Runs `run` with `console.warn` replaced by a mock, and returns the messages
|
|
219
290
|
* it was called with.
|
|
220
291
|
*/
|
|
221
292
|
const captureWarnings = async (
|
|
222
|
-
run: () => Promise<void> |
|
|
293
|
+
run: () => Promise<void> | undefined,
|
|
223
294
|
): Promise<readonly unknown[]> => {
|
|
224
295
|
const warn = vi.fn();
|
|
225
296
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// Sumi bans classes, `this`, method syntax and writes to `this` outside a
|
|
2
|
-
// `mut_` binding; an observable is a class (`ObservableBaseClass` says why),
|
|
3
|
-
//
|
|
2
|
+
// `mut_` binding; an observable is a class (`ObservableBaseClass` says why), so
|
|
3
|
+
// those 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
|
|
7
7
|
// @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
|
|
8
|
-
import {
|
|
8
|
+
import { Optional } from 'ts-data-forge';
|
|
9
|
+
import * as Arr from 'ts-data-forge/array';
|
|
9
10
|
import type { NonEmptyTuple } from 'ts-type-forge';
|
|
10
11
|
import { SyncChildObservableClass } from '../class/index.mjs';
|
|
11
12
|
import type {
|
|
@@ -20,14 +21,13 @@ import type {
|
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Derives one value from the latest values of several observables: one
|
|
23
|
-
* observable that calls `project` with them, as arguments, and emits the
|
|
24
|
-
*
|
|
24
|
+
* observable that calls `project` with them, as arguments, and emits the result
|
|
25
|
+
* unless it equals the one already held.
|
|
25
26
|
*
|
|
26
|
-
* It emits once every parent has a value, and then whenever any of them
|
|
27
|
-
*
|
|
28
|
-
* observables and a tuple per update, `derive` is one, and passes the values
|
|
29
|
-
*
|
|
30
|
-
* again).
|
|
27
|
+
* It emits once every parent has a value, and then whenever any of them emits,
|
|
28
|
+
* as `combine` does; but where `combine(parents).pipe(map(fn))` is two
|
|
29
|
+
* observables and a tuple per update, `derive` is one, and passes the values as
|
|
30
|
+
* arguments (a rest parameter in `project` collects them into an array again).
|
|
31
31
|
* The comparison is `Object.is` unless `equals` says otherwise.
|
|
32
32
|
*
|
|
33
33
|
* @template OS - Tuple type of the parent observables
|
|
@@ -83,8 +83,8 @@ export const derive = <const OS extends NonEmptyTuple<Observable<unknown>>, B>(
|
|
|
83
83
|
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
84
84
|
new DeriveObservableClass(
|
|
85
85
|
parents,
|
|
86
|
-
// `project` is called with the values of `parents`, position by
|
|
87
|
-
//
|
|
86
|
+
// `project` is called with the values of `parents`, position by position,
|
|
87
|
+
// which the checker cannot follow through an index.
|
|
88
88
|
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
89
89
|
project as (...values: readonly unknown[]) => B,
|
|
90
90
|
options?.equals ?? Object.is,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Sumi bans classes, `this`, method syntax and writes to `this` outside a
|
|
2
|
-
// `mut_` binding; an observable is a class (`ObservableBaseClass` says why),
|
|
3
|
-
//
|
|
2
|
+
// `mut_` binding; an observable is a class (`ObservableBaseClass` says why), so
|
|
3
|
+
// those 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
|
package/src/core/combine/zip.mts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// Sumi bans classes, `this`, method syntax and writes to `this` outside a
|
|
2
|
-
// `mut_` binding; an observable is a class (`ObservableBaseClass` says why),
|
|
3
|
-
//
|
|
2
|
+
// `mut_` binding; an observable is a class (`ObservableBaseClass` says why), so
|
|
3
|
+
// those 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
|
|
7
7
|
// @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
|
|
8
|
-
import {
|
|
8
|
+
import { Optional, createQueue, expectType } from 'ts-data-forge';
|
|
9
|
+
import * as Arr from 'ts-data-forge/array';
|
|
9
10
|
import type { NonEmptyTuple, Tuple } from 'ts-type-forge';
|
|
10
11
|
import { SyncChildObservableClass } from '../class/index.mjs';
|
|
11
12
|
import { source } from '../create/index.mjs';
|
|
@@ -26,9 +27,9 @@ import type {
|
|
|
26
27
|
/**
|
|
27
28
|
* Combines multiple observables by pairing their emissions by index.
|
|
28
29
|
* Waits for all sources to emit their nth value before emitting the nth tuple.
|
|
29
|
-
* Completes once a completed source has no value left to pair, as RxJS's
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* Completes once a completed source has no value left to pair, as RxJS's `zip`
|
|
31
|
+
* does: at once if it completes with nothing queued, otherwise after the tuple
|
|
32
|
+
* that takes its last queued value.
|
|
32
33
|
*
|
|
33
34
|
* @template OS - Tuple type of source observables
|
|
34
35
|
* @param parents - Array of observables to zip
|
|
@@ -150,14 +151,16 @@ class ZipObservableClass<const A extends NonEmptyUnknownList>
|
|
|
150
151
|
|
|
151
152
|
this.setNext(nextValue, updateToken);
|
|
152
153
|
|
|
154
|
+
// After the update has reached what is derived from it, as in `take`
|
|
155
|
+
// (#2371).
|
|
153
156
|
if (this.canEmitNoMore()) {
|
|
154
|
-
this.
|
|
157
|
+
this.completeAfterPropagation();
|
|
155
158
|
}
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
/**
|
|
159
|
-
* No tuple can be completed any more once a completed source has nothing
|
|
160
|
-
*
|
|
162
|
+
* No tuple can be completed any more once a completed source has nothing left
|
|
163
|
+
* to pair (#2343).
|
|
161
164
|
*/
|
|
162
165
|
override canEmitNoMore(): boolean {
|
|
163
166
|
return this.parents.some(
|