synstate 3.2.1 → 3.4.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 +2 -2
- package/dist/core/base/create-child-observable.d.mts +9 -2
- package/dist/core/base/create-child-observable.d.mts.map +1 -1
- package/dist/core/base/create-child-observable.mjs +49 -11
- package/dist/core/base/create-child-observable.mjs.map +1 -1
- package/dist/core/base/create-observable-base.d.mts +5 -1
- package/dist/core/base/create-observable-base.d.mts.map +1 -1
- package/dist/core/base/create-observable-base.mjs +24 -4
- package/dist/core/base/create-observable-base.mjs.map +1 -1
- package/dist/core/base/create-root-observable.d.mts +9 -0
- package/dist/core/base/create-root-observable.d.mts.map +1 -1
- package/dist/core/base/create-root-observable.mjs +32 -3
- package/dist/core/base/create-root-observable.mjs.map +1 -1
- package/dist/core/combine/merge.d.mts +0 -3
- package/dist/core/combine/merge.d.mts.map +1 -1
- package/dist/core/combine/merge.mjs +0 -3
- package/dist/core/combine/merge.mjs.map +1 -1
- package/dist/core/combine/zip.d.mts +4 -2
- package/dist/core/combine/zip.d.mts.map +1 -1
- package/dist/core/combine/zip.mjs +21 -3
- package/dist/core/combine/zip.mjs.map +1 -1
- package/dist/core/create/counter.d.mts +9 -3
- package/dist/core/create/counter.d.mts.map +1 -1
- package/dist/core/create/counter.mjs +16 -3
- package/dist/core/create/counter.mjs.map +1 -1
- package/dist/core/create/from-subscribable.d.mts.map +1 -1
- package/dist/core/create/from-subscribable.mjs +34 -11
- package/dist/core/create/from-subscribable.mjs.map +1 -1
- package/dist/core/create/source.d.mts +7 -0
- package/dist/core/create/source.d.mts.map +1 -1
- package/dist/core/create/source.mjs +6 -2
- package/dist/core/create/source.mjs.map +1 -1
- package/dist/core/operators/merge-map.d.mts +0 -3
- package/dist/core/operators/merge-map.d.mts.map +1 -1
- package/dist/core/operators/merge-map.mjs +0 -3
- package/dist/core/operators/merge-map.mjs.map +1 -1
- package/dist/core/operators/skip-until.d.mts.map +1 -1
- package/dist/core/operators/skip-until.mjs +15 -6
- package/dist/core/operators/skip-until.mjs.map +1 -1
- package/dist/core/operators/switch-map.d.mts +0 -3
- package/dist/core/operators/switch-map.d.mts.map +1 -1
- package/dist/core/operators/switch-map.mjs +0 -3
- package/dist/core/operators/switch-map.mjs.map +1 -1
- package/dist/core/operators/take-until.d.mts.map +1 -1
- package/dist/core/operators/take-until.mjs +13 -1
- package/dist/core/operators/take-until.mjs.map +1 -1
- package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
- package/dist/core/operators/with-buffered-from.mjs +12 -3
- package/dist/core/operators/with-buffered-from.mjs.map +1 -1
- package/dist/core/types/observable-family.d.mts +5 -1
- 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 +17 -0
- package/dist/core/types/observable.d.mts.map +1 -1
- package/dist/core/types/observable.mjs.map +1 -1
- package/dist/utils/create-event-emitter.d.mts +2 -2
- package/dist/utils/create-reducer.d.mts +10 -1
- package/dist/utils/create-reducer.d.mts.map +1 -1
- package/dist/utils/create-reducer.mjs +13 -5
- package/dist/utils/create-reducer.mjs.map +1 -1
- package/dist/utils/create-state.d.mts +7 -0
- package/dist/utils/create-state.d.mts.map +1 -1
- package/dist/utils/create-state.mjs +7 -0
- package/dist/utils/create-state.mjs.map +1 -1
- package/package.json +4 -4
- package/src/core/base/create-child-observable.mts +68 -10
- package/src/core/base/create-observable-base.mts +31 -3
- package/src/core/base/create-root-observable.mts +43 -2
- package/src/core/base/write-during-propagation.test.mts +234 -0
- package/src/core/combine/merge.mts +0 -3
- package/src/core/combine/zip.mts +36 -5
- package/src/core/create/counter.mts +27 -6
- package/src/core/create/counter.test.mts +82 -0
- package/src/core/create/from-subscribable.mts +33 -5
- package/src/core/create/from-subscribable.test.mts +129 -0
- package/src/core/create/source.mts +18 -3
- package/src/core/create/source.test.mts +41 -0
- package/src/core/operators/merge-map.mts +0 -3
- package/src/core/operators/skip-until.mts +21 -9
- package/src/core/operators/switch-map.mts +0 -3
- package/src/core/operators/take-until-skip-until-teardown.test.mts +127 -0
- package/src/core/operators/take-until.mts +15 -1
- package/src/core/operators/with-buffered-from.mts +15 -3
- package/src/core/operators/with-buffered-from.test.mts +56 -0
- package/src/core/types/observable-family.mts +5 -1
- package/src/core/types/observable.mts +19 -0
- package/src/utils/create-event-emitter.mts +2 -2
- package/src/utils/create-reducer.mts +14 -8
- package/src/utils/create-reducer.test.mts +12 -0
- package/src/utils/create-state.mts +7 -0
- package/src/utils/create-state.test.mts +20 -0
|
@@ -43,6 +43,13 @@ import type {
|
|
|
43
43
|
*
|
|
44
44
|
* assert.deepStrictEqual(valueHistory, [1, 2, 3]);
|
|
45
45
|
* ```
|
|
46
|
+
*
|
|
47
|
+
* @note Do not call `next` from a subscriber or a projection while another
|
|
48
|
+
* update is propagating: the write propagates immediately, so subscribers can
|
|
49
|
+
* see an intermediate state, and it logs a `console.warn` (once per
|
|
50
|
+
* observable). Derive the value from its source instead —
|
|
51
|
+
* `const b$ = a$.pipe(map((a) => a * 2))` rather than
|
|
52
|
+
* `a$.subscribe((a) => { setB(a * 2); })`.
|
|
46
53
|
*/
|
|
47
54
|
export function source<const A>(
|
|
48
55
|
initialValue: A,
|
|
@@ -51,19 +58,27 @@ export function source<const A>(
|
|
|
51
58
|
export function source<const A>(): SourceObservable<A>;
|
|
52
59
|
|
|
53
60
|
export function source<const A>(...args: readonly A[]): SourceObservable<A> {
|
|
54
|
-
return createRootObservable<
|
|
61
|
+
return createRootObservable<
|
|
62
|
+
A,
|
|
63
|
+
Readonly<{ next: (nextValue: A) => A | undefined }>
|
|
64
|
+
>(
|
|
55
65
|
{
|
|
56
66
|
initialValue: Arr.isNonEmpty(args)
|
|
57
67
|
? Optional.some(args[0])
|
|
58
68
|
: Optional.none,
|
|
59
69
|
},
|
|
60
|
-
({ startUpdate, isCompleted }) => ({
|
|
70
|
+
({ startUpdate, getSnapshot, isCompleted }) => ({
|
|
71
|
+
// Returns the value held after the call: `nextValue`, or, once
|
|
72
|
+
// completed, the value already there — `undefined` only on a source
|
|
73
|
+
// that never had one, whose `next` is typed `void`.
|
|
61
74
|
next: (nextValue: A) => {
|
|
62
75
|
if (isCompleted()) {
|
|
63
|
-
return;
|
|
76
|
+
return Optional.unwrap(getSnapshot());
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
startUpdate(nextValue);
|
|
80
|
+
|
|
81
|
+
return nextValue;
|
|
67
82
|
},
|
|
68
83
|
}),
|
|
69
84
|
);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { expectType } from 'ts-data-forge';
|
|
2
|
+
import type { SourceObservable } from '../types/index.mjs';
|
|
3
|
+
import { source } from './source.mjs';
|
|
4
|
+
|
|
5
|
+
describe(source, () => {
|
|
6
|
+
test('`next` returns the value it passed on', () => {
|
|
7
|
+
const count$ = source<number>(0);
|
|
8
|
+
|
|
9
|
+
assert.strictEqual(count$.next(1), 1);
|
|
10
|
+
|
|
11
|
+
assert.strictEqual(count$.getSnapshot().value, 1);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('once completed, `next` passes nothing on and returns the value held', () => {
|
|
15
|
+
const count$ = source<number>(0);
|
|
16
|
+
|
|
17
|
+
const mut_seen: number[] = [];
|
|
18
|
+
|
|
19
|
+
count$.subscribe((value) => {
|
|
20
|
+
mut_seen.push(value);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
count$.next(1);
|
|
24
|
+
|
|
25
|
+
count$.complete();
|
|
26
|
+
|
|
27
|
+
assert.strictEqual(count$.next(2), 1);
|
|
28
|
+
|
|
29
|
+
assert.deepStrictEqual(mut_seen, [0, 1]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('`next` returns the value only on a source with an initial value', () => {
|
|
33
|
+
const passed = source<number>(0).next(1);
|
|
34
|
+
|
|
35
|
+
expectType<typeof passed, number>('=');
|
|
36
|
+
|
|
37
|
+
assert.strictEqual(passed, 1);
|
|
38
|
+
|
|
39
|
+
expectType<SourceObservable<number>['next'], (value: number) => void>('=');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -98,9 +98,6 @@ import type {
|
|
|
98
98
|
* 'C3',
|
|
99
99
|
* ]);
|
|
100
100
|
* ```
|
|
101
|
-
*
|
|
102
|
-
* @note To improve code readability, consider using `createState` instead of `mergeMap`,
|
|
103
|
-
* subscribing to `parentObservable` and calling `setState` within it.
|
|
104
101
|
*/
|
|
105
102
|
export const mergeMap =
|
|
106
103
|
<A, B>(
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
DropInitialValueOperator,
|
|
5
5
|
Observable,
|
|
6
6
|
SkipUntilOperatorObservable,
|
|
7
|
+
Subscription,
|
|
7
8
|
} from '../types/index.mjs';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -65,12 +66,28 @@ const createSkipUntilObservable = <A,>(
|
|
|
65
66
|
parentObservable: Observable<A>,
|
|
66
67
|
notifier: Observable<unknown>,
|
|
67
68
|
): SkipUntilOperatorObservable<A> => {
|
|
68
|
-
|
|
69
|
+
// A notifier that already holds a value, or has completed, would call
|
|
70
|
+
// `stopSkipping` inside `subscribe`, before the subscription is stored.
|
|
71
|
+
let mut_isSkipping =
|
|
72
|
+
Optional.isNone(notifier.getSnapshot()) && !notifier.isCompleted;
|
|
73
|
+
|
|
74
|
+
let mut_subscription: Subscription | undefined = undefined;
|
|
75
|
+
|
|
76
|
+
const stopSkipping = (): void => {
|
|
77
|
+
mut_isSkipping = false;
|
|
78
|
+
|
|
79
|
+
mut_subscription?.unsubscribe();
|
|
80
|
+
|
|
81
|
+
mut_subscription = undefined;
|
|
82
|
+
};
|
|
69
83
|
|
|
70
84
|
const observable = createSyncChildObservable<A, readonly [A]>(
|
|
71
85
|
{
|
|
72
86
|
parents: [parentObservable],
|
|
73
87
|
initialValue: Optional.none,
|
|
88
|
+
onComplete: () => {
|
|
89
|
+
mut_subscription?.unsubscribe();
|
|
90
|
+
},
|
|
74
91
|
},
|
|
75
92
|
({ setNext }) =>
|
|
76
93
|
(updateToken) => {
|
|
@@ -88,14 +105,9 @@ const createSkipUntilObservable = <A,>(
|
|
|
88
105
|
},
|
|
89
106
|
);
|
|
90
107
|
|
|
91
|
-
|
|
92
|
-
()
|
|
93
|
-
|
|
94
|
-
},
|
|
95
|
-
() => {
|
|
96
|
-
mut_isSkipping = false;
|
|
97
|
-
},
|
|
98
|
-
);
|
|
108
|
+
if (mut_isSkipping) {
|
|
109
|
+
mut_subscription = notifier.subscribe(stopSkipping, stopSkipping);
|
|
110
|
+
}
|
|
99
111
|
|
|
100
112
|
return observable;
|
|
101
113
|
};
|
|
@@ -96,9 +96,6 @@ import type {
|
|
|
96
96
|
* 'C3',
|
|
97
97
|
* ]);
|
|
98
98
|
* ```
|
|
99
|
-
*
|
|
100
|
-
* @note To improve code readability, consider using `createState` instead of `switchMap`,
|
|
101
|
-
* subscribe to `parentObservable` and call `setState` within it.
|
|
102
99
|
*/
|
|
103
100
|
export const switchMap =
|
|
104
101
|
<A, B>(
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { source } from '../create/index.mjs';
|
|
2
|
+
import { take } from '../predefined/index.mjs';
|
|
3
|
+
import { skipUntil } from './skip-until.mjs';
|
|
4
|
+
import { takeUntil } from './take-until.mjs';
|
|
5
|
+
|
|
6
|
+
describe('takeUntil releases the notifier', () => {
|
|
7
|
+
test('when the result is completed before the notifier emits', () => {
|
|
8
|
+
const notifier$ = source<undefined>();
|
|
9
|
+
|
|
10
|
+
const result$ = source<number>().pipe(takeUntil(notifier$));
|
|
11
|
+
|
|
12
|
+
assert.isTrue(notifier$.hasSubscriber);
|
|
13
|
+
|
|
14
|
+
result$.complete();
|
|
15
|
+
|
|
16
|
+
assert.isFalse(notifier$.hasSubscriber);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('when the parent completes', () => {
|
|
20
|
+
const notifier$ = source<undefined>();
|
|
21
|
+
|
|
22
|
+
const num$ = source<number>();
|
|
23
|
+
|
|
24
|
+
num$.pipe(takeUntil(notifier$));
|
|
25
|
+
|
|
26
|
+
num$.complete();
|
|
27
|
+
|
|
28
|
+
assert.isFalse(notifier$.hasSubscriber);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('when the downstream completes', () => {
|
|
32
|
+
const notifier$ = source<undefined>();
|
|
33
|
+
|
|
34
|
+
const num$ = source<number>();
|
|
35
|
+
|
|
36
|
+
const taken$ = num$.pipe(takeUntil(notifier$)).pipe(take(1));
|
|
37
|
+
|
|
38
|
+
taken$.subscribe(() => {});
|
|
39
|
+
|
|
40
|
+
// `take(n)` completes on the emission after its `n`th.
|
|
41
|
+
num$.next(1);
|
|
42
|
+
|
|
43
|
+
num$.next(2);
|
|
44
|
+
|
|
45
|
+
assert.isTrue(taken$.isCompleted);
|
|
46
|
+
|
|
47
|
+
assert.isFalse(notifier$.hasSubscriber);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('when the notifier emits', () => {
|
|
51
|
+
const notifier$ = source<undefined>();
|
|
52
|
+
|
|
53
|
+
const result$ = source<number>().pipe(takeUntil(notifier$));
|
|
54
|
+
|
|
55
|
+
notifier$.next(undefined);
|
|
56
|
+
|
|
57
|
+
assert.isTrue(result$.isCompleted);
|
|
58
|
+
|
|
59
|
+
assert.isFalse(notifier$.hasSubscriber);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('when the notifier has already emitted', () => {
|
|
63
|
+
const notifier$ = source<number>(0);
|
|
64
|
+
|
|
65
|
+
const result$ = source<number>().pipe(takeUntil(notifier$));
|
|
66
|
+
|
|
67
|
+
assert.isTrue(result$.isCompleted);
|
|
68
|
+
|
|
69
|
+
assert.isFalse(notifier$.hasSubscriber);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('skipUntil releases the notifier', () => {
|
|
74
|
+
test('when the notifier emits', () => {
|
|
75
|
+
const notifier$ = source<undefined>();
|
|
76
|
+
|
|
77
|
+
const num$ = source<number>();
|
|
78
|
+
|
|
79
|
+
const result$ = num$.pipe(skipUntil(notifier$));
|
|
80
|
+
|
|
81
|
+
const mut_history: number[] = [];
|
|
82
|
+
|
|
83
|
+
result$.subscribe((x) => {
|
|
84
|
+
mut_history.push(x);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
num$.next(1);
|
|
88
|
+
|
|
89
|
+
notifier$.next(undefined);
|
|
90
|
+
|
|
91
|
+
assert.isFalse(notifier$.hasSubscriber);
|
|
92
|
+
|
|
93
|
+
num$.next(2);
|
|
94
|
+
|
|
95
|
+
assert.deepStrictEqual(mut_history, [2]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('when the notifier has already emitted', () => {
|
|
99
|
+
const notifier$ = source<number>(0);
|
|
100
|
+
|
|
101
|
+
const num$ = source<number>();
|
|
102
|
+
|
|
103
|
+
const result$ = num$.pipe(skipUntil(notifier$));
|
|
104
|
+
|
|
105
|
+
const mut_history: number[] = [];
|
|
106
|
+
|
|
107
|
+
result$.subscribe((x) => {
|
|
108
|
+
mut_history.push(x);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
assert.isFalse(notifier$.hasSubscriber);
|
|
112
|
+
|
|
113
|
+
num$.next(1);
|
|
114
|
+
|
|
115
|
+
assert.deepStrictEqual(mut_history, [1]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('when the result is completed before the notifier emits', () => {
|
|
119
|
+
const notifier$ = source<undefined>();
|
|
120
|
+
|
|
121
|
+
const result$ = source<number>().pipe(skipUntil(notifier$));
|
|
122
|
+
|
|
123
|
+
result$.complete();
|
|
124
|
+
|
|
125
|
+
assert.isFalse(notifier$.hasSubscriber);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -3,6 +3,7 @@ import { createSyncChildObservable } from '../base/index.mjs';
|
|
|
3
3
|
import type {
|
|
4
4
|
KeepInitialValueOperator,
|
|
5
5
|
Observable,
|
|
6
|
+
Subscription,
|
|
6
7
|
TakeUntilOperatorObservable,
|
|
7
8
|
} from '../types/index.mjs';
|
|
8
9
|
|
|
@@ -68,10 +69,15 @@ const createTakeUntilObservable = <A,>(
|
|
|
68
69
|
parentObservable: Observable<A>,
|
|
69
70
|
notifier: Observable<unknown>,
|
|
70
71
|
): TakeUntilOperatorObservable<A> => {
|
|
72
|
+
let mut_subscription: Subscription | undefined = undefined;
|
|
73
|
+
|
|
71
74
|
const observable = createSyncChildObservable<A, readonly [A]>(
|
|
72
75
|
{
|
|
73
76
|
parents: [parentObservable],
|
|
74
77
|
initialValue: parentObservable.getSnapshot(),
|
|
78
|
+
onComplete: () => {
|
|
79
|
+
mut_subscription?.unsubscribe();
|
|
80
|
+
},
|
|
75
81
|
},
|
|
76
82
|
({ setNext }) =>
|
|
77
83
|
(updateToken) => {
|
|
@@ -88,7 +94,7 @@ const createTakeUntilObservable = <A,>(
|
|
|
88
94
|
},
|
|
89
95
|
);
|
|
90
96
|
|
|
91
|
-
notifier.subscribe(
|
|
97
|
+
const subscription = notifier.subscribe(
|
|
92
98
|
() => {
|
|
93
99
|
observable.complete();
|
|
94
100
|
},
|
|
@@ -97,5 +103,13 @@ const createTakeUntilObservable = <A,>(
|
|
|
97
103
|
},
|
|
98
104
|
);
|
|
99
105
|
|
|
106
|
+
// A notifier that already holds a value completes `observable` inside
|
|
107
|
+
// `subscribe`, before `onComplete` can see the subscription.
|
|
108
|
+
if (observable.isCompleted) {
|
|
109
|
+
subscription.unsubscribe();
|
|
110
|
+
} else {
|
|
111
|
+
mut_subscription = subscription;
|
|
112
|
+
}
|
|
113
|
+
|
|
100
114
|
return observable;
|
|
101
115
|
};
|
|
@@ -3,6 +3,7 @@ import { createSyncChildObservable } from '../base/index.mjs';
|
|
|
3
3
|
import type {
|
|
4
4
|
KeepInitialValueOperator,
|
|
5
5
|
Observable,
|
|
6
|
+
Subscription,
|
|
6
7
|
WithBufferedFromOperatorObservable,
|
|
7
8
|
} from '../types/index.mjs';
|
|
8
9
|
import { maxDepth } from '../utils/index.mjs';
|
|
@@ -86,6 +87,8 @@ const createWithBufferedFromObservable = <A, B>(
|
|
|
86
87
|
mut_bufferedValues = [];
|
|
87
88
|
};
|
|
88
89
|
|
|
90
|
+
let mut_subscription: Subscription | undefined = undefined;
|
|
91
|
+
|
|
89
92
|
const result = createSyncChildObservable<
|
|
90
93
|
readonly [A, readonly B[]],
|
|
91
94
|
readonly [A]
|
|
@@ -104,6 +107,11 @@ const createWithBufferedFromObservable = <A, B>(
|
|
|
104
107
|
Optional.isNone(me) ? [] : [me.value],
|
|
105
108
|
] as const),
|
|
106
109
|
).value,
|
|
110
|
+
onComplete: () => {
|
|
111
|
+
mut_subscription?.unsubscribe();
|
|
112
|
+
|
|
113
|
+
clearBuffer();
|
|
114
|
+
},
|
|
107
115
|
},
|
|
108
116
|
({ setNext }) =>
|
|
109
117
|
(updateToken) => {
|
|
@@ -122,9 +130,13 @@ const createWithBufferedFromObservable = <A, B>(
|
|
|
122
130
|
},
|
|
123
131
|
);
|
|
124
132
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
// A child completed at creation has already run `onComplete`; subscribing
|
|
134
|
+
// after it would leave a subscription nothing releases.
|
|
135
|
+
if (!result.isCompleted) {
|
|
136
|
+
mut_subscription = observable.subscribe((value) => {
|
|
137
|
+
mut_bufferedValues = Arr.toPushed(mut_bufferedValues, value);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
128
140
|
|
|
129
141
|
return result;
|
|
130
142
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { source } from '../create/index.mjs';
|
|
2
|
+
import { withBufferedFrom } from './with-buffered-from.mjs';
|
|
3
|
+
|
|
4
|
+
describe('withBufferedFrom teardown', () => {
|
|
5
|
+
test('unsubscribes from the buffered observable on complete', () => {
|
|
6
|
+
const data$ = source<string>();
|
|
7
|
+
|
|
8
|
+
const trigger$ = source<number>();
|
|
9
|
+
|
|
10
|
+
const result$ = trigger$.pipe(withBufferedFrom(data$));
|
|
11
|
+
|
|
12
|
+
assert.isTrue(data$.hasSubscriber);
|
|
13
|
+
|
|
14
|
+
result$.complete();
|
|
15
|
+
|
|
16
|
+
assert.isTrue(result$.isCompleted);
|
|
17
|
+
|
|
18
|
+
assert.isFalse(data$.hasSubscriber);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('unsubscribes from the buffered observable when the parent completes', () => {
|
|
22
|
+
const data$ = source<string>();
|
|
23
|
+
|
|
24
|
+
const trigger$ = source<number>();
|
|
25
|
+
|
|
26
|
+
const result$ = trigger$.pipe(withBufferedFrom(data$));
|
|
27
|
+
|
|
28
|
+
trigger$.complete();
|
|
29
|
+
|
|
30
|
+
assert.isTrue(result$.isCompleted);
|
|
31
|
+
|
|
32
|
+
assert.isFalse(data$.hasSubscriber);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('keeps buffering while subscribed', () => {
|
|
36
|
+
const data$ = source<string>();
|
|
37
|
+
|
|
38
|
+
const trigger$ = source<number>();
|
|
39
|
+
|
|
40
|
+
const result$ = trigger$.pipe(withBufferedFrom(data$));
|
|
41
|
+
|
|
42
|
+
const mut_history: (readonly [number, readonly string[]])[] = [];
|
|
43
|
+
|
|
44
|
+
result$.subscribe((v) => {
|
|
45
|
+
mut_history.push(v);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
data$.next('A');
|
|
49
|
+
|
|
50
|
+
data$.next('B');
|
|
51
|
+
|
|
52
|
+
trigger$.next(1);
|
|
53
|
+
|
|
54
|
+
assert.deepStrictEqual(mut_history, [[1, ['A', 'B']]]);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -20,7 +20,11 @@ import type { NonEmptyUnknownList } from './types.mjs';
|
|
|
20
20
|
// RootObservable
|
|
21
21
|
|
|
22
22
|
export type InitializedSourceObservable<A> = Readonly<{
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Passes `value` on and returns it. Once completed, passes nothing on and
|
|
25
|
+
* returns the value held.
|
|
26
|
+
*/
|
|
27
|
+
next: (value: A) => A;
|
|
24
28
|
}> &
|
|
25
29
|
InitializedRootObservable<A>;
|
|
26
30
|
|
|
@@ -35,9 +35,28 @@ type CreateObservableType<A, Kind extends ObservableKind> = Readonly<{
|
|
|
35
35
|
hasChild: boolean;
|
|
36
36
|
hasActiveChild: () => boolean;
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Whether this observable completes itself once nothing uses it any more —
|
|
40
|
+
* once it has neither subscribers nor live children when one of its children
|
|
41
|
+
* completes. True when completing it would release something: it has a
|
|
42
|
+
* teardown of its own (a timer, an inner subscription) or a parent with
|
|
43
|
+
* `autoComplete`. A `source` or `createState` and everything derived from
|
|
44
|
+
* it alone are never completed this way, so completing a derived observable
|
|
45
|
+
* cannot end a state that something else may still read (#2133).
|
|
46
|
+
*/
|
|
47
|
+
autoComplete: boolean;
|
|
48
|
+
|
|
38
49
|
tryUpdate: (updateToken: UpdateToken) => void;
|
|
39
50
|
tryComplete: () => void;
|
|
40
51
|
complete: () => void;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Completes this observable and takes it out of the graph, without telling
|
|
55
|
+
* its parents: unlike `complete()`, it never completes an upstream
|
|
56
|
+
* observable, whatever its `autoComplete`. Its descendants are completed as
|
|
57
|
+
* by `complete()` once none of their parents is live.
|
|
58
|
+
*/
|
|
59
|
+
dispose: () => void;
|
|
41
60
|
subscribe: (onNext: (v: A) => void, onComplete?: () => void) => Subscription;
|
|
42
61
|
|
|
43
62
|
pipe: PipeMethod<A>;
|
|
@@ -18,13 +18,13 @@ import { source, type Observable } from '../core/index.mjs';
|
|
|
18
18
|
*
|
|
19
19
|
* emitClick(); // logs: Clicked!
|
|
20
20
|
*
|
|
21
|
-
* assert.
|
|
21
|
+
* assert.strictEqual(mut_clickCount.value, 1);
|
|
22
22
|
*
|
|
23
23
|
* emitClick();
|
|
24
24
|
*
|
|
25
25
|
* emitClick();
|
|
26
26
|
*
|
|
27
|
-
* assert.
|
|
27
|
+
* assert.strictEqual(mut_clickCount.value, 3);
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
30
|
export const createEventEmitter = (): readonly [
|
|
@@ -11,7 +11,9 @@ import { source, type InitializedObservable } from '../core/index.mjs';
|
|
|
11
11
|
* @param initialState - The initial value of the state
|
|
12
12
|
* @param options - {@link StateOptions}: `equals`, to pass on only the
|
|
13
13
|
* dispatches that change the state
|
|
14
|
-
* @returns A 3-element tuple: `[state, dispatch, { getSnapshot, initialState }]
|
|
14
|
+
* @returns A 3-element tuple: `[state, dispatch, { getSnapshot, initialState }]`.
|
|
15
|
+
* `dispatch` returns the state it leaves; once `state` is completed, that
|
|
16
|
+
* is the state held.
|
|
15
17
|
*
|
|
16
18
|
* @example
|
|
17
19
|
* ```ts
|
|
@@ -67,6 +69,13 @@ import { source, type InitializedObservable } from '../core/index.mjs';
|
|
|
67
69
|
*
|
|
68
70
|
* assert.deepStrictEqual(countHistory, [0, 2]);
|
|
69
71
|
* ```
|
|
72
|
+
*
|
|
73
|
+
* @note Do not call `dispatch` from a subscriber or a projection while another
|
|
74
|
+
* update is propagating: the write propagates immediately, so subscribers can
|
|
75
|
+
* see an intermediate state, and it logs a `console.warn` (once per
|
|
76
|
+
* observable). Derive the value from its source instead —
|
|
77
|
+
* `const b$ = a$.pipe(map((a) => a * 2))` rather than
|
|
78
|
+
* `a$.subscribe((a) => { setB(a * 2); })`.
|
|
70
79
|
*/
|
|
71
80
|
export const createReducer = <S, A>(
|
|
72
81
|
reducer: Reducer<S, A>,
|
|
@@ -89,13 +98,10 @@ export const createReducer = <S, A>(
|
|
|
89
98
|
|
|
90
99
|
const nextState = reducer(currentState, action);
|
|
91
100
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
state.next(nextState);
|
|
97
|
-
|
|
98
|
-
return nextState;
|
|
101
|
+
// Once completed, `next` keeps the state held and returns it.
|
|
102
|
+
return equals?.(currentState, nextState) === true
|
|
103
|
+
? currentState
|
|
104
|
+
: state.next(nextState);
|
|
99
105
|
};
|
|
100
106
|
|
|
101
107
|
const getSnapshot = (): S => state.getSnapshot().value;
|
|
@@ -65,6 +65,18 @@ describe(createReducer, () => {
|
|
|
65
65
|
|
|
66
66
|
assert.deepStrictEqual(mut_asked, [[0, 5]]);
|
|
67
67
|
});
|
|
68
|
+
|
|
69
|
+
test('once the state is completed, a dispatch returns the current state', () => {
|
|
70
|
+
const [state, dispatch, { getSnapshot }] = createReducer(keepOrAdd, 0);
|
|
71
|
+
|
|
72
|
+
dispatch({ type: 'add', amount: 1 });
|
|
73
|
+
|
|
74
|
+
state.complete();
|
|
75
|
+
|
|
76
|
+
assert.strictEqual(dispatch({ type: 'add', amount: 2 }), 1);
|
|
77
|
+
|
|
78
|
+
assert.strictEqual(getSnapshot(), 1);
|
|
79
|
+
});
|
|
68
80
|
});
|
|
69
81
|
|
|
70
82
|
type Count = Readonly<{ count: number }>;
|
|
@@ -88,6 +88,13 @@ const reducer = <S,>(state: S, action: Action<S>): S => {
|
|
|
88
88
|
* 'label:bug page:2',
|
|
89
89
|
* ]);
|
|
90
90
|
* ```
|
|
91
|
+
*
|
|
92
|
+
* @note Do not call `setState`, `updateState` or `resetState` from a subscriber or a projection while another
|
|
93
|
+
* update is propagating: the write propagates immediately, so subscribers can
|
|
94
|
+
* see an intermediate state, and it logs a `console.warn` (once per
|
|
95
|
+
* observable). Derive the value from its source instead —
|
|
96
|
+
* `const b$ = a$.pipe(map((a) => a * 2))` rather than
|
|
97
|
+
* `a$.subscribe((a) => { setB(a * 2); })`.
|
|
91
98
|
*/
|
|
92
99
|
export const createState = <S,>(
|
|
93
100
|
initialState: S,
|
|
@@ -34,6 +34,26 @@ describe(createState, () => {
|
|
|
34
34
|
assert.deepStrictEqual(seen, [0, 1]);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
test('once the state is completed, setState, updateState and resetState return the current value', () => {
|
|
38
|
+
const [state, setState, { updateState, resetState, getSnapshot }] =
|
|
39
|
+
createState(0);
|
|
40
|
+
|
|
41
|
+
setState(1);
|
|
42
|
+
|
|
43
|
+
state.complete();
|
|
44
|
+
|
|
45
|
+
assert.strictEqual(setState(2), 1);
|
|
46
|
+
|
|
47
|
+
assert.strictEqual(
|
|
48
|
+
updateState((current) => current + 1),
|
|
49
|
+
1,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
assert.strictEqual(resetState(), 1);
|
|
53
|
+
|
|
54
|
+
assert.strictEqual(getSnapshot(), 1);
|
|
55
|
+
});
|
|
56
|
+
|
|
37
57
|
// What `equals` is for: whatever reads the state, directly or through
|
|
38
58
|
// anything derived from it, is told only of a real change, and an equal
|
|
39
59
|
// value written over the current one leaves the object that is there.
|