Skip to content

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

License

Notifications You must be signed in to change notification settings

davidchubbs/superglue

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

40 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Superglue

License Build Dependency Status NPM version

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 in the order they're registered (due to context, explained shortly). Quick note: if they share the same task name, they can be related but should not depend on one another, so order should be irrelevant; if order is significant because they depend on one another, use a separate task name.

// 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 ContextErrors 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

The MIT License

Copyright (c) 2014 David Chubbuck

About

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

Resources

License

Stars

Watchers

Forks

Packages

No packages published