synstate 6.2.1 → 7.0.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 (109) hide show
  1. package/dist/core/class/child-observable-class.d.mts +16 -14
  2. package/dist/core/class/child-observable-class.d.mts.map +1 -1
  3. package/dist/core/class/child-observable-class.mjs +39 -12
  4. package/dist/core/class/child-observable-class.mjs.map +1 -1
  5. package/dist/core/class/create-child-observable.d.mts +1 -1
  6. package/dist/core/class/create-child-observable.d.mts.map +1 -1
  7. package/dist/core/class/create-child-observable.mjs +6 -8
  8. package/dist/core/class/create-child-observable.mjs.map +1 -1
  9. package/dist/core/class/create-root-observable.d.mts +1 -1
  10. package/dist/core/class/create-root-observable.d.mts.map +1 -1
  11. package/dist/core/class/create-root-observable.mjs +1 -5
  12. package/dist/core/class/create-root-observable.mjs.map +1 -1
  13. package/dist/core/class/propagation-state.mjs +1 -1
  14. package/dist/core/class/propagation-state.mjs.map +1 -1
  15. package/dist/core/combine/derive.d.mts.map +1 -1
  16. package/dist/core/combine/derive.mjs +2 -1
  17. package/dist/core/combine/derive.mjs.map +1 -1
  18. package/dist/core/combine/zip.d.mts.map +1 -1
  19. package/dist/core/combine/zip.mjs +5 -2
  20. package/dist/core/combine/zip.mjs.map +1 -1
  21. package/dist/core/create/counter.mjs +2 -2
  22. package/dist/core/create/counter.mjs.map +1 -1
  23. package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
  24. package/dist/core/create/from-abortable-promise.mjs +10 -3
  25. package/dist/core/create/from-abortable-promise.mjs.map +1 -1
  26. package/dist/core/create/from-promise.d.mts.map +1 -1
  27. package/dist/core/create/from-promise.mjs +10 -3
  28. package/dist/core/create/from-promise.mjs.map +1 -1
  29. package/dist/core/create/source.d.mts.map +1 -1
  30. package/dist/core/create/source.mjs +2 -1
  31. package/dist/core/create/source.mjs.map +1 -1
  32. package/dist/core/operators/filter.d.mts +1 -1
  33. package/dist/core/operators/filter.d.mts.map +1 -1
  34. package/dist/core/operators/filter.mjs +2 -3
  35. package/dist/core/operators/filter.mjs.map +1 -1
  36. package/dist/core/operators/merge-map.d.mts +6 -2
  37. package/dist/core/operators/merge-map.d.mts.map +1 -1
  38. package/dist/core/operators/merge-map.mjs +47 -16
  39. package/dist/core/operators/merge-map.mjs.map +1 -1
  40. package/dist/core/operators/skip-while.d.mts +1 -1
  41. package/dist/core/operators/skip-while.d.mts.map +1 -1
  42. package/dist/core/operators/skip-while.mjs +9 -6
  43. package/dist/core/operators/skip-while.mjs.map +1 -1
  44. package/dist/core/operators/switch-map.d.mts +4 -0
  45. package/dist/core/operators/switch-map.d.mts.map +1 -1
  46. package/dist/core/operators/switch-map.mjs +16 -6
  47. package/dist/core/operators/switch-map.mjs.map +1 -1
  48. package/dist/core/operators/take-while.d.mts +2 -2
  49. package/dist/core/operators/take-while.d.mts.map +1 -1
  50. package/dist/core/operators/take-while.mjs +10 -8
  51. package/dist/core/operators/take-while.mjs.map +1 -1
  52. package/dist/core/operators/throttle.mjs +3 -2
  53. package/dist/core/operators/throttle.mjs.map +1 -1
  54. package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
  55. package/dist/core/operators/with-buffered-from.mjs +13 -3
  56. package/dist/core/operators/with-buffered-from.mjs.map +1 -1
  57. package/dist/core/predefined/operators/attach-index.d.mts +1 -1
  58. package/dist/core/predefined/operators/attach-index.d.mts.map +1 -1
  59. package/dist/core/predefined/operators/attach-index.mjs +2 -3
  60. package/dist/core/predefined/operators/attach-index.mjs.map +1 -1
  61. package/dist/core/predefined/operators/skip.d.mts.map +1 -1
  62. package/dist/core/predefined/operators/skip.mjs +2 -2
  63. package/dist/core/predefined/operators/skip.mjs.map +1 -1
  64. package/dist/core/utils/index.d.mts +2 -0
  65. package/dist/core/utils/index.d.mts.map +1 -1
  66. package/dist/core/utils/index.mjs +2 -0
  67. package/dist/core/utils/index.mjs.map +1 -1
  68. package/dist/core/utils/is-development-build.d.mts +15 -0
  69. package/dist/core/utils/is-development-build.d.mts.map +1 -0
  70. package/dist/core/utils/is-development-build.mjs +23 -0
  71. package/dist/core/utils/is-development-build.mjs.map +1 -0
  72. package/dist/core/utils/rethrow-out-of-promise.d.mts +10 -0
  73. package/dist/core/utils/rethrow-out-of-promise.d.mts.map +1 -0
  74. package/dist/core/utils/rethrow-out-of-promise.mjs +17 -0
  75. package/dist/core/utils/rethrow-out-of-promise.mjs.map +1 -0
  76. package/package.json +12 -11
  77. package/src/core/class/child-observable-class.mts +53 -17
  78. package/src/core/class/circular-dependency-comparison.test.mts +36 -40
  79. package/src/core/class/circular-dependency.test.mts +6 -37
  80. package/src/core/class/create-child-observable.mts +8 -5
  81. package/src/core/class/create-root-observable.mts +2 -1
  82. package/src/core/class/propagation-state.mts +1 -1
  83. package/src/core/class/write-during-propagation.test.mts +75 -4
  84. package/src/core/combine/derive.mts +2 -1
  85. package/src/core/combine/zip.mts +5 -2
  86. package/src/core/create/counter.mts +2 -2
  87. package/src/core/create/from-abortable-promise.mts +33 -23
  88. package/src/core/create/from-promise.mts +28 -18
  89. package/src/core/create/from-promise.test.mts +97 -0
  90. package/src/core/create/source.mts +2 -1
  91. package/src/core/operators/filter.mts +2 -3
  92. package/src/core/operators/flat-map-initial-value.test.mts +269 -0
  93. package/src/core/operators/flat-map-sync-inner.test.mts +4 -2
  94. package/src/core/operators/inner-ownership.test.mts +145 -2
  95. package/src/core/operators/merge-map.mts +56 -26
  96. package/src/core/operators/skip-while.mts +12 -8
  97. package/src/core/operators/skip-while.test.mts +59 -0
  98. package/src/core/operators/switch-map.mts +21 -10
  99. package/src/core/operators/take-while.mts +15 -17
  100. package/src/core/operators/take-while.test.mts +51 -0
  101. package/src/core/operators/throttle.mts +4 -3
  102. package/src/core/operators/with-buffered-from.mts +16 -3
  103. package/src/core/operators/with-buffered-from.test.mts +27 -0
  104. package/src/core/predefined/operators/attach-index.mts +2 -3
  105. package/src/core/predefined/operators/skip.mts +2 -2
  106. package/src/core/utils/index.mts +2 -0
  107. package/src/core/utils/is-development-build.mts +32 -0
  108. package/src/core/utils/is-development-build.test.mts +44 -0
  109. package/src/core/utils/rethrow-out-of-promise.mts +16 -0
