<commit_before>/*!
 *  --------------------------------
 *  Spotify Currently Playing v1.0.0
 *  --------------------------------
 *
 *  https://github.com/kjbrum/spotify-currently-playing
 *  Kyle Brumm (http://kylebrumm.com)
 *
 *  Display the song that is currently playing on your Spotify.
 *
 */

;(function(global) {
    var Spotify = function(selector, username, api_key, width, height) {
        return new Spotify.init(selector, username, api_key, width, height);
    }

    Spotify.prototype = {
        /*
         * Display the Spotify player
         */
        displayPlayer: function() {
            if(!this.selector) {
                throw 'Missing selector';
            }

            var self = this;
            // Get the most recent track
            this.queryLastfm(function() {
                // Search Spotify for the track
                self.searchSpotify(function() {
                    // Display the iframe if we found a track URI
                    if(self.spotifyURI != '') {
                        // Check the type of selector that was supplied
                        if(typeof(self.selector) === 'string') {
                            container = document.querySelector(self.selector);
                        }

                        // Build the iframe element
                        var iframe = document.createElement('iframe');
                        iframe.width = self.width;
                        iframe.height = self.height;
                        iframe.src = 'https://embed.spotify.com/?uri='+encodeURIComponent(self.spotifyURI);
                        iframe.frameBorder = 0;
                        iframe.setAttribute('allowtransparency', 'true');
                        container.appendChild(iframe);
                    } else {
                        console.log('No track found');
                    }
                });
            });
        },

        /*
         * Validate the supplied Last.FM username and api key
         */
        validateLastFM: function() {
            // Check for missing username
            if(!this.username) {
                throw 'Missing username';
            }

            // Check for missing api key
            if(!this.api_key) {
                throw 'Missing api_key';
            }

            console.log('Validating Last.FM...');
            console.log('Username: ' + this.username);
            console.log('API Key: ' + this.api_key);

            // TODO
            // 1. Make an API call to see if the username and api_key work
            // 2. Throw an error if something goes wrong
        },

        /*
         * Get the most recently scrobbled track from Last.fm
         */
        queryLastfm: function(callback) {
            var self = this;

            // Set the request URL for Last.fm
            var lastfm_request_url = 'http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user='+this.username+'&api_key='+this.api_key+'&limit=1&format=json';

            // Make a request to the Last.fm API
            var request = new XMLHttpRequest();
            request.open('GET', lastfm_request_url, true);

            // Check for a successful response
            request.onload = function() {
                if(request.status >= 200 && request.status < 400) {
                    // Parse the response
                    var data = JSON.parse(request.responseText);
                    var the_track;

                    // Update our values
                    if(data.recenttracks.track[0]) {
                        the_track = data.recenttracks.track[0];
                    } else {
                        the_track = data.recenttracks.track;
                    }

                    self.lastfmTrack = {
                        title: the_track.name,
                        artist: the_track.artist['#text'],
                        album: the_track.album['#text']
                    };

                    // Run the callback function
                    callback();
                } else {
                    // Error from the server
                    throw 'Some kind of error from the server';
                }
            };

            // Handle any errors
            request.onerror = function() {
                // Connection error
                throw 'Some kind of connection error';
            };

            // Send the request
            request.send();
        },

        /*
         * Search for track information on Spotify
         */
        searchSpotify: function(callback) {
            var self = this;
            var search_query = '';
            var track = self.lastfmTrack;

            for(var key in track) {
                // Skip if the property is from prototype
                if (!track.hasOwnProperty(key)) continue;

                // Build the search query string
                if(track[key]) {
                    search_query += key+':'+track[key]+' ';
                }
            }

            // Set the request URL for Spotify
            var spotify_request_url = 'https://api.spotify.com/v1/search?query='+encodeURIComponent(search_query)+'&offset=0&limit=1&type=track';

            // Make a request to the Spotify API
            var request = new XMLHttpRequest();
            request.open('GET', spotify_request_url, true);

            // Check for a successful response
            request.onload = function() {
                if(request.status >= 200 && request.status < 400) {
                    // Parse the response
                    var data = JSON.parse(request.responseText);

                    // Update our values
                    if(data.tracks.items[0]) {
                        self.spotifyURI = data.tracks.items[0].uri;
                    }

                    // Run the callback function
                    callback();
                } else {
                    // Error from the server
                    throw 'Some kind of error from the server';
                }
            };

            // Handle any errors
            request.onerror = function() {
                // Connection error
                throw 'Some kind of connection error';
            };

            // Send the request
            request.send();
        }
    };

    // Handle initializing our function
    Spotify.init = function(selector, username, api_key, width, height) {
        var self = this;

        self.selector = selector || '';
        self.username = username || '';
        self.api_key = api_key || '';
        self.width = width || '300';
        self.height = height || '400';
        self.lastfmTrack = {
            title: '',
            artist: '',
            album: ''
        };
        self.spotifyURI = '';

        // Validate the Last.fm username and api_key
        self.validateLastFM();

        // Display the Spotify player
        self.displayPlayer();
    }

    Spotify.init.prototype = Spotify.prototype;

    global.Spotify = Spotify;
}(window));
<commit_msg>Update error messages<commit_after>/*!
 *  --------------------------------
 *  Spotify Currently Playing v1.0.0
 *  --------------------------------
 *
 *  https://github.com/kjbrum/spotify-currently-playing
 *  Kyle Brumm (http://kylebrumm.com)
 *
 *  Display the song that is currently playing on your Spotify.
 *
 */

;(function(global) {
    var Spotify = function(selector, username, api_key, width, height) {
        return new Spotify.init(selector, username, api_key, width, height);
    }

    Spotify.prototype = {
        /*
         * Display the Spotify player
         */
        displayPlayer: function() {
            // Check for missing selector
            if(!this.selector) {
                throw 'Missing selector';
            }

            var self = this;
            // Get the most recent track
            this.queryLastfm(function() {
                // Search Spotify for the track
                self.searchSpotify(function() {
                    // Display the iframe if we found a track URI
                    if(self.spotifyURI != '') {
                        // Check the type of selector that was supplied
                        if(typeof(self.selector) === 'string') {
                            container = document.querySelector(self.selector);
                        }

                        // Build the iframe element
                        var iframe = document.createElement('iframe');
                        iframe.width = self.width;
                        iframe.height = self.height;
                        iframe.src = 'https://embed.spotify.com/?uri='+encodeURIComponent(self.spotifyURI);
                        iframe.frameBorder = 0;
                        iframe.setAttribute('allowtransparency', 'true');
                        container.appendChild(iframe);
                    } else {
                        console.log('No track found');
                    }
                });
            });
        },

        /*
         * Validate the supplied Last.FM username and api key
         */
        validateLastFM: function() {
            // Check for missing username
            if(!this.username) {
                throw 'missing username';
            }

            // Check for missing api key
            if(!this.api_key) {
                throw 'missing api_key';
            }

            console.log('Validating Last.FM...');
            console.log('Username: ' + this.username);
            console.log('API Key: ' + this.api_key);

            // TODO
            // 1. Make an API call to see if the username and api_key work
            // 2. Throw an error if something goes wrong
        },

        /*
         * Get the most recently scrobbled track from Last.fm
         */
        queryLastfm: function(callback) {
            var self = this;

            // Set the request URL for Last.fm
            var lastfm_request_url = 'http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user='+this.username+'&api_key='+this.api_key+'&limit=1&format=json';

            // Make a request to the Last.fm API
            var request = new XMLHttpRequest();
            request.open('GET', lastfm_request_url, true);

            // Check for a successful response
            request.onload = function() {
                if(request.status >= 200 && request.status < 400) {
                    // Parse the response
                    var data = JSON.parse(request.responseText);
                    var the_track;

                    // Update our values
                    if(data.recenttracks.track[0]) {
                        the_track = data.recenttracks.track[0];
                    } else {
                        the_track = data.recenttracks.track;
                    }

                    self.lastfmTrack = {
                        title: the_track.name,
                        artist: the_track.artist['#text'],
                        album: the_track.album['#text']
                    };

                    // Run the callback function
                    callback();
                } else {
                    // Error from the server
                    throw 'error from the server';
                }
            };

            // Handle any errors
            request.onerror = function() {
                // Connection error
                throw 'connection error';
            };

            // Send the request
            request.send();
        },

        /*
         * Search for track information on Spotify
         */
        searchSpotify: function(callback) {
            var self = this;
            var search_query = '';
            var track = self.lastfmTrack;

            for(var key in track) {
                // Skip if the property is from prototype
                if (!track.hasOwnProperty(key)) continue;

                // Build the search query string
                if(track[key]) {
                    search_query += key+':'+track[key]+' ';
                }
            }

            // Set the request URL for Spotify
            var spotify_request_url = 'https://api.spotify.com/v1/search?query='+encodeURIComponent(search_query)+'&offset=0&limit=1&type=track';

            // Make a request to the Spotify API
            var request = new XMLHttpRequest();
            request.open('GET', spotify_request_url, true);

            // Check for a successful response
            request.onload = function() {
                if(request.status >= 200 && request.status < 400) {
                    // Parse the response
                    var data = JSON.parse(request.responseText);

                    // Update our values
                    if(data.tracks.items[0]) {
                        self.spotifyURI = data.tracks.items[0].uri;
                    }

                    // Run the callback function
                    callback();
                } else {
                    // Error from the server
                    throw 'error from the server';
                }
            };

            // Handle any errors
            request.onerror = function() {
                // Connection error
                throw 'connection error';
            };

            // Send the request
            request.send();
        }
    };

    // Handle initializing our function
    Spotify.init = function(selector, username, api_key, width, height) {
        var self = this;

        self.selector = selector || '';
        self.username = username || '';
        self.api_key = api_key || '';
        self.width = width || '300';
        self.height = height || '400';
        self.lastfmTrack = {
            title: '',
            artist: '',
            album: ''
        };
        self.spotifyURI = '';

        // Validate the Last.fm username and api_key
        self.validateLastFM();

        // Display the Spotify player
        self.displayPlayer();
    }

    Spotify.init.prototype = Spotify.prototype;

    global.Spotify = Spotify;
}(window));
<|endoftext|>


<commit_before>import { Color, pt, rect } from "lively.graphics";
import { string, obj } from "lively.lang";

const defaultProperties = {
  name: "a morph",
  position:  pt(0,0),
  rotation:  0,
  scale:  1,
  extent: pt(10, 10),
  fill: Color.white,
  clipMode: "visible",
  reactsToPointer: true,
  submorphs:  []
}

export class Morph {

  constructor(props, submorphs) {
    this._nodeType = 'div';
    this._owner = null;
    this._changes = []
    this._pendingChanges = [];
    this._dirty = true; // for initial display
    this._id = string.newUUID();
    Object.assign(this, props);
  }

  get isMorph() { return true; }
  get id() { return this._id; }

  defaultProperty(key) { return defaultProperties[key]; }

  getProperty(key) {
     var c = this.lastChangeFor(key);
     return c ? c.value : this.defaultProperty(key);
  }

  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  // debugging
  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  toString() {
    return `<${this.constructor.name} - ${this.name ? this.name : this.id}>`;
  }

  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  // changes
  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  lastChangeFor(prop, onlyCommited) {
    var changes = this._changes.concat(onlyCommited ? [] : this._pendingChanges);
    for (var i = changes.length-1; i >= 0; i--)
      if (changes[i].prop === prop) return changes[i];
    return null
  }

  change(change) {
    this._pendingChanges.push(change);
    this.makeDirty();
    return change;
  }

  hasPendingChanges() { return !!this._pendingChanges.length; }

  commitChanges() {
    this._changes = this._changes.concat(this._pendingChanges);
    this._pendingChanges = [];
  }

  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  // render hooks
  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  makeDirty() {
    this._dirty = true;
    if (this.owner) this.owner.makeDirty();
  }

  needsRerender() {
    return this._dirty || !!this._pendingChanges.length;
  }

  aboutToRender() {
    this.commitChanges();
    this._dirty = false;
  }

  shape() {
    return {}
  }

  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  // morphic interface
  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  get name()           { return this.getProperty("name"); }
  set name(value)      { this.change({prop: "name", value}); }

  get position()       { return this.getProperty("position"); }
  set position(value)  { this.change({prop: "position", value}); }

  get scale()          { return this.getProperty("scale"); }
  set scale(value)     { this.change({prop: "scale", value}); }

  get rotation()       { return this.getProperty("rotation"); }
  set rotation(value)  { this.change({prop: "rotation", value}); }

  get extent()         { return this.getProperty("extent"); }
  set extent(value)    { this.change({prop: "extent", value}); }

  get fill()           { return this.getProperty("fill"); }
  set fill(value)      { this.change({prop: "fill", value}); }

  get clipMode()       { return this.getProperty("clipMode"); }
  set clipMode(value)  { this.change({prop: "clipMode", value}); }

  get reactsToPointer()       { return this.getProperty("reactsToPointer"); }
  set reactsToPointer(value)  { this.change({prop: "reactsToPointer", value}); }

  get bounds() {
    var {x,y} = this.position, {x:w,y:h} = this.extent;
    return rect(x,y,w,h);
  }

  set bounds(bounds) {
    this.position = bounds.topLeft();
    this.extent = bounds.extent();
  }

  innerBounds() {
    var {x:w, y:h} = this.extent;
    return rect(0,0,w,h);
  }

  align(p1, p2) { return this.moveBy(p2.subPt(p1)); }
  moveBy(delta) { this.position = this.position.addPt(delta); }
  resizeBy(delta) { this.extent = this.extent.addPt(delta); }

  get width()         { return this.extent.x; }
  set width(v)        { return this.extent = pt(v, this.extent.y); }
  get height()        { return this.extent.y; }
  set height(v)       { return this.extent = pt(this.extent.x, v); }

  get left()          { return this.bounds.left(); }
  set left(v)         { return this.moveBy(pt(v - this.left), 0); }
  get right()         { return this.bounds.right(); }
  set right(v)        { return this.moveBy(pt(v - this.right), 0); }
  get top()           { return this.bounds.top(); }
  set top(v)          { return this.moveBy(pt(0, v - this.top)); }
  get bottom()        { return this.bounds.bottom(); }
  set bottom(v)       { return this.moveBy(pt(0, v - this.bottom)); }

  get center()        { return this.bounds.center(); }
  set center(v)       { return this.align(this.center, v); }
  get topLeft()       { return this.bounds.topLeft(); }
  set topLeft(v)      { return this.align(this.topLeft, v); }
  get topRight()      { return this.bounds.topRight(); }
  set topRight(v)     { return this.align(this.topRight, v); }
  get bottomRight()   { return this.bounds.bottomRight(); }
  set bottomRight(v)  { return this.align(this.bottomRight, v); }
  get bottomLeft()    { return this.bounds.bottomLeft(); }
  set bottomLeft(v)   { return this.align(this.bottomLeft, v); }
  get bottomCenter()  { return this.bounds.bottomCenter(); }
  set bottomCenter(v) { return this.align(this.bottomCenter, v); }
  get topCenter()     { return this.bounds.topCenter(); }
  set topCenter(v)    { return this.align(this.topCenter, v); }
  get leftCenter()    { return this.bounds.leftCenter(); }
  set leftCenter(v)   { return this.align(this.leftCenter, v); }
  get rightCenter()   { return this.bounds.rightCenter(); }
  set rightCenter(v)  { return this.align(this.rightCenter, v); }

  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  // morphic relationship
  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  get submorphs() { return this.getProperty("submorphs").slice(); }
  set submorphs(newSubmorphs) {
    this.submorphs.forEach(m => m.remove());
    return newSubmorphs.map(m => this.addMorph(m));
  }

  addMorph(morph, insertBeforeMorph) {
    if (!morph || typeof morph !== "object")
      throw new Error(`${morph} cannot be added as a submorph to ${this}`)

    if (!morph.isMorph) morph = new Morph(morph);

    morph._owner = this;
    var submorphs = this.submorphs,
        insertBeforeMorphIndex = insertBeforeMorph ? submorphs.indexOf(insertBeforeMorph) : -1,
        insertionIndex = insertBeforeMorphIndex === -1 ? submorphs.length : insertBeforeMorphIndex;
    submorphs.splice(insertionIndex, 0, morph);

    this.change({prop: "submorphs", value: submorphs});
    return morph;
  }

  remove() {
    var o = this.owner;
    if (o) {
      this._owner = null;
      var submorphs = o.submorphs,
          index = submorphs.indexOf(this)
      if (index > -1) submorphs.splice(index, 1);
      o.change({prop: "submorphs", value: submorphs});
    }
  }

  get owner() { return this._owner; }

  withAllSubmorphsDetect(testerFunc) {
    if (testerFunc(this)) return this;
    for (let m of this.submorphs) {
      var found = m.withAllSubmorphsDetect(testerFunc);
      if (found) return found;
    }
    return undefined;
  }

  withAllSubmorphsDo(func) {
    func(this)
    for (let m of this.submorphs)
      m.withAllSubmorphsDo(func);
  }

  withAllSubmorphsSelect(testerFunc) {
    var result = [];
    this.withAllSubmorphsDo(m =>
      testerFunc(m) && result.push(m));
    return result;
  }

  ownerChain() {
    return this.owner ? [this.owner].concat(this.owner.ownerChain()) : [];
  }

  world() {
    return this.owner ? this.owner.world() : null;
  }

  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  // undo / redo
  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  undo() {
    // fixme redo stack
    this._changes.pop();
    this.makeDirty();
  }

  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  // nameing
  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  get(name) {
    // search below, search siblings, search upwards
    if (!name) return null;
    try {
      return (this.getNameTest(this, name) && this)
          || this.getInSubmorphs(name)
          || this.getInOwners(name);
    } catch(e) {
      if (e.constructor == RangeError && e.message == "Maximum call stack size exceeded") {
        e = new Error("'get' failed due to a stack overflow. The most\n"
          + "likely source of the problem is using 'get' as part of\n"
          + "toString, because 'get' calls 'getInOwners', which\n"
          + "calls 'toString' on this. Try using 'getInSubmorphs' instead,\n"
          + "which only searches in this' children.");
      }
      throw e
    }
  }

  getNameTest(morph, expectedName) {
    var isRe = obj.isRegExp(expectedName);
    if (isRe) {
      if (expectedName.test(morph.name) || expectedName.test(String(morph))) return true;
    } else {
      if (morph.name === expectedName || String(morph) === expectedName) return true;
    }
    return false;
  }

  getInSubmorphs(name) {
    if (!this.submorphs.length) return null;
    var isRe = obj.isRegExp(name);
    for (var i = 0; i < this.submorphs.length; i++) {
      var morph = this.submorphs[i];
      if (this.getNameTest(morph, name)) return morph
    }
    for (var i = 0; i < this.submorphs.length; i++)  {
      var morph = this.submorphs[i].getInSubmorphs(name);
      if (morph) return morph;
    }
    return null;
  }

