Passed
Branch master (11c3c5)
by Dave
48s
created

eventsource.js ➔ EventSource   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 263
Code Lines 165

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 4
eloc 165
c 1
b 0
f 1
nc 4
dl 0
loc 263
rs 7
nop 2

6 Functions

Rating   Name   Duplication   Size   Complexity  
F eventsource.js ➔ ... ➔ connect 0 154 17
D eventsource.js ➔ ... ➔ parseEventStreamLine 0 43 12
A eventsource.js ➔ ... ➔ _emit 0 5 2
A eventsource.js ➔ ... ➔ onConnectionClosed 0 18 3
A eventsource.js ➔ ... ➔ this._close 0 6 5
A eventsource.js ➔ ... ➔ Object.defineProperty.get 0 3 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
var original = require("original");
2
var parse = require("url").parse
3
var events = require("events");
4
var https = require("https");
5
var http = require("http");
6
var util = require("util");
7
8
var 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
  var readyState = EventSource.CONNECTING;
22
  Object.defineProperty(this, 'readyState', {
23
    get: function () {
24
      return readyState
25
    }
26
  })
27
28
  Object.defineProperty(this, 'url', {
29
    get: function () {
30
      return url
31
    }
32
  })
33
34
  var self = this;
35
  self.reconnectInterval = 60000;
