Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/util/vs/base/common/sseParser.ts
13405 views
1
//!!! DO NOT modify, this file was COPIED from 'microsoft/vscode'
2
3
/*---------------------------------------------------------------------------------------------
4
* Copyright (c) Microsoft Corporation. All rights reserved.
5
* Licensed under the MIT License. See License.txt in the project root for license information.
6
*--------------------------------------------------------------------------------------------*/
7
8
/**
9
* Parser for Server-Sent Events (SSE) streams according to the HTML specification.
10
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
11
*/
12
13
/**
14
* Represents an event dispatched from an SSE stream.
15
*/
16
export interface ISSEEvent {
17
/**
18
* The event type. If not specified, the type is "message".
19
*/
20
type: string;
21
22
/**
23
* The event data.
24
*/
25
data: string;
26
27
/**
28
* The last event ID, used for reconnection.
29
*/
30
id?: string;
31
32
/**
33
* Reconnection time in milliseconds.
34
*/
35
retry?: number;
36
}
37
38
/**
39
* Callback function type for event dispatch.
40
*/
41
export type SSEEventHandler = (event: ISSEEvent) => void;
42
43
const enum Chr {
44
CR = 13, // '\r'
45
LF = 10, // '\n'
46
COLON = 58, // ':'
47
SPACE = 32, // ' '
48
}
49
50
/**
51
* Parser for Server-Sent Events (SSE) streams.
52
*/
53
export class SSEParser {
54
private dataBuffer = '';
55
private eventTypeBuffer = '';
56
private currentEventId?: string;
57
private lastEventIdBuffer?: string;
58
private reconnectionTime?: number;
59
private buffer: Uint8Array[] = [];
60
private endedOnCR = false;
61
private readonly onEventHandler: SSEEventHandler;
62
private readonly decoder: TextDecoder;
63
/**
64
* Creates a new SSE parser.
65
* @param onEvent The callback to invoke when an event is dispatched.
66
*/
67
constructor(onEvent: SSEEventHandler) {
68
this.onEventHandler = onEvent;
69
this.decoder = new TextDecoder('utf-8');
70
}
71
72
/**
73
* Gets the last event ID received by this parser.
74
*/
75
public getLastEventId(): string | undefined {
76
return this.lastEventIdBuffer;
77
}
78
/**
79
* Gets the reconnection time in milliseconds, if one was specified by the server.
80
*/
81
public getReconnectionTime(): number | undefined {
82
return this.reconnectionTime;
83
}
84
85
/**
86
* Feeds a chunk of the SSE stream to the parser.
87
* @param chunk The chunk to parse as a Uint8Array of UTF-8 encoded data.
88
*/
89
public feed(chunk: Uint8Array): void {
90
if (chunk.length === 0) {
91
return;
92
}
93
94
let offset = 0;
95
96
// If the data stream was bifurcated between a CR and LF, avoid processing the CR as an extra newline
97
if (this.endedOnCR && chunk[0] === Chr.LF) {
98
offset++;
99
}
100
this.endedOnCR = false;
101
102
// Process complete lines from the buffer
103
while (offset < chunk.length) {
104
const indexCR = chunk.indexOf(Chr.CR, offset);
105
const indexLF = chunk.indexOf(Chr.LF, offset);
106
const index = indexCR === -1 ? indexLF : (indexLF === -1 ? indexCR : Math.min(indexCR, indexLF));
107
if (index === -1) {
108
break;
109
}
110
111
let str = '';
112
for (const buf of this.buffer) {
113
str += this.decoder.decode(buf, { stream: true });
114
}
115
str += this.decoder.decode(chunk.subarray(offset, index));
116
this.processLine(str);
117
118
this.buffer.length = 0;
119
offset = index + (chunk[index] === Chr.CR && chunk[index + 1] === Chr.LF ? 2 : 1);
120
}
121
122
123
if (offset < chunk.length) {
124
this.buffer.push(chunk.subarray(offset));
125
} else {
126
this.endedOnCR = chunk[chunk.length - 1] === Chr.CR;
127
}
128
}
129
/**
130
* Processes a single line from the SSE stream.
131
*/
132
private processLine(line: string): void {
133
if (!line.length) {
134
this.dispatchEvent();
135
return;
136
}
137
138
if (line.startsWith(':')) {
139
return;
140
}
141
142
// Parse the field name and value
143
let field: string;
144
let value: string;
145
146
const colonIndex = line.indexOf(':');
147
if (colonIndex === -1) {
148
// Line with no colon - the entire line is the field name, value is empty
149
field = line;
150
value = '';
151
} else {
152
// Line with a colon - split into field name and value
153
field = line.substring(0, colonIndex);
154
value = line.substring(colonIndex + 1);
155
156
// If value starts with a space, remove it
157
if (value.startsWith(' ')) {
158
value = value.substring(1);
159
}
160
}
161
162
this.processField(field, value);
163
}
164
/**
165
* Processes a field with the given name and value.
166
*/
167
private processField(field: string, value: string): void {
168
switch (field) {
169
case 'event':
170
this.eventTypeBuffer = value;
171
break;
172
173
case 'data':
174
// Append the value to the data buffer, followed by a newline
175
this.dataBuffer += value;
176
this.dataBuffer += '\n';
177
break;
178
179
case 'id':
180
// If the field value doesn't contain NULL, set the last event ID buffer
181
if (!value.includes('\0')) {
182
this.currentEventId = this.lastEventIdBuffer = value;
183
} else {
184
this.currentEventId = undefined;
185
}
186
break;
187
188
case 'retry':
189
// If the field value consists only of ASCII digits, set the reconnection time
190
if (/^\d+$/.test(value)) {
191
this.reconnectionTime = parseInt(value, 10);
192
}
193
break;
194
195
// Ignore any other fields
196
}
197
}
198
/**
199
* Dispatches the event based on the current buffer states.
200
*/
201
private dispatchEvent(): void {
202
// If the data buffer is empty, reset the buffers and return
203
if (this.dataBuffer === '') {
204
this.dataBuffer = '';
205
this.eventTypeBuffer = '';
206
return;
207
}
208
209
// If the data buffer's last character is a newline, remove it
210
if (this.dataBuffer.endsWith('\n')) {
211
this.dataBuffer = this.dataBuffer.substring(0, this.dataBuffer.length - 1);
212
}
213
214
// Create and dispatch the event
215
const event: ISSEEvent = {
216
type: this.eventTypeBuffer || 'message',
217
data: this.dataBuffer,
218
};
219
220
// Add optional fields if they exist
221
if (this.currentEventId !== undefined) {
222
event.id = this.currentEventId;
223
}
224
225
if (this.reconnectionTime !== undefined) {
226
event.retry = this.reconnectionTime;
227
}
228
229
// Dispatch the event
230
this.onEventHandler(event);
231
232
// Reset the data and event type buffers
233
this.reset();
234
}
235
236
/**
237
* Resets the parser state.
238
*/
239
public reset(): void {
240
this.dataBuffer = '';
241
this.eventTypeBuffer = '';
242
this.currentEventId = undefined;
243
// Note: lastEventIdBuffer is not reset as it's used for reconnection
244
}
245
}
246
247
248
249