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+ $ promise = FiberMap::getPromise ($ fiber );
202+ if ($ promise instanceof CancellablePromiseInterface) {
203+ $ promise ->cancel ();
164204 }
165205 });
166206
167- $ fiber ->start ();
168- });
207+ $ lowLevelFiber = \Fiber::getCurrent ();
208+ if ($ lowLevelFiber !== null ) {
209+ FiberMap::setPromise ($ lowLevelFiber , $ promise );
210+ }
211+
212+ return $ promise ;
213+ };
169214}
170215
171216
@@ -230,9 +275,18 @@ function await(PromiseInterface $promise): mixed
230275 $ rejected = false ;
231276 $ resolvedValue = null ;
232277 $ rejectedThrowable = null ;
278+ $ lowLevelFiber = \Fiber::getCurrent ();
279+
280+ if ($ lowLevelFiber !== null && FiberMap::isCancelled ($ lowLevelFiber ) && $ promise instanceof CancellablePromiseInterface) {
281+ $ promise ->cancel ();
282+ }
233283
234284 $ promise ->then (
235- function (mixed $ value ) use (&$ resolved , &$ resolvedValue , &$ fiber ): void {
285+ function (mixed $ value ) use (&$ resolved , &$ resolvedValue , &$ fiber , $ lowLevelFiber , $ promise ): void {
286+ if ($ lowLevelFiber !== null ) {
287+ FiberMap::unsetPromise ($ lowLevelFiber , $ promise );
288+ }
289+
236290 if ($ fiber === null ) {
237291 $ resolved = true ;
238292 $ resolvedValue = $ value ;
@@ -241,7 +295,11 @@ function (mixed $value) use (&$resolved, &$resolvedValue, &$fiber): void {
241295
242296 $ fiber ->resume ($ value );
243297 },
244- function (mixed $ throwable ) use (&$ rejected , &$ rejectedThrowable , &$ fiber ): void {
298+ function (mixed $ throwable ) use (&$ rejected , &$ rejectedThrowable , &$ fiber , $ lowLevelFiber , $ promise ): void {
299+ if ($ lowLevelFiber !== null ) {
300+ FiberMap::unsetPromise ($ lowLevelFiber , $ promise );
301+ }
302+
245303 if (!$ throwable instanceof \Throwable) {
246304 $ throwable = new \UnexpectedValueException (
247305 'Promise rejected with unexpected value of type ' . (is_object ($ throwable ) ? get_class ($ throwable ) : gettype ($ throwable ))
@@ -285,6 +343,10 @@ function (mixed $throwable) use (&$rejected, &$rejectedThrowable, &$fiber): void
285343 throw $ rejectedThrowable ;
286344 }
287345
346+ if ($ lowLevelFiber !== null ) {
347+ FiberMap::setPromise ($ lowLevelFiber , $ promise );
348+ }
349+
288350 $ fiber = FiberFactory::create ();
289351
290352 return $ fiber ->suspend ();
0 commit comments