*/ private array $data = []; private int $seen = 0; /** @param callable(string, string): void $onEvent */ public function __construct(private $onEvent, private int $maxBytes = 1048576, private int $maxBuffer = 262144) { } public function push(string $chunk): void { $this->seen += strlen($chunk); $this->buffer .= $chunk; if ($this->seen > $this->maxBytes || strlen($this->buffer) > $this->maxBuffer) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- exception messages go to logs and callers, never echoed; public errors are fixed codes throw new ProviderException(ProviderException::UNAVAILABLE, true, 'Provider stream exceeds local limits.'); } while (($pos = strpos($this->buffer, "\n")) !== false) { $line = rtrim(substr($this->buffer, 0, $pos), "\r"); $this->buffer = substr($this->buffer, $pos + 1); if ($line === '') { $this->dispatch(); } elseif (str_starts_with($line, 'event:')) { $this->event = trim(substr($line, 6)); } elseif (str_starts_with($line, 'data:')) { $this->data[] = ltrim(substr($line, 5), ' '); } } } /** Flushes a final event that was not followed by a blank line. */ public function finish(): void { if (trim($this->buffer) !== '') { $this->push("\n"); } $this->dispatch(); } private function dispatch(): void { if ($this->data !== []) { ($this->onEvent)($this->event, implode("\n", $this->data)); } $this->event = ''; $this->data = []; } }