Path: blob/main/extensions/copilot/src/util/vs/base/common/sseParser.ts
13405 views
//!!! DO NOT modify, this file was COPIED from 'microsoft/vscode'12/*---------------------------------------------------------------------------------------------3* Copyright (c) Microsoft Corporation. All rights reserved.4* Licensed under the MIT License. See License.txt in the project root for license information.5*--------------------------------------------------------------------------------------------*/67/**8* Parser for Server-Sent Events (SSE) streams according to the HTML specification.9* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation10*/1112/**13* Represents an event dispatched from an SSE stream.14*/15export interface ISSEEvent {16/**17* The event type. If not specified, the type is "message".18*/19type: string;2021/**22* The event data.23*/24data: string;2526/**27* The last event ID, used for reconnection.28*/29id?: string;3031/**32* Reconnection time in milliseconds.33*/34retry?: number;35}3637/**38* Callback function type for event dispatch.39*/40export type SSEEventHandler = (event: ISSEEvent) => void;4142const enum Chr {43CR = 13, // '\r'44LF = 10, // '\n'45COLON = 58, // ':'46SPACE = 32, // ' '47}4849/**50* Parser for Server-Sent Events (SSE) streams.51*/52export class SSEParser {53private dataBuffer = '';54private eventTypeBuffer = '';55private currentEventId?: string;56private lastEventIdBuffer?: string;57private reconnectionTime?: number;58private buffer: Uint8Array[] = [];59private endedOnCR = false;60private readonly onEventHandler: SSEEventHandler;61private readonly decoder: TextDecoder;62/**63* Creates a new SSE parser.64* @param onEvent The callback to invoke when an event is dispatched.65*/66constructor(onEvent: SSEEventHandler) {67this.onEventHandler = onEvent;68this.decoder = new TextDecoder('utf-8');69}7071/**72* Gets the last event ID received by this parser.73*/74public getLastEventId(): string | undefined {75return this.lastEventIdBuffer;76}77/**78* Gets the reconnection time in milliseconds, if one was specified by the server.79*/80public getReconnectionTime(): number | undefined {81return this.reconnectionTime;82}8384/**85* Feeds a chunk of the SSE stream to the parser.86* @param chunk The chunk to parse as a Uint8Array of UTF-8 encoded data.87*/88public feed(chunk: Uint8Array): void {89if (chunk.length === 0) {90return;91}9293let offset = 0;9495// If the data stream was bifurcated between a CR and LF, avoid processing the CR as an extra newline96if (this.endedOnCR && chunk[0] === Chr.LF) {97offset++;98}99this.endedOnCR = false;100101// Process complete lines from the buffer102while (offset < chunk.length) {103const indexCR = chunk.indexOf(Chr.CR, offset);104const indexLF = chunk.indexOf(Chr.LF, offset);105const index = indexCR === -1 ? indexLF : (indexLF === -1 ? indexCR : Math.min(indexCR, indexLF));106if (index === -1) {107break;108}109110let str = '';111for (const buf of this.buffer) {112str += this.decoder.decode(buf, { stream: true });113}114str += this.decoder.decode(chunk.subarray(offset, index));115this.processLine(str);116117this.buffer.length = 0;118offset = index + (chunk[index] === Chr.CR && chunk[index + 1] === Chr.LF ? 2 : 1);119}120121122if (offset < chunk.length) {123this.buffer.push(chunk.subarray(offset));124} else {125this.endedOnCR = chunk[chunk.length - 1] === Chr.CR;126}127}128/**129* Processes a single line from the SSE stream.130*/131private processLine(line: string): void {132if (!line.length) {133this.dispatchEvent();134return;135}136137if (line.startsWith(':')) {138return;139}140141// Parse the field name and value142let field: string;143let value: string;144145const colonIndex = line.indexOf(':');146if (colonIndex === -1) {147// Line with no colon - the entire line is the field name, value is empty148field = line;149value = '';150} else {151// Line with a colon - split into field name and value152field = line.substring(0, colonIndex);153value = line.substring(colonIndex + 1);154155// If value starts with a space, remove it156if (value.startsWith(' ')) {157value = value.substring(1);158}159}160161this.processField(field, value);162}163/**164* Processes a field with the given name and value.165*/166private processField(field: string, value: string): void {167switch (field) {168case 'event':169this.eventTypeBuffer = value;170break;171172case 'data':173// Append the value to the data buffer, followed by a newline174this.dataBuffer += value;175this.dataBuffer += '\n';176break;177178case 'id':179// If the field value doesn't contain NULL, set the last event ID buffer180if (!value.includes('\0')) {181this.currentEventId = this.lastEventIdBuffer = value;182} else {183this.currentEventId = undefined;184}185break;186187case 'retry':188// If the field value consists only of ASCII digits, set the reconnection time189if (/^\d+$/.test(value)) {190this.reconnectionTime = parseInt(value, 10);191}192break;193194// Ignore any other fields195}196}197/**198* Dispatches the event based on the current buffer states.199*/200private dispatchEvent(): void {201// If the data buffer is empty, reset the buffers and return202if (this.dataBuffer === '') {203this.dataBuffer = '';204this.eventTypeBuffer = '';205return;206}207208// If the data buffer's last character is a newline, remove it209if (this.dataBuffer.endsWith('\n')) {210this.dataBuffer = this.dataBuffer.substring(0, this.dataBuffer.length - 1);211}212213// Create and dispatch the event214const event: ISSEEvent = {215type: this.eventTypeBuffer || 'message',216data: this.dataBuffer,217};218219// Add optional fields if they exist220if (this.currentEventId !== undefined) {221event.id = this.currentEventId;222}223224if (this.reconnectionTime !== undefined) {225event.retry = this.reconnectionTime;226}227228// Dispatch the event229this.onEventHandler(event);230231// Reset the data and event type buffers232this.reset();233}234235/**236* Resets the parser state.237*/238public reset(): void {239this.dataBuffer = '';240this.eventTypeBuffer = '';241this.currentEventId = undefined;242// Note: lastEventIdBuffer is not reset as it's used for reconnection243}244}245246247248249