New upstream version 2.7.0
Dominik George
5 years ago
4 | 4 | - 5.5 |
5 | 5 | - 5.6 |
6 | 6 | - 7.0 |
7 | - nightly | |
8 | - hhvm | |
7 | - 7.1 | |
8 | - nightly # ignore errors, see below | |
9 | - hhvm # ignore errors, see below | |
9 | 10 | |
10 | before_install: | |
11 | - composer self-update | |
11 | # lock distro so new future defaults will not break the build | |
12 | dist: trusty | |
13 | ||
14 | matrix: | |
15 | allow_failures: | |
16 | - php: hhvm | |
17 | - php: nightly | |
12 | 18 | |
13 | 19 | install: |
14 | 20 | - composer install |
0 | 0 | CHANGELOG for 2.x |
1 | 1 | ================= |
2 | ||
3 | * 2.7.0 (2018-06-13) | |
4 | ||
5 | * Feature: Improve memory consumption for pending promises by using static internal callbacks without binding to self. | |
6 | (#124 by @clue) | |
7 | ||
8 | * 2.6.0 (2018-06-11) | |
9 | ||
10 | * Feature: Significantly improve memory consumption and performance by only passing resolver args | |
11 | to resolver and canceller if callback requires them. Also use static callbacks without | |
12 | binding to promise, clean up canceller function reference when they are no longer | |
13 | needed and hide resolver and canceller references from call stack on PHP 7+. | |
14 | (#113, #115, #116, #117, #118, #119 and #123 by @clue) | |
15 | ||
16 | These changes combined mean that rejecting promises with an `Exception` should | |
17 | no longer cause any internal circular references which could cause some unexpected | |
18 | memory growth in previous versions. By explicitly avoiding and explicitly | |
19 | cleaning up said references, we can avoid relying on PHP's circular garbage collector | |
20 | to kick in which significantly improves performance when rejecting many promises. | |
21 | ||
22 | * Mark legacy progress support / notification API as deprecated | |
23 | (#112 by @clue) | |
24 | ||
25 | * Recommend rejecting promises by throwing an exception | |
26 | (#114 by @jsor) | |
27 | ||
28 | * Improve documentation to properly instantiate LazyPromise | |
29 | (#121 by @holtkamp) | |
30 | ||
31 | * Follower cancellation propagation was originally planned for this release | |
32 | but has been reverted for now and is planned for a future release. | |
33 | (#99 by @jsor and #122 by @clue) | |
2 | 34 | |
3 | 35 | * 2.5.1 (2017-03-25) |
4 | 36 |
0 | React/Promise | |
1 | ============= | |
0 | Promise | |
1 | ======= | |
2 | 2 | |
3 | 3 | A lightweight implementation of |
4 | 4 | [CommonJS Promises/A](http://wiki.commonjs.org/wiki/Promises/A) for PHP. |
12 | 12 | 1. [Introduction](#introduction) |
13 | 13 | 2. [Concepts](#concepts) |
14 | 14 | * [Deferred](#deferred) |
15 | * [Promise](#promise) | |
15 | * [Promise](#promise-1) | |
16 | 16 | 3. [API](#api) |
17 | 17 | * [Deferred](#deferred-1) |
18 | 18 | * [Deferred::promise()](#deferredpromise) |
28 | 28 | * [ExtendedPromiseInterface::progress()](#extendedpromiseinterfaceprogress) |
29 | 29 | * [CancellablePromiseInterface](#cancellablepromiseinterface) |
30 | 30 | * [CancellablePromiseInterface::cancel()](#cancellablepromiseinterfacecancel) |
31 | * [Promise](#promise-1) | |
31 | * [Promise](#promise-2) | |
32 | 32 | * [FulfilledPromise](#fulfilledpromise) |
33 | 33 | * [RejectedPromise](#rejectedpromise) |
34 | 34 | * [LazyPromise](#lazypromise) |
50 | 50 | * [Mixed resolution and rejection forwarding](#mixed-resolution-and-rejection-forwarding) |
51 | 51 | * [Progress event forwarding](#progress-event-forwarding) |
52 | 52 | * [done() vs. then()](#done-vs-then) |
53 | 5. [Credits](#credits) | |
54 | 6. [License](#license) | |
53 | 5. [Install](#install) | |
54 | 6. [Credits](#credits) | |
55 | 7. [License](#license) | |
55 | 56 | |
56 | 57 | Introduction |
57 | 58 | ------------ |
58 | 59 | |
59 | React/Promise is a library implementing | |
60 | Promise is a library implementing | |
60 | 61 | [CommonJS Promises/A](http://wiki.commonjs.org/wiki/Promises/A) for PHP. |
61 | 62 | |
62 | 63 | It also provides several other useful promise-related concepts, such as joining |
102 | 103 | |
103 | 104 | The `resolve` and `reject` methods control the state of the deferred. |
104 | 105 | |
105 | The `notify` method is for progress notification. | |
106 | The deprecated `notify` method is for progress notification. | |
106 | 107 | |
107 | 108 | The constructor of the `Deferred` accepts an optional `$canceller` argument. |
108 | See [Promise](#promise-1) for more information. | |
109 | See [Promise](#promise-2) for more information. | |
109 | 110 | |
110 | 111 | #### Deferred::promise() |
111 | 112 | |
145 | 146 | |
146 | 147 | #### Deferred::notify() |
147 | 148 | |
149 | > Deprecated in v2.6.0: Progress support is deprecated and should not be used anymore. | |
150 | ||
148 | 151 | ```php |
149 | 152 | $deferred->notify(mixed $update = null); |
150 | 153 | ``` |
168 | 171 | |
169 | 172 | #### Implementations |
170 | 173 | |
171 | * [Promise](#promise-1) | |
174 | * [Promise](#promise-2) | |
172 | 175 | * [FulfilledPromise](#fulfilledpromise) |
173 | 176 | * [RejectedPromise](#rejectedpromise) |
174 | 177 | * [LazyPromise](#lazypromise) |
189 | 192 | the result as the first argument. |
190 | 193 | * `$onRejected` will be invoked once the promise is rejected and passed the |
191 | 194 | reason as the first argument. |
192 | * `$onProgress` will be invoked whenever the producer of the promise | |
195 | * `$onProgress` (deprecated) will be invoked whenever the producer of the promise | |
193 | 196 | triggers progress notifications and passed a single argument (whatever it |
194 | 197 | wants) to indicate progress. |
195 | 198 | |
204 | 207 | never both. |
205 | 208 | 2. `$onFulfilled` and `$onRejected` will never be called more |
206 | 209 | than once. |
207 | 3. `$onProgress` may be called multiple times. | |
210 | 3. `$onProgress` (deprecated) may be called multiple times. | |
208 | 211 | |
209 | 212 | #### See also |
210 | 213 | |
320 | 323 | |
321 | 324 | #### ExtendedPromiseInterface::progress() |
322 | 325 | |
326 | > Deprecated in v2.6.0: Progress support is deprecated and should not be used anymore. | |
327 | ||
323 | 328 | ```php |
324 | 329 | $promise->progress(callable $onProgress); |
325 | 330 | ``` |
363 | 368 | ```php |
364 | 369 | $resolver = function (callable $resolve, callable $reject, callable $notify) { |
365 | 370 | // Do some work, possibly asynchronously, and then |
366 | // resolve or reject. You can notify of progress events | |
371 | // resolve or reject. You can notify of progress events (deprecated) | |
367 | 372 | // along the way if you want/need. |
368 | 373 | |
369 | 374 | $resolve($awesomeResult); |
375 | // or throw new Exception('Promise rejected'); | |
370 | 376 | // or $resolve($anotherPromise); |
371 | 377 | // or $reject($nastyError); |
372 | 378 | // or $notify($progressNotification); |
373 | 379 | }; |
374 | 380 | |
375 | $canceller = function (callable $resolve, callable $reject, callable $progress) { | |
381 | $canceller = function () { | |
376 | 382 | // Cancel/abort any running operations like network connections, streams etc. |
377 | 383 | |
378 | $reject(new \Exception('Promise cancelled')); | |
384 | // Reject promise by throwing an exception | |
385 | throw new Exception('Promise cancelled'); | |
379 | 386 | }; |
380 | 387 | |
381 | 388 | $promise = new React\Promise\Promise($resolver, $canceller); |
389 | 396 | When called with a non-promise value, fulfills promise with that value. |
390 | 397 | When called with another promise, e.g. `$resolve($otherPromise)`, promise's |
391 | 398 | fate will be equivalent to that of `$otherPromise`. |
392 | * `$reject($reason)` - Function that rejects the promise. | |
393 | * `$notify($update)` - Function that issues progress events for the promise. | |
399 | * `$reject($reason)` - Function that rejects the promise. It is recommended to | |
400 | just throw an exception instead of using `$reject()`. | |
401 | * `$notify($update)` - Deprecated function that issues progress events for the promise. | |
394 | 402 | |
395 | 403 | If the resolver or canceller throw an exception, the promise will be rejected |
396 | 404 | with that thrown exception as the rejection reason. |
434 | 442 | return $deferred->promise(); |
435 | 443 | }; |
436 | 444 | |
437 | $promise = React\Promise\LazyPromise($factory); | |
445 | $promise = new React\Promise\LazyPromise($factory); | |
438 | 446 | |
439 | 447 | // $factory will only be executed once we call then() |
440 | 448 | $promise->then(function ($value) { |
719 | 727 | ``` |
720 | 728 | |
721 | 729 | #### Progress event forwarding |
730 | ||
731 | > Deprecated in v2.6.0: Progress support is deprecated and should not be used anymore. | |
722 | 732 | |
723 | 733 | In the same way as resolution and rejection handlers, your progress handler |
724 | 734 | **MUST** return a progress event to be propagated to the next link in the chain. |
823 | 833 | |
824 | 834 | You can get the original rejection reason by calling `$exception->getReason()`. |
825 | 835 | |
836 | Install | |
837 | ------- | |
838 | ||
839 | The recommended way to install this library is [through Composer](https://getcomposer.org). | |
840 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) | |
841 | ||
842 | This project follows [SemVer](https://semver.org/). | |
843 | This will install the latest supported version: | |
844 | ||
845 | ```bash | |
846 | $ composer require react/promise:^2.7 | |
847 | ``` | |
848 | ||
849 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. | |
850 | ||
851 | This project aims to run on any platform and thus does not require any PHP | |
852 | extensions and supports running on legacy PHP 5.4 through current PHP 7+ and HHVM. | |
853 | It's *highly recommended to use PHP 7+* for this project due to its vast | |
854 | performance improvements. | |
855 | ||
826 | 856 | Credits |
827 | 857 | ------- |
828 | 858 | |
829 | React/Promise is a port of [when.js](https://github.com/cujojs/when) | |
859 | Promise is a port of [when.js](https://github.com/cujojs/when) | |
830 | 860 | by [Brian Cavalier](https://github.com/briancavalier). |
831 | 861 | |
832 | 862 | Also, large parts of the documentation have been ported from the when.js |
836 | 866 | License |
837 | 867 | ------- |
838 | 868 | |
839 | React/Promise is released under the [MIT](https://github.com/reactphp/promise/blob/master/LICENSE) license. | |
869 | Released under the [MIT](LICENSE) license. |
22 | 22 | $this->rejectCallback = $reject; |
23 | 23 | $this->notifyCallback = $notify; |
24 | 24 | }, $this->canceller); |
25 | $this->canceller = null; | |
25 | 26 | } |
26 | 27 | |
27 | 28 | return $this->promise; |
41 | 42 | call_user_func($this->rejectCallback, $reason); |
42 | 43 | } |
43 | 44 | |
45 | /** | |
46 | * @deprecated 2.6.0 Progress support is deprecated and should not be used anymore. | |
47 | * @param mixed $update | |
48 | */ | |
44 | 49 | public function notify($update = null) |
45 | 50 | { |
46 | 51 | $this->promise(); |
4 | 4 | interface ExtendedPromiseInterface extends PromiseInterface |
5 | 5 | { |
6 | 6 | /** |
7 | * | |
8 | * The `$onProgress` argument is deprecated and should not be used anymore. | |
9 | * | |
7 | 10 | * @return void |
8 | 11 | */ |
9 | 12 | public function done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null); |
20 | 23 | |
21 | 24 | /** |
22 | 25 | * @return ExtendedPromiseInterface |
26 | * @deprecated 2.6.0 Progress support is deprecated and should not be used anymore. | |
23 | 27 | */ |
24 | 28 | public function progress(callable $onProgress); |
25 | 29 | } |
15 | 15 | public function __construct(callable $resolver, callable $canceller = null) |
16 | 16 | { |
17 | 17 | $this->canceller = $canceller; |
18 | $this->call($resolver); | |
18 | ||
19 | // Explicitly overwrite arguments with null values before invoking | |
20 | // resolver function. This ensure that these arguments do not show up | |
21 | // in the stack trace in PHP 7+ only. | |
22 | $cb = $resolver; | |
23 | $resolver = $canceller = null; | |
24 | $this->call($cb); | |
19 | 25 | } |
20 | 26 | |
21 | 27 | public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) |
28 | 34 | return new static($this->resolver($onFulfilled, $onRejected, $onProgress)); |
29 | 35 | } |
30 | 36 | |
31 | $this->requiredCancelRequests++; | |
32 | ||
33 | return new static($this->resolver($onFulfilled, $onRejected, $onProgress), function () { | |
34 | if (++$this->cancelRequests < $this->requiredCancelRequests) { | |
35 | return; | |
36 | } | |
37 | ||
38 | $this->cancel(); | |
39 | }); | |
37 | // This promise has a canceller, so we create a new child promise which | |
38 | // has a canceller that invokes the parent canceller if all other | |
39 | // followers are also cancelled. We keep a reference to this promise | |
40 | // instance for the static canceller function and clear this to avoid | |
41 | // keeping a cyclic reference between parent and follower. | |
42 | $parent = $this; | |
43 | ++$parent->requiredCancelRequests; | |
44 | ||
45 | return new static( | |
46 | $this->resolver($onFulfilled, $onRejected, $onProgress), | |
47 | static function () use (&$parent) { | |
48 | if (++$parent->cancelRequests >= $parent->requiredCancelRequests) { | |
49 | $parent->cancel(); | |
50 | } | |
51 | ||
52 | $parent = null; | |
53 | } | |
54 | ); | |
40 | 55 | } |
41 | 56 | |
42 | 57 | public function done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) |
45 | 60 | return $this->result->done($onFulfilled, $onRejected, $onProgress); |
46 | 61 | } |
47 | 62 | |
48 | $this->handlers[] = function (ExtendedPromiseInterface $promise) use ($onFulfilled, $onRejected) { | |
63 | $this->handlers[] = static function (ExtendedPromiseInterface $promise) use ($onFulfilled, $onRejected) { | |
49 | 64 | $promise |
50 | 65 | ->done($onFulfilled, $onRejected); |
51 | 66 | }; |
57 | 72 | |
58 | 73 | public function otherwise(callable $onRejected) |
59 | 74 | { |
60 | return $this->then(null, function ($reason) use ($onRejected) { | |
75 | return $this->then(null, static function ($reason) use ($onRejected) { | |
61 | 76 | if (!_checkTypehint($onRejected, $reason)) { |
62 | 77 | return new RejectedPromise($reason); |
63 | 78 | } |
68 | 83 | |
69 | 84 | public function always(callable $onFulfilledOrRejected) |
70 | 85 | { |
71 | return $this->then(function ($value) use ($onFulfilledOrRejected) { | |
86 | return $this->then(static function ($value) use ($onFulfilledOrRejected) { | |
72 | 87 | return resolve($onFulfilledOrRejected())->then(function () use ($value) { |
73 | 88 | return $value; |
74 | 89 | }); |
75 | }, function ($reason) use ($onFulfilledOrRejected) { | |
90 | }, static function ($reason) use ($onFulfilledOrRejected) { | |
76 | 91 | return resolve($onFulfilledOrRejected())->then(function () use ($reason) { |
77 | 92 | return new RejectedPromise($reason); |
78 | 93 | }); |
100 | 115 | { |
101 | 116 | return function ($resolve, $reject, $notify) use ($onFulfilled, $onRejected, $onProgress) { |
102 | 117 | if ($onProgress) { |
103 | $progressHandler = function ($update) use ($notify, $onProgress) { | |
118 | $progressHandler = static function ($update) use ($notify, $onProgress) { | |
104 | 119 | try { |
105 | 120 | $notify($onProgress($update)); |
106 | 121 | } catch (\Throwable $e) { |
113 | 128 | $progressHandler = $notify; |
114 | 129 | } |
115 | 130 | |
116 | $this->handlers[] = function (ExtendedPromiseInterface $promise) use ($onFulfilled, $onRejected, $resolve, $reject, $progressHandler) { | |
131 | $this->handlers[] = static function (ExtendedPromiseInterface $promise) use ($onFulfilled, $onRejected, $resolve, $reject, $progressHandler) { | |
117 | 132 | $promise |
118 | 133 | ->then($onFulfilled, $onRejected) |
119 | 134 | ->done($resolve, $reject, $progressHandler); |
123 | 138 | }; |
124 | 139 | } |
125 | 140 | |
126 | private function resolve($value = null) | |
141 | private function reject($reason = null) | |
127 | 142 | { |
128 | 143 | if (null !== $this->result) { |
129 | 144 | return; |
130 | 145 | } |
131 | 146 | |
132 | $this->settle(resolve($value)); | |
133 | } | |
134 | ||
135 | private function reject($reason = null) | |
136 | { | |
137 | if (null !== $this->result) { | |
138 | return; | |
139 | } | |
140 | ||
141 | 147 | $this->settle(reject($reason)); |
142 | 148 | } |
143 | 149 | |
144 | private function notify($update = null) | |
145 | { | |
146 | if (null !== $this->result) { | |
147 | return; | |
148 | } | |
149 | ||
150 | foreach ($this->progressHandlers as $handler) { | |
151 | $handler($update); | |
152 | } | |
153 | } | |
154 | ||
155 | 150 | private function settle(ExtendedPromiseInterface $promise) |
156 | 151 | { |
157 | 152 | $promise = $this->unwrap($promise); |
153 | ||
154 | if ($promise === $this) { | |
155 | $promise = new RejectedPromise( | |
156 | new \LogicException('Cannot resolve a promise with itself.') | |
157 | ); | |
158 | } | |
158 | 159 | |
159 | 160 | $handlers = $this->handlers; |
160 | 161 | |
161 | 162 | $this->progressHandlers = $this->handlers = []; |
162 | 163 | $this->result = $promise; |
164 | $this->canceller = null; | |
163 | 165 | |
164 | 166 | foreach ($handlers as $handler) { |
165 | 167 | $handler($promise); |
183 | 185 | $promise = $promise->promise(); |
184 | 186 | } |
185 | 187 | |
186 | if ($promise === $this) { | |
187 | return new RejectedPromise( | |
188 | new \LogicException('Cannot resolve a promise with itself.') | |
189 | ); | |
190 | } | |
191 | ||
192 | 188 | return $promise; |
193 | 189 | } |
194 | 190 | |
195 | private function call(callable $callback) | |
196 | { | |
191 | private function call(callable $cb) | |
192 | { | |
193 | // Explicitly overwrite argument with null value. This ensure that this | |
194 | // argument does not show up in the stack trace in PHP 7+ only. | |
195 | $callback = $cb; | |
196 | $cb = null; | |
197 | ||
198 | // Use reflection to inspect number of arguments expected by this callback. | |
199 | // We did some careful benchmarking here: Using reflection to avoid unneeded | |
200 | // function arguments is actually faster than blindly passing them. | |
201 | // Also, this helps avoiding unnecessary function arguments in the call stack | |
202 | // if the callback creates an Exception (creating garbage cycles). | |
203 | if (is_array($callback)) { | |
204 | $ref = new \ReflectionMethod($callback[0], $callback[1]); | |
205 | } elseif (is_object($callback) && !$callback instanceof \Closure) { | |
206 | $ref = new \ReflectionMethod($callback, '__invoke'); | |
207 | } else { | |
208 | $ref = new \ReflectionFunction($callback); | |
209 | } | |
210 | $args = $ref->getNumberOfParameters(); | |
211 | ||
197 | 212 | try { |
198 | $callback( | |
199 | function ($value = null) { | |
200 | $this->resolve($value); | |
201 | }, | |
202 | function ($reason = null) { | |
203 | $this->reject($reason); | |
204 | }, | |
205 | function ($update = null) { | |
206 | $this->notify($update); | |
207 | } | |
208 | ); | |
213 | if ($args === 0) { | |
214 | $callback(); | |
215 | } else { | |
216 | // Keep references to this promise instance for the static resolve/reject functions. | |
217 | // By using static callbacks that are not bound to this instance | |
218 | // and passing the target promise instance by reference, we can | |
219 | // still execute its resolving logic and still clear this | |
220 | // reference when settling the promise. This helps avoiding | |
221 | // garbage cycles if any callback creates an Exception. | |
222 | // These assumptions are covered by the test suite, so if you ever feel like | |
223 | // refactoring this, go ahead, any alternative suggestions are welcome! | |
224 | $target =& $this; | |
225 | $progressHandlers =& $this->progressHandlers; | |
226 | ||
227 | $callback( | |
228 | static function ($value = null) use (&$target) { | |
229 | if ($target !== null) { | |
230 | $target->settle(resolve($value)); | |
231 | $target = null; | |
232 | } | |
233 | }, | |
234 | static function ($reason = null) use (&$target) { | |
235 | if ($target !== null) { | |
236 | $target->reject($reason); | |
237 | $target = null; | |
238 | } | |
239 | }, | |
240 | static function ($update = null) use (&$progressHandlers) { | |
241 | foreach ($progressHandlers as $handler) { | |
242 | $handler($update); | |
243 | } | |
244 | } | |
245 | ); | |
246 | } | |
209 | 247 | } catch (\Throwable $e) { |
248 | $target = null; | |
210 | 249 | $this->reject($e); |
211 | 250 | } catch (\Exception $e) { |
251 | $target = null; | |
212 | 252 | $this->reject($e); |
213 | 253 | } |
214 | 254 | } |
4 | 4 | interface PromiseInterface |
5 | 5 | { |
6 | 6 | /** |
7 | * | |
8 | * The `$onProgress` argument is deprecated and should not be used anymore. | |
9 | * | |
7 | 10 | * @return PromiseInterface |
8 | 11 | */ |
9 | 12 | public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null); |
38 | 38 | |
39 | 39 | $deferred->progress($sentinel); |
40 | 40 | } |
41 | ||
42 | /** @test */ | |
43 | public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithException() | |
44 | { | |
45 | gc_collect_cycles(); | |
46 | $deferred = new Deferred(function ($resolve, $reject) { | |
47 | $reject(new \Exception('foo')); | |
48 | }); | |
49 | $deferred->promise()->cancel(); | |
50 | unset($deferred); | |
51 | ||
52 | $this->assertSame(0, gc_collect_cycles()); | |
53 | } | |
54 | ||
55 | /** @test */ | |
56 | public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejectsWithException() | |
57 | { | |
58 | gc_collect_cycles(); | |
59 | $deferred = new Deferred(function ($resolve, $reject) { | |
60 | $reject(new \Exception('foo')); | |
61 | }); | |
62 | $deferred->promise()->then()->cancel(); | |
63 | unset($deferred); | |
64 | ||
65 | $this->assertSame(0, gc_collect_cycles()); | |
66 | } | |
67 | ||
68 | /** @test */ | |
69 | public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenceAndExplicitlyRejectWithException() | |
70 | { | |
71 | gc_collect_cycles(); | |
72 | $deferred = new Deferred(function () use (&$deferred) { }); | |
73 | $deferred->reject(new \Exception('foo')); | |
74 | unset($deferred); | |
75 | ||
76 | $this->assertSame(0, gc_collect_cycles()); | |
77 | } | |
78 | ||
79 | /** @test */ | |
80 | public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingDeferred() | |
81 | { | |
82 | gc_collect_cycles(); | |
83 | $deferred = new Deferred(); | |
84 | $deferred->promise(); | |
85 | unset($deferred); | |
86 | ||
87 | $this->assertSame(0, gc_collect_cycles()); | |
88 | } | |
89 | ||
90 | /** @test */ | |
91 | public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingDeferredWithUnusedCanceller() | |
92 | { | |
93 | gc_collect_cycles(); | |
94 | $deferred = new Deferred(function () { }); | |
95 | $deferred->promise(); | |
96 | unset($deferred); | |
97 | ||
98 | $this->assertSame(0, gc_collect_cycles()); | |
99 | } | |
100 | ||
101 | /** @test */ | |
102 | public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingDeferredWithNoopCanceller() | |
103 | { | |
104 | gc_collect_cycles(); | |
105 | $deferred = new Deferred(function () { }); | |
106 | $deferred->promise()->cancel(); | |
107 | unset($deferred); | |
108 | ||
109 | $this->assertSame(0, gc_collect_cycles()); | |
110 | } | |
41 | 111 | } |
46 | 46 | |
47 | 47 | return new FulfilledPromise(new FulfilledPromise()); |
48 | 48 | } |
49 | ||
50 | /** @test */ | |
51 | public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToFulfilledPromiseWithAlwaysFollowers() | |
52 | { | |
53 | gc_collect_cycles(); | |
54 | $promise = new FulfilledPromise(1); | |
55 | $promise->always(function () { | |
56 | throw new \RuntimeException(); | |
57 | }); | |
58 | unset($promise); | |
59 | ||
60 | $this->assertSame(0, gc_collect_cycles()); | |
61 | } | |
62 | ||
63 | /** @test */ | |
64 | public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToFulfilledPromiseWithThenFollowers() | |
65 | { | |
66 | gc_collect_cycles(); | |
67 | $promise = new FulfilledPromise(1); | |
68 | $promise = $promise->then(function () { | |
69 | throw new \RuntimeException(); | |
70 | }); | |
71 | unset($promise); | |
72 | ||
73 | $this->assertSame(0, gc_collect_cycles()); | |
74 | } | |
49 | 75 | } |
13 | 13 | /** @test */ |
14 | 14 | public function cancelShouldCallCancellerWithResolverArguments() |
15 | 15 | { |
16 | $mock = $this->createCallableMock(); | |
17 | $mock | |
18 | ->expects($this->once()) | |
19 | ->method('__invoke') | |
20 | ->with($this->isType('callable'), $this->isType('callable'), $this->isType('callable')); | |
21 | ||
22 | $adapter = $this->getPromiseTestAdapter($mock); | |
23 | ||
24 | $adapter->promise()->cancel(); | |
16 | $args = null; | |
17 | $adapter = $this->getPromiseTestAdapter(function ($resolve, $reject, $notify) use (&$args) { | |
18 | $args = func_get_args(); | |
19 | }); | |
20 | ||
21 | $adapter->promise()->cancel(); | |
22 | ||
23 | $this->assertCount(3, $args); | |
24 | $this->assertTrue(is_callable($args[0])); | |
25 | $this->assertTrue(is_callable($args[1])); | |
26 | $this->assertTrue(is_callable($args[2])); | |
27 | } | |
28 | ||
29 | /** @test */ | |
30 | public function cancelShouldCallCancellerWithoutArgumentsIfNotAccessed() | |
31 | { | |
32 | $args = null; | |
33 | $adapter = $this->getPromiseTestAdapter(function () use (&$args) { | |
34 | $args = func_num_args(); | |
35 | }); | |
36 | ||
37 | $adapter->promise()->cancel(); | |
38 | ||
39 | $this->assertSame(0, $args); | |
25 | 40 | } |
26 | 41 | |
27 | 42 | /** @test */ |
48 | 48 | } |
49 | 49 | |
50 | 50 | /** @test */ |
51 | public function shouldResolveWithoutCreatingGarbageCyclesIfResolverResolvesWithException() | |
52 | { | |
53 | gc_collect_cycles(); | |
54 | $promise = new Promise(function ($resolve) { | |
55 | $resolve(new \Exception('foo')); | |
56 | }); | |
57 | unset($promise); | |
58 | ||
59 | $this->assertSame(0, gc_collect_cycles()); | |
60 | } | |
61 | ||
62 | /** @test */ | |
63 | public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptionWithoutResolver() | |
64 | { | |
65 | gc_collect_cycles(); | |
66 | $promise = new Promise(function () { | |
67 | throw new \Exception('foo'); | |
68 | }); | |
69 | unset($promise); | |
70 | ||
71 | $this->assertSame(0, gc_collect_cycles()); | |
72 | } | |
73 | ||
74 | /** @test */ | |
75 | public function shouldRejectWithoutCreatingGarbageCyclesIfResolverRejectsWithException() | |
76 | { | |
77 | gc_collect_cycles(); | |
78 | $promise = new Promise(function ($resolve, $reject) { | |
79 | $reject(new \Exception('foo')); | |
80 | }); | |
81 | unset($promise); | |
82 | ||
83 | $this->assertSame(0, gc_collect_cycles()); | |
84 | } | |
85 | ||
86 | /** @test */ | |
87 | public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithException() | |
88 | { | |
89 | gc_collect_cycles(); | |
90 | $promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) { | |
91 | $reject(new \Exception('foo')); | |
92 | }); | |
93 | $promise->cancel(); | |
94 | unset($promise); | |
95 | ||
96 | $this->assertSame(0, gc_collect_cycles()); | |
97 | } | |
98 | ||
99 | /** @test */ | |
100 | public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejectsWithException() | |
101 | { | |
102 | gc_collect_cycles(); | |
103 | $promise = new Promise(function ($resolve, $reject) { }, function ($resolve, $reject) { | |
104 | $reject(new \Exception('foo')); | |
105 | }); | |
106 | $promise->then()->then()->then()->cancel(); | |
107 | unset($promise); | |
108 | ||
109 | $this->assertSame(0, gc_collect_cycles()); | |
110 | } | |
111 | ||
112 | /** @test */ | |
113 | public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsException() | |
114 | { | |
115 | gc_collect_cycles(); | |
116 | $promise = new Promise(function ($resolve, $reject) { | |
117 | throw new \Exception('foo'); | |
118 | }); | |
119 | unset($promise); | |
120 | ||
121 | $this->assertSame(0, gc_collect_cycles()); | |
122 | } | |
123 | ||
124 | /** | |
125 | * Test that checks number of garbage cycles after throwing from a canceller | |
126 | * that explicitly uses a reference to the promise. This is rather synthetic, | |
127 | * actual use cases often have implicit (hidden) references which ought not | |
128 | * to be stored in the stack trace. | |
129 | * | |
130 | * Reassigned arguments only show up in the stack trace in PHP 7, so we can't | |
131 | * avoid this on legacy PHP. As an alternative, consider explicitly unsetting | |
132 | * any references before throwing. | |
133 | * | |
134 | * @test | |
135 | * @requires PHP 7 | |
136 | */ | |
137 | public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReferenceThrowsException() | |
138 | { | |
139 | gc_collect_cycles(); | |
140 | $promise = new Promise(function () {}, function () use (&$promise) { | |
141 | throw new \Exception('foo'); | |
142 | }); | |
143 | $promise->cancel(); | |
144 | unset($promise); | |
145 | ||
146 | $this->assertSame(0, gc_collect_cycles()); | |
147 | } | |
148 | ||
149 | /** | |
150 | * @test | |
151 | * @requires PHP 7 | |
152 | * @see self::shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReferenceThrowsException | |
153 | */ | |
154 | public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceThrowsException() | |
155 | { | |
156 | gc_collect_cycles(); | |
157 | $promise = new Promise(function () use (&$promise) { | |
158 | throw new \Exception('foo'); | |
159 | }); | |
160 | unset($promise); | |
161 | ||
162 | $this->assertSame(0, gc_collect_cycles()); | |
163 | } | |
164 | ||
165 | /** | |
166 | * @test | |
167 | * @requires PHP 7 | |
168 | * @see self::shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReferenceThrowsException | |
169 | */ | |
170 | public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenceAndResolverThrowsException() | |
171 | { | |
172 | gc_collect_cycles(); | |
173 | $promise = new Promise(function () { | |
174 | throw new \Exception('foo'); | |
175 | }, function () use (&$promise) { }); | |
176 | unset($promise); | |
177 | ||
178 | $this->assertSame(0, gc_collect_cycles()); | |
179 | } | |
180 | ||
181 | /** @test */ | |
182 | public function shouldIgnoreNotifyAfterReject() | |
183 | { | |
184 | $promise = new Promise(function () { }, function ($resolve, $reject, $notify) { | |
185 | $reject(new \Exception('foo')); | |
186 | $notify(42); | |
187 | }); | |
188 | ||
189 | $promise->then(null, null, $this->expectCallableNever()); | |
190 | $promise->cancel(); | |
191 | } | |
192 | ||
193 | ||
194 | /** @test */ | |
195 | public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromise() | |
196 | { | |
197 | gc_collect_cycles(); | |
198 | $promise = new Promise(function () { }); | |
199 | unset($promise); | |
200 | ||
201 | $this->assertSame(0, gc_collect_cycles()); | |
202 | } | |
203 | ||
204 | /** @test */ | |
205 | public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithThenFollowers() | |
206 | { | |
207 | gc_collect_cycles(); | |
208 | $promise = new Promise(function () { }); | |
209 | $promise->then()->then()->then(); | |
210 | unset($promise); | |
211 | ||
212 | $this->assertSame(0, gc_collect_cycles()); | |
213 | } | |
214 | ||
215 | /** @test */ | |
216 | public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithDoneFollowers() | |
217 | { | |
218 | gc_collect_cycles(); | |
219 | $promise = new Promise(function () { }); | |
220 | $promise->done(); | |
221 | unset($promise); | |
222 | ||
223 | $this->assertSame(0, gc_collect_cycles()); | |
224 | } | |
225 | ||
226 | /** @test */ | |
227 | public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithOtherwiseFollowers() | |
228 | { | |
229 | gc_collect_cycles(); | |
230 | $promise = new Promise(function () { }); | |
231 | $promise->otherwise(function () { }); | |
232 | unset($promise); | |
233 | ||
234 | $this->assertSame(0, gc_collect_cycles()); | |
235 | } | |
236 | ||
237 | /** @test */ | |
238 | public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithAlwaysFollowers() | |
239 | { | |
240 | gc_collect_cycles(); | |
241 | $promise = new Promise(function () { }); | |
242 | $promise->always(function () { }); | |
243 | unset($promise); | |
244 | ||
245 | $this->assertSame(0, gc_collect_cycles()); | |
246 | } | |
247 | ||
248 | /** @test */ | |
249 | public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithProgressFollowers() | |
250 | { | |
251 | gc_collect_cycles(); | |
252 | $promise = new Promise(function () { }); | |
253 | $promise->then(null, null, function () { }); | |
254 | unset($promise); | |
255 | ||
256 | $this->assertSame(0, gc_collect_cycles()); | |
257 | } | |
258 | ||
259 | /** @test */ | |
51 | 260 | public function shouldFulfillIfFullfilledWithSimplePromise() |
52 | 261 | { |
53 | 262 | $adapter = $this->getPromiseTestAdapter(); |
46 | 46 | |
47 | 47 | return new RejectedPromise(new RejectedPromise()); |
48 | 48 | } |
49 | ||
50 | /** @test */ | |
51 | public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToRejectedPromiseWithAlwaysFollowers() | |
52 | { | |
53 | gc_collect_cycles(); | |
54 | $promise = new RejectedPromise(1); | |
55 | $promise->always(function () { | |
56 | throw new \RuntimeException(); | |
57 | }); | |
58 | unset($promise); | |
59 | ||
60 | $this->assertSame(0, gc_collect_cycles()); | |
61 | } | |
62 | ||
63 | /** @test */ | |
64 | public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToRejectedPromiseWithThenFollowers() | |
65 | { | |
66 | gc_collect_cycles(); | |
67 | $promise = new RejectedPromise(1); | |
68 | $promise = $promise->then(null, function () { | |
69 | throw new \RuntimeException(); | |
70 | }); | |
71 | unset($promise); | |
72 | ||
73 | $this->assertSame(0, gc_collect_cycles()); | |
74 | } | |
49 | 75 | } |