synstate 6.1.0 → 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 +4 -4
- 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.d.mts.map +1 -1
- package/dist/core/class/manager-observable-class.mjs +6 -0
- package/dist/core/class/manager-observable-class.mjs.map +1 -1
- package/dist/core/class/observable-base-class.d.mts +32 -26
- package/dist/core/class/observable-base-class.d.mts.map +1 -1
- package/dist/core/class/observable-base-class.mjs +16 -3
- 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 +10 -9
- package/dist/core/class/propagation-state.d.mts.map +1 -1
- package/dist/core/class/propagation-state.mjs +4 -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/id.d.mts +2 -1
- package/dist/core/types/id.d.mts.map +1 -1
- 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 +7 -6
- package/dist/core/types/observable.d.mts.map +1 -1
- 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 +7 -0
- package/dist/core/utils/id-maker.d.mts.map +1 -1
- package/dist/core/utils/id-maker.mjs +9 -0
- package/dist/core/utils/id-maker.mjs.map +1 -1
- 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 +20 -20
- package/dist/utils/create-reducer.d.mts.map +1 -1
- package/dist/utils/create-reducer.mjs +4 -16
- package/dist/utils/create-reducer.mjs.map +1 -1
- package/dist/utils/create-state.d.mts +3 -3
- package/dist/utils/create-state.d.mts.map +1 -1
- package/dist/utils/create-state.mjs +10 -13
- package/dist/utils/create-state.mjs.map +1 -1
- package/dist/utils/state-source.d.mts +19 -0
- package/dist/utils/state-source.d.mts.map +1 -0
- package/dist/utils/state-source.mjs +31 -0
- package/dist/utils/state-source.mjs.map +1 -0
- package/package.json +10 -10
- 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 +16 -9
- package/src/core/class/observable-base-class.mts +48 -30
- package/src/core/class/observable-node.mts +3 -4
- package/src/core/class/propagation-state.mts +31 -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 +194 -0
- 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/id.mts +2 -1
- package/src/core/types/observable-family.mts +4 -5
- package/src/core/types/observable.mts +9 -8
- package/src/core/utils/current-scope.mts +8 -8
- package/src/core/utils/id-maker.mts +10 -0
- package/src/core/utils/observable-brand.mts +5 -5
- package/src/utils/create-boolean-state.mts +2 -2
- package/src/utils/create-reducer.mts +27 -42
- package/src/utils/create-state.mts +14 -31
- package/src/utils/create-state.test.mts +20 -2
- package/src/utils/state-source.mts +48 -0
|
@@ -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
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
} from '../types/index.mjs';
|
|
24
24
|
import {
|
|
25
25
|
getCurrentScopeInternals,
|
|
26
|
+
issueUnqueuedToken,
|
|
26
27
|
notUpdatedYet,
|
|
27
28
|
observableBrand,
|
|
28
29
|
toSubscriber,
|
|
@@ -34,25 +35,23 @@ import { enqueueChildren } from './propagation-state.mjs';
|
|
|
34
35
|
/**
|
|
35
36
|
* The base of every observable, and the reason an observable is a class.
|
|
36
37
|
*
|
|
37
|
-
* A node is created for every derived value of a UI, so creating one has to
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* 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.
|
|
48
48
|
*
|
|
49
49
|
* The methods read the observable they are called on, so taking one off it
|
|
50
50
|
* (`const { subscribe } = obs$`) is not supported.
|
|
51
51
|
*
|
|
52
|
-
* A node reaches another node only through its methods and its public
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* 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.
|
|
56
55
|
*/
|
|
57
56
|
export class ObservableBaseClass<
|
|
58
57
|
A,
|
|
@@ -143,8 +142,8 @@ export class ObservableBaseClass<
|
|
|
143
142
|
}
|
|
144
143
|
|
|
145
144
|
addChild(child: ChildNode): void {
|
|
146
|
-
// A set, not an array: a completed child removes itself, and that has to
|
|
147
|
-
//
|
|
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.
|
|
148
147
|
if (this.#mut_children === undefined) {
|
|
149
148
|
this.#mut_children = new Set<ChildNode>([child]);
|
|
150
149
|
} else {
|
|
@@ -157,10 +156,10 @@ export class ObservableBaseClass<
|
|
|
157
156
|
}
|
|
158
157
|
|
|
159
158
|
/**
|
|
160
|
-
* Stores `nextValue` as the current value, queues the children for the
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
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.
|
|
164
163
|
*/
|
|
165
164
|
protected setNext(nextValue: A, updateToken: UpdateToken): void {
|
|
166
165
|
this.#mut_updateToken = updateToken;
|
|
@@ -178,6 +177,25 @@ export class ObservableBaseClass<
|
|
|
178
177
|
}
|
|
179
178
|
}
|
|
180
179
|
|
|
180
|
+
/**
|
|
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.
|
|
186
|
+
*/
|
|
187
|
+
protected setNextIfUnobserved(nextValue: A): boolean {
|
|
188
|
+
if (this.hasChild || this.hasSubscriber) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.#mut_updateToken = issueUnqueuedToken();
|
|
193
|
+
|
|
194
|
+
this.#mut_snapshot = Optional.some(nextValue);
|
|
195
|
+
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
181
199
|
/**
|
|
182
200
|
* Called with the token of every update that reaches this observable; a
|
|
183
201
|
* parent that did not emit in that update has a different `updateToken`.
|
|
@@ -200,9 +218,9 @@ export class ObservableBaseClass<
|
|
|
200
218
|
|
|
201
219
|
/**
|
|
202
220
|
* Completes the observable: an async child first passes on what it still
|
|
203
|
-
* holds (`flush`), then it is disposed. Completing never reaches the
|
|
204
|
-
*
|
|
205
|
-
*
|
|
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).
|
|
206
224
|
*/
|
|
207
225
|
complete(): void {
|
|
208
226
|
if (this.#mut_isCompleted) {
|
|
@@ -215,8 +233,8 @@ export class ObservableBaseClass<
|
|
|
215
233
|
}
|
|
216
234
|
|
|
217
235
|
/**
|
|
218
|
-
* Completes the observable without passing on anything it still holds,
|
|
219
|
-
*
|
|
236
|
+
* Completes the observable without passing on anything it still holds, runs
|
|
237
|
+
* its teardown, and takes it out of the graph.
|
|
220
238
|
*/
|
|
221
239
|
dispose(): void {
|
|
222
240
|
// A completed node is reached again through every parent that completes
|
|
@@ -255,8 +273,8 @@ export class ObservableBaseClass<
|
|
|
255
273
|
}
|
|
256
274
|
|
|
257
275
|
/**
|
|
258
|
-
* Passes on what the observable still holds, at the beginning of
|
|
259
|
-
*
|
|
276
|
+
* Passes on what the observable still holds, at the beginning of `complete()`
|
|
277
|
+
* and never in `dispose()` (`debounce`'s pending value).
|
|
260
278
|
*/
|
|
261
279
|
// eslint-disable-next-line @typescript-eslint/class-methods-use-this
|
|
262
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
|
|
@@ -18,8 +18,9 @@ import type { UpdateToken } from '../types/index.mjs';
|
|
|
18
18
|
import type { ChildNode } from './observable-node.mjs';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* How many
|
|
22
|
-
* the
|
|
21
|
+
* How many updates are propagating, across every observable, and the token of
|
|
22
|
+
* the outermost one. A write nothing observes opens none
|
|
23
|
+
* (`setNextIfUnobserved`).
|
|
23
24
|
*/
|
|
24
25
|
const mut_propagation: {
|
|
25
26
|
depth: number;
|
|
@@ -27,15 +28,14 @@ const mut_propagation: {
|
|
|
27
28
|
} = { depth: 0, outermostToken: undefined };
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
* Work deferred by {@link runAfterPropagation}, run once the outermost
|
|
31
|
-
*
|
|
31
|
+
* Work deferred by {@link runAfterPropagation}, run once the outermost update
|
|
32
|
+
* has reached every descendant.
|
|
32
33
|
*/
|
|
33
34
|
const mut_afterPropagation: (() => void)[] = [];
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
|
-
* Whether some observable's update is propagating right now. A root written
|
|
37
|
-
*
|
|
38
|
-
* (#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).
|
|
39
39
|
*/
|
|
40
40
|
export const isPropagating = (): boolean => mut_propagation.depth > 0;
|
|
41
41
|
|
|
@@ -57,7 +57,10 @@ export const runAfterPropagation = (task: () => void): void => {
|
|
|
57
57
|
}
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Brackets one update a `startUpdate` opens (a write nothing observes opens
|
|
62
|
+
* none); see {@link endPropagation}.
|
|
63
|
+
*/
|
|
61
64
|
export const beginPropagation = (updateToken: UpdateToken): void => {
|
|
62
65
|
if (mut_propagation.depth === 0) {
|
|
63
66
|
mut_propagation.outermostToken = updateToken;
|
|
@@ -67,8 +70,7 @@ export const beginPropagation = (updateToken: UpdateToken): void => {
|
|
|
67
70
|
};
|
|
68
71
|
|
|
69
72
|
/**
|
|
70
|
-
* Ends one
|
|
71
|
-
* has ended.
|
|
73
|
+
* Ends one update, and runs the deferred work once the outermost one has ended.
|
|
72
74
|
*/
|
|
73
75
|
export const endPropagation = (): void => {
|
|
74
76
|
mut_propagation.depth -= 1;
|
|
@@ -87,11 +89,11 @@ export const endPropagation = (): void => {
|
|
|
87
89
|
};
|
|
88
90
|
|
|
89
91
|
/**
|
|
90
|
-
* The buckets of this copy's open updates, one set per nesting level: a
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* update
|
|
94
|
-
*
|
|
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.
|
|
95
97
|
*/
|
|
96
98
|
const mut_bucketSets: Bucket[][] = [];
|
|
97
99
|
|
|
@@ -121,9 +123,9 @@ export const openUpdate = (depth: number): UpdateToken => {
|
|
|
121
123
|
|
|
122
124
|
/**
|
|
123
125
|
* How many updates of one copy may be open at once, one inside another. An
|
|
124
|
-
* update opens inside another only when a subscriber or projection writes to
|
|
125
|
-
*
|
|
126
|
-
*
|
|
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.
|
|
127
129
|
*/
|
|
128
130
|
export const maxUpdateNesting = 100;
|
|
129
131
|
|
|
@@ -169,10 +171,10 @@ type Bucket = {
|
|
|
169
171
|
*
|
|
170
172
|
* A queued node is marked with the token of the update that queued it
|
|
171
173
|
* (`queuedFor`), which is how an update queues a node once. The mark is not
|
|
172
|
-
* cleared after the visit: a later update has another token. An update does
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
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.
|
|
176
178
|
*/
|
|
177
179
|
class UpdateQueue {
|
|
178
180
|
readonly #base: number;
|
|
@@ -248,8 +250,8 @@ class UpdateQueue {
|
|
|
248
250
|
}
|
|
249
251
|
|
|
250
252
|
if (mark !== undefined && toQueue(mark).isOpen) {
|
|
251
|
-
// Another open update has queued `child` and may not have visited
|
|
252
|
-
//
|
|
253
|
+
// Another open update has queued `child` and may not have visited it
|
|
254
|
+
// yet: leave its mark alone.
|
|
253
255
|
if (this.#mut_queuedDespiteMark === undefined) {
|
|
254
256
|
this.#mut_queuedDespiteMark = new Set<ChildNode>();
|
|
255
257
|
} else if (this.#mut_queuedDespiteMark.has(child)) {
|
|
@@ -270,8 +272,8 @@ class UpdateQueue {
|
|
|
270
272
|
|
|
271
273
|
const mut_buckets = this.#mut_buckets;
|
|
272
274
|
|
|
273
|
-
// `size` grows while the buckets are visited, always past `index`, which
|
|
274
|
-
//
|
|
275
|
+
// `size` grows while the buckets are visited, always past `index`, which a
|
|
276
|
+
// `range` built at the start would miss.
|
|
275
277
|
// eslint-disable-next-line ts-data-forge/prefer-range-for-loop
|
|
276
278
|
for (let mut_index = 0; mut_index < this.#mut_size; mut_index += 1) {
|
|
277
279
|
const mut_bucket = mut_buckets[mut_index];
|
|
@@ -280,9 +282,9 @@ class UpdateQueue {
|
|
|
280
282
|
continue;
|
|
281
283
|
}
|
|
282
284
|
|
|
283
|
-
// A node visited here queues only deeper nodes, so this bucket does
|
|
284
|
-
//
|
|
285
|
-
//
|
|
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.
|
|
286
288
|
const { nodes: mut_nodes, length } = mut_bucket;
|
|
287
289
|
|
|
288
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.';
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { FixedLengthTuple } from 'ts-type-forge';
|
|
2
|
+
import { createState } from '../../utils/index.mjs';
|
|
3
|
+
import { derive, merge } from '../combine/index.mjs';
|
|
4
|
+
import { source } from '../create/index.mjs';
|
|
5
|
+
import { debounce, map, switchMap } from '../operators/index.mjs';
|
|
6
|
+
import { writeDuringPropagationWarning } from './root-observable-class.mjs';
|
|
7
|
+
|
|
8
|
+
// A write to an observable with no child and no subscriber stores the value
|
|
9
|
+
// without opening an update (`ManagerObservableClass.startUpdate`). These pin
|
|
10
|
+
// what that must not change.
|
|
11
|
+
describe('a write nothing observes', () => {
|
|
12
|
+
test('is held, and a later subscriber receives the last one', () => {
|
|
13
|
+
const s$ = source<number>(0);
|
|
14
|
+
|
|
15
|
+
s$.next(1);
|
|
16
|
+
|
|
17
|
+
s$.next(2);
|
|
18
|
+
|
|
19
|
+
const mut_history: number[] = [];
|
|
20
|
+
|
|
21
|
+
s$.subscribe((v) => {
|
|
22
|
+
mut_history.push(v);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
assert.deepStrictEqual(mut_history, [2]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('gets a token of its own each time', () => {
|
|
29
|
+
const s$ = source<number>(0);
|
|
30
|
+
|
|
31
|
+
const before = s$.updateToken;
|
|
32
|
+
|
|
33
|
+
s$.next(1);
|
|
34
|
+
|
|
35
|
+
const first = s$.updateToken;
|
|
36
|
+
|
|
37
|
+
s$.next(2);
|
|
38
|
+
|
|
39
|
+
assert.notStrictEqual(first, before);
|
|
40
|
+
|
|
41
|
+
assert.notStrictEqual(s$.updateToken, first);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('is held after the last subscriber leaves', () => {
|
|
45
|
+
const [s$, setS] = createState(0);
|
|
46
|
+
|
|
47
|
+
const subscription = s$.subscribe(() => {});
|
|
48
|
+
|
|
49
|
+
subscription.unsubscribe();
|
|
50
|
+
|
|
51
|
+
assert.strictEqual(setS(5), 5);
|
|
52
|
+
|
|
53
|
+
const mut_history: number[] = [];
|
|
54
|
+
|
|
55
|
+
s$.subscribe((v) => {
|
|
56
|
+
mut_history.push(v);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
assert.deepStrictEqual(mut_history, [5]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('is where a child created later starts, and later writes reach it', () => {
|
|
63
|
+
const s$ = source<number>(0);
|
|
64
|
+
|
|
65
|
+
s$.next(3);
|
|
66
|
+
|
|
67
|
+
const mut_history: number[] = [];
|
|
68
|
+
|
|
69
|
+
s$.pipe(map((x) => x * 10)).subscribe((v) => {
|
|
70
|
+
mut_history.push(v);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
s$.next(4);
|
|
74
|
+
|
|
75
|
+
assert.deepStrictEqual(mut_history, [30, 40]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('is not taken for an emission of an update another parent starts', () => {
|
|
79
|
+
const a$ = source<number>(0);
|
|
80
|
+
|
|
81
|
+
const b$ = source<number>(0);
|
|
82
|
+
|
|
83
|
+
a$.next(5);
|
|
84
|
+
|
|
85
|
+
const mut_merged: number[] = [];
|
|
86
|
+
|
|
87
|
+
merge([a$, b$]).subscribe((v) => {
|
|
88
|
+
mut_merged.push(v);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const mut_derived: FixedLengthTuple<2, number>[] = [];
|
|
92
|
+
|
|
93
|
+
derive([a$, b$], (a, b) => [a, b] as const).subscribe((v) => {
|
|
94
|
+
mut_derived.push(v);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
b$.next(1);
|
|
98
|
+
|
|
99
|
+
a$.next(6);
|
|
100
|
+
|
|
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
|
+
assert.deepStrictEqual(mut_merged, [1, 6]);
|
|
104
|
+
|
|
105
|
+
assert.deepStrictEqual(mut_derived, [
|
|
106
|
+
[5, 0],
|
|
107
|
+
[5, 1],
|
|
108
|
+
[6, 1],
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('by an async child is held, as a root write is', () => {
|
|
113
|
+
vi.useFakeTimers();
|
|
114
|
+
|
|
115
|
+
const s$ = source<number>(0);
|
|
116
|
+
|
|
117
|
+
const debounced$ = s$.pipe(debounce(100));
|
|
118
|
+
|
|
119
|
+
const subscription = debounced$.subscribe(() => {});
|
|
120
|
+
|
|
121
|
+
subscription.unsubscribe();
|
|
122
|
+
|
|
123
|
+
// debounced$ keeps its parent, but has no observer of its own when the
|
|
124
|
+
// timer fires.
|
|
125
|
+
s$.next(1);
|
|
126
|
+
|
|
127
|
+
vi.runAllTimers();
|
|
128
|
+
|
|
129
|
+
const mut_history: number[] = [];
|
|
130
|
+
|
|
131
|
+
debounced$.subscribe((v) => {
|
|
132
|
+
mut_history.push(v);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
s$.next(2);
|
|
136
|
+
|
|
137
|
+
vi.runAllTimers();
|
|
138
|
+
|
|
139
|
+
assert.deepStrictEqual(mut_history, [1, 2]);
|
|
140
|
+
|
|
141
|
+
vi.useRealTimers();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('by a switchMap inner after the switchMap lost its observers is held', () => {
|
|
145
|
+
const selector$ = source<number>(0);
|
|
146
|
+
|
|
147
|
+
const branch$ = source<number>(10);
|
|
148
|
+
|
|
149
|
+
const switched$ = selector$.pipe(switchMap(() => branch$));
|
|
150
|
+
|
|
151
|
+
const subscription = switched$.subscribe(() => {});
|
|
152
|
+
|
|
153
|
+
// switchMap follows an inner once the selector emits.
|
|
154
|
+
selector$.next(1);
|
|
155
|
+
|
|
156
|
+
subscription.unsubscribe();
|
|
157
|
+
|
|
158
|
+
branch$.next(11);
|
|
159
|
+
|
|
160
|
+
const mut_history: number[] = [];
|
|
161
|
+
|
|
162
|
+
switched$.pipe(map((x) => x * 2)).subscribe((v) => {
|
|
163
|
+
mut_history.push(v);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
branch$.next(12);
|
|
167
|
+
|
|
168
|
+
assert.deepStrictEqual(mut_history, [22, 24]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('from a subscriber still warns as a write during propagation', () => {
|
|
172
|
+
const [a$, setA] = createState(0);
|
|
173
|
+
|
|
174
|
+
const [, setB] = createState(0);
|
|
175
|
+
|
|
176
|
+
a$.subscribe((a) => {
|
|
177
|
+
setB(a);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const warn = vi.fn();
|
|
181
|
+
|
|
182
|
+
vi.stubGlobal('console', { ...console, warn });
|
|
183
|
+
|
|
184
|
+
setA(1);
|
|
185
|
+
|
|
186
|
+
// cspell:disable-next-line
|
|
187
|
+
vi.unstubAllGlobals();
|
|
188
|
+
|
|
189
|
+
assert.deepStrictEqual(
|
|
190
|
+
warn.mock.calls.map((args: readonly unknown[]) => args[0]),
|
|
191
|
+
[writeDuringPropagationWarning],
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -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
|
|
@@ -20,14 +20,13 @@ import type {
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Derives one value from the latest values of several observables: one
|
|
23
|
-
* observable that calls `project` with them, as arguments, and emits the
|
|
24
|
-
*
|
|
23
|
+
* observable that calls `project` with them, as arguments, and emits the result
|
|
24
|
+
* unless it equals the one already held.
|
|
25
25
|
*
|
|
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).
|
|
26
|
+
* It emits once every parent has a value, and then whenever any of them emits,
|
|
27
|
+
* as `combine` does; but where `combine(parents).pipe(map(fn))` is two
|
|
28
|
+
* observables and a tuple per update, `derive` is one, and passes the values as
|
|
29
|
+
* arguments (a rest parameter in `project` collects them into an array again).
|
|
31
30
|
* The comparison is `Object.is` unless `equals` says otherwise.
|
|
32
31
|
*
|
|
33
32
|
* @template OS - Tuple type of the parent observables
|
|
@@ -83,8 +82,8 @@ export const derive = <const OS extends NonEmptyTuple<Observable<unknown>>, B>(
|
|
|
83
82
|
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
84
83
|
new DeriveObservableClass(
|
|
85
84
|
parents,
|
|
86
|
-
// `project` is called with the values of `parents`, position by
|
|
87
|
-
//
|
|
85
|
+
// `project` is called with the values of `parents`, position by position,
|
|
86
|
+
// which the checker cannot follow through an index.
|
|
88
87
|
// @sumi-expect-error banned-syntax/no-unsafe-type-assertion
|
|
89
88
|
project as (...values: readonly unknown[]) => B,
|
|
90
89
|
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,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
|
|
@@ -26,9 +26,9 @@ import type {
|
|
|
26
26
|
/**
|
|
27
27
|
* Combines multiple observables by pairing their emissions by index.
|
|
28
28
|
* 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
|
-
*
|
|
29
|
+
* Completes once a completed source has no value left to pair, as RxJS's `zip`
|
|
30
|
+
* does: at once if it completes with nothing queued, otherwise after the tuple
|
|
31
|
+
* that takes its last queued value.
|
|
32
32
|
*
|
|
33
33
|
* @template OS - Tuple type of source observables
|
|
34
34
|
* @param parents - Array of observables to zip
|
|
@@ -156,8 +156,8 @@ class ZipObservableClass<const A extends NonEmptyUnknownList>
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
/**
|
|
159
|
-
* No tuple can be completed any more once a completed source has nothing
|
|
160
|
-
*
|
|
159
|
+
* No tuple can be completed any more once a completed source has nothing left
|
|
160
|
+
* to pair (#2343).
|
|
161
161
|
*/
|
|
162
162
|
override canEmitNoMore(): boolean {
|
|
163
163
|
return this.parents.some(
|
|
@@ -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
|
|
@@ -12,9 +12,9 @@ import type { CounterObservable, TimerId } from '../types/index.mjs';
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Creates an observable that emits incremental numbers at a specified interval.
|
|
15
|
-
* Emits 0 in the macrotask right after `start()` (which runs at creation
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Emits 0 in the macrotask right after `start()` (which runs at creation unless
|
|
16
|
+
* `startManually` is set), then 1, 2, 3, ... every interval. A subscriber added
|
|
17
|
+
* after that 0 has been emitted does not receive it.
|
|
18
18
|
*
|
|
19
19
|
* @param intervalMilliSeconds - The interval duration in milliseconds
|
|
20
20
|
* @param options - Optional settings
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|