2

I have an element in a component view:

<div>Progress: {{ progress$ | async }}</div>

It is supposed to be updated in a continuous way by a next() call in a subscribe() block:

progress$: BehaviorSubject<number> = new BehaviorSubject(0);
downloadSoundtrack(soundtrack: Soundtrack): void {
  const fileName: string = soundtrack.name + '.' + MIDI_FILE_SUFFIX;
  const progress$: Observable<ProgressTask<Uint8Array>> = this.midiService.progressiveCreateSoundtrackMidi$(soundtrack);
  console.log('Created the observable');
  const piper$: Observable<ProgressTask<Uint8Array>> = progress$.pipe(
    tap((progressTask: ProgressTask<Uint8Array>) => {
      this.progress$.next(progressTask.loaded);
      console.log('Loaded: ' + progressTask.loaded);
    })
  );
  this.download$ = this.downloadService.downloadObservableDataAsBlobWithProgressAndSaveInFile(piper$, fileName);
  console.log('Controller method call complete');
}

When running it, the console log shows:

Created the observable
Controller method call complete
Loaded: 0
Loaded: 1
Loaded: 2
...
Loaded: 591

However, the element in the view is not updating continuously, but only at the last value, changing from 0 to 591 straight.

I tried adding a this.detectChanges(); after the next() call but it didn't help.

I tried wrapping the next() call within an ngZone block but it didn't help.

The sleep method is implemented as:

public sleep(milliseconds: number): void {
  const date = Date.now();
  let currentDate = null;
  do {
    currentDate = Date.now();
  } while (currentDate - date < milliseconds);
}

On Angular 11.

UPDATE: I created another component method to trigger an interval() observable and that worked. It did show the progress value in the template changing continuously.

testProgress(): void {
  console.log('Called the testProgress method');
  interval(1000)
  .subscribe((value: number) => {
    this.progressSubject$.next(value);
    this.detectChanges();
    console.log('The progress: ' + value);
  });
}

UPDATE: I also tried subscribing to the first observable as in:

downloadSoundtrack(soundtrack: Soundtrack): void {
  const fileName: string = soundtrack.name + '.' + MIDI_FILE_SUFFIX;
  const progress$: Observable<ProgressTask<Uint8Array>> = this.midiService.progressiveCreateSoundtrackMidi$(soundtrack);
  console.log('Created the observable');
  const piper$: Observable<ProgressTask<Uint8Array>> = progress$
  .subscribe((progressTask: ProgressTask<Uint8Array>) => {
    this.progressSubject$.next(progressTask.loaded);
    this.detectChanges();
    console.log('Loaded: ' + progressTask.loaded);
  });
  console.log('Controller method call complete');
}

But the progress in the template still didn't update before the last value.

UPDATE: This time I tried to change the way the progress is being produced by the service class. Instead of doing it with a download as in:

public progressiveCreateSoundtrackMidi$(soundtrack: Soundtrack): Observable<ProgressTask<Uint8Array>> {
   return new Observable((observer$ : Subscriber<ProgressTask<Uint8Array>>) => {
     this.createSoundtrackMidi(soundtrack, observer$);
     return { unsubscribe() { } };
   });
}

I faked it with:

public progressiveCreateSoundtrackMidi$(soundtrack: Soundtrack): Observable<ProgressTask<Uint8Array>> {
  return interval(1000)
  .pipe(
    map((value: number) => {
      return this.downloadService.createProgressTask<Uint8Array>(1000, value);
    })
  );
}

And this now works fine with the progress being changed and reflected in the template continuously.

There is something fishy in my createSoundtrackMidi service method:

public createSoundtrackMidi(soundtrack: Soundtrack, progressTask$?: Subscriber<ProgressTask<Uint8Array>>): Uint8Array;

So I faked this above service method with:

public createSoundtrackMidi(soundtrack: Soundtrack, progressTask$?: Subscriber<ProgressTask<Uint8Array>>): Uint8Array {
  const midi: Midi = new Midi();
  if (progressTask$){
    for (let index: number = 0; index < 1000; index++) {
      this.commonService.sleep(10);
      progressTask$.next(this.downloadService.createProgressTask<Uint8Array>(1000, index));
    }
    progressTask$.next(this.downloadService.createProgressTask<Uint8Array>(1000, 1000));
    progressTask$.complete();
  }
  return midi.toArray();
}

and the issue showed up again.

To see if the method parameter progressTask$?: Subscriber<ProgressTask<Uint8Array>> was updating only a local copy I faked the following service method:

