<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';
import FightNavigation from './FightNavigation';

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 parserClass = await config.parser();
    const parser = this.createParser(parserClass, 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;

      timeAvailable && console.time('player event parsing');
      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;
      }
      timeAvailable && console.timeEnd('player event parsing');

      parser.fabricateEvent({
        type: 'finished',
      });
      timeAvailable && console.timeEnd('full parse');
      this.props.setReportProgress(PROGRESS_COMPLETE);
      this.setState({
        finished: true,
      });
    } catch (err) {
      captureException(err);
      if (process.env.NODE_ENV === 'development') {
        // Something went wrong during the analysis of the log, there's probably an issue in your analyzer or one of its modules.
        throw err;
      } else {
        alert(`The report could not be parsed because an error occured while running the analysis. ${err.message}`);
      }
    }
  }
  _isFakeNetworking = false;
  async startFakeNetworkProgress() {
    this._isFakeNetworking = true;
    const expectedDuration = 5000;
    const stepInterval = 50;

    const jobId = this._jobId;

    let step = 1;
    while (this._isFakeNetworking) {
      if (this._jobId !== jobId) {
        // This could happen when switching players/fights while still loading another one
        break;
      }
      const progress = Math.min(1, step * stepInterval / expectedDuration);
      this.props.setReportProgress(PROGRESS_STEP1_INITIALIZATION + ((PROGRESS_STEP2_FETCH_EVENTS - PROGRESS_STEP1_INITIALIZATION) * progress));
      await this.timeout(stepInterval);
      step += 1;
    }
  }
  stopFakeNetworkProgress() {
    this._isFakeNetworking = false;
  }
  timeout(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  async fetchCombatantsForFight(report, fight) {
    try {
      await this.props.fetchCombatants(report.code, fight.start_time, fight.end_time);
    } catch (err) {
      // TODO: Redirect to homepage
      this.reset();
      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);
      }
    }
  }

  reset() {
    this._jobId += 1;
    this.setState({
      config: null,
      parser: null,
      finished: false,
    });
    this.props.setReportProgress(null);
    this.stopFakeNetworkProgress();
  }

  componentWillMount() {
    this.fetchReportIfNecessary({});
  }
  componentDidUpdate(prevProps, prevState) {
    ReactTooltip.rebuild();

    this.fetchReportIfNecessary(prevProps);
    this.fetchCombatantsIfNecessary(prevProps, prevState);
    this.fetchEventsAndParseIfNecessary(prevProps, prevState);
  }
  fetchReportIfNecessary(prevProps) {
    if (this.props.reportCode && this.props.reportCode !== prevProps.reportCode) {
      this.props.fetchReport(this.props.reportCode)
        .catch(err => {
          this.reset();
          if (err instanceof LogNotFoundError) {
            this.props.reportNotFoundError();
          } else if (err instanceof ApiDownError) {
            this.props.apiDownError();
          } else if (err instanceof CorruptResponseError) {
            captureException(err);
            this.props.unknownError('Corrupt Warcraft Logs API response received, this report can not be processed.');
          } 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);
          }
        });
    }
  }
  fetchCombatantsIfNecessary(prevProps, prevState) {
    if (this.isReportValid && this.props.fight && (this.props.report !== prevProps.report || this.props.fight !== prevProps.fight)) {
      // A report has been loaded, it is the report the user wants (this can be a mismatch if a new report is still loading), a fight was selected, and one of the fight-relevant things was changed
      this.fetchCombatantsForFight(this.props.report, this.props.fight);
    }
  }
  fetchEventsAndParseIfNecessary(prevProps, prevState) {
    const changed = this.props.report !== prevProps.report
      || this.props.combatants !== prevProps.combatants
      || this.props.fightId !== prevProps.fightId
      || this.props.playerName !== prevProps.playerName
      || this.props.playerId !== prevProps.playerId;
    if (changed) {
      this.reset();

      const report = this.props.report;
      const fight = this.props.fight;
      const combatants = this.props.combatants;
      const playerId = this.props.playerId;
      const playerName = this.props.playerName;
      const valid = report && fight && combatants && (playerName || playerId);
      if (valid) {
        const player = this.getPlayerFromReport(report, playerId, playerName);
        if (!player) {
          alert(`Unknown player: ${playerName}`);
          return;
        }
        const combatant = combatants.find(combatant => combatant.sourceID === player.id);
        if (!combatant) {
          alert('This player does not seem to be in this fight.');
          return;
        }
        this.fetchEventsAndParse(report, fight, combatants, combatant, player);
        this.appendHistory(report, fight, player);
      }
    }
  }
  getPlayerFromReport(report, playerId, playerName) {
    if (playerId) {
      return report.friendlies.find(friendly => friendly.id === playerId);
    }
    const fetchByNameAttempt = report.friendlies.find(friendly => friendly.name === playerName);
    if (!fetchByNameAttempt) {
      return report.friendlies.find(friendly => friendly.id === Number(playerName));
    }
    return fetchByNameAttempt;
  }
  getPlayerPetsFromReport(report, playerId) {
    return report.friendlyPets.filter(pet => pet.petOwner === playerId);
  }
  appendHistory(report, fight, player) {
    this.props.appendReportHistory({
      code: report.code,
      title: report.title,
      start: Math.floor(report.start / 1000),
      end: Math.floor(report.end / 1000),
      fightId: fight.id,
      fightName: getFightName(report, fight),
      playerId: player.id,
      playerName: player.name,
      playerClass: player.type,
    });
  }

  render() {
    const { report, fightId, playerName } = this.props;

    if (!report) {
      return <ActivityIndicator text="Pulling report info..." />;
    }
    if (!fightId) {
      return <FightSelecter />;
    }
    if (!playerName) {
      return <PlayerSelecter />;
    }

    const { parser } = this.state;
    return (
      <React.Fragment>
        <FightNavigation />
        <div style={{ marginLeft: 60 }}>
          {!parser && <ActivityIndicator text="Initializing analyzer..." />}
          {parser && (
            <Results
              parser={parser}
              finished={this.state.finished}
              makeTabUrl={tab => makeAnalyzerUrl(report, parser.fightId, parser.playerId, tab)}
            />
          )}
        </div>
      </React.Fragment>
    );
  }
}

const mapStateToProps = state => {
  const fightId = getFightId(state);

  return ({
    reportCode: getReportCode(state),
    fightId,
    playerName: getPlayerName(state),
    playerId: getPlayerId(state),

    report: getReport(state),
    fight: getFightById(state, fightId),
    combatants: getCombatants(state),

    articleId: getArticleId(state),

    error: getError(state),
  });
};

