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/
|
/.idea/
|
||||||
/vendor/
|
/vendor/
|
||||||
|
/tags
|
||||||
|
125
README.md
125
README.md
@ -1,61 +1,92 @@
|
|||||||
# ```reactphp-input-stream```
|
# 🚽 Toalett
|
||||||
## 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/).
|
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?
|
## 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?
|
There are only three components to worry about, and one of them is optional! The main components are
|
||||||
I needed to respond to incoming AMQP messages in my event loop and I was feeling adventurous.
|
the [`Source`](src/Source.php) interface and the [`StreamAdapter`](src/StreamAdapter.php) class. The optional component
|
||||||
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?
|
is the [`EndlessTrait`](src/EndlessTrait.php), which can be used in an endless source.
|
||||||
|
|
||||||
I hear you ask: "_Where's the code that deals with the AMQP messages_?"
|
The steps you need to take to use this library are as follows:
|
||||||
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.
|
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`.
|
||||||
|
|
||||||
## Where are the unit tests?
|
_Note:_ This library uses polling under the hood. The default polling interval is 0.5 seconds, though if checking for
|
||||||
_Errrrrr..._
|
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.
|
||||||
|
|
||||||
## How do I install this?
|
_Note:_ The [`StreamAdapter`](src/StreamAdapter.php) reads data eagerly from the source - it won't stop reading until
|
||||||
This should do it [once it's available on Packagist](https://packagist.org/packages/joopschilder/):
|
there is nothing left to read. This prevents congestion when high polling intervals are used but it might block
|
||||||
```bash
|
execution for a while when there is a lot of data to be read or if your `select()` routine takes some time.
|
||||||
composer require joopschilder/reactphp-input-stream
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mandatory block of example code
|
|
||||||
```php
|
```php
|
||||||
// Say, we have an event loop...
|
|
||||||
$loop = Factory::create();
|
$loop = Factory::create();
|
||||||
|
|
||||||
// And say, we have a non-blocking input called $input...
|
$source = new MySource(); // implements Source
|
||||||
$input = new DemoNonBlockingInput();
|
$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();
|
$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",
|
"name": "toalett/react-stream-adapter",
|
||||||
"description": "Wraps a non-blocking input to behave like a stream so it can be used in a ReactPHP EventLoop",
|
|
||||||
"type": "library",
|
"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",
|
"license": "MIT",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Joop Schilder",
|
"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": {
|
"autoload": {
|
||||||
"psr-4": {
|
"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",
|
"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": "eb4b86d14372d5f65b57683c22bd760f",
|
"content-hash": "4c4c46859db5407b7a4dc036efcca228",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "evenement/evenement",
|
"name": "evenement/evenement",
|
||||||
@ -47,6 +47,10 @@
|
|||||||
"event-dispatcher",
|
"event-dispatcher",
|
||||||
"event-emitter"
|
"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"
|
"time": "2017-07-23T21:35:13+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -89,20 +93,24 @@
|
|||||||
"asynchronous",
|
"asynchronous",
|
||||||
"event-loop"
|
"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"
|
"time": "2020-01-01T18:39:52+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "react/stream",
|
"name": "react/stream",
|
||||||
"version": "v1.1.0",
|
"version": "v1.1.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/reactphp/stream.git",
|
"url": "https://github.com/reactphp/stream.git",
|
||||||
"reference": "50426855f7a77ddf43b9266c22320df5bf6c6ce6"
|
"reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/reactphp/stream/zipball/50426855f7a77ddf43b9266c22320df5bf6c6ce6",
|
"url": "https://api.github.com/repos/reactphp/stream/zipball/7c02b510ee3f582c810aeccd3a197b9c2f52ff1a",
|
||||||
"reference": "50426855f7a77ddf43b9266c22320df5bf6c6ce6",
|
"reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -112,7 +120,7 @@
|
|||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"clue/stream-filter": "~1.2",
|
"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",
|
"type": "library",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@ -135,7 +143,11 @@
|
|||||||
"stream",
|
"stream",
|
||||||
"writable"
|
"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": [],
|
"packages-dev": [],
|
||||||
@ -144,6 +156,9 @@
|
|||||||
"stability-flags": [],
|
"stability-flags": [],
|
||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": [],
|
"platform": {
|
||||||
"platform-dev": []
|
"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