  getInOwners(name) {
    var owner = this.owner;
    if (!owner) return null;
    for (var i = 0; i < owner.submorphs.length; i++) {
      var morph = owner.submorphs[i];
      if (morph === this) continue;
      if (this.getNameTest(morph, name)) return morph;
      var foundInMorph = morph.getInSubmorphs(name);
      if (foundInMorph) return foundInMorph;
    }
    return this.owner.getInOwners(name);
  }

  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  // events
  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  onMouseDown(evt) { console.log("clicked on " + this) }
  onMouseUp(evt) {}
  onMouseMove(evt) {}
  onDragStart(evt) {}
  onDrag(evt) {}
  onDragEnd(evt) {}

}

export class WorldMorph extends Morph {

  get isWorld() { return true }

  handForPointerId(pointerId) {
    return this.submorphs.find(m => m instanceof HandMorph && m.pointerId === pointerId)
        || this.addMorph(new HandMorph(pointerId), this.submorphs[0]);
  }

  world() { return this }

  onMouseMove(evt) {
    evt.hand.update(evt);
  }

  onMouseDown(evt) {
  }

  onMouseUp(evt) {
  }
}

export class HandMorph extends Morph {

  constructor(pointerId) {
    super();
    this.pointerId = pointerId;
    this.fill = Color.orange;
    this.extent = pt(4,4);
    this.reactsToPointer = false;
  }

  get isHand() { return true }

  update(evt) {
    this.position = evt.position;
  }

}

export class Ellipse extends Morph {

  shape() {
    return {
      style: {
        borderRadius: this.extent.x + "px/" + this.extent.y + "px"
      }
    }
  }

}

export class Image extends Morph {

  constructor(props, submorphs) {
    super(props, submorphs);
    this._nodeType = 'img';
    if (!this.imageUrl)
      this.imageUrl = 'http://lively-web.org/core/media/lively-web-logo-small.png'
  }

  get imageUrl()       { return this.getProperty("imageUrl"); }
  set imageUrl(value)  { this.change({prop: "imageUrl", value}); }

  shape() {
    return {
      src: this.imageUrl
    }
  }
}
<commit_msg>drag/grab prop<commit_after>import { Color, pt, rect } from "lively.graphics";
import { string, obj } from "lively.lang";

const defaultProperties = {
  name: "a morph",
  position:  pt(0,0),
  rotation:  0,
  scale:  1,
  extent: pt(10, 10),
  fill: Color.white,
  clipMode: "visible",
  reactsToPointer: true,
  draggable: true,
  grabbable: true,
  submorphs:  []
}

export class Morph {

  constructor(props, submorphs) {
    this._nodeType = 'div';
    this._owner = null;
    this._changes = []
    this._pendingChanges = [];
    this._dirty = true; // for initial display
    this._id = string.newUUID();
    Object.assign(this, props);
  }

  get isMorph() { return true; }
  get id() { return this._id; }

  defaultProperty(key) { return defaultProperties[key]; }

  getProperty(key) {
     var c = this.lastChangeFor(key);
     return c ? c.value : this.defaultProperty(key);
  }

  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  // debugging
  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  toString() {
    return `<${this.constructor.name} - ${this.name ? this.name : this.id}>`;
  }

  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  // changes
  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  lastChangeFor(prop, onlyCommited) {
    var changes = this._changes.concat(onlyCommited ? [] : this._pendingChanges);
    for (var i = changes.length-1; i >= 0; i--)
      if (changes[i].prop === prop) return changes[i];
    return null
  }

  change(change) {
    this._pendingChanges.push(change);
    this.makeDirty();
    return change;
  }

  hasPendingChanges() { return !!this._pendingChanges.length; }

  commitChanges() {
    this._changes = this._changes.concat(this._pendingChanges);
    this._pendingChanges = [];
  }

  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  // render hooks
  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  makeDirty() {
    this._dirty = true;
    if (this.owner) this.owner.makeDirty();
  }

  needsRerender() {
    return this._dirty || !!this._pendingChanges.length;
  }

  aboutToRender() {
    this.commitChanges();
    this._dirty = false;
  }

  shape() {
    return {}
  }

  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  // morphic interface
  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  get name()           { return this.getProperty("name"); }
  set name(value)      { this.change({prop: "name", value}); }

  get position()       { return this.getProperty("position"); }
  set position(value)  { this.change({prop: "position", value}); }

  get scale()          { return this.getProperty("scale"); }
  set scale(value)     { this.change({prop: "scale", value}); }

  get rotation()       { return this.getProperty("rotation"); }
  set rotation(value)  { this.change({prop: "rotation", value}); }

  get extent()         { return this.getProperty("extent"); }
  set extent(value)    { this.change({prop: "extent", value}); }

  get fill()           { return this.getProperty("fill"); }
  set fill(value)      { this.change({prop: "fill", value}); }

  get clipMode()       { return this.getProperty("clipMode"); }
  set clipMode(value)  { this.change({prop: "clipMode", value}); }

  get draggable()       { return this.getProperty("draggable"); }
  set draggable(value)  { this.change({prop: "draggable", value}); }

  get grabbable()       { return this.getProperty("grabbable"); }
  set grabbable(value)  { this.change({prop: "grabbable", value}); }

  get reactsToPointer()       { return this.getProperty("reactsToPointer"); }
  set reactsToPointer(value)  { this.change({prop: "reactsToPointer", value}); }

  get bounds() {
    var {x,y} = this.position, {x:w,y:h} = this.extent;
    return rect(x,y,w,h);
  }

  set bounds(bounds) {
    this.position = bounds.topLeft();
    this.extent = bounds.extent();
  }

  innerBounds() {
    var {x:w, y:h} = this.extent;
    return rect(0,0,w,h);
  }

  align(p1, p2) { return this.moveBy(p2.subPt(p1)); }
  moveBy(delta) { this.position = this.position.addPt(delta); }
  resizeBy(delta) { this.extent = this.extent.addPt(delta); }

  get width()         { return this.extent.x; }
  set width(v)        { return this.extent = pt(v, this.extent.y); }
  get height()        { return this.extent.y; }
  set height(v)       { return this.extent = pt(this.extent.x, v); }

  get left()          { return this.bounds.left(); }
  set left(v)         { return this.moveBy(pt(v - this.left), 0); }
  get right()         { return this.bounds.right(); }
  set right(v)        { return this.moveBy(pt(v - this.right), 0); }
  get top()           { return this.bounds.top(); }
  set top(v)          { return this.moveBy(pt(0, v - this.top)); }
  get bottom()        { return this.bounds.bottom(); }
  set bottom(v)       { return this.moveBy(pt(0, v - this.bottom)); }

  get center()        { return this.bounds.center(); }
  set center(v)       { return this.align(this.center, v); }
  get topLeft()       { return this.bounds.topLeft(); }
  set topLeft(v)      { return this.align(this.topLeft, v); }
  get topRight()      { return this.bounds.topRight(); }
  set topRight(v)     { return this.align(this.topRight, v); }
  get bottomRight()   { return this.bounds.bottomRight(); }
  set bottomRight(v)  { return this.align(this.bottomRight, v); }
  get bottomLeft()    { return this.bounds.bottomLeft(); }
  set bottomLeft(v)   { return this.align(this.bottomLeft, v); }
  get bottomCenter()  { return this.bounds.bottomCenter(); }
  set bottomCenter(v) { return this.align(this.bottomCenter, v); }
  get topCenter()     { return this.bounds.topCenter(); }
  set topCenter(v)    { return this.align(this.topCenter, v); }
  get leftCenter()    { return this.bounds.leftCenter(); }
  set leftCenter(v)   { return this.align(this.leftCenter, v); }
  get rightCenter()   { return this.bounds.rightCenter(); }
  set rightCenter(v)  { return this.align(this.rightCenter, v); }

  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  // morphic relationship
  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  get submorphs() { return this.getProperty("submorphs").slice(); }
  set submorphs(newSubmorphs) {
    this.submorphs.forEach(m => m.remove());
    return newSubmorphs.map(m => this.addMorph(m));
  }

  addMorph(morph, insertBeforeMorph) {
    if (!morph || typeof morph !== "object")
      throw new Error(`${morph} cannot be added as a submorph to ${this}`)

    if (!morph.isMorph) morph = new Morph(morph);

    morph._owner = this;
    var submorphs = this.submorphs,
        insertBeforeMorphIndex = insertBeforeMorph ? submorphs.indexOf(insertBeforeMorph) : -1,
        insertionIndex = insertBeforeMorphIndex === -1 ? submorphs.length : insertBeforeMorphIndex;
    submorphs.splice(insertionIndex, 0, morph);

    this.change({prop: "submorphs", value: submorphs});
    return morph;
  }

  remove() {
    var o = this.owner;
    if (o) {
      this._owner = null;
      var submorphs = o.submorphs,
          index = submorphs.indexOf(this)
      if (index > -1) submorphs.splice(index, 1);
      o.change({prop: "submorphs", value: submorphs});
    }
  }

  get owner() { return this._owner; }

  withAllSubmorphsDetect(testerFunc) {
    if (testerFunc(this)) return this;
    for (let m of this.submorphs) {
      var found = m.withAllSubmorphsDetect(testerFunc);
      if (found) return found;
    }
    return undefined;
  }

  withAllSubmorphsDo(func) {
    func(this)
    for (let m of this.submorphs)
      m.withAllSubmorphsDo(func);
  }

  withAllSubmorphsSelect(testerFunc) {
    var result = [];
    this.withAllSubmorphsDo(m =>
      testerFunc(m) && result.push(m));
    return result;
  }

  ownerChain() {
    return this.owner ? [this.owner].concat(this.owner.ownerChain()) : [];
  }

  world() {
    return this.owner ? this.owner.world() : null;
  }

  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  // undo / redo
  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  undo() {
    // fixme redo stack
    this._changes.pop();
    this.makeDirty();
  }

  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  // nameing
  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  get(name) {
    // search below, search siblings, search upwards
    if (!name) return null;
    try {
      return (this.getNameTest(this, name) && this)
          || this.getInSubmorphs(name)
          || this.getInOwners(name);
    } catch(e) {
      if (e.constructor == RangeError && e.message == "Maximum call stack size exceeded") {
        e = new Error("'get' failed due to a stack overflow. The most\n"
          + "likely source of the problem is using 'get' as part of\n"
          + "toString, because 'get' calls 'getInOwners', which\n"
          + "calls 'toString' on this. Try using 'getInSubmorphs' instead,\n"
          + "which only searches in this' children.");
      }
      throw e
    }
  }

  getNameTest(morph, expectedName) {
    var isRe = obj.isRegExp(expectedName);
    if (isRe) {
      if (expectedName.test(morph.name) || expectedName.test(String(morph))) return true;
    } else {
      if (morph.name === expectedName || String(morph) === expectedName) return true;
    }
    return false;
  }

  getInSubmorphs(name) {
    if (!this.submorphs.length) return null;
    var isRe = obj.isRegExp(name);
    for (var i = 0; i < this.submorphs.length; i++) {
      var morph = this.submorphs[i];
      if (this.getNameTest(morph, name)) return morph
    }
    for (var i = 0; i < this.submorphs.length; i++)  {
      var morph = this.submorphs[i].getInSubmorphs(name);
      if (morph) return morph;
    }
    return null;
  }

  getInOwners(name) {
    var owner = this.owner;
    if (!owner) return null;
    for (var i = 0; i < owner.submorphs.length; i++) {
      var morph = owner.submorphs[i];
      if (morph === this) continue;
      if (this.getNameTest(morph, name)) return morph;
      var foundInMorph = morph.getInSubmorphs(name);
      if (foundInMorph) return foundInMorph;
    }
    return this.owner.getInOwners(name);
  }

  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
  // events
  // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

  onMouseDown(evt) { console.log("clicked on " + this) }
  onMouseUp(evt) {}
  onMouseMove(evt) {}
  onDragStart(evt) {}
  onDrag(evt) {}
  onDragEnd(evt) {}

}

export class WorldMorph extends Morph {

  get isWorld() { return true }

  handForPointerId(pointerId) {
    return this.submorphs.find(m => m instanceof HandMorph && m.pointerId === pointerId)
        || this.addMorph(new HandMorph(pointerId), this.submorphs[0]);
  }

  world() { return this }

  onMouseMove(evt) {
    evt.hand.update(evt);
  }

  onMouseDown(evt) {
  }

  onMouseUp(evt) {
  }
}

export class HandMorph extends Morph {

  constructor(pointerId) {
    super();
    this.pointerId = pointerId;
    this.fill = Color.orange;
    this.extent = pt(4,4);
    this.reactsToPointer = false;
  }

  get isHand() { return true }

  update(evt) {
    this.position = evt.position;
  }

}

export class Ellipse extends Morph {

  shape() {
    return {
      style: {
        borderRadius: this.extent.x + "px/" + this.extent.y + "px"
      }
    }
  }

}

export class Image extends Morph {

  constructor(props, submorphs) {
    super(props, submorphs);
    this._nodeType = 'img';
    if (!this.imageUrl)
      this.imageUrl = 'http://lively-web.org/core/media/lively-web-logo-small.png'
  }

  get imageUrl()       { return this.getProperty("imageUrl"); }
  set imageUrl(value)  { this.change({prop: "imageUrl", value}); }

  shape() {
    return {
      src: this.imageUrl
    }
  }
}
<|endoftext|>


<commit_before>const _ = require('underscore');
const request = require('request');
const promise = require('promise');
const RateLimiter = require('limiter').RateLimiter;
const xml2js = require('xml2js');
const parser = new xml2js.Parser();
const util = require('util');
const OAuth = require('oauth');

const ValidGRConditionCodes = [ 10, 20, 30, 40, 50, 60 ];

var GoodReadsAPI = module.exports = function (config) {
    this.config = _.extend({
        grApi: 'https://www.goodreads.com',
        apiKeys: [],
    }, config);

    this.maxRequestsPerSecond = this.config.apiKeys.length;
    this.limiter = new RateLimiter(1, 1000);

    for (var i in this.config.apiKeys) {
        this.oauthClients.push(new OAuth.OAuth(
            'http://www.goodreads.com/oauth/request_token',
            'http://www.goodreads.com/oauth/access_token',
            this.config.apiKeys[i].key,
            this.config.apiKeys[i].secret,
            '1.0A',
            null,
            'HMAC-SHA1'
        ));
    }
}

GoodReadsAPI.prototype = {
    config: null,
    limiter: null,
    maxRequestsPerSecond: 0,
    currentApiKey: 0,
    oauthClients: [],

    wrapApiGetRequest: function wrapApiRequest(reqUrl, requestData, handler) {
        if (typeof requestData === 'function' && typeof handler === 'undefined') {
            handler = requestData;
            requestData = null;
        }

        var _this = this;
        this.limiter.removeTokens(1, function (err, remaining) {
            var thisKey = _this.config.apiKeys[_this.currentApiKey];
            _this.currentApiKey = (++_this.currentApiKey) % _this.config.apiKeys.length;

            if (null !== requestData) {
                var params = [];
                for (var i in requestData) {
                    params.push(i + '=' + requestData[i]);
                }

                reqUrl += '?' + params.join('&');
            }

            request(reqUrl.replace('{{grKey}}', thisKey.key), handler);
        });
    },

    wrapAuthenticatedApiGetRequest: function wrapAuthenticatedApiRequest(reqUrl, userOauthInfo, handler) {
        var _this = this;
        this.limiter.removeTokens(1, function (err, remaining) {
            var thisKeyNo = _this.currentApiKey;
            var thisKey = _this.config.apiKeys[thisKeyNo];
            _this.currentApiKey = (++_this.currentApiKey) % _this.config.apiKeys.length;
            _this.oauthClients[thisKeyNo].get(reqUrl, userOauthInfo.key, userOauthInfo.secret, function (e, data, response) {
                handler(e, response, data);
            });
        });
    },

    wrapAuthenticatedApiPostRequest: function wrapAuthenticatedApiRequest(reqUrl, requestData, userOauthInfo, handler) {
        var _this = this;
        this.limiter.removeTokens(1, function (err, remaining) {
            var thisKeyNo = _this.currentApiKey;
            var thisKey = _this.config.apiKeys[thisKeyNo];
            _this.currentApiKey = (++_this.currentApiKey) % _this.config.apiKeys.length;
            _this.oauthClients[thisKeyNo].post(reqUrl, userOauthInfo.key, userOauthInfo.secret, requestData, function (e, data, response) {
                handler(e, response, data);
            });
        });
    },

    rejectWithError: function rejectWithError(reject, error, response, errorObj) {
        if (typeof errorObj === 'undefined') {
            errorObj = {};
        } else if (typeof errorObj !== 'object') {
            errorObj = { arg: errorObj };
        }

        errorObj.error = error;
        errorObj.response = response;

        reject(errorObj);
    },

    /* GoodReads API Functions *****************/
    isbnToBookId: function isbnToBookId(isbn) {
        var _this = this;
        return new promise(function (resolve, reject) {
            _this.wrapApiGetRequest(_this.config.grApi + '/book/isbn_to_id/' + isbn + '?key={{grKey}}', function (error, response, body) {
                if (error) {
                    return _this.rejectWithError(reject, error, response, {
                        message: 'Error processing request',
                    });
                }

                switch (response.statusCode) {
                    case 200:
                        var match = body.match(/^\d+$/);
                        if (match === null) {
                            return _this.rejectWithError(reject, error, response, {
                                message: 'Invalid response from goodreads',
                            });
                        }
                        return resolve(match);

                    case 404:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Unknown isbn',
                        });

                    default:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Invalid response from goodreads',
                        });
                }
            });
        });
    },

    bookShow: function bookShow(bookId) {
        var _this = this;
        return new promise(function (resolve, reject) {
            _this.wrapApiGetRequest(_this.config.grApi + '/book/show/' + bookId + '.xml?key={{grKey}}', function (error, response, body) {
                if (error) {
                    return _this.rejectWithError(reject, error, response, {
                        message: 'Error processing request',
                    });
                }

                switch (response.statusCode) {
                    case 200:
                        parseGRXmlResponse(body, function(err, result) {
                            if (err) {
                                _this.rejectWithError(reject, err, response, {
                                    message: 'Error parsing response',
                                });
                            } else {
                                resolve(result);
                            }
                        });
                        break;

                    case 404:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Unknown book id',
                        });

                    default:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Invalid response from goodreads',
                        });
                }
            });
        });
    },

    bookShowByIsbn: function bookShow(isbn) {
        var _this = this;
        return new promise(function (resolve, reject) {
            _this.wrapApiGetRequest(_this.config.grApi + '/book/isbn/' + isbn + '?key={{grKey}}', function (error, response, body) {
                if (error) {
                    return _this.rejectWithError(reject, error, response, {
                        message: 'Error processing request',
                    });
                }

                switch (response.statusCode) {
                    case 200:
                        parseGRXmlResponse(body, function(err, result) {
                            if (err) {
                                _this.rejectWithError(reject, err, response, {
                                    message: 'Error parsing response',
                                });
                            } else {
                                resolve(result);
                            }
                        });
                        break;

                    case 404:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Unknown book id',
                        });

                    default:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Invalid response from goodreads',
                        });
                }
            });
        });
    },

    getUserNotifications: function getUserNotifications(userOauthInfo) {
        var _this = this;
        return new promise(function (resolve, reject) {
            _this.wrapAuthenticatedApiGetRequest(_this.config.grApi + '/notifications.xml', userOauthInfo, function (error, response, body) {
                if (error) {
                    return _this.rejectWithError(reject, error, response, {
                        message: 'Error processing request',
                    });
                }

                switch (response.statusCode) {
                    case 200:
                        parseGRXmlResponse(body, function(err, result) {
                            if (err) {
                                _this.rejectWithError(reject, err, response, {
                                    message: 'Error parsing response',
                                });
                            } else {
                                resolve(result);
                            }
                        });
                        break;

                    case 404:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Unknown book id',
                        });

                    default:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Invalid response from goodreads',
                        });
                }
            });
        });
    },

    getUserShelves: function getUserShelves(userId, page) {
        if (typeof page === 'undefined') {
            page = 1;
        }

        var _this = this,
            request = {
                page: page,
                key: '{{grKey}}',
                user_id: userId,
            };
        return new promise(function (resolve, reject) {
            _this.wrapApiGetRequest(_this.config.grApi + '/shelf/list.xml', request, function (error, response, body) {
                if (error) {
                    return _this.rejectWithError(reject, error, response, {
                        message: 'Error processing request',
                    });
                }

                switch (response.statusCode) {
                    case 200:
                        parseGRXmlResponse(body, function(err, result) {
                            console.log('Shelves response: ', result);
                            if (err) {
                                _this.rejectWithError(reject, err, response, {
                                    message: 'Error parsing response',
                                });
                            } else if (parseInt(result.shelves.end) < parseInt(result.shelves.total)) {
                                console.log('More shelves...');
                                _this.getUserShelves(userId, page + 1)
                                    .then(mergeShelves(result))
                                    .then(function(merged) {
                                        resolve(merged);
                                    });
                            } else {
                                resolve(result);
                            }
                        });
                        break;

                    case 404:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Unknown book id',
                        });

                    default:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Invalid response from goodreads',
                        });
                }
            });
        });
    },

    addUserOwnedBook: function addUserOwnedBook(addBookRequest, userOauthInfo) {
        var _this = this;
        if (typeof addBookRequest !== 'object') {
            addBookRequest = {
                bookId: addBookRequest,
            };
        }

        return new promise(function (resolve, reject) {
            var request = {
                'owned_book[book_id]': addBookRequest.bookId,
            };

            if (typeof addBookRequest.condition === 'number' && ValidGRConditionCodes.indexOf(addBookRequest.condition) !== -1) {
                request['owned_book[condition_code]'] = addBookRequest.condition;
            }

            _this.wrapAuthenticatedApiPostRequest(_this.config.grApi + '/owned_books.xml', request, userOauthInfo, function (error, response, body) {
                if (error) {
                    return _this.rejectWithError(reject, error, response, {
                        message: 'Error processing request',
                    });
                }

                switch (response.statusCode) {
                    case 200:
                    case 201:
                    case 202:
                    case 203:
                    case 204:
                        parseGRXmlResponse(body, false, function(err, result) {
                            if (err) {
                                _this.rejectWithError(reject, err, response, {
                                    message: 'Error parsing response',
                                });
                            } else {
                                resolve(result);
                            }
                        });
                        break;

                    case 404:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Unknown book id',
                        });

                    default:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Invalid response from goodreads',
                        });
                }
            });
        });
    },
};

function parseGRXmlResponse(data, rootElementName, callback) {
    if (typeof rootElementName === 'function') {
        callback = rootElementName;
        rootElementName = 'GoodreadsResponse';
    }

    parser.parseString(data, function (err, result) {
        if (err || (rootElementName !== false && typeof result[rootElementName] !== 'object')) {
            return callback(err ? err : true);
        }

        callback(null, removeXmlArrays(rootElementName !== false ? result[rootElementName] : result));
    });
}

function removeXmlArrays(data) {
    for (var i in data) {
        if (Object.prototype.toString.call( data[i] ) === '[object Array]') {
            if (data[i].length === 1 && typeof data[i][0] !== 'object') {
                data[i] = data[i][0];
            } else {
                data[i] = removeXmlArrays(data[i]);
            }
        } else if (typeof data[i] === 'object') {
            data[i] = removeXmlArrays(data[i]);
        }
    }

    return data;
}

function mergeShelves(baseShelves) {
    return function(shelves) {
        return [ baseShelves, shelves ];
    }
}<commit_msg>Improved XML BS parsing<commit_after>const _ = require('underscore');
const request = require('request');
const promise = require('promise');
const RateLimiter = require('limiter').RateLimiter;
const xml2js = require('xml2js');
const parser = new xml2js.Parser();
const util = require('util');
const OAuth = require('oauth');

const ValidGRConditionCodes = [ 10, 20, 30, 40, 50, 60 ];

var GoodReadsAPI = module.exports = function (config) {
    this.config = _.extend({
        grApi: 'https://www.goodreads.com',
        apiKeys: [],
    }, config);

    this.maxRequestsPerSecond = this.config.apiKeys.length;
    this.limiter = new RateLimiter(1, 1000);

    for (var i in this.config.apiKeys) {
        this.oauthClients.push(new OAuth.OAuth(
            'http://www.goodreads.com/oauth/request_token',
            'http://www.goodreads.com/oauth/access_token',
            this.config.apiKeys[i].key,
            this.config.apiKeys[i].secret,
            '1.0A',
            null,
            'HMAC-SHA1'
        ));
    }
}

GoodReadsAPI.prototype = {
    config: null,
    limiter: null,
    maxRequestsPerSecond: 0,
    currentApiKey: 0,
    oauthClients: [],

    wrapApiGetRequest: function wrapApiRequest(reqUrl, requestData, handler) {
        if (typeof requestData === 'function' && typeof handler === 'undefined') {
            handler = requestData;
            requestData = null;
        }

        var _this = this;
        this.limiter.removeTokens(1, function (err, remaining) {
            var thisKey = _this.config.apiKeys[_this.currentApiKey];
            _this.currentApiKey = (++_this.currentApiKey) % _this.config.apiKeys.length;

            if (null !== requestData) {
                var params = [];
                for (var i in requestData) {
                    params.push(i + '=' + requestData[i]);
                }

                reqUrl += '?' + params.join('&');
            }

            request(reqUrl.replace('{{grKey}}', thisKey.key), handler);
        });
    },

    wrapAuthenticatedApiGetRequest: function wrapAuthenticatedApiRequest(reqUrl, userOauthInfo, handler) {
        var _this = this;
        this.limiter.removeTokens(1, function (err, remaining) {
            var thisKeyNo = _this.currentApiKey;
            var thisKey = _this.config.apiKeys[thisKeyNo];
            _this.currentApiKey = (++_this.currentApiKey) % _this.config.apiKeys.length;
            _this.oauthClients[thisKeyNo].get(reqUrl, userOauthInfo.key, userOauthInfo.secret, function (e, data, response) {
                handler(e, response, data);
            });
        });
    },

    wrapAuthenticatedApiPostRequest: function wrapAuthenticatedApiRequest(reqUrl, requestData, userOauthInfo, handler) {
        var _this = this;
        this.limiter.removeTokens(1, function (err, remaining) {
            var thisKeyNo = _this.currentApiKey;
            var thisKey = _this.config.apiKeys[thisKeyNo];
            _this.currentApiKey = (++_this.currentApiKey) % _this.config.apiKeys.length;
            _this.oauthClients[thisKeyNo].post(reqUrl, userOauthInfo.key, userOauthInfo.secret, requestData, function (e, data, response) {
                handler(e, response, data);
            });
        });
    },

    rejectWithError: function rejectWithError(reject, error, response, errorObj) {
        if (typeof errorObj === 'undefined') {
            errorObj = {};
        } else if (typeof errorObj !== 'object') {
            errorObj = { arg: errorObj };
        }

        errorObj.error = error;
        errorObj.response = response;

        reject(errorObj);
    },

    /* GoodReads API Functions *****************/
    isbnToBookId: function isbnToBookId(isbn) {
        var _this = this;
        return new promise(function (resolve, reject) {
            _this.wrapApiGetRequest(_this.config.grApi + '/book/isbn_to_id/' + isbn + '?key={{grKey}}', function (error, response, body) {
                if (error) {
                    return _this.rejectWithError(reject, error, response, {
                        message: 'Error processing request',
                    });
                }

                switch (response.statusCode) {
                    case 200:
                        var match = body.match(/^\d+$/);
                        if (match === null) {
                            return _this.rejectWithError(reject, error, response, {
                                message: 'Invalid response from goodreads',
                            });
                        }
                        return resolve(match);

                    case 404:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Unknown isbn',
                        });

                    default:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Invalid response from goodreads',
                        });
                }
            });
        });
    },

    bookShow: function bookShow(bookId) {
        var _this = this;
        return new promise(function (resolve, reject) {
            _this.wrapApiGetRequest(_this.config.grApi + '/book/show/' + bookId + '.xml?key={{grKey}}', function (error, response, body) {
                if (error) {
                    return _this.rejectWithError(reject, error, response, {
                        message: 'Error processing request',
                    });
                }

                switch (response.statusCode) {
                    case 200:
                        parseGRXmlResponse(body, function(err, result) {
                            if (err) {
                                _this.rejectWithError(reject, err, response, {
                                    message: 'Error parsing response',
                                });
                            } else {
                                resolve(result);
                            }
                        });
                        break;

                    case 404:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Unknown book id',
                        });

                    default:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Invalid response from goodreads',
                        });
                }
            });
        });
    },

    bookShowByIsbn: function bookShow(isbn) {
        var _this = this;
        return new promise(function (resolve, reject) {
            _this.wrapApiGetRequest(_this.config.grApi + '/book/isbn/' + isbn + '?key={{grKey}}', function (error, response, body) {
                if (error) {
                    return _this.rejectWithError(reject, error, response, {
                        message: 'Error processing request',
                    });
                }

                switch (response.statusCode) {
                    case 200:
                        parseGRXmlResponse(body, function(err, result) {
                            if (err) {
                                _this.rejectWithError(reject, err, response, {
                                    message: 'Error parsing response',
                                });
                            } else {
                                resolve(result);
                            }
                        });
                        break;

                    case 404:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Unknown book id',
                        });

                    default:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Invalid response from goodreads',
                        });
                }
            });
        });
    },

    getUserNotifications: function getUserNotifications(userOauthInfo) {
        var _this = this;
        return new promise(function (resolve, reject) {
            _this.wrapAuthenticatedApiGetRequest(_this.config.grApi + '/notifications.xml', userOauthInfo, function (error, response, body) {
                if (error) {
                    return _this.rejectWithError(reject, error, response, {
                        message: 'Error processing request',
                    });
                }

                switch (response.statusCode) {
                    case 200:
                        parseGRXmlResponse(body, function(err, result) {
                            if (err) {
                                _this.rejectWithError(reject, err, response, {
                                    message: 'Error parsing response',
                                });
                            } else {
                                resolve(result);
                            }
                        });
                        break;

                    case 404:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Unknown book id',
                        });

                    default:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Invalid response from goodreads',
                        });
                }
            });
        });
    },

    getUserShelves: function getUserShelves(userId, page) {
        if (typeof page === 'undefined') {
            page = 1;
        }

        var _this = this,
            request = {
                page: page,
                key: '{{grKey}}',
                user_id: userId,
            };
        return new promise(function (resolve, reject) {
            _this.wrapApiGetRequest(_this.config.grApi + '/shelf/list.xml', request, function (error, response, body) {
                if (error) {
                    return _this.rejectWithError(reject, error, response, {
                        message: 'Error processing request',
                    });
                }

                switch (response.statusCode) {
                    case 200:
                        parseGRXmlResponse(body, function(err, result) {
                            console.log('Shelves response: ', result);
                            if (err) {
                                _this.rejectWithError(reject, err, response, {
                                    message: 'Error parsing response',
                                });
                            } else if (parseInt(result.shelves.end) < parseInt(result.shelves.total)) {
                                console.log('More shelves...');
                                _this.getUserShelves(userId, page + 1)
                                    .then(mergeShelves(result))
                                    .then(function(merged) {
                                        resolve(merged);
                                    });
                            } else {
                                resolve(result);
                            }
                        });
                        break;

                    case 404:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Unknown book id',
                        });

                    default:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Invalid response from goodreads',
                        });
                }
            });
        });
    },

    addUserOwnedBook: function addUserOwnedBook(addBookRequest, userOauthInfo) {
        var _this = this;
        if (typeof addBookRequest !== 'object') {
            addBookRequest = {
                bookId: addBookRequest,
            };
        }

        return new promise(function (resolve, reject) {
            var request = {
                'owned_book[book_id]': addBookRequest.bookId,
            };

            if (typeof addBookRequest.condition === 'number' && ValidGRConditionCodes.indexOf(addBookRequest.condition) !== -1) {
                request['owned_book[condition_code]'] = addBookRequest.condition;
            }

            _this.wrapAuthenticatedApiPostRequest(_this.config.grApi + '/owned_books.xml', request, userOauthInfo, function (error, response, body) {
                if (error) {
                    return _this.rejectWithError(reject, error, response, {
                        message: 'Error processing request',
                    });
                }

                switch (response.statusCode) {
                    case 200:
                    case 201:
                    case 202:
                    case 203:
                    case 204:
                        parseGRXmlResponse(body, false, function(err, result) {
                            if (err) {
                                _this.rejectWithError(reject, err, response, {
                                    message: 'Error parsing response',
                                });
                            } else {
                                resolve(result);
                            }
                        });
                        break;

                    case 404:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Unknown book id',
                        });

                    default:
                        return _this.rejectWithError(reject, error, response, {
                            message: 'Invalid response from goodreads',
                        });
                }
            });
        });
    },
};

