Remove PayloadInterface, add README
This commit is contained in:
parent
6a7ea39f5c
commit
4edc6132c4
61
README.md
Normal file
61
README.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# ```reactphp-input-stream```
|
||||||
|
## What is this?
|
||||||
|
A compact library that can wrap a non-blocking input to behave like a [stream](https://reactphp.org/stream/) in a [ReactPHP EventLoop](https://reactphp.org/event-loop/).
|
||||||
|
|
||||||
|
## How do I use this?
|
||||||
|
Check out the `examples` folder. It contains a very basic example of a non-blocking input implementation, as well
|
||||||
|
as a very (_very_) lame joke generator.
|
||||||
|
|
||||||
|
## Why is this useful?
|
||||||
|
I needed to respond to incoming AMQP messages in my event loop and I was feeling adventurous.
|
||||||
|
Admittedly, I could have just used a periodic timer directly (that is what this library uses under the hood), but where's the fun in that?
|
||||||
|
|
||||||
|
I hear you ask: "_Where's the code that deals with the AMQP messages_?"
|
||||||
|
While I was writing this, I felt like it would be useful to
|
||||||
|
separate the logic of dealing with input in a stream-like manner from the logic that
|
||||||
|
deals with AMQP messages.
|
||||||
|
This means you can reuse this library to map practically anything to a readable stream.
|
||||||
|
An extra-lame example of this can be found in `examples/jokes_example.php`.
|
||||||
|
|
||||||
|
When the code for AMQP consumption as stream is finished, I will link it here.
|
||||||
|
|
||||||
|
## Where are the unit tests?
|
||||||
|
_Errrrrr..._
|
||||||
|
|
||||||
|
## How do I install this?
|
||||||
|
This should do it [once it's available on Packagist](https://packagist.org/packages/joopschilder/):
|
||||||
|
```bash
|
||||||
|
composer require joopschilder/reactphp-input-stream
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mandatory block of example code
|
||||||
|
```php
|
||||||
|
// Say, we have an event loop...
|
||||||
|
$loop = Factory::create();
|
||||||
|
|
||||||
|
// And say, we have a non-blocking input called $input...
|
||||||
|
$input = new DemoNonBlockingInput();
|
||||||
|
|
||||||
|
// Then we can create a ReadableStream from it like so:
|
||||||
|
$stream = new ReadableNonBlockingInputStream($input, $loop);
|
||||||
|
|
||||||
|
// If your 'select()' method takes a long time to execute, or you just don't
|
||||||
|
// feel like polling the input availability that often, you can
|
||||||
|
// set a custom polling interval by supplying an instance of PollingInterval
|
||||||
|
// as the third constructor parameter:
|
||||||
|
$lowPollingStream = new ReadableNonBlockingInputStream($input, $loop, new PollingInterval(5));
|
||||||
|
|
||||||
|
// Of course, the stream emits all expected events (except end)
|
||||||
|
$stream->on('data', fn() => print('m'));
|
||||||
|
$stream->on('error', fn() => print('e'));
|
||||||
|
$stream->on('close', fn() => print('c'));
|
||||||
|
|
||||||
|
// If you know what data your input returns, you may type-hint it:
|
||||||
|
$stream->on('data', fn(Joke $joke) => print($joke . PHP_EOL));
|
||||||
|
|
||||||
|
// Add a periodic timer for demonstration purposes
|
||||||
|
$loop->addPeriodicTimer(0.2, fn() => print('.'));
|
||||||
|
|
||||||
|
// And kick 'er off.
|
||||||
|
$loop->run();
|
||||||
|
```
|
@ -1,21 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "joopschilder/reactphp-input-stream",
|
"name": "joopschilder/reactphp-input-stream",
|
||||||
"description": "Wraps a non-blocking input to behave like a stream so it can be used in a ReactPHP EventLoop",
|
"description": "Wraps a non-blocking input to behave like a stream so it can be used in a ReactPHP EventLoop",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Joop Schilder",
|
"name": "Joop Schilder",
|
||||||
"email": "jnmschilder@protonmail.com"
|
"email": "jnmschilder@protonmail.com"
|
||||||
}
|
|
||||||
],
|
|
||||||
"require": {
|
|
||||||
"react/event-loop": "^1.1",
|
|
||||||
"react/stream": "^1.1"
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"JoopSchilder\\React\\Stream\\NonBlockingInput\\": "src"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^7.4",
|
||||||
|
"react/event-loop": "^1.1",
|
||||||
|
"react/stream": "^1.1"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"JoopSchilder\\React\\Stream\\NonBlockingInput\\": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
2
composer.lock
generated
2
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "8cb0549e866a693d6b61e7e7f9a75862",
|
"content-hash": "eb4b86d14372d5f65b57683c22bd760f",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "evenement/evenement",
|
"name": "evenement/evenement",
|
||||||
|
@ -4,12 +4,8 @@ use JoopSchilder\React\Stream\NonBlockingInput\ReadableNonBlockingInputStream;
|
|||||||
use React\EventLoop\Factory;
|
use React\EventLoop\Factory;
|
||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once __DIR__ . '/basic_example_classes.php';
|
require_once __DIR__ . '/demo_classes.php';
|
||||||
|
|
||||||
/**
|
|
||||||
* The input will have data available every second.
|
|
||||||
* After 4 seconds have passed, it will generate an error.
|
|
||||||
*/
|
|
||||||
$input = new DemoNonBlockingInput();
|
$input = new DemoNonBlockingInput();
|
||||||
|
|
||||||
$loop = Factory::create();
|
$loop = Factory::create();
|
||||||
@ -19,4 +15,3 @@ $stream->on('error', fn() => print('e'));
|
|||||||
$stream->on('close', fn() => print('c'));
|
$stream->on('close', fn() => print('c'));
|
||||||
$loop->addPeriodicTimer(0.2, fn() => print('.'));
|
$loop->addPeriodicTimer(0.2, fn() => print('.'));
|
||||||
$loop->run();
|
$loop->run();
|
||||||
|
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use JoopSchilder\React\Stream\NonBlockingInput\NonBlockingInputInterface;
|
|
||||||
use JoopSchilder\React\Stream\NonBlockingInput\PayloadInterface;
|
|
||||||
|
|
||||||
final class DemoEmptyPayload implements PayloadInterface
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
final class DemoNonBlockingInput implements NonBlockingInputInterface
|
|
||||||
{
|
|
||||||
private const DATA_AVAILABLE_INTERVAL_S = 1.0;
|
|
||||||
private const ERROR_AFTER_S = 4.0;
|
|
||||||
|
|
||||||
private float $lastEmission = 0;
|
|
||||||
|
|
||||||
private float $initialEmission = 0;
|
|
||||||
|
|
||||||
|
|
||||||
public function open(): void
|
|
||||||
{
|
|
||||||
$this->initialEmission = microtime(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function select(): ?PayloadInterface
|
|
||||||
{
|
|
||||||
$now = microtime(true);
|
|
||||||
if ($now - $this->initialEmission > self::ERROR_AFTER_S) {
|
|
||||||
throw new Exception('Oh no!');
|
|
||||||
}
|
|
||||||
if ($now - $this->lastEmission > self::DATA_AVAILABLE_INTERVAL_S) {
|
|
||||||
$this->lastEmission = $now;
|
|
||||||
|
|
||||||
return new DemoEmptyPayload();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function close(): void
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
124
examples/demo_classes.php
Normal file
124
examples/demo_classes.php
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use JoopSchilder\React\Stream\NonBlockingInput\NonBlockingInputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class DemoNonBlockingInput
|
||||||
|
* Provides an implementation of a non-blocking input that generates
|
||||||
|
* some data every second. After a lifetime of 4 seconds, an exception
|
||||||
|
* is thrown to show how the stream handles exceptions from the input.
|
||||||
|
*/
|
||||||
|
final class DemoNonBlockingInput implements NonBlockingInputInterface
|
||||||
|
{
|
||||||
|
private const DATA_AVAILABLE_INTERVAL_S = 1.0;
|
||||||
|
private const ERROR_AFTER_S = 4.0;
|
||||||
|
|
||||||
|
private float $lastEmission = 0;
|
||||||
|
|
||||||
|
private float $initialEmission = 0;
|
||||||
|
|
||||||
|
|
||||||
|
public function open(): void
|
||||||
|
{
|
||||||
|
$this->initialEmission = microtime(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function select(): ?object
|
||||||
|
{
|
||||||
|
$now = microtime(true);
|
||||||
|
if ($now - $this->initialEmission > self::ERROR_AFTER_S) {
|
||||||
|
throw new Exception('Oh no!');
|
||||||
|
}
|
||||||
|
if ($now - $this->lastEmission > self::DATA_AVAILABLE_INTERVAL_S) {
|
||||||
|
$this->lastEmission = $now;
|
||||||
|
|
||||||
|
return new stdClass();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function close(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Joke
|
||||||
|
* Used as a payload for the DemoJokeGenerator.
|
||||||
|
* @see DemoJokeGenerator
|
||||||
|
*/
|
||||||
|
final class Joke
|
||||||
|
{
|
||||||
|
private string $joke;
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct(string $joke)
|
||||||
|
{
|
||||||
|
$this->joke = $joke;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return $this->joke;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class DemoJokeGenerator
|
||||||
|
* Generates a random joke at a random interval (0 - 2 seconds).
|
||||||
|
*/
|
||||||
|
final class DemoJokeGenerator implements NonBlockingInputInterface
|
||||||
|
{
|
||||||
|
private float $lastEmission = 0;
|
||||||
|
|
||||||
|
private float $deadline = 0;
|
||||||
|
|
||||||
|
private array $jokes = [
|
||||||
|
'What did the Buddhist ask the hot dog vendor? - Make me one with everything.',
|
||||||
|
'You know why you never see elephants hiding up in trees? - Because they’re really good at it.',
|
||||||
|
'What is red and smells like blue paint? - Red paint.',
|
||||||
|
'A dyslexic man walks into a bra.',
|
||||||
|
'Where does the General keep his armies? - In his sleevies!',
|
||||||
|
'What do you call bears with no ears? - B',
|
||||||
|
'Why dont blind people skydive? - Because it scares the crap out of their dogs.',
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
private function scheduleNextJoke()
|
||||||
|
{
|
||||||
|
$this->lastEmission = microtime(true);
|
||||||
|
$delay = mt_rand() / mt_getrandmax();
|
||||||
|
$this->deadline = $this->lastEmission + (2.0 * $delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function open(): void
|
||||||
|
{
|
||||||
|
$this->scheduleNextJoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function select(): ?object
|
||||||
|
{
|
||||||
|
$now = microtime(true);
|
||||||
|
if ($now > $this->deadline) {
|
||||||
|
$this->scheduleNextJoke();
|
||||||
|
|
||||||
|
return new Joke($this->jokes[array_rand($this->jokes)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function close(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
16
examples/jokes_example.php
Normal file
16
examples/jokes_example.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use JoopSchilder\React\Stream\NonBlockingInput\ReadableNonBlockingInputStream;
|
||||||
|
use React\EventLoop\Factory;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/demo_classes.php';
|
||||||
|
|
||||||
|
$input = new DemoJokeGenerator();
|
||||||
|
|
||||||
|
$loop = Factory::create();
|
||||||
|
$stream = new ReadableNonBlockingInputStream($input, $loop);
|
||||||
|
$stream->on('data', fn(Joke $joke) => print($joke . PHP_EOL));
|
||||||
|
$loop->addPeriodicTimer(0.2, fn() => print('.' . PHP_EOL));
|
||||||
|
$loop->run();
|
||||||
|
|
@ -8,8 +8,8 @@ interface NonBlockingInputInterface
|
|||||||
function open(): void;
|
function open(): void;
|
||||||
|
|
||||||
|
|
||||||
function select(): ?PayloadInterface;
|
function select(): ?object;
|
||||||
|
|
||||||
|
|
||||||
function close(): void;
|
function close(): void;
|
||||||
|
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace JoopSchilder\React\Stream\NonBlockingInput;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marker interface.
|
|
||||||
*/
|
|
||||||
interface PayloadInterface
|
|
||||||
{
|
|
||||||
}
|
|
@ -27,9 +27,9 @@ final class ReadableNonBlockingInputStream extends EventEmitter implements Reada
|
|||||||
|
|
||||||
private ?TimerInterface $periodicTimer = null;
|
private ?TimerInterface $periodicTimer = null;
|
||||||
|
|
||||||
private bool $closed = false;
|
private bool $isClosed = false;
|
||||||
|
|
||||||
private bool $listening = false;
|
private bool $isListening = false;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,7 +48,7 @@ final class ReadableNonBlockingInputStream extends EventEmitter implements Reada
|
|||||||
|
|
||||||
public function isReadable()
|
public function isReadable()
|
||||||
{
|
{
|
||||||
return !$this->closed;
|
return !$this->isClosed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -58,13 +58,13 @@ final class ReadableNonBlockingInputStream extends EventEmitter implements Reada
|
|||||||
*/
|
*/
|
||||||
public function pause()
|
public function pause()
|
||||||
{
|
{
|
||||||
if (!$this->listening) {
|
if (!$this->isListening) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->input->close();
|
$this->input->close();
|
||||||
$this->loop->cancelTimer($this->periodicTimer);
|
$this->loop->cancelTimer($this->periodicTimer);
|
||||||
$this->listening = false;
|
$this->isListening = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ final class ReadableNonBlockingInputStream extends EventEmitter implements Reada
|
|||||||
*/
|
*/
|
||||||
public function resume()
|
public function resume()
|
||||||
{
|
{
|
||||||
if ($this->listening || $this->closed) {
|
if ($this->isListening || $this->isClosed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ final class ReadableNonBlockingInputStream extends EventEmitter implements Reada
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->listening = true;
|
$this->isListening = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -109,11 +109,11 @@ final class ReadableNonBlockingInputStream extends EventEmitter implements Reada
|
|||||||
*/
|
*/
|
||||||
public function close()
|
public function close()
|
||||||
{
|
{
|
||||||
if ($this->closed) {
|
if ($this->isClosed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->closed = true;
|
$this->isClosed = true;
|
||||||
|
|
||||||
$this->emit('close');
|
$this->emit('close');
|
||||||
$this->pause();
|
$this->pause();
|
||||||
@ -123,7 +123,7 @@ final class ReadableNonBlockingInputStream extends EventEmitter implements Reada
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private function read(): ?PayloadInterface
|
private function read(): ?object
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return $this->input->select();
|
return $this->input->select();
|
||||||
|
Loading…
Reference in New Issue
Block a user