Skip to content

Commit 2473951

Browse files
Add support for alternative line endings (fix problem with sse-starlette) (#45)
* Remove global eventType property * Refactor string trim and startsWith functions * Rename lastIndexProcessed to _lastIndexProcessed * Add support for alternative line endings * Simplify event id parsing * Refactor line ending detection * Refactor detectNewLineChar. Throw an error when line ending not detected. Add lineEndingCharacter option. * Display warning when line-ending character not detected. Bring back 'id' handler that resets Last-Event-ID * Improve setting and resetting of lastEventId * Format code --------- Co-authored-by: Wojciech Król <wk20981@gmail.com>
1 parent 6cda129 commit 2473951

File tree

3 files changed

+55
-21
lines changed

3 files changed

+55
-21
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ const options: EventSourceOptions = {
195195
body: undefined, // Your request body sent on connection. Default: undefined
196196
debug: false, // Show console.debug messages for debugging purpose. Default: false
197197
pollingInterval: 5000, // Time (ms) between reconnections. If set to 0, reconnections will be disabled. Default: 5000
198+
lineEndingCharacter: null // Character(s) used to represent line endings in received data. Common values: '\n' for LF (Unix/Linux), '\r\n' for CRLF (Windows), '\r' for CR (older Mac). Default: null (Automatically detect from event)
198199
}
199200
```
200201

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export interface EventSourceOptions {
4949
body?: any;
5050
debug?: boolean;
5151
pollingInterval?: number;
52+
lineEndingCharacter?: string;
5253
}
5354

5455
type BuiltInEventMap = {

src/EventSource.js

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ class EventSource {
1212
OPEN = 1;
1313
CLOSED = 2;
1414

15+
CRLF = '\r\n';
16+
LF = '\n';
17+
CR = '\r';
18+
1519
constructor(url, options = {}) {
1620
this.lastEventId = null;
17-
this.lastIndexProcessed = 0;
18-
this.eventType = undefined;
1921
this.status = this.CONNECTING;
2022

2123
this.eventHandlers = {
@@ -33,9 +35,11 @@ class EventSource {
3335
this.body = options.body || undefined;
3436
this.debug = options.debug || false;
3537
this.interval = options.pollingInterval ?? 5000;
38+
this.lineEndingCharacter = options.lineEndingCharacter || null;
3639

3740
this._xhr = null;
3841
this._pollTimer = null;
42+
this._lastIndexProcessed = 0;
3943

4044
if (!url || (typeof url !== 'string' && typeof url.toString !== 'function')) {
4145
throw new SyntaxError('[EventSource] Invalid URL argument.');
@@ -61,9 +65,10 @@ class EventSource {
6165

6266
open() {
6367
try {
64-
this.lastIndexProcessed = 0;
6568
this.status = this.CONNECTING;
6669

70+
this._lastIndexProcessed = 0;
71+
6772
this._xhr = new XMLHttpRequest();
6873
this._xhr.open(this.method, this.url, true);
6974

@@ -174,59 +179,86 @@ class EventSource {
174179
}
175180

176181
_handleEvent(response) {
177-
const indexOfDoubleNewline = this._getLastDoubleNewlineIndex(response);
182+
if (this.lineEndingCharacter === null) {
183+
const detectedNewlineChar = this._detectNewlineChar(response);
184+
if (detectedNewlineChar !== null) {
185+
this._logDebug(`[EventSource] Automatically detected lineEndingCharacter: ${JSON.stringify(detectedNewlineChar).slice(1, -1)}`);
186+
this.lineEndingCharacter = detectedNewlineChar;
187+
} else {
188+
console.warn("[EventSource] Unable to identify the line ending character. Ensure your server delivers a standard line ending character: \\r\\n, \\n, \\r, or specify your custom character using the 'lineEndingCharacter' option.");
189+
return;
190+
}
191+
}
178192

179-
if (indexOfDoubleNewline <= this.lastIndexProcessed) {
193+
const indexOfDoubleNewline = this._getLastDoubleNewlineIndex(response);
194+
if (indexOfDoubleNewline <= this._lastIndexProcessed) {
180195
return;
181196
}
182197

183-
const parts = response.substring(this.lastIndexProcessed, indexOfDoubleNewline).split('\n');
184-
this.lastIndexProcessed = indexOfDoubleNewline;
198+
const parts = response.substring(this._lastIndexProcessed, indexOfDoubleNewline).split(this.lineEndingCharacter);
199+
this._lastIndexProcessed = indexOfDoubleNewline;
185200

201+
let type = undefined;
202+
let id = null;
186203
let data = [];
187204
let retry = 0;
188205
let line = '';
189206

190207
for (let i = 0; i < parts.length; i++) {
191-
line = parts[i].replace(/^(\s|\u00A0)+|(\s|\u00A0)+$/g, '');
192-
if (line.indexOf('event') === 0) {
193-
this.eventType = line.replace(/event:?\s*/, '');
194-
} else if (line.indexOf('retry') === 0) {
208+
line = parts[i].trim();
209+
if (line.startsWith('event')) {
210+
type = line.replace(/event:?\s*/, '');
211+
} else if (line.startsWith('retry')) {
195212
retry = parseInt(line.replace(/retry:?\s*/, ''), 10);
196213
if (!isNaN(retry)) {
197214
this.interval = retry;
198215
}
199-
} else if (line.indexOf('data') === 0) {
216+
} else if (line.startsWith('data')) {
200217
data.push(line.replace(/data:?\s*/, ''));
201-
} else if (line.indexOf('id:') === 0) {
202-
this.lastEventId = line.replace(/id:?\s*/, '');
203-
} else if (line.indexOf('id') === 0) {
204-
this.lastEventId = null;
218+
} else if (line.startsWith('id')) {
219+
id = line.replace(/id:?\s*/, '');
220+
if (id !== '') {
221+
this.lastEventId = id;
222+
} else {
223+
this.lastEventId = null;
224+
}
205225
} else if (line === '') {
206226
if (data.length > 0) {
207-
const eventType = this.eventType || 'message'
227+
const eventType = type || 'message';
208228
const event = {
209229
type: eventType,
210-
data: data.join("\n"),
230+
data: data.join('\n'),
211231
url: this.url,
212232
lastEventId: this.lastEventId,
213233
};
214234

215235
this.dispatch(eventType, event);
216236

217237
data = [];
218-
this.eventType = undefined;
238+
type = undefined;
219239
}
220240
}
221241
}
222242
}
223243

244+
_detectNewlineChar(response) {
245+
const supportedLineEndings = [this.CRLF, this.LF, this.CR];
246+
for (const char of supportedLineEndings) {
247+
if (response.includes(char)) {
248+
return char;
249+
}
250+
}
251+
return null;
252+
}
253+
224254
_getLastDoubleNewlineIndex(response) {
225-
const lastIndex = response.lastIndexOf('\n\n');
255+
const doubleLineEndingCharacter = this.lineEndingCharacter + this.lineEndingCharacter;
256+
const lastIndex = response.lastIndexOf(doubleLineEndingCharacter);
226257
if (lastIndex === -1) {
227258
return -1;
228259
}
229-
return lastIndex + 2;
260+
261+
return lastIndex + doubleLineEndingCharacter.length;
230262
}
231263

232264
addEventListener(type, listener) {

0 commit comments

Comments
 (0)