<commit_before>/*
 * Copyright 2015 Samuel Mansour <nuvola-app-mixcloud@yay.ovh>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
'use strict';

var nuvola = (function(Nuvola) {
  // verbose mode
  var _debug = false;

  // activate logs
  var _log = false;

  // log to nuvola app by default
  var _console = false;

  // media player component
  var _player = Nuvola.$object(Nuvola.MediaPlayer);

  // aliases
  var PlaybackState = Nuvola.PlaybackState;
  var PlayerAction = Nuvola.PlayerAction;

  // create new WebApp prototype
  var WebApp = Nuvola.$WebApp();

  // custom store
  var Mixcloud = {
    "nodes": {},
    "scopes": {},
    "track": {
      "title": null,
      "artist": null,
      "album": null,
      "artLocation": null
    },
    "cloudcast": {
      "next": null,
      "prev": null
    },
    "html": {
      "wrapper": ["div", {
        "style": "display:none"
      }],
      "playAll": ["span", {
        "m-play-all-button": ""
      }]
    },
    "state": PlaybackState.UNKNOWN,
  };

  // initialization
  WebApp._onInitWebWorker = function(emitter) {
    Nuvola.WebApp._onInitWebWorker.call(this, emitter);

    var state = document.readyState;
    if (state === "interactive" || state === "complete") {
      this._onPageReady();
    } else {
      document.addEventListener("DOMContentLoaded", this._onPageReady.bind(this));
    }
  };

  // page is ready for magic
  WebApp._onPageReady = function() {
    // connect handler for signal ActionActivated
    Nuvola.actions.connect("ActionActivated", this);

    // start nuvola logger
    _logger = _logger.call(Nuvola);

    // build up custom nodes to communicate with the JS API
    this._injectCustomNodestoDom();

    // start update routine
    this.timeout = setInterval(this._setCallback.bind(this), 250);
  };

  // callback function for Mixcloud JS API
  WebApp._setCallback = function() {
    try {
      // API ready
      this._loadApiObjects();

      // API loaded
      clearInterval(this.timeout);

      // reset playback states
      this._reset();

      // add custom listeners
      this._setupWatchers();
    } catch (e) {
      // JS API probably not ready yet
      _logger.error(e);
    }
  };

  // set defaults
  WebApp._reset = function() {
    _logger.event("Nuvola reset");
    _player.setCanPlay(false);
    _player.setCanPause(false);
    _player.setCanGoNext(false);
    _player.setCanGoPrev(false);
    _player.setPlaybackState(PlaybackState.UNKNOWN);
    _player.setTrack(Mixcloud.track);
    _logger.success();
  };

  // load API scopes
  WebApp._loadApiObjects = function() {
    Mixcloud.scopes.global = $(document.body).scope();
    Mixcloud.scopes.PlayerQueueCtrl = $(
            document.querySelector('.ng-scope[ng-controller="PlayerQueueCtrl"]')).scope();
  };

  // watch data
  WebApp._setupWatchers = function() {
    try {
      // watch playback queue
      Mixcloud.scopes.PlayerQueueCtrl.$watch(function($scope) {
        return $scope.playerQueue.queue.cloudcastList.toArray();
      }, function(cloudcastQueue) {
        if (!_isEmpty(cloudcastQueue)) {
          _logger.event("Playback queue changed!");
          WebApp._refreshNextPrevCloudcast();
        }
      });

      // sync playback state
      Mixcloud.scopes.PlayerQueueCtrl.$watch(function($scope) {
        return Mixcloud.scopes.PlayerQueueCtrl.player.playing;
      }, function(playing) {
        _logger.event('playback state updated!');
        _defer(function() {
          var state = (playing === true) ? PlaybackState.PLAYING : PlaybackState.UNKNOWN;
          _player.setPlaybackState(state);
          _player.setCanPlay(state === PlaybackState.UNKNOWN || state === PlaybackState.PAUSED);
          _player.setCanPause(state === PlaybackState.PLAYING);
          Mixcloud.state = state;
          _logger.success();
        });
      });

      // watch track change
      Mixcloud.scopes.PlayerQueueCtrl.$watch(function($scope) {
        return Mixcloud.scopes.PlayerQueueCtrl.player.currentCloudcast;
      }, function(track) {
        if (!_isEmpty(track)) {
          _logger.event('Track loaded into the player!');
          WebApp._updateCurrentAlbum();
        }
      });

      // watch player info changes
      Mixcloud.scopes.PlayerQueueCtrl.$watch(function($scope) {
        return _getPath(Mixcloud.scopes.PlayerQueueCtrl.player, ["nowPlaying",
            "currentDisplayTrack", "title"]);
      }, function(title, previous) {
        if (title !== previous) {
          _logger.event('Track title changed in the player!');
          WebApp._updateCurrentTitle();
        }
      });

      // watch suggested tracks
      Mixcloud.scopes.PlayerQueueCtrl.$watch(function($scope) {
        return _getPath(Mixcloud.scopes.PlayerQueueCtrl.playerQueue.queue, ["upNext", "nextCloudcast"]);
      }, function(nextCloudcast) {
        if (nextCloudcast !== null) {
          _logger.event("Suggested track detected!");
          _defer(function() {
            Mixcloud.cloudcast.suggested = nextCloudcast;
            _player.setCanGoNext(true);
            _logger.success();
          });
        }
      });
    } catch (e) {
      _logger.error(e);
    }
  };

  // update previous & next track
  WebApp._refreshNextPrevCloudcast = function() {
    var currentIndex = Mixcloud.scopes.PlayerQueueCtrl.playerQueue.queue.getNowPlayingIndex();

    Mixcloud.cloudcast.next = Mixcloud.scopes.PlayerQueueCtrl.playerQueue.queue.cloudcastList
            .get(currentIndex + 1);
    Mixcloud.cloudcast.prev = Mixcloud.scopes.PlayerQueueCtrl.playerQueue.queue.cloudcastList
            .get(currentIndex - 1);

    _player.setCanGoNext(typeof Mixcloud.cloudcast.next !== "undefined");
    _player.setCanGoPrev(typeof Mixcloud.cloudcast.prev !== "undefined");

    _logger.success();
  };

  // sync album infos
  WebApp._updateCurrentAlbum = function() {
    var track = {};
    track.album = {};
    track.album.artist = _getPath(Mixcloud.scopes.PlayerQueueCtrl.player.currentCloudcast,
            ["owner"]);
    track.album.title = _getPath(Mixcloud.scopes.PlayerQueueCtrl.player.currentCloudcast, ["title"]);

    track.album = track.album.title === null || track.album.album === null ? null : Nuvola.format(
            "{1} by {2}", track.album.title, track.album.artist);

    track.artLocation = _hasPath(Mixcloud.scopes.PlayerQueueCtrl.player.currentCloudcast,
            "widgetImage") ? Nuvola.format("https:{1}",
            Mixcloud.scopes.PlayerQueueCtrl.player.currentCloudcast.widgetImage) : null;

    this._syncTrack(track);

    _logger.event("Playback changed!");
    this._refreshNextPrevCloudcast();
  };

  // sync title infos
  WebApp._updateCurrentTitle = function() {
    var track = {};
    track.artist = _getPath(Mixcloud.scopes.PlayerQueueCtrl.player, ["nowPlaying",
        "currentDisplayTrack", "artist"]);
    track.title = _getPath(Mixcloud.scopes.PlayerQueueCtrl.player, ["nowPlaying",
        "currentDisplayTrack", "title"]);

    this._syncTrack(track);
  };

  WebApp._syncTrack = function(track) {
    var track = _mergeObject(Mixcloud.track, track);
    _defer(function() {
      _player.setTrack(track);
      Mixcloud.track = track;
      _logger.success();
    });
  };

  // build custom elements and attach to DOM
  WebApp._injectCustomNodestoDom = function() {
    // build
    Mixcloud.nodes.wrapper = Nuvola.makeElement.apply(this, Mixcloud.html.wrapper);
    Mixcloud.nodes.playAll = Nuvola.makeElement.apply(this, Mixcloud.html.playAll);

    // attach to wrapper
    Mixcloud.nodes.wrapper.appendChild(Mixcloud.nodes.playAll);

    // attach to DOM
    document.body.appendChild(Mixcloud.nodes.wrapper);
  };

  // resume or play all
  WebApp._doPlay = function() {
    if (Mixcloud.scopes.global.webPlayer.playerOpen === false) {
      Nuvola.clickOnElement(Mixcloud.nodes.playAll);
    } else {
      WebApp._doPause();
    }
  };

  // pause track
  WebApp._doPause = function() {
    Mixcloud.scopes.PlayerQueueCtrl.player.togglePlayClick();
  };

  // stop track and seek to the start point
  WebApp._doStop = function() {
    Mixcloud.scopes.PlayerQueueCtrl.player.togglePlayClick();
    Mixcloud.scopes.PlayerQueueCtrl.$emit("slider:stop", 0);
  };

  // playback actions controller
  WebApp._onActionActivated = function(emitter, name) {
    try {
      switch (name) {
      case PlayerAction.TOGGLE_PLAY:
        if (Mixcloud.state === PlaybackState.PLAYING) {
          WebApp._doPause();
        } else {
          WebApp._doPlay();
        }
        break;
      case PlayerAction.PLAY:
        if (Mixcloud.state !== PlaybackState.PLAYING) {
          WebApp._doPlay();
        }
        break;
      case PlayerAction.STOP:
        if (Mixcloud.state === PlaybackState.PLAYING) {
          WebApp._doStop();
        }
        break;
      case PlayerAction.PAUSE:
        if (Mixcloud.state === PlaybackState.PLAYING) {
          Mixcloud.scopes.PlayerQueueCtrl.player.togglePlayClick();
        }
        break;
      case PlayerAction.NEXT_SONG:
        if (Mixcloud.cloudcast.next) {
          _logger.event("play next!");
          Mixcloud.scopes.PlayerQueueCtrl.playerQueue.playFromQueue(Mixcloud.cloudcast.next);
        } else {
          _logger.event("play suggested!");
          Mixcloud.scopes.PlayerQueueCtrl.playerQueue.playUpNext();
        }
        break;
      case PlayerAction.PREV_SONG:
        Mixcloud.scopes.PlayerQueueCtrl.playerQueue.playFromQueue(Mixcloud.cloudcast.prev);
        break;
      default:
        throw "Not supported action : " + name;
      }
    } catch (e) {
      _logger.error(e);
    }
  };

  // internal logger that can push to console
  var _logger = function() {
    var state = null, logger = null, console = typeof window.console == "object", internal = {};

    // Nuvola log
    internal.log = this.log.bind(this);

    // logger state mode
    state = _console && console;
    logger = state ? window.console : internal;

    return {
      // route to console if flag is true
      "logToConsole": function(flag) {
        if (typeof flag !== "boolean") {
          throw "this call require a boolean parameter";
        } else if (flag && flag !== state && window.console) {
          logger = window.console;
          state = flag;
          if (_hasPath(window, ["console", "info"])) {
            window.console.info("log to console activated!");
          }
        } else if (!flag && flag !== state) {
          logger = internal;
          state = flag;
          if (_hasPath(window, ["console", "info"])) {
            window.console.info("log to internal app activated!");
          }
        } else {
          throw "Debug is already set to : " + state;
        }
      },
      "log": function() {
        if (_log) {
          logger.log.apply(state && window.console ? window.console : this, arguments);
        }
      },
      "success": function() {
        if (_log) {
          var message = "> ok!";
          this.log.call(this, message.toUpperCase());
        }
      },
      "event": function(message) {
        if (_log) {
          message = "# " + message;
          this.log.call(this, message.toUpperCase());
        }
      },
      "error": function(obj) {
        if (_debug && _log) {
          if (state) {
            logger.error.call(window.console, obj);
          } else {
            this.log.call(this, JSON.stringify(obj));
          }
        }
      }
    };
  };

  // should prevent angular "$digest already in progress" issue
  var _defer = function(callback) {
    if (null === Mixcloud.scopes.global.$$phase) {
      Mixcloud.scopes.global.$apply(callback);
    } else {
      setTimeout.call(this, _defer.bind(this, callback), 25);
      _logger.log("_defer will retry!");
    }
  };

  // checks empty object
  var _isEmpty = function(object) {
    if (object == null || typeof object != "object") return true;
    for ( var key in object) {
      if (object.hasOwnProperty(key)) return false;
    }
    return true;
  };

  // Returns the value of the given path or null if incorrect
  var _getPath = function(obj, keys) {
    if (typeof obj !== "object") {
      return null;
    } else if (typeof keys == "string") {
      keys = keys.split(".");
    }
    var numKeys = keys.length;
    if (obj === null && numKeys > 0) return null;
    if (!(keys[0] in obj)) return null;
    if (numKeys === 1) { return obj[keys]; }
    var first = keys.shift();
    return _getPath(obj[first], keys);
  };

  // Returns a boolean indicating whether there is a property at the path
  // described by the keys given in string or array format
  var _hasPath = function(obj, keys) {
    var value = _getPath.apply(this, arguments);
    return value !== null;
  };

  // Overwrites o1's values with o2's and adds o2's if non existent in o1
  var _mergeObject = function merge_options(o1, o2) {
    var o3 = {};
    for ( var attrname in o1) {
      o3[attrname] = o1[attrname];
    }
    for ( var attrname in o2) {
      o3[attrname] = o2[attrname];
    }
    return o3;
  };

  WebApp.start();

  return {
    "status": function() {
      return (_debug || _log) && console && console.log(Mixcloud);
    },
    "debug": function(flag) {
      _logger.logToConsole.call(this, flag);
    }
  };

})(this); // function(Nuvola)<commit_msg>fix playback state<commit_after>/*
 * Copyright 2015 Samuel Mansour <nuvola-app-mixcloud@yay.ovh>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
'use strict';

var nuvola = (function(Nuvola) {
  // verbose mode
  var _debug = false;

  // activate logs
  var _log = false;

  // log to nuvola app by default
  var _console = false;

  // media player component
  var _player = Nuvola.$object(Nuvola.MediaPlayer);

  // aliases
  var PlaybackState = Nuvola.PlaybackState;
  var PlayerAction = Nuvola.PlayerAction;

  // create new WebApp prototype
  var WebApp = Nuvola.$WebApp();

  // custom store
  var Mixcloud = {
    "nodes": {},
    "scopes": {},
    "track": {
      "title": null,
      "artist": null,
      "album": null,
      "artLocation": null
    },
    "cloudcast": {
      "next": null,
      "prev": null
    },
    "html": {
      "wrapper": ["div", {
        "style": "display:none"
      }],
      "playAll": ["span", {
        "m-play-all-button": ""
      }]
    },
    "state": PlaybackState.UNKNOWN,
    "stoped" : false
  };

  // initialization
  WebApp._onInitWebWorker = function(emitter) {
    Nuvola.WebApp._onInitWebWorker.call(this, emitter);

    var state = document.readyState;
    if (state === "interactive" || state === "complete") {
      this._onPageReady();
    } else {
      document.addEventListener("DOMContentLoaded", this._onPageReady.bind(this));
    }
  };

  // page is ready for magic
  WebApp._onPageReady = function() {
    // connect handler for signal ActionActivated
    Nuvola.actions.connect("ActionActivated", this);

    // start nuvola logger
    _logger = _logger.call(Nuvola);

    // build up custom nodes to communicate with the JS API
    this._injectCustomNodestoDom();

    // start update routine
    this.timeout = setInterval(this._setCallback.bind(this), 250);
  };

  // callback function for Mixcloud JS API
  WebApp._setCallback = function() {
    try {
      // API ready
      this._loadApiObjects();

      // API loaded
      clearInterval(this.timeout);

      // reset playback states
      this._reset();

      // add custom listeners
      this._setupWatchers();
    } catch (e) {
      // JS API probably not ready yet
      _logger.error(e);
    }
  };

  // set defaults
  WebApp._reset = function() {
    _logger.event("Nuvola reset");
    _player.setCanPlay(false);
    _player.setCanPause(false);
    _player.setCanGoNext(false);
    _player.setCanGoPrev(false);
    _player.setPlaybackState(PlaybackState.UNKNOWN);
    _player.setTrack(Mixcloud.track);
    _logger.success();
  };

  // load API scopes
  WebApp._loadApiObjects = function() {
    Mixcloud.scopes.global = $(document.body).scope();
    Mixcloud.scopes.PlayerQueueCtrl = $(
            document.querySelector('.ng-scope[ng-controller="PlayerQueueCtrl"]')).scope();
  };

  // watch data
  WebApp._setupWatchers = function() {
    try {
      // watch playback queue
      Mixcloud.scopes.PlayerQueueCtrl.$watch(function($scope) {
        return $scope.playerQueue.queue.cloudcastList.toArray();
      }, function(cloudcastQueue) {
        if (!_isEmpty(cloudcastQueue)) {
          _logger.event("Playback queue changed!");
          WebApp._refreshNextPrevCloudcast();
        }
      });

      // sync playback state
      Mixcloud.scopes.PlayerQueueCtrl.$watch(function($scope) {
        return Mixcloud.scopes.PlayerQueueCtrl.player.playing;
      }, function(playing) {
        _logger.event('playback state updated!');
        _defer(function() {
          var state = (playing === true) ? PlaybackState.PLAYING : Mixcloud.stopped ? PlaybackState.UNKNOWN : PlaybackState.PAUSED;
          _player.setPlaybackState(state);
          _player.setCanPlay(state === PlaybackState.UNKNOWN || state === PlaybackState.PAUSED);
          _player.setCanPause(state === PlaybackState.PLAYING);
          Mixcloud.state = state;
          _logger.success();
        });
      });

      // watch track change
      Mixcloud.scopes.PlayerQueueCtrl.$watch(function($scope) {
        return Mixcloud.scopes.PlayerQueueCtrl.player.currentCloudcast;
      }, function(track) {
        if (!_isEmpty(track)) {
          _logger.event('Track loaded into the player!');
          WebApp._updateCurrentAlbum();
        }
      });

      // watch player info changes
      Mixcloud.scopes.PlayerQueueCtrl.$watch(function($scope) {
        return _getPath(Mixcloud.scopes.PlayerQueueCtrl.player, ["nowPlaying",
            "currentDisplayTrack", "title"]);
      }, function(title, previous) {
        if (title !== previous) {
          _logger.event('Track title changed in the player!');
          WebApp._updateCurrentTitle();
        }
      });

      // watch suggested tracks
      Mixcloud.scopes.PlayerQueueCtrl.$watch(function($scope) {
        return _getPath(Mixcloud.scopes.PlayerQueueCtrl.playerQueue.queue, ["upNext", "nextCloudcast"]);
      }, function(nextCloudcast) {
        if (nextCloudcast !== null) {
          _logger.event("Suggested track detected!");
          _defer(function() {
            Mixcloud.cloudcast.suggested = nextCloudcast;
            _player.setCanGoNext(true );
            _logger.success();
          });
        }
      });
    } catch (e) {
      _logger.error(e);
    }
  };

  // update previous & next track
  WebApp._refreshNextPrevCloudcast = function() {
    var currentIndex = Mixcloud.scopes.PlayerQueueCtrl.playerQueue.queue.getNowPlayingIndex();

    Mixcloud.cloudcast.next = Mixcloud.scopes.PlayerQueueCtrl.playerQueue.queue.cloudcastList
            .get(currentIndex + 1);
    Mixcloud.cloudcast.prev = Mixcloud.scopes.PlayerQueueCtrl.playerQueue.queue.cloudcastList
            .get(currentIndex - 1);

    _player.setCanGoNext(typeof Mixcloud.cloudcast.next !== "undefined");
    _player.setCanGoPrev(typeof Mixcloud.cloudcast.prev !== "undefined");

    _logger.success();
  };

  // sync album infos
  WebApp._updateCurrentAlbum = function() {
    var track = {};
    track.album = {};
    track.album.artist = _getPath(Mixcloud.scopes.PlayerQueueCtrl.player.currentCloudcast,
            ["owner"]);
    track.album.title = _getPath(Mixcloud.scopes.PlayerQueueCtrl.player.currentCloudcast, ["title"]);

    track.album = track.album.title === null || track.album.album === null ? null : Nuvola.format(
            "{1} by {2}", track.album.title, track.album.artist);

    track.artLocation = _hasPath(Mixcloud.scopes.PlayerQueueCtrl.player.currentCloudcast,
            "widgetImage") ? Nuvola.format("https:{1}",
            Mixcloud.scopes.PlayerQueueCtrl.player.currentCloudcast.widgetImage) : null;

    this._syncTrack(track);

    _logger.event("Playback changed!");
    this._refreshNextPrevCloudcast();
  };

  // sync title infos
  WebApp._updateCurrentTitle = function() {
    var track = {};
    track.artist = _getPath(Mixcloud.scopes.PlayerQueueCtrl.player, ["nowPlaying",
        "currentDisplayTrack", "artist"]);
    track.title = _getPath(Mixcloud.scopes.PlayerQueueCtrl.player, ["nowPlaying",
        "currentDisplayTrack", "title"]);

    this._syncTrack(track);
  };

  WebApp._syncTrack = function(track) {
    var track = _mergeObject(Mixcloud.track, track);
    _defer(function() {
      _player.setTrack(track);
      Mixcloud.track = track;
      _logger.success();
    });
  };

  // build custom elements and attach to DOM
  WebApp._injectCustomNodestoDom = function() {
    // build
    Mixcloud.nodes.wrapper = Nuvola.makeElement.apply(this, Mixcloud.html.wrapper);
    Mixcloud.nodes.playAll = Nuvola.makeElement.apply(this, Mixcloud.html.playAll);

    // attach to wrapper
    Mixcloud.nodes.wrapper.appendChild(Mixcloud.nodes.playAll);

    // attach to DOM
    document.body.appendChild(Mixcloud.nodes.wrapper);
  };

  // resume or play all
  WebApp._doPlay = function() {
    if (Mixcloud.scopes.global.webPlayer.playerOpen === false) {
      Nuvola.clickOnElement(Mixcloud.nodes.playAll);
    } else {
      WebApp._doPause();
    }

    Mixcloud.stopped = false;
  };

  // pause track
  WebApp._doPause = function() {
    Mixcloud.scopes.PlayerQueueCtrl.player.togglePlayClick();
  };

  // stop track and seek to the start point
  WebApp._doStop = function() {
    Mixcloud.scopes.PlayerQueueCtrl.player.togglePlayClick();
    Mixcloud.scopes.PlayerQueueCtrl.$emit("slider:stop", 0);
    Mixcloud.stopped = true;
  };

  // playback actions controller
  WebApp._onActionActivated = function(emitter, name) {
    try {
      switch (name) {
      case PlayerAction.TOGGLE_PLAY:
        if (Mixcloud.state === PlaybackState.PLAYING) {
          WebApp._doPause();
        } else {
          WebApp._doPlay();
        }
        break;
      case PlayerAction.PLAY:
        if (Mixcloud.state !== PlaybackState.PLAYING) {
          WebApp._doPlay();
        }
        break;
      case PlayerAction.STOP:
        if (Mixcloud.state === PlaybackState.PLAYING) {
          WebApp._doStop();
        }
        break;
      case PlayerAction.PAUSE:
        if (Mixcloud.state === PlaybackState.PLAYING) {
          Mixcloud.scopes.PlayerQueueCtrl.player.togglePlayClick();
        }
        break;
      case PlayerAction.NEXT_SONG:
        if (Mixcloud.cloudcast.next) {
          _logger.event("play next!");
          Mixcloud.scopes.PlayerQueueCtrl.playerQueue.playFromQueue(Mixcloud.cloudcast.next);
        } else {
          _logger.event("play suggested!");
          Mixcloud.scopes.PlayerQueueCtrl.playerQueue.playUpNext();
        }
        break;
      case PlayerAction.PREV_SONG:
        Mixcloud.scopes.PlayerQueueCtrl.playerQueue.playFromQueue(Mixcloud.cloudcast.prev);
        break;
      default:
        throw "Not supported action : " + name;
      }
    } catch (e) {
      _logger.error(e);
    }
  };

  // internal logger that can push to console
  var _logger = function() {
    var state = null, logger = null, console = typeof window.console == "object", internal = {};

    // Nuvola log
    internal.log = this.log.bind(this);

    // logger state mode
    state = _console && console;
    logger = state ? window.console : internal;

    return {
      // route to console if flag is true
      "logToConsole": function(flag) {
        if (typeof flag !== "boolean") {
          throw "this call require a boolean parameter";
        } else if (flag && flag !== state && window.console) {
          logger = window.console;
          state = flag;
          if (_hasPath(window, ["console", "info"])) {
            window.console.info("log to console activated!");
          }
        } else if (!flag && flag !== state) {
          logger = internal;
          state = flag;
          if (_hasPath(window, ["console", "info"])) {
            window.console.info("log to internal app activated!");
          }
        } else {
          throw "Debug is already set to : " + state;
        }
      },
      "log": function() {
        if (_log) {
          logger.log.apply(state && window.console ? window.console : this, arguments);
        }
      },
      "success": function() {
        if (_log) {
          var message = "> ok!";
          this.log.call(this, message.toUpperCase());
        }
      },
      "event": function(message) {
        if (_log) {
          message = "# " + message;
          this.log.call(this, message.toUpperCase());
        }
      },
      "error": function(obj) {
        if (_debug && _log) {
          if (state) {
            logger.error.call(window.console, obj);
          } else {
            this.log.call(this, JSON.stringify(obj));
          }
        }
      }
    };
  };

  // should prevent angular "$digest already in progress" issue
  var _defer = function(callback) {
    if (null === Mixcloud.scopes.global.$$phase) {
      Mixcloud.scopes.global.$apply(callback);
    } else {
      setTimeout.call(this, _defer.bind(this, callback), 25);
      _logger.log("_defer will retry!");
    }
  };

  // checks empty object
  var _isEmpty = function(object) {
    if (object == null || typeof object != "object") return true;
    for ( var key in object) {
      if (object.hasOwnProperty(key)) return false;
    }
    return true;
  };

  // Returns the value of the given path or null if incorrect
  var _getPath = function(obj, keys) {
    if (typeof obj !== "object") {
      return null;
    } else if (typeof keys == "string") {
      keys = keys.split(".");
    }
    var numKeys = keys.length;
    if (obj === null && numKeys > 0) return null;
    if (!(keys[0] in obj)) return null;
    if (numKeys === 1) { return obj[keys]; }
    var first = keys.shift();
    return _getPath(obj[first], keys);
  };

  // Returns a boolean indicating whether there is a property at the path
  // described by the keys given in string or array format
  var _hasPath = function(obj, keys) {
    var value = _getPath.apply(this, arguments);
    return value !== null;
  };

  // Overwrites o1's values with o2's and adds o2's if non existent in o1
  var _mergeObject = function merge_options(o1, o2) {
    var o3 = {};
    for ( var attrname in o1) {
      o3[attrname] = o1[attrname];
    }
    for ( var attrname in o2) {
      o3[attrname] = o2[attrname];
    }
    return o3;
  };

  WebApp.start();

  return {
    "status": function() {
      return (_debug || _log) && console && console.log(Mixcloud);
    },
    "debug": function(flag) {
      _logger.logToConsole.call(this, flag);
    }
  };

})(this); // function(Nuvola)<|endoftext|>


<commit_before>/*
 * Copyright 2015 Jason Day <jason@jlogday.com>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

"use strict";

(function(Nuvola)
{

// Create media player component
var player = Nuvola.$object(Nuvola.MediaPlayer);

// Handy aliases
var PlaybackState = Nuvola.PlaybackState;
var PlayerAction = Nuvola.PlayerAction;

// Create new WebApp prototype
var WebApp = Nuvola.$WebApp();

// Initialization routines
WebApp._onInitWebWorker = function(emitter)
{
    Nuvola.WebApp._onInitWebWorker.call(this, emitter);

    var state = document.readyState;
    if (state === "interactive" || state === "complete") {
        this._onPageReady();
    }
    else {
        document.addEventListener("DOMContentLoaded", this._onPageReady.bind(this));
    }
}

// Page is ready for magic
WebApp._onPageReady = function()
{
    // Connect handler for signal ActionActivated
    Nuvola.actions.connect("ActionActivated", this);

    // Start update routine
    this.update();
}

function fetchTrackInfo(className) {
    var rval = null;
    var nl = document.getElementsByClassName(className);
    if (nl.length > 0) {
        rval = nl[0].innerText;
    }

    return rval;
}

function fetchButton(className) {
    var rval = null;
    var nl = document.getElementsByClassName(className);
    if (nl.length > 0) {
        rval = nl[0];
    }

    return rval;
}

function isHidden(elmt) {
    return elmt.className.indexOf('hidden') !== -1;
}

function isDisabled(elmt) {
    return elmt.className.indexOf('disabled') !== -1;
}

// Extract data from the web page
WebApp.update = function()
{
    var track = {};

    // album only appears to be stored as an attribute in a different element
    track.album = null;
    track.artist = fetchTrackInfo('grandparent-title') || 'Unknown Artist';
    track.title = fetchTrackInfo('item-title') || 'Unknown Title';

    player.setTrack(track);

    var state = PlaybackState.UNKNOWN;
    var prevSong = false;
    var nextSong = false;
    try {
        var playButton = document.querySelector('div.mini-controls-center-buttons button.play-btn');
        if (playButton) {
            if (isHidden(playButton)) {
                state = PlaybackState.PLAYING;
            }
            else {
                state = PlaybackState.PAUSED;
            }

            var prevButton = document.querySelector('div.mini-controls-center-buttons button.previous-btn');
            prevSong = prevButton && !isDisabled(prevButton);
            var nextButton = document.querySelector('div.mini-controls-center-buttons button.next-btn');
            nextSong = nextButton && !isDisabled(nextButton);
        }
    }
    catch (e) {
        state = PlaybackState.UNKNOWN;
    }

    player.setPlaybackState(state);
    player.setCanPause(state === PlaybackState.PLAYING);
    player.setCanPlay(state === PlaybackState.PAUSED || state === PlaybackState.UNKNOWN);
    player.setCanGoPrev(prevSong);
    player.setCanGoNext(nextSong);

    // Schedule the next update
    setTimeout(this.update.bind(this), 500);
}

// Handler of playback actions
WebApp._onActionActivated = function(emitter, name, param) {
    switch (name) {
        case PlayerAction.TOGGLE_PLAY:
            var pp;
            var playButton = document.querySelector('div.mini-controls-center-buttons button.play-btn');

            if (playButton && !isHidden(playButton)) {
                pp = playButton;
            }
            else {
                var pauseButton = fetchButton('pause-btn');
                var pauseButton = document.querySelector('div.mini-controls-center-buttons button.pause-btn');
                if (pauseButton && !isHidden(pauseButton)) {
                    pp = pauseButton;
                }
            }

            if (pp) {
                Nuvola.clickOnElement(pp);
            }

            break;
        case PlayerAction.PLAY:
            var playButton = document.querySelector('div.mini-controls-center-buttons button.play-btn');
            if (playButton) {
                Nuvola.clickOnElement(playButton);
            }

            break;
        case PlayerAction.PAUSE:
            var pauseButton = document.querySelector('div.mini-controls-center-buttons button.pause-btn');
            if (pauseButton) {
                Nuvola.clickOnElement(pauseButton);
            }

            break;
        case PlayerAction.STOP:
            var stopButton = document.querySelector('div.mini-controls-center-buttons button.stop-btn');
            if (stopButton) {
                Nuvola.clickOnElement(stopButton);
            }

            break;
        case PlayerAction.PREV_SONG:
            var prevButton = document.querySelector('div.mini-controls-center-buttons button.previous-btn');
            if (prevButton) {
                Nuvola.clickOnElement(prevButton);
            }

            break;
        case PlayerAction.PREV_SONG:
            var nextButton = document.querySelector('div.mini-controls-center-buttons button.next-btn');
            if (nextButton) {
                Nuvola.clickOnElement(nextButton);
            }

            break;
    }
}

WebApp.start();

})(this);  // function(Nuvola)
<commit_msg>Fix typo in play action<commit_after>/*
 * Copyright 2015 Jason Day <jason@jlogday.com>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

"use strict";

(function(Nuvola)
{

// Create media player component
var player = Nuvola.$object(Nuvola.MediaPlayer);

// Handy aliases
var PlaybackState = Nuvola.PlaybackState;
var PlayerAction = Nuvola.PlayerAction;

// Create new WebApp prototype
var WebApp = Nuvola.$WebApp();

// Initialization routines
WebApp._onInitWebWorker = function(emitter)
{
    Nuvola.WebApp._onInitWebWorker.call(this, emitter);

    var state = document.readyState;
    if (state === "interactive" || state === "complete") {
        this._onPageReady();
    }
    else {
        document.addEventListener("DOMContentLoaded", this._onPageReady.bind(this));
    }
}

// Page is ready for magic
WebApp._onPageReady = function()
{
    // Connect handler for signal ActionActivated
    Nuvola.actions.connect("ActionActivated", this);

    // Start update routine
    this.update();
}

function fetchTrackInfo(className) {
    var rval = null;
    var nl = document.getElementsByClassName(className);
    if (nl.length > 0) {
        rval = nl[0].innerText;
    }

    return rval;
}

function fetchButton(className) {
    var rval = null;
    var nl = document.getElementsByClassName(className);
    if (nl.length > 0) {
        rval = nl[0];
    }

    return rval;
}

function isHidden(elmt) {
    return elmt.className.indexOf('hidden') !== -1;
}

function isDisabled(elmt) {
    return elmt.className.indexOf('disabled') !== -1;
}

// Extract data from the web page
WebApp.update = function()
{
    var track = {};

    // album only appears to be stored as an attribute in a different element
    track.album = null;
    track.artist = fetchTrackInfo('grandparent-title') || 'Unknown Artist';
    track.title = fetchTrackInfo('item-title') || 'Unknown Title';

    player.setTrack(track);

    var state = PlaybackState.UNKNOWN;
    var prevSong = false;
    var nextSong = false;
    try {
        var playButton = document.querySelector('div.mini-controls-center-buttons button.play-btn');
        if (playButton) {
            if (isHidden(playButton)) {
                state = PlaybackState.PLAYING;
            }
            else {
                state = PlaybackState.PAUSED;
            }

            var prevButton = document.querySelector('div.mini-controls-center-buttons button.previous-btn');
            prevSong = prevButton && !isDisabled(prevButton);
            var nextButton = document.querySelector('div.mini-controls-center-buttons button.next-btn');
            nextSong = nextButton && !isDisabled(nextButton);
        }
    }
    catch (e) {
        state = PlaybackState.UNKNOWN;
    }

    player.setPlaybackState(state);
    player.setCanPause(state === PlaybackState.PLAYING);
    player.setCanPlay(state === PlaybackState.PAUSED || state === PlaybackState.UNKNOWN);
    player.setCanGoPrev(prevSong);
    player.setCanGoNext(nextSong);

    // Schedule the next update
    setTimeout(this.update.bind(this), 500);
}

// Handler of playback actions
WebApp._onActionActivated = function(emitter, name, param) {
    switch (name) {
        case PlayerAction.TOGGLE_PLAY:
            var pp;
            var playButton = document.querySelector('div.mini-controls-center-buttons button.play-btn');

            if (playButton && !isHidden(playButton)) {
                pp = playButton;
            }
            else {
                var pauseButton = fetchButton('pause-btn');
                var pauseButton = document.querySelector('div.mini-controls-center-buttons button.pause-btn');
                if (pauseButton && !isHidden(pauseButton)) {
                    pp = pauseButton;
                }
            }

            if (pp) {
                Nuvola.clickOnElement(pp);
            }

            break;
        case PlayerAction.PLAY:
            var playButton = document.querySelector('div.mini-controls-center-buttons button.play-btn');
            if (playButton) {
                Nuvola.clickOnElement(playButton);
            }

            break;
        case PlayerAction.PAUSE:
            var pauseButton = document.querySelector('div.mini-controls-center-buttons button.pause-btn');
            if (pauseButton) {
                Nuvola.clickOnElement(pauseButton);
            }

            break;
        case PlayerAction.STOP:
            var stopButton = document.querySelector('div.mini-controls-center-buttons button.stop-btn');
            if (stopButton) {
                Nuvola.clickOnElement(stopButton);
            }

            break;
        case PlayerAction.PREV_SONG:
            var prevButton = document.querySelector('div.mini-controls-center-buttons button.previous-btn');
            if (prevButton) {
                Nuvola.clickOnElement(prevButton);
            }

            break;
        case PlayerAction.NEXT_SONG:
            var nextButton = document.querySelector('div.mini-controls-center-buttons button.next-btn');
            if (nextButton) {
                Nuvola.clickOnElement(nextButton);
            }

            break;
    }
}

WebApp.start();

})(this);  // function(Nuvola)
<|endoftext|>


<commit_before>var cli = require('./index');

/**
 *  Abstract program interface.
 *
 *  Encourages decoupling of program configuration from
 *  the executable to enable testing applications without
 *  creating a child process.
 *
 *  This allows code instrumentation and therefore code
 *  coverage.
 *
 *  @param pkg The package descriptor, object or string.
 *  @param name The program name.
 *  @param description The program description.
 *  @param clazz A Program subclass to instantiate.
 */
var CommandInterface = function(pkg, name, description, clazz) {
  this.program = cli(pkg, name, description, clazz);
  this.configure.call(this.program);
  this.use.call(this.program);
  this.command.call(this.program);
  this.option.call(this.program);
  this.on.call(this.program);
};

/**
 *  Configure the program, scope is the program.
 */
CommandInterface.prototype.configure = function(){};

/**
 *  Configure middleware, scope is the program.
 */
CommandInterface.prototype.use = function(){};

/**
 *  Configure command options, scope is the program.
 */
CommandInterface.prototype.command = function(){};

/**
 *  Configure argument options, scope is the program.
 */
CommandInterface.prototype.option = function(){};

/**
 *  Configure events, scope is the program.
 */
CommandInterface.prototype.events = function(){};

/**
 *  Parse the program arguments, proxies to the program instance.
 */
CommandInterface.prototype.parse = function() {
  return this.program.parse.apply(this.program, arguments);
};

module.exports = CommandInterface;

// legacy, do not use, will be removed
module.exports.CommandInterface = CommandInterface;
module.exports.cli = cli;
<commit_msg>Import interface class to deprecate cli-interface.<commit_after>var cli = require('./index');

/**
 *  Abstract program interface.
 *
 *  Encourages decoupling of program configuration from
 *  the executable to enable testing applications without
 *  creating a child process.
 *
 *  This allows code instrumentation and therefore code
 *  coverage.
 *
 *  @param pkg The package descriptor, object or string.
 *  @param name The program name.
 *  @param description The program description.
 *  @param clazz A Program subclass to instantiate.
 */
var CommandInterface = function(pkg, name, description, clazz) {
  this.program = cli(pkg, name, description, clazz);
  this.configure.call(this.program);
  this.use.call(this.program);
  this.command.call(this.program);
  this.option.call(this.program);
  this.events.call(this.program);
};

/**
 *  Configure the program, scope is the program.
 */
CommandInterface.prototype.configure = function(){};

/**
 *  Configure middleware, scope is the program.
 */
CommandInterface.prototype.use = function(){};

/**
 *  Configure command options, scope is the program.
 */
CommandInterface.prototype.command = function(){};

/**
 *  Configure argument options, scope is the program.
 */
CommandInterface.prototype.option = function(){};

/**
 *  Configure events, scope is the program.
 */
CommandInterface.prototype.events = function(){};

/**
 *  Parse the program arguments, proxies to the program instance.
 */
CommandInterface.prototype.parse = function() {
  return this.program.parse.apply(this.program, arguments);
};

module.exports = CommandInterface;

// legacy, do not use, will be removed
module.exports.CommandInterface = CommandInterface;
module.exports.cli = cli;
<|endoftext|>


<commit_before>import { setRoom } from '../base/conference';
import { setLocationURL } from '../base/connection';
import { setConfig } from '../base/config';
import { loadConfig } from '../base/lib-jitsi-meet';
import { parseURIString } from '../base/util';

import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';

declare var APP: Object;

/**
 * Triggers an in-app navigation to a specific route. Allows navigation to be
 * abstracted between the mobile/React Native and Web/React applications.
 *
 * @param {(string|undefined)} uri - The URI to which to navigate. It may be a
 * full URL with an HTTP(S) scheme, a full or partial URI with the app-specific
 * scheme, or a mere room name.
 * @returns {Function}
 */
export function appNavigate(uri: ?string) {
    return (dispatch: Dispatch<*>, getState: Function) =>
        _appNavigateToOptionalLocation(dispatch, getState, parseURIString(uri));
}

/**
 * Triggers an in-app navigation to a specific location URI.
 *
 * @param {Dispatch} dispatch - The redux {@code dispatch} function.
 * @param {Function} getState - The redux function that gets/retrieves the redux
 * state.
 * @param {Object} newLocation - The location URI to navigate to. The value
 * cannot be undefined and is assumed to have all properties such as
 * {@code host}, {@code contextRoot}, and {@code room} defined. Depending on the
 * property, it may have a value equal to {@code undefined} and that may be
 * acceptable.
 * @private
 * @returns {void}
 */
function _appNavigateToMandatoryLocation(
        dispatch: Dispatch<*>, getState: Function,
        newLocation: Object) {
    _loadConfig(newLocation)
        .then(
            config => configLoaded(/* err */ undefined, config),
            err => configLoaded(err, /* config */ undefined))
        .then(() => dispatch(setRoom(newLocation.room)));

    /**
     * Notifies that an attempt to load a configuration has completed. Due to
     * the asynchronous nature of the loading, the specified <tt>config</tt> may
     * or may not be required by the time the notification arrives.
     *
     * @param {string|undefined} error - If the loading has failed, the error
     * detailing the cause of the failure.
     * @param {Object|undefined} config - If the loading has succeeded, the
     * loaded configuration.
     * @returns {void}
     */
    function configLoaded(error, config) {
        // FIXME Due to the asynchronous nature of the loading, the specified
        // config may or may not be required by the time the notification
        // arrives.

        if (error) {
            // XXX The failure could be, for example, because of a
            // certificate-related error. In which case the connection will
            // fail later in Strophe anyway even if we use the default
            // config here.

            // The function loadConfig will log the err.
            return;
        }

        return (
            dispatch(setLocationURL(new URL(newLocation.toString())))
                .then(() => dispatch(setConfig(config))));
    }
}

/**
 * Triggers an in-app navigation to a specific or undefined location (URI).
 *
 * @param {Dispatch} dispatch - The redux {@code dispatch} function.
 * @param {Function} getState - The redux function that gets/retrieves the redux
 * state.
 * @param {Object} location - The location (URI) to navigate to. The value may
 * be undefined.
 * @private
 * @returns {void}
 */
function _appNavigateToOptionalLocation(
        dispatch: Dispatch<*>, getState: Function,
        location: Object) {
    // If the specified location (URI) does not identify a host, use the app's
    // default.
    if (!location || !location.host) {
        const defaultLocation
            = parseURIString(getState()['features/app'].app._getDefaultURL());

        if (location) {
            location.host = defaultLocation.host;

            // FIXME Turn location's host, hostname, and port properties into
            // setters in order to reduce the risks of inconsistent state.
            location.hostname = defaultLocation.hostname;
            location.port = defaultLocation.port;
            location.protocol = defaultLocation.protocol;
        } else {
            // eslint-disable-next-line no-param-reassign
            location = defaultLocation;
        }
    }

    location.protocol || (location.protocol = 'https:');

    _appNavigateToMandatoryLocation(dispatch, getState, location);
}

/**
 * Signals that a specific App will mount (in the terms of React).
 *
 * @param {App} app - The App which will mount.
 * @returns {{
 *     type: APP_WILL_MOUNT,
 *     app: App
 * }}
 */
export function appWillMount(app) {
    return (dispatch: Dispatch<*>) => {
        dispatch({
            type: APP_WILL_MOUNT,
            app
        });

        // TODO There was a redux action creator appInit which I did not like
        // because we already had the redux action creator appWillMount and,
        // respectively, the redux action APP_WILL_MOUNT. So I set out to remove
        // appInit and managed to move everything it was doing but the
        // following. Which is not extremely bad because we haven't moved the
        // API module into its own feature yet so we're bound to work on that in
        // the future.
        typeof APP === 'object' && APP.API.init();
    };
}

/**
 * Signals that a specific App will unmount (in the terms of React).
 *
 * @param {App} app - The App which will unmount.
 * @returns {{
 *     type: APP_WILL_UNMOUNT,
 *     app: App
 * }}
 */
export function appWillUnmount(app) {
    return {
        type: APP_WILL_UNMOUNT,
        app
    };
}

/**
 * Loads config.js from a specific host.
 *
 * @param {Object} location - The location URI which specifies the host to load
 * the config.js from.
 * @private
 * @returns {Promise<Object>}
 */
function _loadConfig({ contextRoot, host, protocol, room }) {
    // XXX As the mobile/React Native app does not employ config on the
    // WelcomePage, do not download config.js from the deployment when
    // navigating to the WelcomePage - the perceived/visible navigation will be
    // faster.
    if (!room && typeof APP === 'undefined') {
        return Promise.resolve();
    }

    /* eslint-disable no-param-reassign */

    protocol = protocol.toLowerCase();

    // The React Native app supports an app-specific scheme which is sure to not
    // be supported by fetch (or whatever loadConfig utilizes).
    protocol !== 'http:' && protocol !== 'https:' && (protocol = 'https:');

    // TDOO userinfo

    let url = `${protocol}//${host}${contextRoot || '/'}config.js`;

    // XXX In order to support multiple shards, tell the room to the deployment.
    room && (url += `?room=${room.toLowerCase()}`);

    /* eslint-enable no-param-reassign */

    return loadConfig(url);
}
<commit_msg>[RN] Cache configurations in localStorage<commit_after>import { setRoom } from '../base/conference';
import { setLocationURL } from '../base/connection';
import { setConfig } from '../base/config';
import { loadConfig } from '../base/lib-jitsi-meet';
import { parseURIString } from '../base/util';

import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';

declare var APP: Object;

/**
 * Triggers an in-app navigation to a specific route. Allows navigation to be
 * abstracted between the mobile/React Native and Web/React applications.
 *
 * @param {(string|undefined)} uri - The URI to which to navigate. It may be a
 * full URL with an HTTP(S) scheme, a full or partial URI with the app-specific
 * scheme, or a mere room name.
 * @returns {Function}
 */
