1717 */
1818final class StreamResponse implements ResponseHasMetaInformationContract, ResponseStreamContract
1919{
20+ private const STREAM_READ_SIZE = 64 * 1024 ;
21+
22+ private string $ lineBuffer = '' ;
23+
2024 /**
2125 * Creates a new Stream Response instance.
2226 *
@@ -34,64 +38,133 @@ public function __construct(
3438 */
3539 public function getIterator (): Generator
3640 {
37- while (! $ this ->response ->getBody ()-> eof ()) {
38- $ line = $ this -> readLine ( $ this -> response -> getBody ()) ;
41+ $ body = $ this ->response ->getBody ();
42+ $ event = null ;
3943
40- $ event = null ;
44+ while (( $ line = $ this -> readLine ( $ body )) !== null ) {
4145 if (str_starts_with ($ line , 'event: ' )) {
4246 $ event = trim (substr ($ line , strlen ('event: ' )));
43- $ line = $ this ->readLine ($ this ->response ->getBody ());
47+
48+ unset($ line );
49+
50+ continue ;
4451 }
4552
4653 if (! str_starts_with ($ line , 'data: ' )) {
54+ $ event = null ;
55+
56+ unset($ line );
57+
4758 continue ;
4859 }
4960
50- $ data = trim (substr ($ line , strlen ('data: ' )));
61+ $ data = substr ($ line , strlen ('data: ' ));
62+
63+ unset($ line );
64+
65+ if (strlen ($ data ) <= 16 && trim ($ data ) === '[DONE] ' ) {
66+ unset($ data );
5167
52- if ($ data === '[DONE] ' ) {
5368 break ;
5469 }
5570
56- /** @var array{error?: array{message: string|array<int, string>, type: string, code: string}, type?: string} $response */
57- $ response = json_decode ($ data , true , flags: JSON_THROW_ON_ERROR );
71+ /** @var array{error?: array{message: string|array<int, string>, type: string, code: string}, type?: string} $attributes */
72+ $ attributes = json_decode ($ data , true , flags: JSON_THROW_ON_ERROR );
73+
74+ unset($ data );
5875
59- if (isset ($ response ['error ' ])) {
60- throw new ErrorException ($ response ['error ' ], $ this ->response );
76+ if (isset ($ attributes ['error ' ])) {
77+ throw new ErrorException ($ attributes ['error ' ], $ this ->response );
6178 }
6279
6380 $ skippableTypes = ['ping ' , 'keepalive ' , 'response.keep_alive ' ];
64- if (isset ($ response ['type ' ]) && in_array ($ response ['type ' ], $ skippableTypes , true )) {
81+
82+ if (isset ($ attributes ['type ' ]) && in_array ($ attributes ['type ' ], $ skippableTypes , true )) {
83+ $ event = null ;
84+
85+ unset($ attributes );
86+
6587 continue ;
6688 }
6789
6890 if ($ event !== null ) {
69- $ response ['__event ' ] = $ event ;
91+ $ attributes ['__event ' ] = $ event ;
7092 }
71- $ response ['__meta ' ] = $ this ->meta ();
7293
73- yield $ this ->responseClass ::from ($ response );
94+ $ attributes ['__meta ' ] = $ this ->meta ();
95+
96+ $ streamEvent = $ this ->responseClass ::from ($ attributes );
97+
98+ $ event = null ;
99+
100+ unset($ attributes );
101+
102+ yield $ streamEvent ;
103+
104+ unset($ streamEvent );
74105 }
75106 }
76107
77108 /**
78109 * Read a line from the stream.
79110 */
80- private function readLine (StreamInterface $ stream ): string
111+ private function readLine (StreamInterface $ stream ): ? string
81112 {
82- $ buffer = '' ;
113+ $ newLinePosition = strpos ($ this ->lineBuffer , "\n" );
114+
115+ if ($ newLinePosition !== false ) {
116+ $ lineLength = $ newLinePosition + 1 ;
117+
118+ if ($ lineLength === strlen ($ this ->lineBuffer )) {
119+ $ line = $ this ->lineBuffer ;
120+ $ this ->lineBuffer = '' ;
121+
122+ return $ line ;
123+ }
124+
125+ $ line = substr ($ this ->lineBuffer , 0 , $ lineLength );
126+ $ this ->lineBuffer = substr ($ this ->lineBuffer , $ lineLength );
127+
128+ return $ line ;
129+ }
83130
84131 while (! $ stream ->eof ()) {
85- if ('' === ($ byte = $ stream ->read (1 ))) {
86- return $ buffer ;
132+ $ chunk = $ stream ->read (self ::STREAM_READ_SIZE );
133+
134+ if ($ chunk === '' ) {
135+ continue ;
87136 }
88- $ buffer .= $ byte ;
89- if ($ byte === "\n" ) {
90- break ;
137+
138+ // Split the fresh chunk before appending it to avoid copying a large buffered line.
139+ $ newLinePosition = strpos ($ chunk , "\n" );
140+
141+ if ($ newLinePosition === false ) {
142+ $ this ->lineBuffer .= $ chunk ;
143+
144+ continue ;
145+ }
146+
147+ $ lineLength = $ newLinePosition + 1 ;
148+ $ line = $ this ->lineBuffer ;
149+ $ this ->lineBuffer = substr ($ chunk , $ lineLength );
150+
151+ if ($ lineLength === strlen ($ chunk )) {
152+ $ line .= $ chunk ;
153+ } else {
154+ $ line .= substr ($ chunk , 0 , $ lineLength );
91155 }
156+
157+ return $ line ;
92158 }
93159
94- return $ buffer ;
160+ if ($ this ->lineBuffer === '' ) {
161+ return null ;
162+ }
163+
164+ $ line = $ this ->lineBuffer ;
165+ $ this ->lineBuffer = '' ;
166+
167+ return $ line ;
95168 }
96169
97170 public function meta (): MetaInformation
0 commit comments