diff --git a/docs/embedding-extending.md b/docs/embedding-extending.md index 159df3bf..b8c3363b 100644 --- a/docs/embedding-extending.md +++ b/docs/embedding-extending.md @@ -80,6 +80,30 @@ console.log("Started"); // Prints "Started", then "Finished with 19" ``` +If `continuationCallback()` is supplied, `expression.evaluate()` returns `undefined`, the expression is run asynchronously calling continuationCallback after every step which should resolve to true else it will cancel the execution and the `Error` in `callback` will reflect as operation cancelled. + +```javascript +let stepCounter = 0; +async function continueExecution() { + stepCounter++; + return stepCounter<1000; //PREVENT INFINITE LOOPS +} +function completion(err, results) { + if (err) { + console.error(err); + return; + } + console.log(`Completed in ${stepCounter} steps with result:${results}`); +} +const getBit = jsonata(`($x:= λ($c,$n,$b){ $c=$b?$n%2:$x($c+1,$floor($n/2),$b)};$x(0,number,bitIndex))`); + +getBit.evaluate({ "number": 10000000, "bitIndex": 0 }, undefined, completion, continueExecution); //NORMAL +// Prints Completed in 17 steps with result:0 + +getBit.evaluate({ "number": 10000000, "bitIndex": -1 }, undefined, completion, continueExecution); //INFINITE LOOP +// Prints { code: 'U2020', message: 'Operation cancelled.' } +``` + ### expression.assign(name, value) Permanently binds a value to a name in the expression, similar to how `bindings` worked above. Modifies `expression` in place and returns `undefined`. Useful in a JSONata expression factory. diff --git a/jsonata.d.ts b/jsonata.d.ts index f860a969..3d299ba2 100644 --- a/jsonata.d.ts +++ b/jsonata.d.ts @@ -35,6 +35,7 @@ declare namespace jsonata { interface Expression { evaluate(input: any, bindings?: Record): any; evaluate(input: any, bindings: Record | undefined, callback: (err: JsonataError, resp: any) => void): void; + evaluate(input: any, bindings: Record | undefined, callback: (err: JsonataError, resp: any) => void, continuationCallback: () => Promise): void; assign(name: string, value: any): void; registerFunction(name: string, implementation: (this: Focus, ...args: any[]) => any, signature?: string): void; ast(): ExprNode; diff --git a/src/jsonata.js b/src/jsonata.js index aa40ccf4..cae117cf 100644 --- a/src/jsonata.js +++ b/src/jsonata.js @@ -2006,7 +2006,8 @@ var jsonata = (function() { "D3138": "The $single() function expected exactly 1 matching result. Instead it matched more.", "D3139": "The $single() function expected exactly 1 matching result. Instead it matched 0.", "D3140": "Malformed URL passed to ${{{functionName}}}(): {{value}}", - "D3141": "{{{message}}}" + "D3141": "{{{message}}}", + "U2020": "Operation Cancelled" }; /** @@ -2062,7 +2063,7 @@ var jsonata = (function() { }, '<:n>')); return { - evaluate: function (input, bindings, callback) { + evaluate: function (input, bindings, callback, continuationCallback = function(){return Promise.resolve(true)}) { // throw if the expression compiled with syntax errors if(typeof errors !== 'undefined') { var err = { @@ -2110,7 +2111,16 @@ var jsonata = (function() { if (result.done) { callback(null, result.value); } else { - result.value.then(thenHandler).catch(catchHandler); + continuationCallback().then((proceed) => { + if (proceed === true) { + result.value.then(thenHandler).catch(catchHandler); + } + else { + var err = { code: 'U2020' }; + populateMessage(err); + callback(err, null); + } + }).catch(catchHandler); } }; it = evaluate(ast, input, exec_env); diff --git a/test/async-continuation-function.js b/test/async-continuation-function.js new file mode 100644 index 00000000..fa558a25 --- /dev/null +++ b/test/async-continuation-function.js @@ -0,0 +1,38 @@ +"use strict"; + +var jsonata = require('../src/jsonata'); +var chai = require("chai"); +var expect = chai.expect; +var chaiAsPromised = require("chai-as-promised"); +chai.use(chaiAsPromised); + +var jsonataContinuationPromise = function (expr, data, continuationCallback) { + return new Promise(function (resolve, reject) { + expr.evaluate(data, undefined, function (error, response) { + if (error) reject(error); + resolve(response); + }, continuationCallback); + }); +}; + +describe('Invoke JSONata with continuation callback', function () { + describe('Get bit value form valid bit index', function () { + it('should return valid result', function () { + let stepCounter = 0; + const continuationCallback = () => { stepCounter++; return Promise.resolve(stepCounter < 100); }; + const getBit = jsonata(`($x:= λ($c,$n,$b){ $c=$b?$n%2:$x($c+1,$floor($n/2),$b)};$x(0,number,bitIndex))`); + const data = { "number": 10000000, "bitIndex": 0 }; + expect(jsonataContinuationPromise(getBit, data, continuationCallback)).to.eventually.deep.equal(0); + }); + }); + + describe('Get bit value form invalid bit index', function () { + it('should return error rejected', function () { + let stepCounter = 0; + const continuationCallback = () => { stepCounter++; return Promise.resolve(stepCounter < 100); }; + const getBit = jsonata(`($x:= λ($c,$n,$b){ $c=$b?$n%2:$x($c+1,$floor($n/2),$b)};$x(0,number,bitIndex))`); + const data = { "number": 10000000, "bitIndex": -1 }; + expect(jsonataContinuationPromise(getBit, data, continuationCallback)).to.eventually.be.rejected; + }); + }); +});