node package manager

Introducing npm Enterprise add-ons. Integrate third-party dev tools into npm…

pager

JavaScript rule engine without the fuss

Pager

Say you want to email a user for his birthday, or when his visa card expires or maybe if his last post has been shared a 100 times. Those events are not connected to this user's actions, they rely on the passing of time or someone else behavior. This may result in chunky code out of context. Know what? You've just got a message from Pager.

Pager is a lightweight rule engine which associate a set of immediate or postponed rules to actions.

Installation

npm install pager

Usage

When something happened (ex. a post is liked) and you want to check some rules and trigger some actions accordingly, send a message to a Pager instance with the event data (ex. a card expiry date). Immediate rules (if any) will be evaluated and, if they all match, pager's acknowledge method will be invoked. This do not trigger any actions yet.

To trigger the actions, call pager's process method with the task returned by acknowledge. Postponed rules (if any) will be evaluated then, and, if they all match, the actions will be triggered.

As you may have guessed, each task has a date attribute and the process method will only evaluate postponed rules if this date is in the past.

Passing the acknowledge tasks to the process method is up to you, because you may choose to do so every minute, hour, or immediately, depending on your needs. You may also store those tasks in a cache, a db, or not at all.

Before using a Pager instance, it's flow has to be configured:

  • subscribe to every message it will handle (ex. a card expiry)
  • gather each set of multiple Rule + Action under a group named channel (to be referenced to later)
  • list all Rule this subscription should comply and/or reject to trigger its Action
  • list all Action to trigger if every Rule match
var Pager = require('pager'),
    pager = new Pager();
 
pager.subscribe('Card expiry')
    .channel('Invite user to renew visa')
    .comply(new Pager.Rule({
        match: function(context) {
            return new Date(context.expiry) < new Date();
        }
    }))
    .trigger(new Pager.Action({
        fire: function(context) {
            console.log('Please, add a new visa');
        }
    }));

Once configured, a pager can capture a message and its context (ex. a card expiry date), then, it will acknowledge if the context matches the associated rules. Actions are not triggered yet.

pager.message('Card expiry', {expiry: '2015-12'});
 
pager.acknowledge(function(task) {
    // trigger the actions, can be stored in a cache to be processed later 
    pager.process(task);
});

Rules can be verified immediately on a message (and acknowledge if match) or postponed to it (and trigger if match). On initializing, calling after or every with a period defers rules chained after this call.

pager.subscribe('Card expiry')
    .channel('Invite user to deposit')
    .comply(immediateRule)
    .after(5, 'minutes')
    .comply(postponedRule)
    .trigger(action);

A message to this pager will be acknowledge if the immediateRule immediately matches, and, if so, will return a task with a date 5 minutes in the future. Then, when you process this task once its date is in the past, the postponedRule will be matched, and, if so, the actions will be triggered.

The every method follows the same pattern, but additionally updates the task date with the configured period, ready to be processed again later. It also offers a mechanism to avoid repetitive triggers of the same action (ex. “please, change your visa”). A flag is set on the task — null in other cases — here sets as 'available' until its actions has been triggered. Then, the flag is toggle to 'triggered'. A flag set to 'triggered' prevent any action. This flag is turned back to 'available' only when one (or more) postponed rules do not match, ready to be triggered on the next process if they match again.

Documentation

var rule = new Pager.Rule({
    match: function(context, helpers) {
        return new Date(context.expiry) < new Date();
    }
});

[Initializing] A rule can be immediate or postponed (depending on how it's initialized in a channel). Immediate rule are evaluated on a message, and create a task if match. Postponed rules will be evaluated on a process, and trigger actions if match.

  • Should return a boolean or a promise of a boolean
  • Gets the context provided in the message call and the helpers provided in the pager creator
var action = new Pager.Action({
    fire: function(context, helpers) {
        console.log('Account need a deposit');
    }
});

[Initializing] An action is a piece of code to execute, when rules match.

  • May return a value or the promise of a value (if process return is used)
  • Gets the context provided in the message call and the helpers provided in the pager creator.
var task = {
    channel: 'Invite user to deposit',
    event: 'Card expiry',
    context: {expiry: '2015-12'}, // data provided to the message method 
    date: ..., // date object since when this task can be processed 
    flag: ... // flag [null, 'available', 'triggered'], used for forever task 
}

A task is returned by the acknowledge method and awaited by the process method. Its date represents the moment since when this task can be processed (maybe in the future). Its flag is used for forever task (using every method set it to 'available'), default to null. It may be changed manually (with care) if this logic is not desired.

var middleware = function(context, helpers) {
        context.vat = helpers.vat(context.country);
    }
});

[Initializing] A middleware if a function that can update the context.

  • May return a promise
  • Gets the context provided in the message call and the helpers provided in the pager creator.
var pager = new Pager(helpers);

[Initializing] Create a pager with a helpers object that will be given to every rule, action and middleware.

pager.subscribe(event)

[Initializing] Create a subscription to a given event name. Will be invoked though message method with the same event name. Can be chained (returns itself).

var channel = pager.channel(name)

[Initializing] Create a channel — a group of rules & actions — and returns it. A subscription may contains multiple channels. The channel name will be used in the acknowledge callback task value.

pager.message(event, context)

Apply the context to the event subscribed immediate rules. Will eventually trigger the acknowledge callback if all immediate rules match. Return a promise (mostly for test purpose).

pager.acknowledge(callback)

Declare a callback to invoke when a message is invoked and all rules match. The callback argument is a task with attributes {channel, event, context, date, flag}. The date equals the moment since when this task can be processed. The flag helps the every to not trigger action every time.

See acknowledge

pager.process(task)

Evaluate postponed rules and trigger the actions if they match. The task attributes should includes {channel, event, context, date, flag}, the object can be a db wrapper or anything, only flag and date attributes may be updated here. Return a promise (mostly for test purpose).

channel.comply(rule, ...)

[Initializing] Declare a rule (or a list of rules) to match on this channel for the given context. Immediate rule if after or every wasn't called, postponed rule otherwise. Can be chained.

channel.reject(rule, ...)

[Initializing] Declare a rule (or a list of rules) to not match on this channel for the given context. Immediate rule if after or every wasn't called, postponed rule otherwise. Can be chained.

channel.trigger(action, ...)

[Initializing] Declare an action (or a list of rules) to trigger on this channel for the given context. Can be chained.

channel.after(number, unit)

[Initializing] Change following comply and reject from immediate rules declaration (default) to postponed rules declaration. The date of the acknowledge callback value will be incremented by this. Unit default to 'seconds', can also be 'minutes', 'hours', 'days'. Can only be called once per channel (and without every). Can be chained.

channel.every(number, unit)

[Initializing] Same as after but will acknowledge the task after process. Basically, it creates a loop. Can only be called once per channel (and without after). Can be chained.

channel.then(middleware, ...)

[Initializing] Declare a function that will be invoked for the given context and can update it (ex. add a value for further usage). Can be chained.

channel.dedup(function(context) { return ...})

[Initializing] Declare a function that will be invoked for the given context and will add a dedup attribute to each returned task. This help to spot different tasks that should be considered similar — and avoid doing twice an action that don't need to. Can be chained.

channel.channel(name)

[Initializing] Returns a new channel. See pager.channel for more.