<commit_before>'use strict';

import 'isomorphic-fetch';
import { match } from 'tcomb';
import UriTemplate from 'uri-templates';
import nextTick from 'next-tick';

import createDispatcher from './lib/create-dispatcher';
import {
  ResourceType,
  NameOrId,
  NameOrIdList,
  ResourceList,
  OptionsObject,
  Callback,
} from './lib/types';

/**
 * Default configurations for each instance
 */
const DEFAULT_CONFIG = {
  /** The base URL to fetch from */
  baseUrl: 'http://pokeapi.co/api/v2',

  /** The default resource type to use with `getOneById` */
  defaultType: 'pokemon',

  /** Options that get passed to the fetch Request object */
  requests: {
    redirect: 'follow',
  },
};

export default class Pokewrap {
  constructor(opts = {}) {
    const requests = { ...DEFAULT_CONFIG.requests, ...opts.requests };
    const config = {
      ...DEFAULT_CONFIG,
      ...opts,
      requests,
    };
    Object.assign(this, config);
  }

  /**
   * Fetches a single resource based on its resource type and its name or ID
   *
   * @param {ResourceType} type - The resource type
   * @param {NameOrId} id - The name or ID number of the resource
   * @param {OptionsObject} [opts] - Additional options
   * @param {Function} [callback] - A callback for when the object is found
   *
   * @returns {Promise<Object>} The resource object, if found
   *//**
   * @param {ResourceObject} resource
   * @param {OptionsObject} [options]
   * @param {Function} [callback]
   *
   */
  getOne(...args) {
    const patterns = [
      {
        // getOne('pokemon', 1);
        // getOne('pokemon', 'bulbasaur');
        // getOne('pokemon', 'bulbasaur');
        pattern: [ResourceType, NameOrId],
        maybeOpts: true,
        maybeCallback: true,
        onMatch: () => {
          const [type, id, opts, callback] = args;

          return (OptionsObject.is(opts))
            ? this.getOne({ ...opts, type, id }, callback)
            : this.getOne({ type, id }, opts);
        },
      },
      {
        // getOne({ type: 'move', name: 'solar-beam' });
        // getOne({ id: 1 });
        // getOne({ name: 'bulbasaur' });
        pattern: [OptionsObject],
        maybeCallback: true,
        onMatch: () => {
          const [opts, callback] = args;
          const url = createUrl({
            type: this.defaultType,
            baseUrl: this.baseUrl,
            ...opts,
          });

          return this.getByUrl(url, opts.request, callback);
        },
      },
    ];

    const dispatcher = createDispatcher(patterns);
    return match(args, ...dispatcher);
  }

  /**
   * Fetches a single resource based on its name or ID
   *
   * @param {NameOrId} id - The name or ID number of the resource
   * @param {OptionsObject} [opts] - Additional options
   * @param {Function} [callback] - A callback for when the object is found
   *
   * @returns {Promise<Object>} The resource object, if found
   */
  getOneById(id, opts = {}, callback) {
    if (Callback.is(opts)) {
      return this.getOneById(id, undefined, opts);
    }

    const { type = this.defaultType } = opts;
    const resource = createUrl({ baseUrl: this.baseUrl, type, id });

    return this.getByUrl(resource, opts.request, callback);
  }

  /**
   * Alias for getOneById
   *
   * This is included for people who prefer to write `ID` rather than `Id`
   *
   * @see getOneById
   */
  getOneByID(...args) {
    return this.getOneById(...args);
  }

  /**
   * Alias for getOneById
   *
   * This is included because it can be a bit weird to write
   * `getOneById('bulbasaur')`, when a Pokeapi ID usually refers to an integer.
   *
   * @see getOneById
   */
  getOneByName(...args) {
    return this.getOneById(...args);
  }

  /**
   * Fetches information about multiple resources, either from a ResourceList
   * endpoint or as a series of get requests.
   *
   * @param {ResourceType} type
   * @param {OptionsObject} [opts] - Additional options
   * @param {Function} [callback] - A callback for when the object is found
   *
   * @returns {Promise<Object>}
   *//**
   * @param {ResourceType} type
   * @param {NameOrIdList} resources
   * @param {OptionsObject} [opts] - Additional options that will be merged
   *                                 with each item being fetched
   * @param {Function} [callback] - A callback for when the object is found
   *
   * @returns {Promise<Object>}
   *//**
   * @param {ResourceList} resources
   * @param {OptionsObject} [opts] - Additional options that will be merged
   *                                 with each item being fetched
   * @param {Function} [callback] - A callback for when the object is found
   *
   * @returns {Promise<Object>}
   */
  getMany(...args) {
    const patterns = [
      {
        // getMany('item');
        pattern: [ResourceType],
        maybeOpts: true,
        maybeCallback: true,
        onMatch: () => {
          const [type, opts = {}, callback] = args;

          if (Callback.is(opts)) {
            return this.getMany(type, undefined, opts);
          }

          const { limit, offset, request } = opts;

          const url = createUrl({ baseUrl: this.baseUrl, type, limit, offset });
          return this.getByUrl(url, request, callback);
        },
      },
      {
        // getMany('pokemon', ['bulbasaur', 'charmander', 'squirtle']);
        // getMany('item', [1, 2, 3]);
        pattern: [ResourceType, NameOrIdList],
        maybeOpts: true,
        maybeCallback: true,
        onMatch: () => {
          const [type, ids, opts = {}, callback] = args;

          if (Callback.is(opts)) {
            return this.getMany(type, ids, undefined, opts);
          }

          const requests = ids.map((id) => this.getOne(type, id, opts));
          const batchedRequests = Promise.all(requests);
          return maybeCallback(batchedRequests, callback);
        },
      },
      {
        // getMany(['bulbasaur', 'charmander', 'squirtle']);
        // getMany([1, 2, 3]);
        pattern: [ResourceList],
        maybeOpts: true,
        maybeCallback: true,
        onMatch: () => {
          const [resources, opts = {}, callback] = args;

          if (Callback.is(opts)) {
            return this.getMany(resources, undefined, opts);
          }

          const requests = resources.map(
            (resource) => (NameOrId.is(resource))
              ? this.getOneById(resource, opts)
              : this.getOne({ ...opts, ...resource })
          );

          const batchedRequests = Promise.all(requests);
          return maybeCallback(batchedRequests, callback);
        },
      },
    ];

    const dispatcher = createDispatcher(patterns);
    return match(args, ...dispatcher);
  }

  /**
   * Fetches the resource from the provided URL.
   *
   * @param {String} url - The URL to fetch
   * @param {Object} [opts] - Additional options that will be passed to
   *                                 the request object
   * @param {Function} [callback] - A callback for when the object is found
   */
  getByUrl(url, opts = {}, callback) {
    if (Callback.is(opts)) {
      return this.getByUrl(url, undefined, opts);
    }

    const requestOpts = {
      ...this.requests,
      ...opts,

      // the Pokeapi only supports GET requests
      method: 'GET',
    };

    const request = fetch(url, requestOpts).then(getJSON);
    return maybeCallback(request, callback);
  }

  /**
   * Alias for getByURL
   *
   * This is included for people who prefer to write `URL` rather than `Url`
   *
   * @see getByUrl
   */
  getByURL(...args) {
    return this.getByUrl(args);
  }

  /** @private */
  getEmAll() {
    return this.getMany('pokemon', { limit: Number.MAX_SAFE_INTEGER });
  }
}

/**
 * Creates a url based on parameters
 * @private
 */
function createUrl({ baseUrl, type, id, name, limit, offset }) {
  const base = stripTrailingSlash(baseUrl);
  const identifier = id || name;

  const template = new UriTemplate(`${base}{/type}{/identifier}/{?limit,offset}`);

  return template.fillFromObject({ type, identifier, limit, offset });
}

/**
 * Removes a trailing slash from a URL
 * @private
 *
 * @param {String} url
 * @returns {String}
 */
function stripTrailingSlash(url) {
  return url.replace(/\/+$/, '');
}

/**
 * Extracts the JSON body from a fetched response object
 * @private
 *
 * @param {Response} response
 * @returns {Object}
 */
function getJSON(response) {
  if (response.status >= 400) {
    const err = new Error(response.statusText);
    return Promise.reject(err);
  }

  return response.json();
}

/**
 * Calls a callback, if one is provided,  after a Promise resolves
 *
 * @param {Promise} promise
 * @param {Function} callback
 *
 * @returns {Promise}
 */
export default function maybeCallback(promise, callback) {
  if (callback) {
    promise
      .then((data) => { nextTick(() => callback(null, data)); })
      .catch((err) => { nextTick(() => callback(err)); });
  }

  return promise;
}
<commit_msg>Change how options are passed to createUrl<commit_after>'use strict';

import 'isomorphic-fetch';
import { match } from 'tcomb';
import UriTemplate from 'uri-templates';
import nextTick from 'next-tick';

import createDispatcher from './lib/create-dispatcher';
import {
  ResourceType,
  NameOrId,
  NameOrIdList,
  ResourceList,
  OptionsObject,
  Callback,
} from './lib/types';

/**
 * Default configurations for each instance
 */
const DEFAULT_CONFIG = {
  /** The base URL to fetch from */
  baseUrl: 'http://pokeapi.co/api/v2',

  /** The default resource type to use with `getOneById` */
  defaultType: 'pokemon',

  /** Options that get passed to the fetch Request object */
  requests: {
    redirect: 'follow',
  },
};

export default class Pokewrap {
  constructor(opts = {}) {
    const requests = { ...DEFAULT_CONFIG.requests, ...opts.requests };
    const config = {
      ...DEFAULT_CONFIG,
      ...opts,
      requests,
    };

    Object.assign(this, config);
  }

  /**
   * Fetches a single resource based on its resource type and its name or ID
   *
   * @param {ResourceType} type - The resource type
   * @param {NameOrId} id - The name or ID number of the resource
   * @param {OptionsObject} [opts] - Additional options
   * @param {Function} [callback] - A callback for when the object is found
   *
   * @returns {Promise<Object>} The resource object, if found
   *//**
   * @param {ResourceObject} resource
   * @param {OptionsObject} [options]
   * @param {Function} [callback]
   *
   */
  getOne(...args) {
    const patterns = [
      {
        // getOne('pokemon', 1);
        // getOne('pokemon', 'bulbasaur');
        // getOne('pokemon', 'bulbasaur');
        pattern: [ResourceType, NameOrId],
        maybeOpts: true,
        maybeCallback: true,
        onMatch: () => {
          const [type, id, opts, callback] = args;

          return (OptionsObject.is(opts))
            ? this.getOne({ ...opts, type, id }, callback)
            : this.getOne({ type, id }, opts);
        },
      },
      {
        // getOne({ type: 'move', name: 'solar-beam' });
        // getOne({ id: 1 });
        // getOne({ name: 'bulbasaur' });
        pattern: [OptionsObject],
        maybeCallback: true,
        onMatch: () => {
          const [opts, callback] = args;
          const url = createUrl({
            type: this.defaultType,
            baseUrl: this.baseUrl,
            ...opts,
          });

          return this.getByUrl(url, opts, callback);
        },
      },
    ];

    const dispatcher = createDispatcher(patterns);
    return match(args, ...dispatcher);
  }

