Simplified codebase in a major refactor before moving to initial Toalett release
This commit is contained in:
parent
4edc6132c4
commit
baca5b129e
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
/.idea/
|
||||
/vendor/
|
||||
/tags
|
||||
|
129
README.md
129
README.md
@ -1,61 +1,92 @@
|
||||
# ```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/).
|
||||
# 🚽 Toalett
|
||||
|
||||
Welcome to Toalett, a humble initiative. Toalett is the Norwegian word for toilet 💩.
|
||||
|
||||
## What is `toalett/react-stream-adapter`?
|
||||
|
||||
It is a library that allows any datasource to be used as a stream with ReactPHP. It is very small - there
|
||||
is [one interface](src/Source.php), [one class](src/StreamAdapter.php)
|
||||
and [one trait](src/EndlessTrait.php). Its only dependency is `react/stream`.
|
||||
|
||||
The [`StreamAdapter`](src/StreamAdapter.php) takes an implementation of the [`Source`](src/Source.php) interface and
|
||||
makes it approachable as a [stream](https://reactphp.org/stream/) in applications using
|
||||
an [event loop](https://reactphp.org/event-loop/).
|
||||
|
||||
## Installation
|
||||
|
||||
It is available on [Packagist](https://packagist.org/packages/toalett/):
|
||||
|
||||
```bash
|
||||
composer require toalett/react-stream-adapter
|
||||
```
|
||||
|
||||
## Motivation
|
||||
|
||||
I was working on a project that required an application to respond to AMQP messages in a non-blocking way. The
|
||||
application made use of an event loop. Initially I used a periodic timer with a callback, but as the application grew
|
||||
this became a cluttered mess. It slowly started to feel more natural to treat the message queue as a stream. This makes
|
||||
sense if you think about it:
|
||||
|
||||
> In computer science, a stream is a sequence of data elements made available over time. A stream can be thought of as items
|
||||
> on a conveyor belt being processed one at a time rather than in large batches.
|
||||
>
|
||||
> — <cite> [Stream (computing) on Wikipedia](https://en.wikipedia.org/wiki/Stream_(computing)) </cite>
|
||||
|
||||
This definition suits a message queue.
|
||||
|
||||
In the project I mentioned earlier, I use this library to poll an AMQP queue every 10 seconds. This keeps my load low
|
||||
and allows me to do other things in the meantime. This abstraction turned out really useful, so I thought that others
|
||||
might enjoy it too.
|
||||
|
||||
## 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.
|
||||
There are only three components to worry about, and one of them is optional! The main components are
|
||||
the [`Source`](src/Source.php) interface and the [`StreamAdapter`](src/StreamAdapter.php) class. The optional component
|
||||
is the [`EndlessTrait`](src/EndlessTrait.php), which can be used in an endless source.
|
||||
|
||||
## Where are the unit tests?
|
||||
_Errrrrr..._
|
||||
The steps you need to take to use this library are as follows:
|
||||
|
||||
## 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
|
||||
```
|
||||
1. Define a class that is able to generate or provide some data. It must implement the [`Source`](src/Source.php)
|
||||
interface.
|
||||
2. The `select()` method is called periodically. This is where you return your next piece of data. Make sure
|
||||
the `select()` method returns anything that is not `null` when data is available (anything goes) or `null`
|
||||
when there is none. You may add a typehint to your implementation of `select()` such as `select(): ?string`
|
||||
or `select(): ?MyData` for improved clarity.
|
||||
3. The interface also specifies the `close(): void` and `eof(): bool` methods. In an endless (infinite)
|
||||
stream, `close()` may be left empty and `eof()` should return false (EOF is never reached).
|
||||
The [`EndlessTrait`](src/EndlessTrait.php)
|
||||
provides these implementations.
|
||||
4. Use the [`StreamAdapter`](src/StreamAdapter.php) to attach your [`Source`](src/Source.php) to the loop.
|
||||
5. Interact with the adapter as if it were any other `ReadableInputStream`.
|
||||
|
||||
_Note:_ This library uses polling under the hood. The default polling interval is 0.5 seconds, though if checking for
|
||||
data is an intensive operation, you might want to increase the interval a bit to prevent slowdowns. This is a tradeoff
|
||||
between responsiveness and load. Custom intervals can be set by passing them as a third argument to
|
||||
the [`StreamAdapter`](src/StreamAdapter.php) constructor.
|
||||
|
||||
_Note:_ The [`StreamAdapter`](src/StreamAdapter.php) reads data eagerly from the source - it won't stop reading until
|
||||
there is nothing left to read. This prevents congestion when high polling intervals are used but it might block
|
||||
execution for a while when there is a lot of data to be read or if your `select()` routine takes some time.
|
||||
|
||||
## 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();
|
||||
$source = new MySource(); // implements Source
|
||||
$stream = new StreamAdapter($source, $loop);
|
||||
$stream->on('data', function(MyData $data) {
|
||||
/* do something with data */
|
||||
});
|
||||
|
||||
// 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();
|
||||
```
|
||||
|
||||
Check out the [examples](examples) folder for some simple implementations.
|
||||
|
||||
## Questions
|
||||
|
||||
__Q__: _Where is the code that deals with AMQP messages_?
|
||||
__A__: It will be released in a separate package, but it needs some work before it can be published.
|
||||
|
||||
__Q__: _Where are the tests_?
|
||||
__A__: There is only one class, and it is mostly based on the `ReadableResourceStream` from `react/stream`. Tests might
|
||||
be added later, but as of now, I don't really see the value. Feel free to create an issue if this bothers you!
|
||||
|
@ -1,22 +1,36 @@
|
||||
{
|
||||
"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",
|
||||
"name": "toalett/react-stream-adapter",
|
||||
"type": "library",
|
||||
"description": "Use any data source as a stream with ReactPHP",
|
||||
"keywords": [
|
||||
"react",
|
||||
"reactphp",
|
||||
"event-driven",
|
||||
"non-blocking",
|
||||
"readable",
|
||||
"eventloop",
|
||||
"io",
|
||||
"nio",
|
||||
"stream",
|
||||
"source",
|
||||
"adapter"
|
||||
],
|
||||
"homepage": "https://github.com/toalett-io/react-stream-adapter",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Joop Schilder",
|
||||
"email": "jnmschilder@protonmail.com"
|
||||
"email": "jnmschilder@protonmail.com",
|
||||
"homepage": "https://joopschilder.nl/"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.4",
|
||||
"react/event-loop": "^1.1",
|
||||
"react/stream": "^1.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"JoopSchilder\\React\\Stream\\NonBlockingInput\\": "src"
|
||||
"Toalett\\React\\Stream\\": "src"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4",
|
||||
"react/stream": "^1.0"
|
||||
}
|
||||
}
|
||||
|
33
composer.lock
generated
33
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "eb4b86d14372d5f65b57683c22bd760f",
|
||||
"content-hash": "4c4c46859db5407b7a4dc036efcca228",
|
||||
"packages": [
|
||||
{
|
||||
"name": "evenement/evenement",
|
||||
@ -47,6 +47,10 @@
|
||||
"event-dispatcher",
|
||||
"event-emitter"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/igorw/evenement/issues",
|
||||
"source": "https://github.com/igorw/evenement/tree/master"
|
||||
},
|
||||
"time": "2017-07-23T21:35:13+00:00"
|
||||
},
|
||||
{
|
||||
@ -89,20 +93,24 @@
|
||||
"asynchronous",
|
||||
"event-loop"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/reactphp/event-loop/issues",
|
||||
"source": "https://github.com/reactphp/event-loop/tree/v1.1.1"
|
||||
},
|
||||
"time": "2020-01-01T18:39:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "react/stream",
|
||||
"version": "v1.1.0",
|
||||
"version": "v1.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/reactphp/stream.git",
|
||||
"reference": "50426855f7a77ddf43b9266c22320df5bf6c6ce6"
|
||||
"reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/reactphp/stream/zipball/50426855f7a77ddf43b9266c22320df5bf6c6ce6",
|
||||
"reference": "50426855f7a77ddf43b9266c22320df5bf6c6ce6",
|
||||
"url": "https://api.github.com/repos/reactphp/stream/zipball/7c02b510ee3f582c810aeccd3a197b9c2f52ff1a",
|
||||
"reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -112,7 +120,7 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"clue/stream-filter": "~1.2",
|
||||
"phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35"
|
||||
"phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
@ -135,7 +143,11 @@
|
||||
"stream",
|
||||
"writable"
|
||||
],
|
||||
"time": "2019-01-01T16:15:09+00:00"
|
||||
"support": {
|
||||
"issues": "https://github.com/reactphp/stream/issues",
|
||||
"source": "https://github.com/reactphp/stream/tree/v1.1.1"
|
||||
},
|
||||
"time": "2020-05-04T10:17:57+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
@ -144,6 +156,9 @@
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": []
|
||||
"platform": {
|
||||
"php": "^7.4"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.0.0"
|
||||
}
|
||||
|
@ -1,17 +0,0 @@
|
||||
<?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 DemoNonBlockingInput();
|
||||
|
||||
$loop = Factory::create();
|
||||
$stream = new ReadableNonBlockingInputStream($input, $loop);
|
||||
$stream->on('data', fn() => print('m'));
|
||||
$stream->on('error', fn() => print('e'));
|
||||
$stream->on('close', fn() => print('c'));
|
||||
$loop->addPeriodicTimer(0.2, fn() => print('.'));
|
||||
$loop->run();
|
99
examples/classes.php
Normal file
99
examples/classes.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
use Toalett\React\Stream\EndlessTrait;
|
||||
use Toalett\React\Stream\Source;
|
||||
|
||||
final class WillThrowExceptionAfter4Seconds implements Source
|
||||
{
|
||||
use EndlessTrait;
|
||||
|
||||
private const EMISSION_INTERVAL = 1.0;
|
||||
private const SECONDS_BEFORE_EMITTING_ERROR = 4.0;
|
||||
private float $lastEmission = 0;
|
||||
private float $openedAt = 0;
|
||||
|
||||
public function open(): void
|
||||
{
|
||||
$this->openedAt = microtime(true);
|
||||
}
|
||||
|
||||
public function select(): ?float
|
||||
{
|
||||
$now = microtime(true);
|
||||
if ($now - $this->openedAt > self::SECONDS_BEFORE_EMITTING_ERROR) {
|
||||
throw new RuntimeException('An error has occured!');
|
||||
}
|
||||
if ($now - $this->lastEmission > self::EMISSION_INTERVAL) {
|
||||
$this->lastEmission = $now;
|
||||
return $now;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final class Joker implements Source
|
||||
{
|
||||
use EndlessTrait;
|
||||
|
||||
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.',
|
||||
];
|
||||
|
||||
public function open(): void
|
||||
{
|
||||
$this->scheduleNextJoke();
|
||||
}
|
||||
|
||||
public function select(): ?string
|
||||
{
|
||||
if (microtime(true) < $this->deadline) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->scheduleNextJoke();
|
||||
return $this->jokes[array_rand($this->jokes)];
|
||||
}
|
||||
|
||||
private function scheduleNextJoke(): void
|
||||
{
|
||||
$delay = mt_rand() / mt_getrandmax(); // somewhere between 0 - 1
|
||||
$this->deadline = microtime(true) + (5.0 * $delay);
|
||||
}
|
||||
}
|
||||
|
||||
final class ReachesEofIn5Iterations implements Source
|
||||
{
|
||||
private array $buffer = [
|
||||
'line 1',
|
||||
'line 2',
|
||||
'line 3',
|
||||
'line 4',
|
||||
'line 5',
|
||||
];
|
||||
|
||||
public function open(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function select(): ?string
|
||||
{
|
||||
return array_shift($this->buffer);
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function eof(): bool
|
||||
{
|
||||
return count($this->buffer) === 0;
|
||||
}
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
<?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
|
||||
{
|
||||
}
|
||||
|
||||
}
|
29
examples/eof_in_5_iterations.php
Normal file
29
examples/eof_in_5_iterations.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use React\EventLoop\Factory;
|
||||
use Toalett\React\Stream\StreamAdapter;
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/classes.php';
|
||||
|
||||
$loop = Factory::create();
|
||||
|
||||
$stream = new StreamAdapter(new ReachesEofIn5Iterations(), $loop, 0.1);
|
||||
$stream->on('data', fn(string $s) => printf('Data received: %s.%s', $s, PHP_EOL));
|
||||
$stream->on('end', fn() => print('Stream reached eof.' . PHP_EOL));
|
||||
$stream->on('close', fn() => print('Stream closed.' . PHP_EOL));
|
||||
|
||||
print(<<<EOF
|
||||
This program demonstrates an example of a source that reaches EOF after 5 lines.
|
||||
|
||||
The stream adapter reads eagerly from the source: data is emitted as long as
|
||||
select() on the source returns a non-null value. This means that all lines from
|
||||
the source in this example ar read at once. If you want the adapter to read
|
||||
at most one unit (message) from the source, you should probably be using a
|
||||
periodic timer directly, or use time mechanics as used by the other examples.
|
||||
|
||||
|
||||
EOF
|
||||
);
|
||||
|
||||
$loop->run();
|
25
examples/exception_after_4_seconds.php
Normal file
25
examples/exception_after_4_seconds.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use React\EventLoop\Factory;
|
||||
use Toalett\React\Stream\StreamAdapter;
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/classes.php';
|
||||
|
||||
$loop = Factory::create();
|
||||
$loop->addPeriodicTimer(0.2, fn() => print('.'));
|
||||
|
||||
$stream = new StreamAdapter(new WillThrowExceptionAfter4Seconds(), $loop);
|
||||
$stream->on('data', fn() => print('Data received.' . PHP_EOL));
|
||||
$stream->on('error', fn(Throwable $t) => print('Error: ' . $t->getMessage() . PHP_EOL));
|
||||
$stream->on('close', fn() => print('Stream closed.' . PHP_EOL));
|
||||
|
||||
print(<<<EOF
|
||||
This program demonstrates an example of a source that fails after 4 seconds.
|
||||
Press CTRL+C to stop.
|
||||
|
||||
|
||||
EOF
|
||||
);
|
||||
|
||||
$loop->run();
|
23
examples/infinite_stream_of_jokes.php
Normal file
23
examples/infinite_stream_of_jokes.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use React\EventLoop\Factory;
|
||||
use Toalett\React\Stream\StreamAdapter;
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/classes.php';
|
||||
|
||||
$loop = Factory::create();
|
||||
|
||||
$stream = new StreamAdapter(new Joker(), $loop);
|
||||
$stream->on('data', fn(string $joke) => print($joke . PHP_EOL));
|
||||
|
||||
print(<<<EOF
|
||||
This program demonstrates an example of an endless source.
|
||||
The stream presents a joke at random intervals (0.0 - 5.0 seconds).
|
||||
Press CTRL+C to stop.
|
||||
|
||||
|
||||
EOF
|
||||
);
|
||||
|
||||
$loop->run();
|
@ -1,16 +0,0 @@
|
||||
<?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();
|
||||
|
15
src/EndlessTrait.php
Normal file
15
src/EndlessTrait.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Toalett\React\Stream;
|
||||
|
||||
trait EndlessTrait
|
||||
{
|
||||
public function close(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function eof(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace JoopSchilder\React\Stream\NonBlockingInput;
|
||||
|
||||
interface NonBlockingInputInterface
|
||||
{
|
||||
|
||||
function open(): void;
|
||||
|
||||
|
||||
function select(): ?object;
|
||||
|
||||
|
||||
function close(): void;
|
||||
|
||||
}
|
5
src/README.md
Normal file
5
src/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
Three files, because there are only three things to do:
|
||||
|
||||
- Create a data source by implementing `Source`.
|
||||
- If it is endless, use the `EndlessTrait` in your implementation.
|
||||
- Create a readable stream with the `StreamAdapter`.
|
@ -1,140 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace JoopSchilder\React\Stream\NonBlockingInput;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use JoopSchilder\React\Stream\NonBlockingInput\ValueObject\PollingInterval;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\EventLoop\TimerInterface;
|
||||
use React\Stream\ReadableResourceStream;
|
||||
use React\Stream\ReadableStreamInterface;
|
||||
use React\Stream\Util;
|
||||
use React\Stream\WritableStreamInterface;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @see ReadableStreamInterface
|
||||
* @see ReadableResourceStream
|
||||
*/
|
||||
final class ReadableNonBlockingInputStream extends EventEmitter implements ReadableStreamInterface
|
||||
{
|
||||
private NonBlockingInputInterface $input;
|
||||
|
||||
private LoopInterface $loop;
|
||||
|
||||
private PollingInterval $interval;
|
||||
|
||||
private ?TimerInterface $periodicTimer = null;
|
||||
|
||||
private bool $isClosed = false;
|
||||
|
||||
private bool $isListening = false;
|
||||
|
||||
|
||||
/**
|
||||
* @see ReadableStreamInterface
|
||||
* @see ReadableResourceStream for an example
|
||||
*/
|
||||
public function __construct(NonBlockingInputInterface $input, LoopInterface $loop, ?PollingInterval $interval = null)
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->loop = $loop;
|
||||
$this->interval = $interval ?? new PollingInterval();
|
||||
|
||||
$this->resume();
|
||||
}
|
||||
|
||||
|
||||
public function isReadable()
|
||||
{
|
||||
return !$this->isClosed;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see ReadableResourceStream::pause()
|
||||
* Pause consumption of the AMQP queue but do not mark the stream as closed
|
||||
*/
|
||||
public function pause()
|
||||
{
|
||||
if (!$this->isListening) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->input->close();
|
||||
$this->loop->cancelTimer($this->periodicTimer);
|
||||
$this->isListening = false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see ReadableResourceStream::resume()
|
||||
* Register the consumer with the broker and add consumer again
|
||||
*/
|
||||
public function resume()
|
||||
{
|
||||
if ($this->isListening || $this->isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->input->open();
|
||||
$this->periodicTimer = $this->loop->addPeriodicTimer(
|
||||
$this->interval->getInterval(),
|
||||
function () {
|
||||
if ($data = $this->read()) {
|
||||
$this->emit('data', [$data]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$this->isListening = true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param WritableStreamInterface $dest
|
||||
* @param array $options
|
||||
* @return WritableStreamInterface
|
||||
* @see ReadableResourceStream::pipe()
|
||||
*/
|
||||
public function pipe(WritableStreamInterface $dest, array $options = [])
|
||||
{
|
||||
return Util::pipe($this, $dest, $options);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see ReadableResourceStream::close()
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
if ($this->isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->isClosed = true;
|
||||
|
||||
$this->emit('close');
|
||||
$this->pause();
|
||||
$this->removeAllListeners();
|
||||
|
||||
$this->input->close();
|
||||
}
|
||||
|
||||
|
||||
private function read(): ?object
|
||||
{
|
||||
try {
|
||||
return $this->input->select();
|
||||
} catch (Throwable $t) {
|
||||
$this->emit('error', [
|
||||
new RuntimeException('Unable to read data from input: ' . $t->getMessage(), 0, $t),
|
||||
]);
|
||||
$this->close();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
17
src/Source.php
Normal file
17
src/Source.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Toalett\React\Stream;
|
||||
|
||||
interface Source
|
||||
{
|
||||
public function open(): void;
|
||||
|
||||
/**
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function select();
|
||||
|
||||
public function close(): void;
|
||||
|
||||
public function eof(): bool;
|
||||
}
|
107
src/StreamAdapter.php
Normal file
107
src/StreamAdapter.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Toalett\React\Stream;
|
||||
|
||||
use Evenement\EventEmitterTrait;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\EventLoop\TimerInterface;
|
||||
use React\Stream\ReadableResourceStream;
|
||||
use React\Stream\ReadableStreamInterface;
|
||||
use React\Stream\Util;
|
||||
use React\Stream\WritableStreamInterface;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @see ReadableResourceStream
|
||||
*/
|
||||
final class StreamAdapter implements ReadableStreamInterface
|
||||
{
|
||||
use EventEmitterTrait;
|
||||
|
||||
private Source $source;
|
||||
private LoopInterface $loop;
|
||||
private float $pollingInterval;
|
||||
private ?TimerInterface $timer = null;
|
||||
private bool $closed = false;
|
||||
private bool $listening = false;
|
||||
|
||||
public function __construct(Source $source, LoopInterface $loop, float $pollingInterval = 0.5)
|
||||
{
|
||||
$this->source = $source;
|
||||
$this->loop = $loop;
|
||||
$this->pollingInterval = $pollingInterval;
|
||||
|
||||
$this->resume();
|
||||
}
|
||||
|
||||
public function isReadable(): bool
|
||||
{
|
||||
return !$this->closed;
|
||||
}
|
||||
|
||||
public function pause(): void
|
||||
{
|
||||
if (!$this->listening) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->source->close();
|
||||
$this->loop->cancelTimer($this->timer);
|
||||
$this->listening = false;
|
||||
}
|
||||
|
||||
public function resume(): void
|
||||
{
|
||||
if ($this->listening || $this->closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->source->open();
|
||||
$this->timer = $this->loop->addPeriodicTimer($this->pollingInterval, function () {
|
||||
while (!is_null($data = $this->read())) {
|
||||
$this->emit('data', [$data]);
|
||||
}
|
||||
if ($this->source->eof()) {
|
||||
$this->emit('end');
|
||||
$this->close();
|
||||
}
|
||||
});
|
||||
$this->listening = true;
|
||||
}
|
||||
|
||||
public function pipe(WritableStreamInterface $dest, array $options = []): WritableStreamInterface
|
||||
{
|
||||
return Util::pipe($this, $dest, $options);
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
if ($this->closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->closed = true;
|
||||
|
||||
$this->emit('close');
|
||||
$this->pause();
|
||||
$this->removeAllListeners();
|
||||
|
||||
$this->source->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed|null
|
||||
*/
|
||||
private function read()
|
||||
{
|
||||
try {
|
||||
return $this->source->select();
|
||||
} catch (Throwable $t) {
|
||||
$this->emit('error', [new RuntimeException('Unable to read data from source: ' . $t->getMessage(), 0, $t)]);
|
||||
$this->close();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace JoopSchilder\React\Stream\NonBlockingInput\ValueObject;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class PollingInterval
|
||||
{
|
||||
private const DEFAULT_INTERVAL = 0.05;
|
||||
|
||||
private float $interval;
|
||||
|
||||
|
||||
public function __construct(float $interval = self::DEFAULT_INTERVAL)
|
||||
{
|
||||
if ($interval < 0.0) {
|
||||
throw new InvalidArgumentException('Interval must be greater than 0');
|
||||
}
|
||||
|
||||
$this->interval = $interval;
|
||||
}
|
||||
|
||||
|
||||
public function getInterval(): float
|
||||
{
|
||||
return $this->interval;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user