RxJS execution order after subject emission - javascript

I want to understand the code execution order following a 'next' call on a Subject.
Background: I have 3 classes (call them HaveSubject, HaveSubscription1, HaveSubscription2). HaveSubject needs to tell HS1 and HS2 to do something through a Subject that HS1 and HS2 are subscribed to. Their tasks must be completed before HaveSubject goes on to execute method somethingVeryImportant.
Pseudocode:
class HaveSubject {
// Angular service
public mySubject$: Subject<string> = new Subject<string>();
public codeImExecuting() {
this.mySubject$.next('input for tasks')
this.somethingVeryImportant();
}
private somethingVeryImportant() {
// stuff
}
}
class HaveSubscription1 {
// Angular service
constructor(private hs: HaveSubject) {
this.hs.mySubject$.subscribe(inputStr => {
// did I do this before somethingVeryImportant is called?
});
}
}
class HaveSubscription2 {
// Angular service
constructor(private hs: HaveSubject) {
this.hs.mySubject$.subscribe(inputStr => {
// did I do this before somethingVeryImportant is called?
});
}
}
My question is: what is the best way to ensure that HS1 and HS2 have executed the code attached to their subscriptions before going on to execute method somethingVeryImportant? If the order of operations is: HaveSubject calls 'next' on subject -> HS1 and HS2 do their tasks -> HaveSubject goes on to its next line of code, which is somethingVeryImportant, then I have no issues. I'm just not sure that subscriptions are executed immediately after they receive the 'next' item in the subscription.
NOTE: There are a few things I can't do that I would normally, such as have HaveSubject inject into the other two, because the other two are created dynamically (i.e. I may have none, one, or both of HaveSubscriptionX, not clear how many will be created, and these are Angular services that are provided by a component, not in the root...).
Thoughts?

Simplest call events on finishing side work in HaveSubscription# (not ideal a lot duplicated run check second option)
class HaveSubject {
// Angular service
public mySubject$: Subject<string> = new Subject<string>();
public mySubjectDone$: Subject<void> = new Subject<void>();
public constructor() {
this.mySubjectDone$.subscribe(this.somethingVeryImportant.bind(this));
}
public codeImExecuting() {
this.mySubject$.next('input for tasks')
}
private somethingVeryImportant() {
// stuff
}
}
class HaveSubscription1 {
// Angular service
constructor(private hs: HaveSubject) {
this.hs.mySubject$.subscribe(inputStr => {
// did I do this before somethingVeryImportant is called?
hs.mySubjectDone$.next()
});
}
}
class HaveSubscription2 {
// Angular service
constructor(private hs: HaveSubject) {
this.hs.mySubject$.subscribe(inputStr => {
// did I do this before somethingVeryImportant is called?
hs.mySubjectDone$.next()
});
}
}
or if you dont want to require any actions from HaveSubscription# trigger delayed action
class HaveSubject {
// Angular service
public mySubject$: Subject<string> = new Subject<string>();
public constructor() {
this.mySubject$.pipe(
debounceTime(16) // delay or auditTime, debounceTime, ...
).subscribe(this.somethingVeryImportant.bind(this));
}
public codeImExecuting() {
this.mySubject$.next('input for tasks')
}
private somethingVeryImportant() {
// stuff
}
}
class HaveSubscription1 {
// Angular service
constructor(private hs: HaveSubject) {
this.hs.mySubject$.subscribe(inputStr => {
// did I do this before somethingVeryImportant is called?
});
}
}
class HaveSubscription2 {
// Angular service
constructor(private hs: HaveSubject) {
this.hs.mySubject$.subscribe(inputStr => {
// did I do this before somethingVeryImportant is called?
});
}
}
If you have some versioning mechanism that will react to changes done in HaveSubscription# you can do this:
this.mySubject$.pipe(
map(this.calculateVersion),
distinctUntilChanged(),
).subscribe(this.somethingVeryImportant.bind(this));

so this looks like you've made some suspect architecture decisions, since the flow is
service 1 function executes and emits from observable
service 2/3/etc subscribers to service 1 observable execute code that produces some output or side effect
service 1 function needs to execute dependent on some output or side effect from service 2/3/etc functions
this requires the subject to be aware of it's observers, which is the opposite of the rxjs philosophy. The ideal solution here would be to fix these architectural concerns. It's tough to say how to accomplish this without knowing more about how or why things occur this way or what the overall goal is.
However, you can accomplish this in a pretty reliable way, you need to add some subject on your first function that can signal completion by the dependent services:
class HaveSubject {
// Angular service
public mySubject$: Subject<string> = new Subject<string>();
private workDone$: Subject<void> = new Subject<void>();
imDone() {
this.workDone$.next();
}
public codeImExecuting() {
if (this.mySubject$.observers.length) { // check for observers
// if observers, listen for that many emissions from workDone$, only reacting to the last one
this.workDone$.pipe(take(this.mySubject$.observers.length), last())
.subscribe(v => this.somethingVeryImportant());
} else { // or just run the function, doesn't matter
this.somethingVeryImportant();
}
this.mySubject$.next('input for tasks');
}
private somethingVeryImportant() {
// stuff
}
}
and have the observers of mySubject$ call imDone() when they're done.

Related

Typescript control flow behavior

I am new to JS, TS and Angular...
So I have this angular component:
export class AdminProductsMenuComponent implements OnInit{
constructor(private productService: ProductService,
private alertService: AlertService,
private router: Router) {
this.subscribeToDeleteProductEvents();
}
productsAdminModel: IGetProductAdminModel[] = [];
private productId: string;
ngOnInit(): void {
this.executeGetAllProductsAsAdmin();
}
executeGetAllProductsAsAdmin() {
this.productService.getAllProductsAsAdmin().subscribe({
next: (productData) => this.productsAdminModel = productData
})
}
private subscribeToDeleteProductEvents() {
this.alertService.getSubjectAlertEvent().subscribe({
next: (isConfirmed) => {
if (isConfirmed) {
this.productService.deleteProduct(this.productId).subscribe({
next: () => {
this.reloadCurrentResources();
}
});
}
}
});
}
private reloadCurrentResources(): void {
// save current route first
this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => {
this.router.navigate(['/AdminProducts']); // navigate to same route
});
}
executeProductDelete(id: string) {
this.productId = id;
this.alertService.confirmationAlertProductDelete();
}
}
Brief explanation:
I have subscription in the constructor which listens for events during the lifetime of the component.
An event is fired when the last method is called (through the template) which prompts a SweetAlert confirm dialog. Depending on the selected the event is true or false.
Now here is the tricky part - if I move the executeProductDelete() method above reloadCurrentResources() and subscribeToDeleteProductEvents() and invoke it (executeProductDelete) it will complete the action and throw error
I have a feeling that it executes again the subscribeToDeleteProductEvents() and reloadCurrentResources() .
If I move the executeDeleteProduct() as the last method, no error occurs.
Why is this behavior? I have a feeling that they continue to run synchronously. They are not invoked anywhere else.
There seems to be 2 main problems there:
Avoid at all costs "reloading" the same component, try to abstract the reload logic into methods. This could cause weird issues and unecessary loads, as the SPA is meant to be a single page application.
Since you are problably re-instancianting the component over and over again through your reloadResources, the alert service behaviour subjects creates new subscriptions. And since you haven't unsubscribed from them, they will keep listening forever.

Angular - Using a service property set by callback in another component

I am trying to use a DataService property myData that is waiting for callback. But it is undefined when I call in DataComponent. How can I access and use it there?
export class DataService {
public myData;
constructor(private http: HttpClient) {
this.load().then((data) => {
this.myData = data
})
}
load() {
return new Promise((resolve) => {
this.http.get('https://reqres.in/api/users').subscribe(
(res: any) => {
console.log(res.data)
resolve(res.data)
},
(error) => {
console.log(error);
}
)
})
}
}
export class DataComponent implements OnInit {
constructor(private dataService: DataService) {
this.prepareData();
}
prepareData() {
console.log(this.dataService.myData)
}
ngOnInit(): void {
}
}
Here is the source code: https://stackblitz.com/edit/angular-ivy-kbpdpo
You are running into a race condition since this is an asynchronous function.
This change works: https://stackblitz.com/edit/angular-ivy-vf3llg
Consider reading up on https://angular.io/guide/http
Personally, I just have services return raw data and manipulate it elsewhere, but if needed you can tap into the response as I have shown i the updated example.
This question and answer are probably really a duplicate of this question...
What are pipe and tap methods in Angular tutorial?
your load() method is asynchronous, that means that it can return the response after 2 hours, so it will execute your callback after 2 hours, and you are asking myData synchronously which means that you are asking it right now, so it won't work.
you have to wait until the answer is returned, in your code there is no chance to accomplish this, so either remove yourData field and just subscribe it into the component, or create BehaviorSubject and emit value to the component