function parseGRXmlResponse(data, rootElementName, callback) {
    if (typeof rootElementName === 'function') {
        callback = rootElementName;
        rootElementName = 'GoodreadsResponse';
    }

    parser.parseString(data, function (err, result) {
        if (err || (rootElementName !== false && typeof result[rootElementName] !== 'object')) {
            return callback(err ? err : true);
        }

        callback(null, removeXmlArrays(rootElementName !== false ? result[rootElementName] : result));
    });
}

function removeXmlArrays(data) {
    for (var i in data) {
        if (Object.prototype.toString.call( data[i] ) === '[object Array]') {
            if (data[i].length === 1 && typeof data[i][0] !== 'object') {
                if (typeof data[i][0] === 'object' && typeof data[i][0]['_'] !== 'undefined' && typeof data[i][0]['$'] !== 'undefined') {
                    if (data[i][0]['$'].nil === 'true') {
                        data[i] = null;
                    } else if (data[i][0]['$'].type === 'integer') {
                        data[i] = parseInt(data[i][0]['_']);
                    } else if (data[i][0]['$'].type === 'boolean') {
                        data[i] = data[i][0]['_'] === 'true' ? true : false;
                    }
                } else {
                    data[i] = data[i][0];
                }
            } else {
                data[i] = removeXmlArrays(data[i]);
            }
        } else if (typeof data[i] === 'object') {
            data[i] = removeXmlArrays(data[i]);
        }
    }

    return data;
}

function mergeShelves(baseShelves) {
    return function(shelves) {
        return [ baseShelves, shelves ];
    }
}<|endoftext|>


<commit_before>import React from 'react';
import expect, {
  createSpy,
  spyOn,
} from 'expect';
import {
  mount,
  render,
  shallow,
} from 'enzyme';
import Cloud from '../../src/components/Cloud';

/**
 * Fake topics.json
 * @type {Array}
 */
const topicsJson = [
  {
    id: '1751295897__Berlin',
    label: 'Berlin',
    volume: 165,
    type: 'topic',
    sentiment: {
      negative: 3,
      neutral: 133,
      positive: 29,
    },
    sentimentScore: 65,
    burst: 13,
  },
  {
    id: '1751295897__DJ',
    label: 'DJ',
    volume: 48,
    type: 'topic',
    sentiment: {
      neutral: 46,
      positive: 2,
    },
    sentimentScore: 54,
    burst: 29,
  },
];

/**
 * Fake output of d3-cloud. Generated inside Cloud after componentDidMount
 * @type {Array}
 */
const stateDimensions = [
  {
    id: '1751295897__Berlin',
    label: 'Berlin',
    volume: 165,
    type: 'topic',
    sentiment: {
      negative: 3,
      neutral: 133,
      positive: 29,
    },
    sentimentScore: 65,
    burst: 13,
    fontSize: 49,
    text: 'Berlin',
    font: 'Helvetica Neue',
    style: 'normal',
    weight: 'normal',
    rotate: 0,
    size: 49,
    padding: 10,
    x: 0,
    y: 0,
    width: 192,
    height: 98,
    xoff: 0,
    yoff: 0,
    x1: 96,
    y1: 48,
    x0: -96,
    y0: -46,
    hasText: true,
  },
];

function getComponentWithDefaultProps(
  onSelectTopic = () => {},
  fontName = 'Impact',
  fontSizes = [12, 16, 22, 31, 44, 64],
  height = 500,
  topics = topicsJson,
  width = 500
) {
  return (
    <Cloud
      fontName={fontName}
      fontSizes={fontSizes}
      height={height}
      onSelectTopic={onSelectTopic}
      topics={topics}
      width={width}
    />
  );
}

describe('<Cloud />', () => {
  it('renders', () => {
    const wrapper = shallow(getComponentWithDefaultProps());
    expect(wrapper.is('.wordcloud__container_cloud')).toEqual(true);
  });

  it('renders initial loading state', () => {
    const wrapper = shallow(getComponentWithDefaultProps());
    expect(wrapper.contains(Loading...)).toEqual(true);
  });

  it('handles click on topic', () => {
    const onTopicClick = createSpy();
    const wrapper = shallow(getComponentWithDefaultProps(onTopicClick));

    // Simulate result of d3-cloud
    wrapper.setState({
      cloudDimensions: stateDimensions,
      isProcessing: false,
    });
    wrapper.find('.wordcloud__cloud_label').simulate('click');
    expect(onTopicClick.calls.length).toEqual(1);
  });

  it('calls componentDidMount', () => {
    spyOn(Cloud.prototype, 'componentDidMount');
    mount(getComponentWithDefaultProps());
    expect(Cloud.prototype.componentDidMount.calls.length).toEqual(1);
    Cloud.prototype.componentDidMount.restore();
  });
});
<commit_msg>clean imports<commit_after>import React from 'react';
import expect, {
  createSpy,
  spyOn,
} from 'expect';
import {
  mount,
  shallow,
} from 'enzyme';
import Cloud from '../../src/components/Cloud';

/**
 * Fake topics.json
 * @type {Array}
 */
const topicsJson = [
  {
    id: '1751295897__Berlin',
    label: 'Berlin',
    volume: 165,
    type: 'topic',
    sentiment: {
      negative: 3,
      neutral: 133,
      positive: 29,
    },
    sentimentScore: 65,
    burst: 13,
  },
  {
    id: '1751295897__DJ',
    label: 'DJ',
    volume: 48,
    type: 'topic',
    sentiment: {
      neutral: 46,
      positive: 2,
    },
    sentimentScore: 54,
    burst: 29,
  },
];

