<commit_before>/**
The _enyo.Panels_ kind is designed to satisfy a variety of common use cases for
application layout. Using _enyo.Panels_, controls may be arranged as (among
other things) a carousel, a set of collapsing panels, a card stack that fades
between panels, or a grid.

Any Enyo control may be placed inside an _enyo.Panels_, but by convention we
refer to each of these controls as a "panel." From the set of panels in an
_enyo.Panels_, one is considered to be active. The active panel is set by index
using the _setIndex_ method. The actual layout of the panels typically changes
each time the active panel is set, such that the new active panel has the most
prominent position.

For more information, see the
[Panels documentation](https://github.com/enyojs/enyo/wiki/Panels) in the Enyo
Developer Guide.
*/
enyo.kind({
	name: "enyo.Panels",
	classes: "enyo-panels",
	published: {
		/**
			The index of the active panel. The layout of panels is controlled by
			the layoutKind, but as a rule, the active panel is displayed in the
			most prominent position. For example, in the (default) CardArranger
			layout, the active panel is shown and the other panels are hidden.
		*/
		index: 0,
		//* Controls whether the user can drag between panels.
		draggable: true,
		//* Controls whether the panels animate when transitioning; for example,
		//* when _setIndex_ is called.
		animate: true,
		//* Controls whether panels "wrap around" when moving past the end.
		//* The actual effect depends upon the arranger in use.
		wrap: false,
		//* Sets the arranger kind to be used for dynamic layout.
		arrangerKind: "CardArranger",
		//* By default, each panel will be sized to fit the Panels' width when
		//* the screen size is narrow enough (less than ~800px). Set to false
		//* to avoid this behavior.
		narrowFit: true
	},
	events: {
		/**
			Fires at the start of a panel transition, when _setIndex_ is called
			and also during dragging.

			_inEvent.fromIndex_ contains the index of the old panel.

			_inEvent.toIndex_ contains the index of the new panel.
		*/
		onTransitionStart: "",
		/**
			Fires at the end of a panel transition, when _setIndex_ is called
			and also during dragging.

			_inEvent.fromIndex_ contains the index of the old panel.

			_inEvent.toIndex_ contains the index of the new panel.
		*/
		onTransitionFinish: ""
	},
	//* @protected
	handlers: {
		ondragstart: "dragstart",
		ondrag: "drag",
		ondragfinish: "dragfinish"
	},
	tools: [
		{kind: "Animator", onStep: "step", onEnd: "completed"}
	],
	fraction: 0,
	create: function() {
		this.transitionPoints = [];
		this.inherited(arguments);
		this.arrangerKindChanged();
		this.avoidFitChanged();
		this.indexChanged();
	},
	initComponents: function() {
		this.createChrome(this.tools);
		this.inherited(arguments);
	},
	arrangerKindChanged: function() {
		this.setLayoutKind(this.arrangerKind);
	},
	avoidFitChanged: function() {
		this.addRemoveClass("enyo-panels-fit-narrow", this.narrowFit);
	},
	removeControl: function(inControl) {
		this.inherited(arguments);
		if (this.controls.length > 1 && this.isPanel(inControl)) {
			this.setIndex(Math.max(this.index - 1, 0));
			this.flow();
			this.reflow();
		}
	},
	isPanel: function() {
		// designed to be overridden in kinds derived from Panels that have
		// non-panel client controls
		return true;
	},
	flow: function() {
		this.arrangements = [];
		this.inherited(arguments);
	},
	reflow: function() {
		this.arrangements = [];
		this.inherited(arguments);
		this.refresh();
	},
	//* @public
	/**
		Returns an array of contained panels.
		Subclasses can override this if they don't want the arranger to layout all of their children
	*/
	getPanels: function() {
		var p = this.controlParent || this;
		return p.children;
	},
	//* Returns a reference to the active panel--i.e., the panel at the specified index.
	getActive: function() {
		var p$ = this.getPanels();
		return p$[this.index];
	},
	/**
		Returns a reference to the enyo.Animator
		instance used to animate panel transitions. The Panels' animator can be used
		to set the duration of panel transitions, e.g.:

			this.getAnimator().setDuration(1000);
	*/
	getAnimator: function() {
		return this.$.animator;
	},
	/**
		Sets the active panel to the panel specified by the given index.
		Note that if the _animate_ property is set to true, the active panel
		will animate into view.
	*/
	setIndex: function(inIndex) {
		// override setIndex so that indexChanged is called
		// whether this.index has actually changed or not
		this.setPropertyValue("index", inIndex, "indexChanged");
	},
	/**
		Sets the active panel to the panel specified by the given index.
		Regardless of the value of the _animate_ property, the transition to the
		next panel will not animate and will be immediate.
	*/
	setIndexDirect: function(inIndex) {
		this.setIndex(inIndex);
		this.completed();
	},
	//* Transitions to the previous panel--i.e., the panel whose index value is
	//* one less than that of the current active panel.
	previous: function() {
		this.setIndex(this.index-1);
	},
	//* Transitions to the next panel--i.e., the panel whose index value is one
	//* greater than that of the current active panel.
	next: function() {
		this.setIndex(this.index+1);
	},
	//* @protected
	clamp: function(inValue) {
		var l = this.getPanels().length-1;
		if (this.wrap) {
			// FIXME: dragging makes assumptions about direction and from->start indexes.
			//return inValue < 0 ? l : (inValue > l ? 0 : inValue);
			return inValue;
		} else {
			return Math.max(0, Math.min(inValue, l));
		}
	},
	indexChanged: function(inOld) {
		this.lastIndex = inOld;
		this.index = this.clamp(this.index);
		if (!this.dragging) {
			if (this.$.animator.isAnimating()) {
				this.completed();
			}
			this.$.animator.stop();
			if (this.hasNode()) {
				if (this.animate) {
					this.startTransition();
					this.$.animator.play({
						startValue: this.fraction
					});
				} else {
					this.refresh();
				}
			}
		}
	},
	step: function(inSender) {
		this.fraction = inSender.value;
		this.stepTransition();
	},
	completed: function() {
		if (this.$.animator.isAnimating()) {
			this.$.animator.stop();
		}
		this.fraction = 1;
		this.stepTransition();
		this.finishTransition();
	},
	dragstart: function(inSender, inEvent) {
		if (this.draggable && this.layout && this.layout.canDragEvent(inEvent)) {
			inEvent.preventDefault();
			this.dragstartTransition(inEvent);
			this.dragging = true;
			this.$.animator.stop();
			return true;
		}
	},
	drag: function(inSender, inEvent) {
		if (this.dragging) {
			inEvent.preventDefault();
			this.dragTransition(inEvent);
		}
	},
	dragfinish: function(inSender, inEvent) {
		if (this.dragging) {
			this.dragging = false;
			inEvent.preventTap();
			this.dragfinishTransition(inEvent);
		}
	},
	dragstartTransition: function(inEvent) {
		if (!this.$.animator.isAnimating()) {
			var f = this.fromIndex = this.index;
			this.toIndex = f - (this.layout ? this.layout.calcDragDirection(inEvent) : 0);
		} else {
			this.verifyDragTransition(inEvent);
		}
		this.fromIndex = this.clamp(this.fromIndex);
		this.toIndex = this.clamp(this.toIndex);
		//this.log(this.fromIndex, this.toIndex);
		this.fireTransitionStart();
		if (this.layout) {
			this.layout.start();
		}
	},
	dragTransition: function(inEvent) {
		// note: for simplicity we choose to calculate the distance directly between
		// the first and last transition point.
		var d = this.layout ? this.layout.calcDrag(inEvent) : 0;
		var t$ = this.transitionPoints, s = t$[0], f = t$[t$.length-1];
		var as = this.fetchArrangement(s);
		var af = this.fetchArrangement(f);
		var dx = this.layout ? this.layout.drag(d, s, as, f, af) : 0;
		var dragFail = d && !dx;
		if (dragFail) {
			//this.log(dx, s, as, f, af);
		}
		this.fraction += dx;
		var fr = this.fraction;
		if (fr > 1 || fr < 0 || dragFail) {
			if (fr > 0 || dragFail) {
				this.dragfinishTransition(inEvent);
			}
			this.dragstartTransition(inEvent);
			this.fraction = 0;
			// FIXME: account for lost fraction
			//this.dragTransition(inEvent);
		}
		this.stepTransition();
	},
	dragfinishTransition: function(inEvent) {
		this.verifyDragTransition(inEvent);
		this.setIndex(this.toIndex);
		// note: if we're still dragging, then we're at a transition boundary
		// and should fire the finish event
		if (this.dragging) {
			this.fireTransitionFinish();
		}
	},
	verifyDragTransition: function(inEvent) {
		var d = this.layout ? this.layout.calcDragDirection(inEvent) : 0;
		var f = Math.min(this.fromIndex, this.toIndex);
		var t = Math.max(this.fromIndex, this.toIndex);
		if (d > 0) {
			var s = f;
			f = t;
			t = s;
		}
		if (f != this.fromIndex) {
			this.fraction = 1 - this.fraction;
		}
		//this.log("old", this.fromIndex, this.toIndex, "new", f, t);
		this.fromIndex = f;
		this.toIndex = t;
	},
	refresh: function() {
		if (this.$.animator.isAnimating()) {
			this.$.animator.stop();
		}
		this.startTransition();
		this.fraction = 1;
		this.stepTransition();
		this.finishTransition();
	},
	startTransition: function() {
		this.fromIndex = this.fromIndex != null ? this.fromIndex : this.lastIndex || 0;
		this.toIndex = this.toIndex != null ? this.toIndex : this.index;
		//this.log(this.id, this.fromIndex, this.toIndex);
		if (this.layout) {
			this.layout.start();
		}
		this.fireTransitionStart();
	},
	finishTransition: function() {
		if (this.layout) {
			this.layout.finish();
		}
		this.transitionPoints = [];
		this.fraction = 0;
		this.fromIndex = this.toIndex = null;
		this.fireTransitionFinish();
	},
	fireTransitionStart: function() {
		var t = this.startTransitionInfo;
		if (this.hasNode() && (!t || (t.fromIndex != this.fromIndex || t.toIndex != this.toIndex))) {
			this.startTransitionInfo = {fromIndex: this.fromIndex, toIndex: this.toIndex};
			this.doTransitionStart(enyo.clone(this.startTransitionInfo));
		}
	},
	fireTransitionFinish: function() {
		var t = this.finishTransitionInfo;
		if (this.hasNode() && (!t || (t.fromIndex != this.lastIndex || t.toIndex != this.index))) {
			this.finishTransitionInfo = {fromIndex: this.lastIndex, toIndex: this.index};
			this.doTransitionFinish(enyo.clone(this.finishTransitionInfo));
		}
		this.lastIndex=this.index;
	},
	// gambit: we interpolate between arrangements as needed.
	stepTransition: function() {
		if (this.hasNode()) {
			// select correct transition points and normalize fraction.
			var t$ = this.transitionPoints;
			var r = (this.fraction || 0) * (t$.length-1);
			var i = Math.floor(r);
			r = r - i;
			var s = t$[i], f = t$[i+1];
			// get arrangements and lerp between them
			var s0 = this.fetchArrangement(s);
			var s1 = this.fetchArrangement(f);
			this.arrangement = s0 && s1 ? enyo.Panels.lerp(s0, s1, r) : (s0 || s1);
			if (this.arrangement && this.layout) {
				this.layout.flowArrangement();
			}
		}
	},
	fetchArrangement: function(inName) {
		if ((inName != null) && !this.arrangements[inName] && this.layout) {
			this.layout._arrange(inName);
			this.arrangements[inName] = this.readArrangement(this.getPanels());
		}
		return this.arrangements[inName];
	},
	readArrangement: function(inC) {
		var r = [];
		for (var i=0, c$=inC, c; (c=c$[i]); i++) {
			r.push(enyo.clone(c._arranger));
		}
		return r;
	},
	statics: {
		//* @public
		/**
			returns true when window width is 800px or less. This value must
			be the same as the "max-width" media query used for panel sizing
			in _Panels.css_.
		**/
		isScreenNarrow: function() {
			return enyo.dom.getWindowWidth() <= 800;
		},
		//* @protected
		lerp: function(inA0, inA1, inFrac) {
			var r = [];
			for (var i=0, k$=enyo.keys(inA0), k; (k=k$[i]); i++) {
				r.push(this.lerpObject(inA0[k], inA1[k], inFrac));
			}
			return r;
		},
		lerpObject: function(inNew, inOld, inFrac) {
			var b = enyo.clone(inNew), n, o;
			// inOld might be undefined when deleting panels
			if (inOld) {
				for (var i in inNew) {
					n = inNew[i];
					o = inOld[i];
					if (n != o) {
						b[i] = n - (n - o) * inFrac;
					}
				}
			}
			return b;
		}
	}
});

<commit_msg>Added code to constrain the index in Panels getActive function within the array of panels. This was needed in the case of a wrapped set of panels & allows the function to always return the expected panel rather than undefined in the case of wrapped panels.<commit_after>/**
The _enyo.Panels_ kind is designed to satisfy a variety of common use cases for
application layout. Using _enyo.Panels_, controls may be arranged as (among
other things) a carousel, a set of collapsing panels, a card stack that fades
between panels, or a grid.

Any Enyo control may be placed inside an _enyo.Panels_, but by convention we
refer to each of these controls as a "panel." From the set of panels in an
_enyo.Panels_, one is considered to be active. The active panel is set by index
using the _setIndex_ method. The actual layout of the panels typically changes
each time the active panel is set, such that the new active panel has the most
prominent position.

For more information, see the
[Panels documentation](https://github.com/enyojs/enyo/wiki/Panels) in the Enyo
Developer Guide.
*/
enyo.kind({
	name: "enyo.Panels",
	classes: "enyo-panels",
	published: {
		/**
			The index of the active panel. The layout of panels is controlled by
			the layoutKind, but as a rule, the active panel is displayed in the
			most prominent position. For example, in the (default) CardArranger
			layout, the active panel is shown and the other panels are hidden.
		*/
		index: 0,
		//* Controls whether the user can drag between panels.
		draggable: true,
		//* Controls whether the panels animate when transitioning; for example,
		//* when _setIndex_ is called.
		animate: true,
		//* Controls whether panels "wrap around" when moving past the end.
		//* The actual effect depends upon the arranger in use.
		wrap: false,
		//* Sets the arranger kind to be used for dynamic layout.
		arrangerKind: "CardArranger",
		//* By default, each panel will be sized to fit the Panels' width when
		//* the screen size is narrow enough (less than ~800px). Set to false
		//* to avoid this behavior.
		narrowFit: true
	},
	events: {
		/**
			Fires at the start of a panel transition, when _setIndex_ is called
			and also during dragging.

			_inEvent.fromIndex_ contains the index of the old panel.

			_inEvent.toIndex_ contains the index of the new panel.
		*/
		onTransitionStart: "",
		/**
			Fires at the end of a panel transition, when _setIndex_ is called
			and also during dragging.

			_inEvent.fromIndex_ contains the index of the old panel.

			_inEvent.toIndex_ contains the index of the new panel.
		*/
		onTransitionFinish: ""
	},
	//* @protected
	handlers: {
		ondragstart: "dragstart",
		ondrag: "drag",
		ondragfinish: "dragfinish"
	},
	tools: [
		{kind: "Animator", onStep: "step", onEnd: "completed"}
	],
	fraction: 0,
	create: function() {
		this.transitionPoints = [];
		this.inherited(arguments);
		this.arrangerKindChanged();
		this.avoidFitChanged();
		this.indexChanged();
	},
	initComponents: function() {
		this.createChrome(this.tools);
		this.inherited(arguments);
	},
	arrangerKindChanged: function() {
		this.setLayoutKind(this.arrangerKind);
	},
	avoidFitChanged: function() {
		this.addRemoveClass("enyo-panels-fit-narrow", this.narrowFit);
	},
	removeControl: function(inControl) {
		this.inherited(arguments);
		if (this.controls.length > 1 && this.isPanel(inControl)) {
			this.setIndex(Math.max(this.index - 1, 0));
			this.flow();
			this.reflow();
		}
	},
	isPanel: function() {
		// designed to be overridden in kinds derived from Panels that have
		// non-panel client controls
		return true;
	},
	flow: function() {
		this.arrangements = [];
		this.inherited(arguments);
	},
	reflow: function() {
		this.arrangements = [];
		this.inherited(arguments);
		this.refresh();
	},
	//* @public
	/**
		Returns an array of contained panels.
		Subclasses can override this if they don't want the arranger to layout all of their children
	*/
	getPanels: function() {
		var p = this.controlParent || this;
		return p.children;
	},
	//* Returns a reference to the active panel--i.e., the panel at the specified index.
	getActive: function() {
		var p$ = this.getPanels();
		//Constrain the index within the array of panels, needed if wrapping is enabled
		var index = this.index % p$.length;
		index < 0 ? (index += p$.length) : enyo.nop;
		return p$[index];
	},
	/**
		Returns a reference to the enyo.Animator
		instance used to animate panel transitions. The Panels' animator can be used
		to set the duration of panel transitions, e.g.:

			this.getAnimator().setDuration(1000);
	*/
	getAnimator: function() {
		return this.$.animator;
	},
	/**
		Sets the active panel to the panel specified by the given index.
		Note that if the _animate_ property is set to true, the active panel
		will animate into view.
	*/
	setIndex: function(inIndex) {
		// override setIndex so that indexChanged is called
		// whether this.index has actually changed or not
		this.setPropertyValue("index", inIndex, "indexChanged");
	},
	/**
		Sets the active panel to the panel specified by the given index.
		Regardless of the value of the _animate_ property, the transition to the
		next panel will not animate and will be immediate.
	*/
	setIndexDirect: function(inIndex) {
		this.setIndex(inIndex);
		this.completed();
	},
	//* Transitions to the previous panel--i.e., the panel whose index value is
	//* one less than that of the current active panel.
	previous: function() {
		this.setIndex(this.index-1);
	},
	//* Transitions to the next panel--i.e., the panel whose index value is one
	//* greater than that of the current active panel.
	next: function() {
		this.setIndex(this.index+1);
	},
	//* @protected
	clamp: function(inValue) {
		var l = this.getPanels().length-1;
		if (this.wrap) {
			// FIXME: dragging makes assumptions about direction and from->start indexes.
			//return inValue < 0 ? l : (inValue > l ? 0 : inValue);
			return inValue;
		} else {
			return Math.max(0, Math.min(inValue, l));
		}
	},
	indexChanged: function(inOld) {
		this.lastIndex = inOld;
		this.index = this.clamp(this.index);
		if (!this.dragging) {
			if (this.$.animator.isAnimating()) {
				this.completed();
			}
			this.$.animator.stop();
			if (this.hasNode()) {
				if (this.animate) {
					this.startTransition();
					this.$.animator.play({
						startValue: this.fraction
					});
				} else {
					this.refresh();
				}
			}
		}
	},
	step: function(inSender) {
		this.fraction = inSender.value;
		this.stepTransition();
	},
	completed: function() {
		if (this.$.animator.isAnimating()) {
			this.$.animator.stop();
		}
		this.fraction = 1;
		this.stepTransition();
		this.finishTransition();
	},
	dragstart: function(inSender, inEvent) {
		if (this.draggable && this.layout && this.layout.canDragEvent(inEvent)) {
			inEvent.preventDefault();
			this.dragstartTransition(inEvent);
			this.dragging = true;
			this.$.animator.stop();
			return true;
		}
	},
	drag: function(inSender, inEvent) {
		if (this.dragging) {
			inEvent.preventDefault();
			this.dragTransition(inEvent);
		}
	},
	dragfinish: function(inSender, inEvent) {
		if (this.dragging) {
			this.dragging = false;
			inEvent.preventTap();
			this.dragfinishTransition(inEvent);
		}
	},
	dragstartTransition: function(inEvent) {
		if (!this.$.animator.isAnimating()) {
			var f = this.fromIndex = this.index;
			this.toIndex = f - (this.layout ? this.layout.calcDragDirection(inEvent) : 0);
		} else {
			this.verifyDragTransition(inEvent);
		}
		this.fromIndex = this.clamp(this.fromIndex);
		this.toIndex = this.clamp(this.toIndex);
		//this.log(this.fromIndex, this.toIndex);
		this.fireTransitionStart();
		if (this.layout) {
			this.layout.start();
		}
	},
	dragTransition: function(inEvent) {
		// note: for simplicity we choose to calculate the distance directly between
		// the first and last transition point.
		var d = this.layout ? this.layout.calcDrag(inEvent) : 0;
		var t$ = this.transitionPoints, s = t$[0], f = t$[t$.length-1];
		var as = this.fetchArrangement(s);
		var af = this.fetchArrangement(f);
		var dx = this.layout ? this.layout.drag(d, s, as, f, af) : 0;
		var dragFail = d && !dx;
		if (dragFail) {
			//this.log(dx, s, as, f, af);
		}
		this.fraction += dx;
		var fr = this.fraction;
		if (fr > 1 || fr < 0 || dragFail) {
			if (fr > 0 || dragFail) {
				this.dragfinishTransition(inEvent);
			}
			this.dragstartTransition(inEvent);
			this.fraction = 0;
			// FIXME: account for lost fraction
			//this.dragTransition(inEvent);
		}
		this.stepTransition();
	},
	dragfinishTransition: function(inEvent) {
		this.verifyDragTransition(inEvent);
		this.setIndex(this.toIndex);
		// note: if we're still dragging, then we're at a transition boundary
		// and should fire the finish event
		if (this.dragging) {
			this.fireTransitionFinish();
		}
	},
	verifyDragTransition: function(inEvent) {
		var d = this.layout ? this.layout.calcDragDirection(inEvent) : 0;
		var f = Math.min(this.fromIndex, this.toIndex);
		var t = Math.max(this.fromIndex, this.toIndex);
		if (d > 0) {
			var s = f;
			f = t;
			t = s;
		}
		if (f != this.fromIndex) {
			this.fraction = 1 - this.fraction;
		}
		//this.log("old", this.fromIndex, this.toIndex, "new", f, t);
		this.fromIndex = f;
		this.toIndex = t;
	},
	refresh: function() {
		if (this.$.animator.isAnimating()) {
			this.$.animator.stop();
		}
		this.startTransition();
		this.fraction = 1;
		this.stepTransition();
		this.finishTransition();
	},
	startTransition: function() {
		this.fromIndex = this.fromIndex != null ? this.fromIndex : this.lastIndex || 0;
		this.toIndex = this.toIndex != null ? this.toIndex : this.index;
		//this.log(this.id, this.fromIndex, this.toIndex);
		if (this.layout) {
			this.layout.start();
		}
		this.fireTransitionStart();
	},
	finishTransition: function() {
		if (this.layout) {
			this.layout.finish();
		}
		this.transitionPoints = [];
		this.fraction = 0;
		this.fromIndex = this.toIndex = null;
		this.fireTransitionFinish();
	},
	fireTransitionStart: function() {
		var t = this.startTransitionInfo;
		if (this.hasNode() && (!t || (t.fromIndex != this.fromIndex || t.toIndex != this.toIndex))) {
			this.startTransitionInfo = {fromIndex: this.fromIndex, toIndex: this.toIndex};
			this.doTransitionStart(enyo.clone(this.startTransitionInfo));
		}
	},
	fireTransitionFinish: function() {
		var t = this.finishTransitionInfo;
		if (this.hasNode() && (!t || (t.fromIndex != this.lastIndex || t.toIndex != this.index))) {
			this.finishTransitionInfo = {fromIndex: this.lastIndex, toIndex: this.index};
			this.doTransitionFinish(enyo.clone(this.finishTransitionInfo));
		}
		this.lastIndex=this.index;
	},
	// gambit: we interpolate between arrangements as needed.
	stepTransition: function() {
		if (this.hasNode()) {
			// select correct transition points and normalize fraction.
			var t$ = this.transitionPoints;
			var r = (this.fraction || 0) * (t$.length-1);
			var i = Math.floor(r);
			r = r - i;
			var s = t$[i], f = t$[i+1];
			// get arrangements and lerp between them
			var s0 = this.fetchArrangement(s);
			var s1 = this.fetchArrangement(f);
			this.arrangement = s0 && s1 ? enyo.Panels.lerp(s0, s1, r) : (s0 || s1);
			if (this.arrangement && this.layout) {
				this.layout.flowArrangement();
			}
		}
	},
	fetchArrangement: function(inName) {
		if ((inName != null) && !this.arrangements[inName] && this.layout) {
			this.layout._arrange(inName);
			this.arrangements[inName] = this.readArrangement(this.getPanels());
		}
		return this.arrangements[inName];
	},
	readArrangement: function(inC) {
		var r = [];
		for (var i=0, c$=inC, c; (c=c$[i]); i++) {
			r.push(enyo.clone(c._arranger));
		}
		return r;
	},
	statics: {
		//* @public
		/**
			returns true when window width is 800px or less. This value must
			be the same as the "max-width" media query used for panel sizing
			in _Panels.css_.
		**/
		isScreenNarrow: function() {
			return enyo.dom.getWindowWidth() <= 800;
		},
		//* @protected
		lerp: function(inA0, inA1, inFrac) {
			var r = [];
			for (var i=0, k$=enyo.keys(inA0), k; (k=k$[i]); i++) {
				r.push(this.lerpObject(inA0[k], inA1[k], inFrac));
			}
			return r;
		},
		lerpObject: function(inNew, inOld, inFrac) {
			var b = enyo.clone(inNew), n, o;
			// inOld might be undefined when deleting panels
			if (inOld) {
				for (var i in inNew) {
					n = inNew[i];
					o = inOld[i];
					if (n != o) {
						b[i] = n - (n - o) * inFrac;
					}
				}
			}
			return b;
		}
	}
});

<|endoftext|>


<commit_before>/* =========================================================
 * HappyR Dialog is based on bootstrap modal by Twitter Inc.
 *
 * Documentation and examples is found at http://developer.happyr.se
 *
 * =========================================================
 * The MIT License (MIT)
 *
 * Copyright (c) 2013 <HappyRecruiting, Twitter Inc>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of
 * the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
 *  THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 *  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 *  SOFTWARE.
 *
 * ========================================================= */
!function($){"use strict";function happyrDialog_convertStringToObject(string){return void 0===string||""===string?{}:JSON.parse(JSON.stringify(eval("("+string+")")))}function happyrDialog_addWrapper(t,i){var o;o=$(".happyr-dialog-body",t).length>0?t.find(".happyr-dialog-body").html():t.html(),t.html(""),t.addClass("happyr-dialog"),i.animation&&t.addClass("happyr-dialog-animation"),i.showHeader&&happyrDialog_addHeader(t,i);var a=$("

").addClass("happyr-dialog-body").append(o);t.append(a),i.showFooter&&happyrDialog_addFooter(t,i)}function happyrDialog_addHeader(t,i){var o=$("

").addClass("happyr-dialog-header");if(i.showHeaderTitle&&void 0!==i.texts.title){var a=$("
"+i.texts.title+"
");o.append(a)}if(i.showConfirmButton){var e=$("<button type='button' class='close' data-dismiss='happyr-dialog' aria-hidden='true'>x</button>");o.append(e)}o.append($("

").addClass("happyr-dialog-clearfix")),t.append(o)}function happyrDialog_addFooter(t,i){var o=$("

").addClass("happyr-dialog-footer");if(i.showCloseButton){var a=$("<button class='btn' data-dismiss='happyr-dialog' aria-hidden='true'>"+i.texts.close+"</button>");o.append(a)}if(i.showConfirmButton){var e=$("<button class='btn btn-primary'>"+i.texts.confirm+"</button>");e.click(function(){$(document).trigger("happyr-dialog-confirm")}),o.append(e)}t.append(o)}var HappyrDialog=function(t,i){if(this.options=i,this.$element=$(t),this.$element.delegate('[data-dismiss="happyr-dialog"]',"click.dismiss.happyr-dialog",$.proxy(this.hide,this)),this.$element.hide(),happyrDialog_addWrapper(this.$element,i),this.options.remote){var o=this.$element.find(".happyr-dialog-body");void 0!==i.loaderImage&&o.html(''),o.load(this.options.remote,function(){if(i.submitFormOnConfirm){var t=$("form",$(this));t.length>0&&$(document).on("happyr-dialog-confirm",function(){i.getFormResultInDialog?$.ajax({type:t.attr("method"),url:t.attr("action"),data:t.serialize(),success:function(t){o.html(t)}}):i.hideAfterFormSubmit&&(t.submit(),$("#happyr-dialog").happyrDialog("hide"))})}})}};HappyrDialog.prototype={constructor:HappyrDialog,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var t=this,i=$.Event("show");this.$element.trigger(i),this.isShown||i.isDefaultPrevented()||(this.isShown=!0,this.escape(),this.backdrop(function(){t.$element.parent().length||t.$element.appendTo(document.body);var i=function(){t.$element.focus().trigger("shown"),$(document).trigger("happyr-dialog-shown")},o=function(){t.$element.show(),t.$element.attr("aria-hidden",!1),t.enforceFocus()};t.options.animate?(t.$element.css("opacity",0).css("top","-25%"),o(),t.$element.animate({top:"10%",opacity:"1"},t.options.animation.timeDialogShow,"swing",i)):(o(),i())}))},hide:function(t){t&&t.preventDefault();var i=this;if(t=$.Event("hide"),this.$element.trigger(t),this.isShown&&!t.isDefaultPrevented()){this.isShown=!1,this.escape(),$(document).off("focusin.happyr-dialog");var o=function(){i.$element.attr("aria-hidden",!0),i.hideDialog()};i.options.animate?i.$element.animate({top:"-25%",opacity:"0"},i.options.animation.timeDialogHide,"swing",o):o()}},enforceFocus:function(){var t=this;$(document).on("focusin.happyr-dialog",function(i){t.$element[0]===i.target||t.$element.has(i.target).length||t.$element.focus()})},escape:function(){var t=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.happyr-dialog",function(i){27==i.which&&t.hide()}):this.isShown||this.$element.off("keyup.dismiss.happyr-dialog")},hideDialog:function(){var t=this;this.$element.hide(),this.backdrop(function(){t.removeBackdrop(),t.$element.trigger("hidden")})},removeBackdrop:function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},backdrop:function(t){var i=this;this.isShown&&this.options.backdrop?(this.$backdrop=$('
').appendTo(document.body),this.$backdrop.click("static"==this.options.backdrop?$.proxy(this.$element[0].focus,this.$element[0]):$.proxy(this.hide,this)),this.options.animation?(this.$backdrop.css("opacity",0),this.$backdrop.show(),this.$backdrop.fadeTo(i.options.animation.timeBackdropShow,i.options.animation.opacityBackdrop,t)):(this.$backdrop.show(),t())):!this.isShown&&this.$backdrop?this.options.animation?this.$backdrop.fadeTo(i.options.animation.timeDialogHide,0,t):t():t&&t()}};var old=$.fn.happyrDialog;$.fn.happyrDialog=function(t){return this.each(function(){var i=$(this),o=i.data("happyrDialog"),a=$.extend({},$.fn.happyrDialog.defaults,i.data(),"object"==typeof t&&t);o&&o.isShown||i.data("happyrDialog",o=new HappyrDialog(this,a)),"string"==typeof t?o[t]():void 0!==t.action?o[t.action]():a.show&&o.show()})},$.fn.happyrDialog.defaults={animate:!0,animation:{timeBackdropShow:200,timeBackdropHide:200,timeDialogShow:300,timeDialogHide:300,opacityBackdrop:.8},backdrop:!0,keyboard:!0,show:!0,showHeader:!0,showFooter:!0,showCloseButton:!0,showConfirmButton:!0,showHeaderCloseButton:!0,showHeaderTitle:!0,submitFormOnConfirm:!0,texts:{close:"Close",confirm:"Confirm"},getFormResultInDialog:!0,hideAfterFormSubmit:!1},$.fn.happyrDialog.Constructor=HappyrDialog,$.fn.happyrDialog.noConflict=function(){return $.fn.happyrDialog=old,this},$(document).on("click.happyr-dialog.data-api",'[data-toggle="happyr-dialog"]',function(t){var i=$(this),o=i.attr("href"),a=$(i.attr("data-target")||o&&!o.match(/[?=\/&]/)&&o.replace(/.*(?=#[^\s]+$)/,"")),e=happyrDialog_convertStringToObject(i.attr("data-happyr-dialog-settings"));e=$.extend({remote:!/#/.test(o)&&o,action:"toggle"},a.data(),i.data(),e),t.preventDefault(),0===a.length&&(a=$("#happyr-dialog"),0===a.length&&(a=$('
').appendTo(document.body))),a.happyrDialog(e).one("hide",function(){i.focus()})})}(window.jQuery);<commit_msg>updated min<commit_after>/* =========================================================
 * HappyR Dialog is based on bootstrap modal by Twitter Inc.
 *
 * Documentation and examples is found at http://developer.happyr.se
 *
 * =========================================================
 * The MIT License (MIT)
 *
 * Copyright (c) 2013 <HappyRecruiting, Twitter Inc>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of
 * the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
 *  THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 *  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 *  SOFTWARE.
 *
 * ========================================================= */
!function($){"use strict";function happyrDialog_convertStringToObject(string){return void 0===string||""===string?{}:JSON.parse(JSON.stringify(eval("("+string+")")))}function happyrDialog_addWrapper(t,i){var o;o=$(".happyr-dialog-body",t).length>0?t.find(".happyr-dialog-body").html():t.html(),t.html(""),t.addClass("happyr-dialog"),i.showHeader&&happyrDialog_addHeader(t,i);var a=$("

").addClass("happyr-dialog-body").append(o);t.append(a),i.showFooter&&happyrDialog_addFooter(t,i)}function happyrDialog_addHeader(t,i){var o=$("

").addClass("happyr-dialog-header");if(i.showHeaderTitle&&void 0!==i.texts.title){var a=$("
"+i.texts.title+"
");o.append(a)}if(i.showConfirmButton){var e=$("<button type='button' class='close' data-dismiss='happyr-dialog' aria-hidden='true'>x</button>");o.append(e)}o.append($("

").addClass("happyr-dialog-clearfix")),t.append(o)}function happyrDialog_addFooter(t,i){var o=$("

").addClass("happyr-dialog-footer");if(i.showCloseButton){var a=$("<button class='btn' data-dismiss='happyr-dialog' aria-hidden='true'>"+i.texts.close+"</button>");o.append(a)}if(i.showConfirmButton){var e=$("<button class='btn btn-primary'>"+i.texts.confirm+"</button>");e.click(function(){$(document).trigger("happyr-dialog-confirm")}),o.append(e)}t.append(o)}var HappyrDialog=function(t,i){if(this.options=i,this.$element=$(t),this.$element.delegate('[data-dismiss="happyr-dialog"]',"click.dismiss.happyr-dialog",$.proxy(this.hide,this)),this.$element.hide(),happyrDialog_addWrapper(this.$element,i),this.options.remote){var o=this.$element.find(".happyr-dialog-body");void 0!==i.loaderImage&&o.html(''),o.load(this.options.remote,function(){if(i.submitFormOnConfirm){var t=$("form",$(this));t.length>0&&$(document).on("happyr-dialog-confirm",function(){i.getFormResultInDialog?$.ajax({type:t.attr("method"),url:t.attr("action"),data:t.serialize(),success:function(t){o.html(t)}}):i.hideAfterFormSubmit&&(t.submit(),$("#happyr-dialog").happyrDialog("hide"))})}})}};HappyrDialog.prototype={constructor:HappyrDialog,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var t=this,i=$.Event("show");this.$element.trigger(i),this.isShown||i.isDefaultPrevented()||(this.isShown=!0,this.escape(),this.backdrop(function(){t.$element.parent().length||t.$element.appendTo(document.body);var i=function(){t.$element.focus().trigger("shown"),$(document).trigger("happyr-dialog-shown")},o=function(){t.$element.show(),t.$element.attr("aria-hidden",!1),t.enforceFocus()};t.options.animate?(t.$element.css("opacity",0).css("top","-25%"),o(),t.$element.animate({top:"10%",opacity:"1"},t.options.animation.timeDialogShow,"swing",i)):(o(),i())}))},hide:function(t){t&&t.preventDefault();var i=this;if(t=$.Event("hide"),this.$element.trigger(t),this.isShown&&!t.isDefaultPrevented()){this.isShown=!1,this.escape(),$(document).off("focusin.happyr-dialog");var o=function(){i.$element.attr("aria-hidden",!0),i.hideDialog()};i.options.animate?i.$element.animate({top:"-25%",opacity:"0"},i.options.animation.timeDialogHide,"swing",o):o()}},enforceFocus:function(){var t=this;$(document).on("focusin.happyr-dialog",function(i){t.$element[0]===i.target||t.$element.has(i.target).length||t.$element.focus()})},escape:function(){var t=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.happyr-dialog",function(i){27==i.which&&t.hide()}):this.isShown||this.$element.off("keyup.dismiss.happyr-dialog")},hideDialog:function(){var t=this;this.$element.hide(),this.backdrop(function(){t.removeBackdrop(),t.$element.trigger("hidden")})},removeBackdrop:function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},backdrop:function(t){var i=this;this.isShown&&this.options.backdrop?(this.$backdrop=$('
').appendTo(document.body),this.$backdrop.click("static"==this.options.backdrop?$.proxy(this.$element[0].focus,this.$element[0]):$.proxy(this.hide,this)),this.options.animation?(this.$backdrop.css("opacity",0),this.$backdrop.show(),this.$backdrop.fadeTo(i.options.animation.timeBackdropShow,i.options.animation.opacityBackdrop,t)):(this.$backdrop.show(),t())):!this.isShown&&this.$backdrop?this.options.animation?this.$backdrop.fadeTo(i.options.animation.timeDialogHide,0,t):t():t&&t()}};var old=$.fn.happyrDialog;$.fn.happyrDialog=function(t){return this.each(function(){var i=$(this),o=i.data("happyrDialog"),a=$.extend(!0,$.fn.happyrDialog.defaults,i.data(),"object"==typeof t&&t);o&&o.isShown||i.data("happyrDialog",o=new HappyrDialog(this,a)),"string"==typeof t?o[t]():void 0!==t.action?o[t.action]():a.show&&o.show()})},$.fn.happyrDialog.defaults={animate:!0,animation:{timeBackdropShow:200,timeBackdropHide:200,timeDialogShow:300,timeDialogHide:300,opacityBackdrop:.8},backdrop:!0,keyboard:!0,show:!0,showHeader:!0,showFooter:!0,showCloseButton:!0,showConfirmButton:!0,showHeaderCloseButton:!0,showHeaderTitle:!0,submitFormOnConfirm:!0,texts:{close:"Close",confirm:"Confirm"},getFormResultInDialog:!0,hideAfterFormSubmit:!1},$.fn.happyrDialog.Constructor=HappyrDialog,$.fn.happyrDialog.noConflict=function(){return $.fn.happyrDialog=old,this},$(document).on("click.happyr-dialog.data-api",'[data-toggle="happyr-dialog"]',function(t){var i=$(this),o=i.attr("href"),a=$(i.attr("data-target")||o&&!o.match(/[?=\/&]/)&&o.replace(/.*(?=#[^\s]+$)/,"")),e=happyrDialog_convertStringToObject(i.attr("data-happyr-dialog-settings"));i.attr("data-happyr-dialog-title")&&(e.texts=$.extend(!0,e.texts,{title:i.attr("data-happyr-dialog-title")})),e=$.extend(!0,{remote:!/#/.test(o)&&o,action:"toggle"},a.data(),i.data(),e),t.preventDefault(),0===a.length&&(a=$("#happyr-dialog"),0===a.length&&(a=$('
').appendTo(document.body))),a.happyrDialog(e).one("hide",function(){i.focus()})})}(window.jQuery);<|endoftext|>


<commit_before>var http = require('http');
var rest_base = require('rest_base');
var hdhr = require('./lib/hd_wrapper');

rest_base.start_server();
hdhr.discover();
<commit_msg>added insertion of one local route<commit_after>var http = require('http');
var rest_base = require('rest_base');
var hdhr = require('./lib/hd_wrapper');

function setup_routes(app) {
    app.get('/pingy', function(request, response){
        response.status(200).send('pongy');
    });
};

rest_base.set_setup_routes_method(setup_routes);
rest_base.start_server();
hdhr.discover();

<|endoftext|>


<commit_before>import puppeteer from 'puppeteer';
import configuration from './configuration';
import { toBoolean } from './dataTypes';

export async function launch(
  options = {},
  env = configuration,
  puppeteerInstance = puppeteer,
) {
  if (typeof options.ignoreHTTPSErrors === 'undefined') {
    options.ignoreHTTPSErrors = toBoolean(env.PO_IGNORE_HTTPS_ERRORS);
  }

  if (typeof options.headless === 'undefined') {
    options.headless = toBoolean(env.PO_HEADLESS);
  }

  if (typeof options.handleSIGINT === 'undefined') {
    options.handleSIGINT = toBoolean(env.PO_HANDLE_SIGINT);
  }

  if (typeof options.dumpio === 'undefined') {
    options.dumpio = toBoolean(env.PO_DUMP_IO);
  }

  if (typeof options.devtools === 'undefined') {
    options.devtools = toBoolean(env.PO_DEV_TOOLS);
  }

  return await puppeteerInstance.launch(options);
}

export default {
  launch,
};
<commit_msg>no param assigning<commit_after>import puppeteer from 'puppeteer';
import configuration from './configuration';
import { toBoolean } from './dataTypes';

export async function launch(
  options = {},
  env = configuration,
  puppeteerInstance = puppeteer,
) {
  const opts = { ...options };

  if (typeof opts.ignoreHTTPSErrors === 'undefined') {
    opts.ignoreHTTPSErrors = toBoolean(env.PO_IGNORE_HTTPS_ERRORS);
  }

  if (typeof opts.headless === 'undefined') {
    opts.headless = toBoolean(env.PO_HEADLESS);
  }

  if (typeof opts.handleSIGINT === 'undefined') {
    opts.handleSIGINT = toBoolean(env.PO_HANDLE_SIGINT);
  }

  if (typeof opts.dumpio === 'undefined') {
    opts.dumpio = toBoolean(env.PO_DUMP_IO);
  }

  if (typeof opts.devtools === 'undefined') {
    opts.devtools = toBoolean(env.PO_DEV_TOOLS);
  }

  return await puppeteerInstance.launch(opts);
}

export default {
  launch,
};
<|endoftext|>


<commit_before>var serverNumber = parseInt(process.argv[2]),
    port = [1337,1338,1339][serverNumber];
// Downgrade from root
if (process.getuid() === 0)
    require('fs').stat(__filename, function(err, stats) {
	    if (err)
		return console.log(err);
	    process.setuid(stats.uid);
	});
console.log("Started physics server "+serverNumber);
var io = require('socket.io').listen(port),
    redis = require('redis').createClient(),
    GameLogic = require('./public/javascripts/game_logic.js').GameLogic,
    GameDef = require('./public/javascripts/game_def.js').GameDef;

redis.on("error", function (err) {
	console.log("Redis connection error to " + redis.host + ":" + redis.port + " - " + err);
    });

var games = {},
    gameBySocketId = {},
    redisKeys = {},
    gamesPerProcess = 4;

io.sockets.on('connection',function(socket){
	socket.on('subscribe',function(type,gameId,redisKey){
		var room = games[gameId];
		if(typeof room === 'undefined'){
		    socket.emit('error',"Bad room id");
		}
		if(room)
		    if(room.join(socket,type)){
			gameBySocketId[socket.id] = gameId;
			redisKeys[socket.id] = redisKey;
			socket.emit('state',room.physics.getState());
		    }else{
			socket.emit('error',"This game room is full");
		    }
	    });
	socket.on('disconnect',function(){
		var gameId, game;
		if(gameId = gameBySocketId[socket.id]){
		    if(game=games[gameId]){
			game.remove(socket.id);
		    }
		    delete gameBySocketId[socket.id];
		}
		delete redisKeys[socket.id];
	    });
    });
Game = function(id){
    this.gameId = id;
    //Initialize a new physics object here
    this.physics = new GameLogic(GameDef);
    this.c0 = null;
    this.c1 = null;
    this.running = false;
    this.viewers = 0;
    this.controllers = 0;
    this.all_sockets = id+'-all';
    this.controller_sockets = id+'-control';
};
Game.prototype.join = function(socket,type){
    if(type == 'viewer'){
	//Probably can manage 4 viewers and 4 controllers?
	if(this.viewers < 4){
	    socket.join(this.all_sockets);
	    this.viewers++;
	    return true;
	}
    }else if(type == 'controller'){
	if(this.controllers < 4){
	    socket.join(this.all_sockets);
	    socket.join(this.controller_sockets);
	    this.controllers++;
	    return true;
	}
    }
    return false;
};
Game.prototype.empty = function(){
    return (this.viewers == 0 && this.controllers == 0);
};
Game.prototype.remove = function(id){
    var controller_room = io.sockets.manager.rooms['/'+this.controller_sockets] || [];
    if(-1 != controller_room.indexOf(id+'')){
	// If this is c1 or c0, the other one wins.
	if(id == this.c0){
	    this.c0 = null;
	    this.end(1);
	}else if(id == this.c1){
	    this.c1 = null;
	    this.end(0);
	}
	this.controllers--;
    }else
	this.viewers--;
};
Game.prototype.end = function(winner){
    // for both controller sockets (if they exist) start ignoring the paddle data
    this.running = false;
    if(this.c0) io.sockets.socket(this.c0).removeAllListeners('paddle');
    if(this.c1) io.sockets.socket(this.c1).removeAllListeners('paddle');
    winner = [this.c0,this.c1][winner];
    (function(game){
	redis.get(redisKeys[winner],function(err,data){
		var fail = {};
		io.sockets.in(game.all_sockets).emit('win',data || fail);
	    });
    })(this);
};
Game.prototype.begin = function(){
    var controllers = io.sockets.manager.rooms['/'+this.controller_sockets];
    (function(game){
	chooseControllers(controllers,function(a,b){
		game.c0 = a;
		game.c1 = b;
		console.log(a);
		console.log(b);
		io.sockets.socket(game.c0).emit('nominate',0);
		io.sockets.socket(game.c1).emit('nominate',1);
		io.sockets.in(game.all_sockets).emit('time',15);
		setTimeout(function(){
			if(game.running){
			    game.physics.reset();
			    io.sockets.in(game.all_sockets).emit('start','new game state');
			    io.sockets.socket(game.c0).on('controller',function(dat){
				    game.physics.setTouchPoints(0,dat);
				});
			    io.sockets.socket(game.c1).on('controller',function(dat){
				    game.physics.setTouchPoints(1,dat);
				});
			    setupPhysics(game);
			}
		    },15*1000);
	    });
    })(this);
    this.running = true;
};
Game.prototype.redisRecord = function(){
    return {name:this.gameId,viewers:this.viewers,controllers:this.controllers,port:port};
};

for(var i = 0;i<gamesPerProcess;i++){
    games[i] = new Game(i+gamesPerProcess*serverNumber);
}

// Takes an array of possible controller ids and chooses two. Can map controller id to redis keys.
function chooseControllers(set,cont){
    // It turns out that writing the comparison predicate for the data is just a pain in the ass. Randomly schedule, for now.
    /*  var keys = set.map(function(i){return redisKeys[i];});
    redis.mget(keys,function(err,res){
	    if(err){
		console.log("Redis error, with keys :"+keys);
		cont(set[0],set[1]);//Just choose the first two...
	    }else{
		res = res.map(function(obj,index){return {obj:obj,ctrl:set[index]};});
		res.sort(function(a,b){
			//Some predicate for the epicness.

			return Math.random()-0.5;
		    });
		cont(res[0].ctrl,res[1].ctrl);
	    }
	});
    */
    set.sort(function(a,b){return Math.random()-0.5;});
    cont(set[0],set[1]);
}
// Sets up the game's physics loops
function setupPhysics(game){
    var loop = function(){
	// Needs some logic to terminate, however
	if(game.physics.over())
	    game.end();
	if(game.running){
	    // Update the physics here
	    var state = game.physics.step(1/300);
	    io.sockets.in(game.all_sockets).volatile.emit('update',state);
	    setTimeout(loop,1/30)
	}
    };
    loop();
}
// Update the game room data on redis every n seconds.
setInterval(function(){
	var rooms = [];
	for(var i in games){
	    rooms.push(games[i].redisRecord());
	}
	redis.mset('server-'+serverNumber,JSON.stringify(rooms),function(err,_){
		if(err)
		    console.log("Redis-error:"+err);
	    });
    },5000);
//This is the server's real main-loop. Checks if games can be started, and then starts them
setInterval(function(){
	for(var i in games){
	    i = games[i];
	    if(!i.running && i.controllers > 1)
		i.begin();
	}
    },250);

<commit_msg>Changed over to a nicer room stats format<commit_after>var serverNumber = parseInt(process.argv[2]),
    port = [1337,1338,1339][serverNumber];
// Downgrade from root
if (process.getuid() === 0)
    require('fs').stat(__filename, function(err, stats) {
	    if (err)
		return console.log(err);
	    process.setuid(stats.uid);
	});
console.log("Started physics server "+serverNumber);
var io = require('socket.io').listen(port),
    redis = require('redis').createClient(),
    GameLogic = require('./public/javascripts/game_logic.js').GameLogic,
    GameDef = require('./public/javascripts/game_def.js').GameDef;

redis.on("error", function (err) {
	console.log("Redis connection error to " + redis.host + ":" + redis.port + " - " + err);
    });

var games = {},
    gameBySocketId = {},
    redisKeys = {},
    gamesPerProcess = 4;

io.sockets.on('connection',function(socket){
	socket.on('subscribe',function(type,gameId,redisKey){
		var room = games[gameId];
		if(typeof room === 'undefined'){
		    socket.emit('error',"Bad room id");
		}
		if(room)
		    if(room.join(socket,type)){
			gameBySocketId[socket.id] = gameId;
			redisKeys[socket.id] = redisKey;
			socket.emit('state',room.physics.getState());
		    }else{
			socket.emit('error',"This game room is full");
		    }
	    });
	socket.on('disconnect',function(){
		var gameId, game;
		if(gameId = gameBySocketId[socket.id]){
		    if(game=games[gameId]){
			game.remove(socket.id);
		    }
		    delete gameBySocketId[socket.id];
		}
		delete redisKeys[socket.id];
	    });
    });
Game = function(id){
    this.gameId = id;
    //Initialize a new physics object here
    this.physics = new GameLogic(GameDef);
    this.c0 = null;
    this.c1 = null;
    this.running = false;
    this.viewers = 0;
    this.controllers = 0;
    this.all_sockets = id+'-all';
    this.controller_sockets = id+'-control';
};
Game.prototype.join = function(socket,type){
    if(type == 'viewer'){
	//Probably can manage 4 viewers and 4 controllers?
	if(this.viewers < 4){
	    socket.join(this.all_sockets);
	    this.viewers++;
	    return true;
	}
    }else if(type == 'controller'){
	if(this.controllers < 4){
	    socket.join(this.all_sockets);
	    socket.join(this.controller_sockets);
	    this.controllers++;
	    return true;
	}
    }
    return false;
};
Game.prototype.empty = function(){
    return (this.viewers == 0 && this.controllers == 0);
};
Game.prototype.remove = function(id){
    var controller_room = io.sockets.manager.rooms['/'+this.controller_sockets] || [];
    if(-1 != controller_room.indexOf(id+'')){
	// If this is c1 or c0, the other one wins.
	if(id == this.c0){
	    this.c0 = null;
	    this.end(1);
	}else if(id == this.c1){
	    this.c1 = null;
	    this.end(0);
	}
	this.controllers--;
    }else
	this.viewers--;
};
Game.prototype.end = function(winner){
    // for both controller sockets (if they exist) start ignoring the paddle data
    this.running = false;
    if(this.c0) io.sockets.socket(this.c0).removeAllListeners('paddle');
    if(this.c1) io.sockets.socket(this.c1).removeAllListeners('paddle');
    winner = [this.c0,this.c1][winner];
    (function(game){
	redis.get(redisKeys[winner],function(err,data){
		var fail = {};
		io.sockets.in(game.all_sockets).emit('win',data || fail);
	    });
    })(this);
};
Game.prototype.begin = function(){
    var controllers = io.sockets.manager.rooms['/'+this.controller_sockets];
    (function(game){
	chooseControllers(controllers,function(a,b){
		game.c0 = a;
		game.c1 = b;
		console.log(a);
		console.log(b);
		io.sockets.socket(game.c0).emit('nominate',0);
		io.sockets.socket(game.c1).emit('nominate',1);
		io.sockets.in(game.all_sockets).emit('time',15);
		setTimeout(function(){
			if(game.running){
			    game.physics.reset();
			    io.sockets.in(game.all_sockets).emit('start','new game state');
			    io.sockets.socket(game.c0).on('controller',function(dat){
				    game.physics.setTouchPoints(0,dat);
				});
			    io.sockets.socket(game.c1).on('controller',function(dat){
				    game.physics.setTouchPoints(1,dat);
				});
			    setupPhysics(game);
			}
		    },15*1000);
	    });
    })(this);
    this.running = true;
};
Game.prototype.redisRecord = function(){
    return {name:this.gameId,viewers:this.viewers,controllers:this.controllers,port:port};
};

for(var i = 0;i<gamesPerProcess;i++){
    games[i] = new Game(i+gamesPerProcess*serverNumber);
}

// Takes an array of possible controller ids and chooses two. Can map controller id to redis keys.
function chooseControllers(set,cont){
    // It turns out that writing the comparison predicate for the data is just a pain in the ass. Randomly schedule, for now.
    /*  var keys = set.map(function(i){return redisKeys[i];});
    redis.mget(keys,function(err,res){
	    if(err){
		console.log("Redis error, with keys :"+keys);
		cont(set[0],set[1]);//Just choose the first two...
	    }else{
		res = res.map(function(obj,index){return {obj:obj,ctrl:set[index]};});
		res.sort(function(a,b){
			//Some predicate for the epicness.

			return Math.random()-0.5;
		    });
		cont(res[0].ctrl,res[1].ctrl);
	    }
	});
    */
    set.sort(function(a,b){return Math.random()-0.5;});
    cont(set[0],set[1]);
}
// Sets up the game's physics loops
function setupPhysics(game){
    var loop = function(){
	// Needs some logic to terminate, however
	if(game.physics.over())
	    game.end();
	if(game.running){
	    // Update the physics here
	    var state = game.physics.step(1/300);
	    io.sockets.in(game.all_sockets).volatile.emit('update',state);
	    setTimeout(loop,1/30)
	}
    };
    loop();
}
// Update the game room data on redis every n seconds.
setInterval(function(){
	var rooms = {};
	for(var i in games){
	    i = games[i];
	    rooms[i.gameId] = i.redisRecord();
	}
	console.log(rooms);
	redis.hmset('rooms',rooms,function(err,_){
		if(err)
		    console.log('Redis-error:'+err);
	    });
    },5000);

//This is the server's real main-loop. Checks if games can be started, and then starts them
setInterval(function(){
	for(var i in games){
	    i = games[i];
	    if(!i.running && i.controllers > 1)
		i.begin();
	}
    },250);<|endoftext|>


<commit_before>/**
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var path = require('path');
var fs = require('fs');
var deasync = require('deasync');
var removeRecursive = require('rimraf').sync;
var copyRecursive = deasync(require('ncp').ncp);

class FileSystem {
    constructor() {
        this.separator = path.sep;
    }

    /**
     * @return {string}
     */
    get workingDirectory() {
        return process.cwd();
    }

    /**
     * @param {string} directoryPath
     */
    changeWorkingDirectory(directoryPath) {
        try {
            process.chdir(directoryPath);
            return true;
        } catch (e){
            return false;
        }
    }

    /**
     * @param {string} relativePath
     * @return {string}
     */
    absolute(relativePath) {
        relativePath = path.normalize(relativePath);
        if (path.isAbsolute(relativePath))
            return relativePath;
        return path.resolve(path.join(process.cwd(), relativePath));
    }

    /**
     * @param {string} filePath
     * @return {boolean}
     */
    exists(filePath) {
        return fs.existsSync(filePath);
    }

    /**
     * @param {string} fromPath
     * @param {string} toPath
     */
    copy(fromPath, toPath) {
        var content = fs.readFileSync(fromPath);
        fs.writeFileSync(toPath, content);
    }

    /**
     * @param {string} fromPath
     * @param {string} toPath
     */
    move(fromPath, toPath) {
        var content = fs.readFileSync(fromPath);
        fs.writeFileSync(toPath, content);
        fs.unlinkSync(fromPath);
    }

    /**
     * @param {string} filePath
     * @return {number}
     */
    size(filePath) {
        return fs.statSync(filePath).size;
    }

    /**
     * @param {string} filePath
     */
    touch(filePath) {
        fs.closeSync(fs.openSync(filePath, 'a'));
    }

    /**
     * @param {string} filePath
     */
    remove(filePath) {
        fs.unlinkSync(filePath);
    }

    /**
     * @param {string} filePath
     */
    lastModified(filePath) {
        return fs.statSync(filePath).mtime;
    }

    /**
     * @param {string} dirPath
     */
    makeDirectory(dirPath) {
        try {
            fs.mkdirSync(dirPath);
            return true;
        } catch (e) {
            return false;
        }
    }

    /**
     * @param {string} dirPath
     */
    removeTree(dirPath) {
        removeRecursive(dirPath);
    }

    /**
     * @param {string} fromPath
     * @param {string} toPath
     */
    copyTree(fromPath, toPath) {
        copyRecursive(fromPath, toPath);
    }

    /**
     * @param {string} dirPath
     * @return {!Array<string>}
     */
    list(dirPath) {
        return fs.readdirSync(dirPath);
    }

    /**
     * @param {string} linkPath
     * @return {string}
     */
    readLink(linkPath) {
        return fs.readlinkSync(linkPath);
    }

    /**
     * @param {string} filePath
     * @param {Object} data
     * @param {string} mode
     */
    write(filePath, data, mode) {
        var fd = new FileDescriptor(filePath, mode, 'utf8');
        fd.write(data);
        fd.close();
    }

    /**
     * @param {string} somePath
     * @return {boolean}
     */
    isAbsolute(somePath) {
        return path.isAbsolute(somePath);
    }

    /**
     * @return {string}
     */
    read(filePath) {
        return fs.readFileSync(filePath, 'utf8');
    }

    /**
     * @param {string} filePath
     * @return {boolean}
     */
    isFile(filePath) {
        return fs.existsSync(filePath) && fs.lstatSync(filePath).isFile();
    }

    /**
     * @param {string} dirPath
     * @return {boolean}
     */
    isDirectory(dirPath) {
        return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
    }

    /**
     * @param {string} filePath
     * @return {boolean}
     */
    isLink(filePath) {
        return fs.existsSync(filePath) && fs.lstatSync(filePath).isSymbolicLink();
    }

    /**
     * @param {string} filePath
     * @return {boolean}
     */
    isReadable(filePath) {
        try {
            fs.accessSync(filePath, fs.constants.R_OK);
            return true;
        } catch (e) {
            return false;
        }
    }

    /**
     * @param {string} filePath
     * @return {boolean}
     */
    isWritable(filePath) {
        try {
            fs.accessSync(filePath, fs.constants.W_OK);
            return true;
        } catch (e) {
            return false;
        }
    }

    /**
     * @param {string} filePath
     * @return {boolean}
     */
    isExecutable(filePath) {
        try {
            fs.accessSync(filePath, fs.constants.X_OK);
            return true;
        } catch (e) {
            return false;
        }
    }

    /**
     * @param {string} somePath
     * @return {!Array<string>}
     */
    split(somePath) {
        somePath = path.normalize(somePath);
        if (somePath.endsWith(path.sep))
            somePath = somePath.substring(0, somePath.length - path.sep.length);
        return somePath.split(path.sep);
    }

    /**
     * @param {string} path1
     * @param {string} path2
     * @return {string}
     */
    join(...args) {
        if (args[0] === '' && args.length > 1)
            args[0] = path.sep;
        args = args.filter(part => typeof part === 'string');
        return path.join.apply(path, args);
    }

    /**
     * @param {string} filePath
     * @param {(string|!Object)} option
     * @return {!FileDescriptor}
     */
    open(filePath, option) {
        if (typeof option === 'string')
            return new FileDescriptor(filePath, option);
        return new FileDescriptor(filePath, option.mode);
    }
}

var fdwrite = deasync(fs.write);
var fdread = deasync(fs.read);

class FileDescriptor {
    /**
     * @param {string} filePath
     * @param {string} mode
     */
    constructor(filePath, mode) {
        this._position = 0;
        this._encoding = 'utf8';
        if (mode === 'rb') {
            this._mode = 'r';
            this._encoding = 'latin1';
        } else if (mode === 'wb' || mode === 'b') {
            this._mode = 'w';
            this._encoding = 'latin1';
        } else if (mode === 'rw+') {
            this._mode = 'a+';
            this._position = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
        } else {
            this._mode = mode;
        }
        this._fd = fs.openSync(filePath, this._mode);
    }

    /**
     * @param {string} data
     */
    write(data) {
        var buffer = Buffer.from(data, this._encoding);
        var written = fdwrite(this._fd, buffer, 0, buffer.length, this._position);
        this._position += written;
    }

    getEncoding() {
        return 'UTF-8';
    }

    /**
     * @param {string} data
     */
    writeLine(data) {
        this.write(data + '\n');
    }

    /**
     * @param {number=} size
     * @return {string}
     */
    read(size) {
        var position = this._position;
        if (!size) {
            size = fs.fstatSync(this._fd).size;
            position = 0;
        }
        var buffer = new Buffer(size);
        var bytesRead = fdread(this._fd, buffer, 0, size, position);
        this._position += bytesRead;
        return buffer.toString(this._encoding);
    }

    flush() {
        // noop.
    }

    /**
     * @param {number} position
     */
    seek(position) {
        this._position = position;
    }

    close() {
        fs.closeSync(this._fd);
    }

    /**
     * @return {boolean}
     */
    atEnd() {
    }
}

module.exports = FileSystem;
<commit_msg>Implement PhantomJS's fs.makeTree method<commit_after>/**
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var path = require('path');
var fs = require('fs');
var deasync = require('deasync');
var removeRecursive = require('rimraf').sync;
var copyRecursive = deasync(require('ncp').ncp);

class FileSystem {
    constructor() {
        this.separator = path.sep;
    }

    /**
     * @return {string}
     */
    get workingDirectory() {
        return process.cwd();
    }

    /**
     * @param {string} directoryPath
     */
    changeWorkingDirectory(directoryPath) {
        try {
            process.chdir(directoryPath);
            return true;
        } catch (e){
            return false;
        }
    }

    /**
     * @param {string} relativePath
     * @return {string}
     */
    absolute(relativePath) {
        relativePath = path.normalize(relativePath);
        if (path.isAbsolute(relativePath))
            return relativePath;
        return path.resolve(path.join(process.cwd(), relativePath));
    }

    /**
     * @param {string} filePath
     * @return {boolean}
     */
    exists(filePath) {
        return fs.existsSync(filePath);
    }

    /**
     * @param {string} fromPath
     * @param {string} toPath
     */
    copy(fromPath, toPath) {
        var content = fs.readFileSync(fromPath);
        fs.writeFileSync(toPath, content);
    }

    /**
     * @param {string} fromPath
     * @param {string} toPath
     */
    move(fromPath, toPath) {
        var content = fs.readFileSync(fromPath);
        fs.writeFileSync(toPath, content);
        fs.unlinkSync(fromPath);
    }

    /**
     * @param {string} filePath
     * @return {number}
     */
    size(filePath) {
        return fs.statSync(filePath).size;
    }

    /**
     * @param {string} filePath
     */
    touch(filePath) {
        fs.closeSync(fs.openSync(filePath, 'a'));
    }

    /**
     * @param {string} filePath
     */
    remove(filePath) {
        fs.unlinkSync(filePath);
    }

    /**
     * @param {string} filePath
     * @return {boolean}
     */
    lastModified(filePath) {
        return fs.statSync(filePath).mtime;
    }

    /**
     * @param {string} dirPath
     * @return {boolean}
     */
    makeDirectory(dirPath) {
        try {
            fs.mkdirSync(dirPath);
            return true;
        } catch (e) {
            return false;
        }
    }

    /**
     * @param {string} dirPath
     * @return {boolean}
     */
    makeTree(dirPath) {
        return this.makeDirectory(dirPath);
    }

    /**
     * @param {string} dirPath
     */
    removeTree(dirPath) {
        removeRecursive(dirPath);
    }

    /**
     * @param {string} fromPath
     * @param {string} toPath
     */
    copyTree(fromPath, toPath) {
        copyRecursive(fromPath, toPath);
    }

    /**
     * @param {string} dirPath
     * @return {!Array<string>}
     */
    list(dirPath) {
        return fs.readdirSync(dirPath);
    }

    /**
     * @param {string} linkPath
     * @return {string}
     */
    readLink(linkPath) {
        return fs.readlinkSync(linkPath);
    }

    /**
     * @param {string} filePath
     * @param {Object} data
     * @param {string} mode
     */
    write(filePath, data, mode) {
        var fd = new FileDescriptor(filePath, mode, 'utf8');
        fd.write(data);
        fd.close();
    }

    /**
     * @param {string} somePath
     * @return {boolean}
     */
    isAbsolute(somePath) {
        return path.isAbsolute(somePath);
    }

    /**
     * @return {string}
     */
    read(filePath) {
        return fs.readFileSync(filePath, 'utf8');
    }

    /**
     * @param {string} filePath
     * @return {boolean}
     */
    isFile(filePath) {
        return fs.existsSync(filePath) && fs.lstatSync(filePath).isFile();
    }

    /**
     * @param {string} dirPath
     * @return {boolean}
     */
    isDirectory(dirPath) {
        return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
    }

    /**
     * @param {string} filePath
     * @return {boolean}
     */
    isLink(filePath) {
        return fs.existsSync(filePath) && fs.lstatSync(filePath).isSymbolicLink();
    }

    /**
     * @param {string} filePath
     * @return {boolean}
     */
    isReadable(filePath) {
        try {
            fs.accessSync(filePath, fs.constants.R_OK);
            return true;
        } catch (e) {
            return false;
        }
    }

    /**
     * @param {string} filePath
     * @return {boolean}
     */
    isWritable(filePath) {
        try {
            fs.accessSync(filePath, fs.constants.W_OK);
            return true;
        } catch (e) {
            return false;
        }
    }

    /**
     * @param {string} filePath
     * @return {boolean}
     */
    isExecutable(filePath) {
        try {
            fs.accessSync(filePath, fs.constants.X_OK);
            return true;
        } catch (e) {
            return false;
        }
    }

    /**
     * @param {string} somePath
     * @return {!Array<string>}
     */
    split(somePath) {
        somePath = path.normalize(somePath);
        if (somePath.endsWith(path.sep))
            somePath = somePath.substring(0, somePath.length - path.sep.length);
        return somePath.split(path.sep);
    }

    /**
     * @param {string} path1
     * @param {string} path2
     * @return {string}
     */
    join(...args) {
        if (args[0] === '' && args.length > 1)
            args[0] = path.sep;
        args = args.filter(part => typeof part === 'string');
        return path.join.apply(path, args);
    }

    /**
     * @param {string} filePath
     * @param {(string|!Object)} option
     * @return {!FileDescriptor}
     */
    open(filePath, option) {
        if (typeof option === 'string')
            return new FileDescriptor(filePath, option);
        return new FileDescriptor(filePath, option.mode);
    }
}

var fdwrite = deasync(fs.write);
var fdread = deasync(fs.read);

class FileDescriptor {
    /**
     * @param {string} filePath
     * @param {string} mode
     */
    constructor(filePath, mode) {
        this._position = 0;
        this._encoding = 'utf8';
        if (mode === 'rb') {
            this._mode = 'r';
            this._encoding = 'latin1';
        } else if (mode === 'wb' || mode === 'b') {
            this._mode = 'w';
            this._encoding = 'latin1';
        } else if (mode === 'rw+') {
            this._mode = 'a+';
            this._position = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0;
        } else {
            this._mode = mode;
        }
        this._fd = fs.openSync(filePath, this._mode);
    }

    /**
     * @param {string} data
     */
    write(data) {
        var buffer = Buffer.from(data, this._encoding);
        var written = fdwrite(this._fd, buffer, 0, buffer.length, this._position);
        this._position += written;
    }

    getEncoding() {
        return 'UTF-8';
    }

    /**
     * @param {string} data
     */
    writeLine(data) {
        this.write(data + '\n');
    }

    /**
     * @param {number=} size
     * @return {string}
     */
    read(size) {
        var position = this._position;
        if (!size) {
            size = fs.fstatSync(this._fd).size;
            position = 0;
        }
        var buffer = new Buffer(size);
        var bytesRead = fdread(this._fd, buffer, 0, size, position);
        this._position += bytesRead;
        return buffer.toString(this._encoding);
    }

    flush() {
        // noop.
    }

    /**
     * @param {number} position
     */
    seek(position) {
        this._position = position;
    }

    close() {
        fs.closeSync(this._fd);
    }

    /**
     * @return {boolean}
     */
    atEnd() {
    }
}

module.exports = FileSystem;
<|endoftext|>


<commit_before>// Native requires.
var fs = require('fs');

// package.json requires.
var mysql = require('mysql');
var _ = require('lodash');

// Local requires.
var Response = require('./../tools/response');
var common = require('./../tools/common');
var mysqlconf = require('./../conf/mysql.json');

// Removes peers from database whose last update is more than 1 hour.
Database.prototype.flushPeers = function() {
    console.log('Flushing peers');
    // Delete all peers whose last update is more than 1 hour ago.
    this.connection.query('SELECT `id`, `peer_id`, `info_hash`, `left` '
        + 'FROM `nodetracker`.`peers` '
        + 'WHERE `last_update` < DATE_SUB(NOW(), INTERVAL 1 HOUR)'
        , function(err, rows, fields) {
            rows.forEach(function(item) {
                var field = '';
                // If peer had 0 bytes left to download, it was a seeder.
                if (item.left == 0) {
                    field = '`complete`' ;
                } else {
                    field = '`incomplete`';
                }
                this.connection.query('UPDATE `nodetracker`.`torrent` '
                    + 'SET ' + field + ' = ' + field + ' - 1 '
                    + 'WHERE `info_hash` = ' + this.connection.escape(item.info_hash) + ' '
                    + 'AND ' + field + ' > 0'
                    , function(err, result) {
                        console.log('Decremented ' + field + ' on ' + item.info_hash);
                });
                this.connection.query('DELETE FROM `nodetracker`.`peers` '
                    + 'WHERE `id` = ' + this.connection.escape(item.id)
                    , function(err, result) {
                        console.log('Removed peer ' + item.peer_id + ' for torrent: ' + item.info_hash);
                });
            }.bind(this));
    }.bind(this));
};

Database.prototype.checkTorrent = function(peer, callback) {
    this.connection.query('SELECT * FROM `nodetracker`.`torrent` '
        + 'WHERE `info_hash` = ' + this.connection.escape(peer.info_hash)
        , function(err, rows, fields) {
            if (err) {
                callback(err);
            } else if (rows.length == 0) {
                callback(new Error('Torrent not found'));
            } else {
                callback(undefined);
            }
    }.bind(this));
};

// Adds peer to database or returns error if the torrent is not tracked.
// NOTE: failure codes are optional, but included anyway. Can be removed
// for bandwidth saving.
Database.prototype.addPeer = function(peer, callback) {
    this.checkTorrent(peer, function(err) {
        if (err) {
            callback(undefined, common.bencodeFailure(200, 'Torrent not in database.'));
        } else  {
            this.transaction(peer, callback);
        }
    }.bind(this));
};

// Transaction to add a peer.
Database.prototype.transaction = function(peer, callback) {
    this.connection.beginTransaction(function(err) {
        this.connection.query('REPLACE '
            + 'INTO `nodetracker`.`peers` (`id`, `peer_id`, `info_hash`, `ip`, `port`, `uploaded`, `downloaded`, `left`) '
            + 'VALUES (' + peer.toAddPeerString() + ')'
            , function(err, result) {
                if (err) {
                    this.connection.rollback(function() {
                        callback(err, undefined);
                    });
                } else {
                    // TODO: lol fix that it doesn't update every cycle.
                    var field = '';
                    if (peer._left == 0) {
                        field = '`complete`';
                    } else {
                        field = '`incomplete`';
                    }
                    this.connection.query('UPDATE `nodetracker`.`torrent` '
                        + 'SET ' + field + ' = ' + field + ' + 1 '
                        + 'WHERE `info_hash` = ' + this.connection.escape(peer.info_hash)
                        , function(err, result) {
                            if (err) {
                                this.connection.rollback(function() {
                                    callback(err, undefined);
                                });
                            } else {
                                this.connection.commit(function(err) {
                                    if (err) {
                                        this.connection.rollback(function() {
                                            callback(err, undefined);
                                        })
                                    } else {
                                        this.getPeers(peer, callback);
                                    }
                                }.bind(this));
                            }
                    }.bind(this));
                }
        }.bind(this));
    }.bind(this));
};

// Removes peer from database.
Database.prototype.removePeer = function(peer, callback) {
    this.connection.query('SELECT `id`, `peer_id`, `info_hash`, `left` '
        + 'FROM `nodetracker`.`peers` '
        + 'WHERE `id` = ' + this.connection.escape(peer.peer_id + peer.info_hash)
        , function(err, rows, fields) {
            if (err) {
                callback(err, undefined);
            } else {
                // TODO: Convert this to a function. (Also used by flushPeers)
                rows.forEach(function(item) {
                    var field = '';
                    if (item.left == 0) {
                        field = '`complete`' ;
                    } else {
                        field = '`incomplete`';
                    }
                    this.connection.query('UPDATE `nodetracker`.`torrent` '
                        + 'SET ' + field + ' = ' + field + ' - 1 '
                        + 'WHERE `info_hash` = ' + this.connection.escape(item.info_hash) + ' '
                        + 'AND ' + field + ' > 0'
                        , function(err, result) {
                            console.log('Decremented ' + field + ' on ' + item.info_hash);
                    });
                    this.connection.query('DELETE FROM `nodetracker`.`peers` '
                        + 'WHERE `id` = ' + this.connection.escape(item.id)
                        , function(err, result) {
                            console.log('Removed peer ' + item.peer_id + ' for torrent: ' + item.info_hash);
                    });
                    this.getPeers(peer, callback);
                }.bind(this));
            }
    }.bind(this));
};

// Increments the completed count for torrent.
// TODO: Clients can spam complete count and it will keep increasing,
// should add check to prevent clients with left=0 from sending complete
// (or just add completed boolean)
Database.prototype.completePeer = function(peer, callback) {
    this.connection.query('UPDATE `nodetracker`.`torrent` '
        + 'SET `downloaded` = `downloaded` + 1, '
        + '`incomplete` = `incomplete` - 1, '
        + '`complete` = `complete` + 1 '
        + 'WHERE `info_hash` = ' + this.connection.escape(peer.info_hash)
        , function(err, result) {
            if (err) {
                callback(err, undefined);
            } else {
                this.updatePeer(peer, callback);
            }
    }.bind(this));
};

// Updates the peer in the database.
// TODO: uTorrent doesn't send "started" when it resumes from paused/stopped state
// only when it restarts. Peer could have been deleted from database when it resumes.
Database.prototype.updatePeer = function(peer, callback) {
    this.connection.query('UPDATE `nodetracker`.`peers` '
        + 'SET `uploaded` = ' + this.connection.escape(peer.uploaded) + ', '
        + '`downloaded` = ' + this.connection.escape(peer.downloaded) + ', '
        + '`left` = ' + this.connection.escape(peer._left) + ' '
        + 'WHERE `id` = ' + this.connection.escape(peer.peer_id + peer.info_hash)
        , function(err, result) {
            if (err) {
                callback(err, undefined);
            } else {
                this.getPeers(peer, callback);
            }

    }.bind(this));
};

// Returns a randomly ordered set of peers to the client.
// NOTE: Tracker supports compact and non-compact, however non-compact
// requires more bandwidth.
Database.prototype.getPeers = function(peer, callback) {
    var colums = '`ip`, `port` ';
    if (peer._compact === 0) {
        colums = '`peer_id`, ' + colums;
    }

    // TODO: Maybe create a smarter algorithm?
    // Return a certain high download : low download distribution.
    this.connection.query('SELECT ' + colums
        + 'FROM `nodetracker`.`peers` '
        + 'WHERE `info_hash` = ' + this.connection.escape(peer.info_hash) + ' '
        + 'AND `peer_id` != ' + this.connection.escape(peer.peer_id) + ' '
        + 'ORDER BY RAND() '
        + 'LIMIT ' + this.connection.escape(peer.numwant)
        , function(err, rows, result) {
            if (err) {
                callback(err, undefined);
                return;
            }
            this.scrape(peer.info_hash, function(err, response) {
                if (err) {
                    callback(err, undefined);
                } else {
                    // Add all peers to response!
                    rows.forEach(function(item) {
                        response.addPeer(item);
                    });
                    // Return hex IPv4 if client requested compact.
                    if (peer._compact) {
                        callback(undefined, response.bencodePeersIPv4Compact());
                    } else {
                        callback(undefined, response.bencodePeersIPv4());
                    }
                }
            }.bind(this));
    }.bind(this));
};

Database.prototype.scrape = function(info_hash, callback) {
    if (Array.isArray(info_hash)) {
        info_hash = _.map(info_hash, function(item) {
            return this.connection.escape(item);
        }.bind(this))
        info_hash = info_hash.join(',');
    } else {
        info_hash = this.connection.escape(info_hash);
    }

    this.connection.query('SELECT `info_hash`, `complete`, `incomplete`, `downloaded` '
        + 'FROM `nodetracker`.`torrent` '
        + 'WHERE `info_hash` IN (' + info_hash + ')'
        , function(err, rows, fields) {
            if (err) {
                callback(err, undefined);
                return;
            }
            var response = new Response();
            rows.forEach(function(item) {
                response.addScrape(item);
            });
            callback(undefined, response);
    });
};

function Database() {
    this.connection = mysql.createConnection(mysqlconf);

    this.connection.connect();
    // Clear out expired peers.
    this.flushPeers();
    // Set interval to clear expired peers.
    setInterval(this.flushPeers.bind(this), 1000 * 60 * 4);
}

module.exports = Database;<commit_msg>Fixed peer removal return.<commit_after>// Native requires.
var fs = require('fs');

// package.json requires.
var mysql = require('mysql');
var _ = require('lodash');

// Local requires.
var Response = require('./../tools/response');
var common = require('./../tools/common');
var mysqlconf = require('./../conf/mysql.json');

// Removes peers from database whose last update is more than 1 hour.
Database.prototype.flushPeers = function() {
    console.log('Flushing peers');
    // Delete all peers whose last update is more than 1 hour ago.
    this.connection.query('SELECT `id`, `peer_id`, `info_hash`, `left` '
        + 'FROM `nodetracker`.`peers` '
        + 'WHERE `last_update` < DATE_SUB(NOW(), INTERVAL 1 HOUR)'
        , function(err, rows, fields) {
            rows.forEach(function(item) {
                var field = '';
                // If peer had 0 bytes left to download, it was a seeder.
                if (item.left == 0) {
                    field = '`complete`' ;
                } else {
                    field = '`incomplete`';
                }
                this.connection.query('UPDATE `nodetracker`.`torrent` '
                    + 'SET ' + field + ' = ' + field + ' - 1 '
                    + 'WHERE `info_hash` = ' + this.connection.escape(item.info_hash) + ' '
                    + 'AND ' + field + ' > 0'
                    , function(err, result) {
                        console.log('Decremented ' + field + ' on ' + item.info_hash);
                });
                this.connection.query('DELETE FROM `nodetracker`.`peers` '
                    + 'WHERE `id` = ' + this.connection.escape(item.id)
                    , function(err, result) {
                        console.log('Removed peer ' + item.peer_id + ' for torrent: ' + item.info_hash);
                });
            }.bind(this));
    }.bind(this));
};

Database.prototype.checkTorrent = function(peer, callback) {
    this.connection.query('SELECT * FROM `nodetracker`.`torrent` '
        + 'WHERE `info_hash` = ' + this.connection.escape(peer.info_hash)
        , function(err, rows, fields) {
            if (err) {
                callback(err);
            } else if (rows.length == 0) {
                callback(new Error('Torrent not found'));
            } else {
                callback(undefined);
            }
    }.bind(this));
};

// Adds peer to database or returns error if the torrent is not tracked.
// NOTE: failure codes are optional, but included anyway. Can be removed
// for bandwidth saving.
Database.prototype.addPeer = function(peer, callback) {
    this.checkTorrent(peer, function(err) {
        if (err) {
            callback(undefined, common.bencodeFailure(200, 'Torrent not in database.'));
        } else  {
            this.transaction(peer, callback);
        }
    }.bind(this));
};

// Transaction to add a peer.
Database.prototype.transaction = function(peer, callback) {
    this.connection.beginTransaction(function(err) {
        this.connection.query('REPLACE '
            + 'INTO `nodetracker`.`peers` (`id`, `peer_id`, `info_hash`, `ip`, `port`, `uploaded`, `downloaded`, `left`) '
            + 'VALUES (' + peer.toAddPeerString() + ')'
            , function(err, result) {
                if (err) {
                    this.connection.rollback(function() {
                        callback(err, undefined);
                    });
                } else {
                    // TODO: lol fix that it doesn't update every cycle.
                    var field = '';
                    if (peer._left == 0) {
                        field = '`complete`';
                    } else {
                        field = '`incomplete`';
                    }
                    this.connection.query('UPDATE `nodetracker`.`torrent` '
                        + 'SET ' + field + ' = ' + field + ' + 1 '
                        + 'WHERE `info_hash` = ' + this.connection.escape(peer.info_hash)
                        , function(err, result) {
                            if (err) {
                                this.connection.rollback(function() {
                                    callback(err, undefined);
                                });
                            } else {
                                this.connection.commit(function(err) {
                                    if (err) {
                                        this.connection.rollback(function() {
                                            callback(err, undefined);
                                        })
                                    } else {
                                        this.getPeers(peer, callback);
                                    }
                                }.bind(this));
                            }
                    }.bind(this));
                }
        }.bind(this));
    }.bind(this));
};

// Removes peer from database.
Database.prototype.removePeer = function(peer, callback) {
    this.connection.query('SELECT `id`, `peer_id`, `info_hash`, `left` '
        + 'FROM `nodetracker`.`peers` '
        + 'WHERE `id` = ' + this.connection.escape(peer.peer_id + peer.info_hash)
        , function(err, rows, fields) {
            if (err) {
                callback(err, undefined);
            } else {
                // TODO: Convert this to a function. (Also used by flushPeers)
                rows.forEach(function(item) {
                    var field = '';
                    if (item.left == 0) {
                        field = '`complete`' ;
                    } else {
                        field = '`incomplete`';
                    }
                    this.connection.query('UPDATE `nodetracker`.`torrent` '
                        + 'SET ' + field + ' = ' + field + ' - 1 '
                        + 'WHERE `info_hash` = ' + this.connection.escape(item.info_hash) + ' '
                        + 'AND ' + field + ' > 0'
                        , function(err, result) {
                            console.log('Decremented ' + field + ' on ' + item.info_hash);
                    }.bind(this));
                    this.connection.query('DELETE FROM `nodetracker`.`peers` '
                        + 'WHERE `id` = ' + this.connection.escape(item.id)
                        , function(err, result) {
                            console.log('Removed peer ' + item.peer_id + ' for torrent: ' + item.info_hash);
                    }.bind(this));
                    this.getPeers(peer, callback);
                }.bind(this));
            }
    }.bind(this));
};

// Increments the completed count for torrent.
// TODO: Clients can spam complete count and it will keep increasing,
// should add check to prevent clients with left=0 from sending complete
// (or just add completed boolean)
Database.prototype.completePeer = function(peer, callback) {
    this.connection.query('UPDATE `nodetracker`.`torrent` '
        + 'SET `downloaded` = `downloaded` + 1, '
        + '`incomplete` = `incomplete` - 1, '
        + '`complete` = `complete` + 1 '
        + 'WHERE `info_hash` = ' + this.connection.escape(peer.info_hash)
        , function(err, result) {
            if (err) {
                callback(err, undefined);
            } else {
                this.updatePeer(peer, callback);
            }
    }.bind(this));
};

// Updates the peer in the database.
Database.prototype.updatePeer = function(peer, callback) {
    this.connection.query('UPDATE `nodetracker`.`peers` '
        + 'SET `uploaded` = ' + this.connection.escape(peer.uploaded) + ', '
        + '`downloaded` = ' + this.connection.escape(peer.downloaded) + ', '
        + '`left` = ' + this.connection.escape(peer._left) + ' '
        + 'WHERE `id` = ' + this.connection.escape(peer.peer_id + peer.info_hash)
        , function(err, result) {
            if (err) {
                callback(err, undefined);
            } else {
                this.getPeers(peer, callback);
            }

    }.bind(this));
};

// Returns a randomly ordered set of peers to the client.
// NOTE: Tracker supports compact and non-compact, however non-compact
// requires more bandwidth.
Database.prototype.getPeers = function(peer, callback) {
    var colums = '`ip`, `port` ';
    if (peer._compact === 0) {
        colums = '`peer_id`, ' + colums;
    }

    // TODO: Maybe create a smarter algorithm?
    // Return a certain high download : low download distribution.
    this.connection.query('SELECT ' + colums
        + 'FROM `nodetracker`.`peers` '
        + 'WHERE `info_hash` = ' + this.connection.escape(peer.info_hash) + ' '
        + 'AND `peer_id` != ' + this.connection.escape(peer.peer_id) + ' '
        + 'ORDER BY RAND() '
        + 'LIMIT ' + this.connection.escape(peer.numwant)
        , function(err, rows, result) {
            if (err) {
                callback(err, undefined);
                return;
            }
            this.scrape(peer.info_hash, function(err, response) {
                if (err) {
                    callback(err, undefined);
                } else {
                    // Add all peers to response!
                    rows.forEach(function(item) {
                        response.addPeer(item);
                    });
                    // Return hex IPv4 if client requested compact.
                    if (peer._compact) {
                        callback(undefined, response.bencodePeersIPv4Compact());
                    } else {
                        callback(undefined, response.bencodePeersIPv4());
                    }
                }
            }.bind(this));
    }.bind(this));
};

Database.prototype.scrape = function(info_hash, callback) {
    if (Array.isArray(info_hash)) {
        info_hash = _.map(info_hash, function(item) {
            return this.connection.escape(item);
        }.bind(this))
        info_hash = info_hash.join(',');
    } else {
        info_hash = this.connection.escape(info_hash);
    }

    this.connection.query('SELECT `info_hash`, `complete`, `incomplete`, `downloaded` '
        + 'FROM `nodetracker`.`torrent` '
        + 'WHERE `info_hash` IN (' + info_hash + ')'
        , function(err, rows, fields) {
            if (err) {
                callback(err, undefined);
                return;
            }
            var response = new Response();
            rows.forEach(function(item) {
                response.addScrape(item);
            });
            callback(undefined, response);
    });
};

function Database() {
    this.connection = mysql.createConnection(mysqlconf);

    this.connection.connect();
    // Clear out expired peers.
    this.flushPeers();
    // Set interval to clear expired peers.
    setInterval(this.flushPeers.bind(this), 1000 * 60 * 4);
}

module.exports = Database;<|endoftext|>