使用Firebase和AngularFire获取,更新和删除文档时的竞争条件

时间:2019-04-04 00:20:55

标签: javascript angular firebase angularfire2

我遇到了一个难以解决的问题。这两个问题似乎都是由比赛条件引起的。

1。)在完成this.poll.choices附加之前,将执行drawPoll()函数。我通过手动添加3秒setTimeout()来确认这是问题所在。如何确保drawPoll()函数仅在options.ForEach()迭代完成后才执行?

2。)在调用vote(choiceId)函数并减少一个选择时,firebaseService observable不会为“ votes”选择正确的值,因为observable在表决删除完成执行之前就加入了。如何重新排列我的代码,以便可观察的对象等到表决文档删除完成?

我试图将choices.forEach迭代包装在promise中,但是很难使它起作用。而且我不确定在哪里开始为decrementChoice()和getChoices()做一个诺言链,因为getChoices()函数在初始化时并不总是依赖于decrementChoice或crementChoice()函数。它仅取决于投票时的投票者。附件是我的组件和Firebase服务。任何帮助将不胜感激!

poll.component.ts

import { Component, OnInit } from '@angular/core';
import * as Chart from 'chart.js';
import { Observable } from 'rxjs';
import { FirebaseService } from '../services/firebase.service';
import { first } from 'rxjs/operators';
import { Input, Output, EventEmitter } from '@angular/core';
import { CardModule } from 'primeng/card';

@Component({
  selector: 'app-poll',
  templateUrl: './poll.component.html',
  styleUrls: ['./poll.component.scss']
})
export class PollComponent implements OnInit {
  chart:any;
  poll:any;
  votes:[] = [];
  labels:string[] = [];
  title:string = "";
  isDrawn:boolean = false;
  inputChoices:any = [];
  username:string = "";
  points:number;

  @Input()
  pollKey: string;

  @Output()
  editEvent = new EventEmitter<string>();

  @Output()
  deleteEvent = new EventEmitter<string>();

  constructor(private firebaseService: FirebaseService) { }

  ngOnInit() {
    this.firebaseService.getPoll(this.pollKey).subscribe(pollDoc => {
      // ToDo: draw poll choices on create without breaking vote listener
      console.log("details?", pollDoc);
      // Return if subscription was triggered due to poll deletion
      if (!pollDoc.payload.exists) {
        return;
      }
      const pollData:any = pollDoc.payload.data();
      this.poll = {
        id: pollDoc.payload.id,
        helperText: pollData.helperText,
        pollType: pollData.pollType,
        scoringType: pollData.scoringType,
        user: pollData.user
      };

      if (this.poll.pollType == 1) {
        this.title = "Who Do I Start?";
      }
      if (this.poll.pollType == 2) {
        this.title = "Who Do I Drop?";
      }
      if (this.poll.pollType == 3) {
        this.title = "Who Do I Pick Up?";
      }
      if (this.poll.pollType == 4) {
        this.title = "Who Wins This Trade?";
      }

      // Populate username and user points
      this.firebaseService.getUser(pollData.user).subscribe((user:any) => {
        const userDetails = user.payload._document.proto;
        if (userDetails) {
        this.username = userDetails.fields.username.stringValue;
        this.points = userDetails.fields.points.integerValue;
        }
      });

      this.firebaseService.getChoices(this.pollKey).pipe(first()).subscribe(choices => {
        console.log("get choices");
        this.poll.choices = [];
        choices.forEach(choice => {
          const choiceData:any = choice.payload.doc.data();
          const choiceKey:any = choice.payload.doc.id;
          this.firebaseService.getVotes(choiceKey).pipe(first()).subscribe((votes: any) => {
            this.poll.choices.push({
              id: choiceKey,
              text: choiceData.text,
              votes: votes.length
            });
          });
          this.firebaseService.getVotes(choiceKey).subscribe((votes: any) => {
            if (this.isDrawn) {
              const selectedChoice = this.poll.choices.find((choice) => {
                return choice.id == choiceKey
              });
              selectedChoice.votes = votes.length;
              this.drawPoll();
            }
          });
        });
        setTimeout(() => {
          this.drawPoll();
        }, 3000)
      });
    });
  }

  drawPoll() {
    if (this.isDrawn) {
      this.chart.data.datasets[0].data = this.poll.choices.map(choice => choice.votes);
      this.chart.data.datasets[0].label = this.poll.choices.map(choice => choice.text);
      this.chart.update()
    }
    if (!this.isDrawn) {
      console.log("text?", this.poll.choices.map(choice => choice.text));
      this.inputChoices = this.poll.choices;
      var canvas =  <HTMLCanvasElement> document.getElementById(this.pollKey);
      var ctx = canvas.getContext("2d");
      this.chart = new Chart(ctx, {
        type: 'horizontalBar',
        data: {
          labels: this.poll.choices.map(choice => choice.text),
          datasets: [{
            label: this.title,
            data: this.poll.choices.map(choice => choice.votes),
            fill: false,
            backgroundColor: [
              "rgba(255, 4, 40, 0.2)",
              "rgba(19, 32, 98, 0.2)",
              "rgba(255, 4, 40, 0.2)",
              "rgba(19, 32, 98, 0.2)",
              "rgba(255, 4, 40, 0.2)",
              "rgba(19, 32, 98, 0.2)"
            ],
            borderColor: [
              "rgb(255, 4, 40)",
              "rgb(19, 32, 98)",
              "rgb(255, 4, 40)",
              "rgb(19, 32, 98)",
              "rgb(255, 4, 40)",
              "rgb(19, 32, 98)",
            ],
            borderWidth: 1
          }]
        },
        options: {
          events: ["touchend", "click", "mouseout"],
          onClick: function(e) {
            console.log("clicked!", e);
          },
          tooltips: {
            enabled: true
          },
          title: {
            display: true,
            text: this.title,
            fontSize: 14,
            fontColor: '#666'
          },
          legend: {
            display: false
          },
          maintainAspectRatio: true,
          responsive: true,
          scales: {
            xAxes: [{
              ticks: {
                beginAtZero: true,
                precision: 0
              }
            }]
          }
        }
      });
      this.isDrawn = true;
    }
  }

