├── .travis.yml ├── .gitignore ├── package.json ├── LICENSE ├── test.js ├── README.md └── index.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.0" 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-class", 3 | "version": "0.4.1", 4 | "description": "Cleaner ES6 async class methods", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --require co-mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/danielstjules/async-class.git" 12 | }, 13 | "keywords": [ 14 | "async", 15 | "await", 16 | "class", 17 | "method", 18 | "es6", 19 | "promise", 20 | "coroutine", 21 | "generator" 22 | ], 23 | "author": "Daniel St. Jules (http://danielstjules.com/)", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/danielstjules/async-class/issues" 27 | }, 28 | "homepage": "https://github.com/danielstjules/async-class", 29 | "devDependencies": { 30 | "chai": "^3.2.0", 31 | "co-mocha": "^1.1.2", 32 | "mocha": "^2.3.2" 33 | }, 34 | "dependencies": { 35 | "bluebird": "^2.10.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daniel St. Jules 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | let expect = require('chai').expect; 3 | let wrap = require('./index').wrap; 4 | let Promise = require('bluebird'); 5 | 6 | class FakeDataStore { 7 | constructor() { 8 | // In memory store, imagine a DB instead 9 | this.store = new Map(); 10 | } 11 | 12 | static exampleSync() { 13 | return 'foo'; 14 | } 15 | 16 | static examplePromiseAsync() { 17 | return 'foo'; 18 | } 19 | 20 | static *exampleAsync() { 21 | return yield Promise.resolve('foo'); 22 | } 23 | 24 | static *example() { 25 | return yield Promise.resolve('foo'); 26 | } 27 | 28 | keys() { 29 | let keys = []; 30 | for (let key of this.store.keys()) { 31 | keys.push(key); 32 | } 33 | return keys; 34 | } 35 | 36 | get size() { 37 | return this.store.size; 38 | } 39 | 40 | getAsync(key) { 41 | let val = this.store.get(key); 42 | return Promise.resolve(val); 43 | } 44 | 45 | *setAsync(key, value) { 46 | this.store.set(key, value); 47 | return yield Promise.resolve(key); 48 | } 49 | } 50 | 51 | wrap(FakeDataStore); 52 | 53 | describe('async-class', function() { 54 | let dataStore; 55 | 56 | beforeEach(function() { 57 | dataStore = new FakeDataStore(); 58 | }); 59 | 60 | describe('wrap', function() { 61 | it('does not modify getters', function() { 62 | expect(dataStore.size).to.eql(0); 63 | }); 64 | 65 | it('only modifies properties that are functions', function() { 66 | expect(dataStore.store).to.be.instanceOf(Map); 67 | }); 68 | 69 | it('does not wrap functions that do not end with Async', function() { 70 | dataStore.store.set('foo', 'bar'); 71 | let keys = dataStore.keys(); 72 | expect(keys).to.eql(['foo']); 73 | }); 74 | 75 | it('wraps all GeneratorFunctions', function*() { 76 | let res = yield FakeDataStore.example(); 77 | expect(res).to.eql('foo'); 78 | }); 79 | 80 | it('wraps instance methods that end with Async', function*() { 81 | dataStore.store.set('foo', 'bar'); 82 | let res = yield dataStore.getAsync('foo'); 83 | expect(res).to.eql('bar'); 84 | }); 85 | 86 | it('wraps instance methods that use generator functions', function*() { 87 | let key = yield dataStore.setAsync('foo', 'bar'); 88 | let res = dataStore.store.get('foo'); 89 | expect(key).to.eql('foo'); 90 | expect(res).to.eql('bar'); 91 | }); 92 | 93 | it('does not wrap static methods that do not end with Async', function() { 94 | let res = FakeDataStore.exampleSync(); 95 | expect(res).to.eql('foo'); 96 | }); 97 | 98 | it('wraps static methods that end with Async', function*() { 99 | let res = yield FakeDataStore.examplePromiseAsync(); 100 | expect(res).to.eql('foo'); 101 | }); 102 | 103 | it('wraps static methods that use generator functions', function*() { 104 | let res = yield FakeDataStore.exampleAsync(); 105 | expect(res).to.eql('foo'); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # async-class 2 | 3 | Cleaner ES6 async class methods for Node 4.0.0+. A solution to using promises 4 | and coroutines with classes without the overhead of babel, or the need to adopt 5 | unimplemented syntax and features, until v8/node supports ES7 async/await. 6 | 7 | [![Build Status](https://travis-ci.org/danielstjules/async-class.svg?branch=master)](https://travis-ci.org/danielstjules/async-class) 8 | 9 | ## Installation 10 | 11 | ``` 12 | npm install --save async-class 13 | ``` 14 | 15 | ## Overview 16 | 17 | Using only ES6 features, how would you achieve a class like the following? 18 | 19 | ``` javascript 20 | 'use strict'; 21 | 22 | class FakeDataStore { 23 | constructor() { 24 | this.store = new Map(); 25 | } 26 | 27 | async setAsync(key, value) { 28 | this.store.set(key, value); 29 | return await Promise.resolve(key); 30 | } 31 | } 32 | ``` 33 | 34 | You'd use libraries that offer coroutine functionality like `co` or `bluebird`. 35 | However, there's no way to decorate those methods with ES6. Without the ES6 36 | class sugar, we'd like to achieve the following: 37 | 38 | ``` javascript 39 | 'use strict'; 40 | let Promise = require('bluebird'); 41 | 42 | function FakeDataStore { 43 | this.store = new Map(); 44 | } 45 | 46 | FakeDataStore.prototype.setAsync = Promise.coroutine(function*(key, value) { 47 | this.store.set(key, value); 48 | return yield Promise.resolve(key); 49 | }; 50 | ``` 51 | 52 | That's where this library comes in. Using it is simple: 53 | 54 | ``` javascript 55 | 'use strict'; 56 | let wrap = require('async-class').wrap; 57 | 58 | class FakeDataStore { 59 | constructor() { 60 | this.store = new Map(); 61 | } 62 | 63 | *setAsync(key, value) { 64 | this.store.set(key, value); 65 | return yield Promise.resolve(key); 66 | } 67 | } 68 | 69 | module.exports = wrap(FakeDataStore); 70 | ``` 71 | 72 | Clean ES6 classes and async methods! 73 | 74 | ## API 75 | 76 | #### async-class.wrap(klass [, methodNames]) 77 | 78 | Wraps static and instance methods whose name ends with Async, or are 79 | GeneratorFunctions. Any GeneratorFunction is wrapped with 80 | bluebird.coroutine(), and others with bluebird.method(). Accepts an optional 81 | array of method names, wrapping only those found in the array, and disabling 82 | the Async suffix check. Returns the class. 83 | 84 | #### async-class.wrapStaticMethods(klass [, methodNames]) 85 | 86 | Wraps static methods whose name ends with Async or are GeneratorFunctions. 87 | Any GeneratorFunction is wrapped with bluebird.coroutine(), and others with 88 | bluebird.method(). Accepts an optional array of method names, wrapping only 89 | those found in the array, and disabling the Async suffix check. Returns the 90 | class. 91 | 92 | #### async-class.wrapInstanceMethods(klass [, methodNames]) 93 | 94 | Wraps instance methods whose name ends with Async, or are GeneratorFunctions. 95 | Any GeneratorFunction is wrapped with bluebird.coroutine(), and others with 96 | bluebird.method(). Accepts an optional array of method names, wrapping only 97 | those found in the array, and disabling the Async suffix check. Returns the 98 | class. 99 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | let Promise = require('bluebird'); 3 | 4 | /** 5 | * Wraps static and instance methods whose name ends with Async, or are 6 | * GeneratorFunctions. Any GeneratorFunction is wrapped with 7 | * bluebird.coroutine(), and others with bluebird.method(). Accepts an optional 8 | * array of method names, wrapping only those found in the array, and disabling 9 | * the Async suffix check. Returns the class. 10 | * 11 | * @param {function} klass The class to wrap 12 | * @param {string[]} [methodNames] Optional array of method names 13 | * @returns {function} The supplied class 14 | * @throws {Error} If methodNames is provided, but is not an array 15 | */ 16 | function wrap(klass, methodNames) { 17 | validateMethodNames(methodNames); 18 | wrapStaticMethods(klass, methodNames); 19 | wrapInstanceMethods(klass, methodNames); 20 | return klass; 21 | } 22 | 23 | /** 24 | * Wraps static methods whose name ends with Async or are GeneratorFunctions. 25 | * Any GeneratorFunction is wrapped with bluebird.coroutine(), and others with 26 | * bluebird.method(). Accepts an optional array of method names, wrapping only 27 | * those found in the array, and disabling the Async suffix check. Returns the 28 | * class. 29 | * 30 | * @param {function} klass The class to wrap 31 | * @param {string[]} [methodNames] Optional array of method names 32 | * @returns {function} The supplied class 33 | * @throws {Error} If methodNames is provided, but is not an array 34 | */ 35 | function wrapStaticMethods(klass, methodNames) { 36 | validateMethodNames(methodNames); 37 | wrapFunctions(klass, methodNames); 38 | return klass; 39 | } 40 | 41 | /** 42 | * Wraps instance methods whose name ends with Async, or are GeneratorFunctions. 43 | * Any GeneratorFunction is wrapped with bluebird.coroutine(), and others with 44 | * bluebird.method(). Accepts an optional array of method names, wrapping only 45 | * those found in the array, and disabling the Async suffix check. Returns the 46 | * class. 47 | * 48 | * @param {function} klass The class to wrap 49 | * @param {string[]} [methodNames] Optional array of method names 50 | * @returns {function} The supplied class 51 | * @throws {Error} If methodNames is provided, but is not an array 52 | */ 53 | function wrapInstanceMethods(klass, methodNames) { 54 | validateMethodNames(methodNames); 55 | wrapFunctions(klass.prototype, methodNames); 56 | return klass; 57 | } 58 | 59 | /** 60 | * Helper function that validates the methodNames parameter. 61 | * 62 | * @param {string[]} [methodNames] Optional array of method names 63 | * @throws {Error} If methodNames is provided, but is not an array 64 | */ 65 | function validateMethodNames(methodNames) { 66 | if (methodNames && !(methodNames instanceof Array)) { 67 | throw new Error('Optional methodNames should be an array if provided'); 68 | } 69 | } 70 | 71 | function wrapFunctions(target, methodNames) { 72 | _actualMethodKeys(target).forEach(function(key) { 73 | let constructor = target[key].constructor.name; 74 | 75 | if (methodNames) { 76 | if (methodNames.indexOf(key) === -1) return; 77 | } else if (!key.endsWith('Async') && constructor !== 'GeneratorFunction') { 78 | return; 79 | } 80 | 81 | if (target[key].constructor.name === 'GeneratorFunction') { 82 | target[key] = Promise.coroutine(target[key]); 83 | } else { 84 | target[key] = Promise.method(target[key]); 85 | } 86 | }); 87 | } 88 | 89 | function _actualMethodKeys(target) { 90 | return Object.getOwnPropertyNames(target) 91 | .filter(key => { 92 | var propertyDescriptor = Object.getOwnPropertyDescriptor(target, key); 93 | return !propertyDescriptor.get && !propertyDescriptor.set; 94 | }) 95 | .filter(key => typeof target[key] === 'function'); 96 | } 97 | 98 | module.exports = { 99 | wrap, 100 | wrapStaticMethods, 101 | wrapInstanceMethods 102 | }; 103 | --------------------------------------------------------------------------------