├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── atomic-counter.js ├── examples.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.0.3] - 2015-05-10 4 | ### Added 5 | - New `getLastValue` method that retrieves a previously generated value for an 6 | specified counter. 7 | - Request parameters sent to DynamoDB can be extended using `options.dynamodb`. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Sergio Alcantara 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of dynamodb-atomic-counter nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dynamodb-atomic-counter 2 | ======================= 3 | 4 | This library provides atomic counters using Amazon DynamoDB. Each increment request 5 | increments a counter value that is stored in a DynamoDB table (named "AtomicCounters" by default). 6 | Multiple increment requests can be sent simultaneously but each request will receive a 7 | unique value, therefore this library can be use to generate auto-increment ids. 8 | 9 | Installation 10 | ------------ 11 | 12 | Execute the following command at the root of your project: 13 | 14 | npm install dynamodb-atomic-counter 15 | 16 | 17 | `config` 18 | -------- 19 | 20 | This object is an instance of [AWS.Config](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html). 21 | To configure the internal instance of [AWS.DynamoDB](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html), 22 | you can follow one of the many methods described [here](http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/node-configuring.html) or 23 | manually using this `config` instance. 24 | 25 | `increment( counterId, options )` 26 | --------------------------------- 27 | 28 | This method increments the counter for the specified `counterId`. 29 | It returns an AWS-SDK [request](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Request.html) 30 | instance with a jQuery style promise interface applied to it. 31 | See the [jQuery documentation](http://api.jquery.com/category/deferred-object/) for details on how the promises work. 32 | [underscore.deferred](https://github.com/wookiehangover/underscore.deferred) is used to add the Promise interface to the returned request object. 33 | Additionally, success, error, and complete callback can be provided in the second argument. 34 | The `increment` method takes the following arguments: 35 | 36 | * `counterId`: The unique name/identifier of the counter. 37 | * `options` (optional): An options object to overwrite some of the default behaviour of the increment operation. All attributes in this object are optional. 38 | * `options.tableName`: The name of the DynamoDB table that stores the counters. If not specified, it uses "AtomicCounters" by default. 39 | * `options.keyAttribute`: The name of the attribute that stores the counter name/identifier. If not specified, it uses "id" by default. 40 | * `options.countAttribute`: The name of the attribute that stores the last value generated for the specified `counterId`. If not specified, it uses "lastValue" by default. 41 | * `options.increment`: Specifies by how much the counter should be incremented. If not specified, it uses 1 by default. 42 | * `options.success`: Success callback function. It receives a single argument: the value (integer) generated by this increment operation for the specified `counterId`. 43 | * `options.error`: Error callback function. If the DynamoDB UpdateItem request fails, the error callback is executed. It receives a single argument: the error object returned from AWS-SDK. 44 | * `options.complete`: Complete callback function. This callback is executed when the increment operation is completed, whether or not it was successful. It receives a single argument: an integer, if the operation was successful, or an error object if it failed. 45 | * `options.context`: The context object to use in all callbacks. If specified, the value of `this` within all callbacks will be `options.context`. 46 | 47 | `getLastValue( counterId, options )` 48 | ------------------------------------ 49 | 50 | This method retrieves, from DynamoDB, the last generated value for the specified `counterId`. If the counter doesn't exist, 51 | the success callback would receive 0 as the first argument. 52 | It returns an AWS-SDK [request](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Request.html) 53 | instance with a jQuery style promise interface applied to it. 54 | See the [jQuery documentation](http://api.jquery.com/category/deferred-object/) for details on how the promises work. 55 | [underscore.deferred](https://github.com/wookiehangover/underscore.deferred) is used to add the Promise interface to the returned request object. 56 | Additionally, success, error, and complete callback can be provided in the second argument. 57 | The `getLastValue` method takes the following arguments: 58 | 59 | * `counterId`: The unique name/identifier of the counter. 60 | * `options` (optional): The same options supported by the `increment` method are also supported by this method. 61 | 62 | 63 | Basic Usage 64 | ----------- 65 | 66 | The following few lines of code demonstrate basic usage of this library. Additional examples can be found in the examples.js file. 67 | 68 | var atomicCounter = require( 'dynamodb-atomic-counter' ); 69 | 70 | // Configure AWS as needed 71 | atomicCounter.config.update({ region: 'us-east-1' }); 72 | 73 | /** 74 | * Increment the "Users" counter. Make sure there's a table named 75 | * "AtomicCounters", with "id" (string) as the primary hash key, 76 | * in your AWS account. 77 | */ 78 | atomicCounter.increment( 'Users' ).done(function (value) { 79 | // `value` is the new incremented value. 80 | }).fail(function (error) { 81 | // An error occurred 82 | }).always(function (valueOrError) { 83 | // Executed whether or not the increment operation was successful 84 | }); 85 | 86 | /** 87 | * Retrieve the last value generated for the "Clients" counter. 88 | */ 89 | atomicCounter.getLastValue( 'Clients' ).done(function (lastValue) { 90 | // `lastValue` is the last value generated for the "Clients" counter. 91 | // If a values has not been generated before, `lastValue` would be 0. 92 | }).fail(function (error) { 93 | // An error occurred 94 | }).always(function (valueOrError) { 95 | // Executed whether or not the request was successful 96 | }); -------------------------------------------------------------------------------- /atomic-counter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * dynamodb-atomic-counter - (c) 2015 Sergio Alcantara 3 | * Generates unique identifiers using DynamoDB atomic counter update operations. 4 | * 5 | * @author Sergio Alcantara 6 | */ 7 | 8 | /** 9 | * Default name of the DynamoDB table where the atomic counters will be stored. 10 | */ 11 | var DEFAULT_TABLE_NAME = 'AtomicCounters', 12 | /** 13 | * Default attribute name that will identify each counter. 14 | */ 15 | DEFAULT_KEY_ATTRIBUTE = 'id', 16 | /** 17 | * Default attribute name of the count value attribute. 18 | * The count attribute indicates the "last value" used in the last increment operation. 19 | */ 20 | DEFAULT_COUNT_ATTRIBUTE = 'lastValue', 21 | /** 22 | * Default increment value. 23 | */ 24 | DEFAULT_INCREMENT = 1; 25 | 26 | var dynamo, 27 | AWS = require( 'aws-sdk' ), 28 | _ = require( 'underscore' ); 29 | 30 | _.mixin( require('underscore.deferred') ); 31 | 32 | 33 | /** 34 | * A convinience "no operation" function. 35 | */ 36 | var noop = function(){}; 37 | 38 | /** 39 | * Make the `AWS.DynamoDB.config` method available. 40 | */ 41 | exports.config = AWS.config; 42 | 43 | /** 44 | * Increments the counter for the specified `counterId`. 45 | * It returns an AWS-SDK request instance with a jQuery style promise interface applied to it. 46 | * See [jQuery documentation](http://api.jquery.com/category/deferred-object/) to find out how to attach callbacks 47 | * to the returned object using the methods: done, fail, always, and then. 48 | * 49 | * @method increment 50 | * @param {String} counterId The name or identifier of the counter to increment. 51 | * @param {Object} options An options object to overwrite some of the default behaviour of the increment operation. 52 | * @param {String} options.tableName The name of the DynamoDB table that stores the counters. If not specified, it uses "AtomicCounters" by default. 53 | * @param {String} options.keyAttribute The name of the attribute that stores the counter name/identifier. If not specified, it uses "id" by default. 54 | * @param {String} options.countAttribute The name of the attribute that stores the last value generated for the specified `counterId`. 55 | * If not specified, it uses "lastValue" by default. 56 | * @param {Integer} options.increment Specifies by how much the counter should be incremented. If not specified, it uses 1 by default. 57 | * @param {Function} options.success Success callback function. It receives a single argument: the value (integer) generated by this 58 | * increment operation for the specified `counterId`. 59 | * @param {Function} options.error Error callback function. If the DynamoDB UpdateItem request fails, the error callback is executed. 60 | * It receives a single argument: the error object returned from AWS-SDK. 61 | * @param {Function} options.complete Complete callback function. This callback is executed when the increment operation is completed, 62 | * whether or not it was successful. It receives a single argument: a number, if the operation was successful, or an error object if it failed. 63 | * @param options.context The context object to use in all callbacks. If specified, the value of `this` 64 | * within all callbacks will be `options.context`. 65 | * @param {Object} options.dynamodb Additional DynamoDB parameters. These parameters will be added to the parameters sent in the 66 | * [update item](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#updateItem-property) request. 67 | * @return {Request} A DynamoDB UpdateItem [request](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Request.html) object, 68 | * with a [jQuery](http://api.jquery.com/category/deferred-object/) style promise interface applied to it. 69 | */ 70 | exports.increment = function ( counterId, options ) { 71 | options || ( options = {} ); 72 | 73 | var request, 74 | deferred = new _.Deferred(), 75 | params = { 76 | Key: {}, 77 | AttributeUpdates: {}, 78 | ReturnValues: 'UPDATED_NEW', 79 | TableName: options.tableName || DEFAULT_TABLE_NAME 80 | }, 81 | keyAttribute = options.keyAttribute || DEFAULT_KEY_ATTRIBUTE, 82 | countAttribute = options.countAttribute || DEFAULT_COUNT_ATTRIBUTE, 83 | errorFn = _.isFunction( options.error ) ? options.error : noop, 84 | successFn = _.isFunction( options.success ) ? options.success : noop, 85 | completeFn = _.isFunction( options.complete ) ? options.complete : noop; 86 | 87 | params.Key[ keyAttribute ] = { S: counterId }; 88 | params.AttributeUpdates[ countAttribute ] = { 89 | Action: 'ADD', 90 | Value: { 91 | N: '' + ( options.increment || DEFAULT_INCREMENT ) 92 | } 93 | }; 94 | _.extend( params, options.dynamodb ); 95 | 96 | dynamo || ( dynamo = new AWS.DynamoDB() ); 97 | 98 | request = dynamo.updateItem(params, function (error, data) { 99 | var newCountValue; 100 | 101 | try { 102 | if ( error ) { 103 | throw error; 104 | } 105 | 106 | // Try to parse the count value. An exception will be thrown if it's not a valid number. 107 | newCountValue = parseInt( data.Attributes[ countAttribute ].N, 10 ); 108 | 109 | if ( !_.isNumber( newCountValue ) || _.isNaN( newCountValue ) ) { 110 | throw 'Could not parse incremented value (' + newCountValue + ').'; 111 | } 112 | } catch ( e ) { 113 | if ( options.context ) { 114 | deferred.rejectWith( options.context, [ e ] ); 115 | } else { 116 | deferred.reject( e ); 117 | } 118 | 119 | return; 120 | } 121 | 122 | if ( options.context ) { 123 | deferred.resolveWith( options.context, [ newCountValue ] ); 124 | } else { 125 | deferred.resolve( newCountValue ); 126 | } 127 | }); 128 | 129 | /** 130 | * Apply a promise interface to `request`, set the success, error, and complete callback, and return the promise. 131 | */ 132 | return deferred.promise( request ).done( successFn ).fail( errorFn ).always( completeFn ); 133 | }; 134 | 135 | /** 136 | * Gets the last value previously generated for the specified `counterId`. 137 | * It returns an AWS-SDK request instance with a jQuery style promise interface applied to it. 138 | * See [jQuery documentation](http://api.jquery.com/category/deferred-object/) to find out how to attach callbacks 139 | * to the returned object using the methods: done, fail, always, and then. 140 | * 141 | * @method getLastValue 142 | * @param {String} counterId The name or identifier of the counter. 143 | * @param {Object} options An options object to overwrite some of the default options. 144 | * @param {String} options.tableName The name of the DynamoDB table that stores the counters. If not specified, it uses "AtomicCounters" by default. 145 | * @param {String} options.keyAttribute The name of the attribute that stores the counter name/identifier. If not specified, it uses "id" by default. 146 | * @param {String} options.countAttribute The name of the attribute that stores the last value generated for the specified `counterId`. 147 | * If not specified, it uses "lastValue" by default. 148 | * @param {Function} options.success Success callback function. It receives a single argument: the last value (integer) previously generated 149 | * for the specified `counterId`. 150 | * @param {Function} options.error Error callback function. If the DynamoDB GetItem request fails, the error callback is executed. 151 | * It receives a single argument: the error object returned from AWS-SDK or the exception thrown when attempting to parse the response. 152 | * @param {Function} options.complete Complete callback function. This callback is executed when the GetItem request is completed, 153 | * whether or not it was successful. It receives a single argument: a number, if it was successful, or an error object if it failed. 154 | * @param options.context The context object to use in all callbacks. If specified, the value of `this` 155 | * within all callbacks will be `options.context`. 156 | * @param {Object} options.dynamodb Additional DynamoDB parameters. These parameters will be added to the parameters sent in the 157 | * [get item](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#getItem-property) request. 158 | * @return {Request} A DynamoDB GetItem [request](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Request.html) object, 159 | * with a [jQuery](http://api.jquery.com/category/deferred-object/) style promise interface applied to it. 160 | */ 161 | exports.getLastValue = function ( counterId, options ) { 162 | options || ( options = {} ); 163 | 164 | var request, 165 | deferred = new _.Deferred(), 166 | keyAttribute = options.keyAttribute || DEFAULT_KEY_ATTRIBUTE, 167 | countAttribute = options.countAttribute || DEFAULT_COUNT_ATTRIBUTE, 168 | params = { 169 | Key: {}, 170 | AttributesToGet: [ countAttribute ], 171 | TableName: options.tableName || DEFAULT_TABLE_NAME 172 | }, 173 | errorFn = _.isFunction( options.error ) ? options.error : noop, 174 | successFn = _.isFunction( options.success ) ? options.success : noop, 175 | completeFn = _.isFunction( options.complete ) ? options.complete : noop; 176 | 177 | params.Key[ keyAttribute ] = { S: counterId }; 178 | _.extend( params, options.dynamodb ); 179 | 180 | dynamo || ( dynamo = new AWS.DynamoDB() ); 181 | 182 | request = dynamo.getItem(params, function (errorObject, data) { 183 | var error, lastValue; 184 | 185 | if ( errorObject ) { 186 | error = errorObject; 187 | } else if ( _.isEmpty( data ) ) { 188 | /** 189 | * If the item doesn't exist, the response would be empty. 190 | * Set `lastValue` to 0 when the item doesn't exist. 191 | */ 192 | lastValue = 0; 193 | } else { 194 | try { 195 | // Try to parse the count value. An exception will be thrown if it's not a valid number. 196 | lastValue = parseInt( data.Item[ countAttribute ].N, 10 ); 197 | 198 | if ( !_.isNumber( lastValue ) || _.isNaN( lastValue ) ) { 199 | throw 'Could not parse incremented value (' + lastValue + ').'; 200 | } 201 | } catch ( e ) { 202 | error = e; 203 | } 204 | } 205 | 206 | if ( error ) { 207 | if ( options.context ) { 208 | deferred.rejectWith( options.context, [ e ] ); 209 | } else { 210 | deferred.reject( e ); 211 | } 212 | } else { 213 | if ( options.context ) { 214 | deferred.resolveWith( options.context, [ lastValue ] ); 215 | } else { 216 | deferred.resolve( lastValue ); 217 | } 218 | } 219 | }); 220 | 221 | /** 222 | * Apply a promise interface to `request`, set the success, error, and complete callback, and return the promise. 223 | */ 224 | return deferred.promise( request ).done( successFn ).fail( errorFn ).always( completeFn ); 225 | }; -------------------------------------------------------------------------------- /examples.js: -------------------------------------------------------------------------------- 1 | /** 2 | * These are just a few examples of how to use dynamodb-atomic-counter. 3 | * To run these examples execute the following command in your terminal: 4 | * 5 | * nodeunit examples.js 6 | * 7 | * 8 | * IMPORTANT: Before running these examples, create the following DynamoDB tables in your account. 9 | * 10 | * Table Name Primary hash key (string) 11 | * AtomicCounters id 12 | * MyCounters name 13 | */ 14 | 15 | var usersLastValue, 16 | _ = require( 'underscore' ), 17 | atomicCounter = require( './atomic-counter' ); 18 | 19 | _.mixin( require('underscore.deferred') ); 20 | 21 | 22 | /** 23 | * dynamodb-atomic-counter uses AWS-SDK. Visit the following page for details on how to configure AWS-SDK: 24 | * http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/node-configuring.html 25 | * 26 | * You can also manually configure it using the `config` object. 27 | */ 28 | atomicCounter.config.update({ region: 'us-east-1' }); 29 | 30 | /** 31 | * This function demonstrates how to use the success, error, and complete callbacks. 32 | */ 33 | exports[ 'Callbacks in options object' ] = function (test) { 34 | test.expect( 2 ); 35 | 36 | atomicCounter.increment('Users', { 37 | success: function (value) { 38 | usersLastValue = value; 39 | test.ok( true ); // success was executed 40 | }, 41 | error: function (error) { 42 | test.ok( false, 'Increment operation failed: ' + JSON.stringify( error ) ); 43 | }, 44 | complete: function (valueOrError) { 45 | test.ok( true ); // complete was executed 46 | test.done(); 47 | } 48 | }); 49 | }; 50 | 51 | /** 52 | * This function demonstrates how to attach done, fail, and always callbacks to the promise returned by `atomicCounter.increment`. 53 | */ 54 | exports[ 'Callbacks attached to promise' ] = function (test) { 55 | test.expect( 3 ); 56 | 57 | atomicCounter.increment( 'Users' ).done(function (value) { 58 | var expected = usersLastValue + 1; 59 | 60 | test.ok( true ); // success was executed 61 | test.strictEqual( value, expected, 'Expected increment value: ' + expected + ' - Actual increment value: ' + value ); 62 | }).fail(function (error) { 63 | test.ok( false, 'Increment operation failed: ' + JSON.stringify( error ) ); 64 | }).always(function (valueOrError) { 65 | test.ok( true ); // complete was executed 66 | test.done(); 67 | }); 68 | }; 69 | 70 | /** 71 | * This function demonstrates how to overwrite default options. 72 | */ 73 | exports[ 'Overwrite defaults' ] = function (test) { 74 | var context = {}; 75 | test.expect( 5 ); 76 | 77 | atomicCounter.increment('Pages', { 78 | increment: 12, // Increment counter by 12 79 | tableName: 'MyCounters', // Use the table "MyCounters", instead of the default "AtomicCounters" 80 | keyAttribute: 'name', // Use "name" as the identifier attribute of the counter 81 | countAttribute: 'lastIncrementedValue', // Store the incremented value in an attribute named "lastIncrementedValue" 82 | context: context // Specify the context for ALL callbacks 83 | }).done(function (value) { 84 | test.ok( true ); // done was executed 85 | test.strictEqual( this, context, 'Wrong context object.' ); 86 | test.equal( value % 12, 0, 'Did not increment the counter by the specified value. Value received: ' + value ); 87 | }).fail(function (error) { 88 | test.ok( false, 'Increment operation failed: ' + JSON.stringify( error ) ); 89 | }).always(function (valueOrError) { 90 | test.ok( true ); // always was executed 91 | test.strictEqual( this, context, 'Wrong context object.' ); 92 | test.done(); 93 | }); 94 | }; 95 | 96 | /** 97 | * This function tests sending multiple concurrent requests. 98 | */ 99 | exports[ 'Concurrent increments/requests' ] = function (test) { 100 | var i, 101 | total = 10, 102 | promises = []; 103 | 104 | test.expect( 4 ); 105 | 106 | for ( i = 0; i < total; i++ ) { 107 | promises.push( atomicCounter.increment( 'Clients' ) ); 108 | } 109 | 110 | _.when( promises ).done(function() { 111 | var values = _.toArray( arguments ), 112 | uniqueValues = _.unique( values ); 113 | 114 | test.ok( true ); // done was executed 115 | test.equal( values.length, total, 'Expected to receive ' + total + ' but received ' + values.length + '.' ); 116 | test.equal( values.length, uniqueValues.length, ( values.length - uniqueValues.length ) + ' duplicate value(s) received.' ); 117 | }).fail(function () { 118 | test.ok( false, 'One, or more, increment operation(s) failed: ' + JSON.stringify( arguments ) ); 119 | }).always(function() { 120 | test.ok( true ); // always was executed 121 | test.done(); 122 | }); 123 | }; 124 | 125 | /** 126 | * This function demonstrates how to use the getLastValue method. 127 | */ 128 | exports[ 'Get last value generated for a counter using ConsistentRead' ] = function (test) { 129 | var options = { 130 | dynamodb: { 131 | ConsistentRead: true 132 | } 133 | }; 134 | test.expect( 2 ); 135 | 136 | atomicCounter.getLastValue( 'Users', options ).done(function (lastValue) { 137 | test.ok( true ); // success was executed 138 | }).fail(function (error) { 139 | test.ok( false, 'Failed to retrieve the last value used: ' + JSON.stringify( error ) ); 140 | }).always(function (valueOrError) { 141 | test.ok( true ); // complete was executed 142 | test.done(); 143 | }); 144 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamodb-atomic-counter", 3 | "author": "Sergio Alcantara (https://github.com/serg-io)", 4 | "description": "This library provides atomic counters using Amazon DynamoDB.", 5 | "version": "0.1.1", 6 | "homepage": "https://github.com/serg-io/dynamodb-atomic-counter", 7 | "keywords": [ 8 | "atomic-counter", 9 | "atomic", 10 | "counter", 11 | "dynamodb", 12 | "aws", 13 | "amazon" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/serg-io/dynamodb-atomic-counter.git" 18 | }, 19 | "main": "atomic-counter.js", 20 | "scripts": { 21 | "postversion": "git push && git push --tags && npm publish" 22 | }, 23 | "dependencies": { 24 | "aws-sdk": "2.1.26", 25 | "underscore": "1.8.3", 26 | "underscore.deferred": "0.4.0" 27 | }, 28 | "devDependencies": { 29 | "nodeunit": "0.9.1" 30 | } 31 | } --------------------------------------------------------------------------------