Hi Ant,
It's a great module that you have there.
A few years back I wrote a similar rules engine but it's proprietary work which I can't share and which I'm not in possession anymore. I apologize as I can't be too specific so I'll err on the cautious side.
Here are a few things I learned or opinion I have after building that engine. Most of it is probably just rambling words but hopefully there is a bit of value that you can leverage.
1) Keep in mind that the context that I was in was to provide a distributed system. That means objects are serialized, at least between the client and server. Also, a rules engine is a state machine. That state needs to be temporarily stored somewhere, whether in DB, in the client or in some server side cache (memcache, redis, ...).
2) We first used Nools (
https://github.com/C2FO/nools). If you have looked into javascript rules engine, I'm sure you encountered Nools. It is a JS implementation of drools, a Java based rules engine. Nools implements some kind of RETE algorithm. Your module has a lot of similarities with Nools.
Nools had 2 issues for us:
- terrible performance (we realized that half way into the project)
- pretty bad filter API (we realized that very early)
It was pretty clear that RETE was a good place to start as it is a well known algorithm for rules engines.
With RETE a rule applies to one or several entities and can modify these entities.
The goal of RETE is to minimize the CPU cycles necessary to execute all the rules that should apply to a given set of entities. Hence that hierarchy: type -> filters -> logic
I think that the biggest problem that we face with RETE and Javascript is the fact that JS doesn't really have types. And when it does, raw JSON does not include it in the serialized streams.
You don't have that issue as you're using namespaces instead of entity types. But I would argue that you should not have to pass in a namespace. The rule engine should figure out which rules to run based on what data you are passing to it. A bit like a generic workflow that sits on to of you DB,
3) JS in, JS out
Something that bugged me with Nools and that bugs me with your implementation (@ajlopez mentions it) is that for some reason, the filters use some kind of domain specific language (DSL) instead of functional Javascript. You would want to keep your rules engine as powerful as possible.
I understand that you may want an administrator to edit the rules and you wouldn't want that admin to write javascript. That's ok but then there should be an additional layer specific to your app that does the transformation from DSL to javascript but the engine should run javascript and nothing else.
The reason I'm saying this is because I was bit by that. Being limited in functionality because the rules don't support more advanced filters was a moment when I realized I failed the architecture. It was quickly worked around but something worth avoiding.
4) Rules definitions
With Nools, a rule definition has 3 parts:
- the types of entities it applies to
- filters to apply when the types match (conditions to know whether to apply the rule or not)
- logic to apply if the filters match
That is a good thing because it gives 2 advantages:
- possible performance optimization by allowing to retrieve rules based on the data type.
- semantic separation between filters and logic (arguably, the filters can be implemented in the logic).
5) Session resumption
In a distributed system, it is very important to be able to serialize a state, deserialize it and resume rules evaluations.
Nools did not allow it. It did not even allow to easily merge deserialized data into an existing rete instance. It looks like it still can't do that. I'm not sure about your implementation.
The alternate is sticky sessions.
6) Performance
We had performance issues. When I looked into it, I realized that the most time lost was within Nools. When digging deeper, it was clear that Nools was doing plenty of things that we did not need. There was quite some room for improvement.
I ended up rewriting a rules engine specific to our needs but generic enough that it was still an all purpose rules engine. For a complex rule set, we went from 1500-2500ms execution time to 20-50ms. That's a 50x performance improvement.
Your lib is great anyway and I'm sure many people will find it useful !
- Nicolas