├── typings
└── index.d.ts
├── jest.config.js
├── codecov.yml
├── .eslintrc.js
├── phpstan.neon.dist
├── config
└── laravel-echo-api-gateway.php
├── phpunit.xml
├── tsconfig.json
├── LICENSE.md
├── rollup.config.js
├── src
├── ServiceProvider.php
├── ConnectionRepository.php
├── Commands
│ └── VaporHandle.php
├── SubscriptionRepository.php
├── Driver.php
└── Handler.php
├── composer.json
├── package.json
├── js-src
├── Connector.ts
├── Channel.ts
└── Websocket.ts
├── js-tests
└── Connector.test.ts
└── README.md
/typings/index.d.ts:
--------------------------------------------------------------------------------
1 | declare const axios: import('axios').AxiosStatic;
2 | declare const Pusher: any;
3 | declare const io: any;
4 | declare const Vue: any;
5 | declare const jQuery: any;
6 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | transform: {
3 | '^.+\\.tsx?$': 'ts-jest',
4 | },
5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node', 'd.ts'],
6 | transformIgnorePatterns: [
7 | 'node_modules/(?!(laravel-echo)/)',
8 | ],
9 | };
10 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project:
4 | default:
5 | target: 25%
6 | pest:
7 | target: 25%
8 | flags:
9 | - pest
10 | jest:
11 | target: 25%
12 | flags:
13 | - jest
14 |
15 | flags:
16 | pest:
17 | paths:
18 | - src/
19 | carryforward: false
20 | jest:
21 | paths:
22 | - js-src/
23 | carryforward: false
24 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | browser: true,
5 | jest: true,
6 | node: true,
7 | },
8 | parser: "@typescript-eslint/parser",
9 | plugins: ["@typescript-eslint"],
10 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
11 | rules: {
12 | "@typescript-eslint/ban-types": "off",
13 | "@typescript-eslint/no-explicit-any": "off",
14 | "prefer-const": "off",
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/phpstan.neon.dist:
--------------------------------------------------------------------------------
1 | includes:
2 | - ./vendor/nunomaduro/larastan/extension.neon
3 |
4 | parameters:
5 |
6 | phpVersion: 80399
7 |
8 | paths:
9 | - src
10 | - tests
11 |
12 | level: max
13 |
14 | ignoreErrors:
15 | - '#^Cannot call method debug\(\) on Illuminate\\Log\\LogManager\|null.$#'
16 | - '#^Call to an undefined method Mockery\\ExpectationInterface\|Mockery\\HigherOrderMessage::withArgs\(\).$#'
17 | #
18 | # excludePaths:
19 | # - ./*/*/FileToBeExcluded.php
20 |
21 | checkMissingIterableValueType: false
22 |
--------------------------------------------------------------------------------
/config/laravel-echo-api-gateway.php:
--------------------------------------------------------------------------------
1 | [
6 | 'key' => env('AWS_ACCESS_KEY_ID'),
7 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
8 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
9 | ],
10 |
11 | 'api' => [
12 | 'id' => env('LARAVEL_ECHO_API_GATEWAY_API_ID'),
13 | 'stage' => env('LARAVEL_ECHO_API_GATEWAY_API_STAGE'),
14 | ],
15 |
16 | 'dynamodb' => [
17 | 'endpoint' => env('DYNAMODB_ENDPOINT'),
18 | 'table' => env('LARAVEL_ECHO_API_GATEWAY_DYNAMODB_TABLE', 'connections'),
19 | ],
20 |
21 | ];
22 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | ./tests
10 |
11 |
12 |
13 |
14 | ./src
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "module": "es6",
5 | "moduleResolution": "node",
6 | "outDir": "./dist",
7 | "sourceMap": false,
8 | "target": "es6",
9 | "isolatedModules": false,
10 | "esModuleInterop": true,
11 | "preserveSymlinks": true,
12 | "typeRoots": [
13 | "node_modules/@types"
14 | ],
15 | "lib": [
16 | "es2017",
17 | "dom"
18 | ]
19 | },
20 | "include": [
21 | "./typings/",
22 | "./js-src/",
23 | "./node_modules/laravel-echo/src/"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) George Boot
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 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from '@rollup/plugin-babel';
2 | import typescript from 'rollup-plugin-typescript2';
3 |
4 | export default {
5 | input: './js-src/Connector.ts',
6 | output: [
7 | { file: './dist/laravel-echo-api-gateway.js', format: 'esm' },
8 | { file: './dist/laravel-echo-api-gateway.common.js', format: 'cjs' },
9 | { file: './dist/laravel-echo-api-gateway.iife.js', format: 'iife', name: 'LaravelEchoApiGateway' },
10 | ],
11 | plugins: [
12 | typescript({
13 | tsconfig: './tsconfig.json', // Chemin explicite vers votre fichier TypeScript
14 | useTsconfigDeclarationDir: true, // Respecte les options `outDir` et `declarationDir`
15 | }),
16 | babel({
17 | babelHelpers: 'bundled',
18 | exclude: 'node_modules/**',
19 | extensions: ['.ts'],
20 | presets: ['@babel/preset-env'],
21 | plugins: [
22 | ['@babel/plugin-proposal-decorators', { legacy: true }],
23 | '@babel/plugin-proposal-function-sent',
24 | '@babel/plugin-proposal-export-namespace-from',
25 | '@babel/plugin-proposal-numeric-separator',
26 | '@babel/plugin-proposal-throw-expressions',
27 | '@babel/plugin-transform-object-assign',
28 | ],
29 | }),
30 | ],
31 | };
32 |
--------------------------------------------------------------------------------
/src/ServiceProvider.php:
--------------------------------------------------------------------------------
1 | mergeConfigFrom(
15 | __DIR__ . '/../config/laravel-echo-api-gateway.php',
16 | 'laravel-echo-api-gateway'
17 | );
18 |
19 | Config::set('broadcasting.connections.laravel-echo-api-gateway', [
20 | 'driver' => 'laravel-echo-api-gateway',
21 | ]);
22 |
23 | $config = config('laravel-echo-api-gateway');
24 |
25 | $subscriptionRepository = new SubscriptionRepository($config);
26 |
27 | $this->app->bind(SubscriptionRepository::class, fn () => $subscriptionRepository);
28 | $this->app->bind(ConnectionRepository::class, fn () => new ConnectionRepository($subscriptionRepository, $config));
29 | }
30 |
31 | public function boot(BroadcastManager $broadcastManager): void
32 | {
33 | $broadcastManager->extend('laravel-echo-api-gateway', fn () => $this->app->make(Driver::class));
34 |
35 | $this->commands([
36 | VaporHandle::class,
37 | ]);
38 |
39 | $this->publishes([
40 | __DIR__ . '/../config/laravel-echo-api-gateway.php' => config_path('laravel-echo-api-gateway.php'),
41 | ], 'laravel-echo-api-gateway-config');
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/ConnectionRepository.php:
--------------------------------------------------------------------------------
1 | subscriptionRepository = $subscriptionRepository;
19 |
20 | $this->apiGatewayManagementApiClient = new ApiGatewayManagementApiClient(array_merge($config['connection'], [
21 | 'version' => '2018-11-29',
22 | 'endpoint' => "https://{$config['api']['id']}.execute-api.{$config['connection']['region']}.amazonaws.com/{$config['api']['stage']}/",
23 | ]));
24 | }
25 |
26 | public function sendMessage(string $connectionId, string $data): void
27 | {
28 | try {
29 | $this->apiGatewayManagementApiClient->postToConnection([
30 | 'ConnectionId' => $connectionId,
31 | 'Data' => $data,
32 | ]);
33 | } catch (ApiGatewayManagementApiException $e) {
34 | // GoneException: The connection with the provided id no longer exists
35 | if (
36 | $e->getStatusCode() === Response::HTTP_GONE ||
37 | $e->getAwsErrorCode() === 'GoneException'
38 | ) {
39 | $this->subscriptionRepository->clearConnection($connectionId);
40 |
41 | return;
42 | }
43 |
44 | throw $e;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "georgeboot/laravel-echo-api-gateway",
3 | "description": "Use Laravel Echo with API Gateway Websockets",
4 | "keywords": [
5 | "laravel",
6 | "echo",
7 | "websockets",
8 | "bref",
9 | "serverless",
10 | "laravel vapor"
11 | ],
12 | "homepage": "https://github.com/georgeboot/laravel-echo-api-gateway",
13 | "type": "library",
14 | "license": "MIT",
15 | "authors": [
16 | {
17 | "name": "George Boot",
18 | "email": "george@famboot.nl"
19 | }
20 | ],
21 | "require": {
22 | "php": "^8.0|^8.1|^8.2",
23 | "ext-json": "*",
24 | "aws/aws-sdk-php": "^3.308",
25 | "bref/bref": "^1.1|^2.0",
26 | "guzzlehttp/guzzle": "^6.3|^7.0",
27 | "laravel/framework": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0"
28 | },
29 | "require-dev": {
30 | "mockery/mockery": "^1.2",
31 | "nunomaduro/larastan": "^2.5",
32 | "orchestra/testbench": "^7.33.0",
33 | "pestphp/pest": "^1.0",
34 | "phpunit/phpunit": "^8.0|^9.0"
35 | },
36 | "autoload": {
37 | "psr-4": {
38 | "Georgeboot\\LaravelEchoApiGateway\\": "src/"
39 | }
40 | },
41 | "autoload-dev": {
42 | "psr-4": {
43 | "Tests\\": "tests/"
44 | }
45 | },
46 | "scripts": {
47 | "test": "vendor/bin/phpunit"
48 | },
49 | "extra": {
50 | "laravel": {
51 | "providers": [
52 | "Georgeboot\\LaravelEchoApiGateway\\ServiceProvider"
53 | ]
54 | }
55 | },
56 | "config": {
57 | "sort-packages": true,
58 | "platform": {
59 | "php": "8.0.12"
60 | },
61 | "allow-plugins": {
62 | "pestphp/pest-plugin": true
63 | }
64 | },
65 | "minimum-stability": "dev",
66 | "prefer-stable": true
67 | }
68 |
--------------------------------------------------------------------------------
/src/Commands/VaporHandle.php:
--------------------------------------------------------------------------------
1 | websocketHandler = $websocketHandler;
35 | }
36 |
37 | /**
38 | * Execute the console command.
39 | *
40 | * @return int
41 | */
42 | public function handle()
43 | {
44 | if ($this->laravel->isDownForMaintenance()) {
45 | return 0;
46 | }
47 |
48 | // fake a context
49 | $context = new Context($_ENV['AWS_REQUEST_ID'] ?? 'request-1', 0, $_ENV['AWS_LAMBDA_FUNCTION_NAME'] ?? 'arn-1', $_ENV['_X_AMZN_TRACE_ID'] ?? '');
50 |
51 | if (Arr::get($this->message(), 'requestContext.connectionId')) {
52 | $this->handleWebsocketEvent($this->message(), $context);
53 | }
54 |
55 | return 0;
56 | }
57 |
58 | protected function handleWebsocketEvent(array $event, Context $context): void
59 | {
60 | $this->websocketHandler->handle($event, $context);
61 | }
62 |
63 | /**
64 | * Get the decoded message payload.
65 | *
66 | * @return array
67 | */
68 | protected function message()
69 | {
70 | /** @var string $message */
71 | $message = $this->argument('message');
72 |
73 | return tap(json_decode(base64_decode($message), true), function ($message) {
74 | if ($message === false) {
75 | throw new InvalidArgumentException('Unable to unserialize message.');
76 | }
77 | });
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "laravel-echo-api-gateway",
3 | "version": "0.5.5",
4 | "description": "Use Laravel Echo with API Gateway Websockets",
5 | "keywords": [
6 | "laravel",
7 | "echo",
8 | "websockets",
9 | "bref",
10 | "serverless",
11 | "laravel vapor"
12 | ],
13 | "homepage": "https://github.com/georgeboot/laravel-echo-api-gateway",
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/georgeboot/laravel-echo-api-gateway"
17 | },
18 | "author": {
19 | "name": "George Boot"
20 | },
21 | "license": "MIT",
22 | "main": "dist/laravel-echo-api-gateway.common.js",
23 | "module": "dist/laravel-echo-api-gateway.js",
24 | "types": "dist/laravel-echo-api-gateway.d.ts",
25 | "scripts": {
26 | "build": "npm run compile && npm run declarations",
27 | "compile": "./node_modules/.bin/rollup -c",
28 | "declarations": "./node_modules/.bin/tsc --emitDeclarationOnly",
29 | "lint": "eslint --ext .js,.ts ./js-src ./js-tests",
30 | "prepublish": "npm run build",
31 | "release": "npm run test && standard-version && git push --follow-tags && npm publish",
32 | "test": "jest"
33 | },
34 | "devDependencies": {
35 | "@babel/plugin-proposal-decorators": "^7.8.3",
36 | "@babel/plugin-proposal-export-namespace-from": "^7.8.3",
37 | "@babel/plugin-proposal-function-sent": "^7.8.3",
38 | "@babel/plugin-proposal-numeric-separator": "^7.8.3",
39 | "@babel/plugin-proposal-throw-expressions": "^7.8.3",
40 | "@babel/plugin-transform-object-assign": "^7.8.3",
41 | "@babel/preset-env": "^7.9.6",
42 | "@rollup/plugin-babel": "^5.0.0",
43 | "@types/babel__traverse": "^7.20.6",
44 | "@types/jest": "^24.0.18",
45 | "@types/node": "^12.7.5",
46 | "@typescript-eslint/eslint-plugin": "^3.7.0",
47 | "@typescript-eslint/parser": "^3.7.0",
48 | "axios": "^0.21.1",
49 | "eslint": "^7.5.0",
50 | "jest": "^24.9.0",
51 | "jest-websocket-mock": "^2.2.0",
52 | "laravel-echo": "1.10.0",
53 | "mock-socket": "^9.0.3",
54 | "rollup": "^2.10.2",
55 | "rollup-plugin-commonjs": "^10.1.0",
56 | "rollup-plugin-json": "^4.0.0",
57 | "rollup-plugin-node-resolve": "^5.2.0",
58 | "rollup-plugin-typescript2": "^0.27.1",
59 | "standard-version": "^8.0.1",
60 | "ts-jest": "^24.1.0",
61 | "tslib": "^1.10.0",
62 | "typescript": "^5.7.2"
63 | },
64 | "engines": {
65 | "node": ">=10"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/SubscriptionRepository.php:
--------------------------------------------------------------------------------
1 | dynamoDb = new DynamoDbClient(array_merge($config['connection'], [
18 | 'version' => '2012-08-10',
19 | 'endpoint' => $config['dynamodb']['endpoint'],
20 | ]));
21 |
22 | $this->table = $config['dynamodb']['table'];
23 | }
24 |
25 | public function getConnectionIdsForChannel(string ...$channels): Collection
26 | {
27 | $promises = collect($channels)->map(fn ($channel) => $this->dynamoDb->queryAsync([
28 | 'TableName' => $this->table,
29 | 'IndexName' => 'lookup-by-channel',
30 | 'KeyConditionExpression' => 'channel = :channel',
31 | 'ExpressionAttributeValues' => [
32 | ':channel' => ['S' => $channel],
33 | ],
34 | ]))->toArray();
35 |
36 | $responses = Utils::all($promises)->wait();
37 |
38 | return collect($responses)
39 | ->flatmap(fn (\Aws\Result $result): array => $result['Items'])
40 | ->map(fn (array $item): string => $item['connectionId']['S'])
41 | ->unique();
42 | }
43 |
44 | public function clearConnection(string $connectionId): void
45 | {
46 | $response = $this->dynamoDb->query([
47 | 'TableName' => $this->table,
48 | 'IndexName' => 'lookup-by-connection',
49 | 'KeyConditionExpression' => 'connectionId = :connectionId',
50 | 'ExpressionAttributeValues' => [
51 | ':connectionId' => ['S' => $connectionId],
52 | ],
53 | ]);
54 |
55 | if (! empty($response['Items'])) {
56 | $this->dynamoDb->batchWriteItem([
57 | 'RequestItems' => [
58 | $this->table => collect($response['Items'])->map(fn ($item) => [
59 | 'DeleteRequest' => [
60 | 'Key' => Arr::only($item, ['connectionId', 'channel']),
61 | ],
62 | ])->toArray(),
63 | ],
64 | ]);
65 | }
66 | }
67 |
68 | public function subscribeToChannel(string $connectionId, string $channel): void
69 | {
70 | $this->dynamoDb->putItem([
71 | 'TableName' => $this->table,
72 | 'Item' => [
73 | 'connectionId' => ['S' => $connectionId],
74 | 'channel' => ['S' => $channel],
75 | ],
76 | ]);
77 | }
78 |
79 | public function unsubscribeFromChannel(string $connectionId, string $channel): void
80 | {
81 | $this->dynamoDb->deleteItem([
82 | 'TableName' => $this->table,
83 | 'Key' => [
84 | 'connectionId' => ['S' => $connectionId],
85 | 'channel' => ['S' => $channel],
86 | ],
87 | ]);
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/js-src/Connector.ts:
--------------------------------------------------------------------------------
1 | import {Connector as BaseConnector} from "laravel-echo/src/connector/connector";
2 | import {Websocket} from "./Websocket";
3 | import {Channel} from "./Channel";
4 |
5 | export const broadcaster = (options: object): Connector => new Connector(options);
6 |
7 | const LOG_PREFIX = '[LE-AG-Connector]';
8 |
9 | export class Connector extends BaseConnector {
10 |
11 | socket: Websocket;
12 |
13 | /**
14 | * All of the subscribed channel names.
15 | */
16 | channels: { [name: string]: Channel } = {};
17 |
18 | /**
19 | * Create a new class instance.
20 | */
21 | constructor(options: any) {
22 | super(options);
23 | }
24 |
25 | /**
26 | * Create a fresh Socket.io connection.
27 | */
28 | connect(): void {
29 | this.options.debug && console.log(LOG_PREFIX + 'Connect ...' );
30 |
31 | this.socket = new Websocket(this.options);
32 |
33 | return;
34 |
35 | //
36 | // this.socket.on('reconnect', () => {
37 | // Object.values(this.channels).forEach((channel) => {
38 | // channel.subscribe();
39 | // });
40 | // });
41 | //
42 | // return this.socket;
43 | }
44 |
45 | /**
46 | * Get a channel instance by name.
47 | */
48 | channel(name: string): Channel {
49 | if (!this.channels[name]) {
50 | this.channels[name] = new Channel(this.socket, name, this.options);
51 | }
52 |
53 | return this.channels[name];
54 | }
55 |
56 | /**
57 | * Get a private channel instance by name.
58 | */
59 | privateChannel(name: string): Channel {
60 | if (!this.channels['private-' + name]) {
61 | this.channels['private-' + name] = new Channel(this.socket, 'private-' + name, this.options);
62 | }
63 |
64 | return this.channels['private-' + name] as Channel;
65 | }
66 |
67 | /**
68 | * Get a presence channel instance by name.
69 | */
70 | presenceChannel(name: string): Channel {
71 | if (!this.channels['presence-' + name]) {
72 | this.channels['presence-' + name] = new Channel(
73 | this.socket,
74 | 'presence-' + name,
75 | this.options
76 | );
77 | }
78 |
79 | return this.channels['presence-' + name] as Channel;
80 | }
81 |
82 | /**
83 | * Leave the given channel, as well as its private and presence variants.
84 | */
85 | leave(name: string): void {
86 | let channels = [name, 'private-' + name, 'presence-' + name];
87 |
88 | channels.forEach((name) => {
89 | this.leaveChannel(name);
90 | });
91 | }
92 |
93 | /**
94 | * Leave the given channel.
95 | */
96 | leaveChannel(name: string): void {
97 | if (this.channels[name]) {
98 | this.channels[name].unsubscribe();
99 |
100 | delete this.channels[name];
101 | }
102 | }
103 |
104 | /**
105 | * Get the socket ID for the connection.
106 | */
107 | socketId(): string {
108 | return this.socket.getSocketId();
109 | }
110 |
111 | /**
112 | * Disconnect socket connection.
113 | */
114 | disconnect(): void {
115 | this.options.debug && console.log(LOG_PREFIX + 'Disconnect ...' );
116 |
117 | this.socket.close();
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/js-tests/Connector.test.ts:
--------------------------------------------------------------------------------
1 | import WS from "jest-websocket-mock";
2 | import { Connector } from "../js-src/Connector";
3 | import { Channel } from "../js-src/Channel";
4 |
5 | const mockedHost = 'ws://localhost:1234';
6 |
7 | describe('Connector', () => {
8 | let server: WS;
9 |
10 | beforeEach(() => {
11 | jest.useRealTimers();
12 | server = new WS(mockedHost);
13 | });
14 |
15 | afterEach(() => {
16 | server.close()
17 | });
18 |
19 | test('socket id is correctly set', async () => {
20 | const connector = new Connector({
21 | host: mockedHost,
22 | })
23 |
24 | await server.connected;
25 |
26 | await expect(server).toReceiveMessage('{"event":"whoami"}');
27 | server.send('{"event":"whoami","data":{"socket_id":"test-socket-id"}}')
28 |
29 | expect(connector.socketId()).toBe('test-socket-id')
30 | })
31 |
32 | test('we reconnect to the server on error', async () => {
33 | const connector = new Connector({
34 | host: mockedHost,
35 | })
36 |
37 | await server.connected;
38 | await expect(server).toReceiveMessage('{"event":"whoami"}');
39 | server.send('{"event":"whoami","data":{"socket_id":"test-socket-id"}}')
40 |
41 | server.close();
42 | await server.closed;
43 | server.server.stop(() => (server = new WS(mockedHost)));
44 |
45 | await server.connected;
46 | await expect(server).toReceiveMessage('{"event":"whoami"}');
47 | server.send('{"event":"whoami","data":{"socket_id":"test-socket-id2"}}')
48 |
49 | expect(connector.socketId()).toBe('test-socket-id2')
50 | })
51 |
52 | test('we can subscribe to a channel and listen to events', async () => {
53 | const connector = new Connector({
54 | host: mockedHost,
55 | })
56 |
57 | await server.connected;
58 |
59 | await expect(server).toReceiveMessage('{"event":"whoami"}');
60 | server.send('{"event":"whoami","data":{"socket_id":"test-socket-id"}}')
61 |
62 | const channel = connector.channel('my-test-channel')
63 |
64 | await expect(server).toReceiveMessage('{"event":"subscribe","data":{"channel":"my-test-channel"}}');
65 |
66 | server.send('{"event":"subscription_succeeded","channel":"my-test-channel"}')
67 |
68 | expect(channel).toBeInstanceOf(Channel)
69 |
70 | const handler1 = jest.fn();
71 | const handler2 = jest.fn();
72 |
73 | channel.on('my-test-event', handler1)
74 |
75 | server.send('{"event":"my-test-event","channel":"my-test-channel","data":{}}')
76 |
77 | expect(handler1).toBeCalled();
78 | expect(handler2).not.toBeCalled();
79 | })
80 |
81 | test('we can send a whisper event', async () => {
82 | const connector = new Connector({
83 | host: mockedHost,
84 | })
85 |
86 | await server.connected;
87 |
88 | await expect(server).toReceiveMessage('{"event":"whoami"}');
89 | server.send('{"event":"whoami","data":{"socket_id":"test-socket-id"}}')
90 |
91 | const channel = connector.channel('my-test-channel')
92 |
93 | await expect(server).toReceiveMessage('{"event":"subscribe","data":{"channel":"my-test-channel"}}');
94 |
95 | server.send('{"event":"subscription_succeeded","channel":"my-test-channel"}')
96 |
97 | expect(channel).toBeInstanceOf(Channel)
98 |
99 | const handler1 = jest.fn();
100 | const handler2 = jest.fn();
101 |
102 | channel.on('client-whisper', handler1)
103 |
104 | server.send('{"event":"client-whisper","data":"whisper","channel":"my-test-channel","data":{}}')
105 |
106 | expect(handler1).toBeCalled();
107 | expect(handler2).not.toBeCalled();
108 | })
109 | });
110 |
--------------------------------------------------------------------------------
/js-src/Channel.ts:
--------------------------------------------------------------------------------
1 | import { EventFormatter } from 'laravel-echo/src/util';
2 | import { Channel as BaseChannel } from 'laravel-echo/src/channel/channel';
3 | import { PresenceChannel } from "laravel-echo/src/channel";
4 | import { Websocket } from "./Websocket";
5 |
6 | const LOG_PREFIX = '[LE-AG-Channel]';
7 |
8 | /**
9 | * This class represents a Pusher channel.
10 | */
11 | export class Channel extends BaseChannel implements PresenceChannel {
12 | /**
13 | * The Pusher client instance.
14 | */
15 | socket: Websocket;
16 |
17 | /**
18 | * The name of the channel.
19 | */
20 | name: string;
21 |
22 | /**
23 | * Channel options.
24 | */
25 | options: object;
26 |
27 | /**
28 | * The event formatter.
29 | */
30 | eventFormatter: EventFormatter;
31 |
32 | /**
33 | * Create a new class instance.
34 | */
35 | constructor(socket: Websocket, name: string, options: object) {
36 | super();
37 |
38 | this.name = name;
39 | this.socket = socket;
40 | this.options = options;
41 | this.eventFormatter = new EventFormatter(this.options["namespace"]);
42 |
43 | this.subscribe();
44 | }
45 |
46 | /**
47 | * Subscribe to a Pusher channel.
48 | */
49 | subscribe(): any {
50 | this.options["debug"] && console.log(`${LOG_PREFIX} subscribe for channel ${this.name} ...`);
51 |
52 | this.socket.subscribe(this)
53 | }
54 |
55 | /**
56 | * Unsubscribe from a Pusher channel.
57 | */
58 | unsubscribe(): void {
59 | this.options["debug"] && console.log(`${LOG_PREFIX} unsubscribe for channel ${this.name} ...`);
60 |
61 | this.socket.unsubscribe(this);
62 | }
63 |
64 | /**
65 | * Listen for an event on the channel instance.
66 | */
67 | listen(event: string, callback: Function): this {
68 | this.options["debug"] && console.log(`${LOG_PREFIX} listen to ${event} for channel ${this.name} ...`);
69 |
70 | this.on(this.eventFormatter.format(event), callback);
71 |
72 | return this;
73 | }
74 |
75 | /**
76 | * Stop listening for an event on the channel instance.
77 | */
78 | stopListening(event: string, callback?: Function): this {
79 | this.options["debug"] && console.log(`${LOG_PREFIX} stop listening to ${event} for channel ${this.name} ...`);
80 |
81 | this.socket.unbindEvent(this, event, callback)
82 |
83 | return this;
84 | }
85 |
86 | /**
87 | * Register a callback to be called anytime a subscription succeeds.
88 | */
89 | subscribed(callback: Function): this {
90 | this.options["debug"] && console.log(`${LOG_PREFIX} subscribed for channel ${this.name} ...`);
91 |
92 | this.on('subscription_succeeded', () => {
93 | callback();
94 | });
95 |
96 | return this;
97 | }
98 |
99 | /**
100 | * Register a callback to be called anytime a subscription error occurs.
101 | */
102 | error(callback: Function): this {
103 | this.options["debug"] && console.log(`${LOG_PREFIX} error for channel ${this.name} ...`);
104 |
105 | this.on('error', (status) => {
106 | callback(status);
107 | });
108 |
109 | return this;
110 | }
111 |
112 | /**
113 | * Bind a channel to an event.
114 | */
115 | on(event: string, callback: Function): Channel {
116 | this.options["debug"] && console.log(`${LOG_PREFIX} on ${event} for channel ${this.name} ...`);
117 |
118 | this.socket.bind(this, event, callback)
119 |
120 | return this;
121 | }
122 |
123 | whisper(event: string, data: object): this {
124 | let channel = this.name;
125 | let formattedEvent = "client-" + event;
126 | this.socket.send({
127 | "event": formattedEvent,
128 | data,
129 | channel,
130 | })
131 |
132 | return this;
133 | }
134 |
135 | here(callback: Function): this {
136 | // TODO: implement
137 |
138 | return this
139 | }
140 |
141 | /**
142 | * Listen for someone joining the channel.
143 | */
144 | joining(callback: Function): this {
145 | // TODO: implement
146 |
147 | return this
148 | }
149 |
150 | /**
151 | * Listen for someone leaving the channel.
152 | */
153 | leaving(callback: Function): this {
154 | // TODO: implement
155 |
156 | return this
157 | }
158 | }
159 |
160 | export { PresenceChannel };
161 |
--------------------------------------------------------------------------------
/src/Driver.php:
--------------------------------------------------------------------------------
1 | subscriptionRepository = $subscriptionRepository;
23 | $this->connectionRepository = $connectionRepository;
24 | }
25 |
26 | /**
27 | * Authenticate the incoming request for a given channel.
28 | *
29 | * @param \Illuminate\Http\Request $request
30 | *
31 | * @return mixed
32 | */
33 | public function auth($request)
34 | {
35 | $channelName = $this->normalizeChannelName($request->channel_name);
36 |
37 | if (empty($request->channel_name) || ($this->isGuardedChannel($request->channel_name) && ! $this->retrieveUser($request, $channelName))) {
38 | throw new AccessDeniedHttpException();
39 | }
40 |
41 | return parent::verifyUserCanAccessChannel(
42 | $request, $channelName
43 | );
44 | }
45 |
46 | /**
47 | * Return the valid authentication response.
48 | *
49 | * @param \Illuminate\Http\Request $request
50 | * @param mixed $result
51 | *
52 | * @return mixed
53 | */
54 | public function validAuthenticationResponse($request, $result)
55 | {
56 | if (! Str::startsWith($request->channel_name, 'presence')) {
57 | return response()->json(
58 | $this->generateSignature($request->channel_name, $request->socket_id)
59 | );
60 | }
61 |
62 | $channelName = $this->normalizeChannelName($request->channel_name);
63 |
64 | return response()->json(
65 | $this->generateSignaturePresence(
66 | $request->channel_name,
67 | $request->socket_id,
68 | $this->retrieveUser($request, $channelName)->getAuthIdentifier(),
69 | $result
70 | ),
71 | );
72 | }
73 |
74 | protected function generateSignature(string $channel, string $socketId, string $customData = null): array
75 | {
76 | $data = $customData ? "{$socketId}:{$channel}:{$customData}" : "{$socketId}:{$channel}";
77 |
78 | $signature = hash_hmac('sha256', $data, config('app.key'), false);
79 |
80 | $response = [
81 | 'auth' => $signature,
82 | ];
83 |
84 | if ($customData) {
85 | $response['channel_data'] = $customData;
86 | }
87 |
88 | return $response;
89 | }
90 |
91 | protected function generateSignaturePresence(string $channel, string $socketId, int $userId, array $userInfo = null): array
92 | {
93 | $userData = [
94 | 'user_id' => $userId,
95 | ];
96 |
97 | if ($userInfo) {
98 | $userData['user_info'] = $userInfo;
99 | }
100 |
101 | return $this->generateSignature($channel, $socketId, json_encode($userData, JSON_THROW_ON_ERROR));
102 | }
103 |
104 | /**
105 | * Broadcast the given event.
106 | *
107 | * @param Channel[] $channels
108 | * @param string $event
109 | * @param array $payload
110 | * @return void
111 | *
112 | * @throws \Illuminate\Broadcasting\BroadcastException
113 | */
114 | public function broadcast(array $channels, $event, array $payload = [])
115 | {
116 | $skipConnectionId = Arr::pull($payload, 'socket');
117 |
118 | foreach ($channels as $channel) {
119 | $data = json_encode([
120 | 'event' => $event,
121 | 'channel' => $channel->name,
122 | 'data' => $payload,
123 | ], JSON_THROW_ON_ERROR);
124 |
125 | $this->subscriptionRepository->getConnectionIdsForChannel($channel)
126 | ->reject(fn($connectionId) => $connectionId === $skipConnectionId)
127 | ->tap(fn($connectionIds) => logger()->debug("Preparing to send to connections for channel '{$channel->name}'", $connectionIds->toArray()))
128 | ->each(fn(string $connectionId) => $this->sendMessage($connectionId, $data));
129 | }
130 |
131 | return;
132 |
133 | // if ((is_array($response) && $response['status'] >= 200 && $response['status'] <= 299)
134 | // || $response === true) {
135 | // return;
136 | // }
137 | //
138 | // throw new BroadcastException(
139 | // ! empty($response['body'])
140 | // ? sprintf('Pusher error: %s.', $response['body'])
141 | // : 'Failed to connect to Pusher.'
142 | // );
143 | }
144 |
145 | protected function sendMessage(string $connectionId, string $data): void
146 | {
147 | logger()->debug("Sending message to connection '{$connectionId}'");
148 |
149 | try {
150 | $this->connectionRepository->sendMessage($connectionId, $data);
151 | } catch (ApiGatewayManagementApiException $exception) {
152 | if ($exception->getAwsErrorCode() === 'GoneException') {
153 | $this->subscriptionRepository->clearConnection($connectionId);
154 | return;
155 | }
156 |
157 | throw $exception;
158 |
159 | // $exception->getErrorCode() is one of:
160 | // GoneException
161 | // LimitExceededException
162 | // PayloadTooLargeException
163 | // ForbiddenException
164 |
165 | // otherwise: call $exception->getPrevious() which is a guzzle exception
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/src/Handler.php:
--------------------------------------------------------------------------------
1 | subscriptionRepository = $subscriptionRepository;
24 | $this->connectionRepository = $connectionRepository;
25 | }
26 |
27 | public function handleWebsocket(WebsocketEvent $event, Context $context): HttpResponse
28 | {
29 | try {
30 | $method = Str::camel('handle_' . Str::lower($event->getEventType() ?? ''));
31 |
32 | if (! method_exists($this, $method)) {
33 | throw new \InvalidArgumentException("Event type {$event->getEventType()} has no handler implemented.");
34 | }
35 |
36 | $this->$method($event, $context);
37 |
38 | return new HttpResponse('OK');
39 | } catch (Throwable $throwable) {
40 | report($throwable);
41 |
42 | throw $throwable;
43 | }
44 | }
45 |
46 | protected function handleDisconnect(WebsocketEvent $event, Context $context): void
47 | {
48 | $this->subscriptionRepository->clearConnection($event->getConnectionId());
49 | }
50 |
51 | protected function handleMessage(WebsocketEvent $event, Context $context): void
52 | {
53 | $eventBody = json_decode($event->getBody(), true);
54 |
55 | if (! isset($eventBody['event'])) {
56 | throw new \InvalidArgumentException('event missing or no valid json');
57 | }
58 |
59 | $eventType = $eventBody['event'];
60 |
61 | if ($eventType === 'ping') {
62 | $this->sendMessage($event, $context, [
63 | 'event' => 'pong',
64 | 'channel' => $eventBody['channel'] ?? null,
65 | ]);
66 | } elseif ($eventType === 'whoami') {
67 | $this->sendMessage($event, $context, [
68 | 'event' => 'whoami',
69 | 'data' => [
70 | 'socket_id' => $event->getConnectionId(),
71 | ],
72 | ]);
73 | } elseif ($eventType === 'subscribe') {
74 | $this->subscribe($event, $context);
75 | } elseif ($eventType === 'unsubscribe') {
76 | $this->unsubscribe($event, $context);
77 | } elseif (Str::startsWith($eventType, 'client-')) {
78 | $this->broadcastToChannel($event, $context);
79 | } else {
80 | $this->sendMessage($event, $context, [
81 | 'event' => 'error'
82 | ]);
83 | }
84 | }
85 |
86 | protected function subscribe(WebsocketEvent $event, Context $context): void
87 | {
88 | $eventBody = json_decode($event->getBody(), true);
89 |
90 | // fill missing values
91 | $eventBody['data'] += ['auth' => null, 'channel_data' => []];
92 |
93 | [
94 | 'channel' => $channel,
95 | 'auth' => $auth,
96 | 'channel_data' => $channelData,
97 | ] = $eventBody['data'];
98 |
99 | if (Str::startsWith($channel, ['private-', 'presence-'])) {
100 | $data = "{$event->getConnectionId()}:{$channel}";
101 |
102 | if ($channelData) {
103 | $data .= ':' . $channelData;
104 | }
105 |
106 | $signature = hash_hmac('sha256', $data, config('app.key'), false);
107 |
108 | if ($signature !== $auth) {
109 | $this->sendMessage($event, $context, [
110 | 'event' => 'error',
111 | 'channel' => $channel,
112 | 'data' => [
113 | 'message' => 'Invalid auth signature',
114 | ],
115 | ]);
116 |
117 | return;
118 | }
119 | }
120 |
121 | $this->subscriptionRepository->subscribeToChannel($event->getConnectionId(), $channel);
122 |
123 | $this->sendMessage($event, $context, [
124 | 'event' => 'subscription_succeeded',
125 | 'channel' => $channel,
126 | 'data' => [],
127 | ]);
128 | }
129 |
130 | protected function unsubscribe(WebsocketEvent $event, Context $context): void
131 | {
132 | $eventBody = json_decode($event->getBody(), true);
133 | $channel = $eventBody['data']['channel'];
134 |
135 | $this->subscriptionRepository->unsubscribeFromChannel($event->getConnectionId(), $channel);
136 |
137 | $this->sendMessage($event, $context, [
138 | 'event' => 'unsubscription_succeeded',
139 | 'channel' => $channel,
140 | 'data' => [],
141 | ]);
142 | }
143 |
144 | public function broadcastToChannel(WebsocketEvent $event, Context $context): void
145 | {
146 | $skipConnectionId = $event->getConnectionId();
147 | $eventBody = json_decode($event->getBody(), true);
148 | $channel = Arr::get($eventBody, 'channel');
149 | $event = Arr::get($eventBody, 'event');
150 | $payload = Arr::get($eventBody, 'data');
151 | if (is_object($payload) || is_array($payload)) {
152 | $payload = json_encode($payload);
153 | }
154 | $data = json_encode([
155 | 'event'=>$event,
156 | 'channel'=>$channel,
157 | 'data'=>$payload,
158 | ]) ?: '';
159 | $this->subscriptionRepository->getConnectionIdsForChannel($channel)
160 | ->reject(fn ($connectionId) => $connectionId === $skipConnectionId)
161 | ->each(fn (string $connectionId) => $this->sendMessageToConnection($connectionId, $data));
162 | }
163 |
164 | public function sendMessage(WebsocketEvent $event, Context $context, array $data): void
165 | {
166 | $this->connectionRepository->sendMessage($event->getConnectionId(), json_encode($data, JSON_THROW_ON_ERROR));
167 | }
168 |
169 | protected function sendMessageToConnection(string $connectionId, string $data): void
170 | {
171 | try {
172 | $this->connectionRepository->sendMessage($connectionId, $data);
173 | } catch (ApiGatewayManagementApiException $exception) {
174 | if ($exception->getAwsErrorCode() === 'GoneException') {
175 | $this->subscriptionRepository->clearConnection($connectionId);
176 | return;
177 | }
178 |
179 | throw $exception;
180 | }
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # laravel-echo-api-gateway
2 |
3 | [](https://github.com/georgeboot/laravel-echo-api-gateway/actions?query=workflow%3ACI)
4 | [](https://codecov.io/gh/georgeboot/laravel-echo-api-gateway)
5 |
6 | This package enables you to use API Gateway‘s Websockets as a driver for [Laravel Echo](https://github.com/laravel/echo)
7 | , so you don’t have to use services like Pusher or Socket.io.
8 |
9 | It works by setting up a websocket API in API Gateway, and configure it to invoke a Lambda function, every time a
10 | message is sent to the websocket. This package includes and autoconfigures a handler to respond to these websocket
11 | messages. We also configure Laravel to use this connection as a broadcast driver.
12 |
13 | This package currently only works with either [Bref](https://bref.sh) or [Laravel Vapor](https://vapor.laravel.com),
14 | though the latter one involves some manual set-up.
15 |
16 | ## Requirements
17 |
18 | In order to use this package, your project needs to meet the following criteria:
19 |
20 | - PHP 7.4 or 8.x
21 | - Laravel 6 to 11
22 | - Uses either [bref](https://bref.sh) or [Laravel Vapor](https://vapor.laravel.com) to deploy to AWS
23 | - Has a working queue
24 | - Uses Laravel Mix or any other tool to bundle your assets
25 |
26 | ## Installation
27 |
28 | Installation of this package is fairly simply.
29 |
30 | First we have to install both the composer and npm package:
31 |
32 | ```shell
33 | composer require georgeboot/laravel-echo-api-gateway
34 |
35 | yarn add laravel-echo-api-gateway
36 | # or
37 | npm install --save-dev laravel-echo-api-gateway
38 | ```
39 |
40 | ### Platform-specific instructions
41 |
42 | #### A. When using Bref
43 |
44 | Next, when using Bref, we have to add some elements to our `serverless.yml` file. If using Vapor, these resources have
45 | to be created by hand using the AWS CLI or console.
46 |
47 | Add a new function that will handle websocket events (messages etc):
48 |
49 | ```yaml
50 | functions:
51 | # Add this function
52 | websocket:
53 | handler: handlers/websocket.php
54 | layers:
55 | - ${bref:layer.php-80}
56 | events:
57 | - websocket: $disconnect
58 | - websocket: $default
59 | ```
60 |
61 | Add a resource to create and configure our DynamoDB table, where connections will be stored in:
62 |
63 | ```yaml
64 | resources:
65 | Resources:
66 | # Add this resource
67 | ConnectionsTable:
68 | Type: AWS::DynamoDB::Table
69 | Properties:
70 | TableName: connections
71 | AttributeDefinitions:
72 | - AttributeName: connectionId
73 | AttributeType: S
74 | - AttributeName: channel
75 | AttributeType: S
76 | KeySchema:
77 | - AttributeName: connectionId
78 | KeyType: HASH
79 | - AttributeName: channel
80 | KeyType: RANGE
81 | GlobalSecondaryIndexes:
82 | - IndexName: lookup-by-channel
83 | KeySchema:
84 | - AttributeName: channel
85 | KeyType: HASH
86 | Projection:
87 | ProjectionType: ALL
88 | - IndexName: lookup-by-connection
89 | KeySchema:
90 | - AttributeName: connectionId
91 | KeyType: HASH
92 | Projection:
93 | ProjectionType: ALL
94 | BillingMode: PAY_PER_REQUEST
95 | ```
96 |
97 | Add the following `iamRoleStatement` to enable our Lambda function to access the table:
98 |
99 | ```yaml
100 | provider:
101 | name: aws
102 |
103 | iamRoleStatements:
104 | # Add this iamRoleStatement
105 | - Effect: Allow
106 | Action: [ dynamodb:Query, dynamodb:GetItem, dynamodb:PutItem, dynamodb:UpdateItem, dynamodb:DeleteItem, dynamodb:BatchWriteItem ]
107 | Resource:
108 | - !GetAtt ConnectionsTable.Arn
109 | - !Join [ '', [ !GetAtt ConnectionsTable.Arn, '/index/*' ] ]
110 | ```
111 |
112 | Add an environment variable to autogenerate our websocket URL:
113 |
114 | ```yaml
115 | provider:
116 | name: aws
117 |
118 | environment:
119 | # Add these variables
120 | # Please note : in Laravel 11, this setting is now BROADCAST_CONNECTION
121 | BROADCAST_DRIVER: laravel-echo-api-gateway
122 | LARAVEL_ECHO_API_GATEWAY_DYNAMODB_TABLE: !Ref ConnectionsTable
123 | LARAVEL_ECHO_API_GATEWAY_API_ID: !Ref WebsocketsApi
124 | LARAVEL_ECHO_API_GATEWAY_API_STAGE: "${self:provider.stage}"
125 | ```
126 |
127 | Next, create the PHP handler file in `handlers/websocket.php`
128 |
129 | ```php
130 | make(Illuminate\Contracts\Console\Kernel::class);
141 | $kernel->bootstrap();
142 |
143 | return $app->make(Handler::class);
144 | ```
145 |
146 | Now, deploy your app by running `serverless deploy` or similar. Write down the websocket url the output gives you.
147 |
148 | #### B. When using Vapor
149 |
150 | When using Vapor, you will have to create these required resources by hand using the AWS CLI or Console:
151 |
152 | ##### B1. DynamoDB table for connections
153 |
154 | Create a DynamoDB table for the connections. Use `connectionId` (string) as a HASH key, and `channel` (string) as a SORT
155 | key. Set the capacity setting to whatever you like (probably on-demand).
156 |
157 | Create 2 indexes:
158 |
159 | 1. Name: `lookup-by-connection`, key: `connectionId`, no sort key, projected: ALL
160 | 2. Name: `lookup-by-channel`, key: `channel`, no sort key, projected: ALL
161 |
162 | ##### B2. API Gateway
163 |
164 | Create a new Websocket API. Enter a name and leave the route selection expression to what it is. Add a `$disconnect`
165 | and `$default`. Set both integrations to `Lambda` and select your CLI lambda from the list. Set the name of the stage to
166 | what you desire and create the API. Once created, write down the ID, as we'll need it later.
167 |
168 | ##### B3. IAM Permissions
169 |
170 | In IAM, go to roles and open `laravel-vapor-role`. Open the inline policy and edit it. On the JSON tab,
171 | add `"execute-api:*"` to the list of actions.
172 |
173 | Then, login to [Laravel Vapor](https://vapor.laravel.com/app), go to team settings, AWS Accounts, click on Role next to
174 | the correct account and deselect Receive Updates.
175 |
176 | Edit your `.env`:
177 |
178 | ```dotenv
179 | BROADCAST_DRIVER=laravel-echo-api-gateway
180 | LARAVEL_ECHO_API_GATEWAY_DYNAMODB_TABLE=the-table-name-you-entered-when-creating-it
181 | LARAVEL_ECHO_API_GATEWAY_API_ID=your-websocket-api-id
182 | LARAVEL_ECHO_API_GATEWAY_API_STAGE=your-api-stage-name
183 | ```
184 |
185 | ### Generate front-end code
186 |
187 | Add to your javascript file:
188 |
189 | ```js
190 | import Echo from 'laravel-echo';
191 | import {broadcaster} from 'laravel-echo-api-gateway';
192 |
193 | window.Echo = new Echo({
194 | broadcaster,
195 | // replace the placeholders
196 | host: 'wss://{api-ip}.execute-api.{region}.amazonaws.com/{stage}',
197 | authEndpoint: '{auth-url}/broadcasting/auth', // Optional: Use if you have a separate authentication endpoint
198 | bearerToken: '{token}', // Optional: Use if you need a Bearer Token for authentication
199 | });
200 | ```
201 |
202 | You can also enable console output by passing a `debug: true` otpion to your window.Echo intializer :
203 | ```js
204 | import Echo from 'laravel-echo';
205 | import {broadcaster} from 'laravel-echo-api-gateway';
206 |
207 | window.Echo = new Echo({
208 | broadcaster,
209 | // replace the placeholders
210 | host: 'wss://{api-ip}.execute-api.{region}.amazonaws.com/{stage}',
211 | debug: true
212 | });
213 | ```
214 |
215 |
216 |
217 | Lastly, you have to generate your assets by running Laravel Mix. After this step, you should be up and running.
218 |
--------------------------------------------------------------------------------
/js-src/Websocket.ts:
--------------------------------------------------------------------------------
1 | import { AxiosResponse } from "axios";
2 | import { Channel } from "./Channel";
3 | import axios from 'axios';
4 |
5 | export type Options = { authEndpoint: string, host: string, bearerToken: string, auth: any, debug: boolean };
6 |
7 | export type MessageBody = { event: string, channel?: string, data: object };
8 |
9 | const LOG_PREFIX = '[LE-AG-Websocket]';
10 |
11 | export class Websocket {
12 | buffer: Array