<commit_before>const moment = require('moment-timezone');
const Log = require('./debugPrint');
const settings = require('./settings');
const Cache = require('./cache');

const sScheduleDates = Symbol('Schedule Regular Dates');
const sScheduleDatesSpecial = Symbol('Schedule Special Dates');
const sCache = Symbol('Cache');
const sCachePopulated = Symbol('Cache Populated State');
const sCacheWaitCallbacks = Symbol('Cache Initiated Callbacks');
const sCacheTimer = Symbol('Cache Timer');

/**
 * @typedef ScheduleData
 * @type Object
 * @property {Moment} date Date this schedule data applies to
 * @property {Moment} openingTime Opening time for this date
 * @property {Moment} closingTime Closing time for this date
 * @property {String} type Whether this schedule data refers to an "Operating", "Closed" or "Refurbishment" status
 * @property {SpecialScheduleData[]} special Won't exist if no special times exist for this date
 */

/**
 * @typedef SpecialScheduleData
 * @type Object
 * @property {Moment} openingTime Opening time for this special schedule data
 * @property {Moment} closingTime Closing time for this special schedule data
 * @property {String} type Type of special schedule this is (eg. "Extra Magic Hours")
 */

function dateToDay(date) {
  // calculate the day since Unix Epoch
  //  .unix returns in UTC, so we convert to minutes and add on the utcOffset (then convert from minutes to days)
  //  finally we Math.floor to round downwards to get the current day as an integer
  return Math.floor(((date.unix() / 60) + date.utcOffset()) / 1440);
}

function parseDateTime(dateObject, varName) {
  let returnDate = dateObject;

  // check if it's already a valid Moment object
  if (!moment.isMoment(dateObject)) {
    // try and parse if this is a string
    returnDate = moment(dateObject, [
      moment.ISO_8601,
      settings.DefaultTimeFormat,
      settings.DefaultDateFormat,
      'YYYY-MM-DD',
    ]);

    // check if we ended up with a valid timestamp
    if (!returnDate.isValid()) {
      Log(`Invalid scheduleData.${varName}:`, returnDate);
      return false;
    }
  }

  // we got this far, success! return the new Moment object (or the original one if it was always good!)
  return returnDate;
}

/**
 * Schedule class to hold opening and closing times for parks and rides etc.
 * Supports standard and "special" opening times
 * @class
 */
class Schedule {
  /**
   * Create a new Schedule object
   * @param {Object} options
   * @param {String} options.id Unique Schedule ID for caching keys
   */
  constructor(options = {}) {
    // use Map for better structure (int -> data)
    //  int is the number of days since Unix Epoch
    this[sScheduleDates] = new Map();
    // also one for special hours (this is actually int -> data[] to support multiple special times)
    this[sScheduleDatesSpecial] = new Map();

    // setup our cache object
    if (options.id === undefined) throw new Error('No id passed for schedule object');
    this[sCache] = new Cache({
      prefix: `schedule_${options.id}`,
    });
    // store flag of whether we've populated ourselves with cache data yet
    this[sCachePopulated] = false;
    this[sCacheWaitCallbacks] = [];

    // try and populate cache
    this.CheckCacheStatus();
  }

  /**
   * Set schedule data for a date
   * @param {Object} scheduleData
   * @param {Moment|String} [scheduleData.date=scheduleData.openingTime] Moment.js date object (or a valid date String to be parsed by Moment JS). Will use openingTime if this is not supplied
   * @param {Moment|String} [scheduleData.openingTime] Moment.js date object of this day's opening time (or a valid date String to be parsed by Moment JS) (can be ignored if type is Closed)
   * @param {Moment|String} [scheduleData.closingTime] Moment.js date object of this day's closing time (or a valid date String to be parsed by Moment JS) (can be ignored if type is Closed)
   * @param {Boolean} [scheduleData.specialHours=false] Is this schedule data part of schedule special hours?
   * @param {String} [scheduleData.type=Operating] The schedule type. Normal schedules should always be "Operating", "Closed" or "Refurbishment". Special schedules can be any String (eg. Extra Magic Hours).
   * @returns {Boolean} success Returns true if the operation was a success and the data was actually changed
   */
  SetDate({
    // the day to set the schedule data for
    date = null,
    // opening time for this day
    openingTime = null,
    // closing time for this day
    closingTime = null,
    // is this special hours data? (default: false)
    specialHours = false,
    // the type of this schedule date (default: Operating)
    type = 'Operating',
  }) {
    let newDate = date.clone();
    let newOpeningTime = openingTime !== null ? openingTime.clone() : null;
    let newClosingTime = closingTime !== null ? closingTime.clone() : null;

    // if we haven't been supplied a date, use the opening time
    if (!newDate) newDate = openingTime.clone();

    // check our date is a valid momentjs object
    newDate = parseDateTime(newDate, 'date');

    // special case, if this is a closed date, support not passing in opening and closing times
    if (type === 'Closed') {
      if (!openingTime) newOpeningTime = newDate.startOf('day');
      if (!closingTime) newClosingTime = newDate.endOf('day');
    }

    newOpeningTime = parseDateTime(newOpeningTime, 'openingTime');
    newClosingTime = parseDateTime(newClosingTime, 'closingTime');

    // if any of our dates are invalid, return false
    if (!newDate || !newOpeningTime || !newClosingTime) return false;

    // calculate the days since Unix Epoch
    const day = dateToDay(newDate);

    // make sure opening and closing times are in the correct day!
    const todaySet = {
      year: newDate.year(),
      month: newDate.month(),
      date: newDate.date(),
    };
    newOpeningTime.set(todaySet);
    newClosingTime.set(todaySet);

    // work out if the closing time is in the following day
    if (newClosingTime.isBefore(newOpeningTime)) {
      // add 1 day if the closing time comes before the opening time (implying it's open past midnight!)
      newClosingTime.add(1, 'day');
    }

    // build schedule data object and add it to our schedule map
    if (!specialHours) {
      // check our schedule type is sane
      if (type !== 'Operating' && type !== 'Closed' && type !== 'Refurbishment') {
        Log(`Tried to use invalid schedule type ${type} for standard schedule data (must be Operating, Closed or Refurbishment)`);
        return false;
      }

      const newScheduleData = {
        date: newDate,
        openingTime: newOpeningTime,
        closingTime: newClosingTime,
        type,
      };

      // check if we already have this data for this day (don't invalidate cache etc if it hasn't changed)
      if (this[sScheduleDates].has(day)) {
        const checkDirtyObj = this[sScheduleDates].get(day);
        if (
          checkDirtyObj.date === newScheduleData.date
          && checkDirtyObj.openingTime === newScheduleData.openingTime
          && checkDirtyObj.closingTime === newScheduleData.closingTime
          && checkDirtyObj.type === newScheduleData.type) {
          // data is identical to existing object, don't update
          return false;
        }
      }

      // set this day's schedule data
      this[sScheduleDates].set(day, newScheduleData);

      // we have new data, so mark it as dirty to get cached
      this.RequestCache();
    } else {
      // special hours can't be Operating or Closed, that is for normal hours
      if (type === 'Operating' || type === 'Closed') {
        Log(`Tried to use invalid schedule type ${type} for special schedule data (can't be Operating or Closed)`);
        return false;
      }

      // add a new special hours array if we don't already have one
      if (!this[sScheduleDatesSpecial].has(day)) {
        this[sScheduleDatesSpecial].set(day, []);

        // we have new data, so mark it as dirty to get cached
        this.RequestCache();
      }

      const newSpecialScheduleData = {
        openingTime,
        closingTime,
        type,
      };

      // check we don't already have this special data in our array
      const newDataStringified = JSON.stringify(newSpecialScheduleData);
      const specialDayArray = this[sScheduleDatesSpecial].get(day);
      for (let i = 0; i < specialDayArray.length; i += 1) {
        if (JSON.stringify(specialDayArray[i]) === newDataStringified) {
          // this object already exists, so bail out
          return false;
        }
      }

      // add our new data to the specials array
      specialDayArray.push(newSpecialScheduleData);

      // we have new data, so mark it as dirty to get cached
      this.RequestCache();
    }

    return true;
  }

  /**
   * Set a range of dates with the same schedule data
   * @param {Object} scheduleData
   * @param {Moment|String} scheduleData.startDate Moment.js date object to start schedule date range (or a valid date String to be parsed by Moment JS)
   * @param {Moment|String} scheduleData.endDate Moment.js date object to end schedule date range (or a valid date String to be parsed by Moment JS)
   * @param {Moment|String} scheduleData.openingTime Moment.js date object of this day's opening time (or a valid date String to be parsed by Moment JS)
   * @param {Moment|String} scheduleData.closingTime Moment.js date object of this day's closing time (or a valid date String to be parsed by Moment JS)
   * @param {Boolean} [scheduleData.specialHours=false] Is this schedule data part of schedule special hours?
   * @param {String} [scheduleData.type=Operating] The schedule type. Normal schedules should always be "Operating", "Closed" or "Refurbishment". Special schedules can be any String (eg. Extra Magic Hours).
   * @returns {Boolean} success
   */
  SetRange({
    // first date of the range to set schedule for
    startDate = null,
    // first date of the range to set schedule for
    endDate = null,
    // opening time for this day
    openingTime = null,
    // closing time for this day
    closingTime = null,
    // is this special hours data? (default: false)
    specialHours = false,
    // the type of this schedule date (default: Operating)
    type = 'Operating',
  }) {
    // check our input dates are valid
    const newStartDate = parseDateTime(startDate, 'startDate');
    const newEndDate = parseDateTime(endDate, 'endDate');
    const newOpeningTime = parseDateTime(openingTime, 'openingTime');
    const newClosingTime = parseDateTime(closingTime, 'closingTime');

    // if any of our dates are invalid, return false
    if (!newStartDate || !newEndDate || !newOpeningTime || !newClosingTime) return false;

    // if any of our dates result in invalid data, return false
    let retValue = true;

    // add each day using SetDate
    for (let m = newStartDate; m.isSameOrBefore(newEndDate); m.add(1, 'days')) {
      // retValue AND= means this becomes false with any one failed result
      //  if we do fail, we also just keep going to try and get as much done as possible :)
      const successfulSet = this.SetDate({
        date: m,
        newOpeningTime,
        newClosingTime,
        specialHours,
        type,
      });

      retValue = retValue && successfulSet;
    }

    return retValue;
  }

  /**
   * Get schedule data for a specific date
   * @param {Object} dateData
   * @param {Moment|String} dateData.date Moment.js date object to fetch schedule data for (or a valid date String to be parsed by Moment JS)
   * @return {ScheduleData} scheduleResult Can be false if no data exists for the requested date
   */
  GetDate({
    date = null,
  }) {
    // check our date is valid
    const newDate = parseDateTime(date, 'date');
    if (!newDate) return undefined;

    // do we have this day in our schedule data?
    const day = dateToDay(newDate);

    if (!this[sScheduleDates].has(day)) return undefined;

    const dayData = this[sScheduleDates].get(day);
    // copy data into the return object (otherwise we end up modifying the actual date data!)
    const returnObject = {
      date: dayData.date,
      openingTime: dayData.openingTime,
      closingTime: dayData.closingTime,
      type: dayData.type,
    };

    // add special schedules if we have any!
    if (this[sScheduleDatesSpecial].has(day)) {
      returnObject.special = this[sScheduleDatesSpecial].get(day);
    }

    return returnObject;
  }

