@aphorica/server-event-mgr

Implements a server-event service in node Express.

the npm package is here:

Implements a server-event service in node express.

Features:

  • provides rest endpoints for operations
  • provides a singleton ‘manager’ object that actually performs the operations. You can either allow the express rest calls provided (which will use the object, internally), or you can provide your own rest calls and invoke the operations on the manager object.
  • keeps a list of connections - these are used to send messages when a task is completed.
  • the list is periodically scanned for dormant connections. Those inactive for a specific timeout threshold are cleaned up.
  • you can provide a prefix to the built-in rest points. The default prefix is ‘/sse/’

Caveats:

  • IN PROGRESS - STILL DEVELOPING
  • (pull requests welcome, but we need to tightly coordinate any merges.)
  • Currently does not support multiple managers. Could think about that, if required.
  • If the client drops, there is no way for the server to know that happened. Consequently, when the client comes back, its id is invalid. Active re-connection by the client (get a new id and re-register) will resume any currently registered tasks.

Implementation Notes:

Instantiated Router

The manager creates a router via the createRouter() function, which returns a router. The router needs to be added to the Express app:

const ServerEventMgr = require('@aph/server-event-mgr');
app.use(ServerEventMgr.createRouter());

Note you don’t have to do this if you provide your own rest handlers (untested).

Express Global Request Handlers

A typical Express app will have a global request handler to set things like response headers, especially CORS headers.

Except for an OPTIONS preflight request, SSE responses require their own headers. If you have implemented a global request handler to provide headers, you must qualify the request to not send headers (with the exception of an OPTIONS preflight request) when the rest path includes /register-listener/.

An example from the test/demo package follow (I use semi-colons):

var allowCrossDomain = function(req, res, next) {
  console.log('allowXDomain: ' + req.method +
  ' ' + req.path);
  if (req.method === 'OPTIONS' || req.path.indexOf('/register-listener/') === -1) {
            // do NOT do following if registering listener
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Accept, Authorization, Content-Length, X-Requested-With');
    // intercept OPTIONS method
    if (req.method === 'OPTIONS') {
      res.sendStatus(204);
      return;
    }
  }

  next();
};

SSE responses include a CORS header for ‘*’ (Might could make this a settable feature, if desired);

“/submit-task” REST Endpoint

The /submit-task REST endpoint is provided by you. In this endpoint, you will perform the task necessary, and at the end of the task, you will invoke the manager notifyCompletions() function with the client id (provided earlier) and a task id.

The method of the endpoint is entirely up to the you - it can be a PUT, GET, POST, or whatever.

“/listen” REST Endpoint

The /listen REST endpoint is provided by you.

Rest API

The provided rest endpoints are listed, along with their corresponding manager calls. See the ServerEventManager API for operational details.

Main API

  • (pfx)/make-id/:name => getUniqueID(name)
  • (pfx)/register-listener/:id => registerListener(id, response)
  • (pfx)/disconnect-registrant/:id => unregisterListener(id)

Provided By Implementor (you)

  • /submit-task/:id/:taskid => (ultimately) notifyCompletions(id, taskid)

Debugging

  • (pfx)/list-registrants => getListenersJSON()
  • (pfx)/clear-registrants => unregisterAllListeners()
  • (pfx)/trigger-cleanup => triggerCleanup()

Manager API

Setup API

createRouter(prefix)
prefix - the prefix to apply (default: '/sse/')
returns - a new router

The prefix will be prepended to all publicly exposed urls in the router. If not provided, the default ('/sse/') will be used. The returned router must be 'added' to the _Express_ app by the caller.
setVerbose(flag)
flag - true/false

If set, will log diagnostic messages. Used for debugging.
setNotifyListenersChanged(flag) - (experimental)
flag - true/false

If set, when users are added or removed, all listeners will be notified of the change. Mainly for debugging, but might be useful in other contexts.

Main API

getUniqueID(name)
name - typically a username
returns - the unique id

Constructs a unique id based on the provided name, in the form `name_xxxxx` where 'x' represents an alphanum character. The resultant id is used for all subsequent operations that require an id.
isRegistered(id)
id - the unique id for this listener
returns - whether or not the id is registered

Query if the id is in the list of active connections.
registerListener(id, response)
id - the unique id for this listener
response - the original response object passed in the request.

The response object is associated with this id and held. It is used to transmit subsequent notifications to the client(s) associated with this ID.
unregisterListener(id)
id - the unique id for this listener

The listener is removed from the list of active connections and the response is nulled deleted. No other actions occur.
notifyCompletions(id, taskid)
id - the unique id for this listener
taskid - a task-specific id provided by the client

Called by the implementor when a specific task is completed after submission (from the client.) The taskid is returned to the client in the notification.

Debugging API

getListenersJSON()
returns - a JSON of all the listeners as a map

Used mainly for debugging
unregisterAllListeners()
Removes and deletes all listener entries. Also turns off the cleanupInterval mechanism.
notifyListenersChanged() - (experimental)
Will send change notification to all listeners.
triggerCleanup()
Forces an immediate invocation of the cleanup mechanism.