@statedelta-libs/expressions 2.3.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -781
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +241 -667
- package/dist/index.d.ts +241 -667
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,868 +5,227 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@statedelta-libs/expressions)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
##
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- **Scope externo** - Funções puras vêm via scope, não hardcoded
|
|
14
|
-
- **Context externo** - HOCs de continuidade via context (`tryCatch`, `transaction`, etc.)
|
|
15
|
-
- **RuntimeCtx unificado** - Contexto de runtime limpo com `{ data, get }`
|
|
16
|
-
- **Accessor customizado** - Suporte a `ctx.get(path)` para objetos especiais
|
|
17
|
-
- **$pipe** - Composição com valor inicial (sintaxe DSL)
|
|
18
|
-
- **$cb** - Callback expressions para HOCs de continuidade
|
|
19
|
-
- **Path syntax** - Suporte a wildcards (`items[*].price`)
|
|
20
|
-
- **Dependency extraction** - Para dirty tracking/reatividade
|
|
21
|
-
- **Conditions nativo** - Condicionais integradas com expressions nos lados
|
|
22
|
-
- **normalize()** - Helper externo para custom transforms (`$query`, `$mapper`, etc.)
|
|
23
|
-
- **resolveBoundaries()** - Resolve boundaries customizados (`$simulate`, `$context`, etc.) com compilação independente
|
|
24
|
-
- **Type-safe** - TypeScript nativo com inferência
|
|
8
|
+
## O que é
|
|
9
|
+
|
|
10
|
+
Um compilador que transforma expressões declarativas em JSON puro em funções JavaScript de alta performance. A entrada é sempre JSON serializável. A saída é sempre uma `(data) => result`.
|
|
11
|
+
|
|
12
|
+
O compilador não conhece nenhuma função — todas vêm via `scope` fornecido pelo consumer. O mesmo motor serve para matemática, queries de game engine, ETL, NoCode, ou qualquer domínio.
|
|
25
13
|
|
|
26
14
|
## Instalação
|
|
27
15
|
|
|
28
16
|
```bash
|
|
29
|
-
npm install @statedelta-libs/expressions
|
|
30
|
-
# ou
|
|
31
17
|
pnpm add @statedelta-libs/expressions
|
|
32
18
|
```
|
|
33
19
|
|
|
34
20
|
## Quick Start
|
|
35
21
|
|
|
36
22
|
```typescript
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
};
|
|
23
|
+
import { ExpressionCompiler } from '@statedelta-libs/expressions';
|
|
24
|
+
|
|
25
|
+
const compiler = new ExpressionCompiler({
|
|
26
|
+
scope: {
|
|
27
|
+
add: (a, b) => a + b,
|
|
28
|
+
multiply: (a, b) => a * b,
|
|
29
|
+
filter: (pred) => (arr) => arr.filter(pred),
|
|
30
|
+
sum: (arr) => arr.reduce((a, b) => a + b, 0),
|
|
31
|
+
},
|
|
32
|
+
});
|
|
47
33
|
|
|
48
|
-
//
|
|
49
|
-
const
|
|
34
|
+
// Compila uma vez
|
|
35
|
+
const compiled = compiler.compile({
|
|
50
36
|
$pipe: [
|
|
51
37
|
{ $: "items" },
|
|
52
|
-
{ $fn: "filter", args: [{ $
|
|
53
|
-
{ $fn: "map", args: [{ $fn: "getPrice" }] },
|
|
38
|
+
{ $fn: "filter", args: [{ $arrow: { $: "item.active" }, args: ["item"] }] },
|
|
54
39
|
{ $fn: "sum" }
|
|
55
40
|
]
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Executar (muito rápido!)
|
|
59
|
-
const data = {
|
|
60
|
-
items: [
|
|
61
|
-
{ name: "A", price: 10, active: true },
|
|
62
|
-
{ name: "B", price: 20, active: false },
|
|
63
|
-
{ name: "C", price: 30, active: true }
|
|
64
|
-
]
|
|
65
|
-
};
|
|
41
|
+
});
|
|
66
42
|
|
|
67
|
-
|
|
68
|
-
|
|
43
|
+
// Executa milhões de vezes
|
|
44
|
+
compiled.fn({ items: [{ active: true, price: 10 }, { active: false, price: 20 }] });
|
|
45
|
+
compiled.deps; // ["items"]
|
|
69
46
|
```
|
|
70
47
|
|
|
71
|
-
##
|
|
72
|
-
|
|
73
|
-
### compile()
|
|
74
|
-
|
|
75
|
-
Compila expressão JSON DSL para função otimizada usando closures.
|
|
48
|
+
## Os 5 Primitivos
|
|
76
49
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
fn(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
50
|
+
| Primitivo | O que faz | Exemplo DSL | JS equivalente |
|
|
51
|
+
|-----------|-----------|-------------|----------------|
|
|
52
|
+
| `$` | Referência a path | `{ $: "user.name" }` | `data.user.name` |
|
|
53
|
+
| `$fn` | Invocação/referência de função | `{ $fn: "add", args: [...] }` | `add(a, b)` |
|
|
54
|
+
| `$if` | Condicional | `{ $if: cond, then: a, else: b }` | `cond ? a : b` |
|
|
55
|
+
| `$pipe` | Composição left-to-right | `{ $pipe: [x, f, g] }` | `g(f(x))` |
|
|
56
|
+
| `$arrow` | Closure deferida | `{ $arrow: body, args: ["x"] }` | `(x) => body` |
|
|
84
57
|
|
|
85
|
-
|
|
58
|
+
Tudo 100% JSON-serializável. Funções nunca aparecem no DSL — são referenciadas por nome e resolvidas contra o scope em runtime.
|
|
86
59
|
|
|
87
|
-
|
|
60
|
+
## Dois Modos de Compilação
|
|
88
61
|
|
|
89
62
|
```typescript
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const { fn, deps, hash } = compileAST(expression, { scope });
|
|
93
|
-
|
|
94
|
-
fn(data); // Executa (mais rápido que compile())
|
|
63
|
+
compiler.compile(expr); // closures — compilação rápida, ~9-20M ops/s
|
|
64
|
+
compiler.jit(expr); // JIT — compilação lenta, ~25-27M ops/s de execução
|
|
95
65
|
```
|
|
96
66
|
|
|
97
|
-
**
|
|
67
|
+
**Closures** compõe funções JavaScript aninhadas. Ideal para expressões executadas poucas vezes ou ambientes com CSP restritivo.
|
|
98
68
|
|
|
99
|
-
|
|
100
|
-
|---------|--------------|---------|
|
|
101
|
-
| Execução única | `compile()` | Compilação mais rápida |
|
|
102
|
-
| Poucas execuções (<8x) | `compile()` | Overhead de AST não compensa |
|
|
103
|
-
| Muitas execuções (>8x) | `compileAST()` | Execução ~25-170% mais rápida |
|
|
104
|
-
| Hot path crítico | `compileAST()` | V8 JIT otimiza melhor |
|
|
105
|
-
| Expressões complexas | `compileAST()` | Ganho maior em nested calls |
|
|
69
|
+
**JIT** gera código JavaScript via AST e `new Function()`. Ideal para hot paths executados muitas vezes. Break-even em ~8 execuções.
|
|
106
70
|
|
|
107
|
-
|
|
71
|
+
## Extensibilidade
|
|
108
72
|
|
|
109
|
-
|
|
73
|
+
### Scope — funções disponíveis
|
|
110
74
|
|
|
111
75
|
```typescript
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
accessor: (path) => ctx.get(path) // closure que captura ctx
|
|
76
|
+
const compiler = new ExpressionCompiler({
|
|
77
|
+
scope: {
|
|
78
|
+
add: (a, b) => a + b,
|
|
79
|
+
filter: (pred) => (arr) => arr.filter(pred),
|
|
80
|
+
tryCatch: (arrow, params) => {
|
|
81
|
+
try { return arrow(); }
|
|
82
|
+
catch { return params?.fallback ?? null; }
|
|
120
83
|
},
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
fn(); // usa ctx.get("hp") + ctx.get("mp")
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
#### lexicalPrefix (compileAST)
|
|
129
|
-
|
|
130
|
-
Para máxima performance em paths específicos, use `lexicalPrefix` para acesso direto via destructuring:
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
const ctx = new TickContext();
|
|
134
|
-
|
|
135
|
-
const { fn } = compileAST(
|
|
136
|
-
{ $fn: "add", args: [{ $: "hp" }, { $: "params.damage" }] },
|
|
137
|
-
{
|
|
138
|
-
scope: {
|
|
139
|
-
add,
|
|
140
|
-
accessor: (path) => ctx.get(path)
|
|
141
|
-
},
|
|
142
|
-
useAccessor: true,
|
|
143
|
-
lexicalPrefix: "params" // params.* vai direto, resto via accessor
|
|
144
|
-
}
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
fn({ params: { damage: 25 } }); // ctx.get("hp") + params.damage
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### evaluate() / evaluateAST()
|
|
151
|
-
|
|
152
|
-
Compila e executa em um passo.
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
const result = evaluate(
|
|
156
|
-
{ $fn: "add", args: [{ $: "a" }, { $: "b" }] },
|
|
157
|
-
{ a: 1, b: 2 },
|
|
158
|
-
{ scope: { add: (a, b) => a + b } }
|
|
159
|
-
);
|
|
160
|
-
// 3
|
|
161
|
-
|
|
162
|
-
// Versão AST
|
|
163
|
-
const result = evaluateAST(expression, data, { scope });
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### extractDeps()
|
|
167
|
-
|
|
168
|
-
Extrai dependências sem compilar.
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
const deps = extractDeps({
|
|
172
|
-
$if: "$isVip",
|
|
173
|
-
then: { $: "price.vip" },
|
|
174
|
-
else: { $: "price.regular" }
|
|
84
|
+
},
|
|
175
85
|
});
|
|
176
|
-
// ["$isVip", "price.vip", "price.regular"]
|
|
177
86
|
```
|
|
178
87
|
|
|
179
|
-
###
|
|
88
|
+
### Normalize — DSL customizado
|
|
180
89
|
|
|
181
|
-
|
|
90
|
+
Transforma sugar syntax em DSL puro **antes** da compilação. Para quando o resultado é expression DSL válido.
|
|
182
91
|
|
|
183
92
|
```typescript
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const transforms: Transforms = {
|
|
187
|
-
$query: (node) => ({
|
|
188
|
-
$fn: "__query",
|
|
189
|
-
args: [node.$query as string, (node.params ?? {}) as Expression],
|
|
190
|
-
}),
|
|
93
|
+
const transforms = {
|
|
94
|
+
$double: (node) => ({ $fn: "multiply", args: [node.$double, 2] }),
|
|
191
95
|
};
|
|
192
96
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
{ $query: "isAttacked", params: { row: { $: "kingRow" } } },
|
|
196
|
-
transforms
|
|
197
|
-
);
|
|
198
|
-
// → { $fn: "__query", args: ["isAttacked", { row: { $: "kingRow" } }] }
|
|
199
|
-
|
|
200
|
-
// Agora pode compilar normalmente
|
|
201
|
-
const { fn } = compile(pure, { scope });
|
|
97
|
+
const pure = compiler.normalize({ $double: { $: "value" } }, transforms);
|
|
98
|
+
compiler.compile(pure);
|
|
202
99
|
```
|
|
203
100
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
### Literal
|
|
101
|
+
### Boundaries — compiladores externos
|
|
207
102
|
|
|
208
|
-
|
|
209
|
-
42
|
|
210
|
-
"hello"
|
|
211
|
-
true
|
|
212
|
-
[1, 2, 3]
|
|
213
|
-
{ a: 1 }
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
### Reference ($)
|
|
103
|
+
Intercepta nós **durante** a compilação e terceiriza para outro algoritmo. Para quando o nó precisa de um compilador completamente diferente.
|
|
217
104
|
|
|
218
105
|
```typescript
|
|
219
|
-
{
|
|
220
|
-
{ $: "items[0].price" } // Acessa índice
|
|
221
|
-
{ $: "items[*].price" } // Wildcard → [10, 20, 30]
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
### Conditional ($if)
|
|
225
|
-
|
|
226
|
-
```typescript
|
|
227
|
-
{
|
|
228
|
-
$if: { left: { $: "age" }, op: "gte", right: 18 },
|
|
229
|
-
then: "adult",
|
|
230
|
-
else: "minor"
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Shorthand
|
|
234
|
-
{ $if: "$isVip", then: 0.2, else: 0 }
|
|
235
|
-
{ $if: "!$isBlocked", then: "allowed" }
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
### Pipe ($pipe)
|
|
239
|
-
|
|
240
|
-
Composição com valor inicial - o valor passa por cada função em sequência.
|
|
241
|
-
|
|
242
|
-
```typescript
|
|
243
|
-
{
|
|
244
|
-
$pipe: [
|
|
245
|
-
{ $: "items" }, // Valor inicial
|
|
246
|
-
{ $fn: "filter", args: [{ $fn: "isActive" }] }, // Curried filter
|
|
247
|
-
{ $fn: "map", args: [{ $fn: "getPrice" }] }, // Curried map
|
|
248
|
-
{ $fn: "sum" } // Referência à função
|
|
249
|
-
]
|
|
250
|
-
}
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
### Function Call ($fn)
|
|
254
|
-
|
|
255
|
-
Chama função do scope com argumentos compilados.
|
|
256
|
-
|
|
257
|
-
```typescript
|
|
258
|
-
// Com args: chama a função
|
|
259
|
-
{ $fn: "add", args: [{ $: "a" }, { $: "b" }] }
|
|
260
|
-
// → scope.add(data.a, data.b)
|
|
261
|
-
|
|
262
|
-
// Sem args: retorna referência à função (útil em $pipe)
|
|
263
|
-
{ $fn: "sum" }
|
|
264
|
-
// → scope.sum
|
|
265
|
-
|
|
266
|
-
// Com args vazio: chama função sem argumentos
|
|
267
|
-
{ $fn: "getTimestamp", args: [] }
|
|
268
|
-
// → scope.getTimestamp()
|
|
269
|
-
|
|
270
|
-
// Passando função como argumento (referência via $fn)
|
|
271
|
-
{ $fn: "filter", args: [{ $fn: "isActive" }] }
|
|
272
|
-
// → scope.filter(scope.isActive)
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
### Condition
|
|
276
|
-
|
|
277
|
-
Condicionais compiladas internamente. Ambos os lados aceitam qualquer expressão:
|
|
278
|
-
|
|
279
|
-
```typescript
|
|
280
|
-
// Ref vs literal (básico)
|
|
281
|
-
{ left: { $: "user.age" }, op: "gte", right: 18 }
|
|
282
|
-
|
|
283
|
-
// Ref vs Ref
|
|
284
|
-
{ left: { $: "price" }, op: "lte", right: { $: "budget" } }
|
|
285
|
-
|
|
286
|
-
// $fn nos lados
|
|
287
|
-
{ left: { $fn: "add", args: [{ $: "a" }, { $: "b" }] }, op: "gt", right: 100 }
|
|
288
|
-
|
|
289
|
-
// $pipe no lado
|
|
290
|
-
{ left: { $pipe: [{ $: "name" }, { $fn: "upper" }] }, op: "eq", right: "ADMIN" }
|
|
291
|
-
|
|
292
|
-
// Condition group
|
|
293
|
-
{
|
|
294
|
-
logic: "AND",
|
|
295
|
-
conditions: [
|
|
296
|
-
{ left: { $: "active" }, op: "eq", right: true },
|
|
297
|
-
{ left: { $fn: "len", args: [{ $: "items" }] }, op: "gte", right: 3 }
|
|
298
|
-
]
|
|
299
|
-
}
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
**Operadores:** `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `notIn`, `contains`, `notContains`, `exists`, `notExists`, `matches`, `notMatches`, `startsWith`, `endsWith`
|
|
303
|
-
|
|
304
|
-
### Callback ($cb)
|
|
305
|
-
|
|
306
|
-
HOC de continuidade - executa body dentro de um context function.
|
|
307
|
-
|
|
308
|
-
```typescript
|
|
309
|
-
// Básico - tryCatch
|
|
310
|
-
{ $cb: "tryCatch", body: { $fn: "query" } }
|
|
311
|
-
|
|
312
|
-
// Com parâmetros
|
|
313
|
-
{ $cb: "transaction", body: { $fn: "save" }, params: { isolation: "serializable" } }
|
|
314
|
-
|
|
315
|
-
// Aninhado
|
|
316
|
-
{
|
|
317
|
-
$cb: "tryCatch",
|
|
318
|
-
body: {
|
|
319
|
-
$cb: "transaction",
|
|
320
|
-
body: { $fn: "save" }
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
## Context
|
|
326
|
-
|
|
327
|
-
O context define HOCs disponíveis para `$cb`. Diferente de scope (funções puras), context functions controlam a execução do body.
|
|
328
|
-
|
|
329
|
-
### RuntimeCtx
|
|
330
|
-
|
|
331
|
-
O context recebe um `RuntimeCtx` unificado com `{ data, get }`:
|
|
332
|
-
|
|
333
|
-
```typescript
|
|
334
|
-
interface RuntimeCtx<T> {
|
|
335
|
-
data: T; // Dados do runtime
|
|
336
|
-
get: (path: string) => unknown; // Accessor de paths
|
|
337
|
-
}
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
### ContextFn
|
|
106
|
+
import type { BoundaryDef } from '@statedelta-libs/expressions';
|
|
341
107
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
params?: unknown // Parâmetros do $cb (opcional)
|
|
347
|
-
) => R;
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
### Exemplos
|
|
351
|
-
|
|
352
|
-
```typescript
|
|
353
|
-
import type { Context, ContextFn, RuntimeCtx } from '@statedelta-libs/expressions';
|
|
354
|
-
|
|
355
|
-
// tryCatch - captura erros
|
|
356
|
-
const tryCatch: ContextFn = (cb, ctx, params) => {
|
|
357
|
-
try {
|
|
358
|
-
return cb(ctx);
|
|
359
|
-
} catch {
|
|
360
|
-
return (params as { fallback?: unknown })?.fallback ?? null;
|
|
361
|
-
}
|
|
108
|
+
// $raw — passthrough, nada é compilado
|
|
109
|
+
const rawBoundary: BoundaryDef = {
|
|
110
|
+
check: (node) => "$raw" in node,
|
|
111
|
+
handle: (node) => () => node.$raw,
|
|
362
112
|
};
|
|
363
113
|
|
|
364
|
-
//
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
return result;
|
|
372
|
-
} catch (e) {
|
|
373
|
-
tx.rollback();
|
|
374
|
-
throw e;
|
|
375
|
-
}
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
// cached - pode ignorar o cb e retornar outro valor
|
|
379
|
-
const cached: ContextFn = (cb, ctx, params) => {
|
|
380
|
-
const key = (params as { key: string })?.key;
|
|
381
|
-
if (cache.has(key)) {
|
|
382
|
-
return cache.get(key); // Não executa cb
|
|
383
|
-
}
|
|
384
|
-
const result = cb(ctx);
|
|
385
|
-
cache.set(key, result);
|
|
386
|
-
return result;
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
// simulate - injeta accessor customizado
|
|
390
|
-
const simulate: ContextFn = (cb, ctx, params) => {
|
|
391
|
-
// Cria get que lê de store simulado
|
|
392
|
-
const simulatedGet = (path: string) => store.getSimulated(path);
|
|
393
|
-
// Body usa simulatedGet ao invés do get padrão
|
|
394
|
-
return cb({ ...ctx, get: simulatedGet });
|
|
114
|
+
// $rules — DSL estrangeiro com compilador próprio
|
|
115
|
+
const rulesBoundary: BoundaryDef = {
|
|
116
|
+
check: (node) => "$rules" in node,
|
|
117
|
+
handle: (node) => {
|
|
118
|
+
const compiled = ruleEngine.compile(node.$rules);
|
|
119
|
+
return (data) => compiled.evaluate(data);
|
|
120
|
+
},
|
|
395
121
|
};
|
|
396
122
|
|
|
397
|
-
const
|
|
123
|
+
const compiler = new ExpressionCompiler({
|
|
124
|
+
scope,
|
|
125
|
+
boundaries: [rawBoundary, rulesBoundary],
|
|
126
|
+
});
|
|
398
127
|
|
|
399
|
-
|
|
128
|
+
// $rules é interceptado pelo boundary, compilado pelo ruleEngine
|
|
129
|
+
compiler.compile({
|
|
130
|
+
$fn: "add",
|
|
131
|
+
args: [{ $: "base" }, { $rules: { when: "vip", then: 20 } }]
|
|
132
|
+
});
|
|
400
133
|
```
|
|
401
134
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
| Ação | Exemplo |
|
|
405
|
-
|------|---------|
|
|
406
|
-
| **Executar normalmente** | `return cb(ctx)` |
|
|
407
|
-
| **Modificar data** | `return cb({ ...ctx, data: { ...ctx.data, extra } })` |
|
|
408
|
-
| **Injetar accessor** | `return cb({ ...ctx, get: customGet })` |
|
|
409
|
-
| **Ignorar body** | `return cachedValue` (não chama cb) |
|
|
410
|
-
| **Capturar erros** | `try { cb(ctx) } catch { ... }` |
|
|
411
|
-
| **Usar ctx.get** | `const val = ctx.get("user.name")` |
|
|
412
|
-
|
|
413
|
-
## Scope
|
|
414
|
-
|
|
415
|
-
O scope define quais funções estão disponíveis para `$fn`:
|
|
416
|
-
|
|
417
|
-
```typescript
|
|
418
|
-
import * as R from '@statedelta-libs/operators';
|
|
419
|
-
|
|
420
|
-
// Todas as funções do operators
|
|
421
|
-
const scope = R;
|
|
422
|
-
|
|
423
|
-
// Ou seleção específica
|
|
424
|
-
const scope = {
|
|
425
|
-
add: R.add,
|
|
426
|
-
filter: R.filter,
|
|
427
|
-
map: R.map,
|
|
428
|
-
sum: R.sum,
|
|
429
|
-
// Funções custom (predicados, mappers, etc.)
|
|
430
|
-
double: (n) => n * 2,
|
|
431
|
-
isActive: (item) => item.active,
|
|
432
|
-
getPrice: (item) => item.price,
|
|
433
|
-
};
|
|
135
|
+
O handler é uma closure auto-suficiente — captura o que precisa (outros compiladores, databases, etc.) por fora. Zero overhead quando não há boundaries registrados.
|
|
434
136
|
|
|
435
|
-
|
|
436
|
-
|
|
137
|
+
| | `normalize()` | `BoundaryDef` |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| **Quando roda** | Antes da compilação | Durante a compilação (no walk) |
|
|
140
|
+
| **Retorno** | `Expression` (DSL puro) | `CompiledFn` (função pronta) |
|
|
141
|
+
| **Uso típico** | Sugar syntax | DSL estrangeiro, `$raw`, rule engines |
|
|
437
142
|
|
|
438
|
-
|
|
143
|
+
### Handlers — services com contexto
|
|
439
144
|
|
|
440
|
-
|
|
145
|
+
O `scope` é para funções puras e stateless. Quando o consumer precisa de **services inteligentes** que acessam o sistema (accessor, outros handlers, o compilador, o scope), usa `handlers`.
|
|
441
146
|
|
|
442
147
|
```typescript
|
|
443
|
-
import
|
|
444
|
-
|
|
445
|
-
// Define transforms
|
|
446
|
-
const transforms: Transforms = {
|
|
447
|
-
$query: (node) => ({
|
|
448
|
-
$fn: "__query",
|
|
449
|
-
args: [node.$query as string, (node.params ?? {}) as Expression],
|
|
450
|
-
}),
|
|
451
|
-
$mapper: (node) => ({
|
|
452
|
-
$fn: "__mapper",
|
|
453
|
-
args: [node.$mapper as string],
|
|
454
|
-
}),
|
|
455
|
-
};
|
|
148
|
+
import type { HandlerContext } from '@statedelta-libs/expressions';
|
|
456
149
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
);
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
### Aninhamento
|
|
477
|
-
|
|
478
|
-
O `normalize` percorre toda a árvore, então transforms funcionam em qualquer posição:
|
|
479
|
-
|
|
480
|
-
```typescript
|
|
481
|
-
normalize(
|
|
482
|
-
{
|
|
483
|
-
$if: {
|
|
484
|
-
left: { $query: "isAttacked", params: { row: 0 } },
|
|
485
|
-
op: "eq",
|
|
486
|
-
right: true
|
|
150
|
+
const compiler = new ExpressionCompiler({
|
|
151
|
+
scope: { add: (a, b) => a + b },
|
|
152
|
+
handlers: {
|
|
153
|
+
query: {
|
|
154
|
+
find(key: string) {
|
|
155
|
+
return db.find(key);
|
|
156
|
+
},
|
|
157
|
+
findAll() {
|
|
158
|
+
// chamar outro handler
|
|
159
|
+
const valid = this.handlers.validation.check("all");
|
|
160
|
+
return valid ? db.findAll() : [];
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
validation: {
|
|
164
|
+
check(value: unknown) {
|
|
165
|
+
// chamar scope fn
|
|
166
|
+
return this.scope.add(value != null ? 1 : 0, 0) > 0;
|
|
167
|
+
},
|
|
487
168
|
},
|
|
488
|
-
then: "check",
|
|
489
|
-
else: "safe"
|
|
490
169
|
},
|
|
491
|
-
|
|
492
|
-
);
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
### Proteção contra loops
|
|
496
|
-
|
|
497
|
-
Se um transform retornar objeto com a mesma chave, erro é lançado:
|
|
498
|
-
|
|
499
|
-
```typescript
|
|
500
|
-
const badTransforms = {
|
|
501
|
-
$loop: (node) => ({ $loop: node.$loop }), // mesma chave!
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
normalize({ $loop: "test" }, badTransforms);
|
|
505
|
-
// Error: Transform "$loop" returned object with same key — infinite loop
|
|
170
|
+
});
|
|
506
171
|
```
|
|
507
172
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
Para DSL customizados que precisam de **compilação independente** dos slots internos, use `resolveBoundaries()`. Diferente de `normalize()` (que apenas transforma), boundaries param o fluxo de compilação e delegam para um resolver externo.
|
|
511
|
-
|
|
512
|
-
### Conceito
|
|
513
|
-
|
|
514
|
-
Um **boundary** é um "corpo estranho" no DSL que:
|
|
515
|
-
1. **Para** o fluxo normal de compilação
|
|
516
|
-
2. **Delega** para um resolver customizado
|
|
517
|
-
3. O resolver **compila slots internos** independentemente
|
|
518
|
-
4. **Substitui** por uma referência no scope
|
|
173
|
+
Handlers são invocados via `$fn` com sintaxe `"namespace:method"`:
|
|
519
174
|
|
|
175
|
+
```json
|
|
176
|
+
{ "$fn": "query:find", "args": [{ "$": "userId" }] }
|
|
177
|
+
{ "$fn": "validation:check", "args": [{ "$": "value" }] }
|
|
520
178
|
```
|
|
521
|
-
DSL "rico" DSL puro
|
|
522
|
-
{ $simulate: {...} } → { $fn: "__simulate_0", args: [] }
|
|
523
|
-
+ scope["__simulate_0"] = fn compilada
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
### Uso básico
|
|
527
179
|
|
|
528
|
-
|
|
529
|
-
import { resolveBoundaries, compile, type BoundaryResolvers } from '@statedelta-libs/expressions';
|
|
530
|
-
|
|
531
|
-
const resolvers: BoundaryResolvers = {
|
|
532
|
-
$context: (node, { compile: compileFn, genId, scope }) => {
|
|
533
|
-
const id = `__context_${genId()}`;
|
|
534
|
-
const body = (node.$context as { body: Expression }).body;
|
|
535
|
-
|
|
536
|
-
// Compila o body internamente
|
|
537
|
-
const bodyFn = compileFn(body, { scope }).fn;
|
|
538
|
-
|
|
539
|
-
// Retorna função com lifecycle begin/end
|
|
540
|
-
const contextFn = () => (data: unknown) => {
|
|
541
|
-
console.log("begin");
|
|
542
|
-
try {
|
|
543
|
-
return bodyFn(data);
|
|
544
|
-
} finally {
|
|
545
|
-
console.log("end");
|
|
546
|
-
}
|
|
547
|
-
};
|
|
548
|
-
|
|
549
|
-
return {
|
|
550
|
-
expr: { $fn: id, args: [] },
|
|
551
|
-
scopeEntry: [id, contextFn],
|
|
552
|
-
};
|
|
553
|
-
},
|
|
554
|
-
};
|
|
180
|
+
O contexto é acessado via `this`, injetado automaticamente via `.bind()` no construtor. O `HandlerContext` é criado **uma vez** — zero alocação por chamada:
|
|
555
181
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
182
|
+
| Campo | O que contém |
|
|
183
|
+
|-------|-------------|
|
|
184
|
+
| `this.accessor` | Resolver de paths customizado |
|
|
185
|
+
| `this.handlers` | Todos os handlers (wrapped) — permite composição entre handlers |
|
|
186
|
+
| `this.compiler` | Instância do compilador — permite compilar sub-expressões |
|
|
187
|
+
| `this.scope` | Funções puras do scope |
|
|
561
188
|
|
|
562
|
-
|
|
563
|
-
const { fn } = compile(expr, { scope });
|
|
564
|
-
|
|
565
|
-
// 3. Executa
|
|
566
|
-
const contextFn = fn({}); // Retorna a função com lifecycle
|
|
567
|
-
const result = contextFn({ a: 1, b: 2 }); // "begin" → 3 → "end"
|
|
568
|
-
```
|
|
569
|
-
|
|
570
|
-
### Caso de uso: $simulate com múltiplos slots
|
|
189
|
+
Handlers **devem** ser regular functions ou method shorthand (arrow functions ignoram `.bind()`):
|
|
571
190
|
|
|
572
191
|
```typescript
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
effects: Expression[];
|
|
576
|
-
query: Expression;
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const resolvers: BoundaryResolvers = {
|
|
581
|
-
$simulate: (node, { compile: compileFn, genId, scope }) => {
|
|
582
|
-
const id = `__simulate_${genId()}`;
|
|
583
|
-
const sim = (node as unknown as SimulateNode).$simulate;
|
|
584
|
-
|
|
585
|
-
// Compila cada slot independentemente
|
|
586
|
-
const effectFns = sim.effects.map((e) => compileFn(e, { scope }).fn);
|
|
587
|
-
const queryFn = compileFn(sim.query, { scope }).fn;
|
|
588
|
-
|
|
589
|
-
// Cria função que orquestra a execução
|
|
590
|
-
const simulateFn = () => (data: unknown) => {
|
|
591
|
-
const effects = effectFns.map((fn) => fn(data));
|
|
592
|
-
const query = queryFn(data);
|
|
593
|
-
return { effects, query };
|
|
594
|
-
};
|
|
595
|
-
|
|
596
|
-
return {
|
|
597
|
-
expr: { $fn: id, args: [] },
|
|
598
|
-
scopeEntry: [id, simulateFn],
|
|
599
|
-
};
|
|
600
|
-
},
|
|
601
|
-
};
|
|
192
|
+
// method shorthand — funciona
|
|
193
|
+
handlers: { query: { find(key) { this.scope... } } }
|
|
602
194
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
$simulate: {
|
|
606
|
-
effects: [
|
|
607
|
-
{ $fn: "increment", args: [{ $: "hp" }] },
|
|
608
|
-
{ $fn: "decrement", args: [{ $: "mp" }] },
|
|
609
|
-
],
|
|
610
|
-
query: { $fn: "isAlive", args: [{ $: "hp" }] },
|
|
611
|
-
},
|
|
612
|
-
},
|
|
613
|
-
{ resolvers, scope: myScope }
|
|
614
|
-
);
|
|
615
|
-
```
|
|
616
|
-
|
|
617
|
-
### Contexto do resolver
|
|
618
|
-
|
|
619
|
-
O resolver recebe um contexto com:
|
|
620
|
-
|
|
621
|
-
| Propriedade | Tipo | Descrição |
|
|
622
|
-
|-------------|------|-----------|
|
|
623
|
-
| `compile` | `typeof compile` | Mesmo compilador do fluxo principal |
|
|
624
|
-
| `genId` | `() => string` | Gerador de IDs únicos |
|
|
625
|
-
| `scope` | `Scope` | Scope atual (read-only) |
|
|
626
|
-
| `options` | `CompileOptions` | Opções de compilação |
|
|
195
|
+
// regular function — funciona
|
|
196
|
+
handlers: { query: { find: function(key) { this.scope... } } }
|
|
627
197
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
Por padrão, IDs são gerados como `"0"`, `"1"`, `"2"`... Para IDs únicos globais, passe um `genId` customizado:
|
|
631
|
-
|
|
632
|
-
```typescript
|
|
633
|
-
import { nanoid } from 'nanoid';
|
|
634
|
-
|
|
635
|
-
const { expr, scope } = resolveBoundaries(expr, {
|
|
636
|
-
resolvers,
|
|
637
|
-
scope: baseScope,
|
|
638
|
-
genId: () => nanoid(), // IDs únicos globais
|
|
639
|
-
});
|
|
198
|
+
// arrow function — NÃO funciona (this é undefined)
|
|
199
|
+
handlers: { query: { find: (key) => { this.scope... } } }
|
|
640
200
|
```
|
|
641
201
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
| | `normalize()` | `resolveBoundaries()` |
|
|
202
|
+
| | `scope` | `handlers` |
|
|
645
203
|
|---|---|---|
|
|
646
|
-
| **
|
|
647
|
-
| **
|
|
648
|
-
| **
|
|
649
|
-
| **
|
|
650
|
-
| **
|
|
651
|
-
|
|
652
|
-
Use `normalize()` para sugar syntax simples. Use `resolveBoundaries()` quando precisar:
|
|
653
|
-
- Compilar múltiplos slots independentemente
|
|
654
|
-
- Controlar ordem/momento de execução
|
|
655
|
-
- Adicionar lógica de lifecycle (begin/end, try/finally)
|
|
656
|
-
- DSL completamente diferente internamente
|
|
204
|
+
| **Natureza** | Funções puras (Ramda-style) | Services com contexto |
|
|
205
|
+
| **Acessa** | Apenas args compilados | `this` (HandlerContext) + args |
|
|
206
|
+
| **DSL** | `{ $fn: "add", args: [...] }` | `{ $fn: "query:find", args: [...] }` |
|
|
207
|
+
| **Binding** | Nenhum | `.bind(ctx)` uma vez no construtor |
|
|
208
|
+
| **Overhead** | Zero | Zero (ctx e bindings criados uma vez) |
|
|
657
209
|
|
|
658
|
-
|
|
210
|
+
Zero overhead quando nenhum handler é registrado.
|
|
659
211
|
|
|
660
|
-
|
|
212
|
+
### Accessor — objetos inteligentes
|
|
661
213
|
|
|
662
214
|
```typescript
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
// Path reference
|
|
668
|
-
$("player.hp") // { $: "player.hp" }
|
|
669
|
-
|
|
670
|
-
// Function call
|
|
671
|
-
$fn("add", [$("a"), $("b")]) // { $fn: "add", args: [...] }
|
|
672
|
-
$fn("sum") // { $fn: "sum" }
|
|
673
|
-
|
|
674
|
-
// Conditional
|
|
675
|
-
$if($("isVip"), 0.2, 0) // { $if: ..., then: 0.2, else: 0 }
|
|
676
|
-
|
|
677
|
-
// Pipe
|
|
678
|
-
$pipe(
|
|
679
|
-
$("items"),
|
|
680
|
-
$fn("filter", [$fn("isActive")]),
|
|
681
|
-
$fn("sum")
|
|
682
|
-
)
|
|
683
|
-
|
|
684
|
-
// Condition
|
|
685
|
-
$cond($("age"), "gte", 18) // { left: ..., op: "gte", right: 18 }
|
|
686
|
-
|
|
687
|
-
// Callback (context)
|
|
688
|
-
$cb("tryCatch", $fn("query")) // { $cb: "tryCatch", body: {...} }
|
|
689
|
-
$cb("transaction", $fn("save"), { isolation: "serializable" })
|
|
690
|
-
```
|
|
691
|
-
|
|
692
|
-
## TypeScript
|
|
693
|
-
|
|
694
|
-
```typescript
|
|
695
|
-
import type {
|
|
696
|
-
Expression,
|
|
697
|
-
CompiledExpression,
|
|
698
|
-
RefExpr,
|
|
699
|
-
ConditionalExpr,
|
|
700
|
-
FnExpr,
|
|
701
|
-
PipeExpr,
|
|
702
|
-
CbExpr,
|
|
703
|
-
Condition,
|
|
704
|
-
ConditionGroup,
|
|
705
|
-
ConditionExpr,
|
|
706
|
-
ConditionOp,
|
|
707
|
-
Scope,
|
|
708
|
-
Context,
|
|
709
|
-
ContextFn,
|
|
710
|
-
RuntimeCtx,
|
|
711
|
-
PathGetterFn,
|
|
712
|
-
CompileOptions,
|
|
713
|
-
AccessorFn,
|
|
714
|
-
TransformFn,
|
|
715
|
-
Transforms,
|
|
716
|
-
// Boundaries
|
|
717
|
-
BoundaryResolver,
|
|
718
|
-
BoundaryResolvers,
|
|
719
|
-
ResolverContext,
|
|
720
|
-
ResolverResult,
|
|
721
|
-
ResolveBoundariesOptions,
|
|
722
|
-
ResolveBoundariesResult,
|
|
723
|
-
IdGenerator,
|
|
724
|
-
} from '@statedelta-libs/expressions';
|
|
725
|
-
|
|
726
|
-
// Type guards
|
|
727
|
-
import {
|
|
728
|
-
isRef,
|
|
729
|
-
isConditional,
|
|
730
|
-
isFn,
|
|
731
|
-
isPipe,
|
|
732
|
-
isCb,
|
|
733
|
-
isCondition,
|
|
734
|
-
isLiteral,
|
|
735
|
-
} from '@statedelta-libs/expressions';
|
|
215
|
+
const compiler = new ExpressionCompiler({
|
|
216
|
+
scope,
|
|
217
|
+
accessor: (path) => tickContext.get(path), // closure auto-suficiente
|
|
218
|
+
});
|
|
736
219
|
```
|
|
737
220
|
|
|
738
|
-
##
|
|
739
|
-
|
|
740
|
-
### Comparação Execução
|
|
741
|
-
|
|
742
|
-
| Cenário | Closures | AST | Ganho AST |
|
|
743
|
-
|---------|----------|-----|-----------|
|
|
744
|
-
| fn nested | 9M ops/s | 25M ops/s | **+169%** |
|
|
745
|
-
| ref 4 níveis | 16M ops/s | 27M ops/s | **+69%** |
|
|
746
|
-
| conditional nested | 20M ops/s | 25M ops/s | **+25%** |
|
|
747
|
-
|
|
748
|
-
**Break-even:** ~8 execuções - após isso, `compileAST()` compensa o tempo extra de compilação.
|
|
749
|
-
|
|
750
|
-
## Segurança
|
|
751
|
-
|
|
752
|
-
Ambas as abordagens são **seguras contra injeção de código**:
|
|
753
|
-
|
|
754
|
-
| Método | Proteção |
|
|
755
|
-
|--------|----------|
|
|
756
|
-
| `compile()` | Não gera código string - apenas compõe funções |
|
|
757
|
-
| `compileAST()` | Usa destructuring que valida identificadores automaticamente |
|
|
758
|
-
|
|
759
|
-
Funções só são acessíveis se existirem no `scope` fornecido pelo desenvolvedor.
|
|
760
|
-
|
|
761
|
-
## Migração
|
|
221
|
+
## Documentação
|
|
762
222
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
// Depois (referência via scope)
|
|
771
|
-
{ $fn: "filter", args: [{ $fn: "isActive" }] }
|
|
772
|
-
// Com scope: { isActive: (item) => item.active, filter: ... }
|
|
773
|
-
```
|
|
774
|
-
|
|
775
|
-
2. **Removidos**: `CallbackFn`, `FnArg` types
|
|
776
|
-
|
|
777
|
-
3. **normalize()**: Custom transforms agora são externos via `normalize()`, não mais dentro de `CompileOptions`:
|
|
778
|
-
```typescript
|
|
779
|
-
// Antes
|
|
780
|
-
compile(expr, { scope, transforms });
|
|
781
|
-
|
|
782
|
-
// Depois
|
|
783
|
-
const pure = normalize(expr, transforms);
|
|
784
|
-
compile(pure, { scope });
|
|
785
|
-
```
|
|
786
|
-
|
|
787
|
-
4. **Novos tipos**: `Transforms` (alias para `Record<string, TransformFn>`)
|
|
788
|
-
|
|
789
|
-
### v1.2 → v2.0
|
|
790
|
-
|
|
791
|
-
1. **Novo tipo `$cb`**: HOC de continuidade para wrapping contextual:
|
|
792
|
-
```typescript
|
|
793
|
-
// tryCatch, transaction, cached, etc.
|
|
794
|
-
{ $cb: "tryCatch", body: { $fn: "query" }, params: { fallback: [] } }
|
|
795
|
-
```
|
|
796
|
-
|
|
797
|
-
2. **Novo `context` em CompileOptions**: Separado de `scope` para HOCs:
|
|
798
|
-
```typescript
|
|
799
|
-
compile(expr, { scope, context });
|
|
800
|
-
compileAST(expr, { scope, context });
|
|
801
|
-
```
|
|
802
|
-
|
|
803
|
-
3. **Novos tipos**: `CbExpr`, `Context`, `ContextFn`, `PathGetterFn`
|
|
804
|
-
|
|
805
|
-
4. **Novo type guard**: `isCb()`
|
|
806
|
-
|
|
807
|
-
5. **Novo builder**: `$cb(name, body, params?)`
|
|
808
|
-
|
|
809
|
-
6. **Novo extractor**: `extractContextFns()`
|
|
810
|
-
|
|
811
|
-
7. **Novo `resolveBoundaries()`**: Para DSL customizados com compilação independente de slots:
|
|
812
|
-
```typescript
|
|
813
|
-
const { expr, scope } = resolveBoundaries(richExpr, {
|
|
814
|
-
resolvers: { $simulate: (node, ctx) => ... },
|
|
815
|
-
scope: baseScope,
|
|
816
|
-
genId: () => nanoid(), // opcional
|
|
817
|
-
});
|
|
818
|
-
compile(expr, { scope });
|
|
819
|
-
```
|
|
820
|
-
|
|
821
|
-
8. **Novos tipos**: `BoundaryResolver`, `BoundaryResolvers`, `ResolverContext`, `ResolverResult`, `ResolveBoundariesOptions`, `ResolveBoundariesResult`, `IdGenerator`
|
|
822
|
-
|
|
823
|
-
### v2.0 → v2.1
|
|
824
|
-
|
|
825
|
-
1. **Nova API do ContextFn**: Agora usa `RuntimeCtx` unificado ao invés de parâmetros separados:
|
|
826
|
-
```typescript
|
|
827
|
-
// Antes (v2.0)
|
|
828
|
-
const tryCatch: ContextFn = (cb, data, get, params) => {
|
|
829
|
-
try {
|
|
830
|
-
return cb(data, get);
|
|
831
|
-
} catch {
|
|
832
|
-
return params?.fallback;
|
|
833
|
-
}
|
|
834
|
-
};
|
|
835
|
-
|
|
836
|
-
// Depois (v2.1)
|
|
837
|
-
const tryCatch: ContextFn = (cb, ctx, params) => {
|
|
838
|
-
try {
|
|
839
|
-
return cb(ctx);
|
|
840
|
-
} catch {
|
|
841
|
-
return params?.fallback;
|
|
842
|
-
}
|
|
843
|
-
};
|
|
844
|
-
```
|
|
845
|
-
|
|
846
|
-
2. **RuntimeCtx**: Novo tipo que agrupa `data` e `get`:
|
|
847
|
-
```typescript
|
|
848
|
-
interface RuntimeCtx<T> {
|
|
849
|
-
data: T;
|
|
850
|
-
get: (path: string) => unknown;
|
|
851
|
-
}
|
|
852
|
-
```
|
|
853
|
-
|
|
854
|
-
3. **Injeção de accessor**: Context pode injetar `get` customizado:
|
|
855
|
-
```typescript
|
|
856
|
-
const simulate: ContextFn = (cb, ctx) => {
|
|
857
|
-
const customGet = (path) => store.getSimulated(path);
|
|
858
|
-
return cb({ ...ctx, get: customGet });
|
|
859
|
-
};
|
|
860
|
-
```
|
|
861
|
-
|
|
862
|
-
4. **Modificar data**: Agora via spread do ctx:
|
|
863
|
-
```typescript
|
|
864
|
-
// Antes
|
|
865
|
-
cb({ ...data, tx }, get)
|
|
866
|
-
|
|
867
|
-
// Depois
|
|
868
|
-
cb({ ...ctx, data: { ...ctx.data, tx } })
|
|
869
|
-
```
|
|
223
|
+
| Doc | Conteúdo |
|
|
224
|
+
|-----|----------|
|
|
225
|
+
| [docs/EXPRESSIONS-API.md](docs/EXPRESSIONS-API.md) | Referência completa da API pública |
|
|
226
|
+
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Visão geral da infraestrutura |
|
|
227
|
+
| [docs/COMPILE.md](docs/COMPILE.md) | Internals do pipeline closures |
|
|
228
|
+
| [docs/JIT.md](docs/JIT.md) | Internals do pipeline JIT |
|
|
870
229
|
|
|
871
230
|
## Licença
|
|
872
231
|
|