pollsockets / pollsockets-laravel
Serverside implementation of pollsockets taylored for Laravel
Package info
github.com/pollsockets/pollsockets-laravel
pkg:composer/pollsockets/pollsockets-laravel
Requires
- php: ^8.2|^8.3|^8.4
- illuminate/contracts: ^11.0|^12.0|^13.0
- spatie/laravel-package-tools: ^1.92.4
Requires (Dev)
- laravel/pint: ^1.24
- nunomaduro/collision: ^8.0
- nunomaduro/larastan: ^3.5|^3.9.3
- orchestra/testbench: ^10.4|^11.0
- pestphp/pest: ^3.8|^4.0
- pestphp/pest-plugin-laravel: ^3.2|^4.1
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^11.5|^12.3
- rregeer/phpunit-coverage-check: ^0.3
- spatie/laravel-ray: ^1.40
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Why pollsockets?
Pollsockets is a simple, yet powerful, server-client communication library. It is designed to be used in situations where you need to send data from the server to the client. It is a great alternative to websockets, as it is much simpler to implement and use.
How does it work?
Pollsockets uses a simple polling mechanism to send events from the server to the client. Instead of polling an endpoint that returns data, pollsockets polls an endpoint that returns a list of events that have occurred, thus saving bandwidth and response time. The client then processes the events and updates the UI accordingly.
Ecosystem
Server side
Client side
Requirements
- PHP 8.2 or higher
- Laravel 11.0 or higher
- Redis 6.0 or higher
- pollsockets-js 2.x on the client
Installation
You can install the package via composer:
composer require pollsockets/pollsockets-laravel
You can publish the config file with:
php artisan vendor:publish --tag="pollsockets-config"
This is the contents of the published config file:
use Pollsockets\Drivers\RedisChannel; return [ 'driver' => RedisChannel::class, // Redis connection name. null uses the default connection. 'connection' => null, // Events older than this many seconds are pruned on the next publish. Must be at least 1. 'retention' => 60, // Request header that carries the publishing client's id. 'client_header' => 'X-Pollsockets-Client', 'route' => [ 'prefix' => '_pollsockets', // e.g. ['web', 'auth'] if your event names are sensitive. 'middleware' => [], ], ];
Usage
Publishing events
use Pollsockets\Pollsockets; // A channel separates events from different sources, e.g. one channel per team. // Channel names may contain letters, digits, "_", ".", ":" and "-". Pollsockets::channel('team.1')->publish('coupons');
An event name is any non-empty string without |. Each event is stored in a Redis sorted set pollsockets.{channel},
scored with the publish time in Unix milliseconds. Publishing the same event from the same origin again only moves
its time forward, and the client fires each event name once per poll, not once per publish.
Events older than retention seconds are removed on the next publish to the channel.
Publish after the database transaction commits, for example inside DB::afterCommit(...) or after the transaction
block. Otherwise a client can receive the event and reload before the new data is visible.
DB::transaction(function () { // ... write the coupons DB::afterCommit(fn () => Pollsockets::channel('team.1')->publish('coupons')); });
Polling
The client polls GET /_pollsockets/{channel}/poll?since=<ms> and gets:
{
"time": 1791288000500,
"events": {
"coupons|k3f9a": 1791288000124,
"shopping_lists|": 1791288000300
}
}
timeis the server time in Unix milliseconds when the request was handled.eventsholds every event published aftersince(exclusive), as"{event}|{origin}": <publish time ms>. Without a validsince, it holds every stored event. It is always a JSON object.- Expired events stay in Redis until the next publish to the channel prunes them, so a response can contain events
older than
retention. - The response is sent with
Cache-Control: no-store, private.
The client keeps track of the cursor, skips its own events and detects when it was away for longer than the retention window. pollsockets-js 2.x does all of this for you.
Delivery and limitations
An event's score is the time read just before its ZADD, so events are not always stored in score order. A poll can return a newer event while an older-stamped event is not stored yet, and the cursor then moves past it. To cover this, the JS client re-reads the last 2 seconds on every poll and skips the events it already handled. A publish whose ZADD lands up to about 2 seconds after its timestamp is still delivered. A longer delay, for example a slow Redis connection, can cause the event to be missed.
Initial cursor
To make the client receive every change made after the page loaded its data, pass Pollsockets::now() to it as
since. Capture the time before you load the data that the page shows, for example on the first line of the
controller. A change published between the data query and a later timestamp would otherwise be missed.
public function index() { $since = Pollsockets::now(); // before the queries return view('coupons.index', [ 'coupons' => Coupon::all(), 'pollsocketsSince' => $since, ]); }
<script> window.pollsocketsSince = {{ $pollsocketsSince }}; </script>
With Inertia, put the timestamp in a prop that is resolved before the other props.
The client re-reads the 2 seconds before since, so right after start it can fire one extra notification for a
change that the page already shows.
Without it, the client uses the time of its first poll response as the starting point.
Skipping the publishing tab (origin)
The browser tab that caused a change usually does not need to be notified about it. Send the client id of
pollsockets-js in the X-Pollsockets-Client header (configurable with client_header) on your app's own write
requests, for example with an HTTP interceptor. The id is stored as the event's origin, and that client skips the event.
The origin must match ^[A-Za-z0-9_-]{1,64}$. Any other value, and publishes without an HTTP request
(queued jobs, console commands), store an empty origin, so every client receives the event.
Securing the poll route
Anyone who can reach the poll route can see the event names of a channel. The events carry no payload, but if the
names or channel names are sensitive, protect the route with route.middleware, for example ['web', 'auth'].
Note that polling through web starts and writes the session on every poll (about once per second per tab), and
keeps the session alive while the tab is open.
Keeping the client in sync with the config
- If you change
retention, set the pollsockets-jsretentionoption to the same value in milliseconds. - If you change
route.prefix, pass the matchingurloption to pollsockets-js.
Upgrading from v1
publish($message, $delta)is nowpublish($event). Remove the$deltaargument. Events no longer expire after$deltaseconds. The next publish to the channel prunes events older thanretentionseconds.- Event names must not be empty or contain
|. Pollsockets::channel()now throws anInvalidArgumentExceptionfor a channel name that does not match[A-Za-z0-9_.:-]+. In v1 any string was accepted. Rename channels that contain other characters, such as spaces or/.- The poll response changed from
{"event": <unix seconds>}to{"time": <ms>, "events": {"event|origin": <ms>}}, and the route accepts asincequery parameter. Upgrade the client to pollsockets-js 2.x at the same time. - Custom drivers must implement
publish(string $event): voidandpoll(?int $since = null): array.PollsocketsChannel::member($event)builds the stored member, including the origin. - Publish the config file again, or add the new
connection,retention,client_headerandroutekeys to your existing one.
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.