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
@@ -0,0 +1,269 @@
1
+ import { Optional } from 'ts-data-forge';
2
+ import { panic } from 'ts-std-forge';
3
+ import { toNode } from '../class/index.mjs';
4
+ import { just, source } from '../create/index.mjs';
5
+ import { onScopeDispose } from '../scope/index.mjs';
6
+ import type { Observable } from '../types/index.mjs';
7
+ import { filter } from './filter.mjs';
8
+ import { map } from './map.mjs';
9
+ import { mergeMap } from './merge-map.mjs';
10
+ import { skipWhile } from './skip-while.mjs';
11
+ import { switchMap } from './switch-map.mjs';
12
+
13
+ describe('the value the source holds when the operator is created is projected', () => {
14
+ test('switchMap: a sync inner gives the operator its value at once', () => {
15
+ const a$ = source<number>(1);
16
+
17
+ const s$ = a$.pipe(switchMap((x) => just(x * 10)));
18
+
19
+ assert.deepStrictEqual(s$.getSnapshot(), Optional.some(10));
20
+
21
+ const mut_history: number[] = [];
22
+
23
+ s$.subscribe((v) => {
24
+ mut_history.push(v);
25
+ });
26
+
27
+ a$.next(2);
28
+
29
+ assert.deepStrictEqual(mut_history, [10, 20]);
30
+ });
31
+
32
+ test('switchMap: a later value of the first inner is passed on', () => {
33
+ const a$ = source<number>(1);
34
+
35
+ const inner$ = source<string>();
36
+
37
+ const mut_projected: number[] = [];
38
+
39
+ const s$ = a$.pipe(
40
+ switchMap((x) => {
41
+ mut_projected.push(x);
42
+
43
+ return inner$;
44
+ }),
45
+ );
46
+
47
+ assert.deepStrictEqual(mut_projected, [1]);
48
+
49
+ const mut_history: string[] = [];
50
+
51
+ s$.subscribe((v) => {
52
+ mut_history.push(v);
53
+ });
54
+
55
+ inner$.next('a');
56
+
57
+ assert.deepStrictEqual(mut_history, ['a']);
58
+ });
59
+
60
+ test('switchMap: the next value of the source switches away from the first inner', () => {
61
+ const a$ = source<number>(1);
62
+
63
+ const first$ = source<string>();
64
+
65
+ const second$ = source<string>();
66
+
67
+ const s$ = a$.pipe(switchMap((x) => (x === 1 ? first$ : second$)));
68
+
69
+ const mut_history: string[] = [];
70
+
71
+ s$.subscribe((v) => {
72
+ mut_history.push(v);
73
+ });
74
+
75
+ a$.next(2);
76
+
77
+ first$.next('stale');
78
+
79
+ second$.next('b');
80
+
81
+ assert.deepStrictEqual(mut_history, ['b']);
82
+ });
83
+
84
+ test('switchMap: what the first projection created is disposed when it is switched away from', () => {
85
+ const a$ = source<number>(1);
86
+
87
+ const mut_disposed: number[] = [];
88
+
89
+ const s$ = a$.pipe(
90
+ switchMap((x) => {
91
+ onScopeDispose(() => {
92
+ mut_disposed.push(x);
93
+ });
94
+
95
+ return just(x);
96
+ }),
97
+ );
98
+
99
+ s$.subscribe(() => undefined);
100
+
101
+ assert.deepStrictEqual(mut_disposed, []);
102
+
103
+ a$.next(2);
104
+
105
+ assert.deepStrictEqual(mut_disposed, [1]);
106
+ });
107
+
108
+ test('switchMap: a source with no value projects nothing until it emits', () => {
109
+ const a$ = source<number>();
110
+
111
+ const mut_projected: number[] = [];
112
+
113
+ const s$ = a$.pipe(
114
+ switchMap((x) => {
115
+ mut_projected.push(x);
116
+
117
+ return just(x);
118
+ }),
119
+ );
120
+
121
+ assert.isTrue(Optional.isNone(s$.getSnapshot()));
122
+
123
+ a$.next(1);
124
+
125
+ assert.deepStrictEqual(mut_projected, [1]);
126
+ });
127
+
128
+ test('switchMap: a value carried through filter and map is projected', () => {
129
+ const a$ = source<Optional<number>>(Optional.some(42));
130
+
131
+ const s$ = a$
132
+ .pipe(filter(Optional.isSome))
133
+ .pipe(map((some) => some.value))
134
+ .pipe(switchMap((id) => just(`item ${id.toString()}`)));
135
+
136
+ assert.deepStrictEqual(s$.getSnapshot(), Optional.some('item 42'));
137
+ });
138
+
139
+ test('switchMap: a completed source projects nothing and completes', () => {
140
+ const a$ = just(1);
141
+
142
+ const mut_projected: number[] = [];
143
+
144
+ const s$ = a$.pipe(
145
+ switchMap((x) => {
146
+ mut_projected.push(x);
147
+
148
+ return just(x);
149
+ }),
150
+ );
151
+
152
+ assert.deepStrictEqual(mut_projected, []);
153
+
154
+ assert.isTrue(s$.isCompleted);
155
+ });
156
+
157
+ test('mergeMap: a completed source projects nothing and completes', () => {
158
+ const a$ = just(1);
159
+
160
+ const mut_projected: number[] = [];
161
+
162
+ const s$ = a$.pipe(
163
+ mergeMap((x) => {
164
+ mut_projected.push(x);
165
+
166
+ return just(x);
167
+ }),
168
+ );
169
+
170
+ assert.deepStrictEqual(mut_projected, []);
171
+
172
+ assert.isTrue(s$.isCompleted);
173
+ });
174
+
175
+ test('mergeMap: the first inner and the later ones are merged', () => {
176
+ const a$ = source<number>(1);
177
+
178
+ const first$ = source<string>();
179
+
180
+ const second$ = source<string>();
181
+
182
+ const s$ = a$.pipe(mergeMap((x) => (x === 1 ? first$ : second$)));
183
+
184
+ const mut_history: string[] = [];
185
+
186
+ s$.subscribe((v) => {
187
+ mut_history.push(v);
188
+ });
189
+
190
+ a$.next(2);
191
+
192
+ first$.next('a');
193
+
194
+ second$.next('b');
195
+
196
+ assert.deepStrictEqual(mut_history, ['a', 'b']);
197
+ });
198
+
199
+ test('mergeMap: a sync inner gives the operator its value at once', () => {
200
+ const a$ = source<number>(1);
201
+
202
+ const s$ = a$.pipe(mergeMap((x) => just(x * 10)));
203
+
204
+ assert.deepStrictEqual(s$.getSnapshot(), Optional.some(10));
205
+ });
206
+
207
+ test('switchMap: a first projection that throws leaves nothing attached to the source', () => {
208
+ const a$ = source<number>(1);
209
+
210
+ const mut_projected: number[] = [];
211
+
212
+ assert.throws(() => {
213
+ a$.pipe(
214
+ switchMap((x) => {
215
+ mut_projected.push(x);
216
+
217
+ // Made in the projection's scope before it throws.
218
+ a$.pipe(map((v) => v + 1));
219
+
220
+ if (x === 1) {
221
+ panic('first');
222
+ }
223
+
224
+ return just(x);
225
+ }),
226
+ );
227
+ }, /first/u);
228
+
229
+ assert.isFalse(toNode(a$).hasChild);
230
+
231
+ a$.next(2);
232
+
233
+ assert.deepStrictEqual(mut_projected, [1]);
234
+ });
235
+
236
+ test('mergeMap: a first projection that throws leaves nothing attached to the source', () => {
237
+ const a$ = source<number>(1);
238
+
239
+ assert.throws(() => {
240
+ a$.pipe(
241
+ mergeMap((): Observable<number> => {
242
+ a$.pipe(map((v) => v + 1));
243
+
244
+ panic('first');
245
+ }),
246
+ );
247
+ }, /first/u);
248
+
249
+ assert.isFalse(toNode(a$).hasChild);
250
+ });
251
+
252
+ test('switchMap: skipWhile on the held value before it keeps that value from being projected', () => {
253
+ const a$ = source<number>(1);
254
+
255
+ const mut_projected: number[] = [];
256
+
257
+ a$.pipe(skipWhile((_, index) => index === -1)).pipe(
258
+ switchMap((x) => {
259
+ mut_projected.push(x);
260
+
261
+ return just(x);
262
+ }),
263
+ );
264
+
265
+ a$.next(2);
266
+
267
+ assert.deepStrictEqual(mut_projected, [2]);
268
+ });
269
+ });
@@ -24,7 +24,8 @@ describe('an inner observable that holds a value is passed on in the update that
24
24
 
25
25
  a$.next(3);
26
26
 
27
- assert.deepStrictEqual(mut_history, ['m2,s2', 'm3,s3']);
27
+ // 'm1,s1' from the value `a$` held when the operators were created.
28
+ assert.deepStrictEqual(mut_history, ['m1,s1', 'm2,s2', 'm3,s3']);
28
29
  });
29
30
 
30
31
  test('switchMap: a child of both it and its parent emits once per update', () => {
@@ -43,6 +44,7 @@ describe('an inner observable that holds a value is passed on in the update that
43
44
  a$.next(3);
44
45
 
45
46
  assert.deepStrictEqual(mut_history, [
47
+ [1, 10],
46
48
  [2, 20],
47
49
  [3, 30],
48
50
  ]);
@@ -85,6 +87,6 @@ describe('an inner observable that holds a value is passed on in the update that
85
87
 
86
88
  a$.next(2);
87
89
 
88
- assert.deepStrictEqual(mut_history, ['m2,s2']);
90
+ assert.deepStrictEqual(mut_history, ['m1,s1', 'm2,s2']);
89
91
  });
90
92
  });
@@ -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
  });