Repository: jsonata-js/jsonata (js)

Issue:
"Invalid JSONata expression" error when ~> expression evaluated in parallel
See https://github.com/node-red/node-red/issues/2183 for background context.

If an expression contains a `~>` operator and is evaluated twice in parallel, both executions return an error of:

> Argument 2 of function "lookup" does not match function signature"

```
const jsonata = require("jsonata");

const expr = jsonata("{'1':'goat','2': 'cheese'} ~> $lookup($string(payload))");

expr.evaluate({"payload":1}, {}, function(err,result) {
    console.log("1",err);
    console.log("1",result);
});
expr.evaluate({"payload":2}, {}, function(err,result) {
    console.log("2",err);
    console.log("2",result);
});
```

The error does not occur if:
 - the second evaluation is done from the callback of the first or wrapped in a `setImmediate`.
 - the expression is rewritten to avoid the `~>` operator. For example, the equivalent expression `$lookup({'1': 'goat', '2': 'cheese'}, $string(payload))` works.

Pull request description:
fix concurrency bug in ~>
resolves #335 

Patch:
diff --git a/src/jsonata.js b/src/jsonata.js
index 5045674..a9fc9e4 100644
--- a/src/jsonata.js
+++ b/src/jsonata.js
@@ -1158,13 +1158,11 @@ var jsonata = (function() {
         var result;
 
 
+        var lhs = yield * evaluate(expr.lhs, input, environment);
         if(expr.rhs.type === 'function') {
             // this is a function _invocation_; invoke it with lhs expression as the first argument
-            expr.rhs.arguments.unshift(expr.lhs);
-            result = yield * evaluateFunction(expr.rhs, input, environment);
-            expr.rhs.arguments.shift();
+            result = yield * evaluateFunction(expr.rhs, input, environment, { context: lhs });
         } else {
-            var lhs = yield * evaluate(expr.lhs, input, environment);
             var func = yield * evaluate(expr.rhs, input, environment);
 
             if(!isFunction(func)) {
@@ -1197,7 +1195,7 @@ var jsonata = (function() {
      * @param {Object} environment - Environment
      * @returns {*} Evaluated input data
      */
-    function* evaluateFunction(expr, input, environment) {
+    function* evaluateFunction(expr, input, environment, applyto) {
         var result;
 
         // create the procedure
@@ -1217,6 +1215,9 @@ var jsonata = (function() {
         }
 
         var evaluatedArgs = [];
+        if(typeof applyto !== 'undefined') {
+            evaluatedArgs.push(applyto.context);
+        }
         // eager evaluation - evaluate the arguments
         for (var jj = 0; jj < expr.arguments.length; jj++) {
             const arg = yield* evaluate(expr.arguments[jj], input, environment);


Test patch:
diff --git a/test/async-function.js b/test/async-function.js
index 946a1ac..d2a6040 100644
--- a/test/async-function.js
+++ b/test/async-function.js
@@ -91,6 +91,29 @@ describe('Invoke JSONata with callback - return values', function() {
     });
 });
 
+describe('Evaluate concurrent expressions with callbacks', function() {
+    it('should process two expressions concurrently', function(done) {
+        const expr = jsonata("{'1':'goat','2': 'cheese'} ~> $lookup($string(payload))");
+
+        var count = 0;
+        var partDone = function() {
+            count++;
+            if(count >= 2) {
+                done();
+            }
+        };
+
+        expr.evaluate({"payload":1}, {}, function(err,result) {
+            expect(result).to.equal('goat');
+            partDone();
+        });
+        expr.evaluate({"payload":2}, {}, function(err,result) {
+            expect(result).to.equal('cheese');
+            partDone();
+        });
+    });
+});
+
 describe('Handle chained functions that end in promises', function() {
 
     var counter = function(count) {