When you do this:
public onSave(): void {
this.busy = true;
this.service.save()
.pipe(finalize(() => this.busy=false))
.subscribe(() => this.isInSync = true); <--------------
}
You can think of it as a code smell. When you call subscribe, that's where the reactive programming ends. This should not happen until the very last end of what you're trying. In this case, as you subscribe within a method that is called multiple time, you'll loose the ability to queue the requests as you'd like.
I think what you're trying to achieve can be represented with the following code (mix of mock for the HTTP call and actual stream logic):
const mockHttpRequest = (someParam: any) =>
of(`Result: ${someParam}`).pipe(delay(1000));
const change$$ = new Subject<any>();
const queue$ = change$$.pipe(concatMap(change => mockHttpRequest(change)));
const isSyncing$ = combineLatest([
change$$.pipe(
map((_, index) => index),
startWith(-1)
),
queue$.pipe(
map((_, index) => index),
startWith(-1)
)
]).pipe(
map(([changeIndex, queueIndex]) => changeIndex !== queueIndex),
distinctUntilChanged()
);
isSyncing$.subscribe(console.log);
A little bit of explanation on this:
const mockHttpRequest = (someParam: any) =>
of(`Result: ${someParam}`).pipe(delay(1000));
Simply mock an HTTP call with a delay
const change$$ = new Subject<any>();
The subject that you should call whenever a change happens
const queue$ = change$$.pipe(concatMap(change => mockHttpRequest(change)));
As its name says: This is the queue for all the HTTP calls. They'll happen one by one and won't be done in parallel.
const isSyncing$ = combineLatest([
change$$.pipe(
map((_, index) => index),
startWith(-1)
),
queue$.pipe(
map((_, index) => index),
startWith(-1)
)
]).pipe(
map(([changeIndex, queueIndex]) => changeIndex !== queueIndex),
distinctUntilChanged()
);
This observable will let you know the current status of the app: Whether it's syncing some data or not. To do this, we compare how many items have successfully gone out of the HTTP queue with how many saves attempts where made. If it's the same number we know we're in sync otherwise we're syncing.
The distinctUntilChanged is necessary because if we emit let say 2 changes really quickly and the second one we try to send it before the last one comes back, the observable isSyncing$ would emit twice false before emitting true once they're both done. With distinctUntilChanged we ensure that it emits only once false and only once true when that's the case.
Finally to test this we can do the following:
// simulate events happening when a user type at different times
// on the document to test our code
change$$.next('This is some initial text...');
setTimeout(() => {
change$$.next('This is some initial text... And now with some other edit...');
}, 1500);
setTimeout(() => {
change$$.next(
'This is some initial text... And now with some other edit... And a quick one before previous is finished processing...'
);
}, 1500);
setTimeout(() => {
change$$.next(
'This is some initial text... And now with some other edit... And a quick one before previous is finished processing... And a last one'
);
}, 4500);
and it should output
false
true
false
true
false
true
false
Note that the ones happening at the same time (1500) don't emit twice it's just that the status isSyncing$ stays true for a bit longer than the other as the 2 calls are made one after another
false
true
false
true <--- this one
false
true
false
If you want to play with this, here's a stackblitz: https://stackblitz.com/edit/rxjs-vtnk6z?devtoolsheight=60