synstate 6.2.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 (102) hide show
  1. package/README.md +1 -1
  2. package/dist/core/class/child-observable-class.d.mts +16 -2
  3. package/dist/core/class/child-observable-class.d.mts.map +1 -1
  4. package/dist/core/class/child-observable-class.mjs +37 -4
  5. package/dist/core/class/child-observable-class.mjs.map +1 -1
  6. package/dist/core/class/create-child-observable.d.mts +1 -1
  7. package/dist/core/class/create-child-observable.d.mts.map +1 -1
  8. package/dist/core/class/create-child-observable.mjs +6 -8
  9. package/dist/core/class/create-child-observable.mjs.map +1 -1
  10. package/dist/core/class/create-root-observable.d.mts +1 -1
  11. package/dist/core/class/create-root-observable.d.mts.map +1 -1
  12. package/dist/core/class/create-root-observable.mjs +1 -5
  13. package/dist/core/class/create-root-observable.mjs.map +1 -1
  14. package/dist/core/class/propagation-state.mjs +1 -1
  15. package/dist/core/class/propagation-state.mjs.map +1 -1
  16. package/dist/core/combine/derive.d.mts.map +1 -1
  17. package/dist/core/combine/derive.mjs +2 -1
  18. package/dist/core/combine/derive.mjs.map +1 -1
  19. package/dist/core/combine/zip.d.mts.map +1 -1
  20. package/dist/core/combine/zip.mjs +5 -2
  21. package/dist/core/combine/zip.mjs.map +1 -1
  22. package/dist/core/create/counter.mjs +2 -2
  23. package/dist/core/create/counter.mjs.map +1 -1
  24. package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
  25. package/dist/core/create/from-abortable-promise.mjs +10 -3
  26. package/dist/core/create/from-abortable-promise.mjs.map +1 -1
  27. package/dist/core/create/from-promise.d.mts.map +1 -1
  28. package/dist/core/create/from-promise.mjs +10 -3
  29. package/dist/core/create/from-promise.mjs.map +1 -1
  30. package/dist/core/create/source.d.mts.map +1 -1
  31. package/dist/core/create/source.mjs +2 -1
  32. package/dist/core/create/source.mjs.map +1 -1
  33. package/dist/core/operators/filter.d.mts +1 -1
  34. package/dist/core/operators/filter.d.mts.map +1 -1
  35. package/dist/core/operators/filter.mjs +2 -3
  36. package/dist/core/operators/filter.mjs.map +1 -1
  37. package/dist/core/operators/merge-map.d.mts +6 -2
  38. package/dist/core/operators/merge-map.d.mts.map +1 -1
  39. package/dist/core/operators/merge-map.mjs +47 -16
  40. package/dist/core/operators/merge-map.mjs.map +1 -1
  41. package/dist/core/operators/skip-while.d.mts +1 -1
  42. package/dist/core/operators/skip-while.d.mts.map +1 -1
  43. package/dist/core/operators/skip-while.mjs +9 -6
  44. package/dist/core/operators/skip-while.mjs.map +1 -1
  45. package/dist/core/operators/switch-map.d.mts +4 -0
  46. package/dist/core/operators/switch-map.d.mts.map +1 -1
  47. package/dist/core/operators/switch-map.mjs +16 -6
  48. package/dist/core/operators/switch-map.mjs.map +1 -1
  49. package/dist/core/operators/take-while.d.mts +2 -2
  50. package/dist/core/operators/take-while.d.mts.map +1 -1
  51. package/dist/core/operators/take-while.mjs +10 -8
  52. package/dist/core/operators/take-while.mjs.map +1 -1
  53. package/dist/core/operators/throttle.mjs +3 -2
  54. package/dist/core/operators/throttle.mjs.map +1 -1
  55. package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
  56. package/dist/core/operators/with-buffered-from.mjs +13 -3
  57. package/dist/core/operators/with-buffered-from.mjs.map +1 -1
  58. package/dist/core/predefined/operators/attach-index.d.mts +1 -1
  59. package/dist/core/predefined/operators/attach-index.d.mts.map +1 -1
  60. package/dist/core/predefined/operators/attach-index.mjs +2 -3
  61. package/dist/core/predefined/operators/attach-index.mjs.map +1 -1
  62. package/dist/core/predefined/operators/skip.d.mts.map +1 -1
  63. package/dist/core/predefined/operators/skip.mjs +2 -2
  64. package/dist/core/predefined/operators/skip.mjs.map +1 -1
  65. package/dist/core/utils/index.d.mts +1 -0
  66. package/dist/core/utils/index.d.mts.map +1 -1
  67. package/dist/core/utils/index.mjs +1 -0
  68. package/dist/core/utils/index.mjs.map +1 -1
  69. package/dist/core/utils/rethrow-out-of-promise.d.mts +10 -0
  70. package/dist/core/utils/rethrow-out-of-promise.d.mts.map +1 -0
  71. package/dist/core/utils/rethrow-out-of-promise.mjs +17 -0
  72. package/dist/core/utils/rethrow-out-of-promise.mjs.map +1 -0
  73. package/package.json +12 -11
  74. package/src/core/class/child-observable-class.mts +45 -3
  75. package/src/core/class/create-child-observable.mts +8 -5
  76. package/src/core/class/create-root-observable.mts +2 -1
  77. package/src/core/class/propagation-state.mts +1 -1
  78. package/src/core/class/write-during-propagation.test.mts +75 -4
  79. package/src/core/combine/derive.mts +2 -1
  80. package/src/core/combine/zip.mts +5 -2
  81. package/src/core/create/counter.mts +2 -2
  82. package/src/core/create/from-abortable-promise.mts +33 -23
  83. package/src/core/create/from-promise.mts +28 -18
  84. package/src/core/create/from-promise.test.mts +97 -0
  85. package/src/core/create/source.mts +2 -1
  86. package/src/core/operators/filter.mts +2 -3
  87. package/src/core/operators/flat-map-initial-value.test.mts +269 -0
  88. package/src/core/operators/flat-map-sync-inner.test.mts +4 -2
  89. package/src/core/operators/inner-ownership.test.mts +145 -2
  90. package/src/core/operators/merge-map.mts +56 -26
  91. package/src/core/operators/skip-while.mts +12 -8
  92. package/src/core/operators/skip-while.test.mts +59 -0
  93. package/src/core/operators/switch-map.mts +21 -10
  94. package/src/core/operators/take-while.mts +15 -17
  95. package/src/core/operators/take-while.test.mts +51 -0
  96. package/src/core/operators/throttle.mts +4 -3
  97. package/src/core/operators/with-buffered-from.mts +16 -3
  98. package/src/core/operators/with-buffered-from.test.mts +27 -0
  99. package/src/core/predefined/operators/attach-index.mts +2 -3
  100. package/src/core/predefined/operators/skip.mts +2 -2
  101. package/src/core/utils/index.mts +1 -0
  102. package/src/core/utils/rethrow-out-of-promise.mts +16 -0
