laravel / reverb

Laravel Reverb provides a real-time WebSocket communication backend for Laravel applications.
https://reverb.laravel.com
MIT License
942 stars 63 forks source link

Adding events #206

Closed jakubforman closed 1 month ago

jakubforman commented 1 month ago

Adds sending events that can be handled to within Event::listen().

Why I created this PR:

I am responding to #185 in which the need to track these events arose and PresenceChannel is insufficient for this.

Newly added events:

Each event is called similarly to the built-in original MessageReceived and MessageSent events.

The functionality can be extended using this PR as follows:

☠️ Bug detected

When using the example below DeviceDisconnected event inside a Reverb server (tested on local), the server freezes if it is a synchronous send Queue. In the case of an asynchronous send (for example via DB) everything is fine. This is related to https://github.com/laravel/reverb/issues/185#issuecomment-2111819780

Real Example

Showing one admin panel with many of devices (players) and their status.

ezgif-2-0d4c0b7e80

Code sample and usage

Inside EventServiceProvider

<?php
// ...
class EventServiceProvider extends ServiceProvider
{
    // ...
    public function boot()
    {
        // Unsubscribe channel - offline state
        Event::listen( // swap player or auth info
            PusherUnsubscribe::class,
            DeviceDisconnectNotificationWS::class
        );
        // ...
    }
    // ...
}

Event Listener - Chnage in DB + Reverb Event Notification

<?php
namespace App\Listeners;

use App\Events\DeviceDisconnected;
use App\Models\Device;
use Illuminate\Support\Str;
use Laravel\Reverb\Events\PusherUnsubscribe;

class DeviceDisconnectNotificationWS
{
    public function handle(PusherUnsubscribe $event): void
    {
        $id = Str::after($event->channel, 'private-devices.');

        /** @var Device $device */
        $device = Device::find($id);
        if ($device) {
            $device->status = 'offline';
            $device->update();
            DeviceDisconnected::dispatch($device);
        }
    }
}

Custom Event DeviceDisconnected - called v DeviceDisconnectNotificationWS

<?php

namespace App\Events;

use App\Models\Device;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class DeviceDisconnected implements ShouldBroadcast
{
    use Dispatchable;
    use InteractsWithSockets;
    use SerializesModels;

    public Device $device; // Hold status

    public function __construct(Device $device)
    {
        $this->device = $device;
    }

    public function broadcastWith()
    {
        return [
            "deviceId" => $this->device->id,
        ];
    }

    public function broadcastAs(): string
    {
        return 'status.disconnected';
    }

    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('company.' . $this->device->company_id . '.devices'), // channel with authorization
        ];
    }
}

channels.php

Broadcast::channel('company.{companyId}.devices', function (User $user, string $companyId) {
    return $user->can('read', Company::find($companyId));
});

Broadcast::channel('devices.{deviceId}', function (User $user, string $deviceId) {
    return $user->can('read', Device::find($deviceId));
});
joedixon commented 1 month ago

Thank you for the pull request and the detailed explanation of why it is needed for your use case.

We're not adding new events to the project right now as they expose the possibility of blocking the event loop. For example, if the listener in your example is not queued, the event loop will be blocked since the message you are attempting to broadcast cannot be accepted until the listener execution completes and the listener execution cannot complete until the message has been broadcast. You end up in a deadlock.

jakubforman commented 1 month ago

Thank you for the pull request and the detailed explanation of why it is needed for your use case.

We're not adding new events to the project right now as they expose the possibility of blocking the event loop. For example, if the listener in your example is not queued, the event loop will be blocked since the message you are attempting to broadcast cannot be accepted until the listener execution completes and the listener execution cannot complete until the message has been broadcast. You end up in a deadlock.

I understand the deadlock, it has happened to me when I use Queues in sync mode, that's why you need to have it as a Queue through an external service (SQS, DB...) - it hasn't happened there yet. Would there be any chance to get those events in there in the future? Alternatively, what would I have to do to make it possible to catch these events there.

ah-rahimi commented 1 day ago

Hello, I also need such events. As far as I understand, does it mean that if the events are not queued, they will have problems? Well, put a variable in the config file to turn on and off the events so that anyone who needs it can use it. @joedixon