Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
bennothommo committed Oct 11, 2024
1 parent dda41ce commit 22cdb63
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 1 deletion.
30 changes: 29 additions & 1 deletion modules/backend/controllers/Users.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
use Redirect;
use Response;
use BackendMenu;
use BackendAuth;
use Backend\Facades\BackendAuth;
use Backend\Models\UserGroup;
use Backend\Classes\Controller;
use System\Classes\EventStream;
use System\Classes\SettingsManager;

/**
Expand Down Expand Up @@ -260,4 +261,31 @@ public function update_onManualPasswordReset($recordId)

return Redirect::refresh();
}

public function eventregister()
{
$eventStream = new EventStream();
$eventStream->register();

die($eventStream->getId());
}

public function eventtest(string $id)
{
$this->withEventStream($id, function (EventStream $stream) {
$i = 0;

while (true) {
++$i;
if ($i === 25) {
break;
}

if (random_int(1, 3) === 3) {
$stream->set('time', time());
}
sleep(1);
}
});
}
}
163 changes: 163 additions & 0 deletions modules/system/classes/EventStream.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php

namespace System\Classes;

use Illuminate\Support\Facades\Cache;

/**
* Event stream.
*
* Represents a HTML5 event stream that can be interacted with when using the `withEventStream` method in the
* `System\Traits\ResponseMaker` trait. This class is intended to be used with a front-end library that can interpret
* the event stream and manage and display real-time information and updates.
*
* @author Ben Thomson <[email protected]>
* @copyright 2024 Winter CMS Maintainers
*/
class EventStream
{
public function __construct(
protected string $id = '',
protected array $data = [],
protected string $event = 'start',
protected bool $closed = false,
protected bool $changed = true,
protected float $ticks = 1.0,
) {
}

public function register()
{
// Generate new ID
do {
$this->id = 'event-stream-' . $this->generateId();
} while (Cache::has($this->id));

// Store the stream in the cache
$this->saveEvent(30);
}

public static function load(string $id): ?static
{
if (!Cache::has($id)) {
return null;
}

$data = Cache::get($id);

return new static(
id: $id,
data: $data['data'],
event: $data['event'],
closed: $data['closed'],
changed: $data['changed'],
ticks: $data['ticks'],
);
}

public function getId(): string
{
return $this->id;
}

public function getTicks(): float
{
return $this->ticks;
}

public function set(string|array $key, mixed $value = null): void
{
if (is_array($key)) {
$this->data = array_merge($this->data, $key);
} else {
$this->data[$key] = $value;
}

$this->event = 'update';
$this->changed = true;
$this->saveEvent();
}

public function tick(): void
{
$this->event = 'ping';
$this->changed = false;
$this->saveEvent();
}

public function reconnect(): void
{
if ($this->closed) {
return;
}
$this->event = 'reconnect';
$this->closeStream();
}

public function close(): void
{
if ($this->closed) {
return;
}

$this->event = 'close';
$this->closeStream();
}

public function isClosed(): bool
{
return $this->closed;
}

protected function closeStream(): void
{
$this->closed = true;
$this->saveEvent();
}

protected function saveEvent(int $ttl = 5): void
{
var_dump(Cache::set($this->id, [
'event' => $this->event,
'data' => $this->data,
'changed' => $this->changed,
'closed' => $this->closed,
'ticks' => $this->ticks,
], now()->addSeconds($ttl)));
}

protected function generateId(): string
{
$id = '';
for ($i = 0; $i < 32; ++$i) {
$id .= substr('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', random_int(0, 61), 1);
}
return $id;
}

public function streamEvent(): string
{
$data = Cache::get($this->id);

if ($data['closed']) {
return '';
}

$eventData = [];

if ($data['event'] !== 'ping' || $data['changed']) {
$eventData['time'] = date('c');
}
if ($data['changed']) {
$eventData['contents'] = $data['data'];
}

$contents = 'event: ' . $data['event'] . PHP_EOL;
if (count($eventData)) {
$contents .= 'data: ' . json_encode($eventData) . PHP_EOL;
}
$contents .= PHP_EOL;

return $contents;
}
}
67 changes: 67 additions & 0 deletions modules/system/controllers/EventStream.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace System\Controllers;

use Backend\Classes\Controller;
use Illuminate\Support\Facades\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use System\Classes\EventStream as EventStreamInstance;

/**
* Event stream controller.
*
* Handles the delivery of event-streaming data to the client.
*/
class EventStream extends Controller
{
public function register()
{
$eventStream = new EventStreamInstance();
$eventStream->register();

return Response::json(['id' => $eventStream->getId()]);
}

public function subscribe(string $id)
{
$eventStream = EventStreamInstance::load($id);

if (is_null($eventStream)) {
return Response::make('Event stream not found', 404);
}

$response = new StreamedResponse();

$response->headers->set('Content-Type', 'text/event-stream');
$response->headers->set('Cache-Control', 'no-cache');
$response->headers->set('Connection', 'keep-alive');

$response->setCallback(function () use ($eventStream) {
while (true) {
if ($eventStream->isClosed()) {
break;
}

echo $eventStream;

if (ob_get_level() > 0) {
ob_flush();
}

flush();

$eventStream->tick();

if (connection_aborted()) {
break;
}

usleep($eventStream->getTicks() * 1000000);
}
});

$response->send();
$this->responseOverride = $response;
return $response;
}
}
20 changes: 20 additions & 0 deletions modules/system/traits/ResponseMaker.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
<?php namespace System\Traits;

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Event;
use Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpFoundation\Response as BaseResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
use System\Classes\EventStream;

/**
* Response Maker Trait
Expand Down Expand Up @@ -133,4 +138,19 @@ public function makeResponse($contents)

return $contents;
}

public function withEventStream(string $id, callable $callback, array $data = [], float $tick = 1.0): void
{
$eventStream = EventStream::load($id);

if (is_null($eventStream)) {
return;
}

$callback($eventStream);

if (!$eventStream->isClosed()) {
$eventStream->close();
}
}
}

0 comments on commit 22cdb63

Please sign in to comment.