10000 Cancellation support, timeout option and improve long paginations · clue/reactphp-packagist-api@f4d0b59 · GitHub
[go: up one dir, main page]

Skip to content

Commit f4d0b59

Browse files
committed
Cancellation support, timeout option and improve long paginations
1 parent 64158b1 commit f4d0b59

File tree

5 files changed

+132
-13
lines changed

5 files changed

+132
-13
lines changed

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ enriched with the comfort of [ReactPHP's Promises](https://github.com/reactphp/p
1818
* [Usage](#usage)
1919
* [Client](#client)
2020
* [Promises](#promises)
21+
* [Cancellation](#cancellation)
22+
* [Timeouts](#timeouts)
2123
* [search()](#search)
2224
* [get()](#get)
2325
* [all()](#all)
@@ -92,6 +94,68 @@ Sending requests is async (non-blocking), so you can actually send multiple requ
9294
Packagist will respond to each request with a response message, the order is not guaranteed.
9395
Sending requests uses a [Promise](https://github.com/reactphp/promise)-based interface that makes it easy to react to when a request is fulfilled (i.e. either successfully resolved or rejected with an error).
9496

97+
```php
98+
$client->get('clue/graph-composer')->then(
99+
function ($result) {
100+
// result received for get() function
101+
},
102+
function (Exception $e) {
103+
// an error occured while executing the request
104+
}
105+
});
106+
```
107+
108+
#### Cancellation
109+
110+
The returned Promise is implemented in such a way that it can be cancelled
111+
when it is still pending.
112+
Cancelling a pending promise will reject its value with an Exception and
113+
clean up any underlying resources.
114+
115+
```php
116+
$promise = $client->get('clue/graph-composer');
117+
118+
$loop->addTimer(2.0, function () use ($promise) {
119+
$promise->cancel();
120+
});
121+
```
122+
123+
#### Timeouts
124+
125+
This library uses a very efficient HTTP implementation, so most API requests
126+
should usually be completed in mere milliseconds. However, when sending API
127+
requests over an unreliable network (the internet), there are a number of things
128+
that can go wrong and may cause the request to fail after a time. As such,
129+
timeouts are handled by the underlying HTTP library and this library respects
130+
PHP's `default_socket_timeout` setting (default 60s) as a timeout for sending the
131+
outgoing API request and waiting for a successful response and will otherwise
132+
cancel the pending request and reject its value with an Exception.
133+
134+
Note that this timeout value covers creating the underlying transport connection,
135+
sending the API request, waiting for the Packagist service to process the request
136+
and receiving the full API response. To pass a custom timeout value, you can
137+
assign the underlying [`timeout` option](https://github.com/clue/reactphp-buzz#timeouts)
138+
like this:
139+
140+
```php
141+
$browser = new Browser($loop);
142+
$browser = $browser->withOptions(array(
143+
'timeout' => 10.0
144+
));
145+
146+
$client = new Client($browser);
147+
148+
$client->get('clue/graph-composer')->then(function ($result) {
149+
// result received within 10 seconds maximum
150+
var_dump($result);
151+
});
152+
```
153+
154+
Similarly, you can use a negative timeout value to not apply a timeout at all
155+
or use a `null` value to restore the default handling. Note that the underlying
156+
connection may still impose a different timeout value. See also the underlying
157+
[`timeout` option](https://github.com/clue/reactphp-buzz#timeouts) for more details.
158+
95159
#### search()
96160

97161
The `search(string $query, array $filters = array()): PromiseInterface<Package[],Exception>` method can be used to
@@ -108,6 +172,12 @@ $client->search('packagist')->then(function (array $packages) {
108172
});
109173
```
110174

175+
Note that this method follows Packagist's paginated search results which
176+
may contain a large number of matches depending on your search.
177+
Accordingly, this method sends one API request for each page which may
178+
take a while for the whole search to be completed. It is not uncommon to
179+
take around 5-10 seconds to fetch search results for 1000 matches.
180+
111181
#### get()
112182

113183
The `get(string $name): PromiseInterface<Package,Exception>` method can be used to

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
],
1313
"require": {
1414
"php": ">=5.3",
15+
"clue/buzz-react": "^2.5",
1516
"knplabs/packagist-api": "~1.0",
16-
"clue/buzz-react": "^2.0 || ^1.0 || ^0.5",
1717
"rize/uri-template": "^0.3"
1818
},
1919
"require-dev": {

examples/search.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,17 @@
1010
$browser = new Browser($loop);
1111
$client = new Client($browser);
1212

13-
$client->search('packagist')->then(function ($result) {
14-
var_dump('found ' . count($result) . ' packages matching "packagist"');
13+
$client->search('reactphp')->then(function ($result) {
14+
var_dump('found ' . count($result) . ' packages matching "reactphp"');
1515
//var_dump($result);
16-
}, function ($error) {
17-
echo $e;
18-
});
16+
}, 'printf');
1917

2018
$client->get('clue/phar-composer')->then(function (Package $package) {
2119
var_dump($package->getName(), $package->getDescription());
22-
});
20+
}, 'printf');
2321

2422
$client->get('clue/graph')->then(function (Package $package) {
2523
var_dump($package->getName(), $package->getDescription());
26-
});
24+
}, 'printf');
2725

2826
$loop->run();

src/Client.php

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Packagist\Api\Result\Factory;
77
use Packagist\Api\Result\Package;
88
use Psr\Http\Message\ResponseInterface;
9+
use React\Promise\Deferred;
910
use React\Promise\PromiseInterface;
1011
use Rize\UriTemplate;
1112

@@ -45,6 +46,12 @@ public function __construct(Browser $http, Factory $resultFactory = null, UriTem
4546
* });
4647
* ```
4748
*
49+
* Note that this method follows Packagist's paginated search results which
50+
* may contain a large number of matches depending on your search.
51+
* Accordingly, this method sends one API request for each page which may
52+
* take a while for the whole search to be completed. It is not uncommon to
53+
* take around 5-10 seconds to fetch search results for 1000 matches.
54+
*
4855
* @param string $query
4956
* @param array $filters
5057
* @return PromiseInterface<Package[],\Exception>
@@ -63,20 +70,28 @@ public function search($query, array $filters = array())
6370
$results = array();
6471
$that = $this;
6572

66-
$fetch = function ($url) use (&$results, $that, &$fetch) {
67-
return $that->request($url)->then(function (ResponseInterface $response) use (&$results, $that, $fetch) {
73+
$pending = null;
74+
$deferred = new Deferred(function () use (&$pending) {
75+
$pending->cancel();
76+
});
77+
78+
$fetch = function ($url) use (&$results, $that, &$fetch, $deferred, &$pending) {
79+
$pending = $that->request($url)->then(function (ResponseInterface $response) use (&$results, $that, $fetch, $deferred) {
6880
$parsed = $that->parse((string)$response->getBody());
6981
$results = array_merge($results, $that->create($parsed));
7082

7183
if (isset($parsed['next'])) {
72-
return $fetch($parsed['next']);
84+
$fetch($parsed['next']);
7385
} else {
74-
return $results;
86+
$deferred->resolve($results);
7587
}
88+
}, function ($e) use ($deferred) {
89+
$deferred->reject($e);
7690
});
7791
};
92+
$fetch($url);
7893

79-
return $fetch($url);
94+
return $deferred->promise();
8095
}
8196

8297
/**

tests/ClientTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,42 @@ public function testSearchPagination()
6565
$this->expectPromiseResolve($this->client->search('zenity'));
6666
}
6767

68+
public function testSearchRejectsWhenRequestRejects()
69+
{
70+
$this->browser->expects($this->once())->method('get')->willReturn(
71+
$this->createRejectedPromise(new RuntimeException())
72+
);
73+
74+
$promise = $this->client->search('foo');
75+
76+
$promise->then($this->expectCallableNever(), $this->expectCallableOnce());
77+
}
78+
79+
public function testSearchCancelPendingPromiseWillCancelInitialRequest()
80+
{
81+
$deferred = new Deferred($this->expectCallableOnce());
82+
$this->browser->expects($this->once())->method('get')->willReturn($deferred->promise());
83+
84+
$promise = $this->client->search('foo');
85+
$promise->cancel();
86+
}
87+
88+
public function testSearchCancelPendingPromiseWillCancelNextRequestWhenInitialIsCompleted()
89+
{
90+
$first = new Deferred($this->expectCallableNever());
91+
$second = new Deferred($this->expectCallableOnce());
92+
$this->browser->expects($this->exactly(2))->method('get')->willReturnOnConsecutiveCalls(
93+
$first->promise(),
94+
$second->promise()
95+
);
96+
97+
$promise = $this->client->search('foo');
98+
99+
$first->resolve($this->createResponsePromise('{"results":[{"name":"clue\/zenity-react","description":"Build graphical desktop (GUI) applications in PHP","url":"https:\/\/packagist.org\/packages\/clue\/zenity-react","downloads":57,"favers":0,"repository":"https:\/\/github.com\/clue\/reactphp-zenity"}],"total":2, "next": ""}'));
100+
101+
$promise->cancel();
102+
}
103+
68104
public function testHttpError()
69105
{
70106
$this->setupBrowser('/packages/clue%2Finvalid.json', $this->createRejectedPromise(new RuntimeException('error')));

0 commit comments

Comments
 (0)
0