switchMap does not seem to complete when the inner observable completes - rxjs

I'm still a noob when it comes to RxJS but here's a JSBin of what I am trying to do.
https://jsbin.com/wusalibiyu/1/edit?js,console
I have an observable 'a' (in my case it's the current active connection) which emits a new object whenever the connection reconnects. It's an observable itself because it can re-emit a new value.
I now want an observable that completes whenever an action is executed on the current connection. That action notifies it is done when the observable for it completes. This is b.
The problem is that when the inner observable completes, the outer does not complete. How to make the outer observable complete ... . Is there a different operator I should be using in RxJS5?

If I understand your requirement correctly, you can "lift" the inner stream out using a materialize/dematerialize pair (note I refactored as well as part of my never ending war to get people to stop using Observable#create).
JsBin (excerpt below)
function b(a) {
// Emit and complete after 100 millis
return Rx.Observable.timer(100)
// Ignore any values emitted
.ignoreElements()
// Emit the value on start
.startWith(a)
.do(() => console.log('creating observable'))
.finally(() => console.log('b done'));
}
var a$ = Rx.Observable.from(['a', 'b'])
.finally(() => console.log('a done'));
var result$ = a$.switchMap(function(a) {
console.log('switching map for a to b', a);
// This "materializes" the stream, essentially it maps complete -> next
return b(a).materialize();
})
// This does the opposite, and converts complete events back,
// but since we are now in the outer stream
// this results in the outer stream completing as well.
.dematerialize()
.share();
result$.subscribe(function(value) {
console.log('value', value);
}, function(e) {
console.error('e', e);
}, function() {
console.log('completed!');
})
result$.toPromise().then(function(data) {
console.log('this should trigger!?', data);
}, function(e) {
console.error('boom', e.toString());
});

Related

Nestjs #Sse : return result of a promise in rxjs observable

I am trying to go a simple step beyond the nest doc example in implementing #Sse() in a controller but I never used rxjs untill now so Im a bit confused.
The flow is :
client send a POST request with a file payload
server (hopefully) sends back the newly created project with a prop status:UPLOADED
client subscribe to sse route described below passing as param the projectId it just received from server
in the meantime server is doingSomeStuff that could take from 10sec to a min. When doingSomeStuff is done, project status is updated in db from UPLOADED to PARSED
My need is for the #Sse decorated function to execute at x interval of time a "status-check" and return project.status (that may or may not have been updated at the time)
My present code :
#Sse('sse/:projectId')
sse(#Param('projectId') projectId: string): Observable<any> {
const projId$ = from(this.projectService.find(projectId)).pipe(
map((p) => ({
data: {
status: p.status,
},
})),
);
return interval(1000).pipe(switchMap(() => projId$));
}
I don't put code of the service here as it a simple mongooseModel.findById wrapper.
My problem is the status returned remains UPLOADED and is never updated. It doesnt seem the promise is reexecuted at every tick. If I console.log inside my service I can see my log being printed only once with the initial project value while I expect to see a new log at each tick.
This is a two-step process.
We create an observable out of the promise generated by this.service.findById() using the from operator in rxjs. We also use the map operator to set the format of the object we need when someone subscribes to this observable.
We want to return this observable every x seconds. interval(x) creates an observable that emits a value after every x milliseconds. Hence, we use this and then switchMap to the projId$ whenever the interval emits a value. The switchMap operator switches to the inner observable whenever the outer observable emits a value.
Please note: Since your server may take 10 sec, to min for doing the operation, you should set the intervalValue accordingly. In the code snippet below, I've set it to 10,000 milli seconds which is 10 seconds.
const intervalValue = 10000;
#Sse('sse/:projectId')
sse(#Param('projectId') projectId: string): Observable < any > {
return interval(intervalValue).pipe(
switchMap(() => this.projectService.find(projectId)),
map((p) => ({
data: {
status: p.status,
}
})));
}
// OR
#Sse('sse/:projectId')
sse(#Param('projectId') projectId: string): Observable < any > {
const projId$ = defer(() => this.service.findById(projectId)).pipe(
map(() => ({
data: {
_: projectId
}
}))
);
return interval(intervalValue).pipe(switchMap(() => projId$));
}
#softmarshmallow
You can watch model changes and use observable stream to send it.
Something like this
import { Controller, Param, Sse } from '#nestjs/common'
import { filter, map, Observable, Subject } from 'rxjs'
#Controller('project')
export class ProjectStatusController {
private project$ = new Subject()
// watch model event
// this method should be called when project is changed
onProjectChange(project) {
this.project$.next(project)
}
#Sse('sse/:projectId')
sse(#Param('projectId') projectId: string): Observable<any> {
return this.project$.pipe(
filter((project) => project.projectId === projectId),
map((project) => ({
data: {
status: project.status,
},
})),
)
}
}

how to unsubscribe a RXJS subscription inside the subscribe method?

I have some javascript:
this.mySubscription = someObservable.subscribe((obs: any) => {
this.mySubscription.unsubscribe();
this.mySubscription = undefined;
}
on execution, the console logs the error ERROR TypeError: Cannot read property 'unsubscribe' of undefined.
I wonder why I can not unsubscribe inside the subscribe lambda function. Is there a correct way to do so? I have read a bit about using dummy-subjects and completing them or using takeUntil/takeWhile and other pipe operators workArounds.
What is a correct way/workaround to unsubscribe a subscription inside the subscription's subscribe-function?
I am currently using a dummy subscription like so:
mySubscription: BehaviorSubject<any> = new BehaviorSubject<any>(undefined);
// when I do the subscription:
dummySubscription: BehaviorSubject<any> = new BehaviourSubject<any>(this.mySubscription.getValue());
this.mySubscription = someObservable.subscribe((obs: any) => {
// any work...
dummySubscription.next(obs);
dummySubscription.complete();
dummySubscription = undefined;
}, error => {
dummySubscription.error(error);
});
dummySubscription.subscribe((obs: any) => {
// here the actual work to do when mySubscription emits a value, before it should have been unsubscribed upon
}, err => {
// if errors need be
});
You shouldn't try to unsubscribe in the subscribe function.
You can unsubscribe with operators like take, takeWhile or takeUntil.
take
Use take(n) to unsubscribe after someObservable emits n times.
someObservable.pipe(
take(1)
).subscribe(value => console.log(value));
takeWhile
Use takeWhile to unsubscribe when an emitted value fails a condition.
someObservable.pipe(
takeWhile(value => valueIsSave(value))
).subscribe(value => console.log(value));
valueIsSave(value): boolean {
// return true if the subscription should continue
// return false if you want to unsubscribe on that value
}
takeUntil
Use takeUntil(obs$) to unsubscribe when the observable obs$ emits.
const terminate = new Subject();
someObservable.pipe(
takeUntil(terminate)
).subscribe(value => console.log(value));
unsub() {
terminate.next() // trigger unsubscribe
}
If you make your stream asynchronous, what you're doing should work. For example, this will not work:
const sub = from([1,2,3,4,5,6,7,8,9,10]).subscribe(val => {
console.log(val);
if(val > 5) sub.unsubscribe();
});
but this will work:
const sub2 = from([1,2,3,4,5,6,7,8,9,10]).pipe(
delay(0)
).subscribe(val => {
console.log(val);
if(val > 5) sub2.unsubscribe();
});
Because the JS event loop is fairly predictable (blocks of code are always run to completion), If any part of your stream is asynchronous, then you can be sure that your subscription will be defined before your lambda callback is invoked.
Should you do this?
Probably not. If your code relies on the internal (otherwise hidden) machinations of your language/compiler/interpreter/etc, you've created brittle code and/or code that is hard to maintain. The next developer looking at my code is going to be confused as to why there's a delay(0) - that looks like it shouldn't do anything.
Notice that in subscribe(), your lambda has access to its closure as well as the current stream variable. The takeWhile() operator has access to the same closure and the same stream variables.
from([1,2,3,4,5,6,7,8,9,10]).pipe(
takeWhile(val => {
// add custom logic
return val <= 5;
})
).subscribe(val => {
console.log(val);
});
takeWhile() can to anything that sub = subscribe(... sub.unsubscibe() ... ), and has the added benefit of not requiring you to manage a subscription object and being easier to read/maintain.
Inspired by another answer here and especially this article, https://medium.com/#benlesh/rxjs-dont-unsubscribe-6753ed4fda87, I'd like to suggest takeUntil() with following example:
...
let stop$: Subject<any> = new Subject<any>(); // This is the one which will stop the observable ( unsubscribe a like mechanism )
obs$
.pipe(
takeUntil(stop$)
)
.subscribe(res => {
if ( res.something === true ) {
// This next to lines will cause the subscribe to stop
stop$.next();
stop$.complete();
}
});
...
And I'd like to quote sentence RxJS: Don’t Unsubscribe from those article title mentioned above :).

WithLatestFrom($foo) not emitting when $foo emits, possibly because of the use of merge

I am implementing an observable which can be subscribed to before it is "assigned" Think of it like hoisting an observable definition so I dont have to worry about the order in which I create observables derived from other observables, I call it a ColdSubject.
ColdSubject works fine (I can add observables to it, and only when somebody subscribes to the ColdObservable do its operators get evaluated).
However withLatestFrom will never emit while waiting for obs$, despite the observable it's "waiting for" emitting to a subscriber several times!
export class ColdSubject<T> {
// If you subscribe to this before an observable has been added to $acc, you will be notified as soon as one is added, and if you subscribe to this after an observable is added to acc$ you will also be notified
public obs$: Observable<T>;
public acc$ = new BehaviorSubject<Observable<T>>(merge());
constructor() {
this.obs$ = this.acc$.pipe(switchMap(v => v));
}
addObservable(newObservable: Observable<T>) {
this.acc$.next(merge(this.acc$.getValue(), newObservable))
}
}
const foo = new ColdSubject<number>();
# I know this observable is waiting for withLatestFrom because "Tap yeet" is logged
of('yeet').pipe(
tap(v => console.log(`tap ${v}`)),
withLatestFrom(foo.obs$)
).subscribe(v => {
console.log(`WithLatestFrom ${v}`);
});
# This observable will begin emitting 5 seconds into the script, because I wait 5 seconds to subscribe to it
foo.addObservable(
interval(1000).pipe(
take(5),
tap(v => console.log(`Interval ${v}`))
)
);
# Subscribe 5 seconds into script start, so I know that my ColdSubject only evaluates its observables once they're subscribed to
setTimeout(
() => foo.obs$.subscribe(v => console.log(`Subscribe ${v}`)),
5000
);
Why does foo.obs$ emit several times, while the operation waiting for its latest value not emit?
Looking at the source code one can see that withLatestFrom is triggered by_next which is fired by the source Observable calling next:
protected _next(value: T) {
if (this.toRespond.length === 0) {
/**
* value - emitted by the source Observable
* ...this.values - emitted by the Observables passed to `withLatestFrom`
*/
const args = [value, ...this.values];
if (this.project) {
this._tryProject(args);
} else {
this.destination.next(args);
}
}
}
Your issue is that your source completes right away, while the Observable passed to withLatestFrom has not emitted yet. by the time foo.obs emits, your source Observable has long since completed.
What I would recommend of using in your case is combineLatest as demonstrated below:
combineLatest(of("yeet"), foo.obs$)
.pipe(
tap(v => console.log(`tap ${v}`)),
)
.subscribe(v => {});
of('yeet') emits and is complete so withLatestFrom will complete as the source is complete.
Change your subscription to
of('yeet').pipe(
tap(v => console.log(`tap ${v}`)),
withLatestFrom(foo.obs$)
).subscribe({ complete: () => console.log('yeet complete') });
and you will see it is infact complete.
https://stackblitz.com/edit/rxjs-6-opeartors-2nuam1?file=index.ts

Repeat or restart an observable (interval) after its completed

Here is what I am trying to do - step #3 is what I am struggling with.
Set custom interval
trigger an http call (subscribe to it)
Once interval is finished, I want to repeat the above steps indefinitely (until a condition is met)
Steps #1 & #2 are working
setRenewalInterval() {
...
return this.timerSub = interval(CUSTOM_INTERVAL * 1000)
.pipe(
take(1),
map(x => {
if (this.isLoggedIn && !tokenHelper.isTokenExpired()) {
console.log("<Requesting Token Renew>")
this.getNewToken().subscribe(
x => {
console.log("<Token renewed successfully>");
},
err => {
console.log("<Token failed to renew>");
this.setLogOut();
},
() => console.log('<renewal observable completed>')
);
}
else {
console.log("<Not Logged in or Token expired; logging out>");
this.setLogOut();
}
})
)
.subscribe();
}
#3 repeat the above steps after the subscription is completed. Either call setRenewalInterval() or preferably do it inside the setRenewalInterval().
I have looked into rxjs repeat() and expand() but I can't get them to work with the above code. Any help is greatly appreciated, especially a testable/runnable code e.g. jsfiddle.
You're really close, interval(...) is already creating an emission indefinitely however take(1) is causing the entire observable to complete after the first emission.
If you remove the take(1) your code inside the map will be called at your interval forever.
map is a useful function when you want to take values emit in an observable and convert it to a new value synchronously, similar to how Array.map() works. Because you're working with observables inside this map we can use something like mergeMap() which will handle the subscription and piping through of values automatically.
setRenewalInterval() {
// ...
return this.timerSub = interval(CUSTOM_INTERVAL * 1000)
.pipe(
mergeMap(() => {
if (this.isLoggedIn && !tokenHelper.isTokenExpired()) {
return this.getNewToken();
} else {
return this.setLogOut();
}
})
)
.subscribe(x => {
// x is the result of every call to getNewToken() or setLogOut()
});
}

RxJs - forkJoin with empty array

I'm currently using forkJoin to wait for an array of Observable(s) to finish before pipe(ing) and tap(ping).
I noticed if the array is empty nothing is emitted and I cannot even tap. How do I solve this kind of problem? Should I just check if the array is empty?
myFirstFunction(...) {
const observables = ...
return forkJoin(observables)
}
mySecondFunction(...) {
return myFirstFunction().pipe(tap(() => ...))
}
That's because forkJoin requires all source Observables to emit at least one item and when there are no source Observables there's nothing to emit. However, forkJoin will still send the complete notification so you can use for example defaultIfEmpty operator to make sure it always emits at least one next.
forkJoin(observables).pipe(
defaultIfEmpty(null),
).subscribe(...);
Demo: https://stackblitz.com/edit/rxjs-kkd1qa?file=index.ts
Additionally to martin's answer.
I had 2 observables returning arrays and if one of them gives me an empty array, it did not wait for the other observable to finish and completed instantly. You can handle such cases as follows using defaultIfEmpty.
const response1: Observable<any[]> = this.service.getResponse(params1).pipe(defaultIfEmpty([]));
const response2: Observable<any[]> = this.service.getResponse(params2).pipe(defaultIfEmpty([]));
Observable.forkJoin(response1, response2).subscribe((response) => {
console.log(response);
}, () => {
console.log('handle error here');
}, () => {
console.log('completed');
});

Resources