3
\$\begingroup\$

I'm brand new here, but I've read the guidelines and looked at some up-voted questions. Hopefully I understand how I'm supposed to do this... if not, please let me know and I'll fix my question.

Explanation

I have written a simple javascript plugin of sorts. It takes the settings object for a jQuery.ajax request and a few options (frequency, failure tolerance, etc) and then periodically issues the ajax request. If the request fails, it increases the interval to the next request. If success follows a number of failures, the interval is slowly reduced to reach the original frequency.

Usage looks something like this:

var ajax = {url: 'example.com', success: mySuccessFunc};
var cw = Coward(ajax, {}); // Request my url with default settings
cw.Start();
... // Coward is running, refreshes happening periodically 
cw.Stop(); 

Purpose of Review

I'm open to any feedback about my code that you're willing to give me. However, I came here seeking a few things specifically:

  • Are there any bugs that will result in an excessive number of requests or other undesirable behavior? Am I doing anything unsafe? Race conditions?
  • JSLint warns me about scope in a few places, but the code works. Am I violating standards? Should I be handling scope differently (particularly with the 'underscore' functions and the 'live variables')?

The Code

// Coward - One time or periodic refresh that retreats if anything goes wrong.
//
// This tool was designed for use with jQuery 1.9 or later
//
// Written by Adam Jensen

function Coward(userReq, custom) {
  'use strict';
//==============================================
// Options and variables used by the coward
// - Do not change `opts` after invocation -
//==============================================
  // Combine user options with our defaults
  var opts = $.extend({
        frequency: 1000, // How often to issue ajax request
        ceiling: 10,     // After this number of failures, multiple will not be applied
        multiple: 1.5,   // Multiplier applied to
        infinite: false, // If false, coward will stop running when ceiling is passed
        verbose: false   // If true, messages will appear in console log
      }, custom);

  // Live Variables (these will change as the code runs
  // and be reset at restart)
  var running = false;
  var fails = 0;
  var next = opts.frequency;
  var nextId = "";

  // Save out user's callbacks to be executed at the proper time
  if (userReq.hasOwnProperty('success')) {
    var successCallback = userReq.success;
    delete userReq.success;
  }

  if (userReq.hasOwnProperty('error')) {
    var errorCallback = userReq.error;
    delete userReq.error;
  }

  // Combine user request sans callbacks with our default request
  var request = $.extend({
        url: 'localhost',
        method: 'GET',
        timeout: 3000,
        success: _success,
        error: _fail
      }, userReq);

  // Make sure we're not going to accidentally DoS someone
  if (opts.multiple < 1) {
    // multiple less than one results in increasing frequency on fail
    _log("Multiple CANNOT be less than one!");
    return false; // Lack of a callable will throw an error in users code
  }

//==========================================================
// Main body of Coward (_run, _success, _fail)
// - DO NOT export these functions directly -
// - INSTEAD use Start, DelayStart, and Stop -
//==========================================================
  // _run gives us a point to verify the status of the coward before the ajax call
  function _run() {
    if (!running) {
      return false;
    }

    $.ajax(request);
    return true;
  }

  // Our own callback used to check status and set next interval
  function _success(data, status, jqxhr) {
    // If there are failures, start to scale up requests again
    if (fails > 0) {
      fails--;
      next = Math.round(next/opts.multiple);
      _log("Success! Recovered a white flag, now have "+(opts.ceiling-fails)+". Next attempt in "+next);
    }

    // Set the next interval and call the users callback if set
    if (next > 0) {
      nextId = setTimeout(_run, next);
    }
    if (successCallback !== undefined) {
      successCallback(data, status, jqxhr);
    }
  }

  function _fail(jqxhr, status, error) {
    // If we have fails left, log the incident and continue
    if (fails < opts.ceiling) {
      next = Math.round(next * opts.multiple);
      _log('Refresh failed. '+(opts.ceiling-fails)+' white flags remaining. Retreating to '+next+' [front line at '+opts.frequency+'].');
      fails++;
      nextId = setTimeout(_run, next);
    } else {
      if (!opts.infinite) {
        // We've passed the ceiling and infinite is not set. Give up.
        _log('Refresh has failed more than '+fails+' times. I surrender!');
        running = false;
      } else {
        _log('Failed more than '+fails+' times, ceiling '+opts.ceiling+' prevents increasing multiple.');
        nextId = setTimeout(_run, next);
      }
    }
    if (errorCallback !== undefined) {
      errorCallback(jqxhr, status, error);
    }
  }

  function _log(msg) {
    if (opts.verbose) {
      console.log(msg);
    }
  }


//===============================================
// "Publicly Accessible" Functions
//===============================================
  var Start = function() {
    // Reset vars in case of Stop/Start cycle
    fails = 0;
    next = opts.frequency;
    nextId = "";
    running = true;
    return _run();
  };

  var DelayStart = function(delay) {
    // Start after user provided delay or one "frequency cycle"
    if (delay === undefined) {
      setTimeout(Start, opts.frequency);
      return opts.frequency;
    }
    setTimeout(Start, delay);
    return delay;
  };

  var Stop = function() {
    // Stop the run function from creating new ajax calls
    clearTimeout(nextId);
    running = false;
    return false;
  };

  var Status = function() {
    return {
      Opts: opts,
      Next: next,
      Fails: fails,
      NextId: nextId
    };
  };

  return {
    Start: Start,
    DelayStart: DelayStart,
    Stop: Stop,
    Status: Status
  };
}

The code is also available on github.

\$\endgroup\$
3
  • \$\begingroup\$ Fantastic first question. This is exemplary. Welcome to Codereview. \$\endgroup\$
    – Legato
    Oct 5, 2015 at 21:17
  • \$\begingroup\$ "Combine user request sans callbacks with our default request" - I believe your code is broken there. \$\endgroup\$
    – Joseph
    Oct 7, 2015 at 0:37
  • \$\begingroup\$ @JosephtheDreamer I appreciate you taking a look. Could you clarify what you mean when you say the code is broken? Are you seeing unexpected behavior in your testing or did some mistake catch your eye? \$\endgroup\$ Oct 7, 2015 at 4:38

1 Answer 1

2
+50
\$\begingroup\$

You should check that the supplied success and error callbacks are functions not just that they exist.

if (typeof userReq.success === "function") {
    var successCallback = userReq.success;
    delete userReq.success;
}

The way you have it now, if a user uses the following configs :

var ajax = {url: 'example.com', success: null};
var ajax = {url: 'example.com', success: "my success function"};

will cause the check below to pass, resulting in an error

if (successCallback !== undefined) {
   successCallback(data, status, jqxhr);
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.