  /**
   * Get schedule data for a range of dates
   * @param {Object} dateData
   * @param {Moment|String} dateData.startDate Moment.js date object to fetch schedule data from (or a valid date String to be parsed by Moment JS)
   * @param {Moment|String} dateData.endDate Moment.js date object to fetch schedule data from (or a valid date String to be parsed by Moment JS)
   * @return {ScheduleData[]} scheduleResult Can be an empty array if there is no valid data (won't be null)
   */
  GetDateRange({
    startDate = null,
    endDate = null,
  }) {
    // check start and end date are valid
    const newStartDate = parseDateTime(startDate, 'startDate');
    const newEndDate = parseDateTime(endDate, 'endDate');
    if (!newStartDate || !newEndDate) return [];

    // fetch each day of the range and add it to our result
    const returnArray = [];
    for (let m = newStartDate; m.isSameOrBefore(newEndDate); m.add(1, 'days')) {
      const dateSchedule = this.GetDate({
        date: m,
      });
      if (dateSchedule !== undefined) {
        returnArray.push(dateSchedule);
      }
    }

    return returnArray;
  }

  /**
   * Get our cache object
   */
  get Cache() {
    return this[sCache];
  }

  /**
   * Request a cache to happen at the next best opportunity
   */
  RequestCache() {
    // if other caching was queued up, skip it
    if (this[sCacheTimer] !== undefined) {
      clearTimeout(this[sCacheTimer]);
    }

    // schedule caching in half a second
    this[sCacheTimer] = setTimeout(this.WriteToCache.bind(this), 500);
  }

  /**
   * Write schedule data to the cache
   */
  WriteToCache() {
    // if any timer exists for caching, clear it out, we're caching right now
    if (this[sCacheTimer] !== undefined) {
      clearTimeout(this[sCacheTimer]);
    }

    // delete old data (older than 3 days to be safe)
    const threeDaysAgo = dateToDay(moment().subtract(3, 'days'));
    const checkMapForDelete = (value, key, map) => {
      if (key <= threeDaysAgo) {
        map.delete(key);
      }
    };
    this[sScheduleDates].forEach(checkMapForDelete);
    this[sScheduleDatesSpecial].forEach(checkMapForDelete);

    // write data to cache
    return this.Cache.Set('dates', {
      // serialize Map as an array
      dates: [...this[sScheduleDates]],
      special: [...this[sScheduleDatesSpecial]],
    }, 60 * 60 * 6);
  }

  /**
   * Check the cache has been setup. Returns resolved Promise once it is
   */
  CheckCacheStatus() {
    if (this[sCachePopulated]) return Promise.resolve();

    return new Promise((resolve) => {
      // add our Promise resolve to our callback stack
      this[sCacheWaitCallbacks].push(resolve);

      // if we're the first to request the cache state, then actually do it...
      if (this[sCacheWaitCallbacks].length === 1) {
        // request cache read
        this.ReadFromCache().then(() => {
          // fire callbacks if they are present
          if (this[sCacheWaitCallbacks] && this[sCacheWaitCallbacks].length) {
            this[sCacheWaitCallbacks].forEach((cb) => {
              cb();
            });
            this[sCacheWaitCallbacks] = [];
          }
        });
      }
    });
  }

  /**
   * Read data from cache
   */
  ReadFromCache() {
    return this.Cache.Get('dates').then((data) => {
      // mark cache as populated so we don't try and read again
      this[sCachePopulated] = true;

      // if data is valid, update our internal state
      if (data && data.dates && data.special) {
        this[sScheduleDates] = Object.getPrototypeOf(data.dates) === Map.prototype ? data.dates : new Map(data.dates);
        this[sScheduleDatesSpecial] = Object.getPrototypeOf(data.special) === Map.prototype ? data.special : new Map(data.special);
      }

      return Promise.resolve(data);
    });
  }
}

module.exports = Schedule;
<commit_msg>[!] Fix not setting date ranges correct * Missing named argument passing to SetDate function<commit_after>const moment = require('moment-timezone');
const Log = require('./debugPrint');
const settings = require('./settings');
const Cache = require('./cache');

const sScheduleDates = Symbol('Schedule Regular Dates');
const sScheduleDatesSpecial = Symbol('Schedule Special Dates');
const sCache = Symbol('Cache');
const sCachePopulated = Symbol('Cache Populated State');
const sCacheWaitCallbacks = Symbol('Cache Initiated Callbacks');
const sCacheTimer = Symbol('Cache Timer');

/**
 * @typedef ScheduleData
 * @type Object
 * @property {Moment} date Date this schedule data applies to
 * @property {Moment} openingTime Opening time for this date
 * @property {Moment} closingTime Closing time for this date
 * @property {String} type Whether this schedule data refers to an "Operating", "Closed" or "Refurbishment" status
 * @property {SpecialScheduleData[]} special Won't exist if no special times exist for this date
 */

/**
 * @typedef SpecialScheduleData
 * @type Object
 * @property {Moment} openingTime Opening time for this special schedule data
 * @property {Moment} closingTime Closing time for this special schedule data
 * @property {String} type Type of special schedule this is (eg. "Extra Magic Hours")
 */

function dateToDay(date) {
  // calculate the day since Unix Epoch
  //  .unix returns in UTC, so we convert to minutes and add on the utcOffset (then convert from minutes to days)
  //  finally we Math.floor to round downwards to get the current day as an integer
  return Math.floor(((date.unix() / 60) + date.utcOffset()) / 1440);
}

function parseDateTime(dateObject, varName) {
  let returnDate = dateObject;

  // check if it's already a valid Moment object
  if (!moment.isMoment(dateObject)) {
    // try and parse if this is a string
    returnDate = moment(dateObject, [
      moment.ISO_8601,
      settings.DefaultTimeFormat,
      settings.DefaultDateFormat,
      'YYYY-MM-DD',
    ]);

    // check if we ended up with a valid timestamp
    if (!returnDate.isValid()) {
      Log(`Invalid scheduleData.${varName}:`, returnDate);
      return false;
    }
  }

  // we got this far, success! return the new Moment object (or the original one if it was always good!)
  return returnDate;
}

/**
 * Schedule class to hold opening and closing times for parks and rides etc.
 * Supports standard and "special" opening times
 * @class
 */
class Schedule {
  /**
   * Create a new Schedule object
   * @param {Object} options
   * @param {String} options.id Unique Schedule ID for caching keys
   */
  constructor(options = {}) {
    // use Map for better structure (int -> data)
    //  int is the number of days since Unix Epoch
    this[sScheduleDates] = new Map();
    // also one for special hours (this is actually int -> data[] to support multiple special times)
    this[sScheduleDatesSpecial] = new Map();

    // setup our cache object
    if (options.id === undefined) throw new Error('No id passed for schedule object');
    this[sCache] = new Cache({
      prefix: `schedule_${options.id}`,
    });
    // store flag of whether we've populated ourselves with cache data yet
    this[sCachePopulated] = false;
    this[sCacheWaitCallbacks] = [];

    // try and populate cache
    this.CheckCacheStatus();
  }

  /**
   * Set schedule data for a date
   * @param {Object} scheduleData
   * @param {Moment|String} [scheduleData.date=scheduleData.openingTime] Moment.js date object (or a valid date String to be parsed by Moment JS). Will use openingTime if this is not supplied
   * @param {Moment|String} [scheduleData.openingTime] Moment.js date object of this day's opening time (or a valid date String to be parsed by Moment JS) (can be ignored if type is Closed)
   * @param {Moment|String} [scheduleData.closingTime] Moment.js date object of this day's closing time (or a valid date String to be parsed by Moment JS) (can be ignored if type is Closed)
   * @param {Boolean} [scheduleData.specialHours=false] Is this schedule data part of schedule special hours?
   * @param {String} [scheduleData.type=Operating] The schedule type. Normal schedules should always be "Operating", "Closed" or "Refurbishment". Special schedules can be any String (eg. Extra Magic Hours).
   * @returns {Boolean} success Returns true if the operation was a success and the data was actually changed
   */
  SetDate({
    // the day to set the schedule data for
    date = null,
    // opening time for this day
    openingTime = null,
    // closing time for this day
    closingTime = null,
    // is this special hours data? (default: false)
    specialHours = false,
    // the type of this schedule date (default: Operating)
    type = 'Operating',
  }) {
    let newDate = date.clone();
    let newOpeningTime = openingTime !== null ? openingTime.clone() : null;
    let newClosingTime = closingTime !== null ? closingTime.clone() : null;

    // if we haven't been supplied a date, use the opening time
    if (!newDate) newDate = openingTime.clone();

    // check our date is a valid momentjs object
    newDate = parseDateTime(newDate, 'date');

    // special case, if this is a closed date, support not passing in opening and closing times
    if (type === 'Closed') {
      if (!openingTime) newOpeningTime = newDate.startOf('day');
      if (!closingTime) newClosingTime = newDate.endOf('day');
    }

    newOpeningTime = parseDateTime(newOpeningTime, 'openingTime');
    newClosingTime = parseDateTime(newClosingTime, 'closingTime');

    // if any of our dates are invalid, return false
    if (!newDate || !newOpeningTime || !newClosingTime) return false;

    // calculate the days since Unix Epoch
    const day = dateToDay(newDate);

    // make sure opening and closing times are in the correct day!
    const todaySet = {
      year: newDate.year(),
      month: newDate.month(),
      date: newDate.date(),
    };
    newOpeningTime.set(todaySet);
    newClosingTime.set(todaySet);

    // work out if the closing time is in the following day
    if (newClosingTime.isBefore(newOpeningTime)) {
      // add 1 day if the closing time comes before the opening time (implying it's open past midnight!)
      newClosingTime.add(1, 'day');
    }

    // build schedule data object and add it to our schedule map
    if (!specialHours) {
      // check our schedule type is sane
      if (type !== 'Operating' && type !== 'Closed' && type !== 'Refurbishment') {
        Log(`Tried to use invalid schedule type ${type} for standard schedule data (must be Operating, Closed or Refurbishment)`);
        return false;
      }

      const newScheduleData = {
        date: newDate,
        openingTime: newOpeningTime,
        closingTime: newClosingTime,
        type,
      };

      // check if we already have this data for this day (don't invalidate cache etc if it hasn't changed)
      if (this[sScheduleDates].has(day)) {
        const checkDirtyObj = this[sScheduleDates].get(day);
        if (
          checkDirtyObj.date === newScheduleData.date
          && checkDirtyObj.openingTime === newScheduleData.openingTime
          && checkDirtyObj.closingTime === newScheduleData.closingTime
          && checkDirtyObj.type === newScheduleData.type) {
          // data is identical to existing object, don't update
          return false;
        }
      }

      // set this day's schedule data
      this[sScheduleDates].set(day, newScheduleData);

      // we have new data, so mark it as dirty to get cached
      this.RequestCache();
    } else {
      // special hours can't be Operating or Closed, that is for normal hours
      if (type === 'Operating' || type === 'Closed') {
        Log(`Tried to use invalid schedule type ${type} for special schedule data (can't be Operating or Closed)`);
        return false;
      }

      // add a new special hours array if we don't already have one
      if (!this[sScheduleDatesSpecial].has(day)) {
        this[sScheduleDatesSpecial].set(day, []);

        // we have new data, so mark it as dirty to get cached
        this.RequestCache();
      }

      const newSpecialScheduleData = {
        openingTime,
        closingTime,
        type,
      };

      // check we don't already have this special data in our array
      const newDataStringified = JSON.stringify(newSpecialScheduleData);
      const specialDayArray = this[sScheduleDatesSpecial].get(day);
      for (let i = 0; i < specialDayArray.length; i += 1) {
        if (JSON.stringify(specialDayArray[i]) === newDataStringified) {
          // this object already exists, so bail out
          return false;
        }
      }

      // add our new data to the specials array
      specialDayArray.push(newSpecialScheduleData);

      // we have new data, so mark it as dirty to get cached
      this.RequestCache();
    }

    return true;
  }

