Not able to create observable - javascript

Calling init function from another class, console.log is never called. Later, calling broadcast gives the following error:
Uncaught (in promise) TypeError: Cannot read property 'next' of
undefined
File with observable code:
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/share';
import 'rxjs/add/observable/of';
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
import Deferred from './Deferred';
import * as m from '../Models/models';
let sharedServiceInstance = null;
export default class SharedService {
observable: Observable<any>;
observer: Observer<any>;
constructor() {
if(!sharedServiceInstance){
sharedServiceInstance = this;
}
return sharedServiceInstance;
}
init = () =>{
var deferred = new Deferred<any>();
if(this.observable != undefined){
deferred.resolve();
}
else{
this.observable = Observable.create((observer: Observer<any>) => {
this.observer = observer;
console.log("Observer: " + JSON.stringify(this.observer,null,4));
deferred.resolve();
}).share();
}
return deferred.promise;
}
broadcast(event: m.SharedEventModel) {
this.observer.next(event);
}
on(eventName, callback) {
return this.observable.filter((event) => {
return event.Name === eventName;
}).subscribe(callback);
}
}
File where observable is initiated and called:
import { Subject } from 'rxjs/Subject';
import SharedService from '../Services/sharedService';
import * as m from '../Models/models';
let initializeServiceInstance;
export default class InitializeService {
private sharedService = new SharedService();
public constructor(){
if(!initializeServiceInstance){
this.initialize();
initializeServiceInstance = this;
}
return initializeServiceInstance;
}
initialize =() =>{
var promise1 = this.sharedService.init()
.then(()=>{
//Debugger never reaches here
})
.catch((response)=>{
//Debugger never reaches here
var event = new m.SharedEventModel({
Name: m.EventSubjectEnum.AfterLogout
})
this.sharedService.broadcast(event);
})
}
}
NOTE: Using rxjs without angular.

In this block of code:
this.observable = Observable.create((observer: Observer<any>) => {
this.observer = observer;
console.log("Observer: " + JSON.stringify(this.observer,null,4));
}).share();
That inner function is not executed until there's a subscription. The only place subscriptions are happening is in .on, and .on is never called. So if there are no subscriptions yet, then this.observer will be undefined. Since it can be undefined, this.observer.next(event); can throw an error.
Also, if there are ever two subscriptions, then the second subscription will overwrite this.observer, thus making it so the first subscription will not get any notifications.
I think for what you're trying to do, you'll want to use a subject.
import { Subject } from 'rxjs/Subject';
// and other imports
export default class SharedService {
subject: Subject<any>
constructor() {
if(!sharedServiceInstance){
sharedServiceInstance = this;
}
return sharedServiceInstance;
}
init = () => {
this.subject = new Subject();
}
broadcast(event: m.SharedEventModel) {
this.subject.next(event);
}
// I don't recommend mixing callbacks and observables in this way
on(eventName, callback) {
return this.subject.filter((event) => {
return event.Name === eventName;
}).subscribe(callback);
}
// My recomendation would be to just return the observable
// That way the caller can decide whether they want to subscribe
// Or whether they want to do additional manipulation of the stream
//on(eventName) {
// return this.subject.filter((event) => {
// return event.Name === eventName;
// });
//}
}

Related

Mobx observer component doesn't get update after observable change

I have the following MobX class that maintain async operation:
import { makeObservable, observable, action } from "mobx";
class AsyncAction<T, P = void> {
public isLoading = false;
public error?: unknown;
public response?: T;
constructor(
private asyncAction: (payload: P) => Promise<T>,
) {
makeObservable(this, {
error: observable,
isLoading: observable,
response: observable,
setLoading: action,
setError: action,
setResponse: action,
});
}
setLoading(isLoading: boolean) {
this.isLoading = isLoading;
}
setError(error: unknown) {
this.error = error;
}
setResponse(response: T | undefined) {
this.response = response;
}
async run(payload: P) {
try {
this.setLoading(true);
const response = await this.asyncAction(payload);
this.setResponse(response);
} catch (error) {
this.setError(error);
} finally {
this.setLoading(false);
}
}
}
export { AsyncAction };
I also have the following store that extends AsyncAction:
import api from '../api';
import { AsyncAction } from "./AsyncAction";
class StatusesStore extends AsyncAction<Record<string, Status>> {
constructor() {
super(async () => {
const { statusesMap } = await api.fetchStatusesMap();
return statusesMap;
});
makeObservable(this, {
setStatus: action,
});
}
public setStatus(name: string, status: Status) {
if (this.response) {
this.response[name] = status;
}
}
}
When I trigger from my component rootStore.statusesStore.setStatus('name', 'DONE'), the component doesn't get updated.
When open devools, I see the following:
Instead of being wrapped with observable, the object keys are plain strings. This might be a reason why changing the status string triggers nothing.
How can I fix that? What am I missing?
Since async processes are resolved in the next tick of the event loop, mobx can't track the changes after the tick. One of the solutions is to use
runInAction function after every await keyword.
Like this:
import {runInAction} from 'mobx'
async run(payload: P) {
try {
this.setLoading(true);// this is okay
const response = await this.asyncAction(payload);
// everything after "await" must be wrapped
runInAction(()=>{
this.setResponse(response);
})
} catch (error) {
this.setError(error); // wrap in runInAction
} finally {
this.setLoading(false); // wrap in runInAction
}
}
There are also a few other alternatives how you can deal with promises in combination with Mobx. For me, runInAction is the most straightforward way.
For other examples check out official documentation:
Asynchronous actions

avoid nested subscribe if there is a forkjoin inside

Here is my code in angular
this.service.save(body).subscribe(
resp => {
this.dialog.confirmation({
message: 'save object successfully!'
})
.subscribe((ok) => {
if(ok) {
this.pro.status = resp.status;
this.loadingData(resp);
const s1 = this.service.getSummary(this.id);
const s2 = this.service.getCost(this.id);
forkJoin([s1, s2]).subscribe([r1, r2]) => {
this.view = r1;
this.list = r2;
}
}
});
}
);
So there are many levels of subscribe. Not only it is ugly also the result is wrong and I can't not find it out by debugging. How can I rewrite it with rxjs operators?
You can simplify it using the RxJS operators, like the following:
// import { EMPTY, forkJoin } from 'rxjs';
// import { map, mergeMap } from 'rxjs/operators';
this.service
.save(body)
.pipe(
mergeMap((result) =>
// Merge the main observable with the dialog confirmation one..
// and map it to an object that contains the result from both observables.
this.dialog
.confirmation({ message: 'save object successfully!' })
.pipe(map((confirmed) => ({ result, confirmed })))
),
mergeMap(({ result, confirmed }) => {
if (confirmed) {
this.pro.status = result.status;
this.loadingData(result);
const s1 = this.service.getSummary(this.id);
const s2 = this.service.getCost(this.id);
return forkJoin([s1, s2]);
}
// Don't emit any value, if the dialog is not confirmed:
return EMPTY;
})
)
.subscribe(([r1, r2]) => {
this.view = r1;
this.list = r2;
});
Note: To handle the memory leaks, it's highly recommended to unsubscribe from the observable when you don't need it anymore, and this can be achieved based on your use cases, such as assigning the subscribe function result to a Subscription variable and calling unsubscribe in ngOnDestroy lifecycle hook, or using a Subject with takeUntil operator and calling next/complete functions in ngOnDestroy.
And here is how to use the unsubscribe method for example:
// import { Subscription } from 'rxjs';
#Component({...})
export class AppComponent implements OnInit, OnDestroy {
subscription: Subscription
ngOnInit(): void {
this.subscription = this.service.save(body)
// >>> pipe and other RxJS operators <<<
.subscribe(([r1, r2]) => {
this.view = r1;
this.list = r2;
});
}
ngOnDestroy() {
this.subscription.unsubscribe()
}
}
You can read more about that here: https://blog.bitsrc.io/6-ways-to-unsubscribe-from-observables-in-angular-ab912819a78f
This should be roughly equivalent:
this.service.save(body).pipe(
mergeMap(resp =>
this.dialog.confirmation({
message: 'save object successfully!'
}).pipe(
// This filter acts like your `if(ok)` statement. There's no
// else block, so if it's not okay, then nothing happens. The
// view isn't updated etc.
filter(ok => !!ok),
mapTo(resp)
)
),
tap(resp => {
this.pro.status = resp.status;
// If the following line mutates service/global state,
// it probably won't work as expected
this.loadingData(resp);
}),
mergeMap(_ => forkJoin([
this.service.getSummary(this.id),
this.service.getCost(this.id)
]))
).subscribe([view, list]) => {
this.view = view;
this.list = list;
});

