Design, develop and organize code

§Frequently Asked Questions

[⇧]

§Got a question?

Please post an issue to github, marking as “FAQ:” in the title: I want to ask a question!

[⇧]

§Why is pattern-matching so useful?

Because it gives you a clean component model. This lets you build big things out of small things without ending up with spaghetti code.

The trick is to make composition easy. That way you can combine and extend microservices. Instead of modifying an existing microservice, simply add a new one with more functionality. This is a much more scalable way to handle changing requirements without building up technical debt.

There’s a great talk by Gerald Sussman (co-inventor of Scheme) on the basic principles of this idea. There’s also a maintainer blog post explaining this from a Seneca perspective.

A diamond is very pretty.
But it is hard to add to a diamond.
 
A ball of mud is not so pretty.
But you can always add more mud to a ball of mud.
[Joel Moses](https://en.wikipedia.org/wiki/Joel_Moses) / [Paul Penfield](http://www-mtl.mit.edu/~penfield/)

 

[⇧]

Some things that work well:

  • Separate the business logic from the execution. Put your business logic into separate plugins - either separate node modules, different repositories, or simply different files in the same repository.

  • Use execution scripts to compose your app. Don’t be afraid to use different scripts for different contexts. They should be pretty short any way. You want your scripts to look something like:

var SOME_CONFIG = process.env.SOME_CONFIG || 'some-default-value' 

require('seneca')({ some_options: 123 })

  // existing Seneca plugins
  .use('community-plugin-0')
  .use('community-plugin-1', {some_config: SOME_CONFIG})
  .use('community-plugin-2')

  // your own plugins with your own business logic
  .use('project-plugin-module')
  .use('../plugin-repository')
  .use('./lib/local-plugin')

  .listen( ... )
  .client( ... )

  .ready( function() {
    // your own custom code - executed once Seneca is spun up
  })

Things that don’t work well:

  • Mixing Seneca initialization with other framework initialization. Define your express or hapi app in one file, and Seneca in another. Keep it separate and simple.

  • Passing the Seneca instance around. Don’t create a Seneca instance and then pass it as a parameter to stuff you’ve required in, and only then start adding plugins and actions. Instead, use small execution scripts with clear and linear construction of your service from a list of plugins.

Looking for an example structure to copy? The nodezoo workshop is a good place to find one.

[⇧]

§Why is the Seneca process dying with a FATAL error?

Your Seneca microservice will rely on a set of plugins, both community plugins, and your own, to provide the actions that respond to messages. All of the plugins need to load and initialize successfully. If they don’t, your microservice will be in an undefined state. The best thing to do in this case is bail out and die.

For example, if you are using the seneca-mongo-store plugin, and the plugin cannot connect to the database, then your microservice can’t do any work. Maybe the failure is transient. In that case, you want the microservice to die and restart. Your container system (Docker, Kubernetes, etc) will start a new instance of your microservice, but needs the old one to die first.

When Seneca loads a plugin, it is first defined, then initialized. Plugin definition happens inside the plugin definition function, where you add your patterns:

// file: foo.js
// The string `foo` will be the name of the plugin.
module.exports = function foo(options) {

  // `this` === context Seneca instance with fatal$ === true
  // the pattern is `a:1`
  this.add('a:1', function (msg, reply) {
    reply({x: msg.x})
  }) 
}

A failure of any sort here is fatal, as it means the plugin is not fully defined. Some patterns could be missing. Plugin definition is synchronous.

Plugin initialization happens inside the init:<plugin-name> action. Add this inside your plugin definition:

// file: foo.js
// The function `foo` will be the name of the plugin.
module.exports = function foo(options) {

  this.add('a:1', function (msg, reply) {
    reply({x: msg.x})
  }) 

  this.add('init:foo', function (msg, reply) {

    // do something asynchronous
    database_setup(options, function(err) {

      // call reply to indicate that the plugin is initialized,
      // no need for response data
      reply(err)
    })
  })
}

Plugin initialization is optional, and only necessary if you need to perform an asynchronous operation such as interacting with the outside world. Failures in initialization are also fatal.

The seneca.ready callback is not called until all plugins are defined and initialized.

To make an action fatal when it fails, use the fatal$:true directive as part of your message. This is what Seneca itself does for plugin definition (which is run inside an internally created one-off action) and initialization. The fatal$ directive is set as a fixed argument of any messages sent by the plugin Seneca context.

There are times when you want to avoid this behavior. One way, when debugging, is to set the option debug.undead = true:

var Seneca = require('seneca')
var seneca = Seneca({debug: {undead: true}})

Another is to create a new Seneca instance that does not have a fixed fatal$ = true argument:

var fresh_seneca = this.root.delegate()

This is necessary for plugins that will send messages once Seneca is ready, such as the transport plugins.

[⇧]

§How do I respond to a message?

When a message pattern is matched, it triggers execution of the associated action function that you have defined. Your action function is passed three parameters:

  • msg: the message that triggered this action.
  • reply: a callback function that you can use to reply to the message.
  • meta: a meta data object for tracing and debugging (normally ignored).

Thus a typical action definition looks like so:

const Seneca = require('seneca')

const seneca = Seneca()

seneca
  .add({a: 1}, function(msg, reply) {
    reply(null, {x: msg.y})
  })

And to use it, you call:

seneca.act({a: 1, y: 2}, function(err, out) {
  console.log(err) // prints null, as there was no error
  console.log(out) // prints {x: 2}, as that was the response given to `reply`
})

The reply callback follows the normal signature for callbacks: callback(err, result). But it also provides some convenience abbreviations. You can provide a data response using:

  • reply(null, {z: 3})
  • reply({z: 3})

You can provide an error response using:

  • reply(new Error('my error message')

And you can provide an empty response using:

  • reply()

Thus, our example above can be more conveniently written as:

seneca
  .add({a: 1}, function(msg, reply) {
    reply({x: msg.y})
  })

It is also common practice to pass the reply callback into another function expecting a standard signature callback. For example:

const Fs = require('fs')

seneca
  .add({file: 'read'}, function(msg, reply) {
    Fs.stat(msg.path, reply)
  })

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