@statedelta-libs/expressions
v0.0.3
Published
JSON DSL compiler for optimized functions - StateDelta expression engine
Maintainers
Readme
@statedelta-libs/expressions
Compilador de JSON DSL para funções otimizadas.
Características
- Compilação - Compila uma vez, executa milhões de vezes
- Alta performance - ~25-30M ops/s após compilação
- Scope externo - Funções vêm via scope, não hardcoded
- Accessor customizado - Suporte a
ctx.get(path)para objetos especiais - $pipe - Composição com valor inicial (sintaxe DSL)
- Path syntax - Suporte a wildcards (
items[*].price) - Dependency extraction - Para dirty tracking/reatividade
- Integração - Usa
@statedelta-libs/conditionspara condicionais - Type-safe - TypeScript nativo com inferência
Instalação
npm install @statedelta-libs/expressions
# ou
pnpm add @statedelta-libs/expressionsQuick Start
import { compile, evaluate } from '@statedelta-libs/expressions';
import { filter, map, sum } from '@statedelta-libs/operators';
// Define o scope com funções disponíveis
const scope = { filter, map, sum };
// Compilar expressão com $pipe
const expr = compile({
$pipe: [
{ $: "items" },
{ $fn: "filter", args: [(item) => item.active] },
{ $fn: "map", args: [(item) => item.price] },
{ $fn: "sum" }
]
}, { scope });
// Executar (muito rápido!)
const data = {
items: [
{ name: "A", price: 10, active: true },
{ name: "B", price: 20, active: false },
{ name: "C", price: 30, active: true }
]
};
expr.fn(data); // 40
expr.deps; // ["items"]API
compile()
Compila expressão JSON DSL para função otimizada usando closures.
const { fn, deps, hash } = compile(expression, { scope });
fn(data); // Executa
deps; // Paths que a expressão depende
hash; // Hash único (para cache)compileAST()
Compila usando AST + new Function(). Mais rápido na execução, ideal para expressões executadas muitas vezes.
import { compileAST } from '@statedelta-libs/expressions';
const { fn, deps, hash } = compileAST(expression, { scope });
fn(data); // Executa (mais rápido que compile())Quando usar cada um:
| Cenário | Recomendação | Por quê |
|---------|--------------|---------|
| Execução única | compile() | Compilação mais rápida |
| Poucas execuções (<8x) | compile() | Overhead de AST não compensa |
| Muitas execuções (>8x) | compileAST() | Execução ~25-170% mais rápida |
| Hot path crítico | compileAST() | V8 JIT otimiza melhor |
| Expressões complexas | compileAST() | Ganho maior em nested calls |
evaluate() / evaluateAST()
Compila e executa em um passo.
const result = evaluate(
{ $fn: "add", args: [{ $: "a" }, { $: "b" }] },
{ a: 1, b: 2 },
{ scope: { add: (a, b) => a + b } }
);
// 3
// Versão AST
const result = evaluateAST(expression, data, { scope });extractDeps()
Extrai dependências sem compilar.
const deps = extractDeps({
$if: "$isVip",
then: { $: "price.vip" },
else: { $: "price.regular" }
});
// ["$isVip", "price.vip", "price.regular"]Tipos de Expressão
Literal
42
"hello"
true
[1, 2, 3]
{ a: 1 }Reference ($)
{ $: "user.name" } // Acessa user.name
{ $: "items[0].price" } // Acessa índice
{ $: "items[*].price" } // Wildcard → [10, 20, 30]Conditional ($if)
{
$if: { path: "age", op: "gte", value: 18 },
then: "adult",
else: "minor"
}
// Shorthand
{ $if: "$isVip", then: 0.2, else: 0 }
{ $if: "!$isBlocked", then: "allowed" }Pipe ($pipe)
Composição com valor inicial - o valor passa por cada função em sequência.
{
$pipe: [
{ $: "items" }, // Valor inicial
{ $fn: "filter", args: [predicateFn] }, // Retorna função curried
{ $fn: "map", args: [mapperFn] }, // Retorna função curried
{ $fn: "sum" } // Referência à função
]
}Function Call ($fn)
Chama função do scope com argumentos compilados.
// Com args: chama a função
{ $fn: "add", args: [{ $: "a" }, { $: "b" }] }
// → scope.add(data.a, data.b)
// Sem args: retorna referência à função (útil em $pipe)
{ $fn: "sum" }
// → scope.sum
// Com args vazio: chama função sem argumentos
{ $fn: "getTimestamp", args: [] }
// → scope.getTimestamp()Condition (delegado)
// Detectado automaticamente e delegado para @statedelta-libs/conditions
{ path: "user.age", op: "gte", value: 18 }Nota: Apenas objetos com operadores de condition válidos (
eq,neq,gt,gte,lt,lte,in,notIn,contains,notContains,exists,notExists,matches,notMatches,startsWith,endsWith) são detectados como conditions. Objetos comop: "set"ou outros operadores não são conditions e são tratados como literals.
Scope
O scope define quais funções estão disponíveis para $fn:
import * as R from '@statedelta-libs/operators';
// Todas as funções do operators
const scope = R;
// Ou seleção específica
const scope = {
add: R.add,
filter: R.filter,
map: R.map,
sum: R.sum,
// Funções custom
double: (n) => n * 2,
isActive: (item) => item.active
};
const { fn } = compile(expression, { scope });Accessor Customizado
Para objetos que usam interface de acesso customizada (como ctx.get(path)), use o accessor:
// Contexto com método get() customizado
interface TickContext {
get(path: string): unknown;
}
const accessor = (path: string, ctx: TickContext) => ctx.get(path);
// Compilação com accessor
const { fn } = compile<TickContext>(
{ $: "hp:value" },
{ accessor }
);
fn(tickContext); // usa ctx.get('hp:value')
// Com scope + accessor
const { fn } = compile<TickContext>(
{ $fn: "add", args: [{ $: "a" }, { $: "b" }] },
{ scope, accessor }
);O accessor é propagado para:
- Referências (
{ $: "path" }) - Shorthand de
$if({ $if: "path", ... }) - Argumentos de
$fn - Steps de
$pipe - Conditions (delegadas para
@statedelta-libs/conditions)
Performance: Igual ao acesso direto (~25-30M ops/s).
Path Syntax
| Path | Resultado |
|------|-----------|
| user | data.user |
| user.name | data.user.name |
| items[0] | data.items[0] |
| items[*].price | data.items.map(i => i.price) |
Cache
import { cache, cached } from '@statedelta-libs/expressions';
// Com scope
const compiled = cache.get(expression, { scope });
// Ou função helper
const compiled = cached(expression, { scope });
// Gerenciar
cache.size; // Tamanho atual
cache.clear(); // LimparPerformance
compile() - Closures
| Operação | Velocidade | |----------|------------| | Compile (literal) | ~6.5M ops/s | | Compile (ref) | ~3M ops/s | | Compile (fn nested) | ~550K ops/s | | Compile (complex) | ~300K ops/s | | Execute (ref) | ~28-30M ops/s | | Execute (fn nested) | ~9M ops/s | | Execute (com accessor) | ~23-30M ops/s |
compileAST() - AST + new Function
| Operação | Velocidade | |----------|------------| | Compile (literal) | ~1.2M ops/s | | Compile (ref) | ~800K ops/s | | Compile (fn nested) | ~230K ops/s | | Compile (complex) | ~130K ops/s | | Execute (ref) | ~27M ops/s | | Execute (fn nested) | ~25M ops/s ⚡ |
Comparação Execução
| Cenário | Closures | AST | Ganho AST | |---------|----------|-----|-----------| | fn nested | 9M ops/s | 25M ops/s | +169% | | ref 4 níveis | 16M ops/s | 27M ops/s | +69% | | conditional nested | 20M ops/s | 25M ops/s | +25% |
Break-even: ~8 execuções - após isso, compileAST() compensa o tempo extra de compilação.
Segurança
Ambas as abordagens são seguras contra injeção de código:
| Método | Proteção |
|--------|----------|
| compile() | Não gera código string - apenas compõe funções |
| compileAST() | Usa destructuring que valida identificadores automaticamente |
// Tentativa de injeção - FALHA em ambos
{ $fn: "add; console.log('hacked'); //" }
// compile(): tenta acessar scope["add; console.log..."] → undefined
// compileAST(): destructuring inválido → SyntaxErrorFunções só são acessíveis se existirem no scope fornecido pelo desenvolvedor.
Type Detection
O compilador detecta automaticamente o tipo de expressão baseado na estrutura do objeto:
| Tipo | Detecção | Exemplo |
|------|----------|---------|
| Reference | { $ } presente | { $: "user.name" } |
| Conditional | { $if, then } presentes | { $if: cond, then: x, else: y } |
| Function | { $fn } presente | { $fn: "add", args: [...] } |
| Pipe | { $pipe } presente | { $pipe: [...] } |
| Condition | { path, op } com operador válido | { path: "age", op: "gte", value: 18 } |
| Literal | Nenhum dos acima | { foo: "bar" }, 42, "hello" |
Distinção entre Conditions e Effect Objects
Objetos com path e op só são tratados como conditions se op for um operador válido:
// ✓ Condition (op: "eq" é operador válido)
{ path: "user.age", op: "eq", value: 18 }
// ✓ Literal object (op: "set" NÃO é operador de condition)
{ resource: "state", op: "set", path: "currentPlayer", value: "X" }Isso permite que effect objects de handlers (que usam { resource, op: "set", path, value }) sejam processados corretamente sem serem confundidos com conditions.
TypeScript
import type {
Expression,
CompiledExpression,
RefExpr,
ConditionalExpr,
FnExpr,
PipeExpr,
Scope,
CompileOptions,
AccessorFn,
} from '@statedelta-libs/expressions';
// Type guards
import {
isRef,
isConditional,
isFn,
isPipe,
isCondition,
isLiteral,
} from '@statedelta-libs/expressions';Migração de v0.1.x
Breaking Changes
Builtins removidos: Funções não são mais hardcoded. Passe via
scope:// Antes compile({ $fn: "add", args: [1, 2] }) // Depois compile({ $fn: "add", args: [1, 2] }, { scope: { add } })$fn sem args retorna função: Para chamar sem argumentos, use
args: []:// Retorna a função { $fn: "sum" } // Chama a função { $fn: "getTime", args: [] }Removidos:
registerFunction,hasFunction,getFunctionNamesNovo:
$pipecomo sintaxe DSL para composição
Licença
MIT © Anderson D. Rosa
