├── .editorconfig ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.js ├── inline-resources.js ├── package-lock.json ├── package.json ├── publish.sh ├── src └── lib │ ├── index.ts │ ├── laravel-echo.d.ts │ ├── src │ ├── module.ts │ ├── services │ │ ├── interceptor.service.ts │ │ └── lib.service.ts │ └── types.ts │ ├── tsconfig.es5.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | dist/ 3 | node_modules/ 4 | out-tsc/ 5 | debug.log 6 | npm-debug.log 7 | src/**/*.js 8 | !src/demo/systemjs.config.js 9 | !src/demo/systemjs.config.lib.js 10 | !**/*systemjs-angular-loader.js 11 | *.js.map 12 | e2e/**/*.js 13 | e2e/**/*.js.map 14 | coverage 15 | .idea 16 | *.iml 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.github/ 2 | /node_modules/ 3 | /out-tsc/ 4 | /src/ 5 | /.editorconfig 6 | /.gitignore 7 | /build.js 8 | /inline-resources.js 9 | /tsconfig.json 10 | /tslint.json 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | # [2.1.0](https://github.com/chancezeus/angular-laravel-echo/compare/v2.0.11...v2.1.0) (2019-01-24) 7 | 8 | 9 | ### Features 10 | 11 | * Add ability to listen for notifications on different channels ([4d8b2a5](https://github.com/chancezeus/angular-laravel-echo/commit/4d8b2a5)) 12 | 13 | 14 | 15 | 16 | ## [2.0.11](https://github.com/chancezeus/angular-laravel-echo/compare/v2.0.10...v2.0.11) (2018-12-22) 17 | 18 | 19 | 20 | 21 | ## [2.0.10](https://github.com/chancezeus/angular-laravel-echo/compare/v2.0.9...v2.0.10) (2018-10-25) 22 | 23 | 24 | 25 | 26 | ## [2.0.9](https://github.com/chancezeus/angular-laravel-echo/compare/v2.0.8...v2.0.9) (2018-10-25) 27 | 28 | 29 | 30 | 31 | ## [2.0.8](https://github.com/chancezeus/angular-laravel-echo/compare/v2.0.7...v2.0.8) (2018-10-16) 32 | 33 | 34 | 35 | 36 | ## [2.0.7](https://github.com/chancezeus/angular-laravel-echo/compare/v2.0.6...v2.0.7) (2018-10-16) 37 | 38 | 39 | 40 | 41 | ## [2.0.6](https://github.com/chancezeus/angular-laravel-echo/compare/v2.0.5...v2.0.6) (2018-10-16) 42 | 43 | 44 | 45 | 46 | ## [2.0.5](https://github.com/chancezeus/angular-laravel-echo/compare/v2.0.4...v2.0.5) (2018-10-15) 47 | 48 | 49 | 50 | 51 | ## [2.0.4](https://github.com/chancezeus/angular-laravel-echo/compare/v2.0.3...v2.0.4) (2018-10-15) 52 | 53 | 54 | 55 | 56 | ## [2.0.3](https://github.com/chancezeus/angular-laravel-echo/compare/v2.0.2...v2.0.3) (2018-09-29) 57 | 58 | 59 | 60 | 61 | ## [2.0.2](https://github.com/chancezeus/angular-laravel-echo/compare/v2.0.1...v2.0.2) (2018-09-29) 62 | 63 | 64 | 65 | 66 | ## [2.0.1](https://github.com/chancezeus/angular-laravel-echo/compare/v1.1.1...v2.0.1) (2018-09-29) 67 | 68 | 69 | 70 | 71 | ## [2.0.0](https://github.com/chancezeus/angular-laravel-echo/compare/v1.1.1...v2.0.0) (2018-05-31) 72 | 73 | 74 | 75 | 76 | ## [1.1.1](https://github.com/chancezeus/angular-laravel-echo/compare/v1.1.0...v1.1.1) (2018-03-22) 77 | 78 | 79 | 80 | 81 | # [1.1.0](https://github.com/chancezeus/angular-laravel-echo/compare/v1.0.3...v1.1.0) (2018-03-22) 82 | 83 | 84 | ### Features 85 | 86 | * Add `forRoot` to NgModule for configuring the module ([14a9543](https://github.com/chancezeus/angular-laravel-echo/commit/14a9543)) 87 | 88 | 89 | 90 | 91 | ## [1.0.3](https://github.com/chancezeus/angular-laravel-echo/compare/v1.0.2...v1.0.3) (2018-03-22) 92 | 93 | 94 | 95 | 96 | ## [1.0.2](https://github.com/chancezeus/angular-laravel-echo/compare/v1.0.1...v1.0.2) (2018-03-22) 97 | 98 | 99 | 100 | 101 | ## 1.0.1 (2018-03-21) 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018 Mark van Beek 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Laravel Echo 2 | 3 | This package is a simple service to allow easy integration of Laravel Echo into angular. The service tries to follow the general functionality 4 | of Laravel Echo as closely as possible. The most important difference is the use of Observable streams instead of callbacks for events/notifications, 5 | this simplifies integration into Angular a lot and (more importantly) makes sure only one listener per subscribed event/notification has to be created. 6 | 7 | One important note is that (since Laravel Echo itself does not supply a way to stop listening for events) you must make sure to call leave for any channel 8 | that is no longer required, not just unsubscribe the event subscriptions (otherwise a memory leak will occur). 9 | 10 | # Versions 11 | 12 | With the release of Angular 6.0, breaking changes were introduced in the form of the updated dependency on RxJS 6 so consult 13 | the following chart for what version of the package to use based on your version of Angular. 14 | 15 | | Angular Version | Package Version | 16 | |:---------------:|:---------------:| 17 | | \>= 6.0 | 2.* | 18 | | < 6.0 | 1.* | 19 | 20 | # Documentation 21 | 22 | Documentation for the module is available on github pages at [https://chancezeus.github.io/angular-laravel-echo/](https://chancezeus.github.io/angular-laravel-echo/) 23 | 24 | # Contributors 25 | 26 | - [ChanceZeus](https://github.com/chancezeus): Initial author of the package 27 | - [Wizofgoz](https://github.com/Wizofgoz): Angular 6+ support 28 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const glob = require('glob'); 6 | const camelCase = require('camelcase'); 7 | const ngc = require('@angular/compiler-cli/src/main').main; 8 | const rollup = require('rollup'); 9 | const uglify = require('rollup-plugin-uglify').uglify; 10 | const sourcemaps = require('rollup-plugin-sourcemaps'); 11 | const nodeResolve = require('rollup-plugin-node-resolve'); 12 | const commonjs = require('rollup-plugin-commonjs'); 13 | 14 | const inlineResources = require('./inline-resources'); 15 | 16 | const packageJson = require('./package.json'); 17 | const libName = packageJson.name; 18 | const rootFolder = path.join(__dirname); 19 | const compilationFolder = path.join(rootFolder, 'out-tsc'); 20 | const srcFolder = path.join(rootFolder, 'src/lib'); 21 | const distFolder = path.join(rootFolder, 'dist'); 22 | const tempLibFolder = path.join(compilationFolder, 'lib'); 23 | const es5OutputFolder = path.join(compilationFolder, 'lib-es5'); 24 | const es2015OutputFolder = path.join(compilationFolder, 'lib-es2015'); 25 | 26 | return Promise.resolve() 27 | // Copy library to temporary folder and inline html/css. 28 | .then(() => _relativeCopy(`**/*`, srcFolder, tempLibFolder) 29 | .then(() => inlineResources(tempLibFolder)) 30 | .then(() => console.log('Inlining succeeded.')) 31 | ) 32 | // Compile to ES2015. 33 | .then(() => ngc(['--project', `${tempLibFolder}/tsconfig.lib.json`])) 34 | .then(exitCode => exitCode === 0 ? Promise.resolve() : Promise.reject()) 35 | .then(() => console.log('ES2015 compilation succeeded.')) 36 | // Compile to ES5. 37 | .then(() => ngc(['--project', `${tempLibFolder}/tsconfig.es5.json`])) 38 | .then(exitCode => exitCode === 0 ? Promise.resolve() : Promise.reject()) 39 | .then(() => console.log('ES5 compilation succeeded.')) 40 | // Copy typings and metadata to `dist/` folder. 41 | .then(() => Promise.resolve() 42 | .then(() => _relativeCopy('**/*.d.ts', es2015OutputFolder, distFolder)) 43 | .then(() => _relativeCopy('**/*.metadata.json', es2015OutputFolder, distFolder)) 44 | .then(() => console.log('Typings and metadata copy succeeded.')) 45 | ) 46 | // Bundle lib. 47 | .then(() => { 48 | // Base configuration. 49 | const es5Entry = path.join(es5OutputFolder, `${libName}.js`); 50 | const es2015Entry = path.join(es2015OutputFolder, `${libName}.js`); 51 | const rollupBaseConfig = { 52 | output: { 53 | name: camelCase(libName), 54 | globals: { 55 | // The key here is library name, and the value is the the name of the global variable name 56 | // the window object. 57 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#globals for more. 58 | '@angular/common': 'ng.common', 59 | '@angular/common/http': 'ng.common.http', 60 | '@angular/core': 'ng.core', 61 | 'laravel-echo': 'Echo', 62 | 'pusher-js': 'Pusher', 63 | 'rxjs': 'Rx', 64 | 'rxjs/operators': 'Rx.Observable.prototype', 65 | 'socket.io-client': 'io', 66 | }, 67 | sourceMap: true, 68 | }, 69 | // ATTENTION: 70 | // Add any dependency or peer dependency your library to `globals` and `external`. 71 | // This is required for UMD bundle users. 72 | external: [ 73 | // List of dependencies 74 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#external for more. 75 | '@angular/common', 76 | '@angular/common/http', 77 | '@angular/core', 78 | 'laravel-echo', 79 | 'pusher-js', 80 | 'rxjs', 81 | 'rxjs/operators', 82 | 'socket.io-client', 83 | ], 84 | plugins: [ 85 | nodeResolve({jsnext: true, module: true, browser: true}), 86 | commonjs(/*{ 87 | include: [ 88 | 'node_modules/!**', 89 | // 'node_modules/laravel-echo/!**', 90 | // 'node_modules/pusher-js/!**', 91 | // 'node_modules/rxjs/!**', 92 | // 'node_modules/socket.io-client/!**' 93 | ] 94 | }*/), 95 | sourcemaps(), 96 | ] 97 | }; 98 | 99 | // UMD bundle. 100 | const umdConfig = Object.assign({}, rollupBaseConfig, { 101 | input: es5Entry, 102 | output: Object.assign({}, rollupBaseConfig.output, { 103 | file: path.join(distFolder, `bundles`, `${libName}.umd.js`), 104 | format: 'umd', 105 | }), 106 | }); 107 | 108 | // Minified UMD bundle. 109 | const minifiedUmdConfig = Object.assign({}, rollupBaseConfig, { 110 | input: es5Entry, 111 | output: Object.assign({}, rollupBaseConfig.output, { 112 | file: path.join(distFolder, `bundles`, `${libName}.umd.min.js`), 113 | format: 'umd', 114 | }), 115 | plugins: rollupBaseConfig.plugins.concat([uglify({})]) 116 | }); 117 | 118 | // ESM+ES5 flat module bundle. 119 | const esm5config = Object.assign({}, rollupBaseConfig, { 120 | input: es5Entry, 121 | output: Object.assign({}, rollupBaseConfig.output, { 122 | file: path.join(distFolder, `${libName}.es5.js`), 123 | format: 'es', 124 | }), 125 | }); 126 | 127 | // ESM+ES2015 flat module bundle. 128 | const esm2015config = Object.assign({}, rollupBaseConfig, { 129 | input: es2015Entry, 130 | output: Object.assign({}, rollupBaseConfig.output, { 131 | file: path.join(distFolder, `${libName}.js`), 132 | format: 'es', 133 | }), 134 | }); 135 | 136 | const allBundles = [ 137 | umdConfig, 138 | minifiedUmdConfig, 139 | esm5config, 140 | esm2015config 141 | ].map(cfg => rollup.rollup(cfg).then(bundle => bundle.write(cfg.output))); 142 | 143 | return Promise.all(allBundles) 144 | .then(() => console.log('All bundles generated successfully.')) 145 | }) 146 | // Copy package files 147 | .then(() => Promise.resolve() 148 | .then(() => _relativeCopy('laravel-echo.d.ts', path.join(rootFolder, 'src', 'lib'), distFolder)) 149 | .then(() => _relativeCopy('LICENSE', rootFolder, distFolder)) 150 | .then(() => _relativeCopy('README.md', rootFolder, distFolder)) 151 | .then(() => _generatePackageJson(distFolder)) 152 | .then(() => console.log('Package files copy succeeded.')) 153 | ) 154 | .catch(e => { 155 | console.error('Build failed. See below for errors.\n'); 156 | console.error(e); 157 | process.exit(1); 158 | }); 159 | 160 | 161 | function _generatePackageJson(to) { 162 | return new Promise((resolve, reject) => { 163 | const contents = Object.assign({}, packageJson); 164 | delete contents.scripts; 165 | delete contents.dependencies; 166 | delete contents.devDependencies; 167 | 168 | fs.writeFile(path.join(to, 'package.json'), JSON.stringify(contents, null, 2), err => { 169 | if (err) { 170 | reject(err); 171 | return; 172 | } 173 | 174 | resolve(); 175 | }); 176 | }); 177 | } 178 | 179 | // Copy files maintaining relative paths. 180 | function _relativeCopy(fileGlob, from, to) { 181 | return new Promise((resolve, reject) => { 182 | glob(fileGlob, {cwd: from, nodir: true}, (err, files) => { 183 | if (err) { 184 | reject(err); 185 | return; 186 | } 187 | 188 | var promise = Promise.resolve(); 189 | 190 | files.forEach(file => { 191 | const origin = path.join(from, file); 192 | const dest = path.join(to, file); 193 | const dir = path.dirname(dest); 194 | 195 | promise = promise.then(() => _recursiveMkDir(dir).then(() => new Promise((resolve, reject) => { 196 | fs.copyFile(origin, dest, (err) => { 197 | if (err) { 198 | reject(err); 199 | return; 200 | } 201 | 202 | resolve(); 203 | }); 204 | }))); 205 | }); 206 | 207 | promise.then(resolve, reject); 208 | }); 209 | }); 210 | } 211 | 212 | // Recursively create a dir. 213 | function _recursiveMkDir(dir) { 214 | return new Promise((resolve, reject) => { 215 | fs.stat(dir, (err, stat) => { 216 | if (err) { 217 | if (err.code !== 'ENOENT') { 218 | reject(err); 219 | return; 220 | } 221 | 222 | _recursiveMkDir(path.dirname(dir)).then(() => new Promise((resolve, reject) => { 223 | fs.mkdir(dir, (err) => { 224 | if (err) { 225 | reject(err); 226 | return; 227 | } 228 | 229 | resolve(); 230 | }); 231 | })).then(resolve, reject); 232 | return; 233 | } 234 | 235 | if (!stat || !stat.isDirectory()) { 236 | reject('not a directory'); 237 | return; 238 | } 239 | 240 | resolve(); 241 | }); 242 | }); 243 | } 244 | -------------------------------------------------------------------------------- /inline-resources.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const glob = require('glob'); 6 | 7 | 8 | /** 9 | * Simple Promiseify function that takes a Node API and return a version that supports promises. 10 | * We use promises instead of synchronized functions to make the process less I/O bound and 11 | * faster. It also simplifies the code. 12 | */ 13 | function promiseify(fn) { 14 | return function () { 15 | const args = [].slice.call(arguments, 0); 16 | return new Promise((resolve, reject) => { 17 | fn.apply(this, args.concat([function (err, value) { 18 | if (err) { 19 | reject(err); 20 | } else { 21 | resolve(value); 22 | } 23 | }])); 24 | }); 25 | }; 26 | } 27 | 28 | const readFile = promiseify(fs.readFile); 29 | const writeFile = promiseify(fs.writeFile); 30 | 31 | /** 32 | * Inline resources in a tsc/ngc compilation. 33 | * @param projectPath {string} Path to the project. 34 | */ 35 | function inlineResources(projectPath) { 36 | 37 | // Match only TypeScript files in projectPath. 38 | const files = glob.sync('**/*.ts', {cwd: projectPath}); 39 | 40 | // For each file, inline the templates and styles under it and write the new file. 41 | return Promise.all(files.map(filePath => { 42 | const fullFilePath = path.join(projectPath, filePath); 43 | return readFile(fullFilePath, 'utf-8') 44 | .then(content => inlineResourcesFromString(content, url => { 45 | // Resolve the template url. 46 | return path.join(path.dirname(fullFilePath), url); 47 | })) 48 | .then(content => writeFile(fullFilePath, content)) 49 | .catch(err => { 50 | console.error('An error occured: ', err); 51 | }); 52 | })); 53 | } 54 | 55 | /** 56 | * Inline resources from a string content. 57 | * @param content {string} The source file's content. 58 | * @param urlResolver {Function} A resolver that takes a URL and return a path. 59 | * @returns {string} The content with resources inlined. 60 | */ 61 | function inlineResourcesFromString(content, urlResolver) { 62 | // Curry through the inlining functions. 63 | return [ 64 | inlineTemplate, 65 | inlineStyle 66 | ].reduce((content, fn) => fn(content, urlResolver), content); 67 | } 68 | 69 | /** 70 | * Inline the templates for a source file. Simply search for instances of `templateUrl: ...` and 71 | * replace with `template: ...` (with the content of the file included). 72 | * @param content {string} The source file's content. 73 | * @param urlResolver {Function} A resolver that takes a URL and return a path. 74 | * @return {string} The content with all templates inlined. 75 | */ 76 | function inlineTemplate(content, urlResolver) { 77 | return content.replace(/templateUrl:\s*'([^']+?\.html)'/g, function (m, templateUrl) { 78 | const templateFile = urlResolver(templateUrl); 79 | const templateContent = fs.readFileSync(templateFile, 'utf-8'); 80 | const shortenedTemplate = templateContent 81 | .replace(/([\n\r]\s*)+/gm, ' ') 82 | .replace(/"/g, '\\"'); 83 | return `template: "${shortenedTemplate}"`; 84 | }); 85 | } 86 | 87 | 88 | /** 89 | * Inline the styles for a source file. Simply search for instances of `styleUrls: [...]` and 90 | * replace with `styles: [...]` (with the content of the file included). 91 | * @param urlResolver {Function} A resolver that takes a URL and return a path. 92 | * @param content {string} The source file's content. 93 | * @return {string} The content with all styles inlined. 94 | */ 95 | function inlineStyle(content, urlResolver) { 96 | return content.replace(/styleUrls:\s*(\[[\s\S]*?\])/gm, function (m, styleUrls) { 97 | const urls = eval(styleUrls); 98 | return 'styles: [' 99 | + urls.map(styleUrl => { 100 | const styleFile = urlResolver(styleUrl); 101 | const styleContent = fs.readFileSync(styleFile, 'utf-8'); 102 | const shortenedStyle = styleContent 103 | .replace(/([\n\r]\s*)+/gm, ' ') 104 | .replace(/"/g, '\\"'); 105 | return `"${shortenedStyle}"`; 106 | }) 107 | .join(',\n') 108 | + ']'; 109 | }); 110 | } 111 | 112 | module.exports = inlineResources; 113 | module.exports.inlineResourcesFromString = inlineResourcesFromString; 114 | 115 | // Run inlineResources if module is being called directly from the CLI with arguments. 116 | if (require.main === module && process.argv.length > 2) { 117 | console.log('Inlining resources from project:', process.argv[2]); 118 | return inlineResources(process.argv[2]); 119 | } 120 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-laravel-echo", 3 | "version": "2.1.0", 4 | "description": "A Angular service for Laravel Echo", 5 | "main": "./bundles/angular-laravel-echo.umd.js", 6 | "module": "./angular-laravel-echo.es5.js", 7 | "es2015": "./angular-laravel-echo.js", 8 | "typings": "./angular-laravel-echo.d.ts", 9 | "author": "Mark van Beek (https://appelit.com)", 10 | "license": "MIT", 11 | "keywords": [ 12 | "angular", 13 | "laravel", 14 | "echo", 15 | "broadcasting", 16 | "pusher", 17 | "socket.io" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/chancezeus/angular-laravel-echo.git" 22 | }, 23 | "homepage": "https://github.com/chancezeus/angular-laravel-echo", 24 | "bugs": { 25 | "url": "https://github.com/chancezeus/angular-laravel-echo/issues" 26 | }, 27 | "engines": { 28 | "node": ">= 6.9.0", 29 | "npm": ">= 3.0.0" 30 | }, 31 | "scripts": { 32 | "clean": "rimraf out-tsc dist/*", 33 | "prebuild": "npm run clean", 34 | "build": "node build.js", 35 | "doc": "npm run doc:dist && npm run doc:pages", 36 | "doc:dist": "typedoc --out ./dist/docs --mode file --includeDeclarations --excludeExternals --excludePrivate --excludeProtected --module es2015 --target es5 --name \"Angular Laravel Echo\" --hideGenerator ./src/lib", 37 | "doc:pages": "typedoc --out ../gh-pages --mode file --includeDeclarations --excludeExternals --excludePrivate --excludeProtected --module es2015 --target es5 --name \"Angular Laravel Echo\" --hideGenerator ./src/lib --disableOutputCheck", 38 | "lint": "tslint --project tslint.json ./src/**/*.ts", 39 | "release": "npm run lint && standard-version && npm run build && npm run doc" 40 | }, 41 | "devDependencies": { 42 | "@angular/common": ">=6.0.0", 43 | "@angular/compiler": ">=6.0.0", 44 | "@angular/compiler-cli": ">=6.0.0", 45 | "@angular/core": ">=6.0.0", 46 | "@types/core-js": "^2.5.0", 47 | "@types/pusher-js": "^4.2.0", 48 | "@types/socket.io-client": "^1.4.32", 49 | "camelcase": "^5.0.0", 50 | "core-js": "^2.6.0", 51 | "glob": "^7.1.3", 52 | "laravel-echo": "^1.5.1", 53 | "pusher-js": "^4.3.1", 54 | "rimraf": "^2.6.1", 55 | "rollup": "^0.67.4", 56 | "rollup-plugin-commonjs": "^9.2.0", 57 | "rollup-plugin-node-resolve": "^4.0.0", 58 | "rollup-plugin-sourcemaps": "^0.4.1", 59 | "rollup-plugin-uglify": "^6.0.0", 60 | "rxjs": "^6.3.3", 61 | "socket.io-client": "^2.2.0", 62 | "standard-version": "^4.4.0", 63 | "systemjs": "^2.1.1", 64 | "tslint": "^5.11.0", 65 | "typedoc": "^0.11.1", 66 | "typescript": ">=2.7.2 <2.10.0", 67 | "zone.js": "^0.8.26" 68 | }, 69 | "peerDependencies": { 70 | "@angular/common": ">=6.0.0", 71 | "@angular/core": ">=6.0.0", 72 | "laravel-echo": "^1.5.1", 73 | "pusher-js": "^4.3.1", 74 | "rxjs": "^6.0.0", 75 | "socket.io-client": "^2.2.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | npm run release 5 | git push 6 | git push --tags 7 | npm publish dist 8 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/types'; 2 | 3 | export {EchoService} from './src/services/lib.service'; 4 | export {EchoInterceptor} from './src/services/interceptor.service'; 5 | export {AngularLaravelEchoModule} from './src/module'; 6 | -------------------------------------------------------------------------------- /src/lib/laravel-echo.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Echo { 2 | import * as pusher from 'pusher-js'; 3 | import {Pusher} from 'pusher-js'; 4 | import * as io from 'socket.io-client'; 5 | 6 | interface EchoStatic { 7 | /** 8 | * The broadcasting connector. 9 | */ 10 | connector: Echo.PusherConnector | Echo.SocketIoConnector | Echo.NullConnector; 11 | 12 | /** 13 | * The echo options. 14 | */ 15 | options: Echo.Config; 16 | 17 | /** 18 | * Create a new class instance. 19 | * 20 | * @param {Echo.Config} options 21 | * @returns {EchoStatic} 22 | */ 23 | new(options: Echo.Config): EchoStatic; 24 | 25 | /** 26 | * Register a Vue HTTP interceptor to add the X-Socket-ID header. 27 | */ 28 | registerVueRequestInterceptor(): void; 29 | 30 | /** 31 | * Register an Axios HTTP interceptor to add the X-Socket-ID header. 32 | */ 33 | registerAxiosRequestInterceptor(): void; 34 | 35 | /** 36 | * Register jQuery AjaxSetup to add the X-Socket-ID header. 37 | */ 38 | registerjQueryAjaxSetup(): void; 39 | 40 | /** 41 | * Listen for an event on a channel instance. 42 | * 43 | * @param {string} channel 44 | * @param {string} event 45 | * @param {(event: any) => void} callback 46 | * @returns {Echo.Channel} 47 | */ 48 | listen(channel: string, event: string, callback: (event: any) => void): Echo.Channel; 49 | 50 | /** 51 | * Get a channel instance by name. 52 | * 53 | * @param {string} channel 54 | * @returns {Echo.Channel} 55 | */ 56 | channel(channel: string): Echo.Channel; 57 | 58 | /** 59 | * Get a private channel instance by name. 60 | * 61 | * @param {string} channel 62 | * @returns {Echo.Channel} 63 | */ 64 | private(channel: string): Echo.PrivateChannel; 65 | 66 | /** 67 | * Get a presence channel instance by name. 68 | * 69 | * @param {string} channel 70 | * @returns {Echo.PresenceChannel} 71 | */ 72 | join(channel: string): Echo.PresenceChannel; 73 | 74 | /** 75 | * Leave the given channel. 76 | * 77 | * @param {string} channel 78 | */ 79 | leave(channel: string): void; 80 | 81 | /** 82 | * Get the Socket ID for the connection. 83 | * 84 | * @returns {string} 85 | */ 86 | socketId(): string; 87 | 88 | /** 89 | * Disconnect from the Echo server. 90 | */ 91 | disconnect(): void; 92 | } 93 | 94 | interface Config { 95 | /** 96 | * Authentication information for the underlying connector 97 | */ 98 | auth?: { 99 | /** 100 | * Headers to be included with the request 101 | */ 102 | headers?: { [key: string]: any }; 103 | }; 104 | /** 105 | * The authentication endpoint 106 | */ 107 | authEndpoint?: string; 108 | /** 109 | * The broadcaster to use 110 | */ 111 | broadcaster?: 'socket.io' | 'pusher' | 'null'; 112 | /** 113 | * The application CSRF token 114 | */ 115 | csrfToken?: string | null; 116 | /** 117 | * The namespace to use for events 118 | */ 119 | namespace?: string; 120 | } 121 | 122 | interface NullConfig extends Config { 123 | broadcaster: 'null'; 124 | } 125 | 126 | interface PusherConfig extends Config, pusher.Config { 127 | broadcaster?: 'pusher'; 128 | 129 | /** 130 | * A pusher client instance to use 131 | */ 132 | client?: Pusher; 133 | /** 134 | * The pusher host to connect to 135 | */ 136 | host?: string | null; 137 | /** 138 | * The pusher auth key 139 | */ 140 | key?: string | null; 141 | } 142 | 143 | interface SocketIoConfig extends Config, SocketIOClient.ConnectOpts { 144 | broadcaster: 'socket.io'; 145 | 146 | /** 147 | * A reference to the socket.io client to use 148 | */ 149 | client?: SocketIOClientStatic; 150 | 151 | /** 152 | * The url of the laravel echo server instance 153 | */ 154 | host: string; 155 | } 156 | 157 | interface Connector { 158 | /** 159 | * All of the subscribed channel names. 160 | */ 161 | channels: any; 162 | 163 | /** 164 | * Connector options. 165 | */ 166 | options: Config; 167 | 168 | /** 169 | * Create a new class instance. 170 | * 171 | * @param {Echo.Config} options 172 | * @returns {Echo.Connector} 173 | */ 174 | (options: Config): Connector; 175 | 176 | /** 177 | * Create a fresh connection. 178 | */ 179 | connect(): void; 180 | 181 | /** 182 | * Listen for an event on a channel instance. 183 | * 184 | * @param {string} name 185 | * @param {string} event 186 | * @param {pusher.EventCallback} callback 187 | * @returns {Echo.PusherChannel} 188 | */ 189 | listen(name: string, event: string, callback: (event: any) => void): Channel; 190 | 191 | /** 192 | * Get a channel instance by name. 193 | * 194 | * @param {string} channel 195 | * @returns {Echo.Channel} 196 | */ 197 | channel(channel: string): Channel; 198 | 199 | /** 200 | * Get a private channel instance by name. 201 | * 202 | * @param {string} channel 203 | * @returns {Echo.PrivateChannel} 204 | */ 205 | privateChannel(channel: string): PrivateChannel; 206 | 207 | /** 208 | * Get a presence channel instance by name. 209 | * 210 | * @param {string} channel 211 | * @returns {Echo.PresenceChannel} 212 | */ 213 | presenceChannel(channel: string): PresenceChannel; 214 | 215 | /** 216 | * Leave the given channel. 217 | * 218 | * @param {string} channel 219 | */ 220 | leave(channel: string): void; 221 | 222 | /** 223 | * Get the socket_id of the connection. 224 | * 225 | * @returns {string} 226 | */ 227 | socketId(): string; 228 | 229 | /** 230 | * Disconnect from the Echo server. 231 | */ 232 | disconnect(): void; 233 | } 234 | 235 | interface NullConnector extends Connector { 236 | /** 237 | * Create a new class instance. 238 | * 239 | * @param {Echo.NullConfig} options 240 | * @returns {Echo.NullConnector} 241 | */ 242 | (options: NullConfig): Connector; 243 | 244 | /** 245 | * Listen for an event on a channel instance. 246 | * 247 | * @param {string} name 248 | * @param {string} event 249 | * @param {pusher.EventCallback} callback 250 | * @returns {Echo.PusherChannel} 251 | */ 252 | listen(name: string, event: string, callback: pusher.EventCallback): NullChannel; 253 | 254 | /** 255 | * Get a channel instance by name. 256 | * 257 | * @param {string} name 258 | * @returns {Echo.PusherChannel} 259 | */ 260 | channel(name: string): NullChannel; 261 | 262 | /** 263 | * Get a private channel instance by name. 264 | * 265 | * @param {string} name 266 | * @returns {Echo.PusherPrivateChannel} 267 | */ 268 | privateChannel(name: string): NullPrivateChannel; 269 | 270 | /** 271 | * Get a presence channel instance by name. 272 | * 273 | * @param {string} name 274 | * @returns {Echo.PusherPresenceChannel} 275 | */ 276 | presenceChannel(name: string): NullPresenceChannel; 277 | } 278 | 279 | interface PusherConnector extends Connector { 280 | /** 281 | * The Pusher instance. 282 | */ 283 | pusher: Pusher; 284 | 285 | /** 286 | * Create a new class instance. 287 | * 288 | * @param {Echo.PusherConfig} options 289 | * @returns {Echo.PusherConnector} 290 | */ 291 | (options: PusherConfig): Connector; 292 | 293 | /** 294 | * Listen for an event on a channel instance. 295 | * 296 | * @param {string} name 297 | * @param {string} event 298 | * @param {pusher.EventCallback} callback 299 | * @returns {Echo.PusherChannel} 300 | */ 301 | listen(name: string, event: string, callback: pusher.EventCallback): PusherChannel; 302 | 303 | /** 304 | * Get a channel instance by name. 305 | * 306 | * @param {string} name 307 | * @returns {Echo.PusherChannel} 308 | */ 309 | channel(name: string): PusherChannel; 310 | 311 | /** 312 | * Get a private channel instance by name. 313 | * 314 | * @param {string} name 315 | * @returns {Echo.PusherPrivateChannel} 316 | */ 317 | privateChannel(name: string): PusherPrivateChannel; 318 | 319 | /** 320 | * Get a presence channel instance by name. 321 | * 322 | * @param {string} name 323 | * @returns {Echo.PusherPresenceChannel} 324 | */ 325 | presenceChannel(name: string): PusherPresenceChannel; 326 | } 327 | 328 | interface SocketIoConnector extends Connector { 329 | /** 330 | * The Socket.io connection instance. 331 | */ 332 | socket: SocketIOClient.Socket; 333 | 334 | /** 335 | * Create a new class instance. 336 | * 337 | * @param {Echo.SocketIoConfig} options 338 | * @returns {Echo.SocketIoConnector} 339 | */ 340 | (options: SocketIoConfig): Connector; 341 | 342 | /** 343 | * Get socket.io module from global scope or options. 344 | * 345 | * @returns {typeof io} 346 | */ 347 | getSocketIO(): SocketIOClientStatic; 348 | 349 | /** 350 | * Listen for an event on a channel instance. 351 | * 352 | * @param {string} name 353 | * @param {string} event 354 | * @param {(event: any) => void} callback 355 | * @returns {Echo.SocketIoChannel} 356 | */ 357 | listen(name: string, event: string, callback: (event: any) => void): SocketIoChannel; 358 | 359 | /** 360 | * Get a channel instance by name. 361 | * 362 | * @param {string} name 363 | * @returns {Echo.SocketIoChannel} 364 | */ 365 | channel(name: string): SocketIoChannel; 366 | 367 | /** 368 | * Get a private channel instance by name. 369 | * 370 | * @param {string} name 371 | * @returns {Echo.SocketIoPrivateChannel} 372 | */ 373 | privateChannel(name: string): SocketIoPrivateChannel; 374 | 375 | /** 376 | * Get a presence channel instance by name. 377 | * 378 | * @param {string} name 379 | * @returns {Echo.SocketIoPresenceChannel} 380 | */ 381 | presenceChannel(name: string): SocketIoPresenceChannel; 382 | } 383 | 384 | interface Channel { 385 | /** 386 | * The name of the channel. 387 | */ 388 | name: string; 389 | 390 | /** 391 | * Channel options. 392 | */ 393 | options: any; 394 | 395 | /** 396 | * The event formatter. 397 | */ 398 | eventFormatter: EventFormatter; 399 | 400 | /** 401 | * Listen for an event on the channel instance. 402 | * 403 | * @param {string} event 404 | * @param {(event: any) => void} callback 405 | * @returns {Echo.Channel} 406 | */ 407 | listen(event: string, callback: (event: any) => void): Channel; 408 | 409 | /** 410 | * Listen for a notification on the channel instance. 411 | * 412 | * @param {(notification: any) => void} callback 413 | * @returns {Echo.Channel} 414 | */ 415 | notification(callback: (notification: any) => void): Channel; 416 | 417 | /** 418 | * Listen for a whisper event on the channel instance. 419 | * 420 | * @param {string} event 421 | * @param {(data: any) => void} callback 422 | * @returns {Echo.Channel} 423 | */ 424 | listenForWhisper(event: string, callback: (data: any) => void): Channel; 425 | } 426 | 427 | interface PrivateChannel extends Channel { 428 | /** 429 | * Trigger client event on the channel. 430 | * 431 | * @param {string} event 432 | * @param data 433 | * @returns {Echo.PrivateChannel} 434 | */ 435 | whisper(event: string, data: any): PrivateChannel; 436 | } 437 | 438 | interface PresenceChannel extends PrivateChannel { 439 | /** 440 | * Register a callback to be called anytime the member list changes. 441 | * 442 | * @param {(users: any[]) => void} callback 443 | * @returns {Echo.PresenceChannel} 444 | */ 445 | here(callback: (users: any[]) => void): PresenceChannel; 446 | 447 | /** 448 | * Listen for someone joining the channel. 449 | * 450 | * @param {(user: any) => void} callback 451 | * @returns {Echo.PresenceChannel} 452 | */ 453 | joining(callback: (user: any) => void): PresenceChannel; 454 | 455 | /** 456 | * Listen for someone leaving the channel. 457 | * 458 | * @param {(user: any) => void} callback 459 | * @returns {Echo.PresenceChannel} 460 | */ 461 | leaving(callback: (user: any) => void): PresenceChannel; 462 | } 463 | 464 | interface NullChannel extends Channel { 465 | /** 466 | * Subscribe to a Null channel. 467 | */ 468 | subscribe(): void; 469 | 470 | /** 471 | * Unsubscribe from a Null channel. 472 | */ 473 | unsubscribe(): void; 474 | 475 | /** 476 | * Stop listening for an event on the channel instance. 477 | * 478 | * @param {string} event 479 | * @returns {Echo.NullChannel} 480 | */ 481 | stopListening(event: string): Channel; 482 | 483 | /** 484 | * Bind a channel to an event. 485 | * 486 | * @param {string} event 487 | * @param {Null.EventCallback} callback 488 | * @returns {Echo.NullChannel} 489 | */ 490 | on(event: string, callback: Null.EventCallback): Channel; 491 | } 492 | 493 | interface NullPrivateChannel extends NullChannel, PrivateChannel { 494 | } 495 | 496 | interface NullPresenceChannel extends NullPrivateChannel, PresenceChannel { 497 | } 498 | 499 | interface PusherChannel extends Channel { 500 | /** 501 | * The pusher client instance 502 | */ 503 | pusher: Pusher; 504 | 505 | /** 506 | * The subscription of the channel. 507 | */ 508 | subscription: pusher.Channel; 509 | 510 | /** 511 | * Create a new class instance. 512 | * 513 | * @param {pusher} pusher 514 | * @param {string} name 515 | * @param options 516 | * @returns {Echo.PusherChannel} 517 | */ 518 | (pusher: Pusher, name: string, options: any): PusherChannel; 519 | 520 | /** 521 | * Subscribe to a Pusher channel. 522 | */ 523 | subscribe(): void; 524 | 525 | /** 526 | * Unsubscribe from a Pusher channel. 527 | */ 528 | unsubscribe(): void; 529 | 530 | /** 531 | * Stop listening for an event on the channel instance. 532 | * 533 | * @param {string} event 534 | * @returns {Echo.PusherChannel} 535 | */ 536 | stopListening(event: string): Channel; 537 | 538 | /** 539 | * Bind a channel to an event. 540 | * 541 | * @param {string} event 542 | * @param {pusher.EventCallback} callback 543 | * @returns {Echo.PusherChannel} 544 | */ 545 | on(event: string, callback: pusher.EventCallback): Channel; 546 | } 547 | 548 | interface PusherPrivateChannel extends PusherChannel, PrivateChannel { 549 | } 550 | 551 | interface PusherPresenceChannel extends PusherPrivateChannel, PresenceChannel { 552 | } 553 | 554 | interface SocketIoChannel extends Channel { 555 | /** 556 | * The SocketIo client instance 557 | */ 558 | socket: io; 559 | 560 | /** 561 | * The event callbacks applied to the channel. 562 | */ 563 | events: any; 564 | 565 | /** 566 | * Create a new class instance. 567 | * 568 | * @param {io} socket 569 | * @param {string} name 570 | * @param options 571 | * @returns {Echo.SocketIoChannel} 572 | */ 573 | (socket: io, name: string, options: any): SocketIoChannel; 574 | 575 | /** 576 | * Subscribe to a SocketIo channel. 577 | */ 578 | subscribe(): void; 579 | 580 | /** 581 | * Unsubscribe from a SocketIo channel. 582 | */ 583 | unsubscribe(): void; 584 | 585 | /** 586 | * Bind a channel to an event. 587 | * 588 | * @param {string} event 589 | * @param {(event: any) => void} callback 590 | * @returns {Echo.SocketIoChannel} 591 | */ 592 | on(event: string, callback: (event: any) => void): SocketIoChannel; 593 | 594 | /** 595 | * Attach a 'reconnect' listener and bind the event. 596 | */ 597 | configureReconnector(): void; 598 | 599 | /** 600 | * Bind the channel's socket to an event and store the callback. 601 | * 602 | * @param {string} event 603 | * @param {(event: any) => void} callback 604 | * @returns {Echo.SocketIoChannel} 605 | */ 606 | bind(event: string, callback: (event: any) => void): SocketIoChannel; 607 | 608 | /** 609 | * Unbind the channel's socket from all stored event callbacks. 610 | */ 611 | unbind(): void; 612 | } 613 | 614 | interface SocketIoPrivateChannel extends SocketIoChannel, PrivateChannel { 615 | } 616 | 617 | interface SocketIoPresenceChannel extends SocketIoPrivateChannel, PresenceChannel { 618 | } 619 | 620 | interface EventFormatter { 621 | /** 622 | * Event namespace. 623 | */ 624 | namespace: string | boolean; 625 | 626 | /** 627 | * Create a new class instance. 628 | * 629 | * @param {string | boolean} namespace 630 | * @returns {Echo.EventFormatter} 631 | */ 632 | (namespace: string | boolean): EventFormatter; 633 | 634 | /** 635 | * Format the given event name. 636 | * 637 | * @param {string} event 638 | * @returns {string} 639 | */ 640 | format(event: string): string; 641 | 642 | /** 643 | * Set the event namespace. 644 | * 645 | * @param {string | boolean} value 646 | */ 647 | setNamespace(value: string | boolean): void; 648 | } 649 | } 650 | 651 | declare var Echo: Echo.EchoStatic; 652 | 653 | declare module 'laravel-echo' { 654 | export default Echo; 655 | } 656 | -------------------------------------------------------------------------------- /src/lib/src/module.ts: -------------------------------------------------------------------------------- 1 | import {CommonModule} from '@angular/common'; 2 | import {HTTP_INTERCEPTORS} from '@angular/common/http'; 3 | import {ModuleWithProviders, NgModule} from '@angular/core'; 4 | import {EchoInterceptor} from './services/interceptor.service'; 5 | import {ECHO_CONFIG, EchoConfig, EchoService} from './services/lib.service'; 6 | 7 | /** 8 | * Module definition, use [[forRoot]] for easy configuration 9 | * of the service and interceptor 10 | */ 11 | @NgModule({ 12 | imports: [CommonModule], 13 | }) 14 | export class AngularLaravelEchoModule { 15 | /** 16 | * Make the service and interceptor available for the current (root) module, it is recommended that this method 17 | * is only called from the root module otherwise multiple instances of the service and interceptor will be created 18 | * (one for each module it is called in) 19 | */ 20 | public static forRoot(config: EchoConfig): ModuleWithProviders { 21 | return { 22 | ngModule: AngularLaravelEchoModule, 23 | providers: [ 24 | EchoService, 25 | {provide: HTTP_INTERCEPTORS, useClass: EchoInterceptor, multi: true}, 26 | {provide: ECHO_CONFIG, useValue: config}, 27 | ] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/src/services/interceptor.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; 2 | import {Injectable} from '@angular/core'; 3 | import {Observable} from 'rxjs'; 4 | import {EchoService} from './lib.service'; 5 | 6 | /** 7 | * An http interceptor to automatically add the socket ID header, use this as something like 8 | * (or use the [[AngularLaravelEchoModule.forRoot]] method): 9 | * 10 | * ```js 11 | * @NgModule({ 12 | * ... 13 | * providers: [ 14 | * ... 15 | * { provide: HTTP_INTERCEPTORS, useClass: EchoInterceptor, multi: true } 16 | * ... 17 | * ] 18 | * ... 19 | * }) 20 | * ``` 21 | */ 22 | @Injectable() 23 | export class EchoInterceptor implements HttpInterceptor { 24 | constructor(private echoService: EchoService) { 25 | } 26 | 27 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 28 | const socketId = this.echoService.socketId; 29 | if (this.echoService.connected && socketId) { 30 | req = req.clone({headers: req.headers.append('X-Socket-ID', socketId)}); 31 | } 32 | 33 | return next.handle(req); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/src/services/lib.service.ts: -------------------------------------------------------------------------------- 1 | import {Inject, Injectable, InjectionToken, NgZone} from '@angular/core'; 2 | import {Observable, of, ReplaySubject, Subject, throwError} from 'rxjs'; 3 | import {distinctUntilChanged, map, shareReplay, startWith} from 'rxjs/operators'; 4 | import Echo from 'laravel-echo'; 5 | import * as io from 'socket.io-client'; 6 | 7 | /** 8 | * The token used to inject the config in Angular's DI system 9 | */ 10 | export const ECHO_CONFIG = new InjectionToken('echo.config'); 11 | 12 | /** 13 | * Service configuration 14 | */ 15 | export interface EchoConfig { 16 | /** 17 | * The name of the user model of the backend application 18 | */ 19 | userModel: string; 20 | /** 21 | * The name of the namespace for notifications of the backend application 22 | */ 23 | notificationNamespace: string | null; 24 | /** 25 | * Laravel Echo configuration 26 | */ 27 | options: Echo.Config; 28 | } 29 | 30 | export interface NullEchoConfig extends EchoConfig { 31 | /** 32 | * Laravel Echo configuration 33 | */ 34 | options: Echo.NullConfig; 35 | } 36 | 37 | export interface PusherEchoConfig extends EchoConfig { 38 | /** 39 | * Laravel Echo configuration 40 | */ 41 | options: Echo.PusherConfig; 42 | } 43 | 44 | export interface SocketIoEchoConfig extends EchoConfig { 45 | /** 46 | * Laravel Echo configuration 47 | */ 48 | options: Echo.SocketIoConfig; 49 | } 50 | 51 | /** 52 | * Possible channel types 53 | */ 54 | export type ChannelType = 'public' | 'presence' | 'private'; 55 | 56 | /** 57 | * Raw events from the underlying connection 58 | */ 59 | export interface ConnectionEvent { 60 | /** 61 | * The event type 62 | */ 63 | type: string; 64 | } 65 | 66 | /** 67 | * Null connection events 68 | */ 69 | export interface NullConnectionEvent extends ConnectionEvent { 70 | /** 71 | * The event type 72 | */ 73 | type: 'connected' 74 | } 75 | 76 | /** 77 | * Socket.io connection events 78 | */ 79 | export interface SocketIoConnectionEvent extends ConnectionEvent { 80 | /** 81 | * The event type 82 | */ 83 | type: 'connect' | 84 | 'connect_error' | 85 | 'connect_timeout' | 86 | 'error' | 87 | 'disconnect' | 88 | 'reconnect' | 89 | 'reconnect_attempt' | 90 | 'reconnecting' | 91 | 'reconnect_error' | 92 | 'reconnect_failed' | 93 | 'ping' | 94 | 'pong'; 95 | } 96 | 97 | /** 98 | * Socket.io disconnect event 99 | */ 100 | export interface SocketIoConnectionDisconnectEvent extends SocketIoConnectionEvent { 101 | /** 102 | * The event type 103 | */ 104 | type: 'disconnect'; 105 | /** 106 | * The reason, either "io server disconnect" or "io client disconnect" 107 | */ 108 | reason: string; 109 | } 110 | 111 | /** 112 | * Socket.io (*_)error event 113 | */ 114 | export interface SocketIoConnectionErrorEvent extends SocketIoConnectionEvent { 115 | /** 116 | * The event type 117 | */ 118 | type: 'connect_error' | 'error' | 'reconnect_error'; 119 | /** 120 | * The error object 121 | */ 122 | error: any; 123 | } 124 | 125 | /** 126 | * Socket.io reconnect event 127 | */ 128 | export interface SocketIoConnectionReconnectEvent extends SocketIoConnectionEvent { 129 | /** 130 | * The event type 131 | */ 132 | type: 'reconnect' | 'reconnect_attempt' | 'reconnecting'; 133 | /** 134 | * The current attempt count 135 | */ 136 | attemptNumber: number; 137 | } 138 | 139 | /** 140 | * Socket.io timeout event 141 | */ 142 | export interface SocketIoConnectionTimeoutEvent extends SocketIoConnectionEvent { 143 | /** 144 | * The event type 145 | */ 146 | type: 'connect_timeout'; 147 | /** 148 | * The timeout 149 | */ 150 | timeout: number; 151 | } 152 | 153 | /** 154 | * Socket.io pong event 155 | */ 156 | export interface SocketIoConnectionPongEvent extends SocketIoConnectionEvent { 157 | /** 158 | * The event type 159 | */ 160 | type: 'pong'; 161 | /** 162 | * The latency 163 | */ 164 | latency: number; 165 | } 166 | 167 | /** 168 | * All Socket.io events 169 | */ 170 | export type SocketIoConnectionEvents = SocketIoConnectionEvent | 171 | SocketIoConnectionDisconnectEvent | 172 | SocketIoConnectionErrorEvent | 173 | SocketIoConnectionReconnectEvent | 174 | SocketIoConnectionTimeoutEvent | 175 | SocketIoConnectionPongEvent; 176 | 177 | /** 178 | * Pusher connection states 179 | */ 180 | export type PusherStates = 'initialized' | 181 | 'connecting' | 182 | 'connected' | 183 | 'unavailable' | 184 | 'failed' | 185 | 'disconnected'; 186 | 187 | /** 188 | * Pusher connection events 189 | */ 190 | export interface PusherConnectionEvent { 191 | type: PusherStates | 'connecting_in'; 192 | } 193 | 194 | /** 195 | * Pusher connecting in event 196 | */ 197 | export interface PusherConnectionConnectingInEvent extends PusherConnectionEvent { 198 | type: 'connecting_in'; 199 | delay: number; 200 | } 201 | 202 | /** 203 | * All pusher events 204 | */ 205 | export type PusherConnectionEvents = PusherConnectionEvent | PusherConnectionConnectingInEvent; 206 | 207 | /** 208 | * All connection events 209 | */ 210 | export type ConnectionEvents = NullConnectionEvent | SocketIoConnectionEvents | PusherConnectionEvents; 211 | 212 | /** 213 | * @hidden 214 | */ 215 | interface Channel { 216 | name: string; 217 | channel: Echo.Channel; 218 | type: ChannelType; 219 | listeners: { 220 | [key: string]: Subject; 221 | }; 222 | notificationListeners?: { 223 | [key: string]: Subject; 224 | }; 225 | users?: any[] | null; 226 | } 227 | 228 | /** 229 | * @hidden 230 | */ 231 | class TypeFormatter { 232 | /** 233 | * The namespace of the notifications. 234 | */ 235 | private namespace: string | null = null; 236 | 237 | /** 238 | * Constructs a new formatter instance 239 | * 240 | * @param namespace The namespace of the notifications. 241 | */ 242 | constructor(namespace: string | null) { 243 | this.setNamespace(namespace); 244 | } 245 | 246 | /** 247 | * Formats the supplied type 248 | * 249 | * @param notificationType The FQN of the notification class 250 | * @returns The optimized type 251 | */ 252 | format(notificationType: string): string { 253 | if (!this.namespace) { 254 | return notificationType; 255 | } 256 | 257 | if (notificationType.indexOf(this.namespace) === 0) { 258 | return notificationType.substr(this.namespace.length); 259 | } 260 | 261 | return notificationType; 262 | } 263 | 264 | /** 265 | * Sets the namespace 266 | * 267 | * @param namespace The namespace of the notifications. 268 | * @returns The instance for chaining 269 | */ 270 | setNamespace(namespace: string | null): TypeFormatter { 271 | this.namespace = namespace; 272 | 273 | return this; 274 | } 275 | } 276 | 277 | /** 278 | * The service class, use this as something like 279 | * (or use the [[AngularLaravelEchoModule.forRoot]] method): 280 | * 281 | * ```js 282 | * export const echoConfig: SocketIoEchoConfig = { 283 | * userModel: 'App.User', 284 | * notificationNamespace: 'App\\Notifications', 285 | * options: { 286 | * broadcaster: 'socket.io', 287 | * host: window.location.hostname + ':6001' 288 | * } 289 | * } 290 | * 291 | * @NgModule({ 292 | * ... 293 | * providers: [ 294 | * ... 295 | * EchoService, 296 | * { provide: ECHO_CONFIG, useValue: echoConfig } 297 | * ... 298 | * ] 299 | * ... 300 | * }) 301 | * ``` 302 | * 303 | * and import it in your component as 304 | * 305 | * ```js 306 | * @Component({ 307 | * ... 308 | * }) 309 | * export class ExampleComponent { 310 | * constructor(echoService: EchoService) { 311 | * } 312 | * } 313 | * ``` 314 | */ 315 | @Injectable() 316 | export class EchoService { 317 | private readonly _echo: Echo.EchoStatic; 318 | private readonly options: Echo.Config; 319 | private readonly typeFormatter: TypeFormatter; 320 | private readonly connected$: Observable; 321 | private readonly connectionState$: Observable; 322 | 323 | private readonly channels: Array = []; 324 | private readonly notificationListeners: { [key: string]: Subject } = {}; 325 | 326 | private userChannelName: string | null = null; 327 | 328 | /** 329 | * Create a new service instance. 330 | * 331 | * @param ngZone NgZone instance 332 | * @param config Service configuration 333 | */ 334 | constructor(private ngZone: NgZone, 335 | @Inject(ECHO_CONFIG) private config: EchoConfig) { 336 | let options = Object.assign({}, config.options); 337 | if (options.broadcaster === 'socket.io') { 338 | options = Object.assign({ 339 | client: io 340 | }, options); 341 | } 342 | 343 | this._echo = new Echo(options); 344 | 345 | this.options = this.echo.connector.options; 346 | 347 | this.typeFormatter = new TypeFormatter(config.notificationNamespace); 348 | 349 | switch (options.broadcaster) { 350 | case 'null': 351 | this.connectionState$ = of({type: 'connected'}); 352 | break; 353 | case 'socket.io': 354 | this.connectionState$ = new Observable(subscriber => { 355 | const socket = (this._echo.connector).socket; 356 | 357 | const handleConnect = () => this.ngZone.run( 358 | () => subscriber.next({type: 'connect'}) 359 | ); 360 | 361 | const handleConnectError = (error: any) => this.ngZone.run( 362 | () => subscriber.next({type: 'connect_error', error}) 363 | ); 364 | 365 | const handleConnectTimeout = (timeout: number) => this.ngZone.run( 366 | () => subscriber.next({type: 'connect_timeout', timeout}) 367 | ); 368 | 369 | const handleError = (error: any) => this.ngZone.run( 370 | () => subscriber.next({type: 'error', error}) 371 | ); 372 | 373 | const handleDisconnect = (reason: string) => this.ngZone.run( 374 | () => subscriber.next({type: 'disconnect', reason}) 375 | ); 376 | 377 | const handleReconnect = (attemptNumber: number) => this.ngZone.run( 378 | () => subscriber.next({type: 'reconnect', attemptNumber}) 379 | ); 380 | 381 | const handleReconnectAttempt = (attemptNumber: number) => this.ngZone.run( 382 | () => subscriber.next({type: 'reconnect_attempt', attemptNumber}) 383 | ); 384 | 385 | const handleReconnecting = (attemptNumber: number) => this.ngZone.run( 386 | () => subscriber.next({type: 'reconnecting', attemptNumber}) 387 | ); 388 | 389 | const handleReconnectError = (error: any) => this.ngZone.run( 390 | () => subscriber.next({type: 'reconnect_error', error}) 391 | ); 392 | 393 | const handleReconnectFailed = () => this.ngZone.run( 394 | () => subscriber.next({type: 'reconnect_failed'}) 395 | ); 396 | 397 | const handlePing = () => this.ngZone.run( 398 | () => subscriber.next({type: 'ping'}) 399 | ); 400 | 401 | const handlePong = (latency: number) => this.ngZone.run( 402 | () => subscriber.next({type: 'pong', latency}) 403 | ); 404 | 405 | socket.on('connect', handleConnect); 406 | socket.on('connect_error', handleConnectError); 407 | socket.on('connect_timeout', handleConnectTimeout); 408 | socket.on('error', handleError); 409 | socket.on('disconnect', handleDisconnect); 410 | socket.on('reconnect', handleReconnect); 411 | socket.on('reconnect_attempt', handleReconnectAttempt); 412 | socket.on('reconnecting', handleReconnecting); 413 | socket.on('reconnect_error', handleReconnectError); 414 | socket.on('reconnect_failed', handleReconnectFailed); 415 | socket.on('ping', handlePing); 416 | socket.on('pong', handlePong); 417 | 418 | return () => { 419 | socket.off('connect', handleConnect); 420 | socket.off('connect_error', handleConnectError); 421 | socket.off('connect_timeout', handleConnectTimeout); 422 | socket.off('error', handleError); 423 | socket.off('disconnect', handleDisconnect); 424 | socket.off('reconnect', handleReconnect); 425 | socket.off('reconnect_attempt', handleReconnectAttempt); 426 | socket.off('reconnecting', handleReconnecting); 427 | socket.off('reconnect_error', handleReconnectError); 428 | socket.off('reconnect_failed', handleReconnectFailed); 429 | socket.off('ping', handlePing); 430 | socket.off('pong', handlePong); 431 | }; 432 | }).pipe(shareReplay(1)); 433 | break; 434 | case 'pusher': 435 | this.connectionState$ = new Observable(subscriber => { 436 | const socket = (this._echo.connector).pusher.connection; 437 | 438 | const handleStateChange = ({current}: { current: PusherStates }) => this.ngZone.run( 439 | () => subscriber.next({type: current}) 440 | ); 441 | 442 | const handleConnectingIn = (delay: number) => this.ngZone.run( 443 | () => subscriber.next({type: 'connecting_in', delay}) 444 | ); 445 | 446 | socket.bind('state_change', handleStateChange); 447 | socket.bind('connecting_in', handleConnectingIn); 448 | 449 | return () => { 450 | socket.unbind('state_change', handleStateChange); 451 | socket.unbind('connecting_in', handleConnectingIn); 452 | }; 453 | }).pipe(shareReplay(1)); 454 | break; 455 | default: 456 | this.connectionState$ = throwError(new Error('unsupported')); 457 | break; 458 | } 459 | 460 | this.connected$ = (>this.connectionState$).pipe( 461 | map(() => this.connected), 462 | startWith(this.connected), 463 | distinctUntilChanged(), 464 | shareReplay(1) 465 | ); 466 | } 467 | 468 | /** 469 | * Is the socket currently connected 470 | */ 471 | get connected(): boolean { 472 | if (this.options.broadcaster === 'null') { 473 | // Null broadcaster is always connected 474 | return true; 475 | } 476 | 477 | if (this.options.broadcaster === 'pusher') { 478 | return (this._echo.connector).pusher.connection.state === 'connected'; 479 | } 480 | 481 | return (this._echo.connector).socket.connected; 482 | } 483 | 484 | /** 485 | * Observable of connection state changes, emits true when connected and false when disconnected 486 | */ 487 | get connectionState(): Observable { 488 | return this.connected$; 489 | } 490 | 491 | /** 492 | * Observable of raw events of the underlying connection 493 | */ 494 | get rawConnectionState(): Observable { 495 | return this.connectionState$; 496 | } 497 | 498 | /** 499 | * The echo instance, can be used to implement any custom requirements outside of this service (remember to include NgZone.run calls) 500 | */ 501 | get echo(): Echo.EchoStatic { 502 | return this._echo; 503 | } 504 | 505 | /** 506 | * The socket ID 507 | */ 508 | get socketId(): string { 509 | return this.echo.socketId(); 510 | } 511 | 512 | /** 513 | * Gets the named and optionally typed channel from the channels array if it exists 514 | * 515 | * @param name The name of the channel 516 | * @param type The type of channel to lookup 517 | * @returns The channel if found or null 518 | */ 519 | private getChannelFromArray(name: string, type: ChannelType | null = null): Channel | null { 520 | const channel = this.channels.find(channel => channel.name === name); 521 | if (channel) { 522 | if (type && channel.type !== type) { 523 | throw new Error(`Channel ${name} is not a ${type} channel`); 524 | } 525 | 526 | return channel; 527 | } 528 | 529 | return null; 530 | } 531 | 532 | /** 533 | * Gets the named and optionally typed channel from the channels array or throws if it does not exist 534 | * 535 | * @param name The name of the channel 536 | * @param type The type of channel to lookup 537 | * @returns The channel 538 | */ 539 | private requireChannelFromArray(name: string, type: ChannelType | null = null): Channel { 540 | const channel = this.getChannelFromArray(name, type); 541 | if (!channel) { 542 | if (type) { 543 | throw new Error(`${type[0].toUpperCase()}${type.substr(1)} channel ${name} does not exist`); 544 | } 545 | 546 | throw new Error(`Channel ${name} does not exist`); 547 | } 548 | 549 | return channel; 550 | } 551 | 552 | /** 553 | * Fetch or create a public channel 554 | * 555 | * @param name The name of the channel to join 556 | * @returns The fetched or created channel 557 | */ 558 | private publicChannel(name: string): Echo.Channel { 559 | let channel = this.getChannelFromArray(name, 'public'); 560 | if (channel) { 561 | return channel.channel; 562 | } 563 | 564 | const echoChannel = this.echo.channel(name); 565 | 566 | channel = { 567 | name, 568 | channel: echoChannel, 569 | type: 'public', 570 | listeners: {}, 571 | }; 572 | 573 | this.channels.push(channel); 574 | 575 | return echoChannel; 576 | } 577 | 578 | /** 579 | * Fetch or create a presence channel and subscribe to the presence events 580 | * 581 | * @param name The name of the channel to join 582 | * @returns The fetched or created channel 583 | */ 584 | private presenceChannel(name: string): Echo.PresenceChannel { 585 | let channel = this.getChannelFromArray(name, 'presence'); 586 | if (channel) { 587 | return channel.channel as Echo.PresenceChannel; 588 | } 589 | 590 | const echoChannel = this.echo.join(name); 591 | 592 | channel = { 593 | name, 594 | channel: echoChannel, 595 | type: 'presence', 596 | listeners: {}, 597 | users: null, 598 | }; 599 | 600 | this.channels.push(channel); 601 | 602 | echoChannel.here((users: any[]) => { 603 | this.ngZone.run(() => { 604 | if (channel) { 605 | channel.users = users; 606 | 607 | if (channel.listeners['_users_']) { 608 | channel.listeners['_users_'].next(JSON.parse(JSON.stringify(users))); 609 | } 610 | } 611 | }); 612 | }); 613 | 614 | echoChannel.joining((user: any) => { 615 | this.ngZone.run(() => { 616 | if (channel) { 617 | channel.users = channel.users || []; 618 | channel.users.push(user); 619 | 620 | if (channel.listeners['_joining_']) { 621 | channel.listeners['_joining_'].next(JSON.parse(JSON.stringify(user))); 622 | } 623 | } 624 | }); 625 | }); 626 | 627 | echoChannel.leaving((user: any) => { 628 | this.ngZone.run(() => { 629 | if (channel) { 630 | channel.users = channel.users || []; 631 | 632 | const existing = channel.users.find(existing => existing == user); 633 | if (existing) { 634 | const index = channel.users.indexOf(existing); 635 | 636 | if (index !== -1) { 637 | channel.users.splice(index, 1); 638 | } 639 | } 640 | 641 | if (channel.listeners['_leaving_']) { 642 | channel.listeners['_leaving_'].next(JSON.parse(JSON.stringify(user))); 643 | } 644 | } 645 | }); 646 | }); 647 | 648 | return echoChannel; 649 | } 650 | 651 | /** 652 | * Fetch or create a private channel 653 | * 654 | * @param name The name of the channel to join 655 | * @returns The fetched or created channel 656 | */ 657 | private privateChannel(name: string): Echo.PrivateChannel { 658 | let channel = this.getChannelFromArray(name, 'private'); 659 | if (channel) { 660 | return channel.channel as Echo.PrivateChannel; 661 | } 662 | 663 | const echoChannel = this.echo.private(name); 664 | 665 | channel = { 666 | name, 667 | channel: echoChannel, 668 | type: 'private', 669 | listeners: {}, 670 | }; 671 | 672 | this.channels.push(channel); 673 | 674 | return echoChannel; 675 | } 676 | 677 | /** 678 | * Set authentication data and connect to and start listening for notifications on the users private channel 679 | * 680 | * @param headers Authentication headers to send when talking to the service 681 | * @param userId The current user's id 682 | * @returns The instance for chaining 683 | */ 684 | login(headers: { [key: string]: string }, userId: string | number): EchoService { 685 | const newChannelName = `${this.config.userModel.replace('\\', '.')}.${userId}`; 686 | 687 | if (this.userChannelName && this.userChannelName != newChannelName) { 688 | this.logout(); 689 | } 690 | 691 | this.options.auth = this.options.auth || {}; 692 | this.options.auth.headers = Object.assign({}, headers); 693 | 694 | if (this.options.broadcaster === 'pusher') { 695 | const connector = (this._echo.connector); 696 | 697 | if (connector.pusher.config.auth !== this.options.auth) { 698 | connector.pusher.config.auth = this.options.auth; 699 | } 700 | } 701 | 702 | if (this.userChannelName != newChannelName) { 703 | this.userChannelName = newChannelName; 704 | 705 | this.privateChannel(newChannelName).notification((notification: any) => { 706 | const type = this.typeFormatter.format(notification.type); 707 | 708 | if (this.notificationListeners[type]) { 709 | this.ngZone.run(() => this.notificationListeners[type].next(notification)); 710 | } 711 | 712 | if (this.notificationListeners['*']) { 713 | this.ngZone.run(() => this.notificationListeners['*'].next(notification)); 714 | } 715 | }); 716 | } 717 | 718 | return this; 719 | } 720 | 721 | /** 722 | * Clear authentication data and close any presence or private channels. 723 | * 724 | * @returns The instance for chaining 725 | */ 726 | logout(): EchoService { 727 | this.channels 728 | .filter(channel => channel.type !== 'public') 729 | .forEach(channel => this.leave(channel.name)); 730 | 731 | this.options.auth = this.options.auth || {}; 732 | this.options.auth.headers = {}; 733 | 734 | return this; 735 | } 736 | 737 | /** 738 | * Join a channel of specified name and type. 739 | * 740 | * @param name The name of the channel to join 741 | * @param type The type of channel to join 742 | * @returns The instance for chaining 743 | */ 744 | join(name: string, type: ChannelType): EchoService { 745 | switch (type) { 746 | case 'public': 747 | this.publicChannel(name); 748 | break; 749 | case 'presence': 750 | this.presenceChannel(name); 751 | break; 752 | case 'private': 753 | this.privateChannel(name); 754 | break; 755 | } 756 | 757 | return this; 758 | } 759 | 760 | /** 761 | * Leave a channel of the specified name. 762 | * 763 | * @param name The name of the channel to leave 764 | * @returns The instance for chaining 765 | */ 766 | leave(name: string): EchoService { 767 | const channel = this.getChannelFromArray(name); 768 | if (channel) { 769 | this.echo.leave(name); 770 | 771 | Object.keys(channel.listeners).forEach(key => channel.listeners[key].complete()); 772 | 773 | if (channel.notificationListeners) { 774 | Object.keys(channel.notificationListeners).forEach( 775 | key => channel.notificationListeners && channel.notificationListeners[key].complete() 776 | ); 777 | } 778 | 779 | const index = this.channels.indexOf(channel); 780 | if (index !== -1) { 781 | this.channels.splice(index, 1); 782 | } 783 | } 784 | 785 | return this; 786 | } 787 | 788 | /** 789 | * Listen for events on the specified channel. 790 | * 791 | * @param name The name of the channel 792 | * @param event The name of the event 793 | * @returns An observable that emits the event data of the specified event when it arrives 794 | */ 795 | listen(name: string, event: string): Observable { 796 | const channel = this.requireChannelFromArray(name); 797 | if (!channel.listeners[event]) { 798 | const listener = new Subject(); 799 | 800 | channel.channel.listen(event, (event: any) => this.ngZone.run(() => listener.next(event))); 801 | 802 | channel.listeners[event] = listener; 803 | } 804 | 805 | return channel.listeners[event].asObservable(); 806 | } 807 | 808 | /** 809 | * Listen for client sent events (whispers) on the specified private or presence channel channel. 810 | * 811 | * @param name The name of the channel 812 | * @param event The name of the event 813 | * @returns An observable that emits the whisper data of the specified event when it arrives 814 | */ 815 | listenForWhisper(name: string, event: string): Observable { 816 | const channel = this.requireChannelFromArray(name); 817 | if (channel.type === 'public') { 818 | return throwError(new Error('Whisper is not available on public channels')); 819 | } 820 | 821 | if (!channel.listeners[`_whisper_${event}_`]) { 822 | const listener = new Subject(); 823 | 824 | channel.channel.listenForWhisper(event, (event: any) => this.ngZone.run(() => listener.next(event))); 825 | 826 | channel.listeners[`_whisper_${event}_`] = listener; 827 | } 828 | 829 | return channel.listeners[`_whisper_${event}_`].asObservable(); 830 | } 831 | 832 | /** 833 | * Listen for notifications on the users private channel. 834 | * 835 | * @param type The type of notification to listen for or `*` for any 836 | * @param name Optional a different channel to receive notifications on 837 | * @returns An observable that emits the notification of the specified type when it arrives 838 | */ 839 | notification(type: string, name?: string): Observable { 840 | type = this.typeFormatter.format(type); 841 | 842 | if (name && name !== this.userChannelName) { 843 | const channel = this.requireChannelFromArray(name); 844 | 845 | if (!channel.notificationListeners) { 846 | channel.notificationListeners = {}; 847 | 848 | channel.channel.notification((notification: any) => { 849 | const notificationType = this.typeFormatter.format(notification.type); 850 | 851 | if (channel.notificationListeners) { 852 | if (channel.notificationListeners[notificationType]) { 853 | this.ngZone.run(() => channel.notificationListeners && channel.notificationListeners[notificationType].next(notification)); 854 | } 855 | 856 | if (channel.notificationListeners['*']) { 857 | this.ngZone.run(() => channel.notificationListeners && channel.notificationListeners['*'].next(notification)); 858 | } 859 | } 860 | }); 861 | } 862 | 863 | if (!channel.notificationListeners[type]) { 864 | channel.notificationListeners[type] = new Subject(); 865 | } 866 | 867 | return channel.notificationListeners[type].asObservable(); 868 | } 869 | 870 | if (!this.notificationListeners[type]) { 871 | this.notificationListeners[type] = new Subject(); 872 | } 873 | 874 | return this.notificationListeners[type].asObservable(); 875 | } 876 | 877 | /** 878 | * Listen for users joining the specified presence channel. 879 | * 880 | * @param name The name of the channel 881 | * @returns An observable that emits the user when he joins the specified channel 882 | */ 883 | joining(name: string): Observable { 884 | const channel = this.requireChannelFromArray(name, 'presence'); 885 | 886 | if (!channel.listeners[`_joining_`]) { 887 | channel.listeners['_joining_'] = new Subject(); 888 | } 889 | 890 | return channel.listeners['_joining_'].asObservable(); 891 | } 892 | 893 | /** 894 | * Listen for users leaving the specified presence channel. 895 | * 896 | * @param name The name of the channel 897 | * @returns An observable that emits the user when he leaves the specified channel 898 | */ 899 | leaving(name: string): Observable { 900 | const channel = this.requireChannelFromArray(name, 'presence'); 901 | 902 | if (!channel.listeners[`_leaving_`]) { 903 | channel.listeners['_leaving_'] = new Subject(); 904 | } 905 | 906 | return channel.listeners['_leaving_'].asObservable(); 907 | } 908 | 909 | /** 910 | * Listen for user list updates on the specified presence channel. 911 | * 912 | * @param name The name of the channel 913 | * @returns An observable that emits the initial user list as soon as it's available 914 | */ 915 | users(name: string): Observable { 916 | const channel = this.requireChannelFromArray(name, 'presence'); 917 | 918 | if (!channel.listeners[`_users_`]) { 919 | channel.listeners['_users_'] = new ReplaySubject(1); 920 | } 921 | 922 | return channel.listeners['_users_'].asObservable(); 923 | } 924 | 925 | /** 926 | * Send a client event to the specified presence or private channel (whisper). 927 | * 928 | * @param name The name of the channel 929 | * @param event The name of the event 930 | * @param data The payload for the event 931 | * @returns The instance for chaining 932 | */ 933 | whisper(name: string, event: string, data: any): EchoService { 934 | const channel = this.requireChannelFromArray(name); 935 | if (channel.type === 'public') { 936 | throw new Error('Whisper is not available on public channels'); 937 | } 938 | 939 | const echoChannel = channel.channel as Echo.PrivateChannel; 940 | 941 | echoChannel.whisper(event, data); 942 | 943 | return this; 944 | } 945 | } 946 | -------------------------------------------------------------------------------- /src/lib/src/types.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ECHO_CONFIG, 3 | NullEchoConfig, 4 | PusherEchoConfig, 5 | SocketIoEchoConfig, 6 | ConnectionEvents, 7 | NullConnectionEvent, 8 | PusherStates, 9 | PusherConnectionEvent, 10 | PusherConnectionConnectingInEvent, 11 | PusherConnectionEvents, 12 | SocketIoConnectionEvent, 13 | SocketIoConnectionDisconnectEvent, 14 | SocketIoConnectionErrorEvent, 15 | SocketIoConnectionReconnectEvent, 16 | SocketIoConnectionTimeoutEvent, 17 | SocketIoConnectionPongEvent, 18 | SocketIoConnectionEvents 19 | } from './services/lib.service'; 20 | -------------------------------------------------------------------------------- /src/lib/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "outDir": "../../out-tsc/lib-es5/", 6 | "baseUrl": "", 7 | "types": [] 8 | }, 9 | "files": [ 10 | "./index.ts" 11 | ], 12 | "include": [ 13 | "./*.d.ts", 14 | "./**/*.d.ts" 15 | ], 16 | "angularCompilerOptions": { 17 | "annotateForClosureCompiler": true, 18 | "strictMetadataEmit": true, 19 | "skipTemplateCodegen": true, 20 | "flatModuleOutFile": "angular-laravel-echo.js", 21 | "flatModuleId": "angular-laravel-echo", 22 | "genDir": "../../out-tsc/lib-gen-dir/" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib-es2015/", 5 | "target": "es2015", 6 | "rootDir": "./", 7 | "baseUrl": "", 8 | "types": [] 9 | }, 10 | "files": [ 11 | "./index.ts" 12 | ], 13 | "include": [ 14 | "./*.d.ts", 15 | "./**/*.d.ts" 16 | ], 17 | "angularCompilerOptions": { 18 | "annotateForClosureCompiler": true, 19 | "strictMetadataEmit": true, 20 | "skipTemplateCodegen": true, 21 | "flatModuleOutFile": "angular-laravel-echo.js", 22 | "flatModuleId": "angular-laravel-echo", 23 | "genDir": "../../out-tsc/lib-gen-dir/" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "", 5 | "module": "commonjs", 6 | "declaration": false, 7 | "emitDecoratorMetadata": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "inlineSources": true, 8 | "declaration": true, 9 | "experimentalDecorators": true, 10 | "noImplicitAny": true, 11 | "suppressImplicitAnyIndexErrors": true, 12 | "skipLibCheck": true, 13 | "stripInternal": true, 14 | "lib": [ 15 | "es2015", 16 | "dom" 17 | ], 18 | "strict": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "curly": true, 9 | "eofline": true, 10 | "forin": true, 11 | "indent": [ 12 | true, 13 | "spaces" 14 | ], 15 | "label-position": true, 16 | "max-line-length": [ 17 | true, 18 | 140 19 | ], 20 | "member-access": false, 21 | "member-ordering": [ 22 | true, 23 | "static-before-instance", 24 | "variables-before-functions" 25 | ], 26 | "no-arg": true, 27 | "no-bitwise": true, 28 | "no-console": [ 29 | true, 30 | "debug", 31 | "info", 32 | "time", 33 | "timeEnd", 34 | "trace" 35 | ], 36 | "no-construct": true, 37 | "no-debugger": true, 38 | "no-duplicate-variable": true, 39 | "no-empty": false, 40 | "no-eval": true, 41 | "no-inferrable-types": [true, "ignore-params"], 42 | "no-shadowed-variable": true, 43 | "no-string-literal": false, 44 | "no-switch-case-fall-through": true, 45 | "no-trailing-whitespace": true, 46 | "no-unused-expression": true, 47 | "no-use-before-declare": true, 48 | "no-var-keyword": true, 49 | "object-literal-sort-keys": false, 50 | "one-line": [ 51 | true, 52 | "check-open-brace", 53 | "check-catch", 54 | "check-else", 55 | "check-whitespace" 56 | ], 57 | "quotemark": [ 58 | true, 59 | "single" 60 | ], 61 | "radix": true, 62 | "semicolon": [ 63 | "always" 64 | ], 65 | "triple-equals": [ 66 | true, 67 | "allow-null-check" 68 | ], 69 | "typedef-whitespace": [ 70 | true, 71 | { 72 | "call-signature": "nospace", 73 | "index-signature": "nospace", 74 | "parameter": "nospace", 75 | "property-declaration": "nospace", 76 | "variable-declaration": "nospace" 77 | } 78 | ], 79 | "variable-name": false, 80 | "whitespace": [ 81 | true, 82 | "check-branch", 83 | "check-decl", 84 | "check-operator", 85 | "check-separator", 86 | "check-type" 87 | ] 88 | } 89 | } 90 | --------------------------------------------------------------------------------