<commit_before>// Copyright 2015-2018, University of Colorado Boulder

/**
 * Lightweight event & listener abstraction for a single event type.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */
define( require => {
  'use strict';

  // modules
  const axon = require( 'AXON/axon' );
  const EmitterIO = require( 'AXON/EmitterIO' );
  const PhetioObject = require( 'TANDEM/PhetioObject' );
  const Tandem = require( 'TANDEM/Tandem' );
  const Validator = require( 'AXON/Validator' );

  // constants
  const EmitterIOWithNoArgs = EmitterIO( [] );

  // Simulations have thousands of Emitters, so we re-use objects where possible.
  const EMPTY_ARRAY = [];
  assert && Object.freeze( EMPTY_ARRAY );

  /**
   * @param {Object} [options]
   */
  class Emitter extends PhetioObject {
    constructor( options ) {

      options = _.extend( {

        // {Array.<Object>|null} - array of "Validator Options" Objects that hold options for how to validate each
        // argument, see Validator.js for details.
        argumentTypes: EMPTY_ARRAY,

        tandem: Tandem.optional,
        phetioState: false,
        phetioType: EmitterIOWithNoArgs, // subtypes can override with EmitterIO([...]), see EmitterIO.js
        before: null, // {function} [before] optional function to call before listeners
        after: null // {function} [before] optional function to call after listeners
      }, options );

      assert && assert( !options.hasOwnProperty( 'listener' ), 'listener option no longer supported, please use before' );

      // phetioPlayback events need to know the order the arguments occur in order to call EmitterIO.emit()
      // Indicate whether the event is for playback, but leave this "sparse"--only indicate when this happens to be true
      if ( options.phetioPlayback ) {
        options.phetioEventMetadata = options.phetioEventMetadata || {};
        assert && assert( !options.phetioEventMetadata.hasOwnProperty( 'dataKeys' ), 'dataKeys should be supplied by Emitter, not elsewhere' );
        options.phetioEventMetadata.dataKeys = options.phetioType.elements.map( element => element.name );
      }

      super( options );

      Validator.validate( options.argumentTypes, { valueType: Array } );

      if ( assert ) {

        // Iterate through all argumentType validator options and make sure that they won't validate options on validating value
        options.argumentTypes.forEach( validatorOptions => {
          assert && assert(
            validatorOptions.validateOptionsOnValidateValue === undefined,
            'emitter sets its own validateOptionsOnValidateValue for each argument type'
          );
          validatorOptions.validateOptionsOnValidateValue = false;

          // Changing the validator options after construction indicates a logic error
          assert && Object.freeze( validatorOptions );

          // validate the options passed in to validate each emitter argument
          Validator.validateOptions( validatorOptions );
        } );

        // Changing after construction indicates a logic error
        assert && Object.freeze( options.argumentTypes );
      }

      // @private {function|false}
      this.validate = assert && function() {
        assert(
          arguments.length === options.argumentTypes.length,
          `Emitted unexpected number of args. Expected: ${options.argumentTypes.length} and received ${arguments.length}`
        );
        for ( let i = 0; i < options.argumentTypes.length; i++ ) {
          Validator.validate( arguments[ i ], options.argumentTypes[ i ] );
        }
      };

      // @private {function[]} - the listeners that will be called on emit
      this.listeners = [];

      // @private {function[][]} - during emit() keep track of which listeners should receive events in order to manage
      //                         - removal of listeners during emit()
      this.activeListenersStack = [];

      // @private {function|undefined} if defined, called before listeners are notified
      this.before = options.before;

      // @private {function|undefined} if defined, called after listeners are notified
      this.after = options.after;
    }

    /**
     * Dispose an Emitter that is no longer used.  Like Property.dispose, this method checks that there are no leaked
     * listeners.
     */
    dispose() {

      // See https://github.com/phetsims/axon/issues/124
      this.listeners.length = 0;

      PhetioObject.prototype.dispose.call( this );
    }

    /**
     * Adds a listener which will be called during emit.
     * @param {function} listener
     * @public
     */
    addListener( listener ) {

      assert && assert( this.listeners.indexOf( listener ) === -1, 'Cannot add the same listener twice' );

      // If a listener is added during an emit(), we must make a copy of the current list of listeners--the newly added
      // listener will be available for the next emit() but not the one in progress.  This is to match behavior with
      // removeListener.
      this.defendListeners();

      this.listeners.push( listener );
    }

    /**
     * Removes a listener
     * @param {function} listener
     * @public
     */
    removeListener( listener ) {

      const index = this.listeners.indexOf( listener );

      // Throw an error when removing a non-listener (except when the Emitter has already been disposed, see
      // https://github.com/phetsims/sun/issues/394#issuecomment-419998231
      if ( assert && !this.isDisposed ) {
        assert && assert( index >= 0, 'tried to removeListener on something that wasn\'t a listener' );
      }

      // If an emit is in progress, make a copy of the current list of listeners--the removed listener will remain in
      // the list and be called for this emit call, see #72
      this.defendListeners();

      this.listeners.splice( index, 1 );
    }

    /**
     * Removes all the listeners
     * @public
     */
    removeAllListeners() {
      while ( this.listeners.length > 0 ) {
        this.removeListener( this.listeners[ 0 ] );
      }
    }

    /**
     * If addListener/removeListener is called while emit() is in progress, we must make a defensive copy of the array
     * of listeners before changing the array, and use it for the rest of the notifications until the emit call has
     * completed.
     * @private
     */
    defendListeners() {

      for ( let i = this.activeListenersStack.length - 1; i >= 0; i-- ) {

        // Once we meet a level that was already defended, we can stop, since all previous levels are also defended
        if ( this.activeListenersStack[ i ].defended ) {
          break;
        }
        else {
          const defendedListeners = this.listeners.slice();

          // Mark copies as 'defended' so that it will use the original listeners when emit started and not the modified
          // list.
          defendedListeners.defended = true;
          this.activeListenersStack[ i ] = defendedListeners;
        }
      }
    }

    /**
     * Emits a single event.  This method is called many times in a simulation and must be well-optimized.  Listeners
     * are notified in the order they were added via addListener.
     * @params - expected parameters are based on options.argumentTypes, see constructor
     * @public
     */
    emit() {

      // validate the args
      this.validate && this.validate.apply( null, arguments );

      this.before && this.before.apply( null, arguments );

      // handle phet-io data stream for the emitted event
      if ( this.isPhetioInstrumented() ) {

        // null if there are no arguments.  dataStream.js omits null values for data
        let data = null;
        if ( this.phetioType.elements.length > 0 ) {

          // Enumerate named argsObject for the data stream.
          data = {};
          for ( let i = 0; i < this.phetioType.elements.length; i++ ) {
            const element = this.phetioType.elements[ i ];
            data[ element.name ] = element.type.toStateObject( arguments[ i ] );
          }
        }

        this.phetioStartEvent( 'emitted', data );
      }

      // Notify wired-up listeners, if any
      if ( this.listeners.length > 0 ) {

        this.activeListenersStack.push( this.listeners );
        const lastEntry = this.activeListenersStack.length - 1;

        // Notify listeners
        for ( let i = 0; i < this.activeListenersStack[ lastEntry ].length; i++ ) {
          this.activeListenersStack[ lastEntry ][ i ].apply( null, arguments );
        }

        this.activeListenersStack.pop();
      }
      this.isPhetioInstrumented() && this.phetioEndEvent();

      this.after && this.after.apply( null, arguments );
    }

    /**
     * Emits a single event with one argument.  This is a copy-paste of emit() for performance reasons.
     * @param {*} arg0
     * @public
     * @deprecated - please use emit()
     */
    emit1( arg0 ) {

      this.before && this.before( arg0 );

      this.isPhetioInstrumented() && this.phetioStartEvent( 'emitted', {
        args: [ this.phetioType.parameterTypes[ 0 ].toStateObject( arg0 ) ]
      } );

      if ( this.listeners.length > 0 ) {
        this.activeListenersStack.push( this.listeners );
        const lastEntry = this.activeListenersStack.length - 1;

        for ( let i = 0; i < this.activeListenersStack[ lastEntry ].length; i++ ) {
          this.activeListenersStack[ lastEntry ][ i ]( arg0 );
        }

        this.activeListenersStack.pop();
      }
      this.isPhetioInstrumented() && this.phetioEndEvent();

      this.after && this.after( arg0 );
    }

    /**
     * Emits a single event with two arguments.  This is a copy-paste of emit() for performance reasons.
     * @param {*} arg0
     * @param {*} arg1
     * @public
     * @deprecated - please use emit()
     */
    emit2( arg0, arg1 ) {
      this.before && this.before( arg0, arg1 );
      this.isPhetioInstrumented() && this.phetioStartEvent( 'emitted', {
        args: [
          this.phetioType.parameterTypes[ 0 ].toStateObject( arg0 ),
          this.phetioType.parameterTypes[ 1 ].toStateObject( arg1 )
        ]
      } );

      if ( this.listeners.length > 0 ) {
        this.activeListenersStack.push( this.listeners );
        const lastEntry = this.activeListenersStack.length - 1;

        for ( let i = 0; i < this.activeListenersStack[ lastEntry ].length; i++ ) {
          this.activeListenersStack[ lastEntry ][ i ]( arg0, arg1 );
        }
        this.activeListenersStack.pop();
      }
      this.isPhetioInstrumented() && this.phetioEndEvent();
      this.after && this.after( arg0, arg1 );
    }

    /**
     * Emits a single event with three arguments.  This is a copy-paste of emit() for performance reasons.
     * @param {*} arg0
     * @param {*} arg1
     * @param {*} arg2
     * @public
     * @deprecated - please use emit()
     */
    emit3( arg0, arg1, arg2 ) {
      this.before && this.before( arg0, arg1, arg2 );
      this.isPhetioInstrumented() && this.phetioStartEvent( 'emitted', {
        args: [
          this.phetioType.parameterTypes[ 0 ].toStateObject( arg0 ),
          this.phetioType.parameterTypes[ 1 ].toStateObject( arg1 ),
          this.phetioType.parameterTypes[ 2 ].toStateObject( arg2 )
        ]
      } );

      if ( this.listeners.length > 0 ) {
        this.activeListenersStack.push( this.listeners );
        const lastEntry = this.activeListenersStack.length - 1;

        for ( let i = 0; i < this.activeListenersStack[ lastEntry ].length; i++ ) {
          this.activeListenersStack[ lastEntry ][ i ]( arg0, arg1, arg2 );
        }

        this.activeListenersStack.pop();
      }
      this.isPhetioInstrumented() && this.phetioEndEvent();
      this.after && this.after( arg0, arg1, arg2 );
    }

    /**
     * Checks whether a listener is registered with this Emitter
     * @param {function} listener
     * @returns {boolean}
     * @public
     */
    hasListener( listener ) {
      assert && assert( arguments.length === 1, 'Emitter.hasListener should be called with 1 argument' );
      return this.listeners.indexOf( listener ) >= 0;
    }

    /**
     * Returns true if there are any listeners.
     * @returns {boolean}
     * @public
     */
    hasListeners() {
      assert && assert( arguments.length === 0, 'Emitter.hasListeners should be called without arguments' );
      return this.listeners.length > 0;
    }

    /**
     * Returns the number of listeners.
     * @returns {number}
     * @public
     */
    getListenerCount() {
      return this.listeners.length;
    }
  }

  return axon.register( 'Emitter', Emitter );
} );<commit_msg>before/after doc, https://github.com/phetsims/scenery/issues/928<commit_after>// Copyright 2015-2018, University of Colorado Boulder

/**
 * Lightweight event & listener abstraction for a single event type.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */
define( require => {
  'use strict';

  // modules
  const axon = require( 'AXON/axon' );
  const EmitterIO = require( 'AXON/EmitterIO' );
  const PhetioObject = require( 'TANDEM/PhetioObject' );
  const Tandem = require( 'TANDEM/Tandem' );
  const Validator = require( 'AXON/Validator' );

  // constants
  const EmitterIOWithNoArgs = EmitterIO( [] );

  // Simulations have thousands of Emitters, so we re-use objects where possible.
  const EMPTY_ARRAY = [];
  assert && Object.freeze( EMPTY_ARRAY );

  /**
   * @param {Object} [options]
   */
  class Emitter extends PhetioObject {
    constructor( options ) {

      options = _.extend( {

        // {Array.<Object>|null} - array of "Validator Options" Objects that hold options for how to validate each
        // argument, see Validator.js for details.
        argumentTypes: EMPTY_ARRAY,

        tandem: Tandem.optional,
        phetioState: false,
        phetioType: EmitterIOWithNoArgs, // subtypes can override with EmitterIO([...]), see EmitterIO.js
        before: null, // {function|null} optional function to call before listeners, with same args as listeners
        after: null // {function|null} optional function to call after listeners, with same args as listeners
      }, options );

      assert && assert( !options.hasOwnProperty( 'listener' ), 'listener option no longer supported, please use before' );

      // phetioPlayback events need to know the order the arguments occur in order to call EmitterIO.emit()
      // Indicate whether the event is for playback, but leave this "sparse"--only indicate when this happens to be true
      if ( options.phetioPlayback ) {
        options.phetioEventMetadata = options.phetioEventMetadata || {};
        assert && assert( !options.phetioEventMetadata.hasOwnProperty( 'dataKeys' ), 'dataKeys should be supplied by Emitter, not elsewhere' );
        options.phetioEventMetadata.dataKeys = options.phetioType.elements.map( element => element.name );
      }

      super( options );

      Validator.validate( options.argumentTypes, { valueType: Array } );

      if ( assert ) {

        // Iterate through all argumentType validator options and make sure that they won't validate options on validating value
        options.argumentTypes.forEach( validatorOptions => {
          assert && assert(
            validatorOptions.validateOptionsOnValidateValue === undefined,
            'emitter sets its own validateOptionsOnValidateValue for each argument type'
          );
          validatorOptions.validateOptionsOnValidateValue = false;

          // Changing the validator options after construction indicates a logic error
          assert && Object.freeze( validatorOptions );

          // validate the options passed in to validate each emitter argument
          Validator.validateOptions( validatorOptions );
        } );

        // Changing after construction indicates a logic error
        assert && Object.freeze( options.argumentTypes );
      }

      // @private {function|false}
      this.validate = assert && function() {
        assert(
          arguments.length === options.argumentTypes.length,
          `Emitted unexpected number of args. Expected: ${options.argumentTypes.length} and received ${arguments.length}`
        );
        for ( let i = 0; i < options.argumentTypes.length; i++ ) {
          Validator.validate( arguments[ i ], options.argumentTypes[ i ] );
        }
      };

      // @private {function[]} - the listeners that will be called on emit
      this.listeners = [];

      // @private {function[][]} - during emit() keep track of which listeners should receive events in order to manage
      //                         - removal of listeners during emit()
      this.activeListenersStack = [];

      // @private {function|null} if defined, called before listeners are notified
      this.before = options.before;

      // @private {function|null} if defined, called after listeners are notified
      this.after = options.after;
    }

    /**
     * Dispose an Emitter that is no longer used.  Like Property.dispose, this method checks that there are no leaked
     * listeners.
     */
    dispose() {

      // See https://github.com/phetsims/axon/issues/124
      this.listeners.length = 0;

      PhetioObject.prototype.dispose.call( this );
    }

    /**
     * Adds a listener which will be called during emit.
     * @param {function} listener
     * @public
     */
    addListener( listener ) {

      assert && assert( this.listeners.indexOf( listener ) === -1, 'Cannot add the same listener twice' );

      // If a listener is added during an emit(), we must make a copy of the current list of listeners--the newly added
      // listener will be available for the next emit() but not the one in progress.  This is to match behavior with
      // removeListener.
      this.defendListeners();

      this.listeners.push( listener );
    }

    /**
     * Removes a listener
     * @param {function} listener
     * @public
     */
    removeListener( listener ) {

      const index = this.listeners.indexOf( listener );

      // Throw an error when removing a non-listener (except when the Emitter has already been disposed, see
      // https://github.com/phetsims/sun/issues/394#issuecomment-419998231
      if ( assert && !this.isDisposed ) {
        assert && assert( index >= 0, 'tried to removeListener on something that wasn\'t a listener' );
      }

      // If an emit is in progress, make a copy of the current list of listeners--the removed listener will remain in
      // the list and be called for this emit call, see #72
      this.defendListeners();

      this.listeners.splice( index, 1 );
    }

    /**
     * Removes all the listeners
     * @public
     */
    removeAllListeners() {
      while ( this.listeners.length > 0 ) {
        this.removeListener( this.listeners[ 0 ] );
      }
    }

    /**
     * If addListener/removeListener is called while emit() is in progress, we must make a defensive copy of the array
     * of listeners before changing the array, and use it for the rest of the notifications until the emit call has
     * completed.
     * @private
     */
    defendListeners() {

      for ( let i = this.activeListenersStack.length - 1; i >= 0; i-- ) {

        // Once we meet a level that was already defended, we can stop, since all previous levels are also defended
        if ( this.activeListenersStack[ i ].defended ) {
          break;
        }
        else {
          const defendedListeners = this.listeners.slice();

          // Mark copies as 'defended' so that it will use the original listeners when emit started and not the modified
          // list.
          defendedListeners.defended = true;
          this.activeListenersStack[ i ] = defendedListeners;
        }
      }
    }

    /**
     * Emits a single event.  This method is called many times in a simulation and must be well-optimized.  Listeners
     * are notified in the order they were added via addListener.
     * @params - expected parameters are based on options.argumentTypes, see constructor
     * @public
     */
    emit() {

      // validate the args
      this.validate && this.validate.apply( null, arguments );

      this.before && this.before.apply( null, arguments );

      // handle phet-io data stream for the emitted event
      if ( this.isPhetioInstrumented() ) {

        // null if there are no arguments.  dataStream.js omits null values for data
        let data = null;
        if ( this.phetioType.elements.length > 0 ) {

          // Enumerate named argsObject for the data stream.
          data = {};
          for ( let i = 0; i < this.phetioType.elements.length; i++ ) {
            const element = this.phetioType.elements[ i ];
            data[ element.name ] = element.type.toStateObject( arguments[ i ] );
          }
        }

        this.phetioStartEvent( 'emitted', data );
      }

      // Notify wired-up listeners, if any
      if ( this.listeners.length > 0 ) {

        this.activeListenersStack.push( this.listeners );
        const lastEntry = this.activeListenersStack.length - 1;

        // Notify listeners
        for ( let i = 0; i < this.activeListenersStack[ lastEntry ].length; i++ ) {
          this.activeListenersStack[ lastEntry ][ i ].apply( null, arguments );
        }

        this.activeListenersStack.pop();
      }
      this.isPhetioInstrumented() && this.phetioEndEvent();

      this.after && this.after.apply( null, arguments );
    }

    /**
     * Emits a single event with one argument.  This is a copy-paste of emit() for performance reasons.
     * @param {*} arg0
     * @public
     * @deprecated - please use emit()
     */
    emit1( arg0 ) {

      this.before && this.before( arg0 );

      this.isPhetioInstrumented() && this.phetioStartEvent( 'emitted', {
        args: [ this.phetioType.parameterTypes[ 0 ].toStateObject( arg0 ) ]
      } );

      if ( this.listeners.length > 0 ) {
        this.activeListenersStack.push( this.listeners );
        const lastEntry = this.activeListenersStack.length - 1;

        for ( let i = 0; i < this.activeListenersStack[ lastEntry ].length; i++ ) {
          this.activeListenersStack[ lastEntry ][ i ]( arg0 );
        }

        this.activeListenersStack.pop();
      }
      this.isPhetioInstrumented() && this.phetioEndEvent();

      this.after && this.after( arg0 );
    }

    /**
     * Emits a single event with two arguments.  This is a copy-paste of emit() for performance reasons.
     * @param {*} arg0
     * @param {*} arg1
     * @public
     * @deprecated - please use emit()
     */
    emit2( arg0, arg1 ) {
      this.before && this.before( arg0, arg1 );
      this.isPhetioInstrumented() && this.phetioStartEvent( 'emitted', {
        args: [
          this.phetioType.parameterTypes[ 0 ].toStateObject( arg0 ),
          this.phetioType.parameterTypes[ 1 ].toStateObject( arg1 )
        ]
      } );

      if ( this.listeners.length > 0 ) {
        this.activeListenersStack.push( this.listeners );
        const lastEntry = this.activeListenersStack.length - 1;

        for ( let i = 0; i < this.activeListenersStack[ lastEntry ].length; i++ ) {
          this.activeListenersStack[ lastEntry ][ i ]( arg0, arg1 );
        }
        this.activeListenersStack.pop();
      }
      this.isPhetioInstrumented() && this.phetioEndEvent();
      this.after && this.after( arg0, arg1 );
    }

    /**
     * Emits a single event with three arguments.  This is a copy-paste of emit() for performance reasons.
     * @param {*} arg0
     * @param {*} arg1
     * @param {*} arg2
     * @public
     * @deprecated - please use emit()
     */
    emit3( arg0, arg1, arg2 ) {
      this.before && this.before( arg0, arg1, arg2 );
      this.isPhetioInstrumented() && this.phetioStartEvent( 'emitted', {
        args: [
          this.phetioType.parameterTypes[ 0 ].toStateObject( arg0 ),
          this.phetioType.parameterTypes[ 1 ].toStateObject( arg1 ),
          this.phetioType.parameterTypes[ 2 ].toStateObject( arg2 )
        ]
      } );

      if ( this.listeners.length > 0 ) {
        this.activeListenersStack.push( this.listeners );
        const lastEntry = this.activeListenersStack.length - 1;

        for ( let i = 0; i < this.activeListenersStack[ lastEntry ].length; i++ ) {
          this.activeListenersStack[ lastEntry ][ i ]( arg0, arg1, arg2 );
        }

        this.activeListenersStack.pop();
      }
      this.isPhetioInstrumented() && this.phetioEndEvent();
      this.after && this.after( arg0, arg1, arg2 );
    }

    /**
     * Checks whether a listener is registered with this Emitter
     * @param {function} listener
     * @returns {boolean}
     * @public
     */
    hasListener( listener ) {
      assert && assert( arguments.length === 1, 'Emitter.hasListener should be called with 1 argument' );
      return this.listeners.indexOf( listener ) >= 0;
    }

    /**
     * Returns true if there are any listeners.
     * @returns {boolean}
     * @public
     */
    hasListeners() {
      assert && assert( arguments.length === 0, 'Emitter.hasListeners should be called without arguments' );
      return this.listeners.length > 0;
    }

    /**
     * Returns the number of listeners.
     * @returns {number}
     * @public
     */
    getListenerCount() {
      return this.listeners.length;
    }
  }

  return axon.register( 'Emitter', Emitter );
} );<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import EJSON from 'ejson';
import { connect } from 'react-redux';
import {
  debounce,
} from '@theclinician/toolbelt';
import DDPConnector from './DDPConnector.js';
import {
  makeGetSubscriptionsReady,
  makeGetQueriesValues,
  makeGetQueriesReady,
} from './selectors.js';
import {
  createIdGenerator,
  wrapMapState,
} from './utils.js';

const uniqueId = createIdGenerator('listener.');
const increase = (key, value) => prevState => ({
  ...prevState,
  [key]: (prevState[key] || 0) + value,
});

const ddp = ({
  subscriptions: makeMapStateToSubscriptions,
  queries: makeMapStateToQueries,
  mutations = {},
}, {
  onMutationError,
  renderLoader = defaultComponent => React.createElement(defaultComponent),
  getResourceId = params => EJSON.stringify(params),
  queriesUpdateDelay,
  subscriptionsUpdateDalay,
} = {}) => (Inner) => {
  const propTypes = {
    subscriptions: PropTypes.array,
    queries: PropTypes.object,
    subscriptionsReady: PropTypes.bool,
    queriesReady: PropTypes.bool,
  };

  const defaultProps = {
    subscriptions: [],
    queries: {},
    subscriptionsReady: true,
    queriesReady: true,
  };

  const contextTypes = {
    ddpConnector: PropTypes.instanceOf(DDPConnector),
  };

  class Container extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        numberOfPendingMutations: 0,
      };
      const mutate = (request) => {
        if (request) {
          const { name, params } = request;
          this.beginMutation();
          return this.ddpConnector.apply(name, params, {})
            .then(res => this.endMutation(res))
            .catch((err) => {
              this.endMutation();
              if (onMutationError) {
                onMutationError(err);
              } else {
                throw err;
              }
            });
        }
        return Promise.resolve();
      };

      this.handlers = {};
      Object.keys(mutations).forEach((key) => {
        this.handlers[key] = (...args) => {
          mutations[key]({
            ...this.props,
            mutate,
          })(...args);
        };
      });
    }

    componentWillMount() {
      this.id = uniqueId();
      this.ddpConnector = this.context.ddpConnector;

      this.updateSubscriptions = debounce(subscriptions =>
        this.ddpConnector.subsManager.updateRequests(this.id, subscriptions),
        {
          ms: subscriptionsUpdateDalay !== undefined ? subscriptionsUpdateDalay : this.ddpConnector.resourceUpdateDelay,
        },
      );

      this.updateQueries = debounce(queries =>
        this.ddpConnector.queryManager.updateRequests(this.id, Object.keys(queries).map(key => queries[key])),
        {
          ms: queriesUpdateDelay !== undefined ? queriesUpdateDelay : this.ddpConnector.resourceUpdateDelay,
        },
      );
    }

    componentDidMount() {
      this.updateSubscriptions(this.props.subscriptions);
      this.updateQueries(this.props.queries);
    }

    componentWillReceiveProps(newProps) {
      this.updateSubscriptions(newProps.subscriptions);
      this.updateQueries(newProps.queries);
    }

    componentWillUnmount() {
      this.updateSubscriptions([]);
      this.updateQueries([]);
    }

    beginMutation() {
      this.setState(increase('numberOfPendingMutations', 1));
    }

    endMutation(result) {
      this.setState(increase('numberOfPendingMutations', -1));
      return result;
    }

    render() {
      const { ddpConnector } = this.context;
      const {
        numberOfPendingMutations,
      } = this.state;
      const {
        subscriptionsReady,
        queriesReady,
        queries,
        subscriptions,
        ...other
      } = this.props;
      const mutationsReady = numberOfPendingMutations <= 0;
      if (renderLoader &&
        (
          !subscriptionsReady ||
          !mutationsReady ||
          !queriesReady
        )
      ) {
        return renderLoader(ddpConnector.getLoaderComponent(), {
          ...other,
          subscriptionsReady,
          mutationsReady,
          queriesReady,
        });
      }
      return React.createElement(Inner, {
        ...other,
        ...this.handlers,
        queriesReady,
        mutationsReady,
        subscriptionsReady,
      });
    }
  }

  Container.propTypes = propTypes;
  Container.defaultProps = defaultProps;
  Container.contextTypes = contextTypes;

  if (process.env.NODE_ENV !== 'production') {
    Container.displayName = `ddp(${Inner.displayName})`;
  }

  const wrappedMapStateToSubscriptions = wrapMapState(makeMapStateToSubscriptions);
  const wrappedMapStateToQueries = wrapMapState(makeMapStateToQueries);

  return connect(
    () => {
      const getSubscriptionsReady = makeGetSubscriptionsReady((_, x) => getResourceId(x));
      const getQueriesValues = makeGetQueriesValues((_, x) => getResourceId(x));
      const getQueriesReady = makeGetQueriesReady((_, x) => getResourceId(x));
      //----------------------------------------------------------------------
      return (state, ownProps) => {
        const subscriptions = wrappedMapStateToSubscriptions(state, ownProps) || [];
        const queries = wrappedMapStateToQueries(state, ownProps) || {};
        return {
          ...getQueriesValues(state, queries),
          subscriptions,
          queries,
          subscriptionsReady: getSubscriptionsReady(state, { subscriptions }),
          queriesReady: getQueriesReady(state, { queries }),
        };
      };
    },
  )(Container);
};

export default ddp;
<commit_msg>Propagate the promise returned by mutation handler<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import EJSON from 'ejson';
import { connect } from 'react-redux';
import {
  debounce,
} from '@theclinician/toolbelt';
import DDPConnector from './DDPConnector.js';
import {
  makeGetSubscriptionsReady,
  makeGetQueriesValues,
  makeGetQueriesReady,
} from './selectors.js';
import {
  createIdGenerator,
  wrapMapState,
} from './utils.js';

const uniqueId = createIdGenerator('listener.');
const increase = (key, value) => prevState => ({
  ...prevState,
  [key]: (prevState[key] || 0) + value,
});

const ddp = ({
  subscriptions: makeMapStateToSubscriptions,
  queries: makeMapStateToQueries,
  mutations = {},
}, {
  onMutationError,
  renderLoader = defaultComponent => React.createElement(defaultComponent),
  getResourceId = params => EJSON.stringify(params),
  queriesUpdateDelay,
  subscriptionsUpdateDalay,
} = {}) => (Inner) => {
  const propTypes = {
    subscriptions: PropTypes.array,
    queries: PropTypes.object,
    subscriptionsReady: PropTypes.bool,
    queriesReady: PropTypes.bool,
  };

  const defaultProps = {
    subscriptions: [],
    queries: {},
    subscriptionsReady: true,
    queriesReady: true,
  };

  const contextTypes = {
    ddpConnector: PropTypes.instanceOf(DDPConnector),
  };

  class Container extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        numberOfPendingMutations: 0,
      };
      const mutate = (request) => {
        if (request) {
          const { name, params } = request;
          this.beginMutation();
          return this.ddpConnector.apply(name, params, {})
            .then(res => this.endMutation(res))
            .catch((err) => {
              this.endMutation();
              if (onMutationError) {
                onMutationError(err);
              } else {
                throw err;
              }
            });
        }
        return Promise.resolve();
      };

      this.handlers = {};
      Object.keys(mutations).forEach((key) => {
        this.handlers[key] = (...args) => mutations[key]({
          ...this.props,
          mutate,
        })(...args);
      });
    }

    componentWillMount() {
      this.id = uniqueId();
      this.ddpConnector = this.context.ddpConnector;

      this.updateSubscriptions = debounce(subscriptions =>
        this.ddpConnector.subsManager.updateRequests(this.id, subscriptions),
        {
          ms: subscriptionsUpdateDalay !== undefined ? subscriptionsUpdateDalay : this.ddpConnector.resourceUpdateDelay,
        },
      );

      this.updateQueries = debounce(queries =>
        this.ddpConnector.queryManager.updateRequests(this.id, Object.keys(queries).map(key => queries[key])),
        {
          ms: queriesUpdateDelay !== undefined ? queriesUpdateDelay : this.ddpConnector.resourceUpdateDelay,
        },
      );
    }

    componentDidMount() {
      this.updateSubscriptions(this.props.subscriptions);
      this.updateQueries(this.props.queries);
    }

    componentWillReceiveProps(newProps) {
      this.updateSubscriptions(newProps.subscriptions);
      this.updateQueries(newProps.queries);
    }

    componentWillUnmount() {
      this.updateSubscriptions([]);
      this.updateQueries([]);
    }

    beginMutation() {
      this.setState(increase('numberOfPendingMutations', 1));
    }

    endMutation(result) {
      this.setState(increase('numberOfPendingMutations', -1));
      return result;
    }

    render() {
      const { ddpConnector } = this.context;
      const {
        numberOfPendingMutations,
      } = this.state;
      const {
        subscriptionsReady,
        queriesReady,
        queries,
        subscriptions,
        ...other
      } = this.props;
      const mutationsReady = numberOfPendingMutations <= 0;
      if (renderLoader &&
        (
          !subscriptionsReady ||
          !mutationsReady ||
          !queriesReady
        )
      ) {
        return renderLoader(ddpConnector.getLoaderComponent(), {
          ...other,
          subscriptionsReady,
          mutationsReady,
          queriesReady,
        });
      }
      return React.createElement(Inner, {
        ...other,
        ...this.handlers,
        queriesReady,
        mutationsReady,
        subscriptionsReady,
      });
    }
  }

  Container.propTypes = propTypes;
  Container.defaultProps = defaultProps;
  Container.contextTypes = contextTypes;

  if (process.env.NODE_ENV !== 'production') {
    Container.displayName = `ddp(${Inner.displayName})`;
  }

  const wrappedMapStateToSubscriptions = wrapMapState(makeMapStateToSubscriptions);
  const wrappedMapStateToQueries = wrapMapState(makeMapStateToQueries);

  return connect(
    () => {
      const getSubscriptionsReady = makeGetSubscriptionsReady((_, x) => getResourceId(x));
      const getQueriesValues = makeGetQueriesValues((_, x) => getResourceId(x));
      const getQueriesReady = makeGetQueriesReady((_, x) => getResourceId(x));
      //----------------------------------------------------------------------
      return (state, ownProps) => {
        const subscriptions = wrappedMapStateToSubscriptions(state, ownProps) || [];
        const queries = wrappedMapStateToQueries(state, ownProps) || {};
        return {
          ...getQueriesValues(state, queries),
          subscriptions,
          queries,
          subscriptionsReady: getSubscriptionsReady(state, { subscriptions }),
          queriesReady: getQueriesReady(state, { queries }),
        };
      };
    },
  )(Container);
};

export default ddp;
<|endoftext|>


<commit_before>// Copyright 2002-2013, University of Colorado Boulder

/**
 * Horizontal slider.
 *
 * Moved from beers-law-lab/EvaporationSlider on 9/15/2013
 * see https://github.com/phetsims/sun/issues/9
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var Dimension2 = require( 'DOT/Dimension2' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearFunction = require( 'DOT/LinearFunction' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Property = require( 'AXON/Property' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var Input = require( 'SCENERY/input/Input' );
  var Util = require( 'DOT/Util' );

  /**
   * @param {Property.<number>} valueProperty
   * @param { {min:number, max:number} } range
   * @param {Object} [options]
   * @constructor
   */
  function HSlider( valueProperty, range, options ) {

    var thisSlider = this;
    Node.call( thisSlider );

    options = _.extend( {
      // track
      trackSize: new Dimension2( 100, 5 ),
      trackFill: 'white',
      trackStroke: 'black',
      trackLineWidth: 1,
      // {Node} optional thumb, replaces the default. Client is responsible for highlighting and disabling. Centered in the track.
      thumbNode: null,
      // default thumb (ignored if thumbNode is provided)
      thumbSize: new Dimension2( 22, 45 ),
      thumbFillEnabled: 'rgb(50,145,184)',
      thumbFillHighlighted: 'rgb(71,207,255)',
      thumbFillDisabled: '#F0F0F0',
      thumbStroke: 'black',
      thumbLineWidth: 1,
      thumbCenterLineStroke: 'white',
      // ticks
      tickLabelSpacing: 6,
      majorTickLength: 25,
      majorTickStroke: 'black',
      majorTickLineWidth: 1,
      minorTickLength: 10,
      minorTickStroke: 'black',
      minorTickLineWidth: 1,
      // other
      cursor: 'pointer',
      enabledProperty: new Property( true ),
      startDrag: function() {}, // called when a drag sequence starts
      endDrag: function() {}, // called when a drag sequence ends
      constrainValue: function( value ) { return value; }, // called before valueProperty is set
      // tandem
      tandem: null
    }, options );
    this.options = options; // @private TODO save only the options that are needed by prototype functions

    // @private ticks are added to this parent, so they are behind knob
    thisSlider.ticksParent = new Node();
    thisSlider.addChild( thisSlider.ticksParent );

    // @private mapping between value and track position
    thisSlider.valueToPosition = new LinearFunction( range.min, range.max, 0, options.trackSize.width, true /* clamp */ );

    // @private track
    thisSlider.track = new Rectangle( 0, 0, options.trackSize.width, options.trackSize.height,
      { fill: options.trackFill, stroke: options.trackStroke, lineWidth: options.trackLineWidth } );
    thisSlider.addChild( thisSlider.track );

    // click in the track to change the value, continue dragging if desired
    var trackHandler = new SimpleDragHandler( {
      handleTrackEvent: function( event, trail ) {
        if ( options.enabledProperty.get() ) {
          var transform = trail.subtrailTo( thisSlider ).getTransform();
          var x = transform.inversePosition2( event.pointer.point ).x;
          var value = thisSlider.valueToPosition.inverse( x );
          valueProperty.set( options.constrainValue( value ) );
        }
      },
      start: function( event, trail ) {
        if ( options.enabledProperty.get() ) {
          options.startDrag();
        }
        this.handleTrackEvent( event, trail );
      },
      drag: function( event, trail ) {
        this.handleTrackEvent( event, trail );
      },
      end: function() {
        if ( options.enabledProperty.get() ) {
          options.endDrag();
        }
      }
    } );
    thisSlider.track.addInputListener( trackHandler );

    // thumb
    var thumb = options.thumbNode || new ThumbNode( options );

    // Make the thumb focusable for keyboard accessibility
    thumb.focusable = true;

    thumb.centerY = thisSlider.track.centerY;
    thisSlider.addChild( thumb );

    // Keyboard accessibility
    thumb.addInputListener( {
      keydown: function( event, trail ) {
        var keyCode = event.domEvent.keyCode;
        var delta = keyCode === Input.KEY_LEFT_ARROW || keyCode === Input.KEY_DOWN_ARROW ? -1 :
                    keyCode === Input.KEY_RIGHT_ARROW || keyCode === Input.KEY_UP_ARROW ? +1 :
                    0;
        var clampedValue = Util.clamp( valueProperty.get() + (range.max - range.min) * 0.1 * delta, range.min, range.max );
        valueProperty.set( options.constrainValue( clampedValue ) );
      }
    } );

    // thumb touch area
    if ( !options.thumbNode ) {
      var dx = 0.5 * thumb.width;
      var dy = 0.25 * thumb.height;
      thumb.touchArea = Shape.rectangle( ( -thumb.width / 2 ) - dx, ( -thumb.height / 2 ) - dy, thumb.width + dx + dx, thumb.height + dy + dy );
    }

    // update value when thumb is dragged
    var thumbHandler = new SimpleDragHandler( {

      // x-offset between initial click and thumb's origin
      clickXOffset: 0,

      allowTouchSnag: true,

      start: function( event, trail ) {
        if ( options.enabledProperty.get() ) {
          thisSlider.trigger1( 'startedCallbacksForDragStarted', valueProperty.get() );
          options.startDrag();

          var transform = trail.subtrailTo( thisSlider ).getTransform();
          this.clickXOffset = transform.inversePosition2( event.pointer.point ).x - thumb.x;

          thisSlider.trigger1( 'endedCallbacksForDragStarted', valueProperty.get() );
        }
      },

      drag: function( event, trail ) {
        if ( options.enabledProperty.get() ) {
          thisSlider.trigger1( 'startedCallbacksForDragged', valueProperty.get() );
          var transform = trail.subtrailTo( thisSlider ).getTransform(); // we only want the transform to our parent
          var x = transform.inversePosition2( event.pointer.point ).x - this.clickXOffset;
          var newValue = thisSlider.valueToPosition.inverse( x );

          valueProperty.set( options.constrainValue( newValue ) );

          thisSlider.trigger1( 'endedCallbacksForDragged', valueProperty.get() );
        }
      },

      end: function() {
        if ( options.enabledProperty.get() ) {
          thisSlider.trigger1( 'startedCallbacksForDragEnded', valueProperty.get() );
          options.endDrag();
          thisSlider.trigger1( 'endedCallbacksForDragEnded', valueProperty.get() );
        }
      }
    } );
    thumb.addInputListener( thumbHandler );

    // @private enable/disable
    var enabledObserver = function( enabled ) {
      thisSlider.cursor = options.enabledProperty.get() ? options.cursor : 'default';
      if ( !enabled ) {
        if ( thumbHandler.dragging ) { thumbHandler.endDrag(); }
        if ( trackHandler.dragging ) { trackHandler.endDrag(); }
      }
    };
    options.enabledProperty.link( enabledObserver ); // must be unlinked in disposeHSlider

    // @private update thumb location when value changes
    var valueObserver = function( value ) {
      thumb.centerX = thisSlider.valueToPosition( value );
    };
    valueProperty.link( valueObserver ); // must be unlinked in disposeHSlider

    // @private Called by dispose
    this.disposeHSlider = function() {
      thumb.dispose && thumb.dispose(); // in case a custom thumb is provided via options.thumbNode that doesn't implement dispose
      valueProperty.unlink( valueObserver );
      enabledProperty.unlink( enabledObserver );
      options.tandem && options.tandem.removeInstance( thisSlider );
    };

    thisSlider.mutate( options );

    options.tandem && options.tandem.addInstance( this );
  }

  inherit( Node, HSlider, {

    // Ensures that this object is eligible for GC.
    dispose: function() {
      this.disposeHSlider();
    },

    /**
     * Adds a major tick mark.
     * @param {number} value
     * @param {Node} [label] optional
     */
    addMajorTick: function( value, label ) {
      this.addTick( value, label, this.options.majorTickLength, this.options.majorTickStroke, this.options.majorTickLineWidth );
    },

    /**
     * Adds a minor tick mark.
     * @param {number} value
     * @param {Node} [label] optional
     */
    addMinorTick: function( value, label ) {
      this.addTick( value, label, this.options.minorTickLength, this.options.minorTickStroke, this.options.minorTickLineWidth );
    },

    /*
     * Adds a tick mark above the track.
     * @param {number} value
     * @param {Node} [label] optional
     * @param {number} length
     * @param {number} stroke
     * @param {number} lineWidth
     * @private
     */
    addTick: function( value, label, length, stroke, lineWidth ) {
      var labelX = this.valueToPosition( value );
      // ticks
      var tick = new Path( new Shape()
          .moveTo( labelX, this.track.top )
          .lineTo( labelX, this.track.top - length ),
        { stroke: stroke, lineWidth: lineWidth } );
      this.ticksParent.addChild( tick );
      // label
      if ( label ) {
        this.ticksParent.addChild( label );
        label.centerX = tick.centerX;
        label.bottom = tick.top - this.options.tickLabelSpacing;
      }
    },

    set enabled( value ) {
      this.enabledProperty.value = value;
    },

    get enabled() { return this.enabledProperty.value; }
  } );

  /**
   * Default thumb, a rectangle with a vertical white line down the center
   * @param {Object} [options] see HSlider constructor
   * @constructor
   */
  function ThumbNode( options ) {

    assert && assert( options ); // these are options provided to HSlider, not optional for this inner type

    var thisNode = this;

    // rectangle
    var arcWidth = 0.25 * options.thumbSize.width;
    Rectangle.call( thisNode,
      -options.thumbSize.width / 2, -options.thumbSize.height / 2,
      options.thumbSize.width, options.thumbSize.height,
      arcWidth, arcWidth,
      {
        fill: options.enabledProperty.get() ? options.thumbFillEnabled : options.thumbFillDisabled,
        stroke: options.thumbStroke,
        lineWidth: options.thumbLineWidth
      } );

    // vertical line down the center
    var centerLineYMargin = 3;
    thisNode.addChild( new Path( Shape.lineSegment(
        0, -( options.thumbSize.height / 2 ) + centerLineYMargin,
        0, ( options.thumbSize.height / 2 ) - centerLineYMargin ),
      { stroke: options.thumbCenterLineStroke } ) );

    // highlight thumb on pointer over
    thisNode.addInputListener( new ButtonListener( {
      over: function( event ) {
        if ( options.enabledProperty.get() ) { thisNode.fill = options.thumbFillHighlighted; }
      },
      up: function( event ) {
        if ( options.enabledProperty.get() ) { thisNode.fill = options.thumbFillEnabled; }
      }
    } ) );

    // @private enable/disable the look of the thumb
    var enabledObserver = function( enabled ) {
      thisNode.fill = enabled ? options.thumbFillEnabled : options.thumbFillDisabled;
    };
    enabledProperty.link( enabledObserver ); // must be unlinked in disposeThumbNode

    // @private Called by dispose
    this.disposeThumbNode = function() {
      enabledProperty.link( enabledObserver )
    }
  }

  inherit( Rectangle, ThumbNode, {

    // Ensures that this object is eligible for GC.
    dispose: function() {
      this.disposeThumbNode();
    }
  } );

  return HSlider;
} );<commit_msg>#182 handle options.enabledProperty correctly in ThumbNode<commit_after>// Copyright 2002-2013, University of Colorado Boulder

/**
 * Horizontal slider.
 *
 * Moved from beers-law-lab/EvaporationSlider on 9/15/2013
 * see https://github.com/phetsims/sun/issues/9
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var Dimension2 = require( 'DOT/Dimension2' );
  var inherit = require( 'PHET_CORE/inherit' );
  var LinearFunction = require( 'DOT/LinearFunction' );
  var Node = require( 'SCENERY/nodes/Node' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Property = require( 'AXON/Property' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var Shape = require( 'KITE/Shape' );
  var SimpleDragHandler = require( 'SCENERY/input/SimpleDragHandler' );
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var Input = require( 'SCENERY/input/Input' );
  var Util = require( 'DOT/Util' );

  /**
   * @param {Property.<number>} valueProperty
   * @param { {min:number, max:number} } range
   * @param {Object} [options]
   * @constructor
   */
  function HSlider( valueProperty, range, options ) {

    var thisSlider = this;
    Node.call( thisSlider );

    options = _.extend( {
      // track
      trackSize: new Dimension2( 100, 5 ),
      trackFill: 'white',
      trackStroke: 'black',
      trackLineWidth: 1,
      // {Node} optional thumb, replaces the default. Client is responsible for highlighting and disabling. Centered in the track.
      thumbNode: null,
      // default thumb (ignored if thumbNode is provided)
      thumbSize: new Dimension2( 22, 45 ),
      thumbFillEnabled: 'rgb(50,145,184)',
      thumbFillHighlighted: 'rgb(71,207,255)',
      thumbFillDisabled: '#F0F0F0',
      thumbStroke: 'black',
      thumbLineWidth: 1,
      thumbCenterLineStroke: 'white',
      // ticks
      tickLabelSpacing: 6,
      majorTickLength: 25,
      majorTickStroke: 'black',
      majorTickLineWidth: 1,
      minorTickLength: 10,
      minorTickStroke: 'black',
      minorTickLineWidth: 1,
      // other
      cursor: 'pointer',
      enabledProperty: new Property( true ),
      startDrag: function() {}, // called when a drag sequence starts
      endDrag: function() {}, // called when a drag sequence ends
      constrainValue: function( value ) { return value; }, // called before valueProperty is set
      // tandem
      tandem: null
    }, options );
    this.options = options; // @private TODO save only the options that are needed by prototype functions

    // @private ticks are added to this parent, so they are behind knob
    thisSlider.ticksParent = new Node();
    thisSlider.addChild( thisSlider.ticksParent );

    // @private mapping between value and track position
    thisSlider.valueToPosition = new LinearFunction( range.min, range.max, 0, options.trackSize.width, true /* clamp */ );

    // @private track
    thisSlider.track = new Rectangle( 0, 0, options.trackSize.width, options.trackSize.height,
      { fill: options.trackFill, stroke: options.trackStroke, lineWidth: options.trackLineWidth } );
    thisSlider.addChild( thisSlider.track );

    // click in the track to change the value, continue dragging if desired
    var trackHandler = new SimpleDragHandler( {
      handleTrackEvent: function( event, trail ) {
        if ( options.enabledProperty.get() ) {
          var transform = trail.subtrailTo( thisSlider ).getTransform();
          var x = transform.inversePosition2( event.pointer.point ).x;
          var value = thisSlider.valueToPosition.inverse( x );
          valueProperty.set( options.constrainValue( value ) );
        }
      },
      start: function( event, trail ) {
        if ( options.enabledProperty.get() ) {
          options.startDrag();
        }
        this.handleTrackEvent( event, trail );
      },
      drag: function( event, trail ) {
        this.handleTrackEvent( event, trail );
      },
      end: function() {
        if ( options.enabledProperty.get() ) {
          options.endDrag();
        }
      }
    } );
    thisSlider.track.addInputListener( trackHandler );

    // thumb
    var thumb = options.thumbNode || new ThumbNode( options );

    // Make the thumb focusable for keyboard accessibility
    thumb.focusable = true;

    thumb.centerY = thisSlider.track.centerY;
    thisSlider.addChild( thumb );

    // Keyboard accessibility
    thumb.addInputListener( {
      keydown: function( event, trail ) {
        var keyCode = event.domEvent.keyCode;
        var delta = keyCode === Input.KEY_LEFT_ARROW || keyCode === Input.KEY_DOWN_ARROW ? -1 :
                    keyCode === Input.KEY_RIGHT_ARROW || keyCode === Input.KEY_UP_ARROW ? +1 :
                    0;
        var clampedValue = Util.clamp( valueProperty.get() + (range.max - range.min) * 0.1 * delta, range.min, range.max );
        valueProperty.set( options.constrainValue( clampedValue ) );
      }
    } );

    // thumb touch area
    if ( !options.thumbNode ) {
      var dx = 0.5 * thumb.width;
      var dy = 0.25 * thumb.height;
      thumb.touchArea = Shape.rectangle( ( -thumb.width / 2 ) - dx, ( -thumb.height / 2 ) - dy, thumb.width + dx + dx, thumb.height + dy + dy );
    }

    // update value when thumb is dragged
    var thumbHandler = new SimpleDragHandler( {

      // x-offset between initial click and thumb's origin
      clickXOffset: 0,

      allowTouchSnag: true,

      start: function( event, trail ) {
        if ( options.enabledProperty.get() ) {
          thisSlider.trigger1( 'startedCallbacksForDragStarted', valueProperty.get() );
          options.startDrag();

          var transform = trail.subtrailTo( thisSlider ).getTransform();
          this.clickXOffset = transform.inversePosition2( event.pointer.point ).x - thumb.x;

          thisSlider.trigger1( 'endedCallbacksForDragStarted', valueProperty.get() );
        }
      },

      drag: function( event, trail ) {
        if ( options.enabledProperty.get() ) {
          thisSlider.trigger1( 'startedCallbacksForDragged', valueProperty.get() );
          var transform = trail.subtrailTo( thisSlider ).getTransform(); // we only want the transform to our parent
          var x = transform.inversePosition2( event.pointer.point ).x - this.clickXOffset;
          var newValue = thisSlider.valueToPosition.inverse( x );

          valueProperty.set( options.constrainValue( newValue ) );

          thisSlider.trigger1( 'endedCallbacksForDragged', valueProperty.get() );
        }
      },

      end: function() {
        if ( options.enabledProperty.get() ) {
          thisSlider.trigger1( 'startedCallbacksForDragEnded', valueProperty.get() );
          options.endDrag();
          thisSlider.trigger1( 'endedCallbacksForDragEnded', valueProperty.get() );
        }
      }
    } );
    thumb.addInputListener( thumbHandler );

    // @private enable/disable
    var enabledObserver = function( enabled ) {
      thisSlider.cursor = options.enabledProperty.get() ? options.cursor : 'default';
      if ( !enabled ) {
        if ( thumbHandler.dragging ) { thumbHandler.endDrag(); }
        if ( trackHandler.dragging ) { trackHandler.endDrag(); }
      }
    };
    options.enabledProperty.link( enabledObserver ); // must be unlinked in disposeHSlider

    // @private update thumb location when value changes
    var valueObserver = function( value ) {
      thumb.centerX = thisSlider.valueToPosition( value );
    };
    valueProperty.link( valueObserver ); // must be unlinked in disposeHSlider

    // @private Called by dispose
    this.disposeHSlider = function() {
      thumb.dispose && thumb.dispose(); // in case a custom thumb is provided via options.thumbNode that doesn't implement dispose
      valueProperty.unlink( valueObserver );
      enabledProperty.unlink( enabledObserver );
      options.tandem && options.tandem.removeInstance( thisSlider );
    };

    thisSlider.mutate( options );

    options.tandem && options.tandem.addInstance( this );
  }

  inherit( Node, HSlider, {

    // Ensures that this object is eligible for GC.
    dispose: function() {
      this.disposeHSlider();
    },

    /**
     * Adds a major tick mark.
     * @param {number} value
     * @param {Node} [label] optional
     */
    addMajorTick: function( value, label ) {
      this.addTick( value, label, this.options.majorTickLength, this.options.majorTickStroke, this.options.majorTickLineWidth );
    },

    /**
     * Adds a minor tick mark.
     * @param {number} value
     * @param {Node} [label] optional
     */
    addMinorTick: function( value, label ) {
      this.addTick( value, label, this.options.minorTickLength, this.options.minorTickStroke, this.options.minorTickLineWidth );
    },

    /*
     * Adds a tick mark above the track.
     * @param {number} value
     * @param {Node} [label] optional
     * @param {number} length
     * @param {number} stroke
     * @param {number} lineWidth
     * @private
     */
    addTick: function( value, label, length, stroke, lineWidth ) {
      var labelX = this.valueToPosition( value );
      // ticks
      var tick = new Path( new Shape()
          .moveTo( labelX, this.track.top )
          .lineTo( labelX, this.track.top - length ),
        { stroke: stroke, lineWidth: lineWidth } );
      this.ticksParent.addChild( tick );
      // label
      if ( label ) {
        this.ticksParent.addChild( label );
        label.centerX = tick.centerX;
        label.bottom = tick.top - this.options.tickLabelSpacing;
      }
    },

    set enabled( value ) {
      this.enabledProperty.value = value;
    },

    get enabled() { return this.enabledProperty.value; }
  } );

  /**
   * Default thumb, a rectangle with a vertical white line down the center
   * @param {Object} [options] see HSlider constructor
   * @constructor
   */
  function ThumbNode( options ) {

    assert && assert( options ); // these are options provided to HSlider, not optional for this inner type

    var thisNode = this;

    // rectangle
    var arcWidth = 0.25 * options.thumbSize.width;
    Rectangle.call( thisNode,
      -options.thumbSize.width / 2, -options.thumbSize.height / 2,
      options.thumbSize.width, options.thumbSize.height,
      arcWidth, arcWidth,
      {
        fill: options.enabledProperty.get() ? options.thumbFillEnabled : options.thumbFillDisabled,
        stroke: options.thumbStroke,
        lineWidth: options.thumbLineWidth
      } );

    // vertical line down the center
    var centerLineYMargin = 3;
    thisNode.addChild( new Path( Shape.lineSegment(
        0, -( options.thumbSize.height / 2 ) + centerLineYMargin,
        0, ( options.thumbSize.height / 2 ) - centerLineYMargin ),
      { stroke: options.thumbCenterLineStroke } ) );

    // highlight thumb on pointer over
    thisNode.addInputListener( new ButtonListener( {
      over: function( event ) {
        if ( options.enabledProperty.get() ) { thisNode.fill = options.thumbFillHighlighted; }
      },
      up: function( event ) {
        if ( options.enabledProperty.get() ) { thisNode.fill = options.thumbFillEnabled; }
      }
    } ) );

    // @private enable/disable the look of the thumb
    var enabledObserver = function( enabled ) {
      thisNode.fill = enabled ? options.thumbFillEnabled : options.thumbFillDisabled;
    };
    options.enabledProperty.link( enabledObserver ); // must be unlinked in disposeThumbNode

    // @private Called by dispose
    this.disposeThumbNode = function() {
      options.enabledProperty.link( enabledObserver )
    }
  }

  inherit( Rectangle, ThumbNode, {

    // Ensures that this object is eligible for GC.
    dispose: function() {
      this.disposeThumbNode();
    }
  } );

  return HSlider;
} );<|endoftext|>


<commit_before>(function() {
  const browser = typeof window !== 'undefined';

  let fetch;
  let FormData;
  let Package;
  if (browser) {
    fetch = window.fetch; // eslint-disable-line no-undef
    FormData = window.FormData; // eslint-disable-line no-undef
  } else {
    fetch = require('node-fetch');
    FormData = require('./FormData');
    Package = require('../package.json');
  }

  class Snekfetch {
    constructor(method, url) {
      this.url = url;
      this.method = method.toUpperCase();
      this.headers = {};
      this.data = null;
    }

    set(name, value) {
      if (name !== null && typeof name === 'object') {
        for (const key of Object.keys(name)) this.set(key, name[key]);
      } else {
        this.headers[name] = value;
      }
      return this;
    }

    attach(name, data, filename) {
      const form = this._getFormData();
      this.set('Content-Type', `multipart/form-data; boundary=${form.boundary}`);
      form.append(name, data, filename);
      this.data = form;
      return this;
    }

    send(data) {
      if (typeof data === 'object') {
        this.set('Content-Type', 'application/json');
        this.data = JSON.stringify(data);
      } else {
        this.data = data;
      }
      return this;
    }

    end(cb) {
      const recv = {
        text: '',
        body: {},
      };
      if (!Object.keys(this.headers).map(h => h.toLowerCase()).includes('user-agent')) {
        this.set('User-Agent', `snekfetch/${Snekfetch.version} (https://github.com/guscaplan/snekfetch)`);
      }
      const data = this.data ? this.data.end ? this.data.end() : this.data : null;
      return fetch(this.url, {
        method: this.method,
        headers: this.headers,
        body: data,
      }).then((res) => {
        const ctype = res.headers.get('Content-Type');
        if (ctype.includes('application/json')) {
          return res.text().then((t) => {
            recv.text = t;
            recv.body = JSON.parse(t);
            return res;
          });
        } else if (ctype.includes('application/x-www-form-urlencoded')) {
          return res.text().then((t) => {
            recv.text = t;
            recv.body = parseWWWFormUrlEncoded(t);
            return res;
          });
        } else {
          return (browser ? res.arrayBuffer() : res.buffer())
          .then((b) => {
            if (b instanceof ArrayBuffer) b = convertToBuffer(b);
            recv.body = b;
            recv.text = b.toString();
            return res;
          });
        }
      })
      .then((res) => {
        const response = Object.assign({}, res);
        response.body = recv.body;
        response.text = recv.text;
        response.raw = res.body;
        response.headers = {};
        if (res.headers.raw) {
          const headers = res.headers.raw();
          for (const key of Object.keys(headers)) response.headers[key] = headers[key][0];
        } else {
          for (const [name, value] of res.headers.entries()) response.headers[name] = value;
        }
        if (!res.ok) return Promise.reject(response);
        return cb(null, response);
      })
      .catch((err) => {
        let error = err;
        if (err.statusText) {
          error = new Error(`${err.status} ${err.statusText}`.trim());
          error.response = err;
          cb(error, err);
        } else {
          cb(error);
        }
      });
    }

    then(s, f) {
      return new Promise((resolve, reject) => {
        this.end((err, res) => {
          if (err) reject(f ? f(err) : err);
          else resolve(s ? s(res) : res);
        });
      });
    }

    catch(f) {
      return this.then(null, f);
    }

    _getFormData() {
      if (!this._formData) this._formData = new FormData();
      return this._formData;
    }
  }

  if (!browser) Snekfetch.version = Package.version;

  const methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH', 'BREW'];
  for (let method of methods) {
    method = method.toLowerCase();
    Snekfetch[method] = (url) => new Snekfetch(method, url);
    if (browser) continue;
    Snekfetch[`${method}Sync`] = (url, options = {}) => {
      options.url = url;
      options.method = method;
      const cp = require('child_process');
      const result = JSON.parse(
        cp.execSync(`node ${__dirname}/index.js`, {
          env: { __SNEKFETCH_IS_HISSING_AT_YOU: JSON.stringify(options) },
        }).toString(),
        (k, v) => {
          if (v === null) return v;
          if (v.type === 'Buffer' && Array.isArray(v.data)) return new Buffer(v.data);
          if (v.__CONVERT_TO_ERROR) {
            const e = new Error();
            for (const key of Object.keys(v)) {
              if (key === '__CONVERT_TO_ERROR') continue;
              e[key] = v[key];
            }
            return e;
          }
          return v;
        }
      );
      if (result.error) throw result.error;
      return result;
    };
  }

  if (!browser && process.env.__SNEKFETCH_IS_HISSING_AT_YOU) {
    const options = JSON.parse(process.env.__SNEKFETCH_IS_HISSING_AT_YOU);
    const request = Snekfetch[options.method](options.url);
    if (options.headers) request.set(options.headers);
    if (options.body) request.send(options.body);
    if (options.form) {
      for (const key of Object.keys(options.form)) request.attach(key, options[key].name, options[key].filename);
    }
    request.end((err, res = {}) => {
      if (err) {
        const alt = {};
        for (const name of Object.getOwnPropertyNames(err)) alt[name] = err[name];
        res.error = alt;
        res.error.__CONVERT_TO_ERROR = true;
      }
      process.stdout.write(JSON.stringify(res));
    });
  }

  if (browser) window.Snekfetch = Snekfetch;
  if (typeof module !== 'undefined') module.exports = Snekfetch;

  function convertToBuffer(ab) {
    function str2ab(str) {
      const buffer = new ArrayBuffer(str.length * 2);
      const view = new Uint16Array(buffer);
      for (var i = 0, strLen = str.length; i < strLen; i++) view[i] = str.charCodeAt(i);
      return buffer;
    }

    if (typeof ab === 'string') ab = str2ab(ab);
    return Buffer.from(ab);
  }

  function parseWWWFormUrlEncoded(str) {
    const obj = {};
    for (const [k, v] of str.split('&').map(q => q.split('='))) obj[k] = v;
    return obj;
  }
}());
<commit_msg>Delete ded.js<commit_after><|endoftext|>


<commit_before>// Copyright 2021, University of Colorado Boulder

/**
 * Return an object of data about the simulation and the browser
 * much of the code was largely copied and expanded on from SimTroubleshootPage.html in the website repo. Note that
 * key names in the info object are used by the PhET-iO API, do not change without great consideration.
 *
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */

import Utils from '../../scenery/js/util/Utils.js';
import PhetioObject from '../../tandem/js/PhetioObject.js';
import Tandem from '../../tandem/js/Tandem.js';
import ArrayIO from '../../tandem/js/types/ArrayIO.js';
import IOType from '../../tandem/js/types/IOType.js';
import NullableIO from '../../tandem/js/types/NullableIO.js';
import ObjectLiteralIO from '../../tandem/js/types/ObjectLiteralIO.js';
import StringIO from '../../tandem/js/types/StringIO.js';
import joist from './joist.js';
import packageJSON from './packageJSON.js';

class SimInfo extends PhetioObject {

  constructor( sim ) {
    super( {
      tandem: Tandem.GENERAL_MODEL.createTandem( 'simInfo' ),
      phetioType: SimInfo.SimInfoIO,
      phetioReadOnly: true,
      phetioDocumentation: 'A collection of data about the runtime and simulation. Available in the simStarted PhET-iO ' +
                           'data stream event, as well as on demand in the PhET-iO state.'
    } );

    assert && assert( Array.isArray( sim.screens ), 'screens should be set and an array' );

    // @public (SimInfoIO)
    this.info = {};

    // globals
    this.putInfo( 'url', window.location.href );
    this.putInfo( 'randomSeed', window.phet.chipper.queryParameters.randomSeed );
    this.putInfo( 'userAgent', window.navigator.userAgent );
    this.putInfo( 'language', window.navigator.language );
    this.putInfo( 'window', `${window.innerWidth}x${window.innerHeight}` );
    this.putInfo( 'referrer', document.referrer );

    // from Scenery Utils
    this.putInfo( 'checkIE11StencilSupport', Utils.checkIE11StencilSupport() );
    if ( phet.chipper.queryParameters.webgl ) {
      this.putInfo( 'isWebGLSupported', Utils.isWebGLSupported );
    }

    let canvas;
    let context;
    let backingStorePixelRatio;

    try {
      canvas = document.createElement( 'canvas' );
      context = canvas.getContext( '2d' );
      backingStorePixelRatio = Utils.backingStorePixelRatio( context );
    }
    catch( e ) {} // eslint-disable-line

    this.putInfo( 'pixelRatio', `${window.devicePixelRatio || 1}/${backingStorePixelRatio}` );

    const flags = [];
    if ( window.navigator.pointerEnabled ) { flags.push( 'pointerEnabled' ); }
    if ( window.navigator.msPointerEnabled ) { flags.push( 'msPointerEnabled' ); }
    if ( !window.navigator.onLine ) { flags.push( 'offline' ); }
    if ( ( window.devicePixelRatio || 1 ) / backingStorePixelRatio !== 1 ) { flags.push( 'pixelRatioScaling' ); }
    this.putInfo( 'flags', flags.join( ', ' ) );

    canvas = null; // dispose only reference

    // from Sim.js
    this.putInfo( 'simName', sim.simNameProperty.value );
    this.putInfo( 'simVersion', sim.version );
    this.putInfo( 'repoName', packageJSON.name );
    this.putInfo( 'screens', sim.screens.map( screen => {
      const screenObject = {

        // likely null for single screen sims, so use the sim name as a default
        name: screen.nameProperty.value || sim.simNameProperty.value
      };
      if ( Tandem.PHET_IO_ENABLED ) {
        screenObject.phetioID = screen.tandem.phetioID;
      }
      return screenObject;
    } ) );

    // From PhET-iO code
    if ( Tandem.PHET_IO_ENABLED ) {
      this.putInfo( 'screenPropertyValue', sim.screenProperty.value.tandem.phetioID );
      this.putInfo( 'wrapperMetadata', phet.preloads.phetio.simStartedMetadata );
      this.putInfo( 'dataStreamVersion', phet.phetio.dataStream.VERSION );
      this.putInfo( 'phetioCommandProcessorProtocol', phet.phetio.phetioCommandProcessor.PHET_IO_PROTOCOL );
    }
  }

  /**
   * @private
   * @param {string} key
   * @param {*} value
   */
  putInfo( key, value ) {
    if ( value === undefined ) {
      value = '{{undefined}}';
    }
    assert && assert( !this.info.hasOwnProperty( key ), `key already defined: ${key}` );
    this.info[ key ] = value;
  }
}

// @private
SimInfo.SimInfoIO = new IOType( 'SimInfoIO', {
  valueType: SimInfo,
  toStateObject: simInfo => {
    return {
      simName: simInfo.info.simName,
      screens: simInfo.info.screens,
      simVersion: simInfo.info.simVersion,
      repoName: simInfo.info.repoName,

      screenPropertyValue: simInfo.info.screenPropertyValue,
      wrapperMetadata: simInfo.info.wrapperMetadata,
      dataStreamVersion: simInfo.info.dataStreamVersion,
      phetioCommandProcessorProtocol: simInfo.info.phetioCommandProcessorProtocol
    };
  },
  stateSchema: {
    simName: StringIO,
    screens: ArrayIO( ObjectLiteralIO ),
    simVersion: StringIO,
    repoName: StringIO,

    screenPropertyValue: StringIO,
    wrapperMetadata: NullableIO( ObjectLiteralIO ),
    dataStreamVersion: StringIO,
    phetioCommandProcessorProtocol: StringIO
  }
} );

joist.register( 'SimInfo', SimInfo );
export default SimInfo;<commit_msg>Remove simVersion from SimInfo PhET-iO state, see https://github.com/phetsims/gravity-and-orbits/issues/386<commit_after>// Copyright 2021, University of Colorado Boulder

/**
 * Return an object of data about the simulation and the browser
 * much of the code was largely copied and expanded on from SimTroubleshootPage.html in the website repo. Note that
 * key names in the info object are used by the PhET-iO API, do not change without great consideration.
 *
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */

import Utils from '../../scenery/js/util/Utils.js';
import PhetioObject from '../../tandem/js/PhetioObject.js';
import Tandem from '../../tandem/js/Tandem.js';
import ArrayIO from '../../tandem/js/types/ArrayIO.js';
import IOType from '../../tandem/js/types/IOType.js';
import NullableIO from '../../tandem/js/types/NullableIO.js';
import ObjectLiteralIO from '../../tandem/js/types/ObjectLiteralIO.js';
import StringIO from '../../tandem/js/types/StringIO.js';
import joist from './joist.js';
import packageJSON from './packageJSON.js';

class SimInfo extends PhetioObject {

  constructor( sim ) {
    super( {
      tandem: Tandem.GENERAL_MODEL.createTandem( 'simInfo' ),
      phetioType: SimInfo.SimInfoIO,
      phetioReadOnly: true,
      phetioDocumentation: 'A collection of data about the runtime and simulation. Available in the simStarted PhET-iO ' +
                           'data stream event, as well as on demand in the PhET-iO state.'
    } );

    assert && assert( Array.isArray( sim.screens ), 'screens should be set and an array' );

    // @public (SimInfoIO)
    this.info = {};

    // globals
    this.putInfo( 'url', window.location.href );
    this.putInfo( 'randomSeed', window.phet.chipper.queryParameters.randomSeed );
    this.putInfo( 'userAgent', window.navigator.userAgent );
    this.putInfo( 'language', window.navigator.language );
    this.putInfo( 'window', `${window.innerWidth}x${window.innerHeight}` );
    this.putInfo( 'referrer', document.referrer );

    // from Scenery Utils
    this.putInfo( 'checkIE11StencilSupport', Utils.checkIE11StencilSupport() );
    if ( phet.chipper.queryParameters.webgl ) {
      this.putInfo( 'isWebGLSupported', Utils.isWebGLSupported );
    }

    let canvas;
    let context;
    let backingStorePixelRatio;

    try {
      canvas = document.createElement( 'canvas' );
      context = canvas.getContext( '2d' );
      backingStorePixelRatio = Utils.backingStorePixelRatio( context );
    }
    catch( e ) {} // eslint-disable-line

    this.putInfo( 'pixelRatio', `${window.devicePixelRatio || 1}/${backingStorePixelRatio}` );

    const flags = [];
    if ( window.navigator.pointerEnabled ) { flags.push( 'pointerEnabled' ); }
    if ( window.navigator.msPointerEnabled ) { flags.push( 'msPointerEnabled' ); }
    if ( !window.navigator.onLine ) { flags.push( 'offline' ); }
    if ( ( window.devicePixelRatio || 1 ) / backingStorePixelRatio !== 1 ) { flags.push( 'pixelRatioScaling' ); }
    this.putInfo( 'flags', flags.join( ', ' ) );

    canvas = null; // dispose only reference

    // from Sim.js
    this.putInfo( 'simName', sim.simNameProperty.value );
    this.putInfo( 'simVersion', sim.version );
    this.putInfo( 'repoName', packageJSON.name );
    this.putInfo( 'screens', sim.screens.map( screen => {
      const screenObject = {

        // likely null for single screen sims, so use the sim name as a default
        name: screen.nameProperty.value || sim.simNameProperty.value
      };
      if ( Tandem.PHET_IO_ENABLED ) {
        screenObject.phetioID = screen.tandem.phetioID;
      }
      return screenObject;
    } ) );

    // From PhET-iO code
    if ( Tandem.PHET_IO_ENABLED ) {
      this.putInfo( 'screenPropertyValue', sim.screenProperty.value.tandem.phetioID );
      this.putInfo( 'wrapperMetadata', phet.preloads.phetio.simStartedMetadata );
      this.putInfo( 'dataStreamVersion', phet.phetio.dataStream.VERSION );
      this.putInfo( 'phetioCommandProcessorProtocol', phet.phetio.phetioCommandProcessor.PHET_IO_PROTOCOL );
    }
  }

