@huggingface/inference 1.6.2 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/index.d.ts +94 -1
- package/dist/index.js +299 -22
- package/dist/index.mjs +297 -21
- package/package.json +1 -1
- package/src/HfInference.ts +355 -23
- package/src/vendor/fetch-event-source/parse.spec.ts +389 -0
- package/src/vendor/fetch-event-source/parse.ts +216 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { expect, it, describe } from "vitest";
|
|
2
|
+
const fail = (msg: string) => { throw new Error(msg) };
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
This file is a part of fetch-event-source package (as of v2.0.1)
|
|
6
|
+
https://github.com/Azure/fetch-event-source/blob/v2.0.1/src/parse.spec.ts
|
|
7
|
+
|
|
8
|
+
Full package can be used after it is made compatible with nodejs:
|
|
9
|
+
https://github.com/Azure/fetch-event-source/issues/20
|
|
10
|
+
|
|
11
|
+
Below is the fetch-event-source package license:
|
|
12
|
+
|
|
13
|
+
MIT License
|
|
14
|
+
|
|
15
|
+
Copyright (c) Microsoft Corporation.
|
|
16
|
+
|
|
17
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
18
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
19
|
+
in the Software without restriction, including without limitation the rights
|
|
20
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
21
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
22
|
+
furnished to do so, subject to the following conditions:
|
|
23
|
+
|
|
24
|
+
The above copyright notice and this permission notice shall be included in all
|
|
25
|
+
copies or substantial portions of the Software.
|
|
26
|
+
|
|
27
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
28
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
29
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
30
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
31
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
32
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
33
|
+
SOFTWARE
|
|
34
|
+
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import * as parse from './parse';
|
|
38
|
+
|
|
39
|
+
describe('parse', () => {
|
|
40
|
+
const encoder = new TextEncoder();
|
|
41
|
+
const decoder = new TextDecoder();
|
|
42
|
+
|
|
43
|
+
describe('getLines', () => {
|
|
44
|
+
it('single line', () => {
|
|
45
|
+
// arrange:
|
|
46
|
+
let lineNum = 0;
|
|
47
|
+
const next = parse.getLines((line, fieldLength) => {
|
|
48
|
+
++lineNum;
|
|
49
|
+
expect(decoder.decode(line)).toEqual('id: abc');
|
|
50
|
+
expect(fieldLength).toEqual(2);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// act:
|
|
54
|
+
next(encoder.encode('id: abc\n'));
|
|
55
|
+
|
|
56
|
+
// assert:
|
|
57
|
+
expect(lineNum).toBe(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('multiple lines', () => {
|
|
61
|
+
// arrange:
|
|
62
|
+
let lineNum = 0;
|
|
63
|
+
const next = parse.getLines((line, fieldLength) => {
|
|
64
|
+
++lineNum;
|
|
65
|
+
expect(decoder.decode(line)).toEqual(lineNum === 1 ? 'id: abc' : 'data: def');
|
|
66
|
+
expect(fieldLength).toEqual(lineNum === 1 ? 2 : 4);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// act:
|
|
70
|
+
next(encoder.encode('id: abc\n'));
|
|
71
|
+
next(encoder.encode('data: def\n'));
|
|
72
|
+
|
|
73
|
+
// assert:
|
|
74
|
+
expect(lineNum).toBe(2);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('single line split across multiple arrays', () => {
|
|
78
|
+
// arrange:
|
|
79
|
+
let lineNum = 0;
|
|
80
|
+
const next = parse.getLines((line, fieldLength) => {
|
|
81
|
+
++lineNum;
|
|
82
|
+
expect(decoder.decode(line)).toEqual('id: abc');
|
|
83
|
+
expect(fieldLength).toEqual(2);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// act:
|
|
87
|
+
next(encoder.encode('id: a'));
|
|
88
|
+
next(encoder.encode('bc\n'));
|
|
89
|
+
|
|
90
|
+
// assert:
|
|
91
|
+
expect(lineNum).toBe(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('multiple lines split across multiple arrays', () => {
|
|
95
|
+
// arrange:
|
|
96
|
+
let lineNum = 0;
|
|
97
|
+
const next = parse.getLines((line, fieldLength) => {
|
|
98
|
+
++lineNum;
|
|
99
|
+
expect(decoder.decode(line)).toEqual(lineNum === 1 ? 'id: abc' : 'data: def');
|
|
100
|
+
expect(fieldLength).toEqual(lineNum === 1 ? 2 : 4);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// act:
|
|
104
|
+
next(encoder.encode('id: ab'));
|
|
105
|
+
next(encoder.encode('c\nda'));
|
|
106
|
+
next(encoder.encode('ta: def\n'));
|
|
107
|
+
|
|
108
|
+
// assert:
|
|
109
|
+
expect(lineNum).toBe(2);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('new line', () => {
|
|
113
|
+
// arrange:
|
|
114
|
+
let lineNum = 0;
|
|
115
|
+
const next = parse.getLines((line, fieldLength) => {
|
|
116
|
+
++lineNum;
|
|
117
|
+
expect(decoder.decode(line)).toEqual('');
|
|
118
|
+
expect(fieldLength).toEqual(-1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// act:
|
|
122
|
+
next(encoder.encode('\n'));
|
|
123
|
+
|
|
124
|
+
// assert:
|
|
125
|
+
expect(lineNum).toBe(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('comment line', () => {
|
|
129
|
+
// arrange:
|
|
130
|
+
let lineNum = 0;
|
|
131
|
+
const next = parse.getLines((line, fieldLength) => {
|
|
132
|
+
++lineNum;
|
|
133
|
+
expect(decoder.decode(line)).toEqual(': this is a comment');
|
|
134
|
+
expect(fieldLength).toEqual(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// act:
|
|
138
|
+
next(encoder.encode(': this is a comment\n'));
|
|
139
|
+
|
|
140
|
+
// assert:
|
|
141
|
+
expect(lineNum).toBe(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('line with no field', () => {
|
|
145
|
+
// arrange:
|
|
146
|
+
let lineNum = 0;
|
|
147
|
+
const next = parse.getLines((line, fieldLength) => {
|
|
148
|
+
++lineNum;
|
|
149
|
+
expect(decoder.decode(line)).toEqual('this is an invalid line');
|
|
150
|
+
expect(fieldLength).toEqual(-1);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// act:
|
|
154
|
+
next(encoder.encode('this is an invalid line\n'));
|
|
155
|
+
|
|
156
|
+
// assert:
|
|
157
|
+
expect(lineNum).toBe(1);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('line with multiple colons', () => {
|
|
161
|
+
// arrange:
|
|
162
|
+
let lineNum = 0;
|
|
163
|
+
const next = parse.getLines((line, fieldLength) => {
|
|
164
|
+
++lineNum;
|
|
165
|
+
expect(decoder.decode(line)).toEqual('id: abc: def');
|
|
166
|
+
expect(fieldLength).toEqual(2);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// act:
|
|
170
|
+
next(encoder.encode('id: abc: def\n'));
|
|
171
|
+
|
|
172
|
+
// assert:
|
|
173
|
+
expect(lineNum).toBe(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('single byte array with multiple lines separated by \\n', () => {
|
|
177
|
+
// arrange:
|
|
178
|
+
let lineNum = 0;
|
|
179
|
+
const next = parse.getLines((line, fieldLength) => {
|
|
180
|
+
++lineNum;
|
|
181
|
+
expect(decoder.decode(line)).toEqual(lineNum === 1 ? 'id: abc' : 'data: def');
|
|
182
|
+
expect(fieldLength).toEqual(lineNum === 1 ? 2 : 4);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// act:
|
|
186
|
+
next(encoder.encode('id: abc\ndata: def\n'));
|
|
187
|
+
|
|
188
|
+
// assert:
|
|
189
|
+
expect(lineNum).toBe(2);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('single byte array with multiple lines separated by \\r', () => {
|
|
193
|
+
// arrange:
|
|
194
|
+
let lineNum = 0;
|
|
195
|
+
const next = parse.getLines((line, fieldLength) => {
|
|
196
|
+
++lineNum;
|
|
197
|
+
expect(decoder.decode(line)).toEqual(lineNum === 1 ? 'id: abc' : 'data: def');
|
|
198
|
+
expect(fieldLength).toEqual(lineNum === 1 ? 2 : 4);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// act:
|
|
202
|
+
next(encoder.encode('id: abc\rdata: def\r'));
|
|
203
|
+
|
|
204
|
+
// assert:
|
|
205
|
+
expect(lineNum).toBe(2);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('single byte array with multiple lines separated by \\r\\n', () => {
|
|
209
|
+
// arrange:
|
|
210
|
+
let lineNum = 0;
|
|
211
|
+
const next = parse.getLines((line, fieldLength) => {
|
|
212
|
+
++lineNum;
|
|
213
|
+
expect(decoder.decode(line)).toEqual(lineNum === 1 ? 'id: abc' : 'data: def');
|
|
214
|
+
expect(fieldLength).toEqual(lineNum === 1 ? 2 : 4);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// act:
|
|
218
|
+
next(encoder.encode('id: abc\r\ndata: def\r\n'));
|
|
219
|
+
|
|
220
|
+
// assert:
|
|
221
|
+
expect(lineNum).toBe(2);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('getMessages', () => {
|
|
226
|
+
it('happy path', () => {
|
|
227
|
+
// arrange:
|
|
228
|
+
let msgNum = 0;
|
|
229
|
+
const next = parse.getMessages(id => {
|
|
230
|
+
expect(id).toEqual('abc');
|
|
231
|
+
}, retry => {
|
|
232
|
+
expect(retry).toEqual(42);
|
|
233
|
+
}, msg => {
|
|
234
|
+
++msgNum;
|
|
235
|
+
expect(msg).toEqual({
|
|
236
|
+
retry: 42,
|
|
237
|
+
id: 'abc',
|
|
238
|
+
event: 'def',
|
|
239
|
+
data: 'ghi'
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// act:
|
|
244
|
+
next(encoder.encode('retry: 42'), 5);
|
|
245
|
+
next(encoder.encode('id: abc'), 2);
|
|
246
|
+
next(encoder.encode('event:def'), 5);
|
|
247
|
+
next(encoder.encode('data:ghi'), 4);
|
|
248
|
+
next(encoder.encode(''), -1);
|
|
249
|
+
|
|
250
|
+
// assert:
|
|
251
|
+
expect(msgNum).toBe(1);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('skip unknown fields', () => {
|
|
255
|
+
let msgNum = 0;
|
|
256
|
+
const next = parse.getMessages(id => {
|
|
257
|
+
expect(id).toEqual('abc');
|
|
258
|
+
}, _retry => {
|
|
259
|
+
fail('retry should not be called');
|
|
260
|
+
}, msg => {
|
|
261
|
+
++msgNum;
|
|
262
|
+
expect(msg).toEqual({
|
|
263
|
+
id: 'abc',
|
|
264
|
+
data: '',
|
|
265
|
+
event: '',
|
|
266
|
+
retry: undefined,
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// act:
|
|
271
|
+
next(encoder.encode('id: abc'), 2);
|
|
272
|
+
next(encoder.encode('foo: null'), 3);
|
|
273
|
+
next(encoder.encode(''), -1);
|
|
274
|
+
|
|
275
|
+
// assert:
|
|
276
|
+
expect(msgNum).toBe(1);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('ignore non-integer retry', () => {
|
|
280
|
+
let msgNum = 0;
|
|
281
|
+
const next = parse.getMessages(_id => {
|
|
282
|
+
fail('id should not be called');
|
|
283
|
+
}, _retry => {
|
|
284
|
+
fail('retry should not be called');
|
|
285
|
+
}, msg => {
|
|
286
|
+
++msgNum;
|
|
287
|
+
expect(msg).toEqual({
|
|
288
|
+
id: '',
|
|
289
|
+
data: '',
|
|
290
|
+
event: '',
|
|
291
|
+
retry: undefined,
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// act:
|
|
296
|
+
next(encoder.encode('retry: def'), 5);
|
|
297
|
+
next(encoder.encode(''), -1);
|
|
298
|
+
|
|
299
|
+
// assert:
|
|
300
|
+
expect(msgNum).toBe(1);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('skip comment-only messages', () => {
|
|
304
|
+
// arrange:
|
|
305
|
+
let msgNum = 0;
|
|
306
|
+
const next = parse.getMessages(id => {
|
|
307
|
+
expect(id).toEqual('123');
|
|
308
|
+
}, _retry => {
|
|
309
|
+
fail('retry should not be called');
|
|
310
|
+
}, msg => {
|
|
311
|
+
++msgNum;
|
|
312
|
+
expect(msg).toEqual({
|
|
313
|
+
retry: undefined,
|
|
314
|
+
id: '123',
|
|
315
|
+
event: 'foo ',
|
|
316
|
+
data: '',
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// act:
|
|
321
|
+
next(encoder.encode('id:123'), 2);
|
|
322
|
+
next(encoder.encode(':'), 0);
|
|
323
|
+
next(encoder.encode(': '), 0);
|
|
324
|
+
next(encoder.encode('event: foo '), 5);
|
|
325
|
+
next(encoder.encode(''), -1);
|
|
326
|
+
|
|
327
|
+
// assert:
|
|
328
|
+
expect(msgNum).toBe(1);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should append data split across multiple lines', () => {
|
|
332
|
+
// arrange:
|
|
333
|
+
let msgNum = 0;
|
|
334
|
+
const next = parse.getMessages(_id => {
|
|
335
|
+
fail('id should not be called');
|
|
336
|
+
}, _retry => {
|
|
337
|
+
fail('retry should not be called');
|
|
338
|
+
}, msg => {
|
|
339
|
+
++msgNum;
|
|
340
|
+
expect(msg).toEqual({
|
|
341
|
+
data: 'YHOO\n+2\n\n10',
|
|
342
|
+
id: '',
|
|
343
|
+
event: '',
|
|
344
|
+
retry: undefined,
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// act:
|
|
349
|
+
next(encoder.encode('data:YHOO'), 4);
|
|
350
|
+
next(encoder.encode('data: +2'), 4);
|
|
351
|
+
next(encoder.encode('data'), 4);
|
|
352
|
+
next(encoder.encode('data: 10'), 4);
|
|
353
|
+
next(encoder.encode(''), -1);
|
|
354
|
+
|
|
355
|
+
// assert:
|
|
356
|
+
expect(msgNum).toBe(1);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('should reset id if sent multiple times', () => {
|
|
360
|
+
// arrange:
|
|
361
|
+
const expectedIds = ['foo', ''];
|
|
362
|
+
let idsIdx = 0;
|
|
363
|
+
let msgNum = 0;
|
|
364
|
+
const next = parse.getMessages(id => {
|
|
365
|
+
expect(id).toEqual(expectedIds[idsIdx]);
|
|
366
|
+
++idsIdx;
|
|
367
|
+
}, _retry => {
|
|
368
|
+
fail('retry should not be called');
|
|
369
|
+
}, msg => {
|
|
370
|
+
++msgNum;
|
|
371
|
+
expect(msg).toEqual({
|
|
372
|
+
data: '',
|
|
373
|
+
id: '',
|
|
374
|
+
event: '',
|
|
375
|
+
retry: undefined,
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// act:
|
|
380
|
+
next(encoder.encode('id: foo'), 2);
|
|
381
|
+
next(encoder.encode('id'), 2);
|
|
382
|
+
next(encoder.encode(''), -1);
|
|
383
|
+
|
|
384
|
+
// assert:
|
|
385
|
+
expect(idsIdx).toBe(2);
|
|
386
|
+
expect(msgNum).toBe(1);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
This file is a part of fetch-event-source package (as of v2.0.1)
|
|
3
|
+
https://github.com/Azure/fetch-event-source/blob/v2.0.1/src/parse.ts
|
|
4
|
+
|
|
5
|
+
Full package can be used after it is made compatible with nodejs:
|
|
6
|
+
https://github.com/Azure/fetch-event-source/issues/20
|
|
7
|
+
|
|
8
|
+
Below is the fetch-event-source package license:
|
|
9
|
+
|
|
10
|
+
MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) Microsoft Corporation.
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE
|
|
31
|
+
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Represents a message sent in an event stream
|
|
36
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format
|
|
37
|
+
*/
|
|
38
|
+
export interface EventSourceMessage {
|
|
39
|
+
/** The event ID to set the EventSource object's last event ID value. */
|
|
40
|
+
id: string;
|
|
41
|
+
/** A string identifying the type of event described. */
|
|
42
|
+
event: string;
|
|
43
|
+
/** The event data */
|
|
44
|
+
data: string;
|
|
45
|
+
/** The reconnection interval (in milliseconds) to wait before retrying the connection */
|
|
46
|
+
retry?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Converts a ReadableStream into a callback pattern.
|
|
51
|
+
* @param stream The input ReadableStream.
|
|
52
|
+
* @param onChunk A function that will be called on each new byte chunk in the stream.
|
|
53
|
+
* @returns {Promise<void>} A promise that will be resolved when the stream closes.
|
|
54
|
+
*/
|
|
55
|
+
export async function getBytes(stream: ReadableStream<Uint8Array>, onChunk: (arr: Uint8Array) => void) {
|
|
56
|
+
const reader = stream.getReader();
|
|
57
|
+
let result: ReadableStreamReadResult<Uint8Array>;
|
|
58
|
+
while (!(result = await reader.read()).done) {
|
|
59
|
+
onChunk(result.value);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const enum ControlChars {
|
|
64
|
+
NewLine = 10,
|
|
65
|
+
CarriageReturn = 13,
|
|
66
|
+
Space = 32,
|
|
67
|
+
Colon = 58,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parses arbitary byte chunks into EventSource line buffers.
|
|
72
|
+
* Each line should be of the format "field: value" and ends with \r, \n, or \r\n.
|
|
73
|
+
* @param onLine A function that will be called on each new EventSource line.
|
|
74
|
+
* @returns A function that should be called for each incoming byte chunk.
|
|
75
|
+
*/
|
|
76
|
+
export function getLines(onLine: (line: Uint8Array, fieldLength: number) => void) {
|
|
77
|
+
let buffer: Uint8Array | undefined;
|
|
78
|
+
let position: number; // current read position
|
|
79
|
+
let fieldLength: number; // length of the `field` portion of the line
|
|
80
|
+
let discardTrailingNewline = false;
|
|
81
|
+
|
|
82
|
+
// return a function that can process each incoming byte chunk:
|
|
83
|
+
return function onChunk(arr: Uint8Array) {
|
|
84
|
+
if (buffer === undefined) {
|
|
85
|
+
buffer = arr;
|
|
86
|
+
position = 0;
|
|
87
|
+
fieldLength = -1;
|
|
88
|
+
} else {
|
|
89
|
+
// we're still parsing the old line. Append the new bytes into buffer:
|
|
90
|
+
buffer = concat(buffer, arr);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const bufLength = buffer.length;
|
|
94
|
+
let lineStart = 0; // index where the current line starts
|
|
95
|
+
while (position < bufLength) {
|
|
96
|
+
if (discardTrailingNewline) {
|
|
97
|
+
if (buffer[position] === ControlChars.NewLine) {
|
|
98
|
+
lineStart = ++position; // skip to next char
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
discardTrailingNewline = false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// start looking forward till the end of line:
|
|
105
|
+
let lineEnd = -1; // index of the \r or \n char
|
|
106
|
+
for (; position < bufLength && lineEnd === -1; ++position) {
|
|
107
|
+
switch (buffer[position]) {
|
|
108
|
+
case ControlChars.Colon:
|
|
109
|
+
if (fieldLength === -1) { // first colon in line
|
|
110
|
+
fieldLength = position - lineStart;
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
// @ts-ignore:7029 \r case below should fallthrough to \n:
|
|
114
|
+
case ControlChars.CarriageReturn:
|
|
115
|
+
discardTrailingNewline = true;
|
|
116
|
+
case ControlChars.NewLine:
|
|
117
|
+
lineEnd = position;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (lineEnd === -1) {
|
|
123
|
+
// We reached the end of the buffer but the line hasn't ended.
|
|
124
|
+
// Wait for the next arr and then continue parsing:
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// we've reached the line end, send it out:
|
|
129
|
+
onLine(buffer.subarray(lineStart, lineEnd), fieldLength);
|
|
130
|
+
lineStart = position; // we're now on the next line
|
|
131
|
+
fieldLength = -1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (lineStart === bufLength) {
|
|
135
|
+
buffer = undefined; // we've finished reading it
|
|
136
|
+
} else if (lineStart !== 0) {
|
|
137
|
+
// Create a new view into buffer beginning at lineStart so we don't
|
|
138
|
+
// need to copy over the previous lines when we get the new arr:
|
|
139
|
+
buffer = buffer.subarray(lineStart);
|
|
140
|
+
position -= lineStart;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Parses line buffers into EventSourceMessages.
|
|
147
|
+
* @param onId A function that will be called on each `id` field.
|
|
148
|
+
* @param onRetry A function that will be called on each `retry` field.
|
|
149
|
+
* @param onMessage A function that will be called on each message.
|
|
150
|
+
* @returns A function that should be called for each incoming line buffer.
|
|
151
|
+
*/
|
|
152
|
+
export function getMessages(
|
|
153
|
+
onId: (id: string) => void,
|
|
154
|
+
onRetry: (retry: number) => void,
|
|
155
|
+
onMessage?: (msg: EventSourceMessage) => void
|
|
156
|
+
) {
|
|
157
|
+
let message = newMessage();
|
|
158
|
+
const decoder = new TextDecoder();
|
|
159
|
+
|
|
160
|
+
// return a function that can process each incoming line buffer:
|
|
161
|
+
return function onLine(line: Uint8Array, fieldLength: number) {
|
|
162
|
+
if (line.length === 0) {
|
|
163
|
+
// empty line denotes end of message. Trigger the callback and start a new message:
|
|
164
|
+
onMessage?.(message);
|
|
165
|
+
message = newMessage();
|
|
166
|
+
} else if (fieldLength > 0) { // exclude comments and lines with no values
|
|
167
|
+
// line is of format "<field>:<value>" or "<field>: <value>"
|
|
168
|
+
// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
|
|
169
|
+
const field = decoder.decode(line.subarray(0, fieldLength));
|
|
170
|
+
const valueOffset = fieldLength + (line[fieldLength + 1] === ControlChars.Space ? 2 : 1);
|
|
171
|
+
const value = decoder.decode(line.subarray(valueOffset));
|
|
172
|
+
|
|
173
|
+
switch (field) {
|
|
174
|
+
case 'data':
|
|
175
|
+
// if this message already has data, append the new value to the old.
|
|
176
|
+
// otherwise, just set to the new value:
|
|
177
|
+
message.data = message.data
|
|
178
|
+
? message.data + '\n' + value
|
|
179
|
+
: value; // otherwise,
|
|
180
|
+
break;
|
|
181
|
+
case 'event':
|
|
182
|
+
message.event = value;
|
|
183
|
+
break;
|
|
184
|
+
case 'id':
|
|
185
|
+
onId(message.id = value);
|
|
186
|
+
break;
|
|
187
|
+
case 'retry':
|
|
188
|
+
const retry = parseInt(value, 10);
|
|
189
|
+
if (!isNaN(retry)) { // per spec, ignore non-integers
|
|
190
|
+
onRetry(message.retry = retry);
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function concat(a: Uint8Array, b: Uint8Array) {
|
|
199
|
+
const res = new Uint8Array(a.length + b.length);
|
|
200
|
+
res.set(a);
|
|
201
|
+
res.set(b, a.length);
|
|
202
|
+
return res;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function newMessage(): EventSourceMessage {
|
|
206
|
+
// data, event, and id must be initialized to empty strings:
|
|
207
|
+
// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
|
|
208
|
+
// retry should be initialized to undefined so we return a consistent shape
|
|
209
|
+
// to the js engine all the time: https://mathiasbynens.be/notes/shapes-ics#takeaways
|
|
210
|
+
return {
|
|
211
|
+
data: '',
|
|
212
|
+
event: '',
|
|
213
|
+
id: '',
|
|
214
|
+
retry: undefined,
|
|
215
|
+
};
|
|
216
|
+
}
|