<commit_before>import React from 'react';
import ViewerHelper from './viewer-helper';
import ViewerEvent from './viewer-event';
import cursor from './cursor';
import {calculateBox} from './utils';
import {
  TOOL_NONE,
  TOOL_PAN,
  TOOL_ZOOM,
  MODE_IDLE,
  MODE_PANNING,
  MODE_ZOOMING,
} from './constants';

export default class Viewer extends React.Component {

  constructor(props) {
    super(props);
    this.handleSpecialKeyChange = this.handleSpecialKeyChange.bind(this);
  }

  handleStartPan(event) {
    let x = event.nativeEvent.offsetX, y = event.nativeEvent.offsetY;
    let {value, tool, onChange} = this.props;

    if (tool !== TOOL_PAN) return;
    if (value.mode !== MODE_IDLE) return;

    let nextValue = ViewerHelper.startPan(value, x, y);

    event.preventDefault();
    onChange(new ViewerEvent(event, nextValue));
  }

  handleUpdatePan(event) {
    let x = event.nativeEvent.offsetX, y = event.nativeEvent.offsetY;
    let {value, tool, onChange, width, height, children} = this.props;
    let SVGWidth = children.props.width, SVGHeight = children.props.height;

    if (tool !== TOOL_PAN) return;
    if (value.mode !== MODE_PANNING) return;

    //the mouse exited and reentered into svg
    let forceExit = (value.mode === MODE_PANNING && event.buttons === 0);

    let nextValue = forceExit ?
      ViewerHelper.stopPan(value) :
      ViewerHelper.updatePan(value, x, y, 20, SVGWidth, SVGHeight, width, height);

    event.preventDefault();
    onChange(new ViewerEvent(event, nextValue));
  }

  handleStopPan(event) {
    let x = event.nativeEvent.offsetX, y = event.nativeEvent.offsetY;
    let {value, tool, onChange} = this.props;

    if (tool !== TOOL_PAN) return;
    if (value.mode !== MODE_PANNING) return;

    let nextValue = ViewerHelper.stopPan(value, x, y);

    event.preventDefault();
    onChange(new ViewerEvent(event, nextValue));
  }

  handleStartZoom(event) {
    let x = event.nativeEvent.offsetX, y = event.nativeEvent.offsetY;
    let {value, tool, onChange, children} = this.props;
    let SVGWidth = children.props.width, SVGHeight = children.props.height;

    if (tool !== TOOL_ZOOM) return;
    if (value.mode !== MODE_IDLE) return;

    let point = ViewerHelper.getSVGPoint(value, x, y);
    if(!ViewerHelper.isPointInsideSVG(point.x, point.y, SVGWidth, SVGHeight)) return;

    let nextValue = ViewerHelper.startZoomSelection(value, x, y);

    event.preventDefault();
    onChange(new ViewerEvent(event, nextValue));
  }

  handleUpdateZoom(event) {
    let x = event.nativeEvent.offsetX, y = event.nativeEvent.offsetY;
    let {value, tool, onChange, width, height} = this.props;

    if (tool !== TOOL_ZOOM) return;
    if (value.mode !== MODE_ZOOMING) return;

    //the mouse exited and reentered into svg
    let forceExit = (event.buttons === 0);

    let nextValue = forceExit ?
      ViewerHelper.stopZoomSelection(value, width, height)
      : ViewerHelper.updateZoomSelection(value, x, y);

    event.preventDefault();
    onChange(new ViewerEvent(event, nextValue));
  }

  handleStopZoom(event) {
    let abs = Math.abs;
    let x = event.nativeEvent.offsetX, y = event.nativeEvent.offsetY;
    let {value, tool, onChange, width, height} = this.props;
    let {startX, endX, startY, endY, specialKeyEnabled} = value;

    if (tool !== TOOL_ZOOM) return;
    if (value.mode !== MODE_ZOOMING) return;

    let selectionMode = abs(startX - endX) > 2 && abs(startY - endY) > 2;

    let nextValue;

    if (selectionMode) {
      nextValue = ViewerHelper.stopZoomSelection(value, width, height);
    } else {
      let scaleFactor = specialKeyEnabled ? 0.9 : 1.1;
      nextValue = ViewerHelper.zoom(value, scaleFactor, x, y);
    }

    event.preventDefault();
    onChange(new ViewerEvent(event, nextValue));
  }

  handleClick(event) {
    let {value, tool, onClick, children} = this.props;
    let SVGWidth = children.props.width, SVGHeight = children.props.height;

    if (tool !== TOOL_NONE) return;
    if (!onClick) return;

    let viewerEvent = new ViewerEvent(event, value);
    if(!ViewerHelper.isPointInsideSVG(viewerEvent.x, viewerEvent.y, SVGWidth, SVGHeight)) return;

    onClick(viewerEvent);
  }

  handleMouseUp(event) {
    let {value, tool, onMouseUp, children} = this.props;
    let x = event.offsetX, y = event.offsetY;
    let SVGWidth = children.props.width, SVGHeight = children.props.height;

    if (tool !== TOOL_NONE) return;
    if (!onMouseUp) return;

    let viewerEvent = new ViewerEvent(event, value);
    if(!ViewerHelper.isPointInsideSVG(viewerEvent.x, viewerEvent.y, SVGWidth, SVGHeight)) return;

    onMouseUp(viewerEvent);
  }

  handleMouseDown(event) {
    let {value, tool, onMouseDown, children} = this.props;
    let x = event.offsetX, y = event.offsetY;
    let SVGWidth = children.props.width, SVGHeight = children.props.height;

    if (tool !== TOOL_NONE) return;
    if (!onMouseDown) return;

    let viewerEvent = new ViewerEvent(event, value);
    if(!ViewerHelper.isPointInsideSVG(viewerEvent.x, viewerEvent.y, SVGWidth, SVGHeight)) return;

    onMouseDown(viewerEvent);
  }

  handleMouseMove(event) {
    let {value, tool, onMouseMove, children} = this.props;
    let x = event.offsetX, y = event.offsetY;
    let SVGWidth = children.props.width, SVGHeight = children.props.height;

    if (tool !== TOOL_NONE) return;
    if (!onMouseMove) return;

    let viewerEvent = new ViewerEvent(event, value);
    if(!ViewerHelper.isPointInsideSVG(viewerEvent.x, viewerEvent.y, SVGWidth, SVGHeight)) return;

    onMouseMove(viewerEvent);
  }

  handleSpecialKeyChange(event) {
    let {value, onChange} = this.props;
    let key = event.which;
    let active = event.type === "keydown";

    if ([18].indexOf(key) === -1) return;

    let nextValue = active ? ViewerHelper.enableSpecialKey(value) : ViewerHelper.disableSpecialKey(value);

    onChange(new ViewerEvent(event, nextValue));
  }

  componentWillMount(event) {
    window.addEventListener("keydown", this.handleSpecialKeyChange, false);
    window.addEventListener("keyup", this.handleSpecialKeyChange, false);
  }

  componentWillUnmount(event) {
    window.removeEventListener("keydown", this.handleSpecialKeyChange, false);
    window.removeEventListener("keyup", this.handleSpecialKeyChange, false);
  }

  render() {
    let originalSVG = this.props.children;
    let tool = this.props.tool;
    let {matrix, mode, specialKeyEnabled} = this.props.value;
    let matrixStr = `matrix(${matrix.a}, ${matrix.b}, ${matrix.c}, ${matrix.d}, ${matrix.e}, ${matrix.f})`;

    let style = {};
    let gStyle = {};
    if (tool === TOOL_PAN) style.cursor = cursor(mode === MODE_PANNING ? 'grabbing' : 'grab');
    if (tool === TOOL_ZOOM) gStyle.cursor = cursor(specialKeyEnabled ? 'zoom-out' : 'zoom-in');

    let zoomSelectionRect;
    if (mode === MODE_ZOOMING) {
      let {startX, startY, endX, endY} = this.props.value;
      let box = calculateBox({x: startX, y: startY}, {x: endX, y: endY});

      zoomSelectionRect =
        <rect
          stroke="#969FFF"
          strokeOpacity={0.7}
          fill="#F3F4FF"
          fillOpacity={0.7}
          x={box.x}
          y={box.y}
          width={box.width}
          height={box.height}/>
    }

    return (
      <svg
        ref="svg"
        width={this.props.width}
        height={this.props.height}
        style={Object.assign(style, this.props.style)}
        onMouseDown={ event => {this.handleMouseDown(event); this.handleStartPan(event);  this.handleStartZoom(event)} }
        onMouseMove={ event => {this.handleMouseMove(event); this.handleUpdatePan(event); this.handleUpdateZoom(event)} }
        onMouseUp={ event =>   {this.handleMouseUp(event);   this.handleStopPan(event);   this.handleStopZoom(event)} }
        onClick={event => this.handleClick(event)}
      >

        <rect
          fill={this.props.background}
          x={0}
          y={0}
          width={this.props.width}
          height={this.props.height}/>

        <g ref="originalSvg" transform={matrixStr} style={gStyle}>
          <rect
            fill={this.props.SVGBackground}
            x={0}
            y={0}
            width={originalSVG.props.width}
            height={originalSVG.props.height}/>
          <g ref="content">
            {originalSVG.props.children}
          </g>
        </g>
        {zoomSelectionRect}
      </svg>
    );
  }
}

Viewer.propTypes = {
  //width of the viewer displayed on screen
  width: React.PropTypes.number.isRequired,

  //height of the viewer displayed on screen
  height: React.PropTypes.number.isRequired,

  //background of the viewer
  background: React.PropTypes.string,

  //background of the svg
  SVGBackground: React.PropTypes.string,

  //value of the viewer (current point of view)
  value: React.PropTypes.object.isRequired,

  //CSS style of the SVG tag
  style: React.PropTypes.object,

  //handler something changed
  onChange: React.PropTypes.func.isRequired,

  //handler click
  onClick: React.PropTypes.func,

  //handler mouseup
  onMouseUp: React.PropTypes.func,

  //handler mousemove
  onMouseMove: React.PropTypes.func,

  //handler mousedown
  onMouseDown: React.PropTypes.func,

  //current active tool (TOOL_NONE, TOOL_PAN, TOOL_ZOOM)
  tool: React.PropTypes.oneOf([TOOL_NONE, TOOL_PAN, TOOL_ZOOM]),

  //accept only one node SVG
  children: function (props, propName, componentName) {
    // Only accept a single child, of the appropriate type
    //credits: http://www.mattzabriskie.com/blog/react-validating-children
    var prop = props[propName];
    var types = ['svg'];
    if (React.Children.count(prop) !== 1 ||
      types.indexOf(prop.type) === -1) {
      return new Error(
        '`' + componentName + '` ' +
        'should have a single child of the following types: ' +
        ' `' + types.join('`, `') + '`.'
      );
    }
    if (!prop.props.hasOwnProperty('width') || !prop.props.hasOwnProperty('height')) {
      return new Error('SVG should have props `width` and `height`');
    }

  }
};

Viewer.defaultProps = {
  style: {},
  background: "#616264",
  SVGBackground: "#fff",
  tool: TOOL_NONE
};
<commit_msg>fix svg event target<commit_after>import React from 'react';
import ViewerHelper from './viewer-helper';
import ViewerEvent from './viewer-event';
import cursor from './cursor';
import {calculateBox} from './utils';
import {
  TOOL_NONE,
  TOOL_PAN,
  TOOL_ZOOM,
  MODE_IDLE,
  MODE_PANNING,
  MODE_ZOOMING,
} from './constants';

export default class Viewer extends React.Component {

  constructor(props) {
    super(props);
    this.handleSpecialKeyChange = this.handleSpecialKeyChange.bind(this);
  }

