├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGES.md ├── LICENSE ├── README.md ├── docs ├── backoff.html ├── docco.css ├── exponential.html ├── fibonacci.html ├── function_call.html ├── img │ ├── backoff_events.png │ ├── function_call_events.png │ └── layers.png ├── index.html ├── public │ ├── fonts │ │ ├── aller-bold.eot │ │ ├── aller-bold.ttf │ │ ├── aller-bold.woff │ │ ├── aller-light.eot │ │ ├── aller-light.ttf │ │ ├── aller-light.woff │ │ ├── novecento-bold.eot │ │ ├── novecento-bold.ttf │ │ └── novecento-bold.woff │ └── stylesheets │ │ └── normalize.css └── strategy.html ├── examples ├── exponential.js ├── exponential_strategy.js ├── fail.js ├── fibonacci.js ├── fibonacci_strategy.js ├── function_call.js ├── randomized.js ├── readme.js ├── reset.js └── set_timeout.js ├── index.js ├── lib ├── backoff.js ├── function_call.js └── strategy │ ├── exponential.js │ ├── fibonacci.js │ └── strategy.js ├── package.json └── tests ├── api.js ├── backoff.js ├── backoff_strategy.js ├── exponential_backoff_strategy.js ├── fibonacci_backoff_strategy.js └── function_call.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | // Based on jshint defaults: http://goo.gl/OpjUs 2 | 3 | { 4 | // If the scan should stop on first error. 5 | "passfail": false, 6 | // Maximum errors before stopping. 7 | "maxerr": 50, 8 | 9 | 10 | // Predefined globals 11 | 12 | // If the standard browser globals should be predefined. 13 | "browser": false, 14 | // If the Node.js environment globals should be predefined. 15 | "node": true, 16 | // If the Rhino environment globals should be predefined. 17 | "rhino": false, 18 | // If CouchDB globals should be predefined. 19 | "couch": false, 20 | // If the Windows Scripting Host environment globals should be predefined. 21 | "wsh": false, 22 | 23 | // If jQuery globals should be predefined. 24 | "jquery": false, 25 | // If Prototype and Scriptaculous globals should be predefined. 26 | "prototypejs": false, 27 | // If MooTools globals should be predefined. 28 | "mootools": false, 29 | // If Dojo Toolkit globals should be predefined. 30 | "dojo": false, 31 | 32 | // Custom predefined globals. 33 | "predef": [], 34 | 35 | 36 | // Development 37 | 38 | // If debugger statements should be allowed. 39 | "debug": false, 40 | // If logging globals should be predefined (console, alert, etc.). 41 | "devel": false, 42 | 43 | 44 | // ECMAScript 5 45 | 46 | // If ES5 syntax should be allowed. 47 | "es5": false, 48 | // Require the "use strict"; pragma. 49 | "strict": false, 50 | // If global "use strict"; should be allowed (also enables strict). 51 | "globalstrict": false, 52 | 53 | 54 | // The Good Parts 55 | 56 | // If automatic semicolon insertion should be tolerated. 57 | "asi": false, 58 | // If line breaks should not be checked, e.g. `return [\n] x`. 59 | "laxbreak": false, 60 | // If bitwise operators (&, |, ^, etc.) should not be allowed. 61 | "bitwise": false, 62 | // If assignments inside if, for and while should be allowed. Usually 63 | // conditions and loops are for comparison, not assignments. 64 | "boss": true, 65 | // If curly braces around all blocks should be required. 66 | "curly": true, 67 | // If === should be required. 68 | "eqeqeq": false, 69 | // If == null comparisons should be tolerated. 70 | "eqnull": false, 71 | // If eval should be allowed. 72 | "evil": true, 73 | // If ExpressionStatement should be allowed as Programs. 74 | "expr": false, 75 | // If `for in` loops must filter with `hasOwnPrototype`. 76 | "forin": false, 77 | // If immediate invocations must be wrapped in parens, e.g. 78 | // `( function(){}() );`. 79 | "immed": false, 80 | // If use before define should not be tolerated. 81 | "latedef": false, 82 | // If functions should be allowed to be defined within loops. 83 | "loopfunc": false, 84 | // If arguments.caller and arguments.callee should be disallowed. 85 | "noarg": false, 86 | // If the . should not be allowed in regexp literals. 87 | "regexp": false, 88 | // If unescaped first/last dash (-) inside brackets should be tolerated. 89 | "regexdash": false, 90 | // If script-targeted URLs should be tolerated. 91 | "scripturl": false, 92 | // If variable shadowing should be tolerated. 93 | "shadow": false, 94 | // If `new function () { ... };` and `new Object;` should be tolerated. 95 | "supernew": false, 96 | // If variables should be declared before used. 97 | "undef": true, 98 | // If `this` inside a non-constructor function is valid. 99 | "validthis": false, 100 | // If smarttabs should be tolerated 101 | // (http://www.emacswiki.org/emacs/SmartTabs). 102 | "smarttabs": false, 103 | // If the `__proto__` property should be allowed. 104 | "proto": false, 105 | // If one case switch statements should be allowed. 106 | "onecase": false, 107 | // If non-standard (but widely adopted) globals should be predefined. 108 | "nonstandard": false, 109 | // Allow multiline strings. 110 | "multistr": false, 111 | // If line breaks should not be checked around commas. 112 | "laxcomma": false, 113 | // If semicolons may be ommitted for the trailing statements inside of a 114 | // one-line blocks. 115 | "lastsemic": false, 116 | // If the `__iterator__` property should be allowed. 117 | "iterator": false, 118 | // If only function scope should be used for scope tests. 119 | "funcscope": false, 120 | // If es.next specific syntax should be allowed. 121 | "esnext": false, 122 | 123 | 124 | // Style preferences 125 | 126 | // If constructor names must be capitalized. 127 | "newcap": true, 128 | // If empty blocks should be disallowed. 129 | "noempty": false, 130 | // If using `new` for side-effects should be disallowed. 131 | "nonew": false, 132 | // If names should be checked for leading or trailing underscores 133 | // (object._attribute would be disallowed). 134 | "nomen": false, 135 | // If only one var statement per function should be allowed. 136 | "onevar": false, 137 | // If increment and decrement (`++` and `--`) should not be allowed. 138 | "plusplus": false, 139 | // If all forms of subscript notation are tolerated. 140 | "sub": true, 141 | // If trailing whitespace rules apply. 142 | "trailing": true, 143 | // If strict whitespace rules apply. 144 | "white": false, 145 | // Specify indentation. 146 | "indent": 4 147 | } 148 | 149 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | before_install: 4 | - npm install -g npm 5 | - npm install -g jshint 6 | 7 | node_js: 8 | - "node" 9 | - "iojs" 10 | 11 | notifications: 12 | email: 13 | - turcotte.mat@gmail.com 14 | 15 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.5.0 4 | 5 | Those changes are not released yet. 6 | 7 | - In the functional API, invoke the wrapped function callback on abort and emit 8 | an `abort` event. This makes it possible to detect when abort is called. 9 | - Add a method on the function API, `call.retryIf(predicate)`, which specifies 10 | a predicate used to determine whether a given error is retriable or not. The 11 | default behavior is unaffected, errors remain retriable by default. 12 | 13 | ## 2.4.1 14 | 15 | - Add support for specifying the factor to use in the `ExponentialStrategy`. 16 | 17 | ## 2.4.0 18 | 19 | - Replace `FunctionCall.getResults` by `FunctionCall.getLastResult` to avoid 20 | storing intermediary results forever as this may lead to memory exhaustion 21 | when used in conjunction with an infinite number of backoffs. 22 | - Add `FunctionCall.getNumRetries` which returns the number of times the 23 | wrapped function was retried. 24 | 25 | ## 2.3.0 26 | 27 | - Add four new methods to `FunctionCall` to query the state of the call. 28 | - isPending 29 | - isRunning 30 | - isCompleted 31 | - isAborted 32 | 33 | ## 2.2.0 34 | 35 | - To match `Backoff` default behavior, `FunctionCall` no longer sets a 36 | default failAfter of 5, i.e. the maximum number of backoffs is now 37 | unbounded by default. 38 | 39 | ## 2.1.0 40 | 41 | - `Backoff.backoff` now accepts an optional error argument that is re-emitted 42 | as the last argument of the `backoff` and `fail` events. This provides some 43 | context to the listeners as to why a given backoff operation was attempted. 44 | - The `backoff` event emitted by the `FunctionCall` class now contains, as its 45 | last argument, the error that caused the backoff operation to be attempted. 46 | This provides some context to the listeners as to why a given backoff 47 | operation was attempted. 48 | 49 | ## 2.0.0 50 | 51 | - `FunctionCall.call` renamed into `FunctionCall.start`. 52 | - `backoff.call` no longer invokes the wrapped function on `nextTick`. That 53 | way, the first attempt is not delayed until the end of the current event 54 | loop. 55 | 56 | ## 1.2.1 57 | 58 | - Make `FunctionCall.backoffFactory` a private member. 59 | 60 | ## 1.2.0 61 | 62 | - Add `backoff.call` and the associated `FunctionCall` class. 63 | 64 | ## 1.1.0 65 | 66 | - Add a `Backoff.failAfter`. 67 | 68 | ## 1.0.0 69 | 70 | - Rename `start` and `done` events `backoff` and `ready`. 71 | - Remove deprecated `backoff.fibonnaci`. 72 | 73 | ## 0.2.1 74 | 75 | - Create `backoff.fibonacci`. 76 | - Deprecate `backoff.fibonnaci`. 77 | - Expose fibonacci and exponential strategies. 78 | 79 | ## 0.2.0 80 | 81 | - Provide exponential and fibonacci backoffs. 82 | 83 | ## 0.1.0 84 | 85 | - Change `initialTimeout` and `maxTimeout` to `initialDelay` and `maxDelay`. 86 | - Use fibonnaci backoff. 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Mathieu Turcotte 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backoff for Node.js 2 | [![Build Status](https://secure.travis-ci.org/MathieuTurcotte/node-backoff.png?branch=master)](http://travis-ci.org/MathieuTurcotte/node-backoff) 3 | [![NPM version](https://badge.fury.io/js/backoff.png)](http://badge.fury.io/js/backoff) 4 | 5 | Fibonacci and exponential backoffs for Node.js. 6 | 7 | ## Installation 8 | 9 | ``` 10 | npm install backoff 11 | ``` 12 | 13 | ## Unit tests 14 | 15 | ``` 16 | npm test 17 | ``` 18 | 19 | ## Usage 20 | 21 | ### Object Oriented 22 | 23 | The usual way to instantiate a new `Backoff` object is to use one predefined 24 | factory method: `backoff.fibonacci([options])`, `backoff.exponential([options])`. 25 | 26 | `Backoff` inherits from `EventEmitter`. When a backoff starts, a `backoff` 27 | event is emitted and, when a backoff ends, a `ready` event is emitted. 28 | Handlers for these two events are called with the current backoff number and 29 | delay. 30 | 31 | ``` js 32 | var backoff = require('backoff'); 33 | 34 | var fibonacciBackoff = backoff.fibonacci({ 35 | randomisationFactor: 0, 36 | initialDelay: 10, 37 | maxDelay: 300 38 | }); 39 | 40 | fibonacciBackoff.failAfter(10); 41 | 42 | fibonacciBackoff.on('backoff', function(number, delay) { 43 | // Do something when backoff starts, e.g. show to the 44 | // user the delay before next reconnection attempt. 45 | console.log(number + ' ' + delay + 'ms'); 46 | }); 47 | 48 | fibonacciBackoff.on('ready', function(number, delay) { 49 | // Do something when backoff ends, e.g. retry a failed 50 | // operation (DNS lookup, API call, etc.). If it fails 51 | // again then backoff, otherwise reset the backoff 52 | // instance. 53 | fibonacciBackoff.backoff(); 54 | }); 55 | 56 | fibonacciBackoff.on('fail', function() { 57 | // Do something when the maximum number of backoffs is 58 | // reached, e.g. ask the user to check its connection. 59 | console.log('fail'); 60 | }); 61 | 62 | fibonacciBackoff.backoff(); 63 | ``` 64 | 65 | The previous example would print the following. 66 | 67 | ``` 68 | 0 10ms 69 | 1 10ms 70 | 2 20ms 71 | 3 30ms 72 | 4 50ms 73 | 5 80ms 74 | 6 130ms 75 | 7 210ms 76 | 8 300ms 77 | 9 300ms 78 | fail 79 | ``` 80 | 81 | Note that `Backoff` objects are meant to be instantiated once and reused 82 | several times by calling `reset` after a successful "retry". 83 | 84 | ### Functional 85 | 86 | It's also possible to avoid some boilerplate code when invoking an asynchronous 87 | function in a backoff loop by using `backoff.call(fn, [args, ...], callback)`. 88 | 89 | Typical usage looks like the following. 90 | 91 | ``` js 92 | var call = backoff.call(get, 'https://duplika.ca/', function(err, res) { 93 | console.log('Num retries: ' + call.getNumRetries()); 94 | 95 | if (err) { 96 | console.log('Error: ' + err.message); 97 | } else { 98 | console.log('Status: ' + res.statusCode); 99 | } 100 | }); 101 | 102 | call.retryIf(function(err) { return err.status == 503; }); 103 | call.setStrategy(new backoff.ExponentialStrategy()); 104 | call.failAfter(10); 105 | call.start(); 106 | ``` 107 | 108 | ## API 109 | 110 | ### backoff.fibonacci([options]) 111 | 112 | Constructs a Fibonacci backoff (10, 10, 20, 30, 50, etc.). 113 | 114 | The options are the following. 115 | 116 | - randomisationFactor: defaults to 0, must be between 0 and 1 117 | - initialDelay: defaults to 100 ms 118 | - maxDelay: defaults to 10000 ms 119 | 120 | With these values, the backoff delay will increase from 100 ms to 10000 ms. The 121 | randomisation factor controls the range of randomness and must be between 0 122 | and 1. By default, no randomisation is applied on the backoff delay. 123 | 124 | ### backoff.exponential([options]) 125 | 126 | Constructs an exponential backoff (10, 20, 40, 80, etc.). 127 | 128 | The options are the following. 129 | 130 | - randomisationFactor: defaults to 0, must be between 0 and 1 131 | - initialDelay: defaults to 100 ms 132 | - maxDelay: defaults to 10000 ms 133 | - factor: defaults to 2, must be greater than 1 134 | 135 | With these values, the backoff delay will increase from 100 ms to 10000 ms. The 136 | randomisation factor controls the range of randomness and must be between 0 137 | and 1. By default, no randomisation is applied on the backoff delay. 138 | 139 | ### backoff.call(fn, [args, ...], callback) 140 | 141 | - fn: function to call in a backoff handler, i.e. the wrapped function 142 | - args: function's arguments 143 | - callback: function's callback accepting an error as its first argument 144 | 145 | Constructs a `FunctionCall` instance for the given function. The wrapped 146 | function will get retried until it succeds or reaches the maximum number 147 | of backoffs. In both cases, the callback function will be invoked with the 148 | last result returned by the wrapped function. 149 | 150 | It is the caller's responsability to initiate the call by invoking the 151 | `start` method on the returned `FunctionCall` instance. 152 | 153 | ### Class Backoff 154 | 155 | #### new Backoff(strategy) 156 | 157 | - strategy: the backoff strategy to use 158 | 159 | Constructs a new backoff object from a specific backoff strategy. The backoff 160 | strategy must implement the `BackoffStrategy`interface defined bellow. 161 | 162 | #### backoff.failAfter(numberOfBackoffs) 163 | 164 | - numberOfBackoffs: maximum number of backoffs before the fail event gets 165 | emitted, must be greater than 0 166 | 167 | Sets a limit on the maximum number of backoffs that can be performed before 168 | a fail event gets emitted and the backoff instance is reset. By default, there 169 | is no limit on the number of backoffs that can be performed. 170 | 171 | #### backoff.backoff([err]) 172 | 173 | Starts a backoff operation. If provided, the error parameter will be emitted 174 | as the last argument of the `backoff` and `fail` events to let the listeners 175 | know why the backoff operation was attempted. 176 | 177 | An error will be thrown if a backoff operation is already in progress. 178 | 179 | In practice, this method should be called after a failed attempt to perform a 180 | sensitive operation (connecting to a database, downloading a resource over the 181 | network, etc.). 182 | 183 | #### backoff.reset() 184 | 185 | Resets the backoff delay to the initial backoff delay and stop any backoff 186 | operation in progress. After reset, a backoff instance can and should be 187 | reused. 188 | 189 | In practice, this method should be called after having successfully completed 190 | the sensitive operation guarded by the backoff instance or if the client code 191 | request to stop any reconnection attempt. 192 | 193 | #### Event: 'backoff' 194 | 195 | - number: number of backoffs since last reset, starting at 0 196 | - delay: backoff delay in milliseconds 197 | - err: optional error parameter passed to `backoff.backoff([err])` 198 | 199 | Emitted when a backoff operation is started. Signals to the client how long 200 | the next backoff delay will be. 201 | 202 | #### Event: 'ready' 203 | 204 | - number: number of backoffs since last reset, starting at 0 205 | - delay: backoff delay in milliseconds 206 | 207 | Emitted when a backoff operation is done. Signals that the failing operation 208 | should be retried. 209 | 210 | #### Event: 'fail' 211 | 212 | - err: optional error parameter passed to `backoff.backoff([err])` 213 | 214 | Emitted when the maximum number of backoffs is reached. This event will only 215 | be emitted if the client has set a limit on the number of backoffs by calling 216 | `backoff.failAfter(numberOfBackoffs)`. The backoff instance is automatically 217 | reset after this event is emitted. 218 | 219 | ### Interface BackoffStrategy 220 | 221 | A backoff strategy must provide the following methods. 222 | 223 | #### strategy.next() 224 | 225 | Computes and returns the next backoff delay. 226 | 227 | #### strategy.reset() 228 | 229 | Resets the backoff delay to its initial value. 230 | 231 | ### Class ExponentialStrategy 232 | 233 | Exponential (10, 20, 40, 80, etc.) backoff strategy implementation. 234 | 235 | #### new ExponentialStrategy([options]) 236 | 237 | The options are the following. 238 | 239 | - randomisationFactor: defaults to 0, must be between 0 and 1 240 | - initialDelay: defaults to 100 ms 241 | - maxDelay: defaults to 10000 ms 242 | - factor: defaults to 2, must be greater than 1 243 | 244 | ### Class FibonacciStrategy 245 | 246 | Fibonacci (10, 10, 20, 30, 50, etc.) backoff strategy implementation. 247 | 248 | #### new FibonacciStrategy([options]) 249 | 250 | The options are the following. 251 | 252 | - randomisationFactor: defaults to 0, must be between 0 and 1 253 | - initialDelay: defaults to 100 ms 254 | - maxDelay: defaults to 10000 ms 255 | 256 | ### Class FunctionCall 257 | 258 | This class manages the calling of an asynchronous function within a backoff 259 | loop. 260 | 261 | This class should rarely be instantiated directly since the factory method 262 | `backoff.call(fn, [args, ...], callback)` offers a more convenient and safer 263 | way to create `FunctionCall` instances. 264 | 265 | #### new FunctionCall(fn, args, callback) 266 | 267 | - fn: asynchronous function to call 268 | - args: an array containing fn's args 269 | - callback: fn's callback 270 | 271 | Constructs a function handler for the given asynchronous function. 272 | 273 | #### call.isPending() 274 | 275 | Returns whether the call is pending, i.e. hasn't been started. 276 | 277 | #### call.isRunning() 278 | 279 | Returns whether the call is in progress. 280 | 281 | #### call.isCompleted() 282 | 283 | Returns whether the call is completed. 284 | 285 | #### call.isAborted() 286 | 287 | Returns whether the call is aborted. 288 | 289 | #### call.setStrategy(strategy) 290 | 291 | - strategy: strategy instance to use, defaults to `FibonacciStrategy`. 292 | 293 | Sets the backoff strategy to use. This method should be called before 294 | `call.start()` otherwise an exception will be thrown. 295 | 296 | #### call.failAfter(maxNumberOfBackoffs) 297 | 298 | - maxNumberOfBackoffs: maximum number of backoffs before the call is aborted 299 | 300 | Sets the maximum number of backoffs before the call is aborted. By default, 301 | there is no limit on the number of backoffs that can be performed. 302 | 303 | This method should be called before `call.start()` otherwise an exception will 304 | be thrown.. 305 | 306 | #### call.retryIf(predicate) 307 | 308 | - predicate: a function which takes in as its argument the error returned 309 | by the wrapped function and determines whether it is retriable. 310 | 311 | Sets the predicate which will be invoked to determine whether a given error 312 | should be retried or not, e.g. a network error would be retriable while a type 313 | error would stop the function call. By default, all errors are considered to be 314 | retriable. 315 | 316 | This method should be called before `call.start()` otherwise an exception will 317 | be thrown. 318 | 319 | #### call.getLastResult() 320 | 321 | Returns an array containing the last arguments passed to the completion callback 322 | of the wrapped function. For example, to get the error code returned by the last 323 | call, one would do the following. 324 | 325 | ``` js 326 | var results = call.getLastResult(); 327 | // The error code is the first parameter of the callback. 328 | var error = results[0]; 329 | ``` 330 | 331 | Note that if the call was aborted, it will contain the abort error and not the 332 | last error returned by the wrapped function. 333 | 334 | #### call.getNumRetries() 335 | 336 | Returns the number of times the wrapped function call was retried. For a 337 | wrapped function that succeeded immediately, this would return 0. This 338 | method can be called at any point in time during the call life cycle, i.e. 339 | before, during and after the wrapped function invocation. 340 | 341 | #### call.start() 342 | 343 | Initiates the call the wrapped function. This method should only be called 344 | once otherwise an exception will be thrown. 345 | 346 | #### call.abort() 347 | 348 | Aborts the call and causes the completion callback to be invoked with an abort 349 | error if the call was pending or running; does nothing otherwise. This method 350 | can safely be called mutliple times. 351 | 352 | #### Event: 'call' 353 | 354 | - args: wrapped function's arguments 355 | 356 | Emitted each time the wrapped function is called. 357 | 358 | #### Event: 'callback' 359 | 360 | - results: wrapped function's return values 361 | 362 | Emitted each time the wrapped function invokes its callback. 363 | 364 | #### Event: 'backoff' 365 | 366 | - number: backoff number, starts at 0 367 | - delay: backoff delay in milliseconds 368 | - err: the error that triggered the backoff operation 369 | 370 | Emitted each time a backoff operation is started. 371 | 372 | #### Event: 'abort' 373 | 374 | Emitted when a call is aborted. 375 | 376 | ## Annotated source code 377 | 378 | The annotated source code can be found at [mathieuturcotte.github.io/node-backoff/docs](http://mathieuturcotte.github.io/node-backoff/docs/). 379 | 380 | ## License 381 | 382 | This code is free to use under the terms of the [MIT license](http://mturcotte.mit-license.org/). 383 | -------------------------------------------------------------------------------- /docs/backoff.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | backoff.js 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 54 | 55 | 203 |
204 | 205 | 206 | -------------------------------------------------------------------------------- /docs/docco.css: -------------------------------------------------------------------------------- 1 | /*--------------------- Typography ----------------------------*/ 2 | 3 | @font-face { 4 | font-family: 'aller-light'; 5 | src: url('public/fonts/aller-light.eot'); 6 | src: url('public/fonts/aller-light.eot?#iefix') format('embedded-opentype'), 7 | url('public/fonts/aller-light.woff') format('woff'), 8 | url('public/fonts/aller-light.ttf') format('truetype'); 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | 13 | @font-face { 14 | font-family: 'aller-bold'; 15 | src: url('public/fonts/aller-bold.eot'); 16 | src: url('public/fonts/aller-bold.eot?#iefix') format('embedded-opentype'), 17 | url('public/fonts/aller-bold.woff') format('woff'), 18 | url('public/fonts/aller-bold.ttf') format('truetype'); 19 | font-weight: normal; 20 | font-style: normal; 21 | } 22 | 23 | @font-face { 24 | font-family: 'novecento-bold'; 25 | src: url('public/fonts/novecento-bold.eot'); 26 | src: url('public/fonts/novecento-bold.eot?#iefix') format('embedded-opentype'), 27 | url('public/fonts/novecento-bold.woff') format('woff'), 28 | url('public/fonts/novecento-bold.ttf') format('truetype'); 29 | font-weight: normal; 30 | font-style: normal; 31 | } 32 | 33 | /*--------------------- Layout ----------------------------*/ 34 | html { height: 100%; } 35 | body { 36 | font-family: "aller-light"; 37 | font-size: 14px; 38 | line-height: 18px; 39 | color: #30404f; 40 | margin: 0; padding: 0; 41 | height:100%; 42 | } 43 | #container { min-height: 100%; } 44 | 45 | a { 46 | color: #000; 47 | } 48 | 49 | b, strong { 50 | font-weight: normal; 51 | font-family: "aller-bold"; 52 | } 53 | 54 | p { 55 | margin: 15px 0 0px; 56 | } 57 | .annotation ul, .annotation ol { 58 | margin: 25px 0; 59 | } 60 | .annotation ul li, .annotation ol li { 61 | font-size: 14px; 62 | line-height: 18px; 63 | margin: 10px 0; 64 | } 65 | 66 | h1, h2, h3, h4, h5, h6 { 67 | color: #112233; 68 | line-height: 1em; 69 | font-weight: normal; 70 | font-family: "novecento-bold"; 71 | text-transform: uppercase; 72 | margin: 30px 0 15px 0; 73 | } 74 | 75 | h1 { 76 | margin-top: 40px; 77 | } 78 | 79 | hr { 80 | border: 0; 81 | background: 1px #ddd; 82 | height: 1px; 83 | margin: 20px 0; 84 | } 85 | 86 | pre, tt, code { 87 | font-size: 12px; line-height: 16px; 88 | font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; 89 | margin: 0; padding: 0; 90 | } 91 | .annotation pre { 92 | display: block; 93 | margin: 0; 94 | padding: 7px 10px; 95 | background: #fcfcfc; 96 | -moz-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 97 | -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 98 | box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 99 | overflow-x: auto; 100 | } 101 | .annotation pre code { 102 | border: 0; 103 | padding: 0; 104 | background: transparent; 105 | } 106 | 107 | 108 | blockquote { 109 | border-left: 5px solid #ccc; 110 | margin: 0; 111 | padding: 1px 0 1px 1em; 112 | } 113 | .sections blockquote p { 114 | font-family: Menlo, Consolas, Monaco, monospace; 115 | font-size: 12px; line-height: 16px; 116 | color: #999; 117 | margin: 10px 0 0; 118 | white-space: pre-wrap; 119 | } 120 | 121 | ul.sections { 122 | list-style: none; 123 | padding:0 0 5px 0;; 124 | margin:0; 125 | } 126 | 127 | /* 128 | Force border-box so that % widths fit the parent 129 | container without overlap because of margin/padding. 130 | 131 | More Info : http://www.quirksmode.org/css/box.html 132 | */ 133 | ul.sections > li > div { 134 | -moz-box-sizing: border-box; /* firefox */ 135 | -ms-box-sizing: border-box; /* ie */ 136 | -webkit-box-sizing: border-box; /* webkit */ 137 | -khtml-box-sizing: border-box; /* konqueror */ 138 | box-sizing: border-box; /* css3 */ 139 | } 140 | 141 | 142 | /*---------------------- Jump Page -----------------------------*/ 143 | #jump_to, #jump_page { 144 | margin: 0; 145 | background: white; 146 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; 147 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; 148 | font: 16px Arial; 149 | cursor: pointer; 150 | text-align: right; 151 | list-style: none; 152 | } 153 | 154 | #jump_to a { 155 | text-decoration: none; 156 | } 157 | 158 | #jump_to a.large { 159 | display: none; 160 | } 161 | #jump_to a.small { 162 | font-size: 22px; 163 | font-weight: bold; 164 | color: #676767; 165 | } 166 | 167 | #jump_to, #jump_wrapper { 168 | position: fixed; 169 | right: 0; top: 0; 170 | padding: 10px 15px; 171 | margin:0; 172 | } 173 | 174 | #jump_wrapper { 175 | display: none; 176 | padding:0; 177 | } 178 | 179 | #jump_to:hover #jump_wrapper { 180 | display: block; 181 | } 182 | 183 | #jump_page { 184 | padding: 5px 0 3px; 185 | margin: 0 0 25px 25px; 186 | } 187 | 188 | #jump_page .source { 189 | display: block; 190 | padding: 15px; 191 | text-decoration: none; 192 | border-top: 1px solid #eee; 193 | } 194 | 195 | #jump_page .source:hover { 196 | background: #f5f5ff; 197 | } 198 | 199 | #jump_page .source:first-child { 200 | } 201 | 202 | /*---------------------- Low resolutions (> 320px) ---------------------*/ 203 | @media only screen and (min-width: 320px) { 204 | .pilwrap { display: none; } 205 | 206 | ul.sections > li > div { 207 | display: block; 208 | padding:5px 10px 0 10px; 209 | } 210 | 211 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 212 | padding-left: 30px; 213 | } 214 | 215 | ul.sections > li > div.content { 216 | overflow-x:auto; 217 | -webkit-box-shadow: inset 0 0 5px #e5e5ee; 218 | box-shadow: inset 0 0 5px #e5e5ee; 219 | border: 1px solid #dedede; 220 | margin:5px 10px 5px 10px; 221 | padding-bottom: 5px; 222 | } 223 | 224 | ul.sections > li > div.annotation pre { 225 | margin: 7px 0 7px; 226 | padding-left: 15px; 227 | } 228 | 229 | ul.sections > li > div.annotation p tt, .annotation code { 230 | background: #f8f8ff; 231 | border: 1px solid #dedede; 232 | font-size: 12px; 233 | padding: 0 0.2em; 234 | } 235 | } 236 | 237 | /*---------------------- (> 481px) ---------------------*/ 238 | @media only screen and (min-width: 481px) { 239 | #container { 240 | position: relative; 241 | } 242 | body { 243 | background-color: #F5F5FF; 244 | font-size: 15px; 245 | line-height: 21px; 246 | } 247 | pre, tt, code { 248 | line-height: 18px; 249 | } 250 | p, ul, ol { 251 | margin: 0 0 15px; 252 | } 253 | 254 | 255 | #jump_to { 256 | padding: 5px 10px; 257 | } 258 | #jump_wrapper { 259 | padding: 0; 260 | } 261 | #jump_to, #jump_page { 262 | font: 10px Arial; 263 | text-transform: uppercase; 264 | } 265 | #jump_page .source { 266 | padding: 5px 10px; 267 | } 268 | #jump_to a.large { 269 | display: inline-block; 270 | } 271 | #jump_to a.small { 272 | display: none; 273 | } 274 | 275 | 276 | 277 | #background { 278 | position: absolute; 279 | top: 0; bottom: 0; 280 | width: 350px; 281 | background: #fff; 282 | border-right: 1px solid #e5e5ee; 283 | z-index: -1; 284 | } 285 | 286 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 287 | padding-left: 40px; 288 | } 289 | 290 | ul.sections > li { 291 | white-space: nowrap; 292 | } 293 | 294 | ul.sections > li > div { 295 | display: inline-block; 296 | } 297 | 298 | ul.sections > li > div.annotation { 299 | max-width: 350px; 300 | min-width: 350px; 301 | min-height: 5px; 302 | padding: 13px; 303 | overflow-x: hidden; 304 | white-space: normal; 305 | vertical-align: top; 306 | text-align: left; 307 | } 308 | ul.sections > li > div.annotation pre { 309 | margin: 15px 0 15px; 310 | padding-left: 15px; 311 | } 312 | 313 | ul.sections > li > div.content { 314 | padding: 13px; 315 | vertical-align: top; 316 | border: none; 317 | -webkit-box-shadow: none; 318 | box-shadow: none; 319 | } 320 | 321 | .pilwrap { 322 | position: relative; 323 | display: inline; 324 | } 325 | 326 | .pilcrow { 327 | font: 12px Arial; 328 | text-decoration: none; 329 | color: #454545; 330 | position: absolute; 331 | top: 3px; left: -20px; 332 | padding: 1px 2px; 333 | opacity: 0; 334 | -webkit-transition: opacity 0.2s linear; 335 | } 336 | .for-h1 .pilcrow { 337 | top: 47px; 338 | } 339 | .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow { 340 | top: 35px; 341 | } 342 | 343 | ul.sections > li > div.annotation:hover .pilcrow { 344 | opacity: 1; 345 | } 346 | } 347 | 348 | /*---------------------- (> 1025px) ---------------------*/ 349 | @media only screen and (min-width: 1025px) { 350 | 351 | body { 352 | font-size: 16px; 353 | line-height: 24px; 354 | } 355 | 356 | #background { 357 | width: 525px; 358 | } 359 | ul.sections > li > div.annotation { 360 | max-width: 525px; 361 | min-width: 525px; 362 | padding: 10px 25px 1px 50px; 363 | } 364 | ul.sections > li > div.content { 365 | padding: 9px 15px 16px 25px; 366 | } 367 | } 368 | 369 | /*---------------------- Syntax Highlighting -----------------------------*/ 370 | 371 | td.linenos { background-color: #f0f0f0; padding-right: 10px; } 372 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } 373 | /* 374 | 375 | github.com style (c) Vasily Polovnyov 376 | 377 | */ 378 | 379 | pre code { 380 | display: block; padding: 0.5em; 381 | color: #000; 382 | background: #f8f8ff 383 | } 384 | 385 | pre .hljs-comment, 386 | pre .hljs-template_comment, 387 | pre .hljs-diff .hljs-header, 388 | pre .hljs-javadoc { 389 | color: #408080; 390 | font-style: italic 391 | } 392 | 393 | pre .hljs-keyword, 394 | pre .hljs-assignment, 395 | pre .hljs-literal, 396 | pre .hljs-css .hljs-rule .hljs-keyword, 397 | pre .hljs-winutils, 398 | pre .hljs-javascript .hljs-title, 399 | pre .hljs-lisp .hljs-title, 400 | pre .hljs-subst { 401 | color: #954121; 402 | /*font-weight: bold*/ 403 | } 404 | 405 | pre .hljs-number, 406 | pre .hljs-hexcolor { 407 | color: #40a070 408 | } 409 | 410 | pre .hljs-string, 411 | pre .hljs-tag .hljs-value, 412 | pre .hljs-phpdoc, 413 | pre .hljs-tex .hljs-formula { 414 | color: #219161; 415 | } 416 | 417 | pre .hljs-title, 418 | pre .hljs-id { 419 | color: #19469D; 420 | } 421 | pre .hljs-params { 422 | color: #00F; 423 | } 424 | 425 | pre .hljs-javascript .hljs-title, 426 | pre .hljs-lisp .hljs-title, 427 | pre .hljs-subst { 428 | font-weight: normal 429 | } 430 | 431 | pre .hljs-class .hljs-title, 432 | pre .hljs-haskell .hljs-label, 433 | pre .hljs-tex .hljs-command { 434 | color: #458; 435 | font-weight: bold 436 | } 437 | 438 | pre .hljs-tag, 439 | pre .hljs-tag .hljs-title, 440 | pre .hljs-rules .hljs-property, 441 | pre .hljs-django .hljs-tag .hljs-keyword { 442 | color: #000080; 443 | font-weight: normal 444 | } 445 | 446 | pre .hljs-attribute, 447 | pre .hljs-variable, 448 | pre .hljs-instancevar, 449 | pre .hljs-lisp .hljs-body { 450 | color: #008080 451 | } 452 | 453 | pre .hljs-regexp { 454 | color: #B68 455 | } 456 | 457 | pre .hljs-class { 458 | color: #458; 459 | font-weight: bold 460 | } 461 | 462 | pre .hljs-symbol, 463 | pre .hljs-ruby .hljs-symbol .hljs-string, 464 | pre .hljs-ruby .hljs-symbol .hljs-keyword, 465 | pre .hljs-ruby .hljs-symbol .hljs-keymethods, 466 | pre .hljs-lisp .hljs-keyword, 467 | pre .hljs-tex .hljs-special, 468 | pre .hljs-input_number { 469 | color: #990073 470 | } 471 | 472 | pre .hljs-builtin, 473 | pre .hljs-constructor, 474 | pre .hljs-built_in, 475 | pre .hljs-lisp .hljs-title { 476 | color: #0086b3 477 | } 478 | 479 | pre .hljs-preprocessor, 480 | pre .hljs-pi, 481 | pre .hljs-doctype, 482 | pre .hljs-shebang, 483 | pre .hljs-cdata { 484 | color: #999; 485 | font-weight: bold 486 | } 487 | 488 | pre .hljs-deletion { 489 | background: #fdd 490 | } 491 | 492 | pre .hljs-addition { 493 | background: #dfd 494 | } 495 | 496 | pre .hljs-diff .hljs-change { 497 | background: #0086b3 498 | } 499 | 500 | pre .hljs-chunk { 501 | color: #aaa 502 | } 503 | 504 | pre .hljs-tex .hljs-formula { 505 | opacity: 0.5; 506 | } 507 | -------------------------------------------------------------------------------- /docs/exponential.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | exponential.js 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 54 | 55 | 143 |
144 | 145 | 146 | -------------------------------------------------------------------------------- /docs/fibonacci.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | fibonacci.js 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 54 | 55 | 118 |
119 | 120 | 121 | -------------------------------------------------------------------------------- /docs/function_call.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | function_call.js 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 54 | 55 | 490 |
491 | 492 | 493 | -------------------------------------------------------------------------------- /docs/img/backoff_events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/img/backoff_events.png -------------------------------------------------------------------------------- /docs/img/function_call_events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/img/function_call_events.png -------------------------------------------------------------------------------- /docs/img/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/img/layers.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | index.js 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 54 | 55 | 145 |
146 | 147 | 148 | -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/aller-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/aller-bold.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/aller-bold.woff -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/aller-light.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/aller-light.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/aller-light.woff -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/novecento-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/novecento-bold.ttf -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathieuTurcotte/node-backoff/f384eff062d585ebd878aab7ecf29eb56c1efd7d/docs/public/fonts/novecento-bold.woff -------------------------------------------------------------------------------- /docs/public/stylesheets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.0.1 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /* 8 | * Corrects `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | nav, 20 | section, 21 | summary { 22 | display: block; 23 | } 24 | 25 | /* 26 | * Corrects `inline-block` display not defined in IE 8/9. 27 | */ 28 | 29 | audio, 30 | canvas, 31 | video { 32 | display: inline-block; 33 | } 34 | 35 | /* 36 | * Prevents modern browsers from displaying `audio` without controls. 37 | * Remove excess height in iOS 5 devices. 38 | */ 39 | 40 | audio:not([controls]) { 41 | display: none; 42 | height: 0; 43 | } 44 | 45 | /* 46 | * Addresses styling for `hidden` attribute not present in IE 8/9. 47 | */ 48 | 49 | [hidden] { 50 | display: none; 51 | } 52 | 53 | /* ========================================================================== 54 | Base 55 | ========================================================================== */ 56 | 57 | /* 58 | * 1. Sets default font family to sans-serif. 59 | * 2. Prevents iOS text size adjust after orientation change, without disabling 60 | * user zoom. 61 | */ 62 | 63 | html { 64 | font-family: sans-serif; /* 1 */ 65 | -webkit-text-size-adjust: 100%; /* 2 */ 66 | -ms-text-size-adjust: 100%; /* 2 */ 67 | } 68 | 69 | /* 70 | * Removes default margin. 71 | */ 72 | 73 | body { 74 | margin: 0; 75 | } 76 | 77 | /* ========================================================================== 78 | Links 79 | ========================================================================== */ 80 | 81 | /* 82 | * Addresses `outline` inconsistency between Chrome and other browsers. 83 | */ 84 | 85 | a:focus { 86 | outline: thin dotted; 87 | } 88 | 89 | /* 90 | * Improves readability when focused and also mouse hovered in all browsers. 91 | */ 92 | 93 | a:active, 94 | a:hover { 95 | outline: 0; 96 | } 97 | 98 | /* ========================================================================== 99 | Typography 100 | ========================================================================== */ 101 | 102 | /* 103 | * Addresses `h1` font sizes within `section` and `article` in Firefox 4+, 104 | * Safari 5, and Chrome. 105 | */ 106 | 107 | h1 { 108 | font-size: 2em; 109 | } 110 | 111 | /* 112 | * Addresses styling not present in IE 8/9, Safari 5, and Chrome. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: 1px dotted; 117 | } 118 | 119 | /* 120 | * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: bold; 126 | } 127 | 128 | /* 129 | * Addresses styling not present in Safari 5 and Chrome. 130 | */ 131 | 132 | dfn { 133 | font-style: italic; 134 | } 135 | 136 | /* 137 | * Addresses styling not present in IE 8/9. 138 | */ 139 | 140 | mark { 141 | background: #ff0; 142 | color: #000; 143 | } 144 | 145 | 146 | /* 147 | * Corrects font family set oddly in Safari 5 and Chrome. 148 | */ 149 | 150 | code, 151 | kbd, 152 | pre, 153 | samp { 154 | font-family: monospace, serif; 155 | font-size: 1em; 156 | } 157 | 158 | /* 159 | * Improves readability of pre-formatted text in all browsers. 160 | */ 161 | 162 | pre { 163 | white-space: pre; 164 | white-space: pre-wrap; 165 | word-wrap: break-word; 166 | } 167 | 168 | /* 169 | * Sets consistent quote types. 170 | */ 171 | 172 | q { 173 | quotes: "\201C" "\201D" "\2018" "\2019"; 174 | } 175 | 176 | /* 177 | * Addresses inconsistent and variable font size in all browsers. 178 | */ 179 | 180 | small { 181 | font-size: 80%; 182 | } 183 | 184 | /* 185 | * Prevents `sub` and `sup` affecting `line-height` in all browsers. 186 | */ 187 | 188 | sub, 189 | sup { 190 | font-size: 75%; 191 | line-height: 0; 192 | position: relative; 193 | vertical-align: baseline; 194 | } 195 | 196 | sup { 197 | top: -0.5em; 198 | } 199 | 200 | sub { 201 | bottom: -0.25em; 202 | } 203 | 204 | /* ========================================================================== 205 | Embedded content 206 | ========================================================================== */ 207 | 208 | /* 209 | * Removes border when inside `a` element in IE 8/9. 210 | */ 211 | 212 | img { 213 | border: 0; 214 | } 215 | 216 | /* 217 | * Corrects overflow displayed oddly in IE 9. 218 | */ 219 | 220 | svg:not(:root) { 221 | overflow: hidden; 222 | } 223 | 224 | /* ========================================================================== 225 | Figures 226 | ========================================================================== */ 227 | 228 | /* 229 | * Addresses margin not present in IE 8/9 and Safari 5. 230 | */ 231 | 232 | figure { 233 | margin: 0; 234 | } 235 | 236 | /* ========================================================================== 237 | Forms 238 | ========================================================================== */ 239 | 240 | /* 241 | * Define consistent border, margin, and padding. 242 | */ 243 | 244 | fieldset { 245 | border: 1px solid #c0c0c0; 246 | margin: 0 2px; 247 | padding: 0.35em 0.625em 0.75em; 248 | } 249 | 250 | /* 251 | * 1. Corrects color not being inherited in IE 8/9. 252 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 253 | */ 254 | 255 | legend { 256 | border: 0; /* 1 */ 257 | padding: 0; /* 2 */ 258 | } 259 | 260 | /* 261 | * 1. Corrects font family not being inherited in all browsers. 262 | * 2. Corrects font size not being inherited in all browsers. 263 | * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome 264 | */ 265 | 266 | button, 267 | input, 268 | select, 269 | textarea { 270 | font-family: inherit; /* 1 */ 271 | font-size: 100%; /* 2 */ 272 | margin: 0; /* 3 */ 273 | } 274 | 275 | /* 276 | * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in 277 | * the UA stylesheet. 278 | */ 279 | 280 | button, 281 | input { 282 | line-height: normal; 283 | } 284 | 285 | /* 286 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 287 | * and `video` controls. 288 | * 2. Corrects inability to style clickable `input` types in iOS. 289 | * 3. Improves usability and consistency of cursor style between image-type 290 | * `input` and others. 291 | */ 292 | 293 | button, 294 | html input[type="button"], /* 1 */ 295 | input[type="reset"], 296 | input[type="submit"] { 297 | -webkit-appearance: button; /* 2 */ 298 | cursor: pointer; /* 3 */ 299 | } 300 | 301 | /* 302 | * Re-set default cursor for disabled elements. 303 | */ 304 | 305 | button[disabled], 306 | input[disabled] { 307 | cursor: default; 308 | } 309 | 310 | /* 311 | * 1. Addresses box sizing set to `content-box` in IE 8/9. 312 | * 2. Removes excess padding in IE 8/9. 313 | */ 314 | 315 | input[type="checkbox"], 316 | input[type="radio"] { 317 | box-sizing: border-box; /* 1 */ 318 | padding: 0; /* 2 */ 319 | } 320 | 321 | /* 322 | * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. 323 | * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome 324 | * (include `-moz` to future-proof). 325 | */ 326 | 327 | input[type="search"] { 328 | -webkit-appearance: textfield; /* 1 */ 329 | -moz-box-sizing: content-box; 330 | -webkit-box-sizing: content-box; /* 2 */ 331 | box-sizing: content-box; 332 | } 333 | 334 | /* 335 | * Removes inner padding and search cancel button in Safari 5 and Chrome 336 | * on OS X. 337 | */ 338 | 339 | input[type="search"]::-webkit-search-cancel-button, 340 | input[type="search"]::-webkit-search-decoration { 341 | -webkit-appearance: none; 342 | } 343 | 344 | /* 345 | * Removes inner padding and border in Firefox 4+. 346 | */ 347 | 348 | button::-moz-focus-inner, 349 | input::-moz-focus-inner { 350 | border: 0; 351 | padding: 0; 352 | } 353 | 354 | /* 355 | * 1. Removes default vertical scrollbar in IE 8/9. 356 | * 2. Improves readability and alignment in all browsers. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; /* 1 */ 361 | vertical-align: top; /* 2 */ 362 | } 363 | 364 | /* ========================================================================== 365 | Tables 366 | ========================================================================== */ 367 | 368 | /* 369 | * Remove most spacing between table cells. 370 | */ 371 | 372 | table { 373 | border-collapse: collapse; 374 | border-spacing: 0; 375 | } -------------------------------------------------------------------------------- /docs/strategy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | strategy.js 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 54 | 55 | 243 |
244 | 245 | 246 | -------------------------------------------------------------------------------- /examples/exponential.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var backoff = require('../index'); 4 | 5 | var testBackoff = backoff.exponential({ 6 | initialDelay: 10, 7 | maxDelay: 1000 8 | }); 9 | 10 | testBackoff.on('backoff', function(number, delay) { 11 | console.log('Backoff start: ' + number + ' ' + delay + 'ms'); 12 | }); 13 | 14 | testBackoff.on('ready', function(number, delay) { 15 | console.log('Backoff done: ' + number + ' ' + delay + 'ms'); 16 | 17 | if (number < 15) { 18 | testBackoff.backoff(); 19 | } 20 | }); 21 | 22 | testBackoff.backoff(); 23 | -------------------------------------------------------------------------------- /examples/exponential_strategy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var backoff = require('../index'); 4 | 5 | var strategy = new backoff.ExponentialStrategy(); 6 | 7 | for (var i = 0; i < 10; i++) { 8 | console.log(strategy.next()); 9 | } 10 | -------------------------------------------------------------------------------- /examples/fail.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var backoff = require('../index'); 4 | 5 | var testBackoff = backoff.exponential({ 6 | initialDelay: 10, 7 | maxDelay: 1000 8 | }); 9 | 10 | testBackoff.failAfter(5); 11 | 12 | testBackoff.on('backoff', function(number, delay) { 13 | console.log('Backoff start: ' + number + ' ' + delay + 'ms'); 14 | }); 15 | 16 | testBackoff.on('ready', function(number, delay) { 17 | console.log('Backoff done: ' + number + ' ' + delay + 'ms'); 18 | testBackoff.backoff(); // Launch a new backoff. 19 | }); 20 | 21 | testBackoff.on('fail', function() { 22 | console.log('Backoff failure.'); 23 | }); 24 | 25 | testBackoff.backoff(); 26 | -------------------------------------------------------------------------------- /examples/fibonacci.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var backoff = require('../index'); 4 | 5 | var testBackoff = backoff.fibonacci({ 6 | initialDelay: 10, 7 | maxDelay: 1000 8 | }); 9 | 10 | testBackoff.on('backoff', function(number, delay) { 11 | console.log('Backoff start: ' + number + ' ' + delay + 'ms'); 12 | }); 13 | 14 | testBackoff.on('ready', function(number, delay) { 15 | console.log('Backoff done: ' + number + ' ' + delay + 'ms'); 16 | 17 | if (number < 15) { 18 | testBackoff.backoff(); 19 | } 20 | }); 21 | 22 | testBackoff.backoff(); 23 | -------------------------------------------------------------------------------- /examples/fibonacci_strategy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var backoff = require('../index'); 4 | 5 | var strategy = new backoff.FibonacciStrategy(); 6 | 7 | for (var i = 0; i < 10; i++) { 8 | console.log(strategy.next()); 9 | } 10 | -------------------------------------------------------------------------------- /examples/function_call.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var backoff = require('../index.js'), 4 | util = require('util'), 5 | http = require('http'); 6 | 7 | var URL = 'http://www.iana.org/domains/example/'; 8 | 9 | function get(options, callback) { 10 | http.get(options, function(res) { 11 | res.setEncoding('utf8'); 12 | res.data = ''; 13 | res.on('data', function (chunk) { 14 | res.data += chunk; 15 | }); 16 | res.on('end', function() { 17 | callback(null, res); 18 | }); 19 | res.on('close', function(err) { 20 | callback(err, res); 21 | }); 22 | }).on('error', function(err) { 23 | callback(err, null); 24 | }); 25 | } 26 | 27 | 28 | var call = backoff.call(get, URL, function(err, res) { 29 | // Notice how the call is captured inside the closure. 30 | console.log('Num retries: ' + call.getNumRetries()); 31 | 32 | if (err) { 33 | console.log('Error: ' + err.message); 34 | } else { 35 | console.log('Status: ' + res.statusCode); 36 | } 37 | }); 38 | 39 | // Called when function is called with function's args. 40 | call.on('call', function(url) { 41 | console.log('call: ' + util.inspect(arguments)); 42 | }); 43 | 44 | // Called with results each time function returns. 45 | call.on('callback', function(err, res) { 46 | console.log('callback: ' + util.inspect(arguments)); 47 | }); 48 | 49 | // Called on backoff. 50 | call.on('backoff', function(number, delay) { 51 | console.log('backoff: ' + util.inspect(arguments)); 52 | }); 53 | 54 | call.setStrategy(new backoff.ExponentialStrategy()); 55 | call.failAfter(2); 56 | call.start(); 57 | -------------------------------------------------------------------------------- /examples/randomized.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var backoff = require('../index'); 4 | 5 | var randomizedBackoff = backoff.fibonacci({ 6 | randomisationFactor: 0.4, 7 | initialDelay: 10, 8 | maxDelay: 1000 9 | }); 10 | 11 | randomizedBackoff.on('backoff', function(number, delay) { 12 | console.log('Backoff start: ' + number + ' ' + delay + 'ms'); 13 | }); 14 | 15 | randomizedBackoff.on('ready', function(number, delay) { 16 | console.log('Backoff done: ' + number + ' ' + delay + 'ms'); 17 | 18 | if (number < 15) { 19 | randomizedBackoff.backoff(); 20 | } 21 | }); 22 | 23 | randomizedBackoff.backoff(); 24 | -------------------------------------------------------------------------------- /examples/readme.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var backoff = require('../index.js'); 4 | 5 | var fibonacciBackoff = backoff.fibonacci({ 6 | randomisationFactor: 0, 7 | initialDelay: 10, 8 | maxDelay: 300 9 | }); 10 | 11 | fibonacciBackoff.failAfter(10); 12 | 13 | fibonacciBackoff.on('backoff', function(number, delay) { 14 | // Do something when backoff starts, e.g. show to the 15 | // user the delay before next reconnection attempt. 16 | console.log(number + ' ' + delay + 'ms'); 17 | }); 18 | 19 | fibonacciBackoff.on('ready', function(number, delay) { 20 | // Do something when backoff ends, e.g. retry a failed 21 | // operation (DNS lookup, API call, etc.). 22 | fibonacciBackoff.backoff(); 23 | }); 24 | 25 | fibonacciBackoff.on('fail', function() { 26 | // Do something when the maximum number of backoffs is 27 | // reached, e.g. ask the user to check its connection. 28 | console.log('fail'); 29 | }); 30 | 31 | fibonacciBackoff.backoff(); 32 | -------------------------------------------------------------------------------- /examples/reset.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var backoff = require('../index'); 4 | 5 | var backoff = backoff.exponential(); 6 | 7 | backoff.on('ready', function(number, delay) { 8 | console.log('Backoff done: ' + number + ' ' + delay + 'ms'); 9 | 10 | if (number < 15) { 11 | backoff.backoff(); 12 | } 13 | }); 14 | 15 | backoff.backoff(); 16 | 17 | setInterval(function() { 18 | backoff.reset(); 19 | backoff.backoff(); 20 | }, 5000); 21 | -------------------------------------------------------------------------------- /examples/set_timeout.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var backoff = require('../index'); 4 | 5 | // This example demonstrates how the backoff strategy can be used directly 6 | // to drive a backoff operation using direct calls to setTimeout(fn, delay). 7 | 8 | var strategy = new backoff.ExponentialStrategy({ 9 | randomisationFactor: 0.5, 10 | initialDelay: 10, 11 | maxDelay: 1000, 12 | factor: 3 13 | }); 14 | 15 | var attempt = 1; 16 | 17 | function doSomething() { 18 | if (attempt > 10) { 19 | console.log('Success!'); 20 | strategy.reset(); 21 | return; 22 | } 23 | 24 | console.log('Attempt #' + attempt); 25 | attempt++; 26 | setTimeout(doSomething, strategy.next()); 27 | } 28 | 29 | doSomething(); 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Mathieu Turcotte 2 | // Licensed under the MIT license. 3 | 4 | var Backoff = require('./lib/backoff'); 5 | var ExponentialBackoffStrategy = require('./lib/strategy/exponential'); 6 | var FibonacciBackoffStrategy = require('./lib/strategy/fibonacci'); 7 | var FunctionCall = require('./lib/function_call.js'); 8 | 9 | module.exports.Backoff = Backoff; 10 | module.exports.FunctionCall = FunctionCall; 11 | module.exports.FibonacciStrategy = FibonacciBackoffStrategy; 12 | module.exports.ExponentialStrategy = ExponentialBackoffStrategy; 13 | 14 | // Constructs a Fibonacci backoff. 15 | module.exports.fibonacci = function(options) { 16 | return new Backoff(new FibonacciBackoffStrategy(options)); 17 | }; 18 | 19 | // Constructs an exponential backoff. 20 | module.exports.exponential = function(options) { 21 | return new Backoff(new ExponentialBackoffStrategy(options)); 22 | }; 23 | 24 | // Constructs a FunctionCall for the given function and arguments. 25 | module.exports.call = function(fn, vargs, callback) { 26 | var args = Array.prototype.slice.call(arguments); 27 | fn = args[0]; 28 | vargs = args.slice(1, args.length - 1); 29 | callback = args[args.length - 1]; 30 | return new FunctionCall(fn, vargs, callback); 31 | }; 32 | -------------------------------------------------------------------------------- /lib/backoff.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Mathieu Turcotte 2 | // Licensed under the MIT license. 3 | 4 | var events = require('events'); 5 | var precond = require('precond'); 6 | var util = require('util'); 7 | 8 | // A class to hold the state of a backoff operation. Accepts a backoff strategy 9 | // to generate the backoff delays. 10 | function Backoff(backoffStrategy) { 11 | events.EventEmitter.call(this); 12 | 13 | this.backoffStrategy_ = backoffStrategy; 14 | this.maxNumberOfRetry_ = -1; 15 | this.backoffNumber_ = 0; 16 | this.backoffDelay_ = 0; 17 | this.timeoutID_ = -1; 18 | 19 | this.handlers = { 20 | backoff: this.onBackoff_.bind(this) 21 | }; 22 | } 23 | util.inherits(Backoff, events.EventEmitter); 24 | 25 | // Sets a limit, greater than 0, on the maximum number of backoffs. A 'fail' 26 | // event will be emitted when the limit is reached. 27 | Backoff.prototype.failAfter = function(maxNumberOfRetry) { 28 | precond.checkArgument(maxNumberOfRetry > 0, 29 | 'Expected a maximum number of retry greater than 0 but got %s.', 30 | maxNumberOfRetry); 31 | 32 | this.maxNumberOfRetry_ = maxNumberOfRetry; 33 | }; 34 | 35 | // Starts a backoff operation. Accepts an optional parameter to let the 36 | // listeners know why the backoff operation was started. 37 | Backoff.prototype.backoff = function(err) { 38 | precond.checkState(this.timeoutID_ === -1, 'Backoff in progress.'); 39 | 40 | if (this.backoffNumber_ === this.maxNumberOfRetry_) { 41 | this.emit('fail', err); 42 | this.reset(); 43 | } else { 44 | this.backoffDelay_ = this.backoffStrategy_.next(); 45 | this.timeoutID_ = setTimeout(this.handlers.backoff, this.backoffDelay_); 46 | this.emit('backoff', this.backoffNumber_, this.backoffDelay_, err); 47 | } 48 | }; 49 | 50 | // Handles the backoff timeout completion. 51 | Backoff.prototype.onBackoff_ = function() { 52 | this.timeoutID_ = -1; 53 | this.emit('ready', this.backoffNumber_, this.backoffDelay_); 54 | this.backoffNumber_++; 55 | }; 56 | 57 | // Stops any backoff operation and resets the backoff delay to its inital value. 58 | Backoff.prototype.reset = function() { 59 | this.backoffNumber_ = 0; 60 | this.backoffStrategy_.reset(); 61 | clearTimeout(this.timeoutID_); 62 | this.timeoutID_ = -1; 63 | }; 64 | 65 | module.exports = Backoff; 66 | -------------------------------------------------------------------------------- /lib/function_call.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Mathieu Turcotte 2 | // Licensed under the MIT license. 3 | 4 | var events = require('events'); 5 | var precond = require('precond'); 6 | var util = require('util'); 7 | 8 | var Backoff = require('./backoff'); 9 | var FibonacciBackoffStrategy = require('./strategy/fibonacci'); 10 | 11 | // Wraps a function to be called in a backoff loop. 12 | function FunctionCall(fn, args, callback) { 13 | events.EventEmitter.call(this); 14 | 15 | precond.checkIsFunction(fn, 'Expected fn to be a function.'); 16 | precond.checkIsArray(args, 'Expected args to be an array.'); 17 | precond.checkIsFunction(callback, 'Expected callback to be a function.'); 18 | 19 | this.function_ = fn; 20 | this.arguments_ = args; 21 | this.callback_ = callback; 22 | this.lastResult_ = []; 23 | this.numRetries_ = 0; 24 | 25 | this.backoff_ = null; 26 | this.strategy_ = null; 27 | this.failAfter_ = -1; 28 | this.retryPredicate_ = FunctionCall.DEFAULT_RETRY_PREDICATE_; 29 | 30 | this.state_ = FunctionCall.State_.PENDING; 31 | } 32 | util.inherits(FunctionCall, events.EventEmitter); 33 | 34 | // States in which the call can be. 35 | FunctionCall.State_ = { 36 | // Call isn't started yet. 37 | PENDING: 0, 38 | // Call is in progress. 39 | RUNNING: 1, 40 | // Call completed successfully which means that either the wrapped function 41 | // returned successfully or the maximal number of backoffs was reached. 42 | COMPLETED: 2, 43 | // The call was aborted. 44 | ABORTED: 3 45 | }; 46 | 47 | // The default retry predicate which considers any error as retriable. 48 | FunctionCall.DEFAULT_RETRY_PREDICATE_ = function(err) { 49 | return true; 50 | }; 51 | 52 | // Checks whether the call is pending. 53 | FunctionCall.prototype.isPending = function() { 54 | return this.state_ == FunctionCall.State_.PENDING; 55 | }; 56 | 57 | // Checks whether the call is in progress. 58 | FunctionCall.prototype.isRunning = function() { 59 | return this.state_ == FunctionCall.State_.RUNNING; 60 | }; 61 | 62 | // Checks whether the call is completed. 63 | FunctionCall.prototype.isCompleted = function() { 64 | return this.state_ == FunctionCall.State_.COMPLETED; 65 | }; 66 | 67 | // Checks whether the call is aborted. 68 | FunctionCall.prototype.isAborted = function() { 69 | return this.state_ == FunctionCall.State_.ABORTED; 70 | }; 71 | 72 | // Sets the backoff strategy to use. Can only be called before the call is 73 | // started otherwise an exception will be thrown. 74 | FunctionCall.prototype.setStrategy = function(strategy) { 75 | precond.checkState(this.isPending(), 'FunctionCall in progress.'); 76 | this.strategy_ = strategy; 77 | return this; // Return this for chaining. 78 | }; 79 | 80 | // Sets the predicate which will be used to determine whether the errors 81 | // returned from the wrapped function should be retried or not, e.g. a 82 | // network error would be retriable while a type error would stop the 83 | // function call. 84 | FunctionCall.prototype.retryIf = function(retryPredicate) { 85 | precond.checkState(this.isPending(), 'FunctionCall in progress.'); 86 | this.retryPredicate_ = retryPredicate; 87 | return this; 88 | }; 89 | 90 | // Returns all intermediary results returned by the wrapped function since 91 | // the initial call. 92 | FunctionCall.prototype.getLastResult = function() { 93 | return this.lastResult_.concat(); 94 | }; 95 | 96 | // Returns the number of times the wrapped function call was retried. 97 | FunctionCall.prototype.getNumRetries = function() { 98 | return this.numRetries_; 99 | }; 100 | 101 | // Sets the backoff limit. 102 | FunctionCall.prototype.failAfter = function(maxNumberOfRetry) { 103 | precond.checkState(this.isPending(), 'FunctionCall in progress.'); 104 | this.failAfter_ = maxNumberOfRetry; 105 | return this; // Return this for chaining. 106 | }; 107 | 108 | // Aborts the call. 109 | FunctionCall.prototype.abort = function() { 110 | if (this.isCompleted() || this.isAborted()) { 111 | return; 112 | } 113 | 114 | if (this.isRunning()) { 115 | this.backoff_.reset(); 116 | } 117 | 118 | this.state_ = FunctionCall.State_.ABORTED; 119 | this.lastResult_ = [new Error('Backoff aborted.')]; 120 | this.emit('abort'); 121 | this.doCallback_(); 122 | }; 123 | 124 | // Initiates the call to the wrapped function. Accepts an optional factory 125 | // function used to create the backoff instance; used when testing. 126 | FunctionCall.prototype.start = function(backoffFactory) { 127 | precond.checkState(!this.isAborted(), 'FunctionCall is aborted.'); 128 | precond.checkState(this.isPending(), 'FunctionCall already started.'); 129 | 130 | var strategy = this.strategy_ || new FibonacciBackoffStrategy(); 131 | 132 | this.backoff_ = backoffFactory ? 133 | backoffFactory(strategy) : 134 | new Backoff(strategy); 135 | 136 | this.backoff_.on('ready', this.doCall_.bind(this, true /* isRetry */)); 137 | this.backoff_.on('fail', this.doCallback_.bind(this)); 138 | this.backoff_.on('backoff', this.handleBackoff_.bind(this)); 139 | 140 | if (this.failAfter_ > 0) { 141 | this.backoff_.failAfter(this.failAfter_); 142 | } 143 | 144 | this.state_ = FunctionCall.State_.RUNNING; 145 | this.doCall_(false /* isRetry */); 146 | }; 147 | 148 | // Calls the wrapped function. 149 | FunctionCall.prototype.doCall_ = function(isRetry) { 150 | if (isRetry) { 151 | this.numRetries_++; 152 | } 153 | var eventArgs = ['call'].concat(this.arguments_); 154 | events.EventEmitter.prototype.emit.apply(this, eventArgs); 155 | var callback = this.handleFunctionCallback_.bind(this); 156 | this.function_.apply(null, this.arguments_.concat(callback)); 157 | }; 158 | 159 | // Calls the wrapped function's callback with the last result returned by the 160 | // wrapped function. 161 | FunctionCall.prototype.doCallback_ = function() { 162 | this.callback_.apply(null, this.lastResult_); 163 | }; 164 | 165 | // Handles wrapped function's completion. This method acts as a replacement 166 | // for the original callback function. 167 | FunctionCall.prototype.handleFunctionCallback_ = function() { 168 | if (this.isAborted()) { 169 | return; 170 | } 171 | 172 | var args = Array.prototype.slice.call(arguments); 173 | this.lastResult_ = args; // Save last callback arguments. 174 | events.EventEmitter.prototype.emit.apply(this, ['callback'].concat(args)); 175 | 176 | var err = args[0]; 177 | if (err && this.retryPredicate_(err)) { 178 | this.backoff_.backoff(err); 179 | } else { 180 | this.state_ = FunctionCall.State_.COMPLETED; 181 | this.doCallback_(); 182 | } 183 | }; 184 | 185 | // Handles the backoff event by reemitting it. 186 | FunctionCall.prototype.handleBackoff_ = function(number, delay, err) { 187 | this.emit('backoff', number, delay, err); 188 | }; 189 | 190 | module.exports = FunctionCall; 191 | -------------------------------------------------------------------------------- /lib/strategy/exponential.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Mathieu Turcotte 2 | // Licensed under the MIT license. 3 | 4 | var util = require('util'); 5 | var precond = require('precond'); 6 | 7 | var BackoffStrategy = require('./strategy'); 8 | 9 | // Exponential backoff strategy. 10 | function ExponentialBackoffStrategy(options) { 11 | BackoffStrategy.call(this, options); 12 | this.backoffDelay_ = 0; 13 | this.nextBackoffDelay_ = this.getInitialDelay(); 14 | this.factor_ = ExponentialBackoffStrategy.DEFAULT_FACTOR; 15 | 16 | if (options && options.factor !== undefined) { 17 | precond.checkArgument(options.factor > 1, 18 | 'Exponential factor should be greater than 1 but got %s.', 19 | options.factor); 20 | this.factor_ = options.factor; 21 | } 22 | } 23 | util.inherits(ExponentialBackoffStrategy, BackoffStrategy); 24 | 25 | // Default multiplication factor used to compute the next backoff delay from 26 | // the current one. The value can be overridden by passing a custom factor as 27 | // part of the options. 28 | ExponentialBackoffStrategy.DEFAULT_FACTOR = 2; 29 | 30 | ExponentialBackoffStrategy.prototype.next_ = function() { 31 | this.backoffDelay_ = Math.min(this.nextBackoffDelay_, this.getMaxDelay()); 32 | this.nextBackoffDelay_ = this.backoffDelay_ * this.factor_; 33 | return this.backoffDelay_; 34 | }; 35 | 36 | ExponentialBackoffStrategy.prototype.reset_ = function() { 37 | this.backoffDelay_ = 0; 38 | this.nextBackoffDelay_ = this.getInitialDelay(); 39 | }; 40 | 41 | module.exports = ExponentialBackoffStrategy; 42 | -------------------------------------------------------------------------------- /lib/strategy/fibonacci.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Mathieu Turcotte 2 | // Licensed under the MIT license. 3 | 4 | var util = require('util'); 5 | 6 | var BackoffStrategy = require('./strategy'); 7 | 8 | // Fibonacci backoff strategy. 9 | function FibonacciBackoffStrategy(options) { 10 | BackoffStrategy.call(this, options); 11 | this.backoffDelay_ = 0; 12 | this.nextBackoffDelay_ = this.getInitialDelay(); 13 | } 14 | util.inherits(FibonacciBackoffStrategy, BackoffStrategy); 15 | 16 | FibonacciBackoffStrategy.prototype.next_ = function() { 17 | var backoffDelay = Math.min(this.nextBackoffDelay_, this.getMaxDelay()); 18 | this.nextBackoffDelay_ += this.backoffDelay_; 19 | this.backoffDelay_ = backoffDelay; 20 | return backoffDelay; 21 | }; 22 | 23 | FibonacciBackoffStrategy.prototype.reset_ = function() { 24 | this.nextBackoffDelay_ = this.getInitialDelay(); 25 | this.backoffDelay_ = 0; 26 | }; 27 | 28 | module.exports = FibonacciBackoffStrategy; 29 | -------------------------------------------------------------------------------- /lib/strategy/strategy.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Mathieu Turcotte 2 | // Licensed under the MIT license. 3 | 4 | var events = require('events'); 5 | var util = require('util'); 6 | 7 | function isDef(value) { 8 | return value !== undefined && value !== null; 9 | } 10 | 11 | // Abstract class defining the skeleton for the backoff strategies. Accepts an 12 | // object holding the options for the backoff strategy: 13 | // 14 | // * `randomisationFactor`: The randomisation factor which must be between 0 15 | // and 1 where 1 equates to a randomization factor of 100% and 0 to no 16 | // randomization. 17 | // * `initialDelay`: The backoff initial delay in milliseconds. 18 | // * `maxDelay`: The backoff maximal delay in milliseconds. 19 | function BackoffStrategy(options) { 20 | options = options || {}; 21 | 22 | if (isDef(options.initialDelay) && options.initialDelay < 1) { 23 | throw new Error('The initial timeout must be greater than 0.'); 24 | } else if (isDef(options.maxDelay) && options.maxDelay < 1) { 25 | throw new Error('The maximal timeout must be greater than 0.'); 26 | } 27 | 28 | this.initialDelay_ = options.initialDelay || 100; 29 | this.maxDelay_ = options.maxDelay || 10000; 30 | 31 | if (this.maxDelay_ <= this.initialDelay_) { 32 | throw new Error('The maximal backoff delay must be ' + 33 | 'greater than the initial backoff delay.'); 34 | } 35 | 36 | if (isDef(options.randomisationFactor) && 37 | (options.randomisationFactor < 0 || options.randomisationFactor > 1)) { 38 | throw new Error('The randomisation factor must be between 0 and 1.'); 39 | } 40 | 41 | this.randomisationFactor_ = options.randomisationFactor || 0; 42 | } 43 | 44 | // Gets the maximal backoff delay. 45 | BackoffStrategy.prototype.getMaxDelay = function() { 46 | return this.maxDelay_; 47 | }; 48 | 49 | // Gets the initial backoff delay. 50 | BackoffStrategy.prototype.getInitialDelay = function() { 51 | return this.initialDelay_; 52 | }; 53 | 54 | // Template method that computes and returns the next backoff delay in 55 | // milliseconds. 56 | BackoffStrategy.prototype.next = function() { 57 | var backoffDelay = this.next_(); 58 | var randomisationMultiple = 1 + Math.random() * this.randomisationFactor_; 59 | var randomizedDelay = Math.round(backoffDelay * randomisationMultiple); 60 | return randomizedDelay; 61 | }; 62 | 63 | // Computes and returns the next backoff delay. Intended to be overridden by 64 | // subclasses. 65 | BackoffStrategy.prototype.next_ = function() { 66 | throw new Error('BackoffStrategy.next_() unimplemented.'); 67 | }; 68 | 69 | // Template method that resets the backoff delay to its initial value. 70 | BackoffStrategy.prototype.reset = function() { 71 | this.reset_(); 72 | }; 73 | 74 | // Resets the backoff delay to its initial value. Intended to be overridden by 75 | // subclasses. 76 | BackoffStrategy.prototype.reset_ = function() { 77 | throw new Error('BackoffStrategy.reset_() unimplemented.'); 78 | }; 79 | 80 | module.exports = BackoffStrategy; 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backoff", 3 | "description": "Fibonacci and exponential backoffs.", 4 | "version": "2.5.0", 5 | "license": "MIT", 6 | "author": "Mathieu Turcotte ", 7 | "keywords": ["backoff", "retry", "fibonacci", "exponential"], 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/MathieuTurcotte/node-backoff.git" 11 | }, 12 | "dependencies": { 13 | "precond": "0.2" 14 | }, 15 | "devDependencies": { 16 | "sinon": "1.10", 17 | "nodeunit": "0.9" 18 | }, 19 | "scripts": { 20 | "docco" : "docco lib/*.js lib/strategy/* index.js", 21 | "pretest": "jshint lib/ tests/ examples/ index.js", 22 | "test": "node_modules/nodeunit/bin/nodeunit tests/" 23 | }, 24 | "engines": { 25 | "node": ">= 0.6" 26 | }, 27 | "files": [ 28 | "index.js", 29 | "lib", 30 | "tests" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tests/api.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Mathieu Turcotte 3 | * Licensed under the MIT license. 4 | */ 5 | 6 | var sinon = require('sinon'); 7 | 8 | var backoff = require('../index'); 9 | 10 | exports["API"] = { 11 | "backoff.fibonnaci should be a function that returns a backoff instance": function(test) { 12 | test.ok(backoff.fibonacci, 'backoff.fibonacci should be defined.'); 13 | test.equal(typeof backoff.fibonacci, 'function', 14 | 'backoff.fibonacci should be a function.'); 15 | test.equal(backoff.fibonacci().constructor.name, 'Backoff'); 16 | test.done(); 17 | }, 18 | 19 | "backoff.exponential should be a function that returns a backoff instance": function(test) { 20 | test.ok(backoff.exponential, 'backoff.exponential should be defined.'); 21 | test.equal(typeof backoff.exponential, 'function', 22 | 'backoff.exponential should be a function.'); 23 | test.equal(backoff.exponential().constructor.name, 'Backoff'); 24 | test.done(); 25 | }, 26 | 27 | "backoff.call should be a function that returns a FunctionCall instance": function(test) { 28 | var fn = function() {}; 29 | var callback = function() {}; 30 | test.ok(backoff.Backoff, 'backoff.call should be defined.'); 31 | test.equal(typeof backoff.call, 'function', 32 | 'backoff.call should be a function.'); 33 | test.equal(backoff.call(fn, 1, 2, 3, callback).constructor.name, 34 | 'FunctionCall'); 35 | test.done(); 36 | }, 37 | 38 | "backoff.Backoff should be defined and a function": function(test) { 39 | test.ok(backoff.Backoff, 'backoff.Backoff should be defined.'); 40 | test.equal(typeof backoff.Backoff, 'function', 41 | 'backoff.Backoff should be a function.'); 42 | test.done(); 43 | }, 44 | 45 | "backoff.FunctionCall should be defined and a function": function(test) { 46 | test.ok(backoff.FunctionCall, 47 | 'backoff.FunctionCall should be defined.'); 48 | test.equal(typeof backoff.FunctionCall, 'function', 49 | 'backoff.FunctionCall should be a function.'); 50 | test.done(); 51 | }, 52 | 53 | "backoff.FibonacciStrategy should be defined and a function": function(test) { 54 | test.ok(backoff.FibonacciStrategy, 55 | 'backoff.FibonacciStrategy should be defined.'); 56 | test.equal(typeof backoff.FibonacciStrategy, 'function', 57 | 'backoff.FibonacciStrategy should be a function.'); 58 | test.done(); 59 | }, 60 | 61 | "backoff.ExponentialStrategy should be defined and a function": function(test) { 62 | test.ok(backoff.ExponentialStrategy, 63 | 'backoff.ExponentialStrategy should be defined.'); 64 | test.equal(typeof backoff.ExponentialStrategy, 'function', 65 | 'backoff.ExponentialStrategy should be a function.'); 66 | test.done(); 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /tests/backoff.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Mathieu Turcotte 3 | * Licensed under the MIT license. 4 | */ 5 | 6 | var sinon = require('sinon'); 7 | 8 | var Backoff = require('../lib/backoff'); 9 | var BackoffStrategy = require('../lib/strategy/strategy'); 10 | 11 | exports["Backoff"] = { 12 | setUp: function(callback) { 13 | this.backoffStrategy = sinon.stub(new BackoffStrategy()); 14 | this.backoff = new Backoff(this.backoffStrategy); 15 | this.clock = sinon.useFakeTimers(); 16 | this.spy = new sinon.spy(); 17 | callback(); 18 | }, 19 | 20 | tearDown: function(callback) { 21 | this.clock.restore(); 22 | callback(); 23 | }, 24 | 25 | "the backoff event should be emitted when backoff starts": function(test) { 26 | this.backoffStrategy.next.returns(10); 27 | this.backoff.on('backoff', this.spy); 28 | 29 | this.backoff.backoff(); 30 | 31 | test.ok(this.spy.calledOnce, 32 | 'Backoff event should be emitted when backoff starts.'); 33 | test.done(); 34 | }, 35 | 36 | "the ready event should be emitted on backoff completion": function(test) { 37 | this.backoffStrategy.next.returns(10); 38 | this.backoff.on('ready', this.spy); 39 | 40 | this.backoff.backoff(); 41 | this.clock.tick(10); 42 | 43 | test.ok(this.spy.calledOnce, 44 | 'Ready event should be emitted when backoff ends.'); 45 | test.done(); 46 | }, 47 | 48 | "the backoff event should be passed the backoff delay": function(test) { 49 | this.backoffStrategy.next.returns(989); 50 | this.backoff.on('backoff', this.spy); 51 | 52 | this.backoff.backoff(); 53 | 54 | test.equal(this.spy.getCall(0).args[1], 989, 'Backoff event should ' + 55 | 'carry the backoff delay as its second argument.'); 56 | test.done(); 57 | }, 58 | 59 | "the ready event should be passed the backoff delay": function(test) { 60 | this.backoffStrategy.next.returns(989); 61 | this.backoff.on('ready', this.spy); 62 | 63 | this.backoff.backoff(); 64 | this.clock.tick(989); 65 | 66 | test.equal(this.spy.getCall(0).args[1], 989, 'Ready event should ' + 67 | 'carry the backoff delay as its second argument.'); 68 | test.done(); 69 | }, 70 | 71 | "the fail event should be emitted when backoff limit is reached": function(test) { 72 | var err = new Error('Fail'); 73 | 74 | this.backoffStrategy.next.returns(10); 75 | this.backoff.on('fail', this.spy); 76 | 77 | this.backoff.failAfter(2); 78 | 79 | // Consume first 2 backoffs. 80 | for (var i = 0; i < 2; i++) { 81 | this.backoff.backoff(); 82 | this.clock.tick(10); 83 | } 84 | 85 | // Failure should occur on the third call, and not before. 86 | test.ok(!this.spy.calledOnce, 'Fail event shouldn\'t have been emitted.'); 87 | this.backoff.backoff(err); 88 | test.ok(this.spy.calledOnce, 'Fail event should have been emitted.'); 89 | test.equal(this.spy.getCall(0).args[0], err, 'Error should be passed'); 90 | 91 | test.done(); 92 | }, 93 | 94 | "calling backoff while a backoff is in progress should throw an error": function(test) { 95 | this.backoffStrategy.next.returns(10); 96 | var backoff = this.backoff; 97 | 98 | backoff.backoff(); 99 | 100 | test.throws(function() { 101 | backoff.backoff(); 102 | }, /in progress/); 103 | 104 | test.done(); 105 | }, 106 | 107 | "backoff limit should be greater than 0": function(test) { 108 | var backoff = this.backoff; 109 | test.throws(function() { 110 | backoff.failAfter(0); 111 | }, /greater than 0 but got 0/); 112 | test.done(); 113 | }, 114 | 115 | "reset should cancel any backoff in progress": function(test) { 116 | this.backoffStrategy.next.returns(10); 117 | this.backoff.on('ready', this.spy); 118 | 119 | this.backoff.backoff(); 120 | 121 | this.backoff.reset(); 122 | this.clock.tick(100); // 'ready' should not be emitted. 123 | 124 | test.equals(this.spy.callCount, 0, 'Reset should have aborted the backoff.'); 125 | test.done(); 126 | }, 127 | 128 | "reset should reset the backoff strategy": function(test) { 129 | this.backoff.reset(); 130 | test.ok(this.backoffStrategy.reset.calledOnce, 131 | 'The backoff strategy should have been resetted.'); 132 | test.done(); 133 | }, 134 | 135 | "backoff should be reset after fail": function(test) { 136 | this.backoffStrategy.next.returns(10); 137 | 138 | this.backoff.failAfter(1); 139 | 140 | this.backoff.backoff(); 141 | this.clock.tick(10); 142 | this.backoff.backoff(); 143 | 144 | test.ok(this.backoffStrategy.reset.calledOnce, 145 | 'Backoff should have been resetted after failure.'); 146 | test.done(); 147 | }, 148 | 149 | "the backoff number should increase from 0 to N - 1": function(test) { 150 | this.backoffStrategy.next.returns(10); 151 | this.backoff.on('backoff', this.spy); 152 | 153 | var expectedNumbers = [0, 1, 2, 3, 4]; 154 | var actualNumbers = []; 155 | 156 | for (var i = 0; i < expectedNumbers.length; i++) { 157 | this.backoff.backoff(); 158 | this.clock.tick(10); 159 | actualNumbers.push(this.spy.getCall(i).args[0]); 160 | } 161 | 162 | test.deepEqual(expectedNumbers, actualNumbers, 163 | 'Backoff number should increase from 0 to N - 1.'); 164 | test.done(); 165 | } 166 | }; 167 | -------------------------------------------------------------------------------- /tests/backoff_strategy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Mathieu Turcotte 3 | * Licensed under the MIT license. 4 | */ 5 | 6 | var sinon = require('sinon'); 7 | var util = require('util'); 8 | 9 | var BackoffStrategy = require('../lib/strategy/strategy'); 10 | 11 | function SampleBackoffStrategy(options) { 12 | BackoffStrategy.call(this, options); 13 | } 14 | util.inherits(SampleBackoffStrategy, BackoffStrategy); 15 | 16 | SampleBackoffStrategy.prototype.next_ = function() { 17 | return this.getInitialDelay(); 18 | }; 19 | 20 | SampleBackoffStrategy.prototype.reset_ = function() {}; 21 | 22 | exports["BackoffStrategy"] = { 23 | setUp: function(callback) { 24 | this.random = sinon.stub(Math, 'random'); 25 | callback(); 26 | }, 27 | 28 | tearDown: function(callback) { 29 | this.random.restore(); 30 | callback(); 31 | }, 32 | 33 | "the randomisation factor should be between 0 and 1": function(test) { 34 | test.throws(function() { 35 | new BackoffStrategy({ 36 | randomisationFactor: -0.1 37 | }); 38 | }); 39 | 40 | test.throws(function() { 41 | new BackoffStrategy({ 42 | randomisationFactor: 1.1 43 | }); 44 | }); 45 | 46 | test.doesNotThrow(function() { 47 | new BackoffStrategy({ 48 | randomisationFactor: 0.5 49 | }); 50 | }); 51 | 52 | test.done(); 53 | }, 54 | 55 | "the raw delay should be randomized based on the randomisation factor": function(test) { 56 | var strategy = new SampleBackoffStrategy({ 57 | randomisationFactor: 0.5, 58 | initialDelay: 1000 59 | }); 60 | this.random.returns(0.5); 61 | 62 | var backoffDelay = strategy.next(); 63 | 64 | test.equals(backoffDelay, 1000 + (1000 * 0.5 * 0.5)); 65 | test.done(); 66 | }, 67 | 68 | "the initial backoff delay should be greater than 0": function(test) { 69 | test.throws(function() { 70 | new BackoffStrategy({ 71 | initialDelay: -1 72 | }); 73 | }); 74 | 75 | test.throws(function() { 76 | new BackoffStrategy({ 77 | initialDelay: 0 78 | }); 79 | }); 80 | 81 | test.doesNotThrow(function() { 82 | new BackoffStrategy({ 83 | initialDelay: 1 84 | }); 85 | }); 86 | 87 | test.done(); 88 | }, 89 | 90 | "the maximal backoff delay should be greater than 0": function(test) { 91 | test.throws(function() { 92 | new BackoffStrategy({ 93 | maxDelay: -1 94 | }); 95 | }); 96 | 97 | test.throws(function() { 98 | new BackoffStrategy({ 99 | maxDelay: 0 100 | }); 101 | }); 102 | 103 | test.done(); 104 | }, 105 | 106 | "the maximal backoff delay should be greater than the initial backoff delay": function(test) { 107 | test.throws(function() { 108 | new BackoffStrategy({ 109 | initialDelay: 10, 110 | maxDelay: 10 111 | }); 112 | }); 113 | 114 | test.doesNotThrow(function() { 115 | new BackoffStrategy({ 116 | initialDelay: 10, 117 | maxDelay: 11 118 | }); 119 | }); 120 | 121 | test.done(); 122 | } 123 | }; 124 | -------------------------------------------------------------------------------- /tests/exponential_backoff_strategy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Mathieu Turcotte 3 | * Licensed under the MIT license. 4 | */ 5 | 6 | var sinon = require('sinon'); 7 | 8 | var ExponentialBackoffStrategy = require('../lib/strategy/exponential'); 9 | 10 | exports["ExponentialBackoffStrategy"] = { 11 | 12 | "backoff delays should follow an exponential sequence": function(test) { 13 | var strategy = new ExponentialBackoffStrategy({ 14 | initialDelay: 10, 15 | maxDelay: 1000 16 | }); 17 | 18 | // Exponential sequence: x[i] = x[i-1] * 2. 19 | var expectedDelays = [10, 20, 40, 80, 160, 320, 640, 1000, 1000]; 20 | var actualDelays = expectedDelays.map(function () { 21 | return strategy.next(); 22 | }); 23 | 24 | test.deepEqual(expectedDelays, actualDelays, 25 | 'Generated delays should follow an exponential sequence.'); 26 | test.done(); 27 | }, 28 | 29 | "backoff delay factor should be configurable": function (test) { 30 | var strategy = new ExponentialBackoffStrategy({ 31 | initialDelay: 10, 32 | maxDelay: 270, 33 | factor: 3 34 | }); 35 | 36 | // Exponential sequence: x[i] = x[i-1] * 3. 37 | var expectedDelays = [10, 30, 90, 270, 270]; 38 | var actualDelays = expectedDelays.map(function () { 39 | return strategy.next(); 40 | }); 41 | 42 | test.deepEqual(expectedDelays, actualDelays, 43 | 'Generated delays should follow a configurable exponential sequence.'); 44 | test.done(); 45 | }, 46 | 47 | "backoff delays should restart from the initial delay after reset": function(test) { 48 | var strategy = new ExponentialBackoffStrategy({ 49 | initialDelay: 10, 50 | maxDelay: 1000 51 | }); 52 | 53 | strategy.next(); 54 | strategy.reset(); 55 | 56 | var backoffDelay = strategy.next(); 57 | test.equals(backoffDelay, 10, 58 | 'Strategy should return the initial delay after reset.'); 59 | test.done(); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /tests/fibonacci_backoff_strategy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Mathieu Turcotte 3 | * Licensed under the MIT license. 4 | */ 5 | 6 | var sinon = require('sinon'); 7 | 8 | var FibonacciBackoffStrategy = require('../lib/strategy/fibonacci'); 9 | 10 | exports["FibonacciBackoffStrategy"] = { 11 | setUp: function(callback) { 12 | this.strategy = new FibonacciBackoffStrategy({ 13 | initialDelay: 10, 14 | maxDelay: 1000 15 | }); 16 | callback(); 17 | }, 18 | 19 | "backoff delays should follow a Fibonacci sequence": function(test) { 20 | // Fibonacci sequence: x[i] = x[i-1] + x[i-2]. 21 | var expectedDelays = [10, 10, 20, 30, 50, 80, 130, 210, 340, 550, 890, 1000]; 22 | var actualDelays = []; 23 | 24 | for (var i = 0; i < expectedDelays.length; i++) { 25 | actualDelays.push(this.strategy.next()); 26 | } 27 | 28 | test.deepEqual(expectedDelays, actualDelays, 29 | 'Generated delays should follow a Fibonacci sequence.'); 30 | test.done(); 31 | }, 32 | 33 | "backoff delays should restart from the initial delay after reset": function(test) { 34 | var strategy = new FibonacciBackoffStrategy({ 35 | initialDelay: 10, 36 | maxDelay: 1000 37 | }); 38 | 39 | strategy.next(); 40 | strategy.reset(); 41 | 42 | var backoffDelay = strategy.next(); 43 | test.equals(backoffDelay, 10, 44 | 'Strategy should return the initial delay after reset.'); 45 | test.done(); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /tests/function_call.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012 Mathieu Turcotte 3 | * Licensed under the MIT license. 4 | */ 5 | 6 | var assert = require('assert'); 7 | var events = require('events'); 8 | var sinon = require('sinon'); 9 | var util = require('util'); 10 | 11 | var FunctionCall = require('../lib/function_call'); 12 | 13 | function MockBackoff() { 14 | events.EventEmitter.call(this); 15 | 16 | this.reset = sinon.spy(); 17 | this.backoff = sinon.spy(); 18 | this.failAfter = sinon.spy(); 19 | } 20 | util.inherits(MockBackoff, events.EventEmitter); 21 | 22 | exports["FunctionCall"] = { 23 | setUp: function(callback) { 24 | this.wrappedFn = sinon.stub(); 25 | this.callback = sinon.stub(); 26 | this.backoff = new MockBackoff(); 27 | this.backoffFactory = sinon.stub(); 28 | this.backoffFactory.returns(this.backoff); 29 | callback(); 30 | }, 31 | 32 | tearDown: function(callback) { 33 | callback(); 34 | }, 35 | 36 | "constructor's first argument should be a function": function(test) { 37 | test.throws(function() { 38 | new FunctionCall(1, [], function() {}); 39 | }, /Expected fn to be a function./); 40 | test.done(); 41 | }, 42 | 43 | "constructor's last argument should be a function": function(test) { 44 | test.throws(function() { 45 | new FunctionCall(function() {}, [], 3); 46 | }, /Expected callback to be a function./); 47 | test.done(); 48 | }, 49 | 50 | "isPending should return false once the call is started": function(test) { 51 | this.wrappedFn. 52 | onFirstCall().yields(new Error()). 53 | onSecondCall().yields(null, 'Success!'); 54 | var call = new FunctionCall(this.wrappedFn, [], this.callback); 55 | 56 | test.ok(call.isPending()); 57 | 58 | call.start(this.backoffFactory); 59 | test.ok(!call.isPending()); 60 | 61 | this.backoff.emit('ready'); 62 | test.ok(!call.isPending()); 63 | 64 | test.done(); 65 | }, 66 | 67 | "isRunning should return true when call is in progress": function(test) { 68 | this.wrappedFn. 69 | onFirstCall().yields(new Error()). 70 | onSecondCall().yields(null, 'Success!'); 71 | var call = new FunctionCall(this.wrappedFn, [], this.callback); 72 | 73 | test.ok(!call.isRunning()); 74 | 75 | call.start(this.backoffFactory); 76 | test.ok(call.isRunning()); 77 | 78 | this.backoff.emit('ready'); 79 | test.ok(!call.isRunning()); 80 | 81 | test.done(); 82 | }, 83 | 84 | "isCompleted should return true once the call completes": function(test) { 85 | this.wrappedFn. 86 | onFirstCall().yields(new Error()). 87 | onSecondCall().yields(null, 'Success!'); 88 | var call = new FunctionCall(this.wrappedFn, [], this.callback); 89 | 90 | test.ok(!call.isCompleted()); 91 | 92 | call.start(this.backoffFactory); 93 | test.ok(!call.isCompleted()); 94 | 95 | this.backoff.emit('ready'); 96 | test.ok(call.isCompleted()); 97 | 98 | test.done(); 99 | }, 100 | 101 | "isAborted should return true once the call is aborted": function(test) { 102 | this.wrappedFn. 103 | onFirstCall().yields(new Error()). 104 | onSecondCall().yields(null, 'Success!'); 105 | var call = new FunctionCall(this.wrappedFn, [], this.callback); 106 | 107 | test.ok(!call.isAborted()); 108 | call.abort(); 109 | test.ok(call.isAborted()); 110 | 111 | test.done(); 112 | }, 113 | 114 | "setStrategy should overwrite the default strategy": function(test) { 115 | var replacementStrategy = {}; 116 | var call = new FunctionCall(this.wrappedFn, [], this.callback); 117 | call.setStrategy(replacementStrategy); 118 | call.start(this.backoffFactory); 119 | test.ok(this.backoffFactory.calledWith(replacementStrategy), 120 | 'User defined strategy should be used to instantiate ' + 121 | 'the backoff instance.'); 122 | test.done(); 123 | }, 124 | 125 | "setStrategy should throw if the call is in progress": function(test) { 126 | var call = new FunctionCall(this.wrappedFn, [], this.callback); 127 | call.start(this.backoffFactory); 128 | test.throws(function() { 129 | call.setStrategy({}); 130 | }, /in progress/); 131 | test.done(); 132 | }, 133 | 134 | "failAfter should not be set by default": function(test) { 135 | var call = new FunctionCall(this.wrappedFn, [], this.callback); 136 | call.start(this.backoffFactory); 137 | test.equal(0, this.backoff.failAfter.callCount); 138 | test.done(); 139 | }, 140 | 141 | "failAfter should be used as the maximum number of backoffs": function(test) { 142 | var failAfterValue = 99; 143 | var call = new FunctionCall(this.wrappedFn, [], this.callback); 144 | call.failAfter(failAfterValue); 145 | call.start(this.backoffFactory); 146 | test.ok(this.backoff.failAfter.calledWith(failAfterValue), 147 | 'User defined maximum number of backoffs shoud be ' + 148 | 'used to configure the backoff instance.'); 149 | test.done(); 150 | }, 151 | 152 | "failAfter should throw if the call is in progress": function(test) { 153 | var call = new FunctionCall(this.wrappedFn, [], this.callback); 154 | call.start(this.backoffFactory); 155 | test.throws(function() { 156 | call.failAfter(1234); 157 | }, /in progress/); 158 | test.done(); 159 | }, 160 | 161 | "start shouldn't allow overlapping invocation": function(test) { 162 | var call = new FunctionCall(this.wrappedFn, [], this.callback); 163 | var backoffFactory = this.backoffFactory; 164 | 165 | call.start(backoffFactory); 166 | test.throws(function() { 167 | call.start(backoffFactory); 168 | }, /already started/); 169 | test.done(); 170 | }, 171 | 172 | "start shouldn't allow invocation of aborted call": function(test) { 173 | var call = new FunctionCall(this.wrappedFn, [], this.callback); 174 | var backoffFactory = this.backoffFactory; 175 | 176 | call.abort(); 177 | test.throws(function() { 178 | call.start(backoffFactory); 179 | }, /aborted/); 180 | test.done(); 181 | }, 182 | 183 | "call should forward its arguments to the wrapped function": function(test) { 184 | var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback); 185 | call.start(this.backoffFactory); 186 | test.ok(this.wrappedFn.calledWith(1, 2, 3)); 187 | test.done(); 188 | }, 189 | 190 | "call should complete when the wrapped function succeeds": function(test) { 191 | var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback); 192 | this.wrappedFn. 193 | onCall(0).yields(new Error()). 194 | onCall(1).yields(new Error()). 195 | onCall(2).yields(new Error()). 196 | onCall(3).yields(null, 'Success!'); 197 | 198 | call.start(this.backoffFactory); 199 | 200 | for (var i = 0; i < 2; i++) { 201 | this.backoff.emit('ready'); 202 | } 203 | 204 | test.equals(this.callback.callCount, 0); 205 | this.backoff.emit('ready'); 206 | 207 | test.ok(this.callback.calledWith(null, 'Success!')); 208 | test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3)); 209 | test.done(); 210 | }, 211 | 212 | "call should fail when the backoff limit is reached": function(test) { 213 | var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback); 214 | var error = new Error(); 215 | this.wrappedFn.yields(error); 216 | call.start(this.backoffFactory); 217 | 218 | for (var i = 0; i < 3; i++) { 219 | this.backoff.emit('ready'); 220 | } 221 | 222 | test.equals(this.callback.callCount, 0); 223 | 224 | this.backoff.emit('fail'); 225 | 226 | test.ok(this.callback.calledWith(error)); 227 | test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3)); 228 | test.done(); 229 | }, 230 | 231 | "call should fail when the retry predicate returns false": function(test) { 232 | var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback); 233 | call.retryIf(function(err) { return err.retriable; }); 234 | 235 | var retriableError = new Error(); 236 | retriableError.retriable = true; 237 | 238 | var fatalError = new Error(); 239 | fatalError.retriable = false; 240 | 241 | this.wrappedFn. 242 | onCall(0).yields(retriableError). 243 | onCall(1).yields(retriableError). 244 | onCall(2).yields(fatalError); 245 | 246 | call.start(this.backoffFactory); 247 | 248 | for (var i = 0; i < 2; i++) { 249 | this.backoff.emit('ready'); 250 | } 251 | 252 | test.equals(this.callback.callCount, 1); 253 | test.ok(this.callback.calledWith(fatalError)); 254 | test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3)); 255 | test.done(); 256 | }, 257 | 258 | "wrapped function's callback shouldn't be called after abort": function(test) { 259 | var call = new FunctionCall(function(callback) { 260 | call.abort(); // Abort in middle of wrapped function's execution. 261 | callback(null, 'ok'); 262 | }, [], this.callback); 263 | 264 | call.start(this.backoffFactory); 265 | 266 | test.equals(this.callback.callCount, 1, 267 | 'Wrapped function\'s callback shouldn\'t be called after abort.'); 268 | test.ok(this.callback.calledWithMatch(sinon.match(function (err) { 269 | return !!err.message.match(/Backoff aborted/); 270 | }, "abort error"))); 271 | test.done(); 272 | }, 273 | 274 | "abort event is emitted once when abort is called": function(test) { 275 | var call = new FunctionCall(this.wrappedFn, [], this.callback); 276 | this.wrappedFn.yields(new Error()); 277 | var callEventSpy = sinon.spy(); 278 | 279 | call.on('abort', callEventSpy); 280 | call.start(this.backoffFactory); 281 | 282 | call.abort(); 283 | call.abort(); 284 | call.abort(); 285 | 286 | test.equals(callEventSpy.callCount, 1); 287 | test.done(); 288 | }, 289 | 290 | "getLastResult should return the last intermediary result": function(test) { 291 | var call = new FunctionCall(this.wrappedFn, [], this.callback); 292 | this.wrappedFn.yields(1); 293 | call.start(this.backoffFactory); 294 | 295 | for (var i = 2; i < 5; i++) { 296 | this.wrappedFn.yields(i); 297 | this.backoff.emit('ready'); 298 | test.deepEqual([i], call.getLastResult()); 299 | } 300 | 301 | this.wrappedFn.yields(null); 302 | this.backoff.emit('ready'); 303 | test.deepEqual([null], call.getLastResult()); 304 | 305 | test.done(); 306 | }, 307 | 308 | "getNumRetries should return the number of retries": function(test) { 309 | var call = new FunctionCall(this.wrappedFn, [], this.callback); 310 | 311 | this.wrappedFn.yields(1); 312 | call.start(this.backoffFactory); 313 | // The inital call doesn't count as a retry. 314 | test.equals(0, call.getNumRetries()); 315 | 316 | for (var i = 2; i < 5; i++) { 317 | this.wrappedFn.yields(i); 318 | this.backoff.emit('ready'); 319 | test.equals(i - 1, call.getNumRetries()); 320 | } 321 | 322 | this.wrappedFn.yields(null); 323 | this.backoff.emit('ready'); 324 | test.equals(4, call.getNumRetries()); 325 | 326 | test.done(); 327 | }, 328 | 329 | "wrapped function's errors should be propagated": function(test) { 330 | var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback); 331 | this.wrappedFn.throws(new Error()); 332 | test.throws(function() { 333 | call.start(this.backoffFactory); 334 | }, Error); 335 | test.done(); 336 | }, 337 | 338 | "wrapped callback's errors should be propagated": function(test) { 339 | var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback); 340 | this.wrappedFn.yields(null, 'Success!'); 341 | this.callback.throws(new Error()); 342 | test.throws(function() { 343 | call.start(this.backoffFactory); 344 | }, Error); 345 | test.done(); 346 | }, 347 | 348 | "call event should be emitted when wrapped function gets called": function(test) { 349 | this.wrappedFn.yields(1); 350 | var callEventSpy = sinon.spy(); 351 | 352 | var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback); 353 | call.on('call', callEventSpy); 354 | call.start(this.backoffFactory); 355 | 356 | for (var i = 1; i < 5; i++) { 357 | this.backoff.emit('ready'); 358 | } 359 | 360 | test.equal(5, callEventSpy.callCount, 361 | 'The call event should have been emitted 5 times.'); 362 | test.deepEqual([1, 'two'], callEventSpy.getCall(0).args, 363 | 'The call event should carry function\'s args.'); 364 | test.done(); 365 | }, 366 | 367 | "callback event should be emitted when callback is called": function(test) { 368 | var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback); 369 | var callbackSpy = sinon.spy(); 370 | call.on('callback', callbackSpy); 371 | 372 | this.wrappedFn.yields('error'); 373 | call.start(this.backoffFactory); 374 | 375 | this.wrappedFn.yields(null, 'done'); 376 | this.backoff.emit('ready'); 377 | 378 | test.equal(2, callbackSpy.callCount, 379 | 'Callback event should have been emitted 2 times.'); 380 | test.deepEqual(['error'], callbackSpy.firstCall.args, 381 | 'First callback event should carry first call\'s results.'); 382 | test.deepEqual([null, 'done'], callbackSpy.secondCall.args, 383 | 'Second callback event should carry second call\'s results.'); 384 | test.done(); 385 | }, 386 | 387 | "backoff event should be emitted on backoff start": function(test) { 388 | var err = new Error('backoff event error'); 389 | var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback); 390 | var backoffSpy = sinon.spy(); 391 | 392 | call.on('backoff', backoffSpy); 393 | 394 | this.wrappedFn.yields(err); 395 | call.start(this.backoffFactory); 396 | this.backoff.emit('backoff', 3, 1234, err); 397 | 398 | test.ok(this.backoff.backoff.calledWith(err), 399 | 'The backoff instance should have been called with the error.'); 400 | test.equal(1, backoffSpy.callCount, 401 | 'Backoff event should have been emitted 1 time.'); 402 | test.deepEqual([3, 1234, err], backoffSpy.firstCall.args, 403 | 'Backoff event should carry the backoff number, delay and error.'); 404 | test.done(); 405 | } 406 | }; 407 | --------------------------------------------------------------------------------