<commit_before>/**
 * Quickie wrapper around XHR to fetch a file as array buffer chunks.
 *
 * Call streamFile.readBytes() after an onread event's data has been
 * processed to trigger the next read.
 *
 * IE 10: uses MSStream / MSStreamReader for true streaming
 * Firefox: uses moz-chunked-arraybuffer to buffer & deliver during download
 * Safari, Chrome: uses binary string to buffer & deliver during download
 *
 * @author Brion Vibber <brion@pobox.com>
 * @copyright 2014
 * @license MIT-style
 */
function StreamFile(options) {
	var self = this,
		url = options.url,
		started = false,
		onstart = options.onstart || function(){},
		onbuffer = options.onbuffer || function(){},
		onread = options.onread || function(){},
		ondone = options.ondone || function(){},
		onerror = options.onerror || function(){},
		bufferSize = options.bufferSize || 8192,
		minBufferSize = options.minBufferSize || 65536,
		seekPosition = options.seekPosition || 0,
		chunkSize = options.chunkSize || 1024 * 1024, // read/buffer up to a megabyte at a time
		waitingForInput = false,
		doneBuffering = false,
		bytesTotal = 0,
		bytesRead = 0,
		buffers = [];

	// -- internal private methods
	var internal = {
		/**
		 * @var {XMLHttpRequest}
		 */
		xhr: null,

		/**
		 * Test if a given responseType value is valid on an XHR
		 *
		 * @return boolean
		 */
		tryMethod: function(rt) {
			var xhr = new XMLHttpRequest();
			xhr.open("GET", url);
			try {
				// Set the response type and see if it explodes!
				xhr.responseType = rt;
			} catch (e) {
				// Safari 6 throws a DOM Exception on invalid setting
				return false;
			}
			// Other browsers just don't accept the setting, so check
			// whether it made it through.
			return (xhr.responseType == rt);
		},

		setBytesTotal: function(xhr) {
			var contentLength = xhr.getResponseHeader('Content-Length');
			if (contentLength == null || contentLength == '') {
				// Unknown file length... maybe streaming live?
				bytesTotal = 0;
			} else {
				bytesTotal = parseInt(contentLength, 10);
			}
		},

		getMetadata: function() {
			var xhr = new XMLHttpRequest();
			var headUrl = url;

			if (navigator.userAgent.match(/Chrome/)) {
				// HACK ALERT!
				//
				// Cache buster because cached HEAD reqs in Chrome don't
				// include a Content-Length header, which defeats the purpose.
				// And we're not allowed to send cache-control. Oh CORS!
				//
				// Better would be to get the Content-Range from the 206
				// response, but it's not whitelisted yet. *headdesk*
				//
				headUrl += '?cachebuster=' + Math.random()
			}

			xhr.open("HEAD", headUrl);
			xhr.onreadystatechange = function(event) {
				if (xhr.readyState == 2) {
					internal.onXHRHeadersReceived(xhr);
				} else if (xhr.readyState == 4) {
					xhr.onreadystatechange = null;
					xhr = null;
					internal.openXHR();
				}
			};
			xhr.send();
		},

		openXHR: function() {
			var xhr = internal.xhr = new XMLHttpRequest();
			xhr.open("GET", url);

			internal.setXHROptions(xhr);

			var range = null;
			if (seekPosition || chunkSize) {
				range = 'bytes=' + seekPosition + '-';
			}
			if (chunkSize) {
				range += Math.min(seekPosition + chunkSize, bytesTotal) - 1;
			}
			if (range !== null) {
				xhr.setRequestHeader('Range', range);
			}

			bytesRead = 0;

			xhr.onreadystatechange = function(event) {
				if (xhr.readyState == 2) {
					//internal.onXHRHeadersReceived(xhr);
					// @todo check that partial content was supported if relevant
				} else if (xhr.readyState == 3) {
					internal.onXHRLoading(xhr);
				} else if (xhr.readyState == 4) {
					// Complete.
					internal.onXHRDone(xhr);
				}
			};

			xhr.send();
		},

		setXHROptions: function(xhr) {
			throw new Error('abstract function');
		},

		onXHRHeadersReceived: function(xhr) {
			if (xhr.status >= 400) {
				// errrorrrrrrr
				console.log("HTTP " + xhr.status + ": " +xhr.statusText);
				onerror();
				xhr.abort();
			} else {
				internal.setBytesTotal(xhr);
				started = true;
				onstart();
			}
		},

		onXHRLoading: function(xhr) {
			throw new Error('abstract function');
		},

		onXHRDone: function(xhr) {
			console.log("DONE BUFFERING");
			doneBuffering = true;
		},

		abortXHR: function(xhr) {
			xhr.onreadystatechange = null;
			xhr.abort();
		},

		bufferData: function(buffer) {
			if (buffer) {
				buffers.push(buffer);
				onbuffer();

				internal.readNextChunk();
			}
		},

		bytesBuffered: function() {
			var bytes = 0;
			buffers.forEach(function(buffer) {
				bytes += buffer.byteLength;
			});
			return bytes;
		},

		dataToRead: function() {
			return internal.bytesBuffered() >= minBufferSize;
		},

		popBuffer: function() {
			var bufferOut = new ArrayBuffer(bufferSize),
				bytesOut = new Uint8Array(bufferOut),
				byteLength = 0;

			function stuff(bufferIn) {
				var bytesIn = new Uint8Array(bufferIn);
				bytesOut.set(bytesIn, byteLength);
				byteLength += bufferIn.byteLength;
			}

			while (byteLength < minBufferSize) {
				var needBytes = minBufferSize - byteLength,
					nextBuffer = buffers.shift();
				if (!nextBuffer) {
					break;
				}

				if (needBytes >= nextBuffer.byteLength) {
					// if it fits, it sits
					stuff(nextBuffer);
				} else {
					// Split the buffer and requeue the rest
					var croppedBuffer = nextBuffer.slice(0, needBytes),
						remainderBuffer = nextBuffer.slice(needBytes);
					buffers.unshift(remainderBuffer);
					stuff(croppedBuffer);
					break;
				}
			}

			//console.log('got', bufferOut, byteLength);
			bytesRead += byteLength;
			return bufferOut.slice(0, byteLength);
		},

		clearReadState: function() {
			bytesRead = 0;
			doneBuffering = false;
			waitingForInput = true;
		},

		clearBuffers: function() {
			internal.clearReadState();
			buffers.splice(0, buffers.length);
		},

		// Read the next binary buffer out of the buffered data
		readNextChunk: function() {
			if (waitingForInput) {
				waitingForInput = false;
				onread(internal.popBuffer());
				if (doneBuffering && !internal.dataToRead()) {
					internal.onReadDone();
				}
			}
		},

		onReadDone: function() {
			if (self.bytesBuffered < self.bytesTotal) {
				seekPosition += chunkSize;
				console.log('2 seek to: ' + seekPosition);
				internal.clearReadState();
				internal.openXHR();
			} else {
				// We're out of data!
				//setTimeout(function() {
					ondone();
				//}, 0);
			}
		},

		// See if we can seek within already-buffered data
		quickSeek: function(pos) {
			return false;
		}

	};

	// -- Public methods
	self.readBytes = function() {
		if (internal.dataToRead()) {
			var buffer = internal.popBuffer();
			//setTimeout(function() {
				onread(buffer);
			//}, 0);
			if (doneBuffering) {
				console.log('done buffering', self.bytesBuffered, self.bytesTotal);
			}
			if (doneBuffering && self.bytesBuffered < self.bytesTotal) {
				seekPosition += chunkSize;
				console.log('1 seek to: ' + seekPosition);
				internal.clearReadState();
				internal.openXHR();
			}
		} else if (doneBuffering) {
			// We're out of data!
			internal.onReadDone();
		} else {
			// Nothing queued...
			waitingForInput = true;
		}
	};

	self.abort = function() {
		if (internal.xhr) {
			internal.abortXHR(internal.xhr);
			internal.xhr = null;
			internal.clearBuffers();
		}
	};

	self.seek = function(bytePosition) {
		console.log('seeking to: ' + bytePosition);
		if (internal.quickSeek(bytePosition)) {
			console.log('quick seek successful');
		} else {
			self.abort();
			seekPosition = bytePosition;
			internal.openXHR();
		}
	};

	// -- public properties
	Object.defineProperty(self, 'bytesTotal', {
		get: function() {
			return bytesTotal;
		}
	});

	Object.defineProperty(self, 'bytesBuffered', {
		get: function() {
			return self.bytesRead + internal.bytesBuffered();
		}
	});

	Object.defineProperty(self, 'bytesRead', {
		get: function() {
			return seekPosition + bytesRead;
		}
	});

	// Handy way to call super functions
	var orig = {};
	for (var prop in internal) {
		orig[prop] = internal[prop];
	}

	// -- Backend selection and method overrides
	if (internal.tryMethod('moz-chunked-arraybuffer')) {
		console.log("Streaming input using moz-chunked-arraybuffer");

		internal.setXHROptions = function(xhr) {
			xhr.responseType = 'moz-chunked-arraybuffer';

			xhr.onprogress = function() {
				// xhr.response is a per-chunk ArrayBuffer
				internal.bufferData(xhr.response);
			};
		};

		internal.abortXHR = function(xhr) {
			xhr.onprogress = null;
			orig.abortXHR(xhr);
		};

		internal.onXHRLoading = function(xhr) {
			// we have to get from the 'progress' event
		};

	} else if (internal.tryMethod('ms-stream')) {
		// IE 10 supports returning a Stream from XHR.
		console.log("Streaming input using MSStreamReader");

		// Don't bother reading in chunks, MSStream handles it for us
		chunkSize = 0;

		var stream, streamReader;
		var restarted = false;

		internal.setXHROptions = function(xhr) {
			console.log('setting up new xhr');
			xhr.responseType = 'ms-stream';
		};

		internal.abortXHR = function(xhr) {
			console.log('aborting XHR and StreamReader');
			restarted = true;
			if (streamReader) {
				streamReader.abort();
				streamReader = null;
			}
			if (stream) {
				stream.msClose();
				stream = null;
			}
			orig.abortXHR(xhr);
		};

		internal.onXHRLoading = function(xhr) {
			console.log('transferring to StreamReader');
			// Transfer us over to the StreamReader...
			stream = xhr.response;
			xhr.onreadystatechange = null;
			if (waitingForInput) {
				waitingForInput = false;
				self.readBytes();
			}
		};

		internal.bytesBuffered = function() {
			if (stream) {
				return stream.bytesBuffered - bytesRead;
			} else {
				return 0;
			}
		};

		self.readBytes = function() {
			if (stream) {
				streamReader = new MSStreamReader();
				streamReader.onload = function(event) {
					var buffer = event.target.result,
						len = buffer.byteLength;
					if (len > 0) {
						bytesRead += len;
						onread(buffer);
					} else {
						// Zero length means end of stream.
						ondone();
					}
				}
				streamReader.onerror = function(event) {
					onerror('mystery error streaming');
				}
				streamReader.readAsArrayBuffer(stream, bufferSize);
			} else {
				waitingForInput = true;
			}
		};

	} else if ((new XMLHttpRequest()).overrideMimeType !== undefined) {

		// Use old binary string method since we can read reponseText
		// progressively and extract ArrayBuffers from that.
		console.log("Streaming input using XHR progressive binary string");

		internal.setXHROptions = function(xhr) {
			xhr.responseType = "text";
			xhr.overrideMimeType('text/plain; charset=x-user-defined');
		};

		var lastPosition = 0;

		// Is there a better way to do this conversion? :(
		function stringToArrayBuffer(chunk) {
			var len = chunk.length,
				buffer = new ArrayBuffer(len),
				bytes = new Uint8Array(buffer);
			for (var i = 0; i < len; i++) {
				bytes[i] = chunk.charCodeAt(i);
			}
			return buffer;
		}

		internal.clearReadState = function() {
			orig.clearReadState();
			lastPosition = 0;
		};

		internal.onXHRLoading = function(xhr) {
			// xhr.responseText is a binary string of entire file so far
			var str = xhr.responseText;
			if (lastPosition < str.length) {
				var chunk = str.slice(lastPosition),
					buffer = stringToArrayBuffer(chunk);
				lastPosition = str.length;
				internal.bufferData(buffer);
			}
		};

		/*
		internal.quickSeek = function(pos) {
			var bufferedPos = pos - seekPosition;
			if (bufferedPos < 0) {
				return false;
			} else if (bufferedPos >= internal.xhr.responseText.length) {
				return false;
			} else {
				lastPosition = bufferedPos;
				bytesRead = lastPosition;
				setTimeout(function() {
					onbuffer()
				}, 0);
				return true;
			}
		};
		*/
	} else {
		throw new Error("No streaming HTTP input method found.");
	}

	internal.getMetadata();
}
<commit_msg>damn math IS hard. fix buffering for REALS<commit_after>/**
 * Quickie wrapper around XHR to fetch a file as array buffer chunks.
 *
 * Call streamFile.readBytes() after an onread event's data has been
 * processed to trigger the next read.
 *
 * IE 10: uses MSStream / MSStreamReader for true streaming
 * Firefox: uses moz-chunked-arraybuffer to buffer & deliver during download
 * Safari, Chrome: uses binary string to buffer & deliver during download
 *
 * @author Brion Vibber <brion@pobox.com>
 * @copyright 2014
 * @license MIT-style
 */
function StreamFile(options) {
	var self = this,
		url = options.url,
		started = false,
		onstart = options.onstart || function(){},
		onbuffer = options.onbuffer || function(){},
		onread = options.onread || function(){},
		ondone = options.ondone || function(){},
		onerror = options.onerror || function(){},
		bufferSize = options.bufferSize || 8192,
		minBufferSize = options.minBufferSize || 65536,
		seekPosition = options.seekPosition || 0,
		bufferPosition = seekPosition,
		chunkSize = options.chunkSize || 1024 * 1024, // read/buffer up to a megabyte at a time
		waitingForInput = false,
		doneBuffering = false,
		bytesTotal = 0,
		bytesRead = 0,
		buffers = [];

	// -- internal private methods
	var internal = {
		/**
		 * @var {XMLHttpRequest}
		 */
		xhr: null,

		/**
		 * Test if a given responseType value is valid on an XHR
		 *
		 * @return boolean
		 */
		tryMethod: function(rt) {
			var xhr = new XMLHttpRequest();
			xhr.open("GET", url);
			try {
				// Set the response type and see if it explodes!
				xhr.responseType = rt;
			} catch (e) {
				// Safari 6 throws a DOM Exception on invalid setting
				return false;
			}
			// Other browsers just don't accept the setting, so check
			// whether it made it through.
			return (xhr.responseType == rt);
		},

		setBytesTotal: function(xhr) {
			var contentLength = xhr.getResponseHeader('Content-Length');
			if (contentLength == null || contentLength == '') {
				// Unknown file length... maybe streaming live?
				bytesTotal = 0;
			} else {
				bytesTotal = parseInt(contentLength, 10);
			}
		},

		getMetadata: function() {
			var xhr = new XMLHttpRequest();
			var headUrl = url;

			if (navigator.userAgent.match(/Chrome/)) {
				// HACK ALERT!
				//
				// Cache buster because cached HEAD reqs in Chrome don't
				// include a Content-Length header, which defeats the purpose.
				// And we're not allowed to send cache-control. Oh CORS!
				//
				// Better would be to get the Content-Range from the 206
				// response, but it's not whitelisted yet. *headdesk*
				//
				headUrl += '?cachebuster=' + Math.random()
			}

			xhr.open("HEAD", headUrl);
			xhr.onreadystatechange = function(event) {
				if (xhr.readyState == 2) {
					internal.onXHRHeadersReceived(xhr);
				} else if (xhr.readyState == 4) {
					xhr.onreadystatechange = null;
					xhr = null;
					internal.openXHR();
				}
			};
			xhr.send();
		},

		openXHR: function() {
			var xhr = internal.xhr = new XMLHttpRequest();
			xhr.open("GET", url);

			internal.setXHROptions(xhr);

			var range = null;
			if (seekPosition || chunkSize) {
				range = 'bytes=' + seekPosition + '-';
			}
			if (chunkSize) {
				range += Math.min(seekPosition + chunkSize, bytesTotal) - 1;
			}
			if (range !== null) {
				xhr.setRequestHeader('Range', range);
			}

			bytesRead = 0;

			xhr.onreadystatechange = function(event) {
				if (xhr.readyState == 2) {
					//internal.onXHRHeadersReceived(xhr);
					// @todo check that partial content was supported if relevant
				} else if (xhr.readyState == 3) {
					internal.onXHRLoading(xhr);
				} else if (xhr.readyState == 4) {
					// Complete.
					internal.onXHRDone(xhr);
				}
			};

			xhr.send();
		},

		setXHROptions: function(xhr) {
			throw new Error('abstract function');
		},

		onXHRHeadersReceived: function(xhr) {
			if (xhr.status >= 400) {
				// errrorrrrrrr
				console.log("HTTP " + xhr.status + ": " +xhr.statusText);
				onerror();
				xhr.abort();
			} else {
				internal.setBytesTotal(xhr);
				started = true;
				onstart();
			}
		},

		onXHRLoading: function(xhr) {
			throw new Error('abstract function');
		},

		onXHRDone: function(xhr) {
			console.log("DONE BUFFERING");
			doneBuffering = true;
		},

		abortXHR: function(xhr) {
			xhr.onreadystatechange = null;
			xhr.abort();
		},

		bufferData: function(buffer) {
			if (buffer) {
				buffers.push(buffer);
				onbuffer();

				internal.readNextChunk();
			}
		},

		bytesBuffered: function() {
			var bytes = 0;
			buffers.forEach(function(buffer) {
				bytes += buffer.byteLength;
			});
			return bytes;
		},

		dataToRead: function() {
			return internal.bytesBuffered() > 0;
		},

		popBuffer: function() {
			var bufferOut = new ArrayBuffer(bufferSize),
				bytesOut = new Uint8Array(bufferOut),
				byteLength = 0;

			function stuff(bufferIn) {
				var bytesIn = new Uint8Array(bufferIn);
				bytesOut.set(bytesIn, byteLength);
				byteLength += bufferIn.byteLength;
			}

			while (byteLength < minBufferSize) {
				var needBytes = minBufferSize - byteLength,
					nextBuffer = buffers.shift();
				if (!nextBuffer) {
					break;
				}

				if (needBytes >= nextBuffer.byteLength) {
					// if it fits, it sits
					stuff(nextBuffer);
				} else {
					// Split the buffer and requeue the rest
					var croppedBuffer = nextBuffer.slice(0, needBytes),
						remainderBuffer = nextBuffer.slice(needBytes);
					buffers.unshift(remainderBuffer);
					stuff(croppedBuffer);
					break;
				}
			}

			//console.log('got', bufferOut, byteLength);
			bytesRead += byteLength;
			bufferPosition += byteLength;
			return bufferOut.slice(0, byteLength);
		},

		clearReadState: function() {
			bytesRead = 0;
			doneBuffering = false;
			waitingForInput = true;
		},

		clearBuffers: function() {
			internal.clearReadState();
			buffers.splice(0, buffers.length);
			bufferPosition = seekPosition;
		},

		// Read the next binary buffer out of the buffered data
		readNextChunk: function() {
			if (waitingForInput) {
				waitingForInput = false;
				onread(internal.popBuffer());
				if (doneBuffering && !internal.dataToRead()) {
					internal.onReadDone();
				}
			}
		},

		onReadDone: function() {
			console.log('onReadDone');
			ondone();
		},

		// See if we can seek within already-buffered data
		quickSeek: function(pos) {
			return false;
		}

	};

	// -- Public methods
	self.readBytes = function() {
		if (internal.dataToRead()) {
			var buffer = internal.popBuffer();
			onread(buffer);
			if (doneBuffering && self.bytesBuffered < Math.min(bufferPosition + chunkSize, self.bytesTotal)) {
				//console.log('yo!', doneBuffering, seekPosition, bufferPosition, self.bytesRead, self.bytesBuffered, self.bytesTotal);
				seekPosition += chunkSize;
				console.log('1 seek to: ' + seekPosition);
				internal.clearReadState();
				internal.openXHR();
			}
		} else if (doneBuffering) {
			// We're out of data!
			internal.onReadDone();
		} else {
			// Nothing queued...
			waitingForInput = true;
		}
	};

	self.abort = function() {
		if (internal.xhr) {
			internal.abortXHR(internal.xhr);
			internal.xhr = null;
			internal.clearBuffers();
		}
	};

	self.seek = function(bytePosition) {
		console.log('seeking to: ' + bytePosition);
		if (internal.quickSeek(bytePosition)) {
			console.log('quick seek successful');
		} else {
			self.abort();
			seekPosition = bytePosition;
			internal.clearBuffers();
			internal.openXHR();
		}
	};

	// -- public properties
	Object.defineProperty(self, 'bytesTotal', {
		get: function() {
			return bytesTotal;
		}
	});

	Object.defineProperty(self, 'bytesBuffered', {
		get: function() {
			return bufferPosition + internal.bytesBuffered();
		}
	});

	Object.defineProperty(self, 'bytesRead', {
		get: function() {
			return seekPosition + bytesRead;
		}
	});

	// Handy way to call super functions
	var orig = {};
	for (var prop in internal) {
		orig[prop] = internal[prop];
	}

	// -- Backend selection and method overrides
	if (internal.tryMethod('moz-chunked-arraybuffer')) {
		console.log("Streaming input using moz-chunked-arraybuffer");

		internal.setXHROptions = function(xhr) {
			xhr.responseType = 'moz-chunked-arraybuffer';

			xhr.onprogress = function() {
				// xhr.response is a per-chunk ArrayBuffer
				internal.bufferData(xhr.response);
			};
		};

		internal.abortXHR = function(xhr) {
			xhr.onprogress = null;
			orig.abortXHR(xhr);
		};

		internal.onXHRLoading = function(xhr) {
			// we have to get from the 'progress' event
		};

	} else if (internal.tryMethod('ms-stream')) {
		// IE 10 supports returning a Stream from XHR.
		console.log("Streaming input using MSStreamReader");

		// Don't bother reading in chunks, MSStream handles it for us
		chunkSize = 0;

		var stream, streamReader;
		var restarted = false;

		internal.setXHROptions = function(xhr) {
			console.log('setting up new xhr');
			xhr.responseType = 'ms-stream';
		};

		internal.abortXHR = function(xhr) {
			console.log('aborting XHR and StreamReader');
			restarted = true;
			if (streamReader) {
				streamReader.abort();
				streamReader = null;
			}
			if (stream) {
				stream.msClose();
				stream = null;
			}
			orig.abortXHR(xhr);
		};

		internal.onXHRLoading = function(xhr) {
			console.log('transferring to StreamReader');
			// Transfer us over to the StreamReader...
			stream = xhr.response;
			xhr.onreadystatechange = null;
			if (waitingForInput) {
				waitingForInput = false;
				self.readBytes();
			}
		};

		internal.bytesBuffered = function() {
			if (stream) {
				return stream.bytesBuffered - bytesRead;
			} else {
				return 0;
			}
		};

		self.readBytes = function() {
			if (stream) {
				streamReader = new MSStreamReader();
				streamReader.onload = function(event) {
					var buffer = event.target.result,
						len = buffer.byteLength;
					if (len > 0) {
						bytesRead += len;
						bufferPosition += len;
						onread(buffer);
					} else {
						// Zero length means end of stream.
						ondone();
					}
				}
				streamReader.onerror = function(event) {
					onerror('mystery error streaming');
				}
				streamReader.readAsArrayBuffer(stream, bufferSize);
			} else {
				waitingForInput = true;
			}
		};

	} else if ((new XMLHttpRequest()).overrideMimeType !== undefined) {

		// Use old binary string method since we can read reponseText
		// progressively and extract ArrayBuffers from that.
		console.log("Streaming input using XHR progressive binary string");

		internal.setXHROptions = function(xhr) {
			xhr.responseType = "text";
			xhr.overrideMimeType('text/plain; charset=x-user-defined');
		};

		var lastPosition = 0;

		// Is there a better way to do this conversion? :(
		function stringToArrayBuffer(chunk) {
			var len = chunk.length,
				buffer = new ArrayBuffer(len),
				bytes = new Uint8Array(buffer);
			for (var i = 0; i < len; i++) {
				bytes[i] = chunk.charCodeAt(i);
			}
			return buffer;
		}

		internal.clearReadState = function() {
			orig.clearReadState();
			lastPosition = 0;
		};

		internal.onXHRLoading = function(xhr) {
			// xhr.responseText is a binary string of entire file so far
			var str = xhr.responseText;
			if (lastPosition < str.length) {
				var chunk = str.slice(lastPosition),
					buffer = stringToArrayBuffer(chunk);
				lastPosition = str.length;
				internal.bufferData(buffer);
			}
		};

		/*
		internal.quickSeek = function(pos) {
			var bufferedPos = pos - seekPosition;
			if (bufferedPos < 0) {
				return false;
			} else if (bufferedPos >= internal.xhr.responseText.length) {
				return false;
			} else {
				lastPosition = bufferedPos;
				bytesRead = lastPosition;
				setTimeout(function() {
					onbuffer()
				}, 0);
				return true;
			}
		};
		*/
	} else {
		throw new Error("No streaming HTTP input method found.");
	}

	internal.getMetadata();
}
<|endoftext|>


