diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..b088a8e --- /dev/null +++ b/.babelrc @@ -0,0 +1,10 @@ +{ + "env": { + "cjs": { + "presets": ["es2015"] + }, + "es": { + "presets": [["es2015", { "modules": false }]] + } + } +} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..a9be62e --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "extends": "eslint:recommended", + "env": { + "browser": true, + "node": true, + "es6": true, + "mocha": true + }, + "parser": "babel-eslint" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a402d15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules + +# compiled js files +dist diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..3c05ad9 --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +.babelrc +.eslintrc +test diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c50470b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: node_js + +node_js: + - 6.0 + - stable + +before_script: + - "npm install" + +script: + - "npm test" + +notifications: + email: + - cjrodr@yahoo.com diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7ddbcc2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Claudio Rodriguez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index fb3ca3d..2bd4352 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,60 @@ # redux-saga-event-iterator -An easy way of consuming an EventEmitter (e.g. socket.io) in redux-saga + +[![Build Status][travis-image]][travis-url] + +An easy way of consuming an EventEmitter (e.g. [socket.io][socket-io]) in [redux-saga][redux-saga] + +## Installation + +Install using [npm](http://npmjs.org/): + +```bash +$ npm install --save redux-saga-event-iterator +``` + +## Example + +```javascript +import {call} from 'redux-saga/effects'; +import eventIterator from 'redux-saga-event-iterator'; +import io from 'socket.io-client'; + +const socketClient = io('localhost:12345'); + +const listenerSaga = function * (eventName) { + const {nextEvent} = yield call(eventIterator, socketClient, eventName); + + while (true) { + const payload = yield call(nextEvent); + + // Do something with payload + } +}; +``` + +## Testing + +To run the tests: + +```bash +$ npm test +``` + +## Contributing + +Feel free to create a pull request. +Make sure to lint and test: + +```bash +$ npm run lint && npm run test +``` + +## License + +MIT - see [LICENSE][license-url] + +[redux-saga]: https://github.com/yelouafi/redux-saga +[socket-io]: https://github.com/socketio/socket.io +[travis-image]: https://travis-ci.org/claudiorodriguez/ngrammer.svg?branch=master +[travis-url]: https://travis-ci.org/claudiorodriguez/ngrammer +[license-url]: ./LICENSE diff --git a/package.json b/package.json new file mode 100644 index 0000000..c8863f3 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "redux-saga-event-iterator", + "version": "1.0.0", + "description": "An easy way of consuming an EventEmitter (e.g. socket.io) in redux-saga", + "main": "dist/cjs/index.js", + "jsnext:main": "dist/es/index.js", + "scripts": { + "lint": "./node_modules/.bin/eslint './src/**/*.js'", + "test": "./node_modules/.bin/cross-env BABEL_ENV=cjs ./node_modules/.bin/mocha --compilers js:babel-core/register './test/**/*.spec.js'", + "build:cjs": "./node_modules/.bin/rimraf ./dist/cjs && ./node_modules/.bin/cross-env BABEL_ENV=cjs ./node_modules/.bin/babel -d ./dist/cjs/ ./src/", + "build:es": "./node_modules/.bin/rimraf ./dist/es && ./node_modules/.bin/cross-env BABEL_ENV=es ./node_modules/.bin/babel ./src --out-dir ./dist/es", + "build": "npm run build:cjs && npm run build:es", + "prepublish": "npm run lint && npm run test && npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/claudiorodriguez/redux-saga-event-iterator.git" + }, + "keywords": [ + "javascript", + "redux", + "saga", + "socket.io", + "eventemitter", + "iterator" + ], + "author": "Claudio Rodriguez ", + "license": "MIT", + "bugs": { + "url": "https://github.com/claudiorodriguez/redux-saga-event-iterator/issues" + }, + "homepage": "https://github.com/claudiorodriguez/redux-saga-event-iterator#readme", + "dependencies": {}, + "devDependencies": { + "babel-cli": "^6.14.0", + "babel-core": "^6.14.0", + "babel-eslint": "^6.1.2", + "babel-preset-es2015": "^6.14.0", + "chai": "^3.5.0", + "cross-env": "^2.0.1", + "eslint": "^3.4.0", + "mocha": "^3.0.2", + "rimraf": "^2.5.4" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..d9447f2 --- /dev/null +++ b/src/index.js @@ -0,0 +1,37 @@ +const eventIterator = (emitter, eventName) => { + if (typeof Promise === 'undefined') { + throw new Error('an available Promise global is required'); + } + + if (!emitter || typeof emitter.on !== 'function') { + throw new Error('emitter must have an "on" function property'); + } + + if (typeof eventName !== 'string' || !eventName) { + throw new Error('eventName must be a string'); + } + + let deferred; + + emitter.on(eventName, (payload) => { + if (deferred) { + deferred.resolve(payload); + deferred = null; + } + }); + + return { + nextEvent () { + if (!deferred) { + deferred = {}; + deferred.promise = new Promise((resolve) => { + deferred.resolve = resolve; + }); + } + + return deferred.promise; + } + }; +}; + +export default eventIterator; diff --git a/test/index.spec.js b/test/index.spec.js new file mode 100644 index 0000000..187c44e --- /dev/null +++ b/test/index.spec.js @@ -0,0 +1,70 @@ +import {expect} from 'chai'; +import EventEmitter from 'events'; +import eventIterator from '../src/index'; + +class MockEmitter extends EventEmitter {} + +describe('eventIterator', () => { + it('throws error on invalid arguments', () => { + const mockEmitter = {on: () => true}; + + expect(() => eventIterator()).to.throw(Error); + expect(() => eventIterator('a')).to.throw(Error); + expect(() => eventIterator(true)).to.throw(Error); + expect(() => eventIterator({})).to.throw(Error); + expect(() => eventIterator(mockEmitter)).to.throw(Error); + expect(() => eventIterator(mockEmitter, '')).to.throw(Error); + expect(() => eventIterator(mockEmitter, 1)).to.throw(Error); + expect(() => eventIterator(mockEmitter, {})).to.throw(Error); + expect(() => eventIterator(mockEmitter, true)).to.throw(Error); + expect(() => eventIterator({}, 'name')).to.throw(Error); + expect(() => eventIterator(true, 'name')).to.throw(Error); + expect(() => eventIterator('a', 'name')).to.throw(Error); + }); + + it('throws error if Promise not available', () => { + const mockEmitter = {on: () => true}; + const oldPromise = Promise; + global.Promise = undefined; + + expect(() => eventIterator(mockEmitter, 'name')).to.throw(Error); + + global.Promise = oldPromise; + }); + + it('resolves promise when event emitted', (done) => { + const mockEmitter = new MockEmitter(); + const {nextEvent} = eventIterator(mockEmitter, 'someEvent'); + const promise = nextEvent(); + const payloads = [{a: 1}, {b: 2}]; + let resolved, resolvedSecond; + + resolved = false; + resolvedSecond = false; + + expect(promise).to.be.a('Promise'); + + promise.then((firstValue) => { + resolved = true; + expect(firstValue).to.be.eql(payloads[0]); + + const nextPromise = nextEvent(); + + nextPromise.then((secondValue) => { + resolvedSecond = true; + expect(secondValue).to.be.eql(payloads[1]); + done(); + }); + + expect(resolvedSecond).to.be.eql(false); + setTimeout(() => { + mockEmitter.emit('someEvent', payloads[1]); + }, 50); + }); + + expect(resolved).to.be.eql(false); + setTimeout(() => { + mockEmitter.emit('someEvent', payloads[0]); + }, 50); + }); +});