  /**
   * Set a range of dates with the same schedule data
   * @param {Object} scheduleData
   * @param {Moment|String} scheduleData.startDate Moment.js date object to start schedule date range (or a valid date String to be parsed by Moment JS)
   * @param {Moment|String} scheduleData.endDate Moment.js date object to end schedule date range (or a valid date String to be parsed by Moment JS)
   * @param {Moment|String} scheduleData.openingTime Moment.js date object of this day's opening time (or a valid date String to be parsed by Moment JS)
   * @param {Moment|String} scheduleData.closingTime Moment.js date object of this day's closing time (or a valid date String to be parsed by Moment JS)
   * @param {Boolean} [scheduleData.specialHours=false] Is this schedule data part of schedule special hours?
   * @param {String} [scheduleData.type=Operating] The schedule type. Normal schedules should always be "Operating", "Closed" or "Refurbishment". Special schedules can be any String (eg. Extra Magic Hours).
   * @returns {Boolean} success
   */
  SetRange({
    // first date of the range to set schedule for
    startDate = null,
    // first date of the range to set schedule for
    endDate = null,
    // opening time for this day
    openingTime = null,
    // closing time for this day
    closingTime = null,
    // is this special hours data? (default: false)
    specialHours = false,
    // the type of this schedule date (default: Operating)
    type = 'Operating',
  }) {
    // check our input dates are valid
    const newStartDate = parseDateTime(startDate, 'startDate');
    const newEndDate = parseDateTime(endDate, 'endDate');
    const newOpeningTime = parseDateTime(openingTime, 'openingTime');
    const newClosingTime = parseDateTime(closingTime, 'closingTime');

    // if any of our dates are invalid, return false
    if (!newStartDate || !newEndDate || !newOpeningTime || !newClosingTime) return false;

    // if any of our dates result in invalid data, return false
    let retValue = true;

    // add each day using SetDate
    for (let m = newStartDate; m.isSameOrBefore(newEndDate); m.add(1, 'days')) {
      // retValue AND= means this becomes false with any one failed result
      //  if we do fail, we also just keep going to try and get as much done as possible :)
      const successfulSet = this.SetDate({
        date: m,
        openingTime: newOpeningTime,
        closingTime: newClosingTime,
        specialHours,
        type,
      });

      retValue = retValue && successfulSet;
    }

    return retValue;
  }

  /**
   * Get schedule data for a specific date
   * @param {Object} dateData
   * @param {Moment|String} dateData.date Moment.js date object to fetch schedule data for (or a valid date String to be parsed by Moment JS)
   * @return {ScheduleData} scheduleResult Can be false if no data exists for the requested date
   */
  GetDate({
    date = null,
  }) {
    // check our date is valid
    const newDate = parseDateTime(date, 'date');
    if (!newDate) return undefined;

    // do we have this day in our schedule data?
    const day = dateToDay(newDate);

    if (!this[sScheduleDates].has(day)) return undefined;

    const dayData = this[sScheduleDates].get(day);
    // copy data into the return object (otherwise we end up modifying the actual date data!)
    const returnObject = {
      date: dayData.date,
      openingTime: dayData.openingTime,
      closingTime: dayData.closingTime,
      type: dayData.type,
    };

    // add special schedules if we have any!
    if (this[sScheduleDatesSpecial].has(day)) {
      returnObject.special = this[sScheduleDatesSpecial].get(day);
    }

    return returnObject;
  }

  /**
   * Get schedule data for a range of dates
   * @param {Object} dateData
   * @param {Moment|String} dateData.startDate Moment.js date object to fetch schedule data from (or a valid date String to be parsed by Moment JS)
   * @param {Moment|String} dateData.endDate Moment.js date object to fetch schedule data from (or a valid date String to be parsed by Moment JS)
   * @return {ScheduleData[]} scheduleResult Can be an empty array if there is no valid data (won't be null)
   */
  GetDateRange({
    startDate = null,
    endDate = null,
  }) {
    // check start and end date are valid
    const newStartDate = parseDateTime(startDate, 'startDate');
    const newEndDate = parseDateTime(endDate, 'endDate');
    if (!newStartDate || !newEndDate) return [];

    // fetch each day of the range and add it to our result
    const returnArray = [];
    for (let m = newStartDate; m.isSameOrBefore(newEndDate); m.add(1, 'days')) {
      const dateSchedule = this.GetDate({
        date: m,
      });
      if (dateSchedule !== undefined) {
        returnArray.push(dateSchedule);
      }
    }

    return returnArray;
  }

  /**
   * Get our cache object
   */
  get Cache() {
    return this[sCache];
  }

  /**
   * Request a cache to happen at the next best opportunity
   */
  RequestCache() {
    // if other caching was queued up, skip it
    if (this[sCacheTimer] !== undefined) {
      clearTimeout(this[sCacheTimer]);
    }

    // schedule caching in half a second
    this[sCacheTimer] = setTimeout(this.WriteToCache.bind(this), 500);
  }

  /**
   * Write schedule data to the cache
   */
  WriteToCache() {
    // if any timer exists for caching, clear it out, we're caching right now
    if (this[sCacheTimer] !== undefined) {
      clearTimeout(this[sCacheTimer]);
    }

    // delete old data (older than 3 days to be safe)
    const threeDaysAgo = dateToDay(moment().subtract(3, 'days'));
    const checkMapForDelete = (value, key, map) => {
      if (key <= threeDaysAgo) {
        map.delete(key);
      }
    };
    this[sScheduleDates].forEach(checkMapForDelete);
    this[sScheduleDatesSpecial].forEach(checkMapForDelete);

    // write data to cache
    return this.Cache.Set('dates', {
      // serialize Map as an array
      dates: [...this[sScheduleDates]],
      special: [...this[sScheduleDatesSpecial]],
    }, 60 * 60 * 6);
  }

  /**
   * Check the cache has been setup. Returns resolved Promise once it is
   */
  CheckCacheStatus() {
    if (this[sCachePopulated]) return Promise.resolve();

    return new Promise((resolve) => {
      // add our Promise resolve to our callback stack
      this[sCacheWaitCallbacks].push(resolve);

      // if we're the first to request the cache state, then actually do it...
      if (this[sCacheWaitCallbacks].length === 1) {
        // request cache read
        this.ReadFromCache().then(() => {
          // fire callbacks if they are present
          if (this[sCacheWaitCallbacks] && this[sCacheWaitCallbacks].length) {
            this[sCacheWaitCallbacks].forEach((cb) => {
              cb();
            });
            this[sCacheWaitCallbacks] = [];
          }
        });
      }
    });
  }

  /**
   * Read data from cache
   */
  ReadFromCache() {
    return this.Cache.Get('dates').then((data) => {
      // mark cache as populated so we don't try and read again
      this[sCachePopulated] = true;

      // if data is valid, update our internal state
      if (data && data.dates && data.special) {
        this[sScheduleDates] = Object.getPrototypeOf(data.dates) === Map.prototype ? data.dates : new Map(data.dates);
        this[sScheduleDatesSpecial] = Object.getPrototypeOf(data.special) === Map.prototype ? data.special : new Map(data.special);
      }

      return Promise.resolve(data);
    });
  }
}

module.exports = Schedule;
<|endoftext|>


<commit_before>/**
 * Alloy
 * Copyright (c) 2012 by Appcelerator, Inc. All Rights Reserved.
 * See LICENSE for more information on licensing.
 */

exports.cliVersion = '>=3.X';
var SILENT = true;

exports.init = function (logger, config, cli, appc) {
	var path = require('path'),
		fs = require('fs'),
		afs = appc.fs,
		i18n = appc.i18n(__dirname),
		__ = i18n.__,
		__n = i18n.__n,
		pkginfo = appc.pkginfo.package(module),
		exec = require('child_process').exec,
		spawn = require('child_process').spawn,
		parallel = appc.async.parallel;

		if(!process.env.sdk) {
			process.env.sdk = cli.sdk.name;
		}

	function run(deviceFamily, deployType, target, finished, silent) {
		var appDir = path.join(cli.argv['project-dir'], 'app');
		if (!afs.exists(appDir)) {
			logger.info(__('Project not an Alloy app, continuing'));
			finished();
			return;
		}
		logger.info(__('Found Alloy app in %s', appDir.cyan));

		// TODO: Make this check specific to a TiSDK version
		// create a .alloynewcli file to tell old plugins not to run
		var buildDir = path.join(cli.argv['project-dir'], 'build');
		if (!afs.exists(buildDir)) {
			fs.mkdirSync(buildDir);
		}
		fs.writeFileSync(path.join(buildDir, '.alloynewcli'), '');

		var cRequire = afs.resolvePath(__dirname, '..', 'Alloy', 'commands', 'compile', 'index.js'),
			config = {
				platform: /(?:iphone|ipad)/.test(cli.argv.platform) ? 'ios' : cli.argv.platform,
				version: '0',
				simtype: 'none',
				devicefamily: /(?:iphone|ios)/.test(cli.argv.platform) ? deviceFamily : 'none',
				deploytype: deployType || cli.argv['deploy-type'] || 'development',
				target: target
			};
		if(silent) {
			// turn off all logging output for code analyzer build hook
			config.noBanner = 'true';
			config.logLevel = '-1';
		}

		config = Object.keys(config).map(function (c) {
			return c + '=' + config[c];
		}).join(',');

		if (afs.exists(cRequire)) {
			// we're being invoked from the actual alloy directory!
			// no need to subprocess, just require() and run
			var origLimit = Error.stackTraceLimit;
			Error.stackTraceLimit = Infinity;
			try {
				require(cRequire)({}, {
					config: config,
					outputPath: cli.argv['project-dir'],
					_version: pkginfo.version
				});
			} catch (e) {
				logger.error(__('Alloy compiler failed'));
				e.toString().split('\n').forEach(function (line) {
					if (line) { logger.error(line); }
				});
				process.exit(1);
			}
			Error.stackTraceLimit = origLimit;
			finished();
		} else {
			// we have no clue where alloy is installed, so we're going to subprocess
			// alloy and hope it's in the system path or a well known place
			var paths = {};
			parallel(this, ['alloy', 'node'].map(function (bin) {
				return function (done) {
					var envName = 'ALLOY_' + (bin === 'node' ? 'NODE_' : '') + 'PATH';

					paths[bin] = process.env[envName];
					if (paths[bin]) {
						done();
					} else if (process.platform === 'win32' && bin === 'alloy') {
						paths.alloy = 'alloy.cmd';
						done();
					} else {
						exec('which ' + bin, function (err, stdout, strerr) {
							if (!err) {
								paths[bin] = stdout.trim();
								done();
							} else {
								parallel(this, [
									'/usr/local/bin/' + bin,
									'/opt/local/bin/' + bin,
									path.join(process.env.HOME, 'local/bin', bin),
									'/opt/bin/' + bin,
									'/usr/bin/' + bin
								].map(function (p) {
									return function (cb) {
										if (afs.exists(p)) { paths[bin] = p; }
										cb();
									};
								}), done);
							}
						});
					}
				};
			}), function () {

				// compose alloy command execution
				var cmd = [paths.node, paths.alloy, 'compile', appDir, '--config', config];
				if (cli.argv['no-colors'] || cli.argv['color'] === false) { cmd.push('--no-colors'); }

				// process each line of output from alloy
				function checkLine(line) {
					var re = new RegExp(
						'(?:\u001b\\[\\d+m)?\\[?(' +
						logger.getLevels().join('|') +
						')\\]?\s*(?:\u001b\\[\\d+m)?(.*)', 'i'
					);
					if (line) {
						var m = line.match(re);
						if (m) {
							logger[m[1].toLowerCase()](m[2].trim());
						} else {
							logger.debug(line);
						}
					}
				}

				// execute alloy in os-specific manner
				var child;
				if (process.platform === 'win32') {
					cmd.shift();
					logger.info(__('Executing Alloy compile: %s',
						['cmd','/s','/c'].concat(cmd).join(' ').cyan));

					// arg processing from https://github.com/MarcDiethelm/superspawn
					child = spawn('cmd', [['/s', '/c', '"' +
						cmd.map(function(a) {
							if (/^[^"].* .*[^"]/.test(a)) return '"'+a+'"'; return a;
						}).join(" ") + '"'].join(" ")], {
							stdio: 'inherit',
							windowsVerbatimArguments: true
						}
					);
				} else {
					logger.info(__('Executing Alloy compile: %s', cmd.join(' ').cyan));
					child = spawn(cmd.shift(), cmd);
					child.stdout.on('data', function (data) {
						data.toString().split('\n').forEach(checkLine);
					});
					child.stderr.on('data', function (data) {
						data.toString().split('\n').forEach(checkLine);
					});
				}

				// handle the completion of alloy, success or otherwise
				child.on('exit', function (code) {
					if (code) {
						logger.error(__('Alloy compiler failed'));
						process.exit(1);
					} else {
						logger.info(__('Alloy compiler completed successfully'));
					}
					finished();
				});

			});
		}
	}

	cli.addHook('build.pre.compile', function (build, finished) {
		var deployType = build.deployType,
			target = build.target;

		run(build.deviceFamily, deployType, target, finished);
	});

	cli.addHook('codeprocessor.pre.run', function (build, finished) {
		run('none', 'development', undefined, finished, SILENT);
	});
};
<commit_msg>ignoring ti.alloy hooks plugin as it's auto generated<commit_after>/**
 * Alloy
 * Copyright (c) 2012 by Appcelerator, Inc. All Rights Reserved.
 * See LICENSE for more information on licensing.
 */

