<commit_before>'use strict';
// We use the package homepage as the default user agent header
// because it's nice to not trick others that we are a browser.
import pkg from '../package.json';
// TinyEmitter is a really nice Event Emitter. We will extend
// our main class from it.
import TinyEmitter from 'tiny-emitter';

// Lodash is an utility library and makes life easy to work
// and iterate over arrays, objects and manipulate values.
import * as _ from 'lodash';

// Request is one of the most popular modules there is to
// make http requests.
import request from 'request';

// Feedparser is the main choice if you want to parse
// rss feeds in XML.
import FeedParser from 'feedparser';

// Bluebird empowers the default Promise with new
// and amazing methods.
import Promise from 'bluebird';

// MAIN CLASS
// This is where we extend from TinyEmitter and absorve
// the #emit and #on methods to emit 'new-item' events
// when we have new feed items.
class RssFeedEmitter extends TinyEmitter {

  // The constructor special method is called everytime
  // we create a new instance of this "Class".
  constructor( options = {} ) {

    // Since this is a "Class", you have to call #super method
    // for the parent class initialize it's internals.
    super();

    // Also, we are creating a blank array to keep all
    // our feed objects.
    this._feedList = [];

    // If the user has specified a User Agent
    // we will use that as the 'user-agent' header when
    // making requests, otherwise we use the package.homepage:
    this._userAgent = options.userAgent || pkg.homepage;

    // This module manages automatically how many feed items
    // it will keep in memory, and basically it will have a
    // maximum history which is how many items the feed has
    // multiplied by this number below. So, if the feed have
    // 10 items, we will keep 30 items max in the history.
    this._historyLengthMultiplier = 3;

  }

  // PUBLIC METHODS
  // Now we are creating the public methods, these are the
  // ones developers will use in their softwares.
  // Remember: if you change a method signature in a way
  // it's not backwards compatible, you created a breaking
  // change, so design them very well.

  // ADD
  // The #add method is one of the main ones. Basically it
  // receives one parameter with the feed options, for example:
  // {
  //   url: 'http://www.nintendolife.com/feeds/news',
  //   refresh: 2000
  // }

  add( userFeedConfig ) {

    // We are going to use a private method to validate
    // the feed object. If it's valid, everything is ok,
    // otherwise it will throw an exception.
    this._validateFeedObject( userFeedConfig );

    // The developer doesn't need to specify the "refresh"
    // property, for example, so this object have the default
    // value.
    let defaultFeedConfig = {
      refresh: 60000
    };

    // With the #defaults method of Lodash, we can merge the user defined
    // values with the default value we defined before.
    let finalFeedConfig = _.defaults( userFeedConfig, defaultFeedConfig );

    // This is a cool private method which will add this new feed if
    // it doesn't exist in our feedList or if it does exist,
    // we will update it.
    this._addOrUpdateFeedList( finalFeedConfig );

    // In the end, return the feedList to the developer.
    return this._feedList;

  }

  // REMOVE
  // This is a very simple method and its functionality is
  // remove a feed from the feedList.

  remove( url ) {

    // Check if the "url" parameter is a string,
    // otherwise throw.
    if ( typeof url !== 'string' ) {

      throw {
        type: 'type_error',
        message: 'You must call #remove with a string containing the feed url'
      };

    }

    // Try to find the feed object using the
    // private method #_findFeed
    let feed = this._findFeed( {
      url: url
    } );

    // And then remove the feed object from
    // the feedList.
    return this._removeFromFeedList( feed );

  }

  // LIST
  // Just return the feedList array.

  list() {

    return this._feedList;

  }

  // DESTROY
  // Remove all feeds from feedList.

  destroy() {

    // There's a gotcha here since we are iterating over an array
    // while removing its properties. To do this, we have to use
    // a little trick where you iterate backwards to not scramble
    // the index while you're removing the properties.
    for ( let i = this._feedList.length - 1; i >= 0; i-- ) {

      let feed = this._feedList[ i ];

      this._removeFromFeedList( feed );

    }

  }

  // PRIVATE METHODS
  // These methods are not suposed to be used by the developer
  // and you can change them without worrying about compatibilities
  // of the public methods.

  // This is used by the public method #add and will receive
  // a feed object and check if all its properties are valid.
  // If the validation you wan't to make is more complex, I
  // recommend you to use a validation library.
  _validateFeedObject( feed ) {

    if ( !feed ) {

      throw {
        type: 'type_error',
        message: 'You must call #add method with a feed configuration object.'
      };

    }

    if ( !feed.url || typeof feed.url !== 'string' || feed.url === '' ) {

      throw {
        type: 'type_error',
        message: 'Your configuration object should have an "url" key with a string value'
      };

    }

    if ( feed.refresh && typeof feed.refresh !== 'number' ) {

      throw {
        type: 'type_error',
        message: 'Your configuration object should have a "refresh" key with a number value'
      };

    }

  }

  // This is also used by the public method #add and will
  // add a new feed to feedList or update it if it's
  // already in the list.
  _addOrUpdateFeedList( feed ) {

    // First, try to find the feed in the feedList.
    let feedInList = this._findFeed( feed );

    // If there's a feed, remove it from the list,
    // since we will add it later again.
    if ( feedInList ) {

      this._removeFromFeedList( feedInList );

    }

    // Now add the feed in the list.
    return this._addToFeedList( feed );

  }

  // Very simple private method: given a feed object
  // try to find it in the feedList using its "url".
  _findFeed( feed ) {

    return _.find( this._feedList, {
      url: feed.url
    } );

  }

  // Given a feed and item, try to find
  // it inside the feed item list. We will use
  // this to see if there's already an item inside
  // the feed item list. If there is, we know it's
  // not a new item.
  _findItem( feed, item ) {

    return _.find( feed.items, {
      link: item.link,
      title: item.title
    } );

  }

  // This is where we create the feed itself and
  // add it to the feedList
  _addToFeedList( feed ) {

    // First, we add a blank array called "items"
    // to the feed object. This is where we will keep the
    // items history.
    feed.items = [];

    // After this, we will create a setInterval and
    // keep a copy of its reference to be able
    // to clean it later when we remove it.
    // This interval will keep fetching the feed's
    // url for new contents.
    feed.setInterval = this._createSetInterval( feed );

    // In the end, we need to push this object
    // to the feedList array.
    this._feedList.push( feed );

  }

  // This method is a little bit complex
  // but also the most important one. It is
  // responsible to keep fetching the RSS
  // for content and emitting events
  // when new items are found.
  _createSetInterval( feed ) {

    // First, lets keep the "this" reference.
    let instance = this;

    // The getContent function will:
    //
    // 1. call the _fetchFeed to get the content
    //    from the RSS feed.
    //
    // 2. Locate the feed object inside the feedList.
    //
    // 3. Redefine the feed history length (using
    //    the _historyLengthMultiplier variable.
    //
    // 4. Sort the received items by date so we always
    //    work with the items in ascending order
    //
    // 5. Given the items received from the last fetch
    //    and the current items in the feed item list,
    //    select only the new ones.
    //
    // 6. Now take the new ones, push them to the
    //    feed item list and emit while emitting events.
    function getContent() {

      instance._fetchFeed( feed.url )
        .tap( findFeed )
        .tap( redefineItemHistoryMaxLength )
        .tap( sortItemsByDate )
        .tap( identifyOnlyNewItems )
        .tap( populateNewItemsInFeed )
        .catch( ( error ) => {

          // If this chain is iterating over a recently
          // deleted feed, it will not be found in the
          // feedList. If this happens, just ignore
          // silently.
          if ( error.type === 'feed_not_found' ) {

            return;

          }

          // Otherwise, emit an "error" event
          instance.emit( 'error', error );

        } );

      // 2. This happens after we got the results from
      // the private method _fetchFeed. Basically we
      // are going to receive a "data" object with
      // the feed URL and the feed content.
      function findFeed( data ) {

        // Try to find the feed object inside this instance
        // using the feed url from the fetch.
        let foundFeed = instance._findFeed( {
          url: data.feedUrl
        } );

        // If nothing is found, probably we destroyed
        // it in the middle of a fetch. Let's throw an
        // error to get out of this Promise chain, since
        // we can't keep going further.
        if ( !foundFeed ) {

          throw {
            type: 'feed_not_found',
            message: 'Feed not found.'
          };

        }

        // If we found the feed, everything is ok and
        // lets add it to the "data" object.
        data.feed = foundFeed;

      }

      // Since we are managing the max history length,
      // this is a good time to get the items length
      // from the fetched feed, multiply it by the
      // _historyLengthMultiplier and update the
      // feed maxHistoryLength value. This is cool
      // because if the source feed starts to increase
      // or decrese its items quantity, we will ajust
      // automatically how many items we keep in memory.
      function redefineItemHistoryMaxLength( data ) {

        let feedLength = data.items.length;

        data.feed.maxHistoryLength = feedLength * instance._historyLengthMultiplier;

      }

      // Sort all received items since we want to
      // emit them in ascending order.
      function sortItemsByDate( data ) {

        data.items = _.sortBy( data.items, 'date' );

      }

      // Put all new items inside a "newItems" property
      function identifyOnlyNewItems( data ) {

        // We can do this using the "filter" method of the array.
        // The logic behind this is, if you return a "false" value,
        // that item will be skipped, but if you return a "true"
        // value, it will be added to the final array.
        data.newItems = data.items.filter( ( fetchedItem ) => {

          // So, try to find the suposed new item inside the current feed item list.
          let foundItemInsideFeed = instance._findItem( data.feed, fetchedItem );

          // If you found it, that item is not new, so skip it
          // returning a false value.
          if ( foundItemInsideFeed ) {

            return false;

          }

          // But if we couldn't find it, this means it is a new item and we need
          // to add it to the newItems array. To do this, just return a truthy value.
          return fetchedItem;

        } );

      }

      // Now that we have all the new items, add them to the
      // feed item list.
      function populateNewItemsInFeed( data ) {

        data.newItems.forEach( ( item ) => {

          instance._addItemToItemList( data.feed, item );

        } );

      }

    }

    // Call the getContent function to already start getting
    // content, otherwise you will have to wait for a
    // setInterval cicle.
    getContent();

    // Create and return the setInterval itself.
    return setInterval( getContent, feed.refresh );

  }

  // This is the inverse of the _addToFeedList method
  // and we will destroy the setInterval and remove
  // the feed from the feedList.
  _removeFromFeedList( feed ) {

    if ( !feed ) {

      return;

    }

    clearInterval( feed.setInterval );
    _.remove( this._feedList, { url: feed.url } );

  }

  // Add a feed item to its item list.
  _addItemToItemList( feed, item ) {

    // Push them to the items array.
    feed.items.push( item );

    // Keep the max history length in control.
    feed.items = _.takeRight( feed.items, feed.maxHistoryLength );

    // And emit the "new-item" event of this item.
    this.emit( 'new-item', item );

  }

