superglue
Version:
Sticky mediator that promotes loose-coupling and makes app-flow clearer.
293 lines (197 loc) • 11.4 kB
Markdown
Superglue
=========
[](https://tldrlegal.com/license/mit-license "MIT License")
[](https://travis-ci.org/davidchubbs/superglue)
[](https://david-dm.org/davidchubbs/superglue)
[](https://npmjs.org/package/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:
```bash
npm install superglue
```
Documentation
-------------
Superglue *Publishers* are for triggering a **sequence of tasks**, while *Subscribers* are for registering logic to handle that task.
```js
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.
```js
// 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.
```js
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.
```js
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).
```js
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.
```js
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.
```js
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](#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.
```js
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.
```js
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.
```js
// 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:
```js
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
-------
[The MIT License](http://opensource.org/licenses/MIT)
Copyright (c) 2014 David Chubbuck