diff --git a/lib/agenda.js b/lib/agenda.js index 3028d40..39f4b3f 100644 --- a/lib/agenda.js +++ b/lib/agenda.js @@ -84,7 +84,7 @@ module.exports = declare(EventEmitter, { register: function (node) { var agendaGroup = node.rule.agendaGroup; - this.rules[node.name] = {tree: new AVLTree({compare: this.comparator}), factTable: new FactHash()}; + this.rules[node.name] = { tree: new AVLTree({ compare: this.comparator }), factTable: new FactHash(), noLoop: {} }; if (agendaGroup) { this.addAgendaGroup(agendaGroup); } @@ -157,11 +157,21 @@ module.exports = declare(EventEmitter, { if (activation) { this.getAgendaGroup(node.rule.agendaGroup).remove(activation); rule.tree.remove(activation); + delete rule.noLoop[retract.hashCode]; } }, insert: function (node, insert) { var rule = this.rules[node.name], nodeRule = node.rule, agendaGroup = nodeRule.agendaGroup; + if (nodeRule.noLoop) { + if (rule.noLoop[insert.hashCode]) { + rule.factTable.insert(insert); + return; + } + else { + rule.noLoop[insert.hashCode] = true; + } + } rule.tree.insert(insert); this.getAgendaGroup(agendaGroup).insert(insert); if (nodeRule.autoFocus) { diff --git a/lib/parser/nools/tokens.js b/lib/parser/nools/tokens.js index 3ac7080..3d9474c 100644 --- a/lib/parser/nools/tokens.js +++ b/lib/parser/nools/tokens.js @@ -94,6 +94,19 @@ var ruleTokens = { }; })(), + noLoop: (function () { + var noLoopRegexp = /^(noLoop|no-loop)\s*:\s*(true|false)\s*[,;]?/; + return function (src, context) { + if (noLoopRegexp.test(src)) { + var parts = src.match(noLoopRegexp); + context.options.noLoop = Boolean(parts[2]); + return src.replace(parts[0], ""); + } else { + throw new Error("invalid format"); + } + }; + })(), + "agenda-group": function () { return this.agendaGroup.apply(this, arguments); }, @@ -102,6 +115,10 @@ var ruleTokens = { return this.autoFocus.apply(this, arguments); }, + "no-loop": function () { + return this.noLoop.apply(this, arguments); + }, + priority: function () { return this.salience.apply(this, arguments); }, diff --git a/lib/rule.js b/lib/rule.js index 75a73d8..c473960 100644 --- a/lib/rule.js +++ b/lib/rule.js @@ -227,6 +227,8 @@ var Rule = declare({ this.name = name; this.pattern = pattern; this.cb = cb; + this.noLoop = options.noLoop; + if (options.agendaGroup) { this.agendaGroup = options.agendaGroup; this.autoFocus = extd.isBoolean(options.autoFocus) ? options.autoFocus : false; diff --git a/readme.md b/readme.md index aa8897c..5cc5c1b 100644 --- a/readme.md +++ b/readme.md @@ -43,6 +43,7 @@ Or [download the source](https://raw.github.com/C2FO/nools/master/nools.js) ([mi * [Structure](#rule-structure) * [Salience](#rule-salience) * [Scope](#rule-scope) + * [no-loop](#rule-no-loop) * [Constraints](#constraints) * [Custom](#custom-contraints) * [Not](#not-constraint) @@ -1008,6 +1009,32 @@ flow1 console.log(fired); //["Hello1", "Hello2", "Hello3", "Hello4"] }); ``` + +### No-Loop + +When a rule's action modifies a fact it may cause the rule to activate again, causing an infinite loop. Setting no-loop to true will skip the creation of another Activation for the rule with the current set of facts. + +```javascript +this.rule("Hello", {noLoop: true}, [Message, "m", "m.text like /hello/"], function (facts) { + var m = facts.m; + m.text = 'hello world'; + this.modify(m) +}); + +``` +Or using the DSL + +```javascript +rule Hello { + no-loop: true; + when { + m: Message m.name like /hello/; + } + then { modify(m, function() { + m.text = 'hello world' + }); +} +``` diff --git a/test/flow/noLoop.test.js b/test/flow/noLoop.test.js new file mode 100644 index 0000000..113744f --- /dev/null +++ b/test/flow/noLoop.test.js @@ -0,0 +1,91 @@ +"use strict"; +var it = require("it"), + assert = require("assert"), + nools = require("../../"); + +it.describe("no-loop", function (it) { + var fired; + + function Wrapper(n) { + this.n = n; + } + + it.should("not loop with option on", function () { + fired = []; + + return nools + .flow("noLoop", function () { + this.rule("ping", { noLoop: true }, [Wrapper, "w", "w.n < 5"], function (facts) { + var w = facts.w; + w.n++; + this.modify(w); + }); + }) + .getSession(new Wrapper(0)) + .on("fire", function (name) { + fired.push(name); + }) + .match() + .then(function(){ + assert.deepEqual(fired, [ 'ping' ]) + }); + }); + + it.should("avoid only self-recursions", function () { + fired = []; + + return nools + .flow("noLoop2", function () { + this.rule("ping", { noLoop: true }, [Wrapper, "w", "w.n < 5"], function (facts) { + var w = facts.w; + w.n++; + this.modify(w); + }); + + this.rule("pong", { noLoop: true }, [Wrapper, "w", "w.n < 5"], function (facts) { + var w = facts.w; + w.n++; + this.modify(w); + }); + }) + .getSession(new Wrapper(0)) + .on("fire", function (name) { + fired.push(name); + }) + .match() + .then(function(){ + assert.deepEqual(fired, [ 'ping', 'pong', 'ping', 'pong', 'ping' ]) + }); + }); + + it.should("mix with noloop", function () { + fired = []; + + return nools + .flow("noLoop3", function () { + this.rule("a", { noLoop: true }, [Wrapper, "w", "w.n < 5"], function (facts) { + var w = facts.w; + w.n++; + this.modify(w); + }); + + this.rule("b", {}, [Wrapper, "w", "w.n < 5"], function (facts) { + + }); + + this.rule("c", { noLoop: true }, [Wrapper, "w", "w.n < 5"], function (facts) { + var w = facts.w; + w.n++; + this.modify(w); + }); + }) + .getSession(new Wrapper(0)) + .on("fire", function (name) { + fired.push(name); + }) + .match() + .then(function(){ + assert.deepEqual(fired, [ 'a', 'b', 'c', 'a', 'b', 'c', 'a']) + }); + }); +});