<commit_before>/**
 * Copyright 2017 The AMP HTML Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {dev, user} from '../../../src/log';
import {isFiniteNumber} from '../../../src/types';
import {Observable} from '../../../src/observable';

const MIN_REPEAT_INTERVAL = 200;


/**
 * This class implements visibility calculations based on the
 * visibility ratio. It's used for documents, embeds and individual element.
 * @implements {../../../src/service.Disposable}
 */
export class VisibilityModel {
  /**
   * @param {!Object<string, *>} spec
   * @param {function():number} calcVisibility
   */
  constructor(spec, calcVisibility) {
    /** @const @private */
    this.calcVisibility_ = calcVisibility;

    /**
     * Spec parameters.
     * @private {{
     *   visiblePercentageMin: number,
     *   visiblePercentageMax: number,
     *   totalTimeMin: number,
     *   totalTimeMax: number,
     *   continuousTimeMin: number,
     *   continuousTimeMax: number,
     * }}
     */
    this.spec_ = {
      visiblePercentageMin: Number(spec['visiblePercentageMin']) / 100 || 0,
      visiblePercentageMax: Number(spec['visiblePercentageMax']) / 100 || 1,
      totalTimeMin: Number(spec['totalTimeMin']) || 0,
      totalTimeMax: Number(spec['totalTimeMax']) || Infinity,
      continuousTimeMin: Number(spec['continuousTimeMin']) || 0,
      continuousTimeMax: Number(spec['continuousTimeMax']) || Infinity,
    };

    /** @private {boolean} */
    this.repeat_ = false;

    /** @private {?number} */
    this.repeatInterval_ = null;

    let repeat = spec['repeat'];
    if (repeat === true) {
      this.repeat_ = true;
    } else {
      repeat = Number(spec['repeat']);
      if (isFiniteNumber(repeat)) {
        const repeatInterval = Math.max(
            this.spec_.totalTimeMin,
            this.spec_.continuousTimeMin,
            repeat);
        if (repeatInterval >= MIN_REPEAT_INTERVAL) {
          this.repeat_ = true;
          this.repeatInterval_ = repeatInterval;
        } else {
          user().error(
              'AMP-ANALYTICS',
              `Cannot repeat with interval less than ${MIN_REPEAT_INTERVAL}, ` +
              ' repeat set to false');
        }
      }
    }

    /** @private {?function()} */
    this.eventResolver_ = null;

    /** @private {?Observable} */
    this.onTriggerObservable_ = new Observable();

    /** @private */
    this.eventPromise_ = new Promise(resolve => {
      this.eventResolver_ = resolve;
    });

    this.eventPromise_.then(() => {
      this.onTriggerObservable_.fire();
    });

    /** @private {!Array<!UnlistenDef>} */
    this.unsubscribe_ = [];

    /** @const @private {time} */
    this.createdTime_ = Date.now();

    /** @private {boolean} */
    this.ready_ = true;

    /** @private {boolean} */
    this.reportReady_ = true;

    /** @private {?function():!Promise} */
    this.createReportReadyPromise_ = null;

    /** @private {?number} */
    this.scheduledRunId_ = null;

    /** @private {boolean} */
    this.matchesVisibility_ = false;

    /** @private {boolean} */
    this.everMatchedVisibility_ = false;

    /** @private {time} duration in milliseconds */
    this.continuousTime_ = 0;

    /** @private {time} duration in milliseconds */
    this.maxContinuousVisibleTime_ = 0;

    /** @private {time} duration in milliseconds */
    this.totalVisibleTime_ = 0;

    /** @private {time} milliseconds since epoch */
    this.firstSeenTime_ = 0;

    /** @private {time} milliseconds since epoch */
    this.lastSeenTime_ = 0;

    /** @private {time} milliseconds since epoch */
    this.firstVisibleTime_ = 0;

    /** @private {time} milliseconds since epoch */
    this.lastVisibleTime_ = 0;

    /** @private {time} percent value in a [0, 1] range */
    this.loadTimeVisibility_ = 0;

    /** @private {number} percent value in a [0, 1] range */
    this.minVisiblePercentage_ = 0;

    /** @private {number} percent value in a [0, 1] range */
    this.maxVisiblePercentage_ = 0;

    /** @private {time} milliseconds since epoch */
    this.lastVisibleUpdateTime_ = 0;

    /** @private {boolean} */
    this.waitToRefresh_ = false;

    /** @private {?number} */
    this.scheduleRepeatId_ = null;
  }

