The view contains the element:
<div *ngIf="showMe">Hello</div>
When calling the component method:
downloadDemo(): void {
this.download$ = this.downloadService.downloadUrlAsBlobWithProgressAndSaveInFile('assets/skypeforlinux-64.deb', 'demo')
this.download$.subscribe((download: Download) => {
this.showMe = true;
console.log('Progress: ' + download.progress);
})
}
the element shows in the view before all the Progress
loggers. And that is how it should be. This HTTP based downloading works just fine.
However when calling the component method:
downloadSoundtrack(soundtrack: Soundtrack): void {
const fileName: string = soundtrack.name + '.' + MIDI_FILE_SUFFIX;
const progress$: Observable<ProgressTask<Uint8Array>> = this.midiService.progressiveCreateSoundtrackMidi(soundtrack);
this.download$ = this.downloadService.downloadObservableDataAsBlobWithProgressAndSaveInFile(progress$, fileName);
this.download$.subscribe((download: Download) => {
this.showMe = true;
console.log('Progress: ' + download.progress);
})
}
the element shows in the view last after all the Progress
loggers. It is not how it should be. This custom ReplaySubject
based observable is not working as expected. Indeed, the element should show before and not after all the Progress
loggers.
I wanted to see if one subscribe call was blocking.
So I changed the two methods to:
downloadSoundtrack(soundtrack: Soundtrack): void {
const fileName: string = soundtrack.name + '.' + MIDI_FILE_SUFFIX;
const progress$: Observable<ProgressTask<Uint8Array>> = this.midiService.progressiveCreateSoundtrackMidi(soundtrack);
this.download$ = this.downloadService.downloadObservableDataAsBlobWithProgressAndSaveInFile(progress$, fileName);
this.showMe = true;
this.download$.subscribe((download: Download) => {
console.log('Progress: ' + download.progress);
});
console.log('Call done');
}
downloadDemo(): void {
this.download$ = this.downloadService.downloadUrlAsBlobWithProgressAndSaveInFile('assets/skypeforlinux-64.deb', 'demo')
this.showMe = true;
this.download$.subscribe((download: Download) => {
console.log('Progress: ' + download.progress);
});
console.log('Call done');
}
Here are the loggers when calling the downloadDemo()
method:
Progress: 0
Call done
Progress: 0
Progress: 0
Progress: 2
Progress: 3
We can see the subscribe()
call is non blocking.
Here are the loggers when calling the downloadSoundtrack()
method:
Progress: 96
Progress: 97
Progress: 100
Call done
We can see the subscribe()
call is blocking.
Adding an explicit this.detectChanges();
call made no difference:
downloadSoundtrack(soundtrack: Soundtrack): void {
const fileName: string = soundtrack.name + '.' + MIDI_FILE_SUFFIX;
const progress$: Observable<ProgressTask<Uint8Array>> = this.midiService.progressiveCreateSoundtrackMidi(soundtrack);
this.download$ = this.downloadService.downloadObservableDataAsBlobWithProgressAndSaveInFile(progress$, fileName);
this.download$.subscribe((download: Download) => {
this.showMe = true;
this.detectChanges();
console.log('Progress: ' + download.progress);
})
}
It still showed after all the Progress
loggers.
I have also tried some explicit subscription in place of the *ngIf="download$ | async as download"
in the template, but it did not help any:
downloadInProgress(soundtrack: Soundtrack): boolean {
let inProgress: boolean = false;
if (soundtrack.download) {
if (soundtrack.download.progress > 0 && soundtrack.download.progress < 100) {
inProgress = true;
} else if (soundtrack.download.progress == 100) {
console.log('complete');
soundtrack.download = undefined;
}
}
console.log('inProgress ' + inProgress);
return inProgress;
}
The long running service:
public progressiveCreateSoundtrackMidi(soundtrack: Soundtrack): Observable<ProgressTask<Uint8Array>> {
return Observable.create((progressTaskBis$: ReplaySubject<ProgressTask<Uint8Array>>) => {
this.createSoundtrackMidi(soundtrack, progressTaskBis$);
progressTaskBis$.complete();
return { unsubscribe() { } };
});
}
public createSoundtrackMidi(soundtrack: Soundtrack, progressTask$?: ReplaySubject<ProgressTask<Uint8Array>>): Uint8Array {
const midi: Midi = new Midi();
midi.name = soundtrack.name;
midi.header.name = soundtrack.name;
let noteIndex: number = 0;
if (soundtrack.hasTracks()) {
soundtrack.tracks.forEach((track: Track) => {
const midiTrack: any = midi.addTrack();
midiTrack.name = track.name;
midiTrack.channel = track.channel;
if (track.hasMeasures()) {
let totalDurationInSeconds: number = 0;
for (const measure of track.getSortedMeasures()) {
if (measure.placedChords) {
if (!this.notationService.isOnlyEndOfTrackChords(measure.placedChords)) {
for (const placedChord of measure.placedChords) {
if (!this.notationService.isEndOfTrackPlacedChord(placedChord)) {
const duration: string = placedChord.renderDuration();
const durationInSeconds: number = Tone.Time(duration).toSeconds();
const velocity: number = placedChord.velocity;
// const tempoInMicroSecondsPerBeat: number = this.beatsToMicroSeconds(1, measure.getTempo());
// const ticks: number = this.beatsToTicks(durationInBeats, DEFAULT_MIDI_PPQ, tempoInMicroSecondsPerBeat);
for (const note of placedChord.notes) {
if (!this.notationService.isEndOfTrackNote(note)) {
if (progressTask$) {
this.commonService.sleep(50);
progressTask$.next(this.downloadService.createProgressTask<Uint8Array>(soundtrack.getNbNotes(), noteIndex));
}
noteIndex++;
midiTrack.addNote({
midi: this.synthService.textToMidiNote(note.renderAbc()),
time: totalDurationInSeconds,
// ticks: ticks,
name: note.renderAbc(),
pitch: note.renderChroma(),
octave: note.renderOctave(),
velocity: velocity,
duration: durationInSeconds
});
}
}
totalDurationInSeconds += durationInSeconds;
}
}
}
}
}
}
});
}
if (progressTask$) {
progressTask$.next(this.downloadService.createProgressTask<Uint8Array>(soundtrack.getNbNotes(), soundtrack.getNbNotes(), midi.toArray()));
}
return midi.toArray();
}
There is a sleep call of 50ms slowing down the file creation, so as to give some ample time.
The implementation of the download service is based on this article
I'm on Angular 9.1.0
To keep things simple I would suggest to move to a more reactive state-driven approach where you could do something similar to the following:
1.
Change soundtracks
from an array to a public readonly soundtracks: Observable<Soundtrack[]> = this.soundtracksSubject.asObservable()
to enable your UI to register for changes. this.soundtracksSubject
is then private readonly soundtracksSubject: BehaviorSubject<Soundtrack[]> = new BehaviorSubject([]);
that then can be used to trigger observers of soundtracks
to refresh. When you receive the HTTP response you then do not set this.soundtracks = soundtracks;
but rather call this.soundtracksSubject.next(soundtracks))
.
Also when doing the actual download (soundtrack.download = download;
) instead of only changing your model after the change you have to call the subject again to propagate changes in your model to listeners:
const updatedSoundtracks: Soundtrack[] = this.soundtracksSubject.value.map(existingSoundtrack => existingSoundtrack);
const soundtrack: Soundtrack = updatedSoundtracks.find(existingSoundtrack => soundtrack.name === existingSoundtrack.name); // Or whatever identifies your soundtrack
if (soundtrack) {
soundtrack.download = download;
this.soundtracksSubject.next(updatedSoundtracks);
}
Change your UI from <tr *ngFor="let soundtrack of soundtracks">
to <tr *ngFor="let soundtrack of soundtracks | async">
to resolve the Observable
to use it in your Angular component. This also means your component will register to changes on soundtracks
and will get notified once somebody calls the subject/the observable.
Observable
and BehaviorSubject
are both RxJS concepts (import {BehaviorSubject, Observable} from "rxjs/index";
) and are worthwhile to research because they make your life so much easier.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With