<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import ReactTooltip from 'react-tooltip';

import { ApiDownError, CorruptResponseError, JsonParseError, LogNotFoundError } from 'common/fetchWcl';
import fetchEvents from 'common/fetchEvents';
import { captureException } from 'common/errorLogger';
import getFightName from 'common/getFightName';
import { getCombatants } from 'selectors/combatants';
import { getError } from 'selectors/error';
import { getFightId, getPlayerId, getPlayerName, getReportCode } from 'selectors/url/report';
import { getArticleId } from 'selectors/url/news';
import { getReport } from 'selectors/report';
import { getFightById } from 'selectors/fight';
import { fetchReport } from 'actions/report';
import { setReportProgress } from 'actions/reportProgress';
import { fetchCombatants } from 'actions/combatants';
import { apiDownError, reportNotFoundError, unknownError, unknownNetworkIssueError } from 'actions/error';
import { appendReportHistory } from 'actions/reportHistory';
import AVAILABLE_CONFIGS from 'Parser/AVAILABLE_CONFIGS';
import makeAnalyzerUrl from 'Main/makeAnalyzerUrl';

import FightSelecter from './FightSelecter';
import PlayerSelecter from './PlayerSelecter';
import Results from './Results';
import ActivityIndicator from '../ActivityIndicator';

const timeAvailable = console.time && console.timeEnd;

const PROGRESS_STEP1_INITIALIZATION = 0.02;
const PROGRESS_STEP2_FETCH_EVENTS = 0.13;
const PROGRESS_STEP3_PARSE_EVENTS = 0.99;
const PROGRESS_COMPLETE = 1.0;

/* eslint-disable no-alert */

class Report extends React.Component {
  static propTypes = {
    reportCode: PropTypes.string,
    playerName: PropTypes.string,
    playerId: PropTypes.number,
    fightId: PropTypes.number,
    report: PropTypes.shape({
      title: PropTypes.string.isRequired,
      code: PropTypes.string.isRequired,
    }),
    fight: PropTypes.shape({
      start_time: PropTypes.number.isRequired,
      end_time: PropTypes.number.isRequired,
      boss: PropTypes.number.isRequired,
    }),
    combatants: PropTypes.arrayOf(PropTypes.shape({
      sourceID: PropTypes.number.isRequired,
    })),
    fetchReport: PropTypes.func.isRequired,
    setReportProgress: PropTypes.func.isRequired,
    fetchCombatants: PropTypes.func.isRequired,
    reportNotFoundError: PropTypes.func.isRequired,
    apiDownError: PropTypes.func.isRequired,
    unknownNetworkIssueError: PropTypes.func.isRequired,
    unknownError: PropTypes.func.isRequired,
    appendReportHistory: PropTypes.func.isRequired,
  };
  static childContextTypes = {
    config: PropTypes.object,
  };
  state = {
    finished: false,
    config: null,
  };

  getChildContext() {
    return {
      config: this.state.config,
    };
  }