  // This is where the http request happens.
  _fetchFeed( feedUrl ) {

    // Remember the above Promise chain? Everything starts here.
    return new Promise( ( resolve, reject ) => {

      // Create a FeedParser instance.
      let feedparser = new FeedParser();

      // Create that "data" object we used over and over inside
      // the getContent Promise chain. It starts with a feed url
      // property and a blank item array.
      let data = {
        feedUrl: feedUrl,
        items: []
      };

      // Now let's request that feed URL sending a very nice header
      // because some servers doesn't like requests without it.
      request.get( {
        url: feedUrl,
        headers: {
          'user-agent': this._userAgent,
          'accept': 'text/html,application/xhtml+xml,application/xml'
        }
      } )
      // Run this once we get a response from the server.
      .on( 'response', requestOnResponse )
      // If the request is rejected, call this function.
      .on( 'error', requestOnError )
      // If everything is ok, let's parse the feed.
      .pipe( feedparser )
      // and in the end, let's call the finish function.
      .on( 'end', finish );

      // Basically what we do here is, if we doesn't get a "200" status
      // code from the request, reject the promise because probably we
      // received something like a "404" or "500" error.
      function requestOnResponse( res ) {

        let statusOk = 200;

        if ( res.statusCode !== statusOk ) {

          let error = {
            type: 'fetch_url_error',
            message: `This URL returned a ${res.statusCode} status code`,
            feed: feedUrl
          };

          reject( error );

        }

      }

      // This will happen when Node.js itself couldn't connet
      // to the host to get something from it.
      function requestOnError( responseError ) {

        if ( responseError.code === 'ENOTFOUND' ) {

          let error = {
            type: 'fetch_url_error',
            message: `Cannot connect to ${feedUrl}`,
            feed: feedUrl

          };

          reject( error );

        }

      }

      // Everytime the Feedparser emits a "readable" event
      // we have a new feed item, so we need to add it to the
      // data.item list.
      feedparser.on( 'readable', () => {

        // Read the item itself.
        let item = feedparser.read();

        // Force the feed URL inside the feed item because
        // some times the RSS doesn't have the feed url inside
        // every item.
        item.meta.link = feedUrl;

        // Add to the data.items.
        data.items.push( item );

      } );

      // Feedparser will also emit an "error" event
      // if it's not able to parse the XML. If this
      // happens, we need to interrupt the Promise chain
      // since we can't keep going further.
      feedparser.on( 'error', () => {

        let error = {
          type: 'invalid_feed',
          message: `Cannot parse ${feedUrl} XML`,
          feed: feedUrl

        };

        reject( error );

      } );

      // Once finished, let's resolve the Promise
      // with the "data" object.
      function finish() {

        resolve( data );

      }

    } );

  }

}

// In the end, just export the RssFeedEmitter class.
export default RssFeedEmitter;
<commit_msg>refactor(user-agent): move user-agent string to module src<commit_after>'use strict';

// TinyEmitter is a really nice Event Emitter. We will extend
// our main class from it.
import TinyEmitter from 'tiny-emitter';

// Lodash is an utility library and makes life easy to work
// and iterate over arrays, objects and manipulate values.
import * as _ from 'lodash';

// Request is one of the most popular modules there is to
// make http requests.
import request from 'request';

// Feedparser is the main choice if you want to parse
// rss feeds in XML.
import FeedParser from 'feedparser';

// Bluebird empowers the default Promise with new
// and amazing methods.
import Promise from 'bluebird';

// MAIN CLASS
// This is where we extend from TinyEmitter and absorve
// the #emit and #on methods to emit 'new-item' events
// when we have new feed items.
class RssFeedEmitter extends TinyEmitter {

  // The constructor special method is called everytime
  // we create a new instance of this "Class".
  constructor( options = {} ) {

    // Since this is a "Class", you have to call #super method
    // for the parent class initialize it's internals.
    super();

    // Also, we are creating a blank array to keep all
    // our feed objects.
    this._feedList = [];

    // If the user has specified a User Agent
    // we will use that as the 'user-agent' header when
    // making requests, otherwise we use the default option.
    this._userAgent = options.userAgent || 'Node/RssFeedEmitter (https://github.com/filipedeschamps/rss-feed-emitter)';

    // This module manages automatically how many feed items
    // it will keep in memory, and basically it will have a
    // maximum history which is how many items the feed has
    // multiplied by this number below. So, if the feed have
    // 10 items, we will keep 30 items max in the history.
    this._historyLengthMultiplier = 3;

  }

  // PUBLIC METHODS
  // Now we are creating the public methods, these are the
  // ones developers will use in their softwares.
  // Remember: if you change a method signature in a way
  // it's not backwards compatible, you created a breaking
  // change, so design them very well.

  // ADD
  // The #add method is one of the main ones. Basically it
  // receives one parameter with the feed options, for example:
  // {
  //   url: 'http://www.nintendolife.com/feeds/news',
  //   refresh: 2000
  // }

  add( userFeedConfig ) {

    // We are going to use a private method to validate
    // the feed object. If it's valid, everything is ok,
    // otherwise it will throw an exception.
    this._validateFeedObject( userFeedConfig );

    // The developer doesn't need to specify the "refresh"
    // property, for example, so this object have the default
    // value.
    let defaultFeedConfig = {
      refresh: 60000
    };

    // With the #defaults method of Lodash, we can merge the user defined
    // values with the default value we defined before.
    let finalFeedConfig = _.defaults( userFeedConfig, defaultFeedConfig );

    // This is a cool private method which will add this new feed if
    // it doesn't exist in our feedList or if it does exist,
    // we will update it.
    this._addOrUpdateFeedList( finalFeedConfig );

    // In the end, return the feedList to the developer.
    return this._feedList;

  }

  // REMOVE
  // This is a very simple method and its functionality is
  // remove a feed from the feedList.

  remove( url ) {

    // Check if the "url" parameter is a string,
    // otherwise throw.
    if ( typeof url !== 'string' ) {

      throw {
        type: 'type_error',
        message: 'You must call #remove with a string containing the feed url'
      };

    }

    // Try to find the feed object using the
    // private method #_findFeed
    let feed = this._findFeed( {
      url: url
    } );

    // And then remove the feed object from
    // the feedList.
    return this._removeFromFeedList( feed );

  }

  // LIST
  // Just return the feedList array.

  list() {

    return this._feedList;

  }

  // DESTROY
  // Remove all feeds from feedList.

  destroy() {

    // There's a gotcha here since we are iterating over an array
    // while removing its properties. To do this, we have to use
    // a little trick where you iterate backwards to not scramble
    // the index while you're removing the properties.
    for ( let i = this._feedList.length - 1; i >= 0; i-- ) {

      let feed = this._feedList[ i ];

      this._removeFromFeedList( feed );

    }

  }

  // PRIVATE METHODS
  // These methods are not suposed to be used by the developer
  // and you can change them without worrying about compatibilities
  // of the public methods.

  // This is used by the public method #add and will receive
  // a feed object and check if all its properties are valid.
  // If the validation you wan't to make is more complex, I
  // recommend you to use a validation library.
  _validateFeedObject( feed ) {

    if ( !feed ) {

      throw {
        type: 'type_error',
        message: 'You must call #add method with a feed configuration object.'
      };

    }

    if ( !feed.url || typeof feed.url !== 'string' || feed.url === '' ) {

      throw {
        type: 'type_error',
        message: 'Your configuration object should have an "url" key with a string value'
      };

    }

    if ( feed.refresh && typeof feed.refresh !== 'number' ) {

      throw {
        type: 'type_error',
        message: 'Your configuration object should have a "refresh" key with a number value'
      };

    }

  }

  // This is also used by the public method #add and will
  // add a new feed to feedList or update it if it's
  // already in the list.
  _addOrUpdateFeedList( feed ) {

    // First, try to find the feed in the feedList.
    let feedInList = this._findFeed( feed );

    // If there's a feed, remove it from the list,
    // since we will add it later again.
    if ( feedInList ) {

      this._removeFromFeedList( feedInList );

    }

    // Now add the feed in the list.
    return this._addToFeedList( feed );

  }

  // Very simple private method: given a feed object
  // try to find it in the feedList using its "url".
  _findFeed( feed ) {

    return _.find( this._feedList, {
      url: feed.url
    } );

  }

  // Given a feed and item, try to find
  // it inside the feed item list. We will use
  // this to see if there's already an item inside
  // the feed item list. If there is, we know it's
  // not a new item.
  _findItem( feed, item ) {

    return _.find( feed.items, {
      link: item.link,
      title: item.title
    } );

  }

  // This is where we create the feed itself and
  // add it to the feedList
  _addToFeedList( feed ) {

    // First, we add a blank array called "items"
    // to the feed object. This is where we will keep the
    // items history.
    feed.items = [];

    // After this, we will create a setInterval and
    // keep a copy of its reference to be able
    // to clean it later when we remove it.
    // This interval will keep fetching the feed's
    // url for new contents.
    feed.setInterval = this._createSetInterval( feed );

    // In the end, we need to push this object
    // to the feedList array.
    this._feedList.push( feed );

  }

