Projekt

Obecné

Profil

Stáhnout (12.4 KB) Statistiky
| Větev: | Revize:
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', 'checkServerIdentity'
11
]
12

    
13
var bom = [239, 187, 191]
14
var colon = 58
15
var space = 32
16
var lineFeed = 10
17
var carriageReturn = 13
18

    
19
function 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
 **/
32
function 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

    
49
  function onConnectionClosed (message) {
50
    if (readyState === EventSource.CLOSED) return
51
    readyState = EventSource.CONNECTING
52
    _emit('error', new Event('error', {message: message}))
53

    
54
    // The url may have been changed by a temporary
55
    // redirect. If that's the case, revert it now.
56
    if (reconnectUrl) {
57
      url = reconnectUrl
58
      reconnectUrl = null
59
    }
60
    setTimeout(function () {
61
      if (readyState !== EventSource.CONNECTING) {
62
        return
63
      }
64
      connect()
65
    }, self.reconnectInterval)
66
  }
67

    
68
  var req
69
  var lastEventId = ''
70
  if (eventSourceInitDict && eventSourceInitDict.headers && eventSourceInitDict.headers['Last-Event-ID']) {
71
    lastEventId = eventSourceInitDict.headers['Last-Event-ID']
72
    delete eventSourceInitDict.headers['Last-Event-ID']
73
  }
74

    
75
  var discardTrailingNewline = false
76
  var data = ''
77
  var eventName = ''
78

    
79
  var reconnectUrl = null
80

    
81
  function connect () {
82
    var options = parse(url)
83
    var isSecure = options.protocol === 'https:'
84
    options.headers = { 'Cache-Control': 'no-cache', 'Accept': 'text/event-stream' }
85
    if (lastEventId) options.headers['Last-Event-ID'] = lastEventId
86
    if (eventSourceInitDict && eventSourceInitDict.headers) {
87
      for (var i in eventSourceInitDict.headers) {
88
        var header = eventSourceInitDict.headers[i]
89
        if (header) {
90
          options.headers[i] = header
91
        }
92
      }
93
    }
94

    
95
    // Legacy: this should be specified as `eventSourceInitDict.https.rejectUnauthorized`,
96
    // but for now exists as a backwards-compatibility layer
97
    options.rejectUnauthorized = !(eventSourceInitDict && !eventSourceInitDict.rejectUnauthorized)
98

    
99
    // If specify http proxy, make the request to sent to the proxy server,
100
    // and include the original url in path and Host headers
101
    var useProxy = eventSourceInitDict && eventSourceInitDict.proxy
102
    if (useProxy) {
103
      var proxy = parse(eventSourceInitDict.proxy)
104
      isSecure = proxy.protocol === 'https:'
105

    
106
      options.protocol = isSecure ? 'https:' : 'http:'
107
      options.path = url
108
      options.headers.Host = options.host
109
      options.hostname = proxy.hostname
110
      options.host = proxy.host
111
      options.port = proxy.port
112
    }
113

    
114
    // If https options are specified, merge them into the request options
115
    if (eventSourceInitDict && eventSourceInitDict.https) {
116
      for (var optName in eventSourceInitDict.https) {
117
        if (httpsOptions.indexOf(optName) === -1) {
118
          continue
119
        }
120

    
121
        var option = eventSourceInitDict.https[optName]
122
        if (option !== undefined) {
123
          options[optName] = option
124
        }
125
      }
126
    }
127

    
128
    // Pass this on to the XHR
129
    if (eventSourceInitDict && eventSourceInitDict.withCredentials !== undefined) {
130
      options.withCredentials = eventSourceInitDict.withCredentials
131
    }
132

    
133
    req = (isSecure ? https : http).request(options, function (res) {
134
      // Handle HTTP errors
135
      if (res.statusCode === 500 || res.statusCode === 502 || res.statusCode === 503 || res.statusCode === 504) {
136
        _emit('error', new Event('error', {status: res.statusCode, message: res.statusMessage}))
137
        onConnectionClosed()
138
        return
139
      }
140

    
141
      // Handle HTTP redirects
142
      if (res.statusCode === 301 || res.statusCode === 307) {
143
        if (!res.headers.location) {
144
          // Server sent redirect response without Location header.
145
          _emit('error', new Event('error', {status: res.statusCode, message: res.statusMessage}))
146
          return
147
        }
148
        if (res.statusCode === 307) reconnectUrl = url
149
        url = res.headers.location
150
        process.nextTick(connect)
151
        return
152
      }
153

    
154
      if (res.statusCode !== 200) {
155
        _emit('error', new Event('error', {status: res.statusCode, message: res.statusMessage}))
156
        return self.close()
157
      }
158

    
159
      readyState = EventSource.OPEN
160
      res.on('close', function () {
161
        res.removeAllListeners('close')
162
        res.removeAllListeners('end')
163
        onConnectionClosed()
164
      })
165

    
166
      res.on('end', function () {
167
        res.removeAllListeners('close')
168
        res.removeAllListeners('end')
169
        onConnectionClosed()
170
      })
171
      _emit('open', new Event('open'))
172

    
173
      // text/event-stream parser adapted from webkit's
174
      // Source/WebCore/page/EventSource.cpp
175
      var isFirst = true
176
      var buf
177
      res.on('data', function (chunk) {
178
        buf = buf ? Buffer.concat([buf, chunk]) : chunk
179
        if (isFirst && hasBom(buf)) {
180
          buf = buf.slice(bom.length)
181
        }
182

    
183
        isFirst = false
184
        var pos = 0
185
        var length = buf.length
186

    
187
        while (pos < length) {
188
          if (discardTrailingNewline) {
189
            if (buf[pos] === lineFeed) {
190
              ++pos
191
            }
192
            discardTrailingNewline = false
193
          }
194

    
195
          var lineLength = -1
196
          var fieldLength = -1
197
          var c
198

    
199
          for (var i = pos; lineLength < 0 && i < length; ++i) {
200
            c = buf[i]
201
            if (c === colon) {
202
              if (fieldLength < 0) {
203
                fieldLength = i - pos
204
              }
205
            } else if (c === carriageReturn) {
206
              discardTrailingNewline = true
207
              lineLength = i - pos
208
            } else if (c === lineFeed) {
209
              lineLength = i - pos
210
            }
211
          }
212

    
213
          if (lineLength < 0) {
214
            break
215
          }
216

    
217
          parseEventStreamLine(buf, pos, fieldLength, lineLength)
218

    
219
          pos += lineLength + 1
220
        }
221

    
222
        if (pos === length) {
223
          buf = void 0
224
        } else if (pos > 0) {
225
          buf = buf.slice(pos)
226
        }
227
      })
228
    })
229

    
230
    req.on('error', function (err) {
231
      onConnectionClosed(err.message)
232
    })
233

    
234
    if (req.setNoDelay) req.setNoDelay(true)
235
    req.end()
236
  }
