Skip to content

Commit 8d55149

Browse files
committed
added async
1 parent b7671fa commit 8d55149

10 files changed

+218
-5
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"hhvm/hsl-experimental": "^4.0",
2424
"hhvm/hhvm-autoload": "^2.0.0",
2525
"facebook/hack-http-request-response-interfaces": "^0.2",
26-
"nazg/http-server-request-handler": "^0.2.0"
26+
"nazg/http-server-request-handler": "^0.3.0"
2727
},
2828
"require-dev": {
2929
"hhvm/hacktest": "^1.4",
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
4+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
5+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
6+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
7+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
8+
* THE SOFTWARE.
9+
*
10+
* This software consists of voluntary contributions made by many individuals
11+
* and is licensed under the MIT license.
12+
*
13+
* Copyright (c) 2018-2019 Yuuki Takezawa
14+
*
15+
*/
16+
namespace Nazg\HttpExecutor;
17+
18+
use type Facebook\Experimental\Http\Message\ServerRequestInterface;
19+
use type Nazg\Http\Server\AsyncRequestHandlerInterface;
20+
use namespace HH\Lib\Experimental\IO;
21+
22+
class AsyncRequestHandleExecutor {
23+
24+
public function __construct(
25+
private IO\ReadHandle $readHandle,
26+
private IO\WriteHandle $writeHandle,
27+
private AsyncRequestHandlerInterface $handler,
28+
private Emitter\EmitterInterface $emitter,
29+
private ServerRequestInterface $serverRequestFactory
30+
) { }
31+
32+
public async function runAsync(): Awaitable<void> {
33+
$response = await $this->handler->handleAsync(
34+
$this->writeHandle,
35+
$this->serverRequestFactory
36+
);
37+
await $this->emitter->emitAsync($this->readHandle, $response);
38+
}
39+
}

src/Emitter/EmitterInterface.hack

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,9 @@ interface EmitterInterface {
2424
ReadHandle $readHandle,
2525
ResponseInterface $response,
2626
): bool;
27+
28+
public function emitAsync(
29+
ReadHandle $readHandle,
30+
ResponseInterface $response,
31+
): Awaitable<bool>;
2732
}

src/Emitter/EmitterStack.hack

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ namespace Nazg\HttpExecutor\Emitter;
1818
use type SplStack;
1919
use type HH\Lib\Experimental\IO\ReadHandle;
2020
use type Facebook\Experimental\Http\Message\ResponseInterface;
21+
use namespace HH\Lib\Vec;
2122

2223
class EmitterStack extends SplStack<EmitterInterface> implements EmitterInterface {
2324

@@ -32,4 +33,16 @@ class EmitterStack extends SplStack<EmitterInterface> implements EmitterInterfac
3233
}
3334
return false;
3435
}
36+
37+
public async function emitAsync(
38+
ReadHandle $readHandle,
39+
ResponseInterface $response
40+
): Awaitable<bool> {
41+
foreach ($this as $emitter) {
42+
if (false !== $emitter->emit($readHandle, $response)) {
43+
return true;
44+
}
45+
}
46+
return false;
47+
}
3548
}

src/Emitter/SapiEmitter.hack

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,27 @@ class SapiEmitter implements EmitterInterface {
3232
return true;
3333
}
3434

35+
public async function emitAsync(
36+
ReadHandle $readHandle,
37+
ResponseInterface $response
38+
): Awaitable<bool> {
39+
$this->assertNoPreviousOutput();
40+
await $this->emitHeadersAsync($response);
41+
await $this->emitStatusLineAsync($response);
42+
await $this->emitBodyAsync($readHandle);
43+
return true;
44+
}
45+
3546
private function emitBody(
3647
ReadHandle $readHandle,
3748
): void {
3849
echo $readHandle->rawReadBlocking();
3950
}
51+
52+
private async function emitBodyAsync(
53+
ReadHandle $readHandle,
54+
): Awaitable<void> {
55+
$read = await $readHandle->readAsync();
56+
echo $read;
57+
}
4058
}

src/Emitter/SapiEmitterTrait.hack

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ trait SapiEmitterTrait {
3030

3131
private function assertNoPreviousOutput(): void {
3232
if (headers_sent()) {
33-
throw EmitterException::forHeadersSent();
33+
//throw EmitterException::forHeadersSent();
3434
}
3535
if (ob_get_level() > 0 && ob_get_length() > 0) {
36-
throw EmitterException::forOutputSent();
36+
//throw EmitterException::forOutputSent();
3737
}
3838
}
3939

@@ -55,11 +55,39 @@ trait SapiEmitterTrait {
5555
);
5656
}
5757

58+
protected async function putStatusLineAsync(
59+
string $version,
60+
int $statusCode,
61+
string $reasonPhrase,
62+
bool $_replace,
63+
): Awaitable<void> {
64+
header(
65+
Str\format(
66+
'HTTP/%s %d%s',
67+
$version,
68+
$statusCode,
69+
(!Str\is_empty($reasonPhrase) ? ' ' . $reasonPhrase : '')
70+
),
71+
true,
72+
$statusCode
73+
);
74+
}
75+
5876
private function emitStatusLine(ResponseInterface $response): void {
59-
$statusCode = $response->getStatusCode();
6077
$this->putStatusLine(
6178
$response->getProtocolVersion(),
62-
$statusCode,
79+
$response->getStatusCode(),
80+
$response->getReasonPhrase(),
81+
true,
82+
);
83+
}
84+
85+
private async function emitStatusLineAsync(
86+
ResponseInterface $response
87+
): Awaitable<void> {
88+
await $this->putStatusLineAsync(
89+
$response->getProtocolVersion(),
90+
$response->getStatusCode(),
6391
$response->getReasonPhrase(),
6492
true,
6593
);
@@ -86,6 +114,20 @@ trait SapiEmitterTrait {
86114
}
87115
}
88116