@@ -21,6 +21,10 @@ import type {
21
21
  * Projects each source value to an observable, subscribes to it, and emits its values.
22
22
  * When a new value arrives from the source, the previous inner observable is unsubscribed.
23
23
  *
24
+ * A value the source already holds when `switchMap` is created (a state, an
25
+ * observable with an initial value) is projected at once, as `map` and `filter`
26
+ * start from it. A completed source is not projected.
27
+ *
24
28
  * The projection runs in a scope of its own (see `createScope`): what it
25
29
  * creates is disposed when the next value switches away from it and when
26
30
  * `switchMap` completes. An observable it only returns — one created outside
@@ -152,7 +156,9 @@ class SwitchMapObservableClass<A, B>
152
156
 
153
157
  this.#mut_subscription = undefined;
154
158
 
155
- this.attach();
159
+ this.attachProjectingHeldValue(parentObservable, (value, updateToken) => {
160
+ this.#project(value, updateToken);
161
+ });
156
162
  }
157
163
 
158
164
  /** Releases the inner observable, and only the one this operator made. */
@@ -162,6 +168,19 @@ class SwitchMapObservableClass<A, B>
162
168
  this.#mut_innerScope?.dispose();
163
169
  }
164
170
 
171
+ /** Switches to the inner observable of `value`. */
172
+ #project(value: A, updateToken: UpdateToken): void {
173
+ this.#releaseInner();
174
+
175
+ const innerScope = createScope(this.#ownerScope);
176
+
177
+ this.#mut_innerScope = innerScope;
178
+
179
+ const observable = innerScope.run(() => this.#mapToObservable(value));
180
+
181
+ this.#mut_subscription = this.subscribeInner(observable, updateToken);
182
+ }
183
+
165
184
  override tryUpdate(updateToken: UpdateToken): void {
166
185
  const par = this.parents[0];
167
186
 
@@ -171,15 +190,7 @@ class SwitchMapObservableClass<A, B>
171
190
  return; // skip update
172
191
  }
173
192
 
174
- this.#releaseInner();
175
-
176
- const innerScope = createScope(this.#ownerScope);
177
-
178
- this.#mut_innerScope = innerScope;
179
-
180
- const observable = innerScope.run(() => this.#mapToObservable(sn.value));
181
-
182
- this.#mut_subscription = this.subscribeInner(observable, updateToken);
193
+ this.#project(sn.value, updateToken);
183
194
  }
184
195
 
185
196
  protected override teardown(): void {
@@ -5,13 +5,7 @@
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 {
9
- Optional,
10
- SafeUint,
11
- asSafeUint,
12
- expectType,
13
- pipe,
14
- } from 'ts-data-forge';
8
+ import { Optional, asSafeUint, expectType, type SafeUint } from 'ts-data-forge';
15
9
  import { SyncChildObservableClass } from '../class/index.mjs';
16
10
  import { source } from '../create/index.mjs';
17
11
  import type {
@@ -73,7 +67,7 @@ import { withInitialValue } from './with-initial-value.mjs';
73
67
  *
74
68
  * assert.deepStrictEqual(valueHistory, [1, 2, 3, 4]);
75
69
  *
76
- * num$.next(1); // logs: 1
70
+ * num$.next(1); // nothing logged (already completed)
77
71
  *
78
72
  * assert.deepStrictEqual(valueHistory, [1, 2, 3, 4]);
79
73
  * ```
@@ -98,21 +92,26 @@ class TakeWhileObservableClass<A>
98
92
  parentObservable: Observable<A>,
99
93
  predicate: (value: A, index: SafeUint | -1) => boolean,
100
94
  ) {
95
+ const parentSnapshot = parentObservable.getSnapshot();
96
+
97
+ const isTaking =
98
+ Optional.isNone(parentSnapshot) || predicate(parentSnapshot.value, -1);
99
+
101
100
  super({
102
101
  parents: [parentObservable],
103
- initialValue: pipe(parentObservable.getSnapshot()).map((par) =>
104
- Optional.isNone(par)
105
- ? Optional.none
106
- : predicate(par.value, -1)
107
- ? par
108
- : Optional.none,
109
- ).value,
102
+ initialValue: isTaking ? parentSnapshot : Optional.none,
110
103
  });
111
104
 
112
105
  this.#mut_index = -1;
113
106
 
114
107
  this.#predicate = predicate;
115
108
 
109
+ // A parent value the predicate rejects ends it, as any later one would;
110
+ // `attach` then leaves it out of the graph.
111
+ if (!isTaking) {
112
+ this.complete();
113
+ }
114
+
116
115
  this.attach();
117
116
  }
118
117
 
@@ -125,8 +124,7 @@ class TakeWhileObservableClass<A>
125
124
  return; // skip update
126
125
  }
127
126
 
128
- this.#mut_index =
129
- this.#mut_index === -1 ? asSafeUint(0) : SafeUint.add(1, this.#mut_index);
127
+ this.#mut_index = asSafeUint(this.#mut_index + 1);
130
128
 
131
129
  if (this.#predicate(sn.value, this.#mut_index)) {
132
130
  this.setNext(sn.value, updateToken);
@@ -0,0 +1,51 @@
1
+ import { createState } from '../../utils/index.mjs';
2
+ import { takeWhile } from './take-while.mjs';
3
+
4
+ describe(takeWhile, () => {
5
+ test('completes at once when the initial value fails the predicate', () => {
6
+ const [s$, setS] = createState(5);
7
+
8
+ const taken$ = s$.pipe(takeWhile((x) => x < 3));
9
+
10
+ const mut_events: string[] = [];
11
+
12
+ taken$.subscribe(
13
+ (x) => {
14
+ mut_events.push(`next ${x}`);
15
+ },
16
+ () => {
17
+ mut_events.push('complete');
18
+ },
19
+ );
20
+
21
+ assert.isTrue(taken$.isCompleted);
22
+
23
+ setS(1);
24
+
25
+ assert.deepStrictEqual(mut_events, ['complete']);
26
+ });
27
+
28
+ test('keeps taking after an initial value that passes the predicate', () => {
29
+ const [s$, setS] = createState(1);
30
+
31
+ const taken$ = s$.pipe(takeWhile((x) => x < 3));
32
+
33
+ const mut_history: number[] = [];
34
+
35
+ taken$.subscribe((x) => {
36
+ mut_history.push(x);
37
+ });
38
+
39
+ setS(2);
40
+
41
+ assert.isFalse(taken$.isCompleted);
42
+
43
+ setS(5);
44
+
45
+ assert.isTrue(taken$.isCompleted);
46
+
47
+ setS(1);
48
+
49
+ assert.deepStrictEqual(mut_history, [1, 2]);
50
+ });
51
+ });
@@ -139,14 +139,15 @@ class ThrottleObservableClass<A>
139
139
  return; // skip update
140
140
  }
141
141
 
142
- this.setNext(sn.value, updateToken);
143
-
142
+ // The window opens before `setNext`, which can lead back here: a value a
143
+ // subscriber writes to the parent falls inside it.
144
144
  this.#mut_isSkipping = true;
145
145
 
146
- // set timer
147
146
  this.#mut_timerId = setTimeout(() => {
148
147
  this.#mut_isSkipping = false;
149
148
  }, this.#milliSeconds);
149
+
150
+ this.setNext(sn.value, updateToken);
150
151
  }
151
152
 
152
153
  protected override teardown(): void {
@@ -5,7 +5,8 @@
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, pipe } from 'ts-data-forge';
8
+ import { Optional, pipe } from 'ts-data-forge';
9
+ import * as Arr from 'ts-data-forge/array';
9
10
  import { SyncChildObservableClass } from '../class/index.mjs';
10
11
  import type {
11
12
  KeepInitialValueOperator,
@@ -93,11 +94,13 @@ class WithBufferedFromObservableClass<A, B>
93
94
  #mut_bufferedValues: readonly B[];
94
95
 
95
96
  constructor(parentObservable: Observable<A>, observable: Observable<B>) {
97
+ const parentSnapshot = parentObservable.getSnapshot();
98
+
96
99
  super({
97
100
  parents: [parentObservable],
98
101
  depth: 1 + maxDepth([parentObservable, observable]),
99
102
  initialValue: pipe({
100
- par: parentObservable.getSnapshot(),
103
+ par: parentSnapshot,
101
104
  me: observable.getSnapshot(),
102
105
  }).map(({ par, me }) =>
103
106
  Optional.isNone(par)
@@ -115,6 +118,12 @@ class WithBufferedFromObservableClass<A, B>
115
118
  this.#mut_bufferedValues = Arr.toPushed(this.#mut_bufferedValues, value);
116
119
  });
117
120
 
121
+ // `subscribe` has just pushed the buffered observable's current value,
122
+ // which the initial tuple already carries.
123
+ if (Optional.isSome(parentSnapshot)) {
124
+ this.#mut_bufferedValues = [];
125
+ }
126
+
118
127
  this.attach();
119
128
  }
120
129
 
@@ -127,9 +136,13 @@ class WithBufferedFromObservableClass<A, B>
127
136
  return; // skip update
128
137
  }
129
138
 
130
- this.setNext([sn.value, this.#mut_bufferedValues], updateToken);
139
+ const buffered = this.#mut_bufferedValues;
131
140
 
141
+ // Emptied before `setNext`, which can lead back here: a value a subscriber
142
+ // writes to the buffered observable belongs to the next batch.
132
143
  this.#mut_bufferedValues = [];
144
+
145
+ this.setNext([sn.value, buffered], updateToken);
133
146
  }
134
147
 
135
148
  protected override teardown(): void {
@@ -1,3 +1,4 @@
1
+ import { createState } from '../../utils/index.mjs';
1
2
  import { toNode } from '../class/index.mjs';
2
3
  import { source } from '../create/index.mjs';
3
4
  import { withBufferedFrom } from './with-buffered-from.mjs';
@@ -55,3 +56,29 @@ describe('withBufferedFrom teardown', () => {
55
56
  assert.deepStrictEqual(mut_history, [[1, ['A', 'B']]]);
56
57
  });
57
58
  });
59
+
60
+ describe('withBufferedFrom initial value', () => {
61
+ test('reports the current value of the buffered observable once when both hold one (#2767)', () => {
62
+ const [data$] = createState('x');
63
+
64
+ const [trigger$, setTrigger] = createState(0);
65
+
66
+ const result$ = trigger$.pipe(withBufferedFrom(data$));
67
+
68
+ const mut_history: (readonly [number, readonly string[]])[] = [];
69
+
70
+ result$.subscribe((v) => {
71
+ mut_history.push(v);
72
+ });
73
+
74
+ setTrigger(1);
75
+
76
+ setTrigger(2);
77
+
78
+ assert.deepStrictEqual(mut_history, [
79
+ [0, ['x']],
80
+ [1, []],
81
+ [2, []],
82
+ ]);
83
+ });
84
+ });
@@ -5,7 +5,7 @@
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 { Optional, SafeUint, asSafeUint } from 'ts-data-forge';
8
+ import { Optional, asSafeUint, type SafeUint } from 'ts-data-forge';
9
9
  import { SyncChildObservableClass } from '../../class/index.mjs';
10
10
  import type {
11
11
  KeepInitialValueOperator,
@@ -112,8 +112,7 @@ class WithIndexObservableClass<A> extends SyncChildObservableClass<
112
112
  return; // skip update
113
113
  }
114
114
 
115
- this.#mut_index =
116
- this.#mut_index === -1 ? asSafeUint(0) : SafeUint.add(1, this.#mut_index);
115
+ this.#mut_index = asSafeUint(this.#mut_index + 1);
117
116
 
118
117
  this.setNext([this.#mut_index, sn.value], updateToken);
119
118
  }
@@ -1,4 +1,4 @@
1
- import { PositiveSafeInt } from 'ts-data-forge';
1
+ import { isPositiveSafeInt } from 'ts-data-forge';
2
2
  import type { PositiveSafeIntWithSmallInt } from 'ts-type-forge';
3
3
  import { skipWhile } from '../../operators/index.mjs';
4
4
  import type { DropInitialValueOperator } from '../../types/index.mjs';
@@ -53,6 +53,6 @@ import type { DropInitialValueOperator } from '../../types/index.mjs';
53
53
  export const skip = <A,>(
54
54
  n: PositiveSafeIntWithSmallInt,
55
55
  ): DropInitialValueOperator<A, A> =>
56
- !PositiveSafeInt.is(n) ? idFn : skipWhile((_, index) => index + 1 <= n);
56
+ !isPositiveSafeInt(n) ? idFn : skipWhile((_, index) => index + 1 <= n);
57
57
 
58
58
  const idFn = <T,>(value: T): T => value;
@@ -3,3 +3,4 @@ export * from './id-maker.mjs';
3
3
  export * from './max-depth.mjs';
4
4
  export * from './observable-brand.mjs';
5
5
  export * from './observable-utils.mjs';
6
+ export * from './rethrow-out-of-promise.mjs';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Throws `error` again from a task of its own, for a `.catch` after each step
3
+ * of a promise chain that runs subscribers (`fromPromise`): after the step that
4
+ * emits and after the one that completes, so that neither exception replaces
5
+ * the other. A subscriber's exception then surfaces as an uncaught error, as
6
+ * one thrown during an update a timer started does (`debounce`, `timer`),
7
+ * rather than as a rejection or not at all.
8
+ */
9
+ export const rethrowOutOfPromise = (error: unknown): void => {
10
+ setTimeout(() => {
11
+ // The subscriber's own exception, passed on unchanged.
12
+ // @sumi-expect-error exceptions/no-throw
13
+ // eslint-disable-next-line @typescript-eslint/only-throw-error -- the subscriber's own exception, passed on unchanged
14
+ throw error;
15
+ }, 0);
16
+ };