  /**
   * Fetches a single resource based on its name or ID
   *
   * @param {NameOrId} id - The name or ID number of the resource
   * @param {OptionsObject} [opts] - Additional options
   * @param {Function} [callback] - A callback for when the object is found
   *
   * @returns {Promise<Object>} The resource object, if found
   */
  getOneById(id, opts = {}, callback) {
    if (Callback.is(opts)) {
      return this.getOneById(id, undefined, opts);
    }

    const { type = this.defaultType } = opts;
    const resource = createUrl({ baseUrl: this.baseUrl, type, id });

    return this.getByUrl(resource, opts, callback);
  }

  /**
   * Alias for getOneById
   *
   * This is included for people who prefer to write `ID` rather than `Id`
   *
   * @see getOneById
   */
  getOneByID(...args) {
    return this.getOneById(...args);
  }

  /**
   * Alias for getOneById
   *
   * This is included because it can be a bit weird to write
   * `getOneById('bulbasaur')`, when a Pokeapi ID usually refers to an integer.
   *
   * @see getOneById
   */
  getOneByName(...args) {
    return this.getOneById(...args);
  }

  /**
   * Fetches information about multiple resources, either from a ResourceList
   * endpoint or as a series of get requests.
   *
   * @param {ResourceType} type
   * @param {OptionsObject} [opts] - Additional options
   * @param {Function} [callback] - A callback for when the object is found
   *
   * @returns {Promise<Object>}
   *//**
   * @param {ResourceType} type
   * @param {NameOrIdList} resources
   * @param {OptionsObject} [opts] - Additional options that will be merged
   *                                 with each item being fetched
   * @param {Function} [callback] - A callback for when the object is found
   *
   * @returns {Promise<Object>}
   *//**
   * @param {ResourceList} resources
   * @param {OptionsObject} [opts] - Additional options that will be merged
   *                                 with each item being fetched
   * @param {Function} [callback] - A callback for when the object is found
   *
   * @returns {Promise<Object>}
   */
  getMany(...args) {
    const patterns = [
      {
        // getMany('item');
        pattern: [ResourceType],
        maybeOpts: true,
        maybeCallback: true,
        onMatch: () => {
          const [type, opts = {}, callback] = args;

          if (Callback.is(opts)) {
            return this.getMany(type, undefined, opts);
          }

          const { limit, offset } = opts;

          const url = createUrl({ baseUrl: this.baseUrl, type, limit, offset });
          return this.getByUrl(url, opts, callback);
        },
      },
      {
        // getMany('pokemon', ['bulbasaur', 'charmander', 'squirtle']);
        // getMany('item', [1, 2, 3]);
        pattern: [ResourceType, NameOrIdList],
        maybeOpts: true,
        maybeCallback: true,
        onMatch: () => {
          const [type, ids, opts = {}, callback] = args;

          if (Callback.is(opts)) {
            return this.getMany(type, ids, undefined, opts);
          }

          const requests = ids.map((id) => this.getOne(type, id, opts));
          const batchedRequests = Promise.all(requests);
          return maybeCallback(batchedRequests, callback);
        },
      },
      {
        // getMany(['bulbasaur', 'charmander', 'squirtle']);
        // getMany([1, 2, 3]);
        pattern: [ResourceList],
        maybeOpts: true,
        maybeCallback: true,
        onMatch: () => {
          const [resources, opts = {}, callback] = args;

          if (Callback.is(opts)) {
            return this.getMany(resources, undefined, opts);
          }

          const requests = resources.map(
            (resource) => (NameOrId.is(resource))
              ? this.getOneById(resource, opts)
              : this.getOne({ ...opts, ...resource })
          );

          const batchedRequests = Promise.all(requests);
          return maybeCallback(batchedRequests, callback);
        },
      },
    ];

    const dispatcher = createDispatcher(patterns);
    return match(args, ...dispatcher);
  }

  /**
   * Fetches the resource from the provided URL.
   *
   * @param {String} url - The URL to fetch
   * @param {Object} [opts] - Additional options that will be passed to
   *                                 the request object
   * @param {Function} [callback] - A callback for when the object is found
   */
  getByUrl(url, opts = {}, callback) {
    if (Callback.is(opts)) {
      return this.getByUrl(url, undefined, opts);
    }

    const requestOpts = {
      ...this.requests,
      ...opts,

      // the Pokeapi only supports GET requests, which cannot have a body
      method: 'GET',
      body: undefined,
    };

    const request = fetch(url, requestOpts).then(getJSON);
    return maybeCallback(request, callback);
  }

  /**
   * Alias for getByURL
   *
   * This is included for people who prefer to write `URL` rather than `Url`
   *
   * @see getByUrl
   */
  getByURL(...args) {
    return this.getByUrl(args);
  }

  /** @private */
  getEmAll() {
    return this.getMany('pokemon', { limit: Number.MAX_SAFE_INTEGER });
  }
}

/**
 * Creates a url based on parameters
 * @private
 */
function createUrl({ baseUrl, type, id, name, limit, offset }) {
  const base = stripTrailingSlash(baseUrl);
  const identifier = id || name;

  const template = new UriTemplate(`${base}{/type}{/identifier}/{?limit,offset}`);

  return template.fillFromObject({ type, identifier, limit, offset });
}

/**
 * Removes a trailing slash from a URL
 * @private
 *
 * @param {String} url
 * @returns {String}
 */
function stripTrailingSlash(url) {
  return url.replace(/\/+$/, '');
}

/**
 * Extracts the JSON body from a fetched response object
 * @private
 *
 * @param {Response} response
 * @returns {Object}
 */
function getJSON(response) {
  if (response.status >= 400) {
    const err = new Error(response.statusText);
    return Promise.reject(err);
  }

  return response.json();
}

/**
 * Calls a callback, if one is provided,  after a Promise resolves
 *
 * @param {Promise} promise
 * @param {Function} callback
 *
 * @returns {Promise}
 */
export default function maybeCallback(promise, callback) {
  if (callback) {
    promise
      .then((data) => { nextTick(() => callback(null, data)); })
      .catch((err) => { nextTick(() => callback(err)); });
  }

  return promise;
}
<|endoftext|>


<commit_before>'use strict';
const Alexa = require('alexa-sdk');
const https = require('https');
const moment = require('moment');

const languageStrings = require('./languageStrings.js');
const canteens = require('./canteens.js');

exports.handler = function (event, context, callback) {
    const alexa = Alexa.handler(event, context);
    alexa.resources = languageStrings;
    alexa.registerHandlers(handlers);
    alexa.execute();
};

const handlers = {
    'LaunchRequest': function () {
        this.emit('AMAZON.HelpIntent');
    },

    'Unhandled': function () {
        this.emit('AMAZON.HelpIntent');
    },

    'GetMealsForMensa': function () {
        const canteenSlot = this.event.request.intent.slots.Canteen;
        if (canteenSlot == undefined || canteenSlot.value == undefined) {
            this.emit('AMAZON.HelpIntent');
            return false;
        }

        const date = getDate(this.event.request.intent.slots.Date);
        const canteen = getCanteen(this, canteenSlot);
        const requestParams = {
            'mensaId': canteen.id,
            'date': date.date
        };
        console.log('requestParams', requestParams);
        queryMeals(requestParams, (success, myResult) => {
            if (!success) {
                this.emit(':tell', 'Am ' + date.dateName + ' scheint es kein Essen zu geben. Sorry, du musst hungern.');
                return false;
            }
            const meals = getFormattedMeals(myResult);
            const outputSpeech = meals.slice(0, meals.length - 1).join(', <break time="600ms" /> ') + ' <break time="500ms" />' + ' und zuletzt auch noch ' + meals[meals.length - 1];
            console.log(outputSpeech);
            this.emit(':tell', 'In der ' + canteen.name + ' gibt es ' + date.dateName + ' ' + outputSpeech);
        });
    },

    'AMAZON.HelpIntent': function () {
        const speechOutput = this.t('HELP_MESSAGE');
        const reprompt = this.t('HELP_MESSAGE');
        this.emit(':ask', speechOutput, reprompt);
    },
    'AMAZON.CancelIntent': function () {
        this.emit(':tell', this.t('STOP_MESSAGE'));
    },
    'AMAZON.StopIntent': function () {
        this.emit(':tell', this.t('STOP_MESSAGE'));
    },
    'SessionEndedRequest': function () {
        this.emit(':tell', this.t('STOP_MESSAGE'));
    },
};

function getCanteen(that, canteenSlot) {
    const canteenQuery = canteenSlot.value.toLowerCase();
    console.log('canteenQuery', canteenQuery);
    const resultCanteens = canteens.filter(canteen => canteen.names.indexOf(canteenQuery) !== -1);
    if (resultCanteens.length === 0) {
        that.emit('AMAZON.HelpIntent');
        return false;
    } else {
        return {
            'name': resultCanteens[0].names[0],
            'id': resultCanteens[0].id
        };
    }
}

function getDate(dateSlot) {
    const today = moment().format('YYYY-MM-DD');
    const date = dateSlot != undefined && dateSlot.value != undefined ? dateSlot.value : today;
    const dateName = dateSlot != undefined && dateSlot.value != undefined && dateSlot.value != today ? moment(date).locale('de').format('dddd') : 'Heute';
    console.log('Date:', date, 'DateName:', dateName);
    return {
        'dateName': dateName,
        'date': date
    };
}

function getFormattedMeals(jsonResponse) {
    /* Returns an array of formatted meals like ">Schinkennudeln (Vorderschinken) mit Zwiebeln und Ei für 1.9€" */
    const categoryFilter = ['Tagesgericht 1', 'Tagesgericht 2', 'Tagesgericht 3', 'Tagesgericht 4', 'Aktionsessen 1', 'Aktionsessen 2', 'Aktionsessen 3', 'Aktionsessen 4', 'Biogericht 1', 'Biogericht 2', 'Biogericht 3', 'Biogericht 4'];
    return jsonResponse
        .filter(meal => categoryFilter.indexOf(meal.category) !== -1)
        .map(meal => meal.prices.students != null ? meal.name + ' für ' + meal.prices.students + '€' : meal.name);
}

function queryMeals(myData, callback) {
    const options = {
        host: 'openmensa.org',
        port: 443,
        path: '/api/v2/canteens/' + myData.mensaId + '/days/' + myData.date + '/meals',
        method: 'GET'
    };

    const req = https.request(options, res => {
        res.setEncoding('utf8');
        let returnData = '';

        res.on('data', chunk => {
            returnData = returnData + chunk;
        });

        res.on('end', () => {
            if (returnData == '') {
                callback(false, returnData)
            } else {
                callback(true, JSON.parse(returnData));
            }
        });

        res.on('error', e => {
            console.log('ERROR', e);
        });

    });
    req.end();
}
<commit_msg>Use a blacklist filter for the categories instead of whitelist Related #1<commit_after>'use strict';
const Alexa = require('alexa-sdk');
const https = require('https');
const moment = require('moment');

