├── .github ├── no-response.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── babel.config.js ├── lib ├── composite-disposable.js ├── disposable.js ├── emitter.js └── event-kit.js ├── package.json └── spec ├── composite-disposable-spec.js ├── disposable-spec.js └── emitter-spec.js /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an issue is closed for lack of response 4 | daysUntilClose: 28 5 | 6 | # Label requiring a response 7 | responseRequiredLabel: more-information-needed 8 | 9 | # Comment to post when closing an issue for lack of response. Set to `false` to disable. 10 | closeComment: > 11 | This issue has been automatically closed because there has been no response 12 | to our request for more information from the original author. With only the 13 | information that is currently in the issue, we don't have enough information 14 | to take action. Please reach out if you have or find the answers we need so 15 | that we can investigate further. 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | Build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version: 14 13 | - name: Install dependencies 14 | run: npm install 15 | - name: Test 16 | run: npm test 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | npm-debug.log 4 | *.swp 5 | .coffee 6 | api.json 7 | dist 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | spec 2 | script 3 | src 4 | .npmignore 5 | .DS_Store 6 | npm-debug.log 7 | .travis.yml 8 | .pairs 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) 2 | # event-kit 3 | [![CI](https://github.com/atom/event-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/atom/event-kit/actions/workflows/ci.yml) 4 | 5 | This is a simple library for implementing event subscription APIs. 6 | 7 | ## Implementing Event Subscription APIs 8 | 9 | ```js 10 | const {Emitter} = require('event-kit') 11 | 12 | class User { 13 | constructor() { 14 | this.emitter = new Emitter() 15 | } 16 | 17 | onDidChangeName(callback) { 18 | this.emitter.on('did-change-name', callback) 19 | } 20 | 21 | setName(name) { 22 | if (name !== this.name) { 23 | this.name = name 24 | this.emitter.emit('did-change-name', name) 25 | } 26 | 27 | return this.name 28 | } 29 | 30 | destroy() { 31 | this.emitter.dispose() 32 | } 33 | } 34 | ``` 35 | 36 | In the example above, we implement `::onDidChangeName` on the user object, which 37 | will register callbacks to be invoked whenever the user's name changes. To do 38 | so, we make use of an internal `Emitter` instance. We use `::on` to subscribe 39 | the given callback in `::onDidChangeName`, and `::emit` in `::setName` to notify 40 | subscribers. Finally, when the `User` instance is destroyed we call `::dispose` 41 | on the emitter to unsubscribe all subscribers. 42 | 43 | ## Consuming Event Subscription APIs 44 | 45 | `Emitter::on` returns a `Disposable` instance, which has a `::dispose` method. 46 | To unsubscribe, simply call dispose on the returned object. 47 | 48 | ```js 49 | const subscription = user.onDidChangeName((name) => console.log(`My name is ${name}`)) 50 | // Later, to unsubscribe... 51 | subscription.dispose() 52 | ``` 53 | 54 | You can also use `CompositeDisposable` to combine disposable instances together. 55 | 56 | ```js 57 | const {CompositeDisposable} = require('event-kit') 58 | 59 | const subscriptions = new CompositeDisposable() 60 | subscriptions.add(user1.onDidChangeName((name) => console.log(`User 1: ${name}`)) 61 | subscriptions.add(user2.onDidChangeName((name) => console.log(`User 2: ${name}`)) 62 | 63 | // Later, to unsubscribe from *both*... 64 | subscriptions.dispose() 65 | ``` 66 | 67 | ## Creating Your Own Disposables 68 | 69 | Disposables are convenient ways to represent a resource you will no longer 70 | need at some point. You can instantiate a disposable with an action to take when 71 | no longer needed. 72 | 73 | ```js 74 | const {Disposable} = require('event-kit') 75 | 76 | const disposable = new Disposable(() => this.destroyResource()) 77 | ``` 78 | 79 | ### Using ES6 Code 80 | You can use the ES6 style classes from `lib` directory. 81 | ``` 82 | const {Disposable} = require('event-kit/lib/event-kit') 83 | ``` 84 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | let presets = ["babel-preset-atomic"]; 2 | 3 | let plugins = ["@babel/plugin-transform-classes"] // this is needed so Disposabale can be extended by ES5-style classes 4 | 5 | module.exports = { 6 | presets: presets, 7 | plugins: plugins, 8 | exclude: "node_modules/**", 9 | sourceMap: true, 10 | } 11 | -------------------------------------------------------------------------------- /lib/composite-disposable.js: -------------------------------------------------------------------------------- 1 | let CompositeDisposable 2 | let Disposable 3 | 4 | // Essential: An object that aggregates multiple {Disposable} instances together 5 | // into a single disposable, so they can all be disposed as a group. 6 | // 7 | // These are very useful when subscribing to multiple events. 8 | // 9 | // ## Examples 10 | // 11 | // ```js 12 | // const {CompositeDisposable} = require('atom') 13 | // 14 | // class Something { 15 | // constructor() { 16 | // this.disposables = new CompositeDisposable() 17 | // const editor = atom.workspace.getActiveTextEditor() 18 | // this.disposables.add(editor.onDidChange(() => {}) 19 | // this.disposables.add(editor.onDidChangePath(() => {}) 20 | // } 21 | // 22 | // destroy() { 23 | // this.disposables.dispose(); 24 | // } 25 | // } 26 | // ``` 27 | module.exports = class CompositeDisposable { 28 | /* 29 | Section: Construction and Destruction 30 | */ 31 | 32 | // Public: Construct an instance, optionally with one or more disposables 33 | constructor() { 34 | this.disposed = false 35 | this.disposables = new Set() 36 | for (let disposable of arguments) { 37 | this.add(disposable) 38 | } 39 | } 40 | 41 | // Public: Dispose all disposables added to this composite disposable. 42 | // 43 | // If this object has already been disposed, this method has no effect. 44 | dispose() { 45 | if (!this.disposed) { 46 | this.disposed = true 47 | this.disposables.forEach(disposable => disposable.dispose()) 48 | this.disposables = null 49 | } 50 | } 51 | 52 | /* 53 | Section: Managing Disposables 54 | */ 55 | 56 | // Public: Add disposables to be disposed when the composite is disposed. 57 | // 58 | // If this object has already been disposed, this method has no effect. 59 | // 60 | // * `...disposables` {Disposable} instances or any objects with `.dispose()` 61 | // methods. 62 | add() { 63 | if (!this.disposed) { 64 | for (const disposable of arguments) { 65 | assertDisposable(disposable) 66 | this.disposables.add(disposable) 67 | } 68 | } 69 | } 70 | 71 | // Public: Remove a previously added disposable. 72 | // 73 | // * `disposable` {Disposable} instance or any object with a `.dispose()` 74 | // method. 75 | remove(disposable) { 76 | if (!this.disposed) { 77 | this.disposables.delete(disposable) 78 | } 79 | } 80 | 81 | // Public: Alias to {CompositeDisposable::remove} 82 | delete(disposable) { 83 | this.remove(disposable) 84 | } 85 | 86 | // Public: Clear all disposables. They will not be disposed by the next call 87 | // to dispose. 88 | clear() { 89 | if (!this.disposed) { 90 | this.disposables.clear() 91 | } 92 | } 93 | } 94 | 95 | function assertDisposable(disposable) { 96 | if (Disposable == null) { 97 | Disposable = require("./disposable") 98 | } 99 | 100 | if (!Disposable.isDisposable(disposable)) { 101 | throw new TypeError( 102 | "Arguments to CompositeDisposable.add must have a .dispose() method" 103 | ) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/disposable.js: -------------------------------------------------------------------------------- 1 | // Essential: A handle to a resource that can be disposed. For example, 2 | // {Emitter::on} returns disposables representing subscriptions. 3 | module.exports = class Disposable { 4 | // Public: Ensure that `object` correctly implements the `Disposable` 5 | // contract. 6 | // 7 | // * `object` An {Object} you want to perform the check against. 8 | // 9 | // Returns a {Boolean} indicating whether `object` is a valid `Disposable`. 10 | static isDisposable(object) { 11 | return typeof (object != null ? object.dispose : undefined) === "function" 12 | } 13 | 14 | /* 15 | Section: Construction and Destruction 16 | */ 17 | 18 | // Public: Construct a Disposable 19 | // 20 | // * `disposalAction` A {Function} to call when {::dispose} is called for the 21 | // first time. 22 | constructor(disposalAction) { 23 | this.disposed = false 24 | this.disposalAction = disposalAction 25 | } 26 | 27 | // Public: Perform the disposal action, indicating that the resource associated 28 | // with this disposable is no longer needed. 29 | // 30 | // You can call this method more than once, but the disposal action will only 31 | // be performed the first time. 32 | dispose() { 33 | if (!this.disposed) { 34 | this.disposed = true 35 | if (typeof this.disposalAction === "function") { 36 | this.disposalAction() 37 | } 38 | this.disposalAction = null 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/emitter.js: -------------------------------------------------------------------------------- 1 | const Disposable = require("./disposable") 2 | const CompositeDisposable = require("./composite-disposable") 3 | 4 | // Essential: Utility class to be used when implementing event-based APIs that 5 | // allows for handlers registered via `::on` to be invoked with calls to 6 | // `::emit`. Instances of this class are intended to be used internally by 7 | // classes that expose an event-based API. 8 | // 9 | // For example: 10 | // 11 | // ```js 12 | // class User { 13 | // constructor() { 14 | // this.emitter = new Emitter() 15 | // } 16 | // 17 | // onDidChangeName(callback) { 18 | // this.emitter.on('did-change-name', callback) 19 | // } 20 | // 21 | // setName(name) { 22 | // if (name !== this.name) { 23 | // this.name = name 24 | // this.emitter.emit('did-change-name', name) 25 | // } 26 | // 27 | // return this.name 28 | // } 29 | // } 30 | // ``` 31 | class Emitter { 32 | static onEventHandlerException(exceptionHandler) { 33 | if (this.exceptionHandlers.length === 0) { 34 | this.dispatch = this.exceptionHandlingDispatch 35 | } 36 | 37 | this.exceptionHandlers.push(exceptionHandler) 38 | 39 | return new Disposable(() => { 40 | this.exceptionHandlers.splice( 41 | this.exceptionHandlers.indexOf(exceptionHandler), 42 | 1 43 | ) 44 | if (this.exceptionHandlers.length === 0) { 45 | return (this.dispatch = this.simpleDispatch) 46 | } 47 | }) 48 | } 49 | 50 | static simpleDispatch(handler, value) { 51 | return handler(value) 52 | } 53 | 54 | static exceptionHandlingDispatch(handler, value) { 55 | try { 56 | return handler(value) 57 | } catch (exception) { 58 | return this.exceptionHandlers.map(exceptionHandler => 59 | exceptionHandler(exception) 60 | ) 61 | } 62 | } 63 | 64 | /* 65 | Section: Construction and Destruction 66 | */ 67 | 68 | // Public: Construct an emitter. 69 | // 70 | // ```js 71 | // this.emitter = new Emitter() 72 | // ``` 73 | constructor() { 74 | this.disposed = false; 75 | this.clear(); 76 | } 77 | 78 | // Public: Clear out any existing subscribers. 79 | clear() { 80 | if (this.subscriptions != null) { 81 | this.subscriptions.dispose() 82 | } 83 | this.subscriptions = new CompositeDisposable() 84 | this.handlersByEventName = {} 85 | } 86 | 87 | // Public: Unsubscribe all handlers. 88 | dispose() { 89 | this.subscriptions.dispose() 90 | this.handlersByEventName = null 91 | this.disposed = true 92 | } 93 | 94 | /* 95 | Section: Event Subscription 96 | */ 97 | 98 | // Public: Register the given handler function to be invoked whenever events by 99 | // the given name are emitted via {::emit}. 100 | // 101 | // * `eventName` {String} naming the event that you want to invoke the handler 102 | // when emitted. 103 | // * `handler` {Function} to invoke when {::emit} is called with the given 104 | // event name. 105 | // 106 | // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 107 | on(eventName, handler, unshift) { 108 | if (unshift == null) { 109 | unshift = false 110 | } 111 | 112 | if (this.disposed) { 113 | throw new Error("Emitter has been disposed") 114 | } 115 | 116 | if (typeof handler !== "function") { 117 | throw new Error("Handler must be a function") 118 | } 119 | 120 | const currentHandlers = this.handlersByEventName[eventName] 121 | if (currentHandlers) { 122 | if (unshift) { 123 | this.handlersByEventName[eventName].unshift(handler) 124 | } else { 125 | this.handlersByEventName[eventName].push(handler) 126 | } 127 | } else { 128 | this.handlersByEventName[eventName] = [handler] 129 | } 130 | 131 | // When the emitter is disposed, we want to dispose of all subscriptions. 132 | // However, we also need to stop tracking disposables when they're disposed 133 | // from outside, otherwise this class will hold references to all the 134 | // disposables it created (instead of just the active ones). 135 | const cleanup = new Disposable(() => { 136 | this.subscriptions.remove(cleanup) 137 | return this.off(eventName, handler) 138 | }) 139 | this.subscriptions.add(cleanup) 140 | return cleanup 141 | } 142 | 143 | // Public: Register the given handler function to be invoked the next time an 144 | // events with the given name is emitted via {::emit}. 145 | // 146 | // * `eventName` {String} naming the event that you want to invoke the handler 147 | // when emitted. 148 | // * `handler` {Function} to invoke when {::emit} is called with the given 149 | // event name. 150 | // 151 | // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 152 | once(eventName, handler, unshift) { 153 | if (unshift == null) { 154 | unshift = false 155 | } 156 | 157 | const wrapped = function(value) { 158 | disposable.dispose() 159 | return handler(value) 160 | } 161 | 162 | const disposable = this.on(eventName, wrapped, unshift) 163 | return disposable 164 | } 165 | 166 | // Public: Register the given handler function to be invoked *before* all 167 | // other handlers existing at the time of subscription whenever events by the 168 | // given name are emitted via {::emit}. 169 | // 170 | // Use this method when you need to be the first to handle a given event. This 171 | // could be required when a data structure in a parent object needs to be 172 | // updated before third-party event handlers registered on a child object via a 173 | // public API are invoked. Your handler could itself be preempted via 174 | // subsequent calls to this method, but this can be controlled by keeping 175 | // methods based on `::preempt` private. 176 | // 177 | // * `eventName` {String} naming the event that you want to invoke the handler 178 | // when emitted. 179 | // * `handler` {Function} to invoke when {::emit} is called with the given 180 | // event name. 181 | // 182 | // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 183 | preempt(eventName, handler) { 184 | return this.on(eventName, handler, true) 185 | } 186 | 187 | // Private: Used by the disposable. 188 | off(eventName, handlerToRemove) { 189 | if (this.disposed) { 190 | return 191 | } 192 | 193 | const handlers = this.handlersByEventName[eventName] 194 | if (handlers) { 195 | const handlerIndex = handlers.indexOf(handlerToRemove) 196 | if (handlerIndex >= 0) { 197 | handlers.splice(handlerIndex, 1) 198 | } 199 | if (handlers.length === 0) { 200 | delete this.handlersByEventName[eventName] 201 | } 202 | } 203 | } 204 | 205 | /* 206 | Section: Event Emission 207 | */ 208 | 209 | // Public: Invoke handlers registered via {::on} for the given event name. 210 | // 211 | // * `eventName` The name of the event to emit. Handlers registered with {::on} 212 | // for the same name will be invoked. 213 | // * `value` Callbacks will be invoked with this value as an argument. 214 | emit(eventName, value) { 215 | const handlers = 216 | this.handlersByEventName && this.handlersByEventName[eventName] 217 | if (handlers) { 218 | // create a copy of `handlers` so that if any handler mutates `handlers` 219 | // (e.g. by calling `on` on this same emitter), this does not result in 220 | // changing the handlers being called during this same `emit`. 221 | const handlersCopy = handlers.slice() 222 | for (let i = 0; i < handlersCopy.length; i++) { 223 | this.constructor.dispatch(handlersCopy[i], value) 224 | } 225 | } 226 | } 227 | 228 | emitAsync(eventName, value) { 229 | const handlers = 230 | this.handlersByEventName && this.handlersByEventName[eventName] 231 | if (handlers) { 232 | const promises = handlers.map(handler => 233 | this.constructor.dispatch(handler, value) 234 | ) 235 | return Promise.all(promises).then(() => {}) 236 | } 237 | return Promise.resolve() 238 | } 239 | 240 | getEventNames() { 241 | return Object.keys(this.handlersByEventName) 242 | } 243 | 244 | listenerCountForEventName(eventName) { 245 | const handlers = this.handlersByEventName[eventName] 246 | return handlers == null ? 0 : handlers.length 247 | } 248 | 249 | getTotalListenerCount() { 250 | let result = 0 251 | for (let eventName of Object.keys(this.handlersByEventName)) { 252 | result += this.handlersByEventName[eventName].length 253 | } 254 | return result 255 | } 256 | } 257 | 258 | Emitter.dispatch = Emitter.simpleDispatch 259 | Emitter.exceptionHandlers = [] 260 | module.exports = Emitter 261 | -------------------------------------------------------------------------------- /lib/event-kit.js: -------------------------------------------------------------------------------- 1 | exports.Emitter = require("./emitter") 2 | exports.Disposable = require("./disposable") 3 | exports.CompositeDisposable = require("./composite-disposable") 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "event-kit", 3 | "version": "2.5.3", 4 | "description": "Simple library for implementing and consuming evented APIs", 5 | "main": "./dist/event-kit", 6 | "scripts": { 7 | "build": "cross-env BABEL_KEEP_MODULES=false babel lib --out-dir dist --delete-dir-on-start", 8 | "docs": "joanna-tello -o api.json package.json lib", 9 | "prepublish": "npm run build && npm run docs", 10 | "test": "jasmine-focused --captureExceptions --forceexit spec" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/atom/event-kit.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/atom/event-kit/issues" 18 | }, 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@babel/cli": "^7.12.10", 22 | "@babel/core": "^7.12.10", 23 | "babel-preset-atomic": "^3.0.1", 24 | "cross-env": "^7.0.3", 25 | "jasmine-focused": "^1.0.7", 26 | "joanna": "https://github.com/aminya/joanna" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /spec/composite-disposable-spec.js: -------------------------------------------------------------------------------- 1 | const CompositeDisposable = require("../dist/composite-disposable") 2 | const Disposable = require("../dist/disposable") 3 | 4 | describe("CompositeDisposable", function() { 5 | let disposable1, disposable2, disposable3 6 | 7 | beforeEach(function() { 8 | disposable1 = new Disposable() 9 | disposable2 = new Disposable() 10 | disposable3 = new Disposable() 11 | }) 12 | 13 | it("can be constructed with multiple disposables", function() { 14 | const composite = new CompositeDisposable(disposable1, disposable2) 15 | composite.dispose() 16 | 17 | expect(composite.disposed).toBe(true) 18 | expect(disposable1.disposed).toBe(true) 19 | expect(disposable2.disposed).toBe(true) 20 | }) 21 | 22 | it("allows disposables to be added and removed", function() { 23 | const composite = new CompositeDisposable() 24 | composite.add(disposable1) 25 | composite.add(disposable2, disposable3) 26 | composite.delete(disposable1) 27 | composite.remove(disposable3) 28 | 29 | composite.dispose() 30 | 31 | expect(composite.disposed).toBe(true) 32 | expect(disposable1.disposed).toBe(false) 33 | expect(disposable2.disposed).toBe(true) 34 | expect(disposable3.disposed).toBe(false) 35 | }) 36 | 37 | describe("Adding non disposables", function() { 38 | it("throws a TypeError when undefined", function() { 39 | const composite = new CompositeDisposable() 40 | const nonDisposable = undefined 41 | expect(() => composite.add(nonDisposable)).toThrow() 42 | }) 43 | 44 | it("throws a TypeError when object missing .dispose()", function() { 45 | const composite = new CompositeDisposable() 46 | const nonDisposable = {} 47 | expect(() => composite.add(nonDisposable)).toThrow() 48 | }) 49 | 50 | it("throws a TypeError when object with non-function dispose", function() { 51 | const composite = new CompositeDisposable() 52 | const nonDisposable = { dispose: "not a function" } 53 | expect(() => composite.add(nonDisposable)).toThrow() 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /spec/disposable-spec.js: -------------------------------------------------------------------------------- 1 | const Disposable = require("../dist/disposable") 2 | 3 | describe("Disposable", function() { 4 | it("does not try to execute disposalAction when it is not a function", function() { 5 | const disposalAction = {} 6 | const disposable = new Disposable(disposalAction) 7 | expect(disposable.disposalAction).toBe(disposalAction) 8 | 9 | disposable.dispose() 10 | expect(disposable.disposalAction).toBe(null) 11 | }) 12 | 13 | it("dereferences the disposalAction once dispose() is invoked", function() { 14 | const disposalAction = jasmine.createSpy("dummy") 15 | const disposable = new Disposable(disposalAction) 16 | expect(disposable.disposalAction).toBe(disposalAction) 17 | 18 | disposable.dispose() 19 | expect(disposalAction.callCount).toBe(1) 20 | expect(disposable.disposalAction).toBe(null) 21 | 22 | disposable.dispose() 23 | expect(disposalAction.callCount).toBe(1) 24 | }) 25 | 26 | it('can be extended by ES5-style classes', () => { 27 | function MyDisposable () { 28 | // super 29 | Disposable.apply(this, arguments) 30 | } 31 | 32 | MyDisposable.prototype = new Disposable() 33 | 34 | let actionCalled = false 35 | const disposable = new MyDisposable(() => { actionCalled = true }) 36 | disposable.dispose() 37 | expect(actionCalled).toBe(true) 38 | }) 39 | 40 | describe(".isDisposable(object)", () => { 41 | it("true if the object implements the ::dispose function", function() { 42 | expect(Disposable.isDisposable(new Disposable(function() {}))).toBe(true) 43 | expect(Disposable.isDisposable({ dispose() {} })).toBe(true) 44 | 45 | expect(Disposable.isDisposable(null)).toBe(false) 46 | expect(Disposable.isDisposable(undefined)).toBe(false) 47 | expect(Disposable.isDisposable({ foo() {} })).toBe(false) 48 | expect(Disposable.isDisposable({ dispose: 1 })).toBe(false) 49 | }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /spec/emitter-spec.js: -------------------------------------------------------------------------------- 1 | const Emitter = require("../dist/emitter") 2 | 3 | describe("Emitter", function() { 4 | it("invokes the observer when the named event is emitted until disposed", function() { 5 | const emitter = new Emitter() 6 | 7 | const fooEvents = [] 8 | const barEvents = [] 9 | 10 | const sub1 = emitter.on("foo", value => fooEvents.push(["a", value])) 11 | const sub2 = emitter.on("bar", value => barEvents.push(["b", value])) 12 | const sub3 = emitter.preempt("bar", value => barEvents.push(["c", value])) 13 | 14 | emitter.emit("foo", 1) 15 | emitter.emit("foo", 2) 16 | emitter.emit("bar", 3) 17 | 18 | sub1.dispose() 19 | 20 | emitter.emit("foo", 4) 21 | emitter.emit("bar", 5) 22 | 23 | sub2.dispose() 24 | 25 | emitter.emit("bar", 6) 26 | 27 | expect(fooEvents).toEqual([["a", 1], ["a", 2]]) 28 | expect(barEvents).toEqual([ 29 | ["c", 3], 30 | ["b", 3], 31 | ["c", 5], 32 | ["b", 5], 33 | ["c", 6] 34 | ]) 35 | }) 36 | 37 | it("throws an exception when subscribing with a callback that isn't a function", function() { 38 | const emitter = new Emitter() 39 | expect(() => emitter.on("foo", null)).toThrow() 40 | expect(() => emitter.on("foo", "a")).toThrow() 41 | }) 42 | 43 | it("can register a function more than once, and therefore will call it multiple times", function() { 44 | const emitter = new Emitter(); 45 | let callCount = 0; 46 | const fn = () => callCount++ 47 | 48 | emitter.on('foo', fn); 49 | emitter.on('foo', fn); 50 | emitter.emit('foo') 51 | 52 | expect(callCount).toEqual(2); 53 | }) 54 | 55 | it("allows all subsribers to be cleared out at once", function() { 56 | const emitter = new Emitter() 57 | const events = [] 58 | 59 | emitter.on("foo", value => events.push(["a", value])) 60 | emitter.preempt("foo", value => events.push(["b", value])) 61 | emitter.clear() 62 | 63 | emitter.emit("foo", 1) 64 | emitter.emit("foo", 2) 65 | expect(events).toEqual([]) 66 | }) 67 | 68 | it("allows the listeners to be inspected", function() { 69 | const emitter = new Emitter() 70 | 71 | const disposable1 = emitter.on("foo", function() {}) 72 | expect(emitter.getEventNames()).toEqual(["foo"]) 73 | expect(emitter.listenerCountForEventName("foo")).toBe(1) 74 | expect(emitter.listenerCountForEventName("bar")).toBe(0) 75 | expect(emitter.getTotalListenerCount()).toBe(1) 76 | 77 | const disposable2 = emitter.on("bar", function() {}) 78 | expect(emitter.getEventNames()).toEqual(["foo", "bar"]) 79 | expect(emitter.listenerCountForEventName("foo")).toBe(1) 80 | expect(emitter.listenerCountForEventName("bar")).toBe(1) 81 | expect(emitter.getTotalListenerCount()).toBe(2) 82 | 83 | emitter.preempt("foo", function() {}) 84 | expect(emitter.getEventNames()).toEqual(["foo", "bar"]) 85 | expect(emitter.listenerCountForEventName("foo")).toBe(2) 86 | expect(emitter.listenerCountForEventName("bar")).toBe(1) 87 | expect(emitter.getTotalListenerCount()).toBe(3) 88 | 89 | disposable1.dispose() 90 | expect(emitter.getEventNames()).toEqual(["foo", "bar"]) 91 | expect(emitter.listenerCountForEventName("foo")).toBe(1) 92 | expect(emitter.listenerCountForEventName("bar")).toBe(1) 93 | expect(emitter.getTotalListenerCount()).toBe(2) 94 | 95 | disposable2.dispose() 96 | expect(emitter.getEventNames()).toEqual(["foo"]) 97 | expect(emitter.listenerCountForEventName("foo")).toBe(1) 98 | expect(emitter.listenerCountForEventName("bar")).toBe(0) 99 | expect(emitter.getTotalListenerCount()).toBe(1) 100 | 101 | emitter.clear() 102 | expect(emitter.getTotalListenerCount()).toBe(0) 103 | }) 104 | 105 | describe("::once", function() { 106 | it("only invokes the handler once", function() { 107 | const emitter = new Emitter() 108 | let firedCount = 0 109 | emitter.once("foo", () => (firedCount += 1)) 110 | emitter.emit("foo") 111 | emitter.emit("foo") 112 | expect(firedCount).toBe(1) 113 | }) 114 | 115 | it("invokes the handler with the emitted value", function() { 116 | const emitter = new Emitter() 117 | let emittedValue = null 118 | emitter.once("foo", value => (emittedValue = value)) 119 | emitter.emit("foo", "bar") 120 | expect(emittedValue).toBe("bar") 121 | }) 122 | }) 123 | 124 | describe("dispose", function() { 125 | it("disposes of all listeners", function() { 126 | const emitter = new Emitter() 127 | const disposable1 = emitter.on("foo", function() {}) 128 | const disposable2 = emitter.once("foo", function() {}) 129 | emitter.dispose() 130 | expect(disposable1.disposed).toBe(true) 131 | expect(disposable2.disposed).toBe(true) 132 | }) 133 | 134 | it("doesn't keep track of disposed disposables", function() { 135 | const emitter = new Emitter() 136 | const disposable = emitter.on("foo", function() {}) 137 | expect(emitter.subscriptions.disposables.size).toBe(1) 138 | disposable.dispose() 139 | expect(emitter.subscriptions.disposables.size).toBe(0) 140 | }) 141 | }) 142 | 143 | describe("when a handler throws an exception", function() { 144 | describe("when no exception handlers are registered on Emitter", () => 145 | it("throws exceptions as normal, stopping subsequent handlers from firing", function() { 146 | const emitter = new Emitter() 147 | let handler2Fired = false 148 | 149 | emitter.on("foo", function() { 150 | throw new Error() 151 | }) 152 | emitter.on("foo", () => (handler2Fired = true)) 153 | 154 | expect(() => emitter.emit("foo")).toThrow() 155 | expect(handler2Fired).toBe(false) 156 | })) 157 | 158 | describe("when exception handlers are registered on Emitter", () => 159 | it("invokes the exception handlers in the order they were registered and continues to fire subsequent event handlers", function() { 160 | const emitter = new Emitter() 161 | let handler2Fired = false 162 | 163 | emitter.on("foo", function() { 164 | throw new Error("bar") 165 | }) 166 | emitter.on("foo", () => (handler2Fired = true)) 167 | 168 | let errorHandlerInvocations = [] 169 | const disposable1 = Emitter.onEventHandlerException(function(error) { 170 | expect(error.message).toBe("bar") 171 | errorHandlerInvocations.push(1) 172 | }) 173 | 174 | const disposable2 = Emitter.onEventHandlerException(function(error) { 175 | expect(error.message).toBe("bar") 176 | errorHandlerInvocations.push(2) 177 | }) 178 | 179 | emitter.emit("foo") 180 | 181 | expect(errorHandlerInvocations).toEqual([1, 2]) 182 | expect(handler2Fired).toBe(true) 183 | 184 | errorHandlerInvocations = [] 185 | handler2Fired = false 186 | 187 | disposable1.dispose() 188 | emitter.emit("foo") 189 | expect(errorHandlerInvocations).toEqual([2]) 190 | expect(handler2Fired).toBe(true) 191 | 192 | errorHandlerInvocations = [] 193 | handler2Fired = false 194 | 195 | disposable2.dispose() 196 | expect(() => emitter.emit("foo")).toThrow() 197 | expect(errorHandlerInvocations).toEqual([]) 198 | expect(handler2Fired).toBe(false) 199 | })) 200 | }) 201 | 202 | describe("::emitAsync", function() { 203 | it("resolves when all of the promises returned by handlers have resolved", function() { 204 | const emitter = new Emitter() 205 | 206 | let resolveHandler1 = null 207 | let resolveHandler3 = null 208 | const disposable1 = emitter.on( 209 | "foo", 210 | () => 211 | new Promise(function(resolve) { 212 | return (resolveHandler1 = resolve) 213 | }) 214 | ) 215 | const disposable2 = emitter.on("foo", function() {}) 216 | const disposable3 = emitter.on( 217 | "foo", 218 | () => 219 | new Promise(function(resolve) { 220 | return (resolveHandler3 = resolve) 221 | }) 222 | ) 223 | 224 | const result = emitter.emitAsync("foo") 225 | 226 | waitsFor(function(done) { 227 | resolveHandler3() 228 | resolveHandler1() 229 | return result.then(function(result) { 230 | expect(result).toBeUndefined() 231 | done() 232 | }) 233 | }) 234 | }) 235 | 236 | it("rejects when any of the promises returned by handlers reject", function() { 237 | const emitter = new Emitter() 238 | 239 | let rejectHandler1 = null 240 | const disposable1 = emitter.on( 241 | "foo", 242 | () => 243 | new Promise(function(resolve, reject) { 244 | return (rejectHandler1 = reject) 245 | }) 246 | ) 247 | const disposable2 = emitter.on("foo", function() {}) 248 | const disposable3 = emitter.on( 249 | "foo", 250 | () => new Promise(function(resolve) {}) 251 | ) 252 | 253 | const result = emitter.emitAsync("foo") 254 | 255 | waitsFor(function(done) { 256 | rejectHandler1(new Error("Something bad happened")) 257 | return result.catch(function(error) { 258 | expect(error.message).toBe("Something bad happened") 259 | done() 260 | }) 261 | }) 262 | }) 263 | }) 264 | }) 265 | --------------------------------------------------------------------------------