<commit_before>/*
 * The MIT License
 *
 * Copyright (c) 2016, Sebastian Sdorra
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

'use strict';

angular.module('adf.widget.scm')
  .factory('SCM', function($http){

    var endpoint = '/api/scm/';

    function data(response){
      return response.data;
    };

    function request(url){
      return $http.get(endpoint + url).then(data);
    };

    function getRepositories(){
      return request('repositories.json');
    };

    function getRepository(id){
      return request('repositories/' + id + '.json');
    };

    function getCommitsByAuthor(id){
      return request('plugins/statistic/' + id + '/commits-per-author.json');
    };

    function getCommitsByMonth(id){
      return request('plugins/statistic/' + id + '/commits-per-month.json');
    };

    function getCommits(id, limit){
      return request('repositories/' + id + '/changesets.json?limit=' + limit).then(function(data){
        return data.changesets;
      });
    };

    return {
      getRepositories: getRepositories,
      getRepository: getRepository,
      getCommitsByAuthor: getCommitsByAuthor,
      getCommitsByMonth: getCommitsByMonth,
      getCommits: getCommits
    };
  });
<commit_msg>fix some linters<commit_after>/*
 * The MIT License
 *
 * Copyright (c) 2016, Sebastian Sdorra
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

'use strict';

angular.module('adf.widget.scm')
  .factory('SCM', function($http){

    var endpoint = '/api/scm/';

    function data(response){
      return response.data;
    }

    function request(url){
      return $http.get(endpoint + url).then(data);
    }

    function getRepositories(){
      return request('repositories.json');
    }

    function getRepository(id){
      return request('repositories/' + id + '.json');
    }

    function getCommitsByAuthor(id){
      return request('plugins/statistic/' + id + '/commits-per-author.json');
    }

    function getCommitsByMonth(id){
      return request('plugins/statistic/' + id + '/commits-per-month.json');
    }

    function getCommits(id, limit){
      return request('repositories/' + id + '/changesets.json?limit=' + limit).then(function(data){
        return data.changesets;
      });
    }

    return {
      getRepositories: getRepositories,
      getRepository: getRepository,
      getCommitsByAuthor: getCommitsByAuthor,
      getCommitsByMonth: getCommitsByMonth,
      getCommits: getCommits
    };
  });
<|endoftext|>


<commit_before>'use strict';

const _ = require('underscore');
const uuid = require('node-uuid');

const Subscription = require('./subscription');

/**
 * A Session represents the state for a given session. Currently, this is
 * primarily a wrapper for a spark and the subscriptions that a connection
 * has subscribed to.
 */
class Session {

  /**
   * Constructs a session for a given Primus spark.
   *
   * @param {Object} server The PublicationServer that created this Session.
   * @param {Object} spark The Primus spark that this session has access to.
   */
  constructor({server, spark} = {}) {
    this.server = server;
    this.spark = spark;
    this._subscriptions = {};

    this.handlers = {
      sub: this.subscribe.bind(this),
      unsub: this.unsubscribe.bind(this),
      connect: this.connect.bind(this),
      ping: this.ping.bind(this)
    };

    // Set some state on the session.
    this._waitingForConnect = true;
    this.userId = spark.request.userId;
    this.isRunning = true;
    this.attachEventHandlers();
  }

  /**
   * Sends the given payload over the spark.
   * @param {Object} payload The payload to send over the spark. This must be
   *    EJSON serializable.
   */
  send(payload) {
    this.spark.write(payload);
  }

  /**
   * @typedef {Object} ConnectMessage
   * @property {String} msg The message type.
   * @property {String} version The DDP protocol version the client would like
   *    to use.
   * @property {String} session An optional session ID, only provided if the
   *    client is attempting to reconnect.
   * @property {String[]} support Protocol versions that the client supports.
   */

  /**
   * Handles incoming connect messages as specified in the spec:
   * https://github.com/meteor/meteor/blob/master/packages/ddp/DDP.md#messages
   * @param {ConnectMessage} msg The connect message.
   */
  connect(msg) {
    // We only support version '1' currently, not 'pre1' or 'pre2'.
    if (msg.version !== '1') {
      this.send({
        msg: 'failed',
        version: '1'
      });
      this.stop();
      return;
    }

    if (msg.session) {
      this._sessionId = msg.session;
    } else {
      this._sessionId = uuid.v4();
    }

    this._waitingForConnect = false;
    this.send({
      msg: 'connected',
      session: this._sessionId
    });
  }

  /**
   * Received a ping request from the client.
   */
  ping(msg) {
    var resp = {
      msg: 'pong'
    };
    if (msg.id) resp.id = msg.id;
    this.send(resp);
  }

  /**
   * @typedef {Object} SubMessage
   * @property {String} msg The message type.
   * @property {String} id The subscription ID (assigned by the client).
   * @property {String} name The name of the publication to subscribe to.
   * @property {Array} params Params to be passed to the publication.
   */

  /**
   * Subscribes the client to the specified publication.
   * @param {SubMessage} msg The subscription message.
   */
  subscribe(msg) {
    if (this._waitingForConnect) {
      this.send({
        msg: 'error',
        reason: 'connect-must-be-first',
        offendingMessage: msg
      });
      return;
    }

    if (this._subscriptions[msg.id]) {
      // This subscription is already open.
      return;
    }

    const name = msg.name;
    let params = msg.params;
    // Attempt to parse string params, since a JSON object is required.
    if (typeof params === 'string') params = JSON.parse(params);
    const handler = this.server._subscriptions[name];
    // If the publication doesn't exist, reply with `nosub`.
    if (!handler) {
      this.send({
        msg: 'nosub',
        id: msg.id,
        error: 'sub-not-found'
      });
      return;
    }

    // Create the subscription, register it and then start it.
    let subscription = new Subscription({
      session: this,
      handler,
      name,
      params,
      id: msg.id
    });
    this._subscriptions[msg.id] = subscription;
    subscription.start();
  }

  /**
   * @typedef {UnsubMessage}
   * @property {String} msg The message type (`unsub`).
   * @property {String} id The subscription ID.
   */

  /**
   * Unsubscribes the client from the desired publication.
   * @param {UnsubMessage} msg The unsubscribe message.
   */
  unsubscribe(msg) {
    if (this._waitingForConnect) return;

    let sub = this._subscriptions[msg.id];
    if (!sub) {
      // No open subscription with the given name.
      this.send({
        msg: 'error',
        reason: 'no-such-subscription-to-unsub',
        offendingMessage: msg
      });
      return;
    }

    sub.stop();
    delete this._subscriptions[msg.id];
  }

  /**
   * Processes the next message.
   */
  processMsg(msg) {
    // If we're not running, return. We can't throw an error as it's possible
    // that we:
    //   - received a new message and enqueued it
    //   - the connection dropped
    //   - we then tried to process the message
    if (!this.isRunning) {
      return;
    }

    // Find the correct handler if it exists.
    let handler = this.handlers[msg.msg];
    if (!handler) {
      this.send({
        msg: 'error',
        reason: 'unknown-msg-type',
        offendingMessage: msg
      });
      return;
    }

    handler(msg);
  }

  /**
   * Stops the session. This includes (in the given order):
   *   - marking the session as no longer running
   *   - stopping all current subscriptions
   *   - closes the WebSocket
   */
  stop() {
    // Someone already called stop.
    if (!this.isRunning) return;
    this.isRunning = false;

    _.invoke(this._subscriptions, 'stop');

    this.spark.end();
  }

  /**
   * Attaches the event handles to the WebSocket.
   */
  attachEventHandlers() {
    this.spark.on('end', () => {
      this.stop();
    });

    this.spark.on('error', () => {
      this.stop();
    });

    this.spark.on('data', (data) => {
      if (!data.msg) {
        // If no msg type was sent, ignore it.
        this.send({
          msg: 'error',
          reason: 'must-provide-msg-type',
          offendingMessage: data
        });
        return;
      }

      process.nextTick((msg) => {
        this.processMsg(msg);
      }, data);
    });
  }
}

module.exports = Session;
<commit_msg>Add safety belts and send error messages<commit_after>'use strict';

const _ = require('underscore');
const uuid = require('node-uuid');

const Subscription = require('./subscription');

/**
 * A Session represents the state for a given session. Currently, this is
 * primarily a wrapper for a spark and the subscriptions that a connection
 * has subscribed to.
 */
class Session {

  /**
   * Constructs a session for a given Primus spark.
   *
   * @param {Object} server The PublicationServer that created this Session.
   * @param {Object} spark The Primus spark that this session has access to.
   */
  constructor({server, spark} = {}) {
    this.server = server;
    this.spark = spark;
    this._subscriptions = {};

    this.handlers = {
      sub: this.subscribe.bind(this),
      unsub: this.unsubscribe.bind(this),
      connect: this.connect.bind(this),
      ping: this.ping.bind(this)
    };

    // Set some state on the session.
    this._waitingForConnect = true;
    this.userId = spark.request.userId;
    this.isRunning = true;
    this.attachEventHandlers();
  }

  /**
   * Sends the given payload over the spark.
   * @param {Object} payload The payload to send over the spark. This must be
   *    EJSON serializable.
   */
  send(payload) {
    this.spark.write(payload);
  }

  /**
   * @typedef {Object} ConnectMessage
   * @property {String} msg The message type.
   * @property {String} version The DDP protocol version the client would like
   *    to use.
   * @property {String} session An optional session ID, only provided if the
   *    client is attempting to reconnect.
   * @property {String[]} support Protocol versions that the client supports.
   */

  /**
   * Handles incoming connect messages as specified in the spec:
   * https://github.com/meteor/meteor/blob/master/packages/ddp/DDP.md#messages
   * @param {ConnectMessage} msg The connect message.
   */
  connect(msg) {
    // We only support version '1' currently, not 'pre1' or 'pre2'.
    if (msg.version !== '1') {
      this.send({
        msg: 'failed',
        version: '1'
      });
      this.stop();
      return;
    }

    if (msg.session) {
      this._sessionId = msg.session;
    } else {
      this._sessionId = uuid.v4();
    }

    this._waitingForConnect = false;
    this.send({
      msg: 'connected',
      session: this._sessionId
    });
  }

  /**
   * Received a ping request from the client.
   */
  ping(msg) {
    var resp = {
      msg: 'pong'
    };
    if (msg.id) resp.id = msg.id;
    this.send(resp);
  }

  /**
   * @typedef {Object} SubMessage
   * @property {String} msg The message type.
   * @property {String} id The subscription ID (assigned by the client).
   * @property {String} name The name of the publication to subscribe to.
   * @property {Array} params Params to be passed to the publication.
   */

  /**
   * Subscribes the client to the specified publication.
   * @param {SubMessage} msg The subscription message.
   */
  subscribe(msg) {
    if (this._waitingForConnect) {
      this.send({
        msg: 'error',
        reason: 'connect-must-be-first',
        offendingMessage: msg
      });
      return;
    }

    if (this._subscriptions[msg.id]) {
      // This subscription is already open.
      return;
    }

    const name = msg.name;
    let params = msg.params;
    // Attempt to parse string params, since a JSON array is required.
    if (_.isString(params)) {
      try {
        params = JSON.parse(params);
      } catch (err) {
        this.send({
          msg: 'error',
          reason: 'unable-to-parse-params',
          offendingMessage: msg
        });
        return;
      }

      if (!_.isArray(params)) {
        this.send({
          msg: 'error',
          reason: 'invalid-params',
          offendingMessage: msg
        });
        return;
      }
    }

    const handler = this.server._subscriptions[name];
    // If the publication doesn't exist, reply with `nosub`.
    if (!handler) {
      this.send({
        msg: 'nosub',
        id: msg.id,
        error: 'sub-not-found'
      });
      return;
    }

    // Create the subscription, register it and then start it.
    let subscription = new Subscription({
      session: this,
      handler,
      name,
      params,
      id: msg.id
    });
    this._subscriptions[msg.id] = subscription;
    subscription.start();
  }

  /**
   * @typedef {UnsubMessage}
   * @property {String} msg The message type (`unsub`).
   * @property {String} id The subscription ID.
   */

  /**
   * Unsubscribes the client from the desired publication.
   * @param {UnsubMessage} msg The unsubscribe message.
   */
  unsubscribe(msg) {
    if (this._waitingForConnect) return;

    let sub = this._subscriptions[msg.id];
    if (!sub) {
      // No open subscription with the given name.
      this.send({
        msg: 'error',
        reason: 'no-such-subscription-to-unsub',
        offendingMessage: msg
      });
      return;
    }

    sub.stop();
    delete this._subscriptions[msg.id];
  }

  /**
   * Processes the next message.
   */
  processMsg(msg) {
    // If we're not running, return. We can't throw an error as it's possible
    // that we:
    //   - received a new message and enqueued it
    //   - the connection dropped
    //   - we then tried to process the message
    if (!this.isRunning) {
      return;
    }

    // Find the correct handler if it exists.
    let handler = this.handlers[msg.msg];
    if (!handler) {
      this.send({
        msg: 'error',
        reason: 'unknown-msg-type',
        offendingMessage: msg
      });
      return;
    }

    handler(msg);
  }

  /**
   * Stops the session. This includes (in the given order):
   *   - marking the session as no longer running
   *   - stopping all current subscriptions
   *   - closes the WebSocket
   */
  stop() {
    // Someone already called stop.
    if (!this.isRunning) return;
    this.isRunning = false;

    _.invoke(this._subscriptions, 'stop');

    this.spark.end();
  }

  /**
   * Attaches the event handles to the WebSocket.
   */
  attachEventHandlers() {
    this.spark.on('end', () => {
      this.stop();
    });

    this.spark.on('error', () => {
      this.stop();
    });

    this.spark.on('data', (data) => {
      if (!data.msg) {
        // If no msg type was sent, ignore it.
        this.send({
          msg: 'error',
          reason: 'must-provide-msg-type',
          offendingMessage: data
        });
        return;
      }

      process.nextTick((msg) => {
        this.processMsg(msg);
      }, data);
    });
  }
}

module.exports = Session;
<|endoftext|>


<commit_before>/* snapper css snap points carousel */
;(function( w, $ ){
	var pluginName = "snapper";
	$.fn[ pluginName ] = function(optionsOrMethod){
		var pluginArgs = arguments;
		var snapForbidden = false;

		// css snap points feature test.
		// even if this test passes, several behaviors will still be polyfilled, such as snapping after resize, and animated advancing of slides with anchor links or next/prev links
		var testProp = "scroll-snap-type";
		var snapSupported = w.CSS && w.CSS.supports && ( w.CSS.supports( testProp, "mandatory") || w.CSS.supports("-webkit-" + testProp, "mandatory")	 || w.CSS.supports("-ms-" + testProp, "mandatory") );

		// get the snapper_item elements whose left offsets fall within the scroll pane. Returns a wrapped array.
		function itemsAtOffset( elem, offset ){
			var $childNodes = $( elem ).find( "." + pluginName + "_item" );
			var containWidth = $( elem ).width();
			var activeItems = [];
			var snapForbidden = false;

			$childNodes.each(function( i ){
				if( this.offsetLeft >= offset - 5 && this.offsetLeft < offset + containWidth - 5 ){
					activeItems.push( this );
				}
			});
			return $( activeItems );
		}

		function outerWidth( $elem ){
			return $elem.width() + parseFloat( $elem.css( "margin-left" ) ) + parseFloat( $elem.css( "margin-right" ) );
		}

		function outerHeight( $elem ){
			return $elem.height() + parseFloat( $elem.css( "margin-bottom" ) ) + parseFloat( $elem.css( "margin-top" ) );
		}

		// snapEvent dispatches the "snapper.snap" event.
		// The snapper_item elements with left offsets that are inside the scroll viewport are listed in an array in the second callback argument's activeSlides property.
		// use like this: $( ".snapper" ).bind( "snapper.snap", function( event, data ){ console.log( data.activeSlides );	} );
		function snapEvent( elem, x ){
			var activeSlides = itemsAtOffset( elem, x );
			$( elem ).trigger( pluginName + ".snap", { activeSlides: activeSlides } );
		}

		// optional: include overthrow.toss() in your page to get a smooth scroll, otherwise it'll just jump to the slide
		function goto( elem, x, nothrow, callback ){
			snapEvent( elem, x );

			var after = function(){
				$( elem ).trigger( pluginName + ".after-goto", {
					activeSlides: itemsAtOffset( elem, x )
				});

				// resume snapping on scroll
				snapForbidden = false;
				if( callback ){ callback(); };
			};

			// disable snapping on scroll since we're going to scroll right now
			// programatically using overthrow where available
			snapForbidden = true;
			if( typeof w.overthrow !== "undefined" && !nothrow ){
				w.overthrow.toss( elem, { left: x, finished: after });
			}
			else {
				elem.scrollLeft = x;
				after();
			}
		}

		var result, innerResult;

		// Loop through snapper elements and enhance/bind events
		result = this.each(function(){
			if( innerResult !== undefined ){
				return;
			}

			var self = this;
			var $self = $( self );
			var addNextPrev = $self.is( "[data-" + pluginName + "-nextprev]" );
			var autoTiming =
				$self.attr( "data-autoplay" ) || $self.attr( "data-snapper-autoplay" );
			var autoInterval;
			var $slider = $( "." + pluginName + "_pane", self );
			var enhancedClass = pluginName + "-enhanced";
			var $itemsContain = $slider.find( "." + pluginName + "_items" );
			var $items = $itemsContain.children();
			$items.addClass( pluginName + "_item" );
			var numItems = $items.length;
			var $nav = $( "." + pluginName + "_nav", self );
			var navSelectedClass = pluginName + "_nav_item-selected";

			if( typeof optionsOrMethod === "string" ){
				var args = Array.prototype.slice.call(pluginArgs, 1);
				var index;
				var itemWidth = ( $itemsContain.width() / numItems );

				switch(optionsOrMethod) {
				case "goto":
					index = args[0] % numItems;

					// width / items * index to make sure it goes
					offset = itemWidth * index;
					goto( $slider[ 0 ], offset, false, function(){
						// snap the scroll to the right position
						snapScroll();

						// invoke the callback if it was supplied
						if( typeof args[1] === "function" ){
							args[1]();
						}
					});
					break;
				case "getIndex":
					innerResult = Math.floor($slider[ 0 ].scrollLeft / itemWidth);
					break;
				}

				return;
			}

			// NOTE all state manipulation has to come after method invocation to
			// avoid monkeying with the DOM when it's unwarranted
			var $navInner = $( '

' ).append( $nav.children() ).appendTo( $nav );

			// this function updates the widths of the items within the slider, and their container.
			// It factors in margins and converts those to values that make sense when all items are placed in a long row
			function updateWidths(){
				var itemsContainStyle = $itemsContain.attr( "style" );
				$itemsContain.attr( "style", "" );
				var itemStyle = $items.eq(0).attr( "style" );
				$items.eq(0).attr( "style", "" );
				var sliderWidth = $slider.width();
				var itemWidth = $items.eq(0).width();
				var computed = w.getComputedStyle( $items[ 0 ], null );
				var itemLeftMargin = parseFloat( computed.getPropertyValue( "margin-left" ) );
				var itemRightMargin = parseFloat( computed.getPropertyValue( "margin-right" ) );
				var outerItemWidth = itemWidth + itemLeftMargin + itemRightMargin;
				$items.eq(0).attr( "style", itemStyle );
				$itemsContain.attr( "style", itemsContainStyle );
				var parentWidth = numItems / Math.round(sliderWidth / outerItemWidth) * 100;
				var iPercentWidth = itemWidth / sliderWidth * 100;
				var iPercentRightMargin = itemRightMargin / sliderWidth * 100;
				var iPercentLeftMargin = itemLeftMargin / sliderWidth * 100;
				var outerPercentWidth = iPercentWidth + iPercentLeftMargin + iPercentRightMargin;
				var percentAsWidth = iPercentWidth / outerPercentWidth;
				var percentAsRightMargin = iPercentRightMargin / outerPercentWidth;
				var percentAsLeftMargin = iPercentLeftMargin / outerPercentWidth;
				$itemsContain.css( "width", parentWidth + "%");
				$items.css( "width", 100 / numItems * percentAsWidth + "%" );
				$items.css( "margin-left", 100 / numItems * percentAsLeftMargin + "%" );
				$items.css( "margin-right", 100 / numItems * percentAsRightMargin + "%" );
			}

			updateWidths();
			$( self ).addClass( enhancedClass );

			// if the nextprev option is set, add the nextprev nav
			if( addNextPrev ){
				var	$nextprev = $( '

Prev

Next

' );
				var $nextprevContain = $( ".snapper_nextprev_contain", self );
				if( !$nextprevContain.length ){
					$nextprevContain = $( self );
				}
				$nextprev.appendTo( $nextprevContain );
			}

			// This click binding will allow deep-linking to slides without causing the page to scroll to the carousel container
			// this also supports click handling for generated next/prev links
			$( "a", this ).bind( "click", function( e ){
				clearInterval(autoInterval);

				var slideID = $( this ).attr( "href" );
				if( slideID.indexOf( "#" ) === -1 ){
					// only local anchor links
					return;
				}
				e.preventDefault();
				if( $( this ).is( ".snapper_nextprev_next" ) ){
					return arrowNavigate( true );
				}
				else if( $( this ).is( ".snapper_nextprev_prev" ) ){
					return arrowNavigate( false );
				}
				else {
					var $slide = $( slideID, self );
					goto( $slider[ 0 ], $slide[ 0 ].offsetLeft );
					if( "replaceState" in w.history ){
						w.history.replaceState( {}, document.title, slideID );
					}
				}
			});

			// arrow key bindings for next/prev
			$( this )
				.bind( "keyup", function( e ){
					if( e.keyCode === 37 || e.keyCode === 38 ){
						clearInterval(autoInterval);
						e.preventDefault();
						arrowNavigate( false );
					}
					if( e.keyCode === 39 || e.keyCode === 40 ){
						clearInterval(autoInterval);
						e.preventDefault();
						arrowNavigate( true );
					}
				} );

			// snap to nearest slide. Useful after a scroll stops, for polyfilling snap points
			function snapScroll(){
				if(snapForbidden){
					return;
				}

				var currScroll = $slider[ 0 ].scrollLeft;
				var width = $itemsContain.width();
				var itemWidth = $items[ 1 ] ? $items[ 1 ].offsetLeft : outerWidth( $items.eq( 0 ) );
				var roundedScroll = Math.round(currScroll/itemWidth)*itemWidth;
				var maxScroll = width - $slider.width();
				if( roundedScroll > maxScroll ){
					roundedScroll = maxScroll;
				}
				if( currScroll !== roundedScroll ){
					if( snapSupported ){
						snapEvent( $slider[ 0 ], roundedScroll );
					}
					else {
						goto( $slider[ 0 ], roundedScroll );
					}
				}
			}

			// retain snapping on resize (necessary even in scroll-snap supporting browsers currently, unfortunately)
			var startSlide;
			var afterResize;
			function snapStay(){
				var currScroll = $slider[ 0 ].scrollLeft;
				var numItems = $items.length;
				var width = $itemsContain.width();
				if( startSlide === undefined ){
					startSlide = Math.round( currScroll / width * numItems );
				}
				if( afterResize ){
					clearTimeout( afterResize );
				}
				afterResize = setTimeout( function(){
					updateWidths();
					goto( $slider[ 0 ], $items[ startSlide ].offsetLeft, true );
					startSlide = afterResize = undefined;
				}, 50 );
			}
			$( w ).bind( "resize", snapStay );

			// next/prev links or arrows should loop back to the other end when an extreme is reached
			function arrowNavigate( forward ){
				var currScroll = $slider[ 0 ].scrollLeft;
				var width = $itemsContain.width();
				var itemWidth = outerWidth( $slider );
				var maxScroll = width - itemWidth - 5;
				if( forward ){
					if( currScroll >= maxScroll ){
						return first();
					}
					else {
						return next();
					}
				}
				else {
					if( currScroll === 0 ){
						return last();
					}
					else {
						return prev();
					}
				}
			}

			// advance slide one full scrollpane's width forward
			function next(){
				goto( $slider[ 0 ], $slider[ 0 ].scrollLeft + ( $itemsContain.width() / numItems ), false, function(){
					$slider.trigger( pluginName + ".after-next" );
				});
			}

			// advance slide one full scrollpane's width backwards
			function prev(){
				goto( $slider[ 0 ], $slider[ 0 ].scrollLeft - ( $itemsContain.width() / numItems ), false, function(){
					$slider.trigger( pluginName + ".after-prev" );
				});
			}

			// go to first slide
			function first(){
				goto( $slider[ 0 ], 0 );
			}

			// go to last slide
			function last(){
				goto( $slider[ 0 ], $itemsContain.width() - $slider.width() );
			}

			// update thumbnail state on pane scroll
			if( $nav.length ){
				// function for scrolling to the xy of the active thumbnail
				function scrollNav(elem, x, y){
					if( typeof w.overthrow !== "undefined" ){
						w.overthrow.toss( elem, { left: x, top:y });
					}
					else {
						elem.scrollLeft = x;
						elem.scrollTop = y;
					}
				}

				function activeItem(){
					var currScroll = $slider[ 0 ].scrollLeft;
					var width = outerWidth( $itemsContain );
					var navWidth = outerWidth( $nav );
					var navHeight = outerHeight( $nav );
					var activeIndex = Math.round( currScroll / width * numItems );
					var childs = $nav.find( "a" ).removeClass( navSelectedClass );
					var activeChild = childs.eq( activeIndex ).addClass( navSelectedClass );

					var thumbX = activeChild[ 0 ].offsetLeft - (navWidth/2);
					var thumbY = activeChild[ 0 ].offsetTop - (navHeight/2);

					scrollNav( $navInner[ 0 ], thumbX, thumbY );
				}
				// set active item on scroll
				$slider.bind( "scroll", activeItem );
				// set active item on init
				activeItem();
			}

			// apply snapping after scroll, in browsers that don't support CSS scroll-snap
			var scrollStop;
			$slider.bind( "scroll", function(e){
				if( scrollStop ){
					clearTimeout( scrollStop );
				}
				scrollStop = setTimeout( snapScroll, 50 );
			});

			// if a touch event is fired on the snapper we know the user is trying to
			// interact with it and we should disable the auto play
			$self.bind("touchstart", function(){
				clearTimeout(autoInterval);
			});

			// if the `data-autotplay` attribute is assigned a natural number value
			// use it to make the slides cycle until there is a user interaction
			if(autoTiming){
				var parseError = false;

				try {
					autoTiming = parseInt(autoTiming, 10);
				} catch(e) {
					parseError = true;
				}

				// if NaN or there was an error throw an exception
				if( !autoTiming || parseError ) {
					var msg = "Snapper: `data-autoplay` must have an natural number value.";
					throw new Error(msg);
				}

				// autoInterval is cleared in each user interaction binding
				autoInterval = setInterval(function(){
					arrowNavigate(true);
				}, autoTiming);
			}
		});

		return (innerResult !== undefined ? innerResult : result);
	};

	// auto-init on enhance
	$( document ).bind( "enhance", function( e ){
		$( "." + pluginName, e.target ).add( e.target ).filter( "." + pluginName )[ pluginName ]();
	});
}( this, jQuery ));
<commit_msg>create navInner element if not present, but just use it if it is present.<commit_after>/* snapper css snap points carousel */
;(function( w, $ ){
	var pluginName = "snapper";
	$.fn[ pluginName ] = function(optionsOrMethod){
		var pluginArgs = arguments;
		var snapForbidden = false;

		// css snap points feature test.
		// even if this test passes, several behaviors will still be polyfilled, such as snapping after resize, and animated advancing of slides with anchor links or next/prev links
		var testProp = "scroll-snap-type";
		var snapSupported = w.CSS && w.CSS.supports && ( w.CSS.supports( testProp, "mandatory") || w.CSS.supports("-webkit-" + testProp, "mandatory")	 || w.CSS.supports("-ms-" + testProp, "mandatory") );

		// get the snapper_item elements whose left offsets fall within the scroll pane. Returns a wrapped array.
		function itemsAtOffset( elem, offset ){
			var $childNodes = $( elem ).find( "." + pluginName + "_item" );
			var containWidth = $( elem ).width();
			var activeItems = [];
			var snapForbidden = false;

			$childNodes.each(function( i ){
				if( this.offsetLeft >= offset - 5 && this.offsetLeft < offset + containWidth - 5 ){
					activeItems.push( this );
				}
			});
			return $( activeItems );
		}

		function outerWidth( $elem ){
			return $elem.width() + parseFloat( $elem.css( "margin-left" ) ) + parseFloat( $elem.css( "margin-right" ) );
		}

		function outerHeight( $elem ){
			return $elem.height() + parseFloat( $elem.css( "margin-bottom" ) ) + parseFloat( $elem.css( "margin-top" ) );
		}

		// snapEvent dispatches the "snapper.snap" event.
		// The snapper_item elements with left offsets that are inside the scroll viewport are listed in an array in the second callback argument's activeSlides property.
		// use like this: $( ".snapper" ).bind( "snapper.snap", function( event, data ){ console.log( data.activeSlides );	} );
		function snapEvent( elem, x ){
			var activeSlides = itemsAtOffset( elem, x );
			$( elem ).trigger( pluginName + ".snap", { activeSlides: activeSlides } );
		}

		// optional: include overthrow.toss() in your page to get a smooth scroll, otherwise it'll just jump to the slide
		function goto( elem, x, nothrow, callback ){
			snapEvent( elem, x );

			var after = function(){
				$( elem ).trigger( pluginName + ".after-goto", {
					activeSlides: itemsAtOffset( elem, x )
				});

				// resume snapping on scroll
				snapForbidden = false;
				if( callback ){ callback(); };
			};

			// disable snapping on scroll since we're going to scroll right now
			// programatically using overthrow where available
			snapForbidden = true;
			if( typeof w.overthrow !== "undefined" && !nothrow ){
				w.overthrow.toss( elem, { left: x, finished: after });
			}
			else {
				elem.scrollLeft = x;
				after();
			}
		}

		var result, innerResult;

		// Loop through snapper elements and enhance/bind events
		result = this.each(function(){
			if( innerResult !== undefined ){
				return;
			}

			var self = this;
			var $self = $( self );
			var addNextPrev = $self.is( "[data-" + pluginName + "-nextprev]" );
			var autoTiming =
				$self.attr( "data-autoplay" ) || $self.attr( "data-snapper-autoplay" );
			var autoInterval;
			var $slider = $( "." + pluginName + "_pane", self );
			var enhancedClass = pluginName + "-enhanced";
			var $itemsContain = $slider.find( "." + pluginName + "_items" );
			var $items = $itemsContain.children();
			$items.addClass( pluginName + "_item" );
			var numItems = $items.length;
			var $nav = $( "." + pluginName + "_nav", self );
			var navSelectedClass = pluginName + "_nav_item-selected";

			if( typeof optionsOrMethod === "string" ){
				var args = Array.prototype.slice.call(pluginArgs, 1);
				var index;
				var itemWidth = ( $itemsContain.width() / numItems );

				switch(optionsOrMethod) {
				case "goto":
					index = args[0] % numItems;

					// width / items * index to make sure it goes
					offset = itemWidth * index;
					goto( $slider[ 0 ], offset, false, function(){
						// snap the scroll to the right position
						snapScroll();

						// invoke the callback if it was supplied
						if( typeof args[1] === "function" ){
							args[1]();
						}
					});
					break;
				case "getIndex":
					innerResult = Math.floor($slider[ 0 ].scrollLeft / itemWidth);
					break;
				}

				return;
			}

			// NOTE all state manipulation has to come after method invocation to
			// avoid monkeying with the DOM when it's unwarranted
			var $navInner = $nav.find( "." + pluginName + "_nav_inner" );
			if( !$navInner.length ){
				$( '

' ).append( $nav.children() ).appendTo( $nav );
			}

			// this function updates the widths of the items within the slider, and their container.
			// It factors in margins and converts those to values that make sense when all items are placed in a long row
			function updateWidths(){
				var itemsContainStyle = $itemsContain.attr( "style" );
				$itemsContain.attr( "style", "" );
				var itemStyle = $items.eq(0).attr( "style" );
				$items.eq(0).attr( "style", "" );
				var sliderWidth = $slider.width();
				var itemWidth = $items.eq(0).width();
				var computed = w.getComputedStyle( $items[ 0 ], null );
				var itemLeftMargin = parseFloat( computed.getPropertyValue( "margin-left" ) );
				var itemRightMargin = parseFloat( computed.getPropertyValue( "margin-right" ) );
				var outerItemWidth = itemWidth + itemLeftMargin + itemRightMargin;
				$items.eq(0).attr( "style", itemStyle );
				$itemsContain.attr( "style", itemsContainStyle );
				var parentWidth = numItems / Math.round(sliderWidth / outerItemWidth) * 100;
				var iPercentWidth = itemWidth / sliderWidth * 100;
				var iPercentRightMargin = itemRightMargin / sliderWidth * 100;
				var iPercentLeftMargin = itemLeftMargin / sliderWidth * 100;
				var outerPercentWidth = iPercentWidth + iPercentLeftMargin + iPercentRightMargin;
				var percentAsWidth = iPercentWidth / outerPercentWidth;
				var percentAsRightMargin = iPercentRightMargin / outerPercentWidth;
				var percentAsLeftMargin = iPercentLeftMargin / outerPercentWidth;
				$itemsContain.css( "width", parentWidth + "%");
				$items.css( "width", 100 / numItems * percentAsWidth + "%" );
				$items.css( "margin-left", 100 / numItems * percentAsLeftMargin + "%" );
				$items.css( "margin-right", 100 / numItems * percentAsRightMargin + "%" );
			}

			updateWidths();
			$( self ).addClass( enhancedClass );

			// if the nextprev option is set, add the nextprev nav
			if( addNextPrev ){
				var	$nextprev = $( '

Prev

Next

' );
				var $nextprevContain = $( ".snapper_nextprev_contain", self );
				if( !$nextprevContain.length ){
					$nextprevContain = $( self );
				}
				$nextprev.appendTo( $nextprevContain );
			}

			// This click binding will allow deep-linking to slides without causing the page to scroll to the carousel container
			// this also supports click handling for generated next/prev links
			$( "a", this ).bind( "click", function( e ){
				clearInterval(autoInterval);

				var slideID = $( this ).attr( "href" );
				if( slideID.indexOf( "#" ) === -1 ){
					// only local anchor links
					return;
				}
				e.preventDefault();
				if( $( this ).is( ".snapper_nextprev_next" ) ){
					return arrowNavigate( true );
				}
				else if( $( this ).is( ".snapper_nextprev_prev" ) ){
					return arrowNavigate( false );
				}
				else {
					var $slide = $( slideID, self );
					goto( $slider[ 0 ], $slide[ 0 ].offsetLeft );
					if( "replaceState" in w.history ){
						w.history.replaceState( {}, document.title, slideID );
					}
				}
			});

			// arrow key bindings for next/prev
			$( this )
				.bind( "keyup", function( e ){
					if( e.keyCode === 37 || e.keyCode === 38 ){
						clearInterval(autoInterval);
						e.preventDefault();
						arrowNavigate( false );
					}
					if( e.keyCode === 39 || e.keyCode === 40 ){
						clearInterval(autoInterval);
						e.preventDefault();
						arrowNavigate( true );
					}
				} );

			// snap to nearest slide. Useful after a scroll stops, for polyfilling snap points
			function snapScroll(){
				if(snapForbidden){
					return;
				}

				var currScroll = $slider[ 0 ].scrollLeft;
				var width = $itemsContain.width();
				var itemWidth = $items[ 1 ] ? $items[ 1 ].offsetLeft : outerWidth( $items.eq( 0 ) );
				var roundedScroll = Math.round(currScroll/itemWidth)*itemWidth;
				var maxScroll = width - $slider.width();
				if( roundedScroll > maxScroll ){
					roundedScroll = maxScroll;
				}
				if( currScroll !== roundedScroll ){
					if( snapSupported ){
						snapEvent( $slider[ 0 ], roundedScroll );
					}
					else {
						goto( $slider[ 0 ], roundedScroll );
					}
				}
			}

			// retain snapping on resize (necessary even in scroll-snap supporting browsers currently, unfortunately)
			var startSlide;
			var afterResize;
			function snapStay(){
				var currScroll = $slider[ 0 ].scrollLeft;
				var numItems = $items.length;
				var width = $itemsContain.width();
				if( startSlide === undefined ){
					startSlide = Math.round( currScroll / width * numItems );
				}
				if( afterResize ){
					clearTimeout( afterResize );
				}
				afterResize = setTimeout( function(){
					updateWidths();
					goto( $slider[ 0 ], $items[ startSlide ].offsetLeft, true );
					startSlide = afterResize = undefined;
				}, 50 );
			}
			$( w ).bind( "resize", snapStay );

			// next/prev links or arrows should loop back to the other end when an extreme is reached
			function arrowNavigate( forward ){
				var currScroll = $slider[ 0 ].scrollLeft;
				var width = $itemsContain.width();
				var itemWidth = outerWidth( $slider );
				var maxScroll = width - itemWidth - 5;
				if( forward ){
					if( currScroll >= maxScroll ){
						return first();
					}
					else {
						return next();
					}
				}
				else {
					if( currScroll === 0 ){
						return last();
					}
					else {
						return prev();
					}
				}
			}

			// advance slide one full scrollpane's width forward
			function next(){
				goto( $slider[ 0 ], $slider[ 0 ].scrollLeft + ( $itemsContain.width() / numItems ), false, function(){
					$slider.trigger( pluginName + ".after-next" );
				});
			}

			// advance slide one full scrollpane's width backwards
			function prev(){
				goto( $slider[ 0 ], $slider[ 0 ].scrollLeft - ( $itemsContain.width() / numItems ), false, function(){
					$slider.trigger( pluginName + ".after-prev" );
				});
			}

			// go to first slide
			function first(){
				goto( $slider[ 0 ], 0 );
			}

			// go to last slide
			function last(){
				goto( $slider[ 0 ], $itemsContain.width() - $slider.width() );
			}

			// update thumbnail state on pane scroll
			if( $nav.length ){
				// function for scrolling to the xy of the active thumbnail
				function scrollNav(elem, x, y){
					if( typeof w.overthrow !== "undefined" ){
						w.overthrow.toss( elem, { left: x, top:y });
					}
					else {
						elem.scrollLeft = x;
						elem.scrollTop = y;
					}
				}

				function activeItem(){
					var currScroll = $slider[ 0 ].scrollLeft;
					var width = outerWidth( $itemsContain );
					var navWidth = outerWidth( $nav );
					var navHeight = outerHeight( $nav );
					var activeIndex = Math.round( currScroll / width * numItems );
					var childs = $nav.find( "a" ).removeClass( navSelectedClass );
					var activeChild = childs.eq( activeIndex ).addClass( navSelectedClass );

					var thumbX = activeChild[ 0 ].offsetLeft - (navWidth/2);
					var thumbY = activeChild[ 0 ].offsetTop - (navHeight/2);

					scrollNav( $navInner[ 0 ], thumbX, thumbY );
				}
				// set active item on scroll
				$slider.bind( "scroll", activeItem );
				// set active item on init
				activeItem();
			}

			// apply snapping after scroll, in browsers that don't support CSS scroll-snap
			var scrollStop;
			$slider.bind( "scroll", function(e){
				if( scrollStop ){
					clearTimeout( scrollStop );
				}
				scrollStop = setTimeout( snapScroll, 50 );
			});

			// if a touch event is fired on the snapper we know the user is trying to
			// interact with it and we should disable the auto play
			$self.bind("touchstart", function(){
				clearTimeout(autoInterval);
			});

			// if the `data-autotplay` attribute is assigned a natural number value
			// use it to make the slides cycle until there is a user interaction
			if(autoTiming){
				var parseError = false;

				try {
					autoTiming = parseInt(autoTiming, 10);
				} catch(e) {
					parseError = true;
				}

				// if NaN or there was an error throw an exception
				if( !autoTiming || parseError ) {
					var msg = "Snapper: `data-autoplay` must have an natural number value.";
					throw new Error(msg);
				}

				// autoInterval is cleared in each user interaction binding
				autoInterval = setInterval(function(){
					arrowNavigate(true);
				}, autoTiming);
			}
		});

		return (innerResult !== undefined ? innerResult : result);
	};

	// auto-init on enhance
	$( document ).bind( "enhance", function( e ){
		$( "." + pluginName, e.target ).add( e.target ).filter( "." + pluginName )[ pluginName ]();
	});
}( this, jQuery ));
<|endoftext|>


<commit_before>import find from 'array-find';
import WebSocket from 'ws';
import tryJsonParse from 'try-json-parse';

import { vote } from './controllers/booth';
import { sendChatMessage } from './controllers/chat';
import { disconnectUser } from './controllers/users';

import GuestConnection from './sockets/GuestConnection';
import AuthedConnection from './sockets/AuthedConnection';
import LostConnection from './sockets/LostConnection';

const debug = require('debug')('uwave:api:sockets');

export function createCommand(command, data) {
  return JSON.stringify({ command, data });
}

export default class SocketServer {
  connections = [];
  options = {
    timeout: 30
  };

  /**
   * Create a socket server.
   *
   * @param {UWaveServer} uw üWave Core instance.
   * @param {object} options Socket server options.
   * @param {number} options.timeout Time in seconds to wait for disconnected
   *     users to reconnect before removing them.
   */
  constructor(uw, options = {}) {
    this.uw = uw;
    this.sub = uw.subscription();
    Object.assign(this.options, options);

    this.wss = new WebSocket.Server({
      server: uw.server,
      clientTracking: false
    });

    this.sub.on('ready', () => this.sub.subscribe('v1'));
    this.sub.on('message', (channel, command) => {
      this.onServerMessage(channel, command)
        .catch(e => { throw e; });
    });

    this.wss.on('connection', this.onSocketConnected.bind(this));

    this.initLostConnections();
  }

  /**
   * Create `LostConnection`s for every user that's known to be online, but that
   * is not currently connected to the socket server.
   */
  async initLostConnections() {
    const User = this.uw.model('User');
    const userIDs = await this.uw.redis.lrange('users', 0, -1);
    const disconnectedIDs = userIDs.filter(userID => !this.connection(userID));

    const disconnectedUsers = await User.where('_id').in(disconnectedIDs);
    disconnectedUsers.forEach(user => {
      this.add(this.createLostConnection(user));
    });
  }

  onSocketConnected(socket) {
    debug('new connection');

    this.add(this.createGuestConnection(socket));
  }

  /**
   * Get a LostConnection for a user, if one exists.
   */
  getLostConnection(user) {
    return find(this.connections, connection =>
      connection instanceof LostConnection && connection.user.id === user.id
    );
  }

  /**
   * Create a connection instance for an unauthenticated user.
   */
  createGuestConnection(socket) {
    const connection = new GuestConnection(this.uw, socket, {
      secret: this.options.secret
    });
    connection.on('close', () => {
      this.remove(connection);
    });
    connection.on('authenticate', async user => {
      debug('connecting', user.id, user.username);
      if (await connection.isReconnect(user)) {
        debug('is reconnection');
        const previousConnection = this.getLostConnection(user);
        if (previousConnection) this.remove(previousConnection);
      } else {
        this.uw.publish('user:join', { userID: user.id });
      }

      this.replace(connection, this.createAuthedConnection(socket, user));
    });
    return connection;
  }

  /**
   * Create a connection instance for an authenticated user.
   */
  createAuthedConnection(socket, user) {
    const connection = new AuthedConnection(this.uw, socket, user);
    connection.on('close', () => {
      debug('lost connection', user.id, user.username);
      this.replace(connection, this.createLostConnection(user));
    });
    connection.on('command', (command, data) => {
      debug('command', user.id, user.username, command, data);
      if (this.clientActions[command]) {
        this.clientActions[command].call(this, user, data);
      }
    });
    return connection;
  }

  /**
   * Create a connection instance for a user who disconnected.
   */
  createLostConnection(user) {
    const connection = new LostConnection(this.uw, user, this.options.timeout);
    connection.on('close', () => {
      debug('left', user.id, user.username);
      this.remove(connection);
      disconnectUser(this.uw, user);
    });
    return connection;
  }

  /**
   * Add a connection.
   */
  add(connection) {
    debug('adding', String(connection));

    this.connections.push(connection);
  }

  /**
   * Remove a connection.
   */
  remove(connection) {
    debug('removing', String(connection));

    const i = this.connections.indexOf(connection);
    this.connections.splice(i, 1);

    connection.removed();
  }

  /**
   * Replace a connection instance with another connection instance. Useful when
   * a connection changes "type", like GuestConnection → AuthedConnection.
   */
  replace(oldConnection, newConnection) {
    this.remove(oldConnection);
    this.add(newConnection);
  }

  /**
   * Handlers for commands that come in from clients.
   */
  clientActions = {
    sendChat(user, message) {
      debug('sendChat', user, message);
      return sendChatMessage(this.uw, user.id, message);
    },
    vote(user, direction) {
      return vote(this.uw, user.id, direction);
    }
  };

  /**
   * Handlers for commands that come in from the server side.
   */
  serverActions = {
    /**
     * Broadcast the next track.
     */
    'advance:complete'(next) {
      if (next) {
        this.broadcast('advance', {
          historyID: next._id,
          userID: next.user._id,
          item: next.item._id,
          media: next.media,
          played: new Date(next.played).getTime()
        });
      } else {
        this.broadcast('advance', null);
      }
    },
    /**
     * Broadcast a chat message.
     */
    'chat:message'({ userID, message, timestamp }) {
      this.broadcast('chatMessage', {
        _id: userID,
        message,
        timestamp
      });
    },
    /**
     * Broadcast a vote for the current track.
     */
    'booth:vote'({ userID, direction }) {
      this.broadcast('vote', {
        _id: userID,
        value: direction
      });
    },
    /**
     * Cycle a single user's playlist.
     */
    'playlist:cycle'({ userID, playlistID }) {
      this.sendTo(userID, 'playlistCycle', { playlistID });
    },
    /**
     * Broadcast that a user left the waitlist.
     */
    'waitlist:leave'({ userID, waitlist }) {
      this.broadcast('waitlistLeave', { userID, waitlist });
    },
    /**
     * Broadcast that a user was removed from the waitlist.
     */
    'waitlist:remove'({ userID, moderatorID, waitlist }) {
      this.broadcast('waitlistRemove', { userID, moderatorID, waitlist });
    },
    /**
     * Broadcast a waitlist update.
     */
    'waitlist:update'(waitlist) {
      this.broadcast('waitlistUpdate', waitlist);
    },
    async 'user:join'({ userID }) {
      const User = this.uw.model('User');

      await this.uw.redis.rpush('users', userID);
      this.broadcast('join', await User.findById(userID).lean());
    },
    /**
     * Broadcast that a user left the server.
     */
    'user:leave'({ userID }) {
      this.broadcast('leave', userID);
    },
    /**
     * Force-close a connection.
     */
    'api-v1:socket:close'(userID) {
      const connection = this.connection(userID);
      if (connection) {
        connection.close();
      }
    }
  };

  /**
   * Handle command messages coming in from Redis.
   * Some commands are intended to broadcast immediately to all connected
   * clients, but others require special action.
   */
  async onServerMessage(channel, rawCommand) {
    const { command, data } = tryJsonParse(rawCommand) || {};

    if (channel === 'v1') {
      this.broadcast(command, data);
    } else if (channel === 'uwave') {
      if (command in this.serverActions) {
        this.serverActions[command].call(this, data);
      }
    }
  }

  /**
   * Stop the socket server.
   */
  destroy() {
    this.sub.removeAllListeners();
    this.sub.unsubscribe('v1', 'uwave');
    this.sub.close();
    this.wss.shutdown();
  }

  /**
   * Get the connection instance for a specific user.
   *
   * @param {object|string} user The user.
   * @return {Connection}
   */
  connection(user) {
    const userID = typeof user === 'object' ? user.id : user;
    return find(this.connections, connection =>
      connection.user && connection.user.id === userID
    );
  }

  /**
   * Broadcast a command to all connected clients.
   *
   * @param {string} command Command name.
   * @param {*} data Command data.
   */
  broadcast(command: string, data: any) {
    debug('broadcast', command, data);

    this.connections.forEach(connection => {
      connection.send(command, data);
    });
  }

  /**
   * Send a command to a single user.
   *
   * @param {Object|string} user User or user ID to send the command to.
   * @param {string} command Command name.
   * @param {*} data Command data.
   */
  sendTo(user, command: string, data: any) {
    const conn = this.connection(user);
    if (conn) {
      conn.send(command, data);
    }
  }
}
<commit_msg>sockets: only disconnect user if no connections are left open<commit_after>import find from 'array-find';
import WebSocket from 'ws';
import tryJsonParse from 'try-json-parse';

import { vote } from './controllers/booth';
import { sendChatMessage } from './controllers/chat';
import { disconnectUser } from './controllers/users';

import GuestConnection from './sockets/GuestConnection';
import AuthedConnection from './sockets/AuthedConnection';
import LostConnection from './sockets/LostConnection';

const debug = require('debug')('uwave:api:sockets');

export function createCommand(command, data) {
  return JSON.stringify({ command, data });
}

export default class SocketServer {
  connections = [];
  options = {
    timeout: 30
  };

  /**
   * Create a socket server.
   *
   * @param {UWaveServer} uw üWave Core instance.
   * @param {object} options Socket server options.
   * @param {number} options.timeout Time in seconds to wait for disconnected
   *     users to reconnect before removing them.
   */
  constructor(uw, options = {}) {
    this.uw = uw;
    this.sub = uw.subscription();
    Object.assign(this.options, options);

    this.wss = new WebSocket.Server({
      server: uw.server,
      clientTracking: false
    });

    this.sub.on('ready', () => this.sub.subscribe('v1'));
    this.sub.on('message', (channel, command) => {
      this.onServerMessage(channel, command)
        .catch(e => { throw e; });
    });

    this.wss.on('connection', this.onSocketConnected.bind(this));

    this.initLostConnections();
  }

  /**
   * Create `LostConnection`s for every user that's known to be online, but that
   * is not currently connected to the socket server.
   */
  async initLostConnections() {
    const User = this.uw.model('User');
    const userIDs = await this.uw.redis.lrange('users', 0, -1);
    const disconnectedIDs = userIDs.filter(userID => !this.connection(userID));

    const disconnectedUsers = await User.where('_id').in(disconnectedIDs);
    disconnectedUsers.forEach(user => {
      this.add(this.createLostConnection(user));
    });
  }

  onSocketConnected(socket) {
    debug('new connection');

    this.add(this.createGuestConnection(socket));
  }

  /**
   * Get a LostConnection for a user, if one exists.
   */
  getLostConnection(user) {
    return find(this.connections, connection =>
      connection instanceof LostConnection && connection.user.id === user.id
    );
  }

  /**
   * Create a connection instance for an unauthenticated user.
   */
  createGuestConnection(socket) {
    const connection = new GuestConnection(this.uw, socket, {
      secret: this.options.secret
    });
    connection.on('close', () => {
      this.remove(connection);
    });
    connection.on('authenticate', async user => {
      debug('connecting', user.id, user.username);
      if (await connection.isReconnect(user)) {
        debug('is reconnection');
        const previousConnection = this.getLostConnection(user);
        if (previousConnection) this.remove(previousConnection);
      } else {
        this.uw.publish('user:join', { userID: user.id });
      }

      this.replace(connection, this.createAuthedConnection(socket, user));
    });
    return connection;
  }

  /**
   * Create a connection instance for an authenticated user.
   */
  createAuthedConnection(socket, user) {
    const connection = new AuthedConnection(this.uw, socket, user);
    connection.on('close', () => {
      debug('lost connection', user.id, user.username);
      this.replace(connection, this.createLostConnection(user));
    });
    connection.on('command', (command, data) => {
      debug('command', user.id, user.username, command, data);
      if (this.clientActions[command]) {
        this.clientActions[command].call(this, user, data);
      }
    });
    return connection;
  }

  /**
   * Create a connection instance for a user who disconnected.
   */
  createLostConnection(user) {
    const connection = new LostConnection(this.uw, user, this.options.timeout);
    connection.on('close', () => {
      debug('left', user.id, user.username);
      this.remove(connection);
      // Only register that the user left if they didn't have another connection
      // still open.
      if (!this.connection(user)) {
        disconnectUser(this.uw, user);
      }
    });
    return connection;
  }

  /**
   * Add a connection.
   */
  add(connection) {
    debug('adding', String(connection));

    this.connections.push(connection);
  }

  /**
   * Remove a connection.
   */
  remove(connection) {
    debug('removing', String(connection));

    const i = this.connections.indexOf(connection);
    this.connections.splice(i, 1);

    connection.removed();
  }

  /**
   * Replace a connection instance with another connection instance. Useful when
   * a connection changes "type", like GuestConnection → AuthedConnection.
   */
  replace(oldConnection, newConnection) {
    this.remove(oldConnection);
    this.add(newConnection);
  }

  /**
   * Handlers for commands that come in from clients.
   */
  clientActions = {
    sendChat(user, message) {
      debug('sendChat', user, message);
      return sendChatMessage(this.uw, user.id, message);
    },
    vote(user, direction) {
      return vote(this.uw, user.id, direction);
    }
  };

  /**
   * Handlers for commands that come in from the server side.
   */
  serverActions = {
    /**
     * Broadcast the next track.
     */
    'advance:complete'(next) {
      if (next) {
        this.broadcast('advance', {
          historyID: next._id,
          userID: next.user._id,
          item: next.item._id,
          media: next.media,
          played: new Date(next.played).getTime()
        });
      } else {
        this.broadcast('advance', null);
      }
    },
    /**
     * Broadcast a chat message.
     */
    'chat:message'({ userID, message, timestamp }) {
      this.broadcast('chatMessage', {
        _id: userID,
        message,
        timestamp
      });
    },
    /**
     * Broadcast a vote for the current track.
     */
    'booth:vote'({ userID, direction }) {
      this.broadcast('vote', {
        _id: userID,
        value: direction
      });
    },
    /**
     * Cycle a single user's playlist.
     */
    'playlist:cycle'({ userID, playlistID }) {
      this.sendTo(userID, 'playlistCycle', { playlistID });
    },
    /**
     * Broadcast that a user left the waitlist.
     */
    'waitlist:leave'({ userID, waitlist }) {
      this.broadcast('waitlistLeave', { userID, waitlist });
    },
    /**
     * Broadcast that a user was removed from the waitlist.
     */
    'waitlist:remove'({ userID, moderatorID, waitlist }) {
      this.broadcast('waitlistRemove', { userID, moderatorID, waitlist });
    },
    /**
     * Broadcast a waitlist update.
     */
    'waitlist:update'(waitlist) {
      this.broadcast('waitlistUpdate', waitlist);
    },
    async 'user:join'({ userID }) {
      const User = this.uw.model('User');

      await this.uw.redis.rpush('users', userID);
      this.broadcast('join', await User.findById(userID).lean());
    },
    /**
     * Broadcast that a user left the server.
     */
    'user:leave'({ userID }) {
      this.broadcast('leave', userID);
    },
    /**
     * Force-close a connection.
     */
    'api-v1:socket:close'(userID) {
      const connection = this.connection(userID);
      if (connection) {
        connection.close();
      }
    }
  };

  /**
   * Handle command messages coming in from Redis.
   * Some commands are intended to broadcast immediately to all connected
   * clients, but others require special action.
   */
  async onServerMessage(channel, rawCommand) {
    const { command, data } = tryJsonParse(rawCommand) || {};

    if (channel === 'v1') {
      this.broadcast(command, data);
    } else if (channel === 'uwave') {
      if (command in this.serverActions) {
        this.serverActions[command].call(this, data);
      }
    }
  }

  /**
   * Stop the socket server.
   */
  destroy() {
    this.sub.removeAllListeners();
    this.sub.unsubscribe('v1', 'uwave');
    this.sub.close();
    this.wss.shutdown();
  }

  /**
   * Get the connection instance for a specific user.
   *
   * @param {object|string} user The user.
   * @return {Connection}
   */
  connection(user) {
    const userID = typeof user === 'object' ? user.id : user;
    return find(this.connections, connection =>
      connection.user && connection.user.id === userID
    );
  }

  /**
   * Broadcast a command to all connected clients.
   *
   * @param {string} command Command name.
   * @param {*} data Command data.
   */
  broadcast(command: string, data: any) {
    debug('broadcast', command, data);

    this.connections.forEach(connection => {
      connection.send(command, data);
    });
  }

  /**
   * Send a command to a single user.
   *
   * @param {Object|string} user User or user ID to send the command to.
   * @param {string} command Command name.
   * @param {*} data Command data.
   */
  sendTo(user, command: string, data: any) {
    const conn = this.connection(user);
    if (conn) {
      conn.send(command, data);
    }
  }
}
<|endoftext|>


<commit_before>var Crafty = require('./core.js'),
    document = window.document;

// Directive for jshint to ignore evals
// eval is used to support IE8, so this can be removed if we decide to drop that
/* jshint evil:true */

/**@
 * #Storage
 * @category Utilities
 * Utility to allow data to be saved to a permanent storage solution: IndexedDB, WebSql, localstorage or cookies
 */
/**@
 * #.open
 * @comp Storage
 * @sign .open(String gameName)
 * @param gameName - a machine readable string to uniquely identify your game
 *
 * Opens a connection to the database. If the best they have is localstorage or lower, it does nothing
 *
 * @example
 * Open a database
 * ~~~
 * Crafty.storage.open('MyGame');
 * ~~~
 */
/**@
 * #.save
 * @comp Storage
 * @sign .save(String key, String type, Mixed data)
 * @param key - A unique key for identifying this piece of data
 * @param type - 'save' or 'cache'
 * @param data - Some kind of data.
 *
 * Saves a piece of data to the database. Can be anything, although entities are preferred.
 * For all storage methods but IndexedDB, the data will be serialized as a string
 * During serialization, an entity's SaveData event will be triggered.
 * Components should implement a SaveData handler and attach the necessary information to the passed object
 *
 * @example
 * Saves an entity to the database
 * ~~~
 * var ent = Crafty.e("2D, DOM")
 *                     .attr({x: 20, y: 20, w: 100, h:100});
 * Crafty.storage.open('MyGame');
 * Crafty.storage.save('MyEntity', 'save', ent);
 * ~~~
 */
/**@
	 * #.load
	 * @comp Storage
	 * @sign .load(String key, String type)
	 * @param key - A unique key to search for
	 * @param type - 'save' or 'cache'
	 * @param callback - Do things with the data you get back
	 *
	 * Loads a piece of data from the database.
	 * Entities will be reconstructed from the serialized string

	 * @example
	 * Loads an entity from the database
	 * ~~~
	 * Crafty.storage.open('MyGame');
	 * Crafty.storage.load('MyEntity', 'save', function (data) { // do things });
	 * ~~~
	 */
/**@
	 * #.getAllKeys
	 * @comp Storage
	 * @sign .getAllKeys(String type)
	 * @param type - 'save' or 'cache'
	 * Gets all the keys for a given type

	 * @example
	 * Gets all the save games saved
	 * ~~~
	 * Crafty.storage.open('MyGame');
	 * var saves = Crafty.storage.getAllKeys('save');
	 * ~~~
	 */
/**@
 * #.external
 * @comp Storage
 * @sign .external(String url)
 * @param url - URL to an external to save games too
 *
 * Enables and sets the url for saving games to an external server
 *
 * @example
 * Save an entity to an external server
 * ~~~
 * Crafty.storage.external('http://somewhere.com/server.php');
 * Crafty.storage.open('MyGame');
 * var ent = Crafty.e('2D, DOM')
 *                     .attr({x: 20, y: 20, w: 100, h:100});
 * Crafty.storage.save('save01', 'save', ent);
 * ~~~
 */
/**@
 * #SaveData event
 * @comp Storage
 * @param data - An object containing all of the data to be serialized
 * @param prepare - The function to prepare an entity for serialization
 *
 * Any data a component wants to save when it's serialized should be added to this object.
 * Straight attribute should be set in data.attr.
 * Anything that requires a special handler should be set in a unique property.
 *
 * @example
 * Saves the innerHTML of an entity
 * ~~~
 * Crafty.e("2D DOM").bind("SaveData", function (data, prepare) {
 *     data.attr.x = this.x;
 *     data.attr.y = this.y;
 *     data.dom = this.element.innerHTML;
 * });
 * ~~~
 */
/**@
 * #LoadData event
 * @comp Storage
 * @param data - An object containing all the data that been saved
 * @param process - The function to turn a string into an entity
 *
 * Handlers for processing any data that needs more than straight assignment
 *
 * Note that data stored in the .attr object is automatically added to the entity.
 * It does not need to be handled here
 *
 * @example
 * ~~~
 * Sets the innerHTML from a saved entity
 * Crafty.e("2D DOM").bind("LoadData", function (data, process) {
 *     this.element.innerHTML = data.dom;
 * });
 * ~~~
 */
Crafty.storage = (function () {
    var db = null,
        url, gameName, timestamps = {},
        transactionType = {
            READ: "readonly",
            READ_WRITE: "readwrite"
        };

    /*
     * Processes a retrieved object.
     * Creates an entity if it is one
     */

    function process(obj) {
        if (obj.c) {
            var d = Crafty.e(obj.c)
                .attr(obj.attr)
                .trigger('LoadData', obj, process);
            return d;
        } else if (typeof obj == 'object') {
            for (var prop in obj) {
                obj[prop] = process(obj[prop]);
            }
        }
        return obj;
    }

    function unserialize(str) {
        if (typeof str != 'string') return null;
        var data = (JSON ? JSON.parse(str) : eval('(' + str + ')'));
        return process(data);
    }

    /* recursive function
     * searches for entities in an object and processes them for serialization
     */

    function prep(obj) {
        if (obj.__c) {
            // object is entity
            var data = {
                c: [],
                attr: {}
            };
            obj.trigger("SaveData", data, prep);
            for (var i in obj.__c) {
                data.c.push(i);
            }
            data.c = data.c.join(', ');
            obj = data;
        } else if (typeof obj == 'object') {
            // recurse and look for entities
            for (var prop in obj) {
                obj[prop] = prep(obj[prop]);
            }
        }
        return obj;
    }

    function serialize(e) {
        if (JSON) {
            var data = prep(e);
            return JSON.stringify(data);
        } else {
            alert("Crafty does not support saving on your browser. Please upgrade to a newer browser.");
            return false;
        }
    }

    // for saving a game to a central server

    function external(setUrl) {
        url = setUrl;
    }

    function openExternal() {
        if (1 && typeof url == "undefined") return;
        // get the timestamps for external saves and compare them to local
        // if the external is newer, load it

        var xml = new XMLHttpRequest();
        xhr.open("POST", url);
        xhr.onreadystatechange = function (evt) {
            if (xhr.readyState == 4) {
                if (xhr.status == 200) {
                    var data = eval("(" + xhr.responseText + ")");
                    for (var i in data) {
                        if (Crafty.storage.check(data[i].key, data[i].timestamp)) {
                            loadExternal(data[i].key);
                        }
                    }
                }
            }
        };
        xhr.send("mode=timestamps&game=" + gameName);
    }

    function saveExternal(key, data, ts) {
        if (1 && typeof url == "undefined") return;
        var xhr = new XMLHttpRequest();
        xhr.open("POST", url);
        xhr.send("mode=save&key=" + key + "&data=" + encodeURIComponent(data) + "&ts=" + ts + "&game=" + gameName);
    }

    function loadExternal(key) {
        if (1 && typeof url == "undefined") return;
        var xhr = new XMLHttpRequest();
        xhr.open("POST", url);
        xhr.onreadystatechange = function (evt) {
            if (xhr.readyState == 4) {
                if (xhr.status == 200) {
                    var data = eval("(" + xhr.responseText + ")");
                    Crafty.storage.save(key, 'save', data);
                }
            }
        };
        xhr.send("mode=load&key=" + key + "&game=" + gameName);
    }

    /**
     * get timestamp
     */

    function ts() {
        var d = new Date();
        return d.getTime();
    }

    // everyone names their object different. Fix that nonsense.
    if (typeof indexedDB != 'object') {
        window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
        window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
        window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;

        /* Numeric constants for transaction type are deprecated
         * Ensure that the script will work consistenly for recent and legacy browser versions
         */
        if (typeof IDBTransaction == 'object') {
            transactionType.READ = IDBTransaction.READ || IDBTransaction.readonly || transactionType.READ || 'read';
            transactionType.READ_WRITE = IDBTransaction.READ_WRITE || IDBTransaction.readwrite || transactionType.READ_WRITE || 'readwrite';
        }
    }

    if (typeof indexedDB == 'object') {

        return {
            open: function (gameName_n) {
                gameName = gameName_n;
                var stores = [];

                if (arguments.length == 1) {
                    stores.push('save');
                    stores.push('cache');
                } else {
                    stores = arguments;
                    stores.shift();
                    stores.push('save');
                    stores.push('cache');
                }
                if (db === null) {
                    var request = indexedDB.open(gameName);
                    request.onsuccess = function (e) {
                        db = e.target.result;
                        getTimestamps();
                        openExternal();
                    };
                    request.onupgradeneeded = function (e) {
                        createStores();
                    };
                } else {
                    createStores();
                    getTimestamps();
                    openExternal();
                }

                // get all the timestamps for existing keys

                function getTimestamps() {
                    try {
                        var trans = db.transaction(['save'], "read"),
                            store = trans.objectStore('save'),
                            request = store.getAll();
                        request.onsuccess = function (e) {
                            var i = 0,
                                a = event.target.result,
                                l = a.length;
                            for (; i < l; i++) {
                                timestamps[a[i].key] = a[i].timestamp;
                            }
                        };
                    } catch (e) {}
                }

                function createStores() {
                    var request = db.setVersion("1.0");
                    request.onsuccess = function (e) {
                        for (var i = 0; i < stores.length; i++) {
                            var st = stores[i];
                            if (db.objectStoreNames.contains(st)) continue;
                            var store = db.createObjectStore(st, {
                                keyPath: "key"
                            });
                        }
                    };
                }
            },

            save: function (key, type, data, callback) {
                if (db === null) {
                    setTimeout(function () {
                        Crafty.storage.save(key, type, data);
                    }, 1);
                    return;
                }

                var str = serialize(data),
                    t = ts();
                if (type == 'save') saveExternal(key, str, t);
                try {
                    var request = db.transaction([type], transactionType.READ_WRITE).objectStore(type).add({
                        "data": str,
                        "timestamp": t,
                        "key": key
                    });
                    if (typeof callback == 'function') {
                        request.onsuccess = callback;
                    }
                } catch (e) {
                    console.error(e);
                }
            },

            load: function (key, type, callback) {
                if (db === null) {
                    setTimeout(function () {
                        Crafty.storage.load(key, type, callback);
                    }, 1);
                    return;
                }
                try {
                    var request = db.transaction([type], transactionType.READ).objectStore(type).get(key);
                    request.onsuccess = function (e) {
                        callback(unserialize(e.target.result.data));
                    };
                } catch (e) {
                    console.error(e);
                }
            },

            getAllKeys: function (type, callback) {
                if (db === null) {
                    setTimeout(function () {
                        Crafty.storage.getAllkeys(type, callback);
                    }, 1);
                }
                try {
                    var request = db.transaction([type], transactionType.READ).objectStore(type).openCursor(),
                        res = [];
                    request.onsuccess = function (e) {
                        var cursor = e.target.result;
                        if (cursor) {
                            res.push(cursor.key);
                            // 'continue' is a reserved word, so .continue() causes IE8 to completely bark with "SCRIPT1010: Expected identifier".
                            cursor['continue']();
                        } else {
                            callback(res);
                        }
                    };
                } catch (e) {
                    console.error(e);
                }
            },

            check: function (key, timestamp) {
                return (timestamps[key] > timestamp);
            },

            external: external
        };
    } else if (typeof openDatabase == 'function') {
        return {
            open: function (gameName_n) {
                gameName = gameName_n;
                if (arguments.length == 1) {
                    db = {
                        save: openDatabase(gameName_n + '_save', '1.0', 'Saves games for ' + gameName_n, 5 * 1024 * 1024),
                        cache: openDatabase(gameName_n + '_cache', '1.0', 'Cache for ' + gameName_n, 5 * 1024 * 1024)
                    };
                } else {
                    // allows for any other types that can be thought of
                    var args = arguments,
                        i = 0;
                    args.shift();
                    for (; i < args.length; i++) {
                        if (typeof db[args[i]] == 'undefined')
                            db[args[i]] = openDatabase(gameName + '_' + args[i], '1.0', type, 5 * 1024 * 1024);
                    }
                }

                db.save.transaction(function (tx) {
                    tx.executeSql('SELECT key, timestamp FROM data', [], function (tx, res) {
                        var i = 0,
                            a = res.rows,
                            l = a.length;
                        for (; i < l; i++) {
                            timestamps[a.item(i).key] = a.item(i).timestamp;
                        }
                    });
                });
            },

            save: function (key, type, data) {
                if (typeof db[type] == 'undefined' && gameName !== '') {
                    this.open(gameName, type);
                }

                var str = serialize(data),
                    t = ts();
                if (type == 'save') saveExternal(key, str, t);
                db[type].transaction(function (tx) {
                    tx.executeSql('CREATE TABLE IF NOT EXISTS data (key unique, text, timestamp)');
                    tx.executeSql('SELECT * FROM data WHERE key = ?', [key], function (tx, results) {
                        if (results.rows.length) {
                            tx.executeSql('UPDATE data SET text = ?, timestamp = ? WHERE key = ?', [str, t, key]);
                        } else {
                            tx.executeSql('INSERT INTO data VALUES (?, ?, ?)', [key, str, t]);
                        }
                    });
                });
            },

            load: function (key, type, callback) {
                if (typeof db[type] === 'undefined') {
                    setTimeout(function () {
                        Crafty.storage.load(key, type, callback);
                    }, 1);
                    return;
                }
                db[type].transaction(function (tx) {
                    tx.executeSql('SELECT text FROM data WHERE key = ?', [key], function (tx, results) {
                        if (results.rows.length) {
                            res = unserialize(results.rows.item(0).text);
                            callback(res);
                        }
                    });
                });
            },

            getAllKeys: function (type, callback) {
                if (typeof db[type] === 'undefined') {
                    setTimeout(function () {
                        Crafty.storage.getAllKeys(type, callback);
                    }, 1);
                    return;
                }
                db[type].transaction(function (tx) {
                    tx.executeSql('SELECT key FROM data', [], function (tx, results) {
                        callback(results.rows);
                    });
                });
            },

            check: function (key, timestamp) {
                return (timestamps[key] > timestamp);
            },

            external: external
        };
    } else if (typeof window.localStorage == 'object') {
        return {
            open: function (gameName_n) {
                gameName = gameName_n;
            },

            save: function (key, type, data) {
                var k = gameName + '.' + type + '.' + key,
                    str = serialize(data),
                    t = ts();
                if (type == 'save') saveExternal(key, str, t);
                window.localStorage[k] = str;
                if (type == 'save')
                    window.localStorage[k + '.ts'] = t;
            },

            load: function (key, type, callback) {
                var k = gameName + '.' + type + '.' + key,
                    str = window.localStorage[k];

                callback(unserialize(str));
            },

            getAllKeys: function (type, callback) {
                var res = {}, output = [],
                    header = gameName + '.' + type;
                for (var i in window.localStorage) {
                    if (i.indexOf(header) != -1) {
                        var key = i.replace(header, '').replace('.ts', '');
                        res[key] = true;
                    }
                }
                for (i in res) {
                    output.push(i);
                }
                callback(output);
            },

            check: function (key, timestamp) {
                var ts = window.localStorage[gameName + '.save.' + key + '.ts'];

                return (parseInt(timestamp, 10) > parseInt(ts, 10));
            },

            external: external
        };
    } else {
        // default fallback to cookies
        return {
            open: function (gameName_n) {
                gameName = gameName_n;
            },

            save: function (key, type, data) {
                // cookies are very limited in space. we can only keep saves there
                if (type != 'save') return;
                var str = serialize(data),
                    t = ts();
                if (type == 'save') saveExternal(key, str, t);
                document.cookie = gameName + '_' + key + '=' + str + '; ' + gameName + '_' + key + '_ts=' + t + '; expires=Thur, 31 Dec 2099 23:59:59 UTC; path=/';
            },

            load: function (key, type, callback) {
                if (type != 'save') return;
                var reg = new RegExp(gameName + '_' + key + '=[^;]*'),
                    result = reg.exec(document.cookie),
                    data = unserialize(result[0].replace(gameName + '_' + key + '=', ''));

                callback(data);
            },

            getAllKeys: function (type, callback) {
                if (type != 'save') return;
                var reg = new RegExp(gameName + '_[^_=]', 'g'),
                    matches = reg.exec(document.cookie),
                    i = 0,
                    l = matches.length,
                    res = {}, output = [];
                for (; i < l; i++) {
                    var key = matches[i].replace(gameName + '_', '');
                    res[key] = true;
                }
                for (i in res) {
                    output.push(i);
                }
                callback(output);
            },

            check: function (key, timestamp) {
                var header = gameName + '_' + key + '_ts',
                    reg = new RegExp(header + '=[^;]'),
                    result = reg.exec(document.cookie),
                    ts = result[0].replace(header + '=', '');

                return (parseInt(timestamp, 10) > parseInt(ts, 10));
            },

            external: external
        };
    }
    /* template
	return {
		open: function (gameName) {
		},
		save: function (key, type, data) {
		},
		load: function (key, type, callback) {
		},
	}*/
})();<commit_msg>More documentation for storage function<commit_after>var Crafty = require('./core.js'),
    document = window.document;

// Directive for jshint to ignore evals
// eval is used to support IE8, so this can be removed if we decide to drop that
/* jshint evil:true */

/**@
 * #Storage
 * @category Utilities
 * Very simple way to get and set values, which will persist when the browser is closed also.
 */
/**@
 * #.storage
 * @comp Storage
 * @sign .storage(String key)
 * @param key - a key you would like to get from the storage. It will return null if the key does not exists.
 * @sign .storage(String key, String value)
 * @param key - the key you would like to save the data under.
 * @param value - the value you would like to save.
 * @sign .storage(String key, [Object value, Array value, Boolean value])
 * @param key - the key you would like to save the data under.
 * @param value - the value you would like to save, can be an Object or an Array.
 *
 * Storage function is very simple and can be used to either get or set values.
 * You can store both booleans, strings, objects and arrays.
 *
 * @example
 * Get an already stored value
 * ~~~
 * var playername = Crafty.storage('playername');
 * ~~~
 *
 * @example
 * Save a value
 * ~~~
 * Crafty.storage('playername', 'Hero');
 * ~~~
 *
 * @example
 * Test to see if a value is already there.
 * ~~~
 * var heroname = Crafty.storage('name');
 * if(!heroname){
 *   // Maybe ask the player what their name is here
 *   heroname = 'Guest';
 * }
 * // Do something with heroname
 * ~~~
 */

Crafty.storage = function(key, value){
  var storage = window.localStorage,
      _value = value;

  if(arguments.length === 1) {
    try {
      return JSON.parse(storage.getItem(key));
    }
    catch (e) {
      return storage.getItem(key);
    }
  } else {
    if(typeof value === "object") {
      _value = JSON.stringify(value);
    }

    storage.setItem(key, _value);

  }

};

Crafty.storage.remove = function(key){
  window.localStorage.removeItem(key);
};<|endoftext|>


<commit_before>import fetch from 'unfetch';
import cms from './utils/firebase/cms';
import firebaseUploader from './utils/firebase/uploader';

const persist = (data, roomID) => new Promise((resolve, reject) => {
  firebaseUploader.upload(data, roomID, (error, recordingID) => {
    if (error) return reject(error);
    resolve(recordingID);
  });
});

const loadPlaylist = async () => {
  if (process.env.FLAVOR !== 'cms') {
    const response = await fetch('https://storage.googleapis.com/you-move-me.appspot.com/playlists/playlist.json', {
      credentials: 'include',
    });
    const data = await response.json();
    return data;
  }
  const data = await cms.getDraftPlaylist();
  return data.playlist;
};

export default {
  persist, loadPlaylist,
};
<commit_msg>Fix reference error<commit_after>import fetch from 'unfetch';
import cms from './utils/firebase/cms';
import firebaseUploader from './utils/firebase/uploader';

const persist = (data, roomID) => new Promise((resolve, reject) => {
  firebaseUploader.upload(data, roomID, (error, recordingID) => {
    if (error) return reject(error);
    resolve(recordingID);
  });
});

const loadPlaylist = async () => {
  if (process.env.FLAVOR !== 'cms') {
    const response = await fetch('https://storage.googleapis.com/you-move-me.appspot.com/playlists/playlist.json', {
      credentials: 'include',
    });
    const data = await response.json();
    return data;
  }

  const response = await cms.getDraftPlaylist();
  return response.data.playlist;
};

export default {
  persist, loadPlaylist,
};
<|endoftext|>


<commit_before>import * as lucid from '../../../Lucid/Lucid/src';
import polymorph from '../../../Polymorph/Polymorph/src/polymorph';
import sQuery from '../../../sQuery/sQuery/src/squery';

// import * as lucid from '../../../Lucid/Lucid/dist/lucid';
// import polymorph from '../../../Polymorph/Polymorph/dist/polymorph';
// import sQuery from '../../../sQuery/sQuery/dist/squery';

import deepextend from 'deep-extend';

if (typeof window !== 'undefined') {
    // Attach Synergy tools to global object
    Object.assign(window, {
        Synergy: window.Synergy || {},
        ...lucid
    });

    // Declare global Synergy properties
    Object.assign(Synergy, {
        styleParser: polymorph,
        config: (...params) => deepextend({}, ...params),
        theme: theme
    });
}

/**
 * Synergy Theme
 *
 * @param {*} modules
 * @param {*} ui
 * @param {*} theme
 */
function theme(modules, ui, theme) {
    window.ui = Synergy.config(ui, theme);

    delete window.ui.modules;

    sQuery.init({
        modifierGlue: theme['modifier-glue'],
        componentGlue: theme['component-glue']
    });

    Synergy.CssClassProps = theme['css-class-props'];

    Object.values(modules).forEach(MODULE => MODULE.defaults && (
        window[MODULE.name] = Object.assign(MODULE, {
            config: Module.config(MODULE.defaults(ui), theme.modules[MODULE.name])
        })
    ));

    if (typeof ui.foundation === 'function') {
        ui.foundation(ui);
    }
}<commit_msg>updating readme<commit_after>import * as lucid from '../../../Lucid/Lucid/src';
import polymorph from '../../../Polymorph/Polymorph/src/polymorph';
import sQuery from '../../../sQuery/sQuery/src/squery';

// import * as lucid from '../../../Lucid/Lucid/dist/lucid';
// import polymorph from '../../../Polymorph/Polymorph/dist/polymorph';
// import sQuery from '../../../sQuery/sQuery/dist/squery';

// @TODO sQuery.init needs to be called upon importing this file...

import deepextend from 'deep-extend';

if (typeof window !== 'undefined') {
    // Attach Synergy tools to global object
    Object.assign(window, {
        Synergy: window.Synergy || {},
        ...lucid
    });

    // Declare global Synergy properties
    Object.assign(Synergy, {
        styleParser: polymorph,
        config: (...params) => deepextend({}, ...params),
        theme: theme
    });
}

/**
 * Synergy Theme
 *
 * @param {*} modules
 * @param {*} globals
 * @param {*} theme
 */
function theme(modules, theme, globals) {
    window.ui = Synergy.config(globals, theme);

    delete window.ui.modules;

    sQuery.init({
        modifierGlue: theme['modifier-glue'],
        componentGlue: theme['component-glue']
    });

    Synergy.CssClassProps = theme['css-class-props'];

    Object.values(modules).forEach(MODULE => MODULE.defaults && (
        window[MODULE.name] = Object.assign(MODULE, {
            config: Module.config(MODULE.defaults(ui), theme.modules[MODULE.name])
        })
    ));

    if (typeof ui.foundation === 'function') {
        ui.foundation(ui);
    }
}<|endoftext|>


<commit_before>// Twitter buttons
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');

$(function() {
  var customStyles = {}
  // Colorpicker for the icons
  $('#iconPicker').colorpicker().on('changeColor', function(ev){
    $('.vjs-default-skin').css('color', ev.color.toHex());
    $(this).css('background-color', ev.color.toHex());

    // Add new color to the customStyles hash
    customStyles.color = ev.color.toHex();
  });

  // Colorpicker for the progress bar
  $('#progressPicker').colorpicker().on('changeColor', function(ev){
    $('.vjs-play-progress, .vjs-volume-level').css('background-color', ev.color.toHex());
    $(this).css('background-color', ev.color.toHex());

    // Add new progress bar background color to customStyles hash
    customStyles.progress = ev.color.toHex();
  });

  // Colorpicker for the player background
  $('#backgroundPicker').colorpicker().on('changeColor', function(ev){
    var color = ev.color.toRGB();

    var colorString = 'rgba('+color.r+','+color.g+','+color.b+','+color.a+')';
    $('.vjs-control-bar, .vjs-big-play-button').css('background', colorString);

    var sliderBackground = 'rgba('+color.r+','+color.g+','+color.b+','+color.a/3+')';
    $('.vjs-slider').css('background', sliderBackground);

    $(this).css('background-color', colorString);

    // Add new player background color (in rgba() format) to customStyles hash
    customStyles.background = {
      main: colorString,
      slider: sliderBackground
    }
  });

  // Slider for font size
  $('#fontSize').slider().on('slide', function(ev){
    $('.vjs-default-skin').css('font-size', ev.value + '%');
    customStyles.fontSize = ev.value;
  });

  // Show embed code btn
  $('#showEmbedModal').click(function(e) {
    e.preventDefault();

    // Build custom <style> tag
    var style = '';
    if (customStyles.color) { style += "  .vjs-default-skin { color: "+ customStyles.color +" }\n" }
    if (customStyles.progress) { style += "  .vjs-play-progress, .vjs-volume-level { background-color: "+ customStyles.progress +" }\n" }
    if (customStyles.background) {
      style += "  .vjs-control-bar, .vjs-big-play-button { background: "+ customStyles.background.main +" }\n"
      style += "  .vjs-slider { background: "+ customStyles.background.slider +" }\n"
    }
    if (customStyles.fontSize) { style += "  .vjs-control-bar { font-size: "+ customStyles.fontSize +"% }\n" }
    if (style) {
      $('#customStyleTag').text('\n<style type="text/css">\n'+style+'</style>\n')
    }
  });

  videojs("home_video", {"height":"auto", "width":"auto"}).ready(function(){
    var myPlayer = this;    // Store the video object
    var aspectRatio = 5/12; // Make up an aspect ratio

    function resizeVideoJS(){
      // Get the parent element's actual width
      var width = document.getElementById(myPlayer.id()).parentElement.offsetWidth;
      // Set width to fill parent element, Set height
      myPlayer.width(width).height( width * aspectRatio );
    }

    resizeVideoJS(); // Initialize the function
    window.onresize = resizeVideoJS; // Call the function on resize
  });

});<commit_msg>Scoped custom style values. fixes #8<commit_after>// Twitter buttons
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');

$(function() {
  var customStyles = {}
  // Colorpicker for the icons
  $('#iconPicker').colorpicker().on('changeColor', function(ev){
    $('.vjs-default-skin').css('color', ev.color.toHex());
    $(this).css('background-color', ev.color.toHex());

    // Add new color to the customStyles hash
    customStyles.color = ev.color.toHex();
  });

  // Colorpicker for the progress bar
  $('#progressPicker').colorpicker().on('changeColor', function(ev){
    $('.vjs-play-progress, .vjs-volume-level').css('background-color', ev.color.toHex());
    $(this).css('background-color', ev.color.toHex());

    // Add new progress bar background color to customStyles hash
    customStyles.progress = ev.color.toHex();
  });

  // Colorpicker for the player background
  $('#backgroundPicker').colorpicker().on('changeColor', function(ev){
    var color = ev.color.toRGB();

    var colorString = 'rgba('+color.r+','+color.g+','+color.b+','+color.a+')';
    $('.vjs-control-bar, .vjs-big-play-button').css('background', colorString);

    var sliderBackground = 'rgba('+color.r+','+color.g+','+color.b+','+color.a/3+')';
    $('.vjs-slider').css('background', sliderBackground);

    $(this).css('background-color', colorString);

    // Add new player background color (in rgba() format) to customStyles hash
    customStyles.background = {
      main: colorString,
      slider: sliderBackground
    }
  });

  // Slider for font size
  $('#fontSize').slider().on('slide', function(ev){
    $('.vjs-default-skin').css('font-size', ev.value + '%');
    customStyles.fontSize = ev.value;
  });

  // Show embed code btn
  $('#showEmbedModal').click(function(e) {
    e.preventDefault();

    // Build custom <style> tag
    var style = '';
    if (customStyles.color) { style += "  .vjs-default-skin { color: "+ customStyles.color +" }\n" }
    if (customStyles.progress) { style += "  .vjs-default-skin .vjs-play-progress, .vjs-default-skin .vjs-volume-level { background-color: "+ customStyles.progress +" }\n" }
    if (customStyles.background) {
      style += "  .vjs-default-skin .vjs-control-bar, .vjs-default-skin .vjs-big-play-button { background: "+ customStyles.background.main +" }\n"
      style += "  .vjs-default-skin .vjs-slider { background: "+ customStyles.background.slider +" }\n"
    }
    if (customStyles.fontSize) { style += "  .vjs-default-skin .vjs-control-bar { font-size: "+ customStyles.fontSize +"% }\n" }
    if (style) {
      $('#customStyleTag').text('\n<style type="text/css">\n'+style+'</style>\n')
    }
  });

  videojs("home_video", {"height":"auto", "width":"auto"}).ready(function(){
    var myPlayer = this;    // Store the video object
    var aspectRatio = 5/12; // Make up an aspect ratio

    function resizeVideoJS(){
      // Get the parent element's actual width
      var width = document.getElementById(myPlayer.id()).parentElement.offsetWidth;
      // Set width to fill parent element, Set height
      myPlayer.width(width).height( width * aspectRatio );
    }

    resizeVideoJS(); // Initialize the function
    window.onresize = resizeVideoJS; // Call the function on resize
  });

});<|endoftext|>