diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index c9bb555..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-node_modules
-nools.iml
-.idea
-atlassian-ide-plugin.xml
diff --git a/lib/agenda.js b/lib/agenda.js
index 99fd57e..cd585bc 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);
}
@@ -162,6 +162,14 @@ module.exports = declare(EventEmitter, {
insert: function (node, insert) {
var rule = this.rules[node.name], nodeRule = node.rule, agendaGroup = nodeRule.agendaGroup;
+ if (nodeRule.noLoop) {
+ if (rule.noLoop[insert.hashCode]) {
+ return;
+ }
+ else {
+ rule.noLoop[insert.hashCode] = true;
+ }
+ }
rule.tree.insert(insert);
this.getAgendaGroup(agendaGroup).insert(insert);
if (agendaGroup) {
diff --git a/lib/parser/nools/tokens.js b/lib/parser/nools/tokens.js
index 3ac7080..23391e2 100644
--- a/lib/parser/nools/tokens.js
+++ b/lib/parser/nools/tokens.js
@@ -94,6 +94,20 @@ 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 +116,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..75ee26e 100644
--- a/lib/rule.js
+++ b/lib/rule.js
@@ -227,6 +227,7 @@ 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 e5bc0d9..56d58f0 100644
--- a/readme.md
+++ b/readme.md
@@ -37,6 +37,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)
@@ -1003,6 +1004,33 @@ flow1
});
```
+
+### 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..bcebe58
--- /dev/null
+++ b/test/flow/noLoop.test.js
@@ -0,0 +1,67 @@
+"use strict";
+var it = require("it"),
+ assert = require("assert"),
+ nools = require("../../");
+
+it.describe("no-loop", function (it) {
+ /*jshint indent*/
+ function Message(name) {
+ this.name = name;
+ }
+ var cnt = 0;
+
+ var flow1 = nools.flow("noLoop1", function () {
+
+ this.rule("Hello2", { noLoop: true }, [Message, "m", "m.name =~ /Hello/"], function (facts) {
+ var m = facts.m;
+ m.name = 'Hello World';
+ this.modify(m);
+ });
+ }),
+
+ flow2 = nools.flow("noLoop2", function () {
+
+ this.rule("Hello1", [Message, "m", "m.name =~ /Hello/"], function (facts) {
+ var m = facts.m;
+ if (cnt++ < 2) {
+ m.name = 'Hello World';
+ this.modify(m);
+ }
+ });
+ });
+
+ var noolsSource = "rule 'Hello3' { no-loop: true; when {m: Message m.name =~/Hello/;}then {modify(m, function () { this.name = 'Hello World'; });}}";
+
+ var flow3 = nools.compile(noolsSource, {
+ name: 'testDsl'
+ ,define: {
+ Message: Message
+ }
+ });
+
+ it.should("not loop with option on and loop otherwise", function () {
+ var fired1 = [], fired2 = [], fired3 = [];
+ var session1 = flow1.getSession(new Message("Hello")).on("fire", function (name) {
+ fired1.push(name);
+ }),
+ session2 = flow2.getSession(new Message("Hello")).on("fire", function (name) {
+ fired2.push(name);
+ }),
+ session3 = flow3.getSession(new Message("Hello")).on("fire", function (name) {
+ fired3.push(name);
+ });
+ return session1.match()
+ .then(function () {
+ return session2.match().then(function () {
+ return session3.match().then(function () {
+ })
+ })
+ })
+ .then(function () {
+ assert.deepEqual(fired1, ["Hello2"]);
+ assert.deepEqual(fired2, ["Hello1", "Hello1", "Hello1"]);
+ assert.deepEqual(fired3, ["Hello3"]);
+ });
+ });
+
+});