@@ -13,61 +13,6 @@ type ServerSentEvent = {
13
13
raw : string [ ] ;
14
14
} ;
15
15
16
- class SSEDecoder {
17
- private data : string [ ] ;
18
- private event : string | null ;
19
- private chunks : string [ ] ;
20
-
21
- constructor ( ) {
22
- this . event = null ;
23
- this . data = [ ] ;
24
- this . chunks = [ ] ;
25
- }
26
-
27
- decode ( line : string ) {
28
- if ( line . endsWith ( '\r' ) ) {
29
- line = line . substring ( 0 , line . length - 1 ) ;
30
- }
31
-
32
- if ( ! line ) {
33
- // empty line and we didn't previously encounter any messages
34
- if ( ! this . event && ! this . data . length ) return null ;
35
-
36
- const sse : ServerSentEvent = {
37
- event : this . event ,
38
- data : this . data . join ( '\n' ) ,
39
- raw : this . chunks ,
40
- } ;
41
-
42
- this . event = null ;
43
- this . data = [ ] ;
44
- this . chunks = [ ] ;
45
-
46
- return sse ;
47
- }
48
-
49
- this . chunks . push ( line ) ;
50
-
51
- if ( line . startsWith ( ':' ) ) {
52
- return null ;
53
- }
54
-
55
- let [ fieldname , _ , value ] = partition ( line , ':' ) ;
56
-
57
- if ( value . startsWith ( ' ' ) ) {
58
- value = value . substring ( 1 ) ;
59
- }
60
-
61
- if ( fieldname === 'event' ) {
62
- this . event = value ;
63
- } else if ( fieldname === 'data' ) {
64
- this . data . push ( value ) ;
65
- }
66
-
67
- return null ;
68
- }
69
- }
70
-
71
16
export class Stream < Item > implements AsyncIterable < Item > , APIResponse < Stream < Item > > {
72
17
/** @deprecated - please use the async iterator instead. We plan to add additional helper methods shortly. */
73
18
response : Response ;
@@ -93,9 +38,7 @@ export class Stream<Item> implements AsyncIterable<Item>, APIResponse<Stream<Ite
93
38
94
39
const iter = readableStreamAsyncIterable < Bytes > ( this . response . body ) ;
95
40
for await ( const chunk of iter ) {
96
- const text = decodeText ( chunk ) ;
97
-
98
- for ( const line of lineDecoder . decode ( text ) ) {
41
+ for ( const line of lineDecoder . decode ( chunk ) ) {
99
42
const sse = this . decoder . decode ( line ) ;
100
43
if ( sse ) yield sse ;
101
44
}
@@ -143,7 +86,60 @@ export class Stream<Item> implements AsyncIterable<Item>, APIResponse<Stream<Ite
143
86
}
144
87
}
145
88
146
- const NEWLINE_CHARS = '\n\r\x0b\x0c\x1c\x1d\x1e\x85\u2028\u2029' ;
89
+ class SSEDecoder {
90
+ private data : string [ ] ;
91
+ private event : string | null ;
92
+ private chunks : string [ ] ;
93
+
94
+ constructor ( ) {
95
+ this . event = null ;
96
+ this . data = [ ] ;
97
+ this . chunks = [ ] ;
98
+ }
99
+
100
+ decode ( line : string ) {
101
+ if ( line . endsWith ( '\r' ) ) {
102
+ line = line . substring ( 0 , line . length - 1 ) ;
103
+ }
104
+
105
+ if ( ! line ) {
106
+ // empty line and we didn't previously encounter any messages
107
+ if ( ! this . event && ! this . data . length ) return null ;
108
+
109
+ const sse : ServerSentEvent = {
110
+ event : this . event ,
111
+ data : this . data . join ( '\n' ) ,
112
+ raw : this . chunks ,
113
+ } ;
114
+
115
+ this . event = null ;
116
+ this . data = [ ] ;
117
+ this . chunks = [ ] ;
118
+
119
+ return sse ;
120
+ }
121
+
122
+ this . chunks . push ( line ) ;
123
+
124
+ if ( line . startsWith ( ':' ) ) {
125
+ return null ;
126
+ }
127
+
128
+ let [ fieldname , _ , value ] = partition ( line , ':' ) ;
129
+
130
+ if ( value . startsWith ( ' ' ) ) {
131
+ value = value . substring ( 1 ) ;
132
+ }
133
+
134
+ if ( fieldname === 'event' ) {
135
+ this . event = value ;
136
+ } else if ( fieldname === 'data' ) {
137
+ this . data . push ( value ) ;
138
+ }
139
+
140
+ return null ;
141
+ }
142
+ }
147
143
148
144
/**
149
145
* A re-implementation of httpx's `LineDecoder` in Python that handles incrementally
@@ -152,15 +148,22 @@ const NEWLINE_CHARS = '\n\r\x0b\x0c\x1c\x1d\x1e\x85\u2028\u2029';
152
148
* https://github.com/encode/httpx/blob/920333ea98118e9cf617f246905d7b202510941c/httpx/_decoders.py#L258
153
149
*/
154
150
class LineDecoder {
151
+ // prettier-ignore
152
+ static NEWLINE_CHARS = new Set ( [ '\n' , '\r' , '\x0b' , '\x0c' , '\x1c' , '\x1d' , '\x1e' , '\x85' , '\u2028' , '\u2029' ] ) ;
153
+ static NEWLINE_REGEXP = / \r \n | [ \n \r \x0b \x0c \x1c \x1d \x1e \x85 \u2028 \u2029 ] / g;
154
+
155
155
buffer : string [ ] ;
156
156
trailingCR : boolean ;
157
+ textDecoder : any ; // TextDecoder found in browsers; not typed to avoid pulling in either "dom" or "node" types.
157
158
158
159
constructor ( ) {
159
160
this . buffer = [ ] ;
160
161
this . trailingCR = false ;
161
162
}
162
163
163
- decode ( text : string ) : string [ ] {
164
+ decode ( chunk : Bytes ) : string [ ] {
165
+ let text = this . decodeText ( chunk ) ;
166
+
164
167
if ( this . trailingCR ) {
165
168
text = '\r' + text ;
166
169
this . trailingCR = false ;
@@ -174,10 +177,10 @@ class LineDecoder {
174
177
return [ ] ;
175
178
}
176
179
177
- const trailing_newline = NEWLINE_CHARS . includes ( text . slice ( - 1 ) ) ;
178
- let lines = text . split ( / \r \n | [ \n \r \x0b \x0c \x1c \x1d \x1e \x85 \u2028 \u2029 ] / g ) ;
180
+ const trailingNewline = LineDecoder . NEWLINE_CHARS . has ( text [ text . length - 1 ] || '' ) ;
181
+ let lines = text . split ( LineDecoder . NEWLINE_REGEXP ) ;
179
182
180
- if ( lines . length === 1 && ! trailing_newline ) {
183
+ if ( lines . length === 1 && ! trailingNewline ) {
181
184
this . buffer . push ( lines [ 0 ] ! ) ;
182
185
return [ ] ;
183
186
}
@@ -187,13 +190,50 @@ class LineDecoder {
187
190
this . buffer = [ ] ;
188
191
}
189
192
190
- if ( ! trailing_newline ) {
193
+ if ( ! trailingNewline ) {
191
194
this . buffer = [ lines . pop ( ) || '' ] ;
192
195
}
193
196
194
197
return lines ;
195
198
}
196
199
200
+ decodeText ( bytes : Bytes ) : string {
201
+ if ( bytes == null ) return '' ;
202
+ if ( typeof bytes === 'string' ) return bytes ;
203
+
204
+ // Node:
205
+ if ( typeof Buffer !== 'undefined' ) {
206
+ if ( bytes instanceof Buffer ) {
207
+ return bytes . toString ( ) ;
208
+ }
209
+ if ( bytes instanceof Uint8Array ) {
210
+ return Buffer . from ( bytes ) . toString ( ) ;
211
+ }
212
+
213
+ throw new Error (
214
+ `Unexpected: received non-Uint8Array (${ bytes . constructor . name } ) stream chunk in an environment with a global "Buffer" defined, which this library assumes to be Node. Please report this error.` ,
215
+ ) ;
216
+ }
217
+
218
+ // Browser
219
+ if ( typeof TextDecoder !== 'undefined' ) {
220
+ if ( bytes instanceof Uint8Array || bytes instanceof ArrayBuffer ) {
221
+ this . textDecoder ??= new TextDecoder ( 'utf8' ) ;
222
+ return this . textDecoder . decode ( bytes ) ;
223
+ }
224
+
225
+ throw new Error (
226
+ `Unexpected: received non-Uint8Array/ArrayBuffer (${
227
+ ( bytes as any ) . constructor . name
228
+ } ) in a web platform. Please report this error.`,
229
+ ) ;
230
+ }
231
+
232
+ throw new Error (
233
+ `Unexpected: neither Buffer nor TextDecoder are available as globals. Please report this error.` ,
234
+ ) ;
235
+ }
236
+
197
237
flush ( ) : string [ ] {
198
238
if ( ! this . buffer . length && ! this . trailingCR ) {
199
239
return [ ] ;
@@ -215,61 +255,22 @@ function partition(str: string, delimiter: string): [string, string, string] {
215
255
return [ str , '' , '' ] ;
216
256
}
217
257
218
- let _textDecoder ;
219
- function decodeText ( bytes : Bytes ) : string {
220
- if ( bytes == null ) return '' ;
221
- if ( typeof bytes === 'string' ) return bytes ;
222
-
223
- // Node:
224
- if ( typeof Buffer !== 'undefined' ) {
225
- if ( bytes instanceof Buffer ) {
226
- return bytes . toString ( ) ;
227
- }
228
- if ( bytes instanceof Uint8Array ) {
229
- return Buffer . from ( bytes ) . toString ( ) ;
230
- }
231
-
232
- throw new Error ( `Unexpected: received non-Uint8Array (${ bytes . constructor . name } ) in Node.` ) ;
233
- }
234
-
235
- // Browser
236
- if ( typeof TextDecoder !== 'undefined' ) {
237
- if ( bytes instanceof Uint8Array || bytes instanceof ArrayBuffer ) {
238
- _textDecoder ??= new TextDecoder ( 'utf8' ) ;
239
- return _textDecoder . decode ( bytes ) ;
240
- }
241
-
242
- throw new Error (
243
- `Unexpected: received non-Uint8Array/ArrayBuffer (${
244
- ( bytes as any ) . constructor . name
245
- } ) in a web platform.`,
246
- ) ;
247
- }
248
-
249
- throw new Error ( `Unexpected: neither Buffer nor TextDecoder are available as globals.` ) ;
250
- }
251
-
252
258
/**
253
259
* Most browsers don't yet have async iterable support for ReadableStream,
254
260
* and Node has a very different way of reading bytes from its "ReadableStream".
255
261
*
256
262
* This polyfill was pulled from https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1624185965
257
- *
258
- * We make extensive use of "any" here to avoid pulling in either "node" or "dom" types
259
- * to library users' type scopes.
260
263
*/
261
264
function readableStreamAsyncIterable < T > ( stream : any ) : AsyncIterableIterator < T > {
262
- if ( stream [ Symbol . asyncIterator ] ) {
263
- return stream [ Symbol . asyncIterator ] ;
264
- }
265
+ if ( stream [ Symbol . asyncIterator ] ) return stream [ Symbol . asyncIterator ] ;
265
266
266
267
const reader = stream . getReader ( ) ;
267
-
268
268
return {
269
269
next ( ) {
270
270
return reader . read ( ) ;
271
271
} ,
272
272
async return ( ) {
273
+ reader . cancel ( ) ;
273
274
reader . releaseLock ( ) ;
274
275
return { done : true , value : undefined } ;
275
276
} ,
0 commit comments