My server is developed on Node.js. It is a long-polling service (e.g. chat): it gives the following API:
join() //listening for new events
align(fromId) //retrieving events from an id
send(data) //creating an event
The long-polling is implemented by the join(): it sends a request and the server answers when there is a new event.
Front end with Ionic2
There are 2 pages: Page1 and Page2. Where Page2 is the viewer of my events, where the long-polling communication is running.
So I start from Page1 and then I push() the second page Page2. Until now everything works fine; but if I pop() the Page2 and then push() again the Page2 then I can see that there is still running the join() of the previous instance of my Page2. This behaviour creates duplicated join(): if I push/pop Page2 many times I will have many long-polling communication with the server.
So I'm trying to find a way to kill the join() instance, which is a HTTP.get request, when leaving the page.
Let's see now my code.
This is the provider of my Ionic2 in charge of the communication with the server
import { Injectable } from '#angular/core';
import { Http, Headers } from '#angular/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
#Injectable()
export class MyProvider {
...
constructor(private http: Http) {
this.token_access = null;
this.token_room = null;
}
...
join(){
let headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('x-access-token',this.getToken());
return Observable.create(observer =>{
this.http.get('/localhost/chat/'+this.room,{headers : headers})
.map(res => res.json())
.subscribe(
data=>{
observer.next(data);
},
(err) =>{
observer.error(err);
}
);
})
}
send(message){
let headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('x-access-token',this.getToken());
headers.append('x-chat-token',this.getRoomToken());
return Observable.create(observer =>{
this.http.post('/localhost/chat/'+this.room+'/send', JSON.stringify({
event: message
}),{headers : headers})
.map(res => res.json())
.subscribe(
data=>{
observer.next(data);
},
(err) =>{
observer.error(err);
}
);
})
}
align(from){
let headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('x-access-token',this.getToken());
headers.append('x-chat-token',this.getRoomToken());
return Observable.create(observer =>{
this.http.post('/localhost/chat/'+this.room+'/align', JSON.stringify({
fromId: from
}),{headers : headers})
.map(res => res.json())
.subscribe(
data=>{
observer.next(data);
},
(err) =>{
observer.error(err);
}
);
})
}
}
The Page1 just push the Page2 with a button that calls the the following code (page1.ts):
...
export class Page1 {
...
constructor(public navCtrl: NavController, public myProviderService: MyProvider) {
}
.....
toPage2(){
this.navCtrl.push(Page2);
}
And my Page2 is implemented by the following code:
import { Component } from '#angular/core';
import { NavController } from 'ionic-angular';
import { MyProvider } from '../../providers/myprovider';
import { Event } from '../../components/event';
#Component({
selector: 'page-chat',
templateUrl: 'chat.html'
})
export class ChatPage {
eventsList: Array<Event>;
message: any;
last_event: any;
msg: any;
constructor(public navCtrl: NavController, public myProviderService: MyProvider) {
this.last_event = -1;
this.join();
this.eventsList= new Array();
}
join(){
this.myProviderService.join().subscribe(
(data)=>{
if(data.success){
this.last_event = this.last_event + 1;
if(this.last_event == data.event.id){
//up to now all events are correctly received
this.eventsList.push(data.event);
}else{
//some events are missing
this.last_event = this.last_event - 1;
this.align();
}
this.join();
}else{
this.message=data.message;
//TBD sleep....
//this.join();
}
},
(err) => {
this.message="Connectivity with server Lost...";
//TBD sleep....
//this.join();
});
}
align(){
this.myProviderService.align(this.last_event + 1).subscribe((data)=>{
if(data.success){
for (var i=0;i<data.events.length;i++) {
this.eventsList.push(new Event(data.events[i].id,data.events[i].data,data.events[i].user));
this.last_event = this.last_event + 1;
};
}else{
this.message=data.message;
}
},
(err) => {
this.message="Failure receiving messages";
});
}
send(): void{
this.myProviderService.send(this.msg).subscribe((data)=>{
if(data.success){
this.msg='';
}else this.message=data.message;
},
(err) => {
this.message="Error while authenticating";
})
}
ionViewDidLoad() {
}
ionViewDidEnter() {
}
}
So coming back to my question:
How can I kill the join() (kill the HTTP.get request) instance of my Page2 when this is not used, in order to prevent duplicated join()?
I think that if you have a provider that is globally added to Providers section of your app (meaning that it can act as a Singleton service), then you can use the following:
Every time Page 2 calls the join() method of your provider check a hasAlreadyJoined boolean variable in your provider.
This variable is set to true every time the join() method is called.
If the join() has not been called, call it and update the variable accordingly.
So, even though every time Page 2 calls the join() method of MyProvider, this method does the actual http request only if hasAlreadyJoined is false.
For you to be sure that every time a MyProvider instance is initiated it's variables are "static", the provider should be declared at the global Providers section of your app module file, not at the page's providers section.
Related
I want to:
Obtain param from url
Then send http request to API with that param
If I done it undirectly via browser url box it works great but when I use router.navigate(["/users", "userName"]); it work to slow.
ngOnInit(): void {
this.sub = this._route.params.subscribe(
params => {
this.userName = params['name'];
});
this._usersService.getUser(this.userName)
.subscribe(user => this.user = user,
error => this.errorMessage = <any>error);
}
This is code inside UserComponent.
And the this._userService is sometimes done before this.sub is.
How to fix it?
You can use switchMap in order to switch from params observable to the observable provided by getUser().
// an observable of users
this.user$ = this._route.params
// switch to the observable provided by getUser() which queries the resource
.switchMap(params => this.getUser(params['name']))
// log error
.catch(error => {
console.log('Error occurred - ' + error.message);
// rethrow
return Observable.throw(error);
})
// share a single subscription among the subscribers so that getUser() will not be called for every subscriber
.share();
Then you can project this observable into another one that is useful for your case, for example:
// an observable of user details to be displayed on template
this.userInfo$ = this.user$.map(user => user.username + ' - ' + user.date.toISOString());
And finally you can use the observable in your template:
The user is: {{ userInfo$ | async }}
See the working sample in this PLUNKER
Full code for the UserComponent:
import { Component, OnInit } from '#angular/core';
import {ActivatedRoute} from '#angular/router';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/share';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import 'rxjs/add/observable/of';
#Component({
selector: 'app-user',
template: `The user is: {{ userInfo$ | async }}`
})
export class UserComponent implements OnInit {
public user$: Observable<User>;
public userInfo$: Observable<string>;
constructor(private _route: ActivatedRoute) {
}
ngOnInit() {
// an observable of users
this.user$ = this._route.params
// switch to the observable provided by getUser() which queries the resource
.switchMap(params => this.getUser(params['name']))
// log error
.catch(error => {
console.log('Error occurred - ' + error.message);
// rethrow
return Observable.throw(error);
})
// share a single subscription among the subscribers so that getUser() will not be called for every subscriber
.share();
// an observable of user details to be displayed on template
this.userInfo$ = this.user$.map(user => user.username + ' - ' + user.date.toISOString());
}
private getUser(username: string): Observable<User> {
// occasionally create error
if (username === 'error') {
return Observable.throw(new Error('Occasional error'));
}
// create a user after 300 ms
return Observable.of({username: username, date: new Date()}).delay(300);
}
}
export interface User {
username: string;
date: Date;
}
New to Angular 2, still trying to get my head around certain things. Where I am stuck is I have login service and login component. I send a login request from the login component to the login service to post the username and password to a login API. If successful it posts the token to the localstorage. Where I am stuck is after the token is sent storage I want to return a boolean response back to the login component. Based on the boolean response it will perform execute a function in the component.
I can do everything until I get the response. I don't know how to handle a response back to the login component. Appreciate if someone could point me in the right direction. My code as follows:
LOGIN SERVICE
import { Injectable } from '#angular/core';
import { Token } from './login';
import { APIDOMAIN } from '../../../shared/api';
import { Http, Response, Headers, RequestOptions } from '#angular/http';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/toPromise';
#Injectable()
export class LoginService {
url: string = APIDOMAIN;
constructor(private http: Http) { }
login(username: string, password: string) {
console.log('Login API');
let headers = new Headers();
let data = null;
headers.append("Authorization", "Basic " + btoa(username + ":" + password));
headers.append("Content-Type", "application/x-www-form-urlencoded");
this.http.post(this.url+ '/login', data, {headers: headers})
.map(res => res.json())
.subscribe(
token => { console.log(token); localStorage.setItem('id_token',token.token); },
err => { console.log(err);},
() => console.log('Request Complete')
);
}
logout(): void {
localStorage.removeItem('id_token');
}
}
LOGIN COMPONENT
import { Component, OnInit } from '#angular/core';
import { LoginService } from './shared/login.service';
import { Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Rx';
#Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
username: string;
password: string;
constructor(private loginService: LoginService) { }
ngOnInit() {}
login(): void {
this.loginService.login(this.username,this.password)
// PEFORM SOME FUNCTION BASED BOOLEAN RESPONSE
}
}
Here's one solution:
export class LoginService {
status: EventEmitter<boolean> = new EventEmitter();
login(username: string, password: string) {
this.http.post(...)
.map(res => res.json())
.subscribe(token => {
console.log(token);
localStorage.setItem('id_token',token.token);
this.status.emit(true);
});
logout() {
localStorage.removeItem('id_token');
this.status.emit(false);
}
}
export class LoginComponent implements OnInit {
constructor(private loginService: LoginService) { }
ngOnInit() {
this.loginService.status.subscribe(console.info);
}
}
I have the following code which is a simple service that goes back to the server to fetch some data:
import { Injectable } from '#angular/core';
import { Action } from '../shared';
import { Http, Response, Headers, RequestOptions } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { Authenticated } from '../authenticated';
import 'rxjs/Rx';
#Injectable()
export class ActionsService {
private url = 'http://localhost/api/actions';
constructor(private http: Http, private authenticated : Authenticated) {}
getActions(search:string): Observable<Action[]> {
let options = this.getOptions(false);
let queryString = `?page=1&size=10&search=${search}`;
return this.http.get(`${this.url + queryString}`, options)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(response: Response) {
let body = response.json();
return body || { };
}
private handleError (error: any) {
let errMsg = (error.message) ? error.message : error.status ? `${error.status} - ${error.statusText}` : 'Server error';
console.error(errMsg); // log to console instead
if (error.status == 403) {
this.authenticated.logout();
}
return Observable.throw(errMsg);
}
private getOptions(addContentType: boolean) : RequestOptions {
let headers = new Headers();
if (addContentType) {
headers.append('Content-Type', 'application/json');
}
let authToken = JSON.parse(localStorage.getItem('auth_token'));
headers.append('Authorization', `Bearer ${authToken.access_token}`);
return new RequestOptions({ headers: headers });
}
}
Everything works as expected except for handleError. As soon as getActions receives an error from the server it goes into the this.handleError method which again works fine up until the section where this.authenticated.logout() should be called. this.autenticated is undefined and I am not sure if it is because "this" is referring to another object or if ActionSerivce's local variables are made null when and http exception occurs. The authenticated local variable is properly injected (I did a console.log in the constructor and it was there).
The problem is that you are not binding the this context in your callback function. You should declare your http call like this for example:
return this.http.get(`${this.url + queryString}`, options)
.map(this.extractData.bind(this)) //bind
.catch(this.handleError.bind(this)); //bind
Another option could be to pass an anonymous function and call the callback from there:
return this.http.get(`${this.url + queryString}`, options)
.map((result) => { return this.extractData(result)})
.catch((result) => { return this.handleError(result}));
And yet another option is to declare your callback functions a little differently, you can keep your http call the way you had it before:
private extractData: Function = (response: Response): any => {
let body = response.json();
return body || { };
}
private handleError: Function = (error: any): any => {
//...
}
I have a little problem with my Angular2 app. I want to get some data from server for my user login, but my code is going ahead and I have a lot of bugs with it. I want to wait for answer from server, then do something with my data.
This is my code:
import { Injectable } from '#angular/core';
import { Http, Response, Headers } from '#angular/http';
import { Observable } from 'rxjs/Rx';
import { User } from './user';
#Injectable()
export class UserService {
public usersTmp: Array<Object> = new Array<Object>();
public users: Array<User>;
public user: User = new User();
public noteToSend;
constructor(private http: Http) { }
getUsers() {
var headers = new Headers();
headers.append('Accept', 'q=0.8;application/json;q=0.9');
this.http.get('/AngularApp/api/users', { headers: headers })
.map((res: Response) => res.json())
.subscribe(
data => {
console.log(data);
this.usersTmp = data;
},
err => console.error(err),
() => console.log('done')
);
this.users = new Array<User>();
for (var i = 0; i < this.usersTmp.length; i++) {
this.user = new User();
this.user.id = this.usersTmp[i]["userId"];
this.user.name = this.usersTmp[i]["userName"];
this.user.email = this.usersTmp[i]["userEmail"];
this.user.pass = this.usersTmp[i]["userPassword"];
this.users.push(this.user);
}
return this.users;
}
As I noticed my code is going to the for loop until I get answer from server, so I return just empty array. Anyone can help me with that?
In the service, you should return the Observable that your component can subscribe to. It cannot work they way you do it due to the asynchronous mode of the get request.
As a proposal, your service could look similar to this
getUsers() {
let headers = new Headers();
headers.append('Accept', 'q=0.8;application/json;q=0.9');
return this.http.get('/AngularApp/api/users', { headers: headers })
.map((res: Response) => res.json());
}
And the relevant part of your component like this:
constructor(private userService:UserService) {
this.userService.getUsers().subscribe(
data => this.iterateOverUsers(data));
}
iterateOverUsers(data) {
// here comes your for loop
}
How to cancel a HTTPRequest in Angular 2?
I know how to reject the request promise only.
return new Promise((resolve, reject) => {
this.currentLoading.set(url, {resolve, reject});
this.http.get(url, {headers: reqHeaders})
.subscribe(
(res) => {
res = res.json();
this.currentLoading.delete(url);
this.cache.set(url, res);
resolve(res);
}
);
});
You can use the following simple solution:
if ( this.subscription ) {
this.subscription.unsubscribe();
}
this.subscription = this.http.get( 'awesomeApi' )
.subscribe((res)=> {
// your awesome code..
})
You can call unsubscribe
let sub = this.http.get(url, {headers: reqHeaders})
.subscribe(
(res) => {
res = res.json();
this.currentLoading.delete(url);
this.cache.set(url, res);
resolve(res);
}
);
sub.unsubscribe();
More info here: http://www.syntaxsuccess.com/viewarticle/angular-2.0-and-http
You can use SwitchMap on the observable which will cancel any previous request's responses and only request the latest:
https://www.learnrxjs.io/operators/transformation/switchmap.html
A little late for the party, but here is my take:
import { Injectable } from '#angular/core'
import { Http } from '#angular/http'
import { Observable } from 'rxjs/Observable'
import { Subscriber } from 'rxjs/Subscriber'
#Injectable ()
export class SomeHttpServiceService {
private subscriber: Subscriber<any>
constructor(private http: Http){ }
public cancelableRequest() {
let o = new Observable(obs => subscriber = obs)
return this.http.get('someurl').takeUntil(o)
.toPromise() //I dont like observables
.then(res => {
o.unsubscribe
return res
})
}
public cancelRequest() {
subscriber.error('whatever')
}
}
This allows you to manually cancel a request. I sometimes end up with an observable or promise that will make changes to a result on the page. If the request was initiated automatically (user didn't type anyting in a field for x millis) being able to abort the request is nice (user is suddenly typing something again)...
takeUntil should also work with a simple timeout (Observable.timer) if that is what you are looking for
https://www.learnrxjs.io/learn-rxjs/operators/filtering/takeuntil
Use switchMap [docs], which will cancel all in-flight requests and use only the latest.
get(endpoint: string): Observable<any> {
const headers: Observable<{url: string, headers: HttpHeaders}> = this.getConfig();
return headers.pipe(
switchMap(obj => this.http.get(`${obj.url}${endpoint}`, { headers: obj.headers, params: params }) ),
shareReplay(1)
);
}
shareReplay will emit the latest value for any late subscribers.
This is a great thread, and I have a little more info to provide. I have an API call that could potentially go on for a very long time. So I needed the previous request to cancel with a timeout. I just figured out today that I can add a timeout operator to the pipe function. Once the timeout completes its count, that will cancel the previous HTTP request.
Example...
return this.exampleHttpRequest()
.pipe(
timeout(3000),
catchError(err => console.log(error)
)