<commit_before>import path from 'path'
import fse from 'fs-extra'

import {
  coreUtils,
  config,
  abeExtend
} from '../../cli'

var middleware = function(req, res, next) {
  if (req.originalUrl.indexOf('/abe') > -1) {
    return next()
  }

  if (req.originalUrl === '' || req.originalUrl === '/' || req.originalUrl.indexOf('.') === -1) {
    res.redirect('/abe')
  } else if (req.originalUrl.indexOf('.' + config.files.templates.extension) > -1) {

    html = ''

    var page = path.join(config.root, config.publish.url, req.originalUrl)
    if (coreUtils.file.exist(page)) {
      html = fse.readFileSync(page, 'utf8')
    }else {
      return next()
    }

    html = abeExtend.hooks.instance.trigger('beforePreview', html, req, res, next)
    res.set('Content-Type', 'text/html')
    return res.send(html)

  } else {
    return next()
  }
}

export default middleware<commit_msg>fix: variable html was not defined<commit_after>import path from 'path'
import fse from 'fs-extra'

import {
  coreUtils,
  config,
  abeExtend
} from '../../cli'

var middleware = function(req, res, next) {
  if (req.originalUrl.indexOf('/abe') > -1) {
    return next()
  }

  if (req.originalUrl === '' || req.originalUrl === '/' || req.originalUrl.indexOf('.') === -1) {
    res.redirect('/abe')
  } else if (req.originalUrl.indexOf('.' + config.files.templates.extension) > -1) {

    var html = ''

    var page = path.join(config.root, config.publish.url, req.originalUrl)
    if (coreUtils.file.exist(page)) {
      html = fse.readFileSync(page, 'utf8')
    }else {
      return next()
    }

    html = abeExtend.hooks.instance.trigger('beforePreview', html, req, res, next)
    res.set('Content-Type', 'text/html')
    return res.send(html)

  } else {
    return next()
  }
}

export default middleware<|endoftext|>


<commit_before>// ***********************************************************************************
// ID of the Google spreadsheet
// ***********************************************************************************
// Make sure your spreadsheet is published (File > Publish to the web…)
var spreadsheetId = "1wslTooejJndx0gIU3RDlC_7P9FkQjntyW8VTUzRRctw";

// ***********************************************************************************
// Your FC and IGN
// ***********************************************************************************
var friendCode = "2208-8894-6617";
var inGameName = "Razator";

// ***********************************************************************************
// URL of your Reddit/Twitter account
// ***********************************************************************************
var contactUrl = "http://reddit.com/u/mathwizx2";

// ***********************************************************************************
// URL of your PGL trainer icon (optional)
// ***********************************************************************************
// Download it from the Pokémon Global Link and re-upload to an image-sharing website
var trainerIconUrl = "https://raw.githubusercontent.com/razator73/poke-trade-sheet/master/static/trainerpic.png";
<commit_msg>Update config.js<commit_after>// ***********************************************************************************
// ID of the Google spreadsheet
// ***********************************************************************************
// Make sure your spreadsheet is published (File > Publish to the web…)
var spreadsheetId = "1Ajsn12EW6WNOSFjjD58JXW9YPkpvY4FC3xe7j1mwvJg";

// ***********************************************************************************
// Your FC and IGN
// ***********************************************************************************
var friendCode = "2208-8894-6617";
var inGameName = "Razator";

// ***********************************************************************************
// URL of your Reddit/Twitter account
// ***********************************************************************************
var contactUrl = "http://reddit.com/u/mathwizx2";

// ***********************************************************************************
// URL of your PGL trainer icon (optional)
// ***********************************************************************************
// Download it from the Pokémon Global Link and re-upload to an image-sharing website
var trainerIconUrl = "https://raw.githubusercontent.com/razator73/poke-trade-sheet/master/static/trainerpic.png";
<|endoftext|>


<commit_before>import { normalize } from 'normalizr';
import HttpError from 'standard-http-error';

export default function requestBuilder(url, requestOpts = {}, { handleResponse = false, normalizeSchema = false, handleError = false } = {}) {
  return (payload, meta = {}) => {
    const callUrl = typeof url === 'function' ? url(payload, meta) : url;
    const callOpts = {
      ...requestOpts,
    };

    let headers = {};

    if (meta.authToken) {
      headers['Authorization'] = `Bearer ${meta.authToken}`;
    }
    if (typeof callOpts.headers === 'function') {
      headers = callOpts.headers(headers, payload, meta);
    }
    if (headers.Authorization) {
      headers['Access-Control-Allow-Credentials'] = true;
      callOpts.credentials = 'include';
    }

    if (typeof callOpts.body === 'function') {
      callOpts.body = callOpts.body(...args);
    }
    if (typeof callOpts.body !== 'undefined' && typeof callOpts.body !== 'string') {
      callOpts.body = JSON.stringify(callOpts.body);
    }

    callOpts.headers = headers;

    return fetch(callUrl, callOpts)
      .then((response) => {
        switch (response.status) {
          case 200:
          case 201:
            return response;  // Keep going!
          default:
            throw new HttpError(response.status, { response });
        }
      })
      .then((response) => handleResponse ? handleResponse(response, payload, meta) : response.json())
      .then((data) => normalizeSchema ? normalize(data, normalizeSchema) : data)
      .catch(handleError ? (err) => handleError(err, payload, meta) : (err) => {
        throw err;
      });
  };
}
<commit_msg>requestBuilder: Rework request body, only allow on POST, PUT, PATCH<commit_after>import { normalize } from 'normalizr';
import HttpError from 'standard-http-error';

const hasBody = ['POST', 'PUT', 'PATCH'];

