Skip to content

Latest commit

 

History

History
219 lines (197 loc) · 5.7 KB

boilerplate-service-event-source.md

File metadata and controls

219 lines (197 loc) · 5.7 KB

Usage

<?php
Route::get('/event-source/demo', function () {

    return EventSource::make('deployment', 10000, function(EventSource $event){
        $event->send('status', array(
            'message' => "Starting Deployment",
            'error'   => false,
        ));
        sleep(1); //Fake Process

        $event->send('status', array(
            'message' => "Compiling Assets",
            'error'   => false,
        ));
        sleep(1); //Fake Process

        $index = 1;
        while($index < 7){
            $event->send('status', array(
                'message' => "Installing Package $index...",
            ));
            sleep(1);
            $event->send('status', array(
                'message' => "Package $index Installed!",
            ));
            $index++;
        }
        sleep(1); //Fake Process
        $event->send('status', array(
            'message' => "Error Encountered...",
            'error'   => true,
        ));
        $event->send('status', array(
            'message' => "Package XXX failed to be installed...",
            'error'   => true,
        ));
        sleep(2); //Fake Process
        $event->send('status', array(
            'message' => "Deployment Failed!",
            'error'   => true,
        ));
    });
});

EventSource Class

<?php declare(strict_types=1);

namespace App\Services;

use Illuminate\Contracts\Support\Responsable;
use Symfony\Component\HttpFoundation\StreamedResponse;

/**
 * Event Source Stream
 * @source https://www.html5rocks.com/en/tutorials/eventsource/basics/
 * @source https://dev.to/mesadhan/event-stream-server-send-events-5afk
 */
class EventSource implements Responsable
{

    protected $id;
    protected $timeout;
    public $closure;

    public function __construct(string $id, int $retryTimeout, \Closure $closure)
    {
        $this->id = $id;
        $this->timeout = $retryTimeout;
        $this->closure = $closure;
    }

    public static function make(string $id, int $retryTimeout, \Closure $closure)
    {
        return new self($id, $retryTimeout, $closure);
    }

    public function start(): void
    {
        ini_set('max_execution_time', "{$this->timeout}");
        echo "retry: {$this->timeout}" . PHP_EOL;
        echo "id: {$this->id}" . PHP_EOL;
        $this->flush();
    }

    public function send(string $name, array $data = array()): void
    {
        echo "event: $name" . PHP_EOL;
        echo "data: " . $this->encode($data) . PHP_EOL;
        $this->flush();
    }

    public function close(): void
    {
        echo "id: close\n" . PHP_EOL;
        echo "data: \n\n\n" . PHP_EOL;
        $this->flush();
    }

    protected function encode(array $data): string
    {
        return (string)json_encode($data);
    }

    protected function flush(): void
    {
        echo PHP_EOL;
        if (ob_get_level() > 0) {
            ob_flush();
            flush();
        }
    }

    public function toResponse($request): StreamedResponse
    {

        $response = new StreamedResponse(function (){
            $closure = $this->closure;
            $this->start();
            $closure($this);
            $this->close();
        })
        $response->headers->set('X-Accel-Buffering', 'no');
        $response->headers->set('Cach-Control', 'no-cache');
        $response->headers->set('Content-Type', 'text/event-stream');
        return $response;
    }
}

VueJS Component

<script>
    export default {
        name: 'EventSource',
        data() {
            return {
                output: [],
                isLoading: false,
                isSupported: false,
            }
        },
        methods: {
            run() {
                this.output = []
                this.isLoading = true
                this.$options.stream = new EventSource('/event-source');
                this.$options.stream.addEventListener('error', this.close, false)
                this.$options.stream.addEventListener('status', this.record, false)
            },
            record(event){
                if (event.data) {
                    this.output.push(JSON.parse(event.data))
                    this.$nextTick(this.scrollTop)
                }
                if (event.lastEventId === 'close') {
                    this.close()
                }
            },
            close() {
                this.isLoading = false
                this.$options.stream.close()
                this.$options.stream = null
            },
            scrollTop() {
                if (this.$refs.console) {
                    this.$refs.console.scrollTop = this.$refs.console.scrollHeight
                }
            },
        },
        created() {
            this.isSupported = 'EventSource' in window
        }
    }
</script>
<template>
    <div>
        <div v-if="isSupported">
            <button
                @click="run"
                type="button"
                :disabled="isLoading"
                class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
                Run Command
            </button>
            <hr>
            <div class="console-output" ref="console">
                <pre v-for="(entry) in output" :class="{'console-error': entry.error}">{{ entry.message }}</pre>
            </div>
        </div>
        <div v-else>
            "EventStream" is not supported by this browser. Time to upgrade?
        </div>
    </div>
</template>
<style lang="sass">
    .console-output
        padding: 15px
        height: 600px
        max-height: 768px
        overflow-y: scroll
        background: black
        color: #00ebff
        pre
            padding: 5px 0
            &.console-error
                color: red
</style>