1 //** Start JS Lint Settings **
  2 /*globals self,Worker,Blob,rtn*/
  3 /*jslint vars:true, devel:true, browser:true, evil:true*/
  4 //** End JS Lint Settings **
  5 
  6 /*
  7 * Title: WebHamsters
  8 * Description: Javascript library to add multi-threading support to javascript by exploiting concurrent web workers
  9 * Author: Austin K. Smith
 10 * Contact: austin@asmithdev.com
 11 * Copyright: 2015 Austin K. Smith - austin@asmithdev.com
 12 * License: Artistic License 2.0
 13 */
 14 var hamsters = {
 15   version: '2.7',
 16   debug: false,
 17   cache: false,
 18   maxThreads: Math.ceil((navigator.hardwareConcurrency || 1) * 1.25),
 19   tools: {},
 20   wheel: {
 21     legacy: false,
 22     queue: {
 23       running: [],
 24       pending: []
 25     }, 
 26     tasks: [],
 27     errors: [],
 28     setup: {}
 29   }
 30 };
 31 
 32 /**
 33  * @description: Initializes and sets up library functionality
 34  * @method wakeUp
 35  * @return
 36  */
 37 hamsters.wheel.wakeUp = function() {
 38   "use strict";
 39 
 40   /**
 41    * @description: Detect Internet Explorer by Version IE10 and below
 42    * @method isIE
 43    * @param {integer} version
 44    * @return CallExpression
 45    */
 46   hamsters.tools.isIE = function(version) {
 47     return (new RegExp('msie' + (!isNaN(version) ? ('\\s'+version) : ''), 'i').test(navigator.userAgent));
 48   };
 49 
 50   /**
 51    * Description
 52    * @description: Detect browser support for web workers
 53    * @method isLegacy
 54    * @return
 55    */
 56   hamsters.wheel.setup.isLegacy = function(callback) {
 57     if(!window.Worker || navigator.userAgent.indexOf('Kindle/3.0') !== -1 || navigator.userAgent.indexOf('Mobile/8F190') !== -1  || navigator.userAgent.indexOf('IEMobile') !== -1  || hamsters.tools.isIE(10)) {
 58       hamsters.wheel.legacy = true;
 59     } else if(navigator.userAgent.toLowerCase().indexOf('firefox') !== -1) {
 60       window.firefox = window.firefox || true;
 61     }
 62     if(callback) {
 63       callback(hamsters.wheel.legacy);
 64     }
 65   };
 66 
 67   /**
 68    * @description: Return browser support for library
 69    * @method isLegacy
 70    * @return MemberExpression
 71    */
 72   hamsters.isLegacy = function() {
 73     return hamsters.wheel.legacy;
 74   };
 75 
 76   /**
 77    * @description: Method for checking wheel error log
 78    * @method checkErrors
 79    * @return ObjectExpression
 80    */
 81   hamsters.checkErrors = function() {
 82     var errors = hamsters.wheel.errors || [];
 83     return {
 84       'msg': 'There are currently ' + errors.length + ' errors captured in the wheel',
 85       'total': errors.length,
 86       'errors': errors
 87     };
 88   };
 89   
 90   /**
 91    * @description: Splits an array into equal sized subarrays for individual workers
 92    * @constructor
 93    * @method splitArray
 94    * @param {array} array - incoming array to be split
 95    * @param {integer} n - total subarrays  
 96    * @return ArrayExpression
 97    */
 98   hamsters.tools.splitArray = function(array, n) {
 99     if(array.length && !array.slice) {
100       array = hamsters.wheel.normalizeArray(array);
101     }
102     var tasks = [];
103     var i = 0;
104     if(array) {
105       var len = array.length;
106       var size = Math.ceil(len/n);
107       while (i < len) {
108         tasks.push(array.slice(i, i += size));
109       }
110       return tasks;
111     }
112     return [];
113   };
114 
115   /**
116    * @description: Generates a worker which generates an array of random numbers for testing
117    * @constructor
118    * @function randomArray
119    * @method randomArray
120    * @param {integer} count - array size
121    * @param {function} callback - callback when array ready
122    * @return 
123    */
124   hamsters.tools.randomArray = function(count, callback) {
125     if(!count || !callback) {
126       hamsters.wheel.errors.push({
127         'msg': 'Unable to generate random array, missing required params'
128       });
129       return;
130     }
131     var params = {
132       'count': count
133     };
134     hamsters.run(params, function() {
135       var total = params.count;
136       var i = 0;
137       while(i < total) {
138         rtn.data[rtn.data.length] = Math.round(Math.random() * (100 - 1) + 1);
139         i+=1;
140       }
141     }, function(output) {
142       if(callback) {
143         callback(output);
144       }
145     }, 1, false, 'Int32', false);
146   };
147 
148    /**
149    * Description
150    * @method checkCache
151    * @param {string} fn
152    * @param {array} input
153    * @param {string} dataType
154    * @return 
155    */
156   hamsters.wheel.checkCache = function(hash, dataType) {
157     var item;
158     for (var i = 0, len = sessionStorage.length; i < len; i+=1) {
159       item = JSON.parse(sessionStorage[i]);
160       if(item && item['#'] === hash && item.dT === dataType) {
161         var rtn = item.oP;
162         if(dataType && !hamsters.wheel.legacy) {
163           rtn = hamsters.wheel.processDataType(dataType, item.oP);
164         }
165         return rtn;
166       }
167     }
168   };
169 
170   /**
171    * Description
172    * @method memoize
173    * @param {string} fn
174    * @param {array} input
175    * @param {array} output
176    * @param {string} dataType
177    * @return 
178    */
179   hamsters.wheel.memoize = function(fn, input, output, dataType) {
180     var hash = hamsters.wheel.hashResult({'func': fn, 'dT': dataType, 'input': input});
181     if(hamsters.wheel.checkCache(hash, dataType)) {
182       return;
183     }
184     try {
185       sessionStorage.setItem(sessionStorage.length, JSON.stringify({'#': hash,  'dT': dataType, 'oP': output}));
186     } catch(eve) {
187       if(eve.name === 'QuotaExceededError') {
188         sessionStorage.clear();
189         try {
190           sessionStorage.setItem(sessionStorage.length, JSON.stringify({'#': hash,  'dT': dataType, 'oP': output}));
191         } catch(e) { //Do nothing, can't cache this result..too large
192           return;
193         }
194       }
195     }
196   };
197 
198   /**
199    * Description
200    * @method compareArrays
201    * @param {array} array1
202    * @param {array} array2
203    * @return CallExpression
204    */
205   hamsters.wheel.compareArrays = function (array1, array2) {
206       if (array1.length !== array2.length) {
207           return false;
208       }
209       return array1.every(function (el, i) {
210           return (el === array2[i]);
211       });
212   };
213 
214   /**
215    * @description: Setups dom objects for web worker use with library boilerplate
216    * @constructor
217    * @function populateElements
218    * @method populateElements
219    * @param {integer} count
220    * @param {function} callback - optional callback once thread boilerplates setup
221    * @return 
222    */
223   hamsters.wheel.setup.populateElements = function(count, callback) {
224     for (var i = 0, len = count; i < len; i+=1) {
225       hamsters.wheel.setup.getOrCreateElement(i);
226     }
227     if(callback) {
228       callback.call();
229     }
230   };
231   
232   /**
233    * @description: Setups dom objects for web worker use with library boilerplate
234    * @constructor
235    * @method getOrCreateElement
236    * @param {integer} id - thread # to populate 
237    * @return script
238    */
239   hamsters.wheel.setup.getOrCreateElement = function(id) {
240     var script = (document.getElementById('hamster'+id) || null);
241     if(!script) {
242       var work = hamsters.wheel.giveHamsterWork();
243       script = document.createElement('script');
244       script.type = ('javascript/worker');
245       script.id = ('hamster'+id);
246       script.text = '(' + String(work) + '());';
247       document.getElementsByTagName('head')[0].appendChild(script);
248       return script;
249     }
250     return script;
251   };
252   
253   /**
254    * @description: Creates boiler plate logic for worker thread
255    * @constructor
256    * @method giveHamsterWork
257    * @return work
258    */
259   hamsters.wheel.giveHamsterWork = function() {
260     var work = function() {
261       var params;
262       /**
263        * Description
264        * @method respond
265        * @param {object} rtn
266        * @param {string} msg
267        * @return 
268        */
269       var respond = function(rtn, msg) {
270         if(params.dataType && params.dataType !== "na") {
271           var output = processDataType(params.dataType, rtn.data);
272           rtn.data = output.buffer;
273           rtn.dataType = params.dataType;
274           self.postMessage({
275             'results': rtn || null,
276             'msg': msg || ''
277           }, [output.buffer]);
278         } else {
279           self.postMessage({
280             'results': rtn || null,
281             'msg': msg || ''
282           });
283         }
284       };
285       /**
286        * Description
287        * @method processDataType
288        * @param {string} dataType
289        * @param {array} buffer
290        * @return arr
291        */
292       var processDataType = function(dataType, buffer) {
293         if (dataType === 'uint32') {
294           return new Uint32Array(buffer);
295         }
296         if (dataType === 'uint16') {
297           return new Uint16Array(buffer);
298         }
299         if (dataType === 'uint8') {
300           return new Uint8Array(buffer);
301         }
302         if (dataType === 'uint8clamped') {
303           return new Uint8ClampedArray(buffer);
304         }
305         if (dataType === 'int32') {
306           return new Int32Array(buffer);
307         }
308         if (dataType === 'int16') {
309           return new Int16Array(buffer);
310         }
311         if (dataType === 'int8') {
312           return new Int8Array(buffer);
313         }
314         if (dataType === 'float32') {
315           return new Float32Array(buffer);
316         }
317         if (dataType === 'float64') {
318           return new Float64Array(buffer);
319         }
320         return buffer;
321       };
322 
323       /**
324        * Description
325        * @method onmessage
326        * @param {object} e
327        * @return 
328        */
329       self.onmessage = function(e) {
330         var rtn = {
331           'success': true, 
332           'data': []
333         };
334         params = e.data;
335         if(typeof params === 'string') {
336           params = JSON.parse(e.data);
337         }
338         if(params.dataType && params.dataType !== "na" && typeof params.array === 'object') {
339           params.array = processDataType(params.dataType, params.array);
340         }
341         if(params.fn) {
342           var fn = eval('('+params.fn+')');
343           if(fn && typeof fn === 'function') {
344             fn();
345             respond(rtn);
346           } else {
347             rtn.success = false;
348             rtn.error = 'Missing function';
349             rtn.msg = 'Error encounted check errors for details';
350             respond(rtn);
351           }
352         }
353       };
354     };
355     return work;
356   };
357 
358   /**
359    * @description: Sorts an array of objects based on incoming property param
360    * @constructor
361    * @method sort
362    * @param {string} property - property to sort by
363    * @return FunctionExpression
364    */
365   hamsters.wheel.sort = function(arr, order) {
366     if(order === 'desc') {
367       return Array.prototype.sort.call(arr, function(a, b) {
368         return b - a; 
369      });
370     } 
371     if(order === 'asc') {
372       return Array.prototype.sort.call(arr, function(a, b) {
373         return a - b; 
374      });
375     }
376     if(order === 'ascAlpha') {
377       return arr.sort();
378     }
379     if(order === 'descAlpha') {
380       return arr.reverse();
381     }
382   };
383 
384   /**
385    * @description: Takes an incoming sequential function and automatically splits the work across as many defined threads for paralell execution
386    * @constructor
387    * @method run
388    * @param {object} params - incoming params object for task
389    * @param {function} fn - Sequential function to execute
390    * @param {function} callback - task callback when all threads complete
391    * @param {integer} workers - total number of threads to use
392    * @param {boolean} aggregate - boolean aggregate individual thread outputs into final array  
393    * @param {string} dataType
394    * @return 
395    */
396   hamsters.run = function(params, fn, callback, workers, aggregate, dataType, memoize, order) {
397     if(!params || !fn) {
398       return 'Error processing for loop, missing params or function';
399     }
400     var taskid = hamsters.wheel.tasks.length;
401     workers = workers || hamsters.maxThreads;
402     hamsters.wheel.newTask(taskid, workers, order, dataType, fn, callback);
403     var task = hamsters.wheel.tasks[taskid];
404     callback = (callback || null);
405     var hamsterfood = {'array':[]};
406     hamsterfood.fn = fn.toString();
407     if(dataType) {
408       dataType = dataType.toLowerCase();
409     } else {
410       dataType = "na";
411     }
412     if(hamsters.cache && params.array && params.array.length !== 0) {
413       memoize = memoize || true;
414       var hash = hamsters.wheel.hashResult({'func': fn, 'dT': "na", 'input': params.array});
415       var result = hamsters.wheel.checkCache(hash, dataType);
416       if(result && callback) {
417         setTimeout(function() {
418           callback(result);
419           hamsters.wheel.tasks[taskid] = null; //Clean up our task, not needed any longer
420         }, 4);
421         return;
422       }
423     }
424     var key;
425     for(key in params) {
426       if(params.hasOwnProperty(key)) {
427         if(key !== 'array') {
428           hamsterfood[key] = params[key];
429         }
430       }
431     }
432     hamsterfood.dataType = dataType || null;
433     var workArray = params.array || null;
434     if(params.array && task.threads !== 1) {
435       workArray = hamsters.tools.splitArray(params.array, task.threads); //Divide our array into equal array sizes
436     }
437     var i = 0;
438     while(i < task.threads) {
439       if(workArray && task.threads !== 1) {
440         hamsters.wheel.newWheel(workArray[i], hamsterfood, aggregate, callback, taskid, i, null, null, memoize);
441       } else {
442         hamsters.wheel.newWheel(workArray, hamsterfood, aggregate, callback, taskid, i, null, null, memoize);
443       }
444       i+=1;
445     }
446   };
447 
448   hamsters.wheel.newTask = function(taskid, workers, order, dataType, fn, callback) {
449     hamsters.wheel.tasks.push({
450       'id': taskid,
451       'workers': [],
452       'count': 0,
453       'threads': workers, 
454       'input': [],
455       'dataType': dataType || null,
456       'fn': fn || null,
457       'output': [], 
458       'order': order || null,
459       'callback': callback
460     });
461   };
462 
463   hamsters.wheel.trackInput = function(task, inputArray, thread, taskid, hamsterfood) {
464     task.input.push({ 
465       'input': inputArray,
466       'workerid': thread, 
467       'taskid': taskid, 
468       'params': hamsterfood, 
469       'start': new Date().getTime()
470     });
471   };
472 
473   hamsters.wheel.poolThread = function(queue, inputArray, hamsterfood, threadid, callback, taskid, aggregate, memoize) {
474     queue.pending.push({
475       'memoize': memoize,
476       'input': inputArray,
477       'params': hamsterfood,
478       'workerid': threadid, 
479       'callback': callback, 
480       'taskid': taskid, 
481       'aggregate': aggregate
482     });
483   };
484 
485   hamsters.wheel.trackThread = function(task, queue, thread) {
486     task.workers[task.workers.length] = thread; //Keep track of threads scoped to current task
487     queue.running[queue.running.length] = thread; //Keep track of all currently running threads
488   };
489 
490   /**
491    * @description: Simulates threading for execution on devices that don't support workers
492    * @constructor
493    * @method legacyProcessor
494    * @param {object} food - Input params object
495    * @param {array} inputArray - Input array
496    * @param {function} callback - Callback function to return response
497    * @return 
498    */
499   hamsters.wheel.legacyProcessor = function(food, inputArray, callback) {
500     setTimeout(function() {
501       var params = food;
502       var rtn = {
503         'success': true, 
504         'data': []
505       };
506       var respond = function(rtn) {
507         if(callback) {
508           callback(rtn); // Return legacy output
509         }
510       };
511       if(params.fn) {
512         params.array = inputArray;
513         var fn = eval('('+params.fn+')');
514         if(fn && typeof fn === 'function') {
515           try {
516             fn();
517             respond(rtn);
518           } catch(exception) {
519             rtn.success = false;
520             rtn.error = exception;
521             rtn.msg = 'Error encounted check errors for details';
522             respond(rtn);
523           }
524         }
525       }
526     }, 4); //4ms delay (HTML5 spec minimum), simulate threading
527   };
528 
529   /**
530    * @description: Creates web worker thread
531    * @constructor
532    * @method createHamster
533    * @param {integer} thread - Thread #
534    * @return ObjectExpression
535    */
536   hamsters.wheel.createHamster = function(thread) {
537     var hamster = hamsters.wheel.setup.getOrCreateElement(thread);
538     var blob = hamsters.wheel.createBlob(hamster.textContent);
539     var uri = window.URL.createObjectURL(blob);
540     return {'worker': new Worker(uri), 'dataBlob': blob, 'blobUri': uri};
541   };
542 
543   /**
544    * @description: Sends ajax request 
545    * @constructor
546    * @method sendRequest
547    * @param {string} type - 'GET' || 'POST'
548    * @param {string} url - Url to connect to
549    * @param {string} responseType - 'ArrayBuffer','','blob','document','json','text'
550    * @param {string} callback - Callback to return response
551    * @return 
552    */
553   hamsters.wheel.sendRequest = function(type, url, responseType, callback) {
554     var xhr = new XMLHttpRequest();
555     xhr.open(type, url);
556     xhr.responseType = responseType;
557     /**
558      * Description
559      * @method onload
560      * @return 
561      */
562     xhr.onload = function() {
563       callback(this.response);
564     };
565     xhr.send();
566   };
567 
568   /**
569    * @description: Creates array buffer by issuing a fake ajax request with response of arraybuffer
570    * @constructor
571    * @method fetchArrayBuffer
572    * @param {string} string - input params object
573    * @param {function} callback - callback function to return buffer to
574    * @return 
575    */
576   hamsters.wheel.fetchArrayBuffer = function(string, callback) {
577     var url = window.URL.createObjectURL(hamsters.wheel.createBlob(string));
578     hamsters.wheel.sendRequest('GET', url, 'arraybuffer', function(arrayBuffer) {
579       if(callback) {
580         callback(arrayBuffer);
581       }
582     });
583   };
584 
585   /**
586    * @description: Creates dataBlob for worker generation
587    * @constructor
588    * @method createBlob
589    * @param {string} textContent - Web worker boiler plate
590    * @return blob
591    */
592   hamsters.wheel.createBlob = function(textContent) {
593     if(Blob) {
594       return new Blob([textContent], {type: 'application/javascript'});
595     }
596     var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder;
597     if(BlobBuilder) { //Fallback for browsers that don't support blob constructor
598       var blob = new BlobBuilder();
599       blob.append([textContent], {type: 'application/javascript'});
600       blob = blob.getBlob();
601       return blob;
602     }
603   };
604 
605   /**
606    * @description: Aggregates individual hamster outputs into a single array
607    * @constructor
608    * @method aggregate
609    * @param {array} input - incoming array of subarrays
610    * @param {string} dataType
611    * @return output
612    */
613   hamsters.tools.aggregate = function(input, dataType) {
614     if(!input) {
615       console.error("Missing array");
616       return;
617     }
618     if(input.length > 20) {
619       return input;
620     }
621     if(dataType) {
622       return hamsters.wheel.aggregateTypedArrays(input, dataType);
623     }
624     return input.reduce(function(a, b) {
625       return a.concat(b);
626     });
627   };
628 
629   /**
630    * @description: Get our nested output values from each task, return array of subarrays
631    * @constructor
632    * @method getOutput
633    * @param {array} output - incoming task output
634    * @param {boolean} aggregate
635    * @param {string} dataType
636    * @return rtn
637    */
638   hamsters.wheel.getOutput = function(output, aggregate, dataType) {
639     if(aggregate) {
640       return hamsters.tools.aggregate(output, dataType);
641     } else {
642       return output;
643     }
644   };
645 
646   /**
647    * @description: Process next item in queue
648    * @constructor
649    * @method processQueue
650    * @param {object} hamster - Most recently finished web worker, for reuse
651    * @param {blob} dataBlob
652    * @return 
653    */
654   hamsters.wheel.processQueue = function(hamster, dataBlob) {
655     var item = hamsters.wheel.queue.pending.shift(); //Get and remove first item from queue
656     if(item) {
657       hamsters.wheel.newWheel(item.input, item.params, item.aggregate, item.callback, item.taskid, item.workerid, hamster, dataBlob, item.memoize); //Assign most recently finished thread to queue item
658     }
659   };
660 
661   /**
662    * @description: Cleans up memory used by dataBlob 
663    * @constructor
664    * @method terminateHamster
665    * @param {object} dataBlob - dataBlob to free from memory, critical for IE11 support
666    * @return 
667    */
668   hamsters.wheel.terminateHamster = function(dataBlob) {
669     if(dataBlob) {
670       if(dataBlob.blob.close) {
671         dataBlob.blob.close();
672       } else if(dataBlob.blob.msClose) {
673         dataBlob.blob.msClose();
674       } else if(dataBlob.blob.slice) {
675         dataBlob.blob = dataBlob.blob.slice(0,0);
676       }
677       window.URL.revokeObjectURL(dataBlob.uri);
678     }
679   };
680 
681   /**
682    * @description: Handle response from worker thread, setup error handling
683    * @constructor
684    * @method trainHamster
685    * @param {integer} id - global wheel threadid
686    * @param {boolean} aggregate - boolean aggregate individual thread outputs into final array  
687    * @param {function} callback - task callback when all hamsters complete
688    * @param {integer} taskid - global wheel task id
689    * @param {integer} workerid - worker wheel threadid
690    * @param {object} hamster - web worker
691    * @param {blob} dataBlob
692    * @return 
693    */
694   hamsters.wheel.trainHamster = function(id, aggregate, callback, taskid, workerid, hamster, dataBlob, memoize) {
695 
696     /**
697      * @description: Runs when a hamster (thread) finishes it's work
698      * @constructor
699      * @method onmessage
700      * @param {object} e - Web Worker event object
701      * @return 
702      */
703     hamster.onmessage = function(e) {
704       var queue = hamsters.wheel.queue;
705       if(queue.pending.length === 0) {
706         setTimeout(function() {
707           hamster.terminate(); //Kill the thread only if no items waiting to run (20-22% performance improvement observed during testing, repurposing threads vs recreating them)
708           hamsters.wheel.terminateHamster(dataBlob);
709         }, 4);
710       }
711       queue.running.splice(queue.running.indexOf(id), 1); //Remove thread from running pool
712       var task = hamsters.wheel.tasks[taskid];
713       if(!task) {
714         hamsters.wheel.errors = hamsters.wheel.errors.concat({
715           'timeStamp': e.timeStamp, 
716           'msg': 'Error, unable to match thread to task, throwing exception', 
717           'taskid': taskid, 
718           'workerid': workerid, 
719           'aggregate': aggregate, 
720           'callback': callback
721         });
722         console.error('Fatal Exception, unable to match thread #'+workerid+' to task #'+ taskid + ', cannot continue. Check errors for more details');
723         return;
724       }
725       task.workers.splice(task.workers.indexOf(workerid), 1); //Remove thread from task running pool
726       var results = e.data.results;
727       if(results.dataType && typeof results.data === 'object') {
728         results.data = hamsters.wheel.processDataType(results.dataType, results.data);
729       }
730       task.output[workerid] = results.data;
731       if(task.workers.length === 0 && task.count === task.threads) {
732         var output = hamsters.wheel.getOutput(task.output, aggregate, results.dataType);
733         if(callback) {
734           if(hamsters.debug) {
735             console.info('Execution Complete! Elapsed: ' + ((e.timeStamp - task.input[0].start)/1000) + 's');
736           }
737           if(task.order) {
738             callback(hamsters.wheel.sort(output, task.order));
739           } else {
740             callback(output);
741           }
742           hamsters.wheel.tasks[taskid] = null; //Clean up our task, not needed any longer
743           if(hamsters.cache && memoize !== false) {
744             var inputArray = task.input[0].input;
745             if(output.length > 0 && !results.dataType) {
746               setTimeout(hamsters.wheel.memoize(task.fn, inputArray, output, 'na'), 4);
747             } else if(output.length > 0 && results.dataType) {
748               setTimeout(hamsters.wheel.memoize(task.fn, inputArray, hamsters.wheel.normalizeArray(output), results.dataType), 4);
749             }
750           }
751         }
752       } else if(hamsters.debug === 'verbose') {
753         console.info('Hamster #' + id + ' finished ' + '@ ' + e.timeStamp);
754       }
755       if(queue.pending.length !== 0) {
756         hamsters.wheel.processQueue(hamster, dataBlob);
757       }
758     };
759 
760     /**
761      * @description: Setup error handling
762      * @constructor
763      * @method onerror
764      * @param {object} e - Web Worker event object
765      * @return 
766      */
767     hamster.onerror = function(e) {
768       hamster.terminate(); //Kill the thread
769       var msg = 'Error Hamster #' + id + ': Line ' + e.lineno + ' in ' + e.filename + ': ' + e.message;
770       hamsters.wheel.errors = hamsters.wheel.errors.concat({
771         'msg': msg
772       });
773       console.error(msg);
774     };
775   };
776 
777   /**
778    * @description: Normalizes typed array into normal array
779    * @constructor
780    * @method normalizeArray
781    * @param {object} input - typedArray input
782    * @return arr
783    */
784   hamsters.wheel.normalizeArray = function(input) {
785     var arr = [];
786     for (var n = 0, len = input.length; n < len; n++) {
787       arr.push(input[n]);
788     }
789     return arr;
790   };
791 
792   /**
793    * Description
794    * @method aggregateTypedArrays
795    * @param {array} input
796    * @param {string} dataType
797    * @return output
798    */
799   hamsters.wheel.aggregateTypedArrays = function(input, dataType) {
800     var output;
801     var length = 0;
802     var offset = 0;
803     for (var i = 0, len = input.length; i < len; i+=1) {
804       length += input[i].length;
805     }
806     output = hamsters.wheel.processDataType(dataType, length);
807     for (var n = 0, len2 = input.length; n < len2; n+=1) {
808       output.set(input[n], offset);
809       offset += input[n].length;
810     }
811     return output;
812   };
813 
814   /**
815    * @description: Converts array buffer or normal array into a typed array
816    * @constructor
817    * @method processDataType
818    * @param {string} dataType - dataType config param
819    * @param {object} buffer - buffer object or normal array
820    * @return arr
821    */
822   hamsters.wheel.processDataType = function(dataType, buffer) {
823     if (dataType === 'uint32') {
824       return new Uint32Array(buffer);
825     }
826     if (dataType === 'uint16') {
827       return new Uint16Array(buffer);
828     }
829     if (dataType === 'uint8') {
830       return new Uint8Array(buffer);
831     }
832     if (dataType === 'uint8clamped') {
833       return new Uint8ClampedArray(buffer);
834     }
835     if (dataType === 'int32') {
836       return new Int32Array(buffer);
837     }
838     if (dataType === 'int16') {
839       return new Int16Array(buffer);
840     }
841     if (dataType === 'int8') {
842       return new Int8Array(buffer);
843     }
844     if (dataType === 'float32') {
845       return new Float32Array(buffer);
846     }
847     if (dataType === 'float64') {
848       return new Float64Array(buffer);
849     }
850     return buffer;
851   };
852 
853   hamsters.wheel.generateHash = function(string) {
854     string = String(string);
855     var hash = 0;
856     for (var i = string.length - 1; i >= 0; i--) { //Shift 5 bits
857       hash += (((hash << 5) - hash) + string.charCodeAt(i)) & 0xFFFFFFFF;
858     }
859     return hash;
860   };
861 
862   hamsters.wheel.hashResult = function(obj) {
863     var result = 0;
864     for(var key in obj) {
865         if(obj.hasOwnProperty(key)) {
866           if(typeof obj[key] === 'object' && obj[key].length && !obj[key].slice) {
867             result += hamsters.wheel.generateHash(key + String(hamsters.wheel.normalizeArray(obj[key])));
868           } else {
869             result += hamsters.wheel.generateHash(key + obj[key]);
870           }
871         }
872     }
873     return result;
874   };
875 
876   /**
877    * @description: Sends message to worker thread to invoke execution
878    * @constructor
879    * @method feedHamster
880    * @param {object} hamster - web worker
881    * @param {object} food - params object for worker
882    * @param {array} inputArray
883    * @return 
884    */
885   hamsters.wheel.feedHamster = function(hamster, food, inputArray) {
886     if(inputArray && food.dataType) { //Transferable object transfer if using typed array
887       food.array = hamsters.wheel.processDataType(food.dataType, inputArray);
888     } else if(inputArray) {
889       food.array = inputArray;
890     }
891     if(food.array && food.array.buffer) {
892       var buffer = food.array.buffer;
893       food.array = buffer;
894       hamster.postMessage(food,  [buffer]);
895     } else if((window.chrome || window.firefox) && food.array.length <= 35000000) { //Stringify data for chrome/firefox on less than 35M array size, good performance boost 
896       hamster.postMessage(JSON.stringify(food));
897     } else {
898       hamster.postMessage(food); //Use structured cloning for Safari/IE or for data sets > 40M+
899     }
900   };
901 
902   /**
903    * @description: Creates new worker thread with body of work to be completed
904    * @constructor
905    * @method newWheel
906    * @param {array} inputArray
907    * @param {object} hamsterfood - incoming params object for worker
908    * @param {boolean} aggregate - boolean aggregate individual thread outputs into final array  
909    * @param {function} callback - task callback when all hamsters complete
910    * @param {integer} taskid - global wheel task id
911    * @param {integer} threadid - global wheel threadid
912    * @param {object} hamster - web worker
913    * @param {blob} dataBlob
914    * @return 
915    */
916   hamsters.wheel.setup.isLegacy(function(legacy) {
917     if(legacy) {
918       hamsters.wheel.newWheel = function(inputArray, hamsterfood, aggregate, callback, taskid, threadid, hamster, dataBlob, memoize) {
919         var task = hamsters.wheel.tasks[taskid];
920         if(!task) {
921           hamsters.wheel.errors.push({
922             'msg': 'Error, unable to match thread to task, throwing exception', 
923             'params': hamsterfood, 
924             'aggregate': aggregate, 
925             'callback': callback
926           });
927           console.error('Error, unable to match thread to task ' + taskid + ', throwing exception. Check errors for more details');
928           return;
929         }
930         var debug = hamsters.debug;
931         hamsters.wheel.legacyProcessor(hamsterfood, inputArray, function(output) {
932           task.count++; //Thread finished
933           task.output[threadid] = output.data;
934           if(task.count === task.threads) { //Task complete get output and return
935             var rtn = hamsters.wheel.getOutput(task.output, aggregate, output.dataType);
936             if(callback) {
937               if(debug) {
938                 console.info('Execution Complete! Elapsed: ' + ((new Date().getTime() - task.input[0].start)/1000) + 's');
939               }
940               callback(rtn);
941               hamsters.wheel.tasks[taskid] = null; //Clean up our task, not needed any longer
942               if(hamsters.cache && memoize !== false) {
943                 if(output.data.length > 0 && !output.dataType) {
944                   setTimeout(hamsters.wheel.memoize(task.fn, task.input, output.data, 'na'), 4);
945                 } else if(output.data.length > 0 && output.dataType) {
946                   setTimeout(hamsters.wheel.memoize(task.fn, task.input, hamsters.wheel.normalizeArray(output.data), output.dataType), 4);
947                 }
948               }
949             }
950           }
951         });
952         return;
953       };
954     } else {
955       hamsters.wheel.newWheel = function(inputArray, hamsterfood, aggregate, callback, taskid, threadid, hamster, dataBlob, memoize) {
956         var task = hamsters.wheel.tasks[taskid];
957         if(!task) {
958           hamsters.wheel.errors.push({
959             'msg': 'Error, unable to match thread to task, throwing exception', 
960             'params': hamsterfood, 
961             'aggregate': aggregate, 
962             'callback': callback
963           });
964           console.error('Error, unable to match thread to task ' + taskid + ', throwing exception. Check errors for more details');
965           return;
966         }
967         var queue = hamsters.wheel.queue;
968         if(hamsters.maxThreads && hamsters.maxThreads <= queue.running.length) {
969           hamsters.wheel.poolThread(queue, inputArray, hamsterfood, threadid, callback, taskid, aggregate, memoize);
970           return;
971         }
972         var thread = (threadid || task.count); //Determine threadid depending on currently running threads
973         var debug = hamsters.debug;
974         if(debug || memoize) {
975           hamsters.wheel.trackInput(task, inputArray, thread, taskid, hamsterfood);
976           if(debug === 'verbose') {
977             console.info('Spawning Hamster #' + thread + ' @ ' + new Date().getTime());
978           }
979         }
980         hamsters.wheel.trackThread(task, queue, thread);
981         var blobObject = (dataBlob || null);
982         var hamsterData;
983         if(!hamster) {
984           hamsterData = hamsters.wheel.createHamster(thread);
985           hamster = hamsterData.worker;
986           blobObject = {'blob': hamsterData.dataBlob, 'uri': hamsterData.blobUri};
987         }
988         hamsters.wheel.trainHamster(thread, aggregate, callback, taskid, thread, hamster, blobObject, memoize);
989         hamsters.wheel.feedHamster(hamster, hamsterfood, inputArray);
990         task.count++; //Increment count, thread is running
991       };
992     }
993   });
994   hamsters.wheel.setup.populateElements(hamsters.maxThreads);
995 };
996 //Wake 'em up
997 hamsters.wheel.wakeUp();