  vote(choiceId) {
    if (choiceId) {
      const choiceInput:any = document.getElementById(choiceId);
      const checked = choiceInput.checked;
      this.poll.choices.forEach(choice => {
        const choiceEl:any = document.getElementById(choice.id);
        if (choiceId !== choiceEl.id && checked) choiceEl.disabled = true;
        if (!checked) choiceEl.disabled = false;
      });
      if (checked) this.firebaseService.incrementChoice(choiceId);
      if (!checked) this.firebaseService.decrementChoice(choiceId);
    }
  }

  edit() {
    this.editEvent.emit(this.poll);
  }

  delete() {
    this.deleteEvent.emit(this.poll);
  }

}

firebase.service.ts

import { Injectable } from '@angular/core';
import { AngularFirestore } from '@angular/fire/firestore';
import { map, switchMap, first } from 'rxjs/operators';
import { Observable, from } from 'rxjs';
import * as firebase from 'firebase';
import { AngularFireAuth } from '@angular/fire/auth';

@Injectable({
  providedIn: 'root'
})
export class FirebaseService {
  // Source: https://github.com/AngularTemplates/angular-firebase-crud/blob/master/src/app/services/firebase.service.ts
  constructor(public db: AngularFirestore, private afAuth: AngularFireAuth) { }

  getPoll(pollKey) {
    return this.db.collection('polls').doc(pollKey).snapshotChanges();
  }

  getChoices(pollKey) {
    return this.db.collection('choices', ref => ref.where('poll', '==', pollKey)).snapshotChanges();
  }


  incrementChoice(choiceKey) {
    const userId = this.afAuth.auth.currentUser.uid;
    const choiceDoc:any = this.db.collection('choices').doc(choiceKey);
    // Check if user voted already
    choiceDoc.ref.get().then(choice => {
      let pollKey = choice.data().poll
      this.db.collection('votes').snapshotChanges().pipe(first()).subscribe((votes:any) => {
        let filteredVote = votes.filter((vote) => {
          const searchedPollKey = vote.payload.doc._document.proto.fields.poll.stringValue;
          const searchedChoiceKey = vote.payload.doc._document.proto.fields.choice.stringValue;
          const searchedUserKey = vote.payload.doc._document.proto.fields.user.stringValue;
          return (searchedPollKey == pollKey && searchedChoiceKey == choiceKey && searchedUserKey == userId);
        });
        if (filteredVote.length) {
          // This person aleady voted
          return false;
        } else {
          let votes = choice.data().votes
          choiceDoc.update({
            votes: ++votes
          });
          const userDoc:any = this.db.collection('users').doc(userId);
          userDoc.ref.get().then(user => {
            let points = user.data().points
            userDoc.update({
              points: ++points
            });
          });
          this.createVote({
            choiceKey: choiceKey,
            pollKey: pollKey,
            userKey: userId
          });
        }
      });
    });
  }

  decrementChoice(choiceKey) {
    const choiceDoc:any = this.db.collection('choices').doc(choiceKey);
    const userId = this.afAuth.auth.currentUser.uid;
    choiceDoc.ref.get().then(choice => {
      let pollKey = choice.data().poll
      let votes = choice.data().votes
      choiceDoc.update({
        votes: --votes
      });
      const userDoc:any = this.db.collection('users').doc(userId);
      userDoc.ref.get().then(user => {
        let points = user.data().points
        userDoc.update({
          points: --points
        });
      });
      // Find & delete vote
      this.db.collection('votes').snapshotChanges().pipe(first()).subscribe((votes:any) => {
        let filteredVote = votes.filter((vote) => {
          const searchedPollKey = vote.payload.doc._document.proto.fields.poll.stringValue;
          const searchedChoiceKey = vote.payload.doc._document.proto.fields.choice.stringValue;
          const searchedUserKey = vote.payload.doc._document.proto.fields.user.stringValue;
          return (searchedPollKey == pollKey && searchedChoiceKey == choiceKey && searchedUserKey == userId);
        });
        this.deleteVote(filteredVote[0].payload.doc.id);
      });
    });
  }


  createVote(value) {
    this.db.collection('votes').add({
      choice: value.choiceKey,
      poll: value.pollKey,
      user: value.userKey
    }).then(vote => {
      console.log("Vote created successfully", vote);
    }).catch(err => {
      console.log("Error creating vote", err);  
    });
  }

  deleteVote(voteKey) {
    this.db.collection('votes').doc(voteKey).delete().then((vote) => {
      console.log("Vote deleted successfully");
    }).catch(err => {
      console.log("Error deleting vote", err);
    });
  }

  getVotes(choiceKey) {
    return this.db.collection('votes', ref => ref.where('choice', '==', choiceKey)).snapshotChanges().pipe(first());
  }

}

**更新**

我能够通过为投票更新创建单独的订阅来解决问题2。该代码相当笨拙,但是至少现在#2不再是问题。我仍然遇到与#1相同的问题,其中在this.poll.choices完成迭代和追加之前已执行drawPoll()函数。我更新了问题以反映我更新的代码。

0 个答案:

没有答案