public progressiveCreateSoundtrackMidi$(soundtrack: Soundtrack): Observable<ProgressTask<Uint8Array>> {
  return new Observable((observer$ : Subscriber<ProgressTask<Uint8Array>>) => {
    for (let index: number = 0; index < 1000; index++) {
      this.commonService.sleep(10);
      observer$.next(this.downloadService.createProgressTask<Uint8Array>(1000, index));
    }
    observer$.next(this.downloadService.createProgressTask<Uint8Array>(1000, 1000));
    observer$.complete();
    return { unsubscribe() { } };
  });
}

and the issue showed up. The method call is not the cause for the issue.

The issue does not show either when using my own interval implementation:

public progressiveCreateSoundtrackMidi$(soundtrack: Soundtrack): Observable<ProgressTask<Uint8Array>> {
  const max: number = 200;
  return this.interval(max)
  .pipe(
    map((value: number) => {
      return this.downloadService.createProgressTask<Uint8Array>(max, value);
    })
  );
}

private interval(period: number): Observable<number> {
  return new Observable((observer$: Subscriber<number>) => {
    let i = 0;
    const handler = setInterval(() => observer$.next(i++), period);
    return () => clearInterval(handler);
  });
}

Is it because the setInterval() function is un-blocking between each iteration ?

The issue does not show either with:

public progressiveCreateSoundtrackMidi$(soundtrack: Soundtrack): Observable<ProgressTask<Uint8Array>> {
  const max: number = 200;

  return new Observable((observer$: Subscriber<ProgressTask<Uint8Array>>) => {
    let index = 0;
    const handler = setInterval(() => {
      observer$.next(this.downloadService.createProgressTask<Uint8Array>(max, index));
      index++;
    }, max);
    return () => clearInterval(handler);
  });
}

But a loop in a setTimeout() function call shows the issue:

public progressiveCreateSoundtrackMidi$(soundtrack: Soundtrack): Observable<ProgressTask<Uint8Array>> {
  const max: number = 200;

  return new Observable((observer$: Subscriber<ProgressTask<Uint8Array>>) => {
    const handler = setTimeout(() => {
      for (let index: number = 0; index < max; index++) {
        this.commonService.sleep(10);
        observer$.next(this.downloadService.createProgressTask<Uint8Array>(max, index));
      }
      observer$.next(this.downloadService.createProgressTask<Uint8Array>(max, max));
      observer$.complete();
    }, max);
    return () => clearTimeout(handler);
  });
}

How to un-block between each loop iteration, in the same fashion the setInterval() function is doing ?

Stephane
  • 11,836
  • 25
  • 112
  • 175
  • Did you try to create observable from subject and use this observable instead subject in view? – rad11 Nov 29 '20 at 13:51
  • This should not be happening the snippet looks fine. Can you create a repro on stackblitz.com ? – alt255 Nov 29 '20 at 13:54
  • @rad11 I just tried, adding an observable member as ` progressSubject$: BehaviorSubject = new BehaviorSubject(0); progress$: Observable = this.progressSubject$.asObservable();` but the issue did not change one bit. – Stephane Nov 29 '20 at 14:00
  • Maybe try use change detector ref and method markForCheck() – rad11 Nov 29 '20 at 14:15
  • @rad11 I had also tried the `markForcheck` alternative but it didn't help. – Stephane Nov 29 '20 at 14:16
  • Hmmm so its strange – rad11 Nov 29 '20 at 14:17
  • Try create getter method for subject and return in this.progress$.geValue(), and on view use this getter – rad11 Nov 29 '20 at 14:20
  • @rad11 The `getValue()` method does not exist on type `Observable` – Stephane Nov 29 '20 at 14:28
  • 1
    I sad on subject – rad11 Nov 29 '20 at 14:28
  • With `{{ getProgress() }}` and `getProgress(): number { return this.progressSubject$.getValue(); }` it is the very same issue. – Stephane Nov 29 '20 at 14:33
  • I narrowed the issue down to the service method. I added an update to the question to reflect this. – Stephane Nov 29 '20 at 16:32
  • `this.commonService.sleep(10);` - what is this? Is that a synchronous sleep function that you are using? Is your Observable emitting all of its values synchronously and therefore not giving the change detection any chance to run? – Will Taylor Nov 29 '20 at 17:08
  • I added the `sleep()` method implementation in the question. It is a synchronous thread blocking sleep. I use it to simulate a slow-responding long-running service method. I want to see that progress progressing. I don't know what to answer to your second question. I'd say, yes, the `progressiveCreateSoundtrackMidi()` service method is called from the component, but it returns an observable. – Stephane Nov 29 '20 at 17:26
  • I created a stackblitz https://stackblitz.com/edit/angular-ivy-u3gz1t with one thing to note: the `faulty` alternative does not behave exactly like on my application. Instead of staying at 0 during the console log progress (as it is doing in my application) it jumps to the maximum value before the console log progress. I made sure to have the exact same dependencies and their versions. – Stephane Nov 30 '20 at 10:43

0 Answers0