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.

// file: my_plugin.js
module.exports = function my_plugin(options) {
  var world = options.world

  this.add('say:hello', function(msg, reply) {
    reply({ hello: world })
  })
}

// file: my_service.js
const Seneca = require('seneca')

Seneca()
  .quiet() // only log errors
  .use('my_plugin', {world: 'Earth'})
  .act('say:hello', Seneca.util.print)

// prints:
// > { hello: 'Earth' }

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.

Sometimes you to need initialize your plugin (to connect to a database, say), and this needs to happen before your plugin can be used. You can define an init action inside your plugin that must complete without errors before Seneca is ready.

// file: my_init_plugin.js
module.exports = function my_init_plugin(options) {
  var world = null

  this.add('say:hello', function(msg, reply) {
    reply({ hello: world })
  })

  this.init(function(done) {
    connect_to_database(function(err, output) {
      if(err) return done(err)
      world = output
      done()
    })
  })
}

// Simulate delay in connecting to database and getting some data.
function connect_to_database(done) {
  setTimeout(function() {
    done(null, 'Mars')
  }, 100)
}

// file: my_init_service.js
const Seneca = require('seneca')

Seneca()
  .quiet() // only log errors
  .use('my_init_plugin')
  .act('say:hello', Seneca.util.print)

// prints:
// > { hello: 'Mars' }

There are many Seneca plugins published on NPM. Most of them can be extended and modified by overriding their action patterns (that’s the Seneca way!). There’s also a list of core plugins that are directly developed by the Seneca team.

Plugins provide you with a way to organize your own code, and to make use of the microservices approach to software architecture. Here’s the recommended way to define a Seneca microservice based on plugins:

// file: recommended_service.js
const Seneca = require('seneca')

Seneca({tag: 'my_microservice'})
  .quiet()
  .use('my_plugin')
  .use('my_init_plugin')
  .ready(function() {
    console.log('service ready:', this.id)
    console.log('plugins:', Object.keys(this.list_plugins()))
  })

// prints:
// > service ready: <random-id>/<start-time>/<pid>/<version>/my_microservice
// > plugins: [ 'my_plugin', 'my_init_plugin', ... <other-plugins> ... ]

Since a service cannot work with plugins that failed to initialize, Seneca will terminate the service process if any plugin fails. You should configure your services to restart on failure in any case, so this handles intermittent failures or restarts of other services that you depend on (but read on for more options).

§Contents

§A Simple Plugin

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

// file: simple.js
const simple_plugin = function(options) {

  this.add({foo: 'bar'}, function(msg, reply) {
    reply({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, the name of the plugin will be “simple_plugin” (as that is the value of name property of the Function object defining the plugin.

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 send messages matching your action pattern. The use method takes two arguments: a plugin name or definition, and an optional options object (which is passed to the plugin). Here is the full code to define and use a plugin:

// file: simple.js
const Seneca = require('seneca')

const simple_plugin = function(options) {

  this.add({foo: 'bar'}, function(msg, reply) {
    reply({color: options.color})
  })
}

Seneca()
  .quiet()
  .use(simple_plugin, {color:'pink'})
  .act({foo:'bar'}, Seneca.util.print)

Run this script as follows:

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

The output is a JavaScript object with a single property color, the value of which is set from the original options given to the plugin. The example uses the utility function Seneca.util.print to print the action result neatly so that you don’t have to worry about error handling. The example is run in “quiet” mode so that the default logging (which is suitable for production deployment) doesn’t clutter up the output.

§Loading a Plugin

Seneca will look for your plugin using the first argument that you give to the use method. If the first argument is a string value it will be interpreted as file path or a module name and will be passed to the [require][] built-in function. If the first argument is a function it will be interpreted as the plugin definition function.

Let’s take the “simple” plugin example and break it apart into two files:

  • simple_plugin.js: a script defining the plugin
  • simple_service.js: a script the starts Seneca and loads the plugin

This configuration is the recommended way to structure your own microservices—run the microservice using a service script that loads all the plugins you need to define the service.

Here is the plugin code, which exposes the plugin definition function as the exported module value:

// file: simple_plugin.js
module.exports = function simple_plugin(options) {

  this.add({foo: 'bar'}, function(msg, reply) {
    reply({color: options.color})
  })
}

And here is the service script that loads and runs the plugin:

// file: simple_service.js
const Seneca = require('seneca')

Seneca()
  .quiet()
  .use('simple_plugin', {color:'pink'})
  .act({foo:'bar'}, Seneca.util.print)

Run this service as follows:

$ node simple_service.js
{ color: 'pink' }

You’ll also notice that Seneca uses method chaining as an API design principle. This functionality is provided as a convenience, so that your service definition code can be a concise chain of [use[]] calls.

The .use method in the above example refered to a plugin directly by name “simple_plugin”. Seneca was able to find this plugin because a file in the same folder as the service script existed called simple_plugin.js. The Seneca .use method passes the plugin name to the builtin system require function. Thus, the following are essentially equivalent:

seneca.use('simple_plugin')
seneca.use(require('./simple_plugin'))

Thus, if Seneca can’t find your plugin for some reason, you can just require it in directly.

Framework plugins that provide general functionality (such as database connections, or user account business logic) are prefixed with seneca- to place them in a separate namespace from your own plugins. Nonetheless, since you’ll often want to use multiple framework plugins per service, it can be tedious and verbose to have to enter their full names. Seneca allows you to optionally omit the seneca- prefix. Thus: seneca.use('user') is equivalent to seneca.use('seneca-user') (assuming you’ve run npm install seneca-user).

TODO: update seneca-echo

§Give Your Plugin a Name

Your plugin needs a name so that you can track and debug its behavior. The easiest way to do this is to give the function that defines your plugin a name.

function mercury(options) {
  this.add('say:hello', function (msg, reply) {
    reply({ hello: 'world' })
  })
}

Then you can observe the registration and configuration of the plugin in the Seneca logs:

$ node -e "require('seneca').use('./mercury-plugin.js')" - --seneca.test | grep mercury
46/1x/- add/ADD  name:mercury,plugin:define,role:seneca,seq:2,tag:undefined ...
60/1x/- act/IN/a  vu/xv  name:mercury,plugin:define,role:seneca,seq:2,tag:undefined ...
60/1x/- plugin/init  mercury
61/1x/- plugin/install  mercury
62/1x/- act/DEFAULT/s  xz/xv  {role:'seneca',plugin:'init',seq:2,init:'mercury',tag:undefined} 
62/1x/- plugin/ready  mercury
63/1x/- act/OUT/a  vu/xv  name:mercury,plugin:define,role:seneca,seq:2,tag:undefined ...

In the above command (which uses the -e option of node.js to quickly load the plugin), the argument --seneca.test makes Seneca run in test mode, which output developer friendly debug logs. To remove clutter, grep filters the logs down to those containing the string “mercury”, the name of the plugin.

We’ll cover what these log entries mean later. For now, just observe that you can use the plugin name to identify the logs relevant to your plugin.

If you don’t give your plugin definition function a name, then the file path of the script file containing the plugin is used.

// file: venus.js
function (options) {
  this.add('say:hello', function (msg, reply) {
    reply({ hello: 'world' })
  })
}

// venus-service.js
const Seneca = require('seneca')

Seneca()

  // The name of the plugin will be "./venus.js"
  .use('./venus.js')

===================

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' }

§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' )

§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.