<commit_before>var Milight = require("node-milight-promise").MilightController;
var helper = require("node-milight-promise").helper;

"use strict";

var Service, Characteristic;

module.exports = function (homebridge) {
  Service = homebridge.hap.Service;
  Characteristic = homebridge.hap.Characteristic;

  homebridge.registerPlatform("homebridge-milight", "MiLight", MiLightPlatform);
};

function MiLightPlatform(log, config) {
  this.log = log;
  this.config = config;
}

MiLightPlatform.prototype.accessories = function (callback) {
  var foundZones = [];

  if (this.config.bridges) {
    if (this.config.zones) {
      this.log.warn("Bridges and Zones keys detected in config. Only using bridges config.");
    }

    if (this.config.bridges.length === 0) {
      this.log.error("No bridges found in configuration.");
    } else {
      for (var bridge in this.config.bridges) {
        var returnedZones = this._addLamps(this.config.bridges[bridge]);
        Array.prototype.push.apply(foundZones, returnedZones);
      }
    }

  } else if (this.config.zones) {
    this.log.warn("DEPRECATED: See README for details of setting multiple lights in new bridges key.");
    foundZones = this._addLamps(this.config);
  } else {
    this.log.error("Could not read any zones/bridges from configuration.");
  }

  if (foundZones.length > 0) {
    callback(foundZones);
  } else {
    this.log.error("Unable to find any valid lights.");
  }

};

MiLightPlatform.prototype._addLamps = function (bridgeConfig) {
  var zones = [];
  var bridgeController;

  // Setting appropriate commands per bridge version. Defaults to v2 as those are the commands that previous versions of the plugin used
  if (!bridgeConfig.version) {
    bridgeConfig.version = "v2";
  }

  if (bridgeConfig.version === "v6") {
    bridgeConfig.commands = require("node-milight-promise").commandsV6;
  } else if (bridgeConfig.version === "v3") {
    bridgeConfig.commands = require("node-milight-promise").commands2;
  } else {
    bridgeConfig.commands = require("node-milight-promise").commands;
  }

  if (!bridgeConfig.zones && !bridgeConfig.lights) {
    this.log.error("No lights or zones defined for this bridge. Check your configuration.");
  } else {
    if (bridgeConfig.lights) {
      if (bridgeConfig.zones) {
        this.log.warn("Zones and Lights keys both detected for bridge. Using only Lights key.");
      }
    } else if (bridgeConfig.zones) {
      this.log.warn("DEPRECATED: See README for details of setting multiple light types per bridge key.");

      if (!bridgeConfig.type) {
        this.log.warn("Type not specified, defaulting to rgbw");
        bridgeConfig.type = "rgbw";
      }

      bridgeConfig.lights = {};
      bridgeConfig.lights[bridgeConfig.type] =  bridgeConfig.zones;
    }

    if (Object.keys(bridgeConfig.lights).length > 0) {

      for (var lightType in bridgeConfig.lights) {
        if (["fullColor", "rgbw", "rgb", "white"].indexOf(lightType) === -1) {
          this.log.error("Invalid light type specified.");
        } else if (bridgeConfig.version !== "v6" && lightType === "fullColor") {
          this.log.error("Full colour bulbs only avaliable with v6 bridge!");
        } else {
          var zonesLength = bridgeConfig.lights[lightType].length;

          if (zonesLength < 1) {
            this.log.error("No zones found in configuration.");
            zonesLength = 0;
          } else if (lightType === "rgb" && zonesLength > 1) {
            this.log.warn("RGB lamps only have a single zone. Only the first defined zone will be used.");
            zonesLength = 1;
          } else if (zonesLength > 4) {
            this.log.warn("Only a maximum of 4 zones are supported per bridge. Only recognizing the first 4 zones.");
            zonesLength = 4;
          }

          if (zonesLength > 0) {
            // If it hasn't been already, initialize a new controller to be used for all zones defined for this bridge
            if (typeof(bridgeController) != "object") {
              bridgeController = new Milight({
                ip: bridgeConfig.ip_address,
                port: bridgeConfig.port,
                delayBetweenCommands: bridgeConfig.delay,
                commandRepeat: bridgeConfig.repeat,
                type: bridgeConfig.version
              });
            }

            // Create lamp accessories for all of the defined zones
            for (var i = 0; i < zonesLength; i++) {
              if (bridgeConfig.name = bridgeConfig.lights[lightType][i]) {
                bridgeConfig.type = lightType;
                bridgeConfig.zone = i + 1;
                var lamp = new MiLightAccessory(this.log, bridgeConfig, bridgeController);
                zones.push(lamp);
              } else if (bridgeConfig.lights[lightType][i] !== null) {
                this.log.error("Unable to add light.");
              }
            }
          }
        }
      }
     } else {
      this.log.error("Could not read any zones/lights from configuration.");
    }
  }

  return zones;
};

function MiLightAccessory(log, lampConfig, lampController) {
  this.log = log;

  // config info
  this.name = lampConfig.name;
  this.zone = lampConfig.zone;
  this.type = lampConfig.type;
  this.version = lampConfig.version;

  // have to keep track of the last values we set brightness and colour temp to for rgb/white bulbs
  this.brightness = -1;
  this.hue = -1;

  // assign to the bridge
  this.light = lampController;

  // use the right commands for this bridge
  this.commands = lampConfig.commands;

}

MiLightAccessory.prototype.setPowerState = function (powerOn, callback) {
  if (powerOn) {
    this.log("[" + this.name + "] Setting power state to on");
    this.light.sendCommands(this.commands[this.type].on(this.zone));
  } else {
    this.log("[" + this.name + "] Setting power state to off");
    this.light.sendCommands(this.commands[this.type].off(this.zone));
  }
  callback(null);
};

MiLightAccessory.prototype.setBrightness = function (level, callback) {
  if (level === 0) {
    // If brightness is set to 0, turn off the lamp
    this.log("[" + this.name + "] Setting brightness to 0 (off)");
    this.lightbulbService.setCharacteristic(Characteristic.On, false);
  } else if (level <= 5 && (this.type === "rgbw" || this.type === "white" || this.type === "fullColor")) {
    // If setting brightness to 5 or lower, instead set night mode for lamps that support it
    this.log("[" + this.name + "] Setting night mode");

    this.light.sendCommands(this.commands[this.type].off(this.zone));
    // Ensure we're pausing for 100ms between these commands as per the spec
    this.light.pause(100);
    this.light.sendCommands(this.commands[this.type].nightMode(this.zone));

  } else {
    // Send on command to ensure we're addressing the right bulb
    this.lightbulbService.setCharacteristic(Characteristic.On, true);

    this.log("[" + this.name + "] Setting brightness to %s", level);

    // If this is an rgbw lamp, set the absolute brightness specified
    if (this.type === "rgbw" || this.type === "fullColor") {
      if (this.version === "v6") {
        this.light.sendCommands(this.commands[this.type].brightness(this.zone, level));
      } else {
        this.light.sendCommands(this.commands[this.type].brightness(level));
      }
    } else {
      // If this is an rgb or a white lamp, they only support brightness up and down.
      if (this.type === "white" && level === 100) {
        // But the white lamps do have a "maximum brightness" command
        this.light.sendCommands(this.commands[this.type].maxBright(this.zone));
        this.brightness = 100;
      } else {
        // We're going to send the number of brightness up or down commands required to get to get from
        // the current value that HomeKit knows to the target value

        // Keeping track of the value separately from Homebridge so we know when to change across multiple small adjustments
        if (this.brightness === -1) this.brightness = this.lightbulbService.getCharacteristic(Characteristic.Brightness).value;
        var currentLevel = this.brightness;

        var targetLevel = level - currentLevel;
        var targetDirection = Math.sign(targetLevel);
        targetLevel = Math.max(0,(Math.round(Math.abs(targetLevel)/10))); // There are 10 steps of brightness

        if (targetDirection === 0 || targetDirection === -0 || targetLevel === 0) {
          this.log("[" + this.name + "] Change not large enough to move to next step for bulb");
        } else {
          this.brightness = level;

          for (; targetLevel !== 0; targetLevel--) {
            if (targetDirection === 1) {
              this.light.sendCommands(this.commands[this.type].brightUp(this.zone));
              this.log.debug("[" + this.name + "] Sending brightness up command");
            } else if (targetDirection === -1) {
              this.light.sendCommands(this.commands[this.type].brightDown(this.zone));
              this.log.debug("[" + this.name + "] Sending brightness down command");
            }
          }
        }
      }
    }
  }
  callback(null);
};

MiLightAccessory.prototype.setHue = function (value, callback) {
  // Send on command to ensure we're addressing the right bulb
  this.lightbulbService.setCharacteristic(Characteristic.On, true);

  this.log("[" + this.name + "] Setting hue to %s", value);

  var hue = [value, 0, 0];

  if (this.type === "rgbw" || this.type === "fullColor") {
    if (this.lightbulbService.getCharacteristic(Characteristic.Saturation).value === 0 && this.hue !== -1) {
      this.log("[" + this.name + "] Saturation is 0, making sure bulb is in white mode");
      this.light.sendCommands(this.commands[this.type].whiteMode(this.zone));
    } else {
      this.hue = value;

      if (this.version === "v6") {
        this.light.sendCommands(this.commands[this.type].hue(this.zone, helper.hsvToMilightColor(hue), true));
      } else {
        this.light.sendCommands(this.commands[this.type].hue(helper.hsvToMilightColor(hue)));
      }
    }
  } else if (this.type === "rgb") {
    if (this.version === "v6") {
      this.light.sendCommands(this.commands[this.type].hue(this.zone, helper.hsvToMilightColor(hue), true));
    } else {
      this.light.sendCommands(this.commands[this.type].hue(helper.hsvToMilightColor(hue)));
    }
  } else if (this.type === "white") {
    // Again, white lamps don't support setting an absolue colour temp, so we'll do some math to figure out how to get there

    // Keeping track of the value separately from Homebridge so we know when to change across multiple small adjustments
    if (this.hue === -1) this.hue = this.lightbulbService.getCharacteristic(Characteristic.Hue).value;
    var currentLevel = this.hue;

    var targetLevel = value - currentLevel;
    var targetDirection = Math.sign(targetLevel);
    targetLevel = Math.max(0,(Math.round(Math.abs(targetLevel)/36))); // There are 10 steps of colour temp (360/10)

    if (targetDirection === 0 || targetDirection === -0 || targetLevel === 0) {
      this.log("[" + this.name + "] Change not large enough to move to next step for bulb");
    } else {
      this.hue = value;

      for (; targetLevel !== 0; targetLevel--) {
        if (targetDirection === 1) {
          this.light.sendCommands(this.commands[this.type].cooler(this.zone));
          this.log.debug("[" + this.name + "] Sending bulb cooler command");
        } else if (targetDirection === -1) {
          this.light.sendCommands(this.commands[this.type].warmer(this.zone));
          this.log.debug("[" + this.name + "] Sending bulb warmer command");
        }
      }
    }
  }
  callback(null);
};

