dispatch:emissary-router

v0.11.1Published 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-router

This package facilitates the decision process for turning an event liked "document created" into one or more notifications.

For example, with the appropriate configuration, the below code could end up queuing an SMS message to user #1, a push notification to user #2, and a webhook POST to a third-party integration.

1EmissaryRouter.emit('document created', {
2  someKey:'someVal'
3});

How it works

Configuration

This package uses the dispatch:configuration package under the hood to facilitate schema-agnostic, inherited configuration. The configuration schema specific to the router is defined, and the defaults are initialized, when you run EmissaryRouter.init. Here's an example (the properties are described below):

1Emissary.emit({
2  events:['<event 1>', '<event 2>'],
3  notificationTypes:[{
4    type:'push',
5    multi:false,
6    formatter:function(recipient) {
7      return {
8        userId:recipient._id
9      }
10    }
11  }, {
12    type:'sms',
13    multi:false,
14    formatter:function(recipient) {
15      return recipient.phoneNumber;
16    }
17  }, {
18    type:'email',
19    multi:true,
20    formatter:function(recipient, recipientConfig) {
21      return recipientConfig.conf.emailAddress;
22    }
23  }],
24  receivePreferences:[{
25    type:'always',
26    check:function(){
27      return true
28    }
29  }, {
30    type:'at night',
31    check:function(recipient) {
32      return someTimeFunction.isItNightTime(recipient.timezone);
33  }],
34  prefix:'myNotifications',
35  getPotentialRecipientsForEvent:function(){...},
36  retrieveEntity:function(entityType, entityId) {...},
37  generateTemplateData:function() {...},
38  transformJob:function(job) {...}
39});

events

These are the types of events that can possibly be sent. Because this package allows entity-level configuration on a per event basis, they must be defined and added to the schema here. For example:

1events:['userLoggedIn', 'todoListCreated', 'todoItemCompleted']

IMPORTANT! - event names are used as keys in the configuration document in MongoDB. As such, they cannot have dots/periods in them nor can they start with a dollar sign.

notificationTypes

Notification types in this package should match their type properties one-to-one with the types registered with Emissary.registerType. The built-in Emissary types (sms, email, push, webhook) are already registered, with webhook being a multi:true notification type (see below).

formatter Function

This function returns the value that is passed as the to property to Emissary.queueTask. Obviously this should be something that the workers on the other side of the equation can use. It receives four arguments:

  1. recipient - the value returned by your retrieveEntity function
  2. recipientConfig - see below
  3. eventName - the name of the event sent with EmissaryRouter.send
  4. eventData - the data passed along with the event name to EmissaryRouter.send

The recipientConfig argument is the combined type-level configuration AND type/event-level configuration (more specific). The type/event-level configuration overrides individual keys in the type-level configuration. For example, given the following:

1{
2  "notifications":{
3    "sms":{
4      "config":{
5        "foo":"bar",
6        "nested":{
7          "other":"field"
8        }
9      },
10      "events":{
11        "event1":{
12          "config":{
13            "nested":{
14              "other2":"field2"
15            }
16          }
17        }
18      }
19    }
20  }
21}

...the value of recipientConfig will be:

1{
2  "foo":"bar",
3  "nested":{
4    "other":"field",
5    "other2":"field2"
6  }
7}
The multi option

The multi property defines if there could potentially be multiple different configurations for this type. For example, you may want to use the email transport but have different configurations for "work email" vs "personal email". In this case, the value of recipientConfig.conf in the formatter function is the value for the particular configuration set that should be sent. Take, for example, the following configuration data:

1{
2  "notifications":{
3    "email":[
4      {
5        "when":{
6          "always":["event1"],
7          "daytime_only":["event2"]
8        },
9        "events":{
10          "event1":{
11            "templates":{...},
12            "timing":{...},
13            "config":{
14              "emailAddress":"email1@test.com"
15            }
16          }, 
17          "event2":{
18            "templates":{...},
19            "timing":{...},
20            "config":{}
21          }
22        }, 
23        "config":{
24          "emailAddress":"default@test.com"
25        }
26      }, {
27        "when":{
28          "night_only":["event3"]
29        },
30        "events":{
31          "event3":{
32            "templates":{...},
33            "timing":{...},
34            "config":{}
35          }  
36        },
37        "config":{
38          "emailAddress":"other@test.com"
39        }
40      }
41    ]
42  }
43}

With this configuration, if you send event1 and this recipient is returned by the getPotentialRecipientsForEvent function, it will send an email to "email1@test.com". If you send event2 during the day, it will send an email to "default@test.com" using the configuration in email[0].events.event2, since the config is inherited and deep-extended. Likewise, if you send event3 at nighttime, it will send an email to "other@test.com".

Important: there is currently one caveat with using arrays with the dispatch:configuration package - arrays are extended using full replacement. Meaning, if the above recipient can inherit from another entity's configuration, then it will not inherit any of the elements in the notifications.email array unless its own notifications.email array is not defined.

receivePreferences

Your recipients may want to receive different messages for different events, well, differently. EmissaryRouter lets you define one or more "receive preference" types with a function to determine if that type is currently valid. Those functions are then used internally by the router to determine if the recipient should be sent a message for a particular event.

For example, you may want to send different message types for different events based on the time of day (let's say "always", "day", and "night" are the options). In this case, you'd pass those three options to EmissaryRouter.init:

1EmissaryRouter.init({
2  (...)
3  receivePreferences:[
4    {
5      type:'always',
6      check:function(){ return true;}
7    }, {
8      type:'night',
9      check:function(recipient) {
10        var timezone = recipient.timezone;
11        var time = getCurrentTimeInTimezone(timezone);
12        return time.hour >= 20 || time.hour < 5;
13      }
14    }, {
15      type:'day',
16      check:function(recipient) {
17        ver timezone = recipient.timezone;
18        var time = getCurrentTimeInTimezone(timezone);
19        return time.hour >= 5 && time.hour < 20;
20      }
21    }
22  ]
23  (...)
24});

The preferences key of the configuration, then, will look like this (let's say you have events ["event1", "event2"] and message types ["sms", "email"]):

1{
2  notifications:{
3    preferences:{
4      sms:{
5        day:['event1'],
6        always:['event2']
7      },
8      email:{
9        always:['event1', 'event2']
10      }
11    }
12  }
13}

In this case, this recipient will receive an SMS for event1 only if it's during the daytime, but will always receive an SMS for event2. Similarly, she will always receive an email for both event1 and event2, regardless of the time of day.

prefix

The prefix property allows you to configure the key on which all emissary-specific configuration is stored via the dispatch:configuration package. It defaults to notifications.

getPotentialRecipientsForEvent

See Deciding who to potentially notify - this is the function that does the decision logic. There are three basic functions this package performs: deciding who to potentially notify when an event occurs, deciding how to notify them, and queuing those notifications with Emissary.

retrieveEntity

This is a function used by the router to fetch the entity document from the entity tuple (["<entity type>", "<entity id>"]). The return value is not used internally by the router, but is passed to several user-provided functions like the "to formatters" and the skipFilter function. For example:

1EmissaryRouter.init({
2  (...)
3  retrieveEntity:function(type, id) {
4    return myCollectionMap[type].findOne(id);
5  }
6  (...)
7});

generateTemplateData

This function is used to generate the templateData object written to the Emissary message queue (which is in turn used to fill out the templates using Handlebars). Similar to the getPotentialRecipientsForEvent function, there's nothing we can really do to abstract this functionality since it really depends on your application.

For example:

1EmissaryRouter.init({
2  (...)
3  generateTemplateData:function(eventName, eventData) {
4    if(eventName === 'listCreated') {
5      return {
6        list:ListCollection.findOne(eventData.id)
7      }
8    }
9  }
10  (...)
11});

transformJob

This is an optional function that you can use to modify the vsivsi:job-collection Job entity before it is written to the queue. See the job API reference for that package for more information (the Job is the single argument passed to the transform function and it expects the mutated (or unmutated) job in return).

skipFilter

This optional function lets you define which message types to skip given a recipient and an event. For example, you could use this as a failsafe to make sure that push notifications are only attempted to be sent to one specific type of user. In that case, the function should return ["push"] to skip push notifications, even if the configuration dictates otherwise.

For example:

1EmissaryRouter.init({
2  (...)
3  skipFilter:function(recipient, recipientConfig, eventName) {
4    if(recipient.type !== 'special type') {
5      return ['push'];
6    } 
7    return [];
8  }
9  (...)
10});

Decision Logic

Deciding who to potentially notify

From what we can tell, there's no good way to abstract this function out of your application code and into this package - there are just too many options for the decision logic. For that reason, the meat of this function comes from the [getPotentialRecipientsForEvent](#getPotentialRecipientsForEvent) function passed as a configuration option when running EmissaryRouter.init() .

That function is passed two arguments, the eventName and eventData ('document created' and {someKey:'someVal'} in the above example, respectively), and is expected to return an array of tuples (IE ['<entity type>', '<entity ID>'] readable by the dispatch:configuration package. For example:

1return [
2  ['user', '12345'],
3  ['user', '55555'],
4  ['integration_partner', '54321']
5];

In the above example, your application code has determined that based on the event and event data, user#12345, user#55555, and integration_partner#54321 should potentially be notified. The key here is the potentiality - the actual decision process for whether to actually send those entities a notification comes later.

Deciding whether to notify them

This decision is made based on the configuration for each potential recipient. The preferences configuration key provided during EmissaryRouter.init (see receivePreferences). For each recipient, the router reads the configuration preferences and uses the preference.check skipFilter functions to end up with an array of notification types to send, e.g. ["sms", "email"]

Deciding how to notify them

In this step, the recipient document is passed through the corresponding "to formatter" for each notification type, and the payload is constructed from that, the template data returned by the getTemplateData function, the templates defined in the template section of the configuration schema, and the timing (delay, timeout) defined in the timing section of the configuration schema.

These are written as jobs to the Emissary queue using Emissary.queueTask, and from there, your registered workers will take over.