├── .editorconfig ├── .gitattributes ├── .gitignore ├── .prettierrc.js ├── .travis.yml ├── CHANGELOG.md ├── License ├── README.md ├── __tests__ ├── fixture │ └── .keep ├── helper.ts └── main.test.ts ├── package-lock.json ├── package.json ├── src └── index.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | /node_modules/ 4 | dist/ 5 | public/ 6 | **/*debug.log 7 | coverage/ 8 | .entry 9 | .dist 10 | .vscode 11 | /lib/ 12 | /types/ 13 | /es/ 14 | 15 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | useTabs: false, // Indent lines with tabs instead of spaces. 3 | printWidth: 120, // Specify the length of line that the printer will wrap on. 4 | tabWidth: 2, // Specify the number of spaces per indentation-level. 5 | singleQuote: true, // Use single quotes instead of double quotes. 6 | trailingComma: 'none', 7 | semi: false, 8 | jsxBracketSameLine: false 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 10 5 | - 12 6 | - 14 7 | 8 | cache: 9 | bundle: true 10 | directories: 11 | - node_modules 12 | 13 | script: 14 | - npm install codecov nyc --no-save 15 | - npm test -- --coverage 16 | after_script: 17 | - npx codecov --token=$CODECOV_TOKEN 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.2](https://github.com/imcuttle/node-await-event-emitter/compare/v2.0.1...v2.0.2) (2020-11-24) 2 | 3 | ## [2.0.1](https://github.com/imcuttle/node-await-event-emitter/compare/v2.0.0...v2.0.1) (2020-11-03) 4 | 5 | # 2.0.0 (2020-11-03) 6 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) imcuttle 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # await-event-emitter 2 | 3 | **Note: node-await-event-emitter is just implements the series processing, If you need parallel case, Please use the package [tapable](https://www.npmjs.com/package/tapable) which is used by webpack.** 4 | 5 | Await events library like EventEmitter 6 | 7 | [![build status](https://img.shields.io/travis/imcuttle/node-await-event-emitter/master.svg?style=flat-square)](https://travis-ci.org/imcuttle/node-await-event-emitter) 8 | [![Test coverage](https://img.shields.io/codecov/c/github/imcuttle/node-await-event-emitter.svg?style=flat-square)](https://codecov.io/github/imcuttle/node-await-event-emitter?branch=master) 9 | [![NPM version](https://img.shields.io/npm/v/await-event-emitter.svg?style=flat-square)](https://www.npmjs.com/package/await-event-emitter) 10 | [![NPM Downloads](https://img.shields.io/npm/dm/await-event-emitter.svg?style=flat-square&maxAge=43200)](https://www.npmjs.com/package/await-event-emitter) 11 | 12 | ## Why? 13 | 14 | The concept of Webpack plugin has lots of lifecycle hooks, they implement this via EventEmitter. 15 | In the primitive [events](https://nodejs.org/dist/latest/docs/api/events.html) module on nodejs, the usage as follows 16 | 17 | ```javascript 18 | const EventEmitter = require('events') 19 | const emitter = new EventEmitter() 20 | 21 | emitter 22 | .on('event', () => { 23 | // do something *synchronously* 24 | }) 25 | .emit('event', '...arguments') 26 | ``` 27 | 28 | The listener must be **synchronous**, that is way i wrote it. 29 | And await-event-emitter support synchronous emitter magically :smile: 30 | 31 | ## Installation 32 | 33 | ```bash 34 | npm install --save await-event-emitter 35 | ``` 36 | 37 | ## Usage 38 | 39 | ```javascript 40 | const AwaitEventEmitter = require('await-event-emitter').default 41 | 42 | const emitter = new AwaitEventEmitter() 43 | const tick = () => 44 | new Promise((resolve) => { 45 | setTimeout(() => { 46 | console.log('tick') 47 | resolve() 48 | }, 1000) 49 | }) 50 | 51 | emitter.on('event', async () => { 52 | // wait to print 53 | await tick() 54 | }) 55 | 56 | async function run() { 57 | // NOTE: it's important to `await` the reset process 58 | await emitter.emit('event', '...arguments') 59 | await emitter.emit('event', 'again') 60 | 61 | // support emit it synchronously 62 | emitter.emitSync('event', 'again') 63 | } 64 | 65 | run() 66 | ``` 67 | 68 | ## API 69 | 70 | ### Class `AwaitEventEmitter` 71 | 72 | - `addListener(event, listener)` : AwaitEventEmitter 73 | alias: `on` 74 | - `once(event, listener)` 75 | - `prependListener(event, listener)` : AwaitEventEmitter 76 | alias: `prepend` 77 | - `prependOnceListener(event, listener)` : AwaitEventEmitter 78 | alias: `prependOnce` 79 | - `removeListener(event, listener)` : AwaitEventEmitter 80 | alias: `off` 81 | - `listeners(event)` : [] 82 | - `emit(event, ...args)` : Promise.resolve(boolean) 83 | emit listeners asynchronously, we recommended await it resolved the result 84 | - `emitSync(event, ...args)` : boolean 85 | emit listeners synchronously 86 | 87 | ## Test 88 | 89 | ```bash 90 | npm test 91 | ``` 92 | 93 | ## Contributing 94 | 95 | - Fork it! 96 | - Create your new branch: 97 | `git checkout -b feature-new` or `git checkout -b fix-which-bug` 98 | - Start your magic work now 99 | - Make sure npm test passes 100 | - Commit your changes: 101 | `git commit -am 'feat: some description (close #123)'` or `git commit -am 'fix: some description (fix #123)'` 102 | - Push to the branch: `git push` 103 | - Submit a pull request :) 104 | 105 | ## Authors 106 | 107 | This library is written and maintained by imcuttle, imcuttle@163.com. 108 | 109 | ## License 110 | 111 | MIT - [imcuttle](https://github.com/imcuttle) 🐟 112 | -------------------------------------------------------------------------------- /__tests__/fixture/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imcuttle/node-await-event-emitter/b96233d732093cd7357f84e6ac90e9629db83ac5/__tests__/fixture/.keep -------------------------------------------------------------------------------- /__tests__/helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file helper 3 | */ 4 | 5 | import * as nps from 'path' 6 | 7 | function fixture(...argv: string[]) { 8 | return nps.join.apply(nps, [__dirname, 'fixture'].concat(argv)) 9 | } 10 | 11 | export { fixture } 12 | -------------------------------------------------------------------------------- /__tests__/main.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file: await-event-emitter.test.js 3 | * @author: Cuttle Cong 4 | * @date: 2018/1/21 5 | * @description: 6 | */ 7 | import AwaitEventEmitter from '../src' 8 | 9 | function tick(func) { 10 | return new Promise((resolve) => { 11 | setTimeout(() => { 12 | func() 13 | resolve() 14 | }, 1000) 15 | }) 16 | } 17 | 18 | describe('await-event-emitter', () => { 19 | it('on', () => { 20 | const emitter = new AwaitEventEmitter() 21 | let flag = 1 22 | const listener = async (a, b) => { 23 | flag = b 24 | } 25 | emitter.on('event', listener) 26 | emitter.emit('event', 2, 4) 27 | emitter.emit('event', 2, 6) 28 | 29 | expect(flag).toEqual(6) 30 | expect(emitter._events['event'][0].fn).toEqual(listener) 31 | }) 32 | 33 | it('once', () => { 34 | const emitter = new AwaitEventEmitter() 35 | let flag = 1 36 | const listener = (a, b) => { 37 | flag = b 38 | } 39 | emitter.once('event', listener) 40 | emitter.emit('event', 2, 4) 41 | emitter.emit('event', 2, 6) 42 | 43 | expect(flag).toEqual(4) 44 | expect(emitter.listeners('event')).toEqual([]) 45 | }) 46 | 47 | it('prependListener', () => { 48 | const emitter = new AwaitEventEmitter() 49 | let flag = 1 50 | const listener = (a, b) => { 51 | flag = b 52 | } 53 | emitter.addListener('event', (a, b) => { 54 | flag = b + 1 55 | }) 56 | emitter.prependListener('event', listener) 57 | emitter.emit('event', 2, 4) 58 | emitter.emit('event', 2, 6) 59 | 60 | expect(flag).toEqual(7) 61 | expect(emitter.listeners('event').length).toEqual(2) 62 | expect(emitter.listeners('event')[0]).toEqual(listener) 63 | expect(emitter.listeners('event')[1]).not.toEqual(listener) 64 | }) 65 | 66 | it('sync!', () => { 67 | const emitter = new AwaitEventEmitter() 68 | let flag = 1 69 | const listener = async (a, b) => { 70 | flag = b 71 | } 72 | emitter.addListener('event', async (a, b) => { 73 | flag = b + 1 74 | }) 75 | emitter.prependListener('event', listener) 76 | emitter.emitSync('event', 2, 4) 77 | emitter.emitSync('event', 2, 6) 78 | emitter.emitSync('event', 2, 9) 79 | 80 | expect(flag).toEqual(10) 81 | expect(emitter.listeners('event').length).toEqual(2) 82 | expect(emitter.listeners('event')[0]).toEqual(listener) 83 | expect(emitter.listeners('event')[1]).not.toEqual(listener) 84 | }) 85 | 86 | it('removeListener', () => { 87 | const emitter = new AwaitEventEmitter() 88 | let flag = 1 89 | const listener = (a, b) => { 90 | flag = b 91 | } 92 | emitter 93 | .addListener('event', listener) 94 | .prependListener('event', listener) 95 | .prependOnceListener('event', listener) 96 | .removeListener('event', listener) 97 | expect(emitter.listeners('event').length).toEqual(0) 98 | 99 | const listenerA = () => {} 100 | emitter.addListener('event', listener).prependListener('event', listenerA).removeListener('event', listener) 101 | expect(emitter.listeners('event').length).toEqual(1) 102 | expect(emitter.listeners('event')[0]).toEqual(listenerA) 103 | }) 104 | 105 | it('prependOnceListener', () => { 106 | const emitter = new AwaitEventEmitter() 107 | let flag = 1 108 | const listener = (a, b) => { 109 | flag = b 110 | } 111 | emitter.prependOnceListener('event', () => (flag = 5)).prependListener('event', listener) 112 | 113 | emitter.emit('event', 2, 1) 114 | expect(flag).toEqual(5) 115 | expect(emitter.listeners('event').length).toEqual(1) 116 | }) 117 | 118 | it('sync', () => { 119 | const emitter = new AwaitEventEmitter() 120 | let flag = 1 121 | emitter.on('event', (a, b) => { 122 | flag = b 123 | }) 124 | emitter.on('event', (a, b) => { 125 | flag = b + 1 126 | }) 127 | emitter.emit('event', 2, 4) 128 | expect(flag).toEqual(5) 129 | 130 | emitter.on('event-a', (a, b) => { 131 | flag = b 132 | }) 133 | emitter.on('event-a', (a, b) => { 134 | tick(() => (flag = b + 1)) 135 | }) 136 | emitter.emit('event-a', 2, 4) 137 | expect(flag).toEqual(4) 138 | }) 139 | 140 | it('async', async () => { 141 | const emitter = new AwaitEventEmitter() 142 | let flag = 1 143 | emitter.on('event', (a, b) => { 144 | flag = b 145 | }) 146 | emitter.on('event', (a, b) => { 147 | return tick(() => (flag = b + 1)) 148 | }) 149 | await emitter.emit('event', 2, 4) 150 | expect(flag).toEqual(5) 151 | 152 | emitter.removeListener('event') 153 | emitter.on('event', (a, b) => { 154 | flag = b 155 | }) 156 | emitter.on('event', async (a, b) => { 157 | return tick(() => (flag = b + 1)) 158 | }) 159 | await emitter.emit('event', 2, 4) 160 | expect(flag).toEqual(5) 161 | 162 | emitter.removeListener('event') 163 | emitter.on('event', (a, b) => { 164 | flag = b 165 | }) 166 | emitter.on('event', async (a, b) => { 167 | await tick(() => (flag = b + 1)) 168 | }) 169 | await emitter.emit('event', 2, 4) 170 | expect(flag).toEqual(5) 171 | 172 | emitter.removeListener('event') 173 | emitter.on('event', (a, b) => { 174 | flag = b 175 | }) 176 | emitter.on('event', async (a, b) => { 177 | tick(() => (flag = b + 1)) 178 | }) 179 | await emitter.emit('event', 2, 4) 180 | expect(flag).toEqual(4) 181 | }) 182 | 183 | it('once remove', async function () { 184 | const emitter = new AwaitEventEmitter() 185 | 186 | emitter.once('aa', function () {}) 187 | emitter.once('aa', function () {}) 188 | emitter.once('aa', function () {}) 189 | await emitter.emit('aa') 190 | }) 191 | }) 192 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "await-event-emitter", 3 | "version": "2.0.2", 4 | "description": "Await events library like EventEmitter", 5 | "author": "imcuttle ", 6 | "scripts": { 7 | "test": "npx jest", 8 | "test:watch": "npm test -- --watch", 9 | "preversion": "npm test", 10 | "build": "npm run clean && run-p --print-label \"build:**\"", 11 | "dev": "TSC_OPTIONS=\"--watch\" npm run build", 12 | "build:es": "tsc $TSC_OPTIONS --outDir es --module es6", 13 | "build:cjs": "tsc $TSC_OPTIONS --outDir lib", 14 | "build:tds": "tsc $TSC_OPTIONS --emitDeclarationOnly -d", 15 | "clean": "rimraf types es lib", 16 | "prepare": "npm run build", 17 | "version": "npm run changelog", 18 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md" 19 | }, 20 | "husky": { 21 | "hooks": { 22 | "pre-commit": "pretty-quick --staged", 23 | "commit-msg": "commitlint -e $HUSKY_GIT_PARAMS" 24 | } 25 | }, 26 | "sideEffects": false, 27 | "engines": { 28 | "node": ">=10" 29 | }, 30 | "files": [ 31 | "es", 32 | "types", 33 | "lib", 34 | "src" 35 | ], 36 | "keywords": [ 37 | "imcuttle", 38 | "node", 39 | "await", 40 | "event", 41 | "emitter", 42 | "node-await-event-emitter" 43 | ], 44 | "main": "lib", 45 | "types": "types", 46 | "license": "MIT", 47 | "repository": "imcuttle/node-await-event-emitter", 48 | "module": "es", 49 | "jest": { 50 | "transform": { 51 | "^.+\\.tsx?$": "ts-jest", 52 | "^.+\\.jsx?$": "babel-jest" 53 | }, 54 | "moduleFileExtensions": [ 55 | "ts", 56 | "tsx", 57 | "js", 58 | "jsx", 59 | "json", 60 | "node" 61 | ], 62 | "testMatch": [ 63 | "**/__test{s,}__/*.(spec|test).{t,j}s{x,}" 64 | ] 65 | }, 66 | "commitlint": { 67 | "extends": [ 68 | "@commitlint/config-conventional" 69 | ] 70 | }, 71 | "devDependencies": { 72 | "@commitlint/cli": "^11.0.0", 73 | "@commitlint/config-conventional": "^11.0.0", 74 | "@types/jest": "^26.0.15", 75 | "conventional-changelog-cli": "^2.1.0", 76 | "husky": "^4.3.0", 77 | "jest": "^26.6.2", 78 | "npm-run-all": "^4.1.5", 79 | "prettier": "^2.1.2", 80 | "pretty-quick": "^3.1.0", 81 | "rimraf": "^3.0.2", 82 | "ts-jest": "^26.4.3", 83 | "typescript": "^4.0.5" 84 | }, 85 | "dependencies": { 86 | "is-promise": "^4.0.0" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Await events library like EventEmitter 3 | * @author imcuttle 4 | */ 5 | 6 | const isPromise = require('is-promise') 7 | const TYPE_KEY_NAME = 8 | typeof Symbol === 'function' ? Symbol.for('--[[await-event-emitter]]--') : '--[[await-event-emitter]]--' 9 | 10 | function assertType(type) { 11 | if (typeof type !== 'string' && typeof type !== 'symbol') { 12 | throw new TypeError('type is not type of string or symbol!') 13 | } 14 | } 15 | 16 | type SymbolKey = string | any 17 | 18 | function assertFn(fn) { 19 | if (typeof fn !== 'function') { 20 | throw new TypeError('fn is not type of Function!') 21 | } 22 | } 23 | 24 | function alwaysListener(fn) { 25 | return { 26 | [TYPE_KEY_NAME]: 'always', 27 | fn 28 | } 29 | } 30 | 31 | function onceListener(fn) { 32 | return { 33 | [TYPE_KEY_NAME]: 'once', 34 | fn 35 | } 36 | } 37 | 38 | class AwaitEventEmitter { 39 | _events: Record> = {} 40 | 41 | addListener(type: SymbolKey, fn: Function) { 42 | return this.on(type, fn) 43 | } 44 | on(type: SymbolKey, fn: Function) { 45 | assertType(type) 46 | assertFn(fn) 47 | this._events[type] = this._events[type] || [] 48 | this._events[type].push(alwaysListener(fn)) 49 | return this 50 | } 51 | prependListener(type: SymbolKey, fn: Function) { 52 | return this.prepend(type, fn) 53 | } 54 | prepend(type: SymbolKey, fn: Function) { 55 | assertType(type) 56 | assertFn(fn) 57 | this._events[type] = this._events[type] || [] 58 | this._events[type].unshift(alwaysListener(fn)) 59 | return this 60 | } 61 | 62 | prependOnceListener(type: SymbolKey, fn: Function) { 63 | return this.prependOnce(type, fn) 64 | } 65 | prependOnce(type: SymbolKey, fn: Function) { 66 | assertType(type) 67 | assertFn(fn) 68 | this._events[type] = this._events[type] || [] 69 | this._events[type].unshift(onceListener(fn)) 70 | return this 71 | } 72 | listeners(type: SymbolKey) { 73 | return (this._events[type] || []).map((x) => x.fn) 74 | } 75 | 76 | once(type: SymbolKey, fn: Function) { 77 | assertType(type) 78 | assertFn(fn) 79 | this._events[type] = this._events[type] || [] 80 | this._events[type].push(onceListener(fn)) 81 | return this 82 | } 83 | 84 | removeAllListeners() { 85 | this._events = {} 86 | } 87 | 88 | off(type: SymbolKey, nullOrFn?: Function) { 89 | return this.removeListener(type, nullOrFn) 90 | } 91 | removeListener(type: SymbolKey, nullOrFn?: Function) { 92 | assertType(type) 93 | 94 | const listeners = this.listeners(type) 95 | if (typeof nullOrFn === 'function') { 96 | let index = -1 97 | let found = false 98 | 99 | while ((index = listeners.indexOf(nullOrFn)) >= 0) { 100 | listeners.splice(index, 1) 101 | this._events[type].splice(index, 1) 102 | found = true 103 | } 104 | return found 105 | } else { 106 | return delete this._events[type] 107 | } 108 | } 109 | 110 | async emit(type: SymbolKey, ...args: unknown[]) { 111 | assertType(type) 112 | const listeners = this.listeners(type) 113 | 114 | const onceListeners = [] 115 | if (listeners && listeners.length) { 116 | for (let i = 0; i < listeners.length; i++) { 117 | const event = listeners[i] 118 | const rlt = event.apply(this, args) 119 | if (isPromise(rlt)) { 120 | await rlt 121 | } 122 | if (this._events[type] && this._events[type][i] && this._events[type][i][TYPE_KEY_NAME] === 'once') { 123 | onceListeners.push(event) 124 | } 125 | } 126 | onceListeners.forEach((event) => this.removeListener(type, event)) 127 | 128 | return true 129 | } 130 | return false 131 | } 132 | 133 | emitSync(type: SymbolKey, ...args: unknown[]) { 134 | assertType(type) 135 | const listeners = this.listeners(type) 136 | const onceListeners = [] 137 | if (listeners && listeners.length) { 138 | for (let i = 0; i < listeners.length; i++) { 139 | const event = listeners[i] 140 | event.apply(this, args) 141 | 142 | if (this._events[type] && this._events[type][i] && this._events[type][i][TYPE_KEY_NAME] === 'once') { 143 | onceListeners.push(event) 144 | } 145 | } 146 | onceListeners.forEach((event) => this.removeListener(type, event)) 147 | 148 | return true 149 | } 150 | return false 151 | } 152 | } 153 | 154 | export default AwaitEventEmitter 155 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "sourceMap": false, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "esModuleInterop": false, 9 | "outDir": "types" 10 | }, 11 | "include": ["./src"], 12 | "exclude": ["node_modules", "test", "__tests__"] 13 | } 14 | --------------------------------------------------------------------------------