Extending Drupal's Node.js Integration

The Node.js integration Drupal module offers an API that allows developers to add real-time push notification functionality to their modules. Real-time communication could enable features like chat, pop-up notifications, or real-time content update. Chatroom is a great example of how a module can leverage Node.js. 

What does Node.js integration do?

When a visitor opens a page on which the Node.js module is enabled, the client-side JavaScript opens a socket connection between the visitor's browser and the Node.js server. The Node.js server runs the drupal-nodejs application that communicates with Drupal and passes data back and forth between the visitor and Drupal. This connection remains open as long as the visitor is on the page. This allows Drupal to push data to the visitor's browser in real-time so that the visitor doesn't need to refresh the page, and neither does the browser need to continually poll data using AJAX requests. 

Developers can leverage the Node.js Drupal module to push messages to the client side from their module (such as when content is updated), and write client-side JavaScript that responds to those messages (such as by updating the displayed content). In most cases, this fulfills all the real-time messaging needs. In more advanced use-cases, you may need to add custom logic on the Node.js server, such as when the socket connection is opened or the client is authenticated. This is the point where you will need to write an extension to the drupal-nodejs server application.

Writing an extension

An extension is a standard Node.js module, written in JavaScript, that lives in the /extensions subdirectory of the server application. Start by creating an empty JavaScript file in that directory. If your extension will consist of multiple files, or need other Node.js packages, you can create a directory instead, and place your files inside it. 

The drupal-nodejs application borrows the notion of hooks from Drupal, so your extension will need to implement certain hooks to modify the behavior of the server application. As is standard with Node.js modules, your custom functions will need to be exposed via the module.exports property. To be exact, hooks are methods on the object exported by your extension. The following hook methods are available:

  • alterRoutes(routes): Allows you to override route handlers defined by the application. The routes parameter is an object defined in /lib/routes.js. See that file to find out what handlers you can override. 
  • alterSettings(settings): Allows you to modify the settings loaded from the configuration file. The settings parameter corresponds to the settings defined in the nodejs.config.js file.
  • setup(clientManager): Invoked when the application starts so that your extension has a chance to initialize itself. Via the clientManager, you will have access to all the internal data that the application maintains, such as socket connections and channels. See /lib/client-manager.js for details.

To implement one of the hooks, attach a method with the same name to the object your module exports. For example:

// myextension.js

var myExtension = {};

myExtension.alterRoutes = function (routes) {
  // Modify built-in route handlers here...
};

module.exports = myExtension;

You can define your own routes by defining a .routes property on the exported object. This property needs to be an array of objects, each of which should contain the following properties:

  • path: The path that the route will handle.
  • type: The type of the request. May be "get" or "post".
  • handler: The callback function to call when this route is requested.
  • auth: Specifies whether the service key should be validated before requests to this path are served. The service key authentication depends on the path, so if this value is true, a prefix will be added to your path. E.g. the path /mypath will become /nodejs/mypath.

Custom routes could be used to add brand new functionality to the Node.js server, such as retrieving data that is not currently exposed. You would use these routes when making requests from Drupal to the Node.js server. The following is an example route definition.

// myextension.js

var myExtension = {};

myExtension.myRouteHandler = function (req, res) {
  // Handle request, and send response. 
  // res.send({text: 'Hello world.'});
};

myExtension.routes = [
  {
    path: '/mypath',
    type: 'get',
    auth: false,
    handler: myExtension.myRouteHandler
  }
];

module.exports = myExtension;

When the route above is defined, requests to the path /mypath will be routed to the myRouteHandler() method. Route handlers should follow Express standards. That is, they will receive the request and the response object, and they are supposed to send a response. The response should be a JSON object.

In addition to hooks and custom routes, the extension can listen to events that the application dispatches on certain occasions. These events are dispatched on the global process object, which is a built-in Node.js variable. The following is a list of events that the application dispatches. Shown in parentheses are the parameters that each event listener will receive.

  • client-connection(socketId): Dispatched when a new client socket connects to the server.
  • client-authenticated(sessionId, authData): Dispatched after the connecting client has been authenticated via Drupal.
  • client-to-client-message(socketId, message): Dispatched when a client socket sends a messages to another client.
  • client-to-channel-message(socketId, message): Dispatched when a client socket sends a messages to a channel.
  • client-disconnect(socketId): Dispatched when a client socket disconnects.
  • message-published(message, sentCount): Dispatched when a messages is published by Drupal to one or more connected client sockets.

To react to these events, attach a listener to the process object in the setup hook:

// myextension.js

var myExtension = {};

myExtension.setup = function (clientManager) {
  process.on('client-authenticated', function (sessionId, authData) {
    // React to authentication...
  });
};

module.exports = myExtension;

Finally, in order for your extension to be loaded, it will need to be listed in the extensions property in your nodejs.config.js file.

// nodejs.config.js

settings = {
  // ...
  extensions: ['myextension.js'],
  // ...
}

I hope this post will help popularize the Node.js integration module amongst developers. I would love to see more modules that rely on Node.js pop up. If you need assistance or have any suggestions, feel free to open an issue on GitHub (for the server application) or on drupal.org (for the Drupal module).