const languageStrings = require('./languageStrings.js');
const canteens = require('./canteens.js');

exports.handler = function (event, context, callback) {
    const alexa = Alexa.handler(event, context);
    alexa.resources = languageStrings;
    alexa.registerHandlers(handlers);
    alexa.execute();
};

const handlers = {
    'LaunchRequest': function () {
        this.emit('AMAZON.HelpIntent');
    },

    'Unhandled': function () {
        this.emit('AMAZON.HelpIntent');
    },

    'GetMealsForMensa': function () {
        const canteenSlot = this.event.request.intent.slots.Canteen;
        if (canteenSlot == undefined || canteenSlot.value == undefined) {
            this.emit('AMAZON.HelpIntent');
            return false;
        }

        const date = getDate(this.event.request.intent.slots.Date);
        const canteen = getCanteen(this, canteenSlot);
        const requestParams = {
            'mensaId': canteen.id,
            'date': date.date
        };
        console.log('requestParams', requestParams);
        queryMeals(requestParams, (success, myResult) => {
            if (!success) {
                this.emit(':tell', 'Am ' + date.dateName + ' scheint es kein Essen zu geben. Sorry, du musst hungern.');
                return false;
            }
            const meals = getFormattedMeals(myResult);
            const outputSpeech = meals.slice(0, meals.length - 1).join(', <break time="600ms" /> ') + ' <break time="500ms" />' + ' und zuletzt auch noch ' + meals[meals.length - 1];
            console.log(outputSpeech);
            this.emit(':tell', 'In der ' + canteen.name + ' gibt es ' + date.dateName + ' ' + outputSpeech);
        });
    },

    'AMAZON.HelpIntent': function () {
        const speechOutput = this.t('HELP_MESSAGE');
        const reprompt = this.t('HELP_MESSAGE');
        this.emit(':ask', speechOutput, reprompt);
    },
    'AMAZON.CancelIntent': function () {
        this.emit(':tell', this.t('STOP_MESSAGE'));
    },
    'AMAZON.StopIntent': function () {
        this.emit(':tell', this.t('STOP_MESSAGE'));
    },
    'SessionEndedRequest': function () {
        this.emit(':tell', this.t('STOP_MESSAGE'));
    },
};

function getCanteen(that, canteenSlot) {
    const canteenQuery = canteenSlot.value.toLowerCase();
    console.log('canteenQuery', canteenQuery);
    const resultCanteens = canteens.filter(canteen => canteen.names.indexOf(canteenQuery) !== -1);
    if (resultCanteens.length === 0) {
        that.emit('AMAZON.HelpIntent');
        return false;
    } else {
        return {
            'name': resultCanteens[0].names[0],
            'id': resultCanteens[0].id
        };
    }
}

function getDate(dateSlot) {
    const today = moment().format('YYYY-MM-DD');
    const date = dateSlot != undefined && dateSlot.value != undefined ? dateSlot.value : today;
    const dateName = dateSlot != undefined && dateSlot.value != undefined && dateSlot.value != today ? moment(date).locale('de').format('dddd') : 'Heute';
    console.log('Date:', date, 'DateName:', dateName);
    return {
        'dateName': dateName,
        'date': date
    };
}

function getFormattedMeals(jsonResponse) {
    /* Returns an array of formatted meals like ">Schinkennudeln (Vorderschinken) mit Zwiebeln und Ei für 1.9€" */
    // const categoryFilter = ['Tagesgericht 1', 'Tagesgericht 2', 'Tagesgericht 3', 'Tagesgericht 4', 'Aktionsessen 1', 'Aktionsessen 2', 'Aktionsessen 3', 'Aktionsessen 4', 'Biogericht 1', 'Biogericht 2', 'Biogericht 3', 'Biogericht 4'];
    const categoriesToIgnore = ['Beilagen', 'Aktion'];
    return jsonResponse
        .filter(meal => categoriesToIgnore.indexOf(meal.category) === -1)
        .map(meal => meal.prices.students != null ? meal.name + ' für ' + meal.prices.students + '€' : meal.name);
}

function queryMeals(myData, callback) {
    const options = {
        host: 'openmensa.org',
        port: 443,
        path: '/api/v2/canteens/' + myData.mensaId + '/days/' + myData.date + '/meals',
        method: 'GET'
    };

    const req = https.request(options, res => {
        res.setEncoding('utf8');
        let returnData = '';

        res.on('data', chunk => {
            returnData = returnData + chunk;
        });

        res.on('end', () => {
            if (returnData == '') {
                callback(false, returnData)
            } else {
                callback(true, JSON.parse(returnData));
            }
        });

        res.on('error', e => {
            console.log('ERROR', e);
        });

    });
    req.end();
}
<|endoftext|>


<commit_before>'use strict';

var angular = require('angular');

module.exports = angular.module('pacApp', [])

.directive('pacBookDetails', require('./BookDetails.3.directive'))
.directive('pacBookOverview', require('./BookOverview.directive'))

.factory('httpBook', require('./HttpBook.3.factory'))

// warmup
//.run(function(httpBook) { httpBook.get(); })

// simulate illegal op
//.run(function(httpBook) { httpBook.get(function(book) { book.countPages(); }); })

.name;
<commit_msg>step4<commit_after>'use strict';

var angular = require('angular');

module.exports = angular.module('pacApp', [])

.directive('pacBookDetails', require('./BookDetails.3.directive'))
.directive('pacBookOverview', require('./BookOverview.directive'))

.factory('httpBook', require('./HttpBook.4.factory'))

// warmup
//.run(function(httpBook) { httpBook.get(); })

// simulate illegal op
//.run(function(httpBook) { httpBook.get(function(book) { book.countPages(); }); })

.name;
<|endoftext|>


<commit_before>
// This will be needed for the browser version
// Check .babelrc for asyncToGenerator for browser
// Babel-core will need whacking back into deps
//import 'babel/polyfill'

import path from 'path'
import { Writable } from 'stream'

import { config } from 'xdg-basedir'
import EventEmitter from 'eventemitter3'
import request from 'superagent'
import co from 'co'

import pkg from '../package.json'
import CONSTANTS from './constants'
import fileUtils from './file'

export default class Client extends EventEmitter {
    constructor( options ) {
        super()

        let opts = options || {}

        this._connected = false

        this.connect = opts.connectURL || CONSTANTS.CONNECT_URL
        this.token = null
        this.configPath = path.join( config, pkg.name )
        this.tokenFile = path.join( this.configPath, 'token' )

        if ( !this.connect ) {
            throw new Error( 'Connect URL invalid' )
        }

        // Perform init
        // @TODO add pluggable storage mechanism
        co( this._init() )
            .then( () => {
                this.emit( 'ready' )
                return
            })
            .catch( err => {
                console.error( 'Initialisation error' )
                console.error( err )
            })
    }

    _checkConnection() {
        if ( !this.token ) {
            throw new Error( 'Connection lost, was the connection ready?' )
        }
    }

    /**
     * Init generator
     * Sets up the persistence layer the client needs
     */
    *_init() {
        // Ensure config directory exists
        try {
            yield fileUtils.mkdirp( this.configPath )
        } catch( err ) {
            console.error( 'make config directory error' )
            throw new Error( err )
        }

        // Try to grab the token, create if necessary
        try {
            let res = yield fileUtils.readFile( this.tokenFile )
            this.token = res.toString()
        } catch( err ) {
            // If token file does not exist then request a fresh token
            if ( err.code === 'ENOENT' ) {
                yield this._requestToken()
                return
            }

            throw new Error( err )
        }
    }

    /**
     * Request token generator
     * Grabs a fresh token from the server and persists it
     */
    *_requestToken() {
        var res = null

        try {
            res = yield this._request({
                method: 'POST',
                url: CONSTANTS.TOKEN_REQUEST_URL
            })

            console.log( 'Token request success' )

            try {
                yield fileUtils.writeFile( this.tokenFile, res.body.id )
                this.token = res.body.id

                // Return the token and let the executor choose what to do with it
                return res.body.id
            } catch( err ) {
                console.error( 'Error persisting token' )
                throw new Error( err )
            }
        } catch( err ) {
            console.error( 'Error requesting token' )
            throw new Error( err )
        }

    }

    /**
     * Simple promisified request
     * Resolves with the entire response
     * @param opts <Object>
     *   method <String> http method to use
     *   url <String> url to hit
     *   headers <Array:Object> headers to append
     */
    _request( opts ) {
        return new Promise( ( resolve, reject ) => {
            let req = request( opts.method, CONSTANTS.CONNECT_PROTOCOL + path.join( this.connect, opts.url ) )

            // Add custom header
            req.set( CONSTANTS.TOKEN_HEADER, this.token || 'new' )

            // Add any other headers from the options map
            if ( opts.headers ) {
                Object.keys( opts.headers ).forEach( header => {
                    req.set( header, opts.headers[ header ] )
                })
            }

            if ( opts.data ) {
                req.send( opts.data )
            }

            // Push out the request
            req.end( ( err, res ) => {
                if ( err ) {
                    reject( err )
                    return
                }

                resolve( res )
            })
        })
    }

    /*-----------------------------------------------------------*
     *
     *  Server access methods
     *
     *-----------------------------------------------------------*/

    /**
     * Returns a single value from a group
     */
    get( group, key, noRefresh ) {
        this._checkConnection()

        if ( !group || !key ) {
            throw new Error( 'GET requires a group and key' )
        }

        return new Promise( ( resolve, reject ) => {
            this._request({
                method: 'GET',
                url: path.join( group, key )
            })
                .then( res => resolve( res.body ) )
                .catch( err => {
                    // If we get a forbidden then a token refresh will probably solve it
                    if ( err.status === 403 ) {
                        // Bail if refreshing the token still fails
                        if ( noRefresh ) {
                            reject({
                                status: 403,
                                body: 'Authentication can not be established'
                            })
                            return
                        }

                        // Attempt a token refresh
                        co( this._requestToken() )
                            .then( () => {
                                this.get( group, key, true )
                                    .then( resolve )
                                    .catch( reject )
                            })
                            .catch( err => reject({
                                status: 403,
                                body: 'Authentication can not be established',
                                err: err
                            }))

                        return
                    }

                    // Any other sort of error and just punt it out
                    reject( err )
                })
        })
    }

    /**
     * Deletes a single value from a group
     */
    delete( group, key, noRefresh ) {
        this._checkConnection()

        if ( !group || !key ) {
            throw new Error( 'DELETE requires a group and key' )
        }

        return new Promise( ( resolve, reject ) => {
            this._request({
                method: 'DELETE',
                url: path.join( group, key )
            })
                .then( res => resolve( res.body ) )
                .catch( err => {
                    // If we get a forbidden then a token refresh will probably solve it
                    if ( err.status === 403 ) {
                        // Bail if refreshing the token still fails
                        if ( noRefresh ) {
                            reject({
                                status: 403,
                                body: 'Authentication can not be established'
                            })
                            return
                        }

                        // Attempt a token refresh
                        co( this._requestToken() )
                            .then( () => {
                                this.delete( group, key, true )
                                    .then( resolve )
                                    .catch( reject )
                            })
                            .catch( err => reject({
                                status: 403,
                                body: 'Authentication can not be established',
                                err: err
                            }))

                        return
                    }

                    // Any other sort of error and just punt it out
                    reject( err )
                })
        })
    }

    /**
     * Puts a single value into a group
     */
    put( group, key, data, noRefresh ) {
        this._checkConnection()

        if ( !group || !key ) {
            throw new Error( 'PUT requires a group and key' )
        }

        if ( !data ) {
            console.warn( 'PUT no data, is this intentional?' )
        }

        return new Promise( ( resolve, reject ) => {
            this._request({
                method: 'POST',
                url: path.join( group, key ),
                headers: {
                    'Content-Type': 'application/json'
                },
                data: data
            })
                .then( res => resolve( res.body ) )
                .catch( err => {
                    // If we get a forbidden then a token refresh will probably solve it
                    if ( err.status === 403 ) {
                        // Bail if refreshing the token still fails
                        if ( noRefresh ) {
                            reject({
                                status: 403,
                                body: 'Authentication can not be established'
                            })
                            return
                        }

                        // Attempt a token refresh
                        co( this._requestToken() )
                            .then( () => {
                                this.put( group, key, data, true )
                                    .then( resolve )
                                    .catch( reject )
                            })
                            .catch( err => reject({
                                status: 403,
                                body: 'Authentication can not be established',
                                err: err
                            }))

                        return
                    }

                    // Any other sort of error and just punt it out
                    reject( err )
                })
        })
    }

    /**
     * Performs a batch into a group
     */
    batch( group, ops, noRefresh ) {
        this._checkConnection()

        if ( !group || !ops ) {
            throw new Error( 'BATCH requires a group and some data' )
        }

        if ( !ops || !ops.length ) {
            throw new Error( 'BATCH requires an array of data' )
        }

        ops.forEach( op => {
            if ( !op.type ) {
                throw new Error( 'BATCH invalid operations' )
            }
        })

        return new Promise( ( resolve, reject ) => {
            this._request({
                method: 'POST',
                url: path.join( group ),
                headers: {
                    'Content-Type': 'application/json'
                },
                data: ops
            })
                .then( res => resolve( res.body ) )
                .catch( err => {
                    // If we get a forbidden then a token refresh will probably solve it
                    if ( err.status === 403 ) {
                        // Bail if refreshing the token still fails
                        if ( noRefresh ) {
                            reject({
                                status: 403,
                                body: 'Authentication can not be established'
                            })
                            return
                        }

                        // Attempt a token refresh
                        co( this._requestToken() )
                            .then( () => {
                                this.put( group, ops, true )
                                    .then( resolve )
                                    .catch( reject )
                            })
                            .catch( err => reject({
                                status: 403,
                                body: 'Authentication can not be established',
                                err: err
                            }))

                        return
                    }

                    // Any other sort of error and just punt it out
                    reject( err )
                })
        })
    }

    /**
     * Returns a stream of values from a group
     */
    read( group, noRefresh ) {
        this._checkConnection()

        if ( !group ) {
            throw new Error( 'READ requires a group to stream from' )
        }

        var stream = null

        // Stream->emitter
        const emitter = new Writable({
            write: function( chunk, enc, next ) {
                if ( stream.res.statusCode !== 200 ) {
                    this.emit( 'error', stream.res.statusCode )
                    return
                }
                this.emit( 'data', chunk.toString() )
                next()
            }
        })

        stream = request
            .get( CONSTANTS.CONNECT_PROTOCOL + path.join( this.connect, group ) )
            .set( CONSTANTS.TOKEN_HEADER, this.token )
            .type( 'json' )
            .on( 'error', err => {
                console.error( 'Error creating read stream' )
                console.error( err )
            })

        stream.pipe( emitter )

        return emitter
    }

}
<commit_msg>#add read emits objects<commit_after>
// This will be needed for the browser version
// Check .babelrc for asyncToGenerator for browser
// Babel-core will need whacking back into deps
//import 'babel/polyfill'

import path from 'path'
import { Writable } from 'stream'

import { config } from 'xdg-basedir'
import EventEmitter from 'eventemitter3'
import request from 'superagent'
import co from 'co'

import pkg from '../package.json'
import CONSTANTS from './constants'
import fileUtils from './file'

export default class Client extends EventEmitter {
    constructor( options ) {
        super()

        let opts = options || {}

        this._connected = false

        this.connect = opts.connectURL || CONSTANTS.CONNECT_URL
        this.token = null
        this.configPath = path.join( config, pkg.name )
        this.tokenFile = path.join( this.configPath, 'token' )

        if ( !this.connect ) {
            throw new Error( 'Connect URL invalid' )
        }

        // Perform init
        // @TODO add pluggable storage mechanism
        co( this._init() )
            .then( () => {
                this.emit( 'ready' )
                return
            })
            .catch( err => {
                console.error( 'Initialisation error' )
                console.error( err )
            })
    }

    _checkConnection() {
        if ( !this.token ) {
            throw new Error( 'Connection lost, was the connection ready?' )
        }
    }

    /**
     * Init generator
     * Sets up the persistence layer the client needs
     */
    *_init() {
        // Ensure config directory exists
        try {
            yield fileUtils.mkdirp( this.configPath )
        } catch( err ) {
            console.error( 'make config directory error' )
            throw new Error( err )
        }

        // Try to grab the token, create if necessary
        try {
            let res = yield fileUtils.readFile( this.tokenFile )
            this.token = res.toString()
        } catch( err ) {
            // If token file does not exist then request a fresh token
            if ( err.code === 'ENOENT' ) {
                yield this._requestToken()
                return
            }

            throw new Error( err )
        }
    }

    /**
     * Request token generator
     * Grabs a fresh token from the server and persists it
     */
    *_requestToken() {
        var res = null

        try {
            res = yield this._request({
                method: 'POST',
                url: CONSTANTS.TOKEN_REQUEST_URL
            })

            console.log( 'Token request success' )

            try {
                yield fileUtils.writeFile( this.tokenFile, res.body.id )
                this.token = res.body.id

                // Return the token and let the executor choose what to do with it
                return res.body.id
            } catch( err ) {
                console.error( 'Error persisting token' )
                throw new Error( err )
            }
        } catch( err ) {
            console.error( 'Error requesting token' )
            throw new Error( err )
        }

    }

    /**
     * Simple promisified request
     * Resolves with the entire response
     * @param opts <Object>
     *   method <String> http method to use
     *   url <String> url to hit
     *   headers <Array:Object> headers to append
     */
    _request( opts ) {
        return new Promise( ( resolve, reject ) => {
            let req = request( opts.method, CONSTANTS.CONNECT_PROTOCOL + path.join( this.connect, opts.url ) )

            // Add custom header
            req.set( CONSTANTS.TOKEN_HEADER, this.token || 'new' )

            // Add any other headers from the options map
            if ( opts.headers ) {
                Object.keys( opts.headers ).forEach( header => {
                    req.set( header, opts.headers[ header ] )
                })
            }

            if ( opts.data ) {
                req.send( opts.data )
            }

            // Push out the request
            req.end( ( err, res ) => {
                if ( err ) {
                    reject( err )
                    return
                }

                resolve( res )
            })
        })
    }

    /*-----------------------------------------------------------*
     *
     *  Server access methods
     *
     *-----------------------------------------------------------*/

    /**
     * Returns a single value from a group
     */
    get( group, key, noRefresh ) {
        this._checkConnection()

        if ( !group || !key ) {
            throw new Error( 'GET requires a group and key' )
        }

        return new Promise( ( resolve, reject ) => {
            this._request({
                method: 'GET',
                url: path.join( group, key )
            })
                .then( res => resolve( res.body ) )
                .catch( err => {
                    // If we get a forbidden then a token refresh will probably solve it
                    if ( err.status === 403 ) {
                        // Bail if refreshing the token still fails
                        if ( noRefresh ) {
                            reject({
                                status: 403,
                                body: 'Authentication can not be established'
                            })
                            return
                        }

                        // Attempt a token refresh
                        co( this._requestToken() )
                            .then( () => {
                                this.get( group, key, true )
                                    .then( resolve )
                                    .catch( reject )
                            })
                            .catch( err => reject({
                                status: 403,
                                body: 'Authentication can not be established',
                                err: err
                            }))

                        return
                    }

                    // Any other sort of error and just punt it out
                    reject( err )
                })
        })
    }

    /**
     * Deletes a single value from a group
     */
    delete( group, key, noRefresh ) {
        this._checkConnection()

        if ( !group || !key ) {
            throw new Error( 'DELETE requires a group and key' )
        }

        return new Promise( ( resolve, reject ) => {
            this._request({
                method: 'DELETE',
                url: path.join( group, key )
            })
                .then( res => resolve( res.body ) )
                .catch( err => {
                    // If we get a forbidden then a token refresh will probably solve it
                    if ( err.status === 403 ) {
                        // Bail if refreshing the token still fails
                        if ( noRefresh ) {
                            reject({
                                status: 403,
                                body: 'Authentication can not be established'
                            })
                            return
                        }

                        // Attempt a token refresh
                        co( this._requestToken() )
                            .then( () => {
                                this.delete( group, key, true )
                                    .then( resolve )
                                    .catch( reject )
                            })
                            .catch( err => reject({
                                status: 403,
                                body: 'Authentication can not be established',
                                err: err
                            }))

                        return
                    }

                    // Any other sort of error and just punt it out
                    reject( err )
                })
        })
    }

    /**
     * Puts a single value into a group
     */
    put( group, key, data, noRefresh ) {
        this._checkConnection()

        if ( !group || !key ) {
            throw new Error( 'PUT requires a group and key' )
        }

        if ( !data ) {
            console.warn( 'PUT no data, is this intentional?' )
        }

        return new Promise( ( resolve, reject ) => {
            this._request({
                method: 'POST',
                url: path.join( group, key ),
                headers: {
                    'Content-Type': 'application/json'
                },
                data: data
            })
                .then( res => resolve( res.body ) )
                .catch( err => {
                    // If we get a forbidden then a token refresh will probably solve it
                    if ( err.status === 403 ) {
                        // Bail if refreshing the token still fails
                        if ( noRefresh ) {
                            reject({
                                status: 403,
                                body: 'Authentication can not be established'
                            })
                            return
                        }

                        // Attempt a token refresh
                        co( this._requestToken() )
                            .then( () => {
                                this.put( group, key, data, true )
                                    .then( resolve )
                                    .catch( reject )
                            })
                            .catch( err => reject({
                                status: 403,
                                body: 'Authentication can not be established',
                                err: err
                            }))

                        return
                    }

                    // Any other sort of error and just punt it out
                    reject( err )
                })
        })
    }

    /**
     * Performs a batch into a group
     */
    batch( group, ops, noRefresh ) {
        this._checkConnection()

        if ( !group || !ops ) {
            throw new Error( 'BATCH requires a group and some data' )
        }

        if ( !ops || !ops.length ) {
            throw new Error( 'BATCH requires an array of data' )
        }

        ops.forEach( op => {
            if ( !op.type ) {
                throw new Error( 'BATCH invalid operations' )
            }
        })

        return new Promise( ( resolve, reject ) => {
            this._request({
                method: 'POST',
                url: path.join( group ),
                headers: {
                    'Content-Type': 'application/json'
                },
                data: ops
            })
                .then( res => resolve( res.body ) )
                .catch( err => {
                    // If we get a forbidden then a token refresh will probably solve it
                    if ( err.status === 403 ) {
                        // Bail if refreshing the token still fails
                        if ( noRefresh ) {
                            reject({
                                status: 403,
                                body: 'Authentication can not be established'
                            })
                            return
                        }

                        // Attempt a token refresh
                        co( this._requestToken() )
                            .then( () => {
                                this.put( group, ops, true )
                                    .then( resolve )
                                    .catch( reject )
                            })
                            .catch( err => reject({
                                status: 403,
                                body: 'Authentication can not be established',
                                err: err
                            }))

                        return
                    }

                    // Any other sort of error and just punt it out
                    reject( err )
                })
        })
    }

    /**
     * Returns a stream of values from a group
     * Returns an emitter with 'data' and 'error' events
     * Emits key,value data objects on 'data'
     */
    read( group, noRefresh ) {
        this._checkConnection()

        if ( !group ) {
            throw new Error( 'READ requires a group to stream from' )
        }

        var stream = null

        // Stream->emitter
        const emitter = new Writable({
            write: function( chunk, enc, next ) {
                if ( stream.res.statusCode !== 200 ) {
                    this.emit( 'error', stream.res.statusCode )
                    return
                }
                this.emit( 'data', JSON.parse( chunk.toString() ) )
                next()
            }
        }, {
            objectMode: true
        })

        stream = request
            .get( CONSTANTS.CONNECT_PROTOCOL + path.join( this.connect, group ) )
            .set( CONSTANTS.TOKEN_HEADER, this.token )
            .type( 'json' )
            .on( 'error', err => {
                console.error( 'Error creating read stream' )
                console.error( err )
            })

        stream.pipe( emitter )

        return emitter
    }

}
<|endoftext|>


<commit_before>/*
 * 1. Check if the browser supports `position: sticky` natively or is too old to run the polyfill.
 *    If either of these is the case set `seppuku` flag. It will be checked later to disable key features
 *    of the polyfill, but the API will remain functional to avoid breaking things.
 */
let seppuku = false;

// The polyfill cant’t function properly without `getComputedStyle`.
if (!window.getComputedStyle) seppuku = true;
// Dont’t get in a way if the browser supports `position: sticky` natively.
else {
    const testNode = document.createElement('div');

    if (
        ['', '-webkit-', '-moz-', '-ms-'].some(prefix => {
            try {
                testNode.style.position = prefix + 'sticky';
            }
            catch(e) {}

            return testNode.style.position != '';
        })
    ) seppuku = true;
}

/*
 * 2. “Global” vars used across the polyfill
 */

// Last saved scroll position
const scroll = {
    top: null,
    left: null
};

// Array of created Sticky instances
const stickies = [];

/*
 * 3. Utility functions
 */
function extend(targetObj, sourceObject) {
    for (var key in sourceObject) {
        if (sourceObject.hasOwnProperty(key)) {
            targetObj[key] = sourceObject[key];
        }
    }
}

function parseNumeric(val) {
    return parseFloat(val) || 0;
}

function getDocOffsetTop(node) {
    let docOffsetTop = 0;

    while (node) {
        docOffsetTop += node.offsetTop;
        node = node.offsetParent;
    }

    return docOffsetTop;
}

/*
 * 4. Sticky class
 */
class Sticky {
    constructor(node) {
        if (!(node instanceof HTMLElement))
            throw new Error('First argument must be HTMLElement');
        if (stickies.some(sticky => sticky._node === node))
            throw new Error('Stickyfill is already applied to this node');

        this._node = node;
        this._stickyMode = null;
        this._active = false;

        stickies.push(this);

        this.refresh();
    }

    refresh() {
        if (seppuku) return;
        if (this._active) this._deactivate();

        const node = this._node;

        /*
         * 1. Check if the node can be activated
         */
        const nodeComputedStyle = getComputedStyle(node);

        if (
            isNaN(parseFloat(nodeComputedStyle.top)) ||
            nodeComputedStyle.display == 'table-cell' ||
            nodeComputedStyle.display == 'none'
        ) return;

        this._active = true;

        /*
         * 2. Get necessary node parameters
         */
        const parentNode = node.parentNode;
        const nodeWinOffset = node.getBoundingClientRect();
        const parentWinOffset = parentNode.getBoundingClientRect();
        const parentComputedStyle = getComputedStyle(parentNode);

        this._parent = {
            node: parentNode,
            styles: {
                position: parentNode.style.position
            },
            offsetHeight: parentNode.offsetHeight
        };
        this._winOffset = {
            left: nodeWinOffset.left,
            right: document.documentElement.clientWidth - nodeWinOffset.right
        };
        this._docOffset = {
            top: nodeWinOffset.top - parentWinOffset.top - parseNumeric(parentComputedStyle.borderTopWidth),
            left: nodeWinOffset.left - parentWinOffset.left - parseNumeric(parentComputedStyle.borderLeftWidth),
            right: -nodeWinOffset.right + parentWinOffset.right - parseNumeric(parentComputedStyle.borderRightWidth)
        };
        this._styles = {
            position: node.style.position,
            top: node.style.top,
            bottom: node.style.bottom,
            left: node.style.left,
            right: node.style.right,
            width: node.style.width,
            marginTop: node.style.marginTop,
            marginLeft: node.style.marginLeft,
            marginRight: node.style.marginRight
        };

        const nodeTopValue = parseNumeric(nodeComputedStyle.top);
        this._limits = {
            start: nodeWinOffset.top + window.pageYOffset - nodeTopValue,
            end: parentWinOffset.top + window.pageYOffset + parentNode.offsetHeight -
                parseNumeric(parentComputedStyle.borderBottomWidth) - node.offsetHeight -
                nodeTopValue - parseNumeric(nodeComputedStyle.marginBottom)
        };

        /*
         * 3. Create a clone
         */
        const clone = this._clone = {};

        clone.node = document.createElement('div');
        // Apply styles to the clone
        extend(clone.node.style, {
            width: nodeWinOffset.right - nodeWinOffset.left + 'px',
            height: nodeWinOffset.bottom - nodeWinOffset.top + 'px',
            marginTop: nodeComputedStyle.marginTop,
            marginBottom: nodeComputedStyle.marginBottom,
            marginLeft: nodeComputedStyle.marginLeft,
            marginRight: nodeComputedStyle.marginRight,
            cssFloat: nodeComputedStyle.cssFloat,
            padding: 0,
            border: 0,
            borderSpacing: 0,
            fontSize: '1em',
            position: 'static'
        });

        parentNode.insertBefore(clone.node, node);
        clone.docOffsetTop = getDocOffsetTop(clone.node);

        /*
         * 4. Ensure that the node will be positioned relatively to the parent node
         */
        const parentPosition = parentComputedStyle.position;

        if (
            parentPosition != 'absolute' &&
            parentPosition != 'relative'
        ) {
            parentNode.style.position = 'relative';
        }

        this._recalcPosition();
    }

    _recalcPosition() {
        if (!this._active) return;

        const stickyMode = scroll.top <= this._limits.start? 'start': scroll.top >= this._limits.end? 'end': 'middle';

        if (this._stickyMode == stickyMode) return;

        switch (stickyMode) {
            case 'start':
                extend(this._node.style, {
                    position: 'absolute',
                    left: this._docOffset.left + 'px',
                    right: this._docOffset.right + 'px',
                    top: this._docOffset.top + 'px',
                    bottom: 'auto',
                    width: 'auto',
                    marginLeft: 0,
                    marginRight: 0,
                    marginTop: 0
                });
                break;

            case 'middle':
                extend(this._node.style, {
                    position: 'fixed',
                    left: this._winOffset.left + 'px',
                    right: this._winOffset.right + 'px',
                    top: this._styles.top,
                    bottom: 'auto',
                    width: 'auto',
                    marginLeft: 0,
                    marginRight: 0,
                    marginTop: 0
                });
                break;

            case 'end':
                extend(this._node.style, {
                    position: 'absolute',
                    left: this._docOffset.left + 'px',
                    right: this._docOffset.right + 'px',
                    top: 'auto',
                    bottom: 0,
                    width: 'auto',
                    marginLeft: 0,
                    marginRight: 0
                });
                break;
        }

        this._stickyMode = stickyMode;
    }

    _fastCheck() {
        if (!this._active) return true;

        if (Math.abs(getDocOffsetTop(this._clone.node) - this._clone.docOffsetTop) >= 2) return false;
        if (Math.abs(this._parent.node.offsetHeight - this._parent.offsetHeight) >= 2) return false;

        return true;
    }

    _deactivate() {
        if (!this._active) return;

        this._clone.node.parentNode.removeChild(this._clone.node);
        delete this._clone;

        extend(this._node.style, this._styles);
        delete this._styles;

        // Check whether element’s parent node is used by other stickies.
        // If not, restore parent node’s styles.
        if (!stickies.some(sticky => sticky !== this && sticky._parent && sticky._parent.node === this._parent.node)) {
            extend(this._parent.node.style, this._parent.styles);
        }
        delete this._parent;

        this._stickyMode = null;
        this._active = false;

        delete this._winOffset;
        delete this._docOffset;
        delete this._limits;
    }

    kill() {
        this._deactivate();

        stickies.some((sticky, index) => {
            if (sticky._node === this._node) {
                stickies.splice(index, 1);
                return true;
            }
        });
    }
}

/*
 * 5. Stickyfill API
 */
const Stickyfill = {
    stickies,
    Sticky,

    add(nodeList) {
        if (nodeList instanceof HTMLElement) nodeList = [nodeList];
        if (!nodeList[0] || !nodeList.length) return;

        const addedStickies = [];

        for (let i = 0; i < nodeList.length; i++) {
            let node = nodeList[i];
            if (!(node instanceof HTMLElement)) continue;

            //check if Stickyfill is already applied to the node
            if (stickies.every(sticky => sticky._node !== node))
                addedStickies.push(new Sticky(node));
        }

        return addedStickies;
    },

    addOne(node) {
        if (node[0]) node = node[0];
        if (!(node instanceof HTMLElement)) return;

        //check if Stickyfill is already applied to the node
        if (stickies.some(sticky => sticky._node === node)) return;

        return new Sticky(node);
    },

    remove(node) {
        stickies.some((sticky, index) => {
            if (sticky._node === node) {
                sticky.kill();
                return true;
            }
        });
    },

    refreshAll() {
        stickies.forEach(sticky => sticky.refresh());
    },

    removeAll() {
        while (stickies.length) stickies[0].kill();
    }
};