  handleStartPan(event) {
    let x = event.nativeEvent.offsetX, y = event.nativeEvent.offsetY;
    let {value, tool, onChange} = this.props;

    if (tool !== TOOL_PAN) return;
    if (value.mode !== MODE_IDLE) return;

    let nextValue = ViewerHelper.startPan(value, x, y);

    event.preventDefault();
    onChange(new ViewerEvent(event, nextValue));
  }

  handleUpdatePan(event) {
    let x = event.nativeEvent.offsetX, y = event.nativeEvent.offsetY;
    let {value, tool, onChange, width, height, children} = this.props;
    let SVGWidth = children.props.width, SVGHeight = children.props.height;

    if (tool !== TOOL_PAN) return;
    if (value.mode !== MODE_PANNING) return;

    //the mouse exited and reentered into svg
    let forceExit = (value.mode === MODE_PANNING && event.buttons === 0);

    let nextValue = forceExit ?
      ViewerHelper.stopPan(value) :
      ViewerHelper.updatePan(value, x, y, 20, SVGWidth, SVGHeight, width, height);

    event.preventDefault();
    onChange(new ViewerEvent(event, nextValue));
  }

  handleStopPan(event) {
    let x = event.nativeEvent.offsetX, y = event.nativeEvent.offsetY;
    let {value, tool, onChange} = this.props;

    if (tool !== TOOL_PAN) return;
    if (value.mode !== MODE_PANNING) return;

    let nextValue = ViewerHelper.stopPan(value, x, y);

    event.preventDefault();
    onChange(new ViewerEvent(event, nextValue));
  }

  handleStartZoom(event) {
    let x = event.nativeEvent.offsetX, y = event.nativeEvent.offsetY;
    let {value, tool, onChange, children} = this.props;
    let SVGWidth = children.props.width, SVGHeight = children.props.height;

    if (tool !== TOOL_ZOOM) return;
    if (value.mode !== MODE_IDLE) return;

    let point = ViewerHelper.getSVGPoint(value, x, y);
    if(!ViewerHelper.isPointInsideSVG(point.x, point.y, SVGWidth, SVGHeight)) return;

    let nextValue = ViewerHelper.startZoomSelection(value, x, y);

    event.preventDefault();
    onChange(new ViewerEvent(event, nextValue));
  }

  handleUpdateZoom(event) {
    let x = event.nativeEvent.offsetX, y = event.nativeEvent.offsetY;
    let {value, tool, onChange, width, height} = this.props;

    if (tool !== TOOL_ZOOM) return;
    if (value.mode !== MODE_ZOOMING) return;

    //the mouse exited and reentered into svg
    let forceExit = (event.buttons === 0);

    let nextValue = forceExit ?
      ViewerHelper.stopZoomSelection(value, width, height)
      : ViewerHelper.updateZoomSelection(value, x, y);

    event.preventDefault();
    onChange(new ViewerEvent(event, nextValue));
  }

  handleStopZoom(event) {
    let abs = Math.abs;
    let x = event.nativeEvent.offsetX, y = event.nativeEvent.offsetY;
    let {value, tool, onChange, width, height} = this.props;
    let {startX, endX, startY, endY, specialKeyEnabled} = value;

    if (tool !== TOOL_ZOOM) return;
    if (value.mode !== MODE_ZOOMING) return;

    let selectionMode = abs(startX - endX) > 2 && abs(startY - endY) > 2;

    let nextValue;

    if (selectionMode) {
      nextValue = ViewerHelper.stopZoomSelection(value, width, height);
    } else {
      let scaleFactor = specialKeyEnabled ? 0.9 : 1.1;
      nextValue = ViewerHelper.zoom(value, scaleFactor, x, y);
    }

    event.preventDefault();
    onChange(new ViewerEvent(event, nextValue));
  }

  handleClick(event) {
    let {value, tool, onClick, children} = this.props;
    let SVGWidth = children.props.width, SVGHeight = children.props.height;

    if (tool !== TOOL_NONE) return;
    if (!onClick) return;

    let viewerEvent = new ViewerEvent(event, value);
    if(!ViewerHelper.isPointInsideSVG(viewerEvent.x, viewerEvent.y, SVGWidth, SVGHeight)) return;

    onClick(viewerEvent);
  }

  handleMouseUp(event) {
    let {value, tool, onMouseUp, children} = this.props;
    let x = event.offsetX, y = event.offsetY;
    let SVGWidth = children.props.width, SVGHeight = children.props.height;

    if (tool !== TOOL_NONE) return;
    if (!onMouseUp) return;

    let viewerEvent = new ViewerEvent(event, value);
    if(!ViewerHelper.isPointInsideSVG(viewerEvent.x, viewerEvent.y, SVGWidth, SVGHeight)) return;

    onMouseUp(viewerEvent);
  }

  handleMouseDown(event) {
    let {value, tool, onMouseDown, children} = this.props;
    let x = event.offsetX, y = event.offsetY;
    let SVGWidth = children.props.width, SVGHeight = children.props.height;

    if (tool !== TOOL_NONE) return;
    if (!onMouseDown) return;

    let viewerEvent = new ViewerEvent(event, value);
    if(!ViewerHelper.isPointInsideSVG(viewerEvent.x, viewerEvent.y, SVGWidth, SVGHeight)) return;

    onMouseDown(viewerEvent);
  }

  handleMouseMove(event) {
    let {value, tool, onMouseMove, children} = this.props;
    let x = event.offsetX, y = event.offsetY;
    let SVGWidth = children.props.width, SVGHeight = children.props.height;

    if (tool !== TOOL_NONE) return;
    if (!onMouseMove) return;

    let viewerEvent = new ViewerEvent(event, value);
    if(!ViewerHelper.isPointInsideSVG(viewerEvent.x, viewerEvent.y, SVGWidth, SVGHeight)) return;

    onMouseMove(viewerEvent);
  }

  handleSpecialKeyChange(event) {
    let {value, onChange} = this.props;
    let key = event.which;
    let active = event.type === "keydown";

    if ([18].indexOf(key) === -1) return;

    let nextValue = active ? ViewerHelper.enableSpecialKey(value) : ViewerHelper.disableSpecialKey(value);

    onChange(new ViewerEvent(event, nextValue));
  }

  componentWillMount(event) {
    window.addEventListener("keydown", this.handleSpecialKeyChange, false);
    window.addEventListener("keyup", this.handleSpecialKeyChange, false);
  }

  componentWillUnmount(event) {
    window.removeEventListener("keydown", this.handleSpecialKeyChange, false);
    window.removeEventListener("keyup", this.handleSpecialKeyChange, false);
  }

  render() {
    let originalSVG = this.props.children;
    let tool = this.props.tool;
    let {matrix, mode, specialKeyEnabled} = this.props.value;
    let matrixStr = `matrix(${matrix.a}, ${matrix.b}, ${matrix.c}, ${matrix.d}, ${matrix.e}, ${matrix.f})`;

    let style = {};
    let gStyle = {pointerEvents: "none"};
    if (tool === TOOL_PAN) style.cursor = cursor(mode === MODE_PANNING ? 'grabbing' : 'grab');
    if (tool === TOOL_ZOOM) gStyle.cursor = cursor(specialKeyEnabled ? 'zoom-out' : 'zoom-in');

    let zoomSelectionRect;
    if (mode === MODE_ZOOMING) {
      let {startX, startY, endX, endY} = this.props.value;
      let box = calculateBox({x: startX, y: startY}, {x: endX, y: endY});

      zoomSelectionRect =
        <rect
          stroke="#969FFF"
          strokeOpacity={0.7}
          fill="#F3F4FF"
          fillOpacity={0.7}
          x={box.x}
          y={box.y}
          width={box.width}
          height={box.height}/>
    }

    return (
      <svg
        ref="svg"
        width={this.props.width}
        height={this.props.height}
        style={Object.assign(style, this.props.style)}
        onMouseDown={ event => {this.handleMouseDown(event); this.handleStartPan(event);  this.handleStartZoom(event)} }
        onMouseMove={ event => {this.handleMouseMove(event); this.handleUpdatePan(event); this.handleUpdateZoom(event)} }
        onMouseUp={ event =>   {this.handleMouseUp(event);   this.handleStopPan(event);   this.handleStopZoom(event)} }
        onClick={event => this.handleClick(event)}
      >

        <rect
          fill={this.props.background}
          x={0}
          y={0}
          width={this.props.width}
          height={this.props.height}
          style={{pointerEvents: "none"}}
        />

        <g ref="originalSvg" transform={matrixStr} style={gStyle}>
          <rect
            fill={this.props.SVGBackground}
            x={0}
            y={0}
            width={originalSVG.props.width}
            height={originalSVG.props.height}/>
          <g ref="content">
            {originalSVG.props.children}
          </g>
        </g>
        {zoomSelectionRect}
      </svg>
    );
  }
}

Viewer.propTypes = {
  //width of the viewer displayed on screen
  width: React.PropTypes.number.isRequired,

  //height of the viewer displayed on screen
  height: React.PropTypes.number.isRequired,

  //background of the viewer
  background: React.PropTypes.string,

  //background of the svg
  SVGBackground: React.PropTypes.string,

  //value of the viewer (current point of view)
  value: React.PropTypes.object.isRequired,

  //CSS style of the SVG tag
  style: React.PropTypes.object,

  //handler something changed
  onChange: React.PropTypes.func.isRequired,

  //handler click
  onClick: React.PropTypes.func,

  //handler mouseup
  onMouseUp: React.PropTypes.func,

  //handler mousemove
  onMouseMove: React.PropTypes.func,

  //handler mousedown
  onMouseDown: React.PropTypes.func,

  //current active tool (TOOL_NONE, TOOL_PAN, TOOL_ZOOM)
  tool: React.PropTypes.oneOf([TOOL_NONE, TOOL_PAN, TOOL_ZOOM]),

  //accept only one node SVG
  children: function (props, propName, componentName) {
    // Only accept a single child, of the appropriate type
    //credits: http://www.mattzabriskie.com/blog/react-validating-children
    var prop = props[propName];
    var types = ['svg'];
    if (React.Children.count(prop) !== 1 ||
      types.indexOf(prop.type) === -1) {
      return new Error(
        '`' + componentName + '` ' +
        'should have a single child of the following types: ' +
        ' `' + types.join('`, `') + '`.'
      );
    }
    if (!prop.props.hasOwnProperty('width') || !prop.props.hasOwnProperty('height')) {
      return new Error('SVG should have props `width` and `height`');
    }

  }
};

Viewer.defaultProps = {
  style: {},
  background: "#616264",
  SVGBackground: "#fff",
  tool: TOOL_NONE
};
<|endoftext|>


