I have spent few days but could not find a way to do "distinct throttle" in RxJS.
Assume each event completes in 4 dashes, a "distinct throttle" will perform as follows:
-①-②-①---------①-----|->
[distinct throttle]
-①-②-------------①-----|->
How can I use existing RxJS operators to build a "distinct throttle"?
You can use groupBy to separate the notifications by value and can then apply throttleTime and can then merge the grouped observables using mergeMap. Like this:
const { Subject } = rxjs;
const { groupBy, mergeMap, throttleTime } = rxjs.operators;
const source = new Subject();
const result = source.pipe(
groupBy(value => value),
mergeMap(grouped => grouped.pipe(
throttleTime(400)
))
);
result.subscribe(value => console.log(value));
setTimeout(() => source.next(1), 100);
setTimeout(() => source.next(2), 300);
setTimeout(() => source.next(1), 400);
setTimeout(() => source.next(1), 900);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/rxjs#6/bundles/rxjs.umd.min.js"></script>
distinct and throttle have 2 different characteristics regarding item pick. distinct will pick the first item while throttle will pick the last.
Sometimes you want to keep throttle's behavior.
Let's say the stream is: chat-message-edit events carrying the updated text. A user may edit a specific message multiple times within the throttle period.
You want to be sure that you always keep the last version of each message (among a stream of edits of differrent messages ).
A possible solution I would follow for this is the one below
const source$ = from([
{id:1,content:"1a"},
{id:1,content:"1b"},
{id:1,content:"1c"},
{id:2,content:"2a"},
{id:2,content:"2b"},
{id:3,content:"3a"},
{id:3,content:"3b"},
{id:1,content:"1d"},
{id:1,content:"1e"},
{id:4,content:"4a"},
{id:4,content:"4b"},
{id:4,content:"4c"},
{id:4,content:"4e"},
{id:4,content:"4f"},
{id:3,content:"3c"},
{id:3,content:"3d"},
{id:3,content:"3e"}
]).pipe(concatMap((el)=> of(el).pipe(delay(500)) ));
const distinctThrottle = (throttleTime, keySelector)=>
pipe(bufferTime(throttleTime),
concatMap((arr)=>from(arr.reverse()).pipe(distinct(keySelector))
)) ;
let throttledStream = source$.pipe(distinctThrottle(1550, ({id})=>id));
throttledStream.subscribe(console.log);
Related
Here is a typical rxjs example of drag-and-drop:
const drag$ = mousedown$.pipe(
switchMap(
(start) => {
return mousemove$.pipe(map(move => {
move.preventDefault();
return {
left: move.clientX - start.offsetX,
top: move.clientY - start.offsetY
}
}),
takeUntil(mouseup$));
}));
drag$.subscribe(pos => {
box.style.top = `${pos.top}px`
box.style.left = `${pos.left}px`
});
The takeUntil will unsubscribe the inner observable of mouseMove$. But what if I need the last value in the inner observable in order to do something with it upon the mouseUp? I know I can probably assign a value to a global temporary variable to get at it that way but I'm trying to avoid that.
The more involved story of what I'm trying to do is this:
I'm working on a 3D game where I want to implement grab-and-throw. I've set it up similar to the rxjs conanical drag-and-drop scenario where I'm subscribing to a controller 'squeeze' event, then switchMaping to another observable of controller movement where I 'drag' and reposition the object where ever my hand position is, and finally I takeUntil the 'squeeze' is released, which lets go of the object.
But there is one more piece I need to implement for 'throwing' the object, which is I need the velocity vector of my hand movement at the moment the squeeze is released to apply an impulse force to the object so it flies away when I let go. That vector can be calculated on every frame inside the switchMap's pipe using scan.
The problem is takeUntil just stops the data stream and I don't have access the last value that was in the accumulator of scan.
You can use the withLatestFrom() operator to get the most recent emission from another observable. The result is an array of the stream value, followed by the other observable's last emission:
mouseup$.pipe(
withLatestFrom(drag$)
).subscribe(
([mouseup, drag]) => console.log({drag, mouseup})
);
Here's a StackBlitz example.
This is a very interesting problem, I'm guessing you also need the values as the drag event takes place, but also its last emitted value before the mouseup event occurs.
One approach I think would be this:
const drag$ = mousedown$.pipe(
switchMap(
(start) => {
mousemove$ = mousemove$.pipe(
map(move => {
move.preventDefault();
return {
left: move.clientX - start.offsetX,
top: move.clientY - start.offsetY
}
}),
takeUntil(mouseup$),
share(),
);
return concat(
// get the `mousemove` events as they take place
mousemove$,
mousemove$.pipe(
reduce((acc, crt) => { /* ... */ }),
),
)
}));
The reduce operator works the same as scan, with the exception that the former will emit the accumulated value when the source completes.
If you want to use the same scan logic in both observables, then the switchMap's inner observable could look like this:
mousemove$ = mousemove$.pipe(
map(move => {
move.preventDefault();
return {
left: move.clientX - start.offsetX,
top: move.clientY - start.offsetY
}
}),
scan((acc, crt) => { /* ... */ }),
takeUntil(mouseup$),
share(),
);
return concat(
mousemove$,
mousemove$.pipe(
// it will emit the last emitted value when the source completes
takeLast(1),
),
)
I'm new to RxJs and need help/understanding for the following.
I have page that displays current covid cases. I have it setup to poll every 60 seconds. What I'm trying to understand is, if I subscribe to this observable via another new component, I have wait until the next iteration of 60 seconds is complete to get the data. My question is, if I want to share, is there any way to force to send the data and restart the timer?
I don't want 2 different 60 second intervals calling the API. I just want one, and the interval to restart if a new subscriber is initialized. Hope that makes sense.
this.covidCases$ = timer(1, 60000).pipe(
switchMap(() =>
this.covidService.getCovidCases().pipe(
map(data => {
return data.cases;
}),
),
),
retry(),
share(),
);
I think this should work:
const newSubscription = new Subject();
const covidCases$ = interval(60 * 1000).pipe(
takeUntil(newSubscription),
repeat(),
switchMap(() =>
this.covidService.getCovidCases().pipe(
/* ... */
),
),
takeUntil(this.stopPolling),
shareReplay(1),
src$ => defer(() => (newSubscription.next(), src$))
);
I replaced timer(1, 60 * 1000) + retry() with interval(60 * 1000).
My reasoning was that in order to restart the timer(the interval()), we must re-subscribe to it. But before re-subscribing, we should first unsubscribed from it.
So this is what these lines do:
interval(60 * 1000).pipe(
takeUntil(newSubscription),
repeat(),
/* ... */
)
We have a timer going on, until newSubscription emits. When that happens, takeUntil will emit a complete notification, then it will unsubscribe from its source(the source produced by interval in this case).
repeat will intercept that complete notification, and will re-subscribe to the source observable(source = interval().pipe(takeUntil())), meaning that the timer will restart.
shareReplay(1) makes sure that a new subscriber will receive the latest emitted value.
Then, placing src$ => defer(() => (newSubscription.next(), src$)) after shareReplay is very important. By using defer(), we are able to determine the moment when a new subscriber arrives.
If you were to put src$ => defer(() => (console.log('sub'), src$)) above shareReplay(1), you should see sub executed logged only once, after the first subscriber is created.
By putting it below shareReplay(1), you should see that message logged every time a subscriber is created.
Back to our example, when a new subscriber is registered, newSubscription will emit, meaning that the timer will be restarted, but because we're also using repeat, the complete notification won't be passed along to shareReplay, unless stopPolling emits.
StackBlitz demo.
This code creates an observable onject. I think what you should do is to add a Replaysubject instead of the Observable.
Replaysubjects gives the possibility to emit the same event when a new subscription occurs.
timer(1, 60000).pipe(
switchMap(() =>
this.covidService.getCovidCases().pipe(
tap(result => {
if (!result.page.totalElements) {
this.stopPolling.next();
}
}),
map(data => {
return data.cases;
}),
tap(results =>
results.sort(
(a, b) =>
new Date(b.covidDateTime).getTime() -
new Date(a.covidDateTime).getTime(),
),
),
),
),
retry(),
share(),
takeUntil(this.stopPolling),
).subscribe((val)=>{this.covidcases.next(val)});
This modification results in creating the timer once so when you subscribe to the subject it will emit the latest value immediately
You can write an operator that pushes the number of newly added subscriber to an given subject:
const { Subject, timer, Observable } = rxjs;
const { takeUntil, repeat, map, share } = rxjs.operators;
// Operator
function subscriberAdded (subscriberAdded$) {
let subscriberAddedCounter = 0;
return function (source$) {
return new Observable(subscriber => {
source$.subscribe(subscriber)
subscriberAddedCounter += 1;
subscriberAdded$.next(subscriberAddedCounter)
});
}
}
// Usage
const subscriberAdded$ = new Subject();
const covidCases$ = timer(1, 4000).pipe(
takeUntil(subscriberAdded$),
repeat(),
map(() => 'testValue'),
share(),
subscriberAdded(subscriberAdded$)
)
covidCases$.subscribe(v => console.info('subscribe 1: ', v));
setTimeout(() => covidCases$.subscribe(v => console.info('subscribe 2: ', v)), 5000);
subscriberAdded$.subscribe(v => console.warn('subscriber added: ', v));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>
Future possibilities:
You can update the operator easily to decrease the number in case you want to react on unsubscribers
!important
The takeUnit + repeat has already been postet by #AndreiGătej. I only provided an alternative way for receiving an event when a subscriber is added.
Running stackblitz with typescript
If the subscriberAdded operator needs some adjustements, please let me know and I will update
I have an Observable, mySourceObs, and the following code
concurrencyFactor = 10;
mySourceObs.pipe(
mergeMap(data => generateAnotherObservable(data), concurrencyFactor)
).subscribe();
Is there a way to change dynamically the concurrencyFactor once the chain of Observables has been subscribed?
You can do it like this, but inner observable will restart when concurrencyFactor changes.
concurrencyFactor = new BehaviorSubject(10);
combineLatest(mySourceObs,concurrent).pipe(
switchMap(([data,concurrent]) => generateAnotherObservable(data), concurrent)
).subscribe();
concurrencyFactor.next(5)
I have figured out a way of controlling the concurrency of mergeMap with a combination of an external Subject, switchMap and defer.
This is the sample code
// when this Observable emits, the concurrency is recalculated
const mySwitch = new Subject<number>();
let myConcurrency = 1;
// this block emits five times, every time setting a new value for the concurrency
// and the making mySwitch emit
merge(interval(1000).pipe(map(i => i + 2)), of(1))
.pipe(
delay(0),
take(5),
tap(i => {
myConcurrency = i * 2;
console.log("switch emitted", i);
mySwitch.next(i);
})
)
.subscribe();
// source is an hot Observable which emits an incresing number every 100 ms
const source = new Subject<number>();
interval(100)
.pipe(take(100))
.subscribe(i => source.next(i));
// this is the core of the logic
// every time mySwitch emits the old subscription is cancelled and a new one is created
mySwitch
.pipe(
switchMap(() =>
// defer is critical here since we want a new Observable to be generated at subscription time
// so that the new concurrency value is actually used
defer(() =>
source.pipe(
mergeMap(
i =>
// this is just a simulation of a an inner Observable emitting something
interval(100).pipe(
delay(100),
map(j => `Observable of ${i} and ${j}`),
take(10)
),
myConcurrency
)
)
)
)
)
.subscribe({
next: data => console.log(data)
});
Let's say I have a rather typical use of rx that does requests every time some change event comes in (I write this in the .NET style, but I'm really thinking of Javascript):
myChanges
.Throttle(200)
.Select(async data => {
await someLongRunningWriteRequest(data);
})
If the request takes longer than 200ms, there's a chance a new request begins before the old one is done - potentially even that the new request is completed first.
How to synchronize this?
Note that this has nothing to do with multithreading, and that's the only thing I could find information about when googling for "rx synchronization" or something similar.
You could use concatMap operator which will start working on the next item only after previous was completed.
Here is an example where events$ appear with the interval of 200ms and then processed successively with a different duration:
const { Observable } = Rx;
const fakeWriteRequest = data => {
console.log('started working on: ', data);
return Observable.of(data).delay(Math.random() * 2000);
}
const events$ = Observable.interval(200);
events$.take(10)
.concatMap(i => fakeWriteRequest(i))
.subscribe(e => console.log(e));
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>
I'm working on a turn-based game in Angular that communicates to a backend via a socket.io implementation. In my component, I am listening for several types of communication from the server, each communication gives information on how to update my view to reflect the current state of the data in the server.
Right now, updates are immediately applied to the component's data. However I'd prefer to render each update with some delay in-between, so that the user has time to see the effect of each update.
(See my image at top for essentially what I'm trying to do)
I believe that I would achieve this via the subscribeOn operator, but unsure of how to specify my 'interval' n.
const example = Rx.Observable
.create(observer => {
observer.next(0);
observer.next(1);
observer.next(2);
setTimeout(() => {
observer.next(3);
observer.next(4);
observer.complete();
}, 2500);
});
const source = example
.subscribeOn(Scheduler.timeout);
source.subscribe(console.log);
Use the concatMap operator as follows:
const nInterval = 500;
const example$ = Rx.Observable.from([0, 1, 2])
.concat(Rx.Observable.from([3,4]).delay(2500));
const source$ = example$
.concatMap(item =>
Rx.Observable.of(item)
.concat(
Rx.Observable.of('ignored')
.delay(nInterval)
.ignoreElements()
)
);
source$.subscribe(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.2/Rx.min.js"></script>