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.
- package/README.md +1 -1
- package/dist/core/class/child-observable-class.d.mts +16 -2
- package/dist/core/class/child-observable-class.d.mts.map +1 -1
- package/dist/core/class/child-observable-class.mjs +37 -4
- package/dist/core/class/child-observable-class.mjs.map +1 -1
- package/dist/core/class/create-child-observable.d.mts +1 -1
- package/dist/core/class/create-child-observable.d.mts.map +1 -1
- package/dist/core/class/create-child-observable.mjs +6 -8
- package/dist/core/class/create-child-observable.mjs.map +1 -1
- package/dist/core/class/create-root-observable.d.mts +1 -1
- package/dist/core/class/create-root-observable.d.mts.map +1 -1
- package/dist/core/class/create-root-observable.mjs +1 -5
- package/dist/core/class/create-root-observable.mjs.map +1 -1
- package/dist/core/class/propagation-state.mjs +1 -1
- package/dist/core/class/propagation-state.mjs.map +1 -1
- package/dist/core/combine/derive.d.mts.map +1 -1
- package/dist/core/combine/derive.mjs +2 -1
- package/dist/core/combine/derive.mjs.map +1 -1
- package/dist/core/combine/zip.d.mts.map +1 -1
- package/dist/core/combine/zip.mjs +5 -2
- package/dist/core/combine/zip.mjs.map +1 -1
- package/dist/core/create/counter.mjs +2 -2
- package/dist/core/create/counter.mjs.map +1 -1
- package/dist/core/create/from-abortable-promise.d.mts.map +1 -1
- package/dist/core/create/from-abortable-promise.mjs +10 -3
- package/dist/core/create/from-abortable-promise.mjs.map +1 -1
- package/dist/core/create/from-promise.d.mts.map +1 -1
- package/dist/core/create/from-promise.mjs +10 -3
- package/dist/core/create/from-promise.mjs.map +1 -1
- package/dist/core/create/source.d.mts.map +1 -1
- package/dist/core/create/source.mjs +2 -1
- package/dist/core/create/source.mjs.map +1 -1
- package/dist/core/operators/filter.d.mts +1 -1
- package/dist/core/operators/filter.d.mts.map +1 -1
- package/dist/core/operators/filter.mjs +2 -3
- package/dist/core/operators/filter.mjs.map +1 -1
- package/dist/core/operators/merge-map.d.mts +6 -2
- package/dist/core/operators/merge-map.d.mts.map +1 -1
- package/dist/core/operators/merge-map.mjs +47 -16
- package/dist/core/operators/merge-map.mjs.map +1 -1
- package/dist/core/operators/skip-while.d.mts +1 -1
- package/dist/core/operators/skip-while.d.mts.map +1 -1
- package/dist/core/operators/skip-while.mjs +9 -6
- package/dist/core/operators/skip-while.mjs.map +1 -1
- package/dist/core/operators/switch-map.d.mts +4 -0
- package/dist/core/operators/switch-map.d.mts.map +1 -1
- package/dist/core/operators/switch-map.mjs +16 -6
- package/dist/core/operators/switch-map.mjs.map +1 -1
- package/dist/core/operators/take-while.d.mts +2 -2
- package/dist/core/operators/take-while.d.mts.map +1 -1
- package/dist/core/operators/take-while.mjs +10 -8
- package/dist/core/operators/take-while.mjs.map +1 -1
- package/dist/core/operators/throttle.mjs +3 -2
- package/dist/core/operators/throttle.mjs.map +1 -1
- package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
- package/dist/core/operators/with-buffered-from.mjs +13 -3
- package/dist/core/operators/with-buffered-from.mjs.map +1 -1
- package/dist/core/predefined/operators/attach-index.d.mts +1 -1
- package/dist/core/predefined/operators/attach-index.d.mts.map +1 -1
- package/dist/core/predefined/operators/attach-index.mjs +2 -3
- package/dist/core/predefined/operators/attach-index.mjs.map +1 -1
- package/dist/core/predefined/operators/skip.d.mts.map +1 -1
- package/dist/core/predefined/operators/skip.mjs +2 -2
- package/dist/core/predefined/operators/skip.mjs.map +1 -1
- package/dist/core/utils/index.d.mts +1 -0
- package/dist/core/utils/index.d.mts.map +1 -1
- package/dist/core/utils/index.mjs +1 -0
- package/dist/core/utils/index.mjs.map +1 -1
- package/dist/core/utils/rethrow-out-of-promise.d.mts +10 -0
- package/dist/core/utils/rethrow-out-of-promise.d.mts.map +1 -0
- package/dist/core/utils/rethrow-out-of-promise.mjs +17 -0
- package/dist/core/utils/rethrow-out-of-promise.mjs.map +1 -0
- package/package.json +12 -11
- package/src/core/class/child-observable-class.mts +45 -3
- package/src/core/class/create-child-observable.mts +8 -5
- package/src/core/class/create-root-observable.mts +2 -1
- package/src/core/class/propagation-state.mts +1 -1
- package/src/core/class/write-during-propagation.test.mts +75 -4
- package/src/core/combine/derive.mts +2 -1
- package/src/core/combine/zip.mts +5 -2
- package/src/core/create/counter.mts +2 -2
- package/src/core/create/from-abortable-promise.mts +33 -23
- package/src/core/create/from-promise.mts +28 -18
- package/src/core/create/from-promise.test.mts +97 -0
- package/src/core/create/source.mts +2 -1
- package/src/core/operators/filter.mts +2 -3
- package/src/core/operators/flat-map-initial-value.test.mts +269 -0
- package/src/core/operators/flat-map-sync-inner.test.mts +4 -2
- package/src/core/operators/inner-ownership.test.mts +145 -2
- package/src/core/operators/merge-map.mts +56 -26
- package/src/core/operators/skip-while.mts +12 -8
- package/src/core/operators/skip-while.test.mts +59 -0
- package/src/core/operators/switch-map.mts +21 -10
- package/src/core/operators/take-while.mts +15 -17
- package/src/core/operators/take-while.test.mts +51 -0
- package/src/core/operators/throttle.mts +4 -3
- package/src/core/operators/with-buffered-from.mts +16 -3
- package/src/core/operators/with-buffered-from.test.mts +27 -0
- package/src/core/predefined/operators/attach-index.mts +2 -3
- package/src/core/predefined/operators/skip.mts +2 -2
- package/src/core/utils/index.mts +1 -0
- 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
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
|
26
|
-
*
|
|
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
|
-
|
|
140
|
-
|
|
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.#
|
|
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.#
|
|
178
|
+
this.#mut_liveInners.delete(innerScope);
|
|
158
179
|
|
|
159
|
-
|
|
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
|
-
|
|
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
|
|
188
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|