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.
Files changed (189) hide show
  1. package/README.md +3 -3
  2. package/dist/core/authoring.d.mts +3 -3
  3. package/dist/core/class/child-observable-class.d.mts +40 -27
  4. package/dist/core/class/child-observable-class.d.mts.map +1 -1
  5. package/dist/core/class/child-observable-class.mjs +78 -46
  6. package/dist/core/class/child-observable-class.mjs.map +1 -1
  7. package/dist/core/class/create-child-observable.d.mts +28 -30
  8. package/dist/core/class/create-child-observable.d.mts.map +1 -1
  9. package/dist/core/class/create-child-observable.mjs +6 -8
  10. package/dist/core/class/create-child-observable.mjs.map +1 -1
  11. package/dist/core/class/create-root-observable.d.mts +12 -12
  12. package/dist/core/class/create-root-observable.d.mts.map +1 -1
  13. package/dist/core/class/create-root-observable.mjs +1 -5
  14. package/dist/core/class/create-root-observable.mjs.map +1 -1
  15. package/dist/core/class/manager-observable-class.d.mts +6 -6
  16. package/dist/core/class/manager-observable-class.mjs.map +1 -1
  17. package/dist/core/class/observable-base-class.d.mts +29 -31
  18. package/dist/core/class/observable-base-class.d.mts.map +1 -1
  19. package/dist/core/class/observable-base-class.mjs +0 -2
  20. package/dist/core/class/observable-base-class.mjs.map +1 -1
  21. package/dist/core/class/observable-node.d.mts +3 -4
  22. package/dist/core/class/observable-node.d.mts.map +1 -1
  23. package/dist/core/class/observable-node.mjs +0 -1
  24. package/dist/core/class/observable-node.mjs.map +1 -1
  25. package/dist/core/class/propagation-state.d.mts +6 -8
  26. package/dist/core/class/propagation-state.d.mts.map +1 -1
  27. package/dist/core/class/propagation-state.mjs +1 -3
  28. package/dist/core/class/propagation-state.mjs.map +1 -1
  29. package/dist/core/class/root-observable-class.d.mts +4 -4
  30. package/dist/core/combine/derive.d.mts +7 -8
  31. package/dist/core/combine/derive.d.mts.map +1 -1
  32. package/dist/core/combine/derive.mjs +2 -2
  33. package/dist/core/combine/derive.mjs.map +1 -1
  34. package/dist/core/combine/merge.mjs.map +1 -1
  35. package/dist/core/combine/zip.d.mts +3 -3
  36. package/dist/core/combine/zip.d.mts.map +1 -1
  37. package/dist/core/combine/zip.mjs +5 -2
  38. package/dist/core/combine/zip.mjs.map +1 -1
  39. package/dist/core/create/counter.d.mts +3 -3
  40. package/dist/core/create/counter.mjs +2 -2
  41. package/dist/core/create/counter.mjs.map +1 -1
  42. package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
  43. package/dist/core/create/from-abortable-promise.mjs +10 -3
  44. package/dist/core/create/from-abortable-promise.mjs.map +1 -1
  45. package/dist/core/create/from-promise.d.mts.map +1 -1
  46. package/dist/core/create/from-promise.mjs +10 -3
  47. package/dist/core/create/from-promise.mjs.map +1 -1
  48. package/dist/core/create/from-subscribable.mjs.map +1 -1
  49. package/dist/core/create/source.d.mts.map +1 -1
  50. package/dist/core/create/source.mjs +2 -1
  51. package/dist/core/create/source.mjs.map +1 -1
  52. package/dist/core/create/timer.mjs.map +1 -1
  53. package/dist/core/operators/audit.mjs.map +1 -1
  54. package/dist/core/operators/debounce.mjs.map +1 -1
  55. package/dist/core/operators/filter.d.mts +1 -1
  56. package/dist/core/operators/filter.d.mts.map +1 -1
  57. package/dist/core/operators/filter.mjs +2 -3
  58. package/dist/core/operators/filter.mjs.map +1 -1
  59. package/dist/core/operators/keep-equal-parts.d.mts +14 -15
  60. package/dist/core/operators/keep-equal-parts.d.mts.map +1 -1
  61. package/dist/core/operators/keep-equal-parts.mjs +0 -1
  62. package/dist/core/operators/keep-equal-parts.mjs.map +1 -1
  63. package/dist/core/operators/map.mjs.map +1 -1
  64. package/dist/core/operators/merge-map.d.mts +7 -3
  65. package/dist/core/operators/merge-map.d.mts.map +1 -1
  66. package/dist/core/operators/merge-map.mjs +47 -16
  67. package/dist/core/operators/merge-map.mjs.map +1 -1
  68. package/dist/core/operators/pairwise.mjs.map +1 -1
  69. package/dist/core/operators/scan.mjs.map +1 -1
  70. package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
  71. package/dist/core/operators/skip-until.mjs.map +1 -1
  72. package/dist/core/operators/skip-while.d.mts +1 -1
  73. package/dist/core/operators/skip-while.d.mts.map +1 -1
  74. package/dist/core/operators/skip-while.mjs +9 -6
  75. package/dist/core/operators/skip-while.mjs.map +1 -1
  76. package/dist/core/operators/switch-map.d.mts +4 -0
  77. package/dist/core/operators/switch-map.d.mts.map +1 -1
  78. package/dist/core/operators/switch-map.mjs +16 -6
  79. package/dist/core/operators/switch-map.mjs.map +1 -1
  80. package/dist/core/operators/take-until.mjs.map +1 -1
  81. package/dist/core/operators/take-while.d.mts +2 -2
  82. package/dist/core/operators/take-while.d.mts.map +1 -1
  83. package/dist/core/operators/take-while.mjs +10 -8
  84. package/dist/core/operators/take-while.mjs.map +1 -1
  85. package/dist/core/operators/take.mjs.map +1 -1
  86. package/dist/core/operators/throttle.mjs +3 -2
  87. package/dist/core/operators/throttle.mjs.map +1 -1
  88. package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
  89. package/dist/core/operators/with-buffered-from.mjs +13 -3
  90. package/dist/core/operators/with-buffered-from.mjs.map +1 -1
  91. package/dist/core/operators/with-current-value-from.mjs.map +1 -1
  92. package/dist/core/operators/with-initial-value.mjs.map +1 -1
  93. package/dist/core/predefined/operators/attach-index.d.mts +1 -1
  94. package/dist/core/predefined/operators/attach-index.d.mts.map +1 -1
  95. package/dist/core/predefined/operators/attach-index.mjs +2 -3
  96. package/dist/core/predefined/operators/attach-index.mjs.map +1 -1
  97. package/dist/core/predefined/operators/map-to.d.mts +3 -4
  98. package/dist/core/predefined/operators/map-to.d.mts.map +1 -1
  99. package/dist/core/predefined/operators/map-to.mjs +0 -1
  100. package/dist/core/predefined/operators/map-to.mjs.map +1 -1
  101. package/dist/core/predefined/operators/skip.d.mts.map +1 -1
  102. package/dist/core/predefined/operators/skip.mjs +2 -2
  103. package/dist/core/predefined/operators/skip.mjs.map +1 -1
  104. package/dist/core/scope/scope.d.mts +4 -4
  105. package/dist/core/types/observable-family.d.mts.map +1 -1
  106. package/dist/core/types/observable-family.mjs.map +1 -1
  107. package/dist/core/types/observable.d.mts +5 -5
  108. package/dist/core/types/observable.mjs.map +1 -1
  109. package/dist/core/utils/current-scope.d.mts +6 -6
  110. package/dist/core/utils/id-maker.d.mts +2 -2
  111. package/dist/core/utils/index.d.mts +1 -0
  112. package/dist/core/utils/index.d.mts.map +1 -1
  113. package/dist/core/utils/index.mjs +1 -0
  114. package/dist/core/utils/index.mjs.map +1 -1
  115. package/dist/core/utils/observable-brand.d.mts +5 -5
  116. package/dist/core/utils/rethrow-out-of-promise.d.mts +10 -0
  117. package/dist/core/utils/rethrow-out-of-promise.d.mts.map +1 -0
  118. package/dist/core/utils/rethrow-out-of-promise.mjs +17 -0
  119. package/dist/core/utils/rethrow-out-of-promise.mjs.map +1 -0
  120. package/dist/utils/create-boolean-state.d.mts +2 -2
  121. package/dist/utils/create-reducer.d.mts +18 -18
  122. package/dist/utils/create-state.d.mts +2 -2
  123. package/dist/utils/create-state.mjs.map +1 -1
  124. package/dist/utils/state-source.d.mts +3 -3
  125. package/package.json +12 -11
  126. package/src/core/authoring.mts +3 -3
  127. package/src/core/class/child-observable-class.mts +131 -90
  128. package/src/core/class/circular-dependency-comparison.test.mts +2 -2
  129. package/src/core/class/create-child-observable.mts +35 -34
  130. package/src/core/class/create-root-observable.mts +13 -12
  131. package/src/core/class/manager-observable-class.mts +12 -12
  132. package/src/core/class/observable-base-class.mts +33 -35
  133. package/src/core/class/observable-node.mts +3 -4
  134. package/src/core/class/propagation-state.mts +27 -29
  135. package/src/core/class/remove-completed.test.mts +2 -2
  136. package/src/core/class/root-observable-class.mts +9 -9
  137. package/src/core/class/unobserved-write.test.mts +4 -4
  138. package/src/core/class/write-during-propagation.test.mts +75 -4
  139. package/src/core/combine/derive.mts +12 -12
  140. package/src/core/combine/merge.mts +2 -2
  141. package/src/core/combine/zip.mts +12 -9
  142. package/src/core/create/counter.mts +7 -7
  143. package/src/core/create/from-abortable-promise.mts +35 -25
  144. package/src/core/create/from-promise.mts +30 -20
  145. package/src/core/create/from-promise.test.mts +97 -0
  146. package/src/core/create/from-subscribable.mts +2 -2
  147. package/src/core/create/source.mts +7 -6
  148. package/src/core/create/timer.mts +2 -2
  149. package/src/core/operators/audit.mts +2 -2
  150. package/src/core/operators/debounce.mts +2 -2
  151. package/src/core/operators/filter.mts +6 -7
  152. package/src/core/operators/flat-map-initial-value.test.mts +269 -0
  153. package/src/core/operators/flat-map-sync-inner.test.mts +4 -2
  154. package/src/core/operators/inner-ownership.test.mts +145 -2
  155. package/src/core/operators/keep-equal-parts.mts +16 -17
  156. package/src/core/operators/map.mts +2 -2
  157. package/src/core/operators/merge-map.mts +59 -29
  158. package/src/core/operators/pairwise.mts +4 -4
  159. package/src/core/operators/scan.mts +2 -2
  160. package/src/core/operators/skip-if-no-change.mts +4 -4
  161. package/src/core/operators/skip-until.mts +4 -4
  162. package/src/core/operators/skip-while.mts +14 -10
  163. package/src/core/operators/skip-while.test.mts +59 -0
  164. package/src/core/operators/switch-map.mts +26 -15
  165. package/src/core/operators/take-until.mts +2 -2
  166. package/src/core/operators/take-while.mts +17 -19
  167. package/src/core/operators/take-while.test.mts +51 -0
  168. package/src/core/operators/take.mts +2 -2
  169. package/src/core/operators/throttle.mts +6 -5
  170. package/src/core/operators/with-buffered-from.mts +18 -5
  171. package/src/core/operators/with-buffered-from.test.mts +27 -0
  172. package/src/core/operators/with-current-value-from.mts +2 -2
  173. package/src/core/operators/with-initial-value.mts +2 -2
  174. package/src/core/predefined/operators/attach-index.mts +4 -5
  175. package/src/core/predefined/operators/map-to.mts +3 -4
  176. package/src/core/predefined/operators/skip.mts +2 -2
  177. package/src/core/scope/scope.mts +6 -6
  178. package/src/core/types/observable-family.mts +4 -5
  179. package/src/core/types/observable.mts +7 -7
  180. package/src/core/utils/current-scope.mts +8 -8
  181. package/src/core/utils/id-maker.mts +2 -2
  182. package/src/core/utils/index.mts +1 -0
  183. package/src/core/utils/observable-brand.mts +5 -5
  184. package/src/core/utils/rethrow-out-of-promise.mts +16 -0
  185. package/src/utils/create-boolean-state.mts +2 -2
  186. package/src/utils/create-reducer.mts +18 -18
  187. package/src/utils/create-state.mts +4 -4
  188. package/src/utils/create-state.test.mts +2 -2
  189. 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 { Arr, type Optional } from 'ts-data-forge';
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
- * subclass of `RootObservableClass` inherits.
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
- * to run at the beginning of `complete()`, in the order registered. Called
28
- * from `init`, it lets the teardown close over what `init` creates.
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
- * of its own.
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
- * public members to merge into the observable (e.g. `next` for a source,
45
- * `start` for a timer). It may register asynchronous callbacks that use the
46
- * tools, and may call `complete` synchronously.
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
- * an async child (`debounce`, `switchMap`), which emits later than its
17
- * parents do. Its update visits, shallowest first, the nodes its value
18
- * reaches: each node that emits puts its children in the update's queue
19
- * (`propagation-state.mts`), so that each sees its parents' values of that
20
- * update (no glitch) and a node no value reached is not visited.
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
- // written before anything subscribes): the update is the value alone,
31
- // and opening one would cost more than storing it.
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
- // down regardless, or every later write would warn. The exception itself
42
- // is not caught: it reaches the caller of `next` as it always has.
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
- // `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
@@ -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
- * be cheap, and most of its cost was its methods: each observable used to be
40
- * a set of closures made for it, a dozen or more per node (#2421). Sharing
41
- * the methods among all nodes removes that cost, and a shared method has to
42
- * read the node it is called on from `this`. Once `this` is unavoidable, a
43
- * class is the plainest way to say it: the methods live once on the
44
- * prototype, `this` is typed, a creator or operator is a subclass that
45
- * overrides `tryUpdate` (and `flush` / `teardown` where it holds something),
46
- * and assigning every field in the constructor gives the nodes of one class
47
- * one shape for V8. The collections (`children`, subscribers) are made when
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
- * fields, never through a `#private` one: the other node may be an instance
55
- * of another copy of synstate in the same program, whose classes are not
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
- // 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.
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
- * update `updateToken`, and notifies subscribers. The children are queued
163
- * first, so that a child a subscriber creates, which starts from this
164
- * 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.
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
- * observable — no child to queue and no subscriber to call — so that the
185
- * caller need not open an update; returns `false`, storing nothing,
186
- * otherwise. The value still gets a token of its own, so that whoever
187
- * stored the last one sees that it emitted.
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
- * parents: an upstream observable may be shared, and only whoever owns it
225
- * 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).
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
- * 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.
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
- * `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).
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
- * 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
@@ -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 { Arr } from 'ts-data-forge';
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
- * of the outermost one. A write nothing observes opens none
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
- * update has reached every descendant.
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
- * to while one is, is written to from inside a subscriber or a projection
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
- * bucket per depth below the node that started the update, holding the
96
- * queued nodes and how many of its slots are in use. Kept from update to
97
- * update, so that an update allocates nothing but its token; emptying a
98
- * 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.
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
- * a root while the outer one is propagating; a chain of such writes this deep
130
- * 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.
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
- * not overwrite the mark of an update that is still open — one it is nested
178
- * in, or one of another copy of synstate — and remembers the nodes it
179
- * 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.
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
- // 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.
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
- // 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.
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
- // not grow while it is visited. Its length is kept until the end, so
289
- // 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.
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
- // 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.';
@@ -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
- // pin what that must not change.
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
- // only a$, never the value a$ was given while nothing observed it.
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 { map, switchMap } from '../operators/index.mjs';
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
- assert.deepStrictEqual(mut_history, [10]);
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
- assert.deepStrictEqual(mut_history, [10]);
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> | 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
- // 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
7
7
  // @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
8
- import { Arr, Optional } from 'ts-data-forge';
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
- * result unless it equals the one already held.
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
- * 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).
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
- // position, which the checker cannot follow through an index.
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
- // 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,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
- // 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
7
7
  // @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
8
- import { Arr, Optional, createQueue, expectType } from 'ts-data-forge';
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
- * `zip` does: at once if it completes with nothing queued, otherwise after
31
- * the tuple that takes its last queued value.
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.complete();
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
- * left to pair (#2343).
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(