/*
 * 6. Setup events (unless the polyfill was disabled)
 */
function init() {
    // Watch for scroll position changes and trigger recalc/refresh if needed
    function checkScroll() {
        if (window.pageXOffset != scroll.left) {
            scroll.top = window.pageYOffset;
            scroll.left = window.pageXOffset;

            Stickyfill.refreshAll();
        }
        else if (window.pageYOffset != scroll.top) {
            scroll.top = window.pageYOffset;
            scroll.left = window.pageXOffset;

            // recalc position for all stickies
            stickies.forEach(sticky => sticky._recalcPosition());
        }
    }

    checkScroll();
    window.addEventListener('scroll', checkScroll);

    // Watch for window resizes and device orientation cahnges and trigger refresh
    window.addEventListener('resize', Stickyfill.refreshAll);
    window.addEventListener('orientationchange', Stickyfill.refreshAll);

    // Fast dirty check for layout changes every 500ms
    // let fastCheckTimer;

    // function fastCheck() {
    //     return stickies.every(sticky => sticky._fastCheck());
    // }

    // function startFastCheckTimer() {
    //     fastCheckTimer = setInterval(function() {
    //         if (!fastCheck()) Stickyfill.refreshAll();
    //     }, 500);
    // }

    // function stopFastCheckTimer() {
    //     clearInterval(fastCheckTimer);
    // }

    // let docHiddenKey;
    // let visibilityChangeEventName;

    // if ('hidden' in document) {
    //     docHiddenKey = 'hidden';
    //     visibilityChangeEventName = 'visibilitychange';
    // }
    // else if ('webkitHidden' in document) {
    //     docHiddenKey = 'webkitHidden';
    //     visibilityChangeEventName = 'webkitvisibilitychange';
    // }

    // if (visibilityChangeEventName) {
    //     if (!document[docHiddenKey]) startFastCheckTimer();

    //     document.addEventListener(visibilityChangeEventName, () => {
    //         if (document[docHiddenKey]) {
    //             stopFastCheckTimer();
    //         }
    //         else {
    //             startFastCheckTimer();
    //         }
    //     });
    // }
    // else startFastCheckTimer();
}

if (!seppuku) init();

/*
 * 7. Expose Stickyfill
 */
if (typeof(module) != 'undefined' && module.exports) {
    module.exports = Stickyfill;
}
else {
    window.Stickyfill = Stickyfill;
}
<commit_msg>More appropriate names for properties holding offset parameters 📌<commit_after>/*
 * 1. Check if the browser supports `position: sticky` natively or is too old to run the polyfill.
 *    If either of these is the case set `seppuku` flag. It will be checked later to disable key features
 *    of the polyfill, but the API will remain functional to avoid breaking things.
 */
let seppuku = false;

// The polyfill cant’t function properly without `getComputedStyle`.
if (!window.getComputedStyle) seppuku = true;
// Dont’t get in a way if the browser supports `position: sticky` natively.
else {
    const testNode = document.createElement('div');

    if (
        ['', '-webkit-', '-moz-', '-ms-'].some(prefix => {
            try {
                testNode.style.position = prefix + 'sticky';
            }
            catch(e) {}

            return testNode.style.position != '';
        })
    ) seppuku = true;
}

/*
 * 2. “Global” vars used across the polyfill
 */

// Last saved scroll position
const scroll = {
    top: null,
    left: null
};

// Array of created Sticky instances
const stickies = [];

/*
 * 3. Utility functions
 */
function extend(targetObj, sourceObject) {
    for (var key in sourceObject) {
        if (sourceObject.hasOwnProperty(key)) {
            targetObj[key] = sourceObject[key];
        }
    }
}

function parseNumeric(val) {
    return parseFloat(val) || 0;
}

function getDocOffsetTop(node) {
    let docOffsetTop = 0;

    while (node) {
        docOffsetTop += node.offsetTop;
        node = node.offsetParent;
    }

    return docOffsetTop;
}

/*
 * 4. Sticky class
 */
class Sticky {
    constructor(node) {
        if (!(node instanceof HTMLElement))
            throw new Error('First argument must be HTMLElement');
        if (stickies.some(sticky => sticky._node === node))
            throw new Error('Stickyfill is already applied to this node');

        this._node = node;
        this._stickyMode = null;
        this._active = false;

        stickies.push(this);

        this.refresh();
    }

    refresh() {
        if (seppuku) return;
        if (this._active) this._deactivate();

        const node = this._node;

        /*
         * 1. Check if the node can be activated
         */
        const nodeComputedStyle = getComputedStyle(node);

        if (
            isNaN(parseFloat(nodeComputedStyle.top)) ||
            nodeComputedStyle.display == 'table-cell' ||
            nodeComputedStyle.display == 'none'
        ) return;

        this._active = true;

        /*
         * 2. Get necessary node parameters
         */
        const parentNode = node.parentNode;
        const nodeWinOffset = node.getBoundingClientRect();
        const parentWinOffset = parentNode.getBoundingClientRect();
        const parentComputedStyle = getComputedStyle(parentNode);

        this._parent = {
            node: parentNode,
            styles: {
                position: parentNode.style.position
            },
            offsetHeight: parentNode.offsetHeight
        };
        this._offsetToWindow = {
            left: nodeWinOffset.left,
            right: document.documentElement.clientWidth - nodeWinOffset.right
        };
        this._offsetToParent = {
            top: nodeWinOffset.top - parentWinOffset.top - parseNumeric(parentComputedStyle.borderTopWidth),
            left: nodeWinOffset.left - parentWinOffset.left - parseNumeric(parentComputedStyle.borderLeftWidth),
            right: -nodeWinOffset.right + parentWinOffset.right - parseNumeric(parentComputedStyle.borderRightWidth)
        };
        this._styles = {
            position: node.style.position,
            top: node.style.top,
            bottom: node.style.bottom,
            left: node.style.left,
            right: node.style.right,
            width: node.style.width,
            marginTop: node.style.marginTop,
            marginLeft: node.style.marginLeft,
            marginRight: node.style.marginRight
        };

        const nodeTopValue = parseNumeric(nodeComputedStyle.top);
        this._limits = {
            start: nodeWinOffset.top + window.pageYOffset - nodeTopValue,
            end: parentWinOffset.top + window.pageYOffset + parentNode.offsetHeight -
                parseNumeric(parentComputedStyle.borderBottomWidth) - node.offsetHeight -
                nodeTopValue - parseNumeric(nodeComputedStyle.marginBottom)
        };

        /*
         * 3. Create a clone
         */
        const clone = this._clone = {};

        clone.node = document.createElement('div');
        // Apply styles to the clone
        extend(clone.node.style, {
            width: nodeWinOffset.right - nodeWinOffset.left + 'px',
            height: nodeWinOffset.bottom - nodeWinOffset.top + 'px',
            marginTop: nodeComputedStyle.marginTop,
            marginBottom: nodeComputedStyle.marginBottom,
            marginLeft: nodeComputedStyle.marginLeft,
            marginRight: nodeComputedStyle.marginRight,
            cssFloat: nodeComputedStyle.cssFloat,
            padding: 0,
            border: 0,
            borderSpacing: 0,
            fontSize: '1em',
            position: 'static'
        });

        parentNode.insertBefore(clone.node, node);
        clone.docOffsetTop = getDocOffsetTop(clone.node);

        /*
         * 4. Ensure that the node will be positioned relatively to the parent node
         */
        const parentPosition = parentComputedStyle.position;

        if (
            parentPosition != 'absolute' &&
            parentPosition != 'relative'
        ) {
            parentNode.style.position = 'relative';
        }

        this._recalcPosition();
    }

    _recalcPosition() {
        if (!this._active) return;

        const stickyMode = scroll.top <= this._limits.start? 'start': scroll.top >= this._limits.end? 'end': 'middle';

        if (this._stickyMode == stickyMode) return;

        switch (stickyMode) {
            case 'start':
                extend(this._node.style, {
                    position: 'absolute',
                    left: this._offsetToParent.left + 'px',
                    right: this._offsetToParent.right + 'px',
                    top: this._offsetToParent.top + 'px',
                    bottom: 'auto',
                    width: 'auto',
                    marginLeft: 0,
                    marginRight: 0,
                    marginTop: 0
                });
                break;

            case 'middle':
                extend(this._node.style, {
                    position: 'fixed',
                    left: this._offsetToWindow.left + 'px',
                    right: this._offsetToWindow.right + 'px',
                    top: this._styles.top,
                    bottom: 'auto',
                    width: 'auto',
                    marginLeft: 0,
                    marginRight: 0,
                    marginTop: 0
                });
                break;

            case 'end':
                extend(this._node.style, {
                    position: 'absolute',
                    left: this._offsetToParent.left + 'px',
                    right: this._offsetToParent.right + 'px',
                    top: 'auto',
                    bottom: 0,
                    width: 'auto',
                    marginLeft: 0,
                    marginRight: 0
                });
                break;
        }

        this._stickyMode = stickyMode;
    }

    _fastCheck() {
        if (!this._active) return true;

        if (Math.abs(getDocOffsetTop(this._clone.node) - this._clone.docOffsetTop) >= 2) return false;
        if (Math.abs(this._parent.node.offsetHeight - this._parent.offsetHeight) >= 2) return false;

        return true;
    }

    _deactivate() {
        if (!this._active) return;

        this._clone.node.parentNode.removeChild(this._clone.node);
        delete this._clone;

        extend(this._node.style, this._styles);
        delete this._styles;

        // Check whether element’s parent node is used by other stickies.
        // If not, restore parent node’s styles.
        if (!stickies.some(sticky => sticky !== this && sticky._parent && sticky._parent.node === this._parent.node)) {
            extend(this._parent.node.style, this._parent.styles);
        }
        delete this._parent;

        this._stickyMode = null;
        this._active = false;

        delete this._offsetToWindow;
        delete this._offsetToParent;
        delete this._limits;
    }

    kill() {
        this._deactivate();

        stickies.some((sticky, index) => {
            if (sticky._node === this._node) {
                stickies.splice(index, 1);
                return true;
            }
        });
    }
}

/*
 * 5. Stickyfill API
 */
const Stickyfill = {
    stickies,
    Sticky,

    add(nodeList) {
        if (nodeList instanceof HTMLElement) nodeList = [nodeList];
        if (!nodeList[0] || !nodeList.length) return;

        const addedStickies = [];

        for (let i = 0; i < nodeList.length; i++) {
            let node = nodeList[i];
            if (!(node instanceof HTMLElement)) continue;

            //check if Stickyfill is already applied to the node
            if (stickies.every(sticky => sticky._node !== node))
                addedStickies.push(new Sticky(node));
        }

        return addedStickies;
    },

    addOne(node) {
        if (node[0]) node = node[0];
        if (!(node instanceof HTMLElement)) return;

        //check if Stickyfill is already applied to the node
        if (stickies.some(sticky => sticky._node === node)) return;

        return new Sticky(node);
    },

    remove(node) {
        stickies.some((sticky, index) => {
            if (sticky._node === node) {
                sticky.kill();
                return true;
            }
        });
    },

    refreshAll() {
        stickies.forEach(sticky => sticky.refresh());
    },

    removeAll() {
        while (stickies.length) stickies[0].kill();
    }
};