237

    
238
  connect()
239

    
240
  function _emit () {
241
    if (self.listeners(arguments[0]).length > 0) {
242
      self.emit.apply(self, arguments)
243
    }
244
  }
245

    
246
  this._close = function () {
247
    if (readyState === EventSource.CLOSED) return
248
    readyState = EventSource.CLOSED
249
    if (req.abort) req.abort()
250
    if (req.xhr && req.xhr.abort) req.xhr.abort()
251
  }
252

    
253
  function parseEventStreamLine (buf, pos, fieldLength, lineLength) {
254
    if (lineLength === 0) {
255
      if (data.length > 0) {
256
        var type = eventName || 'message'
257
        _emit(type, new MessageEvent(type, {
258
          data: data.slice(0, -1), // remove trailing newline
259
          lastEventId: lastEventId,
260
          origin: original(url)
261
        }))
262
        data = ''
263
      }
264
      eventName = void 0
265
    } else if (fieldLength > 0) {
266
      var noValue = fieldLength < 0
267
      var step = 0
268
      var field = buf.slice(pos, pos + (noValue ? lineLength : fieldLength)).toString()
269

    
270
      if (noValue) {
271
        step = lineLength
272
      } else if (buf[pos + fieldLength + 1] !== space) {
273
        step = fieldLength + 1
274
      } else {
275
        step = fieldLength + 2
276
      }
277
      pos += step
278

    
279
      var valueLength = lineLength - step
280
      var value = buf.slice(pos, pos + valueLength).toString()
281

    
282
      if (field === 'data') {
283
        data += value + '\n'
284
      } else if (field === 'event') {
285
        eventName = value
286
      } else if (field === 'id') {
287
        lastEventId = value
288
      } else if (field === 'retry') {
289
        var retry = parseInt(value, 10)
290
        if (!Number.isNaN(retry)) {
291
          self.reconnectInterval = retry
292
        }
293
      }
294
    }
295
  }