exports.cliVersion = '>=3.X';
var SILENT = true;

exports.init = function (logger, config, cli, appc) {
	var path = require('path'),
		fs = require('fs'),
		afs = appc.fs,
		i18n = appc.i18n(__dirname),
		__ = i18n.__,
		__n = i18n.__n,
		pkginfo = appc.pkginfo.package(module),
		exec = require('child_process').exec,
		spawn = require('child_process').spawn,
		parallel = appc.async.parallel;

		if(!process.env.sdk) {
			process.env.sdk = cli.sdk.name;
		}

	function run(deviceFamily, deployType, target, finished, silent) {
		var appDir = path.join(cli.argv['project-dir'], 'app');
		if (!afs.exists(appDir)) {
			logger.info(__('Project not an Alloy app, continuing'));
			finished();
			return;
		}
		logger.info(__('Found Alloy app in %s', appDir.cyan));

		// TODO: Make this check specific to a TiSDK version
		// create a .alloynewcli file to tell old plugins not to run
		var buildDir = path.join(cli.argv['project-dir'], 'build');
		if (!afs.exists(buildDir)) {
			fs.mkdirSync(buildDir);
		}
		fs.writeFileSync(path.join(buildDir, '.alloynewcli'), '');

		var cRequire = afs.resolvePath(__dirname, '..', 'Alloy', 'commands', 'compile', 'index.js'),
			config = {
				platform: /(?:iphone|ipad)/.test(cli.argv.platform) ? 'ios' : cli.argv.platform,
				version: '0',
				simtype: 'none',
				devicefamily: /(?:iphone|ios)/.test(cli.argv.platform) ? deviceFamily : 'none',
				deploytype: deployType || cli.argv['deploy-type'] || 'development',
				target: target
			};
		if(silent) {
			// turn off all logging output for code analyzer build hook
			config.noBanner = 'true';
			config.logLevel = '-1';
		}

		config = Object.keys(config).map(function (c) {
			return c + '=' + config[c];
		}).join(',');

		if (afs.exists(cRequire)) {
			// we're being invoked from the actual alloy directory!
			// no need to subprocess, just require() and run
			var origLimit = Error.stackTraceLimit;
			Error.stackTraceLimit = Infinity;
			try {
				require(cRequire)({}, {
					config: config,
					outputPath: cli.argv['project-dir'],
					_version: pkginfo.version
				});
			} catch (e) {
				logger.error(__('Alloy compiler failed'));
				e.toString().split('\n').forEach(function (line) {
					if (line) { logger.error(line); }
				});
				process.exit(1);
			}
			Error.stackTraceLimit = origLimit;
			finished();
		} else {
			// we have no clue where alloy is installed, so we're going to subprocess
			// alloy and hope it's in the system path or a well known place
			var paths = {};
			parallel(this, ['alloy', 'node'].map(function (bin) {
				return function (done) {
					var envName = 'ALLOY_' + (bin === 'node' ? 'NODE_' : '') + 'PATH';

					paths[bin] = process.env[envName];
					if (paths[bin]) {
						done();
					} else if (process.platform === 'win32' /*&& bin === 'alloy'*/) {
						paths.alloy = 'alloy.cmd';
						done();
					} else {
						exec('which ' + bin, function (err, stdout, strerr) {
							if (!err) {
								paths[bin] = stdout.trim();
								done();
							} else {
								parallel(this, [
									'/usr/local/bin/' + bin,
									'/opt/local/bin/' + bin,
									path.join(process.env.HOME, 'local/bin', bin),
									'/opt/bin/' + bin,
									'/usr/bin/' + bin
								].map(function (p) {
									return function (cb) {
										if (afs.exists(p)) { paths[bin] = p; }
										cb();
									};
								}), done);
							}
						});
					}
				};
			}), function () {

				// compose alloy command execution
				var cmd = [paths.node, paths.alloy, 'compile', appDir, '--config', config];
				if (cli.argv['no-colors'] || cli.argv['color'] === false) { cmd.push('--no-colors'); }

				// process each line of output from alloy
				function checkLine(line) {
					var re = new RegExp(
						'(?:\u001b\\[\\d+m)?\\[?(' +
						logger.getLevels().join('|') +
						')\\]?\s*(?:\u001b\\[\\d+m)?(.*)', 'i'
					);
					if (line) {
						var m = line.match(re);
						if (m) {
							logger[m[1].toLowerCase()](m[2].trim());
						} else {
							logger.debug(line);
						}
					}
				}

				// execute alloy in os-specific manner
				var child;
				if (process.platform === 'win32') {
					cmd.shift();
					logger.info(__('Executing Alloy compile: %s',
						['cmd','/s','/c'].concat(cmd).join(' ').cyan));

					// arg processing from https://github.com/MarcDiethelm/superspawn
					child = spawn('cmd', [['/s', '/c', '"' +
						cmd.map(function(a) {
							if (/^[^"].* .*[^"]/.test(a)) return '"'+a+'"'; return a;
						}).join(" ") + '"'].join(" ")], {
							stdio: 'inherit',
							windowsVerbatimArguments: true
						}
					);
				} else {
					logger.info(__('Executing Alloy compile: %s', cmd.join(' ').cyan));
					child = spawn(cmd.shift(), cmd);
					child.stdout.on('data', function (data) {
						data.toString().split('\n').forEach(checkLine);
					});
					child.stderr.on('data', function (data) {
						data.toString().split('\n').forEach(checkLine);
					});
				}

				// handle the completion of alloy, success or otherwise
				child.on('exit', function (code) {
					if (code) {
						logger.error(__('Alloy compiler failed'));
						process.exit(1);
					} else {
						logger.info(__('Alloy compiler completed successfully'));
					}
					finished();
				});

			});
		}
	}

	cli.addHook('build.pre.compile', function (build, finished) {
		var deployType = build.deployType,
			target = build.target;

		run(build.deviceFamily, deployType, target, finished);
	});

	cli.addHook('codeprocessor.pre.run', function (build, finished) {
		run('none', 'development', undefined, finished, SILENT);
	});
};
<|endoftext|>


<commit_before>"use strict";

var _                   = require('lodash')
  , Promise             = require('bluebird')
  , path                = require('path')
  , fs                  = require('fs-extra')
  , createOutputStream  = require('create-output-stream')
  , config              = require('config-url')
  , urljoin             = require('url-join')
  , contentType         = require('content-type')
  , parser              = require('parse-cache-control')
  , mime                = require('mime-type/with-db')
  , Headers             = require('../headers')
  ;

Promise.config({ cancellation: true });

// todo [akamel] replicate this concept in cloud and cache
const __writes = {};
const __pending_writes = {};

class Storage {
  constructor(id, options) {
    options = _.defaults({}, options, {
        dir : '.tmp'
    });

    this.id = id;

    this.body_name = path.join(options.dir, id);
    this.head_name = path.join(options.dir, id + '.head');
  }

  write(headers, stream, options) {
    // current approach is to ignore all writes w/ same id until this one is done
    if (__writes[this.id]) {
      return Promise.resolve();
    }

    var ret =  Promise
                .all([
                    // note [akamel] don't use 'on-finished'/ results in partial file [when file is big]
                    Promise.fromCallback((cb) => {
                      var enc   = mime.charset(headers.get('content-type'))
                        , to    = createOutputStream(this.body_name, { defaultEncoding : enc || 'binary' })
                        , body  = stream.pipe(to)
                        ;

                      to.on('error', cb);
                      // https://github.com/nodejs/node-v0.x-archive/issues/6438
                      // writestream should fsync(2) before close(2)
                      to.once('open', (fd) => {
                        to.once('close', () => {
                          fs.fsync(fd, () => {
                            cb();
                          });
                        });
                      });
                    })
                  , Promise.fromCallback((cb) => {
                      let to = createOutputStream(this.head_name, { defaultEncoding : 'utf-8' })

                      to.write(JSON.stringify(options.metadata));

                      to.on('error', cb);
                      // https://github.com/nodejs/node-v0.x-archive/issues/6438
                      // writestream should fsync(2) before close(2)
                      to.once('open', (fd) => {
                        to.once('close', () => {
                          fs.fsync(fd, () => {
                            cb();
                          });
                        });
                      });
                    })
                  // , Promise.promisify(fs.outputJson)(this.head_name, options.metadata)
                ])
                .finally(() => {
                  delete __writes[this.id];
                });

    __writes[this.id] = ret;

    return ret;
  }

  metadata() {
    return Promise
            .resolve(__writes[this.id])
            .then(() => {
              var p$head  = Promise.promisify(fs.readJson)(this.head_name).catchThrow((err) => { console.error('metadata/readJson', this.id, err); })
                , p$stats = Promise.promisify(fs.stat)(this.head_name).catchThrow((err) => { console.error('metadata/stat', this.id, err); })
                ;

              return Promise
                      .all([ p$head, p$stats ])
                      .spread((result, stats) => {
                        var headers       = new Headers(result.headers)
                          , content_type  = headers.get('content-type')
                          , cache         = parser(headers.get('cache-control')) || {}
                          , age           = (Date.now() - stats.ctime) / 1000 //in seconds
                          ;

                        // todo [akamel] return status-code as a seperate property here as well
                        return {
                            id              : this.id
                          , url             : urljoin(config.getUrl('log'), 'read', this.id)
                          , headers         : headers.toObject()
                          , 'status-code'   : result['status-code']
                          , contentType     : !_.isEmpty(content_type) ? contentType.parse(content_type) : { }
                          , age             : age
                          , ttl             : cache['max-age'] - age
                          , 'max-age'       : cache['max-age']
                        };
                      });
            });
  }

  read() {
    return Promise
            .resolve(__writes[this.id])
            .then(() => {
              return Promise.resolve(fs.createReadStream(this.body_name)).catchThrow((err) => { console.error('read/createReadStream', this.id, err); });
            });
  }

  // todo [akamel] wait for eads to end before deleting
  delete() {
    fs.remove(this.body_name, (err) => {
      if (err) {
        console.log('error deleting file:', this.body_name, err);
      }
    });

    fs.remove(this.head_name, (err) => {
      if (err) {
        console.log('error deleting file:', this.head_name, err);
      }
    });
  }
}

