Design, develop and organize code

§How to Write a Seneca Plugin

When you use the Seneca framework, you write plugins all the time. They are an easy way to organize your action patterns.

A Seneca plugin is just a function that gets passed an options object, and has a Seneca instance as its this variable. You then add some action patterns in the body of the function, and you’re done. There is no callback.

This article will show some plugin examples, with code, going from basic to advanced. It will cover the plugin API, and the conventions to use when writing them. You’ll need to log the behaviour of your plugins, and you’ll need to know how to debug them, so that will be discussed too.

There are many Seneca plugins published on NPM. Most of them can be extended and modified by overriding their actions. You’ll also need to know how to do this.

Finally, plugins provide you with a way to organize your own code, and to make use of the micro-services approach to software architecture, so that will be discussed too.

§Contents

§A Simple Plugin

Let’s write a plugin that defines one action. The action uses the plugin options argument to build a result.

var plugin = function( options ) {

  this.add( {foo:'bar'}, function( args, done ) {
    done( null, {color: options.color} )
  })

}

The example above defines a single action pattern, foo:bar. This action provides a result based on the options provided to the plugin. Plugin options are not required, but if they are provided, they are passed in as the first argument to the plugin definition function. The options argument is just a JavaScript object with some properties. Seneca makes sure it always exists. Even in the case where you have no options, you’ll still get an empty object.

The context object of the plugin function (that is, the value of this), is a Seneca instance that you can use to define actions. That means you don’t need to call require('seneca') when defining a plugin. This Seneca instance provides the standard API, but the logging methods are special - they append information about the plugin. So when you call this.log.debug('stuff about my plugin'), the log output will contain extra fields identifying the plugin, such as its name. In this example, you haven’t given the plugin a name (you’ll see how to do that in a moment), so Seneca will generate a short random name for you.

You can use the plugin by calling the use method of the Seneca object. This loads the plugin into Seneca, after which the action patterns defined by the plugin are available. You can then call the act method to trigger them, like so:

// simple.js

var seneca = require('seneca')()

var plugin = function( options ) { ... } // as above

seneca.use( plugin, {color:'pink'} )
seneca.act( {foo:'bar'}, console.log )

This code is available in the doc/examples/write-a-plugin example, in the simple.js script. Running the script produces:

$ node simple.js
null { color: 'pink' }

In the output, the null is the first argument to console.log, and indicates that there was no error. The output is a JavaScript object with single property color, the value of which is set from the original options given to the plugin.

§Initializing a Plugin

Let’s look at our example again.

// simple.js

var plugin = function( options ) {

  this.add( {foo:'bar'}, function( args, done ) {
    done( null, {color: options.color} )
  })

}

As we can see, a plugin is just a function. You can see that there is no callback passed into this function that defines the plugin. So, how does Seneca know that the plugin has fully initialized? It’s an important question, because the plugin might depend on establishing a database connection before it can operate properly.

Many plugins don’t even need to initialize, because all they do is define a set of action patterns. Let’s say in this case we would like to initialise our plugin. As with most things in Seneca, you define an action pattern to handle initialization and make sure it happens in the proper order.

// init.js

var plugin = function( options ) {

  seneca.add( {init:'pluginName'}, function( args, done ) {
    // do stuff, e.g.
    console.log('connecting to db...')
    setTimeout(function(){
      console.log('connected!')
      done()
    }, 1000)
  })

  this.add( {foo:'bar'}, function( args, done ) {
    done( null, {color: options.color} )
  })

  return 'pluginName'

}

For this to work, our plugin needs to have a name. Plugin name and init value must be exactly the same. In this case return 'pluginName' serves that purpose. See Give Your Plugin a Name for alternatives to this approach.

When plugin is fed into use method, seneca waits for its init to finish before continuing. That’s why we call done() even when it does nothing.

§A Plugin is a Module

