Server Sent Events from Node.js to Web Client - pushing user command line input to all Subscribed Browser Sessions image 31

Server Sent Events from Node.js to Web Client – pushing user command line input to all Subscribed Browser Sessions

imageNode can push messages to browser clients, using WebSockets and the simpler Server Sent Events (SSE) mechanism. We will use the latter in this article – to push updates (event) to all subscribed browser clients.

The browser loads index.html from the Node.js server. This file contains a JavaScript fragment that makes an AJAX call to the Node.js server (URL: /updates ) to register as a SSE Client. As part of that subscription, a callback function is registered that will be called when the browser receives SSE events from the Node.js server on this channel.

The Node application – apart from initializing an Express HTTP Server that handles incoming requests – for index.html and for the /updates path – also uses NPM package prompt to run a loop to request user input on the command line. Whenever the user types some input, it is published as an SSE event by the application and pushed to the SSE Clients: the browsers. The callback handler function in the browser received the SSE event and in this case simply writes the contents to the UI.

More glamorous and useful applications of this mechanism are not hard to conceive of.

GitHub Repository with sources for this article:  https://github.com/lucasjellema/nodejs-serversentevents-quickstart .

 

The application is very straightforward:

image

Inspect file app.js. This file serves a static file – index.html – and exposes a single endpoint – /updates – where SSE clients can register.

var express = require('express'); //npm install express
var bodyParser = require('body-parser'); // npm install body-parser
var http = require('http');
var sseMW = require('./sse');
var prompt = require('prompt'); //https://www.npmjs.com/package/prompt

var APP_VERSION = "0.8";

var PORT = process.env.PORT || 3000;

var app = express();
var server = http.createServer(app);
server.listen(PORT, function () {
    console.log('Server running, version ' + APP_VERSION + ', Express is listening... at ' + PORT + " for requests");
});

app.use(bodyParser.json()); // for parsing application/json
app.use(express.static(__dirname + '/public'))

//configure sseMW.sseMiddleware as function to get a stab at incoming requests, in this case by adding a Connection property to the request
app.use(sseMW.sseMiddleware)

// Realtime updates
var sseClients = new sseMW.Topic();

app.get('/updates', function (req, res) {
    console.log("res (should have sseConnection)= " + res.sseConnection);
    var sseConnection = res.sseConnection;
    console.log("sseConnection= ");
    sseConnection.setup();
    sseClients.add(sseConnection);
});

File index.html contains a JavaScript fragment, where the client registers with the SSE Server – at the /updates endpoint. A listener is associated with this SSE connection – to read the consumed message and update the UI subsequently, in the plainest way imaginable.

<html>
<head>
    <title>A static web page with dynamic pushed updated</title>
        <script>
	  // assume that API service is published on same server that is the server of this HTML file
      var source = new EventSource("../updates");
      source.onmessage = function(event) {
        var data = JSON.parse(event.data);
        var inputCell= document.getElementById("input");
        inputCell.innerHTML = JSON.stringify(data) + ';'+ inputCell.innerHTML; 
      };//onMessage
    </script>    

</head>
<body>
<h1>Hello World!</h1>
Are you having fun today?
<br/>
<div id="input">
</div>
</body>
</html>

Back to app.js: it uses the prompt module to solicit input from the user on the command line. Every piece of input is published as server sent event to all SSE clients, to be published in the browser UI.

var m;
updateSseClients = function (message) {
    console.log("update all Sse Client with message " + message);
    this.m = message;
    sseClients.forEach(function (sseConnection) {
        console.log("send sse message global m" + this.m);
        sseConnection.send(this.m);
    }
        , this // this second argument to forEach is the thisArg (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach) 
    );
}

promptForInput();

// go into loop where user can provide input
var timeToExit = false;
var allInput = [];

function promptForInput() {
    prompt.get(['yourInput'], function (err, result) {
        console.log('Your Input:' + result.yourInput);
        // send input to function that forwards it to all SSE clients
        updateSseClients(result.yourInput);
        timeToExit = ('exit' == result.yourInput)
        if (timeToExit) {
            wrapItUp();
        }
        else {
            allInput.push(result.yourInput);
            promptForInput();
        }
    });
}

function wrapItUp() {
    console.log('It was nice talking to you. Goodbye!');
    // final recap of the dialog:
    console.log("All your input:\n " + JSON.stringify(allInput));
}

You can look at file sse.js for some more details on how the SSE clients and topics internals are implemented. You will notice that there is not too much to it.

"use strict";

console.log("loading sse.js");

// ... with this middleware:
function sseMiddleware(req, res, next) {
    console.log(" sseMiddleware is activated with " + req + " res: " + res);
    res.sseConnection = new Connection(res);
    console.log(" res has now connection  res: " + res.sseConnection);
    next();
}
exports.sseMiddleware = sseMiddleware;
/**
 * A Connection is a simple SSE manager for 1 client.
 */
var Connection = (function () {
    function Connection(res) {
        console.log(" sseMiddleware construct connection for response ");

        this.res = res;
    }
    Connection.prototype.setup = function () {
        console.log("set up SSE stream for response");
        this.res.writeHead(200, {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive'
        });
    };
    Connection.prototype.send = function (data) {
        console.log("send event to SSE stream " + JSON.stringify(data));
        this.res.write("data: " + JSON.stringify(data) + "\n\n");
    };
    return Connection;
} ());

exports.Connection = Connection;
/** 
 * A Topic handles a bundle of connections with cleanup after lost connection.
 */
var Topic = (function () {
    function Topic() {
        console.log(" constructor for Topic");

        this.connections = [];
    }
    Topic.prototype.add = function (conn) {
        var connections = this.connections;
        connections.push(conn);
        console.log('New client connected, the number of clients is now: ', connections.length);
        conn.res.on('close', function () {
            var i = connections.indexOf(conn);
            if (i >= 0) {
                connections.splice(i, 1);
            }
            console.log('Client disconnected, now: ', connections.length);
        });
    };
    Topic.prototype.forEach = function (cb) {
        this.connections.forEach(cb);
    };
    return Topic;
} ());
exports.Topic = Topic;

Run app.js on the command line:  node app.js

image

And open your browser at http://127.0.0.1:3000/.
image

Type some input on the command line. This input should appear in the browser.
image

image

Open a second browser window at the same URL. Type some more input on the command line. This input should appear in all browsers.

image