├── lib ├── .gitignore └── seq-queue.js ├── AUTHORS ├── index.js ├── .gitignore ├── Makefile ├── .jshintrc ├── package.json ├── LICENSE ├── README.md └── test └── seq-queue-test.js /lib/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | * Yongchang Zhou -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/seq-queue'); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | node_modules/ 3 | lib/doc/ 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test/*.js 2 | REPORTER = spec 3 | TIMEOUT = 5000 4 | 5 | test: 6 | @./node_modules/.bin/mocha \ 7 | --reporter $(REPORTER) --timeout $(TIMEOUT) $(TESTS) 8 | 9 | .PHONY: test -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "describe", 4 | "it", 5 | "before", 6 | "after", 7 | "window", 8 | "__resources__" 9 | ], 10 | "es5": true, 11 | "node": true, 12 | "eqeqeq": true, 13 | "undef": true, 14 | "curly": true, 15 | "bitwise": true, 16 | "immed": false, 17 | "newcap": true, 18 | "nonew": true 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seq-queue", 3 | "author": "changchang ", 4 | "version": "0.0.5", 5 | "description": "A simple tool to keep requests to be executed in order.", 6 | "homepage": "https://github.com/changchang/seq-queue", 7 | "repository": { 8 | "type": "git", 9 | "url": "git@github.com:changchang/seq-queue.git" 10 | }, 11 | "dependencies": { 12 | }, 13 | "devDependencies": { 14 | "mocha": ">=0.0.1", 15 | "should": ">=0.0.1" 16 | } 17 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012 Netease, Inc. and other pomelo contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | seq-queue - queue to keep request process in sequence 2 | ===================================================== 3 | 4 | Seq-queue is simple tool to keep requests to be executed in order. 5 | 6 | As we known, Node.js codes run in asynchronous mode and the callbacks are unordered. But sometimes we may need the requests to be processed in order. For example, in a game, a player would do some operations such as turn right and go ahead. And in the server side, we would like to process these requests one by one, not do them all at the same time. 7 | 8 | Seq-queue takes the responsibility to make the asynchronous, unordered processing flow into serial and ordered. It's simple but not a repeated wheel. 9 | 10 | Seq-queue is a FIFO task queue and we can push tasks as we wish, anytime(before the queue closed), anywhere(if we hold the queue instance). A task is known as a function and we can do anything in the function and just need to call `task.done()` to tell the queue current task has finished. It promises that a task in queue would not be executed util all tasks before it finished. 11 | 12 | Seq-queue add timeout for each task execution. If a task throws an uncaught exception in its call back or a developer forgets to call `task.done()` callback, queue would be blocked and would not execute the left tasks. To avoid these situations, seq-queue set a timeout for each task. If a task timeout, queue would drop the task and notify develop by a 'timeout' event and then invoke the next task. Any `task.done()` invoked in a timeout task would be ignored. 13 | 14 | * Tags: node.js 15 | 16 | ##Installation 17 | ``` 18 | npm install seq-queue 19 | ``` 20 | 21 | ##Usage 22 | ``` javascript 23 | var seqqueue = require('seq-queue'); 24 | 25 | var queue = seqqueue.createQueue(1000); 26 | 27 | queue.push( 28 | function(task) { 29 | setTimeout(function() { 30 | console.log('hello '); 31 | task.done(); 32 | }, 500); 33 | }, 34 | function() { 35 | console.log('task timeout'); 36 | }, 37 | 1000 38 | ); 39 | 40 | queue.push( 41 | function(task) { 42 | setTimeout(function() { 43 | console.log('world~'); 44 | task.done(); 45 | }, 500); 46 | } 47 | ); 48 | ``` 49 | 50 | ##API 51 | ###seqqueue.createQueue(timeout) 52 | Create a new queue instance. A global timeout value in ms for the new instance can be set by `timeout` parameter or use the default timeout (3s) by no parameter. 53 | 54 | ###queue.push(fn, ontimeout, timeout) 55 | Add a task into the queue instance. 56 | ####Arguments 57 | + fn(task) - The function that describes the content of task and would be invoke by queue. `fn` takes a arguemnt task and we *must* call task.done() to tell queue current task has finished. 58 | + ontimeout() - Callback for task timeout. 59 | + timeout - Timeout in ms for `fn`. If specified, it would overwrite the global timeout that set by `createQueue` for `fn`. 60 | 61 | ###queue.close(force) 62 | Close the queue. A closed queue would stop receiving new task immediately. And the left tasks would be treated in different ways decided by `force`. 63 | ####Arguments 64 | + force - If true, queue would stop working immediately and ignore any tasks left in queue. Otherwise queue would execute the tasks in queue and then stop. 65 | 66 | ##Event 67 | Seq-queue instances extend the EventEmitter and would emit events in their life cycles. 68 | ###'timeout'(totask) 69 | If current task not invoke task.done() within the timeout ms, a timeout event would be emit. totask.fn and totask.timeout is the `fn` and `timeout` arguments that passed by `queue.push(2)`. 70 | ###'error'(err, task) 71 | If the task function (not callbacks) throws an uncaught error, queue would emit an error event and passes the err and task informations by event callback arguments. 72 | ###'closed' 73 | Emit when the close(false) is invoked. 74 | ###'drained' 75 | Emit when close(true) is invoked or all tasks left have finished in closed status. 76 | -------------------------------------------------------------------------------- /lib/seq-queue.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var util = require('util'); 3 | 4 | var DEFAULT_TIMEOUT = 3000; 5 | var INIT_ID = 0; 6 | var EVENT_CLOSED = 'closed'; 7 | var EVENT_DRAINED = 'drained'; 8 | 9 | /** 10 | * Instance a new queue 11 | * 12 | * @param {Number} timeout a global timeout for new queue 13 | * @class 14 | * @constructor 15 | */ 16 | var SeqQueue = function(timeout) { 17 | EventEmitter.call(this); 18 | 19 | if(timeout && timeout > 0) { 20 | this.timeout = timeout; 21 | } else { 22 | this.timeout = DEFAULT_TIMEOUT; 23 | } 24 | 25 | this.status = SeqQueueManager.STATUS_IDLE; 26 | this.curId = INIT_ID; 27 | this.queue = []; 28 | }; 29 | util.inherits(SeqQueue, EventEmitter); 30 | 31 | /** 32 | * Add a task into queue. 33 | * 34 | * @param fn new request 35 | * @param ontimeout callback when task timeout 36 | * @param timeout timeout for current request. take the global timeout if this is invalid 37 | * @returns true or false 38 | */ 39 | SeqQueue.prototype.push = function(fn, ontimeout, timeout) { 40 | if(this.status !== SeqQueueManager.STATUS_IDLE && this.status !== SeqQueueManager.STATUS_BUSY) { 41 | //ignore invalid status 42 | return false; 43 | } 44 | 45 | if(typeof fn !== 'function') { 46 | throw new Error('fn should be a function.'); 47 | } 48 | this.queue.push({fn: fn, ontimeout: ontimeout, timeout: timeout}); 49 | 50 | if(this.status === SeqQueueManager.STATUS_IDLE) { 51 | this.status = SeqQueueManager.STATUS_BUSY; 52 | var self = this; 53 | process.nextTick(function() { 54 | self._next(self.curId); 55 | }); 56 | } 57 | return true; 58 | }; 59 | 60 | /** 61 | * Close queue 62 | * 63 | * @param {Boolean} force if true will close the queue immediately else will execute the rest task in queue 64 | */ 65 | SeqQueue.prototype.close = function(force) { 66 | if(this.status !== SeqQueueManager.STATUS_IDLE && this.status !== SeqQueueManager.STATUS_BUSY) { 67 | //ignore invalid status 68 | return; 69 | } 70 | 71 | if(force) { 72 | this.status = SeqQueueManager.STATUS_DRAINED; 73 | if(this.timerId) { 74 | clearTimeout(this.timerId); 75 | this.timerId = undefined; 76 | } 77 | this.emit(EVENT_DRAINED); 78 | } else { 79 | this.status = SeqQueueManager.STATUS_CLOSED; 80 | this.emit(EVENT_CLOSED); 81 | } 82 | }; 83 | 84 | /** 85 | * Invoke next task 86 | * 87 | * @param {String|Number} tid last executed task id 88 | * @api private 89 | */ 90 | SeqQueue.prototype._next = function(tid) { 91 | if(tid !== this.curId || this.status !== SeqQueueManager.STATUS_BUSY && this.status !== SeqQueueManager.STATUS_CLOSED) { 92 | //ignore invalid next call 93 | return; 94 | } 95 | 96 | if(this.timerId) { 97 | clearTimeout(this.timerId); 98 | this.timerId = undefined; 99 | } 100 | 101 | var task = this.queue.shift(); 102 | if(!task) { 103 | if(this.status === SeqQueueManager.STATUS_BUSY) { 104 | this.status = SeqQueueManager.STATUS_IDLE; 105 | this.curId++; //modify curId to invalidate timeout task 106 | } else { 107 | this.status = SeqQueueManager.STATUS_DRAINED; 108 | this.emit(EVENT_DRAINED); 109 | } 110 | return; 111 | } 112 | 113 | var self = this; 114 | task.id = ++this.curId; 115 | 116 | var timeout = task.timeout > 0 ? task.timeout : this.timeout; 117 | timeout = timeout > 0 ? timeout : DEFAULT_TIMEOUT; 118 | this.timerId = setTimeout(function() { 119 | process.nextTick(function() { 120 | self._next(task.id); 121 | }); 122 | self.emit('timeout', task); 123 | if(task.ontimeout) { 124 | task.ontimeout(); 125 | } 126 | }, timeout); 127 | 128 | try { 129 | task.fn({ 130 | done: function() { 131 | var res = task.id === self.curId; 132 | process.nextTick(function() { 133 | self._next(task.id); 134 | }); 135 | return res; 136 | } 137 | }); 138 | } catch(err) { 139 | self.emit('error', err, task); 140 | process.nextTick(function() { 141 | self._next(task.id); 142 | }); 143 | } 144 | }; 145 | 146 | /** 147 | * Queue manager. 148 | * 149 | * @module 150 | */ 151 | var SeqQueueManager = module.exports; 152 | 153 | /** 154 | * Queue status: idle, welcome new tasks 155 | * 156 | * @const 157 | * @type {Number} 158 | * @memberOf SeqQueueManager 159 | */ 160 | SeqQueueManager.STATUS_IDLE = 0; 161 | 162 | /** 163 | * Queue status: busy, queue is working for some tasks now 164 | * 165 | * @const 166 | * @type {Number} 167 | * @memberOf SeqQueueManager 168 | */ 169 | SeqQueueManager.STATUS_BUSY = 1; 170 | 171 | /** 172 | * Queue status: closed, queue has closed and would not receive task any more 173 | * and is processing the remaining tasks now. 174 | * 175 | * @const 176 | * @type {Number} 177 | * @memberOf SeqQueueManager 178 | */ 179 | SeqQueueManager.STATUS_CLOSED = 2; 180 | 181 | /** 182 | * Queue status: drained, queue is ready to be destroy 183 | * 184 | * @const 185 | * @type {Number} 186 | * @memberOf SeqQueueManager 187 | */ 188 | SeqQueueManager.STATUS_DRAINED = 3; 189 | 190 | /** 191 | * Create Sequence queue 192 | * 193 | * @param {Number} timeout a global timeout for the new queue instance 194 | * @return {Object} new queue instance 195 | * @memberOf SeqQueueManager 196 | */ 197 | SeqQueueManager.createQueue = function(timeout) { 198 | return new SeqQueue(timeout); 199 | }; -------------------------------------------------------------------------------- /test/seq-queue-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var SeqQueue = require('../lib/seq-queue'); 3 | 4 | var timeout = 1000; 5 | 6 | describe('seq-queue', function() { 7 | 8 | describe('#createQueue', function() { 9 | it('should return a seq-queue instance with init properties', function() { 10 | var queue = SeqQueue.createQueue(timeout); 11 | should.exist(queue); 12 | queue.should.have.property('timeout', timeout); 13 | queue.should.have.property('status', SeqQueue.IDLE); 14 | }); 15 | }); 16 | 17 | describe('#push' , function() { 18 | it('should change the queue status from idle to busy and invoke the task at once when task finish when queue idle', function(done) { 19 | var queue = SeqQueue.createQueue(timeout); 20 | queue.should.have.property('status', SeqQueue.IDLE); 21 | queue.push(function(task) { 22 | should.exist(task); 23 | task.done(); 24 | queue.should.have.property('status', SeqQueue.IDLE); 25 | done(); 26 | }); 27 | queue.should.have.property('status', SeqQueue.BUSY); 28 | }); 29 | 30 | it('should keep the status busy and keep the new task wait until the former tasks finish when queue busy', function(done) { 31 | var queue = SeqQueue.createQueue(timeout); 32 | var formerTaskFinished = false; 33 | //add first task 34 | queue.push(function(task) { 35 | formerTaskFinished = true; 36 | task.done(); 37 | }); 38 | queue.should.have.property('status', SeqQueue.BUSY); 39 | //add second task 40 | queue.push(function(task) { 41 | formerTaskFinished.should.be.true; 42 | queue.should.have.property('status', SeqQueue.BUSY); 43 | task.done(); 44 | queue.should.have.property('status', SeqQueue.IDLE); 45 | done(); 46 | }); 47 | queue.should.have.property('status', SeqQueue.BUSY); 48 | }); 49 | 50 | it('should ok if the task call done() directly', function(done) { 51 | var queue = SeqQueue.createQueue(); 52 | var taskCount = 0; 53 | queue.push(function(task) { 54 | taskCount++; 55 | task.done(); 56 | }); 57 | queue.push(function(task) { 58 | taskCount++; 59 | task.done(); 60 | }); 61 | setTimeout(function() { 62 | taskCount.should.equal(2); 63 | done(); 64 | }, 500); 65 | }); 66 | }); 67 | 68 | describe('#close', function() { 69 | it('should not accept new request but should execute the rest task in queue when close gracefully', function(done) { 70 | var queue = SeqQueue.createQueue(timeout); 71 | var closedEventCount = 0; 72 | var drainedEventCount = 0; 73 | queue.on('closed', function() { 74 | closedEventCount++; 75 | }); 76 | queue.on('drained', function() { 77 | drainedEventCount++; 78 | }); 79 | var executedTaskCount = 0; 80 | queue.push(function(task) { 81 | executedTaskCount++; 82 | task.done(); 83 | }).should.be.true; 84 | queue.close(false); 85 | queue.should.have.property('status', SeqQueue.CLOSED); 86 | 87 | queue.push(function(task) { 88 | // never should be executed 89 | executedTaskCount++; 90 | task.done(); 91 | }).should.be.false; 92 | 93 | // wait all task finished 94 | setTimeout(function() { 95 | executedTaskCount.should.equal(1); 96 | closedEventCount.should.equal(1); 97 | drainedEventCount.should.equal(1); 98 | done(); 99 | }, 1000); 100 | }); 101 | 102 | it('should not execute any task and emit a drained event when close forcefully', function(done) { 103 | var queue = SeqQueue.createQueue(timeout); 104 | var drainedEventCount = 0; 105 | queue.on('drained', function() { 106 | drainedEventCount++; 107 | }); 108 | var executedTaskCount = 0; 109 | queue.push(function(task) { 110 | //never should be executed 111 | executedTaskCount++; 112 | task.done(); 113 | }).should.be.true; 114 | queue.close(true); 115 | queue.should.have.property('status', SeqQueue.DRAINED); 116 | 117 | // wait all task finished 118 | setTimeout(function() { 119 | executedTaskCount.should.equal(0); 120 | drainedEventCount.should.equal(1); 121 | done(); 122 | }, 1000); 123 | }); 124 | }); 125 | 126 | describe('#timeout', function() { 127 | it('should emit timeout event and execute the next task when a task timeout by default', function(done) { 128 | var queue = SeqQueue.createQueue(); 129 | var executedTaskCount = 0; 130 | var timeoutCount = 0; 131 | var onTimeoutCount = 0; 132 | //add timeout listener 133 | queue.on('timeout', function(task) { 134 | task.should.be.a('object'); 135 | task.fn.should.be.a('function'); 136 | timeoutCount++; 137 | }); 138 | 139 | queue.push(function(task) { 140 | executedTaskCount++; 141 | //no task.done() invoke to cause a timeout 142 | }, function() { 143 | onTimeoutCount++; 144 | }).should.be.true; 145 | 146 | queue.push(function(task) { 147 | executedTaskCount++; 148 | task.done(); 149 | }).should.be.true; 150 | 151 | setTimeout(function() { 152 | //wait all task finish 153 | executedTaskCount.should.be.equal(2); 154 | timeoutCount.should.be.equal(1); 155 | onTimeoutCount.should.be.equal(1); 156 | done(); 157 | }, 4000); //default timeout is 3s 158 | }); 159 | 160 | it('should return false when invoke task.done() if task has already timeout', function(done) { 161 | var queue = SeqQueue.createQueue(); 162 | var executedTaskCount = 0; 163 | var timeoutCount = 0; 164 | var timeout = 1000; 165 | 166 | //add timeout listener 167 | queue.on('timeout', function(task) { 168 | task.should.be.a('object'); 169 | task.fn.should.be.a('function'); 170 | timeoutCount++; 171 | }); 172 | 173 | queue.push(function(task) { 174 | executedTaskCount++; 175 | task.done().should.be.true; 176 | }).should.be.true; 177 | 178 | queue.push(function(task) { 179 | //sleep to make a timeout 180 | setTimeout(function() { 181 | executedTaskCount++; 182 | task.done().should.be.false; 183 | }, timeout + 1000); 184 | }, null, timeout).should.be.true; 185 | 186 | setTimeout(function() { 187 | //wait all task finish 188 | executedTaskCount.should.be.equal(2); 189 | timeoutCount.should.be.equal(1); 190 | done(); 191 | }, 4000); 192 | }); 193 | 194 | it('should never timeout after close forcefully', function(done) { 195 | var queue = SeqQueue.createQueue(timeout); 196 | var timeoutCount = 0; 197 | //add timeout listener 198 | queue.on('timeout', function(task) { 199 | //should never enter here 200 | timeoutCount++; 201 | }); 202 | 203 | queue.push(function(task) { 204 | //no task.done() invoke to cause a timeout 205 | }).should.be.true; 206 | 207 | queue.close(true); 208 | 209 | setTimeout(function() { 210 | //wait all task finish 211 | timeoutCount.should.be.equal(0); 212 | done(); 213 | }, timeout * 2); 214 | }); 215 | 216 | it('should use the global timeout value by default', function(done) { 217 | var globalTimeout = timeout + 100; 218 | var queue = SeqQueue.createQueue(globalTimeout); 219 | //add timeout listener 220 | queue.on('timeout', function(task) { 221 | (Date.now() - start).should.not.be.below(globalTimeout); 222 | done(); 223 | }); 224 | 225 | queue.push(function(task) { 226 | //no task.done() invoke to cause a timeout 227 | }).should.be.true; 228 | var start = Date.now(); 229 | }); 230 | 231 | it('should use the timeout value in #push if it was assigned', function(done) { 232 | var localTimeout = timeout / 2; 233 | var queue = SeqQueue.createQueue(timeout); 234 | //add timeout listener 235 | queue.on('timeout', function(task) { 236 | var diff = Date.now() - start; 237 | diff.should.not.be.below(localTimeout); 238 | diff.should.not.be.above(timeout); 239 | done(); 240 | }); 241 | 242 | queue.push(function(task) { 243 | //no task.done() invoke to cause a timeout 244 | }, null, localTimeout).should.be.true; 245 | var start = Date.now(); 246 | }); 247 | }); 248 | 249 | describe('#error', function() { 250 | it('should emit an error event and invoke next task when a task throws an event', function(done) { 251 | var queue = SeqQueue.createQueue(); 252 | var errorCount = 0; 253 | var taskCount = 0; 254 | //add timeout listener 255 | queue.on('error', function(err, task) { 256 | errorCount++; 257 | should.exist(err); 258 | should.exist(task); 259 | }); 260 | 261 | queue.push(function(task) { 262 | taskCount++; 263 | throw new Error('some error'); 264 | }).should.be.true; 265 | 266 | queue.push(function(task) { 267 | taskCount++; 268 | task.done(); 269 | }); 270 | 271 | setTimeout(function() { 272 | taskCount.should.equal(2); 273 | errorCount.should.equal(1); 274 | done(); 275 | }, 500); 276 | }); 277 | 278 | it('should be ok when task throw a error after done was invoked', function(done) { 279 | var queue = SeqQueue.createQueue(); 280 | var errorCount = 0; 281 | var taskCount = 0; 282 | //add timeout listener 283 | queue.on('error', function(err, task) { 284 | errorCount++; 285 | should.exist(err); 286 | should.exist(task); 287 | }); 288 | 289 | queue.push(function(task) { 290 | taskCount++; 291 | task.done(); 292 | throw new Error('some error'); 293 | }).should.be.true; 294 | 295 | queue.push(function(task) { 296 | taskCount++; 297 | task.done(); 298 | }); 299 | 300 | setTimeout(function() { 301 | taskCount.should.equal(2); 302 | errorCount.should.equal(1); 303 | done(); 304 | }, 500); 305 | }); 306 | }); 307 | }); --------------------------------------------------------------------------------