I am facing a peculiar issue. My component subscribes to a Subject. I receive data from the associated observable when first error is called but not from the subsequent ones. I have not unsubscribed.
I sign up with details of an existing user. I get error the first time (right behaviour). Then I again click sign up button to send the same request but this time the component doesn't receive the message from error.
Interestingly, if I use next instead of error then the code works ok. Does observable stops working if error is called ?
The code snippets
component.ts
ngOnInit() {
this.userSignupSubscription = this.subscribeToSignupAttempt();
this.createForm();
}
subscribeToSignupAttempt() {
return this.userManagementService.userSignUpState$.subscribe(
(res: Result) => { console.log('signup response ',res);this.handleSignupResponse(res); },
(error: Result) => { console.log('signup error response ',error);this.handleSignupErrorResponse(error); },
);
}
the backend service sending data is
#Injectable()
export class UserManagementService{
...
private signUpStateSubject: Subject<Result>;
public userSignUpState$: Observable<Result>; // naming convention for Streams has $ in the end.
constructor(){
this.signUpStateSubject = new Subject<Result>();
this.userSignUpState$ = this.signUpStateSubject.asObservable();
}
addUser(){
...
(error: ServerResponseAPI) => { //This code send the message.
console.log("got error from the Observable: ",error);
const errorMessage: string = this.helper.userFriendlyErrorMessage(error);
this.signUpStateSubject.error(new Result(errorMessage, error['additional-info'])); //change this to .next and the code works
}
}
see the pic below
It seems that I should use next instead of error because it is how rxjs works. It is the contract of rxjs. error means that the stream is not operational anymore and new one would need to be opened.
Related
I am building a logger object that asynchronously obtains the IP address and then logs all values with this IP address. It must start collecting logs as early as it is instantiated, but emit them only after the IP address has been obtained; and after that it should emit as normal.
Here is my class:
class LoggerService {
constructor() {
let thisIp;
const getIp = Observable.create(function(observer) {
// doing it with a timeout to emulate bad network
setTimeout(() => {
fetch('https://api.ipify.org?format=json').then(response => response.json()).then(response => {
thisIp = response.ip;
console.log('fetched IP: ', thisIp);
observer.next(response.ip);
observer.complete();
});
}, 5000)
});
// this is where I plan to buffer logs until IP is obtained
this.logStream = new Subject().pipe(buffer(getIp));
// for starters - just log to the console with the IP address
this.logStream.subscribe((value) => console.log(thisIp, value));
}
emit = (message) => this.logStream.next(message);
}
But it does not work as I need; it does output all buffered values as an array but stops emitting them after the IP has been obtained:
const logger = new LoggerService();
setInterval(() => {
logger.emit('Hey ' + Math.random())
}, 1000);
// I get five messages and that's it
How do I make it emit my values even after buffering?
Update:
Looking back at this a year and a half later, I notice combineLatest/of aren't necessary; we can simply pipe the ip observable and map it to the desired shape. Here's how I would do it today:
export class LoggerService {
private messages$ = new Subject<string>();
private formattedMessages$ = this.messages$.pipe(
mergeMap(message => this.service.ipAddress$.pipe(
map(ip => `[${ip}] ${message}`)
))
);
constructor(private service: GenericService) {
this.formattedMessages$.subscribe(
message => console.log(message) // actual logging logic goes here...
);
}
public log(message: string) {
this.messages$.next(message);
}
}
Original Answer
You don't need to "buffer" the values per se, but you can rather create a stream that depends on the async ipAddress$, so the value won't get emitted until the ip address had been emitted. combineLatest will work well for this purpose.
Let's give the LoggerService a message stream called message$ and a simple log() method that pushes the provided string through this stream.
We can construct a stream of messagesWithIpAddresses$ that use combineLatest to create an observable that emits the provided message along with the ipAddress$, but only after both have actually emitted a value.
export class LoggerService {
private messages$ = new Subject<string>();
public log(message: string): void {
this.messages$.next(message);
}
constructor(service: GenericService) {
const messagesWithIpAddresses$ = this.messages$.pipe(
mergeMap(message => combineLatest(service.ipAddress$, of(message)))
);
messagesWithIpAddresses$.subscribe(
([ip, message]) => {
// actual logging logic would go here...
console.log(`[${ip}] ${message}`);
}
);
}
}
Since of(message) will emit immediately, we will just be waiting for ipAddress$. But, if a value has already been emitted, then it too will be immediate.
Check out this working StackBlitz
Update: below answer won't work. After viewing buffer documentation
your logger Subject() still won't emit anymore messages because it fires only when getIp fires. In your code, getIp fires only once, after the http request.
I think we need more details of what you want to achieve in order to propose a correct rxjs pipeline.
I would remove this line
observer.complete(); // remove it
That signals the subscriptor the stream is over, that's why you are not receiving any messages more.
I have this link that changes the final of the url with a time stamp:
getAvatar(channelId): BehaviorSubject<string> {
return new BehaviorSubject(`${link}?${new Date().getTime()}`);
}
And in the Avatar Component, that is a child of at least 10 other components i call this subscribe:
getAvatar() {
this.userService.getAvatar(this.channelId)
.subscribe(res => {
this.avatar = res;
this.cdr.detectChanges();
this.cdr.markForCheck();
});
}
OBS: Im using the OnPush changeDetection strategy
And in another component i have a function that changes this profile picture inside the link:
this.userService.changeProfilePicture(picture, this.myChannelId)
.subscribe(
() => {
this.loading.hide();
this.userService.getAvatar(this.id);
this.screenService.showToastMessage('Foto de perfil alterada.', true);
}
As you can see, im recalling the getAvatar() function that returns a BehaviorSubject to generate another link and the AvatarComponent doenst detect the change of the behaviorSubject, what im doing wrong ?
And theres another way to recall the getAvatar() function of all the AvatarComponent instances to reload each avatar instance ?
OBS2: I tried to use of rxjs operator, creating a new Observable(), tried the Subject class, all of those seems to not get detected by the subscribe inside the AvatarComponent
OBS3: I tried to get the AvatarComponent with #ViewChild() and call this this.avatarCmp.getAvatar(); to reload the avatar, but reloads just one instance of the Avatar Component
your service needs to be more like this:
private avatarSource = new BehaviorSubject(`${link}?${new Date().getTime()}`);
avatar$ = this.avatarSource.asObservable();
setAvatar(channelId): BehaviorSubject<string> {
this.avatarSource.next(`${link}?${new Date().getTime()}`);
}
then you need to subscribe to avatar$ and update with setAvatar
I'm using rxjs with NodeJS in backend.
I have a Rest API which allow consumers to run remote yarn installation process. The install function returns an observable of the process. So when the module is installed successfully it emits a value in the observable and complete. At this point, the Rest API will returns a response to the user to say that the installation is successful. In case that the installation fails, the process will throw an Error in the stream and the Rest API returns another response with the error information.
My issue is:
The API is called multiple times in parallel by consumers, so there will be a parallel installations in the backend.
I tried throttle operator to create a queue but it keeps the first stream active. So if the first process is "completed", it returns "true" but the stream doesn't complete
export class MyService {
// the function called by the REST API
installGlobal(moduleName: string): Observable < boolean > {
// I think, there are something to do here to make it queuing
return this.run('yarn', ['global', 'add', moduleName]);
}
private run(cmd: string, args: string[]): Observable < boolean > {
const cmd$ = fromPromise(spawn(cmd, args)).pipe(
map(stdout => {
this.logger.info(`Install Module Successfully`);
this.logger.info(`stdout: ${stdout.toString()}`);
return true;
}),
catchError(error => {
const errorMessage: string = error.stderr.toString();
return _throw(errorMessage.substr(errorMessage.indexOf(' ') + 1));
})
);
return cmd$;
}
}
My expectation:
Either there are multiple request, they must be queued. So the first one will be treated and all parallel onces must be queued. When the first is processed, it must returns the response to the API consumers (like 200 completed) and resume the next stream from the queue.
[UPDATE-01 July 2019]: adding an example
You can fetch a demo of the code at stackblitz
I have reimplemented the existant code and i'm simulating my API call by subscribing multi time to the service which will call the queue
A simple queque in Rxjs can be done like below
const queque=new Subject()
// sequential processing
queue.pipe(concatMap(item=>yourObservableFunction(item)).subscribe()
// add stuff to the queue
queque.next(item)
So i have pretty straight forward scenario. One subject and observable. When client logs in i publish success, when user logs out i publish false.
Problem is in subscribe method in LoginComponent
First time everything works great. User logs in i get one event, but after that when user logs out second time and logs in again i get 2 same events, again if user logs out and then logs in i get 3 duplicate events and so on.
AuthService.ts
public _loggedIn: Subject<LoggedInOrResetPassword> = new Subject();
public loggedId: Observable<LoggedInOrResetPassword> = this._loggedIn.asObservable();
obtainAccessToken(){
// ommitted
this.httpClient.post(environment.baseUrl + url, null, requestOptions)
.subscribe(data => {
this.saveToken(data);
this._loggedIn.next(LoggedInOrResetPassword.createTrue());
});
// ommitted
}
private logout(navigateTo?: string){
this._loggedIn.next(LoggedInOrResetPassword.createFalse());
}
LoginComponent.ts
ngOnInit() {
this.authservice.loggedId.subscribe( ( loggedInOrResetPassword: LoggedInOrResetPassword ) => {
// HERE I GET DUPLICATE VALUES
});
The reason is that you are NOT unsubscribing when LoginComponent is destroyed.
Your code should be changed as follows
First add an instance property to LoginComponent to store the subscription, such as
export class LoginComponent implements OnInit, OnDestroy {
.....
loginSubscription: Subscription;
.....
}
Then change ngOnInit so that you store the subscription in the newly added property
ngOnInit() {
this.loginSubscription = this.authservice.loggedId.subscribe( ( loggedInOrResetPassword: LoggedInOrResetPassword ) => {
// HERE I GET DUPLICATE VALUES
});
Eventually add ngOnDestroy to make sure you unsubscribe when the component gets destroyed
ngOnDestroy {
if (this.loginSubscription) {
this.loginSubscription.unsubscribe();
}
}
Take a look at the async pipe of Angular as an alternative method to subscribe to Observables and automatically unsubscribe.
So I am working on couple of cases in my app where I need the following to happen
When event triggered, do the following
List item
check if the data with that context is already cached, serve cached
if no cache, debounce 500ms
check if other http calls are running (for the same context) and kill them
make http call
On success cache and update/replace model data
Pretty much standard when it comes to typeahead functionality
I would like to use observables with this... in the way, I can cancel them if previous calls are running
any good tutorials on that? I was looking around, couldn't find anything remotely up to date
OK, to give you some clue what I did now:
onChartSelection(chart: any){
let date1:any, date2:any;
try{
date1 = Math.round(chart.xAxis[0].min);
date2 = Math.round(chart.xAxis[0].max);
let data = this.tableService.getCachedChartData(this.currentTable, date1, date2);
if(data){
this.table.data = data;
}else{
if(this.chartTableRes){
this.chartTableRes.unsubscribe();
}
this.chartTableRes = this.tableService.getChartTable(this.currentTable, date1, date2)
.subscribe(
data => {
console.log(data);
this.table.data = data;
this.chartTableRes = null;
},
error => {
console.log(error);
}
);
}
}catch(e){
throw e;
}
}
Missing debounce here
-- I ended up implementing lodash's debounce
import {debounce} from 'lodash';
...
onChartSelectionDebaunced: Function;
constructor(...){
...
this.onChartSelectionDebaunced = debounce(this.onChartSelection, 200);
}
For debaunce you can use Underscore.js. The function will look this way:
onChartSelection: Function = _.debounce((chart: any) => {
...
});
Regarding the cancelation of Observable, it is better to use Observable method share. In your case you should change the method getChartTable in your tableService by adding .share() to your Observable that you return.
This way there will be only one call done to the server even if you subscribe to it multiple times (without this every new subscription will invoke new call).
Take a look at: What is the correct way to share the result of an Angular 2 Http network call in RxJs 5?