diff --git a/packages/sse/README.md b/packages/sse/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c6172bce3bac85add0a8187bbc437147527c9126 --- /dev/null +++ b/packages/sse/README.md @@ -0,0 +1,31 @@ +# pubsweet-sse + +Broadcast server-sent events to connected Express clients. + +## Install + +``` +npm install pubsweet-sse +``` + +or + +``` +yarn install pubsweet-sse +``` + +## Example usage in Express + +```js +const sse = require('pubsweet-sse') + +// set the /updates route for authenticated clients to connect using EventSource +app.get('/updates', passport.authenticate('bearer'), sse.connect) + +// broadcast data to all connected clients +sse.send({ foo: 'bar' }) +``` + +## Alternatives + +This module was originally adapted from [express-sse](https://github.com/dpskvn/express-sse) diff --git a/packages/sse/lib/SSE.js b/packages/sse/lib/SSE.js new file mode 100644 index 0000000000000000000000000000000000000000..f222777051026cabdd9a2a128f14bf3de74ad5da --- /dev/null +++ b/packages/sse/lib/SSE.js @@ -0,0 +1,72 @@ +const EventEmitter = require('events').EventEmitter + +class SSE extends EventEmitter { + constructor () { + super() + + this.connect = this.connect.bind(this) + this.messageId = 0 + this.pulse() + } + + connect (req, res) { + // if (req.header('Accept').indexOf('text/event-stream') === -1) { + // TODO: throw exception? + // } + + req.socket.setTimeout(Number.MAX_SAFE_INTEGER) + req.socket.setNoDelay(true) + req.socket.setKeepAlive(true) + + res.statusCode = 200 + + res.set({ + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no' // prevent buffering by nginx + }) + + this.setMaxListeners(this.getMaxListeners() + 1) + + const write = (type, data) => { + res.write(type + ': ' + data) + res.write('\n') + } + + const dataListener = data => { + write('id', this.messageId++) + + if (data.event) { + write('event', data.event) + } + + write('data', JSON.stringify(data.data)) + + res.write('\n') + } + + // TODO: store all updates, use Last-Event-ID to send missed messages on reconnect + + this.on('data', dataListener) + + req.on('close', () => { + this.removeListener('data', dataListener) + this.setMaxListeners(this.getMaxListeners() - 1) + }) + } + + pulse () { + const pulseInterval = setInterval(() => { + this.emit('data', {event: 'pulse', data: Date.now()}) + }, 10000) + + pulseInterval.unref() + } + + send (data, event) { + this.emit('data', {data, event}) + } +} + +module.exports = new SSE() diff --git a/packages/sse/package.json b/packages/sse/package.json new file mode 100644 index 0000000000000000000000000000000000000000..7239c3371809ca49a6fc3b6c94cea450e924da0f --- /dev/null +++ b/packages/sse/package.json @@ -0,0 +1,28 @@ +{ + "name": "pubsweet-sse", + "description": "Broadcast server-sent events to connected Express clients", + "version": "0.1.3", + "homepage": "https://gitlab.coko.foundation/pubsweet/pubsweet-sse", + "repository": { + "type": "git", + "url": "git+https://gitlab.coko.foundation/pubsweet/pubsweet-sse.git" + }, + "author": "Collaborative Knowledge Foundation", + "license": "MIT", + "main": "lib/SSE.js", + "files": [ + "lib" + ], + "engines": { + "node": ">= 6" + }, + "devDependencies": { + "standard": "^10.0.2" + }, + "eslintConfig": { + "extends": "standard", + "parserOptions": { + "ecmaVersion": 6 + } + } +}