/**
 * Fake output of d3-cloud. Generated inside Cloud after componentDidMount
 * @type {Array}
 */
const stateDimensions = [
  {
    id: '1751295897__Berlin',
    label: 'Berlin',
    volume: 165,
    type: 'topic',
    sentiment: {
      negative: 3,
      neutral: 133,
      positive: 29,
    },
    sentimentScore: 65,
    burst: 13,
    fontSize: 49,
    text: 'Berlin',
    font: 'Helvetica Neue',
    style: 'normal',
    weight: 'normal',
    rotate: 0,
    size: 49,
    padding: 10,
    x: 0,
    y: 0,
    width: 192,
    height: 98,
    xoff: 0,
    yoff: 0,
    x1: 96,
    y1: 48,
    x0: -96,
    y0: -46,
    hasText: true,
  },
];

function getComponentWithDefaultProps(
  onSelectTopic = () => {},
  fontName = 'Impact',
  fontSizes = [12, 16, 22, 31, 44, 64],
  height = 500,
  topics = topicsJson,
  width = 500
) {
  return (
    <Cloud
      fontName={fontName}
      fontSizes={fontSizes}
      height={height}
      onSelectTopic={onSelectTopic}
      topics={topics}
      width={width}
    />
  );
}

describe('<Cloud />', () => {
  it('renders', () => {
    const wrapper = shallow(getComponentWithDefaultProps());
    expect(wrapper.is('.wordcloud__container_cloud')).toEqual(true);
  });

  it('renders initial loading state', () => {
    const wrapper = shallow(getComponentWithDefaultProps());
    expect(wrapper.contains(Loading...)).toEqual(true);
  });

  it('handles click on topic', () => {
    const onTopicClick = createSpy();
    const wrapper = shallow(getComponentWithDefaultProps(onTopicClick));

    // Simulate result of d3-cloud
    wrapper.setState({
      cloudDimensions: stateDimensions,
      isProcessing: false,
    });
    wrapper.find('.wordcloud__cloud_label').simulate('click');
    expect(onTopicClick.calls.length).toEqual(1);
  });

  it('calls componentDidMount', () => {
    spyOn(Cloud.prototype, 'componentDidMount');
    mount(getComponentWithDefaultProps());
    expect(Cloud.prototype.componentDidMount.calls.length).toEqual(1);
    Cloud.prototype.componentDidMount.restore();
  });
});
<|endoftext|>


<commit_before><commit_msg>Returning rewrite code (uncompiled/untested)<commit_after><|endoftext|>


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

/**
 * IO type for Focus
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Jesse Greenberg (PhET Interactive Simulations)
 */
define( function( require ) {
  'use strict';

  // modules
  var scenery = require( 'SCENERY/scenery' );

  // ifphetio
  var assertInstanceOf = require( 'ifphetio!PHET_IO/assertInstanceOf' );
  var ObjectIO = require( 'ifphetio!PHET_IO/types/ObjectIO' );
  var phetioInherit = require( 'ifphetio!PHET_IO/phetioInherit' );

  /**
   * IO type for phet/sun's Faucet class.
   * @param {Focus} focus - the focus region which has {display,trail}
   * @param {string} phetioID - the unique tandem assigned to the focus
   * @constructor
   */
  function FocusIO( focus, phetioID ) {
    assert && assertInstanceOf( focus, scenery.Focus );
    ObjectIO.call( this, focus, phetioID );
  }

  phetioInherit( ObjectIO, 'FocusIO', FocusIO, {}, {

    /**
     * Convert the focus region to a plain JS object for serialization.
     * @param {Object} focus - the focus region which has {display,trail}
     * @returns {Object} - the serialized object
     */
    toStateObject: function( focus ) {

      // If nothing is focused, the focus is nulls
      if ( focus === null ) {
        return null;
      }
      else {
        assert && assertInstanceOf( focus, scenery.Focus );
        var phetioIDIndices = [];
        focus.trail.nodes.forEach( function( node, i ) {

          // Don't include the last node, since it is the focused node
          if ( i < focus.trail.nodes.length - 1 ) {

            // If the node was PhET-iO instrumented, include its phetioID instead of its index (because phetioID is more stable)
            if ( node.tandem ) {
              phetioIDIndices.push( node.tandem.phetioID );
            }
            else {
              phetioIDIndices.push( focus.trail.indices[ i ] );
            }
          }
        } );

        return {
          focusedPhetioID: focus.trail.lastNode().tandem.phetioID,
          indices: focus.trail.indices,
          phetioIDIndices: phetioIDIndices
        };
      }
    },

    /**
     * Convert the serialized instance back to a focus object
     * @param {Object} stateObject
     * @returns {Object} with {display,trail}
     */
    fromStateObject: function( stateObject ) {

      if ( stateObject === null ) {

        // support unfocused
        return null;
      }
      else {
        var indices = stateObject.indices;

        // Follow the path of children based on their indices, starting from the root of the display.
        // There is always one more node in Trail than indices, representing the root node.
        var currentNode = phet.joist.sim.display.rootNode;
        var nodes = [ currentNode ];
        for ( var i = 0; i < indices.length; i++ ) {
          var index = indices[ i ];
          currentNode = currentNode.children[ index ];
          nodes.push( currentNode );
        }

        return { display: phet.joist.sim.display, trail: new phet.scenery.Trail( nodes ) };
      }
    },

    documentation: 'A IO type for the instance in the simulation which currently has keyboard focus.'
  } );

  scenery.register( 'FocusIO', FocusIO );

  return FocusIO;
} );<commit_msg>Overrides, see https://github.com/phetsims/phet-io/issues/1278<commit_after>// Copyright 2016, University of Colorado Boulder

/**
 * IO type for Focus
 *
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Jesse Greenberg (PhET Interactive Simulations)
 */
define( function( require ) {
  'use strict';

  // modules
  var scenery = require( 'SCENERY/scenery' );

  // ifphetio
  var assertInstanceOf = require( 'ifphetio!PHET_IO/assertInstanceOf' );
  var ObjectIO = require( 'ifphetio!PHET_IO/types/ObjectIO' );
  var phetioInherit = require( 'ifphetio!PHET_IO/phetioInherit' );

  /**
   * IO type for phet/sun's Faucet class.
   * @param {Focus} focus - the focus region which has {display,trail}
   * @param {string} phetioID - the unique tandem assigned to the focus
   * @constructor
   */
  function FocusIO( focus, phetioID ) {
    assert && assertInstanceOf( focus, scenery.Focus );
    ObjectIO.call( this, focus, phetioID );
  }

  phetioInherit( ObjectIO, 'FocusIO', FocusIO, {}, {

    /**
     * Convert the focus region to a plain JS object for serialization.
     * @param {Object} focus - the focus region which has {display,trail}
     * @returns {Object} - the serialized object
     * @override
     */
    toStateObject: function( focus ) {

      // If nothing is focused, the focus is nulls
      if ( focus === null ) {
        return null;
      }
      else {
        assert && assertInstanceOf( focus, scenery.Focus );
        var phetioIDIndices = [];
        focus.trail.nodes.forEach( function( node, i ) {

          // Don't include the last node, since it is the focused node
          if ( i < focus.trail.nodes.length - 1 ) {

            // If the node was PhET-iO instrumented, include its phetioID instead of its index (because phetioID is more stable)
            if ( node.tandem ) {
              phetioIDIndices.push( node.tandem.phetioID );
            }
            else {
              phetioIDIndices.push( focus.trail.indices[ i ] );
            }
          }
        } );

        return {
          focusedPhetioID: focus.trail.lastNode().tandem.phetioID,
          indices: focus.trail.indices,
          phetioIDIndices: phetioIDIndices
        };
      }
    },

    /**
     * Convert the serialized instance back to a focus object
     * @param {Object} stateObject
     * @returns {Object} with {display,trail}
     * @override
     */
    fromStateObject: function( stateObject ) {

      if ( stateObject === null ) {

        // support unfocused
        return null;
      }
      else {
        var indices = stateObject.indices;

        // Follow the path of children based on their indices, starting from the root of the display.
        // There is always one more node in Trail than indices, representing the root node.
        var currentNode = phet.joist.sim.display.rootNode;
        var nodes = [ currentNode ];
        for ( var i = 0; i < indices.length; i++ ) {
          var index = indices[ i ];
          currentNode = currentNode.children[ index ];
          nodes.push( currentNode );
        }

        return { display: phet.joist.sim.display, trail: new phet.scenery.Trail( nodes ) };
      }
    },

    documentation: 'A IO type for the instance in the simulation which currently has keyboard focus.'
  } );

  scenery.register( 'FocusIO', FocusIO );

  return FocusIO;
} );<|endoftext|>


<commit_before>define(["js/core/Base"], function(Base) {

    return Base.inherit('sprd.data.BasketService', {

        addBasketItem: function (element, quantity, callback) {
            callback && callback();
        }

    });

});<commit_msg>removed unused BasketService<commit_after><|endoftext|>


<commit_before>///
/// utility functions for computing progress of complex tasks
///
/// State callback here is an object with these keys:
///   done: bool, true if done
///   current: number, the current progress value
///   end: number, optional, the value of current where we expect to be done
///
/// If end is not set, we'll display a spinner instead of a progress bar
///

var _ = require('underscore');
var Future = require('fibers/future');
var consolejs = require('./console.js');

