-
Notifications
You must be signed in to change notification settings - Fork 2
/
Promise.php
248 lines (229 loc) · 7.11 KB
/
Promise.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
<?php
namespace Aternos\Taskmaster\Communication\Promise;
use Aternos\Taskmaster\Task\TaskInterface;
use Closure;
use Exception;
use Fiber;
use ReflectionException;
use ReflectionFunction;
use ReflectionIntersectionType;
use ReflectionType;
use ReflectionUnionType;
use RuntimeException;
use Throwable;
/**
* Class Promise
*
* Promise implementation, mainly used internally as return value for async functions
* An async function can immediately return a promise and resolve it later
* You can add success and exception handlers to the promise using {@see Promise::then()} and {@see Promise::catch()}
* The promise can be resolved using {@see Promise::resolve()} and rejected using {@see Promise::reject()}
* If you are in a fiber (e.g. in {@link TaskInterface::run()}), you can wait for the promise to resolve or throw using {@see Promise::wait()}.
*
* @package Aternos\Taskmaster\Communication\Promise
*/
class Promise
{
/**
* @var Closure[]
*/
protected array $successHandlers = [];
/**
* @var Closure[]
*/
protected array $exceptionHandlers = [];
/**
* @var Fiber[]
*/
protected array $fibers = [];
protected mixed $value = null;
protected Exception $exception;
protected bool $resolved = false;
protected bool $failed = false;
/**
* Add a success handler to the promise, the promise result will be passed as first argument to the callback
*
* Success handlers are called in the order they were added.
*
* @param Closure $callback
* @return $this
*/
public function then(Closure $callback): static
{
if ($this->resolved) {
$callback($this->value, ...$this->getAdditionalResolveArguments());
return $this;
}
$this->successHandlers[] = $callback;
return $this;
}
/**
* Add an exception handler to the promise, the exception will be passed as first argument to the callback
*
* Exception handlers can define which exception types they want to handle by adding a type hint to the first argument.
* If the exception does not match the type hint, the handler will not be called.
* If no type hint is defined, the handler will be called for all exceptions.
* Exception handlers are called in the order they were added.
*
* @param Closure $callback
* @return $this
*/
public function catch(Closure $callback): static
{
if ($this->failed) {
$callback($this->exception, ...$this->getAdditionalRejectArguments());
return $this;
}
$this->exceptionHandlers[] = $callback;
return $this;
}
/**
* Resolve the promise
*
* A promise can only be resolved once.
* All success handlers will be called with the given value.
* After that all waiting fibers will be resumed with the given value.
*
* @param mixed $value
* @return $this
* @throws Throwable
*/
public function resolve(mixed $value = null): static
{
if ($this->resolved || $this->failed) {
return $this;
}
$this->resolved = true;
$this->value = $value;
foreach ($this->successHandlers as $callback) {
$callback($value, ...$this->getAdditionalResolveArguments());
}
foreach ($this->fibers as $fiber) {
$fiber->resume($value);
}
return $this;
}
/**
* Reject the promise with an exception
*
* A promise can only be rejected once.
* Matching exception handlers will be called with the given exception, see {@see Promise::catch()}.
* After that all waiting fibers will be resumed by throwing the given exception.
*
* @param Exception $exception
* @return $this
* @throws Throwable
*/
public function reject(Exception $exception): static
{
if ($this->failed || $this->resolved) {
return $this;
}
$this->failed = true;
$this->exception = $exception;
foreach ($this->exceptionHandlers as $callback) {
if (!$this->matchesFirstArgument($callback, $exception)) {
continue;
}
$callback($exception, ...$this->getAdditionalRejectArguments());
}
foreach ($this->fibers as $fiber) {
$fiber->throw($exception);
}
return $this;
}
/**
* Check if the exception matches the first argument type hint of the given callback
*
* @throws ReflectionException
*/
protected function matchesFirstArgument(Closure $callback, Exception $exception): bool
{
$reflection = new ReflectionFunction($callback);
$parameters = $reflection->getParameters();
if (count($parameters) === 0) {
return true;
}
$firstArgument = $parameters[0];
$type = $firstArgument->getType();
if ($type === null) {
return true;
}
return $this->matchesType($exception, $type);
}
/**
* Check if the given object matches the given type
*
* Resolves union and intersection types recursively
*
* @param Exception $object
* @param ReflectionType $type
* @return bool
*/
protected function matchesType(Exception $object, ReflectionType $type): bool
{
if ($type instanceof ReflectionUnionType) {
foreach ($type->getTypes() as $unionType) {
if ($this->matchesType($object, $unionType)) {
return true;
}
}
return false;
}
if ($type instanceof ReflectionIntersectionType) {
foreach ($type->getTypes() as $intersectionType) {
if (!$this->matchesType($object, $intersectionType)) {
return false;
}
}
return true;
}
if ($type instanceof \ReflectionNamedType) {
if ($type->getName() === "mixed" || $type->getName() === "object") {
return true;
}
return is_a($object, $type->getName());
}
return false;
}
/**
* Wait for the promise to resolve or throw
*
* This method can only be called from within a fiber, e.g. in {@link TaskInterface::run()}.
*
* @return mixed
* @throws Throwable
*/
public function wait(): mixed
{
if ($this->resolved) {
return $this->value;
}
if ($this->failed) {
throw $this->exception;
}
if (!Fiber::getCurrent()) {
throw new RuntimeException("Promise::wait() can only be called from within a fiber");
}
$this->fibers[] = Fiber::getCurrent();
return Fiber::suspend();
}
/**
* Get additional arguments for success handlers
*
* @return array
*/
protected function getAdditionalResolveArguments(): array
{
return [];
}
/**
* Get additional arguments for exception handlers
*
* @return array
*/
protected function getAdditionalRejectArguments(): array
{
return [];
}
}