export default connect(
  mapStateToProps,
  {
    fetchReport,
    setReportProgress,
    fetchCombatants,
    reportNotFoundError,
    apiDownError,
    unknownNetworkIssueError,
    unknownError,
    appendReportHistory,
  }
)(Report);
<commit_msg>Fix import<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';
import FightNavigationBar from './FightNavigationBar';

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 parserClass = await config.parser();
    const parser = this.createParser(parserClass, 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;

      timeAvailable && console.time('player event parsing');
      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;
      }
      timeAvailable && console.timeEnd('player event parsing');

      parser.fabricateEvent({
        type: 'finished',
      });
      timeAvailable && console.timeEnd('full parse');
      this.props.setReportProgress(PROGRESS_COMPLETE);
      this.setState({
        finished: true,
      });
    } catch (err) {
      captureException(err);
      if (process.env.NODE_ENV === 'development') {
        // Something went wrong during the analysis of the log, there's probably an issue in your analyzer or one of its modules.
        throw err;
      } else {
        alert(`The report could not be parsed because an error occured while running the analysis. ${err.message}`);
      }
    }
  }
  _isFakeNetworking = false;
  async startFakeNetworkProgress() {
    this._isFakeNetworking = true;
    const expectedDuration = 5000;
    const stepInterval = 50;

    const jobId = this._jobId;

    let step = 1;
    while (this._isFakeNetworking) {
      if (this._jobId !== jobId) {
        // This could happen when switching players/fights while still loading another one
        break;
      }
      const progress = Math.min(1, step * stepInterval / expectedDuration);
      this.props.setReportProgress(PROGRESS_STEP1_INITIALIZATION + ((PROGRESS_STEP2_FETCH_EVENTS - PROGRESS_STEP1_INITIALIZATION) * progress));
      await this.timeout(stepInterval);
      step += 1;
    }
  }
  stopFakeNetworkProgress() {
    this._isFakeNetworking = false;
  }
  timeout(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  async fetchCombatantsForFight(report, fight) {
    try {
      await this.props.fetchCombatants(report.code, fight.start_time, fight.end_time);
    } catch (err) {
      // TODO: Redirect to homepage
      this.reset();
      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);
      }
    }
  }

  reset() {
    this._jobId += 1;
    this.setState({
      config: null,
      parser: null,
      finished: false,
    });
    this.props.setReportProgress(null);
    this.stopFakeNetworkProgress();
  }

  componentWillMount() {
    this.fetchReportIfNecessary({});
  }
  componentDidUpdate(prevProps, prevState) {
    ReactTooltip.rebuild();

    this.fetchReportIfNecessary(prevProps);
    this.fetchCombatantsIfNecessary(prevProps, prevState);
    this.fetchEventsAndParseIfNecessary(prevProps, prevState);
  }
  fetchReportIfNecessary(prevProps) {
    if (this.props.reportCode && this.props.reportCode !== prevProps.reportCode) {
      this.props.fetchReport(this.props.reportCode)
        .catch(err => {
          this.reset();
          if (err instanceof LogNotFoundError) {
            this.props.reportNotFoundError();
          } else if (err instanceof ApiDownError) {
            this.props.apiDownError();
          } else if (err instanceof CorruptResponseError) {
            captureException(err);
            this.props.unknownError('Corrupt Warcraft Logs API response received, this report can not be processed.');
          } 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);
          }
        });
    }
  }
  fetchCombatantsIfNecessary(prevProps, prevState) {
    if (this.isReportValid && this.props.fight && (this.props.report !== prevProps.report || this.props.fight !== prevProps.fight)) {
      // A report has been loaded, it is the report the user wants (this can be a mismatch if a new report is still loading), a fight was selected, and one of the fight-relevant things was changed
      this.fetchCombatantsForFight(this.props.report, this.props.fight);
    }
  }
  fetchEventsAndParseIfNecessary(prevProps, prevState) {
    const changed = this.props.report !== prevProps.report
      || this.props.combatants !== prevProps.combatants
      || this.props.fightId !== prevProps.fightId
      || this.props.playerName !== prevProps.playerName
      || this.props.playerId !== prevProps.playerId;
    if (changed) {
      this.reset();

      const report = this.props.report;
      const fight = this.props.fight;
      const combatants = this.props.combatants;
      const playerId = this.props.playerId;
      const playerName = this.props.playerName;
      const valid = report && fight && combatants && (playerName || playerId);
      if (valid) {
        const player = this.getPlayerFromReport(report, playerId, playerName);
        if (!player) {
          alert(`Unknown player: ${playerName}`);
          return;
        }
        const combatant = combatants.find(combatant => combatant.sourceID === player.id);
        if (!combatant) {
          alert('This player does not seem to be in this fight.');
          return;
        }
        this.fetchEventsAndParse(report, fight, combatants, combatant, player);
        this.appendHistory(report, fight, player);
      }
    }
  }
  getPlayerFromReport(report, playerId, playerName) {
    if (playerId) {
      return report.friendlies.find(friendly => friendly.id === playerId);
    }
    const fetchByNameAttempt = report.friendlies.find(friendly => friendly.name === playerName);
    if (!fetchByNameAttempt) {
      return report.friendlies.find(friendly => friendly.id === Number(playerName));
    }
    return fetchByNameAttempt;
  }
  getPlayerPetsFromReport(report, playerId) {
    return report.friendlyPets.filter(pet => pet.petOwner === playerId);
  }
  appendHistory(report, fight, player) {
    this.props.appendReportHistory({
      code: report.code,
      title: report.title,
      start: Math.floor(report.start / 1000),
      end: Math.floor(report.end / 1000),
      fightId: fight.id,
      fightName: getFightName(report, fight),
      playerId: player.id,
      playerName: player.name,
      playerClass: player.type,
    });
  }

  render() {
    const { report, fightId, playerName } = this.props;

    if (!report) {
      return <ActivityIndicator text="Pulling report info..." />;
    }
    if (!fightId) {
      return <FightSelecter />;
    }
    if (!playerName) {
      return <PlayerSelecter />;
    }

    const { parser } = this.state;
    return (
      <React.Fragment>
        <FightNavigationBar />
        <div style={{ marginLeft: 60 }}>
          {!parser && <ActivityIndicator text="Initializing analyzer..." />}
          {parser && (
            <Results
              parser={parser}
              finished={this.state.finished}
              makeTabUrl={tab => makeAnalyzerUrl(report, parser.fightId, parser.playerId, tab)}
            />
          )}
        </div>
      </React.Fragment>
    );
  }
}

const mapStateToProps = state => {
  const fightId = getFightId(state);

  return ({
    reportCode: getReportCode(state),
    fightId,
    playerName: getPlayerName(state),
    playerId: getPlayerId(state),

    report: getReport(state),
    fight: getFightById(state, fightId),
    combatants: getCombatants(state),

    articleId: getArticleId(state),

    error: getError(state),
  });
};

export default connect(
  mapStateToProps,
  {
    fetchReport,
    setReportProgress,
    fetchCombatants,
    reportNotFoundError,
    apiDownError,
    unknownNetworkIssueError,
    unknownError,
    appendReportHistory,
  }
)(Report);
<|endoftext|>


<commit_before>import { deepEqual } from 'fast-equals'
import { settings } from './settings.js'
import { Evented } from './Evented.js'
import { getFirebase, getDatabase } from './defaultFirebase.js'
import { Channel } from './Channel.js'

import { getPeerList as _getPeerList } from './peerDatabaseUtils.js'
import { firebaseTreeTrimmer } from './firebaseTreeTrimmer.js'

export function P2PServerFactory(options) {
    const { PeerBinary, debug } = options

    return class P2PServer extends Evented {
        constructor(options = {}, initialPeerInfo = {}) {
            super() //no idea what this does
            console.assert(
                options.iceServers,
                'Server: no ice servers yet. Using defaults'
            )
            this.MAX_CONNECTIONS = 50
            this.isListening = false

            this.id = 'server_' + Math.floor(Math.random() * 100000)
            this.myID = this.id
            this.peerID = this.id

            this.stream = undefined
            this.iceServers =
                options.iceServers ||
                options.ICE_SERVERS ||
                settings.ICE_SERVERS
            this.POLLING_FREQUENCY =
                options.POLLING_FREQUENCY || settings.POLLING_FREQUENCY

            Object.assign(this, options)

            if (options.database) {
                this.database = options.database
            } else {
                this.database = getDatabase()
            }

            this.debug = !!debug || !!options.debug
            this.initialPeerInfo = initialPeerInfo
            this.initialPeerInfo.id = this.id

            if (this.debug) console.log(this.id)
            if (!options.dontCallInitYet) {
                this.init()
            }

            this._peerInfo = null
        }

        init() {
            var fbref = this.database

            // the below assumes that tree trimming would happen at the same lavel as the peers ref or would be passed explicitly
            this.treeTrimmer = new firebaseTreeTrimmer({
                peersRef: this.database,
                treeTrimmingRef:
                    this.treeTrimmingRef ||
                    this.database.parent.child('treeTrimming'),
                id: this.id,
            })

            this.userRef = fbref.child(this.id)

            this.userRef.on('value', (snapshot) => {
                // handle being tree trimmed while asleep
                let newPeerInfo = snapshot.val()
                if (
                    newPeerInfo &&
                    newPeerInfo.id &&
                    !deepEqual(
                        { ...this._peerInfo, lastUpdate: null },
                        { ...newPeerInfo, lastUpdate: null }
                    )
                ) {
                    console.log('got new user info: ', newPeerInfo)
                    this._peerInfo = newPeerInfo
                } else if (
                    this._peerInfo &&
                    this._peerInfo.id &&
                    !(newPeerInfo && newPeerInfo.id)
                ) {
                    console.log(
                        'peerInfo lost, updating with saved version: ',
                        this._peerInfo,
                        newPeerInfo
                    )
                    this.userRef.update({
                        ...this._peerInfo,
                        lastUpdate: getFirebase().database.ServerValue
                            .TIMESTAMP,
                    })
                } else if (this._peerInfo) {
                    //console.log('no update needed')
                } else {
                    console.warn(
                        'Appears we have not yet set peerInfo: ',
                        this._peerInfo,
                        newPeerInfo
                    )
                }
            })

            this.userRef.onDisconnect().remove()

            if (this.initialPeerInfo) this.userRef.update(this.initialPeerInfo)

            this.updateRef = this.userRef.child('lastUpdate')
            this.updateRef.set(getFirebase().database.ServerValue.TIMESTAMP)

            this.channelRef = this.userRef.child('channels')
            if (this.stream) {
                this.userRef.child('isStream').set(true)
            }
            this.channelRef.set([])

            this.connections = []
            this._intervalID = setInterval(() => {
                this.fire('updateTimeStamp', undefined)
                this._updateOnFireBase()
            }, this.POLLING_FREQUENCY)
            this.listenToChannels()
            this.isListening = true
            this.fire('init', undefined)
        }

        _updateOnFireBase() {
            // one may want to overwrite this
            this.updateRef.set(getFirebase().database.ServerValue.TIMESTAMP)
        }

        sendToAll(data) {
            for (var conx of this.connections) {
                if (conx && conx.peer) {
                    try {
                        conx.peer.send.bind(conx.peer)(data)
                    } catch (err) {
                        console.error(
                            err,

                            'Got an error, interrupted connection? '
                        )
                    }
                }
            }
        }

        sendToAllBig(data) {
            for (var conx of this.connections) {
                if (conx && conx.peer) {
                    try {
                        conx.peer.sendBig.bind(conx.peer)(data)
                    } catch (err) {
                        console.error(
                            err,

                            'Got an error, interrupted connection? '
                        )
                    }
                }
            }
        }

        listenToChannels() {
            // disabling no-loop-func because these loops are correct usage
            // https://eslint.org/docs/rules/no-loop-func
            // when a new channel is added, listen to it.
            this.channelRef.on('child_added', (ev) => {
                if (this.connections.length > this.MAX_CONNECTIONS) {
                    console.error(
                        'Too many connections. TODO:close/remove old stale connections'
                    )
                    return
                }
                var val = ev.val()
                if (this.debug) {
                    console.log(val, 'new child')
                }
                for (var i in val.fromClient) {
                    var sig = val.fromClient[i]
                    if (this.debug) console.log({ sig })
                    if (sig.type === 'offer') {
                        var mykey = ev.key
                        var { peerID, myID } = sig
                        var channel = new Channel(
                            this.channelRef.child(mykey),
                            this._makePeer(myID)
                        )
                        this.connections = [...this.connections, channel]
                        this.fire('addConnection', channel)

                        // on message through webRTC (simple-peer)
                        //eslint-disable-next-line no-loop-func
                        var answerSentYet = false
                        channel.peer.on('signal', (data) => {
                            if (data.type === 'answer') {
                                if (answerSentYet) {
                                    console.warn(
                                        'Why am i trying to send multiple answers'
                                    )
                                }
                                channel.outRef.push(data)
                                answerSentYet = true
                            } else if (data.candidate) {
                                channel.outRef.push(data)
                            } else {
                                console.warn(
                                    data,

                                    'unexpected message from WebRTC'
                                )
                            }
                        })

                        // on message through firebase
                        //eslint-disable-next-line no-loop-func
                        channel.inRef.on('child_added', (ev2) => {
                            var val2 = ev2.val()
                            if (this.debug) {
                                console.log(val2, 'child_added -- firebase')
                            }
                            if (val2.candidate) {
                                if (this.debug) {
                                    console.log(
                                        val2,
                                        'server got candidate from firebase'
                                    )
                                }
                                channel.peer.signal(val2)
                            } else if (val2.type === 'offer') {
                                channel.peer.signal(val2)
                            } else if (val2.type === 'answer') {
                                //ignore this. It was probably from me.
                            } else {
                                console.warn(
                                    val2,
                                    'unexpected message from Firebase'
                                )
                            }
                        })
                    }
                }
            })
        }

        _makePeer(peerID) {
            if (this.debug)
                console.log('_makePeer called with peerID: ', peerID)
            this.fire('makePeer', undefined)
            var myoptions = {
                initiator: false,
                trickle: true,
                config: {
                    iceServers: this.iceServers,
                },
                peerID,
            }
            if (this.stream) myoptions.stream = this.stream
            var p = new PeerBinary(myoptions)

            // fire events
            p.on('error', (err) => {
                console.error('server: error', err)
                this.fire('error', { peer: p, err: err })
            })
            p.on('connect', () => {
                if (this.debug) console.log('server: client connected')
                this.fire('connect', { peer: p })
            })
            p.on('data', (data) => {
                if (this.debug)
                    console.log('server: server recieved some data: ', data)
                this.fire('data', { peer: p, data: data })
            })
            p.on('close', () => {
                if (this.debug) console.log('server: connection closed', p)
                this._removeConnection(p)
                this.fire('close', { peer: p })
            })
            p.on('dataBig', (data) => {
                this.fire('dataBig', { peer: p, data: data })
            })
            p.on('stream', (stream) => {
                if (this.debug)
                    console.log('Server: connected to stream', stream)
                this.fire('stream', { peer: p, stream: stream })
            })
            p.on('signal', (data) => {
                if (this.debug) console.log('Server: received signal', data)
                this.fire('signal', data)
            })

            //TODO make it so server can register events that will get called on each individual connection
            return p
        }

        getPeerList(callback) {
            return _getPeerList(this.database, callback)
        }

        destroy() {
            this.channelRef.remove()
            this.updateRef.remove()
            this.channelRef.off()
            this.updateRef.off()
            this.userRef.off()
            this.isListening = false
            for (var x of this.connections) {
                x.destroy()
            }
            this.fire('destroyed', {})
            this.connections = []
            clearInterval(this._intervalID)
        }

        _removeConnection(peer) {
            var index = -1
            for (var i = 0; i < this.connections.length; i++) {
                var conn = this.connections[i]
                if (conn.peer == peer) {
                    if (this.debug) console.log('found my connection', i, conn)
                    index = i
                }
            }
            if (index >= 0) {
                var conn = this.connections[index]
                conn.destroy()
                this.connections.splice(index, 1)
                this.connections = [...this.connections]
                this.fire('removeConnection', conn)
                if (this.debug) console.log(this.connections)
            }
        }
    }
}
<commit_msg>respond to ack<commit_after>import { deepEqual } from 'fast-equals'
import { settings } from './settings.js'
import { Evented } from './Evented.js'
import { getFirebase, getDatabase } from './defaultFirebase.js'
import { Channel } from './Channel.js'

import { getPeerList as _getPeerList } from './peerDatabaseUtils.js'
import { firebaseTreeTrimmer } from './firebaseTreeTrimmer.js'