  /**
   * @private
   * @param {string} key
   * @param {*} value
   */
  putInfo( key, value ) {
    if ( value === undefined ) {
      value = '{{undefined}}';
    }
    assert && assert( !this.info.hasOwnProperty( key ), `key already defined: ${key}` );
    this.info[ key ] = value;
  }
}

// @private
SimInfo.SimInfoIO = new IOType( 'SimInfoIO', {
  valueType: SimInfo,
  toStateObject: simInfo => {
    return {
      simName: simInfo.info.simName,
      screens: simInfo.info.screens,
      repoName: simInfo.info.repoName,

      screenPropertyValue: simInfo.info.screenPropertyValue,
      wrapperMetadata: simInfo.info.wrapperMetadata,
      dataStreamVersion: simInfo.info.dataStreamVersion,
      phetioCommandProcessorProtocol: simInfo.info.phetioCommandProcessorProtocol
    };
  },
  stateSchema: {
    simName: StringIO,
    screens: ArrayIO( ObjectLiteralIO ),
    repoName: StringIO,

    screenPropertyValue: StringIO,
    wrapperMetadata: NullableIO( ObjectLiteralIO ),
    dataStreamVersion: StringIO,
    phetioCommandProcessorProtocol: StringIO
  }
} );

joist.register( 'SimInfo', SimInfo );
export default SimInfo;<|endoftext|>


<commit_before>// Copyright 2018, University of Colorado Boulder

/**
 * "definition" type for a "Type" that a value can be validated against.
 * TypeDef accepts multiple ways to test
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */

define( require => {
  'use strict';

  const ObjectIO = require( 'TANDEM/types/ObjectIO' );
  const axon = require( 'AXON/axon' );

  const TypeDef = {
    /**
     * Returns whether the parameter is considered to be a TypeDef.
     * @public
     *
     * @param {*} type
     * @returns {boolean}
     */
    isTypeDef( type ) {
      if ( typeof type === 'function' ) {
        if ( type.isPredicate || !type.name ) { // TODO: are these the best criteria?
          return typeof type() === 'boolean'; // should return a boolean is a predicate function
        }

        // Here we are assuming that whatever the type is will be tested with `instanceof`,
        // though there is no way to check if a function is constructable since not all phet constructors use
        // `class`, see https://stackoverflow.com/a/40922715
        return true;
      }
      if ( ( type === ObjectIO || type.prototype instanceof ObjectIO ) && // is a TypeIO
           typeof type.isInstance === 'function' && // the TypeIO has a isInstance predicate
           typeof type.isInstance() === 'boolean' ) {
        return true;
      }
      return typeof type === 'string'; // like 'number' or 'string'
    },

    /**
     * Test if a value is valid
     * @param {*} value
     * @param {TypeDef} valueType
     * @returns {boolean} - if the value is a valid one given the valueType
     */
    validValue( value, valueType ) {
      if ( typeof valueType === 'string' ) {

        // primitive type
        return typeof value === valueType;
      }

      // If valueType is a TypeIO, then it should have its own validating predicate.
      // Test to see if valueType is a TypeIO, see https://stackoverflow.com/questions/18939192/how-to-test-if-b-is-a-subclass-of-a-in-javascript-node
      else if ( valueType === ObjectIO || valueType.prototype instanceof ObjectIO ) {
        assert && assert( valueType.isInstance, 'unsupported phet-io value type validation.' );
        return valueType.isInstance( value );
      }
      else if ( typeof valueType === 'function' ) {

        // support predicate functions, if passing in an anonymous function, then assume it is a predicate. Also support
        // a `isPredicate` property marker on functions.
        if ( valueType.isPredicate || !valueType.name ) {
          return valueType( value );
        }

        // constructor like `Node` or `Path`
        else {
          return value instanceof valueType;
        }
      }

      // value was not of correct type, or valueType was of an unexpected kind. TODO: error out instead?
      return false;
    },

    /**
     * Given a list of valid values, return a predicate that, when given anything, tests if it an item in the list.
     * An example of a potential "Validator" function in https://github.com/phetsims/axon/issues/189#issuecomment-433183909
     * similar in function to Property.validValues (perhaps that is deprecated).
     *
     * @param {Array} list
     * @returns {function: boolean}
     */
    predicateFromArray( list ) {
      assert && assert( Array.isArray( list ) );
      const predicate = ( value ) => { return _.includes( list, value );};
      predicate.isPredicate = true; // add on this marking so that it is known how to test it.
      return predicate;
    },

    /**
     * Return a predicate that accepts null or the typeof the typeString provided
     * @param typeString
     * @returns {function(*=): boolean}
     */
    getNullOrTypeofPredicate( typeString ) {
      assert && assert( typeof typeString === 'string' );
      const predicate = v => v === null || typeof v === typeString;
      predicate.isPredicate = true;
      return predicate;
    },

    /**
     * Return a predicate that accepts null or the typeof the typeString provided
     * @param typeString
     * @returns {function(*=): boolean}
     */
    getNullOrInstanceOfPredicate( type ) {
      assert && assert( typeof type === 'function' );
      const predicate = v => v === null || v instanceof type;
      predicate.isPredicate = true;
      return predicate;
    }

  };

  axon.register( 'TypeDef', TypeDef );

  return TypeDef;
} );
<commit_msg>remove type.name check, no support on IE11, #189<commit_after>// Copyright 2018, University of Colorado Boulder

/**
 * "definition" type for a "Type" that a value can be validated against.
 * TypeDef accepts multiple ways to test
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */

define( require => {
  'use strict';

  const ObjectIO = require( 'TANDEM/types/ObjectIO' );
  const axon = require( 'AXON/axon' );

  const TypeDef = {
    /**
     * Returns whether the parameter is considered to be a TypeDef.
     * @public
     *
     * @param {*} type
     * @returns {boolean}
     */
    isTypeDef( type ) {
      if ( typeof type === 'function' ) {
        if ( type.isPredicate ) { // TODO: are these the best criteria? Function.name ISN'T SUPPORTED ON IE11
          return typeof type() === 'boolean'; // should return a boolean is a predicate function
        }

        // Here we are assuming that whatever the type is will be tested with `instanceof`,
        // though there is no way to check if a function is constructable since not all phet constructors use
        // `class`, see https://stackoverflow.com/a/40922715
        return true;
      }
      if ( ( type === ObjectIO || type.prototype instanceof ObjectIO ) && // is a TypeIO
           typeof type.isInstance === 'function' && // the TypeIO has a isInstance predicate
           typeof type.isInstance() === 'boolean' ) {
        return true;
      }
      return typeof type === 'string'; // like 'number' or 'string'
    },

    /**
     * Test if a value is valid
     * @param {*} value
     * @param {TypeDef} valueType
     * @returns {boolean} - if the value is a valid one given the valueType
     */
    validValue( value, valueType ) {
      if ( typeof valueType === 'string' ) {

        // primitive type
        return typeof value === valueType;
      }

      // If valueType is a TypeIO, then it should have its own validating predicate.
      // Test to see if valueType is a TypeIO, see https://stackoverflow.com/questions/18939192/how-to-test-if-b-is-a-subclass-of-a-in-javascript-node
      else if ( valueType === ObjectIO || valueType.prototype instanceof ObjectIO ) {
        assert && assert( valueType.isInstance, 'unsupported phet-io value type validation.' );
        return valueType.isInstance( value );
      }
      else if ( typeof valueType === 'function' ) {

        // support predicate functions, if passing in an anonymous function, then assume it is a predicate. Also support
        // a `isPredicate` property marker on functions.
        if ( valueType.isPredicate || !valueType.name ) {
          return valueType( value );
        }

        // constructor like `Node` or `Path`
        else {
          return value instanceof valueType;
        }
      }

      // value was not of correct type, or valueType was of an unexpected kind. TODO: error out instead?
      return false;
    },

    /**
     * Given a list of valid values, return a predicate that, when given anything, tests if it an item in the list.
     * An example of a potential "Validator" function in https://github.com/phetsims/axon/issues/189#issuecomment-433183909
     * similar in function to Property.validValues (perhaps that is deprecated).
     *
     * @param {Array} list
     * @returns {function: boolean}
     */
    predicateFromArray( list ) {
      assert && assert( Array.isArray( list ) );
      const predicate = ( value ) => { return _.includes( list, value );};
      predicate.isPredicate = true; // add on this marking so that it is known how to test it.
      return predicate;
    },

    /**
     * Return a predicate that accepts null or the typeof the typeString provided
     * @param typeString
     * @returns {function(*=): boolean}
     */
    getNullOrTypeofPredicate( typeString ) {
      assert && assert( typeof typeString === 'string' );
      const predicate = v => v === null || typeof v === typeString;
      predicate.isPredicate = true;
      return predicate;
    },

    /**
     * Return a predicate that accepts null or the typeof the typeString provided
     * @param typeString
     * @returns {function(*=): boolean}
     */
    getNullOrInstanceOfPredicate( type ) {
      assert && assert( typeof type === 'function' );
      const predicate = v => v === null || v instanceof type;
      predicate.isPredicate = true;
      return predicate;
    }

  };

  axon.register( 'TypeDef', TypeDef );

  return TypeDef;
} );
<|endoftext|>