UNPKG

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