change the source of observable at runtime (rxjs) - javascript

i need to change the source of an observable with a swith
.
this.su = this.appService._sub1.subscribe(data => {
this.items.push(data);
});
//appService
setSub(name) {
if (name == 'A') {
console.log('B');
this._sub1 = this.sub2;
} else if (name == 'B') {
console.log('B');
this._sub1 = this.sub3;
}
console.log(this._sub1);
}
however, the source of the first observable keeps sending data, how can I do it?
Stackblitz link
https://stackblitz.com/edit/angular-ivy-evczcx?file=src/app/app.service.ts

Ultimately, I think you will want to use switchMap. You don't really want to switch the subscription, but rather the source.
It's cleaner if you design your service to expose observables, then have your components subscribe to them (ideally with AsyncPipe).
As a sample, something like this should work:
export class AppService {
private source$ = new BehaviorSubject<SourceType>('frutas');
private frutas = ['pera', 'manzana', 'platano', 'fresa'];
private electronicos = ['celular', 'pc', 'cable'];
private frutas$ = interval(2000).pipe(
map(n => n % 4),
map(i => this.frutas[i])
);
private electronicos$ = interval(2000).pipe(
map(n => n % 3),
map(i => this.electronicos[i])
);
data$ = this.source$.pipe(
switchMap(source => source === 'frutas' ? this.frutas$ : this.electronicos$)
);
setSource(name: SourceType) {
this.source$.next(name);
}
}
Here's a working StackBlitz demo.

Swapping a reference to a subscription does nothing to the subscription itself. You need to unsubscribe. Also keep in mind, that interval produces a "hot" observable. Meaning it will push data to the stream wether someone has subscribed or not.
In real life, you would probably use something like httpClient.get() which is not "hot" (runs only when subscribed to, and completes once it's done). So it's not an issue usually, but with interval, you will end up with memory leaks and performance hits unless you manually stop the scheduler from running.
this._sub1.unsubscribe();

Related

Avoid subscribing in Angular Service using RxJS?

I am working on an angular project that needs to load up pages and then display them one by one / two by two.
As per this article and some other sources, subscribing in services is almost never necessary. So is there a way to rewrite this in pure reactive style using RxJS operators?
Here's what I have (simplified) :
export class NavigationService {
private pages: Page[] = [];
private mode = Mode.SinglePage;
private index = 0;
private currentPages = new BehaviorSubject<Page[]>([]);
constructor(
private pageService: PageService,
private view: ViewService,
) {
this.pageService.pages$.subscribe(pages => {
this.setPages(pages);
});
this.view.mode$.subscribe(mode => {
this.setMode(mode);
});
}
private setPages(pages: Page[]) {
this.pages = pages;
this.updateCurrentPages();
}
private setMode(mode: Mode) {
this.mode = mode;
this.updateCurrentPages();
}
private updateCurrentPages() {
// get an array of current pages depending on pages array, mode & index
this.currentPages.next(...);
}
public goToNextPage() {
this.index += 1;
this.updateCurrentPages();
}
public get currentPages$() {
return this.currentPages.asObservable();
}
}
I've tried multiple solutions and didn't manage to get it right. The closest I got was using scan(), but it always reset my accumulated value when the outer observables (pages, mode) got updated.
Any help is appreciated, thanks !
You can use merge to create reducer functions from observables. These functions will update part of a state maintained by the service. They are past along to the scan operator which will update the prior state from the reducer. After the reducer is run, currentPages is set on the new state and that new state is returned.
export class NavigationService {
private readonly relativePageChangeSubject = new Subject<number>();
readonly state$ = merge(
this.pageService.pages$.pipe(map((pages) => (vm) => ({ ...vm, pages }))),
this.relativePageChangeSubject.pipe(map((rel) => (vm) => ({ ...vm, index: vm.index + rel }))),
this.view.mode$.pipe(map((mode) => (vm) => ({ ...vm, mode })))
).pipe(
startWith((s) => s), // if necessary, force an initial value to be emitted from the initial value in scan.
scan((s, reducer) => {
const next = reducer(s);
// update currentPages on the next state here.
return next;
}, { currentPages: [], index: 0, mode: Mode.SinglePage, pages: [] }),
shareReplay(1)
)
readonly currentPages$ = this.state$.pipe(
map(x => x.currentPages),
distinctUntilChanged()
);
constructor(private pageService: PageService, private view: ViewService) { }
goToNextPage() {
this.relativePageChangeSubject.next(1);
}
}
Notes:
Instead of having a nextPage Subject, a more flexible relative change subject is used that will modify the index from the value in the prior state.
The currentPage$ observable isn't necessary, as a consumer could just attach to the main state$ and map as needed. Feel free to make state$ private or remove currentPage$.
Let's first detail what you are doing:
You subscribe to pageService.pages$ and view.mode$
Those subscriptions take the values and put them in a private variabkle
Then fire a function to use those two variables
Finally, trigger a value push.
All this can be done in a simple pipeline. You'd need to include the index as an observable (behaviour subject in our case) to react to that change too.
Use combineLatest, this will subscribe to all observables we want, and trigger the pipe WHEN ALL have fired once, and every time one changes afterwards. You may want to use .pipe(startWith("something")) on observables that should have a default value so your observable pipe triggers asap.
CombineLatest will then provide an object as value, with each value in the object key passed when created. Here pages, mode and index. I've used a switchMap to demo here if updateCurrentPages passes an observable, but you could use a map if there is no async task to be done.
export class NavigationService {
readonly currentPages$:Observable<Pages[]>;
constructor(
private pageService: PageService,
private view: ViewService,
) {
this.paginator = new Paginator(this.pageService.pages$);
this.currentPages$ = combineLatest({
pages:this.pageService.pages$,
mode:this.view.mode$,
index:this.this.paginator.pageChange$
}).pipe(
switchMap(({pages,mode,index})=>{
return this.updateCurrentPages(pages,mode);
}),
);
}
private updateCurrentPages() {
// get an array of current pages depending on pages array, mode & index
this.currentPages.next(...);
}
public goToNextPage() {
this.paginator.next();
}
}
class Paginator{
pageChange$ = combineLatest({
total:this.pages$.pipe(map(pages=>pages.length)),
wanted:this.pageMove$}).pipe(map({total,wanted}=>{
// Make sure it is between 0 and maximum according to pages.
return Math.max(Math.min(total-1,wanted),0);
}),
// Do not emit twice the same page (pressing next when already at last)
dinstinctUntilChanged());
);
pageMove$ = new BehaviorSubject<number>(0);
constructor(pages$: Observable<Pages[]>){
}
next(){
this.pageMove$.next(this.pageMove$.value()+1);
}
previous(){
this.pageMove$.next(this.pageMove$.value()-1);
}
to(i:number){
this.pageMove$.next(i);
}
}
Beware tough about stating that subscribe is never needed. You may want to subscribe to some events for some reason. It is just that combining everything in a pipeline makes things easier to handle... In the example above, the observables will be unsubscribed to when the consumer of your service unsubscribes to currentPages$. So one thing less to handle.
Also, note that if multiple consumers subscribe to this service's currentPages$ the pipeline will be duplicated and unecessary work will be done, once for each subscriber. While this MAY be good, you might want to have everyone subscribe to the same "final" observable. This is easily do-able by adding share() or shareReplay(1) at the end of your pipeline. Share will make sure the same observable pipeline will be used for the new subscriber, and they will receive new values starting from then. Using shareReplay(1), will do the same but also emit the latest value directly on subscribe (just like BehaviourSubject) the 1 as parameter is indeed the number of replays to send out...
Hope this helps! When you master the RxJS you'll see that things will get easier and easier (see the difference in code amount!) but getting the hang of it take a little bit of time. Do not worry, just perseverate you'll get there. (Hint to get better, using outside variables/properties are the evil of handling pipelines)

