Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Synchronous blocking subscribe call to a custom ReplaySubject based observable

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

like image 856
Stephane Avatar asked Nov 06 '22 04:11

Stephane


1 Answers

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.

like image 112
Smutje Avatar answered Nov 15 '22 11:11

Smutje