madul v1.7.0
Madul
TL;DR
PLEASE NOTE
Madul is written in and intended to be used in CoffeeScript.
Installation
npm install --save madulUsage
import Module from 'madul'
class Caller extends Module
# Can be core node modules, third party modules, or project files
deps: [ 'fs', 'dance', 'drink', 'phone' ]
# $ methods are all executed exactly once during madul initialization
$go_to_club: (done) ->
@dance()
.then => @drink()
.then done
# All methods not starting with an underscore are wrapped, with
# state callbacks passed as the last arguments to all invocations
maybe: (person, done, fail) ->
# Items in the deps array get assigned as instance properties
@fs.readFile 'contacts', 'utf8', (err, numbers) =>
if numbers.split('\n').includes person
self.phone.dial(person).then done
else
fail "There's always next Friday ..."What is madul?
Madul is a simpe, fun, testable way to write performant, maintainable, async code.
It's intended to be the base of an inheritence hierarchy - think: Object in Java.
Overview
There are several benefits to Madul:
- Maduls are highly testable :: Dependency loading and initialization are handled by Madul. Dependencies are attached to a madul as properties, making them very easy to swap out for testing.
- Async dependency support ::
requireonly works synchronously and AMD adds a lot of boilerplate. Madul supports both sync and async depencnedy loading via a single, boilerplateless mechanism (specifying dependencies as an array of strings to thedepsproperty). - Simple, unobtrusive async behavior :: Madul takes all the work out of making methods async. All methods that don't start with an underscore are wrapped in a Promise (methods that start with a
$are 'initializers' and executing during initialization). Callbacks for success/completion, error/failure, and update/progress states are passed as the last three arguments to all wrapped methods. - Logging made easy :: Madul handles logging via EventEmitter2 and events. More on that below ...
- Clean, understandable, fast code :: All the pieces of Madul come together to produce code that is compact, easy-to-understand, fun to write, easy to test, easy to monitor, and performant as runtime.
Logging
Logging deserves special attention as it may seem a bit odd at first.
Madul has no direct support for logging. Instead, Madul provides a simple mechanism to fire and consume events. Logging is just one way to handle consumed events.
Example
import Madul from 'madul'
Madul.LISTEN '**', (...args_passed_to_FIRE) =>
console.log(@event, args_passed_to_FIRE)The above will log all events to the console.
Events
There are three distinct types of events Madul supports:
$= Internal events intended to be used by Madul for debugging. Madul lifecycle events (initialization, dependency registration, etc) are of this type.!= Error events.@= Madul instance events. They are fired by Madul when a method is invoked and when state callback methods (success/completion, error/failure, update/progress) are executed. They are also the event type fired when a madul calls itsfiremethod.
The format for an event name is as follows:
{type}.{Madul class name}.{lifecycle stage/method name}.{lifecycle stage/additional specifiers}Examples
Some example event names (for a hypothetical DB madul) are:
$.DB.request_instancefired whenevernew DB().initialize()is called!.DB.connection.failurefired if the DB cannot be connected to@.DB.get_id.invokefired whenself.db.get_id()is called@.DB.get_id.resolvefired whenself.db.get_id()successfully completes
Events API
There are two global event API methods:
Madul.LISTEN(event_name, callback)-> This static method allows you to specify any event you'd like to consume, across all maduls.Madul.FIRE(event_name, args)-> This is the static method used to actually fire all events. NOTE This method does not enforce any formatting rules for event names - meaning you may fire events that are not consumable as they do not follow the Madul event naming conventions. Please use this method carefully.
Additionally, there are three instance level event API methods. The event_name you pass to these methods is appended to the @.{Madul class name}. that gets passed to Madul.LISTEN/Madul.FIRE.
listen(event_name, callback)=> This method is a convenience wrapper limiting the scope of theMadul.LISTENmethod to just your maduls events.fire(event_name, args)=> This method wrapsMadul.FIRE, properly formatting the event name to specify it as a madul-level event - guaranteeinglistenwill work as expected.warn(event_name, details)=> This method fires a madul-level error event.
NOTE Event names must me dot . delimited.
Built-in events
The following are all the built-in events:
$.{Madul}.request_instanceFired every timenew {Madul}().initialize()is invoked$.{Madul}.initilizeFired exactly once for a Madul - the first timenew {Madul}().initialize()is invoked$.{Madul}.{method}.wrapFired once for each method as it gets wrapped$.{Madul}.dependency.registerFired when a loaded dependency is added as a property to a madul$.{Madul}.initializedFired exactly once for a Madul; when it has been successfully initialized@.{Madul}.{method}.invokeFired every time a wrapped method is called@.{Madul}.{method}.resolveFired every time thedone(first) state callback is invoked@.{Madul}.{method}.rejectFired every time thefail(second) state callback in invoked@.{Madul}.{method}.updateFired every time theupdate(third) state callback is invoked!.{Madul}.file-not-foundFired if a dependency cannot be found during initialization
The invoke, resolve, reject, and update madul-level events all receive the following information:
uuidA universally unique identifier for the method invocationargsThe arguments passed to the method invocation/state callbacktimestampA microsecond precision timestamp (represented as an integer) for when the event occured
User created events
You can create new madul-level event types simply by passing dot-delimited strings to fire.
You can also create entirely new global event types by passing dot-delimited strings to Madul.FIRE. Please be very careful when doing this.
Example
To configure a madul to log its events to the console you'd do the following:
import Madul from 'madul'
class Example extends Madul
$setup_logging_for_everthing: (done) ->
# This will send any event for Example to the console
@listen '**', => console.log this.event, arguments
# Invoking state callbacks is not required, but it is good practice
done()
$setup_just_invocation_logging: (done) ->
# This will only send invoke events to the console
@listen '*.invoke', (arg) => console.log "#{this.event}(#{arg})"
# Output would look like: @.Example.do_somthing.invoke(example arg)
# NOTE: This would not log resolve, reject, or update events
done()
do_something: (arg, done, fail, update) ->
# Do things
@fire 'did_something.awesome', arg
done()
do_something_else: (done, fail) ->
# DO ALL THE THINGS
done()Dependencies
There's a bit to go over when it comes to dependencies.
Dependencies are specified using strings formatted to a spec: the dependency_spec. A dependency_spec has several properties:
search_rootSpecifies the root of the file path to search when loading a dependency.nameThe actual name of the dependency. For a core node module or thrid party module it's the argument passed torequire.aliasA more convenient/meaningful way to refer to a dependency.initializerThe method to execute when the dependency has been hydrated.prerequisitesA list of other dependencies that must be hydrated and initialized before executing a dependency'sinitializer.
Aside from name all spec properties are optional.
An example of a dependency_spec with all properties would look like this:
example#dependency -> dep = configure:other_dep,even_moreLet's step through each property.
search_root
The search_root precedes the # symbol in the example above. Maduls specify a search root to make loading files other than index.js ro whatever is specified as main in package.json easy.
A Madul specifies a search root like so (in index.js, for example):
Madul = require 'madul'
Madul.SEARCH_ROOT 'example', require.resolve '.'
class Example extends Madul
# Madul deps and methods
module.exports = ExampleThen, alongside Example, if there were a file named something_cool.js with the following contents:
Madul = require 'madul'
class SomethingCool extends Madul
# Madul deps and methods
module.exports = SomethingCoolAnother Madul would then specify it's dependency on SomethingCool as follows:
Madul = require 'madul'
class Foo extends Madul
deps: [ 'fs', 'path', 'example#something_cool' ]
# Madul methods
module.exports = FooDependencies are hydrated by recursively searching all files and folders in a search root - so you can structure you're Maduls however you'd like. To load them, client code only needs to know the file name of the Madul they actually want to load.
alias
An alias is a more convenient/meaningful way to refer to a dependency. If an alias is specified, it is the only way to refer to the dependency in code.
Madul = require 'madul'
class Foo extends Madul
deps: [ 'example -> ex' ]
do_something: (done) ->
@ex.foo() # Cannot use example for this dep, since it was given an alias
.then done
module.exports = Fooinitializer
A dependency initializer in a dependency_spec specifies the method on the dependant Madul to be invoked when the dependency has been hydrated.
Madul = require 'madul'
class Bar extends Madul
deps: [ 'example = configure' ]
configure: (done) ->
# Gets called automatically after example has been hydrated
module.exports = BarA dependency initializer is just a plain old Madul method.
prerequisites
preqrequisites is a comma-seperated list of dependendies that must be ready to use before calling a dependency initializer - specifically, all prerequisites' initializer methods must be called, in the order specified, before the dependency's initializer executes.
Madul = require 'madul'
class Baz extends Madul
deps: [ 'example = configure:args', 'arguments -> args = load_args' ]
load_args: (done) ->
# Do whatever needs to be done
configure: (done) ->
# CONFIGURE ALL THE THINGS
module.exports = BazThe order of execution above would be:
- Hydrate
exampleandargumentsin parallel. - Execute
Baz.load_args - Execute
Baz.configure
Initialization in depth
Calling new on a Madul class is not enough to get it into a usable state. Since constructors are synchronous Madul implements all its heavy lifting in the initialize method.
You will never need to override the initialize method. $ initializer methods exist specifically to save you from overriding initialize.
The steps to get a usable madul are detailed below:
import Example from './example'
new Example() # 1
.initialize() # 2
.then (madul) -> # 3
madul.execute_behavior() # 4
.then (output) ->
# Do something else- Madul constructors works just like any other constructor
initializewires up all the dependencies, wraps all the methods, and executes$initializers- The fully baked, ready-to-use madul is passed to the
initialize().thencallback - All wrapped madul methods return a promise
NOTE A Madul is only initialized once. All subsequent calls to initialize after the first simply return the already-initialized instance.
You'll likely never see or need to worry about this, as it's all handled behind the scenes for you, but if you ever need to initialize a madul outside of the deps array this may come in handy.
Bugs/feature requests
If you fix a bug - thank you! :heart_eyes: Please fork the repo, create a test, fix the bug, and submit a pull request.
If you find a bug feel free to open an issue - and please provide as much info as possible :blush:
Feature requests can be submitted as issues too.
Contributing
Contributions are welcome and appreciated! :metal: Please just fork, code, get passing tests, and create a pull request.