diff --git a/src/_Private/ConditionState.php b/src/_Private/ConditionState.php new file mode 100644 index 00000000..72cb2b0b --- /dev/null +++ b/src/_Private/ConditionState.php @@ -0,0 +1,139 @@ +> +interface ConditionState { + public function waitForNotificationAsync( + Condition $condition, + Awaitable $notifiers, + ): Awaitable; + public function trySucceed(Condition $condition, T $result): bool; + public function tryFail(Condition $condition, \Exception $exception): bool; +} + +final class NotStarted implements ConditionState { + private function __construct() {} + <<__Memoize>> + public static function getInstance(): this { + return new self(); + } + + public async function waitForNotificationAsync( + Condition $condition, + Awaitable $notifiers, + ): Awaitable { + $handle = ConditionWaitHandle::create($notifiers); + $condition->setState(new AsyncResult($handle)); + try { + return await $handle; + } finally { + $condition->setState(Finished::getInstance()); + } + } + + public function trySucceed(Condition $condition, T $result): bool { + $condition->setState( + new SyncResult( + async { + return $result; + }, + ), + ); + return true; + } + public function tryFail( + Condition $condition, + \Exception $exception, + ): bool { + $condition->setState( + new SyncResult( + async { + throw $exception; + }, + ), + ); + return true; + } +} + +final class AsyncResult implements ConditionState { + public function __construct(private ConditionWaitHandle $resultHandle) {} + public function waitForNotificationAsync( + Condition $_state_ref, + Awaitable $_notifiers, + ): Awaitable { + invariant_violation('Unable to wait for notification twice'); + } + public function trySucceed(Condition $condition, T $result): bool { + $this->resultHandle->succeed($result); + return true; + } + public function tryFail( + Condition $condition, + \Exception $exception, + ): bool { + $this->resultHandle->fail($exception); + return true; + } + +} + +final class SyncResult implements ConditionState { + public function __construct(private Awaitable $resultAwaitable) {} + public function waitForNotificationAsync( + Condition $condition, + Awaitable $_notifiers, + ): Awaitable { + $condition->setState(Finished::getInstance()); + return $this->resultAwaitable; + } + + public function trySucceed(Condition $condition, T $result): bool { + return false; + } + public function tryFail( + Condition $condition, + \Exception $exception, + ): bool { + return false; + } +} + +final class Finished implements ConditionState { + private function __construct() {} + <<__Memoize>> + public static function getInstance(): this { + return new self(); + } + public function waitForNotificationAsync( + Condition $_state_ref, + Awaitable $_notifiers, + ): Awaitable { + invariant_violation('Unable to wait for notification twice'); + } + public function trySucceed(Condition $condition, T $result): bool { + return false; + } + public function tryFail( + Condition $condition, + \Exception $exception, + ): bool { + return false; + } +} diff --git a/src/async/Condition.php b/src/async/Condition.php index 64acefab..196c4c82 100644 --- a/src/async/Condition.php +++ b/src/async/Condition.php @@ -10,46 +10,42 @@ namespace HH\Lib\Async; +use namespace HH\Lib\_Private; + /** * A wrapper around ConditionWaitHandle that allows notification events * to occur before the condition is awaited. */ -class Condition { - private ?Awaitable $condition = null; +class Condition implements ConditionNotifyee { + private _Private\ConditionState $state; + public function __construct() { + $this->state = _Private\NotStarted::getInstance(); + } + + public function setState(_Private\ConditionState $state): void { + $this->state = $state; + } - /** - * Notify the condition variable of success and set the result. - */ final public function succeed(T $result): void { - if ($this->condition === null) { - $this->condition = async { - return $result; - }; - } else { - invariant( - $this->condition is ConditionWaitHandle<_>, - 'Unable to notify AsyncCondition twice', - ); - /* HH_FIXME[4110]: Type error revealed by type-safe instanceof feature. See https://fburl.com/instanceof */ - $this->condition->succeed($result); - } + invariant( + $this->state->trySucceed($this, $result), + 'Unable to notify Condition twice', + ); } - /** - * Notify the condition variable of failure and set the exception. - */ final public function fail(\Exception $exception): void { - if ($this->condition === null) { - $this->condition = async { - throw $exception; - }; - } else { - invariant( - $this->condition is ConditionWaitHandle<_>, - 'Unable to notify AsyncCondition twice', - ); - $this->condition->fail($exception); - } + invariant( + $this->state->tryFail($this, $exception), + 'Unable to notify Condition twice', + ); + } + + final public function trySucceed(T $result): bool { + return $this->state->trySucceed($this, $result); + } + + final public function tryFail(\Exception $exception): bool { + return $this->state->tryFail($this, $exception); } /** @@ -66,9 +62,54 @@ final public function fail(\Exception $exception): void { final public async function waitForNotificationAsync( Awaitable $notifiers, ): Awaitable { - if ($this->condition === null) { - $this->condition = ConditionWaitHandle::create($notifiers); - } - return await $this->condition; + return await $this->state->waitForNotificationAsync($this, $notifiers); } } + + +/** + * Asynchronously wait for the condition variable to be notified and + * return the result or throw the exception received via notification. + * + * The caller must provide an Awaitable $notifiers (which must be a + * WaitHandle) that must not finish before the notification is received. + * This means $notifiers must represent work that is guaranteed to + * eventually trigger the notification. As long as the notification is + * issued only once, asynchronous execution unrelated to $notifiers is + * allowed to trigger the notification. + */ +function wait_for_notification_async( + (function(ConditionNotifyee): Awaitable) $notifiers, +): Awaitable { + $condition = new Condition(); + return $condition->waitForNotificationAsync($notifiers($condition)); +} + +interface ConditionNotifyee<-T> { + + /** + * Notify the condition variable of success and set the $result. + */ + public function succeed(T $result): void; + /** + * Notify the condition variable of success and set the $result. + * + * @return + * true if the condition is set to $result successfully, false if the + * condition was previously set to another result or exception. + */ + public function trySucceed(T $result): bool; + + /** + * Notify the condition variable of failure and set the exception. + */ + public function fail(\Exception $exception): void; + /** + * Notify the condition variable of failure and set the $exception. + * + * @return + * true if the condition is set to $exception successfully, false if the + * condition was previously set to another result or exception. + */ + public function tryFail(\Exception $exception): bool; +}