0

I am not an expert with Rxjs. I have the following code. I am trying to upload multiple files and for each file, i want to base64 convert and have all the result in final array. Below code is not working and the finalize is not invoked at all. Can anyone help me what the problem with my code?

private validateSelectedFiles(files: FileList): void {
  this.spinnerService.show();
  from(files)
    .pipe(
      concatMap(f => this.convertFile(f)),
      finalize(() => {
        console.log('finalize'); //not coming here
        this.spinnerService.hide();
      }),
      scan((acc, curr) => {
        acc.push(curr)
        return acc;
      }, [])
    ).subscribe(result => {
      console.log('result', result); //Only once it is coming here
    })
}

private convertFile(file: File): Observable<string> {
  const result  = new ReplaySubject<string>(1);
  const reader = new FileReader();
  reader.readAsBinaryString(file);
  reader.onload = (event) => {
    result.next(btoa(event.target.result.toString()))
  };
  return result;
}

Expected functionality:

  1. for each upload files, convert it to base64
  2. get the array of base64 converted result for further processing

Following code worked for me:

private convertAttachmentsToBase64(): void {
        forkJoin(Array.from(this.addedFiles).map(file => this.convertFile(file)))
        .pipe(
            takeUntil(this.destroy$),
            finalize(() => this.spinnerService.hide()),
        )
        .subscribe((result) => this.attachmentsToUpload = result);      
    }   



// Convert file to Base64   
    private convertFile(file: File): Observable<AttachmentToUpload> {
        const result  = new Subject<AttachmentToUpload>();
        const reader = new FileReader();
        reader.readAsBinaryString(file);
        reader.onload = (event) => {
            result.next({
                fileName: file.name,
                data: btoa(event.target.result.toString())
            })
            result.complete();
        };
        return result;
    }   
Mukil Deepthi
  • 6,072
  • 13
  • 71
  • 156

2 Answers2

1

I could suggest the following

  1. Use RxJS forkJoin + Array#from + Array#map instead of from + scan combination.
  2. Use new Observable() to create an observable instead of a ReplaySubject.
  3. I don't see a HTTP request in the question at the moment. I've added it using a switchMap operator.
  4. I am not sure if the convertFile() actually converts the file to Base64. As an alternative you could try this solution.
  5. Important: forkJoin will only emit if all the source observable emit. So if the onload function isn't triggered by the FileReader(), the forkJoin won't emit and the finalize and subscribe won't be triggered.
private validateSelectedFiles(files: FileList): void {
  forkJoin(Array.from(files).map(file => this.convertFile(file))).pipe(
    switchMap((filesB64) => this.http.post('some url')),
    finalize(() => this.spinnerService.hide())
  ).subscribe({
    next: (result: any) => {
      // handle response
    },
    error: (error: any) => {
      // handle errors
    }
  });
}

private convertFile(file: File): Observable<string> {
  return new Observable((observer: any) => {
    const reader = new FileReader();
    reader.onload = (event: any) => {
      observer.next(btoa(event.target.result.toString()));
      observer.complete();
    };
    reader.readAsBinaryString(file);
  });
}

Update: Add Array#from() for FileList (credit: https://stackoverflow.com/a/40902462/6513921)

ruth
  • 29,535
  • 4
  • 30
  • 57
  • Thanks for your reply. FIrstly not able to use Array#map with FileList And in my code i am using btoa to convert base64. solution in the example you provided not fitting into my api. thanks anyway. – Mukil Deepthi Jan 21 '22 at 09:36
  • @MukilDeepthi: You could use `Array.from(files)`. I've updated the answer. Naturally, converting using `btoa()` could be used as well. It was only a suggestion. – ruth Jan 21 '22 at 09:43
  • Thanks Michael. this was really useful and the forkJoin worked for my case. – Mukil Deepthi Jan 21 '22 at 12:42
0

finalize() is a tear-down handler that is called when the chain is terminated (after source Observable completes, errors or the chain is unsubscribed).

In your case you're using concatMap() to merge another Observable into the chain. However, you're merging ReplaySubject that never completes on its own so the chain is never terminated and thus finalize() is never invoked.

It doesn't look like you even need to be using ReplaySubject so you can complete the Subject immediately when the file is loaded.

private convertFile(file: File): Observable<string> {
  const result  = new Subject<string>();
  const reader = new FileReader();
  reader.readAsBinaryString(file);
  reader.onload = (event) => {
    result.next(btoa(event.target.result.toString()));
    result.complete();
  };
  return result;
}
martin
  • 93,354
  • 25
  • 191
  • 226