superglue
v0.1.3
Published
Sticky mediator that promotes loose-coupling and makes app-flow clearer.
Downloads
12
Maintainers
Readme
Superglue
No dependencies.
Description
Superglue is interested in the organization of your application by promoting loosely coupled components and defining the order of their execution—or, application flow. It is inspired by a few different design patterns and attempts to consolidate some of their benefits. To understand better, see the examples below :)
Installation
Install superglue:
npm install superglue
Documentation
Superglue Publishers are for triggering a sequence of tasks, while Subscribers are for registering logic to handle that task.
superglue.subscribe("task1").then(function () { /* fired 1st */ });
superglue.subscribe("task2").then(function () { /* fired 2nd */ });
superglue.publish("task1", "task2");
As usual, multiple subscribers can be listening to the same task, which are fired one at a time (due to context, explained shortly), in no particular order.
// both subscribers listening to the same task,
// so both will be invoked when task is triggered
superglue.subscribe("task").then(...);
superglue.subscribe("task").then(...);
Publishers can also fire their own logic at any time during the sequence of tasks.
superglue.publish()
.events("task1", "task2")
.then(function () { ... })
.event("task3");
Superglue also allows Publishers to define the context, which is then accessible to every Subscriber via this
. This means updating the context will update the context for subsequent tasks.
superglue.subscribe("build-name").then(function () {
this.name = this.first + " " + this.last;
});
superglue.publish()
.context({ first: "Julie", last: "Chubbs" })
.event("build-name")
.then(function () {
console.log(this.name);
});
//=> "Julie Chubbs"
Sometimes you will only want to trigger subscriber logic if the context has a certain state. You can do this using the .filter()
method. If any functions passed into .filter()
evaluate to false
, the subscriber will be skipped (but the task will continue).
superglue.subscribe("build-name")
.filter(function () {
return typeof this.first === "string" && typeof this.last === "string";
})
.then( /* only triggered if .filter returns true */ );
Other times, invoking the subscriber logic will be mandatory but the logic may still depend on the context having a certain state. To require a particular state, use the .require()
method. If any functions passed into .require()
evaluate to false
, a ContextError
will be generated and the sequence of tasks will be stopped.
superglue.subscribe("build-name")
.require(function () {
return typeof this.first === "string" && typeof this.last === "string";
})
.then( /* if .require returns false, a ContextError is generated and this function is not invoked */ );
Superglue halts the sequence of tasks once an error occurs. Besides the ContextError
s generated by .require()
methods, you can also set errors yourself by returning errors (instanceof Error
) from your Subscriber or Publisher .then()
methods, at which point, superglue will skip all remaining tasks.
If you would like to catch and handle errors, pass in a function with the signature function (err)
into your publisher's .then()
method; currently, only Publishers can catch and handle errors.
superglue.subscribe("task1").then(function () {
return Error("here's what happened...");
});
superglue.subscribe("task2").then(function () {
// never invoked
});
superglue.publish("task1", "task2")
.then(function () { /* this one is skipped, since missing `err` argument */ })
.then(function (err) {
console.log(err.message);
});
//=> here's what happened...
See Advanced Error Handling for greater error handling control.
Express Example
Lets look at a more useful example, building on top of Express. Notice the convention of namespacing tasks with the :
character, which by doing so, allows you to use the .tasks(namespace, [task1, ...])
method.
superglue.subscribe("user:save:validate").then(function () {
if (!this.body.first || !this.body.last) {
return Error("First & last names are required");
}
});
superglue.subscribe("user:save:db").then(function () {
// add to db
});
superglue.subscribe("user:save:feedback").then(function () {
this.feedback = this.body.name + " was created successfully!";
});
...
app.post("/user", function (req, res) {
superglue.publish()
.context(req)
.tasks("user:save", ["validate", "db", "feedback"])
.then(function (err) {
if (err) {
res.send(422, err.message);
} else {
res.send(this.feedback);
}
});
});
By triggering task names, it becomes clearer what your application is actually doing by firing tasks with semantical names; for instance, in the example above we can see that we are using Express' req
object as the context, then validating & saving the incoming data, and then providing feedback to the user. Furthermore, the pub/sub influence of Superglue keeps your application decoupled and your code DRY. In essence, Superglue glues together your application components while providing a high-level view of how your application works.
While publishers define the flow of tasks fired, it can be useful to group tasks together on the subscriber side as well.
superglue.group("user:save").tasks("user:save", ["validate", "db", "feedback"]);
...
app.post("/user", function (req, res) {
superglue.publish()
.context(req)
// this fires "user:save:validate", "user:save:db", "user:save:feedback" for you
.event("user:save")
.then(...);
});
Advanced Error Handling
Superglue stores error constructors in the superglue.errors
object. You can add your own error constructors to the superglue.errors
object directly, or you can use the convenience method .addError(constructor, [name])
, which adds your error constructor to superglue.errors
.
When an error occurs, you can check what task created the error using the error's .failedOn
property.
// SpecialError constructor, extending Error
function SpecialError (msg) {
Error.call(this, msg);
...
}
SpecialError.prototype = Object.create(Error.prototype);
// add SpecialError to superglue.errors
superglue.addError(SpecialError);
superglue.subscribe("build-name")
.then(function () {
var SpecialError = superglue.errors.SpecialError;
if (!this.first || !this.last) {
return new SpecialError("missing name");
}
});
superglue.publish("build-name")
.then(function (err) {
// .name - if SpecialError was a "defined function"
// .failedOn - task name that created the error
if (err.name === "SpecialError" && err.failedOn === "build-name") {
// now we have a pretty good idea about what happened...
}
});
Please Note: If your constructor does not extend an Error (either as a parent, grandparent, or somewhere along your prototype chain), then it will be ignored when a task returns it.
API
This section will be fixed soon, as right now it's not very useful.
Subscriber API
superglue.subscribe(task1 [, task2, ...])
creates a fresh subscriber instance listening to whatever task names passed in.
superglue.subscribe(...).replace
flushes out any susbcribers & groups with the same task names, allowing this subscriber to replace the existing subscribers/groups. .replace
must be called immediately after subscribe()
in order to work properly.
superglue.subscribe(...).filter(filterFn, ...)
invokes the filterFn
and if the returned value is truthy, the subscriber's logic is also invoked.
superglue.subscribe(...).require(reqFn, ...)
invokes the reqFn
and if the returned value is truthy, the subscriber's logic is invoked; if the returned value is falsy, a ContextError
is generated and subsequent tasks are not triggered.
superglue.subscribe(...).then(fn, ...)
invokes the fn
. If errors are returned, subsequent tasks are not triggered.
.filter()
, .require()
, and .then()
can all be given multiple functions as their arguments.
Even though it is not required (except for .replace
), it is useful to order method invocation in the same order it is processed internally, which is:
superglue.subscribe("namespace:task")
.replace // if needed
.filter(function () {})
.require(function () {})
.then(function () {})
Group-Subscriber API
superglue.group(groupName1 [, groupName2, ...])
creates a fresh subscriber-group instance listening to whatever group-names are passed in.
superglue.group(...).replace
is identical to .subscribe().replace
.
superglue.group(...).events(name1 [, name2, ...])
will group all event/task names passed in.
superglue.group(...).event
alias for .events
.
superglue.group(...).tasks(namespace, tasks)
will prepend tasks with the namespace and then fire them when the group name is triggered. tasks
can either be a string if it is a single task name, or an array of task names.
superglue.group(...).task
alias for .tasks
.
Publisher API
superglue.publish([context, taskName1, ...])
creates a fresh publisher instance. If you pass in an object, it will be set as the context. If you pass in a string, it will be fired as a task name. You can pass in as many contexts and task names as you would like. However, the order is significant—if you invoke superglue.publish("task1", {name:"Julie"}, "task2")
, only task2
had the context with this.name
, since the context was set after task1
was triggered.
superglue.publish().context(cxt)
sets the cxt
object as the context for subsequent tasks fired. The context can be fired more than once if you desire to change the context for subsequent tasks.
superglue.publish().events(task1 [, task2, ...])
fires the tasks in the order provided.
superglue.publish().event
alias for .events
.
superglue.publish().tasks(namespace, tasks)
will prepend tasks with the namespace and then fire them. tasks
can either be a string if it is a single task name, or an array of task names.
superglue.publish().task
alias for .tasks
.
Remember that order is important. Tasks are triggered the moment their task-name is received, which means that tasks are fired in the order they are received, and contexts are only available to tasks fired after the context is set.
Road Map
Adding test coverage stats soon. Let me know what else you'd like to see!
Feedback
I would love feedback! Let me know what parts are confusing or what could be improved. Easiest way to discuss it is to submit an issue :)
License
Copyright (c) 2014 David Chubbuck