@@ -1,5 +1,7 @@
1
- import { source } from '../create/index.mjs';
2
- import { createScope } from '../scope/index.mjs';
1
+ import { panic } from 'ts-std-forge';
2
+ import { toNode } from '../class/index.mjs';
3
+ import { just, source } from '../create/index.mjs';
4
+ import { createScope, onScopeDispose } from '../scope/index.mjs';
3
5
  import { map } from './map.mjs';
4
6
  import { mergeMap } from './merge-map.mjs';
5
7
  import { switchMap } from './switch-map.mjs';
@@ -278,3 +280,144 @@ describe('mergeMap releases only the inner observables its projection created (#
278
280
  assert.isFalse(shared$.isCompleted);
279
281
  });
280
282
  });
283
+
284
+ describe('mergeMap releases an inner observable as soon as it completes (#2769)', () => {
285
+ test('disposes what the projection made once its inner observable completes', () => {
286
+ const click$ = source<number>();
287
+
288
+ const mut_inners: ReturnType<typeof source<string>>[] = [];
289
+
290
+ const mut_disposed: number[] = [];
291
+
292
+ const merged$ = click$.pipe(
293
+ mergeMap((n) => {
294
+ onScopeDispose(() => {
295
+ mut_disposed.push(n);
296
+ });
297
+
298
+ const inner$ = source<string>();
299
+
300
+ mut_inners.push(inner$);
301
+
302
+ return inner$;
303
+ }),
304
+ );
305
+
306
+ const mut_values: string[] = [];
307
+
308
+ merged$.subscribe((v) => {
309
+ mut_values.push(v);
310
+ });
311
+
312
+ click$.next(1);
313
+
314
+ click$.next(2);
315
+
316
+ mut_inners[0]?.next('a');
317
+
318
+ mut_inners[0]?.complete();
319
+
320
+ assert.deepStrictEqual(mut_disposed, [1]);
321
+
322
+ mut_inners[1]?.next('b');
323
+
324
+ assert.deepStrictEqual(mut_values, ['a', 'b']);
325
+
326
+ merged$.complete();
327
+
328
+ // Each projection's scope is disposed once.
329
+ assert.deepStrictEqual(mut_disposed, [1, 2]);
330
+ });
331
+
332
+ test('disposes at once what the projection made when its inner observable completes while subscribed', () => {
333
+ const click$ = source<number>();
334
+
335
+ const mut_disposed: number[] = [];
336
+
337
+ const merged$ = click$.pipe(
338
+ mergeMap((n) => {
339
+ onScopeDispose(() => {
340
+ mut_disposed.push(n);
341
+ });
342
+
343
+ return just(n);
344
+ }),
345
+ );
346
+
347
+ const mut_values: number[] = [];
348
+
349
+ merged$.subscribe((v) => {
350
+ mut_values.push(v);
351
+ });
352
+
353
+ click$.next(1);
354
+
355
+ click$.next(2);
356
+
357
+ assert.deepStrictEqual(mut_values, [1, 2]);
358
+
359
+ assert.deepStrictEqual(mut_disposed, [1, 2]);
360
+
361
+ merged$.complete();
362
+
363
+ assert.deepStrictEqual(mut_disposed, [1, 2]);
364
+ });
365
+
366
+ test('disposes what a projection that threw made when mergeMap completes', () => {
367
+ const click$ = source<number>();
368
+
369
+ const mut_disposed: number[] = [];
370
+
371
+ const merged$ = click$.pipe(
372
+ mergeMap((n) => {
373
+ onScopeDispose(() => {
374
+ mut_disposed.push(n);
375
+ });
376
+
377
+ return panic('projection failed');
378
+ }),
379
+ );
380
+
381
+ merged$.subscribe(() => {});
382
+
383
+ expect(() => {
384
+ click$.next(1);
385
+ }).toThrow('projection failed');
386
+
387
+ merged$.complete();
388
+
389
+ assert.deepStrictEqual(mut_disposed, [1]);
390
+ });
391
+
392
+ test('releases an inner observable when mergeMap completes while subscribing to it', () => {
393
+ const click$ = source<number>();
394
+
395
+ const inner$ = source<string>('first');
396
+
397
+ const mut_disposed: number[] = [];
398
+
399
+ const merged$ = click$.pipe(
400
+ mergeMap((n) => {
401
+ onScopeDispose(() => {
402
+ mut_disposed.push(n);
403
+ });
404
+
405
+ return inner$;
406
+ }),
407
+ );
408
+
409
+ merged$.subscribe((v) => {
410
+ if (v === 'first') {
411
+ merged$.complete();
412
+ }
413
+ });
414
+
415
+ click$.next(1);
416
+
417
+ assert.isTrue(merged$.isCompleted);
418
+
419
+ assert.deepStrictEqual(mut_disposed, [1]);
420
+
421
+ assert.isFalse(toNode(inner$).hasSubscriber);
422
+ });
423
+ });
@@ -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 } from 'ts-data-forge';
8
+ import { Optional } from 'ts-data-forge';
9
+ import type { MutableMap } from 'ts-type-forge';
9
10
  import { AsyncChildObservableClass } from '../class/index.mjs';
