2017-05-14 22:46:33 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Amp;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A cancellation token source provides a mechanism to cancel operations.
|
|
|
|
*
|
|
|
|
* Cancellation of operation works by creating a cancellation token source and passing the corresponding token when
|
|
|
|
* starting the operation. To cancel the operation, invoke `CancellationTokenSource::cancel()`.
|
|
|
|
*
|
|
|
|
* Any operation can decide what to do on a cancellation request, it has "don't care" semantics. An operation SHOULD be
|
|
|
|
* aborted, but MAY continue. Example: A DNS client might continue to receive and cache the response, as the query has
|
|
|
|
* been sent anyway. An HTTP client would usually close a connection, but might not do so in case a response is close to
|
|
|
|
* be fully received to reuse the connection.
|
|
|
|
*
|
|
|
|
* **Example**
|
|
|
|
*
|
|
|
|
* ```php
|
2017-05-16 21:46:52 +02:00
|
|
|
* $tokenSource = new CancellationTokenSource;
|
|
|
|
* $token = $tokenSource->getToken();
|
2017-05-14 22:46:33 +02:00
|
|
|
*
|
2020-08-23 16:18:28 +02:00
|
|
|
* $response = yield $httpClient->request("https://example.com/pipeline", $token);
|
2017-05-14 22:46:33 +02:00
|
|
|
* $responseBody = $response->getBody();
|
|
|
|
*
|
|
|
|
* while (($chunk = yield $response->read()) !== null) {
|
|
|
|
* // consume $chunk
|
|
|
|
*
|
|
|
|
* if ($noLongerInterested) {
|
|
|
|
* $cancellationTokenSource->cancel();
|
|
|
|
* break;
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @see CancellationToken
|
|
|
|
* @see CancelledException
|
|
|
|
*/
|
2018-06-18 20:00:01 +02:00
|
|
|
final class CancellationTokenSource
|
|
|
|
{
|
2020-09-25 05:17:13 +02:00
|
|
|
private CancellationToken $token;
|
2020-03-28 12:23:46 +01:00
|
|
|
|
|
|
|
/** @var callable|null */
|
2017-05-14 22:46:33 +02:00
|
|
|
private $onCancel;
|
|
|
|
|
2018-06-18 20:00:01 +02:00
|
|
|
public function __construct()
|
|
|
|
{
|
2020-10-02 20:55:46 +02:00
|
|
|
$onCancel = &$this->onCancel;
|
2020-03-28 12:23:46 +01:00
|
|
|
|
|
|
|
$this->token = new class($onCancel) implements CancellationToken {
|
2020-09-25 05:17:13 +02:00
|
|
|
private string $nextId = "a";
|
2017-05-22 19:26:09 +02:00
|
|
|
|
|
|
|
/** @var callable[] */
|
2020-09-25 05:17:13 +02:00
|
|
|
private array $callbacks = [];
|
2017-05-22 19:26:09 +02:00
|
|
|
|
|
|
|
/** @var \Throwable|null */
|
2020-09-25 05:17:13 +02:00
|
|
|
private ?\Throwable $exception = null;
|
2017-05-14 22:46:33 +02:00
|
|
|
|
2020-03-28 12:23:46 +01:00
|
|
|
/**
|
2020-10-02 20:55:46 +02:00
|
|
|
* @param callable|null $onCancel
|
2020-03-28 12:23:46 +01:00
|
|
|
* @param-out callable $onCancel
|
|
|
|
*/
|
2020-10-02 20:55:46 +02:00
|
|
|
public function __construct(?callable &$onCancel)
|
2018-06-18 20:00:01 +02:00
|
|
|
{
|
2020-10-02 20:55:46 +02:00
|
|
|
$onCancel = function (\Throwable $exception): void {
|
2017-05-14 22:46:33 +02:00
|
|
|
$this->exception = $exception;
|
|
|
|
|
|
|
|
$callbacks = $this->callbacks;
|
|
|
|
$this->callbacks = [];
|
|
|
|
|
|
|
|
foreach ($callbacks as $callback) {
|
|
|
|
$this->invokeCallback($callback);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-03-28 12:23:46 +01:00
|
|
|
/**
|
|
|
|
* @param callable $callback
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
2020-09-25 05:17:13 +02:00
|
|
|
private function invokeCallback(callable $callback): void
|
2018-06-18 20:00:01 +02:00
|
|
|
{
|
2017-05-14 22:46:33 +02:00
|
|
|
// No type declaration to prevent exception outside the try!
|
|
|
|
try {
|
2020-03-28 12:23:46 +01:00
|
|
|
/** @var mixed $result */
|
2017-05-14 22:46:33 +02:00
|
|
|
$result = $callback($this->exception);
|
|
|
|
|
2020-10-30 16:17:52 +01:00
|
|
|
if ($result instanceof Promise) {
|
2020-10-30 16:36:19 +01:00
|
|
|
Promise\rethrow($result);
|
2017-05-14 22:46:33 +02:00
|
|
|
}
|
|
|
|
} catch (\Throwable $exception) {
|
2020-10-02 20:55:46 +02:00
|
|
|
Loop::defer(static function () use ($exception): void {
|
2017-05-14 22:46:33 +02:00
|
|
|
throw $exception;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-18 20:00:01 +02:00
|
|
|
public function subscribe(callable $callback): string
|
|
|
|
{
|
2017-05-14 22:46:33 +02:00
|
|
|
$id = $this->nextId++;
|
|
|
|
|
|
|
|
if ($this->exception) {
|
|
|
|
$this->invokeCallback($callback);
|
|
|
|
} else {
|
|
|
|
$this->callbacks[$id] = $callback;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $id;
|
|
|
|
}
|
|
|
|
|
2020-09-25 05:17:13 +02:00
|
|
|
public function unsubscribe(string $id): void
|
2018-06-18 20:00:01 +02:00
|
|
|
{
|
2017-05-14 22:46:33 +02:00
|
|
|
unset($this->callbacks[$id]);
|
|
|
|
}
|
2017-05-22 19:26:09 +02:00
|
|
|
|
2018-06-18 20:00:01 +02:00
|
|
|
public function isRequested(): bool
|
|
|
|
{
|
2017-05-22 19:26:09 +02:00
|
|
|
return isset($this->exception);
|
|
|
|
}
|
|
|
|
|
2020-09-25 05:17:13 +02:00
|
|
|
public function throwIfRequested(): void
|
2018-06-18 20:00:01 +02:00
|
|
|
{
|
2017-05-22 19:26:09 +02:00
|
|
|
if (isset($this->exception)) {
|
|
|
|
throw $this->exception;
|
|
|
|
}
|
|
|
|
}
|
2017-05-14 22:46:33 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-06-18 20:00:01 +02:00
|
|
|
public function getToken(): CancellationToken
|
|
|
|
{
|
2017-05-14 22:46:33 +02:00
|
|
|
return $this->token;
|
|
|
|
}
|
|
|
|
|
2017-06-13 19:41:47 +02:00
|
|
|
/**
|
|
|
|
* @param \Throwable|null $previous Exception to be used as the previous exception to CancelledException.
|
|
|
|
*/
|
2020-10-02 20:55:46 +02:00
|
|
|
public function cancel(\Throwable $previous = null): void
|
2018-06-18 20:00:01 +02:00
|
|
|
{
|
2017-05-14 22:46:33 +02:00
|
|
|
if ($this->onCancel === null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$onCancel = $this->onCancel;
|
|
|
|
$this->onCancel = null;
|
2020-10-02 20:55:46 +02:00
|
|
|
Loop::defer(static fn () => $onCancel(new CancelledException($previous)));
|
2017-05-14 22:46:33 +02:00
|
|
|
}
|
|
|
|
}
|