🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@marcbachmann/cel-js

Package Overview
Dependencies
Maintainers
1
Versions
87
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@marcbachmann/cel-js - npm Package Compare versions

Comparing version
7.5.0
to
7.5.1
+7
-6
lib/evaluator.js

@@ -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) {

@@ -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)}`,

@@ -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()))

@@ -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}'`)

{
"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 [![npm version](https://img.shields.io/npm/v/@marcbachmann/cel-js.svg)](https://www.npmjs.com/package/@marcbachmann/cel-js) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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