opinionated-machine
Version:
Very opinionated DI framework for fastify, built on top of awilix
225 lines • 6.19 kB
JavaScript
/**
* SSE (Server-Sent Events) parsing utilities.
*
* This module provides utilities for parsing SSE event streams according
* to the W3C Server-Sent Events specification.
*
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html
*
* @module sseParser
*/
/**
* A parsed SSE event.
*
* SSE events consist of optional id, event type, data, and retry fields.
* The data field is always present and contains the event payload as a string.
*
* @example
* ```typescript
* const event: ParsedSSEEvent = {
* id: 'msg-123',
* event: 'message',
* data: '{"text":"Hello, world!"}',
* retry: 3000,
* }
*
* // Parse the JSON data
* const payload = JSON.parse(event.data)
* ```
*/
/**
* Parse a single SSE line and update the event state.
* Returns true if a complete event was found (empty line with data).
*/
function parseSSELine(line, currentEvent, dataLines) {
if (line.startsWith('id:')) {
currentEvent.id = line.slice(3).trim();
}
else if (line.startsWith('event:')) {
currentEvent.event = line.slice(6).trim();
}
else if (line.startsWith('data:')) {
dataLines.push(line.slice(5).trim());
}
else if (line.startsWith('retry:')) {
currentEvent.retry = Number.parseInt(line.slice(6).trim(), 10);
}
else if (line === '' && dataLines.length > 0) {
return true; // Event complete
}
// Comment lines (starting with :) are implicitly ignored
return false;
}
/**
* Parse SSE events from a complete text response.
*
* This function parses a complete SSE response body into individual events.
* SSE events are separated by blank lines, and each event can have multiple fields.
*
* **SSE Format:**
* ```
* id: event-id
* event: event-name
* data: line1
* data: line2
* retry: 3000
*
* ```
*
* **Field Rules:**
* - `id:` - Event ID for Last-Event-ID reconnection
* - `event:` - Event type (defaults to 'message')
* - `data:` - Event payload (multiple data lines are joined with newlines)
* - `retry:` - Reconnection delay in milliseconds
* - Lines starting with `:` are comments and ignored
*
* @param text - Raw SSE text to parse
* @returns Array of parsed events
*
* @example
* ```typescript
* // Parse a simple SSE response
* const text = `event: message
* data: {"text":"hello"}
*
* event: done
* data: {"status":"complete"}
*
* `
* const events = parseSSEEvents(text)
* // events = [
* // { event: 'message', data: '{"text":"hello"}' },
* // { event: 'done', data: '{"status":"complete"}' }
* // ]
* ```
*
* @example
* ```typescript
* // Parse events with IDs (for reconnection)
* const text = `id: 1
* event: update
* data: {"value":42}
*
* id: 2
* event: update
* data: {"value":43}
*
* `
* const events = parseSSEEvents(text)
* // Store last ID for reconnection: events[events.length - 1].id
* ```
*
* @example
* ```typescript
* // Multi-line data
* const text = `event: log
* data: Line 1
* data: Line 2
* data: Line 3
*
* `
* const events = parseSSEEvents(text)
* // events[0].data === "Line 1\nLine 2\nLine 3"
* ```
*/
export function parseSSEEvents(text) {
const events = [];
const lines = text.split('\n');
let currentEvent = {};
let dataLines = [];
for (const line of lines) {
if (parseSSELine(line, currentEvent, dataLines)) {
events.push({
...currentEvent,
data: dataLines.join('\n'),
});
currentEvent = {};
dataLines = [];
}
}
// Handle case where stream doesn't end with double newline
if (dataLines.length > 0) {
events.push({
...currentEvent,
data: dataLines.join('\n'),
});
}
return events;
}
/**
* Parse SSE events incrementally from a buffer.
*
* This function is designed for streaming scenarios where data arrives
* in chunks. It parses complete events and returns any incomplete data
* that should be prepended to the next chunk.
*
* **Usage Pattern:**
* 1. Append new chunk to buffer
* 2. Call parseSSEBuffer(buffer)
* 3. Process the events
* 4. Set buffer = remaining for next iteration
*
* @param buffer - Current buffer containing SSE data
* @returns Object with parsed events and remaining incomplete buffer
*
* @example
* ```typescript
* // Streaming SSE parsing with fetch
* const response = await fetch(url)
* const reader = response.body.getReader()
* const decoder = new TextDecoder()
* let buffer = ''
*
* while (true) {
* const { done, value } = await reader.read()
* if (done) break
*
* buffer += decoder.decode(value, { stream: true })
* const { events, remaining } = parseSSEBuffer(buffer)
* buffer = remaining
*
* for (const event of events) {
* console.log('Received:', event.event, JSON.parse(event.data))
* }
* }
* ```
*
* @example
* ```typescript
* // Node.js readable stream
* let buffer = ''
* stream.on('data', (chunk: Buffer) => {
* buffer += chunk.toString()
* const { events, remaining } = parseSSEBuffer(buffer)
* buffer = remaining
*
* events.forEach(event => emit('sse-event', event))
* })
* ```
*/
export function parseSSEBuffer(buffer) {
const events = [];
const lines = buffer.split('\n');
let currentEvent = {};
let dataLines = [];
let lastCompleteEventEnd = 0;
let currentPosition = 0;
for (const line of lines) {
currentPosition += line.length + 1; // +1 for the \n
if (parseSSELine(line, currentEvent, dataLines)) {
events.push({
...currentEvent,
data: dataLines.join('\n'),
});
currentEvent = {};
dataLines = [];
lastCompleteEventEnd = currentPosition;
}
}
// Return remaining incomplete data
// Preserve any unconsumed content after the last complete event,
// including incomplete events with only id:/event:/retry: lines (not just data: lines)
const remaining = lastCompleteEventEnd < buffer.length ? buffer.slice(lastCompleteEventEnd) : '';
return { events, remaining };
}
//# sourceMappingURL=sseParser.js.map