|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import type {EventSourceParseCallback, EventSourceParser} from './types.js' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function createParser(onParse: EventSourceParseCallback): EventSourceParser { |
|
|
|
let isFirstChunk: boolean |
|
let buffer: string |
|
let startingPosition: number |
|
let startingFieldLength: number |
|
|
|
|
|
let eventId: string | undefined |
|
let eventName: string | undefined |
|
let data: string |
|
|
|
reset() |
|
return {feed, reset} |
|
|
|
function reset(): void { |
|
isFirstChunk = true |
|
buffer = '' |
|
startingPosition = 0 |
|
startingFieldLength = -1 |
|
|
|
eventId = undefined |
|
eventName = undefined |
|
data = '' |
|
} |
|
|
|
function feed(chunk: string): void { |
|
buffer = buffer ? buffer + chunk : chunk |
|
|
|
|
|
|
|
|
|
if (isFirstChunk && hasBom(buffer)) { |
|
buffer = buffer.slice(BOM.length) |
|
} |
|
|
|
isFirstChunk = false |
|
|
|
|
|
const length = buffer.length |
|
let position = 0 |
|
let discardTrailingNewline = false |
|
|
|
|
|
while (position < length) { |
|
|
|
|
|
|
|
|
|
|
|
if (discardTrailingNewline) { |
|
if (buffer[position] === '\n') { |
|
++position |
|
} |
|
discardTrailingNewline = false |
|
} |
|
|
|
let lineLength = -1 |
|
let fieldLength = startingFieldLength |
|
let character: string |
|
|
|
for (let index = startingPosition; lineLength < 0 && index < length; ++index) { |
|
character = buffer[index] |
|
if (character === ':' && fieldLength < 0) { |
|
fieldLength = index - position |
|
} else if (character === '\r') { |
|
discardTrailingNewline = true |
|
lineLength = index - position |
|
} else if (character === '\n') { |
|
lineLength = index - position |
|
} |
|
} |
|
|
|
if (lineLength < 0) { |
|
startingPosition = length - position |
|
startingFieldLength = fieldLength |
|
break |
|
} else { |
|
startingPosition = 0 |
|
startingFieldLength = -1 |
|
} |
|
|
|
parseEventStreamLine(buffer, position, fieldLength, lineLength) |
|
|
|
position += lineLength + 1 |
|
} |
|
|
|
if (position === length) { |
|
|
|
buffer = '' |
|
} else if (position > 0) { |
|
|
|
|
|
buffer = buffer.slice(position) |
|
} |
|
} |
|
|
|
function parseEventStreamLine( |
|
lineBuffer: string, |
|
index: number, |
|
fieldLength: number, |
|
lineLength: number, |
|
) { |
|
if (lineLength === 0) { |
|
|
|
if (data.length > 0) { |
|
onParse({ |
|
type: 'event', |
|
id: eventId, |
|
event: eventName || undefined, |
|
data: data.slice(0, -1), |
|
}) |
|
|
|
data = '' |
|
eventId = undefined |
|
} |
|
eventName = undefined |
|
return |
|
} |
|
|
|
const noValue = fieldLength < 0 |
|
const field = lineBuffer.slice(index, index + (noValue ? lineLength : fieldLength)) |
|
let step = 0 |
|
|
|
if (noValue) { |
|
step = lineLength |
|
} else if (lineBuffer[index + fieldLength + 1] === ' ') { |
|
step = fieldLength + 2 |
|
} else { |
|
step = fieldLength + 1 |
|
} |
|
|
|
const position = index + step |
|
const valueLength = lineLength - step |
|
const value = lineBuffer.slice(position, position + valueLength).toString() |
|
|
|
if (field === 'data') { |
|
data += value ? `${value}\n` : '\n' |
|
} else if (field === 'event') { |
|
eventName = value |
|
} else if (field === 'id' && !value.includes('\u0000')) { |
|
eventId = value |
|
} else if (field === 'retry') { |
|
const retry = parseInt(value, 10) |
|
if (!Number.isNaN(retry)) { |
|
onParse({type: 'reconnect-interval', value: retry}) |
|
} |
|
} |
|
} |
|
} |
|
|
|
const BOM = [239, 187, 191] |
|
|
|
function hasBom(buffer: string) { |
|
return BOM.every((charCode: number, index: number) => buffer.charCodeAt(index) === charCode) |
|
} |
|
|