I’m building ACL support in NodeJS. For example, I’ll allow to write:
{[ .can-example | 1.hilite(=javascript=) ]}
For any given action, my ´Can´ function must check a list of permissions. If one matches, then the action ´Can´ asks for is granted, otherwise (no permission exists) it’s denied. Of course I’m only interested in the first permission to match, and to allow for optimizations, I want permissions to be checked in the order I provide.
It’s easy to see that my problem is solved by the Array.prototype.find method. The problem I have, though, is that it only works with immediate predicates, but my checks can entail both immediate and promised predicates. For example, I allow predicates to access the database.
I googled my problem and found this StackOverflow page. Bergi’s answer gives both recursive and non-recursive solutions. (Side note. There was a time when recursive was opposed to iterative. With promises, that’s no longer the case. In fact, the non-recursive solution is a chain of ´catch´ handlers. An iteration is used to build the chain but promises themselves, throwing exceptions, control the iteration.) Benjamin Gruenbaum’s answer gives a recursive solution.
Here are their issues.
- The predicate is hacky because it signals a ´false´ by throwing an exception.
- The promise management is hacky because (a) it must cater for the predicate with ´.catch()´, and (b) it signals the “not found” outcome with ´reject()´.
- The promise management and the predicate are very coupled.
- The contract is different from that of the Array.prototype.find method.
So I came up with this one.
{[ .ArrayFind | 1.hilite(=javascript=) ]}
Apart from being a global function instead of an Array instance method, the contract is exactly the same as that of the Array.prototype.find method.
- The only hack I used is to immediately exit when an element is found instead of continuing until the end of the ´.then()´ chain. But how I implemented it is both robust (as in Robustness) and hidden (as in Information Hiding). To make sure I do not mistake a rightful exception with my hack, I throw my own fake exception which is a wrapper around the found element. Thus, my fake exception is caught by the last ´.catch()´ and the element is returned.
- The predicate can be both immediate or a promise, thanks to ´Promise.resolve(Predicate…)´. If it’s immediate, it can throw an exception if it has to, not if it doesn’t hold true. If it’s a promise, it can reject() if it has to, not if it doesn’t hold true.
Examples
Here are some examples.
{[ .predicates | 1.hilite(=javascript=) ]}
The result is a promise
Here is how ArrayFind compares to Array.prototype.find when no exceptions are thrown:
[1,13,5,4,7].find(ImmediatePredicate) VM834:3 -- 1 VM834:3 -- 13 VM834:3 -- 5 VM834:3 -- 4 4 ArrayFind([1,13,5,4,7], ImmediatePredicate) VM834:3 -- 1 VM834:3 -- 13 VM834:3 -- 5 VM834:3 -- 4 Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
Of course the result is a Promise instead of the found element, but we can append additional handlers, like
ArrayFind([1,13,5,4,7], ImmediatePredicate) .then(function(result){ console.log(result); }) .catch(function(reason){ console.warn(reason) }) VM834:3 -- 1 VM834:3 -- 13 VM834:3 -- 5 VM834:3 -- 4 VM880:4 4 Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
An exception makes the promise reject
Here is how ArrayFind compares to Array.prototype.find when an exception is thrown:
[1,3,5,4,7].find(ImmediatePredicate) VM834:3 -- 1 VM834:3 -- 3 VM834:4 Uncaught dirty ArrayFind([1,3,5,4,7], ImmediatePredicate) .then(function(result){ console.log(result); }) .catch(function(reason){ console.warn(reason) }) VM834:3 -- 1 VM834:3 -- 3 VM925:7 dirty Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
Immediate and promised predicates work equally well
Here you can see how ArrayFind supports at the same time immediate and promised predicates:
ArrayFind([1,13,5,4,7], MixedPredicate) .then(function(result){ console.log(result); }) .catch(function(reason){ console.warn(reason) }) VM834:3 -- 1 Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined} VM834:12 -- 13 VM834:3 -- 5 VM834:3 -- 4 VM955:4 4 ArrayFind([1,3,5,4,7], MixedPredicate) .then(function(result){ console.log(result); }) .catch(function(reason){ console.warn(reason) }) VM834:3 -- 1 VM834:3 -- 3 VM974:7 dirty Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined} ArrayFind([1,12,5,4,7], MixedPredicate) .then(function(result){ console.log(result); }) .catch(function(reason){ console.warn(reason) }) VM834:3 -- 1 Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined} VM834:12 -- 12 VM975:7 dirty
You can’t appreciate from the output above, but the immediate predicate really outputs immediately. 🙂
Also notice that in the second to last example the warned ´dirty´ comes from the immediate predicate because 3 < 10, while in the last example it comes from the promised predicate because 12 >= 10.