dispatch:emissary

v0.11.2Published 10 years ago

This package has not had recent updates. Please investigate it's current state before committing to using it in your project.

Emissary

Emissary is a flexible, scalable notifications framework for Meteor.

What's it do?

In a nutshell, Emissary does the following:

  • Configure transports to deliver messages of a certain type (e.g. "sms" or "email")
  • Use a message queue (vsivsi:job-collection) to defer the work (sending the message) to any number of worker servers
  • Handle retry/fatal error logic for transport-specific error types

Basic Concepts

The Queue

Emissary uses vsivsi:job-collection under the hood to queue up messages to be sent using a transport. The Job class from that package is wrapped in an EmissaryJob to provide additional functionality specific to Emissary.

To queue messages to be sent, you can use Emissary.queueTask(), like so:

1Emissary.queueTask('<message type>', {
2  bodyTemplate:'<handlebars template>',
3  subjectTemplate:'<optional handlebars template (depends on transport)>',
4  templateData:{'<data to pass to templates>'},
5  transportConfig:'<format depends on message type>',
6  recipient:'<arbitrary recipient data of any type>'
7});

Message Types

Types are defined using Emissary.registerType. Transports work messages of a certain type. The types determine the format and data type of the to property when running queueTask.

There are four built-in message types:

sms

1{
2  // phone number
3  transportConfig:{
4    to:String
5  }
6}

email

1{
2  // email address
3  transportConfig:{
4    to:String
5  }
6}

webhook

1{
2  transportConfig: {
3    headers: Match.Optional(Object),
4    url: String,
5    method: Match.OneOf('GET', 'POST', 'PUT', 'DELETE', 'PATCH'),
6    basicAuth: Match.Optional(String),
7    expectStatus: Match.Optional(Number)
8  }
9}

push

1{
2  transportConfig:{
3    to:String,
4    badge:Match.Optional(Number),
5    payload:Match.Optional(Object)
6  }
7}

Webhook Endpoints

Some transports (e.g. those relying on third-party APIs) can not know the status of the message synchronously after calling the API. For example, the Twilio API sends back a status of "queued", and then later hits a designated webhook for status updates. Emissary makes it easy for transports to register webhook endpoints to accommodate these asynchronous processes.

If you use a transport that needs webhook support, you must simply call Emissary.enableWebhooks on a server exposed to the public (of course, this must happen after you define and register your transports just as you do in your workers). It is best practice to define and register all of your transports in a shared package, and then you can run Emissary.enableWebhooks() on your webhook endpoint server and Emissary.workQueue() on your worker server(s).

You must run Emissary.setRootUrl('<your application URL>'); in order for webhooks to work. This is the URL of the webhook, so in both your workers and the webhook server it should be the same.

Transports

Transports are separate packages that send messages of a certain type. For example, the Twilio transport sends messages of type sms using the Twilio API.

Creating your own Transport

It's simple to create your own transport. The transport is responsible for registering itself as a worker on the Emissary job queue, so as long as the message type is registered using Emissary.registerType, you can add a worker function for that type.

When a message is read from the queue, the worker function is executed with the EmissaryJob as its single argument. The job exposes several useful functions:

  • job.done(err) - complete the job. If err is defined, it will be considered a failure. If you just run job.done() it will be completed successfully. You can also pass an Emissary.FatalError instance to job.done, in which case the job will be failed fatally (meaning, it will not be retried).
  • job.log(level, msg, data) - log an arbitrary message about the job
  • job.getInfo() - return the data from the vsivsi:job-collection job document, including properties like status and runId
  • job.getMessage() - return the data passed to Emissary.queueTask

You can also make use of job.handleResponse() to automatically run job.done() with the appropriate error type (or lack thereof) based on a common "response" format. This is useful if your transport uses an external API, so you can translate the API response into the common response and then pass it to this function.

1{
2  // Set to false to signify an error, true to signify "ok/continue"
3  ok:Boolean,
4
5  // Was the message delivered successfully?
6  done:Boolean,
7
8  // If there was an error, what was it?
9  error:String,
10
11  // Emissary.ERROR_LEVEL.NONE|MINOR|FATAL|CATASTROPHIC
12  errorLevel:Number,
13
14  // Current status of the message (specific to the transport)
15  status:String
16
17  // If there was a fatal error, how should it be resolved?
18  resolution:String
19}

A fatal or catastrophic error will emit a "turnOff" event on the global Emissary object, signaling that notifications of that type should no longer be sent to that specific recipient until some action is performed. The action can be defined by the resolution property of the response. A minor error results in the message being retried. When done is true, the process is considered a success.

Router

You can use the emissary-router package to automatically queue notifications to be sent based on certain events. It is completely configurable and easy to use.