How do I unsubscribe correctly from angular service results?

I have a method in the component that calls a service which returns an observable
Component Method code
public upload(file) {
this.Service.ToBase64(files[0])
.subscribe(data => (this.convertedFile = data));
}
This works fine but when I chain unsubscribe to it, it stops working.
With Unsubscribe - This does not work
public upload(file) {
this.Service.ToBase64(files[0])
.subscribe(data => (this.convertedFile = data)).Unsubscribe();
}
Service Code method
convertedFile$: Subject<string> = new Subject<string>();
ToBase64(file: any) {
const myReader = new FileReader();
myReader.onloadend = e => {
this.convertedFile$.next(myReader.result.toString().split(',')[1]);
};
myReader.readAsDataURL(file);
return this.convertedFile$.asObservable();
}
As this a subject, I would like to unsubscribe. How can I do that correctly?
You must declare a Subscription property
First in your component
import { Subscription } from 'rxjs';
Then
fileSubscription: Subscription;
And in your method
public upload(file) {
this.fileSubscription = this.Service.ToBase64(files[0])
.subscribe(data => (this.convertedFile = data));
}
In ngOnDestroy method
ngOnDestroy() {
if (this.fileSubscription) {
this.fileSubscription.unsubscribe();
}
}
Regards
The method is unsubscribe() not Unsubscribe(). But a more elegant way to get a value of observable and destroy the subscription is use the first operator like that:
import { first } from 'rxjs/operators';
public upload(file) {
this.Service.ToBase64(files[0]).pipe(first())
.subscribe(data => (this.convertedFile = data));
}

How to show spinner only if data are fetched from Http service?

