Server Sent Events using Laravel and Vue

Recently I was working on a new project, where I had to stream stock data and display it on a chart. My first instinct was to setup a loop using setTimeout, but I quickly realized that wouldn’t work for my exact needs. Stock prices change so frequently, often happening several times a second. Doing an ajax call every second seemed like the worst overhead baggage I could possibly program myself into. After doing a bit of research, I stumbled upon Server-Sent Events. 

Server-Sent Events is a web API for subscribing to a data stream sent by a server. Essentially this opens up a network request to the server that you can stream. Think of it like a Promise that never resolves. Implementing this API is fairly easy on the front-end using native JavaScript. For our tutorial, I’m going to wrap it in a VueJS instance.

Setting up the JavaScript Feed

The first thing we’ll do is setup our new Vue app.


const Vue = require('vue');

const app = new Vue({
    el: '#app',
    data: {
        stockData: null
    }
}

Once our app is created, we want to trigger a method for setting up the EventStream. Let’s call our method using the created() helper method that Vue provides.

Now we just need to implement that method. Let’s create our setupStream() method in the methods object. We’ll create a new EventSource and then add a listener for when new data comes in. When the new data comes in, we will update our data object for Vue.


created() {
    this.setupStream();
},
methods: {
    setupStream() {
        // Not a real URL, just using for demo purposes
        let es = new EventSource('http://awesomestockdata.com/feed');

        es.addEventListener('message', event => {
            let data = JSON.parse(event.data);
            this.stockData = data.stockData;
        }, false);
    }
}

It may be beneficial to know when the EventSource has closed for some reason, or if there was an error. In this case, we can add another event listener right below the first one.


es.addEventListener('error', event => {
    if (event.readyState == EventSource.CLOSED) {
        console.log('Event was closed');
        console.log(EventSource);
    }
}, false);

At this point we have a working EventSource feed collecting Server-Sent Events and storing the data into our Vue data model. Here is how our entire code should look.


const Vue = require('vue');

const app = new Vue({
    el: '#app',
    data: {
        stockData: null
    },
    created() {
        this.setupStream();
    },
    methods: {
        setupStream() {
            // Not a real URL, just using for demo purposes
            let es = new EventSource('http://awesomestockdata.com/feed');

            es.addEventListener('message', event => {
                let data = JSON.parse(event.data);
                this.stockData = data.stockData;
            }, false);

            es.addEventListener('error', event => {
                if (event.readyState == EventSource.CLOSED) {
                    console.log('Event was closed');
                    console.log(EventSource);
                }
            }, false);
        }
    }
});

Streaming Data Using Laravel

Laravel does support server-sent events out of the box. Your server may or may not need some extra configuration to get this to work. As of this writing I was using Nginx 1.10 with PHP 7.1, and everything worked for me out of the box.

To do this in Laravel we use Symfony’s StreamedResponse class. It does require a bit more configuration then what you are use to seeing in a clean Laravel app, but it works just the same. Let’s take a look at the code first, and then we’ll walk through what is happening here.


$response = new StreamedResponse(function() use ($request) {
    while(true) {
        echo 'data: ' . json_encode(Stock::all()) . "\n\n";
        ob_flush();
        flush();
        usleep(200000);
    }
});
$response->headers->set('Content-Type', 'text/event-stream');
$response->headers->set('X-Accel-Buffering', 'no');
$response->headers->set('Cach-Control', 'no-cache');
return $response;

The first thing we are doing is creating a new StreamedResponse class, and sending through our callback. In this callback, I’m running a simple endless loop, and querying the data. I’m using a basic Eloquent call here, but you should really use a Redis or some kind of fast data store if you are querying this much.

To finish off the loop, I’m waiting about 200ms to continue. Feel free to set this to whatever time interval your data needs to update.

Finally we set our headers. We need to tell the response to set the Content-Type to text/event-stream so your browser and JavaScript know what to do with it. The last two lines are for caching. I needed these to work on Nginx, but they may not be required for different Nginx setups, or for Apache.

Use With Caution

This solution really worked for me, and I would recommend it for anyone doing the same type of instant data streaming. Keep in mind that you are not running multiple ajax queries here. You are bundling everything into a single call. If that call fails, it will not restart.

The server will also be under above-average load. You won’t want to run this on your $5 Digital Ocean or Linode box. Remember that each one of your visitors will have an unending stream of data from your server, until they leave or close the browser. As always, plan your resources accordingly.


Also published on Medium.

Comments

  1. has a matter of precaution, you should cap your connection to at list the execution time. It will prevent unecessary server side error. an behave more like a long polling (with less overhead)

    $start = time();
    $maxExecution = ini_get(‘max_execution_time’);
    $response = new StreamedResponse(function() use ($request, $start, $maxExecution) {

    while(true) {
    if(time() > = $start + $maxExectution) {
    exit(200);
    }

    echo ‘data: ‘ . json_encode(Stock::all()) . “\n\n”;
    ob_flush();
    flush();
    usleep(200000);
    }
    });
    $response->headers->set(‘Content-Type’, ‘text/event-stream’);
    $response->headers->set(‘X-Accel-Buffering’, ‘no’);
    $response->headers->set(‘Cach-Control’, ‘no-cache’);
    return $response;

Leave a Reply

Your email address will not be published. Required fields are marked *