Skip to content

Extensions

Benjamin Arthur Lupton edited this page Jun 24, 2014 · 5 revisions

Extension Types

There are three extension types in Chainy:

  • action which is an extension added to the chain's task runner queue
  • utility which is an extension added directly to the chain's object
  • custom which is an extension that adds itself

The default extension type is action, however you should always specify the extension type for clarity and consistency.

Action Extensions

Action extensions are for when you want to perform an action on the data or with it. They are run serially (one after the other, in sequence), ensuring that the previous action has completed, before the next action begins.

The following defines an action extension that sets the chain's data with the passed argument:

require('chainy').create()
	.addExtension('mylog', 'action', function(currentValue){
		console.log(currentValue)
	})
	.addExtension('myset', 'action', function(currentValue, newValue){
		return newValue
	})
	.mylog()  // null
	.myset('some data')
	.mylog()  // some data

Actions accept the chain's current data as the first argument they receive.

Actions can accept an optional completion callback argument as the last argument. It doesn't have to be named in any special way, the only condition is that it isn't provided by the user when they call your action, as it is something we inject ourselves.

Actions can apply replace the chain's data with something else, by either returning the new data, or by giving the completion callback the new data.

Completion callbacks have the signature (err, newData). If err is defined, then the action is considered failed, and the chain will exit immediately, not running any remaining actions. If newData is defined, it will replace the chain's data. If multiple data arguments can be supplied, they will be applied to the chain's data as an array.

The following defines an action extension that uses a completion callback to set the chain's data after 10 seconds:

require('chainy').create()
	.addExtension('mylog', 'action', function(currentValue){
		console.log(currentValue)
	})
	.addExtension('myset', 'action', function(currentValue, newValue, next){
		setTimeout(function(){
			return next(null, newValue)
		}, 1000*10)
	})
	.mylog()  // null
	.myset('some data')
	.mylog()  // some data

Actions can accept as many or as little user arguments as they want, however a completion callback must be used if any of the arguments are optional.

In the above example, mylog accepts no user arguments, only the single currentValue argument that Chainy automatically provides to all actions. Whereas myset accepts the user argument newValue which is set to 'some data' in our example.

Utility Extensions

Utility extensions are for when you want to interact with the chainy instance rather than with its data. It's useful for extensions like progress bars and debug helpers that listen to the progress of the actions and output data as they complete.

Custom Extensions

Custom extensions are for when you want to alter the Chainy class, or inject extensions manually. It's useful for when you want an extension to have a custom name, or would like to load multiple extensions at once, or even to over-ride multiple chainy methods.

See the the autoinstall and autoload plugins for an example of the custom extension type.

Tips for extensions

  1. Keep the API as concise as possible, if you can utilise other extensions, then do it

  2. Plugin names can only contain lowercase letters, as anything else would be confusing to the user about which naming format to use, hot to use it, and it could possibly cause plugin naming conflicts

  3. You can create subclasses of Chainy by using the .subclass() method, this allows you to create a sublcass that you can then load plugins on, instead of loading plugins on a chain instance. You can do this via:

    var Chainy = require('chainy').subclass().require('set log')
    var chainyInstance = Chainy.create().set('some data').log()
  4. If you are exporting a Chainy class, be sure to .freeze() it, so that namespace pollution across modules does not occur.

Writing Extensions

Defining Extensions

Extensions are injected into the Chainy prototype using the Chainy.addExtension(...) method, which can accept the following argument combinations:

  • (name, type, method, options)
  • (method), method.extensionType, method.extensionOptions
  • ({name, type, method, options})

Essentially, these are all different ways of creating an extension object, which contains:

  • name - the name for your extension, for action and utility extensions, this will be the location of where your method will be injected

  • method - the synchronous or asynchronous method for your extension

  • type - the extension type

  • options - an object of options applicable to the extension, for now this is only for internal use only

Action Execution

Action extensions execute in exactly the same way as .action(method, options) does. For instance, this action code:

require('chainy')
	.set([1, 2, 3])
	.action(function(value, next){
		this.create().set(value).map(function(item){
			return value*5;
		}).done(next)
	})
	.log()  // [5, 10, 15]

Can be converted to this action extension code:

require('chainy')
	.addExtension('x5', 'action', function(value, next){
		this.create().set(value).map(function(item){
			return value*5;
		}).done(next)
	})
	.set([1, 2, 3])
	.x5()
	.log()  // [5, 10, 15]

The context of the extension's method (what this means) is set to the chain that the extension's method was called on. In the above example, this.create() creates a sub-chain on the original parent chain. Sub-chains are very useful for performing nested operations.

Just like with the .action() call, you can also accept arguments into your action extension:

require('chainy')
	.addExtension('x', 'action', function(value, multiplier, next){
		if ( multiplier == null )  return next(new Error('no multiplier specified'))
		this.create().set(value).map(function(item){
			return value*multiplier;
		}).done(next)
	})
	.set([1, 2, 3])
	.x(10)
	.log()  // [10, 20, 30]

One may wonder how action extensions can execute in order. It's because when you execute an action call, it actually adds the action to a TaskGroup runner (aka a queue), which then executes order with the chain's concurrency (defaults to 1 — aka serial execution — aka one at a time). TaskGroup runners are great because they can also capture errors that occur, and pause all remaining actions, while skipping on over to our done completion callback with the error that occured. This allows our chain to operate safely:

require('chainy').create().require('set')
	.addExtension('oops', 'action' function(){
		throw new Error('something went wrong!');
	})
	.set('one')
	.oops()
	.set('two')
	.done(function(err, data}){
		if ( err )  console.log('error:', err.stack or err)
		console.log(data)  // one
	})