/*
 * 6. Setup events (unless the polyfill was disabled)
 */
function init() {
    // Watch for scroll position changes and trigger recalc/refresh if needed
    function checkScroll() {
        if (window.pageXOffset != scroll.left) {
            scroll.top = window.pageYOffset;
            scroll.left = window.pageXOffset;

            Stickyfill.refreshAll();
        }
        else if (window.pageYOffset != scroll.top) {
            scroll.top = window.pageYOffset;
            scroll.left = window.pageXOffset;

            // recalc position for all stickies
            stickies.forEach(sticky => sticky._recalcPosition());
        }
    }

    checkScroll();
    window.addEventListener('scroll', checkScroll);

    // Watch for window resizes and device orientation cahnges and trigger refresh
    window.addEventListener('resize', Stickyfill.refreshAll);
    window.addEventListener('orientationchange', Stickyfill.refreshAll);

    // Fast dirty check for layout changes every 500ms
    // let fastCheckTimer;

    // function fastCheck() {
    //     return stickies.every(sticky => sticky._fastCheck());
    // }

    // function startFastCheckTimer() {
    //     fastCheckTimer = setInterval(function() {
    //         if (!fastCheck()) Stickyfill.refreshAll();
    //     }, 500);
    // }

    // function stopFastCheckTimer() {
    //     clearInterval(fastCheckTimer);
    // }

    // let docHiddenKey;
    // let visibilityChangeEventName;

    // if ('hidden' in document) {
    //     docHiddenKey = 'hidden';
    //     visibilityChangeEventName = 'visibilitychange';
    // }
    // else if ('webkitHidden' in document) {
    //     docHiddenKey = 'webkitHidden';
    //     visibilityChangeEventName = 'webkitvisibilitychange';
    // }

    // if (visibilityChangeEventName) {
    //     if (!document[docHiddenKey]) startFastCheckTimer();

    //     document.addEventListener(visibilityChangeEventName, () => {
    //         if (document[docHiddenKey]) {
    //             stopFastCheckTimer();
    //         }
    //         else {
    //             startFastCheckTimer();
    //         }
    //     });
    // }
    // else startFastCheckTimer();
}

if (!seppuku) init();

/*
 * 7. Expose Stickyfill
 */
if (typeof(module) != 'undefined' && module.exports) {
    module.exports = Stickyfill;
}
else {
    window.Stickyfill = Stickyfill;
}
<|endoftext|>


<commit_before>/* @flow */
/**
 *  Copyright (c) 2015, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 */

// A Function, which when given an Array of keys, returns a Promise of an Array
// of values or Errors.
type BatchLoadFn<K, V> = (keys: Array<K>) => Promise<Array<V | Error>>

// Optionally turn off batching or caching.
type Options = { batch?: boolean, cache?: boolean }

/**
 A `DataLoader` creates a public API for loading data from a particular
 * data back-end with unique keys such as the `id` column of a SQL table or
 * document name in a MongoDB database, given a batch loading function.
 *
 * Each `DataLoader` instance contains a unique memoized cache. Use caution when
 * used in long-lived applications or those which serve many users with
 * different access permissions and consider creating a new instance per
 * web request.
 */
export default class DataLoader<K, V> {
  constructor(
    batchLoadFn: BatchLoadFn<K, V>,
    options?: Options
  ) {
    if (typeof batchLoadFn !== 'function') {
      throw new TypeError(
        'DataLoader must be constructed with a function which accepts ' +
        `Array<key> and returns Promise<Array<value>>, but got: ${batchLoadFn}.`
      );
    }
    this._batchLoadFn = batchLoadFn;
    this._options = options;
    this._promiseCache = new Map();
    this._queue = [];
  }

  // Private
  _batchLoadFn: BatchLoadFn<K, V>;
  _options: ?Options;
  _promiseCache: Map<K, Promise<V>>;
  _queue: LoaderQueue<K, V>;

  /**
   * Loads a key, returning a `Promise` for the value represented by that key.
   */
  load(key: K): Promise<V> {
    if (key === null || key === undefined) {
      throw new TypeError(
        'The loader.load() function must be called with a value,' +
        `but got: ${key}.`
      );
    }

    // Determine options
    var options = this._options;
    var shouldBatch = !options || options.batch !== false;
    var shouldCache = !options || options.cache !== false;

    // If caching and there is a cache-hit, return cached Promise.
    if (shouldCache && this._promiseCache.has(key)) {
      return this._promiseCache.get(key);
    }

    // Otherwise, produce a new Promise for this value.
    var promise = new Promise((resolve, reject) => {

      // Enqueue this Promise to be dispatched, and determine if a new dispatch
      // needs to be created: if this is the first enqueue upon an empty queue.
      var needsDispatch = this._queue.length === 0;
      this._queue.push({ key, resolve, reject });

      if (needsDispatch) {
        if (shouldBatch) {
          // If batching, schedule a task to dispatch the queue.
          enqueuePostPromiseJob(() => dispatchQueue(this));
        } else {
          // Otherwise dispatch the (queue of one) immediately.
          dispatchQueue(this);
        }
      }
    });

    // If caching, cache this promise.
    if (shouldCache) {
      this._promiseCache.set(key, promise);
    }

    return promise;
  }

  /**
   * Loads multiple keys, promising an array of values:
   *
   *     var [ a, b ] = await myLoader.loadMany([ 'a', 'b' ]);
   *
   * This is equivalent to the more verbose:
   *
   *     var [ a, b ] = await Promise.all([
   *       myLoader.load('a'),
   *       myLoader.load('b')
   *     ]);
   *
   */
  loadMany(keys: Array<K>): Promise<Array<V>> {
    if (!Array.isArray(keys)) {
      throw new TypeError(
        'The loader.loadMany() function must be called with Array<key> ' +
        `but got: ${keys}.`
      );
    }
    return Promise.all(keys.map(key => this.load(key)));
  }

  /**
   * Clears the value at `key` from the cache, if it exists. Returns itself for
   * method chaining.
   */
  clear(key: K): DataLoader<K, V> {
    this._promiseCache.delete(key);
    return this;
  }

  /**
   * Clears the entire cache. To be used when some event results in unknown
   * invalidations across this particular `DataLoader`. Returns itself for
   * method chaining.
   */
  clearAll(): DataLoader<K, V> {
    this._promiseCache.clear();
    return this;
  }
}

// Private: Enqueue a Job to be executed after all "PromiseJobs" Jobs.
//
// ES6 JavaScript uses the concepts Job and JobQueue to schedule work to occur
// after the current execution context has completed:
// http://www.ecma-international.org/ecma-262/6.0/#sec-jobs-and-job-queues
//
// Node.js uses the `process.nextTick` mechanism to implement the concept of a
// Job, maintaining a global FIFO JobQueue for all Jobs, which is flushed after
// the current call stack ends.
//
// When calling `then` on a Promise, it enqueues a Job on a specific
// "PromiseJobs" JobQueue which is flushed in Node as a single Job on the
// global JobQueue.
//
// DataLoader batches all loads which occur in a single frame of execution, but
// should include in the batch all loads which occur during the flushing of the
// "PromiseJobs" JobQueue after that same execution frame.
//
// In order to avoid the DataLoader dispatch Job occuring before "PromiseJobs",
// A Promise Job is created with the sole purpose of enqueuing a global Job,
// ensuring that it always occurs after "PromiseJobs" ends.
function enqueuePostPromiseJob(fn) {
  if (!resolvedPromise) {
    resolvedPromise = Promise.resolve();
  }
  resolvedPromise.then(() => process.nextTick(fn));
}

// Private: cached resolved Promise instance
var resolvedPromise;

// Private: given the current state of a Loader instance, perform a batch load
// from its current queue.
function dispatchQueue<K, V>(loader: DataLoader<K, V>) {
  // Take the current loader queue, replacing it with an empty queue.
  var queue = loader._queue;
  loader._queue = [];

  // Collect all keys to be loaded in this dispatch
  var keys = queue.map(({ key }) => key);

  // Call the provided batchLoadFn for this loader with the loader queue's keys.
  var batchLoadFn = loader._batchLoadFn;
  var batchPromise = batchLoadFn(keys);

  // Assert the expected response from batchLoadFn
  if (!batchPromise || typeof batchPromise.then !== 'function') {
    return failedDispatch(loader, queue, new TypeError(
      'DataLoader must be constructed with a function which accepts ' +
      'Array<key> and returns Promise<Array<value>>, but the function did ' +
      `not return a Promise: ${batchPromise}.`
    ));
  }

  // Await the resolution of the call to batchLoadFn.
  batchPromise.then(values => {

    // Assert the expected resolution from batchLoadFn.
    if (!Array.isArray(values)) {
      throw new Error(
        'DataLoader must be constructed with a function which accepts ' +
        'Array<key> and returns Promise<Array<value>>, but the function did ' +
        `not return a Promise of an Array: ${values}.`
      );
    }
    if (values.length !== keys.length) {
      throw new Error(
        'DataLoader must be constructed with a function which accepts ' +
        'Array<key> and returns Promise<Array<value>>, but the function did ' +
        'not return a Promise of an Array of the same length as the Array ' +
        'of keys.' +
        `\n\nKeys:\n${keys}` +
        `\n\nValues:\n${values}`
      );
    }

    // Step through the values, resolving or rejecting each Promise in the
    // loaded queue.
    queue.forEach(({ key, resolve, reject }, index) => {
      var value = values[index];
      if (value instanceof Error) {
        reject(value);
      } else {
        resolve(value);
      }
    });
  }).catch(error => failedDispatch(loader, queue, error));
}

// Private: do not cache individual loads if the entire batch dispatch fails,
// but still reject each request so they do not hang.
function failedDispatch<K, V>(
  loader: DataLoader<K, V>,
  queue: LoaderQueue<K, V>,
  error: Error
) {
  queue.forEach(({ key, reject }) => {
    loader.clear(key);
    reject(error);
  });
}

// Private
type LoaderQueue<K, V> = Array<{
  key: K,
  resolve: (value: V) => void,
  reject: (error: Error) => void
}>
<commit_msg>Clarify queue dispatch logic<commit_after>/* @flow */
/**
 *  Copyright (c) 2015, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 */

// A Function, which when given an Array of keys, returns a Promise of an Array
// of values or Errors.
type BatchLoadFn<K, V> = (keys: Array<K>) => Promise<Array<V | Error>>

// Optionally turn off batching or caching.
type Options = { batch?: boolean, cache?: boolean }

/**
 A `DataLoader` creates a public API for loading data from a particular
 * data back-end with unique keys such as the `id` column of a SQL table or
 * document name in a MongoDB database, given a batch loading function.
 *
 * Each `DataLoader` instance contains a unique memoized cache. Use caution when
 * used in long-lived applications or those which serve many users with
 * different access permissions and consider creating a new instance per
 * web request.
 */
