Streaming ReactPHP in ReactJS

The Streaming Server

server
|
└─── src
| |
| └─── api.php
| └─── constants.php
| └─── filesystem.php
└─── composer.json
└─── composer.lock
└─── lyrics.txt
└─── server.php
{
"require": {
"chemem/asyncify": "~0",
"nikic/fast-route": "~1",
"react/http": "~1"
},
"autoload": {
"files": [
"src/api.php",
"src/constants.php",
"src/filesystem.php"
]
}
}
namespace App\Filesystem;use React\EventLoop\Loop;
use React\Promise\PromiseInterface;
use function Chemem\Asyncify\call;
function freadl(string $file, int $line): string
{
$fp = \fopen($file, 'r');
$count = 0;

while ($count < $line) {
$contents = \fgets($fp, 1024);
\fseek($fp, \ftell($fp));
$count++;
}

fclose($fp);
return !$contents ? '' : $contents;
}
function freadlAsync(string $file, int $line): PromiseInterface
{
$async = call(Loop::get());
return $async(
__NAMESPACE__ . '\\freadl',
[$file, $line],
);
}
namespace App\Api;use App\Constants;use App\Filesystem;
use React\EventLoop\Loop;
use React\Http\Message\Response;
use React\Promise\PromiseInterface;
use React\Stream\ThroughStream;
use function React\Promise\resolve;
function streamLyrics(): PromiseInterface
{
$line = 1;
$stream = new ThroughStream();

$timer = Loop::addPeriodicTimer(
2,
function () use (&$line, $stream, &$timer) {
Filesystem\freadlAsync(
__DIR__ . '/../lyrics.txt',
$line++,
)->then(
function (string $data) use ($stream, $timer) {
if (empty($data)) {
$stream->end();
Loop::cancelTimer($timer);
}
$stream->write($data);
},
);
},
);
$stream->on(
'close',
function () use ($timer) {
Loop::cancelTimer($timer);
},
);

return resolve(
new Response(
200,
\array_merge(Constants\CORS_HEADERS, Constants\TEXT_TYPE),
$stream,
),
);
}
namespace App\Constants;const CORS_HEADERS = [
'access-control-allow-origin' => '*',
'access-control-allow-methods' => 'GET, OPTIONS',
];
const JSON_TYPE = [
'content-type' => 'application/json',
];
const TEXT_TYPE = [
'content-type' => 'text/plain',
];
use App\Api;
use App\Constants;
use FastRoute\Dispatcher;
use FastRoute\RouteCollector;
use React\Http\HttpServer;
use React\Http\Message\Response;
use React\Promise\PromiseInterface;
use React\Socket\SocketServer;
use Psr\Http\Message\ServerRequestInterface;
use function FastRoute\simpleDispatcher;
use function React\Promise\resolve;
$http = new HttpServer(
function (ServerRequestInterface $request): PromiseInterface {
$dispatcher = simpleDispatcher(
function (RouteCollector $route) {
$route->get('/api/lyrics', fn () => Api\streamLyrics());

return $route;
},
);
$response = $dispatcher->dispatch(
$request->getMethod(),
$request->getUri()->getPath(),
);
$info = $response[0];

if ($info === Dispatcher::METHOD_NOT_ALLOWED) {
return resolve(
new Response(
405,
array_merge(Constants\JSON_TYPE, Constants\CORS_HEADERS),
json_encode(['message' => 'Invalid request method']),
),
);
}
if ($info === Dispatcher::NOT_FOUND) {
return resolve(
new Response(
404,
array_merge(Constants\JSON_TYPE, Constants\CORS_HEADERS),
json_encode(['message' => 'Route not found']),
),
);
}
return $response[1]();
},
);
$socket = new SocketServer('127.0.0.1:5000');
$http->listen($socket);
echo 'Listening on port 5000' . PHP_EOL;

The Streaming Client

$ npx create-next-app client --use-npm | --use-yarn
const [stop, stopStream] = useState(false)
const [lyrics, addLyrics] = useState([])
useEffect(() => {
const streamLyrics = async () => {
const response = await fetch('http://localhost:5000/api/lyrics')

const reader = response.body
.pipeThrough(new textDecoderStream())
.getReader()
while (true) {
const { value, done } = await reader.read()
if (done) {
stopStream(true)
break
}

addLyrics((prev) => [...new Set(prev.concat([value]))])
}
stopStream(true)
}
// rest of useEffect
}, [lyrics, stop])
if (!stop) {
streamLyrics()
}
return (
<>
<div>
<h2>Lana Del Rey - West Coast Lyrics</h2>
<ul style={{ listStyleType: 'none' }}>
{lyrics.length > 0 &&
lyrics.map((lyric, idx) => (
<li
key={idx.toString()}
style={{ lineHeight: '2.5rem' }}
>
{lyric}
</li>
))}
</ul>
</div>
</>
)

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store