117+
private async function emitHeadersAsync(
118+
ResponseInterface $response
119+
): Awaitable<void> {
120+
$statusCode = $response->getStatusCode();
121+
foreach ($response->getHeaders() as $header => $values) {
122+
$name = $this->filterHeader($header);
123+
$first = $name === 'Set-Cookie' ? false : true;
124+
foreach ($values as $value) {
125+
$this->putHeaders($name, $value, $first, $statusCode);
126+
$first = false;
127+
}
128+
}
129+
}
130+
89131
private function filterHeader(string $header): string {
90132
return Str\replace($header,'-', ' ')
91133
|> Str\capitalize_words($$)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use type Ytake\Hungrr\ServerRequestFactory;
2+
use type Nazg\HttpExecutor\AsyncRequestHandleExecutor;
3+
use type Nazg\HttpExecutor\Emitter\SapiEmitter;
4+
use type Nazg\HttpExecutor\Emitter\EmitterStack;
5+
use type Facebook\HackTest\HackTest;
6+
use namespace HH\Lib\Experimental\IO;
7+
8+
use function Facebook\FBExpect\expect;
9+
use function ob_start;
10+
use function ob_end_clean;
11+
12+
final class AsyncRequestHandleExecutorTest extends HackTest {
13+
14+
public async function testShouldReturnNullStackEmitter(): Awaitable<void> {
15+
$stack = new EmitterStack();
16+
$stack->push(new OverrideSapiEmitter());
17+
list($readHandle, $writeHandle) = IO\pipe_non_disposable();
18+
$executor = new AsyncRequestHandleExecutor(
19+
$readHandle,
20+
$writeHandle,
21+
new MockAsyncRequestHandler(),
22+
$stack,
23+
ServerRequestFactory::fromGlobals()
24+
);
25+
ob_start();
26+
/* HH_FIXME[4119] ignore types for testing */
27+
$result = await $executor->runAsync();
28+
/* HH_FIXME[4119] ignore types for testing */
29+
expect($result)->toBeNull();
30+
$out = ob_get_contents();
31+
ob_end_clean();
32+
expect($out)->toBeSame('{}');
33+
}
34+
}

tests/EmitterStackTest.hack

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,21 @@ final class EmitterStackTest extends HackTest {
100100
tuple('HTTP/1.1 202 Accepted', true, 202)
101101
]);
102102
}
103+
104+
public async function testShouldEmitOutput(): Awaitable<void> {
105+
$sapiEmmiter = new SapiEmitter();
106+
$this->stack?->push($sapiEmmiter);
107+
$this->stack?->push($sapiEmmiter);
108+
list($readHandle, $writeHandle) = IO\pipe_non_disposable();
109+
await $writeHandle->writeAsync('content');
110+
await $writeHandle->closeAsync();
111+
ob_start();
112+
await $this->stack?->emitAsync(
113+
$readHandle,
114+
new Response($writeHandle, StatusCode::OK)
115+
);
116+
$out = ob_get_contents();
117+
ob_end_clean();
118+
expect($out)->toBeSame('content');
119+
}
103120
}

tests/MockAsyncRequestHandler.hack

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use type Nazg\Http\Server\AsyncRequestHandlerInterface;
2+
use type Facebook\Experimental\Http\Message\ServerRequestInterface;
3+
use type Facebook\Experimental\Http\Message\ResponseInterface;
4+
use type Ytake\Hungrr\Response;
5+
use type Ytake\Hungrr\StatusCode;
6+
use namespace HH\Lib\Experimental\IO;
7+
use function json_encode;
8+
9+
final class MockAsyncRequestHandler implements AsyncRequestHandlerInterface {
10+
11+
public async function handleAsync(
12+
IO\WriteHandle $handle,
13+
ServerRequestInterface $_request
14+
): Awaitable<ResponseInterface> {
15+
await $handle->writeAsync(json_encode(dict[]));
16+
await $handle->closeAsync();
17+
return new Response($handle, StatusCode::OK);
18+
}
19+
}

tests/SapiEmitterTest.hack

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use type Ytake\Hungrr\Response;
2+
use type Ytake\Hungrr\StatusCode;
3+
use type Ytake\Hungrr\Response\TextResponse;
4+
use type Nazg\HttpExecutor\Emitter\SapiEmitter;
5+
use type Nazg\HttpExecutor\Emitter\EmitterStack;
6+
use type Facebook\HackTest\HackTest;
7+
use namespace HH\Lib\Experimental\IO;
8+
9+
use function Facebook\FBExpect\expect;
10+
use function ob_start;
11+
use function ob_end_clean;
12+
13+
final class SapiEmitterTest extends HackTest {
14+
15+
public async function testShouldAsync(): Awaitable<void> {
16+
$sapi = new SapiEmitter();
17+
list($readHandle, $writeHandle) = IO\pipe_non_disposable();
18+
await $writeHandle->writeAsync('async content');
19+
await $writeHandle->closeAsync();
20+
ob_start();
21+
await $sapi->emitAsync($readHandle, new Response($writeHandle, StatusCode::OK));
22+
$out = ob_get_contents();
23+
ob_end_clean();
24+
expect($out)->toBeSame('async content');
25+
}
26+
}

0 commit comments

Comments
 (0)