Merge remote-tracking branch 'upstream/2.x' into 2.x
This commit is contained in:
commit
9782bd2a6a
|
|
@ -0,0 +1,18 @@
|
||||||
|
codecov:
|
||||||
|
notify:
|
||||||
|
require_ci_to_pass: yes
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
precision: 2
|
||||||
|
round: down
|
||||||
|
range: "70...100"
|
||||||
|
|
||||||
|
status:
|
||||||
|
project: yes
|
||||||
|
patch: yes
|
||||||
|
changes: no
|
||||||
|
|
||||||
|
comment:
|
||||||
|
layout: "reach, diff, flags, files, footer"
|
||||||
|
behavior: default
|
||||||
|
require_changes: no
|
||||||
|
|
@ -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:${{ 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,6 +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
|
||||||
|
|
|
||||||
|
|
@ -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,14 @@
|
||||||
{
|
{
|
||||||
"name": "beyondcode/laravel-websockets",
|
"name": "beyondcode/laravel-websockets",
|
||||||
"description": "An easy to use WebSocket server",
|
"description": "An easy to launch a Pusher-compatible WebSockets server for Laravel.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"beyondcode",
|
"beyondcode",
|
||||||
"laravel-websockets"
|
"laravel-websockets",
|
||||||
|
"laravel",
|
||||||
|
"php"
|
||||||
],
|
],
|
||||||
"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 +21,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": {
|
||||||
|
|
@ -27,42 +34,49 @@
|
||||||
"cboden/ratchet": "^0.4.1",
|
"cboden/ratchet": "^0.4.1",
|
||||||
"clue/buzz-react": "^2.5",
|
"clue/buzz-react": "^2.5",
|
||||||
"clue/redis-react": "^2.3",
|
"clue/redis-react": "^2.3",
|
||||||
|
"doctrine/dbal": "^2.0",
|
||||||
"evenement/evenement": "^2.0|^3.0",
|
"evenement/evenement": "^2.0|^3.0",
|
||||||
"facade/ignition-contracts": "^1.0",
|
"facade/ignition-contracts": "^1.0",
|
||||||
"guzzlehttp/psr7": "^1.5",
|
"guzzlehttp/psr7": "^1.5",
|
||||||
"illuminate/broadcasting": "^6.0|^7.0",
|
"illuminate/broadcasting": "^6.3|^7.0|^8.0",
|
||||||
"illuminate/console": "^6.0|^7.0",
|
"illuminate/console": "^6.3|^7.0|^8.0",
|
||||||
"illuminate/http": "^6.0|^7.0",
|
"illuminate/http": "^6.3|^7.0|^8.0",
|
||||||
"illuminate/routing": "^6.0|^7.0",
|
"illuminate/queue": "^6.3|^7.0|^8.0",
|
||||||
"illuminate/support": "^6.0|^7.0",
|
"illuminate/routing": "^6.3|^7.0|^8.0",
|
||||||
|
"illuminate/support": "^6.3|^7.0|^8.0",
|
||||||
"pusher/pusher-php-server": "^3.0|^4.0",
|
"pusher/pusher-php-server": "^3.0|^4.0",
|
||||||
"react/dns": "^1.1",
|
"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": {
|
"require-dev": {
|
||||||
|
"clue/block-react": "^1.4",
|
||||||
|
"laravel/legacy-factories": "^1.0.4",
|
||||||
"mockery/mockery": "^1.3",
|
"mockery/mockery": "^1.3",
|
||||||
"orchestra/testbench": "3.8.*|^4.0|^5.0",
|
"orchestra/testbench-browser-kit": "^4.0|^5.0|^6.0",
|
||||||
|
"orchestra/database": "^4.0|^5.0|^6.0",
|
||||||
"phpunit/phpunit": "^8.0|^9.0"
|
"phpunit/phpunit": "^8.0|^9.0"
|
||||||
},
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-pcntl": "Running the server needs pcntl to listen to command signals and soft-shutdown."
|
||||||
|
},
|
||||||
"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"
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"sort-packages": true
|
"sort-packages": true
|
||||||
},
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
"extra": {
|
"extra": {
|
||||||
"laravel": {
|
"laravel": {
|
||||||
"providers": [
|
"providers": [
|
||||||
|
|
|
||||||
|
|
@ -42,21 +42,6 @@ return [
|
||||||
|
|
||||||
'app' => \BeyondCode\LaravelWebSockets\Apps\ConfigAppManager::class,
|
'app' => \BeyondCode\LaravelWebSockets\Apps\ConfigAppManager::class,
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Channel Manager
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| When users subscribe or unsubscribe from specific channels,
|
|
||||||
| the connections are stored to keep track of any interaction with the
|
|
||||||
| WebSocket server.
|
|
||||||
| You can however add your own implementation that will help the store
|
|
||||||
| of the channels alongside their connections.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'channel' => \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManagers\ArrayChannelManager::class,
|
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -78,6 +63,7 @@ return [
|
||||||
[
|
[
|
||||||
'id' => env('PUSHER_APP_ID'),
|
'id' => env('PUSHER_APP_ID'),
|
||||||
'name' => env('APP_NAME'),
|
'name' => env('APP_NAME'),
|
||||||
|
'host' => env('PUSHER_APP_HOST'),
|
||||||
'key' => env('PUSHER_APP_KEY'),
|
'key' => env('PUSHER_APP_KEY'),
|
||||||
'secret' => env('PUSHER_APP_SECRET'),
|
'secret' => env('PUSHER_APP_SECRET'),
|
||||||
'path' => env('PUSHER_APP_PATH'),
|
'path' => env('PUSHER_APP_PATH'),
|
||||||
|
|
@ -90,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' => env('WEBSOCKETS_REDIS_REPLICATION_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
|
||||||
|
|
@ -144,101 +265,33 @@ return [
|
||||||
|
|
||||||
'handlers' => [
|
'handlers' => [
|
||||||
|
|
||||||
'websocket' => \BeyondCode\LaravelWebSockets\WebSockets\WebSocketHandler::class,
|
'websocket' => \BeyondCode\LaravelWebSockets\Server\WebSocketHandler::class,
|
||||||
|
|
||||||
|
'health' => \BeyondCode\LaravelWebSockets\Server\HealthHandler::class,
|
||||||
|
|
||||||
|
'trigger_event' => \BeyondCode\LaravelWebSockets\API\TriggerEvent::class,
|
||||||
|
|
||||||
|
'fetch_channels' => \BeyondCode\LaravelWebSockets\API\FetchChannels::class,
|
||||||
|
|
||||||
|
'fetch_channel' => \BeyondCode\LaravelWebSockets\API\FetchChannel::class,
|
||||||
|
|
||||||
|
'fetch_users' => \BeyondCode\LaravelWebSockets\API\FetchUsers::class,
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Broadcasting Replication PubSub
|
| Promise Resolver
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| You can enable replication to publish and subscribe to
|
| The promise resolver is a class that takes a input value and is
|
||||||
| messages across the driver.
|
| able to make sure the PHP code runs async by using ->then(). You can
|
||||||
|
|
| use your own Promise Resolver. This is usually changed when you want to
|
||||||
| By default, it is set to 'local', but you can configure it to use drivers
|
| intercept values by the promises throughout the app, like in testing
|
||||||
| like Redis to ensure connection between multiple instances of
|
| to switch from async to sync.
|
||||||
| WebSocket servers. Just set the driver to 'redis' to enable the PubSub using Redis.
|
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'replication' => [
|
'promise_resolver' => \React\Promise\FulfilledPromise::class,
|
||||||
|
|
||||||
'driver' => env('LARAVEL_WEBSOCKETS_REPLICATION_DRIVER', 'local'),
|
|
||||||
|
|
||||||
'redis' => [
|
|
||||||
|
|
||||||
'connection' => env('LARAVEL_WEBSOCKETS_REPLICATION_CONNECTION', 'default'),
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
'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 Logger Handler
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| The Statistics Logger will, by default, handle the incoming statistics,
|
|
||||||
| store them into an array and then store them into the database
|
|
||||||
| on each interval.
|
|
||||||
|
|
|
||||||
| You can opt-in to avoid any statistics storage by setting the logger
|
|
||||||
| to the built-in NullLogger.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'logger' => \BeyondCode\LaravelWebSockets\Statistics\Logger\MemoryStatisticsLogger::class,
|
|
||||||
// 'logger' => \BeyondCode\LaravelWebSockets\Statistics\Logger\NullStatisticsLogger::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,
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class RenameStatisticsCounters extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('websockets_statistics_entries', function (Blueprint $table) {
|
||||||
|
$table->renameColumn('peak_connection_count', 'peak_connections_count');
|
||||||
|
$table->renameColumn('websocket_message_count', 'websocket_messages_count');
|
||||||
|
$table->renameColumn('api_message_count', 'api_messages_count');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('websockets_statistics_entries', function (Blueprint $table) {
|
||||||
|
$table->renameColumn('peak_connections_count', 'peak_connection_count');
|
||||||
|
$table->renameColumn('websocket_messages_count', 'websocket_message_count');
|
||||||
|
$table->renameColumn('api_messages_count', 'api_message_count');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ Depending on your setup, you might have your app configuration stored elsewhere
|
||||||
|
|
||||||
> Make sure that you do **not** perform any IO blocking tasks in your `AppManager`, as they will interfere with the asynchronous WebSocket execution.
|
> Make sure that you do **not** perform any IO blocking tasks in your `AppManager`, as they will interfere with the asynchronous WebSocket execution.
|
||||||
|
|
||||||
In order to create your custom `AppManager`, create a class that implements the `BeyondCode\LaravelWebSockets\Apps\AppManager` interface.
|
In order to create your custom `AppManager`, create a class that implements the `BeyondCode\LaravelWebSockets\Contracts\AppManager` interface.
|
||||||
|
|
||||||
This is what it looks like:
|
This is what it looks like:
|
||||||
|
|
||||||
|
|
@ -34,11 +34,11 @@ interface AppManager
|
||||||
|
|
||||||
The following is an example AppManager that utilizes an Eloquent model:
|
The following is an example AppManager that utilizes an Eloquent model:
|
||||||
```php
|
```php
|
||||||
namespace App\Appmanagers;
|
namespace App\Managers;
|
||||||
|
|
||||||
use App\Application;
|
use App\Application;
|
||||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||||
use BeyondCode\LaravelWebSockets\Apps\AppManager;
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
|
|
||||||
class MyCustomAppManager implements AppManager
|
class MyCustomAppManager implements AppManager
|
||||||
{
|
{
|
||||||
|
|
@ -51,22 +51,22 @@ class MyCustomAppManager implements AppManager
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findById($appId) : ? App
|
public function findById($appId) : ?App
|
||||||
{
|
{
|
||||||
return $this->normalize(Application::findById($appId)->toArray());
|
return $this->normalize(Application::findById($appId)->toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findByKey($appKey) : ? App
|
public function findByKey($appKey) : ?App
|
||||||
{
|
{
|
||||||
return $this->normalize(Application::findByKey($appKey)->toArray());
|
return $this->normalize(Application::findByKey($appKey)->toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findBySecret($appSecret) : ? App
|
public function findBySecret($appSecret) : ?App
|
||||||
{
|
{
|
||||||
return $this->normalize(Application::findBySecret($appSecret)->toArray());
|
return $this->normalize(Application::findBySecret($appSecret)->toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function normalize(?array $appAttributes) : ? App
|
protected function normalize(?array $appAttributes) : ?App
|
||||||
{
|
{
|
||||||
if (! $appAttributes) {
|
if (! $appAttributes) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -116,7 +116,5 @@ Once you have implemented your own AppManager, you need to set it in the `websoc
|
||||||
|
|
||||||
'app' => \App\Managers\MyCustomAppManager::class,
|
'app' => \App\Managers\MyCustomAppManager::class,
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,13 @@ Once implemented, you will have a class that looks something like this:
|
||||||
```php
|
```php
|
||||||
namespace App;
|
namespace App;
|
||||||
|
|
||||||
|
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 MyCustomWebSocketHandler implements MessageComponentInterface
|
class MyCustomWebSocketHandler implements MessageComponentInterface
|
||||||
{
|
{
|
||||||
|
|
||||||
public function onOpen(ConnectionInterface $connection)
|
public function onOpen(ConnectionInterface $connection)
|
||||||
{
|
{
|
||||||
// TODO: Implement onOpen() method.
|
// TODO: Implement onOpen() method.
|
||||||
|
|
@ -32,7 +32,7 @@ class MyCustomWebSocketHandler implements MessageComponentInterface
|
||||||
// TODO: Implement onClose() method.
|
// TODO: Implement onClose() method.
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onError(ConnectionInterface $connection, \Exception $e)
|
public function onError(ConnectionInterface $connection, Exception $e)
|
||||||
{
|
{
|
||||||
// TODO: Implement onError() method.
|
// TODO: Implement onError() method.
|
||||||
}
|
}
|
||||||
|
|
@ -48,12 +48,12 @@ In the class itself you have full control over all the lifecycle events of your
|
||||||
|
|
||||||
The only part missing is, that you will need to tell our WebSocket server to load this handler at a specific route endpoint. This can be achieved using the `WebSocketsRouter` facade.
|
The only part missing is, that you will need to tell our WebSocket server to load this handler at a specific route endpoint. This can be achieved using the `WebSocketsRouter` facade.
|
||||||
|
|
||||||
This class takes care of registering the routes with the actual webSocket server. You can use the `webSocket` method to define a custom WebSocket endpoint. The method needs two arguments: the path where the WebSocket handled should be available and the fully qualified classname of the WebSocket handler class.
|
This class takes care of registering the routes with the actual webSocket server. You can use the `get` method to define a custom WebSocket endpoint. The method needs two arguments: the path where the WebSocket handled should be available and the fully qualified classname of the WebSocket handler class.
|
||||||
|
|
||||||
This could, for example, be done inside your `routes/web.php` file.
|
This could, for example, be done inside your `routes/web.php` file.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
WebSocketsRouter::webSocket('/my-websocket', \App\MyCustomWebSocketHandler::class);
|
WebSocketsRouter::get('/my-websocket', \App\MyCustomWebSocketHandler::class);
|
||||||
```
|
```
|
||||||
|
|
||||||
Once you've added the custom WebSocket route, be sure to restart our WebSocket server for the changes to take place.
|
Once you've added the custom WebSocket route, be sure to restart our WebSocket server for the changes to take place.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
---
|
||||||
|
title: Dispatched Events
|
||||||
|
order: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
# Dispatched Events
|
||||||
|
|
||||||
|
Laravel WebSockets takes advantage of Laravel's Event dispatching observer, in a way that you can handle in-server events outside of it.
|
||||||
|
|
||||||
|
For example, you can listen for events like when a new connection establishes or when an user joins a presence channel.
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
Below you will find a list of dispatched events:
|
||||||
|
|
||||||
|
- `BeyondCode\LaravelWebSockets\Events\NewConnection` - when a connection successfully establishes on the server
|
||||||
|
- `BeyondCode\LaravelWebSockets\Events\ConnectionClosed` - when a connection leaves the server
|
||||||
|
- `BeyondCode\LaravelWebSockets\Events\SubscribedToChannel` - when a connection subscribes to a specific channel
|
||||||
|
- `BeyondCode\LaravelWebSockets\Events\UnsubscribedFromChannel` - when a connection unsubscribes from a specific channel
|
||||||
|
- `BeyondCode\LaravelWebSockets\Events\WebSocketMessageReceived` - when the server receives a message
|
||||||
|
- `BeyondCode\LaravelWebSockets\EventsConnectionPonged` - when a connection pings to the server that it is still alive
|
||||||
|
|
||||||
|
## Queued Listeners
|
||||||
|
|
||||||
|
Because the default Redis connection (either PhpRedis or Predis) is a blocking I/O method and can cause problems with the server speed and availability, you might want to check the [Non-Blocking Queue Driver](non-blocking-queue-driver.md) documentation that helps you create the Async Redis queue driver that is going to fix the Blocking I/O issue.
|
||||||
|
|
||||||
|
If set up, you can use the `async-redis` queue driver in your listeners:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Events\NewConnection;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
|
||||||
|
class HandleNewConnections implements ShouldQueue
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name of the connection the job should be sent to.
|
||||||
|
*
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
public $connection = 'async-redis';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the event listener.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the event.
|
||||||
|
*
|
||||||
|
* @param NewConnection $event
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle(NewConnection $event)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `EventServiceProvider` might look like this, registering the listeners that are going to be used by the event dispatching:
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* The event listener mappings for the application.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $listen = [
|
||||||
|
\BeyondCode\LaravelWebSockets\Events\NewConnection::class => [
|
||||||
|
App\Listeners\HandleNewConnections::class,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
title: Non-Blocking Queue Driver
|
||||||
|
order: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
# Non-Blocking Queue Driver
|
||||||
|
|
||||||
|
In Laravel, he default Redis connection also interacts with the queues. Since you might want to dispatch jobs on Redis from the server, you can encounter an anti-pattern of using a blocking I/O connection (like PhpRedis or PRedis) within the WebSockets server.
|
||||||
|
|
||||||
|
To solve this issue, you can configure the built-in queue driver that uses the Async Redis connection when it's possible, like within the WebSockets server. It's highly recommended to switch your queue to it if you are going to use the queues within the server controllers, for example.
|
||||||
|
|
||||||
|
Add the `async-redis` queue driver to your list of connections. The configuration parameters are compatible with the default `redis` driver:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'connections' => [
|
||||||
|
'async-redis' => [
|
||||||
|
'driver' => 'async-redis',
|
||||||
|
'connection' => env('WEBSOCKETS_REDIS_REPLICATION_CONNECTION', 'default'),
|
||||||
|
'queue' => env('REDIS_QUEUE', 'default'),
|
||||||
|
'retry_after' => 90,
|
||||||
|
'block_for' => null,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Also, make sure that the default queue driver is set to `async-redis`:
|
||||||
|
|
||||||
|
```
|
||||||
|
QUEUE_CONNECTION=async-redis
|
||||||
|
```
|
||||||
|
|
@ -36,7 +36,7 @@ class WebSocketHandler extends BaseWebSocketHandler
|
||||||
// Run code on close.
|
// Run code on close.
|
||||||
// $connection->app contains the app details
|
// $connection->app contains the app details
|
||||||
// $this->channelManager is accessible
|
// $this->channelManager is accessible
|
||||||
}****
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ To make it clear, the package does not restrict connections numbers or depend on
|
||||||
|
|
||||||
To make use of the Laravel WebSockets package in combination with Pusher, you first need to install the official Pusher PHP SDK.
|
To make use of the Laravel WebSockets package in combination with Pusher, you first need to install the official Pusher PHP SDK.
|
||||||
|
|
||||||
If you are not yet familiar with the concept of Broadcasting in Laravel, please take a look at the [Laravel documentation](https://laravel.com/docs/6.0/broadcasting).
|
If you are not yet familiar with the concept of Broadcasting in Laravel, please take a look at the [Laravel documentation](https://laravel.com/docs/8.0/broadcasting).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
composer require pusher/pusher-php-server "~4.0"
|
composer require pusher/pusher-php-server "~4.0"
|
||||||
|
|
@ -40,9 +40,13 @@ To do this, you should add the `host` and `port` configuration key to your `conf
|
||||||
'options' => [
|
'options' => [
|
||||||
'cluster' => env('PUSHER_APP_CLUSTER'),
|
'cluster' => env('PUSHER_APP_CLUSTER'),
|
||||||
'encrypted' => true,
|
'encrypted' => true,
|
||||||
'host' => '127.0.0.1',
|
'host' => env('PUSHER_APP_HOST', '127.0.0.1'),
|
||||||
'port' => 6001,
|
'port' => env('PUSHER_APP_PORT', 6001),
|
||||||
'scheme' => 'http',
|
'scheme' => env('PUSHER_APP_SCHEME', 'http'),
|
||||||
|
'curl_options' => [
|
||||||
|
CURLOPT_SSL_VERIFYHOST => 0,
|
||||||
|
CURLOPT_SSL_VERIFYPEER => 0,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
@ -95,8 +99,8 @@ To enable or disable the statistics for one of your apps, you can modify the `en
|
||||||
|
|
||||||
## Usage with Laravel Echo
|
## Usage with Laravel Echo
|
||||||
|
|
||||||
The Laravel WebSockets package integrates nicely into [Laravel Echo](https://laravel.com/docs/6.0/broadcasting#receiving-broadcasts) to integrate into your frontend application and receive broadcasted events.
|
The Laravel WebSockets package integrates nicely into [Laravel Echo](https://laravel.com/docs/8.0/broadcasting#receiving-broadcasts) to integrate into your frontend application and receive broadcasted events.
|
||||||
If you are new to Laravel Echo, be sure to take a look at the [official documentation](https://laravel.com/docs/6.0/broadcasting#receiving-broadcasts).
|
If you are new to Laravel Echo, be sure to take a look at the [official documentation](https://laravel.com/docs/8.0/broadcasting#receiving-broadcasts).
|
||||||
|
|
||||||
To make Laravel Echo work with Laravel WebSockets, you need to make some minor configuration changes when working with Laravel Echo. Add the `wsHost` and `wsPort` parameters and point them to your Laravel WebSocket server host and port.
|
To make Laravel Echo work with Laravel WebSockets, you need to make some minor configuration changes when working with Laravel Echo. Add the `wsHost` and `wsPort` parameters and point them to your Laravel WebSocket server host and port.
|
||||||
|
|
||||||
|
|
@ -107,7 +111,7 @@ When using Laravel WebSockets in combination with a custom SSL certificate, be s
|
||||||
:::
|
:::
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import Echo from "laravel-echo"
|
import Echo from 'laravel-echo';
|
||||||
|
|
||||||
window.Pusher = require('pusher-js');
|
window.Pusher = require('pusher-js');
|
||||||
|
|
||||||
|
|
@ -122,4 +126,4 @@ window.Echo = new Echo({
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Now you can use all Laravel Echo features in combination with Laravel WebSockets, such as [Presence Channels](https://laravel.com/docs/7.x/broadcasting#presence-channels), [Notifications](https://laravel.com/docs/7.x/broadcasting#notifications) and [Client Events](https://laravel.com/docs/7.x/broadcasting#client-events).
|
Now you can use all Laravel Echo features in combination with Laravel WebSockets, such as [Presence Channels](https://laravel.com/docs/8.x/broadcasting#presence-channels), [Notifications](https://laravel.com/docs/8.x/broadcasting#notifications) and [Client Events](https://laravel.com/docs/8.x/broadcasting#client-events).
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ order: 4
|
||||||
|
|
||||||
If you use Supervisor to keep your server alive, you might want to restart it just like `queue:restart` does.
|
If you use Supervisor to keep your server alive, you might want to restart it just like `queue:restart` does.
|
||||||
|
|
||||||
To do so, consider using the `websockets:restart`. In a maximum of 10 seconds, the server will be restarted automatically.
|
To do so, consider using the `websockets:restart`. In a maximum of 10 seconds since issuing the command, the server will be restarted.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php artisan websockets:restart
|
php artisan websockets:restart
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ Since most of the web's traffic is going through HTTPS, it's also crucial to sec
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The SSL configuration takes place in your `config/websockets.php` file.
|
The SSL configuration takes place in your `config/websockets.php` file.
|
||||||
|
|
||||||
The default configuration has a SSL section that looks like this:
|
The default configuration has a SSL section that looks like this:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
|
|
@ -31,6 +32,7 @@ The default configuration has a SSL section that looks like this:
|
||||||
```
|
```
|
||||||
|
|
||||||
But this is only a subset of all the available configuration options.
|
But this is only a subset of all the available configuration options.
|
||||||
|
|
||||||
This packages makes use of the official PHP [SSL context options](http://php.net/manual/en/context.ssl.php).
|
This packages makes use of the official PHP [SSL context options](http://php.net/manual/en/context.ssl.php).
|
||||||
|
|
||||||
So if you find yourself in the need of adding additional configuration settings, take a look at the PHP documentation and simply add the configuration parameters that you need.
|
So if you find yourself in the need of adding additional configuration settings, take a look at the PHP documentation and simply add the configuration parameters that you need.
|
||||||
|
|
@ -64,7 +66,13 @@ window.Echo = new Echo({
|
||||||
|
|
||||||
## Server configuration
|
## Server configuration
|
||||||
|
|
||||||
When broadcasting events from your Laravel application to the WebSocket server, you also need to tell Laravel to make use of HTTPS instead of HTTP. You can do this by setting the `scheme` option in your `config/broadcasting.php` file to `https`:
|
When broadcasting events from your Laravel application to the WebSocket server, you also need to tell Laravel to make use of HTTPS instead of HTTP. You can do this by setting the `PUSHER_APP_SCHEME` variable to `https`
|
||||||
|
|
||||||
|
```env
|
||||||
|
PUSHER_APP_SCHEME=https
|
||||||
|
```
|
||||||
|
|
||||||
|
Your connection from `config/broadcasting.php` would look like this:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
'pusher' => [
|
'pusher' => [
|
||||||
|
|
@ -75,9 +83,9 @@ When broadcasting events from your Laravel application to the WebSocket server,
|
||||||
'options' => [
|
'options' => [
|
||||||
'cluster' => env('PUSHER_APP_CLUSTER'),
|
'cluster' => env('PUSHER_APP_CLUSTER'),
|
||||||
'encrypted' => true,
|
'encrypted' => true,
|
||||||
'host' => '127.0.0.1',
|
'host' => env('PUSHER_APP_HOST', '127.0.0.1'),
|
||||||
'port' => 6001,
|
'port' => env('PUSHER_APP_PORT', 6001),
|
||||||
'scheme' => 'https',
|
'scheme' => env('PUSHER_APP_SCHEME', 'http'),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
@ -100,7 +108,7 @@ Make sure that you replace `YOUR-USERNAME` with your Mac username and `VALET-SIT
|
||||||
|
|
||||||
'capath' => env('LARAVEL_WEBSOCKETS_SSL_CA', null),
|
'capath' => env('LARAVEL_WEBSOCKETS_SSL_CA', null),
|
||||||
|
|
||||||
'local_pk' => 'local_pk' => '/Users/YOUR-USERNAME/.config/valet/Certificates/VALET-SITE.TLD.key',
|
'local_pk' => '/Users/YOUR-USERNAME/.config/valet/Certificates/VALET-SITE.TLD.key',
|
||||||
|
|
||||||
'passphrase' => env('LARAVEL_WEBSOCKETS_SSL_PASSPHRASE', null),
|
'passphrase' => env('LARAVEL_WEBSOCKETS_SSL_PASSPHRASE', null),
|
||||||
|
|
||||||
|
|
@ -124,13 +132,13 @@ You also need to disable SSL verification.
|
||||||
'options' => [
|
'options' => [
|
||||||
'cluster' => env('PUSHER_APP_CLUSTER'),
|
'cluster' => env('PUSHER_APP_CLUSTER'),
|
||||||
'encrypted' => true,
|
'encrypted' => true,
|
||||||
'host' => '127.0.0.1',
|
'host' => env('PUSHER_APP_HOST', '127.0.0.1'),
|
||||||
'port' => 6001,
|
'port' => env('PUSHER_APP_PORT', 6001),
|
||||||
'scheme' => 'https',
|
'scheme' => env('PUSHER_APP_SCHEME', 'http'),
|
||||||
'curl_options' => [
|
'curl_options' => [
|
||||||
CURLOPT_SSL_VERIFYHOST => 0,
|
CURLOPT_SSL_VERIFYHOST => 0,
|
||||||
CURLOPT_SSL_VERIFYPEER => 0,
|
CURLOPT_SSL_VERIFYPEER => 0,
|
||||||
]
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
@ -264,28 +272,20 @@ You know you've reached this limit of your Nginx error logs contain similar mess
|
||||||
|
|
||||||
Remember to restart your Nginx after you've modified the `worker_connections`.
|
Remember to restart your Nginx after you've modified the `worker_connections`.
|
||||||
|
|
||||||
### Example using Caddy
|
### Example using Caddy v2
|
||||||
|
|
||||||
[Caddy](https://caddyserver.com) can also be used to automatically obtain a TLS certificate from Let's Encrypt and terminate TLS before proxying to your echo server.
|
[Caddy](https://caddyserver.com) can also be used to automatically obtain a TLS certificate from Let's Encrypt and terminate TLS before proxying to your websocket server.
|
||||||
|
|
||||||
An example configuration would look like this:
|
An example configuration would look like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
socket.yourapp.tld {
|
socket.yourapp.tld {
|
||||||
rewrite / {
|
@ws {
|
||||||
if {>Connection} has Upgrade
|
header Connection *Upgrade*
|
||||||
if {>Upgrade} is websocket
|
header Upgrade websocket
|
||||||
to /websocket-proxy/{path}?{query}
|
|
||||||
}
|
}
|
||||||
|
reverse_proxy @ws 127.0.0.1:6001
|
||||||
proxy /websocket-proxy 127.0.0.1:6001 {
|
|
||||||
without /special-websocket-url
|
|
||||||
transparent
|
|
||||||
websocket
|
|
||||||
}
|
|
||||||
|
|
||||||
tls youremail.com
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note the `to /websocket-proxy`, this is a dummy path to allow the `proxy` directive to only proxy on websocket connections. This should be a path that will never be used by your application's routing. Also, note that you should change `127.0.0.1` to the hostname of your websocket server. For example, if you're running in a Docker environment, this might be the container name of your websocket server.
|
Note that you should change `127.0.0.1` to the hostname of your websocket server. For example, if you're running in a Docker environment, this might be the container name of your websocket server.
|
||||||
|
|
|
||||||
|
|
@ -71,35 +71,12 @@ protected function schedule(Schedule $schedule)
|
||||||
|
|
||||||
Each app contains an `enable_statistics` that defines wether that app generates statistics or not. The statistics are being stored for the `interval_in_seconds` seconds and then they are inserted in the database.
|
Each app contains an `enable_statistics` that defines wether that app generates statistics or not. The statistics are being stored for the `interval_in_seconds` seconds and then they are inserted in the database.
|
||||||
|
|
||||||
However, to disable it entirely and void any incoming statistic, you can uncomment the following line in the config:
|
However, to disable it entirely and void any incoming statistic, you can call `--disable-statistics` when running the server command:
|
||||||
|
|
||||||
```php
|
```bash
|
||||||
/*
|
php artisan websockets:serve --disable-statistics
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Statistics Logger Handler
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| The Statistics Logger will, by default, handle the incoming statistics,
|
|
||||||
| store them into an array and then store them into the database
|
|
||||||
| on each interval.
|
|
||||||
|
|
|
||||||
| You can opt-in to avoid any statistics storage by setting the logger
|
|
||||||
| to the built-in NullLogger.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 'logger' => \BeyondCode\LaravelWebSockets\Statistics\Logger\MemoryStatisticsLogger::class,
|
|
||||||
'logger' => \BeyondCode\LaravelWebSockets\Statistics\Logger\NullStatisticsLogger::class, // use the `NullStatisticsLogger` instead
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Custom Statistics Drivers
|
|
||||||
|
|
||||||
By default, the package comes with a few drivers like the Database driver which stores the data into the database.
|
|
||||||
|
|
||||||
You should add your custom drivers under the `statistics` key in `websockets.php` and create a driver class that implements the `\BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver` interface.
|
|
||||||
|
|
||||||
Take a quick look at the `\BeyondCode\LaravelWebSockets\Statistics\Drivers\DatabaseDriver` driver to see how to perform your integration.
|
|
||||||
|
|
||||||
## Event Creator
|
## Event Creator
|
||||||
|
|
||||||
The dashboard also comes with an easy-to-use event creator, that lets you manually send events to your channels.
|
The dashboard also comes with an easy-to-use event creator, that lets you manually send events to your channels.
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,7 @@ Here is another benchmark that was run on a 2GB Digital Ocean droplet with 2 CPU
|
||||||

|

|
||||||
|
|
||||||
Make sure to take a look at the [Deployment Tips](/docs/laravel-websockets/faq/deploying) to find out how to improve your specific setup.
|
Make sure to take a look at the [Deployment Tips](/docs/laravel-websockets/faq/deploying) to find out how to improve your specific setup.
|
||||||
|
|
||||||
|
# Horizontal Scaling
|
||||||
|
|
||||||
|
When deploying to multi-node environments, you will notice that the server won't behave correctly. Check [Horizontal Scaling](../horizontal-scaling/getting-started.md) section.
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsSe
|
||||||
|
|
||||||
# Statistics
|
# Statistics
|
||||||
|
|
||||||
This package comes with a migration to store statistic information while running your WebSocket server. For more info, check the [Debug Dashboard](../debugging/dashboard.md) section.
|
This package comes with migrations to store statistic information while running your WebSocket server. For more info, check the [Debug Dashboard](../debugging/dashboard.md) section.
|
||||||
|
|
||||||
You can publish the migration file using:
|
You can publish the migration file using:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@ order: 1
|
||||||
---
|
---
|
||||||
|
|
||||||
# Laravel WebSockets 🛰
|
# Laravel WebSockets 🛰
|
||||||
|
|
||||||
WebSockets for Laravel. Done right.
|
WebSockets for Laravel. Done right.
|
||||||
|
|
||||||
Laravel WebSockets is a package for Laravel 5.7 and up that will get your application started with WebSockets in no-time! It has a drop-in Pusher API replacement, has a debug dashboard, realtime statistics and even allows you to create custom WebSocket controllers.
|
Laravel WebSockets is a package for Laravel that will get your application started with WebSockets in no-time! It has a drop-in Pusher API replacement, has a debug dashboard, realtime statistics and even allows you to create custom WebSocket controllers.
|
||||||
|
|
||||||
Once installed, you can start it with one simple command:
|
Once installed, you can start it with one simple command:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,54 +15,20 @@ For example, Redis does a great job by encapsulating the both the way of notifyi
|
||||||
|
|
||||||
## Configure the replication
|
## Configure the replication
|
||||||
|
|
||||||
To enable the replication, simply change the `replication.driver` name in the `websockets.php` file:
|
To enable the replication, simply change the `replication.mode` name in the `websockets.php` file:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
'replication' => [
|
'replication' => [
|
||||||
|
|
||||||
'driver' => 'redis',
|
'mode' => 'redis',
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Now, when your app broadcasts the message, it will make sure the connection reaches other servers which are under the same load balancer.
|
||||||
|
|
||||||
The available drivers for replication are:
|
The available drivers for replication are:
|
||||||
|
|
||||||
- [Redis](redis)
|
- [Redis](redis)
|
||||||
|
|
||||||
## Configure the Broadcasting driver
|
|
||||||
|
|
||||||
Laravel WebSockets comes with an additional `websockets` broadcaster driver that accepts configurations like the Pusher driver, but will make sure the broadcasting will work across all websocket servers:
|
|
||||||
|
|
||||||
```php
|
|
||||||
'connections' => [
|
|
||||||
'pusher' => [
|
|
||||||
...
|
|
||||||
],
|
|
||||||
|
|
||||||
'websockets' => [
|
|
||||||
'driver' => 'websockets',
|
|
||||||
'key' => env('PUSHER_APP_KEY'),
|
|
||||||
'secret' => env('PUSHER_APP_SECRET'),
|
|
||||||
'app_id' => env('PUSHER_APP_ID'),
|
|
||||||
'options' => [
|
|
||||||
'cluster' => env('PUSHER_APP_CLUSTER'),
|
|
||||||
'encrypted' => true,
|
|
||||||
'host' => '127.0.0.1',
|
|
||||||
'port' => 6001,
|
|
||||||
'curl_options' => [
|
|
||||||
CURLOPT_SSL_VERIFYHOST => 0,
|
|
||||||
CURLOPT_SSL_VERIFYPEER => 0,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure to change the `BROADCAST_DRIVER`:
|
|
||||||
|
|
||||||
```
|
|
||||||
BROADCAST_DRIVER=websockets
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, when your app broadcasts the message, it will make sure the connection reaches other servers which are under the same load balancer.
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,20 @@
|
||||||
---
|
---
|
||||||
title: Redis
|
title: Redis Mode
|
||||||
order: 2
|
order: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
## Configure the Redis driver
|
# Redis Mode
|
||||||
|
|
||||||
To enable the replication, simply change the `replication.driver` name in the `websockets.php` file to `redis`:
|
Redis has the powerful ability to act both as a key-value store and as a PubSub service. This way, the connected servers will communicate between them whenever a message hits the server, so you can scale out to any amount of servers while preserving the WebSockets functionalities.
|
||||||
|
|
||||||
|
## Configure Redis mode
|
||||||
|
|
||||||
|
To enable the replication, simply change the `replication.mode` name in the `websockets.php` file to `redis`:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
'replication' => [
|
'replication' => [
|
||||||
|
|
||||||
'driver' => 'redis',
|
'mode' => 'redis',
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
@ -22,7 +26,7 @@ You can set the connection name to the Redis database under `redis`:
|
||||||
```php
|
```php
|
||||||
'replication' => [
|
'replication' => [
|
||||||
|
|
||||||
...
|
'modes' =>
|
||||||
|
|
||||||
'redis' => [
|
'redis' => [
|
||||||
|
|
||||||
|
|
@ -30,8 +34,9 @@ You can set the connection name to the Redis database under `redis`:
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
||||||
The connections can be found in your `config/database.php` file, under the `redis` key. It defaults to connection `default`.
|
The connections can be found in your `config/database.php` file, under the `redis` key.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -252,7 +252,7 @@
|
||||||
form: {
|
form: {
|
||||||
channel: null,
|
channel: null,
|
||||||
event: null,
|
event: null,
|
||||||
data: null,
|
data: {},
|
||||||
},
|
},
|
||||||
logs: [],
|
logs: [],
|
||||||
},
|
},
|
||||||
|
|
@ -261,15 +261,15 @@
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
if (this.refreshTicker) {
|
if (this.refreshTicker) {
|
||||||
this.clearRefreshInterval();
|
this.stopRefreshInterval();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
connected (newVal) {
|
connected (newVal) {
|
||||||
newVal ? this.startRefreshInterval() : this.clearRefreshInterval();
|
newVal ? this.startRefreshInterval() : this.stopRefreshInterval();
|
||||||
},
|
},
|
||||||
autoRefresh (newVal) {
|
autoRefresh (newVal) {
|
||||||
newVal ? this.startRefreshInterval() : this.clearRefreshInterval();
|
newVal ? this.startRefreshInterval() : this.stopRefreshInterval();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -314,7 +314,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
this.pusher.connection.bind('error', event => {
|
this.pusher.connection.bind('error', event => {
|
||||||
if (event.error.data.code === 4100) {
|
if (event.data.code === 4100) {
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
this.logs = [];
|
this.logs = [];
|
||||||
this.chart = null;
|
this.chart = null;
|
||||||
|
|
@ -347,14 +347,14 @@
|
||||||
name: '# Peak Connections'
|
name: '# Peak Connections'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
x: data.websocket_message_count.x,
|
x: data.websocket_messages_count.x,
|
||||||
y: data.websocket_message_count.y,
|
y: data.websocket_messages_count.y,
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
name: '# Websocket Messages'
|
name: '# Websocket Messages'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
x: data.api_message_count.x,
|
x: data.api_messages_count.x,
|
||||||
y: data.api_message_count.y,
|
y: data.api_messages_count.y,
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
name: '# API Messages'
|
name: '# API Messages'
|
||||||
},
|
},
|
||||||
|
|
@ -395,9 +395,9 @@
|
||||||
|
|
||||||
let payload = {
|
let payload = {
|
||||||
_token: '{{ csrf_token() }}',
|
_token: '{{ csrf_token() }}',
|
||||||
|
appId: this.app.id,
|
||||||
key: this.app.key,
|
key: this.app.key,
|
||||||
secret: this.app.secret,
|
secret: this.app.secret,
|
||||||
appId: this.app.id,
|
|
||||||
channel: this.form.channel,
|
channel: this.form.channel,
|
||||||
event: this.form.event,
|
event: this.form.event,
|
||||||
data: JSON.stringify(this.form.data),
|
data: JSON.stringify(this.form.data),
|
||||||
|
|
@ -405,13 +405,7 @@
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post('/event', payload)
|
.post('/event', payload)
|
||||||
.then(() => {
|
.then(() => {})
|
||||||
this.form = {
|
|
||||||
channel: null,
|
|
||||||
event: null,
|
|
||||||
data: null,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
alert('Error sending event.');
|
alert('Error sending event.');
|
||||||
})
|
})
|
||||||
|
|
@ -430,10 +424,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,10 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\HttpApi\Controllers;
|
namespace BeyondCode\LaravelWebSockets\API;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||||
use BeyondCode\LaravelWebSockets\QueryParameters;
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
use BeyondCode\LaravelWebSockets\Server\QueryParameters;
|
||||||
use Exception;
|
use Exception;
|
||||||
use GuzzleHttp\Psr7\Response;
|
use GuzzleHttp\Psr7\Response;
|
||||||
use GuzzleHttp\Psr7\ServerRequest;
|
use GuzzleHttp\Psr7\ServerRequest;
|
||||||
|
|
@ -47,14 +47,14 @@ 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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the request.
|
* Initialize the request.
|
||||||
*
|
*
|
||||||
* @param \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager $channelManager
|
* @param ChannelManager $channelManager
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function __construct(ChannelManager $channelManager)
|
public function __construct(ChannelManager $channelManager)
|
||||||
|
|
@ -192,6 +192,10 @@ abstract class Controller implements HttpServerInterface
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($response instanceof HttpException) {
|
||||||
|
throw $response;
|
||||||
|
}
|
||||||
|
|
||||||
$this->sendAndClose($connection, $response);
|
$this->sendAndClose($connection, $response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,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());
|
||||||
|
|
@ -247,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\Http\Request;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
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,77 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\API;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Channels\Channel;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
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,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\API;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
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,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\API;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||||
|
use BeyondCode\LaravelWebSockets\Facades\StatisticsCollector;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
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 = [
|
||||||
|
'event' => $request->name,
|
||||||
|
'channel' => $channelName,
|
||||||
|
'data' => $request->data,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($channel) {
|
||||||
|
$channel->broadcastLocallyToEveryoneExcept(
|
||||||
|
(object) $payload,
|
||||||
|
$request->socket_id,
|
||||||
|
$request->appId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->channelManager->broadcastAcrossServers(
|
||||||
|
$request->appId, $request->socket_id, $channelName, (object) $payload
|
||||||
|
);
|
||||||
|
|
||||||
|
StatisticsCollector::apiMessage($request->appId);
|
||||||
|
|
||||||
|
DashboardLogger::log($request->appId, DashboardLogger::TYPE_API_MESSAGE, [
|
||||||
|
'event' => $request->name,
|
||||||
|
'channel' => $channelName,
|
||||||
|
'payload' => $request->data,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $request->json()->all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Apps;
|
namespace BeyondCode\LaravelWebSockets\Apps;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Exceptions\InvalidApp;
|
use BeyondCode\LaravelWebSockets\Contracts\AppManager;
|
||||||
|
|
||||||
class App
|
class App
|
||||||
{
|
{
|
||||||
/** @var int */
|
/** @var string|int */
|
||||||
public $id;
|
public $id;
|
||||||
|
|
||||||
/** @var string */
|
/** @var string */
|
||||||
|
|
@ -39,7 +39,7 @@ class App
|
||||||
/**
|
/**
|
||||||
* Find the app by id.
|
* Find the app by id.
|
||||||
*
|
*
|
||||||
* @param mixed $appId
|
* @param string|int $appId
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||||
*/
|
*/
|
||||||
public static function findById($appId)
|
public static function findById($appId)
|
||||||
|
|
@ -50,7 +50,7 @@ class App
|
||||||
/**
|
/**
|
||||||
* Find the app by app key.
|
* Find the app by app key.
|
||||||
*
|
*
|
||||||
* @param mixed $appId
|
* @param string $appKey
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||||
*/
|
*/
|
||||||
public static function findByKey($appKey): ?self
|
public static function findByKey($appKey): ?self
|
||||||
|
|
@ -61,7 +61,7 @@ class App
|
||||||
/**
|
/**
|
||||||
* Find the app by app secret.
|
* Find the app by app secret.
|
||||||
*
|
*
|
||||||
* @param mixed $appId
|
* @param string $appSecret
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||||
*/
|
*/
|
||||||
public static function findBySecret($appSecret): ?self
|
public static function findBySecret($appSecret): ?self
|
||||||
|
|
@ -72,22 +72,13 @@ class App
|
||||||
/**
|
/**
|
||||||
* Initialize the Web Socket app instance.
|
* Initialize the Web Socket app instance.
|
||||||
*
|
*
|
||||||
* @param mixed $appId
|
* @param string|int $appId
|
||||||
* @param mixed $key
|
* @param string $key
|
||||||
* @param mixed $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();
|
||||||
}
|
}
|
||||||
|
|
@ -38,46 +40,40 @@ class ConfigAppManager implements AppManager
|
||||||
/**
|
/**
|
||||||
* Get app by id.
|
* Get app by id.
|
||||||
*
|
*
|
||||||
* @param mixed $appId
|
* @param string|int $appId
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||||
*/
|
*/
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get app by app key.
|
* Get app by app key.
|
||||||
*
|
*
|
||||||
* @param mixed $appKey
|
* @param string $appKey
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||||
*/
|
*/
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get app by secret.
|
* Get app by secret.
|
||||||
*
|
*
|
||||||
* @param mixed $appSecret
|
* @param string $appSecret
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||||
*/
|
*/
|
||||||
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,521 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\ChannelManagers;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Channels\Channel;
|
||||||
|
use BeyondCode\LaravelWebSockets\Channels\PresenceChannel;
|
||||||
|
use BeyondCode\LaravelWebSockets\Channels\PrivateChannel;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
|
use BeyondCode\LaravelWebSockets\Helpers;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Cache\ArrayLock;
|
||||||
|
use Illuminate\Cache\ArrayStore;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use React\EventLoop\LoopInterface;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
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 = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of users by socket and their attached id.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $userSockets = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wether the current instance accepts new connections.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected $acceptsNewConnections = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ArrayStore instance of locks.
|
||||||
|
*
|
||||||
|
* @var \Illuminate\Cache\ArrayStore
|
||||||
|
*/
|
||||||
|
protected $store;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The lock name to use on Array to avoid multiple
|
||||||
|
* actions that might lead to multiple processings.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected static $lockName = 'laravel-websockets:channel-manager:lock';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new channel manager instance.
|
||||||
|
*
|
||||||
|
* @param LoopInterface $loop
|
||||||
|
* @param string|null $factoryClass
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(LoopInterface $loop, $factoryClass = null)
|
||||||
|
{
|
||||||
|
$this->store = new ArrayStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 the local connections, regardless of the channel
|
||||||
|
* they are connected to.
|
||||||
|
*
|
||||||
|
* @return \React\Promise\PromiseInterface
|
||||||
|
*/
|
||||||
|
public function getLocalConnections(): PromiseInterface
|
||||||
|
{
|
||||||
|
$connections = collect($this->channels)
|
||||||
|
->map(function ($channelsWithConnections, $appId) {
|
||||||
|
return collect($channelsWithConnections)->values();
|
||||||
|
})
|
||||||
|
->values()->collapse()
|
||||||
|
->map(function ($channel) {
|
||||||
|
return collect($channel->getConnections());
|
||||||
|
})
|
||||||
|
->values()->collapse()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
return Helpers::createFulfilledPromise($connections);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Helpers::createFulfilledPromise(
|
||||||
|
$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 PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function unsubscribeFromAllChannels(ConnectionInterface $connection): PromiseInterface
|
||||||
|
{
|
||||||
|
if (! isset($connection->app)) {
|
||||||
|
return new FuilfilledPromise(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Helpers::createFulfilledPromise(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe the connection to a specific channel.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param string $channelName
|
||||||
|
* @param stdClass $payload
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function subscribeToChannel(ConnectionInterface $connection, string $channelName, stdClass $payload): PromiseInterface
|
||||||
|
{
|
||||||
|
$channel = $this->findOrCreate($connection->app->id, $channelName);
|
||||||
|
|
||||||
|
return Helpers::createFulfilledPromise(
|
||||||
|
$channel->subscribe($connection, $payload)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe the connection from the channel.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param string $channelName
|
||||||
|
* @param stdClass $payload
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function unsubscribeFromChannel(ConnectionInterface $connection, string $channelName, stdClass $payload): PromiseInterface
|
||||||
|
{
|
||||||
|
$channel = $this->findOrCreate($connection->app->id, $channelName);
|
||||||
|
|
||||||
|
return Helpers::createFulfilledPromise(
|
||||||
|
$channel->unsubscribe($connection, $payload)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe the connection to a specific channel, returning
|
||||||
|
* a promise containing the amount of connections.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @return PromiseInterface[int]
|
||||||
|
*/
|
||||||
|
public function subscribeToApp($appId): PromiseInterface
|
||||||
|
{
|
||||||
|
return Helpers::createFulfilledPromise(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe the connection from the channel, returning
|
||||||
|
* a promise containing the amount of connections after decrement.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @return PromiseInterface[int]
|
||||||
|
*/
|
||||||
|
public function unsubscribeFromApp($appId): PromiseInterface
|
||||||
|
{
|
||||||
|
return Helpers::createFulfilledPromise(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the connections count on the app
|
||||||
|
* for the current server instance.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string|null $channelName
|
||||||
|
* @return PromiseInterface[int]
|
||||||
|
*/
|
||||||
|
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 PromiseInterface[int]
|
||||||
|
*/
|
||||||
|
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|null $socketId
|
||||||
|
* @param string $channel
|
||||||
|
* @param stdClass $payload
|
||||||
|
* @param string|null $serverId
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function broadcastAcrossServers($appId, ?string $socketId, string $channel, stdClass $payload, string $serverId = null): PromiseInterface
|
||||||
|
{
|
||||||
|
return Helpers::createFulfilledPromise(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the user when it joined a presence channel.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param stdClass $user
|
||||||
|
* @param string $channel
|
||||||
|
* @param stdClass $payload
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function userJoinedPresenceChannel(ConnectionInterface $connection, stdClass $user, string $channel, stdClass $payload): PromiseInterface
|
||||||
|
{
|
||||||
|
$this->users["{$connection->app->id}:{$channel}"][$connection->socketId] = json_encode($user);
|
||||||
|
$this->userSockets["{$connection->app->id}:{$channel}:{$user->user_id}"][] = $connection->socketId;
|
||||||
|
|
||||||
|
return Helpers::createFulfilledPromise(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the user when it left a presence channel.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param stdClass $user
|
||||||
|
* @param string $channel
|
||||||
|
* @param stdClass $payload
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function userLeftPresenceChannel(ConnectionInterface $connection, stdClass $user, string $channel): PromiseInterface
|
||||||
|
{
|
||||||
|
unset($this->users["{$connection->app->id}:{$channel}"][$connection->socketId]);
|
||||||
|
|
||||||
|
$deletableSocketKey = array_search(
|
||||||
|
$connection->socketId,
|
||||||
|
$this->userSockets["{$connection->app->id}:{$channel}:{$user->user_id}"]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($deletableSocketKey !== false) {
|
||||||
|
unset($this->userSockets["{$connection->app->id}:{$channel}:{$user->user_id}"][$deletableSocketKey]);
|
||||||
|
|
||||||
|
if (count($this->userSockets["{$connection->app->id}:{$channel}:{$user->user_id}"]) === 0) {
|
||||||
|
unset($this->userSockets["{$connection->app->id}:{$channel}:{$user->user_id}"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Helpers::createFulfilledPromise(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
})->unique('user_id')->toArray();
|
||||||
|
|
||||||
|
return Helpers::createFulfilledPromise($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 Helpers::createFulfilledPromise($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 Helpers::createFulfilledPromise($results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the socket IDs for a presence channel member.
|
||||||
|
*
|
||||||
|
* @param string|int $userId
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string $channelName
|
||||||
|
* @return \React\Promise\PromiseInterface
|
||||||
|
*/
|
||||||
|
public function getMemberSockets($userId, $appId, $channelName): PromiseInterface
|
||||||
|
{
|
||||||
|
return Helpers::createFulfilledPromise(
|
||||||
|
$this->userSockets["{$appId}:{$channelName}:{$userId}"] ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep tracking the connections availability when they pong.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function connectionPonged(ConnectionInterface $connection): PromiseInterface
|
||||||
|
{
|
||||||
|
$connection->lastPongedAt = Carbon::now();
|
||||||
|
|
||||||
|
return $this->updateConnectionInChannels($connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the obsolete connections that didn't ponged in a while.
|
||||||
|
*
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function removeObsoleteConnections(): PromiseInterface
|
||||||
|
{
|
||||||
|
if (! $this->lock()->acquire()) {
|
||||||
|
return Helpers::createFulfilledPromise(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getLocalConnections()->then(function ($connections) {
|
||||||
|
foreach ($connections as $connection) {
|
||||||
|
$differenceInSeconds = $connection->lastPongedAt->diffInSeconds(Carbon::now());
|
||||||
|
|
||||||
|
if ($differenceInSeconds > 120) {
|
||||||
|
$this->unsubscribeFromAllChannels($connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Helpers::createFulfilledPromise(
|
||||||
|
$this->lock()->release()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the connection in all channels.
|
||||||
|
*
|
||||||
|
* @param ConnectionInterface $connection
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function updateConnectionInChannels($connection): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->getLocalChannels($connection->app->id)
|
||||||
|
->then(function ($channels) use ($connection) {
|
||||||
|
foreach ($channels as $channel) {
|
||||||
|
if ($channel->hasConnection($connection)) {
|
||||||
|
$channel->saveConnection($connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the current instance as unable to accept new connections.
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function declineNewConnections()
|
||||||
|
{
|
||||||
|
$this->acceptsNewConnections = false;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current server instance
|
||||||
|
* accepts new connections.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function acceptsNewConnections(): bool
|
||||||
|
{
|
||||||
|
return $this->acceptsNewConnections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a new ArrayLock instance to avoid race conditions.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Cache\CacheLock
|
||||||
|
*/
|
||||||
|
protected function lock()
|
||||||
|
{
|
||||||
|
return new ArrayLock($this->store, static::$lockName, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,797 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\ChannelManagers;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Channels\Channel;
|
||||||
|
use BeyondCode\LaravelWebSockets\Helpers;
|
||||||
|
use BeyondCode\LaravelWebSockets\Server\MockableConnection;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Clue\React\Redis\Client;
|
||||||
|
use Clue\React\Redis\Factory;
|
||||||
|
use Illuminate\Cache\RedisLock;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use React\EventLoop\LoopInterface;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Redis manager instance.
|
||||||
|
*
|
||||||
|
* @var \Illuminate\Redis\RedisManager
|
||||||
|
*/
|
||||||
|
protected $redis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The lock name to use on Redis to avoid multiple
|
||||||
|
* actions that might lead to multiple processings.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected static $lockName = 'laravel-websockets:channel-manager:lock';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
$this->redis = Redis::connection(
|
||||||
|
config('websockets.replication.modes.redis.connection', 'default')
|
||||||
|
);
|
||||||
|
|
||||||
|
$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 the local connections, regardless of the channel
|
||||||
|
* they are connected to.
|
||||||
|
*
|
||||||
|
* @return \React\Promise\PromiseInterface
|
||||||
|
*/
|
||||||
|
public function getLocalConnections(): PromiseInterface
|
||||||
|
{
|
||||||
|
return parent::getLocalConnections();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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->publishClient->smembers(
|
||||||
|
$this->getRedisKey($appId, null, ['channels'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove connection from all channels.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function unsubscribeFromAllChannels(ConnectionInterface $connection): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->getGlobalChannels($connection->app->id)
|
||||||
|
->then(function ($channels) use ($connection) {
|
||||||
|
foreach ($channels as $channel) {
|
||||||
|
$this->unsubscribeFromChannel($connection, $channel, new stdClass);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->then(function () use ($connection) {
|
||||||
|
return parent::unsubscribeFromAllChannels($connection);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe the connection to a specific channel.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param string $channelName
|
||||||
|
* @param stdClass $payload
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function subscribeToChannel(ConnectionInterface $connection, string $channelName, stdClass $payload): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->subscribeToTopic($connection->app->id, $channelName)
|
||||||
|
->then(function () use ($connection) {
|
||||||
|
return $this->addConnectionToSet($connection, Carbon::now());
|
||||||
|
})
|
||||||
|
->then(function () use ($connection, $channelName) {
|
||||||
|
return $this->addChannelToSet($connection->app->id, $channelName);
|
||||||
|
})
|
||||||
|
->then(function () use ($connection, $channelName) {
|
||||||
|
return $this->incrementSubscriptionsCount($connection->app->id, $channelName, 1);
|
||||||
|
})
|
||||||
|
->then(function () use ($connection, $channelName, $payload) {
|
||||||
|
return parent::subscribeToChannel($connection, $channelName, $payload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe the connection from the channel.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param string $channelName
|
||||||
|
* @param stdClass $payload
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function unsubscribeFromChannel(ConnectionInterface $connection, string $channelName, stdClass $payload): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->getGlobalConnectionsCount($connection->app->id, $channelName)
|
||||||
|
->then(function ($count) use ($connection, $channelName) {
|
||||||
|
if ($count === 0) {
|
||||||
|
// Make sure to not stay subscribed to the PubSub topic
|
||||||
|
// if there are no connections.
|
||||||
|
$this->unsubscribeFromTopic($connection->app->id, $channelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->decrementSubscriptionsCount($connection->app->id, $channelName)
|
||||||
|
->then(function ($count) use ($connection, $channelName) {
|
||||||
|
// If the total connections count gets to 0 after unsubscribe,
|
||||||
|
// try again to check & unsubscribe from the PubSub topic if needed.
|
||||||
|
if ($count < 1) {
|
||||||
|
$this->unsubscribeFromTopic($connection->app->id, $channelName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->then(function () use ($connection, $channelName) {
|
||||||
|
return $this->removeChannelFromSet($connection->app->id, $channelName);
|
||||||
|
})
|
||||||
|
->then(function () use ($connection) {
|
||||||
|
return $this->removeConnectionFromSet($connection);
|
||||||
|
})
|
||||||
|
->then(function () use ($connection, $channelName, $payload) {
|
||||||
|
return parent::unsubscribeFromChannel($connection, $channelName, $payload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe the connection to a specific channel, returning
|
||||||
|
* a promise containing the amount of connections.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @return PromiseInterface[int]
|
||||||
|
*/
|
||||||
|
public function subscribeToApp($appId): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->subscribeToTopic($appId)
|
||||||
|
->then(function () use ($appId) {
|
||||||
|
return $this->incrementSubscriptionsCount($appId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe the connection from the channel, returning
|
||||||
|
* a promise containing the amount of connections after decrement.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @return PromiseInterface[int]
|
||||||
|
*/
|
||||||
|
public function unsubscribeFromApp($appId): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->unsubscribeFromTopic($appId)
|
||||||
|
->then(function () use ($appId) {
|
||||||
|
return $this->decrementSubscriptionsCount($appId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the connections count on the app
|
||||||
|
* for the current server instance.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string|null $channelName
|
||||||
|
* @return PromiseInterface[int]
|
||||||
|
*/
|
||||||
|
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 PromiseInterface[int]
|
||||||
|
*/
|
||||||
|
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|null $socketId
|
||||||
|
* @param string $channel
|
||||||
|
* @param stdClass $payload
|
||||||
|
* @param string|null $serverId
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function broadcastAcrossServers($appId, ?string $socketId, string $channel, stdClass $payload, string $serverId = null): PromiseInterface
|
||||||
|
{
|
||||||
|
$payload->appId = $appId;
|
||||||
|
$payload->socketId = $socketId;
|
||||||
|
$payload->serverId = $serverId ?: $this->getServerId();
|
||||||
|
|
||||||
|
return $this->publishClient
|
||||||
|
->publish($this->getRedisKey($appId, $channel), json_encode($payload))
|
||||||
|
->then(function () use ($appId, $socketId, $channel, $payload, $serverId) {
|
||||||
|
return parent::broadcastAcrossServers($appId, $socketId, $channel, $payload, $serverId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the user when it joined a presence channel.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param stdClass $user
|
||||||
|
* @param string $channel
|
||||||
|
* @param stdClass $payload
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function userJoinedPresenceChannel(ConnectionInterface $connection, stdClass $user, string $channel, stdClass $payload): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->storeUserData($connection->app->id, $channel, $connection->socketId, json_encode($user))
|
||||||
|
->then(function () use ($connection, $channel, $user) {
|
||||||
|
return $this->addUserSocket($connection->app->id, $channel, $user, $connection->socketId);
|
||||||
|
})
|
||||||
|
->then(function () use ($connection, $user, $channel, $payload) {
|
||||||
|
return parent::userJoinedPresenceChannel($connection, $user, $channel, $payload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the user when it left a presence channel.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param stdClass $user
|
||||||
|
* @param string $channel
|
||||||
|
* @param stdClass $payload
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function userLeftPresenceChannel(ConnectionInterface $connection, stdClass $user, string $channel): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->removeUserData($connection->app->id, $channel, $connection->socketId)
|
||||||
|
->then(function () use ($connection, $channel, $user) {
|
||||||
|
return $this->removeUserSocket($connection->app->id, $channel, $user, $connection->socketId);
|
||||||
|
})
|
||||||
|
->then(function () use ($connection, $user, $channel) {
|
||||||
|
return parent::userLeftPresenceChannel($connection, $user, $channel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the presence channel members.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string $channel
|
||||||
|
* @return \React\Promise\PromiseInterface[array]
|
||||||
|
*/
|
||||||
|
public function getChannelMembers($appId, string $channel): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->publishClient
|
||||||
|
->hgetall($this->getRedisKey($appId, $channel, ['users']))
|
||||||
|
->then(function ($list) {
|
||||||
|
return collect(Helpers::redisListToArray($list))->map(function ($user) {
|
||||||
|
return json_decode($user);
|
||||||
|
})->unique('user_id')->toArray();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a member from a presence channel based on connection.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param string $channel
|
||||||
|
* @return \React\Promise\PromiseInterface[null|array]
|
||||||
|
*/
|
||||||
|
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[array]
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the socket IDs for a presence channel member.
|
||||||
|
*
|
||||||
|
* @param string|int $userId
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string $channelName
|
||||||
|
* @return \React\Promise\PromiseInterface[array]
|
||||||
|
*/
|
||||||
|
public function getMemberSockets($userId, $appId, $channelName): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->publishClient->smembers(
|
||||||
|
$this->getRedisKey($appId, $channelName, [$userId, 'userSockets'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep tracking the connections availability when they pong.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function connectionPonged(ConnectionInterface $connection): PromiseInterface
|
||||||
|
{
|
||||||
|
// This will update the score with the current timestamp.
|
||||||
|
return $this->addConnectionToSet($connection, Carbon::now())
|
||||||
|
->then(function () use ($connection) {
|
||||||
|
return parent::connectionPonged($connection);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the obsolete connections that didn't ponged in a while.
|
||||||
|
*
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function removeObsoleteConnections(): PromiseInterface
|
||||||
|
{
|
||||||
|
$this->lock()->get(function () {
|
||||||
|
$this->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
|
||||||
|
->then(function ($connections) {
|
||||||
|
foreach ($connections as $socketId => $appId) {
|
||||||
|
$connection = $this->fakeConnectionForApp($appId, $socketId);
|
||||||
|
|
||||||
|
$this->unsubscribeFromAllChannels($connection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return parent::removeObsoleteConnections();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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->broadcastLocallyToEveryoneExcept($payload, $socketId, $appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Redis client used by other classes.
|
||||||
|
*
|
||||||
|
* @return Client
|
||||||
|
*/
|
||||||
|
public function getRedisClient()
|
||||||
|
{
|
||||||
|
return $this->getPublishClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the unique identifier for the server.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getServerId(): string
|
||||||
|
{
|
||||||
|
return $this->serverId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the subscribed count number.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string|null $channel
|
||||||
|
* @param int $increment
|
||||||
|
* @return PromiseInterface[int]
|
||||||
|
*/
|
||||||
|
public function incrementSubscriptionsCount($appId, string $channel = null, int $increment = 1): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->publishClient->hincrby(
|
||||||
|
$this->getRedisKey($appId, $channel, ['stats']), 'connections', $increment
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrement the subscribed count number.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string|null $channel
|
||||||
|
* @param int $decrement
|
||||||
|
* @return PromiseInterface[int]
|
||||||
|
*/
|
||||||
|
public function decrementSubscriptionsCount($appId, string $channel = null, int $increment = 1): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->incrementSubscriptionsCount($appId, $channel, $increment * -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the connection to the sorted list.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param \DateTime|string|null $moment
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function addConnectionToSet(ConnectionInterface $connection, $moment = null): PromiseInterface
|
||||||
|
{
|
||||||
|
$moment = $moment ? Carbon::parse($moment) : Carbon::now();
|
||||||
|
|
||||||
|
return $this->publishClient->zadd(
|
||||||
|
$this->getRedisKey(null, null, ['sockets']),
|
||||||
|
$moment->format('U'), "{$connection->app->id}:{$connection->socketId}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the connection from the sorted list.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function removeConnectionFromSet(ConnectionInterface $connection): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->publishClient->zrem(
|
||||||
|
$this->getRedisKey(null, null, ['sockets']),
|
||||||
|
"{$connection->app->id}:{$connection->socketId}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the connections from the sorted list, with last
|
||||||
|
* connection between certain timestamps.
|
||||||
|
*
|
||||||
|
* @param int $start
|
||||||
|
* @param int $stop
|
||||||
|
* @param bool $strict
|
||||||
|
* @return PromiseInterface[array]
|
||||||
|
*/
|
||||||
|
public function getConnectionsFromSet(int $start = 0, int $stop = 0, bool $strict = true): PromiseInterface
|
||||||
|
{
|
||||||
|
if ($strict) {
|
||||||
|
$start = "({$start}";
|
||||||
|
$stop = "({$stop}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->publishClient
|
||||||
|
->zrangebyscore($this->getRedisKey(null, null, ['sockets']), $start, $stop)
|
||||||
|
->then(function ($list) {
|
||||||
|
return collect($list)->mapWithKeys(function ($appWithSocket) {
|
||||||
|
[$appId, $socketId] = explode(':', $appWithSocket);
|
||||||
|
|
||||||
|
return [$socketId => $appId];
|
||||||
|
})->toArray();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a channel to the set list.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string $channel
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function addChannelToSet($appId, string $channel): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->publishClient->sadd(
|
||||||
|
$this->getRedisKey($appId, null, ['channels']), $channel
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a channel from the set list.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string $channel
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function removeChannelFromSet($appId, string $channel): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->publishClient->srem(
|
||||||
|
$this->getRedisKey($appId, null, ['channels']), $channel
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set data for a topic. Might be used for the presence channels.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string|null $channel
|
||||||
|
* @param string $key
|
||||||
|
* @param string $data
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function storeUserData($appId, string $channel = null, string $key, $data): PromiseInterface
|
||||||
|
{
|
||||||
|
return $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): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->publishClient->hdel(
|
||||||
|
$this->getRedisKey($appId, $channel, ['users']), $key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to the topic for the app, or app and channel.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string|null $channel
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
public function subscribeToTopic($appId, string $channel = null): PromiseInterface
|
||||||
|
{
|
||||||
|
return $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 PromiseInterface
|
||||||
|
*/
|
||||||
|
public function unsubscribeFromTopic($appId, string $channel = null): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->subscribeClient->unsubscribe(
|
||||||
|
$this->getRedisKey($appId, $channel)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the Presence Channel's User's Socket ID to a list.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string $channel
|
||||||
|
* @param stdClass $user
|
||||||
|
* @param string $socketId
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
protected function addUserSocket($appId, string $channel, stdClass $user, string $socketId): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->publishClient->sadd(
|
||||||
|
$this->getRedisKey($appId, $channel, [$user->user_id, 'userSockets']), $socketId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the Presence Channel's User's Socket ID from the list.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string $channel
|
||||||
|
* @param stdClass $user
|
||||||
|
* @param string $socketId
|
||||||
|
* @return PromiseInterface
|
||||||
|
*/
|
||||||
|
protected function removeUserSocket($appId, string $channel, stdClass $user, string $socketId): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->publishClient->srem(
|
||||||
|
$this->getRedisKey($appId, $channel, [$user->user_id, 'userSockets']), $socketId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Redis Keyspace name to handle subscriptions
|
||||||
|
* and other key-value sets.
|
||||||
|
*
|
||||||
|
* @param string|int|null $appId
|
||||||
|
* @param string|null $channel
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getRedisKey($appId = null, string $channel = null, array $suffixes = []): string
|
||||||
|
{
|
||||||
|
$prefix = config('database.redis.options.prefix', null);
|
||||||
|
|
||||||
|
$hash = "{$prefix}{$appId}";
|
||||||
|
|
||||||
|
if ($channel) {
|
||||||
|
$suffixes = array_merge([$channel], $suffixes);
|
||||||
|
}
|
||||||
|
|
||||||
|
$suffixes = implode(':', $suffixes);
|
||||||
|
|
||||||
|
if ($suffixes) {
|
||||||
|
$hash .= ":{$suffixes}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a new RedisLock instance to avoid race conditions.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Cache\CacheLock
|
||||||
|
*/
|
||||||
|
protected function lock()
|
||||||
|
{
|
||||||
|
return new RedisLock($this->redis, static::$lockName, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a fake connection for app that will mimick a connection
|
||||||
|
* by app ID and Socket ID to be able to be passed to the methods
|
||||||
|
* that accepts a connection class.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string $socketId
|
||||||
|
* @return ConnectionInterface
|
||||||
|
*/
|
||||||
|
public function fakeConnectionForApp($appId, string $socketId)
|
||||||
|
{
|
||||||
|
return new MockableConnection($appId, $socketId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,246 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Channels;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
|
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||||
|
use BeyondCode\LaravelWebSockets\Events\SubscribedToChannel;
|
||||||
|
use BeyondCode\LaravelWebSockets\Events\UnsubscribedFromChannel;
|
||||||
|
use BeyondCode\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
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 bool
|
||||||
|
*/
|
||||||
|
public function subscribe(ConnectionInterface $connection, stdClass $payload): bool
|
||||||
|
{
|
||||||
|
$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(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
SubscribedToChannel::dispatch(
|
||||||
|
$connection->app->id,
|
||||||
|
$connection->socketId,
|
||||||
|
$this->getName(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe connection from the channel.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function unsubscribe(ConnectionInterface $connection): bool
|
||||||
|
{
|
||||||
|
if (! $this->hasConnection($connection)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($this->connections[$connection->socketId]);
|
||||||
|
|
||||||
|
UnsubscribedFromChannel::dispatch(
|
||||||
|
$connection->app->id,
|
||||||
|
$connection->socketId,
|
||||||
|
$this->getName()
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given connection exists.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasConnection(ConnectionInterface $connection): bool
|
||||||
|
{
|
||||||
|
return isset($this->connections[$connection->socketId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the connection to the subscribers list.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public 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, null, $this->getName(), $payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast a payload to the locally-subscribed connections.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param \stdClass $payload
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function broadcastLocally($appId, stdClass $payload): bool
|
||||||
|
{
|
||||||
|
return $this->broadcast($appId, $payload, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, $socketId, $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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast the payload, but exclude a specific socket id.
|
||||||
|
*
|
||||||
|
* @param \stdClass $payload
|
||||||
|
* @param string|null $socketId
|
||||||
|
* @param string|int $appId
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function broadcastLocallyToEveryoneExcept(stdClass $payload, ?string $socketId, $appId)
|
||||||
|
{
|
||||||
|
return $this->broadcastToEveryoneExcept(
|
||||||
|
$payload, $socketId, $appId, false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,154 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Channels;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||||
|
use BeyondCode\LaravelWebSockets\Events\SubscribedToChannel;
|
||||||
|
use BeyondCode\LaravelWebSockets\Events\UnsubscribedFromChannel;
|
||||||
|
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 bool
|
||||||
|
* @throws InvalidSignature
|
||||||
|
*/
|
||||||
|
public function subscribe(ConnectionInterface $connection, stdClass $payload): bool
|
||||||
|
{
|
||||||
|
$this->verifySignature($connection, $payload);
|
||||||
|
|
||||||
|
$this->saveConnection($connection);
|
||||||
|
|
||||||
|
$user = json_decode($payload->channel_data);
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->userJoinedPresenceChannel($connection, $user, $this->getName(), $payload)
|
||||||
|
->then(function () use ($connection) {
|
||||||
|
$this->channelManager
|
||||||
|
->getChannelMembers($connection->app->id, $this->getName())
|
||||||
|
->then(function ($users) use ($connection) {
|
||||||
|
$hash = [];
|
||||||
|
|
||||||
|
foreach ($users as $socketId => $user) {
|
||||||
|
$hash[$user->user_id] = $user->user_info ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection->send(json_encode([
|
||||||
|
'event' => 'pusher_internal:subscription_succeeded',
|
||||||
|
'channel' => $this->getName(),
|
||||||
|
'data' => json_encode([
|
||||||
|
'presence' => [
|
||||||
|
'ids' => collect($users)->map(function ($user) {
|
||||||
|
return (string) $user->user_id;
|
||||||
|
})->values(),
|
||||||
|
'hash' => $hash,
|
||||||
|
'count' => count($users),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->then(function () use ($connection, $user, $payload) {
|
||||||
|
// The `pusher_internal:member_added` event is triggered when a user joins a channel.
|
||||||
|
// It's quite possible that a user can have multiple connections to the same channel
|
||||||
|
// (for example by having multiple browser tabs open)
|
||||||
|
// and in this case the events will only be triggered when the first tab is opened.
|
||||||
|
$this->channelManager
|
||||||
|
->getMemberSockets($user->user_id, $connection->app->id, $this->getName())
|
||||||
|
->then(function ($sockets) use ($payload, $connection, $user) {
|
||||||
|
if (count($sockets) === 1) {
|
||||||
|
$memberAddedPayload = [
|
||||||
|
'event' => 'pusher_internal:member_added',
|
||||||
|
'channel' => $this->getName(),
|
||||||
|
'data' => $payload->channel_data,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->broadcastToEveryoneExcept(
|
||||||
|
(object) $memberAddedPayload, $connection->socketId,
|
||||||
|
$connection->app->id
|
||||||
|
);
|
||||||
|
|
||||||
|
SubscribedToChannel::dispatch(
|
||||||
|
$connection->app->id,
|
||||||
|
$connection->socketId,
|
||||||
|
$this->getName(),
|
||||||
|
$user
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_SUBSCRIBED, [
|
||||||
|
'socketId' => $connection->socketId,
|
||||||
|
'channel' => $this->getName(),
|
||||||
|
'duplicate-connection' => count($sockets) > 1,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe connection from the channel.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function unsubscribe(ConnectionInterface $connection): bool
|
||||||
|
{
|
||||||
|
$truth = parent::unsubscribe($connection);
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->getChannelMember($connection, $this->getName())
|
||||||
|
->then(function ($user) {
|
||||||
|
return @json_decode($user);
|
||||||
|
})
|
||||||
|
->then(function ($user) use ($connection) {
|
||||||
|
if (! $user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->channelManager
|
||||||
|
->userLeftPresenceChannel($connection, $user, $this->getName())
|
||||||
|
->then(function () use ($connection, $user) {
|
||||||
|
// The `pusher_internal:member_removed` is triggered when a user leaves a channel.
|
||||||
|
// It's quite possible that a user can have multiple connections to the same channel
|
||||||
|
// (for example by having multiple browser tabs open)
|
||||||
|
// and in this case the events will only be triggered when the last one is closed.
|
||||||
|
$this->channelManager
|
||||||
|
->getMemberSockets($user->user_id, $connection->app->id, $this->getName())
|
||||||
|
->then(function ($sockets) use ($connection, $user) {
|
||||||
|
if (count($sockets) === 0) {
|
||||||
|
$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
|
||||||
|
);
|
||||||
|
|
||||||
|
UnsubscribedFromChannel::dispatch(
|
||||||
|
$connection->app->id,
|
||||||
|
$connection->socketId,
|
||||||
|
$this->getName(),
|
||||||
|
$user
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return $truth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
@ -14,13 +14,13 @@ class PrivateChannel extends Channel
|
||||||
* @see https://pusher.com/docs/pusher_protocol#presence-channel-events
|
* @see https://pusher.com/docs/pusher_protocol#presence-channel-events
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
* @param \stdClass $payload
|
* @param \stdClass $payload
|
||||||
* @return void
|
* @return bool
|
||||||
* @throws InvalidSignature
|
* @throws InvalidSignature
|
||||||
*/
|
*/
|
||||||
public function subscribe(ConnectionInterface $connection, stdClass $payload)
|
public function subscribe(ConnectionInterface $connection, stdClass $payload): bool
|
||||||
{
|
{
|
||||||
$this->verifySignature($connection, $payload);
|
$this->verifySignature($connection, $payload);
|
||||||
|
|
||||||
parent::subscribe($connection, $payload);
|
return parent::subscribe($connection, $payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Concerns;
|
||||||
|
|
||||||
|
use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster;
|
||||||
|
use Pusher\Pusher;
|
||||||
|
|
||||||
|
trait PushesToPusher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the right Pusher broadcaster for the used driver.
|
||||||
|
*
|
||||||
|
* @param array $app
|
||||||
|
* @return \Illuminate\Broadcasting\Broadcasters\Broadcaster
|
||||||
|
*/
|
||||||
|
public function getPusherBroadcaster(array $app)
|
||||||
|
{
|
||||||
|
return new PusherBroadcaster(
|
||||||
|
new Pusher(
|
||||||
|
$app['key'],
|
||||||
|
$app['secret'],
|
||||||
|
$app['id'],
|
||||||
|
config('broadcasting.connections.pusher.options', [])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Console\Commands;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Facades\StatisticsCollector;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class FlushCollectedStatistics extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'websockets:flush';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
protected $description = 'Flush the collected statistics.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the command.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->comment('Flushing the collected WebSocket Statistics...');
|
||||||
|
|
||||||
|
StatisticsCollector::flush();
|
||||||
|
|
||||||
|
$this->line('Flush complete!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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!'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,317 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Console\Commands;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
|
use BeyondCode\LaravelWebSockets\Facades\StatisticsCollector as StatisticsCollectorFacade;
|
||||||
|
use BeyondCode\LaravelWebSockets\Facades\WebSocketRouter;
|
||||||
|
use BeyondCode\LaravelWebSockets\Server\Loggers\ConnectionLogger;
|
||||||
|
use BeyondCode\LaravelWebSockets\Server\Loggers\HttpLogger;
|
||||||
|
use BeyondCode\LaravelWebSockets\Server\Loggers\WebSocketsLogger;
|
||||||
|
use BeyondCode\LaravelWebSockets\ServerFactory;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use React\EventLoop\Factory as LoopFactory;
|
||||||
|
|
||||||
|
class StartServer extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'websockets:serve
|
||||||
|
{--host=0.0.0.0}
|
||||||
|
{--port=6001}
|
||||||
|
{--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.}
|
||||||
|
{--loop : Programatically inject the loop.}
|
||||||
|
';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
protected $description = 'Start the LaravelWebSockets server.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the loop instance.
|
||||||
|
*
|
||||||
|
* @var \React\EventLoop\LoopInterface
|
||||||
|
*/
|
||||||
|
protected $loop;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Pusher server instance.
|
||||||
|
*
|
||||||
|
* @var \Ratchet\Server\IoServer
|
||||||
|
*/
|
||||||
|
public $server;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the command.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->loop = LoopFactory::create();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the command.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->configureLoggers();
|
||||||
|
|
||||||
|
$this->configureManagers();
|
||||||
|
|
||||||
|
$this->configureStatistics();
|
||||||
|
|
||||||
|
$this->configureRestartTimer();
|
||||||
|
|
||||||
|
$this->configureRoutes();
|
||||||
|
|
||||||
|
$this->configurePcntlSignal();
|
||||||
|
|
||||||
|
$this->configurePongTracker();
|
||||||
|
|
||||||
|
$this->startServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the loggers used for the console.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function configureLoggers()
|
||||||
|
{
|
||||||
|
$this->configureHttpLogger();
|
||||||
|
$this->configureMessageLogger();
|
||||||
|
$this->configureConnectionLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the managers that are not resolved
|
||||||
|
* in the package service provider.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function configureManagers()
|
||||||
|
{
|
||||||
|
$this->laravel->singleton(ChannelManager::class, function () {
|
||||||
|
$mode = config('websockets.replication.mode', 'local');
|
||||||
|
|
||||||
|
$class = config("websockets.replication.modes.{$mode}.channel_manager");
|
||||||
|
|
||||||
|
return new $class($this->loop);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the Statistics Collectors that
|
||||||
|
* are not resolved in the package service provider.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function configureStatistics()
|
||||||
|
{
|
||||||
|
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 restart timer.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function configureRestartTimer()
|
||||||
|
{
|
||||||
|
$this->lastRestart = $this->getLastRestart();
|
||||||
|
|
||||||
|
$this->loop->addPeriodicTimer(10, function () {
|
||||||
|
if ($this->getLastRestart() !== $this->lastRestart) {
|
||||||
|
$this->triggerSoftShutdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the routes for the server.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function configureRoutes()
|
||||||
|
{
|
||||||
|
WebSocketRouter::routes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the PCNTL signals for soft shutdown.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function configurePcntlSignal()
|
||||||
|
{
|
||||||
|
// When the process receives a SIGTERM or a SIGINT
|
||||||
|
// signal, it should mark the server as unavailable
|
||||||
|
// to receive new connections, close the current connections,
|
||||||
|
// then stopping the loop.
|
||||||
|
|
||||||
|
$this->loop->addSignal(SIGTERM, function () {
|
||||||
|
$this->line('Closing existing connections...');
|
||||||
|
|
||||||
|
$this->triggerSoftShutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->loop->addSignal(SIGINT, function () {
|
||||||
|
$this->line('Closing existing connections...');
|
||||||
|
|
||||||
|
$this->triggerSoftShutdown();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the tracker that will delete
|
||||||
|
* from the store the connections that.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function configurePongTracker()
|
||||||
|
{
|
||||||
|
$this->loop->addPeriodicTimer(10, function () {
|
||||||
|
$this->laravel
|
||||||
|
->make(ChannelManager::class)
|
||||||
|
->removeObsoleteConnections();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the HTTP logger class.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function configureHttpLogger()
|
||||||
|
{
|
||||||
|
$this->laravel->singleton(HttpLogger::class, function () {
|
||||||
|
return (new HttpLogger($this->output))
|
||||||
|
->enable($this->option('debug') ?: config('app.debug'))
|
||||||
|
->verbose($this->output->isVerbose());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the logger for messages.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function configureMessageLogger()
|
||||||
|
{
|
||||||
|
$this->laravel->singleton(WebSocketsLogger::class, function () {
|
||||||
|
return (new WebSocketsLogger($this->output))
|
||||||
|
->enable($this->option('debug') ?: config('app.debug'))
|
||||||
|
->verbose($this->output->isVerbose());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the server.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function startServer()
|
||||||
|
{
|
||||||
|
$this->info("Starting the WebSocket server on port {$this->option('port')}...");
|
||||||
|
|
||||||
|
$this->buildServer();
|
||||||
|
|
||||||
|
$this->server->run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the server instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function buildServer()
|
||||||
|
{
|
||||||
|
$this->server = new ServerFactory(
|
||||||
|
$this->option('host'), $this->option('port')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($loop = $this->option('loop')) {
|
||||||
|
$this->loop = $loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->server = $this->server
|
||||||
|
->setLoop($this->loop)
|
||||||
|
->withRoutes(WebSocketRouter::getRoutes())
|
||||||
|
->setConsoleOutput($this->output)
|
||||||
|
->createServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last time the server restarted.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
protected function getLastRestart()
|
||||||
|
{
|
||||||
|
return Cache::get(
|
||||||
|
'beyondcode:websockets:restart', 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger a soft shutdown for the process.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function triggerSoftShutdown()
|
||||||
|
{
|
||||||
|
$channelManager = $this->laravel->make(ChannelManager::class);
|
||||||
|
|
||||||
|
// Close the new connections allowance on this server.
|
||||||
|
$channelManager->declineNewConnections();
|
||||||
|
|
||||||
|
// Get all local connections and close them. They will
|
||||||
|
// be automatically be unsubscribed from all channels.
|
||||||
|
$channelManager->getLocalConnections()
|
||||||
|
->then(function ($connections) {
|
||||||
|
foreach ($connections as $connection) {
|
||||||
|
$connection->close();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->then(function () {
|
||||||
|
$this->loop->stop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,273 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Console;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Facades\StatisticsLogger;
|
|
||||||
use BeyondCode\LaravelWebSockets\Facades\WebSocketsRouter;
|
|
||||||
use BeyondCode\LaravelWebSockets\PubSub\Drivers\LocalClient;
|
|
||||||
use BeyondCode\LaravelWebSockets\PubSub\Drivers\RedisClient;
|
|
||||||
use BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface;
|
|
||||||
use BeyondCode\LaravelWebSockets\Server\Logger\ConnectionLogger;
|
|
||||||
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\Support\Facades\Cache;
|
|
||||||
use React\EventLoop\Factory as LoopFactory;
|
|
||||||
|
|
||||||
class StartWebSocketServer extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'websockets:serve
|
|
||||||
{--host=0.0.0.0}
|
|
||||||
{--port=6001}
|
|
||||||
{--statistics-interval= : Overwrite the statistics interval set in the config.}
|
|
||||||
{--debug : Forces the loggers to be enabled and thereby overriding the APP_DEBUG setting.}
|
|
||||||
{--test : Prepare the server, but do not start it.}
|
|
||||||
';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string|null
|
|
||||||
*/
|
|
||||||
protected $description = 'Start the Laravel WebSocket Server';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the loop instance.
|
|
||||||
*
|
|
||||||
* @var \React\EventLoop\LoopInterface
|
|
||||||
*/
|
|
||||||
protected $loop;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Pusher server instance.
|
|
||||||
*
|
|
||||||
* @var \Ratchet\Server\IoServer
|
|
||||||
*/
|
|
||||||
public $server;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Track the last restart.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $lastRestart;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the command.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
|
|
||||||
$this->loop = LoopFactory::create();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run the command.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
$this
|
|
||||||
->configureStatisticsLogger()
|
|
||||||
->configureHttpLogger()
|
|
||||||
->configureMessageLogger()
|
|
||||||
->configureConnectionLogger()
|
|
||||||
->configureRestartTimer()
|
|
||||||
->configurePubSub()
|
|
||||||
->registerRoutes()
|
|
||||||
->startWebSocketServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure the statistics logger class.
|
|
||||||
*
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
protected function configureStatisticsLogger()
|
|
||||||
{
|
|
||||||
$this->laravel->singleton(StatisticsLoggerInterface::class, function () {
|
|
||||||
$class = config('websockets.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.
|
|
||||||
*
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
protected function configureHttpLogger()
|
|
||||||
{
|
|
||||||
$this->laravel->singleton(HttpLogger::class, function () {
|
|
||||||
return (new HttpLogger($this->output))
|
|
||||||
->enable($this->option('debug') ?: config('app.debug'))
|
|
||||||
->verbose($this->output->isVerbose());
|
|
||||||
});
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure the logger for messages.
|
|
||||||
*
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
protected function configureMessageLogger()
|
|
||||||
{
|
|
||||||
$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 $this
|
|
||||||
*/
|
|
||||||
protected function configureConnectionLogger()
|
|
||||||
{
|
|
||||||
$this->laravel->bind(ConnectionLogger::class, function () {
|
|
||||||
return (new ConnectionLogger($this->output))
|
|
||||||
->enable(config('app.debug'))
|
|
||||||
->verbose($this->output->isVerbose());
|
|
||||||
});
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure the Redis PubSub handler.
|
|
||||||
*
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function configureRestartTimer()
|
|
||||||
{
|
|
||||||
$this->lastRestart = $this->getLastRestart();
|
|
||||||
|
|
||||||
$this->loop->addPeriodicTimer(10, function () {
|
|
||||||
if ($this->getLastRestart() !== $this->lastRestart) {
|
|
||||||
$this->loop->stop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure the replicators.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function configurePubSub()
|
|
||||||
{
|
|
||||||
if (config('websockets.replication.driver', 'local') === 'local') {
|
|
||||||
$this->laravel->singleton(ReplicationInterface::class, function () {
|
|
||||||
return new LocalClient;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config('websockets.replication.driver', 'local') === 'redis') {
|
|
||||||
$this->laravel->singleton(ReplicationInterface::class, function () {
|
|
||||||
return (new RedisClient)->boot($this->loop);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->laravel
|
|
||||||
->get(ReplicationInterface::class)
|
|
||||||
->boot($this->loop);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the routes.
|
|
||||||
*
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
protected function registerRoutes()
|
|
||||||
{
|
|
||||||
WebSocketsRouter::routes();
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the server.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function startWebSocketServer()
|
|
||||||
{
|
|
||||||
$this->info("Starting the WebSocket server on port {$this->option('port')}...");
|
|
||||||
|
|
||||||
$this->buildServer();
|
|
||||||
|
|
||||||
// For testing, just boot up the server, run it
|
|
||||||
// but exit after the next tick.
|
|
||||||
if ($this->option('test')) {
|
|
||||||
$this->loop->futureTick(function () {
|
|
||||||
$this->loop->stop();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🛰 Start the server 🛰 */
|
|
||||||
$this->server->run();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the server instance.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function buildServer()
|
|
||||||
{
|
|
||||||
$this->server = new WebSocketServerFactory(
|
|
||||||
$this->option('host'), $this->option('port')
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->server = $this->server
|
|
||||||
->setLoop($this->loop)
|
|
||||||
->useRoutes(WebSocketsRouter::getRoutes())
|
|
||||||
->setConsoleOutput($this->output)
|
|
||||||
->createServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the last time the server restarted.
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
protected function getLastRestart()
|
|
||||||
{
|
|
||||||
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
|
||||||
{
|
{
|
||||||
|
|
@ -14,7 +16,7 @@ interface AppManager
|
||||||
/**
|
/**
|
||||||
* Get app by id.
|
* Get app by id.
|
||||||
*
|
*
|
||||||
* @param mixed $appId
|
* @param string|int $appId
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||||
*/
|
*/
|
||||||
public function findById($appId): ?App;
|
public function findById($appId): ?App;
|
||||||
|
|
@ -22,7 +24,7 @@ interface AppManager
|
||||||
/**
|
/**
|
||||||
* Get app by app key.
|
* Get app by app key.
|
||||||
*
|
*
|
||||||
* @param mixed $appKey
|
* @param string $appKey
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||||
*/
|
*/
|
||||||
public function findByKey($appKey): ?App;
|
public function findByKey($appKey): ?App;
|
||||||
|
|
@ -30,7 +32,7 @@ interface AppManager
|
||||||
/**
|
/**
|
||||||
* Get app by secret.
|
* Get app by secret.
|
||||||
*
|
*
|
||||||
* @param mixed $appSecret
|
* @param string $appSecret
|
||||||
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
* @return \BeyondCode\LaravelWebSockets\Apps\App|null
|
||||||
*/
|
*/
|
||||||
public function findBySecret($appSecret): ?App;
|
public function findBySecret($appSecret): ?App;
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Contracts;
|
||||||
|
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use React\EventLoop\LoopInterface;
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
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 the local connections, regardless of the channel
|
||||||
|
* they are connected to.
|
||||||
|
*
|
||||||
|
* @return \React\Promise\PromiseInterface
|
||||||
|
*/
|
||||||
|
public function getLocalConnections(): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function unsubscribeFromAllChannels(ConnectionInterface $connection): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe the connection to a specific channel.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param string $channelName
|
||||||
|
* @param stdClass $payload
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function subscribeToChannel(ConnectionInterface $connection, string $channelName, stdClass $payload): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe the connection from the channel.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param string $channelName
|
||||||
|
* @param stdClass $payload
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function unsubscribeFromChannel(ConnectionInterface $connection, string $channelName, stdClass $payload): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe the connection to a specific channel, returning
|
||||||
|
* a promise containing the amount of connections.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @return PromiseInterface[int]
|
||||||
|
*/
|
||||||
|
public function subscribeToApp($appId): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe the connection from the channel, returning
|
||||||
|
* a promise containing the amount of connections after decrement.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @return PromiseInterface[int]
|
||||||
|
*/
|
||||||
|
public function unsubscribeFromApp($appId): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the connections count on the app
|
||||||
|
* for the current server instance.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string|null $channelName
|
||||||
|
* @return PromiseInterface[int]
|
||||||
|
*/
|
||||||
|
public function getLocalConnectionsCount($appId, string $channelName = null): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the connections count
|
||||||
|
* across multiple servers.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string|null $channelName
|
||||||
|
* @return PromiseInterface[int]
|
||||||
|
*/
|
||||||
|
public function getGlobalConnectionsCount($appId, string $channelName = null): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast the message across multiple servers.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string|null $socketId
|
||||||
|
* @param string $channel
|
||||||
|
* @param stdClass $payload
|
||||||
|
* @param string|null $serverId
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function broadcastAcrossServers($appId, ?string $socketId, string $channel, stdClass $payload, string $serverId = null): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the user when it joined a presence channel.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param stdClass $user
|
||||||
|
* @param string $channel
|
||||||
|
* @param stdClass $payload
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function userJoinedPresenceChannel(ConnectionInterface $connection, stdClass $user, string $channel, stdClass $payload): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the user when it left a presence channel.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param stdClass $user
|
||||||
|
* @param string $channel
|
||||||
|
* @param stdClass $payload
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function userLeftPresenceChannel(ConnectionInterface $connection, stdClass $user, string $channel): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the socket IDs for a presence channel member.
|
||||||
|
*
|
||||||
|
* @param string|int $userId
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string $channelName
|
||||||
|
* @return \React\Promise\PromiseInterface
|
||||||
|
*/
|
||||||
|
public function getMemberSockets($userId, $appId, $channelName): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep tracking the connections availability when they pong.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function connectionPonged(ConnectionInterface $connection): PromiseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the obsolete connections that didn't ponged in a while.
|
||||||
|
*
|
||||||
|
* @return PromiseInterface[bool]
|
||||||
|
*/
|
||||||
|
public function removeObsoleteConnections(): PromiseInterface;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Messages;
|
namespace BeyondCode\LaravelWebSockets\Contracts;
|
||||||
|
|
||||||
interface PusherMessage
|
interface PusherMessage
|
||||||
{
|
{
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Contracts;
|
||||||
|
|
||||||
|
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,55 @@
|
||||||
|
<?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
|
||||||
|
* @param callable $processCollection
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getForGraph(callable $processQuery = null, callable $processCollection = null): array;
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,14 @@
|
||||||
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||||
|
use BeyondCode\LaravelWebSockets\Concerns\PushesToPusher;
|
||||||
use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster;
|
use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Pusher\Pusher;
|
|
||||||
|
|
||||||
class AuthenticateDashboard
|
class AuthenticateDashboard
|
||||||
{
|
{
|
||||||
|
use PushesToPusher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the app by using the header
|
* Find the app by using the header
|
||||||
* and then reconstruct the PusherBroadcaster
|
* and then reconstruct the PusherBroadcaster
|
||||||
|
|
@ -19,14 +21,13 @@ 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 = new PusherBroadcaster(new Pusher(
|
$broadcaster = $this->getPusherBroadcaster([
|
||||||
$app->key,
|
'key' => $app->key,
|
||||||
$app->secret,
|
'secret' => $app->secret,
|
||||||
$app->id,
|
'id' =>$app->id,
|
||||||
[]
|
]);
|
||||||
));
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Since the dashboard itself is already secured by the
|
* Since the dashboard itself is already secured by the
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class DashboardApiController
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Get statistics for an app ID.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver $driver
|
|
||||||
* @param mixed $appId
|
|
||||||
* @return \Illuminate\Http\Response
|
|
||||||
*/
|
|
||||||
public function getStatistics(Request $request, StatisticsDriver $driver, $appId)
|
|
||||||
{
|
|
||||||
return $driver::get($appId, $request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,13 +2,15 @@
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Statistics\Rules\AppId;
|
use BeyondCode\LaravelWebSockets\Concerns\PushesToPusher;
|
||||||
use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster;
|
use BeyondCode\LaravelWebSockets\Rules\AppId;
|
||||||
|
use Exception;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Pusher\Pusher;
|
|
||||||
|
|
||||||
class SendMessage
|
class SendMessage
|
||||||
{
|
{
|
||||||
|
use PushesToPusher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send the message to the requested channel.
|
* Send the message to the requested channel.
|
||||||
*
|
*
|
||||||
|
|
@ -17,39 +19,38 @@ class SendMessage
|
||||||
*/
|
*/
|
||||||
public function __invoke(Request $request)
|
public function __invoke(Request $request)
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$request->validate([
|
||||||
'appId' => ['required', new AppId],
|
'appId' => ['required', new AppId],
|
||||||
'key' => 'required|string',
|
'key' => 'required|string',
|
||||||
'secret' => 'required|string',
|
'secret' => 'required|string',
|
||||||
'channel' => 'required|string',
|
|
||||||
'event' => 'required|string',
|
'event' => 'required|string',
|
||||||
|
'channel' => 'required|string',
|
||||||
'data' => 'required|json',
|
'data' => 'required|json',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->getPusherBroadcaster($validated)->broadcast(
|
$broadcaster = $this->getPusherBroadcaster([
|
||||||
[$validated['channel']],
|
'key' => $request->key,
|
||||||
$validated['event'],
|
'secret' => $request->secret,
|
||||||
json_decode($validated['data'], true)
|
'id' => $request->appId,
|
||||||
);
|
]);
|
||||||
|
|
||||||
return 'ok';
|
try {
|
||||||
|
$decodedData = json_decode($request->data, true);
|
||||||
|
|
||||||
|
$broadcaster->broadcast(
|
||||||
|
[$request->channel],
|
||||||
|
$request->event,
|
||||||
|
$decodedData ?: []
|
||||||
|
);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'ok' => false,
|
||||||
|
'exception' => $e->getMessage(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return response()->json([
|
||||||
* Get the pusher broadcaster for the current request.
|
'ok' => true,
|
||||||
*
|
]);
|
||||||
* @param array $validated
|
|
||||||
* @return \Illuminate\Broadcasting\Broadcasters\PusherBroadcaster
|
|
||||||
*/
|
|
||||||
protected function getPusherBroadcaster(array $validated): PusherBroadcaster
|
|
||||||
{
|
|
||||||
$pusher = new Pusher(
|
|
||||||
$validated['key'],
|
|
||||||
$validated['secret'],
|
|
||||||
$validated['appId'],
|
|
||||||
config('broadcasting.connections.pusher.options', [])
|
|
||||||
);
|
|
||||||
|
|
||||||
return new PusherBroadcaster($pusher);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Facades\StatisticsStore;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ShowStatistics
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get statistics for an app ID.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param mixed $appId
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request, $appId)
|
||||||
|
{
|
||||||
|
$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,8 +12,6 @@ class DashboardLogger
|
||||||
|
|
||||||
const TYPE_CONNECTED = 'connected';
|
const TYPE_CONNECTED = 'connected';
|
||||||
|
|
||||||
const TYPE_VACATED = 'vacated';
|
|
||||||
|
|
||||||
const TYPE_OCCUPIED = 'occupied';
|
const TYPE_OCCUPIED = 'occupied';
|
||||||
|
|
||||||
const TYPE_SUBSCRIBED = 'subscribed';
|
const TYPE_SUBSCRIBED = 'subscribed';
|
||||||
|
|
@ -42,7 +40,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,11 +62,11 @@ 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',
|
'event' => 'log-message',
|
||||||
'channel' => $channelName,
|
'channel' => $channelName,
|
||||||
'data' => [
|
'data' => [
|
||||||
|
|
@ -77,6 +74,22 @@ class DashboardLogger
|
||||||
'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->broadcastLocally(
|
||||||
|
$appId, (object) $payload, true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$channelManager->broadcastAcrossServers(
|
||||||
|
$appId, null, $channelName, (object) $payload
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Events;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class ConnectionClosed
|
||||||
|
{
|
||||||
|
use Dispatchable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The WebSockets app id that the user connected to.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $appId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Socket ID associated with the connection.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $socketId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @param string $appId
|
||||||
|
* @param string $socketId
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(string $appId, string $socketId)
|
||||||
|
{
|
||||||
|
$this->appId = $appId;
|
||||||
|
$this->socketId = $socketId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Events;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class ConnectionPonged
|
||||||
|
{
|
||||||
|
use Dispatchable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The WebSockets app id that the user connected to.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $appId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Socket ID associated with the connection.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $socketId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @param string $appId
|
||||||
|
* @param string $socketId
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(string $appId, string $socketId)
|
||||||
|
{
|
||||||
|
$this->appId = $appId;
|
||||||
|
$this->socketId = $socketId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Events;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class NewConnection
|
||||||
|
{
|
||||||
|
use Dispatchable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The WebSockets app id that the user connected to.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $appId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Socket ID associated with the connection.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $socketId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @param string $appId
|
||||||
|
* @param string $socketId
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(string $appId, string $socketId)
|
||||||
|
{
|
||||||
|
$this->appId = $appId;
|
||||||
|
$this->socketId = $socketId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Events;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class SubscribedToChannel
|
||||||
|
{
|
||||||
|
use Dispatchable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The WebSockets app id that the user connected to.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $appId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Socket ID associated with the connection.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $socketId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The channel name.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $channelName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user received on presence channel.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @param string $appId
|
||||||
|
* @param string $socketId
|
||||||
|
* @param string $channelName
|
||||||
|
* @param stdClass|null $user
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(string $appId, string $socketId, string $channelName, ?stdClass $user = null)
|
||||||
|
{
|
||||||
|
$this->appId = $appId;
|
||||||
|
$this->socketId = $socketId;
|
||||||
|
$this->channelName = $channelName;
|
||||||
|
$this->user = $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Events;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class UnsubscribedFromChannel
|
||||||
|
{
|
||||||
|
use Dispatchable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The WebSockets app id that the user connected to.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $appId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Socket ID associated with the connection.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $socketId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The channel name.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $channelName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user received on presence channel.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @param string $appId
|
||||||
|
* @param string $socketId
|
||||||
|
* @param string $channelName
|
||||||
|
* @param stdClass|null $user
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(string $appId, string $socketId, string $channelName, ?stdClass $user = null)
|
||||||
|
{
|
||||||
|
$this->appId = $appId;
|
||||||
|
$this->socketId = $socketId;
|
||||||
|
$this->channelName = $channelName;
|
||||||
|
$this->user = $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Events;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Ratchet\RFC6455\Messaging\MessageInterface;
|
||||||
|
|
||||||
|
class WebSocketMessageReceived
|
||||||
|
{
|
||||||
|
use Dispatchable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The WebSockets app id that the user connected to.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $appId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Socket ID associated with the connection.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $socketId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The message received.
|
||||||
|
*
|
||||||
|
* @var MessageInterface
|
||||||
|
*/
|
||||||
|
public $message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The decoded message as array.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $decodedMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @param string $appId
|
||||||
|
* @param string $socketId
|
||||||
|
* @param MessageInterface $message
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(string $appId, string $socketId, MessageInterface $message)
|
||||||
|
{
|
||||||
|
$this->appId = $appId;
|
||||||
|
$this->socketId = $socketId;
|
||||||
|
$this->message = $message;
|
||||||
|
$this->decodedMessage = json_decode($message->getPayload(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Exceptions;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
use Facade\IgnitionContracts\BaseSolution;
|
|
||||||
use Facade\IgnitionContracts\ProvidesSolution;
|
|
||||||
use Facade\IgnitionContracts\Solution;
|
|
||||||
|
|
||||||
class InvalidApp extends Exception implements ProvidesSolution
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Throw an "app not found by id" exception.
|
|
||||||
*
|
|
||||||
* @param mixed $appId
|
|
||||||
* @return \BeyondCode\LaravelWebSockets\Exceptions\InvalidApp
|
|
||||||
*/
|
|
||||||
public static function notFound($appId)
|
|
||||||
{
|
|
||||||
return new static("Could not find app for app id `{$appId}`.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Throw an "app id required" exception.
|
|
||||||
*
|
|
||||||
* @param string $name
|
|
||||||
* @param mixed $appId
|
|
||||||
* @return \BeyondCode\LaravelWebSockets\Exceptions\InvalidApp
|
|
||||||
*/
|
|
||||||
public static function valueIsRequired($name, $appId)
|
|
||||||
{
|
|
||||||
return new static("{$name} is required but was empty for app id `{$appId}`.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provide the solution for Igniter.
|
|
||||||
*
|
|
||||||
* @return \Facade\IgnitionContracts\BaseSolution
|
|
||||||
*/
|
|
||||||
public function getSolution(): Solution
|
|
||||||
{
|
|
||||||
return BaseSolution::create('Your application id could not be found')
|
|
||||||
->setSolutionDescription('Make sure that your `config/websockets.php` contains the app key you are trying to use.')
|
|
||||||
->setDocumentationLinks([
|
|
||||||
'Configuring WebSocket Apps (official documentation)' => 'https://docs.beyondco.de/laravel-websockets/1.0/basic-usage/pusher.html#configuring-websocket-apps',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Exceptions;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
use Ratchet\WebSocket\MessageComponentInterface;
|
|
||||||
|
|
||||||
class InvalidWebSocketController extends Exception
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Allocate a controller to the error.
|
|
||||||
*
|
|
||||||
* @param string $controllerClass
|
|
||||||
* @return \BeyondCode\LaravelWebSockets\Exceptions\InvalidWebSocketController
|
|
||||||
*/
|
|
||||||
public static function withController(string $controllerClass)
|
|
||||||
{
|
|
||||||
$class = MessageComponentInterface::class;
|
|
||||||
|
|
||||||
return new static(
|
|
||||||
"Invalid WebSocket Controller provided. Expected instance of `{$class}`, but received `{$controllerClass}`."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Facades;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\StatisticsCollector as StatisticsCollectorInterface;
|
||||||
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
|
||||||
|
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 BeyondCode\LaravelWebSockets\Contracts\StatisticsStore as StatisticsStoreInterface;
|
||||||
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
|
||||||
|
class StatisticsStore extends Facade
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the registered name of the component.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected static function getFacadeAccessor()
|
||||||
|
{
|
||||||
|
return StatisticsStoreInterface::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,11 +4,7 @@ namespace BeyondCode\LaravelWebSockets\Facades;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Facade;
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
|
||||||
/**
|
class WebSocketRouter extends Facade
|
||||||
* @see \BeyondCode\LaravelWebSockets\Server\Router
|
|
||||||
* @mixin \BeyondCode\LaravelWebSockets\Server\Router
|
|
||||||
*/
|
|
||||||
class WebSocketsRouter extends Facade
|
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the registered name of the component.
|
* Get the registered name of the component.
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets;
|
||||||
|
|
||||||
|
use React\Promise\PromiseInterface;
|
||||||
|
|
||||||
|
class Helpers
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The loop used to create the Fulfilled Promise.
|
||||||
|
*
|
||||||
|
* @var null|\React\EventLoop\LoopInterface
|
||||||
|
*/
|
||||||
|
public static $loop = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the Redis' list of key after value
|
||||||
|
* to key-value pairs.
|
||||||
|
*
|
||||||
|
* @param array $list
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function redisListToArray(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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new fulfilled promise with a value.
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
* @return \React\Promise\PromiseInterface
|
||||||
|
*/
|
||||||
|
public static function createFulfilledPromise($value): PromiseInterface
|
||||||
|
{
|
||||||
|
$resolver = config(
|
||||||
|
'websockets.promise_resolver', \React\Promise\FulfilledPromise::class
|
||||||
|
);
|
||||||
|
|
||||||
|
return new $resolver($value, static::$loop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,90 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\HttpApi\Controllers;
|
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
|
||||||
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
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The replicator driver.
|
|
||||||
*
|
|
||||||
* @var \BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface
|
|
||||||
*/
|
|
||||||
protected $replicator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the class.
|
|
||||||
*
|
|
||||||
* @param \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager $channelManager
|
|
||||||
* @param \BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface $replicator
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct(ChannelManager $channelManager, ReplicationInterface $replicator)
|
|
||||||
{
|
|
||||||
parent::__construct($channelManager);
|
|
||||||
|
|
||||||
$this->replicator = $replicator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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,41 +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);
|
|
||||||
|
|
||||||
foreach ($request->json()->get('channels', []) as $channelName) {
|
|
||||||
$channel = $this->channelManager->find($request->appId, $channelName);
|
|
||||||
|
|
||||||
optional($channel)->broadcastToEveryoneExcept([
|
|
||||||
'channel' => $channelName,
|
|
||||||
'event' => $request->json()->get('name'),
|
|
||||||
'data' => $request->json()->get('data'),
|
|
||||||
], $request->json()->get('socket_id'), $request->appId);
|
|
||||||
|
|
||||||
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,150 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\PubSub\Broadcasters;
|
|
||||||
|
|
||||||
use Illuminate\Broadcasting\Broadcasters\Broadcaster;
|
|
||||||
use Illuminate\Broadcasting\Broadcasters\UsePusherChannelConventions;
|
|
||||||
use Illuminate\Contracts\Redis\Factory as Redis;
|
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Pusher\Pusher;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
|
|
||||||
class RedisPusherBroadcaster extends Broadcaster
|
|
||||||
{
|
|
||||||
use UsePusherChannelConventions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Pusher SDK instance.
|
|
||||||
*
|
|
||||||
* @var \Pusher\Pusher
|
|
||||||
*/
|
|
||||||
protected $pusher;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Pusher app ID, to be passed in the payload.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $appId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Redis instance.
|
|
||||||
*
|
|
||||||
* @var \Illuminate\Contracts\Redis\Factory
|
|
||||||
*/
|
|
||||||
protected $redis;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Redis connection to use for broadcasting.
|
|
||||||
*
|
|
||||||
* @var string|null
|
|
||||||
*/
|
|
||||||
protected $connection;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new broadcaster instance.
|
|
||||||
*
|
|
||||||
* @param Pusher $pusher
|
|
||||||
* @param $appId
|
|
||||||
* @param \Illuminate\Contracts\Redis\Factory $redis
|
|
||||||
* @param string|null $connection
|
|
||||||
*/
|
|
||||||
public function __construct(Pusher $pusher, $appId, Redis $redis, $connection = null)
|
|
||||||
{
|
|
||||||
$this->pusher = $pusher;
|
|
||||||
$this->appId = $appId;
|
|
||||||
$this->redis = $redis;
|
|
||||||
$this->connection = $connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authenticate the incoming request for a given channel.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @return mixed
|
|
||||||
*
|
|
||||||
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
|
|
||||||
*/
|
|
||||||
public function auth($request)
|
|
||||||
{
|
|
||||||
$channelName = $this->normalizeChannelName($request->channel_name);
|
|
||||||
|
|
||||||
if ($this->isGuardedChannel($request->channel_name) &&
|
|
||||||
! $this->retrieveUser($request, $channelName)) {
|
|
||||||
throw new AccessDeniedHttpException;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent::verifyUserCanAccessChannel(
|
|
||||||
$request, $channelName
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the valid authentication response.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param mixed $result
|
|
||||||
* @return mixed
|
|
||||||
* @throws \Pusher\PusherException
|
|
||||||
*/
|
|
||||||
public function validAuthenticationResponse($request, $result)
|
|
||||||
{
|
|
||||||
if (Str::startsWith($request->channel_name, 'private')) {
|
|
||||||
return $this->decodePusherResponse(
|
|
||||||
$request, $this->pusher->socket_auth($request->channel_name, $request->socket_id)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$channelName = $this->normalizeChannelName($request->channel_name);
|
|
||||||
|
|
||||||
return $this->decodePusherResponse(
|
|
||||||
$request,
|
|
||||||
$this->pusher->presence_auth(
|
|
||||||
$request->channel_name, $request->socket_id,
|
|
||||||
$this->retrieveUser($request, $channelName)->getAuthIdentifier(), $result
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode the given Pusher response.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param mixed $response
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected function decodePusherResponse($request, $response)
|
|
||||||
{
|
|
||||||
if (! $request->input('callback', false)) {
|
|
||||||
return json_decode($response, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json(json_decode($response, true))
|
|
||||||
->withCallback($request->callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcast the given event.
|
|
||||||
*
|
|
||||||
* @param array $channels
|
|
||||||
* @param string $event
|
|
||||||
* @param array $payload
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function broadcast(array $channels, $event, array $payload = [])
|
|
||||||
{
|
|
||||||
$connection = $this->redis->connection($this->connection);
|
|
||||||
|
|
||||||
$payload = json_encode([
|
|
||||||
'appId' => $this->appId,
|
|
||||||
'event' => $event,
|
|
||||||
'data' => $payload,
|
|
||||||
'socket' => Arr::pull($payload, 'socket'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
foreach ($this->formatChannels($channels) as $channel) {
|
|
||||||
$connection->publish("{$this->appId}:$channel", $payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,374 +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->__call('publish', ["{$appId}:{$channel}", $payload]);
|
|
||||||
|
|
||||||
DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_MESSAGE_PUBLISHED, [
|
|
||||||
'channel' => $channel,
|
|
||||||
'serverId' => $this->getServerId(),
|
|
||||||
'payload' => $payload,
|
|
||||||
'pubsub' => "{$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->__call('subscribe', ["{$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' => "{$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->__call('unsubscribe', ["{$appId}:{$channel}"]);
|
|
||||||
|
|
||||||
unset($this->subscribedChannels["{$appId}:{$channel}"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_UNSUBSCRIBED, [
|
|
||||||
'channel' => $channel,
|
|
||||||
'serverId' => $this->getServerId(),
|
|
||||||
'pubsub' => "{$appId}:{$channel}",
|
|
||||||
]);
|
|
||||||
|
|
||||||
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->__call('hset', ["{$appId}:{$channel}", $socketId, $data]);
|
|
||||||
|
|
||||||
DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_JOINED_CHANNEL, [
|
|
||||||
'channel' => $channel,
|
|
||||||
'serverId' => $this->getServerId(),
|
|
||||||
'socketId' => $socketId,
|
|
||||||
'data' => $data,
|
|
||||||
'pubsub' => "{$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->__call('hdel', ["{$appId}:{$channel}", $socketId]);
|
|
||||||
|
|
||||||
DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_LEFT_CHANNEL, [
|
|
||||||
'channel' => $channel,
|
|
||||||
'serverId' => $this->getServerId(),
|
|
||||||
'socketId' => $socketId,
|
|
||||||
'pubsub' => "{$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->__call('hgetall', ["{$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->__call('multi', []);
|
|
||||||
|
|
||||||
foreach ($channelNames as $channel) {
|
|
||||||
$this->publishClient->__call('hlen', ["{$appId}:{$channel}"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->publishClient->__call('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
|
|
||||||
*/
|
|
||||||
protected 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
$socket = $payload->socket ?? null;
|
|
||||||
$serverId = $payload->serverId ?? null;
|
|
||||||
|
|
||||||
// Remove fields intended for internal use from the payload.
|
|
||||||
unset($payload->socket);
|
|
||||||
unset($payload->serverId);
|
|
||||||
unset($payload->appId);
|
|
||||||
|
|
||||||
// Push the message out to connected websocket clients.
|
|
||||||
$channel->broadcastToEveryoneExcept($payload, $socket, $appId, false);
|
|
||||||
|
|
||||||
DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_MESSAGE_RECEIVED, [
|
|
||||||
'channel' => $channel->getChannelName(),
|
|
||||||
'redisChannel' => $redisChannel,
|
|
||||||
'serverId' => $this->getServerId(),
|
|
||||||
'incomingServerId' => $serverId,
|
|
||||||
'incomingSocketId' => $socket,
|
|
||||||
'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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Queue;
|
||||||
|
|
||||||
|
use Illuminate\Queue\Connectors\RedisConnector;
|
||||||
|
|
||||||
|
class AsyncRedisConnector extends RedisConnector
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Establish a queue connection.
|
||||||
|
*
|
||||||
|
* @param array $config
|
||||||
|
* @return \Illuminate\Contracts\Queue\Queue
|
||||||
|
*/
|
||||||
|
public function connect(array $config)
|
||||||
|
{
|
||||||
|
return new AsyncRedisQueue(
|
||||||
|
$this->redis, $config['queue'],
|
||||||
|
$config['connection'] ?? $this->connection,
|
||||||
|
$config['retry_after'] ?? 60,
|
||||||
|
$config['block_for'] ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Queue;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
|
use Illuminate\Queue\RedisQueue;
|
||||||
|
|
||||||
|
class AsyncRedisQueue extends RedisQueue
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the connection for the queue.
|
||||||
|
*
|
||||||
|
* @return \BeyondCode\LaravelWebSockets\Contracts\ChannelManager|\Illuminate\Redis\Connections\Connection
|
||||||
|
*/
|
||||||
|
public function getConnection()
|
||||||
|
{
|
||||||
|
$channelManager = $this->container->bound(ChannelManager::class)
|
||||||
|
? $this->container->make(ChannelManager::class)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return $channelManager && method_exists($channelManager, 'getRedisClient')
|
||||||
|
? $channelManager->getRedisClient()
|
||||||
|
: parent::getConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Server;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use GuzzleHttp\Psr7\Response;
|
||||||
|
use Psr\Http\Message\RequestInterface;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use Ratchet\Http\HttpServerInterface;
|
||||||
|
|
||||||
|
class HealthHandler implements HttpServerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the socket opening.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param \Psr\Http\Message\RequestInterface $request
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function onOpen(ConnectionInterface $connection, RequestInterface $request = null)
|
||||||
|
{
|
||||||
|
$response = new Response(
|
||||||
|
200,
|
||||||
|
['Content-Type' => 'application/json'],
|
||||||
|
json_encode(['ok' => true])
|
||||||
|
);
|
||||||
|
|
||||||
|
tap($connection)->send(\GuzzleHttp\Psr7\str($response))->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the incoming message.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param string $message
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function onMessage(ConnectionInterface $connection, $message)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the websocket close.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function onClose(ConnectionInterface $connection)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the websocket errors.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param WebSocketException $exception
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function onError(ConnectionInterface $connection, Exception $exception)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,10 @@
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Server;
|
namespace BeyondCode\LaravelWebSockets\Server;
|
||||||
|
|
||||||
|
use Ratchet\Http\HttpServer as BaseHttpServer;
|
||||||
use Ratchet\Http\HttpServerInterface;
|
use Ratchet\Http\HttpServerInterface;
|
||||||
|
|
||||||
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.
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Server\Messages;
|
||||||
|
|
||||||
|
use BeyondCode\LaravelWebSockets\Events\ConnectionPonged;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class PusherChannelProtocolMessage extends PusherClientMessage
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Respond with the payload.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function respond()
|
||||||
|
{
|
||||||
|
$eventName = Str::camel(Str::after($this->payload->event, ':'));
|
||||||
|
|
||||||
|
if (method_exists($this, $eventName) && $eventName !== 'respond') {
|
||||||
|
call_user_func([$this, $eventName], $this->connection, $this->payload->data ?? new stdClass());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ping the connection.
|
||||||
|
*
|
||||||
|
* @see https://pusher.com/docs/pusher_protocol#ping-pong
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function ping(ConnectionInterface $connection)
|
||||||
|
{
|
||||||
|
$this->channelManager
|
||||||
|
->connectionPonged($connection)
|
||||||
|
->then(function () use ($connection) {
|
||||||
|
$connection->send(json_encode(['event' => 'pusher:pong']));
|
||||||
|
|
||||||
|
ConnectionPonged::dispatch($connection->app->id, $connection->socketId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to channel.
|
||||||
|
*
|
||||||
|
* @see https://pusher.com/docs/pusher_protocol#pusher-subscribe
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param \stdClass $payload
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function subscribe(ConnectionInterface $connection, stdClass $payload)
|
||||||
|
{
|
||||||
|
$this->channelManager->subscribeToChannel($connection, $payload->channel, $payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from the channel.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @param \stdClass $payload
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function unsubscribe(ConnectionInterface $connection, stdClass $payload)
|
||||||
|
{
|
||||||
|
$this->channelManager->unsubscribeFromChannel($connection, $payload->channel, $payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Messages;
|
namespace BeyondCode\LaravelWebSockets\Server\Messages;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Dashboard\DashboardLogger;
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
use BeyondCode\LaravelWebSockets\Contracts\PusherMessage;
|
||||||
|
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
@ -60,15 +61,19 @@ 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,
|
|
||||||
'event' => $this->payload->event,
|
'event' => $this->payload->event,
|
||||||
|
'channel' => $this->payload->channel,
|
||||||
'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,8 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets\Messages;
|
namespace BeyondCode\LaravelWebSockets\Server\Messages;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
|
use BeyondCode\LaravelWebSockets\Contracts\PusherMessage;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use Ratchet\RFC6455\Messaging\MessageInterface;
|
use Ratchet\RFC6455\Messaging\MessageInterface;
|
||||||
|
|
@ -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(
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BeyondCode\LaravelWebSockets\Server;
|
||||||
|
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class MockableConnection implements ConnectionInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new Mockable connection.
|
||||||
|
*
|
||||||
|
* @param string|int $appId
|
||||||
|
* @param string $socketId
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($appId, string $socketId)
|
||||||
|
{
|
||||||
|
$this->app = new stdClass;
|
||||||
|
|
||||||
|
$this->app->id = $appId;
|
||||||
|
$this->socketId = $socketId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send data to the connection.
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @return \Ratchet\ConnectionInterface
|
||||||
|
*/
|
||||||
|
public function send($data)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the connection.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function close()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets;
|
namespace BeyondCode\LaravelWebSockets\Server;
|
||||||
|
|
||||||
use Psr\Http\Message\RequestInterface;
|
use Psr\Http\Message\RequestInterface;
|
||||||
|
|
||||||
|
|
@ -2,14 +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 Ratchet\WebSocket\MessageComponentInterface;
|
use Ratchet\WebSocket\MessageComponentInterface;
|
||||||
use Ratchet\WebSocket\WsServer;
|
use Ratchet\WebSocket\WsServer;
|
||||||
use Symfony\Component\Routing\Route;
|
use Symfony\Component\Routing\Route;
|
||||||
|
|
@ -24,13 +17,6 @@ class Router
|
||||||
*/
|
*/
|
||||||
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 +25,6 @@ class Router
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->routes = new RouteCollection;
|
$this->routes = new RouteCollection;
|
||||||
$this->customRoutes = new Collection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -53,22 +38,18 @@ 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', TriggerEventController::class);
|
$this->get('/apps/{appId}/channels', config('websockets.handlers.fetch_channels'));
|
||||||
$this->get('/apps/{appId}/channels', FetchChannelsController::class);
|
$this->get('/apps/{appId}/channels/{channelName}', config('websockets.handlers.fetch_channel'));
|
||||||
$this->get('/apps/{appId}/channels/{channelName}', FetchChannelController::class);
|
$this->get('/apps/{appId}/channels/{channelName}/users', config('websockets.handlers.fetch_users'));
|
||||||
$this->get('/apps/{appId}/channels/{channelName}/users', FetchUsersController::class);
|
$this->get('/health', config('websockets.handlers.health'));
|
||||||
|
|
||||||
$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,17 +1,14 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\WebSockets;
|
namespace BeyondCode\LaravelWebSockets\Server;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Apps\App;
|
use BeyondCode\LaravelWebSockets\Apps\App;
|
||||||
use BeyondCode\LaravelWebSockets\Dashboard\DashboardLogger;
|
use BeyondCode\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
use BeyondCode\LaravelWebSockets\Facades\StatisticsLogger;
|
use BeyondCode\LaravelWebSockets\DashboardLogger;
|
||||||
use BeyondCode\LaravelWebSockets\QueryParameters;
|
use BeyondCode\LaravelWebSockets\Events\ConnectionClosed;
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
|
use BeyondCode\LaravelWebSockets\Events\NewConnection;
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\ConnectionsOverCapacity;
|
use BeyondCode\LaravelWebSockets\Events\WebSocketMessageReceived;
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\OriginNotAllowed;
|
use BeyondCode\LaravelWebSockets\Facades\StatisticsCollector;
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\UnknownAppKey;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\WebSocketException;
|
|
||||||
use BeyondCode\LaravelWebSockets\WebSockets\Messages\PusherMessageFactory;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use Ratchet\RFC6455\Messaging\MessageInterface;
|
use Ratchet\RFC6455\Messaging\MessageInterface;
|
||||||
|
|
@ -22,14 +19,14 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
/**
|
/**
|
||||||
* The channel manager.
|
* The channel manager.
|
||||||
*
|
*
|
||||||
* @var \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager
|
* @var ChannelManager
|
||||||
*/
|
*/
|
||||||
protected $channelManager;
|
protected $channelManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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)
|
||||||
|
|
@ -45,11 +42,33 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
*/
|
*/
|
||||||
public function onOpen(ConnectionInterface $connection)
|
public function onOpen(ConnectionInterface $connection)
|
||||||
{
|
{
|
||||||
|
if (! $this->connectionCanBeMade($connection)) {
|
||||||
|
return $connection->close();
|
||||||
|
}
|
||||||
|
|
||||||
$this->verifyAppKey($connection)
|
$this->verifyAppKey($connection)
|
||||||
->verifyOrigin($connection)
|
->verifyOrigin($connection)
|
||||||
->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);
|
||||||
|
|
||||||
|
$this->channelManager->connectionPonged($connection);
|
||||||
|
|
||||||
|
DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_CONNECTED, [
|
||||||
|
'origin' => "{$request->getUri()->getScheme()}://{$request->getUri()->getHost()}",
|
||||||
|
'socketId' => $connection->socketId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
NewConnection::dispatch($connection->app->id, $connection->socketId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -61,11 +80,21 @@ 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);
|
if (! isset($connection->app)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$message->respond();
|
Messages\PusherMessageFactory::createForMessage(
|
||||||
|
$message, $connection, $this->channelManager
|
||||||
|
)->respond();
|
||||||
|
|
||||||
StatisticsLogger::webSocketMessage($connection);
|
StatisticsCollector::webSocketMessage($connection->app->id);
|
||||||
|
|
||||||
|
WebSocketMessageReceived::dispatch(
|
||||||
|
$connection->app->id,
|
||||||
|
$connection->socketId,
|
||||||
|
$message
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -76,13 +105,21 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
*/
|
*/
|
||||||
public function onClose(ConnectionInterface $connection)
|
public function onClose(ConnectionInterface $connection)
|
||||||
{
|
{
|
||||||
$this->channelManager->removeFromAllChannels($connection);
|
$this->channelManager
|
||||||
|
->unsubscribeFromAllChannels($connection)
|
||||||
|
->then(function (bool $unsubscribed) use ($connection) {
|
||||||
|
if (isset($connection->app)) {
|
||||||
|
StatisticsCollector::disconnection($connection->app->id);
|
||||||
|
|
||||||
|
$this->channelManager->unsubscribeFromApp($connection->app->id);
|
||||||
|
|
||||||
DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_DISCONNECTED, [
|
DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_DISCONNECTED, [
|
||||||
'socketId' => $connection->socketId,
|
'socketId' => $connection->socketId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
StatisticsLogger::disconnection($connection);
|
ConnectionClosed::dispatch($connection->app->id, $connection->socketId);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -94,13 +131,25 @@ 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()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the connection can be made for the
|
||||||
|
* current server instance.
|
||||||
|
*
|
||||||
|
* @param \Ratchet\ConnectionInterface $connection
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function connectionCanBeMade(ConnectionInterface $connection): bool
|
||||||
|
{
|
||||||
|
return $this->channelManager->acceptsNewConnections();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify the app key validity.
|
* Verify the app key validity.
|
||||||
*
|
*
|
||||||
|
|
@ -109,10 +158,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;
|
||||||
|
|
@ -137,7 +188,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;
|
||||||
|
|
@ -152,10 +203,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->getConnectionCount($connection->app->id);
|
$this->channelManager
|
||||||
|
->getGlobalConnectionsCount($connection->app->id)
|
||||||
|
->then(function ($connectionsCount) use ($capacity, $connection) {
|
||||||
if ($connectionsCount >= $capacity) {
|
if ($connectionsCount >= $capacity) {
|
||||||
throw new ConnectionsOverCapacity();
|
$exception = new Exceptions\ConnectionsOverCapacity;
|
||||||
|
|
||||||
|
$payload = json_encode($exception->getPayload());
|
||||||
|
|
||||||
|
tap($connection)->send($payload)->close();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -192,16 +250,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);
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BeyondCode\LaravelWebSockets\Server;
|
namespace BeyondCode\LaravelWebSockets;
|
||||||
|
|
||||||
use BeyondCode\LaravelWebSockets\Server\Logger\HttpLogger;
|
use BeyondCode\LaravelWebSockets\Server\HttpServer;
|
||||||
|
use BeyondCode\LaravelWebSockets\Server\Loggers\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;
|
||||||
|
|
@ -14,7 +15,7 @@ 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;
|
||||||
|
|
||||||
class WebSocketServerFactory
|
class ServerFactory
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The host the server will run on.
|
* The host the server will run on.
|
||||||
|
|
@ -72,7 +73,7 @@ class WebSocketServerFactory
|
||||||
* @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;
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue