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}
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. Iferr
is defined, it will be considered a failure. If you just runjob.done()
it will be completed successfully. You can also pass anEmissary.FatalError
instance tojob.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 jobjob.getInfo()
- return the data from thevsivsi:job-collection
job document, including properties likestatus
andrunId
job.getMessage()
- return the data passed toEmissary.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.