export default function requestBuilder(url, requestOpts = {}, { handleResponse = false, normalizeSchema = false, handleError = false } = {}) {
  return (payload, meta = {}) => {
    const callUrl = typeof url === 'function' ? url(payload, meta) : url;
    const callOpts = {
      ...requestOpts,
    };

    let headers = {};
    let body = hasBody.includes(requestOpts.method) ? payload : undefined;

    if (meta.authToken) {
      headers['Authorization'] = `Bearer ${meta.authToken}`;
    }
    if (typeof callOpts.headers === 'function') {
      headers = callOpts.headers(headers, payload, meta);
    }
    if (headers.Authorization) {
      headers['Access-Control-Allow-Credentials'] = true;
      callOpts.credentials = 'include';
    }

    if (typeof callOpts.body === 'function') {
      body = callOpts.body(body, meta);
    }
    if (typeof body !== 'undefined' && typeof body !== 'string') {
      body = JSON.stringify(body);
      headers['Accept'] = 'application/json';
      headers['Content-Type'] = 'application/json';
    }

    callOpts.headers = headers;
    callOpts.body = body;

    return fetch(callUrl, callOpts)
      .then((response) => {
        switch (response.status) {
          case 200:
          case 201:
            return response;  // Keep going!
          default:
            throw new HttpError(response.status, { response });
        }
      })
      .then((response) => handleResponse ? handleResponse(response, payload, meta) : response.json())
      .then((data) => normalizeSchema ? normalize(data, normalizeSchema) : data)
      .catch(handleError ? (err) => handleError(err, payload, meta) : (err) => {
        throw err;
      });
  };
}
<|endoftext|>


<commit_before>'use strict';

/**
 * @file
 * Configure group routes
 */

import express from 'express';
const GroupRoutes = express.Router();

import React from 'react';
import ReactDOM from 'react-dom/server';
import GroupSearchContainerComponent from '../../client/components/Group/Search/GroupSearchContainer.component.js';
import GroupPostComponent from '../../client/components/Group/Post/GroupPost.component.js';

import {stringToObject} from '../../utils/QueryParser.util.js';
import dbcMiddleware from './middleware.js';

GroupRoutes.get(['/search*'], (req, res) => {
  let query = req.query || [];
  let qObj = query.text ? stringToObject(query) : [];
  query = JSON.stringify(query);

  let promiseResponse = req.app.get('serviceProvider').trigger(
    'queryGroups', qObj.map((val) => {
      return val.value;
    }).join(' '), {request: req});

  dbcMiddleware.setupSSR(req, res, promiseResponse, (err, result, serviceTime) => {
    let groupData = result ? result[0].groups : [];
    let groupSearchString = ReactDOM.renderToString(
      <GroupSearchContainerComponent groupData={groupData} query={qObj} />);
    dbcMiddleware.renderPage(res, 'group_index', {
      query: query,
      groupSearchProps: JSON.stringify({
        groupData: groupData,
        qObj
      }),
      groupSearchString: groupSearchString
    }, serviceTime);
  });
});

GroupRoutes.get(['/:groupId/post/:id'], (req, res) => {
  let promiseResponse = req.app.get('serviceProvider').trigger('getGroupPost', req.params.id, {request: req});

  dbcMiddleware.setupSSR(req, res, promiseResponse, (err, result, serviceTime) => {
    let groupPostData = result ? result[0] : {};
    let groupPostString = ReactDOM.renderToString(
      <GroupPostComponent groupId={parseInt(req.params.groupId, 10)}
                          groupPostData={groupPostData}
                          groupPostId={parseInt(req.params.id, 10)} />);
    dbcMiddleware.renderPage(res, 'group_post_view', {
      groupId: req.params.groupId,
      id: req.params.id,
      groupPostData: JSON.stringify(groupPostData),
      groupPostString: groupPostString
    }, serviceTime);
  });
});

GroupRoutes.get('/create', dbcMiddleware.ensureAuthenticated, (req, res) => {
  res.render('group', {});
});

GroupRoutes.get('/:id', (req, res) => {
  var id = req.params.id;
  res.render('group', {id});
});

export default GroupRoutes;
<commit_msg>Accumulated commit of the following from branch 'origin/ready/mra':<commit_after>'use strict';

/**
 * @file
 * Configure group routes
 */

import express from 'express';
const GroupRoutes = express.Router();

import React from 'react';
import ReactDOM from 'react-dom/server';
import GroupSearchContainerComponent from '../../client/components/Group/Search/GroupSearchContainer.component.js';
import GroupPostComponent from '../../client/components/Group/Post/GroupPost.component.js';

import {stringToObject} from '../../utils/QueryParser.util.js';
import dbcMiddleware from './middleware.js';

GroupRoutes.get(['/search*'], (req, res) => {
  let query = req.query || [];
  let qObj = query.text ? stringToObject(query) : [];
  query = JSON.stringify(query);

  let promiseResponse = req.app.get('serviceProvider').trigger(
    'queryGroups', qObj.map((val) => {
      return val.value;
    }).join(' '), {request: req});

  dbcMiddleware.setupSSR(req, res, promiseResponse, (err, result, serviceTime) => {
    let groupData = result ? result[0].groups : [];
    let groupSearchString = ReactDOM.renderToString(
      <GroupSearchContainerComponent groupData={groupData} query={qObj} />);
    dbcMiddleware.renderPage(res, 'group_index', {
      query: query,
      groupSearchProps: JSON.stringify({
        groupData: groupData,
        qObj
      }),
      groupSearchString: groupSearchString
    }, serviceTime);
  });
});

GroupRoutes.get(['/:groupId/post/:id'], (req, res) => {
  let promiseResponse = req.app.get('serviceProvider').trigger('getGroupPost', req.params.id, {request: req});

  dbcMiddleware.setupSSR(req, res, promiseResponse, (err, result, serviceTime) => {
    let groupPostData = result ? result[0] : {};
    let groupPostString = ReactDOM.renderToString(
      <GroupPostComponent groupId={parseInt(req.params.groupId, 10)}
                          groupPostData={groupPostData}
                          groupPostId={parseInt(req.params.id, 10)} />);
    dbcMiddleware.renderPage(res, 'group_post_view', {
      groupId: req.params.groupId,
      id: req.params.id,
      groupPostData: JSON.stringify(groupPostData),
      groupPostString: groupPostString
    }, serviceTime);
  });
});

GroupRoutes.get('/create', dbcMiddleware.ensureAuthenticated, (req, res) => {
  res.render('group', {});
});

GroupRoutes.get('/:id', (req, res) => {
  var id = req.params.id;
  if (isNaN(id)) {
    res.status(404)        // HTTP status 404: NotFound
      .send('Not found');
  }
  else {
    res.render('group', {id});
  }
});

GroupRoutes.get('/:id', (req, res) => {
  var id = req.params.id;
  res.render('group', {id});
});

export default GroupRoutes;
<|endoftext|>


<commit_before>L.esri.Tasks.Task = L.Class.extend({
  //Generate a method for each methodName:paramName in the setters for this task.
  generateSetter: function(param, context){
    var isArray = param.match(/([a-zA-Z]+)\[\]/);

    param = (isArray) ? isArray[1] : param;

    if(isArray){
      return L.Util.bind(function(value){
        // this.params[param] = (this.params[param]) ? this.params[param] + ',' : '';
        if (L.Util.isArray(value)) {
          this.params[param] = value.join(',');
        } else {
          this.params[param] = value;
        }
        return this;
      }, context);
    } else {
      return L.Util.bind(function(value){
        this.params[param] = value;
        return this;
      }, context);
    }
  },

  initialize: function(endpoint){
    // endpoint can be either a url to an ArcGIS Rest Service or an instance of L.esri.Service
    if(endpoint instanceof L.esri.Service){
      this._service = endpoint;
      this.url = endpoint.url;
    } else {
      this.url = L.esri.Util.cleanUrl(endpoint);
    }

    // clone default params into this object
    this.params = L.Util.extend({}, this.params || {});

    // generate setter methods based on the setters object implimented a child class
    if(this.setters){
      for (var setter in this.setters){
        var param = this.setters[setter];
        this[setter] = this.generateSetter(param, this);
      }
    }
  },

  token: function(token){
    if(this._service){
      this._service.authenticate(token);
    } else {
      this.params.token = token;
    }
    return this;
  },

  request: function(callback, context){
    if(this._service){
      return this._service.request(this.path, this.params, callback, context);
    } else {
      return L.esri.request(this.url + this.path, this.params, callback, context);
    }
  }
});<commit_msg>fix service check in tasks<commit_after>L.esri.Tasks.Task = L.Class.extend({
  //Generate a method for each methodName:paramName in the setters for this task.
  generateSetter: function(param, context){
    var isArray = param.match(/([a-zA-Z]+)\[\]/);

    param = (isArray) ? isArray[1] : param;

    if(isArray){
      return L.Util.bind(function(value){
        // this.params[param] = (this.params[param]) ? this.params[param] + ',' : '';
        if (L.Util.isArray(value)) {
          this.params[param] = value.join(',');
        } else {
          this.params[param] = value;
        }
        return this;
      }, context);
    } else {
      return L.Util.bind(function(value){
        this.params[param] = value;
        return this;
      }, context);
    }
  },

  initialize: function(endpoint){
    // endpoint can be either a url to an ArcGIS Rest Service or an instance of L.esri.Service
    if(endpoint instanceof L.esri.Services.Service){
      this._service = endpoint;
      this.url = endpoint.url;
    } else {
      this.url = L.esri.Util.cleanUrl(endpoint);
    }

    // clone default params into this object
    this.params = L.Util.extend({}, this.params || {});

    // generate setter methods based on the setters object implimented a child class
    if(this.setters){
      for (var setter in this.setters){
        var param = this.setters[setter];
        this[setter] = this.generateSetter(param, this);
      }
    }
  },

  token: function(token){
    if(this._service){
      this._service.authenticate(token);
    } else {
      this.params.token = token;
    }
    return this;
  },

  request: function(callback, context){
    if(this._service){
      return this._service.request(this.path, this.params, callback, context);
    } else {
      return L.esri.request(this.url + this.path, this.params, callback, context);
    }
  }
});<|endoftext|>


<commit_before>'use strict';

/**
 * @ngdoc function
 * @name Pear2Pear.service:SwellRTSession
 * @description
 * # SwellRTSession service
 * Provides an API to handle SwellRT sessions and network events
 */

angular.module('Pear2Pear')
  .factory(
    'SwellRTSession', ['$q', '$timeout', 'SharedState', function($q, $timeout, SharedState){

      var swellRTDef = $q.defer();
      var swellRTpromise = swellRTDef.promise;

      window.onSwellRTReadyCalled = false;

      window.onSwellRTReady = function(){
        swellRTDef.resolve();
        window.onSwellRTReadyCalled = true;
      };

      if (window.SwellRT && !window.onSwellRTReadyCalled){
        window.onSwellRTReady();
      }

      var sessionConnected = false;
      var dataSync = true;
      var lastDataSync;
      var connecting = false;

      // TODO use this to handle fatal exceptions
      var setFatalExceptionHandler = function(handler){
        swellRTpromise.then(function(){
          SwellRT.on(SwellRT.events.FATAL_EXCEPTION, function(){
            $timeout(function(){
              sessionConnected = false;
            });
            handler();
          });
        });
      };

      var sessionDef = $q.defer();
      var users = {
        password: '$password$',
        current: function() {
          return window.localStorage.getItem('userId');
        },
        setCurrent: function(name) {
          var cleanedName = name ? name.trim() : name;

          return window.localStorage.setItem('userId', cleanedName);
        },
        isCurrent: function(user) {
          return user === users.current();
        },
        loggedIn: function() {
          return users.current() !== 'undefined' && users.current() !== null;
        },
        clearCurrent: function() {
          window.localStorage.removeItem('userId');
        }
      };

      var registerUser = function(userName, password, onSuccess, onError) {
        swellRTpromise.then(function(){
          SwellRT.registerUser(SwellRTConfig.server, userName, password, onSuccess, onError);
        });
      };

      var stopSession = function(){
        swellRTpromise.then(function(){
          SwellRT.stopSession();
          users.clearCurrent();
        });
      };

      swellRTpromise.then(function(){
        SwellRT.on(SwellRT.events.NETWORK_CONNECTED, function(){
          $timeout(function(){
            sessionConnected = true;
          });
        });

        SwellRT.on(SwellRT.events.NETWORK_DISCONNECTED, function(){
          $timeout(function(){
            sessionConnected = false;
          });
        });

        var dataStatusTimeout;

        SwellRT.on(SwellRT.events.DATA_STATUS_CHANGED, function(data){
          if (data.inFlightSize === 0 &&
              data.uncommittedSize === 0 &&
              data.unacknowledgedSize  === 0) {

            dataSync = true;
            lastDataSync = new Date();
            if (dataStatusTimeout){
              $timeout.cancel(dataStatusTimeout);
              dataStatusTimeout = undefined;
            }
          }
          else {
            if (!dataStatusTimeout){
              dataStatusTimeout = $timeout(function(){
                dataSync = false;
              }, 3000);
            }
          }
        });
      });
      // check variable connecting before calling startSession
      var startSession = function(userName, password, onSuccess, onError) {
        connecting = true;
        swellRTpromise.then(function(){
          if (sessionConnected) {
            if (userName && __session.address &&
                __session.address.split('@')[0] === userName.split('@')[0]) {
              return; // Session already started
            }
            // close other user's session
            else {
              stopSession();
            }
          }

          SwellRT.startSession(
            SwellRTConfig.server, userName || SwellRT.user.ANONYMOUS, password || '',
            function(){
              SwellRTConfig.swellrtServerDomain = __session.domain;
              if (userName){
                users.setCurrent(__session.address);
              } else {
                users.clearCurrent();
              }

              sessionDef.resolve(SwellRT);
              onSuccess();

              sessionConnected = true;
              connecting = false;
            }, function() {
              onError();
              connecting = false;
            });
        });
      };

      var autoStartSession = function(){
        var user, pass;

        if (users.current() !== null) {
          user = users.current();
          pass = users.password;
        }

        startSession(
          user, pass, function(){
            $timeout(
              function() {
              });
          },
          function(error) {
            console.log(error);
          });
      };

      function loginRequired(cb) {
        if (! users.loggedIn()) {
          SharedState.turnOn('shouldLoginSharedState');
          // Invoque $timout to refresh scope and actually show modal
          $timeout(function() {
            return;
          });
        } else {
          cb();
        }
      }

      return {
        users: users,
        registerUser: registerUser,
        startSession: startSession,
        stopSession: stopSession,
        loginRequired: loginRequired,
        setFatalExceptionHandler: setFatalExceptionHandler,
        // TODO no restart session without user saying so
        // TODO stop session before start session after a timeout
        // TODO if reconnected, get again all objects
        status: {
          isConnected: function(){
            return sessionConnected;
          },
          isDataSync: function(){
            return dataSync;
          },
          lastDataSync: function(){
            return lastDataSync;
          }
        },

        // TODO refactor with Prototype version of proxy objects to avoid the use of onLoad
        onLoad: function(f) {
          if (!sessionConnected && !connecting){
            autoStartSession();
          }
          sessionDef.promise.then(f);
        }
      };
    }]);
<commit_msg>Increase timeout<commit_after>'use strict';

/**
 * @ngdoc function
 * @name Pear2Pear.service:SwellRTSession
 * @description
 * # SwellRTSession service
 * Provides an API to handle SwellRT sessions and network events
 */

angular.module('Pear2Pear')
  .factory(
    'SwellRTSession', ['$q', '$timeout', 'SharedState', function($q, $timeout, SharedState){

      var swellRTDef = $q.defer();
      var swellRTpromise = swellRTDef.promise;

      window.onSwellRTReadyCalled = false;

      window.onSwellRTReady = function(){
        swellRTDef.resolve();
        window.onSwellRTReadyCalled = true;
      };

      if (window.SwellRT && !window.onSwellRTReadyCalled){
        window.onSwellRTReady();
      }

      var sessionConnected = false;
      var dataSync = true;
      var lastDataSync;
      var connecting = false;

      // TODO use this to handle fatal exceptions
      var setFatalExceptionHandler = function(handler){
        swellRTpromise.then(function(){
          SwellRT.on(SwellRT.events.FATAL_EXCEPTION, function(){
            $timeout(function(){
              sessionConnected = false;
            });
            handler();
          });
        });
      };

      var sessionDef = $q.defer();
      var users = {
        password: '$password$',
        current: function() {
          return window.localStorage.getItem('userId');
        },
        setCurrent: function(name) {
          var cleanedName = name ? name.trim() : name;

          return window.localStorage.setItem('userId', cleanedName);
        },
        isCurrent: function(user) {
          return user === users.current();
        },
        loggedIn: function() {
          return users.current() !== 'undefined' && users.current() !== null;
        },
        clearCurrent: function() {
          window.localStorage.removeItem('userId');
        }
      };

      var registerUser = function(userName, password, onSuccess, onError) {
        swellRTpromise.then(function(){
          SwellRT.registerUser(SwellRTConfig.server, userName, password, onSuccess, onError);
        });
      };

      var stopSession = function(){
        swellRTpromise.then(function(){
          SwellRT.stopSession();
          users.clearCurrent();
        });
      };

      swellRTpromise.then(function(){
        SwellRT.on(SwellRT.events.NETWORK_CONNECTED, function(){
          $timeout(function(){
            sessionConnected = true;
          });
        });

        SwellRT.on(SwellRT.events.NETWORK_DISCONNECTED, function(){
          $timeout(function(){
            sessionConnected = false;
          });
        });

        var dataStatusTimeout;

        SwellRT.on(SwellRT.events.DATA_STATUS_CHANGED, function(data){
          if (data.inFlightSize === 0 &&
              data.uncommittedSize === 0 &&
              data.unacknowledgedSize  === 0) {

            dataSync = true;
            lastDataSync = new Date();

            if (dataStatusTimeout){
              $timeout.cancel(dataStatusTimeout);
              dataStatusTimeout = undefined;
            }
          } else {
            if (!dataStatusTimeout){
              dataStatusTimeout = $timeout(function(){
                dataSync = false;
              }, 5000);
            }
          }
        });
      });
      // check variable connecting before calling startSession
      var startSession = function(userName, password, onSuccess, onError) {
        connecting = true;
        swellRTpromise.then(function(){
          if (sessionConnected) {
            if (userName && __session.address &&
                __session.address.split('@')[0] === userName.split('@')[0]) {
              return; // Session already started
            }
            // close other user's session
            else {
              stopSession();
            }
          }

          SwellRT.startSession(
            SwellRTConfig.server, userName || SwellRT.user.ANONYMOUS, password || '',
            function(){
              SwellRTConfig.swellrtServerDomain = __session.domain;
              if (userName){
                users.setCurrent(__session.address);
              } else {
                users.clearCurrent();
              }

              sessionDef.resolve(SwellRT);
              onSuccess();

              sessionConnected = true;
              connecting = false;
            }, function() {
              onError();
              connecting = false;
            });
        });
      };

      var autoStartSession = function(){
        var user, pass;

        if (users.current() !== null) {
          user = users.current();
          pass = users.password;
        }

        startSession(
          user, pass, function(){
            $timeout(
              function() {
              });
          },
          function(error) {
            console.log(error);
          });
      };

      function loginRequired(cb) {
        if (! users.loggedIn()) {
          SharedState.turnOn('shouldLoginSharedState');
          // Invoque $timout to refresh scope and actually show modal
          $timeout(function() {
            return;
          });
        } else {
          cb();
        }
      }

      return {
        users: users,
        registerUser: registerUser,
        startSession: startSession,
        stopSession: stopSession,
        loginRequired: loginRequired,
        setFatalExceptionHandler: setFatalExceptionHandler,
        // TODO no restart session without user saying so
        // TODO stop session before start session after a timeout
        // TODO if reconnected, get again all objects
        status: {
          isConnected: function(){
            return sessionConnected;
          },
          isDataSync: function(){
            return dataSync;
          },
          lastDataSync: function(){
            return lastDataSync;
          }
        },

        // TODO refactor with Prototype version of proxy objects to avoid the use of onLoad
        onLoad: function(f) {
          if (!sessionConnected && !connecting){
            autoStartSession();
          }
          sessionDef.promise.then(f);
        }
      };
    }]);
<|endoftext|>


<commit_before>/* @flow */

import type {Config} from './config';

import appendImportantToEachValue from './append-important-to-each-value';
import cssRuleSetToString from './css-rule-set-to-string';
import getState from './get-state';
import getStateKey from './get-state-key';
import hash from './hash';
import {isNestedStyle, mergeStyles} from './merge-styles';
import Plugins from './plugins/';

import ExecutionEnvironment from 'exenv';
import React from 'react';

const DEFAULT_CONFIG = {
  plugins: [
    Plugins.mergeStyleArray,
    Plugins.checkProps,
    Plugins.resolveMediaQueries,
    Plugins.resolveInteractionStyles,
    Plugins.keyframes,
    Plugins.visited,
    Plugins.removeNestedStyles,
    Plugins.prefix,
    Plugins.checkProps
  ]
};

// Gross
let globalState = {};

// Declare early for recursive helpers.
let resolveStyles = ((null: any): (
  component: any, // ReactComponent, flow+eslint complaining
  renderedElement: any,
  config: Config,
  existingKeyMap?: {[key: string]: bool},
  shouldCheckBeforeResolve: true
) => any);

const _shouldResolveStyles = function(component) {
  return (component.type && !component.type._isRadiumEnhanced);
};

const _resolveChildren = function({
  children,
  component,
  config,
  existingKeyMap
}) {
  if (!children) {
    return children;
  }

  const childrenType = typeof children;

  if (childrenType === 'string' || childrenType === 'number') {
    // Don't do anything with a single primitive child
    return children;
  }

  if (childrenType === 'function') {
    // Wrap the function, resolving styles on the result
    return function() {
      const result = children.apply(this, arguments);
      if (React.isValidElement(result)) {
        return resolveStyles(component, result, config, existingKeyMap, true);
      }
      return result;
    };
  }

  if (React.Children.count(children) === 1 && children.type) {
    // If a React Element is an only child, don't wrap it in an array for
    // React.Children.map() for React.Children.only() compatibility.
    const onlyChild = React.Children.only(children);
    return resolveStyles(component, onlyChild, config, existingKeyMap, true);
  }

  return React.Children.map(
    children,
    function(child) {
      if (React.isValidElement(child)) {
        return resolveStyles(component, child, config, existingKeyMap, true);
      }

      return child;
    }
  );
};

// Recurse over props, just like children
const _resolveProps = function({
  component,
  config,
  existingKeyMap,
  props
}) {
  let newProps = props;

  Object.keys(props).forEach(prop => {
    // We already recurse over children above
    if (prop === 'children') {
      return;
    }

    const propValue = props[prop];
    if (React.isValidElement(propValue)) {
      newProps = {...newProps};
      newProps[prop] = resolveStyles(
        component,
        propValue,
        config,
        existingKeyMap,
        true
      );
    }
  });

  return newProps;
};

const _buildGetKey = function({
  componentName,
  existingKeyMap,
  renderedElement
}) {
  // We need a unique key to correlate state changes due to user interaction
  // with the rendered element, so we know to apply the proper interactive
  // styles.
  const originalKey = typeof renderedElement.ref === 'string' ?
    renderedElement.ref :
    renderedElement.key;
  const key = getStateKey(originalKey);

  let alreadyGotKey = false;
  const getKey = function() {
    if (alreadyGotKey) {
      return key;
    }

    alreadyGotKey = true;

    if (existingKeyMap[key]) {
      let elementName;
      if (typeof renderedElement.type === 'string') {
        elementName = renderedElement.type;
      } else if (renderedElement.type.constructor) {
        elementName = renderedElement.type.constructor.displayName ||
          renderedElement.type.constructor.name;
      }

      throw new Error(
        'Radium requires each element with interactive styles to have a unique ' +
        'key, set using either the ref or key prop. ' +
        (originalKey ?
          'Key "' + originalKey + '" is a duplicate.' :
          'Multiple elements have no key specified.') + ' ' +
        'Component: "' + componentName + '". ' +
        (elementName ? 'Element: "' + elementName + '".' : '')
      );
    }

    existingKeyMap[key] = true;

    return key;
  };

  return getKey;
};

const _setStyleState = function(component, key, stateKey, value) {
  if (!component._radiumIsMounted) {
    return;
  }

  const existing = component._lastRadiumState ||
    component.state && component.state._radiumStyleState || {};

  const state = { _radiumStyleState: {...existing} };
  state._radiumStyleState[key] = {...state._radiumStyleState[key]};
  state._radiumStyleState[key][stateKey] = value;

  component._lastRadiumState = state._radiumStyleState;
  component.setState(state);
};

const _runPlugins = function({
  component,
  config,
  existingKeyMap,
  props,
  renderedElement
}) {
  // Don't run plugins if renderedElement is not a simple ReactDOMElement or has
  // no style.
  if (
    !React.isValidElement(renderedElement) ||
    typeof renderedElement.type !== 'string' ||
    !props.style
  ) {
    return props;
  }

  let newProps = props;

  const plugins = config.plugins || DEFAULT_CONFIG.plugins;

  const componentName = component.constructor.displayName ||
    component.constructor.name;
  const getKey = _buildGetKey({renderedElement, existingKeyMap, componentName});
  const getComponentField = key => component[key];
  const getGlobalState = key => globalState[key];
  const componentGetState = (stateKey, elementKey) =>
    getState(component.state, elementKey || getKey(), stateKey);
  const setState = (stateKey, value, elementKey) =>
    _setStyleState(component, elementKey || getKey(), stateKey, value);

  const addCSS = css => {
    const styleKeeper = component._radiumStyleKeeper ||
      component.context._radiumStyleKeeper;
    if (!styleKeeper) {
      if (__isTestModeEnabled) {
        return {remove() {}};
      }

      throw new Error(
        'To use plugins requiring `addCSS` (e.g. keyframes, media queries), ' +
          'please wrap your application in the StyleRoot component. Component ' +
          'name: `' + componentName + '`.',
      );
    }

    return styleKeeper.addCSS(css);
  };

  let newStyle = props.style;

  plugins.forEach(plugin => {
    const result = plugin({
      ExecutionEnvironment,
      addCSS,
      appendImportantToEachValue,
      componentName,
      config,
      cssRuleSetToString,
      getComponentField,
      getGlobalState,
      getState: componentGetState,
      hash,
      mergeStyles,
      props: newProps,
      setState,
      isNestedStyle,
      style: newStyle
    }) || {};

    newStyle = result.style || newStyle;

    newProps = result.props && Object.keys(result.props).length ?
      {...newProps, ...result.props} :
      newProps;

    const newComponentFields = result.componentFields || {};
    Object.keys(newComponentFields).forEach(fieldName => {
      component[fieldName] = newComponentFields[fieldName];
    });

    const newGlobalState = result.globalState || {};
    Object.keys(newGlobalState).forEach(key => {
      globalState[key] = newGlobalState[key];
    });
  });

  if (newStyle !== props.style) {
    newProps = {...newProps, style: newStyle};
  }

  return newProps;
};

// Wrapper around React.cloneElement. To avoid processing the same element
// twice, whenever we clone an element add a special prop to make sure we don't
// process this element again.
const _cloneElement = function(renderedElement, newProps, newChildren) {
  // Only add flag if this is a normal DOM element
  if (typeof renderedElement.type === 'string') {
    newProps = {...newProps, _radiumDidResolveStyles: true};
  }

  return React.cloneElement(renderedElement, newProps, newChildren);
};

//
// The nucleus of Radium. resolveStyles is called on the rendered elements
// before they are returned in render. It iterates over the elements and
// children, rewriting props to add event handlers required to capture user
// interactions (e.g. mouse over). It also replaces the style prop because it
// adds in the various interaction styles (e.g. :hover).
//
resolveStyles = function(
  component: any, // ReactComponent, flow+eslint complaining
  renderedElement: any, // ReactElement
  config: Config = DEFAULT_CONFIG,
  existingKeyMap?: {[key: string]: boolean},
  shouldCheckBeforeResolve: boolean = false,
): any { // ReactElement
  existingKeyMap = existingKeyMap || {};
  if (
    !renderedElement ||
    // Bail if we've already processed this element. This ensures that only the
    // owner of an element processes that element, since the owner's render
    // function will be called first (which will always be the case, since you
    // can't know what else to render until you render the parent component).
    (renderedElement.props && renderedElement.props._radiumDidResolveStyles) ||

    // Bail if this element is a radium enhanced element, because if it is,
    // then it will take care of resolving its own styles.
    (shouldCheckBeforeResolve && !_shouldResolveStyles(renderedElement))
  ) {
    return renderedElement;
  }

  const newChildren = _resolveChildren({
    children: renderedElement.props.children,
    component,
    config,
    existingKeyMap
  });

  let newProps = _resolveProps({
    component,
    config,
    existingKeyMap,
    props: renderedElement.props
  });

  newProps = _runPlugins({
    component,
    config,
    existingKeyMap,
    props: newProps,
    renderedElement
  });

  // If nothing changed, don't bother cloning the element. Might be a bit
  // wasteful, as we add the sentinal to stop double-processing when we clone.
  // Assume benign double-processing is better than unneeded cloning.
  if (
    newChildren === renderedElement.props.children &&
    newProps === renderedElement.props
  ) {
    return renderedElement;
  }

  return _cloneElement(
    renderedElement,
    newProps !== renderedElement.props ? newProps : {},
    newChildren
  );
};

// Only for use by tests
let __isTestModeEnabled = false;
if (process.env.NODE_ENV !== 'production') {
  resolveStyles.__clearStateForTests = function() {
    globalState = {};
  };
  resolveStyles.__setTestMode = function(isEnabled) {
    __isTestModeEnabled = isEnabled;
  };
}

export default resolveStyles;
<commit_msg>Replace _radiumDidResolveStyles with data-radium<commit_after>/* @flow */

import type {Config} from './config';

import appendImportantToEachValue from './append-important-to-each-value';
import cssRuleSetToString from './css-rule-set-to-string';
import getState from './get-state';
import getStateKey from './get-state-key';
import hash from './hash';
import {isNestedStyle, mergeStyles} from './merge-styles';
import Plugins from './plugins/';

import ExecutionEnvironment from 'exenv';
import React from 'react';

const DEFAULT_CONFIG = {
  plugins: [
    Plugins.mergeStyleArray,
    Plugins.checkProps,
    Plugins.resolveMediaQueries,
    Plugins.resolveInteractionStyles,
    Plugins.keyframes,
    Plugins.visited,
    Plugins.removeNestedStyles,
    Plugins.prefix,
    Plugins.checkProps
  ]
};

// Gross
let globalState = {};

// Declare early for recursive helpers.
let resolveStyles = ((null: any): (
  component: any, // ReactComponent, flow+eslint complaining
  renderedElement: any,
  config: Config,
  existingKeyMap?: {[key: string]: bool},
  shouldCheckBeforeResolve: true
) => any);

const _shouldResolveStyles = function(component) {
  return (component.type && !component.type._isRadiumEnhanced);
};

const _resolveChildren = function({
  children,
  component,
  config,
  existingKeyMap
}) {
  if (!children) {
    return children;
  }

  const childrenType = typeof children;

  if (childrenType === 'string' || childrenType === 'number') {
    // Don't do anything with a single primitive child
    return children;
  }

  if (childrenType === 'function') {
    // Wrap the function, resolving styles on the result
    return function() {
      const result = children.apply(this, arguments);
      if (React.isValidElement(result)) {
        return resolveStyles(component, result, config, existingKeyMap, true);
      }
      return result;
    };
  }

  if (React.Children.count(children) === 1 && children.type) {
    // If a React Element is an only child, don't wrap it in an array for
    // React.Children.map() for React.Children.only() compatibility.
    const onlyChild = React.Children.only(children);
    return resolveStyles(component, onlyChild, config, existingKeyMap, true);
  }

  return React.Children.map(
    children,
    function(child) {
      if (React.isValidElement(child)) {
        return resolveStyles(component, child, config, existingKeyMap, true);
      }

      return child;
    }
  );
};