MiLightAccessory.prototype.setSaturation = function (value, callback) {
  if (this.type === "rgbw") {
    if (value === 0) {
      // Send on command to ensure we're addressing the right bulb
      this.lightbulbService.setCharacteristic(Characteristic.On, true);

      this.log("[" + this.name + "] Saturation set to 0, setting bulb to white");
      this.light.sendCommands(this.commands[this.type].whiteMode(this.zone));
    } else if (this.lightbulbService.getCharacteristic(Characteristic.Hue).value !== 0) {
      // Send on command to ensure we're addressing the right bulb
      this.lightbulbService.setCharacteristic(Characteristic.On, true);

      this.log.info("[" + this.name + "] Saturation set to %s, but hue is not 0, resetting hue", value);
      this.lightbulbService.setCharacteristic(Characteristic.Hue, this.lightbulbService.getCharacteristic(Characteristic.Hue).value);
    } else {
      this.log.info("[" + this.name + "] Setting saturation to %s (NOTE: No impact on %s %s bulbs)", value, this.type, this.log.prefix);
    }
  } else if (this.type === "fullColor"){
    // Send on command to ensure we're addressing the right bulb
    this.lightbulbService.setCharacteristic(Characteristic.On, true);

    this.log("[" + this.name + "] Setting saturation to %s", value);
    this.light.sendCommands(this.commands[this.type].saturation(this.zone, value));

  } else {
    this.log.info("[" + this.name + "] Setting saturation to %s (NOTE: No impact on %s %s bulbs)", value, this.type, this.log.prefix);
  }
  callback(null);
};

MiLightAccessory.prototype.identify = function (callback) {
  this.log("[" + this.name + "] Identify requested!");
  callback(null); // success
};

MiLightAccessory.prototype.getServices = function () {
  this.informationService = new Service.AccessoryInformation();

  this.informationService
    .setCharacteristic(Characteristic.Manufacturer, this.log.prefix)
    .setCharacteristic(Characteristic.Model, this.type)
    .setCharacteristic(Characteristic.SerialNumber, "12345");

  this.lightbulbService = new Service.Lightbulb(this.name);

  this.lightbulbService
    .getCharacteristic(Characteristic.On)
    .on("set", this.setPowerState.bind(this));

  this.lightbulbService
    .addCharacteristic(new Characteristic.Brightness())
    .on("set", this.setBrightness.bind(this));

  this.lightbulbService
    .addCharacteristic(new Characteristic.Saturation())
    .on("set", this.setSaturation.bind(this));

  this.lightbulbService
    .addCharacteristic(new Characteristic.Hue())
    .on("set", this.setHue.bind(this));

  return [this.informationService, this.lightbulbService];
};
<commit_msg>Send one less 'on' command, and do a little code cleanup<commit_after>var Milight = require("node-milight-promise").MilightController;
var helper = require("node-milight-promise").helper;

"use strict";

var Service, Characteristic;

module.exports = function (homebridge) {
  Service = homebridge.hap.Service;
  Characteristic = homebridge.hap.Characteristic;

  homebridge.registerPlatform("homebridge-milight", "MiLight", MiLightPlatform);
};

function MiLightPlatform(log, config) {
  this.log = log;
  this.config = config;
}

MiLightPlatform.prototype.accessories = function (callback) {
  var foundZones = [];

  if (this.config.bridges) {
    if (this.config.zones) {
      this.log.warn("Bridges and Zones keys detected in config. Only using bridges config.");
    }

    if (this.config.bridges.length === 0) {
      this.log.error("No bridges found in configuration.");
    } else {
      for (var bridge in this.config.bridges) {
        var returnedZones = this._addLamps(this.config.bridges[bridge]);
        Array.prototype.push.apply(foundZones, returnedZones);
      }
    }

  } else if (this.config.zones) {
    this.log.warn("DEPRECATED: See README for details of setting multiple lights in new bridges key.");
    foundZones = this._addLamps(this.config);
  } else {
    this.log.error("Could not read any zones/bridges from configuration.");
  }

  if (foundZones.length > 0) {
    callback(foundZones);
  } else {
    this.log.error("Unable to find any valid lights.");
  }

};

MiLightPlatform.prototype._addLamps = function (bridgeConfig) {
  var zones = [];
  var bridgeController;

  // Setting appropriate commands per bridge version. Defaults to v2 as those are the commands that previous versions of the plugin used
  if (!bridgeConfig.version) {
    bridgeConfig.version = "v2";
  }

  if (bridgeConfig.version === "v6") {
    bridgeConfig.commands = require("node-milight-promise").commandsV6;
  } else if (bridgeConfig.version === "v3") {
    bridgeConfig.commands = require("node-milight-promise").commands2;
  } else {
    bridgeConfig.commands = require("node-milight-promise").commands;
  }

  if (!bridgeConfig.zones && !bridgeConfig.lights) {
    this.log.error("No lights or zones defined for this bridge. Check your configuration.");
  } else {
    if (bridgeConfig.lights) {
      if (bridgeConfig.zones) {
        this.log.warn("Zones and Lights keys both detected for bridge. Using only Lights key.");
      }
    } else if (bridgeConfig.zones) {
      this.log.warn("DEPRECATED: See README for details of setting multiple light types per bridge key.");

      if (!bridgeConfig.type) {
        this.log.warn("Type not specified, defaulting to rgbw");
        bridgeConfig.type = "rgbw";
      }

      bridgeConfig.lights = {};
      bridgeConfig.lights[bridgeConfig.type] =  bridgeConfig.zones;
    }

    if (Object.keys(bridgeConfig.lights).length > 0) {

      for (var lightType in bridgeConfig.lights) {
        if (["fullColor", "rgbw", "rgb", "white"].indexOf(lightType) === -1) {
          this.log.error("Invalid light type specified.");
        } else if (bridgeConfig.version !== "v6" && lightType === "fullColor") {
          this.log.error("Full colour bulbs only avaliable with v6 bridge!");
        } else {
          var zonesLength = bridgeConfig.lights[lightType].length;

          if (zonesLength < 1) {
            this.log.error("No zones found in configuration.");
            zonesLength = 0;
          } else if (lightType === "rgb" && zonesLength > 1) {
            this.log.warn("RGB lamps only have a single zone. Only the first defined zone will be used.");
            zonesLength = 1;
          } else if (zonesLength > 4) {
            this.log.warn("Only a maximum of 4 zones are supported per bridge. Only recognizing the first 4 zones.");
            zonesLength = 4;
          }

          if (zonesLength > 0) {
            // If it hasn't been already, initialize a new controller to be used for all zones defined for this bridge
            if (typeof(bridgeController) != "object") {
              bridgeController = new Milight({
                ip: bridgeConfig.ip_address,
                port: bridgeConfig.port,
                delayBetweenCommands: bridgeConfig.delay,
                commandRepeat: bridgeConfig.repeat,
                type: bridgeConfig.version
              });
            }

            // Create lamp accessories for all of the defined zones
            for (var i = 0; i < zonesLength; i++) {
              if (bridgeConfig.name = bridgeConfig.lights[lightType][i]) {
                bridgeConfig.type = lightType;
                bridgeConfig.zone = i + 1;
                var lamp = new MiLightAccessory(this.log, bridgeConfig, bridgeController);
                zones.push(lamp);
              } else if (bridgeConfig.lights[lightType][i] !== null) {
                this.log.error("Unable to add light.");
              }
            }
          }
        }
      }
     } else {
      this.log.error("Could not read any zones/lights from configuration.");
    }
  }

  return zones;
};

function MiLightAccessory(log, lampConfig, lampController) {
  this.log = log;

  // config info
  this.name = lampConfig.name;
  this.zone = lampConfig.zone;
  this.type = lampConfig.type;
  this.version = lampConfig.version;

  // have to keep track of the last values we set brightness and colour temp to for rgb/white bulbs
  this.brightness = -1;
  this.hue = -1;

  // assign to the bridge
  this.light = lampController;

  // use the right commands for this bridge
  this.commands = lampConfig.commands;

}

MiLightAccessory.prototype.setPowerState = function (powerOn, callback) {
  if (powerOn) {
    this.log("[" + this.name + "] Setting power state to on");
    this.light.sendCommands(this.commands[this.type].on(this.zone));
  } else {
    this.log("[" + this.name + "] Setting power state to off");
    this.light.sendCommands(this.commands[this.type].off(this.zone));
  }
  callback(null);
};

