Skip to content
Merged
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
10 changes: 8 additions & 2 deletions src/Response/SseResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ final class SseResponse extends Text implements StreamingResponseInterface
private ?\Closure $onError;

/**
* @param iterable<Event|string|array>|\Closure(EmitterInterface): void $producer
* @param iterable<Event|string|array|object>|\Closure(EmitterInterface): void $producer
* @param array{
* heartbeat?: int,
* max_duration?: int,
Expand Down Expand Up @@ -115,8 +115,14 @@ public function send(): void
@set_time_limit(0);
@ignore_user_abort(true);

$emitter = new CgiEmitter($this->maxDuration);
$this->emit(new CgiEmitter($this->maxDuration));
}

/**
* 使用指定发送器执行生产者;便于测试和自定义运行时注入。
*/
public function emit(EmitterInterface $emitter): void
{
try {
if ($this->retry !== null) {
$emitter->retry($this->retry);
Expand Down
59 changes: 59 additions & 0 deletions src/Sse/AbstractEmitter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace FastD\Http\Sse;

/**
* SSE 发送器公共实现。
*
* 统一帧归一化、注释帧、重连间隔和关闭状态;具体运行时只需实现 write()。
*/
abstract class AbstractEmitter implements EmitterInterface
{
protected bool $closed = false;

public function send(string|array|object $event): void
{
if ($this->isClosed()) {
return;
}

$this->write($this->normalize($event)->format());
}

public function comment(string $text): void
{
if ($this->isClosed()) {
return;
}

$this->write(': ' . str_replace(["\r", "\n"], ' ', $text) . "\n\n");
}

public function retry(int $milliseconds): void
{
if ($milliseconds < 0 || $this->isClosed()) {
return;
}

$this->write('retry: ' . $milliseconds . "\n\n");
}

public function isClosed(): bool
{
return $this->closed;
}

public function close(): void
{
$this->closed = true;
}

abstract protected function write(string $frame): void;

protected function normalize(string|array|object $event): Event
{
return $event instanceof Event ? $event : Event::data($event);
}
}
43 changes: 2 additions & 41 deletions src/Sse/CgiEmitter.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@
* 是否停止。生产者在长时间阻塞(如等待上游)期间无法产生心跳——需要心跳
* 的 AI 流式场景建议使用 Swoole 运行时。
*/
final class CgiEmitter implements EmitterInterface
final class CgiEmitter extends AbstractEmitter
{
private bool $closed = false;

/** @var \Closure(string): void */
private \Closure $writer;

Expand Down Expand Up @@ -51,33 +49,6 @@ public function __construct(
$this->deadline = $maxDuration > 0 ? ($this->clock)() + (float) $maxDuration : null;
}

public function send(Event|string|array $event): void
{
if ($this->isClosed()) {
return;
}

$this->write($this->normalize($event)->format());
}

public function comment(string $text): void
{
if ($this->isClosed()) {
return;
}

$this->write(': ' . str_replace(["\r", "\n"], ' ', $text) . "\n\n");
}

public function retry(int $milliseconds): void
{
if ($this->isClosed() || $milliseconds < 0) {
return;
}

$this->write('retry: ' . $milliseconds . "\n\n");
}

public function isClosed(): bool
{
if ($this->closed) {
Expand All @@ -99,18 +70,8 @@ public function isClosed(): bool
return false;
}

public function close(): void
{
$this->closed = true;
}

private function write(string $frame): void
protected function write(string $frame): void
{
($this->writer)($frame);
}

private function normalize(Event|string|array $event): Event
{
return $event instanceof Event ? $event : Event::data($event);
}
}
4 changes: 2 additions & 2 deletions src/Sse/EmitterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ interface EmitterInterface
/**
* 发送一条事件。
*
* @param Event|string|array<array-key, mixed> $event string 作为 data;数组/对象 JSON 编码
* @param Event|string|array<array-key, mixed>|object $event string 作为 data;数组/对象 JSON 编码
*/
public function send(Event|string|array $event): void;
public function send(string|array|object $event): void;

/**
* 发送注释帧(常用于心跳)。
Expand Down
2 changes: 2 additions & 0 deletions tests/sse/CgiEmitterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ public function testSendAcceptsStringArrayAndEvent(): void

$emitter->send('hello');
$emitter->send(['name' => 'fastd']);
$emitter->send((object) ['source' => 'object']);
$emitter->send(new Event('done', 'done', '9'));

$this->assertSame([
"data: hello\n\n",
"data: {\"name\":\"fastd\"}\n\n",
"data: {\"source\":\"object\"}\n\n",
"id: 9\nevent: done\ndata: done\n\n",
], $this->frames);
}
Expand Down
17 changes: 16 additions & 1 deletion tests/sse/SseResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ public function testClosureProducerReceivesEmitter(): void
$this->assertSame([": connected\n\n", "event: notice\ndata: hello\n\n"], $emitter->frames);
}

public function testEmitUsesInjectedEmitterAndRetry(): void
{
$response = new SseResponse(
function (EmitterInterface $emitter): void {
$emitter->send('hello');
},
['retry' => 1500],
);

$emitter = $this->collectingEmitter();
$response->emit($emitter);

$this->assertSame(["retry: 1500\n\n", "data: hello\n\n"], $emitter->frames);
}

public function testOptionsAreExposed(): void
{
$response = new SseResponse([], [
Expand Down Expand Up @@ -159,7 +174,7 @@ private function collectingEmitter(): EmitterInterface
/** @var list<string> */
public array $frames = [];

public function send(Event|string|array $event): void
public function send(string|array|object $event): void
{
$this->frames[] = ($event instanceof Event ? $event : Event::data($event))->format();
}
Expand Down
Loading