148148 * });
149149 * ```
150150 *
151+ * Promises returned by `async()` can be cancelled, and when done any currently
152+ * and future awaited promise inside that and any nested fibers with their
153+ * awaited promises will also be cancelled. As such the following example will
154+ * only output `ab` as the [`sleep()`](https://reactphp.org/promise-timer/#sleep)
155+ * between `a` and `b` is cancelled throwing a timeout exception that bubbles up
156+ * through the fibers ultimately to the end user through the [`await()`](#await)
157+ * on the last line of the example.
158+ *
159+ * ```php
160+ * $promise = async(static function (): int {
161+ * echo 'a';
162+ * await(async(static function(): void {
163+ * echo 'b';
164+ * await(sleep(2));
165+ * echo 'c';
166+ * })());
167+ * echo 'd';
168+ *
169+ * return time();
170+ * })();
171+ *
172+ * $promise->cancel();
173+ * await($promise);
174+ * ```
175+ *
151176 * @param callable(mixed ...$args):mixed $function
152177 * @return callable(): PromiseInterface<mixed>
153178 * @since 4.0.0
154179 * @see coroutine()
155180 */
156181function async (callable $ function ): callable
157182{
158- return static fn (mixed ...$ args ): PromiseInterface => new Promise (function (callable $ resolve , callable $ reject ) use ($ function , $ args ): void {
159- $ fiber = new \Fiber (function () use ($ resolve , $ reject , $ function , $ args ): void {
160- try {
161- $ resolve ($ function (...$ args ));
162- } catch (\Throwable $ exception ) {
163- $ reject ($ exception );
183+ return static function (mixed ...$ args ) use ($ function ): PromiseInterface {
184+ $ fiber = null ;
185+ $ promise = new Promise (function (callable $ resolve , callable $ reject ) use ($ function , $ args , &$ fiber ): void {
186+ $ fiber = new \Fiber (function () use ($ resolve , $ reject , $ function , $ args , &$ fiber ): void {
187+ try {
188+ $ resolve ($ function (...$ args ));
189+ } catch (\Throwable $ exception ) {
190+ $ reject ($ exception );
191+ } finally {
192+ FiberMap::unregister ($ fiber );
193+ }
194+ });
195+
196+ FiberMap::register ($ fiber );
197+
198+ $ fiber ->start ();
199+ }, function () use (&$ fiber ): void {
200+ FiberMap::cancel ($ fiber );
201+ foreach (FiberMap::getPromises ($ fiber ) as $ promise ) {
202+ if ($ promise instanceof CancellablePromiseInterface) {
203+ $ promise ->cancel ();
204+ }
164205 }
165206 });
166207
167- $ fiber ->start ();
168- });
208+ $ lowLevelFiber = \Fiber::getCurrent ();
209+ if ($ lowLevelFiber !== null ) {
210+ FiberMap::attachPromise ($ lowLevelFiber , $ promise );
211+ }
212+
213+ return $ promise ;
214+ };
169215}
170216
171217
@@ -230,9 +276,18 @@ function await(PromiseInterface $promise): mixed
230276 $ rejected = false ;
231277 $ resolvedValue = null ;
232278 $ rejectedThrowable = null ;
279+ $ lowLevelFiber = \Fiber::getCurrent ();
280+
281+ if ($ lowLevelFiber !== null && FiberMap::isCancelled ($ lowLevelFiber ) && $ promise instanceof CancellablePromiseInterface) {
282+ $ promise ->cancel ();
283+ }
233284
234285 $ promise ->then (
235- function (mixed $ value ) use (&$ resolved , &$ resolvedValue , &$ fiber ): void {
286+ function (mixed $ value ) use (&$ resolved , &$ resolvedValue , &$ fiber , $ lowLevelFiber , $ promise ): void {
287+ if ($ lowLevelFiber !== null ) {
288+ FiberMap::detachPromise ($ lowLevelFiber , $ promise );
289+ }
290+
236291 if ($ fiber === null ) {
237292 $ resolved = true ;
238293 $ resolvedValue = $ value ;
@@ -241,7 +296,11 @@ function (mixed $value) use (&$resolved, &$resolvedValue, &$fiber): void {
241296
242297 $ fiber ->resume ($ value );
243298 },
244- function (mixed $ throwable ) use (&$ rejected , &$ rejectedThrowable , &$ fiber ): void {
299+ function (mixed $ throwable ) use (&$ rejected , &$ rejectedThrowable , &$ fiber , $ lowLevelFiber , $ promise ): void {
300+ if ($ lowLevelFiber !== null ) {
301+ FiberMap::detachPromise ($ lowLevelFiber , $ promise );
302+ }
303+
245304 if (!$ throwable instanceof \Throwable) {
246305 $ throwable = new \UnexpectedValueException (
247306 'Promise rejected with unexpected value of type ' . (is_object ($ throwable ) ? get_class ($ throwable ) : gettype ($ throwable ))
@@ -285,6 +344,10 @@ function (mixed $throwable) use (&$rejected, &$rejectedThrowable, &$fiber): void
285344 throw $ rejectedThrowable ;
286345 }
287346
347+ if ($ lowLevelFiber !== null ) {
348+ FiberMap::attachPromise ($ lowLevelFiber , $ promise );
349+ }
350+
288351 $ fiber = FiberFactory::create ();
289352
290353 return $ fiber ->suspend ();
0 commit comments