forked from fgnass/sendevent
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
139 lines (114 loc) · 3.39 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
var EventEmitter = require('events').EventEmitter
/**
* Returns a middleware that keeps track of all open connections and exposes a
* `broadcast` method that can be called to send data to all connected clients.
*/
module.exports = function(mount) {
if (!mount) mount = '/eventstream'
var emitter = new EventEmitter()
var clients = []
var id = 1
function Client(req, res) {
this.id = id++
this.headers = req.headers
this.res = res
this.send = req.headers.accept == 'text/event-stream'
? sse(this)
: iframe(this, req.url.match(/close/))
clients.push(this)
res.on('close', this.close.bind(this))
}
Client.prototype.close = function() {
emitter.emit('disconnect', this)
this.res.end()
remove(this, clients)
this.closed = true
}
function middleware(req, res, next) {
if (req.url.indexOf(mount) !== 0) return next && next()
var client = new Client(req, res)
var re = req.url.match(/reconnect/)
emitter.emit(re? 'reconnect' : 'connect', client)
}
middleware.on = emitter.on.bind(emitter)
/**
* Broadcast a message to all connected clients.
*/
middleware.broadcast = function(message) {
clients.forEach(function(client) {
client.send(message)
})
}
return middleware
}
/**
* Remove an item from an array.
*/
function remove(item, array) {
var i = array.indexOf(item)
if (~i) array.splice(i, 1)
}
/**
* Initialize the event stream and return a function that sends
* text/event-stream formatted data.
*/
function sse(client) {
client.res.setTimeout(0)
client.res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
})
client.res.write(':hello\n')
return function(data) {
if (data) client.res.write('data: ' + JSON.stringify(data) + '\n')
client.res.write('\n\n') // Add extra line-breaks for Opera Mobile
}
}
/**
* Initializes the iframe and returns a function that emits script tags.
*/
function iframe(client, autoClose) {
var timeout
client.res.writeHead(200, {
'Content-Type': 'text/html',
'Cache-Control': 'no-cache'
})
function script(code, cb) {
if (client.closed) return
client.res.write('<script>')
client.res.write(code)
client.res.write('</script>\n', cb)
}
function close() {
clearTimeout(timeout)
script('setTimeout(function() { location.search = "close&reconnect="+new Date() }, 0)', function () {
client.close()
})
}
client.res.write('<html><body>')
// Emit the p() function that passes the messages to the parent window
script('function p(msg) { parent.handleSentEvent(msg) }')
if (autoClose) {
// If requested with `?close` close the response after the first event
// has been sent or the timeout of 60s is reached.
timeout = setTimeout(close, 60000)
}
else {
// After a certain timeout the "htmlfile" ActiveXObject closes the
// connection and fires an onload event ...
script(
'window.onload = function() {' +
'location.search = "v="+new Date()' +
'}', function() {
// Add 4K padding so that the browser starts to parse the document
client.res.write(new Array(4096).join('.'))
})
}
// Return a function that emits inline scripts which call p()
return function(message) {
script('p(' + JSON.stringify(message) + ')', function() {
if (autoClose) close()
})
}
}