var Progress = function (options) {
  var self = this;

  options = options || {};

  self._lastState = null;
  self._parent = options.parent;
  self._watchers = options.watchers || [];

  self._title = options.title;

  // XXX: Should we have a strict/mdg mode that enables this test?
  //if (!self._title && self._parent) {
  //  throw new Error("No title passed");
  //}

  self._forkJoin = options.forkJoin;

  self._allTasks = [];

  self._selfState = { current: 0, done: false };
  self._state = _.clone(self._selfState);

  self._isDone = false;

  self.startTime = +(new Date);
};

_.extend(Progress.prototype, {
  toString: function() {
    var self = this;
    return "Progress [state=" + JSON.stringify(self._state) + "]";
  },

  reportProgressDone: function () {
    var self = this;

    var state = _.clone(self._selfState);
    state.done = true;
    if (state.end !== undefined) {
      if (state.current > state.end) {
        state.end = state.current;
      }
      state.current = state.end;
    }
    self.reportProgress(state);
  },

  // Tries to determine which is the 'current' job in the tree
  // This is very heuristical... we use some hints, like:
  // don't descend into fork-join jobs; we know these execute concurrently,
  // so we assume the top-level task has the title
  // i.e. "Downloading packages", not "downloading supercool-1.0"
  getCurrentProgress: function () {
    var self = this;

    var isRoot = !self._parent;

    if (self._isDone) {
      // A done task cannot be the active task
      return null;
    }

    if (!self._state.done && (self._state.current != 0) && !isRoot) {
      // We are not done and we have interesting state to report
      return self;
    }

    if (self._forkJoin) {
      // Don't descend into fork-join tasks (by choice)
      return self;
    }

    if (self._allTasks.length) {
      var candidates = _.map(self._allTasks, function (task) {
        return task.getCurrentProgress();
      });
      var active = _.filter(candidates, function (s) {
        return !!s;
      });
      if (active.length) {
        // pick one to display, somewhat arbitrarily
        return active[active.length - 1];
      }
      // No single active task, return self
      return self;
    }

    return self;
  },

  // Creates a subtask that must be completed as part of this (bigger) task
  addChildTask: function (options) {
    var self = this;
    options = _.extend({ parent: self }, options || {});
    var child = new Progress(options);
    self._allTasks.push(child);
    self._reportChildState(child, child._state);
    return child;
  },

  // Dumps the tree, for debug
  dump: function (stream, options, prefix) {
    var self = this;

    options = options || {};
    if (options.skipDone && self._isDone) {
      return;
    }

    if (prefix) {
      stream.write(prefix);
    }
    var end = self._state.end;
    if (!end) {
      end = '?';
    }
    stream.write("Task [" + self._title + "] " + self._state.current + "/" + end
      + (self._isDone ? " done" : "") +"\n");
    if (self._allTasks.length) {
      _.each(self._allTasks, function (child) {
        child.dump(stream, options, (prefix || '') + '  ');
      });
    }
  },

  // Receives a state report indicating progress of self
  reportProgress: function (state) {
    var self = this;

    self._selfState = state;

    self._updateTotalState();

    // Nudge the spinner/progress bar, but don't yield (might not be safe to yield)
    consolejs.Console.nudge(false);

    self._notifyState();
  },

  // Subscribes a watcher to changes
  addWatcher: function (watcher) {
    var self = this;

    self._watchers.push(watcher);
  },

  // Notifies watchers & parents
  _notifyState: function () {
    var self = this;

    if (self._parent) {
      self._parent._reportChildState(self, self._state);
    }

    if (self._watchers.length) {
      _.each(self._watchers, function (watcher) {
        watcher(self._state);
      });
    }
  },

  // Recomputes state, incorporating children's states
  _updateTotalState: function () {
    var self = this;

    var state = _.clone(self._selfState);

    var allChildrenDone = true;
    var state = _.clone(self._selfState);
    _.each(self._allTasks, function (child) {
      var childState = child._state;

      if (!child._isDone) {
        allChildrenDone = false;
      }

      state.current += childState.current;
      if (state.end !== undefined) {
        if (childState.done) {
          state.end += childState.current;
        } else if (childState.end !== undefined) {
          state.end += childState.end;
        } else {
          state.end = undefined;
        }
      }
    });
    self._isDone = allChildrenDone && !!self._selfState.done;
    if (!allChildrenDone) {
      state.done = false;
    }

    if (!state.done && self._state.done) {
      // This shouldn't happen
      throw new Error("Progress transition from done => !done");
    }

    self._state = state;
  },

  // Called by a child when its state changes
  _reportChildState: function (child, state) {
    var self = this;

    self._updateTotalState();
    self._notifyState();
  },

  getState: function() {
    return this._state;
  }
});

exports.Progress = Progress;
<commit_msg>Fix “Uploading” progress bar on meteor deploy<commit_after>///
/// utility functions for computing progress of complex tasks
///
/// State callback here is an object with these keys:
///   done: bool, true if done
///   current: number, the current progress value
///   end: number, optional, the value of current where we expect to be done
///
/// If end is not set, we'll display a spinner instead of a progress bar
///

var _ = require('underscore');
var Future = require('fibers/future');
var consolejs = require('./console.js');

var Progress = function (options) {
  var self = this;

  options = options || {};

  self._lastState = null;
  self._parent = options.parent;
  self._watchers = options.watchers || [];

  self._title = options.title;

  // XXX: Should we have a strict/mdg mode that enables this test?
  //if (!self._title && self._parent) {
  //  throw new Error("No title passed");
  //}

  self._forkJoin = options.forkJoin;

  self._allTasks = [];

  self._selfState = { current: 0, done: false };
  self._state = _.clone(self._selfState);

  self._isDone = false;

  self.startTime = +(new Date);
};

_.extend(Progress.prototype, {
  toString: function() {
    var self = this;
    return "Progress [state=" + JSON.stringify(self._state) + "]";
  },

  reportProgressDone: function () {
    var self = this;

    var state = _.clone(self._selfState);
    state.done = true;
    if (state.end !== undefined) {
      if (state.current > state.end) {
        state.end = state.current;
      }
      state.current = state.end;
    }
    self.reportProgress(state);
  },

  // Tries to determine which is the 'current' job in the tree
  // This is very heuristical... we use some hints, like:
  // don't descend into fork-join jobs; we know these execute concurrently,
  // so we assume the top-level task has the title
  // i.e. "Downloading packages", not "downloading supercool-1.0"
  getCurrentProgress: function () {
    var self = this;

    var isRoot = !self._parent;

    if (self._isDone) {
      // A done task cannot be the active task
      return null;
    }

    if (!self._state.done && (self._state.current != 0) && self._state.end &&
        !isRoot) {
      // We are not done and we have interesting state to report
      return self;
    }

    if (self._forkJoin) {
      // Don't descend into fork-join tasks (by choice)
      return self;
    }

    if (self._allTasks.length) {
      var candidates = _.map(self._allTasks, function (task) {
        return task.getCurrentProgress();
      });
      var active = _.filter(candidates, function (s) {
        return !!s;
      });
      if (active.length) {
        // pick one to display, somewhat arbitrarily
        return active[active.length - 1];
      }
      // No single active task, return self
      return self;
    }

    return self;
  },

  // Creates a subtask that must be completed as part of this (bigger) task
  addChildTask: function (options) {
    var self = this;
    options = _.extend({ parent: self }, options || {});
    var child = new Progress(options);
    self._allTasks.push(child);
    self._reportChildState(child, child._state);
    return child;
  },

  // Dumps the tree, for debug
  dump: function (stream, options, prefix) {
    var self = this;

    options = options || {};
    if (options.skipDone && self._isDone) {
      return;
    }

    if (prefix) {
      stream.write(prefix);
    }
    var end = self._state.end;
    if (!end) {
      end = '?';
    }
    stream.write("Task [" + self._title + "] " + self._state.current + "/" + end
      + (self._isDone ? " done" : "") +"\n");
    if (self._allTasks.length) {
      _.each(self._allTasks, function (child) {
        child.dump(stream, options, (prefix || '') + '  ');
      });
    }
  },

  // Receives a state report indicating progress of self
  reportProgress: function (state) {
    var self = this;

    self._selfState = state;

    self._updateTotalState();

    // Nudge the spinner/progress bar, but don't yield (might not be safe to yield)
    consolejs.Console.nudge(false);

    self._notifyState();
  },

  // Subscribes a watcher to changes
  addWatcher: function (watcher) {
    var self = this;

    self._watchers.push(watcher);
  },

  // Notifies watchers & parents
  _notifyState: function () {
    var self = this;

    if (self._parent) {
      self._parent._reportChildState(self, self._state);
    }

    if (self._watchers.length) {
      _.each(self._watchers, function (watcher) {
        watcher(self._state);
      });
    }
  },

  // Recomputes state, incorporating children's states
  _updateTotalState: function () {
    var self = this;

    var state = _.clone(self._selfState);

    var allChildrenDone = true;
    var state = _.clone(self._selfState);
    _.each(self._allTasks, function (child) {
      var childState = child._state;

      if (!child._isDone) {
        allChildrenDone = false;
      }

      state.current += childState.current;
      if (state.end !== undefined) {
        if (childState.done) {
          state.end += childState.current;
        } else if (childState.end !== undefined) {
          state.end += childState.end;
        } else {
          state.end = undefined;
        }
      }
    });
    self._isDone = allChildrenDone && !!self._selfState.done;
    if (!allChildrenDone) {
      state.done = false;
    }

    if (!state.done && self._state.done) {
      // This shouldn't happen
      throw new Error("Progress transition from done => !done");
    }

    self._state = state;
  },

  // Called by a child when its state changes
  _reportChildState: function (child, state) {
    var self = this;

    self._updateTotalState();
    self._notifyState();
  },

  getState: function() {
    return this._state;
  }
});

exports.Progress = Progress;
<|endoftext|>