  /**
   * Refresh counter on visible reset.
   * TODO: Right now all state value are scoped state values that gets reset.
   * We may need to add support to global state values,
   * that never reset like globalTotalVisibleTime.
   * Note: loadTimeVisibility is an exception.
   * @private
   */
  refresh_() {
    dev().assert(!this.eventResolver_,
        'Attempt to refresh visible event before previous one resolve');
    this.eventPromise_ = new Promise(resolve => {
      this.eventResolver_ = resolve;
    });
    this.eventPromise_.then(() => {
      this.onTriggerObservable_.fire();
    });
    this.waitToRefresh_ = false;
    this.scheduleRepeatId_ = null;
    this.everMatchedVisibility_ = false;
    this.matchesVisibility_ = false;
    this.continuousTime_ = 0;
    this.maxContinuousVisibleTime_ = 0;
    this.totalVisibleTime_ = 0;
    this.firstVisibleTime_ = 0;
    this.firstSeenTime_ = 0;
    this.lastSeenTime_ = 0;
    this.lastVisibleTime_ = 0;
    this.minVisiblePercentage_ = 0;
    this.maxVisiblePercentage_ = 0;
    this.lastVisibleUpdateTime_ = 0;
    this.waitToRefresh_ = false;
  }

  /**
   * Function that visibilityManager can used to dispose model or reset model
   */
  disposeOrReset() {
    // We may need a maxIntervalWindow to stop visible event even with repeat
    // true
    if (!this.repeat_) {
      this.dispose();
      return;
    }
    if (this.repeatInterval_) {
      const now = Date.now();
      const interval = now - this.firstVisibleTime_;

      // Unit in milliseconds
      const timeUntilRepeat = Math.max(0, this.repeatInterval_ - interval);
      this.ready_ = false;
      dev().assert(!this.scheduleRepeatId_, 'Should not repeat twice');
      this.scheduleRepeatId_ = setTimeout(() => {
        this.refresh_();
        this.setReady(true);
      }, timeUntilRepeat);
    } else {
      // Reset after element falls out of (minPercentage, maxPercentage]
      this.waitToRefresh_ = true;
    }
  }

  /** @override */
  dispose() {
    if (this.scheduledRunId_) {
      clearTimeout(this.scheduledRunId_);
      this.scheduledRunId_ = null;
    }
    if (this.scheduleRepeatId_) {
      clearTimeout(this.scheduleRepeatId_);
      this.scheduleRepeatId_ = null;
    }
    this.unsubscribe_.forEach(unsubscribe => {
      unsubscribe();
    });
    this.unsubscribe_.length = 0;
    this.eventResolver_ = null;
    this.onTriggerObservable_.removeAll();
    this.onTriggerObservable_ = null;
  }

  /**
   * Adds the unsubscribe handler that will be called when this visibility
   * model is destroyed.
   * @param {!UnlistenDef} handler
   */
  unsubscribe(handler) {
    this.unsubscribe_.push(handler);
  }

  /**
   * Adds the event handler that will be called when all visibility conditions
   * have been met.
   * @param {function()} handler
   */
  onTriggerEvent(handler) {
    this.onTriggerObservable_.add(handler);
    if (this.eventPromise_ && !this.eventResolver_) {<commit_msg>Protect from null pointer exeption and silent failure (#12308)<commit_after>/**
 * Copyright 2017 The AMP HTML Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {dev, user} from '../../../src/log';
import {isFiniteNumber} from '../../../src/types';
import {Observable} from '../../../src/observable';

const MIN_REPEAT_INTERVAL = 200;


/**
 * This class implements visibility calculations based on the
 * visibility ratio. It's used for documents, embeds and individual element.
 * @implements {../../../src/service.Disposable}
 */
export class VisibilityModel {
  /**
   * @param {!Object<string, *>} spec
   * @param {function():number} calcVisibility
   */
  constructor(spec, calcVisibility) {
    /** @const @private */
    this.calcVisibility_ = calcVisibility;

    /**
     * Spec parameters.
     * @private {{
     *   visiblePercentageMin: number,
     *   visiblePercentageMax: number,
     *   totalTimeMin: number,
     *   totalTimeMax: number,
     *   continuousTimeMin: number,
     *   continuousTimeMax: number,
     * }}
     */
    this.spec_ = {
      visiblePercentageMin: Number(spec['visiblePercentageMin']) / 100 || 0,
      visiblePercentageMax: Number(spec['visiblePercentageMax']) / 100 || 1,
      totalTimeMin: Number(spec['totalTimeMin']) || 0,
      totalTimeMax: Number(spec['totalTimeMax']) || Infinity,
      continuousTimeMin: Number(spec['continuousTimeMin']) || 0,
      continuousTimeMax: Number(spec['continuousTimeMax']) || Infinity,
    };

    /** @private {boolean} */
    this.repeat_ = false;

    /** @private {?number} */
    this.repeatInterval_ = null;

    let repeat = spec['repeat'];
    if (repeat === true) {
      this.repeat_ = true;
    } else {
      repeat = Number(spec['repeat']);
      if (isFiniteNumber(repeat)) {
        const repeatInterval = Math.max(
            this.spec_.totalTimeMin,
            this.spec_.continuousTimeMin,
            repeat);
        if (repeatInterval >= MIN_REPEAT_INTERVAL) {
          this.repeat_ = true;
          this.repeatInterval_ = repeatInterval;
        } else {
          user().error(
              'AMP-ANALYTICS',
              `Cannot repeat with interval less than ${MIN_REPEAT_INTERVAL}, ` +
              ' repeat set to false');
        }
      }
    }

    /** @private {?function()} */
    this.eventResolver_ = null;

    /** @private {?Observable} */
    this.onTriggerObservable_ = new Observable();

    /** @private */
    this.eventPromise_ = new Promise(resolve => {
      this.eventResolver_ = resolve;
    });

    this.eventPromise_.then(() => {
      this.onTriggerObservable_.fire();
    });

    /** @private {!Array<!UnlistenDef>} */
    this.unsubscribe_ = [];

    /** @const @private {time} */
    this.createdTime_ = Date.now();

    /** @private {boolean} */
    this.ready_ = true;

    /** @private {boolean} */
    this.reportReady_ = true;

    /** @private {?function():!Promise} */
    this.createReportReadyPromise_ = null;

    /** @private {?number} */
    this.scheduledRunId_ = null;

    /** @private {boolean} */
    this.matchesVisibility_ = false;

    /** @private {boolean} */
    this.everMatchedVisibility_ = false;

    /** @private {time} duration in milliseconds */
    this.continuousTime_ = 0;

    /** @private {time} duration in milliseconds */
    this.maxContinuousVisibleTime_ = 0;

    /** @private {time} duration in milliseconds */
    this.totalVisibleTime_ = 0;

    /** @private {time} milliseconds since epoch */
    this.firstSeenTime_ = 0;

    /** @private {time} milliseconds since epoch */
    this.lastSeenTime_ = 0;

    /** @private {time} milliseconds since epoch */
    this.firstVisibleTime_ = 0;

    /** @private {time} milliseconds since epoch */
    this.lastVisibleTime_ = 0;

    /** @private {time} percent value in a [0, 1] range */
    this.loadTimeVisibility_ = 0;

    /** @private {number} percent value in a [0, 1] range */
    this.minVisiblePercentage_ = 0;

    /** @private {number} percent value in a [0, 1] range */
    this.maxVisiblePercentage_ = 0;

    /** @private {time} milliseconds since epoch */
    this.lastVisibleUpdateTime_ = 0;

    /** @private {boolean} */
    this.waitToRefresh_ = false;

    /** @private {?number} */
    this.scheduleRepeatId_ = null;
  }

  /**
   * Refresh counter on visible reset.
   * TODO: Right now all state value are scoped state values that gets reset.
   * We may need to add support to global state values,
   * that never reset like globalTotalVisibleTime.
   * Note: loadTimeVisibility is an exception.
   * @private
   */
  refresh_() {
    dev().assert(!this.eventResolver_,
        'Attempt to refresh visible event before previous one resolve');
    this.eventPromise_ = new Promise(resolve => {
      this.eventResolver_ = resolve;
    });
    this.eventPromise_.then(() => {
      this.onTriggerObservable_.fire();
    });
    this.waitToRefresh_ = false;
    this.scheduleRepeatId_ = null;
    this.everMatchedVisibility_ = false;
    this.matchesVisibility_ = false;
    this.continuousTime_ = 0;
    this.maxContinuousVisibleTime_ = 0;
    this.totalVisibleTime_ = 0;
    this.firstVisibleTime_ = 0;
    this.firstSeenTime_ = 0;
    this.lastSeenTime_ = 0;
    this.lastVisibleTime_ = 0;
    this.minVisiblePercentage_ = 0;
    this.maxVisiblePercentage_ = 0;
    this.lastVisibleUpdateTime_ = 0;
    this.waitToRefresh_ = false;
  }

  /**
   * Function that visibilityManager can used to dispose model or reset model
   */
  disposeOrReset() {
    // We may need a maxIntervalWindow to stop visible event even with repeat
    // true
    if (!this.repeat_) {
      this.dispose();
      return;
    }
    if (this.repeatInterval_) {
      const now = Date.now();
      const interval = now - this.firstVisibleTime_;

      // Unit in milliseconds
      const timeUntilRepeat = Math.max(0, this.repeatInterval_ - interval);
      this.ready_ = false;
      dev().assert(!this.scheduleRepeatId_, 'Should not repeat twice');
      this.scheduleRepeatId_ = setTimeout(() => {
        this.refresh_();
        this.setReady(true);
      }, timeUntilRepeat);
    } else {
      // Reset after element falls out of (minPercentage, maxPercentage]
      this.waitToRefresh_ = true;
    }
  }

  /** @override */
  dispose() {
    if (this.scheduledRunId_) {
      clearTimeout(this.scheduledRunId_);
      this.scheduledRunId_ = null;
    }
    if (this.scheduleRepeatId_) {
      clearTimeout(this.scheduleRepeatId_);
      this.scheduleRepeatId_ = null;
    }
    this.unsubscribe_.forEach(unsubscribe => {
      unsubscribe();
    });
    this.unsubscribe_.length = 0;
    this.eventResolver_ = null;
    // TODO(jonkeller): Investigate why dispose() can be called twice,
    // necessitating this "if"
    if (this.onTriggerObservable_) {
      this.onTriggerObservable_.removeAll();
      this.onTriggerObservable_ = null;
    }
  }

  /**
   * Adds the unsubscribe handler that will be called when this visibility
   * model is destroyed.
   * @param {!UnlistenDef} handler
   */
  unsubscribe(handler) {
    this.unsubscribe_.push(handler);
  }

  /**
   * Adds the event handler that will be called when all visibility conditions
   * have been met.
   * @param {function()} handler
   */
  onTriggerEvent(handler) {
    this.onTriggerObservable_.add(handler);
    if (this.eventPromise_ && !this.eventResolver_) {
{'fraction_non_alphanumeric': 0.1275373420145538, 'fraction_numerical': 0.006702412868632708, 'mean_word_length': 2.734684147794994, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, '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': '818507', 'n_tokens_mistral': 4723, 'n_tokens_neox': 4528, 'n_words': 1197}