<commit_before>!function (definition) {
  if (typeof module == "object" && module.exports) module.exports = definition();
  else if (typeof define == "function" && typeof define.amd == "object") define(definition);
  else this.tz = definition();
} (function () {
  var __slice = [].slice, __push = [].push;
/*
  function die () {
    console.log.apply(console, __slice.call(arguments, 0));
    return process.exit(1);
  };

  function say () { return console.log.apply(console, __slice.call(arguments, 0)) }
*/
  function format (posix, rest) {
    var wallclock = new Date(convertToWallclock(this, posix)), self = this;
    return rest.replace(/%([-0_^]|:{0,3})?(\d*)(.)/g, function (matched, flag, padding, specifier) {
      var f, value = matched, fill = "0";
      if (f = self[specifier]) {
        value = String(f.call(self, wallclock, posix, (flag || "").length));
        if ((flag || f.style) == "_") fill = " ";
        pad = parseInt(flag == "-" ? 0 : (f.pad || 0));
        while (value.length < pad) value = fill + value;
        pad = parseInt(flag == "-" ? 0 : (padding || f.pad));
        while (value.length < pad) value = fill + value;
        if (specifier == "N" && pad < value.length) value = value.slice(0, pad);
        if (flag == "^") value = value.toUpperCase();
      }
      return value;
    });
  };

  function makeDate (request, date) {
    var posix, z, i, I;
    if (~(z = date.indexOf('+')) || ~(z = date.indexOf('-'))) {
      date[z] += 1;
      posix = true;
    } else {
      date[z = date.length] = 1;
    }
    for (i = 0; i < 11; i++) {
      date[i] = date[i] || 0;
    }
    --date[1];
    date = Date.UTC.apply(Date.UTC, date.slice(0, z)) + -date[z] * (date[z + 1] * 36e5 + date[z + 2] * 6e4 + date[z + 3] * 1e3);
    return posix ? date : convertToPOSIX(request, date);
  }

  function parse (request, pattern) {
    var parts = pattern.split(/T|\s/), date = [], match;
    if (0 < parts.length && parts.length < 3) {
      if (match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(parts[0])) {
        __push.apply(date, match.slice(1, 4));
        parts.shift();
      } else {
        __push.apply(date, request.convert([ request.clock(), "%Y-%m-%d" ]).split(/-/));
      }
      if (parts[0]) {
        if (match = /^(\d{2}):(\d{2})(?::(\d{2})(\.(\d+))?)?(Z|(([+-])(\d{2}(:\d{2}){0,2})))?$/.exec(parts[0])) {
          __push.apply(date, match.slice(1, 4));
          __push.call(date, match[5] || 0);
          if (match[7]) {
            __push.call(date, match[8]);
            __push.apply(date, match[9].split(/:/));
          } else if (match[6]) {
            __push.call(date, "+");
          }
        } else return;
      }
      return makeDate(request, date);
    }
  }

  function actualize (entry, rule, year) {
    var actualized, fields, date = rule.day[1];

    do {
      fields = new Date(Date.UTC(year, rule.month, Math.abs(date++)));
    } while (fields.getUTCMonth() != rule.month || (rule.day[0] >= 0 && fields.getUTCDay() != rule.day[0]))

    actualized = {
      clock: rule.clock,
      sortable: fields.getTime(),
      rule: rule,
      save: rule.save * 6e4,
      offset: entry.offset
    };

    // TODO Why is `saved` 0?
    actualized[actualized.clock] = fields.getTime() + rule.time * 6e4;

    if (actualized.clock == "posix") {
      actualized.wallclock = actualized.posix + (entry.offset + rule.saved);
    } else {
      actualized.posix = actualized.wallclock - (entry.offset + rule.saved);
    }

    return actualized;
  }

  function getYear (time) { return new Date(time).getUTCFullYear() }

  function applicable (entry, rules, actualized, time, clock) {
    var last, i, I, j, year = getYear(time);
    for (j = year + 1; j >= year - 1; --j) {
      for (i = 0, I = rules.length; i < I; i++) {
          if (rules[i].from <= j && j <= rules[i].to) {
            actualized.push(actualize(entry, rules[i], j, true));
          } else if (rules[i].to < j) {
            last = rules[i].to;
            break;
        }
      }
    }
    return last;
  }

  function find (request, clock, time) {
    var i, I, entry, year = getYear(time), found, zone = request[request.zone], actualized = [], to, abbrevs;
    for (i = 0, I = zone.length; i < I; i++) if (zone[i][clock] <= time) break;
    entry = zone[i];
    if (entry.rules) {
      rules = request[entry.rules];
      to = applicable(entry, rules, actualized, time, clock);
      if (to != null) applicable(entry, rules, actualized, Date.UTC(to, 5, 1), "wallclock");
      if (actualized.length) {
        actualized.sort(function (a, b) { return a.sortable - b.sortable });
        for (i = 0, I = actualized.length; i < I; i++) {
          if (time >= actualized[i][clock] && actualized[i][actualized[i].clock] > entry[actualized[i].clock]) found = actualized[i];
        }
      }
    }
    if (found) {
      if (abbrevs = /^(.*)\/(.*)$/.exec(entry.format)) {
        found.abbrev = abbrevs[found.save != 0 ? 2 : 1];
      } else {
        found.abbrev = entry.format.replace(/%s/, found.rule.letter);
      }
    }
    return found || entry;
  }

  function convertToWallclock (request, posix) {
    if (request.zone == "UTC") return posix;
    request.entry = find(request, "posix", posix);
    return posix + request.entry.offset + request.entry.save;
  };

  function convertToPOSIX (request, wallclock) {
    if (request.zone == "UTC") return wallclock;

    var entry, diff;
    request.entry = entry = find(request, "wallclock", wallclock);
    diff = wallclock - entry.wallclock;

    return 0 < diff && diff < entry.save ? null : wallclock - entry.offset - entry.save;
  };

  var UNITS = "sunday|monday|tuesday|wednesday|thursday|friday|saturday|year|month|day|hour|minute|second|milli|millisecond"
    , UNIT_RE = new RegExp("^\\s*([+-])(\\d+)\\s+(" + UNITS + ")s?\\s*$", "i")
    , TIME = [ 36e5, 6e4, 1e3, 1, 1 ]
    ;

  UNITS = UNITS.split("|");

  function parseAdjustment (pattern) {
    var match = UNIT_RE.exec(pattern);
    return match && function (request, posix) { return adjust(request, posix, match[1], match[2], match[3]) }
  };

  function adjust (request, posix, sign, count, unit) {
    var increment = parseInt(sign + 1)
      , offset = parseInt(count, 10) * increment
      , index = UNITS.indexOf(unit.toLowerCase())
      , date
      ;
    if (index > 9) {
      posix += offset * TIME[index - 10];
    } else {
      date = new Date(convertToWallclock(request, posix));
      if (index < 7) {
        while (offset != 0) {
          date.setUTCDate(date.getUTCDate() + increment);
          if (date.getUTCDay() == index) offset -= increment;
        }
      } else if (index == 7) {
        date.setUTCFullYear(date.getUTCFullYear() + offset);
      } else if (index == 8) {
        date.setUTCMonth(date.getUTCMonth() + offset);
      } else {
        date.setUTCDate(date.getUTCDate() + offset);
      }
      if ((posix = convertToPOSIX(request, date.getTime())) == null) {
        posix = convertToPOSIX(request, date.getTime() + 864e5 * increment) - 864e5 * increment;
      }
    }
    return posix;
  };

  function convert (splat) {
    if (splat.length == 0) return this.clock();

    var i, I, adjustment, argument, date, posix, type
      , request = Object.create(this)
      , adjustments = []
      , index = 0
      ;

    while (splat.length) {
      argument = splat.shift();
      type = typeof argument;
      if (type == "number" && index == 0) {
        request.date = argument;
      } else if (type == "string") {
        if (~argument.indexOf("%")) {
          request.format = argument;
        } else if (/^\w{2}_\w{2}$/.test(argument)) {
          request.locale = argument;
        } else if (adjustment = parseAdjustment(argument)) {
          adjustments.push(adjustment);
        } else if (request[argument]) {
          request.zone = argument;
        } else if (index == 0) {
          request.date = argument;
        }
      } else if (type == "function") {
        argument.call(request);
      } else if (Array.isArray(argument)) {
        if (index == 0 && typeof argument[0] == "number") {
          request.date = argument;
        } else {
          splat.unshift.apply(splat, argument);
        }
      } else if (type == "object") {
        if (/^\w{2}_\w{2}$/.test(argument.name)) {
          request[argument.name] = argument;
        } else if (argument.z) {
          for (var key in argument.z.zones) request[key] = argument.z.zones[key];
          for (var key in argument.z.rules) request[key] = argument.z.rules[key];
        }
      }
      index++;
    }

    if ((date = request.date) != null) {
      if (request.locale && !request[request.locale]) throw new Error("unknown locale");

      if (typeof date == "string") {
        if ((posix = parse(request, date)) == null) {
          throw new Error("invalid date");
        }
      } else if (typeof date == "number") {
        posix = date;
      } else if (Array.isArray(date)) {
        posix = makeDate(request, date);
      }

      for (i = 0, I = adjustments.length; i < I; i++) {
        posix = adjustments[i](request, posix);
      }

      return request.format ?  format.call(request, posix, request.format) : posix;
    }

    return function() { return convert.call(request, __slice.call(arguments, 0)) };
  };

  var context =
    { zone: "UTC"
    , entry: { abbrev: "UTC", offset: 0 }
    , clock: function () { return (+new Date()) }
    , convert: convert
    , d: function(date) { return date.getUTCDate() }
    , m: function(date) { return date.getUTCMonth() + 1 }
    , Y: function(date) { return date.getUTCFullYear() }
    , F: function(date, posix) { return this.convert([ posix, "%Y-%m-%d" ]) }
    , H: function(date) { return date.getUTCHours() }
    , M: function(date) { return date.getUTCMinutes() }
    , s: function(date) { return Math.floor(date.getTime() / 1000) }
    , S: function(date) { return date.getUTCSeconds() }
    , N: function(date) { return (date.getTime() % 1000) * 1000000 }
    , R: function(date, posix) { return this.convert([ posix, "%H:%M" ]) }
    , T: function(date, posix) { return this.convert([ posix, "%H:%M:%S" ]) }
    , z: function(date, posix, delimiters) {
        var offset = this.entry.offset + this.entry.save
          , seconds = Math.abs(offset / 1000), parts = [ 60, 60, 60 ], part, i, z;
        for (i = parts.length - 1; i > -1; i--) {
          part = parts[i];
          parts[i] = ("0" + (seconds % part)).slice(-2);
          seconds -= seconds % part;
          seconds /= part;
        }
        if (delimiters) {
          if (delimiters == 3) {
            z = parts.join(":").replace(/:00$/, "").replace(/:00$/, "");
          } else {
            z = parts.slice(0, delimiters + 1).join(":");
          }
        } else {
          z = parts.slice(0, 2).join("");
        }
        return (offset < 0 ? "-" : "+") + z;
      }
    , Z: function(date) { return this.entry.abbrev }
    , "%": function(date) { return "%" }
    , "n": function(date) { return "\n" }
    , "t": function(date) { return "\t" }
    , "$": function (date, posix, delimiters) {
        return this.entry.offset == 0 ? "Z" : delimiters ? this.z(date, posix, 2).replace(/:00$/, "") : this.z(date, posix, 1)
      }
    };

  context.d.pad = 2;
  context.m.pad = 2;
  context.H.pad = 2;
  context.M.pad = 2;
  context.S.pad = 2;
  context.N.pad = 9;

  return function () { return convert.call(context, __slice.call(arguments, 0)) }
});
<commit_msg>Squish getYear.<commit_after>!function (definition) {
  if (typeof module == "object" && module.exports) module.exports = definition();
  else if (typeof define == "function" && typeof define.amd == "object") define(definition);
  else this.tz = definition();
} (function () {
  var __slice = [].slice, __push = [].push;
/*
  function die () {
    console.log.apply(console, __slice.call(arguments, 0));
    return process.exit(1);
  };

  function say () { return console.log.apply(console, __slice.call(arguments, 0)) }
*/
  function format (posix, rest) {
    var wallclock = new Date(convertToWallclock(this, posix)), self = this;
    return rest.replace(/%([-0_^]|:{0,3})?(\d*)(.)/g, function (matched, flag, padding, specifier) {
      var f, value = matched, fill = "0";
      if (f = self[specifier]) {
        value = String(f.call(self, wallclock, posix, (flag || "").length));
        if ((flag || f.style) == "_") fill = " ";
        pad = parseInt(flag == "-" ? 0 : (f.pad || 0));
        while (value.length < pad) value = fill + value;
        pad = parseInt(flag == "-" ? 0 : (padding || f.pad));
        while (value.length < pad) value = fill + value;
        if (specifier == "N" && pad < value.length) value = value.slice(0, pad);
        if (flag == "^") value = value.toUpperCase();
      }
      return value;
    });
  };

  function makeDate (request, date) {
    var posix, z, i, I;
    if (~(z = date.indexOf('+')) || ~(z = date.indexOf('-'))) {
      date[z] += 1;
      posix = true;
    } else {
      date[z = date.length] = 1;
    }
    for (i = 0; i < 11; i++) {
      date[i] = date[i] || 0;
    }
    --date[1];
    date = Date.UTC.apply(Date.UTC, date.slice(0, z)) + -date[z] * (date[z + 1] * 36e5 + date[z + 2] * 6e4 + date[z + 3] * 1e3);
    return posix ? date : convertToPOSIX(request, date);
  }

  function parse (request, pattern) {
    var parts = pattern.split(/T|\s/), date = [], match;
    if (0 < parts.length && parts.length < 3) {
      if (match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(parts[0])) {
        __push.apply(date, match.slice(1, 4));
        parts.shift();
      } else {
        __push.apply(date, request.convert([ request.clock(), "%Y-%m-%d" ]).split(/-/));
      }
      if (parts[0]) {
        if (match = /^(\d{2}):(\d{2})(?::(\d{2})(\.(\d+))?)?(Z|(([+-])(\d{2}(:\d{2}){0,2})))?$/.exec(parts[0])) {
          __push.apply(date, match.slice(1, 4));
          __push.call(date, match[5] || 0);
          if (match[7]) {
            __push.call(date, match[8]);
            __push.apply(date, match[9].split(/:/));
          } else if (match[6]) {
            __push.call(date, "+");
          }
        } else return;
      }
      return makeDate(request, date);
    }
  }

  function actualize (entry, rule, year) {
    var actualized, fields, date = rule.day[1];

    do {
      fields = new Date(Date.UTC(year, rule.month, Math.abs(date++)));
    } while (fields.getUTCMonth() != rule.month || (rule.day[0] >= 0 && fields.getUTCDay() != rule.day[0]))

    actualized = {
      clock: rule.clock,
      sortable: fields.getTime(),
      rule: rule,
      save: rule.save * 6e4,
      offset: entry.offset
    };

    // TODO Why is `saved` 0?
    actualized[actualized.clock] = fields.getTime() + rule.time * 6e4;

    if (actualized.clock == "posix") {
      actualized.wallclock = actualized.posix + (entry.offset + rule.saved);
    } else {
      actualized.posix = actualized.wallclock - (entry.offset + rule.saved);
    }

    return actualized;
  }

  function applicable (entry, rules, actualized, time, clock) {
    var last, i, I, j, year = new Date(time).getUTCFullYear();
    for (j = year + 1; j >= year - 1; --j) {
      for (i = 0, I = rules.length; i < I; i++) {
          if (rules[i].from <= j && j <= rules[i].to) {
            actualized.push(actualize(entry, rules[i], j, true));
          } else if (rules[i].to < j) {
            last = rules[i].to;
            break;
        }
      }
    }
    return last;
  }

  function find (request, clock, time) {
    var i, I, entry, year = new Date(time).getUTCFullYear(), found, zone = request[request.zone], actualized = [], to, abbrevs;
    for (i = 0, I = zone.length; i < I; i++) if (zone[i][clock] <= time) break;
    entry = zone[i];
    if (entry.rules) {
      rules = request[entry.rules];
      to = applicable(entry, rules, actualized, time, clock);
      if (to != null) applicable(entry, rules, actualized, Date.UTC(to, 5, 1), "wallclock");
      if (actualized.length) {
        actualized.sort(function (a, b) { return a.sortable - b.sortable });
        for (i = 0, I = actualized.length; i < I; i++) {
          if (time >= actualized[i][clock] && actualized[i][actualized[i].clock] > entry[actualized[i].clock]) found = actualized[i];
        }
      }
    }
    if (found) {
      if (abbrevs = /^(.*)\/(.*)$/.exec(entry.format)) {
        found.abbrev = abbrevs[found.save != 0 ? 2 : 1];
      } else {
        found.abbrev = entry.format.replace(/%s/, found.rule.letter);
      }
    }
    return found || entry;
  }

  function convertToWallclock (request, posix) {
    if (request.zone == "UTC") return posix;
    request.entry = find(request, "posix", posix);
    return posix + request.entry.offset + request.entry.save;
  };

  function convertToPOSIX (request, wallclock) {
    if (request.zone == "UTC") return wallclock;

    var entry, diff;
    request.entry = entry = find(request, "wallclock", wallclock);
    diff = wallclock - entry.wallclock;

    return 0 < diff && diff < entry.save ? null : wallclock - entry.offset - entry.save;
  };

  var UNITS = "sunday|monday|tuesday|wednesday|thursday|friday|saturday|year|month|day|hour|minute|second|milli|millisecond"
    , UNIT_RE = new RegExp("^\\s*([+-])(\\d+)\\s+(" + UNITS + ")s?\\s*$", "i")
    , TIME = [ 36e5, 6e4, 1e3, 1, 1 ]
    ;

  UNITS = UNITS.split("|");

  function parseAdjustment (pattern) {
    var match = UNIT_RE.exec(pattern);
    return match && function (request, posix) { return adjust(request, posix, match[1], match[2], match[3]) }
  };

  function adjust (request, posix, sign, count, unit) {
    var increment = parseInt(sign + 1)
      , offset = parseInt(count, 10) * increment
      , index = UNITS.indexOf(unit.toLowerCase())
      , date
      ;
    if (index > 9) {
      posix += offset * TIME[index - 10];
    } else {
      date = new Date(convertToWallclock(request, posix));
      if (index < 7) {
        while (offset != 0) {
          date.setUTCDate(date.getUTCDate() + increment);
          if (date.getUTCDay() == index) offset -= increment;
        }
      } else if (index == 7) {
        date.setUTCFullYear(date.getUTCFullYear() + offset);
      } else if (index == 8) {
        date.setUTCMonth(date.getUTCMonth() + offset);
      } else {
        date.setUTCDate(date.getUTCDate() + offset);
      }
      if ((posix = convertToPOSIX(request, date.getTime())) == null) {
        posix = convertToPOSIX(request, date.getTime() + 864e5 * increment) - 864e5 * increment;
      }
    }
    return posix;
  };

  function convert (splat) {
    if (splat.length == 0) return this.clock();

    var i, I, adjustment, argument, date, posix, type
      , request = Object.create(this)
      , adjustments = []
      , index = 0
      ;

    while (splat.length) {
      argument = splat.shift();
      type = typeof argument;
      if (type == "number" && index == 0) {
        request.date = argument;
      } else if (type == "string") {
        if (~argument.indexOf("%")) {
          request.format = argument;
        } else if (/^\w{2}_\w{2}$/.test(argument)) {
          request.locale = argument;
        } else if (adjustment = parseAdjustment(argument)) {
          adjustments.push(adjustment);
        } else if (request[argument]) {
          request.zone = argument;
        } else if (index == 0) {
          request.date = argument;
        }
      } else if (type == "function") {
        argument.call(request);
      } else if (Array.isArray(argument)) {
        if (index == 0 && typeof argument[0] == "number") {
          request.date = argument;
        } else {
          splat.unshift.apply(splat, argument);
        }
      } else if (type == "object") {
        if (/^\w{2}_\w{2}$/.test(argument.name)) {
          request[argument.name] = argument;
        } else if (argument.z) {
          for (var key in argument.z.zones) request[key] = argument.z.zones[key];
          for (var key in argument.z.rules) request[key] = argument.z.rules[key];
        }
      }
      index++;
    }

    if ((date = request.date) != null) {
      if (request.locale && !request[request.locale]) throw new Error("unknown locale");

      if (typeof date == "string") {
        if ((posix = parse(request, date)) == null) {
          throw new Error("invalid date");
        }
      } else if (typeof date == "number") {
        posix = date;
      } else if (Array.isArray(date)) {
        posix = makeDate(request, date);
      }

      for (i = 0, I = adjustments.length; i < I; i++) {
        posix = adjustments[i](request, posix);
      }

      return request.format ?  format.call(request, posix, request.format) : posix;
    }

    return function() { return convert.call(request, __slice.call(arguments, 0)) };
  };

  var context =
    { zone: "UTC"
    , entry: { abbrev: "UTC", offset: 0 }
    , clock: function () { return (+new Date()) }
    , convert: convert
    , d: function(date) { return date.getUTCDate() }
    , m: function(date) { return date.getUTCMonth() + 1 }
    , Y: function(date) { return date.getUTCFullYear() }
    , F: function(date, posix) { return this.convert([ posix, "%Y-%m-%d" ]) }
    , H: function(date) { return date.getUTCHours() }
    , M: function(date) { return date.getUTCMinutes() }
    , s: function(date) { return Math.floor(date.getTime() / 1000) }
    , S: function(date) { return date.getUTCSeconds() }
    , N: function(date) { return (date.getTime() % 1000) * 1000000 }
    , R: function(date, posix) { return this.convert([ posix, "%H:%M" ]) }
    , T: function(date, posix) { return this.convert([ posix, "%H:%M:%S" ]) }
    , z: function(date, posix, delimiters) {
        var offset = this.entry.offset + this.entry.save
          , seconds = Math.abs(offset / 1000), parts = [ 60, 60, 60 ], part, i, z;
        for (i = parts.length - 1; i > -1; i--) {
          part = parts[i];
          parts[i] = ("0" + (seconds % part)).slice(-2);
          seconds -= seconds % part;
          seconds /= part;
        }
        if (delimiters) {
          if (delimiters == 3) {
            z = parts.join(":").replace(/:00$/, "").replace(/:00$/, "");
          } else {
            z = parts.slice(0, delimiters + 1).join(":");
          }
        } else {
          z = parts.slice(0, 2).join("");
        }
        return (offset < 0 ? "-" : "+") + z;
      }
    , Z: function(date) { return this.entry.abbrev }
    , "%": function(date) { return "%" }
    , "n": function(date) { return "\n" }
    , "t": function(date) { return "\t" }
    , "$": function (date, posix, delimiters) {
        return this.entry.offset == 0 ? "Z" : delimiters ? this.z(date, posix, 2).replace(/:00$/, "") : this.z(date, posix, 1)
      }
    };

  context.d.pad = 2;
  context.m.pad = 2;
  context.H.pad = 2;
  context.M.pad = 2;
  context.S.pad = 2;
  context.N.pad = 9;

  return function () { return convert.call(context, __slice.call(arguments, 0)) }
});
<|endoftext|>


<commit_before>var assert = require('assert')
var crypto = require('crypto')
var networks = require('./networks')

var Address = require('./address')
var HDNode = require('./hdnode')
var Transaction = require('./transaction')
var Script = require('./script')

function Wallet(seed, network, unspents) {
  seed = seed || crypto.randomBytes(32)
  network = network || networks.bitcoin

  // Stored in a closure to make accidental serialization less likely
  var masterkey = HDNode.fromSeedBuffer(seed, network)
  var me = this

  // HD first-level child derivation method should be hardened
  // See https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254
  var accountZero = masterkey.deriveHardened(0)
  var externalAccount = accountZero.derive(0)
  var internalAccount = accountZero.derive(1)

  // Addresses
  this.addresses = []
  this.changeAddresses = []

  this.network = network

  // Transaction output data
  this.outputs = unspents ? processUnspentOutputs(unspents) : {}

  // FIXME: remove in 2.x.y
  this.newMasterKey = function(seed) {
    console.warn('newMasterKey is deprecated, please make a new Wallet instance instead')

    seed = seed || crypto.randomBytes(32)
    masterkey = HDNode.fromSeedBuffer(seed, network)

    // HD first-level child derivation method should be hardened
    // See https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254
    accountZero = masterkey.deriveHardened(0)
    externalAccount = accountZero.derive(0)
    internalAccount = accountZero.derive(1)

    me.addresses = []
    me.changeAddresses = []

    me.outputs = {}
  }

  this.processPendingTx = function(tx){
    processTx(tx, true)
  }

  this.processConfirmedTx = function(tx){
    processTx(tx, false)
  }

  var me = this

  function processTx(tx, isPending) {
    var txid = tx.getId()

    tx.outs.forEach(function(txOut, i) {
      var address

      try {
        address = Address.fromOutputScript(txOut.script, network).toString()
      } catch(e) {
        if (!(e.message.match(/has no matching Address/))) throw e
      }

      if (isMyAddress(address)) {
        var output = txid + ':' + i

        me.outputs[output] = {
          from: output,
          value: txOut.value,
          address: address,
          pending: isPending
        }
      }
    })

    tx.ins.forEach(function(txIn, i) {
      // copy and convert to big-endian hex
      var txinId = new Buffer(txIn.hash)
      Array.prototype.reverse.call(txinId)
      txinId = txinId.toString('hex')

      var output = txinId + ':' + txIn.index

      if (!(output in me.outputs)) return

      if (isPending) {
        me.outputs[output].to = txid + ':' + i
        me.outputs[output].pending = true
      } else {
        delete me.outputs[output]
      }
    })
  }

  this.getMasterKey = function() { return masterkey }
  this.getAccountZero = function() { return accountZero }
  this.getInternalAccount = function() { return internalAccount }
  this.getExternalAccount = function() { return externalAccount }

  this.getPrivateKeyForAddress = function(address) {
    assert(isMyAddress(address), 'Unknown address. Make sure the address is from the keychain and has been generated')

    if (isReceiveAddress(address)) {
      var index = this.addresses.indexOf(address)

      return this.getPrivateKey(index)
    }

    if (isChangeAddress(address)) {
      var index = this.changeAddresses.indexOf(address)

      return this.getInternalPrivateKey(index)
    }
  }

  function isReceiveAddress(address){
    return me.addresses.indexOf(address) > -1
  }

  function isChangeAddress(address){
    return me.changeAddresses.indexOf(address) > -1
  }

  function isMyAddress(address) {
    return isReceiveAddress(address) || isChangeAddress(address)
  }
}

Wallet.prototype.createTx = function(to, value, fixedFee, changeAddress) {
  assert(value > this.network.dustThreshold, value + ' must be above dust threshold (' + this.network.dustThreshold + ' Satoshis)')

  var utxos = getCandidateOutputs(this.outputs, value)
  var accum = 0
  var subTotal = value
  var addresses = []

  var tx = new Transaction()
  tx.addOutput(to, value)

  for (var i = 0; i < utxos.length; ++i) {
    var utxo = utxos[i]
    addresses.push(utxo.address)

    var outpoint = utxo.from.split(':')
    tx.addInput(outpoint[0], parseInt(outpoint[1]))

    var fee = fixedFee == undefined ? estimatePaddedFee(tx, this.network) : fixedFee

    accum += utxo.value
    subTotal = value + fee
    if (accum >= subTotal) {
      var change = accum - subTotal

      if (change > this.network.dustThreshold) {
        tx.addOutput(changeAddress || this.getChangeAddress(), change)
      }

      break
    }
  }

  assert(accum >= subTotal, 'Not enough funds (incl. fee): ' + accum + ' < ' + subTotal)

  this.signWith(tx, addresses)
  return tx
}

Wallet.prototype.generateAddress = function() {
  var k = this.addresses.length
  var address = this.getExternalAccount().derive(k).getAddress()

  this.addresses.push(address.toString())

  return this.getReceiveAddress()
}

Wallet.prototype.generateChangeAddress = function() {
  var k = this.changeAddresses.length
  var address = this.getInternalAccount().derive(k).getAddress()

  this.changeAddresses.push(address.toString())

  return this.getChangeAddress()
}

Wallet.prototype.getBalance = function() {
  return this.getUnspentOutputs().reduce(function(accum, output) {
    return accum + output.value
  }, 0)
}

Wallet.prototype.getChangeAddress = function() {
  if (this.changeAddresses.length === 0) {
    this.generateChangeAddress()
  }

  return this.changeAddresses[this.changeAddresses.length - 1]
}

Wallet.prototype.getInternalPrivateKey = function(index) {
  return this.getInternalAccount().derive(index).privKey
}

Wallet.prototype.getPrivateKey = function(index) {
  return this.getExternalAccount().derive(index).privKey
}

Wallet.prototype.getReceiveAddress = function() {
  if (this.addresses.length === 0) {
    this.generateAddress()
  }

  return this.addresses[this.addresses.length - 1]
}

Wallet.prototype.getUnspentOutputs = function() {
  var utxo = []

  for(var key in this.outputs){
    var output = this.outputs[key]
    if(!output.to) utxo.push(outputToUnspentOutput(output))
  }

  return utxo
}

Wallet.prototype.setUnspentOutputs = function(utxo) {
  console.warn('setUnspentOutputs is deprecated, please use the constructor option instead')

  this.outputs = processUnspentOutputs(utxo)
}

Wallet.prototype.signWith = function(tx, addresses) {
  assert.equal(tx.ins.length, addresses.length, 'Number of addresses must match number of transaction inputs')

  addresses.forEach(function(address, i) {
    var key = this.getPrivateKeyForAddress(address)

    tx.sign(i, key)
  }, this)

  return tx
}

function outputToUnspentOutput(output){
  var hashAndIndex = output.from.split(":")

  return {
    hash: hashAndIndex[0],
    index: parseInt(hashAndIndex[1]),
    address: output.address,
    value: output.value,
    pending: output.pending
  }
}

function estimatePaddedFee(tx, network) {
  var tmpTx = tx.clone()
  tmpTx.addOutput(Script.EMPTY, network.dustSoftThreshold || 0)

  return network.estimateFee(tmpTx)
}

function processUnspentOutputs(utxos) {
  var outputs = {}

  utxos.forEach(function(utxo){
    var hash = new Buffer(utxo.hash, 'hex')
    var index = utxo.index
    var address = utxo.address
    var value = utxo.value

    // FIXME: remove alternative in 2.x.y
    if (index === undefined) index = utxo.outputIndex

    assert.equal(hash.length, 32, 'Expected hash length of 32, got ' + hash.length)
    assert.equal(typeof index, 'number', 'Expected number index, got ' + index)
    assert.doesNotThrow(function() { Address.fromBase58Check(address) }, 'Expected Base58 Address, got ' + address)
    assert.equal(typeof value, 'number', 'Expected number value, got ' + value)

    var key = utxo.hash + ':' + utxo.index

    outputs[key] = {
      from: key,
      address: address,
      value: value,
      pending: utxo.pending
    }
  })

  return outputs
}

function getCandidateOutputs(outputs/*, value*/) {
  var unspent = []

  for (var key in outputs) {
    var output = outputs[key]
    if (!output.pending) unspent.push(output)
  }

  var sortByValueDesc = unspent.sort(function(o1, o2){
    return o2.value - o1.value
  })

  return sortByValueDesc
}

module.exports = Wallet
<commit_msg>Wallet: rename masterkey to masterKey<commit_after>var assert = require('assert')
var crypto = require('crypto')
var networks = require('./networks')

var Address = require('./address')
var HDNode = require('./hdnode')
var Transaction = require('./transaction')
var Script = require('./script')

function Wallet(seed, network, unspents) {
  seed = seed || crypto.randomBytes(32)
  network = network || networks.bitcoin

  // Stored in a closure to make accidental serialization less likely
  var masterKey = HDNode.fromSeedBuffer(seed, network)
  var me = this

  // HD first-level child derivation method should be hardened
  // See https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254
  var accountZero = masterKey.deriveHardened(0)
  var externalAccount = accountZero.derive(0)
  var internalAccount = accountZero.derive(1)

  // Addresses
  this.addresses = []
  this.changeAddresses = []

  this.network = network

  // Transaction output data
  this.outputs = unspents ? processUnspentOutputs(unspents) : {}

  // FIXME: remove in 2.x.y
  this.newMasterKey = function(seed) {
    console.warn('newMasterKey is deprecated, please make a new Wallet instance instead')

    seed = seed || crypto.randomBytes(32)
    masterKey = HDNode.fromSeedBuffer(seed, network)

    // HD first-level child derivation method should be hardened
    // See https://bitcointalk.org/index.php?topic=405179.msg4415254#msg4415254
    accountZero = masterKey.deriveHardened(0)
    externalAccount = accountZero.derive(0)
    internalAccount = accountZero.derive(1)

    me.addresses = []
    me.changeAddresses = []

    me.outputs = {}
  }

  this.processPendingTx = function(tx){
    processTx(tx, true)
  }

  this.processConfirmedTx = function(tx){
    processTx(tx, false)
  }

  var me = this

  function processTx(tx, isPending) {
    var txid = tx.getId()

    tx.outs.forEach(function(txOut, i) {
      var address

      try {
        address = Address.fromOutputScript(txOut.script, network).toString()
      } catch(e) {
        if (!(e.message.match(/has no matching Address/))) throw e
      }

      if (isMyAddress(address)) {
        var output = txid + ':' + i

        me.outputs[output] = {
          from: output,
          value: txOut.value,
          address: address,
          pending: isPending
        }
      }
    })

    tx.ins.forEach(function(txIn, i) {
      // copy and convert to big-endian hex
      var txinId = new Buffer(txIn.hash)
      Array.prototype.reverse.call(txinId)
      txinId = txinId.toString('hex')

      var output = txinId + ':' + txIn.index

      if (!(output in me.outputs)) return

      if (isPending) {
        me.outputs[output].to = txid + ':' + i
        me.outputs[output].pending = true
      } else {
        delete me.outputs[output]
      }
    })
  }

  this.getMasterKey = function() { return masterKey }
  this.getAccountZero = function() { return accountZero }
  this.getExternalAccount = function() { return externalAccount }
  this.getInternalAccount = function() { return internalAccount }

  this.getPrivateKeyForAddress = function(address) {
    assert(isMyAddress(address), 'Unknown address. Make sure the address is from the keychain and has been generated')

    if (isReceiveAddress(address)) {
      var index = this.addresses.indexOf(address)

      return this.getPrivateKey(index)
    }

    if (isChangeAddress(address)) {
      var index = this.changeAddresses.indexOf(address)

      return this.getInternalPrivateKey(index)
    }
  }

  function isReceiveAddress(address){
    return me.addresses.indexOf(address) > -1
  }

  function isChangeAddress(address){
    return me.changeAddresses.indexOf(address) > -1
  }

  function isMyAddress(address) {
    return isReceiveAddress(address) || isChangeAddress(address)
  }
}

Wallet.prototype.createTx = function(to, value, fixedFee, changeAddress) {
  assert(value > this.network.dustThreshold, value + ' must be above dust threshold (' + this.network.dustThreshold + ' Satoshis)')

  var utxos = getCandidateOutputs(this.outputs, value)
  var accum = 0
  var subTotal = value
  var addresses = []

  var tx = new Transaction()
  tx.addOutput(to, value)

  for (var i = 0; i < utxos.length; ++i) {
    var utxo = utxos[i]
    addresses.push(utxo.address)

    var outpoint = utxo.from.split(':')
    tx.addInput(outpoint[0], parseInt(outpoint[1]))

    var fee = fixedFee == undefined ? estimatePaddedFee(tx, this.network) : fixedFee

    accum += utxo.value
    subTotal = value + fee
    if (accum >= subTotal) {
      var change = accum - subTotal

      if (change > this.network.dustThreshold) {
        tx.addOutput(changeAddress || this.getChangeAddress(), change)
      }

      break
    }
  }

  assert(accum >= subTotal, 'Not enough funds (incl. fee): ' + accum + ' < ' + subTotal)

  this.signWith(tx, addresses)
  return tx
}