  // This method is a little bit complex
  // but also the most important one. It is
  // responsible to keep fetching the RSS
  // for content and emitting events
  // when new items are found.
  _createSetInterval( feed ) {

    // First, lets keep the "this" reference.
    let instance = this;

    // The getContent function will:
    //
    // 1. call the _fetchFeed to get the content
    //    from the RSS feed.
    //
    // 2. Locate the feed object inside the feedList.
    //
    // 3. Redefine the feed history length (using
    //    the _historyLengthMultiplier variable.
    //
    // 4. Sort the received items by date so we always
    //    work with the items in ascending order
    //
    // 5. Given the items received from the last fetch
    //    and the current items in the feed item list,
    //    select only the new ones.
    //
    // 6. Now take the new ones, push them to the
    //    feed item list and emit while emitting events.
    function getContent() {

      instance._fetchFeed( feed.url )
        .tap( findFeed )
        .tap( redefineItemHistoryMaxLength )
        .tap( sortItemsByDate )
        .tap( identifyOnlyNewItems )
        .tap( populateNewItemsInFeed )
        .catch( ( error ) => {

          // If this chain is iterating over a recently
          // deleted feed, it will not be found in the
          // feedList. If this happens, just ignore
          // silently.
          if ( error.type === 'feed_not_found' ) {

            return;

          }

          // Otherwise, emit an "error" event
          instance.emit( 'error', error );

        } );

      // 2. This happens after we got the results from
      // the private method _fetchFeed. Basically we
      // are going to receive a "data" object with
      // the feed URL and the feed content.
      function findFeed( data ) {

        // Try to find the feed object inside this instance
        // using the feed url from the fetch.
        let foundFeed = instance._findFeed( {
          url: data.feedUrl
        } );

        // If nothing is found, probably we destroyed
        // it in the middle of a fetch. Let's throw an
        // error to get out of this Promise chain, since
        // we can't keep going further.
        if ( !foundFeed ) {

          throw {
            type: 'feed_not_found',
            message: 'Feed not found.'
          };

        }

        // If we found the feed, everything is ok and
        // lets add it to the "data" object.
        data.feed = foundFeed;

      }

      // Since we are managing the max history length,
      // this is a good time to get the items length
      // from the fetched feed, multiply it by the
      // _historyLengthMultiplier and update the
      // feed maxHistoryLength value. This is cool
      // because if the source feed starts to increase
      // or decrese its items quantity, we will ajust
      // automatically how many items we keep in memory.
      function redefineItemHistoryMaxLength( data ) {

        let feedLength = data.items.length;

        data.feed.maxHistoryLength = feedLength * instance._historyLengthMultiplier;

      }

      // Sort all received items since we want to
      // emit them in ascending order.
      function sortItemsByDate( data ) {

        data.items = _.sortBy( data.items, 'date' );

      }

      // Put all new items inside a "newItems" property
      function identifyOnlyNewItems( data ) {

        // We can do this using the "filter" method of the array.
        // The logic behind this is, if you return a "false" value,
        // that item will be skipped, but if you return a "true"
        // value, it will be added to the final array.
        data.newItems = data.items.filter( ( fetchedItem ) => {

          // So, try to find the suposed new item inside the current feed item list.
          let foundItemInsideFeed = instance._findItem( data.feed, fetchedItem );

          // If you found it, that item is not new, so skip it
          // returning a false value.
          if ( foundItemInsideFeed ) {

            return false;

          }

          // But if we couldn't find it, this means it is a new item and we need
          // to add it to the newItems array. To do this, just return a truthy value.
          return fetchedItem;

        } );

      }

      // Now that we have all the new items, add them to the
      // feed item list.
      function populateNewItemsInFeed( data ) {

        data.newItems.forEach( ( item ) => {

          instance._addItemToItemList( data.feed, item );

        } );

      }

    }

    // Call the getContent function to already start getting
    // content, otherwise you will have to wait for a
    // setInterval cicle.
    getContent();

    // Create and return the setInterval itself.
    return setInterval( getContent, feed.refresh );

  }

  // This is the inverse of the _addToFeedList method
  // and we will destroy the setInterval and remove
  // the feed from the feedList.
  _removeFromFeedList( feed ) {

    if ( !feed ) {

      return;

    }

    clearInterval( feed.setInterval );
    _.remove( this._feedList, { url: feed.url } );

  }

  // Add a feed item to its item list.
  _addItemToItemList( feed, item ) {

    // Push them to the items array.
    feed.items.push( item );

    // Keep the max history length in control.
    feed.items = _.takeRight( feed.items, feed.maxHistoryLength );

    // And emit the "new-item" event of this item.
    this.emit( 'new-item', item );

  }

  // This is where the http request happens.
  _fetchFeed( feedUrl ) {

    // Remember the above Promise chain? Everything starts here.
    return new Promise( ( resolve, reject ) => {

      // Create a FeedParser instance.
      let feedparser = new FeedParser();

      // Create that "data" object we used over and over inside
      // the getContent Promise chain. It starts with a feed url
      // property and a blank item array.
      let data = {
        feedUrl: feedUrl,
        items: []
      };

      // Now let's request that feed URL sending a very nice header
      // because some servers doesn't like requests without it.
      request.get( {
        url: feedUrl,
        headers: {
          'user-agent': this._userAgent,
          'accept': 'text/html,application/xhtml+xml,application/xml'
        }
      } )
      // Run this once we get a response from the server.
      .on( 'response', requestOnResponse )
      // If the request is rejected, call this function.
      .on( 'error', requestOnError )
      // If everything is ok, let's parse the feed.
      .pipe( feedparser )
      // and in the end, let's call the finish function.
      .on( 'end', finish );

      // Basically what we do here is, if we doesn't get a "200" status
      // code from the request, reject the promise because probably we
      // received something like a "404" or "500" error.
      function requestOnResponse( res ) {

        let statusOk = 200;

        if ( res.statusCode !== statusOk ) {

          let error = {
            type: 'fetch_url_error',
            message: `This URL returned a ${res.statusCode} status code`,
            feed: feedUrl
          };

          reject( error );

        }

      }

      // This will happen when Node.js itself couldn't connet
      // to the host to get something from it.
      function requestOnError( responseError ) {

        if ( responseError.code === 'ENOTFOUND' ) {

          let error = {
            type: 'fetch_url_error',
            message: `Cannot connect to ${feedUrl}`,
            feed: feedUrl

          };

          reject( error );

        }

      }

      // Everytime the Feedparser emits a "readable" event
      // we have a new feed item, so we need to add it to the
      // data.item list.
      feedparser.on( 'readable', () => {

        // Read the item itself.
        let item = feedparser.read();

        // Force the feed URL inside the feed item because
        // some times the RSS doesn't have the feed url inside
        // every item.
        item.meta.link = feedUrl;

        // Add to the data.items.
        data.items.push( item );

      } );

      // Feedparser will also emit an "error" event
      // if it's not able to parse the XML. If this
      // happens, we need to interrupt the Promise chain
      // since we can't keep going further.
      feedparser.on( 'error', () => {

        let error = {
          type: 'invalid_feed',
          message: `Cannot parse ${feedUrl} XML`,
          feed: feedUrl

        };

        reject( error );

      } );

      // Once finished, let's resolve the Promise
      // with the "data" object.
      function finish() {

        resolve( data );

      }

    } );

  }

}

// In the end, just export the RssFeedEmitter class.
export default RssFeedEmitter;
<|endoftext|>


<commit_before>import { put } from 'redux-saga/effects';
import { Convert } from 'pvtsutils';
import { ErrorActions } from '../../actions/state';

/**
 * Returns index of key from key storage
 * @param {Crypto}      crypto
 * @param {CryptoKey}   key
 * @returns {string | null}
 */
export function* keyIndexOf(crypto, key) {
  return yield crypto.keyStorage.indexOf(key);
}

/**
 * Returns thumbprint of public key
 * @param {Crypto} crypto
 * @param {CryptoKey} publicKey
 */
export function* publicKeyThumbprint(crypto, publicKey, hash = 'SHA-256') {
  if (publicKey.type !== 'public') {
    throw new Error(`Wrong type of key '${publicKey.type}'. Must be 'public'`);
  }
  const spki = yield crypto.subtle.exportKey('spki', publicKey);
  // NOTE: Use native digest instead of service provider
  const thumbprint = yield window.crypto.subtle.digest(hash, spki);
  return Convert.ToHex(thumbprint);
}

/**
 * Get provider key IDs
 * @param {object} crypto
 * @returns {Promise|Array}
 */
export function* keyGetIDs(crypto) {
  return yield crypto.keyStorage.keys();
}

/**
 * Set provider key
 * @param {object} crypto
 * @param {string} key
 * @returns {Promise|[]}
 */
export function* keySet(crypto, key) {
  return yield crypto.keyStorage.setItem(key);
}

/**
 * Get provider key
 * @param {object} crypto
 * @param {string} id
 * @returns {Promise|Boolean}
 */
export function* keyGet(crypto, id) {
  try {
    return yield crypto.keyStorage.getItem(id);
  } catch (error) {
    console.error(error);
    return false;
  }
}

/**
 * Remove provider key
 * @param {object} crypto
 * @param {string} id
 * @returns {boolean}
 */
export function* keyRemove(crypto, id) {
  yield crypto.keyStorage.removeItem(id);
}
<commit_msg>Use native digest for publicKeyThumbprint<commit_after>import { put } from 'redux-saga/effects';
import { Convert } from 'pvtsutils';
import { ErrorActions } from '../../actions/state';

/**
 * Returns index of key from key storage
 * @param {Crypto}      crypto
 * @param {CryptoKey}   key
 * @returns {string | null}
 */
export function* keyIndexOf(crypto, key) {
  return yield crypto.keyStorage.indexOf(key);
}

/**
 * Returns thumbprint of public key
 * @param {Crypto} crypto
 * @param {CryptoKey} publicKey
 */
export function* publicKeyThumbprint(crypto, publicKey, hash = 'SHA-256') {
  if (publicKey.type !== 'public') {
    throw new Error(`Wrong type of key '${publicKey.type}'. Must be 'public'`);
  }
  const spki = yield crypto.subtle.exportKey('raw', publicKey);
  // NOTE: Use native digest instead of service provider
  const thumbprint = yield window.crypto.subtle.digest(hash, spki);
  return Convert.ToHex(thumbprint);
}

/**
 * Get provider key IDs
 * @param {object} crypto
 * @returns {Promise|Array}
 */
export function* keyGetIDs(crypto) {
  return yield crypto.keyStorage.keys();
}

/**
 * Set provider key
 * @param {object} crypto
 * @param {string} key
 * @returns {Promise|[]}
 */
export function* keySet(crypto, key) {
  return yield crypto.keyStorage.setItem(key);
}

/**
 * Get provider key
 * @param {object} crypto
 * @param {string} id
 * @returns {Promise|Boolean}
 */
export function* keyGet(crypto, id) {
  try {
    return yield crypto.keyStorage.getItem(id);
  } catch (error) {
    console.error(error);
    return false;
  }
}

/**
 * Remove provider key
 * @param {object} crypto
 * @param {string} id
 * @returns {boolean}
 */
export function* keyRemove(crypto, id) {
  yield crypto.keyStorage.removeItem(id);
}
<|endoftext|>


<commit_before>/*
 *  TitleScene.js
 *  2014/06/19
 *  @auther minimo
 *  This Program is MIT license.
 *
 */

