/////////////////////////// // 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);