├── .nvmrc ├── .npmrc ├── .eslintignore ├── index.js ├── jest.config.js ├── .babelrc.js ├── LICENSE ├── .gitignore ├── src ├── utils.js └── index.js ├── package.json ├── .eslintrc.js ├── rollup.config.js ├── CHANGELOG.md ├── test └── index.spec.js └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-prefix='~' 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/** 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const PubSub = require('./src/index'); 2 | 3 | module.exports = PubSub; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | coverageReporters: ['json', 'lcov', 'text', 'clover', 'html'] 3 | }; 4 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | const { NODE_ENV } = process.env; 2 | 3 | module.exports = { 4 | presets: [ 5 | [ 6 | '@babel/preset-env', 7 | { 8 | modules: NODE_ENV === 'test' ? 'auto' : false 9 | } 10 | ] 11 | ], 12 | plugins: [ 13 | '@babel/plugin-proposal-object-rest-spread' 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-present George Raptis 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # production 61 | dist 62 | lib 63 | 64 | # misc 65 | .DS_Store 66 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const forOwn = (obj, callback, thisArg) => { 2 | for (let key in obj) { 3 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 4 | if (callback && callback.call(thisArg, obj[key], key, obj) === false) { 5 | return; 6 | } 7 | } 8 | } 9 | 10 | return obj; 11 | }; 12 | 13 | export const alias = (fn, instance) => (...args) => instance[fn].apply(instance, args); 14 | 15 | export const deliverTopic = (topic, data, instance) => { 16 | const topics = instance._pubsub_topics; 17 | const subscribers = topics[topic] ? [...topics[topic]] : []; 18 | 19 | for (let i = 0, len = subscribers.length; i < len; i += 1) { 20 | const token = subscribers[i].token; 21 | const currentSubscriber = subscribers[i]; 22 | 23 | if (!instance._options.immediateExceptions) { 24 | try { 25 | currentSubscriber.callback(data, { name: topic, token: token }); 26 | } catch (exception) { 27 | setTimeout(() => { 28 | throw exception; 29 | }, 0); 30 | } 31 | } else { 32 | currentSubscriber.callback(data, { name: topic, token: token }); 33 | } 34 | 35 | // Unsubscribe from event based on tokenized reference, 36 | // if subscriber's property once is set to true. 37 | if (currentSubscriber.once === true) { 38 | instance.unsubscribe(token); 39 | } 40 | } 41 | }; 42 | 43 | export const publishData = (topic, ...data) => data.length <= 1 ? data[0] : [...data]; 44 | 45 | export const publish = (instance, topic, data, sync) => { 46 | const topics = instance._pubsub_topics; 47 | 48 | if (!topics[topic]) { 49 | return false; 50 | } 51 | 52 | sync ? deliverTopic(topic, data, instance) : setTimeout(() => { 53 | deliverTopic(topic, data, instance); 54 | }, 0); 55 | 56 | return true; 57 | }; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PubSub", 3 | "version": "4.0.1", 4 | "description": "Javascript implementation of the Publish/Subscribe pattern.", 5 | "main": "dist/PubSub.cjs.min.js", 6 | "module": "dist/PubSub.esm.min.js", 7 | "unpkg": "dist/PubSub.umd.min.js", 8 | "files": [ 9 | "src/", 10 | "dist/" 11 | ], 12 | "scripts": { 13 | "build": "rollup -c --environment BUILD:production", 14 | "dev": "rollup -c -w", 15 | "lint": "eslint src/**/*.js", 16 | "test": "jest --config jest.config.js", 17 | "test:watch": "npm run test -- --watch", 18 | "test:coverage": "npm run test -- --coverage", 19 | "clean": "rimraf dist coverage", 20 | "prepare": "npm-run-all clean lint test build" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/georapbox/PubSub.git" 25 | }, 26 | "keywords": [ 27 | "subscribe", 28 | "publish", 29 | "publish/subscribe", 30 | "pubsub", 31 | "pub/sub", 32 | "event", 33 | "emitter", 34 | "eventemitter" 35 | ], 36 | "author": "George Raptis ", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/georapbox/PubSub/issues" 40 | }, 41 | "homepage": "https://github.com/georapbox/PubSub#readme", 42 | "devDependencies": { 43 | "@babel/core": "~7.12.10", 44 | "@babel/plugin-proposal-object-rest-spread": "~7.12.1", 45 | "@babel/preset-env": "~7.12.11", 46 | "@babel/register": "~7.12.10", 47 | "@rollup/plugin-babel": "~5.2.2", 48 | "cross-env": "~7.0.3", 49 | "eslint": "~7.18.0", 50 | "jest": "~26.6.3", 51 | "npm-run-all": "~4.1.5", 52 | "rimraf": "~3.0.2", 53 | "rollup": "~2.38.0", 54 | "rollup-plugin-terser": "~7.0.2" 55 | }, 56 | "browserslist": "> 0.5%, last 2 versions, Firefox ESR, not dead" 57 | } 58 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | node: true, 5 | jest: true 6 | }, 7 | parserOptions: { 8 | ecmaVersion: 9, 9 | sourceType: 'module', 10 | allowImportExportEverywhere: true 11 | }, 12 | extends: [ 13 | 'eslint:recommended' 14 | ], 15 | rules: { 16 | // Possible Errors 17 | 'no-empty': ['error', { 18 | 'allowEmptyCatch': true 19 | }], 20 | 'no-use-before-define': ['error', { 21 | 'functions': false 22 | }], 23 | // Stylistic Issues 24 | 'indent': ['warn', 2, { 25 | 'SwitchCase': 1, 26 | 'ignoredNodes': ['TemplateLiteral'] 27 | }], 28 | 'quotes': ['warn', 'single', { 29 | 'allowTemplateLiterals': true 30 | }], 31 | 'no-multiple-empty-lines': ['warn', { 32 | 'max': 1 33 | }], 34 | 'space-infix-ops': ['warn', { 35 | 'int32Hint': false 36 | }], 37 | 'semi': ['warn', 'always'], 38 | 'no-trailing-spaces': ['warn'], 39 | 'comma-spacing': ['warn'], 40 | 'comma-style': ['warn'], 41 | 'operator-linebreak': ['warn', 'before'], 42 | 'brace-style': ['warn'], 43 | 'keyword-spacing': ['warn'], 44 | 'object-curly-spacing': ['warn', 'always'], 45 | 'space-before-blocks': ['warn', 'always'], 46 | 'spaced-comment': ['warn', 'always'], 47 | 'space-before-function-paren': ['warn', { 48 | 'anonymous': 'always', 49 | 'named': 'never', 50 | 'asyncArrow': 'always' 51 | }], 52 | 'padded-blocks': ['warn', 'never'], 53 | 'comma-dangle': ['warn', 'never'], 54 | // Best Practices 55 | 'curly': ['warn'], 56 | 'eqeqeq': ['error', 'always', { 57 | 'null': 'ignore' 58 | }], 59 | 'no-multi-spaces': ['warn', { 60 | 'ignoreEOLComments': true, 61 | 'exceptions': { 62 | 'Property': false 63 | } 64 | }] 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import { terser } from 'rollup-plugin-terser'; 3 | import pkg from './package.json'; 4 | 5 | const LIBRARY_NAME = 'PubSub'; 6 | 7 | const banner = `/*! 8 | * ${pkg.name} 9 | * ${pkg.description} 10 | * 11 | * @version v${pkg.version} 12 | * @author ${pkg.author} 13 | * @homepage ${pkg.homepage} 14 | * @repository ${pkg.repository.url} 15 | * @license ${pkg.license} 16 | */`; 17 | 18 | const makeConfig = (env = 'development') => { 19 | let bundleSuffix = ''; 20 | 21 | if (env === 'production') { 22 | bundleSuffix = 'min.'; 23 | } 24 | 25 | const config = { 26 | input: 'src/index.js', 27 | output: [ 28 | { 29 | banner, 30 | name: LIBRARY_NAME, 31 | file: `dist/${LIBRARY_NAME}.umd.${bundleSuffix}js`, // UMD 32 | format: 'umd', 33 | exports: 'auto' 34 | }, 35 | { 36 | banner, 37 | file: `dist/${LIBRARY_NAME}.cjs.${bundleSuffix}js`, // CommonJS 38 | format: 'cjs', 39 | exports: 'default' 40 | }, 41 | { 42 | banner, 43 | file: `dist/${LIBRARY_NAME}.esm.${bundleSuffix}js`, // ESM 44 | format: 'es', 45 | exports: 'auto' 46 | } 47 | ], 48 | plugins: [ 49 | babel({ 50 | babelHelpers: 'bundled', 51 | exclude: ['node_modules/**'] 52 | }) 53 | ] 54 | }; 55 | 56 | if (env === 'production') { 57 | config.plugins.push(terser({ 58 | output: { 59 | comments: /^!/ 60 | } 61 | })); 62 | } 63 | 64 | return config; 65 | }; 66 | 67 | export default commandLineArgs => { 68 | const configs = [ 69 | makeConfig() 70 | ]; 71 | 72 | // Production 73 | if (commandLineArgs.environment === 'BUILD:production') { 74 | configs.push(makeConfig('production')); 75 | } 76 | 77 | return configs; 78 | }; 79 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v4.0.1 (2025-04-05) 4 | 5 | - Modified README.md to update CDN links for the library. 6 | - Updated LICENSE to reflect the current copyright year. 7 | - Introduced a new .nvmrc file specifying Node.js version 20. 8 | - Removed CI configuration file as part of repository cleanup. 9 | 10 | ## v4.0.0 (2021-24-01) 11 | 12 | ### Breaking changes 13 | 14 | - Remove static method `noConflict()`. 15 | 16 | ### New features 17 | 18 | - Add a new static method `createInstance()` as alternative way to create a new `PubSub` instance. 19 | - Export library in UMD, ESM and CommonJS formats. 20 | 21 | ### Internal changes 22 | 23 | - Refactor source code to ES2015+. 24 | - Use rollup.js to bundle the library. 25 | - Replace Mocha with Jest as testing framework. 26 | - Improve tests and coverage. 27 | - Replace Travis with Github Actions for CI. 28 | 29 | ## v3.6.2 30 | 31 | - Update `devDependencies` 32 | - Update CI configuration 33 | - Delete examples folder 34 | 35 | ## v3.6.0 36 | 37 | - Fix issue #4 38 | - Add `immediateExceptions` option when creating instance 39 | 40 | 41 | ## v3.5.0 42 | 43 | - Update `devDependencies` 44 | - Use `mocha` and `chai` for testing instead of `karma` and `jasmine` 45 | - Drop support Bower support 46 | - Exclude `dist` folder from source control 47 | 48 | ## v3.4.0 49 | 50 | - Add static method `PubSub.noConflict()` to roll back the global `PubSub` identifier. Used in a normal browser global namespace environment to avoid conflicts, etc. 51 | 52 | ## v3.3.0 53 | 54 | - If there is no subscriber for a topic, delete topic property when unsubscribing. Used to leave it as an empty array before. 55 | - The result of `subscribers` and `subscribersByTopic` methods is just a copy of the original object or array accordingly. 56 | - Keep devDependencies up to date. 57 | 58 | ## v3.2.7 59 | 60 | Allow passing multiple data arguments to `publish` and `publishSync` methods. 61 | 62 | ```js 63 | var pubsub = new PubSub(); 64 | 65 | pubsub.subscribe('event', function (data) { 66 | console.log(data); 67 | // => Array [{fname: 'John'}, {lname: 'Doe'}, [1, 2, 3], 'Lorem ipsum dolor sit amet.'] 68 | 69 | console.log(data[0]); 70 | // => Object {lname: 'John'} 71 | 72 | console.log(data[1]); 73 | // => Object {lname: 'Doe'} 74 | 75 | console.log(data[2]); 76 | // => Array [1, 2, 3] 77 | 78 | console.log(data[3]); 79 | // => String "Lorem ipsum dolor sit amet." 80 | }); 81 | 82 | pubsub.publish('event', {fname: 'John'}, {lname: 'Doe'}, [1, 2, 3], 'Lorem ipsum dolor sit amet.'); 83 | ``` 84 | 85 | ## v3.2.6 86 | 87 | - Ensure that listeners registered on the same topic are invoked in the order they are added. 88 | - Minor updates on documentation. 89 | - Update angular_1.x_example. 90 | 91 | ## v3.2.5 92 | 93 | - Add working example using Angular 1.x. 94 | - Update devDependencies. 95 | 96 | ## v3.2.4 97 | 98 | - Improve tests and coverage 99 | 100 | ## v3.2.3 101 | 102 | - Return a new instance of `PubSub` if it is invoked without the `new` keyword. 103 | - Add code coverage. 104 | 105 | ## v3.2.2 106 | 107 | - Keep devDependencies up to date 108 | 109 | ## v3.2.1 110 | 111 | - Fix License 112 | 113 | ## v3.2.0 114 | 115 | - Add public method `subscribersByTopic()` to get an array of subscribers for a specific topic. 116 | 117 | ## v3.1.0 118 | 119 | - `hasSubscribers` checks if there is at least one subscriber, no matter its name, if no argument is passed. 120 | - Add public method `subscribers()` to get a readonly object of the current subscribers. 121 | 122 | ## v3.0.0 123 | 124 | ### Breaking changes 125 | 126 | The default API method aliases are deprecated and removed from v3.0.0 onwards. However there is a new method `alias` introduced, that allows to create your own aliases. Therefore, if you already use those aliases in a project you can use the `alias` method to provide your own. 127 | 128 | Below is a map of the default aliases that existed prior to version 3.0.0: 129 | 130 | | Original method | Alias method | 131 | | --------------- | ------------- | 132 | | `subscribe` | `on` | 133 | | `subscribeOnce` | `once` | 134 | | `publishSync` | `triggerSync` | 135 | | `unsubscribe` | `off` | 136 | | `hasSubscribers` | `has` | 137 | 138 | To create your own aliases: 139 | 140 | ```js 141 | var pubsub = new PubSub().alias({ 142 | subscribe: 'on', 143 | subscribeOnce: 'once', 144 | publish: 'trigger', 145 | publishSync: 'triggerSync', 146 | unsubscribe: 'off', 147 | hasSubscribers: 'has' 148 | }); 149 | ``` 150 | 151 | ### Other updates 152 | 153 | - Add public method `unsubscribeAll` to clear all subscriptions whatsoever. 154 | - Add public method `alias` to create your own method aliases. (See above) 155 | - Provide source-map for the minified library. 156 | 157 | ## v2.1.0 158 | 159 | - Add support for publishing events synchronously using `publishSync` method. 160 | - Add public method `hasSubscribers` to check if there are subscribers for a specific topic. 161 | 162 | ## v2.0.3 163 | 164 | - Add support for Travis CI. 165 | - Lint source code using ESLint. 166 | 167 | ## v2.0.2 168 | 169 | - Keep devDependencies up to date. 170 | 171 | ## v2.0.0 172 | 173 | ### Breaking changes 174 | 175 | - Reverse the arguments the `callback` function accepts, in order to allow the usage of `data` argument without the need to also specify the `topic` if not needed. 176 | - Throw exception if `callback` is not a `function` or is not provided at all. 177 | 178 | ### Other updates 179 | - Return token on `subscribeOnce` method. 180 | - Correct annotations and provide examples. 181 | - Update devDependencies. 182 | - Provide `npm` scripts to run the tasks. No more need for global dependencies installed (Grunt). 183 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | import PubSub from '../src/index'; 2 | 3 | const noop = () => {}; 4 | 5 | describe('PubSub', () => { 6 | it('Should create a new instance of PubSub.', () => { 7 | const pubsub = new PubSub(); 8 | 9 | expect(pubsub).not.toBeUndefined(); 10 | 11 | expect(pubsub instanceof PubSub).toBe(true); 12 | }); 13 | 14 | it('Should create a new instance of PubSub using the "createInstance()" static method.', () => { 15 | const pubsub = PubSub.createInstance({ 16 | immediateExceptions: true 17 | }); 18 | 19 | expect(pubsub).not.toBeNull(); 20 | 21 | expect(pubsub._options.immediateExceptions).toBe(true); 22 | 23 | expect(pubsub instanceof PubSub).toBe(true); 24 | }); 25 | 26 | it('Should subscribe to an event.', () => { 27 | const pubsub = new PubSub(); 28 | 29 | expect(pubsub.subscribe('topic', noop)).toBe(0); 30 | }); 31 | 32 | it('Should subscribe to an event only once', () => { 33 | const pubsub = new PubSub(); 34 | 35 | pubsub.subscribeOnce('one-time-event', noop); 36 | 37 | pubsub.publishSync('one-time-event'); // Publish once 38 | 39 | expect(pubsub.hasSubscribers('one-time-event')).toBe(false); 40 | }); 41 | 42 | it('Should throw exception if a callback is not provided to the subscriber.', () => { 43 | const pubsub = new PubSub(); 44 | 45 | expect(() => { 46 | return pubsub.subscribe('topic'); 47 | }).toThrow(new TypeError('When subscribing for an event, a callback function must be defined.')); 48 | }); 49 | 50 | it('Should publish an event with name "topic" passing data: "{ dummyKey : \'dummyValue\' }"', () => { 51 | const pubsub = new PubSub(); 52 | const spy = jest.spyOn(pubsub, 'publish'); 53 | 54 | pubsub.subscribe('topic', noop); 55 | 56 | pubsub.publish('topic', { 57 | dummyKey: 'dummyValue' 58 | }); 59 | 60 | expect(spy).toHaveBeenCalledWith('topic', { dummyKey: 'dummyValue' }); 61 | 62 | spy.mockRestore(); 63 | }); 64 | 65 | it('Should not publish an event that was not subscribed.', () => { 66 | const pubsub = new PubSub(); 67 | 68 | pubsub.subscribe('topic', noop); 69 | 70 | expect(pubsub.publish('unknown-topic')).toBe(false); 71 | }); 72 | 73 | it('Should publish an event asynchronously.', done => { 74 | const pubsub = new PubSub(); 75 | let counter = 0; 76 | 77 | pubsub.subscribe('topic', () => { 78 | counter += 1; 79 | expect(counter).toBe(1); 80 | done(); 81 | }); 82 | 83 | pubsub.publish('topic'); 84 | 85 | expect(counter).toBe(0); 86 | }); 87 | 88 | it('Should publish an event synchronously.', () => { 89 | const pubsub = new PubSub(); 90 | let counter = 0; 91 | 92 | pubsub.subscribe('topic', () => { 93 | counter += 1; 94 | }); 95 | 96 | pubsub.publishSync('topic'); 97 | 98 | expect(counter).toBe(1); 99 | }); 100 | 101 | it('Should allow to pass multiple data arguments to "publish" methods.', done => { 102 | const pubsub = new PubSub(); 103 | 104 | pubsub.subscribe('topic', data => { 105 | expect(data).toEqual(['foo', 'bar']); 106 | done(); 107 | }); 108 | 109 | pubsub.publish('topic', 'foo', 'bar'); 110 | }); 111 | 112 | it('Should allow to pass multiple data arguments to "publishSync" methods.', done => { 113 | const pubsub = new PubSub(); 114 | 115 | pubsub.subscribe('topic', data => { 116 | expect(data).toEqual(['foo', 'bar']); 117 | done(); 118 | }); 119 | 120 | pubsub.publishSync('topic', 'foo', 'bar'); 121 | }); 122 | 123 | it('Should unsubscribe from event using the event name "topic".', () => { 124 | const pubsub = new PubSub(); 125 | 126 | pubsub.subscribe('topic', noop); 127 | 128 | const unsub = pubsub.unsubscribe('topic'); 129 | 130 | expect(unsub).toBe('topic'); 131 | expect(pubsub.hasSubscribers('topic')).toBe(false); 132 | }); 133 | 134 | it('Should unsubscribe from event using tokenized reference to the subscription.', () => { 135 | const pubsub = new PubSub(); 136 | const sub = pubsub.subscribe('topic', noop); 137 | 138 | pubsub.subscribe('topic', noop); 139 | pubsub.subscribe('topic', noop); 140 | 141 | expect(pubsub.unsubscribe(sub)).toBe(0); 142 | expect(pubsub.subscribers()['topic']).toHaveLength(2); 143 | }); 144 | 145 | it('Should unsubscribe from an event that was not subscribed before.', () => { 146 | const pubsub = new PubSub(); 147 | const unsub = pubsub.unsubscribe('topic'); 148 | 149 | expect(unsub).toBe(false); 150 | }); 151 | 152 | it('Should return true when checking if there are subscribers for an event that has been a subscription for.', () => { 153 | const pubsub = new PubSub(); 154 | 155 | pubsub.subscribe('topic', noop); 156 | 157 | expect(pubsub.hasSubscribers('topic')).toBe(true); 158 | }); 159 | 160 | it('Should return false when checking if there are subscribers for an event that we unsubscribed from.', () => { 161 | const pubsub = new PubSub(); 162 | 163 | pubsub.subscribe('topic', noop); 164 | pubsub.unsubscribe('topic'); 165 | 166 | expect(pubsub.hasSubscribers('topic')).toBe(false); 167 | }); 168 | 169 | it('Should return false when checking if there are subscribers for an event that has NOT been a subscription for.', () => { 170 | const pubsub = new PubSub(); 171 | 172 | expect(pubsub.hasSubscribers('topic')).toBe(false); 173 | }); 174 | 175 | it('Should return false when checking if there are any subscribers after unsubscribing ALL subscribers.', () => { 176 | const pubsub = new PubSub(); 177 | 178 | pubsub.subscribe('topic-A', noop); 179 | pubsub.subscribe('topic-B', noop); 180 | pubsub.subscribe('topic-C', noop); 181 | 182 | pubsub.unsubscribeAll(); 183 | 184 | expect(pubsub.hasSubscribers()).toBe(false); 185 | }); 186 | 187 | it('Should return true when checking if there any subscribers when we have subscribed at least for one event.', () => { 188 | const pubsub = new PubSub(); 189 | 190 | pubsub.subscribe('topic', noop); 191 | 192 | expect(pubsub.hasSubscribers()).toBe(true); 193 | }); 194 | 195 | it('Should return an array of 2 subscribers for topic named "topic".', () => { 196 | const pubsub = new PubSub(); 197 | 198 | pubsub.subscribe('topic', noop); 199 | pubsub.subscribe('topic', noop); 200 | 201 | expect(pubsub.subscribersByTopic('topic')).toHaveLength(2); 202 | }); 203 | 204 | it('Should return an empty array if we ask for subscribers for a not existing topic.', () => { 205 | const pubsub = new PubSub(); 206 | 207 | expect(pubsub.subscribersByTopic('topic')).toHaveLength(0); 208 | }); 209 | 210 | it('Should create aliases "on" and "off" for "subscribe" and "unsubscribe" methods respectively.', () => { 211 | const pubsub = new PubSub(); 212 | 213 | pubsub.alias({ 214 | subscribe: 'on', 215 | unsubscribe: 'off' 216 | }); 217 | 218 | const t = pubsub.on('topic', noop); 219 | 220 | expect(PubSub.prototype.on).not.toBeUndefined(); 221 | expect(typeof PubSub.prototype.on).toBe('function'); 222 | 223 | expect(PubSub.prototype.off).not.toBeUndefined(); 224 | expect(typeof PubSub.prototype.off).toBe('function'); 225 | 226 | expect(pubsub.off(t)).toBe(0); 227 | }); 228 | 229 | it('Should invoke every listener in the order that was subscribed.', () => { 230 | const pubsub = new PubSub(); 231 | const arr = []; 232 | 233 | function listener1() { 234 | arr.push('A'); 235 | } 236 | 237 | function listener2() { 238 | arr.push('B'); 239 | } 240 | 241 | function listener3() { 242 | arr.push('C'); 243 | } 244 | 245 | pubsub.subscribe('topic', listener1); 246 | pubsub.subscribe('topic', listener2); 247 | pubsub.subscribe('topic', listener3); 248 | 249 | pubsub.publishSync('topic'); 250 | 251 | expect(arr).toEqual(['A', 'B', 'C']); 252 | }); 253 | 254 | it('Should throw exceptions inside subscribers immediately when "immediateExceptions" option is a truthy value.', () => { 255 | const pubsub = new PubSub({ 256 | immediateExceptions: true 257 | }); 258 | 259 | pubsub.subscribe('topic', () => { 260 | throw new Error('Ooops!'); 261 | }); 262 | 263 | expect(() => { 264 | return pubsub.publishSync('topic'); 265 | }).toThrow(new Error('Ooops!')); 266 | }); 267 | 268 | it('Should not throw exceptions inside subscribers immediately when "immediateExceptions" option is a falsy value.', () => { 269 | const pubsub = new PubSub({ 270 | immediateExceptions: false 271 | }); 272 | 273 | pubsub.subscribe('topic', () => { 274 | throw new Error('Ooops!'); 275 | }); 276 | 277 | expect(() => { 278 | return pubsub.publishSync('topic'); 279 | }).not.toThrow(new Error('Ooops!')); 280 | }); 281 | }); 282 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { forOwn, publish, publishData, alias } from './utils'; 2 | 3 | class PubSub { 4 | /** 5 | * Creates a PubSub instance. 6 | * @constructor PubSub 7 | * 8 | * @param {object} [options] User options 9 | * @param {boolean} [options.immediateExceptions=false] Forces exceptions to be thrown immediately instead of delayed exceptions 10 | */ 11 | constructor(options) { 12 | const defaults = { 13 | immediateExceptions: false 14 | }; 15 | 16 | this._pubsub_topics = {}; // Storage for topics that can be broadcast or listened to. 17 | this._pubsub_uid = -1; // A topic identifier. 18 | this._options = { ...defaults, ...options }; 19 | } 20 | 21 | /** 22 | * Subscribe to events of interest with a specific topic name and a 23 | * callback function, to be executed when the topic/event is observed. 24 | * 25 | * @memberof PubSub 26 | * @this {PubSub} 27 | * @param {string} topic The topic's name 28 | * @param {function} callback Callback function to execute on event, taking two arguments: 29 | * - {*} data The data passed when publishing an event 30 | * - {object} The topic's info (name & token) 31 | * @param {boolean} [once=false] Checks if event will be triggered only one time 32 | * @return {number} The topic's token 33 | * @example 34 | * 35 | * const pubsub = new PubSub(); 36 | * 37 | * const onUserAdd = pubsub.subscribe('user_add', (data, topic) => { 38 | * console.log('User added'); 39 | * console.log('user data:', data); 40 | * }); 41 | */ 42 | subscribe(topic, callback, once) { 43 | const topics = this._pubsub_topics; 44 | const token = this._pubsub_uid += 1; 45 | const obj = {}; 46 | 47 | if (typeof callback !== 'function') { 48 | throw new TypeError('When subscribing for an event, a callback function must be defined.'); 49 | } 50 | 51 | if (!topics[topic]) { 52 | topics[topic] = []; 53 | } 54 | 55 | obj.token = token; 56 | obj.callback = callback; 57 | obj.once = !!once; 58 | 59 | topics[topic].push(obj); 60 | 61 | return token; 62 | } 63 | 64 | /** 65 | * Subscribe to events of interest setting a flag 66 | * indicating the event will be published only one time. 67 | * 68 | * @memberof PubSub 69 | * @this {PubSub} 70 | * @param {string} topic The topic's name 71 | * @param {function} callback Callback function to execute on event, taking two arguments: 72 | * - {*} data The data passed when publishing an event 73 | * - {object} The topic's info (name & token) 74 | * @return {number} The topic's token 75 | * @example 76 | * 77 | * const onUserAdd = pubsub.subscribeOnce('user_add', (data, topic) => { 78 | * console.log('User added'); 79 | * console.log('user data:', data); 80 | * }); 81 | */ 82 | subscribeOnce(topic, callback) { 83 | return this.subscribe(topic, callback, true); 84 | } 85 | 86 | /** 87 | * Publishes a topic asynchronously, passing the data to its subscribers. 88 | * Asynchronous publication helps in that the originator of the topics will 89 | * not be blocked while consumers process them. 90 | * For synchronous topic publication check `publishSync`. 91 | * 92 | * @memberof PubSub 93 | * @this {PubSub} 94 | * @param {string} topic The topic's name 95 | * @param {...*} [data] The data to be passed to its subscribers 96 | * @return {boolean} Returns `true` if topic exists and event is published; otheriwse `false` 97 | * @example 98 | * 99 | * pubsub.publish('user_add', { 100 | * firstName: 'John', 101 | * lastName: 'Doe', 102 | * email: 'johndoe@gmail.com' 103 | * }); 104 | */ 105 | publish(topic, ...data) { 106 | return publish(this, topic, publishData(topic, ...data), false); 107 | } 108 | 109 | /** 110 | * Publishes a topic synchronously, passing the data to its subscribers. 111 | * 112 | * @memberof PubSub 113 | * @this {PubSub} 114 | * @param {string} topic The topic's name 115 | * @param {...*} [data] The data to be passed to its subscribers 116 | * @return {boolean} Returns `true` if topic exists and event is published; otheriwse `false` 117 | * @example 118 | * 119 | * pubsub.publishSync('user_add', { 120 | * firstName: 'John', 121 | * lastName: 'Doe', 122 | * email: 'johndoe@gmail.com' 123 | * }); 124 | */ 125 | publishSync(topic, ...data) { 126 | return publish(this, topic, publishData(topic, ...data), true); 127 | } 128 | 129 | /** 130 | * Unsubscribes from a specific topic, based on the topic name, 131 | * or based on a tokenized reference to the subscription. 132 | * 133 | * @memberof PubSub 134 | * @this {PubSub} 135 | * @param {string|number} topic Topic's name or subscription reference 136 | * @return {boolean|string} Returns `false` if `topic` does not match a subscribed event; otherwise the topic's name 137 | * @example 138 | * 139 | * // Unsubscribe using the topic's name. 140 | * pubsub.unsubscribe('user_add'); 141 | * 142 | * // Unsubscribe using a tokenized reference to the subscription. 143 | * pubsub.unsubscribe(onUserAdd); 144 | */ 145 | unsubscribe(topic) { 146 | const topics = this._pubsub_topics; 147 | let tf = false; 148 | 149 | for (const prop in topics) { 150 | if (Object.prototype.hasOwnProperty.call(topics, prop)) { 151 | if (topics[prop]) { 152 | let len = topics[prop].length; 153 | 154 | while (len) { 155 | len -= 1; 156 | 157 | // `topic` is a tokenized reference to the subscription. 158 | if (topics[prop][len].token === topic) { 159 | topics[prop].splice(len, 1); 160 | if (topics[prop].length === 0) { 161 | delete topics[prop]; 162 | } 163 | return topic; 164 | } 165 | 166 | // `topic` is the event name. 167 | if (prop === topic) { 168 | topics[prop].splice(len, 1); 169 | if (topics[prop].length === 0) { 170 | delete topics[prop]; 171 | } 172 | tf = true; 173 | } 174 | } 175 | 176 | if (tf === true) { 177 | return topic; 178 | } 179 | } 180 | } 181 | } 182 | 183 | return false; 184 | } 185 | 186 | /** 187 | * Clears all subscriptions whatsoever. 188 | * 189 | * @memberof PubSub 190 | * @this {PubSub} 191 | * @return {PubSub} The PubSub instance. 192 | * @example 193 | * 194 | * const pubsub = new PubSub(); 195 | * pubsub.subscribe('message1', () => {}); 196 | * pubsub.subscribe('message2', () => {}); 197 | * pubsub.subscribe('message3', () => {}); 198 | * pubsub.unsubscribeAll(); 199 | * pubsub.hasSubscribers(); // -> false 200 | */ 201 | unsubscribeAll() { 202 | this._pubsub_topics = {}; 203 | return this; 204 | } 205 | 206 | /** 207 | * Checks if there are subscribers for a specific topic. 208 | * If `topic` is not provided, checks if there is at least one subscriber. 209 | * 210 | * @memberof PubSub 211 | * @this {PubSub} 212 | * @param {string} [topic] The topic's name to check 213 | * @return {boolean} Returns `true` there are subscribers; otherwise `false` 214 | * @example 215 | * 216 | * const pubsub = new PubSub(); 217 | * pubsub.on('message', data => console.log(data)); 218 | * 219 | * pubsub.hasSubscribers('message'); 220 | * // -> true 221 | */ 222 | hasSubscribers(topic) { 223 | const topics = this._pubsub_topics; 224 | let hasSubscribers = false; 225 | 226 | // If no arguments passed 227 | if (topic == null) { 228 | forOwn(topics, (value, key) => { 229 | if (key) { 230 | hasSubscribers = true; 231 | return false; 232 | } 233 | }); 234 | 235 | return hasSubscribers; 236 | } 237 | 238 | // If a topic's name is passed as argument 239 | return Object.prototype.hasOwnProperty.call(topics, topic); 240 | } 241 | 242 | /** 243 | * Gets all the subscribers as a set of key value pairs that 244 | * represent the topic's name and the event listener(s) bound. 245 | * 246 | * @NOTE Mutating the result of this method does not affect the real subscribers. This is for reference only. 247 | * 248 | * @memberof PubSub 249 | * @this {PubSub} 250 | * @return {object} A readonly object with all subscribers. 251 | * @example 252 | * 253 | * const pubsub = new PubSub(); 254 | * 255 | * pubsub.subscribe('message', listener); 256 | * pubsub.subscribe('message', listener); 257 | * pubsub.subscribe('another_message', listener); 258 | * 259 | * pubsub.subscribers(); 260 | * // -> Object { message: Array[2], another_message: Array[1] } 261 | */ 262 | subscribers() { 263 | const res = {}; 264 | 265 | forOwn(this._pubsub_topics, (topicValue, topicKey) => { 266 | res[topicKey] = [...topicValue]; 267 | }); 268 | 269 | return res; 270 | } 271 | 272 | /** 273 | * Gets subscribers for a specific topic. 274 | * 275 | * @NOTE Mutating the result of this method does not affect the real subscribers. This is for reference only. 276 | * 277 | * @memberof PubSub 278 | * @this {PubSub} 279 | * @param {string} topic The topic's name to check for subscribers 280 | * @return {array} A copy array of all subscribers for a topic if exist; otherwise an empty array 281 | * @example 282 | * 283 | * const pubsub = new PubSub(); 284 | * 285 | * pubsub.subscribe('message', listener1); 286 | * pubsub.subscribeOnce('message', listener2); 287 | * pubsub.subscribe('another_message', listener1); 288 | * 289 | * pubsub.subscribersByTopic('message'); 290 | * // -> Array [{token: 0, once: false, callback: listener1()}, {token: 1, once: true, callback: listener2()}] 291 | * 292 | * pubsub.subscribersByTopic('another_message'); 293 | * // -> Array [{token: 2, once: false, callback: listener1()}] 294 | * 295 | * pubsub.subscribersByTopic('some_message_not_existing'); 296 | * // -> Array [] 297 | */ 298 | subscribersByTopic(topic) { 299 | return this._pubsub_topics[topic] ? [...this._pubsub_topics[topic]] : []; 300 | } 301 | 302 | /** 303 | * Creates aliases for public methods. 304 | * 305 | * @memberof PubSub 306 | * @this {PubSub} 307 | * @param {object} aliasMap A plain object that maps the public methods to their aliases. 308 | * @return {PubSub} The PubSub instance. 309 | * @example 310 | * 311 | * const pubsub = new PubSub().alias({ 312 | * subscribe: 'on', 313 | * subscribeOnce: 'once', 314 | * publish: 'trigger', 315 | * publishSync: 'triggerSync', 316 | * unsubscribe: 'off', 317 | * hasSubscribers: 'has' 318 | * }); 319 | */ 320 | alias(aliasMap) { 321 | forOwn(aliasMap, (value, key) => { 322 | if (PubSub.prototype[key]) { 323 | PubSub.prototype[aliasMap[key]] = alias(key, this); 324 | } 325 | }); 326 | 327 | return this; 328 | } 329 | } 330 | 331 | PubSub.createInstance = options => new PubSub(options); 332 | 333 | export default PubSub; 334 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://img.shields.io/npm/v/PubSub.svg)](https://www.npmjs.com/package/PubSub) 2 | [![npm license](https://img.shields.io/npm/l/PubSub.svg)](https://www.npmjs.com/package/PubSub) 3 | 4 | # PubSub 5 | 6 | Javascript implementation of the [Publish/Subscribe](http://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) pattern. 7 | 8 | ## Install 9 | 10 | ### npm 11 | ```sh 12 | $ npm install PubSub 13 | ``` 14 | 15 | ## Usage 16 | 17 | The library is exported in UMD, CommonJS, and ESM formats. You can import it the following ways: 18 | 19 | ### Using ESM import statement 20 | 21 | ```js 22 | import PubSub from 'PubSub'; 23 | ``` 24 | 25 | ### Using CommonJS require statement 26 | 27 | ```js 28 | const PubSub = require('PubSub'); 29 | 30 | // If you use a bundler like Webpack, you may need to import it the following way 31 | // as it might try to use the ESM module instead of the CommonJS. 32 | const PubSub = require('PubSub').default; 33 | ``` 34 | 35 | ### Old school browser global 36 | 37 | ```html 38 | 39 | ``` 40 | 41 | ## API 42 | 43 | * [PubSub](#PubSub) 44 | * [new PubSub([options])](#new_PubSub_new) 45 | * _instance_ 46 | * [.subscribe(topic, callback, [once])](#PubSub+subscribe) ⇒ number 47 | * [.subscribeOnce(topic, callback)](#PubSub+subscribeOnce) ⇒ number 48 | * [.publish(topic, [...data])](#PubSub+publish) ⇒ boolean 49 | * [.publishSync(topic, [...data])](#PubSub+publishSync) ⇒ boolean 50 | * [.unsubscribe(topic)](#PubSub+unsubscribe) ⇒ boolean \| string 51 | * [.unsubscribeAll()](#PubSub+unsubscribeAll) ⇒ [PubSub](#PubSub) 52 | * [.hasSubscribers([topic])](#PubSub+hasSubscribers) ⇒ boolean 53 | * [.subscribers()](#PubSub+subscribers) ⇒ object 54 | * [.subscribersByTopic(topic)](#PubSub+subscribersByTopic) ⇒ array 55 | * [.alias(aliasMap)](#PubSub+alias) ⇒ [PubSub](#PubSub) 56 | * _static_ 57 | * [.createInstance([options])](#PubSub.createInstance) ⇒ [PubSub](#PubSub) 58 | 59 | 60 | 61 | ### new PubSub([options]) 62 | Creates a PubSub instance. 63 | 64 | #### Available options 65 | 66 | | Param | Type | Default | Description | 67 | | --- | --- | --- | --- | 68 | | immediateExceptions1 | boolean | false | Force immediate exceptions (instead of delayed exceptions). | 69 | 70 | 1 *Before version 3.6.0 PubSub would fail to deliver your topics to all subscribers if one or more failed (see issue [#4](https://github.com/georapbox/PubSub/issues/4)). As of version 3.6.0 PubSub handles this by delaying thrown exceptions by default. You can set `immediateExceptions` to `true` or any truthy value in order to maintain the stack trace for development reasons but this is not recommended for production.* 71 | 72 | ## Public Methods 73 | 74 | 75 | 76 | ### subscribe(topic, callback, [once]) ⇒ number 77 | Subscribe to events of interest with a specific topic name and a 78 | callback function, to be executed when the topic/event is observed. 79 | 80 | **Kind**: instance method of [PubSub](#PubSub) 81 | **Returns**: number - The topic's token 82 | 83 | | Param | Type | Default | Description | 84 | | --- | --- | --- | --- | 85 | | topic | string | | The topic's name | 86 | | callback | function | | Callback function to execute on event, taking two arguments: - {*} data The data passed when publishing an event - {object} The topic's info (name & token) | 87 | | [once] | boolean | false | Checks if event will be triggered only one time | 88 | 89 | **Example** 90 | ```js 91 | const pubsub = new PubSub(); 92 | 93 | const onUserAdd = pubsub.subscribe('user_add', (data, topic) => { 94 | console.log('User added'); 95 | console.log('user data:', data); 96 | }); 97 | ``` 98 | 99 | 100 | ### subscribeOnce(topic, callback) ⇒ number 101 | Subscribe to events of interest setting a flag 102 | indicating the event will be published only one time. 103 | 104 | **Kind**: instance method of [PubSub](#PubSub) 105 | **Returns**: number - The topic's token 106 | 107 | | Param | Type | Description | 108 | | --- | --- | --- | 109 | | topic | string | The topic's name | 110 | | callback | function | Callback function to execute on event, taking two arguments: - {*} data The data passed when publishing an event - {object} The topic's info (name & token) | 111 | 112 | **Example** 113 | ```js 114 | const pubsub = new PubSub(); 115 | 116 | const onUserAdd = pubsub.subscribeOnce('user_add', (data, topic) => { 117 | console.log('User added'); 118 | console.log('user data:', data); 119 | }); 120 | ``` 121 | 122 | 123 | ### publish(topic, [data]) ⇒ boolean 124 | Publishes a topic **asynchronously**, passing the data to its subscribers. 125 | Asynchronous publication helps in that the originator of the topics will not be blocked while consumers process them. 126 | For synchronous topic publication check `publishSync`. 127 | 128 | **Kind**: instance method of [PubSub](#PubSub) 129 | **Returns**: boolean - Returns `true` if topic exists and event is published; otheriwse `false` 130 | 131 | | Param | Type | Description | 132 | | --- | --- | --- | 133 | | topic | string | The topic's name | 134 | | [data] | ...\* | The data to be passed to its subscribers | 135 | 136 | **Example** 137 | ```js 138 | const pubsub = new PubSub(); 139 | 140 | pubsub.publish('user_add', { 141 | firstName: 'John', 142 | lastName: 'Doe', 143 | email: 'johndoe@gmail.com' 144 | }); 145 | ``` 146 | 147 | 148 | ### publishSync(topic, [data]) ⇒ boolean 149 | Publishes a topic **synchronously**, passing the data to its subscribers. 150 | 151 | **Kind**: instance method of [PubSub](#PubSub) 152 | **Returns**: boolean - Returns `true` if topic exists and event is published; otheriwse `false` 153 | 154 | | Param | Type | Description | 155 | | --- | --- | --- | 156 | | topic | string | The topic's name | 157 | | [data] | ...\* | The data to be passed to its subscribers | 158 | 159 | **Example** 160 | ```js 161 | const pubsub = new PubSub(); 162 | 163 | pubsub.publishSync('user_add', { 164 | firstName: 'John', 165 | lastName: 'Doe', 166 | email: 'johndoe@gmail.com' 167 | }); 168 | ``` 169 | 170 | 171 | ### unsubscribe(topic) ⇒ boolean \| string 172 | Unsubscribes from a specific topic, based on the topic name, 173 | or based on a tokenized reference to the subscription. 174 | 175 | **Kind**: instance method of [PubSub](#PubSub) 176 | **Returns**: boolean \| string - Returns `false` if `topic` does not match a subscribed event; otherwise the topic's name 177 | 178 | | Param | Type | Description | 179 | | --- | --- | --- | 180 | | topic | string \| number | Topic's name or subscription reference | 181 | 182 | **Example** 183 | ```js 184 | const pubsub = new PubSub(); 185 | 186 | // Unsubscribe using the topic's name. 187 | pubsub.unsubscribe('user_add'); 188 | 189 | // Unsubscribe using a tokenized reference to the subscription. 190 | pubsub.unsubscribe(onUserAdd); 191 | ``` 192 | 193 | 194 | ### unsubscribeAll() ⇒ [PubSub](#PubSub) 195 | Clears all subscriptions whatsoever. 196 | 197 | **Kind**: instance method of [PubSub](#PubSub) 198 | **Returns**: [PubSub](#PubSub) - The PubSub instance. 199 | **Example** 200 | ```js 201 | const pubsub = new PubSub(); 202 | 203 | pubsub.subscribe('message1', () => {}); 204 | pubsub.subscribe('message2', () => {}); 205 | pubsub.subscribe('message3', () => {}); 206 | pubsub.unsubscribeAll(); 207 | pubsub.hasSubscribers(); // -> false 208 | ``` 209 | 210 | 211 | ### hasSubscribers([topic]) ⇒ boolean 212 | Checks if there are subscribers for a specific topic. 213 | If `topic` is not provided, checks if there is at least one subscriber. 214 | 215 | **Kind**: instance method of [PubSub](#PubSub) 216 | **Returns**: boolean - Returns `true` there are subscribers; otherwise `false` 217 | 218 | | Param | Type | Description | 219 | | --- | --- | --- | 220 | | [topic] | string | The topic's name to check | 221 | 222 | **Example** 223 | ```js 224 | const pubsub = new PubSub(); 225 | 226 | pubsub.on('message', data => console.log(data)); 227 | 228 | pubsub.hasSubscribers('message'); 229 | // -> true 230 | ``` 231 | 232 | 233 | ### subscribers() ⇒ object 234 | Gets all the subscribers as a set of key value pairs that 235 | represent the topic's name and the event listener(s) bound. 236 | 237 | **Kind**: instance method of [PubSub](#PubSub) 238 | **Returns**: object - A readonly object with all subscribers. 239 | **Note**: Mutating the result of this method does not affect the real subscribers. This is for reference only. 240 | **Example** 241 | ```js 242 | const pubsub = new PubSub(); 243 | 244 | pubsub.subscribe('message', listener); 245 | pubsub.subscribe('message', listener); 246 | pubsub.subscribe('another_message', listener); 247 | 248 | pubsub.subscribers(); 249 | // -> Object { message: Array[2], another_message: Array[1] } 250 | ``` 251 | 252 | 253 | ### subscribersByTopic(topic) ⇒ array 254 | Gets subscribers for a specific topic. 255 | 256 | **Kind**: instance method of [PubSub](#PubSub) 257 | **Returns**: array - A copy array of all subscribers for a topic if exist; otherwise an empty array 258 | **Note**: Mutating the result of this method does not affect the real subscribers. This is for reference only. 259 | 260 | | Param | Type | Description | 261 | | --- | --- | --- | 262 | | topic | String | The topic's name to check for subscribers | 263 | 264 | **Example** 265 | ```js 266 | const pubsub = new PubSub(); 267 | 268 | pubsub.subscribe('message', listener1); 269 | pubsub.subscribeOnce('message', listener2); 270 | pubsub.subscribe('another_message', listener1); 271 | 272 | pubsub.subscribersByTopic('message'); 273 | // -> Array [{token: 0, once: false, callback: listener1()}, {token: 1, once: true, callback: listener2()}] 274 | 275 | pubsub.subscribersByTopic('another_message'); 276 | // -> Array [{token: 2, once: false, callback: listener1()}] 277 | 278 | pubsub.subscribersByTopic('some_message_not_existing'); 279 | // -> Array [] 280 | ``` 281 | 282 | 283 | ### alias(aliasMap) ⇒ [PubSub](#PubSub) 284 | Creates aliases for public methods. 285 | 286 | **Kind**: instance method of [PubSub](#PubSub) 287 | **Returns**: [PubSub](#PubSub) - The PubSub instance. 288 | 289 | | Param | Type | Description | 290 | | --- | --- | --- | 291 | | aliasMap | Object | A plain object that maps the public methods to their aliases. | 292 | 293 | **Example** 294 | ```js 295 | const pubsub = new PubSub().alias({ 296 | subscribe: 'on', 297 | subscribeOnce: 'once', 298 | publish: 'trigger', 299 | publishSync: 'triggerSync', 300 | unsubscribe: 'off', 301 | hasSubscribers: 'has' 302 | }); 303 | ``` 304 | 305 | ## Static methods 306 | 307 | 308 | 309 | ### PubSub.createInstance([options]) ⇒ [PubSub](#PubSub) 310 | Creates a PubSub instance. This is an alternative way to create a new instance if you don't prefer using the `new` keyword. 311 | 312 | **Kind**: static method of [PubSub](#PubSub) 313 | **Returns**: [PubSub](#PubSub) - The PubSub constructor. 314 | **Example** 315 | ```js 316 | const pubsub = PubSub.createInstance(); 317 | ``` 318 | 319 | ## Changelog 320 | 321 | For API updates and breaking changes, check the [CHANGELOG](https://github.com/georapbox/PubSub/blob/master/CHANGELOG.md). 322 | 323 | ## More about Publish/Subscribe pattern 324 | 325 | - [The Observer Pattern - Addy Osmani](https://addyosmani.com/resources/essentialjsdesignpatterns/book/#observerpatternjavascript) 326 | - [Publish–Subscribe pattern - Wikipedia](http://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) 327 | 328 | ## License 329 | 330 | [The MIT License (MIT)](https://github.com/georapbox/PubSub/blob/master/LICENSE) 331 | --------------------------------------------------------------------------------