Условия гонки при извлечении, обновлении и удалении документов с использованием Firebase с AngularFire - PullRequest
0 голосов
/ 04 апреля 2019

Я столкнулся с проблемой, которую мне трудно решить.Есть две проблемы, каждая из которых, по-видимому, вызвана условиями гонки.

1.) Функция drawPoll () выполняется до добавления this.poll.choices.Я подтвердил, что это проблема, добавив вручную 3 секунды setTimeout ().Как мне убедиться, что функция drawPoll () выполняется только после того, как итерация choices.ForEach () завершена?

2.) При вызове функции голосования (choiceId) и уменьшении выбора наблюдаемая firebaseService не выбираетдо правильного значения для «голосов», потому что наблюдаемое вступает в силу до того, как удаление голоса завершится.Как я переставляю свой код так, чтобы наблюдаемое ожидало, пока удаление документа голосования завершено?

Я пытался обернуть итерацию choices.forEach в обещание, но мне было трудно заставить это работать.И я не был уверен, где вообще начинать создавать цепочку обещаний для decmentChoice () и getChoices (), поскольку функция getChoices () не всегда полагается на функцию decmentChoice или incrementChoice (), когда она инициализируется.Это зависит только от тех, кто голосует.Прилагается мой компонент и служба 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, где функция drawPoll () выполняется до того, как this.poll.choices завершит итерацию и добавление.Я обновил вопрос, чтобы отразить мой обновленный код.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...