Angular 5, rxjs- wait for observable to finish only if it is in a middle of a run before running another process

I have a timer in my main component of 3 seconds. inside the timer I perform http call-
constructor(){
this.timer = timer(3000, 3000);
this.timerObservable = this.timer.subscribe(x => {
this.http.get(URL).subscribe(()=>{
//DO SOMETHING
});
});
}
In another component I have a button that suppose to perform a different http call, pressing on the button invoke the sumbit function-
submit(){
this.http.get("/sumbitForm").subscribe(()=> {
//DO SOMETHING
})
}
When a user clicks on the button, if the timer is in process (the http inside of it was called and not resolved yet) I want to wait before I perform the http call on the button until it resolved, but if the timer is not in process (the time from the previous call did not passed yet) I want to execute it immediately.
I think that forkJoin and concat is not relevant here (this is a timer and not a 'regular' subscription that I want to wait to its execution either way) and I could not find a pretty way to do it, any idea?
You need to share some information between your two components, i.e. when the polling request is in progress and when it isn't. You should use a Service for this. It's also always good practice to move your http request logic to a Service instead of using the HttpClient directly in the component. This allows you to do your general error handling in one place.
Let's call this Service ApiService.
ApiService
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Observable, BehaviorSubject, interval } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
#Injectable({ providedIn: 'root' })
export class ApiService {
// Use a BehaviorSubject to notify listeners about ongoing polling requests.
// A BahaviorSubject always has a current value that late subscribers will
// receive immediately upon subscribing
private pollRequestInProgress = new BehaviorSubject<boolean>(false);
// Share this BehaviorSubject as an Observable
public pollRequestInProgress$ = pollRequestInProgress.asObservable();
constructor(private http: HttpClient)
doPoll(url: string): Observable<any> {
return interval(3000).pipe( // interval(3000) is equivalent to timer(3000, 3000)
tap(_ => pollRequestInProgress.next(true)), // notify that a poll request is about to happen
switchMap(_ => this.http.get(url)), // do your poll request
tap(_ => pollRequestInProgress.next(false)) // notify that your poll request ended
);
}
}
MainComponent
This is the Component where you want to start your polling from.
private destroy$: Subject<void> = new Subject<void>();
constructor(private apiService: ApiService) {}
// move logic to ngOnInit instead of constructor
ngOnInit() {
// subscribe and thereby start the polling
this.apiService.doPoll(URL).pipe(takeUntil(this.destroy$))
.subscribe(pollResponse => {
//DO SOMETHING
});
}
ngOnDestroy() {
// unsubscribe when the Component gets destroyed.
this.destroy$.next();
this.destroy$.complete();
}
FeatureComponent
This is the Component where you want to perform a http request when you click a button.
constructor(private http: HttpClient, private apiService: apiService) {}
submit() {
// Listen to whether a polling request is currently in progress.
// We will immediately receive a value upon subscribing here, because we used
// a BehaviorSubject as the underlying source.
this.apiService.pollRequestInProgress$.pipe(
// Emit the first item that tells you that no polling request is in progress.
// i.e. let the first 'requestInProgress === false' through but not any
// other items before or after.
first(requestInProgress => !requestInProgress),
// If no polling request is in progress, switch to the http request you want
// to perform
switchMap(this.http.get("/sumbitForm")) // <-- consider moving this http.get to your ApiService aswell
).subscribe(httpResponse => {
// you've got your http response here
});
// you don't have to unsubscribe here as first and http.get both complete
// and thus unsubscribe automatically
}
Check out a simple example of the code logic above here: https://stackblitz.com/edit/rxjs-t4hjcr
You can use Angular Subject
import { Injectable } from '#angular/core';
import { Observable, Subject } from 'rxjs';
#Injectable({ providedIn: 'root' })
export class CallService {
private subject = new Subject<any>();
timerCompleted() {
this.subject.next();
}
checkTimer(): Observable<any> {
return this.subject.asObservable();
}
}
The app component uses the call service to subscribe to timer complete and make them available to the app component template.
import { Component, OnDestroy } from '#angular/core';
import { Subscription } from 'rxjs';
import { CallService } from './_services/index';
#Component({
selector: 'app',
templateUrl: 'app.component.html'
})
export class AppComponent implements OnDestroy {
subscription: Subscription;
constructor(private callService: CallService) {
this.subscription = this.callService.checkTimer().subscribe(() => {
// call your api after timer complete
});
}
ngOnDestroy() {
// unsubscribe to ensure no memory leaks
this.subscription.unsubscribe();
}
}
add below code in your timer
this.timer = timer(3000, 3000);
this.timerObservable = this.timer.subscribe(x => {
this.http.get(URL).subscribe(()=>{
this.callService.timerCompleted();
});
});
For more reference you can check http://jasonwatmore.com/post/2018/06/25/angular-6-communicating-between-components-with-observable-subject

