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.