296
}
297

    
298
module.exports = EventSource
299

    
300
util.inherits(EventSource, events.EventEmitter)
301
EventSource.prototype.constructor = EventSource; // make stacktraces readable
302

    
303
['open', 'error', 'message'].forEach(function (method) {
304
  Object.defineProperty(EventSource.prototype, 'on' + method, {
305
    /**
306
     * Returns the current listener
307
     *
308
     * @return {Mixed} the set function or undefined
309
     * @api private
310
     */
311
    get: function get () {
312
      var listener = this.listeners(method)[0]
313
      return listener ? (listener._listener ? listener._listener : listener) : undefined
314
    },
315

    
316
    /**
317
     * Start listening for events
318
     *
319
     * @param {Function} listener the listener
320
     * @return {Mixed} the set function or undefined
321
     * @api private
322
     */
323
    set: function set (listener) {
324
      this.removeAllListeners(method)
325
      this.addEventListener(method, listener)
326
    }
327
  })
328
})
329

    
330
/**
331
 * Ready states
332
 */
333
Object.defineProperty(EventSource, 'CONNECTING', {enumerable: true, value: 0})
334
Object.defineProperty(EventSource, 'OPEN', {enumerable: true, value: 1})
335
Object.defineProperty(EventSource, 'CLOSED', {enumerable: true, value: 2})
336

    
337
EventSource.prototype.CONNECTING = 0
338
EventSource.prototype.OPEN = 1
339
EventSource.prototype.CLOSED = 2
340

    
341
/**
342
 * Closes the connection, if one is made, and sets the readyState attribute to 2 (closed)
343
 *
344
 * @see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/close
345
 * @api public
346
 */
347
EventSource.prototype.close = function () {
348
  this._close()
349
}
350

    
351
/**
352
 * Emulates the W3C Browser based WebSocket interface using addEventListener.
353
 *
354
 * @param {String} type A string representing the event type to listen out for
355
 * @param {Function} listener callback
356
 * @see https://developer.mozilla.org/en/DOM/element.addEventListener
357
 * @see http://dev.w3.org/html5/websockets/#the-websocket-interface
358
 * @api public
359
 */
360
EventSource.prototype.addEventListener = function addEventListener (type, listener) {
361
  if (typeof listener === 'function') {
362
    // store a reference so we can return the original function again
363
    listener._listener = listener
364
    this.on(type, listener)
365
  }
366
}
367

    
368
/**
369
 * Emulates the W3C Browser based WebSocket interface using dispatchEvent.
370
 *
371
 * @param {Event} event An event to be dispatched
372
 * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent
373
 * @api public
374
 */
375
EventSource.prototype.dispatchEvent = function dispatchEvent (event) {
376
  if (!event.type) {
377
    throw new Error('UNSPECIFIED_EVENT_TYPE_ERR')
378
  }
379
  // if event is instance of an CustomEvent (or has 'details' property),
380
  // send the detail object as the payload for the event
381
  this.emit(event.type, event.detail)
382
}
383

    
384
/**
385
 * Emulates the W3C Browser based WebSocket interface using removeEventListener.
386
 *
387
 * @param {String} type A string representing the event type to remove
388
 * @param {Function} listener callback
389
 * @see https://developer.mozilla.org/en/DOM/element.removeEventListener
390
 * @see http://dev.w3.org/html5/websockets/#the-websocket-interface
391
 * @api public
392
 */
393
EventSource.prototype.removeEventListener = function removeEventListener (type, listener) {
394
  if (typeof listener === 'function') {
395
    listener._listener = undefined
396
    this.removeListener(type, listener)
397
  }
398
}
399

    
400
/**
401
 * W3C Event
402
 *
403
 * @see http://www.w3.org/TR/DOM-Level-3-Events/#interface-Event
404
 * @api private
405
 */
406
function Event (type, optionalProperties) {
407
  Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true })
408
  if (optionalProperties) {
409
    for (var f in optionalProperties) {
410
      if (optionalProperties.hasOwnProperty(f)) {
411
        Object.defineProperty(this, f, { writable: false, value: optionalProperties[f], enumerable: true })
412
      }
413
    }
414
  }
415
}
416

    
417
/**
418
 * W3C MessageEvent
419
 *
420
 * @see http://www.w3.org/TR/webmessaging/#event-definitions
421
 * @api private
422
 */
423
function MessageEvent (type, eventInitDict) {
424
  Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true })
425
  for (var f in eventInitDict) {
426
    if (eventInitDict.hasOwnProperty(f)) {
427
      Object.defineProperty(this, f, { writable: false, value: eventInitDict[f], enumerable: true })
428
    }
429
  }
430
}
(2-2/2)