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
@@ -8,6 +8,7 @@
8
8
  import { Optional, Result } from 'ts-data-forge';
9
9
  import { RootObservableClass } from '../class/index.mjs';
10
10
  import type { FromPromiseObservable } from '../types/index.mjs';
11
+ import { rethrowOutOfPromise } from '../utils/index.mjs';
11
12
 
12
13
  /**
13
14
  * Creates an observable from a Promise factory that receives an `AbortSignal`.
@@ -96,35 +97,44 @@ class FromAbortablePromiseObservableClass<A, E = unknown>
96
97
 
97
98
  const promise = factory(this.#abortController.signal);
98
99
 
100
+ // `onRejected` beside `onFulfilled`, so that only the promise's own
101
+ // rejection becomes an Err: what a subscriber throws on the Ok value is not
102
+ // the promise's (#2931). A `.catch` after `.then` would take both, and is
103
+ // also a step later.
99
104
  promise
100
- .then((value) => {
101
- if (this.isCompleted) {
102
- return;
103
- }
105
+ // eslint-disable-next-line unicorn/prefer-then-catch -- its suggestion, `.then(onFulfilled).catch(onRejected)`, would take what a subscriber throws too
106
+ .then(
107
+ (value) => {
108
+ if (this.isCompleted) {
109
+ return;
110
+ }
104
111
 
105
- this.startUpdate(Result.ok(value));
106
- })
107
- .catch((error: unknown) => {
108
- if (this.isCompleted) {
109
- return;
110
- }
112
+ this.startUpdate(Result.ok(value));
113
+ },
114
+ (error: unknown) => {
115
+ if (this.isCompleted) {
116
+ return;
117
+ }
111
118
 
112
- // Silently ignore AbortError — it means the observable was
113
- // intentionally completed (e.g., by switchMap).
114
- if (error instanceof DOMException && error.name === 'AbortError') {
115
- return;
116
- }
119
+ // Silently ignore AbortError — it means the observable was
120
+ // intentionally completed (e.g., by switchMap).
121
+ if (error instanceof DOMException && error.name === 'AbortError') {
122
+ return;
123
+ }
117
124
 
118
- this.startUpdate(
119
- Result.err(
120
- // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
121
- error as E,
122
- ),
123
- );
124
- })
125
+ this.startUpdate(
126
+ Result.err(
127
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
128
+ error as E,
129
+ ),
130
+ );
131
+ },
132
+ )
133
+ .catch(rethrowOutOfPromise)
125
134
  .finally(() => {
126
135
  this.complete();
127
- });
136
+ })
137
+ .catch(rethrowOutOfPromise);
128
138
  }
129
139
 
130
140
  protected override teardown(): void {
@@ -7,6 +7,7 @@
7
7
  import { Optional, Result } from 'ts-data-forge';
8
8
  import { RootObservableClass } from '../class/index.mjs';
9
9
  import type { FromPromiseObservable } from '../types/index.mjs';
10
+ import { rethrowOutOfPromise } from '../utils/index.mjs';
10
11
 
11
12
  /**
12
13
  * Creates an observable from a Promise.
@@ -66,28 +67,37 @@ class FromPromiseObservableClass<A, E = unknown>
66
67
  constructor(promise: Promise<A>) {
67
68
  super({ initialValue: Optional.none });
68
69
 
70
+ // `onRejected` beside `onFulfilled`, so that only the promise's own
71
+ // rejection becomes an Err: what a subscriber throws on the Ok value is not
72
+ // the promise's (#2931). A `.catch` after `.then` would take both, and is
73
+ // also a step later.
69
74
  promise
70
- .then((value) => {
71
- if (this.isCompleted) {
72
- return;
73
- }
75
+ // eslint-disable-next-line unicorn/prefer-then-catch -- its suggestion, `.then(onFulfilled).catch(onRejected)`, would take what a subscriber throws too
76
+ .then(
77
+ (value) => {
78
+ if (this.isCompleted) {
79
+ return;
80
+ }
74
81
 
75
- this.startUpdate(Result.ok(value));
76
- })
77
- .catch((error: unknown) => {
78
- if (this.isCompleted) {
79
- return;
80
- }
82
+ this.startUpdate(Result.ok(value));
83
+ },
84
+ (error: unknown) => {
85
+ if (this.isCompleted) {
86
+ return;
87
+ }
81
88
 
82
- this.startUpdate(
83
- Result.err(
84
- // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
85
- error as E,
86
- ),
87
- );
88
- })
89
+ this.startUpdate(
90
+ Result.err(
91
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
92
+ error as E,
93
+ ),
94
+ );
95
+ },
96
+ )
97
+ .catch(rethrowOutOfPromise)
89
98
  .finally(() => {
90
99
  this.complete();
91
- });
100
+ })
101
+ .catch(rethrowOutOfPromise);
92
102
  }
93
103
  }
@@ -0,0 +1,97 @@
1
+ import { Optional, Result } from 'ts-data-forge';
2
+ import { panic } from 'ts-std-forge';
3
+ import { fromAbortablePromise } from './from-abortable-promise.mjs';
4
+ import { fromPromise } from './from-promise.mjs';
5
+
6
+ describe('fromPromise and fromAbortablePromise report only the promise’s own rejection as an Err (#2931)', () => {
7
+ test.each([
8
+ ['fromPromise', () => fromPromise(Promise.resolve(1))],
9
+ [
10
+ 'fromAbortablePromise',
11
+ () => fromAbortablePromise(() => Promise.resolve(1)),
12
+ ],
13
+ ] as const)(
14
+ '%s: a subscriber that throws on the Ok value gets no Err',
15
+ async (_, create) => {
16
+ vi.useFakeTimers();
17
+
18
+ onTestFinished(() => {
19
+ vi.useRealTimers();
20
+ });
21
+
22
+ const mut_events: string[] = [];
23
+
24
+ const result$ = create();
25
+
26
+ result$.subscribe(
27
+ (r) => {
28
+ mut_events.push(Result.isOk(r) ? `ok ${r.value}` : 'err');
29
+
30
+ if (Result.isOk(r)) {
31
+ panic('bug in a subscriber');
32
+ }
33
+ },
34
+ () => {
35
+ mut_events.push('complete');
36
+ },
37
+ );
38
+
39
+ await settlePromises();
40
+
41
+ assert.deepStrictEqual(mut_events, ['ok 1', 'complete']);
42
+
43
+ // The subscriber's exception is not swallowed: it is thrown again from a
44
+ // task of its own.
45
+ expect(() => {
46
+ vi.runOnlyPendingTimers();
47
+ }).toThrow('bug in a subscriber');
48
+ },
49
+ );
50
+
51
+ const rejection = new Error('rejected');
52
+
53
+ test.each([
54
+ ['fromPromise', () => fromPromise(Promise.reject(rejection))],
55
+ [
56
+ 'fromAbortablePromise',
57
+ () => fromAbortablePromise(() => Promise.reject(rejection)),
58
+ ],
59
+ ] as const)('%s: a rejection is an Err', async (_, create) => {
60
+ const result$ = create();
61
+
62
+ const mut_events: string[] = [];
63
+
64
+ result$.subscribe(
65
+ (r) => {
66
+ mut_events.push(
67
+ Result.isErr(r) && r.value === rejection ? 'err rejected' : 'ok',
68
+ );
69
+ },
70
+ () => {
71
+ mut_events.push('complete');
72
+ },
73
+ );
74
+
75
+ await settlePromises();
76
+
77
+ assert.deepStrictEqual(mut_events, ['err rejected', 'complete']);
78
+ });
79
+
80
+ test('fromPromise holds the value by the time an await of the same promise resumes', async () => {
81
+ const promise = Promise.resolve(1);
82
+
83
+ const result$ = fromPromise(promise);
84
+
85
+ await promise;
86
+
87
+ assert.deepStrictEqual(result$.getSnapshot(), Optional.some(Result.ok(1)));
88
+ });
89
+ });
90
+
91
+ /** Lets the promise chains already started run to their end. */
92
+ const settlePromises = async (): Promise<void> => {
93
+ for (const _ of Array.from({ length: 10 })) {
94
+ // eslint-disable-next-line no-await-in-loop -- each await lets the chains take one more step
95
+ await Promise.resolve();
96
+ }
97
+ };
@@ -4,7 +4,8 @@
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
- import { Arr, Optional } from 'ts-data-forge';
7
+ import { Optional } from 'ts-data-forge';
8
+ import * as Arr from 'ts-data-forge/array';
8
9
  import { RootObservableClass } from '../class/index.mjs';
9
10
  import type {
10
11
  InitializedSourceObservable,
@@ -12,7 +12,7 @@ import {
12
12
  isString,
13
13
  Optional,
14
14
  pipe,
15
- SafeUint,
15
+ type SafeUint,
16
16
  } from 'ts-data-forge';
17
17
  import { SyncChildObservableClass } from '../class/index.mjs';
18
18
  import { source } from '../create/index.mjs';
@@ -132,8 +132,7 @@ class FilterObservableClass<A>
132
132
  return; // skip update
133
133
  }
134
134
 
135
- this.#mut_index =
136
- this.#mut_index === -1 ? asSafeUint(0) : SafeUint.add(1, this.#mut_index);
135
+ this.#mut_index = asSafeUint(this.#mut_index + 1);
137
136
 
138
137
  if (this.#predicate(sn.value, this.#mut_index)) {
139
138
  this.setNext(sn.value, updateToken);
@@ -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
  });