export function P2PServerFactory(options) {
    const { PeerBinary, debug } = options

    return class P2PServer extends Evented {
        constructor(options = {}, initialPeerInfo = {}) {
            super() //no idea what this does
            console.assert(
                options.iceServers,
                'Server: no ice servers yet. Using defaults'
            )
            this.MAX_CONNECTIONS = 50
            this.isListening = false

            this.id = 'server_' + Math.floor(Math.random() * 100000)
            this.myID = this.id
            this.peerID = this.id

            this.stream = undefined
            this.iceServers =
                options.iceServers ||
                options.ICE_SERVERS ||
                settings.ICE_SERVERS
            this.POLLING_FREQUENCY =
                options.POLLING_FREQUENCY || settings.POLLING_FREQUENCY

            Object.assign(this, options)

            if (options.database) {
                this.database = options.database
            } else {
                this.database = getDatabase()
            }

            this.debug = !!debug || !!options.debug
            this.initialPeerInfo = initialPeerInfo
            this.initialPeerInfo.id = this.id

            if (this.debug) console.log(this.id)
            if (!options.dontCallInitYet) {
                this.init()
            }

            this._peerInfo = null
        }

        init() {
            var fbref = this.database

            // the below assumes that tree trimming would happen at the same lavel as the peers ref or would be passed explicitly
            this.treeTrimmer = new firebaseTreeTrimmer({
                peersRef: this.database,
                treeTrimmingRef:
                    this.treeTrimmingRef ||
                    this.database.parent.child('treeTrimming'),
                id: this.id,
            })

            this.userRef = fbref.child(this.id)

            this.userRef.on('value', (snapshot) => {
                // handle being tree trimmed while asleep
                let newPeerInfo = snapshot.val()
                if (
                    newPeerInfo &&
                    newPeerInfo.id &&
                    !deepEqual(
                        { ...this._peerInfo, lastUpdate: null },
                        { ...newPeerInfo, lastUpdate: null }
                    )
                ) {
                    console.log('got new user info: ', newPeerInfo)
                    this._peerInfo = newPeerInfo
                } else if (
                    this._peerInfo &&
                    this._peerInfo.id &&
                    !(newPeerInfo && newPeerInfo.id)
                ) {
                    console.log(
                        'peerInfo lost, updating with saved version: ',
                        this._peerInfo,
                        newPeerInfo
                    )
                    this.userRef.update({
                        ...this._peerInfo,
                        lastUpdate: getFirebase().database.ServerValue
                            .TIMESTAMP,
                    })
                } else if (this._peerInfo) {
                    //console.log('no update needed')
                } else {
                    console.warn(
                        'Appears we have not yet set peerInfo: ',
                        this._peerInfo,
                        newPeerInfo
                    )
                }
            })

            this.userRef.onDisconnect().remove()

            if (this.initialPeerInfo) this.userRef.update(this.initialPeerInfo)

            this.updateRef = this.userRef.child('lastUpdate')
            this.updateRef.set(getFirebase().database.ServerValue.TIMESTAMP)

            this.channelRef = this.userRef.child('channels')
            if (this.stream) {
                this.userRef.child('isStream').set(true)
            }
            this.channelRef.set([])

            this.connections = []
            this._intervalID = setInterval(() => {
                this.fire('updateTimeStamp', undefined)
                this._updateOnFireBase()
            }, this.POLLING_FREQUENCY)
            this.listenToChannels()
            this.isListening = true
            this.fire('init', undefined)
        }

        _updateOnFireBase() {
            // one may want to overwrite this
            this.updateRef.set(getFirebase().database.ServerValue.TIMESTAMP)
        }

        sendToAll(data) {
            for (var conx of this.connections) {
                if (conx && conx.peer) {
                    try {
                        conx.peer.send.bind(conx.peer)(data)
                    } catch (err) {
                        console.error(
                            err,

                            'Got an error, interrupted connection? '
                        )
                    }
                }
            }
        }

        sendToAllBig(data) {
            for (var conx of this.connections) {
                if (conx && conx.peer) {
                    try {
                        conx.peer.sendBig.bind(conx.peer)(data)
                    } catch (err) {
                        console.error(
                            err,

                            'Got an error, interrupted connection? '
                        )
                    }
                }
            }
        }

        listenToChannels() {
            // disabling no-loop-func because these loops are correct usage
            // https://eslint.org/docs/rules/no-loop-func
            // when a new channel is added, listen to it.
            this.channelRef.on('child_added', (ev) => {
                if (this.connections.length > this.MAX_CONNECTIONS) {
                    console.error(
                        'Too many connections. TODO:close/remove old stale connections'
                    )
                    return
                }
                var val = ev.val()
                if (this.debug) {
                    console.log(val, 'new child')
                }
                for (var i in val.fromClient) {
                    var sig = val.fromClient[i]
                    if (this.debug) console.log({ sig })
                    if (sig.type === 'offer') {
                        var mykey = ev.key
                        var { peerID, myID } = sig
                        var channel = new Channel(
                            this.channelRef.child(mykey),
                            this._makePeer(myID)
                        )
                        this.connections = [...this.connections, channel]
                        this.fire('addConnection', channel)

                        // on message through webRTC (simple-peer)
                        //eslint-disable-next-line no-loop-func
                        var answerSentYet = false
                        channel.peer.on('signal', (data) => {
                            if (data.type === 'answer') {
                                if (answerSentYet) {
                                    console.warn(
                                        'Why am i trying to send multiple answers'
                                    )
                                }
                                channel.outRef.push(data)
                                answerSentYet = true
                            } else if (data.candidate) {
                                channel.outRef.push(data)
                            } else {
                                console.warn(
                                    data,

                                    'unexpected message from WebRTC'
                                )
                            }
                        })

                        // on message through firebase
                        //eslint-disable-next-line no-loop-func
                        channel.inRef.on('child_added', (ev2) => {
                            var val2 = ev2.val()
                            if (this.debug) {
                                console.log(val2, 'child_added -- firebase')
                            }
                            if (val2.candidate) {
                                if (this.debug) {
                                    console.log(
                                        val2,
                                        'server got candidate from firebase'
                                    )
                                }
                                channel.peer.signal(val2)
                            } else if (val2.type === 'offer') {
                                channel.peer.signal(val2)
                            } else if (val2.type === 'answer') {
                                //ignore this. It was probably from me.
                            } else {
                                console.warn(
                                    val2,
                                    'unexpected message from Firebase'
                                )
                            }
                        })
                    }
                }
            })
        }

        _makePeer(peerID) {
            if (this.debug)
                console.log('_makePeer called with peerID: ', peerID)
            this.fire('makePeer', undefined)
            var myoptions = {
                initiator: false,
                trickle: true,
                config: {
                    iceServers: this.iceServers,
                },
                peerID,
            }
            if (this.stream) myoptions.stream = this.stream
            var p = new PeerBinary(myoptions)

            // fire events
            p.on('error', (err) => {
                console.error('server: error', err)
                this.fire('error', { peer: p, err: err })
            })
            p.on('connect', () => {
                if (this.debug) console.log('server: client connected')
                this.fire('connect', { peer: p })
            })
            p.on('data', (data) => {
                if (this.debug)
                    console.log('server: server recieved some data: ', data)
                this.fire('data', { peer: p, data: data })
            })
            p.on('close', () => {
                if (this.debug) console.log('server: connection closed', p)
                this._removeConnection(p)
                this.fire('close', { peer: p })
            })
            p.on('dataBig', (data) => {
                if (data && data.type === 'ack')
                    p.sendBig({
                        type: 'ackack',
                        data: { id: this.id, date: new Date.getTime() },
                    })
                this.fire('dataBig', { peer: p, data: data })
            })
            p.on('stream', (stream) => {
                if (this.debug)
                    console.log('Server: connected to stream', stream)
                this.fire('stream', { peer: p, stream: stream })
            })
            p.on('signal', (data) => {
                if (this.debug) console.log('Server: received signal', data)
                this.fire('signal', data)
            })

            //TODO make it so server can register events that will get called on each individual connection
            return p
        }

        getPeerList(callback) {
            return _getPeerList(this.database, callback)
        }

        destroy() {
            this.channelRef.remove()
            this.updateRef.remove()
            this.channelRef.off()
            this.updateRef.off()
            this.userRef.off()
            this.isListening = false
            for (var x of this.connections) {
                x.destroy()
            }
            this.fire('destroyed', {})
            this.connections = []
            clearInterval(this._intervalID)
        }

        _removeConnection(peer) {
            var index = -1
            for (var i = 0; i < this.connections.length; i++) {
                var conn = this.connections[i]
                if (conn.peer == peer) {
                    if (this.debug) console.log('found my connection', i, conn)
                    index = i
                }
            }
            if (index >= 0) {
                var conn = this.connections[index]
                conn.destroy()
                this.connections.splice(index, 1)
                this.connections = [...this.connections]
                this.fire('removeConnection', conn)
                if (this.debug) console.log(this.connections)
            }
        }
    }
}
<|endoftext|>


<commit_before>/* @flow */

import type { ChildProcess } from 'child_process';
import uuid from 'uuid/v4';
import tmp from 'tmp';
import getport from 'get-port';
import Debug from 'debug';
import MongoInstance from './util/MongoInstance';

tmp.setGracefulCleanup();

export type MongoMemoryServerOptsT = {
  instance: {
    port?: ?number,
    dbPath?: string,
    dbName?: string,
    storageEngine?: string,
    debug?: boolean | Function,
  },
  binary: {
    version?: string,
    downloadDir?: string,
    platform?: string,
    arch?: string,
    http?: any,
    debug?: boolean | Function,
  },
  debug?: boolean,
  spawn: any,
  autoStart?: boolean,
};

export type MongoInstanceDataT = {
  port: number,
  dbPath: string,
  dbName: string,
  uri: string,
  storageEngine: string,
  instance: MongoInstance,
  childProcess: ChildProcess,
  tmpDir?: {
    name: string,
    removeCallback: Function,
  },
};

async function generateDbName(dbName?: string): Promise<string> {
  return dbName || uuid();
}