export default class DataLoader<K, V> {
  constructor(
    batchLoadFn: BatchLoadFn<K, V>,
    options?: Options
  ) {
    if (typeof batchLoadFn !== 'function') {
      throw new TypeError(
        'DataLoader must be constructed with a function which accepts ' +
        `Array<key> and returns Promise<Array<value>>, but got: ${batchLoadFn}.`
      );
    }
    this._batchLoadFn = batchLoadFn;
    this._options = options;
    this._promiseCache = new Map();
    this._queue = [];
  }

  // Private
  _batchLoadFn: BatchLoadFn<K, V>;
  _options: ?Options;
  _promiseCache: Map<K, Promise<V>>;
  _queue: LoaderQueue<K, V>;

  /**
   * Loads a key, returning a `Promise` for the value represented by that key.
   */
  load(key: K): Promise<V> {
    if (key === null || key === undefined) {
      throw new TypeError(
        'The loader.load() function must be called with a value,' +
        `but got: ${key}.`
      );
    }

    // Determine options
    var options = this._options;
    var shouldBatch = !options || options.batch !== false;
    var shouldCache = !options || options.cache !== false;

    // If caching and there is a cache-hit, return cached Promise.
    if (shouldCache && this._promiseCache.has(key)) {
      return this._promiseCache.get(key);
    }

    // Otherwise, produce a new Promise for this value.
    var promise = new Promise((resolve, reject) => {
      // Enqueue this Promise to be dispatched.
      this._queue.push({ key, resolve, reject });

      // Determine if a dispatch of this queue should be scheduled.
      // A single dispatch should be scheduled per queue at the time when the
      // queue changes from "empty" to "full".
      if (this._queue.length === 1) {
        if (shouldBatch) {
          // If batching, schedule a task to dispatch the queue.
          enqueuePostPromiseJob(() => dispatchQueue(this));
        } else {
          // Otherwise dispatch the (queue of one) immediately.
          dispatchQueue(this);
        }
      }
    });

    // If caching, cache this promise.
    if (shouldCache) {
      this._promiseCache.set(key, promise);
    }

    return promise;
  }

  /**
   * Loads multiple keys, promising an array of values:
   *
   *     var [ a, b ] = await myLoader.loadMany([ 'a', 'b' ]);
   *
   * This is equivalent to the more verbose:
   *
   *     var [ a, b ] = await Promise.all([
   *       myLoader.load('a'),
   *       myLoader.load('b')
   *     ]);
   *
   */
  loadMany(keys: Array<K>): Promise<Array<V>> {
    if (!Array.isArray(keys)) {
      throw new TypeError(
        'The loader.loadMany() function must be called with Array<key> ' +
        `but got: ${keys}.`
      );
    }
    return Promise.all(keys.map(key => this.load(key)));
  }

  /**
   * Clears the value at `key` from the cache, if it exists. Returns itself for
   * method chaining.
   */
  clear(key: K): DataLoader<K, V> {
    this._promiseCache.delete(key);
    return this;
  }

  /**
   * Clears the entire cache. To be used when some event results in unknown
   * invalidations across this particular `DataLoader`. Returns itself for
   * method chaining.
   */
  clearAll(): DataLoader<K, V> {
    this._promiseCache.clear();
    return this;
  }
}

// Private: Enqueue a Job to be executed after all "PromiseJobs" Jobs.
//
// ES6 JavaScript uses the concepts Job and JobQueue to schedule work to occur
// after the current execution context has completed:
// http://www.ecma-international.org/ecma-262/6.0/#sec-jobs-and-job-queues
//
// Node.js uses the `process.nextTick` mechanism to implement the concept of a
// Job, maintaining a global FIFO JobQueue for all Jobs, which is flushed after
// the current call stack ends.
//
// When calling `then` on a Promise, it enqueues a Job on a specific
// "PromiseJobs" JobQueue which is flushed in Node as a single Job on the
// global JobQueue.
//
// DataLoader batches all loads which occur in a single frame of execution, but
// should include in the batch all loads which occur during the flushing of the
// "PromiseJobs" JobQueue after that same execution frame.
//
// In order to avoid the DataLoader dispatch Job occuring before "PromiseJobs",
// A Promise Job is created with the sole purpose of enqueuing a global Job,
// ensuring that it always occurs after "PromiseJobs" ends.
function enqueuePostPromiseJob(fn) {
  if (!resolvedPromise) {
    resolvedPromise = Promise.resolve();
  }
  resolvedPromise.then(() => process.nextTick(fn));
}

// Private: cached resolved Promise instance
var resolvedPromise;

// Private: given the current state of a Loader instance, perform a batch load
// from its current queue.
function dispatchQueue<K, V>(loader: DataLoader<K, V>) {
  // Take the current loader queue, replacing it with an empty queue.
  var queue = loader._queue;
  loader._queue = [];

  // Collect all keys to be loaded in this dispatch
  var keys = queue.map(({ key }) => key);

  // Call the provided batchLoadFn for this loader with the loader queue's keys.
  var batchLoadFn = loader._batchLoadFn;
  var batchPromise = batchLoadFn(keys);

  // Assert the expected response from batchLoadFn
  if (!batchPromise || typeof batchPromise.then !== 'function') {
    return failedDispatch(loader, queue, new TypeError(
      'DataLoader must be constructed with a function which accepts ' +
      'Array<key> and returns Promise<Array<value>>, but the function did ' +
      `not return a Promise: ${batchPromise}.`
    ));
  }

  // Await the resolution of the call to batchLoadFn.
  batchPromise.then(values => {

    // Assert the expected resolution from batchLoadFn.
    if (!Array.isArray(values)) {
      throw new Error(
        'DataLoader must be constructed with a function which accepts ' +
        'Array<key> and returns Promise<Array<value>>, but the function did ' +
        `not return a Promise of an Array: ${values}.`
      );
    }
    if (values.length !== keys.length) {
      throw new Error(
        'DataLoader must be constructed with a function which accepts ' +
        'Array<key> and returns Promise<Array<value>>, but the function did ' +
        'not return a Promise of an Array of the same length as the Array ' +
        'of keys.' +
        `\n\nKeys:\n${keys}` +
        `\n\nValues:\n${values}`
      );
    }

    // Step through the values, resolving or rejecting each Promise in the
    // loaded queue.
    queue.forEach(({ key, resolve, reject }, index) => {
      var value = values[index];
      if (value instanceof Error) {
        reject(value);
      } else {
        resolve(value);
      }
    });
  }).catch(error => failedDispatch(loader, queue, error));
}

// Private: do not cache individual loads if the entire batch dispatch fails,
// but still reject each request so they do not hang.
function failedDispatch<K, V>(
  loader: DataLoader<K, V>,
  queue: LoaderQueue<K, V>,
  error: Error
) {
  queue.forEach(({ key, reject }) => {
    loader.clear(key);
    reject(error);
  });
}

// Private
type LoaderQueue<K, V> = Array<{
  key: K,
  resolve: (value: V) => void,
  reject: (error: Error) => void
}>
<|endoftext|>


<commit_before>/*
GreedyNav.js - http://lukejacksonn.com/actuate
Licensed under the MIT license - http://opensource.org/licenses/MIT
Copyright (c) 2015 Luke Jackson
*/

// Smartresize by Paul Irish
// http://www.paulirish.com/2009/throttled-smartresize-jquery-event-handler/

!function(e){var n,t,i=e.event;n=i.special.debouncedresize={setup:function(){e(this).on("resize",n.handler)},teardown:function(){e(this).off("resize",n.handler)},handler:function(e,r){var o=this,s=arguments,u=function(){e.type="debouncedresize",i.dispatch.apply(o,s)};t&&clearTimeout(t),r?u():t=setTimeout(u,n.threshold)},threshold:150}}(jQuery);

$(function () {

  var $nav = $('.greedy-nav');
  var $btn = $('.greedy-nav button');
  var $vlinks = $('.greedy-nav .visible-links');
  var $hlinks = $('.greedy-nav .hidden-links');

  var breaks = [];

  function updateNav() {

    var availableSpace = $nav.width() - $btn.width() - 15;

    // The visible list is overflowing the nav
    if($vlinks.width() > availableSpace) {

      // Record the width of the list
      breaks.push($vlinks.width());

      // Move item to the hidden list
      $vlinks.children().last().prependTo($hlinks);

      // Show the dropdown btn
      if($btn.hasClass('hidden')) {
        $btn.removeClass('hidden');
      }

    // The visible list is not overflowing
    } else {

      // There is space for another item in the nav
      if(availableSpace > breaks[breaks.length-1]) {

        // Move the item to the visible list
        $hlinks.children().first().appendTo($vlinks);
        breaks.pop();
      }

      // Hide the dropdown btn if hidden list is empty
      if(breaks.length < 1) {
        $btn.addClass('hidden');
        $hlinks.addClass('hidden');
      }
    }

    // Keep counter updated
    $btn.attr("count", breaks.length);

    // Recur if the visible list is still overflowing the nav
    if($vlinks.width() > availableSpace) {
      updateNav();
    }

  }

  // Window resize listener

  $(window).on('debouncedresize', function() {
      updateNav();
  });

  $btn.on('click', function() {
    $hlinks.toggleClass('hidden');
  });

  updateNav();

});
<commit_msg>Removed smartresize dependency<commit_after>/*
GreedyNav.js - http://lukejacksonn.com/actuate
Licensed under the MIT license - http://opensource.org/licenses/MIT
Copyright (c) 2015 Luke Jackson
*/

$(function () {

var $nav = $('.greedy-nav');
var $btn = $('.greedy-nav button');
var $vlinks = $('.greedy-nav .visible-links');
var $hlinks = $('.greedy-nav .hidden-links');

var breaks = [];

  function updateNav() {

    var availableSpace = $btn.hasClass('hidden') ? $nav.width() : $nav.width() - $btn.width() - 30;

    // The visible list is overflowing the nav
    if($vlinks.width() > availableSpace) {

      // Record the width of the list
      breaks.push($vlinks.width());

      // Move item to the hidden list
      $vlinks.children().last().prependTo($hlinks);

      // Show the dropdown btn
      if($btn.hasClass('hidden')) {
        $btn.removeClass('hidden');
      }

    // The visible list is not overflowing
    } else {

      // There is space for another item in the nav
      if(availableSpace > breaks[breaks.length-1]) {

        // Move the item to the visible list
        $hlinks.children().first().appendTo($vlinks);
        breaks.pop();
      }

      // Hide the dropdown btn if hidden list is empty
      if(breaks.length < 1) {
        $btn.addClass('hidden');
        $hlinks.addClass('hidden');
      }
    }

    // Keep counter updated
    $btn.attr("count", breaks.length);

    // Recur if the items are still overflowing the nav
    if($vlinks.width() > availableSpace) {
      updateNav();
    }

  }

  // Window listeners

  $(window).resize(function() {
      updateNav();
  });

  $btn.on('click', function() {
    $hlinks.toggleClass('hidden');
  });

  updateNav();

});
<|endoftext|>