Skip to content

Commit

Permalink
perf: improve evaluation speed of conditional queries
Browse files Browse the repository at this point in the history
Use NodeJS' vm.Script class to cache the script that the context needs
to be evaluated against. This provides large speed improvements (~50%)
when a path contains conditional or JS logic.
  • Loading branch information
Jacob Roschen committed Aug 24, 2022
1 parent 5d740b3 commit 258832a
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 25 deletions.
18 changes: 16 additions & 2 deletions src/jsonpath-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 32,24 @@ const moveToAnotherArray = function (source, target, conditionCb) {
}
};

JSONPath.prototype.vm = {
/**
* In-browser replacement for NodeJS' VM.Script.
*/
class Script {
/**
* @param {string} expr Expression to evaluate
*/
constructor (expr) {
this.code = expr;
}

/**
* @param {PlainObject} context Object whose items will be added
* to evaluation
* @returns {EvaluatedResult} Result of evaluated code
*/
runInNewContext (expr, context) {
runInNewContext (context) {
let expr = this.code;
const keys = Object.keys(context);
const funcs = [];
moveToAnotherArray(keys, funcs, (key) => {
Expand Down Expand Up @@ -81,6 91,10 @@ JSONPath.prototype.vm = {
// eslint-disable-next-line no-new-func
return (new Function(...keys, code))(...values);
}
}

JSONPath.prototype.vm = {
Script
};

export {JSONPath};
46 changes: 24 additions & 22 deletions src/jsonpath.js
Original file line number Diff line number Diff line change
Expand Up @@ -588,32 588,34 @@ JSONPath.prototype._slice = function (
JSONPath.prototype._eval = function (
code, _v, _vname, path, parent, parentPropName
) {
if (code.includes('@parentProperty')) {
this.currSandbox._$_parentProperty = parentPropName;
code = code.replace(/@parentProperty/gu, '_$_parentProperty');
}
if (code.includes('@parent')) {
this.currSandbox._$_parent = parent;
code = code.replace(/@parent/gu, '_$_parent');
}
if (code.includes('@property')) {
this.currSandbox._$_property = _vname;
code = code.replace(/@property/gu, '_$_property');
}
if (code.includes('@path')) {
this.currSandbox._$_parentProperty = parentPropName;
this.currSandbox._$_parent = parent;
this.currSandbox._$_property = _vname;
this.currSandbox._$_root = this.json;
this.currSandbox._$_v = _v;

const containsPath = code.includes('@path');
if (containsPath) {
this.currSandbox._$_path = JSONPath.toPathString(path.concat([_vname]));
code = code.replace(/@path/gu, '_$_path');
}
if (code.includes('@root')) {
this.currSandbox._$_root = this.json;
code = code.replace(/@root/gu, '_$_root');
}
if ((/@([.\s)[])/u).test(code)) {
this.currSandbox._$_v = _v;
code = code.replace(/@([.\s)[])/gu, '_$_v$1');

const scriptCacheKey = 'script:' code;
if (!JSONPath.cache[scriptCacheKey]) {
let script = code
.replace(/@parentProperty/gu, '_$_parentProperty')
.replace(/@parent/gu, '_$_parent')
.replace(/@property/gu, '_$_property')
.replace(/@root/gu, '_$_root')
.replace(/@([.\s)[])/gu, '_$_v$1');
if (containsPath) {
script = script.replace(/@path/gu, '_$_path');
}

JSONPath.cache[scriptCacheKey] = new this.vm.Script(script);
}

try {
return this.vm.runInNewContext(code, this.currSandbox);
return JSONPath.cache[scriptCacheKey].runInNewContext(this.currSandbox);
} catch (e) {
throw new Error('jsonPath: ' e.message ': ' code);
}
Expand Down
2 changes: 1 addition & 1 deletion test/test.errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 20,7 @@ checkBuiltInVMAndNodeVM(function (vmType, setBuiltInState) {
it('should throw with a bad filter', () => {
expect(() => {
jsonpath({json: {book: []}, path: '$..[?(@.category === category)]'});
}).to.throw(Error, 'jsonPath: category is not defined: _$_v.category === category');
}).to.throw(Error, 'jsonPath: category is not defined: @.category === category');
});

it('should throw with a bad result type', () => {
Expand Down

0 comments on commit 258832a

Please sign in to comment.