throttle.ts
5.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import { Subscription } from '../Subscription';
import { MonoTypeOperatorFunction, ObservableInput } from '../types';
import { operate } from '../util/lift';
import { createOperatorSubscriber } from './OperatorSubscriber';
import { innerFrom } from '../observable/innerFrom';
/**
* An object interface used by {@link throttle} or {@link throttleTime} that ensure
* configuration options of these operators.
*
* @see {@link throttle}
* @see {@link throttleTime}
*/
export interface ThrottleConfig {
/**
* If `true`, the resulting Observable will emit the first value from the source
* Observable at the **start** of the "throttling" process (when starting an
* internal timer that prevents other emissions from the source to pass through).
* If `false`, it will not emit the first value from the source Observable at the
* start of the "throttling" process.
*
* If not provided, defaults to: `true`.
*/
leading?: boolean;
/**
* If `true`, the resulting Observable will emit the last value from the source
* Observable at the **end** of the "throttling" process (when ending an internal
* timer that prevents other emissions from the source to pass through).
* If `false`, it will not emit the last value from the source Observable at the
* end of the "throttling" process.
*
* If not provided, defaults to: `false`.
*/
trailing?: boolean;
}
/**
* Emits a value from the source Observable, then ignores subsequent source
* values for a duration determined by another Observable, then repeats this
* process.
*
* <span class="informal">It's like {@link throttleTime}, but the silencing
* duration is determined by a second Observable.</span>
*
* 
*
* `throttle` emits the source Observable values on the output Observable
* when its internal timer is disabled, and ignores source values when the timer
* is enabled. Initially, the timer is disabled. As soon as the first source
* value arrives, it is forwarded to the output Observable, and then the timer
* is enabled by calling the `durationSelector` function with the source value,
* which returns the "duration" Observable. When the duration Observable emits a
* value, the timer is disabled, and this process repeats for the
* next source value.
*
* ## Example
*
* Emit clicks at a rate of at most one click per second
*
* ```ts
* import { fromEvent, throttle, interval } from 'rxjs';
*
* const clicks = fromEvent(document, 'click');
* const result = clicks.pipe(throttle(() => interval(1000)));
*
* result.subscribe(x => console.log(x));
* ```
*
* @see {@link audit}
* @see {@link debounce}
* @see {@link delayWhen}
* @see {@link sample}
* @see {@link throttleTime}
*
* @param durationSelector A function that receives a value from the source
* Observable, for computing the silencing duration for each source value,
* returned as an `ObservableInput`.
* @param config A configuration object to define `leading` and `trailing`
* behavior. Defaults to `{ leading: true, trailing: false }`.
* @return A function that returns an Observable that performs the throttle
* operation to limit the rate of emissions from the source.
*/
export function throttle<T>(durationSelector: (value: T) => ObservableInput<any>, config?: ThrottleConfig): MonoTypeOperatorFunction<T> {
return operate((source, subscriber) => {
const { leading = true, trailing = false } = config ?? {};
let hasValue = false;
let sendValue: T | null = null;
let throttled: Subscription | null = null;
let isComplete = false;
const endThrottling = () => {
throttled?.unsubscribe();
throttled = null;
if (trailing) {
send();
isComplete && subscriber.complete();
}
};
const cleanupThrottling = () => {
throttled = null;
isComplete && subscriber.complete();
};
const startThrottle = (value: T) =>
(throttled = innerFrom(durationSelector(value)).subscribe(createOperatorSubscriber(subscriber, endThrottling, cleanupThrottling)));
const send = () => {
if (hasValue) {
// Ensure we clear out our value and hasValue flag
// before we emit, otherwise reentrant code can cause
// issues here.
hasValue = false;
const value = sendValue!;
sendValue = null;
// Emit the value.
subscriber.next(value);
!isComplete && startThrottle(value);
}
};
source.subscribe(
createOperatorSubscriber(
subscriber,
// Regarding the presence of throttled.closed in the following
// conditions, if a synchronous duration selector is specified - weird,
// but legal - an already-closed subscription will be assigned to
// throttled, so the subscription's closed property needs to be checked,
// too.
(value) => {
hasValue = true;
sendValue = value;
!(throttled && !throttled.closed) && (leading ? send() : startThrottle(value));
},
() => {
isComplete = true;
!(trailing && hasValue && throttled && !throttled.closed) && subscriber.complete();
}
)
);
});
}