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.
Files changed (151) hide show
  1. package/README.md +4 -4
  2. package/dist/core/authoring.d.mts +3 -3
  3. package/dist/core/class/child-observable-class.d.mts +24 -25
  4. package/dist/core/class/child-observable-class.d.mts.map +1 -1
  5. package/dist/core/class/child-observable-class.mjs +41 -42
  6. package/dist/core/class/child-observable-class.mjs.map +1 -1
  7. package/dist/core/class/create-child-observable.d.mts +27 -29
  8. package/dist/core/class/create-child-observable.d.mts.map +1 -1
  9. package/dist/core/class/create-child-observable.mjs.map +1 -1
  10. package/dist/core/class/create-root-observable.d.mts +11 -11
  11. package/dist/core/class/manager-observable-class.d.mts +6 -6
  12. package/dist/core/class/manager-observable-class.d.mts.map +1 -1
  13. package/dist/core/class/manager-observable-class.mjs +6 -0
  14. package/dist/core/class/manager-observable-class.mjs.map +1 -1
  15. package/dist/core/class/observable-base-class.d.mts +32 -26
  16. package/dist/core/class/observable-base-class.d.mts.map +1 -1
  17. package/dist/core/class/observable-base-class.mjs +16 -3
  18. package/dist/core/class/observable-base-class.mjs.map +1 -1
  19. package/dist/core/class/observable-node.d.mts +3 -4
  20. package/dist/core/class/observable-node.d.mts.map +1 -1
  21. package/dist/core/class/observable-node.mjs +0 -1
  22. package/dist/core/class/observable-node.mjs.map +1 -1
  23. package/dist/core/class/propagation-state.d.mts +10 -9
  24. package/dist/core/class/propagation-state.d.mts.map +1 -1
  25. package/dist/core/class/propagation-state.mjs +4 -2
  26. package/dist/core/class/propagation-state.mjs.map +1 -1
  27. package/dist/core/class/root-observable-class.d.mts +4 -4
  28. package/dist/core/combine/derive.d.mts +7 -8
  29. package/dist/core/combine/derive.d.mts.map +1 -1
  30. package/dist/core/combine/derive.mjs +0 -1
  31. package/dist/core/combine/derive.mjs.map +1 -1
  32. package/dist/core/combine/merge.mjs.map +1 -1
  33. package/dist/core/combine/zip.d.mts +3 -3
  34. package/dist/core/combine/zip.mjs.map +1 -1
  35. package/dist/core/create/counter.d.mts +3 -3
  36. package/dist/core/create/counter.mjs.map +1 -1
  37. package/dist/core/create/from-abortable-promise.mjs.map +1 -1
  38. package/dist/core/create/from-promise.mjs.map +1 -1
  39. package/dist/core/create/from-subscribable.mjs.map +1 -1
  40. package/dist/core/create/source.mjs.map +1 -1
  41. package/dist/core/create/timer.mjs.map +1 -1
  42. package/dist/core/operators/audit.mjs.map +1 -1
  43. package/dist/core/operators/debounce.mjs.map +1 -1
  44. package/dist/core/operators/filter.mjs.map +1 -1
  45. package/dist/core/operators/keep-equal-parts.d.mts +14 -15
  46. package/dist/core/operators/keep-equal-parts.d.mts.map +1 -1
  47. package/dist/core/operators/keep-equal-parts.mjs +0 -1
  48. package/dist/core/operators/keep-equal-parts.mjs.map +1 -1
  49. package/dist/core/operators/map.mjs.map +1 -1
  50. package/dist/core/operators/merge-map.d.mts +3 -3
  51. package/dist/core/operators/merge-map.mjs.map +1 -1
  52. package/dist/core/operators/pairwise.mjs.map +1 -1
  53. package/dist/core/operators/scan.mjs.map +1 -1
  54. package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
  55. package/dist/core/operators/skip-until.mjs.map +1 -1
  56. package/dist/core/operators/skip-while.mjs.map +1 -1
  57. package/dist/core/operators/switch-map.mjs.map +1 -1
  58. package/dist/core/operators/take-until.mjs.map +1 -1
  59. package/dist/core/operators/take-while.mjs.map +1 -1
  60. package/dist/core/operators/take.mjs.map +1 -1
  61. package/dist/core/operators/throttle.mjs.map +1 -1
  62. package/dist/core/operators/with-buffered-from.mjs.map +1 -1
  63. package/dist/core/operators/with-current-value-from.mjs.map +1 -1
  64. package/dist/core/operators/with-initial-value.mjs.map +1 -1
  65. package/dist/core/predefined/operators/attach-index.mjs.map +1 -1
  66. package/dist/core/predefined/operators/map-to.d.mts +3 -4
  67. package/dist/core/predefined/operators/map-to.d.mts.map +1 -1
  68. package/dist/core/predefined/operators/map-to.mjs +0 -1
  69. package/dist/core/predefined/operators/map-to.mjs.map +1 -1
  70. package/dist/core/scope/scope.d.mts +4 -4
  71. package/dist/core/types/id.d.mts +2 -1
  72. package/dist/core/types/id.d.mts.map +1 -1
  73. package/dist/core/types/observable-family.d.mts.map +1 -1
  74. package/dist/core/types/observable-family.mjs.map +1 -1
  75. package/dist/core/types/observable.d.mts +7 -6
  76. package/dist/core/types/observable.d.mts.map +1 -1
  77. package/dist/core/types/observable.mjs.map +1 -1
  78. package/dist/core/utils/current-scope.d.mts +6 -6
  79. package/dist/core/utils/id-maker.d.mts +7 -0
  80. package/dist/core/utils/id-maker.d.mts.map +1 -1
  81. package/dist/core/utils/id-maker.mjs +9 -0
  82. package/dist/core/utils/id-maker.mjs.map +1 -1
  83. package/dist/core/utils/observable-brand.d.mts +5 -5
  84. package/dist/utils/create-boolean-state.d.mts +2 -2
  85. package/dist/utils/create-reducer.d.mts +20 -20
  86. package/dist/utils/create-reducer.d.mts.map +1 -1
  87. package/dist/utils/create-reducer.mjs +4 -16
  88. package/dist/utils/create-reducer.mjs.map +1 -1
  89. package/dist/utils/create-state.d.mts +3 -3
  90. package/dist/utils/create-state.d.mts.map +1 -1
  91. package/dist/utils/create-state.mjs +10 -13
  92. package/dist/utils/create-state.mjs.map +1 -1
  93. package/dist/utils/state-source.d.mts +19 -0
  94. package/dist/utils/state-source.d.mts.map +1 -0
  95. package/dist/utils/state-source.mjs +31 -0
  96. package/dist/utils/state-source.mjs.map +1 -0
  97. package/package.json +10 -10
  98. package/src/core/authoring.mts +3 -3
  99. package/src/core/class/child-observable-class.mts +86 -87
  100. package/src/core/class/circular-dependency-comparison.test.mts +2 -2
  101. package/src/core/class/create-child-observable.mts +27 -29
  102. package/src/core/class/create-root-observable.mts +11 -11
  103. package/src/core/class/manager-observable-class.mts +16 -9
  104. package/src/core/class/observable-base-class.mts +48 -30
  105. package/src/core/class/observable-node.mts +3 -4
  106. package/src/core/class/propagation-state.mts +31 -29
  107. package/src/core/class/remove-completed.test.mts +2 -2
  108. package/src/core/class/root-observable-class.mts +9 -9
  109. package/src/core/class/unobserved-write.test.mts +194 -0
  110. package/src/core/combine/derive.mts +10 -11
  111. package/src/core/combine/merge.mts +2 -2
  112. package/src/core/combine/zip.mts +7 -7
  113. package/src/core/create/counter.mts +5 -5
  114. package/src/core/create/from-abortable-promise.mts +2 -2
  115. package/src/core/create/from-promise.mts +2 -2
  116. package/src/core/create/from-subscribable.mts +2 -2
  117. package/src/core/create/source.mts +5 -5
  118. package/src/core/create/timer.mts +2 -2
  119. package/src/core/operators/audit.mts +2 -2
  120. package/src/core/operators/debounce.mts +2 -2
  121. package/src/core/operators/filter.mts +4 -4
  122. package/src/core/operators/keep-equal-parts.mts +16 -17
  123. package/src/core/operators/map.mts +2 -2
  124. package/src/core/operators/merge-map.mts +5 -5
  125. package/src/core/operators/pairwise.mts +4 -4
  126. package/src/core/operators/scan.mts +2 -2
  127. package/src/core/operators/skip-if-no-change.mts +4 -4
  128. package/src/core/operators/skip-until.mts +4 -4
  129. package/src/core/operators/skip-while.mts +2 -2
  130. package/src/core/operators/switch-map.mts +5 -5
  131. package/src/core/operators/take-until.mts +2 -2
  132. package/src/core/operators/take-while.mts +2 -2
  133. package/src/core/operators/take.mts +2 -2
  134. package/src/core/operators/throttle.mts +2 -2
  135. package/src/core/operators/with-buffered-from.mts +2 -2
  136. package/src/core/operators/with-current-value-from.mts +2 -2
  137. package/src/core/operators/with-initial-value.mts +2 -2
  138. package/src/core/predefined/operators/attach-index.mts +2 -2
  139. package/src/core/predefined/operators/map-to.mts +3 -4
  140. package/src/core/scope/scope.mts +6 -6
  141. package/src/core/types/id.mts +2 -1
  142. package/src/core/types/observable-family.mts +4 -5
  143. package/src/core/types/observable.mts +9 -8
  144. package/src/core/utils/current-scope.mts +8 -8
  145. package/src/core/utils/id-maker.mts +10 -0
  146. package/src/core/utils/observable-brand.mts +5 -5
  147. package/src/utils/create-boolean-state.mts +2 -2
  148. package/src/utils/create-reducer.mts +27 -42
  149. package/src/utils/create-state.mts +14 -31
  150. package/src/utils/create-state.test.mts +20 -2
  151. 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
