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.
- package/README.md +3 -3
- package/dist/core/authoring.d.mts +3 -3
- package/dist/core/class/child-observable-class.d.mts +40 -27
- package/dist/core/class/child-observable-class.d.mts.map +1 -1
- package/dist/core/class/child-observable-class.mjs +78 -46
- package/dist/core/class/child-observable-class.mjs.map +1 -1
- package/dist/core/class/create-child-observable.d.mts +28 -30
- 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 +12 -12
- 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/manager-observable-class.d.mts +6 -6
- package/dist/core/class/manager-observable-class.mjs.map +1 -1
- package/dist/core/class/observable-base-class.d.mts +29 -31
- package/dist/core/class/observable-base-class.d.mts.map +1 -1
- package/dist/core/class/observable-base-class.mjs +0 -2
- package/dist/core/class/observable-base-class.mjs.map +1 -1
- package/dist/core/class/observable-node.d.mts +3 -4
- package/dist/core/class/observable-node.d.mts.map +1 -1
- package/dist/core/class/observable-node.mjs +0 -1
- package/dist/core/class/observable-node.mjs.map +1 -1
- package/dist/core/class/propagation-state.d.mts +6 -8
- package/dist/core/class/propagation-state.d.mts.map +1 -1
- package/dist/core/class/propagation-state.mjs +1 -3
- package/dist/core/class/propagation-state.mjs.map +1 -1
- package/dist/core/class/root-observable-class.d.mts +4 -4
- package/dist/core/combine/derive.d.mts +7 -8
- package/dist/core/combine/derive.d.mts.map +1 -1
- package/dist/core/combine/derive.mjs +2 -2
- package/dist/core/combine/derive.mjs.map +1 -1
- package/dist/core/combine/merge.mjs.map +1 -1
- package/dist/core/combine/zip.d.mts +3 -3
- 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.d.mts +3 -3
- 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/from-subscribable.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/create/timer.mjs.map +1 -1
- package/dist/core/operators/audit.mjs.map +1 -1
- package/dist/core/operators/debounce.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/keep-equal-parts.d.mts +14 -15
- package/dist/core/operators/keep-equal-parts.d.mts.map +1 -1
- package/dist/core/operators/keep-equal-parts.mjs +0 -1
- package/dist/core/operators/keep-equal-parts.mjs.map +1 -1
- package/dist/core/operators/map.mjs.map +1 -1
- package/dist/core/operators/merge-map.d.mts +7 -3
- 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/pairwise.mjs.map +1 -1
- package/dist/core/operators/scan.mjs.map +1 -1
- package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
- package/dist/core/operators/skip-until.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-until.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/take.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/operators/with-current-value-from.mjs.map +1 -1
- package/dist/core/operators/with-initial-value.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/map-to.d.mts +3 -4
- package/dist/core/predefined/operators/map-to.d.mts.map +1 -1
- package/dist/core/predefined/operators/map-to.mjs +0 -1
- package/dist/core/predefined/operators/map-to.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/scope/scope.d.mts +4 -4
- package/dist/core/types/observable-family.d.mts.map +1 -1
- package/dist/core/types/observable-family.mjs.map +1 -1
- package/dist/core/types/observable.d.mts +5 -5
- package/dist/core/types/observable.mjs.map +1 -1
- package/dist/core/utils/current-scope.d.mts +6 -6
- package/dist/core/utils/id-maker.d.mts +2 -2
- 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/observable-brand.d.mts +5 -5
- 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/dist/utils/create-boolean-state.d.mts +2 -2
- package/dist/utils/create-reducer.d.mts +18 -18
- package/dist/utils/create-state.d.mts +2 -2
- package/dist/utils/create-state.mjs.map +1 -1
- package/dist/utils/state-source.d.mts +3 -3
- package/package.json +12 -11
- package/src/core/authoring.mts +3 -3
- package/src/core/class/child-observable-class.mts +131 -90
- package/src/core/class/circular-dependency-comparison.test.mts +2 -2
- package/src/core/class/create-child-observable.mts +35 -34
- package/src/core/class/create-root-observable.mts +13 -12
- package/src/core/class/manager-observable-class.mts +12 -12
- package/src/core/class/observable-base-class.mts +33 -35
- package/src/core/class/observable-node.mts +3 -4
- package/src/core/class/propagation-state.mts +27 -29
- package/src/core/class/remove-completed.test.mts +2 -2
- package/src/core/class/root-observable-class.mts +9 -9
- package/src/core/class/unobserved-write.test.mts +4 -4
- package/src/core/class/write-during-propagation.test.mts +75 -4
- package/src/core/combine/derive.mts +12 -12
- package/src/core/combine/merge.mts +2 -2
- package/src/core/combine/zip.mts +12 -9
- package/src/core/create/counter.mts +7 -7
- package/src/core/create/from-abortable-promise.mts +35 -25
- package/src/core/create/from-promise.mts +30 -20
- package/src/core/create/from-promise.test.mts +97 -0
- package/src/core/create/from-subscribable.mts +2 -2
- package/src/core/create/source.mts +7 -6
- package/src/core/create/timer.mts +2 -2
- package/src/core/operators/audit.mts +2 -2
- package/src/core/operators/debounce.mts +2 -2
- package/src/core/operators/filter.mts +6 -7
- 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/keep-equal-parts.mts +16 -17
- package/src/core/operators/map.mts +2 -2
- package/src/core/operators/merge-map.mts +59 -29
- package/src/core/operators/pairwise.mts +4 -4
- package/src/core/operators/scan.mts +2 -2
- package/src/core/operators/skip-if-no-change.mts +4 -4
- package/src/core/operators/skip-until.mts +4 -4
- package/src/core/operators/skip-while.mts +14 -10
- package/src/core/operators/skip-while.test.mts +59 -0
- package/src/core/operators/switch-map.mts +26 -15
- package/src/core/operators/take-until.mts +2 -2
- package/src/core/operators/take-while.mts +17 -19
- package/src/core/operators/take-while.test.mts +51 -0
- package/src/core/operators/take.mts +2 -2
- package/src/core/operators/throttle.mts +6 -5
- package/src/core/operators/with-buffered-from.mts +18 -5
- package/src/core/operators/with-buffered-from.test.mts +27 -0
- package/src/core/operators/with-current-value-from.mts +2 -2
- package/src/core/operators/with-initial-value.mts +2 -2
- package/src/core/predefined/operators/attach-index.mts +4 -5
- package/src/core/predefined/operators/map-to.mts +3 -4
- package/src/core/predefined/operators/skip.mts +2 -2
- package/src/core/scope/scope.mts +6 -6
- package/src/core/types/observable-family.mts +4 -5
- package/src/core/types/observable.mts +7 -7
- package/src/core/utils/current-scope.mts +8 -8
- package/src/core/utils/id-maker.mts +2 -2
- package/src/core/utils/index.mts +1 -0
- package/src/core/utils/observable-brand.mts +5 -5
- package/src/core/utils/rethrow-out-of-promise.mts +16 -0
- package/src/utils/create-boolean-state.mts +2 -2
- package/src/utils/create-reducer.mts +18 -18
- package/src/utils/create-state.mts +4 -4
- package/src/utils/create-state.test.mts +2 -2
- 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
|
-
|
|
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
|
+
});
|
|
@@ -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
|
-
//
|
|
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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
*
|
|
31
|
-
* `createReducer` does it for a state written whole, and `replaceEqualDeep`
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
-
* //
|
|
56
|
-
* //
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 {
|
|
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
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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,
|
|
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
|
});
|
|
@@ -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
|
-
//
|
|
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
|
-
*
|
|
131
|
-
*
|
|
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.
|
|
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 {
|
|
@@ -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
|
-
//
|
|
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
|