1<?php
2
3namespace React\Promise\Timer;
4
5use React\EventLoop\LoopInterface;
6use React\Promise\CancellablePromiseInterface;
7use React\Promise\PromiseInterface;
8use React\Promise\Promise;
9
10function timeout(PromiseInterface $promise, $time, LoopInterface $loop)
11{
12    // cancelling this promise will only try to cancel the input promise,
13    // thus leaving responsibility to the input promise.
14    $canceller = null;
15    if ($promise instanceof CancellablePromiseInterface || (!\interface_exists('React\Promise\CancellablePromiseInterface') && \method_exists($promise, 'cancel'))) {
16        // pass promise by reference to clean reference after cancellation handler
17        // has been invoked once in order to avoid garbage references in call stack.
18        $canceller = function () use (&$promise) {
19            $promise->cancel();
20            $promise = null;
21        };
22    }
23
24    return new Promise(function ($resolve, $reject) use ($loop, $time, $promise) {
25        $timer = null;
26        $promise = $promise->then(function ($v) use (&$timer, $loop, $resolve) {
27            if ($timer) {
28                $loop->cancelTimer($timer);
29            }
30            $timer = false;
31            $resolve($v);
32        }, function ($v) use (&$timer, $loop, $reject) {
33            if ($timer) {
34                $loop->cancelTimer($timer);
35            }
36            $timer = false;
37            $reject($v);
38        });
39
40        // promise already resolved => no need to start timer
41        if ($timer === false) {
42            return;
43        }
44
45        // start timeout timer which will cancel the input promise
46        $timer = $loop->addTimer($time, function () use ($time, &$promise, $reject) {
47            $reject(new TimeoutException($time, 'Timed out after ' . $time . ' seconds'));
48
49            // try to invoke cancellation handler of input promise and then clean
50            // reference in order to avoid garbage references in call stack.
51            if ($promise instanceof CancellablePromiseInterface || (!\interface_exists('React\Promise\CancellablePromiseInterface') && \method_exists($promise, 'cancel'))) {
52                $promise->cancel();
53            }
54            $promise = null;
55        });
56    }, $canceller);
57}
58
59function resolve($time, LoopInterface $loop)
60{
61    return new Promise(function ($resolve) use ($loop, $time, &$timer) {
62        // resolve the promise when the timer fires in $time seconds
63        $timer = $loop->addTimer($time, function () use ($time, $resolve) {
64            $resolve($time);
65        });
66    }, function () use (&$timer, $loop) {
67        // cancelling this promise will cancel the timer, clean the reference
68        // in order to avoid garbage references in call stack and then reject.
69        $loop->cancelTimer($timer);
70        $timer = null;
71
72        throw new \RuntimeException('Timer cancelled');
73    });
74}
75
76function reject($time, LoopInterface $loop)
77{
78    return resolve($time, $loop)->then(function ($time) {
79        throw new TimeoutException($time, 'Timer expired after ' . $time . ' seconds');
80    });
81}
82