diff --git a/.gitignore b/.gitignore index 7dccd97..8afa17e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,5 @@ -lib-cov -*.seed -*.log -*.csv -*.dat -*.out -*.pid -*.gz - -pids -logs -results - +.nyc_output +coverage node_modules -npm-debug.log \ No newline at end of file + +npm-debug.log diff --git a/.travis.yml b/.travis.yml index 00d6ca5..bb38269 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,18 @@ +sudo: false +os: linux language: node_js node_js: - - "0.12" - "0.10" - - "0.8" - - "iojs" - - "iojs-v1.0.4" - - "iojs-v2" - - "iojs-v3" -sudo: false + - "0.11" + - "0.12" + - "4" + - "5" + - "6" +env: + matrix: + - TEST_SUITE=unit +matrix: + include: + - node_js: "4" + env: TEST_SUITE=lint +script: npm run $TEST_SUITE diff --git a/README.md b/README.md index 351b873..27737a6 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,25 @@ -semaphore.js -============ +# semaphore -[![Build Status](https://travis-ci.org/abrkn/semaphore.js.svg?branch=master)](https://travis-ci.org/abrkn/semaphore.js) +[![NPM Package](https://img.shields.io/npm/v/semaphore.svg?style=flat-square)](https://www.npmjs.org/package/semaphore) +[![Build Status](https://img.shields.io/travis/abrkn/semaphore.js.svg?branch=master&style=flat-square)](https://travis-ci.org/abrkn/semaphore.js) -Install: +[![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) + + +## Installation + +``` npm install semaphore +``` + +## Examples Limit simultaneous access to a resource. ```javascript // Create -var sem = require('semaphore')(capacity); +const Semaphore = require('semaphore') +const sem = new Semaphore(capacity) // Take sem.take(fn[, n=1]) @@ -20,49 +29,46 @@ sem.take(n, fn) sem.leave([n]) ``` - ```javascript // Limit concurrent db access -var sem = require('semaphore')(1); -var server = require('http').createServer(req, res) { - sem.take(function() { - expensive_database_operation(function(err, res) { - sem.leave(); - - if (err) return res.end("Error"); - - return res.end(res); - }); - }); -}); +const Semaphore = require('semaphore') +const sem = new Semaphore(1) +const server = require('http').createServer((req, res) => { + sem.take(() => { + expensive_database_operation((err, res) => { + sem.leave() + res.end(err === null ? res : 'error') + }) + }) +}) ``` ```javascript // 2 clients at a time -var sem = require('semaphore')(2); -var server = require('http').createServer(req, res) { - res.write("Then good day, madam!"); - - sem.take(function() { - res.end("We hope to see you soon for tea."); - sem.leave(); - }); -}); +const Semaphore = require('semaphore') +const sem = new Semaphore(2) +const server = require('http').createServer((req, res) => { + res.write("Then good day, madam!") + + sem.take(() => { + res.end("We hope to see you soon for tea.") + sem.leave() + }) +}) ``` ```javascript // Rate limit -var sem = require('semaphore')(10); -var server = require('http').createServer(req, res) { - sem.take(function() { - res.end("."); - - setTimeout(sem.leave, 500) - }); -}); +const Semaphore = require('semaphore') +const sem = new Semaphore(10) +const server = require('http').createServer((req, res) => { + sem.take(() => { + res.end(".") + setTimeout(() => sem.leave(), 500) + }) +}) ``` -License -=== +## License MIT diff --git a/bower.json b/bower.json index af193c0..a6b6b18 100644 --- a/bower.json +++ b/bower.json @@ -1,26 +1,29 @@ { "name": "semaphore.js", - "version": "1.0.3", - "homepage": "https://github.com/abrkn/semaphore.js", - "authors": [ - "Andreas Brekken " - ], "description": "Limit simultaneous access to a resource.", - "main": "lib/semaphore.js", + "main": "./index.js", "moduleType": [ "globals", + "amd", "node" ], - "keywords": [ - "semaphore", - "concurrency" - ], "license": "MIT", "ignore": [ "**/.*", "node_modules", "bower_components", - "test", - "tests" - ] + "test" + ], + "keywords": [ + "concurrency", + "semaphore" + ], + "authors": [ + "Andreas Brekken " + ], + "homepage": "https://github.com/abrkn/semaphore.js", + "repository": { + "type": "git", + "url": "https://github.com/abrkn/semaphore.js.git" + } } diff --git a/index.js b/index.js new file mode 100644 index 0000000..d38b415 --- /dev/null +++ b/index.js @@ -0,0 +1,84 @@ +;(function (global) { + 'use strict' + + var nextTick = function (fn) { setTimeout(fn, 0) } + if (typeof process !== 'undefined' && process && typeof process.nextTick === 'function') { + // node.js and the like + nextTick = process.nextTick + } + + function checkNumber (n) { + if (typeof n !== 'number' || isNaN(n) || !isFinite(n)) { + throw new TypeError('expected number, got ' + n) + } + if (n % 1 !== 0 || n < 1) { + throw new RangeError('expected positive integer number, got ' + n) + } + } + + function checkFunction (f) { + if (typeof f !== 'function') throw new TypeError('expected function, got ' + f) + } + + function Semaphore (capacity) { + if (!(this instanceof Semaphore)) return new Semaphore(capacity) + + this._capacity = capacity || 1 + checkNumber(this._capacity) + + this._current = 0 + this._queue = [] + this._leave = this.leave.bind(this) + } + + Semaphore.prototype.take = function () { + var item = {} + if (typeof arguments[0] === 'function') { + item.task = arguments[0] + item.n = arguments[1] || 1 + } else { + item.task = arguments[1] + item.n = arguments[0] || 1 + } + + checkFunction(item.task) + checkNumber(item.n) + if (item.n > this._capacity) { + throw new RangeError('expected number in [1, ' + this._capacity + '], got ' + item.n) + } + + if (this._current + item.n > this._capacity) { + this._queue.push(item) + } else { + this._current += item.n + item.task(this._leave) + } + } + + Semaphore.prototype.leave = function (n) { + n = n || 1 + checkNumber(n) + + this._current = Math.max(this._current - n, 0) + + if (this._queue.length === 0) return + if (this._current + this._queue[0].n > this._capacity) return + + var item = this._queue.shift() + this._current += item.n + + var leave = this._leave + nextTick(function () { item.task(leave) }) + } + + if (typeof exports === 'object') { + // node export + module.exports = Semaphore + } else if (typeof define === 'function' && define.amd) { + // amd export + define(function () { return Semaphore }) + } else { + // browser global + global.semaphore = global.Semaphore = Semaphore + } +}(this)) diff --git a/lib/semaphore.js b/lib/semaphore.js deleted file mode 100644 index 81fbbd4..0000000 --- a/lib/semaphore.js +++ /dev/null @@ -1,96 +0,0 @@ -;(function(global) { - -'use strict'; - -var nextTick = function (fn) { setTimeout(fn, 0); } -if (typeof process != 'undefined' && process && typeof process.nextTick == 'function') { - // node.js and the like - nextTick = process.nextTick; -} - -function semaphore(capacity) { - var semaphore = { - capacity: capacity || 1, - current: 0, - queue: [], - firstHere: false, - - take: function() { - if (semaphore.firstHere === false) { - semaphore.current++; - semaphore.firstHere = true; - var isFirst = 1; - } else { - var isFirst = 0; - } - var item = { n: 1 }; - - if (typeof arguments[0] == 'function') { - item.task = arguments[0]; - } else { - item.n = arguments[0]; - } - - if (arguments.length >= 2) { - if (typeof arguments[1] == 'function') item.task = arguments[1]; - else item.n = arguments[1]; - } - - var task = item.task; - item.task = function() { task(semaphore.leave); }; - - if (semaphore.current + item.n - isFirst > semaphore.capacity) { - if (isFirst === 1) { - semaphore.current--; - semaphore.firstHere = false; - } - return semaphore.queue.push(item); - } - - semaphore.current += item.n - isFirst; - item.task(semaphore.leave); - if (isFirst === 1) semaphore.firstHere = false; - }, - - leave: function(n) { - n = n || 1; - - semaphore.current -= n; - - if (!semaphore.queue.length) { - if (semaphore.current < 0) { - throw new Error('leave called too many times.'); - } - - return; - } - - var item = semaphore.queue[0]; - - if (item.n + semaphore.current > semaphore.capacity) { - return; - } - - semaphore.queue.shift(); - semaphore.current += item.n; - - nextTick(item.task); - } - }; - - return semaphore; -}; - -if (typeof exports === 'object') { - // node export - module.exports = semaphore; -} else if (typeof define === 'function' && define.amd) { - // amd export - define(function () { - return semaphore; - }); -} else { - // browser global - global.semaphore = semaphore; -} -}(this)); diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json deleted file mode 100644 index d14744a..0000000 --- a/npm-shrinkwrap.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "semaphore", - "version": "1.0.5", - "dependencies": {} -} diff --git a/package.json b/package.json index 0151a2f..cbb532d 100644 --- a/package.json +++ b/package.json @@ -2,21 +2,34 @@ "name": "semaphore", "version": "1.0.5", "description": "semaphore for node", - "engines": { - "node": ">=0.8.0" - }, - "main": "./lib/semaphore.js", - "dependencies": {}, - "devDependencies": { - "mocha": "2.x.x", - "should": "8.x.x" - }, + "keywords": [ + "concurrency", + "semaphore" + ], "homepage": "https://github.com/abrkn/semaphore.js", + "files": [ + "index.js" + ], + "main": "./index.js", "repository": { "type": "git", - "url": "git@github.com:abrkn/semaphore.js.git" + "url": "https://github.com/abrkn/semaphore.js.git" }, "scripts": { - "test": "mocha" + "coverage": "nyc tape test/*.js", + "lint": "standard", + "test": "npm run lint && npm run unit", + "unit": "tape test/*.js" + }, + "dependencies": {}, + "devDependencies": { + "nyc": "^6.4.3", + "standard": "^7.0.1", + "tape": "^4.5.1" + }, + "standard": { + "globals": [ + "define" + ] } } diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..06de258 --- /dev/null +++ b/test/index.js @@ -0,0 +1,149 @@ +var test = require('tape') +var Semaphore = require('../') + +function Phone () { + this.state = 'free' +} + +Phone.prototype.dial = function (callback) { + if (this.state === 'busy') return callback(new Error('The phone is busy')) + this.state = 'busy' + setTimeout(function () { callback(null) }, 100) +} + +Phone.prototype.hangup = function (callback) { + if (this.state === 'free') return callback(new Error('The phone is not in use')) + this.state = 'free' +} + +test('should not be using a bad example', function (t) { + var phone = new Phone() + + // Call Bob + phone.dial(function (err) { + t.error(err) + phone.hangup() + }) + + // Cannot call Bret, because the phone is already busy with Bob. + phone.dial(function (err) { + t.same(err.message, 'The phone is busy') + t.end() + }) +}) + +test('should not break the phone', function (t) { + var phone = new Phone() + var sem = new Semaphore(1) + + // Call Jane + sem.take(function () { + phone.dial(function (err) { + t.error(err) + phone.hangup() + sem.leave() + }) + }) + + // Call Jon (will need to wait for call with Jane to complete) + sem.take(function () { + phone.dial(function (err) { + t.error(err) + phone.hangup() + sem.leave() + t.end() + }) + }) +}) + +test('should not be slow', function (t) { + var s = new Semaphore(3) + var values = [] + + function push (n) { + values.push(n) + s.leave + } + + push(1) + push(2) + push(3) + push(4) + push(5) + + t.same(values, [1, 2, 3, 4, 5]) + t.end() +}) + +test('should not let past more than capacity', function (t) { + var s = new Semaphore(3) + var values = [] + var speed = 50 + + s.take(function (leave) { values.push(1); setTimeout(leave, speed * 1) }) + s.take(function (leave) { values.push(2); setTimeout(leave, speed * 2) }) + s.take(function (leave) { values.push(3); setTimeout(leave, speed * 3) }) + s.take(function () { values.push(4) }) + s.take(function () { values.push(5) }) + + ;(function tick (n) { + switch (n) { + case 0: // After 0 sec + console.log('0 seconds passed.') + t.same(s._current, s._capacity) + t.same(s._queue.length, 2) + t.same(values, [1, 2, 3]) + break + case 1: // After 1 sec + console.log('1 seconds passed.') + t.same(s._current, s._capacity) + t.same(s._queue.length, 1) + t.same(values, [1, 2, 3, 4]) + break + case 2: // After 2 sec + console.log('2 seconds passed.') + t.same(s._current, s._capacity) + t.same(s._queue.length, 0) + t.same(values, [1, 2, 3, 4, 5]) + break + case 3: // After 3 sec + console.log('3 seconds passed.') + t.same(s._current, s._capacity - 1) + t.same(s._queue.length, 0) + t.same(values, [1, 2, 3, 4, 5]) + t.end() + break + } + + if (n < 3) setTimeout(tick.bind(null, n + 1), speed * 1.1) + })(0) +}) + +test('should respect number', function (t) { + t.test('should fail when taking more than the capacity allows', function (t) { + var s = new Semaphore(1) + t.throws(function () { + s.take(2, function () {}) + }, /^RangeError: expected number in \[1, 1\], got 2$/) + t.end() + }) + + t.test('should work fine with correct input values', function (t) { + t.plan(3) + + var s = new Semaphore(10) // 10 + s.take(5, function (leave) { // 5 + t.pass() + s.take(4, function () { // 1 + leave(4) // 5 + t.pass() + + s.take(5, function () { + t.pass() + }) // 0 + }) + }) + + setTimeout(t.end, 25) + }) +}) diff --git a/test/semaphore.js b/test/semaphore.js deleted file mode 100644 index c3358b1..0000000 --- a/test/semaphore.js +++ /dev/null @@ -1,167 +0,0 @@ -var should = require('should'); -var assert = require('assert'); -var semaphore = require("../lib/semaphore.js"); -require('mocha'); - -var Phone = function() { - return { - state: "free", - - dial: function(callback) { - if (this.state != "free") { - return callback(new Error("The phone is busy")); - } - - this.state = "busy"; - - setTimeout(function() { - callback(); - }, 100); - }, - - hangup: function() { - if (this.state == "free") { - return callback(new Error("The phone is not in use")); - } - - this.state = "free"; - } - }; -}; - -it("should not be using a bad example", function(done) { - var phone = new Phone(); - - // Call Bob - phone.dial(function(err) { - if (err) return done(err); - - phone.hangup(); - }); - - // Cannot call Bret, because the phone is already busy with Bob. - phone.dial(function(err) { - should.exist(err); - done(); - }); -}); - -it("should not break the phone", function(done) { - var phone = new Phone(); - var sem = require('../lib/semaphore.js')(1); - - // Call Jane - sem.take(function() { - phone.dial(function(err) { - if (err) return done(err); - - phone.hangup(); - - sem.leave(); - }); - }); - - // Call Jon (will need to wait for call with Jane to complete) - sem.take(function() { - phone.dial(function(err) { - if (err) return done(err); - - phone.hangup(); - - sem.leave(); - - done(); - }); - }); -}); - -it('should not be slow', function(done) { - var s = require('../lib/semaphore.js')(3); - var values = []; - - s.take(function() { values.push(1); s.leave(); }); - s.take(function() { values.push(2); s.leave(); }); - s.take(function() { values.push(3); s.leave(); }); - s.take(function() { values.push(4); s.leave(); }); - s.take(function() { values.push(5); s.leave(); }); - - process.nextTick(function() { - values.length.should.equal(5); - done(); - }); -}); - -it('should not let past more than capacity', function(done) { - this.timeout(6000); - - var s = require('../lib/semaphore.js')(3); - var values = []; - var speed = 250; - - s.take(function() { values.push(1); setTimeout(function() { s.leave(); }, speed * 1); }); - s.take(function() { values.push(2); setTimeout(function() { s.leave(); }, speed * 2); }); - s.take(function(leave) { values.push(3); setTimeout(function() { leave(); }, speed * 3); }); - s.take(function() { values.push(4); }); - s.take(function() { values.push(5); }); - - var tickN = 0; - - var check = function() { - switch (tickN++) { - case 0: // After 0 sec - console.log("0 seconds passed.") - s.current.should.equal(s.capacity); - s.queue.length.should.equal(2); - values.should.eql([1, 2, 3]); - break; - case 1: // After 1 sec - console.log("1 seconds passed."); - s.current.should.equal(s.capacity); - s.queue.length.should.equal(1); - values.should.eql([1, 2, 3, 4]); - break; - case 2: // After 2 sec - console.log("2 seconds passed."); - s.current.should.equal(3); - s.queue.length.should.equal(0); - values.should.eql([1, 2, 3, 4, 5]); - break; - case 3: // After 3 sec - console.log("3 seconds passed."); - s.current.should.equal(2); - s.queue.length.should.equal(0); - values.should.eql([1, 2, 3, 4, 5]); - return done(); - } - - setTimeout(check, speed * 1.1); - }; - - check(); -}); - -describe("should respect number", function() { - it("should fail when taking more than the capacity allows", function(done) { - var s = semaphore(1); - - s.take(2, function() { - assert.fail(); - }); - - process.nextTick(done); - }); - - it("should work fine with correct input values", function(done) { - var s = semaphore(10); // 10 - - s.take(5, function(leave) { // 5 - s.take(4, function() { // 1 - leave(4); // 5 - - s.take(5, function() { - return done() - }); // 0 - }); - }); - }); -});