// Recurse over props, just like children
const _resolveProps = function({
  component,
  config,
  existingKeyMap,
  props
}) {
  let newProps = props;

  Object.keys(props).forEach(prop => {
    // We already recurse over children above
    if (prop === 'children') {
      return;
    }

    const propValue = props[prop];
    if (React.isValidElement(propValue)) {
      newProps = {...newProps};
      newProps[prop] = resolveStyles(
        component,
        propValue,
        config,
        existingKeyMap,
        true
      );
    }
  });

  return newProps;
};

const _buildGetKey = function({
  componentName,
  existingKeyMap,
  renderedElement
}) {
  // We need a unique key to correlate state changes due to user interaction
  // with the rendered element, so we know to apply the proper interactive
  // styles.
  const originalKey = typeof renderedElement.ref === 'string' ?
    renderedElement.ref :
    renderedElement.key;
  const key = getStateKey(originalKey);

  let alreadyGotKey = false;
  const getKey = function() {
    if (alreadyGotKey) {
      return key;
    }

    alreadyGotKey = true;

    if (existingKeyMap[key]) {
      let elementName;
      if (typeof renderedElement.type === 'string') {
        elementName = renderedElement.type;
      } else if (renderedElement.type.constructor) {
        elementName = renderedElement.type.constructor.displayName ||
          renderedElement.type.constructor.name;
      }

      throw new Error(
        'Radium requires each element with interactive styles to have a unique ' +
        'key, set using either the ref or key prop. ' +
        (originalKey ?
          'Key "' + originalKey + '" is a duplicate.' :
          'Multiple elements have no key specified.') + ' ' +
        'Component: "' + componentName + '". ' +
        (elementName ? 'Element: "' + elementName + '".' : '')
      );
    }

    existingKeyMap[key] = true;

    return key;
  };

  return getKey;
};

const _setStyleState = function(component, key, stateKey, value) {
  if (!component._radiumIsMounted) {
    return;
  }

  const existing = component._lastRadiumState ||
    component.state && component.state._radiumStyleState || {};

  const state = { _radiumStyleState: {...existing} };
  state._radiumStyleState[key] = {...state._radiumStyleState[key]};
  state._radiumStyleState[key][stateKey] = value;

  component._lastRadiumState = state._radiumStyleState;
  component.setState(state);
};

const _runPlugins = function({
  component,
  config,
  existingKeyMap,
  props,
  renderedElement
}) {
  // Don't run plugins if renderedElement is not a simple ReactDOMElement or has
  // no style.
  if (
    !React.isValidElement(renderedElement) ||
    typeof renderedElement.type !== 'string' ||
    !props.style
  ) {
    return props;
  }

  let newProps = props;

  const plugins = config.plugins || DEFAULT_CONFIG.plugins;

  const componentName = component.constructor.displayName ||
    component.constructor.name;
  const getKey = _buildGetKey({renderedElement, existingKeyMap, componentName});
  const getComponentField = key => component[key];
  const getGlobalState = key => globalState[key];
  const componentGetState = (stateKey, elementKey) =>
    getState(component.state, elementKey || getKey(), stateKey);
  const setState = (stateKey, value, elementKey) =>
    _setStyleState(component, elementKey || getKey(), stateKey, value);

  const addCSS = css => {
    const styleKeeper = component._radiumStyleKeeper ||
      component.context._radiumStyleKeeper;
    if (!styleKeeper) {
      if (__isTestModeEnabled) {
        return {remove() {}};
      }

      throw new Error(
        'To use plugins requiring `addCSS` (e.g. keyframes, media queries), ' +
          'please wrap your application in the StyleRoot component. Component ' +
          'name: `' + componentName + '`.',
      );
    }

    return styleKeeper.addCSS(css);
  };

  let newStyle = props.style;

  plugins.forEach(plugin => {
    const result = plugin({
      ExecutionEnvironment,
      addCSS,
      appendImportantToEachValue,
      componentName,
      config,
      cssRuleSetToString,
      getComponentField,
      getGlobalState,
      getState: componentGetState,
      hash,
      mergeStyles,
      props: newProps,
      setState,
      isNestedStyle,
      style: newStyle
    }) || {};

    newStyle = result.style || newStyle;

    newProps = result.props && Object.keys(result.props).length ?
      {...newProps, ...result.props} :
      newProps;

    const newComponentFields = result.componentFields || {};
    Object.keys(newComponentFields).forEach(fieldName => {
      component[fieldName] = newComponentFields[fieldName];
    });

    const newGlobalState = result.globalState || {};
    Object.keys(newGlobalState).forEach(key => {
      globalState[key] = newGlobalState[key];
    });
  });

  if (newStyle !== props.style) {
    newProps = {...newProps, style: newStyle};
  }

  return newProps;
};

// Wrapper around React.cloneElement. To avoid processing the same element
// twice, whenever we clone an element add a special prop to make sure we don't
// process this element again.
const _cloneElement = function(renderedElement, newProps, newChildren) {
  // Only add flag if this is a normal DOM element
  if (typeof renderedElement.type === 'string') {
    newProps = {...newProps, 'data-radium': true};
  }

  return React.cloneElement(renderedElement, newProps, newChildren);
};

//
// The nucleus of Radium. resolveStyles is called on the rendered elements
// before they are returned in render. It iterates over the elements and
// children, rewriting props to add event handlers required to capture user
// interactions (e.g. mouse over). It also replaces the style prop because it
// adds in the various interaction styles (e.g. :hover).
//
resolveStyles = function(
  component: any, // ReactComponent, flow+eslint complaining
  renderedElement: any, // ReactElement
  config: Config = DEFAULT_CONFIG,
  existingKeyMap?: {[key: string]: boolean},
  shouldCheckBeforeResolve: boolean = false,
): any { // ReactElement
  existingKeyMap = existingKeyMap || {};
  if (
    !renderedElement ||
    // Bail if we've already processed this element. This ensures that only the
    // owner of an element processes that element, since the owner's render
    // function will be called first (which will always be the case, since you
    // can't know what else to render until you render the parent component).
    (renderedElement.props && renderedElement.props['data-radium']) ||

    // Bail if this element is a radium enhanced element, because if it is,
    // then it will take care of resolving its own styles.
    (shouldCheckBeforeResolve && !_shouldResolveStyles(renderedElement))
  ) {
    return renderedElement;
  }

  const newChildren = _resolveChildren({
    children: renderedElement.props.children,
    component,
    config,
    existingKeyMap
  });

  let newProps = _resolveProps({
    component,
    config,
    existingKeyMap,
    props: renderedElement.props
  });

  newProps = _runPlugins({
    component,
    config,
    existingKeyMap,
    props: newProps,
    renderedElement
  });

  // If nothing changed, don't bother cloning the element. Might be a bit
  // wasteful, as we add the sentinal to stop double-processing when we clone.
  // Assume benign double-processing is better than unneeded cloning.
  if (
    newChildren === renderedElement.props.children &&
    newProps === renderedElement.props
  ) {
    return renderedElement;
  }

  return _cloneElement(
    renderedElement,
    newProps !== renderedElement.props ? newProps : {},
    newChildren
  );
};

// Only for use by tests
let __isTestModeEnabled = false;
if (process.env.NODE_ENV !== 'production') {
  resolveStyles.__clearStateForTests = function() {
    globalState = {};
  };
  resolveStyles.__setTestMode = function(isEnabled) {
    __isTestModeEnabled = isEnabled;
  };
}

export default resolveStyles;
<|endoftext|>


<commit_before>import NotAuthorisedError from 'exceptions/notAuthorised';

import { Recipe } from 'models/recipe';

export default class {

    constructor( payload ) {
        this.id = payload.id;
        this.currentUserId = payload.userId;

        this.recipe = null;
    }

    execute() {
        return getRecipeFromDatabase.call( this )
            .bind( this )
            .then( setRecipe )
            .then( checkIfMyRecipe )
            .then( deleteRecipeAndOils )
            .then( returnRecipe );
    }
}

////////////////////
///// private

function getRecipeFromDatabase() {
    return Recipe
        .forge( { id: this.id } )
        .fetch( {
            require: true,
            withRelated: [ 'oils' ]
        } );
}

function setRecipe( recipe ) {
    this.recipe = recipe;
}

function checkIfMyRecipe() {
    if ( Number( this.currentUserId ) !== Number( this.recipe.get( 'user_id' ) ) ) {
        throw new NotAuthorisedError( 'Cannot delete: recipe owner mismatch.' );
    }
}

function deleteRecipeAndOils() {

    function deleteRecipeOils() {
        return this.recipe.oils().detach();
    }

    function deleteRecipe() {
        return this.recipe.destroy();
    }

    return deleteRecipeOils.call( this )
        .then( deleteRecipe.bind( this ) );
}

function returnRecipe() {
    return true;
}<commit_msg>fixes #24<commit_after>import bookshelf from 'db/bookshelf';
import Promise from 'bluebird';

import NotAuthorisedError from 'exceptions/notAuthorised';

import { Recipe } from 'models/recipe';

export default class {

    constructor( payload ) {
        this.id = payload.id;
        this.currentUserId = payload.userId;

        this.recipe = null;
        this.transaction = null;
    }

    execute() {
        return getRecipeFromDatabase.call( this )
            .bind( this )
            .then( setRecipe )
            .then( checkIfMyRecipe )
            .then( startTransaction )
            .then( deleteRecipeRelations )
            .then( commit )
            .then( done );
    }
}

////////////////////
///// private

function getRecipeFromDatabase() {
    return Recipe
        .forge( { id: this.id } )
        .fetch( {
            require: true,
            withRelated: [ 'comments', 'images', 'oils', 'recipeJournals.comments', 'recipeJournals.images' ]
        } );
}

function setRecipe( recipe ) {
    this.recipe = recipe;
}

function checkIfMyRecipe() {
    if ( Number( this.currentUserId ) !== Number( this.recipe.get( 'user_id' ) ) ) {
        throw new NotAuthorisedError( 'Cannot delete: recipe owner mismatch.' );
    }
}

function startTransaction() {
    return new Promise( resolve => {
        bookshelf.transaction( t => resolve( t ) );
    } )
        .then( transaction => this.transaction = transaction );
}

