Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Event Source Laravel Package

funkyoz/event-source

A Laravel event sourcing package for persisting domain events, rebuilding aggregates, and projecting read models. It provides building blocks to model state changes as an append-only event stream with clean, framework-friendly APIs.

View on GitHub
Deep Wiki
Context7

Getting Started

This package gives you a Laravel-native way to push a stream of events from a controller to the browser over text/event-stream, without hand-rolling echo/flush() loops. Day to day it replaces polling endpoints and “please wait” spinners with real-time progress, notifications, and token streaming.

  1. Install and (if the package ships one) publish the config: composer require <vendor>/<package> then php artisan vendor:publish --tag=<package>-config.
  2. Read two things first: the config file (buffer/heartbeat/retry defaults) and the stream response class in src/ — that class is where the buffering behaviour described in 2.0.1 lives, and it’s the file you’ll come back to when a stream “doesn’t show up until the end”.
  3. Register a route. A stream route is an ordinary Laravel route; the controller returns the package’s stream response instead of a view/JSON. Keep it on its own route, outside any middleware group that buffers or compresses output.
  4. First use case: a progress endpoint. Kick off a queued job from a normal POST, have the job write a percentage into the cache, then return a stream that polls the cache and emits an event until it sees done. On the frontend, new EventSource('/progress/123') and listen for the named event.

Sanity-check it with curl -N http://app.test/progress/123 before touching JavaScript — that isolates “is the server flushing?” from “is my EventSource handler wrong?”.

Implementation Patterns

Named events + typed listeners. Emit an event name on every payload (progress, message, done) and attach one addEventListener per name in JS. A single anonymous onmessage handler gets unwieldy fast and makes it impossible to distinguish error events from data events.

IDs for resumability. Set an incrementing id on each event. Browsers automatically reconnect to EventSource and send Last-Event-ID; read that header in the controller and skip already-sent events. Without ids, every network blip replays the whole stream from the start.

Producer/consumer split. Don’t do slow work inside the stream loop. Dispatch a job, have it append to a cache list / Redis stream / database table, and let the stream tail that store with a short sleep(). This keeps the stream loop cheap, makes it testable, and survives max_execution_time on the worker.

Heartbeats. Emit a comment (: ping) or a heartbeat event every 10–20 seconds. Load balancers and nginx proxy_read_timeout typically cut idle connections at 30–60s; heartbeat interval must be lower than the shortest idle timeout in your stack.

Close deliberately. Send a terminal event (done, error) and return from the loop. Client code should EventSource.close() on that event, otherwise the browser reconnects forever and you get a request loop.

Streaming LLM/token output. If you’re proxying a token stream from an HTTP client, emit per chunk and flush after each; keep the upstream client’s timeout generous and handle upstream failure by emitting an error event rather than throwing (a thrown exception mid-stream produces a truncated, unparseable response).

Testing. Assert on the response’s streamed content rather than making a real HTTP call. The 2.0.1 note that CLI output buffers are intentionally kept exists precisely so tests can capture the stream — so don’t “fix” an ob_get_level() mismatch in a test by adding ob_end_clean(), you’ll break capture.

Auth and sessions. Stream routes still go through middleware, so auth works — but call session()->save() (or use a stateless guard) before entering the loop. PHP’s file session driver holds an exclusive lock for the request’s lifetime, and a long-lived stream will block every other request from that user.

Gotchas and Tips

The 2.0.1 buffer change is a real behaviour change, even though it’s a patch. The stream now flushes and closes output buffers when it starts, matching Symfony’s EventStreamResponse. Consequences:

  • If you wrapped the stream call in your own ob_start() to capture output, that buffer will be closed underneath you. Capture via the test helpers, not by manual buffering.
  • If a framework or plugin (Debugbar, a response-caching middleware, a legacy ob_start() in a service provider) left buffers open, events previously sat in them until the stream ended — the “everything arrives at once after 30 seconds” symptom. That’s fixed on web requests.
  • On the command line the buffers are left alone on purpose, so tests can capture. This means CLI and web behaviour genuinely differ; a green test does not prove the browser sees incremental events. Always verify once with curl -N.

Unflushable buffers are now skipped, not flushed. A buffer created with ob_start(null, 0, 0) (zero chunk size, non-removable flags) can’t be flushed, and 2.0.1 leaves it alone instead of emitting an ob_flush() notice per event. The trade-off: if such a buffer is still wrapping your output, events will still be delivered late and you’ll no longer get the notice telling you so. When a stream stalls, the first diagnostic is dump(ob_get_level()) at the top of the controller plus a scan for ob_start in your own code, Debugbar, and any response-rewriting middleware. Remove/disable the buffer rather than trying to flush around it.

Disable compression and proxy buffering. Content-Encoding: gzip and mod_deflate will hold events until a compression block fills. Send X-Accel-Buffering: no for nginx, and confirm with curl -N --compressed versus without.

Never dd(), dump(), or ray() inside a stream loop. It injects HTML/JSON into the event stream and the browser silently drops the connection as malformed. Log to file instead.

Guard the loop against abandonment. Check connection_aborted() each iteration and bail; otherwise a user closing a tab leaves a PHP-FPM worker spinning until it hits max_execution_time. Pair with set_time_limit(0) on the stream route, but keep the heartbeat + abort check so “infinite” doesn’t mean “leaked”.

Queue workers vs Octane. Under Laravel Octane, a stream occupies a worker for its full duration. Cap concurrent streams (separate worker pool / route-level throttling) or a handful of open dashboards will starve the app.

Reconnect storms. EventSource retries automatically with the server-suggested retry: value. If your terminal event isn’t honoured client-side, you get an exponential request loop — watch access logs for repeated identical stream URLs as your first signal.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource