cirovladimir
cirovladimir

Reputation: 613

How to return an observable from service in angular after chaining pipe operations

I'm trying to chain / pipe operations and return an Observable from a Service in angular which uses angular fire.

With promises I have this working

Service

saveDiploma(diploma: { title: any; description: any; picture: any }) {
        return new Observable(observer => {
            const id = this.db.createId();
            this.storage.ref(`diplomas/${id}/original.jpg`)
                .putString(diploma.picture, 'data_url')
                .then(task => {
                    task.ref.getDownloadURL()
                        .then(url => {
                            const saved = {
                                title: diploma.title,
                                description: diploma.description,
                                url,
                                createdAt: firebase.firestore.FieldValue.serverTimestamp(),
                                createdBy: this.auth.auth.currentUser ? this.auth.auth.currentUser.uid : 'anonymous'
                            };
                            this.db.doc(`diplomas/${id}`)
                                .set(saved)
                                .then(() => {
                                    observer.next(saved);
                                    observer.complete();
                                })
                                .catch(e => observer.error(e));
                        })
                        .catch(e => observer.error(e));
                })
                .catch(e => observer.error(e));
        });
    }

Component

save() {
        this.diplomasService.saveDiploma({
            title: this.diplomaForm.value.title,
            description: this.diplomaForm.value.description,
            picture: this.currentImage
        }).subscribe(diploma => {
            console.log('saved diploma', diploma);
        }, e => console.error('error while saving the diploma', e));
    }

I'm trying to use Observables in the service instead of Promises and pipe them in order like so

saveDiploma(diploma: { title: any; description: any; picture: any }) {
        const id = this.db.createId();
        const ref = this.storage.ref(`diplomas/${id}/original.jpg`);
        return ref.putString(diploma.picture, 'data_url').snapshotChanges().pipe(
            concatMap(task => {
                console.log('getDownloadURL');
                return from(task.ref.getDownloadURL());
            }),
            concatMap(url => {
                console.log('url', url);
                const saved = {
                    title: diploma.title,
                    description: diploma.description,
                    url,
                    createdAt: firebase.firestore.FieldValue.serverTimestamp(),
                    createdBy: this.auth.auth.currentUser ? this.auth.auth.currentUser.uid : 'anonymous'
                };
                return from(this.db.doc(`diplomas/${id}`).set(saved));
            })
        );
    }

but the getDownloadURL method is getting fired before the upload is complete and hence returning an error storage/object-not-found. I've tried adding a finalize or filter (on task.state == 'success') before the concatMap(getDownloadURL) but I have failed getting it to work.

Does anyone know how to pipe this operations and return an Observable from them?

I'm using Angular 8.1.2, Angular Fire 5.2.1 and rxjs 6.5.1

Upvotes: 1

Views: 472

Answers (2)

frido
frido

Reputation: 14109

According to the AngularFire documentation ref.putString(..).snapshotChanges()

Emits the raw UploadTaskSnapshot as the file upload progresses.

So your problem is that .snapshotChanges() emits before the file upload is complete. concatMap gets triggered on every emit from the source not just on complete. You should use concat.

saveDiploma(diploma: { title: any; description: any; picture: any }) {
  const id = this.db.createId();
  const ref = this.storage.ref(`diplomas/${id}/original.jpg`);
  return concat(
    ref.putString(diploma.picture, 'data_url').snapshotChanges().pipe(ignoreElements()),
    defer(() => ref.getDownloadURL().pipe(
      switchMap(url => {
        console.log('url', url);
        const saved = {
          title: diploma.title,
          description: diploma.description,
          url,
          createdAt: firebase.firestore.FieldValue.serverTimestamp(),
          createdBy: this.auth.auth.currentUser ? this.auth.auth.currentUser.uid : 'anonymous'
        };
        return this.db.doc(`diplomas/${id}`).set(saved); // you can return a Promise directly
      })
    ))
  );
}

Possible alternative:

saveDiploma(diploma: { title: any; description: any; picture: any }) {
  const id = this.db.createId();
  const ref = this.storage.ref(`diplomas/${id}/original.jpg`);
  return ref.putString(diploma.picture, 'data_url').snapshotChanges().pipe(
    last(),
    switchMap(() => ref.getDownloadURL()),
    map(url => ({
      title: diploma.title,
      description: diploma.description,
      url,
      createdAt: firebase.firestore.FieldValue.serverTimestamp(),
      createdBy: this.auth.auth.currentUser ? this.auth.auth.currentUser.uid : 'anonymous'
    })),
    switchMap(saved => this.db.doc(`diplomas/${id}`).set(saved))
  );
}

Upvotes: 1

Jan-Niklas Wortmann
Jan-Niklas Wortmann

Reputation: 421

The problem here is that promises are by default eager. I think wrapping the from operator with the defer operator (https://rxjs.dev/api/index/function/defer) should solve your problem. So the code would look something like this:

return ref.putString(diploma.picture, 'data_url').snapshotChanges().pipe(
            concatMap(task => defer(() => {
                console.log('getDownloadURL');
                return from(task.ref.getDownloadURL());
            })),
            concatMap(url => defer(() => {
                console.log('url', url);
                const saved = {
                    title: diploma.title,
                    description: diploma.description,
                    url,
                    createdAt: firebase.firestore.FieldValue.serverTimestamp(),
                    createdBy: this.auth.auth.currentUser ? this.auth.auth.currentUser.uid : 'anonymous'
                };
                return from(this.db.doc(`diplomas/${id}`).set(saved));
            }))

The method passed to defer is evaluated as soon as it is subscribed to. ConcatMap will automatically subscribe to the inner observable as soon as there is an incoming notification from the source observable.

Upvotes: 0

Related Questions