  // Parsing a fight for a player is a "job", if the selected player or fight changes we want to stop parsing it. This integer gives each job an id that if it mismatches stops the job.
  _jobId = 0;
  get isReportValid() {
    return this.props.report && this.props.report.code === this.props.reportCode;
  }
  getConfig(specId) {
    return AVAILABLE_CONFIGS.find(config => config.spec.id === specId);
  }
  createParser(ParserClass, report, fight, player) {
    const playerPets = this.getPlayerPetsFromReport(report, player.id);

    return new ParserClass(report, player, playerPets, fight);
  }
  async setStatePromise(newState) {
    return new Promise((resolve, reject) => {
      this.setState(newState, resolve);
    });
  }
  async fetchEventsAndParse(report, fight, combatants, combatant, player) {
    // We use the setState callback for triggering UI updates to allow our CSS animations to work

    this.reset();
    this.props.setReportProgress(0);
    const config = this.getConfig(combatant.specID);
    timeAvailable && console.time('full parse');
    const parser = this.createParser(config.parser, report, fight, player);
    // We send combatants already to the analyzer so it can show the results page with the correct items and talents while waiting for the API request
    parser.initialize(combatants);
    await this.setStatePromise({
      config,
      parser,
    });
    this.props.setReportProgress(PROGRESS_STEP1_INITIALIZATION);
    await this.parse(parser, report, player, fight);
  }
  async parse(parser, report, player, fight) {
    this._jobId += 1;
    const jobId = this._jobId;
    let events;
    try {
      this.startFakeNetworkProgress();
      events = await fetchEvents(report.code, fight.start_time, fight.end_time, player.id);
      this.stopFakeNetworkProgress();
    } catch (err) {
      this.stopFakeNetworkProgress();
      if (err instanceof LogNotFoundError) {
        this.props.reportNotFoundError();
      } else if (err instanceof ApiDownError) {
        this.props.apiDownError();
      } else if (err instanceof JsonParseError) {
        captureException(err);
        this.props.unknownError('JSON parse error, the API response is probably corrupt. Let us know on Discord and we may be able to fix it for you.');
      } else {
        // Some kind of network error, internet may be down.
        captureException(err);
        this.props.unknownNetworkIssueError(err);
      }
      return;
    }
    try {
      events = parser.normalize(events);
      this.props.setReportProgress(PROGRESS_STEP2_FETCH_EVENTS);

      const batchSize = 300;
      const numEvents = events.length;
      let offset = 0;

      while (offset < numEvents) {
        if (this._jobId !== jobId) {
          return;
        }
        const eventsBatch = events.slice(offset, offset + batchSize);
        parser.parseEvents(eventsBatch);
        const progress = Math.min(1, (offset + batchSize) / numEvents);
        this.props.setReportProgress(PROGRESS_STEP2_FETCH_EVENTS + (PROGRESS_STEP3_PARSE_EVENTS - PROGRESS_STEP2_FETCH_EVENTS) * progress);
        // await-ing setState does not ensure we wait until a render completed, so instead we wait 1 frame
        await this.timeout(0);

        offset += batchSize;
      }<commit_msg>Fix bug where the selected player's Combatant instance being reinitialize could lead to unexpected behavior<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import ReactTooltip from 'react-tooltip';

import { ApiDownError, CorruptResponseError, JsonParseError, LogNotFoundError } from 'common/fetchWcl';
import fetchEvents from 'common/fetchEvents';
import { captureException } from 'common/errorLogger';
import getFightName from 'common/getFightName';
import { getCombatants } from 'selectors/combatants';
import { getError } from 'selectors/error';
import { getFightId, getPlayerId, getPlayerName, getReportCode } from 'selectors/url/report';
import { getArticleId } from 'selectors/url/news';
import { getReport } from 'selectors/report';
import { getFightById } from 'selectors/fight';
import { fetchReport } from 'actions/report';
import { setReportProgress } from 'actions/reportProgress';
import { fetchCombatants } from 'actions/combatants';
import { apiDownError, reportNotFoundError, unknownError, unknownNetworkIssueError } from 'actions/error';
import { appendReportHistory } from 'actions/reportHistory';
import AVAILABLE_CONFIGS from 'Parser/AVAILABLE_CONFIGS';
import makeAnalyzerUrl from 'Main/makeAnalyzerUrl';

import FightSelecter from './FightSelecter';
import PlayerSelecter from './PlayerSelecter';
import Results from './Results';
import ActivityIndicator from '../ActivityIndicator';

const timeAvailable = console.time && console.timeEnd;

const PROGRESS_STEP1_INITIALIZATION = 0.02;
const PROGRESS_STEP2_FETCH_EVENTS = 0.13;
const PROGRESS_STEP3_PARSE_EVENTS = 0.99;
const PROGRESS_COMPLETE = 1.0;

/* eslint-disable no-alert */

class Report extends React.Component {
  static propTypes = {
    reportCode: PropTypes.string,
    playerName: PropTypes.string,
    playerId: PropTypes.number,
    fightId: PropTypes.number,
    report: PropTypes.shape({
      title: PropTypes.string.isRequired,
      code: PropTypes.string.isRequired,
    }),
    fight: PropTypes.shape({
      start_time: PropTypes.number.isRequired,
      end_time: PropTypes.number.isRequired,
      boss: PropTypes.number.isRequired,
    }),
    combatants: PropTypes.arrayOf(PropTypes.shape({
      sourceID: PropTypes.number.isRequired,
    })),
    fetchReport: PropTypes.func.isRequired,
    setReportProgress: PropTypes.func.isRequired,
    fetchCombatants: PropTypes.func.isRequired,
    reportNotFoundError: PropTypes.func.isRequired,
    apiDownError: PropTypes.func.isRequired,
    unknownNetworkIssueError: PropTypes.func.isRequired,
    unknownError: PropTypes.func.isRequired,
    appendReportHistory: PropTypes.func.isRequired,
  };
  static childContextTypes = {
    config: PropTypes.object,
  };
  state = {
    finished: false,
    config: null,
  };

  getChildContext() {
    return {
      config: this.state.config,
    };
  }

  // Parsing a fight for a player is a "job", if the selected player or fight changes we want to stop parsing it. This integer gives each job an id that if it mismatches stops the job.
  _jobId = 0;
  get isReportValid() {
    return this.props.report && this.props.report.code === this.props.reportCode;
  }
  getConfig(specId) {
    return AVAILABLE_CONFIGS.find(config => config.spec.id === specId);
  }
  createParser(ParserClass, report, fight, player) {
    const playerPets = this.getPlayerPetsFromReport(report, player.id);

    return new ParserClass(report, player, playerPets, fight);
  }
  async setStatePromise(newState) {
    return new Promise((resolve, reject) => {
      this.setState(newState, resolve);
    });
  }
  async fetchEventsAndParse(report, fight, combatants, combatant, player) {
    // We use the setState callback for triggering UI updates to allow our CSS animations to work

    this.reset();
    this.props.setReportProgress(0);
    const config = this.getConfig(combatant.specID);
    timeAvailable && console.time('full parse');
    const parser = this.createParser(config.parser, report, fight, player);
    // We send combatants already to the analyzer so it can show the results page with the correct items and talents while waiting for the API request
    parser.initialize(combatants);
    await this.setStatePromise({
      config,
      parser,
    });
    this.props.setReportProgress(PROGRESS_STEP1_INITIALIZATION);
    await this.parse(parser, report, player, fight);
  }
  async parse(parser, report, player, fight) {
    this._jobId += 1;
    const jobId = this._jobId;
    let events;
    try {
      this.startFakeNetworkProgress();
      events = await fetchEvents(report.code, fight.start_time, fight.end_time, player.id);
      this.stopFakeNetworkProgress();
    } catch (err) {
      this.stopFakeNetworkProgress();
      if (err instanceof LogNotFoundError) {
        this.props.reportNotFoundError();
      } else if (err instanceof ApiDownError) {
        this.props.apiDownError();
      } else if (err instanceof JsonParseError) {
        captureException(err);
        this.props.unknownError('JSON parse error, the API response is probably corrupt. Let us know on Discord and we may be able to fix it for you.');
      } else {
        // Some kind of network error, internet may be down.
        captureException(err);
        this.props.unknownNetworkIssueError(err);
      }
      return;
    }
    try {
      // The events we fetched will be all events related to the selected player. This includes the `combatantinfo` for the selected player. However we have already parsed this event when we loaded the combatants in the `initializeAnalyzers` of the CombatLogParser. Loading the selected player again could lead to bugs since it would reinitialize and overwrite the existing entity (the selected player) in the Combatants module.
      events = events.filter(event => event.type !== 'combatantinfo');
      events = parser.normalize(events);
      this.props.setReportProgress(PROGRESS_STEP2_FETCH_EVENTS);

      const batchSize = 300;
      const numEvents = events.length;
      let offset = 0;

      while (offset < numEvents) {
        if (this._jobId !== jobId) {
          return;
        }
        const eventsBatch = events.slice(offset, offset + batchSize);
        parser.parseEvents(eventsBatch);
        const progress = Math.min(1, (offset + batchSize) / numEvents);
        this.props.setReportProgress(PROGRESS_STEP2_FETCH_EVENTS + (PROGRESS_STEP3_PARSE_EVENTS - PROGRESS_STEP2_FETCH_EVENTS) * progress);
        // await-ing setState does not ensure we wait until a render completed, so instead we wait 1 frame
        await this.timeout(0);

        offset += batchSize;
      }
{'fraction_non_alphanumeric': 0.09744634184552718, 'fraction_numerical': 0.00454331818893937, 'mean_word_length': 3.7762813318368873, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 8, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7453412', 'n_tokens_mistral': 3669, 'n_tokens_neox': 3665, 'n_words': 1016}