I have to show a spinner only during http service call, and dismiss it when my component receives data.
I wrote a little cache service in order to fetch data from http service only the first time, and load that data from the cache during every other call, avoiding to call another time the http service.
The service is working as expected,but what if I'd like to show the spinner only during the http call and not when data are fetched from cache?
This is my component's code, it works when getReviewsCategory(this.id) method of my service calls http service, but when it fetches from cache the spinner is never dismissed.
Data are loaded in correct way in the background, but the spinner keeps going.
presentLoading() method is in ngOnInit so it's called everytime, what if I want to call it only when data are fetched from cache? How my component could know it?
ngOnInit() {
this.presentLoading();
this.CategoryCtrl();
}
CategoryCtrl() {
this.serverService.getReviewsCategory(this.id)
.subscribe((data) => {
this.category_sources = data['value'];
this.stopLoading();
});
}
async presentLoading() {
const loadingController = this.loadingController;
const loadingElement = await loadingController.create({
spinner: 'crescent',
});
return await loadingElement.present()
}
async stopLoading() {
return await this.loadingController.dismiss();
}
}
EDIT1: this is the CacheService:
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class CachingService {
constructor() { }
private _cache = {};
isCashed(url: string) {
return this._cache[url];
}
getData(url: string) {
return this._cache[url];
}
setData(url) {
return (data) => {
if (data && (data instanceof Error) === false) {
this._cache[url] = data;
};
}
}
reset() {
this._cache = {};
}
}
And this is the server service's method:
getReviewsCategory(cat_id) : Observable<any> {
if (this._c.isCashed(url)) {
return of(this._c.getData(url));
}else{
var modeapp = window.sessionStorage.modeapp;
var typemodeapp = typeof(window.sessionStorage.modeapp);
if (modeapp === "online") {
let promise = new Promise ((resolve, reject) => {
this.httpNative.get(url, {}, {}).
then((data) => {
let mydata = JSON.parse(data.data);
console.log("Data from HTTP: ");
console.log(mydata);
resolve(mydata);
}, (error) => {
console.log("error in HTTP");
reject(error.error);
}
);
});
var observable = from(promise);
}
}
return observable
.pipe(
tap(this._c.setData(url))
);
I can see you're returning an observable from the service, you can try the following to see if this helps.
CategoryCtrl() {
this.serverService.getReviewsCategory(this.id)
.subscribe((data) => {
this.category_sources = data['value'];
this.stopLoading();
},
(error) => console.log(error),
() => this.stopLoading(); // This always execute
);}
Docs: http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-subscribe
However, I believe the problem may come from the object you're calling .dismiss()
from. You should be calling dismiss on the instance of the element and not the object itself.
let loadingElement: Loading = null;
async presentLoading() {
const loadingController = this.loadingController;
this.loadingElement = await loadingController.create({
spinner: 'crescent',
});
return await loadingElement.present()
}
async stopLoading() {
return await this.loadingElement.dismiss();
}
You can use an HttpInterceptor class to intercept all http calls, and in the intercept method, you can stop and start a spinner.
Broadly speaking, the structure is:
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Start the spinner.
return next.handle(req).pipe(
map((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
// Stop the spinner
}
return event;
})
);

Inconsitant mergeMap behaviour

I am currently working on a file uploading method which requires me to limit the number of concurrent requests coming through.
I've begun by writing a prototype to how it should be handled
const items = Array.from({ length: 50 }).map((_, n) => n);
from(items)
.pipe(
mergeMap(n => {
return of(n).pipe(delay(2000));
}, 5)
)
.subscribe(n => {
console.log(n);
});
And it did work, however as soon as I swapped out the of with the actual call. It only processes one chunk, so let's say 5 out of 20 files
from(files)
.pipe(mergeMap(handleFile, 5))
.subscribe(console.log);
The handleFile function returns a call to my custom ajax implementation
import { Observable, Subscriber } from 'rxjs';
import axios from 'axios';
const { CancelToken } = axios;
class AjaxSubscriber extends Subscriber {
constructor(destination, settings) {
super(destination);
this.send(settings);
}
send(settings) {
const cancelToken = new CancelToken(cancel => {
// An executor function receives a cancel function as a parameter
this.cancel = cancel;
});
axios(Object.assign({ cancelToken }, settings))
.then(resp => this.next([null, resp.data]))
.catch(e => this.next([e, null]));
}
next(config) {
this.done = true;
const { destination } = this;
destination.next(config);
}
unsubscribe() {
if (this.cancel) {
this.cancel();
}
super.unsubscribe();
}
}
export class AjaxObservable extends Observable {
static create(settings) {
return new AjaxObservable(settings);
}
constructor(settings) {
super();
this.settings = settings;
}
_subscribe(subscriber) {
return new AjaxSubscriber(subscriber, this.settings);
}
}
So it looks something like this like
function handleFile() {
return AjaxObservable.create({
url: "https://jsonplaceholder.typicode.com/todos/1"
});
}
CodeSandbox
If I remove the concurrency parameter from the merge map function everything works fine, but it uploads all files all at once. Is there any way to fix this?
Turns out the problem was me not calling complete() method inside AjaxSubscriber, so I modified the code to:
pass(response) {
this.next(response);
this.complete();
}
And from axios call:
axios(Object.assign({ cancelToken }, settings))
.then(resp => this.pass([null, resp.data]))
.catch(e => this.pass([e, null]));

Categories