export function appNavigate(uri: ?string) {
    return (dispatch: Dispatch<*>, getState: Function) =>
        _appNavigateToOptionalLocation(dispatch, getState, parseURIString(uri));
}

/**
 * Triggers an in-app navigation to a specific location URI.
 *
 * @param {Dispatch} dispatch - The redux {@code dispatch} function.
 * @param {Function} getState - The redux function that gets/retrieves the redux
 * state.
 * @param {Object} newLocation - The location URI to navigate to. The value
 * cannot be undefined and is assumed to have all properties such as
 * {@code host}, {@code contextRoot}, and {@code room} defined. Depending on the
 * property, it may have a value equal to {@code undefined} and that may be
 * acceptable.
 * @private
 * @returns {void}
 */
function _appNavigateToMandatoryLocation(
        dispatch: Dispatch<*>, getState: Function,
        newLocation: Object) {
    _loadConfig(newLocation)
        .then(
            config => configLoaded(/* err */ undefined, config),
            err => configLoaded(err, /* config */ undefined))
        .then(() => dispatch(setRoom(newLocation.room)));

    /**
     * Notifies that an attempt to load a configuration has completed. Due to
     * the asynchronous nature of the loading, the specified <tt>config</tt> may
     * or may not be required by the time the notification arrives.
     *
     * @param {string|undefined} error - If the loading has failed, the error
     * detailing the cause of the failure.
     * @param {Object|undefined} config - If the loading has succeeded, the
     * loaded configuration.
     * @returns {void}
     */
    function configLoaded(error, config) {
        // FIXME Due to the asynchronous nature of the loading, the specified
        // config may or may not be required by the time the notification
        // arrives.

        if (error) {
            // XXX The failure could be, for example, because of a
            // certificate-related error. In which case the connection will
            // fail later in Strophe anyway even if we use the default
            // config here.

            // The function loadConfig will log the err.
            return;
        }

        return (
            dispatch(setLocationURL(new URL(newLocation.toString())))
                .then(() => dispatch(setConfig(config))));
    }
}

/**
 * Triggers an in-app navigation to a specific or undefined location (URI).
 *
 * @param {Dispatch} dispatch - The redux {@code dispatch} function.
 * @param {Function} getState - The redux function that gets/retrieves the redux
 * state.
 * @param {Object} location - The location (URI) to navigate to. The value may
 * be undefined.
 * @private
 * @returns {void}
 */
function _appNavigateToOptionalLocation(
        dispatch: Dispatch<*>, getState: Function,
        location: Object) {
    // If the specified location (URI) does not identify a host, use the app's
    // default.
    if (!location || !location.host) {
        const defaultLocation
            = parseURIString(getState()['features/app'].app._getDefaultURL());

        if (location) {
            location.host = defaultLocation.host;

            // FIXME Turn location's host, hostname, and port properties into
            // setters in order to reduce the risks of inconsistent state.
            location.hostname = defaultLocation.hostname;
            location.port = defaultLocation.port;
            location.protocol = defaultLocation.protocol;
        } else {
            // eslint-disable-next-line no-param-reassign
            location = defaultLocation;
        }
    }

    location.protocol || (location.protocol = 'https:');

    _appNavigateToMandatoryLocation(dispatch, getState, location);
}

/**
 * Signals that a specific App will mount (in the terms of React).
 *
 * @param {App} app - The App which will mount.
 * @returns {{
 *     type: APP_WILL_MOUNT,
 *     app: App
 * }}
 */
export function appWillMount(app) {
    return (dispatch: Dispatch<*>) => {
        dispatch({
            type: APP_WILL_MOUNT,
            app
        });

        // TODO There was a redux action creator appInit which I did not like
        // because we already had the redux action creator appWillMount and,
        // respectively, the redux action APP_WILL_MOUNT. So I set out to remove
        // appInit and managed to move everything it was doing but the
        // following. Which is not extremely bad because we haven't moved the
        // API module into its own feature yet so we're bound to work on that in
        // the future.
        typeof APP === 'object' && APP.API.init();
    };
}

/**
 * Signals that a specific App will unmount (in the terms of React).
 *
 * @param {App} app - The App which will unmount.
 * @returns {{
 *     type: APP_WILL_UNMOUNT,
 *     app: App
 * }}
 */
export function appWillUnmount(app) {
    return {
        type: APP_WILL_UNMOUNT,
        app
    };
}

/**
 * Loads config.js from a specific host.
 *
 * @param {Object} location - The location URI which specifies the host to load
 * the config.js from.
 * @private
 * @returns {Promise<Object>}
 */
function _loadConfig({ contextRoot, host, protocol, room }) {
    // XXX As the mobile/React Native app does not employ config on the
    // WelcomePage, do not download config.js from the deployment when
    // navigating to the WelcomePage - the perceived/visible navigation will be
    // faster.
    if (!room && typeof APP === 'undefined') {
        return Promise.resolve();
    }

    /* eslint-disable no-param-reassign */

    protocol = protocol.toLowerCase();

    // The React Native app supports an app-specific scheme which is sure to not
    // be supported by fetch (or whatever loadConfig utilizes).
    protocol !== 'http:' && protocol !== 'https:' && (protocol = 'https:');

    // TDOO userinfo

    const rootUrl = `${protocol}//${host}${contextRoot || '/'}`;
    let url = `${rootUrl}config.js`;

    // XXX In order to support multiple shards, tell the room to the deployment.
    room && (url += `?room=${room.toLowerCase()}`);

    /* eslint-enable no-param-reassign */

    const key = `config/${rootUrl}`;

    return (
        loadConfig(url)
            .then(config => {
                // Try to store the configuration in localStorage. If the
                // deployment specified the 'getroom' option as a function, for
                // example, we cannot store it, so don't.
                try {
                    window.localStorage.setItem(key, JSON.stringify(config));
                } catch (e) {

                    // Ignore the error, we won't cache this config.
                }

                return config;
            })
            .catch(error => {
                // We failed to load the requested config, try to use the last
                // one which was fetched for that deployment. It may not match
                // the shard, but it's probably better than nothing.
                const config = window.localStorage.getItem(key);

                if (config) {
                    try {
                        return JSON.parse(config);
                    } catch (e) {

                        // Somehow incorrect data ended up in the storage. Clean
                        // up.
                        window.localStorage.removeItem(key);
                    }
                }

                throw error;
            })
    );
}
<|endoftext|>


<commit_before>
/**
 * Module dependencies.
 */

var debug = require('debug')('wpcom:follow');

/**
 * Follow
 *
 * @param {String} site_id - site id
 * @param {WPCOM} wpcom
 * @api public
 */

function Follow(site_id, wpcom) {
  if (!site_id) {
    throw new Error('`site id` is not correctly defined');
  }

  if (!(this instanceof Follow)) {
    return new Follow(site_id, wpcom);
  }

  this.wpcom = wpcom;
  this._sid = site_id;
}

/**
 * Get the follow status for current
 * user on current blog sites
 *
 * @param {Object} [query]
 * @param {Function} fn
 */

Follow.prototype.state =
Follow.prototype.mine = function (query, fn) {
  var path = '/sites/' + this._sid + '/follows/mine';
  return this.wpcom.req.get(path, query, fn);
};

/**
 * Follow the site
 *
 * @param {Object} [query]
 * @param {Function} fn
 */

Follow.prototype.follow =
Follow.prototype.add = function (query, fn) {
  var path = '/sites/' + this._sid + '/follows/new';
  return this.wpcom.req.put(path, query, null, fn);
};

/**
 * Unfollow the site
 *
 * @param {Object} [query]
 * @param {Function} fn
 */

Follow.prototype.unfollow =
Follow.prototype.del = function (query, fn) {
  var path = '/sites/' + this._sid + '/follows/mine/delete';
  return this.wpcom.req.del(path, query, null, fn);
};

/**
 * Expose `Follow` module
 */

module.exports = Follow;<commit_msg>Add missing '@api public' tags<commit_after>
/**
 * Module dependencies.
 */

var debug = require('debug')('wpcom:follow');

/**
 * Follow
 *
 * @param {String} site_id - site id
 * @param {WPCOM} wpcom
 * @api public
 */

function Follow(site_id, wpcom) {
  if (!site_id) {
    throw new Error('`site id` is not correctly defined');
  }

  if (!(this instanceof Follow)) {
    return new Follow(site_id, wpcom);
  }

  this.wpcom = wpcom;
  this._sid = site_id;
}

/**
 * Get the follow status for current
 * user on current blog sites
 *
 * @param {Object} [query]
 * @param {Function} fn
 * @api public
 */

Follow.prototype.state =
Follow.prototype.mine = function (query, fn) {
  var path = '/sites/' + this._sid + '/follows/mine';
  return this.wpcom.req.get(path, query, fn);
};

/**
 * Follow the site
 *
 * @param {Object} [query]
 * @param {Function} fn
 * @api public
 */

Follow.prototype.follow =
Follow.prototype.add = function (query, fn) {
  var path = '/sites/' + this._sid + '/follows/new';
  return this.wpcom.req.put(path, query, null, fn);
};

/**
 * Unfollow the site
 *
 * @param {Object} [query]
 * @param {Function} fn
 * @api public
 */

Follow.prototype.unfollow =
Follow.prototype.del = function (query, fn) {
  var path = '/sites/' + this._sid + '/follows/mine/delete';
  return this.wpcom.req.del(path, query, null, fn);
};

/**
 * Expose `Follow` module
 */

module.exports = Follow;<|endoftext|>


<commit_before>'use strict'

const Wrap = require('word-wrap')
const HTML2Text = require('html-to-text')
const SanitizeHTML = require('sanitize-html')

// Default wrap limit;
// Prefer the actual tty's columns if available,
// unless greater than MAX_COLUMNS
//
const MAX_COLUMNS = 80
const TTY_COLUMNS = process.stdout.columns || Infinity
const DEFAULT_WRAPLIMIT = Math.min(TTY_COLUMNS, MAX_COLUMNS)

// Indent string for wrapping
//
const INDENT_STRING = '  '

// Horizontal rule char
//
const HR_CHAR = '-'

// (Internal) cache for hr memoization.
// See hr() below
//
const hrMemoTable = new Map()

// Return nicely formatted plain text for
// the given html.
//
// @param {string} html
// The html to format as plain text
//
// n.b. This adds a wrapping and sanitization
//      step to the html because the html
//      returned from algolia/HN is wonky
//
const plaintext = function (html) {
  let wrapped = `<p>${html}</p>`
  let sanitized = SanitizeHTML(wrapped)

  return HTML2Text.fromString(sanitized, {
    wordwrap: false,
    hideLinkHrefIfSameAsText: true
  })
}

module.exports = {

  // Returns the args passed to the process
  // as a single string, (excluding the initial
  // process info bits of argv)
  //
  // @return {string} arguments
  //
  args () {
    let result = process.argv.slice(2)
    return result.join(' ')
  },

  // Retuns an ascii horizontal rule at the given width.
  // Uses the HR_CHAR const to form the string.
  //
  // @param {int} width
  // The width at which to make the rule
  //
  hr (width) {
    if (hrMemoTable.has(width)) {
      return hrMemoTable.get(width)
    }

    const result = HR_CHAR.repeat(width)
    hrMemoTable.set(width, result)

    return result
  },

  // Formats an html blob as plain text.
  //
  // @param {string} html
  // A string with some amount of html in it
  //
  // @param {number} wraplimit
  // (optional) Char limit for wrapping
  //
  // @return {string}
  // A string with the html formatting removed
  //
  html (input, wraplimit) {
    wraplimit = wraplimit || DEFAULT_WRAPLIMIT

    let opts = {
      width: wraplimit - (INDENT_STRING.length * 2),
      indent: INDENT_STRING
    }

    let divider = this.hr(wraplimit)
    let output = Wrap(plaintext(input), opts)

    return `\n\n${divider}\n\n${output}`
  }
}
<commit_msg>Refactor format module. Hide hr(), remove this<commit_after>'use strict'

const Wrap = require('word-wrap')
const HTML2Text = require('html-to-text')
const SanitizeHTML = require('sanitize-html')

// Default wrap limit;
// Prefer the actual tty's columns if available,
// unless greater than MAX_COLUMNS
//
const MAX_COLUMNS = 80
const TTY_COLUMNS = process.stdout.columns || Infinity
const DEFAULT_WRAPLIMIT = Math.min(TTY_COLUMNS, MAX_COLUMNS)

// Indent string for wrapping
//
const INDENT_STRING = '  '

// Horizontal rule char
//
const HR_CHAR = '-'

// (Internal) cache for hr memoization.
// See hr() below
//
const hrMemoTable = new Map()

// Retuns an ascii horizontal rule at the given width.
// Uses the HR_CHAR const to form the string.
//
// @param {int} width
// The width at which to make the rule
//
const hr = function (width) {
  if (hrMemoTable.has(width)) {
    return hrMemoTable.get(width)
  }

  const result = HR_CHAR.repeat(width)
  hrMemoTable.set(width, result)

  return result
}

// Return nicely formatted plain text for
// the given html.
//
// @param {string} html
// The html to format as plain text
//
// n.b. This adds a wrapping and sanitization
//      step to the html because the html
//      returned from algolia/HN is wonky
//
const plaintext = function (html) {
  let wrapped = `<p>${html}</p>`
  let sanitized = SanitizeHTML(wrapped)

  return HTML2Text.fromString(sanitized, {
    wordwrap: false,
    hideLinkHrefIfSameAsText: true
  })
}

module.exports = {

  // Returns the args passed to the process
  // as a single string, (excluding the initial
  // process info bits of argv)
  //
  // @return {string} arguments
  //
  args () {
    let result = process.argv.slice(2)
    return result.join(' ')
  },

  // Formats an html blob as plain text.
  //
  // @param {string} html
  // A string with some amount of html in it
  //
  // @param {number} wraplimit
  // (optional) Char limit for wrapping
  //
  // @return {string}
  // A string with the html formatting removed
  //
  html (input, wraplimit) {
    wraplimit = wraplimit || DEFAULT_WRAPLIMIT

    let opts = {
      width: wraplimit - (INDENT_STRING.length * 2),
      indent: INDENT_STRING
    }

    let divider = hr(wraplimit)
    let output = Wrap(plaintext(input), opts)

    return `\n\n${divider}\n\n${output}`
  }
}
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2013, Simone Margaritelli <evilsocket at gmail dot com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of Gibson nor the names of its contributors may be used
 *     to endorse or promote products derived from this software without
 *     specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
var util = require('util'),
    net = require('net'),
    url = require('url'),
    Protocol = require('./protocol').Protocol;

var Client = exports.Client = function(connection) {
    var parsed = url.parse( connection || 'unix:///var/run/gibson.sock' );

    this.port      = parsed.port || -1;
    this.host      = parsed.hostname || parsed.path;
    this.buffer    = null;
    this.conn      = null;
    this.callbacks = [];
};

util.inherits(Client, process.EventEmitter);

Client.prototype.append_chunk = function( chunk ){
    if( this.buffer == null ) {
        this.buffer = chunk;
    }
    else {
        // check for concat
        if( Buffer.concat !== undefined ){
            this.buffer = Buffer.concat([this.buffer, chunk]);
        }
        else {
            var tmp = new Buffer( this.buffer.length + chunk.length );

            this.buffer.copy( tmp, 0 );
            chunk.copy( tmp, this.buffer.length );

            this.buffer = tmp;
        }
    }
};

Client.prototype.connect = function () {
	if (!this.conn) {
        // tcp socket
        if( this.port > 0 ){
	        this.conn = new net.createConnection(this.port, this.host);
        }
        // unix socket
        else {
            this.conn = new net.createConnection(this.host);
        }

		var self = this;

	    this.conn.on('connect', function () {
	        // TODO: Make those values customizables
            this.setTimeout(0);
	        this.setNoDelay();
		  	self.emit('connect');
	    });

	    this.conn.on('data', function (chunk) {
            self.append_chunk(chunk);
            self.handle_incoming_data();
	    });

	    this.conn.on('end', function () {
	    	if (self.conn && self.conn.readyState) {
	    		self.conn.end();
	        	self.conn = null;
	      	}
	    });

	    this.conn.on('close', function () {
	    	self.conn = null;
	      	self.emit('close');
	    });

        this.conn.on('timeout', function () {
            self.conn = null;
            self.emit('timeout');
        });

        this.conn.on('error', function (ex) {
            self.conn = null;
            self.emit('error', ex);
        });
    }
};

Client.prototype.decode = function( code, encoding, size, data, offset ){
    if( code === Protocol.replies.REPL_VAL )
    {
        if( encoding === Protocol.encodings.GB_ENC_PLAIN ){
            // TODO: Make the user specify the encoding.
            return data.slice(offset, offset + size).toString();
        }
        else if( encoding === Protocol.encodings.GB_ENC_NUMBER ){
            // 64 bit signed long
            if( size == 8 ){
                /*
                 * NOTE:
                 * Javascript uses internally 64 bit floating numbers, wich means you
                 * can only represent exactly numbers up to 2^53, or 9007199254740992.
                 */
                var word0 = data.readUInt32LE(offset);
                var word1 = data.readUInt32LE(offset + 4);

                if (!(word1 & 0x80000000))
                    return word0 + 0x100000000 * word1;

                else
                    return -((((~word1)>>>0) * 0x100000000) + ((~word0)>>>0) + 1);
            }
            // 32 bit signed integer
            else {
                return data.readInt32LE(offset);
            }
        }
        else
            this.emit('error','Unknown encoding');
    }
    else if( code === Protocol.replies.REPL_KVAL )
    {
        var obj = {}, count = 0, i, klen, key, enc, vsize, v;

        count = data.readUInt32LE(offset);

        for( i = 0, offset = offset + 4; i < count; i++ ){
            // four bytes, unsigned int 32 bit of key length
            klen    = data.readUInt32LE(offset);
            offset += 4;
            // 'key length' bytes of the key
            // TODO: Make the user specify the encoding.
            key     = data.toString( 'utf8', offset, offset + klen );
            offset += klen;
            // one unsigned byte of encoding
            enc     = data.readUInt8(offset);
            offset += 1;
            // four bytes, unsigned int 32 bit of data size
            vsize   = data.readUInt32LE(offset);
            offset += 4;
            // 'vsize' bytes of value
            obj[key] = this.decode( Protocol.replies.REPL_VAL, enc, vsize, data, offset );
            offset += vsize;
        }

        return obj;
    }
    else if( code == Protocol.replies.REPL_OK ){
        return true;
    }
    else
        return data;
};

// if a callback throws an exception, re-throw it on a new stack so the parser can keep going.
// put this try/catch in its own function because V8 doesn't optimize this well yet.
function safe_callback(callback, error, data) {
    try {
        callback(error, data);
    } catch (err) {
        process.nextTick(function () {
            throw err;
        });
    }
}

Client.prototype.handle_incoming_data = function() {
    var bsize = 0, packet_size = 0, left = 0,
        packet, code, encoding, datalen, data,
        callback, err;

    // keep parsing while we have at least a header to read
    while ( ( bsize = this.buffer.length ) >= Protocol.header_size ){
        code     = this.buffer.readUInt16LE(0);
        encoding = this.buffer.readUInt8(2);
        datalen  = this.buffer.readUInt32LE(3);

        packet_size = Protocol.header_size + datalen;

        // do we have a full response packet ?
        if( bsize >= packet_size ){
            // decode raw data
            data = this.decode( code, encoding, datalen, this.buffer, Protocol.header_size );
            // execute user callback if specified
            callback = this.callbacks.shift();
            if( callback != null ){
                err = null;
                if( Protocol.isErrorCode(code) ){
                    err = new Error( Protocol.errors[code] );
                }

                safe_callback( callback, err, data );
            }

            // go to the next packed if available
            this.buffer = this.buffer.slice( packet_size );
        }
	}
};

Client.prototype.close = function() {
	if( this.conn && this.conn.readyState === 'open' ) {
		this.conn.end();
		this.conn = null;
	}
};

Client.prototype.query = function( opcode, payload, callback ) {
    this.callbacks.push(callback);

    var psize  = Buffer.byteLength( payload );
    var packet = new Buffer( 6 /* 4 query length + 2 opcode */ + psize );

    packet.writeUInt32LE( 2 + psize, 0 );
    packet.writeUInt16LE( opcode,    4 );
    packet.write( payload, 6, psize );

    this.conn.write(packet);
};

function to_array(args) {
    for( var i = 0, len = args.length, arr = new Array(len); i < len; i += 1 ){
        arr[i] = args[i];
    }

    return arr;
}

// auto map every command prototype using the protocol definition
Object.keys( Protocol.commands ).forEach( function(cmd){
    var lwr    = cmd.toLowerCase(),
        opcode = Protocol.commands[cmd];

    Client.prototype[cmd] =
    Client.prototype[lwr] = function(){
        var args = to_array(arguments), cb = undefined;

        if( args.length && typeof(args[ args.length - 1 ]) == 'function' ){
            cb = args.pop();
        }

        return this.query( opcode, args.join(' '), cb );
    };
});
<commit_msg>custom timeout, nodelay flag and encoding<commit_after>/*
 * Copyright (c) 2013, Simone Margaritelli <evilsocket at gmail dot com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of Gibson nor the names of its contributors may be used
 *     to endorse or promote products derived from this software without
 *     specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
var util = require('util'),
    net = require('net'),
    url = require('url'),
    Protocol = require('./protocol').Protocol;

var Client = exports.Client = function(connection, timeout, nodelay, encoding) {
    var parsed = url.parse( connection || 'unix:///var/run/gibson.sock' );

    this.port      = parsed.port || -1;
    this.host      = parsed.hostname || parsed.path;
    this.timeout   = timeout || 0;
    this.nodelay   = nodelay || true;
    this.encoding  = encoding || 'utf8';
    this.buffer    = null;
    this.conn      = null;
    this.callbacks = [];
};

util.inherits(Client, process.EventEmitter);

Client.prototype.append_chunk = function( chunk ){
    if( this.buffer == null ) {
        this.buffer = chunk;
    }
    else {
        // check for concat
        if( Buffer.concat !== undefined ){
            this.buffer = Buffer.concat([this.buffer, chunk]);
        }
        else {
            var tmp = new Buffer( this.buffer.length + chunk.length );

            this.buffer.copy( tmp, 0 );
            chunk.copy( tmp, this.buffer.length );

            this.buffer = tmp;
        }
    }
};

Client.prototype.connect = function () {
	if (!this.conn) {
        // tcp socket
        if( this.port > 0 ){
	        this.conn = new net.createConnection(this.port, this.host);
        }
        // unix socket
        else {
            this.conn = new net.createConnection(this.host);
        }

		var self = this;

	    this.conn.on('connect', function () {
            this.setTimeout( self.timeout );
            if( self.nodelay == true )
	            this.setNoDelay();

            self.emit('connect');
	    });

	    this.conn.on('data', function (chunk) {
            self.append_chunk(chunk);
            self.handle_incoming_data();
	    });

	    this.conn.on('end', function () {
	    	if (self.conn && self.conn.readyState) {
	    		self.conn.end();
	        	self.conn = null;
	      	}
	    });

	    this.conn.on('close', function () {
	    	self.conn = null;
	      	self.emit('close');
	    });

        this.conn.on('timeout', function () {
            self.conn = null;
            self.emit('timeout');
        });

        this.conn.on('error', function (ex) {
            self.conn = null;
            self.emit('error', ex);
        });
    }
};

Client.prototype.decode = function( code, encoding, size, data, offset ){
    if( code === Protocol.replies.REPL_VAL )
    {
        if( encoding === Protocol.encodings.GB_ENC_PLAIN ){
            return data.slice(offset, offset + size).toString( this.encoding );
        }
        else if( encoding === Protocol.encodings.GB_ENC_NUMBER ){
            // 64 bit signed long
            if( size == 8 ){
                /*
                 * NOTE:
                 * Javascript uses internally 64 bit floating numbers, wich means you
                 * can only represent exactly numbers up to 2^53, or 9007199254740992.
                 */
                var word0 = data.readUInt32LE(offset);
                var word1 = data.readUInt32LE(offset + 4);

                if (!(word1 & 0x80000000))
                    return word0 + 0x100000000 * word1;

                else
                    return -((((~word1)>>>0) * 0x100000000) + ((~word0)>>>0) + 1);
            }
            // 32 bit signed integer
            else {
                return data.readInt32LE(offset);
            }
        }
        else
            this.emit('error','Unknown encoding');
    }
    else if( code === Protocol.replies.REPL_KVAL )
    {
        var obj = {}, count = 0, i, klen, key, enc, vsize, v;

        count = data.readUInt32LE(offset);

        for( i = 0, offset = offset + 4; i < count; i++ ){
            // four bytes, unsigned int 32 bit of key length
            klen    = data.readUInt32LE(offset);
            offset += 4;
            // 'key length' bytes of the key
            key     = data.toString( this.encoding, offset, offset + klen );
            offset += klen;
            // one unsigned byte of encoding
            enc     = data.readUInt8(offset);
            offset += 1;
            // four bytes, unsigned int 32 bit of data size
            vsize   = data.readUInt32LE(offset);
            offset += 4;
            // 'vsize' bytes of value
            obj[key] = this.decode( Protocol.replies.REPL_VAL, enc, vsize, data, offset );
            offset += vsize;
        }

        return obj;
    }
    else if( code == Protocol.replies.REPL_OK ){
        return true;
    }
    else
        return data;
};

// if a callback throws an exception, re-throw it on a new stack so the parser can keep going.
// put this try/catch in its own function because V8 doesn't optimize this well yet.
function safe_callback(callback, error, data) {
    try {
        callback(error, data);
    } catch (err) {
        process.nextTick(function () {
            throw err;
        });
    }
}

Client.prototype.handle_incoming_data = function() {
    var bsize = 0, packet_size = 0, left = 0,
        packet, code, encoding, datalen, data,
        callback, err;

    // keep parsing while we have at least a header to read
    while ( ( bsize = this.buffer.length ) >= Protocol.header_size ){
        code     = this.buffer.readUInt16LE(0);
        encoding = this.buffer.readUInt8(2);
        datalen  = this.buffer.readUInt32LE(3);

        packet_size = Protocol.header_size + datalen;

        // do we have a full response packet ?
        if( bsize >= packet_size ){
            // decode raw data
            data = this.decode( code, encoding, datalen, this.buffer, Protocol.header_size );
            // execute user callback if specified
            callback = this.callbacks.shift();
            if( callback != null ){
                err = null;
                if( Protocol.isErrorCode(code) ){
                    err = new Error( Protocol.errors[code] );
                }

                safe_callback( callback, err, data );
            }

            // go to the next packed if available
            this.buffer = this.buffer.slice( packet_size );
        }
	}
};

Client.prototype.close = function() {
	if( this.conn && this.conn.readyState === 'open' ) {
		this.conn.end();
		this.conn = null;
	}
};

Client.prototype.query = function( opcode, payload, callback ) {
    this.callbacks.push(callback);

    var psize  = Buffer.byteLength( payload );
    var packet = new Buffer( 6 /* 4 query length + 2 opcode */ + psize );

    packet.writeUInt32LE( 2 + psize, 0 );
    packet.writeUInt16LE( opcode,    4 );
    packet.write( payload, 6, psize, this.encoding );

    this.conn.write(packet);
};

function to_array(args) {
    for( var i = 0, len = args.length, arr = new Array(len); i < len; i += 1 ){
        arr[i] = args[i];
    }

    return arr;
}

// auto map every command prototype using the protocol definition
Object.keys( Protocol.commands ).forEach( function(cmd){
    var lwr    = cmd.toLowerCase(),
        opcode = Protocol.commands[cmd];

    Client.prototype[cmd] =
    Client.prototype[lwr] = function(){
        var args = to_array(arguments), cb = undefined;

        if( args.length && typeof(args[ args.length - 1 ]) == 'function' ){
            cb = args.pop();
        }

        return this.query( opcode, args.join(' '), cb );
    };
});
<|endoftext|>