async function generateConnectionString(port: number, dbName: string): Promise<string> {
  return `mongodb://localhost:${port}/${dbName}`;
}

export default class MongoMemoryServer {
  isRunning: boolean = false;
  runningInstance: ?Promise<MongoInstanceDataT>;
  opts: MongoMemoryServerOptsT;
  debug: Function;

  constructor(opts?: $Shape<MongoMemoryServerOptsT> = {}) {
    this.opts = opts;
    if (!this.opts.instance) this.opts.instance = {};
    if (!this.opts.binary) this.opts.binary = {};

    this.debug = (msg: string) => {
      if (this.opts.debug) {
        console.log(msg);
      }
    };

    // autoStart by default
    if (!opts.hasOwnProperty('autoStart') || opts.autoStart) {
      this.debug('Autostarting MongoDB instance...');
      this.start();
    }
  }

  async start(): Promise<boolean> {
    if (this.runningInstance) {
      throw new Error(
        'MongoDB instance already in status startup/running/error. Use opts.debug = true for more info.'
      );
    }

    this.runningInstance = this._startUpInstance()
      .catch(err => {
        if (err.message === 'Mongod shutting down' || err === 'Mongod shutting down') {
          this.debug(`Mongodb does not started. Trying to start on another port one more time...`);
          this.opts.instance.port = null;
          return this._startUpInstance();
        }
        throw err;
      })
      .catch(err => {
        if (!this.opts.debug) {
          throw new Error(
            `${err.message}\n\nUse debug option for more info: new MongoMemoryServer({ debug: true })`
          );
        }
        throw err;
      });

    return this.runningInstance.then(() => true);
  }

  async _startUpInstance(): Promise<MongoInstanceDataT> {
    const data = {};
    let tmpDir;

    const instOpts = this.opts.instance;
    data.port = await getport(instOpts.port);
    this.debug = Debug(`Mongo[${data.port}]`);
    this.debug.enabled = !!this.opts.debug;
    data.dbName = await generateDbName(instOpts.dbName);
    data.uri = await generateConnectionString(data.port, data.dbName);
    data.storageEngine = instOpts.storageEngine || 'ephemeralForTest';
    if (instOpts.dbPath) {
      data.dbPath = instOpts.dbPath;
    } else {
      tmpDir = tmp.dirSync({ prefix: 'mongo-mem-', unsafeCleanup: true });
      data.dbPath = tmpDir.name;
    }

    this.debug(`Starting MongoDB instance with following options: ${JSON.stringify(data)}`);

    // Download if not exists mongo binaries in ~/.mongodb-prebuilt
    // After that startup MongoDB instance
    const instance = await MongoInstance.run({
      instance: {
        port: data.port,
        storageEngine: data.storageEngine,
        dbPath: data.dbPath,
        debug: this.opts.instance.debug,
      },
      binary: this.opts.binary,
      spawn: this.opts.spawn,
      debug: this.debug,
    });
    data.instance = instance;
    data.childProcess = instance.childProcess;
    data.tmpDir = tmpDir;

    return data;
  }

  async stop(): Promise<boolean> {
    const { childProcess, port, tmpDir } = (await this.getInstanceData(): MongoInstanceDataT);

    if (childProcess && childProcess.kill) {
      this.debug(`Shutdown MongoDB server on port ${port} with pid ${childProcess.pid}`);
      await new Promise(resolve => {
        childProcess.once(`exit`, resolve);
        childProcess.kill();
      });
    }

    if (tmpDir) {
      this.debug(`Removing tmpDir ${tmpDir.name}`);
      tmpDir.removeCallback();
    }

    this.runningInstance = null;
    return true;
  }

  async getInstanceData(): Promise<MongoInstanceDataT> {
    if (this.runningInstance) {
      return this.runningInstance;
    }
    throw new Error(
      'Database instance is not running. You should start database by calling start() method. BTW it should start automatically if opts.autoStart!=false. Also you may provide opts.debug=true for more info.'
    );
  }

  async getUri(otherDbName?: string | boolean = false): Promise<string> {
    const { uri, port } = (await this.getInstanceData(): MongoInstanceDataT);

    // IF true OR string
    if (otherDbName) {
      if (typeof otherDbName === 'string') {
        // generate uri with provided DB name on existed DB instance
        return generateConnectionString(port, otherDbName);
      }
      // generate new random db name
      return generateConnectionString(port, await generateDbName());
    }

    return uri;
  }

  async getConnectionString(otherDbName: string | boolean = false): Promise<string> {
    return this.getUri(otherDbName);
  }

  async getPort(): Promise<number> {
    const { port } = (await this.getInstanceData(): MongoInstanceDataT);
    return port;
  }

  async getDbPath(): Promise<string> {
    const { dbPath } = (await this.getInstanceData(): MongoInstanceDataT);
    return dbPath;
  }
}
<commit_msg>fix(MemoryServer): use instance.kill() instead of childProcess.kill()<commit_after>/* @flow */

import type { ChildProcess } from 'child_process';
import uuid from 'uuid/v4';
import tmp from 'tmp';
import getport from 'get-port';
import Debug from 'debug';
import MongoInstance from './util/MongoInstance';

tmp.setGracefulCleanup();

export type MongoMemoryServerOptsT = {
  instance: {
    port?: ?number,
    dbPath?: string,
    dbName?: string,
    storageEngine?: string,
    debug?: boolean | Function,
  },
  binary: {
    version?: string,
    downloadDir?: string,
    platform?: string,
    arch?: string,
    http?: any,
    debug?: boolean | Function,
  },
  debug?: boolean,
  spawn: any,
  autoStart?: boolean,
};

export type MongoInstanceDataT = {
  port: number,
  dbPath: string,
  dbName: string,
  uri: string,
  storageEngine: string,
  instance: MongoInstance,
  childProcess: ChildProcess,
  tmpDir?: {
    name: string,
    removeCallback: Function,
  },
};

async function generateDbName(dbName?: string): Promise<string> {
  return dbName || uuid();
}

async function generateConnectionString(port: number, dbName: string): Promise<string> {
  return `mongodb://localhost:${port}/${dbName}`;
}

export default class MongoMemoryServer {
  isRunning: boolean = false;
  runningInstance: ?Promise<MongoInstanceDataT>;
  opts: MongoMemoryServerOptsT;
  debug: Function;

  constructor(opts?: $Shape<MongoMemoryServerOptsT> = {}) {
    this.opts = opts;
    if (!this.opts.instance) this.opts.instance = {};
    if (!this.opts.binary) this.opts.binary = {};

    this.debug = (msg: string) => {
      if (this.opts.debug) {
        console.log(msg);
      }
    };

    // autoStart by default
    if (!opts.hasOwnProperty('autoStart') || opts.autoStart) {
      this.debug('Autostarting MongoDB instance...');
      this.start();
    }
  }

  async start(): Promise<boolean> {
    if (this.runningInstance) {
      throw new Error(
        'MongoDB instance already in status startup/running/error. Use opts.debug = true for more info.'
      );
    }

    this.runningInstance = this._startUpInstance()
      .catch(err => {
        if (err.message === 'Mongod shutting down' || err === 'Mongod shutting down') {
          this.debug(`Mongodb does not started. Trying to start on another port one more time...`);
          this.opts.instance.port = null;
          return this._startUpInstance();
        }
        throw err;
      })
      .catch(err => {
        if (!this.opts.debug) {
          throw new Error(
            `${err.message}\n\nUse debug option for more info: new MongoMemoryServer({ debug: true })`
          );
        }
        throw err;
      });

    return this.runningInstance.then(() => true);
  }

  async _startUpInstance(): Promise<MongoInstanceDataT> {
    const data = {};
    let tmpDir;

    const instOpts = this.opts.instance;
    data.port = await getport(instOpts.port);
    this.debug = Debug(`Mongo[${data.port}]`);
    this.debug.enabled = !!this.opts.debug;
    data.dbName = await generateDbName(instOpts.dbName);
    data.uri = await generateConnectionString(data.port, data.dbName);
    data.storageEngine = instOpts.storageEngine || 'ephemeralForTest';
    if (instOpts.dbPath) {
      data.dbPath = instOpts.dbPath;
    } else {
      tmpDir = tmp.dirSync({ prefix: 'mongo-mem-', unsafeCleanup: true });
      data.dbPath = tmpDir.name;
    }

    this.debug(`Starting MongoDB instance with following options: ${JSON.stringify(data)}`);

    // Download if not exists mongo binaries in ~/.mongodb-prebuilt
    // After that startup MongoDB instance
    const instance = await MongoInstance.run({
      instance: {
        port: data.port,
        storageEngine: data.storageEngine,
        dbPath: data.dbPath,
        debug: this.opts.instance.debug,
      },
      binary: this.opts.binary,
      spawn: this.opts.spawn,
      debug: this.debug,
    });
    data.instance = instance;
    data.childProcess = instance.childProcess;
    data.tmpDir = tmpDir;

    return data;
  }

  async stop(): Promise<boolean> {
    const { instance, port, tmpDir } = (await this.getInstanceData(): MongoInstanceDataT);

    this.debug(`Shutdown MongoDB server on port ${port} with pid ${instance.getPid() || ''}`);
    await instance.kill();

    if (tmpDir) {
      this.debug(`Removing tmpDir ${tmpDir.name}`);
      tmpDir.removeCallback();
    }

    this.runningInstance = null;
    return true;
  }

  async getInstanceData(): Promise<MongoInstanceDataT> {
    if (this.runningInstance) {
      return this.runningInstance;
    }
    throw new Error(
      'Database instance is not running. You should start database by calling start() method. BTW it should start automatically if opts.autoStart!=false. Also you may provide opts.debug=true for more info.'
    );
  }

  async getUri(otherDbName?: string | boolean = false): Promise<string> {
    const { uri, port } = (await this.getInstanceData(): MongoInstanceDataT);

    // IF true OR string
    if (otherDbName) {
      if (typeof otherDbName === 'string') {
        // generate uri with provided DB name on existed DB instance
        return generateConnectionString(port, otherDbName);
      }
      // generate new random db name
      return generateConnectionString(port, await generateDbName());
    }

    return uri;
  }

  async getConnectionString(otherDbName: string | boolean = false): Promise<string> {
    return this.getUri(otherDbName);
  }

  async getPort(): Promise<number> {
    const { port } = (await this.getInstanceData(): MongoInstanceDataT);
    return port;
  }

  async getDbPath(): Promise<string> {
    const { dbPath } = (await this.getInstanceData(): MongoInstanceDataT);
    return dbPath;
  }
}
<|endoftext|>


<commit_before>/*
	Copyright (C) 2018 Claude SIMON (http://q37.info/contact/).

	This file is part of XDHq.

	XDHq is free software: you can redistribute it and/or
	modify it under the terms of the GNU Affero General Public License as
	published by the Free Software Foundation, either version 3 of the
	License, or (at your option) any later version.

	XDHq is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
	Affero General Public License for more details.

	You should have received a copy of the GNU Affero General Public License
	along with XDHq. If not, see <http://www.gnu.org/licenses/>.
*/

const { app, BrowserWindow } = require('electron');
const path = require('path');
const url = require('url');
const fs = require('fs');

const args = process.argv;

// console.log( args );

function help() {
  process.stdout.write(". -m=h:/bin/xdhqxdh <dir>");
  process.exit(-1);
}

if (args.length < 4)
  help();
else if (args[3][0] == '-')
  help();

const cdnPath = path.posix.join(args[3], '/');

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;

function createWindow() {
  // Create the browser window.
  win = new BrowserWindow({ width: 800, height: 600 })

  // Open the DevTools.
  win.webContents.openDevTools();

  let headFilename = (path.join(cdnPath, "head.html"));
  let head = () => { if (fs.existsSync(headFilename)) return fs.readFileSync(headFilename); else return ""; };
  let html = fs.readFileSync(path.join(__dirname, "XDHELCq.html"), "utf8").replace("<!-- $USER_HEAD$ -->", head).replace(/\$XDHELCQ_PATH\$/g, __dirname.replace(/\\/g, "\\\\"));

  win.loadURL("data:text/html;charset=utf-8," + encodeURI(html), { baseURLForDataURL: "file://" + cdnPath });

  // Emitted when the window is closed.
  win.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    win = null
  })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
console.log( "Before !");
app.on('ready', createWindow)
console.log( "After !");

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (win === null) {
    createWindow()
  }
})

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
<commit_msg>09/07/2018 21:06:01<commit_after>/*
	Copyright (C) 2018 Claude SIMON (http://q37.info/contact/).

	This file is part of XDHq.

	XDHq is free software: you can redistribute it and/or
	modify it under the terms of the GNU Affero General Public License as
	published by the Free Software Foundation, either version 3 of the
	License, or (at your option) any later version.

	XDHq is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
	Affero General Public License for more details.

	You should have received a copy of the GNU Affero General Public License
	along with XDHq. If not, see <http://www.gnu.org/licenses/>.
*/

const { app, BrowserWindow } = require('electron');
const path = require('path');
const url = require('url');
const fs = require('fs');

const args = process.argv;

// console.log( args );

function help() {
  process.stdout.write(". -m=h:/bin/xdhqxdh <dir>");
  process.exit(-1);
}

if (args.length < 4)
  help();
else if (args[3][0] == '-')
  help();

const cdnPath = path.posix.join(args[3], '/');

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;

function createWindow() {
  // Create the browser window.
  win = new BrowserWindow({ width: 800, height: 600 })

  // Open the DevTools.
//  win.webContents.openDevTools();

  let headFilename = (path.join(cdnPath, "head.html"));
  let head = () => { if (fs.existsSync(headFilename)) return fs.readFileSync(headFilename); else return ""; };
  let html = fs.readFileSync(path.join(__dirname, "XDHELCq.html"), "utf8").replace("<!-- $USER_HEAD$ -->", head).replace(/\$XDHELCQ_PATH\$/g, __dirname.replace(/\\/g, "\\\\"));

  win.loadURL("data:text/html;charset=utf-8," + encodeURI(html), { baseURLForDataURL: "file://" + cdnPath });

  // Emitted when the window is closed.
  win.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    win = null
  })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
console.log( "Before !");
app.on('ready', createWindow)
console.log( "After !");

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (win === null) {
    createWindow()
  }
})

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import Spinner from './Spinner';

let colors = ['blue', 'red', 'yellow', 'green'];

class Preloader extends Component {
  render() {
    const { active, size, color, flashing, className } = this.props;

    let classes = cx('preloader-wrapper', { active: active, size: size });

    let spinners;
    if (flashing) {
      spinners = colors.map(color => (
        <Spinner color={color} only={false} key={color} />
      ));
    } else {
      spinners = <Spinner color={color} />;
    }
    return
{spinners}
;
  }
}

Preloader.propTypes = {
  className: PropTypes.string,
  /**
   * The scale of the circle
   * @default 'medium'
   */
  size: PropTypes.oneOf(['big', 'small', 'medium']),
  /**
   * Whether to spin
   * @default true
   */
  active: PropTypes.bool,
  /**
   * The color of the circle, if not flashing
   * @default 'blue'
   */
  color: PropTypes.oneOf(['blue', 'red', 'yellow', 'green']),
  /**
   * Wheter to circle four different colors
   * @default false
   */
  flashing: PropTypes.bool
};

Preloader.defaultProps = {
  active: true,
  flashing: false,
  color: 'blue'
};

export default Preloader;
<commit_msg>Preloader: convert to stateless<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';

import Spinner from './Spinner';

const colors = ['blue', 'red', 'yellow', 'green'];

const Preloader = ({ active, size, color, flashing, className }) => {
  let classes = cx('preloader-wrapper', { active, size });

  let spinners;
  if (flashing) {
    spinners = colors.map(color => (
      <Spinner color={color} only={false} key={color} />
    ));
  } else {
    spinners = <Spinner color={color} />;
  }
  return
{spinners}
;
};

Preloader.propTypes = {
  className: PropTypes.string,
  /**
   * The scale of the circle
   * @default 'medium'
   */
  size: PropTypes.oneOf(['big', 'small', 'medium']),
  /**
   * Whether to spin
   * @default true
   */
  active: PropTypes.bool,
  /**
   * The color of the circle, if not flashing
   * @default 'blue'
   */
  color: PropTypes.oneOf(['blue', 'red', 'yellow', 'green']),
  /**
   * Wheter to circle four different colors
   * @default false
   */
  flashing: PropTypes.bool
};

Preloader.defaultProps = {
  active: true,
  flashing: false,
  color: 'blue'
};

