DEV3 Technologies home

Reverb chat with polling fallback

Broadcast new chat messages over Laravel Reverb and fall back to cursor-based polling when the WebSocket is unavailable.

LaravelNotificationsreverbbroadcastwebsocketpolling

Code

<?php

namespace App\Events;

use App\Models\Message;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;

class MessageSent implements ShouldBroadcast
{
    use Dispatchable;

    public function __construct(public Message $message) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel('conversation.'.$this->message->conversation_id)];
    }

    public function broadcastAs(): string
    {
        return 'message.sent';
    }

    // Send a minimal payload, never the whole model with hidden relations.
    public function broadcastWith(): array
    {
        return $this->message->only(['id', 'body', 'sender', 'created_at']);
    }
}

// routes/api.php: the polling endpoint returns anything newer than the client's last id.
// Route::get('/conversations/{c}/messages', fn (Conversation $c, Request $r) =>
//     $c->messages()->where('id', '>', (int) $r->query('after', 0))->orderBy('id')->limit(50)->get(['id','body','sender','created_at']));

Needs: laravel-echo@^2.0, pusher-js@^8.4

How it works

What it solves

WebSockets give instant chat, but corporate proxies, flaky mobile networks and a stopped Reverb process all happen in real life. A chat that silently stops updating feels broken. This pair of files keeps messages flowing by degrading to short polling and recovering automatically.

How it works

MessageSent implements ShouldBroadcast and sends to a private channel named after the conversation, using a dedicated event name and a small payload of only the fields the UI needs.

On the client, one accept() function receives messages from both sources. It remembers the highest id seen and ignores anything at or below it, so a message that arrives by socket and again by poll renders once. The poll endpoint returns rows with an id greater than the after cursor, ordered and limited, which makes it cheap and idempotent.

The Echo connection reports its state. When it becomes unavailable, failed or disconnected, a four second interval starts. When it is connected again, polling stops and one catch-up poll runs, which fills any gap from the outage.

Adapting it

  • Back the polling interval off to 10 seconds after a minute of failures.
  • Authorise the private channel in routes/channels.php, checking the user belongs to the conversation.
  • Use ShouldBroadcastNow if you do not run a queue worker.

Pitfalls

  • Broadcasting is queued by default; without a worker nothing is sent.
  • Do not broadcast model attributes you would not show to every participant.

How to use it

  1. Run php artisan install:broadcasting and npm i laravel-echo pusher-js.
  2. Start Reverb with php artisan reverb:start and a queue worker.
  3. Put the event in app/Events/ and add the poll route from the comment.
  4. Authorise the channel in routes/channels.php for conversation.{id}.
  5. Fire it after saving: MessageSent::dispatch($message);, and call connectChat(id, renderFn) on the page.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets