├── .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 |
--------------------------------------------------------------------------------