Wallet.prototype.generateAddress = function() {
  var k = this.addresses.length
  var address = this.getExternalAccount().derive(k).getAddress()

  this.addresses.push(address.toString())

  return this.getReceiveAddress()
}

Wallet.prototype.generateChangeAddress = function() {
  var k = this.changeAddresses.length
  var address = this.getInternalAccount().derive(k).getAddress()

  this.changeAddresses.push(address.toString())

  return this.getChangeAddress()
}

Wallet.prototype.getBalance = function() {
  return this.getUnspentOutputs().reduce(function(accum, output) {
    return accum + output.value
  }, 0)
}

Wallet.prototype.getChangeAddress = function() {
  if (this.changeAddresses.length === 0) {
    this.generateChangeAddress()
  }

  return this.changeAddresses[this.changeAddresses.length - 1]
}

Wallet.prototype.getInternalPrivateKey = function(index) {
  return this.getInternalAccount().derive(index).privKey
}

Wallet.prototype.getPrivateKey = function(index) {
  return this.getExternalAccount().derive(index).privKey
}

Wallet.prototype.getReceiveAddress = function() {
  if (this.addresses.length === 0) {
    this.generateAddress()
  }

  return this.addresses[this.addresses.length - 1]
}

Wallet.prototype.getUnspentOutputs = function() {
  var utxo = []

  for(var key in this.outputs){
    var output = this.outputs[key]
    if(!output.to) utxo.push(outputToUnspentOutput(output))
  }

  return utxo
}

Wallet.prototype.setUnspentOutputs = function(utxo) {
  console.warn('setUnspentOutputs is deprecated, please use the constructor option instead')

  this.outputs = processUnspentOutputs(utxo)
}

Wallet.prototype.signWith = function(tx, addresses) {
  assert.equal(tx.ins.length, addresses.length, 'Number of addresses must match number of transaction inputs')

  addresses.forEach(function(address, i) {
    var key = this.getPrivateKeyForAddress(address)

    tx.sign(i, key)
  }, this)

  return tx
}

function outputToUnspentOutput(output){
  var hashAndIndex = output.from.split(":")

  return {
    hash: hashAndIndex[0],
    index: parseInt(hashAndIndex[1]),
    address: output.address,
    value: output.value,
    pending: output.pending
  }
}

function estimatePaddedFee(tx, network) {
  var tmpTx = tx.clone()
  tmpTx.addOutput(Script.EMPTY, network.dustSoftThreshold || 0)

  return network.estimateFee(tmpTx)
}

function processUnspentOutputs(utxos) {
  var outputs = {}

  utxos.forEach(function(utxo){
    var hash = new Buffer(utxo.hash, 'hex')
    var index = utxo.index
    var address = utxo.address
    var value = utxo.value

    // FIXME: remove alternative in 2.x.y
    if (index === undefined) index = utxo.outputIndex

    assert.equal(hash.length, 32, 'Expected hash length of 32, got ' + hash.length)
    assert.equal(typeof index, 'number', 'Expected number index, got ' + index)
    assert.doesNotThrow(function() { Address.fromBase58Check(address) }, 'Expected Base58 Address, got ' + address)
    assert.equal(typeof value, 'number', 'Expected number value, got ' + value)

    var key = utxo.hash + ':' + utxo.index

    outputs[key] = {
      from: key,
      address: address,
      value: value,
      pending: utxo.pending
    }
  })

  return outputs
}

function getCandidateOutputs(outputs/*, value*/) {
  var unspent = []

  for (var key in outputs) {
    var output = outputs[key]
    if (!output.pending) unspent.push(output)
  }

  var sortByValueDesc = unspent.sort(function(o1, o2){
    return o2.value - o1.value
  })

  return sortByValueDesc
}

module.exports = Wallet
<|endoftext|>


<commit_before>/* --------------------
 * got-resume module
 * Transfer class
 * ------------------*/

'use strict';

// Modules
const streamModule = require('stream'),
	got = require('got');

// Imports
const backoff = require('./backoff.js'),
	{TransferError, PreError, CancelError} = require('./errors.js');

// Exports

/*
 * Transfer class
 */

/**
 * Transfer constructor
 * @constructor
 * @param {Object} options - Options object
 */
function Transfer(options) {
	// Save options to transfer object
	this.options = options;
	this.url = options.url;
	this.length = options.length;
	this.log = options.log || (() => {});

	this.gotOptions = {...options.got, encoding: null};
	this.gotOptions.headers = {...this.gotOptions.headers};
	if (options.needLength) this.gotOptions.headers['accept-encoding'] = 'identity';

	this.idleTimeout = options.timeout && options.timeout.idle;

	// Init transfer object
	this.attempt = 0;
	this.attemptTotal = 0;
	this.position = this.options.offset;
	this.total = undefined;
	this.cancelled = false;

	// Create output stream
	this.stream = new streamModule.PassThrough();

	// Add `.cancel()` method to stream
	this.stream.cancel = this.cancel.bind(this);

	// Record transfer object on stream
	this.stream.transfer = this;
}

module.exports = Transfer;

/**
 * Start transfer.
 * Runs `options.pre` if provided and calls `transfer.get()`.
 * Called at start of transfer and again repeatedly for each retry.
 * @returns {undefined}
 */
Transfer.prototype.start = function() {
	this.attempt++;
	this.attemptTotal++;
	delete this.err;

	this.log('Starting transfer', {transfer: logObj(this)});

	const {pre} = this.options;

	if (!pre) {
		this.get();
		return;
	}

	this.log('Calling pre function');

	this.prePromise = pre(this);

	this.prePromise.then(() => {
		delete this.prePromise;

		if (!this.url) throw new PreError('pre function did not set transfer.url');
		if (this.cancelled) throw new CancelError('Transfer cancelled');

		this.log('Completed pre function', {url: this.url});

		this.get();
	}).catch((err) => {
		delete this.prePromise;

		this.err = err;

		this.failed(err, true);
	});
};

/**
 * Create HTTP request and pipe to stream.
 * Pipes result of request to output stream.
 * When transfer is complete, `end` event emmitted on output stream.
 * If transfer fails, calls `transfer.failed()` to retry or exit.
 * @returns {undefined}
 */
Transfer.prototype.get = function() {
	this.log('Starting fetch', {transfer: logObj(this)});

	// Set range options
	// Disable compression for ranges as mucks up range request
	if (this.position) {
		this.gotOptions.headers.range = `bytes=${this.position}-`;
		this.gotOptions.headers['accept-encoding'] = 'identity';
	}

	// Idle timer to abort if transfer stalls
	let idleTimer = null;
	const {idleTimeout} = this;
	const setIdleTimeout = () => {
		if (idleTimeout) {
			clearIdleTimeout();
			idleTimer = setTimeout(() => this.req.abort(), idleTimeout);
		}
	};
	function clearIdleTimeout() {
		if (idleTimer) {
			clearTimeout(idleTimer);
			idleTimer = null;
		}
	}

	// Use `got` module to stream URL
	let aborted = false,
		ended = false;
	const stream = got.stream(this.url, this.gotOptions);

	stream.on('error', (err, body) => {
		// Ignore error after end + repeated error events
		if (ended) return;
		ended = true;

		this.log('Stream error', {err, body});

		clearIdleTimeout();

		// Delete req from transfer object
		delete this.req;

		// Save error object to transfer
		this.err = err;

		// Request timeouts do not end stream - end manually
		// (see https://github.com/sindresorhus/got/issues/337)
		// TODO Alter this if issue resolved
		stream.emit('end');
	});

	// When request made, record to transfer object
	// (so it can be aborted if required)
	stream.on('request', (req) => {
		this.log('Sent HTTP request', {headers: req._headers});

		// Emit request event
		if (!this.req) this.stream.emit('request', req);

		// If transfer cancelled, abort request
		if (this.cancelled) req.abort();

		// Record req to transfer object
		this.req = req;
	});

	stream.on('end', () => {
		// Ignore repeated end events (some errors emit an end event too)
		if (ended) return;
		ended = true;

		this.log('Got stream ended');

		clearIdleTimeout();

		// Delete req from transfer object
		delete this.req;
	});

	// When response headers received, record to transfer object,
	// set length and check range headers correct
	stream.on('response', (res) => {
		this.log('Received HTTP response', {url: res.url, headers: res.headers});

		// Process response
		// Errors with e.g. range headers are emitted as errors
		const first = !this.res;
		try {
			// Check range headers match what requested
			// and set length from headers if not supplied in options.length
			const {headers} = res;

			if (this.position) {
				if (!headers['content-range']) throw new TransferError('No range header');
				const range = headers['content-range'].match(/^bytes (\d+)-\d+\/(\d+)$/);
				if (!range) {
					throw new TransferError(`Malformed range header '${headers['content-range']}'`);
				}
				if (range[1] !== `${this.position}`) {
					throw new TransferError(
						`Server returned wrong range '${range}', expected start at ${this.position}`
					);
				}

				if (first && this.length == null) this.length = range[2] * 1;
			} else if (
				first && this.length == null && headers['content-length'] && !headers['content-encoding']
			) {
				this.length = headers['content-length'] * 1;
			}

			// Record/check last modified date and/or eTag to ensure resource has not
			// changed between requests.
			if (first) {
				this.lastMod = headers['last-modified'];
				this.etag = headers.etag;

				// Record total bytes to be transferred
				if (this.length != null) this.total = this.length - this.options.offset;

				// Emit progress event
				this.stream.emit('progress', {transferred: 0, total: this.total});
			} else {
				if (this.lastMod && headers['last-modified'] !== this.lastMod) {
					throw new TransferError(
						`Last modified date has changed: '${headers['last-modified']}' from '${this.lastMod}'`
					);
				}
				if (this.etag && headers.etag !== this.etag) {
					throw new TransferError(`ETag has changed: '${headers.etag}' from '${this.etag}'`);
				}
			}
		} catch (err) {
			this.log('Response error', {err});
			aborted = true;
			this.req.abort(err);
			return;
		}

		// Record res to transfer object
		this.res = res;

		// Start idle timer
		setIdleTimeout();

		// Emit response event
		if (first) this.stream.emit('response', res);
	});

	// Pipe transfer to output via transform stream to limit to range
	// and handle end of transfer
	const transfer = this;
	let received = 0;
	const transformStream = new streamModule.Transform({
		transform(chunk, encoding, callback) {
			// If transfer aborted or error, discard incoming data
			// Solves https://github.com/overlookmotel/got-resume/issues/3
			if (aborted || transfer.err) {
				callback();
				return;
			}

			// Increment count of bytes received
			received += chunk.length;

			// Reset idle timer
			setIdleTimeout();

			// If entire chunk is before range required, discard chunk
			const {length, options: {offset}} = transfer;
			let chunkEnd = transfer.position + chunk.length;
			if (chunkEnd <= offset) {
				transfer.position = chunkEnd;
				callback();
				return;
			}

			// If only part of chunk is within range required, shorten chunk
			if (transfer.position < offset) {
				chunk = chunk.slice(offset - transfer.position);
				transfer.position = offset;
			}

			if (length != null && chunkEnd > length) {
				chunk = chunk.slice(0, length - transfer.position);
				chunkEnd = length;
			}

			// Output chunk + update position
			transfer.position = chunkEnd;
			if (chunk.length) this.push(chunk);

			// Emit progress event
			transfer.stream.emit('progress', {
				transferred: chunkEnd - transfer.options.offset,
				total: transfer.total
			});

			// If transfer complete, end stream and cancel request
			if (length != null && chunkEnd === length) {
				this.push(null);
				if (transfer.req) transfer.req.abort();
			}

			// Done
			callback();
		}
	});

	transformStream.on('end', () => {
		this.log('Transform stream ended');

		// Unpipe stream from output stream
		transformStream.unpipe();

		// Check if transfer complete
		if (this.position === this.length || (this.length == null && this.res && !this.err)) {
			// Transfer complete - end output stream
			transfer.stream.end();
			this.log('Finished', {transfer: logObj(this)});
		} else {
			// Transfer incomplete - try again
			transfer.failed(
				new TransferError(
					`Transfer stopped before end - at ${transfer.position} not ${transfer.length}`
				),
				!received
			);
		}
	});

	stream.pipe(transformStream).pipe(this.stream, {end: false});
};