function deleteRecipeRelations() {

    return deleteRecipeOils.call( this )
        .bind( this )
        .then( deleteComments )
        .then( deleteImages )
        .then( deleteJournalComments )
        .then( deleteJournalImages )
        .then( deleteJournals )
        .then( deleteRecipe );

    function deleteRecipeOils() {
        return this.recipe.oils().detach(null,  { transacting: this.transaction } );
    }

    function deleteComments() {
        return this.recipe
            .related( 'comments' )
            .invokeThen( 'destroy', { transacting: this.transaction } );
    }

    function deleteImages() {
        return this.recipe
            .related( 'images' )
            .invokeThen( 'destroy', { transacting: this.transaction } );
    }

    function deleteJournalImages() {
        return this.recipe
            .related( 'recipeJournals' )
            .each( journal => journal.related( 'comments' ).invokeThen( 'destroy', { transacting: this.transaction } ) );
    }

    function deleteJournalComments() {
        return this.recipe
            .related( 'recipeJournals' )
            .each( journal => journal.related( 'images' ).invokeThen( 'destroy', { transacting: this.transaction } ) );
    }

    function deleteJournals() {
        return this.recipe
            .related( 'recipeJournals' )
            .invokeThen( 'destroy', { transacting: this.transaction } );
    }

    function deleteRecipe() {
        return this.recipe.destroy( { transacting: this.transaction } );
    }

}

function commit() {
    this.transaction.commit();
}

function done() {
    return true;
}<|endoftext|>


<commit_before>'use strict';

module.exports = function (grunt) {
  var _ = require('lodash');
  var Q = require('q');
  var utils = require('./utils')(grunt);
  var Job = require('./Job')(grunt);
  var WrapperError = require('./WrapperError');

  Q.longStackSupport = true;

  /**
   * Test runner.
   *
   * @constructor
   * @param {Object} properties - Configuration options.
   * @param {String} framework - The unit test framework's name. Can be 'yasmine', 'qunit',
   *   'YUI Test', 'mocha' or 'custom'.
   * @param {Function} onProgress - Progress handler.
   */
  var TestRunner = function (properties, framework, onProgress) {
    this.user = properties.username;
    this.key = properties.key;
    this.pollInterval = properties.pollInterval;
    this.statusCheckAttempts = properties.statusCheckAttempts;
    this.framework = framework;
    this.tunneled = properties.tunneled;
    this.tunnelId = properties.identifier;
    this.testName = properties.testname;
    this.build = properties.build;
    this.public = properties.public;
    this.tags = properties.tags;
    this.sauceConfig = properties.sauceConfig;
    this.onTestComplete = properties.onTestComplete;
    this.throttled = properties.throttled;
    this.browsers = properties.browsers;
    this.urls = properties.url || properties.urls;
    this.maxRetries = properties.maxRetries;
    this.onProgress = onProgress;

    if (properties['max-duration']) {
      // max-duration is actually a sauce selenium capability
      this.sauceConfig['max-duration'] = properties['max-duration'];
    }
    this.urls = this.urls.length !== undefined ? this.urls : [this.urls];
    this.numberOfJobs = this.browsers.length * this.urls.length;
    this.startedJobs = 0;
  };

  /**
  * Reports progress.
  * @param {Object} progress - Progress data.
  */
  TestRunner.prototype.reportProgress = function (progress) {
    if (this.onProgress) {
      this.onProgress(progress);
    }
  };

  /**
   * Runs the test in all of the browsers-URL combinations.
   *
   * @returns {Object} - A promise which will be eventually resolved with the test results
   *   (a boolean). Progress is reported after each job is started and completed.
   */
  TestRunner.prototype.runTests = function () {
    var me = this;
    var throttledRunTest, promises;

    throttledRunTest = utils.limitConcurrency(this.runTest.bind(this), this.throttled || Number.MAX_VALUE);

    promises = this.urls
      .map(function (url) {
        return this.browsers.map(function (browser) {
          return throttledRunTest(browser, url);
        });
      }, this)
      .reduce(function (acc, promisesForUrl) {
        return acc.concat(promisesForUrl);
      }, []);

    return Q
      .all(promises)
      .then(function (results) {
        var passed = results.indexOf(false) === -1;

        me.reportProgress({
          type: 'testCompleted',
          passed: passed
        });

        return passed;
      });
  };

  /**
   * Runs a test with the specified URL in the specified environment.
   *
   * @param {Object} browser - The environment to run the test on.
   * @param {String} url - An URL that will be loaded in the browsers.
   * @returns {Object} - A promise which will be eventually resolved with the test results
   *   (a boolean). Progress is reported after the job is started and completed.
   */
  TestRunner.prototype.runTest = function (browser, url) {
    var me = this;
    var retry = 0;

    function getResult() {
      var job = new Job(me, url, browser);

      return job
        .start()
        .then(function () { return job.getResult(); })
        .then(function (result) {
          // when a test times out then the result property is a string
          if (result.result &&
            result.result.indexOf &&
            result.result.indexOf('Test exceeded maximum duration') > -1 &&
            retry < me.maxRetries) {
            retry += 1;

            me.reportProgress({
              type: 'retrying',
              url: url,
              browser: browser
            });

            return job
              .stop()
            // Try to delete the timed out job in order to prevent the SauceLabs badge to
            // indicate failure. The del() function can also fail, in that case swallow the
            // exception.
              .then(function () { return job.del().fail(_.noop); })
              .then(getResult);
          }
          return result;
        });
    }

    return Q
      .fcall(function () {
        me.startedJobs += 1;
        me.reportProgress({
          type: 'jobStarted',
          numberOfJobs: me.numberOfJobs,
          startedJobs: me.startedJobs
        });
      })
      .then(getResult)
      .then(function (result) {
        if (me.onTestComplete) {
          var clone = _.clone(result, true);
          return Q
            .nfcall(me.onTestComplete, clone)
            .then(
              function (passed) {
                if (passed !== undefined) {
                  result.passed = !!passed;
                }
                return result;
              },
              function (error) {
                throw new WrapperError('onTestComplete raised an error.', error);
              }
            );
        }
        return result;
      })
      .then(function (result) {
        me.reportProgress({
          type: 'jobCompleted',
          url: url,
          jobUrl: result.url,
          platform: result.platform,
          passed: result.passed,
          tunnelId: me.tunnelId
        });

        return result.passed;
      });
  };

  return TestRunner;
};
<commit_msg>fix typo<commit_after>'use strict';

module.exports = function (grunt) {
  var _ = require('lodash');
  var Q = require('q');
  var utils = require('./utils')(grunt);
  var Job = require('./Job')(grunt);
  var WrapperError = require('./WrapperError');

  Q.longStackSupport = true;

  /**
   * Test runner.
   *
   * @constructor
   * @param {Object} properties - Configuration options.
   * @param {String} framework - The unit test framework's name. Can be 'jasmine', 'qunit',
   *   'YUI Test', 'mocha' or 'custom'.
   * @param {Function} onProgress - Progress handler.
   */
  var TestRunner = function (properties, framework, onProgress) {
    this.user = properties.username;
    this.key = properties.key;
    this.pollInterval = properties.pollInterval;
    this.statusCheckAttempts = properties.statusCheckAttempts;
    this.framework = framework;
    this.tunneled = properties.tunneled;
    this.tunnelId = properties.identifier;
    this.testName = properties.testname;
    this.build = properties.build;
    this.public = properties.public;
    this.tags = properties.tags;
    this.sauceConfig = properties.sauceConfig;
    this.onTestComplete = properties.onTestComplete;
    this.throttled = properties.throttled;
    this.browsers = properties.browsers;
    this.urls = properties.url || properties.urls;
    this.maxRetries = properties.maxRetries;
    this.onProgress = onProgress;

    if (properties['max-duration']) {
      // max-duration is actually a sauce selenium capability
      this.sauceConfig['max-duration'] = properties['max-duration'];
    }
    this.urls = this.urls.length !== undefined ? this.urls : [this.urls];
    this.numberOfJobs = this.browsers.length * this.urls.length;
    this.startedJobs = 0;
  };

  /**
  * Reports progress.
  * @param {Object} progress - Progress data.
  */
  TestRunner.prototype.reportProgress = function (progress) {
    if (this.onProgress) {
      this.onProgress(progress);
    }
  };

  /**
   * Runs the test in all of the browsers-URL combinations.
   *
   * @returns {Object} - A promise which will be eventually resolved with the test results
   *   (a boolean). Progress is reported after each job is started and completed.
   */
  TestRunner.prototype.runTests = function () {
    var me = this;
    var throttledRunTest, promises;

    throttledRunTest = utils.limitConcurrency(this.runTest.bind(this), this.throttled || Number.MAX_VALUE);

    promises = this.urls
      .map(function (url) {
        return this.browsers.map(function (browser) {
          return throttledRunTest(browser, url);
        });
      }, this)
      .reduce(function (acc, promisesForUrl) {
        return acc.concat(promisesForUrl);
      }, []);

    return Q
      .all(promises)
      .then(function (results) {
        var passed = results.indexOf(false) === -1;

        me.reportProgress({
          type: 'testCompleted',
          passed: passed
        });

        return passed;
      });
  };

  /**
   * Runs a test with the specified URL in the specified environment.
   *
   * @param {Object} browser - The environment to run the test on.
   * @param {String} url - An URL that will be loaded in the browsers.
   * @returns {Object} - A promise which will be eventually resolved with the test results
   *   (a boolean). Progress is reported after the job is started and completed.
   */
  TestRunner.prototype.runTest = function (browser, url) {
    var me = this;
    var retry = 0;

    function getResult() {
      var job = new Job(me, url, browser);

      return job
        .start()
        .then(function () { return job.getResult(); })
        .then(function (result) {
          // when a test times out then the result property is a string
          if (result.result &&
            result.result.indexOf &&
            result.result.indexOf('Test exceeded maximum duration') > -1 &&
            retry < me.maxRetries) {
            retry += 1;

            me.reportProgress({
              type: 'retrying',
              url: url,
              browser: browser
            });

            return job
              .stop()
            // Try to delete the timed out job in order to prevent the SauceLabs badge to
            // indicate failure. The del() function can also fail, in that case swallow the
            // exception.
              .then(function () { return job.del().fail(_.noop); })
              .then(getResult);
          }
          return result;
        });
    }

    return Q
      .fcall(function () {
        me.startedJobs += 1;
        me.reportProgress({
          type: 'jobStarted',
          numberOfJobs: me.numberOfJobs,
          startedJobs: me.startedJobs
        });
      })
      .then(getResult)
      .then(function (result) {
        if (me.onTestComplete) {
          var clone = _.clone(result, true);
          return Q
            .nfcall(me.onTestComplete, clone)
            .then(
              function (passed) {
                if (passed !== undefined) {
                  result.passed = !!passed;
                }
                return result;
              },
              function (error) {
                throw new WrapperError('onTestComplete raised an error.', error);
              }
            );
        }
        return result;
      })
      .then(function (result) {
        me.reportProgress({
          type: 'jobCompleted',
          url: url,
          jobUrl: result.url,
          platform: result.platform,
          passed: result.passed,
          tunnelId: me.tunnelId
        });

        return result.passed;
      });
  };

  return TestRunner;
};
<|endoftext|>