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
@@ -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
+ });
@@ -1,6 +1,6 @@
1
1
  // Sumi bans classes, `this`, method syntax and writes to `this` outside a
2
- // `mut_` binding; an observable is a class (`ObservableBaseClass` says why),
3
- // so those rules are waived here (D-63).
2
+ // `mut_` binding; an observable is a class (`ObservableBaseClass` says why), so
3
+ // those rules are waived here (D-63).
4
4
  // @sumi-expect-error-file classes/no-class
5
5
  // @sumi-expect-error-file banned-syntax/no-this
6
6
  // @sumi-expect-error-file functions/prefer-arrow-function
@@ -20,19 +20,19 @@ import type {
20
20
 
21
21
  /**
22
22
  * Passes on each value with every part deeply equal to the same part of the
23
- * value passed on before replaced by that part, and skips a value deeply
24
- * equal to it altogether (structural sharing). A reader of a part that did
25
- * not change keeps the object it has, so `skipIfNoChange()` with `Object.is`
26
- * stops it. Superset of `skipIfNoChange(fastDeepEqual)`.
23
+ * value passed on before replaced by that part, and skips a value deeply equal
24
+ * to it altogether (structural sharing). A reader of a part that did not change
25
+ * keeps the object it has, so `skipIfNoChange()` with `Object.is` stops it.
26
+ * Superset of `skipIfNoChange(fastDeepEqual)`.
27
27
  *
28
28
  * **Prefer keeping references where the value is written** — compare before
29
- * `setState`, and skip it when nothing changed — so that nothing downstream
30
- * has anything to undo. The `keepEqualParts` option of `createState` and
31
- * `createReducer` does it for a state written whole, and `replaceEqualDeep`
32
- * of ts-data-forge does it for any other write. This operator lets every
33
- * update that changes nothing reach the source and everything up to it, and
34
- * walks the whole value again on each one; reach for it when the source is
35
- * not yours to change (a stream that arrives whole from outside).
29
+ * `setState`, and skip it when nothing changed — so that nothing downstream has
30
+ * anything to undo. The `keepEqualParts` option of `createState` and
31
+ * `createReducer` does it for a state written whole, and `replaceEqualDeep` of
32
+ * ts-data-forge does it for any other write. This operator lets every update
33
+ * that changes nothing reach the source and everything up to it, and walks the
34
+ * whole value again on each one; reach for it when the source is not yours to
35
+ * change (a stream that arrives whole from outside).
36
36
  *
37
37
  * Array items are matched by index; pass `{ key }` to match them by key, an
38
38
  * item for which `key` returns `undefined` being matched by index. Only plain
@@ -51,10 +51,9 @@ import type {
51
51
  * cards: { id: number; text: string }[];
52
52
  * }>;
53
53
  *
54
- * // A stream that arrives whole from outside (a WebSocket, say), each
55
- * // message a new object even when nothing changed. A state you write
56
- * // yourself takes `createState(initial, { keepEqualParts: true })`
57
- * // instead.
54
+ * // A stream that arrives whole from outside (a WebSocket, say), each message
55
+ * // a new object even when nothing changed. A state you write yourself takes
56
+ * // `createState(initial, { keepEqualParts: true })` instead.
58
57
  * const message$ = source<Board>();
59
58
  *
60
59
  * const board$ = message$.pipe(keepEqualParts());
@@ -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 } 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,10 +22,14 @@ 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
26
- * returns — one created outside the projection and shared with others — is
27
- * unsubscribed and left running.
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
32
+ * and left running.
28
33
  *
29
34
  * @template A - The type of values from the source
30
35
  * @template B - The type of values from the projected observable
@@ -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
  }
@@ -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
@@ -90,8 +90,8 @@ class PairwiseObservableClass<A>
90
90
  initialValue: Optional.none,
91
91
  });
92
92
 
93
- // parentObservable.snapshot has value
94
- // if parentObservable is InitializedObservable
93
+ // parentObservable.snapshot has value if parentObservable is
94
+ // InitializedObservable
95
95
  this.#mut_previousValue = parentObservable.getSnapshot();
96
96
 
97
97
  this.attach();
@@ -1,6 +1,6 @@
1
1
  // Sumi bans classes, `this`, method syntax and writes to `this` outside a
2
- // `mut_` binding; an observable is a class (`ObservableBaseClass` says why),
3
- // so those rules are waived here (D-63).
2
+ // `mut_` binding; an observable is a class (`ObservableBaseClass` says why), so
3
+ // those rules are waived here (D-63).
4
4
  // @sumi-expect-error-file classes/no-class
5
5
  // @sumi-expect-error-file banned-syntax/no-this
6
6
  // @sumi-expect-error-file functions/prefer-arrow-function
@@ -1,6 +1,6 @@
1
1
  // Sumi bans classes, `this`, method syntax and writes to `this` outside a
2
- // `mut_` binding; an observable is a class (`ObservableBaseClass` says why),
3
- // so those rules are waived here (D-63).
2
+ // `mut_` binding; an observable is a class (`ObservableBaseClass` says why), so
3
+ // those rules are waived here (D-63).
4
4
  // @sumi-expect-error-file classes/no-class
5
5
  // @sumi-expect-error-file banned-syntax/no-this
6
6
  // @sumi-expect-error-file functions/prefer-arrow-function
@@ -104,8 +104,8 @@ class SkipIfNoChangeObservableClass<A>
104
104
  initialValue: parentObservable.getSnapshot(),
105
105
  });
106
106
 
107
- // parentObservable.snapshot has value
108
- // if parentObservable is InitializedObservable
107
+ // parentObservable.snapshot has value if parentObservable is
108
+ // InitializedObservable
109
109
  this.#mut_previousValue = parentObservable.getSnapshot();
110
110
 
111
111
  this.#eq = eq;
@@ -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
@@ -83,8 +83,8 @@ class SkipUntilObservableClass<A>
83
83
  initialValue: Optional.none,
84
84
  });
85
85
 
86
- // A notifier that already holds a value, or has completed, would stop
87
- // the skipping inside `subscribe`, before the subscription is stored.
86
+ // A notifier that already holds a value, or has completed, would stop the
87
+ // skipping inside `subscribe`, before the subscription is stored.
88
88
  this.#mut_isSkipping =
89
89
  Optional.isNone(notifier.getSnapshot()) && !notifier.isCompleted;
90
90
 
@@ -1,11 +1,11 @@
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 { 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
  });
@@ -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
@@ -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
@@ -126,9 +130,9 @@ class SwitchMapObservableClass<A, B>
126
130
  readonly #mapToObservable: (curr: A) => Observable<B>;
127
131
 
128
132
  /**
129
- * The scope the operator was created in, so that each inner scope is
130
- * disposed with it; the projection itself runs during propagation, when the
131
- * current scope is whatever happens to be running.
133
+ * The scope the operator was created in, so that each inner scope is disposed
134
+ * with it; the projection itself runs during propagation, when the current
135
+ * scope is whatever happens to be running.
132
136
  */
133
137
  readonly #ownerScope: Scope | undefined;
134
138
 
@@ -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 {
@@ -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