├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | test.js 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '9' 5 | - '8' 6 | - '6' 7 | - '4' 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Thomas Watson Steen 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # secret-event-listener 2 | 3 | Add an event listener without causing any side effects. 4 | 5 | [![npm](https://nodei.co/npm/secret-event-listener.png)](https://www.npmjs.com/package/secret-event-listener) 6 | 7 | [![Build status](https://travis-ci.org/watson/secret-event-listener.svg?branch=master)](https://travis-ci.org/watson/secret-event-listener) 8 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://github.com/feross/standard) 9 | 10 | ## Why? 11 | 12 | Adding an event listener to an emitter object will normally trigger 13 | certain side effects. E.g. the 14 | [`newListener`](https://nodejs.org/api/events.html#events_event_newlistener) 15 | event will be emitted for every new listener added. A 16 | `MaxListenersExceededWarning` process warning will be emitted if the 17 | emitter reaches is [maximum listener 18 | count](https://nodejs.org/api/events.html#events_emitter_setmaxlisteners_n). 19 | Or a readable stream will enter [flowing 20 | mode](https://nodejs.org/api/stream.html#stream_two_modes) if a `data` 21 | listener is added. 22 | 23 | This module gives you the ability to attach a listener without 24 | triggering any of these side effects. 25 | 26 | **Only use this module if you are sure you don't want any side effects 27 | and there's other way to achieve your goals!** 28 | 29 | ## Installation 30 | 31 | ``` 32 | npm install secret-event-listener --save 33 | ``` 34 | 35 | ## Usage 36 | 37 | ```js 38 | const {EventEmitter} = require('events') 39 | const addSecretListener = require('secret-event-listener') 40 | 41 | const emitter = new EventEmitter() 42 | 43 | emitter.on('newListener', function () { 44 | throw new Error('should not fire the newListener event!') 45 | }) 46 | 47 | addSecretListener(emitter, 'foo', function () { 48 | console.log('foo event fired :)') 49 | }) 50 | 51 | emitter.emit('foo') 52 | ``` 53 | 54 | ## API 55 | 56 | ### `addSecretListener(emitter, eventName, listener)` 57 | 58 | Arguments: 59 | 60 | - `emitter` - an object inheriting from 61 | [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter) 62 | - `eventName` - the event to add the `listener` to 63 | - `listener` - a listener function which should be called every time an 64 | event of `eventName` is emitted. 65 | 66 | ### `addSecretListener.prepend(emitter, eventName, listener)` 67 | 68 | Like the main `addSecretListener` function, except the `listener` is 69 | added to the beginning of the listeners array for the event named 70 | `eventName`. 71 | 72 | ## License 73 | 74 | [MIT](https://github.com/watson/secret-event-listener/blob/master/LICENSE) 75 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = addSecretListener 4 | 5 | addSecretListener.prepend = function (target, type, listener) { 6 | addSecretListener(target, type, listener, true) 7 | } 8 | 9 | function addSecretListener (target, type, listener, prepend) { 10 | let events = target._events 11 | 12 | if (events === undefined) { 13 | events = target._events = Object.create(null) 14 | target._eventsCount = 0 15 | } 16 | 17 | const existing = events[type] 18 | 19 | if (existing === undefined) { 20 | events[type] = listener 21 | target._eventsCount++ 22 | } else { 23 | if (typeof existing === 'function') { 24 | events[type] = prepend ? [listener, existing] : [existing, listener] 25 | } else if (prepend) { 26 | existing.unshift(listener) 27 | } else { 28 | existing.push(listener) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secret-event-listener", 3 | "version": "1.0.0", 4 | "description": "Add an event listener without causing any side effects", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "standard": "^11.0.1", 9 | "tape": "^4.9.1" 10 | }, 11 | "scripts": { 12 | "test": "standard && node test.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/watson/secret-event-listener.git" 17 | }, 18 | "keywords": [ 19 | "eavesdrop", 20 | "EventEmitter", 21 | "listen", 22 | "listener", 23 | "event", 24 | "events", 25 | "spy", 26 | "side-effect", 27 | "inject" 28 | ], 29 | "author": "Thomas Watson (https://twitter.com/wa7son)", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/watson/secret-event-listener/issues" 33 | }, 34 | "homepage": "https://github.com/watson/secret-event-listener#readme", 35 | "coordinates": [ 36 | 55.777603, 37 | 12.590177 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const util = require('util') 4 | const EventEmitter = require('events') 5 | const PassThrough = require('stream').PassThrough 6 | const test = require('tape') 7 | const addSecretListener = require('./') 8 | 9 | test('add listener normally should have side effects', function (t) { 10 | t.plan(3) 11 | const emitter = new EventEmitter() 12 | emitter.on('newListener', function (type, listener) { 13 | t.equal(type, 'foo') 14 | t.equal(listener, onFoo) 15 | }) 16 | emitter.on('foo', onFoo) 17 | emitter.emit('foo', 'baz') 18 | function onFoo (bar) { 19 | t.equal(bar, 'baz') 20 | t.end() 21 | } 22 | }) 23 | 24 | test('inject listener', function (t) { 25 | const emitter = new EventEmitter() 26 | addSecretListener(emitter, 'foo', function (bar) { 27 | t.equal(bar, 'baz') 28 | t.end() 29 | }) 30 | emitter.emit('foo', 'baz') 31 | }) 32 | 33 | test('inject listener - no super constructor', function (t) { 34 | function MyEmitter () {} 35 | util.inherits(MyEmitter, EventEmitter) 36 | const emitter = new MyEmitter() 37 | addSecretListener(emitter, 'foo', function (bar) { 38 | t.equal(bar, 'baz') 39 | t.end() 40 | }) 41 | emitter.emit('foo', 'baz') 42 | }) 43 | 44 | test('inject listener - no EventEmitter side effects', function (t) { 45 | const emitter = new EventEmitter() 46 | emitter.on('newListener', fail) 47 | addSecretListener(emitter, 'foo', function (bar) { 48 | t.equal(bar, 'baz') 49 | t.end() 50 | }) 51 | emitter.emit('foo', 'baz') 52 | }) 53 | 54 | test('inject listener - no stream side effects', function (t) { 55 | t.plan(3) 56 | let waited = false 57 | const stream = new PassThrough() 58 | stream.write('hello world') 59 | addSecretListener(stream, 'data', function (chunk) { 60 | t.equal(waited, true) 61 | t.equal(chunk.toString(), 'hello world') 62 | }) 63 | setTimeout(function () { 64 | waited = true 65 | stream.on('data', function (chunk) { 66 | t.equal(chunk.toString(), 'hello world') 67 | }) 68 | }, 100) 69 | }) 70 | 71 | test('inject listener to the end', function (t) { 72 | t.plan(2) 73 | const emitter = new EventEmitter() 74 | emitter.on('foo', function (bar) { 75 | t.equal(bar, 'baz') 76 | }) 77 | addSecretListener(emitter, 'foo', function (bar) { 78 | t.equal(bar, 'baz') 79 | t.end() 80 | }) 81 | emitter.emit('foo', 'baz') 82 | }) 83 | 84 | test('inject listener to the start', function (t) { 85 | t.plan(2) 86 | const emitter = new EventEmitter() 87 | emitter.on('foo', function (bar) { 88 | t.equal(bar, 'baz') 89 | t.end() 90 | }) 91 | addSecretListener(emitter, 'foo', function (bar) { 92 | t.equal(bar, 'baz') 93 | }, true) 94 | emitter.emit('foo', 'baz') 95 | }) 96 | 97 | test('remove only listener from unused emitter using removeAllListeners()', function (t) { 98 | const emitter = new EventEmitter() 99 | addSecretListener(emitter, 'foo', fail) 100 | emitter.removeAllListeners() 101 | emitter.emit('foo') 102 | t.end() 103 | }) 104 | 105 | test('remove only listener from unused emitter using removeAllListeners(type)', function (t) { 106 | const emitter = new EventEmitter() 107 | addSecretListener(emitter, 'foo', fail) 108 | emitter.removeAllListeners('foo') 109 | emitter.emit('foo') 110 | t.end() 111 | }) 112 | 113 | test('remove only listener from unused emitter using removeListener(type, listener)', function (t) { 114 | const emitter = new EventEmitter() 115 | addSecretListener(emitter, 'foo', fail) 116 | emitter.removeListener('foo', fail) 117 | emitter.emit('foo') 118 | t.end() 119 | }) 120 | 121 | test('remove only listener from emitter that previously had other listeners', function (t) { 122 | const emitter = new EventEmitter() 123 | emitter.on('foo', function () {}) 124 | emitter.removeAllListeners() 125 | addSecretListener(emitter, 'foo', fail) 126 | emitter.removeListener('foo', fail) 127 | emitter.emit('foo') 128 | t.end() 129 | }) 130 | 131 | test('remove listeners number 2', function (t) { 132 | t.plan(2) 133 | const emitter = new EventEmitter() 134 | emitter.on('foo', function () {}) 135 | addSecretListener(emitter, 'foo', fail) 136 | emitter.on('removeListener', function (type, fn) { 137 | t.equal(type, 'foo') 138 | t.equal(fn, fail) 139 | }) 140 | emitter.removeListener('foo', fail) 141 | emitter.emit('foo') 142 | }) 143 | 144 | test('remove listeners number 3', function (t) { 145 | t.plan(2) 146 | const emitter = new EventEmitter() 147 | emitter.on('foo', function () {}) 148 | emitter.on('foo', function () {}) 149 | addSecretListener(emitter, 'foo', fail) 150 | emitter.on('removeListener', function (type, fn) { 151 | t.equal(type, 'foo') 152 | t.equal(fn, fail) 153 | }) 154 | emitter.removeListener('foo', fail) 155 | emitter.emit('foo') 156 | }) 157 | 158 | function fail () { 159 | throw new Error('should not fire listener') 160 | } 161 | --------------------------------------------------------------------------------