* Stateful SSE event decoder. Feed lines one at a time via decode(). * Returns a ServerSentEvent on blank lines (dispatch), null otherwise. * * Per WHATWG spec §9.2.6: * - lastEventId persists across dispatches * - retry persists across dispatches (global reconnection setting) * - eventType re
| 188 | * - retry field must be ASCII digits only |
| 189 | */ |
| 190 | class SSEDecoder { |
| 191 | private data: string[] = []; |
| 192 | private eventType = ''; |
| 193 | private lastEventId: string | null = null; |
| 194 | private retry: number | null = null; |
| 195 | |
| 196 | decode(line: string): ServerSentEvent | null { |
| 197 | // Blank line → dispatch event or reset |
| 198 | if (line === '') { |
| 199 | return this.dispatch(); |
| 200 | } |
| 201 | |
| 202 | // Comment line (starts with ':') |
| 203 | if (line[0] === ':') { |
| 204 | return null; |
| 205 | } |
| 206 | |
| 207 | // Parse field:value |
| 208 | const colonIdx = line.indexOf(':'); |
| 209 | let field: string; |
| 210 | let value: string; |
| 211 | |
| 212 | if (colonIdx === -1) { |
| 213 | // No colon → field is entire line, value is empty |
| 214 | field = line; |
| 215 | value = ''; |
| 216 | } else { |
| 217 | field = line.slice(0, colonIdx); |
| 218 | value = line.slice(colonIdx + 1); |
| 219 | // Strip exactly ONE leading space (if present) |
| 220 | if (value[0] === ' ') { |
| 221 | value = value.slice(1); |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | switch (field) { |
| 226 | case 'data': |
| 227 | this.data.push(value); |
| 228 | break; |
| 229 | case 'event': |
| 230 | this.eventType = value; |
| 231 | break; |
| 232 | case 'id': |
| 233 | // Ignore if value contains NULL character (security: WHATWG spec) |
| 234 | if (!value.includes('\0')) { |
| 235 | this.lastEventId = value; |
| 236 | } |
| 237 | break; |
| 238 | case 'retry': |
| 239 | // Must consist of ASCII digits only (no negatives, no whitespace) |
| 240 | if (/^[0-9]+$/.test(value)) { |
| 241 | this.retry = parseInt(value, 10); |
| 242 | } |
| 243 | break; |
| 244 | // Unknown fields: ignore silently |
| 245 | } |
| 246 | |
| 247 | return null; |
nothing calls this directly
no outgoing calls
no test coverage detected