MiLightAccessory.prototype.setBrightness = function (level, callback) {
  if (level === 0) {
    // If brightness is set to 0, turn off the lamp
    this.log("[" + this.name + "] Setting brightness to 0 (off)");
    this.lightbulbService.setCharacteristic(Characteristic.On, false);
  } else if (level <= 5 && (this.type === "rgbw" || this.type === "white" || this.type === "fullColor")) {
    // If setting brightness to 5 or lower, instead set night mode for lamps that support it
    this.log("[" + this.name + "] Setting night mode");

    this.light.sendCommands(this.commands[this.type].off(this.zone));
    // Ensure we're pausing for 100ms between these commands as per the spec
    this.light.pause(100);
    this.light.sendCommands(this.commands[this.type].nightMode(this.zone));

  } else {
    // Send on command to ensure we're addressing the right bulb
    this.lightbulbService.setCharacteristic(Characteristic.On, true);

    this.log("[" + this.name + "] Setting brightness to %s", level);

    // If this is an rgbw lamp, set the absolute brightness specified
    if (this.type === "rgbw" || this.type === "fullColor") {
      if (this.version === "v6") {
        this.light.sendCommands(this.commands[this.type].brightness(this.zone, level));
      } else {
        this.light.sendCommands(this.commands[this.type].brightness(level));
      }
    } else {
      // If this is an rgb or a white lamp, they only support brightness up and down.
      if (this.type === "white" && level === 100) {
        // But the white lamps do have a "maximum brightness" command
        this.light.sendCommands(this.commands[this.type].maxBright(this.zone));
        this.brightness = 100;
      } else {
        // We're going to send the number of brightness up or down commands required to get to get from
        // the current value that HomeKit knows to the target value

        // Keeping track of the value separately from Homebridge so we know when to change across multiple small adjustments
        if (this.brightness === -1) this.brightness = this.lightbulbService.getCharacteristic(Characteristic.Brightness).value;
        var currentLevel = this.brightness;

        var targetLevel = level - currentLevel;
        var targetDirection = Math.sign(targetLevel);
        targetLevel = Math.max(0,(Math.round(Math.abs(targetLevel)/10))); // There are 10 steps of brightness

        if (targetDirection === 0 || targetDirection === -0 || targetLevel === 0) {
          this.log("[" + this.name + "] Change not large enough to move to next step for bulb");
        } else {
          this.brightness = level;

          for (; targetLevel !== 0; targetLevel--) {
            if (targetDirection === 1) {
              this.light.sendCommands(this.commands[this.type].brightUp(this.zone));
              this.log.debug("[" + this.name + "] Sending brightness up command");
            } else if (targetDirection === -1) {
              this.light.sendCommands(this.commands[this.type].brightDown(this.zone));
              this.log.debug("[" + this.name + "] Sending brightness down command");
            }
          }
        }
      }
    }
  }
  callback(null);
};

MiLightAccessory.prototype.setHue = function (value, callback) {
  // Send on command to ensure we're addressing the right bulb
  this.lightbulbService.setCharacteristic(Characteristic.On, true);

  this.log("[" + this.name + "] Setting hue to %s", value);

  if ((this.type === "rgbw" || this.type === "fullColor") && this.lightbulbService.getCharacteristic(Characteristic.Saturation).value === 0 && this.hue !== -1) {
    this.log("[" + this.name + "] Saturation is 0, making sure bulb is in white mode");
    this.light.sendCommands(this.commands[this.type].whiteMode(this.zone));
  } else if (this.type === "rgbw" || this.type === "rgb" || this.type === "fullColor") {
    this.hue = value;

    if (this.version === "v6") {
      this.light.sendCommands(this.commands[this.type].hue(this.zone, helper.hsvToMilightColor([value, 0, 0]), true));
    } else {
      this.light.sendCommands(this.commands[this.type].hue(helper.hsvToMilightColor([value, 0, 0])));
    }
  } else if (this.type === "white") {
    // Again, white lamps don't support setting an absolue colour temp, so we'll do some math to figure out how to get there

    // Keeping track of the value separately from Homebridge so we know when to change across multiple small adjustments
    if (this.hue === -1) this.hue = this.lightbulbService.getCharacteristic(Characteristic.Hue).value;
    var currentLevel = this.hue;

    var targetLevel = value - currentLevel;
    var targetDirection = Math.sign(targetLevel);
    targetLevel = Math.max(0,(Math.round(Math.abs(targetLevel)/36))); // There are 10 steps of colour temp (360/10)

    if (targetDirection === 0 || targetDirection === -0 || targetLevel === 0) {
      this.log("[" + this.name + "] Change not large enough to move to next step for bulb");
    } else {
      this.hue = value;

      for (; targetLevel !== 0; targetLevel--) {
        if (targetDirection === 1) {
          this.light.sendCommands(this.commands[this.type].cooler(this.zone));
          this.log.debug("[" + this.name + "] Sending bulb cooler command");
        } else if (targetDirection === -1) {
          this.light.sendCommands(this.commands[this.type].warmer(this.zone));
          this.log.debug("[" + this.name + "] Sending bulb warmer command");
        }
      }
    }
  }
  callback(null);
};

MiLightAccessory.prototype.setSaturation = function (value, callback) {
  if (this.type === "rgbw") {
    if (value === 0) {
      // Send on command to ensure we're addressing the right bulb
      this.lightbulbService.setCharacteristic(Characteristic.On, true);

      this.log("[" + this.name + "] Saturation set to 0, setting bulb to white");
      this.light.sendCommands(this.commands[this.type].whiteMode(this.zone));
    } else if (this.lightbulbService.getCharacteristic(Characteristic.Hue).value !== 0) {
      // We can get these commands out-of-order, so set the hue again just to be sure
      this.log.info("[" + this.name + "] Saturation set to %s, but hue is not 0, resetting hue", value);
      this.lightbulbService.setCharacteristic(Characteristic.Hue, this.lightbulbService.getCharacteristic(Characteristic.Hue).value);
    } else {
      this.log.info("[" + this.name + "] Setting saturation to %s (NOTE: No impact on %s %s bulbs)", value, this.type, this.log.prefix);
    }
  } else if (this.type === "fullColor"){
    // Send on command to ensure we're addressing the right bulb
    this.lightbulbService.setCharacteristic(Characteristic.On, true);

    this.log("[" + this.name + "] Setting saturation to %s", value);
    this.light.sendCommands(this.commands[this.type].saturation(this.zone, value));

  } else {
    this.log.info("[" + this.name + "] Setting saturation to %s (NOTE: No impact on %s %s bulbs)", value, this.type, this.log.prefix);
  }
  callback(null);
};

MiLightAccessory.prototype.identify = function (callback) {
  this.log("[" + this.name + "] Identify requested!");
  callback(null); // success
};

MiLightAccessory.prototype.getServices = function () {
  this.informationService = new Service.AccessoryInformation();

  this.informationService
    .setCharacteristic(Characteristic.Manufacturer, this.log.prefix)
    .setCharacteristic(Characteristic.Model, this.type)
    .setCharacteristic(Characteristic.SerialNumber, "12345");

  this.lightbulbService = new Service.Lightbulb(this.name);

  this.lightbulbService
    .getCharacteristic(Characteristic.On)
    .on("set", this.setPowerState.bind(this));

  this.lightbulbService
    .addCharacteristic(new Characteristic.Brightness())
    .on("set", this.setBrightness.bind(this));

  this.lightbulbService
    .addCharacteristic(new Characteristic.Saturation())
    .on("set", this.setSaturation.bind(this));

  this.lightbulbService
    .addCharacteristic(new Characteristic.Hue())
    .on("set", this.setHue.bind(this));

  return [this.informationService, this.lightbulbService];
};
<|endoftext|>


<commit_before>var inside = require('turf-inside');

/**
 * Calculates the maximum value of a field for points within a set of polygons features.
 *
 * @module turf/max
 * @category aggregation
 * @param {FeatureCollection<Polygon>} polygons input polygons
 * @param {FeatureCollection<Point>} points input points
 * @param {String} inField the field in input data to analyze
 * @param {String} outField the field in which to store results
 * @return {FeatureCollection<Polygon>} polygons
 * with properties listed as `outField` values
 * @example
 * var polygons = {
 *   "type": "FeatureCollection",
 *   "features": [
 *     {
 *       "type": "Feature",
 *       "properties": {},
 *       "geometry": {
 *         "type": "Polygon",
 *         "coordinates": [[
 *           [101.551437, 3.150114],
 *           [101.551437, 3.250208],
 *           [101.742324, 3.250208],
 *           [101.742324, 3.150114],
 *           [101.551437, 3.150114]
 *         ]]
 *       }
 *     }, {
 *       "type": "Feature",
 *       "properties": {},
 *       "geometry": {
 *         "type": "Polygon",
 *         "coordinates": [[
 *           [101.659927, 3.011612],
 *           [101.659927, 3.143944],
 *           [101.913986, 3.143944],
 *           [101.913986, 3.011612],
 *           [101.659927, 3.011612]
 *         ]]
 *       }
 *     }
 *   ]
 * };
 * var points = {
 *   "type": "FeatureCollection",
 *   "features": [
 *     {
 *       "type": "Feature",
 *       "properties": {
 *         "population": 200
 *       },
 *       "geometry": {
 *         "type": "Point",
 *         "coordinates": [101.56105, 3.213874]
 *       }
 *     }, {
 *       "type": "Feature",
 *       "properties": {
 *         "population": 600
 *       },
 *       "geometry": {
 *         "type": "Point",
 *         "coordinates": [101.709365, 3.211817]
 *       }
 *     }, {
 *       "type": "Feature",
 *       "properties": {
 *         "population": 100
 *       },
 *       "geometry": {
 *         "type": "Point",
 *         "coordinates": [101.645507, 3.169311]
 *       }
 *     }, {
 *       "type": "Feature",
 *       "properties": {
 *         "population": 200
 *       },
 *       "geometry": {
 *         "type": "Point",
 *         "coordinates": [101.708679, 3.071266]
 *       }
 *     }, {
 *       "type": "Feature",
 *       "properties": {
 *         "population": 300
 *       },
 *       "geometry": {
 *         "type": "Point",
 *         "coordinates": [101.826782, 3.081551]
 *       }
 *     }
 *   ]
 * };
 *
 * var aggregated = turf.max(
 *   polygons, points, 'population', 'max');
 *
 * var resultFeatures = points.features.concat(
 *   aggregated.features);
 * var result = {
 *   "type": "FeatureCollection",
 *   "features": resultFeatures
 * };
 *
 * //=result
 */
module.exports = function(polyFC, ptFC, inField, outField){
  polyFC.features.forEach(function(poly){
    if(!poly.properties){
      poly.properties = {};
    }
    var values = [];
    ptFC.features.forEach(function(pt){
      if (inside(pt, poly)) {
        values.push(pt.properties[inField]);
      }
    });
    poly.properties[outField] = max(values);
  });

  return polyFC;
}

function max(x) {
    var value;
    for (var i = 0; i < x.length; i++) {
        // On the first iteration of this loop, max is
        // undefined and is thus made the maximum element in the array
        if (x[i] > value || value === undefined) value = x[i];
    }
    return value;
}
<commit_msg>Switch doc to closure compiler templating<commit_after>var inside = require('turf-inside');

