wip
This commit is contained in:
parent
de6b1b28ba
commit
6f32b89459
|
|
@ -11,7 +11,7 @@ end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[*.blade.php]
|
[*.{blade.php,yml,yaml}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
|
|
|
||||||
|
|
@ -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 }}
|
||||||
|
|
@ -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'
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
/vendor
|
||||||
|
/.idea
|
||||||
build
|
build
|
||||||
composer.lock
|
|
||||||
vendor
|
|
||||||
coverage
|
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
.idea/
|
coverage
|
||||||
|
composer.phar
|
||||||
|
composer.lock
|
||||||
|
.DS_Store
|
||||||
database.sqlite
|
database.sqlite
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
filter:
|
filter:
|
||||||
excluded_paths: [tests/*]
|
excluded_paths: [tests/*]
|
||||||
|
|
||||||
checks:
|
checks:
|
||||||
php:
|
php:
|
||||||
remove_extra_empty_lines: true
|
remove_extra_empty_lines: true
|
||||||
remove_php_closing_tag: true
|
remove_php_closing_tag: true
|
||||||
remove_trailing_whitespace: true
|
remove_trailing_whitespace: true
|
||||||
fix_use_statements:
|
fix_use_statements:
|
||||||
remove_unused: true
|
remove_unused: true
|
||||||
preserve_multiple: false
|
preserve_multiple: false
|
||||||
preserve_blanklines: true
|
preserve_blanklines: true
|
||||||
order_alphabetically: true
|
order_alphabetically: true
|
||||||
fix_php_opening_tag: true
|
fix_php_opening_tag: true
|
||||||
fix_linefeed: true
|
fix_linefeed: true
|
||||||
fix_line_ending: true
|
fix_line_ending: true
|
||||||
fix_identation_4spaces: true
|
fix_identation_4spaces: true
|
||||||
fix_doc_comments: true
|
fix_doc_comments: true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1 @@
|
||||||
preset: laravel
|
preset: laravel
|
||||||
|
|
||||||
disabled:
|
|
||||||
- single_class_element_per_statement
|
|
||||||
21
CHANGELOG.md
21
CHANGELOG.md
|
|
@ -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
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "beyondcode/laravel-websockets",
|
"name": "beyondcode/laravel-websockets",
|
||||||
"description": "An easy to use WebSocket server",
|
"description": ":package_description",
|
||||||
"keywords": [
|
"keywords": ["laravel", "php"],
|
||||||
"beyondcode",
|
|
||||||
"laravel-websockets"
|
|
||||||
],
|
|
||||||
"homepage": "https://github.com/beyondcode/laravel-websockets",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"homepage": "https://github.com/beyondcode/laravel-websockets",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Marcel Pociot",
|
"name": "Marcel Pociot",
|
||||||
|
|
@ -19,6 +16,11 @@
|
||||||
"email": "freek@spatie.be",
|
"email": "freek@spatie.be",
|
||||||
"homepage": "https://spatie.be",
|
"homepage": "https://spatie.be",
|
||||||
"role": "Developer"
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Alex Renoki",
|
||||||
|
"homepage": "https://github.com/rennokki",
|
||||||
|
"role": "Developer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
|
|
@ -28,50 +30,43 @@
|
||||||
"clue/buzz-react": "^2.5",
|
"clue/buzz-react": "^2.5",
|
||||||
"clue/redis-react": "^2.3",
|
"clue/redis-react": "^2.3",
|
||||||
"evenement/evenement": "^2.0|^3.0",
|
"evenement/evenement": "^2.0|^3.0",
|
||||||
"facade/ignition-contracts": "^1.0",
|
|
||||||
"guzzlehttp/psr7": "^1.5",
|
"guzzlehttp/psr7": "^1.5",
|
||||||
"illuminate/broadcasting": "^6.0|^7.0",
|
"laravel/framework": "^6.0|^7.0|^8.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",
|
|
||||||
"pusher/pusher-php-server": "^3.0|^4.0",
|
"pusher/pusher-php-server": "^3.0|^4.0",
|
||||||
"react/promise": "^2.0",
|
"react/promise": "^2.0",
|
||||||
"symfony/http-kernel": "^4.0|^5.0",
|
"symfony/http-kernel": "^4.0|^5.0",
|
||||||
"symfony/psr-http-message-bridge": "^1.1|^2.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": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"BeyondCode\\LaravelWebSockets\\": "src"
|
"BeyondCode\\LaravelWebSockets\\": "src/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"BeyondCode\\LaravelWebSockets\\Tests\\": "tests"
|
"BeyondCode\\LaravelWebSockets\\Test\\": "tests"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vendor/bin/phpunit",
|
"test": "vendor/bin/phpunit"
|
||||||
"test-coverage": "vendor/bin/phpunit --coverage-html coverage"
|
},
|
||||||
|
"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": {
|
"config": {
|
||||||
"sort-packages": true
|
"sort-packages": true
|
||||||
},
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
"extra": {
|
"extra": {
|
||||||
"laravel": {
|
"laravel": {
|
||||||
"providers": [
|
"providers": [
|
||||||
"BeyondCode\\LaravelWebSockets\\WebSocketsServiceProvider"
|
"BeyondCode\\LaravelWebSockets\\WebSocketsServiceProvider"
|
||||||
],
|
]
|
||||||
"aliases": {
|
|
||||||
"WebSocketRouter": "BeyondCode\\LaravelWebSockets\\Facades\\WebSocketRouter"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
| Maximum Request Size
|
||||||
|
|
@ -130,130 +265,15 @@ return [
|
||||||
|
|
||||||
'handlers' => [
|
'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,
|
'fetch_users' => \BeyondCode\LaravelWebSockets\API\FetchUsers::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,
|
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@ class CreateWebSocketsStatisticsEntriesTable extends Migration
|
||||||
Schema::create('websockets_statistics_entries', function (Blueprint $table) {
|
Schema::create('websockets_statistics_entries', function (Blueprint $table) {
|
||||||
$table->increments('id');
|
$table->increments('id');
|
||||||
$table->string('app_id');
|
$table->string('app_id');
|
||||||
$table->integer('peak_connection_count');
|
$table->integer('peak_connections_count');
|
||||||
$table->integer('websocket_message_count');
|
$table->integer('websocket_messages_count');
|
||||||
$table->integer('api_message_count');
|
$table->integer('api_messages_count');
|
||||||
$table->nullableTimestamps();
|
$table->nullableTimestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
processIsolation="false"
|
processIsolation="false"
|
||||||
stopOnFailure="false">
|
stopOnFailure="false">
|
||||||
<testsuites>
|
<testsuites>
|
||||||
<testsuite name="BeyondCode Test Suite">
|
<testsuite name="Test Suite">
|
||||||
<directory>tests</directory>
|
<directory>tests</directory>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
</whitelist>
|
</whitelist>
|
||||||
</filter>
|
</filter>
|
||||||
<php>
|
<php>
|
||||||
<env name="DB_CONNECTION" value="testing"/>
|
<server name="APP_DEBUG" value="1" />
|
||||||
|
<server name="APP_ENV" value="testing" />
|
||||||
</php>
|
</php>
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|
@ -395,8 +395,6 @@
|
||||||
|
|
||||||
let payload = {
|
let payload = {
|
||||||
_token: '{{ csrf_token() }}',
|
_token: '{{ csrf_token() }}',
|
||||||
key: this.app.key,
|
|
||||||
secret: this.app.secret,
|
|
||||||
appId: this.app.id,
|
appId: this.app.id,
|
||||||
channel: this.form.channel,
|
channel: this.form.channel,
|
||||||
event: this.form.event,
|
event: this.form.event,
|
||||||
|
|
@ -424,10 +422,6 @@
|
||||||
return 'bg-green-700 text-white';
|
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)) {
|
if (['disconnection', 'occupied', 'replicator-unsubscribed', 'replicator-left'].includes(log.type)) {
|
||||||
return 'bg-red-700 text-white';
|
return 'bg-red-700 text-white';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\HttpApi\Controllers;
|
namespace BeyondCode\LaravelWebSockets\API;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
use BeyondCode\LaravelWebSockets\Server\QueryParameters;
|
||||||
use BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface;
|
use Ratchet\Http\HttpServerInterface;
|
||||||
use BeyondCode\LaravelWebSockets\QueryParameters;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use GuzzleHttp\Psr7\Response;
|
use GuzzleHttp\Psr7\Response;
|
||||||
use GuzzleHttp\Psr7\ServerRequest;
|
use GuzzleHttp\Psr7\ServerRequest;
|
||||||
|
|
@ -16,10 +14,11 @@ use Illuminate\Support\Collection;
|
||||||
use Psr\Http\Message\RequestInterface;
|
use Psr\Http\Message\RequestInterface;
|
||||||
use Pusher\Pusher;
|
use Pusher\Pusher;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use Ratchet\Http\HttpServerInterface;
|
|
||||||
use React\Promise\PromiseInterface;
|
use React\Promise\PromiseInterface;
|
||||||
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
|
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
|
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||||
|
|
||||||
abstract class Controller implements HttpServerInterface
|
abstract class Controller implements HttpServerInterface
|
||||||
{
|
{
|
||||||
|
|
@ -48,28 +47,19 @@ abstract class Controller implements HttpServerInterface
|
||||||
/**
|
/**
|
||||||
* The channel manager.
|
* The channel manager.
|
||||||
*
|
*
|
||||||
* @var \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager
|
* @var \BeyondCode\LaravelWebSockets\Contracts\ChannelManager
|
||||||
*/
|
*/
|
||||||
protected $channelManager;
|
protected $channelManager;
|
||||||
|
|
||||||
/**
|
|
||||||
* The replicator driver.
|
|
||||||
*
|
|
||||||
* @var \BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface
|
|
||||||
*/
|
|
||||||
protected $replicator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the request.
|
* Initialize the request.
|
||||||
*
|
*
|
||||||
* @param \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager $channelManager
|
* @param ChannelManager $channelManager
|
||||||
* @param \BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface $replicator
|
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function __construct(ChannelManager $channelManager, ReplicationInterface $replicator)
|
public function __construct(ChannelManager $channelManager)
|
||||||
{
|
{
|
||||||
$this->channelManager = $channelManager;
|
$this->channelManager = $channelManager;
|
||||||
$this->replicator = $replicator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -202,6 +192,10 @@ abstract class Controller implements HttpServerInterface
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($response instanceof HttpException) {
|
||||||
|
throw $response;
|
||||||
|
}
|
||||||
|
|
||||||
$this->sendAndClose($connection, $response);
|
$this->sendAndClose($connection, $response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,11 +237,12 @@ abstract class Controller implements HttpServerInterface
|
||||||
*/
|
*/
|
||||||
protected function ensureValidSignature(Request $request)
|
protected function ensureValidSignature(Request $request)
|
||||||
{
|
{
|
||||||
/*
|
// The `auth_signature` & `body_md5` parameters are not included when calculating the `auth_signature` value.
|
||||||
* 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.
|
||||||
* The `appId`, `appKey` & `channelName` parameters are actually route parameters and are never supplied by the client.
|
|
||||||
*/
|
$params = Arr::except($request->query(), [
|
||||||
$params = Arr::except($request->query(), ['auth_signature', 'body_md5', 'appId', 'appKey', 'channelName']);
|
'auth_signature', 'body_md5', 'appId', 'appKey', 'channelName',
|
||||||
|
]);
|
||||||
|
|
||||||
if ($request->getContent() !== '') {
|
if ($request->getContent() !== '') {
|
||||||
$params['body_md5'] = md5($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);
|
$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')) {
|
if ($authSignature !== $request->get('auth_signature')) {
|
||||||
throw new HttpException(401, 'Invalid auth signature provided.');
|
throw new HttpException(401, 'Invalid auth signature provided.');
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\API;
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
|
||||||
|
class FetchChannel extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request)
|
||||||
|
{
|
||||||
|
$channel = $this->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,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\API;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Channels\Channel;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use stdClass;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
|
||||||
|
class FetchChannels extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request)
|
||||||
|
{
|
||||||
|
$attributes = [];
|
||||||
|
|
||||||
|
if ($request->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,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\API;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\WebSockets\Channels\PresenceChannel;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
|
||||||
|
class FetchUsers extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request)
|
||||||
|
{
|
||||||
|
if (! Str::startsWith($request->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,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\API;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||||
|
use BeyondCode\LaravelWebSockets\WebSockets\Channels\PresenceChannel;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
use BeyondCode\LaravelWebSockets\Facades\StatisticsCollector;
|
||||||
|
|
||||||
|
class TriggerEvent extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request)
|
||||||
|
{
|
||||||
|
$channels = $request->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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Apps;
|
namespace BeyondCode\LaravelWebSockets\Apps;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Exceptions\InvalidApp;
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
|
|
||||||
class App
|
class App
|
||||||
{
|
{
|
||||||
|
|
@ -76,18 +76,9 @@ class App
|
||||||
* @param string $key
|
* @param string $key
|
||||||
* @param string $secret
|
* @param string $secret
|
||||||
* @return void
|
* @return void
|
||||||
* @throws \BeyondCode\LaravelWebSockets\Exceptions\InvalidApp
|
|
||||||
*/
|
*/
|
||||||
public function __construct($appId, $appKey, $appSecret)
|
public function __construct($appId, $appKey, $appSecret)
|
||||||
{
|
{
|
||||||
if ($appKey === '') {
|
|
||||||
throw InvalidApp::valueIsRequired('appKey', $appId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($appSecret === '') {
|
|
||||||
throw InvalidApp::valueIsRequired('appSecret', $appId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->id = $appId;
|
$this->id = $appId;
|
||||||
$this->key = $appKey;
|
$this->key = $appKey;
|
||||||
$this->secret = $appSecret;
|
$this->secret = $appSecret;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Apps;
|
namespace BeyondCode\LaravelWebSockets\Apps;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
|
|
||||||
class ConfigAppManager implements AppManager
|
class ConfigAppManager implements AppManager
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
@ -30,7 +32,7 @@ class ConfigAppManager implements AppManager
|
||||||
{
|
{
|
||||||
return $this->apps
|
return $this->apps
|
||||||
->map(function (array $appAttributes) {
|
->map(function (array $appAttributes) {
|
||||||
return $this->instantiate($appAttributes);
|
return $this->convertIntoApp($appAttributes);
|
||||||
})
|
})
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
@ -43,11 +45,9 @@ class ConfigAppManager implements AppManager
|
||||||
*/
|
*/
|
||||||
public function findById($appId): ?App
|
public function findById($appId): ?App
|
||||||
{
|
{
|
||||||
$appAttributes = $this
|
return $this->convertIntoApp(
|
||||||
->apps
|
$this->apps->firstWhere('id', $appId)
|
||||||
->firstWhere('id', $appId);
|
);
|
||||||
|
|
||||||
return $this->instantiate($appAttributes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -58,11 +58,9 @@ class ConfigAppManager implements AppManager
|
||||||
*/
|
*/
|
||||||
public function findByKey($appKey): ?App
|
public function findByKey($appKey): ?App
|
||||||
{
|
{
|
||||||
$appAttributes = $this
|
return $this->convertIntoApp(
|
||||||
->apps
|
$this->apps->firstWhere('key', $appKey)
|
||||||
->firstWhere('key', $appKey);
|
);
|
||||||
|
|
||||||
return $this->instantiate($appAttributes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -73,11 +71,9 @@ class ConfigAppManager implements AppManager
|
||||||
*/
|
*/
|
||||||
public function findBySecret($appSecret): ?App
|
public function findBySecret($appSecret): ?App
|
||||||
{
|
{
|
||||||
$appAttributes = $this
|
return $this->convertIntoApp(
|
||||||
->apps
|
$this->apps->firstWhere('secret', $appSecret)
|
||||||
->firstWhere('secret', $appSecret);
|
);
|
||||||
|
|
||||||
return $this->instantiate($appAttributes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -86,7 +82,7 @@ class ConfigAppManager implements AppManager
|
||||||
* @param array|null $app
|
* @param array|null $app
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||||
*/
|
*/
|
||||||
protected function instantiate(?array $appAttributes): ?App
|
protected function convertIntoApp(?array $appAttributes): ?App
|
||||||
{
|
{
|
||||||
if (! $appAttributes) {
|
if (! $appAttributes) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,334 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\ChannelManagers;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use BeyondCode\LaravelWebSockets\Channels\Channel;
|
||||||
|
use BeyondCode\LaravelWebSockets\Channels\PresenceChannel;
|
||||||
|
use BeyondCode\LaravelWebSockets\Channels\PrivateChannel;
|
||||||
|
use React\Promise\FulfilledPromise;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
use stdClass;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use React\EventLoop\LoopInterface;
|
||||||
|
|
||||||
|
class LocalChannelManager implements ChannelManager
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The list of stored channels.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $channels = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of users that joined the presence channel.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $users = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new channel manager instance.
|
||||||
|
*
|
||||||
|
* @param LoopInterface $loop
|
||||||
|
* @param string|null $factoryClass
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(LoopInterface $loop, $factoryClass = null)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the channel by app & name.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string $channel
|
||||||
|
* @return null|BeyondCode\LaravelWebSockets\Channels\Channel
|
||||||
|
*/
|
||||||
|
public function find($appId, string $channel)
|
||||||
|
{
|
||||||
|
return $this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,548 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\ChannelManagers;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use BeyondCode\LaravelWebSockets\Channels\Channel;
|
||||||
|
use BeyondCode\LaravelWebSockets\Channels\PresenceChannel;
|
||||||
|
use BeyondCode\LaravelWebSockets\Channels\PrivateChannel;
|
||||||
|
use React\Promise\FulfilledPromise;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
use Clue\React\Redis\Client;
|
||||||
|
use Clue\React\Redis\Factory;
|
||||||
|
use stdClass;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use React\EventLoop\LoopInterface;
|
||||||
|
|
||||||
|
class RedisChannelManager extends LocalChannelManager
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The running loop.
|
||||||
|
*
|
||||||
|
* @var LoopInterface
|
||||||
|
*/
|
||||||
|
protected $loop;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unique server identifier.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $serverId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pub client.
|
||||||
|
*
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
protected $publishClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sub client.
|
||||||
|
*
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
protected $subscribeClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new channel manager instance.
|
||||||
|
*
|
||||||
|
* @param LoopInterface $loop
|
||||||
|
* @param string|null $factoryClass
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(LoopInterface $loop, $factoryClass = null)
|
||||||
|
{
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Channels;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use stdClass;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||||
|
|
||||||
|
class Channel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The channel name.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The connections that got subscribed to this channel.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $connections = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance.
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(string $name)
|
||||||
|
{
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Channels;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class PresenceChannel extends PrivateChannel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Subscribe to the channel.
|
||||||
|
*
|
||||||
|
* @see https://pusher.com/docs/pusher_protocol#presence-channel-events
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param \stdClass $payload
|
||||||
|
* @return void
|
||||||
|
* @throws InvalidSignature
|
||||||
|
*/
|
||||||
|
public function subscribe(ConnectionInterface $connection, stdClass $payload)
|
||||||
|
{
|
||||||
|
parent::subscribe($connection, $payload);
|
||||||
|
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Channels;
|
namespace BeyondCode\LaravelWebSockets\Channels;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\InvalidSignature;
|
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Console;
|
namespace BeyondCode\LaravelWebSockets\Console\Commands;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver;
|
use BeyondCode\LaravelWebSockets\Facades\StatisticsStore;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class CleanStatistics extends Command
|
class CleanStatistics extends Command
|
||||||
|
|
@ -14,6 +14,7 @@ class CleanStatistics extends Command
|
||||||
*/
|
*/
|
||||||
protected $signature = 'websockets:clean
|
protected $signature = 'websockets:clean
|
||||||
{appId? : (optional) The app id that will be cleaned.}
|
{appId? : (optional) The app id that will be cleaned.}
|
||||||
|
{--days= : Delete records older than this amount of days since now.}
|
||||||
';
|
';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -21,20 +22,23 @@ class CleanStatistics extends Command
|
||||||
*
|
*
|
||||||
* @var string|null
|
* @var string|null
|
||||||
*/
|
*/
|
||||||
protected $description = 'Clean up old statistics from the websocket log.';
|
protected $description = 'Clean up old statistics from the WebSocket statistics storage.';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the command.
|
* Run the command.
|
||||||
*
|
*
|
||||||
* @param \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver $driver
|
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function handle(StatisticsDriver $driver)
|
public function handle()
|
||||||
{
|
{
|
||||||
$this->comment('Cleaning WebSocket Statistics...');
|
$this->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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Console;
|
namespace BeyondCode\LaravelWebSockets\Console\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\InteractsWithTime;
|
use Illuminate\Support\InteractsWithTime;
|
||||||
|
|
||||||
class RestartWebSocketServer extends Command
|
class RestartServer extends Command
|
||||||
{
|
{
|
||||||
use InteractsWithTime;
|
use InteractsWithTime;
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ class RestartWebSocketServer extends Command
|
||||||
*
|
*
|
||||||
* @var string|null
|
* @var string|null
|
||||||
*/
|
*/
|
||||||
protected $description = 'Restart the Laravel WebSocket Server';
|
protected $description = 'Signal the WebSockets server to restart.';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the command.
|
* Run the command.
|
||||||
|
|
@ -31,8 +31,13 @@ class RestartWebSocketServer extends Command
|
||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
Cache::forever('beyondcode:websockets:restart', $this->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!'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,22 +1,20 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Console;
|
namespace BeyondCode\LaravelWebSockets\Console\Commands;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Facades\StatisticsLogger;
|
use BeyondCode\LaravelWebSockets\Server\Loggers\ConnectionLogger;
|
||||||
use BeyondCode\LaravelWebSockets\Facades\WebSocketsRouter;
|
use BeyondCode\LaravelWebSockets\Server\Loggers\HttpLogger;
|
||||||
use BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface;
|
use BeyondCode\LaravelWebSockets\Server\Loggers\WebSocketsLogger;
|
||||||
use BeyondCode\LaravelWebSockets\Server\Logger\ConnectionLogger;
|
use BeyondCode\LaravelWebSockets\ServerFactory;
|
||||||
use BeyondCode\LaravelWebSockets\Server\Logger\HttpLogger;
|
|
||||||
use BeyondCode\LaravelWebSockets\Server\Logger\WebsocketsLogger;
|
|
||||||
use BeyondCode\LaravelWebSockets\Server\WebSocketServerFactory;
|
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver;
|
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Logger\StatisticsLogger as StatisticsLoggerInterface;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use BeyondCode\LaravelWebSockets\Facades\WebSocketsRouter;
|
||||||
|
use BeyondCode\LaravelWebSockets\Facades\StatisticsCollector as StatisticsCollectorFacade;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\StatisticsCollector;
|
||||||
use React\EventLoop\Factory as LoopFactory;
|
use React\EventLoop\Factory as LoopFactory;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class StartWebSocketServer extends Command
|
class StartServer extends Command
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The name and signature of the console command.
|
* The name and signature of the console command.
|
||||||
|
|
@ -26,7 +24,8 @@ class StartWebSocketServer extends Command
|
||||||
protected $signature = 'websockets:serve
|
protected $signature = 'websockets:serve
|
||||||
{--host=0.0.0.0}
|
{--host=0.0.0.0}
|
||||||
{--port=6001}
|
{--port=6001}
|
||||||
{--statistics-interval= : Overwrite the statistics interval set in the config.}
|
{--disable-statistics : Disable the statistics tracking.}
|
||||||
|
{--statistics-interval= : The amount of seconds to tick between statistics saving.}
|
||||||
{--debug : Forces the loggers to be enabled and thereby overriding the APP_DEBUG setting.}
|
{--debug : Forces the loggers to be enabled and thereby overriding the APP_DEBUG setting.}
|
||||||
{--test : Prepare the server, but do not start it.}
|
{--test : Prepare the server, but do not start it.}
|
||||||
';
|
';
|
||||||
|
|
@ -36,7 +35,7 @@ class StartWebSocketServer extends Command
|
||||||
*
|
*
|
||||||
* @var string|null
|
* @var string|null
|
||||||
*/
|
*/
|
||||||
protected $description = 'Start the Laravel WebSocket Server';
|
protected $description = 'Start the LaravelWebSockets server.';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the loop instance.
|
* Get the loop instance.
|
||||||
|
|
@ -52,13 +51,6 @@ class StartWebSocketServer extends Command
|
||||||
*/
|
*/
|
||||||
public $server;
|
public $server;
|
||||||
|
|
||||||
/**
|
|
||||||
* Track the last restart.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $lastRestart;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the command.
|
* Initialize the command.
|
||||||
*
|
*
|
||||||
|
|
@ -78,96 +70,83 @@ class StartWebSocketServer extends Command
|
||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
$this
|
$this->configureLoggers();
|
||||||
->configureStatisticsLogger()
|
|
||||||
->configureHttpLogger()
|
$this->configureManagers();
|
||||||
->configureMessageLogger()
|
|
||||||
->configureConnectionLogger()
|
$this->configureStatistics();
|
||||||
->configureRestartTimer()
|
|
||||||
->configurePubSub()
|
$this->configureRestartTimer();
|
||||||
->registerRoutes()
|
|
||||||
->startWebSocketServer();
|
$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 () {
|
$this->configureHttpLogger();
|
||||||
$replicationDriver = config('websockets.replication.driver', 'local');
|
$this->configureMessageLogger();
|
||||||
|
$this->configureConnectionLogger();
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 () {
|
$this->laravel->singleton(ChannelManager::class, function () {
|
||||||
return (new HttpLogger($this->output))
|
$mode = config('websockets.replication.mode', 'local');
|
||||||
->enable($this->option('debug') ?: config('app.debug'))
|
|
||||||
->verbose($this->output->isVerbose());
|
|
||||||
});
|
|
||||||
|
|
||||||
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 () {
|
$this->laravel->singleton(StatisticsCollector::class, function () {
|
||||||
return (new WebsocketsLogger($this->output))
|
$replicationMode = config('websockets.replication.mode', 'local');
|
||||||
->enable($this->option('debug') ?: config('app.debug'))
|
|
||||||
->verbose($this->output->isVerbose());
|
$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
|
* @return void
|
||||||
*/
|
|
||||||
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
|
|
||||||
*/
|
*/
|
||||||
public function configureRestartTimer()
|
public function configureRestartTimer()
|
||||||
{
|
{
|
||||||
|
|
@ -178,45 +157,48 @@ class StartWebSocketServer extends Command
|
||||||
$this->loop->stop();
|
$this->loop->stop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure the replicators.
|
* Configure the HTTP logger class.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function configurePubSub()
|
protected function configureHttpLogger()
|
||||||
{
|
{
|
||||||
$this->laravel->singleton(ReplicationInterface::class, function () {
|
$this->laravel->singleton(HttpLogger::class, function () {
|
||||||
$driver = config('websockets.replication.driver', 'local');
|
return (new HttpLogger($this->output))
|
||||||
|
->enable($this->option('debug') ?: config('app.debug'))
|
||||||
$client = config(
|
->verbose($this->output->isVerbose());
|
||||||
"websockets.replication.{$driver}.client",
|
|
||||||
\BeyondCode\LaravelWebSockets\PubSub\Drivers\LocalClient::class
|
|
||||||
);
|
|
||||||
|
|
||||||
return (new $client)->boot($this->loop);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$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
|
* @return void
|
||||||
*/
|
*/
|
||||||
protected function startWebSocketServer()
|
protected function startServer()
|
||||||
{
|
{
|
||||||
$this->info("Starting the WebSocket server on port {$this->option('port')}...");
|
$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();
|
$this->server->run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -249,13 +230,13 @@ class StartWebSocketServer extends Command
|
||||||
*/
|
*/
|
||||||
protected function buildServer()
|
protected function buildServer()
|
||||||
{
|
{
|
||||||
$this->server = new WebSocketServerFactory(
|
$this->server = new ServerFactory(
|
||||||
$this->option('host'), $this->option('port')
|
$this->option('host'), $this->option('port')
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->server = $this->server
|
$this->server = $this->server
|
||||||
->setLoop($this->loop)
|
->setLoop($this->loop)
|
||||||
->useRoutes(WebSocketsRouter::getRoutes())
|
->withRoutes(WebSocketsRouter::getRoutes())
|
||||||
->setConsoleOutput($this->output)
|
->setConsoleOutput($this->output)
|
||||||
->createServer();
|
->createServer();
|
||||||
}
|
}
|
||||||
|
|
@ -267,6 +248,8 @@ class StartWebSocketServer extends Command
|
||||||
*/
|
*/
|
||||||
protected function getLastRestart()
|
protected function getLastRestart()
|
||||||
{
|
{
|
||||||
return Cache::get('beyondcode:websockets:restart', 0);
|
return Cache::get(
|
||||||
|
'beyondcode:websockets:restart', 0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Apps;
|
namespace BeyondCode\LaravelWebSockets\Contracts;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||||
|
|
||||||
interface AppManager
|
interface AppManager
|
||||||
{
|
{
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Contracts;
|
||||||
|
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
use stdClass;
|
||||||
|
use React\EventLoop\LoopInterface;
|
||||||
|
|
||||||
|
interface ChannelManager
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new channel manager instance.
|
||||||
|
*
|
||||||
|
* @param LoopInterface $loop
|
||||||
|
* @param string|null $factoryClass
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(LoopInterface $loop, $factoryClass = null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the channel by app & name.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string $channel
|
||||||
|
* @return null|BeyondCode\LaravelWebSockets\Channels\Channel
|
||||||
|
*/
|
||||||
|
public function find($appId, string $channel);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all channels for a specific app
|
||||||
|
* across multiple servers.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @return \React\Promise\PromiseInterface[array]
|
||||||
|
*/
|
||||||
|
public function getGlobalChannels($appId): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove connection from all channels.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function unsubscribeFromAllChannels(ConnectionInterface $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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe the connection to a specific channel.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function subscribeToApp($appId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe the connection from the channel.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the presence channel members.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string $channel
|
||||||
|
* @return \React\Promise\PromiseInterface
|
||||||
|
*/
|
||||||
|
public function getChannelMembers($appId, string $channel): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Messages;
|
namespace BeyondCode\LaravelWebSockets\Contracts;
|
||||||
|
|
||||||
interface PusherMessage
|
interface PusherMessage
|
||||||
{
|
{
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Contracts;
|
||||||
|
|
||||||
|
use React\Promise\FulfilledPromise;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
|
||||||
|
interface StatisticsCollector
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the incoming websocket message.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function webSocketMessage($appId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the incoming API message.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function apiMessage($appId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the new conection.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function connection($appId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle disconnections.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function disconnection($appId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save all the stored statistics.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function save();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush the stored statistics.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function flush();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the saved statistics.
|
||||||
|
*
|
||||||
|
* @return PromiseInterface[array]
|
||||||
|
*/
|
||||||
|
public function getStatistics(): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the saved statistics for an app.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @return PromiseInterface[\BeyondCode\LaravelWebSockets\Statistics\Statistic|null]
|
||||||
|
*/
|
||||||
|
public function getAppStatistics($appId): PromiseInterface;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Contracts;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
interface StatisticsStore
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Store a new record in the database and return
|
||||||
|
* the created instance.
|
||||||
|
*
|
||||||
|
* @param array $data
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public static function store(array $data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete records older than the given moment,
|
||||||
|
* for a specific app id (if given), returning
|
||||||
|
* the amount of deleted records.
|
||||||
|
*
|
||||||
|
* @param \Carbon\Carbon $moment
|
||||||
|
* @param string|int|null $appId
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public static function delete(Carbon $moment, $appId = null): int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the query result as eloquent collection.
|
||||||
|
*
|
||||||
|
* @param callable $processQuery
|
||||||
|
* @return \Illuminate\Support\Collection
|
||||||
|
*/
|
||||||
|
public function getRawRecords(callable $processQuery = null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,7 @@ class AuthenticateDashboard
|
||||||
*/
|
*/
|
||||||
public function __invoke(Request $request)
|
public function __invoke(Request $request)
|
||||||
{
|
{
|
||||||
$app = App::findById($request->header('x-app-id'));
|
$app = App::findById($request->header('X-App-Id'));
|
||||||
|
|
||||||
$broadcaster = $this->getPusherBroadcaster([
|
$broadcaster = $this->getPusherBroadcaster([
|
||||||
'key' => $app->key,
|
'key' => $app->key,
|
||||||
|
|
|
||||||
|
|
@ -2,51 +2,53 @@
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Concerns\PushesToPusher;
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Rules\AppId;
|
use BeyondCode\LaravelWebSockets\Rules\AppId;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class SendMessage
|
class SendMessage
|
||||||
{
|
{
|
||||||
use PushesToPusher;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send the message to the requested channel.
|
* Send the message to the requested channel.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \BeyondCode\LaravelWebSockets\Contracts\ChannelManager $channelManager
|
||||||
* @return \Illuminate\Http\Response
|
* @return \Illuminate\Http\Response
|
||||||
*/
|
*/
|
||||||
public function __invoke(Request $request)
|
public function __invoke(Request $request, ChannelManager $channelManager)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'appId' => ['required', new AppId],
|
'appId' => ['required', new AppId],
|
||||||
'key' => 'required|string',
|
|
||||||
'secret' => 'required|string',
|
|
||||||
'channel' => 'required|string',
|
'channel' => 'required|string',
|
||||||
'event' => 'required|string',
|
'event' => 'required|string',
|
||||||
'data' => 'required|json',
|
'data' => 'required|json',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$broadcaster = $this->getPusherBroadcaster([
|
$payload = [
|
||||||
'key' => $request->key,
|
'channel' => $request->channel,
|
||||||
'secret' => $request->secret,
|
'event' => $request->event,
|
||||||
'id' => $request->appId,
|
'data' => json_decode($request->data, true),
|
||||||
]);
|
];
|
||||||
|
|
||||||
try {
|
// Here you can use the ->find(), even if the channel
|
||||||
$decodedData = @json_decode($request->data, true);
|
// 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(
|
if ($channel) {
|
||||||
[$request->channel],
|
$channel->broadcastToEveryoneExcept(
|
||||||
$request->event,
|
(object) $payload,
|
||||||
$decodedData ?: []
|
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([
|
return response()->json([
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Apps\AppManager;
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
use BeyondCode\LaravelWebSockets\Dashboard\DashboardLogger;
|
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class ShowDashboard
|
class ShowDashboard
|
||||||
|
|
@ -12,7 +12,7 @@ class ShowDashboard
|
||||||
* Show the dashboard.
|
* Show the dashboard.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
* @param \BeyondCode\LaravelWebSockets\Apps\AppManager $apps
|
* @param \BeyondCode\LaravelWebSockets\Contracts\AppManager $apps
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function __invoke(Request $request, AppManager $apps)
|
public function __invoke(Request $request, AppManager $apps)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver;
|
use BeyondCode\LaravelWebSockets\Facades\StatisticsStore;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class ShowStatistics
|
class ShowStatistics
|
||||||
|
|
@ -11,12 +11,23 @@ class ShowStatistics
|
||||||
* Get statistics for an app ID.
|
* Get statistics for an app ID.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
* @param \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver $driver
|
|
||||||
* @param mixed $appId
|
* @param mixed $appId
|
||||||
* @return \Illuminate\Http\Response
|
* @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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Dashboard;
|
namespace BeyondCode\LaravelWebSockets;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
|
|
||||||
class DashboardLogger
|
class DashboardLogger
|
||||||
{
|
{
|
||||||
|
|
@ -12,7 +12,6 @@ class DashboardLogger
|
||||||
|
|
||||||
const TYPE_CONNECTED = 'connected';
|
const TYPE_CONNECTED = 'connected';
|
||||||
|
|
||||||
const TYPE_VACATED = 'vacated';
|
|
||||||
|
|
||||||
const TYPE_OCCUPIED = 'occupied';
|
const TYPE_OCCUPIED = 'occupied';
|
||||||
|
|
||||||
|
|
@ -42,7 +41,6 @@ class DashboardLogger
|
||||||
public static $channels = [
|
public static $channels = [
|
||||||
self::TYPE_DISCONNECTED,
|
self::TYPE_DISCONNECTED,
|
||||||
self::TYPE_CONNECTED,
|
self::TYPE_CONNECTED,
|
||||||
self::TYPE_VACATED,
|
|
||||||
self::TYPE_OCCUPIED,
|
self::TYPE_OCCUPIED,
|
||||||
self::TYPE_SUBSCRIBED,
|
self::TYPE_SUBSCRIBED,
|
||||||
self::TYPE_WS_MESSAGE,
|
self::TYPE_WS_MESSAGE,
|
||||||
|
|
@ -65,18 +63,36 @@ class DashboardLogger
|
||||||
*/
|
*/
|
||||||
public static function log($appId, string $type, array $details = [])
|
public static function log($appId, string $type, array $details = [])
|
||||||
{
|
{
|
||||||
|
$channelManager = app(ChannelManager::class);
|
||||||
|
|
||||||
$channelName = static::LOG_CHANNEL_PREFIX.$type;
|
$channelName = static::LOG_CHANNEL_PREFIX.$type;
|
||||||
|
|
||||||
$channel = app(ChannelManager::class)->find($appId, $channelName);
|
$payload = [
|
||||||
|
|
||||||
optional($channel)->broadcast([
|
|
||||||
'event' => 'log-message',
|
|
||||||
'channel' => $channelName,
|
'channel' => $channelName,
|
||||||
|
'event' => 'log-message',
|
||||||
'data' => [
|
'data' => [
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
'time' => strftime('%H:%M:%S'),
|
'time' => strftime('%H:%M:%S'),
|
||||||
'details' => $details,
|
'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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Events;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class MessagesBroadcasted
|
|
||||||
{
|
|
||||||
use Dispatchable, SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The amount of messages sent.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $sentMessagesCount;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the event.
|
|
||||||
*
|
|
||||||
* @param int $sentMessagesCount
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct(int $sentMessagesCount = 0)
|
|
||||||
{
|
|
||||||
$this->sentMessagesCount = $sentMessagesCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Events;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Ratchet\ConnectionInterface;
|
|
||||||
|
|
||||||
class Subscribed
|
|
||||||
{
|
|
||||||
use Dispatchable, SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The channel name the user has subscribed to.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $channelName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The connection that initiated the subscription.
|
|
||||||
*
|
|
||||||
* @var \Ratchet\ConnectionInterface
|
|
||||||
*/
|
|
||||||
protected $connection;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the event.
|
|
||||||
*
|
|
||||||
* @param string $channelName
|
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct(string $channelName, ConnectionInterface $connection)
|
|
||||||
{
|
|
||||||
$this->channelName = $channelName;
|
|
||||||
$this->connection = $connection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Events;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Events\Dispatchable;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Ratchet\ConnectionInterface;
|
|
||||||
|
|
||||||
class Unsubscribed
|
|
||||||
{
|
|
||||||
use Dispatchable, SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The channel name the user has unsubscribed from.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $channelName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The connection that initiated the unsubscription.
|
|
||||||
*
|
|
||||||
* @var \Ratchet\ConnectionInterface
|
|
||||||
*/
|
|
||||||
protected $connection;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the event.
|
|
||||||
*
|
|
||||||
* @param string $channelName
|
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct(string $channelName, ConnectionInterface $connection)
|
|
||||||
{
|
|
||||||
$this->channelName = $channelName;
|
|
||||||
$this->connection = $connection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Facades;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\StatisticsCollector as StatisticsCollectorInterface;
|
||||||
|
|
||||||
|
class StatisticsCollector extends Facade
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the registered name of the component.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected static function getFacadeAccessor()
|
||||||
|
{
|
||||||
|
return StatisticsCollectorInterface::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Facades;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Logger\StatisticsLogger as StatisticsLoggerInterface;
|
|
||||||
use Illuminate\Support\Facades\Facade;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see \BeyondCode\LaravelWebSockets\Statistics\Logger\MemoryStatisticsLogger
|
|
||||||
* @mixin \BeyondCode\LaravelWebSockets\Statistics\Logger\MemoryStatisticsLogger
|
|
||||||
*/
|
|
||||||
class StatisticsLogger extends Facade
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Get the registered name of the component.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
protected static function getFacadeAccessor()
|
|
||||||
{
|
|
||||||
return StatisticsLoggerInterface::class;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Facades;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\StatisticsStore as StatisticsStoreInterface;
|
||||||
|
|
||||||
|
class StatisticsStore extends Facade
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the registered name of the component.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected static function getFacadeAccessor()
|
||||||
|
{
|
||||||
|
return StatisticsStoreInterface::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,10 +4,6 @@ namespace BeyondCode\LaravelWebSockets\Facades;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Facade;
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
|
||||||
/**
|
|
||||||
* @see \BeyondCode\LaravelWebSockets\Server\Router
|
|
||||||
* @mixin \BeyondCode\LaravelWebSockets\Server\Router
|
|
||||||
*/
|
|
||||||
class WebSocketsRouter extends Facade
|
class WebSocketsRouter extends Facade
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\HttpApi\Controllers;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
||||||
|
|
||||||
class FetchChannelController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Handle the incoming request.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @return \Illuminate\Http\Response
|
|
||||||
*/
|
|
||||||
public function __invoke(Request $request)
|
|
||||||
{
|
|
||||||
$channel = $this->channelManager->find($request->appId, $request->channelName);
|
|
||||||
|
|
||||||
if (is_null($channel)) {
|
|
||||||
throw new HttpException(404, "Unknown channel `{$request->channelName}`.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return $channel->toArray($request->appId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\HttpApi\Controllers;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\PresenceChannel;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use stdClass;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
||||||
|
|
||||||
class FetchChannelsController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Handle the incoming request.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @return \Illuminate\Http\Response
|
|
||||||
*/
|
|
||||||
public function __invoke(Request $request)
|
|
||||||
{
|
|
||||||
$attributes = [];
|
|
||||||
|
|
||||||
if ($request->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,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\HttpApi\Controllers;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\PresenceChannel;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
||||||
|
|
||||||
class FetchUsersController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Handle the incoming request.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @return \Illuminate\Http\Response
|
|
||||||
*/
|
|
||||||
public function __invoke(Request $request)
|
|
||||||
{
|
|
||||||
$channel = $this->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(),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\HttpApi\Controllers;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Dashboard\DashboardLogger;
|
|
||||||
use BeyondCode\LaravelWebSockets\Facades\StatisticsLogger;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class TriggerEventController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Handle the incoming request.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @return \Illuminate\Http\Response
|
|
||||||
*/
|
|
||||||
public function __invoke(Request $request)
|
|
||||||
{
|
|
||||||
$this->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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Statistics\Models;
|
namespace BeyondCode\LaravelWebSockets\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
|
@ -9,10 +9,10 @@ class WebSocketsStatisticsEntry extends Model
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
protected $guarded = [];
|
protected $table = 'websockets_statistics_entries';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
protected $table = 'websockets_statistics_entries';
|
protected $guarded = [];
|
||||||
}
|
}
|
||||||
|
|
@ -1,184 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\PubSub\Drivers;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface;
|
|
||||||
use React\EventLoop\LoopInterface;
|
|
||||||
use React\Promise\FulfilledPromise;
|
|
||||||
use React\Promise\PromiseInterface;
|
|
||||||
use stdClass;
|
|
||||||
|
|
||||||
class LocalClient implements ReplicationInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Mapping of the presence JSON data for users in each channel.
|
|
||||||
*
|
|
||||||
* @var string[][]
|
|
||||||
*/
|
|
||||||
protected $channelData = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Boot the pub/sub provider (open connections, initial subscriptions, etc).
|
|
||||||
*
|
|
||||||
* @param LoopInterface $loop
|
|
||||||
* @param string|null $factoryClass
|
|
||||||
* @return self
|
|
||||||
*/
|
|
||||||
public function boot(LoopInterface $loop, $factoryClass = null): ReplicationInterface
|
|
||||||
{
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Publish a payload on a specific channel, for a specific app.
|
|
||||||
*
|
|
||||||
* @param string $appId
|
|
||||||
* @param string $channel
|
|
||||||
* @param stdClass $payload
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function publish($appId, string $channel, stdClass $payload): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to receive messages for a channel.
|
|
||||||
*
|
|
||||||
* @param string $appId
|
|
||||||
* @param string $channel
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function subscribe($appId, string $channel): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unsubscribe from a channel.
|
|
||||||
*
|
|
||||||
* @param string $appId
|
|
||||||
* @param string $channel
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function unsubscribe($appId, string $channel): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to the app's pubsub keyspace.
|
|
||||||
*
|
|
||||||
* @param mixed $appId
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function subscribeToApp($appId): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unsubscribe from the app's pubsub keyspace.
|
|
||||||
*
|
|
||||||
* @param mixed $appId
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function unsubscribeFromApp($appId): bool
|
|
||||||
{
|
|
||||||
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->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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,437 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\PubSub\Drivers;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Dashboard\DashboardLogger;
|
|
||||||
use BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
|
||||||
use Clue\React\Redis\Client;
|
|
||||||
use Clue\React\Redis\Factory;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use React\EventLoop\LoopInterface;
|
|
||||||
use React\Promise\PromiseInterface;
|
|
||||||
use stdClass;
|
|
||||||
|
|
||||||
class RedisClient extends LocalClient
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The running loop.
|
|
||||||
*
|
|
||||||
* @var LoopInterface
|
|
||||||
*/
|
|
||||||
protected $loop;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The unique server identifier.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $serverId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The pub client.
|
|
||||||
*
|
|
||||||
* @var Client
|
|
||||||
*/
|
|
||||||
protected $publishClient;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The sub client.
|
|
||||||
*
|
|
||||||
* @var Client
|
|
||||||
*/
|
|
||||||
protected $subscribeClient;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapping of subscribed channels, where the key is the channel name,
|
|
||||||
* and the value is the amount of connections which are subscribed to
|
|
||||||
* that channel. Used to keep track of whether we still need to stay
|
|
||||||
* subscribed to those channels with Redis.
|
|
||||||
*
|
|
||||||
* @var int[]
|
|
||||||
*/
|
|
||||||
protected $subscribedChannels = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new Redis client.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\PubSub;
|
|
||||||
|
|
||||||
use React\EventLoop\LoopInterface;
|
|
||||||
use React\Promise\PromiseInterface;
|
|
||||||
use stdClass;
|
|
||||||
|
|
||||||
interface ReplicationInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Boot the pub/sub provider (open connections, initial subscriptions, etc).
|
|
||||||
*
|
|
||||||
* @param LoopInterface $loop
|
|
||||||
* @param string|null $factoryClass
|
|
||||||
* @return self
|
|
||||||
*/
|
|
||||||
public function boot(LoopInterface $loop, $factoryClass = null): self;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Publish a payload on a specific channel, for a specific app.
|
|
||||||
*
|
|
||||||
* @param string $appId
|
|
||||||
* @param string $channel
|
|
||||||
* @param stdClass $payload
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function publish($appId, string $channel, stdClass $payload): bool;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to receive messages for a channel.
|
|
||||||
*
|
|
||||||
* @param string $appId
|
|
||||||
* @param string $channel
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function subscribe($appId, string $channel): bool;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unsubscribe from a channel.
|
|
||||||
*
|
|
||||||
* @param string $appId
|
|
||||||
* @param string $channel
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function unsubscribe($appId, string $channel): bool;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to the app's pubsub keyspace.
|
|
||||||
*
|
|
||||||
* @param mixed $appId
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function subscribeToApp($appId): bool;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unsubscribe from the app's pubsub keyspace.
|
|
||||||
*
|
|
||||||
* @param mixed $appId
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function unsubscribeFromApp($appId): bool;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the amount of unique connections.
|
|
||||||
*
|
|
||||||
* @param mixed $appId
|
|
||||||
* @return null|int
|
|
||||||
*/
|
|
||||||
public function getLocalConnectionsCount($appId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the amount of connections aggregated on multiple instances.
|
|
||||||
*
|
|
||||||
* @param mixed $appId
|
|
||||||
* @return null|int|\React\Promise\PromiseInterface
|
|
||||||
*/
|
|
||||||
public function getGlobalConnectionsCount($appId);
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Statistics\Rules;
|
namespace BeyondCode\LaravelWebSockets\Rules;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Apps\AppManager;
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
use Illuminate\Contracts\Validation\Rule;
|
use Illuminate\Contracts\Validation\Rule;
|
||||||
|
|
||||||
class AppId implements Rule
|
class AppId implements Rule
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Exceptions;
|
namespace BeyondCode\LaravelWebSockets\Server\Exceptions;
|
||||||
|
|
||||||
class ConnectionsOverCapacity extends WebSocketException
|
class ConnectionsOverCapacity extends WebSocketException
|
||||||
{
|
{
|
||||||
|
|
@ -12,7 +12,6 @@ class ConnectionsOverCapacity extends WebSocketException
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->message = 'Over capacity';
|
$this->trigger("Over capacity", 4100);
|
||||||
$this->code = 4100;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Exceptions;
|
namespace BeyondCode\LaravelWebSockets\Server\Exceptions;
|
||||||
|
|
||||||
class InvalidSignature extends WebSocketException
|
class InvalidSignature extends WebSocketException
|
||||||
{
|
{
|
||||||
|
|
@ -12,7 +12,6 @@ class InvalidSignature extends WebSocketException
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->message = 'Invalid Signature';
|
$this->trigger("Invalid Signature", 4009);
|
||||||
$this->code = 4009;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Server\Exceptions;
|
||||||
|
|
||||||
|
class OriginNotAllowed extends WebSocketException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Initalize the exception.
|
||||||
|
*
|
||||||
|
* @param string $appKey
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($appKey)
|
||||||
|
{
|
||||||
|
$this->trigger("The origin is not allowed for `{$appKey}`.", 4009);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Server\Exceptions;
|
||||||
|
|
||||||
|
class UnknownAppKey extends WebSocketException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Initalize the exception.
|
||||||
|
*
|
||||||
|
* @param string $appKey
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($appKey)
|
||||||
|
{
|
||||||
|
$this->trigger("Could not find app key `{$appKey}`.", 4001);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Exceptions;
|
namespace BeyondCode\LaravelWebSockets\Server\Exceptions;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
|
|
@ -21,4 +21,17 @@ class WebSocketException extends Exception
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger the exception message.
|
||||||
|
*
|
||||||
|
* @param string $message
|
||||||
|
* @param int $code
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function trigger(string $message, int $code = 4001)
|
||||||
|
{
|
||||||
|
$this->message = $message;
|
||||||
|
$this->code = $code;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,8 +3,9 @@
|
||||||
namespace BeyondCode\LaravelWebSockets\Server;
|
namespace BeyondCode\LaravelWebSockets\Server;
|
||||||
|
|
||||||
use Ratchet\Http\HttpServerInterface;
|
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.
|
* Create a new server instance.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Server\Logger;
|
namespace BeyondCode\LaravelWebSockets\Server\Loggers;
|
||||||
|
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@ class ConnectionLogger extends Logger implements ConnectionInterface
|
||||||
/**
|
/**
|
||||||
* Send data through the connection.
|
* Send data through the connection.
|
||||||
*
|
*
|
||||||
* @param mixed $data
|
* @param string $data
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function send($data)
|
public function send($data)
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Server\Logger;
|
namespace BeyondCode\LaravelWebSockets\Server\Loggers;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Server\Logger;
|
namespace BeyondCode\LaravelWebSockets\Server\Loggers;
|
||||||
|
|
||||||
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
|
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
@ -35,7 +35,9 @@ class Logger
|
||||||
*/
|
*/
|
||||||
public static function isEnabled(): bool
|
public static function isEnabled(): bool
|
||||||
{
|
{
|
||||||
return app(WebsocketsLogger::class)->enabled;
|
$logger = app(WebSocketsLogger::class);
|
||||||
|
|
||||||
|
return $logger->enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Server\Logger;
|
namespace BeyondCode\LaravelWebSockets\Server\Loggers;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\QueryParameters;
|
use BeyondCode\LaravelWebSockets\Server\QueryParameters;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use Ratchet\RFC6455\Messaging\MessageInterface;
|
use Ratchet\RFC6455\Messaging\MessageInterface;
|
||||||
use Ratchet\WebSocket\MessageComponentInterface;
|
use Ratchet\WebSocket\MessageComponentInterface;
|
||||||
|
|
||||||
class WebsocketsLogger extends Logger implements MessageComponentInterface
|
class WebSocketsLogger extends Logger implements MessageComponentInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The HTTP app instance to watch.
|
* The HTTP app instance to watch.
|
||||||
|
|
@ -1,51 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Messages;
|
namespace BeyondCode\LaravelWebSockets\Server\Messages;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\PusherMessage;
|
||||||
|
|
||||||
class PusherChannelProtocolMessage implements PusherMessage
|
class PusherChannelProtocolMessage extends PusherClientMessage
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* The payload to send.
|
|
||||||
*
|
|
||||||
* @var \stdClass
|
|
||||||
*/
|
|
||||||
protected $payload;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The socket connection.
|
|
||||||
*
|
|
||||||
* @var \Ratchet\ConnectionInterface
|
|
||||||
*/
|
|
||||||
protected $connection;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The channel manager.
|
|
||||||
*
|
|
||||||
* @var ChannelManager
|
|
||||||
*/
|
|
||||||
protected $channelManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new instance.
|
|
||||||
*
|
|
||||||
* @param \stdClass $payload
|
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
|
||||||
* @param ChannelManager $channelManager
|
|
||||||
*/
|
|
||||||
public function __construct(stdClass $payload, ConnectionInterface $connection, ChannelManager $channelManager)
|
|
||||||
{
|
|
||||||
$this->payload = $payload;
|
|
||||||
|
|
||||||
$this->connection = $connection;
|
|
||||||
|
|
||||||
$this->channelManager = $channelManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Respond with the payload.
|
* Respond with the payload.
|
||||||
*
|
*
|
||||||
|
|
@ -84,9 +48,7 @@ class PusherChannelProtocolMessage implements PusherMessage
|
||||||
*/
|
*/
|
||||||
protected function subscribe(ConnectionInterface $connection, stdClass $payload)
|
protected function subscribe(ConnectionInterface $connection, stdClass $payload)
|
||||||
{
|
{
|
||||||
$channel = $this->channelManager->findOrCreate($connection->app->id, $payload->channel);
|
$this->channelManager->subscribeToChannel($connection, $payload->channel, $payload);
|
||||||
|
|
||||||
$channel->subscribe($connection, $payload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -98,8 +60,6 @@ class PusherChannelProtocolMessage implements PusherMessage
|
||||||
*/
|
*/
|
||||||
public function unsubscribe(ConnectionInterface $connection, stdClass $payload)
|
public function unsubscribe(ConnectionInterface $connection, stdClass $payload)
|
||||||
{
|
{
|
||||||
$channel = $this->channelManager->findOrCreate($connection->app->id, $payload->channel);
|
$this->channelManager->unsubscribeFromChannel($connection, $payload->channel, $payload);
|
||||||
|
|
||||||
$channel->unsubscribe($connection);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Messages;
|
namespace BeyondCode\LaravelWebSockets\Server\Messages;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Dashboard\DashboardLogger;
|
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\PusherMessage;
|
||||||
|
|
||||||
class PusherClientMessage implements PusherMessage
|
class PusherClientMessage implements PusherMessage
|
||||||
{
|
{
|
||||||
|
|
@ -60,6 +61,14 @@ class PusherClientMessage implements PusherMessage
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$channel = $this->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, [
|
DashboardLogger::log($this->connection->app->id, DashboardLogger::TYPE_WS_MESSAGE, [
|
||||||
'socketId' => $this->connection->socketId,
|
'socketId' => $this->connection->socketId,
|
||||||
'channel' => $this->payload->channel,
|
'channel' => $this->payload->channel,
|
||||||
|
|
@ -67,8 +76,5 @@ class PusherClientMessage implements PusherMessage
|
||||||
'data' => $this->payload,
|
'data' => $this->payload,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$channel = $this->channelManager->find($this->connection->app->id, $this->payload->channel);
|
|
||||||
|
|
||||||
optional($channel)->broadcastToOthers($this->connection, $this->payload);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Messages;
|
namespace BeyondCode\LaravelWebSockets\Server\Messages;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use Ratchet\RFC6455\Messaging\MessageInterface;
|
use Ratchet\RFC6455\Messaging\MessageInterface;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\PusherMessage;
|
||||||
|
|
||||||
class PusherMessageFactory
|
class PusherMessageFactory
|
||||||
{
|
{
|
||||||
|
|
@ -14,7 +15,7 @@ class PusherMessageFactory
|
||||||
*
|
*
|
||||||
* @param \Ratchet\RFC6455\Messaging\MessageInterface $message
|
* @param \Ratchet\RFC6455\Messaging\MessageInterface $message
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
* @param \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager $channelManager
|
* @param \BeyondCode\LaravelWebSockets\Contracts\ChannelManager $channelManager
|
||||||
* @return PusherMessage
|
* @return PusherMessage
|
||||||
*/
|
*/
|
||||||
public static function createForMessage(
|
public static function createForMessage(
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets;
|
namespace BeyondCode\LaravelWebSockets\Server;
|
||||||
|
|
||||||
use Psr\Http\Message\RequestInterface;
|
use Psr\Http\Message\RequestInterface;
|
||||||
|
|
||||||
|
|
@ -2,13 +2,7 @@
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Server;
|
namespace BeyondCode\LaravelWebSockets\Server;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Exceptions\InvalidWebSocketController;
|
use BeyondCode\LaravelWebSockets\Server\Loggers\WebSocketsLogger;
|
||||||
use BeyondCode\LaravelWebSockets\HttpApi\Controllers\FetchChannelController;
|
|
||||||
use BeyondCode\LaravelWebSockets\HttpApi\Controllers\FetchChannelsController;
|
|
||||||
use BeyondCode\LaravelWebSockets\HttpApi\Controllers\FetchUsersController;
|
|
||||||
use BeyondCode\LaravelWebSockets\HttpApi\Controllers\TriggerEventController;
|
|
||||||
use BeyondCode\LaravelWebSockets\Server\Logger\WebsocketsLogger;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\WebSocketHandler;
|
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Ratchet\WebSocket\MessageComponentInterface;
|
use Ratchet\WebSocket\MessageComponentInterface;
|
||||||
use Ratchet\WebSocket\WsServer;
|
use Ratchet\WebSocket\WsServer;
|
||||||
|
|
@ -21,16 +15,9 @@ class Router
|
||||||
* The implemented routes.
|
* The implemented routes.
|
||||||
*
|
*
|
||||||
* @var \Symfony\Component\Routing\RouteCollection
|
* @var \Symfony\Component\Routing\RouteCollection
|
||||||
*/
|
*/
|
||||||
protected $routes;
|
protected $routes;
|
||||||
|
|
||||||
/**
|
|
||||||
* The custom routes defined by the user.
|
|
||||||
*
|
|
||||||
* @var \Symfony\Component\Routing\RouteCollection
|
|
||||||
*/
|
|
||||||
protected $customRoutes;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the class.
|
* Initialize the class.
|
||||||
*
|
*
|
||||||
|
|
@ -39,7 +26,6 @@ class Router
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->routes = new RouteCollection;
|
$this->routes = new RouteCollection;
|
||||||
$this->customRoutes = new Collection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -53,22 +39,17 @@ class Router
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the routes.
|
* Register the default routes.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function routes()
|
public function routes()
|
||||||
{
|
{
|
||||||
$this->get('/app/{appKey}', config('websockets.handlers.websocket', WebSocketHandler::class));
|
$this->get('/app/{appKey}', config('websockets.handlers.websocket'));
|
||||||
|
$this->post('/apps/{appId}/events', config('websockets.handlers.trigger_event'));
|
||||||
$this->post('/apps/{appId}/events', config('websockets.handlers.trigger_event', TriggerEventController::class));
|
$this->get('/apps/{appId}/channels', config('websockets.handlers.fetch_channels'));
|
||||||
$this->get('/apps/{appId}/channels', config('websockets.handlers.fetch_channels', FetchChannelsController::class));
|
$this->get('/apps/{appId}/channels/{channelName}', config('websockets.handlers.fetch_channel'));
|
||||||
$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'));
|
||||||
$this->get('/apps/{appId}/channels/{channelName}/users', config('websockets.handlers.fetch_users', FetchUsersController::class));
|
|
||||||
|
|
||||||
$this->customRoutes->each(function ($action, $uri) {
|
|
||||||
$this->get($uri, $action);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -131,23 +112,6 @@ class Router
|
||||||
$this->addRoute('DELETE', $uri, $action);
|
$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.
|
* Add a new route to the list.
|
||||||
*
|
*
|
||||||
|
|
@ -171,12 +135,6 @@ class Router
|
||||||
*/
|
*/
|
||||||
protected function getRoute(string $method, string $uri, $action): Route
|
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)
|
$action = is_subclass_of($action, MessageComponentInterface::class)
|
||||||
? $this->createWebSocketsServer($action)
|
? $this->createWebSocketsServer($action)
|
||||||
: app($action);
|
: app($action);
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,34 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets;
|
namespace BeyondCode\LaravelWebSockets\Server;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||||
use BeyondCode\LaravelWebSockets\Dashboard\DashboardLogger;
|
|
||||||
use BeyondCode\LaravelWebSockets\Facades\StatisticsLogger;
|
|
||||||
use BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface;
|
|
||||||
use BeyondCode\LaravelWebSockets\QueryParameters;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\ConnectionsOverCapacity;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\OriginNotAllowed;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\UnknownAppKey;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\WebSocketException;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Messages\PusherMessageFactory;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Ratchet\WebSocket\MessageComponentInterface;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use Ratchet\RFC6455\Messaging\MessageInterface;
|
use Ratchet\RFC6455\Messaging\MessageInterface;
|
||||||
use Ratchet\WebSocket\MessageComponentInterface;
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
use React\Promise\PromiseInterface;
|
use BeyondCode\LaravelWebSockets\Facades\StatisticsCollector;
|
||||||
|
|
||||||
class WebSocketHandler implements MessageComponentInterface
|
class WebSocketHandler implements MessageComponentInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The channel manager.
|
* The channel manager.
|
||||||
*
|
*
|
||||||
* @var \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager
|
* @var ChannelManager
|
||||||
*/
|
*/
|
||||||
protected $channelManager;
|
protected $channelManager;
|
||||||
|
|
||||||
/**
|
|
||||||
* The replicator client.
|
|
||||||
*
|
|
||||||
* @var ReplicationInterface
|
|
||||||
*/
|
|
||||||
protected $replicator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize a new handler.
|
* Initialize a new handler.
|
||||||
*
|
*
|
||||||
* @param \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager $channelManager
|
* @param \BeyondCode\LaravelWebSockets\Contracts\ChannelManager $channelManager
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function __construct(ChannelManager $channelManager)
|
public function __construct(ChannelManager $channelManager)
|
||||||
{
|
{
|
||||||
$this->channelManager = $channelManager;
|
$this->channelManager = $channelManager;
|
||||||
$this->replicator = app(ReplicationInterface::class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -60,6 +44,20 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
->limitConcurrentConnections($connection)
|
->limitConcurrentConnections($connection)
|
||||||
->generateSocketId($connection)
|
->generateSocketId($connection)
|
||||||
->establishConnection($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)
|
public function onMessage(ConnectionInterface $connection, MessageInterface $message)
|
||||||
{
|
{
|
||||||
$message = PusherMessageFactory::createForMessage($message, $connection, $this->channelManager);
|
Messages\PusherMessageFactory::createForMessage(
|
||||||
|
$message, $connection, $this->channelManager
|
||||||
|
)->respond();
|
||||||
|
|
||||||
$message->respond();
|
StatisticsCollector::webSocketMessage($connection->app->id);
|
||||||
|
|
||||||
StatisticsLogger::webSocketMessage($connection->app->id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -86,15 +84,17 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
*/
|
*/
|
||||||
public function onClose(ConnectionInterface $connection)
|
public function onClose(ConnectionInterface $connection)
|
||||||
{
|
{
|
||||||
$this->channelManager->removeFromAllChannels($connection);
|
$this->channelManager->unsubscribeFromAllChannels($connection);
|
||||||
|
|
||||||
DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_DISCONNECTED, [
|
if (isset($connection->app)) {
|
||||||
'socketId' => $connection->socketId,
|
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)
|
public function onError(ConnectionInterface $connection, Exception $exception)
|
||||||
{
|
{
|
||||||
if ($exception instanceof WebSocketException) {
|
if ($exception instanceof Exceptions\WebSocketException) {
|
||||||
$connection->send(json_encode(
|
$connection->send(json_encode(
|
||||||
$exception->getPayload()
|
$exception->getPayload()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->replicator->unsubscribeFromApp($connection->app->id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -123,10 +121,12 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
*/
|
*/
|
||||||
protected function verifyAppKey(ConnectionInterface $connection)
|
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)) {
|
if (! $app = App::findByKey($appKey)) {
|
||||||
throw new UnknownAppKey($appKey);
|
throw new Exceptions\UnknownAppKey($appKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
$connection->app = $app;
|
$connection->app = $app;
|
||||||
|
|
@ -151,7 +151,7 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
$origin = parse_url($header, PHP_URL_HOST) ?: $header;
|
$origin = parse_url($header, PHP_URL_HOST) ?: $header;
|
||||||
|
|
||||||
if (! $header || ! in_array($origin, $connection->app->allowedOrigins)) {
|
if (! $header || ! in_array($origin, $connection->app->allowedOrigins)) {
|
||||||
throw new OriginNotAllowed($connection->app->key);
|
throw new Exceptions\OriginNotAllowed($connection->app->key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -166,17 +166,17 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
protected function limitConcurrentConnections(ConnectionInterface $connection)
|
protected function limitConcurrentConnections(ConnectionInterface $connection)
|
||||||
{
|
{
|
||||||
if (! is_null($capacity = $connection->app->capacity)) {
|
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) {
|
$payload = json_encode($exception->getPayload());
|
||||||
$connectionsCount->then(function ($connectionsCount) use ($capacity, $connection) {
|
|
||||||
$connectionsCount = $connectionsCount ?: 0;
|
|
||||||
|
|
||||||
$this->sendExceptionIfOverCapacity($connectionsCount, $capacity, $connection);
|
tap($connection)->send($payload)->close();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
$this->throwExceptionIfOverCapacity($connectionsCount, $capacity);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
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;
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Server;
|
namespace BeyondCode\LaravelWebSockets;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Server\Logger\HttpLogger;
|
|
||||||
use Ratchet\Http\Router;
|
use Ratchet\Http\Router;
|
||||||
use Ratchet\Server\IoServer;
|
use Ratchet\Server\IoServer;
|
||||||
use React\EventLoop\Factory as LoopFactory;
|
use React\EventLoop\Factory as LoopFactory;
|
||||||
|
|
@ -13,8 +12,10 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Routing\Matcher\UrlMatcher;
|
use Symfony\Component\Routing\Matcher\UrlMatcher;
|
||||||
use Symfony\Component\Routing\RequestContext;
|
use Symfony\Component\Routing\RequestContext;
|
||||||
use Symfony\Component\Routing\RouteCollection;
|
use Symfony\Component\Routing\RouteCollection;
|
||||||
|
use BeyondCode\LaravelWebSockets\Server\HttpServer;
|
||||||
|
use BeyondCode\LaravelWebSockets\Server\Loggers\HttpLogger;
|
||||||
|
|
||||||
class WebSocketServerFactory
|
class ServerFactory
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The host the server will run on.
|
* The host the server will run on.
|
||||||
|
|
@ -69,10 +70,10 @@ class WebSocketServerFactory
|
||||||
/**
|
/**
|
||||||
* Add the routes.
|
* Add the routes.
|
||||||
*
|
*
|
||||||
* @param \Symfony\Component\Routing\RouteCollection $routes
|
* @param \Symfony\Component\Routing\RouteCollection $routes
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function useRoutes(RouteCollection $routes)
|
public function withRoutes(RouteCollection $routes)
|
||||||
{
|
{
|
||||||
$this->routes = $routes;
|
$this->routes = $routes;
|
||||||
|
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Statistics\Collectors;
|
||||||
|
|
||||||
|
use React\Promise\FulfilledPromise;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
use BeyondCode\LaravelWebSockets\Facades\StatisticsStore;
|
||||||
|
use BeyondCode\LaravelWebSockets\Statistics\Statistic;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\StatisticsCollector;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
|
|
||||||
|
class MemoryCollector implements StatisticsCollector
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The list of stored statistics.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $statistics = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Channel manager.
|
||||||
|
*
|
||||||
|
* @var \BeyondCode\LaravelWebSockets\Contracts\ChannelManager
|
||||||
|
*/
|
||||||
|
protected $channelManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the logger.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,407 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Statistics\Collectors;
|
||||||
|
|
||||||
|
use React\Promise\FulfilledPromise;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
use BeyondCode\LaravelWebSockets\Facades\StatisticsStore;
|
||||||
|
use BeyondCode\LaravelWebSockets\Statistics\Statistic;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\StatisticsCollector;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
|
use Illuminate\Cache\RedisLock;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
|
||||||
|
class RedisCollector extends MemoryCollector
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The Redis manager instance.
|
||||||
|
*
|
||||||
|
* @var \Illuminate\Redis\RedisManager
|
||||||
|
*/
|
||||||
|
protected $redis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The set name for the Redis storage.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected static $redisSetName = 'laravel-websockets:apps';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The lock name to use on Redis to avoid multiple
|
||||||
|
* collector-to-store actions that may result
|
||||||
|
* in multiple data points set to the store.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected static $redisLockName = 'laravel-websockets:lock';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the logger.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Statistics\Drivers;
|
|
||||||
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class DatabaseDriver implements StatisticsDriver
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The model that controls the database table.
|
|
||||||
*
|
|
||||||
* @var \BeyondCode\LaravelWebSockets\Statistics\Models\WebSocketsStatisticsEntry|null
|
|
||||||
*/
|
|
||||||
protected $record;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the driver.
|
|
||||||
*
|
|
||||||
* @param \BeyondCode\LaravelWebSockets\Statistics\Models\WebSocketsStatisticsEntry|null $record
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct($record = null)
|
|
||||||
{
|
|
||||||
$this->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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Statistics\Drivers;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
interface StatisticsDriver
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Initialize the driver with a stored record.
|
|
||||||
*
|
|
||||||
* @param mixed $record
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct($record = null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the app ID for the stats.
|
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function getAppId();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the time value. Should be Y-m-d H:i:s.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getTime(): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the peak connection count for the time.
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getPeakConnectionCount(): int;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the websocket messages count for the time.
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getWebsocketMessageCount(): int;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the API message count for the time.
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getApiMessageCount(): int;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new statistic in the store.
|
|
||||||
*
|
|
||||||
* @param array $data
|
|
||||||
* @return \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver
|
|
||||||
*/
|
|
||||||
public static function create(array $data): StatisticsDriver;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the records to show to the dashboard.
|
|
||||||
*
|
|
||||||
* @param mixed $appId
|
|
||||||
* @param \Illuminate\Http\Request|null $request
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function get($appId, ?Request $request);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Statistics\Logger;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver;
|
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Statistic;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
|
||||||
|
|
||||||
class MemoryStatisticsLogger implements StatisticsLogger
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The list of stored statistics.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $statistics = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Channel manager.
|
|
||||||
*
|
|
||||||
* @var \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager
|
|
||||||
*/
|
|
||||||
protected $channelManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The statistics driver instance.
|
|
||||||
*
|
|
||||||
* @var \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver
|
|
||||||
*/
|
|
||||||
protected $driver;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the logger.
|
|
||||||
*
|
|
||||||
* @param \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager $channelManager
|
|
||||||
* @param \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver $driver
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct(ChannelManager $channelManager, StatisticsDriver $driver)
|
|
||||||
{
|
|
||||||
$this->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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Statistics\Logger;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
|
||||||
|
|
||||||
class NullStatisticsLogger implements StatisticsLogger
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The Channel manager.
|
|
||||||
*
|
|
||||||
* @var \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager
|
|
||||||
*/
|
|
||||||
protected $channelManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The statistics driver instance.
|
|
||||||
*
|
|
||||||
* @var \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver
|
|
||||||
*/
|
|
||||||
protected $driver;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the logger.
|
|
||||||
*
|
|
||||||
* @param \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager $channelManager
|
|
||||||
* @param \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver $driver
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct(ChannelManager $channelManager, StatisticsDriver $driver)
|
|
||||||
{
|
|
||||||
$this->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()
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,309 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Statistics\Logger;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
|
||||||
use BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface;
|
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
|
||||||
use Illuminate\Cache\RedisLock;
|
|
||||||
use Illuminate\Support\Facades\Redis;
|
|
||||||
|
|
||||||
class RedisStatisticsLogger implements StatisticsLogger
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The Channel manager.
|
|
||||||
*
|
|
||||||
* @var \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager
|
|
||||||
*/
|
|
||||||
protected $channelManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The statistics driver instance.
|
|
||||||
*
|
|
||||||
* @var \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver
|
|
||||||
*/
|
|
||||||
protected $driver;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The replicator client.
|
|
||||||
*
|
|
||||||
* @var ReplicationInterface
|
|
||||||
*/
|
|
||||||
protected $replicator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Redis manager instance.
|
|
||||||
*
|
|
||||||
* @var \Illuminate\Redis\RedisManager
|
|
||||||
*/
|
|
||||||
protected $redis;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the logger.
|
|
||||||
*
|
|
||||||
* @param \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager $channelManager
|
|
||||||
* @param \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver $driver
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct(ChannelManager $channelManager, StatisticsDriver $driver)
|
|
||||||
{
|
|
||||||
$this->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,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Statistics\Logger;
|
|
||||||
|
|
||||||
interface StatisticsLogger
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
|
|
@ -18,33 +18,33 @@ class Statistic
|
||||||
*
|
*
|
||||||
* @var int
|
* @var int
|
||||||
*/
|
*/
|
||||||
protected $currentConnectionCount = 0;
|
protected $currentConnectionsCount = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The peak connections count ticker.
|
* The peak connections count ticker.
|
||||||
*
|
*
|
||||||
* @var int
|
* @var int
|
||||||
*/
|
*/
|
||||||
protected $peakConnectionCount = 0;
|
protected $peakConnectionsCount = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The websockets connections count ticker.
|
* The websockets connections count ticker.
|
||||||
*
|
*
|
||||||
* @var int
|
* @var int
|
||||||
*/
|
*/
|
||||||
protected $webSocketMessageCount = 0;
|
protected $webSocketMessagesCount = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The api messages connections count ticker.
|
* The api messages connections count ticker.
|
||||||
*
|
*
|
||||||
* @var int
|
* @var int
|
||||||
*/
|
*/
|
||||||
protected $apiMessageCount = 0;
|
protected $apiMessagesCount = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new statistic.
|
* Create a new statistic.
|
||||||
*
|
*
|
||||||
* @param mixed $appId
|
* @param string|int $appId
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function __construct($appId)
|
public function __construct($appId)
|
||||||
|
|
@ -52,6 +52,58 @@ class Statistic
|
||||||
$this->appId = $appId;
|
$this->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.
|
* Check if the app has statistics enabled.
|
||||||
*
|
*
|
||||||
|
|
@ -69,9 +121,9 @@ class Statistic
|
||||||
*/
|
*/
|
||||||
public function connection()
|
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()
|
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()
|
public function webSocketMessage()
|
||||||
{
|
{
|
||||||
$this->webSocketMessageCount++;
|
$this->webSocketMessagesCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -103,21 +155,21 @@ class Statistic
|
||||||
*/
|
*/
|
||||||
public function apiMessage()
|
public function apiMessage()
|
||||||
{
|
{
|
||||||
$this->apiMessageCount++;
|
$this->apiMessagesCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset all the connections to a specific count.
|
* Reset all the connections to a specific count.
|
||||||
*
|
*
|
||||||
* @param int $currentConnectionCount
|
* @param int $currentConnectionsCount
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function reset(int $currentConnectionCount)
|
public function reset(int $currentConnectionsCount)
|
||||||
{
|
{
|
||||||
$this->currentConnectionCount = $currentConnectionCount;
|
$this->currentConnectionsCount = $currentConnectionsCount;
|
||||||
$this->peakConnectionCount = $currentConnectionCount;
|
$this->peakConnectionsCount = $currentConnectionsCount;
|
||||||
$this->webSocketMessageCount = 0;
|
$this->webSocketMessagesCount = 0;
|
||||||
$this->apiMessageCount = 0;
|
$this->apiMessagesCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -129,9 +181,9 @@ class Statistic
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'app_id' => $this->appId,
|
'app_id' => $this->appId,
|
||||||
'peak_connection_count' => $this->peakConnectionCount,
|
'peak_connections_count' => $this->peakConnectionsCount,
|
||||||
'websocket_message_count' => $this->webSocketMessageCount,
|
'websocket_messages_count' => $this->webSocketMessagesCount,
|
||||||
'api_message_count' => $this->apiMessageCount,
|
'api_messages_count' => $this->apiMessagesCount,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Statistics\Stores;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\StatisticsStore;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class DatabaseStore implements StatisticsStore
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The model that will interact with the database.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected static $model = \BeyondCode\LaravelWebSockets\Models\WebSocketsStatisticsEntry::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a new record in the database and return
|
||||||
|
* the created instance.
|
||||||
|
*
|
||||||
|
* @param array $data
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public static function store(array $data)
|
||||||
|
{
|
||||||
|
return static::$model::create($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete records older than the given moment,
|
||||||
|
* for a specific app id (if given), returning
|
||||||
|
* the amount of deleted records.
|
||||||
|
*
|
||||||
|
* @param \Carbon\Carbon $moment
|
||||||
|
* @param string|int|null $appId
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public static function delete(Carbon $moment, $appId = null): int
|
||||||
|
{
|
||||||
|
return static::$model::where('created_at', '<', $moment->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(),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,254 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Channels;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Dashboard\DashboardLogger;
|
|
||||||
use BeyondCode\LaravelWebSockets\Events\MessagesBroadcasted;
|
|
||||||
use BeyondCode\LaravelWebSockets\Events\Subscribed;
|
|
||||||
use BeyondCode\LaravelWebSockets\Events\Unsubscribed;
|
|
||||||
use BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\InvalidSignature;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Ratchet\ConnectionInterface;
|
|
||||||
use stdClass;
|
|
||||||
|
|
||||||
class Channel
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The channel name.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $channelName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The replicator client.
|
|
||||||
*
|
|
||||||
* @var ReplicationInterface
|
|
||||||
*/
|
|
||||||
protected $replicator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The connections that got subscribed.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $subscribedConnections = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new instance.
|
|
||||||
*
|
|
||||||
* @param string $channelName
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct(string $channelName)
|
|
||||||
{
|
|
||||||
$this->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),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Channels;
|
|
||||||
|
|
||||||
use Ratchet\ConnectionInterface;
|
|
||||||
|
|
||||||
interface ChannelManager
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Find a channel by name or create one.
|
|
||||||
*
|
|
||||||
* @param mixed $appId
|
|
||||||
* @param string $channelName
|
|
||||||
* @return \BeyondCode\LaravelWebSockets\WebSockets\Channels\Channel
|
|
||||||
*/
|
|
||||||
public function findOrCreate($appId, string $channelName): Channel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a channel by name.
|
|
||||||
*
|
|
||||||
* @param mixed $appId
|
|
||||||
* @param string $channelName
|
|
||||||
* @return \BeyondCode\LaravelWebSockets\WebSockets\Channels\Channel
|
|
||||||
*/
|
|
||||||
public function find($appId, string $channelName): ?Channel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all channels.
|
|
||||||
*
|
|
||||||
* @param mixed $appId
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getChannels($appId): array;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the connections count on the app.
|
|
||||||
*
|
|
||||||
* @param mixed $appId
|
|
||||||
* @return int|\React\Promise\PromiseInterface
|
|
||||||
*/
|
|
||||||
public function getLocalConnectionsCount($appId): int;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the connections count across multiple servers.
|
|
||||||
*
|
|
||||||
* @param mixed $appId
|
|
||||||
* @return int|\React\Promise\PromiseInterface
|
|
||||||
*/
|
|
||||||
public function getGlobalConnectionsCount($appId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove connection from all channels.
|
|
||||||
*
|
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function removeFromAllChannels(ConnectionInterface $connection);
|
|
||||||
}
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManagers;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\Channel;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\PresenceChannel;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\PrivateChannel;
|
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Ratchet\ConnectionInterface;
|
|
||||||
|
|
||||||
class ArrayChannelManager implements ChannelManager
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The app id.
|
|
||||||
*
|
|
||||||
* @var mixed
|
|
||||||
*/
|
|
||||||
protected $appId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of channels.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $channels = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a channel by name or create one.
|
|
||||||
*
|
|
||||||
* @param mixed $appId
|
|
||||||
* @param string $channelName
|
|
||||||
* @return \BeyondCode\LaravelWebSockets\WebSockets\Channels
|
|
||||||
*/
|
|
||||||
public function findOrCreate($appId, string $channelName): Channel
|
|
||||||
{
|
|
||||||
if (! isset($this->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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManagers;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface;
|
|
||||||
|
|
||||||
class RedisChannelManager extends ArrayChannelManager
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The replicator driver.
|
|
||||||
*
|
|
||||||
* @var \BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface
|
|
||||||
*/
|
|
||||||
protected $replicator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the channel manager.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Channels;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\InvalidSignature;
|
|
||||||
use Ratchet\ConnectionInterface;
|
|
||||||
use React\Promise\PromiseInterface;
|
|
||||||
use stdClass;
|
|
||||||
|
|
||||||
class PresenceChannel extends Channel
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Data for the users connected to this channel.
|
|
||||||
*
|
|
||||||
* Note: If replication is enabled, this will only contain entries
|
|
||||||
* for the users directly connected to this server instance. Requests
|
|
||||||
* for data for all users in the channel should be routed through
|
|
||||||
* ReplicationInterface.
|
|
||||||
*
|
|
||||||
* @var string[]
|
|
||||||
*/
|
|
||||||
protected $users = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the members in the presence channel.
|
|
||||||
*
|
|
||||||
* @param string $appId
|
|
||||||
* @return PromiseInterface
|
|
||||||
*/
|
|
||||||
public function getUsers($appId)
|
|
||||||
{
|
|
||||||
return $this->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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Exceptions;
|
|
||||||
|
|
||||||
class InvalidConnection extends WebSocketException
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Initialize the instance.
|
|
||||||
*
|
|
||||||
* @see https://pusher.com/docs/pusher_protocol#error-codes
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->message = 'Invalid Connection';
|
|
||||||
$this->code = 4009;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Exceptions;
|
|
||||||
|
|
||||||
class OriginNotAllowed extends WebSocketException
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Initialize the instance.
|
|
||||||
*
|
|
||||||
* @see https://pusher.com/docs/pusher_protocol#error-codes
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct(string $appKey)
|
|
||||||
{
|
|
||||||
$this->message = "The origin is not allowed for `{$appKey}`.";
|
|
||||||
$this->code = 4009;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Exceptions;
|
|
||||||
|
|
||||||
class UnknownAppKey extends WebSocketException
|
|
||||||
{
|
|
||||||
public function __construct($appKey)
|
|
||||||
{
|
|
||||||
$this->message = "Could not find app key `{$appKey}`.";
|
|
||||||
|
|
||||||
$this->code = 4001;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,19 +2,15 @@
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets;
|
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\AuthenticateDashboard;
|
||||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\SendMessage;
|
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\SendMessage;
|
||||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowDashboard;
|
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowDashboard;
|
||||||
use BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers\ShowStatistics;
|
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\Gate;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Illuminate\Support\ServiceProvider;
|
|
||||||
|
|
||||||
class WebSocketsServiceProvider extends ServiceProvider
|
class WebSocketsServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
|
@ -26,23 +22,20 @@ class WebSocketsServiceProvider extends ServiceProvider
|
||||||
public function boot()
|
public function boot()
|
||||||
{
|
{
|
||||||
$this->publishes([
|
$this->publishes([
|
||||||
__DIR__.'/../config/websockets.php' => base_path('config/websockets.php'),
|
__DIR__.'/../config/websockets.php' => config_path('websockets.php'),
|
||||||
], 'config');
|
], 'config');
|
||||||
|
|
||||||
|
$this->mergeConfigFrom(
|
||||||
|
__DIR__.'/../config/websockets.php', 'websockets'
|
||||||
|
);
|
||||||
|
|
||||||
$this->publishes([
|
$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'),
|
__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');
|
], 'migrations');
|
||||||
|
|
||||||
$this->registerDashboardRoutes()
|
$this->registerDashboard();
|
||||||
->registerDashboardGate();
|
|
||||||
|
|
||||||
$this->loadViewsFrom(__DIR__.'/../resources/views/', 'websockets');
|
$this->registerCommands();
|
||||||
|
|
||||||
$this->commands([
|
|
||||||
Console\StartWebSocketServer::class,
|
|
||||||
Console\CleanStatistics::class,
|
|
||||||
Console\RestartWebSocketServer::class,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -52,34 +45,59 @@ class WebSocketsServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
public function register()
|
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 () {
|
$this->app->singleton('websockets.router', function () {
|
||||||
return new Router();
|
return new Router;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$this->app->singleton(ChannelManager::class, function () {
|
/**
|
||||||
$replicationDriver = config('websockets.replication.driver', 'local');
|
* Register the managers for the app.
|
||||||
|
*
|
||||||
$class = config("websockets.replication.{$replicationDriver}.channel_manager", ArrayChannelManager::class);
|
* @return void
|
||||||
|
*/
|
||||||
return new $class;
|
protected function registerManagers()
|
||||||
});
|
{
|
||||||
|
$this->app->singleton(Contracts\AppManager::class, function () {
|
||||||
$this->app->singleton(AppManager::class, function () {
|
|
||||||
return $this->app->make(config('websockets.managers.app'));
|
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('/auth', AuthenticateDashboard::class)->name('auth');
|
||||||
Route::post('/event', SendMessage::class)->name('event');
|
Route::post('/event', SendMessage::class)->name('event');
|
||||||
});
|
});
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -113,7 +129,5 @@ class WebSocketsServiceProvider extends ServiceProvider
|
||||||
Gate::define('viewWebSocketsDashboard', function ($user = null) {
|
Gate::define('viewWebSocketsDashboard', function ($user = null) {
|
||||||
return $this->app->environment('local');
|
return $this->app->environment('local');
|
||||||
});
|
});
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Tests\Channels;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Tests\Mocks\Message;
|
|
||||||
use BeyondCode\LaravelWebSockets\Tests\TestCase;
|
|
||||||
|
|
||||||
class ChannelReplicationTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
$this->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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Tests\Channels;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Tests\Mocks\Message;
|
|
||||||
use BeyondCode\LaravelWebSockets\Tests\TestCase;
|
|
||||||
|
|
||||||
class ChannelTest extends TestCase
|
|
||||||
{
|
|
||||||
/** @test */
|
|
||||||
public function 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 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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Tests\Channels;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Tests\Mocks\Message;
|
|
||||||
use BeyondCode\LaravelWebSockets\Tests\TestCase;
|
|
||||||
|
|
||||||
class PresenceChannelReplicationTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
$this->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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Tests\Channels;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Tests\Mocks\Message;
|
|
||||||
use BeyondCode\LaravelWebSockets\Tests\TestCase;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\InvalidSignature;
|
|
||||||
|
|
||||||
class PresenceChannelTest extends TestCase
|
|
||||||
{
|
|
||||||
/** @test */
|
|
||||||
public function clients_need_valid_auth_signatures_to_join_presence_channels()
|
|
||||||
{
|
|
||||||
$this->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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Tests\Channels;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Tests\Mocks\Message;
|
|
||||||
use BeyondCode\LaravelWebSockets\Tests\TestCase;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\InvalidSignature;
|
|
||||||
|
|
||||||
class PrivateChannelReplicationTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
$this->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',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Tests\Channels;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Tests\Mocks\Message;
|
|
||||||
use BeyondCode\LaravelWebSockets\Tests\TestCase;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\InvalidSignature;
|
|
||||||
|
|
||||||
class PrivateChannelTest extends TestCase
|
|
||||||
{
|
|
||||||
/** @test */
|
|
||||||
public function 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 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',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Tests\ClientProviders;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
|
||||||
use BeyondCode\LaravelWebSockets\Exceptions\InvalidApp;
|
|
||||||
use BeyondCode\LaravelWebSockets\Tests\TestCase;
|
|
||||||
|
|
||||||
class AppTest extends TestCase
|
|
||||||
{
|
|
||||||
/** @test */
|
|
||||||
public function it_can_create_a_client()
|
|
||||||
{
|
|
||||||
new App(1, 'appKey', 'appSecret');
|
|
||||||
|
|
||||||
$this->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', '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Tests\ClientProviders;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Apps\ConfigAppManager;
|
|
||||||
use BeyondCode\LaravelWebSockets\Tests\TestCase;
|
|
||||||
|
|
||||||
class ConfigAppManagerTest extends TestCase
|
|
||||||
{
|
|
||||||
/** @var \BeyondCode\LaravelWebSockets\Apps\ConfigAppManager */
|
|
||||||
protected $appManager;
|
|
||||||
|
|
||||||
public function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
$this->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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue