A tests, documentation for helpers and lifecycle
- Introduced `helpers-and-testing.md` to document global helpers and WebsocketService class usage. - Created `HandlerLifecycleTest.php` to test the full WebSocket handler lifecycle, including connection management, channel subscriptions, and message routing. - Added `WebsocketServiceTest.php` to validate state tracking methods in WebsocketService, covering user authentication, channel tracking, and broadcast functionality.
This commit is contained in:
parent
77e12db729
commit
093bbe3a44
127
README.md
127
README.md
|
|
@ -1,33 +1,128 @@
|
||||||
# Laravel WebSockets 🛰
|
# Laravel WebSockets
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> This is a fork from [https://github.com/beyondcode/laravel-websockets](beyondcode/laravel-websockets) which has been abandoned on Feb 7, 2024 due to the release of Laravel Reverb.
|
> This package is actively maintained as a fork of beyondcode/laravel-websockets.
|
||||||
>
|
|
||||||
Bring the power of WebSockets to your Laravel application. Drop-in Pusher replacement, SSL support, Laravel Echo support and a debug dashboard are just some of its features.
|
Plug-and-play WebSockets for Laravel with a Pusher-compatible protocol, a fast fork-based handler, and practical helpers for broadcasting and testing.
|
||||||
|
|
||||||
|
## Why this package
|
||||||
|
|
||||||
|
- Drop-in broadcasting backend for Laravel apps that already use Echo/Pusher-compatible clients
|
||||||
|
- Fast local handler with async processing via `pcntl_fork`
|
||||||
|
- Protocol compatibility for both modern `websocket.*` and legacy `pusher:*` action formats
|
||||||
|
- Built-in developer ergonomics: helper functions, service methods, and rich test helpers
|
||||||
|
|
||||||
|
## Install in 2 minutes
|
||||||
|
|
||||||
|
1. Install package
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require blax-software/laravel-websockets
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Publish config
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan vendor:publish --provider="BlaxSoftware\\LaravelWebSockets\\WebSocketsServiceProvider" --tag="config"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Start server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan websockets:serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Default server URL is `ws://127.0.0.1:6001`.
|
||||||
|
|
||||||
|
## Helper functions (broadcast from anywhere)
|
||||||
|
|
||||||
|
The package ships with global helpers in `src/helpers_global.php`.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Broadcast to everyone on a channel
|
||||||
|
ws_broadcast('chat.message', ['text' => 'Hello'], 'chat');
|
||||||
|
|
||||||
|
// Whisper to specific socket IDs only
|
||||||
|
ws_whisper('chat.typing', ['typing' => true], ['1234.1', '1234.2'], 'chat');
|
||||||
|
|
||||||
|
// Broadcast to everyone except listed sockets
|
||||||
|
ws_broadcast_except('chat.message', ['text' => 'Server msg'], ['1234.1'], 'chat');
|
||||||
|
|
||||||
|
// Check if local unix-socket broadcaster is available
|
||||||
|
if (ws_available()) {
|
||||||
|
ws_broadcast('app.health', ['ok' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build protocol auth payload for private/presence channels
|
||||||
|
$auth = wsSession('private-updates', ['user_id' => 7, 'user_info' => ['name' => 'Jane']]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service API
|
||||||
|
|
||||||
|
Use the service directly when you prefer explicit class calls over helpers.
|
||||||
|
|
||||||
|
```php
|
||||||
|
use BlaxSoftware\LaravelWebSockets\Services\WebsocketService;
|
||||||
|
|
||||||
|
WebsocketService::send('metrics.tick', ['count' => 1], 'websocket');
|
||||||
|
WebsocketService::whisper('chat.typing', ['typing' => true], ['1234.1'], 'chat');
|
||||||
|
WebsocketService::broadcastExcept('chat.message', ['text' => 'Hi'], ['1234.1'], 'chat');
|
||||||
|
|
||||||
|
// Optional in-process tracking helpers
|
||||||
|
WebsocketService::setUserAuthed($socketId, $userId);
|
||||||
|
$authed = WebsocketService::getAuthedUsers();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing experience
|
||||||
|
|
||||||
|
The test suite includes helper-first patterns so WebSocket tests stay short and readable.
|
||||||
|
|
||||||
|
### Test helpers
|
||||||
|
|
||||||
|
- `newConnection()`
|
||||||
|
- `newActiveConnection(['channel'])`
|
||||||
|
- `newPrivateConnection('private-channel')`
|
||||||
|
- `newPresenceConnection('presence-channel', ['user_id' => 1, 'user_info' => [...]])`
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```php
|
||||||
|
$connection = $this->newActiveConnection(['chat']);
|
||||||
|
|
||||||
|
$this->wsHandler->onMessage($connection, new Message([
|
||||||
|
'event' => 'websocket.ping',
|
||||||
|
'data' => new stdClass(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
$connection->assertSentEvent('websocket.pong');
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vendor/bin/phpunit --exclude-group=stability,stress,integration,requires-server
|
||||||
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
### Features
|
- Main docs: [docs](docs)
|
||||||
* Laravel native event broadcasting
|
- Getting started: [docs/getting-started/introduction.md](docs/getting-started/introduction.md)
|
||||||
* Async with pcntl_fork
|
- Helper & testing guide: [docs/advanced-usage/helpers-and-testing.md](docs/advanced-usage/helpers-and-testing.md)
|
||||||
* SSL support
|
|
||||||
* Laravel Echo support
|
|
||||||
* Docker & Traefik capable
|
|
||||||
|
|
||||||
### Changelog
|
## Changelog
|
||||||
|
|
||||||
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
|
See [CHANGELOG](CHANGELOG.md).
|
||||||
|
|
||||||
### Security
|
## Security
|
||||||
|
|
||||||
If you discover any security related issues, please email office@blax.at or the the issue tracker.
|
Please report vulnerabilities via issue tracker or by email: office@blax.at.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- [Marcel Pociot (beyondco.de)](https://github.com/mpociot)
|
- [Marcel Pociot](https://github.com/mpociot)
|
||||||
- [Freek Van der Herten](https://github.com/freekmurze)
|
- [Freek Van der Herten](https://github.com/freekmurze)
|
||||||
- [All Contributors](../../contributors)
|
- [All Contributors](../../contributors)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
|
MIT. See [LICENSE.md](LICENSE.md).
|
||||||
|
|
|
||||||
|
|
@ -287,14 +287,6 @@ return [
|
||||||
|
|
||||||
'health' => \BlaxSoftware\LaravelWebSockets\Server\HealthHandler::class,
|
'health' => \BlaxSoftware\LaravelWebSockets\Server\HealthHandler::class,
|
||||||
|
|
||||||
'trigger_event' => \BlaxSoftware\LaravelWebSockets\API\TriggerEvent::class,
|
|
||||||
|
|
||||||
'fetch_channels' => \BlaxSoftware\LaravelWebSockets\API\FetchChannels::class,
|
|
||||||
|
|
||||||
'fetch_channel' => \BlaxSoftware\LaravelWebSockets\API\FetchChannel::class,
|
|
||||||
|
|
||||||
'fetch_users' => \BlaxSoftware\LaravelWebSockets\API\FetchUsers::class,
|
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,12 @@
|
||||||
title: Advanced Usage
|
title: Advanced Usage
|
||||||
order: 4
|
order: 4
|
||||||
---
|
---
|
||||||
|
|
||||||
|
# Advanced Usage
|
||||||
|
|
||||||
|
- [Custom WebSocket handlers](custom-websocket-handlers.md)
|
||||||
|
- [Helpers and testing](helpers-and-testing.md)
|
||||||
|
- [Dispatched events](dispatched-events.md)
|
||||||
|
- [App providers](app-providers.md)
|
||||||
|
- [Webhooks](webhooks.md)
|
||||||
|
- [Non-blocking queue driver](non-blocking-queue-driver.md)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
---
|
||||||
|
title: Helpers and Testing
|
||||||
|
order: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Helpers and Testing
|
||||||
|
|
||||||
|
This package is designed to be easy to use from both app code and test code.
|
||||||
|
|
||||||
|
## Global helpers
|
||||||
|
|
||||||
|
Global helpers live in `src/helpers_global.php` and call into the same core service used by the server.
|
||||||
|
|
||||||
|
### Broadcast to channel
|
||||||
|
|
||||||
|
```php
|
||||||
|
ws_broadcast('chat.message', ['text' => 'Hello world'], 'chat');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Whisper to specific sockets
|
||||||
|
|
||||||
|
```php
|
||||||
|
ws_whisper('chat.typing', ['typing' => true], ['1234.1', '1234.2'], 'chat');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Broadcast except sockets
|
||||||
|
|
||||||
|
```php
|
||||||
|
ws_broadcast_except('chat.message', ['text' => 'Server update'], ['1234.1'], 'chat');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Runtime availability check
|
||||||
|
|
||||||
|
```php
|
||||||
|
if (ws_available()) {
|
||||||
|
ws_broadcast('system.heartbeat', ['ok' => true]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate auth payload
|
||||||
|
|
||||||
|
```php
|
||||||
|
$auth = wsSession('presence-room', [
|
||||||
|
'user_id' => 42,
|
||||||
|
'user_info' => ['name' => 'Amelia'],
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this when you need to produce channel auth payloads in custom flows.
|
||||||
|
|
||||||
|
## WebsocketService class
|
||||||
|
|
||||||
|
If you prefer explicit classes over helpers:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use BlaxSoftware\LaravelWebSockets\Services\WebsocketService;
|
||||||
|
|
||||||
|
WebsocketService::send('chat.message', ['text' => 'Hello'], 'chat');
|
||||||
|
WebsocketService::whisper('chat.typing', ['typing' => true], ['1234.1'], 'chat');
|
||||||
|
WebsocketService::broadcastExcept('chat.message', ['text' => 'Hi'], ['1234.1'], 'chat');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tracking helpers
|
||||||
|
|
||||||
|
`WebsocketService` also exposes lightweight in-process tracking helpers:
|
||||||
|
|
||||||
|
- `setUserAuthed($socketId, $userId)`
|
||||||
|
- `clearUserAuthed($socketId)`
|
||||||
|
- `getAuth()`
|
||||||
|
- `getAuthedUsers()`
|
||||||
|
- `isUserConnected($userId)`
|
||||||
|
- `getUserSocketIds($userId)`
|
||||||
|
- `getActiveChannels()`
|
||||||
|
- `getChannelConnections($channel)`
|
||||||
|
- `resetAllTracking()`
|
||||||
|
|
||||||
|
## Testing with package helpers
|
||||||
|
|
||||||
|
The package test base class includes high-level helpers that make tests concise:
|
||||||
|
|
||||||
|
- `newConnection()`
|
||||||
|
- `newActiveConnection(['channel'])`
|
||||||
|
- `newPrivateConnection('private-channel')`
|
||||||
|
- `newPresenceConnection('presence-channel', ['user_id' => 1, 'user_info' => [...]])`
|
||||||
|
|
||||||
|
### Example ping/pong test
|
||||||
|
|
||||||
|
```php
|
||||||
|
$connection = $this->newActiveConnection(['websocket']);
|
||||||
|
$connection->resetEvents();
|
||||||
|
|
||||||
|
$this->wsHandler->onMessage($connection, new Message([
|
||||||
|
'event' => 'websocket.ping',
|
||||||
|
'data' => new stdClass(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
$connection->assertSentEvent('websocket.pong');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example subscription test
|
||||||
|
|
||||||
|
```php
|
||||||
|
$connection = $this->newPrivateConnection('private-chat');
|
||||||
|
$connection->assertSentEvent('websocket_internal.subscription_succeeded');
|
||||||
|
```
|
||||||
|
|
||||||
|
## New reference test files
|
||||||
|
|
||||||
|
For complete examples, see:
|
||||||
|
|
||||||
|
- `tests/WebsocketServiceTest.php`
|
||||||
|
- `tests/HandlerLifecycleTest.php`
|
||||||
|
|
||||||
|
These files cover helper-first testing patterns and full handler lifecycle behavior.
|
||||||
|
|
@ -1,296 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\API;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Apps\App;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\QueryParameters;
|
|
||||||
use Exception;
|
|
||||||
use GuzzleHttp\Psr7\Message;
|
|
||||||
use GuzzleHttp\Psr7\Response;
|
|
||||||
use GuzzleHttp\Psr7\ServerRequest;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Arr;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Psr\Http\Message\RequestInterface;
|
|
||||||
use Pusher\Pusher;
|
|
||||||
use Ratchet\ConnectionInterface;
|
|
||||||
use Ratchet\Http\HttpServerInterface;
|
|
||||||
use React\Promise\Deferred;
|
|
||||||
use React\Promise\PromiseInterface;
|
|
||||||
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
||||||
|
|
||||||
abstract class Controller implements HttpServerInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The request buffer.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $requestBuffer = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The incoming request.
|
|
||||||
*
|
|
||||||
* @var \Psr\Http\Message\RequestInterface
|
|
||||||
*/
|
|
||||||
protected $request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The content length that will
|
|
||||||
* be calculated.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $contentLength;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The channel manager.
|
|
||||||
*
|
|
||||||
* @var \BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager
|
|
||||||
*/
|
|
||||||
protected $channelManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the request.
|
|
||||||
*
|
|
||||||
* @param ChannelManager $channelManager
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct(ChannelManager $channelManager)
|
|
||||||
{
|
|
||||||
$this->channelManager = $channelManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the opened socket connection.
|
|
||||||
*
|
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
|
||||||
* @param \Psr\Http\Message\RequestInterface $request
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function onOpen(ConnectionInterface $connection, RequestInterface $request = null)
|
|
||||||
{
|
|
||||||
$this->request = $request;
|
|
||||||
|
|
||||||
$this->contentLength = $this->findContentLength($request->getHeaders());
|
|
||||||
|
|
||||||
$this->requestBuffer = (string) $request->getBody();
|
|
||||||
|
|
||||||
if (! $this->verifyContentLength()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->handleRequest($connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the oncoming message and add it to buffer.
|
|
||||||
*
|
|
||||||
* @param \Ratchet\ConnectionInterface $from
|
|
||||||
* @param mixed $msg
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function onMessage(ConnectionInterface $from, $msg)
|
|
||||||
{
|
|
||||||
$this->requestBuffer .= $msg;
|
|
||||||
|
|
||||||
if (! $this->verifyContentLength()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->handleRequest($from);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the socket closing.
|
|
||||||
*
|
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function onClose(ConnectionInterface $connection)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the errors.
|
|
||||||
*
|
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
|
||||||
* @param Exception $exception
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function onError(ConnectionInterface $connection, Exception $exception)
|
|
||||||
{
|
|
||||||
if (! $exception instanceof HttpException) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = new Response($exception->getStatusCode(), [
|
|
||||||
'Content-Type' => 'application/json',
|
|
||||||
], json_encode([
|
|
||||||
'error' => $exception->getMessage(),
|
|
||||||
]));
|
|
||||||
|
|
||||||
tap($connection)->send(Message::toString($response))->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the content length from the headers.
|
|
||||||
*
|
|
||||||
* @param array $headers
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
protected function findContentLength(array $headers): int
|
|
||||||
{
|
|
||||||
return Collection::make($headers)->first(function ($values, $header) {
|
|
||||||
return strtolower($header) === 'content-length';
|
|
||||||
})[0] ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check the content length.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
protected function verifyContentLength()
|
|
||||||
{
|
|
||||||
return strlen($this->requestBuffer) === $this->contentLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the oncoming connection.
|
|
||||||
*
|
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function handleRequest(ConnectionInterface $connection)
|
|
||||||
{
|
|
||||||
$serverRequest = (new ServerRequest(
|
|
||||||
$this->request->getMethod(),
|
|
||||||
$this->request->getUri(),
|
|
||||||
$this->request->getHeaders(),
|
|
||||||
$this->requestBuffer,
|
|
||||||
$this->request->getProtocolVersion()
|
|
||||||
))->withQueryParams(QueryParameters::create($this->request)->all());
|
|
||||||
|
|
||||||
$laravelRequest = Request::createFromBase((new HttpFoundationFactory)->createRequest($serverRequest));
|
|
||||||
|
|
||||||
$this
|
|
||||||
->ensureValidAppId($laravelRequest->appId)
|
|
||||||
->then(function ($app) use ($laravelRequest, $connection) {
|
|
||||||
try {
|
|
||||||
$this->ensureValidSignature($app, $laravelRequest);
|
|
||||||
} catch (HttpException $exception) {
|
|
||||||
$this->onError($connection, $exception);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invoke the controller action
|
|
||||||
try {
|
|
||||||
$response = $this($laravelRequest);
|
|
||||||
} catch (HttpException $exception) {
|
|
||||||
$this->onError($connection, $exception);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow for async IO in the controller action
|
|
||||||
if ($response instanceof PromiseInterface) {
|
|
||||||
$response->then(function ($response) use ($connection) {
|
|
||||||
$this->sendAndClose($connection, $response);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($response instanceof HttpException) {
|
|
||||||
$this->onError($connection, $response);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->sendAndClose($connection, $response);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send the response and close the connection.
|
|
||||||
*
|
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
|
||||||
* @param mixed $response
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function sendAndClose(ConnectionInterface $connection, $response)
|
|
||||||
{
|
|
||||||
tap($connection)->send(new JsonResponse($response))->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure app existence.
|
|
||||||
*
|
|
||||||
* @param mixed $appId
|
|
||||||
* @return PromiseInterface
|
|
||||||
*
|
|
||||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
|
||||||
*/
|
|
||||||
public function ensureValidAppId($appId)
|
|
||||||
{
|
|
||||||
$deferred = new Deferred();
|
|
||||||
|
|
||||||
App::findById($appId)
|
|
||||||
->then(function ($app) use ($appId, $deferred) {
|
|
||||||
if (! $app) {
|
|
||||||
throw new HttpException(401, "Unknown app id `{$appId}` provided.");
|
|
||||||
}
|
|
||||||
$deferred->resolve($app);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $deferred->promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure signature integrity coming from an
|
|
||||||
* authorized application.
|
|
||||||
*
|
|
||||||
* @param App $app
|
|
||||||
* @param Request $request
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
protected function ensureValidSignature(App $app, Request $request)
|
|
||||||
{
|
|
||||||
// The `auth_signature` & `body_md5` parameters are not included when calculating the `auth_signature` value.
|
|
||||||
// The `appId`, `appKey` & `channelName` parameters are actually route parameters and are never supplied by the client.
|
|
||||||
|
|
||||||
$params = Arr::except($request->query(), [
|
|
||||||
'auth_signature', 'body_md5', 'appId', 'appKey', 'channelName',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($request->getContent() !== '') {
|
|
||||||
$params['body_md5'] = md5($request->getContent());
|
|
||||||
}
|
|
||||||
|
|
||||||
ksort($params);
|
|
||||||
|
|
||||||
$signature = "{$request->getMethod()}\n/{$request->path()}\n".Pusher::array_implode('=', '&', $params);
|
|
||||||
|
|
||||||
$authSignature = hash_hmac('sha256', $signature, $app->secret);
|
|
||||||
|
|
||||||
if ($authSignature !== $request->get('auth_signature')) {
|
|
||||||
throw new HttpException(401, 'Invalid auth signature provided.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the incoming request.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
abstract public function __invoke(Request $request);
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\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,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\API;
|
|
||||||
|
|
||||||
use BlaxSoftware\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,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\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,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\API;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\DashboardLogger;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Facades\StatisticsCollector;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use React\Promise\Deferred;
|
|
||||||
use React\Promise\PromiseInterface;
|
|
||||||
|
|
||||||
class TriggerEvent extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Handle the incoming request.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @return PromiseInterface
|
|
||||||
*/
|
|
||||||
public function __invoke(Request $request)
|
|
||||||
{
|
|
||||||
if ($request->has('channel')) {
|
|
||||||
$channels = [$request->get('channel')];
|
|
||||||
} else {
|
|
||||||
$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
|
|
||||||
);
|
|
||||||
|
|
||||||
$deferred = new Deferred();
|
|
||||||
|
|
||||||
$this->ensureValidAppId($request->appId)
|
|
||||||
->then(function ($app) use ($request, $channelName, $deferred) {
|
|
||||||
if ($app->statisticsEnabled) {
|
|
||||||
StatisticsCollector::apiMessage($request->appId);
|
|
||||||
}
|
|
||||||
|
|
||||||
DashboardLogger::log($request->appId, DashboardLogger::TYPE_API_MESSAGE, [
|
|
||||||
'event' => $request->name,
|
|
||||||
'channel' => $channelName,
|
|
||||||
'payload' => $request->data,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$deferred->resolve((object) []);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return $deferred->promise();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Apps;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\AppManager;
|
|
||||||
use React\MySQL\ConnectionInterface;
|
|
||||||
use React\MySQL\QueryResult;
|
|
||||||
use React\Promise\Deferred;
|
|
||||||
use React\Promise\PromiseInterface;
|
|
||||||
|
|
||||||
class MysqlAppManager implements AppManager
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The database connection.
|
|
||||||
*
|
|
||||||
* @var ConnectionInterface
|
|
||||||
*/
|
|
||||||
protected $database;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the class.
|
|
||||||
*
|
|
||||||
* @param ConnectionInterface $database
|
|
||||||
*/
|
|
||||||
public function __construct(ConnectionInterface $database)
|
|
||||||
{
|
|
||||||
$this->database = $database;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getTableName(): string
|
|
||||||
{
|
|
||||||
return config('websockets.managers.mysql.table');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all apps.
|
|
||||||
*
|
|
||||||
* @return PromiseInterface
|
|
||||||
*/
|
|
||||||
public function all(): PromiseInterface
|
|
||||||
{
|
|
||||||
$deferred = new Deferred();
|
|
||||||
|
|
||||||
$this->database->query('SELECT * FROM `'.$this->getTableName().'`')
|
|
||||||
->then(function (QueryResult $result) use ($deferred) {
|
|
||||||
$deferred->resolve($result->resultRows);
|
|
||||||
}, function ($error) use ($deferred) {
|
|
||||||
$deferred->reject($error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $deferred->promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get app by id.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return PromiseInterface
|
|
||||||
*/
|
|
||||||
public function findById($appId): PromiseInterface
|
|
||||||
{
|
|
||||||
$deferred = new Deferred();
|
|
||||||
|
|
||||||
$this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `id` = ?', [$appId])
|
|
||||||
->then(function (QueryResult $result) use ($deferred) {
|
|
||||||
$deferred->resolve($this->convertIntoApp($result->resultRows[0]));
|
|
||||||
}, function ($error) use ($deferred) {
|
|
||||||
$deferred->reject($error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $deferred->promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get app by app key.
|
|
||||||
*
|
|
||||||
* @param string $appKey
|
|
||||||
* @return PromiseInterface
|
|
||||||
*/
|
|
||||||
public function findByKey($appKey): PromiseInterface
|
|
||||||
{
|
|
||||||
$deferred = new Deferred();
|
|
||||||
|
|
||||||
$this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `key` = ?', [$appKey])
|
|
||||||
->then(function (QueryResult $result) use ($deferred) {
|
|
||||||
$deferred->resolve($this->convertIntoApp($result->resultRows[0]));
|
|
||||||
}, function ($error) use ($deferred) {
|
|
||||||
$deferred->reject($error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $deferred->promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get app by secret.
|
|
||||||
*
|
|
||||||
* @param string $appSecret
|
|
||||||
* @return PromiseInterface
|
|
||||||
*/
|
|
||||||
public function findBySecret($appSecret): PromiseInterface
|
|
||||||
{
|
|
||||||
$deferred = new Deferred();
|
|
||||||
|
|
||||||
$this->database->query('SELECT * from `'.$this->getTableName().'` WHERE `secret` = ?', [$appSecret])
|
|
||||||
->then(function (QueryResult $result) use ($deferred) {
|
|
||||||
$deferred->resolve($this->convertIntoApp($result->resultRows[0]));
|
|
||||||
}, function ($error) use ($deferred) {
|
|
||||||
$deferred->reject($error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $deferred->promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map the app into an App instance.
|
|
||||||
*
|
|
||||||
* @param array|null $app
|
|
||||||
* @return \BlaxSoftware\LaravelWebSockets\Apps\App|null
|
|
||||||
*/
|
|
||||||
protected function convertIntoApp(?array $appAttributes): ?App
|
|
||||||
{
|
|
||||||
if (! $appAttributes) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$app = new App(
|
|
||||||
$appAttributes['id'],
|
|
||||||
$appAttributes['key'],
|
|
||||||
$appAttributes['secret']
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isset($appAttributes['name'])) {
|
|
||||||
$app->setName($appAttributes['name']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($appAttributes['host'])) {
|
|
||||||
$app->setHost($appAttributes['host']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($appAttributes['path'])) {
|
|
||||||
$app->setPath($appAttributes['path']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$app
|
|
||||||
->enableClientMessages((bool) $appAttributes['enable_client_messages'])
|
|
||||||
->enableStatistics((bool) $appAttributes['enable_statistics'])
|
|
||||||
->setCapacity($appAttributes['capacity'] ?? null)
|
|
||||||
->setAllowedOrigins(array_filter(explode(',', $appAttributes['allowed_origins'])));
|
|
||||||
|
|
||||||
return $app;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritDoc
|
|
||||||
*/
|
|
||||||
public function createApp($appData): PromiseInterface
|
|
||||||
{
|
|
||||||
$deferred = new Deferred();
|
|
||||||
|
|
||||||
$this->database->query(
|
|
||||||
'INSERT INTO `'.$this->getTableName().'` (`id`, `key`, `secret`, `name`, `enable_client_messages`, `enable_statistics`, `allowed_origins`, `capacity`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
||||||
[$appData['id'], $appData['key'], $appData['secret'], $appData['name'], $appData['enable_client_messages'], $appData['enable_statistics'], $appData['allowed_origins'] ?? '', $appData['capacity'] ?? null])
|
|
||||||
->then(function () use ($deferred) {
|
|
||||||
$deferred->resolve();
|
|
||||||
}, function ($error) use ($deferred) {
|
|
||||||
$deferred->reject($error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $deferred->promise();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,167 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Apps;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\AppManager;
|
|
||||||
use Clue\React\SQLite\DatabaseInterface;
|
|
||||||
use Clue\React\SQLite\Result;
|
|
||||||
use React\Promise\Deferred;
|
|
||||||
use React\Promise\PromiseInterface;
|
|
||||||
|
|
||||||
class SQLiteAppManager implements AppManager
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The database connection.
|
|
||||||
*
|
|
||||||
* @var DatabaseInterface
|
|
||||||
*/
|
|
||||||
protected $database;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the class.
|
|
||||||
*
|
|
||||||
* @param DatabaseInterface $database
|
|
||||||
*/
|
|
||||||
public function __construct(DatabaseInterface $database)
|
|
||||||
{
|
|
||||||
$this->database = $database;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all apps.
|
|
||||||
*
|
|
||||||
* @return PromiseInterface
|
|
||||||
*/
|
|
||||||
public function all(): PromiseInterface
|
|
||||||
{
|
|
||||||
$deferred = new Deferred();
|
|
||||||
|
|
||||||
$this->database->query('SELECT * FROM `apps`')
|
|
||||||
->then(function (Result $result) use ($deferred) {
|
|
||||||
$deferred->resolve($result->rows);
|
|
||||||
}, function ($error) use ($deferred) {
|
|
||||||
$deferred->reject($error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $deferred->promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get app by id.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return PromiseInterface
|
|
||||||
*/
|
|
||||||
public function findById($appId): PromiseInterface
|
|
||||||
{
|
|
||||||
$deferred = new Deferred();
|
|
||||||
|
|
||||||
$this->database->query('SELECT * from apps WHERE `id` = :id', ['id' => $appId])
|
|
||||||
->then(function (Result $result) use ($deferred) {
|
|
||||||
$deferred->resolve($this->convertIntoApp($result->rows[0]));
|
|
||||||
}, function ($error) use ($deferred) {
|
|
||||||
$deferred->reject($error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $deferred->promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get app by app key.
|
|
||||||
*
|
|
||||||
* @param string $appKey
|
|
||||||
* @return PromiseInterface
|
|
||||||
*/
|
|
||||||
public function findByKey($appKey): PromiseInterface
|
|
||||||
{
|
|
||||||
$deferred = new Deferred();
|
|
||||||
|
|
||||||
$this->database->query('SELECT * from apps WHERE `key` = :key', ['key' => $appKey])
|
|
||||||
->then(function (Result $result) use ($deferred) {
|
|
||||||
$deferred->resolve($this->convertIntoApp($result->rows[0]));
|
|
||||||
}, function ($error) use ($deferred) {
|
|
||||||
$deferred->reject($error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $deferred->promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get app by secret.
|
|
||||||
*
|
|
||||||
* @param string $appSecret
|
|
||||||
* @return PromiseInterface
|
|
||||||
*/
|
|
||||||
public function findBySecret($appSecret): PromiseInterface
|
|
||||||
{
|
|
||||||
$deferred = new Deferred();
|
|
||||||
|
|
||||||
$this->database->query('SELECT * from apps WHERE `secret` = :secret', ['secret' => $appSecret])
|
|
||||||
->then(function (Result $result) use ($deferred) {
|
|
||||||
$deferred->resolve($this->convertIntoApp($result->rows[0]));
|
|
||||||
}, function ($error) use ($deferred) {
|
|
||||||
$deferred->reject($error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $deferred->promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map the app into an App instance.
|
|
||||||
*
|
|
||||||
* @param array|null $app
|
|
||||||
* @return \BlaxSoftware\LaravelWebSockets\Apps\App|null
|
|
||||||
*/
|
|
||||||
protected function convertIntoApp(?array $appAttributes): ?App
|
|
||||||
{
|
|
||||||
if (! $appAttributes) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$app = new App(
|
|
||||||
$appAttributes['id'],
|
|
||||||
$appAttributes['key'],
|
|
||||||
$appAttributes['secret']
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isset($appAttributes['name'])) {
|
|
||||||
$app->setName($appAttributes['name']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($appAttributes['host'])) {
|
|
||||||
$app->setHost($appAttributes['host']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($appAttributes['path'])) {
|
|
||||||
$app->setPath($appAttributes['path']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$app
|
|
||||||
->enableClientMessages((bool) $appAttributes['enable_client_messages'])
|
|
||||||
->enableStatistics((bool) $appAttributes['enable_statistics'])
|
|
||||||
->setCapacity($appAttributes['capacity'] ?? null)
|
|
||||||
->setAllowedOrigins(array_filter(explode(',', $appAttributes['allowed_origins'])));
|
|
||||||
|
|
||||||
return $app;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritDoc
|
|
||||||
*/
|
|
||||||
public function createApp($appData): PromiseInterface
|
|
||||||
{
|
|
||||||
$deferred = new Deferred();
|
|
||||||
|
|
||||||
$this->database->query('
|
|
||||||
INSERT INTO apps (id, key, secret, name, host, path, enable_client_messages, enable_statistics, capacity, allowed_origins)
|
|
||||||
VALUES (:id, :key, :secret, :name, :host, :path, :enable_client_messages, :enable_statistics, :capacity, :allowed_origins)
|
|
||||||
', $appData)
|
|
||||||
->then(function (Result $result) use ($deferred) {
|
|
||||||
$deferred->resolve();
|
|
||||||
}, function ($error) use ($deferred) {
|
|
||||||
$deferred->reject($error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $deferred->promise();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Cache;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Helpers;
|
|
||||||
use Illuminate\Cache\ArrayLock as LaravelLock;
|
|
||||||
use React\Promise\PromiseInterface;
|
|
||||||
|
|
||||||
class ArrayLock extends Lock
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The parent array cache store.
|
|
||||||
*
|
|
||||||
* @var \Illuminate\Cache\ArrayStore
|
|
||||||
*/
|
|
||||||
protected $store;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal Laravel Array Lock.
|
|
||||||
*
|
|
||||||
* @var \Illuminate\Cache\ArrayLock
|
|
||||||
*/
|
|
||||||
protected $lock;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new lock instance.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Cache\ArrayStore $store
|
|
||||||
* @param string $name
|
|
||||||
* @param int $seconds
|
|
||||||
* @param string|null $owner
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct($store, $name, $seconds, $owner = null)
|
|
||||||
{
|
|
||||||
parent::__construct($name, $seconds, $owner);
|
|
||||||
|
|
||||||
$this->lock = new LaravelLock($store, $name, $seconds, $owner);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function acquire(): PromiseInterface
|
|
||||||
{
|
|
||||||
return Helpers::createFulfilledPromise($this->lock->acquire());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get($callback = null): PromiseInterface
|
|
||||||
{
|
|
||||||
return $this->lock->get($callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function release(): PromiseInterface
|
|
||||||
{
|
|
||||||
return Helpers::createFulfilledPromise($this->lock->release());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Cache;
|
|
||||||
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use React\Promise\PromiseInterface;
|
|
||||||
|
|
||||||
abstract class Lock
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name of the lock.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of seconds the lock should be maintained.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $seconds;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The scope identifier of this lock.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $owner;
|
|
||||||
|
|
||||||
public function __construct($name, $seconds, $owner = null)
|
|
||||||
{
|
|
||||||
if (is_null($owner)) {
|
|
||||||
$owner = Str::random();
|
|
||||||
}
|
|
||||||
$this->name = $name;
|
|
||||||
$this->seconds = $seconds;
|
|
||||||
$this->owner = $owner;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract public function acquire(): PromiseInterface;
|
|
||||||
|
|
||||||
abstract public function get($callback = null): PromiseInterface;
|
|
||||||
|
|
||||||
abstract public function release(): PromiseInterface;
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Cache;
|
|
||||||
|
|
||||||
use Clue\React\Redis\Client;
|
|
||||||
use Illuminate\Cache\LuaScripts;
|
|
||||||
use React\Promise\Deferred;
|
|
||||||
use React\Promise\PromiseInterface;
|
|
||||||
|
|
||||||
class RedisLock extends Lock
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The asynchronous redis client.
|
|
||||||
*
|
|
||||||
* @var Client
|
|
||||||
*/
|
|
||||||
protected $redis;
|
|
||||||
|
|
||||||
public function __construct(Client $redis, $name, $seconds, $owner = null)
|
|
||||||
{
|
|
||||||
parent::__construct($name, $seconds, $owner);
|
|
||||||
|
|
||||||
$this->redis = $redis;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function acquire(): PromiseInterface
|
|
||||||
{
|
|
||||||
$promise = new Deferred();
|
|
||||||
|
|
||||||
if ($this->seconds > 0) {
|
|
||||||
$this->redis
|
|
||||||
->set($this->name, $this->owner, 'EX', $this->seconds, 'NX')
|
|
||||||
->then(function ($result) use ($promise) {
|
|
||||||
$promise->resolve($result === true);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$this->redis
|
|
||||||
->setnx($this->name, $this->owner)
|
|
||||||
->then(function ($result) use ($promise) {
|
|
||||||
$promise->resolve($result === 1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return $promise->promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get($callback = null): PromiseInterface
|
|
||||||
{
|
|
||||||
$promise = new Deferred();
|
|
||||||
|
|
||||||
$this->acquire()
|
|
||||||
->then(function ($result) use ($callback, $promise) {
|
|
||||||
if ($result) {
|
|
||||||
try {
|
|
||||||
$callback();
|
|
||||||
} finally {
|
|
||||||
$promise->resolve($this->release());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return $promise->promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function release(): PromiseInterface
|
|
||||||
{
|
|
||||||
return $this->redis->eval(LuaScripts::releaseLock(), 1, $this->name, $this->owner);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\ChannelManagers;
|
namespace BlaxSoftware\LaravelWebSockets\ChannelManagers;
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Cache\ArrayLock;
|
use Illuminate\Cache\ArrayLock;
|
||||||
use BlaxSoftware\LaravelWebSockets\Channels\Channel;
|
use BlaxSoftware\LaravelWebSockets\Channels\Channel;
|
||||||
use BlaxSoftware\LaravelWebSockets\Channels\OpenPresenceChannel;
|
use BlaxSoftware\LaravelWebSockets\Channels\OpenPresenceChannel;
|
||||||
use BlaxSoftware\LaravelWebSockets\Channels\PresenceChannel;
|
use BlaxSoftware\LaravelWebSockets\Channels\PresenceChannel;
|
||||||
|
|
|
||||||
|
|
@ -1,891 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\ChannelManagers;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Cache\RedisLock;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Channels\Channel;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\DashboardLogger;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Helpers;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\MockableConnection;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Clue\React\Redis\Client;
|
|
||||||
use Clue\React\Redis\Factory;
|
|
||||||
use Illuminate\Support\Facades\Redis;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Ratchet\ConnectionInterface;
|
|
||||||
use React\EventLoop\LoopInterface;
|
|
||||||
use React\Promise\PromiseInterface;
|
|
||||||
use stdClass;
|
|
||||||
|
|
||||||
use function React\Promise\all;
|
|
||||||
|
|
||||||
class RedisChannelManager extends LocalChannelManager
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The running loop.
|
|
||||||
*
|
|
||||||
* @var LoopInterface
|
|
||||||
*/
|
|
||||||
protected $loop;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The pub client.
|
|
||||||
*
|
|
||||||
* @var Client
|
|
||||||
*/
|
|
||||||
protected $publishClient;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The sub client.
|
|
||||||
*
|
|
||||||
* @var Client
|
|
||||||
*/
|
|
||||||
protected $subscribeClient;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Redis manager instance.
|
|
||||||
*
|
|
||||||
* @var \Illuminate\Redis\RedisManager
|
|
||||||
*/
|
|
||||||
protected $redis;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new channel manager instance.
|
|
||||||
*
|
|
||||||
* @param LoopInterface $loop
|
|
||||||
* @param string|null $factoryClass
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct(LoopInterface $loop, $factoryClass = null)
|
|
||||||
{
|
|
||||||
parent::__construct($loop, $factoryClass);
|
|
||||||
|
|
||||||
$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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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->getChannelsRedisHash($appId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
$promises = [];
|
|
||||||
foreach ($channels as $channel) {
|
|
||||||
$promises[] = $this->unsubscribeFromChannel($connection, $channel, new stdClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
return all($promises);
|
|
||||||
})
|
|
||||||
->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.
|
|
||||||
return $this->unsubscribeFromTopic($connection->app->id, $channelName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Helpers::createFulfilledPromise(null);
|
|
||||||
})
|
|
||||||
->then(function () use ($connection, $channelName) {
|
|
||||||
return $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) {
|
|
||||||
$promises = [];
|
|
||||||
|
|
||||||
$promises[] = $this->unsubscribeFromTopic($connection->app->id, $channelName);
|
|
||||||
$promises[] = $this->removeChannelFromSet($connection->app->id, $channelName);
|
|
||||||
|
|
||||||
return all($promises);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
->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
|
|
||||||
* 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->getStatsRedisHash($appId, $channelName), '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->getRedisTopicName($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->getUsersRedisHash($appId, $channel))
|
|
||||||
->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->getUsersRedisHash($connection->app->id, $channel), $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->getUsersRedisHash($appId, $channel)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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->getUserSocketsRedisHash($appId, $channelName, $userId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
$payload = [
|
|
||||||
'socketId' => $connection->socketId,
|
|
||||||
'appId' => $connection->app->id,
|
|
||||||
'serverId' => $this->getServerId(),
|
|
||||||
];
|
|
||||||
|
|
||||||
return $this->publishClient
|
|
||||||
->publish($this->getPongRedisHash($connection->app->id), json_encode($payload));
|
|
||||||
})
|
|
||||||
->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
|
|
||||||
{
|
|
||||||
return $this->lock()->get(function () {
|
|
||||||
return $this->getConnectionsFromSet(0, now()->subMinutes(2)->format('U'))
|
|
||||||
->then(function ($connections) {
|
|
||||||
$promises = [];
|
|
||||||
foreach ($connections as $socketId => $appId) {
|
|
||||||
$connection = $this->fakeConnectionForApp($appId, $socketId);
|
|
||||||
|
|
||||||
$promises[] = $this->unsubscribeFromAllChannels($connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
return all($promises);
|
|
||||||
});
|
|
||||||
})->then(function () {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($redisChannel == $this->getPongRedisHash($payload->appId)) {
|
|
||||||
$connection = $this->fakeConnectionForApp($payload->appId, $payload->socketId);
|
|
||||||
|
|
||||||
return parent::connectionPonged($connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
$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;
|
|
||||||
|
|
||||||
DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_MESSAGE_RECEIVED, [
|
|
||||||
'fromServerId' => $serverId,
|
|
||||||
'fromSocketId' => $socketId,
|
|
||||||
'receiverServerId' => $this->getServerId(),
|
|
||||||
'channel' => $channel,
|
|
||||||
'payload' => $payload,
|
|
||||||
]);
|
|
||||||
|
|
||||||
unset($payload->socketId);
|
|
||||||
unset($payload->serverId);
|
|
||||||
unset($payload->appId);
|
|
||||||
|
|
||||||
$channel->broadcastLocallyToEveryoneExcept($payload, $socketId, $appId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function find($appId, string $channel)
|
|
||||||
{
|
|
||||||
if (! $channelInstance = parent::find($appId, $channel)) {
|
|
||||||
$class = $this->getChannelClassName($channel);
|
|
||||||
$this->channels[$appId][$channel] = new $class($channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent::find($appId, $channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the Redis connection URL from Laravel database config.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
protected function getConnectionUri()
|
|
||||||
{
|
|
||||||
$name = config('websockets.replication.modes.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['db'] = $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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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->getStatsRedisHash($appId, $channel), '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->getSocketsRedisHash(),
|
|
||||||
$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->getSocketsRedisHash(),
|
|
||||||
"{$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->getSocketsRedisHash(), $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->getChannelsRedisHash($appId), $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->getChannelsRedisHash($appId), $channel
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if channel is on the list.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @param string $channel
|
|
||||||
* @return PromiseInterface
|
|
||||||
*/
|
|
||||||
public function isChannelInSet($appId, string $channel): PromiseInterface
|
|
||||||
{
|
|
||||||
return $this->publishClient->sismember(
|
|
||||||
$this->getChannelsRedisHash($appId), $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->getUsersRedisHash($appId, $channel), $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->getUsersRedisHash($appId, $channel), $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
|
|
||||||
{
|
|
||||||
$topic = $this->getRedisTopicName($appId, $channel);
|
|
||||||
|
|
||||||
DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_SUBSCRIBED, [
|
|
||||||
'serverId' => $this->getServerId(),
|
|
||||||
'pubsubTopic' => $topic,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $this->subscribeClient->subscribe($topic);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
{
|
|
||||||
$topic = $this->getRedisTopicName($appId, $channel);
|
|
||||||
|
|
||||||
DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_UNSUBSCRIBED, [
|
|
||||||
'serverId' => $this->getServerId(),
|
|
||||||
'pubsubTopic' => $topic,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $this->subscribeClient->unsubscribe($topic);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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->getUserSocketsRedisHash($appId, $channel, $user->user_id), $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->getUserSocketsRedisHash($appId, $channel, $user->user_id), $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 the pong Redis hash.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
*/
|
|
||||||
public function getPongRedisHash($appId): string
|
|
||||||
{
|
|
||||||
return $this->getRedisKey($appId, null, ['pong']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the statistics Redis hash.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @param string|null $channel
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getStatsRedisHash($appId, string $channel = null): string
|
|
||||||
{
|
|
||||||
return $this->getRedisKey($appId, $channel, ['stats']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the sockets Redis hash used to store all sockets ids.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getSocketsRedisHash(): string
|
|
||||||
{
|
|
||||||
return $this->getRedisKey(null, null, ['sockets']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the channels Redis hash for a specific app id, used
|
|
||||||
* to store existing channels.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getChannelsRedisHash($appId): string
|
|
||||||
{
|
|
||||||
return $this->getRedisKey($appId, null, ['channels']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the Redis hash for storing presence channels users.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @param string|null $channel
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getUsersRedisHash($appId, string $channel = null): string
|
|
||||||
{
|
|
||||||
return $this->getRedisKey($appId, $channel, ['users']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the Redis hash for storing socket ids
|
|
||||||
* for a specific presence channels user.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @param string|null $channel
|
|
||||||
* @param string|int|null $userId
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getUserSocketsRedisHash($appId, string $channel = null, $userId = null): string
|
|
||||||
{
|
|
||||||
return $this->getRedisKey($appId, $channel, [$userId, 'userSockets']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the Redis topic name for PubSub
|
|
||||||
* used to transfer info between servers.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @param string|null $channel
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getRedisTopicName($appId, string $channel = null): string
|
|
||||||
{
|
|
||||||
return $this->getRedisKey($appId, $channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a new RedisLock instance to avoid race conditions.
|
|
||||||
*
|
|
||||||
* @return RedisLock
|
|
||||||
*/
|
|
||||||
protected function lock()
|
|
||||||
{
|
|
||||||
return new RedisLock($this->publishClient, 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,9 +3,6 @@
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Channels;
|
namespace BlaxSoftware\LaravelWebSockets\Channels;
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
use BlaxSoftware\LaravelWebSockets\DashboardLogger;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Events\SubscribedToChannel;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Events\UnsubscribedFromChannel;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Helpers;
|
use BlaxSoftware\LaravelWebSockets\Helpers;
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
@ -103,21 +100,10 @@ class Channel
|
||||||
$this->saveConnection($connection);
|
$this->saveConnection($connection);
|
||||||
|
|
||||||
$connection->send(json_encode([
|
$connection->send(json_encode([
|
||||||
'event' => 'pusher_internal:subscription_succeeded',
|
'event' => 'websocket_internal.subscription_succeeded',
|
||||||
'channel' => $this->getName(),
|
'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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,12 +121,6 @@ class Channel
|
||||||
|
|
||||||
unset($this->connections[$connection->socketId]);
|
unset($this->connections[$connection->socketId]);
|
||||||
|
|
||||||
UnsubscribedFromChannel::dispatch(
|
|
||||||
$connection->app->id,
|
|
||||||
$connection->socketId,
|
|
||||||
$this->getName()
|
|
||||||
);
|
|
||||||
|
|
||||||
return Helpers::createFulfilledPromise(true);
|
return Helpers::createFulfilledPromise(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,6 @@
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Channels;
|
namespace BlaxSoftware\LaravelWebSockets\Channels;
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\DashboardLogger;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Events\SubscribedToChannel;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Events\UnsubscribedFromChannel;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Helpers;
|
use BlaxSoftware\LaravelWebSockets\Helpers;
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
|
|
@ -45,7 +42,7 @@ class PresenceChannel extends PrivateChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
$connection->send(json_encode([
|
$connection->send(json_encode([
|
||||||
'event' => 'pusher_internal:subscription_succeeded',
|
'event' => 'websocket_internal.subscription_succeeded',
|
||||||
'channel' => $this->getName(),
|
'channel' => $this->getName(),
|
||||||
'data' => json_encode([
|
'data' => json_encode([
|
||||||
'presence' => [
|
'presence' => [
|
||||||
|
|
@ -60,7 +57,7 @@ class PresenceChannel extends PrivateChannel
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
->then(function () use ($connection, $user, $payload) {
|
->then(function () use ($connection, $user, $payload) {
|
||||||
// The `pusher_internal:member_added` event is triggered when a user joins a channel.
|
// The `websocket_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
|
// It's quite possible that a user can have multiple connections to the same channel
|
||||||
// (for example by having multiple browser tabs open)
|
// (for example by having multiple browser tabs open)
|
||||||
// and in this case the events will only be triggered when the first tab is opened.
|
// and in this case the events will only be triggered when the first tab is opened.
|
||||||
|
|
@ -69,7 +66,7 @@ class PresenceChannel extends PrivateChannel
|
||||||
->then(function ($sockets) use ($payload, $connection, $user) {
|
->then(function ($sockets) use ($payload, $connection, $user) {
|
||||||
if (count($sockets) === 1) {
|
if (count($sockets) === 1) {
|
||||||
$memberAddedPayload = [
|
$memberAddedPayload = [
|
||||||
'event' => 'pusher_internal:member_added',
|
'event' => 'websocket_internal.member_added',
|
||||||
'channel' => $this->getName(),
|
'channel' => $this->getName(),
|
||||||
'data' => $payload->channel_data,
|
'data' => $payload->channel_data,
|
||||||
];
|
];
|
||||||
|
|
@ -78,20 +75,7 @@ class PresenceChannel extends PrivateChannel
|
||||||
(object) $memberAddedPayload, $connection->socketId,
|
(object) $memberAddedPayload, $connection->socketId,
|
||||||
$connection->app->id
|
$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,
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -121,7 +105,7 @@ class PresenceChannel extends PrivateChannel
|
||||||
return $this->channelManager
|
return $this->channelManager
|
||||||
->userLeftPresenceChannel($connection, $user, $this->getName())
|
->userLeftPresenceChannel($connection, $user, $this->getName())
|
||||||
->then(function () use ($connection, $user) {
|
->then(function () use ($connection, $user) {
|
||||||
// The `pusher_internal:member_removed` is triggered when a user leaves a channel.
|
// The `websocket_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
|
// It's quite possible that a user can have multiple connections to the same channel
|
||||||
// (for example by having multiple browser tabs open)
|
// (for example by having multiple browser tabs open)
|
||||||
// and in this case the events will only be triggered when the last one is closed.
|
// and in this case the events will only be triggered when the last one is closed.
|
||||||
|
|
@ -130,7 +114,7 @@ class PresenceChannel extends PrivateChannel
|
||||||
->then(function ($sockets) use ($connection, $user) {
|
->then(function ($sockets) use ($connection, $user) {
|
||||||
if (count($sockets) === 0) {
|
if (count($sockets) === 0) {
|
||||||
$memberRemovedPayload = [
|
$memberRemovedPayload = [
|
||||||
'event' => 'pusher_internal:member_removed',
|
'event' => 'websocket_internal.member_removed',
|
||||||
'channel' => $this->getName(),
|
'channel' => $this->getName(),
|
||||||
'data' => json_encode([
|
'data' => json_encode([
|
||||||
'user_id' => $user->user_id,
|
'user_id' => $user->user_id,
|
||||||
|
|
@ -141,13 +125,6 @@ class PresenceChannel extends PrivateChannel
|
||||||
(object) $memberRemovedPayload, $connection->socketId,
|
(object) $memberRemovedPayload, $connection->socketId,
|
||||||
$connection->app->id
|
$connection->app->id
|
||||||
);
|
);
|
||||||
|
|
||||||
UnsubscribedFromChannel::dispatch(
|
|
||||||
$connection->app->id,
|
|
||||||
$connection->socketId,
|
|
||||||
$this->getName(),
|
|
||||||
$user
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\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,44 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Console\Commands;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Facades\StatisticsStore;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
|
|
||||||
class CleanStatistics extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'websockets:clean
|
|
||||||
{appId? : (optional) The app id that will be cleaned.}
|
|
||||||
{--days= : Delete records older than this amount of days since now.}
|
|
||||||
';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string|null
|
|
||||||
*/
|
|
||||||
protected $description = 'Clean up old statistics from the WebSocket statistics storage.';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run the command.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
$this->comment('Cleaning WebSocket Statistics...');
|
|
||||||
|
|
||||||
$days = $this->option('days') ?: config('statistics.delete_statistics_older_than_days');
|
|
||||||
|
|
||||||
$amountDeleted = StatisticsStore::delete(
|
|
||||||
now()->subDays($days), $this->argument('appId')
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->info("Deleted {$amountDeleted} record(s) from the WebSocket statistics storage.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Console\Commands;
|
|
||||||
|
|
||||||
use BlaxSoftware\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!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,6 @@ namespace BlaxSoftware\LaravelWebSockets\Console\Commands;
|
||||||
use BlaxSoftware\LaravelWebSockets\Broadcast\BroadcastSocketServer;
|
use BlaxSoftware\LaravelWebSockets\Broadcast\BroadcastSocketServer;
|
||||||
use BlaxSoftware\LaravelWebSockets\Cache\IpcCache;
|
use BlaxSoftware\LaravelWebSockets\Cache\IpcCache;
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
use BlaxSoftware\LaravelWebSockets\Facades\StatisticsCollector as StatisticsCollectorFacade;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Facades\WebSocketRouter;
|
use BlaxSoftware\LaravelWebSockets\Facades\WebSocketRouter;
|
||||||
use BlaxSoftware\LaravelWebSockets\Ipc\SocketPairIpc;
|
use BlaxSoftware\LaravelWebSockets\Ipc\SocketPairIpc;
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\Loggers\ConnectionLogger;
|
use BlaxSoftware\LaravelWebSockets\Server\Loggers\ConnectionLogger;
|
||||||
|
|
@ -243,22 +242,9 @@ class StartServer extends Command
|
||||||
*/
|
*/
|
||||||
protected function configureStatistics()
|
protected function configureStatistics()
|
||||||
{
|
{
|
||||||
if (! $this->option('disable-statistics')) {
|
// Statistics collection has been removed.
|
||||||
$intervalInSeconds = $this->option('statistics-interval') ?: config('websockets.statistics.interval_in_seconds', 3600);
|
// A new CLI monitoring system will replace it.
|
||||||
|
\Log::channel('websocket')->debug('Statistics disabled (removed)');
|
||||||
\Log::channel('websocket')->debug('Statistics enabled', [
|
|
||||||
'interval_seconds' => $intervalInSeconds,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->loop->addPeriodicTimer($intervalInSeconds, function () {
|
|
||||||
\Log::channel('websocket')->debug('Statistics timer tick, saving...');
|
|
||||||
$this->line('Saving statistics...', null, OutputInterface::VERBOSITY_VERBOSE);
|
|
||||||
|
|
||||||
StatisticsCollectorFacade::save();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
\Log::channel('websocket')->debug('Statistics disabled');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\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[\BlaxSoftware\LaravelWebSockets\Statistics\Statistic|null]
|
|
||||||
*/
|
|
||||||
public function getAppStatistics($appId): PromiseInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all app traces from the database if no connections have been set
|
|
||||||
* in the meanwhile since last save.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function resetAppTraces($appId);
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Apps\App;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Concerns\PushesToPusher;
|
|
||||||
use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use React\EventLoop\LoopInterface;
|
|
||||||
|
|
||||||
use function Clue\React\Block\await;
|
|
||||||
|
|
||||||
class AuthenticateDashboard
|
|
||||||
{
|
|
||||||
use PushesToPusher;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the app by using the header
|
|
||||||
* and then reconstruct the PusherBroadcaster
|
|
||||||
* using our own app selection.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @return \Illuminate\Http\Response
|
|
||||||
*/
|
|
||||||
public function __invoke(Request $request)
|
|
||||||
{
|
|
||||||
$app = await(App::findById($request->header('X-App-Id')), app(LoopInterface::class));
|
|
||||||
|
|
||||||
$broadcaster = $this->getPusherBroadcaster([
|
|
||||||
'key' => $app->key,
|
|
||||||
'secret' => $app->secret,
|
|
||||||
'id' => $app->id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Since the dashboard itself is already secured by the
|
|
||||||
* Authorize middleware, we can trust all channel
|
|
||||||
* authentication requests in here.
|
|
||||||
*/
|
|
||||||
return $broadcaster->validAuthenticationResponse($request, []);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Concerns\PushesToPusher;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Rules\AppId;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
class SendMessage
|
|
||||||
{
|
|
||||||
use PushesToPusher;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send the message to the requested channel.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @return \Illuminate\Http\Response
|
|
||||||
*/
|
|
||||||
public function __invoke(Request $request)
|
|
||||||
{
|
|
||||||
$request->validate([
|
|
||||||
'appId' => ['required', new AppId],
|
|
||||||
'key' => 'required|string',
|
|
||||||
'secret' => 'required|string',
|
|
||||||
'event' => 'required|string',
|
|
||||||
'channel' => 'required|string',
|
|
||||||
'data' => 'required|json',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$broadcaster = $this->getPusherBroadcaster([
|
|
||||||
'key' => $request->key,
|
|
||||||
'secret' => $request->secret,
|
|
||||||
'id' => $request->appId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$decodedData = json_decode($request->data, true);
|
|
||||||
|
|
||||||
$broadcaster->broadcast(
|
|
||||||
[$request->channel],
|
|
||||||
$request->event,
|
|
||||||
$decodedData ?: []
|
|
||||||
);
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
return response()->json([
|
|
||||||
'ok' => false,
|
|
||||||
'exception' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'ok' => true,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\AppManager;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use React\EventLoop\LoopInterface;
|
|
||||||
|
|
||||||
use function Clue\React\Block\await;
|
|
||||||
|
|
||||||
class ShowApps
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Show the configured apps.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param \BlaxSoftware\LaravelWebSockets\Contracts\AppManager $apps
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __invoke(Request $request, AppManager $apps)
|
|
||||||
{
|
|
||||||
return view('websockets::apps', [
|
|
||||||
'apps' => await($apps->all(), app(LoopInterface::class), 2.0),
|
|
||||||
'port' => config('websockets.dashboard.port', 6001),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\AppManager;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\DashboardLogger;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use React\EventLoop\LoopInterface;
|
|
||||||
|
|
||||||
use function Clue\React\Block\await;
|
|
||||||
|
|
||||||
class ShowDashboard
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Show the dashboard.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param \BlaxSoftware\LaravelWebSockets\Contracts\AppManager $apps
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __invoke(Request $request, AppManager $apps)
|
|
||||||
{
|
|
||||||
return view('websockets::dashboard', [
|
|
||||||
'apps' => await($apps->all(), app(LoopInterface::class), 2.0),
|
|
||||||
'port' => config('websockets.dashboard.port', 6001),
|
|
||||||
'channels' => DashboardLogger::$channels,
|
|
||||||
'logPrefix' => DashboardLogger::LOG_CHANNEL_PREFIX,
|
|
||||||
'refreshInterval' => config('websockets.statistics.interval_in_seconds'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers;
|
|
||||||
|
|
||||||
use BlaxSoftware\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,37 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\AppManager;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Dashboard\Http\Requests\StoreAppRequest;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use React\EventLoop\LoopInterface;
|
|
||||||
|
|
||||||
use function Clue\React\Block\await;
|
|
||||||
|
|
||||||
class StoreApp
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Show the configured apps.
|
|
||||||
*
|
|
||||||
* @param StoreAppRequest $request
|
|
||||||
* @param \BlaxSoftware\LaravelWebSockets\Contracts\AppManager $apps
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __invoke(StoreAppRequest $request, AppManager $apps)
|
|
||||||
{
|
|
||||||
$appData = [
|
|
||||||
'id' => (string) Str::uuid(),
|
|
||||||
'key' => (string) Str::uuid(),
|
|
||||||
'secret' => (string) Str::uuid(),
|
|
||||||
'name' => $request->get('name'),
|
|
||||||
'enable_client_messages' => $request->has('enable_client_messages'),
|
|
||||||
'enable_statistics' => $request->has('enable_statistics'),
|
|
||||||
'allowed_origins' => $request->get('allowed_origins'),
|
|
||||||
];
|
|
||||||
|
|
||||||
await($apps->createApp($appData), app(LoopInterface::class));
|
|
||||||
|
|
||||||
return redirect()->route('laravel-websockets.apps');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Dashboard\Http\Middleware;
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Gate;
|
|
||||||
|
|
||||||
class Authorize
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Authorize the current user.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param \Closure $next
|
|
||||||
* @return \Illuminate\Http\Response
|
|
||||||
*/
|
|
||||||
public function handle($request, $next)
|
|
||||||
{
|
|
||||||
return Gate::check('viewWebSocketsDashboard', [$request->user()])
|
|
||||||
? $next($request)
|
|
||||||
: abort(403);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Dashboard\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class StoreAppRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authenticate()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function rules()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'name' => 'required',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
|
||||||
|
|
||||||
class DashboardLogger
|
|
||||||
{
|
|
||||||
const LOG_CHANNEL_PREFIX = 'private-websockets-dashboard-';
|
|
||||||
|
|
||||||
const TYPE_DISCONNECTED = 'disconnected';
|
|
||||||
const TYPE_CONNECTED = 'connected';
|
|
||||||
const TYPE_SUBSCRIBED = 'subscribed';
|
|
||||||
const TYPE_WS_MESSAGE = 'ws-message';
|
|
||||||
const TYPE_API_MESSAGE = 'api-message';
|
|
||||||
const TYPE_REPLICATOR_SUBSCRIBED = 'replicator-subscribed';
|
|
||||||
const TYPE_REPLICATOR_UNSUBSCRIBED = 'replicator-unsubscribed';
|
|
||||||
const TYPE_REPLICATOR_MESSAGE_RECEIVED = 'replicator-message-received';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of all channels.
|
|
||||||
*/
|
|
||||||
public static array $channels = [
|
|
||||||
self::TYPE_DISCONNECTED,
|
|
||||||
self::TYPE_CONNECTED,
|
|
||||||
self::TYPE_SUBSCRIBED,
|
|
||||||
self::TYPE_WS_MESSAGE,
|
|
||||||
self::TYPE_API_MESSAGE,
|
|
||||||
self::TYPE_REPLICATOR_SUBSCRIBED,
|
|
||||||
self::TYPE_REPLICATOR_UNSUBSCRIBED,
|
|
||||||
self::TYPE_REPLICATOR_MESSAGE_RECEIVED,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether dashboard logging is enabled.
|
|
||||||
* Cached to avoid repeated config lookups.
|
|
||||||
*/
|
|
||||||
private static ?bool $enabled = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cached channel manager instance.
|
|
||||||
*/
|
|
||||||
private static ?ChannelManager $channelManager = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log an event for an app.
|
|
||||||
* Optimized: Early exit if disabled, cached config lookups.
|
|
||||||
*
|
|
||||||
* @param mixed $appId
|
|
||||||
* @param string $type
|
|
||||||
* @param array $details
|
|
||||||
*/
|
|
||||||
public static function log($appId, string $type, array $details = []): void
|
|
||||||
{
|
|
||||||
// Cache enabled check
|
|
||||||
if (self::$enabled === null) {
|
|
||||||
self::$enabled = config('websockets.dashboard.enabled', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if dashboard is disabled
|
|
||||||
if (!self::$enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache channel manager
|
|
||||||
if (self::$channelManager === null) {
|
|
||||||
self::$channelManager = app(ChannelManager::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
$channelName = static::LOG_CHANNEL_PREFIX . $type;
|
|
||||||
|
|
||||||
// Build payload - use date() instead of deprecated strftime()
|
|
||||||
$payload = (object) [
|
|
||||||
'event' => 'log-message',
|
|
||||||
'channel' => $channelName,
|
|
||||||
'data' => [
|
|
||||||
'type' => $type,
|
|
||||||
'time' => date('H:i:s'),
|
|
||||||
'details' => $details,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Check if channel exists locally and broadcast
|
|
||||||
$channel = self::$channelManager->find($appId, $channelName);
|
|
||||||
if ($channel) {
|
|
||||||
$channel->broadcastLocally($appId, $payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always broadcast across servers (preserving original behavior)
|
|
||||||
// The channel manager handles the replication logic
|
|
||||||
self::$channelManager->broadcastAcrossServers(
|
|
||||||
$appId,
|
|
||||||
null,
|
|
||||||
$channelName,
|
|
||||||
$payload
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset cached state (useful for testing)
|
|
||||||
*/
|
|
||||||
public static function reset(): void
|
|
||||||
{
|
|
||||||
self::$enabled = null;
|
|
||||||
self::$channelManager = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\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,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Facades;
|
|
||||||
|
|
||||||
use BlaxSoftware\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,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Facades;
|
|
||||||
|
|
||||||
use BlaxSoftware\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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class WebSocketsStatisticsEntry extends Model
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
protected $table = 'websockets_statistics_entries';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
protected $guarded = [];
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Queue;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
|
||||||
use Illuminate\Queue\RedisQueue;
|
|
||||||
|
|
||||||
class AsyncRedisQueue extends RedisQueue
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Get the connection for the queue.
|
|
||||||
*
|
|
||||||
* @return \BlaxSoftware\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,36 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Rules;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\AppManager;
|
|
||||||
use Illuminate\Contracts\Validation\Rule;
|
|
||||||
use React\EventLoop\Factory;
|
|
||||||
|
|
||||||
use function Clue\React\Block\await;
|
|
||||||
|
|
||||||
class AppId implements Rule
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Create a new rule.
|
|
||||||
*
|
|
||||||
* @param mixed $attribute
|
|
||||||
* @param mixed $value
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function passes($attribute, $value)
|
|
||||||
{
|
|
||||||
$manager = app(AppManager::class);
|
|
||||||
|
|
||||||
return await($manager->findById($value), Factory::create()) ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The validation message.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function message()
|
|
||||||
{
|
|
||||||
return 'There is no app registered with the given id. Make sure the websockets config file contains an app for this id or that your custom AppManager returns an app for this id.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,14 +7,14 @@ use Exception;
|
||||||
class WebSocketException extends Exception
|
class WebSocketException extends Exception
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the payload, Pusher-like formatted.
|
* Get the payload.
|
||||||
*
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function getPayload()
|
public function getPayload()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'event' => 'pusher.error',
|
'event' => 'websocket.error',
|
||||||
'data' => [
|
'data' => [
|
||||||
'message' => $this->getMessage(),
|
'message' => $this->getMessage(),
|
||||||
'code' => $this->getCode(),
|
'code' => $this->getCode(),
|
||||||
|
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Server\Messages;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Events\ConnectionPonged;
|
|
||||||
use Ratchet\ConnectionInterface;
|
|
||||||
use stdClass;
|
|
||||||
|
|
||||||
class PusherChannelProtocolMessage extends PusherClientMessage
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Pre-encoded pong response for performance
|
|
||||||
*/
|
|
||||||
private const PONG_RESPONSE = '{"event":"pusher.pong"}';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Respond with the payload.
|
|
||||||
* Optimized: Uses direct method dispatch instead of reflection.
|
|
||||||
*/
|
|
||||||
public function respond(): void
|
|
||||||
{
|
|
||||||
$event = $this->payload->event ?? '';
|
|
||||||
|
|
||||||
// Fast path for ping - most common pusher protocol message
|
|
||||||
if ($event === 'pusher:ping' || $event === 'pusher.ping') {
|
|
||||||
$this->pingFast($this->connection);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract method name from event (e.g., 'pusher:subscribe' -> 'subscribe')
|
|
||||||
$colonPos = strpos($event, ':');
|
|
||||||
if ($colonPos !== false) {
|
|
||||||
$eventName = substr($event, $colonPos + 1);
|
|
||||||
} else {
|
|
||||||
$dotPos = strpos($event, '.');
|
|
||||||
$eventName = $dotPos !== false ? substr($event, $dotPos + 1) : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to camelCase if needed (e.g., 'channel-name' -> 'channelName')
|
|
||||||
if (strpos($eventName, '-') !== false) {
|
|
||||||
$eventName = lcfirst(str_replace('-', '', ucwords($eventName, '-')));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($eventName && $eventName !== 'respond' && method_exists($this, $eventName)) {
|
|
||||||
$this->$eventName($this->connection, $this->payload->data ?? new stdClass());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fast ping handler - avoids promise chain and event dispatch
|
|
||||||
*/
|
|
||||||
protected function pingFast(ConnectionInterface $connection): void
|
|
||||||
{
|
|
||||||
// Update timestamp directly on connection (no promise chain)
|
|
||||||
$connection->lastPongedAt = time();
|
|
||||||
|
|
||||||
// Send pre-encoded response (no json_encode overhead)
|
|
||||||
$connection->send(self::PONG_RESPONSE);
|
|
||||||
|
|
||||||
// Skip event dispatch for ping - it's high frequency and events are expensive
|
|
||||||
// If you need ping events, use: ConnectionPonged::dispatch($connection->app->id, $connection->socketId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy ping handler - kept for compatibility
|
|
||||||
* @deprecated Use pingFast instead
|
|
||||||
*/
|
|
||||||
protected function ping(ConnectionInterface $connection): void
|
|
||||||
{
|
|
||||||
$this->pingFast($connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to channel.
|
|
||||||
*
|
|
||||||
* @see https://pusher.com/docs/pusher_protocol#pusher-subscribe
|
|
||||||
*/
|
|
||||||
protected function subscribe(ConnectionInterface $connection, stdClass $payload): void
|
|
||||||
{
|
|
||||||
$this->channelManager->subscribeToChannel($connection, $payload->channel, $payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unsubscribe from the channel.
|
|
||||||
*/
|
|
||||||
public function unsubscribe(ConnectionInterface $connection, stdClass $payload): void
|
|
||||||
{
|
|
||||||
$this->channelManager->unsubscribeFromChannel($connection, $payload->channel, $payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Server\Messages;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\PusherMessage;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\DashboardLogger;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Ratchet\ConnectionInterface;
|
|
||||||
use stdClass;
|
|
||||||
|
|
||||||
class PusherClientMessage implements PusherMessage
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The payload to send.
|
|
||||||
*
|
|
||||||
* @var \stdClass
|
|
||||||
*/
|
|
||||||
protected $payload;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The socket connection.
|
|
||||||
*
|
|
||||||
* @var \Ratchet\ConnectionInterface
|
|
||||||
*/
|
|
||||||
protected $connection;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The channel manager.
|
|
||||||
*
|
|
||||||
* @var ChannelManager
|
|
||||||
*/
|
|
||||||
protected $channelManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new instance.
|
|
||||||
*
|
|
||||||
* @param \stdClass $payload
|
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
|
||||||
* @param ChannelManager $channelManager
|
|
||||||
*/
|
|
||||||
public function __construct(stdClass $payload, ConnectionInterface $connection, ChannelManager $channelManager)
|
|
||||||
{
|
|
||||||
$this->payload = $payload;
|
|
||||||
$this->connection = $connection;
|
|
||||||
$this->channelManager = $channelManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Respond to the message construction.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function respond()
|
|
||||||
{
|
|
||||||
if (! Str::startsWith($this->payload->event, 'client-')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $this->connection->app->clientMessagesEnabled) {
|
|
||||||
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, [
|
|
||||||
'socketId' => $this->connection->socketId,
|
|
||||||
'event' => $this->payload->event,
|
|
||||||
'channel' => $this->payload->channel,
|
|
||||||
'data' => $this->payload,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Server\Messages;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\PusherMessage;
|
|
||||||
use Ratchet\ConnectionInterface;
|
|
||||||
use Ratchet\RFC6455\Messaging\MessageInterface;
|
|
||||||
|
|
||||||
class PusherMessageFactory
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Create a new message.
|
|
||||||
* Optimized: Uses direct string comparison instead of Str::startsWith.
|
|
||||||
*
|
|
||||||
* @param \Ratchet\RFC6455\Messaging\MessageInterface $message
|
|
||||||
* @param \Ratchet\ConnectionInterface $connection
|
|
||||||
* @param \BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager $channelManager
|
|
||||||
* @return PusherMessage
|
|
||||||
*/
|
|
||||||
public static function createForMessage(
|
|
||||||
MessageInterface $message,
|
|
||||||
ConnectionInterface $connection,
|
|
||||||
ChannelManager $channelManager
|
|
||||||
): PusherMessage {
|
|
||||||
$payload = json_decode($message->getPayload());
|
|
||||||
$event = $payload->event ?? '';
|
|
||||||
|
|
||||||
// Fast string prefix check (faster than Str::startsWith)
|
|
||||||
// Check first 7 chars for 'pusher.' or 'pusher:'
|
|
||||||
$isPusherEvent = (
|
|
||||||
isset($event[6]) &&
|
|
||||||
$event[0] === 'p' &&
|
|
||||||
$event[1] === 'u' &&
|
|
||||||
$event[2] === 's' &&
|
|
||||||
$event[3] === 'h' &&
|
|
||||||
$event[4] === 'e' &&
|
|
||||||
$event[5] === 'r' &&
|
|
||||||
($event[6] === '.' || $event[6] === ':')
|
|
||||||
);
|
|
||||||
|
|
||||||
return $isPusherEvent
|
|
||||||
? new PusherChannelProtocolMessage($payload, $connection, $channelManager)
|
|
||||||
: new PusherClientMessage($payload, $connection, $channelManager);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -71,10 +71,6 @@ class Router
|
||||||
public function registerRoutes()
|
public function registerRoutes()
|
||||||
{
|
{
|
||||||
$this->get('/app/{appKey}', 'websockets.handler');
|
$this->get('/app/{appKey}', 'websockets.handler');
|
||||||
$this->post('/apps/{appId}/events', config('websockets.handlers.trigger_event'));
|
|
||||||
$this->get('/apps/{appId}/channels', config('websockets.handlers.fetch_channels'));
|
|
||||||
$this->get('/apps/{appId}/channels/{channelName}', config('websockets.handlers.fetch_channel'));
|
|
||||||
$this->get('/apps/{appId}/channels/{channelName}/users', config('websockets.handlers.fetch_users'));
|
|
||||||
$this->get('/health', config('websockets.handlers.health'));
|
$this->get('/health', config('websockets.handlers.health'));
|
||||||
|
|
||||||
$this->registerCustomRoutes();
|
$this->registerCustomRoutes();
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,12 @@ namespace BlaxSoftware\LaravelWebSockets\Server;
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Apps\App;
|
use BlaxSoftware\LaravelWebSockets\Apps\App;
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
use BlaxSoftware\LaravelWebSockets\DashboardLogger;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Events\ConnectionClosed;
|
use BlaxSoftware\LaravelWebSockets\Events\ConnectionClosed;
|
||||||
use BlaxSoftware\LaravelWebSockets\Events\NewConnection;
|
use BlaxSoftware\LaravelWebSockets\Events\NewConnection;
|
||||||
use BlaxSoftware\LaravelWebSockets\Events\WebSocketMessageReceived;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Facades\StatisticsCollector;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Helpers;
|
use BlaxSoftware\LaravelWebSockets\Helpers;
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\WebSocketException;
|
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\WebSocketException;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use Ratchet\RFC6455\Messaging\MessageInterface;
|
use Ratchet\RFC6455\Messaging\MessageInterface;
|
||||||
use Ratchet\WebSocket\MessageComponentInterface;
|
use Ratchet\WebSocket\MessageComponentInterface;
|
||||||
|
|
@ -62,19 +60,10 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
/** @var \GuzzleHttp\Psr7\Request $request */
|
/** @var \GuzzleHttp\Psr7\Request $request */
|
||||||
$request = $connection->httpRequest;
|
$request = $connection->httpRequest;
|
||||||
|
|
||||||
if ($connection->app->statisticsEnabled) {
|
|
||||||
StatisticsCollector::connection($connection->app->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->channelManager->subscribeToApp($connection->app->id);
|
$this->channelManager->subscribeToApp($connection->app->id);
|
||||||
|
|
||||||
$this->channelManager->connectionPonged($connection);
|
$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);
|
NewConnection::dispatch($connection->app->id, $connection->socketId);
|
||||||
}
|
}
|
||||||
} catch (WebSocketException $exception) {
|
} catch (WebSocketException $exception) {
|
||||||
|
|
@ -98,19 +87,49 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Messages\PusherMessageFactory::createForMessage(
|
$payload = json_decode($message->getPayload());
|
||||||
$message, $connection, $this->channelManager
|
|
||||||
)->respond();
|
|
||||||
|
|
||||||
if ($connection->app->statisticsEnabled) {
|
if (! isset($payload->event)) {
|
||||||
StatisticsCollector::webSocketMessage($connection->app->id);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
WebSocketMessageReceived::dispatch(
|
$event = $payload->event;
|
||||||
$connection->app->id,
|
|
||||||
$connection->socketId,
|
if ($this->isProtocolAction($event, 'ping')) {
|
||||||
$message
|
$connection->send(json_encode(['event' => 'websocket.pong']));
|
||||||
);
|
$this->channelManager->connectionPonged($connection);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isProtocolAction($event, 'subscribe')) {
|
||||||
|
$channel = $payload->data->channel ?? null;
|
||||||
|
if ($channel) {
|
||||||
|
$this->channelManager->subscribeToChannel($connection, $channel, $payload->data ?? new \stdClass);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isProtocolAction($event, 'unsubscribe')) {
|
||||||
|
$channel = $payload->data->channel ?? null;
|
||||||
|
if ($channel) {
|
||||||
|
$this->channelManager->unsubscribeFromChannel($connection, $channel, $payload->data ?? new \stdClass);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client events (whisper) — must start with "client-"
|
||||||
|
if (Str::startsWith($event, 'client-')) {
|
||||||
|
$channel = $payload->channel ?? ($payload->data->channel ?? null);
|
||||||
|
if ($channel) {
|
||||||
|
$ch = $this->channelManager->find($connection->app->id, $channel);
|
||||||
|
if ($ch) {
|
||||||
|
$ch->broadcastToEveryoneExcept(
|
||||||
|
$payload, $connection->socketId, $connection->app->id, false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -125,10 +144,6 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
->unsubscribeFromAllChannels($connection)
|
->unsubscribeFromAllChannels($connection)
|
||||||
->then(function (bool $unsubscribed) use ($connection) {
|
->then(function (bool $unsubscribed) use ($connection) {
|
||||||
if (isset($connection->app)) {
|
if (isset($connection->app)) {
|
||||||
if ($connection->app->statisticsEnabled) {
|
|
||||||
StatisticsCollector::disconnection($connection->app->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->channelManager->unsubscribeFromApp($connection->app->id);
|
return $this->channelManager->unsubscribeFromApp($connection->app->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,10 +151,6 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
})
|
})
|
||||||
->then(function () use ($connection) {
|
->then(function () use ($connection) {
|
||||||
if (isset($connection->app)) {
|
if (isset($connection->app)) {
|
||||||
DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_DISCONNECTED, [
|
|
||||||
'socketId' => $connection->socketId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
ConnectionClosed::dispatch($connection->app->id, $connection->socketId);
|
ConnectionClosed::dispatch($connection->app->id, $connection->socketId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -273,7 +284,7 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
protected function establishConnection(ConnectionInterface $connection)
|
protected function establishConnection(ConnectionInterface $connection)
|
||||||
{
|
{
|
||||||
$connection->send(json_encode([
|
$connection->send(json_encode([
|
||||||
'event' => 'pusher.connection_established',
|
'event' => 'websocket.connection_established',
|
||||||
'data' => json_encode([
|
'data' => json_encode([
|
||||||
'socket_id' => $connection->socketId,
|
'socket_id' => $connection->socketId,
|
||||||
'activity_timeout' => 30,
|
'activity_timeout' => 30,
|
||||||
|
|
@ -282,4 +293,17 @@ class WebSocketHandler implements MessageComponentInterface
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an event matches a protocol action (e.g., subscribe, ping).
|
||||||
|
* Matches both dot and colon delimiters for backward compatibility.
|
||||||
|
*
|
||||||
|
* @param string $event
|
||||||
|
* @param string $action
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function isProtocolAction(string $event, string $action): bool
|
||||||
|
{
|
||||||
|
return str_ends_with($event, '.' . $action) || str_ends_with($event, ':' . $action);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ class WebsocketService
|
||||||
|
|
||||||
// Subscribe (public channel)
|
// Subscribe (public channel)
|
||||||
$client->send(json_encode([
|
$client->send(json_encode([
|
||||||
'event' => 'pusher:subscribe',
|
'event' => 'websocket.subscribe',
|
||||||
'data' => ['channel' => 'websocket'],
|
'data' => ['channel' => 'websocket'],
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,191 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Statistics\Collectors;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\StatisticsCollector;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Facades\StatisticsStore;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Helpers;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Statistics\Statistic;
|
|
||||||
use React\Promise\PromiseInterface;
|
|
||||||
|
|
||||||
class MemoryCollector implements StatisticsCollector
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The list of stored statistics.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $statistics = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Channel manager.
|
|
||||||
*
|
|
||||||
* @var \BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager
|
|
||||||
*/
|
|
||||||
protected $channelManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the logger.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->channelManager = app(ChannelManager::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the incoming websocket message.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function webSocketMessage($appId)
|
|
||||||
{
|
|
||||||
$this->findOrMake($appId)
|
|
||||||
->webSocketMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the incoming API message.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function apiMessage($appId)
|
|
||||||
{
|
|
||||||
$this->findOrMake($appId)
|
|
||||||
->apiMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the new conection.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function connection($appId)
|
|
||||||
{
|
|
||||||
$this->findOrMake($appId)
|
|
||||||
->connection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle disconnections.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function disconnection($appId)
|
|
||||||
{
|
|
||||||
$this->findOrMake($appId)
|
|
||||||
->disconnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save all the stored statistics.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function save()
|
|
||||||
{
|
|
||||||
$this->getStatistics()->then(function ($statistics) {
|
|
||||||
foreach ($statistics as $appId => $statistic) {
|
|
||||||
$statistic->isEnabled()->then(function ($isEnabled) use ($appId, $statistic) {
|
|
||||||
if (! $isEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($statistic->shouldHaveTracesRemoved()) {
|
|
||||||
$this->resetAppTraces($appId);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->createRecord($statistic, $appId);
|
|
||||||
|
|
||||||
$this->channelManager
|
|
||||||
->getGlobalConnectionsCount($appId)
|
|
||||||
->then(function ($connections) use ($statistic) {
|
|
||||||
$statistic->reset(
|
|
||||||
is_null($connections) ? 0 : $connections
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flush the stored statistics.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function flush()
|
|
||||||
{
|
|
||||||
$this->statistics = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the saved statistics.
|
|
||||||
*
|
|
||||||
* @return PromiseInterface[array]
|
|
||||||
*/
|
|
||||||
public function getStatistics(): PromiseInterface
|
|
||||||
{
|
|
||||||
return Helpers::createFulfilledPromise($this->statistics);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the saved statistics for an app.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return PromiseInterface[\BlaxSoftware\LaravelWebSockets\Statistics\Statistic|null]
|
|
||||||
*/
|
|
||||||
public function getAppStatistics($appId): PromiseInterface
|
|
||||||
{
|
|
||||||
return Helpers::createFulfilledPromise(
|
|
||||||
$this->statistics[$appId] ?? null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all app traces from the database if no connections have been set
|
|
||||||
* in the meanwhile since last save.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function resetAppTraces($appId)
|
|
||||||
{
|
|
||||||
unset($this->statistics[$appId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find or create a defined statistic for an app.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return \BlaxSoftware\LaravelWebSockets\Statistics\Statistic
|
|
||||||
*/
|
|
||||||
protected function findOrMake($appId): Statistic
|
|
||||||
{
|
|
||||||
if (! isset($this->statistics[$appId])) {
|
|
||||||
$this->statistics[$appId] = Statistic::new($appId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->statistics[$appId];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new record using the Statistic Store.
|
|
||||||
*
|
|
||||||
* @param \BlaxSoftware\LaravelWebSockets\Statistics\Statistic $statistic
|
|
||||||
* @param mixed $appId
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function createRecord(Statistic $statistic, $appId)
|
|
||||||
{
|
|
||||||
StatisticsStore::store($statistic->toArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,374 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Statistics\Collectors;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Helpers;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Statistics\Statistic;
|
|
||||||
use Illuminate\Cache\RedisLock;
|
|
||||||
use Illuminate\Support\Facades\Redis;
|
|
||||||
use React\Promise\PromiseInterface;
|
|
||||||
|
|
||||||
class RedisCollector extends MemoryCollector
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The Redis manager instance.
|
|
||||||
*
|
|
||||||
* @var \Illuminate\Redis\RedisManager
|
|
||||||
*/
|
|
||||||
protected $redis;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The set name for the Redis storage.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected static $redisSetName = 'laravel-websockets:apps';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The lock name to use on Redis to avoid multiple
|
|
||||||
* collector-to-store actions that may result
|
|
||||||
* in multiple data points set to the store.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected static $redisLockName = 'laravel-websockets:collector:lock';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the logger.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
|
|
||||||
$this->redis = Redis::connection(
|
|
||||||
config('websockets.replication.modes.redis.connection', 'default')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the incoming websocket message.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function webSocketMessage($appId)
|
|
||||||
{
|
|
||||||
$this->ensureAppIsInSet($appId)
|
|
||||||
->hincrby($this->channelManager->getStatsRedisHash($appId, null), 'websocket_messages_count', 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the incoming API message.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function apiMessage($appId)
|
|
||||||
{
|
|
||||||
$this->ensureAppIsInSet($appId)
|
|
||||||
->hincrby($this->channelManager->getStatsRedisHash($appId, null), 'api_messages_count', 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the new conection.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function connection($appId)
|
|
||||||
{
|
|
||||||
// Increment the current connections count by 1.
|
|
||||||
$this->ensureAppIsInSet($appId)
|
|
||||||
->hincrby(
|
|
||||||
$this->channelManager->getStatsRedisHash($appId, null),
|
|
||||||
'current_connections_count', 1
|
|
||||||
)
|
|
||||||
->then(function ($currentConnectionsCount) use ($appId) {
|
|
||||||
// Get the peak connections count from Redis.
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->hget(
|
|
||||||
$this->channelManager->getStatsRedisHash($appId, null),
|
|
||||||
'peak_connections_count'
|
|
||||||
)
|
|
||||||
->then(function ($currentPeakConnectionCount) use ($currentConnectionsCount, $appId) {
|
|
||||||
// Extract the greatest number between the current peak connection count
|
|
||||||
// and the current connection number.
|
|
||||||
$peakConnectionsCount = is_null($currentPeakConnectionCount)
|
|
||||||
? $currentConnectionsCount
|
|
||||||
: max($currentPeakConnectionCount, $currentConnectionsCount);
|
|
||||||
|
|
||||||
// Then set it to the database.
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->hset(
|
|
||||||
$this->channelManager->getStatsRedisHash($appId, null),
|
|
||||||
'peak_connections_count', $peakConnectionsCount
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle disconnections.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function disconnection($appId)
|
|
||||||
{
|
|
||||||
// Decrement the current connections count by 1.
|
|
||||||
$this->ensureAppIsInSet($appId)
|
|
||||||
->hincrby($this->channelManager->getStatsRedisHash($appId, null), 'current_connections_count', -1)
|
|
||||||
->then(function ($currentConnectionsCount) use ($appId) {
|
|
||||||
// Get the peak connections count from Redis.
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->hget($this->channelManager->getStatsRedisHash($appId, null), 'peak_connections_count')
|
|
||||||
->then(function ($currentPeakConnectionCount) use ($currentConnectionsCount, $appId) {
|
|
||||||
// Extract the greatest number between the current peak connection count
|
|
||||||
// and the current connection number.
|
|
||||||
$peakConnectionsCount = is_null($currentPeakConnectionCount)
|
|
||||||
? $currentConnectionsCount
|
|
||||||
: max($currentPeakConnectionCount, $currentConnectionsCount);
|
|
||||||
|
|
||||||
// Then set it to the database.
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->hset(
|
|
||||||
$this->channelManager->getStatsRedisHash($appId, null),
|
|
||||||
'peak_connections_count', $peakConnectionsCount
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save all the stored statistics.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function save()
|
|
||||||
{
|
|
||||||
$this->lock()->get(function () {
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->smembers(static::$redisSetName)
|
|
||||||
->then(function ($members) {
|
|
||||||
foreach ($members as $appId) {
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->hgetall($this->channelManager->getStatsRedisHash($appId, null))
|
|
||||||
->then(function ($list) use ($appId) {
|
|
||||||
if (! $list) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$statistic = $this->arrayToStatisticInstance(
|
|
||||||
$appId, Helpers::redisListToArray($list)
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($statistic->shouldHaveTracesRemoved()) {
|
|
||||||
return $this->resetAppTraces($appId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->createRecord($statistic, $appId);
|
|
||||||
|
|
||||||
$this->channelManager
|
|
||||||
->getGlobalConnectionsCount($appId)
|
|
||||||
->then(function ($currentConnectionsCount) use ($appId) {
|
|
||||||
$currentConnectionsCount === 0 || is_null($currentConnectionsCount)
|
|
||||||
? $this->resetAppTraces($appId)
|
|
||||||
: $this->resetStatistics($appId, $currentConnectionsCount);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flush the stored statistics.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function flush()
|
|
||||||
{
|
|
||||||
$this->getStatistics()->then(function ($statistics) {
|
|
||||||
foreach ($statistics as $appId => $statistic) {
|
|
||||||
$this->resetAppTraces($appId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the saved statistics.
|
|
||||||
*
|
|
||||||
* @return PromiseInterface[array]
|
|
||||||
*/
|
|
||||||
public function getStatistics(): PromiseInterface
|
|
||||||
{
|
|
||||||
return $this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->smembers(static::$redisSetName)
|
|
||||||
->then(function ($members) {
|
|
||||||
$appsWithStatistics = [];
|
|
||||||
|
|
||||||
foreach ($members as $appId) {
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->hgetall($this->channelManager->getStatsRedisHash($appId, null))
|
|
||||||
->then(function ($list) use ($appId, &$appsWithStatistics) {
|
|
||||||
$appsWithStatistics[$appId] = $this->arrayToStatisticInstance(
|
|
||||||
$appId, Helpers::redisListToArray($list)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return $appsWithStatistics;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the saved statistics for an app.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return PromiseInterface[\BlaxSoftware\LaravelWebSockets\Statistics\Statistic|null]
|
|
||||||
*/
|
|
||||||
public function getAppStatistics($appId): PromiseInterface
|
|
||||||
{
|
|
||||||
return $this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->hgetall($this->channelManager->getStatsRedisHash($appId, null))
|
|
||||||
->then(function ($list) use ($appId) {
|
|
||||||
return $this->arrayToStatisticInstance(
|
|
||||||
$appId, Helpers::redisListToArray($list)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset the statistics to a specific connection count.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @param int $currentConnectionCount
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function resetStatistics($appId, int $currentConnectionCount)
|
|
||||||
{
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->hset(
|
|
||||||
$this->channelManager->getStatsRedisHash($appId, null),
|
|
||||||
'current_connections_count', $currentConnectionCount
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->hset(
|
|
||||||
$this->channelManager->getStatsRedisHash($appId, null),
|
|
||||||
'peak_connections_count', max(0, $currentConnectionCount)
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->hset(
|
|
||||||
$this->channelManager->getStatsRedisHash($appId, null),
|
|
||||||
'websocket_messages_count', 0
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->hset(
|
|
||||||
$this->channelManager->getStatsRedisHash($appId, null),
|
|
||||||
'api_messages_count', 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all app traces from the database if no connections have been set
|
|
||||||
* in the meanwhile since last save.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function resetAppTraces($appId)
|
|
||||||
{
|
|
||||||
parent::resetAppTraces($appId);
|
|
||||||
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->hdel(
|
|
||||||
$this->channelManager->getStatsRedisHash($appId, null),
|
|
||||||
'current_connections_count'
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->hdel(
|
|
||||||
$this->channelManager->getStatsRedisHash($appId, null),
|
|
||||||
'peak_connections_count'
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->hdel(
|
|
||||||
$this->channelManager->getStatsRedisHash($appId, null),
|
|
||||||
'websocket_messages_count'
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->hdel(
|
|
||||||
$this->channelManager->getStatsRedisHash($appId, null),
|
|
||||||
'api_messages_count'
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->srem(static::$redisSetName, $appId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure the app id is stored in the Redis database.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return \Clue\React\Redis\Client
|
|
||||||
*/
|
|
||||||
protected function ensureAppIsInSet($appId)
|
|
||||||
{
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->sadd(static::$redisSetName, $appId);
|
|
||||||
|
|
||||||
return $this->channelManager->getPublishClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a new RedisLock instance to avoid race conditions.
|
|
||||||
*
|
|
||||||
* @return \Illuminate\Cache\CacheLock
|
|
||||||
*/
|
|
||||||
protected function lock()
|
|
||||||
{
|
|
||||||
return new RedisLock($this->redis, static::$redisLockName, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform a key-value pair to a Statistic instance.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @param array $stats
|
|
||||||
* @return \BlaxSoftware\LaravelWebSockets\Statistics\Statistic
|
|
||||||
*/
|
|
||||||
protected function arrayToStatisticInstance($appId, array $stats)
|
|
||||||
{
|
|
||||||
return Statistic::new($appId)
|
|
||||||
->setCurrentConnectionsCount($stats['current_connections_count'] ?? 0)
|
|
||||||
->setPeakConnectionsCount($stats['peak_connections_count'] ?? 0)
|
|
||||||
->setWebSocketMessagesCount($stats['websocket_messages_count'] ?? 0)
|
|
||||||
->setApiMessagesCount($stats['api_messages_count'] ?? 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,220 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Statistics;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Apps\App;
|
|
||||||
use React\Promise\Deferred;
|
|
||||||
use React\Promise\PromiseInterface;
|
|
||||||
|
|
||||||
class Statistic
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The app id.
|
|
||||||
*
|
|
||||||
* @var mixed
|
|
||||||
*/
|
|
||||||
protected $appId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current connections count ticker.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $currentConnectionsCount = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The peak connections count ticker.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $peakConnectionsCount = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The websockets connections count ticker.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $webSocketMessagesCount = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The api messages connections count ticker.
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $apiMessagesCount = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new statistic.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct($appId)
|
|
||||||
{
|
|
||||||
$this->appId = $appId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new statistic instance.
|
|
||||||
*
|
|
||||||
* @param string|int $appId
|
|
||||||
* @return \BlaxSoftware\LaravelWebSockets\Statistics\Statistic
|
|
||||||
*/
|
|
||||||
public static function new($appId)
|
|
||||||
{
|
|
||||||
return new static($appId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the current connections count.
|
|
||||||
*
|
|
||||||
* @param int $currentConnectionsCount
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setCurrentConnectionsCount(int $currentConnectionsCount)
|
|
||||||
{
|
|
||||||
$this->currentConnectionsCount = $currentConnectionsCount;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the peak connections count.
|
|
||||||
*
|
|
||||||
* @param int $peakConnectionsCount
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setPeakConnectionsCount(int $peakConnectionsCount)
|
|
||||||
{
|
|
||||||
$this->peakConnectionsCount = $peakConnectionsCount;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the peak connections count.
|
|
||||||
*
|
|
||||||
* @param int $webSocketMessagesCount
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setWebSocketMessagesCount(int $webSocketMessagesCount)
|
|
||||||
{
|
|
||||||
$this->webSocketMessagesCount = $webSocketMessagesCount;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the peak connections count.
|
|
||||||
*
|
|
||||||
* @param int $apiMessagesCount
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setApiMessagesCount(int $apiMessagesCount)
|
|
||||||
{
|
|
||||||
$this->apiMessagesCount = $apiMessagesCount;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the app has statistics enabled.
|
|
||||||
*
|
|
||||||
* @return PromiseInterface
|
|
||||||
*/
|
|
||||||
public function isEnabled(): PromiseInterface
|
|
||||||
{
|
|
||||||
$deferred = new Deferred();
|
|
||||||
|
|
||||||
App::findById($this->appId)->then(function ($app) use ($deferred) {
|
|
||||||
$deferred->resolve($app->statisticsEnabled);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $deferred->promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a new connection increment.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function connection()
|
|
||||||
{
|
|
||||||
$this->currentConnectionsCount++;
|
|
||||||
|
|
||||||
$this->peakConnectionsCount = max($this->currentConnectionsCount, $this->peakConnectionsCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a disconnection decrement.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function disconnection()
|
|
||||||
{
|
|
||||||
$this->currentConnectionsCount--;
|
|
||||||
|
|
||||||
$this->peakConnectionsCount = max($this->currentConnectionsCount, $this->peakConnectionsCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a new websocket message.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function webSocketMessage()
|
|
||||||
{
|
|
||||||
$this->webSocketMessagesCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a new api message.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function apiMessage()
|
|
||||||
{
|
|
||||||
$this->apiMessagesCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset all the connections to a specific count.
|
|
||||||
*
|
|
||||||
* @param int $currentConnectionsCount
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function reset(int $currentConnectionsCount)
|
|
||||||
{
|
|
||||||
$this->currentConnectionsCount = $currentConnectionsCount;
|
|
||||||
$this->peakConnectionsCount = max(0, $currentConnectionsCount);
|
|
||||||
$this->webSocketMessagesCount = 0;
|
|
||||||
$this->apiMessagesCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the current statistic entry is empty. This means
|
|
||||||
* that the statistic entry can be easily deleted if no activity
|
|
||||||
* occured for a while.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function shouldHaveTracesRemoved(): bool
|
|
||||||
{
|
|
||||||
return $this->currentConnectionsCount === 0 && $this->peakConnectionsCount === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform the statistic to array.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function toArray()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'app_id' => $this->appId,
|
|
||||||
'peak_connections_count' => $this->peakConnectionsCount,
|
|
||||||
'websocket_messages_count' => $this->webSocketMessagesCount,
|
|
||||||
'api_messages_count' => $this->apiMessagesCount,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Statistics\Stores;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\StatisticsStore;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
|
|
||||||
class DatabaseStore implements StatisticsStore
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The model that will interact with the database.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
public static $model = \BlaxSoftware\LaravelWebSockets\Models\WebSocketsStatisticsEntry::class;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a new record in the database and return
|
|
||||||
* the created instance.
|
|
||||||
*
|
|
||||||
* @param array $data
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public static function store(array $data)
|
|
||||||
{
|
|
||||||
return static::$model::create($data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete records older than the given moment,
|
|
||||||
* for a specific app id (if given), returning
|
|
||||||
* the amount of deleted records.
|
|
||||||
*
|
|
||||||
* @param \Carbon\Carbon $moment
|
|
||||||
* @param string|int|null $appId
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public static function delete(Carbon $moment, $appId = null): int
|
|
||||||
{
|
|
||||||
return static::$model::where('created_at', '<', $moment->toDateTimeString())
|
|
||||||
->when(! is_null($appId), function ($query) use ($appId) {
|
|
||||||
return $query->whereAppId($appId);
|
|
||||||
})
|
|
||||||
->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the query result as eloquent collection.
|
|
||||||
*
|
|
||||||
* @param callable $processQuery
|
|
||||||
* @return \Illuminate\Support\Collection
|
|
||||||
*/
|
|
||||||
public function getRawRecords(callable $processQuery = null)
|
|
||||||
{
|
|
||||||
return static::$model::query()
|
|
||||||
->when(! is_null($processQuery), function ($query) use ($processQuery) {
|
|
||||||
return call_user_func($processQuery, $query);
|
|
||||||
}, function ($query) {
|
|
||||||
return $query->latest()->limit(120);
|
|
||||||
})->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the results for a specific query.
|
|
||||||
*
|
|
||||||
* @param callable $processQuery
|
|
||||||
* @param callable $processCollection
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getRecords(callable $processQuery = null, callable $processCollection = null): array
|
|
||||||
{
|
|
||||||
return $this->getRawRecords($processQuery)
|
|
||||||
->when(! is_null($processCollection), function ($collection) use ($processCollection) {
|
|
||||||
return call_user_func($processCollection, $collection);
|
|
||||||
})
|
|
||||||
->map(function (Model $statistic) {
|
|
||||||
return $this->statisticToArray($statistic);
|
|
||||||
})
|
|
||||||
->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
{
|
|
||||||
$statistics = collect(
|
|
||||||
$this->getRecords($processQuery, $processCollection)
|
|
||||||
);
|
|
||||||
|
|
||||||
return $this->statisticsToGraph($statistics);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turn the statistic model to an array.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Database\Eloquent\Model $statistic
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected function statisticToArray(Model $statistic): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'timestamp' => (string) $statistic->created_at,
|
|
||||||
'peak_connections_count' => $statistic->peak_connections_count,
|
|
||||||
'websocket_messages_count' => $statistic->websocket_messages_count,
|
|
||||||
'api_messages_count' => $statistic->api_messages_count,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turn the statistics collection to an array used for graph.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Support\Collection $statistics
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected function statisticsToGraph(Collection $statistics): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'peak_connections' => [
|
|
||||||
'x' => $statistics->pluck('timestamp')->toArray(),
|
|
||||||
'y' => $statistics->pluck('peak_connections_count')->toArray(),
|
|
||||||
],
|
|
||||||
'websocket_messages_count' => [
|
|
||||||
'x' => $statistics->pluck('timestamp')->toArray(),
|
|
||||||
'y' => $statistics->pluck('websocket_messages_count')->toArray(),
|
|
||||||
],
|
|
||||||
'api_messages_count' => [
|
|
||||||
'x' => $statistics->pluck('timestamp')->toArray(),
|
|
||||||
'y' => $statistics->pluck('api_messages_count')->toArray(),
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,37 +2,14 @@
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets;
|
namespace BlaxSoftware\LaravelWebSockets;
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\StatisticsCollector;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\StatisticsStore;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers\AuthenticateDashboard;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers\SendMessage;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers\ShowApps;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers\ShowDashboard;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers\ShowStatistics;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Dashboard\Http\Controllers\StoreApp;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Dashboard\Http\Middleware\Authorize as AuthorizeDashboard;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Queue\AsyncRedisConnector;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\Router;
|
use BlaxSoftware\LaravelWebSockets\Server\Router;
|
||||||
use Clue\React\SQLite\DatabaseInterface;
|
|
||||||
use Clue\React\SQLite\Factory as SQLiteFactory;
|
|
||||||
use Illuminate\Support\Facades\Gate;
|
|
||||||
use Illuminate\Support\Facades\Queue;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use React\EventLoop\Factory;
|
use React\EventLoop\Factory;
|
||||||
use React\EventLoop\LoopInterface;
|
use React\EventLoop\LoopInterface;
|
||||||
use React\MySQL\ConnectionInterface;
|
|
||||||
use React\MySQL\Factory as MySQLFactory;
|
|
||||||
use SplFileInfo;
|
|
||||||
use Symfony\Component\Finder\Finder;
|
|
||||||
|
|
||||||
class WebSocketsServiceProvider extends ServiceProvider
|
class WebSocketsServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Boot the service provider.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function boot()
|
public function boot()
|
||||||
{
|
{
|
||||||
$this->publishes([
|
$this->publishes([
|
||||||
|
|
@ -49,46 +26,19 @@ class WebSocketsServiceProvider extends ServiceProvider
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->registerDefaultWebsocketChannels();
|
$this->registerDefaultWebsocketChannels();
|
||||||
|
|
||||||
$this->registerEventLoop();
|
$this->registerEventLoop();
|
||||||
|
|
||||||
$this->registerSQLiteDatabase();
|
|
||||||
|
|
||||||
$this->registerMySqlDatabase();
|
|
||||||
|
|
||||||
$this->registerAsyncRedisQueueDriver();
|
|
||||||
|
|
||||||
$this->registerWebSocketHandler();
|
$this->registerWebSocketHandler();
|
||||||
|
|
||||||
$this->registerRouter();
|
$this->registerRouter();
|
||||||
|
|
||||||
$this->registerManagers();
|
$this->registerManagers();
|
||||||
|
|
||||||
$this->registerStatistics();
|
|
||||||
|
|
||||||
$this->registerDashboard();
|
|
||||||
|
|
||||||
$this->registerBroadcastAuthRoute();
|
$this->registerBroadcastAuthRoute();
|
||||||
|
|
||||||
$this->registerCommands();
|
$this->registerCommands();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the service provider.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function register()
|
public function register()
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers Broadcast::channel('websocket', fn () => true); in channels.php
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function registerDefaultWebsocketChannels()
|
protected function registerDefaultWebsocketChannels()
|
||||||
{
|
{
|
||||||
\Illuminate\Support\Facades\Broadcast::channel('websocket', fn() => true);
|
\Illuminate\Support\Facades\Broadcast::channel('websocket', fn() => true);
|
||||||
|
|
@ -102,117 +52,15 @@ class WebSocketsServiceProvider extends ServiceProvider
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the async, non-blocking Redis queue driver.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function registerAsyncRedisQueueDriver()
|
|
||||||
{
|
|
||||||
Queue::extend('async-redis', function () {
|
|
||||||
return new AsyncRedisConnector($this->app['redis']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function registerSQLiteDatabase()
|
|
||||||
{
|
|
||||||
$this->app->singleton(DatabaseInterface::class, function () {
|
|
||||||
$factory = new SQLiteFactory($this->app->make(LoopInterface::class));
|
|
||||||
|
|
||||||
$database = $factory->openLazy(
|
|
||||||
config('websockets.managers.sqlite.database', ':memory:')
|
|
||||||
);
|
|
||||||
|
|
||||||
$migrations = (new Finder())
|
|
||||||
->files()
|
|
||||||
->ignoreDotFiles(true)
|
|
||||||
->in(__DIR__ . '/../database/migrations/sqlite')
|
|
||||||
->name('*.sql');
|
|
||||||
|
|
||||||
/** @var SplFileInfo $migration */
|
|
||||||
foreach ($migrations as $migration) {
|
|
||||||
$database->exec($migration->getContents());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $database;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function registerMySqlDatabase()
|
|
||||||
{
|
|
||||||
$this->app->singleton(ConnectionInterface::class, function () {
|
|
||||||
$factory = new MySQLFactory($this->app->make(LoopInterface::class));
|
|
||||||
|
|
||||||
$connectionKey = 'database.connections.' . config('websockets.managers.mysql.connection');
|
|
||||||
|
|
||||||
$auth = trim(config($connectionKey . '.username') . ':' . config($connectionKey . '.password'), ':');
|
|
||||||
$connection = trim(config($connectionKey . '.host') . ':' . config($connectionKey . '.port'), ':');
|
|
||||||
$database = config($connectionKey . '.database');
|
|
||||||
|
|
||||||
$database = $factory->createLazyConnection(trim("{$auth}@{$connection}/{$database}", '@'));
|
|
||||||
|
|
||||||
return $database;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the statistics-related contracts.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function registerStatistics()
|
|
||||||
{
|
|
||||||
$this->app->singleton(StatisticsStore::class, function ($app) {
|
|
||||||
$config = $app['config']['websockets'];
|
|
||||||
$class = $config['statistics']['store'];
|
|
||||||
|
|
||||||
return new $class;
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->app->singleton(StatisticsCollector::class, function ($app) {
|
|
||||||
$config = $app['config']['websockets'];
|
|
||||||
$replicationMode = $config['replication']['mode'] ?? 'local';
|
|
||||||
|
|
||||||
$class = $config['replication']['modes'][$replicationMode]['collector'];
|
|
||||||
|
|
||||||
return new $class;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the dashboard components.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function registerDashboard()
|
|
||||||
{
|
|
||||||
$this->loadViewsFrom(__DIR__ . '/../resources/views/', 'websockets');
|
|
||||||
|
|
||||||
$this->registerDashboardRoutes();
|
|
||||||
$this->registerDashboardGate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the package commands.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function registerCommands()
|
protected function registerCommands()
|
||||||
{
|
{
|
||||||
$this->commands([
|
$this->commands([
|
||||||
Console\Commands\StartServer::class,
|
Console\Commands\StartServer::class,
|
||||||
Console\Commands\RestartServer::class,
|
Console\Commands\RestartServer::class,
|
||||||
Console\Commands\SteerServer::class,
|
Console\Commands\SteerServer::class,
|
||||||
Console\Commands\CleanStatistics::class,
|
|
||||||
Console\Commands\FlushCollectedStatistics::class,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the routing.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function registerRouter()
|
protected function registerRouter()
|
||||||
{
|
{
|
||||||
$this->app->singleton('websockets.router', function () {
|
$this->app->singleton('websockets.router', function () {
|
||||||
|
|
@ -220,11 +68,6 @@ class WebSocketsServiceProvider extends ServiceProvider
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the managers for the app.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function registerManagers()
|
protected function registerManagers()
|
||||||
{
|
{
|
||||||
$this->app->singleton(Contracts\AppManager::class, function ($app) {
|
$this->app->singleton(Contracts\AppManager::class, function ($app) {
|
||||||
|
|
@ -234,48 +77,8 @@ class WebSocketsServiceProvider extends ServiceProvider
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the dashboard routes.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function registerDashboardRoutes()
|
|
||||||
{
|
|
||||||
Route::group([
|
|
||||||
'domain' => config('websockets.dashboard.domain'),
|
|
||||||
'prefix' => config('websockets.dashboard.path'),
|
|
||||||
'as' => 'laravel-websockets.',
|
|
||||||
'middleware' => config('websockets.dashboard.middleware', [AuthorizeDashboard::class]),
|
|
||||||
], function () {
|
|
||||||
Route::get('/', ShowDashboard::class)->name('dashboard');
|
|
||||||
Route::get('/apps', ShowApps::class)->name('apps');
|
|
||||||
Route::post('/apps', StoreApp::class)->name('apps.store');
|
|
||||||
Route::get('/api/{appId}/statistics', ShowStatistics::class)->name('statistics');
|
|
||||||
Route::post('/auth', AuthenticateDashboard::class)->name('auth');
|
|
||||||
Route::post('/event', SendMessage::class)->name('event');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the dashboard gate.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function registerDashboardGate()
|
|
||||||
{
|
|
||||||
Gate::define('viewWebSocketsDashboard', function ($user = null) {
|
|
||||||
return $this->app->environment('local');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the default broadcasting routes if not already defined.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function registerBroadcastAuthRoute()
|
protected function registerBroadcastAuthRoute()
|
||||||
{
|
{
|
||||||
// If broadcasting/auth route is not defined, load the default routes
|
|
||||||
if (! Route::has('broadcasting/auth')) {
|
if (! Route::has('broadcasting/auth')) {
|
||||||
$this->loadRoutesFrom(__DIR__ . '/../routes/api.php');
|
$this->loadRoutesFrom(__DIR__ . '/../routes/api.php');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Websocket;
|
namespace BlaxSoftware\LaravelWebSockets\Websocket;
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\ChannelManagers\LocalChannelManager;
|
use BlaxSoftware\LaravelWebSockets\ChannelManagers\LocalChannelManager;
|
||||||
use BlaxSoftware\LaravelWebSockets\DashboardLogger;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Helpers;
|
use BlaxSoftware\LaravelWebSockets\Helpers;
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\MockableConnection;
|
use BlaxSoftware\LaravelWebSockets\Server\MockableConnection;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
@ -397,14 +396,6 @@ class ChannelManager extends LocalChannelManager
|
||||||
$socketId = $payload->socketId ?? null;
|
$socketId = $payload->socketId ?? null;
|
||||||
$serverId = $payload->serverId ?? null;
|
$serverId = $payload->serverId ?? null;
|
||||||
|
|
||||||
DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_MESSAGE_RECEIVED, [
|
|
||||||
'fromServerId' => $serverId,
|
|
||||||
'fromSocketId' => $socketId,
|
|
||||||
'receiverServerId' => $this->getServerId(),
|
|
||||||
'channel' => $channel,
|
|
||||||
'payload' => $payload,
|
|
||||||
]);
|
|
||||||
|
|
||||||
unset($payload->socketId, $payload->serverId, $payload->appId);
|
unset($payload->socketId, $payload->serverId, $payload->appId);
|
||||||
|
|
||||||
$channel->broadcastLocallyToEveryoneExcept($payload, $socketId, $appId);
|
$channel->broadcastLocallyToEveryoneExcept($payload, $socketId, $appId);
|
||||||
|
|
@ -604,11 +595,6 @@ class ChannelManager extends LocalChannelManager
|
||||||
{
|
{
|
||||||
$topic = $this->getRedisTopicName($appId, $channel);
|
$topic = $this->getRedisTopicName($appId, $channel);
|
||||||
|
|
||||||
DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_SUBSCRIBED, [
|
|
||||||
'serverId' => $this->getServerId(),
|
|
||||||
'pubsubTopic' => $topic,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $this->subscribeClient->subscribe($topic);
|
return $this->subscribeClient->subscribe($topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -621,11 +607,6 @@ class ChannelManager extends LocalChannelManager
|
||||||
{
|
{
|
||||||
$topic = $this->getRedisTopicName($appId, $channel);
|
$topic = $this->getRedisTopicName($appId, $channel);
|
||||||
|
|
||||||
DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_UNSUBSCRIBED, [
|
|
||||||
'serverId' => $this->getServerId(),
|
|
||||||
'pubsubTopic' => $topic,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $this->subscribeClient->unsubscribe($topic);
|
return $this->subscribeClient->unsubscribe($topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Websocket;
|
namespace BlaxSoftware\LaravelWebSockets\Websocket;
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\ChannelManagers\LocalChannelManager;
|
use BlaxSoftware\LaravelWebSockets\ChannelManagers\LocalChannelManager;
|
||||||
use BlaxSoftware\LaravelWebSockets\ChannelManagers\RedisChannelManager;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Channels\Channel;
|
use BlaxSoftware\LaravelWebSockets\Channels\Channel;
|
||||||
use BlaxSoftware\LaravelWebSockets\Channels\PresenceChannel;
|
use BlaxSoftware\LaravelWebSockets\Channels\PresenceChannel;
|
||||||
use BlaxSoftware\LaravelWebSockets\Channels\PrivateChannel;
|
use BlaxSoftware\LaravelWebSockets\Channels\PrivateChannel;
|
||||||
|
|
@ -22,7 +21,7 @@ class Controller
|
||||||
protected ConnectionInterface $connection,
|
protected ConnectionInterface $connection,
|
||||||
protected PrivateChannel|Channel|PresenceChannel|null $channel,
|
protected PrivateChannel|Channel|PresenceChannel|null $channel,
|
||||||
protected string $event,
|
protected string $event,
|
||||||
protected LocalChannelManager|RedisChannelManager $channelManager
|
protected LocalChannelManager $channelManager
|
||||||
) {
|
) {
|
||||||
$this->isMockConnection = $connection instanceof MockConnectionSocketPair;
|
$this->isMockConnection = $connection instanceof MockConnectionSocketPair;
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +56,7 @@ class Controller
|
||||||
ConnectionInterface $connection,
|
ConnectionInterface $connection,
|
||||||
PrivateChannel|Channel|PresenceChannel $channel,
|
PrivateChannel|Channel|PresenceChannel $channel,
|
||||||
array $message,
|
array $message,
|
||||||
LocalChannelManager|RedisChannelManager $channelManager
|
LocalChannelManager $channelManager
|
||||||
) {
|
) {
|
||||||
$event = self::get_event($message);
|
$event = self::get_event($message);
|
||||||
if (count($event) !== 2) {
|
if (count($event) !== 2) {
|
||||||
|
|
@ -378,16 +377,8 @@ class Controller
|
||||||
|
|
||||||
private static function get_event($message)
|
private static function get_event($message)
|
||||||
{
|
{
|
||||||
$event = explode('.', $message['event']);
|
// Split on '.' delimiter to get [controller, method, ...]
|
||||||
|
// e.g. "admin.dashboard[abc]" → ["admin", "dashboard[abc]"]
|
||||||
if (strpos($event[0], 'pusher.') > -1) {
|
return explode('.', $message['event']);
|
||||||
$event = explode('.', $event[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strpos($event[0], 'pusher:') > -1) {
|
|
||||||
$event = explode(':', $event[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $event;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class Handler implements MessageComponentInterface
|
||||||
* Pre-encoded static JSON responses for performance
|
* Pre-encoded static JSON responses for performance
|
||||||
* Encoding once at startup is faster than encoding every time
|
* Encoding once at startup is faster than encoding every time
|
||||||
*/
|
*/
|
||||||
private static string $PONG_RESPONSE = '{"event":"pusher.pong"}';
|
private static string $PONG_RESPONSE = '{"event":"websocket.pong"}';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GC collection counter - only collect every N pings
|
* GC collection counter - only collect every N pings
|
||||||
|
|
@ -141,8 +141,8 @@ class Handler implements MessageComponentInterface
|
||||||
|
|
||||||
$event = $data['event'] ?? '';
|
$event = $data['event'] ?? '';
|
||||||
|
|
||||||
// Direct string comparison (faster than strtolower + comparison)
|
// Match any prefix with . or : delimiter followed by 'ping'
|
||||||
if ($event !== 'pusher:ping' && $event !== 'pusher.ping') {
|
if (!self::isProtocolAction($event, 'ping')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,8 +210,8 @@ class Handler implements MessageComponentInterface
|
||||||
// Decode message (we already have payload string)
|
// Decode message (we already have payload string)
|
||||||
$messageArray = json_decode($payload, true, 512, JSON_THROW_ON_ERROR);
|
$messageArray = json_decode($payload, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
// Handle pusher protocol messages (subscribe, unsubscribe, etc.)
|
// Handle protocol messages (client-* broadcasts)
|
||||||
$this->handlePusherProtocolMessage($message, $connection, $messageArray);
|
$this->handleProtocolMessage($message, $connection, $messageArray);
|
||||||
|
|
||||||
$channel = $this->handleChannelSubscriptions($messageArray, $connection);
|
$channel = $this->handleChannelSubscriptions($messageArray, $connection);
|
||||||
|
|
||||||
|
|
@ -226,7 +226,7 @@ class Handler implements MessageComponentInterface
|
||||||
Log::channel('websocket')->debug('[' . $connection->socketId . ']@' . $channel->getName() . ' | ' . $payload);
|
Log::channel('websocket')->debug('[' . $connection->socketId . ']@' . $channel->getName() . ' | ' . $payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->handlePusherEvent($messageArray, $connection)) {
|
if ($this->handleProtocolEvent($messageArray, $connection)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,20 +237,14 @@ class Handler implements MessageComponentInterface
|
||||||
* Handle pusher protocol messages (formerly in PusherMessageFactory)
|
* Handle pusher protocol messages (formerly in PusherMessageFactory)
|
||||||
* Inlined for performance - avoids object creation
|
* Inlined for performance - avoids object creation
|
||||||
*/
|
*/
|
||||||
private function handlePusherProtocolMessage(
|
private function handleProtocolMessage(
|
||||||
MessageInterface $message,
|
MessageInterface $message,
|
||||||
ConnectionInterface $connection,
|
ConnectionInterface $connection,
|
||||||
array $messageArray
|
array $messageArray
|
||||||
): void {
|
): void {
|
||||||
$event = $messageArray['event'] ?? '';
|
$event = $messageArray['event'] ?? '';
|
||||||
|
|
||||||
// Fast check - most messages don't start with 'pusher' or 'client-'
|
// Check for client- broadcast messages
|
||||||
$firstChar = $event[0] ?? '';
|
|
||||||
if ($firstChar !== 'p' && $firstChar !== 'c') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for client- messages
|
|
||||||
if (strpos($event, 'client-') === 0) {
|
if (strpos($event, 'client-') === 0) {
|
||||||
if (!$connection->app->clientMessagesEnabled) {
|
if (!$connection->app->clientMessagesEnabled) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -269,11 +263,7 @@ class Handler implements MessageComponentInterface
|
||||||
$connection->app->id
|
$connection->app->id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for pusher: or pusher. messages (subscribe/unsubscribe handled elsewhere)
|
|
||||||
// This is handled by handleChannelSubscriptions for subscribe/unsubscribe
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onOpen(ConnectionInterface $connection): void
|
public function onOpen(ConnectionInterface $connection): void
|
||||||
|
|
@ -354,7 +344,7 @@ class Handler implements MessageComponentInterface
|
||||||
protected function shouldRejectMessage(?Channel $channel, ConnectionInterface $connection, array $message): bool
|
protected function shouldRejectMessage(?Channel $channel, ConnectionInterface $connection, array $message): bool
|
||||||
{
|
{
|
||||||
$event = $message['event'] ?? '';
|
$event = $message['event'] ?? '';
|
||||||
$isUnsubscribe = $event === 'pusher:unsubscribe' || $event === 'pusher.unsubscribe';
|
$isUnsubscribe = self::isProtocolAction($event, 'unsubscribe');
|
||||||
|
|
||||||
if (!$channel?->hasConnection($connection) && !$isUnsubscribe) {
|
if (!$channel?->hasConnection($connection) && !$isUnsubscribe) {
|
||||||
// The connection may have been removed from Channel::$connections by
|
// The connection may have been removed from Channel::$connections by
|
||||||
|
|
@ -394,14 +384,21 @@ class Handler implements MessageComponentInterface
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function handlePusherEvent(array $message, ConnectionInterface $connection): bool
|
/**
|
||||||
|
* Handle protocol-level events (subscribe, unsubscribe, ping, etc.).
|
||||||
|
* These are events with a known action after the delimiter (e.g. websocket.subscribe).
|
||||||
|
* Sends an immediate :response acknowledgement and short-circuits forkWithSocketPair.
|
||||||
|
*/
|
||||||
|
protected function handleProtocolEvent(array $message, ConnectionInterface $connection): bool
|
||||||
{
|
{
|
||||||
if (!str_contains($message['event'], 'pusher')) {
|
$event = $message['event'] ?? '';
|
||||||
|
|
||||||
|
if (!self::isProtocolAction($event, 'subscribe') && !self::isProtocolAction($event, 'unsubscribe')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$connection->send(json_encode([
|
$connection->send(json_encode([
|
||||||
'event' => $message['event'] . ':response',
|
'event' => $event . ':response',
|
||||||
'data' => [
|
'data' => [
|
||||||
'message' => 'Success',
|
'message' => 'Success',
|
||||||
],
|
],
|
||||||
|
|
@ -409,6 +406,22 @@ class Handler implements MessageComponentInterface
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an event name ends with a known protocol action.
|
||||||
|
* Matches any prefix with either . or : as delimiter.
|
||||||
|
*
|
||||||
|
* Examples that match isProtocolAction($event, 'subscribe'):
|
||||||
|
* websocket.subscribe, pusher:subscribe, my.prefix.subscribe
|
||||||
|
*
|
||||||
|
* Examples that do NOT match:
|
||||||
|
* admin.unsubscribeUserStatus (does not end with .subscribe or :subscribe)
|
||||||
|
*/
|
||||||
|
protected static function isProtocolAction(string $event, string $action): bool
|
||||||
|
{
|
||||||
|
return str_ends_with($event, '.' . $action)
|
||||||
|
|| str_ends_with($event, ':' . $action);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if hot reload mode is enabled
|
* Check if hot reload mode is enabled
|
||||||
*/
|
*/
|
||||||
|
|
@ -1070,7 +1083,7 @@ class Handler implements MessageComponentInterface
|
||||||
protected function establishConnection(ConnectionInterface $connection)
|
protected function establishConnection(ConnectionInterface $connection)
|
||||||
{
|
{
|
||||||
$connection->send(json_encode([
|
$connection->send(json_encode([
|
||||||
'event' => 'pusher.connection_established',
|
'event' => 'websocket.connection_established',
|
||||||
'data' => json_encode([
|
'data' => json_encode([
|
||||||
'socket_id' => $connection->socketId,
|
'socket_id' => $connection->socketId,
|
||||||
'activity_timeout' => 30,
|
'activity_timeout' => 30,
|
||||||
|
|
@ -1108,14 +1121,14 @@ class Handler implements MessageComponentInterface
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$eventLower = strtolower($message['event']);
|
$event = $message['event'];
|
||||||
|
|
||||||
if ($eventLower === 'pusher.subscribe' || $eventLower === 'pusher:subscribe') {
|
if (self::isProtocolAction($event, 'unsubscribe')) {
|
||||||
$this->handleSubscription($channel, $channel_name, $connection, $message);
|
$this->handleUnsubscription($channel, $channel_name, $connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (str_contains($message['event'], '.unsubscribe')) {
|
if (self::isProtocolAction($event, 'subscribe')) {
|
||||||
$this->handleUnsubscription($channel, $channel_name, $connection);
|
$this->handleSubscription($channel, $channel_name, $connection, $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $channel;
|
return $channel;
|
||||||
|
|
@ -1151,9 +1164,9 @@ class Handler implements MessageComponentInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$channel->subscribe($connection, (object) $message);
|
$channel->subscribe($connection, (object) ($message['data'] ?? []));
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Silently handle subscription errors
|
// Silently handle subscription errors (e.g. invalid signatures)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test\Apps;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Apps\App;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Apps\MysqlAppManager;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\AppManager;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Test\TestCase;
|
|
||||||
|
|
||||||
class MysqlAppManagerTest extends TestCase
|
|
||||||
{
|
|
||||||
/** @var AppManager */
|
|
||||||
protected $apps;
|
|
||||||
|
|
||||||
public function getEnvironmentSetUp($app)
|
|
||||||
{
|
|
||||||
parent::getEnvironmentSetUp($app);
|
|
||||||
|
|
||||||
$app['config']->set('websockets.managers.app', MysqlAppManager::class);
|
|
||||||
$app['config']->set('database.connections.mysql.database', 'websockets_test');
|
|
||||||
$app['config']->set('database.connections.mysql.username', 'root');
|
|
||||||
$app['config']->set('database.connections.mysql.password', 'password');
|
|
||||||
|
|
||||||
$app['config']->set('websockets.managers.mysql.table', 'websockets_apps');
|
|
||||||
$app['config']->set('websockets.managers.mysql.connection', 'mysql');
|
|
||||||
$app['config']->set('database.connections.default', 'mysql');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
// Skip if MySQL is not available
|
|
||||||
try {
|
|
||||||
\DB::connection('mysql')->getPdo();
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->markTestSkipped('MySQL connection is not available: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->artisan('migrate:fresh', [
|
|
||||||
'--database' => 'mysql',
|
|
||||||
'--realpath' => true,
|
|
||||||
'--path' => __DIR__ . '/../../database/migrations/',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->apps = app()->make(AppManager::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_return_all_apps()
|
|
||||||
{
|
|
||||||
$apps = $this->await($this->apps->all());
|
|
||||||
$this->assertCount(0, $apps);
|
|
||||||
|
|
||||||
$this->await($this->apps->createApp([
|
|
||||||
'id' => 1,
|
|
||||||
'key' => 'test',
|
|
||||||
'secret' => 'secret',
|
|
||||||
'name' => 'Test',
|
|
||||||
'enable_client_messages' => true,
|
|
||||||
'enable_statistics' => false,
|
|
||||||
]));
|
|
||||||
|
|
||||||
$apps = $this->await($this->apps->all());
|
|
||||||
$this->assertCount(1, $apps);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_find_apps_by_id()
|
|
||||||
{
|
|
||||||
$this->await($this->apps->createApp([
|
|
||||||
'id' => 1,
|
|
||||||
'key' => 'test',
|
|
||||||
'secret' => 'secret',
|
|
||||||
'name' => 'Test',
|
|
||||||
'enable_client_messages' => true,
|
|
||||||
'enable_statistics' => false,
|
|
||||||
]));
|
|
||||||
|
|
||||||
$app = $this->await($this->apps->findById(1));
|
|
||||||
|
|
||||||
$this->assertInstanceOf(App::class, $app);
|
|
||||||
$this->assertSame('test', $app->key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_find_apps_by_key()
|
|
||||||
{
|
|
||||||
$this->await($this->apps->createApp([
|
|
||||||
'id' => 1,
|
|
||||||
'key' => 'key',
|
|
||||||
'secret' => 'secret',
|
|
||||||
'name' => 'Test',
|
|
||||||
'enable_client_messages' => true,
|
|
||||||
'enable_statistics' => false,
|
|
||||||
]));
|
|
||||||
|
|
||||||
$app = $this->await($this->apps->findByKey('key'));
|
|
||||||
|
|
||||||
$this->assertInstanceOf(App::class, $app);
|
|
||||||
$this->assertSame('key', $app->key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_find_apps_by_secret()
|
|
||||||
{
|
|
||||||
$this->await($this->apps->createApp([
|
|
||||||
'id' => 1,
|
|
||||||
'key' => 'key',
|
|
||||||
'secret' => 'secret',
|
|
||||||
'name' => 'Test',
|
|
||||||
'enable_client_messages' => true,
|
|
||||||
'enable_statistics' => false,
|
|
||||||
]));
|
|
||||||
|
|
||||||
$app = $this->await($this->apps->findBySecret('secret'));
|
|
||||||
|
|
||||||
$this->assertInstanceOf(App::class, $app);
|
|
||||||
$this->assertSame('key', $app->key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test\Apps;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Apps\App;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Apps\SQLiteAppManager;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\AppManager;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Test\TestCase;
|
|
||||||
|
|
||||||
class SqliteAppManagerTest extends TestCase
|
|
||||||
{
|
|
||||||
/** @var AppManager */
|
|
||||||
protected $apps;
|
|
||||||
|
|
||||||
public function getEnvironmentSetUp($app)
|
|
||||||
{
|
|
||||||
parent::getEnvironmentSetUp($app);
|
|
||||||
|
|
||||||
$app['config']->set('websockets.managers.app', SQLiteAppManager::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
$this->apps = app()->make(AppManager::class);
|
|
||||||
|
|
||||||
// Test if SQLite async database is working
|
|
||||||
try {
|
|
||||||
$this->await($this->apps->all(), null, 2);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->markTestSkipped('SQLite async database is not available: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_return_all_apps()
|
|
||||||
{
|
|
||||||
$apps = $this->await($this->apps->all());
|
|
||||||
$this->assertCount(0, $apps);
|
|
||||||
|
|
||||||
$this->await($this->apps->createApp([
|
|
||||||
'id' => 1,
|
|
||||||
'key' => 'test',
|
|
||||||
'secret' => 'secret',
|
|
||||||
'name' => 'Test',
|
|
||||||
'enable_client_messages' => true,
|
|
||||||
'enable_statistics' => false,
|
|
||||||
]));
|
|
||||||
|
|
||||||
$apps = $this->await($this->apps->all());
|
|
||||||
$this->assertCount(1, $apps);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_find_apps_by_id()
|
|
||||||
{
|
|
||||||
$this->await($this->apps->createApp([
|
|
||||||
'id' => 1,
|
|
||||||
'key' => 'test',
|
|
||||||
'secret' => 'secret',
|
|
||||||
'name' => 'Test',
|
|
||||||
'enable_client_messages' => true,
|
|
||||||
'enable_statistics' => false,
|
|
||||||
]));
|
|
||||||
|
|
||||||
$app = $this->await($this->apps->findById(1));
|
|
||||||
|
|
||||||
$this->assertInstanceOf(App::class, $app);
|
|
||||||
$this->assertSame('test', $app->key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_find_apps_by_key()
|
|
||||||
{
|
|
||||||
$this->await($this->apps->createApp([
|
|
||||||
'id' => 1,
|
|
||||||
'key' => 'key',
|
|
||||||
'secret' => 'secret',
|
|
||||||
'name' => 'Test',
|
|
||||||
'enable_client_messages' => true,
|
|
||||||
'enable_statistics' => false,
|
|
||||||
]));
|
|
||||||
|
|
||||||
$app = $this->await($this->apps->findByKey('key'));
|
|
||||||
|
|
||||||
$this->assertInstanceOf(App::class, $app);
|
|
||||||
$this->assertSame('key', $app->key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_find_apps_by_secret()
|
|
||||||
{
|
|
||||||
$this->await($this->apps->createApp([
|
|
||||||
'id' => 1,
|
|
||||||
'key' => 'key',
|
|
||||||
'secret' => 'secret',
|
|
||||||
'name' => 'Test',
|
|
||||||
'enable_client_messages' => true,
|
|
||||||
'enable_statistics' => false,
|
|
||||||
]));
|
|
||||||
|
|
||||||
$app = $this->await($this->apps->findBySecret('secret'));
|
|
||||||
|
|
||||||
$this->assertInstanceOf(App::class, $app);
|
|
||||||
$this->assertSame('key', $app->key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -94,7 +94,7 @@ class IpcCacheTest extends TestCase
|
||||||
public function test_it_can_store_complex_data()
|
public function test_it_can_store_complex_data()
|
||||||
{
|
{
|
||||||
$complexData = [
|
$complexData = [
|
||||||
'event' => 'pusher:connection_established',
|
'event' => 'websocket.connection_established',
|
||||||
'data' => [
|
'data' => [
|
||||||
'socket_id' => '123.456',
|
'socket_id' => '123.456',
|
||||||
'activity_timeout' => 120,
|
'activity_timeout' => 120,
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test;
|
|
||||||
|
|
||||||
class StatisticsCleanTest extends TestCase
|
|
||||||
{
|
|
||||||
public function test_clean_statistics_for_app_id()
|
|
||||||
{
|
|
||||||
$rick = $this->newActiveConnection(['public-channel']);
|
|
||||||
$morty = $this->newActiveConnection(['public-channel'], 'TestKey2');
|
|
||||||
|
|
||||||
$this->statisticsCollector->save();
|
|
||||||
|
|
||||||
$this->assertCount(2, $records = $this->statisticsStore->getRecords());
|
|
||||||
|
|
||||||
foreach ($this->statisticsStore->getRawRecords() as $record) {
|
|
||||||
$record->update(['created_at' => now()->subDays(10)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->artisan('websockets:clean', [
|
|
||||||
'appId' => '12345',
|
|
||||||
'--days' => 1,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->assertCount(1, $records = $this->statisticsStore->getRecords());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_clean_statistics_older_than_given_days()
|
|
||||||
{
|
|
||||||
$rick = $this->newActiveConnection(['public-channel']);
|
|
||||||
$morty = $this->newActiveConnection(['public-channel'], 'TestKey2');
|
|
||||||
|
|
||||||
$this->statisticsCollector->save();
|
|
||||||
|
|
||||||
$this->assertCount(2, $records = $this->statisticsStore->getRecords());
|
|
||||||
|
|
||||||
foreach ($this->statisticsStore->getRawRecords() as $record) {
|
|
||||||
$record->update(['created_at' => now()->subDays(10)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->artisan('websockets:clean', ['--days' => 1]);
|
|
||||||
|
|
||||||
$this->assertCount(0, $records = $this->statisticsStore->getRecords());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -16,23 +16,7 @@ class ConnectionTest extends TestCase
|
||||||
$this->startServer();
|
$this->startServer();
|
||||||
|
|
||||||
$response = $this->await($this->joinWebSocketServer(['public-channel'], 'NonWorkingKey'));
|
$response = $this->await($this->joinWebSocketServer(['public-channel'], 'NonWorkingKey'));
|
||||||
$this->assertSame('{"event":"pusher.error","data":{"message":"Could not find app key `NonWorkingKey`.","code":4001}}', (string) $response);
|
$this->assertSame('{"event":"websocket.error","data":{"message":"Could not find app key `NonWorkingKey`.","code":4001}}', (string) $response);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @group integration
|
|
||||||
* @group requires-server
|
|
||||||
*/
|
|
||||||
public function test_unconnected_app_cannot_store_statistics()
|
|
||||||
{
|
|
||||||
$this->skipIfServerUnavailable();
|
|
||||||
$this->startServer();
|
|
||||||
|
|
||||||
$response = $this->await($this->joinWebSocketServer(['public-channel'], 'NonWorkingKey'));
|
|
||||||
$this->assertSame('{"event":"pusher.error","data":{"message":"Could not find app key `NonWorkingKey`.","code":4001}}', (string) $response);
|
|
||||||
|
|
||||||
$count = $this->await($this->statisticsCollector->getStatistics());
|
|
||||||
$this->assertCount(0, $count);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -45,7 +29,7 @@ class ConnectionTest extends TestCase
|
||||||
$this->startServer();
|
$this->startServer();
|
||||||
|
|
||||||
$response = $this->await($this->joinWebSocketServer(['public-channel'], 'TestOrigin'));
|
$response = $this->await($this->joinWebSocketServer(['public-channel'], 'TestOrigin'));
|
||||||
$this->assertSame('{"event":"pusher.error","data":{"message":"The origin is not allowed for `TestOrigin`.","code":4009}}', (string) $response);
|
$this->assertSame('{"event":"websocket.error","data":{"message":"The origin is not allowed for `TestOrigin`.","code":4009}}', (string) $response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -58,16 +42,16 @@ class ConnectionTest extends TestCase
|
||||||
$this->startServer();
|
$this->startServer();
|
||||||
|
|
||||||
$response = $this->await($this->joinWebSocketServer(['public-channel'], 'TestOrigin', ['Origin' => 'https://google.ro']));
|
$response = $this->await($this->joinWebSocketServer(['public-channel'], 'TestOrigin', ['Origin' => 'https://google.ro']));
|
||||||
$this->assertSame('{"event":"pusher.error","data":{"message":"The origin is not allowed for `TestOrigin`.","code":4009}}', (string) $response);
|
$this->assertSame('{"event":"websocket.error","data":{"message":"The origin is not allowed for `TestOrigin`.","code":4009}}', (string) $response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_origin_validation_should_pass_for_the_right_origin()
|
public function test_origin_validation_should_pass_for_the_right_origin()
|
||||||
{
|
{
|
||||||
$connection = $this->newConnection('TestOrigin', ['Origin' => 'https://test.origin.com']);
|
$connection = $this->newConnection('TestOrigin', ['Origin' => 'https://test.origin.com']);
|
||||||
|
|
||||||
$this->pusherServer->onOpen($connection);
|
$this->wsHandler->onOpen($connection);
|
||||||
|
|
||||||
$connection->assertSentEvent('pusher.connection_established');
|
$connection->assertSentEvent('websocket.connection_established');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_close_connection()
|
public function test_close_connection()
|
||||||
|
|
@ -86,7 +70,7 @@ class ConnectionTest extends TestCase
|
||||||
$this->assertEquals(1, $total);
|
$this->assertEquals(1, $total);
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->pusherServer->onClose($connection);
|
$this->wsHandler->onClose($connection);
|
||||||
|
|
||||||
$this->channelManager
|
$this->channelManager
|
||||||
->getGlobalConnectionsCount('1234')
|
->getGlobalConnectionsCount('1234')
|
||||||
|
|
@ -105,9 +89,9 @@ class ConnectionTest extends TestCase
|
||||||
{
|
{
|
||||||
$connection = $this->newActiveConnection(['public-channel']);
|
$connection = $this->newActiveConnection(['public-channel']);
|
||||||
|
|
||||||
$this->pusherServer->onError($connection, new UnknownAppKey('NonWorkingKey'));
|
$this->wsHandler->onError($connection, new UnknownAppKey('NonWorkingKey'));
|
||||||
|
|
||||||
$connection->assertSentEvent('pusher.error', [
|
$connection->assertSentEvent('websocket.error', [
|
||||||
'data' => [
|
'data' => [
|
||||||
'message' => 'Could not find app key `NonWorkingKey`.',
|
'message' => 'Could not find app key `NonWorkingKey`.',
|
||||||
'code' => 4001,
|
'code' => 4001,
|
||||||
|
|
@ -125,15 +109,15 @@ class ConnectionTest extends TestCase
|
||||||
$failedConnection = $this->newActiveConnection(['test-channel']);
|
$failedConnection = $this->newActiveConnection(['test-channel']);
|
||||||
|
|
||||||
$failedConnection
|
$failedConnection
|
||||||
->assertSentEvent('pusher.error', ['data' => ['message' => 'Over capacity', 'code' => 4100]])
|
->assertSentEvent('websocket.error', ['data' => ['message' => 'Over capacity', 'code' => 4100]])
|
||||||
->assertClosed();
|
->assertClosed();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_close_all_new_connections_after_stating_the_server_does_not_accept_new_connections()
|
public function test_close_all_new_connections_after_stating_the_server_does_not_accept_new_connections()
|
||||||
{
|
{
|
||||||
$this->newActiveConnection(['test-channel'])
|
$this->newActiveConnection(['test-channel'])
|
||||||
->assertSentEvent('pusher.connection_established')
|
->assertSentEvent('websocket.connection_established')
|
||||||
->assertSentEvent('pusher_internal:subscription_succeeded');
|
->assertSentEvent('websocket_internal.subscription_succeeded');
|
||||||
|
|
||||||
$this->channelManager->declineNewConnections();
|
$this->channelManager->declineNewConnections();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test\Dashboard;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Apps\SQLiteAppManager;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Test\Models\User;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Test\TestCase;
|
|
||||||
|
|
||||||
class AppsTest extends TestCase
|
|
||||||
{
|
|
||||||
public function getEnvironmentSetUp($app)
|
|
||||||
{
|
|
||||||
parent::getEnvironmentSetUp($app);
|
|
||||||
|
|
||||||
$app['config']->set('websockets.managers.app', SQLiteAppManager::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_list_all_apps()
|
|
||||||
{
|
|
||||||
$this->actingAs(factory(User::class)->create())
|
|
||||||
->get(route('laravel-websockets.apps'))
|
|
||||||
->assertViewHas('apps', []);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_create_app()
|
|
||||||
{
|
|
||||||
$this->actingAs(factory(User::class)->create())
|
|
||||||
->post(route('laravel-websockets.apps.store', [
|
|
||||||
'name' => 'New App',
|
|
||||||
]));
|
|
||||||
|
|
||||||
$this->actingAs(factory(User::class)->create())
|
|
||||||
->get(route('laravel-websockets.apps'))
|
|
||||||
->assertViewHas('apps', function ($apps) {
|
|
||||||
return count($apps) === 1 && $apps[0]['name'] === 'New App';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test\Dashboard;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Test\Mocks\SignedMessage;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Test\Models\User;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Test\TestCase;
|
|
||||||
|
|
||||||
class AuthTest extends TestCase
|
|
||||||
{
|
|
||||||
public function test_can_authenticate_dashboard_over_channel()
|
|
||||||
{
|
|
||||||
$connection = $this->newActiveConnection(['test-channel']);
|
|
||||||
|
|
||||||
$this->pusherServer->onOpen($connection);
|
|
||||||
|
|
||||||
$this->actingAs(factory(User::class)->create())
|
|
||||||
->json('POST', route('laravel-websockets.auth'), [
|
|
||||||
'socket_id' => $connection->socketId,
|
|
||||||
'channel_name' => 'test-channel',
|
|
||||||
], ['x-app-id' => '1234'])
|
|
||||||
->seeJsonStructure([
|
|
||||||
'auth',
|
|
||||||
'channel_data',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_authenticate_dashboard_over_private_channel()
|
|
||||||
{
|
|
||||||
$connection = $this->newConnection();
|
|
||||||
|
|
||||||
$this->pusherServer->onOpen($connection);
|
|
||||||
|
|
||||||
$message = new SignedMessage([
|
|
||||||
'event' => 'pusher.subscribe',
|
|
||||||
'data' => [
|
|
||||||
'channel' => 'private-channel',
|
|
||||||
],
|
|
||||||
], $connection, 'private-channel');
|
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
|
||||||
|
|
||||||
$connection->assertSentEvent('pusher_internal:subscription_succeeded', [
|
|
||||||
'channel' => 'private-channel',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->actingAs(factory(User::class)->create())
|
|
||||||
->json('POST', route('laravel-websockets.auth'), [
|
|
||||||
'socket_id' => $connection->socketId,
|
|
||||||
'channel_name' => 'private-test-channel',
|
|
||||||
], ['x-app-id' => '1234'])
|
|
||||||
->seeJsonStructure([
|
|
||||||
'auth',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_authenticate_dashboard_over_presence_channel()
|
|
||||||
{
|
|
||||||
$connection = $this->newConnection();
|
|
||||||
|
|
||||||
$this->pusherServer->onOpen($connection);
|
|
||||||
|
|
||||||
$user = json_encode([
|
|
||||||
'user_id' => 1,
|
|
||||||
'user_info' => [
|
|
||||||
'name' => 'Rick',
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$message = new SignedMessage([
|
|
||||||
'event' => 'pusher.subscribe',
|
|
||||||
'data' => [
|
|
||||||
'channel' => 'presence-channel',
|
|
||||||
'channel_data' => $user,
|
|
||||||
],
|
|
||||||
], $connection, 'presence-channel', $user);
|
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
|
||||||
|
|
||||||
$this->actingAs(factory(User::class)->create())
|
|
||||||
->json('POST', route('laravel-websockets.auth'), [
|
|
||||||
'socket_id' => $connection->socketId,
|
|
||||||
'channel_name' => 'presence-channel',
|
|
||||||
], ['x-app-id' => '1234'])
|
|
||||||
->seeJsonStructure([
|
|
||||||
'auth',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test\Dashboard;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Test\Models\User;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Test\TestCase;
|
|
||||||
|
|
||||||
class DashboardTest extends TestCase
|
|
||||||
{
|
|
||||||
public function test_cant_see_dashboard_without_authorization()
|
|
||||||
{
|
|
||||||
$this->get(route('laravel-websockets.dashboard'))
|
|
||||||
->assertResponseStatus(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_see_dashboard()
|
|
||||||
{
|
|
||||||
$this->actingAs(factory(User::class)->create())
|
|
||||||
->get(route('laravel-websockets.dashboard'))
|
|
||||||
->assertResponseOk();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test\Dashboard;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Test\Models\User;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Test\TestCase;
|
|
||||||
|
|
||||||
class SendMessageTest extends TestCase
|
|
||||||
{
|
|
||||||
public function test_can_send_message()
|
|
||||||
{
|
|
||||||
$this->actingAs(factory(User::class)->create())
|
|
||||||
->json('POST', route('laravel-websockets.event'), [
|
|
||||||
'appId' => '1234',
|
|
||||||
'key' => 'TestKey',
|
|
||||||
'secret' => 'TestSecret',
|
|
||||||
'channel' => 'test-channel',
|
|
||||||
'event' => 'some-event',
|
|
||||||
'data' => json_encode(['data' => 'yes']),
|
|
||||||
])
|
|
||||||
->seeJson([
|
|
||||||
'ok' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->markTestIncomplete(
|
|
||||||
'Broadcasting is not possible to be tested without receiving a Pusher error.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_cant_send_message_for_invalid_app()
|
|
||||||
{
|
|
||||||
$this->actingAs(factory(User::class)->create())
|
|
||||||
->json('POST', route('laravel-websockets.event'), [
|
|
||||||
'appId' => '9999',
|
|
||||||
'key' => 'TestKey',
|
|
||||||
'secret' => 'TestSecret',
|
|
||||||
'channel' => 'test-channel',
|
|
||||||
'event' => 'some-event',
|
|
||||||
'data' => json_encode(['data' => 'yes']),
|
|
||||||
])
|
|
||||||
->assertResponseStatus(422);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test\Dashboard;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Test\Models\User;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Test\TestCase;
|
|
||||||
|
|
||||||
class StatisticsTest extends TestCase
|
|
||||||
{
|
|
||||||
public function test_can_get_statistics()
|
|
||||||
{
|
|
||||||
$rick = $this->newActiveConnection(['public-channel']);
|
|
||||||
$morty = $this->newActiveConnection(['public-channel']);
|
|
||||||
|
|
||||||
$this->statisticsCollector->save();
|
|
||||||
|
|
||||||
$response = $this->actingAs(factory(User::class)->create())
|
|
||||||
->json('GET', route('laravel-websockets.statistics', ['appId' => '1234']))
|
|
||||||
->assertResponseOk()
|
|
||||||
->seeJsonStructure([
|
|
||||||
'peak_connections' => ['x', 'y'],
|
|
||||||
'websocket_messages_count' => ['x', 'y'],
|
|
||||||
'api_messages_count' => ['x', 'y'],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_cant_get_statistics_for_invalid_app_id()
|
|
||||||
{
|
|
||||||
$rick = $this->newActiveConnection(['public-channel']);
|
|
||||||
$morty = $this->newActiveConnection(['public-channel']);
|
|
||||||
|
|
||||||
$this->statisticsCollector->save();
|
|
||||||
|
|
||||||
$this->actingAs(factory(User::class)->create())
|
|
||||||
->json('GET', route('laravel-websockets.statistics', ['appId' => 'not_found']))
|
|
||||||
->seeJson([
|
|
||||||
'peak_connections' => ['x' => [], 'y' => []],
|
|
||||||
'websocket_messages_count' => ['x' => [], 'y' => []],
|
|
||||||
'api_messages_count' => ['x' => [], 'y' => []],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\API\FetchChannel;
|
|
||||||
use GuzzleHttp\Psr7\Request;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Pusher\Pusher;
|
|
||||||
|
|
||||||
class FetchChannelTest extends TestCase
|
|
||||||
{
|
|
||||||
public function test_invalid_signatures_can_not_access_the_api()
|
|
||||||
{
|
|
||||||
$this->startServer();
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channels/my-channel';
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
|
||||||
'TestKey', 'InvalidSecret', 'GET', $requestPath
|
|
||||||
));
|
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}");
|
|
||||||
|
|
||||||
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
|
||||||
|
|
||||||
$this->assertSame(401, $response->getStatusCode());
|
|
||||||
$this->assertSame('{"error":"Invalid auth signature provided."}', $response->getBody()->getContents());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_returns_the_channel_information()
|
|
||||||
{
|
|
||||||
$this->newActiveConnection(['my-channel']);
|
|
||||||
$this->newActiveConnection(['my-channel']);
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channel/my-channel';
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
'channelName' => 'my-channel',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(FetchChannel::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame([
|
|
||||||
'occupied' => true,
|
|
||||||
'subscription_count' => 2,
|
|
||||||
], json_decode($response->getContent(), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_returns_presence_channel_information()
|
|
||||||
{
|
|
||||||
$this->newPresenceConnection('presence-channel', ['user_id' => 1]);
|
|
||||||
$this->newPresenceConnection('presence-channel', ['user_id' => 2]);
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channel/my-channel';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
'channelName' => 'presence-channel',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(FetchChannel::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame([
|
|
||||||
'occupied' => true,
|
|
||||||
'subscription_count' => 2,
|
|
||||||
'user_count' => 2,
|
|
||||||
], json_decode($response->getContent(), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_returns_404_for_invalid_channels()
|
|
||||||
{
|
|
||||||
$this->skipOnRedisReplication();
|
|
||||||
|
|
||||||
$this->startServer();
|
|
||||||
|
|
||||||
$this->newActiveConnection(['my-channel']);
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channels/invalid-channel';
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
|
||||||
|
|
||||||
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
|
||||||
|
|
||||||
$this->assertSame(404, $response->getStatusCode());
|
|
||||||
$this->assertSame('{"error":"Unknown channel `invalid-channel`."}', $response->getBody()->getContents());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\API\FetchChannels;
|
|
||||||
use GuzzleHttp\Psr7\Request;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Pusher\Pusher;
|
|
||||||
|
|
||||||
class FetchChannelsTest extends TestCase
|
|
||||||
{
|
|
||||||
public function test_invalid_signatures_can_not_access_the_api()
|
|
||||||
{
|
|
||||||
$this->startServer();
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channels';
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
|
||||||
'TestKey', 'InvalidSecret', 'GET', $requestPath
|
|
||||||
));
|
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}");
|
|
||||||
|
|
||||||
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
|
||||||
|
|
||||||
$this->assertSame(401, $response->getStatusCode());
|
|
||||||
$this->assertSame('{"error":"Invalid auth signature provided."}', $response->getBody()->getContents());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_returns_the_channel_information()
|
|
||||||
{
|
|
||||||
$this->newPresenceConnection('presence-channel');
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channels';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
|
||||||
'TestKey', 'TestSecret', 'GET', $requestPath
|
|
||||||
));
|
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(FetchChannels::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame([
|
|
||||||
'channels' => [
|
|
||||||
'presence-channel' => [],
|
|
||||||
],
|
|
||||||
], json_decode($response->getContent(), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_returns_the_channel_information_for_prefix()
|
|
||||||
{
|
|
||||||
$this->newPresenceConnection('presence-global.1');
|
|
||||||
$this->newPresenceConnection('presence-global.1');
|
|
||||||
$this->newPresenceConnection('presence-global.2');
|
|
||||||
$this->newPresenceConnection('presence-notglobal.2');
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channels';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath, [
|
|
||||||
'filter_by_prefix' => 'presence-global',
|
|
||||||
]));
|
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(FetchChannels::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame([
|
|
||||||
'channels' => [
|
|
||||||
'presence-global.1' => [],
|
|
||||||
'presence-global.2' => [],
|
|
||||||
],
|
|
||||||
], json_decode($response->getContent(), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_returns_the_channel_information_for_prefix_with_user_count()
|
|
||||||
{
|
|
||||||
$this->newPresenceConnection('presence-global.1', ['user_id' => 1]);
|
|
||||||
$this->newPresenceConnection('presence-global.1', ['user_id' => 2]);
|
|
||||||
$this->newPresenceConnection('presence-global.2', ['user_id' => 3]);
|
|
||||||
$this->newPresenceConnection('presence-notglobal.2', ['user_id' => 4]);
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channels';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath, [
|
|
||||||
'filter_by_prefix' => 'presence-global',
|
|
||||||
'info' => 'user_count',
|
|
||||||
]));
|
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(FetchChannels::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame([
|
|
||||||
'channels' => [
|
|
||||||
'presence-global.1' => [
|
|
||||||
'user_count' => 2,
|
|
||||||
],
|
|
||||||
'presence-global.2' => [
|
|
||||||
'user_count' => 1,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
], json_decode($response->getContent(), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_can_not_get_non_presence_channel_user_count()
|
|
||||||
{
|
|
||||||
$this->startServer();
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channels';
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath, [
|
|
||||||
'info' => 'user_count',
|
|
||||||
]));
|
|
||||||
|
|
||||||
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
|
||||||
|
|
||||||
$this->assertSame(400, $response->getStatusCode());
|
|
||||||
$this->assertSame('{"error":"Request must be limited to presence channels in order to fetch user_count"}', $response->getBody()->getContents());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_returns_empty_object_for_no_channels_found()
|
|
||||||
{
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channels';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(FetchChannels::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame('{"channels":{}}', $response->getContent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test;
|
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\API\FetchUsers;
|
|
||||||
use GuzzleHttp\Psr7\Request;
|
|
||||||
use Pusher\Pusher;
|
|
||||||
|
|
||||||
class FetchUsersTest extends TestCase
|
|
||||||
{
|
|
||||||
public function test_invalid_signatures_can_not_access_the_api()
|
|
||||||
{
|
|
||||||
$this->startServer();
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channels/my-channel/users';
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
|
||||||
'TestKey', 'InvalidSecret', 'GET', $requestPath
|
|
||||||
));
|
|
||||||
|
|
||||||
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
|
||||||
|
|
||||||
$this->assertSame(401, $response->getStatusCode());
|
|
||||||
$this->assertSame('{"error":"Invalid auth signature provided."}', $response->getBody()->getContents());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_only_returns_data_for_presence_channels()
|
|
||||||
{
|
|
||||||
$this->startServer();
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channels/my-channel/users';
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
|
||||||
'TestKey', 'TestSecret', 'GET', $requestPath
|
|
||||||
));
|
|
||||||
|
|
||||||
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
|
||||||
|
|
||||||
$this->assertSame(400, $response->getStatusCode());
|
|
||||||
$this->assertSame('{"error":"Invalid presence channel `my-channel`"}', $response->getBody()->getContents());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_returns_400_for_invalid_channels()
|
|
||||||
{
|
|
||||||
$this->startServer();
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channels/invalid-channel/users';
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
|
||||||
'TestKey', 'TestSecret', 'GET', $requestPath
|
|
||||||
));
|
|
||||||
|
|
||||||
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
|
||||||
|
|
||||||
$this->assertSame(400, $response->getStatusCode());
|
|
||||||
$this->assertSame('{"error":"Invalid presence channel `invalid-channel`"}', $response->getBody()->getContents());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_returns_connected_user_information()
|
|
||||||
{
|
|
||||||
$this->newPresenceConnection('presence-channel');
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channel/presence-channel/users';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
'channelName' => 'presence-channel',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(FetchUsers::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var \Illuminate\Http\JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame([
|
|
||||||
'users' => [['id' => 1]],
|
|
||||||
], json_decode($response->getContent(), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_multiple_clients_with_same_id_gets_counted_once()
|
|
||||||
{
|
|
||||||
$rick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]);
|
|
||||||
$morty = $this->newPresenceConnection('presence-channel', ['user_id' => 1]);
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/channel/presence-channel/users';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
'channelName' => 'presence-channel',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params('TestKey', 'TestSecret', 'GET', $requestPath));
|
|
||||||
|
|
||||||
$request = new Request('GET', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(FetchUsers::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var \Illuminate\Http\JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame([
|
|
||||||
'users' => [['id' => 1]],
|
|
||||||
], json_decode($response->getContent(), true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,472 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BlaxSoftware\LaravelWebSockets\Test;
|
||||||
|
|
||||||
|
use BlaxSoftware\LaravelWebSockets\Services\WebsocketService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comprehensive tests for the full WebSocket Handler lifecycle.
|
||||||
|
*
|
||||||
|
* These tests use the TestCase helper methods (newConnection, newActiveConnection,
|
||||||
|
* newPrivateConnection, newPresenceConnection) to exercise the Handler's
|
||||||
|
* connection management, channel subscriptions, and message routing.
|
||||||
|
*
|
||||||
|
* Covers:
|
||||||
|
* - Connection lifecycle (open, close, error)
|
||||||
|
* - Public/private/presence channel subscribe/unsubscribe
|
||||||
|
* - Ping/pong heartbeat
|
||||||
|
* - Protocol event responses
|
||||||
|
* - Connection isolation (errors don't leak across connections)
|
||||||
|
* - Cache tracking updates via Handler
|
||||||
|
*/
|
||||||
|
class HandlerLifecycleTest extends TestCase
|
||||||
|
{
|
||||||
|
// =========================================================================
|
||||||
|
// Connection establishment
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_new_connection_receives_connection_established_event()
|
||||||
|
{
|
||||||
|
$connection = $this->newConnection();
|
||||||
|
$this->wsHandler->onOpen($connection);
|
||||||
|
|
||||||
|
$connection->assertSentEvent('websocket.connection_established');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_connection_established_contains_socket_id()
|
||||||
|
{
|
||||||
|
$connection = $this->newConnection();
|
||||||
|
$this->wsHandler->onOpen($connection);
|
||||||
|
|
||||||
|
$event = collect($connection->sentData)->firstWhere('event', 'websocket.connection_established');
|
||||||
|
$data = json_decode($event['data'], true);
|
||||||
|
|
||||||
|
$this->assertNotNull($data['socket_id']);
|
||||||
|
$this->assertStringContainsString('.', $data['socket_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_connection_established_contains_activity_timeout()
|
||||||
|
{
|
||||||
|
$connection = $this->newConnection();
|
||||||
|
$this->wsHandler->onOpen($connection);
|
||||||
|
|
||||||
|
$event = collect($connection->sentData)->firstWhere('event', 'websocket.connection_established');
|
||||||
|
$data = json_decode($event['data'], true);
|
||||||
|
|
||||||
|
$this->assertEquals(30, $data['activity_timeout']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_multiple_connections_get_unique_socket_ids()
|
||||||
|
{
|
||||||
|
$conn1 = $this->newConnection();
|
||||||
|
$conn2 = $this->newConnection();
|
||||||
|
$conn3 = $this->newConnection();
|
||||||
|
|
||||||
|
$this->wsHandler->onOpen($conn1);
|
||||||
|
$this->wsHandler->onOpen($conn2);
|
||||||
|
$this->wsHandler->onOpen($conn3);
|
||||||
|
|
||||||
|
$socket1 = json_decode((collect($conn1->sentData)->firstWhere('event', 'websocket.connection_established')['data'] ?? '{}'), true)['socket_id'] ?? null;
|
||||||
|
$socket2 = json_decode((collect($conn2->sentData)->firstWhere('event', 'websocket.connection_established')['data'] ?? '{}'), true)['socket_id'] ?? null;
|
||||||
|
$socket3 = json_decode((collect($conn3->sentData)->firstWhere('event', 'websocket.connection_established')['data'] ?? '{}'), true)['socket_id'] ?? null;
|
||||||
|
|
||||||
|
$this->assertNotNull($socket1);
|
||||||
|
$this->assertNotNull($socket2);
|
||||||
|
$this->assertNotNull($socket3);
|
||||||
|
$this->assertNotEquals($socket1, $socket2);
|
||||||
|
$this->assertNotEquals($socket2, $socket3);
|
||||||
|
$this->assertNotEquals($socket1, $socket3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Public channel subscribe/unsubscribe
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_subscribe_to_public_channel()
|
||||||
|
{
|
||||||
|
$connection = $this->newActiveConnection(['test-channel']);
|
||||||
|
|
||||||
|
$connection->assertSentEvent('websocket_internal.subscription_succeeded');
|
||||||
|
|
||||||
|
$channel = $this->channelManager->find('1234', 'test-channel');
|
||||||
|
$this->assertTrue($channel->hasConnection($connection));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_subscribe_to_multiple_public_channels()
|
||||||
|
{
|
||||||
|
$connection = $this->newActiveConnection(['channel-a', 'channel-b', 'channel-c']);
|
||||||
|
|
||||||
|
$channelA = $this->channelManager->find('1234', 'channel-a');
|
||||||
|
$channelB = $this->channelManager->find('1234', 'channel-b');
|
||||||
|
$channelC = $this->channelManager->find('1234', 'channel-c');
|
||||||
|
|
||||||
|
$this->assertTrue($channelA->hasConnection($connection));
|
||||||
|
$this->assertTrue($channelB->hasConnection($connection));
|
||||||
|
$this->assertTrue($channelC->hasConnection($connection));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_unsubscribe_from_public_channel()
|
||||||
|
{
|
||||||
|
$connection = $this->newActiveConnection(['test-channel']);
|
||||||
|
$channel = $this->channelManager->find('1234', 'test-channel');
|
||||||
|
|
||||||
|
$this->assertTrue($channel->hasConnection($connection));
|
||||||
|
|
||||||
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
|
'event' => 'websocket.unsubscribe',
|
||||||
|
'data' => ['channel' => 'test-channel'],
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertFalse($channel->hasConnection($connection));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_unsubscribe_does_not_affect_other_channels()
|
||||||
|
{
|
||||||
|
$connection = $this->newActiveConnection(['channel-a', 'channel-b']);
|
||||||
|
|
||||||
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
|
'event' => 'websocket.unsubscribe',
|
||||||
|
'data' => ['channel' => 'channel-a'],
|
||||||
|
]));
|
||||||
|
|
||||||
|
$channelA = $this->channelManager->find('1234', 'channel-a');
|
||||||
|
$channelB = $this->channelManager->find('1234', 'channel-b');
|
||||||
|
|
||||||
|
$this->assertFalse($channelA->hasConnection($connection));
|
||||||
|
$this->assertTrue($channelB->hasConnection($connection));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Private channel subscribe/unsubscribe
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_subscribe_to_private_channel_with_valid_signature()
|
||||||
|
{
|
||||||
|
$connection = $this->newPrivateConnection('private-test');
|
||||||
|
|
||||||
|
$connection->assertSentEvent('websocket_internal.subscription_succeeded');
|
||||||
|
|
||||||
|
$channel = $this->channelManager->find('1234', 'private-test');
|
||||||
|
$this->assertTrue($channel->hasConnection($connection));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_subscribe_to_private_channel_with_invalid_signature_is_rejected()
|
||||||
|
{
|
||||||
|
$connection = $this->newConnection();
|
||||||
|
$this->wsHandler->onOpen($connection);
|
||||||
|
|
||||||
|
$message = new Mocks\Message([
|
||||||
|
'event' => 'websocket.subscribe',
|
||||||
|
'data' => [
|
||||||
|
'auth' => 'invalid-signature',
|
||||||
|
'channel' => 'private-test',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->wsHandler->onMessage($connection, $message);
|
||||||
|
|
||||||
|
// Invalid signature is silently caught — no subscription_succeeded
|
||||||
|
$connection->assertNotSentEvent('websocket_internal.subscription_succeeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Presence channel subscribe/unsubscribe
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_subscribe_to_presence_channel()
|
||||||
|
{
|
||||||
|
$connection = $this->newPresenceConnection('presence-chat', [
|
||||||
|
'user_id' => 1,
|
||||||
|
'user_info' => ['name' => 'Rick'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$connection->assertSentEvent('websocket_internal.subscription_succeeded');
|
||||||
|
|
||||||
|
$channel = $this->channelManager->find('1234', 'presence-chat');
|
||||||
|
$this->assertTrue($channel->hasConnection($connection));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_presence_channel_member_added_on_second_connection()
|
||||||
|
{
|
||||||
|
$rick = $this->newPresenceConnection('presence-chat', [
|
||||||
|
'user_id' => 1,
|
||||||
|
'user_info' => ['name' => 'Rick'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$rick->resetEvents();
|
||||||
|
|
||||||
|
$morty = $this->newPresenceConnection('presence-chat', [
|
||||||
|
'user_id' => 2,
|
||||||
|
'user_info' => ['name' => 'Morty'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Rick should receive a member_added event
|
||||||
|
$rick->assertSentEvent('websocket_internal.member_added');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_presence_channel_shows_member_count()
|
||||||
|
{
|
||||||
|
$rick = $this->newPresenceConnection('presence-chat', [
|
||||||
|
'user_id' => 1,
|
||||||
|
'user_info' => ['name' => 'Rick'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$morty = $this->newPresenceConnection('presence-chat', [
|
||||||
|
'user_id' => 2,
|
||||||
|
'user_info' => ['name' => 'Morty'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$channel = $this->channelManager->find('1234', 'presence-chat');
|
||||||
|
$this->assertTrue($channel->hasConnection($rick));
|
||||||
|
$this->assertTrue($channel->hasConnection($morty));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Ping/pong heartbeat
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_ping_receives_pong()
|
||||||
|
{
|
||||||
|
$connection = $this->newActiveConnection(['websocket']);
|
||||||
|
$connection->resetEvents();
|
||||||
|
|
||||||
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
|
'event' => 'websocket.ping',
|
||||||
|
'data' => new \stdClass(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
$connection->assertSentEvent('websocket.pong');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_backward_compat_colon_ping_receives_pong()
|
||||||
|
{
|
||||||
|
$connection = $this->newActiveConnection(['websocket']);
|
||||||
|
$connection->resetEvents();
|
||||||
|
|
||||||
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
|
'event' => 'pusher:ping',
|
||||||
|
'data' => new \stdClass(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
$connection->assertSentEvent('websocket.pong');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_ping_does_not_require_channel_subscription()
|
||||||
|
{
|
||||||
|
$connection = $this->newConnection();
|
||||||
|
$this->wsHandler->onOpen($connection);
|
||||||
|
$connection->resetEvents();
|
||||||
|
|
||||||
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
|
'event' => 'websocket.ping',
|
||||||
|
'data' => new \stdClass(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
$connection->assertSentEvent('websocket.pong');
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Protocol :response suffix
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_subscribe_gets_response_suffix()
|
||||||
|
{
|
||||||
|
$connection = $this->newActiveConnection(['websocket']);
|
||||||
|
$connection->resetEvents();
|
||||||
|
|
||||||
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
|
'event' => 'websocket.subscribe',
|
||||||
|
'data' => ['channel' => 'websocket'],
|
||||||
|
]));
|
||||||
|
|
||||||
|
$response = collect($connection->sentData)->firstWhere('event', 'websocket.subscribe:response');
|
||||||
|
$this->assertNotNull($response);
|
||||||
|
$this->assertEquals('Success', $response['data']['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_unsubscribe_gets_response_suffix()
|
||||||
|
{
|
||||||
|
$connection = $this->newActiveConnection(['websocket']);
|
||||||
|
$connection->resetEvents();
|
||||||
|
|
||||||
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
|
'event' => 'websocket.unsubscribe',
|
||||||
|
'data' => ['channel' => 'websocket'],
|
||||||
|
]));
|
||||||
|
|
||||||
|
$response = collect($connection->sentData)->firstWhere('event', 'websocket.unsubscribe:response');
|
||||||
|
$this->assertNotNull($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Message rejection without subscription
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_message_to_unsubscribed_channel_gets_error()
|
||||||
|
{
|
||||||
|
$connection = $this->newActiveConnection(['websocket']);
|
||||||
|
|
||||||
|
// Unsubscribe first
|
||||||
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
|
'event' => 'websocket.unsubscribe',
|
||||||
|
'data' => ['channel' => 'websocket'],
|
||||||
|
]));
|
||||||
|
|
||||||
|
$connection->resetEvents();
|
||||||
|
|
||||||
|
// Try to send a message to the channel
|
||||||
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
|
'event' => 'app.someAction',
|
||||||
|
'data' => ['test' => true],
|
||||||
|
'channel' => 'websocket',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$error = collect($connection->sentData)->firstWhere('event', 'app.someAction:error');
|
||||||
|
$this->assertNotNull($error);
|
||||||
|
$this->assertEquals('Subscription not established', $error['data']['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Connection close and error handling
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_on_close_removes_connection_from_channels()
|
||||||
|
{
|
||||||
|
$connection = $this->newActiveConnection(['websocket']);
|
||||||
|
$channel = $this->channelManager->find('1234', 'websocket');
|
||||||
|
|
||||||
|
$this->assertTrue($channel->hasConnection($connection));
|
||||||
|
|
||||||
|
$this->wsHandler->onClose($connection);
|
||||||
|
|
||||||
|
$this->assertFalse($channel->hasConnection($connection));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_on_error_does_not_close_other_connections()
|
||||||
|
{
|
||||||
|
$conn1 = $this->newActiveConnection(['websocket']);
|
||||||
|
$conn2 = $this->newActiveConnection(['websocket']);
|
||||||
|
|
||||||
|
$this->wsHandler->onError($conn1, new \Exception('Test error'));
|
||||||
|
|
||||||
|
// Generic exceptions are ignored; only ExceptionsWebSocketException is emitted.
|
||||||
|
$conn1->assertNotSentEvent('websocket.error');
|
||||||
|
$conn2->assertNotSentEvent('websocket.error');
|
||||||
|
|
||||||
|
$channel = $this->channelManager->find('1234', 'websocket');
|
||||||
|
$this->assertTrue($channel->hasConnection($conn2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Broadcasting between connections (via Channel class)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_broadcast_to_public_channel_reaches_all_connections()
|
||||||
|
{
|
||||||
|
$conn1 = $this->newActiveConnection(['news']);
|
||||||
|
$conn2 = $this->newActiveConnection(['news']);
|
||||||
|
$conn3 = $this->newActiveConnection(['news']);
|
||||||
|
|
||||||
|
$conn1->resetEvents();
|
||||||
|
$conn2->resetEvents();
|
||||||
|
$conn3->resetEvents();
|
||||||
|
|
||||||
|
$channel = $this->channelManager->find('1234', 'news');
|
||||||
|
$channel->broadcast('1234', (object) [
|
||||||
|
'event' => 'news.update',
|
||||||
|
'data' => ['title' => 'Breaking News'],
|
||||||
|
'channel' => 'news',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$conn1->assertSentEvent('news.update');
|
||||||
|
$conn2->assertSentEvent('news.update');
|
||||||
|
$conn3->assertSentEvent('news.update');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_broadcast_except_excludes_specified_socket()
|
||||||
|
{
|
||||||
|
$conn1 = $this->newActiveConnection(['chat']);
|
||||||
|
$conn2 = $this->newActiveConnection(['chat']);
|
||||||
|
|
||||||
|
$established = collect($conn1->sentData)->firstWhere('event', 'websocket.connection_established');
|
||||||
|
$excludeSocketId = json_decode($established['data'] ?? '{}', true)['socket_id'] ?? '';
|
||||||
|
|
||||||
|
$conn1->resetEvents();
|
||||||
|
$conn2->resetEvents();
|
||||||
|
|
||||||
|
$channel = $this->channelManager->find('1234', 'chat');
|
||||||
|
$channel->broadcastToEveryoneExcept(
|
||||||
|
(object) ['event' => 'chat.message', 'data' => ['text' => 'Hello']],
|
||||||
|
$excludeSocketId,
|
||||||
|
'1234'
|
||||||
|
);
|
||||||
|
|
||||||
|
// conn1 excluded, conn2 receives
|
||||||
|
$conn1->assertNotSentEvent('chat.message');
|
||||||
|
$conn2->assertSentEvent('chat.message');
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Connection count tracking
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_channel_connection_count_tracks_subscribe_and_unsubscribe()
|
||||||
|
{
|
||||||
|
$conn1 = $this->newActiveConnection(['counter-channel']);
|
||||||
|
$conn2 = $this->newActiveConnection(['counter-channel']);
|
||||||
|
|
||||||
|
$channel = $this->channelManager->find('1234', 'counter-channel');
|
||||||
|
$this->assertTrue($channel->hasConnection($conn1));
|
||||||
|
$this->assertTrue($channel->hasConnection($conn2));
|
||||||
|
|
||||||
|
// Unsubscribe one
|
||||||
|
$this->wsHandler->onMessage($conn1, new Mocks\Message([
|
||||||
|
'event' => 'websocket.unsubscribe',
|
||||||
|
'data' => ['channel' => 'counter-channel'],
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertFalse($channel->hasConnection($conn1));
|
||||||
|
$this->assertTrue($channel->hasConnection($conn2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Message without app context
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_message_without_on_open_is_ignored()
|
||||||
|
{
|
||||||
|
$connection = new Mocks\Connection();
|
||||||
|
$connection->httpRequest = new \GuzzleHttp\Psr7\Request('GET', '/?appKey=TestKey');
|
||||||
|
|
||||||
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
|
'event' => 'websocket.ping',
|
||||||
|
'data' => new \stdClass(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertEmpty($connection->sentData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Re-subscription after unsubscribe
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_resubscribe_after_unsubscribe_works()
|
||||||
|
{
|
||||||
|
$connection = $this->newActiveConnection(['websocket']);
|
||||||
|
$channel = $this->channelManager->find('1234', 'websocket');
|
||||||
|
|
||||||
|
// Unsubscribe
|
||||||
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
|
'event' => 'websocket.unsubscribe',
|
||||||
|
'data' => ['channel' => 'websocket'],
|
||||||
|
]));
|
||||||
|
$this->assertFalse($channel->hasConnection($connection));
|
||||||
|
|
||||||
|
// Re-subscribe
|
||||||
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
|
'event' => 'websocket.subscribe',
|
||||||
|
'data' => ['channel' => 'websocket'],
|
||||||
|
]));
|
||||||
|
$this->assertTrue($channel->hasConnection($connection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,9 +11,9 @@ class HealthTest extends TestCase
|
||||||
{
|
{
|
||||||
$connection = $this->newConnection();
|
$connection = $this->newConnection();
|
||||||
|
|
||||||
$this->pusherServer = app(HealthHandler::class);
|
$this->wsHandler = app(HealthHandler::class);
|
||||||
|
|
||||||
$this->pusherServer->onOpen($connection);
|
$this->wsHandler->onOpen($connection);
|
||||||
|
|
||||||
$this->assertTrue(
|
$this->assertTrue(
|
||||||
Str::contains($connection->sentRawData[0], '{"ok":true}')
|
Str::contains($connection->sentRawData[0], '{"ok":true}')
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
|
|
||||||
// Simulate what happens when a client connects
|
// Simulate what happens when a client connects
|
||||||
$connectionEstablished = json_encode([
|
$connectionEstablished = json_encode([
|
||||||
'event' => 'pusher:connection_established',
|
'event' => 'websocket.connection_established',
|
||||||
'data' => json_encode([
|
'data' => json_encode([
|
||||||
'socket_id' => '123.456',
|
'socket_id' => '123.456',
|
||||||
'activity_timeout' => 30,
|
'activity_timeout' => 30,
|
||||||
|
|
@ -81,7 +81,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
$this->assertNotNull($receivedData);
|
$this->assertNotNull($receivedData);
|
||||||
|
|
||||||
$decoded = json_decode($receivedData, true);
|
$decoded = json_decode($receivedData, true);
|
||||||
$this->assertEquals('pusher:connection_established', $decoded['event']);
|
$this->assertEquals('websocket.connection_established', $decoded['event']);
|
||||||
|
|
||||||
$data = json_decode($decoded['data'], true);
|
$data = json_decode($decoded['data'], true);
|
||||||
$this->assertEquals('123.456', $data['socket_id']);
|
$this->assertEquals('123.456', $data['socket_id']);
|
||||||
|
|
@ -117,7 +117,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
|
|
||||||
// Simulate subscription success response
|
// Simulate subscription success response
|
||||||
$subscriptionSuccess = json_encode([
|
$subscriptionSuccess = json_encode([
|
||||||
'event' => 'pusher_internal:subscription_succeeded',
|
'event' => 'websocket_internal.subscription_succeeded',
|
||||||
'channel' => 'public-channel',
|
'channel' => 'public-channel',
|
||||||
'data' => '{}',
|
'data' => '{}',
|
||||||
]);
|
]);
|
||||||
|
|
@ -144,7 +144,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
pcntl_waitpid($pid, $status);
|
pcntl_waitpid($pid, $status);
|
||||||
|
|
||||||
$decoded = json_decode($receivedData, true);
|
$decoded = json_decode($receivedData, true);
|
||||||
$this->assertEquals('pusher_internal:subscription_succeeded', $decoded['event']);
|
$this->assertEquals('websocket_internal.subscription_succeeded', $decoded['event']);
|
||||||
$this->assertEquals('public-channel', $decoded['channel']);
|
$this->assertEquals('public-channel', $decoded['channel']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,7 +189,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
];
|
];
|
||||||
|
|
||||||
$subscriptionSuccess = json_encode([
|
$subscriptionSuccess = json_encode([
|
||||||
'event' => 'pusher_internal:subscription_succeeded',
|
'event' => 'websocket_internal.subscription_succeeded',
|
||||||
'channel' => 'presence-room.1',
|
'channel' => 'presence-room.1',
|
||||||
'data' => json_encode($presenceData),
|
'data' => json_encode($presenceData),
|
||||||
]);
|
]);
|
||||||
|
|
@ -216,7 +216,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
pcntl_waitpid($pid, $status);
|
pcntl_waitpid($pid, $status);
|
||||||
|
|
||||||
$decoded = json_decode($receivedData, true);
|
$decoded = json_decode($receivedData, true);
|
||||||
$this->assertEquals('pusher_internal:subscription_succeeded', $decoded['event']);
|
$this->assertEquals('websocket_internal.subscription_succeeded', $decoded['event']);
|
||||||
$this->assertEquals('presence-room.1', $decoded['channel']);
|
$this->assertEquals('presence-room.1', $decoded['channel']);
|
||||||
|
|
||||||
$data = json_decode($decoded['data'], true);
|
$data = json_decode($decoded['data'], true);
|
||||||
|
|
@ -387,7 +387,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
|
|
||||||
// Simulate an error response
|
// Simulate an error response
|
||||||
$errorResponse = json_encode([
|
$errorResponse = json_encode([
|
||||||
'event' => 'pusher:error',
|
'event' => 'websocket.error',
|
||||||
'data' => [
|
'data' => [
|
||||||
'message' => 'Could not find app key `InvalidKey`.',
|
'message' => 'Could not find app key `InvalidKey`.',
|
||||||
'code' => 4001,
|
'code' => 4001,
|
||||||
|
|
@ -416,7 +416,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
pcntl_waitpid($pid, $status);
|
pcntl_waitpid($pid, $status);
|
||||||
|
|
||||||
$decoded = json_decode($receivedData, true);
|
$decoded = json_decode($receivedData, true);
|
||||||
$this->assertEquals('pusher:error', $decoded['event']);
|
$this->assertEquals('websocket.error', $decoded['event']);
|
||||||
$this->assertEquals(4001, $decoded['data']['code']);
|
$this->assertEquals(4001, $decoded['data']['code']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -448,7 +448,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
$ipc->setupChild();
|
$ipc->setupChild();
|
||||||
|
|
||||||
$memberAdded = json_encode([
|
$memberAdded = json_encode([
|
||||||
'event' => 'pusher_internal:member_added',
|
'event' => 'websocket_internal.member_added',
|
||||||
'channel' => 'presence-room.1',
|
'channel' => 'presence-room.1',
|
||||||
'data' => json_encode([
|
'data' => json_encode([
|
||||||
'user_id' => 'user_4',
|
'user_id' => 'user_4',
|
||||||
|
|
@ -478,7 +478,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
pcntl_waitpid($pid, $status);
|
pcntl_waitpid($pid, $status);
|
||||||
|
|
||||||
$decoded = json_decode($receivedData, true);
|
$decoded = json_decode($receivedData, true);
|
||||||
$this->assertEquals('pusher_internal:member_added', $decoded['event']);
|
$this->assertEquals('websocket_internal.member_added', $decoded['event']);
|
||||||
|
|
||||||
$data = json_decode($decoded['data'], true);
|
$data = json_decode($decoded['data'], true);
|
||||||
$this->assertEquals('user_4', $data['user_id']);
|
$this->assertEquals('user_4', $data['user_id']);
|
||||||
|
|
@ -512,7 +512,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
$ipc->setupChild();
|
$ipc->setupChild();
|
||||||
|
|
||||||
$memberRemoved = json_encode([
|
$memberRemoved = json_encode([
|
||||||
'event' => 'pusher_internal:member_removed',
|
'event' => 'websocket_internal.member_removed',
|
||||||
'channel' => 'presence-room.1',
|
'channel' => 'presence-room.1',
|
||||||
'data' => json_encode([
|
'data' => json_encode([
|
||||||
'user_id' => 'user_2',
|
'user_id' => 'user_2',
|
||||||
|
|
@ -541,7 +541,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
pcntl_waitpid($pid, $status);
|
pcntl_waitpid($pid, $status);
|
||||||
|
|
||||||
$decoded = json_decode($receivedData, true);
|
$decoded = json_decode($receivedData, true);
|
||||||
$this->assertEquals('pusher_internal:member_removed', $decoded['event']);
|
$this->assertEquals('websocket_internal.member_removed', $decoded['event']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -574,7 +574,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
|
|
||||||
// Simulate pong response
|
// Simulate pong response
|
||||||
$pongResponse = json_encode([
|
$pongResponse = json_encode([
|
||||||
'event' => 'pusher:pong',
|
'event' => 'websocket.pong',
|
||||||
'data' => '{}',
|
'data' => '{}',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -602,7 +602,7 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
pcntl_waitpid($pid, $status);
|
pcntl_waitpid($pid, $status);
|
||||||
|
|
||||||
$decoded = json_decode($receivedData, true);
|
$decoded = json_decode($receivedData, true);
|
||||||
$this->assertEquals('pusher:pong', $decoded['event']);
|
$this->assertEquals('websocket.pong', $decoded['event']);
|
||||||
|
|
||||||
// Ping/pong should be very fast
|
// Ping/pong should be very fast
|
||||||
$this->assertLessThan(50, $latency, "Ping/pong latency {$latency}ms exceeds 50ms");
|
$this->assertLessThan(50, $latency, "Ping/pong latency {$latency}ms exceeds 50ms");
|
||||||
|
|
@ -637,13 +637,13 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
|
|
||||||
// 1. Connection established
|
// 1. Connection established
|
||||||
$ipc->sendToParent(json_encode([
|
$ipc->sendToParent(json_encode([
|
||||||
'event' => 'pusher:connection_established',
|
'event' => 'websocket.connection_established',
|
||||||
'data' => json_encode(['socket_id' => '123.456', 'activity_timeout' => 30]),
|
'data' => json_encode(['socket_id' => '123.456', 'activity_timeout' => 30]),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
// 2. Subscribe to channel
|
// 2. Subscribe to channel
|
||||||
$ipc->sendToParent(json_encode([
|
$ipc->sendToParent(json_encode([
|
||||||
'event' => 'pusher_internal:subscription_succeeded',
|
'event' => 'websocket_internal.subscription_succeeded',
|
||||||
'channel' => 'public-chat',
|
'channel' => 'public-chat',
|
||||||
'data' => '{}',
|
'data' => '{}',
|
||||||
]));
|
]));
|
||||||
|
|
@ -657,13 +657,13 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
|
|
||||||
// 4. Ping response
|
// 4. Ping response
|
||||||
$ipc->sendToParent(json_encode([
|
$ipc->sendToParent(json_encode([
|
||||||
'event' => 'pusher:pong',
|
'event' => 'websocket.pong',
|
||||||
'data' => '{}',
|
'data' => '{}',
|
||||||
]));
|
]));
|
||||||
|
|
||||||
// 5. Unsubscribe
|
// 5. Unsubscribe
|
||||||
$ipc->sendToParent(json_encode([
|
$ipc->sendToParent(json_encode([
|
||||||
'event' => 'pusher_internal:unsubscribed',
|
'event' => 'websocket_internal.unsubscribed',
|
||||||
'channel' => 'public-chat',
|
'channel' => 'public-chat',
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
|
@ -693,11 +693,11 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
$this->assertCount(5, $receivedMessages);
|
$this->assertCount(5, $receivedMessages);
|
||||||
|
|
||||||
// Verify lifecycle order
|
// Verify lifecycle order
|
||||||
$this->assertEquals('pusher:connection_established', $receivedMessages[0]['event']);
|
$this->assertEquals('websocket.connection_established', $receivedMessages[0]['event']);
|
||||||
$this->assertEquals('pusher_internal:subscription_succeeded', $receivedMessages[1]['event']);
|
$this->assertEquals('websocket_internal.subscription_succeeded', $receivedMessages[1]['event']);
|
||||||
$this->assertEquals('new-message', $receivedMessages[2]['event']);
|
$this->assertEquals('new-message', $receivedMessages[2]['event']);
|
||||||
$this->assertEquals('pusher:pong', $receivedMessages[3]['event']);
|
$this->assertEquals('websocket.pong', $receivedMessages[3]['event']);
|
||||||
$this->assertEquals('pusher_internal:unsubscribed', $receivedMessages[4]['event']);
|
$this->assertEquals('websocket_internal.unsubscribed', $receivedMessages[4]['event']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -744,8 +744,8 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
$mock = new MockConnectionSocketPair($realConnection, $ipc);
|
$mock = new MockConnectionSocketPair($realConnection, $ipc);
|
||||||
|
|
||||||
// Simulate sending multiple messages through mock
|
// Simulate sending multiple messages through mock
|
||||||
$mock->send(json_encode(['event' => 'pusher:connection_established', 'data' => '{}']));
|
$mock->send(json_encode(['event' => 'websocket.connection_established', 'data' => '{}']));
|
||||||
$mock->send(json_encode(['event' => 'pusher_internal:subscription_succeeded', 'channel' => 'test']));
|
$mock->send(json_encode(['event' => 'websocket_internal.subscription_succeeded', 'channel' => 'test']));
|
||||||
$mock->send(json_encode(['event' => 'message', 'data' => 'Hello']));
|
$mock->send(json_encode(['event' => 'message', 'data' => 'Hello']));
|
||||||
|
|
||||||
$ipc->closeChild();
|
$ipc->closeChild();
|
||||||
|
|
@ -772,8 +772,8 @@ class SocketPairIpcWebsocketWorkflowTest extends TestCase
|
||||||
pcntl_waitpid($pid, $status);
|
pcntl_waitpid($pid, $status);
|
||||||
|
|
||||||
$this->assertCount(3, $receivedMessages);
|
$this->assertCount(3, $receivedMessages);
|
||||||
$this->assertEquals('pusher:connection_established', $receivedMessages[0]['event']);
|
$this->assertEquals('websocket.connection_established', $receivedMessages[0]['event']);
|
||||||
$this->assertEquals('pusher_internal:subscription_succeeded', $receivedMessages[1]['event']);
|
$this->assertEquals('websocket_internal.subscription_succeeded', $receivedMessages[1]['event']);
|
||||||
$this->assertEquals('message', $receivedMessages[2]['event']);
|
$this->assertEquals('message', $receivedMessages[2]['event']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ class PingTest extends TestCase
|
||||||
{
|
{
|
||||||
$connection = $this->newActiveConnection(['public-channel']);
|
$connection = $this->newActiveConnection(['public-channel']);
|
||||||
|
|
||||||
$message = new Mocks\Message(['event' => 'pusher.ping']);
|
$message = new Mocks\Message(['event' => 'websocket.ping']);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
$this->wsHandler->onMessage($connection, $message);
|
||||||
|
|
||||||
$connection->assertSentEvent('pusher.pong');
|
$connection->assertSentEvent('websocket.pong');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,38 +2,35 @@
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test;
|
namespace BlaxSoftware\LaravelWebSockets\Test;
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\API\TriggerEvent;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||||
use GuzzleHttp\Psr7\Request;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Pusher\Pusher;
|
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
|
|
||||||
class PresenceChannelTest extends TestCase
|
class PresenceChannelTest extends TestCase
|
||||||
{
|
{
|
||||||
public function test_connect_to_presence_channel_with_invalid_signature()
|
public function test_connect_to_presence_channel_with_invalid_signature()
|
||||||
{
|
{
|
||||||
$this->expectException(InvalidSignature::class);
|
|
||||||
|
|
||||||
$connection = $this->newConnection();
|
$connection = $this->newConnection();
|
||||||
|
|
||||||
$message = new Mocks\Message([
|
$message = new Mocks\Message([
|
||||||
'event' => 'pusher.subscribe',
|
'event' => 'websocket.subscribe',
|
||||||
'data' => [
|
'data' => [
|
||||||
'auth' => 'invalid',
|
'auth' => 'invalid',
|
||||||
'channel' => 'presence-channel',
|
'channel' => 'presence-channel',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onOpen($connection);
|
$this->wsHandler->onOpen($connection);
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
$this->wsHandler->onMessage($connection, $message);
|
||||||
|
|
||||||
|
// Invalid signature should be silently rejected — no subscription_succeeded sent
|
||||||
|
$connection->assertNotSentEvent('websocket_internal.subscription_succeeded');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_connect_to_presence_channel_with_valid_signature()
|
public function test_connect_to_presence_channel_with_valid_signature()
|
||||||
{
|
{
|
||||||
$connection = $this->newConnection();
|
$connection = $this->newConnection();
|
||||||
|
|
||||||
$this->pusherServer->onOpen($connection);
|
$this->wsHandler->onOpen($connection);
|
||||||
|
|
||||||
$user = [
|
$user = [
|
||||||
'user_id' => 1,
|
'user_id' => 1,
|
||||||
|
|
@ -45,16 +42,16 @@ class PresenceChannelTest extends TestCase
|
||||||
$encodedUser = json_encode($user);
|
$encodedUser = json_encode($user);
|
||||||
|
|
||||||
$message = new Mocks\SignedMessage([
|
$message = new Mocks\SignedMessage([
|
||||||
'event' => 'pusher.subscribe',
|
'event' => 'websocket.subscribe',
|
||||||
'data' => [
|
'data' => [
|
||||||
'channel' => 'presence-channel',
|
'channel' => 'presence-channel',
|
||||||
'channel_data' => $encodedUser,
|
'channel_data' => $encodedUser,
|
||||||
],
|
],
|
||||||
], $connection, 'presence-channel', $encodedUser);
|
], $connection, 'presence-channel', $encodedUser);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
$this->wsHandler->onMessage($connection, $message);
|
||||||
|
|
||||||
$connection->assertSentEvent('pusher_internal:subscription_succeeded', [
|
$connection->assertSentEvent('websocket_internal.subscription_succeeded', [
|
||||||
'channel' => 'presence-channel',
|
'channel' => 'presence-channel',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -72,12 +69,12 @@ class PresenceChannelTest extends TestCase
|
||||||
$pickleRick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]);
|
$pickleRick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]);
|
||||||
|
|
||||||
foreach ([$rick, $morty, $pickleRick] as $connection) {
|
foreach ([$rick, $morty, $pickleRick] as $connection) {
|
||||||
$connection->assertSentEvent('pusher_internal:subscription_succeeded', [
|
$connection->assertSentEvent('websocket_internal.subscription_succeeded', [
|
||||||
'channel' => 'presence-channel',
|
'channel' => 'presence-channel',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$rick->assertSentEvent('pusher_internal:subscription_succeeded', [
|
$rick->assertSentEvent('websocket_internal.subscription_succeeded', [
|
||||||
'channel' => 'presence-channel',
|
'channel' => 'presence-channel',
|
||||||
'data' => json_encode([
|
'data' => json_encode([
|
||||||
'presence' => [
|
'presence' => [
|
||||||
|
|
@ -88,7 +85,7 @@ class PresenceChannelTest extends TestCase
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$morty->assertSentEvent('pusher_internal:subscription_succeeded', [
|
$morty->assertSentEvent('websocket_internal.subscription_succeeded', [
|
||||||
'channel' => 'presence-channel',
|
'channel' => 'presence-channel',
|
||||||
'data' => json_encode([
|
'data' => json_encode([
|
||||||
'presence' => [
|
'presence' => [
|
||||||
|
|
@ -101,7 +98,7 @@ class PresenceChannelTest extends TestCase
|
||||||
|
|
||||||
// The duplicated-user_id connection should get basically the list of ids
|
// The duplicated-user_id connection should get basically the list of ids
|
||||||
// without dealing with duplicate user ids.
|
// without dealing with duplicate user ids.
|
||||||
$pickleRick->assertSentEvent('pusher_internal:subscription_succeeded', [
|
$pickleRick->assertSentEvent('websocket_internal.subscription_succeeded', [
|
||||||
'channel' => 'presence-channel',
|
'channel' => 'presence-channel',
|
||||||
'data' => json_encode([
|
'data' => json_encode([
|
||||||
'presence' => [
|
'presence' => [
|
||||||
|
|
@ -130,7 +127,7 @@ class PresenceChannelTest extends TestCase
|
||||||
$rick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]);
|
$rick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]);
|
||||||
$morty = $this->newPresenceConnection('presence-channel', ['user_id' => 2]);
|
$morty = $this->newPresenceConnection('presence-channel', ['user_id' => 2]);
|
||||||
|
|
||||||
$rick->assertSentEvent('pusher_internal:member_added', [
|
$rick->assertSentEvent('websocket_internal.member_added', [
|
||||||
'channel' => 'presence-channel',
|
'channel' => 'presence-channel',
|
||||||
'data' => json_encode(['user_id' => 2]),
|
'data' => json_encode(['user_id' => 2]),
|
||||||
]);
|
]);
|
||||||
|
|
@ -141,9 +138,9 @@ class PresenceChannelTest extends TestCase
|
||||||
$this->assertCount(2, $members);
|
$this->assertCount(2, $members);
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->pusherServer->onClose($morty);
|
$this->wsHandler->onClose($morty);
|
||||||
|
|
||||||
$rick->assertSentEvent('pusher_internal:member_removed', [
|
$rick->assertSentEvent('websocket_internal.member_removed', [
|
||||||
'channel' => 'presence-channel',
|
'channel' => 'presence-channel',
|
||||||
'data' => json_encode(['user_id' => 2]),
|
'data' => json_encode(['user_id' => 2]),
|
||||||
]);
|
]);
|
||||||
|
|
@ -173,13 +170,13 @@ class PresenceChannelTest extends TestCase
|
||||||
});
|
});
|
||||||
|
|
||||||
$message = new Mocks\Message([
|
$message = new Mocks\Message([
|
||||||
'event' => 'pusher.unsubscribe',
|
'event' => 'websocket.unsubscribe',
|
||||||
'data' => [
|
'data' => [
|
||||||
'channel' => 'presence-channel',
|
'channel' => 'presence-channel',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
$this->wsHandler->onMessage($connection, $message);
|
||||||
|
|
||||||
$this->channelManager
|
$this->channelManager
|
||||||
->getGlobalConnectionsCount('1234', 'presence-channel')
|
->getGlobalConnectionsCount('1234', 'presence-channel')
|
||||||
|
|
@ -201,7 +198,7 @@ class PresenceChannelTest extends TestCase
|
||||||
'channel' => 'presence-channel',
|
'channel' => 'presence-channel',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($rick, $message);
|
$this->wsHandler->onMessage($rick, $message);
|
||||||
|
|
||||||
$rick->assertNotSentEvent('client-test-whisper');
|
$rick->assertNotSentEvent('client-test-whisper');
|
||||||
$morty->assertSentEvent('client-test-whisper', ['data' => [], 'channel' => 'presence-channel']);
|
$morty->assertSentEvent('client-test-whisper', ['data' => [], 'channel' => 'presence-channel']);
|
||||||
|
|
@ -218,35 +215,12 @@ class PresenceChannelTest extends TestCase
|
||||||
'channel' => 'presence-channel',
|
'channel' => 'presence-channel',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($rick, $message);
|
$this->wsHandler->onMessage($rick, $message);
|
||||||
|
|
||||||
$rick->assertNotSentEvent('client-test-whisper');
|
$rick->assertNotSentEvent('client-test-whisper');
|
||||||
$morty->assertNotSentEvent('client-test-whisper');
|
$morty->assertNotSentEvent('client-test-whisper');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_statistics_get_collected_for_presenece_channels()
|
|
||||||
{
|
|
||||||
$rick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]);
|
|
||||||
$morty = $this->newPresenceConnection('presence-channel', ['user_id' => 2]);
|
|
||||||
|
|
||||||
$this->statisticsCollector
|
|
||||||
->getStatistics()
|
|
||||||
->then(function ($statistics) {
|
|
||||||
$this->assertCount(1, $statistics);
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->statisticsCollector
|
|
||||||
->getAppStatistics('1234')
|
|
||||||
->then(function ($statistic) {
|
|
||||||
$this->assertEquals([
|
|
||||||
'peak_connections_count' => 2,
|
|
||||||
'websocket_messages_count' => 2,
|
|
||||||
'api_messages_count' => 0,
|
|
||||||
'app_id' => '1234',
|
|
||||||
], $statistic->toArray());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_local_connections_for_presence_channels()
|
public function test_local_connections_for_presence_channels()
|
||||||
{
|
{
|
||||||
$this->newPresenceConnection('presence-channel', ['user_id' => 1]);
|
$this->newPresenceConnection('presence-channel', ['user_id' => 1]);
|
||||||
|
|
@ -274,8 +248,8 @@ class PresenceChannelTest extends TestCase
|
||||||
$firstConnection = $this->newPresenceConnection('presence-channel', ['user_id' => '1']);
|
$firstConnection = $this->newPresenceConnection('presence-channel', ['user_id' => '1']);
|
||||||
|
|
||||||
// Make sure the observer sees a `member_added` event for `user:1`
|
// Make sure the observer sees a `member_added` event for `user:1`
|
||||||
$observerConnection->assertSentEvent('pusher_internal:member_added', [
|
$observerConnection->assertSentEvent('websocket_internal.member_added', [
|
||||||
'event' => 'pusher_internal:member_added',
|
'event' => 'websocket_internal.member_added',
|
||||||
'channel' => 'presence-channel',
|
'channel' => 'presence-channel',
|
||||||
'data' => json_encode(['user_id' => '1']),
|
'data' => json_encode(['user_id' => '1']),
|
||||||
])->resetEvents();
|
])->resetEvents();
|
||||||
|
|
@ -284,19 +258,19 @@ class PresenceChannelTest extends TestCase
|
||||||
$secondConnection = $this->newPresenceConnection('presence-channel', ['user_id' => '1']);
|
$secondConnection = $this->newPresenceConnection('presence-channel', ['user_id' => '1']);
|
||||||
|
|
||||||
// Make sure the observer was not notified of a `member_added` event (user was already connected)
|
// Make sure the observer was not notified of a `member_added` event (user was already connected)
|
||||||
$observerConnection->assertNotSentEvent('pusher_internal:member_added');
|
$observerConnection->assertNotSentEvent('websocket_internal.member_added');
|
||||||
|
|
||||||
// Disconnect the first socket for user `1` on the server
|
// Disconnect the first socket for user `1` on the server
|
||||||
$this->pusherServer->onClose($firstConnection);
|
$this->wsHandler->onClose($firstConnection);
|
||||||
|
|
||||||
// Make sure the observer was not notified of a `member_removed` event (user still connected on another socket)
|
// Make sure the observer was not notified of a `member_removed` event (user still connected on another socket)
|
||||||
$observerConnection->assertNotSentEvent('pusher_internal:member_removed');
|
$observerConnection->assertNotSentEvent('websocket_internal.member_removed');
|
||||||
|
|
||||||
// Disconnect the second (and last) socket for user `1` on the server
|
// Disconnect the second (and last) socket for user `1` on the server
|
||||||
$this->pusherServer->onClose($secondConnection);
|
$this->wsHandler->onClose($secondConnection);
|
||||||
|
|
||||||
// Make sure the observer was notified of a `member_removed` event (last socket for user was disconnected)
|
// Make sure the observer was notified of a `member_removed` event (last socket for user was disconnected)
|
||||||
$observerConnection->assertSentEvent('pusher_internal:member_removed');
|
$observerConnection->assertSentEvent('websocket_internal.member_removed');
|
||||||
|
|
||||||
$this->channelManager
|
$this->channelManager
|
||||||
->getMemberSockets('1', '1234', 'presence-channel')
|
->getMemberSockets('1', '1234', 'presence-channel')
|
||||||
|
|
@ -406,146 +380,4 @@ class PresenceChannelTest extends TestCase
|
||||||
$message->getPayload(),
|
$message->getPayload(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_fires_the_event_to_presence_channel()
|
|
||||||
{
|
|
||||||
$this->newPresenceConnection('presence-channel');
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/events';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
|
||||||
'name' => 'some-event',
|
|
||||||
'channels' => ['presence-channel'],
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(TriggerEvent::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame([], json_decode($response->getContent(), true));
|
|
||||||
|
|
||||||
$this->statisticsCollector
|
|
||||||
->getAppStatistics('1234')
|
|
||||||
->then(function ($statistic) {
|
|
||||||
$this->assertEquals([
|
|
||||||
'peak_connections_count' => 1,
|
|
||||||
'websocket_messages_count' => 1,
|
|
||||||
'api_messages_count' => 1,
|
|
||||||
'app_id' => '1234',
|
|
||||||
], $statistic->toArray());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_fires_event_across_servers_when_there_are_not_users_locally_for_presence_channel()
|
|
||||||
{
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/events';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
|
||||||
'name' => 'some-event',
|
|
||||||
'channels' => ['presence-channel'],
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(TriggerEvent::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame([], json_decode($response->getContent(), true));
|
|
||||||
|
|
||||||
if (method_exists($this->channelManager, 'getPublishClient')) {
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->assertCalledWithArgsCount(1, 'publish', [
|
|
||||||
$this->channelManager->getRedisKey('1234', 'presence-channel'),
|
|
||||||
json_encode([
|
|
||||||
'event' => 'some-event',
|
|
||||||
'channel' => 'presence-channel',
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
'appId' => '1234',
|
|
||||||
'socketId' => null,
|
|
||||||
'serverId' => $this->channelManager->getServerId(),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_fires_event_across_servers_when_there_are_users_locally_for_presence_channel()
|
|
||||||
{
|
|
||||||
$wsConnection = $this->newPresenceConnection('presence-channel');
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/events';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
|
||||||
'name' => 'some-event',
|
|
||||||
'channels' => ['presence-channel'],
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(TriggerEvent::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame([], json_decode($response->getContent(), true));
|
|
||||||
|
|
||||||
if (method_exists($this->channelManager, 'getPublishClient')) {
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->assertCalledWithArgsCount(1, 'publish', [
|
|
||||||
$this->channelManager->getRedisKey('1234', 'presence-channel'),
|
|
||||||
json_encode([
|
|
||||||
'event' => 'some-event',
|
|
||||||
'channel' => 'presence-channel',
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
'appId' => '1234',
|
|
||||||
'socketId' => null,
|
|
||||||
'serverId' => $this->channelManager->getServerId(),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$wsConnection->assertSentEvent('some-event', [
|
|
||||||
'channel' => 'presence-channel',
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,49 +2,46 @@
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test;
|
namespace BlaxSoftware\LaravelWebSockets\Test;
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\API\TriggerEvent;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
use BlaxSoftware\LaravelWebSockets\Server\Exceptions\InvalidSignature;
|
||||||
use GuzzleHttp\Psr7\Request;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Pusher\Pusher;
|
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
|
|
||||||
class PrivateChannelTest extends TestCase
|
class PrivateChannelTest extends TestCase
|
||||||
{
|
{
|
||||||
public function test_connect_to_private_channel_with_invalid_signature()
|
public function test_connect_to_private_channel_with_invalid_signature()
|
||||||
{
|
{
|
||||||
$this->expectException(InvalidSignature::class);
|
|
||||||
|
|
||||||
$connection = $this->newConnection();
|
$connection = $this->newConnection();
|
||||||
|
|
||||||
$message = new Mocks\Message([
|
$message = new Mocks\Message([
|
||||||
'event' => 'pusher.subscribe',
|
'event' => 'websocket.subscribe',
|
||||||
'data' => [
|
'data' => [
|
||||||
'auth' => 'invalid',
|
'auth' => 'invalid',
|
||||||
'channel' => 'private-channel',
|
'channel' => 'private-channel',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onOpen($connection);
|
$this->wsHandler->onOpen($connection);
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
$this->wsHandler->onMessage($connection, $message);
|
||||||
|
|
||||||
|
// Invalid signature should be silently rejected — no subscription_succeeded sent
|
||||||
|
$connection->assertNotSentEvent('websocket_internal.subscription_succeeded');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_connect_to_private_channel_with_valid_signature()
|
public function test_connect_to_private_channel_with_valid_signature()
|
||||||
{
|
{
|
||||||
$connection = $this->newConnection();
|
$connection = $this->newConnection();
|
||||||
|
|
||||||
$this->pusherServer->onOpen($connection);
|
$this->wsHandler->onOpen($connection);
|
||||||
|
|
||||||
$message = new Mocks\SignedMessage([
|
$message = new Mocks\SignedMessage([
|
||||||
'event' => 'pusher.subscribe',
|
'event' => 'websocket.subscribe',
|
||||||
'data' => [
|
'data' => [
|
||||||
'channel' => 'private-channel',
|
'channel' => 'private-channel',
|
||||||
],
|
],
|
||||||
], $connection, 'private-channel');
|
], $connection, 'private-channel');
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
$this->wsHandler->onMessage($connection, $message);
|
||||||
|
|
||||||
$connection->assertSentEvent('pusher_internal:subscription_succeeded', [
|
$connection->assertSentEvent('websocket_internal.subscription_succeeded', [
|
||||||
'channel' => 'private-channel',
|
'channel' => 'private-channel',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -66,13 +63,13 @@ class PrivateChannelTest extends TestCase
|
||||||
});
|
});
|
||||||
|
|
||||||
$message = new Mocks\Message([
|
$message = new Mocks\Message([
|
||||||
'event' => 'pusher.unsubscribe',
|
'event' => 'websocket.unsubscribe',
|
||||||
'data' => [
|
'data' => [
|
||||||
'channel' => 'private-channel',
|
'channel' => 'private-channel',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
$this->wsHandler->onMessage($connection, $message);
|
||||||
|
|
||||||
$this->channelManager
|
$this->channelManager
|
||||||
->getGlobalConnectionsCount('1234', 'private-channel')
|
->getGlobalConnectionsCount('1234', 'private-channel')
|
||||||
|
|
@ -94,7 +91,7 @@ class PrivateChannelTest extends TestCase
|
||||||
'channel' => 'private-channel',
|
'channel' => 'private-channel',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($rick, $message);
|
$this->wsHandler->onMessage($rick, $message);
|
||||||
|
|
||||||
$rick->assertNotSentEvent('client-test-whisper');
|
$rick->assertNotSentEvent('client-test-whisper');
|
||||||
$morty->assertSentEvent('client-test-whisper', ['data' => [], 'channel' => 'private-channel']);
|
$morty->assertSentEvent('client-test-whisper', ['data' => [], 'channel' => 'private-channel']);
|
||||||
|
|
@ -111,35 +108,12 @@ class PrivateChannelTest extends TestCase
|
||||||
'channel' => 'private-channel',
|
'channel' => 'private-channel',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($rick, $message);
|
$this->wsHandler->onMessage($rick, $message);
|
||||||
|
|
||||||
$rick->assertNotSentEvent('client-test-whisper');
|
$rick->assertNotSentEvent('client-test-whisper');
|
||||||
$morty->assertNotSentEvent('client-test-whisper');
|
$morty->assertNotSentEvent('client-test-whisper');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_statistics_get_collected_for_private_channels()
|
|
||||||
{
|
|
||||||
$rick = $this->newPrivateConnection('private-channel');
|
|
||||||
$morty = $this->newPrivateConnection('private-channel');
|
|
||||||
|
|
||||||
$this->statisticsCollector
|
|
||||||
->getStatistics()
|
|
||||||
->then(function ($statistics) {
|
|
||||||
$this->assertCount(1, $statistics);
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->statisticsCollector
|
|
||||||
->getAppStatistics('1234')
|
|
||||||
->then(function ($statistic) {
|
|
||||||
$this->assertEquals([
|
|
||||||
'peak_connections_count' => 2,
|
|
||||||
'websocket_messages_count' => 2,
|
|
||||||
'api_messages_count' => 0,
|
|
||||||
'app_id' => '1234',
|
|
||||||
], $statistic->toArray());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_local_connections_for_private_channels()
|
public function test_local_connections_for_private_channels()
|
||||||
{
|
{
|
||||||
$this->newPrivateConnection('private-channel');
|
$this->newPrivateConnection('private-channel');
|
||||||
|
|
@ -227,145 +201,4 @@ class PrivateChannelTest extends TestCase
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_fires_the_event_to_private_channel()
|
|
||||||
{
|
|
||||||
$this->newPrivateConnection('private-channel');
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/events';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
|
||||||
'name' => 'some-event',
|
|
||||||
'channels' => ['private-channel'],
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(TriggerEvent::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame([], json_decode($response->getContent(), true));
|
|
||||||
|
|
||||||
$this->statisticsCollector
|
|
||||||
->getAppStatistics('1234')
|
|
||||||
->then(function ($statistic) {
|
|
||||||
$this->assertEquals([
|
|
||||||
'peak_connections_count' => 1,
|
|
||||||
'websocket_messages_count' => 1,
|
|
||||||
'api_messages_count' => 1,
|
|
||||||
'app_id' => '1234',
|
|
||||||
], $statistic->toArray());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_fires_event_across_servers_when_there_are_not_users_locally_for_private_channel()
|
|
||||||
{
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/events';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
|
||||||
'name' => 'some-event',
|
|
||||||
'channels' => ['private-channel'],
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(TriggerEvent::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame([], json_decode($response->getContent(), true));
|
|
||||||
|
|
||||||
if (method_exists($this->channelManager, 'getPublishClient')) {
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->assertCalledWithArgsCount(1, 'publish', [
|
|
||||||
$this->channelManager->getRedisKey('1234', 'private-channel'),
|
|
||||||
json_encode([
|
|
||||||
'event' => 'some-event',
|
|
||||||
'channel' => 'private-channel',
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
'appId' => '1234',
|
|
||||||
'socketId' => null,
|
|
||||||
'serverId' => $this->channelManager->getServerId(),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_fires_event_across_servers_when_there_are_users_locally_for_private_channel()
|
|
||||||
{
|
|
||||||
$wsConnection = $this->newPrivateConnection('private-channel');
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/events';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
|
||||||
'name' => 'some-event',
|
|
||||||
'channels' => ['private-channel'],
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(TriggerEvent::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame([], json_decode($response->getContent(), true));
|
|
||||||
|
|
||||||
if (method_exists($this->channelManager, 'getPublishClient')) {
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->assertCalledWithArgsCount(1, 'publish', [
|
|
||||||
$this->channelManager->getRedisKey('1234', 'private-channel'),
|
|
||||||
json_encode([
|
|
||||||
'event' => 'some-event',
|
|
||||||
'channel' => 'private-channel',
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
'appId' => '1234',
|
|
||||||
'socketId' => null,
|
|
||||||
'serverId' => $this->channelManager->getServerId(),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$wsConnection->assertSentEvent('some-event', [
|
|
||||||
'channel' => 'private-channel',
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,6 @@
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test;
|
namespace BlaxSoftware\LaravelWebSockets\Test;
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\API\TriggerEvent;
|
|
||||||
use GuzzleHttp\Psr7\Request;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Pusher\Pusher;
|
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
|
|
||||||
class PublicChannelTest extends TestCase
|
class PublicChannelTest extends TestCase
|
||||||
|
|
@ -21,7 +17,7 @@ class PublicChannelTest extends TestCase
|
||||||
});
|
});
|
||||||
|
|
||||||
$connection->assertSentEvent(
|
$connection->assertSentEvent(
|
||||||
'pusher.connection_established',
|
'websocket.connection_established',
|
||||||
[
|
[
|
||||||
'data' => json_encode([
|
'data' => json_encode([
|
||||||
'socket_id' => $connection->socketId,
|
'socket_id' => $connection->socketId,
|
||||||
|
|
@ -31,7 +27,7 @@ class PublicChannelTest extends TestCase
|
||||||
);
|
);
|
||||||
|
|
||||||
$connection->assertSentEvent(
|
$connection->assertSentEvent(
|
||||||
'pusher_internal:subscription_succeeded',
|
'websocket_internal.subscription_succeeded',
|
||||||
['channel' => 'public-channel']
|
['channel' => 'public-channel']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -47,13 +43,13 @@ class PublicChannelTest extends TestCase
|
||||||
});
|
});
|
||||||
|
|
||||||
$message = new Mocks\Message([
|
$message = new Mocks\Message([
|
||||||
'event' => 'pusher.unsubscribe',
|
'event' => 'websocket.unsubscribe',
|
||||||
'data' => [
|
'data' => [
|
||||||
'channel' => 'public-channel',
|
'channel' => 'public-channel',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
$this->wsHandler->onMessage($connection, $message);
|
||||||
|
|
||||||
$this->channelManager
|
$this->channelManager
|
||||||
->getGlobalConnectionsCount('1234', 'public-channel')
|
->getGlobalConnectionsCount('1234', 'public-channel')
|
||||||
|
|
@ -75,7 +71,7 @@ class PublicChannelTest extends TestCase
|
||||||
'channel' => 'public-channel',
|
'channel' => 'public-channel',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($rick, $message);
|
$this->wsHandler->onMessage($rick, $message);
|
||||||
|
|
||||||
$rick->assertNotSentEvent('client-test-whisper');
|
$rick->assertNotSentEvent('client-test-whisper');
|
||||||
$morty->assertSentEvent('client-test-whisper', ['data' => [], 'channel' => 'public-channel']);
|
$morty->assertSentEvent('client-test-whisper', ['data' => [], 'channel' => 'public-channel']);
|
||||||
|
|
@ -92,35 +88,12 @@ class PublicChannelTest extends TestCase
|
||||||
'channel' => 'public-channel',
|
'channel' => 'public-channel',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($rick, $message);
|
$this->wsHandler->onMessage($rick, $message);
|
||||||
|
|
||||||
$rick->assertNotSentEvent('client-test-whisper');
|
$rick->assertNotSentEvent('client-test-whisper');
|
||||||
$morty->assertNotSentEvent('client-test-whisper');
|
$morty->assertNotSentEvent('client-test-whisper');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_statistics_get_collected_for_public_channels()
|
|
||||||
{
|
|
||||||
$rick = $this->newActiveConnection(['public-channel']);
|
|
||||||
$morty = $this->newActiveConnection(['public-channel']);
|
|
||||||
|
|
||||||
$this->statisticsCollector
|
|
||||||
->getStatistics()
|
|
||||||
->then(function ($statistics) {
|
|
||||||
$this->assertCount(1, $statistics);
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->statisticsCollector
|
|
||||||
->getAppStatistics('1234')
|
|
||||||
->then(function ($statistic) {
|
|
||||||
$this->assertEquals([
|
|
||||||
'peak_connections_count' => 2,
|
|
||||||
'websocket_messages_count' => 2,
|
|
||||||
'api_messages_count' => 0,
|
|
||||||
'app_id' => '1234',
|
|
||||||
], $statistic->toArray());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_local_connections_for_public_channels()
|
public function test_local_connections_for_public_channels()
|
||||||
{
|
{
|
||||||
$this->newActiveConnection(['public-channel']);
|
$this->newActiveConnection(['public-channel']);
|
||||||
|
|
@ -207,133 +180,4 @@ class PublicChannelTest extends TestCase
|
||||||
$message->getPayload(),
|
$message->getPayload(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_fires_the_event_to_public_channel()
|
|
||||||
{
|
|
||||||
$this->startServer();
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/events';
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
|
||||||
'name' => 'some-event',
|
|
||||||
'channels' => ['public-channel'],
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
|
|
||||||
$response = $this->await($this->browser->post('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
|
||||||
|
|
||||||
$this->assertSame([], json_decode((string) $response->getBody(), true));
|
|
||||||
|
|
||||||
$this->statisticsCollector
|
|
||||||
->getAppStatistics('1234')
|
|
||||||
->then(function ($statistic) {
|
|
||||||
$this->assertEquals([
|
|
||||||
'peak_connections_count' => 0,
|
|
||||||
'websocket_messages_count' => 0,
|
|
||||||
'api_messages_count' => 1,
|
|
||||||
'app_id' => '1234',
|
|
||||||
], $statistic->toArray());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_fires_event_across_servers_when_there_are_not_users_locally_for_public_channel()
|
|
||||||
{
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/events';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
|
||||||
'name' => 'some-event',
|
|
||||||
'channels' => ['public-channel'],
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(TriggerEvent::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame([], json_decode($response->getContent(), true));
|
|
||||||
|
|
||||||
if (method_exists($this->channelManager, 'getPublishClient')) {
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->assertCalledWithArgsCount(1, 'publish', [
|
|
||||||
$this->channelManager->getRedisKey('1234', 'public-channel'),
|
|
||||||
json_encode([
|
|
||||||
'event' => 'some-event',
|
|
||||||
'channel' => 'public-channel',
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
'appId' => '1234',
|
|
||||||
'socketId' => null,
|
|
||||||
'serverId' => $this->channelManager->getServerId(),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_fires_event_across_servers_when_there_are_users_locally_for_public_channel()
|
|
||||||
{
|
|
||||||
$wsConnection = $this->newActiveConnection(['public-channel']);
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/events';
|
|
||||||
|
|
||||||
$routeParams = [
|
|
||||||
'appId' => '1234',
|
|
||||||
];
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
|
||||||
'TestKey', 'TestSecret', 'POST', $requestPath, [
|
|
||||||
'name' => 'some-event',
|
|
||||||
'channels' => ['public-channel'],
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
|
|
||||||
$request = new Request('POST', "{$requestPath}?{$queryString}&".http_build_query($routeParams));
|
|
||||||
|
|
||||||
$controller = app(TriggerEvent::class);
|
|
||||||
|
|
||||||
$controller->onOpen($connection, $request);
|
|
||||||
|
|
||||||
/** @var JsonResponse $response */
|
|
||||||
$response = array_pop($connection->sentRawData);
|
|
||||||
|
|
||||||
$this->assertSame([], json_decode($response->getContent(), true));
|
|
||||||
|
|
||||||
if (method_exists($this->channelManager, 'getPublishClient')) {
|
|
||||||
$this->channelManager
|
|
||||||
->getPublishClient()
|
|
||||||
->assertCalledWithArgsCount(1, 'publish', [
|
|
||||||
$this->channelManager->getRedisKey('1234', 'public-channel'),
|
|
||||||
json_encode([
|
|
||||||
'event' => 'some-event',
|
|
||||||
'channel' => 'public-channel',
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
'appId' => '1234',
|
|
||||||
'socketId' => null,
|
|
||||||
'serverId' => $this->channelManager->getServerId(),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$wsConnection->assertSentEvent('some-event', [
|
|
||||||
'channel' => 'public-channel',
|
|
||||||
'data' => json_encode(['some-data' => 'yes']),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class ReplicationTest extends TestCase
|
||||||
{
|
{
|
||||||
$connection = $this->newActiveConnection(['public-channel']);
|
$connection = $this->newActiveConnection(['public-channel']);
|
||||||
|
|
||||||
$this->pusherServer->onClose($connection);
|
$this->wsHandler->onClose($connection);
|
||||||
|
|
||||||
$this->getSubscribeClient()
|
$this->getSubscribeClient()
|
||||||
->assertCalledWithArgs('unsubscribe', [$this->channelManager->getRedisKey('1234')])
|
->assertCalledWithArgs('unsubscribe', [$this->channelManager->getRedisKey('1234')])
|
||||||
|
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test;
|
|
||||||
|
|
||||||
class StatisticsStoreTest extends TestCase
|
|
||||||
{
|
|
||||||
public function test_store_statistics_on_public_channel()
|
|
||||||
{
|
|
||||||
$rick = $this->newActiveConnection(['public-channel']);
|
|
||||||
$morty = $this->newActiveConnection(['public-channel']);
|
|
||||||
|
|
||||||
$this->statisticsCollector->save();
|
|
||||||
|
|
||||||
$this->assertCount(1, $records = $this->statisticsStore->getRecords());
|
|
||||||
|
|
||||||
$this->assertEquals('2', $records[0]['peak_connections_count']);
|
|
||||||
$this->assertEquals('2', $records[0]['websocket_messages_count']);
|
|
||||||
$this->assertEquals('0', $records[0]['api_messages_count']);
|
|
||||||
|
|
||||||
$this->pusherServer->onClose($rick);
|
|
||||||
$this->pusherServer->onClose($morty);
|
|
||||||
|
|
||||||
$this->statisticsCollector->save();
|
|
||||||
|
|
||||||
$this->assertCount(2, $records = $this->statisticsStore->getRecords());
|
|
||||||
|
|
||||||
$this->assertEquals('2', $records[1]['peak_connections_count']);
|
|
||||||
|
|
||||||
$this->statisticsCollector->save();
|
|
||||||
|
|
||||||
// The last one should not generate any more records
|
|
||||||
// since the current state is empty.
|
|
||||||
$this->assertCount(2, $records = $this->statisticsStore->getRecords());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_store_statistics_on_private_channel()
|
|
||||||
{
|
|
||||||
$rick = $this->newPrivateConnection('private-channel');
|
|
||||||
$morty = $this->newPrivateConnection('private-channel');
|
|
||||||
|
|
||||||
$this->statisticsCollector->save();
|
|
||||||
|
|
||||||
$this->assertCount(1, $records = $this->statisticsStore->getRecords());
|
|
||||||
|
|
||||||
$this->assertEquals('2', $records[0]['peak_connections_count']);
|
|
||||||
$this->assertEquals('2', $records[0]['websocket_messages_count']);
|
|
||||||
$this->assertEquals('0', $records[0]['api_messages_count']);
|
|
||||||
|
|
||||||
$this->pusherServer->onClose($rick);
|
|
||||||
$this->pusherServer->onClose($morty);
|
|
||||||
|
|
||||||
$this->statisticsCollector->save();
|
|
||||||
|
|
||||||
$this->assertCount(2, $records = $this->statisticsStore->getRecords());
|
|
||||||
|
|
||||||
$this->assertEquals('2', $records[1]['peak_connections_count']);
|
|
||||||
|
|
||||||
$this->statisticsCollector->save();
|
|
||||||
|
|
||||||
// The last one should not generate any more records
|
|
||||||
// since the current state is empty.
|
|
||||||
$this->assertCount(2, $records = $this->statisticsStore->getRecords());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_store_statistics_on_presence_channel()
|
|
||||||
{
|
|
||||||
$rick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]);
|
|
||||||
$morty = $this->newPresenceConnection('presence-channel', ['user_id' => 2]);
|
|
||||||
$pickleRick = $this->newPresenceConnection('presence-channel', ['user_id' => 1]);
|
|
||||||
|
|
||||||
$this->statisticsCollector->save();
|
|
||||||
|
|
||||||
$this->assertCount(1, $records = $this->statisticsStore->getRecords());
|
|
||||||
|
|
||||||
$this->assertEquals('3', $records[0]['peak_connections_count']);
|
|
||||||
$this->assertEquals('3', $records[0]['websocket_messages_count']);
|
|
||||||
$this->assertEquals('0', $records[0]['api_messages_count']);
|
|
||||||
|
|
||||||
$this->pusherServer->onClose($rick);
|
|
||||||
$this->pusherServer->onClose($morty);
|
|
||||||
$this->pusherServer->onClose($pickleRick);
|
|
||||||
|
|
||||||
$this->statisticsCollector->save();
|
|
||||||
|
|
||||||
$this->assertCount(2, $records = $this->statisticsStore->getRecords());
|
|
||||||
|
|
||||||
$this->assertEquals('3', $records[1]['peak_connections_count']);
|
|
||||||
|
|
||||||
$this->statisticsCollector->save();
|
|
||||||
|
|
||||||
// The last one should not generate any more records
|
|
||||||
// since the current state is empty.
|
|
||||||
$this->assertCount(2, $records = $this->statisticsStore->getRecords());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,8 +3,6 @@
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test;
|
namespace BlaxSoftware\LaravelWebSockets\Test;
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\StatisticsCollector;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Contracts\StatisticsStore;
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Facades\WebSocketRouter;
|
use BlaxSoftware\LaravelWebSockets\Facades\WebSocketRouter;
|
||||||
use BlaxSoftware\LaravelWebSockets\Helpers;
|
use BlaxSoftware\LaravelWebSockets\Helpers;
|
||||||
use BlaxSoftware\LaravelWebSockets\Server\Loggers\HttpLogger;
|
use BlaxSoftware\LaravelWebSockets\Server\Loggers\HttpLogger;
|
||||||
|
|
@ -42,11 +40,11 @@ abstract class TestCase extends Orchestra
|
||||||
protected $server;
|
protected $server;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A test Pusher server.
|
* The WebSocket handler under test.
|
||||||
*
|
*
|
||||||
* @var \BlaxSoftware\LaravelWebSockets\Server\WebSocketHandler
|
* @var \BlaxSoftware\LaravelWebSockets\Server\WebSocketHandler
|
||||||
*/
|
*/
|
||||||
protected $pusherServer;
|
protected $wsHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The test Channel manager.
|
* The test Channel manager.
|
||||||
|
|
@ -55,19 +53,7 @@ abstract class TestCase extends Orchestra
|
||||||
*/
|
*/
|
||||||
protected $channelManager;
|
protected $channelManager;
|
||||||
|
|
||||||
/**
|
|
||||||
* The test Channel manager.
|
|
||||||
*
|
|
||||||
* @var \BlaxSoftware\LaravelWebSockets\Contracts\StatisticsCollector
|
|
||||||
*/
|
|
||||||
protected $statisticsCollector;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The test Channel manager.
|
|
||||||
*
|
|
||||||
* @var \BlaxSoftware\LaravelWebSockets\Contracts\StatisticsStore
|
|
||||||
*/
|
|
||||||
protected $statisticsStore;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the loop instance.
|
* Get the loop instance.
|
||||||
|
|
@ -132,11 +118,7 @@ abstract class TestCase extends Orchestra
|
||||||
|
|
||||||
$this->registerManagers();
|
$this->registerManagers();
|
||||||
|
|
||||||
$this->registerStatisticsCollectors();
|
$this->wsHandler = $this->app->make(config('websockets.handlers.websocket'));
|
||||||
|
|
||||||
$this->registerStatisticsStores();
|
|
||||||
|
|
||||||
$this->pusherServer = $this->app->make(config('websockets.handlers.websocket'));
|
|
||||||
|
|
||||||
if ($this->replicationMode === 'redis') {
|
if ($this->replicationMode === 'redis') {
|
||||||
$this->registerRedis();
|
$this->registerRedis();
|
||||||
|
|
@ -268,12 +250,6 @@ abstract class TestCase extends Orchestra
|
||||||
$app['config']->set('websockets.replication.modes', [
|
$app['config']->set('websockets.replication.modes', [
|
||||||
'local' => [
|
'local' => [
|
||||||
'channel_manager' => \BlaxSoftware\LaravelWebSockets\ChannelManagers\LocalChannelManager::class,
|
'channel_manager' => \BlaxSoftware\LaravelWebSockets\ChannelManagers\LocalChannelManager::class,
|
||||||
'collector' => \BlaxSoftware\LaravelWebSockets\Statistics\Collectors\MemoryCollector::class,
|
|
||||||
],
|
|
||||||
'redis' => [
|
|
||||||
'channel_manager' => \BlaxSoftware\LaravelWebSockets\ChannelManagers\RedisChannelManager::class,
|
|
||||||
'connection' => 'default',
|
|
||||||
'collector' => \BlaxSoftware\LaravelWebSockets\Statistics\Collectors\RedisCollector::class,
|
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
@ -341,34 +317,7 @@ abstract class TestCase extends Orchestra
|
||||||
$this->app->offsetUnset(ChannelManager::class);
|
$this->app->offsetUnset(ChannelManager::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the statistics collectors.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function registerStatisticsCollectors()
|
|
||||||
{
|
|
||||||
$this->statisticsCollector = $this->app->make(StatisticsCollector::class);
|
|
||||||
|
|
||||||
$this->artisan('websockets:flush');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the statistics stores that are
|
|
||||||
* not resolved by the package service provider.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function registerStatisticsStores()
|
|
||||||
{
|
|
||||||
$this->app->singleton(StatisticsStore::class, function () {
|
|
||||||
$class = config('websockets.statistics.store');
|
|
||||||
|
|
||||||
return new $class;
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->statisticsStore = $this->app->make(StatisticsStore::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the Redis components for testing.
|
* Register the Redis components for testing.
|
||||||
|
|
@ -424,17 +373,17 @@ abstract class TestCase extends Orchestra
|
||||||
{
|
{
|
||||||
$connection = $this->newConnection($appKey, $headers);
|
$connection = $this->newConnection($appKey, $headers);
|
||||||
|
|
||||||
$this->pusherServer->onOpen($connection);
|
$this->wsHandler->onOpen($connection);
|
||||||
|
|
||||||
foreach ($channelsToJoin as $channel) {
|
foreach ($channelsToJoin as $channel) {
|
||||||
$message = new Mocks\Message([
|
$message = new Mocks\Message([
|
||||||
'event' => 'pusher.subscribe',
|
'event' => 'websocket.subscribe',
|
||||||
'data' => [
|
'data' => [
|
||||||
'channel' => $channel,
|
'channel' => $channel,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
$this->wsHandler->onMessage($connection, $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $connection;
|
return $connection;
|
||||||
|
|
@ -453,7 +402,7 @@ abstract class TestCase extends Orchestra
|
||||||
{
|
{
|
||||||
$connection = $this->newConnection($appKey, $headers);
|
$connection = $this->newConnection($appKey, $headers);
|
||||||
|
|
||||||
$this->pusherServer->onOpen($connection);
|
$this->wsHandler->onOpen($connection);
|
||||||
|
|
||||||
$user = $user ?: [
|
$user = $user ?: [
|
||||||
'user_id' => 1,
|
'user_id' => 1,
|
||||||
|
|
@ -463,14 +412,14 @@ abstract class TestCase extends Orchestra
|
||||||
$encodedUser = json_encode($user);
|
$encodedUser = json_encode($user);
|
||||||
|
|
||||||
$message = new Mocks\SignedMessage([
|
$message = new Mocks\SignedMessage([
|
||||||
'event' => 'pusher.subscribe',
|
'event' => 'websocket.subscribe',
|
||||||
'data' => [
|
'data' => [
|
||||||
'channel' => $channel,
|
'channel' => $channel,
|
||||||
'channel_data' => $encodedUser,
|
'channel_data' => $encodedUser,
|
||||||
],
|
],
|
||||||
], $connection, $channel, $encodedUser);
|
], $connection, $channel, $encodedUser);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
$this->wsHandler->onMessage($connection, $message);
|
||||||
|
|
||||||
return $connection;
|
return $connection;
|
||||||
}
|
}
|
||||||
|
|
@ -487,16 +436,16 @@ abstract class TestCase extends Orchestra
|
||||||
{
|
{
|
||||||
$connection = $this->newConnection($appKey, $headers);
|
$connection = $this->newConnection($appKey, $headers);
|
||||||
|
|
||||||
$this->pusherServer->onOpen($connection);
|
$this->wsHandler->onOpen($connection);
|
||||||
|
|
||||||
$message = new Mocks\SignedMessage([
|
$message = new Mocks\SignedMessage([
|
||||||
'event' => 'pusher.subscribe',
|
'event' => 'websocket.subscribe',
|
||||||
'data' => [
|
'data' => [
|
||||||
'channel' => $channel,
|
'channel' => $channel,
|
||||||
],
|
],
|
||||||
], $connection, $channel);
|
], $connection, $channel);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
$this->wsHandler->onMessage($connection, $message);
|
||||||
|
|
||||||
return $connection;
|
return $connection;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Test;
|
|
||||||
|
|
||||||
use Pusher\Pusher;
|
|
||||||
|
|
||||||
class TriggerEventTest extends TestCase
|
|
||||||
{
|
|
||||||
public function test_invalid_signatures_can_not_fire_the_event()
|
|
||||||
{
|
|
||||||
$this->startServer();
|
|
||||||
|
|
||||||
$connection = new Mocks\Connection;
|
|
||||||
|
|
||||||
$requestPath = '/apps/1234/events';
|
|
||||||
|
|
||||||
$queryString = http_build_query(Pusher::build_auth_query_params(
|
|
||||||
'TestKey', 'InvalidSecret', 'GET', $requestPath
|
|
||||||
));
|
|
||||||
|
|
||||||
$response = $this->await($this->browser->get('http://localhost:4000'."{$requestPath}?{$queryString}"));
|
|
||||||
|
|
||||||
$this->assertSame(405, $response->getStatusCode());
|
|
||||||
$this->assertSame('', $response->getBody()->getContents());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,11 +3,11 @@
|
||||||
namespace BlaxSoftware\LaravelWebSockets\Tests\Unit;
|
namespace BlaxSoftware\LaravelWebSockets\Tests\Unit;
|
||||||
|
|
||||||
use BlaxSoftware\LaravelWebSockets\Websocket\ControllerResolver;
|
use BlaxSoftware\LaravelWebSockets\Websocket\ControllerResolver;
|
||||||
use PHPUnit\Framework\TestCase;
|
use BlaxSoftware\LaravelWebSockets\Test\TestCase;
|
||||||
|
|
||||||
class ControllerResolverTest extends TestCase
|
class ControllerResolverTest extends TestCase
|
||||||
{
|
{
|
||||||
protected function setUp(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
ControllerResolver::clearCache();
|
ControllerResolver::clearCache();
|
||||||
|
|
@ -26,18 +26,24 @@ class ControllerResolverTest extends TestCase
|
||||||
/** @test */
|
/** @test */
|
||||||
public function it_caches_resolved_controllers()
|
public function it_caches_resolved_controllers()
|
||||||
{
|
{
|
||||||
|
// Disable hot_reload so caching is used
|
||||||
|
config()->set('websockets.hot_reload', false);
|
||||||
|
ControllerResolver::clearCache();
|
||||||
|
|
||||||
// First call resolves and caches
|
// First call resolves and caches
|
||||||
ControllerResolver::resolve('pusher');
|
ControllerResolver::resolve('pusher');
|
||||||
|
|
||||||
$stats = ControllerResolver::getStats();
|
$stats = ControllerResolver::getStats();
|
||||||
// scanned may or may not be true (we no longer auto-scan on resolve)
|
|
||||||
// but cached should be > 0
|
|
||||||
$this->assertGreaterThan(0, $stats['cached']);
|
$this->assertGreaterThan(0, $stats['cached']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function it_caches_null_for_nonexistent_controllers()
|
public function it_caches_null_for_nonexistent_controllers()
|
||||||
{
|
{
|
||||||
|
// Disable hot_reload so caching is used
|
||||||
|
config()->set('websockets.hot_reload', false);
|
||||||
|
ControllerResolver::clearCache();
|
||||||
|
|
||||||
// First call - not found
|
// First call - not found
|
||||||
$result1 = ControllerResolver::resolve('nonexistent-controller-xyz');
|
$result1 = ControllerResolver::resolve('nonexistent-controller-xyz');
|
||||||
$this->assertNull($result1);
|
$this->assertNull($result1);
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ class HandlerForkPathTest extends TestCase
|
||||||
|
|
||||||
// The handler should automatically use socket pair IPC
|
// The handler should automatically use socket pair IPC
|
||||||
// We can verify this by checking the handler was created successfully
|
// We can verify this by checking the handler was created successfully
|
||||||
$this->assertNotNull($this->pusherServer);
|
$this->assertNotNull($this->wsHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -54,18 +54,18 @@ class HandlerForkPathTest extends TestCase
|
||||||
$connection = $this->newActiveConnection(['fork-test-channel']);
|
$connection = $this->newActiveConnection(['fork-test-channel']);
|
||||||
|
|
||||||
// Verify connection was established (subscription event has pre-existing test issues)
|
// Verify connection was established (subscription event has pre-existing test issues)
|
||||||
$connection->assertSentEvent('pusher.connection_established');
|
$connection->assertSentEvent('websocket.connection_established');
|
||||||
|
|
||||||
// Now unsubscribe
|
// Now unsubscribe
|
||||||
$message = new Mocks\Message([
|
$message = new Mocks\Message([
|
||||||
'event' => 'pusher:unsubscribe',
|
'event' => 'websocket.unsubscribe',
|
||||||
'data' => ['channel' => 'fork-test-channel'],
|
'data' => ['channel' => 'fork-test-channel'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
$this->wsHandler->onMessage($connection, $message);
|
||||||
|
|
||||||
// No error should be sent
|
// No error should be sent
|
||||||
$connection->assertNotSentEvent('pusher:unsubscribe:error');
|
$connection->assertNotSentEvent('websocket.unsubscribe:error');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -82,7 +82,7 @@ class HandlerForkPathTest extends TestCase
|
||||||
'channel' => 'channel-two', // Not subscribed!
|
'channel' => 'channel-two', // Not subscribed!
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
$this->wsHandler->onMessage($connection, $message);
|
||||||
|
|
||||||
// Should receive an error event
|
// Should receive an error event
|
||||||
$connection->assertSentEvent('custom.action:error');
|
$connection->assertSentEvent('custom.action:error');
|
||||||
|
|
@ -99,17 +99,17 @@ class HandlerForkPathTest extends TestCase
|
||||||
for ($i = 0; $i < 5; $i++) {
|
for ($i = 0; $i < 5; $i++) {
|
||||||
// Unsubscribe
|
// Unsubscribe
|
||||||
$unsubMsg = new Mocks\Message([
|
$unsubMsg = new Mocks\Message([
|
||||||
'event' => 'pusher:unsubscribe',
|
'event' => 'websocket.unsubscribe',
|
||||||
'data' => ['channel' => 'cycle-channel'],
|
'data' => ['channel' => 'cycle-channel'],
|
||||||
]);
|
]);
|
||||||
$this->pusherServer->onMessage($connection, $unsubMsg);
|
$this->wsHandler->onMessage($connection, $unsubMsg);
|
||||||
|
|
||||||
// Resubscribe
|
// Resubscribe
|
||||||
$subMsg = new Mocks\Message([
|
$subMsg = new Mocks\Message([
|
||||||
'event' => 'pusher:subscribe',
|
'event' => 'websocket.subscribe',
|
||||||
'data' => ['channel' => 'cycle-channel'],
|
'data' => ['channel' => 'cycle-channel'],
|
||||||
]);
|
]);
|
||||||
$this->pusherServer->onMessage($connection, $subMsg);
|
$this->wsHandler->onMessage($connection, $subMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No errors should have been sent
|
// No errors should have been sent
|
||||||
|
|
@ -149,7 +149,7 @@ class HandlerForkPathTest extends TestCase
|
||||||
'channel' => 'empty-data-channel',
|
'channel' => 'empty-data-channel',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($sender, $message);
|
$this->wsHandler->onMessage($sender, $message);
|
||||||
|
|
||||||
$receiver->assertSentEvent('client-empty', [
|
$receiver->assertSentEvent('client-empty', [
|
||||||
'data' => [],
|
'data' => [],
|
||||||
|
|
@ -172,19 +172,19 @@ class HandlerForkPathTest extends TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test pusher: prefixed events receive response.
|
* Test that ping/pong works correctly.
|
||||||
*/
|
*/
|
||||||
public function test_pusher_prefixed_events_handled()
|
public function test_ping_pong_handled()
|
||||||
{
|
{
|
||||||
$connection = $this->newActiveConnection(['pusher-event-channel']);
|
$connection = $this->newActiveConnection(['pusher-event-channel']);
|
||||||
|
|
||||||
// Ping should work
|
// Ping should work
|
||||||
$pingMsg = new Mocks\Message([
|
$pingMsg = new Mocks\Message([
|
||||||
'event' => 'pusher.ping',
|
'event' => 'websocket.ping',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, $pingMsg);
|
$this->wsHandler->onMessage($connection, $pingMsg);
|
||||||
$connection->assertSentEvent('pusher.pong');
|
$connection->assertSentEvent('websocket.pong');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -204,7 +204,7 @@ class HandlerForkPathTest extends TestCase
|
||||||
'channel' => 'no-whisper-channel',
|
'channel' => 'no-whisper-channel',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($sender, $message);
|
$this->wsHandler->onMessage($sender, $message);
|
||||||
|
|
||||||
// Neither should receive (whisper blocked)
|
// Neither should receive (whisper blocked)
|
||||||
$sender->assertNotSentEvent('client-blocked');
|
$sender->assertNotSentEvent('client-blocked');
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use BlaxSoftware\LaravelWebSockets\Test\TestCase;
|
||||||
*
|
*
|
||||||
* These tests verify that the complete message flow works correctly when using
|
* These tests verify that the complete message flow works correctly when using
|
||||||
* the event-driven socket pair IPC mechanism. Unlike the isolated IPC tests,
|
* the event-driven socket pair IPC mechanism. Unlike the isolated IPC tests,
|
||||||
* these tests use the actual Handler with pusherServer->onMessage().
|
* these tests use the actual Handler with wsHandler->onMessage().
|
||||||
*
|
*
|
||||||
* Note: These tests require pcntl_fork() and socket_create_pair() to be available.
|
* Note: These tests require pcntl_fork() and socket_create_pair() to be available.
|
||||||
*/
|
*/
|
||||||
|
|
@ -41,14 +41,14 @@ class HandlerSocketPairIntegrationTest extends TestCase
|
||||||
$connection = $this->newActiveConnection(['public-channel']);
|
$connection = $this->newActiveConnection(['public-channel']);
|
||||||
|
|
||||||
$message = new Mocks\Message([
|
$message = new Mocks\Message([
|
||||||
'event' => 'pusher.ping',
|
'event' => 'websocket.ping',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$startTime = hrtime(true);
|
$startTime = hrtime(true);
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
$this->wsHandler->onMessage($connection, $message);
|
||||||
$elapsed = (hrtime(true) - $startTime) / 1_000_000; // ms
|
$elapsed = (hrtime(true) - $startTime) / 1_000_000; // ms
|
||||||
|
|
||||||
$connection->assertSentEvent('pusher.pong');
|
$connection->assertSentEvent('websocket.pong');
|
||||||
|
|
||||||
// Fast path should be very fast (< 15ms typically)
|
// Fast path should be very fast (< 15ms typically)
|
||||||
$this->assertLessThan(15, $elapsed, "Ping/pong took {$elapsed}ms - should be < 15ms for fast path");
|
$this->assertLessThan(15, $elapsed, "Ping/pong took {$elapsed}ms - should be < 15ms for fast path");
|
||||||
|
|
@ -71,7 +71,7 @@ class HandlerSocketPairIntegrationTest extends TestCase
|
||||||
'channel' => 'test-channel',
|
'channel' => 'test-channel',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($sender, $message);
|
$this->wsHandler->onMessage($sender, $message);
|
||||||
|
|
||||||
// Sender should NOT receive their own whisper
|
// Sender should NOT receive their own whisper
|
||||||
$sender->assertNotSentEvent('client-test-event');
|
$sender->assertNotSentEvent('client-test-event');
|
||||||
|
|
@ -85,7 +85,7 @@ class HandlerSocketPairIntegrationTest extends TestCase
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test channel subscription sends connection established event.
|
* Test channel subscription sends connection established event.
|
||||||
* Note: The pusher_internal:subscription_succeeded event has pre-existing
|
* Note: The websocket_internal.subscription_succeeded event has pre-existing
|
||||||
* issues in the test framework (channel->hasConnection check).
|
* issues in the test framework (channel->hasConnection check).
|
||||||
*/
|
*/
|
||||||
public function test_channel_connection_established()
|
public function test_channel_connection_established()
|
||||||
|
|
@ -93,7 +93,7 @@ class HandlerSocketPairIntegrationTest extends TestCase
|
||||||
$connection = $this->newActiveConnection(['my-channel']);
|
$connection = $this->newActiveConnection(['my-channel']);
|
||||||
|
|
||||||
// Verify connection established was sent (this always works)
|
// Verify connection established was sent (this always works)
|
||||||
$connection->assertSentEvent('pusher.connection_established');
|
$connection->assertSentEvent('websocket.connection_established');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -113,7 +113,7 @@ class HandlerSocketPairIntegrationTest extends TestCase
|
||||||
'channel' => 'broadcast-channel',
|
'channel' => 'broadcast-channel',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($alice, $message);
|
$this->wsHandler->onMessage($alice, $message);
|
||||||
|
|
||||||
// Alice (sender) should NOT receive
|
// Alice (sender) should NOT receive
|
||||||
$alice->assertNotSentEvent('client-hello');
|
$alice->assertNotSentEvent('client-hello');
|
||||||
|
|
@ -136,7 +136,7 @@ class HandlerSocketPairIntegrationTest extends TestCase
|
||||||
{
|
{
|
||||||
$connection = $this->newActiveConnection(['test-channel']);
|
$connection = $this->newActiveConnection(['test-channel']);
|
||||||
|
|
||||||
$connection->assertSentEvent('pusher.connection_established', [
|
$connection->assertSentEvent('websocket.connection_established', [
|
||||||
'data' => json_encode([
|
'data' => json_encode([
|
||||||
'socket_id' => $connection->socketId,
|
'socket_id' => $connection->socketId,
|
||||||
'activity_timeout' => 30,
|
'activity_timeout' => 30,
|
||||||
|
|
@ -146,7 +146,7 @@ class HandlerSocketPairIntegrationTest extends TestCase
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test subscribing to multiple channels via separate connections.
|
* Test subscribing to multiple channels via separate connections.
|
||||||
* Note: pusher_internal:subscription_succeeded has pre-existing test issues.
|
* Note: websocket_internal.subscription_succeeded has pre-existing test issues.
|
||||||
*/
|
*/
|
||||||
public function test_subscribe_to_multiple_channels_separately()
|
public function test_subscribe_to_multiple_channels_separately()
|
||||||
{
|
{
|
||||||
|
|
@ -156,9 +156,9 @@ class HandlerSocketPairIntegrationTest extends TestCase
|
||||||
$connC = $this->newActiveConnection(['channel-c']);
|
$connC = $this->newActiveConnection(['channel-c']);
|
||||||
|
|
||||||
// Each should have received connection established
|
// Each should have received connection established
|
||||||
$connA->assertSentEvent('pusher.connection_established');
|
$connA->assertSentEvent('websocket.connection_established');
|
||||||
$connB->assertSentEvent('pusher.connection_established');
|
$connB->assertSentEvent('websocket.connection_established');
|
||||||
$connC->assertSentEvent('pusher.connection_established');
|
$connC->assertSentEvent('websocket.connection_established');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -178,7 +178,7 @@ class HandlerSocketPairIntegrationTest extends TestCase
|
||||||
'data' => ['count' => $i],
|
'data' => ['count' => $i],
|
||||||
'channel' => 'rapid-channel',
|
'channel' => 'rapid-channel',
|
||||||
]);
|
]);
|
||||||
$this->pusherServer->onMessage($sender, $message);
|
$this->wsHandler->onMessage($sender, $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// At least one message should be received by receiver
|
// At least one message should be received by receiver
|
||||||
|
|
@ -199,7 +199,7 @@ class HandlerSocketPairIntegrationTest extends TestCase
|
||||||
|
|
||||||
// This should not throw an exception - should handle gracefully
|
// This should not throw an exception - should handle gracefully
|
||||||
try {
|
try {
|
||||||
$this->pusherServer->onMessage($connection, $message);
|
$this->wsHandler->onMessage($connection, $message);
|
||||||
} catch (\JsonException $e) {
|
} catch (\JsonException $e) {
|
||||||
// Expected - Handler may throw JsonException for invalid JSON
|
// Expected - Handler may throw JsonException for invalid JSON
|
||||||
$this->assertTrue(true);
|
$this->assertTrue(true);
|
||||||
|
|
@ -227,7 +227,7 @@ class HandlerSocketPairIntegrationTest extends TestCase
|
||||||
'channel' => 'channel-A',
|
'channel' => 'channel-A',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->pusherServer->onMessage($channelA_User1, $message);
|
$this->wsHandler->onMessage($channelA_User1, $message);
|
||||||
|
|
||||||
// Only channel-A users should receive
|
// Only channel-A users should receive
|
||||||
$channelA_User2->assertSentEvent('client-isolated');
|
$channelA_User2->assertSentEvent('client-isolated');
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,12 @@ use Carbon\Carbon;
|
||||||
*
|
*
|
||||||
* Tests mirror the real-life frontend (Websocket.client.ts) behavior:
|
* Tests mirror the real-life frontend (Websocket.client.ts) behavior:
|
||||||
* - Default channel: 'websocket' (public channel, the production default)
|
* - Default channel: 'websocket' (public channel, the production default)
|
||||||
* - Heartbeat: raw socket.send({ event: 'pusher.ping', data: {} }) every 20s
|
* - Heartbeat: raw socket.send({ event: 'websocket.ping', data: {} }) every 20s
|
||||||
* - Subscribe: pusher.subscribe with channel in data (dot format)
|
* - Subscribe: websocket.subscribe with channel in data (dot format)
|
||||||
* - Unsubscribe: pusher.unsubscribe (dot format — server only recognizes dots)
|
* - Unsubscribe: websocket.unsubscribe (dot format)
|
||||||
* - Error recovery: "Subscription not established" → re-subscribe → retry
|
* - Error recovery: "Subscription not established" → re-subscribe → retry
|
||||||
* - Pusher events get :response suffix from handlePusherEvent()
|
* - Protocol events get :response suffix from handleProtocolEvent()
|
||||||
|
* - Server accepts any prefix (websocket.*, pusher.*, pusher:*, etc.)
|
||||||
*
|
*
|
||||||
* Groups (run subsets with --group / --exclude-group):
|
* Groups (run subsets with --group / --exclude-group):
|
||||||
* @group stability — Real-time tests using event loop timers (4+ minutes)
|
* @group stability — Real-time tests using event loop timers (4+ minutes)
|
||||||
|
|
@ -50,15 +51,15 @@ class HandlerStabilityTest extends TestCase
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$this->pingMsg = new Mocks\Message([
|
$this->pingMsg = new Mocks\Message([
|
||||||
'event' => 'pusher.ping',
|
'event' => 'websocket.ping',
|
||||||
'data' => new \stdClass(),
|
'data' => new \stdClass(),
|
||||||
]);
|
]);
|
||||||
$this->subMsg = new Mocks\Message([
|
$this->subMsg = new Mocks\Message([
|
||||||
'event' => 'pusher.subscribe',
|
'event' => 'websocket.subscribe',
|
||||||
'data' => ['channel' => 'websocket'],
|
'data' => ['channel' => 'websocket'],
|
||||||
]);
|
]);
|
||||||
$this->unsubMsg = new Mocks\Message([
|
$this->unsubMsg = new Mocks\Message([
|
||||||
'event' => 'pusher.unsubscribe',
|
'event' => 'websocket.unsubscribe',
|
||||||
'data' => ['channel' => 'websocket'],
|
'data' => ['channel' => 'websocket'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
@ -87,8 +88,8 @@ class HandlerStabilityTest extends TestCase
|
||||||
$this->runOnlyOnLocalReplication();
|
$this->runOnlyOnLocalReplication();
|
||||||
|
|
||||||
$connection = $this->newActiveConnection(['websocket']);
|
$connection = $this->newActiveConnection(['websocket']);
|
||||||
$connection->assertSentEvent('pusher.connection_established');
|
$connection->assertSentEvent('websocket.connection_established');
|
||||||
$connection->assertSentEvent('pusher_internal:subscription_succeeded');
|
$connection->assertSentEvent('websocket_internal.subscription_succeeded');
|
||||||
$connection->resetEvents();
|
$connection->resetEvents();
|
||||||
|
|
||||||
$pingsSent = 0;
|
$pingsSent = 0;
|
||||||
|
|
@ -126,10 +127,10 @@ class HandlerStabilityTest extends TestCase
|
||||||
// Client heartbeat every 20s
|
// Client heartbeat every 20s
|
||||||
if ($now >= $nextPing) {
|
if ($now >= $nextPing) {
|
||||||
$connection->resetEvents();
|
$connection->resetEvents();
|
||||||
$this->pusherServer->onMessage($connection, $this->pingMsg);
|
$this->wsHandler->onMessage($connection, $this->pingMsg);
|
||||||
$pingsSent++;
|
$pingsSent++;
|
||||||
|
|
||||||
$pong = collect($connection->sentData)->firstWhere('event', 'pusher.pong');
|
$pong = collect($connection->sentData)->firstWhere('event', 'websocket.pong');
|
||||||
$this->assertNotNull($pong, "Ping #{$pingsSent} at ~" . ($pingsSent * 20) . "s should get pong");
|
$this->assertNotNull($pong, "Ping #{$pingsSent} at ~" . ($pingsSent * 20) . "s should get pong");
|
||||||
$pongsSeen++;
|
$pongsSeen++;
|
||||||
$nextPing = $now + 20;
|
$nextPing = $now + 20;
|
||||||
|
|
@ -182,7 +183,7 @@ class HandlerStabilityTest extends TestCase
|
||||||
for ($cycle = 0; $cycle < 8; $cycle++) {
|
for ($cycle = 0; $cycle < 8; $cycle++) {
|
||||||
$activeConnection->lastPongedAt = Carbon::now();
|
$activeConnection->lastPongedAt = Carbon::now();
|
||||||
$this->channelManager->updateConnectionInChannels($activeConnection);
|
$this->channelManager->updateConnectionInChannels($activeConnection);
|
||||||
$this->pusherServer->onMessage($activeConnection, $this->pingMsg);
|
$this->wsHandler->onMessage($activeConnection, $this->pingMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stale: >120s without pong
|
// Stale: >120s without pong
|
||||||
|
|
@ -215,7 +216,7 @@ class HandlerStabilityTest extends TestCase
|
||||||
public function test_connection_stable_under_message_bombardment()
|
public function test_connection_stable_under_message_bombardment()
|
||||||
{
|
{
|
||||||
$connection = $this->newActiveConnection(['websocket']);
|
$connection = $this->newActiveConnection(['websocket']);
|
||||||
$connection->assertSentEvent('pusher.connection_established');
|
$connection->assertSentEvent('websocket.connection_established');
|
||||||
$connection->resetEvents();
|
$connection->resetEvents();
|
||||||
|
|
||||||
// Phase 1: 10s of rapid pings (tryHandlePingFast hot path)
|
// Phase 1: 10s of rapid pings (tryHandlePingFast hot path)
|
||||||
|
|
@ -225,7 +226,7 @@ class HandlerStabilityTest extends TestCase
|
||||||
|
|
||||||
while (microtime(true) - $phaseStart < 10) {
|
while (microtime(true) - $phaseStart < 10) {
|
||||||
for ($batch = 0; $batch < 50; $batch++) {
|
for ($batch = 0; $batch < 50; $batch++) {
|
||||||
$this->pusherServer->onMessage($connection, $this->pingMsg);
|
$this->wsHandler->onMessage($connection, $this->pingMsg);
|
||||||
$totalPings++;
|
$totalPings++;
|
||||||
}
|
}
|
||||||
$totalPongs += count($connection->sentData);
|
$totalPongs += count($connection->sentData);
|
||||||
|
|
@ -242,8 +243,8 @@ class HandlerStabilityTest extends TestCase
|
||||||
$subUnsubCycles = 0;
|
$subUnsubCycles = 0;
|
||||||
|
|
||||||
while (microtime(true) - $phaseStart < 10) {
|
while (microtime(true) - $phaseStart < 10) {
|
||||||
$this->pusherServer->onMessage($connection, $this->unsubMsg);
|
$this->wsHandler->onMessage($connection, $this->unsubMsg);
|
||||||
$this->pusherServer->onMessage($connection, $this->subMsg);
|
$this->wsHandler->onMessage($connection, $this->subMsg);
|
||||||
$subUnsubCycles++;
|
$subUnsubCycles++;
|
||||||
if ($subUnsubCycles % 25 === 0) {
|
if ($subUnsubCycles % 25 === 0) {
|
||||||
$connection->resetEvents();
|
$connection->resetEvents();
|
||||||
|
|
@ -265,15 +266,15 @@ class HandlerStabilityTest extends TestCase
|
||||||
$mixedPongs = 0;
|
$mixedPongs = 0;
|
||||||
|
|
||||||
while (microtime(true) - $phaseStart < 10) {
|
while (microtime(true) - $phaseStart < 10) {
|
||||||
$this->pusherServer->onMessage($connection, $this->pingMsg);
|
$this->wsHandler->onMessage($connection, $this->pingMsg);
|
||||||
$mixedPings++;
|
$mixedPings++;
|
||||||
$this->pusherServer->onMessage($connection, $this->subMsg);
|
$this->wsHandler->onMessage($connection, $this->subMsg);
|
||||||
$this->pusherServer->onMessage($connection, $this->unsubMsg);
|
$this->wsHandler->onMessage($connection, $this->unsubMsg);
|
||||||
$this->pusherServer->onMessage($connection, $this->subMsg);
|
$this->wsHandler->onMessage($connection, $this->subMsg);
|
||||||
$mixedCount++;
|
$mixedCount++;
|
||||||
|
|
||||||
if ($mixedCount % 10 === 0) {
|
if ($mixedCount % 10 === 0) {
|
||||||
$mixedPongs += collect($connection->sentData)->where('event', 'pusher.pong')->count();
|
$mixedPongs += collect($connection->sentData)->where('event', 'websocket.pong')->count();
|
||||||
|
|
||||||
$errors = collect($connection->sentData)->filter(
|
$errors = collect($connection->sentData)->filter(
|
||||||
fn($e) =>
|
fn($e) =>
|
||||||
|
|
@ -285,14 +286,14 @@ class HandlerStabilityTest extends TestCase
|
||||||
gc_collect_cycles();
|
gc_collect_cycles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$mixedPongs += collect($connection->sentData)->where('event', 'pusher.pong')->count();
|
$mixedPongs += collect($connection->sentData)->where('event', 'websocket.pong')->count();
|
||||||
$this->assertEquals($mixedPings, $mixedPongs, 'Phase 3: All pings should produce pongs');
|
$this->assertEquals($mixedPings, $mixedPongs, 'Phase 3: All pings should produce pongs');
|
||||||
$this->assertGreaterThan(500, $mixedCount, 'Phase 3: Should process substantial mixed volume');
|
$this->assertGreaterThan(500, $mixedCount, 'Phase 3: Should process substantial mixed volume');
|
||||||
|
|
||||||
// Final: connection still alive
|
// Final: connection still alive
|
||||||
$connection->resetEvents();
|
$connection->resetEvents();
|
||||||
$this->pusherServer->onMessage($connection, $this->pingMsg);
|
$this->wsHandler->onMessage($connection, $this->pingMsg);
|
||||||
$connection->assertSentEvent('pusher.pong');
|
$connection->assertSentEvent('websocket.pong');
|
||||||
|
|
||||||
$channel = $this->channelManager->find('1234', 'websocket');
|
$channel = $this->channelManager->find('1234', 'websocket');
|
||||||
$this->assertTrue($channel->hasConnection($connection), 'Connection must survive 30s bombardment');
|
$this->assertTrue($channel->hasConnection($connection), 'Connection must survive 30s bombardment');
|
||||||
|
|
@ -314,7 +315,7 @@ class HandlerStabilityTest extends TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($connections as $conn) {
|
foreach ($connections as $conn) {
|
||||||
$conn->assertSentEvent('pusher.connection_established');
|
$conn->assertSentEvent('websocket.connection_established');
|
||||||
$conn->resetEvents();
|
$conn->resetEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -324,7 +325,7 @@ class HandlerStabilityTest extends TestCase
|
||||||
|
|
||||||
while (microtime(true) - $start < 10) {
|
while (microtime(true) - $start < 10) {
|
||||||
foreach ($connections as $conn) {
|
foreach ($connections as $conn) {
|
||||||
$this->pusherServer->onMessage($conn, $this->pingMsg);
|
$this->wsHandler->onMessage($conn, $this->pingMsg);
|
||||||
$totalPings++;
|
$totalPings++;
|
||||||
}
|
}
|
||||||
// Flush all connections to prevent OOM
|
// Flush all connections to prevent OOM
|
||||||
|
|
@ -345,7 +346,7 @@ class HandlerStabilityTest extends TestCase
|
||||||
|
|
||||||
// Close first 50
|
// Close first 50
|
||||||
for ($i = 0; $i < 50; $i++) {
|
for ($i = 0; $i < 50; $i++) {
|
||||||
$this->pusherServer->onClose($connections[$i]);
|
$this->wsHandler->onClose($connections[$i]);
|
||||||
}
|
}
|
||||||
gc_collect_cycles();
|
gc_collect_cycles();
|
||||||
|
|
||||||
|
|
@ -356,7 +357,7 @@ class HandlerStabilityTest extends TestCase
|
||||||
|
|
||||||
while (microtime(true) - $start2 < 5) {
|
while (microtime(true) - $start2 < 5) {
|
||||||
foreach ($remaining as $conn) {
|
foreach ($remaining as $conn) {
|
||||||
$this->pusherServer->onMessage($conn, $this->pingMsg);
|
$this->wsHandler->onMessage($conn, $this->pingMsg);
|
||||||
$phase2Pings++;
|
$phase2Pings++;
|
||||||
}
|
}
|
||||||
foreach ($remaining as $conn) {
|
foreach ($remaining as $conn) {
|
||||||
|
|
@ -403,7 +404,7 @@ class HandlerStabilityTest extends TestCase
|
||||||
|
|
||||||
while (microtime(true) - $start < 10) {
|
while (microtime(true) - $start < 10) {
|
||||||
foreach ($allConnections as $conn) {
|
foreach ($allConnections as $conn) {
|
||||||
$this->pusherServer->onMessage($conn, $this->pingMsg);
|
$this->wsHandler->onMessage($conn, $this->pingMsg);
|
||||||
$totalPings++;
|
$totalPings++;
|
||||||
}
|
}
|
||||||
foreach ($allConnections as $conn) {
|
foreach ($allConnections as $conn) {
|
||||||
|
|
@ -416,7 +417,7 @@ class HandlerStabilityTest extends TestCase
|
||||||
|
|
||||||
// Close all on 'blog' channel
|
// Close all on 'blog' channel
|
||||||
foreach ($connections['blog'] as $conn) {
|
foreach ($connections['blog'] as $conn) {
|
||||||
$this->pusherServer->onClose($conn);
|
$this->wsHandler->onClose($conn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Other 4 channels fully operational — verify with ping
|
// Other 4 channels fully operational — verify with ping
|
||||||
|
|
@ -425,8 +426,8 @@ class HandlerStabilityTest extends TestCase
|
||||||
$this->assertNotNull($channel, "{$channelName} should still exist");
|
$this->assertNotNull($channel, "{$channelName} should still exist");
|
||||||
foreach ($connections[$channelName] as $idx => $conn) {
|
foreach ($connections[$channelName] as $idx => $conn) {
|
||||||
$conn->resetEvents();
|
$conn->resetEvents();
|
||||||
$this->pusherServer->onMessage($conn, $this->pingMsg);
|
$this->wsHandler->onMessage($conn, $this->pingMsg);
|
||||||
$conn->assertSentEvent('pusher.pong');
|
$conn->assertSentEvent('websocket.pong');
|
||||||
$this->assertTrue(
|
$this->assertTrue(
|
||||||
$channel->hasConnection($conn),
|
$channel->hasConnection($conn),
|
||||||
"{$channelName} conn #{$idx} should be subscribed"
|
"{$channelName} conn #{$idx} should be subscribed"
|
||||||
|
|
@ -445,7 +446,7 @@ class HandlerStabilityTest extends TestCase
|
||||||
public function test_rapid_connect_disconnect_cycles()
|
public function test_rapid_connect_disconnect_cycles()
|
||||||
{
|
{
|
||||||
$permanentConnection = $this->newActiveConnection(['websocket']);
|
$permanentConnection = $this->newActiveConnection(['websocket']);
|
||||||
$permanentConnection->assertSentEvent('pusher.connection_established');
|
$permanentConnection->assertSentEvent('websocket.connection_established');
|
||||||
$permanentConnection->resetEvents();
|
$permanentConnection->resetEvents();
|
||||||
|
|
||||||
$start = microtime(true);
|
$start = microtime(true);
|
||||||
|
|
@ -453,13 +454,13 @@ class HandlerStabilityTest extends TestCase
|
||||||
|
|
||||||
while (microtime(true) - $start < 15) {
|
while (microtime(true) - $start < 15) {
|
||||||
$temp = $this->newActiveConnection(['websocket']);
|
$temp = $this->newActiveConnection(['websocket']);
|
||||||
$this->pusherServer->onClose($temp);
|
$this->wsHandler->onClose($temp);
|
||||||
$cycles++;
|
$cycles++;
|
||||||
|
|
||||||
// Every 100 cycles, verify permanent connection is alive
|
// Every 100 cycles, verify permanent connection is alive
|
||||||
if ($cycles % 100 === 0) {
|
if ($cycles % 100 === 0) {
|
||||||
$this->pusherServer->onMessage($permanentConnection, $this->pingMsg);
|
$this->wsHandler->onMessage($permanentConnection, $this->pingMsg);
|
||||||
$permanentConnection->assertSentEvent('pusher.pong');
|
$permanentConnection->assertSentEvent('websocket.pong');
|
||||||
$permanentConnection->resetEvents();
|
$permanentConnection->resetEvents();
|
||||||
gc_collect_cycles();
|
gc_collect_cycles();
|
||||||
}
|
}
|
||||||
|
|
@ -469,8 +470,8 @@ class HandlerStabilityTest extends TestCase
|
||||||
|
|
||||||
// Final verification
|
// Final verification
|
||||||
$permanentConnection->resetEvents();
|
$permanentConnection->resetEvents();
|
||||||
$this->pusherServer->onMessage($permanentConnection, $this->pingMsg);
|
$this->wsHandler->onMessage($permanentConnection, $this->pingMsg);
|
||||||
$permanentConnection->assertSentEvent('pusher.pong');
|
$permanentConnection->assertSentEvent('websocket.pong');
|
||||||
|
|
||||||
$channel = $this->channelManager->find('1234', 'websocket');
|
$channel = $this->channelManager->find('1234', 'websocket');
|
||||||
$this->assertNotNull($channel);
|
$this->assertNotNull($channel);
|
||||||
|
|
@ -494,7 +495,7 @@ class HandlerStabilityTest extends TestCase
|
||||||
$bad = $this->newActiveConnection(['websocket']);
|
$bad = $this->newActiveConnection(['websocket']);
|
||||||
|
|
||||||
$bad->resetEvents();
|
$bad->resetEvents();
|
||||||
$this->pusherServer->onMessage($bad, new Mocks\Message([
|
$this->wsHandler->onMessage($bad, new Mocks\Message([
|
||||||
'event' => 'blog.show[abc123]',
|
'event' => 'blog.show[abc123]',
|
||||||
'data' => ['id' => '123'],
|
'data' => ['id' => '123'],
|
||||||
'channel' => 'nonexistent-channel',
|
'channel' => 'nonexistent-channel',
|
||||||
|
|
@ -503,16 +504,16 @@ class HandlerStabilityTest extends TestCase
|
||||||
|
|
||||||
$good1->resetEvents();
|
$good1->resetEvents();
|
||||||
$good2->resetEvents();
|
$good2->resetEvents();
|
||||||
$this->pusherServer->onMessage($good1, new Mocks\Message([
|
$this->wsHandler->onMessage($good1, new Mocks\Message([
|
||||||
'event' => 'pusher.ping',
|
'event' => 'websocket.ping',
|
||||||
'data' => new \stdClass(),
|
'data' => new \stdClass(),
|
||||||
]));
|
]));
|
||||||
$this->pusherServer->onMessage($good2, new Mocks\Message([
|
$this->wsHandler->onMessage($good2, new Mocks\Message([
|
||||||
'event' => 'pusher.ping',
|
'event' => 'websocket.ping',
|
||||||
'data' => new \stdClass(),
|
'data' => new \stdClass(),
|
||||||
]));
|
]));
|
||||||
$good1->assertSentEvent('pusher.pong');
|
$good1->assertSentEvent('websocket.pong');
|
||||||
$good2->assertSentEvent('pusher.pong');
|
$good2->assertSentEvent('websocket.pong');
|
||||||
|
|
||||||
$channel = $this->channelManager->find('1234', 'websocket');
|
$channel = $this->channelManager->find('1234', 'websocket');
|
||||||
$this->assertTrue($channel->hasConnection($good1));
|
$this->assertTrue($channel->hasConnection($good1));
|
||||||
|
|
@ -528,16 +529,17 @@ class HandlerStabilityTest extends TestCase
|
||||||
public function test_subscription_not_established_error_is_recoverable()
|
public function test_subscription_not_established_error_is_recoverable()
|
||||||
{
|
{
|
||||||
$connection = $this->newActiveConnection(['websocket']);
|
$connection = $this->newActiveConnection(['websocket']);
|
||||||
$connection->assertSentEvent('pusher.connection_established');
|
$connection->assertSentEvent('websocket.connection_established');
|
||||||
$connection->assertSentEvent('pusher_internal:subscription_succeeded');
|
$connection->assertSentEvent('websocket_internal.subscription_succeeded');
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, new Mocks\Message([
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
'event' => 'pusher.unsubscribe',
|
'event' => 'websocket.unsubscribe',
|
||||||
'data' => ['channel' => 'websocket'],
|
'data' => ['channel' => 'websocket'],
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
// After unsubscribe, sending a message should get :error
|
||||||
$connection->resetEvents();
|
$connection->resetEvents();
|
||||||
$this->pusherServer->onMessage($connection, new Mocks\Message([
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
'event' => 'pusher.custom[xyz789]',
|
'event' => 'pusher.custom[xyz789]',
|
||||||
'data' => ['test' => 'recovery'],
|
'data' => ['test' => 'recovery'],
|
||||||
'channel' => 'websocket',
|
'channel' => 'websocket',
|
||||||
|
|
@ -547,24 +549,25 @@ class HandlerStabilityTest extends TestCase
|
||||||
$this->assertNotNull($errorEvent, 'Should get :error');
|
$this->assertNotNull($errorEvent, 'Should get :error');
|
||||||
$this->assertEquals('Subscription not established', $errorEvent['data']['message']);
|
$this->assertEquals('Subscription not established', $errorEvent['data']['message']);
|
||||||
|
|
||||||
|
// Re-subscribe and verify recovery
|
||||||
$connection->resetEvents();
|
$connection->resetEvents();
|
||||||
$this->pusherServer->onMessage($connection, new Mocks\Message([
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
'event' => 'pusher.subscribe',
|
'event' => 'websocket.subscribe',
|
||||||
'data' => ['channel' => 'websocket'],
|
'data' => ['channel' => 'websocket'],
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$channel = $this->channelManager->find('1234', 'websocket');
|
$channel = $this->channelManager->find('1234', 'websocket');
|
||||||
$this->assertTrue($channel->hasConnection($connection), 'Re-subscribed');
|
$this->assertTrue($channel->hasConnection($connection), 'Re-subscribed after error');
|
||||||
|
|
||||||
|
// Protocol events should get :response again after recovery
|
||||||
$connection->resetEvents();
|
$connection->resetEvents();
|
||||||
$this->pusherServer->onMessage($connection, new Mocks\Message([
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
'event' => 'pusher.custom[def456]',
|
'event' => 'websocket.subscribe',
|
||||||
'data' => ['test' => 'post-recovery'],
|
'data' => ['channel' => 'websocket'],
|
||||||
'channel' => 'websocket',
|
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$responseEvent = collect($connection->sentData)->firstWhere('event', 'pusher.custom[def456]:response');
|
$responseEvent = collect($connection->sentData)->firstWhere('event', 'websocket.subscribe:response');
|
||||||
$this->assertNotNull($responseEvent, 'Post-recovery should get :response');
|
$this->assertNotNull($responseEvent, 'Post-recovery subscribe should get :response');
|
||||||
$this->assertEquals('Success', $responseEvent['data']['message']);
|
$this->assertEquals('Success', $responseEvent['data']['message']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -580,24 +583,24 @@ class HandlerStabilityTest extends TestCase
|
||||||
$conn3 = $this->newActiveConnection(['websocket']);
|
$conn3 = $this->newActiveConnection(['websocket']);
|
||||||
|
|
||||||
$exception = new \BlaxSoftware\LaravelWebSockets\Server\Exceptions\UnknownAppKey('BadKey');
|
$exception = new \BlaxSoftware\LaravelWebSockets\Server\Exceptions\UnknownAppKey('BadKey');
|
||||||
$this->pusherServer->onError($conn1, $exception);
|
$this->wsHandler->onError($conn1, $exception);
|
||||||
|
|
||||||
$conn1->assertSentEvent('pusher.error');
|
$conn1->assertSentEvent('websocket.error');
|
||||||
$conn2->assertNotSentEvent('pusher.error');
|
$conn2->assertNotSentEvent('websocket.error');
|
||||||
$conn3->assertNotSentEvent('pusher.error');
|
$conn3->assertNotSentEvent('websocket.error');
|
||||||
|
|
||||||
$conn2->resetEvents();
|
$conn2->resetEvents();
|
||||||
$conn3->resetEvents();
|
$conn3->resetEvents();
|
||||||
$this->pusherServer->onMessage($conn2, new Mocks\Message([
|
$this->wsHandler->onMessage($conn2, new Mocks\Message([
|
||||||
'event' => 'pusher.ping',
|
'event' => 'websocket.ping',
|
||||||
'data' => new \stdClass(),
|
'data' => new \stdClass(),
|
||||||
]));
|
]));
|
||||||
$this->pusherServer->onMessage($conn3, new Mocks\Message([
|
$this->wsHandler->onMessage($conn3, new Mocks\Message([
|
||||||
'event' => 'pusher.ping',
|
'event' => 'websocket.ping',
|
||||||
'data' => new \stdClass(),
|
'data' => new \stdClass(),
|
||||||
]));
|
]));
|
||||||
$conn2->assertSentEvent('pusher.pong');
|
$conn2->assertSentEvent('websocket.pong');
|
||||||
$conn3->assertSentEvent('pusher.pong');
|
$conn3->assertSentEvent('websocket.pong');
|
||||||
|
|
||||||
$channel = $this->channelManager->find('1234', 'websocket');
|
$channel = $this->channelManager->find('1234', 'websocket');
|
||||||
$this->assertTrue($channel->hasConnection($conn2));
|
$this->assertTrue($channel->hasConnection($conn2));
|
||||||
|
|
@ -615,20 +618,20 @@ class HandlerStabilityTest extends TestCase
|
||||||
$survivor2 = $this->newActiveConnection(['websocket']);
|
$survivor2 = $this->newActiveConnection(['websocket']);
|
||||||
$doomed = $this->newActiveConnection(['websocket']);
|
$doomed = $this->newActiveConnection(['websocket']);
|
||||||
|
|
||||||
$this->pusherServer->onClose($doomed);
|
$this->wsHandler->onClose($doomed);
|
||||||
|
|
||||||
$survivor1->resetEvents();
|
$survivor1->resetEvents();
|
||||||
$survivor2->resetEvents();
|
$survivor2->resetEvents();
|
||||||
$this->pusherServer->onMessage($survivor1, new Mocks\Message([
|
$this->wsHandler->onMessage($survivor1, new Mocks\Message([
|
||||||
'event' => 'pusher.ping',
|
'event' => 'websocket.ping',
|
||||||
'data' => new \stdClass(),
|
'data' => new \stdClass(),
|
||||||
]));
|
]));
|
||||||
$this->pusherServer->onMessage($survivor2, new Mocks\Message([
|
$this->wsHandler->onMessage($survivor2, new Mocks\Message([
|
||||||
'event' => 'pusher.ping',
|
'event' => 'websocket.ping',
|
||||||
'data' => new \stdClass(),
|
'data' => new \stdClass(),
|
||||||
]));
|
]));
|
||||||
$survivor1->assertSentEvent('pusher.pong');
|
$survivor1->assertSentEvent('websocket.pong');
|
||||||
$survivor2->assertSentEvent('pusher.pong');
|
$survivor2->assertSentEvent('websocket.pong');
|
||||||
|
|
||||||
$channel = $this->channelManager->find('1234', 'websocket');
|
$channel = $this->channelManager->find('1234', 'websocket');
|
||||||
$this->assertNotNull($channel);
|
$this->assertNotNull($channel);
|
||||||
|
|
@ -650,17 +653,17 @@ class HandlerStabilityTest extends TestCase
|
||||||
$rawMessage = $this->createRawMessage('{invalid json!!!}');
|
$rawMessage = $this->createRawMessage('{invalid json!!!}');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->pusherServer->onMessage($badConn, $rawMessage);
|
$this->wsHandler->onMessage($badConn, $rawMessage);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Handler should catch, but even if it propagates, others unaffected
|
// Handler should catch, but even if it propagates, others unaffected
|
||||||
}
|
}
|
||||||
|
|
||||||
$goodConn->resetEvents();
|
$goodConn->resetEvents();
|
||||||
$this->pusherServer->onMessage($goodConn, new Mocks\Message([
|
$this->wsHandler->onMessage($goodConn, new Mocks\Message([
|
||||||
'event' => 'pusher.ping',
|
'event' => 'websocket.ping',
|
||||||
'data' => new \stdClass(),
|
'data' => new \stdClass(),
|
||||||
]));
|
]));
|
||||||
$goodConn->assertSentEvent('pusher.pong');
|
$goodConn->assertSentEvent('websocket.pong');
|
||||||
|
|
||||||
$channel = $this->channelManager->find('1234', 'websocket');
|
$channel = $this->channelManager->find('1234', 'websocket');
|
||||||
$this->assertTrue($channel->hasConnection($goodConn));
|
$this->assertTrue($channel->hasConnection($goodConn));
|
||||||
|
|
@ -678,9 +681,9 @@ class HandlerStabilityTest extends TestCase
|
||||||
public function test_full_client_lifecycle_mirrors_frontend()
|
public function test_full_client_lifecycle_mirrors_frontend()
|
||||||
{
|
{
|
||||||
$connection = $this->newConnection('TestKey');
|
$connection = $this->newConnection('TestKey');
|
||||||
$this->pusherServer->onOpen($connection);
|
$this->wsHandler->onOpen($connection);
|
||||||
|
|
||||||
$established = collect($connection->sentData)->firstWhere('event', 'pusher.connection_established');
|
$established = collect($connection->sentData)->firstWhere('event', 'websocket.connection_established');
|
||||||
$this->assertNotNull($established);
|
$this->assertNotNull($established);
|
||||||
|
|
||||||
$data = json_decode($established['data'], true);
|
$data = json_decode($established['data'], true);
|
||||||
|
|
@ -688,49 +691,49 @@ class HandlerStabilityTest extends TestCase
|
||||||
$this->assertNotEmpty($data['socket_id']);
|
$this->assertNotEmpty($data['socket_id']);
|
||||||
|
|
||||||
$connection->resetEvents();
|
$connection->resetEvents();
|
||||||
$this->pusherServer->onMessage($connection, new Mocks\Message([
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
'event' => 'pusher.subscribe',
|
'event' => 'websocket.subscribe',
|
||||||
'data' => ['channel' => 'websocket', 'auth' => 'TestKey:fake-signature'],
|
'data' => ['channel' => 'websocket', 'auth' => 'TestKey:fake-signature'],
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$connection->assertSentEvent('pusher_internal:subscription_succeeded');
|
$connection->assertSentEvent('websocket_internal.subscription_succeeded');
|
||||||
$connection->assertSentEvent('pusher.subscribe:response');
|
$connection->assertSentEvent('websocket.subscribe:response');
|
||||||
|
|
||||||
$channel = $this->channelManager->find('1234', 'websocket');
|
$channel = $this->channelManager->find('1234', 'websocket');
|
||||||
$this->assertNotNull($channel);
|
$this->assertNotNull($channel);
|
||||||
$this->assertTrue($channel->hasConnection($connection));
|
$this->assertTrue($channel->hasConnection($connection));
|
||||||
|
|
||||||
$connection->resetEvents();
|
$connection->resetEvents();
|
||||||
$this->pusherServer->onMessage($connection, $this->pingMsg);
|
$this->wsHandler->onMessage($connection, $this->pingMsg);
|
||||||
$connection->assertSentEvent('pusher.pong');
|
$connection->assertSentEvent('websocket.pong');
|
||||||
|
|
||||||
$this->pusherServer->onClose($connection);
|
$this->wsHandler->onClose($connection);
|
||||||
$this->assertFalse($channel->hasConnection($connection));
|
$this->assertFalse($channel->hasConnection($connection));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pusher-prefixed events get :response suffix from handlePusherEvent().
|
* Protocol events get :response suffix from handleProtocolEvent().
|
||||||
*
|
*
|
||||||
* @group protocol
|
* @group protocol
|
||||||
*/
|
*/
|
||||||
public function test_pusher_events_get_response_suffix()
|
public function test_protocol_events_get_response_suffix()
|
||||||
{
|
{
|
||||||
$connection = $this->newActiveConnection(['websocket']);
|
$connection = $this->newActiveConnection(['websocket']);
|
||||||
$connection->resetEvents();
|
$connection->resetEvents();
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, new Mocks\Message([
|
// Subscribe/unsubscribe are the protocol events that get :response
|
||||||
'event' => 'pusher.custom-event',
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
'data' => ['payload' => 'test'],
|
'event' => 'websocket.subscribe',
|
||||||
'channel' => 'websocket',
|
'data' => ['channel' => 'websocket'],
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$response = collect($connection->sentData)->firstWhere('event', 'pusher.custom-event:response');
|
$response = collect($connection->sentData)->firstWhere('event', 'websocket.subscribe:response');
|
||||||
$this->assertNotNull($response, 'Pusher events should get :response');
|
$this->assertNotNull($response, 'Protocol subscribe events should get :response');
|
||||||
$this->assertEquals('Success', $response['data']['message']);
|
$this->assertEquals('Success', $response['data']['message']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Both ping formats produce pongs: pusher.ping (frontend) and pusher:ping (Pusher spec).
|
* Both ping formats produce pongs: websocket.ping (dot) and pusher:ping (colon, backward compat).
|
||||||
*
|
*
|
||||||
* @group protocol
|
* @group protocol
|
||||||
*/
|
*/
|
||||||
|
|
@ -739,47 +742,49 @@ class HandlerStabilityTest extends TestCase
|
||||||
$connection = $this->newActiveConnection(['websocket']);
|
$connection = $this->newActiveConnection(['websocket']);
|
||||||
$connection->resetEvents();
|
$connection->resetEvents();
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, new Mocks\Message([
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
'event' => 'pusher.ping',
|
'event' => 'websocket.ping',
|
||||||
'data' => new \stdClass(),
|
'data' => new \stdClass(),
|
||||||
]));
|
]));
|
||||||
$this->pusherServer->onMessage($connection, new Mocks\Message([
|
$this->wsHandler->onMessage($connection, new Mocks\Message([
|
||||||
'event' => 'pusher:ping',
|
'event' => 'pusher:ping',
|
||||||
'data' => new \stdClass(),
|
'data' => new \stdClass(),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
2,
|
2,
|
||||||
collect($connection->sentData)->where('event', 'pusher.pong')->count(),
|
collect($connection->sentData)->where('event', 'websocket.pong')->count(),
|
||||||
'Both ping formats should produce pongs'
|
'Both ping formats should produce pongs'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unsubscribe only works with dot format (pusher.unsubscribe).
|
* Unsubscribe works with dot format (websocket.unsubscribe).
|
||||||
* Colon format (pusher:unsubscribe) is NOT recognized.
|
* Colon format (pusher:unsubscribe) is also recognized via isProtocolAction().
|
||||||
*
|
*
|
||||||
* @group protocol
|
* @group protocol
|
||||||
*/
|
*/
|
||||||
public function test_unsubscribe_only_works_with_dot_format()
|
public function test_unsubscribe_works_with_both_dot_and_colon_format()
|
||||||
{
|
{
|
||||||
|
// Dot format: websocket.unsubscribe
|
||||||
$conn1 = $this->newActiveConnection(['websocket']);
|
$conn1 = $this->newActiveConnection(['websocket']);
|
||||||
$this->pusherServer->onMessage($conn1, new Mocks\Message([
|
$this->wsHandler->onMessage($conn1, new Mocks\Message([
|
||||||
'event' => 'pusher.unsubscribe',
|
'event' => 'websocket.unsubscribe',
|
||||||
'data' => ['channel' => 'websocket'],
|
'data' => ['channel' => 'websocket'],
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$channel = $this->channelManager->find('1234', 'websocket');
|
$channel = $this->channelManager->find('1234', 'websocket');
|
||||||
$this->assertFalse($channel->hasConnection($conn1), 'Dot-format unsubscribes');
|
$this->assertFalse($channel->hasConnection($conn1), 'Dot-format unsubscribes');
|
||||||
|
|
||||||
|
// Colon format: pusher:unsubscribe — also recognized by isProtocolAction()
|
||||||
$conn2 = $this->newActiveConnection(['websocket']);
|
$conn2 = $this->newActiveConnection(['websocket']);
|
||||||
$this->pusherServer->onMessage($conn2, new Mocks\Message([
|
$this->wsHandler->onMessage($conn2, new Mocks\Message([
|
||||||
'event' => 'pusher:unsubscribe',
|
'event' => 'pusher:unsubscribe',
|
||||||
'data' => ['channel' => 'websocket'],
|
'data' => ['channel' => 'websocket'],
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$channel = $this->channelManager->find('1234', 'websocket');
|
$channel = $this->channelManager->find('1234', 'websocket');
|
||||||
$this->assertTrue($channel->hasConnection($conn2), 'Colon-format does NOT unsubscribe');
|
$this->assertFalse($channel->hasConnection($conn2), 'Colon-format also unsubscribes');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -792,7 +797,7 @@ class HandlerStabilityTest extends TestCase
|
||||||
$connection = new Mocks\Connection();
|
$connection = new Mocks\Connection();
|
||||||
$connection->httpRequest = new \GuzzleHttp\Psr7\Request('GET', '/?appKey=TestKey');
|
$connection->httpRequest = new \GuzzleHttp\Psr7\Request('GET', '/?appKey=TestKey');
|
||||||
|
|
||||||
$this->pusherServer->onMessage($connection, $this->pingMsg);
|
$this->wsHandler->onMessage($connection, $this->pingMsg);
|
||||||
|
|
||||||
$this->assertEmpty($connection->sentData, 'No data sent to connection without app');
|
$this->assertEmpty($connection->sentData, 'No data sent to connection without app');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BlaxSoftware\LaravelWebSockets\Test;
|
||||||
|
|
||||||
|
use BlaxSoftware\LaravelWebSockets\Services\WebsocketService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for WebsocketService state tracking methods.
|
||||||
|
*
|
||||||
|
* These methods use Laravel's cache to track WebSocket connection state
|
||||||
|
* (authed users, active channels, connections). No running WS server needed.
|
||||||
|
*/
|
||||||
|
class WebsocketServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Start clean for each test
|
||||||
|
WebsocketService::resetAllTracking();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// resetAllTracking
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_reset_all_tracking_clears_all_ws_cache_keys()
|
||||||
|
{
|
||||||
|
// Seed some data
|
||||||
|
cache()->forever('ws_active_channels', ['websocket', 'private-user']);
|
||||||
|
cache()->forever('ws_socket_authed_users', ['1.1' => 1]);
|
||||||
|
cache()->forever('ws_connection_1-1', ['id' => '1.1']);
|
||||||
|
|
||||||
|
$this->assertTrue(WebsocketService::resetAllTracking());
|
||||||
|
|
||||||
|
$this->assertEmpty(WebsocketService::getActiveChannels());
|
||||||
|
$this->assertEmpty(WebsocketService::getAuthedUsers());
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// User auth tracking
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_set_user_authed_stores_user_in_cache()
|
||||||
|
{
|
||||||
|
$user = (object) ['id' => 42];
|
||||||
|
|
||||||
|
WebsocketService::setUserAuthed('1.123456', $user);
|
||||||
|
|
||||||
|
$this->assertEquals([
|
||||||
|
'1.123456' => 42,
|
||||||
|
], WebsocketService::getAuthedUsers());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_auth_returns_stored_user_for_socket()
|
||||||
|
{
|
||||||
|
$user = (object) ['id' => 42, 'name' => 'Rick'];
|
||||||
|
|
||||||
|
WebsocketService::setUserAuthed('1.123456', $user);
|
||||||
|
|
||||||
|
$auth = WebsocketService::getAuth('1.123456');
|
||||||
|
$this->assertNotNull($auth);
|
||||||
|
$this->assertEquals(42, $auth->id);
|
||||||
|
$this->assertEquals('Rick', $auth->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_auth_returns_null_for_unknown_socket()
|
||||||
|
{
|
||||||
|
$this->assertNull(WebsocketService::getAuth('99.999'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_clear_user_authed_removes_user()
|
||||||
|
{
|
||||||
|
$user = (object) ['id' => 42];
|
||||||
|
WebsocketService::setUserAuthed('1.123', $user);
|
||||||
|
|
||||||
|
$this->assertCount(1, WebsocketService::getAuthedUsers());
|
||||||
|
|
||||||
|
WebsocketService::clearUserAuthed('1.123');
|
||||||
|
|
||||||
|
$this->assertEmpty(WebsocketService::getAuthedUsers());
|
||||||
|
$this->assertNull(WebsocketService::getAuth('1.123'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_multiple_users_can_be_authed_simultaneously()
|
||||||
|
{
|
||||||
|
WebsocketService::setUserAuthed('1.100', (object) ['id' => 1]);
|
||||||
|
WebsocketService::setUserAuthed('1.200', (object) ['id' => 2]);
|
||||||
|
WebsocketService::setUserAuthed('1.300', (object) ['id' => 3]);
|
||||||
|
|
||||||
|
$authed = WebsocketService::getAuthedUsers();
|
||||||
|
|
||||||
|
$this->assertCount(3, $authed);
|
||||||
|
$this->assertEquals(1, $authed['1.100']);
|
||||||
|
$this->assertEquals(2, $authed['1.200']);
|
||||||
|
$this->assertEquals(3, $authed['1.300']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_clear_one_user_does_not_affect_others()
|
||||||
|
{
|
||||||
|
WebsocketService::setUserAuthed('1.100', (object) ['id' => 1]);
|
||||||
|
WebsocketService::setUserAuthed('1.200', (object) ['id' => 2]);
|
||||||
|
|
||||||
|
WebsocketService::clearUserAuthed('1.100');
|
||||||
|
|
||||||
|
$authed = WebsocketService::getAuthedUsers();
|
||||||
|
$this->assertCount(1, $authed);
|
||||||
|
$this->assertEquals(2, $authed['1.200']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// isUserConnected / getUserSocketIds
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_is_user_connected_returns_true_for_authed_user()
|
||||||
|
{
|
||||||
|
WebsocketService::setUserAuthed('1.100', (object) ['id' => 42]);
|
||||||
|
|
||||||
|
$this->assertTrue(WebsocketService::isUserConnected(42));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_is_user_connected_returns_false_for_unknown_user()
|
||||||
|
{
|
||||||
|
$this->assertFalse(WebsocketService::isUserConnected(999));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_is_user_connected_returns_false_after_clear()
|
||||||
|
{
|
||||||
|
WebsocketService::setUserAuthed('1.100', (object) ['id' => 42]);
|
||||||
|
WebsocketService::clearUserAuthed('1.100');
|
||||||
|
|
||||||
|
$this->assertFalse(WebsocketService::isUserConnected(42));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_user_socket_ids_returns_all_sockets_for_user()
|
||||||
|
{
|
||||||
|
// Same user connected from two devices
|
||||||
|
WebsocketService::setUserAuthed('1.100', (object) ['id' => 42]);
|
||||||
|
WebsocketService::setUserAuthed('1.200', (object) ['id' => 42]);
|
||||||
|
WebsocketService::setUserAuthed('1.300', (object) ['id' => 99]);
|
||||||
|
|
||||||
|
$sockets = WebsocketService::getUserSocketIds(42);
|
||||||
|
|
||||||
|
$this->assertCount(2, $sockets);
|
||||||
|
$this->assertContains('1.100', $sockets);
|
||||||
|
$this->assertContains('1.200', $sockets);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_user_socket_ids_returns_empty_for_unknown_user()
|
||||||
|
{
|
||||||
|
$this->assertEmpty(WebsocketService::getUserSocketIds(999));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Channel and connection tracking
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_get_active_channels_returns_empty_by_default()
|
||||||
|
{
|
||||||
|
$this->assertEmpty(WebsocketService::getActiveChannels());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_active_channels_returns_cached_channels()
|
||||||
|
{
|
||||||
|
cache()->forever('ws_active_channels', ['websocket', 'private-user.1']);
|
||||||
|
|
||||||
|
$channels = WebsocketService::getActiveChannels();
|
||||||
|
|
||||||
|
$this->assertEquals(['websocket', 'private-user.1'], $channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_channel_connections_returns_empty_for_unknown_channel()
|
||||||
|
{
|
||||||
|
$this->assertEmpty(WebsocketService::getChannelConnections('nonexistent'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_channel_connections_returns_cached_sockets()
|
||||||
|
{
|
||||||
|
cache()->forever('ws_channel_connections_websocket', ['1.100', '1.200', '1.300']);
|
||||||
|
|
||||||
|
$connections = WebsocketService::getChannelConnections('websocket');
|
||||||
|
|
||||||
|
$this->assertCount(3, $connections);
|
||||||
|
$this->assertContains('1.100', $connections);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// BroadcastClient availability
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_ws_available_returns_false_when_socket_file_missing()
|
||||||
|
{
|
||||||
|
// No Unix socket file exists in test — ws_available should be false
|
||||||
|
$this->assertFalse(ws_available());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_websocket_service_whisper_returns_false_when_unavailable()
|
||||||
|
{
|
||||||
|
$this->assertFalse(
|
||||||
|
WebsocketService::whisper('event', ['data' => 'test'], ['1.100'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_websocket_service_broadcast_except_returns_false_when_unavailable()
|
||||||
|
{
|
||||||
|
$this->assertFalse(
|
||||||
|
WebsocketService::broadcastExcept('event', ['data' => 'test'], ['1.100'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue