@voyantjs/workflows 0.28.3 → 0.30.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/dist/driver.d.ts +237 -0
- package/dist/driver.d.ts.map +1 -0
- package/dist/driver.js +53 -0
- package/dist/events/compile.d.ts +34 -0
- package/dist/events/compile.d.ts.map +1 -0
- package/dist/events/compile.js +204 -0
- package/dist/events/index.d.ts +8 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +11 -0
- package/dist/events/input-mapper.d.ts +24 -0
- package/dist/events/input-mapper.d.ts.map +1 -0
- package/dist/events/input-mapper.js +169 -0
- package/dist/events/manifest-builder.d.ts +32 -0
- package/dist/events/manifest-builder.d.ts.map +1 -0
- package/dist/events/manifest-builder.js +66 -0
- package/dist/events/payload-hash.d.ts +46 -0
- package/dist/events/payload-hash.d.ts.map +1 -0
- package/dist/events/payload-hash.js +98 -0
- package/dist/events/predicate.d.ts +77 -0
- package/dist/events/predicate.d.ts.map +1 -0
- package/dist/events/predicate.js +347 -0
- package/dist/events/registry.d.ts +37 -0
- package/dist/events/registry.d.ts.map +1 -0
- package/dist/events/registry.js +47 -0
- package/dist/handler/index.d.ts +8 -0
- package/dist/handler/index.d.ts.map +1 -1
- package/dist/handler/index.js +1 -0
- package/dist/http-ingest.d.ts +54 -0
- package/dist/http-ingest.d.ts.map +1 -0
- package/dist/http-ingest.js +214 -0
- package/dist/protocol/index.d.ts +17 -2
- package/dist/protocol/index.d.ts.map +1 -1
- package/dist/runtime/ctx.d.ts +9 -0
- package/dist/runtime/ctx.d.ts.map +1 -1
- package/dist/runtime/ctx.js +17 -0
- package/dist/runtime/executor.d.ts +7 -0
- package/dist/runtime/executor.d.ts.map +1 -1
- package/dist/runtime/executor.js +1 -0
- package/dist/trigger.d.ts +28 -14
- package/dist/trigger.d.ts.map +1 -1
- package/dist/trigger.js +4 -4
- package/dist/workflow.d.ts +10 -0
- package/dist/workflow.d.ts.map +1 -1
- package/package.json +14 -2
- package/src/driver.ts +277 -0
- package/src/events/compile.ts +268 -0
- package/src/events/index.ts +42 -0
- package/src/events/input-mapper.ts +201 -0
- package/src/events/manifest-builder.ts +97 -0
- package/src/events/payload-hash.ts +110 -0
- package/src/events/predicate.ts +390 -0
- package/src/events/registry.ts +88 -0
- package/src/handler/index.ts +9 -0
- package/src/http-ingest.ts +299 -0
- package/src/protocol/index.ts +17 -2
- package/src/runtime/ctx.ts +29 -0
- package/src/runtime/executor.ts +8 -0
- package/src/trigger.ts +31 -15
- package/src/workflow.ts +11 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
// Predicate DSL — the structured `where` filter on EventFilterDeclaration.
|
|
2
|
+
//
|
|
3
|
+
// Closed grammar of 12 operators, evaluated against the standard
|
|
4
|
+
// EventEnvelope shape (`data`, `metadata`, `name`, `emittedAt`). No `eval`,
|
|
5
|
+
// no Function constructor, no callback-via-network — everything is data.
|
|
6
|
+
//
|
|
7
|
+
// Authoring shape (from a module's source):
|
|
8
|
+
//
|
|
9
|
+
// trigger.on("promotion.changed", {
|
|
10
|
+
// target: bulkReindexProducts,
|
|
11
|
+
// where: { eq: [{ path: "data.affected.kind" }, { lit: "all" }] },
|
|
12
|
+
// input: { ... },
|
|
13
|
+
// })
|
|
14
|
+
//
|
|
15
|
+
// Architecture: docs/architecture/workflows-runtime-architecture.md §13.1.
|
|
16
|
+
|
|
17
|
+
// ---- Path / literal references ----
|
|
18
|
+
|
|
19
|
+
/** Either a path into the envelope or an inline literal value. */
|
|
20
|
+
export type PathOrLit = { path: string } | { lit: string | number | boolean | null }
|
|
21
|
+
|
|
22
|
+
// ---- The grammar ----
|
|
23
|
+
|
|
24
|
+
export type PredicateExpr =
|
|
25
|
+
| { eq: [PathOrLit, PathOrLit] }
|
|
26
|
+
| { neq: [PathOrLit, PathOrLit] }
|
|
27
|
+
| { in: [PathOrLit, PathOrLit[]] }
|
|
28
|
+
| { gt: [PathOrLit, PathOrLit] }
|
|
29
|
+
| { gte: [PathOrLit, PathOrLit] }
|
|
30
|
+
| { lt: [PathOrLit, PathOrLit] }
|
|
31
|
+
| { lte: [PathOrLit, PathOrLit] }
|
|
32
|
+
| { exists: PathOrLit }
|
|
33
|
+
| { not: PredicateExpr }
|
|
34
|
+
| { and: PredicateExpr[] }
|
|
35
|
+
| { or: PredicateExpr[] }
|
|
36
|
+
|
|
37
|
+
// ---- Envelope view (what paths address into) ----
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Minimal structural envelope the evaluator reads. Matches the standard
|
|
41
|
+
* `EventEnvelope` from `@voyantjs/core`. Declared structurally here so the
|
|
42
|
+
* SDK package stays a leaf.
|
|
43
|
+
*/
|
|
44
|
+
export interface PredicateEnvelope<TData = unknown> {
|
|
45
|
+
name: string
|
|
46
|
+
data: TData
|
|
47
|
+
metadata?: Record<string, unknown> | undefined
|
|
48
|
+
emittedAt: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---- Public API ----
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Evaluate a predicate against an event envelope. Returns `true` / `false`.
|
|
55
|
+
* Path resolution against missing keys yields `undefined`, which makes
|
|
56
|
+
* comparison ops `false` (not throw). The evaluator never throws on data
|
|
57
|
+
* mismatches — registration-time linting catches structural errors via
|
|
58
|
+
* {@link validatePredicate}.
|
|
59
|
+
*
|
|
60
|
+
* Throws `PredicateEvalError` only on unexpected shape errors (the predicate
|
|
61
|
+
* itself was constructed wrong, e.g. malformed operator). Drivers catch
|
|
62
|
+
* this and surface it as `IngestMatch.status === "skipped"` with reason
|
|
63
|
+
* `"where_eval_error"`.
|
|
64
|
+
*/
|
|
65
|
+
export function evaluatePredicate(expr: PredicateExpr, envelope: PredicateEnvelope): boolean {
|
|
66
|
+
if ("eq" in expr) {
|
|
67
|
+
return strictEquals(resolveSide(expr.eq[0], envelope), resolveSide(expr.eq[1], envelope))
|
|
68
|
+
}
|
|
69
|
+
if ("neq" in expr) {
|
|
70
|
+
return !strictEquals(resolveSide(expr.neq[0], envelope), resolveSide(expr.neq[1], envelope))
|
|
71
|
+
}
|
|
72
|
+
if ("in" in expr) {
|
|
73
|
+
const lhs = resolveSide(expr.in[0], envelope)
|
|
74
|
+
const rhs = expr.in[1].map((item) => resolveSide(item, envelope))
|
|
75
|
+
return rhs.some((candidate) => strictEquals(lhs, candidate))
|
|
76
|
+
}
|
|
77
|
+
if ("gt" in expr) return compareTwo(expr.gt, envelope, ">")
|
|
78
|
+
if ("gte" in expr) return compareTwo(expr.gte, envelope, ">=")
|
|
79
|
+
if ("lt" in expr) return compareTwo(expr.lt, envelope, "<")
|
|
80
|
+
if ("lte" in expr) return compareTwo(expr.lte, envelope, "<=")
|
|
81
|
+
if ("exists" in expr) {
|
|
82
|
+
return resolveSide(expr.exists, envelope) !== undefined
|
|
83
|
+
}
|
|
84
|
+
if ("not" in expr) return !evaluatePredicate(expr.not, envelope)
|
|
85
|
+
if ("and" in expr) {
|
|
86
|
+
if (!Array.isArray(expr.and)) {
|
|
87
|
+
throw new PredicateEvalError("`and` clause must be an array of predicates")
|
|
88
|
+
}
|
|
89
|
+
return expr.and.every((sub) => evaluatePredicate(sub, envelope))
|
|
90
|
+
}
|
|
91
|
+
if ("or" in expr) {
|
|
92
|
+
if (!Array.isArray(expr.or)) {
|
|
93
|
+
throw new PredicateEvalError("`or` clause must be an array of predicates")
|
|
94
|
+
}
|
|
95
|
+
return expr.or.some((sub) => evaluatePredicate(sub, envelope))
|
|
96
|
+
}
|
|
97
|
+
throw new PredicateEvalError(`unknown predicate operator: ${stringify(expr)}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve a path string against an envelope. Public so consumers (input
|
|
102
|
+
* mapper, manifest builder) can share the same path semantics without
|
|
103
|
+
* re-implementing them.
|
|
104
|
+
*
|
|
105
|
+
* Path syntax: dot-separated; `[N]` for array index; missing intermediate
|
|
106
|
+
* keys produce `undefined`. Roots: `data`, `metadata`, `name`, `emittedAt`.
|
|
107
|
+
*
|
|
108
|
+
* Returns `undefined` on any shape mismatch — that's how runtime evaluation
|
|
109
|
+
* stays no-throw.
|
|
110
|
+
*/
|
|
111
|
+
export function resolvePath(path: string, envelope: PredicateEnvelope): unknown {
|
|
112
|
+
const segments = parsePath(path)
|
|
113
|
+
if (segments.length === 0) return undefined
|
|
114
|
+
const [root, ...rest] = segments
|
|
115
|
+
let value: unknown
|
|
116
|
+
switch (root) {
|
|
117
|
+
case "name":
|
|
118
|
+
value = envelope.name
|
|
119
|
+
break
|
|
120
|
+
case "emittedAt":
|
|
121
|
+
value = envelope.emittedAt
|
|
122
|
+
break
|
|
123
|
+
case "data":
|
|
124
|
+
value = envelope.data
|
|
125
|
+
break
|
|
126
|
+
case "metadata":
|
|
127
|
+
value = envelope.metadata
|
|
128
|
+
break
|
|
129
|
+
default:
|
|
130
|
+
// Unknown roots evaluate to undefined at runtime. Registration-time
|
|
131
|
+
// linter surfaces this as a structural error so callers see it earlier.
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
for (const segment of rest) {
|
|
135
|
+
if (value === undefined || value === null) return undefined
|
|
136
|
+
if (typeof segment === "number") {
|
|
137
|
+
if (!Array.isArray(value)) return undefined
|
|
138
|
+
value = value[segment]
|
|
139
|
+
} else {
|
|
140
|
+
if (typeof value !== "object") return undefined
|
|
141
|
+
value = (value as Record<string, unknown>)[segment]
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return value
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---- Static linter (registration-time) ----
|
|
148
|
+
|
|
149
|
+
export interface PredicateValidationResult {
|
|
150
|
+
ok: boolean
|
|
151
|
+
errors: string[]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Static structural check on a `PredicateExpr`. Catches path roots that
|
|
156
|
+
* aren't `data`/`metadata`/`name`/`emittedAt`, type mismatches on
|
|
157
|
+
* comparison operators (number vs string lhs/rhs), and malformed grammars.
|
|
158
|
+
* Surfaced at `trigger.on()` registration so authoring errors fail fast.
|
|
159
|
+
*/
|
|
160
|
+
export function validatePredicate(expr: PredicateExpr): PredicateValidationResult {
|
|
161
|
+
const errors: string[] = []
|
|
162
|
+
walk(expr, errors, [])
|
|
163
|
+
return { ok: errors.length === 0, errors }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function walk(expr: PredicateExpr, errors: string[], path: string[]): void {
|
|
167
|
+
if (typeof expr !== "object" || expr === null) {
|
|
168
|
+
errors.push(`${pathLabel(path)}: predicate must be an object, got ${typeof expr}`)
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
const keys = Object.keys(expr)
|
|
172
|
+
if (keys.length !== 1) {
|
|
173
|
+
errors.push(
|
|
174
|
+
`${pathLabel(path)}: a predicate object must have exactly one operator key, got ${keys.length} (${keys.join(", ")})`,
|
|
175
|
+
)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
const op = keys[0] as keyof PredicateExpr
|
|
179
|
+
switch (op) {
|
|
180
|
+
case "eq":
|
|
181
|
+
case "neq":
|
|
182
|
+
case "gt":
|
|
183
|
+
case "gte":
|
|
184
|
+
case "lt":
|
|
185
|
+
case "lte": {
|
|
186
|
+
const raw = (expr as Record<string, unknown>)[op]
|
|
187
|
+
if (!Array.isArray(raw) || raw.length !== 2) {
|
|
188
|
+
errors.push(`${pathLabel([...path, op])}: expected [lhs, rhs] tuple`)
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
const sides = raw as [PathOrLit, PathOrLit]
|
|
192
|
+
validateSide(sides[0], errors, [...path, op, "0"])
|
|
193
|
+
validateSide(sides[1], errors, [...path, op, "1"])
|
|
194
|
+
// Type-sanity for ordered comparisons: both literals must be the same numeric/string flavor.
|
|
195
|
+
if (op !== "eq" && op !== "neq") {
|
|
196
|
+
const lhsLit = sideLitType(sides[0])
|
|
197
|
+
const rhsLit = sideLitType(sides[1])
|
|
198
|
+
if (
|
|
199
|
+
lhsLit !== "unknown" &&
|
|
200
|
+
rhsLit !== "unknown" &&
|
|
201
|
+
lhsLit !== rhsLit &&
|
|
202
|
+
(lhsLit === "number" || lhsLit === "string")
|
|
203
|
+
) {
|
|
204
|
+
errors.push(
|
|
205
|
+
`${pathLabel([...path, op])}: ordered comparison sides must agree on type (got ${lhsLit} vs ${rhsLit})`,
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
case "in": {
|
|
212
|
+
const tuple = (expr as { in: [PathOrLit, PathOrLit[]] }).in
|
|
213
|
+
if (!Array.isArray(tuple) || tuple.length !== 2) {
|
|
214
|
+
errors.push(`${pathLabel([...path, "in"])}: expected [lhs, rhs[]] tuple`)
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
validateSide(tuple[0], errors, [...path, "in", "0"])
|
|
218
|
+
if (!Array.isArray(tuple[1])) {
|
|
219
|
+
errors.push(`${pathLabel([...path, "in", "1"])}: rhs must be an array of paths/literals`)
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
for (let i = 0; i < tuple[1].length; i++) {
|
|
223
|
+
const s = tuple[1][i] as PathOrLit
|
|
224
|
+
validateSide(s, errors, [...path, "in", "1", String(i)])
|
|
225
|
+
}
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
case "exists":
|
|
229
|
+
validateSide((expr as { exists: PathOrLit }).exists, errors, [...path, "exists"])
|
|
230
|
+
return
|
|
231
|
+
case "not":
|
|
232
|
+
walk((expr as { not: PredicateExpr }).not, errors, [...path, "not"])
|
|
233
|
+
return
|
|
234
|
+
case "and":
|
|
235
|
+
case "or": {
|
|
236
|
+
const raw = (expr as Record<string, unknown>)[op]
|
|
237
|
+
if (!Array.isArray(raw)) {
|
|
238
|
+
errors.push(`${pathLabel([...path, op])}: ${op} must be an array of predicates`)
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
const arr = raw as PredicateExpr[]
|
|
242
|
+
for (let i = 0; i < arr.length; i++) {
|
|
243
|
+
const sub = arr[i] as PredicateExpr
|
|
244
|
+
walk(sub, errors, [...path, op, String(i)])
|
|
245
|
+
}
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
default:
|
|
249
|
+
errors.push(`${pathLabel(path)}: unknown predicate operator "${String(op)}"`)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function validateSide(side: PathOrLit, errors: string[], path: string[]): void {
|
|
254
|
+
if (typeof side !== "object" || side === null) {
|
|
255
|
+
errors.push(`${pathLabel(path)}: expected { path } or { lit }, got ${typeof side}`)
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
if ("path" in side) {
|
|
259
|
+
if (typeof side.path !== "string" || side.path.length === 0) {
|
|
260
|
+
errors.push(`${pathLabel(path)}: "path" must be a non-empty string`)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
const root = parsePath(side.path)[0]
|
|
264
|
+
if (root !== "data" && root !== "metadata" && root !== "name" && root !== "emittedAt") {
|
|
265
|
+
errors.push(
|
|
266
|
+
`${pathLabel(path)}: path root "${String(root)}" is not one of data | metadata | name | emittedAt`,
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
if ("lit" in side) {
|
|
272
|
+
const t = typeof side.lit
|
|
273
|
+
if (t !== "string" && t !== "number" && t !== "boolean" && side.lit !== null) {
|
|
274
|
+
errors.push(`${pathLabel(path)}: lit must be string | number | boolean | null`)
|
|
275
|
+
}
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
errors.push(`${pathLabel(path)}: must specify "path" or "lit"`)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---- Internal helpers ----
|
|
282
|
+
|
|
283
|
+
class PredicateEvalError extends Error {
|
|
284
|
+
constructor(message: string) {
|
|
285
|
+
super(message)
|
|
286
|
+
this.name = "PredicateEvalError"
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function resolveSide(side: PathOrLit, envelope: PredicateEnvelope): unknown {
|
|
291
|
+
if ("lit" in side) return side.lit
|
|
292
|
+
if ("path" in side) return resolvePath(side.path, envelope)
|
|
293
|
+
return undefined
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function strictEquals(a: unknown, b: unknown): boolean {
|
|
297
|
+
if (a === undefined || b === undefined) return false
|
|
298
|
+
if (a === null && b === null) return true
|
|
299
|
+
if (a === null || b === null) return false
|
|
300
|
+
if (typeof a !== typeof b) return false
|
|
301
|
+
if (typeof a === "object") {
|
|
302
|
+
// Strict equality only for primitives + null. Object equality is not
|
|
303
|
+
// supported in v1; users who need it project specific paths to compare.
|
|
304
|
+
return false
|
|
305
|
+
}
|
|
306
|
+
return a === b
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function compareTwo(
|
|
310
|
+
sides: [PathOrLit, PathOrLit],
|
|
311
|
+
envelope: PredicateEnvelope,
|
|
312
|
+
op: ">" | ">=" | "<" | "<=",
|
|
313
|
+
): boolean {
|
|
314
|
+
const lhs = resolveSide(sides[0], envelope)
|
|
315
|
+
const rhs = resolveSide(sides[1], envelope)
|
|
316
|
+
if (lhs === undefined || rhs === undefined) return false
|
|
317
|
+
if (typeof lhs !== typeof rhs) return false
|
|
318
|
+
if (typeof lhs !== "number" && typeof lhs !== "string") return false
|
|
319
|
+
switch (op) {
|
|
320
|
+
case ">":
|
|
321
|
+
return (lhs as number | string) > (rhs as number | string)
|
|
322
|
+
case ">=":
|
|
323
|
+
return (lhs as number | string) >= (rhs as number | string)
|
|
324
|
+
case "<":
|
|
325
|
+
return (lhs as number | string) < (rhs as number | string)
|
|
326
|
+
case "<=":
|
|
327
|
+
return (lhs as number | string) <= (rhs as number | string)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function sideLitType(side: PathOrLit): "number" | "string" | "boolean" | "null" | "unknown" {
|
|
332
|
+
if ("lit" in side) {
|
|
333
|
+
if (side.lit === null) return "null"
|
|
334
|
+
const t = typeof side.lit
|
|
335
|
+
if (t === "number" || t === "string" || t === "boolean") return t
|
|
336
|
+
}
|
|
337
|
+
return "unknown"
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Parse a dot-and-bracket path into segments. `data.items[0].id` becomes
|
|
342
|
+
* `["data", "items", 0, "id"]`. Numeric segments inside `[N]` are returned
|
|
343
|
+
* as numbers; everything else as strings. Malformed input yields `[]`.
|
|
344
|
+
*/
|
|
345
|
+
function parsePath(path: string): Array<string | number> {
|
|
346
|
+
if (typeof path !== "string" || path.length === 0) return []
|
|
347
|
+
const segments: Array<string | number> = []
|
|
348
|
+
let i = 0
|
|
349
|
+
let buf = ""
|
|
350
|
+
const flushBuf = (): void => {
|
|
351
|
+
if (buf.length > 0) {
|
|
352
|
+
segments.push(buf)
|
|
353
|
+
buf = ""
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
while (i < path.length) {
|
|
357
|
+
const c = path[i]
|
|
358
|
+
if (c === ".") {
|
|
359
|
+
flushBuf()
|
|
360
|
+
i++
|
|
361
|
+
continue
|
|
362
|
+
}
|
|
363
|
+
if (c === "[") {
|
|
364
|
+
flushBuf()
|
|
365
|
+
const end = path.indexOf("]", i)
|
|
366
|
+
if (end === -1) return []
|
|
367
|
+
const idx = Number(path.slice(i + 1, end))
|
|
368
|
+
if (!Number.isInteger(idx) || idx < 0) return []
|
|
369
|
+
segments.push(idx)
|
|
370
|
+
i = end + 1
|
|
371
|
+
continue
|
|
372
|
+
}
|
|
373
|
+
buf += c
|
|
374
|
+
i++
|
|
375
|
+
}
|
|
376
|
+
flushBuf()
|
|
377
|
+
return segments
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function pathLabel(path: string[]): string {
|
|
381
|
+
return path.length === 0 ? "(root)" : path.join(".")
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function stringify(value: unknown): string {
|
|
385
|
+
try {
|
|
386
|
+
return JSON.stringify(value)
|
|
387
|
+
} catch {
|
|
388
|
+
return String(value)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Process-local registry for event-filter runtime entries.
|
|
2
|
+
//
|
|
3
|
+
// `trigger.on(eventName, filter)` adds an entry here at module-load time.
|
|
4
|
+
// `createApp()` (PR4) walks `getEventFilterRegistry().list()` to build a
|
|
5
|
+
// manifest, hand it to the configured driver, and install the EventBus
|
|
6
|
+
// forwarder.
|
|
7
|
+
//
|
|
8
|
+
// Backed by `globalThis` so bundles that inline their own copy of
|
|
9
|
+
// `@voyantjs/workflows` still share the registry with the loader's copy
|
|
10
|
+
// — same pattern `getWorkflow()` uses for the workflow registry.
|
|
11
|
+
//
|
|
12
|
+
// Architecture: docs/architecture/workflows-runtime-architecture.md §12.
|
|
13
|
+
|
|
14
|
+
import type { EventFilterManifestEntry } from "../protocol/index.js"
|
|
15
|
+
import type { EventFilterDeclaration } from "../trigger.js"
|
|
16
|
+
|
|
17
|
+
const REGISTRY_KEY = "__voyantEventFilterRegistry" as const
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Internal/runtime form of a registered event filter. Carries:
|
|
21
|
+
* - The serializable manifest entry (sent to drivers, persisted in
|
|
22
|
+
* manifest stores, evaluated by the event router).
|
|
23
|
+
* - A debug-only structural copy of the original declaration. Not
|
|
24
|
+
* serialized; used by the dashboard / dev mode for human-readable
|
|
25
|
+
* filter inspection.
|
|
26
|
+
*
|
|
27
|
+
* Concrete `WorkflowDefinition` from `workflow.ts` satisfies the structural
|
|
28
|
+
* `target` shape via TypeScript compat.
|
|
29
|
+
*/
|
|
30
|
+
export interface EventFilterRuntimeEntry {
|
|
31
|
+
readonly id: string
|
|
32
|
+
readonly eventType: string
|
|
33
|
+
readonly manifest: EventFilterManifestEntry
|
|
34
|
+
/** Debug-only — non-serializable. */
|
|
35
|
+
readonly declaration: EventFilterDeclaration<unknown>
|
|
36
|
+
/** Workflow id this filter targets, for quick lookup. */
|
|
37
|
+
readonly targetWorkflowId: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface EventFilterRegistry {
|
|
41
|
+
add(entry: EventFilterRuntimeEntry): void
|
|
42
|
+
list(): EventFilterRuntimeEntry[]
|
|
43
|
+
/** Reset to empty. Test-only — production code never calls this. */
|
|
44
|
+
reset(): void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const globalRef = globalThis as unknown as Record<
|
|
48
|
+
typeof REGISTRY_KEY,
|
|
49
|
+
Map<string, EventFilterRuntimeEntry> | undefined
|
|
50
|
+
>
|
|
51
|
+
|
|
52
|
+
const REGISTRY: Map<string, EventFilterRuntimeEntry> =
|
|
53
|
+
globalRef[REGISTRY_KEY] ?? new Map<string, EventFilterRuntimeEntry>()
|
|
54
|
+
globalRef[REGISTRY_KEY] = REGISTRY
|
|
55
|
+
|
|
56
|
+
const registry: EventFilterRegistry = {
|
|
57
|
+
add(entry) {
|
|
58
|
+
if (REGISTRY.has(entry.id)) {
|
|
59
|
+
// Same id implies same canonicalized declaration. HMR re-imports
|
|
60
|
+
// and re-evaluations of the same module file land here; replace
|
|
61
|
+
// (overwrite is harmless since the entry is content-addressed).
|
|
62
|
+
// For genuine duplicates from different declarations, the canonical
|
|
63
|
+
// hash would differ, so different ids would result.
|
|
64
|
+
REGISTRY.set(entry.id, entry)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
REGISTRY.set(entry.id, entry)
|
|
68
|
+
},
|
|
69
|
+
list() {
|
|
70
|
+
return [...REGISTRY.values()]
|
|
71
|
+
},
|
|
72
|
+
reset() {
|
|
73
|
+
REGISTRY.clear()
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Process-local event-filter registry. Singleton per realm. */
|
|
78
|
+
export function getEventFilterRegistry(): EventFilterRegistry {
|
|
79
|
+
return registry
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Internal: clear every registered filter. Used by tests that import the
|
|
84
|
+
* SDK and need a clean slate between cases. Not part of the public API.
|
|
85
|
+
*/
|
|
86
|
+
export function __resetEventFilterRegistry(): void {
|
|
87
|
+
registry.reset()
|
|
88
|
+
}
|
package/src/handler/index.ts
CHANGED
|
@@ -69,6 +69,14 @@ export interface StepHandlerDeps {
|
|
|
69
69
|
* the target runtime; the executor only cares that a runner exists.
|
|
70
70
|
*/
|
|
71
71
|
nodeStepRunner?: StepRunner
|
|
72
|
+
/**
|
|
73
|
+
* Read-only service resolver, surfaced to step bodies as `ctx.services`.
|
|
74
|
+
* The framework's `createApp()` wires this from its `ModuleContainer`;
|
|
75
|
+
* raw orchestrator harnesses (tests, ad-hoc scripts) typically leave
|
|
76
|
+
* it unset, in which case `ctx.services.resolve(...)` throws with a
|
|
77
|
+
* clear message. See architecture doc §11.
|
|
78
|
+
*/
|
|
79
|
+
services?: import("../driver.js").ServiceResolver
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
/** The HTTP request body the orchestrator sends. */
|
|
@@ -235,6 +243,7 @@ async function runStepInner(
|
|
|
235
243
|
stepRunner,
|
|
236
244
|
nodeStepRunner: deps.nodeStepRunner,
|
|
237
245
|
rateLimiter: deps.rateLimiter,
|
|
246
|
+
services: deps.services,
|
|
238
247
|
now,
|
|
239
248
|
abortSignal: opts.signal,
|
|
240
249
|
onStreamChunk: opts.onStreamChunk,
|