export default Preloader;
<|endoftext|>


<commit_before>'use strict';

var test = require('tape');

var u = require('../lib/helpers.js');

test('random_string', function (t) {
    t.plan(5);

    t.notEqual(
        u.random_string(8),
        u.random_string(8)
    );

    [1, 2, 3, 128].forEach(function (i) {
        t.equal(u.random_string(i).length, i);
    });
});

test('random_number_string', function (t) {
    t.plan(55);

    for (var i = 0; i <= 10; i++) {
        t.equal(u.random_number_string(10).length, 1);
        t.equal(u.random_number_string(100).length, 2);
        t.equal(u.random_number_string(1000).length, 3);
        t.equal(u.random_number_string(10000).length, 4);
        t.equal(u.random_number_string(100000).length, 5);
    }
});

test('amendUrl', function (t) {
    // NOTE: i stripped out tests that were depending on the DOM:
    t.plan(6);

    t.throws(function () {
        u.amendUrl('');
    }, 'Wrong url');

    t.throws(function () {
        u.amendUrl(false);
    },'Wrong url');

    t.throws(function () {
        u.amendUrl('http://abc?a=a');
    }, 'Only basic urls are supported');

    t.throws(function () {
        u.amendUrl('http://abc#a');
    }, 'Only basic urls are supported');

    t.equal(u.amendUrl('https://a:80/abc'), 'https://a:80/abc');

    t.equal(u.amendUrl('http://a:443/abc'), 'http://a:443/abc');

});

test('quote', function (t) {
    var all_chars = new Array(65535).map(function (_, i) {
        return String.fromCharCode(i);
    }).join('');

    t.plan(8);

    t.equal(u.quote(''), '""');

    t.equal(u.quote('a'), '"a"');

    t.ok(['"\\t"', '"\\u0009"'].indexOf(u.quote('\t')) !== -1);

    t.ok(['"\\n"', '"\\u000a"'].indexOf(u.quote('\n')) !== -1);

    t.equal(u.quote('\x00\udfff\ufffe\uffff'), '"\\u0000\\udfff\\ufffe\\uffff"');

    t.equal(u.quote('\ud85c\udff7\ud800\ud8ff'), '"\\ud85c\\udff7\\ud800\\ud8ff"');

    t.equal(u.quote('\u2000\u2001\u0300\u0301'), '"\\u2000\\u2001\\u0300\\u0301"');

    t.ok(JSON.parse(u.quote(all_chars)) === all_chars, 'Quote/unquote all 64K chars.');
});

test('detectProtocols', function (t) {
    var node_probed = {
        'websocket': true
    };
    t.plan(1);
    t.deepEqual(u.detectProtocols(node_probed, null, {}), ['websocket']);
 });

test('REventTarget', function (t) {
    var REventTarget = require('../lib/reventtarget.js');
    var bluff, handler0, handler1, handler2, handler3, handler4, log, r, single;
    t.plan(7);
    r = new REventTarget();
    r.addEventListener('message', function () {
        t.ok(true);
    });
    r.onmessage = function () {
        t.ok(false);
    };
    bluff = function () {
        t.ok(false);
    };
    r.addEventListener('message', bluff);
    r.removeEventListener('message', bluff);
    r.addEventListener('message', bluff);
    r.addEventListener('message', function () {
        t.ok(true);
    });
    r.onmessage = function () {
        t.ok(true);
    };
    r.removeEventListener('message', bluff);
    r.dispatchEvent({
        type: 'message'
    });
    handler0 = function () {
        log.push(0);
    };
    handler1 = function () {
        log.push(1);
        r.removeEventListener('test', handler0);
        r.removeEventListener('test', handler2);
        r.addEventListener('test', handler3);
        r.addEventListener('test', handler4);
    };
    handler2 = function () {
        log.push(2);
    };
    handler3 = function () {
        log.push(3);
        r.removeEventListener('test', handler1);
        r.removeEventListener('test', handler3);
        r.removeEventListener('test', handler4);
    };
    handler4 = function () {
        log.push(4);
    };
    r.addEventListener('test', handler0);
    r.addEventListener('test', handler1);
    r.addEventListener('test', handler2);
    log = [];
    r.dispatchEvent({
        type: 'test'
    });
    t.deepEqual(log, [0, 1, 2]);
    log = [];
    r.dispatchEvent({
        type: 'test'
    });
    t.deepEqual(log, [1, 3, 4]);
    log = [];
    r.dispatchEvent({
        type: 'test'
    });
    t.deepEqual(log, []);
    single = function () {
        t.ok(true);
    };
    r.addEventListener('close', single);
    r.addEventListener('close', single);
    r.dispatchEvent({
        type: 'close'
    });
    r.removeEventListener('close', single);
    r.dispatchEvent({
        type: 'close'
    });
    r.removeEventListener('unknown', function () {});
});

test('safe json parse', function (t) {
    var jsonParse = require('../lib/safe-json-parse.js');
    t.plan(2);
    t.deepEqual(jsonParse('invalid json'), null);
    t.deepEqual(jsonParse('{"valid":"json"}'), {"valid":"json"});
});
<commit_msg>s/deepEqual/equal/<commit_after>'use strict';

var test = require('tape');

var u = require('../lib/helpers.js');

test('random_string', function (t) {
    t.plan(5);

    t.notEqual(
        u.random_string(8),
        u.random_string(8)
    );

    [1, 2, 3, 128].forEach(function (i) {
        t.equal(u.random_string(i).length, i);
    });
});

test('random_number_string', function (t) {
    t.plan(55);

    for (var i = 0; i <= 10; i++) {
        t.equal(u.random_number_string(10).length, 1);
        t.equal(u.random_number_string(100).length, 2);
        t.equal(u.random_number_string(1000).length, 3);
        t.equal(u.random_number_string(10000).length, 4);
        t.equal(u.random_number_string(100000).length, 5);
    }
});

test('amendUrl', function (t) {
    // NOTE: i stripped out tests that were depending on the DOM:
    t.plan(6);

    t.throws(function () {
        u.amendUrl('');
    }, 'Wrong url');

    t.throws(function () {
        u.amendUrl(false);
    },'Wrong url');

    t.throws(function () {
        u.amendUrl('http://abc?a=a');
    }, 'Only basic urls are supported');

    t.throws(function () {
        u.amendUrl('http://abc#a');
    }, 'Only basic urls are supported');

    t.equal(u.amendUrl('https://a:80/abc'), 'https://a:80/abc');

    t.equal(u.amendUrl('http://a:443/abc'), 'http://a:443/abc');

});

test('quote', function (t) {
    var all_chars = new Array(65535).map(function (_, i) {
        return String.fromCharCode(i);
    }).join('');

    t.plan(8);

    t.equal(u.quote(''), '""');

    t.equal(u.quote('a'), '"a"');

    t.ok(['"\\t"', '"\\u0009"'].indexOf(u.quote('\t')) !== -1);

    t.ok(['"\\n"', '"\\u000a"'].indexOf(u.quote('\n')) !== -1);

    t.equal(u.quote('\x00\udfff\ufffe\uffff'), '"\\u0000\\udfff\\ufffe\\uffff"');

    t.equal(u.quote('\ud85c\udff7\ud800\ud8ff'), '"\\ud85c\\udff7\\ud800\\ud8ff"');

    t.equal(u.quote('\u2000\u2001\u0300\u0301'), '"\\u2000\\u2001\\u0300\\u0301"');

    t.ok(JSON.parse(u.quote(all_chars)) === all_chars, 'Quote/unquote all 64K chars.');
});

test('detectProtocols', function (t) {
    var node_probed = {
        'websocket': true
    };
    t.plan(1);
    t.deepEqual(u.detectProtocols(node_probed, null, {}), ['websocket']);
 });