module.exports = Storage;<commit_msg>move writes<commit_after>"use strict";

var _                   = require('lodash')
  , Promise             = require('bluebird')
  , path                = require('path')
  , fs                  = require('fs-extra')
  , createOutputStream  = require('create-output-stream')
  , config              = require('config-url')
  , urljoin             = require('url-join')
  , contentType         = require('content-type')
  , parser              = require('parse-cache-control')
  , mime                = require('mime-type/with-db')
  , Headers             = require('../headers')
  ;

Promise.config({ cancellation: true });

// todo [akamel] replicate this concept in cloud and cache
const __writes = {};
const __pending_writes = {};

class Storage {
  constructor(id, options) {
    options = _.defaults({}, options, {
        dir : '.tmp'
    });

    this.id = id;

    this.body_name = path.join(options.dir, id);
    this.head_name = path.join(options.dir, id + '.head');
  }

  write(headers, stream, options) {
    // current approach is to ignore all writes w/ same id until this one is done
    if (__writes[this.id]) {
      return Promise.resolve();
    }

    var ret =  Promise
                .all([
                    // note [akamel] don't use 'on-finished'/ results in partial file [when file is big]
                    Promise.fromCallback((cb) => {
                      var enc   = mime.charset(headers.get('content-type'))
                        , to    = createOutputStream(this.body_name, { defaultEncoding : enc || 'binary' })
                        ;

                      to.on('error', cb);
                      // https://github.com/nodejs/node-v0.x-archive/issues/6438
                      // writestream should fsync(2) before close(2)
                      to.once('open', (fd) => {
                        to.once('close', () => {
                          fs.fsync(fd, cb);
                        });
                      });

                      stream.pipe(to);
                    })
                  , Promise.fromCallback((cb) => {
                      let to = createOutputStream(this.head_name, { defaultEncoding : 'utf-8' })

                      to.on('error', cb);
                      // https://github.com/nodejs/node-v0.x-archive/issues/6438
                      // writestream should fsync(2) before close(2)
                      to.once('open', (fd) => {
                        to.once('close', () => {
                          fs.fsync(fd, cb);
                        });
                      });

                      to.write(JSON.stringify(options.metadata));
                    })
                  // , Promise.promisify(fs.outputJson)(this.head_name, options.metadata)
                ])
                .finally(() => {
                  delete __writes[this.id];
                });

    __writes[this.id] = ret;

    return ret;
  }

  metadata() {
    return Promise
            .resolve(__writes[this.id])
            .then(() => {
              var p$head  = Promise.promisify(fs.readJson)(this.head_name).catchThrow((err) => { console.error('metadata/readJson', this.id, err); })
                , p$stats = Promise.promisify(fs.stat)(this.head_name).catchThrow((err) => { console.error('metadata/stat', this.id, err); })
                ;

              return Promise
                      .all([ p$head, p$stats ])
                      .spread((result, stats) => {
                        var headers       = new Headers(result.headers)
                          , content_type  = headers.get('content-type')
                          , cache         = parser(headers.get('cache-control')) || {}
                          , age           = (Date.now() - stats.ctime) / 1000 //in seconds
                          ;

                        // todo [akamel] return status-code as a seperate property here as well
                        return {
                            id              : this.id
                          , url             : urljoin(config.getUrl('log'), 'read', this.id)
                          , headers         : headers.toObject()
                          , 'status-code'   : result['status-code']
                          , contentType     : !_.isEmpty(content_type) ? contentType.parse(content_type) : { }
                          , age             : age
                          , ttl             : cache['max-age'] - age
                          , 'max-age'       : cache['max-age']
                        };
                      });
            });
  }

  read() {
    return Promise
            .resolve(__writes[this.id])
            .then(() => {
              return Promise.resolve(fs.createReadStream(this.body_name)).catchThrow((err) => { console.error('read/createReadStream', this.id, err); });
            });
  }

  // todo [akamel] wait for eads to end before deleting
  delete() {
    fs.remove(this.body_name, (err) => {
      if (err) {
        console.log('error deleting file:', this.body_name, err);
      }
    });

    fs.remove(this.head_name, (err) => {
      if (err) {
        console.log('error deleting file:', this.head_name, err);
      }
    });
  }
}

module.exports = Storage;<|endoftext|>