- // `ObservableBaseClass` below), so those four rules are waived here (D-63).
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
- * be cheap, and most of its cost was its methods: each observable used to be
39
- * a set of closures made for it, a dozen or more per node (#2421). Sharing
40
- * the methods among all nodes removes that cost, and a shared method has to
41
- * read the node it is called on from `this`. Once `this` is unavoidable, a
42
- * class is the plainest way to say it: the methods live once on the
43
- * prototype, `this` is typed, a creator or operator is a subclass that
44
- * overrides `tryUpdate` (and `flush` / `teardown` where it holds something),
45
- * and assigning every field in the constructor gives the nodes of one class
46
- * one shape for V8. The collections (`children`, subscribers) are made when
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
- * fields, never through a `#private` one: the other node may be an instance
54
- * of another copy of synstate in the same program, whose classes are not
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
- // be O(1) for a UI that creates and completes derived nodes on every mount.
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
- * update `updateToken`, and notifies subscribers. The children are queued
162
- * first, so that a child a subscriber creates, which starts from this
163
- * value, is not visited for it again.
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
- * parents: an upstream observable may be shared, and only whoever owns it
205
- * decides when it ends (#2133).
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
- * runs its teardown, and takes it out of the graph.
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
- * `complete()` and never in `dispose()` (`debounce`'s pending value).
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
- * observable is built by a synstate class, and the public types carry a
43
- * brand an object literal cannot supply, so this holds for every value of
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 `startUpdate` calls are running, across every observable, and
22
- * the token of the outermost one.
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
- * `startUpdate` has reached every descendant.
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
- * to while one is, is written to from inside a subscriber or a projection
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
- /** Brackets one `startUpdate`; see {@link endPropagation}. */
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 `startUpdate`, and runs the deferred work once the outermost 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
- * bucket per depth below the node that started the update, holding the
92
- * queued nodes and how many of its slots are in use. Kept from update to
93
- * update, so that an update allocates nothing but its token; emptying a
94
- * bucket is setting its length to 0.
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
- * a root while the outer one is propagating; a chain of such writes this deep
126
- * is a loop that would otherwise run until the stack overflows.
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
- * not overwrite the mark of an update that is still open — one it is nested
174
- * in, or one of another copy of synstate — and remembers the nodes it
175
- * queues despite such a mark in a set of its own instead.
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
- // it yet: leave its mark alone.
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
- // a `range` built at the start would miss.
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
- // not grow while it is visited. Its length is kept until the end, so
285
- // that `close` can clear what an exception leaves.
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
- // is under way.
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
- * created while an update propagates (inside a `switchMap` projection,
21
- * say) may emit while it is created, as `fromSubscribable` does for a
22
- * synchronous source. Nothing is derived from it yet, so that is its
23
- * initial value rather than a write (#2336).
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
- * from a subscriber or a projection. The write still propagates immediately,
69
- * as it always has; the warning is there because the pattern is wrong, not
70
- * because anything was refused.
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
- // so those rules are waived here (D-63).
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
- * result unless it equals the one already held.
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
- * emits, 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
29
- * as arguments (a rest parameter in `project` collects them into an array
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
- // position, which the checker cannot follow through an index.
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
- // so those rules are waived here (D-63).
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
- // so those rules are waived here (D-63).
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
- * `zip` does: at once if it completes with nothing queued, otherwise after
31
- * the tuple that takes its last queued value.
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
- * left to pair (#2343).
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
- // so those rules are waived here (D-63).
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
- * unless `startManually` is set), then 1, 2, 3, ... every interval. A
17
- * subscriber added after that 0 has been emitted does not receive it.
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
- // so those rules are waived here (D-63).
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
- // so those rules are waived here (D-63).
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
- // so those rules are waived here (D-63).
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