tm.define("shotgun.TitleScene", {
    superClass: tm.app.Scene,

    //ボタン用フォントパラメータ
    buttonParam: {fontFamily:"azuki", align: "center", baseline:"middle", outlineWidth:4 },
    labelParam: {fontFamily:"azuki", align: "center", baseline:"middle", outlineWidth:2 },
    scoreParam: {fontFamily:"azuki", align: "left", baseline:"middle", outlineWidth:2 },

    bgColor: 'rgba(50, 150, 50, 1)',

    init: function() {
        this.superInit();
        this.background = "rgba(0, 0, 0, 0.0)";

        //バックグラウンド
        this.bg = tm.display.Shape(SC_W, SC_H)
            .addChildTo(this)
            .setPosition(SC_W*0.5, SC_H*0.5)
            .renderRectangle({fillStyle: this.bgColor, strokeStyle: this.bgColor});

        //タイトルとチュートリアルを分けてレイヤーを作成
        this.titleLayer = tm.app.Object2D().addChildTo(this);
        this.underLayer = tm.app.Object2D().addChildTo(this.titleLayer);
        this.tutorialLayer = tm.app.Object2D().addChildTo(this);

        //チュートリアル側バックグラウンド
        this.bg2 = tm.display.Shape(SC_W, SC_H)
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*1.5, SC_H*0.5)
            .renderRectangle({fillStyle: this.bgColor, strokeStyle: this.bgColor});

        //各画面セットアップ
        this.setupTitle();

        //目隠し
        this.mask = tm.display.Shape(SC_W, SC_H)
            .addChildTo(this)
            .setPosition(SC_W*0.5, SC_H*0.5)
            .renderRectangle({fillStyle: "rgba(0, 0, 0, 1.0)", strokeStyle: "rgba(0, 0, 0, 1.0)"});
        this.mask.tweener.clear().fadeOut(200);

        this.time = 0;

        appMain.playBGM("titleBGM");

        if(ENABLE_ADMOB && AdMob) {
            AdMob.showBannerAtXY(0, 0);
        }
    },

    onresume: function() {
        this.mask.tweener.clear().fadeOut(200);
    },

    setupTitle: function() {
/*
        var fillStyle = tm.graphics.LinearGradient(-SC_W*0.2, 0, SC_W*0.1, 64)
            .addColorStopList([
                { offset: 0.1, color: "hsla(130, 90%, 0%, 0.5)"},
                { offset: 0.5, color: "hsla(130, 90%, 0%, 0.9)"},
                { offset: 0.9, color: "hsla(140, 90%, 0%, 0.5)"},
            ]).toStyle();
*/
        var fillStyle = "Red";
        var outlineStyle = "White";
        var shadowColor = 'rgba(160, 160, 160, 1)';

        //ショットガンシルエット
        var sg = tm.display.Sprite("shotgun", 640, 250)
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.5, SC_H*0.2);
        sg.scaleX = -1;
        sg.rotation = -10;

        //タイトルロゴ
        tm.display.Sprite("titlelogo", 600, 300)
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.5, SC_H*0.2);

        var that = this;
        var width = 300, height = 70;
        var param = {fillStyle:'rgba(0,80,0,1)', lineWidth:4};

        //プレイスタート
        shotgun.Button(width, height, "START")
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.5, SC_H*0.40)
            .addEventListener("pushed", function() {
                that.mask.tweener.clear().fadeIn(200).call(function(){appMain.replaceScene(shotgun.MainScene());});
            });

        //チュートリアル
        shotgun.Button(width, height, "TUTORIAL")
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.5, SC_H*0.50)
            .addEventListener("pushed", function() {
                that.titleLayer.tweener.clear()
                    .moveBy(-SC_W, 0, 500, "easeOutQuint")
                    .call(function(){
                        appMain.pushScene(shotgun.TutorialScene());
                    })
                    .moveBy(SC_W, 0, 500, "easeOutQuint")
                    .call(function(){
                        appMain.playBGM("titleBGM");
                    });
            });

        //設定
        shotgun.Button(width, height, "OPTION")
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.5, SC_H*0.60)
            .addEventListener("pushed", function() {
                that.mask.tweener.clear()
                    .fadeIn(200)
                    .call(function(){
                        appMain.pushScene(shotgun.SettingScene());
                    });
            });

        //クレジット
        shotgun.Button(width, height, "CREDIT")
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.5, SC_H*0.70)
            .addEventListener("pushed", function() {
                that.mask.tweener.clear()
                    .fadeIn(200)
                    .call(function(){
                        appMain.pushScene(shotgun.CreditScene());
                    });
            });

        //GAMECENTER
        shotgun.Button(width, height, "GAME CENTER")
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.5, SC_H*0.80)
            .addEventListener("pushed", function() {
                if (!PHONEGAP) return;
                if (!GAMECENTER) {
                    gamecenter.auth(onGamecenterSuccess, onGamecenterFailure);
//                    AdvanceAlert('GAMECENTERの準備が出来ていません');
                }
                var data = {
                    period: "today",
                    leaderboardId: "board"
                };
                gamecenter.showLeaderboard(function() {}, function() {AdvanceAlert($trans("GAMECENTERのアクセスに失敗しました"));}, data);
//                that.mask.tweener.clear().fadeIn(200).call(function(){appMain.pushScene(shotgun.CreditScene());});
            });

            //バージョン表示
            tm.display.OutlineLabel("Version "+appMain.version, 30)
                .addChildTo(this.titleLayer)
                .setPosition(SC_W*0.5, SC_H*0.9)
                .setParam({fontFamily:"CasinoRegular", align: "center", baseline:"middle", outlineWidth:3 });
    },

    addButton: function(page, finish) {
        var that = this;
        var width = 230, height = 60;
        var param = {fillStyle:'rgba(0,80,0,1)', lineWidth:4};

        //戻る
        shotgun.Button(width, height, "PREV")
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.25+SC_W*page, SC_H*0.9)
            .addEventListener("pushed", function() {
                that.titleLayer.tweener.clear().moveBy(SC_W, 0, 500, "easeOutQuint");
            });

        if (!finish) {
            //次
            shotgun.Button(width, height, "NEXT")
                .addChildTo(this.titleLayer)
                .setPosition(SC_W*0.75+SC_W*page, SC_H*0.9)
                .addEventListener("pushed", function() {
                    that.titleLayer.tweener.clear().moveBy(-SC_W, 0, 500, "easeOutQuint");
                });
        } else {
            //終了
            shotgun.Button(width, height, "EXIT")
                .addChildTo(this.titleLayer)
                .setPosition(SC_W*0.75+SC_W*page, SC_H*0.9)
                .addEventListener("pushed", function() {
                    that.titleLayer.tweener.clear().moveBy(SC_W*page, 0, 500, "easeOutQuint");
                });
        }
    },

    setupScoreList: function() {
        var page = 3;
        var that = this;
        var width = 230, height = 60;
        var param = {fillStyle:'rgba(0,80,0,1)', lineWidth:4};

        tm.display.OutlineLabel("得点表", 40)
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.5+SC_W*page, SC_H*0.1)
            .setParam(this.labelParam);

        for (var i = 0; i < 10; i++) {
            tm.display.OutlineLabel($trans(appMain.handList[i+2].name)+" : "+appMain.handList[i+2].point+"pts", 40)
                .addChildTo(this.titleLayer)
                .setPosition(SC_W*0.1+SC_W*page, SC_H*0.17+(i*70))
                .setParam(this.scoreParam);
        }

        this.addButton(page, true);
    },

    update: function() {
        if (this.time % 7 == 0) {
            var c = tm.display.Sprite("card", CARD_W, CARD_H)
                .addChildTo(this.underLayer)
                .setPosition(rand(0, SC_W), -100-rand(0, 50))
                .setFrameIndex(rand(0, 54));
            var d = rand(0, 10);
            if (d == 3) c.setFrameIndex(52);
            if (d == 4) c.setFrameIndex(53);
            c.update = function() {
                this.rotation+=this.vr;
                this.y+=this.vy;
                if (this.y > SC_H*1.2) {this.remove();}
            }
            c.vr = rand(-5, 5) || 1;
            c.vy = rand(5, 15);
            c.setScale(rand(7, 10)/10);
        }

        //スクリーンショット保存
        var kb = appMain.keyboard;
        if (kb.getKeyDown("s")) appMain.canvas.saveAsImage();

        this.time++;
    },

    //タッチorクリック開始処理
    ontouchstart: function(e) {
    },

    //タッチorクリック移動処理
    ontouchmove: function(e) {
    },

    //タッチorクリック終了処理
    ontouchend: function(e) {
        tm.sound.WebAudio.unlock();
//        if (this.time > 30) appMain.replaceScene(shotgun.MainScene());
    },

});
<commit_msg>バナー表示変更<commit_after>/*
 *  TitleScene.js
 *  2014/06/19
 *  @auther minimo
 *  This Program is MIT license.
 *
 */

