UNPKG

superglue

Version:

Sticky mediator that promotes loose-coupling and makes app-flow clearer.

293 lines (197 loc) 11.4 kB
Superglue ========= [![License](https://img.shields.io/badge/license-MIT-317BF9.svg?style=flat)](https://tldrlegal.com/license/mit-license "MIT License") [![Build](https://img.shields.io/travis/coderaiser/minify/dev.svg?style=flat)](https://travis-ci.org/davidchubbs/superglue) [![Dependency Status](https://david-dm.org/davidchubbs/superglue.svg?style=flat)](https://david-dm.org/davidchubbs/superglue) [![NPM version](https://img.shields.io/npm/v/superglue.svg?style=flat)](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