Index: frontend/node_modules/tryer/.gitlab-ci.yml
===================================================================
--- frontend/node_modules/tryer/.gitlab-ci.yml	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/tryer/.gitlab-ci.yml	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,29 @@
+cache:
+  key: "$CI_JOB_NAME"
+  untracked: true
+  paths:
+    - node_modules/
+
+before_script:
+  - npm install
+
+.test_template: &npm_test
+  script:
+    - npm run lint
+    - npm test
+
+test:node4:
+  image: node:4
+  <<: *npm_test
+
+test:node6:
+  image: node:6
+  <<: *npm_test
+
+test:node8:
+  image: node:8
+  <<: *npm_test
+
+test:node10:
+  image: node:10
+  <<: *npm_test
Index: frontend/node_modules/tryer/.jshintrc
===================================================================
--- frontend/node_modules/tryer/.jshintrc	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/tryer/.jshintrc	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,68 @@
+{
+    "bitwise": true,
+    "camelcase": true,
+    "curly": true,
+    "eqeqeq": true,
+    "es3": true,
+    "forin": true,
+	"freeze": true,
+    "immed": true,
+    "indent": 2,
+    "latedef": "nofunc",
+    "newcap": true,
+    "noarg": true,
+    "noempty": true,
+	"nonbsp": true,
+    "nonew": true,
+    "plusplus": true,
+    "quotmark": "single",
+    "strict": true,
+    "trailing": true,
+    "undef": true,
+    "unused": true,
+
+    "asi": false,
+    "boss": false,
+    "debug": false,
+    "eqnull": false,
+    "es5": false,
+    "esnext": false,
+    "evil": false,
+    "expr": false,
+    "funcscope": false,
+    "iterator": false,
+    "lastsemic": false,
+    "laxbreak": false,
+    "laxcomma": false,
+    "loopfunc": false,
+    "moz": false,
+    "multistr": false,
+    "proto": false,
+    "scripturl": false,
+    "shadow": false,
+    "smarttabs": false,
+    "sub": false,
+    "supernew": false,
+    "validthis": false,
+
+    "browser": false,
+    "couch": false,
+    "devel": false,
+    "dojo": false,
+    "jquery": false,
+    "mootools": false,
+    "node": false,
+    "nonstandard": false,
+    "phantom": false,
+    "prototypejs": false,
+    "rhino": false,
+    "worker": false,
+    "wsh": false,
+    "yui": false,
+
+    "nomen": true,
+    "onevar": true,
+    "passfail": false,
+    "white": false
+}
+
Index: frontend/node_modules/tryer/.travis.yml
===================================================================
--- frontend/node_modules/tryer/.travis.yml	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/tryer/.travis.yml	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,11 @@
+language: node_js
+node_js:
+  - "8"
+  - "6"
+  - "4"
+  - "0.12"
+  - "0.10"
+branches:
+  only:
+    - master
+
Index: frontend/node_modules/tryer/AUTHORS
===================================================================
--- frontend/node_modules/tryer/AUTHORS	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/tryer/AUTHORS	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,3 @@
+Phil Booth <pmbooth@gmail.com> (https://philbooth.me/)
+Andrew Lawson (https://github.com/adlawson)
+Tim O'Sulg (https://github.com/timgluz)
Index: frontend/node_modules/tryer/CHANGES.md
===================================================================
--- frontend/node_modules/tryer/CHANGES.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/tryer/CHANGES.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,36 @@
+# Changes
+
+## 1.0.1
+
+### Other changes
+
+* project: migrate to gitlab (0046018)
+
+## 1.0.0
+
+* Update:a83cc21 Update dependencies. (a83cc21)
+* Fix:0c80670 Fix component info. (0c80670)
+* chore: add change log (2787c9f)
+* breaking change: export the entrypoint function directly (e56e066)
+* chore: set indentation to two spaces (04419ce)
+* chore: convert the unit tests to vanilla js (d87f514)
+* feature: add browser-based test runner (24bdeab)
+* chore: update dev deps (d56cbd4)
+* fix: fix lint errors (52ae898)
+* chore: update target node versions (d5a8b6b)
+* fix: speed up the unit tests a bit (b8adf00)
+* chore: insert whitespace (012e12b)
+* breaking change: rename from trier to tryer (d7b121d)
+* fix: tolerate missing options argument (7417ed9)
+* fix: improve the readme (807112e)
+* breaking change: ditch the stupid context and args options (009ef33)
+* feature: handle promises returned by actions (e24a0cf)
+* fix: fix lint errors (d4e6321)
+* chore: update minified lib (066bbc5)
+* chore: add a note about lib size to the readme (55b883b)
+* chore: update copyright statement (3805ac6)
+
+## 0.3.6
+
+Legacy version, `trier`.
+
Index: frontend/node_modules/tryer/COPYING
===================================================================
--- frontend/node_modules/tryer/COPYING	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/tryer/COPYING	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,20 @@
+Copyright © 2013, 2017 Phil Booth
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
Index: frontend/node_modules/tryer/README.md
===================================================================
--- frontend/node_modules/tryer/README.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/tryer/README.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,303 @@
+# tryer
+
+[![Build status](https://gitlab.com/philbooth/tryer/badges/master/pipeline.svg)](https://gitlab.com/philbooth/tryer/pipelines)
+[![Package status](https://img.shields.io/npm/v/tryer.svg)](https://www.npmjs.com/package/tryer)
+[![Downloads](https://img.shields.io/npm/dm/tryer.svg)](https://www.npmjs.com/package/tryer)
+[![License](https://img.shields.io/npm/l/tryer.svg)](https://opensource.org/licenses/MIT)
+
+
+Because everyone loves a tryer!
+Conditional
+and repeated
+function invocation
+for node
+and browser.
+
+* [Say what?](#say-what)
+* [What size is it?](#what-size-is-it)
+* [How do I install it?](#how-do-i-install-it)
+* [How do I use it?](#how-do-i-use-it)
+  * [Loading the library](#loading-the-library)
+  * [Calling the exported function](#calling-the-exported-function)
+  * [Examples](#examples)
+* [How do I set up the dev environment?](#how-do-i-set-up-the-dev-environment)
+* [What license is it released under?](#what-license-is-it-released-under)
+
+## Say what?
+
+Sometimes,
+you want to defer
+calling a function
+until a certain
+pre-requisite condition is met.
+Other times,
+you want to
+call a function
+repeatedly
+until some post-requisite condition
+is satisfied.
+Occasionally,
+you might even want
+to do both
+for the same function.
+
+To save you writing
+explicit conditions
+and loops
+on each of those occasions,
+`tryer` implements
+a predicate-based approach
+that hides the cruft
+behind a simple,
+functional interface.
+
+Additionally,
+it allows you to easily specify
+retry intervals
+and limits,
+so that your code
+doesn't hog the CPU.
+It also supports
+exponential backoff
+of retry intervals,
+which can be useful
+when handling
+indefinite error states
+such as network failure.
+
+## What size is it?
+
+5.6 kb unminified with comments, 1.1 kb minified, 0.5 kb minified + gzipped.
+
+## How do I install it?
+
+Via npm:
+
+```
+npm i tryer --save
+```
+
+Or if you just want the git repo:
+
+```
+git clone git@gitlab.com:philbooth/tryer.git
+```
+
+## How do I use it?
+
+### Loading the library
+
+If you are running in
+Node.js
+or another CommonJS-style
+environment,
+you can `require`
+tryer like so:
+
+```javascript
+const tryer = require('tryer');
+```
+
+It also the supports
+the AMD-style format
+preferred by Require.js.
+
+If you are
+including `tryer`
+with an HTML `<script>` tag,
+or neither of the above environments
+are detected,
+it will be exported globally as `tryer`.
+
+### Calling the exported function
+
+`tryer` is a function
+that can be invoked to
+call other functions
+conditionally and repeatedly,
+without the need for
+explicit `if` statements
+or loops in your own code.
+
+`tryer` takes one argument,
+an options object
+that supports
+the following properties:
+
+* `action`:
+  The function that you want to invoke.
+  If `action` returns a promise,
+  iterations will not end
+  until the promise is resolved or rejected.
+  Alternatively,
+  `action` may take a callback argument, `done`,
+  to signal that it is asynchronous.
+  In that case,
+  you are responsible
+  for calling `done`
+  when the action is finished.
+  If `action` is not set,
+  it defaults to an empty function.
+
+* `when`:
+  A predicate
+  that tests the pre-condition
+  for invoking `action`.
+  Until `when` returns true
+  (or a truthy value),
+  `action` will not be called.
+  Defaults to
+  a function that immediately returns `true`.
+
+* `until`:
+  A predicate
+  that tests the post-condition
+  for invoking `action`.
+  After `until` returns true
+  (or a truthy value),
+  `action` will no longer be called.
+  Defaults to
+  a function that immediately returns `true`.
+
+* `fail`:
+  The error handler.
+  A function
+  that will be called
+  if `limit` falsey values
+  are returned by `when` or `until`.
+  Defaults to an empty function.
+
+* `pass`:
+  Success handler.
+  A function
+  that will be called
+  after `until` has returned truthily.
+  Defaults to an empty function.
+
+* `limit`:
+  Failure limit,
+  representing the maximum number
+  of falsey returns from `when` or `until`
+  that will be permitted
+  before invocation is deemed to have failed.
+  A negative number
+  indicates that the attempt
+  should never fail,
+  instead continuing 
+  for as long as `when` and `until`
+  have returned truthy values.
+  Defaults to `-1`.
+
+* `interval`:
+  The retry interval,
+  in milliseconds.
+  A negative number indicates
+  that each subsequent retry
+  should wait for twice the interval
+  from the preceding iteration
+  (i.e. exponential backoff).
+  The default value is `-1000`,
+  signifying that
+  the initial retry interval
+  should be one second
+  and that each subsequent attempt
+  should wait for double the length
+  of the previous interval.
+
+### Examples
+
+```javascript
+// Attempt to insert a database record, waiting until `db.isConnected`
+// before doing so. The retry interval is 1 second on each iteration
+// and the call will fail after 10 attempts.
+tryer({
+  action: () => db.insert(record),
+  when: () => db.isConnected,
+  interval: 1000,
+  limit: 10,
+  fail () {
+    log.error('No database connection, terminating.');
+    process.exit(1);
+  }
+});
+```
+
+```javascript
+// Attempt to send an email message, optionally retrying with
+// exponential backoff starting at 1 second. Continue to make
+// attempts indefinitely until the call succeeds.
+let sent = false;
+tryer({
+  action (done) {
+    smtp.send(email, error => {
+      if (! error) {
+        sent = true;
+      }
+      done();
+    });
+  },
+  until: () => sent,
+  interval: -1000,
+  limit: -1
+});
+```
+
+```javascript
+// Poll a device at 30-second intervals, continuing indefinitely.
+tryer({
+  action: () => device.poll().then(response => handle(response)),
+  interval: 30000,
+  limit: -1
+});
+```
+
+## How do I set up the dev environment?
+
+The dev environment relies on
+[Chai],
+[JSHint],
+[Mocha],
+[please-release-me],
+[spooks.js] and
+[UglifyJS].
+The source code is in
+`src/tryer.js`
+and the unit tests are in
+`test/unit.js`.
+
+To install the dependencies:
+
+```
+npm i
+```
+
+To run the tests:
+
+```
+npm t
+```
+
+To lint the code:
+
+```
+npm run lint
+```
+
+To regenerate the minified lib:
+
+```
+npm run minify
+```
+
+## What license is it released under?
+
+[MIT](COPYING)
+
+[chai]: http://chaijs.com/
+[jshint]: http://jshint.com/
+[mocha]: http://mochajs.org/
+[please-release-me]: https://gitlab.com/philbooth/please-release-me
+[spooks.js]: https://gitlab.com/philbooth/spooks.js
+[uglifyjs]: http://lisperator.net/uglifyjs/
+[license]: COPYING
+
Index: frontend/node_modules/tryer/bower.json
===================================================================
--- frontend/node_modules/tryer/bower.json	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/tryer/bower.json	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,16 @@
+{
+  "name": "tryer",
+  "version": "1.0.1",
+  "main": "lib/tryer.min.js",
+  "ignore": [
+    "AUTHORS",
+    "component.json",
+    "config",
+    "node_modules",
+    "package.json",
+    "README.md",
+    "src",
+    "test"
+  ]
+}
+
Index: frontend/node_modules/tryer/component.json
===================================================================
--- frontend/node_modules/tryer/component.json	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/tryer/component.json	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,24 @@
+{
+  "name": "tryer",
+  "version": "1.0.1",
+  "description": "Because everyone loves a tryer! Conditional and repeated task invocation for node and browser.",
+  "repo": "philbooth/tryer",
+  "license": "MIT",
+  "main": "src/tryer.js",
+  "keywords": [
+    "repeat",
+    "retry",
+    "predicate",
+    "conditional",
+    "invocation",
+    "execution",
+    "loop",
+    "condition",
+    "termination",
+    "exponential",
+    "backoff"
+  ],
+  "scripts": [
+    "src/tryer.js"
+  ]
+}
Index: frontend/node_modules/tryer/lib/tryer.min.js
===================================================================
--- frontend/node_modules/tryer/lib/tryer.min.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/tryer/lib/tryer.min.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,1 @@
+!function(n){"use strict";function t(r){var n;function t(){i("when",t)&&e()}function i(n,t){return!!r[n]()||(r.count+=1,0<=(u=r).limit&&u.count>=u.limit?r.fail():(i=t,e=function(n){var t=n.interval;n.interval<0&&(n.interval*=2);return t}(r),setTimeout(i,Math.abs(e))),!1);var i,e,u}function e(){var n;if(0===r.action.length)return(n=r.action())&&f(n.then)?n.then(u,u):u();r.action(u)}function u(){i("until",e)&&r.pass()}r={count:0,when:o((n=(n=r)||{}).when),until:o(n.until),action:c(n.action),fail:c(n.fail),pass:c(n.pass),interval:a(n.interval,-1e3),limit:a(n.limit,-1)},t()}function o(n){return u(n,f,i)}function f(n){return"function"==typeof n}function i(){return!0}function c(n){return u(n,f,e)}function e(){}function u(n,t,i){return t(n)?n:i}function a(n,t){return u(n,r,t)}function r(n){return"number"==typeof n&&n==n}"function"==typeof define&&define.amd?define(function(){return t}):"undefined"!=typeof module&&null!==module?module.exports=t:n.tryer=t}(this);
Index: frontend/node_modules/tryer/package.json
===================================================================
--- frontend/node_modules/tryer/package.json	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/tryer/package.json	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,40 @@
+{
+  "name": "tryer",
+  "version": "1.0.1",
+  "description": "Because everyone loves a tryer! Conditional and repeated task invocation for node and browser.",
+  "homepage": "https://gitlab.com/philbooth/tryer",
+  "bugs": "https://gitlab.com/philbooth/tryer/issues",
+  "license": "MIT",
+  "author": "Phil Booth <pmbooth@gmail.com> (https://philbooth.me/)",
+  "main": "./src/tryer",
+  "repository": {
+    "type": "git",
+    "url": "git+https://gitlab.com/philbooth/tryer.git"
+  },
+  "keywords": [
+    "repeat",
+    "retry",
+    "predicate",
+    "conditional",
+    "invocation",
+    "execution",
+    "loop",
+    "condition",
+    "termination",
+    "exponential",
+    "backoff"
+  ],
+  "devDependencies": {
+    "chai": "4.1.x",
+    "jshint": "2.9.x",
+    "mocha": "5.2.x",
+    "please-release-me": "2.0.x",
+    "spooks": "2.0.x",
+    "uglify-js": "3.4.x"
+  },
+  "scripts": {
+    "lint": "jshint src/tryer.js test/unit.js",
+    "test": "mocha --ui tdd --reporter spec --colors test/unit.js",
+    "minify": "uglifyjs ./src/tryer.js --compress --mangle --output ./lib/tryer.min.js"
+  }
+}
Index: frontend/node_modules/tryer/src/tryer.js
===================================================================
--- frontend/node_modules/tryer/src/tryer.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/tryer/src/tryer.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,210 @@
+// Conditional and repeated task invocation for node and browser.
+
+/*globals setTimeout, define, module */
+
+(function (globals) {
+  'use strict';
+
+  if (typeof define === 'function' && define.amd) {
+    define(function () {
+      return tryer;
+    });
+  } else if (typeof module !== 'undefined' && module !== null) {
+    module.exports = tryer;
+  } else {
+    globals.tryer = tryer;
+  }
+
+  // Public function `tryer`.
+  //
+  // Performs some action when pre-requisite conditions are met and/or until
+  // post-requisite conditions are satisfied.
+  //
+  // @option action {function} The function that you want to invoke. Defaults to `() => {}`.
+  //                           If `action` returns a promise, iterations will not end until
+  //                           the promise is resolved or rejected. Alternatively, `action`
+  //                           may take a callback argument, `done`, to signal that it is
+  //                           asynchronous. In that case, you are responsible for calling
+  //                           `done` when the action is finished.
+  //
+  // @option when {function}   Predicate used to test pre-conditions. Should return `false`
+  //                           to postpone `action` or `true` to perform it. Defaults to
+  //                           `() => true`.
+  //
+  // @option until {function}  Predicate used to test post-conditions. Should return `false`
+  //                           to retry `action` or `true` to terminate it. Defaults to
+  //                           `() => true`.
+  //
+  // @option fail {function}   Callback to be invoked if `limit` tries are reached. Defaults
+  //                           to `() => {}`.
+  //
+  // @option pass {function}   Callback to be invoked after `until` has returned truthily.
+  //                           Defaults to `() => {}`.
+  //
+  // @option interval {number} Retry interval in milliseconds. A negative number indicates
+  //                           that subsequent retries should wait for double the interval
+  //                           from the preceding iteration (exponential backoff). Defaults
+  //                           to -1000.
+  //
+  // @option limit {number}    Maximum retry count, at which point the call fails and retries
+  //                           will cease. A negative number indicates that retries should
+  //                           continue indefinitely. Defaults to -1.
+  //
+  // @example
+  //   tryer({
+  //     when: () => db.isConnected,
+  //     action: () => db.insert(user),
+  //     fail () {
+  //       log.error('No database connection, terminating.');
+  //       process.exit(1);
+  //     },
+  //     interval: 1000,
+  //     limit: 10
+  //   });
+  //
+  // @example
+  //   let sent = false;
+  //   tryer({
+  //     until: () => sent,
+  //     action: done => {
+  //       smtp.send(email, error => {
+  //         if (! error) {
+  //           sent = true;
+  //         }
+  //         done();
+  //       });
+  //     },
+  //     pass: next,
+  //     interval: -1000,
+  //     limit: -1
+  //   });
+  function tryer (options) {
+    options = normaliseOptions(options);
+
+    iterateWhen();
+
+    function iterateWhen () {
+      if (preRecur()) {
+        iterateUntil();
+      }
+    }
+
+    function preRecur () {
+      return conditionallyRecur('when', iterateWhen);
+    }
+
+    function conditionallyRecur (predicateKey, iterate) {
+      if (! options[predicateKey]()) {
+        incrementCount(options);
+
+        if (shouldFail(options)) {
+          options.fail();
+        }  else {
+          recur(iterate, postIncrementInterval(options));
+        }
+
+        return false;
+      }
+
+      return true;
+    }
+
+    function iterateUntil () {
+      var result;
+
+      if (isActionSynchronous(options)) {
+        result = options.action();
+
+        if (result && isFunction(result.then)) {
+          return result.then(postRecur, postRecur);
+        }
+
+        return postRecur();
+      }
+
+      options.action(postRecur);
+    }
+
+    function postRecur () {
+      if (conditionallyRecur('until', iterateUntil)) {
+        options.pass();
+      }
+    }
+  }
+
+  function normaliseOptions (options) {
+    options = options || {};
+    return {
+      count: 0,
+      when: normalisePredicate(options.when),
+      until: normalisePredicate(options.until),
+      action: normaliseFunction(options.action),
+      fail: normaliseFunction(options.fail),
+      pass: normaliseFunction(options.pass),
+      interval: normaliseNumber(options.interval, -1000),
+      limit: normaliseNumber(options.limit, -1)
+    };
+  }
+
+  function normalisePredicate (fn) {
+    return normalise(fn, isFunction, yes);
+  }
+
+  function isFunction (fn) {
+    return typeof fn === 'function';
+  }
+
+  function yes () {
+    return true;
+  }
+
+  function normaliseFunction (fn) {
+    return normalise(fn, isFunction, nop);
+  }
+
+  function nop () {
+  }
+
+  function normalise (thing, predicate, defaultValue) {
+    if (predicate(thing)) {
+      return thing;
+    }
+
+    return defaultValue;
+  }
+
+  function normaliseNumber (number, defaultNumber) {
+    return normalise(number, isNumber, defaultNumber);
+  }
+
+  function isNumber (number) {
+    return typeof number === 'number' && number === number;
+  }
+
+  function isActionSynchronous (options) {
+    return options.action.length === 0;
+  }
+
+  function incrementCount (options) {
+    options.count += 1;
+  }
+
+  function shouldFail (options) {
+    return options.limit >= 0 && options.count >= options.limit;
+  }
+
+  function postIncrementInterval (options) {
+    var currentInterval = options.interval;
+
+    if (options.interval < 0) {
+      options.interval *= 2;
+    }
+
+    return currentInterval;
+  }
+
+  function recur (fn, interval) {
+    setTimeout(fn, Math.abs(interval));
+  }
+}(this));
+
Index: frontend/node_modules/tryer/test/index.html
===================================================================
--- frontend/node_modules/tryer/test/index.html	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/tryer/test/index.html	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <title>tryer unit tests</title>
+    <link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
+  </head>
+  <body>
+    <div id="mocha"></div>
+    <script src="../node_modules/chai/chai.js"></script>
+    <script src="../node_modules/spooks/src/spooks.min.js"></script>
+    <script src="../node_modules/mocha/mocha.js"></script>
+    <script>
+      mocha.setup('tdd');
+    </script>
+    <script src="../lib/tryer.min.js"></script>
+    <script src="unit.js"></script>
+    <script>
+      mocha.checkLeaks();
+      mocha.run();
+    </script>
+  </body>
+</html>
+
Index: frontend/node_modules/tryer/test/unit.js
===================================================================
--- frontend/node_modules/tryer/test/unit.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/tryer/test/unit.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,513 @@
+/*globals chai, spooks, require, tryer, suite, setup, test, setTimeout, Promise */
+
+(function (require, spooks) {
+  'use strict';
+
+  var assert, modulePath;
+
+  if (require === undefined) {
+    assert = chai.assert;
+    require = function () { return tryer; };
+  } else {
+    assert = require('chai').assert;
+    spooks = require('spooks');
+    modulePath = '../src/tryer';
+  }
+  
+  suite('tryer:', function () {
+    test('require does not throw', function () {
+      assert.doesNotThrow(function () {
+        require(modulePath);
+      });
+    });
+  
+    suite('require:', function () {
+      var tryer;
+  
+      setup(function () {
+        tryer = require(modulePath);
+      });
+  
+      test('function is exported', function () {
+        assert.isFunction(tryer);
+      });
+  
+      test('tryer does not throw when options is missing', function () {
+        assert.doesNotThrow(function () {
+          tryer();
+        });
+      });
+  
+      test('tryer does not throw when options is object', function () {
+        assert.doesNotThrow(function () {
+          tryer({});
+        });
+      });
+  
+      suite('when passing immediately:', function () {
+        var log, predicate, action, fail, pass;
+  
+        setup(function (done) {
+          log = {};
+          predicate = spooks.fn({ name: 'predicate', log: log, results: [ true ] });
+          action = spooks.fn({ name: 'action', log: log });
+          fail = spooks.fn({ name: 'fail', log: log, callback: done });
+          pass = spooks.fn({ name: 'pass', log: log, callback: done });
+          tryer({
+            when: predicate,
+            action: action,
+            fail: fail,
+            pass: pass,
+            interval: 0,
+            limit: 3
+          });
+        });
+  
+        test('predicate was called once', function () {
+          assert.strictEqual(log.counts.predicate, 1);
+        });
+  
+        test('action was called once', function () {
+          assert.strictEqual(log.counts.action, 1);
+        });
+  
+        test('fail was not called', function () {
+          assert.strictEqual(log.counts.fail, 0);
+        });
+  
+        test('pass was called once', function () {
+          assert.strictEqual(log.counts.pass, 1);
+        });
+      });
+  
+      suite('when failing three times:', function () {
+        var log, predicate, action, fail, pass;
+  
+        setup(function (done) {
+          log = {};
+          predicate = spooks.fn({ name: 'predicate', log: log, results: [ false ] });
+          action = spooks.fn({ name: 'action', log: log });
+          fail = spooks.fn({ name: 'fail', log: log, callback: done });
+          pass = spooks.fn({ name: 'pass', log: log, callback: done });
+          tryer({
+            when: predicate,
+            action: action,
+            fail: fail,
+            pass: pass,
+            interval: 0,
+            limit: 3
+          });
+        });
+  
+        test('predicate was called three times', function () {
+          assert.strictEqual(log.counts.predicate, 3);
+        });
+  
+        test('action was not called', function () {
+          assert.strictEqual(log.counts.action, 0);
+        });
+  
+        test('fail was called once', function () {
+          assert.strictEqual(log.counts.fail, 1);
+        });
+  
+        test('pass was not called', function () {
+          assert.strictEqual(log.counts.pass, 0);
+        });
+      });
+  
+      suite('when failing five times:', function () {
+        var log, predicate, action, fail;
+  
+        setup(function (done) {
+          log = {};
+          predicate = spooks.fn({ name: 'predicate', log: log, results: [ false ] });
+          action = spooks.fn({ name: 'action', log: log, callback: done });
+          fail = spooks.fn({ name: 'fail', log: log, callback: done });
+          tryer({ when: predicate, action: action, fail: fail, interval: 0, limit: 5 });
+        });
+  
+        test('predicate was called five times', function () {
+          assert.strictEqual(log.counts.predicate, 5);
+        });
+  
+        test('action was not called', function () {
+          assert.strictEqual(log.counts.action, 0);
+        });
+  
+        test('fail was called once', function () {
+          assert.strictEqual(log.counts.fail, 1);
+        });
+      });
+  
+      suite('when failing exponentially:', function () {
+        var log, timestamps, predicate, action, fail;
+  
+        setup(function (done) {
+          log = {};
+          timestamps = [];
+          predicate = spooks.fn({
+            name: 'predicate',
+            log: log,
+            results: [ false ],
+            callback: function () {
+              timestamps.push(Date.now());
+            }
+          });
+          action = spooks.fn({ name: 'action', log: log, callback: done });
+          fail = spooks.fn({ name: 'fail', log: log, callback: done });
+          timestamps.push(Date.now());
+          tryer({ when: predicate, action: action, fail: fail, interval: -10, limit: 4 });
+        });
+  
+        test('five timestamps were recorded', function () {
+          assert.lengthOf(timestamps, 5);
+        });
+  
+        test('first interval is immediate', function () {
+          assert.isTrue(timestamps[1] < timestamps[0] + 5);
+        });
+  
+        test('second interval is about 10 ms', function () {
+          assert.isTrue(timestamps[2] >= timestamps[1] + 10);
+          assert.isTrue(timestamps[2] < timestamps[1] + 15);
+        });
+  
+        test('third interval is about 20 ms', function () {
+          assert.isTrue(timestamps[3] >= timestamps[2] + 20);
+          assert.isTrue(timestamps[3] < timestamps[2] + 30);
+        });
+  
+        test('fourth interval is about 40 ms', function () {
+          assert.isTrue(timestamps[4] >= timestamps[3] + 40);
+          assert.isTrue(timestamps[4] < timestamps[3] + 50);
+        });
+      });
+  
+      suite('until passing immediately:', function () {
+        var log, predicate, action, fail, pass;
+  
+        setup(function (done) {
+          log = {};
+          predicate = spooks.fn({ name: 'predicate', log: log, results: [ true ] });
+          action = spooks.fn({ name: 'action', log: log });
+          fail = spooks.fn({ name: 'fail', log: log, callback: done });
+          pass = spooks.fn({ name: 'pass', log: log, callback: done });
+          tryer({
+            until: predicate,
+            action: action,
+            fail: fail,
+            pass: pass,
+            interval: 0,
+            limit: 3
+          });
+        });
+  
+        test('predicate was called once', function () {
+          assert.strictEqual(log.counts.predicate, 1);
+        });
+  
+        test('action was called once', function () {
+          assert.strictEqual(log.counts.action, 1);
+        });
+  
+        test('fail was not called', function () {
+          assert.strictEqual(log.counts.fail, 0);
+        });
+  
+        test('pass was called once', function () {
+          assert.strictEqual(log.counts.pass, 1);
+        });
+  
+        test('pass was called once', function () {
+          assert.strictEqual(log.counts.pass, 1);
+        });
+      });
+  
+      suite('until failing three times:', function () {
+        var log, predicate, action, fail, pass;
+  
+        setup(function (done) {
+          log = {};
+          predicate = spooks.fn({ name: 'predicate', log: log, results: [ false ] });
+          action = spooks.fn({ name: 'action', log: log });
+          fail = spooks.fn({ name: 'fail', log: log, callback: done });
+          pass = spooks.fn({ name: 'pass', log: log, callback: done });
+          tryer({
+            until: predicate,
+            action: action,
+            fail: fail,
+            pass: pass,
+            interval: 0,
+            limit: 3
+          });
+        });
+  
+        test('predicate was called three times', function () {
+          assert.strictEqual(log.counts.predicate, 3);
+        });
+  
+        test('action was called three times', function () {
+          assert.strictEqual(log.counts.action, 3);
+        });
+  
+        test('fail was called once', function () {
+          assert.strictEqual(log.counts.fail, 1);
+        });
+  
+        test('pass was not called', function () {
+          assert.strictEqual(log.counts.pass, 0);
+        });
+      });
+  
+      suite('until failing five times:', function () {
+        var log, predicate, action, fail;
+  
+        setup(function (done) {
+          log = {};
+          predicate = spooks.fn({ name: 'predicate', log: log, results: [ false ] });
+          action = spooks.fn({ name: 'action', log: log });
+          fail = spooks.fn({ name: 'fail', log: log, callback: done });
+          tryer({ until: predicate, action: action, fail: fail, interval: 0, limit: 5 });
+        });
+  
+        test('predicate was called five times', function () {
+          assert.strictEqual(log.counts.predicate, 5);
+        });
+  
+        test('action was called five times', function () {
+          assert.strictEqual(log.counts.action, 5);
+        });
+  
+        test('fail was called once', function () {
+          assert.strictEqual(log.counts.fail, 1);
+        });
+      });
+  
+      suite('until failing exponentially:', function () {
+        var log, timestamps, predicate, action, fail;
+  
+        setup(function (done) {
+          log = {};
+          timestamps = [];
+          predicate = spooks.fn({
+            name: 'predicate',
+            log: log,
+            results: [ false ],
+            callback: function () {
+              timestamps.push(Date.now());
+            }
+          });
+          action = spooks.fn({ name: 'action', log: log });
+          fail = spooks.fn({ name: 'fail', log: log, callback: done });
+          timestamps.push(Date.now());
+          tryer({ until: predicate, action: action, fail: fail, interval: -10, limit: 4 });
+        });
+  
+        test('five timestamps were recorded', function () {
+          assert.lengthOf(timestamps, 5);
+        });
+  
+        test('first interval is immediate', function () {
+          assert.isTrue(timestamps[1] < timestamps[0] + 5);
+        });
+  
+        test('second interval is about 10 ms', function () {
+          assert.isTrue(timestamps[2] >= timestamps[1] + 10);
+          assert.isTrue(timestamps[2] < timestamps[1] + 20);
+        });
+  
+        test('third interval is about 20 ms', function () {
+          assert.isTrue(timestamps[3] >= timestamps[2] + 20);
+          assert.isTrue(timestamps[3] < timestamps[2] + 30);
+        });
+  
+        test('fourth interval is about 40 ms', function () {
+          assert.isTrue(timestamps[4] >= timestamps[3] + 40);
+          assert.isTrue(timestamps[4] < timestamps[3] + 50);
+        });
+      });
+  
+      suite('when failing once then passing and until failing twice then passing', function () {
+        var log, predicateLoggers, predicates, action, fail, pass;
+  
+        setup(function (done) {
+          log = {};
+          predicateLoggers = {
+            when: spooks.fn({ name: 'when', log: log }),
+            until: spooks.fn({ name: 'until', log: log })
+          };
+          predicates = {
+            when: function () {
+              predicateLoggers.when.apply(this, arguments);
+              if (log.counts.when === 1) {
+                return false;
+              }
+              return true;
+            },
+            until: function () {
+              predicateLoggers.until.apply(this, arguments);
+              if (log.counts.until < 3) {
+                return false;
+              }
+              return true;
+            }
+          };
+          action = spooks.fn({ name: 'action', log: log });
+          fail = spooks.fn({ name: 'fail', log: log, callback: done });
+          pass = spooks.fn({ name: 'pass', log: log, callback: done });
+          tryer({
+            when: predicates.when,
+            until: predicates.until,
+            action: action,
+            fail: fail,
+            pass: pass,
+            interval: 0,
+            limit: 4
+          });
+        });
+  
+        test('when was called twice', function () {
+          assert.strictEqual(log.counts.when, 2);
+        });
+  
+        test('action was called three times', function () {
+          assert.strictEqual(log.counts.action, 3);
+        });
+  
+        test('until was called three times', function () {
+          assert.strictEqual(log.counts.until, 3);
+        });
+  
+        test('fail was not called', function () {
+          assert.strictEqual(log.counts.fail, 0);
+        });
+  
+        test('pass was called once', function () {
+          assert.strictEqual(log.counts.pass, 1);
+        });
+      });
+  
+      suite('asynchronous action:', function () {
+        var log, timestamps, predicate, action;
+  
+        setup(function (done) {
+          log = {};
+          timestamps = [];
+          predicate = function () {
+            timestamps.push(Date.now());
+            return false;
+          };
+          action = function (tryerDone) {
+            setTimeout(tryerDone, 10);
+          };
+          timestamps.push(Date.now());
+          tryer({ until: predicate, action: action, fail: done, interval: 0, limit: 3 });
+        });
+  
+        test('four timestamps were recorded', function () {
+          assert.lengthOf(timestamps, 4);
+        });
+  
+        test('first interval is about 10 ms', function () {
+          assert.isTrue(timestamps[1] >= timestamps[0] + 10);
+          assert.isTrue(timestamps[1] < timestamps[0] + 20);
+        });
+  
+        test('second interval is about 10 ms', function () {
+          assert.isTrue(timestamps[2] >= timestamps[1] + 10);
+          assert.isTrue(timestamps[2] < timestamps[1] + 20);
+        });
+  
+        test('third interval is about 10 ms', function () {
+          assert.isTrue(timestamps[3] >= timestamps[2] + 10);
+          assert.isTrue(timestamps[3] < timestamps[2] + 20);
+        });
+      });
+  
+      if (typeof Promise === 'function') {
+        suite('promise-resolving action:', function () {
+          var log, timestamps, predicate, action;
+  
+          setup(function (done) {
+            log = {};
+            timestamps = [];
+            predicate = function () {
+              timestamps.push(Date.now());
+              return false;
+            };
+            action = function () {
+              return new Promise(function (resolve) {
+                setTimeout(resolve, 10);
+              });
+            };
+            timestamps.push(Date.now());
+            tryer({ until: predicate, action: action, fail: done, interval: 0, limit: 3 });
+          });
+  
+          test('four timestamps were recorded', function () {
+            assert.lengthOf(timestamps, 4);
+          });
+  
+          test('first interval is about 10 ms', function () {
+            assert.isTrue(timestamps[1] >= timestamps[0] + 10);
+            assert.isTrue(timestamps[1] < timestamps[0] + 20);
+          });
+  
+          test('second interval is about 10 ms', function () {
+            assert.isTrue(timestamps[2] >= timestamps[1] + 10);
+            assert.isTrue(timestamps[2] < timestamps[1] + 20);
+          });
+  
+          test('third interval is about 10 ms', function () {
+            assert.isTrue(timestamps[3] >= timestamps[2] + 10);
+            assert.isTrue(timestamps[3] < timestamps[2] + 20);
+          });
+        });
+
+        suite('promise-rejecting action:', function () {
+          var log, timestamps, predicate, action;
+  
+          setup(function (done) {
+            log = {};
+            timestamps = [];
+            predicate = function () {
+              timestamps.push(Date.now());
+              return false;
+            };
+            action = function () {
+              return new Promise(function (_, reject) {
+                setTimeout(reject, 10);
+              });
+            };
+            timestamps.push(Date.now());
+            tryer({ until: predicate, action: action, fail: done, interval: 0, limit: 3 });
+          });
+  
+          test('four timestamps were recorded', function () {
+            assert.lengthOf(timestamps, 4);
+          });
+  
+          test('first interval is about 10 ms', function () {
+            assert.isTrue(timestamps[1] >= timestamps[0] + 10);
+            assert.isTrue(timestamps[1] < timestamps[0] + 20);
+          });
+  
+          test('second interval is about 10 ms', function () {
+            assert.isTrue(timestamps[2] >= timestamps[1] + 10);
+            assert.isTrue(timestamps[2] < timestamps[1] + 20);
+          });
+  
+          test('third interval is about 10 ms', function () {
+            assert.isTrue(timestamps[3] >= timestamps[2] + 10);
+            assert.isTrue(timestamps[3] < timestamps[2] + 20);
+          });
+        });
+      }
+    });
+  });
+}(
+  typeof require === 'function' ? require : undefined,
+  typeof spooks === 'object' ? spooks : undefined)
+);
+  