/**
 * Calculates the maximum value of a field for points within a set of polygons.
 *
 * @module turf/max
 * @category aggregation
 * @param {FeatureCollection<Polygon>} polygons input polygons
 * @param {FeatureCollection<Point>} points input points
 * @param {String} inField the field in input data to analyze
 * @param {String} outField the field in which to store results
 * @return {FeatureCollection<Polygon>} polygons
 * with properties listed as `outField` values
 * @example
 * var polygons = {
 *   "type": "FeatureCollection",
 *   "features": [
 *     {
 *       "type": "Feature",
 *       "properties": {},
 *       "geometry": {
 *         "type": "Polygon",
 *         "coordinates": [[
 *           [101.551437, 3.150114],
 *           [101.551437, 3.250208],
 *           [101.742324, 3.250208],
 *           [101.742324, 3.150114],
 *           [101.551437, 3.150114]
 *         ]]
 *       }
 *     }, {
 *       "type": "Feature",
 *       "properties": {},
 *       "geometry": {
 *         "type": "Polygon",
 *         "coordinates": [[
 *           [101.659927, 3.011612],
 *           [101.659927, 3.143944],
 *           [101.913986, 3.143944],
 *           [101.913986, 3.011612],
 *           [101.659927, 3.011612]
 *         ]]
 *       }
 *     }
 *   ]
 * };
 * var points = {
 *   "type": "FeatureCollection",
 *   "features": [
 *     {
 *       "type": "Feature",
 *       "properties": {
 *         "population": 200
 *       },
 *       "geometry": {
 *         "type": "Point",
 *         "coordinates": [101.56105, 3.213874]
 *       }
 *     }, {
 *       "type": "Feature",
 *       "properties": {
 *         "population": 600
 *       },
 *       "geometry": {
 *         "type": "Point",
 *         "coordinates": [101.709365, 3.211817]
 *       }
 *     }, {
 *       "type": "Feature",
 *       "properties": {
 *         "population": 100
 *       },
 *       "geometry": {
 *         "type": "Point",
 *         "coordinates": [101.645507, 3.169311]
 *       }
 *     }, {
 *       "type": "Feature",
 *       "properties": {
 *         "population": 200
 *       },
 *       "geometry": {
 *         "type": "Point",
 *         "coordinates": [101.708679, 3.071266]
 *       }
 *     }, {
 *       "type": "Feature",
 *       "properties": {
 *         "population": 300
 *       },
 *       "geometry": {
 *         "type": "Point",
 *         "coordinates": [101.826782, 3.081551]
 *       }
 *     }
 *   ]
 * };
 *
 * var aggregated = turf.max(
 *   polygons, points, 'population', 'max');
 *
 * var resultFeatures = points.features.concat(
 *   aggregated.features);
 * var result = {
 *   "type": "FeatureCollection",
 *   "features": resultFeatures
 * };
 *
 * //=result
 */
module.exports = function(polyFC, ptFC, inField, outField){
  polyFC.features.forEach(function(poly){
    if(!poly.properties){
      poly.properties = {};
    }
    var values = [];
    ptFC.features.forEach(function(pt){
      if (inside(pt, poly)) {
        values.push(pt.properties[inField]);
      }
    });
    poly.properties[outField] = max(values);
  });

  return polyFC;
}

function max(x) {
    var value;
    for (var i = 0; i < x.length; i++) {
        // On the first iteration of this loop, max is
        // undefined and is thus made the maximum element in the array
        if (x[i] > value || value === undefined) value = x[i];
    }
    return value;
}
<|endoftext|>


<commit_before>const Table = require('cli-table2')
const uuid = require('node-uuid')
const pg = require('pg-promise')()

const isDuplicate = err => err.code === '23505'

function Stairs (config) {
  const stairs = {}
  const sessions = {}
  const db = pg(config.db)

  let currentSessionId
  let achievements = []

  function onError (err) {
    console.error(`${new Date()} ${err.stack || err}`)
  }

  db.query('SELECT * FROM sessions ORDER by created_at DESC')
    .then(res => res[0])
    .then(session => {
      if (!session) {
        return
      }

      currentSessionId = session.id
      sessions[currentSessionId] = session
    })
    .catch(onError)

  db.query('SELECT * FROM achievements ORDER BY height')
    .then(res => achievements = res)
    .catch(onError)

  function onStairs (message) {
    currentSessionId = String(message.thread || uuid.v4())

    console.log(`#stairs ${currentSessionId}`)

    return db.query('INSERT INTO sessions (id) VALUES ($1) RETURNING *', [currentSessionId])
      .then(res => res[0])
      .then(session => {
        sessions[currentSessionId] = session
      })
      .catch(err => {
        // Ignore if the session already existed.
        if (!isDuplicate(err)) {
          throw err
        }
      })
  }

  function onDone (message) {
    const session = sessions[message.thread || currentSessionId]

    console.log(`#done ${message.author.name}`)

    if (!session) {
      return message.reply('There was no stairs session here!')
    }

    const number = message.words[message.words.indexOf(message.doneHash) + 1]
    const floors = number && number.match(/^\d+/) ? Number(number) : config.floors

    console.log(`#done ${session.id} ${message.author.name} ${floors}`)

    return db.query('INSERT INTO users (id, name) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET name = $2', [message.author.id, message.author.name])
      .then(() => db.query('INSERT INTO runs (user_id, session_id, floors) VALUES ($1, $2, $3)', [message.author.id, session.id, floors]))
      .then(() => message.tag('#gg'))
      .then(() => db.query('SELECT SUM(floors) AS total FROM runs'))
      .then(res => res[0].total * config.floorHeight)
      .then(total => (console.log(`#total ${total}`), total))
      .then(total => achievements.find(({ height }) => height > (total - (floors * config.floorHeight)) && height <= total))
      .then(achievement => achievement && message.send(`@team, you just reached the ${achievement.name} (${achievement.location}), with a total of ${achievement.height} meters!`))
  }

  function onLeaderboard (message) {
    return db.query(`
        SELECT users.name,
               sum(floors) AS floors
          FROM runs
          JOIN users
            ON users.id = runs.user_id
      GROUP BY user_id, users.name
      ORDER BY sum(floors) DESC
    `)
      .then(rows => {
        const table = new Table({
          head: ['#', 'name', 'floors'],
          style: { head: [], border: [] }
        })

        rows.forEach((row, rank) => table.push([rank + 1, row.name, row.floors]))
        return message.send('```\n' + table.toString() + '\n```')
      })
  }

  function onAchievements (message) {
    return db.query('SELECT SUM(floors) AS total FROM runs')
      .then(res => res[0].total * config.floorHeight)
      .then(total => {
        const table = new Table({
          head: ['#', 'name', 'location', 'height'],
          style: { head: [], border: [] }
        })

        const next = achievements.find(({ height }) => height > total)
        const leftMeters = next.height - total
        const leftRuns = leftMeters / config.floorHeight / config.floors

        achievements.forEach(({ name, location, height }) => table.push([total >= height ? '✓' : '✗', name, location, `${height} meters`]))

        return Promise.all([
          message.send('```\n' + table.toString() + '\n```'),
          message.send(`${Math.round(leftRuns)} runs (${Math.round(leftMeters)} meters) left for next achievement (${next.name}).`)
        ])
      })
  }

  function onMessage (message) {
    message = Object.assign({}, message, { words: message.text.split(/(?::|,|!|\.|\?)?(?: +|$)/) })
    const actions = []

    if (message.words.includes('#stairs')) {
      actions.push(onStairs)
    }

    if (message.words.includes('#done')) {
      message.doneHash = '#done';
      actions.push(onDone)
    }

    if (message.words.includes('#std')) {
      message.doneHash = '#std';
      actions.push(onStairs)
      actions.push(onDone)
    }

    if (message.words.includes('#lead') || message.words.includes('#leaderboard')) {
      actions.push(onLeaderboard)
    }

    if (message.words.includes('#achievements')) {
      actions.push(onAchievements)
    }

    return actions.reduce((promise, action) => promise.then(() => action(message)), Promise.resolve())
      .catch(onError)
  }

  if (!config.adapter) {
    // External adapter will push messages.
    stairs.onMessage = onMessage
  } else {
    config.adapter.on('message', onMessage)
  }

  console.log('Started listening for messages')

  stairs.end = () => {
    db.end()

    if (config.adapter) {
      config.adapter.end()
    }
  }

  return stairs
}

module.exports = Stairs
<commit_msg>Show only recent users in the leaderboard<commit_after>const Table = require('cli-table2')
const uuid = require('node-uuid')
const pg = require('pg-promise')()

const isDuplicate = err => err.code === '23505'