/**
 * Called when transfer fails (or `options.preFn` rejects).
 * Retries depending on how many failures, and `attempt` options.
 * Calls `transfer.start()` again to retry, or `transfer.fatal()` if retries exhausted.
 * @returns {undefined}
 */
Transfer.prototype.failed = function(err, empty) {
	this.log('Transfer failed', {err});

	// If transfer cancelled, emit error on stream and stop
	if (this.cancelled) {
		this.fatal();
		return;
	}

	// If retries limit hit, emit error on stream and stop
	if (
		(this.options.attempts && this.attempt >= this.options.attempts)
		|| (this.options.attemptsTotal && this.attemptTotal >= this.options.attemptsTotal)
	) {
		this.fatal();
		return;
	}

	// If response from last attempt was not empty, reset attempt timer
	if (!empty) this.attempt = 0;

	// Get time to pause before next retry
	const pause = (this.options.backoff || backoff)(this.attempt, this);
	if (pause === false) {
		this.fatal();
		return;
	}

	// Schedule retry
	this.log(`Scheduling retry in ${pause} ms`);

	this.waitTimer = setTimeout(() => {
		delete this.waitTimer;
		this.start();
	}, pause);
};

/**
 * Called when transfer fails fatally.
 * Emits `error` event on output stream.
 * @returns {undefined}
 */
Transfer.prototype.fatal = function() {
	this.log('Transfer failed permanently', {transfer: logObj(this)});

	const err = this.cancelled
		? new CancelError('Transfer cancelled')
		: new TransferError(`Failed - Retries exhausted (${this.attempt}, ${this.attemptTotal} total)`);
	this.stream.emit('error', err);
	this.stream.emit('end');
};

/**
 * Abort transfer.
 * @returns {undefined}
 */
Transfer.prototype.cancel = function() {
	// If already cancelled, ignore
	if (this.cancelled) return;

	// Set cancelled flag
	this.cancelled = true;

	this.log('Transfer cancelled', {transfer: logObj(this)});

	// Cancel current action
	if (this.prePromise) {
		// pre function running - try to cancel it
		if (typeof this.prePromise.cancel === 'function') this.prePromise.cancel();
	} else if (this.req) {
		// Request in progress - abort it
		this.req.abort();
	} else if (this.waitTimer) {
		// Waiting to make new request - cancel timer and fail
		clearTimeout(this.waitTimer);
		delete this.waitTimer;
		this.failed(new CancelError('Transfer cancelled'), true);
	}
};

/*
 * Utility functions
 */

/**
 * Create short copy of transfer object with `.req`, `.res`, `.err` and `.stream` removed for logging
 * @param {Transfer} transfer - Transfer object
 * @returns {Object} - Short copy of transfer object
 */
function logObj(transfer) {
	const out = {...transfer};
	delete out.req;
	delete out.res;
	delete out.err;
	delete out.stream;
	if (out.options && out.options.transform) {
		out.options = {...out.options};
		out.options.transform = '[Stream]';
	}
	return out;
}
<commit_msg>Log idle timeouts [improve]<commit_after>/* --------------------
 * got-resume module
 * Transfer class
 * ------------------*/

'use strict';

// Modules
const streamModule = require('stream'),
	got = require('got');

// Imports
const backoff = require('./backoff.js'),
	{TransferError, PreError, CancelError} = require('./errors.js');

// Exports

/*
 * Transfer class
 */

/**
 * Transfer constructor
 * @constructor
 * @param {Object} options - Options object
 */
function Transfer(options) {
	// Save options to transfer object
	this.options = options;
	this.url = options.url;
	this.length = options.length;
	this.log = options.log || (() => {});

	this.gotOptions = {...options.got, encoding: null};
	this.gotOptions.headers = {...this.gotOptions.headers};
	if (options.needLength) this.gotOptions.headers['accept-encoding'] = 'identity';

	this.idleTimeout = options.timeout && options.timeout.idle;

	// Init transfer object
	this.attempt = 0;
	this.attemptTotal = 0;
	this.position = this.options.offset;
	this.total = undefined;
	this.cancelled = false;

	// Create output stream
	this.stream = new streamModule.PassThrough();

	// Add `.cancel()` method to stream
	this.stream.cancel = this.cancel.bind(this);

	// Record transfer object on stream
	this.stream.transfer = this;
}

module.exports = Transfer;

/**
 * Start transfer.
 * Runs `options.pre` if provided and calls `transfer.get()`.
 * Called at start of transfer and again repeatedly for each retry.
 * @returns {undefined}
 */
Transfer.prototype.start = function() {
	this.attempt++;
	this.attemptTotal++;
	delete this.err;

	this.log('Starting transfer', {transfer: logObj(this)});

	const {pre} = this.options;

	if (!pre) {
		this.get();
		return;
	}

	this.log('Calling pre function');

	this.prePromise = pre(this);

	this.prePromise.then(() => {
		delete this.prePromise;

		if (!this.url) throw new PreError('pre function did not set transfer.url');
		if (this.cancelled) throw new CancelError('Transfer cancelled');

		this.log('Completed pre function', {url: this.url});

		this.get();
	}).catch((err) => {
		delete this.prePromise;

		this.err = err;

		this.failed(err, true);
	});
};

/**
 * Create HTTP request and pipe to stream.
 * Pipes result of request to output stream.
 * When transfer is complete, `end` event emmitted on output stream.
 * If transfer fails, calls `transfer.failed()` to retry or exit.
 * @returns {undefined}
 */
Transfer.prototype.get = function() {
	this.log('Starting fetch', {transfer: logObj(this)});

	// Set range options
	// Disable compression for ranges as mucks up range request
	if (this.position) {
		this.gotOptions.headers.range = `bytes=${this.position}-`;
		this.gotOptions.headers['accept-encoding'] = 'identity';
	}

	// Idle timer to abort if transfer stalls
	let idleTimer = null;
	const {idleTimeout} = this;
	const setIdleTimeout = () => {
		if (idleTimeout) {
			clearIdleTimeout();
			idleTimer = setTimeout(() => {
				this.log(`Connection idle for ${idleTimeout}ms - aborting`);
				this.req.abort();
			}, idleTimeout);
		}
	};
	function clearIdleTimeout() {
		if (idleTimer) {
			clearTimeout(idleTimer);
			idleTimer = null;
		}
	}

	// Use `got` module to stream URL
	let aborted = false,
		ended = false;
	const stream = got.stream(this.url, this.gotOptions);

	stream.on('error', (err, body) => {
		// Ignore error after end + repeated error events
		if (ended) return;
		ended = true;

		this.log('Stream error', {err, body});

		clearIdleTimeout();

		// Delete req from transfer object
		delete this.req;

		// Save error object to transfer
		this.err = err;

		// Request timeouts do not end stream - end manually
		// (see https://github.com/sindresorhus/got/issues/337)
		// TODO Alter this if issue resolved
		stream.emit('end');
	});

	// When request made, record to transfer object
	// (so it can be aborted if required)
	stream.on('request', (req) => {
		this.log('Sent HTTP request', {headers: req._headers});

		// Emit request event
		if (!this.req) this.stream.emit('request', req);

		// If transfer cancelled, abort request
		if (this.cancelled) req.abort();

		// Record req to transfer object
		this.req = req;
	});

	stream.on('end', () => {
		// Ignore repeated end events (some errors emit an end event too)
		if (ended) return;
		ended = true;

		this.log('Got stream ended');

		clearIdleTimeout();

		// Delete req from transfer object
		delete this.req;
	});

	// When response headers received, record to transfer object,
	// set length and check range headers correct
	stream.on('response', (res) => {
		this.log('Received HTTP response', {url: res.url, headers: res.headers});

		// Process response
		// Errors with e.g. range headers are emitted as errors
		const first = !this.res;
		try {
			// Check range headers match what requested
			// and set length from headers if not supplied in options.length
			const {headers} = res;

			if (this.position) {
				if (!headers['content-range']) throw new TransferError('No range header');
				const range = headers['content-range'].match(/^bytes (\d+)-\d+\/(\d+)$/);
				if (!range) {
					throw new TransferError(`Malformed range header '${headers['content-range']}'`);
				}
				if (range[1] !== `${this.position}`) {
					throw new TransferError(
						`Server returned wrong range '${range}', expected start at ${this.position}`
					);
				}

				if (first && this.length == null) this.length = range[2] * 1;
			} else if (
				first && this.length == null && headers['content-length'] && !headers['content-encoding']
			) {
				this.length = headers['content-length'] * 1;
			}

			// Record/check last modified date and/or eTag to ensure resource has not
			// changed between requests.
			if (first) {
				this.lastMod = headers['last-modified'];
				this.etag = headers.etag;

				// Record total bytes to be transferred
				if (this.length != null) this.total = this.length - this.options.offset;

				// Emit progress event
				this.stream.emit('progress', {transferred: 0, total: this.total});
			} else {
				if (this.lastMod && headers['last-modified'] !== this.lastMod) {
					throw new TransferError(
						`Last modified date has changed: '${headers['last-modified']}' from '${this.lastMod}'`
					);
				}
				if (this.etag && headers.etag !== this.etag) {
					throw new TransferError(`ETag has changed: '${headers.etag}' from '${this.etag}'`);
				}
			}
		} catch (err) {
			this.log('Response error', {err});
			aborted = true;
			this.req.abort(err);
			return;
		}

		// Record res to transfer object
		this.res = res;

		// Start idle timer
		setIdleTimeout();

		// Emit response event
		if (first) this.stream.emit('response', res);
	});

	// Pipe transfer to output via transform stream to limit to range
	// and handle end of transfer
	const transfer = this;
	let received = 0;
	const transformStream = new streamModule.Transform({
		transform(chunk, encoding, callback) {
			// If transfer aborted or error, discard incoming data
			// Solves https://github.com/overlookmotel/got-resume/issues/3
			if (aborted || transfer.err) {
				callback();
				return;
			}

			// Increment count of bytes received
			received += chunk.length;

			// Reset idle timer
			setIdleTimeout();

			// If entire chunk is before range required, discard chunk
			const {length, options: {offset}} = transfer;
			let chunkEnd = transfer.position + chunk.length;
			if (chunkEnd <= offset) {
				transfer.position = chunkEnd;
				callback();
				return;
			}

			// If only part of chunk is within range required, shorten chunk
			if (transfer.position < offset) {
				chunk = chunk.slice(offset - transfer.position);
				transfer.position = offset;
			}

			if (length != null && chunkEnd > length) {
				chunk = chunk.slice(0, length - transfer.position);
				chunkEnd = length;
			}

			// Output chunk + update position
			transfer.position = chunkEnd;
			if (chunk.length) this.push(chunk);

			// Emit progress event
			transfer.stream.emit('progress', {
				transferred: chunkEnd - transfer.options.offset,
				total: transfer.total
			});

			// If transfer complete, end stream and cancel request
			if (length != null && chunkEnd === length) {
				this.push(null);
				if (transfer.req) transfer.req.abort();
			}

			// Done
			callback();
		}
	});

	transformStream.on('end', () => {
		this.log('Transform stream ended');

		// Unpipe stream from output stream
		transformStream.unpipe();

		// Check if transfer complete
		if (this.position === this.length || (this.length == null && this.res && !this.err)) {
			// Transfer complete - end output stream
			transfer.stream.end();
			this.log('Finished', {transfer: logObj(this)});
		} else {
			// Transfer incomplete - try again
			transfer.failed(
				new TransferError(
					`Transfer stopped before end - at ${transfer.position} not ${transfer.length}`
				),
				!received
			);
		}
	});

	stream.pipe(transformStream).pipe(this.stream, {end: false});
};

/**
 * Called when transfer fails (or `options.preFn` rejects).
 * Retries depending on how many failures, and `attempt` options.
 * Calls `transfer.start()` again to retry, or `transfer.fatal()` if retries exhausted.
 * @returns {undefined}
 */
Transfer.prototype.failed = function(err, empty) {
	this.log('Transfer failed', {err});

	// If transfer cancelled, emit error on stream and stop
	if (this.cancelled) {
		this.fatal();
		return;
	}

	// If retries limit hit, emit error on stream and stop
	if (
		(this.options.attempts && this.attempt >= this.options.attempts)
		|| (this.options.attemptsTotal && this.attemptTotal >= this.options.attemptsTotal)
	) {
		this.fatal();
		return;
	}

	// If response from last attempt was not empty, reset attempt timer
	if (!empty) this.attempt = 0;

	// Get time to pause before next retry
	const pause = (this.options.backoff || backoff)(this.attempt, this);
	if (pause === false) {
		this.fatal();
		return;
	}

	// Schedule retry
	this.log(`Scheduling retry in ${pause} ms`);

	this.waitTimer = setTimeout(() => {
		delete this.waitTimer;
		this.start();
	}, pause);
};

/**
 * Called when transfer fails fatally.
 * Emits `error` event on output stream.
 * @returns {undefined}
 */
Transfer.prototype.fatal = function() {
	this.log('Transfer failed permanently', {transfer: logObj(this)});

	const err = this.cancelled
		? new CancelError('Transfer cancelled')
		: new TransferError(`Failed - Retries exhausted (${this.attempt}, ${this.attemptTotal} total)`);
	this.stream.emit('error', err);
	this.stream.emit('end');
};

/**
 * Abort transfer.
 * @returns {undefined}
 */
Transfer.prototype.cancel = function() {
	// If already cancelled, ignore
	if (this.cancelled) return;

	// Set cancelled flag
	this.cancelled = true;

	this.log('Transfer cancelled', {transfer: logObj(this)});

	// Cancel current action
	if (this.prePromise) {
		// pre function running - try to cancel it
		if (typeof this.prePromise.cancel === 'function') this.prePromise.cancel();
	} else if (this.req) {
		// Request in progress - abort it
		this.req.abort();
	} else if (this.waitTimer) {
		// Waiting to make new request - cancel timer and fail
		clearTimeout(this.waitTimer);
		delete this.waitTimer;
		this.failed(new CancelError('Transfer cancelled'), true);
	}
};

/*
 * Utility functions
 */

/**
 * Create short copy of transfer object with `.req`, `.res`, `.err` and `.stream` removed for logging
 * @param {Transfer} transfer - Transfer object
 * @returns {Object} - Short copy of transfer object
 */
function logObj(transfer) {
	const out = {...transfer};
	delete out.req;
	delete out.res;
	delete out.err;
	delete out.stream;
	if (out.options && out.options.transform) {
		out.options = {...out.options};
		out.options.transform = '[Stream]';
	}
	return out;
}
<|endoftext|>


<commit_before>(function(window) {

  var haveLocalStorage;
  var LS_STATE_KEY = "remotestorage:widget:state";

  function stateSetter(widget, state) {
    return function() {
      if(haveLocalStorage) {
        localStorage[LS_STATE_KEY] = state;
      }
      if(widget.view) {
        if(widget.rs.remote) {
          widget.view.setUserAddress(widget.rs.remote.userAddress);
        }
        widget.view.setState(state, arguments);
      }
    };
  }
  function errorsHandler(widget){
    //decided to not store error state
    return function(error){
      if(error instanceof RemoteStorage.DiscoveryError) {
        console.log('discovery failed',  error, '"' + error.message + '"');
        widget.view.setState('initial', [error.message]);
      } else if(error instanceof RemoteStorage.SyncError) {
        widget.view.setState('offline', []);
      } else if(error instanceof RemoteStorage.Unauthorized){
        widget.view.setState('unauthorized')
      } else {
        widget.view.setState('error', [error]);
      }
    }
  }
  RemoteStorage.Widget = function(remoteStorage) {

    // setting event listeners on rs events to put
    // the widget into corresponding states
    this.rs = remoteStorage;
    this.rs.on('ready', stateSetter(this, 'connected'));
    this.rs.on('disconnected', stateSetter(this, 'initial'));
    this.rs.on('authing', stateSetter(this, 'authing'));
    this.rs.on('sync-busy', stateSetter(this, 'busy'));
    this.rs.on('sync-done', stateSetter(this, 'connected'));
    this.rs.on('error', errorsHandler(this) );
    if(haveLocalStorage) {
      var state = localStorage[LS_STATE_KEY] = state;
      if(state) {
        this._rememberedState = state;
      }
    }
  };

  RemoteStorage.Widget.prototype = {
    // Methods :
    //   display(domID)
    //     displays the widget via the view.display method
    //    returns: this
    //
    //   setView(view)
    //     sets the view and initializes event listeners to
    //     react on widget events
    //

    display: function(domID) {
      if(! this.view) {
        this.setView(new RemoteStorage.Widget.View(domID));
      }
      this.view.display.apply(this.view, arguments);
      return this;
    },

    setView: function(view) {
      this.view = view;
      this.view.on('connect', this.rs.connect.bind(this.rs));
      this.view.on('disconnect', this.rs.disconnect.bind(this.rs));
      this.view.on('sync', this.rs.sync.bind(this.rs));
      try {
        this.view.on('reset', function(){
          this.rs.on('disconnected', document.location.reload.bind(document.location))
          this.rs.disconnect()
        }.bind(this));
      } catch(e) {
        if(e.message && e.message.match(/Unknown event/)) {
          // ignored. (the 0.7 widget-view interface didn't have a 'reset' event)
        } else {
          throw e;
        }
      }

      if(this._rememberedState) {
        stateSetter(this, this._rememberedState)();
        delete this._rememberedState;
      }
    }
  };

  RemoteStorage.prototype.displayWidget = function(domID) {
    this.widget.display(domID);
  };

  RemoteStorage.Widget._rs_init = function(remoteStorage) {
    if(! remoteStorage.widget) {
      remoteStorage.widget = new RemoteStorage.Widget(remoteStorage);
    }
  };

  RemoteStorage.Widget._rs_supported = function(remoteStorage) {
    haveLocalStorage = 'localStorage' in window;
    return true;
  };

})(this);
<commit_msg>only recover widget state from localStorage when it actually makes sense.<commit_after>(function(window) {

  var haveLocalStorage;
  var LS_STATE_KEY = "remotestorage:widget:state";
  // states allowed to immediately jump into after a reload.
  var VALID_ENTRY_STATES = {
    initial: true, connected: true, offline: true
  };

  function stateSetter(widget, state) {
    return function() {
      if(haveLocalStorage) {
        localStorage[LS_STATE_KEY] = state;
      }
      if(widget.view) {
        if(widget.rs.remote) {
          widget.view.setUserAddress(widget.rs.remote.userAddress);
        }
        widget.view.setState(state, arguments);
      }
    };
  }
  function errorsHandler(widget){
    //decided to not store error state
    return function(error){
      if(error instanceof RemoteStorage.DiscoveryError) {
        console.log('discovery failed',  error, '"' + error.message + '"');
        widget.view.setState('initial', [error.message]);
      } else if(error instanceof RemoteStorage.SyncError) {
        widget.view.setState('offline', []);
      } else if(error instanceof RemoteStorage.Unauthorized){
        widget.view.setState('unauthorized')
      } else {
        widget.view.setState('error', [error]);
      }
    }
  }
  RemoteStorage.Widget = function(remoteStorage) {

    // setting event listeners on rs events to put
    // the widget into corresponding states
    this.rs = remoteStorage;
    this.rs.on('ready', stateSetter(this, 'connected'));
    this.rs.on('disconnected', stateSetter(this, 'initial'));
    this.rs.on('authing', stateSetter(this, 'authing'));
    this.rs.on('sync-busy', stateSetter(this, 'busy'));
    this.rs.on('sync-done', stateSetter(this, 'connected'));
    this.rs.on('error', errorsHandler(this) );
    if(haveLocalStorage) {
      var state = localStorage[LS_STATE_KEY] = state;
      if(state && VALID_ENTRY_STATES[state]) {
        this._rememberedState = state;
      }
    }
  };

  RemoteStorage.Widget.prototype = {
    // Methods :
    //   display(domID)
    //     displays the widget via the view.display method
    //    returns: this
    //
    //   setView(view)
    //     sets the view and initializes event listeners to
    //     react on widget events
    //

    display: function(domID) {
      if(! this.view) {
        this.setView(new RemoteStorage.Widget.View(domID));
      }
      this.view.display.apply(this.view, arguments);
      return this;
    },

    setView: function(view) {
      this.view = view;
      this.view.on('connect', this.rs.connect.bind(this.rs));
      this.view.on('disconnect', this.rs.disconnect.bind(this.rs));
      this.view.on('sync', this.rs.sync.bind(this.rs));
      try {
        this.view.on('reset', function(){
          this.rs.on('disconnected', document.location.reload.bind(document.location))
          this.rs.disconnect()
        }.bind(this));
      } catch(e) {
        if(e.message && e.message.match(/Unknown event/)) {
          // ignored. (the 0.7 widget-view interface didn't have a 'reset' event)
        } else {
          throw e;
        }
      }

      if(this._rememberedState) {
        stateSetter(this, this._rememberedState)();
        delete this._rememberedState;
      }
    }
  };

  RemoteStorage.prototype.displayWidget = function(domID) {
    this.widget.display(domID);
  };

  RemoteStorage.Widget._rs_init = function(remoteStorage) {
    if(! remoteStorage.widget) {
      remoteStorage.widget = new RemoteStorage.Widget(remoteStorage);
    }
  };

  RemoteStorage.Widget._rs_supported = function(remoteStorage) {
    haveLocalStorage = 'localStorage' in window;
    return true;
  };

})(this);
<|endoftext|>


<commit_before>import React from 'react';
import ReactDOM from 'react-dom';
import Mimic from 'ui/components/Mimic';

const root = document.createElement('mimic-div');
root.style.width = '100%';
root.style.top = 0;
root.style.left = 0;
root.style.zIndex = 2147483646;

export default function bootstrapUI() {
  document.body.appendChild(root);

  ReactDOM.render(<Mimic />, root);
}
<commit_msg>Support loading mimic inside <head> tag<commit_after>import React from 'react';
import ReactDOM from 'react-dom';
import Mimic from 'ui/components/Mimic';

const root = document.createElement('mimic-div');
root.style.width = '100%';
root.style.top = 0;
root.style.left = 0;
root.style.zIndex = 2147483646;

export default function bootstrapUI() {
  document.addEventListener("DOMContentLoaded", function() {
      document.body.appendChild(root);
      ReactDOM.render(<Mimic />, root);
  });
}
<|endoftext|>