<commit_before>var $loader = {
	_ie: false,
	_ieVersion: null,
	_embedCounter: null,

	_loader: null,
	_frameworkURL: null,
	_routToScriptURL: null,

	_appDefinition: null,
	_resources: null,
	_resourceIDs: null,
	_resourcesLoaded: null,

	_deepResources: null,
	_deepResourcesLoaded: null,

	_resourcesLoadedMap: null,

	_waitForReadyInterval: null,
	_attempts: null,
	_allLoadCallback: null,
	_loadFailCallback: null,

	_allowLoadAll: null,
	_skipPreload: null,

	setLoader: function(loader) {
		$loader._loader = loader;

		if(!$loader._waitForReadyInterval) {
			$loader._waitForReadyInterval = setInterval($loader._waitForReady, 20);
		}
	},

	init: function(frameworkURL, routToScriptURL, loadScriptURL, resources, appDefinition, skipPreload) {
		$loader._frameworkURL = frameworkURL;
		$loader._routToScriptURL = routToScriptURL;

		$loader._appDefinition = appDefinition;

		$loader._resourceIDs = {};

		$loader._resources = resources;
		$loader._resources.push($loader._routToScriptURL);
		$loader._resourceIDs[$loader._routToScriptURL] = 'jComboInitScript';

		$loader._resourcesLoaded = [];
		$loader._deepResources = {};
		$loader._deepResourcesLoaded = {};
		$loader._resourcesLoadedMap = {};
		$loader._allowLoadAll = true;

		$loader._allLoadCallback = null;
		$loader._loadFailCallback = null;
		$loader._attempts = 0;
		$loader._embedCounter = 0;

		if(/MSIE (\d+\.\d+);/.test(navigator.userAgent)) {
			$loader._ie = true;
			$loader._ieVersion = new Number(RegExp.$1)
		}

		$loader._skipPreload = skipPreload;

		if(skipPreload) {
			$loader._waitForReadyInterval = setInterval($loader._waitForReady, 20);
		} else {
			$loader.scriptTag(loadScriptURL, 'text/javascript');
		}
	},

	getAppDefinition: function() {
		return $loader._appDefinition;
	},

	_waitForReady: function() {
		var head = document.getElementsByTagName('head')[0];

		if(head && document.body) {
			clearInterval($loader._waitForReadyInterval);
			if($loader._skipPreload) {
				$loader.loadAll(function() {
					$loader._embedAllResources();
				});
			} else {
				$loader._startLoading();
			}
		}
	},

	_startLoading: function() {
		$loader._loader.start($loader._frameworkURL, $loader._resources);
	},

	_embedAllResources: function() {
		if($loader._embedCounter < $loader._resources.length) {
			var url = $loader._resources[$loader._embedCounter];
			var id = $loader._resourceIDs[url];

			if(/[.]js$/.test(url)) {
				$loader.scriptTag(url, 'text/javascript', id, function() {
					$loader._embedCounter++;
					$loader._embedAllResources();
				});
			} else if(/[.]css$/.test(url)) {
				$loader.linkTag(url, 'text/css', 'stylesheet', id);
				$loader._embedCounter++;
				$loader._embedAllResources();
			}
		}
	},

	abortLoadAll: function() {
		$loader._allowLoadAll = false;
	},

	loadAll: function(successCallback, errorCallback) {
		if($loader._allowLoadAll) {
			if(successCallback) {
				$loader._allLoadCallback = successCallback;
			}
			if(errorCallback) {
				$loader._loadFailCallback = errorCallback;
			}

			if($loader._resourcesLoaded.length < $loader._resources.length) {
				$loader._loadDeepResourceToCache($loader._resources[$loader._resourcesLoaded.length], function(){$loader.loadAll()}, $loader._loadAllFail);
			} else {
				if($loader._allLoadCallback) {
					$loader._allLoadCallback($loader._resourcesLoaded);
				}
			}
		} else {
			$loader._allowLoadAll = true;
		}
	},

	_loadAllFail: function() {
		if($loader._loadFailCallback) {
			$loader._loadFailCallback($loader._arrayExclude($loader._resources, $loader._resourcesLoaded));
		} else {
			throw 'Exception: One or more resources could not be loaded';
		}
	},

	finish: function() {
		$loader._embedAllResources();
	},

	ajax: function(settings) {
		var type;
		if(settings.type) {
			type = settings.type;
		} else {
			type = "GET";
		}

		var xmlhttp = $loader._getHTTPReqObject();
		xmlhttp.open(type, settings.url, true);
		xmlhttp.onreadystatechange = function() {
			if(xmlhttp.readyState == 4) {
				if(xmlhttp.status == 200) {
					if(settings.success) {
						settings.success(xmlhttp.responseText);
					}
				} else {
					if(settings.error) {
						settings.error(xmlhttp.statusText);
					} else {
						throw "Failed to load resource: " + url;
					}
				}
			}
		}
		xmlhttp.send();
	},

	/**
		Insert a script tag into the current document as it is being constructed.
		The id & callback parameters are optional.
	*/
	scriptTag: function(url, type, id, callback) {
		var head = document.getElementsByTagName('head')[0];

		var script = document.createElement('script');

		if(callback) {
			if(!$loader._ie || $loader._ieVersion > 8) {
				script.onload = function() {callback(url);};
			} else {
				script.onreadystatechange = function() {
					if(this.readyState == 'loaded') {
						callback(url);
					}
				};
			}
		}

		if(id) {
			script.id = id;
		}
		script.type = type;
		script.src = url;

		head.appendChild(script);
	},

	/**
		Insert a link tag into the current document as it is being constructed.
		The id & callback parameters are optional.
	*/
	linkTag: function(url, type, rel, id) {
		var head = document.getElementsByTagName('head')[0];

		var curScripts = document.getElementsByTagName('script');
		var firstScript = null;
		var firstIndex = 0;

		if(curScripts) {
			var len = curScripts.length;
			while(firstIndex < len && curScripts[firstIndex].parentNode != head) {
				firstIndex++;
			}
			if(firstIndex < len) {
				firstScript = curScripts[firstIndex];
			}
		}

		var link = document.createElement('link');

		if(id) {
			link.id = id;
		}
		link.rel = rel;
		link.type = type;
		link.href = url;

		if(firstScript) {
			head.insertBefore(link, firstScript);
		} else {
			var curLinks = document.getElementsByTagName('link');
			var lastLink = null;
			var lastIndex = curLinks.length - 1;
			if(curLinks) {
				while(lastIndex >= 0 && curLinks[lastIndex].parentNode != head) {
					lastIndex--;
				}
				if(lastIndex >= 0) {
					lastLink = curLinks[lastIndex];
				}
			}

			if(lastLink) {
				if(lastLink.nextSibling) {
					head.insertBefore(link, lastLink.nextSibling);
				} else {
					head.appendChild(link);
				}
			} else {
				head.appendChild(link);
			}
		}
	},

	_loadDeepResourceToCache: function(url, successCallback, errorCallback, rootURL) {
		if(!$loader._resourcesLoadedMap[url]) {
			if(!rootURL || url == rootURL) {
				rootURL = url;
				$loader._deepResources[rootURL] = [];
				$loader._deepResourcesLoaded[rootURL] = [];
			}

			$loader._deepResources[rootURL].push(url);

			if(/[.](png|jpg|gif)$/.test(url)) {
				// images
				var img = new Image();
				img.onload = function() {
					$loader._resourcesLoadedMap[url] = true;
					$loader._deepResourcesLoaded[rootURL].push(url);

					if($loader._deepResourcesLoaded[rootURL].length >= $loader._deepResources[rootURL].length) {
						$loader._resourcesLoaded.push(rootURL);
						if(successCallback) {
							successCallback(rootURL);
						}
					}
				};

				img.onerror = function() {
					if(errorCallback) {
						errorCallback(url);
					} else {
						throw "Exception: Image failed to load";
					}
				};

				img.src = url;
			} else {
				// all text-based files
				$loader.ajax({
					url: url,
					type: "GET",
					success: function(data) {
						$loader._resourcesLoadedMap[url] = true;
						$loader._deepResourcesLoaded[rootURL].push(url);
						var urls, nonLoadedURLs;
						if(/[.]css$/.test(url)) {
							nonLoadedURLs = [];
							urls = $loader._parseDeepCSSURLs(data, url);

							var i, curURL;
							var len = urls.length;
							for(i=0; i<len; i++) {
								curURL = urls[i];

								if(!$loader._resourcesLoadedMap[curURL]) {
									$loader._deepResources[rootURL].push(curURL);
									nonLoadedURLs.push(curURL);
								}
							}

							len = nonLoadedURLs.length;

							for(i=0; i<len; i++) {
								$loader._loadDeepResourceToCache(nonLoadedURLs[i], successCallback, errorCallback, rootURL);
							}
						}

						if($loader._deepResourcesLoaded[rootURL].length >= $loader._deepResources[rootURL].length) {
							$loader._resourcesLoaded.push(rootURL);
							if(successCallback) {
								successCallback(rootURL);
							}
						}
					},

					error: function() {
						if(errorCallback) {
							errorCallback(url);
						} else {
							throw "Exception: One or more resources failed to load";
						}
					}
				});
			}
		}
	},

	_parseDeepCSSURLs: function(fileContent, fileURL) {
		var urlMap = {};
		var urls = [];
		var fileDirURL = fileURL.match(/^(.*)\//)[0];

		var chuncks = $j.grab._parseFunctionCalls(fileContent, ['url']);

		var imports = fileContent.match(/@import +["'][^"']+["']/g);
		if(imports) {
			chuncks = chuncks.concat(imports);
		}

		var isolateURL = /(^url[(][ ]*"?|"?[)]$|^@import[ ]*["']|"$)/g;
		var absolute = /^https?:[/][/]/;

		var i, curURL;
		var len = chuncks.length;
		for(i=0; i<len; i++) {
			curURL = chuncks[i].replace(isolateURL, '');
			if(curURL != "" && !urlMap.hasOwnProperty(curURL)) {
				if(!absolute.test(curURL)) {
					urls.push(fileDirURL + curURL);
				} else {
					urls.push(curURL);
				}
				urlMap[curURL] = true;
			}
		}

		return urls;
	},

	_parseFunctionCalls: function(string, functionNames) {
		var functionCalls = [];
		var functionsRegex = new RegExp('(([^A-Za-z0-9]|^)' + functionNames.join(' *[(]|([^A-Za-z0-9]|^)') + ' *[(])', 'gm');
		var startPos = 0;
		var i, ch, len, curFunc, bt;
		while(true) {
			startPos = string.search(functionsRegex);
			if(startPos < 0) {
				break;
			}

			if(string.charAt(startPos) == '(') {
				startPos++;
			}

			curFunc = '';
			len = string.length;
			bt = 0;
			for(i=startPos; i<len; i++) {
				ch = string.charAt(i);
				curFunc += ch;

				if(ch == '(') {
					bt++;
				} else if(ch == ')') {
					if(--bt == 0) {
						functionCalls.push(curFunc.replace(/^[^A-Za-z0-9]/, ''));
						break;
					}
				}
			}
			string = string.substr(startPos + 2);
		}
		return functionCalls;
	},

	_arrayExclude: function(array, excludeArray) {
		var excl = [];
		var mapExcl = {};
		var i;
		var len = excludeArray.length;
		for(i=0; i<len; i++) {
			mapExcl[excludeArray[i]] = true;
		}

		len = array.length;
		for(i=0; i<len; i++) {
			if(!mapExcl[array[i]]) {
				excl.push(array[i]);
			}
		}
		return excl;
	},

	_getHTTPReqObject: function() {
		xmlhttp = null;

		if($loader._ie) {
			try {
				xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
			} catch (exceptionA) {
				try {
					xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
				} catch (exceptionB) {
					xmlhttp = null;
				}
			}
		}

		if (!xmlhttp && typeof XMLHttpRequest != 'undefined') {
			try {
				xmlhttp = new XMLHttpRequest();
			} catch (e) {
				xmlhttp = null;
			}
		}

		if(!xmlhttp) {
			throw "Could not instantiate XMLHttpRequest";
		}

		return xmlhttp;
	}
};<commit_msg>Update<commit_after>var $loader = {
	_ie: false,
	_ieVersion: null,
	_embedCounter: null,

	_loader: null,
	_frameworkURL: null,
	_routToScriptURL: null,

	_appDefinition: null,
	_resources: null,
	_resourceIDs: null,
	_resourcesLoaded: null,

	_deepResources: null,
	_deepResourcesLoaded: null,

	_resourcesLoadedMap: null,

	_waitForReadyInterval: null,
	_attempts: null,
	_allLoadCallback: null,
	_loadFailCallback: null,

	_allowLoadAll: null,
	_skipPreload: null,

	setLoader: function(loader) {
		$loader._loader = loader;

		if(!$loader._waitForReadyInterval) {
			$loader._waitForReadyInterval = setInterval($loader._waitForReady, 20);
		}
	},

	init: function(frameworkURL, routToScriptURL, loadScriptURL, resources, appDefinition, skipPreload) {
		$loader._frameworkURL = frameworkURL;
		$loader._routToScriptURL = routToScriptURL;

		$loader._appDefinition = appDefinition;

		$loader._resourceIDs = {};

		$loader._resources = resources;
		$loader._resources.push($loader._routToScriptURL);
		$loader._resourceIDs[$loader._routToScriptURL] = 'jComboInitScript';

		$loader._resourcesLoaded = [];
		$loader._deepResources = {};
		$loader._deepResourcesLoaded = {};
		$loader._resourcesLoadedMap = {};
		$loader._allowLoadAll = true;

		$loader._allLoadCallback = null;
		$loader._loadFailCallback = null;
		$loader._attempts = 0;
		$loader._embedCounter = 0;

		if(/MSIE (\d+\.\d+);/.test(navigator.userAgent)) {
			$loader._ie = true;
			$loader._ieVersion = new Number(RegExp.$1)
		}

		$loader._skipPreload = skipPreload;

		if(skipPreload) {
			$loader._waitForReadyInterval = setInterval($loader._waitForReady, 20);
		} else {
			$loader.scriptTag(loadScriptURL, 'text/javascript');
		}
	},

	getAppDefinition: function() {
		return $loader._appDefinition;
	},

	_waitForReady: function() {
		var head = document.getElementsByTagName('head')[0];

		if(head && document.body) {
			clearInterval($loader._waitForReadyInterval);
			if($loader._skipPreload) {
				$loader.loadAll(function() {
					$loader._embedAllResources();
				});
			} else {
				$loader._startLoading();
			}
		}
	},

	_startLoading: function() {
		$loader._loader.start($loader._frameworkURL, $loader._resources);
	},

	_embedAllResources: function() {
		if($loader._embedCounter < $loader._resources.length) {
			var url = $loader._resources[$loader._embedCounter];
			var id = $loader._resourceIDs[url];

			if(/[.]js$/.test(url)) {
				$loader.scriptTag(url, 'text/javascript', id, function() {
					$loader._embedCounter++;
					$loader._embedAllResources();
				});
			} else if(/[.]css$/.test(url)) {
				$loader.linkTag(url, 'text/css', 'stylesheet', id);
				$loader._embedCounter++;
				$loader._embedAllResources();
			}
		}
	},

	abortLoadAll: function() {
		$loader._allowLoadAll = false;
	},

	loadAll: function(successCallback, errorCallback) {
		if($loader._allowLoadAll) {
			if(successCallback) {
				$loader._allLoadCallback = successCallback;
			}
			if(errorCallback) {
				$loader._loadFailCallback = errorCallback;
			}

			if($loader._resourcesLoaded.length < $loader._resources.length) {
				$loader._loadDeepResourceToCache($loader._resources[$loader._resourcesLoaded.length], function(){$loader.loadAll()}, $loader._loadAllFail);
			} else {
				if($loader._allLoadCallback) {
					$loader._allLoadCallback($loader._resourcesLoaded);
				}
			}
		} else {
			$loader._allowLoadAll = true;
		}
	},

	_loadAllFail: function() {
		if($loader._loadFailCallback) {
			$loader._loadFailCallback($loader._arrayExclude($loader._resources, $loader._resourcesLoaded));
		} else {
			throw 'Exception: One or more resources could not be loaded';
		}
	},

	finish: function() {
		$loader._embedAllResources();
	},

	ajax: function(settings) {
		var type;
		if(settings.type) {
			type = settings.type;
		} else {
			type = "GET";
		}

		var xmlhttp = $loader._getHTTPReqObject();
		xmlhttp.open(type, settings.url, true);
		xmlhttp.onreadystatechange = function() {
			if(xmlhttp.readyState == 4) {
				if(xmlhttp.status == 200) {
					if(settings.success) {
						settings.success(xmlhttp.responseText);
					}
				} else {
					if(settings.error) {
						settings.error(xmlhttp.statusText);
					} else {
						throw "Failed to load resource: " + url;
					}
				}
			}
		}
		xmlhttp.send();
	},

	/**
		Insert a script tag into the current document as it is being constructed.
		The id & callback parameters are optional.
	*/
	scriptTag: function(url, type, id, callback) {
		var head = document.getElementsByTagName('head')[0];

		var script = document.createElement('script');

		if(callback) {
			if(!$loader._ie || $loader._ieVersion > 8) {
				script.onload = function() {callback(url);};
			} else {
				script.onreadystatechange = function() {
					if(this.readyState == 'loaded') {
						callback(url);
					}
				};
			}
		}

		if(id) {
			script.id = id;
		}
		script.type = type;
		script.src = url;

		head.appendChild(script);
	},

	/**
		Insert a link tag into the current document as it is being constructed.
		The id & callback parameters are optional.
	*/
	linkTag: function(url, type, rel, id) {
		var head = document.getElementsByTagName('head')[0];

		var curScripts = document.getElementsByTagName('script');
		var firstScript = null;
		var firstIndex = 0;

		if(curScripts) {
			var len = curScripts.length;
			while(firstIndex < len && curScripts[firstIndex].parentNode != head) {
				firstIndex++;
			}
			if(firstIndex < len) {
				firstScript = curScripts[firstIndex];
			}
		}

		var link = document.createElement('link');

		if(id) {
			link.id = id;
		}
		link.rel = rel;
		link.type = type;
		link.href = url;

		if(firstScript) {
			head.insertBefore(link, firstScript);
		} else {
			var curLinks = document.getElementsByTagName('link');
			var lastLink = null;
			var lastIndex = curLinks.length - 1;
			if(curLinks) {
				while(lastIndex >= 0 && curLinks[lastIndex].parentNode != head) {
					lastIndex--;
				}
				if(lastIndex >= 0) {
					lastLink = curLinks[lastIndex];
				}
			}

			if(lastLink) {
				if(lastLink.nextSibling) {
					head.insertBefore(link, lastLink.nextSibling);
				} else {
					head.appendChild(link);
				}
			} else {
				head.appendChild(link);
			}
		}
	},

	_loadDeepResourceToCache: function(url, successCallback, errorCallback, rootURL) {
		if(!$loader._resourcesLoadedMap[url]) {
			var resourceData = null;

			if(!rootURL || url == rootURL) {
				rootURL = url;
				//$loader._resources.push(url);
				$loader._deepResources[rootURL] = [];
				$loader._deepResources[rootURL].push(url);

				$loader._deepResourcesLoaded[rootURL] = [];
			}

			if(/[.](png|jpg|gif)$/.test(url)) {
				// images
				var img = new Image();
				img.onload = function() {
					if(url == rootURL) {
						resourceData = img;
					}
					$loader._resourcesLoadedMap[url] = true;
					$loader._deepResourcesLoaded[rootURL].push(url);

					if($loader._deepResourcesLoaded[rootURL].length >= $loader._deepResources[rootURL].length) {
						$loader._resourcesLoaded.push(rootURL);
						if(successCallback) {
							successCallback(rootURL, resourceData);
						}
					}
				};

				img.onerror = function() {
					if(errorCallback) {
						errorCallback(url);
					} else {
						throw "Exception: Image failed to load";
					}
				};

				img.src = url;
			} else {
				// all text-based files
				$loader.ajax({
					url: url,
					type: "GET",
					success: function(data) {
						if(url == rootURL) {
							resourceData = data;
						}

						$loader._resourcesLoadedMap[url] = true;
						$loader._deepResourcesLoaded[rootURL].push(url);

						var urls, nonLoadedURLs;
						if(/[.]css$/.test(url)) {
							nonLoadedURLs = [];
							urls = $loader._parseDeepCSSURLs(data, url);

							var i, curURL;
							var len = urls.length;
							for(i=0; i<len; i++) {
								curURL = urls[i];

								if(!$loader._resourcesLoadedMap[curURL]) {
									$loader._deepResources[rootURL].push(curURL);
									nonLoadedURLs.push(curURL);
								}
							}

							len = nonLoadedURLs.length;

							for(i=0; i<len; i++) {
								$loader._loadDeepResourceToCache(nonLoadedURLs[i], successCallback, errorCallback, rootURL);
							}
						}

						if($loader._deepResourcesLoaded[rootURL].length >= $loader._deepResources[rootURL].length) {
							$loader._resourcesLoaded.push(rootURL);
							if(successCallback) {
								successCallback(rootURL, resourceData);
							}
						}
					},

					error: function() {
						if(errorCallback) {
							errorCallback(url);
						} else {
							throw "Exception: One or more resources failed to load";
						}
					}
				});
			}
		}
	},

	_parseDeepCSSURLs: function(fileContent, fileURL) {
		var urlMap = {};
		var urls = [];
		var fileDirURL = fileURL.match(/^(.*)\//)[0];

		var chuncks = $loader._parseFunctionCalls(fileContent, ['url']);

		var imports = fileContent.match(/@import +["'][^"']+["']/g);
		if(imports) {
			chuncks = chuncks.concat(imports);
		}

		var isolateURL = /(^url[(][ ]*"?|"?[)]$|^@import[ ]*["']|"$)/g;
		var absolute = /^https?:[/][/]/;

		var i, curURL;
		var len = chuncks.length;
		for(i=0; i<len; i++) {
			curURL = chuncks[i].replace(isolateURL, '');
			if(curURL != "" && !urlMap.hasOwnProperty(curURL)) {
				if(!absolute.test(curURL)) {
					urls.push(fileDirURL + curURL);
				} else {
					urls.push(curURL);
				}
				urlMap[curURL] = true;
			}
		}

		return urls;
	},

	_parseFunctionCalls: function(string, functionNames) {
		var functionCalls = [];
		var functionsRegex = new RegExp('(([^A-Za-z0-9]|^)' + functionNames.join(' *[(]|([^A-Za-z0-9]|^)') + ' *[(])', 'gm');
		var startPos = 0;
		var i, ch, len, curFunc, bt;
		while(true) {
			startPos = string.search(functionsRegex);
			if(startPos < 0) {
				break;
			}

			if(string.charAt(startPos) == '(') {
				startPos++;
			}

			curFunc = '';
			len = string.length;
			bt = 0;
			for(i=startPos; i<len; i++) {
				ch = string.charAt(i);
				curFunc += ch;

				if(ch == '(') {
					bt++;
				} else if(ch == ')') {
					if(--bt == 0) {
						functionCalls.push(curFunc.replace(/^[^A-Za-z0-9]/, ''));
						break;
					}
				}
			}
			string = string.substr(startPos + 2);
		}
		return functionCalls;
	},

	_arrayExclude: function(array, excludeArray) {
		var excl = [];
		var mapExcl = {};
		var i;
		var len = excludeArray.length;
		for(i=0; i<len; i++) {
			mapExcl[excludeArray[i]] = true;
		}

		len = array.length;
		for(i=0; i<len; i++) {
			if(!mapExcl[array[i]]) {
				excl.push(array[i]);
			}
		}
		return excl;
	},

	_getHTTPReqObject: function() {
		xmlhttp = null;

		if($loader._ie) {
			try {
				xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
			} catch (exceptionA) {
				try {
					xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
				} catch (exceptionB) {
					xmlhttp = null;
				}
			}
		}

		if (!xmlhttp && typeof XMLHttpRequest != 'undefined') {
			try {
				xmlhttp = new XMLHttpRequest();
			} catch (e) {
				xmlhttp = null;
			}
		}

		if(!xmlhttp) {
			throw "Could not instantiate XMLHttpRequest";
		}

		return xmlhttp;
	}
};<|endoftext|>


<commit_before>var Stream = require('stream');

module.exports = function Sequence (options, sequence) {

    options = options || {};

    var seq = Stream.Transform({
        objectMode: typeof options.objectMode === 'undefined' ? true : !!options.objectMode,
        transform: function (chunk, enc, next) {

            var pos = -1;
            var handler;
            var runSeq = function (err, data) {

                // emit error
                if (err instanceof Error || (err && data === undefined)) {
                    err._ = chunk;
                    return next(err);
                }

                // just push data to readable if data and error is true
                if (err && data === true) {
                    return seq.push(err);
                }

                // get handler
                if (!(handler = sequence[++pos])) {
                    return next(null, data);
                }

                // call next data handler
                if (!handler[4]) {

                    // once handler
                    if (handler[3]) {
                        handler[4] = true;
                    }

                    // attach flow composition options to caller arguments
                    options._ = handler[1];

                    // call data handler
                    handler[0].call(handler[2], options, data, runSeq);
                } else {
                    runSeq(null, data);
                }
            };

            if (!sequence) {
                return next(null, chunk);
            }

            runSeq(null, chunk);
        }
    });

    return seq;
};
<commit_msg>add todo comment<commit_after>var Stream = require('stream');

module.exports = function Sequence (options, sequence) {

    options = options || {};

    var seq = Stream.Transform({
        objectMode: typeof options.objectMode === 'undefined' ? true : !!options.objectMode,
        transform: function (chunk, enc, next) {

            var pos = -1;
            var handler;
            var runSeq = function (err, data) {

                // emit error
                if (err instanceof Error || (err && data === undefined)) {
                    err._ = chunk;
                    return next(err);
                }

                // just push data to readable if data and error is true
                // TODO call also data handlers with this data
                if (err && data === true) {
                    return seq.push(err);
                }

                // get handler
                if (!(handler = sequence[++pos])) {
                    return next(null, data);
                }

                // call next data handler
                if (!handler[4]) {

                    // once handler
                    if (handler[3]) {
                        handler[4] = true;
                    }

                    // attach flow composition options to caller arguments
                    options._ = handler[1];

                    // call data handler
                    handler[0].call(handler[2], options, data, runSeq);
                } else {
                    runSeq(null, data);
                }
            };

            if (!sequence) {
                return next(null, chunk);
            }

            runSeq(null, chunk);
        }
    });

    return seq;
};
<|endoftext|>


<commit_before>'use strict';

var findFiles = require('./findFiles'),
    logger = require('./logger');

var loaded_services = false;

var isReady = false,
    pending = [],
    services = null;

function initServices(err, files) {
    if (err || files.length === 0) {
        logger.warning('No services found');
        return bootstrapServices();
    }

    files = files || [];

    services = files.map(function(f) {
        return {
            name: f.substr(f.lastIndexOf('/') + 1, f.length).replace('--service.js', ''),
            service: require(f),
            startTime: new Date()
        };
    });

    services.forEach(function(s){
        s.service.on('connected', function(srv) {
            const startupDuration = new Date() - s.startTime;
            logger.success(srv.name, 'service ready in ' + startupDuration + 'ms');
            srv.connected = true;
            bootstrapServices();
        }.bind(this, s));

        s.service.on('serviceDown', function(srv) {
            logger.error(srv.name, 'service is down. Cannot start app');
            srv.connected = false;
            process.exit();
        }.bind(this, s));

        s.service.initialize();
    });
}

function execCallbacks() {
    pending.forEach(function(callback) {
        callback();
    });
}

function bootstrapServices(cb, path) {
    if (isReady && cb) {
        return cb();
    }

    if (!loaded_services) {
        loaded_services = true;
        findFiles(path, '--service.js', initServices);
    }
    else if (loaded_services && !services) {
        // Execute all pending callbacks
        execCallbacks();
    }

    var connections;

    if (services) {
        connections = services.filter(function(s) {
            return s.connected;
        });
    }

    if (connections && connections.length === services.length) {
        isReady = true;
        execCallbacks();
    }
    else {
        if (cb) {
            pending.push(cb);
        }
    }
}

module.exports = bootstrapServices;<commit_msg>back to es5<commit_after>'use strict';

var findFiles = require('./findFiles'),
    logger = require('./logger');

var loaded_services = false;

var isReady = false,
    pending = [],
    services = null;

function initServices(err, files) {
    if (err || files.length === 0) {
        logger.warning('No services found');
        return bootstrapServices();
    }

    files = files || [];

    services = files.map(function(f) {
        return {
            name: f.substr(f.lastIndexOf('/') + 1, f.length).replace('--service.js', ''),
            service: require(f),
            startTime: new Date()
        };
    });

    services.forEach(function(s){
        s.service.on('connected', function(srv) {
            var startupDuration = new Date() - s.startTime;
            logger.success(srv.name, 'service ready in ' + startupDuration + 'ms');
            srv.connected = true;
            bootstrapServices();
        }.bind(this, s));

        s.service.on('serviceDown', function(srv) {
            logger.error(srv.name, 'service is down. Cannot start app');
            srv.connected = false;
            process.exit();
        }.bind(this, s));

        s.service.initialize();
    });
}

function execCallbacks() {
    pending.forEach(function(callback) {
        callback();
    });
}

function bootstrapServices(cb, path) {
    if (isReady && cb) {
        return cb();
    }

    if (!loaded_services) {
        loaded_services = true;
        findFiles(path, '--service.js', initServices);
    }
    else if (loaded_services && !services) {
        // Execute all pending callbacks
        execCallbacks();
    }

    var connections;

    if (services) {
        connections = services.filter(function(s) {
            return s.connected;
        });
    }

    if (connections && connections.length === services.length) {
        isReady = true;
        execCallbacks();
    }
    else {
        if (cb) {
            pending.push(cb);
        }
    }
}

module.exports = bootstrapServices;<|endoftext|>


<commit_before>var util = require("util"),
    events = require("events"),
    fs = require("fs"),
    path = require("path"),
    sessionStores = {};

function Sessions(store, opts) {
	events.EventEmitter.call(this);

	this.store = (typeof store != "undefined" && store !== null ? new store() : new module.exports.stores.memory());
	this.opts = opts || {};
	this._currentSessions = 0;

	this._checkTimeoutId = null;

	if (!this.opts.hasOwnProperty("expires")) {
		this.opts.expires = 300;
	}
	if (!this.opts.hasOwnProperty("domain")) {
		this.opts.domain = null;
	}
	if (!this.opts.hasOwnProperty("path")) {
		this.opts.path = "/";
	}
}
util.inherits(Sessions, events.EventEmitter);

Sessions.prototype.total = function () {
	return this._currentSessions;
};
Sessions.prototype.get = function (uid, cb) {
	var sessions = this;

	this.store.get(uid, function (err, meta, data) {
		if (err) {
			return cb(err);
		}
		return cb(null, buildSessionUtils(uid, meta, data, sessions.store, sessions.opts.expires, sessions), sessions);
	});

	return this;
};
Sessions.prototype.create = function () {
	var sessions = this,
	    uid = null,
	    data = {},
	    meta = { expires: Date.now() + (this.opts.expires * 1000) },
	    cb = null;

	for (var i = 0; i < arguments.length; i++) {
		switch (typeof arguments[i]) {
			case "string":
				uid = arguments[i];
				break;
			case "object":
				data = arguments[i];
				break;
			case "function":
				cb = arguments[i];
				break;
		}
	}

	if (uid === null) {
		uid = this.uid();
	}

	this.store.add(uid, meta, data, function (err, data) {
		if (err) {
			return cb(err);
		}

		if (sessions._checkTimeoutId === null) {
			sessions._checkTimeoutId = setTimeout(function () {
				sessions.checkExpiration();
			}, (sessions.opts.expires + 1) * 1000);
		}

		return cb(null, buildSessionUtils(uid, meta, data, sessions.store, sessions.opts.expires, sessions), sessions);
	});
};
Sessions.prototype.uid = function () {
	return String(Math.random()).substr(2) + String(Math.random()).substr(2);
};
Sessions.prototype.httpRequest = function (req, res, cb) {
	var cookies = parseCookies(req),
	    sessions = this;

	if (!cookies.hasOwnProperty("uid")) {
		return this.create(function (err, session) {
			if (err) {
				return cb(err);
			}

			setHttpCookie(res, "uid", session.uid(), session.expires(), sessions.opts.path, sessions.opts.domain);

			return cb(null, session, sessions);
		});
	}

	this.get(cookies.uid, function (err, session) {
		if (err) {
			return sessions.create(function (err, session) {
				if (err) {
					return cb(err);
				}

				setHttpCookie(res, "uid", session.uid(), session.expires(), sessions.opts.path, sessions.opts.domain);

				return cb(null, session);
			});
		} else {
			return cb(null, session, sessions);
		}
	});

	return this;
};
Sessions.prototype.checkExpiration = function () {
	var sessions = this,
	    conf = { dt: Date.now(), next: null };

	this.store.uids(function (err, uids) {
		if (err) {
			return;
		}

		var sess, i = 0, l = uids.length, missing = l;

		sessions.emit("expirecheck");

		for (; i < l; i++) {
			checkSessionExpiration(sessions, uids[i], conf, function () {
				missing -= 1;
				if (missing == 0) {
					if (conf.next !== null) {
						i = (conf.next - Date.now()) / 1000;

						sessions._checkTimeoutId = setTimeout(function () {
							sessions.checkExpiration();
						}, (i > 0 ? (i + 1) : 10) * 1000);
					}
				}
			});
		}
	});
};

initStores();

module.exports = Sessions;
module.exports.stores = sessionStores;
module.exports.parseCookies = parseCookies;

function initStores() {
	var stores, store, i = 0;

	try {
		stores = fs.readdirSync(__dirname + "/store/");

		for (; i < stores.length; i++) {
			store = path.basename(stores[i], path.extname(stores[i]));

			sessionStores[store] = require(__dirname + "/store/" + stores[i]);
		}
	} catch (e) { }
}

function parseCookies(req) {
	var cookies = {};

	req.headers.cookie && req.headers.cookie.split(";").forEach(function (param) {
		var part = param.split("=", 2);

		cookies[part[0].toLowerCase()] = part[1] || true;
	});

	return cookies;
}

function checkSessionExpiration(sessions, uid, conf, cb) {
	sessions.store.get(uid, function (err, sess) {
		if (err) return cb();

		if (sess.expires <= conf.dt) {
			sessions.emit("expired", uid, false);
			sessions.store.remove(uid);
			return cb();
		}
		if (conf.next === null) {
			conf.next = sess.expires;
			return cb();
		}
		if (sess.expires < conf.next) {
			conf.next = sess.expires;
		}
		return cb();
	});
}

function setHttpCookie(res, key, value, expires, path, domain) {
	var cookie = [], dt;

	cookie.push(key + "=" + value);

	if (domain !== null) {
		cookie.push("domain=" + domain);
	}
	if (path !== null) {
		cookie.push("path=" + path);
	}

	if (expires !== null) {
		if (util.isDate(expires)) {
			dt = expires;
		} else {
			dt = new Date();
			dt.setTime(expires);
		}

		cookie.push("expires=" + dt.toGMTString());
	}

	res.setHeader("Set-cookie", cookie.join("; "));
}

function buildSessionUtils(uid, meta, data, store, expires, sessions) {
	return {
		get: function (key) {
			if (typeof key == "undefined") {
				return data;
			}
			return data[key] || null;
		},
		set: function (key, value, cb) {
			if (typeof key == "object") {
				store.set(uid, key, cb);
			} else {
				var o = {};
				o[key] = value;

				store.set(uid, o, cb);
			}
			return this;
		},
		refresh: function (expire) {
			meta.expires = Date.now() + ((expire || expires) * 1000);

			store.set(uid, meta, {});
			return this;
		},
		expire: function () {
			store.remove(uid);

			sessions.emit("expired", uid, true);

			return this;
		},
		uid: function () {
			return uid;
		},
		expires: function () {
			return meta.expires;
		}
	};
}<commit_msg>Fix bug on session.set() replacing meta instead of data<commit_after>var util = require("util"),
    events = require("events"),
    fs = require("fs"),
    path = require("path"),
    sessionStores = {};

function Sessions(store, opts) {
	events.EventEmitter.call(this);

	this.store = (typeof store != "undefined" && store !== null ? new store() : new module.exports.stores.memory());
	this.opts = opts || {};
	this._currentSessions = 0;

	this._checkTimeoutId = null;

	if (!this.opts.hasOwnProperty("expires")) {
		this.opts.expires = 300;
	}
	if (!this.opts.hasOwnProperty("domain")) {
		this.opts.domain = null;
	}
	if (!this.opts.hasOwnProperty("path")) {
		this.opts.path = "/";
	}
}
util.inherits(Sessions, events.EventEmitter);

Sessions.prototype.total = function () {
	return this._currentSessions;
};
Sessions.prototype.get = function (uid, cb) {
	var sessions = this;

	this.store.get(uid, function (err, meta, data) {
		if (err) {
			return cb(err);
		}
		return cb(null, buildSessionUtils(uid, meta, data, sessions.store, sessions.opts.expires, sessions), sessions);
	});

	return this;
};
Sessions.prototype.create = function () {
	var sessions = this,
	    uid = null,
	    data = {},
	    meta = { expires: Date.now() + (this.opts.expires * 1000) },
	    cb = null;

	for (var i = 0; i < arguments.length; i++) {
		switch (typeof arguments[i]) {
			case "string":
				uid = arguments[i];
				break;
			case "object":
				data = arguments[i];
				break;
			case "function":
				cb = arguments[i];
				break;
		}
	}

	if (uid === null) {
		uid = this.uid();
	}

	this.store.add(uid, meta, data, function (err, data) {
		if (err) {
			return cb(err);
		}

		if (sessions._checkTimeoutId === null) {
			sessions._checkTimeoutId = setTimeout(function () {
				sessions.checkExpiration();
			}, (sessions.opts.expires + 1) * 1000);
		}

		return cb(null, buildSessionUtils(uid, meta, data, sessions.store, sessions.opts.expires, sessions), sessions);
	});
};
Sessions.prototype.uid = function () {
	return String(Math.random()).substr(2) + String(Math.random()).substr(2);
};
Sessions.prototype.httpRequest = function (req, res, cb) {
	var cookies = parseCookies(req),
	    sessions = this;

	if (!cookies.hasOwnProperty("uid")) {
		return this.create(function (err, session) {
			if (err) {
				return cb(err);
			}

			setHttpCookie(res, "uid", session.uid(), session.expires(), sessions.opts.path, sessions.opts.domain);

			return cb(null, session, sessions);
		});
	}

	this.get(cookies.uid, function (err, session) {
		if (err) {
			return sessions.create(function (err, session) {
				if (err) {
					return cb(err);
				}

				setHttpCookie(res, "uid", session.uid(), session.expires(), sessions.opts.path, sessions.opts.domain);

				return cb(null, session);
			});
		} else {
			return cb(null, session, sessions);
		}
	});

	return this;
};
Sessions.prototype.checkExpiration = function () {
	var sessions = this,
	    conf = { dt: Date.now(), next: null };

	this.store.uids(function (err, uids) {
		if (err) {
			return;
		}

		var sess, i = 0, l = uids.length, missing = l;

		sessions.emit("expirecheck");

		for (; i < l; i++) {
			checkSessionExpiration(sessions, uids[i], conf, function () {
				missing -= 1;
				if (missing == 0) {
					if (conf.next !== null) {
						i = (conf.next - Date.now()) / 1000;

						sessions._checkTimeoutId = setTimeout(function () {
							sessions.checkExpiration();
						}, (i > 0 ? (i + 1) : 10) * 1000);
					}
				}
			});
		}
	});
};

initStores();

module.exports = Sessions;
module.exports.stores = sessionStores;
module.exports.parseCookies = parseCookies;

function initStores() {
	var stores, store, i = 0;

	try {
		stores = fs.readdirSync(__dirname + "/store/");

		for (; i < stores.length; i++) {
			store = path.basename(stores[i], path.extname(stores[i]));

			sessionStores[store] = require(__dirname + "/store/" + stores[i]);
		}
	} catch (e) { }
}

function parseCookies(req) {
	var cookies = {};

	req.headers.cookie && req.headers.cookie.split(";").forEach(function (param) {
		var part = param.split("=", 2);

		cookies[part[0].toLowerCase()] = part[1] || true;
	});

	return cookies;
}

function checkSessionExpiration(sessions, uid, conf, cb) {
	sessions.store.get(uid, function (err, sess) {
		if (err) return cb();

		if (sess.expires <= conf.dt) {
			sessions.emit("expired", uid, false);
			sessions.store.remove(uid);
			return cb();
		}
		if (conf.next === null) {
			conf.next = sess.expires;
			return cb();
		}
		if (sess.expires < conf.next) {
			conf.next = sess.expires;
		}
		return cb();
	});
}

function setHttpCookie(res, key, value, expires, path, domain) {
	var cookie = [], dt;

	cookie.push(key + "=" + value);

	if (domain !== null) {
		cookie.push("domain=" + domain);
	}
	if (path !== null) {
		cookie.push("path=" + path);
	}

	if (expires !== null) {
		if (util.isDate(expires)) {
			dt = expires;
		} else {
			dt = new Date();
			dt.setTime(expires);
		}

		cookie.push("expires=" + dt.toGMTString());
	}

	res.setHeader("Set-cookie", cookie.join("; "));
}

function buildSessionUtils(uid, meta, data, store, expires, sessions) {
	return {
		get: function (key) {
			if (typeof key == "undefined") {
				return data;
			}
			return data[key] || null;
		},
		set: function (key, value, cb) {
			if (typeof key == "object") {
				store.set(uid, {}, key, cb);
			} else {
				var o = {};
				o[key] = value;

				store.set(uid, {}, o, cb);
			}
			return this;
		},
		refresh: function (expire) {
			meta.expires = Date.now() + ((expire || expires) * 1000);

			store.set(uid, meta, {});
			return this;
		},
		expire: function () {
			store.remove(uid);

			sessions.emit("expired", uid, true);

			return this;
		},
		uid: function () {
			return uid;
		},
		expires: function () {
			return meta.expires;
		}
	};
}
<|endoftext|>