function Stairs (config) {
  const stairs = {}
  const sessions = {}
  const db = pg(config.db)

  let currentSessionId
  let achievements = []

  function onError (err) {
    console.error(`${new Date()} ${err.stack || err}`)
  }

  db.query('SELECT * FROM sessions ORDER by created_at DESC')
    .then(res => res[0])
    .then(session => {
      if (!session) {
        return
      }

      currentSessionId = session.id
      sessions[currentSessionId] = session
    })
    .catch(onError)

  db.query('SELECT * FROM achievements ORDER BY height')
    .then(res => achievements = res)
    .catch(onError)

  function onStairs (message) {
    currentSessionId = String(message.thread || uuid.v4())

    console.log(`#stairs ${currentSessionId}`)

    return db.query('INSERT INTO sessions (id) VALUES ($1) RETURNING *', [currentSessionId])
      .then(res => res[0])
      .then(session => {
        sessions[currentSessionId] = session
      })
      .catch(err => {
        // Ignore if the session already existed.
        if (!isDuplicate(err)) {
          throw err
        }
      })
  }

  function onDone (message) {
    const session = sessions[message.thread || currentSessionId]

    console.log(`#done ${message.author.name}`)

    if (!session) {
      return message.reply('There was no stairs session here!')
    }

    const number = message.words[message.words.indexOf(message.doneHash) + 1]
    const floors = number && number.match(/^\d+/) ? Number(number) : config.floors

    console.log(`#done ${session.id} ${message.author.name} ${floors}`)

    return db.query('INSERT INTO users (id, name) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET name = $2', [message.author.id, message.author.name])
      .then(() => db.query('INSERT INTO runs (user_id, session_id, floors) VALUES ($1, $2, $3)', [message.author.id, session.id, floors]))
      .then(() => message.tag('#gg'))
      .then(() => db.query('SELECT SUM(floors) AS total FROM runs'))
      .then(res => res[0].total * config.floorHeight)
      .then(total => (console.log(`#total ${total}`), total))
      .then(total => achievements.find(({ height }) => height > (total - (floors * config.floorHeight)) && height <= total))
      .then(achievement => achievement && message.send(`@team, you just reached the ${achievement.name} (${achievement.location}), with a total of ${achievement.height} meters!`))
  }

  function onLeaderboard (message) {
    return db.query(`
          WITH recent_users
            AS (
                  SELECT users.*
                    FROM users
                    JOIN runs
                      ON runs.user_id = users.id
                GROUP BY users.id
                  HAVING max(runs.created_at) > now() - interval '2 months'
               )
        SELECT recent_users.name,
               sum(floors) AS floors
          FROM runs
          JOIN recent_users
            ON recent_users.id = runs.user_id
      GROUP BY user_id, recent_users.name
      ORDER BY sum(floors) DESC
    `)
      .then(rows => {
        const table = new Table({
          head: ['#', 'name', 'floors'],
          style: { head: [], border: [] }
        })

        rows.forEach((row, rank) => table.push([rank + 1, row.name, row.floors]))
        return message.send('```\n' + table.toString() + '\n```')
      })
  }

  function onAchievements (message) {
    return db.query('SELECT SUM(floors) AS total FROM runs')
      .then(res => res[0].total * config.floorHeight)
      .then(total => {
        const table = new Table({
          head: ['#', 'name', 'location', 'height'],
          style: { head: [], border: [] }
        })

        const next = achievements.find(({ height }) => height > total)
        const leftMeters = next.height - total
        const leftRuns = leftMeters / config.floorHeight / config.floors

        achievements.forEach(({ name, location, height }) => table.push([total >= height ? '✓' : '✗', name, location, `${height} meters`]))

        return Promise.all([
          message.send('```\n' + table.toString() + '\n```'),
          message.send(`${Math.round(leftRuns)} runs (${Math.round(leftMeters)} meters) left for next achievement (${next.name}).`)
        ])
      })
  }

  function onMessage (message) {
    message = Object.assign({}, message, { words: message.text.split(/(?::|,|!|\.|\?)?(?: +|$)/) })
    const actions = []

    if (message.words.includes('#stairs')) {
      actions.push(onStairs)
    }

    if (message.words.includes('#done')) {
      message.doneHash = '#done';
      actions.push(onDone)
    }

    if (message.words.includes('#std')) {
      message.doneHash = '#std';
      actions.push(onStairs)
      actions.push(onDone)
    }

    if (message.words.includes('#lead') || message.words.includes('#leaderboard')) {
      actions.push(onLeaderboard)
    }

    if (message.words.includes('#achievements')) {
      actions.push(onAchievements)
    }

    return actions.reduce((promise, action) => promise.then(() => action(message)), Promise.resolve())
      .catch(onError)
  }

  if (!config.adapter) {
    // External adapter will push messages.
    stairs.onMessage = onMessage
  } else {
    config.adapter.on('message', onMessage)
  }

  console.log('Started listening for messages')

  stairs.end = () => {
    db.end()

    if (config.adapter) {
      config.adapter.end()
    }
  }

  return stairs
}

module.exports = Stairs
<|endoftext|>


<commit_before>import React from 'react';

export default {AsyncComponent};

function AsyncComponent(asyncFunction) {
	return React.createClass({
		displayName: `Transmitter(${asyncFunction.displayName || asyncFunction.name})`,

		getInitialState() {
			return {body: <noscript />};
		},

		update(props) {
			const result = asyncFunction(props);

			// TODO: add failure path
			Promise.resolve(result).then(body => this.setState({body}))
		},

		componentWillMount() {
			this.update(this.props);
		},

		// TODO: add shouldComponentUpdate
		componentWillReceiveProps(nextProps) {
			this.update(nextProps);
		},

		render() {
			return this.state.body;
		}
	});
}
<commit_msg>Added missed semicolon<commit_after>import React from 'react';

export default {AsyncComponent};

function AsyncComponent(asyncFunction) {
	return React.createClass({
		displayName: `Transmitter(${asyncFunction.displayName || asyncFunction.name})`,

		getInitialState() {
			return {body: <noscript />};
		},

		update(props) {
			const result = asyncFunction(props);

			// TODO: add failure path
			Promise.resolve(result).then(body => this.setState({body}));
		},

		componentWillMount() {
			this.update(this.props);
		},

		// TODO: add shouldComponentUpdate
		componentWillReceiveProps(nextProps) {
			this.update(nextProps);
		},

		render() {
			return this.state.body;
		}
	});
}
<|endoftext|>


<commit_before>
import { NativeModules } from 'react-native';

const { RNTensorflow } = NativeModules;

class Tensorflow {

  initWithModel(modelFileName) {
    RNTensorflow.initTensorflow(modelFileName)
  }

  feedWithDims(inputName, src, dims) {
    RNTensorflow.feed(inputName, src, dims);
  }

  feed(inputName, src) {
    RNTensorflow.feed(inputName, src);
  }

  run(outputNames) {
    RNTensorflow.run(outputNames);
  }

  runWithStats(outputNames) {
    RNTensorflow.run(outputNames, true);
  }

  fetch(outputName, outputSize) {
    return RNTensorflow.fetch(outputName, dst);
  }

  graph() {
    return RNTensorflow.graph();
  }

  graphOperation(operationName) {
    return RNTensorflow.graphOperation(operationName);
  }

  stats() {
    return RNTensorflow.stats();
  }

  close() {
    RNTensorflow.close();
  }
}

export default new Tensorflow();
<commit_msg>Allow multiple tensorflow instances in js<commit_after>
import { NativeModules } from 'react-native';

const { RNTensorflow } = NativeModules;

class Tensorflow {

  static initWithModel(modelFileName, cb) {
    tensorflow = new Tensorflow(modelFileName)
    cb(tensorflow)
    tensorflow.close()
  }

  constructor(modelFileName) {
    super()
    RNTensorflow.initTensorflow(modelFileName)
  }

  feedWithDims(inputName, src, dims) {
    RNTensorflow.feed(inputName, src, dims);
  }

  feed(inputName, src) {
    RNTensorflow.feed(inputName, src);
  }

  run(outputNames) {
    RNTensorflow.run(outputNames);
  }

  runWithStats(outputNames) {
    RNTensorflow.run(outputNames, true);
  }

  fetch(outputName, outputSize) {
    return RNTensorflow.fetch(outputName, dst);
  }

  graph() {
    return RNTensorflow.graph();
  }

  stats() {
    return RNTensorflow.stats();
  }

  close() {
    RNTensorflow.close();
  }
}

export default Tensorflow;
<|endoftext|>


<commit_before>var request = require('request');
var separator = require('simple-random-id')();