Which RxJS type to use when a method may or may not fetch data asynchronously?

Imagine we have the following factory:
#Injectable()
export class DataClassFactory {
constructor(
private dataService: DataService,
) { }
public createThing(initialData?: InitialData): AsyncSubject<DataClass> {
let dataClass: AsyncSubject<DataClass> = new AsyncSubject<DataClass>();
if (!!initialData) {
dataClass.next(new DataClass(initialData));
dataClass.complete();
} else {
this.dataService.getData().subscribe((dataResponse) => {
dataClass.next(new ReportRequest(dataResponse));
dataClass.complete();
});
}
}
return dataClass;
}
}
We inject this factory, invoke the createThing method, and subscribe to the response in some component. I originally tried to use a plain Subject, but then I realized that in the case where we already have initial data, next() is called before the response is returned, so the subscriber in the component never gets that value.
My question is: is this correct situation in which to use an AsyncSubject, or is there a different/better way to handle this sort of method that has potential synchronous and asynchronous timelines?
I would do something along these lines
public createThing(initialData?: InitialData): Observable<DataClass | ReportRequest> {
if (!!initialData) {
const data = new DataClass(initialData);
return of(data);
} else {
return this.dataService.getData()
.pipe(map(dataResponse => new ReportRequest(dataResponse));
}
}
Whoever calls createThing would get an Observable to which it would have to subscribe.
This Observable would emit an instance of DataClass if initialData is not null, otherwise it would return and instance of ReportRequest as soon as dataService responds.

Angular 2+ http service is being called, but request is not going out

I want to be able to instantiate a model class, but also give it access to services.
For example, say I have these endpoints:
/book/:id
/book/:id/author
I want to have a BooksService service to fetch a list a Book instance. I'd like the book instances to be instantiated with new, given a definition JSON through the constructor, while still being able to use Angular dependencies.
Example of what I want to do:
BooksService.getBook(1) // makes call to /book/1
.subscribe((book) => {
book.getAuthor() // makes call to /book/1/author
...
});
To accomplish this, I tried to use a factory to new an instance of a book model. I then pass in a reference to the Http injected dependency that the factory injects.
Here's what I have:
BooksService
#Injectable()
export class BookService {
constructor(
private http: Http,
private bookModelFactory: BookModelFactory
) {}
getBook(): Observable<BookModel> {
return this.http.get('http://localhost:3000/book/1')
.map((res) => {
return this.bookModelFactory.make(res.json().data);
});
}
}
BookModel, BookModelFactory
#Injectable()
export class BookModelFactory {
constructor (private http: Http) {}
make(def: object): BookModel {
var book = new BookModel(def);
book.http = this.http;
return book;
}
}
export class BookModel {
def: any;
http: Http;
constructor (def: object) {
this.def = def;
}
getAuthor() {
console.log('http', this.http);
this.http.get('http://localhost:3000/book/1/author');
}
}
When I try to use this code, I see the console log for the http object in book.getAuthor(). It exists, and I can see the get method on it. But it never makes the API request. Nothing in the network tab has anything about a call to /book/1/author. There are no errors. Simply put, nothing happens.
Why isn't the request being made when this.http.get('...') is being called in getAuthors()?
Thanks in advance.
Using Angular 4.
(Imports statements are removed for brevity)
2) If this is a good strategy... why isn't the request being made when this.http.get('...') is being called in getAuthors()?
Because no-one ever subscribed to the results of this call:
this.http.get('http://localhost:3000/book/1/author').subscribe(res => {
// do something with the results here
});
In angular if you do not subscribe to the results of the HTTP call, then this call will never be made.
Or maybe you want your getAuthor method to return an Observable<Author> so that it is the caller of this method that can subscribe to the results:
getAuthor(): Observable<Author> {
return this.http.get('http://localhost:3000/book/1/author').map(res => {
return res.json().data;
});
}
So that you can subscribe to it later:
BooksService.getBook(1) // makes call to /book/1
.subscribe(book => {
book.getAuthor() // makes call to /book/1/author
.subscribe(author => {
// do something with the book author here
});
...
});
So remember, if you do not subscribe, the AJAX call will not be made.

Categories

Resources