Search code examples
angularfirebasegoogle-cloud-firestoreangularfire2

Firestore listener subscription triggers random number of times


I have a Firestore listener that seems to trigger a random number of times. On the first page load, it might trigger 5 times, refreshing the page and it fires 13 times.

@Injectable({
  providedIn: 'root',
})
export class AuthService {

  user$: BehaviorSubject<SavedUser | undefined> = new BehaviorSubject<SavedUser | undefined>(undefined);

  constructor(private angularFireAuth: AngularFireAuth, private firestore: AngularFirestore, private router: Router) {
    this.onAuthStateChanged();
  }

  onAuthStateChanged() {

    let counter = 0;

    this.angularFireAuth.authState.subscribe(user => {
      if (user) {
        this.firestore
          .collection('user')
          .doc<User>(user.uid)
          .valueChanges()
          .subscribe(userRecord => {
            counter++

            console.log(counter); // Testing

            user.getIdToken(true);

            this.user$.next(userRecord);
          });
      }
    });
  }
}

The output of the console log:

auth.service.ts:35 1
auth.service.ts:35 2
auth.service.ts:35 3
auth.service.ts:35 4
auth.service.ts:35 5
auth.service.ts:35 6
auth.service.ts:35 7
auth.service.ts:35 8
auth.service.ts:35 9
auth.service.ts:35 10
auth.service.ts:35 11

All I'm looking to do here is refresh the user's token when the document it's backed by changes.

I know the this.angularFireAuth.onAuthStateChanged(user => {...} is only triggered once, and no changes are happening to the document.

I've tried unsubscribing from the Firestore subscription via onDestroy however that made no difference.

As a "fix" I thought I would be able to read the first value and stop processing via

this.firestore
  .collection('user')
  .doc<User>(user.uid)
  .valueChanges()
  .pipe(first())
  .subscribe(userRecord => {...});

Which did work at first sight, however that stops the .valueChanges() from triggering when the document is later changed.

Any tips?


Solution

  • Well I've solved my actual problem by setting a timestamp on the object via another firestore listener, then comparing the current and previous to only trigger once.

    I'll post this code if anyone comes here doing a similar thing, but I'd still want to find out why my subscriber is going off so many times.

    Cloud function:

    export class UserUpdateListener {
    
      public listen = functions.firestore
        .document('user/{uid}')
        .onWrite(async (snapshot) => {
    
          const before: User = snapshot.before.data() as User;
          const after: User = snapshot.after.data() as User;
    
          const skipUpdate = before.lastUpdate && after.lastUpdate && !before.lastUpdate.isEqual(after.lastUpdate);
          if (skipUpdate) {
            functions.logger.info('No changes, skipping timestamp update');
            return;
          }
    
          await snapshot.after.ref.update({ lastUpdate: admin.firestore.FieldValue.serverTimestamp() });
        });
    }
    

    I then check the timestamp in my client using:

    constructor(private angularFireAuth: AngularFireAuth, private firestore: AngularFirestore, private router: Router) {
      this.onAuthStateChanged();
    
      let lastUpdate: Timestamp;
    
      this.user$.subscribe(async (user) => {
        if (this.authUser && user && user.lastUpdate) {
          if (lastUpdate && !user.lastUpdate.isEqual(lastUpdate)) {
            await this.authUser.getIdToken(true);
          }
          lastUpdate = user.lastUpdate;
        }
      });
    }
    
    

    Hopefully this will save someone some time.