diff --git a/.editorconfig b/.editorconfig
index 32de2af..9718070 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -11,7 +11,7 @@ end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
-[*.blade.php]
+[*.{blade.php,yml,yaml}]
indent_size = 2
[*.md]
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..8ab4ff7
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,65 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - '*'
+ tags:
+ - '*'
+ pull_request:
+ branches:
+ - '*'
+
+jobs:
+ build:
+ if: "!contains(github.event.head_commit.message, 'skip ci')"
+
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ php: ['7.2', '7.3', '7.4']
+ laravel: ['6.*', '7.*', '8.*']
+ prefer: ['prefer-lowest', 'prefer-stable']
+ include:
+ - laravel: '6.*'
+ testbench: '4.*'
+ - laravel: '7.*'
+ testbench: '5.*'
+ - laravel: '8.*'
+ testbench: '6.*'
+
+ name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} --${{ matrix.prefer }}
+
+ steps:
+ - uses: actions/checkout@v1
+
+ - name: Setup Redis
+ uses: supercharge/redis-github-action@1.1.0
+ with:
+ redis-version: 6
+
+ - uses: actions/cache@v1
+ name: Cache dependencies
+ with:
+ path: ~/.composer/cache/files
+ key: composer-php-${{ matrix.php }}-${{ matrix.laravel }}-${{ matrix.prefer }}-${{ hashFiles('composer.json') }}
+
+ - name: Install dependencies
+ run: |
+ composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench-browser-kit:${{ matrix.testbench }}" "orchestra/database:${{ matrix.testbench }}" --no-interaction --no-update
+ composer update --${{ matrix.prefer }} --prefer-dist --no-interaction --no-suggest
+
+ - name: Run tests for Local
+ run: |
+ REPLICATION_MODE=local phpunit --coverage-text --coverage-clover=coverage_local.xml
+
+ - name: Run tests for Redis
+ run: |
+ REPLICATION_MODE=redis phpunit --coverage-text --coverage-clover=coverage_redis.xml
+
+ - uses: codecov/codecov-action@v1
+ with:
+ fail_ci_if_error: false
+ file: '*.xml'
+ token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
deleted file mode 100644
index c3e4762..0000000
--- a/.github/workflows/run-tests.yml
+++ /dev/null
@@ -1,65 +0,0 @@
-name: run-tests
-
-on: [push, pull_request]
-
-jobs:
- test:
- runs-on: ${{ matrix.os }}
- strategy:
- fail-fast: false
- matrix:
- os: [ubuntu-latest, windows-latest]
- php: [7.4, 7.3, 7.2]
- laravel: [6.*, 7.*]
- dependency-version: [prefer-lowest, prefer-stable]
- include:
- - laravel: 7.*
- testbench: 5.*
- - laravel: 6.*
- testbench: 4.*
-
- name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }}
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v1
-
- - name: Setup Redis
- uses: supercharge/redis-github-action@1.1.0
- with:
- redis-version: 6
- if: ${{ matrix.os == 'ubuntu-latest' }}
-
- - name: Cache dependencies
- uses: actions/cache@v1
- with:
- path: ~/.composer/cache/files
- key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
-
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
- extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
- coverage: xdebug
-
- - name: Install dependencies
- run: |
- composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench-browser-kit:${{ matrix.testbench }}" --no-interaction --no-update
- composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest
-
- - name: Execute tests with Local driver
- run: vendor/bin/phpunit --coverage-text --coverage-clover=coverage_local.xml
- env:
- REPLICATION_DRIVER: local
-
- - name: Execute tests with Redis driver
- run: vendor/bin/phpunit --coverage-text --coverage-clover=coverage_redis.xml
- if: ${{ matrix.os == 'ubuntu-latest' }}
- env:
- REPLICATION_DRIVER: redis
-
- - uses: codecov/codecov-action@v1
- with:
- fail_ci_if_error: false
- file: '*.xml'
diff --git a/.gitignore b/.gitignore
index a4753bd..65e1146 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,9 @@
+/vendor
+/.idea
build
-composer.lock
-vendor
-coverage
.phpunit.result.cache
-.idea/
+coverage
+composer.phar
+composer.lock
+.DS_Store
database.sqlite
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
index df16b68..76733d0 100644
--- a/.scrutinizer.yml
+++ b/.scrutinizer.yml
@@ -1,19 +1,19 @@
filter:
- excluded_paths: [tests/*]
+ excluded_paths: [tests/*]
checks:
- php:
- remove_extra_empty_lines: true
- remove_php_closing_tag: true
- remove_trailing_whitespace: true
- fix_use_statements:
- remove_unused: true
- preserve_multiple: false
- preserve_blanklines: true
- order_alphabetically: true
- fix_php_opening_tag: true
- fix_linefeed: true
- fix_line_ending: true
- fix_identation_4spaces: true
- fix_doc_comments: true
+ php:
+ remove_extra_empty_lines: true
+ remove_php_closing_tag: true
+ remove_trailing_whitespace: true
+ fix_use_statements:
+ remove_unused: true
+ preserve_multiple: false
+ preserve_blanklines: true
+ order_alphabetically: true
+ fix_php_opening_tag: true
+ fix_linefeed: true
+ fix_line_ending: true
+ fix_identation_4spaces: true
+ fix_doc_comments: true
diff --git a/.styleci.yml b/.styleci.yml
index f4d3cbc..c3bb259 100644
--- a/.styleci.yml
+++ b/.styleci.yml
@@ -1,4 +1 @@
-preset: laravel
-
-disabled:
- - single_class_element_per_statement
+preset: laravel
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index a3341a8..0000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# Changelog
-
-All notable changes to `laravel-websockets` will be documented in this file
-
-## 1.4.0 - 2020-03-03
-
-- add support for Laravel 7
-
-## 1.0.2 - 2018-12-06
-
-- Fix issue with wrong namespaces
-
-## 1.0.1 - 2018-12-04
-
-- Remove VueJS debug mode on dashboard
-- Allow setting app hosts to use when connecting via the dashboard
-- Added debug mode when starting the WebSocket server
-
-## 1.0.0 - 2018-12-04
-
-- initial release
diff --git a/LICENSE.md b/LICENSE
similarity index 100%
rename from LICENSE.md
rename to LICENSE
diff --git a/composer.json b/composer.json
index 09c15b6..50592a1 100644
--- a/composer.json
+++ b/composer.json
@@ -1,12 +1,9 @@
{
"name": "beyondcode/laravel-websockets",
- "description": "An easy to use WebSocket server",
- "keywords": [
- "beyondcode",
- "laravel-websockets"
- ],
- "homepage": "https://github.com/beyondcode/laravel-websockets",
+ "description": ":package_description",
+ "keywords": ["laravel", "php"],
"license": "MIT",
+ "homepage": "https://github.com/beyondcode/laravel-websockets",
"authors": [
{
"name": "Marcel Pociot",
@@ -19,6 +16,11 @@
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
+ },
+ {
+ "name": "Alex Renoki",
+ "homepage": "https://github.com/rennokki",
+ "role": "Developer"
}
],
"require": {
@@ -28,50 +30,43 @@
"clue/buzz-react": "^2.5",
"clue/redis-react": "^2.3",
"evenement/evenement": "^2.0|^3.0",
- "facade/ignition-contracts": "^1.0",
"guzzlehttp/psr7": "^1.5",
- "illuminate/broadcasting": "^6.0|^7.0",
- "illuminate/console": "^6.0|^7.0",
- "illuminate/http": "^6.0|^7.0",
- "illuminate/routing": "^6.0|^7.0",
- "illuminate/support": "^6.0|^7.0",
+ "laravel/framework": "^6.0|^7.0|^8.0",
"pusher/pusher-php-server": "^3.0|^4.0",
"react/promise": "^2.0",
"symfony/http-kernel": "^4.0|^5.0",
"symfony/psr-http-message-bridge": "^1.1|^2.0"
},
- "require-dev": {
- "clue/block-react": "^1.4",
- "mockery/mockery": "^1.3",
- "orchestra/testbench-browser-kit": "^4.0|^5.0",
- "phpunit/phpunit": "^8.0|^9.0"
- },
"autoload": {
"psr-4": {
- "BeyondCode\\LaravelWebSockets\\": "src"
+ "BeyondCode\\LaravelWebSockets\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
- "BeyondCode\\LaravelWebSockets\\Tests\\": "tests"
+ "BeyondCode\\LaravelWebSockets\\Test\\": "tests"
}
},
"scripts": {
- "test": "vendor/bin/phpunit",
- "test-coverage": "vendor/bin/phpunit --coverage-html coverage"
-
+ "test": "vendor/bin/phpunit"
+ },
+ "require-dev": {
+ "clue/block-react": "^1.4",
+ "laravel/legacy-factories": "^1.0.4",
+ "mockery/mockery": "^1.3",
+ "orchestra/testbench-browser-kit": "^4.0|^5.0|^6.0",
+ "orchestra/database": "^4.0|^5.0|^6.0",
+ "phpunit/phpunit": "^8.0|^9.0"
},
"config": {
"sort-packages": true
},
+ "minimum-stability": "dev",
"extra": {
"laravel": {
"providers": [
"BeyondCode\\LaravelWebSockets\\WebSocketsServiceProvider"
- ],
- "aliases": {
- "WebSocketRouter": "BeyondCode\\LaravelWebSockets\\Facades\\WebSocketRouter"
- }
+ ]
}
}
}
diff --git a/config/websockets.php b/config/websockets.php
index f5d9faf..e36f3cd 100644
--- a/config/websockets.php
+++ b/config/websockets.php
@@ -76,6 +76,141 @@ return [
],
],
+ /*
+ |--------------------------------------------------------------------------
+ | Broadcasting Replication PubSub
+ |--------------------------------------------------------------------------
+ |
+ | You can enable replication to publish and subscribe to
+ | messages across the driver.
+ |
+ | By default, it is set to 'local', but you can configure it to use drivers
+ | like Redis to ensure connection between multiple instances of
+ | WebSocket servers. Just set the driver to 'redis' to enable the PubSub using Redis.
+ |
+ */
+
+ 'replication' => [
+
+ 'mode' => env('WEBSOCKETS_REPLICATION_MODE', 'local'),
+
+ 'modes' => [
+
+ /*
+ |--------------------------------------------------------------------------
+ | Local Replication
+ |--------------------------------------------------------------------------
+ |
+ | Local replication is actually a null replicator, meaning that it
+ | is the default behaviour of storing the connections into an array.
+ |
+ */
+
+ 'local' => [
+
+ /*
+ |--------------------------------------------------------------------------
+ | Channel Manager
+ |--------------------------------------------------------------------------
+ |
+ | The channel manager is responsible for storing, tracking and retrieving
+ | the channels as long as their memebers and connections.
+ |
+ */
+
+ 'channel_manager' => \BeyondCode\LaravelWebSockets\ChannelManagers\LocalChannelManager::class,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Statistics Collector
+ |--------------------------------------------------------------------------
+ |
+ | The Statistics Collector will, by default, handle the incoming statistics,
+ | storing them until they will become dumped into another database, usually
+ | a MySQL database or a time-series database.
+ |
+ */
+
+ 'collector' => \BeyondCode\LaravelWebSockets\Statistics\Collectors\MemoryCollector::class,
+
+ ],
+
+ 'redis' => [
+
+ 'connection' => 'default',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Channel Manager
+ |--------------------------------------------------------------------------
+ |
+ | The channel manager is responsible for storing, tracking and retrieving
+ | the channels as long as their memebers and connections.
+ |
+ */
+
+ 'channel_manager' => \BeyondCode\LaravelWebSockets\ChannelManagers\RedisChannelManager::class,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Statistics Collector
+ |--------------------------------------------------------------------------
+ |
+ | The Statistics Collector will, by default, handle the incoming statistics,
+ | storing them until they will become dumped into another database, usually
+ | a MySQL database or a time-series database.
+ |
+ */
+
+ 'collector' => \BeyondCode\LaravelWebSockets\Statistics\Collectors\RedisCollector::class,
+
+ ],
+
+ ],
+
+ ],
+
+ 'statistics' => [
+
+ /*
+ |--------------------------------------------------------------------------
+ | Statistics Store
+ |--------------------------------------------------------------------------
+ |
+ | The Statistics Store is the place where all the temporary stats will
+ | be dumped. This is a much reliable store and will be used to display
+ | graphs or handle it later on your app.
+ |
+ */
+
+ 'store' => \BeyondCode\LaravelWebSockets\Statistics\Stores\DatabaseStore::class,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Statistics Interval Period
+ |--------------------------------------------------------------------------
+ |
+ | Here you can specify the interval in seconds at which
+ | statistics should be logged.
+ |
+ */
+
+ 'interval_in_seconds' => 60,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Statistics Deletion Period
+ |--------------------------------------------------------------------------
+ |
+ | When the clean-command is executed, all recorded statistics older than
+ | the number of days specified here will be deleted.
+ |
+ */
+
+ 'delete_statistics_older_than_days' => 60,
+
+ ],
+
/*
|--------------------------------------------------------------------------
| Maximum Request Size
@@ -130,130 +265,15 @@ return [
'handlers' => [
- 'websocket' => \BeyondCode\LaravelWebSockets\WebSockets\WebSocketHandler::class,
+ 'websocket' => \BeyondCode\LaravelWebSockets\Server\WebSocketHandler::class,
- 'trigger_event' => \BeyondCode\LaravelWebSockets\HttpApi\Controllers\TriggerEventController::class,
+ 'trigger_event' => \BeyondCode\LaravelWebSockets\API\TriggerEvent::class,
- 'fetch_channels' => \BeyondCode\LaravelWebSockets\HttpApi\Controllers\FetchChannelsController::class,
+ 'fetch_channels' => \BeyondCode\LaravelWebSockets\API\FetchChannels::class,
- 'fetch_channel' => \BeyondCode\LaravelWebSockets\HttpApi\Controllers\FetchChannelController::class,
+ 'fetch_channel' => \BeyondCode\LaravelWebSockets\API\FetchChannel::class,
- 'fetch_users' => \BeyondCode\LaravelWebSockets\HttpApi\Controllers\FetchUsersController::class,
-
- ],
-
- /*
- |--------------------------------------------------------------------------
- | Broadcasting Replication PubSub
- |--------------------------------------------------------------------------
- |
- | You can enable replication to publish and subscribe to
- | messages across the driver.
- |
- | By default, it is set to 'local', but you can configure it to use drivers
- | like Redis to ensure connection between multiple instances of
- | WebSocket servers. Just set the driver to 'redis' to enable the PubSub using Redis.
- |
- */
-
- 'replication' => [
-
- 'driver' => env('LARAVEL_WEBSOCKETS_REPLICATION_DRIVER', 'local'),
-
- /*
- |--------------------------------------------------------------------------
- | Local Replication
- |--------------------------------------------------------------------------
- |
- | Local replication is actually a null replicator, meaning that it
- | is the default behaviour of storing the connections into an array.
- |
- */
-
- 'local' => [
-
- 'client' => \BeyondCode\LaravelWebSockets\PubSub\Drivers\LocalClient::class,
-
- 'statistics_logger' => \BeyondCode\LaravelWebSockets\Statistics\Logger\MemoryStatisticsLogger::class,
-
- 'channel_manager' => \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManagers\ArrayChannelManager::class,
-
- ],
-
- /*
- |--------------------------------------------------------------------------
- | Redis Replication
- |--------------------------------------------------------------------------
- |
- | Redis replication relies on the Redis' Pub/Sub protocol. When users
- | are connected across multiple nodes, whenever some event gets triggered
- | on one instance, the rest of the instances get the same copy and, in
- | case the connected users to other instances are valid to receive
- | the event, they will receive it.
- |
- */
-
- 'redis' => [
-
- 'connection' => env('LARAVEL_WEBSOCKETS_REPLICATION_CONNECTION', 'default'),
-
- 'client' => \BeyondCode\LaravelWebSockets\PubSub\Drivers\RedisClient::class,
-
- 'statistics_logger' => \BeyondCode\LaravelWebSockets\Statistics\Logger\RedisStatisticsLogger::class,
-
- 'channel_manager' => \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManagers\RedisChannelManager::class,
-
- ],
-
- ],
-
- 'statistics' => [
-
- /*
- |--------------------------------------------------------------------------
- | Statistics Driver
- |--------------------------------------------------------------------------
- |
- | Here you can specify which driver to use to store the statistics to.
- | See down below for each driver's setting.
- |
- | Available: database
- |
- */
-
- 'driver' => env('LARAVEL_WEBSOCKETS_STATISTICS_DRIVER', 'database'),
-
- 'database' => [
-
- 'driver' => \BeyondCode\LaravelWebSockets\Statistics\Drivers\DatabaseDriver::class,
-
- 'model' => \BeyondCode\LaravelWebSockets\Statistics\Models\WebSocketsStatisticsEntry::class,
-
- ],
-
- /*
- |--------------------------------------------------------------------------
- | Statistics Interval Period
- |--------------------------------------------------------------------------
- |
- | Here you can specify the interval in seconds at which
- | statistics should be logged.
- |
- */
-
- 'interval_in_seconds' => 60,
-
- /*
- |--------------------------------------------------------------------------
- | Statistics Deletion Period
- |--------------------------------------------------------------------------
- |
- | When the clean-command is executed, all recorded statistics older than
- | the number of days specified here will be deleted.
- |
- */
-
- 'delete_statistics_older_than_days' => 60,
+ 'fetch_users' => \BeyondCode\LaravelWebSockets\API\FetchUsers::class,
],
diff --git a/database/migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php b/database/migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php
index 1b89b4a..0989f28 100644
--- a/database/migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php
+++ b/database/migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php
@@ -16,9 +16,9 @@ class CreateWebSocketsStatisticsEntriesTable extends Migration
Schema::create('websockets_statistics_entries', function (Blueprint $table) {
$table->increments('id');
$table->string('app_id');
- $table->integer('peak_connection_count');
- $table->integer('websocket_message_count');
- $table->integer('api_message_count');
+ $table->integer('peak_connections_count');
+ $table->integer('websocket_messages_count');
+ $table->integer('api_messages_count');
$table->nullableTimestamps();
});
}
diff --git a/phpunit.xml.dist b/phpunit.xml
similarity index 82%
rename from phpunit.xml.dist
rename to phpunit.xml
index 179f0b3..229ec35 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml
@@ -10,7 +10,7 @@
processIsolation="false"
stopOnFailure="false">
-
+
tests
@@ -20,6 +20,7 @@
-
+
+
diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php
index b2ce662..a7d9a76 100644
--- a/resources/views/dashboard.blade.php
+++ b/resources/views/dashboard.blade.php
@@ -395,8 +395,6 @@
let payload = {
_token: '{{ csrf_token() }}',
- key: this.app.key,
- secret: this.app.secret,
appId: this.app.id,
channel: this.form.channel,
event: this.form.event,
@@ -424,10 +422,6 @@
return 'bg-green-700 text-white';
}
- if (log.type === 'vacated') {
- return 'bg-orange-500 text-white';
- }
-
if (['disconnection', 'occupied', 'replicator-unsubscribed', 'replicator-left'].includes(log.type)) {
return 'bg-red-700 text-white';
}
diff --git a/src/HttpApi/Controllers/Controller.php b/src/API/Controller.php
similarity index 85%
rename from src/HttpApi/Controllers/Controller.php
rename to src/API/Controller.php
index cd47d1e..994447d 100644
--- a/src/HttpApi/Controllers/Controller.php
+++ b/src/API/Controller.php
@@ -1,11 +1,9 @@
channelManager = $channelManager;
- $this->replicator = $replicator;
}
/**
@@ -202,6 +192,10 @@ abstract class Controller implements HttpServerInterface
return;
}
+ if ($response instanceof HttpException) {
+ throw $response;
+ }
+
$this->sendAndClose($connection, $response);
}
@@ -243,11 +237,12 @@ abstract class Controller implements HttpServerInterface
*/
protected function ensureValidSignature(Request $request)
{
- /*
- * The `auth_signature` & `body_md5` parameters are not included when calculating the `auth_signature` value.
- * The `appId`, `appKey` & `channelName` parameters are actually route parameters and are never supplied by the client.
- */
- $params = Arr::except($request->query(), ['auth_signature', 'body_md5', 'appId', 'appKey', 'channelName']);
+ // The `auth_signature` & `body_md5` parameters are not included when calculating the `auth_signature` value.
+ // The `appId`, `appKey` & `channelName` parameters are actually route parameters and are never supplied by the client.
+
+ $params = Arr::except($request->query(), [
+ 'auth_signature', 'body_md5', 'appId', 'appKey', 'channelName',
+ ]);
if ($request->getContent() !== '') {
$params['body_md5'] = md5($request->getContent());
@@ -257,7 +252,9 @@ abstract class Controller implements HttpServerInterface
$signature = "{$request->getMethod()}\n/{$request->path()}\n".Pusher::array_implode('=', '&', $params);
- $authSignature = hash_hmac('sha256', $signature, App::findById($request->get('appId'))->secret);
+ $app = App::findById($request->get('appId'));
+
+ $authSignature = hash_hmac('sha256', $signature, $app->secret);
if ($authSignature !== $request->get('auth_signature')) {
throw new HttpException(401, 'Invalid auth signature provided.');
diff --git a/src/API/FetchChannel.php b/src/API/FetchChannel.php
new file mode 100644
index 0000000..73650b4
--- /dev/null
+++ b/src/API/FetchChannel.php
@@ -0,0 +1,52 @@
+channelManager->find(
+ $request->appId, $request->channelName
+ );
+
+ if (is_null($channel)) {
+ return new HttpException(404, "Unknown channel `{$request->channelName}`.");
+ }
+
+ return $this->channelManager
+ ->getGlobalConnectionsCount($request->appId, $request->channelName)
+ ->then(function ($connectionsCount) use ($request) {
+ // For the presence channels, we need a slightly different response
+ // that need an additional call.
+ if (Str::startsWith($request->channelName, 'presence-')) {
+ return $this->channelManager
+ ->getChannelsMembersCount($request->appId, [$request->channelName])
+ ->then(function ($channelMembers) use ($connectionsCount, $request) {
+ return [
+ 'occupied' => $connectionsCount > 0,
+ 'subscription_count' => $connectionsCount,
+ 'user_count' => $channelMembers[$request->channelName] ?? 0,
+ ];
+ });
+ }
+
+ // For the rest of the channels, we might as well
+ // send the basic response with the subscriptions count.
+ return [
+ 'occupied' => $connectionsCount > 0,
+ 'subscription_count' => $connectionsCount,
+ ];
+ });
+ }
+}
diff --git a/src/API/FetchChannels.php b/src/API/FetchChannels.php
new file mode 100644
index 0000000..7eff6ee
--- /dev/null
+++ b/src/API/FetchChannels.php
@@ -0,0 +1,80 @@
+has('info')) {
+ $attributes = explode(',', trim($request->info));
+
+ if (in_array('user_count', $attributes) && ! Str::startsWith($request->filter_by_prefix, 'presence-')) {
+ throw new HttpException(400, 'Request must be limited to presence channels in order to fetch user_count');
+ }
+ }
+
+ return $this->channelManager
+ ->getGlobalChannels($request->appId)
+ ->then(function ($channels) use ($request, $attributes) {
+ $channels = collect($channels)->keyBy(function ($channel) {
+ return $channel instanceof Channel
+ ? $channel->getName()
+ : $channel;
+ });
+
+ if ($request->has('filter_by_prefix')) {
+ $channels = $channels->filter(function ($channel, $channelName) use ($request) {
+ return Str::startsWith($channelName, $request->filter_by_prefix);
+ });
+ }
+
+ $channelNames = $channels->map(function ($channel) {
+ return $channel instanceof Channel
+ ? $channel->getName()
+ : $channel;
+ })->toArray();
+
+ return $this->channelManager
+ ->getChannelsMembersCount($request->appId, $channelNames)
+ ->then(function ($counts) use ($channels, $attributes) {
+ $channels = $channels->map(function ($channel) use ($counts, $attributes) {
+ $info = new stdClass;
+
+ $channelName = $channel instanceof Channel
+ ? $channel->getName()
+ : $channel;
+
+ if (in_array('user_count', $attributes)) {
+ $info->user_count = $counts[$channelName];
+ }
+
+ return $info;
+ })
+ ->sortBy(function ($content, $name) {
+ return $name;
+ })
+ ->all();
+
+ return [
+ 'channels' => $channels ?: new stdClass,
+ ];
+ });
+ });
+ }
+}
diff --git a/src/API/FetchUsers.php b/src/API/FetchUsers.php
new file mode 100644
index 0000000..79176fc
--- /dev/null
+++ b/src/API/FetchUsers.php
@@ -0,0 +1,37 @@
+channelName, 'presence-')) {
+ return new HttpException(400, "Invalid presence channel `{$request->channelName}`");
+ }
+
+ return $this->channelManager
+ ->getChannelMembers($request->appId, $request->channelName)
+ ->then(function ($members) {
+ $users = collect($members)->map(function ($user) {
+ return ['id' => $user->user_id];
+ })->values()->toArray();
+
+ return [
+ 'users' => $users,
+ ];
+ });
+ }
+}
diff --git a/src/API/TriggerEvent.php b/src/API/TriggerEvent.php
new file mode 100644
index 0000000..4ec9cd2
--- /dev/null
+++ b/src/API/TriggerEvent.php
@@ -0,0 +1,67 @@
+channels ?: [];
+
+ if (is_string($channels)) {
+ $channels = [$channels];
+ }
+
+ foreach ($channels as $channelName) {
+ // Here you can use the ->find(), even if the channel
+ // does not exist on the server. If it does not exist,
+ // then the message simply will get broadcasted
+ // across the other servers.
+ $channel = $this->channelManager->find(
+ $request->appId, $channelName
+ );
+
+ $payload = [
+ 'channel' => $channelName,
+ 'event' => $request->name,
+ 'data' => $request->data,
+ ];
+
+ if ($channel) {
+ $channel->broadcastToEveryoneExcept(
+ (object) $payload,
+ $request->socket_id,
+ $request->appId
+ );
+ } else {
+ $this->channelManager->broadcastAcrossServers(
+ $request->appId, $channelName, (object) $payload
+ );
+ }
+
+ StatisticsCollector::apiMessage($request->appId);
+
+ DashboardLogger::log($request->appId, DashboardLogger::TYPE_API_MESSAGE, [
+ 'channel' => $channelName,
+ 'event' => $request->name,
+ 'payload' => $request->data,
+ ]);
+ }
+
+ return $request->json()->all();
+ }
+}
diff --git a/src/Apps/App.php b/src/Apps/App.php
index acb2150..19d10f6 100644
--- a/src/Apps/App.php
+++ b/src/Apps/App.php
@@ -2,7 +2,7 @@
namespace BeyondCode\LaravelWebSockets\Apps;
-use BeyondCode\LaravelWebSockets\Exceptions\InvalidApp;
+use BeyondCode\LaravelWebSockets\Contracts\AppManager;
class App
{
@@ -76,18 +76,9 @@ class App
* @param string $key
* @param string $secret
* @return void
- * @throws \BeyondCode\LaravelWebSockets\Exceptions\InvalidApp
*/
public function __construct($appId, $appKey, $appSecret)
{
- if ($appKey === '') {
- throw InvalidApp::valueIsRequired('appKey', $appId);
- }
-
- if ($appSecret === '') {
- throw InvalidApp::valueIsRequired('appSecret', $appId);
- }
-
$this->id = $appId;
$this->key = $appKey;
$this->secret = $appSecret;
diff --git a/src/Apps/ConfigAppManager.php b/src/Apps/ConfigAppManager.php
index 03e5458..eb3d5db 100644
--- a/src/Apps/ConfigAppManager.php
+++ b/src/Apps/ConfigAppManager.php
@@ -2,6 +2,8 @@
namespace BeyondCode\LaravelWebSockets\Apps;
+use BeyondCode\LaravelWebSockets\Contracts\AppManager;
+
class ConfigAppManager implements AppManager
{
/**
@@ -30,7 +32,7 @@ class ConfigAppManager implements AppManager
{
return $this->apps
->map(function (array $appAttributes) {
- return $this->instantiate($appAttributes);
+ return $this->convertIntoApp($appAttributes);
})
->toArray();
}
@@ -43,11 +45,9 @@ class ConfigAppManager implements AppManager
*/
public function findById($appId): ?App
{
- $appAttributes = $this
- ->apps
- ->firstWhere('id', $appId);
-
- return $this->instantiate($appAttributes);
+ return $this->convertIntoApp(
+ $this->apps->firstWhere('id', $appId)
+ );
}
/**
@@ -58,11 +58,9 @@ class ConfigAppManager implements AppManager
*/
public function findByKey($appKey): ?App
{
- $appAttributes = $this
- ->apps
- ->firstWhere('key', $appKey);
-
- return $this->instantiate($appAttributes);
+ return $this->convertIntoApp(
+ $this->apps->firstWhere('key', $appKey)
+ );
}
/**
@@ -73,11 +71,9 @@ class ConfigAppManager implements AppManager
*/
public function findBySecret($appSecret): ?App
{
- $appAttributes = $this
- ->apps
- ->firstWhere('secret', $appSecret);
-
- return $this->instantiate($appAttributes);
+ return $this->convertIntoApp(
+ $this->apps->firstWhere('secret', $appSecret)
+ );
}
/**
@@ -86,7 +82,7 @@ class ConfigAppManager implements AppManager
* @param array|null $app
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
*/
- protected function instantiate(?array $appAttributes): ?App
+ protected function convertIntoApp(?array $appAttributes): ?App
{
if (! $appAttributes) {
return null;
diff --git a/src/ChannelManagers/LocalChannelManager.php b/src/ChannelManagers/LocalChannelManager.php
new file mode 100644
index 0000000..914e585
--- /dev/null
+++ b/src/ChannelManagers/LocalChannelManager.php
@@ -0,0 +1,334 @@
+channels[$appId][$channel] ?? null;
+ }
+
+ /**
+ * Find a channel by app & name or create one.
+ *
+ * @param string|int $appId
+ * @param string $channel
+ * @return BeyondCode\LaravelWebSockets\Channels\Channel
+ */
+ public function findOrCreate($appId, string $channel)
+ {
+ if (! $channelInstance = $this->find($appId, $channel)) {
+ $class = $this->getChannelClassName($channel);
+
+ $this->channels[$appId][$channel] = new $class($channel);
+ }
+
+ return $this->channels[$appId][$channel];
+ }
+
+ /**
+ * Get all channels for a specific app
+ * for the current instance.
+ *
+ * @param string|int $appId
+ * @return \React\Promise\PromiseInterface[array]
+ */
+ public function getLocalChannels($appId): PromiseInterface
+ {
+ return new FulfilledPromise(
+ $this->channels[$appId] ?? []
+ );
+ }
+
+ /**
+ * Get all channels for a specific app
+ * across multiple servers.
+ *
+ * @param string|int $appId
+ * @return \React\Promise\PromiseInterface[array]
+ */
+ public function getGlobalChannels($appId): PromiseInterface
+ {
+ return $this->getLocalChannels($appId);
+ }
+
+ /**
+ * Remove connection from all channels.
+ *
+ * @param \Ratchet\ConnectionInterface $connection
+ * @return void
+ */
+ public function unsubscribeFromAllChannels(ConnectionInterface $connection)
+ {
+ if (! isset($connection->app)) {
+ return;
+ }
+
+ $this->getLocalChannels($connection->app->id)
+ ->then(function ($channels) use ($connection) {
+ collect($channels)->each->unsubscribe($connection);
+
+ collect($channels)
+ ->reject->hasConnections()
+ ->each(function (Channel $channel, string $channelName) use ($connection) {
+ unset($this->channels[$connection->app->id][$channelName]);
+ });
+ });
+
+ $this->getLocalChannels($connection->app->id)
+ ->then(function ($channels) use ($connection) {
+ if (count($channels) === 0) {
+ unset($this->channels[$connection->app->id]);
+ }
+ });
+ }
+
+ /**
+ * Subscribe the connection to a specific channel.
+ *
+ * @param \Ratchet\ConnectionInterface $connection
+ * @param string $channelName
+ * @param stdClass $payload
+ * @return void
+ */
+ public function subscribeToChannel(ConnectionInterface $connection, string $channelName, stdClass $payload)
+ {
+ $channel = $this->findOrCreate($connection->app->id, $channelName);
+
+ $channel->subscribe($connection, $payload);
+ }
+
+ /**
+ * Unsubscribe the connection from the channel.
+ *
+ * @param \Ratchet\ConnectionInterface $connection
+ * @param string $channelName
+ * @param stdClass $payload
+ * @return void
+ */
+ public function unsubscribeFromChannel(ConnectionInterface $connection, string $channelName, stdClass $payload)
+ {
+ $channel = $this->findOrCreate($connection->app->id, $channelName);
+
+ $channel->unsubscribe($connection, $payload);
+ }
+
+ /**
+ * Subscribe the connection to a specific channel.
+ *
+ * @param string|int $appId
+ * @return void
+ */
+ public function subscribeToApp($appId)
+ {
+ //
+ }
+
+ /**
+ * Unsubscribe the connection from the channel.
+ *
+ * @param string|int $appId
+ * @return void
+ */
+ public function unsubscribeFromApp($appId)
+ {
+ //
+ }
+
+ /**
+ * Get the connections count on the app
+ * for the current server instance.
+ *
+ * @param string|int $appId
+ * @param string|null $channelName
+ * @return \React\Promise\PromiseInterface
+ */
+ public function getLocalConnectionsCount($appId, string $channelName = null): PromiseInterface
+ {
+ return $this->getLocalChannels($appId)
+ ->then(function ($channels) use ($channelName) {
+ return collect($channels)
+ ->when(! is_null($channelName), function ($collection) use ($channelName) {
+ return $collection->filter(function (Channel $channel) use ($channelName) {
+ return $channel->getName() === $channelName;
+ });
+ })
+ ->flatMap(function (Channel $channel) {
+ return collect($channel->getConnections())->pluck('socketId');
+ })
+ ->unique()
+ ->count();
+ });
+ }
+
+ /**
+ * Get the connections count
+ * across multiple servers.
+ *
+ * @param string|int $appId
+ * @param string|null $channelName
+ * @return \React\Promise\PromiseInterface
+ */
+ public function getGlobalConnectionsCount($appId, string $channelName = null): PromiseInterface
+ {
+ return $this->getLocalConnectionsCount($appId, $channelName);
+ }
+
+ /**
+ * Broadcast the message across multiple servers.
+ *
+ * @param string|int $appId
+ * @param string $channel
+ * @param stdClass $payload
+ * @return bool
+ */
+ public function broadcastAcrossServers($appId, string $channel, stdClass $payload)
+ {
+ return true;
+ }
+
+ /**
+ * Handle the user when it joined a presence channel.
+ *
+ * @param \Ratchet\ConnectionInterface $connection
+ * @param stdClass $user
+ * @param string $channel
+ * @param stdClass $payload
+ * @return void
+ */
+ public function userJoinedPresenceChannel(ConnectionInterface $connection, stdClass $user, string $channel, stdClass $payload)
+ {
+ $this->users["{$connection->app->id}:{$channel}"][$connection->socketId] = json_encode($user);
+ }
+
+ /**
+ * Handle the user when it left a presence channel.
+ *
+ * @param \Ratchet\ConnectionInterface $connection
+ * @param stdClass $user
+ * @param string $channel
+ * @param stdClass $payload
+ * @return void
+ */
+ public function userLeftPresenceChannel(ConnectionInterface $connection, stdClass $user, string $channel)
+ {
+ unset($this->users["{$connection->app->id}:{$channel}"][$connection->socketId]);
+ }
+
+ /**
+ * Get the presence channel members.
+ *
+ * @param string|int $appId
+ * @param string $channel
+ * @return \React\Promise\PromiseInterface
+ */
+ public function getChannelMembers($appId, string $channel): PromiseInterface
+ {
+ $members = $this->users["{$appId}:{$channel}"] ?? [];
+
+ $members = collect($members)->map(function ($user) {
+ return json_decode($user);
+ })->toArray();
+
+ return new FulfilledPromise($members);
+ }
+
+ /**
+ * Get a member from a presence channel based on connection.
+ *
+ * @param \Ratchet\ConnectionInterface $connection
+ * @param string $channel
+ * @return \React\Promise\PromiseInterface
+ */
+ public function getChannelMember(ConnectionInterface $connection, string $channel): PromiseInterface
+ {
+ $member = $this->users["{$connection->app->id}:{$channel}"][$connection->socketId] ?? null;
+
+ return new FulfilledPromise($member);
+ }
+
+ /**
+ * Get the presence channels total members count.
+ *
+ * @param string|int $appId
+ * @param array $channelNames
+ * @return \React\Promise\PromiseInterface
+ */
+ public function getChannelsMembersCount($appId, array $channelNames): PromiseInterface
+ {
+ $results = collect($channelNames)
+ ->reduce(function ($results, $channel) use ($appId) {
+ $results[$channel] = isset($this->users["{$appId}:{$channel}"])
+ ? count($this->users["{$appId}:{$channel}"])
+ : 0;
+
+ return $results;
+ }, []);
+
+ return new FulfilledPromise($results);
+ }
+
+ /**
+ * Get the channel class by the channel name.
+ *
+ * @param string $channelName
+ * @return string
+ */
+ protected function getChannelClassName(string $channelName): string
+ {
+ if (Str::startsWith($channelName, 'private-')) {
+ return PrivateChannel::class;
+ }
+
+ if (Str::startsWith($channelName, 'presence-')) {
+ return PresenceChannel::class;
+ }
+
+ return Channel::class;
+ }
+}
diff --git a/src/ChannelManagers/RedisChannelManager.php b/src/ChannelManagers/RedisChannelManager.php
new file mode 100644
index 0000000..ba7557e
--- /dev/null
+++ b/src/ChannelManagers/RedisChannelManager.php
@@ -0,0 +1,548 @@
+loop = $loop;
+
+ $connectionUri = $this->getConnectionUri();
+
+ $factoryClass = $factoryClass ?: Factory::class;
+ $factory = new $factoryClass($this->loop);
+
+ $this->publishClient = $factory->createLazyClient($connectionUri);
+ $this->subscribeClient = $factory->createLazyClient($connectionUri);
+
+ $this->subscribeClient->on('message', function ($channel, $payload) {
+ $this->onMessage($channel, $payload);
+ });
+
+ $this->serverId = Str::uuid()->toString();
+ }
+
+ /**
+ * Get all channels for a specific app
+ * for the current instance.
+ *
+ * @param string|int $appId
+ * @return \React\Promise\PromiseInterface[array]
+ */
+ public function getLocalChannels($appId): PromiseInterface
+ {
+ return parent::getLocalChannels($appId);
+ }
+
+ /**
+ * Get all channels for a specific app
+ * across multiple servers.
+ *
+ * @param string|int $appId
+ * @return \React\Promise\PromiseInterface[array]
+ */
+ public function getGlobalChannels($appId): PromiseInterface
+ {
+ return $this->getPublishClient()->smembers(
+ $this->getRedisKey($appId, null, ['channels'])
+ );
+ }
+
+ /**
+ * Remove connection from all channels.
+ *
+ * @param \Ratchet\ConnectionInterface $connection
+ * @return void
+ */
+ public function unsubscribeFromAllChannels(ConnectionInterface $connection)
+ {
+ $this->getGlobalChannels($connection->app->id)
+ ->then(function ($channels) use ($connection) {
+ foreach ($channels as $channel) {
+ $this->unsubscribeFromChannel(
+ $connection, $channel, new stdClass
+ );
+ }
+ });
+
+ parent::unsubscribeFromAllChannels($connection);
+ }
+
+ /**
+ * Subscribe the connection to a specific channel.
+ *
+ * @param \Ratchet\ConnectionInterface $connection
+ * @param string $channelName
+ * @param stdClass $payload
+ * @return void
+ */
+ public function subscribeToChannel(ConnectionInterface $connection, string $channelName, stdClass $payload)
+ {
+ $this->getGlobalConnectionsCount($connection->app->id, $channelName)
+ ->then(function ($count) use ($connection, $channelName) {
+ if ($count === 0) {
+ $this->subscribeToTopic($connection->app->id, $channelName);
+ }
+ });
+
+ $this->getPublishClient()->sadd(
+ $this->getRedisKey($connection->app->id, null, ['channels']),
+ $channelName
+ );
+
+ $this->incrementSubscriptionsCount(
+ $connection->app->id, $channelName, 1
+ );
+
+ parent::subscribeToChannel($connection, $channelName, $payload);
+ }
+
+ /**
+ * Unsubscribe the connection from the channel.
+ *
+ * @param \Ratchet\ConnectionInterface $connection
+ * @param string $channelName
+ * @param stdClass $payload
+ * @return void
+ */
+ public function unsubscribeFromChannel(ConnectionInterface $connection, string $channelName, stdClass $payload)
+ {
+ $this->getGlobalConnectionsCount($connection->app->id, $channelName)
+ ->then(function ($count) use ($connection, $channelName) {
+ if ($count === 0) {
+ $this->unsubscribeFromTopic($connection->app->id, $channelName);
+
+ $this->getPublishClient()->srem(
+ $this->getRedisKey($connection->app->id, null, ['channels']),
+ $channelName
+ );
+
+ return;
+ }
+
+ $increment = $this->incrementSubscriptionsCount(
+ $connection->app->id, $channelName, -1
+ )
+ ->then(function ($count) use ($connection, $channelName) {
+ if ($count < 1) {
+ $this->unsubscribeFromTopic($connection->app->id, $channelName);
+
+ $this->getPublishClient()->srem(
+ $this->getRedisKey($connection->app->id, null, ['channels']),
+ $channelName
+ );
+ }
+ });
+ });
+
+ parent::unsubscribeFromChannel($connection, $channelName, $payload);
+ }
+
+ /**
+ * Subscribe the connection to a specific channel.
+ *
+ * @param string|int $appId
+ * @return void
+ */
+ public function subscribeToApp($appId)
+ {
+ $this->subscribeToTopic($appId);
+
+ $this->incrementSubscriptionsCount($appId);
+ }
+
+ /**
+ * Unsubscribe the connection from the channel.
+ *
+ * @param string|int $appId
+ * @return void
+ */
+ public function unsubscribeFromApp($appId)
+ {
+ $this->unsubscribeFromTopic($appId);
+
+ $this->incrementSubscriptionsCount($appId, null, -1);
+ }
+
+ /**
+ * Get the connections count on the app
+ * for the current server instance.
+ *
+ * @param string|int $appId
+ * @param string|null $channelName
+ * @return \React\Promise\PromiseInterface
+ */
+ public function getLocalConnectionsCount($appId, string $channelName = null): PromiseInterface
+ {
+ return parent::getLocalConnectionsCount($appId, $channelName);
+ }
+
+ /**
+ * Get the connections count
+ * across multiple servers.
+ *
+ * @param string|int $appId
+ * @param string|null $channelName
+ * @return \React\Promise\PromiseInterface
+ */
+ public function getGlobalConnectionsCount($appId, string $channelName = null): PromiseInterface
+ {
+ return $this->publishClient
+ ->hget($this->getRedisKey($appId, $channelName, ['stats']), 'connections')
+ ->then(function ($count) {
+ return is_null($count) ? 0 : (int) $count;
+ });
+ }
+
+ /**
+ * Broadcast the message across multiple servers.
+ *
+ * @param string|int $appId
+ * @param string $channel
+ * @param stdClass $payload
+ * @return bool
+ */
+ public function broadcastAcrossServers($appId, string $channel, stdClass $payload)
+ {
+ $payload->appId = $appId;
+ $payload->serverId = $this->getServerId();
+
+ $this->publishClient->publish($this->getRedisKey($appId, $channel), json_encode($payload));
+
+ return true;
+ }
+
+ /**
+ * Handle the user when it joined a presence channel.
+ *
+ * @param \Ratchet\ConnectionInterface $connection
+ * @param stdClass $user
+ * @param string $channel
+ * @param stdClass $payload
+ * @return void
+ */
+ public function userJoinedPresenceChannel(ConnectionInterface $connection, stdClass $user, string $channel, stdClass $payload)
+ {
+ $this->storeUserData(
+ $connection->app->id, $channel, $connection->socketId, json_encode($user)
+ );
+ }
+
+ /**
+ * Handle the user when it left a presence channel.
+ *
+ * @param \Ratchet\ConnectionInterface $connection
+ * @param stdClass $user
+ * @param string $channel
+ * @param stdClass $payload
+ * @return void
+ */
+ public function userLeftPresenceChannel(ConnectionInterface $connection, stdClass $user, string $channel)
+ {
+ $this->removeUserData(
+ $connection->app->id, $channel, $connection->socketId
+ );
+ }
+
+ /**
+ * Get the presence channel members.
+ *
+ * @param string|int $appId
+ * @param string $channel
+ * @return \React\Promise\PromiseInterface
+ */
+ public function getChannelMembers($appId, string $channel): PromiseInterface
+ {
+ return $this->publishClient
+ ->hgetall($this->getRedisKey($appId, $channel, ['users']))
+ ->then(function ($members) {
+ [$keys, $values] = collect($members)->partition(function ($value, $key) {
+ return $key % 2 === 0;
+ });
+
+ return collect(array_combine($keys->all(), $values->all()))
+ ->map(function ($user) {
+ return json_decode($user);
+ })
+ ->toArray();
+ });
+ }
+
+ /**
+ * Get a member from a presence channel based on connection.
+ *
+ * @param \Ratchet\ConnectionInterface $connection
+ * @param string $channel
+ * @return \React\Promise\PromiseInterface
+ */
+ public function getChannelMember(ConnectionInterface $connection, string $channel): PromiseInterface
+ {
+ return $this->publishClient->hget(
+ $this->getRedisKey($connection->app->id, $channel, ['users']), $connection->socketId
+ );
+ }
+
+ /**
+ * Get the presence channels total members count.
+ *
+ * @param string|int $appId
+ * @param array $channelNames
+ * @return \React\Promise\PromiseInterface
+ */
+ public function getChannelsMembersCount($appId, array $channelNames): PromiseInterface
+ {
+ $this->publishClient->multi();
+
+ foreach ($channelNames as $channel) {
+ $this->publishClient->hlen(
+ $this->getRedisKey($appId, $channel, ['users'])
+ );
+ }
+
+ return $this->publishClient
+ ->exec()
+ ->then(function ($data) use ($channelNames) {
+ return array_combine($channelNames, $data);
+ });
+ }
+
+ /**
+ * Handle a message received from Redis on a specific channel.
+ *
+ * @param string $redisChannel
+ * @param string $payload
+ * @return void
+ */
+ public function onMessage(string $redisChannel, string $payload)
+ {
+ $payload = json_decode($payload);
+
+ if (isset($payload->serverId) && $this->getServerId() === $payload->serverId) {
+ return;
+ }
+
+ $payload->channel = Str::after($redisChannel, "{$payload->appId}:");
+
+ if (! $channel = $this->find($payload->appId, $payload->channel)) {
+ return;
+ }
+
+ $appId = $payload->appId ?? null;
+ $socketId = $payload->socketId ?? null;
+ $serverId = $payload->serverId ?? null;
+
+ unset($payload->socketId);
+ unset($payload->serverId);
+ unset($payload->appId);
+
+ $channel->broadcastToEveryoneExcept($payload, $socketId, $appId, false);
+ }
+
+ /**
+ * Build the Redis connection URL from Laravel database config.
+ *
+ * @return string
+ */
+ protected function getConnectionUri()
+ {
+ $name = config('websockets.replication.redis.connection', 'default');
+ $config = config("database.redis.{$name}");
+
+ $host = $config['host'];
+ $port = $config['port'] ?: 6379;
+
+ $query = [];
+
+ if ($config['password']) {
+ $query['password'] = $config['password'];
+ }
+
+ if ($config['database']) {
+ $query['database'] = $config['database'];
+ }
+
+ $query = http_build_query($query);
+
+ return "redis://{$host}:{$port}".($query ? "?{$query}" : '');
+ }
+
+ /**
+ * Get the Subscribe client instance.
+ *
+ * @return Client
+ */
+ public function getSubscribeClient()
+ {
+ return $this->subscribeClient;
+ }
+
+ /**
+ * Get the Publish client instance.
+ *
+ * @return Client
+ */
+ public function getPublishClient()
+ {
+ return $this->publishClient;
+ }
+
+ /**
+ * Get the unique identifier for the server.
+ *
+ * @return string
+ */
+ public function getServerId()
+ {
+ return $this->serverId;
+ }
+
+ /**
+ * Increment the subscribed count number.
+ *
+ * @param string|int $appId
+ * @param string|null $channel
+ * @param int $increment
+ * @return PromiseInterface
+ */
+ public function incrementSubscriptionsCount($appId, string $channel = null, int $increment = 1)
+ {
+ return $this->publishClient->hincrby(
+ $this->getRedisKey($appId, $channel, ['stats']), 'connections', $increment
+ );
+ }
+
+ /**
+ * Set data for a topic. Might be used for the presence channels.
+ *
+ * @param string|int $appId
+ * @param string|null $channel
+ * @param string $key
+ * @param mixed $data
+ * @return PromiseInterface
+ */
+ public function storeUserData($appId, string $channel = null, string $key, $data)
+ {
+ $this->publishClient->hset(
+ $this->getRedisKey($appId, $channel, ['users']), $key, $data
+ );
+ }
+
+ /**
+ * Remove data for a topic. Might be used for the presence channels.
+ *
+ * @param string|int $appId
+ * @param string|null $channel
+ * @param string $key
+ * @return PromiseInterface
+ */
+ public function removeUserData($appId, string $channel = null, string $key)
+ {
+ return $this->publishClient->hdel(
+ $this->getRedisKey($appId, $channel), $key
+ );
+ }
+
+ /**
+ * Subscribe to the topic for the app, or app and channel.
+ *
+ * @param string|int $appId
+ * @param string|null $channel
+ * @return void
+ */
+ public function subscribeToTopic($appId, string $channel = null)
+ {
+ $this->subscribeClient->subscribe(
+ $this->getRedisKey($appId, $channel)
+ );
+ }
+
+ /**
+ * Unsubscribe from the topic for the app, or app and channel.
+ *
+ * @param string|int $appId
+ * @param string|null $channel
+ * @return void
+ */
+ public function unsubscribeFromTopic($appId, string $channel = null)
+ {
+ $this->subscribeClient->unsubscribe(
+ $this->getRedisKey($appId, $channel)
+ );
+ }
+
+ /**
+ * Get the Redis Keyspace name to handle subscriptions
+ * and other key-value sets.
+ *
+ * @param mixed $appId
+ * @param string|null $channel
+ * @return string
+ */
+ public function getRedisKey($appId, string $channel = null, array $suffixes = []): string
+ {
+ $prefix = config('database.redis.options.prefix', null);
+
+ $hash = "{$prefix}{$appId}";
+
+ if ($channel) {
+ $hash .= ":{$channel}";
+ }
+
+ $suffixes = join(':', $suffixes);
+
+ if ($suffixes) {
+ $hash .= $suffixes;
+ }
+
+ return $hash;
+ }
+}
diff --git a/src/Channels/Channel.php b/src/Channels/Channel.php
new file mode 100644
index 0000000..c21e951
--- /dev/null
+++ b/src/Channels/Channel.php
@@ -0,0 +1,190 @@
+name = $name;
+ $this->channelManager = app(ChannelManager::class);
+ }
+
+ /**
+ * Get channel name.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Get the list of subscribed connections.
+ *
+ * @return array
+ */
+ public function getConnections()
+ {
+ return $this->connections;
+ }
+
+ /**
+ * Check if the channel has connections.
+ *
+ * @return bool
+ */
+ public function hasConnections(): bool
+ {
+ return count($this->getConnections()) > 0;
+ }
+
+ /**
+ * Add a new connection to the channel.
+ *
+ * @see https://pusher.com/docs/pusher_protocol#presence-channel-events
+ * @param \Ratchet\ConnectionInterface $connection
+ * @param \stdClass $payload
+ * @return void
+ */
+ public function subscribe(ConnectionInterface $connection, stdClass $payload)
+ {
+ $this->saveConnection($connection);
+
+ $connection->send(json_encode([
+ 'event' => 'pusher_internal:subscription_succeeded',
+ 'channel' => $this->getName(),
+ ]));
+
+ DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_SUBSCRIBED, [
+ 'socketId' => $connection->socketId,
+ 'channel' => $this->getName(),
+ ]);
+ }
+
+ /**
+ * Unsubscribe connection from the channel.
+ *
+ * @param \Ratchet\ConnectionInterface $connection
+ * @return void
+ */
+ public function unsubscribe(ConnectionInterface $connection)
+ {
+ if (! isset($this->connections[$connection->socketId])) {
+ return;
+ }
+
+ unset($this->connections[$connection->socketId]);
+ }
+
+ /**
+ * Store the connection to the subscribers list.
+ *
+ * @param \Ratchet\ConnectionInterface $connection
+ * @return void
+ */
+ protected function saveConnection(ConnectionInterface $connection)
+ {
+ $this->connections[$connection->socketId] = $connection;
+ }
+
+ /**
+ * Broadcast a payload to the subscribed connections.
+ *
+ * @param string|int $appId
+ * @param \stdClass $payload
+ * @param bool $replicate
+ * @return bool
+ */
+ public function broadcast($appId, stdClass $payload, bool $replicate = true): bool
+ {
+ collect($this->getConnections())
+ ->each->send(json_encode($payload));
+
+ if ($replicate) {
+ $this->channelManager->broadcastAcrossServers($appId, $this->getName(), $payload);
+ }
+
+ return true;
+ }
+
+ /**
+ * Broadcast the payload, but exclude a specific socket id.
+ *
+ * @param \stdClass $payload
+ * @param string|null $socketId
+ * @param string|int $appId
+ * @param bool $replicate
+ * @return bool
+ */
+ public function broadcastToEveryoneExcept(stdClass $payload, ?string $socketId, $appId, bool $replicate = true)
+ {
+ if ($replicate) {
+ $this->channelManager->broadcastAcrossServers($appId, $this->getName(), $payload);
+ }
+
+ if (is_null($socketId)) {
+ return $this->broadcast($appId, $payload, $replicate);
+ }
+
+ collect($this->getConnections())->each(function (ConnectionInterface $connection) use ($socketId, $payload) {
+ if ($connection->socketId !== $socketId) {
+ $connection->send(json_encode($payload));
+ }
+ });
+
+ return true;
+ }
+
+ /**
+ * Check if the signature for the payload is valid.
+ *
+ * @param \Ratchet\ConnectionInterface $connection
+ * @param \stdClass $payload
+ * @return void
+ * @throws InvalidSignature
+ */
+ protected function verifySignature(ConnectionInterface $connection, stdClass $payload)
+ {
+ $signature = "{$connection->socketId}:{$this->getName()}";
+
+ if (isset($payload->channel_data)) {
+ $signature .= ":{$payload->channel_data}";
+ }
+
+ if (! hash_equals(
+ hash_hmac('sha256', $signature, $connection->app->secret),
+ Str::after($payload->auth, ':'))
+ ) {
+ throw new InvalidSignature;
+ }
+ }
+}
diff --git a/src/Channels/PresenceChannel.php b/src/Channels/PresenceChannel.php
new file mode 100644
index 0000000..75808b9
--- /dev/null
+++ b/src/Channels/PresenceChannel.php
@@ -0,0 +1,139 @@
+channelManager->userJoinedPresenceChannel(
+ $connection,
+ $user = json_decode($payload->channel_data),
+ $this->getName(),
+ $payload
+ );
+
+ $this->channelManager
+ ->getChannelMembers($connection->app->id, $this->getName())
+ ->then(function ($users) use ($connection) {
+ $connection->send(json_encode([
+ 'event' => 'pusher_internal:subscription_succeeded',
+ 'channel' => $this->getName(),
+ 'data' => json_encode($this->getChannelData($users)),
+ ]));
+ });
+
+ $memberAddedPayload = [
+ 'event' => 'pusher_internal:member_added',
+ 'channel' => $this->getName(),
+ 'data' => $payload->channel_data,
+ ];
+
+ $this->broadcastToEveryoneExcept(
+ (object) $memberAddedPayload, $connection->socketId,
+ $connection->app->id
+ );
+ }
+
+ /**
+ * Unsubscribe connection from the channel.
+ *
+ * @param \Ratchet\ConnectionInterface $connection
+ * @return void
+ */
+ public function unsubscribe(ConnectionInterface $connection)
+ {
+ parent::unsubscribe($connection);
+
+ $this->channelManager
+ ->getChannelMember($connection, $this->getName())
+ ->then(function ($user) use ($connection) {
+ $user = @json_decode($user);
+
+ if (! $user) {
+ return;
+ }
+
+ $this->channelManager->userLeftPresenceChannel(
+ $connection, $user, $this->getName()
+ );
+
+ $memberRemovedPayload = [
+ 'event' => 'pusher_internal:member_removed',
+ 'channel' => $this->getName(),
+ 'data' => json_encode([
+ 'user_id' => $user->user_id,
+ ]),
+ ];
+
+ $this->broadcastToEveryoneExcept(
+ (object) $memberRemovedPayload, $connection->socketId,
+ $connection->app->id
+ );
+ });
+ }
+
+ /**
+ * Get the Presence channel data.
+ *
+ * @param array $users
+ * @return array
+ */
+ protected function getChannelData(array $users): array
+ {
+ return [
+ 'presence' => [
+ 'ids' => $this->getUserIds($users),
+ 'hash' => $this->getHash($users),
+ 'count' => count($users),
+ ],
+ ];
+ }
+
+ /**
+ * Get the Presence Channel's users.
+ *
+ * @param array $users
+ * @return array
+ */
+ protected function getUserIds(array $users): array
+ {
+ return collect($users)
+ ->map(function ($user) {
+ return (string) $user->user_id;
+ })
+ ->values();
+ }
+
+ /**
+ * Compute the hash for the presence channel integrity.
+ *
+ * @param array $users
+ * @return array
+ */
+ protected function getHash(array $users): array
+ {
+ $hash = [];
+
+ foreach ($users as $socketId => $user) {
+ $hash[$user->user_id] = $user->user_info ?? [];
+ }
+
+ return $hash;
+ }
+}
diff --git a/src/WebSockets/Channels/PrivateChannel.php b/src/Channels/PrivateChannel.php
similarity index 81%
rename from src/WebSockets/Channels/PrivateChannel.php
rename to src/Channels/PrivateChannel.php
index 5f84308..e5d987c 100644
--- a/src/WebSockets/Channels/PrivateChannel.php
+++ b/src/Channels/PrivateChannel.php
@@ -1,8 +1,8 @@
comment('Cleaning WebSocket Statistics...');
- $amountDeleted = $driver::delete($this->argument('appId'));
+ $days = $this->option('days') ?: config('statistics.delete_statistics_older_than_days');
- $this->info("Deleted {$amountDeleted} record(s) from the WebSocket statistics.");
+ $amountDeleted = StatisticsStore::delete(
+ now()->subDays($days), $this->argument('appId')
+ );
+
+ $this->info("Deleted {$amountDeleted} record(s) from the WebSocket statistics storage.");
}
}
diff --git a/src/Console/RestartWebSocketServer.php b/src/Console/Commands/RestartServer.php
similarity index 56%
rename from src/Console/RestartWebSocketServer.php
rename to src/Console/Commands/RestartServer.php
index eac1b65..69fe58f 100644
--- a/src/Console/RestartWebSocketServer.php
+++ b/src/Console/Commands/RestartServer.php
@@ -1,12 +1,12 @@
currentTime());
+ Cache::forever(
+ 'beyondcode:websockets:restart',
+ $this->currentTime()
+ );
- $this->info('Broadcasting WebSocket server restart signal.');
+ $this->info(
+ 'Broadcasted the restart signal to the WebSocket server!'
+ );
}
}
diff --git a/src/Console/StartWebSocketServer.php b/src/Console/Commands/StartServer.php
similarity index 51%
rename from src/Console/StartWebSocketServer.php
rename to src/Console/Commands/StartServer.php
index 0707e05..4ad9338 100644
--- a/src/Console/StartWebSocketServer.php
+++ b/src/Console/Commands/StartServer.php
@@ -1,22 +1,20 @@
configureStatisticsLogger()
- ->configureHttpLogger()
- ->configureMessageLogger()
- ->configureConnectionLogger()
- ->configureRestartTimer()
- ->configurePubSub()
- ->registerRoutes()
- ->startWebSocketServer();
+ $this->configureLoggers();
+
+ $this->configureManagers();
+
+ $this->configureStatistics();
+
+ $this->configureRestartTimer();
+
+ $this->startServer();
}
/**
- * Configure the statistics logger class.
+ * Configure the loggers used for the console.
*
- * @return $this
+ * @return void
*/
- protected function configureStatisticsLogger()
+ protected function configureLoggers()
{
- $this->laravel->singleton(StatisticsLoggerInterface::class, function () {
- $replicationDriver = config('websockets.replication.driver', 'local');
-
- $class = config("websockets.replication.{$replicationDriver}.statistics_logger", \BeyondCode\LaravelWebSockets\Statistics\Logger\MemoryStatisticsLogger::class);
-
- return new $class(
- $this->laravel->make(ChannelManager::class),
- $this->laravel->make(StatisticsDriver::class)
- );
- });
-
- $this->loop->addPeriodicTimer($this->option('statistics-interval') ?: config('websockets.statistics.interval_in_seconds'), function () {
- $this->line('Saving statistics...');
-
- StatisticsLogger::save();
- });
-
- return $this;
+ $this->configureHttpLogger();
+ $this->configureMessageLogger();
+ $this->configureConnectionLogger();
}
/**
- * Configure the HTTP logger class.
+ * Register the managers that are not resolved
+ * in the package service provider.
*
- * @return $this
+ * @return void
*/
- protected function configureHttpLogger()
+ protected function configureManagers()
{
- $this->laravel->singleton(HttpLogger::class, function () {
- return (new HttpLogger($this->output))
- ->enable($this->option('debug') ?: config('app.debug'))
- ->verbose($this->output->isVerbose());
- });
+ $this->laravel->singleton(ChannelManager::class, function () {
+ $mode = config('websockets.replication.mode', 'local');
- return $this;
+ $class = config("websockets.replication.modes.{$mode}.channel_manager");
+
+ return new $class($this->loop);
+ });
}
/**
- * Configure the logger for messages.
+ * Register the Statistics Collectors that
+ * are not resolved in the package service provider.
*
- * @return $this
+ * @return void
*/
- protected function configureMessageLogger()
+ protected function configureStatistics()
{
- $this->laravel->singleton(WebsocketsLogger::class, function () {
- return (new WebsocketsLogger($this->output))
- ->enable($this->option('debug') ?: config('app.debug'))
- ->verbose($this->output->isVerbose());
+ $this->laravel->singleton(StatisticsCollector::class, function () {
+ $replicationMode = config('websockets.replication.mode', 'local');
+
+ $class = config("websockets.replication.modes.{$replicationMode}.collector");
+
+ return new $class;
});
- return $this;
+ $this->laravel->singleton(StatisticsStore::class, function () {
+ $class = config('websockets.statistics.store');
+
+ return new $class;
+ });
+
+ if (! $this->option('disable-statistics')) {
+ $intervalInSeconds = $this->option('statistics-interval') ?: config('websockets.statistics.interval_in_seconds', 3600);
+
+ $this->loop->addPeriodicTimer($intervalInSeconds, function () {
+ $this->line('Saving statistics...');
+
+ StatisticsCollectorFacade::save();
+ });
+ }
}
/**
- * Configure the connection logger.
+ * Configure the restart timer.
*
- * @return $this
- */
- protected function configureConnectionLogger()
- {
- $this->laravel->bind(ConnectionLogger::class, function () {
- return (new ConnectionLogger($this->output))
- ->enable(config('app.debug'))
- ->verbose($this->output->isVerbose());
- });
-
- return $this;
- }
-
- /**
- * Configure the Redis PubSub handler.
- *
- * @return $this
+ * @return void
*/
public function configureRestartTimer()
{
@@ -178,45 +157,48 @@ class StartWebSocketServer extends Command
$this->loop->stop();
}
});
-
- return $this;
}
/**
- * Configure the replicators.
+ * Configure the HTTP logger class.
*
* @return void
*/
- public function configurePubSub()
+ protected function configureHttpLogger()
{
- $this->laravel->singleton(ReplicationInterface::class, function () {
- $driver = config('websockets.replication.driver', 'local');
-
- $client = config(
- "websockets.replication.{$driver}.client",
- \BeyondCode\LaravelWebSockets\PubSub\Drivers\LocalClient::class
- );
-
- return (new $client)->boot($this->loop);
+ $this->laravel->singleton(HttpLogger::class, function () {
+ return (new HttpLogger($this->output))
+ ->enable($this->option('debug') ?: config('app.debug'))
+ ->verbose($this->output->isVerbose());
});
-
- $this->laravel
- ->get(ReplicationInterface::class)
- ->boot($this->loop);
-
- return $this;
}
/**
- * Register the routes.
+ * Configure the logger for messages.
*
- * @return $this
+ * @return void
*/
- protected function registerRoutes()
+ protected function configureMessageLogger()
{
- WebSocketsRouter::routes();
+ $this->laravel->singleton(WebSocketsLogger::class, function () {
+ return (new WebSocketsLogger($this->output))
+ ->enable($this->option('debug') ?: config('app.debug'))
+ ->verbose($this->output->isVerbose());
+ });
+ }
- return $this;
+ /**
+ * Configure the connection logger.
+ *
+ * @return void
+ */
+ protected function configureConnectionLogger()
+ {
+ $this->laravel->bind(ConnectionLogger::class, function () {
+ return (new ConnectionLogger($this->output))
+ ->enable(config('app.debug'))
+ ->verbose($this->output->isVerbose());
+ });
}
/**
@@ -224,7 +206,7 @@ class StartWebSocketServer extends Command
*
* @return void
*/
- protected function startWebSocketServer()
+ protected function startServer()
{
$this->info("Starting the WebSocket server on port {$this->option('port')}...");
@@ -238,7 +220,6 @@ class StartWebSocketServer extends Command
});
}
- /* 🛰 Start the server 🛰 */
$this->server->run();
}
@@ -249,13 +230,13 @@ class StartWebSocketServer extends Command
*/
protected function buildServer()
{
- $this->server = new WebSocketServerFactory(
+ $this->server = new ServerFactory(
$this->option('host'), $this->option('port')
);
$this->server = $this->server
->setLoop($this->loop)
- ->useRoutes(WebSocketsRouter::getRoutes())
+ ->withRoutes(WebSocketsRouter::getRoutes())
->setConsoleOutput($this->output)
->createServer();
}
@@ -267,6 +248,8 @@ class StartWebSocketServer extends Command
*/
protected function getLastRestart()
{
- return Cache::get('beyondcode:websockets:restart', 0);
+ return Cache::get(
+ 'beyondcode:websockets:restart', 0
+ );
}
}
diff --git a/src/Apps/AppManager.php b/src/Contracts/AppManager.php
similarity index 88%
rename from src/Apps/AppManager.php
rename to src/Contracts/AppManager.php
index 86497c0..153eda8 100644
--- a/src/Apps/AppManager.php
+++ b/src/Contracts/AppManager.php
@@ -1,6 +1,8 @@
header('x-app-id'));
+ $app = App::findById($request->header('X-App-Id'));
$broadcaster = $this->getPusherBroadcaster([
'key' => $app->key,
diff --git a/src/Dashboard/Http/Controllers/SendMessage.php b/src/Dashboard/Http/Controllers/SendMessage.php
index ae359ee..90155d1 100644
--- a/src/Dashboard/Http/Controllers/SendMessage.php
+++ b/src/Dashboard/Http/Controllers/SendMessage.php
@@ -2,51 +2,53 @@
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
-use BeyondCode\LaravelWebSockets\Concerns\PushesToPusher;
-use BeyondCode\LaravelWebSockets\Statistics\Rules\AppId;
+use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
+use BeyondCode\LaravelWebSockets\Rules\AppId;
use Exception;
use Illuminate\Http\Request;
class SendMessage
{
- use PushesToPusher;
-
/**
* Send the message to the requested channel.
*
* @param \Illuminate\Http\Request $request
+ * @param \BeyondCode\LaravelWebSockets\Contracts\ChannelManager $channelManager
* @return \Illuminate\Http\Response
*/
- public function __invoke(Request $request)
+ public function __invoke(Request $request, ChannelManager $channelManager)
{
$request->validate([
'appId' => ['required', new AppId],
- 'key' => 'required|string',
- 'secret' => 'required|string',
'channel' => 'required|string',
'event' => 'required|string',
'data' => 'required|json',
]);
- $broadcaster = $this->getPusherBroadcaster([
- 'key' => $request->key,
- 'secret' => $request->secret,
- 'id' => $request->appId,
- ]);
+ $payload = [
+ 'channel' => $request->channel,
+ 'event' => $request->event,
+ 'data' => json_decode($request->data, true),
+ ];
- try {
- $decodedData = @json_decode($request->data, true);
+ // Here you can use the ->find(), even if the channel
+ // does not exist on the server. If it does not exist,
+ // then the message simply will get broadcasted
+ // across the other servers.
+ $channel = $channelManager->find(
+ $request->appId, $request->channel
+ );
- $broadcaster->broadcast(
- [$request->channel],
- $request->event,
- $decodedData ?: []
+ if ($channel) {
+ $channel->broadcastToEveryoneExcept(
+ (object) $payload,
+ null,
+ $request->appId
+ );
+ } else {
+ $channelManager->broadcastAcrossServers(
+ $request->appId, $request->channel, (object) $payload
);
- } catch (Exception $e) {
- return response()->json([
- 'ok' => false,
- 'exception' => $e->getMessage(),
- ]);
}
return response()->json([
diff --git a/src/Dashboard/Http/Controllers/ShowDashboard.php b/src/Dashboard/Http/Controllers/ShowDashboard.php
index f6dc6b1..eabd22d 100644
--- a/src/Dashboard/Http/Controllers/ShowDashboard.php
+++ b/src/Dashboard/Http/Controllers/ShowDashboard.php
@@ -2,8 +2,8 @@
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
-use BeyondCode\LaravelWebSockets\Apps\AppManager;
-use BeyondCode\LaravelWebSockets\Dashboard\DashboardLogger;
+use BeyondCode\LaravelWebSockets\Contracts\AppManager;
+use BeyondCode\LaravelWebSockets\DashboardLogger;
use Illuminate\Http\Request;
class ShowDashboard
@@ -12,7 +12,7 @@ class ShowDashboard
* Show the dashboard.
*
* @param \Illuminate\Http\Request $request
- * @param \BeyondCode\LaravelWebSockets\Apps\AppManager $apps
+ * @param \BeyondCode\LaravelWebSockets\Contracts\AppManager $apps
* @return void
*/
public function __invoke(Request $request, AppManager $apps)
diff --git a/src/Dashboard/Http/Controllers/ShowStatistics.php b/src/Dashboard/Http/Controllers/ShowStatistics.php
index 134cb62..cec51c6 100644
--- a/src/Dashboard/Http/Controllers/ShowStatistics.php
+++ b/src/Dashboard/Http/Controllers/ShowStatistics.php
@@ -2,7 +2,7 @@
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
-use BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver;
+use BeyondCode\LaravelWebSockets\Facades\StatisticsStore;
use Illuminate\Http\Request;
class ShowStatistics
@@ -11,12 +11,23 @@ class ShowStatistics
* Get statistics for an app ID.
*
* @param \Illuminate\Http\Request $request
- * @param \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver $driver
* @param mixed $appId
* @return \Illuminate\Http\Response
*/
- public function __invoke(Request $request, StatisticsDriver $driver, $appId)
+ public function __invoke(Request $request, $appId)
{
- return $driver::get($appId, $request);
+ $processQuery = function ($query) use ($appId) {
+ return $query->whereAppId($appId)
+ ->latest()
+ ->limit(120);
+ };
+
+ $processCollection = function ($collection) {
+ return $collection->reverse();
+ };
+
+ return StatisticsStore::getForGraph(
+ $processQuery, $processCollection
+ );
}
}
diff --git a/src/Dashboard/DashboardLogger.php b/src/DashboardLogger.php
similarity index 70%
rename from src/Dashboard/DashboardLogger.php
rename to src/DashboardLogger.php
index 70397ce..cfd09ba 100644
--- a/src/Dashboard/DashboardLogger.php
+++ b/src/DashboardLogger.php
@@ -1,8 +1,8 @@
find($appId, $channelName);
-
- optional($channel)->broadcast([
- 'event' => 'log-message',
+ $payload = [
'channel' => $channelName,
+ 'event' => 'log-message',
'data' => [
'type' => $type,
'time' => strftime('%H:%M:%S'),
'details' => $details,
],
- ]);
+ ];
+
+ // Here you can use the ->find(), even if the channel
+ // does not exist on the server. If it does not exist,
+ // then the message simply will get broadcasted
+ // across the other servers.
+ $channel = $channelManager->find($appId, $channelName);
+
+ if ($channel) {
+ $channel->broadcastToEveryoneExcept(
+ (object) $payload,
+ null,
+ $appId
+ );
+ } else {
+ $channelManager->broadcastAcrossServers(
+ $appId, $channelName, (object) $payload
+ );
+ }
}
}
diff --git a/src/Events/MessagesBroadcasted.php b/src/Events/MessagesBroadcasted.php
deleted file mode 100644
index 5f78870..0000000
--- a/src/Events/MessagesBroadcasted.php
+++ /dev/null
@@ -1,29 +0,0 @@
-sentMessagesCount = $sentMessagesCount;
- }
-}
diff --git a/src/Events/Subscribed.php b/src/Events/Subscribed.php
deleted file mode 100644
index 9bdae48..0000000
--- a/src/Events/Subscribed.php
+++ /dev/null
@@ -1,39 +0,0 @@
-channelName = $channelName;
- $this->connection = $connection;
- }
-}
diff --git a/src/Events/Unsubscribed.php b/src/Events/Unsubscribed.php
deleted file mode 100644
index 66c412a..0000000
--- a/src/Events/Unsubscribed.php
+++ /dev/null
@@ -1,39 +0,0 @@
-channelName = $channelName;
- $this->connection = $connection;
- }
-}
diff --git a/src/Facades/StatisticsCollector.php b/src/Facades/StatisticsCollector.php
new file mode 100644
index 0000000..5dd1377
--- /dev/null
+++ b/src/Facades/StatisticsCollector.php
@@ -0,0 +1,19 @@
+channelManager->find($request->appId, $request->channelName);
-
- if (is_null($channel)) {
- throw new HttpException(404, "Unknown channel `{$request->channelName}`.");
- }
-
- return $channel->toArray($request->appId);
- }
-}
diff --git a/src/HttpApi/Controllers/FetchChannelsController.php b/src/HttpApi/Controllers/FetchChannelsController.php
deleted file mode 100644
index bb0d24e..0000000
--- a/src/HttpApi/Controllers/FetchChannelsController.php
+++ /dev/null
@@ -1,67 +0,0 @@
-has('info')) {
- $attributes = explode(',', trim($request->info));
-
- if (in_array('user_count', $attributes) && ! Str::startsWith($request->filter_by_prefix, 'presence-')) {
- throw new HttpException(400, 'Request must be limited to presence channels in order to fetch user_count');
- }
- }
-
- $channels = Collection::make($this->channelManager->getChannels($request->appId));
-
- if ($request->has('filter_by_prefix')) {
- $channels = $channels->filter(function ($channel, $channelName) use ($request) {
- return Str::startsWith($channelName, $request->filter_by_prefix);
- });
- }
-
- // We want to get the channel user count all in one shot when
- // using a replication backend rather than doing individual queries.
- // To do so, we first collect the list of channel names.
- $channelNames = $channels->map(function (PresenceChannel $channel) {
- return $channel->getChannelName();
- })->toArray();
-
- // We ask the replication backend to get us the member count per channel.
- // We get $counts back as a key-value array of channel names and their member count.
- return $this->replicator
- ->channelMemberCounts($request->appId, $channelNames)
- ->then(function (array $counts) use ($channels, $attributes) {
- $channels = $channels->map(function (PresenceChannel $channel) use ($counts, $attributes) {
- $info = new stdClass;
-
- if (in_array('user_count', $attributes)) {
- $info->user_count = $counts[$channel->getChannelName()];
- }
-
- return $info;
- })->toArray();
-
- return [
- 'channels' => $channels ?: new stdClass,
- ];
- });
- }
-}
diff --git a/src/HttpApi/Controllers/FetchUsersController.php b/src/HttpApi/Controllers/FetchUsersController.php
deleted file mode 100644
index 25acee9..0000000
--- a/src/HttpApi/Controllers/FetchUsersController.php
+++ /dev/null
@@ -1,40 +0,0 @@
-channelManager->find($request->appId, $request->channelName);
-
- if (is_null($channel)) {
- throw new HttpException(404, 'Unknown channel "'.$request->channelName.'"');
- }
-
- if (! $channel instanceof PresenceChannel) {
- throw new HttpException(400, 'Invalid presence channel "'.$request->channelName.'"');
- }
-
- return $channel
- ->getUsers($request->appId)
- ->then(function (array $users) {
- return [
- 'users' => Collection::make($users)->map(function ($user) {
- return ['id' => $user->user_id];
- })->values(),
- ];
- });
- }
-}
diff --git a/src/HttpApi/Controllers/TriggerEventController.php b/src/HttpApi/Controllers/TriggerEventController.php
deleted file mode 100644
index 9dc3b7d..0000000
--- a/src/HttpApi/Controllers/TriggerEventController.php
+++ /dev/null
@@ -1,57 +0,0 @@
-ensureValidSignature($request);
-
- $channels = $request->channels ?: [];
-
- foreach ($channels as $channelName) {
- $channel = $this->channelManager->find($request->appId, $channelName);
-
- $payload = (object) [
- 'channel' => $channelName,
- 'event' => $request->name,
- 'data' => $request->data,
- ];
-
- if ($channel) {
- $channel->broadcastToEveryoneExcept(
- $payload, $request->socket_id, $request->appId
- );
- } else {
- // If the setup is horizontally-scaled using the Redis Pub/Sub,
- // then we're going to make sure it gets streamed to the other
- // servers as well that are subscribed to the Pub/Sub topics
- // attached to the current iterated app & channel.
- // For local setups, the local driver will ignore the publishes.
-
- $this->replicator->publish($request->appId, $channelName, $payload);
- }
-
- DashboardLogger::log($request->appId, DashboardLogger::TYPE_API_MESSAGE, [
- 'channel' => $channelName,
- 'event' => $request->json()->get('name'),
- 'payload' => $request->json()->get('data'),
- ]);
-
- StatisticsLogger::apiMessage($request->appId);
- }
-
- return $request->json()->all();
- }
-}
diff --git a/src/Statistics/Models/WebSocketsStatisticsEntry.php b/src/Models/WebSocketsStatisticsEntry.php
similarity index 81%
rename from src/Statistics/Models/WebSocketsStatisticsEntry.php
rename to src/Models/WebSocketsStatisticsEntry.php
index edd0de1..e1d0d6b 100644
--- a/src/Statistics/Models/WebSocketsStatisticsEntry.php
+++ b/src/Models/WebSocketsStatisticsEntry.php
@@ -1,6 +1,6 @@
channelData["{$appId}:{$channel}"][$socketId] = $data;
- }
-
- /**
- * Remove a member from the channel. To be called when they have
- * unsubscribed from the channel.
- *
- * @param string $appId
- * @param string $channel
- * @param string $socketId
- * @return void
- */
- public function leaveChannel($appId, string $channel, string $socketId)
- {
- unset($this->channelData["{$appId}:{$channel}"][$socketId]);
-
- if (empty($this->channelData["{$appId}:{$channel}"])) {
- unset($this->channelData["{$appId}:{$channel}"]);
- }
- }
-
- /**
- * Retrieve the full information about the members in a presence channel.
- *
- * @param string $appId
- * @param string $channel
- * @return PromiseInterface
- */
- public function channelMembers($appId, string $channel): PromiseInterface
- {
- $members = $this->channelData["{$appId}:{$channel}"] ?? [];
-
- $members = array_map(function ($user) {
- return json_decode($user);
- }, $members);
-
- return new FulfilledPromise($members);
- }
-
- /**
- * Get the amount of users subscribed for each presence channel.
- *
- * @param string $appId
- * @param array $channelNames
- * @return PromiseInterface
- */
- public function channelMemberCounts($appId, array $channelNames): PromiseInterface
- {
- $results = [];
-
- // Count the number of users per channel
- foreach ($channelNames as $channel) {
- $results[$channel] = isset($this->channelData["{$appId}:{$channel}"])
- ? count($this->channelData["{$appId}:{$channel}"])
- : 0;
- }
-
- return new FulfilledPromise($results);
- }
-
- /**
- * Get the amount of unique connections.
- *
- * @param mixed $appId
- * @return null|int
- */
- public function getLocalConnectionsCount($appId)
- {
- return null;
- }
-
- /**
- * Get the amount of connections aggregated on multiple instances.
- *
- * @param mixed $appId
- * @return null|int|\React\Promise\PromiseInterface
- */
- public function getGlobalConnectionsCount($appId)
- {
- return null;
- }
-}
diff --git a/src/PubSub/Drivers/RedisClient.php b/src/PubSub/Drivers/RedisClient.php
deleted file mode 100644
index 78acef4..0000000
--- a/src/PubSub/Drivers/RedisClient.php
+++ /dev/null
@@ -1,437 +0,0 @@
-serverId = Str::uuid()->toString();
- }
-
- /**
- * Boot the RedisClient, initializing the connections.
- *
- * @param LoopInterface $loop
- * @param string|null $factoryClass
- * @return ReplicationInterface
- */
- public function boot(LoopInterface $loop, $factoryClass = null): ReplicationInterface
- {
- $factoryClass = $factoryClass ?: Factory::class;
-
- $this->loop = $loop;
-
- $connectionUri = $this->getConnectionUri();
- $factory = new $factoryClass($this->loop);
-
- $this->publishClient = $factory->createLazyClient($connectionUri);
- $this->subscribeClient = $factory->createLazyClient($connectionUri);
-
- // The subscribed client gets a message, it triggers the onMessage().
- $this->subscribeClient->on('message', function ($channel, $payload) {
- $this->onMessage($channel, $payload);
- });
-
- return $this;
- }
-
- /**
- * Publish a message to a channel on behalf of a websocket user.
- *
- * @param string $appId
- * @param string $channel
- * @param stdClass $payload
- * @return bool
- */
- public function publish($appId, string $channel, stdClass $payload): bool
- {
- $payload->appId = $appId;
- $payload->serverId = $this->getServerId();
-
- $payload = json_encode($payload);
-
- $this->publishClient->publish($this->getTopicName($appId, $channel), $payload);
-
- DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_MESSAGE_PUBLISHED, [
- 'channel' => $channel,
- 'serverId' => $this->getServerId(),
- 'payload' => $payload,
- 'pubsub' => $this->getTopicName($appId, $channel),
- ]);
-
- return true;
- }
-
- /**
- * Subscribe to a channel on behalf of websocket user.
- *
- * @param string $appId
- * @param string $channel
- * @return bool
- */
- public function subscribe($appId, string $channel): bool
- {
- if (! isset($this->subscribedChannels["{$appId}:{$channel}"])) {
- // We're not subscribed to the channel yet, subscribe and set the count to 1
- $this->subscribeClient->subscribe($this->getTopicName($appId, $channel));
- $this->subscribedChannels["{$appId}:{$channel}"] = 1;
- } else {
- // Increment the subscribe count if we've already subscribed
- $this->subscribedChannels["{$appId}:{$channel}"]++;
- }
-
- DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_SUBSCRIBED, [
- 'channel' => $channel,
- 'serverId' => $this->getServerId(),
- 'pubsub' => $this->getTopicName($appId, $channel),
- ]);
-
- return true;
- }
-
- /**
- * Unsubscribe from a channel on behalf of a websocket user.
- *
- * @param string $appId
- * @param string $channel
- * @return bool
- */
- public function unsubscribe($appId, string $channel): bool
- {
- if (! isset($this->subscribedChannels["{$appId}:{$channel}"])) {
- return false;
- }
-
- // Decrement the subscription count for this channel
- $this->subscribedChannels["{$appId}:{$channel}"]--;
-
- // If we no longer have subscriptions to that channel, unsubscribe
- if ($this->subscribedChannels["{$appId}:{$channel}"] < 1) {
- $this->subscribeClient->unsubscribe($this->getTopicName($appId, $channel));
-
- unset($this->subscribedChannels["{$appId}:{$channel}"]);
- }
-
- DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_UNSUBSCRIBED, [
- 'channel' => $channel,
- 'serverId' => $this->getServerId(),
- 'pubsub' => $this->getTopicName($appId, $channel),
- ]);
-
- return true;
- }
-
- /**
- * Subscribe to the app's pubsub keyspace.
- *
- * @param mixed $appId
- * @return bool
- */
- public function subscribeToApp($appId): bool
- {
- $this->subscribeClient->subscribe($this->getTopicName($appId));
-
- $this->publishClient->hincrby($this->getTopicName($appId), 'connections', 1);
-
- return true;
- }
-
- /**
- * Unsubscribe from the app's pubsub keyspace.
- *
- * @param mixed $appId
- * @return bool
- */
- public function unsubscribeFromApp($appId): bool
- {
- $this->subscribeClient->unsubscribe($this->getTopicName($appId));
-
- $this->publishClient->hincrby($this->getTopicName($appId), 'connections', -1);
-
- return true;
- }
-
- /**
- * Add a member to a channel. To be called when they have
- * subscribed to the channel.
- *
- * @param string $appId
- * @param string $channel
- * @param string $socketId
- * @param string $data
- * @return void
- */
- public function joinChannel($appId, string $channel, string $socketId, string $data)
- {
- $this->publishClient->hset($this->getTopicName($appId, $channel), $socketId, $data);
-
- DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_JOINED_CHANNEL, [
- 'channel' => $channel,
- 'serverId' => $this->getServerId(),
- 'socketId' => $socketId,
- 'data' => $data,
- 'pubsub' => $this->getTopicName($appId, $channel),
- ]);
- }
-
- /**
- * Remove a member from the channel. To be called when they have
- * unsubscribed from the channel.
- *
- * @param string $appId
- * @param string $channel
- * @param string $socketId
- * @return void
- */
- public function leaveChannel($appId, string $channel, string $socketId)
- {
- $this->publishClient->hdel($this->getTopicName($appId, $channel), $socketId);
-
- DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_LEFT_CHANNEL, [
- 'channel' => $channel,
- 'serverId' => $this->getServerId(),
- 'socketId' => $socketId,
- 'pubsub' => $this->getTopicName($appId, $channel),
- ]);
- }
-
- /**
- * Retrieve the full information about the members in a presence channel.
- *
- * @param string $appId
- * @param string $channel
- * @return PromiseInterface
- */
- public function channelMembers($appId, string $channel): PromiseInterface
- {
- return $this->publishClient->hgetall($this->getTopicName($appId, $channel))
- ->then(function ($members) {
- // The data is expected as objects, so we need to JSON decode
- return array_map(function ($user) {
- return json_decode($user);
- }, $members);
- });
- }
-
- /**
- * Get the amount of users subscribed for each presence channel.
- *
- * @param string $appId
- * @param array $channelNames
- * @return PromiseInterface
- */
- public function channelMemberCounts($appId, array $channelNames): PromiseInterface
- {
- $this->publishClient->multi();
-
- foreach ($channelNames as $channel) {
- $this->publishClient->hlen($this->getTopicName($appId, $channel));
- }
-
- return $this->publishClient
- ->exec()
- ->then(function ($data) use ($channelNames) {
- return array_combine($channelNames, $data);
- });
- }
-
- /**
- * Get the amount of connections aggregated on multiple instances.
- *
- * @param mixed $appId
- * @return null|int|\React\Promise\PromiseInterface
- */
- public function getGlobalConnectionsCount($appId)
- {
- return $this->publishClient->hget($this->getTopicName($appId), 'connections');
- }
-
- /**
- * Handle a message received from Redis on a specific channel.
- *
- * @param string $redisChannel
- * @param string $payload
- * @return void
- */
- public function onMessage(string $redisChannel, string $payload)
- {
- $payload = json_decode($payload);
-
- // Ignore messages sent by ourselves.
- if (isset($payload->serverId) && $this->getServerId() === $payload->serverId) {
- return;
- }
-
- // Pull out the app ID. See RedisPusherBroadcaster
- $appId = $payload->appId;
-
- // We need to put the channel name in the payload.
- // We strip the app ID from the channel name, websocket clients
- // expect the channel name to not include the app ID.
- $payload->channel = Str::after($redisChannel, "{$appId}:");
-
- $channelManager = app(ChannelManager::class);
-
- // Load the Channel instance to sync.
- $channel = $channelManager->find($appId, $payload->channel);
-
- // If no channel is found, none of our connections want to
- // receive this message, so we ignore it.
- if (! $channel) {
- return;
- }
-
- $socketId = $payload->socketId ?? null;
- $serverId = $payload->serverId ?? null;
-
- // Remove fields intended for internal use from the payload.
- unset($payload->socketId);
- unset($payload->serverId);
- unset($payload->appId);
-
- // Push the message out to connected websocket clients.
- $channel->broadcastToEveryoneExcept($payload, $socketId, $appId, false);
-
- DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_MESSAGE_RECEIVED, [
- 'channel' => $channel->getChannelName(),
- 'redisChannel' => $redisChannel,
- 'serverId' => $this->getServerId(),
- 'incomingServerId' => $serverId,
- 'incomingSocketId' => $socketId,
- 'payload' => $payload,
- ]);
- }
-
- /**
- * Build the Redis connection URL from Laravel database config.
- *
- * @return string
- */
- protected function getConnectionUri()
- {
- $name = config('websockets.replication.redis.connection', 'default');
- $config = config("database.redis.{$name}");
-
- $host = $config['host'];
- $port = $config['port'] ?: 6379;
-
- $query = [];
-
- if ($config['password']) {
- $query['password'] = $config['password'];
- }
-
- if ($config['database']) {
- $query['database'] = $config['database'];
- }
-
- $query = http_build_query($query);
-
- return "redis://{$host}:{$port}".($query ? "?{$query}" : '');
- }
-
- /**
- * Get the Subscribe client instance.
- *
- * @return Client
- */
- public function getSubscribeClient()
- {
- return $this->subscribeClient;
- }
-
- /**
- * Get the Publish client instance.
- *
- * @return Client
- */
- public function getPublishClient()
- {
- return $this->publishClient;
- }
-
- /**
- * Get the unique identifier for the server.
- *
- * @return string
- */
- public function getServerId()
- {
- return $this->serverId;
- }
-
- /**
- * Get the Pub/Sub Topic name to subscribe based on the
- * app ID and channel name.
- *
- * @param mixed $appId
- * @param string|null $channel
- * @return string
- */
- protected function getTopicName($appId, string $channel = null): string
- {
- $prefix = config('database.redis.options.prefix', null);
-
- $hash = "{$prefix}{$appId}";
-
- if ($channel) {
- $hash .= ":{$channel}";
- }
-
- return $hash;
- }
-}
diff --git a/src/PubSub/ReplicationInterface.php b/src/PubSub/ReplicationInterface.php
deleted file mode 100644
index 5ca3ee3..0000000
--- a/src/PubSub/ReplicationInterface.php
+++ /dev/null
@@ -1,120 +0,0 @@
-message = 'Over capacity';
- $this->code = 4100;
+ $this->trigger("Over capacity", 4100);
}
}
diff --git a/src/WebSockets/Exceptions/InvalidSignature.php b/src/Server/Exceptions/InvalidSignature.php
similarity index 64%
rename from src/WebSockets/Exceptions/InvalidSignature.php
rename to src/Server/Exceptions/InvalidSignature.php
index b0229b3..0cfbb22 100644
--- a/src/WebSockets/Exceptions/InvalidSignature.php
+++ b/src/Server/Exceptions/InvalidSignature.php
@@ -1,6 +1,6 @@
message = 'Invalid Signature';
- $this->code = 4009;
+ $this->trigger("Invalid Signature", 4009);
}
}
diff --git a/src/Server/Exceptions/OriginNotAllowed.php b/src/Server/Exceptions/OriginNotAllowed.php
new file mode 100644
index 0000000..cd24fff
--- /dev/null
+++ b/src/Server/Exceptions/OriginNotAllowed.php
@@ -0,0 +1,17 @@
+trigger("The origin is not allowed for `{$appKey}`.", 4009);
+ }
+}
diff --git a/src/Server/Exceptions/UnknownAppKey.php b/src/Server/Exceptions/UnknownAppKey.php
new file mode 100644
index 0000000..013d9be
--- /dev/null
+++ b/src/Server/Exceptions/UnknownAppKey.php
@@ -0,0 +1,17 @@
+trigger("Could not find app key `{$appKey}`.", 4001);
+ }
+}
diff --git a/src/WebSockets/Exceptions/WebSocketException.php b/src/Server/Exceptions/WebSocketException.php
similarity index 54%
rename from src/WebSockets/Exceptions/WebSocketException.php
rename to src/Server/Exceptions/WebSocketException.php
index d38da70..cc7cbf9 100644
--- a/src/WebSockets/Exceptions/WebSocketException.php
+++ b/src/Server/Exceptions/WebSocketException.php
@@ -1,6 +1,6 @@
message = $message;
+ $this->code = $code;
+ }
}
diff --git a/src/Server/HttpServer.php b/src/Server/HttpServer.php
index b497d34..67a8d44 100644
--- a/src/Server/HttpServer.php
+++ b/src/Server/HttpServer.php
@@ -3,8 +3,9 @@
namespace BeyondCode\LaravelWebSockets\Server;
use Ratchet\Http\HttpServerInterface;
+use Ratchet\Http\HttpServer as BaseHttpServer;
-class HttpServer extends \Ratchet\Http\HttpServer
+class HttpServer extends BaseHttpServer
{
/**
* Create a new server instance.
diff --git a/src/Server/Logger/ConnectionLogger.php b/src/Server/Loggers/ConnectionLogger.php
similarity index 95%
rename from src/Server/Logger/ConnectionLogger.php
rename to src/Server/Loggers/ConnectionLogger.php
index 4a1b02d..60e2ffb 100644
--- a/src/Server/Logger/ConnectionLogger.php
+++ b/src/Server/Loggers/ConnectionLogger.php
@@ -1,6 +1,6 @@
enabled;
+ $logger = app(WebSocketsLogger::class);
+
+ return $logger->enabled;
}
/**
diff --git a/src/Server/Logger/WebsocketsLogger.php b/src/Server/Loggers/WebSocketsLogger.php
similarity index 94%
rename from src/Server/Logger/WebsocketsLogger.php
rename to src/Server/Loggers/WebSocketsLogger.php
index cb68f20..a9555e1 100644
--- a/src/Server/Logger/WebsocketsLogger.php
+++ b/src/Server/Loggers/WebSocketsLogger.php
@@ -1,14 +1,14 @@
payload = $payload;
-
- $this->connection = $connection;
-
- $this->channelManager = $channelManager;
- }
-
/**
* Respond with the payload.
*
@@ -84,9 +48,7 @@ class PusherChannelProtocolMessage implements PusherMessage
*/
protected function subscribe(ConnectionInterface $connection, stdClass $payload)
{
- $channel = $this->channelManager->findOrCreate($connection->app->id, $payload->channel);
-
- $channel->subscribe($connection, $payload);
+ $this->channelManager->subscribeToChannel($connection, $payload->channel, $payload);
}
/**
@@ -98,8 +60,6 @@ class PusherChannelProtocolMessage implements PusherMessage
*/
public function unsubscribe(ConnectionInterface $connection, stdClass $payload)
{
- $channel = $this->channelManager->findOrCreate($connection->app->id, $payload->channel);
-
- $channel->unsubscribe($connection);
+ $this->channelManager->unsubscribeFromChannel($connection, $payload->channel, $payload);
}
}
diff --git a/src/WebSockets/Messages/PusherClientMessage.php b/src/Server/Messages/PusherClientMessage.php
similarity index 75%
rename from src/WebSockets/Messages/PusherClientMessage.php
rename to src/Server/Messages/PusherClientMessage.php
index cab08d1..2211de0 100644
--- a/src/WebSockets/Messages/PusherClientMessage.php
+++ b/src/Server/Messages/PusherClientMessage.php
@@ -1,12 +1,13 @@
channelManager->find(
+ $this->connection->app->id, $this->payload->channel
+ );
+
+ optional($channel)->broadcastToEveryoneExcept(
+ $this->payload, $this->connection->socketId, $this->connection->app->id
+ );
+
DashboardLogger::log($this->connection->app->id, DashboardLogger::TYPE_WS_MESSAGE, [
'socketId' => $this->connection->socketId,
'channel' => $this->payload->channel,
@@ -67,8 +76,5 @@ class PusherClientMessage implements PusherMessage
'data' => $this->payload,
]);
- $channel = $this->channelManager->find($this->connection->app->id, $this->payload->channel);
-
- optional($channel)->broadcastToOthers($this->connection, $this->payload);
}
}
diff --git a/src/WebSockets/Messages/PusherMessageFactory.php b/src/Server/Messages/PusherMessageFactory.php
similarity index 76%
rename from src/WebSockets/Messages/PusherMessageFactory.php
rename to src/Server/Messages/PusherMessageFactory.php
index 0136449..acfb2db 100644
--- a/src/WebSockets/Messages/PusherMessageFactory.php
+++ b/src/Server/Messages/PusherMessageFactory.php
@@ -1,11 +1,12 @@
routes = new RouteCollection;
- $this->customRoutes = new Collection();
}
/**
@@ -53,22 +39,17 @@ class Router
}
/**
- * Register the routes.
+ * Register the default routes.
*
* @return void
*/
public function routes()
{
- $this->get('/app/{appKey}', config('websockets.handlers.websocket', WebSocketHandler::class));
-
- $this->post('/apps/{appId}/events', config('websockets.handlers.trigger_event', TriggerEventController::class));
- $this->get('/apps/{appId}/channels', config('websockets.handlers.fetch_channels', FetchChannelsController::class));
- $this->get('/apps/{appId}/channels/{channelName}', config('websockets.handlers.fetch_channel', FetchChannelController::class));
- $this->get('/apps/{appId}/channels/{channelName}/users', config('websockets.handlers.fetch_users', FetchUsersController::class));
-
- $this->customRoutes->each(function ($action, $uri) {
- $this->get($uri, $action);
- });
+ $this->get('/app/{appKey}', config('websockets.handlers.websocket'));
+ $this->post('/apps/{appId}/events', config('websockets.handlers.trigger_event'));
+ $this->get('/apps/{appId}/channels', config('websockets.handlers.fetch_channels'));
+ $this->get('/apps/{appId}/channels/{channelName}', config('websockets.handlers.fetch_channel'));
+ $this->get('/apps/{appId}/channels/{channelName}/users', config('websockets.handlers.fetch_users'));
}
/**
@@ -131,23 +112,6 @@ class Router
$this->addRoute('DELETE', $uri, $action);
}
- /**
- * Add a WebSocket GET route that should
- * comply with the MessageComponentInterface interface.
- *
- * @param string $uri
- * @param string $action
- * @return void
- */
- public function webSocket(string $uri, $action)
- {
- if (! is_subclass_of($action, MessageComponentInterface::class)) {
- throw InvalidWebSocketController::withController($action);
- }
-
- $this->customRoutes->put($uri, $action);
- }
-
/**
* Add a new route to the list.
*
@@ -171,12 +135,6 @@ class Router
*/
protected function getRoute(string $method, string $uri, $action): Route
{
- /**
- * If the given action is a class that handles WebSockets, then it's not a regular
- * controller but a WebSocketHandler that needs to converted to a WsServer.
- *
- * If the given action is a regular controller we'll just instantiate it.
- */
$action = is_subclass_of($action, MessageComponentInterface::class)
? $this->createWebSocketsServer($action)
: app($action);
diff --git a/src/WebSockets/WebSocketHandler.php b/src/Server/WebSocketHandler.php
similarity index 50%
rename from src/WebSockets/WebSocketHandler.php
rename to src/Server/WebSocketHandler.php
index 8b363d2..3593611 100644
--- a/src/WebSockets/WebSocketHandler.php
+++ b/src/Server/WebSocketHandler.php
@@ -1,50 +1,34 @@
channelManager = $channelManager;
- $this->replicator = app(ReplicationInterface::class);
}
/**
@@ -60,6 +44,20 @@ class WebSocketHandler implements MessageComponentInterface
->limitConcurrentConnections($connection)
->generateSocketId($connection)
->establishConnection($connection);
+
+ if (isset($connection->app)) {
+ /** @var \GuzzleHttp\Psr7\Request $request */
+ $request = $connection->httpRequest;
+
+ StatisticsCollector::connection($connection->app->id);
+
+ $this->channelManager->subscribeToApp($connection->app->id);
+
+ DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_CONNECTED, [
+ 'origin' => "{$request->getUri()->getScheme()}://{$request->getUri()->getHost()}",
+ 'socketId' => $connection->socketId,
+ ]);
+ }
}
/**
@@ -71,11 +69,11 @@ class WebSocketHandler implements MessageComponentInterface
*/
public function onMessage(ConnectionInterface $connection, MessageInterface $message)
{
- $message = PusherMessageFactory::createForMessage($message, $connection, $this->channelManager);
+ Messages\PusherMessageFactory::createForMessage(
+ $message, $connection, $this->channelManager
+ )->respond();
- $message->respond();
-
- StatisticsLogger::webSocketMessage($connection->app->id);
+ StatisticsCollector::webSocketMessage($connection->app->id);
}
/**
@@ -86,15 +84,17 @@ class WebSocketHandler implements MessageComponentInterface
*/
public function onClose(ConnectionInterface $connection)
{
- $this->channelManager->removeFromAllChannels($connection);
+ $this->channelManager->unsubscribeFromAllChannels($connection);
- DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_DISCONNECTED, [
- 'socketId' => $connection->socketId,
- ]);
+ if (isset($connection->app)) {
+ StatisticsCollector::disconnection($connection->app->id);
- StatisticsLogger::disconnection($connection->app->id);
+ $this->channelManager->unsubscribeFromApp($connection->app->id);
- $this->replicator->unsubscribeFromApp($connection->app->id);
+ DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_DISCONNECTED, [
+ 'socketId' => $connection->socketId,
+ ]);
+ }
}
/**
@@ -106,13 +106,11 @@ class WebSocketHandler implements MessageComponentInterface
*/
public function onError(ConnectionInterface $connection, Exception $exception)
{
- if ($exception instanceof WebSocketException) {
+ if ($exception instanceof Exceptions\WebSocketException) {
$connection->send(json_encode(
$exception->getPayload()
));
}
-
- $this->replicator->unsubscribeFromApp($connection->app->id);
}
/**
@@ -123,10 +121,12 @@ class WebSocketHandler implements MessageComponentInterface
*/
protected function verifyAppKey(ConnectionInterface $connection)
{
- $appKey = QueryParameters::create($connection->httpRequest)->get('appKey');
+ $query = QueryParameters::create($connection->httpRequest);
+
+ $appKey = $query->get('appKey');
if (! $app = App::findByKey($appKey)) {
- throw new UnknownAppKey($appKey);
+ throw new Exceptions\UnknownAppKey($appKey);
}
$connection->app = $app;
@@ -151,7 +151,7 @@ class WebSocketHandler implements MessageComponentInterface
$origin = parse_url($header, PHP_URL_HOST) ?: $header;
if (! $header || ! in_array($origin, $connection->app->allowedOrigins)) {
- throw new OriginNotAllowed($connection->app->key);
+ throw new Exceptions\OriginNotAllowed($connection->app->key);
}
return $this;
@@ -166,17 +166,17 @@ class WebSocketHandler implements MessageComponentInterface
protected function limitConcurrentConnections(ConnectionInterface $connection)
{
if (! is_null($capacity = $connection->app->capacity)) {
- $connectionsCount = $this->channelManager->getGlobalConnectionsCount($connection->app->id);
+ $this->channelManager
+ ->getGlobalConnectionsCount($connection->app->id)
+ ->then(function ($connectionsCount) use ($capacity, $connection) {
+ if ($connectionsCount >= $capacity) {
+ $exception = new Exceptions\ConnectionsOverCapacity;
- if ($connectionsCount instanceof PromiseInterface) {
- $connectionsCount->then(function ($connectionsCount) use ($capacity, $connection) {
- $connectionsCount = $connectionsCount ?: 0;
+ $payload = json_encode($exception->getPayload());
- $this->sendExceptionIfOverCapacity($connectionsCount, $capacity, $connection);
+ tap($connection)->send($payload)->close();
+ }
});
- } else {
- $this->throwExceptionIfOverCapacity($connectionsCount, $capacity);
- }
}
return $this;
@@ -213,51 +213,6 @@ class WebSocketHandler implements MessageComponentInterface
]),
]));
- /** @var \GuzzleHttp\Psr7\Request $request */
- $request = $connection->httpRequest;
-
- DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_CONNECTED, [
- 'origin' => "{$request->getUri()->getScheme()}://{$request->getUri()->getHost()}",
- 'socketId' => $connection->socketId,
- ]);
-
- StatisticsLogger::connection($connection->app->id);
-
- $this->replicator->subscribeToApp($connection->app->id);
-
return $this;
}
-
- /**
- * Throw a ConnectionsOverCapacity exception.
- *
- * @param int $connectionsCount
- * @param int $capacity
- * @return void
- * @throws ConnectionsOverCapacity
- */
- protected function throwExceptionIfOverCapacity(int $connectionsCount, int $capacity)
- {
- if ($connectionsCount >= $capacity) {
- throw new ConnectionsOverCapacity;
- }
- }
-
- /**
- * Send the ConnectionsOverCapacity exception through
- * the connection and close the channel.
- *
- * @param int $connectionsCount
- * @param int $capacity
- * @param ConnectionInterface $connection
- * @return void
- */
- protected function sendExceptionIfOverCapacity(int $connectionsCount, int $capacity, ConnectionInterface $connection)
- {
- if ($connectionsCount >= $capacity) {
- $payload = json_encode((new ConnectionsOverCapacity)->getPayload());
-
- tap($connection)->send($payload)->close();
- }
- }
}
diff --git a/src/Server/WebSocketServerFactory.php b/src/ServerFactory.php
similarity index 90%
rename from src/Server/WebSocketServerFactory.php
rename to src/ServerFactory.php
index 163495a..ac79ca6 100644
--- a/src/Server/WebSocketServerFactory.php
+++ b/src/ServerFactory.php
@@ -1,8 +1,7 @@
routes = $routes;
diff --git a/src/Statistics/Collectors/MemoryCollector.php b/src/Statistics/Collectors/MemoryCollector.php
new file mode 100644
index 0000000..bf5fc80
--- /dev/null
+++ b/src/Statistics/Collectors/MemoryCollector.php
@@ -0,0 +1,171 @@
+channelManager = app(ChannelManager::class);
+ }
+
+ /**
+ * Handle the incoming websocket message.
+ *
+ * @param string|int $appId
+ * @return void
+ */
+ public function webSocketMessage($appId)
+ {
+ $this->findOrMake($appId)
+ ->webSocketMessage();
+ }
+
+ /**
+ * Handle the incoming API message.
+ *
+ * @param string|int $appId
+ * @return void
+ */
+ public function apiMessage($appId)
+ {
+ $this->findOrMake($appId)
+ ->apiMessage();
+ }
+
+ /**
+ * Handle the new conection.
+ *
+ * @param string|int $appId
+ * @return void
+ */
+ public function connection($appId)
+ {
+ $this->findOrMake($appId)
+ ->connection();
+ }
+
+ /**
+ * Handle disconnections.
+ *
+ * @param string|int $appId
+ * @return void
+ */
+ public function disconnection($appId)
+ {
+ $this->findOrMake($appId)
+ ->disconnection();
+ }
+
+ /**
+ * Save all the stored statistics.
+ *
+ * @return void
+ */
+ public function save()
+ {
+ $this->getStatistics()->then(function ($statistics) {
+ foreach ($statistics as $appId => $statistic) {
+ if (! $statistic->isEnabled()) {
+ continue;
+ }
+
+ $this->createRecord($statistic, $appId);
+
+ $this->channelManager
+ ->getGlobalConnectionsCount($appId)
+ ->then(function ($connections) use ($statistic) {
+ $statistic->reset(
+ is_null($connections) ? 0 : $connections
+ );
+ });
+ }
+ });
+ }
+
+ /**
+ * Flush the stored statistics.
+ *
+ * @return void
+ */
+ public function flush()
+ {
+ $this->statistics = [];
+ }
+
+ /**
+ * Get the saved statistics.
+ *
+ * @return PromiseInterface[array]
+ */
+ public function getStatistics(): PromiseInterface
+ {
+ return new FulfilledPromise($this->statistics);
+ }
+
+ /**
+ * Get the saved statistics for an app.
+ *
+ * @param string|int $appId
+ * @return PromiseInterface[\BeyondCode\LaravelWebSockets\Statistics\Statistic|null]
+ */
+ public function getAppStatistics($appId): PromiseInterface
+ {
+ return new FulfilledPromise(
+ $this->statistics[$appId] ?? null
+ );
+ }
+
+ /**
+ * Find or create a defined statistic for an app.
+ *
+ * @param string|int $appId
+ * @return \BeyondCode\LaravelWebSockets\Statistics\Statistic
+ */
+ protected function findOrMake($appId): Statistic
+ {
+ if (! isset($this->statistics[$appId])) {
+ $this->statistics[$appId] = new Statistic($appId);
+ }
+
+ return $this->statistics[$appId];
+ }
+
+ /**
+ * Create a new record using the Statistic Store.
+ *
+ * @param \BeyondCode\LaravelWebSockets\Statistics\Statistic $statistic
+ * @param mixed $appId
+ * @return void
+ */
+ public function createRecord(Statistic $statistic, $appId)
+ {
+ StatisticsStore::store($statistic->toArray());
+ }
+}
diff --git a/src/Statistics/Collectors/RedisCollector.php b/src/Statistics/Collectors/RedisCollector.php
new file mode 100644
index 0000000..43776d5
--- /dev/null
+++ b/src/Statistics/Collectors/RedisCollector.php
@@ -0,0 +1,407 @@
+redis = Redis::connection(
+ config('websockets.replication.modes.redis.connection', 'default')
+ );
+ }
+
+ /**
+ * Handle the incoming websocket message.
+ *
+ * @param string|int $appId
+ * @return void
+ */
+ public function webSocketMessage($appId)
+ {
+ $this->ensureAppIsInSet($appId)
+ ->hincrby(
+ $this->channelManager->getRedisKey($appId, null, ['stats']),
+ 'websocket_messages_count', 1
+ );
+ }
+
+ /**
+ * Handle the incoming API message.
+ *
+ * @param string|int $appId
+ * @return void
+ */
+ public function apiMessage($appId)
+ {
+ $this->ensureAppIsInSet($appId)
+ ->hincrby(
+ $this->channelManager->getRedisKey($appId, null, ['stats']),
+ 'api_messages_count', 1
+ );
+ }
+
+ /**
+ * Handle the new conection.
+ *
+ * @param string|int $appId
+ * @return void
+ */
+ public function connection($appId)
+ {
+ // Increment the current connections count by 1.
+ $this->ensureAppIsInSet($appId)
+ ->hincrby(
+ $this->channelManager->getRedisKey($appId, null, ['stats']),
+ 'current_connections_count', 1
+ )
+ ->then(function ($currentConnectionsCount) use ($appId) {
+ // Get the peak connections count from Redis.
+ $this->channelManager
+ ->getPublishClient()
+ ->hget(
+ $this->channelManager->getRedisKey($appId, null, ['stats']),
+ 'peak_connections_count'
+ )
+ ->then(function ($currentPeakConnectionCount) use ($currentConnectionsCount, $appId) {
+ // Extract the greatest number between the current peak connection count
+ // and the current connection number.
+ $peakConnectionsCount = is_null($currentPeakConnectionCount)
+ ? $currentConnectionsCount
+ : max($currentPeakConnectionCount, $currentConnectionsCount);
+
+ // Then set it to the database.
+ $this->channelManager
+ ->getPublishClient()
+ ->hset(
+ $this->channelManager->getRedisKey($appId, null, ['stats']),
+ 'peak_connections_count', $peakConnectionsCount
+ );
+ });
+ });
+ }
+
+ /**
+ * Handle disconnections.
+ *
+ * @param string|int $appId
+ * @return void
+ */
+ public function disconnection($appId)
+ {
+ // Decrement the current connections count by 1.
+ $this->ensureAppIsInSet($appId)
+ ->hincrby(
+ $this->channelManager->getRedisKey($appId, null, ['stats']),
+ 'current_connections_count', -1
+ )
+ ->then(function ($currentConnectionsCount) use ($appId) {
+ // Get the peak connections count from Redis.
+ $this->channelManager
+ ->getPublishClient()
+ ->hget(
+ $this->channelManager->getRedisKey($appId, null, ['stats']),
+ 'peak_connections_count'
+ )
+ ->then(function ($currentPeakConnectionCount) use ($currentConnectionsCount, $appId) {
+ // Extract the greatest number between the current peak connection count
+ // and the current connection number.
+ $peakConnectionsCount = is_null($currentPeakConnectionCount)
+ ? $currentConnectionsCount
+ : max($currentPeakConnectionCount, $currentConnectionsCount);
+
+ // Then set it to the database.
+ $this->channelManager
+ ->getPublishClient()
+ ->hset(
+ $this->channelManager->getRedisKey($appId, null, ['stats']),
+ 'peak_connections_count', $peakConnectionsCount
+ );
+ });
+ });
+ }
+
+ /**
+ * Save all the stored statistics.
+ *
+ * @return void
+ */
+ public function save()
+ {
+ $this->lock()->get(function () {
+ $this->channelManager
+ ->getPublishClient()
+ ->smembers(static::$redisSetName)
+ ->then(function ($members) {
+ foreach ($members as $appId) {
+ $this->channelManager
+ ->getPublishClient()
+ ->hgetall($this->channelManager->getRedisKey($appId, null, ['stats']))
+ ->then(function ($list) use ($appId) {
+ if (! $list) {
+ return;
+ }
+
+ $statistic = $this->listToStatisticInstance(
+ $appId, $list
+ );
+
+ $this->createRecord($statistic, $appId);
+
+ $this->channelManager
+ ->getGlobalConnectionsCount($appId)
+ ->then(function ($currentConnectionsCount) use ($appId) {
+ $currentConnectionsCount === 0 || is_null($currentConnectionsCount)
+ ? $this->resetAppTraces($appId)
+ : $this->resetStatistics($appId, $currentConnectionsCount);
+ });
+ });
+ }
+ });
+ });
+ }
+
+ /**
+ * Flush the stored statistics.
+ *
+ * @return void
+ */
+ public function flush()
+ {
+ $this->getStatistics()->then(function ($statistics) {
+ foreach ($statistics as $appId => $statistic) {
+ $this->resetAppTraces($appId);
+ }
+ });
+ }
+
+ /**
+ * Get the saved statistics.
+ *
+ * @return PromiseInterface[array]
+ */
+ public function getStatistics(): PromiseInterface
+ {
+ return $this->channelManager
+ ->getPublishClient()
+ ->smembers(static::$redisSetName)
+ ->then(function ($members) use (&$statistics) {
+ $appsWithStatistics = [];
+
+ foreach ($members as $appId) {
+ $this->channelManager
+ ->getPublishClient()
+ ->hgetall($this->channelManager->getRedisKey($appId, null, ['stats']))
+ ->then(function ($list) use ($appId, &$appsWithStatistics) {
+ $appsWithStatistics[$appId] = $this->listToStatisticInstance(
+ $appId, $list
+ );
+ });
+ }
+
+ return $appsWithStatistics;
+ });
+ }
+
+ /**
+ * Get the saved statistics for an app.
+ *
+ * @param string|int $appId
+ * @return PromiseInterface[\BeyondCode\LaravelWebSockets\Statistics\Statistic|null]
+ */
+ public function getAppStatistics($appId): PromiseInterface
+ {
+ return $this->channelManager
+ ->getPublishClient()
+ ->hgetall($this->channelManager->getRedisKey($appId, null, ['stats']))
+ ->then(function ($list) use ($appId, &$appStatistics) {
+ return $this->listToStatisticInstance(
+ $appId, $list
+ );
+ });
+ }
+
+ /**
+ * Reset the statistics to a specific connection count.
+ *
+ * @param string|int $appId
+ * @param int $currentConnectionCount
+ * @return void
+ */
+ public function resetStatistics($appId, int $currentConnectionCount)
+ {
+ $this->channelManager
+ ->getPublishClient()
+ ->hset(
+ $this->channelManager->getRedisKey($appId, null, ['stats']),
+ 'current_connections_count', $currentConnectionCount
+ );
+
+ $this->channelManager
+ ->getPublishClient()
+ ->hset(
+ $this->channelManager->getRedisKey($appId, null, ['stats']),
+ 'peak_connections_count', $currentConnectionCount
+ );
+
+ $this->channelManager
+ ->getPublishClient()
+ ->hset(
+ $this->channelManager->getRedisKey($appId, null, ['stats']),
+ 'websocket_messages_count', 0
+ );
+
+ $this->channelManager
+ ->getPublishClient()
+ ->hset(
+ $this->channelManager->getRedisKey($appId, null, ['stats']),
+ 'api_messages_count', 0
+ );
+ }
+
+ /**
+ * Remove all app traces from the database if no connections have been set
+ * in the meanwhile since last save.
+ *
+ * @param string|int $appId
+ * @return void
+ */
+ public function resetAppTraces($appId)
+ {
+ $this->channelManager
+ ->getPublishClient()
+ ->hdel(
+ $this->channelManager->getRedisKey($appId, null, ['stats']),
+ 'current_connections_count'
+ );
+
+ $this->channelManager
+ ->getPublishClient()
+ ->hdel(
+ $this->channelManager->getRedisKey($appId, null, ['stats']),
+ 'peak_connections_count'
+ );
+
+ $this->channelManager
+ ->getPublishClient()
+ ->hdel(
+ $this->channelManager->getRedisKey($appId, null, ['stats']),
+ 'websocket_messages_count'
+ );
+
+ $this->channelManager
+ ->getPublishClient()
+ ->hdel(
+ $this->channelManager->getRedisKey($appId, null, ['stats']),
+ 'api_messages_count'
+ );
+
+ $this->channelManager
+ ->getPublishClient()
+ ->srem(static::$redisSetName, $appId);
+ }
+
+ /**
+ * Ensure the app id is stored in the Redis database.
+ *
+ * @param string|int $appId
+ * @return \Clue\React\Redis\Client
+ */
+ protected function ensureAppIsInSet($appId)
+ {
+ $this->channelManager
+ ->getPublishClient()
+ ->sadd(static::$redisSetName, $appId);
+
+ return $this->channelManager->getPublishClient();
+ }
+
+ /**
+ * Get a new RedisLock instance to avoid race conditions.
+ *
+ * @return \Illuminate\Cache\CacheLock
+ */
+ protected function lock()
+ {
+ return new RedisLock($this->redis, static::$redisLockName, 0);
+ }
+
+ /**
+ * Transform the Redis' list of key after value
+ * to key-value pairs.
+ *
+ * @param array $list
+ * @return array
+ */
+ protected function listToKeyValue(array $list)
+ {
+ // Redis lists come into a format where the keys are on even indexes
+ // and the values are on odd indexes. This way, we know which
+ // ones are keys and which ones are values and their get combined
+ // later to form the key => value array.
+
+ [$keys, $values] = collect($list)->partition(function ($value, $key) {
+ return $key % 2 === 0;
+ });
+
+ return array_combine($keys->all(), $values->all());
+ }
+
+ /**
+ * Transform a list coming from a Redis list
+ * to a Statistic instance.
+ *
+ * @param string|int $appId
+ * @param array $list
+ * @return \BeyondCode\LaravelWebSockets\Statistics\Statistic
+ */
+ protected function listToStatisticInstance($appId, array $list)
+ {
+ $list = $this->listToKeyValue($list);
+
+ return (new Statistic($appId))
+ ->setCurrentConnectionsCount($list['current_connections_count'] ?? 0)
+ ->setPeakConnectionsCount($list['peak_connections_count'] ?? 0)
+ ->setWebSocketMessagesCount($list['websocket_messages_count'] ?? 0)
+ ->setApiMessagesCount($list['api_messages_count'] ?? 0);
+ }
+}
diff --git a/src/Statistics/Drivers/DatabaseDriver.php b/src/Statistics/Drivers/DatabaseDriver.php
deleted file mode 100644
index 034e4d4..0000000
--- a/src/Statistics/Drivers/DatabaseDriver.php
+++ /dev/null
@@ -1,153 +0,0 @@
-record = $record;
- }
-
- /**
- * Get the app ID for the stats.
- *
- * @return mixed
- */
- public function getAppId()
- {
- return $this->record->app_id;
- }
-
- /**
- * Get the time value. Should be Y-m-d H:i:s.
- *
- * @return string
- */
- public function getTime(): string
- {
- return Carbon::parse($this->record->created_at)->toDateTimeString();
- }
-
- /**
- * Get the peak connection count for the time.
- *
- * @return int
- */
- public function getPeakConnectionCount(): int
- {
- return $this->record->peak_connection_count ?? 0;
- }
-
- /**
- * Get the websocket messages count for the time.
- *
- * @return int
- */
- public function getWebsocketMessageCount(): int
- {
- return $this->record->websocket_message_count ?? 0;
- }
-
- /**
- * Get the API message count for the time.
- *
- * @return int
- */
- public function getApiMessageCount(): int
- {
- return $this->record->api_message_count ?? 0;
- }
-
- /**
- * Create a new statistic in the store.
- *
- * @param array $data
- * @return \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver
- */
- public static function create(array $data): StatisticsDriver
- {
- $class = config('websockets.statistics.database.model');
-
- return new static($class::create($data));
- }
-
- /**
- * Get the records to show to the dashboard.
- *
- * @param mixed $appId
- * @param \Illuminate\Http\Request|null $request
- * @return array
- */
- public static function get($appId, ?Request $request): array
- {
- $class = config('websockets.statistics.database.model');
-
- $statistics = $class::whereAppId($appId)
- ->latest()
- ->limit(120)
- ->get()
- ->map(function ($statistic) {
- return [
- 'timestamp' => (string) $statistic->created_at,
- 'peak_connection_count' => $statistic->peak_connection_count,
- 'websocket_message_count' => $statistic->websocket_message_count,
- 'api_message_count' => $statistic->api_message_count,
- ];
- })->reverse();
-
- return [
- 'peak_connections' => [
- 'x' => $statistics->pluck('timestamp'),
- 'y' => $statistics->pluck('peak_connection_count'),
- ],
- 'websocket_message_count' => [
- 'x' => $statistics->pluck('timestamp'),
- 'y' => $statistics->pluck('websocket_message_count'),
- ],
- 'api_message_count' => [
- 'x' => $statistics->pluck('timestamp'),
- 'y' => $statistics->pluck('api_message_count'),
- ],
- ];
- }
-
- /**
- * Delete statistics from the store,
- * optionally by app id, returning
- * the number of deleted records.
- *
- * @param mixed $appId
- * @return int
- */
- public static function delete($appId = null): int
- {
- $cutOffDate = Carbon::now()->subDay(
- config('websockets.statistics.delete_statistics_older_than_days')
- )->format('Y-m-d H:i:s');
-
- $class = config('websockets.statistics.database.model');
-
- return $class::where('created_at', '<', $cutOffDate)
- ->when($appId, function ($query) use ($appId) {
- return $query->whereAppId($appId);
- })
- ->delete();
- }
-}
diff --git a/src/Statistics/Drivers/StatisticsDriver.php b/src/Statistics/Drivers/StatisticsDriver.php
deleted file mode 100644
index fd77b2c..0000000
--- a/src/Statistics/Drivers/StatisticsDriver.php
+++ /dev/null
@@ -1,78 +0,0 @@
-channelManager = $channelManager;
- $this->driver = $driver;
- }
-
- /**
- * Handle the incoming websocket message.
- *
- * @param mixed $appId
- * @return void
- */
- public function webSocketMessage($appId)
- {
- $this->findOrMakeStatisticForAppId($appId)
- ->webSocketMessage();
- }
-
- /**
- * Handle the incoming API message.
- *
- * @param mixed $appId
- * @return void
- */
- public function apiMessage($appId)
- {
- $this->findOrMakeStatisticForAppId($appId)
- ->apiMessage();
- }
-
- /**
- * Handle the new conection.
- *
- * @param mixed $appId
- * @return void
- */
- public function connection($appId)
- {
- $this->findOrMakeStatisticForAppId($appId)
- ->connection();
- }
-
- /**
- * Handle disconnections.
- *
- * @param mixed $appId
- * @return void
- */
- public function disconnection($appId)
- {
- $this->findOrMakeStatisticForAppId($appId)
- ->disconnection();
- }
-
- /**
- * Save all the stored statistics.
- *
- * @return void
- */
- public function save()
- {
- foreach ($this->statistics as $appId => $statistic) {
- if (! $statistic->isEnabled()) {
- continue;
- }
-
- $this->createRecord($statistic, $appId);
-
- $currentConnectionCount = $this->channelManager->getGlobalConnectionsCount($appId);
-
- $statistic->reset($currentConnectionCount);
- }
- }
-
- /**
- * Find or create a defined statistic for an app.
- *
- * @param mixed $appId
- * @return Statistic
- */
- protected function findOrMakeStatisticForAppId($appId): Statistic
- {
- if (! isset($this->statistics[$appId])) {
- $this->statistics[$appId] = new Statistic($appId);
- }
-
- return $this->statistics[$appId];
- }
-
- /**
- * Get the saved statistics.
- *
- * @return array
- */
- public function getStatistics(): array
- {
- return $this->statistics;
- }
-
- /**
- * Create a new record using the Statistic Driver.
- *
- * @param Statistic $statistic
- * @param mixed $appId
- * @return void
- */
- public function createRecord(Statistic $statistic, $appId)
- {
- $this->driver::create($statistic->toArray());
- }
-}
diff --git a/src/Statistics/Logger/NullStatisticsLogger.php b/src/Statistics/Logger/NullStatisticsLogger.php
deleted file mode 100644
index 1120c2e..0000000
--- a/src/Statistics/Logger/NullStatisticsLogger.php
+++ /dev/null
@@ -1,90 +0,0 @@
-channelManager = $channelManager;
- $this->driver = $driver;
- }
-
- /**
- * Handle the incoming websocket message.
- *
- * @param mixed $appId
- * @return void
- */
- public function webSocketMessage($appId)
- {
- //
- }
-
- /**
- * Handle the incoming API message.
- *
- * @param mixed $appId
- * @return void
- */
- public function apiMessage($appId)
- {
- //
- }
-
- /**
- * Handle the new conection.
- *
- * @param mixed $appId
- * @return void
- */
- public function connection($appId)
- {
- //
- }
-
- /**
- * Handle disconnections.
- *
- * @param mixed $appId
- * @return void
- */
- public function disconnection($appId)
- {
- //
- }
-
- /**
- * Save all the stored statistics.
- *
- * @return void
- */
- public function save()
- {
- //
- }
-}
diff --git a/src/Statistics/Logger/RedisStatisticsLogger.php b/src/Statistics/Logger/RedisStatisticsLogger.php
deleted file mode 100644
index 696188d..0000000
--- a/src/Statistics/Logger/RedisStatisticsLogger.php
+++ /dev/null
@@ -1,309 +0,0 @@
-channelManager = $channelManager;
- $this->driver = $driver;
- $this->replicator = app(ReplicationInterface::class);
-
- $this->redis = Redis::connection(
- config('websockets.replication.redis.connection', 'default')
- );
- }
-
- /**
- * Handle the incoming websocket message.
- *
- * @param mixed $appId
- * @return void
- */
- public function webSocketMessage($appId)
- {
- $this->ensureAppIsSet($appId)
- ->hincrby($this->getHash($appId), 'websocket_message_count', 1);
- }
-
- /**
- * Handle the incoming API message.
- *
- * @param mixed $appId
- * @return void
- */
- public function apiMessage($appId)
- {
- $this->ensureAppIsSet($appId)
- ->hincrby($this->getHash($appId), 'api_message_count', 1);
- }
-
- /**
- * Handle the new conection.
- *
- * @param mixed $appId
- * @return void
- */
- public function connection($appId)
- {
- // Increment the current connections count by 1.
- $incremented = $this->ensureAppIsSet($appId)
- ->hincrby($this->getHash($appId), 'current_connection_count', 1);
-
- $incremented->then(function ($currentConnectionCount) use ($appId) {
- // Get the peak connections count from Redis.
- $peakConnectionCount = $this->replicator
- ->getPublishClient()
- ->hget($this->getHash($appId), 'peak_connection_count');
-
- $peakConnectionCount->then(function ($currentPeakConnectionCount) use ($currentConnectionCount, $appId) {
- // Extract the greatest number between the current peak connection count
- // and the current connection number.
-
- $peakConnectionCount = is_null($currentPeakConnectionCount)
- ? $currentConnectionCount
- : max($currentPeakConnectionCount, $currentConnectionCount);
-
- // Then set it to the database.
- $this->replicator
- ->getPublishClient()
- ->hset($this->getHash($appId), 'peak_connection_count', $peakConnectionCount);
- });
- });
- }
-
- /**
- * Handle disconnections.
- *
- * @param mixed $appId
- * @return void
- */
- public function disconnection($appId)
- {
- // Decrement the current connections count by 1.
- $decremented = $this->ensureAppIsSet($appId)
- ->hincrby($this->getHash($appId), 'current_connection_count', -1);
-
- $decremented->then(function ($currentConnectionCount) use ($appId) {
- // Get the peak connections count from Redis.
- $peakConnectionCount = $this->replicator
- ->getPublishClient()
- ->hget($this->getHash($appId), 'peak_connection_count');
-
- $peakConnectionCount->then(function ($currentPeakConnectionCount) use ($currentConnectionCount, $appId) {
- // Extract the greatest number between the current peak connection count
- // and the current connection number.
-
- $peakConnectionCount = is_null($currentPeakConnectionCount)
- ? $currentConnectionCount
- : max($currentPeakConnectionCount, $currentConnectionCount);
-
- // Then set it to the database.
- $this->replicator
- ->getPublishClient()
- ->hset($this->getHash($appId), 'peak_connection_count', $peakConnectionCount);
- });
- });
- }
-
- /**
- * Save all the stored statistics.
- *
- * @return void
- */
- public function save()
- {
- $this->lock()->get(function () {
- $setMembers = $this->replicator
- ->getPublishClient()
- ->smembers('laravel-websockets:apps');
-
- $setMembers->then(function ($members) {
- foreach ($members as $appId) {
- $member = $this->replicator
- ->getPublishClient()
- ->hgetall($this->getHash($appId));
-
- $member->then(function ($statistic) use ($appId) {
- if (! $statistic) {
- return;
- }
-
- // Statistics come into a list where the keys are on even indexes
- // and the values are on odd indexes. This way, we know which
- // ones are keys and which ones are values and their get combined
- // later to form the key => value array
-
- [$keys, $values] = collect($statistic)->partition(function ($value, $key) {
- return $key % 2 === 0;
- });
-
- $statistic = array_combine($keys->all(), $values->all());
-
- $this->createRecord($statistic, $appId);
-
- $this->channelManager
- ->getGlobalConnectionsCount($appId)
- ->then(function ($currentConnectionCount) use ($appId) {
- $currentConnectionCount === 0 || is_null($currentConnectionCount)
- ? $this->resetAppTraces($appId)
- : $this->resetStatistics($appId, $currentConnectionCount);
- });
- });
- }
- });
- });
- }
-
- /**
- * Ensure the app id is stored in the Redis database.
- *
- * @param mixed $appId
- * @return \Illuminate\Redis\RedisManager
- */
- protected function ensureAppIsSet($appId)
- {
- $this->replicator
- ->getPublishClient()
- ->sadd('laravel-websockets:apps', $appId);
-
- return $this->replicator->getPublishClient();
- }
-
- /**
- * Reset the statistics to a specific connection count.
- *
- * @param mixed $appId
- * @param int $currentConnectionCount
- * @return void
- */
- public function resetStatistics($appId, int $currentConnectionCount)
- {
- $this->replicator
- ->getPublishClient()
- ->hset($this->getHash($appId), 'current_connection_count', $currentConnectionCount);
-
- $this->replicator
- ->getPublishClient()
- ->hset($this->getHash($appId), 'peak_connection_count', $currentConnectionCount);
-
- $this->replicator
- ->getPublishClient()
- ->hset($this->getHash($appId), 'websocket_message_count', 0);
-
- $this->replicator
- ->getPublishClient()
- ->hset($this->getHash($appId), 'api_message_count', 0);
- }
-
- /**
- * Remove all app traces from the database if no connections have been set
- * in the meanwhile since last save.
- *
- * @param mixed $appId
- * @return void
- */
- public function resetAppTraces($appId)
- {
- $this->replicator
- ->getPublishClient()
- ->hdel($this->getHash($appId), 'current_connection_count');
-
- $this->replicator
- ->getPublishClient()
- ->hdel($this->getHash($appId), 'peak_connection_count');
-
- $this->replicator
- ->getPublishClient()
- ->hdel($this->getHash($appId), 'websocket_message_count');
-
- $this->replicator
- ->getPublishClient()
- ->hdel($this->getHash($appId), 'api_message_count');
-
- $this->replicator
- ->getPublishClient()
- ->srem('laravel-websockets:apps', $appId);
- }
-
- /**
- * Get the Redis hash name for the app.
- *
- * @param mixed $appId
- * @return string
- */
- protected function getHash($appId): string
- {
- return "laravel-websockets:app:{$appId}";
- }
-
- /**
- * Get a new RedisLock instance to avoid race conditions.
- *
- * @return \Illuminate\Cache\CacheLock
- */
- protected function lock()
- {
- return new RedisLock($this->redis, 'laravel-websockets:lock', 0);
- }
-
- /**
- * Create a new record using the Statistic Driver.
- *
- * @param array $statistic
- * @param mixed $appId
- * @return void
- */
- protected function createRecord(array $statistic, $appId): void
- {
- $this->driver::create([
- 'app_id' => $appId,
- 'peak_connection_count' => $statistic['peak_connection_count'] ?? 0,
- 'websocket_message_count' => $statistic['websocket_message_count'] ?? 0,
- 'api_message_count' => $statistic['api_message_count'] ?? 0,
- ]);
- }
-}
diff --git a/src/Statistics/Logger/StatisticsLogger.php b/src/Statistics/Logger/StatisticsLogger.php
deleted file mode 100644
index 6f6fe0c..0000000
--- a/src/Statistics/Logger/StatisticsLogger.php
+++ /dev/null
@@ -1,45 +0,0 @@
-appId = $appId;
}
+ /**
+ * Set the current connections count.
+ *
+ * @param int $currentConnectionsCount
+ * @return $this
+ */
+ public function setCurrentConnectionsCount(int $currentConnectionsCount)
+ {
+ $this->currentConnectionsCount = $currentConnectionsCount;
+
+ return $this;
+ }
+
+ /**
+ * Set the peak connections count.
+ *
+ * @param int $peakConnectionsCount
+ * @return $this
+ */
+ public function setPeakConnectionsCount(int $peakConnectionsCount)
+ {
+ $this->peakConnectionsCount = $peakConnectionsCount;
+
+ return $this;
+ }
+
+ /**
+ * Set the peak connections count.
+ *
+ * @param int $webSocketMessagesCount
+ * @return $this
+ */
+ public function setWebSocketMessagesCount(int $webSocketMessagesCount)
+ {
+ $this->webSocketMessagesCount = $webSocketMessagesCount;
+
+ return $this;
+ }
+
+ /**
+ * Set the peak connections count.
+ *
+ * @param int $apiMessagesCount
+ * @return $this
+ */
+ public function setApiMessagesCount(int $apiMessagesCount)
+ {
+ $this->apiMessagesCount = $apiMessagesCount;
+
+ return $this;
+ }
+
/**
* Check if the app has statistics enabled.
*
@@ -69,9 +121,9 @@ class Statistic
*/
public function connection()
{
- $this->currentConnectionCount++;
+ $this->currentConnectionsCount++;
- $this->peakConnectionCount = max($this->currentConnectionCount, $this->peakConnectionCount);
+ $this->peakConnectionsCount = max($this->currentConnectionsCount, $this->peakConnectionsCount);
}
/**
@@ -81,9 +133,9 @@ class Statistic
*/
public function disconnection()
{
- $this->currentConnectionCount--;
+ $this->currentConnectionsCount--;
- $this->peakConnectionCount = max($this->currentConnectionCount, $this->peakConnectionCount);
+ $this->peakConnectionsCount = max($this->currentConnectionsCount, $this->peakConnectionsCount);
}
/**
@@ -93,7 +145,7 @@ class Statistic
*/
public function webSocketMessage()
{
- $this->webSocketMessageCount++;
+ $this->webSocketMessagesCount++;
}
/**
@@ -103,21 +155,21 @@ class Statistic
*/
public function apiMessage()
{
- $this->apiMessageCount++;
+ $this->apiMessagesCount++;
}
/**
* Reset all the connections to a specific count.
*
- * @param int $currentConnectionCount
+ * @param int $currentConnectionsCount
* @return void
*/
- public function reset(int $currentConnectionCount)
+ public function reset(int $currentConnectionsCount)
{
- $this->currentConnectionCount = $currentConnectionCount;
- $this->peakConnectionCount = $currentConnectionCount;
- $this->webSocketMessageCount = 0;
- $this->apiMessageCount = 0;
+ $this->currentConnectionsCount = $currentConnectionsCount;
+ $this->peakConnectionsCount = $currentConnectionsCount;
+ $this->webSocketMessagesCount = 0;
+ $this->apiMessagesCount = 0;
}
/**
@@ -129,9 +181,9 @@ class Statistic
{
return [
'app_id' => $this->appId,
- 'peak_connection_count' => $this->peakConnectionCount,
- 'websocket_message_count' => $this->webSocketMessageCount,
- 'api_message_count' => $this->apiMessageCount,
+ 'peak_connections_count' => $this->peakConnectionsCount,
+ 'websocket_messages_count' => $this->webSocketMessagesCount,
+ 'api_messages_count' => $this->apiMessagesCount,
];
}
}
diff --git a/src/Statistics/Stores/DatabaseStore.php b/src/Statistics/Stores/DatabaseStore.php
new file mode 100644
index 0000000..d9a6ad4
--- /dev/null
+++ b/src/Statistics/Stores/DatabaseStore.php
@@ -0,0 +1,116 @@
+toDateTimeString())
+ ->when(! is_null($appId), function ($query) use ($appId) {
+ return $query->whereAppId($appId);
+ })
+ ->delete();
+ }
+
+ /**
+ * Get the query result as eloquent collection.
+ *
+ * @param callable $processQuery
+ * @return \Illuminate\Support\Collection
+ */
+ public function getRawRecords(callable $processQuery = null)
+ {
+ return static::$model::query()
+ ->when(! is_null($processQuery), function ($query) use ($processQuery) {
+ return call_user_func($processQuery, $query);
+ }, function ($query) {
+ return $query->latest()->limit(120);
+ })->get();
+ }
+
+ /**
+ * Get the results for a specific query.
+ *
+ * @param callable $processQuery
+ * @param callable $processCollection
+ * @return array
+ */
+ public function getRecords(callable $processQuery = null, callable $processCollection = null): array
+ {
+ return $this->getRawRecords($processQuery)
+ ->when(! is_null($processCollection), function ($collection) use ($processCollection) {
+ return call_user_func($processCollection, $collection);
+ })
+ ->map(function (Model $statistic) {
+ return [
+ 'timestamp' => (string) $statistic->created_at,
+ 'peak_connections_count' => $statistic->peak_connections_count,
+ 'websocket_messages_count' => $statistic->websocket_messages_count,
+ 'api_messages_count' => $statistic->api_messages_count,
+ ];
+ })
+ ->toArray();
+ }
+
+ /**
+ * Get the results for a specific query into a
+ * format that is easily to read for graphs.
+ *
+ * @param callable $processQuery
+ * @return array
+ */
+ public function getForGraph(callable $processQuery = null): array
+ {
+ $statistics = collect(
+ $this->getRecords($processQuery)
+ );
+
+ return [
+ 'peak_connections' => [
+ 'x' => $statistics->pluck('timestamp')->toArray(),
+ 'y' => $statistics->pluck('peak_connections_count')->toArray(),
+ ],
+ 'websocket_messages_count' => [
+ 'x' => $statistics->pluck('timestamp')->toArray(),
+ 'y' => $statistics->pluck('websocket_messages_count')->toArray(),
+ ],
+ 'api_messages_count' => [
+ 'x' => $statistics->pluck('timestamp')->toArray(),
+ 'y' => $statistics->pluck('api_messages_count')->toArray(),
+ ],
+ ];
+ }
+}
diff --git a/src/WebSockets/Channels/Channel.php b/src/WebSockets/Channels/Channel.php
deleted file mode 100644
index c282563..0000000
--- a/src/WebSockets/Channels/Channel.php
+++ /dev/null
@@ -1,254 +0,0 @@
-channelName = $channelName;
- $this->replicator = app(ReplicationInterface::class);
- }
-
- /**
- * Get the channel name.
- *
- * @return string
- */
- public function getChannelName(): string
- {
- return $this->channelName;
- }
-
- /**
- * Check if the channel has connections.
- *
- * @return bool
- */
- public function hasConnections(): bool
- {
- return count($this->subscribedConnections) > 0;
- }
-
- /**
- * Get all subscribed connections.
- *
- * @return array
- */
- public function getSubscribedConnections(): array
- {
- return $this->subscribedConnections;
- }
-
- /**
- * Check if the signature for the payload is valid.
- *
- * @param \Ratchet\ConnectionInterface $connection
- * @param \stdClass $payload
- * @return void
- * @throws InvalidSignature
- */
- protected function verifySignature(ConnectionInterface $connection, stdClass $payload)
- {
- $signature = "{$connection->socketId}:{$this->channelName}";
-
- if (isset($payload->channel_data)) {
- $signature .= ":{$payload->channel_data}";
- }
-
- if (! hash_equals(
- hash_hmac('sha256', $signature, $connection->app->secret),
- Str::after($payload->auth, ':'))
- ) {
- throw new InvalidSignature();
- }
- }
-
- /**
- * Subscribe to the channel.
- *
- * @see https://pusher.com/docs/pusher_protocol#presence-channel-events
- * @param \Ratchet\ConnectionInterface $connection
- * @param \stdClass $payload
- * @return void
- */
- public function subscribe(ConnectionInterface $connection, stdClass $payload)
- {
- $this->saveConnection($connection);
-
- $connection->send(json_encode([
- 'event' => 'pusher_internal:subscription_succeeded',
- 'channel' => $this->channelName,
- ]));
-
- $this->replicator->subscribe($connection->app->id, $this->channelName);
-
- Subscribed::dispatch($this->channelName, $connection);
- }
-
- /**
- * Unsubscribe connection from the channel.
- *
- * @param \Ratchet\ConnectionInterface $connection
- * @return void
- */
- public function unsubscribe(ConnectionInterface $connection)
- {
- unset($this->subscribedConnections[$connection->socketId]);
-
- $this->replicator->unsubscribe($connection->app->id, $this->channelName);
-
- if (! $this->hasConnections()) {
- DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_VACATED, [
- 'socketId' => $connection->socketId,
- 'channel' => $this->channelName,
- ]);
- }
-
- Unsubscribed::dispatch($this->channelName, $connection);
- }
-
- /**
- * Store the connection to the subscribers list.
- *
- * @param \Ratchet\ConnectionInterface $connection
- * @return void
- */
- protected function saveConnection(ConnectionInterface $connection)
- {
- $hadConnectionsPreviously = $this->hasConnections();
-
- $this->subscribedConnections[$connection->socketId] = $connection;
-
- if (! $hadConnectionsPreviously) {
- DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_OCCUPIED, [
- 'channel' => $this->channelName,
- ]);
- }
-
- DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_SUBSCRIBED, [
- 'socketId' => $connection->socketId,
- 'channel' => $this->channelName,
- ]);
- }
-
- /**
- * Broadcast a payload to the subscribed connections.
- *
- * @param \stdClass $payload
- * @return void
- */
- public function broadcast($payload)
- {
- foreach ($this->subscribedConnections as $connection) {
- $connection->send(json_encode($payload));
- }
-
- MessagesBroadcasted::dispatch(count($this->subscribedConnections));
- }
-
- /**
- * Broadcast the payload, but exclude the current connection.
- *
- * @param \Ratchet\ConnectionInterface $connection
- * @param \stdClass $payload
- * @return void
- */
- public function broadcastToOthers(ConnectionInterface $connection, stdClass $payload)
- {
- $this->broadcastToEveryoneExcept(
- $payload, $connection->socketId, $connection->app->id
- );
- }
-
- /**
- * Broadcast the payload, but exclude a specific socket id.
- *
- * @param \stdClass $payload
- * @param string|null $socketId
- * @param mixed $appId
- * @param bool $publish
- * @return void
- */
- public function broadcastToEveryoneExcept(stdClass $payload, ?string $socketId, $appId, bool $publish = true)
- {
- // Also broadcast via the other websocket server instances.
- // This is set false in the Redis client because we don't want to cause a loop
- // in this case. If this came from TriggerEventController, then we still want
- // to publish to get the message out to other server instances.
- if ($publish) {
- $this->replicator->publish($appId, $this->channelName, $payload);
- }
-
- // Performance optimization, if we don't have a socket ID,
- // then we avoid running the if condition in the foreach loop below
- // by calling broadcast() instead.
- if (is_null($socketId)) {
- $this->broadcast($payload);
-
- return;
- }
-
- $connections = collect($this->subscribedConnections)
- ->reject(function ($connection) use ($socketId) {
- return $connection->socketId === $socketId;
- });
-
- foreach ($connections as $connection) {
- $connection->send(json_encode($payload));
- }
-
- MessagesBroadcasted::dispatch($connections->count());
- }
-
- /**
- * Convert the channel to array.
- *
- * @param mixed $appId
- * @return array
- */
- public function toArray($appId = null)
- {
- return [
- 'occupied' => count($this->subscribedConnections) > 0,
- 'subscription_count' => count($this->subscribedConnections),
- ];
- }
-}
diff --git a/src/WebSockets/Channels/ChannelManager.php b/src/WebSockets/Channels/ChannelManager.php
deleted file mode 100644
index 2baedc3..0000000
--- a/src/WebSockets/Channels/ChannelManager.php
+++ /dev/null
@@ -1,58 +0,0 @@
-channels[$appId][$channelName])) {
- $channelClass = $this->determineChannelClass($channelName);
-
- $this->channels[$appId][$channelName] = new $channelClass($channelName);
- }
-
- return $this->channels[$appId][$channelName];
- }
-
- /**
- * Find a channel by name.
- *
- * @param mixed $appId
- * @param string $channelName
- * @return \BeyondCode\LaravelWebSockets\WebSockets\Channels
- */
- public function find($appId, string $channelName): ?Channel
- {
- return $this->channels[$appId][$channelName] ?? null;
- }
-
- /**
- * Get all channels.
- *
- * @param mixed $appId
- * @return array
- */
- public function getChannels($appId): array
- {
- return $this->channels[$appId] ?? [];
- }
-
- /**
- * Get the connections count on the app.
- *
- * @param mixed $appId
- * @return int|\React\Promise\PromiseInterface
- */
- public function getLocalConnectionsCount($appId): int
- {
- return collect($this->getChannels($appId))
- ->flatMap(function (Channel $channel) {
- return collect($channel->getSubscribedConnections())->pluck('socketId');
- })
- ->unique()
- ->count();
- }
-
- /**
- * Get the connections count across multiple servers.
- *
- * @param mixed $appId
- * @return int|\React\Promise\PromiseInterface
- */
- public function getGlobalConnectionsCount($appId)
- {
- return $this->getLocalConnectionsCount($appId);
- }
-
- /**
- * Remove connection from all channels.
- *
- * @param \Ratchet\ConnectionInterface $connection
- * @return void
- */
- public function removeFromAllChannels(ConnectionInterface $connection)
- {
- if (! isset($connection->app)) {
- return;
- }
-
- collect(Arr::get($this->channels, $connection->app->id, []))
- ->each->unsubscribe($connection);
-
- collect(Arr::get($this->channels, $connection->app->id, []))
- ->reject->hasConnections()
- ->each(function (Channel $channel, string $channelName) use ($connection) {
- unset($this->channels[$connection->app->id][$channelName]);
- });
-
- if (count(Arr::get($this->channels, $connection->app->id, [])) === 0) {
- unset($this->channels[$connection->app->id]);
- }
- }
-
- /**
- * Get the channel class by the channel name.
- *
- * @param string $channelName
- * @return string
- */
- protected function determineChannelClass(string $channelName): string
- {
- if (Str::startsWith($channelName, 'private-')) {
- return PrivateChannel::class;
- }
-
- if (Str::startsWith($channelName, 'presence-')) {
- return PresenceChannel::class;
- }
-
- return Channel::class;
- }
-}
diff --git a/src/WebSockets/Channels/ChannelManagers/RedisChannelManager.php b/src/WebSockets/Channels/ChannelManagers/RedisChannelManager.php
deleted file mode 100644
index cda98df..0000000
--- a/src/WebSockets/Channels/ChannelManagers/RedisChannelManager.php
+++ /dev/null
@@ -1,36 +0,0 @@
-replicator = app(ReplicationInterface::class);
- }
-
- /**
- * Get the connections count across multiple servers.
- *
- * @param mixed $appId
- * @return int|\React\Promise\PromiseInterface
- */
- public function getGlobalConnectionsCount($appId)
- {
- return $this->replicator->getGlobalConnectionsCount($appId);
- }
-}
diff --git a/src/WebSockets/Channels/PresenceChannel.php b/src/WebSockets/Channels/PresenceChannel.php
deleted file mode 100644
index a3e58aa..0000000
--- a/src/WebSockets/Channels/PresenceChannel.php
+++ /dev/null
@@ -1,178 +0,0 @@
-replicator->channelMembers($appId, $this->channelName);
- }
-
- /**
- * Subscribe the connection to the channel.
- *
- * @param ConnectionInterface $connection
- * @param stdClass $payload
- * @return void
- * @throws InvalidSignature
- * @see https://pusher.com/docs/pusher_protocol#presence-channel-events
- */
- public function subscribe(ConnectionInterface $connection, stdClass $payload)
- {
- $this->verifySignature($connection, $payload);
-
- $this->saveConnection($connection);
-
- $channelData = json_decode($payload->channel_data);
- $this->users[$connection->socketId] = $channelData;
-
- // Add the connection as a member of the channel
- $this->replicator->joinChannel(
- $connection->app->id,
- $this->channelName,
- $connection->socketId,
- json_encode($channelData)
- );
-
- // We need to pull the channel data from the replication backend,
- // otherwise we won't be sending the full details of the channel
- $this->replicator
- ->channelMembers($connection->app->id, $this->channelName)
- ->then(function ($users) use ($connection) {
- $connection->send(json_encode([
- 'event' => 'pusher_internal:subscription_succeeded',
- 'channel' => $this->channelName,
- 'data' => json_encode($this->getChannelData($users)),
- ]));
- });
-
- $this->broadcastToOthers($connection, (object) [
- 'event' => 'pusher_internal:member_added',
- 'channel' => $this->channelName,
- 'data' => json_encode($channelData),
- ]);
- }
-
- /**
- * Unsubscribe the connection from the Presence channel.
- *
- * @param ConnectionInterface $connection
- * @return void
- */
- public function unsubscribe(ConnectionInterface $connection)
- {
- parent::unsubscribe($connection);
-
- if (! isset($this->users[$connection->socketId])) {
- return;
- }
-
- // Remove the connection as a member of the channel
- $this->replicator
- ->leaveChannel(
- $connection->app->id,
- $this->channelName,
- $connection->socketId
- );
-
- $this->broadcastToOthers($connection, (object) [
- 'event' => 'pusher_internal:member_removed',
- 'channel' => $this->channelName,
- 'data' => json_encode([
- 'user_id' => $this->users[$connection->socketId]->user_id,
- ]),
- ]);
-
- unset($this->users[$connection->socketId]);
- }
-
- /**
- * Get the Presence Channel to array.
- *
- * @param string|null $appId
- * @return PromiseInterface
- */
- public function toArray($appId = null)
- {
- return $this->replicator
- ->channelMembers($appId, $this->channelName)
- ->then(function ($users) {
- return array_merge(parent::toArray(), [
- 'user_count' => count($users),
- ]);
- });
- }
-
- /**
- * Get the Presence channel data.
- *
- * @param array $users
- * @return array
- */
- protected function getChannelData(array $users): array
- {
- return [
- 'presence' => [
- 'ids' => $this->getUserIds($users),
- 'hash' => $this->getHash($users),
- 'count' => count($users),
- ],
- ];
- }
-
- /**
- * Get the Presence Channel's users.
- *
- * @param array $users
- * @return array
- */
- protected function getUserIds(array $users): array
- {
- $userIds = array_map(function ($channelData) {
- return (string) $channelData->user_id;
- }, $users);
-
- return array_values($userIds);
- }
-
- /**
- * Compute the hash for the presence channel integrity.
- *
- * @param array $users
- * @return array
- */
- protected function getHash(array $users): array
- {
- $hash = [];
-
- foreach ($users as $socketId => $channelData) {
- $hash[$channelData->user_id] = $channelData->user_info ?? [];
- }
-
- return $hash;
- }
-}
diff --git a/src/WebSockets/Exceptions/InvalidConnection.php b/src/WebSockets/Exceptions/InvalidConnection.php
deleted file mode 100644
index 268b55f..0000000
--- a/src/WebSockets/Exceptions/InvalidConnection.php
+++ /dev/null
@@ -1,18 +0,0 @@
-message = 'Invalid Connection';
- $this->code = 4009;
- }
-}
diff --git a/src/WebSockets/Exceptions/OriginNotAllowed.php b/src/WebSockets/Exceptions/OriginNotAllowed.php
deleted file mode 100644
index 87fef2c..0000000
--- a/src/WebSockets/Exceptions/OriginNotAllowed.php
+++ /dev/null
@@ -1,18 +0,0 @@
-message = "The origin is not allowed for `{$appKey}`.";
- $this->code = 4009;
- }
-}
diff --git a/src/WebSockets/Exceptions/UnknownAppKey.php b/src/WebSockets/Exceptions/UnknownAppKey.php
deleted file mode 100644
index f872f33..0000000
--- a/src/WebSockets/Exceptions/UnknownAppKey.php
+++ /dev/null
@@ -1,13 +0,0 @@
-message = "Could not find app key `{$appKey}`.";
-
- $this->code = 4001;
- }
-}
diff --git a/src/WebSocketsServiceProvider.php b/src/WebSocketsServiceProvider.php
index a2ca289..937a57d 100644
--- a/src/WebSocketsServiceProvider.php
+++ b/src/WebSocketsServiceProvider.php
@@ -2,19 +2,15 @@
namespace BeyondCode\LaravelWebSockets;
-use BeyondCode\LaravelWebSockets\Apps\AppManager;
+use Illuminate\Support\ServiceProvider;
+use BeyondCode\LaravelWebSockets\Server\Router;
+use BeyondCode\LaravelWebSockets\Dashboard\Http\Middleware\Authorize as AuthorizeDashboard;
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\AuthenticateDashboard;
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\SendMessage;
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowDashboard;
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowStatistics;
-use BeyondCode\LaravelWebSockets\Dashboard\Http\Middleware\Authorize as AuthorizeDashboard;
-use BeyondCode\LaravelWebSockets\Server\Router;
-use BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver;
-use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
-use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManagers\ArrayChannelManager;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
-use Illuminate\Support\ServiceProvider;
class WebSocketsServiceProvider extends ServiceProvider
{
@@ -26,23 +22,20 @@ class WebSocketsServiceProvider extends ServiceProvider
public function boot()
{
$this->publishes([
- __DIR__.'/../config/websockets.php' => base_path('config/websockets.php'),
+ __DIR__.'/../config/websockets.php' => config_path('websockets.php'),
], 'config');
+ $this->mergeConfigFrom(
+ __DIR__.'/../config/websockets.php', 'websockets'
+ );
+
$this->publishes([
__DIR__.'/../database/migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php' => database_path('migrations/0000_00_00_000000_create_websockets_statistics_entries_table.php'),
], 'migrations');
- $this->registerDashboardRoutes()
- ->registerDashboardGate();
+ $this->registerDashboard();
- $this->loadViewsFrom(__DIR__.'/../resources/views/', 'websockets');
-
- $this->commands([
- Console\StartWebSocketServer::class,
- Console\CleanStatistics::class,
- Console\RestartWebSocketServer::class,
- ]);
+ $this->registerCommands();
}
/**
@@ -52,34 +45,59 @@ class WebSocketsServiceProvider extends ServiceProvider
*/
public function register()
{
- $this->mergeConfigFrom(__DIR__.'/../config/websockets.php', 'websockets');
+ $this->registerRouter();
+ $this->registerManagers();
+ }
+ /**
+ * Regsiter the dashboard components.
+ *
+ * @return void
+ */
+ protected function registerDashboard()
+ {
+ $this->loadViewsFrom(__DIR__.'/../resources/views/', 'websockets');
+
+ $this->registerDashboardRoutes();
+ $this->registerDashboardGate();
+ }
+
+ /**
+ * Register the package commands.
+ *
+ * @return void
+ */
+ protected function registerCommands()
+ {
+ $this->commands([
+ Console\Commands\StartServer::class,
+ Console\Commands\RestartServer::class,
+ Console\Commands\CleanStatistics::class,
+ ]);
+ }
+
+ /**
+ * Register the routing.
+ *
+ * @return void
+ */
+ protected function registerRouter()
+ {
$this->app->singleton('websockets.router', function () {
- return new Router();
+ return new Router;
});
+ }
- $this->app->singleton(ChannelManager::class, function () {
- $replicationDriver = config('websockets.replication.driver', 'local');
-
- $class = config("websockets.replication.{$replicationDriver}.channel_manager", ArrayChannelManager::class);
-
- return new $class;
- });
-
- $this->app->singleton(AppManager::class, function () {
+ /**
+ * Register the managers for the app.
+ *
+ * @return void
+ */
+ protected function registerManagers()
+ {
+ $this->app->singleton(Contracts\AppManager::class, function () {
return $this->app->make(config('websockets.managers.app'));
});
-
- $this->app->singleton(StatisticsDriver::class, function () {
- $driver = config('websockets.statistics.driver', 'local');
-
- return $this->app->make(
- config(
- "websockets.statistics.{$driver}.driver",
- \BeyondCode\LaravelWebSockets\Statistics\Drivers\DatabaseDriver::class
- )
- );
- });
}
/**
@@ -99,8 +117,6 @@ class WebSocketsServiceProvider extends ServiceProvider
Route::post('/auth', AuthenticateDashboard::class)->name('auth');
Route::post('/event', SendMessage::class)->name('event');
});
-
- return $this;
}
/**
@@ -113,7 +129,5 @@ class WebSocketsServiceProvider extends ServiceProvider
Gate::define('viewWebSocketsDashboard', function ($user = null) {
return $this->app->environment('local');
});
-
- return $this;
}
}
diff --git a/tests/Channels/ChannelReplicationTest.php b/tests/Channels/ChannelReplicationTest.php
deleted file mode 100644
index adf1e9a..0000000
--- a/tests/Channels/ChannelReplicationTest.php
+++ /dev/null
@@ -1,158 +0,0 @@
-runOnlyOnRedisReplication();
- }
-
- /** @test */
- public function replication_clients_can_subscribe_to_channels()
- {
- $connection = $this->getWebSocketConnection();
-
- $message = new Message([
- 'event' => 'pusher:subscribe',
- 'data' => [
- 'channel' => 'basic-channel',
- ],
- ]);
-
- $this->pusherServer->onOpen($connection);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $connection->assertSentEvent('pusher_internal:subscription_succeeded', [
- 'channel' => 'basic-channel',
- ]);
- }
-
- /** @test */
- public function replication_clients_can_unsubscribe_from_channels()
- {
- $connection = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $channel = $this->getChannel($connection, 'test-channel');
-
- $this->assertTrue($channel->hasConnections());
-
- $message = new Message([
- 'event' => 'pusher:unsubscribe',
- 'data' => [
- 'channel' => 'test-channel',
- ],
- ]);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $this->assertFalse($channel->hasConnections());
- }
-
- /** @test */
- public function replication_a_client_cannot_broadcast_to_other_clients_by_default()
- {
- // One connection inside channel "test-channel".
- $existingConnection = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $connection = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $message = new Message(['event' => 'client-test', 'data' => [], 'channel' => 'test-channel']);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $existingConnection->assertNotSentEvent('client-test');
- }
-
- /** @test */
- public function replication_a_client_can_be_enabled_to_broadcast_to_other_clients()
- {
- config()->set('websockets.apps.0.enable_client_messages', true);
-
- // One connection inside channel "test-channel".
- $existingConnection = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $connection = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $message = new Message(['event' => 'client-test', 'data' => [], 'channel' => 'test-channel']);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $existingConnection->assertSentEvent('client-test');
- }
-
- /** @test */
- public function replication_closed_connections_get_removed_from_all_connected_channels()
- {
- $connection = $this->getConnectedWebSocketConnection(['test-channel-1', 'test-channel-2']);
-
- $channel1 = $this->getChannel($connection, 'test-channel-1');
- $channel2 = $this->getChannel($connection, 'test-channel-2');
-
- $this->assertTrue($channel1->hasConnections());
- $this->assertTrue($channel2->hasConnections());
-
- $this->pusherServer->onClose($connection);
-
- $this->assertFalse($channel1->hasConnections());
- $this->assertFalse($channel2->hasConnections());
- }
-
- /** @test */
- public function replication_channels_can_broadcast_messages_to_all_connections()
- {
- $connection1 = $this->getConnectedWebSocketConnection(['test-channel']);
- $connection2 = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $channel = $this->getChannel($connection1, 'test-channel');
-
- $channel->broadcast([
- 'event' => 'broadcasted-event',
- 'channel' => 'test-channel',
- ]);
-
- $connection1->assertSentEvent('broadcasted-event');
- $connection2->assertSentEvent('broadcasted-event');
- }
-
- /** @test */
- public function replication_channels_can_broadcast_messages_to_all_connections_except_the_given_connection()
- {
- $connection1 = $this->getConnectedWebSocketConnection(['test-channel']);
- $connection2 = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $channel = $this->getChannel($connection1, 'test-channel');
-
- $channel->broadcastToOthers($connection1, (object) [
- 'event' => 'broadcasted-event',
- 'channel' => 'test-channel',
- ]);
-
- $connection1->assertNotSentEvent('broadcasted-event');
- $connection2->assertSentEvent('broadcasted-event');
- }
-
- /** @test */
- public function replication_it_responds_correctly_to_the_ping_message()
- {
- $connection = $this->getConnectedWebSocketConnection();
-
- $message = new Message([
- 'event' => 'pusher:ping',
- ]);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $connection->assertSentEvent('pusher:pong');
- }
-}
diff --git a/tests/Channels/ChannelTest.php b/tests/Channels/ChannelTest.php
deleted file mode 100644
index 333a38d..0000000
--- a/tests/Channels/ChannelTest.php
+++ /dev/null
@@ -1,148 +0,0 @@
-getWebSocketConnection();
-
- $message = new Message([
- 'event' => 'pusher:subscribe',
- 'data' => [
- 'channel' => 'basic-channel',
- ],
- ]);
-
- $this->pusherServer->onOpen($connection);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $connection->assertSentEvent('pusher_internal:subscription_succeeded', [
- 'channel' => 'basic-channel',
- ]);
- }
-
- /** @test */
- public function clients_can_unsubscribe_from_channels()
- {
- $connection = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $channel = $this->getChannel($connection, 'test-channel');
-
- $this->assertTrue($channel->hasConnections());
-
- $message = new Message([
- 'event' => 'pusher:unsubscribe',
- 'data' => [
- 'channel' => 'test-channel',
- ],
- ]);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $this->assertFalse($channel->hasConnections());
- }
-
- /** @test */
- public function a_client_cannot_broadcast_to_other_clients_by_default()
- {
- // One connection inside channel "test-channel".
- $existingConnection = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $connection = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $message = new Message(['event' => 'client-test', 'data' => [], 'channel' => 'test-channel']);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $existingConnection->assertNotSentEvent('client-test');
- }
-
- /** @test */
- public function a_client_can_be_enabled_to_broadcast_to_other_clients()
- {
- config()->set('websockets.apps.0.enable_client_messages', true);
-
- // One connection inside channel "test-channel".
- $existingConnection = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $connection = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $message = new Message(['event' => 'client-test', 'data' => [], 'channel' => 'test-channel']);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $existingConnection->assertSentEvent('client-test');
- }
-
- /** @test */
- public function closed_connections_get_removed_from_all_connected_channels()
- {
- $connection = $this->getConnectedWebSocketConnection(['test-channel-1', 'test-channel-2']);
-
- $channel1 = $this->getChannel($connection, 'test-channel-1');
- $channel2 = $this->getChannel($connection, 'test-channel-2');
-
- $this->assertTrue($channel1->hasConnections());
- $this->assertTrue($channel2->hasConnections());
-
- $this->pusherServer->onClose($connection);
-
- $this->assertFalse($channel1->hasConnections());
- $this->assertFalse($channel2->hasConnections());
- }
-
- /** @test */
- public function channels_can_broadcast_messages_to_all_connections()
- {
- $connection1 = $this->getConnectedWebSocketConnection(['test-channel']);
- $connection2 = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $channel = $this->getChannel($connection1, 'test-channel');
-
- $channel->broadcast([
- 'event' => 'broadcasted-event',
- 'channel' => 'test-channel',
- ]);
-
- $connection1->assertSentEvent('broadcasted-event');
- $connection2->assertSentEvent('broadcasted-event');
- }
-
- /** @test */
- public function channels_can_broadcast_messages_to_all_connections_except_the_given_connection()
- {
- $connection1 = $this->getConnectedWebSocketConnection(['test-channel']);
- $connection2 = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $channel = $this->getChannel($connection1, 'test-channel');
-
- $channel->broadcastToOthers($connection1, (object) [
- 'event' => 'broadcasted-event',
- 'channel' => 'test-channel',
- ]);
-
- $connection1->assertNotSentEvent('broadcasted-event');
- $connection2->assertSentEvent('broadcasted-event');
- }
-
- /** @test */
- public function it_responds_correctly_to_the_ping_message()
- {
- $connection = $this->getConnectedWebSocketConnection();
-
- $message = new Message([
- 'event' => 'pusher:ping',
- ]);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $connection->assertSentEvent('pusher:pong');
- }
-}
diff --git a/tests/Channels/PresenceChannelReplicationTest.php b/tests/Channels/PresenceChannelReplicationTest.php
deleted file mode 100644
index 67ade9f..0000000
--- a/tests/Channels/PresenceChannelReplicationTest.php
+++ /dev/null
@@ -1,140 +0,0 @@
-runOnlyOnRedisReplication();
- }
-
- /** @test */
- public function clients_with_valid_auth_signatures_can_join_presence_channels()
- {
- $connection = $this->getWebSocketConnection();
-
- $this->pusherServer->onOpen($connection);
-
- $channelData = [
- 'user_id' => 1,
- 'user_info' => [
- 'name' => 'Marcel',
- ],
- ];
-
- $signature = "{$connection->socketId}:presence-channel:".json_encode($channelData);
-
- $message = new Message([
- 'event' => 'pusher:subscribe',
- 'data' => [
- 'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
- 'channel' => 'presence-channel',
- 'channel_data' => json_encode($channelData),
- ],
- ]);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $this->getPublishClient()
- ->assertCalledWithArgs('hset', [
- 'laravel_database_1234:presence-channel',
- $connection->socketId,
- json_encode($channelData),
- ])
- ->assertCalledWithArgs('hgetall', ['laravel_database_1234:presence-channel'])
- ->assertCalled('publish');
-
- $this->assertNotNull(
- $this->redis->hget('laravel_database_1234:presence-channel', $connection->socketId)
- );
- }
-
- /** @test */
- public function clients_with_valid_auth_signatures_can_leave_presence_channels()
- {
- $connection = $this->getWebSocketConnection();
-
- $this->pusherServer->onOpen($connection);
-
- $channelData = [
- 'user_id' => 1,
- ];
-
- $signature = "{$connection->socketId}:presence-channel:".json_encode($channelData);
-
- $message = new Message([
- 'event' => 'pusher:subscribe',
- 'data' => [
- 'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
- 'channel' => 'presence-channel',
- 'channel_data' => json_encode($channelData),
- ],
- ]);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $this->getSubscribeClient()
- ->assertEventDispatched('message');
-
- $this->getPublishClient()
- ->assertCalled('hset')
- ->assertCalledWithArgs('hgetall', ['laravel_database_1234:presence-channel'])
- ->assertCalled('publish');
-
- $this->getPublishClient()
- ->resetAssertions();
-
- $message = new Message([
- 'event' => 'pusher:unsubscribe',
- 'data' => [
- 'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
- 'channel' => 'presence-channel',
- ],
- ]);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $this->getPublishClient()
- ->assertCalled('hdel')
- ->assertCalled('publish');
- }
-
- /** @test */
- public function clients_with_no_user_info_can_join_presence_channels()
- {
- $connection = $this->getWebSocketConnection();
-
- $this->pusherServer->onOpen($connection);
-
- $channelData = [
- 'user_id' => 1,
- ];
-
- $signature = "{$connection->socketId}:presence-channel:".json_encode($channelData);
-
- $message = new Message([
- 'event' => 'pusher:subscribe',
- 'data' => [
- 'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
- 'channel' => 'presence-channel',
- 'channel_data' => json_encode($channelData),
- ],
- ]);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $this->getPublishClient()
- ->assertCalled('hset')
- ->assertCalledWithArgs('hgetall', ['laravel_database_1234:presence-channel'])
- ->assertCalled('publish');
- }
-}
diff --git a/tests/Channels/PresenceChannelTest.php b/tests/Channels/PresenceChannelTest.php
deleted file mode 100644
index f6481af..0000000
--- a/tests/Channels/PresenceChannelTest.php
+++ /dev/null
@@ -1,165 +0,0 @@
-expectException(InvalidSignature::class);
-
- $connection = $this->getWebSocketConnection();
-
- $message = new Message([
- 'event' => 'pusher:subscribe',
- 'data' => [
- 'auth' => 'invalid',
- 'channel' => 'presence-channel',
- ],
- ]);
-
- $this->pusherServer->onOpen($connection);
-
- $this->pusherServer->onMessage($connection, $message);
- }
-
- /** @test */
- public function clients_with_valid_auth_signatures_can_join_presence_channels()
- {
- $this->skipOnRedisReplication();
-
- $connection = $this->getWebSocketConnection();
-
- $this->pusherServer->onOpen($connection);
-
- $channelData = [
- 'user_id' => 1,
- 'user_info' => [
- 'name' => 'Marcel',
- ],
- ];
-
- $signature = "{$connection->socketId}:presence-channel:".json_encode($channelData);
-
- $message = new Message([
- 'event' => 'pusher:subscribe',
- 'data' => [
- 'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
- 'channel' => 'presence-channel',
- 'channel_data' => json_encode($channelData),
- ],
- ]);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $connection->assertSentEvent('pusher_internal:subscription_succeeded', [
- 'channel' => 'presence-channel',
- ]);
- }
-
- /** @test */
- public function clients_with_valid_auth_signatures_can_leave_presence_channels()
- {
- $this->skipOnRedisReplication();
-
- $connection = $this->getWebSocketConnection();
-
- $this->pusherServer->onOpen($connection);
-
- $channelData = [
- 'user_id' => 1,
- ];
-
- $signature = "{$connection->socketId}:presence-channel:".json_encode($channelData);
-
- $message = new Message([
- 'event' => 'pusher:subscribe',
- 'data' => [
- 'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
- 'channel' => 'presence-channel',
- 'channel_data' => json_encode($channelData),
- ],
- ]);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $connection->assertSentEvent('pusher_internal:subscription_succeeded', [
- 'channel' => 'presence-channel',
- ]);
-
- $message = new Message([
- 'event' => 'pusher:unsubscribe',
- 'data' => [
- 'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
- 'channel' => 'presence-channel',
- ],
- ]);
-
- $this->pusherServer->onMessage($connection, $message);
- }
-
- /** @test */
- public function clients_with_no_user_info_can_join_presence_channels()
- {
- $this->skipOnRedisReplication();
-
- $connection = $this->getWebSocketConnection();
-
- $this->pusherServer->onOpen($connection);
-
- $channelData = [
- 'user_id' => 1,
- ];
-
- $signature = "{$connection->socketId}:presence-channel:".json_encode($channelData);
-
- $message = new Message([
- 'event' => 'pusher:subscribe',
- 'data' => [
- 'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
- 'channel' => 'presence-channel',
- 'channel_data' => json_encode($channelData),
- ],
- ]);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $connection->assertSentEvent('pusher_internal:subscription_succeeded', [
- 'channel' => 'presence-channel',
- ]);
- }
-
- /** @test */
- public function clients_with_valid_auth_signatures_cannot_leave_channels_they_are_not_in()
- {
- $connection = $this->getWebSocketConnection();
-
- $this->pusherServer->onOpen($connection);
-
- $channelData = [
- 'user_id' => 1,
- 'user_info' => [
- 'name' => 'Marcel',
- ],
- ];
-
- $signature = "{$connection->socketId}:presence-channel:".json_encode($channelData);
-
- $message = new Message([
- 'event' => 'pusher:unsubscribe',
- 'data' => [
- 'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
- 'channel' => 'presence-channel',
- ],
- ]);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $this->assertTrue(true);
- }
-}
diff --git a/tests/Channels/PrivateChannelReplicationTest.php b/tests/Channels/PrivateChannelReplicationTest.php
deleted file mode 100644
index 3a16412..0000000
--- a/tests/Channels/PrivateChannelReplicationTest.php
+++ /dev/null
@@ -1,66 +0,0 @@
-runOnlyOnRedisReplication();
- }
-
- /** @test */
- public function replication_clients_need_valid_auth_signatures_to_join_private_channels()
- {
- $this->expectException(InvalidSignature::class);
-
- $connection = $this->getWebSocketConnection();
-
- $message = new Message([
- 'event' => 'pusher:subscribe',
- 'data' => [
- 'auth' => 'invalid',
- 'channel' => 'private-channel',
- ],
- ]);
-
- $this->pusherServer->onOpen($connection);
-
- $this->pusherServer->onMessage($connection, $message);
- }
-
- /** @test */
- public function replication_clients_with_valid_auth_signatures_can_join_private_channels()
- {
- $connection = $this->getWebSocketConnection();
-
- $this->pusherServer->onOpen($connection);
-
- $signature = "{$connection->socketId}:private-channel";
-
- $hashedAppSecret = hash_hmac('sha256', $signature, $connection->app->secret);
-
- $message = new Message([
- 'event' => 'pusher:subscribe',
- 'data' => [
- 'auth' => "{$connection->app->key}:{$hashedAppSecret}",
- 'channel' => 'private-channel',
- ],
- ]);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $connection->assertSentEvent('pusher_internal:subscription_succeeded', [
- 'channel' => 'private-channel',
- ]);
- }
-}
diff --git a/tests/Channels/PrivateChannelTest.php b/tests/Channels/PrivateChannelTest.php
deleted file mode 100644
index 91f48d0..0000000
--- a/tests/Channels/PrivateChannelTest.php
+++ /dev/null
@@ -1,56 +0,0 @@
-expectException(InvalidSignature::class);
-
- $connection = $this->getWebSocketConnection();
-
- $message = new Message([
- 'event' => 'pusher:subscribe',
- 'data' => [
- 'auth' => 'invalid',
- 'channel' => 'private-channel',
- ],
- ]);
-
- $this->pusherServer->onOpen($connection);
-
- $this->pusherServer->onMessage($connection, $message);
- }
-
- /** @test */
- public function clients_with_valid_auth_signatures_can_join_private_channels()
- {
- $connection = $this->getWebSocketConnection();
-
- $this->pusherServer->onOpen($connection);
-
- $signature = "{$connection->socketId}:private-channel";
-
- $hashedAppSecret = hash_hmac('sha256', $signature, $connection->app->secret);
-
- $message = new Message([
- 'event' => 'pusher:subscribe',
- 'data' => [
- 'auth' => "{$connection->app->key}:{$hashedAppSecret}",
- 'channel' => 'private-channel',
- ],
- ]);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $connection->assertSentEvent('pusher_internal:subscription_succeeded', [
- 'channel' => 'private-channel',
- ]);
- }
-}
diff --git a/tests/ClientProviders/AppTest.php b/tests/ClientProviders/AppTest.php
deleted file mode 100644
index b20e38f..0000000
--- a/tests/ClientProviders/AppTest.php
+++ /dev/null
@@ -1,34 +0,0 @@
-assertTrue(true);
- }
-
- /** @test */
- public function it_will_not_accept_an_empty_appKey()
- {
- $this->expectException(InvalidApp::class);
-
- new App(1, '', 'appSecret');
- }
-
- /** @test */
- public function it_will_not_accept_an_empty_appSecret()
- {
- $this->expectException(InvalidApp::class);
-
- new App(1, 'appKey', '');
- }
-}
diff --git a/tests/ClientProviders/ConfigAppManagerTest.php b/tests/ClientProviders/ConfigAppManagerTest.php
deleted file mode 100644
index 9ba5561..0000000
--- a/tests/ClientProviders/ConfigAppManagerTest.php
+++ /dev/null
@@ -1,88 +0,0 @@
-appManager = new ConfigAppManager;
- }
-
- /** @test */
- public function it_can_get_apps_from_the_config_file()
- {
- $apps = $this->appManager->all();
-
- $this->assertCount(2, $apps);
-
- /** @var $app */
- $app = $apps[0];
-
- $this->assertEquals('Test App', $app->name);
- $this->assertEquals(1234, $app->id);
- $this->assertEquals('TestKey', $app->key);
- $this->assertEquals('TestSecret', $app->secret);
- $this->assertFalse($app->clientMessagesEnabled);
- $this->assertTrue($app->statisticsEnabled);
- }
-
- /** @test */
- public function it_can_find_app_by_id()
- {
- $app = $this->appManager->findById(0000);
-
- $this->assertNull($app);
-
- $app = $this->appManager->findById(1234);
-
- $this->assertEquals('Test App', $app->name);
- $this->assertEquals(1234, $app->id);
- $this->assertEquals('TestKey', $app->key);
- $this->assertEquals('TestSecret', $app->secret);
- $this->assertFalse($app->clientMessagesEnabled);
- $this->assertTrue($app->statisticsEnabled);
- }
-
- /** @test */
- public function it_can_find_app_by_key()
- {
- $app = $this->appManager->findByKey('InvalidKey');
-
- $this->assertNull($app);
-
- $app = $this->appManager->findByKey('TestKey');
-
- $this->assertEquals('Test App', $app->name);
- $this->assertEquals(1234, $app->id);
- $this->assertEquals('TestKey', $app->key);
- $this->assertEquals('TestSecret', $app->secret);
- $this->assertFalse($app->clientMessagesEnabled);
- $this->assertTrue($app->statisticsEnabled);
- }
-
- /** @test */
- public function it_can_find_app_by_secret()
- {
- $app = $this->appManager->findBySecret('InvalidSecret');
-
- $this->assertNull($app);
-
- $app = $this->appManager->findBySecret('TestSecret');
-
- $this->assertEquals('Test App', $app->name);
- $this->assertEquals(1234, $app->id);
- $this->assertEquals('TestKey', $app->key);
- $this->assertEquals('TestSecret', $app->secret);
- $this->assertFalse($app->clientMessagesEnabled);
- $this->assertTrue($app->statisticsEnabled);
- }
-}
diff --git a/tests/Commands/CleanStatisticsTest.php b/tests/Commands/CleanStatisticsTest.php
deleted file mode 100644
index 9e26a6d..0000000
--- a/tests/Commands/CleanStatisticsTest.php
+++ /dev/null
@@ -1,75 +0,0 @@
-app['config']->set('websockets.statistics.delete_statistics_older_than_days', 31);
- }
-
- /** @test */
- public function it_can_clean_the_statistics()
- {
- Collection::times(60)->each(function (int $index) {
- WebSocketsStatisticsEntry::create([
- 'app_id' => 'app_id',
- 'peak_connection_count' => 1,
- 'websocket_message_count' => 2,
- 'api_message_count' => 3,
- 'created_at' => Carbon::now()->subDays($index)->startOfDay(),
- ]);
- });
-
- $this->assertCount(60, WebSocketsStatisticsEntry::all());
-
- Artisan::call('websockets:clean');
-
- $this->assertCount(31, WebSocketsStatisticsEntry::all());
-
- $cutOffDate = Carbon::now()->subDays(31)->format('Y-m-d H:i:s');
-
- $this->assertCount(0, WebSocketsStatisticsEntry::where('created_at', '<', $cutOffDate)->get());
- }
-
- /** @test */
- public function it_can_clean_the_statistics_for_app_id_only()
- {
- Collection::times(60)->each(function (int $index) {
- WebSocketsStatisticsEntry::create([
- 'app_id' => 'app_id',
- 'peak_connection_count' => 1,
- 'websocket_message_count' => 2,
- 'api_message_count' => 3,
- 'created_at' => Carbon::now()->subDays($index)->startOfDay(),
- ]);
- });
-
- Collection::times(60)->each(function (int $index) {
- WebSocketsStatisticsEntry::create([
- 'app_id' => 'app_id2',
- 'peak_connection_count' => 1,
- 'websocket_message_count' => 2,
- 'api_message_count' => 3,
- 'created_at' => Carbon::now()->subDays($index)->startOfDay(),
- ]);
- });
-
- $this->assertCount(120, WebSocketsStatisticsEntry::all());
-
- Artisan::call('websockets:clean', ['appId' => 'app_id']);
-
- $this->assertCount(91, WebSocketsStatisticsEntry::all());
- }
-}
diff --git a/tests/Commands/RestartServerTest.php b/tests/Commands/RestartServerTest.php
new file mode 100644
index 0000000..8ea2802
--- /dev/null
+++ b/tests/Commands/RestartServerTest.php
@@ -0,0 +1,23 @@
+currentTime();
+
+ $this->artisan('websockets:restart');
+
+ $this->assertGreaterThanOrEqual(
+ $start, Cache::get('beyondcode:websockets:restart', 0)
+ );
+ }
+}
diff --git a/tests/Commands/RestartWebSocketServerTest.php b/tests/Commands/RestartWebSocketServerTest.php
deleted file mode 100644
index e80748a..0000000
--- a/tests/Commands/RestartWebSocketServerTest.php
+++ /dev/null
@@ -1,23 +0,0 @@
-currentTime();
-
- Artisan::call('websockets:restart');
-
- $this->assertGreaterThanOrEqual($start, Cache::get('beyondcode:websockets:restart', 0));
- }
-}
diff --git a/tests/Commands/StartServerTest.php b/tests/Commands/StartServerTest.php
new file mode 100644
index 0000000..223331c
--- /dev/null
+++ b/tests/Commands/StartServerTest.php
@@ -0,0 +1,15 @@
+artisan('websockets:serve', ['--test' => true, '--debug' => true]);
+
+ $this->assertTrue(true);
+ }
+}
diff --git a/tests/Commands/StartWebSocketServerTest.php b/tests/Commands/StartWebSocketServerTest.php
deleted file mode 100644
index 00d0d32..0000000
--- a/tests/Commands/StartWebSocketServerTest.php
+++ /dev/null
@@ -1,16 +0,0 @@
-artisan('websockets:serve', ['--test' => true, '--debug' => true]);
-
- $this->assertTrue(true);
- }
-}
diff --git a/tests/Commands/StatisticsCleanTest.php b/tests/Commands/StatisticsCleanTest.php
new file mode 100644
index 0000000..5d64902
--- /dev/null
+++ b/tests/Commands/StatisticsCleanTest.php
@@ -0,0 +1,47 @@
+newActiveConnection(['public-channel']);
+ $morty = $this->newActiveConnection(['public-channel'], 'TestKey2');
+
+ $this->statisticsCollector->save();
+
+ $this->assertCount(2, $records = $this->statisticsStore->getRecords());
+
+ foreach ($this->statisticsStore->getRawRecords() as $record) {
+ $record->update(['created_at' => now()->subDays(10)]);
+ };
+
+ $this->artisan('websockets:clean', [
+ 'appId' => '12345',
+ '--days' => 1,
+ ]);
+
+ $this->assertCount(1, $records = $this->statisticsStore->getRecords());
+ }
+
+ public function test_clean_statistics_older_than_given_days()
+ {
+ $rick = $this->newActiveConnection(['public-channel']);
+ $morty = $this->newActiveConnection(['public-channel'], 'TestKey2');
+
+ $this->statisticsCollector->save();
+
+ $this->assertCount(2, $records = $this->statisticsStore->getRecords());
+
+ foreach ($this->statisticsStore->getRawRecords() as $record) {
+ $record->update(['created_at' => now()->subDays(10)]);
+ };
+
+ $this->artisan('websockets:clean', ['--days' => 1]);
+
+ $this->assertCount(0, $records = $this->statisticsStore->getRecords());
+ }
+}
diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php
index 60392d4..1ff8d15 100644
--- a/tests/ConnectionTest.php
+++ b/tests/ConnectionTest.php
@@ -1,127 +1,110 @@
expectException(UnknownAppKey::class);
- $this->pusherServer->onOpen($this->getWebSocketConnection('test'));
+ $this->newActiveConnection(['public-channel'], 'NonWorkingKey');
}
- /** @test */
- public function known_app_keys_can_connect()
+ public function test_unconnected_app_cannot_store_statistics()
{
- $connection = $this->getWebSocketConnection();
+ $this->expectException(UnknownAppKey::class);
+
+ $this->newActiveConnection(['public-channel'], 'NonWorkingKey');
+
+ $this->assertCount(0, $this->statisticsCollector->getStatistics());
+ }
+
+ public function test_origin_validation_should_fail_for_no_origin()
+ {
+ $this->expectException(OriginNotAllowed::class);
+
+ $connection = $this->newConnection('TestOrigin');
+
+ $this->pusherServer->onOpen($connection);
+ }
+
+ public function test_origin_validation_should_fail_for_wrong_origin()
+ {
+ $this->expectException(OriginNotAllowed::class);
+
+ $connection = $this->newConnection('TestOrigin', ['Origin' => 'https://google.ro']);
+
+ $this->pusherServer->onOpen($connection);
+ }
+
+ public function test_origin_validation_should_pass_for_the_right_origin()
+ {
+ $connection = $this->newConnection('TestOrigin', ['Origin' => 'https://test.origin.com']);
$this->pusherServer->onOpen($connection);
$connection->assertSentEvent('pusher:connection_established');
}
- /** @test */
- public function app_can_not_exceed_maximum_capacity()
+ public function test_close_connection()
{
- $this->runOnlyOnLocalReplication();
+ $connection = $this->newActiveConnection(['public-channel']);
- $this->app['config']->set('websockets.apps.0.capacity', 2);
+ $this->channelManager
+ ->getGlobalChannels('1234')
+ ->then(function ($channels) {
+ $this->assertCount(1, $channels);
+ });
- $this->getConnectedWebSocketConnection(['test-channel']);
- $this->getConnectedWebSocketConnection(['test-channel']);
- $this->expectException(ConnectionsOverCapacity::class);
- $this->getConnectedWebSocketConnection(['test-channel']);
+ $this->channelManager
+ ->getGlobalConnectionsCount('1234')
+ ->then(function ($total) {
+ $this->assertEquals(1, $total);
+ });
+
+ $this->pusherServer->onClose($connection);
+
+ $this->channelManager
+ ->getGlobalConnectionsCount('1234')
+ ->then(function ($total) {
+ $this->assertEquals(0, $total);
+ });
+
+ $this->channelManager
+ ->getGlobalChannels('1234')
+ ->then(function ($channels) {
+ $this->assertCount(0, $channels);
+ });
}
- /** @test */
- public function app_can_not_exceed_maximum_capacity_on_redis_replication()
+ public function test_websocket_exceptions_are_sent()
{
- $this->runOnlyOnRedisReplication();
+ $connection = $this->newActiveConnection(['public-channel']);
- $this->redis->hdel('laravel_database_1234', 'connections');
+ $this->pusherServer->onError($connection, new UnknownAppKey('NonWorkingKey'));
+ $connection->assertSentEvent('pusher:error', [
+ 'data' => [
+ 'message' => 'Could not find app key `NonWorkingKey`.',
+ 'code' => 4001,
+ ],
+ ]);
+ }
+
+ public function test_capacity_limit()
+ {
$this->app['config']->set('websockets.apps.0.capacity', 2);
- $this->getConnectedWebSocketConnection(['test-channel']);
- $this->getConnectedWebSocketConnection(['test-channel']);
+ $this->newActiveConnection(['test-channel']);
+ $this->newActiveConnection(['test-channel']);
- $this->getPublishClient()
- ->assertCalledWithArgsCount(2, 'hincrby', ['laravel_database_1234', 'connections', 1]);
-
- $failedConnection = $this->getConnectedWebSocketConnection(['test-channel']);
+ $failedConnection = $this->newActiveConnection(['test-channel']);
$failedConnection
->assertSentEvent('pusher:error', ['data' => ['message' => 'Over capacity', 'code' => 4100]])
->assertClosed();
}
-
- /** @test */
- public function successful_connections_have_the_app_attached()
- {
- $connection = $this->getWebSocketConnection();
-
- $this->pusherServer->onOpen($connection);
-
- $this->assertInstanceOf(App::class, $connection->app);
- $this->assertSame('1234', $connection->app->id);
- $this->assertSame('TestKey', $connection->app->key);
- $this->assertSame('TestSecret', $connection->app->secret);
- $this->assertSame('Test App', $connection->app->name);
- }
-
- /** @test */
- public function ping_returns_pong()
- {
- $connection = $this->getWebSocketConnection();
-
- $message = new Message(['event' => 'pusher:ping']);
-
- $this->pusherServer->onOpen($connection);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $connection->assertSentEvent('pusher:pong');
- }
-
- /** @test */
- public function origin_validation_should_fail_for_no_origin()
- {
- $this->expectException(OriginNotAllowed::class);
-
- $connection = $this->getWebSocketConnection('TestOrigin');
-
- $this->pusherServer->onOpen($connection);
-
- $connection->assertSentEvent('pusher:connection_established');
- }
-
- /** @test */
- public function origin_validation_should_fail_for_wrong_origin()
- {
- $this->expectException(OriginNotAllowed::class);
-
- $connection = $this->getWebSocketConnection('TestOrigin', ['Origin' => 'https://google.ro']);
-
- $this->pusherServer->onOpen($connection);
-
- $connection->assertSentEvent('pusher:connection_established');
- }
-
- /** @test */
- public function origin_validation_should_pass_for_the_right_origin()
- {
- $connection = $this->getWebSocketConnection('TestOrigin', ['Origin' => 'https://test.origin.com']);
-
- $this->pusherServer->onOpen($connection);
-
- $connection->assertSentEvent('pusher:connection_established');
- }
}
diff --git a/tests/Dashboard/AuthTest.php b/tests/Dashboard/AuthTest.php
index cf73ac5..5522bca 100644
--- a/tests/Dashboard/AuthTest.php
+++ b/tests/Dashboard/AuthTest.php
@@ -1,17 +1,16 @@
getConnectedWebSocketConnection(['test-channel']);
+ $connection = $this->newActiveConnection(['test-channel']);
$this->pusherServer->onOpen($connection);
@@ -26,10 +25,9 @@ class AuthTest extends TestCase
]);
}
- /** @test */
- public function can_authenticate_dashboard_over_private_channel()
+ public function test_can_authenticate_dashboard_over_private_channel()
{
- $connection = $this->getWebSocketConnection();
+ $connection = $this->newConnection();
$this->pusherServer->onOpen($connection);
@@ -61,17 +59,16 @@ class AuthTest extends TestCase
]);
}
- /** @test */
- public function can_authenticate_dashboard_over_presence_channel()
+ public function test_can_authenticate_dashboard_over_presence_channel()
{
- $connection = $this->getWebSocketConnection();
+ $connection = $this->newConnection();
$this->pusherServer->onOpen($connection);
$channelData = [
'user_id' => 1,
'user_info' => [
- 'name' => 'Marcel',
+ 'name' => 'Rick',
],
];
diff --git a/tests/Dashboard/DashboardTest.php b/tests/Dashboard/DashboardTest.php
index 1d6716d..d25d1e0 100644
--- a/tests/Dashboard/DashboardTest.php
+++ b/tests/Dashboard/DashboardTest.php
@@ -1,21 +1,19 @@
get(route('laravel-websockets.dashboard'))
->assertResponseStatus(403);
}
- /** @test */
- public function can_see_dashboard()
+ public function test_can_see_dashboard()
{
$this->actingAs(factory(User::class)->create())
->get(route('laravel-websockets.dashboard'))
diff --git a/tests/Dashboard/RedisStatisticsTest.php b/tests/Dashboard/RedisStatisticsTest.php
deleted file mode 100644
index 52b0148..0000000
--- a/tests/Dashboard/RedisStatisticsTest.php
+++ /dev/null
@@ -1,73 +0,0 @@
-runOnlyOnRedisReplication();
- }
-
- /** @test */
- public function can_get_statistics()
- {
- $connection = $this->getConnectedWebSocketConnection(['channel-1']);
-
- $logger = new RedisStatisticsLogger(
- $this->channelManager,
- $this->statisticsDriver
- );
-
- $logger->webSocketMessage($connection->app->id);
- $logger->apiMessage($connection->app->id);
- $logger->connection($connection->app->id);
- $logger->disconnection($connection->app->id);
-
- $logger->save();
-
- $this->actingAs(factory(User::class)->create())
- ->json('GET', route('laravel-websockets.statistics', ['appId' => '1234']))
- ->assertResponseOk()
- ->seeJsonStructure([
- 'peak_connections' => ['x', 'y'],
- 'websocket_message_count' => ['x', 'y'],
- 'api_message_count' => ['x', 'y'],
- ]);
- }
-
- /** @test */
- public function cant_get_statistics_for_invalid_app_id()
- {
- $connection = $this->getConnectedWebSocketConnection(['channel-1']);
-
- $logger = new RedisStatisticsLogger(
- $this->channelManager,
- $this->statisticsDriver
- );
-
- $logger->webSocketMessage($connection->app->id);
- $logger->apiMessage($connection->app->id);
- $logger->connection($connection->app->id);
- $logger->disconnection($connection->app->id);
-
- $logger->save();
-
- $this->actingAs(factory(User::class)->create())
- ->json('GET', route('laravel-websockets.statistics', ['appId' => 'not_found']))
- ->seeJson([
- 'peak_connections' => ['x' => [], 'y' => []],
- 'websocket_message_count' => ['x' => [], 'y' => []],
- 'api_message_count' => ['x' => [], 'y' => []],
- ]);
- }
-}
diff --git a/tests/Dashboard/SendMessageTest.php b/tests/Dashboard/SendMessageTest.php
index c6d5dd9..eb71a6b 100644
--- a/tests/Dashboard/SendMessageTest.php
+++ b/tests/Dashboard/SendMessageTest.php
@@ -1,69 +1,46 @@
skipOnRedisReplication();
-
- // Because the Pusher server is not active,
- // we expect it to turn out ok: false.
-
$this->actingAs(factory(User::class)->create())
->json('POST', route('laravel-websockets.event'), [
'appId' => '1234',
- 'key' => 'TestKey',
- 'secret' => 'TestSecret',
'channel' => 'test-channel',
'event' => 'some-event',
'data' => json_encode(['data' => 'yes']),
])
->seeJson([
- 'ok' => false,
+ 'ok' => true,
]);
+
+ if (method_exists($this->channelManager, 'getPublishClient')) {
+ $this->channelManager
+ ->getPublishClient()
+ ->assertCalledWithArgs('publish', [
+ $this->channelManager->getRedisKey('1234', 'test-channel'),
+ json_encode([
+ 'channel' => 'test-channel',
+ 'event' => 'some-event',
+ 'data' => ['data' => 'yes'],
+ 'appId' => '1234',
+ 'serverId' => $this->channelManager->getServerId(),
+ ]),
+ ]);
+ }
}
- /** @test */
- public function can_send_message_on_redis_replication()
+ public function test_cant_send_message_for_invalid_app()
{
- $this->skipOnLocalReplication();
-
- // Because the Pusher server is not active,
- // we expect it to turn out ok: false.
- // However, the driver is set to redis,
- // so Redis would take care of this
- // and stream the message to all active servers instead.
-
- $this->actingAs(factory(User::class)->create())
- ->json('POST', route('laravel-websockets.event'), [
- 'appId' => '1234',
- 'key' => 'TestKey',
- 'secret' => 'TestSecret',
- 'channel' => 'test-channel',
- 'event' => 'some-event',
- 'data' => json_encode(['data' => 'yes']),
- ]);
- }
-
- /** @test */
- public function cant_send_message_for_invalid_app()
- {
- $this->skipOnRedisReplication();
-
- // Because the Pusher server is not active,
- // we expect it to turn out ok: false.
-
$this->actingAs(factory(User::class)->create())
->json('POST', route('laravel-websockets.event'), [
'appId' => '9999',
- 'key' => 'TestKey',
- 'secret' => 'TestSecret',
'channel' => 'test-channel',
'event' => 'some-event',
'data' => json_encode(['data' => 'yes']),
diff --git a/tests/Dashboard/StatisticsTest.php b/tests/Dashboard/StatisticsTest.php
index 9de6354..9e62193 100644
--- a/tests/Dashboard/StatisticsTest.php
+++ b/tests/Dashboard/StatisticsTest.php
@@ -1,73 +1,43 @@
newActiveConnection(['public-channel']);
+ $morty = $this->newActiveConnection(['public-channel']);
- $this->runOnlyOnLocalReplication();
- }
+ $this->statisticsCollector->save();
- /** @test */
- public function can_get_statistics()
- {
- $connection = $this->getConnectedWebSocketConnection(['channel-1']);
-
- $logger = new MemoryStatisticsLogger(
- $this->channelManager,
- $this->statisticsDriver
- );
-
- $logger->webSocketMessage($connection->app->id);
- $logger->apiMessage($connection->app->id);
- $logger->connection($connection->app->id);
- $logger->disconnection($connection->app->id);
-
- $logger->save();
-
- $this->actingAs(factory(User::class)->create())
+ $response = $this->actingAs(factory(User::class)->create())
->json('GET', route('laravel-websockets.statistics', ['appId' => '1234']))
->assertResponseOk()
->seeJsonStructure([
'peak_connections' => ['x', 'y'],
- 'websocket_message_count' => ['x', 'y'],
- 'api_message_count' => ['x', 'y'],
+ 'websocket_messages_count' => ['x', 'y'],
+ 'api_messages_count' => ['x', 'y'],
]);
}
- /** @test */
- public function cant_get_statistics_for_invalid_app_id()
+ public function test_cant_get_statistics_for_invalid_app_id()
{
- $connection = $this->getConnectedWebSocketConnection(['channel-1']);
+ $rick = $this->newActiveConnection(['public-channel']);
+ $morty = $this->newActiveConnection(['public-channel']);
- $logger = new MemoryStatisticsLogger(
- $this->channelManager,
- $this->statisticsDriver
- );
-
- $logger->webSocketMessage($connection->app->id);
- $logger->apiMessage($connection->app->id);
- $logger->connection($connection->app->id);
- $logger->disconnection($connection->app->id);
-
- $logger->save();
+ $this->statisticsCollector->save();
$this->actingAs(factory(User::class)->create())
->json('GET', route('laravel-websockets.statistics', ['appId' => 'not_found']))
->seeJson([
'peak_connections' => ['x' => [], 'y' => []],
- 'websocket_message_count' => ['x' => [], 'y' => []],
- 'api_message_count' => ['x' => [], 'y' => []],
+ 'websocket_messages_count' => ['x' => [], 'y' => []],
+ 'api_messages_count' => ['x' => [], 'y' => []],
]);
}
}
diff --git a/tests/HttpApi/FetchChannelTest.php b/tests/FetchChannelTest.php
similarity index 67%
rename from tests/HttpApi/FetchChannelTest.php
rename to tests/FetchChannelTest.php
index e1ca22d..6b274fb 100644
--- a/tests/HttpApi/FetchChannelTest.php
+++ b/tests/FetchChannelTest.php
@@ -1,10 +1,8 @@
expectException(HttpException::class);
$this->expectExceptionMessage('Invalid auth signature provided.');
- $connection = new Connection();
+ $connection = new Mocks\Connection;
$requestPath = '/apps/1234/channel/my-channel';
+
$routeParams = [
'appId' => '1234',
'channelName' => 'my-channel',
];
- $queryString = Pusher::build_auth_query_string('TestKey', 'InvalidSecret', 'GET', $requestPath);
+ $queryString = Pusher::build_auth_query_string(
+ 'TestKey', 'InvalidSecret', 'GET', $requestPath
+ );
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
- $controller = app(FetchChannelController::class);
+ $controller = app(FetchChannel::class);
$controller->onOpen($connection, $request);
}
- /** @test */
- public function it_returns_the_channel_information()
+ public function test_it_returns_the_channel_information()
{
- $this->getConnectedWebSocketConnection(['my-channel']);
- $this->getConnectedWebSocketConnection(['my-channel']);
+ $this->newActiveConnection(['my-channel']);
+ $this->newActiveConnection(['my-channel']);
- $connection = new Connection();
+ $connection = new Mocks\Connection;
$requestPath = '/apps/1234/channel/my-channel';
$routeParams = [
@@ -53,7 +52,7 @@ class FetchChannelTest extends TestCase
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
- $controller = app(FetchChannelController::class);
+ $controller = app(FetchChannel::class);
$controller->onOpen($connection, $request);
@@ -66,17 +65,15 @@ class FetchChannelTest extends TestCase
], json_decode($response->getContent(), true));
}
- /** @test */
- public function it_returns_presence_channel_information()
+ public function test_it_returns_presence_channel_information()
{
- $this->runOnlyOnLocalReplication();
+ $this->newPresenceConnection('presence-channel');
+ $this->newPresenceConnection('presence-channel');
- $this->joinPresenceChannel('presence-channel');
- $this->joinPresenceChannel('presence-channel');
-
- $connection = new Connection();
+ $connection = new Mocks\Connection;
$requestPath = '/apps/1234/channel/my-channel';
+
$routeParams = [
'appId' => '1234',
'channelName' => 'presence-channel',
@@ -86,7 +83,7 @@ class FetchChannelTest extends TestCase
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
- $controller = app(FetchChannelController::class);
+ $controller = app(FetchChannel::class);
$controller->onOpen($connection, $request);
@@ -100,17 +97,17 @@ class FetchChannelTest extends TestCase
], json_decode($response->getContent(), true));
}
- /** @test */
- public function it_returns_404_for_invalid_channels()
+ public function test_it_returns_404_for_invalid_channels()
{
$this->expectException(HttpException::class);
$this->expectExceptionMessage('Unknown channel');
- $this->getConnectedWebSocketConnection(['my-channel']);
+ $this->newActiveConnection(['my-channel']);
- $connection = new Connection();
+ $connection = new Mocks\Connection;
$requestPath = '/apps/1234/channel/invalid-channel';
+
$routeParams = [
'appId' => '1234',
'channelName' => 'invalid-channel',
@@ -120,7 +117,7 @@ class FetchChannelTest extends TestCase
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
- $controller = app(FetchChannelController::class);
+ $controller = app(FetchChannel::class);
$controller->onOpen($connection, $request);
diff --git a/tests/HttpApi/FetchChannelsTest.php b/tests/FetchChannelsTest.php
similarity index 64%
rename from tests/HttpApi/FetchChannelsTest.php
rename to tests/FetchChannelsTest.php
index 05e7fe5..9b0549c 100644
--- a/tests/HttpApi/FetchChannelsTest.php
+++ b/tests/FetchChannelsTest.php
@@ -1,10 +1,8 @@
expectException(HttpException::class);
$this->expectExceptionMessage('Invalid auth signature provided.');
- $connection = new Connection();
+ $connection = new Mocks\Connection;
$requestPath = '/apps/1234/channels';
+
$routeParams = [
'appId' => '1234',
];
- $queryString = Pusher::build_auth_query_string('TestKey', 'InvalidSecret', 'GET', $requestPath);
+ $queryString = Pusher::build_auth_query_string(
+ 'TestKey', 'InvalidSecret', 'GET', $requestPath
+ );
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
- $controller = app(FetchChannelsController::class);
+ $controller = app(FetchChannels::class);
$controller->onOpen($connection, $request);
}
- /** @test */
- public function it_returns_the_channel_information()
+ public function test_it_returns_the_channel_information()
{
- $this->skipOnRedisReplication();
+ $this->newPresenceConnection('presence-channel');
- $this->joinPresenceChannel('presence-channel');
-
- $connection = new Connection();
+ $connection = new Mocks\Connection;
$requestPath = '/apps/1234/channels';
+
$routeParams = [
'appId' => '1234',
];
- $queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
+ $queryString = Pusher::build_auth_query_string(
+ 'TestKey', 'TestSecret', 'GET', $requestPath
+ );
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
- $controller = app(FetchChannelsController::class);
+ $controller = app(FetchChannels::class);
$controller->onOpen($connection, $request);
@@ -66,19 +66,17 @@ class FetchChannelsTest extends TestCase
], json_decode($response->getContent(), true));
}
- /** @test */
- public function it_returns_the_channel_information_for_prefix()
+ public function test_it_returns_the_channel_information_for_prefix()
{
- $this->skipOnRedisReplication();
+ $this->newPresenceConnection('presence-global.1');
+ $this->newPresenceConnection('presence-global.1');
+ $this->newPresenceConnection('presence-global.2');
+ $this->newPresenceConnection('presence-notglobal.2');
- $this->joinPresenceChannel('presence-global.1');
- $this->joinPresenceChannel('presence-global.1');
- $this->joinPresenceChannel('presence-global.2');
- $this->joinPresenceChannel('presence-notglobal.2');
-
- $connection = new Connection();
+ $connection = new Mocks\Connection;
$requestPath = '/apps/1234/channels';
+
$routeParams = [
'appId' => '1234',
];
@@ -89,7 +87,7 @@ class FetchChannelsTest extends TestCase
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
- $controller = app(FetchChannelsController::class);
+ $controller = app(FetchChannels::class);
$controller->onOpen($connection, $request);
@@ -104,19 +102,17 @@ class FetchChannelsTest extends TestCase
], json_decode($response->getContent(), true));
}
- /** @test */
- public function it_returns_the_channel_information_for_prefix_with_user_count()
+ public function test_it_returns_the_channel_information_for_prefix_with_user_count()
{
- $this->skipOnRedisReplication();
+ $this->newPresenceConnection('presence-global.1');
+ $this->newPresenceConnection('presence-global.1');
+ $this->newPresenceConnection('presence-global.2');
+ $this->newPresenceConnection('presence-notglobal.2');
- $this->joinPresenceChannel('presence-global.1');
- $this->joinPresenceChannel('presence-global.1');
- $this->joinPresenceChannel('presence-global.2');
- $this->joinPresenceChannel('presence-notglobal.2');
-
- $connection = new Connection();
+ $connection = new Mocks\Connection;
$requestPath = '/apps/1234/channels';
+
$routeParams = [
'appId' => '1234',
];
@@ -128,7 +124,7 @@ class FetchChannelsTest extends TestCase
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
- $controller = app(FetchChannelsController::class);
+ $controller = app(FetchChannels::class);
$controller->onOpen($connection, $request);
@@ -147,15 +143,15 @@ class FetchChannelsTest extends TestCase
], json_decode($response->getContent(), true));
}
- /** @test */
- public function can_not_get_non_presence_channel_user_count()
+ public function test_can_not_get_non_presence_channel_user_count()
{
$this->expectException(HttpException::class);
$this->expectExceptionMessage('Request must be limited to presence channels in order to fetch user_count');
- $connection = new Connection();
+ $connection = new Mocks\Connection;
$requestPath = '/apps/1234/channels';
+
$routeParams = [
'appId' => '1234',
];
@@ -166,7 +162,7 @@ class FetchChannelsTest extends TestCase
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
- $controller = app(FetchChannelsController::class);
+ $controller = app(FetchChannels::class);
$controller->onOpen($connection, $request);
@@ -174,14 +170,12 @@ class FetchChannelsTest extends TestCase
$response = array_pop($connection->sentRawData);
}
- /** @test */
- public function it_returns_empty_object_for_no_channels_found()
+ public function test_it_returns_empty_object_for_no_channels_found()
{
- $this->skipOnRedisReplication();
-
- $connection = new Connection();
+ $connection = new Mocks\Connection;
$requestPath = '/apps/1234/channels';
+
$routeParams = [
'appId' => '1234',
];
@@ -190,7 +184,7 @@ class FetchChannelsTest extends TestCase
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
- $controller = app(FetchChannelsController::class);
+ $controller = app(FetchChannels::class);
$controller->onOpen($connection, $request);
diff --git a/tests/HttpApi/FetchUsersTest.php b/tests/FetchUsersTest.php
similarity index 57%
rename from tests/HttpApi/FetchUsersTest.php
rename to tests/FetchUsersTest.php
index f68af14..bda1e20 100644
--- a/tests/HttpApi/FetchUsersTest.php
+++ b/tests/FetchUsersTest.php
@@ -1,99 +1,101 @@
expectException(HttpException::class);
$this->expectExceptionMessage('Invalid auth signature provided.');
- $connection = new Connection();
+ $connection = new Mocks\Connection;
$requestPath = '/apps/1234/channel/my-channel';
+
$routeParams = [
'appId' => '1234',
'channelName' => 'my-channel',
];
- $queryString = Pusher::build_auth_query_string('TestKey', 'InvalidSecret', 'GET', $requestPath);
+ $queryString = Pusher::build_auth_query_string(
+ 'TestKey', 'InvalidSecret', 'GET', $requestPath
+ );
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
- $controller = app(FetchUsersController::class);
+ $controller = app(FetchUsers::class);
$controller->onOpen($connection, $request);
}
- /** @test */
- public function it_only_returns_data_for_presence_channels()
+ public function test_it_only_returns_data_for_presence_channels()
{
$this->expectException(HttpException::class);
$this->expectExceptionMessage('Invalid presence channel');
- $this->getConnectedWebSocketConnection(['my-channel']);
+ $this->newActiveConnection(['my-channel']);
- $connection = new Connection();
+ $connection = new Mocks\Connection;
$requestPath = '/apps/1234/channel/my-channel/users';
+
$routeParams = [
'appId' => '1234',
'channelName' => 'my-channel',
];
- $queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
+ $queryString = Pusher::build_auth_query_string(
+ 'TestKey', 'TestSecret', 'GET', $requestPath
+ );
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
- $controller = app(FetchUsersController::class);
+ $controller = app(FetchUsers::class);
$controller->onOpen($connection, $request);
}
- /** @test */
- public function it_returns_404_for_invalid_channels()
+ public function test_it_returns_404_for_invalid_channels()
{
$this->expectException(HttpException::class);
- $this->expectExceptionMessage('Unknown channel');
+ $this->expectExceptionMessage('Invalid presence channel');
- $this->getConnectedWebSocketConnection(['my-channel']);
+ $this->newActiveConnection(['my-channel']);
- $connection = new Connection();
+ $connection = new Mocks\Connection;
$requestPath = '/apps/1234/channel/invalid-channel/users';
+
$routeParams = [
'appId' => '1234',
'channelName' => 'invalid-channel',
];
- $queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
+ $queryString = Pusher::build_auth_query_string(
+ 'TestKey', 'TestSecret', 'GET', $requestPath
+ );
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
- $controller = app(FetchUsersController::class);
+ $controller = app(FetchUsers::class);
$controller->onOpen($connection, $request);
}
- /** @test */
- public function it_returns_connected_user_information()
+ public function test_it_returns_connected_user_information()
{
- $this->skipOnRedisReplication();
+ $this->newPresenceConnection('presence-channel');
- $this->joinPresenceChannel('presence-channel');
-
- $connection = new Connection();
+ $connection = new Mocks\Connection;
$requestPath = '/apps/1234/channel/presence-channel/users';
+
$routeParams = [
'appId' => '1234',
'channelName' => 'presence-channel',
@@ -103,7 +105,7 @@ class FetchUsersTest extends TestCase
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
- $controller = app(FetchUsersController::class);
+ $controller = app(FetchUsers::class);
$controller->onOpen($connection, $request);
@@ -111,11 +113,7 @@ class FetchUsersTest extends TestCase
$response = array_pop($connection->sentRawData);
$this->assertSame([
- 'users' => [
- [
- 'id' => 1,
- ],
- ],
+ 'users' => [['id' => 1]],
], json_decode($response->getContent(), true));
}
}
diff --git a/tests/HttpApi/FetchChannelReplicationTest.php b/tests/HttpApi/FetchChannelReplicationTest.php
deleted file mode 100644
index 3d36f91..0000000
--- a/tests/HttpApi/FetchChannelReplicationTest.php
+++ /dev/null
@@ -1,153 +0,0 @@
-runOnlyOnRedisReplication();
- }
-
- /** @test */
- public function replication_invalid_signatures_can_not_access_the_api()
- {
- $this->expectException(HttpException::class);
- $this->expectExceptionMessage('Invalid auth signature provided.');
-
- $connection = new Connection();
-
- $requestPath = '/apps/1234/channel/my-channel';
- $routeParams = [
- 'appId' => '1234',
- 'channelName' => 'my-channel',
- ];
-
- $queryString = Pusher::build_auth_query_string('TestKey', 'InvalidSecret', 'GET', $requestPath);
-
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
-
- $controller = app(FetchChannelController::class);
-
- $controller->onOpen($connection, $request);
- }
-
- /** @test */
- public function replication_it_returns_the_channel_information()
- {
- $this->getConnectedWebSocketConnection(['my-channel']);
- $this->getConnectedWebSocketConnection(['my-channel']);
-
- $connection = new Connection();
-
- $requestPath = '/apps/1234/channel/my-channel';
- $routeParams = [
- 'appId' => '1234',
- 'channelName' => 'my-channel',
- ];
-
- $queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
-
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
-
- $controller = app(FetchChannelController::class);
-
- $controller->onOpen($connection, $request);
-
- /** @var JsonResponse $response */
- $response = array_pop($connection->sentRawData);
-
- $this->assertSame([
- 'occupied' => true,
- 'subscription_count' => 2,
- ], json_decode($response->getContent(), true));
- }
-
- /** @test */
- public function replication_it_returns_presence_channel_information()
- {
- $this->skipOnRedisReplication();
-
- $this->joinPresenceChannel('presence-channel');
- $this->joinPresenceChannel('presence-channel');
-
- $connection = new Connection();
-
- $requestPath = '/apps/1234/channel/my-channel';
- $routeParams = [
- 'appId' => '1234',
- 'channelName' => 'presence-channel',
- ];
-
- $queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
-
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
-
- $controller = app(FetchChannelController::class);
-
- $controller->onOpen($connection, $request);
-
- /** @var JsonResponse $response */
- $response = array_pop($connection->sentRawData);
-
- $this->getSubscribeClient()
- ->assertEventDispatched('message');
-
- $this->getPublishClient()
- ->assertCalled('hset')
- ->assertCalled('hgetall')
- ->assertCalled('publish');
-
- $this->assertSame([
- 'occupied' => true,
- 'subscription_count' => 2,
- 'user_count' => 2,
- ], json_decode($response->getContent(), true));
- }
-
- /** @test */
- public function replication_it_returns_404_for_invalid_channels()
- {
- $this->expectException(HttpException::class);
- $this->expectExceptionMessage('Unknown channel');
-
- $this->getConnectedWebSocketConnection(['my-channel']);
-
- $connection = new Connection();
-
- $requestPath = '/apps/1234/channel/invalid-channel';
- $routeParams = [
- 'appId' => '1234',
- 'channelName' => 'invalid-channel',
- ];
-
- $queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
-
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
-
- $controller = app(FetchChannelController::class);
-
- $controller->onOpen($connection, $request);
-
- /** @var JsonResponse $response */
- $response = array_pop($connection->sentRawData);
-
- $this->assertSame([
- 'occupied' => true,
- 'subscription_count' => 2,
- ], json_decode($response->getContent(), true));
- }
-}
diff --git a/tests/HttpApi/FetchChannelsReplicationTest.php b/tests/HttpApi/FetchChannelsReplicationTest.php
deleted file mode 100644
index 8c691c3..0000000
--- a/tests/HttpApi/FetchChannelsReplicationTest.php
+++ /dev/null
@@ -1,180 +0,0 @@
-runOnlyOnRedisReplication();
- }
-
- /** @test */
- public function replication_it_returns_the_channel_information()
- {
- $this->joinPresenceChannel('presence-channel');
-
- $connection = new Connection();
-
- $requestPath = '/apps/1234/channels';
- $routeParams = [
- 'appId' => '1234',
- ];
-
- $queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
-
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
-
- $controller = app(FetchChannelsController::class);
-
- $controller->onOpen($connection, $request);
-
- /** @var JsonResponse $response */
- $response = array_pop($connection->sentRawData);
-
- $this->getSubscribeClient()
- ->assertEventDispatched('message');
-
- $this->getPublishClient()
- ->assertCalled('hset')
- ->assertCalledWithArgs('hgetall', ['laravel_database_1234:presence-channel'])
- ->assertCalled('publish')
- ->assertCalled('multi')
- ->assertCalledWithArgs('hlen', ['laravel_database_1234:presence-channel'])
- ->assertCalled('exec');
- }
-
- /** @test */
- public function replication_it_returns_the_channel_information_for_prefix()
- {
- $this->joinPresenceChannel('presence-global.1');
- $this->joinPresenceChannel('presence-global.1');
- $this->joinPresenceChannel('presence-global.2');
- $this->joinPresenceChannel('presence-notglobal.2');
-
- $connection = new Connection();
-
- $requestPath = '/apps/1234/channels';
- $routeParams = [
- 'appId' => '1234',
- ];
-
- $queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath, [
- 'filter_by_prefix' => 'presence-global',
- ]);
-
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
-
- $controller = app(FetchChannelsController::class);
-
- $controller->onOpen($connection, $request);
-
- /** @var JsonResponse $response */
- $response = array_pop($connection->sentRawData);
-
- $this->getSubscribeClient()
- ->assertEventDispatched('message');
-
- $this->getPublishClient()
- ->assertCalled('hset')
- ->assertCalledWithArgs('hgetall', ['laravel_database_1234:presence-global.1'])
- ->assertCalledWithArgs('hgetall', ['laravel_database_1234:presence-global.2'])
- ->assertCalledWithArgs('hgetall', ['laravel_database_1234:presence-notglobal.2'])
- ->assertCalled('publish')
- ->assertCalled('multi')
- ->assertCalledWithArgs('hlen', ['laravel_database_1234:presence-global.1'])
- ->assertCalledWithArgs('hlen', ['laravel_database_1234:presence-global.2'])
- ->assertNotCalledWithArgs('hlen', ['laravel_database_1234:presence-notglobal.2'])
- ->assertCalled('exec');
- }
-
- /** @test */
- public function replication_it_returns_the_channel_information_for_prefix_with_user_count()
- {
- $this->joinPresenceChannel('presence-global.1');
- $this->joinPresenceChannel('presence-global.1');
- $this->joinPresenceChannel('presence-global.2');
- $this->joinPresenceChannel('presence-notglobal.2');
-
- $connection = new Connection();
-
- $requestPath = '/apps/1234/channels';
- $routeParams = [
- 'appId' => '1234',
- ];
-
- $queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath, [
- 'filter_by_prefix' => 'presence-global',
- 'info' => 'user_count',
- ]);
-
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
-
- $controller = app(FetchChannelsController::class);
-
- $controller->onOpen($connection, $request);
-
- /** @var JsonResponse $response */
- $response = array_pop($connection->sentRawData);
-
- $this->getSubscribeClient()
- ->assertEventDispatched('message');
-
- $this->getPublishClient()
- ->assertCalled('hset')
- ->assertCalledWithArgs('hgetall', ['laravel_database_1234:presence-global.1'])
- ->assertCalledWithArgs('hgetall', ['laravel_database_1234:presence-global.2'])
- ->assertCalledWithArgs('hgetall', ['laravel_database_1234:presence-notglobal.2'])
- ->assertCalled('publish')
- ->assertCalled('multi')
- ->assertCalledWithArgs('hlen', ['laravel_database_1234:presence-global.1'])
- ->assertCalledWithArgs('hlen', ['laravel_database_1234:presence-global.2'])
- ->assertNotCalledWithArgs('hlen', ['laravel_database_1234:presence-notglobal.2'])
- ->assertCalled('exec');
- }
-
- /** @test */
- public function replication_it_returns_empty_object_for_no_channels_found()
- {
- $connection = new Connection();
-
- $requestPath = '/apps/1234/channels';
- $routeParams = [
- 'appId' => '1234',
- ];
-
- $queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
-
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
-
- $controller = app(FetchChannelsController::class);
-
- $controller->onOpen($connection, $request);
-
- /** @var JsonResponse $response */
- $response = array_pop($connection->sentRawData);
-
- $this->getSubscribeClient()
- ->assertEventDispatched('message');
-
- $this->getPublishClient()
- ->assertNotCalled('hset')
- ->assertNotCalled('hgetall')
- ->assertNotCalled('publish')
- ->assertCalled('multi')
- ->assertNotCalled('hlen')
- ->assertCalled('exec');
- }
-}
diff --git a/tests/HttpApi/FetchUsersReplicationTest.php b/tests/HttpApi/FetchUsersReplicationTest.php
deleted file mode 100644
index 9fa7a96..0000000
--- a/tests/HttpApi/FetchUsersReplicationTest.php
+++ /dev/null
@@ -1,131 +0,0 @@
-runOnlyOnRedisReplication();
- }
-
- /** @test */
- public function invalid_signatures_can_not_access_the_api()
- {
- $this->expectException(HttpException::class);
- $this->expectExceptionMessage('Invalid auth signature provided.');
-
- $connection = new Connection();
-
- $requestPath = '/apps/1234/channel/my-channel';
- $routeParams = [
- 'appId' => '1234',
- 'channelName' => 'my-channel',
- ];
-
- $queryString = Pusher::build_auth_query_string('TestKey', 'InvalidSecret', 'GET', $requestPath);
-
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
-
- $controller = app(FetchUsersController::class);
-
- $controller->onOpen($connection, $request);
- }
-
- /** @test */
- public function it_only_returns_data_for_presence_channels()
- {
- $this->expectException(HttpException::class);
- $this->expectExceptionMessage('Invalid presence channel');
-
- $this->getConnectedWebSocketConnection(['my-channel']);
-
- $connection = new Connection();
-
- $requestPath = '/apps/1234/channel/my-channel/users';
- $routeParams = [
- 'appId' => '1234',
- 'channelName' => 'my-channel',
- ];
-
- $queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
-
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
-
- $controller = app(FetchUsersController::class);
-
- $controller->onOpen($connection, $request);
- }
-
- /** @test */
- public function it_returns_404_for_invalid_channels()
- {
- $this->expectException(HttpException::class);
- $this->expectExceptionMessage('Unknown channel');
-
- $this->getConnectedWebSocketConnection(['my-channel']);
-
- $connection = new Connection();
-
- $requestPath = '/apps/1234/channel/invalid-channel/users';
- $routeParams = [
- 'appId' => '1234',
- 'channelName' => 'invalid-channel',
- ];
-
- $queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
-
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
-
- $controller = app(FetchUsersController::class);
-
- $controller->onOpen($connection, $request);
- }
-
- /** @test */
- public function it_returns_connected_user_information()
- {
- $this->skipOnRedisReplication();
-
- $this->joinPresenceChannel('presence-channel');
-
- $connection = new Connection();
-
- $requestPath = '/apps/1234/channel/presence-channel/users';
- $routeParams = [
- 'appId' => '1234',
- 'channelName' => 'presence-channel',
- ];
-
- $queryString = Pusher::build_auth_query_string('TestKey', 'TestSecret', 'GET', $requestPath);
-
- $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
-
- $controller = app(FetchUsersController::class);
-
- $controller->onOpen($connection, $request);
-
- /** @var \Illuminate\Http\JsonResponse $response */
- $response = array_pop($connection->sentRawData);
-
- $this->assertSame([
- 'users' => [
- [
- 'id' => 1,
- ],
- ],
- ], json_decode($response->getContent(), true));
- }
-}
diff --git a/tests/Messages/PusherClientMessageTest.php b/tests/Messages/PusherClientMessageTest.php
deleted file mode 100644
index fed8e98..0000000
--- a/tests/Messages/PusherClientMessageTest.php
+++ /dev/null
@@ -1,63 +0,0 @@
-getConnectedWebSocketConnection(['test-channel']);
-
- $message = new Message([
- 'event' => 'client-test',
- 'channel' => 'test-channel',
- 'data' => [
- 'client-event' => 'test',
- ],
- ]);
-
- $this->pusherServer->onMessage($connection, $message);
-
- $connection->assertNotSentEvent('client-test');
- }
-
- /** @test */
- public function client_messages_get_broadcasted_when_enabled()
- {
- $this->app['config']->set('websockets.apps', [
- [
- 'name' => 'Test App',
- 'id' => 1234,
- 'key' => 'TestKey',
- 'secret' => 'TestSecret',
- 'enable_client_messages' => true,
- 'enable_statistics' => true,
- ],
- ]);
-
- $connection1 = $this->getConnectedWebSocketConnection(['test-channel']);
- $connection2 = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $message = new Message([
- 'event' => 'client-test',
- 'channel' => 'test-channel',
- 'data' => [
- 'client-event' => 'test',
- ],
- ]);
-
- $this->pusherServer->onMessage($connection1, $message);
-
- $connection1->assertNotSentEvent('client-test');
-
- $connection2->assertSentEvent('client-test', [
- 'data' => [
- 'client-event' => 'test',
- ],
- ]);
- }
-}
diff --git a/tests/Mocks/Connection.php b/tests/Mocks/Connection.php
index f7fb5b4..8de4a7b 100644
--- a/tests/Mocks/Connection.php
+++ b/tests/Mocks/Connection.php
@@ -1,6 +1,6 @@
statistics as $appId => $statistic) {
- $currentConnectionCount = $this->channelManager->getGlobalConnectionsCount($appId);
-
- $statistic->reset($currentConnectionCount);
- }
- }
-
- /**
- * Get app by id.
- *
- * @param mixed $appId
- * @return array
- */
- public function getForAppId($appId): array
- {
- $statistic = $this->findOrMakeStatisticForAppId($appId);
-
- return $statistic->toArray();
- }
-}
diff --git a/tests/Mocks/FakeRedisStatisticsLogger.php b/tests/Mocks/FakeRedisStatisticsLogger.php
deleted file mode 100644
index 8fae00d..0000000
--- a/tests/Mocks/FakeRedisStatisticsLogger.php
+++ /dev/null
@@ -1,24 +0,0 @@
- $appId,
- 'peak_connection_count' => $this->redis->hget($this->getHash($appId), 'peak_connection_count') ?: 0,
- 'websocket_message_count' => $this->redis->hget($this->getHash($appId), 'websocket_message_count') ?: 0,
- 'api_message_count' => $this->redis->hget($this->getHash($appId), 'api_message_count') ?: 0,
- ];
- }
-}
diff --git a/tests/Mocks/LazyClient.php b/tests/Mocks/LazyClient.php
index 0382a6f..abd07ce 100644
--- a/tests/Mocks/LazyClient.php
+++ b/tests/Mocks/LazyClient.php
@@ -1,6 +1,6 @@
promise, $this->loop
);
- $onFulfilled($result);
+ $result = call_user_func($onFulfilled, $result);
- return $this->promise;
+ return $result instanceof PromiseInterface
+ ? $result
+ : new FulfilledPromise($result);
}
/**
diff --git a/tests/Mocks/RedisFactory.php b/tests/Mocks/RedisFactory.php
index da28b08..f897f4b 100644
--- a/tests/Mocks/RedisFactory.php
+++ b/tests/Mocks/RedisFactory.php
@@ -1,6 +1,6 @@
newActiveConnection(['public-channel']);
+
+ $message = new Mocks\Message(['event' => 'pusher:ping']);
+
+ $this->pusherServer->onMessage($connection, $message);
+
+ $connection->assertSentEvent('pusher:pong');
+ }
+}
diff --git a/tests/PresenceChannelTest.php b/tests/PresenceChannelTest.php
new file mode 100644
index 0000000..b7d0b8a
--- /dev/null
+++ b/tests/PresenceChannelTest.php
@@ -0,0 +1,188 @@
+expectException(InvalidSignature::class);
+
+ $connection = $this->newConnection();
+
+ $message = new Mocks\Message([
+ 'event' => 'pusher:subscribe',
+ 'data' => [
+ 'auth' => 'invalid',
+ 'channel' => 'presence-channel',
+ ],
+ ]);
+
+ $this->pusherServer->onOpen($connection);
+ $this->pusherServer->onMessage($connection, $message);
+ }
+
+ public function test_connect_to_presence_channel_with_valid_signature()
+ {
+ $connection = $this->newConnection();
+
+ $this->pusherServer->onOpen($connection);
+
+ $user = [
+ 'user_id' => 1,
+ 'user_info' => [
+ 'name' => 'Rick',
+ ],
+ ];
+
+ $encodedUser = json_encode($user);
+
+ $signature = "{$connection->socketId}:presence-channel:".$encodedUser;
+ $hashedAppSecret = hash_hmac('sha256', $signature, $connection->app->secret);
+
+ $message = new Mocks\Message([
+ 'event' => 'pusher:subscribe',
+ 'data' => [
+ 'auth' => "{$connection->app->key}:{$hashedAppSecret}",
+ 'channel' => 'presence-channel',
+ 'channel_data' => json_encode($user),
+ ],
+ ]);
+
+ $this->pusherServer->onMessage($connection, $message);
+
+ $connection->assertSentEvent('pusher_internal:subscription_succeeded', [
+ 'channel' => 'presence-channel',
+ ]);
+
+ $this->channelManager
+ ->getGlobalConnectionsCount('1234', 'presence-channel')
+ ->then(function ($total) {
+ $this->assertEquals(1, $total);
+ });
+ }
+
+ public function test_presence_channel_broadcast_member_events()
+ {
+ $rick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]);
+ $morty = $this->newPresenceConnection('presence-channel', ['user_id' => 2]);
+
+ $rick->assertSentEvent('pusher_internal:member_added', [
+ 'channel' => 'presence-channel',
+ 'data' => json_encode(['user_id' => 2]),
+ ]);
+
+ $this->channelManager
+ ->getChannelMembers('1234', 'presence-channel')
+ ->then(function ($members) {
+ $this->assertCount(2, $members);
+ });
+
+ $this->pusherServer->onClose($morty);
+
+ $rick->assertSentEvent('pusher_internal:member_removed', [
+ 'channel' => 'presence-channel',
+ 'data' => json_encode(['user_id' => 2]),
+ ]);
+
+ $this->channelManager
+ ->getGlobalConnectionsCount('1234', 'presence-channel')
+ ->then(function ($total) {
+ $this->assertEquals(1, $total);
+ });
+
+ $this->channelManager
+ ->getChannelMembers('1234', 'presence-channel')
+ ->then(function ($members) {
+ $this->assertCount(1, $members);
+ $this->assertEquals(1, $members[0]->user_id);
+ });
+ }
+
+ public function test_unsubscribe_from_presence_channel()
+ {
+ $connection = $this->newPresenceConnection('presence-channel', ['user_id' => 1]);
+
+ $this->channelManager
+ ->getGlobalConnectionsCount('1234', 'presence-channel')
+ ->then(function ($total) {
+ $this->assertEquals(1, $total);
+ });
+
+ $message = new Mocks\Message([
+ 'event' => 'pusher:unsubscribe',
+ 'data' => [
+ 'channel' => 'presence-channel',
+ ],
+ ]);
+
+ $this->pusherServer->onMessage($connection, $message);
+
+ $this->channelManager
+ ->getGlobalConnectionsCount('1234', 'presence-channel')
+ ->then(function ($total) {
+ $this->assertEquals(0, $total);
+ });
+ }
+
+ public function test_can_whisper_to_private_channel()
+ {
+ $this->app['config']->set('websockets.apps.0.enable_client_messages', true);
+
+ $rick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]);
+ $morty = $this->newPresenceConnection('presence-channel', ['user_id' => 2]);
+
+ $message = new Mocks\Message([
+ 'event' => 'client-test-whisper',
+ 'data' => [],
+ 'channel' => 'presence-channel',
+ ]);
+
+ $this->pusherServer->onMessage($rick, $message);
+
+ $rick->assertNotSentEvent('client-test-whisper');
+ $morty->assertSentEvent('client-test-whisper', ['data' => [], 'channel' => 'presence-channel']);
+ }
+
+ public function test_cannot_whisper_to_public_channel_if_having_whispering_disabled()
+ {
+ $rick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]);
+ $morty = $this->newPresenceConnection('presence-channel', ['user_id' => 2]);
+
+ $message = new Mocks\Message([
+ 'event' => 'client-test-whisper',
+ 'data' => [],
+ 'channel' => 'presence-channel',
+ ]);
+
+ $this->pusherServer->onMessage($rick, $message);
+
+ $rick->assertNotSentEvent('client-test-whisper');
+ $morty->assertNotSentEvent('client-test-whisper');
+ }
+
+ public function test_statistics_get_collected_for_presenece_channels()
+ {
+ $rick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]);
+ $morty = $this->newPresenceConnection('presence-channel', ['user_id' => 2]);
+
+ $this->statisticsCollector
+ ->getStatistics()
+ ->then(function ($statistics) {
+ $this->assertCount(1, $statistics);
+ });
+
+ $this->statisticsCollector
+ ->getAppStatistics('1234')
+ ->then(function ($statistic) {
+ $this->assertEquals([
+ 'peak_connections_count' => 2,
+ 'websocket_messages_count' => 2,
+ 'api_messages_count' => 0,
+ 'app_id' => '1234',
+ ], $statistic->toArray());
+ });
+ }
+}
diff --git a/tests/PrivateChannelTest.php b/tests/PrivateChannelTest.php
new file mode 100644
index 0000000..bfc4807
--- /dev/null
+++ b/tests/PrivateChannelTest.php
@@ -0,0 +1,141 @@
+expectException(InvalidSignature::class);
+
+ $connection = $this->newConnection();
+
+ $message = new Mocks\Message([
+ 'event' => 'pusher:subscribe',
+ 'data' => [
+ 'auth' => 'invalid',
+ 'channel' => 'private-channel',
+ ],
+ ]);
+
+ $this->pusherServer->onOpen($connection);
+ $this->pusherServer->onMessage($connection, $message);
+ }
+
+ public function test_connect_to_private_channel_with_valid_signature()
+ {
+ $connection = $this->newConnection();
+
+ $this->pusherServer->onOpen($connection);
+
+ $signature = "{$connection->socketId}:private-channel";
+ $hashedAppSecret = hash_hmac('sha256', $signature, $connection->app->secret);
+
+ $message = new Mocks\Message([
+ 'event' => 'pusher:subscribe',
+ 'data' => [
+ 'auth' => "{$connection->app->key}:{$hashedAppSecret}",
+ 'channel' => 'private-channel',
+ ],
+ ]);
+
+ $this->pusherServer->onMessage($connection, $message);
+
+ $connection->assertSentEvent('pusher_internal:subscription_succeeded', [
+ 'channel' => 'private-channel',
+ ]);
+
+ $this->channelManager
+ ->getGlobalConnectionsCount('1234', 'private-channel')
+ ->then(function ($total) {
+ $this->assertEquals(1, $total);
+ });
+ }
+
+ public function test_unsubscribe_from_private_channel()
+ {
+ $connection = $this->newPrivateConnection('private-channel');
+
+ $this->channelManager
+ ->getGlobalConnectionsCount('1234', 'private-channel')
+ ->then(function ($total) {
+ $this->assertEquals(1, $total);
+ });
+
+ $message = new Mocks\Message([
+ 'event' => 'pusher:unsubscribe',
+ 'data' => [
+ 'channel' => 'private-channel',
+ ],
+ ]);
+
+ $this->pusherServer->onMessage($connection, $message);
+
+ $this->channelManager
+ ->getGlobalConnectionsCount('1234', 'private-channel')
+ ->then(function ($total) {
+ $this->assertEquals(0, $total);
+ });
+ }
+
+ public function test_can_whisper_to_private_channel()
+ {
+ $this->app['config']->set('websockets.apps.0.enable_client_messages', true);
+
+ $rick = $this->newPrivateConnection('private-channel');
+ $morty = $this->newPrivateConnection('private-channel');
+
+ $message = new Mocks\Message([
+ 'event' => 'client-test-whisper',
+ 'data' => [],
+ 'channel' => 'private-channel',
+ ]);
+
+ $this->pusherServer->onMessage($rick, $message);
+
+ $rick->assertNotSentEvent('client-test-whisper');
+ $morty->assertSentEvent('client-test-whisper', ['data' => [], 'channel' => 'private-channel']);
+ }
+
+ public function test_cannot_whisper_to_public_channel_if_having_whispering_disabled()
+ {
+ $rick = $this->newPrivateConnection('private-channel');
+ $morty = $this->newPrivateConnection('private-channel');
+
+ $message = new Mocks\Message([
+ 'event' => 'client-test-whisper',
+ 'data' => [],
+ 'channel' => 'private-channel',
+ ]);
+
+ $this->pusherServer->onMessage($rick, $message);
+
+ $rick->assertNotSentEvent('client-test-whisper');
+ $morty->assertNotSentEvent('client-test-whisper');
+ }
+
+ public function test_statistics_get_collected_for_private_channels()
+ {
+ $rick = $this->newPrivateConnection('private-channel');
+ $morty = $this->newPrivateConnection('private-channel');
+
+ $this->statisticsCollector
+ ->getStatistics()
+ ->then(function ($statistics) {
+ $this->assertCount(1, $statistics);
+ });
+
+ $this->statisticsCollector
+ ->getAppStatistics('1234')
+ ->then(function ($statistic) {
+ $this->assertEquals([
+ 'peak_connections_count' => 2,
+ 'websocket_messages_count' => 2,
+ 'api_messages_count' => 0,
+ 'app_id' => '1234',
+ ], $statistic->toArray());
+ });
+ }
+}
diff --git a/tests/PubSub/RedisDriverTest.php b/tests/PubSub/RedisDriverTest.php
deleted file mode 100644
index b018fcc..0000000
--- a/tests/PubSub/RedisDriverTest.php
+++ /dev/null
@@ -1,122 +0,0 @@
-runOnlyOnRedisReplication();
-
- Redis::hdel('laravel_database_1234', 'connections');
- }
-
- /** @test */
- public function redis_listener_responds_properly_on_payload()
- {
- $connection = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $this->pusherServer->onOpen($connection);
-
- $channelData = [
- 'user_id' => 1,
- 'user_info' => [
- 'name' => 'Marcel',
- ],
- ];
-
- $payload = json_encode([
- 'appId' => '1234',
- 'event' => 'test',
- 'data' => $channelData,
- 'socketId' => $connection->socketId,
- ]);
-
- $this->getSubscribeClient()->onMessage('1234:test-channel', $payload);
-
- $this->getSubscribeClient()
- ->assertEventDispatched('message')
- ->assertCalledWithArgs('subscribe', ['laravel_database_1234:test-channel'])
- ->assertCalledWithArgs('onMessage', [
- '1234:test-channel', $payload,
- ]);
- }
-
- /** @test */
- public function redis_listener_responds_properly_on_payload_by_direct_call()
- {
- $connection = $this->getConnectedWebSocketConnection(['test-channel']);
-
- $this->pusherServer->onOpen($connection);
-
- $channelData = [
- 'user_id' => 1,
- 'user_info' => [
- 'name' => 'Marcel',
- ],
- ];
-
- $payload = json_encode([
- 'appId' => '1234',
- 'event' => 'test',
- 'data' => $channelData,
- 'socketId' => $connection->socketId,
- ]);
-
- $client = (new RedisClient)->boot(
- LoopFactory::create(), RedisFactory::class
- );
-
- $client->onMessage('1234:test-channel', $payload);
-
- $client->getSubscribeClient()
- ->assertEventDispatched('message');
- }
-
- /** @test */
- public function redis_tracks_app_connections_count()
- {
- $connection = $this->getWebSocketConnection();
-
- $this->pusherServer->onOpen($connection);
-
- $this->getSubscribeClient()
- ->assertCalledWithArgs('subscribe', ['laravel_database_1234']);
-
- $this->getPublishClient()
- ->assertCalledWithArgs('hincrby', ['laravel_database_1234', 'connections', 1]);
- }
-
- /** @test */
- public function redis_tracks_app_connections_count_on_disconnect()
- {
- $connection = $this->getWebSocketConnection();
-
- $this->pusherServer->onOpen($connection);
-
- $this->getSubscribeClient()
- ->assertCalledWithArgs('subscribe', ['laravel_database_1234'])
- ->assertNotCalledWithArgs('unsubscribe', ['laravel_database_1234']);
-
- $this->getPublishClient()
- ->assertCalledWithArgs('hincrby', ['laravel_database_1234', 'connections', 1]);
-
- $this->pusherServer->onClose($connection);
-
- $this->getPublishClient()
- ->assertCalledWithArgs('hincrby', ['laravel_database_1234', 'connections', -1]);
-
- $this->assertEquals(0, Redis::hget('laravel_database_1234', 'connections'));
- }
-}
diff --git a/tests/PublicChannelTest.php b/tests/PublicChannelTest.php
new file mode 100644
index 0000000..373f2f3
--- /dev/null
+++ b/tests/PublicChannelTest.php
@@ -0,0 +1,117 @@
+newActiveConnection(['public-channel']);
+
+ $this->channelManager
+ ->getGlobalConnectionsCount('1234', 'public-channel')
+ ->then(function ($total) {
+ $this->assertEquals(1, $total);
+ });
+
+ $connection->assertSentEvent(
+ 'pusher:connection_established',
+ [
+ 'data' => json_encode([
+ 'socket_id' => $connection->socketId,
+ 'activity_timeout' => 30,
+ ]),
+ ],
+ );
+
+ $connection->assertSentEvent(
+ 'pusher_internal:subscription_succeeded',
+ ['channel' => 'public-channel']
+ );
+ }
+
+ public function test_unsubscribe_from_public_channel()
+ {
+ $connection = $this->newActiveConnection(['public-channel']);
+
+ $this->channelManager
+ ->getGlobalConnectionsCount('1234', 'public-channel')
+ ->then(function ($total) {
+ $this->assertEquals(1, $total);
+ });
+
+ $message = new Mocks\Message([
+ 'event' => 'pusher:unsubscribe',
+ 'data' => [
+ 'channel' => 'public-channel',
+ ],
+ ]);
+
+ $this->pusherServer->onMessage($connection, $message);
+
+ $this->channelManager
+ ->getGlobalConnectionsCount('1234', 'public-channel')
+ ->then(function ($total) {
+ $this->assertEquals(0, $total);
+ });
+ }
+
+ public function test_can_whisper_to_public_channel()
+ {
+ $this->app['config']->set('websockets.apps.0.enable_client_messages', true);
+
+ $rick = $this->newActiveConnection(['public-channel']);
+ $morty = $this->newActiveConnection(['public-channel']);
+
+ $message = new Mocks\Message([
+ 'event' => 'client-test-whisper',
+ 'data' => [],
+ 'channel' => 'public-channel',
+ ]);
+
+ $this->pusherServer->onMessage($rick, $message);
+
+ $rick->assertNotSentEvent('client-test-whisper');
+ $morty->assertSentEvent('client-test-whisper', ['data' => [], 'channel' => 'public-channel']);
+ }
+
+ public function test_cannot_whisper_to_public_channel_if_having_whispering_disabled()
+ {
+ $rick = $this->newActiveConnection(['public-channel']);
+ $morty = $this->newActiveConnection(['public-channel']);
+
+ $message = new Mocks\Message([
+ 'event' => 'client-test-whisper',
+ 'data' => [],
+ 'channel' => 'public-channel',
+ ]);
+
+ $this->pusherServer->onMessage($rick, $message);
+
+ $rick->assertNotSentEvent('client-test-whisper');
+ $morty->assertNotSentEvent('client-test-whisper');
+ }
+
+ public function test_statistics_get_collected_for_public_channels()
+ {
+ $rick = $this->newActiveConnection(['public-channel']);
+ $morty = $this->newActiveConnection(['public-channel']);
+
+ $this->statisticsCollector
+ ->getStatistics()
+ ->then(function ($statistics) {
+ $this->assertCount(1, $statistics);
+ });
+
+ $this->statisticsCollector
+ ->getAppStatistics('1234')
+ ->then(function ($statistic) {
+ $this->assertEquals([
+ 'peak_connections_count' => 2,
+ 'websocket_messages_count' => 2,
+ 'api_messages_count' => 0,
+ 'app_id' => '1234',
+ ], $statistic->toArray());
+ });
+ }
+}
diff --git a/tests/ReplicationTest.php b/tests/ReplicationTest.php
new file mode 100644
index 0000000..00ee615
--- /dev/null
+++ b/tests/ReplicationTest.php
@@ -0,0 +1,35 @@
+runOnlyOnRedisReplication();
+
+ $connection = $this->newActiveConnection(['public-channel']);
+
+ $message = [
+ 'appId' => '1234',
+ 'serverId' => 0,
+ 'event' => 'some-event',
+ 'data' => [
+ 'channel' => 'public-channel',
+ 'test' => 'yes',
+ ],
+ ];
+
+ $channel = $this->channelManager->find('1234', 'public-channel');
+
+ $channel->broadcastToEveryoneExcept(
+ (object) $message, null, '1234', true
+ );
+
+ $connection->assertSentEvent('some-event', [
+ 'appId' => '1234',
+ 'serverId' => $this->channelManager->getServerId(),
+ 'data' => ['channel' => 'public-channel', 'test' => 'yes'],
+ ]);
+ }
+}
diff --git a/tests/Statistics/Logger/RedisStatisticsLoggerTest.php b/tests/Statistics/Logger/RedisStatisticsLoggerTest.php
deleted file mode 100644
index 1b70b7f..0000000
--- a/tests/Statistics/Logger/RedisStatisticsLoggerTest.php
+++ /dev/null
@@ -1,102 +0,0 @@
-runOnlyOnRedisReplication();
-
- StatisticsLogger::resetStatistics('1234', 0);
- StatisticsLogger::resetAppTraces('1234');
-
- $this->redis->hdel('laravel_database_1234', 'connections');
-
- $this->getPublishClient()->resetAssertions();
- }
-
- /** @test */
- public function it_counts_connections_on_redis_replication()
- {
- $connections = [];
-
- $connections[] = $this->getConnectedWebSocketConnection(['channel-1']);
- $connections[] = $this->getConnectedWebSocketConnection(['channel-1']);
- $connections[] = $this->getConnectedWebSocketConnection(['channel-1']);
-
- $this->getPublishClient()
- ->assertCalledWithArgsCount(6, 'sadd', ['laravel-websockets:apps', '1234'])
- ->assertCalledWithArgsCount(3, 'hincrby', ['laravel-websockets:app:1234', 'current_connection_count', 1])
- ->assertCalledWithArgsCount(3, 'hincrby', ['laravel-websockets:app:1234', 'websocket_message_count', 1]);
-
- $this->pusherServer->onClose(array_pop($connections));
-
- StatisticsLogger::save();
-
- $this->getPublishClient()
- ->assertCalledWithArgs('hincrby', ['laravel-websockets:app:1234', 'current_connection_count', -1])
- ->assertCalledWithArgs('smembers', ['laravel-websockets:apps']);
- }
-
- /** @test */
- public function it_counts_unique_connections_no_channel_subscriptions_on_redis()
- {
- $connections = [];
-
- $connections[] = $this->getConnectedWebSocketConnection(['channel-1', 'channel-2']);
- $connections[] = $this->getConnectedWebSocketConnection(['channel-1', 'channel-2']);
- $connections[] = $this->getConnectedWebSocketConnection(['channel-1']);
-
- $this->getPublishClient()
- ->assertCalledWithArgsCount(3, 'hincrby', ['laravel-websockets:app:1234', 'current_connection_count', 1])
- ->assertCalledWithArgsCount(5, 'hincrby', ['laravel-websockets:app:1234', 'websocket_message_count', 1]);
-
- $this->pusherServer->onClose(array_pop($connections));
- $this->pusherServer->onClose(array_pop($connections));
-
- StatisticsLogger::save();
-
- $this->getPublishClient()
- ->assertCalledWithArgsCount(2, 'hincrby', ['laravel-websockets:app:1234', 'current_connection_count', -1])
- ->assertCalledWithArgs('smembers', ['laravel-websockets:apps']);
- }
-
- /** @test */
- public function it_counts_connections_with_redis_logger_with_no_data()
- {
- config(['cache.default' => 'redis']);
-
- $logger = new RedisStatisticsLogger(
- $this->channelManager,
- $this->statisticsDriver
- );
-
- $logger->resetAppTraces('1');
- $logger->resetAppTraces('1234');
-
- $connection = $this->getConnectedWebSocketConnection(['channel-1']);
-
- $logger->apiMessage($connection->app->id);
-
- $logger->save();
-
- $this->assertCount(1, WebSocketsStatisticsEntry::all());
-
- $entry = WebSocketsStatisticsEntry::first();
-
- $this->assertEquals(1, $entry->peak_connection_count);
- $this->assertEquals(1, $entry->websocket_message_count);
- $this->assertEquals(1, $entry->api_message_count);
- }
-}
diff --git a/tests/Statistics/Logger/StatisticsLoggerTest.php b/tests/Statistics/Logger/StatisticsLoggerTest.php
deleted file mode 100644
index 08a8039..0000000
--- a/tests/Statistics/Logger/StatisticsLoggerTest.php
+++ /dev/null
@@ -1,105 +0,0 @@
-runOnlyOnLocalReplication();
- }
-
- /** @test */
- public function it_counts_connections()
- {
- $connections = [];
-
- $connections[] = $this->getConnectedWebSocketConnection(['channel-1']);
- $connections[] = $this->getConnectedWebSocketConnection(['channel-1']);
- $connections[] = $this->getConnectedWebSocketConnection(['channel-1']);
-
- $this->assertEquals(3, StatisticsLogger::getForAppId(1234)['peak_connection_count']);
-
- $this->pusherServer->onClose(array_pop($connections));
-
- StatisticsLogger::save();
-
- $this->assertEquals(2, StatisticsLogger::getForAppId(1234)['peak_connection_count']);
- }
-
- /** @test */
- public function it_counts_unique_connections_no_channel_subscriptions()
- {
- $connections = [];
-
- $connections[] = $this->getConnectedWebSocketConnection(['channel-1', 'channel-2']);
- $connections[] = $this->getConnectedWebSocketConnection(['channel-1', 'channel-2']);
- $connections[] = $this->getConnectedWebSocketConnection(['channel-1']);
-
- $this->assertEquals(3, StatisticsLogger::getForAppId(1234)['peak_connection_count']);
-
- $this->pusherServer->onClose(array_pop($connections));
- $this->pusherServer->onClose(array_pop($connections));
-
- StatisticsLogger::save();
-
- $this->assertEquals(1, StatisticsLogger::getForAppId(1234)['peak_connection_count']);
- }
-
- /** @test */
- public function it_counts_connections_with_memory_logger()
- {
- $connection = $this->getConnectedWebSocketConnection(['channel-1']);
-
- $logger = new MemoryStatisticsLogger(
- $this->channelManager,
- $this->statisticsDriver
- );
-
- $logger->webSocketMessage($connection->app->id);
- $logger->apiMessage($connection->app->id);
- $logger->connection($connection->app->id);
- $logger->disconnection($connection->app->id);
-
- $logger->save();
-
- $this->assertCount(1, WebSocketsStatisticsEntry::all());
-
- $entry = WebSocketsStatisticsEntry::first();
-
- $this->assertEquals(1, $entry->peak_connection_count);
- $this->assertEquals(1, $entry->websocket_message_count);
- $this->assertEquals(1, $entry->api_message_count);
- }
-
- /** @test */
- public function it_counts_connections_with_null_logger()
- {
- $connection = $this->getConnectedWebSocketConnection(['channel-1']);
-
- $logger = new NullStatisticsLogger(
- $this->channelManager,
- $this->statisticsDriver
- );
-
- $logger->webSocketMessage($connection->app->id);
- $logger->apiMessage($connection->app->id);
- $logger->connection($connection->app->id);
- $logger->disconnection($connection->app->id);
-
- $logger->save();
-
- $this->assertCount(0, WebSocketsStatisticsEntry::all());
- }
-}
diff --git a/tests/Statistics/Rules/AppIdTest.php b/tests/Statistics/Rules/AppIdTest.php
deleted file mode 100644
index 0849d0b..0000000
--- a/tests/Statistics/Rules/AppIdTest.php
+++ /dev/null
@@ -1,18 +0,0 @@
-assertTrue($rule->passes('app_id', config('websockets.apps.0.id')));
- $this->assertFalse($rule->passes('app_id', 'invalid-app-id'));
- }
-}
diff --git a/tests/StatisticsStoreTest.php b/tests/StatisticsStoreTest.php
new file mode 100644
index 0000000..6fe6cc2
--- /dev/null
+++ b/tests/StatisticsStoreTest.php
@@ -0,0 +1,48 @@
+newActiveConnection(['public-channel']);
+ $morty = $this->newActiveConnection(['public-channel']);
+
+ $this->statisticsCollector->save();
+
+ $this->assertCount(1, $records = $this->statisticsStore->getRecords());
+
+ $this->assertEquals('2', $records[0]['peak_connections_count']);
+ $this->assertEquals('2', $records[0]['websocket_messages_count']);
+ $this->assertEquals('0', $records[0]['api_messages_count']);
+ }
+
+ public function test_store_statistics_on_private_channel()
+ {
+ $rick = $this->newPrivateConnection('private-channel');
+ $morty = $this->newPrivateConnection('private-channel');
+
+ $this->statisticsCollector->save();
+
+ $this->assertCount(1, $records = $this->statisticsStore->getRecords());
+
+ $this->assertEquals('2', $records[0]['peak_connections_count']);
+ $this->assertEquals('2', $records[0]['websocket_messages_count']);
+ $this->assertEquals('0', $records[0]['api_messages_count']);
+ }
+
+ public function test_store_statistics_on_presence_channel()
+ {
+ $rick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]);
+ $morty = $this->newPresenceConnection('presence-channel', ['user_id' => 2]);
+
+ $this->statisticsCollector->save();
+
+ $this->assertCount(1, $records = $this->statisticsStore->getRecords());
+
+ $this->assertEquals('2', $records[0]['peak_connections_count']);
+ $this->assertEquals('2', $records[0]['websocket_messages_count']);
+ $this->assertEquals('0', $records[0]['api_messages_count']);
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index d83bd9b..c013b1c 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -1,43 +1,51 @@
loop = LoopFactory::create();
+ $this->replicationMode = getenv('REPLICATION_MODE') ?: 'local';
+
$this->resetDatabase();
-
$this->loadLaravelMigrations(['--database' => 'sqlite']);
-
+ $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
$this->withFactories(__DIR__.'/database/factories');
- $this->configurePubSub();
+ $this->registerManagers();
- $this->channelManager = $this->app->make(ChannelManager::class);
+ $this->registerStatisticsCollectors();
- $this->statisticsDriver = $this->app->make(StatisticsDriver::class);
-
- $this->configureStatisticsLogger();
-
- $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
+ $this->registerStatisticsStores();
$this->pusherServer = $this->app->make(config('websockets.handlers.websocket'));
+
+ if ($this->replicationMode === 'redis') {
+ $this->registerRedis();
+ }
}
/**
@@ -95,20 +104,54 @@ abstract class TestCase extends BaseTestCase
/**
* {@inheritdoc}
*/
- protected function getEnvironmentSetUp($app)
+ public function getEnvironmentSetUp($app)
{
- $app['config']->set('app.key', 'wslxrEFGWY6GfGhvN9L3wH3KSRJQQpBD');
-
- $app['config']->set('auth.providers.users.model', Models\User::class);
+ $this->replicationMode = getenv('REPLICATION_MODE') ?: 'local';
$app['config']->set('database.default', 'sqlite');
$app['config']->set('database.connections.sqlite', [
- 'driver' => 'sqlite',
+ 'driver' => 'sqlite',
'database' => __DIR__.'/database.sqlite',
- 'prefix' => '',
+ 'prefix' => '',
]);
+ $app['config']->set(
+ 'broadcasting.connections.websockets', [
+ 'driver' => 'pusher',
+ 'key' => 'TestKey',
+ 'secret' => 'TestSecret',
+ 'app_id' => '1234',
+ 'options' => [
+ 'cluster' => 'mt1',
+ 'encrypted' => true,
+ 'host' => '127.0.0.1',
+ 'port' => 6001,
+ 'scheme' => 'http',
+ ],
+ ]
+ );
+
+ $app['config']->set('auth.providers.users.model', Models\User::class);
+
+ $app['config']->set('app.key', 'wslxrEFGWY6GfGhvN9L3wH3KSRJQQpBD');
+
+ $app['config']->set('database.redis.default', [
+ 'host' => env('REDIS_HOST', '127.0.0.1'),
+ 'password' => env('REDIS_PASSWORD', null),
+ 'port' => env('REDIS_PORT', '6379'),
+ 'database' => env('REDIS_DB', '0'),
+ ]);
+
+ $app['config']->set(
+ 'websockets.replication.mode', $this->replicationMode
+ );
+
+ if ($this->replicationMode === 'redis') {
+ $app['config']->set('broadcasting.default', 'pusher');
+ $app['config']->set('cache.default', 'redis');
+ }
+
$app['config']->set('websockets.apps', [
[
'name' => 'Test App',
@@ -133,53 +176,109 @@ abstract class TestCase extends BaseTestCase
'test.origin.com',
],
],
+ [
+ 'name' => 'Test App 2',
+ 'id' => '12345',
+ 'key' => 'TestKey2',
+ 'secret' => 'TestSecret2',
+ 'host' => 'localhost',
+ 'capacity' => null,
+ 'enable_client_messages' => false,
+ 'enable_statistics' => true,
+ 'allowed_origins' => [],
+ ],
]);
- $app['config']->set('database.redis.default', [
- 'host' => env('REDIS_HOST', '127.0.0.1'),
- 'password' => env('REDIS_PASSWORD', null),
- 'port' => env('REDIS_PORT', '6379'),
- 'database' => env('REDIS_DB', '0'),
+ $app['config']->set('websockets.replication.modes', [
+ 'local' => [
+ 'channel_manager' => \BeyondCode\LaravelWebSockets\ChannelManagers\LocalChannelManager::class,
+ 'collector' => \BeyondCode\LaravelWebSockets\Statistics\Collectors\MemoryCollector::class,
+ ],
+ 'redis' => [
+ 'channel_manager' => \BeyondCode\LaravelWebSockets\ChannelManagers\RedisChannelManager::class,
+ 'connection' => 'default',
+ 'collector' => \BeyondCode\LaravelWebSockets\Statistics\Collectors\RedisCollector::class,
+ ],
]);
-
- $replicationDriver = getenv('REPLICATION_DRIVER') ?: 'local';
-
- $app['config']->set(
- 'websockets.replication.driver', $replicationDriver
- );
-
- $app['config']->set(
- 'broadcasting.connections.websockets', [
- 'driver' => 'pusher',
- 'key' => 'TestKey',
- 'secret' => 'TestSecret',
- 'app_id' => '1234',
- 'options' => [
- 'cluster' => 'mt1',
- 'encrypted' => true,
- 'host' => '127.0.0.1',
- 'port' => 6001,
- 'scheme' => 'http',
- ],
- ]
- );
-
- if (in_array($replicationDriver, ['redis'])) {
- $app['config']->set('broadcasting.default', 'pusher');
- $app['config']->set('cache.default', 'redis');
- }
}
/**
- * Get the websocket connection for a specific URL.
+ * Register the managers that are not resolved
+ * by the package service provider.
*
- * @param mixed $appKey
- * @param array $headers
- * @return \BeyondCode\LaravelWebSockets\Tests\Mocks\Connection
+ * @return void
*/
- protected function getWebSocketConnection(string $appKey = 'TestKey', array $headers = []): Connection
+ protected function registerManagers()
{
- $connection = new Connection;
+ $this->app->singleton(ChannelManager::class, function () {
+ $mode = config('websockets.replication.mode', $this->replicationMode);
+
+ $class = config("websockets.replication.modes.{$mode}.channel_manager");
+
+ return new $class($this->loop, Mocks\RedisFactory::class);
+ });
+
+ $this->channelManager = $this->app->make(ChannelManager::class);
+ }
+
+ /**
+ * Register the statistics collectors that are
+ * not resolved by the package service provider.
+ *
+ * @return void
+ */
+ protected function registerStatisticsCollectors()
+ {
+ $this->app->singleton(StatisticsCollector::class, function () {
+ $class = config("websockets.replication.modes.{$this->replicationMode}.collector");
+
+ return new $class;
+ });
+
+ $this->statisticsCollector = $this->app->make(StatisticsCollector::class);
+
+ $this->statisticsCollector->flush();
+ }
+
+ /**
+ * Register the statistics stores that are
+ * not resolved by the package service provider.
+ *
+ * @return void
+ */
+ protected function registerStatisticsStores()
+ {
+ $this->app->singleton(StatisticsStore::class, function () {
+ $class = config('websockets.statistics.store');
+
+ return new $class;
+ });
+
+ $this->statisticsStore = $this->app->make(StatisticsStore::class);
+ }
+
+ /**
+ * Register the Redis components for testing.
+ *
+ * @return void
+ */
+ protected function registerRedis()
+ {
+ $this->redis = Redis::connection();
+
+ $this->redis->flushdb();
+ }
+
+ /**
+ * Get the websocket connection for a specific key.
+ *
+ * @param string $appKey
+ * @param array $headers
+ * @return Mocks\Connection
+ */
+ protected function newConnection(string $appKey = 'TestKey', array $headers = [])
+ {
+ $connection = new Mocks\Connection;
$connection->httpRequest = new Request('GET', "/?appKey={$appKey}", $headers);
@@ -192,18 +291,16 @@ abstract class TestCase extends BaseTestCase
* @param array $channelsToJoin
* @param string $appKey
* @param array $headers
- * @return \BeyondCode\LaravelWebSockets\Tests\Mocks\Connection
+ * @return Mocks\Connection
*/
- protected function getConnectedWebSocketConnection(array $channelsToJoin = [], string $appKey = 'TestKey', array $headers = []): Connection
+ protected function newActiveConnection(array $channelsToJoin = [], string $appKey = 'TestKey', array $headers = [])
{
- $connection = new Connection;
-
- $connection->httpRequest = new Request('GET', "/?appKey={$appKey}", $headers);
+ $connection = $this->newConnection($appKey, $headers);
$this->pusherServer->onOpen($connection);
foreach ($channelsToJoin as $channel) {
- $message = new Message([
+ $message = new Mocks\Message([
'event' => 'pusher:subscribe',
'data' => [
'channel' => $channel,
@@ -220,29 +317,30 @@ abstract class TestCase extends BaseTestCase
* Join a presence channel.
*
* @param string $channel
- * @return \BeyondCode\LaravelWebSockets\Tests\Mocks\Connection
+ * @param array $user
+ * @return Mocks\Connection
*/
- protected function joinPresenceChannel($channel): Connection
+ protected function newPresenceConnection($channel, array $user = [])
{
- $connection = $this->getWebSocketConnection();
+ $connection = $this->newConnection();
$this->pusherServer->onOpen($connection);
- $channelData = [
+ $user = $user ?: [
'user_id' => 1,
- 'user_info' => [
- 'name' => 'Marcel',
- ],
+ 'user_info' => ['name' => 'Rick'],
];
- $signature = "{$connection->socketId}:{$channel}:".json_encode($channelData);
+ $signature = "{$connection->socketId}:{$channel}:".json_encode($user);
- $message = new Message([
+ $hash = hash_hmac('sha256', $signature, $connection->app->secret);
+
+ $message = new Mocks\Message([
'event' => 'pusher:subscribe',
'data' => [
- 'auth' => $connection->app->key.':'.hash_hmac('sha256', $signature, $connection->app->secret),
+ 'auth' => "{$connection->app->key}:{$hash}",
'channel' => $channel,
- 'channel_data' => json_encode($channelData),
+ 'channel_data' => json_encode($user),
],
]);
@@ -252,119 +350,52 @@ abstract class TestCase extends BaseTestCase
}
/**
- * Get a channel from connection.
+ * Join a private channel.
*
- * @param \Ratchet\ConnectionInterface $connection
- * @param string $channelName
- * @return \BeyondCode\LaravelWebSockets\WebSockets\Channels\Channel|null
+ * @param string $channel
+ * @return Mocks\Connection
*/
- protected function getChannel(ConnectionInterface $connection, string $channelName)
+ protected function newPrivateConnection($channel)
{
- return $this->channelManager->findOrCreate($connection->app->id, $channelName);
- }
+ $connection = $this->newConnection();
- /**
- * Configure the replicator clients.
- *
- * @return void
- */
- protected function configurePubSub()
- {
- $replicationDriver = config('websockets.replication.driver', 'local');
+ $this->pusherServer->onOpen($connection);
- // Replace the publish and subscribe clients with a Mocked
- // factory lazy instance on boot.
- $this->app->singleton(ReplicationInterface::class, function () use ($replicationDriver) {
- $client = config(
- "websockets.replication.{$replicationDriver}.client",
- \BeyondCode\LaravelWebSockets\PubSub\Drivers\LocalClient::class
- );
+ $signature = "{$connection->socketId}:{$channel}";
- return (new $client)->boot(
- $this->loop, Mocks\RedisFactory::class
- );
- });
+ $hash = hash_hmac('sha256', $signature, $connection->app->secret);
- if ($replicationDriver === 'redis') {
- $this->redis = Redis::connection();
- }
- }
+ $message = new Mocks\Message([
+ 'event' => 'pusher:subscribe',
+ 'data' => [
+ 'auth' => "{$connection->app->key}:{$hash}",
+ 'channel' => $channel,
+ ],
+ ]);
- /**
- * Configure the statistics logger for the right driver.
- *
- * @return void
- */
- protected function configureStatisticsLogger()
- {
- $replicationDriver = getenv('REPLICATION_DRIVER') ?: 'local';
+ $this->pusherServer->onMessage($connection, $message);
- if ($replicationDriver === 'local') {
- StatisticsLogger::swap(new FakeMemoryStatisticsLogger(
- $this->channelManager,
- app(StatisticsDriver::class)
- ));
- }
-
- if ($replicationDriver === 'redis') {
- StatisticsLogger::swap(new FakeRedisStatisticsLogger(
- $this->channelManager,
- app(StatisticsDriver::class),
- $this->app->make(ReplicationInterface::class)
- ));
- }
- }
-
- protected function runOnlyOnRedisReplication()
- {
- if (config('websockets.replication.driver') !== 'redis') {
- $this->markTestSkipped('Skipped test because the replication driver is not set to Redis.');
- }
- }
-
- protected function runOnlyOnLocalReplication()
- {
- if (config('websockets.replication.driver') !== 'local') {
- $this->markTestSkipped('Skipped test because the replication driver is not set to Local.');
- }
- }
-
- protected function skipOnRedisReplication()
- {
- if (config('websockets.replication.driver') === 'redis') {
- $this->markTestSkipped('Skipped test because the replication driver is Redis.');
- }
- }
-
- protected function skipOnLocalReplication()
- {
- if (config('websockets.replication.driver') === 'local') {
- $this->markTestSkipped('Skipped test because the replication driver is Local.');
- }
+ return $connection;
}
/**
* Get the subscribed client for the replication.
*
- * @return ReplicationInterface
+ * @return Mocks\LazyClient
*/
protected function getSubscribeClient()
{
- return $this->app
- ->make(ReplicationInterface::class)
- ->getSubscribeClient();
+ return $this->channelManager->getSubscribeClient();
}
/**
* Get the publish client for the replication.
*
- * @return ReplicationInterface
+ * @return Mocks\LazyClient
*/
protected function getPublishClient()
{
- return $this->app
- ->make(ReplicationInterface::class)
- ->getPublishClient();
+ return $this->channelManager->getPublishClient();
}
/**
@@ -376,4 +407,32 @@ abstract class TestCase extends BaseTestCase
{
file_put_contents(__DIR__.'/database.sqlite', null);
}
+
+ protected function runOnlyOnRedisReplication()
+ {
+ if ($this->replicationMode !== 'redis') {
+ $this->markTestSkipped('Skipped test because the replication mode is not set to Redis.');
+ }
+ }
+
+ protected function runOnlyOnLocalReplication()
+ {
+ if ($this->replicationMode !== 'local') {
+ $this->markTestSkipped('Skipped test because the replication mode is not set to Local.');
+ }
+ }
+
+ protected function skipOnRedisReplication()
+ {
+ if ($this->replicationMode === 'redis') {
+ $this->markTestSkipped('Skipped test because the replication mode is Redis.');
+ }
+ }
+
+ protected function skipOnLocalReplication()
+ {
+ if ($this->replicationMode === 'local') {
+ $this->markTestSkipped('Skipped test because the replication mode is Local.');
+ }
+ }
}
diff --git a/tests/TestServiceProvider.php b/tests/TestServiceProvider.php
index 958086e..c43ce45 100644
--- a/tests/TestServiceProvider.php
+++ b/tests/TestServiceProvider.php
@@ -1,6 +1,6 @@
expectException(HttpException::class);
+ $this->expectExceptionMessage('Invalid auth signature provided.');
+
+ $connection = new Mocks\Connection;
+
+ $requestPath = '/apps/1234/events';
+
+ $routeParams = [
+ 'appId' => '1234',
+ ];
+
+ $queryString = Pusher::build_auth_query_string(
+ 'TestKey', 'InvalidSecret', 'GET', $requestPath
+ );
+
+ $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
+
+ $controller = app(TriggerEvent::class);
+
+ $controller->onOpen($connection, $request);
+ }
+
+ public function test_it_fires_the_event_to_public_channel()
+ {
+ $this->newActiveConnection(['public-channel']);
+
+ $connection = new Mocks\Connection;
+
+ $requestPath = '/apps/1234/events';
+
+ $routeParams = [
+ 'appId' => '1234',
+ ];
+
+ $queryString = Pusher::build_auth_query_string(
+ 'TestKey', 'TestSecret', 'GET', $requestPath, [
+ 'channels' => 'public-channel',
+ ],
+ );
+
+ $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
+
+ $controller = app(TriggerEvent::class);
+
+ $controller->onOpen($connection, $request);
+
+ /** @var JsonResponse $response */
+ $response = array_pop($connection->sentRawData);
+
+ $this->assertSame([], json_decode($response->getContent(), true));
+
+ $this->statisticsCollector
+ ->getAppStatistics('1234')
+ ->then(function ($statistics) {
+ $this->assertEquals([
+ 'peak_connections_count' => 1,
+ 'websocket_messages_count' => 1,
+ 'api_messages_count' => 1,
+ 'app_id' => '1234',
+ ], $statistic->toArray());
+ });
+ }
+
+ public function test_it_fires_the_event_to_presence_channel()
+ {
+ $this->newPresenceConnection('presence-channel');
+
+ $connection = new Mocks\Connection;
+
+ $requestPath = '/apps/1234/events';
+
+ $routeParams = [
+ 'appId' => '1234',
+ ];
+
+ $queryString = Pusher::build_auth_query_string(
+ 'TestKey', 'TestSecret', 'GET', $requestPath, [
+ 'channels' => 'presence-channel',
+ ],
+ );
+
+ $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
+
+ $controller = app(TriggerEvent::class);
+
+ $controller->onOpen($connection, $request);
+
+ /** @var JsonResponse $response */
+ $response = array_pop($connection->sentRawData);
+
+ $this->assertSame([], json_decode($response->getContent(), true));
+
+ $this->statisticsCollector
+ ->getAppStatistics('1234')
+ ->then(function ($statistics) {
+ $this->assertEquals([
+ 'peak_connections_count' => 1,
+ 'websocket_messages_count' => 1,
+ 'api_messages_count' => 1,
+ 'app_id' => '1234',
+ ], $statistic->toArray());
+ });
+ }
+
+ public function test_it_fires_the_event_to_private_channel()
+ {
+ $this->newPresenceConnection('private-channel');
+
+ $connection = new Mocks\Connection;
+
+ $requestPath = '/apps/1234/events';
+
+ $routeParams = [
+ 'appId' => '1234',
+ ];
+
+ $queryString = Pusher::build_auth_query_string(
+ 'TestKey', 'TestSecret', 'GET', $requestPath, [
+ 'channels' => 'private-channel',
+ ],
+ );
+
+ $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
+
+ $controller = app(TriggerEvent::class);
+
+ $controller->onOpen($connection, $request);
+
+ /** @var JsonResponse $response */
+ $response = array_pop($connection->sentRawData);
+
+ $this->assertSame([], json_decode($response->getContent(), true));
+
+ $this->statisticsCollector
+ ->getAppStatistics('1234')
+ ->then(function ($statistics) {
+ $this->assertEquals([
+ 'peak_connections_count' => 1,
+ 'websocket_messages_count' => 1,
+ 'api_messages_count' => 1,
+ 'app_id' => '1234',
+ ], $statistic->toArray());
+ });
+ }
+
+ public function test_it_fires_event_across_servers()
+ {
+ $connection = new Mocks\Connection;
+
+ $requestPath = '/apps/1234/events';
+
+ $routeParams = [
+ 'appId' => '1234',
+ ];
+
+ $queryString = Pusher::build_auth_query_string(
+ 'TestKey', 'TestSecret', 'GET', $requestPath, [
+ 'channels' => 'public-channel',
+ ],
+ );
+
+ $request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
+
+ $controller = app(TriggerEvent::class);
+
+ $controller->onOpen($connection, $request);
+
+ /** @var JsonResponse $response */
+ $response = array_pop($connection->sentRawData);
+
+ $this->assertSame([], json_decode($response->getContent(), true));
+
+ if (method_exists($this->channelManager, 'getPublishClient')) {
+ $this->channelManager
+ ->getPublishClient()
+ ->assertCalledWithArgs('publish', [
+ $this->channelManager->getRedisKey('1234', 'public-channel'),
+ json_encode([
+ 'channel' => 'public-channel',
+ 'event' => null,
+ 'data' => null,
+ 'appId' => '1234',
+ 'serverId' => $this->channelManager->getServerId(),
+ ]),
+ ]);
+ }
+ }
+}
diff --git a/tests/database/factories/UserFactory.php b/tests/database/factories/UserFactory.php
index b6aaf0d..07d6e7b 100644
--- a/tests/database/factories/UserFactory.php
+++ b/tests/database/factories/UserFactory.php
@@ -12,7 +12,7 @@
use Illuminate\Support\Str;
-$factory->define(\BeyondCode\LaravelWebSockets\Tests\Models\User::class, function () {
+$factory->define(\BeyondCode\LaravelWebSockets\Test\Models\User::class, function () {
return [
'name' => 'Name'.Str::random(5),
'email' => Str::random(5).'@gmail.com',