select-when
v0.1.9
Published
nodejs library to create expressions that pattern match over an event stream
Downloads
25
Readme
select-when
This javascript library makes it easy to create rules that pattern match on event streams.
Rationale
It's based on the ruleset pattern and event expressions of the Kinetic Rule Language (KRL). Read more rational here, and look into pico-engine if you want to run KRL code.
Declarative Event Expressions
Describe event patterns you want to select on.
For example:
- When aaa or bbb signals
or(e("aaa"), e("bbb"))
- When aaa comes after bbb
after(e("aaa"), e("bbb"))
- When any 2 of these 3 events happen within 1 second
within(1000, any(2, e("a1"), e("a2"), e("a3")))
Organize code and execution into Rulesets
Create a set of rules to run serially in the order they are declared. This makes it easy for programmers to understand their program and reason about ordering while still building in the asynchronous javascript environment.
Event Anatomy
Events in this system are simple json objects that have 4 parts. The domain
, name
, data
and time
.
interface Event<DataT> {
// The domain/namespace of the event, this is optional
domain?: string;
// The name of event, required
name: string;
// Payload data of any kind to go with the event
data?: DataT;
// a unix timestamp, number of milliseconds since Jan 1, 1970 UTC
time: int; // defaults to Date.now()
}
One can use strings as a shorthand for representing events.
"aaa" { domain: null, name: "aaa" }
"bbb:ccc" { domain: "bbb", name: "ccc" }
Example
import { SelectWhen, e, or, then } from "select-when";
let rs = new SelectWhen();
// KRL: select when hello:world
rs.when(e("hello:world"), function(event, state) {
console.log("rule 1 ->", event);
});
// KRL: select when hello:world or (*:a then *:b)
rs.when(or(e("hello:world"), then(e("a"), e("b"))), function(event, state) {
console.log("rule 2 ->", event);
});
rs.send("hello:world");
// rule 1 -> { domain: 'hello', name: 'world', data: null, time: 1541... }
// rule 2 -> { domain: 'hello', name: 'world', data: null, time: 1541... }
rs.send("a");
rs.send("b");
// rule 2 -> { domain: null, name: 'b', data: null, time: 1541... }
API
rs = new SelectWhen()
Create a new ruleset.
rs.when(Rule | StateMachine, body)
Call the body function when a given rule or state machine matches an event.
The body
is a function(event, state){}
that runs when the rule matches. It can also be async (return a promise).
rs.send(event)
Send an event to be processed by the ruleset. This returns a promise that resolves when all the rules have finished processing. Rules process serially in the order they are declared. Events sent are json objects or the string shorthad. (See the Event Anatomy section above.)
rs.getSaliance()
Returns an array of { domain: string, name: string }
that are salient for the ruleset. "*"
means any.
rule = new Rule()
Create a rule.
let rule = new Rule();
// set the initial state, under the hood this will go through Object.freeze
rule.state = {};
// set which domain/name patterns that this rule will care about
// by default all events are salient
rule.saliance = [
{ domain: "*", name: "aaa" }, // all events with name = "aaa"
{ domain: "bbb", name: "*" } // all events with domain = "bbb"
];
rule.matcher = function(event, state) {
// This function is called on all salient events.
// Return whether or not the event matches, and the new state.
// The state is similar to a memo in a reducer function.
// NOTE: this function can also be async (i.e. return a promise)
return {
match: true,
state: Object.assign({}, state, { some: "change" })
};
};
// Ask the rule to determine if an event matches, this will also update rule.state
rule.select(event).then(function(didMatch) {
if (didMatch) {
// do something
}
});
Event Expressions
These functions create StateMachine's or Rules that can be passed into rs.when(..
e(str, matcher?)
This creates a basic state machine to match events. This is the basic building block for all event expressions.
str
- A salient event pattern (see examples below)matcher
- An optional matcher function (seerule.matcher
for more info)
"bbb:ccc" { domain: "bbb", name: "ccc" }
"bbb:*" { domain: "bbb", name: "*" }
"aaa" { domain: "*" , name: "aaa" }
"*:*" { domain: "*" , name: "*" }
For example:
rs.when(e("aaa:*"), function(event, state) {
// run this on all events with domain "aaa"
});
or(a, b)
A state machine that matches when a or b
matches.
and(a, b)
A state machine that matches when a and b
matches.
before(a, b)
A state machine that matches when a before b
matches.
then(a, b)
A state machine that matches when a then b
matches, with no interleaving salient events.
after(a, b)
A state machine that matches when a after b
matches.
between(a, b, c)
A state machine that matches when a
comes between
b
and c
.
notBetween(a, b, c)
A state machine that matches when a
comes not between
b
and c
.
any(n, ...a)
A state machine that matches any n
of the events.
For example: any(2, e("a"), e("b"), e("c"))
a
b // match
a
z
c // match
b
a // match
count(n, a)
A state machine that matches after n
of a
's have matched.
For example: count(3, e("a"))
a
a
a // match
a
a
a // match
a
z
z
z
a
a // match
repeat(n, a)
The same as count
except once it matches, it will always match on a
For example: repeat(3, e("a"))
a
z
a
z
z
a // match
a // match
z
a // match
a // match
within(timeLimit, a)
A rule that will reset the statemachine a
when the time
has expired.
timeLimit
is be the number of milliseconds ms
or a function that returns the time limit (event, state) => ms
For example: within(1000, count(3, e("a")),
a
a
// wait 60 minutes
a // the machine reset so we are back to the 1st event
a
a // match
NOTE: It uses the time
on the event object, not the current execution time.
License
MIT