@marcbachmann/cel-js
Advanced tools
+7
-6
@@ -134,3 +134,3 @@ import {createRegistry, RootContext} from './registry.js' | ||
| if (first === undefined) return fb | ||
| return this.registry.getListType(this.debugRuntimeType(first, fb.valueType)) | ||
| return this.registry.getListType(this.#inferType(first)) | ||
| } | ||
@@ -147,4 +147,4 @@ | ||
| return this.registry.getMapType( | ||
| this.debugRuntimeType(first[0], fb.keyType), | ||
| this.debugRuntimeType(first[1], fb.valueType) | ||
| fb.keyType.hasDynType ? this.#inferType(first[0]) : fb.keyType, | ||
| fb.valueType.hasDynType ? this.#inferType(first[1]) : fb.valueType | ||
| ) | ||
@@ -154,9 +154,10 @@ } | ||
| debugOperandType(value, checkedType) { | ||
| if (checkedType?.hasDynType === false) return checkedType | ||
| return this.debugRuntimeType(value, checkedType).wrappedType | ||
| return checkedType?.hasDynType === false ? checkedType : this.#inferType(value).wrappedType | ||
| } | ||
| debugRuntimeType(value, checkedType) { | ||
| if (checkedType?.hasDynType === false) return checkedType | ||
| return checkedType?.hasDynType === false ? checkedType : this.#inferType(value) | ||
| } | ||
| #inferType(value) { | ||
| const runtimeType = this.debugType(value) | ||
@@ -163,0 +164,0 @@ switch (runtimeType.kind) { |
+5
-4
@@ -203,8 +203,9 @@ import {celTypes} from './registry.js' | ||
| if (!(left.hasDynType || right.hasDynType)) { | ||
| ast.staticHandler = chk.registry.findBinaryOverload(op, left, right) | ||
| } | ||
| const type = | ||
| left.hasDynType || right.hasDynType | ||
| ? chk.registry.checkBinaryOverload(op, left, right) | ||
| : (ast.staticHandler = chk.registry.findBinaryOverload(op, left, right))?.returnType | ||
| const type = ast.staticHandler?.returnType || chk.registry.checkBinaryOverload(op, left, right) | ||
| if (type) return type | ||
| throw new chk.Error( | ||
@@ -211,0 +212,0 @@ `no such overload: ${chk.formatType(left)} ${op} ${chk.formatType(right)}`, |
+0
-3
@@ -31,4 +31,2 @@ import {UnsignedInt, Duration} from './functions.js' | ||
| unaryOverload('-', 'double', (a) => -a) | ||
| binaryOverload('dyn<double>', `==`, `int`, (a, b) => a == b) // eslint-disable-line eqeqeq | ||
| binaryOverload('dyn<double>', `==`, `uint`, (a, b) => a == b.valueOf()) // eslint-disable-line eqeqeq | ||
| binaryOverload('double', '*', 'double', (a, b) => a * b) | ||
@@ -154,3 +152,2 @@ binaryOverload('double', '+', 'double', (a, b) => a + b) | ||
| binaryOverload('dyn<uint>', `==`, `double`, (a, b) => a.valueOf() == b) // eslint-disable-line eqeqeq | ||
| binaryOverload('dyn<uint>', `==`, `int`, (a, b) => a.valueOf() == b) // eslint-disable-line eqeqeq | ||
@@ -157,0 +154,0 @@ binaryOverload('uint', '+', 'uint', (a, b) => new UnsignedInt(a.valueOf() + b.valueOf())) |
+54
-30
@@ -575,2 +575,7 @@ import {EvaluationError} from './errors.js' | ||
| const fallbackDynEqualityMatchers = { | ||
| '==': [{handler: (a, b) => a === b, returnType: celTypes.bool}], | ||
| '!=': [{handler: (a, b) => a !== b, returnType: celTypes.bool}] | ||
| } | ||
| export class Registry { | ||
@@ -583,3 +588,2 @@ #overloadResolutionCache = {} | ||
| #functionsCache = new Map() | ||
| #functionsCacheRec = new Map() | ||
| #listTypes = new Map() | ||
@@ -671,9 +675,11 @@ #mapTypes = new Map() | ||
| #getCandidates(useReceiver, name, argLen) { | ||
| let cache = useReceiver ? this.#functionsCacheRec : this.#functionsCache | ||
| cache = cache.get(name) || cache.set(name, new Map()).get(name) | ||
| return cache.get(argLen) || cache.set(argLen, new FunctionCandidates(this)).get(argLen) | ||
| // to reduce Map allocations, we're keying functions by argLen | ||
| // Receiver functions are below 0, regular ones above 0 | ||
| const l = useReceiver ? -(argLen + 1) : argLen | ||
| const cache = this.#functionsCache.get(l) || this.#functionsCache.set(l, new Map()).get(l) | ||
| return cache.get(name) || cache.set(name, new FunctionCandidates(this)).get(name) | ||
| } | ||
| getFunctionCandidates(rec, name, argLen) { | ||
| const cached = (rec ? this.#functionsCacheRec : this.#functionsCache).get(name)?.get(argLen) | ||
| const cached = this.#functionsCache.get(rec ? -(argLen + 1) : argLen)?.get(name) | ||
| if (cached) return cached | ||
@@ -725,3 +731,5 @@ | ||
| if (typename === 'ast') return astType | ||
| return this.#parseTypeString(typename, true) | ||
| const t = this.#parseTypeString(typename, true) | ||
| if (t.kind === 'dyn' && t.valueType) throw new Error(`type '${t.name}' is not supported`) | ||
| return t | ||
| } | ||
@@ -824,7 +832,4 @@ | ||
| const nonexactMatches = [] | ||
| const leftTypeUnwrap = leftType.unwrappedType | ||
| const rightTypeUnwrap = rightType.unwrappedType | ||
| for (const [, decl] of this.#operatorDeclarations) { | ||
| if (decl.operator !== operator) continue | ||
| if (decl.leftType === leftTypeUnwrap && decl.rightType === rightTypeUnwrap) return [decl] | ||
| if (decl.leftType === leftType && decl.rightType === rightType) return [decl] | ||
@@ -839,6 +844,5 @@ | ||
| (operator === '==' || operator === '!=') && | ||
| (leftType.kind === 'dyn' || rightType.kind === 'dyn') | ||
| leftType.kind === 'dyn' | ||
| ) { | ||
| const handler = operator === '==' ? (a, b) => a === b : (a, b) => a !== b | ||
| return [{handler, returnType: this.getType('bool')}] | ||
| return fallbackDynEqualityMatchers[operator] | ||
| } | ||
@@ -864,2 +868,5 @@ | ||
| findBinaryOverload(op, left, right) { | ||
| if (left.kind === 'dyn' && left.valueType) right = right.wrappedType | ||
| else if (right.kind === 'dyn' && right.valueType) left = left.wrappedType | ||
| return ( | ||
@@ -890,4 +897,4 @@ this.#overloadResolutionCache[op]?.get(left)?.get(right) ?? | ||
| #findBinaryOverloadUncached(operator, leftType, rightType) { | ||
| const ops = this.#findBinaryOverloads(operator, leftType, rightType) | ||
| #findBinaryOverloadUncached(operator, left, right) { | ||
| const ops = this.#findBinaryOverloads(operator, left, right) | ||
| if (ops.length === 0) return false | ||
@@ -928,3 +935,9 @@ if (ops.length === 1) return ops[0] | ||
| if ((decl.operator === '==' || decl.operator === '!=') && !leftType.matchesBoth(rightType)) | ||
| if ( | ||
| (decl.operator === '==' || decl.operator === '!=') && | ||
| decl.leftType.kind === 'dyn' && | ||
| decl.leftType.valueType && | ||
| actualLeft.kind !== 'dyn' && | ||
| actualRight.kind !== 'dyn' | ||
| ) | ||
| return false | ||
@@ -934,2 +947,3 @@ | ||
| ? { | ||
| signature: decl.signature, | ||
| handler: decl.handler, | ||
@@ -1043,7 +1057,8 @@ leftType, | ||
| const verifiedProto = Object.create(null) | ||
| const conversions = Object.create(null) | ||
| for (const k of keys) { | ||
| const type = fields[k] | ||
| if (type.kind !== 'message') verifiedProto[k] = true | ||
| else verifiedProto[k] = this.objectTypes.get(type.name) | ||
| const decl = type.kind === 'message' && this.objectTypes.get(type.name) | ||
| if (decl === false) conversions[k] = false | ||
| else conversions[k] = decl.convert ? decl : false | ||
| } | ||
@@ -1065,12 +1080,18 @@ | ||
| get(field) { | ||
| const v = super.get(field) | ||
| if (v !== undefined) return v | ||
| let v = super.get(field) | ||
| if (v !== undefined || this.has(field)) return v | ||
| const dec = verifiedProto[field] | ||
| if (dec === undefined || this.has(field)) return | ||
| const dec = conversions[field] | ||
| if (dec === undefined) return | ||
| const value = this.#raw instanceof Map ? this.#raw.get(field) : this.#raw[field] | ||
| if (dec === true || value === undefined) return (super.set(field, value), value) | ||
| super.set(field, dec.convert ? dec.convert(value) : value) | ||
| return super.get(field) | ||
| v = this.#raw instanceof Map ? this.#raw.get(field) : this.#raw?.[field] | ||
| if (dec && v && typeof v === 'object') { | ||
| switch (v.constructor) { | ||
| case undefined: | ||
| case Object: | ||
| case Map: | ||
| v = dec.convert(v) | ||
| } | ||
| } | ||
| return (super.set(field, v), v) | ||
| } | ||
@@ -1235,4 +1256,3 @@ } | ||
| this.#functionDeclarations.set(dec.signature, dec) | ||
| this.#functionsCacheRec.clear() | ||
| this.#functionsCache.clear() | ||
| if (this.#functionsCache.size) this.#functionsCache.clear() | ||
| } | ||
@@ -1281,6 +1301,10 @@ | ||
| const sig = `${leftTypeStr} ${op} ${rightTypeStr}: ${returnTypeStr}` | ||
| const leftType = this.assertType(leftTypeStr, 'left type', sig) | ||
| const rightType = this.assertType(rightTypeStr, 'right type', sig) | ||
| let leftType = this.assertType(leftTypeStr, 'left type', sig) | ||
| let rightType = this.assertType(rightTypeStr, 'right type', sig) | ||
| const returnType = this.assertType(returnTypeStr, 'return type', sig) | ||
| // Register both types as wrapped with dyn<> if one of them is wrapped | ||
| if (leftType.kind === 'dyn' && leftType.valueType) rightType = rightType.wrappedType | ||
| else if (rightType.kind === 'dyn' && rightType.valueType) leftType = leftType.wrappedType | ||
| if (isRelational(op) && returnType.type !== 'bool') { | ||
@@ -1287,0 +1311,0 @@ throw new Error(`Comparison operator '${op}' must return 'bool', got '${returnType.type}'`) |
+6
-6
| { | ||
| "name": "@marcbachmann/cel-js", | ||
| "version": "7.5.0", | ||
| "version": "7.5.1", | ||
| "description": "A lightweight Common Expression Language (CEL) implementation in JavaScript with zero dependencies", | ||
@@ -72,12 +72,12 @@ "keywords": [ | ||
| "service": "drone", | ||
| "commit": "0a47f533eb9b3772529f1a829b2bfbe957b654bb", | ||
| "build": "241", | ||
| "buildUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/241", | ||
| "commit": "172a41986f75638bd7cfde20856f68553c554972", | ||
| "build": "246", | ||
| "buildUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/246", | ||
| "branch": "main", | ||
| "jobUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/241", | ||
| "jobUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/246", | ||
| "isPr": false, | ||
| "slug": "marcbachmann/cel-js", | ||
| "root": "/drone/src", | ||
| "date": "2026-02-16T17:48:36.155Z" | ||
| "date": "2026-02-17T22:14:43.222Z" | ||
| } | ||
| } |
+42
-26
@@ -14,3 +14,3 @@ # @marcbachmann/cel-js [](https://www.npmjs.com/package/@marcbachmann/cel-js) [](https://opensource.org/licenses/MIT) | ||
| - 🚀 **Zero Dependencies** - No external packages required | ||
| - ⚡ **High Performance** - Up to 22x faster evaluation, 3x faster parsing than alternatives | ||
| - ⚡ **High Performance** - About 10x faster than alternatives (compared to [cel-js](https://www.npmjs.com/package/cel-js)) | ||
| - 📦 **ES Modules** - Modern ESM with full tree-shaking support | ||
@@ -72,11 +72,19 @@ - 🔒 **Type Safe** - Environment API with type checking for variables, custom types and functions | ||
| import {Environment} from '@marcbachmann/cel-js' | ||
| class User { | ||
| constructor ({email, age}) { | ||
| this.email = email | ||
| this.age = age | ||
| } | ||
| } | ||
| const env = new Environment() | ||
| .registerVariable('skipAgeCheck', 'bool') | ||
| .registerVariable('user', { | ||
| schema: { | ||
| .registerType('User', { | ||
| fields: { | ||
| email: 'string', | ||
| age: 'int', | ||
| } | ||
| age: 'int' | ||
| }, | ||
| ctor: User | ||
| }) | ||
| .registerVariable('skipAgeCheck', 'bool') | ||
| .registerVariable('user', 'User') | ||
| .registerConstant('minAge', 'int', 18n) | ||
@@ -87,4 +95,6 @@ .registerFunction('isAdult(int): bool', age => age >= 18n) | ||
| // Type-checked evaluation with constant | ||
| env.evaluate('isAdult(user.age) && (user.age >= minAge || skipAgeCheck)', { | ||
| user: {age: 25n}, skipAgeCheck: true | ||
| env.evaluate( | ||
| 'skipAgeCheck || (isAdult(user.age) && (user.age >= minAge))', { | ||
| user: new User({age: 25n}), | ||
| skipAgeCheck: true | ||
| }) | ||
@@ -194,2 +204,3 @@ | ||
| // Name + object with nested schema (registers nested types automatically) | ||
| // But this type can only be used during variable registration. | ||
| env.registerType('Vector', {schema: {x: 'double', y: 'double'}}) | ||
@@ -205,2 +216,3 @@ | ||
| When `fields` or `schema` is provided without a `ctor`, an internal wrapper class is auto-generated and plain objects are automatically converted at runtime. A custom `convert` function can be passed to override this default conversion. | ||
| In that case the type should only be used during variable registration. | ||
@@ -563,25 +575,29 @@ When using the `schema` declaration, we're creating a new Map instance for the specific type when retrieving the values by variable from a context object. | ||
| Benchmark results comparing against the `cel-js` package on Node.js v24.8.0 (Apple Silicon): | ||
| There are a few expressions compared with the `cel-js` module in `./benchmark/comparison.js` where `@marcbachmann/cel-js` is about 10x faster in average. | ||
| ### Parsing Performance | ||
| - **Average: 3.1x faster** (range: 0.76x - 14.8x) | ||
| - Simple expressions: **7-15x faster** | ||
| - Array/Map creation: **8-10x faster** | ||
| Benchmark results comparing against the `cel-js` package on Node.js v24.13.1(Macbook Air, Apple Silicon M3). | ||
| ### Evaluation Performance | ||
| - **Average: 22x faster** (range: 5.5x - 111x) | ||
| - Simple values: **64-111x faster** | ||
| - Collections: **46-58x faster** | ||
| - Complex logic: **5-14x faster** | ||
| ``` | ||
| $ ./benchmark/comparison.js | ||
| ### Highlights | ||
| marcbachmann parse (variable lookups) x 6,491,393 ops/sec (10 runs sampled) min..max=(149.34ns...156.11ns) | ||
| chromeGG/cel parse (variable lookups) x 530,216 ops/sec (12 runs sampled) min..max=(1.56us...2.04us) | ||
| marcbachmann evaluate (variable lookups) x 14,026,229 ops/sec (10 runs sampled) min..max=(70.35ns...71.76ns) | ||
| chromeGG/cel evaluate (variable lookups) x 997,079 ops/sec (10 runs sampled) min..max=(970.67ns...1.01us) | ||
| marcbachmann parse (check container ports) x 533,663 ops/sec (11 runs sampled) min..max=(1.86us...1.88us) | ||
| chromeGG/cel parse (check container ports) x 64,376 ops/sec (9 runs sampled) min..max=(15.24us...15.91us) | ||
| marcbachmann evaluate (check container ports) x 2,121,321 ops/sec (11 runs sampled) min..max=(463.34ns...478.94ns) | ||
| chromeGG/cel evaluate (check container ports) x 210,473 ops/sec (10 runs sampled) min..max=(4.51us...5.14us) | ||
| marcbachmann parse (check jwt claims) x 554,418 ops/sec (10 runs sampled) min..max=(1.76us...1.81us) | ||
| chromeGG/cel parse (check jwt claims) x 60,222 ops/sec (12 runs sampled) min..max=(13.73us...17.89us) | ||
| marcbachmann evaluate (check jwt claims) x 1,716,346 ops/sec (11 runs sampled) min..max=(579.29ns...588.51ns) | ||
| chromeGG/cel evaluate (check jwt claims) x 152,115 ops/sec (9 runs sampled) min..max=(6.37us...6.57us) | ||
| marcbachmann parse (access log filtering) x 1,297,845 ops/sec (11 runs sampled) min..max=(762.15ns...784.65ns) | ||
| chromeGG/cel parse (access log filtering) x 127,437 ops/sec (12 runs sampled) min..max=(6.68us...8.28us) | ||
| marcbachmann evaluate (access log filtering) x 3,507,039 ops/sec (10 runs sampled) min..max=(281.53ns...286.70ns) | ||
| chromeGG/cel evaluate (access log filtering) x 409,620 ops/sec (10 runs sampled) min..max=(2.38us...2.51us) | ||
| ``` | ||
| | Operation | Parsing | Evaluation | | ||
| |-----------|---------|------------| | ||
| | Simple number | 7.3x | 111x | | ||
| | Array creation | 10.1x | 57.9x | | ||
| | Map creation | 8.6x | 46x | | ||
| | Complex authorization | 1.3x | 5.5x | | ||
| To run the benchmarks against previous versions of this module, you can run `./benchmark/index.js`. | ||
| Run benchmarks: `npm run benchmark` | ||
@@ -588,0 +604,0 @@ ## Error Handling |
201543
1.13%5010
0.38%721
2.27%