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.
- package/dist/core/class/child-observable-class.d.mts +16 -14
- package/dist/core/class/child-observable-class.d.mts.map +1 -1
- package/dist/core/class/child-observable-class.mjs +39 -12
- 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 +2 -0
- package/dist/core/utils/index.d.mts.map +1 -1
- package/dist/core/utils/index.mjs +2 -0
- package/dist/core/utils/index.mjs.map +1 -1
- package/dist/core/utils/is-development-build.d.mts +15 -0
- package/dist/core/utils/is-development-build.d.mts.map +1 -0
- package/dist/core/utils/is-development-build.mjs +23 -0
- package/dist/core/utils/is-development-build.mjs.map +1 -0
- 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 +53 -17
- package/src/core/class/circular-dependency-comparison.test.mts +36 -40
- package/src/core/class/circular-dependency.test.mts +6 -37
- 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 +2 -0
- package/src/core/utils/is-development-build.mts +32 -0
- package/src/core/utils/is-development-build.test.mts +44 -0
- package/src/core/utils/rethrow-out-of-promise.mts +16 -0
|
@@ -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
|
});
|
|
@@ -21,6 +21,10 @@ import type {
|
|
|
21
21
|
* Projects each source value to an observable, subscribes to it, and emits its values.
|
|
22
22
|
* When a new value arrives from the source, the previous inner observable is unsubscribed.
|
|
23
23
|
*
|
|
24
|
+
* A value the source already holds when `switchMap` is created (a state, an
|
|
25
|
+
* observable with an initial value) is projected at once, as `map` and `filter`
|
|
26
|
+
* start from it. A completed source is not projected.
|
|
27
|
+
*
|
|
24
28
|
* The projection runs in a scope of its own (see `createScope`): what it
|
|
25
29
|
* creates is disposed when the next value switches away from it and when
|
|
26
30
|
* `switchMap` completes. An observable it only returns — one created outside
|
|
@@ -152,7 +156,9 @@ class SwitchMapObservableClass<A, B>
|
|
|
152
156
|
|
|
153
157
|
this.#mut_subscription = undefined;
|
|
154
158
|
|
|
155
|
-
this.
|
|
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.#
|
|
175
|
-
|
|
176
|
-
const innerScope = createScope(this.#ownerScope);
|
|
177
|
-
|
|
178
|
-
this.#mut_innerScope = innerScope;
|
|
179
|
-
|
|
180
|
-
const observable = innerScope.run(() => this.#mapToObservable(sn.value));
|
|
181
|
-
|
|
182
|
-
this.#mut_subscription = this.subscribeInner(observable, updateToken);
|
|
193
|
+
this.#project(sn.value, updateToken);
|
|
183
194
|
}
|
|
184
195
|
|
|
185
196
|
protected override teardown(): void {
|
|
@@ -5,13 +5,7 @@
|
|
|
5
5
|
// @sumi-expect-error-file banned-syntax/no-this
|
|
6
6
|
// @sumi-expect-error-file functions/prefer-arrow-function
|
|
7
7
|
// @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
|
|
8
|
-
import {
|
|
9
|
-
Optional,
|
|
10
|
-
SafeUint,
|
|
11
|
-
asSafeUint,
|
|
12
|
-
expectType,
|
|
13
|
-
pipe,
|
|
14
|
-
} from 'ts-data-forge';
|
|
8
|
+
import { Optional, asSafeUint, expectType, type SafeUint } from 'ts-data-forge';
|
|
15
9
|
import { SyncChildObservableClass } from '../class/index.mjs';
|
|
16
10
|
import { source } from '../create/index.mjs';
|
|
17
11
|
import type {
|
|
@@ -73,7 +67,7 @@ import { withInitialValue } from './with-initial-value.mjs';
|
|
|
73
67
|
*
|
|
74
68
|
* assert.deepStrictEqual(valueHistory, [1, 2, 3, 4]);
|
|
75
69
|
*
|
|
76
|
-
* num$.next(1); //
|
|
70
|
+
* num$.next(1); // nothing logged (already completed)
|
|
77
71
|
*
|
|
78
72
|
* assert.deepStrictEqual(valueHistory, [1, 2, 3, 4]);
|
|
79
73
|
* ```
|
|
@@ -98,21 +92,26 @@ class TakeWhileObservableClass<A>
|
|
|
98
92
|
parentObservable: Observable<A>,
|
|
99
93
|
predicate: (value: A, index: SafeUint | -1) => boolean,
|
|
100
94
|
) {
|
|
95
|
+
const parentSnapshot = parentObservable.getSnapshot();
|
|
96
|
+
|
|
97
|
+
const isTaking =
|
|
98
|
+
Optional.isNone(parentSnapshot) || predicate(parentSnapshot.value, -1);
|
|
99
|
+
|
|
101
100
|
super({
|
|
102
101
|
parents: [parentObservable],
|
|
103
|
-
initialValue:
|
|
104
|
-
Optional.isNone(par)
|
|
105
|
-
? Optional.none
|
|
106
|
-
: predicate(par.value, -1)
|
|
107
|
-
? par
|
|
108
|
-
: Optional.none,
|
|
109
|
-
).value,
|
|
102
|
+
initialValue: isTaking ? parentSnapshot : Optional.none,
|
|
110
103
|
});
|
|
111
104
|
|
|
112
105
|
this.#mut_index = -1;
|
|
113
106
|
|
|
114
107
|
this.#predicate = predicate;
|
|
115
108
|
|
|
109
|
+
// A parent value the predicate rejects ends it, as any later one would;
|
|
110
|
+
// `attach` then leaves it out of the graph.
|
|
111
|
+
if (!isTaking) {
|
|
112
|
+
this.complete();
|
|
113
|
+
}
|
|
114
|
+
|
|
116
115
|
this.attach();
|
|
117
116
|
}
|
|
118
117
|
|
|
@@ -125,8 +124,7 @@ class TakeWhileObservableClass<A>
|
|
|
125
124
|
return; // skip update
|
|
126
125
|
}
|
|
127
126
|
|
|
128
|
-
this.#mut_index =
|
|
129
|
-
this.#mut_index === -1 ? asSafeUint(0) : SafeUint.add(1, this.#mut_index);
|
|
127
|
+
this.#mut_index = asSafeUint(this.#mut_index + 1);
|
|
130
128
|
|
|
131
129
|
if (this.#predicate(sn.value, this.#mut_index)) {
|
|
132
130
|
this.setNext(sn.value, updateToken);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createState } from '../../utils/index.mjs';
|
|
2
|
+
import { takeWhile } from './take-while.mjs';
|
|
3
|
+
|
|
4
|
+
describe(takeWhile, () => {
|
|
5
|
+
test('completes at once when the initial value fails the predicate', () => {
|
|
6
|
+
const [s$, setS] = createState(5);
|
|
7
|
+
|
|
8
|
+
const taken$ = s$.pipe(takeWhile((x) => x < 3));
|
|
9
|
+
|
|
10
|
+
const mut_events: string[] = [];
|
|
11
|
+
|
|
12
|
+
taken$.subscribe(
|
|
13
|
+
(x) => {
|
|
14
|
+
mut_events.push(`next ${x}`);
|
|
15
|
+
},
|
|
16
|
+
() => {
|
|
17
|
+
mut_events.push('complete');
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
assert.isTrue(taken$.isCompleted);
|
|
22
|
+
|
|
23
|
+
setS(1);
|
|
24
|
+
|
|
25
|
+
assert.deepStrictEqual(mut_events, ['complete']);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('keeps taking after an initial value that passes the predicate', () => {
|
|
29
|
+
const [s$, setS] = createState(1);
|
|
30
|
+
|
|
31
|
+
const taken$ = s$.pipe(takeWhile((x) => x < 3));
|
|
32
|
+
|
|
33
|
+
const mut_history: number[] = [];
|
|
34
|
+
|
|
35
|
+
taken$.subscribe((x) => {
|
|
36
|
+
mut_history.push(x);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
setS(2);
|
|
40
|
+
|
|
41
|
+
assert.isFalse(taken$.isCompleted);
|
|
42
|
+
|
|
43
|
+
setS(5);
|
|
44
|
+
|
|
45
|
+
assert.isTrue(taken$.isCompleted);
|
|
46
|
+
|
|
47
|
+
setS(1);
|
|
48
|
+
|
|
49
|
+
assert.deepStrictEqual(mut_history, [1, 2]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -139,14 +139,15 @@ class ThrottleObservableClass<A>
|
|
|
139
139
|
return; // skip update
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
// The window opens before `setNext`, which can lead back here: a value a
|
|
143
|
+
// subscriber writes to the parent falls inside it.
|
|
144
144
|
this.#mut_isSkipping = true;
|
|
145
145
|
|
|
146
|
-
// set timer
|
|
147
146
|
this.#mut_timerId = setTimeout(() => {
|
|
148
147
|
this.#mut_isSkipping = false;
|
|
149
148
|
}, this.#milliSeconds);
|
|
149
|
+
|
|
150
|
+
this.setNext(sn.value, updateToken);
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
protected override teardown(): void {
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// @sumi-expect-error-file banned-syntax/no-this
|
|
6
6
|
// @sumi-expect-error-file functions/prefer-arrow-function
|
|
7
7
|
// @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
|
|
8
|
-
import {
|
|
8
|
+
import { Optional, pipe } from 'ts-data-forge';
|
|
9
|
+
import * as Arr from 'ts-data-forge/array';
|
|
9
10
|
import { SyncChildObservableClass } from '../class/index.mjs';
|
|
10
11
|
import type {
|
|
11
12
|
KeepInitialValueOperator,
|
|
@@ -93,11 +94,13 @@ class WithBufferedFromObservableClass<A, B>
|
|
|
93
94
|
#mut_bufferedValues: readonly B[];
|
|
94
95
|
|
|
95
96
|
constructor(parentObservable: Observable<A>, observable: Observable<B>) {
|
|
97
|
+
const parentSnapshot = parentObservable.getSnapshot();
|
|
98
|
+
|
|
96
99
|
super({
|
|
97
100
|
parents: [parentObservable],
|
|
98
101
|
depth: 1 + maxDepth([parentObservable, observable]),
|
|
99
102
|
initialValue: pipe({
|
|
100
|
-
par:
|
|
103
|
+
par: parentSnapshot,
|
|
101
104
|
me: observable.getSnapshot(),
|
|
102
105
|
}).map(({ par, me }) =>
|
|
103
106
|
Optional.isNone(par)
|
|
@@ -115,6 +118,12 @@ class WithBufferedFromObservableClass<A, B>
|
|
|
115
118
|
this.#mut_bufferedValues = Arr.toPushed(this.#mut_bufferedValues, value);
|
|
116
119
|
});
|
|
117
120
|
|
|
121
|
+
// `subscribe` has just pushed the buffered observable's current value,
|
|
122
|
+
// which the initial tuple already carries.
|
|
123
|
+
if (Optional.isSome(parentSnapshot)) {
|
|
124
|
+
this.#mut_bufferedValues = [];
|
|
125
|
+
}
|
|
126
|
+
|
|
118
127
|
this.attach();
|
|
119
128
|
}
|
|
120
129
|
|
|
@@ -127,9 +136,13 @@ class WithBufferedFromObservableClass<A, B>
|
|
|
127
136
|
return; // skip update
|
|
128
137
|
}
|
|
129
138
|
|
|
130
|
-
|
|
139
|
+
const buffered = this.#mut_bufferedValues;
|
|
131
140
|
|
|
141
|
+
// Emptied before `setNext`, which can lead back here: a value a subscriber
|
|
142
|
+
// writes to the buffered observable belongs to the next batch.
|
|
132
143
|
this.#mut_bufferedValues = [];
|
|
144
|
+
|
|
145
|
+
this.setNext([sn.value, buffered], updateToken);
|
|
133
146
|
}
|
|
134
147
|
|
|
135
148
|
protected override teardown(): void {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createState } from '../../utils/index.mjs';
|
|
1
2
|
import { toNode } from '../class/index.mjs';
|
|
2
3
|
import { source } from '../create/index.mjs';
|
|
3
4
|
import { withBufferedFrom } from './with-buffered-from.mjs';
|
|
@@ -55,3 +56,29 @@ describe('withBufferedFrom teardown', () => {
|
|
|
55
56
|
assert.deepStrictEqual(mut_history, [[1, ['A', 'B']]]);
|
|
56
57
|
});
|
|
57
58
|
});
|
|
59
|
+
|
|
60
|
+
describe('withBufferedFrom initial value', () => {
|
|
61
|
+
test('reports the current value of the buffered observable once when both hold one (#2767)', () => {
|
|
62
|
+
const [data$] = createState('x');
|
|
63
|
+
|
|
64
|
+
const [trigger$, setTrigger] = createState(0);
|
|
65
|
+
|
|
66
|
+
const result$ = trigger$.pipe(withBufferedFrom(data$));
|
|
67
|
+
|
|
68
|
+
const mut_history: (readonly [number, readonly string[]])[] = [];
|
|
69
|
+
|
|
70
|
+
result$.subscribe((v) => {
|
|
71
|
+
mut_history.push(v);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
setTrigger(1);
|
|
75
|
+
|
|
76
|
+
setTrigger(2);
|
|
77
|
+
|
|
78
|
+
assert.deepStrictEqual(mut_history, [
|
|
79
|
+
[0, ['x']],
|
|
80
|
+
[1, []],
|
|
81
|
+
[2, []],
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// @sumi-expect-error-file banned-syntax/no-this
|
|
6
6
|
// @sumi-expect-error-file functions/prefer-arrow-function
|
|
7
7
|
// @sumi-expect-error-file mutation/no-mutation-without-mut-prefix
|
|
8
|
-
import { Optional,
|
|
8
|
+
import { Optional, asSafeUint, type SafeUint } from 'ts-data-forge';
|
|
9
9
|
import { SyncChildObservableClass } from '../../class/index.mjs';
|
|
10
10
|
import type {
|
|
11
11
|
KeepInitialValueOperator,
|
|
@@ -112,8 +112,7 @@ class WithIndexObservableClass<A> extends SyncChildObservableClass<
|
|
|
112
112
|
return; // skip update
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
this.#mut_index =
|
|
116
|
-
this.#mut_index === -1 ? asSafeUint(0) : SafeUint.add(1, this.#mut_index);
|
|
115
|
+
this.#mut_index = asSafeUint(this.#mut_index + 1);
|
|
117
116
|
|
|
118
117
|
this.setNext([this.#mut_index, sn.value], updateToken);
|
|
119
118
|
}
|