The Seneca use method can also accept module references. That is, if you can require it, you can use it! Let’s update the simple example to show this. First, create a file called foo.js containing the plugin code (all the files in this article are available on the Seneca github at (doc/examples/write-a-plugin).

// foo.js

module.exports = function( options ) {

  this.add( {foo:'bar'}, function( args, done ) {
    done( null, {color: options.color} )
  })

}

The foo.js file is a normal JavaScript file you can load into Node.js with require. It exposes a single function that takes the plugin options. To use the plugin, the code is almost the same as before, except that you pass in the foo.js relative file path in the same way you would for require.

// module.js

var seneca = require('seneca')()

seneca.use( './foo.js', {color:'pink'} )
seneca.act( {foo:'bar'}, console.log )

The code produces the same output as before:

$ node module.js
null { color: 'pink' }

As well as local files and local modules, you can use public plugin modules from npmjs.org. Let’s use seneca-echo plugin as an example. This plugin echoes back arguments you send to the role:echo pattern. First, npm install it:

$ npm install seneca-echo

Then use it:

// echo.js

var seneca = require('seneca')()

seneca.use( 'seneca-echo' )
seneca.act( {role:'echo', foo:'bar'}, console.log )

Running echo.js produces:

$ node echo.js
null { foo: 'bar' }

You aren’t using any options in this example. The seneca-echo plugin just reproduces the arguments passed in. In this case foo:bar. The role property is not included in the output.

The Seneca framework comes with many plugins written by the community. Feel free to write one yourself (after reading this article!). By convention, public and generically useful plugins are prefixed with seneca- as part of their name. This lets you know the module is a Seneca plugin if you see it on NPM. However, its a bit tedious to type in “seneca-“ all the time, so you are allowed to abbreviate plugin names by dropping the “seneca-“ prefix. That means you can use the the seneca-echo by just providing the “echo” part of the name:

seneca.use( 'echo' )

§Give Your Plugin a Name

Your plugin needs a name. You can return a string from the plugin definition function to give it one. When you look at the Seneca logs, you can see what your plugin is doing. Let’s try it!

// name0.js

var plugin = function( options ) {

  this.add( {foo:'bar'}, function( args, done ) {
    done( null, {color: options.color} )
  })

  return 'name0'
}

var seneca = require('seneca')()

seneca.use( plugin, {color:'pink'} )
seneca.act( {foo:'bar'}, console.log )

And then run it like so:

$ node name0.js --seneca.log=plugin:name0
... DEBUG  act  name0  -  yvgt5y48wqjb  IN   {foo=bar}  ...
... DEBUG  act  name0  -  yvgt5y48wqjb  OUT  {color=pink}  ...
null { color: 'pink' }

This uses Seneca’s log filtering feature to focus on the log lines that you care about. For more details on log filtering, read the logging tutorial.

To avoid repetition, the public plugins drop their “seneca-“ prefix when registering their names. Try this:

$ node echo.js --seneca.log=plugin:echo
... DEBUG  plugin  echo  -  add  echo  -  {role=echo}  ...
... DEBUG  act     echo  -  lkmlk29r6uwt  IN   {role=echo,foo=bar}  ...
... DEBUG  act     echo  -  lkmlk29r6uwt  OUT  {foo=bar}  ...
null { foo: 'bar' }

You may have noticed something interesting. There were three lines of logging output that time. Why didn’t you see an “add” line for your “name0” plugin? During the execution of its definition function, it didn’t have a name. You only gave it one when you returned a name. Sometimes this is useful, because you can set a name dynamically. Still, is it possible to set the name initially? Yes! Just give the defining function a name:

// name1.js

var plugin = function name1( options ) {

  this.add( {foo:'bar'}, function( args, done ) {
    done( null, {color: options.color} )
  })
}

var seneca = require('seneca')()

seneca.use( plugin, {color:'pink'} )
seneca.act( {foo:'bar'}, console.log )

Running this gives:

$ node name1.js --seneca.log=plugin:name1
... DEBUG  plugin  name1  -  add  name1  -  {foo=bar}  ...
... DEBUG  act     name1  -  b3uamicogfnm  IN   {foo=bar}  ...
... DEBUG  act     name1  -  b3uamicogfnm  OUT  {color=pink}  ...
null { color: 'pink' }

When you load a plugin as a module then the module reference, as supplied to the use method, becomes the initial name of the module (Of course, you can override this by returning your own name) Here’s the foo.js plugin again:

$ node module.js --seneca.log=plugin:./foo.js
... DEBUG  plugin  ./foo.js  -  add  ./foo.js  -  {foo=bar}  ...
... DEBUG  act     ./foo.js  -  47ssblskuj59  IN   {foo=bar}  ...
... DEBUG  act     ./foo.js  -  47ssblskuj59  OUT  {color=pink}  ...
null { color: 'pink' }

There’s an obvious risk that you might have a naming conflict. Seneca allows this because it’s more useful to have the ability to override plugins. If you’re defining your own set of plugin names, it’s best to choose a short prefix for your project. This is a good idea in general for many frameworks!

For example, if you’re working on the Manhattan project, choose the prefix mh. Then call your “Trinity” plugin mh-trinity.

There are no hard and fast rules for naming your action patterns. However, there are some conventions that help to organize the patterns. Your plugin is providing functionality to the system. This functionality fulfills a role in the system. So it makes sense to use the form role:plugin-name as part of your action pattern. This creates a pattern namespace to avoid clashes with other plugin patterns. The use of the word “role” also indicates that other plugins may override some aspects of this role (that is, aspects of this functionality) by providing extensions to some of the action patterns.

For example, the seneca-vcache plugin overrides the standard entity patterns, of the form role:entity, cmd:*. It does this to transparently add caching to the database store operations.

Another common convention is to use the property “cmd” for the main public commands exposed by the plugin. So, you might have, for example:

var plugin = function trinity( options ) {

  this.add( {role:'trinity', cmd:'detonate'}, function( args, done ) {
    // ... compress plutonium, etc
  })
}

Many of the public Seneca plugins on NPM follow this pattern. You may find other patterns more useful in your own projects, so don’t feel obligated to follow this one.

If you load a plugin multiple times, only the last one loaded will be used. You can however load multiple separate instances of the same plugin, by using tag strings. NOTE: the action patterns will still be overridden, unless the plugin handles this for you (like the example below). The data store plugins, in particular, use this mechanism to support multiple databases in the same system. For more details, read the data entities tutorial. data entities tutorial.

Here’s a simple example that uses tags. In this case, the bar.js plugin defines an action pattern using one of its option properties. This means that different action patterns are defined depending on the options provided.

// bar.js
module.exports = function( options ) {
  var tag = this.context.tag

  this.add( {foo:'bar', zed:options.zed}, function( args, done ) {
    done( null, {color: options.color, tag:tag} )
  })

}

You can access the tag value from the context property of the plugin Seneca instance: this.context.tag

You still want to debug and track each instance of this plugin, so you provide a tag each time you register it with the use method. Tags can be supplied in two ways, either by description object for the plugin, or by suffixing a $ character, and then the tag, to the plugin module reference. Here’s the example code:

// tags.js
var seneca = require('seneca')()

seneca.use( {name:'./bar.js',tag:'AAA'}, {zed:1,color:'red'} )
seneca.use( './bar.js$BBB',              {zed:2,color:'green'} )

seneca.act( {foo:'bar',zed:1}, console.log )
seneca.act( {foo:'bar',zed:2}, console.log )

Running this code produces the output:

$ node tags.js
null { color: 'red', tag: 'AAA' }
null { color: 'green', tag: 'BBB' }

Using the debug log shows the different instances of the plugin in action:

$ node tags.js --seneca.log=plugin:./bar.js
... DEBUG  plugin  ./bar.js  AAA  add  ./bar.js  AAA  {foo=bar,zed=1}  ...
... DEBUG  plugin  ./bar.js  BBB  add  ./bar.js  BBB  {foo=bar,zed=2}  ...
... DEBUG  act     ./bar.js  AAA  pamds7vlteyv  IN   {foo=bar,zed=1}  ...
... DEBUG  act     ./bar.js  BBB  4uxz90gcczn5  IN   {foo=bar,zed=2}  ...
... DEBUG  act     ./bar.js  AAA  pamds7vlteyv  OUT  {color=red,tag=AAA}  ...
null { color: 'red', tag: 'AAA' }
... DEBUG  act     ./bar.js  BBB  4uxz90gcczn5  OUT  {color=green,tag=BBB}  ...
null { color: 'green', tag: 'BBB' }

To isolate a tag, use these log settings:

$ node tags.js --seneca.log=plugin:./bar.js,tag:AAA
... DEBUG  plugin  ./bar.js  AAA  add  ./bar.js  AAA  {foo=bar,zed=1}  ...
... DEBUG  act     ./bar.js  AAA  9rp8luozaf92  IN   {foo=bar,zed=1}  ...
... DEBUG  act     ./bar.js  AAA  9rp8luozaf92  OUT  {color=red,tag=AAA}  ...
null { color: 'red', tag: 'AAA' }
null { color: 'green', tag: 'BBB' }

§Dealing with Options

It’s useful to provide default option values for users of your plugin. Seneca provides a utility function to support this: seneca.util.deepextend. The deepextend function works much the same as _.extend, except that it can handle properties at any level. For example:

// deepextend.js
var seneca = require('seneca')()

var foo = {
  bar: 1,
  colors: {
    red:   50,
    green: 100,
    blue:  150,
  }
}

var bar = seneca.util.deepextend(foo,{
  bar: 2,
  colors: {
    red: 200
  }
})

console.log(bar)
// { bar: 2, colors: { red: 200, green: 100, blue: 150 } }

The property colors.red is overridden, but the other colors retain their default values.

You can use this in your own plugins. Let’s add default options to the foo.js module (as above).

// foo-defopts.js
module.exports = function( options ) {

  // Default options
  options = this.util.deepextend({
    color: 'red',
    box: {
      width:  100,
      height: 200
    }
  },options)


  this.add( {foo:'bar'}, function( args, done ){
    done( null, { color:      options.color,
                   box_width:  options.box.width,
                   box_height: options.box.height
                })
  })

  return {name:'foo'}
}

(As an aside, note that you can also specify the name of the plugin by returning an object of the form {name:...}. You’ll see some more properties you can add this return object below).

The default option structure is used as the base for the user supplied options. Let’s supply some user options that will override the defaults:

// module-defopts.js
var seneca = require('seneca')()

seneca.use( './foo-defopts.js', {
  color:'pink',
  box:{
    width:50
  }
})

seneca.act( {foo:'bar'}, console.log )

This code runs the foo:bar action, which produces:

$ node module-defopts.js
null { color: 'pink', box_width: 50, box_height: 200 }

The default values for color and box.width (red and 100, respectively), have been overridden by the options provided as the second argument to seneca.use when the plugin is loaded (pink and 50).

You can load plugin options from configuration files. Seneca looks for a file named seneca.options.js in the current folder, and requires the file if it exists. This file should be a Node.js module that exports a JSON object. For example:

// seneca.options.js
module.exports = {
  zed: {
    red:   50,
    green: 100,
    blue:  150,
  },
  'zed$tag0': {
    red:   55,
  }
}

You can specify global Seneca options in this file, and you can specify options for individual plugins. Top level properties that match the name of a plugin are used to provide options to plugins when they are loaded.

Let’s see this in action. The zed.js script defines a plugin that prints out the plugin name and tag using this.context (see above), and also prints out the options provided to the plugin by Seneca.

// zed.js
function zed( options ) {
  console.log( this.context.name, this.context.tag, options )
}

var seneca = require('seneca')()

seneca.use( zed )

As the example seneca.options.js file defines a zed property, this is used to provide options to the zed plugin. Running the zed.js script prints out the options loaded from seneca.options.js:

$ node zed.js
zed undefined { red: 50, green: 100, blue: 150 }

If you are using tags to create multiple instances of the same plugin, you can use the $suffix convention to specify options particular to a given tagged plugin instance. The zed-tag.js script is the same as the zed.js script, except that it also creates an additional tagged instance of the zed plugin. Note that the definition of the plugin uses a properties object, with the init property specifying the plugin definition function.

// zed-tag.js
function zed( options ) {
  console.log( this.context.name, this.context.tag, options )
}

var seneca = require('seneca')()

seneca.use( zed )
seneca.use( {init:zed, name:'zed', tag:'tag0'} )

The seneca.options.js file also defines a zed$tag0 property, and the options for the tag0 instance of the zed plugin are taken from this. However, if you run the code, you’ll notice that it also picks up the options defined for the main zed plugin. These become base defaults, so that the special case option, red: 55 overrides the main value.

$ node zed-tag.js
zed undefined { red: 50, green: 100, blue: 150 }
zed tag0       { red: 55, green: 100, blue: 150 }

Sometimes you need to access to all the options provided to Seneca. For example, there is a global timeout value that you might want to use for timeouts. The transport family of plugins do this, see redis-transport for an example.

Inside your plugin function, you can call this.options() to get back an object containing the entire Seneca options tree:

// zed-access.js
function zed( options ) {
  console.log( this.options() )
}

var seneca = require('../../../lib/seneca.js')()

seneca.use( zed )

Running this script produces:

$ node zed-access.js
{ ...
  timeout: 33333,
  ...
  zed: { red: 50, green: 100, blue: 150 },
  'zed$tag0': { red: 55 },
  ...
}

You are not required to use the seneca.options.js file. If it exists, it will be loaded and used as the base default for options. You can specify your own configuration file (or an object containing option values), by providing an argument to seneca.options(). This is useful for different deployment scenarios. For example, the file dev.options.js defines a custom configuration for the zed plugin:

// dev.options.js
module.exports = {
  zed: {
    green: 110,
  }
}

The zed-dev.js script uses this options file, but also gets the default options from seneca.options.js:

function zed( options ) {
  console.log( this.context.name, this.context.tag, options )
}

var seneca = require('seneca')()
seneca.options('./dev.options.js')

seneca.use( zed )

And the output has the overridden value for the green option.

$ node zed-dev.js
zed undefined { red: 50, green: 110, blue: 150 }

Finally, you can specify options on the command line, either via an argument, or an environment variable. Here are some examples using the zed-dev.js script. Use the --seneca.options command line argument to provide option values. You can use “dot notation” to specify nested options, and you can specify multiple options:

$ node zed-dev.js --seneca.options.zed.red=10 --seneca.options.zed.blue=200
zed undefined { red: 10, green: 110, blue: 200 }

Alternatively, you can use the environment variable SENECA_OPTIONS to specify options that will be merged into the base defaults (using seneca.util.deepextend). The format is jsonic jsonic, a lenient, abbreviated, fully compatible version of JSON for lazy developers.

$ SENECA_OPTIONS="{zed:{red:10,blue:200}}" node zed-dev.js
zed undefined { red: 10, green: 110, blue: 200 }

Command line options always override options from other sources. Here is the order of priority, from highest to lowest:

  • Command line
  • Environment variable
  • Source code
  • Custom options file
  • Default options file
  • Internal defaults

Issues? From spelling errors to broken tutorials and everything in between, report them here.