1
|
|
|
const original = require('original'); |
2
|
|
|
const parse = require('url').parse; |
3
|
|
|
const events = require('events'); |
4
|
|
|
const https = require('https'); |
5
|
|
|
const http = require('http'); |
6
|
|
|
const util = require('util'); |
7
|
|
|
|
8
|
|
|
const httpsOptions = [ |
9
|
|
|
'pfx', 'key', 'passphrase', 'cert', 'ca', 'ciphers', |
10
|
|
|
'rejectUnauthorized', 'secureProtocol', 'servername', |
11
|
|
|
]; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* Creates a new EventSource object |
15
|
|
|
* |
16
|
|
|
* @param {String} url the URL to which to connect |
17
|
|
|
* @param {Object} [eventSourceInitDict] extra init params. See README for details. |
18
|
|
|
* @api public |
19
|
|
|
* */ |
20
|
|
|
function EventSource(url, eventSourceInitDict) { |
21
|
|
|
let readyState = EventSource.CONNECTING; |
22
|
|
|
Object.defineProperty(this, 'readyState', { |
23
|
|
|
get() { |
24
|
|
|
return readyState; |
25
|
|
|
}, |
26
|
|
|
}); |
27
|
|
|
|
28
|
|
|
Object.defineProperty(this, 'url', { |
29
|
|
|
get() { |
30
|
|
|
return url; |
31
|
|
|
}, |
32
|
|
|
}); |
33
|
|
|
|
34
|
|
|
const self = this; |
35
|
|
|
self.reconnectInterval = 60000; |
36
|
|
|
|
37
|
|
|
function onConnectionClosed() { |
38
|
|
|
if (readyState === EventSource.CLOSED) return; |
|
|
|
|
39
|
|
|
readyState = EventSource.CONNECTING; |
40
|
|
|
_emit('error', new Event('error')); |
41
|
|
|
|
42
|
|
|
// The url may have been changed by a temporary |
43
|
|
|
// redirect. If that's the case, revert it now. |
44
|
|
|
if (reconnectUrl) { |
|
|
|
|
45
|
|
|
url = reconnectUrl; |
46
|
|
|
reconnectUrl = null; |
|
|
|
|
47
|
|
|
} |
48
|
|
|
setTimeout(() => { |
49
|
|
|
if (readyState !== EventSource.CONNECTING) { |
50
|
|
|
return; |
51
|
|
|
} |
52
|
|
|
connect(); |
53
|
|
|
}, self.reconnectInterval); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
let req; |
57
|
|
|
let lastEventId = ''; |
58
|
|
|
if (eventSourceInitDict && eventSourceInitDict.headers && eventSourceInitDict.headers['Last-Event-ID']) { |
59
|
|
|
lastEventId = eventSourceInitDict.headers['Last-Event-ID']; |
60
|
|
|
delete eventSourceInitDict.headers['Last-Event-ID']; |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
let discardTrailingNewline = false; |
64
|
|
|
let data = ''; |
65
|
|
|
let eventName = ''; |
66
|
|
|
|
67
|
|
|
let reconnectUrl = null; |
|
|
|
|
68
|
|
|
|
69
|
|
|
function connect() { |
70
|
|
|
const options = parse(url); |
71
|
|
|
let isSecure = options.protocol === 'https:'; |
72
|
|
|
options.headers = { 'Cache-Control': 'no-cache', Accept: 'text/event-stream' }; |
73
|
|
|
if (lastEventId) options.headers['Last-Event-ID'] = lastEventId; |
|
|
|
|
74
|
|
|
if (eventSourceInitDict && eventSourceInitDict.headers) { |
75
|
|
|
for (const i in eventSourceInitDict.headers) { |
76
|
|
|
const header = eventSourceInitDict.headers[i]; |
77
|
|
|
if (header) { |
78
|
|
|
options.headers[i] = header; |
79
|
|
|
} |
80
|
|
|
} |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
// Legacy: this should be specified as `eventSourceInitDict.https.rejectUnauthorized`, |
84
|
|
|
// but for now exists as a backwards-compatibility layer |
85
|
|
|
options.rejectUnauthorized = !(eventSourceInitDict && !eventSourceInitDict.rejectUnauthorized); |
86
|
|
|
|
87
|
|
|
// If specify http proxy, make the request to sent to the proxy server, |
88
|
|
|
// and include the original url in path and Host headers |
89
|
|
|
const useProxy = eventSourceInitDict && eventSourceInitDict.proxy; |
90
|
|
|
if (useProxy) { |
91
|
|
|
const proxy = parse(eventSourceInitDict.proxy); |
92
|
|
|
isSecure = proxy.protocol === 'https:'; |
93
|
|
|
|
94
|
|
|
options.protocol = isSecure ? 'https:' : 'http:'; |
95
|
|
|
options.path = url; |
96
|
|
|
options.headers.Host = options.host; |
97
|
|
|
options.hostname = proxy.hostname; |
98
|
|
|
options.host = proxy.host; |
99
|
|
|
options.port = proxy.port; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
// If https options are specified, merge them into the request options |
103
|
|
|
if (eventSourceInitDict && eventSourceInitDict.https) { |
104
|
|
|
for (const optName in eventSourceInitDict.https) { |
105
|
|
|
if (httpsOptions.indexOf(optName) === -1) { |
106
|
|
|
continue; |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
const option = eventSourceInitDict.https[optName]; |
110
|
|
|
if (option !== undefined) { |
111
|
|
|
options[optName] = option; |
112
|
|
|
} |
113
|
|
|
} |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
// Pass this on to the XHR |
117
|
|
|
if (eventSourceInitDict && eventSourceInitDict.withCredentials !== undefined) { |
118
|
|
|
options.withCredentials = eventSourceInitDict.withCredentials; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
req = (isSecure ? https : http).request(options, (res) => { |
122
|
|
|
// Handle HTTP errors |
123
|
|
|
if (res.statusCode === 500 || res.statusCode === 502 |
124
|
|
|
|| res.statusCode === 503 || res.statusCode === 504) { |
125
|
|
|
_emit('error', new Event('error', { status: res.statusCode })); |
126
|
|
|
onConnectionClosed(); |
127
|
|
|
return; |
|
|
|
|
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
// Handle HTTP redirects |
131
|
|
|
if (res.statusCode === 301 || res.statusCode === 307) { |
132
|
|
|
if (!res.headers.location) { |
133
|
|
|
// Server sent redirect response without Location header. |
134
|
|
|
_emit('error', new Event('error', { status: res.statusCode })); |
135
|
|
|
return; |
|
|
|
|
136
|
|
|
} |
137
|
|
|
if (res.statusCode === 307) reconnectUrl = url; |
|
|
|
|
138
|
|
|
url = res.headers.location; |
139
|
|
|
process.nextTick(connect); |
140
|
|
|
return; |
|
|
|
|
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
if (res.statusCode !== 200) { |
144
|
|
|
_emit('error', new Event('error', { status: res.statusCode })); |
145
|
|
|
return self.close(); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
// protect against multiple connects |
149
|
|
|
// https://github.com/tigertext/eventsource/commit/ca8a6e0ca0db10c23ba7bf2b7f8affaa23d7a265 |
150
|
|
|
if (readyState === EventSource.OPEN) { |
151
|
|
|
return; |
|
|
|
|
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
readyState = EventSource.OPEN; |
155
|
|
|
res.on('close', () => { |
156
|
|
|
res.removeAllListeners('close'); |
157
|
|
|
res.removeAllListeners('end'); |
158
|
|
|
onConnectionClosed(); |
159
|
|
|
}); |
160
|
|
|
|
161
|
|
|
res.on('end', () => { |
162
|
|
|
res.removeAllListeners('close'); |
163
|
|
|
res.removeAllListeners('end'); |
164
|
|
|
onConnectionClosed(); |
165
|
|
|
}); |
166
|
|
|
_emit('open', new Event('open')); |
167
|
|
|
|
168
|
|
|
// text/event-stream parser adapted from webkit's |
169
|
|
|
// Source/WebCore/page/EventSource.cpp |
170
|
|
|
let buf = ''; |
171
|
|
|
res.on('data', (chunk) => { |
172
|
|
|
buf += chunk; |
173
|
|
|
|
174
|
|
|
let pos = 0; |
175
|
|
|
const length = buf.length; |
176
|
|
|
|
177
|
|
|
while (pos < length) { |
178
|
|
|
if (discardTrailingNewline) { |
|
|
|
|
179
|
|
|
if (buf[pos] === '\n') { |
180
|
|
|
++pos; |
181
|
|
|
} |
182
|
|
|
discardTrailingNewline = false; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
let lineLength = -1; |
186
|
|
|
let fieldLength = -1; |
187
|
|
|
var c; |
188
|
|
|
|
189
|
|
|
for (let i = pos; lineLength < 0 && i < length; ++i) { |
190
|
|
|
c = buf[i]; |
191
|
|
|
if (c === ':') { |
192
|
|
|
if (fieldLength < 0) { |
193
|
|
|
fieldLength = i - pos; |
194
|
|
|
} |
195
|
|
|
} else if (c === '\r') { |
196
|
|
|
discardTrailingNewline = true; |
197
|
|
|
lineLength = i - pos; |
198
|
|
|
} else if (c === '\n') { |
199
|
|
|
lineLength = i - pos; |
200
|
|
|
} |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
if (lineLength < 0) { |
204
|
|
|
break; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
parseEventStreamLine(buf, pos, fieldLength, lineLength); |
208
|
|
|
|
209
|
|
|
pos += lineLength + 1; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
if (pos === length) { |
213
|
|
|
buf = ''; |
214
|
|
|
} else if (pos > 0) { |
215
|
|
|
buf = buf.slice(pos); |
216
|
|
|
} |
217
|
|
|
}); |
|
|
|
|
218
|
|
|
}); |
219
|
|
|
|
220
|
|
|
req.on('error', onConnectionClosed); |
221
|
|
|
if (req.setNoDelay) req.setNoDelay(true); |
|
|
|
|
222
|
|
|
req.end(); |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
connect(); |
226
|
|
|
|
227
|
|
|
function _emit() { |
228
|
|
|
if (self.listeners(arguments[0]).length > 0) { |
229
|
|
|
self.emit(...arguments); |
230
|
|
|
} |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
this._close = function () { |
234
|
|
|
if (readyState === EventSource.CLOSED) return; |
|
|
|
|
235
|
|
|
readyState = EventSource.CLOSED; |
236
|
|
|
if (req.abort) req.abort(); |
|
|
|
|
237
|
|
|
if (req.xhr && req.xhr.abort) req.xhr.abort(); |
|
|
|
|
238
|
|
|
}; |
239
|
|
|
|
240
|
|
|
function parseEventStreamLine(buf, pos, fieldLength, lineLength) { |
241
|
|
|
if (lineLength === 0) { |
242
|
|
|
if (data.length > 0) { |
243
|
|
|
const type = eventName || 'message'; |
244
|
|
|
_emit(type, new MessageEvent(type, { |
245
|
|
|
data: data.slice(0, -1), // remove trailing newline |
246
|
|
|
lastEventId, |
247
|
|
|
origin: original(url), |
248
|
|
|
})); |
249
|
|
|
data = ''; |
250
|
|
|
} |
251
|
|
|
eventName = void 0; |
|
|
|
|
252
|
|
|
} else if (fieldLength > 0) { |
253
|
|
|
const noValue = fieldLength < 0; |
254
|
|
|
let step = 0; |
255
|
|
|
const field = buf.slice(pos, pos + (noValue ? lineLength : fieldLength)); |
256
|
|
|
|
257
|
|
|
if (noValue) { |
258
|
|
|
step = lineLength; |
259
|
|
|
} else if (buf[pos + fieldLength + 1] !== ' ') { |
260
|
|
|
step = fieldLength + 1; |
261
|
|
|
} else { |
262
|
|
|
step = fieldLength + 2; |
263
|
|
|
} |
264
|
|
|
pos += step; |
265
|
|
|
|
266
|
|
|
const valueLength = lineLength - step; |
267
|
|
|
const value = buf.slice(pos, pos + valueLength); |
268
|
|
|
|
269
|
|
|
if (field === 'data') { |
270
|
|
|
data += `${value}\n`; |
271
|
|
|
} else if (field === 'event') { |
272
|
|
|
eventName = value; |
273
|
|
|
} else if (field === 'id') { |
274
|
|
|
lastEventId = value; |
275
|
|
|
} else if (field === 'retry') { |
276
|
|
|
const retry = parseInt(value, 10); |
277
|
|
|
if (!Number.isNaN(retry)) { |
278
|
|
|
self.reconnectInterval = retry; |
279
|
|
|
} |
280
|
|
|
} |
281
|
|
|
} |
282
|
|
|
} |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
module.exports = EventSource; |
286
|
|
|
|
287
|
|
|
util.inherits(EventSource, events.EventEmitter); |
288
|
|
|
EventSource.prototype.constructor = EventSource; // make stacktraces readable |
289
|
|
|
|
290
|
|
|
['open', 'error', 'message'].forEach((method) => { |
291
|
|
|
Object.defineProperty(EventSource.prototype, `on${method}`, { |
292
|
|
|
/** |
293
|
|
|
* Returns the current listener |
294
|
|
|
* |
295
|
|
|
* @return {Mixed} the set function or undefined |
296
|
|
|
* @api private |
297
|
|
|
*/ |
298
|
|
|
get: function get() { |
299
|
|
|
const listener = this.listeners(method)[0]; |
300
|
|
|
return listener ? (listener._listener ? listener._listener : listener) : undefined; |
301
|
|
|
}, |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* Start listening for events |
305
|
|
|
* |
306
|
|
|
* @param {Function} listener the listener |
307
|
|
|
* @return {Mixed} the set function or undefined |
308
|
|
|
* @api private |
309
|
|
|
*/ |
310
|
|
|
set: function set(listener) { |
311
|
|
|
this.removeAllListeners(method); |
312
|
|
|
this.addEventListener(method, listener); |
313
|
|
|
}, |
314
|
|
|
}); |
315
|
|
|
}); |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
* Ready states |
319
|
|
|
*/ |
320
|
|
|
Object.defineProperty(EventSource, 'CONNECTING', { enumerable: true, value: 0 }); |
321
|
|
|
Object.defineProperty(EventSource, 'OPEN', { enumerable: true, value: 1 }); |
322
|
|
|
Object.defineProperty(EventSource, 'CLOSED', { enumerable: true, value: 2 }); |
323
|
|
|
|
324
|
|
|
EventSource.prototype.CONNECTING = 0; |
325
|
|
|
EventSource.prototype.OPEN = 1; |
326
|
|
|
EventSource.prototype.CLOSED = 2; |
327
|
|
|
|
328
|
|
|
/** |
329
|
|
|
* Closes the connection, if one is made, and sets the readyState attribute to 2 (closed) |
330
|
|
|
* |
331
|
|
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/close |
332
|
|
|
* @api public |
333
|
|
|
*/ |
334
|
|
|
EventSource.prototype.close = function () { |
335
|
|
|
this._close(); |
336
|
|
|
}; |
337
|
|
|
|
338
|
|
|
/** |
339
|
|
|
* Emulates the W3C Browser based WebSocket interface using addEventListener. |
340
|
|
|
* |
341
|
|
|
* @param {String} type A string representing the event type to listen out for |
342
|
|
|
* @param {Function} listener callback |
343
|
|
|
* @see https://developer.mozilla.org/en/DOM/element.addEventListener |
344
|
|
|
* @see http://dev.w3.org/html5/websockets/#the-websocket-interface |
345
|
|
|
* @api public |
346
|
|
|
*/ |
347
|
|
|
EventSource.prototype.addEventListener = function addEventListener(type, listener) { |
348
|
|
|
if (typeof listener === 'function') { |
349
|
|
|
// store a reference so we can return the original function again |
350
|
|
|
listener._listener = listener; |
351
|
|
|
this.on(type, listener); |
352
|
|
|
} |
353
|
|
|
}; |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* Emulates the W3C Browser based WebSocket interface using removeEventListener. |
357
|
|
|
* |
358
|
|
|
* @param {String} type A string representing the event type to remove |
359
|
|
|
* @param {Function} listener callback |
360
|
|
|
* @see https://developer.mozilla.org/en/DOM/element.removeEventListener |
361
|
|
|
* @see http://dev.w3.org/html5/websockets/#the-websocket-interface |
362
|
|
|
* @api public |
363
|
|
|
*/ |
364
|
|
|
EventSource.prototype.removeEventListener = function removeEventListener(type, listener) { |
365
|
|
|
if (typeof listener === 'function') { |
366
|
|
|
listener._listener = undefined; |
367
|
|
|
this.removeListener(type, listener); |
368
|
|
|
} |
369
|
|
|
}; |
370
|
|
|
|
371
|
|
|
/** |
372
|
|
|
* W3C Event |
373
|
|
|
* |
374
|
|
|
* @see http://www.w3.org/TR/DOM-Level-3-Events/#interface-Event |
375
|
|
|
* @api private |
376
|
|
|
*/ |
377
|
|
|
function Event(type, optionalProperties) { |
378
|
|
|
Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true }); |
379
|
|
|
if (optionalProperties) { |
380
|
|
|
for (const f in optionalProperties) { |
381
|
|
|
if (optionalProperties.hasOwnProperty(f)) { |
382
|
|
|
Object.defineProperty(this, f, { writable: false, value: optionalProperties[f], enumerable: true }); |
383
|
|
|
} |
384
|
|
|
} |
385
|
|
|
} |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
/** |
389
|
|
|
* W3C MessageEvent |
390
|
|
|
* |
391
|
|
|
* @see http://www.w3.org/TR/webmessaging/#event-definitions |
392
|
|
|
* @api private |
393
|
|
|
*/ |
394
|
|
|
function MessageEvent(type, eventInitDict) { |
395
|
|
|
Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true }); |
396
|
|
|
for (const f in eventInitDict) { |
397
|
|
|
if (eventInitDict.hasOwnProperty(f)) { |
398
|
|
|
Object.defineProperty(this, f, { |
399
|
|
|
writable: false, value: eventInitDict[f], enumerable: true |
400
|
|
|
}); |
401
|
|
|
} |
402
|
|
|
} |
403
|
|
|
} |
404
|
|
|
|
Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.
Consider:
If you or someone else later decides to put another statement in, only the first statement will be executed.
In this case the statement
b = 42
will always be executed, while the logging statement will be executed conditionally.ensures that the proper code will be executed conditionally no matter how many statements are added or removed.