tm.define("shotgun.TitleScene", {
    superClass: tm.app.Scene,

    //ボタン用フォントパラメータ
    buttonParam: {fontFamily:"azuki", align: "center", baseline:"middle", outlineWidth:4 },
    labelParam: {fontFamily:"azuki", align: "center", baseline:"middle", outlineWidth:2 },
    scoreParam: {fontFamily:"azuki", align: "left", baseline:"middle", outlineWidth:2 },

    bgColor: 'rgba(50, 150, 50, 1)',

    init: function() {
        this.superInit();
        this.background = "rgba(0, 0, 0, 0.0)";

        //バックグラウンド
        this.bg = tm.display.Shape(SC_W, SC_H)
            .addChildTo(this)
            .setPosition(SC_W*0.5, SC_H*0.5)
            .renderRectangle({fillStyle: this.bgColor, strokeStyle: this.bgColor});

        //タイトルとチュートリアルを分けてレイヤーを作成
        this.titleLayer = tm.app.Object2D().addChildTo(this);
        this.underLayer = tm.app.Object2D().addChildTo(this.titleLayer);
        this.tutorialLayer = tm.app.Object2D().addChildTo(this);

        //チュートリアル側バックグラウンド
        this.bg2 = tm.display.Shape(SC_W, SC_H)
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*1.5, SC_H*0.5)
            .renderRectangle({fillStyle: this.bgColor, strokeStyle: this.bgColor});

        //各画面セットアップ
        this.setupTitle();

        //目隠し
        this.mask = tm.display.Shape(SC_W, SC_H)
            .addChildTo(this)
            .setPosition(SC_W*0.5, SC_H*0.5)
            .renderRectangle({fillStyle: "rgba(0, 0, 0, 1.0)", strokeStyle: "rgba(0, 0, 0, 1.0)"});
        this.mask.tweener.clear().fadeOut(200);

        this.time = 0;

        appMain.playBGM("titleBGM");
    },

    onresume: function() {
        this.mask.tweener.clear().fadeOut(200);
    },

    setupTitle: function() {
/*
        var fillStyle = tm.graphics.LinearGradient(-SC_W*0.2, 0, SC_W*0.1, 64)
            .addColorStopList([
                { offset: 0.1, color: "hsla(130, 90%, 0%, 0.5)"},
                { offset: 0.5, color: "hsla(130, 90%, 0%, 0.9)"},
                { offset: 0.9, color: "hsla(140, 90%, 0%, 0.5)"},
            ]).toStyle();
*/
        var fillStyle = "Red";
        var outlineStyle = "White";
        var shadowColor = 'rgba(160, 160, 160, 1)';

        //ショットガンシルエット
        var sg = tm.display.Sprite("shotgun", 640, 250)
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.5, SC_H*0.2);
        sg.scaleX = -1;
        sg.rotation = -10;

        //タイトルロゴ
        tm.display.Sprite("titlelogo", 600, 300)
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.5, SC_H*0.2);

        var that = this;
        var width = 300, height = 70;
        var param = {fillStyle:'rgba(0,80,0,1)', lineWidth:4};

        //プレイスタート
        shotgun.Button(width, height, "START")
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.5, SC_H*0.40)
            .addEventListener("pushed", function() {
                appMain.bonusLife = 0;
                that.mask.tweener.clear().fadeIn(200).call(function(){appMain.replaceScene(shotgun.MainScene());});
            });

        //チュートリアル
        shotgun.Button(width, height, "TUTORIAL")
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.5, SC_H*0.50)
            .addEventListener("pushed", function() {
                that.titleLayer.tweener.clear()
                    .moveBy(-SC_W, 0, 500, "easeOutQuint")
                    .call(function(){
                        appMain.pushScene(shotgun.TutorialScene());
                    })
                    .moveBy(SC_W, 0, 500, "easeOutQuint")
                    .call(function(){
                        appMain.playBGM("titleBGM");
                    });
            });

        //設定
        shotgun.Button(width, height, "OPTION")
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.5, SC_H*0.60)
            .addEventListener("pushed", function() {
                that.mask.tweener.clear()
                    .fadeIn(200)
                    .call(function(){
                        appMain.pushScene(shotgun.SettingScene());
                    });
            });

        //クレジット
        shotgun.Button(width, height, "CREDIT")
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.5, SC_H*0.70)
            .addEventListener("pushed", function() {
                that.mask.tweener.clear()
                    .fadeIn(200)
                    .call(function(){
                        appMain.pushScene(shotgun.CreditScene());
                    });
            });

        //GAMECENTER
        shotgun.Button(width, height, "GAME CENTER")
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.5, SC_H*0.80)
            .addEventListener("pushed", function() {
                if (!PHONEGAP) return;
                if (!GAMECENTER) {
                    gamecenter.auth(onGamecenterSuccess, onGamecenterFailure);
//                    AdvanceAlert('GAMECENTERの準備が出来ていません');
                }
                var data = {
                    period: "today",
                    leaderboardId: "board"
                };
                gamecenter.showLeaderboard(function() {}, function() {AdvanceAlert($trans("GAMECENTERのアクセスに失敗しました"));}, data);
//                that.mask.tweener.clear().fadeIn(200).call(function(){appMain.pushScene(shotgun.CreditScene());});
            });

            //バージョン表示
            tm.display.OutlineLabel("Version "+appMain.version, 30)
                .addChildTo(this.titleLayer)
                .setPosition(SC_W*0.5, SC_H*0.9)
                .setParam({fontFamily:"CasinoRegular", align: "center", baseline:"middle", outlineWidth:3 });
    },

    addButton: function(page, finish) {
        var that = this;
        var width = 230, height = 60;
        var param = {fillStyle:'rgba(0,80,0,1)', lineWidth:4};

        //戻る
        shotgun.Button(width, height, "PREV")
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.25+SC_W*page, SC_H*0.9)
            .addEventListener("pushed", function() {
                that.titleLayer.tweener.clear().moveBy(SC_W, 0, 500, "easeOutQuint");
            });

        if (!finish) {
            //次
            shotgun.Button(width, height, "NEXT")
                .addChildTo(this.titleLayer)
                .setPosition(SC_W*0.75+SC_W*page, SC_H*0.9)
                .addEventListener("pushed", function() {
                    that.titleLayer.tweener.clear().moveBy(-SC_W, 0, 500, "easeOutQuint");
                });
        } else {
            //終了
            shotgun.Button(width, height, "EXIT")
                .addChildTo(this.titleLayer)
                .setPosition(SC_W*0.75+SC_W*page, SC_H*0.9)
                .addEventListener("pushed", function() {
                    that.titleLayer.tweener.clear().moveBy(SC_W*page, 0, 500, "easeOutQuint");
                });
        }
    },

    setupScoreList: function() {
        var page = 3;
        var that = this;
        var width = 230, height = 60;
        var param = {fillStyle:'rgba(0,80,0,1)', lineWidth:4};

        tm.display.OutlineLabel("得点表", 40)
            .addChildTo(this.titleLayer)
            .setPosition(SC_W*0.5+SC_W*page, SC_H*0.1)
            .setParam(this.labelParam);

        for (var i = 0; i < 10; i++) {
            tm.display.OutlineLabel($trans(appMain.handList[i+2].name)+" : "+appMain.handList[i+2].point+"pts", 40)
                .addChildTo(this.titleLayer)
                .setPosition(SC_W*0.1+SC_W*page, SC_H*0.17+(i*70))
                .setParam(this.scoreParam);
        }

        this.addButton(page, true);
    },

    update: function() {
        if (this.time % 7 == 0) {
            var c = tm.display.Sprite("card", CARD_W, CARD_H)
                .addChildTo(this.underLayer)
                .setPosition(rand(0, SC_W), -100-rand(0, 50))
                .setFrameIndex(rand(0, 54));
            var d = rand(0, 10);
            if (d == 3) c.setFrameIndex(52);
            if (d == 4) c.setFrameIndex(53);
            c.update = function() {
                this.rotation+=this.vr;
                this.y+=this.vy;
                if (this.y > SC_H*1.2) {this.remove();}
            }
            c.vr = rand(-5, 5) || 1;
            c.vy = rand(5, 15);
            c.setScale(rand(7, 10)/10);
        }

        //スクリーンショット保存
        var kb = appMain.keyboard;
        if (kb.getKeyDown("s")) appMain.canvas.saveAsImage();

        this.time++;
    },

    //タッチorクリック開始処理
    ontouchstart: function(e) {
    },

    //タッチorクリック移動処理
    ontouchmove: function(e) {
    },

    //タッチorクリック終了処理
    ontouchend: function(e) {
        tm.sound.WebAudio.unlock();
//        if (this.time > 30) appMain.replaceScene(shotgun.MainScene());
    },

});
<|endoftext|>


<commit_before>(function() {

/**
 * HTTP 1.1 Caching header helpers
 */

// Regular expressions for cache-control directives.
// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
var ccPublic = /^\s*public\s*$/
  , ccPrivate = /^\s*private\s*$/
  , ccNoCache = /^\s*no-cache\s*$/
  , ccNoStore = /^\s*no-store\s*$/
  , ccNoTransform = /^\s*no-store\s*$/
  , ccMustRevalidate = /^\s*must-revalidate\s*$/
  , ccProxyRevalidate = /^\s*proxy-revalidate\s*$/
  , ccMaxAge = /^\s*max-age\s*=\s*(\d+)\s*$/
  , ccSMaxAge = /^\s*s-maxage\s*=\s*(\d+)\s*$/

    /**
     * ccParse - takes a string argument that is an HTTP 1.1
     * Cache-Control directive and returns a parsed objects with booleans set for
     * specified values and integers for max-age and s-maxage
     */
  , ccParse = function (directive) {
        var directives = directive.split(',')

            // A default object with keys for every cache-control response property,
            // all of them will be null or boolean except max-age and s-maxage which
            // should be positive integers
          , parsedDirective = {
                'public': null
              , 'private': null
              , 'no-cache': null
              , 'no-store': null
              , 'no-transform': null
              , 'must-revalidate': null
              , 'proxy-revalidate': null
              , 'max-age': null
              , 's-maxage': null
            };

        directives.forEach(function(d) {
            var matches;
            if (ccPublic.test(d)) parsedDirective['public'] = true;
            else if (ccPrivate.test(d)) parsedDirective['private'] = true;
            else if (ccNoCache.test(d)) parsedDirective['no-cache'] = true;
            else if (ccNoStore.test(d)) parsedDirective['no-store'] = true;
            else if (ccNoTransform.test(d)) parsedDirective['no-transform'] = true;
            else if (ccMustRevalidate.test(d)) {
                parsedDirective['must-revalidate'] = true;
            } else if (ccProxyRevalidate.test(d)) {
                parsedDirective['proxy-revalidate'] = true;
            } else if (matches = ccMaxAge.exec(d)) {
                parsedDirective['max-age'] = matches[1];
            } else if (matches = ccSMaxAge.exec(d)) {
                parsedDirective['s-maxage'] = matches[1];
            }
        })

        return parsedDirective;
    }

    /**
     * A function to compute the age of an HTTP response, returns the age in
     * milliseconds
     */
  , getAge = function(response) {
        var apparentAge, date, age = 0, now = Date.now();
        if (date = response.headers['Date']) {
            date = Date.parse(date);
        } else {
            date = now;
        }
        return apparentAge = now - date;
    }

    /**
     * getFreshnessLifetime - returns the freshnessLifetime of the response in
     * milliseconds
     */
  , getFreshnessLifetime = function (response) {
        var cacheControl, maxAge, expires, now;
        now = Date.now();

        // If there's a max-age cache-control directive, return it
        if (cacheControl = response.headers['cache-control']) {
            cacheControl = ccParse(cacheControl);
            if ((cacheControl['max-age'] !== null) &&
                (cacheControl['private'] === null) &&
                (cacheControl['no-store'] === null) &&
                (cacheControl['no-cache'] === null)) {

                maxAge = parseInt(cacheControl['max-age']);
                // max-age is in seconds, these functions deal in milliseconds
                return maxAge * 1000;
            }
        }

        // Otherewise, try to compute a max-age from the Expires header
        if (expires = response.headers['expires']) {
            expires = Date.parse(expires);
            return expires - now;
        }

        // Otherwise, the freshness lifetime is 0
        return 0;
    }

    /**
     * maxAge - takes an http response object, returns a new max-age value,
     * in seconds, for a new response containing it to be sent now.
     */
  , maxAge = function (response) {
        var newMaxAge = getFreshnessLifetime(response) - getAge(response);
        return Math.floor(newMaxAge/1000);
    }

    /**
     * A function to create a data URI from a mobify combo resource object
     */
  , dataURI = function(resource) {
        return 'data:' +
            resource.headers['content-type'] +
            (!resource['text'] ? (';base64,' + resource.body) :
                (',' + encodeURIComponent(resource.body)));
    }

    // Global resource dictionary, to be found later at Mobify.com.resources
  , resources = {}

  , cache = Mobify.httpCaching = {

      // key used for localStorage caching
      lsKey: 'Mobify-Combo-Cache-v1.0'
      /**
       * Predicate for determining whether or not a comboed resource is "stale" by
       * HTTP 1.1 caching rules.
       */
      , isStale: function(resource) {
          return getAge(resource) > maxAge(resource)
        }

      , isCacheable: function(resource) {
            return getFreshnessLifetime(resource) > 0 && !cache.isStale(resource)
        }

        /**
         * Deletes resources from the dictionary that are considered stale by
         * HTTP 1.1 caching rules.
         */
      , evictStale: function() {
            for (var i in resources) {
                if (resources.hasOwnProperty(i) && cache.isStale(resources[i])) {
                    delete resources[i]
                }
            }
        }

        /**
         * Takes an array of urls to be comboed, returns a list of those URLs that
         * are not already in the resources dictionary.
         */
      , notCachedUrls: function(urls) {
            var notCachedUrls = [];
            for (var i = 0; i < urls.length; i++) {
                if (!(resources[urls[i]])) {
                    notCachedUrls.push(urls[i]);
                }
            }

            //DEBUG
            console.log('urls: ' + JSON.stringify(urls));
            console.log('notCachedUrls: ' + JSON.stringify(notCachedUrls));

            return notCachedUrls;
        }

        /**
         * Takes an array of urls to be comboed, returns those which are already in
         * the resources dictionary.
         */
      , cachedUrls: function(urls) {
            var cachedUrls = [];
            for (var i = 0; i < urls.length; i++) {
                if (resources[urls[i]]) {
                    cachedUrls.push(urls[i]);
                }
            }

            return cachedUrls;
        }
    }

  , localStorageAvailable = (function() {
        var k = 'MobifyTestLocalStorage', v = 'Yay!';
        try {
            localStorage.setItem(k, v);
            if(v === localStorage.getItem(k)) {
                localStorage.removeItem(k);
                return true;
            } else {
                return false;
            }
        } catch(e) {
            return false;
        }
    })()

  , storeResource = function(resource) {
        var url = resource.url;

        /* ensure this response was successfully fetched by the service */
        if (resource.status == 'ready') {
            combo.resources[url] = resource;
        } else {
            console.log("Combo service failed to retrieve: %s", url);
        }
    }

    /* asynchronously recursive function that attempts to whittle down a
       cache to a storeable size */
  , evictAndStore = function(resources, attempts) {
        var serialzed;
        if (attempts == 0) {
            console.log('Mobify.combo.dehydrateCache: evict and store attempts exceeded, aborting');
        // get rid of something.
        } else {
            evictOne(resources);
            try {
                serialzed = JSON.stringify(resources)
            } catch(e) {
                console.log("Mobify.combo.dehydrateCache error stringifying: " + e.message);
                return;
            }

            try {
                localStorage.setItem(cache.lsKey, serialzed);
            // If localStorage is full, try again with one less item, "co-operatively".
            } catch(e) {
                setTimeout(function() {
                    evictAndStore(resources, attempts - 1)
                }, 0);
            }
        }
    }

    /**
     * Evict one item from a set of resources, using a least recently used cache
     * eviction policy.
     */
  , evictOne = function(resources) {
            /* beginning of the epoch */
        var START_OF_TIME = 0
            /* end of the epoch, 2^53 - 1 milliseconds after the beginning,
               285000+ years from now */
          , END_OF_TIME = 9007199254740991
          , lruKey
          , lruTime = END
          , r;
        for (var k in resources) {
            if (resources.hasOwnProperty(k)) {
                r = resources[k];
                if(r.lastUsed) {
                    // if this resource has been used less recently than the so
                    // far least recently used resource, this gets nominated
                    if(r.lastUsed <= lruTime) {
                        lruKey = k;
                        lruTime = r.lastUsed;
                    }
                } else {
                    // if lastUsed is not set, we will consider this to never
                    // have been used, so, this key gets nominated
                    lruKey = k;
                    lruTime = BEGINNING;
                    break;
                }
            }
        }
        // lruKey will be set by now
        delete resources[lruKey];
    }

  , combo = Mobify.combo = {

        resources: resources

        /**
         * Store a resoruce or an array of resources from the mobify combo service
         * in our resource dictionary.
         */
      , store: function(r) {
            if (r instanceof Array) {
                for (var i = 0; i < r.length; i++) {
                    storeResource(r[i]);
                }
            } else {
                storeResource(r);
            }
            combo.dehydrateCache();
        }

        /**
         * Retrieve a JS resource from the combo.resources object and write out a
         * script tag with it as a dataURI.
         * Note, document.writing the script tag is probably the only way to
         * preserve execution order:
         * http://blog.getify.com/ff4-script-loaders-and-order-preservation/
         */
      , loadSync: function(url) {
            var r;

            /* add it if we have it */
            if (r = resources[url]) {
                // do a little accounting for caching purposes
                r.lastUsed = Date.now();
                r.useCount = r.useCount++ || 1
                /* if we have the resource in our dictionary, use data uri rather
                   than a network uri */
                url = dataURI(r)
            }
            /* write out a script tag which contains either the data uri of the
               resource or the original network uri if for some reason it was not in
               combo's resource dictionary */
            document.write('<script src="' + url + '"></script>');
        }

        , loadAsync: function(url) {
            var r, s;
            s = document.createElement('SCRIPT');
            /* add it if we have it */
            if(r = resources['url']) {
                r.lastUsed = Date.now();
                r.useCount = r.useCount++ || 1
                s.src = dataURI(r);
            } else {
                /* otherwise, make the browser get it itself */
                s.src = url;
            }
            document.body.appendChild(s);
        }

        , storeAndLoadAsync: function(resources) {
            combo.store(resources);
            for(var i = 0; i < resources.length; i++) {
                // iterate through the resources we've been asked to and load them
                combo.loadAsync(resources[i]['url']);
            }
        }

        /* A guard property to ensure that we only rehydrate the cache once */

      , rehydratedCache: false

        /**
         * Get keys out of the localStorage cache and into our in-memory reosurce
         * dictionary, if we haven't already.
         */
      , rehydrateCache: function() {
            if (!localStorageAvailable || combo.rehydratedCache == true) {
                return;
            }

            //DEBUG
            console.log("rehydrating cache!")

            var r, k, cacheContents = localStorage.getItem(cache.lsKey);
            if (cacheContents !== null) {
                try {
                    cacheContents = JSON.parse(cacheContents);
                } catch(e) {
                    console.log('Mobify.combo.rehydrateCache: error parsing localStorage[' +
                        this.key + ']: ', e.message);
                    return;
                }

                // Extract keys which are not loaded.
                for (k in cacheContents) {
                    if (cacheContents.hasOwnProperty(k) && !resources[k]) {
                        resources[k] = cacheContents[k];
                    }
                }
            }
            combo.rehydratedCache = true;
        }

        /**
         * Store keys from the local reource dictionary back into the localStorage
         * cache.
         */
      , dehydrateCache: function() {
            if (!localStorageAvailable) return;

            var MAX_ATTEMPTS = 10
              , toBeCached = {};

            // start by shallow copying the global resources dictionary, since
            // we're going to modify its key list, but not its values
            for (var i in resources) {
                if (resources.hasOwnProperty(i)) {
                    toBeCached[i] = resources[i];
                }
            }

            /* These might get expensive, do them on next tick */
            setTimeout( function() {
                /* serialize the shallow copy */
                try {
                    var serialized = JSON.stringify(toBeCached)
                } catch(e) {
                    console.log("Mobify.combo.dehydrateCache error stringifying: " + e.message);
                    return;
                }

                try {
                    // The only indicationw e get of a full localstorage is an exception
                    localStorage.setItem(cache.lsKey, serialized);
                    console.log('dehydrated cache!')
                // when localStorage is full, try again with one less item, on next tick
                } catch(e) {
                    setTimeout(function() {
                        evictAndStore(toBeCached, MAX_ATTEMPTS)
                    }, 0);
                }
            }, 0);
        }
        /**
         * Clear out the cache both in localStorage and in memory
         */
      , clearCache: function() {
            if(localStorageAvailable) {
                localStorage.removeItem(cache.lsKey);
            }
            Mobify.combo.resources = resources = {};
        }
    };

})(Mobify);<commit_msg>fix initialization of lruTime<commit_after>(function() {

/**
 * HTTP 1.1 Caching header helpers
 */

// Regular expressions for cache-control directives.
// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
var ccPublic = /^\s*public\s*$/
  , ccPrivate = /^\s*private\s*$/
  , ccNoCache = /^\s*no-cache\s*$/
  , ccNoStore = /^\s*no-store\s*$/
  , ccNoTransform = /^\s*no-store\s*$/
  , ccMustRevalidate = /^\s*must-revalidate\s*$/
  , ccProxyRevalidate = /^\s*proxy-revalidate\s*$/
  , ccMaxAge = /^\s*max-age\s*=\s*(\d+)\s*$/
  , ccSMaxAge = /^\s*s-maxage\s*=\s*(\d+)\s*$/

    /**
     * ccParse - takes a string argument that is an HTTP 1.1
     * Cache-Control directive and returns a parsed objects with booleans set for
     * specified values and integers for max-age and s-maxage
     */
  , ccParse = function (directive) {
        var directives = directive.split(',')

            // A default object with keys for every cache-control response property,
            // all of them will be null or boolean except max-age and s-maxage which
            // should be positive integers
          , parsedDirective = {
                'public': null
              , 'private': null
              , 'no-cache': null
              , 'no-store': null
              , 'no-transform': null
              , 'must-revalidate': null
              , 'proxy-revalidate': null
              , 'max-age': null
              , 's-maxage': null
            };

        directives.forEach(function(d) {
            var matches;
            if (ccPublic.test(d)) parsedDirective['public'] = true;
            else if (ccPrivate.test(d)) parsedDirective['private'] = true;
            else if (ccNoCache.test(d)) parsedDirective['no-cache'] = true;
            else if (ccNoStore.test(d)) parsedDirective['no-store'] = true;
            else if (ccNoTransform.test(d)) parsedDirective['no-transform'] = true;
            else if (ccMustRevalidate.test(d)) {
                parsedDirective['must-revalidate'] = true;
            } else if (ccProxyRevalidate.test(d)) {
                parsedDirective['proxy-revalidate'] = true;
            } else if (matches = ccMaxAge.exec(d)) {
                parsedDirective['max-age'] = matches[1];
            } else if (matches = ccSMaxAge.exec(d)) {
                parsedDirective['s-maxage'] = matches[1];
            }
        })

        return parsedDirective;
    }

    /**
     * A function to compute the age of an HTTP response, returns the age in
     * milliseconds
     */
  , getAge = function(response) {
        var apparentAge, date, age = 0, now = Date.now();
        if (date = response.headers['Date']) {
            date = Date.parse(date);
        } else {
            date = now;
        }
        return apparentAge = now - date;
    }

    /**
     * getFreshnessLifetime - returns the freshnessLifetime of the response in
     * milliseconds
     */
  , getFreshnessLifetime = function (response) {
        var cacheControl, maxAge, expires, now;
        now = Date.now();

        // If there's a max-age cache-control directive, return it
        if (cacheControl = response.headers['cache-control']) {
            cacheControl = ccParse(cacheControl);
            if ((cacheControl['max-age'] !== null) &&
                (cacheControl['private'] === null) &&
                (cacheControl['no-store'] === null) &&
                (cacheControl['no-cache'] === null)) {

                maxAge = parseInt(cacheControl['max-age']);
                // max-age is in seconds, these functions deal in milliseconds
                return maxAge * 1000;
            }
        }

        // Otherewise, try to compute a max-age from the Expires header
        if (expires = response.headers['expires']) {
            expires = Date.parse(expires);
            return expires - now;
        }

        // Otherwise, the freshness lifetime is 0
        return 0;
    }

    /**
     * maxAge - takes an http response object, returns a new max-age value,
     * in seconds, for a new response containing it to be sent now.
     */
  , maxAge = function (response) {
        var newMaxAge = getFreshnessLifetime(response) - getAge(response);
        return Math.floor(newMaxAge/1000);
    }

    /**
     * A function to create a data URI from a mobify combo resource object
     */
  , dataURI = function(resource) {
        return 'data:' +
            resource.headers['content-type'] +
            (!resource['text'] ? (';base64,' + resource.body) :
                (',' + encodeURIComponent(resource.body)));
    }

    // Global resource dictionary, to be found later at Mobify.com.resources
  , resources = {}

  , cache = Mobify.httpCaching = {

      // key used for localStorage caching
      lsKey: 'Mobify-Combo-Cache-v1.0'
      /**
       * Predicate for determining whether or not a comboed resource is "stale" by
       * HTTP 1.1 caching rules.
       */
      , isStale: function(resource) {
          return getAge(resource) > maxAge(resource)
        }

      , isCacheable: function(resource) {
            return getFreshnessLifetime(resource) > 0 && !cache.isStale(resource)
        }

        /**
         * Deletes resources from the dictionary that are considered stale by
         * HTTP 1.1 caching rules.
         */
      , evictStale: function() {
            for (var i in resources) {
                if (resources.hasOwnProperty(i) && cache.isStale(resources[i])) {
                    delete resources[i]
                }
            }
        }

        /**
         * Takes an array of urls to be comboed, returns a list of those URLs that
         * are not already in the resources dictionary.
         */
      , notCachedUrls: function(urls) {
            var notCachedUrls = [];
            for (var i = 0; i < urls.length; i++) {
                if (!(resources[urls[i]])) {
                    notCachedUrls.push(urls[i]);
                }
            }

            //DEBUG
            console.log('urls: ' + JSON.stringify(urls));
            console.log('notCachedUrls: ' + JSON.stringify(notCachedUrls));

            return notCachedUrls;
        }

        /**
         * Takes an array of urls to be comboed, returns those which are already in
         * the resources dictionary.
         */
      , cachedUrls: function(urls) {
            var cachedUrls = [];
            for (var i = 0; i < urls.length; i++) {
                if (resources[urls[i]]) {
                    cachedUrls.push(urls[i]);
                }
            }

            return cachedUrls;
        }
    }

  , localStorageAvailable = (function() {
        var k = 'MobifyTestLocalStorage', v = 'Yay!';
        try {
            localStorage.setItem(k, v);
            if(v === localStorage.getItem(k)) {
                localStorage.removeItem(k);
                return true;
            } else {
                return false;
            }
        } catch(e) {
            return false;
        }
    })()

  , storeResource = function(resource) {
        var url = resource.url;

        /* ensure this response was successfully fetched by the service */
        if (resource.status == 'ready') {
            combo.resources[url] = resource;
        } else {
            console.log("Combo service failed to retrieve: %s", url);
        }
    }

    /* asynchronously recursive function that attempts to whittle down a
       cache to a storeable size */
  , evictAndStore = function(resources, attempts) {
        var serialzed;
        if (attempts == 0) {
            console.log('Mobify.combo.dehydrateCache: evict and store attempts exceeded, aborting');
        // get rid of something.
        } else {
            evictOne(resources);
            try {
                serialzed = JSON.stringify(resources)
            } catch(e) {
                console.log("Mobify.combo.dehydrateCache error stringifying: " + e.message);
                return;
            }

            try {
                localStorage.setItem(cache.lsKey, serialzed);
            // If localStorage is full, try again with one less item, "co-operatively".
            } catch(e) {
                setTimeout(function() {
                    evictAndStore(resources, attempts - 1)
                }, 0);
            }
        }
    }

    /**
     * Evict one item from a set of resources, using a least recently used cache
     * eviction policy.
     */
  , evictOne = function(resources) {
            /* beginning of the epoch */
        var START_OF_TIME = 0
            /* end of the epoch, 2^53 - 1 milliseconds after the beginning,
               285000+ years from now */
          , END_OF_TIME = 9007199254740991
          , lruKey
          , lruTime = END_OF_TIME
          , r;
        for (var k in resources) {
            if (resources.hasOwnProperty(k)) {
                r = resources[k];
                if(r.lastUsed) {
                    // if this resource has been used less recently than the so
                    // far least recently used resource, this gets nominated
                    if(r.lastUsed <= lruTime) {
                        lruKey = k;
                        lruTime = r.lastUsed;
                    }
                } else {
                    // if lastUsed is not set, we will consider this to never
                    // have been used, so, this key gets nominated
                    lruKey = k;
                    lruTime = BEGINNING;
                    break;
                }
            }
        }
        // lruKey will be set by now
        delete resources[lruKey];
    }

  , combo = Mobify.combo = {

        resources: resources

        /**
         * Store a resoruce or an array of resources from the mobify combo service
         * in our resource dictionary.
         */
      , store: function(r) {
            if (r instanceof Array) {
                for (var i = 0; i < r.length; i++) {
                    storeResource(r[i]);
                }
            } else {
                storeResource(r);
            }
            combo.dehydrateCache();
        }

        /**
         * Retrieve a JS resource from the combo.resources object and write out a
         * script tag with it as a dataURI.
         * Note, document.writing the script tag is probably the only way to
         * preserve execution order:
         * http://blog.getify.com/ff4-script-loaders-and-order-preservation/
         */
      , loadSync: function(url) {
            var r;

            /* add it if we have it */
            if (r = resources[url]) {
                // do a little accounting for caching purposes
                r.lastUsed = Date.now();
                r.useCount = r.useCount++ || 1
                /* if we have the resource in our dictionary, use data uri rather
                   than a network uri */
                url = dataURI(r)
            }
            /* write out a script tag which contains either the data uri of the
               resource or the original network uri if for some reason it was not in
               combo's resource dictionary */
            document.write('<script src="' + url + '"></script>');
        }

        , loadAsync: function(url) {
            var r, s;
            s = document.createElement('SCRIPT');
            /* add it if we have it */
            if(r = resources['url']) {
                r.lastUsed = Date.now();
                r.useCount = r.useCount++ || 1
                s.src = dataURI(r);
            } else {
                /* otherwise, make the browser get it itself */
                s.src = url;
            }
            document.body.appendChild(s);
        }

        , storeAndLoadAsync: function(resources) {
            combo.store(resources);
            for(var i = 0; i < resources.length; i++) {
                // iterate through the resources we've been asked to and load them
                combo.loadAsync(resources[i]['url']);
            }
        }

        /* A guard property to ensure that we only rehydrate the cache once */

      , rehydratedCache: false

        /**
         * Get keys out of the localStorage cache and into our in-memory reosurce
         * dictionary, if we haven't already.
         */
      , rehydrateCache: function() {
            if (!localStorageAvailable || combo.rehydratedCache == true) {
                return;
            }

            //DEBUG
            console.log("rehydrating cache!")

            var r, k, cacheContents = localStorage.getItem(cache.lsKey);
            if (cacheContents !== null) {
                try {
                    cacheContents = JSON.parse(cacheContents);
                } catch(e) {
                    console.log('Mobify.combo.rehydrateCache: error parsing localStorage[' +
                        this.key + ']: ', e.message);
                    return;
                }

                // Extract keys which are not loaded.
                for (k in cacheContents) {
                    if (cacheContents.hasOwnProperty(k) && !resources[k]) {
                        resources[k] = cacheContents[k];
                    }
                }
            }
            combo.rehydratedCache = true;
        }

        /**
         * Store keys from the local reource dictionary back into the localStorage
         * cache.
         */
      , dehydrateCache: function() {
            if (!localStorageAvailable) return;

            var MAX_ATTEMPTS = 10
              , toBeCached = {};

            // start by shallow copying the global resources dictionary, since
            // we're going to modify its key list, but not its values
            for (var i in resources) {
                if (resources.hasOwnProperty(i)) {
                    toBeCached[i] = resources[i];
                }
            }

            /* These might get expensive, do them on next tick */
            setTimeout( function() {
                /* serialize the shallow copy */
                try {
                    var serialized = JSON.stringify(toBeCached)
                } catch(e) {
                    console.log("Mobify.combo.dehydrateCache error stringifying: " + e.message);
                    return;
                }

                try {
                    // The only indicationw e get of a full localstorage is an exception
                    localStorage.setItem(cache.lsKey, serialized);
                    console.log('dehydrated cache!')
                // when localStorage is full, try again with one less item, on next tick
                } catch(e) {
                    setTimeout(function() {
                        evictAndStore(toBeCached, MAX_ATTEMPTS)
                    }, 0);
                }
            }, 0);
        }
        /**
         * Clear out the cache both in localStorage and in memory
         */
      , clearCache: function() {
            if(localStorageAvailable) {
                localStorage.removeItem(cache.lsKey);
            }
            Mobify.combo.resources = resources = {};
        }
    };

})(Mobify);<|endoftext|>


<commit_before>import AWS from 'aws-sdk';
import Promise from 'bluebird';
import merge from 'merge';
import helpers from '../../utils/helpers';
import {
  GraphQLObjectType,
  GraphQLNonNull,
  GraphQLList,
  GraphQLString,
  GraphQLInputObjectType
} from 'graphql/lib/type';

const sqs = new Promise.promisifyAll(new AWS.SQS());

const attributeFields = {
  DelaySeconds: {
    type: GraphQLString,
    description: `The time in seconds that the delivery of
                  all messages in the queue will be delayed.
                  An integer from 0 to 900 (15 minutes).
                  The default for this attribute is 0 (zero).`
  },
  MaximumMessageSize: {
    type: GraphQLString,
    description: `The limit of how many bytes a message can
                  contain before Amazon SQS rejects it. An
                  integer from 1024 bytes (1 KiB) up to 262144
                  bytes (256 KiB). The default for this attribute
                  is 262144 (256 KiB).`
  },
  MessageRetentionPeriod: {
    type: GraphQLString,
    description: `The number of seconds Amazon SQS retains a message.
                  Integer representing seconds, from 60 (1 minute) to
                  1209600 (14 days). The default for this attribute
                  is 345600 (4 days).`
  },
  Policy: {
    type: GraphQLString,
    description: `The queue's policy. A valid AWS policy.`
  },
  ReceiveMessageWaitTimeSeconds: {
    type: GraphQLString,
    description: `The time for which a ReceiveMessage call will wait
                  for a message to arrive. An integer from 0 to 20
                  (seconds). The default for this attribute is 0.`
  },
  VisibilityTimeout: {
    type: GraphQLString,
    description: `The visibility timeout for the queue.
                  An integer from 0 to 43200 (12 hours).
                  The default for this attribute is 30.`
  }
};