test('REventTarget', function (t) {
    var REventTarget = require('../lib/reventtarget.js');
    var bluff, handler0, handler1, handler2, handler3, handler4, log, r, single;
    t.plan(7);
    r = new REventTarget();
    r.addEventListener('message', function () {
        t.ok(true);
    });
    r.onmessage = function () {
        t.ok(false);
    };
    bluff = function () {
        t.ok(false);
    };
    r.addEventListener('message', bluff);
    r.removeEventListener('message', bluff);
    r.addEventListener('message', bluff);
    r.addEventListener('message', function () {
        t.ok(true);
    });
    r.onmessage = function () {
        t.ok(true);
    };
    r.removeEventListener('message', bluff);
    r.dispatchEvent({
        type: 'message'
    });
    handler0 = function () {
        log.push(0);
    };
    handler1 = function () {
        log.push(1);
        r.removeEventListener('test', handler0);
        r.removeEventListener('test', handler2);
        r.addEventListener('test', handler3);
        r.addEventListener('test', handler4);
    };
    handler2 = function () {
        log.push(2);
    };
    handler3 = function () {
        log.push(3);
        r.removeEventListener('test', handler1);
        r.removeEventListener('test', handler3);
        r.removeEventListener('test', handler4);
    };
    handler4 = function () {
        log.push(4);
    };
    r.addEventListener('test', handler0);
    r.addEventListener('test', handler1);
    r.addEventListener('test', handler2);
    log = [];
    r.dispatchEvent({
        type: 'test'
    });
    t.deepEqual(log, [0, 1, 2]);
    log = [];
    r.dispatchEvent({
        type: 'test'
    });
    t.deepEqual(log, [1, 3, 4]);
    log = [];
    r.dispatchEvent({
        type: 'test'
    });
    t.deepEqual(log, []);
    single = function () {
        t.ok(true);
    };
    r.addEventListener('close', single);
    r.addEventListener('close', single);
    r.dispatchEvent({
        type: 'close'
    });
    r.removeEventListener('close', single);
    r.dispatchEvent({
        type: 'close'
    });
    r.removeEventListener('unknown', function () {});
});

test('safe json parse', function (t) {
    var jsonParse = require('../lib/safe-json-parse.js');
    t.plan(2);
    t.equal(jsonParse('invalid json'), null);
    t.deepEqual(jsonParse('{"valid":"json"}'), {"valid":"json"});
});
<|endoftext|>


<commit_before>import { createSelector} from 'reselect';

const getMaterias = state => state.seguidor.materias
const getEstados = state => state.seguidor.estados
const getCorrelativas = state => state.seguidor.correlativas

/*
 *   Estados:
 *   1 - Pendiente
 *   2 - Cursando
 *   3 - Firmada
 *   4 - Aprobada
 *
 *   Los que importan en este caso son firma(3) y aprobación(4).
*/
const ESTADO_FIRMA = 3;
const ESTADO_APROBACION = 4;

/*
 * @name: validarReq
 * @desc: Valida un conjunto de requerimientos contra un conjunto de estados.
 * @param requerimientos: el conjunto de requerimientos a validar.
 * @param estados: estados contra los que se comparan los requerimientos.
 * @param estado: el estado que se quiere que tengan los requerimientos (aprobada/firmada).
*/
function validarReq(requerimientos, estados, estado)
{
    let _valido = true;

	// No hay requisitos, la condición es verdadera.
	if(!requerimientos || requerimientos.length === 0)
		return true;

    for(var req of requerimientos)
	{
	  var result = estados.filter(e => e.id == req && e.status >= estado);

  	  if(result.length === 0)
	  {
	     _valido = false;
		 break;
      }
	}

	return _valido;
}

/*
 * @name: getEstadoMateria
 * @desc: Obtiene el estado de una materia (si se puede cursar o dar final).
 * @param materia: materia cuyo estado se desea conocer.
 * @param correlativas: correlativas de esa materia.
 * @param aprobadas: materias aprobadas hasta el momento.
*/
function getEstadoMateria(_correlativas, estados)
{

   if(_correlativas.length == 0)
       return { cursada: true, final: true};

   // Requisitos para cursar.
   const c_cursadas = _correlativas["rCC"];
   const c_finales  = _correlativas["rFC"];

   // Requisitos para dar final.
   const f_cursadas = _correlativas["rCF"];
   const f_finales  = _correlativas["rFF"];

   const puedeCursar = validarReq(c_cursadas, estados, ESTADO_FIRMA) &&
                       validarReq(c_finales, estados, ESTADO_APROBACION);

   const puededarFinal = validarReq(f_cursadas, estados, ESTADO_FIRMA) &&
                         validarReq(f_finales, estados, ESTADO_APROBACION);

   return { cursada: puedeCursar, final: puededarFinal };
}

// Obtiene las materias con estado.
const getMateriasEstado = createSelector(
  [ getMaterias, getEstados, getCorrelativas ],
  (materias, estados, correlativas) => {
	 for(var materia of materias)
	 {
		materia.id = parseInt(materia.id, 10);
		const _materiaEstado = estados.filter(c => c.id === materia.id);
		let estadoMateria = 1; // Por defecto, la materia esta pendiente.

		if(_materiaEstado.length > 0)
          estadoMateria = _materiaEstado[0].status

		let _correlativas = correlativas.filter(c => c.m === materia.id);

		if(_correlativas.length > 0)
          _correlativas = _correlativas[0].d;

		let estado = getEstadoMateria(_correlativas, estados);

		materia.status = (estado.cursada === false ? 1 : estadoMateria);
		materia.cursada = estado.cursada;
		materia.final = estado.final;

	  }

	  return materias;
   }
);

export default getMateriasEstado;<commit_msg>Mas fixes para el linter.<commit_after>import { createSelector } from 'reselect';

const getMaterias = state => state.seguidor.materias
const getEstados = state => state.seguidor.estados
const getCorrelativas = state => state.seguidor.correlativas

/*
 *   Estados:
 *   1 - Pendiente
 *   2 - Cursando
 *   3 - Firmada
 *   4 - Aprobada
 *
 *   Los que importan en este caso son firma(3) y aprobación(4).
*/
const ESTADO_FIRMA = 3;
const ESTADO_APROBACION = 4;

function reqSatisfy(estados, req, estado) {
  const result = estados.filter(e => e.id == req && e.status >= estado);
  return result.length !== 0;
}

/*
 * @name: validarReq
 * @desc: Valida un conjunto de requerimientos contra un conjunto de estados.
 * @param requerimientos: el conjunto de requerimientos a validar.
 * @param estados: estados contra los que se comparan los requerimientos.
 * @param estado: el estado que se quiere que tengan los requerimientos (aprobada/firmada).
*/
function validarReq(requerimientos, estados, estado) {

  // No hay requisitos, la condición es verdadera.
  if (!requerimientos || requerimientos.length === 0) {
    return true;
  }

  const valido = requerimientos.every(req => reqSatisfy(estados, req, estado));
  return valido;
}

/*
 * @name: getEstadoMateria
 * @desc: Obtiene el estado de una materia (si se puede cursar o dar final).
 * @param materia: materia cuyo estado se desea conocer.
 * @param correlativas: correlativas de esa materia.
 * @param aprobadas: materias aprobadas hasta el momento.
*/
function getEstadoMateria(correlativas, estados) {

  if (correlativas.length == 0) {
    return { cursada: true, final: true };
  }

  // Requisitos para cursar.
  const Ccursadas = correlativas.rCC;
  const CFinales = correlativas.rFC;

  // Requisitos para dar final.
  const Fcursadas = correlativas.rCF;
  const Ffinales = correlativas.rFF;

  const puedeCursar = validarReq(Ccursadas, estados, ESTADO_FIRMA) &&
                      validarReq(CFinales, estados, ESTADO_APROBACION);

  const puededarFinal = validarReq(Fcursadas, estados, ESTADO_FIRMA) &&
                        validarReq(Ffinales, estados, ESTADO_APROBACION);

  return { cursada: puedeCursar, final: puededarFinal };
}

// Obtiene las materias con estado.
const getMateriasEstado = createSelector(
  [getMaterias, getEstados, getCorrelativas],
  (materias, estados, correlativas) => {
    for (const materia of materias) {

      materia.id = parseInt(materia.id, 10);
      const materiaEstado = estados.filter(c => c.id === materia.id);
      let estadoMateria = 1; // Por defecto, la materia esta pendiente.

      if (materiaEstado.length > 0) {
        estadoMateria = materiaEstado[0].status;
      }

      let correlativasMateria = correlativas.filter(c => c.m === materia.id);

      if (correlativasMateria.length > 0) {
        correlativasMateria = correlativasMateria[0].d;
      }

      const estado = getEstadoMateria(correlativasMateria, estados);

      materia.status = (estado.cursada === false ? 1 : estadoMateria);
      materia.cursada = estado.cursada;
      materia.final = estado.final;
    }

    return materias;
  }
);

export default getMateriasEstado;
<|endoftext|>