10
11
  import { createScope, getCurrentScope } from '../scope/index.mjs';
11
12
  import type {
@@ -21,9 +22,13 @@ import type {
21
22
  * Projects each source value to an observable and merges all inner observables.
22
23
  * Unlike `switchMap`, does not cancel previous inner observables.
23
24
  *
25
+ * A value the source already holds when `mergeMap` is created is projected at
26
+ * once, as with `switchMap`. A completed source is not projected.
27
+ *
24
28
  * Each projection runs in a scope of its own (see `createScope`): what it
25
- * creates is disposed when `mergeMap` completes. An observable it only returns
26
- * — one created outside the projection and shared with others — is unsubscribed
29
+ * creates is disposed when the observable it returned completes, or when
30
+ * `mergeMap` completes, whichever comes first. An observable it only returns —
31
+ * one created outside the projection and shared with others — is unsubscribed
27
32
  * and left running.
28
33
  *
29
34
  * @template A - The type of values from the source
@@ -136,8 +141,12 @@ class MergeMapObservableClass<A, B>
136
141
  /** See `SwitchMapObservableClass`. */
137
142
  readonly #ownerScope: Scope | undefined;
138
143
 
139
- #mut_innerScopes: readonly Scope[];
140
- #mut_subscriptions: readonly Subscription[];
144
+ /**
145
+ * The inner observables still live, by the scope their projection ran in.
146
+ * One that completes is released at once, so a long-lived `mergeMap` holds
147
+ * only those (#2769).
148
+ */
149
+ readonly #mut_liveInners: MutableMap<Scope, Subscription | undefined>;
141
150
 
142
151
  constructor(
143
152
  parentObservable: Observable<A>,
@@ -152,11 +161,47 @@ class MergeMapObservableClass<A, B>
152
161
 
153
162
  this.#ownerScope = getCurrentScope();
154
163
 
155
- this.#mut_innerScopes = [];
164
+ this.#mut_liveInners = new Map<Scope, Subscription | undefined>();
165
+
166
+ this.attachProjectingHeldValue(parentObservable, (value, updateToken) => {
167
+ this.#project(value, updateToken);
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Unsubscribes from an inner observable and disposes what its projection
173
+ * made.
174
+ */
175
+ #releaseInner(innerScope: Scope): void {
176
+ this.#mut_liveInners.get(innerScope)?.unsubscribe();
156
177
 
157
- this.#mut_subscriptions = [];
178
+ this.#mut_liveInners.delete(innerScope);
158
179
 
159
- this.attach();
180
+ innerScope.dispose();
181
+ }
182
+
183
+ /** Adds the inner observable of `value` to those merged. */
184
+ #project(value: A, updateToken: UpdateToken): void {
185
+ const innerScope = createScope(this.#ownerScope);
186
+
187
+ // Held before the projection runs, so that `teardown` disposes what it made
188
+ // even if it throws, or if `mergeMap` completes while the inner observable
189
+ // is being subscribed.
190
+ this.#mut_liveInners.set(innerScope, undefined);
191
+
192
+ const observable = innerScope.run(() => this.#mapToObservable(value));
193
+
194
+ const subscription = this.subscribeInner(observable, updateToken, () => {
195
+ this.#releaseInner(innerScope);
196
+ });
197
+
198
+ if (this.#mut_liveInners.has(innerScope)) {
199
+ this.#mut_liveInners.set(innerScope, subscription);
200
+ } else {
201
+ // Released while it was subscribed: the inner observable completed
202
+ // (`just(x)`), or `mergeMap` did.
203
+ subscription.unsubscribe();
204
+ }
160
205
  }
161
206
 
162
207
  override tryUpdate(updateToken: UpdateToken): void {
@@ -168,28 +213,13 @@ class MergeMapObservableClass<A, B>
168
213
  return; // skip update
169
214
  }
170
215
 
171
- const innerScope = createScope(this.#ownerScope);
172
-
173
- this.#mut_innerScopes = Arr.toPushed(this.#mut_innerScopes, innerScope);
174
-
175
- const observable = innerScope.run(() => this.#mapToObservable(sn.value));
176
-
177
- const subscription = this.subscribeInner(observable, updateToken);
178
-
179
- this.#mut_subscriptions = Arr.toPushed(
180
- this.#mut_subscriptions,
181
- subscription,
182
- );
216
+ this.#project(sn.value, updateToken);
183
217
  }
184
218
 
185
219
  /** Releases the inner observables, and only those this operator made. */
186
220
  protected override teardown(): void {
187
- for (const s of this.#mut_subscriptions) {
188
- s.unsubscribe();
189
- }
190
-
191
- for (const scope of this.#mut_innerScopes) {
192
- scope.dispose();
221
+ for (const innerScope of this.#mut_liveInners.keys()) {
222
+ this.#releaseInner(innerScope);
193
223
  }
194
224
  }
195
225
  }
@@ -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, pipe } from 'ts-data-forge';
8
+ import { Optional, asSafeUint, pipe, type SafeUint } from 'ts-data-forge';
9
9
  import { SyncChildObservableClass } from '../class/index.mjs';
10
10
  import type {
11
11
  DropInitialValueOperator,
@@ -93,16 +93,20 @@ class SkipWhileObservableClass<A>
93
93
  parentObservable: Observable<A>,
94
94
  predicate: (value: A, index: SafeUint | -1) => boolean,
95
95
  ) {
96
+ const initialValue = pipe(parentObservable.getSnapshot()).map((sn) =>
97
+ Optional.isNone(sn) || predicate(sn.value, -1) ? Optional.none : sn,
98
+ ).value;
99
+
96
100
  super({
97
101
  parents: [parentObservable],
98
- initialValue: pipe(parentObservable.getSnapshot()).map((sn) =>
99
- Optional.isNone(sn) || predicate(sn.value, -1) ? Optional.none : sn,
100
- ).value,
102
+ initialValue,
101
103
  });
102
104
 
103
105
  this.#mut_index = -1;
104
106
 
105
- this.#mut_isSkipping = true;
107
+ // A parent value the predicate let through has ended the skipping, as any
108
+ // later one would.
109
+ this.#mut_isSkipping = Optional.isNone(initialValue);
106
110
 
107
111
  this.#predicate = predicate;
108
112
 
@@ -118,10 +122,10 @@ class SkipWhileObservableClass<A>
118
122
  return; // skip update
119
123
  }
120
124
 
121
- this.#mut_index =
122
- this.#mut_index === -1 ? asSafeUint(0) : SafeUint.add(1, this.#mut_index);
125
+ this.#mut_index = asSafeUint(this.#mut_index + 1);
123
126
 
124
- if (!this.#predicate(sn.value, this.#mut_index)) {
127
+ // Once skipping has ended, the predicate is not asked again.
128
+ if (this.#mut_isSkipping && !this.#predicate(sn.value, this.#mut_index)) {
125
129
  this.#mut_isSkipping = false;
126
130
  }
127
131
 
@@ -1,4 +1,5 @@
1
1
  import { Optional } from 'ts-data-forge';
2
+ import { createState } from '../../utils/index.mjs';
2
3
  import { just } from '../create/index.mjs';
3
4
  import { skipWhile } from './skip-while.mjs';
4
5
 
@@ -17,4 +18,62 @@ describe(skipWhile, () => {
17
18
 
18
19
  assert.isTrue(Optional.isNone(dropped$.getSnapshot()));
19
20
  });
21
+
22
+ test('passes every later value once the initial value has ended the skipping', () => {
23
+ const [s$, setS] = createState(5);
24
+
25
+ const passed$ = s$.pipe(skipWhile((x) => x < 3));
26
+
27
+ const mut_history: number[] = [];
28
+
29
+ passed$.subscribe((x) => {
30
+ mut_history.push(x);
31
+ });
32
+
33
+ setS(1);
34
+
35
+ setS(2);
36
+
37
+ assert.deepStrictEqual(mut_history, [5, 1, 2]);
38
+ });
39
+
40
+ test('keeps skipping after an initial value the predicate skips', () => {
41
+ const [s$, setS] = createState(1);
42
+
43
+ const passed$ = s$.pipe(skipWhile((x) => x < 3));
44
+
45
+ const mut_history: number[] = [];
46
+
47
+ passed$.subscribe((x) => {
48
+ mut_history.push(x);
49
+ });
50
+
51
+ setS(2);
52
+
53
+ setS(5);
54
+
55
+ setS(1);
56
+
57
+ assert.deepStrictEqual(mut_history, [5, 1]);
58
+ });
59
+
60
+ test('asks the predicate no more once the initial value has ended the skipping', () => {
61
+ const [s$, setS] = createState(5);
62
+
63
+ let mut_calls = 0;
64
+
65
+ s$.pipe(
66
+ skipWhile((x) => {
67
+ mut_calls += 1;
68
+
69
+ return x < 3;
70
+ }),
71
+ ).subscribe(() => {});
72
+
73
+ setS(1);
74
+
75
+ setS(2);
76
+
77
+ assert.strictEqual(mut_calls, 1);
78
+ });
20
79
  });
@@ -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
  }