Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 130 additions & 36 deletions README.md

Large diffs are not rendered by default.

83 changes: 59 additions & 24 deletions src/DaisukeDaisuke/AwaitFormOptions/AwaitFormOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,41 @@ final private function __construct(){
}

/**
* @param array<FormOptions> $options
* @throws \Throwable
* Releases every child before detaching the bridge. Cleanup is deliberately
* best-effort: one faulty userDispose() hook cannot strand the remaining
* options or their child coroutines.
*
* @param list<FormOptions|MenuOptions> $options
*/
private static function cleanup(RequestResponseBridge $bridge, array $options) : void{
$firstException = null;
try{
$bridge->close(new AwaitFormOptionsChildException("", AwaitFormOptionsChildException::ERR_COROUTINE_ABORTED));
}catch(\Throwable $exception){
$firstException = $exception;
}
foreach($options as $option){
try{
$option->dispose();
}catch(\Throwable $exception){
$firstException ??= $exception;
}
}
$bridge->dispose();
if($firstException !== null){
throw $firstException;
}
}

/**
* Starts sendFormAsync() as a standalone coroutine.
*
* Player rejection and parent-level form failures are swallowed. Developer
* errors, including invalid option types and invalid request payloads, are not
* swallowed.
*
* @param array<int|string, FormOptions> $options
* @throws AwaitFormOptionsExpectedCrashException
*/
public static function sendForm(Player $player, string $title, array $options) : void{
Await::f2c(function() use ($options, $title, $player){
Expand Down Expand Up @@ -81,8 +114,9 @@ public static function sendForm(Player $player, string $title, array $options) :
* Developer mistakes such as invalid option types or invalid request values
* are reported as AwaitFormOptionsExpectedCrashException subclasses.
*
* @param array<FormOptions> $options Awaitable form option providers
* @throws FormValidationException|AwaitFormOptionsParentException|AwaitFormOptionsInvalidValueException I don't write \throwable because it's enough to piss off phpstan :<
* @param array<int|string, FormOptions> $options Awaitable form option providers
* @return \Generator<mixed, mixed, mixed, array<int|string, array<int|string, mixed>>>
* @throws AwaitFormOptionsExpectedCrashException|AwaitFormOptionsParentException
*/
public static function sendFormAsync(Player $player, string $title, array $options) : \Generator{
$bridge = new RequestResponseBridge();
Expand All @@ -105,7 +139,7 @@ public static function sendFormAsync(Player $player, string $title, array $optio
Utils::validateArrayValueType($forms, static function(\Generator|FormOptions $value) : void{
});
}catch(\TypeError){
throw new AwaitFormOptionsInvalidValueException($option::class . "::getOptions() must return an array(list) of \Generator, see also AwaitFormOptions::sendFromAsync()");
throw new AwaitFormOptionsInvalidValueException($option::class . "::getOptions() must return an array of \Generator or nested FormOptions, see also AwaitFormOptions::sendFormAsync()");
}

foreach($forms as $key1 => $item){
Expand Down Expand Up @@ -160,8 +194,9 @@ public static function sendFormAsync(Player $player, string $title, array $optio
/** @phpstan-ignore function.impossibleType */
if(!is_scalar($key) || is_object($key)){
//HACK: Making backtraces useful
$bridge->reject($id, new AwaitFormOptionsExpectedCrashException("key must be scalar, see also AwaitFormOptions::sendFormAsync()"));
return [];
$exception = new AwaitFormOptionsExpectedCrashException("key must be scalar, see also AwaitFormOptions::sendFormAsync()");
$bridge->reject($id, $exception);
throw $exception;
}
$keys[] = $key;
$options[] = $item;
Expand Down Expand Up @@ -197,18 +232,21 @@ public static function sendFormAsync(Player $player, string $title, array $optio
throw new AwaitFormOptionsExpectedCrashException("Unhandled AwaitFormOptionsChildException", $exception->getCode(), $exception);
}
}finally{
foreach($needDispose as $item){
$item->dispose();
}
$bridge->dispose();
self::cleanup($bridge, $needDispose);
unset($bridge, $needDispose);
}
//This code path should be unreachable :(
}

/**
* @param array<MenuOptions> $buttons
* @throws \Throwable
* Starts sendMenuAsync() as a standalone coroutine.
*
* Player rejection and parent-level form failures are swallowed. Developer
* errors, including invalid option types and invalid request payloads, are not
* swallowed.
*
* @param array<int|string, MenuOptions> $buttons
* @throws AwaitFormOptionsExpectedCrashException
*/
public static function sendMenu(Player $player, string $title, string $content, array $buttons) : void{
Await::f2c(function() use ($content, $buttons, $title, $player){
Expand Down Expand Up @@ -252,9 +290,9 @@ public static function sendMenu(Player $player, string $title, string $content,
* Developer mistakes such as invalid option types or invalid request values
* are reported as AwaitFormOptionsExpectedCrashException subclasses.
*
* @param array<MenuOptions> $buttons Awaitable menu option providers
* @return \Generator<mixed>
* @throws FormValidationException|AwaitFormOptionsExpectedCrashException|AwaitFormOptionsParentException I don't write \throwable because it's enough to piss off phpstan :<
* @param array<int|string, MenuOptions> $buttons Awaitable menu option providers
* @return \Generator<mixed, mixed, mixed, mixed>
* @throws AwaitFormOptionsExpectedCrashException|AwaitFormOptionsParentException
*/
public static function sendMenuAsync(Player $player, string $title, string $content, array $buttons) : \Generator{
$bridge = new RequestResponseBridge();
Expand All @@ -279,7 +317,7 @@ public static function sendMenuAsync(Player $player, string $title, string $cont
Utils::validateArrayValueType($array, static function(\Generator|MenuOptions $value) : void{
});
}catch(\TypeError){
throw new AwaitFormOptionsInvalidValueException($option::class . "::getOptions() must return an array(list) of \Generator, see also AwaitFormOptions::sendMenuAsync()");
throw new AwaitFormOptionsInvalidValueException($option::class . "::getOptions() must return an array of \Generator or nested MenuOptions, see also AwaitFormOptions::sendMenuAsync()");
}

foreach($array as $key2 => $item){
Expand Down Expand Up @@ -309,7 +347,7 @@ public static function sendMenuAsync(Player $player, string $title, string $cont
// 各 MenuOptions に紐づくボタン群を構築
$counter = 0;
$index = []; // id => [start, end, keys]
$flatButtons = []; // 表示用に flatten された Button[]
$flatButtons = []; // 表示用に flatten された MenuElement[]

foreach(yield from $bridge->getAllExpected() as $id => $array){
$keys = [];
Expand All @@ -320,9 +358,9 @@ public static function sendMenuAsync(Player $player, string $title, string $cont
if(!(array_is_list($item) && count($item) === 2)){
if(array_is_list($item) && count($item) !== 2){
$exception = new AwaitFormOptionsExpectedCrashException(
"The request value must be a 2-element list array [Button, key], but an array with " . count($item) . " element(s) was given. \n" .
"The request value must be a 2-element list array [MenuElement, key], but an array with " . count($item) . " element(s) was given. \n" .
" (key: " . $key . "). " .
"Ensure that your form returns an array like [Button, SelectedKey]. " .
"Ensure that your menu returns an array like [MenuElement, SelectedValue]. " .
"See also: AwaitFormOptions::sendMenuAsync()"
);
$bridge->reject($id, $exception);
Expand Down Expand Up @@ -376,10 +414,7 @@ public static function sendMenuAsync(Player $player, string $title, string $cont
// 該当しなかった場合はフォーム不正とみなす
throw new AwaitFormOptionsParentException("An invalid MenuElement selection was made", AwaitFormOptionsParentException::ERR_VERIFICATION_FAILED);
}finally{
foreach($needDispose as $item){
$item->dispose();
}
$bridge->dispose();
self::cleanup($bridge, $needDispose);
unset($bridge, $needDispose, $buttons, $flatButtons, $flatOptions, $index, $keys, $returns);
}
}
Expand Down
25 changes: 17 additions & 8 deletions src/DaisukeDaisuke/AwaitFormOptions/FormBridgeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,22 @@ trait FormBridgeTrait{
private bool $requested = false;
private ?int $reservesId = null;
private bool $disposed = false;
private bool $attached = false;

/**
* Attaches the shared request/response bridge to this option instance.
*
* Option instances are single-use. Once dispose() has been called, setting a new
* bridge would allow stale coroutine state to be reused, so it is rejected early.
* Option instances are single-use. Attaching the same instance while it is
* already active would overwrite its bridge and corrupt both parent coroutines.
*
* @internal
* @throws AwaitFormOptionsInvalidValueException
*/
final public function setBridge(RequestResponseBridge $bridge) : void{
if($this->isDisposed()){
if($this->isDisposed() || $this->attached){
throw new AwaitFormOptionsInvalidValueException("Option reuse detected, class: " . static::class);
}
$this->attached = true;
$this->bridge = $bridge;
}

Expand All @@ -45,9 +47,16 @@ final public function setBridge(RequestResponseBridge $bridge) : void{
* @internal
*/
final public function dispose() : void{
if($this->isDisposed()){
return;
}

$this->setDisposed(true);
unset($this->bridge, $this->reservesId);
$this->userDispose();
try{
$this->userDispose();
}finally{
unset($this->bridge, $this->reservesId);
}
}

/**
Expand All @@ -63,7 +72,7 @@ abstract protected function userDispose() : void;
*
* Higher priority values are resumed first when the parent calls tryFinalize().
*
* @return \Generator<mixed>
* @return \Generator<mixed, mixed, mixed, void>
*/
final protected function finalize(int $priority = 0) : \Generator{
yield from $this->bridge->finalize($priority);
Expand Down Expand Up @@ -94,8 +103,8 @@ final protected function schedule() : void{
* treated as an expected crash because the parent request accounting would no
* longer be reliable.
*
* @param array{FormControl|MenuElement, mixed}|array<FormControl|MenuElement|list<FormControl|MenuElement>|list<array{FormControl|MenuElement, mixed}>> $value
* @return \Generator<mixed>
* @param array{FormControl|MenuElement, mixed}|array<int|string, FormControl|MenuElement|array{FormControl|MenuElement, mixed}> $value
* @return \Generator<mixed, mixed, mixed, mixed>
* @throws AwaitFormOptionsChildException|AwaitFormOptionsExpectedCrashException
*/
final protected function request(array $value) : \Generator{
Expand Down
2 changes: 1 addition & 1 deletion src/DaisukeDaisuke/AwaitFormOptions/FormOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ abstract class FormOptions{
use FormBridgeTrait;

/**
* @return array<\Generator>|array<FormOptions>
* @return array<int|string, \Generator|FormOptions>
*/
abstract public function getOptions() : array;
}
2 changes: 1 addition & 1 deletion src/DaisukeDaisuke/AwaitFormOptions/MenuOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ abstract class MenuOptions{
use FormBridgeTrait;

/**
* @return array<\Generator>|array<MenuOptions>
* @return array<int|string, \Generator|MenuOptions>
*/
abstract public function getOptions() : array;
}
Loading