const type = new GraphQLObjectType({
  name: 'Queue',
  description: 'Represents an Amazon SQS message queue.',
  fields: () => ({
    QueueUrl: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'The url for the message queue.',
      resolve: (root) => { return root.QueueUrl || root; }
    },
    Attributes: {
      type: attributesType,
      resolve: (root) => {
        const params = {
          QueueUrl: root.QueueUrl,
          AttributeNames: ['All']
        };
        return sqs.getQueueAttributesAsync(params)
                .then(result => result.Attributes);
      }
    }
  })
});

const attributesType = new GraphQLObjectType({
  name: 'Queue Attributes',
  fields: () => (attributeFields)
});

const attributesInputType = new GraphQLInputObjectType({
  name: 'Queue Attributes Input',
  fields: () => (attributeFields)
});

const messageType = new GraphQLObjectType({
  name: 'Message',
  description: 'Represents an Amazon SQS message queue.',
  fields: () => ({
    MessageId: {
      type: GraphQLString,
      description: `An element containing the message ID
                    of the message sent to the queue.`
    },
    MD5OfMessageBody: {
      type: GraphQLString,
      description: `An MD5 digest of the non-URL-encoded
                    message body string. This can be used to
                    verify that Amazon SQS received the message
                    correctly. Amazon SQS first URL decodes the
                    message before creating the MD5 digest`,
      resolve: (root) => { return root.MD5OfMessageBody || root.MD5OfBody; }
    },
    MD5OfMessageAttributes: {
      type: GraphQLString,
      description: `An MD5 digest of the non-URL-encoded message
                    attribute string. This can be used to verify
                    that Amazon SQS received the message correctly.
                    Amazon SQS first URL decodes the message before
                    creating the MD5 digest.`
    },
    Body: {
      type: GraphQLString,
      description: `The message's contents (not URL-encoded).`
    }
  })
});

exports.queries = {
  queues: {
    type: new GraphQLList(GraphQLString),
    args: {
      QueueNamePrefix: {
        name: 'QueueNamePrefix',
        type: GraphQLString,
        description: `Only those queues whose name begins
                      with the specified string are returned.`
      }
    },
    resolve: (root, {QueueNamePrefix}) => {
      return sqs.listQueuesAsync(QueueNamePrefix)
              .then(result => result.QueueUrls);
    }
  }
};

exports.mutations = {
  sendMessage: {
    type: messageType,
    args: {
      QueueUrl: {
        description: 'The URL of the Amazon SQS queue to take action on.',
        type: new GraphQLNonNull(GraphQLString)
      },
      MessageBody: {
        description: 'The message to send. String maximum 256 KB in size.',
        type: new GraphQLNonNull(GraphQLString)
      }
    },
    resolve: (obj, {QueueUrl, MessageBody}) => {
      const params = {
        QueueUrl: QueueUrl,
        MessageBody: MessageBody
      };
      return sqs.sendMessageAsync(params);
    }
  },
  receiveMessage: {
    type: new GraphQLList(messageType),
    args: {
      QueueUrl: {
        description: 'The URL of the Amazon SQS queue to take action on.',
        type: new GraphQLNonNull(GraphQLString)
      }
    },
    resolve: (obj, {QueueUrl}) => {
      const params = {
        QueueUrl: QueueUrl
      };
      return sqs.receiveMessageAsync(params)
              .then(result => result.Messages);
    }
  },
  deleteQueue: {
    type: new GraphQLNonNull(GraphQLString),
    args: {
      QueueUrl: {
        description: 'The URL of the Amazon SQS queue to take action on.',
        type: new GraphQLNonNull(GraphQLString)
      }
    },
    resolve: (obj, {QueueUrl}) => {
      const params = {
        QueueUrl: QueueUrl
      };
      return sqs.deleteQueueAsync(params);
    }
  },
  createQueue: {
    type: type,
    description: 'Creates a new queue, or returns the URL of an existing one.',
    args: {
      QueueName: {
        name: 'QueueName',
        type: new GraphQLNonNull(GraphQLString),
        description: 'The name for the queue to be created.'
      },
      Attributes: {
        name: 'Attributes',
        type: attributesInputType,
        description: 'An attributes object with corresponding values.'
      }
    },
    resolve: (obj, {QueueName, Attributes}) => {
      const params = {
        QueueName: QueueName
      };
      if (Attributes) {
        merge(params, { Attributes: helpers.removeUndefinedKeys(Attributes) });
      }
      return sqs.createQueueAsync(params)
        .then((result) => { return { QueueUrl: result.QueueUrl }; });
    }
  }
};
<commit_msg>Rename queues query to listQueues<commit_after>import AWS from 'aws-sdk';
import Promise from 'bluebird';
import merge from 'merge';
import helpers from '../../utils/helpers';
import {
  GraphQLObjectType,
  GraphQLNonNull,
  GraphQLList,
  GraphQLString,
  GraphQLInputObjectType
} from 'graphql/lib/type';

const sqs = new Promise.promisifyAll(new AWS.SQS());

const attributeFields = {
  DelaySeconds: {
    type: GraphQLString,
    description: `The time in seconds that the delivery of
                  all messages in the queue will be delayed.
                  An integer from 0 to 900 (15 minutes).
                  The default for this attribute is 0 (zero).`
  },
  MaximumMessageSize: {
    type: GraphQLString,
    description: `The limit of how many bytes a message can
                  contain before Amazon SQS rejects it. An
                  integer from 1024 bytes (1 KiB) up to 262144
                  bytes (256 KiB). The default for this attribute
                  is 262144 (256 KiB).`
  },
  MessageRetentionPeriod: {
    type: GraphQLString,
    description: `The number of seconds Amazon SQS retains a message.
                  Integer representing seconds, from 60 (1 minute) to
                  1209600 (14 days). The default for this attribute
                  is 345600 (4 days).`
  },
  Policy: {
    type: GraphQLString,
    description: `The queue's policy. A valid AWS policy.`
  },
  ReceiveMessageWaitTimeSeconds: {
    type: GraphQLString,
    description: `The time for which a ReceiveMessage call will wait
                  for a message to arrive. An integer from 0 to 20
                  (seconds). The default for this attribute is 0.`
  },
  VisibilityTimeout: {
    type: GraphQLString,
    description: `The visibility timeout for the queue.
                  An integer from 0 to 43200 (12 hours).
                  The default for this attribute is 30.`
  }
};

const type = new GraphQLObjectType({
  name: 'Queue',
  description: 'Represents an Amazon SQS message queue.',
  fields: () => ({
    QueueUrl: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'The url for the message queue.',
      resolve: (root) => { return root.QueueUrl || root; }
    },
    Attributes: {
      type: attributesType,
      resolve: (root) => {
        const params = {
          QueueUrl: root.QueueUrl,
          AttributeNames: ['All']
        };
        return sqs.getQueueAttributesAsync(params)
                .then(result => result.Attributes);
      }
    }
  })
});

const attributesType = new GraphQLObjectType({
  name: 'Queue Attributes',
  fields: () => (attributeFields)
});

const attributesInputType = new GraphQLInputObjectType({
  name: 'Queue Attributes Input',
  fields: () => (attributeFields)
});

const messageType = new GraphQLObjectType({
  name: 'Message',
  description: 'Represents an Amazon SQS message queue.',
  fields: () => ({
    MessageId: {
      type: GraphQLString,
      description: `An element containing the message ID
                    of the message sent to the queue.`
    },
    MD5OfMessageBody: {
      type: GraphQLString,
      description: `An MD5 digest of the non-URL-encoded
                    message body string. This can be used to
                    verify that Amazon SQS received the message
                    correctly. Amazon SQS first URL decodes the
                    message before creating the MD5 digest`,
      resolve: (root) => { return root.MD5OfMessageBody || root.MD5OfBody; }
    },
    MD5OfMessageAttributes: {
      type: GraphQLString,
      description: `An MD5 digest of the non-URL-encoded message
                    attribute string. This can be used to verify
                    that Amazon SQS received the message correctly.
                    Amazon SQS first URL decodes the message before
                    creating the MD5 digest.`
    },
    Body: {
      type: GraphQLString,
      description: `The message's contents (not URL-encoded).`
    }
  })
});

exports.queries = {
  listQueues: {
    type: new GraphQLList(GraphQLString),
    args: {
      QueueNamePrefix: {
        name: 'QueueNamePrefix',
        type: GraphQLString,
        description: `Only those queues whose name begins
                      with the specified string are returned.`
      }
    },
    resolve: (root, {QueueNamePrefix}) => {
      return sqs.listQueuesAsync(QueueNamePrefix)
              .then(result => result.QueueUrls);
    }
  }
};

exports.mutations = {
  sendMessage: {
    type: messageType,
    args: {
      QueueUrl: {
        description: 'The URL of the Amazon SQS queue to take action on.',
        type: new GraphQLNonNull(GraphQLString)
      },
      MessageBody: {
        description: 'The message to send. String maximum 256 KB in size.',
        type: new GraphQLNonNull(GraphQLString)
      }
    },
    resolve: (obj, {QueueUrl, MessageBody}) => {
      const params = {
        QueueUrl: QueueUrl,
        MessageBody: MessageBody
      };
      return sqs.sendMessageAsync(params);
    }
  },
  receiveMessage: {
    type: new GraphQLList(messageType),
    args: {
      QueueUrl: {
        description: 'The URL of the Amazon SQS queue to take action on.',
        type: new GraphQLNonNull(GraphQLString)
      }
    },
    resolve: (obj, {QueueUrl}) => {
      const params = {
        QueueUrl: QueueUrl
      };
      return sqs.receiveMessageAsync(params)
              .then(result => result.Messages);
    }
  },
  deleteQueue: {
    type: new GraphQLNonNull(GraphQLString),
    args: {
      QueueUrl: {
        description: 'The URL of the Amazon SQS queue to take action on.',
        type: new GraphQLNonNull(GraphQLString)
      }
    },
    resolve: (obj, {QueueUrl}) => {
      const params = {
        QueueUrl: QueueUrl
      };
      return sqs.deleteQueueAsync(params);
    }
  },
  createQueue: {
    type: type,
    description: 'Creates a new queue, or returns the URL of an existing one.',
    args: {
      QueueName: {
        name: 'QueueName',
        type: new GraphQLNonNull(GraphQLString),
        description: 'The name for the queue to be created.'
      },
      Attributes: {
        name: 'Attributes',
        type: attributesInputType,
        description: 'An attributes object with corresponding values.'
      }
    },
    resolve: (obj, {QueueName, Attributes}) => {
      const params = {
        QueueName: QueueName
      };
      if (Attributes) {
        merge(params, { Attributes: helpers.removeUndefinedKeys(Attributes) });
      }
      return sqs.createQueueAsync(params)
        .then((result) => { return { QueueUrl: result.QueueUrl }; });
    }
  }
};
<|endoftext|>