36
37
  function onConnectionClosed () {
38
    if (readyState === EventSource.CLOSED) return
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

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 (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
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(function () {
49
      if (readyState !== EventSource.CONNECTING) {
50
        return;
51
      }
52
      connect();
53
    }, self.reconnectInterval)
54
  }
55
56
  var req;
57
  var 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
  var discardTrailingNewline = false;
64
  var data = '';
65
  var eventName = '';
66
67
  var reconnectUrl = null;
68
69
  function connect () {
70
    var options = parse(url)
71
    var isSecure = options.protocol === 'https:'
72
    options.headers = { 'Cache-Control': 'no-cache', 'Accept': 'text/event-stream' }
73
    if (lastEventId) options.headers['Last-Event-ID'] = lastEventId
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

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 (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
74
    if (eventSourceInitDict && eventSourceInitDict.headers) {
75
      for (var i in eventSourceInitDict.headers) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
76
        var 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
    var useProxy = eventSourceInitDict && eventSourceInitDict.proxy
90
    if (useProxy) {
91
      var 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 (var optName in eventSourceInitDict.https) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
105
        if (httpsOptions.indexOf(optName) === -1) {
106
          continue
107
        }
108
109
        var 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, function (res) {
122
      // Handle HTTP errors
123
      if (res.statusCode === 500 || res.statusCode === 502 || res.statusCode === 503 || res.statusCode === 504) {
124
        _emit('error', new Event('error', {status: res.statusCode}))
125
        onConnectionClosed()
126
        return
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
127
      }
128
129
      // Handle HTTP redirects
130
      if (res.statusCode === 301 || res.statusCode === 307) {
131
        if (!res.headers.location) {
132
          // Server sent redirect response without Location header.
133
          _emit('error', new Event('error', {status: res.statusCode}))
134
          return
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
135
        }
136
        if (res.statusCode === 307) reconnectUrl = url
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

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 (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
137
        url = res.headers.location
138
        process.nextTick(connect)
139
        return
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
140
      }
141
142
      if (res.statusCode !== 200) {
143
        _emit('error', new Event('error', {status: res.statusCode}))
144
        return self.close()
145
      }
146
147
      // protect against multiple connects
148
      //https://github.com/tigertext/eventsource/commit/ca8a6e0ca0db10c23ba7bf2b7f8affaa23d7a265
149
      if (readyState === EventSource.OPEN) {
150
          return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
151
      }
152
153
      readyState = EventSource.OPEN
154
      res.on('close', function () {
155
        res.removeAllListeners('close')
156
        res.removeAllListeners('end')
157
        onConnectionClosed()
158
      })
159
160
      res.on('end', function () {
161
        res.removeAllListeners('close')
162
        res.removeAllListeners('end')
163
        onConnectionClosed()
164
      })
165
      _emit('open', new Event('open'))
166
167
      // text/event-stream parser adapted from webkit's
168
      // Source/WebCore/page/EventSource.cpp
169
      var buf = ''
170
      res.on('data', function (chunk) {
171
        buf += chunk
172
173
        var pos = 0
174
        var length = buf.length
175
176
        while (pos < length) {
177
          if (discardTrailingNewline) {
0 ignored issues
show
Bug introduced by
The variable discardTrailingNewline is changed as part of the while loop for example by false on line 181. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
178
            if (buf[pos] === '\n') {
179
              ++pos
180
            }
181
            discardTrailingNewline = false
182
          }
183
184
          var lineLength = -1
185
          var fieldLength = -1
186
          var c
187
188
          for (var i = pos; lineLength < 0 && i < length; ++i) {
189
            c = buf[i]
190
            if (c === ':') {
191
              if (fieldLength < 0) {
192
                fieldLength = i - pos
193
              }
194
            } else if (c === '\r') {
195
              discardTrailingNewline = true
196
              lineLength = i - pos
197
            } else if (c === '\n') {
198
              lineLength = i - pos
199
            }
200
          }
201
202
          if (lineLength < 0) {
203
            break
204
          }
205
206
          parseEventStreamLine(buf, pos, fieldLength, lineLength)
207
208
          pos += lineLength + 1
209
        }
210
211
        if (pos === length) {
212
          buf = ''
213
        } else if (pos > 0) {
214
          buf = buf.slice(pos)
215
        }
216
      })
217
    })
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
218
219
    req.on('error', onConnectionClosed)
220
    if (req.setNoDelay) req.setNoDelay(true)
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

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 (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
221
    req.end()
222
  }
223
224
  connect()
225
226
  function _emit () {
227
    if (self.listeners(arguments[0]).length > 0) {
228
      self.emit.apply(self, arguments)
229
    }
230
  }
231
232
  this._close = function () {
233
    if (readyState === EventSource.CLOSED) return
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

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 (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
234
    readyState = EventSource.CLOSED
235
    if (req.abort) req.abort()
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

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 (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
236
    if (req.xhr && req.xhr.abort) req.xhr.abort()
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

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 (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
237
  }
238
239
  function parseEventStreamLine (buf, pos, fieldLength, lineLength) {
240
    if (lineLength === 0) {
241
      if (data.length > 0) {
242
        var type = eventName || 'message'
243
        _emit(type, new MessageEvent(type, {
244
          data: data.slice(0, -1), // remove trailing newline
245
          lastEventId: lastEventId,
246
          origin: original(url)
247
        }))
248
        data = ''
249
      }
250
      eventName = void 0
0 ignored issues
show
Coding Style introduced by
Consider using undefined instead of void(0). It is equivalent and more straightforward to read.
Loading history...
251
    } else if (fieldLength > 0) {
252
      var noValue = fieldLength < 0
253
      var step = 0
254
      var field = buf.slice(pos, pos + (noValue ? lineLength : fieldLength))
255
256
      if (noValue) {
257
        step = lineLength
258
      } else if (buf[pos + fieldLength + 1] !== ' ') {
259
        step = fieldLength + 1
260
      } else {
261
        step = fieldLength + 2
262
      }
263
      pos += step
264
265
      var valueLength = lineLength - step
266
      var value = buf.slice(pos, pos + valueLength)
267
268
      if (field === 'data') {
269
        data += value + '\n'
270
      } else if (field === 'event') {
271
        eventName = value
272
      } else if (field === 'id') {
273
        lastEventId = value
274
      } else if (field === 'retry') {
275
        var retry = parseInt(value, 10)
276
        if (!Number.isNaN(retry)) {
277
          self.reconnectInterval = retry
278
        }
279
      }
280
    }
281
  }
282
}
283
284
module.exports = EventSource
285
286
util.inherits(EventSource, events.EventEmitter)
287
EventSource.prototype.constructor = EventSource; // make stacktraces readable
288
289
['open', 'error', 'message'].forEach(function (method) {
290
  Object.defineProperty(EventSource.prototype, 'on' + method, {
291
    /**
292
     * Returns the current listener
293
     *
294
     * @return {Mixed} the set function or undefined
295
     * @api private
296
     */
297
    get: function get () {
298
      var listener = this.listeners(method)[0]
299
      return listener ? (listener._listener ? listener._listener : listener) : undefined
300
    },
301
302
    /**
303
     * Start listening for events
304
     *
305
     * @param {Function} listener the listener
306
     * @return {Mixed} the set function or undefined
307
     * @api private
308
     */
309
    set: function set (listener) {
310
      this.removeAllListeners(method)
311
      this.addEventListener(method, listener)
312
    }
313
  })
314
})
315
316
/**
317
 * Ready states
318
 */
319
Object.defineProperty(EventSource, 'CONNECTING', {enumerable: true, value: 0})
320
Object.defineProperty(EventSource, 'OPEN', {enumerable: true, value: 1})
321
Object.defineProperty(EventSource, 'CLOSED', {enumerable: true, value: 2})
322
323
EventSource.prototype.CONNECTING = 0
324
EventSource.prototype.OPEN = 1
325
EventSource.prototype.CLOSED = 2
326
327
/**
328
 * Closes the connection, if one is made, and sets the readyState attribute to 2 (closed)
329
 *
330
 * @see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/close
331
 * @api public
332
 */
333
EventSource.prototype.close = function () {
334
  this._close()
335
}
336
337
/**
338
 * Emulates the W3C Browser based WebSocket interface using addEventListener.
339
 *
340
 * @param {String} type A string representing the event type to listen out for
341
 * @param {Function} listener callback
342
 * @see https://developer.mozilla.org/en/DOM/element.addEventListener
343
 * @see http://dev.w3.org/html5/websockets/#the-websocket-interface
344
 * @api public
345
 */
346
EventSource.prototype.addEventListener = function addEventListener (type, listener) {
347
  if (typeof listener === 'function') {
348
    // store a reference so we can return the original function again
349
    listener._listener = listener
350
    this.on(type, listener)
351
  }
352
}
353
354
/**
355
 * Emulates the W3C Browser based WebSocket interface using removeEventListener.
356
 *
357
 * @param {String} type A string representing the event type to remove
358
 * @param {Function} listener callback
359
 * @see https://developer.mozilla.org/en/DOM/element.removeEventListener
360
 * @see http://dev.w3.org/html5/websockets/#the-websocket-interface
361
 * @api public
362
 */
363
EventSource.prototype.removeEventListener = function removeEventListener (type, listener) {
364
  if (typeof listener === 'function') {
365
    listener._listener = undefined
366
    this.removeListener(type, listener)
367
  }
368
}
369
370
/**
371
 * W3C Event
372
 *
373
 * @see http://www.w3.org/TR/DOM-Level-3-Events/#interface-Event
374
 * @api private
375
 */
376
function Event (type, optionalProperties) {
377
  Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true })
378
  if (optionalProperties) {
379
    for (var f in optionalProperties) {
380
      if (optionalProperties.hasOwnProperty(f)) {
381
        Object.defineProperty(this, f, { writable: false, value: optionalProperties[f], enumerable: true })
382
      }
383
    }
384
  }
385
}
386
387
/**
388
 * W3C MessageEvent
389
 *
390
 * @see http://www.w3.org/TR/webmessaging/#event-definitions
391
 * @api private
392
 */
393
function MessageEvent (type, eventInitDict) {
394
  Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true })
395
  for (var f in eventInitDict) {
396
    if (eventInitDict.hasOwnProperty(f)) {
397
      Object.defineProperty(this, f, { writable: false, value: eventInitDict[f], enumerable: true })
398
    }
399
  }
400
}
401