Skip to content

Instantly share code, notes, and snippets.

@jhorman
Last active December 28, 2015 11:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jhorman/7493652 to your computer and use it in GitHub Desktop.
Save jhorman/7493652 to your computer and use it in GitHub Desktop.
///////////////////////////
// JAVASCRIPT RULE ENGINE
///////////////////////////
/*global define*/
define([
'lodash.noconflict'
], function (_) {
'use strict';
function Rules(options) {
var self = this,
rules = [],
facts = options && options.facts || {},
chain = {},
inFire = false,
queueFire = false;
this.rules = rules;
this.facts = facts;
function fire() {
// if a rule fire asserts more facts, queue fire until all of the
// current rules are processed.
if (inFire) {
queueFire = true;
} else {
inFire = true;
try {
_.each(rules, function (rule) {
var result = rule.condition(facts);
// only fire a rule if its condition result changes
if (result && result !== chain[rule.id]) {
self.fireRule(rule);
}
chain[rule.id] = result;
});
} finally {
inFire = false;
}
var shouldFire = queueFire;
queueFire = false;
if (shouldFire) {
fire();
}
}
}
this.fireRule = function (rule) {
rule.fire(this, facts);
};
this.add = function (newRules) {
rules = rules.concat(newRules);
_.each(rules, function (rule, index) {
rule.id = index;
});
return this;
};
this.fact = function (name, value) {
if (facts[name] !== value) {
var oldFacts = _.clone(facts); // probably needs to be a deep clone
facts[name] = value;
try {
fire();
return true;
} catch (e) {
// if asserting a fact throws, reset state
facts = oldFacts;
this.facts = oldFacts;
throw e;
}
}
return false;
};
// add the rules passed into the construct. we use ".add" since it
// id's the rules, which we need for tracking them in "chain".
if (options && options.rules) {
this.add(options.rules);
}
}
function resolveLeft(facts, left) {
return typeof(left) === 'function' ? left(facts) : facts[left];
}
function resolveRight(facts, right) {
return typeof(right) === 'function' ? right(facts) : right;
}
Rules.fact = function (name) {
return function (facts) {
return facts[name];
};
};
Rules.and = function (f1, f2) {
return function (facts) {
return f1(facts) && f2(facts);
};
};
Rules.or = function (f1, f2) {
return function (facts) {
return f1(facts) || f2(facts);
};
};
Rules.eq = function (fact, value) {
return function (facts) {
return resolveLeft(facts, fact) === resolveRight(facts, value);
};
};
Rules.neq = function (fact, value) {
return function (facts) {
return resolveLeft(facts, fact) !== resolveRight(facts, value);
};
};
Rules.gt = function (fact, value) {
return function (facts) {
return facts[fact] !== undefined && facts[fact] > value;
};
};
Rules.lt = function (fact, value) {
return function (facts) {
return facts[fact] !== undefined && facts[fact] < value;
};
};
Rules.gte = function (fact, value) {
return function (facts) {
return facts[fact] !== undefined && facts[fact] >= value;
};
};
Rules.lte = function (fact, value) {
return function (facts) {
return facts[fact] !== undefined && facts[fact] <= value;
};
};
Rules.setFact = function (name, value) {
return function (rules) {
rules.fact(name, value);
};
};
return Rules;
});
///////////////////////////////
// HTML5 Video Rules
///////////////////////////////
var rules = new Rules({
facts: {
readyState: 0,
duration: null,
lastDuration: null,
progressAmount: 0,
event: null
}
});
rules.add([
{
name: 'duration change',
condition: function (facts) {
return facts.duration !== facts.lastDuration;
},
fire: function (facts) {
console.log('durationchange ' + facts.duration);
rules.fact('lastDuration', facts.duration);
}
},
{
name: 'readyState gt 0',
condition: rules.gt('readyState', 0),
fire: function () {
console.log('loadedmetadata');
}
},
{
name: 'readyState gt 1',
condition: rules.gt('readyState', 1),
fire: function () {
console.log('loadeddata');
}
},
{
name: 'readyState gt 2',
condition: rules.gt('readyState', 2),
fire: function () {
console.log('canplay');
}
},
{
name: 'readyState gt 3',
condition: rules.gt('readyState', 3),
fire: function () {
console.log('canplaythrough');
}
},
{
name: 'duration set',
condition: rules.gt('duration', 0),
fire: rules.setFact('readyState', 1)
},
{
name: 'progress',
condition: rules.eq('event', 'progress'),
fire: rules.setFact('readyState', 2)
},
{
name: 'canplay',
condition: rules.eq('event', 'canplay'),
fire: rules.setFact('readyState', 3)
},
{
name: 'canplaythrough',
condition: rules.eq('event', 'canplaythrough'),
fire: rules.setFact('readyState', 4)
},
{
name: 'progressAmount >= duration',
condition: function (facts) {
return facts.progressAmount !== undefined && facts.duration !== undefined && facts.duration > 0 &&
facts.progressAmount >= facts.duration;
},
fire: rules.setFact('readyState', 4)
}
]);
// TEST, DURATION 1 should fire durationchange and loadedmetadata
rules.fact('duration', 1);
// TEST, SHOULD ONLY FIRE DURATION CHANGE
rules.fact('duration', 2);
// TEST, SHOULD ONLY FIRE DURATION CHANGE
rules.fact('duration', 3);
// TEST, SHOULD FIRE LOADEDDATA, CANPLAY, CANPLAYTHROUGH
rules.fact('progressAmount', 3);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment