diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f3128b6..64ef716b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # jsonld ChangeLog +## 8.1.1 - 2023-02-dd + +### Fixed +- Improved `types.isObject` internal API performance. +- Improved `graphTypes.*` internal API performance. +- Improved `util.addValue` performance. +- Improved `util.compareValues` performance. + ## 8.1.0 - 2022-08-29 ### Fixed diff --git a/README.md b/README.md index 045325a4..dd7c372e 100644 --- a/README.md +++ b/README.md @@ -476,7 +476,7 @@ Use a command line with a test suite and a benchmark flag: EARL reports with benchmark data can be generated with an optional environment details: - JSONLD_TESTS=`pwd`/../json-ld.org/benchmarks/b001-manifiest.jsonld JSONLD_BENCHMARK=1 EARL=earl-test.jsonld TEST_ENV=1 npm test + JSONLD_TESTS=`pwd`/../json-ld.org/benchmarks/b001-manifest.jsonld JSONLD_BENCHMARK=1 EARL=earl-test.jsonld TEST_ENV=1 npm test See `tests/test.js` for more `TEST_ENV` control and options. diff --git a/lib/graphTypes.js b/lib/graphTypes.js index 3d06d6cf..ac8ca410 100644 --- a/lib/graphTypes.js +++ b/lib/graphTypes.js @@ -1,5 +1,5 @@ -/* - * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. +/*! + * Copyright (c) 2017-2023 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; @@ -52,7 +52,7 @@ api.isValue = v => // Note: A value is a @value if all of these hold true: // 1. It is an Object. // 2. It has the @value property. - types.isObject(v) && ('@value' in v); + types.isObject(v) && v['@value'] !== undefined; /** * Returns true if the given value is a @list. @@ -65,7 +65,7 @@ api.isList = v => // Note: A value is a @list if all of these hold true: // 1. It is an Object. // 2. It has the @list property. - types.isObject(v) && ('@list' in v); + types.isObject(v) && v['@list'] !== undefined; /** * Returns true if the given value is a @graph. @@ -78,7 +78,7 @@ api.isGraph = v => { // 2. It has an `@graph` key. // 3. It may have '@id' or '@index' return types.isObject(v) && - '@graph' in v && + v['@graph'] !== undefined && Object.keys(v) .filter(key => key !== '@id' && key !== '@index').length === 1; }; @@ -93,7 +93,7 @@ api.isSimpleGraph = v => { // 1. It is an object. // 2. It has an `@graph` key. // 3. It has only 1 key or 2 keys where one of them is `@index`. - return api.isGraph(v) && !('@id' in v); + return api.isGraph(v) && v['@id'] === undefined; }; /** @@ -109,12 +109,13 @@ api.isBlankNode = v => { // 2. If it has an @id key that is not a string OR begins with '_:'. // 3. It has no keys OR is not a @value, @set, or @list. if(types.isObject(v)) { - if('@id' in v) { - const id = v['@id']; + const id = v['@id']; + if(id !== undefined) { return !types.isString(id) || id.indexOf('_:') === 0; } - return (Object.keys(v).length === 0 || - !(('@value' in v) || ('@set' in v) || ('@list' in v))); + return (v['@value'] === undefined && + v['@set'] === undefined && + v['@list'] === undefined) || Object.keys(v).length === 0; } return false; }; diff --git a/lib/nodeMap.js b/lib/nodeMap.js index 9e61d5e0..e62c957b 100644 --- a/lib/nodeMap.js +++ b/lib/nodeMap.js @@ -201,6 +201,7 @@ api.createNodeMap = (input, graphs, graph, issuer, name, list) => { {propertyIsArray: true, allowDuplicate: false}); api.createNodeMap(o, graphs, graph, issuer, id); } else if(graphTypes.isValue(o)) { + // handle @value util.addValue( subject, property, o, {propertyIsArray: true, allowDuplicate: false}); @@ -213,7 +214,7 @@ api.createNodeMap = (input, graphs, graph, issuer, name, list) => { subject, property, o, {propertyIsArray: true, allowDuplicate: false}); } else { - // handle @value + // handle remaining cases api.createNodeMap(o, graphs, graph, issuer, name); util.addValue( subject, property, o, {propertyIsArray: true, allowDuplicate: false}); diff --git a/lib/types.js b/lib/types.js index d07e4fa1..38cc4402 100644 --- a/lib/types.js +++ b/lib/types.js @@ -1,5 +1,5 @@ -/* - * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. +/*! + * Copyright (c) 2017-2023 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; @@ -70,7 +70,7 @@ api.isNumeric = v => !isNaN(parseFloat(v)) && isFinite(v); * * @return true if the value is an Object, false if not. */ -api.isObject = v => Object.prototype.toString.call(v) === '[object Object]'; +api.isObject = v => v !== null && typeof v === 'object' && !Array.isArray(v); /** * Returns true if the given value is a String. diff --git a/lib/util.js b/lib/util.js index 57bf9f74..f4c8820f 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,5 +1,5 @@ -/* - * Copyright (c) 2017-2019 Digital Bazaar, Inc. All rights reserved. +/*! + * Copyright (c) 2017-2023 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; @@ -191,7 +191,7 @@ api.validateTypeValue = (v, isFrame) => { api.hasProperty = (subject, property) => { if(subject.hasOwnProperty(property)) { const value = subject[property]; - return (!types.isArray(value) || value.length > 0); + return !types.isArray(value) || value.length > 0; } return false; }; @@ -206,24 +206,16 @@ api.hasProperty = (subject, property) => { * @return true if the value exists, false if not. */ api.hasValue = (subject, property, value) => { - if(api.hasProperty(subject, property)) { - let val = subject[property]; - const isList = graphTypes.isList(val); - if(types.isArray(val) || isList) { - if(isList) { - val = val['@list']; - } - for(let i = 0; i < val.length; ++i) { - if(api.compareValues(value, val[i])) { - return true; - } - } - } else if(!types.isArray(value)) { - // avoid matching the set of values with an array value parameter - return api.compareValues(value, val); - } + if(!api.hasProperty(subject, property)) { + return false; } - return false; + let target = subject[property]; + const isList = graphTypes.isList(target); + if(isList) { + target = target['@list']; + } + const isArray = types.isArray(target); + return _hasValue(target, isArray, value); }; /** @@ -245,56 +237,20 @@ api.hasValue = (subject, property, value) => { */ api.addValue = (subject, property, value, options) => { options = options || {}; - if(!('propertyIsArray' in options)) { + if(options.propertyIsArray === undefined) { options.propertyIsArray = false; } - if(!('valueIsArray' in options)) { + if(options.valueIsArray === undefined) { options.valueIsArray = false; } - if(!('allowDuplicate' in options)) { + if(options.allowDuplicate === undefined) { options.allowDuplicate = true; } - if(!('prependValue' in options)) { + if(options.prependValue === undefined) { options.prependValue = false; } - if(options.valueIsArray) { - subject[property] = value; - } else if(types.isArray(value)) { - if(value.length === 0 && options.propertyIsArray && - !subject.hasOwnProperty(property)) { - subject[property] = []; - } - if(options.prependValue) { - value = value.concat(subject[property]); - subject[property] = []; - } - for(let i = 0; i < value.length; ++i) { - api.addValue(subject, property, value[i], options); - } - } else if(subject.hasOwnProperty(property)) { - // check if subject already has value if duplicates not allowed - const hasValue = (!options.allowDuplicate && - api.hasValue(subject, property, value)); - - // make property an array if value not present or always an array - if(!types.isArray(subject[property]) && - (!hasValue || options.propertyIsArray)) { - subject[property] = [subject[property]]; - } - - // add new value - if(!hasValue) { - if(options.prependValue) { - subject[property].unshift(value); - } else { - subject[property].push(value); - } - } - } else { - // add new value as set or single value - subject[property] = options.propertyIsArray ? [value] : value; - } + _addValue(subject, property, value, options); }; /** @@ -389,11 +345,10 @@ api.compareValues = (v1, v2) => { } // 3. equal @ids - if(types.isObject(v1) && - ('@id' in v1) && - types.isObject(v2) && - ('@id' in v2)) { - return v1['@id'] === v2['@id']; + if(types.isObject(v1) && types.isObject(v2)) { + const id1 = v1['@id']; + const id2 = v2['@id']; + return id1 !== undefined && id1 === id2; } return false; @@ -420,6 +375,131 @@ api.compareShortestLeast = (a, b) => { return (a < b) ? -1 : 1; }; +// internal helper for `api.addValue` +function _addValue(subject, property, value, options) { + // if value is an array, assume no special checks, just set it + if(options.valueIsArray) { + subject[property] = value; + return; + } + + // handle adding multiple values + const multipleValues = types.isArray(value); + if(multipleValues) { + // array of length `1` is handled as a single value below + if(value.length !== 1) { + return _addValues(subject, property, value, options); + } + value = value[0]; + } + + if(subject.hasOwnProperty(property)) { + _addToExistingProperty(subject, property, value, options); + } else { + // add new value as set or single value + subject[property] = options.propertyIsArray ? [value] : value; + } +} + +// internal helper for `api.addValue`; `value.length !== 1` +function _addValues(subject, property, value, options) { + // handle empty array + if(value.length === 0) { + // ensure property is set to an array + if(options.propertyIsArray && !subject.hasOwnProperty(property)) { + subject[property] = []; + } + return; + } + + // add each element of `value` to the `target`, which may start out as + // `undefined` if `property` is not yet set in `subject` + let target = subject[property]; + let isArray = types.isArray(target); + for(const nextValue of value) { + // if no target set yet... + if(target === undefined) { + if(options.propertyIsArray) { + target = [nextValue]; + isArray = true; + } else { + target = nextValue; + } + continue; + } + + // if duplicates not allowed, skip if `nextValue` is a dupe + if(!options.allowDuplicate) { + if(_hasValue(target, isArray, nextValue)) { + continue; + } + } + + // add `nextValue` to target + if(isArray) { + if(options.prependValue) { + target.unshift(nextValue); + } else { + target.push(nextValue); + } + } else { + if(options.prependValue) { + target = [nextValue, target]; + } else { + target = [target, nextValue]; + } + isArray = true; + } + } + // ensure subject property value is updated to `target` + subject[property] = target; +} + +// internal helper for `api.addValue` +function _addToExistingProperty(subject, property, value, options) { + const target = subject[property]; + const isArray = types.isArray(target); + + // consider subject has having value if duplicates are allowed or if target + // has no matching value + const hasValue = !options.allowDuplicate && + _hasValue(target, isArray, value); + + if(!isArray) { + // make property value an array if value not present or always an array + if(hasValue) { + if(options.propertyIsArray) { + // value already present, just convert to an array + subject[property] = [target]; + } + } else if(options.prependValue) { + subject[property] = [value, target]; + } else { + subject[property] = [target, value]; + } + return; + } + + // add new value to target array if not present + if(!hasValue) { + if(options.prependValue) { + target.unshift(value); + } else { + target.push(value); + } + } +} + +// internal helper to see if `target` has `value`; `target` can be an array +// to look for `value` in or a simple value / undefined to compare against +// `value` +function _hasValue(target, isArray, value) { + if(isArray) { + return target.some(t => api.compareValues(value, t)); + } + return api.compareValues(value, target); +} + /** * Labels the blank nodes in the given value using the given IdentifierIssuer. *