Explorar o código

small refactoring, and added config panel that can edit, add/remove targets

Torkel Ödegaard %!s(int64=12) %!d(string=hai) anos
pai
achega
516a8f1176

+ 7 - 2
src/app/components/require.config.js

@@ -43,15 +43,20 @@ require.config({
 
     modernizr:                '../vendor/modernizr-2.6.1',
     elasticjs:                '../vendor/elasticjs/elastic-angular-client',
+    rq:                       '../vendor/rq',
+    setImmediate:             '../vendor/setImmediate',
 
-    'ts-widget':              '../vendor/timeserieswidget/jquery.tswidget',
-    'ts-graphite-helpers':    '../vendor/timeserieswidget/graphite_helpers'
   },
   shim: {
     underscore: {
       exports: '_'
     },
 
+    rq: {
+        deps: ['setImmediate'],
+        exports: 'RQ'
+    },
+
     angular: {
       deps: ['jquery'],
       exports: 'angular'

+ 13 - 44
src/app/panels/graphite/editor.html

@@ -1,48 +1,17 @@
 <div class="editor-row">
-  <div class="section">
-    <h5>Values</h5>
-    <div class="editor-option">
-      <label class="small">Chart value</label>
-      <select ng-change="set_refresh(true)" class="input-small" ng-model="panel.mode" ng-options="f for f in ['count','min','mean','max','total']"></select>
+
+	<h5>Graphite Targets</h5>
+
+  	<div ng-repeat="target in panel.targets">
+
+  	<div class="row-fluid">
+  		<div class="span12">
+    		<input type="text" ng-model="target.target" class="input-large" style="width:95%" ng-change="render()" />
+    		<i ng-click="panel.targets = _.without(panel.targets, target)" class="pointer icon-remove" style="position: relative; top: -5px; left: 5px;"></i>
+    	</div>
     </div>
-    <div class="editor-option" ng-show="panel.mode != 'count'">
-      <label class="small">Value Field <tip>This field must contain a numeric value</tip></label>
-        <input ng-change="set_refresh(true)" placeholder="Start typing" bs-typeahead="fields.list" type="text" class="input-large" ng-model="panel.value_field">
-    </div>
-  </div>
-  <div class="section">
-    <h5>Transform Series</h5>
-    <div class="editor-option" ng-show="panel.mode != 'count'">
-      <label class="small">Scale</label>
-        <input type="text" class="input-mini" ng-model="panel.scale">
-    </div>
-    <div class="editor-option">
-      <label class="small">Seconds <tip>Normalize intervals to per-second</tip></label><input type="checkbox" ng-model="panel.scaleSeconds" ng-checked="panel.scaleSeconds">
-    </div>
-    <div class="editor-option">
-      <label class="small">Derivative <tip>Plot the change per interval in the series</tip></label><input type="checkbox" ng-model="panel.derivative" ng-checked="panel.derivative" ng-change="set_refresh(true)">
-    </div>
-  </div>
-</div>
-<h5>Time Options</h5>
-<div class="editor-row">
-  <div class="editor-option">
-    <label class="small">Time Field</label>
-      <input ng-change="set_refresh(true)" placeholder="Start typing" bs-typeahead="fields.list" type="text" class="input-small" ng-model="panel.time_field">
-  </div>
-  <div class="editor-option">
-    <label class="small">Time correction</label>
-    <select ng-model="panel.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select>
-  </div>
-  <div class="editor-option">
-    <label class="small">Auto-interval</label><input type="checkbox" ng-model="panel.auto_int" ng-checked="panel.auto_int" />
-  </div>
-  <div class="editor-option" ng-show='panel.auto_int'>
-    <label class="small">Resolution <tip>Shoot for this many data points, rounding to sane intervals</tip></label>
-    <input type="number" class='input-mini' ng-model="panel.resolution" ng-change='set_refresh(true)'/>
-  </div>
-  <div class="editor-option" ng-hide='panel.auto_int'>
-    <label class="small">Interval <tip>Use Elasticsearch date math format (eg 1m, 5m, 1d, 2w, 1y)</tip></label>
-    <input type="text" class='input-mini' ng-model="panel.interval" ng-change='set_refresh(true)'/>
   </div>
+
+  <button ng-click="add_target(panel.target);" class="btn btn-success" ng-show="editor.index == 1">Add target</button>
+
 </div>

+ 94 - 0
src/app/panels/graphite/graphiteSrv.js

@@ -0,0 +1,94 @@
+define([
+  'jquery',
+  'rq',
+  'config'
+],
+function ($, RQ, config) {
+  'use strict';
+
+
+  function build_graphite_options(options, raw) {
+    raw = raw || false;
+    var clean_options = [];
+    //var internal_options = ['_t'];
+    var graphite_options = ['target', 'targets', 'from', 'until', 'rawData', 'format'];
+    var graphite_png_options = ['areaMode', 'width', 'height', 'template', 'margin', 'bgcolor',
+                         'fgcolor', 'fontName', 'fontSize', 'fontBold', 'fontItalic',
+                         'yMin', 'yMax', 'colorList', 'title', 'vtitle', 'lineMode',
+                         'lineWith', 'hideLegend', 'hideAxes', 'hideGrid', 'minXstep',
+                         'majorGridlineColor', 'minorGridLineColor', 'minorY',
+                         'thickness', 'min', 'max', 'tz'];
+
+    if(raw) {
+      options['format'] = 'json';
+    } else {
+      // use random parameter to force image refresh
+      options["_t"] = options["_t"] || Math.random();
+    }
+
+    $.each(options, function (key, value) {
+      if(raw) {
+        if ($.inArray(key, graphite_options) === -1) {
+          return;
+        }
+      } else {
+        if ($.inArray(key, graphite_options) === -1 && $.inArray(key, graphite_png_options) === -1) {
+          return;
+        }
+      }
+      if (key === "targets") {
+        $.each(value, function (index, value) {
+          if (raw) {
+            // it's normally pointless to use alias() in raw mode, because we apply an alias (name) ourself
+            // in the client rendering step.  we just need graphite to return the target.
+            // but graphite sometimes alters the name of the target in the returned data
+            // (https://github.com/graphite-project/graphite-web/issues/248)
+            // so we need a good string identifier and set it using alias() (which graphite will honor)
+            // so that we recognize the returned output. simplest is just to include the target spec again
+            // though this duplicates a lot of info in the url.
+            clean_options.push("target=" + encodeURIComponent(value.target));
+          } else {
+            clean_options.push("target=alias(color(" +encodeURIComponent(value.target + ",'" + value.color) +"'),'" + value.name +"')");
+          }
+        });
+      } else if (value !== null) {
+        clean_options.push(key + "=" + encodeURIComponent(value));
+      }
+    });
+
+    return clean_options;
+  }
+
+  function loadGraphiteData(requestion, options)
+  {
+    var graphOptions = {
+      from: $.plot.formatDate(options.range.from, '%H%:%M_%Y%m%d'),
+      until: $.plot.formatDate(options.range.to, '%H%:%M_%Y%m%d'),
+      targets: options.targets
+    }
+
+    var graphiteParameters = build_graphite_options(graphOptions, true);
+    getGraphiteData(graphiteParameters)
+      .done(function(data) {
+        requestion(data);
+      })
+      .fail(function() {
+        requestion(null, 'Error in ajax call to graphite');
+      });
+  }
+
+  function getGraphiteData(parameters) {
+    return $.ajax({
+      accepts: { text: 'application/json' },
+      cache: false,
+      dataType: 'json',
+      url: config.graphiteUrl,
+      type: "POST",
+      data: parameters.join('&')
+    });
+  }
+
+  return {
+    loadGraphiteData: loadGraphiteData
+  };
+});

+ 0 - 151
src/app/panels/graphite/graphiteUtil.js

@@ -1,151 +0,0 @@
-define([
-  'jquery'
-],
-function ($) {
-  'use strict';
-
-  String.prototype.graphiteGlob = function(glob) {
-    var regex = '^';
-    for (var i = 0; i < glob.length; i++ ) {
-      var c = glob.charAt(i);
-      switch (c) {
-        case '*':
-          regex += '[^\.]+';
-          break;
-        case '.':
-          regex += '\\.';
-          break;
-        default:
-          regex += c;
-      }
-    }
-    regex += '$';
-    return this.match(regex);
-  }
-
-  function build_graphite_options(options, raw) {
-    raw = raw || false;
-    var clean_options = [];
-    //var internal_options = ['_t'];
-    var graphite_options = ['target', 'targets', 'from', 'until', 'rawData', 'format'];
-    var graphite_png_options = ['areaMode', 'width', 'height', 'template', 'margin', 'bgcolor',
-                         'fgcolor', 'fontName', 'fontSize', 'fontBold', 'fontItalic',
-                         'yMin', 'yMax', 'colorList', 'title', 'vtitle', 'lineMode',
-                         'lineWith', 'hideLegend', 'hideAxes', 'hideGrid', 'minXstep',
-                         'majorGridlineColor', 'minorGridLineColor', 'minorY',
-                         'thickness', 'min', 'max', 'tz'];
-
-    if(raw) {
-      options['format'] = 'json';
-    } else {
-      // use random parameter to force image refresh
-      options["_t"] = options["_t"] || Math.random();
-    }
-
-    $.each(options, function (key, value) {
-      if(raw) {
-        if ($.inArray(key, graphite_options) === -1) {
-          return;
-        }
-      } else {
-        if ($.inArray(key, graphite_options) === -1 && $.inArray(key, graphite_png_options) === -1) {
-          return;
-        }
-      }
-      if (key === "targets") {
-        $.each(value, function (index, value) {
-          if (raw) {
-            // it's normally pointless to use alias() in raw mode, because we apply an alias (name) ourself
-            // in the client rendering step.  we just need graphite to return the target.
-            // but graphite sometimes alters the name of the target in the returned data
-            // (https://github.com/graphite-project/graphite-web/issues/248)
-            // so we need a good string identifier and set it using alias() (which graphite will honor)
-            // so that we recognize the returned output. simplest is just to include the target spec again
-            // though this duplicates a lot of info in the url.
-            clean_options.push("target=" + encodeURIComponent(value.target));
-          } else {
-            clean_options.push("target=alias(color(" +encodeURIComponent(value.target + ",'" + value.color) +"'),'" + value.name +"')");
-          }
-        });
-      } else if (value !== null) {
-        clean_options.push(key + "=" + encodeURIComponent(value));
-      }
-    });
-
-    return clean_options;
-  }
-
-  function build_graphite_url(options) {
-    var limit = 2000;  // http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers
-    var url = options.graphite_url + "?";
-
-    options = build_graphite_options(options, false);
-    $.map(options, function(option) {
-        if (url.length + option.length < limit) {
-            url += '&' + option;
-        }
-    });
-    return url.replace(/\?&/, "?");
-  }
-
-  function find_definition (target_graphite, options) {
-    var matching_i = undefined;
-
-    for (var cfg_i = 0; cfg_i < options.targets.length && matching_i == undefined; cfg_i++) {
-      // string match (no globbing)
-      if(options.targets[cfg_i].target == target_graphite.target) {
-          matching_i = cfg_i;
-      }
-      // glob match?
-      else if(target_graphite.target.graphiteGlob(options.targets[cfg_i].target)) {
-          matching_i = cfg_i;
-      }
-    }
-
-    if (matching_i == undefined) {
-      console.error ("internal error: could not figure out which target_option target_graphite '" +
-              target_graphite.target + "' comes from");
-      return [];
-    }
-
-    return options.targets[matching_i];
-  }
-
-  function add_targets(options, response_data) {
-    var all_targets = [];
-    for (var res_i = 0; res_i < response_data.length; res_i++) {
-      var target = find_definition(response_data[res_i], options);
-      target.label = target.name; // flot wants 'label'
-      target.data = [];
-      var nulls = 0;
-      var non_nulls = 0;
-      for (var i in response_data[res_i].datapoints) {
-        if(response_data[res_i].datapoints[i][0] == null) {
-          nulls++;
-          if('drawNullAsZero' in options && options['drawNullAsZero']) {
-            response_data[res_i].datapoints[i][0] = 0;
-          } else {
-            // don't tell flot about null values, it prevents adjacent non-null values from
-            // being rendered correctly
-            continue;
-          }
-        } else {
-          non_nulls++;
-        }
-        target.data.push([response_data[res_i].datapoints[i][1] * 1000, response_data[res_i].datapoints[i][0]]);
-      }
-      if (nulls/non_nulls > 0.3) {
-        console.log("warning: rendered target contains " + nulls + " null values, " + non_nulls + " non_nulls");
-      }
-      all_targets.push(target);
-    }
-
-    return all_targets;
-  }
-
-  return {
-    build_graphite_options: build_graphite_options,
-    build_graphite_url: build_graphite_url,
-    add_targets: add_targets
-  };
-});

+ 25 - 55
src/app/panels/graphite/module.js

@@ -19,8 +19,7 @@ define([
   'kbn',
   'moment',
   './timeSeries',
-  './graphiteUtil',
-  'config',
+  './graphiteSrv',
   'jquery.flot',
   'jquery.flot.events',
   'jquery.flot.selection',
@@ -29,7 +28,7 @@ define([
   'jquery.flot.stack',
   'jquery.flot.stackpercent'
 ],
-function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
+function (angular, app, $, _, kbn, moment, timeSeries, graphiteSrv) {
 
   'use strict';
 
@@ -50,11 +49,7 @@ function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
         {
           title:'Style',
           src:'app/panels/graphite/styleEditor.html'
-        },
-        {
-          title:'Queries',
-          src:'app/panels/graphite/queriesEditor.html'
-        },
+        }
       ],
       status  : "Stable",
       description : "A bucketed time series chart of the current query or queries. Uses the "+
@@ -168,10 +163,6 @@ function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
        * legend:: Display the legond
        */
       legend        : true,
-      /** @scratch /panels/histogram/3
-       * show_query:: If no alias is set, should the query be displayed?
-       */
-      show_query    : true,
       /** @scratch /panels/histogram/3
        * interactive:: Enable click-and-drag to zoom functionality
        */
@@ -197,16 +188,11 @@ function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
       /** @scratch /panels/histogram/3
        * derivative:: Show each point on the x-axis as the change from the previous point
        */
-      derivative    : false,
-      /** @scratch /panels/histogram/3
-       * tooltip object::
-       * tooltip.value_type::: Individual or cumulative controls how tooltips are display on stacked charts
-       * tooltip.query_as_alias::: If no alias is set, should the query be displayed?
-       */
       tooltip       : {
         value_type: 'cumulative',
         query_as_alias: true
-      }
+      },
+      targets: []
     };
 
     _.defaults($scope.panel,_d);
@@ -215,7 +201,6 @@ function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
     _.defaults($scope.panel.grid,_d.grid);
 
 
-
     $scope.init = function() {
       // Hide view options by default
       $scope.options = false;
@@ -268,33 +253,6 @@ function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
       return $scope.panel.interval;
     };
 
-    var graphOptions = {
-      until: 'now',
-      targets: [
-        {
-          name: 'series 1',
-          color: '#CC6699',
-          target: "summarize(sum(prod.apps.tradera_site.*.counters.global.request_status.code_404.count), '30s')",
-          //target: 'integral(prod.apps.touchweb.snake.counters.login.success.count)',
-          //target: "randomWalk('random1')",
-        }
-      ]
-    };
-
-    $scope.getGraphiteData = function (options, parameters) {
-      return $.ajax({
-        accepts: { text: 'application/json' },
-        cache: false,
-        dataType: 'json',
-        url: config.graphiteUrl,
-        type: "POST",
-        data: parameters.join('&'),
-        error: function(xhr, textStatus, errorThrown) {
-          $scope.panel.error = 'Failed to do graphite POST request: ' + textStatus + ' : '  + errorThrown;
-        }
-      });
-    };
-
     $scope.colors = [
       "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", //1
       "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", //2
@@ -317,18 +275,24 @@ function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
     $scope.get_data = function() {
       delete $scope.panel.error;
 
+      $scope.panelMeta.loading = true;
+
       var range = $scope.get_time_range();
       var interval = $scope.get_interval(range);
 
-      graphOptions.from = $.plot.formatDate(range.from, '%H%:%M_%Y%m%d');
+      var graphiteLoadOptions = {
+        range: range,
+        targets: $scope.panel.targets
+      };
 
-      $scope.panelMeta.loading = true;
+      var result = RQ.sequence([graphiteSrv.loadGraphiteData]);
 
-      var graphiteParameters = graphiteUtil.build_graphite_options(graphOptions, true);
-      var request = $scope.getGraphiteData(graphOptions, graphiteParameters);
-      $scope.populate_modal(graphiteParameters);
+      result(function (results, failure) {
+        if (failure || !results) {
+          $scope.panel.error = 'Failed to do fetch graphite data: ' + failure;
+          return;
+        }
 
-      request.done(function(results) {
         $scope.data = [];
         $scope.panelMeta.loading = false;
 
@@ -339,7 +303,7 @@ function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
         console.log('Data from graphite:', results);
 
         var tsOpts = {
-          interval: "30s",
+          interval: interval,
           start_date: range && range.from,
           end_date: range && range.to,
           fill_style: 'connect'
@@ -380,7 +344,13 @@ function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
 
         // Tell the histogram directive to render.
         $scope.$emit('render');
-      });
+
+      }, graphiteLoadOptions);
+
+    };
+
+    $scope.add_target = function() {
+      $scope.panel.targets.push({target: ''});
     };
 
     // function $scope.zoom

+ 0 - 43
src/app/panels/graphite/queriesEditor.html

@@ -1,43 +0,0 @@
-<h4>Charted</h4>
-<div ng-include src="'app/partials/querySelect.html'"></div>
-
-<div class="editor-row">
-  <h4>Markers</h4>
-
-  <div class="small">
-    Here you can specify a query to be plotted on your chart as a marker. Hovering over a marker will display the field you specify below. If more documents are found than the limit you set, they will be scored by Elasticsearch and events that best match your query will be displayed.
-  </div>
-  <style>
-    .querySelect .query {
-      margin-right: 5px;
-    }
-    .querySelect .selected {
-      border: 3px solid;
-    }
-    .querySelect .unselected {
-      border: 0px solid;
-    }
-  </style>
-  <p>
-  <div class="editor-option">
-    <label class="small">Enable</label>
-    <input type="checkbox" ng-change="set_refresh(true)" ng-model="panel.annotate.enable" ng-checked="panel.annotate.enable">
-  </div>
-  <div class="editor-option" ng-show="panel.annotate.enable">
-    <label class="small">Marker Query</label>
-    <input type="text" ng-change="set_refresh(true)" class="input-large" ng-model="panel.annotate.query"/>
-  </div>
-  <div class="editor-option" ng-show="panel.annotate.enable">
-    <label class="small">Tooltip field</label>
-    <input type="text" class="input-small" ng-model="panel.annotate.field" bs-typeahead="fields.list"/>
-  </div>
-  <div class="editor-option" ng-show="panel.annotate.enable">
-    <label class="small">Limit <tip>Max markers on the chart</tip></label>
-    <input type="number" class="input-mini" ng-model="panel.annotate.size" ng-change="set_refresh(true)"/>
-  </div>
-  <div class="editor-option" ng-show="panel.annotate.enable">
-    <label class="small">Sort <tip>Determine the most relevant markers using this field</tip></label>
-    <input type="text" class="input-small" bs-typeahead="fields.list" ng-model="panel.annotate.sort[0]" ng-change="set_refresh(true)" />
-    <i ng-click="panel.annotate.sort[1] = _.toggle(panel.annotate.sort[1],'desc','asc');set_refresh(true)" ng-class="{'icon-chevron-up': panel.annotate.sort[1] == 'asc','icon-chevron-down': panel.annotate.sort[1] == 'desc'}"></i>
-  </div>
-</div>

+ 588 - 0
src/vendor/rq.js

@@ -0,0 +1,588 @@
+/*
+    rq.js
+
+    Douglas Crockford
+    2013-10-11
+    Public Domain
+
+This package uses four kinds of functions:
+    requestor
+    requestion
+    quash
+    requestory
+
+
+requestor(requestion [, initial])
+    may return a quash function
+
+    A requestor is a function that makes a request. Such a request need not
+    be satisified immediately. It is likely that the request will not be
+    satisified until some future turn. Requestors provide a means of dealing
+    with future activities without blocking.
+
+    A requestor is a function that takes a requestion function as its first
+    parameter, and optionally an initial value as its second parameter. The
+    requestor uses the requestion to report its result. A requestor may
+    optionally return a quash function that might be used to cancel the
+    request, triggering the requestion function with a failure result.
+
+    The initial parameter contains a value that may be used to initialize the
+    request. It is provided specifically for RQ.sequence, but it may be passed
+    to any requestor.
+
+
+requestion(success, failure)
+    returns undefined
+
+    A requestion function is a continuation or callback. It is used to deliver
+    the result of a request. A requestion takes two arguments: success and
+    failure. If the request succeeds, then the result will be passed to the
+    requestion function as the success parameter, and the failure parameter
+    will be undefined. If the request fails, then the requestion function will
+    be passed the reason as the failure parameter. If failure is undefined,
+    then the request succeeded. If failure is any other value, then the request
+    failed.
+
+
+quash(reason)
+    returns undefined
+
+    If a request is likely to be expensive to satisfy, the requestor may
+    optionally return a quash function that would allow the request to be
+    cancelled. A requestor is not required to return a quash function, and
+    the quash function will not be guaranteed to cancel the request. The
+    quash's reason argument may become the requestion's failure argument.
+
+
+requestory([arguments])
+    returns a requestor function
+
+    A requestory is a factory function that produces a requestor function. A
+    requestory function will usually take parameters that will customize or
+    specialize a request. It is possible to write requestor functions by hand,
+    but it is usually easier to generate them with requestories.
+
+
+The RQ object contains some requestory functions that permit the composition of
+requestors:
+
+    RQ.fallback(requestors, milliseconds)
+    RQ.race(requestors, milliseconds)
+    RQ.parallel(requestors, optionals, milliseconds, tilliseconds)
+    RQ.sequence(requestors, milliseconds)
+
+Each of these four requestory functions returns a requestor function that
+returns a quash function.
+
+
+RQ.fallback(requestors, milliseconds)
+
+    RQ.fallback returns a requestor function that will call the first element
+    in the requestors array. If that is ultimately successful, its value will
+    be passed to the requestion. But if it fails, the next element will be
+    called, and so on. If none of the elements are successful, then the
+    fallback fails. If any succeeds, then the fallback succeeds.
+
+    If the optional milliseconds argument is supplied, then if a request is not
+    successful in the allotted time, then the fallback fails, and the pending
+    requestor is cancelled.
+
+
+RQ.race(requestors [, milliseconds])
+
+    RQ.race returns a requestor that starts all of the functions in the
+    requestors array in parallel. Its result is the result of the first of
+    those requestors to successfully finish (all of the other requestors are
+    cancelled). If all of those requestors fail, then the race fails.
+
+    If the optional milliseconds argument is supplied, then if no requestor has
+    been successful in the allotted time, then the race fails, and all pending
+    requestors are cancelled.
+
+
+RQ.parallel(requestors [, milliseconds])
+RQ.parallel(requestors, optionals [, milliseconds, [tilliseconds]])
+
+    RQ.parallel returns a requestor that processes many requestors in parallel,
+    producing an array of all of the successful results. It can take two arrays
+    of requests: Those that are required to produce results, and those that may
+    optionally produce results. Each of the optional requestors has until all
+    of the required requestors have finished, or until the optional
+    tilliseconds timer has expired.
+
+    The result maps the requestors and optionals into a single array. The
+    value produced by the first element of the requestors array provides the
+    first element of the result.
+
+    If the optional milliseconds argument is supplied, then if all of the
+    required requestors are not successful in the allotted time, then the
+    parallel fails. If there are no required requestors, and if at least one
+    optional requestor is successful within the allotted time, then the
+    parallel succeeds.
+
+
+RQ.sequence(requestors [, milliseconds])
+
+    RQ.sequence returns a requestor that processes each element of the
+    requestors array one at a time. Each will be passed the result of the
+    previous. If all succeed, then the sequence succeeds, having the result of
+    the last of the requestors. If any fail, then the sequence fails.
+
+    If the optional milliseconds argument is supplied, then if all of the
+    requestors have not all completed in the allotted time, then the sequence
+    fails and the pending requestor is cancelled.
+*/
+
+/*global
+    clearTimeout, setImmediate, setTimeout
+*/
+
+/*properties
+    array, evidence, fallback, freeze, forEach, index, isArray, length,
+    message, method, milliseconds, name, parallel, race, sequence, value
+*/
+
+var RQ = (function () {
+    'use strict';
+
+    function expired(method, milliseconds) {
+
+// Make an expired exception.
+
+        return {
+            name: "expired",
+            method: method,
+            message: "expired after " + milliseconds,
+            milliseconds: milliseconds
+        };
+    }
+
+    function check(method, requestors, milliseconds, optionals, tilliseconds) {
+
+// Verify that the arguments are typed properly.
+
+        function is_function(value, index, array) {
+            if (typeof value !== 'function') {
+                var e = new TypeError("not a function");
+                e.array = array;
+                e.index = index;
+                e.method = method;
+                e.value = value;
+                throw e;
+            }
+        }
+
+// requestors must be an array of functions, and it may be empty only if
+// optionals is present.
+
+        if (optionals === undefined) {
+            if (!Array.isArray(requestors) || requestors.length === 0) {
+                throw new TypeError(method + " requestors");
+            }
+        } else {
+            if (requestors && !Array.isArray(requestors)) {
+                throw new TypeError(method + " requestors");
+            }
+            if (!Array.isArray(optionals) || optionals.length === 0) {
+                throw new TypeError(method + " optionals");
+            }
+            optionals.forEach(is_function);
+        }
+        requestors.forEach(is_function);
+        if (milliseconds &&
+                (typeof milliseconds !== 'number' || milliseconds < 0)) {
+            throw new TypeError(method + " milliseconds");
+        }
+        if (tilliseconds &&
+                (typeof tilliseconds !== 'number' || tilliseconds < 0)) {
+            throw new TypeError(method + " tilliseconds");
+        }
+    }
+
+    function check_requestion(method, requestion, initial) {
+        if (typeof requestion !== 'function') {
+            throw new TypeError(method + " requestion");
+        }
+        if (initial !== null && typeof initial === 'object') {
+            Object.freeze(initial);
+        }
+    }
+
+    return {
+        fallback : function fallback(requestors, milliseconds) {
+
+// RQ.fallback takes an array of requestor functions, and returns a requestor
+// that will call them each in order until it finds a successful outcome.
+
+// If all of the requestor functions fail, then the fallback fails. If the time
+// expires, then work in progress is cancelled.
+
+            check("RQ.fallack", requestors, milliseconds);
+            return function requestor(requestion, initial) {
+                var cancel,
+                    timeout_id;
+
+                function finish(success, failure) {
+                    var r = requestion;
+                    cancel = null;
+                    if (r) {
+                        if (timeout_id) {
+                            clearTimeout(timeout_id);
+                        }
+                        requestion = null;
+                        timeout_id = null;
+                        return r(success, failure);
+                    }
+                }
+
+                function quash(reason) {
+                    if (requestion && typeof cancel === 'function') {
+                        setImmediate(cancel, reason);
+                    }
+                    return finish(undefined, reason || true);
+                }
+
+                check_requestion("RQ.fallack", requestion, initial);
+                if (milliseconds) {
+                    timeout_id = setTimeout(function () {
+                        return quash(expired("RQ.fallback", milliseconds));
+                    }, milliseconds);
+                }
+                (function next(index, failure) {
+                    if (typeof requestion === 'function') {
+
+// If there are no more requestors, then signal failure.
+
+                        if (index >= requestors.length) {
+                            clearTimeout(timeout_id);
+                            cancel = null;
+                            return quash(failure);
+                        }
+
+// If there is another requestor, call it in the next turn, passing the value
+// and a requestion that will take the next step.
+
+                        var requestor = requestors[index];
+                        setImmediate(function () {
+                            var once = true;
+                            if (typeof requestion === 'function') {
+                                cancel = requestor(
+                                    function requestion(success, failure) {
+                                        if (once) {
+                                            once = false;
+                                            cancel = null;
+                                            return failure === undefined
+                                                ? finish(success)
+                                                : next(index + 1, failure);
+                                        }
+                                    },
+                                    initial
+                                );
+                            }
+                        });
+                    }
+                }(0));
+                return quash;
+            };
+        },
+        parallel: function parallel(requestors, optionals, milliseconds,
+                tilliseconds) {
+
+// RQ.parallel takes an array of requestors, and an optional second array of
+// requestors, and starts them all. It succeeds if all of the requestors in
+// the first array finish successfully before the time expires. The result
+// is an array collecting the results of all of the requestors.
+
+            if (typeof optionals === 'number') {
+                milliseconds = optionals;
+                tilliseconds = undefined;
+                optionals = undefined;
+            }
+            check("RQ.parallel", requestors, milliseconds, optionals,
+                tilliseconds);
+
+            return function requestor(requestion, initial) {
+                var quashes = [],
+                    optionals_remaining,
+                    optionals_successes = 0,
+                    requestors_length = requestors.length,
+                    requestors_remaining = requestors.length,
+                    results = [],
+                    timeout_till,
+                    timeout_id;
+
+                function finish(success, failure) {
+                    var r = requestion;
+                    if (r) {
+                        requestion = null;
+                        if (timeout_id) {
+                            clearTimeout(timeout_id);
+                            timeout_id = null;
+                        }
+                        if (timeout_till) {
+                            clearTimeout(timeout_till);
+                            timeout_till = null;
+                        }
+                        quashes.forEach(function (quash) {
+                            if (typeof quash === 'function') {
+                                return setImmediate(quash, failure);
+                            }
+                        });
+                        quashes = null;
+                        results = null;
+                        return r(success, failure);
+                    }
+                }
+
+                function quash(reason) {
+                    return finish(undefined, reason || true);
+                }
+
+                check_requestion("RQ.parallel", requestion, initial);
+
+// milliseconds, if specified, says take no longer to process this request. If
+// any of the required requestors are not successful by this time, the parallel
+// requestor fails.
+
+                if (milliseconds) {
+                    timeout_id = setTimeout(function () {
+                        timeout_id = null;
+                        return requestors_remaining === 0 &&
+                                (requestors_length > 0 ||
+                                optionals_successes > 0)
+                            ? finish(results)
+                            : quash(expired("RQ.parallel", milliseconds));
+                    }, milliseconds);
+
+// tilliseconds, if specified, gives more time for the optional requestors to
+// complete. Normally, the optional requestors have until all of the required
+// requestors finish. If tilliseconds is larger than milliseconds, milliseconds
+// wins.
+
+                }
+                if (tilliseconds) {
+                    timeout_till = setTimeout(function () {
+                        timeout_till = null;
+                        if (requestors_remaining === 0) {
+                            return finish(results);
+                        }
+                    }, tilliseconds);
+                }
+                if (requestors) {
+                    requestors.forEach(function (requestor, index) {
+                        return setImmediate(function () {
+                            var once = true, cancel = requestor(
+                                function requestion(success, failure) {
+                                    if (once && quashes) {
+                                        once = false;
+                                        quashes[index] = null;
+                                        if (failure !== undefined) {
+                                            return quash(failure);
+                                        }
+                                        results[index] = success;
+                                        requestors_remaining -= 1;
+                                        if (requestors_remaining === 0 &&
+                                                !timeout_till) {
+                                            return finish(results);
+                                        }
+                                    }
+                                },
+                                initial
+                            );
+                            if (quashes && quashes[index] === undefined) {
+                                quashes[index] = cancel;
+                            }
+                        });
+                    });
+                }
+                if (optionals) {
+                    optionals_remaining = optionals.length;
+                    optionals.forEach(function (requestor, index) {
+                        return setImmediate(function () {
+                            var once = true, cancel = requestor(
+                                function requestion(success, failure) {
+                                    if (once && quashes) {
+                                        once = false;
+                                        quashes[requestors_length + index]
+                                            = null;
+                                        if (failure === undefined) {
+                                            results[requestors_length + index]
+                                                = success;
+                                            optionals_successes += 1;
+                                        }
+                                        optionals_remaining -= 1;
+                                        if (optionals_remaining === 0) {
+                                            if (requestors_remaining === 0) {
+                                                return requestors_length > 0 ||
+                                                        optionals_successes > 0
+                                                    ? finish(results)
+                                                    : quash(failure);
+                                            }
+                                            if (timeout_till) {
+                                                clearTimeout(timeout_till);
+                                                timeout_till = null;
+                                            }
+                                        }
+                                    }
+                                },
+                                initial
+                            );
+                            if (quashes[requestors_length + index] ===
+                                    undefined) {
+                                quashes[requestors_length + index] = cancel;
+                            }
+                        });
+                    });
+                }
+                return quash;
+            };
+        },
+        race: function race(requestors, milliseconds) {
+
+// RQ.race takes an array of requestor functions. It starts them all
+// immediately. The first to finish wins. A race is successful if any
+// contestant is successful. It fails if all requestors fail or if the time
+// expires.
+
+            check("RQ.race", requestors, milliseconds);
+            return function requestor(requestion, initial) {
+                var quashes = [],
+                    remaining = requestors.length,
+                    timeout_id;
+
+                function finish(success, failure) {
+                    var r = requestion;
+                    if (r) {
+                        requestion = null;
+                        if (timeout_id) {
+                            clearTimeout(timeout_id);
+                        }
+                        quashes.forEach(function stop(quash) {
+                            if (typeof quash === 'function') {
+                                return setImmediate(quash);
+                            }
+                        });
+                        quashes = null;
+                        return r(success, failure);
+                    }
+                }
+
+                function quash(reason) {
+                    return finish(undefined, reason || true);
+                }
+
+                check_requestion("RQ.race", requestion, initial);
+                if (milliseconds) {
+                    timeout_id = setTimeout(function timeout_id() {
+                        return quash(expired("RQ.race", milliseconds));
+                    }, milliseconds);
+                }
+                requestors.forEach(function (requestor, index) {
+                    return setImmediate(function () {
+                        var once = true, cancel = requestor(
+                            function requestion(success, failure) {
+                                if (once && quashes) {
+                                    once = false;
+                                    quashes[index] = null;
+                                    if (failure === undefined) {
+                                        return finish(success);
+                                    }
+                                    remaining -= 1;
+                                    if (remaining === 0) {
+                                        return quash(failure);
+                                    }
+                                }
+                            },
+                            initial
+                        );
+                        if (quashes[index] === undefined) {
+                            quashes[index] = cancel;
+                        }
+                    });
+                });
+                return quash;
+            };
+        },
+        sequence: function sequence(requestors, milliseconds) {
+
+// RQ.sequence takes an array of requestor functions, and returns a requestor
+// that will call them each in order. An initial value is passed to each, which
+// is the previous success result.
+
+// If any of the requestor functions fail, then the whole sequence fails, and
+// the remaining requestors are not called.
+
+            check("RQ.sequence", requestors, milliseconds);
+            return function requestor(requestion, initial) {
+                var cancel,
+                    timeout_id;
+
+                function finish(success, failure) {
+                    var r = requestion;
+                    cancel = null;
+                    if (r) {
+                        if (timeout_id) {
+                            clearTimeout(timeout_id);
+                        }
+                        requestion = null;
+                        return r(success, failure);
+                    }
+                }
+
+                function quash(reason) {
+                    if (requestion && typeof cancel === 'function') {
+                        setImmediate(cancel, reason);
+                    }
+                    return finish(undefined, reason || true);
+                }
+
+                check_requestion("RQ.sequence", requestion, initial);
+                if (milliseconds) {
+                    timeout_id = setTimeout(function () {
+                        timeout_id = null;
+                        return quash(expired("RQ.sequence", milliseconds));
+                    }, milliseconds);
+                }
+                (function next(index) {
+                    var requestor, r = requestion;
+                    if (typeof r === 'function') {
+
+// If there are no more requestors, then signal success.
+
+                        if (index >= requestors.length) {
+                            if (timeout_id) {
+                                clearTimeout(timeout_id);
+                            }
+                            requestion = null;
+                            cancel = null;
+                            return r(initial);
+                        }
+
+// If there is another requestor, call it in the next turn, passing the value
+// and a requestion that will take the next step.
+
+                        requestor = requestors[index];
+                        setImmediate(function () {
+                            var once = true;
+                            cancel = requestor(
+                                function requestion(success, failure) {
+                                    if (once) {
+                                        once = false;
+                                        cancel = null;
+                                        if (failure !== undefined) {
+                                            return quash(failure);
+                                        }
+                                        initial = success;
+                                        return next(index + 1);
+                                    }
+                                },
+                                initial
+                            );
+                        });
+                    }
+                }(0));
+                return quash;
+            };
+        }
+    };
+}());

+ 218 - 0
src/vendor/setImmediate.js

@@ -0,0 +1,218 @@
+(function (global, undefined) {
+    "use strict";
+
+    var tasks = (function () {
+        function Task(handler, args) {
+            this.handler = handler;
+            this.args = args;
+        }
+        Task.prototype.run = function () {
+            // See steps in section 5 of the spec.
+            if (typeof this.handler === "function") {
+                // Choice of `thisArg` is not in the setImmediate spec; `undefined` is in the setTimeout spec though:
+                // http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html
+                this.handler.apply(undefined, this.args);
+            } else {
+                var scriptSource = "" + this.handler;
+                /*jshint evil: true */
+                eval(scriptSource);
+            }
+        };
+
+        var nextHandle = 1; // Spec says greater than zero
+        var tasksByHandle = {};
+        var currentlyRunningATask = false;
+
+        return {
+            addFromSetImmediateArguments: function (args) {
+                var handler = args[0];
+                var argsToHandle = Array.prototype.slice.call(args, 1);
+                var task = new Task(handler, argsToHandle);
+
+                var thisHandle = nextHandle++;
+                tasksByHandle[thisHandle] = task;
+                return thisHandle;
+            },
+            runIfPresent: function (handle) {
+                // From the spec: "Wait until any invocations of this algorithm started before this one have completed."
+                // So if we're currently running a task, we'll need to delay this invocation.
+                if (!currentlyRunningATask) {
+                    var task = tasksByHandle[handle];
+                    if (task) {
+                        currentlyRunningATask = true;
+                        try {
+                            task.run();
+                        } finally {
+                            delete tasksByHandle[handle];
+                            currentlyRunningATask = false;
+                        }
+                    }
+                } else {
+                    // Delay by doing a setTimeout. setImmediate was tried instead, but in Firefox 7 it generated a
+                    // "too much recursion" error.
+                    global.setTimeout(function () {
+                        tasks.runIfPresent(handle);
+                    }, 0);
+                }
+            },
+            remove: function (handle) {
+                delete tasksByHandle[handle];
+            }
+        };
+    }());
+
+    function canUseNextTick() {
+        // Don't get fooled by e.g. browserify environments.
+        return typeof process === "object" &&
+               Object.prototype.toString.call(process) === "[object process]";
+    }
+
+    function canUseMessageChannel() {
+        return !!global.MessageChannel;
+    }
+
+    function canUsePostMessage() {
+        // The test against `importScripts` prevents this implementation from being installed inside a web worker,
+        // where `global.postMessage` means something completely different and can't be used for this purpose.
+
+        if (!global.postMessage || global.importScripts) {
+            return false;
+        }
+
+        var postMessageIsAsynchronous = true;
+        var oldOnMessage = global.onmessage;
+        global.onmessage = function () {
+            postMessageIsAsynchronous = false;
+        };
+        global.postMessage("", "*");
+        global.onmessage = oldOnMessage;
+
+        return postMessageIsAsynchronous;
+    }
+
+    function canUseReadyStateChange() {
+        return "document" in global && "onreadystatechange" in global.document.createElement("script");
+    }
+
+    function installNextTickImplementation(attachTo) {
+        attachTo.setImmediate = function () {
+            var handle = tasks.addFromSetImmediateArguments(arguments);
+
+            process.nextTick(function () {
+                tasks.runIfPresent(handle);
+            });
+
+            return handle;
+        };
+    }
+
+    function installMessageChannelImplementation(attachTo) {
+        var channel = new global.MessageChannel();
+        channel.port1.onmessage = function (event) {
+            var handle = event.data;
+            tasks.runIfPresent(handle);
+        };
+        attachTo.setImmediate = function () {
+            var handle = tasks.addFromSetImmediateArguments(arguments);
+
+            channel.port2.postMessage(handle);
+
+            return handle;
+        };
+    }
+
+    function installPostMessageImplementation(attachTo) {
+        // Installs an event handler on `global` for the `message` event: see
+        // * https://developer.mozilla.org/en/DOM/window.postMessage
+        // * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages
+
+        var MESSAGE_PREFIX = "com.bn.NobleJS.setImmediate" + Math.random();
+
+        function isStringAndStartsWith(string, putativeStart) {
+            return typeof string === "string" && string.substring(0, putativeStart.length) === putativeStart;
+        }
+
+        function onGlobalMessage(event) {
+            // This will catch all incoming messages (even from other windows!), so we need to try reasonably hard to
+            // avoid letting anyone else trick us into firing off. We test the origin is still this window, and that a
+            // (randomly generated) unpredictable identifying prefix is present.
+            if (event.source === global && isStringAndStartsWith(event.data, MESSAGE_PREFIX)) {
+                var handle = event.data.substring(MESSAGE_PREFIX.length);
+                tasks.runIfPresent(handle);
+            }
+        }
+        if (global.addEventListener) {
+            global.addEventListener("message", onGlobalMessage, false);
+        } else {
+            global.attachEvent("onmessage", onGlobalMessage);
+        }
+
+        attachTo.setImmediate = function () {
+            var handle = tasks.addFromSetImmediateArguments(arguments);
+
+            // Make `global` post a message to itself with the handle and identifying prefix, thus asynchronously
+            // invoking our onGlobalMessage listener above.
+            global.postMessage(MESSAGE_PREFIX + handle, "*");
+
+            return handle;
+        };
+    }
+
+    function installReadyStateChangeImplementation(attachTo) {
+        attachTo.setImmediate = function () {
+            var handle = tasks.addFromSetImmediateArguments(arguments);
+
+            // Create a <script> element; its readystatechange event will be fired asynchronously once it is inserted
+            // into the document. Do so, thus queuing up the task. Remember to clean up once it's been called.
+            var scriptEl = global.document.createElement("script");
+            scriptEl.onreadystatechange = function () {
+                tasks.runIfPresent(handle);
+
+                scriptEl.onreadystatechange = null;
+                scriptEl.parentNode.removeChild(scriptEl);
+                scriptEl = null;
+            };
+            global.document.documentElement.appendChild(scriptEl);
+
+            return handle;
+        };
+    }
+
+    function installSetTimeoutImplementation(attachTo) {
+        attachTo.setImmediate = function () {
+            var handle = tasks.addFromSetImmediateArguments(arguments);
+
+            global.setTimeout(function () {
+                tasks.runIfPresent(handle);
+            }, 0);
+
+            return handle;
+        };
+    }
+
+    if (!global.setImmediate) {
+        // If supported, we should attach to the prototype of global, since that is where setTimeout et al. live.
+        var attachTo = typeof Object.getPrototypeOf === "function" && "setTimeout" in Object.getPrototypeOf(global) ?
+                          Object.getPrototypeOf(global)
+                        : global;
+
+        if (canUseNextTick()) {
+            // For Node.js before 0.9
+            installNextTickImplementation(attachTo);
+        } else if (canUsePostMessage()) {
+            // For non-IE10 modern browsers
+            installPostMessageImplementation(attachTo);
+        } else if (canUseMessageChannel()) {
+            // For web workers, where supported
+            installMessageChannelImplementation(attachTo);
+        } else if (canUseReadyStateChange()) {
+            // For IE 6–8
+            installReadyStateChangeImplementation(attachTo);
+        } else {
+            // For older browsers
+            installSetTimeoutImplementation(attachTo);
+        }
+
+        attachTo.clearImmediate = tasks.remove;
+    }
+}(typeof global === "object" && global ? global : this));