var getInfobox = function(page, language, cb) {
  var apiURL = 'http://'+ language + '.wikipedia.org/w/api.php?format' +
               '=json&action=query&prop=revisions&rvprop=content&titles=' +
               page;

  var wikiURL = 'http://' + language +'.wikipedia.org/wiki/';

  request.get(apiURL, function(error, data, body){
    if (error) {
      cb(error);
      return;
    }

    // Create a JSON object from the response
    var content = JSON.parse(body);

    // The API answers with quite complex JSON structure, similar to this:
    // {
    //   "query" : {
    //     "pages" : {
    //       "32908": {
    //         "revisions": [
    //           "*": CONTENT OF THE PAGE
    // ]}}}}
    //
    // So that is how we will get the content
    content = content.query.pages;
    var page = Object.keys(content);
    content = content[page].revisions[0]['*'];

    // Let's find the beginning of our infobox, it will start with two
    // mustache brackets (I don't know proper english workd for this),
    // unlimited number of spaces and 'infobox' label (can also start with
    // uppercase char).
    var startingPointRegex = /\{\{\s*[Ii]nfobox/;

    // What is the position of the string we are looking for
    // in the whole document?
    var start = content.match(startingPointRegex).index;
    // And how big is the block. Since we cannot simply match brackets in
    // JS using RegExp, I wrote small `parse` function that iterates throught
    // the document and counts opened and closed brackets. Dumm as hell but
    // works and performance is really good
    var end = parse(content.substr(start, content.length));

    // And we truncate the string exactly where we need
    content = content.substr(start+2, end);

    // This part is very stupid, but I had no time to figure out something
    // smarter.
    // Remove all the new lines, data in the infobox is separated with '|'
    // anyway, so it doesn't change anything.
    content = content.replace(/\n/g, ' ');

    // Now, find all the links ([[IMMA LINK]]) and templates ({{LIKE ME}})
    var result = content.match(/\[\[(.+?)\]\]|\{\{(.+?)\}\}/ig);

    // Iterate thru all of them
    result.forEach(function(link) {
      // And replace each '|' for our custom, random separator string
      content = content.replace(link, link.replace(/\|/g, separator));
    });

    // Because of the stupid thing we did before, we are sure that all the '|'
    // chars (what's the english name for this again?) are separators between
    // fields of the infobox, not special characters in links or templates.
    content = content.split('|');

    // Throw out the first element (it's something like '{{ Infobox', so has
    // no value for us)
    content.shift();

    var output = {};
    // Iterate thru all the fields of the infobox
    content.forEach(function(element) {
      // Every field is a key=value pair, separated by equal sign.
      var splited = element.split('=');
      // Some of them have a lot of white characters what makes no sense at all,
      // so let's trim them.
      splited = splited.map(function(el) {
        return el.trim();
      });

      // This trycatch is needed because sometimes Infobox ends with |}} what is
      // not right (think of it as of additional coma at the end of the array)
      // I know it's not the best way to fix it, but it's the fastest.
      try {
        // Replace previously changed '|' chars again and parse final string
        output[splited[0]] = stringToObject(
          splited[0], // I put this in here because maybe in the future we will
                      // want to determine way of parsing the string based on
                      // type, etc. 'image' field is sometimes just and url,
                      // not a proper wikipedia image ([[File:file_patch.jpg]])
          splited[1].replace(new RegExp(separator, 'g'), '|')
        );

      } catch(error) {
        cb(error);
      }

    });

    // return the output to callback
    cb(null, output);

  });

  var stringToObject = function(name, value) {
    // We can find a lot of objects in one infobox field, so let's grab all of
    // them using simple trick with .replace() callback
    var matches = [];
    value.replace(/\[\[(.*?)\]\]/g, function(g0,g1){matches.push(g1);});
    if (matches.length > 0) {
      var results = [];
      var obj = {
        type: 'link'
      };
      matches.forEach(function(matchElement) {
        obj = {
          type: 'link'
        }

        if (matchElement.indexOf('File:') > -1 || matchElement.indexOf('Image:') > -1) {
          obj.type = 'image';
        }
        matchElement = matchElement.split('|');
        if (matchElement.length > 1) {
          obj.text = matchElement[1];
          obj.url = wikiURL + matchElement[0];
        } else {
          obj.text = matchElement[0];
          obj.url = wikiURL + matchElement[0];
        }
        results.push(obj);
      });

      if (results.length === 1) {
        results = results.pop();
      }
      return results;

    } else {
      return value;
    }
  }

  var parse = function(text) {
    var brackets = 0;

    for (var i=0, l=text.length; i<l; i++) {
      if (text.charAt(i) === '{') {
        brackets++;
      } else if (text.charAt(i) === '}') {
        brackets--;
      }

      if (brackets === 0 && i > 0) {
        return i-1;
      }
    }
  }
}

getInfobox('Warsaw', 'en', function(error, result) {
  if (error) {
    console.log('Error');
    return;
  }

  console.log(result);
});
<commit_msg>[WIKI-INFOBOX] - :bowtie: image/link differentiation logic improved<commit_after>var request = require('request');
var separator = require('simple-random-id')();

var getInfobox = function(page, language, cb) {
  var apiURL = 'http://'+ language + '.wikipedia.org/w/api.php?format' +
               '=json&action=query&prop=revisions&rvprop=content&titles=' +
               page;

  var wikiURL = 'http://' + language +'.wikipedia.org/wiki/';

  request.get(apiURL, function(error, data, body){
    if (error) {
      cb(error);
      return;
    }

    // Create a JSON object from the response
    var content = JSON.parse(body);

    // The API answers with quite complex JSON structure, similar to this:
    // {
    //   "query" : {
    //     "pages" : {
    //       "32908": {
    //         "revisions": [
    //           "*": CONTENT OF THE PAGE
    // ]}}}}
    //
    // So that is how we will get the content
    content = content.query.pages;
    var page = Object.keys(content);
    content = content[page].revisions[0]['*'];

    // Let's find the beginning of our infobox, it will start with two
    // mustache brackets (I don't know proper english workd for this),
    // unlimited number of spaces and 'infobox' label (can also start with
    // uppercase char).
    var startingPointRegex = /\{\{\s*[Ii]nfobox/;

    // What is the position of the string we are looking for
    // in the whole document?
    var start = content.match(startingPointRegex).index;
    // And how big is the block. Since we cannot simply match brackets in
    // JS using RegExp, I wrote small `parse` function that iterates throught
    // the document and counts opened and closed brackets. Dumm as hell but
    // works and performance is really good
    var end = parse(content.substr(start, content.length));

    // And we truncate the string exactly where we need
    content = content.substr(start+2, end);

    // This part is very stupid, but I had no time to figure out something
    // smarter.
    // Remove all the new lines, data in the infobox is separated with '|'
    // anyway, so it doesn't change anything.
    content = content.replace(/\n/g, ' ');

    // Now, find all the links ([[IMMA LINK]]) and templates ({{LIKE ME}})
    var result = content.match(/\[\[(.+?)\]\]|\{\{(.+?)\}\}/ig);

    // Iterate thru all of them
    result.forEach(function(link) {
      // And replace each '|' for our custom, random separator string
      content = content.replace(link, link.replace(/\|/g, separator));
    });

    // Because of the stupid thing we did before, we are sure that all the '|'
    // chars (what's the english name for this again?) are separators between
    // fields of the infobox, not special characters in links or templates.
    content = content.split('|');

    // Throw out the first element (it's something like '{{ Infobox', so has
    // no value for us)
    content.shift();

    var output = {};
    // Iterate thru all the fields of the infobox
    content.forEach(function(element) {
      // Every field is a key=value pair, separated by equal sign.
      var splited = element.split('=');
      // Some of them have a lot of white characters what makes no sense at all,
      // so let's trim them.
      splited = splited.map(function(el) {
        return el.trim();
      });

      // This trycatch is needed because sometimes Infobox ends with |}} what is
      // not right (think of it as of additional coma at the end of the array)
      // I know it's not the best way to fix it, but it's the fastest.
      try {
        // Replace previously changed '|' chars again and parse final string
        output[splited[0]] = stringToObject(
          splited[0], // I put this in here because maybe in the future we will
                      // want to determine way of parsing the string based on
                      // type, etc. 'image' field is sometimes just and url,
                      // not a proper wikipedia image ([[File:file_patch.jpg]])
          splited[1].replace(new RegExp(separator, 'g'), '|')
        );

      } catch(error) {
        cb(error);
      }

    });

    // return the output to callback
    cb(null, output);

  });

  var stringToObject = function(name, value) {
    // We can find a lot of objects in one infobox field, so let's grab all of
    // them using simple trick with .replace() callback
    var matches = [];
    value.replace(/\[\[(.*?)\]\]/g, function(g0,g1){matches.push(g1);});
    if (matches.length > 0) {
      var results = [];
      var obj = {
        type: 'link'
      };
      matches.forEach(function(matchElement) {
        // If it's an image, set the type to image
        if (
          matchElement.indexOf('File:') > -1 ||
          matchElement.indexOf('Image:') > -1
        ) {
          obj.type = 'image';
        } else {
          // If not, its almost always a link
          obj = {
            type: 'link'
          }
        }

        matchElement = matchElement.split('|');
        if (matchElement.length > 1) {
          obj.text = matchElement[1];
          obj.url = wikiURL + matchElement[0];
        } else {
          obj.text = matchElement[0];
          obj.url = wikiURL + matchElement[0];
        }
        results.push(obj);
      });

      if (results.length === 1) {
        results = results.pop();
      }
      return results;

    } else {
      return value;
    }
  }

  var parse = function(text) {
    var brackets = 0;

    for (var i=0, l=text.length; i<l; i++) {
      if (text.charAt(i) === '{') {
        brackets++;
      } else if (text.charAt(i) === '}') {
        brackets--;
      }

      if (brackets === 0 && i > 0) {
        return i-1;
      }
    }
  }
}

getInfobox('Warsaw', 'en', function(error, result) {
  if (error) {
    console.log('Error');
    return;
  }

  console.log(result);
});
<|endoftext|>


<commit_before>#!/usr/bin/env node

'use strict';

var argv = require('optimist').argv;
if (argv.h || argv.help) {
  console.log('\nusage:');
  console.log('-h, --help        : show help');
  console.log('-p, --port        : port (default 5080)');
  console.log('-P, --pouch-port  : pouchdb-server port (default 16984)');
  console.log('-l, --log         : pouchdb-server log level (dev|short|tiny|combined|off)');
  console.log('-r, --remote      : remote fullfatdb (default https://registry.npmjs.org)');
  console.log('-R, --remote-skim : remote skimdb (default https://skimdb.npmjs.com/registry)');
  console.log();
  return process.exit(0);
}

var port = argv.p || argv.port || 5080;
var pouchPort = argv.P || argv['pouch-port'] || 16984;

var NUM_PARALLEL_TASKS = 10;

var FAT_REMOTE = argv.r || argv.remote || 'https://registry.npmjs.org';
var SKIM_REMOTE = argv.R || argv['remote-skim'] || 'https://skimdb.npmjs.com/registry';
var FAT_LOCAL = 'http://localhost:' + pouchPort + '/fullfatdb';
var SKIM_LOCAL = 'http://localhost:' + pouchPort + '/skimdb';

console.log('\nWelcome!');
console.log('To start using local-npm, just run: ');
console.log('\n  $ npm set registry http://127.0.0.1:' + port);
console.log('\nTo switch back, you can run: ');
console.log('\n  $ npm set registry ' + FAT_REMOTE);

var request = require('request');
var Promise = require('bluebird');
var fs = require('fs');
var Fullfat = require('npm-fullfat-registry');
var pouchdbServerLite = require('./pouchdb-server-lite');
var app = pouchdbServerLite.app;
var PouchDB = pouchdbServerLite.PouchDB;

var skimRemote = new PouchDB(SKIM_REMOTE);
var skimPouch = new PouchDB('skimdb');
var fatPouch = new PouchDB('fullfatdb');
// I tend to use more than the default 10 listeners
skimPouch.setMaxListeners(NUM_PARALLEL_TASKS * 100);
fatPouch.setMaxListeners(NUM_PARALLEL_TASKS * 100);

// replicate from central skimdb
var upToDate;
var startingTimeout = 1000;
var backoff = 1.1;
var sync;

function replicateSkim() {
  skimRemote.info().then(function (info) {
    sync = skimPouch.replicate.from(skimRemote, {
      live: true,
      batch_size: 1000
    }).on('change', function (change) {
      var percent = Math.min(100,
        (Math.floor(change.last_seq / info.update_seq * 10000) / 100).toFixed(2));
      console.log('Replicating skimdb, last_seq is: ' + change.last_seq + ' (' + percent + '%)');
    }).on('uptodate', function () {
      console.log('local skimdb is up to date');
      fs.writeFile('uptodate.txt', '1');
      upToDate = true;
    }).on('error', function (err) {
      console.error('error during replication with skimdb');
      console.error(err);
      restartReplication();
    });
  }).catch(function (err) {
    console.error('error doing info() on ' + SKIM_REMOTE);
    console.error(err);
    restartReplication();
  });
}
function restartReplication() {
  // just keep going
  startingTimeout *= backoff;
  setTimeout(replicateSkim, Math.round(startingTimeout));
}
replicateSkim();

var fullFat;

// start doing exciting shit
Promise.resolve().then(function () {
  return Promise.promisify(fs.readFile)('uptodate.txt', {
    encoding: 'utf8'
  }).catch(function (err) {
    return '0'; // default
  });
}).then(function (upToDateFile) {
  upToDate = upToDateFile === '1';
  if (upToDate) {
    console.log('local skimdb is up to date');
  }
}).then(function () {
  // trick Fullfat into never doing automatic syncing; we don't use it for that
  return Promise.promisify(fs.writeFile)('registry.seq', '999999999999');
}).then(function () {
  fullFat = new Fullfat({
    skim: SKIM_LOCAL,
    fat: FAT_LOCAL,
    seq_file: 'registry.seq',
    missing_log: 'missing.log'
  })
  .on('start', function () {
    // we want the changes feed to only be local,
    // but all other operations should be remote
    // if we're not done replicating yet
    process.nextTick(function () {
      fullFat.skim = upToDate ? SKIM_LOCAL : SKIM_REMOTE;
    });
  })
  .on('error', function (err) {
    console.error("fullfat hit an error");
    console.error(err);
  });
}).then(function () {
  // start user-facing proxy server

  // only execute n fullFat.js operations in parallel
  var queues = [];
  for (var i = 0; i < NUM_PARALLEL_TASKS; i++) {
    queues.push(Promise.resolve());
  }

  function processWithFullFat(docId) {
    console.log(docId + ': got request...');
    // recover by fetching with fullFat
    // wait for changes to let us know it was fetched
    var queueIdx = docId.charCodeAt(0) % queues.length;
    queues[queueIdx] = queues[queueIdx].then(function () {
      console.log(docId + ': looking up in pouch');
      return new Promise(function (resolve, reject) {
        var changes = fatPouch.changes({
          since: 'latest',
          live: true
        }).on('change', function onChange(change) {
          console.log(change.id + ': got change');
          if (change.id === docId) {
            console.log('successfully wrote to local fullfatdb: ' + docId);
            changes.cancel();
            resolve();
          }
        }).on('error', function (err) {
          console.error('change hit error');
          reject(err);
        });
        console.log(docId + ': fetching in the background');
        // this seq is only used by fullFat to determine the file name to write tgz's to
        fullFat.getDoc({id: docId, seq: Math.round(Math.random() * 1000000)});
      });
    });
    return queues[queueIdx];
  }

  function updateAfterIncomingChange() {
    // keep a log of what the last seq we checked was
    var queue = Promise.resolve();
    fs.readFile('skim-seq.txt', {encoding: 'utf8'}, function (err, data) {
      var seq = data ? parseInt(data) : 0;
      console.log('reading skimPouch changes since ' + seq);
      skimPouch.changes({since: seq, live: true}).on('change', function (change) {
        queue = queue.then(function () {
          var seqStr = change.seq.toString();
          return Promise.promisify(fs.writeFile)('skim-seq.txt', seqStr);
        }).then(function () {
          return fatPouch.allDocs({keys: [change.id]});
        }).then(function (res) {
          console.log('got res: ' + JSON.stringify(res));
          var first = res.rows[0];
          if (first && first.value && first.value.rev !== change.changes[0].rev) {
            console.log('new change came in for ' + change.id + ', updating...');
            process.nextTick(function () {
              processWithFullFat(change.id);
            });
          }
        }).catch(function (err) {
          console.error('unhandled skimPouch allDocs() err');
          console.error(err);
        });
      }).on('error', function (err) {
        console.error('unhandled skimPouch changes err');
        console.error(err);
      });
    });
  }
  if (upToDate) {
    updateAfterIncomingChange();
  } else {
    skimPouch.on('uptodate', function () {
      fullFat.skim = SKIM_LOCAL; // internal API, probably shouldn't do this
      skimPouch.info().then(function (info) {
        return Promise.promisify(fs.writeFile)('skim-seq.txt',
            info.update_seq.toString()).then(function () {
          updateAfterIncomingChange();
        });
      }).catch(function (err) {
        console.error('unhandled writeFile err');
        console.error(err);
      });
    });
  }

  var server = require('http').createServer(function (req, res) {
    var docId;
    Promise.resolve().then(function () {
      if (req.method.toLowerCase() === 'get') {
        // simple doc or attachment request (get)
        var url = require('url').parse(req.url);
        docId = decodeURIComponent(url.pathname.split('/')[1]);
        if (!docId) {
          return;
        }
        console.log(docId + ': simple doc request');
        // check if it exists first
        return fatPouch.get(docId).catch(function (err) {
          if (err.status === 404) {
            console.log('not found locally: ' + docId);
            process.nextTick(function () {
              processWithFullFat(docId); // exec in background
            });
          }
          throw err;
        });
      } else {
        throw new Error('bad_local_request');
      }
    }).then(function () {
      if (docId) {
        console.log(docId + ': doc exists locally');
      }
      request.get('http://127.0.0.1:' + pouchPort + '/fullfatdb' + req.url).pipe(res);
    }).catch(function (err) {
      if (err.name === 'bad_local_request') {
        console.error('got a bad request: ' + req.url);
        res.status(400).send({error: 'this proxy only accepts GETs'});
      } else {
        console.error(docId + ': no local doc, need to proxy to remote');
        // we got an error, so let's proxy to the remote fat
        // so we can return immediately
        request.get(FAT_REMOTE + req.url).pipe(res);
      }
    });
  });
  server.listen(5080, function (err) {
    if (err) {
      console.error(err);
    } else {
      console.log('Proxy server started at http://127.0.0.1:' + port);
    }
  });
}).catch(function (err) {
  console.error(err);
  throw err;
});

process.on('SIGINT', function () {
  // close gracefully
  sync.cancel();
  Promise.all([fatPouch, skimPouch].map(function (pouch) {
    return pouch.close();
  })).then(function () {
    process.exit(0);
  });
});
<commit_msg>ensure update function only called once<commit_after>#!/usr/bin/env node

'use strict';

var argv = require('optimist').argv;
if (argv.h || argv.help) {
  console.log('\nusage:');
  console.log('-h, --help        : show help');
  console.log('-p, --port        : port (default 5080)');
  console.log('-P, --pouch-port  : pouchdb-server port (default 16984)');
  console.log('-l, --log         : pouchdb-server log level (dev|short|tiny|combined|off)');
  console.log('-r, --remote      : remote fullfatdb (default https://registry.npmjs.org)');
  console.log('-R, --remote-skim : remote skimdb (default https://skimdb.npmjs.com/registry)');
  console.log();
  return process.exit(0);
}

var port = argv.p || argv.port || 5080;
var pouchPort = argv.P || argv['pouch-port'] || 16984;

var NUM_PARALLEL_TASKS = 10;

var FAT_REMOTE = argv.r || argv.remote || 'https://registry.npmjs.org';
var SKIM_REMOTE = argv.R || argv['remote-skim'] || 'https://skimdb.npmjs.com/registry';
var FAT_LOCAL = 'http://localhost:' + pouchPort + '/fullfatdb';
var SKIM_LOCAL = 'http://localhost:' + pouchPort + '/skimdb';

console.log('\nWelcome!');
console.log('To start using local-npm, just run: ');
console.log('\n  $ npm set registry http://127.0.0.1:' + port);
console.log('\nTo switch back, you can run: ');
console.log('\n  $ npm set registry ' + FAT_REMOTE);

var request = require('request');
var Promise = require('bluebird');
var fs = require('fs');
var Fullfat = require('npm-fullfat-registry');
var pouchdbServerLite = require('./pouchdb-server-lite');
var app = pouchdbServerLite.app;
var PouchDB = pouchdbServerLite.PouchDB;

var skimRemote = new PouchDB(SKIM_REMOTE);
var skimPouch = new PouchDB('skimdb');
var fatPouch = new PouchDB('fullfatdb');
// I tend to use more than the default 10 listeners
skimPouch.setMaxListeners(NUM_PARALLEL_TASKS * 100);
fatPouch.setMaxListeners(NUM_PARALLEL_TASKS * 100);

// replicate from central skimdb
var upToDate;
var startingTimeout = 1000;
var backoff = 1.1;
var sync;

function replicateSkim() {
  skimRemote.info().then(function (info) {
    sync = skimPouch.replicate.from(skimRemote, {
      live: true,
      batch_size: 1000
    }).on('change', function (change) {
      var percent = Math.min(100,
        (Math.floor(change.last_seq / info.update_seq * 10000) / 100).toFixed(2));
      console.log('Replicating skimdb, last_seq is: ' + change.last_seq + ' (' + percent + '%)');
    }).on('uptodate', function () {
      console.log('local skimdb is up to date');
      fs.writeFile('uptodate.txt', '1');
      upToDate = true;
    }).on('error', function (err) {
      console.error('error during replication with skimdb');
      console.error(err);
      restartReplication();
    });
  }).catch(function (err) {
    console.error('error doing info() on ' + SKIM_REMOTE);
    console.error(err);
    restartReplication();
  });
}
function restartReplication() {
  // just keep going
  startingTimeout *= backoff;
  setTimeout(replicateSkim, Math.round(startingTimeout));
}
replicateSkim();

var fullFat;

// start doing exciting shit
Promise.resolve().then(function () {
  return Promise.promisify(fs.readFile)('uptodate.txt', {
    encoding: 'utf8'
  }).catch(function (err) {
    return '0'; // default
  });
}).then(function (upToDateFile) {
  upToDate = upToDateFile === '1';
  if (upToDate) {
    console.log('local skimdb is up to date');
  }
}).then(function () {
  // trick Fullfat into never doing automatic syncing; we don't use it for that
  return Promise.promisify(fs.writeFile)('registry.seq', '999999999999');
}).then(function () {
  fullFat = new Fullfat({
    skim: SKIM_LOCAL,
    fat: FAT_LOCAL,
    seq_file: 'registry.seq',
    missing_log: 'missing.log'
  })
  .on('start', function () {
    // we want the changes feed to only be local,
    // but all other operations should be remote
    // if we're not done replicating yet
    process.nextTick(function () {
      fullFat.skim = upToDate ? SKIM_LOCAL : SKIM_REMOTE;
    });
  })
  .on('error', function (err) {
    console.error("fullfat hit an error");
    console.error(err);
  });
}).then(function () {
  // start user-facing proxy server

  // only execute n fullFat.js operations in parallel
  var queues = [];
  for (var i = 0; i < NUM_PARALLEL_TASKS; i++) {
    queues.push(Promise.resolve());
  }

  function processWithFullFat(docId) {
    console.log(docId + ': got request...');
    // recover by fetching with fullFat
    // wait for changes to let us know it was fetched
    var queueIdx = docId.charCodeAt(0) % queues.length;
    queues[queueIdx] = queues[queueIdx].then(function () {
      console.log(docId + ': looking up in pouch');
      return new Promise(function (resolve, reject) {
        var changes = fatPouch.changes({
          since: 'latest',
          live: true
        }).on('change', function onChange(change) {
          console.log(change.id + ': got change');
          if (change.id === docId) {
            console.log('successfully wrote to local fullfatdb: ' + docId);
            changes.cancel();
            resolve();
          }
        }).on('error', function (err) {
          console.error('change hit error');
          reject(err);
        });
        console.log(docId + ': fetching in the background');
        // this seq is only used by fullFat to determine the file name to write tgz's to
        fullFat.getDoc({id: docId, seq: Math.round(Math.random() * 1000000)});
      });
    });
    return queues[queueIdx];
  }

  var alreadyUpdating = false;
  function updateAfterIncomingChange() {
    if (alreadyUpdating) {
      return;
    }
    alreadyUpdating = true;
    // keep a log of what the last seq we checked was
    var queue = Promise.resolve();
    fs.readFile('skim-seq.txt', {encoding: 'utf8'}, function (err, data) {
      var seq = data ? parseInt(data) : 0;
      console.log('reading skimPouch changes since ' + seq);
      skimPouch.changes({since: seq, live: true}).on('change', function (change) {
        queue = queue.then(function () {
          var seqStr = change.seq.toString();
          return Promise.promisify(fs.writeFile)('skim-seq.txt', seqStr);
        }).then(function () {
          return fatPouch.allDocs({keys: [change.id]});
        }).then(function (res) {
          console.log('got res: ' + JSON.stringify(res));
          var first = res.rows[0];
          if (first && first.value && first.value.rev !== change.changes[0].rev) {
            console.log('new change came in for ' + change.id + ', updating...');
            process.nextTick(function () {
              processWithFullFat(change.id);
            });
          }
        }).catch(function (err) {
          console.error('unhandled skimPouch allDocs() err');
          console.error(err);
        });
      }).on('error', function (err) {
        console.error('unhandled skimPouch changes err');
        console.error(err);
      });
    });
  }
  if (upToDate) {
    updateAfterIncomingChange();
  } else {
    skimPouch.on('uptodate', function () {
      fullFat.skim = SKIM_LOCAL; // internal API, probably shouldn't do this
      skimPouch.info().then(function (info) {
        return Promise.promisify(fs.writeFile)('skim-seq.txt',
            info.update_seq.toString()).then(function () {
          updateAfterIncomingChange();
        });
      }).catch(function (err) {
        console.error('unhandled writeFile err');
        console.error(err);
      });
    });
  }

  var server = require('http').createServer(function (req, res) {
    var docId;
    Promise.resolve().then(function () {
      if (req.method.toLowerCase() === 'get') {
        // simple doc or attachment request (get)
        var url = require('url').parse(req.url);
        docId = decodeURIComponent(url.pathname.split('/')[1]);
        if (!docId) {
          return;
        }
        console.log(docId + ': simple doc request');
        // check if it exists first
        return fatPouch.get(docId).catch(function (err) {
          if (err.status === 404) {
            console.log('not found locally: ' + docId);
            process.nextTick(function () {
              processWithFullFat(docId); // exec in background
            });
          }
          throw err;
        });
      } else {
        throw new Error('bad_local_request');
      }
    }).then(function () {
      if (docId) {
        console.log(docId + ': doc exists locally');
      }
      request.get('http://127.0.0.1:' + pouchPort + '/fullfatdb' + req.url).pipe(res);
    }).catch(function (err) {
      if (err.name === 'bad_local_request') {
        console.error('got a bad request: ' + req.url);
        res.status(400).send({error: 'this proxy only accepts GETs'});
      } else {
        console.error(docId + ': no local doc, need to proxy to remote');
        // we got an error, so let's proxy to the remote fat
        // so we can return immediately
        request.get(FAT_REMOTE + req.url).pipe(res);
      }
    });
  });
  server.listen(5080, function (err) {
    if (err) {
      console.error(err);
    } else {
      console.log('Proxy server started at http://127.0.0.1:' + port);
    }
  });
}).catch(function (err) {
  console.error(err);
  throw err;
});

process.on('SIGINT', function () {
  // close gracefully
  sync.cancel();
  Promise.all([fatPouch, skimPouch].map(function (pouch) {
    return pouch.close();
  })).then(function () {
    process.exit(0);
  });
});
<|endoftext|>


<commit_before>/**
 * @Author: Chris Moyer <cmoyer@newstex.com>
 * @Description: An "Event Q" or "Waiting Counter", based on EventEmitter,
 * this allows you to call a bunch of asynchronous functions, and have a callback
 * called whenever they are all completed.
 *
 * Usage:
 *   var q = new EventQ();
 *
 *   items.forEach(function(item){
 *     // Add to our counter
 *     q.add();
 *     my_async_function_with_callback(item, function(){
 *       // Pop one off our counter
 *       // "val" is optional, and lets you return something
 *       // to the "ready" handler
 *       q.done(val);
 *     });
 *   });
 *
 *   q.add(function(done){
 *     // do async task
 *     done();
 *   });
 *
 *
 *
 *   q.on('ready', function(vals){
 *     console.log('Ready!');
 *   });
 *
 */
'use strict';

var events = require('events');
var util = require('util');

/**
 * Create a new EventQ
 * Keeps an internal counter, when the counter reaches zero it fires a "ready" event
 * You should emit "
 * @param count: An optional count to start at, defaults to 0
 * @param max_wait_time: An optional maximum time to wait before triggering ready
 */
function EventQ(count, max_wait_time){
	var self = this;
	events.EventEmitter.call(this);
	self.counter = count || 0;
	self.vals = [];
	if(max_wait_time){
		self.timeout = setTimeout(function eventQTimeout(){
			if(self.counter !== null){
				self.counter = null;
				self.emit('ready', self.vals);
			}
		}, max_wait_time);
	}
}
util.inherits(EventQ, events.EventEmitter);

/**
 * Add one to our counter
 */
EventQ.prototype.add = function eventQ_add(fnc){
	// Only add to the counter if we haven't set it to disabled
	if(this.counter !== null){
		this.counter++;
	}
	// Allow passing in a function to be run asynchronously
	if(fnc){
		var self = this;
		// Only allow the "completed" function to fire once
		var completed = false;
		// Fire this asynchronously
		setImmediate(fnc, function complete(val){
			if(!completed){
				completed = true;
				self.done(val);
			} else {
				console.warn('WARNING: Completion event fired multiple times');
			}
		});
	}
};

/**
 * Delete one from our counter
 */
EventQ.prototype.done = function eventQ_done(val){
	// If we've disabled the counter, it's because we've already fired
	if(this.counter === null){
		return;
	}
	this.counter--;
	// Add this value if it is not undefined
	if (val !== undefined){
		this.vals.push(val);
	}
	// If we're at zero, fire the ready event
	if(this.counter <= 0){
		this.counter = null;
		this.emit('ready', this.vals);
	}
};

module.exports = EventQ;

<commit_msg>Submitting a count of 0 will emit a ready event in 100ms<commit_after>/**
 * @Author: Chris Moyer <cmoyer@newstex.com>
 * @Description: An "Event Q" or "Waiting Counter", based on EventEmitter,
 * this allows you to call a bunch of asynchronous functions, and have a callback
 * called whenever they are all completed.
 *
 * Usage:
 *   var q = new EventQ();
 *
 *   items.forEach(function(item){
 *     // Add to our counter
 *     q.add();
 *     my_async_function_with_callback(item, function(){
 *       // Pop one off our counter
 *       // "val" is optional, and lets you return something
 *       // to the "ready" handler
 *       q.done(val);
 *     });
 *   });
 *
 *   q.add(function(done){
 *     // do async task
 *     done();
 *   });
 *
 *
 *
 *   q.on('ready', function(vals){
 *     console.log('Ready!');
 *   });
 *
 */
'use strict';

var events = require('events');
var util = require('util');

/**
 * Create a new EventQ
 * Keeps an internal counter, when the counter reaches zero it fires a "ready" event
 * You should emit "
 * @param count: An optional count to start at, defaults to 0
 * @param max_wait_time: An optional maximum time to wait before triggering ready
 */
function EventQ(count, max_wait_time){
	var self = this;
	events.EventEmitter.call(this);
	self.counter = count || 0;
	self.vals = [];
	// If we submit with a count of 0, emit that we're ready
	if(count === 0){
		setTimeout(function(){
			self.emit('ready', self.vals);
		}, 100);
	} else if(max_wait_time){
		self.timeout = setTimeout(function eventQTimeout(){
			if(self.counter !== null){
				self.counter = null;
				self.emit('ready', self.vals);
			}
		}, max_wait_time);
	}

}
util.inherits(EventQ, events.EventEmitter);

/**
 * Add one to our counter
 */
EventQ.prototype.add = function eventQ_add(fnc){
	// Only add to the counter if we haven't set it to disabled
	if(this.counter !== null){
		this.counter++;
	}
	// Allow passing in a function to be run asynchronously
	if(fnc){
		var self = this;
		// Only allow the "completed" function to fire once
		var completed = false;
		// Fire this asynchronously
		setImmediate(fnc, function complete(val){
			if(!completed){
				completed = true;
				self.done(val);
			} else {
				console.warn('WARNING: Completion event fired multiple times');
			}
		});
	}
};

/**
 * Delete one from our counter
 */
EventQ.prototype.done = function eventQ_done(val){
	// If we've disabled the counter, it's because we've already fired
	if(this.counter === null){
		return;
	}
	this.counter--;
	// Add this value if it is not undefined
	if (val !== undefined){
		this.vals.push(val);
	}
	// If we're at zero, fire the ready event
	if(this.counter <= 0){
		this.counter = null;
		this.emit('ready', this.vals);
	}
};

module.exports = EventQ;

<|endoftext|>