├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── browser.js ├── example.js ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | script: "npm test" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # event-source-stream 2 | 3 | [EventSource](https://developer.mozilla.org/en-US/docs/Server-sent_events/Using_server-sent_events) implemented in node as a readable stream 4 | 5 | ``` js 6 | npm install event-source-stream 7 | ``` 8 | 9 | [![build status](http://img.shields.io/travis/mafintosh/event-source-stream.svg?style=flat)](http://travis-ci.org/mafintosh/event-source-stream) 10 | ![dat](http://img.shields.io/badge/Development%20sponsored%20by-dat-green.svg?style=flat) 11 | 12 | ## Usage 13 | 14 | ``` js 15 | var ess = require('event-source-stream') 16 | 17 | ess('http://server-sent-events-demo.herokuapp.com/update') 18 | .on('data', function(data) { 19 | console.log('received event:', data) 20 | }) 21 | ``` 22 | 23 | Per default it will retry after 3s when the connection terminates. Change this by setting the `retry` option 24 | 25 | ``` js 26 | // no retries 27 | ess('http://server-sent-events-demo.herokuapp.com/update', {retry:false}).pipe(...) 28 | 29 | // retry after 10s 30 | ess('http://server-sent-events-demo.herokuapp.com/update', {retry:10000}).pipe(...) 31 | ``` 32 | 33 | ## Browser support 34 | 35 | It also works in the browser using browserify 36 | 37 | ``` js 38 | var ess = require('event-source-stream') // will use EventSource behind the scene 39 | 40 | ess('http://server-sent-events-demo.herokuapp.com/update') 41 | .on('data', function(data) { 42 | console.log('recevied event in the browser', data) 43 | }) 44 | ``` 45 | 46 | ## License 47 | 48 | MIT -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | var stream = require('readable-stream') 2 | 3 | module.exports = function(url, opts) { 4 | if (!opts) opts = {} 5 | 6 | var es = new EventSource(url) 7 | var rs = new stream.Readable({objectMode:true}) 8 | 9 | var json = !!opts.json 10 | var decode = function (data) { 11 | try { 12 | if (json) return JSON.parse(data) 13 | return data 14 | } catch (err) { 15 | return undefined 16 | } 17 | } 18 | 19 | rs._read = function() {} 20 | 21 | es.onmessage = function(e) { 22 | rs.push(decode(e.data)) 23 | } 24 | 25 | es.onerror = function(err) { 26 | if (rs.listeners('error').length) rs.emit('error', err) 27 | } 28 | 29 | es.onopen = function () { 30 | rs.emit('open') 31 | } 32 | 33 | var destroyed = false 34 | rs.destroy = function() { 35 | if (destroyed) return 36 | destroyed = true 37 | es.close() 38 | rs.emit('close') 39 | } 40 | 41 | return rs 42 | } 43 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var es = require('./') 2 | 3 | es('http://server-sent-events-demo.herokuapp.com/update').on('data', console.log) -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | var split = require('split2') 3 | var once = require('once') 4 | 5 | module.exports = function(url, opts) { 6 | if (!opts) opts = {} 7 | if (typeof opts.retry !== 'number' && opts.retry !== false) opts.retry = 3000 8 | 9 | var json = !!opts.json 10 | var decode = function (data) { 11 | try { 12 | if (json) return JSON.parse(data) 13 | return data 14 | } catch (err) { 15 | return undefined 16 | } 17 | } 18 | 19 | var buf = '' 20 | var req 21 | var timeout 22 | var opened = false 23 | 24 | var parse = split(function(line) { 25 | if (!line) { 26 | if (!buf) return 27 | var data = buf 28 | buf = '' 29 | return decode(data) 30 | } 31 | if (line.indexOf('data: ') === 0) buf += (buf ? '\n' : '') + line.slice(6) 32 | }) 33 | 34 | var connect = function() { 35 | var reqOpts = opts.request || {} 36 | reqOpts.url = url 37 | buf = '' 38 | req = request(reqOpts) 39 | 40 | var onclose = once(function () { 41 | if (destroyed) return 42 | 43 | if (!opts.retry) { 44 | destroyed = true 45 | return parse.end() 46 | } 47 | 48 | timeout = setTimeout(connect, opts.retry) 49 | parse.emit('retry') 50 | }) 51 | 52 | req.on('error', function(err) { 53 | if (!opts.retry) parse.emit('error', err) 54 | onclose() 55 | }) 56 | 57 | req.on('complete', onclose) 58 | 59 | req.on('response', function (res) { 60 | if (!opened) { 61 | parse.emit('open') 62 | opened = true 63 | } else { 64 | parse.emit('reconnect') 65 | } 66 | res.on('end', onclose) 67 | }) 68 | 69 | req.pipe(parse, {end:false}) 70 | } 71 | 72 | connect() 73 | 74 | var destroyed = false 75 | parse.destroy = function() { 76 | if (destroyed) return 77 | destroyed = true 78 | clearTimeout(timeout) 79 | if (req) req.abort() 80 | parse.emit('close') 81 | } 82 | 83 | return parse 84 | } 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "event-source-stream", 3 | "version": "1.6.0", 4 | "description": "EventSource implemented in node as a readable stream", 5 | "main": "index.js", 6 | "dependencies": { 7 | "once": "^1.3.1", 8 | "readable-stream": "^2.2.2", 9 | "request": "^2.36.0", 10 | "split2": "^0.1.2" 11 | }, 12 | "devDependencies": { 13 | "tape": "^2.13.3" 14 | }, 15 | "scripts": { 16 | "test": "tape test.js" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/mafintosh/event-source-stream" 21 | }, 22 | "keywords": [ 23 | "event", 24 | "source", 25 | "stream", 26 | "client", 27 | "readable", 28 | "html5" 29 | ], 30 | "author": "Mathias Buus", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/mafintosh/event-source-stream/issues" 34 | }, 35 | "homepage": "https://github.com/mafintosh/event-source-stream", 36 | "browser": "browser.js" 37 | } 38 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var http = require('http') 3 | var ess = require('./') 4 | 5 | var server = http.createServer(function(req, res) { 6 | if (req.url === '/multiline') { 7 | res.write('data: a\n') 8 | res.write('data: b\n\n') 9 | } 10 | if (req.url === '/basic') { 11 | res.write('data: hello world\n\n') 12 | } 13 | if (req.url === '/header') { 14 | res.write('data: hello ' + req.headers.custom + '\n\n') 15 | } 16 | if (req.url === '/crash') { 17 | res.write('data: test\n\n') 18 | res.end() 19 | } 20 | }) 21 | 22 | server.listen(0, function() { 23 | server.unref() 24 | var addr = 'http://localhost:'+server.address().port 25 | 26 | tape('events', function(t) { 27 | var stream = ess(addr+'/basic') 28 | 29 | stream.on('data', function(data) { 30 | stream.destroy() 31 | t.same(data, 'hello world') 32 | t.end() 33 | }) 34 | }) 35 | 36 | tape('multiline events', function(t) { 37 | var stream = ess(addr+'/multiline') 38 | 39 | stream.on('data', function(data) { 40 | stream.destroy() 41 | t.same(data, 'a\nb') 42 | t.end() 43 | }) 44 | }) 45 | 46 | tape('retry', function(t) { 47 | var stream = ess(addr+'/crash', {retry:100}) 48 | var cnt = 2 49 | 50 | stream.on('data', function(data) { 51 | if (!--cnt) { 52 | stream.destroy() 53 | t.end() 54 | } 55 | t.same(data, 'test') 56 | }) 57 | }) 58 | 59 | tape('send custom header', function(t) { 60 | var headerValue = 'value' 61 | var stream = ess(addr+'/header', {request: {headers: {Custom: headerValue}}}) 62 | 63 | stream.on('data', function(data) { 64 | stream.destroy() 65 | t.same(data, 'hello ' + headerValue) 66 | t.end() 67 | }) 68 | }) 69 | }) 70 | --------------------------------------------------------------------------------