Rxjs/lodash throttle - how to use it on condition and in case that the condition may change while app runing?

I'm listening to the observable that may return true or false value - the only thing that I want to do is to set throttleTime for function call when it's true and don't have it when it's false. So I did some kind of workaround for that but I don't like this solution. I have tried a different approach where I tried to do it in the actions' effect but without success..
So this is the observable:
this.store$
.pipe(
takeUntil(this.componentDestroyed$),
select(selectGlobalsFiltered([
GlobalPreferencesKeys.liveAircraftMovement])),
distinctUntilChanged()
)
.subscribe((globals) => {
if (globals && globals[GlobalPreferencesKeys.liveAircraftMovement] !== undefined) {
this.isLiveMovementEnabled = (globals[GlobalPreferencesKeys.liveAircraftMovement] === 'true');
}
if (!this.isLiveMovementEnabled) {
this.processPlaneData = throttle(this.processPlaneData, 4000);
} else {
this.processPlaneData = this.notThrottledFunction;
}
});
And as you can see I've created excat the same method that is 'pure' - notThrottledFunction and I'm assigning it when it's needed.
processPlaneData(data: Airplane[]): void {
this.store$.dispatch(addAllAirplanes({ airplanes: data }));
}
notThrottledFunction(data: Airplane[]): void {
this.store$.dispatch(addAllAirplanes({ airplanes: data }));
}
So basically this is working solution, but I'm pretty sure there is a better approach for doing such a things.
*throttle(this.processPlaneData, isLiveMovementEnabled ? 0 : 4000) doesn't work
So the second approch where I tried to do this inside of effect, I added a new argument for addAllAirplanes action - isLiveMovementEnabled: this.isLiveMovementEnabled
addAllAirplanes$ = createEffect(() =>
this.actions$.pipe(
ofType(ActionTypes.ADD_ALL_AIRPLANES),
map((data) => {
if (data.isLiveMovementEnabled) {
return addAllAirplanesSuccessWithThrottle(data);
} else {
return addAllAirplanesSuccess(data);
}
}
)
);
And then I added another effect for addAllAirplanesSuccessWithThrottle
addAllAirplanesThrottle$ = createEffect(() =>
this.actions$.pipe(
ofType(ActionTypes.ADD_ALL_AIRPLANES_THROTTLE),
throttleTime(4000),
map((data) => addAllAirplanesSuccess(data))
)
);
But it doesn't work. Can someone help me?
(It's not clear how the data arrives in your example code, but I'll assume an Observable)
throttle and throttleTime are similar to your use case, but I think it makes sense to build your own custom implementation without them. I'd suggest managing the timing yourself, using Date to determine time deltas.
You've already cached the live filtering boolean in the component state, so we can just handle all of the data stream from your position update observable and filter them out manually (which is all throttle does, but it expects you to be able to feed it an interval at subscription time, and yours needs to be dynamic).
Setup component scoped variables to contain previous timestamps, as
private prevTime: number;
private intervalLimit: number = 4000;
Supposing data$ is your input plane position data stream:
data$.pipe(filter(data => {
const now: number = Date.now();
const diff = now - this.prevTime;
if (this.isLiveMovementEnabled) {
// no throttle - pass every update, but prepare for disabling too
// record when we last allowed an update & allow the update
this.prevTime = now;
return true;
} else if (diff > intervalLimit) {
// we are throttling results, but this one gets through!
this.prevTime = now;
return true;
} else {
// we're throttling, and we're in the throttle period. eat the result!
return false;
}
}
Something like that gives you full control over the logic used whenever data comes in. You can add other operations like takeUntil and distinctUntilChanges into the pipe and trust that when you subscribe you'll be getting updated when you want them.
You can even adjust the intervalLimit to dynamically adjust the throttle period on the fly.

Polling with RxJS that can recover missed events

I am trying to use RxJS to poll for events. However, I only have access to one function, which is getEvent(). I can do 2 things with the getEvent function:
getEvent("latest") — this will give me the latest event object
getEvent(eventId) - I pass in an integer and it will give me the event object corresponding to the eventId.
Event IDs always increment from 0, but the problem is, if my polling interval isn't small enough, I might miss events.
For example, if I do a getEvent("latest") and I get an event that has an ID of 1, that's great. But if the next time I call it, I get an ID of 3, I know that I missed an event.
In this case, I want to use a higher-order observable to call getEvent(2) and getEvent(3) so that the consumer of the stream I am creating won't have to worry about missing an event.
Right now, all I have is something like this:
timer(0, 500).pipe(
concatMap(() => from(getEvent("latest"))
)
For some context, I'm working off of this blogpost: https://itnext.io/polling-using-rxjs-b56cd3531815
Using expand to recursively call GET fits here perfectly. Here is an example with DEMO:
const source = timer(0, 2000)
const _stream = new Subject();
const stream = _stream.asObservable();
const s1 = source.pipe(tap(random)).subscribe()
const sub = stream.pipe(
startWith(0),
pairwise(),
concatMap((v: Array<number>) => {
let missing = v[1] - v[0];
return missing ? getMissing(v[0], missing) : EMPTY
})
).subscribe(console.log)
function getMissing(start, count) {
return getById(start).pipe(
expand(id => getById(id+1)),
take(count)
)
}
// helper functions for DEMO
let i = 1;
function random() {. // THIS IS YOUR getEvent('latest')
if (i < 10) {
i+=2;
_stream.next(i
// (Math.floor(Math.random() * 8))
)
}
}
function getById(id) {. // THIS IS YOUR getEvent(eventId)
return of(id).pipe(delay(1000)) // delay to mimic network
}

Run action once per Observable's value emission

Suppose we have the following code (Stackblitz):
public counter: number = 0; // Some arbitrary global state
public ngOnInit(): void {
const subj: Subject<number> = new Subject<number>();
const obs: Observable<number> = subj.asObservable().pipe(
tap((): void => {
++this.counter;
}));
// this.counter increments for each subscription.
// But supposed to increment once per obs new value emition.
obs.subscribe();
obs.subscribe();
obs.subscribe();
subj.next(1);
// Now this.counter === 3
}
The result is that this.counter equals 3 after executing the above, it has been incremented one time per each subscription.
But how can we increment it once per Observable's new value emission in a Reactive way?
I know that we should avoid state outside the Reactive flow, but still it is needed time to time in real world apps.
The reason why you see number 3 is a little different. When you use of(1) it emits one next notification and right after that the complete notification. It does this every time you call obs.subscribe().
So if you want to see 1 you need make an intermediate Subject where you make all subscriptions and then this intermediate Subject subscribe to the source Observable (with publish() operator most easily that creates the Subject for you):
const obs: ConnectableObservable<number> = of(1).pipe(
tap((): void => {
++this.counter;
}),
publish(),
) as ConnectableObservable<number>;
obs.subscribe();
obs.subscribe();
obs.subscribe();
obs.connect();
Your updated demo: https://stackblitz.com/edit/rxjs-update-global-state-ejdkie?file=app/app.component.ts
Be aware that of(1) will emit complete notification anyway which means that the subject inside publish() will complete as well. This might (or might not) be a problem for you but it really depends on your use-case.
Edit: This is updated demo where source is Subject: https://stackblitz.com/edit/rxjs-update-global-state-bws36m?file=app/app.component.ts
Try something like this please once. I cannot run it anywhere. If it gives an error let me know
public counter: number = 0;
public items: any = [];
public ngOnInit(): void {
const obs: Observable<number> = of(1).pipe(
tap((value): void => {
if(! Array.prototype.includes(value)) {
this.items.push(value);
++this.counter;
}
}));
// this.counter increments for each subscription.
// How to make it increment once per obs new value emission.
obs.subscribe(1);
obs.subscribe(2);
obs.subscribe(1);
// Now this.counter === 3
}

Angular Observable subscription fallback?

I'm using Angular 4 and I have a component that lives at the 'player/:id' route in my application. When the player navigates to this route from within the application I use a currentPlayerService to sync the current player within the app. This works great with the below code.
this.currentPlayerService.getPlayer()
.subscribe((player) => this.currentPlayer = player);
However, when the application loads directly to the 'player/:id' route (from an external link) I can comment out the above and use the code below to set the currentPlayer from the route params.
this.route.params.subscribe(params => {
this.playerService.getPlayer(params.id)
.subscribe((player) => {
this.currentPlayer = player;
this.currentPlayerService.setPlayer(player);
});
});
What I'd like to do (and am having difficulty finding the "reactive function programming" way of saying) is to load the player from the params only if this.currentPlayerService.getPlayer() doesn't have a value on load. I'm having a hard time conceiving of a way to use the Observable in some kind of logic control flow, so any help would be appreciated.
Thanks!
Observables itself is stateless. To keep track of a value (or current value), you will need to use either Subject or BehaviorSubject.
In your currentPlayerService, create a BehaviorSubject called playerBSubject:
export class CurrentPlayerService{
public playerBSubject: BehaviorSubject<any>;//can be type of Player if you have such type
constructor(){
this.playerBSubject = new BehaviorSubject({})//any value that you want to initialize
}
getPlayer(){
//do your logic here, whatever that is.
//I am using an Observable.of to mimic a http request
Observable.of({})
.subscribe((player)=>{
this.playerBSubject.next(player)//this will update the subject with the latest value
});
return this.playerBSubject.asObservable();
}
}
Note that the .next() method will update the values of your subject, and you can return it as an observable using .asObservable().
Now, in your component controller, you can check if your BehaviourSubject exist (or has the value you want), and only call playerService.getPlayer() if need to.
this.route.params.subscribe(params => {
//check if BehaviorSubject exist or not
if (this.currentPlayerService.playerBSubject.getValue() === {}) {
this.playerService.getPlayer(params.id)
.subscribe((player) => {
this.currentPlayer = player;
this.currentPlayerService.setPlayer(player);
});
}
});
Suggestions:
I am not sure why you need two service namely currentPlayerService and playerService. If your currentPlayerService is just to keep track of "current" value of the player, then you do not need it at all, should you use BehaviorSubject to keep track of your current player. All of them can boil down into one single service.
export class PlayerService {
public playerBSubject: BehaviorSubject<any>;
constructor() {
this.playerBSubject = new BehaviorSubject({})
}
getPlayer(id) {
Observable.of({id}) // implement your own logic
.subscribe((player) => {
this.playerBSubject.next(player)
});
return this.playerBSubject.asObservable();
}
setPlayer(id) {
return Observable.of({id})//implement your own logic
.subscribe(newPlayer => this.playerBSubject.next(newPlayer))
}
}
And in your controller if you want to get the current value you can just do:
this.currentPlayer = this.playerService.playerBSubject.getValue();
And with a little help of asObservable you can do this:
this.playerService
.asObservable()
.subscribe(player => {
if (player === {}) {
this.route.params.subscribe(params => {
this.playerService.getPlayer(params.id); //voila, your player is updated
})
}
//remember to update the value
this.currentPlayer = player
})

Categories