1

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

cirovladimir
  • 593
  • 6
  • 25
  • 1
    Does your file upload perhaps `next`s upload progress? In that case you may not want to `concatMap()` on the `value`, as this value is being passed `onNext()` every time. You may want to wait for `complete()` or an event looking like that to be passed before mapping. Depending on how you build your stream, you could either ignore the progress events or pass them along to a separate function to pass them along. – Bjorn 'Bjeaurn' S Aug 27 '19 at 07:55
  • That's what I tried with `filter`, to filter the values until `success` is returned -it means it has completed the upload-. But it didn't work . I'll search if there's a `complete()` event as you mentioned. Thanks for the pointer anyway. – cirovladimir Aug 27 '19 at 17:08
  • @cirovladimir please post the Angular, RxJS and AngularFire versions you're using. – frido Aug 27 '19 at 17:28
  • The `complete()` is from RxJS self; when an Observable comples it will fire that once and call it's teardown logic. If the Observable you are using for uploading is implemented nicely; it would call `complete()` as the final step and you could handle your Observable results in there without having to filter. But it also seems like you could rewrite your filter and it should work too. – Bjorn 'Bjeaurn' S Aug 28 '19 at 09:31

2 Answers2

1

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))
  );
}
frido
  • 13,065
  • 5
  • 42
  • 56
  • that's what I thought and that's why I tried to filter until it returned success, even tried with finalize but it didn't work. I tried this and it's still returning `storage/object-not-found`. ‍♂️ – cirovladimir Aug 27 '19 at 17:05
  • @cirovladimir You should add your code where you're using `finalize` to your question, so we can see exactly what you tried and what isn't working for you. In the meantime try surrounding `ref.getDownloadURL()` with `defer` as @Jan suggests. It might be needed if `getDownloadURL` immediately queries the database. I updated my anwser. – frido Aug 27 '19 at 19:29
  • With the `defer` wrapping around getDownloadURL it's working as expected. My original goal on using Observables and `pipe` was making my code more readable, sort of `putString | getDownloadURL | set (save)`. I'm not sure I achieved my original goal but it's working Thank you very much! – cirovladimir Aug 27 '19 at 19:47
  • @cirovladimir Glad I could help. I still think that this approach is cleaner than the one using Promises. I added alternative code that's using `last` and is a little shorter to my answer. Please try it out to check if this works too. – frido Aug 28 '19 at 10:54
0

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.

  • Thanks, I tried this but it didn't work. It's still returning `storage/object-not-found` as if it is still calling getDownloadURL before the upload completes. – cirovladimir Aug 27 '19 at 17:14