@@ -143,6 +143,124 @@ public static function all($concurrency, array $jobs, $handler)
143143 });
144144 }
145145
146+ /**
147+ * Concurrently process given jobs through the given `$handler` and resolve
148+ * with first resolution value.
149+ *
150+ * This is a convenience method which uses the `Queue` internally to
151+ * schedule all jobs while limiting concurrency to ensure no more than
152+ * `$concurrency` jobs ever run at once. It will return a promise which
153+ * resolves with the result of the first job on success and will then try
154+ * to `cancel()` all outstanding jobs.
155+ *
156+ * ```php
157+ * $loop = React\EventLoop\Factory::create();
158+ * $browser = new Clue\React\Buzz\Browser($loop);
159+ *
160+ * $promise = Queue::any(3, $urls, function ($url) use ($browser) {
161+ * return $browser->get($url);
162+ * });
163+ *
164+ * $promise->then(function (ResponseInterface $response) {
165+ * echo 'First response: ' . $response->getBody() . PHP_EOL;
166+ * });
167+ * ```
168+ *
169+ * If all of the jobs fail, it will reject the resulting promise. Similarly,
170+ * calling `cancel()` on the resulting promise will try to cancel all
171+ * outstanding jobs. See [promises](#promises) and
172+ * [cancellation](#cancellation) for details.
173+ *
174+ * The `$concurrency` parameter sets a new soft limit for the maximum number
175+ * of jobs to handle concurrently. Finding a good concurrency limit depends
176+ * on your particular use case. It's common to limit concurrency to a rather
177+ * small value, as doing more than a dozen of things at once may easily
178+ * overwhelm the receiving side. Using a `1` value will ensure that all jobs
179+ * are processed one after another, effectively creating a "waterfall" of
180+ * jobs. Using a value less than 1 will reject with an
181+ * `InvalidArgumentException` without processing any jobs.
182+ *
183+ * ```php
184+ * // handle up to 10 jobs concurrently
185+ * $promise = Queue::any(10, $jobs, $handler);
186+ * ```
187+ *
188+ * ```php
189+ * // handle each job after another without concurrency (waterfall)
190+ * $promise = Queue::any(1, $jobs, $handler);
191+ * ```
192+ *
193+ * The `$jobs` parameter must be an array with all jobs to process. Each
194+ * value in this array will be passed to the `$handler` to start one job.
195+ * The array keys have no effect, the promise will simply resolve with the
196+ * job results of the first successful job as returned by the `$handler`.
197+ * If this array is empty, this method will reject without processing any
198+ * jobs.
199+ *
200+ * The `$handler` parameter must be a valid callable that accepts your job
201+ * parameters, invokes the appropriate operation and returns a Promise as a
202+ * placeholder for its future result. If the given argument is not a valid
203+ * callable, this method will reject with an `InvalidArgumentExceptionn`
204+ * without processing any jobs.
205+ *
206+ * ```php
207+ * // using a Closure as handler is usually recommended
208+ * $promise = Queue::any(10, $jobs, function ($url) use ($browser) {
209+ * return $browser->get($url);
210+ * });
211+ * ```
212+ *
213+ * ```php
214+ * // accepts any callable, so PHP's array notation is also supported
215+ * $promise = Queue::any(10, $jobs, array($browser, 'get'));
216+ * ```
217+ *
218+ * @param int $concurrency concurrency soft limit
219+ * @param array $jobs
220+ * @param callable $handler
221+ * @return PromiseInterface Returns a Promise<mixed> which resolves with a single resolution value
222+ * or rejects when all of the operations reject.
223+ */
224+ public static function any ($ concurrency , array $ jobs , $ handler )
225+ {
226+ // explicitly reject with empty jobs (https://github.com/reactphp/promise/pull/34)
227+ if (!$ jobs ) {
228+ return Promise \reject (new \UnderflowException ('No jobs given ' ));
229+ }
230+
231+ try {
232+ // limit number of concurrent operations
233+ $ q = new self ($ concurrency , null , $ handler );
234+ } catch (\InvalidArgumentException $ e ) {
235+ // reject if $concurrency or $handler is invalid
236+ return Promise \reject ($ e );
237+ }
238+
239+ // try invoking all operations and automatically queue excessive ones
240+ $ promises = array_map ($ q , $ jobs );
241+
242+ return new Promise \Promise (function ($ resolve , $ reject ) use ($ promises ) {
243+ Promise \any ($ promises )->then (function ($ result ) use ($ promises , $ resolve ) {
244+ // cancel all pending promises if a single result is ready
245+ foreach (array_reverse ($ promises ) as $ promise ) {
246+ if ($ promise instanceof CancellablePromiseInterface) {
247+ $ promise ->cancel ();
248+ }
249+ }
250+
251+ // resolve with original resolution value
252+ $ resolve ($ result );
253+ }, $ reject );
254+ }, function () use ($ promises ) {
255+ // cancel all pending promises on cancellation
256+ foreach (array_reverse ($ promises ) as $ promise ) {
257+ if ($ promise instanceof CancellablePromiseInterface) {
258+ $ promise ->cancel ();
259+ }
260+ }
261+ });
262+ }
263+
146264 /**
147265 * Instantiates a new queue object.
148266 *
0 commit comments