Writing extensions

Last updated on
30 April 2025

Please note it has been a while since I wrote this implementation, but it appears nobody ever documented this properly. So this is to give people some directions on where to look. Feel free to update and add to this document.

--

The Node.js module allows one to extend the behavior of the server. This way we can emit and handle custom event handlers.

The usage of event handlers and callbacks is already covered in Custom javascript events with NodeJS, so I'd like to extend a bit on the server side extension.

Making node.js aware of your module

If you are extending the serverside of node.js, you need to make sure the node server is aware of your module. This can be done by altering your nodejs.config.js file and adding your module's server-side js file in the 'extensions' array like this:

settings = {
  scheme: 'http',
  port: 8080,
  host: 'localhost',
  resource: '/socket.io',
  serviceKey: '',
  backend: {
    port: 80,
    host: 'drupalhost',
    scheme: 'http',
    basePath: '',
    messagePath: '/nodejs/message'
  },
  ...
  extensions: [
    '../my_module/server-side.js'
  ],
  ...
}

The node server runs in the nodejs module folder, so you go up one folder (to sites/all/modules) and then descend in your own module again.

Using the node.js server functions

The default node.js server exposes several functions and variables for extensions to use. The full list can be found at the bottom of the server.js file in the nodejs module. As of writing it looks like this:

var extensionsConfig = {
  'publishMessageToChannel': publishMessageToChannel,
  'publishMessageToClient': publishMessageToClient,
  'publishMessageToContentChannel': publishMessageToContentChannel,
  'addClientToChannel': addClientToChannel,
  'settings': settings,
  'channels': channels,
  'io': io,
  'tokenChannels': tokenChannels,
  'authenticatedClients': authenticatedClients,
  'request': request,
  'server': server,
  'sendMessageToBackend': sendMessageToBackend
};

These variables are available to your extension like this:

'use strict';

var io;
var server;
var settings;
var tokenChannels;
var sendMessageToBackend;
var publishMessageToClient;

exports.setup = function (config) {
  // Inherit function from NodeJS module.
  io = config.io;
  server = config.server;
  settings = config.settings;
  tokenChannels = config.tokenChannels;
  sendMessageToBackend = config.sendMessageToBackend;
  publishMessageToClient = config.publishMessageToClient;
}

Handling nodejs server events

The nodejs server emits several events that can be used in extensions. The events can be found in the server.js file by searching for 'process.emit' and then can be used by your extension by invoking 'process.on'.

To detect a client connection one could use:

process.on('client-connection', function (socket_id) {
  // Do something
});

Please note that a connection does not mean the user is allowed on the channel, so the node.js server attempts to authenticate the user as soon as he connects and then emits the 'client-authenticated' event:

process.on('client-authenticated', function(sessionId, authData) {
  // Do something
});

The full list of events the node.js server emits is:

  • process.emit('message-published', message, sentCount);
  • process.emit('client-authenticated', sessionId, authData);
  • process.emit('client-connection', socket.id);
  • process.emit('client-to-channel-message', socket.id, message);
  • process.emit('client-to-client-message', socket.id, message);
  • process.emit('client-disconnect', socket.id);

To find out what the values of these variables are, I sent them to the node.js process using console.log.

Sending something to the backend

So let's create a simple module that sends a message to the backend and the user as soon as somebody enters a session.

'use strict';

var io;
var server;
var settings;
var tokenChannels;
var sendMessageToBackend;
var publishMessageToClient;

exports.setup = function (config) {
  // Inherit function from NodeJS module.
  io = config.io;
  server = config.server;
  settings = config.settings;
  tokenChannels = config.tokenChannels;
  sendMessageToBackend = config.sendMessageToBackend;
  publishMessageToClient = config.publishMessageToClient;

  // When a user joins a room and is allowed to proceed.
  process.on('client-authenticated', function(sessionId, authData) {
    // This is the only place the uid is exposed, so we store it.
    var uid = authData.uid;

    // Compose a message
    var message = {
      callback: 'myModuleCallback',
      channel: 'myModuleChannel',
      body: 'User ' + uid + ' just joined this channel',
      // This confused me, but frontend and backend appear to be using different types. If one is missing communication might fail.
      type: 'myModule',
      messageType: 'myModule'
    };

    // Send a message to Drupal
    sendMessageToBackend(message, function (error, response, body) {
      if (error) {
        console.log('Error sending message to backend.', error);
      }
    });

    // Send a message to the client
    publishMessageToClient(socketId, message);
  }
};

Handling of custom events

I am not sure about the security of this part, as the nodejs module takes a lot of things from our shoulders. So proceed on your own risk here. I assume only allowing a user to send and receive data after authentication should do, but I am not entirely sure.

The default implementation of the nodejs server emits every message as a message. This means you will have to send and process everything with

config.publishMessageToChannel(message);
process.on('client-to-channel-message', function(socketId, message){ ...  });

However this is highly incompatible with npm packages that want a simple websocket to operate on. Luckily the socket.io object is available to the extension, this allows us to include other modules in our server extension.

process.on('client-authenticated', function(sessionId, authData) {
  // Fetch the socket object from the io object.
  var socket = io.sockets.connected[sessionId];

  // Emit data to the socket of the authenticated user
  var data = 'welcome';
  socket.emit('my-custom-event', data);
  
  // Only start receiving messages after authentication
  process.on('client-message', function(sessionId, message) {
    sendMessageToTokenChannel(message);
  });
});

The message the user is receiving will be delivered to the browser. So here we will switch to Drupal javascript. Make sure the file is included either via your module's .info file or via drupal_add_js().

(function ($) {
  Drupal.behaviors.myModule= {
    attach: function (context, settings) {
      var moduleSettings = settings.myModule
      var socket = Drupal.Nodejs.socket;

      socket.on('my-custom-event', function(message) {
        console.log(message);
      });
    }
  }
})(jQuery);

You can populate the settings via:

  global $user;

  drupal_add_js(array(
    'myModule' => array(
      'channel' => 'myModuleChannel',
      'name' => check_plain(format_username($user)),
    ),
  ), 'setting');

More reading

Most of this document is a combination of fiddling around and reading the documentation by Ryan Oles. He has put some of his modules on github and explains them on his blog. There is also this video of him speaking at Drupalcamp Austin 2013 https://www.youtube.com/watch?v=Sxsmw5rZehs explaining the basic concepts of node.js and a simple extension.

Module Development with NodeJS Integration
Talking to Drupal with Node.js
Using Drupal Content Channel Tokens with Nodejs

Help improve this page

Page status: Not set

You can: