I'm looking for the right operator and an elegent way to debounce a observable based on the output timing of another.
Basic problem:
'If during the past 3 seconds observable A has emitted, debounce the emit of Observable B until these three seconds have passed'
Additonally, this is applied in the context of NGRX actions/effects, rephrasing the basic problem in this context yields:
'Debounce an effect based on the recent history of another effect or action'
This should do what you are after:
const since = Date.now();
const actionA = new Rx.Subject();
const actionB = new Rx.Subject();
const debouncedB = Rx.Observable
.combineLatest(
actionA.switchMap(() => Rx.Observable.concat(
Rx.Observable.of(true),
Rx.Observable.of(false).delay(3000)
))
.startWith(false),
actionB
)
.filter(([debouncing]) => !debouncing)
.map(([, b]) => b)
.distinctUntilChanged();
debouncedB.subscribe(
(value) => console.log(value, `T+${((Date.now() - since) / 1000).toFixed(0)}`)
);
actionB.next("b1");
actionA.next("a1");
actionB.next("b2");
actionB.next("b3");
actionB.next("b4");
setTimeout(() => actionB.next("b5"), 4000);
setTimeout(() => actionA.next("a2"), 5000);
setTimeout(() => actionB.next("b6"), 6000);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/rxjs#5/bundles/Rx.min.js"></script>
It converts each A action into a debouncing signal that's either true or false. When true, B actions will be debounced until the signal becomes false.
switchMap is used so that if another A action is received whilst the signal is true, the signal wont be set to false until a three seconds after the latest A action.
Related
I have a costly server ajax request which has one input (full: boolean). If full is false, the server can return either a partial or a full response (response.isFull == true); but if full is true, the server will return a full response. Normally the partial response is good enough, but there are certain conditions that will require a full response. I need to avoid requesting a full response explicitly as much as possible, so I thought I'd start with a BehaviorSubject which I can eventually feed with true and combine it with distinctUntilChanged if I ever need to get the full response. This will give me an observable with false initially and that can give me true if I feed that into it:
const fullSubject = new BehaviorSubject<boolean>(false);
Then I've got a function that takes a boolean parameter and returns an observable with the server request (retried, transformed, etc.). As said, the answer can be partial or full, but it can be full even if the input parameter was false at the server's discretion. For example:
interface IdentityData {
...
isFull: boolean;
}
private getSimpleIdentity(full: boolean): Observable<IdentityData> {
return Axios.get(`/api/identity${full?"?full=true":""}`)
.pipe( ... retry logic ...,
... transformation logic ...,
shareReplay(1) );
}
I need to know how can I combine these so that the following is true:
The server needs to be queried at most twice.
If the first answer is a full answer, no further queries must be performed to the server.
If the first answer is a partial answer, and true is fed into fullSubject, a full answer must be requested.
The expected output from all this is an observable that emits either one full response, or a partial response and, when asked, a full response.
Environment: Vue 2.6.11, RxJS 6.5.5, Axios 0.19.2, TypeScript 3.7.5.
Thanks in advance
Here would be my approach:
const fullSubject = new BehaviorSubject(false);
const src$ = fullSubject.pipe(
switchMap(isFull => Axios.get('...')),
take(2), // Server required at most twice
takeWhile(response => !response.isFull, true), // When `isFull`, it will complete & unsubscribe -> no more requests to the server
shareReplay(1),
);
src$.subscribe(() => { /* ... */ });
function getFullAnswer () {
fullSubject.next(true);
}
takeWhile takes a second argument, inclusive. When set to true, when the predicate function evaluates to false(e.g isFull is true) it will send that value as well. –
if I've got it correctly
private getSimpleIdentity(): Observable<IdentityData> {
return fullSubject.pipe(
switchMap(full => Axios.get(`/api/identity${full ? "?full=true" : ""}`)),
shareReplay(1),
);
}
Uses the retryWhen() operator
const source = of("").pipe(map(() => Math.floor(Math.random() * 10 + 1)));
const example = source
.pipe(
tap((val) => console.log("tap", val)),
map((val) => {
//error will be picked up by retryWhen
if (val !== 5) throw val;
return val;
}),
retryWhen((errors) =>
errors.pipe(
tap(() => console.log("--Wait 1 seconds then repeat")),
delay(1000)
)
)
)
.subscribe((val) => console.log("subscription", val));
/*
output:
tap 3
--Wait 1 seconds then repeat
tap 8
--Wait 1 seconds then repeat
tap 1
--Wait 1 seconds then repeat
tap 4
--Wait 1 seconds then repeat
tap 7
--Wait 1 seconds then repeat
tap 5
subscription 5
*/
I found many examples of how to reset timer, but they usually concerned manual reset (e.g. on-click button event).
I need a logic that will automatically reset the value when the countdown ends.
Timer:
type seconds = number;
const getRemainingTime$ = (store: Store): Observable<seconds> => {
// calculate fullTime based on the TriggerDate extracted from the State
// ...
return fullTime$.pipe(
switchMap((fullTime: seconds) =>
timer(0, 1000).pipe(
map((tickCount: number) => fullTime - tickCount),
takeWhile((remainingTime: seconds) => remainingTime >= 0)
)
)
);
}
Trigger (wait for 0 value on timer)
getRemainingTime$(this.store).pipe(
skipWhile((remainingTime: seconds) => remainingTime > 0),
)
.subscribe(data => {
const newTriggerDate: Date = new Date(new Date().getTime() + 60 * 1000); // +60 seconds
this.store.dispatch([new SetTriggerDateAction({ newTriggerDate })]);
});
...and it doesn't work -
When the remaining time is zero, the trigger goes crazy and dispatch an infinite number of actions. What is wrong?
PS: When I manually dispatch SetTriggerDateAction (onClick button), the problem disappears.
It was enough to replace skipWhile to a filter.
skipWhile
All values are emitted if the condition in skipWhile is met at least once.
filter
Only those values specified in the condition are emitted.
I have the following, and it does work, it keeps increasing the delay and eventually timing out which is what I wanted.
But because I am using Concatmap i lose the original value from the interval.
let x = 1
let source2$ = interval(500)
.pipe(
concatMap(() => {
x++
let newtime = x * 500
console.log("newtime ", newtime)
return of(5).pipe(delay(newtime))
}),
timeout(3000),
map((data) => {
return 'Source 2: ' + data
})
)
so it prints Source 2: 5.. where as i want it to print the value of the interval.
I got working what i wanted using the concatmap but i think its the wrong operator as I lose the original value.
Can somebody help?
More info
TO summarize, all i would like to do is emit values using the interval and after each emit increase the delay time - eventually it hits the timeout of 3000 ms and errors out.
I've mentioned in comments that you can use concatMap for this that receives ever increasing index from interval:
concatMap(index => {
let newtime = index * 500
console.log("newtime ", newtime)
return of(index).pipe(delay(newtime))
}),
Notice, that I'm returning the value back to the stream by of(index).
I think I understand what were you concerned about returning another Observable. Since you want to emit items in sequence (emit one only after the previous one completes) then you have to use concatMap with another inner Observable. There isn't a special operator only for this functionality because this is "composable behavior" which means you can achieve this behavior by combining existing operators.
const source2$ = interval(500)
.pipe(
map(x => x * 500),
switchMap(x => timer(x)),
timeout(3000),
map(data => 'Source 2: ' + data)
)
UPDATE:
https://stackblitz.com/edit/rxjs-iywcm6?devtoolsheight=60
const source2$ = interval(500)
.pipe(
tap(x => console.log('Tick before delay', x)),
concatMap(x => timer((x + 1) * 500).pipe(mapTo(x))),
tap(x => console.log('Tick after delay', x)),
map(data => 'Source 2: ' + data),
timeout(3000)
).subscribe(
(data) => console.log(data),
e => console.error('Timeout', e))
Hope someone can help me with this problem.
I have 2 streams that I need to use the operator combineLatest on. After a while I need to add dynamically streams that also need to use combineLatest on.
Here is what I need to do:
stream a ---------a---------------b-------------c------------------------>
stream b ----1--------2-----3-------------------------4------------------>
stream c (not defined at start) -----z-----------------x------------>
stream d (not defined at start) ----------k------>
(combineLatest)
result ---------(a1)(a2)--(a3)--(b3)----(b3z)-(c3z)-(c4z)-(c4x)-(c4xk)->
UPDATE
To be more specific I want to turn this STREAM (link)
To this result:
A----B---B0-C0--D0--D1--E1--E1a--F1a--F2a---G2a---G3a--H3a-H3b--I3b
The idea it that everything is a stream. Even stream of streams :)
const onNew$ = new Rx.Subject();
const a$ = Rx.Observable.interval(1000).mapTo('a');
const b$ = Rx.Observable.interval(1000).mapTo('b');
const comb$ = Rx.Observable
.merge(
onNew$,
Rx.Observable.from([a$, b$]),
)
.scan((acc, v) => {
acc.push(v);
return acc;
}, [])
.switchMap(vs => Rx.Observable.combineLatest(vs))
comb$.take(4).subscribe(v => console.log(v));
setTimeout(
() => onNew$.next(Rx.Observable.interval(1000).mapTo('c')),
2000,
);
setTimeout(
() => onNew$.next(Rx.Observable.interval(1000).mapTo('d')),
4000,
);
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>
Taking Oles' answer, simplifying a little and adding test data as given in question update
const Subject = Rx.ReplaySubject
const ReplaySubject = Rx.ReplaySubject
const newStream = new Subject()
// Set up output, no streams yet
const streamOfStreams = newStream
.scan( (acc, stream) => {
acc.push(stream);
return acc;
}, [])
.switchMap(vs => Observable.combineLatest(vs))
.map(arrayOfValues => arrayOfValues.join('')) // declutter
.subscribe(console.log)
// Add a stream
const s1 = new ReplaySubject()
newStream.next(s1)
// emit on streams
s1.next('A'); s1.next('B')
// Add a stream
const s2 = new ReplaySubject()
newStream.next(s2)
// emit on streams
s2.next('0'); s1.next('C')
s1.next('D'); s2.next('1'); s1.next('E');
// Add a stream
const s3 = new ReplaySubject()
newStream.next(s3)
// emit on streams
s3.next('a');
s1.next('F'); s2.next('2'); s1.next('G'); s2.next('3'); s1.next('H');
s3.next('b'); s1.next('I')
Working example: CodePen
Update
Christian has kindly supplied some test streams which are more 'real world' than the sequenced Subjects I've used above. Unfortunately, these highlight a bug in the solution as it stands.
For reference, the new test streams are
const streamA = Rx.Observable.timer(0,800).map(x => String.fromCharCode(x+ 65));
const streamB = Rx.Observable.timer(0,1300).map(x => x);
const streamC = Rx.Observable.timer(1100, 2000).map(x => String.fromCharCode(x+ 97));
setTimeout(() => newStream.next(streamA), 500);
setTimeout(() => newStream.next(streamB), 2000);
setTimeout(() => newStream.next(streamC), 3000);
Problem #1
The first problem stems from the core line in streamOfStreams,
.switchMap(vs => Observable.combineLatest(vs))
This essentially says, every time a new array of streams appears, map it to a combineLatest() of the new array and switch to the new observable. However, the test observables are cold, which means each re-subscription gets the full stream.
Ref: Introduction to Rx - Hot and Cold observables
Some observable sequences can appear to be hot when they are in fact
cold. A couple of examples that surprise many is Observable.Interval
and Observable.Timer
So we get
- expected A--B--B0...
- actual A--B--A0--B0...
The obvious solution is to turn the cold observables hot,
const asHot = (stream) => {
const hot = stream.multicast(() => new Rx.Subject())
hot.connect()
return hot
}
but this omits B0 from the sequence, A--B--C0..., so we want hot + 1 previous which can be had with a buffer size one
const asBuffered = (stream) => {
const bufferOne = new ReplaySubject(1)
stream.subscribe(value => bufferOne.next(value))
return bufferOne
}
Problem #2
The second problem comes from the fact that streamC delays it's first emit by 1100ms (good test Christian!).
This results is
- expected A--B--B0--C0--D0--D1--E1--E1a...
- actual A--B--B0--C0--D0--E1a...
which means we need to delay adding a stream until it's first emit
const addStreamOnFirstEmit = (stream) => {
const buffered = asBuffered(stream)
buffered.first().subscribe( _ => {
newStream.next(buffered)
})
}
Working example: CodePen
Notes on the CodePen
I've left in the various streamAdder functions for experimentation, and there are also _debug versions that emit the streams and the addStream events to show the sequence.
Also limited the source streams so that the console doesn't scroll too much.
Note on the expected output
The new solution diverges from the expected output given in the question after 'G3a'
expected A----B---B0-C0--D0--D1--E1--F1---F2---F2a---G2a---G3a--H3a--H3b--I3b
actual A----B---B0-C0--D0--D1--E1--E1a--F1a--F2a---G2a---G3a--G3b--H3b--I3b
which is due to the simultaneous emission of of 'H' and 'b'. Problem #3?
One more test
In order to see if the solution failed if streamC delayed first emission until after two emits of streamA & streamB, I changed the delay to 1800ms
const streamC = Rx.Observable.timer(1800, 2000).map(x => String.fromCharCode(x+ 97));
I believe the output for this test is correct.
Can be done if you can unsubscribe and re-subscribe for each new stream
// Start with two streams
const s1 = new ReplaySubject(1)
const s2 = new ReplaySubject(1)
let out = Observable.combineLatest(s1, s2)
let subscription = out.subscribe(console.log)
s2.next('1'); s1.next('a'); s2.next('2'); s2.next('3'); s1.next('b')
// Add a new stream
subscription.unsubscribe()
const s3 = new ReplaySubject(1)
out = Observable.combineLatest(s1, s2, s3)
subscription = out.subscribe(console.log)
s3.next('z'); s1.next('c'); s2.next('4'); s3.next('x')
// Add a new stream
subscription.unsubscribe()
const s4 = new ReplaySubject(1)
out = Observable.combineLatest(s1, s2, s3, s4)
subscription = out.subscribe(console.log)
s4.next('k')
Working example: CodePen
I have some pre-defined events set to occur at specific times.
And I have a timer, like this:
const timer = Rx.Observable.interval(100).timeInterval()
.map(x => x.interval)
.scan((ms, total) => total + ms, 0)
The timer emits something close to 100,200,300,400,500 (although in reality it's more like 101,200,302,401,500...which is totally fine)
I also have some stuff I want to do at certain times. For example, let's say I want to do stuff at the following times:
const stuff = Rx.Observable.from([1000, 2000, 2250, 3000, 5000]);
What I'd like is to combine "stuff" and "timer" in such a way that the resulting stream emits a value once per time defined in "stuff" at that time (or ever so slightly later). in this case, that would be t=1000 ms, 2000 ms, 2250 ms, 3000 ms and 5000 ms. Note: the 2250 guy should emit around time 2300 because of the interval size. that's fine. they just can't come early or more than once.
I have one solution, but it's not very good. it re-starts "stuff" every single step (every single 100 ms in this case) and filters it and takes 1. I would prefer that, once an event is emitted from "stuff", that it be gone, so subsequent filters on it don't have those values.
In the real application, there will be stuff and stuff2 and maybe stuff3...(but I will call them something else!)
Thanks in advance! I hope that was clear.
If I've understood what you're after correctly, this should be achievable with a simple projection:
const times$ = stuff.flatMap(x => Rx.Observable.timer(x));
Here's a working sample: https://jsbin.com/negiyizibu/edit?html,js,console,output
Edit
For the second requirement, try something like this:
const times$ = Rx.Observable
.from([{"val":"jeff", "t": 1000}, {"val":"fred", "t": 2500}])
.flatMap(x => Rx.Observable.timer(x.t).map(y => x.val));
https://jsbin.com/cegijudoci/edit?js,console,output
Here's a typescript function I wrote based on Matt's solution.
import {from, timer} from 'rxjs';
import {flatMap, map} from 'rxjs/operators';
export interface ActionQueueEntry {
action: string;
payload?: any;
delay: number;
}
export function actionQueue(entries: ActionQueueEntry[]) {
return from(entries).pipe(flatMap((x: any) => {
return timer(x.delay).pipe(map(y => x));
}));
}
const q = actionQueue([
{action: 'say: hi', delay: 500},
{action: 'ask: how you are', delay: 2500},
{action: 'say: im fine', delay: 5000},
]);
q.subscribe(console.log);