@nanobpm/workflow 0.8.2 → 0.9.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/declarative.d.ts +172 -10
- package/dist/declarative.js +213 -355
- package/dist/nodes/branch.d.ts +11 -0
- package/dist/nodes/branch.js +40 -0
- package/dist/nodes/break.d.ts +8 -0
- package/dist/nodes/break.js +19 -0
- package/dist/nodes/continue.d.ts +8 -0
- package/dist/nodes/continue.js +18 -0
- package/dist/nodes/forEach.d.ts +24 -0
- package/dist/nodes/forEach.js +104 -0
- package/dist/nodes/index.d.ts +11 -0
- package/dist/nodes/index.js +18 -0
- package/dist/nodes/loop.d.ts +9 -0
- package/dist/nodes/loop.js +25 -0
- package/dist/nodes/parallel.d.ts +9 -0
- package/dist/nodes/parallel.js +40 -0
- package/dist/nodes/registry.d.ts +33 -0
- package/dist/nodes/registry.js +71 -0
- package/dist/nodes/run.d.ts +10 -0
- package/dist/nodes/run.js +21 -0
- package/dist/nodes/signal.d.ts +11 -0
- package/dist/nodes/signal.js +21 -0
- package/dist/nodes/switch.d.ts +11 -0
- package/dist/nodes/switch.js +51 -0
- package/dist/nodes/task.d.ts +11 -0
- package/dist/nodes/task.js +21 -0
- package/dist/nodes/timer.d.ts +9 -0
- package/dist/nodes/timer.js +34 -0
- package/dist/test-support/diff.d.ts +8 -0
- package/dist/test-support/diff.js +70 -0
- package/dist/test-support/index.d.ts +8 -0
- package/dist/test-support/index.js +49 -0
- package/dist/test-support/normalize.d.ts +19 -0
- package/dist/test-support/normalize.js +245 -0
- package/dist/test-support/parity.d.ts +28 -0
- package/dist/test-support/parity.js +61 -0
- package/dist/test-support/xml.d.ts +17 -0
- package/dist/test-support/xml.js +135 -0
- package/dist/types.d.ts +20 -56
- package/package.json +12 -4
- package/src/declarative.ts +327 -359
- package/src/nodes/README.md +123 -0
- package/src/nodes/branch.ts +48 -0
- package/src/nodes/break.ts +25 -0
- package/src/nodes/continue.ts +24 -0
- package/src/nodes/forEach.ts +137 -0
- package/src/nodes/index.ts +19 -0
- package/src/nodes/loop.ts +34 -0
- package/src/nodes/parallel.ts +48 -0
- package/src/nodes/registry.ts +105 -0
- package/src/nodes/run.ts +29 -0
- package/src/nodes/signal.ts +29 -0
- package/src/nodes/switch.ts +56 -0
- package/src/nodes/task.ts +29 -0
- package/src/nodes/timer.ts +42 -0
- package/src/test-support/diff.ts +69 -0
- package/src/test-support/index.ts +53 -0
- package/src/test-support/normalize.ts +274 -0
- package/src/test-support/parity.ts +85 -0
- package/src/test-support/xml.ts +140 -0
- package/src/types.ts +20 -30
package/dist/declarative.js
CHANGED
|
@@ -36,7 +36,13 @@
|
|
|
36
36
|
// Typed data envelopes are LIFTED into the model (nano:shape + dataEnvelope
|
|
37
37
|
// zeebe:property), so the generated .bpmn is ejectable to model-first with its
|
|
38
38
|
// typed contracts intact.
|
|
39
|
-
import { assertIdent,
|
|
39
|
+
import { assertIdent, assertTimerCycle, assertTimerDate, assertTimerDuration, escapeXml, jobType, messageName } from "./xml.js";
|
|
40
|
+
import { eachNodeKind, requireNodeKind } from "./nodes/registry.js";
|
|
41
|
+
// Import the generated barrel purely for its registration side effects: every
|
|
42
|
+
// built-in (and slice-added) kind module registers its FlowNode variant, builder
|
|
43
|
+
// method, and walk/emit handlers at import time. MUST run before `makeBuilder`
|
|
44
|
+
// or the `Compiler` dispatch through the registry.
|
|
45
|
+
import "./nodes/index.js";
|
|
40
46
|
/** Ids the emitter generates for structural nodes / flows / messages. A step
|
|
41
47
|
* name that collides with one of these would produce a duplicate BPMN id and an
|
|
42
48
|
* invalid model, so reject them at authoring time. */
|
|
@@ -90,200 +96,166 @@ function makeBuilder(id, out, ctx, isRoot = false) {
|
|
|
90
96
|
fn(makeBuilder(id, body, { ...ctx, loopDepth: 0 }));
|
|
91
97
|
return body;
|
|
92
98
|
};
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
const setStartTimer = (t) => {
|
|
100
|
+
if (!isRoot) {
|
|
101
|
+
throw new Error(`startOn() is only valid at the top level of flow "${id}" (not inside switch/branch/loop)`);
|
|
102
|
+
}
|
|
103
|
+
if (out.length !== 0)
|
|
104
|
+
throw new Error(`startOn() must be the first statement in flow "${id}"`);
|
|
105
|
+
if (ctx.startTimer)
|
|
106
|
+
throw new Error(`startOn() may be called only once in flow "${id}"`);
|
|
107
|
+
ctx.startTimer = t;
|
|
108
|
+
};
|
|
109
|
+
let builderRef;
|
|
110
|
+
const api = {
|
|
111
|
+
id,
|
|
112
|
+
isRoot,
|
|
113
|
+
out,
|
|
114
|
+
contracts: ctx.contracts,
|
|
115
|
+
handlers: ctx.handlers,
|
|
116
|
+
get loopDepth() {
|
|
117
|
+
return ctx.loopDepth;
|
|
101
118
|
},
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
assertJobType("task jobType", override);
|
|
107
|
-
out.push({ kind: "task", name, envelopes: contractEnvelopes(ctx, name), jobType: override });
|
|
108
|
-
return b;
|
|
119
|
+
self() {
|
|
120
|
+
if (!builderRef)
|
|
121
|
+
throw new Error("internal: builder method invoked before assembly");
|
|
122
|
+
return builderRef;
|
|
109
123
|
},
|
|
110
|
-
|
|
124
|
+
child,
|
|
125
|
+
childScoped,
|
|
126
|
+
claim(name) {
|
|
111
127
|
claimName(ctx, id, name);
|
|
112
|
-
if (!opts || !opts.correlationKey)
|
|
113
|
-
throw new Error(`signal("${name}") needs { correlationKey }`);
|
|
114
|
-
assertIdent("correlationKey", opts.correlationKey);
|
|
115
|
-
out.push({ kind: "signal", name, correlationKey: opts.correlationKey, payload: ctx.contracts[name]?.in });
|
|
116
|
-
return b;
|
|
117
|
-
},
|
|
118
|
-
timer(name, opts) {
|
|
119
|
-
claimName(ctx, id, name);
|
|
120
|
-
const hasAfter = "after" in opts && typeof opts.after === "string";
|
|
121
|
-
const hasAt = "at" in opts && typeof opts.at === "string";
|
|
122
|
-
if (hasAfter === hasAt) {
|
|
123
|
-
throw new Error(`timer("${name}") needs exactly one of { after } (a delay) or { at } (an instant)`);
|
|
124
|
-
}
|
|
125
|
-
if ("after" in opts) {
|
|
126
|
-
const after = opts.after.trim();
|
|
127
|
-
assertTimerDuration(`timer("${name}") after`, after);
|
|
128
|
-
out.push({ kind: "timer", name, after });
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
const at = opts.at.trim();
|
|
132
|
-
assertTimerDate(`timer("${name}") at`, at);
|
|
133
|
-
out.push({ kind: "timer", name, at });
|
|
134
|
-
}
|
|
135
|
-
return b;
|
|
136
|
-
},
|
|
137
|
-
startOn(spec) {
|
|
138
|
-
if (!isRoot) {
|
|
139
|
-
throw new Error(`startOn() is only valid at the top level of flow "${id}" (not inside switch/branch/loop)`);
|
|
140
|
-
}
|
|
141
|
-
if (out.length !== 0)
|
|
142
|
-
throw new Error(`startOn() must be the first statement in flow "${id}"`);
|
|
143
|
-
if (ctx.startTimer)
|
|
144
|
-
throw new Error(`startOn() may be called only once in flow "${id}"`);
|
|
145
|
-
const setCount = TIMER_START_KEYS.filter((key) => typeof timerStartValue(spec, key) === "string").length;
|
|
146
|
-
if (setCount !== 1) {
|
|
147
|
-
throw new Error(`startOn() needs exactly one of { cycle }, { after }, or { at } in flow "${id}"`);
|
|
148
|
-
}
|
|
149
|
-
if ("cycle" in spec) {
|
|
150
|
-
const cycle = spec.cycle.trim();
|
|
151
|
-
assertTimerCycle(`startOn cycle`, cycle);
|
|
152
|
-
ctx.startTimer = { cycle };
|
|
153
|
-
}
|
|
154
|
-
else if ("after" in spec) {
|
|
155
|
-
const after = spec.after.trim();
|
|
156
|
-
assertTimerDuration(`startOn after`, after);
|
|
157
|
-
ctx.startTimer = { after };
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
const at = spec.at.trim();
|
|
161
|
-
assertTimerDate(`startOn at`, at);
|
|
162
|
-
ctx.startTimer = { at };
|
|
163
|
-
}
|
|
164
|
-
return b;
|
|
165
|
-
},
|
|
166
|
-
switch(subject, cases) {
|
|
167
|
-
if (typeof subject !== "string" || subject.trim() === "") {
|
|
168
|
-
throw new Error(`switch() needs a non-empty subject expression`);
|
|
169
|
-
}
|
|
170
|
-
const caseNodes = [];
|
|
171
|
-
for (const [value, fn] of Object.entries(cases)) {
|
|
172
|
-
if (value === "default")
|
|
173
|
-
continue;
|
|
174
|
-
if (typeof fn !== "function")
|
|
175
|
-
throw new Error(`switch("${subject}") cases must be blocks (b) => {…}`);
|
|
176
|
-
caseNodes.push({ value, body: child(fn, false) });
|
|
177
|
-
}
|
|
178
|
-
if (caseNodes.length === 0)
|
|
179
|
-
throw new Error(`switch("${subject}") needs at least one case`);
|
|
180
|
-
const def = cases.default ? child(cases.default, false) : undefined;
|
|
181
|
-
out.push({ kind: "switch", subject, cases: caseNodes, default: def });
|
|
182
|
-
return b;
|
|
183
128
|
},
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
throw new Error(`branch() needs a non-empty FEEL condition`);
|
|
187
|
-
}
|
|
188
|
-
if (!arms || typeof arms.then !== "function")
|
|
189
|
-
throw new Error(`branch("${condition}") needs a then arm`);
|
|
190
|
-
out.push({
|
|
191
|
-
kind: "branch",
|
|
192
|
-
condition,
|
|
193
|
-
then: child(arms.then, false),
|
|
194
|
-
else: arms.else ? child(arms.else, false) : undefined,
|
|
195
|
-
});
|
|
196
|
-
return b;
|
|
197
|
-
},
|
|
198
|
-
loop(body) {
|
|
199
|
-
if (typeof body !== "function")
|
|
200
|
-
throw new Error(`loop() needs a body function`);
|
|
201
|
-
out.push({ kind: "loop", body: child(body, true) });
|
|
202
|
-
return b;
|
|
203
|
-
},
|
|
204
|
-
parallel(branches) {
|
|
205
|
-
if (!Array.isArray(branches) || branches.length < 2) {
|
|
206
|
-
throw new Error(`parallel() needs at least two branch blocks`);
|
|
207
|
-
}
|
|
208
|
-
for (const fn of branches) {
|
|
209
|
-
if (typeof fn !== "function")
|
|
210
|
-
throw new Error(`parallel() branches must all be blocks (b) => {…}`);
|
|
211
|
-
}
|
|
212
|
-
// Each branch runs in its own token scope up to the AND-join, so a
|
|
213
|
-
// `break`/`continue` must not escape the fork/join into an enclosing
|
|
214
|
-
// loop. Scope branches (loopDepth resets to 0) so cross-boundary
|
|
215
|
-
// break/continue is rejected at build time, matching forEach.
|
|
216
|
-
out.push({ kind: "parallel", branches: branches.map((fn) => childScoped(fn)) });
|
|
217
|
-
return b;
|
|
218
|
-
},
|
|
219
|
-
forEach(collection, itemVar, body, opts) {
|
|
220
|
-
if (typeof collection !== "string" || collection.trim() === "") {
|
|
221
|
-
throw new Error(`forEach() needs a non-empty FEEL collection expression`);
|
|
222
|
-
}
|
|
223
|
-
// Authors may paste a leading "=" (common in Zeebe FEEL examples). Strip
|
|
224
|
-
// it so the emitter's own "=" prefix never produces an invalid "==...".
|
|
225
|
-
const collExpr = collection.trim().replace(/^=/, "").trim();
|
|
226
|
-
if (collExpr === "") {
|
|
227
|
-
throw new Error(`forEach() collection expression is empty after stripping the leading "="`);
|
|
228
|
-
}
|
|
229
|
-
assertIdent("forEach itemVar", itemVar);
|
|
230
|
-
if (typeof body !== "function")
|
|
231
|
-
throw new Error(`forEach("${itemVar}") needs a body function`);
|
|
232
|
-
const nodes = childScoped(body);
|
|
233
|
-
if (nodes.length === 0)
|
|
234
|
-
throw new Error(`forEach("${itemVar}") body declared no steps`);
|
|
235
|
-
const node = {
|
|
236
|
-
kind: "forEach",
|
|
237
|
-
collection: collExpr,
|
|
238
|
-
itemVar,
|
|
239
|
-
body: nodes,
|
|
240
|
-
};
|
|
241
|
-
if (opts?.sequential)
|
|
242
|
-
node.sequential = true;
|
|
243
|
-
if (opts?.outputCollection !== undefined) {
|
|
244
|
-
assertIdent("forEach outputCollection", opts.outputCollection);
|
|
245
|
-
node.outputCollection = opts.outputCollection;
|
|
246
|
-
}
|
|
247
|
-
if (opts?.outputElement !== undefined) {
|
|
248
|
-
if (typeof opts.outputElement !== "string" || opts.outputElement.trim() === "") {
|
|
249
|
-
throw new Error(`forEach("${itemVar}") outputElement must be a non-empty FEEL expression`);
|
|
250
|
-
}
|
|
251
|
-
const outEl = opts.outputElement.trim().replace(/^=/, "").trim();
|
|
252
|
-
if (outEl === "") {
|
|
253
|
-
throw new Error(`forEach("${itemVar}") outputElement is empty after stripping the leading "="`);
|
|
254
|
-
}
|
|
255
|
-
node.outputElement = outEl;
|
|
256
|
-
}
|
|
257
|
-
if (opts?.completionCondition !== undefined) {
|
|
258
|
-
if (typeof opts.completionCondition !== "string" || opts.completionCondition.trim() === "") {
|
|
259
|
-
throw new Error(`forEach("${itemVar}") completionCondition must be a non-empty FEEL expression`);
|
|
260
|
-
}
|
|
261
|
-
const compCond = opts.completionCondition.trim().replace(/^=/, "").trim();
|
|
262
|
-
if (compCond === "") {
|
|
263
|
-
throw new Error(`forEach("${itemVar}") completionCondition is empty after stripping the leading "="`);
|
|
264
|
-
}
|
|
265
|
-
node.completionCondition = compCond;
|
|
266
|
-
}
|
|
267
|
-
if (node.outputElement && !node.outputCollection) {
|
|
268
|
-
throw new Error(`forEach("${itemVar}") outputElement needs an outputCollection to collect into`);
|
|
269
|
-
}
|
|
270
|
-
out.push(node);
|
|
271
|
-
return b;
|
|
272
|
-
},
|
|
273
|
-
break() {
|
|
274
|
-
if (ctx.loopDepth === 0)
|
|
275
|
-
throw new Error(`break() is only valid inside a loop`);
|
|
276
|
-
out.push({ kind: "break" });
|
|
277
|
-
return b;
|
|
278
|
-
},
|
|
279
|
-
continue() {
|
|
280
|
-
if (ctx.loopDepth === 0)
|
|
281
|
-
throw new Error(`continue() is only valid inside a loop`);
|
|
282
|
-
out.push({ kind: "continue" });
|
|
283
|
-
return b;
|
|
129
|
+
contractEnvelopes(name) {
|
|
130
|
+
return contractEnvelopes(ctx, name);
|
|
284
131
|
},
|
|
132
|
+
setStartTimer,
|
|
133
|
+
};
|
|
134
|
+
// `startOn` is not a node KIND — it lifts a timer onto the start event and
|
|
135
|
+
// emits no `FlowNode` — so it is installed centrally. Every actual node kind's
|
|
136
|
+
// builder method is contributed by its own module through the registry.
|
|
137
|
+
// A NULL-PROTOTYPE table: collision detection below is an OWN-property check,
|
|
138
|
+
// and building on `Object.prototype` would make both `in`/`Object.hasOwn`
|
|
139
|
+
// spuriously see inherited names (`toString`, `constructor`, …) as registered
|
|
140
|
+
// methods — so a kind whose builder method is legitimately named `toString`
|
|
141
|
+
// would be rejected as a "duplicate" — and would turn a `__proto__` method
|
|
142
|
+
// name into a prototype-mutating footgun. A null prototype removes both.
|
|
143
|
+
const methods = Object.create(null);
|
|
144
|
+
methods.startOn = startOnMethod(api);
|
|
145
|
+
for (const [kind, handlers] of eachNodeKind()) {
|
|
146
|
+
if (!handlers.build)
|
|
147
|
+
continue;
|
|
148
|
+
const method = handlers.builderMethod ?? kind;
|
|
149
|
+
if (Object.hasOwn(methods, method)) {
|
|
150
|
+
throw new Error(`two flow-node kinds both contribute the builder method "${method}"`);
|
|
151
|
+
}
|
|
152
|
+
methods[method] = handlers.build(api);
|
|
153
|
+
}
|
|
154
|
+
if (!isFlowBuilder(methods)) {
|
|
155
|
+
throw new Error(assemblyFailureMessage(methods));
|
|
156
|
+
}
|
|
157
|
+
// `methods` is now the untyped `FlowBuilder` (object) stored for `api.self()`;
|
|
158
|
+
// a chaining method's precise return type is the interface's, not this value's.
|
|
159
|
+
builderRef = methods;
|
|
160
|
+
if (!isFlowBuilder(methods)) {
|
|
161
|
+
throw new Error(assemblyFailureMessage(methods));
|
|
162
|
+
}
|
|
163
|
+
return methods;
|
|
164
|
+
}
|
|
165
|
+
/** The STABLE built-in authoring surface every assembled `FlowBuilder` must
|
|
166
|
+
* expose: `startOn` (installed centrally by {@link startOnMethod}) plus every
|
|
167
|
+
* core method a wave-0 built-in node-kind module under `src/nodes/`
|
|
168
|
+
* contributes. The runtime guard below fails fast if any of these is missing
|
|
169
|
+
* (e.g. a built-in module was tree-shaken away).
|
|
170
|
+
*
|
|
171
|
+
* `satisfies readonly (keyof FlowBuilder)[]` pins each listed name to a real
|
|
172
|
+
* `FlowBuilder` method — a typo or a renamed built-in is caught at compile
|
|
173
|
+
* time. This is deliberately a ONE-DIRECTIONAL pin: it does NOT assert the
|
|
174
|
+
* list mirrors *every* `FlowBuilder` key. `FlowBuilder<C>` is an OPEN,
|
|
175
|
+
* declaration-merged interface — the whole point of the `src/nodes/` seam is
|
|
176
|
+
* that a feature slice adds a builder method by merging into `FlowBuilder`
|
|
177
|
+
* from its own module with NO central edit (see `src/nodes/README.md`). An
|
|
178
|
+
* exhaustiveness check here would force every such slice to also edit this
|
|
179
|
+
* central list, defeating that seam; a slice's own method is instead installed
|
|
180
|
+
* and exercised through its `registerNodeKind` registration and tests. */
|
|
181
|
+
const BUILTIN_BUILDER_METHODS = [
|
|
182
|
+
"startOn",
|
|
183
|
+
"run",
|
|
184
|
+
"task",
|
|
185
|
+
"signal",
|
|
186
|
+
"timer",
|
|
187
|
+
"switch",
|
|
188
|
+
"branch",
|
|
189
|
+
"loop",
|
|
190
|
+
"parallel",
|
|
191
|
+
"forEach",
|
|
192
|
+
"break",
|
|
193
|
+
"continue",
|
|
194
|
+
];
|
|
195
|
+
/** A dynamically-assembled builder satisfies `FlowBuilder<C>` once every core
|
|
196
|
+
* built-in method is installed. The check is a runtime type guard (not an `as`
|
|
197
|
+
* cast): each own-property must be a function, AND every built-in method in
|
|
198
|
+
* {@link BUILTIN_BUILDER_METHODS} must be present — so a missing or
|
|
199
|
+
* tree-shaken node-kind registration fails fast here at builder assembly (a
|
|
200
|
+
* clear "missing registered methods" error) instead of surfacing later as an
|
|
201
|
+
* opaque `w.run is not a function` at authoring time. */
|
|
202
|
+
export function isFlowBuilder(x) {
|
|
203
|
+
return missingBuilderMethods(x).length === 0 && nonFunctionOwnProperties(x).length === 0;
|
|
204
|
+
}
|
|
205
|
+
/** The subset of {@link BUILTIN_BUILDER_METHODS} not installed as a function on
|
|
206
|
+
* `x`. Single source of truth for both the {@link isFlowBuilder} guard and the
|
|
207
|
+
* assembly-failure diagnostic below, so the two never drift. */
|
|
208
|
+
function missingBuilderMethods(x) {
|
|
209
|
+
return BUILTIN_BUILDER_METHODS.filter((method) => typeof Reflect.get(x, method) !== "function");
|
|
210
|
+
}
|
|
211
|
+
/** The own-property names of `x` whose value is not a function — the second
|
|
212
|
+
* half of the {@link isFlowBuilder} invariant, factored out so the
|
|
213
|
+
* assembly-failure diagnostic can name the offending property without drifting
|
|
214
|
+
* from the guard. */
|
|
215
|
+
function nonFunctionOwnProperties(x) {
|
|
216
|
+
return Object.getOwnPropertyNames(x).filter((k) => typeof Reflect.get(x, k) !== "function");
|
|
217
|
+
}
|
|
218
|
+
/** Diagnostic for a failed builder assembly. Names the missing built-in
|
|
219
|
+
* method(s) when a registration was tree-shaken/omitted so the failure is
|
|
220
|
+
* actionable; otherwise, when every built-in is present but some own-property
|
|
221
|
+
* isn't a function, names that offending property instead. Exported (like
|
|
222
|
+
* {@link isFlowBuilder}) so the assembly invariant is unit-testable. */
|
|
223
|
+
export function assemblyFailureMessage(x) {
|
|
224
|
+
const missing = missingBuilderMethods(x);
|
|
225
|
+
if (missing.length > 0) {
|
|
226
|
+
return `internal: assembled FlowBuilder is missing registered methods: ${missing.join(", ")}`;
|
|
227
|
+
}
|
|
228
|
+
const nonFunctions = nonFunctionOwnProperties(x);
|
|
229
|
+
if (nonFunctions.length > 0) {
|
|
230
|
+
const label = nonFunctions.length === 1 ? "property" : "properties";
|
|
231
|
+
return `internal: assembled FlowBuilder has non-function own-${label}: ${nonFunctions.join(", ")}`;
|
|
232
|
+
}
|
|
233
|
+
return "internal: assembled FlowBuilder failed its assembly invariant";
|
|
234
|
+
}
|
|
235
|
+
/** The central `startOn` builder method (not a node kind). */
|
|
236
|
+
function startOnMethod(api) {
|
|
237
|
+
return (spec) => {
|
|
238
|
+
const setCount = TIMER_START_KEYS.filter((key) => typeof timerStartValue(spec, key) === "string").length;
|
|
239
|
+
if (setCount !== 1) {
|
|
240
|
+
throw new Error(`startOn() needs exactly one of { cycle }, { after }, or { at } in flow "${api.id}"`);
|
|
241
|
+
}
|
|
242
|
+
if ("cycle" in spec) {
|
|
243
|
+
const cycle = spec.cycle.trim();
|
|
244
|
+
assertTimerCycle(`startOn cycle`, cycle);
|
|
245
|
+
api.setStartTimer({ cycle });
|
|
246
|
+
}
|
|
247
|
+
else if ("after" in spec) {
|
|
248
|
+
const after = spec.after.trim();
|
|
249
|
+
assertTimerDuration(`startOn after`, after);
|
|
250
|
+
api.setStartTimer({ after });
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
const at = spec.at.trim();
|
|
254
|
+
assertTimerDate(`startOn at`, at);
|
|
255
|
+
api.setStartTimer({ at });
|
|
256
|
+
}
|
|
257
|
+
return api.self();
|
|
285
258
|
};
|
|
286
|
-
return b;
|
|
287
259
|
}
|
|
288
260
|
export function defineFlow(id, second, third) {
|
|
289
261
|
assertIdent("workflow id", id);
|
|
@@ -315,36 +287,17 @@ export function defineFlow(id, second, third) {
|
|
|
315
287
|
return flow;
|
|
316
288
|
}
|
|
317
289
|
// --- Tree walkers ------------------------------------------------------------
|
|
318
|
-
/** Depth-first visit of every node in a flow tree
|
|
319
|
-
*
|
|
290
|
+
/** Depth-first visit of every node in a flow tree. Recursion into a kind's
|
|
291
|
+
* nested bodies is DISPATCHED through the registry (each kind's `walk` handler),
|
|
292
|
+
* so a slice's structural combinator recurses without editing a central switch.
|
|
293
|
+
* Fails fast (via `requireNodeKind`) on an unregistered kind — matching the
|
|
294
|
+
* emitter — so a tree-shaken/missing registration surfaces as a clear error
|
|
295
|
+
* rather than silently skipping recursion into that node's nested bodies. A
|
|
296
|
+
* registered leaf kind with no `walk` handler is fine (nothing to recurse). */
|
|
320
297
|
export function walkNodes(nodes, visit) {
|
|
321
298
|
for (const n of nodes) {
|
|
322
299
|
visit(n);
|
|
323
|
-
|
|
324
|
-
case "switch":
|
|
325
|
-
for (const c of n.cases)
|
|
326
|
-
walkNodes(c.body, visit);
|
|
327
|
-
if (n.default)
|
|
328
|
-
walkNodes(n.default, visit);
|
|
329
|
-
break;
|
|
330
|
-
case "branch":
|
|
331
|
-
walkNodes(n.then, visit);
|
|
332
|
-
if (n.else)
|
|
333
|
-
walkNodes(n.else, visit);
|
|
334
|
-
break;
|
|
335
|
-
case "loop":
|
|
336
|
-
walkNodes(n.body, visit);
|
|
337
|
-
break;
|
|
338
|
-
case "parallel":
|
|
339
|
-
for (const branch of n.branches)
|
|
340
|
-
walkNodes(branch, visit);
|
|
341
|
-
break;
|
|
342
|
-
case "forEach":
|
|
343
|
-
walkNodes(n.body, visit);
|
|
344
|
-
break;
|
|
345
|
-
default:
|
|
346
|
-
break;
|
|
347
|
-
}
|
|
300
|
+
requireNodeKind(n.kind).walk?.(n, (body) => walkNodes(body, visit));
|
|
348
301
|
}
|
|
349
302
|
}
|
|
350
303
|
/** The job types of a flow's external `task` steps (anywhere in the tree) — the
|
|
@@ -390,8 +343,36 @@ class Compiler {
|
|
|
390
343
|
live = [];
|
|
391
344
|
seq = 0;
|
|
392
345
|
gw = 0;
|
|
346
|
+
/** The emit surface handed to each kind's registered `emit` handler. */
|
|
347
|
+
emitApi;
|
|
393
348
|
constructor(flow) {
|
|
394
349
|
this.flow = flow;
|
|
350
|
+
this.emitApi = {
|
|
351
|
+
flowId: flow.id,
|
|
352
|
+
newEdge: (from, opts) => this.newEdge(from, opts),
|
|
353
|
+
connect: (incoming, toId) => this.connect(incoming, toId),
|
|
354
|
+
recordEnvelope: (env) => this.recordEnvelope(env),
|
|
355
|
+
emitList: (list, incoming, loop) => this.emitList(list, incoming, loop),
|
|
356
|
+
addNode: (node) => {
|
|
357
|
+
this.nodes.push({ ...node, scope: node.scope ?? this.currentScope() });
|
|
358
|
+
},
|
|
359
|
+
addServiceTask: (node, mi) => this.addServiceTask(node, mi),
|
|
360
|
+
addCatchEvent: (node) => this.addCatchEvent(node),
|
|
361
|
+
addTimerCatchEvent: (node) => this.addTimerCatchEvent(node),
|
|
362
|
+
addGateway: (id, name) => this.addGateway(id, name),
|
|
363
|
+
addParallelGateway: (id) => this.addParallelGateway(id),
|
|
364
|
+
addSubProcess: (id, mi) => this.addSubProcess(id, mi),
|
|
365
|
+
addPlainStart: (id) => this.addPlainStart(id),
|
|
366
|
+
addPlainEnd: (id) => this.addPlainEnd(id),
|
|
367
|
+
nextGw: () => this.gw++,
|
|
368
|
+
currentScope: () => this.currentScope(),
|
|
369
|
+
pushScope: (id) => {
|
|
370
|
+
this.scopeStack.push(id);
|
|
371
|
+
},
|
|
372
|
+
popScope: () => {
|
|
373
|
+
this.scopeStack.pop();
|
|
374
|
+
},
|
|
375
|
+
};
|
|
395
376
|
}
|
|
396
377
|
currentScope() {
|
|
397
378
|
return this.scopeStack.length ? this.scopeStack[this.scopeStack.length - 1] : undefined;
|
|
@@ -529,145 +510,16 @@ class Compiler {
|
|
|
529
510
|
this.nodes.push(node);
|
|
530
511
|
return node;
|
|
531
512
|
}
|
|
532
|
-
/** The `<bpmn:multiInstanceLoopCharacteristics>` block for a `forEach` node —
|
|
533
|
-
* the same shape whether lifted onto a leaf service task or a sub-process. */
|
|
534
|
-
miCharacteristics(node) {
|
|
535
|
-
const seq = node.sequential ? "true" : "false";
|
|
536
|
-
const attrs = [
|
|
537
|
-
`inputCollection="${escapeXml(`=${node.collection}`)}"`,
|
|
538
|
-
`inputElement="${escapeXml(node.itemVar)}"`,
|
|
539
|
-
];
|
|
540
|
-
if (node.outputCollection)
|
|
541
|
-
attrs.push(`outputCollection="${escapeXml(node.outputCollection)}"`);
|
|
542
|
-
if (node.outputElement)
|
|
543
|
-
attrs.push(`outputElement="${escapeXml(`=${node.outputElement}`)}"`);
|
|
544
|
-
const completion = node.completionCondition
|
|
545
|
-
? ` <bpmn:completionCondition>${escapeXml(`=${node.completionCondition}`)}</bpmn:completionCondition>\n`
|
|
546
|
-
: "";
|
|
547
|
-
return (` <bpmn:multiInstanceLoopCharacteristics isSequential="${seq}">\n` +
|
|
548
|
-
` <bpmn:extensionElements>\n` +
|
|
549
|
-
` <zeebe:loopCharacteristics ${attrs.join(" ")} />\n` +
|
|
550
|
-
` </bpmn:extensionElements>\n` +
|
|
551
|
-
completion +
|
|
552
|
-
` </bpmn:multiInstanceLoopCharacteristics>\n`);
|
|
553
|
-
}
|
|
554
513
|
emitList(list, incoming, loop) {
|
|
555
514
|
let cur = incoming;
|
|
556
515
|
for (const node of list)
|
|
557
516
|
cur = this.emitNode(node, cur, loop);
|
|
558
517
|
return cur;
|
|
559
518
|
}
|
|
519
|
+
/** Emit one node by DISPATCHING to its registered `emit` handler — no central
|
|
520
|
+
* switch on `node.kind`. A slice's kind is emitted by its own module. */
|
|
560
521
|
emitNode(node, incoming, loop) {
|
|
561
|
-
|
|
562
|
-
case "run":
|
|
563
|
-
case "task": {
|
|
564
|
-
this.addServiceTask(node);
|
|
565
|
-
this.connect(incoming, node.name);
|
|
566
|
-
return [this.newEdge(node.name)];
|
|
567
|
-
}
|
|
568
|
-
case "signal": {
|
|
569
|
-
this.addCatchEvent(node);
|
|
570
|
-
this.connect(incoming, node.name);
|
|
571
|
-
this.recordEnvelope(node.payload);
|
|
572
|
-
return [this.newEdge(node.name)];
|
|
573
|
-
}
|
|
574
|
-
case "timer": {
|
|
575
|
-
this.addTimerCatchEvent(node);
|
|
576
|
-
this.connect(incoming, node.name);
|
|
577
|
-
return [this.newEdge(node.name)];
|
|
578
|
-
}
|
|
579
|
-
case "switch": {
|
|
580
|
-
const id = `Gw_${this.gw++}`;
|
|
581
|
-
const gw = this.addGateway(id, node.subject);
|
|
582
|
-
this.connect(incoming, id);
|
|
583
|
-
const out = [];
|
|
584
|
-
for (const c of node.cases) {
|
|
585
|
-
const e = this.newEdge(id, { condition: feelEquals(node.subject, c.value), name: c.value });
|
|
586
|
-
out.push(...this.emitList(c.body, [e], loop));
|
|
587
|
-
}
|
|
588
|
-
// The default (or a synthesised fall-through) is the unconditional edge.
|
|
589
|
-
const de = this.newEdge(id, { name: "default" });
|
|
590
|
-
gw.defaultFlow = de.id;
|
|
591
|
-
out.push(...this.emitList(node.default ?? [], [de], loop));
|
|
592
|
-
return out;
|
|
593
|
-
}
|
|
594
|
-
case "branch": {
|
|
595
|
-
const id = `Gw_${this.gw++}`;
|
|
596
|
-
const gw = this.addGateway(id);
|
|
597
|
-
this.connect(incoming, id);
|
|
598
|
-
const te = this.newEdge(id, { condition: feel(node.condition), name: "then" });
|
|
599
|
-
const out = this.emitList(node.then, [te], loop);
|
|
600
|
-
const ee = this.newEdge(id, { name: "else" });
|
|
601
|
-
gw.defaultFlow = ee.id;
|
|
602
|
-
out.push(...this.emitList(node.else ?? [], [ee], loop));
|
|
603
|
-
return out;
|
|
604
|
-
}
|
|
605
|
-
case "loop": {
|
|
606
|
-
const headId = `Loop_${this.gw++}`;
|
|
607
|
-
this.addGateway(headId);
|
|
608
|
-
this.connect(incoming, headId);
|
|
609
|
-
const ctx = { headId, breaks: [] };
|
|
610
|
-
const headOut = this.newEdge(headId);
|
|
611
|
-
const bodyOut = this.emitList(node.body, [headOut], ctx);
|
|
612
|
-
// Normal fall-through of the body loops back to the head (continue).
|
|
613
|
-
this.connect(bodyOut, headId);
|
|
614
|
-
// The loop exits only via break edges.
|
|
615
|
-
return ctx.breaks;
|
|
616
|
-
}
|
|
617
|
-
case "break": {
|
|
618
|
-
if (!loop)
|
|
619
|
-
throw new Error(`break outside a loop in flow "${this.flow.id}"`);
|
|
620
|
-
// The incoming edges (from the previous node) become the loop's exit
|
|
621
|
-
// danglers; this path does not fall through.
|
|
622
|
-
loop.breaks.push(...incoming);
|
|
623
|
-
return [];
|
|
624
|
-
}
|
|
625
|
-
case "continue": {
|
|
626
|
-
if (!loop)
|
|
627
|
-
throw new Error(`continue outside a loop in flow "${this.flow.id}"`);
|
|
628
|
-
this.connect(incoming, loop.headId);
|
|
629
|
-
return [];
|
|
630
|
-
}
|
|
631
|
-
case "parallel": {
|
|
632
|
-
// A diverging parallel gateway forks a token onto every branch; a
|
|
633
|
-
// converging one joins them (AND-join) into a single continuation.
|
|
634
|
-
const splitId = `Gw_${this.gw++}`;
|
|
635
|
-
this.addParallelGateway(splitId);
|
|
636
|
-
this.connect(incoming, splitId);
|
|
637
|
-
const joinId = `Gw_${this.gw++}`;
|
|
638
|
-
this.addParallelGateway(joinId);
|
|
639
|
-
for (const branch of node.branches) {
|
|
640
|
-
const e = this.newEdge(splitId);
|
|
641
|
-
const branchOut = this.emitList(branch, [e], loop);
|
|
642
|
-
this.connect(branchOut, joinId);
|
|
643
|
-
}
|
|
644
|
-
return [this.newEdge(joinId)];
|
|
645
|
-
}
|
|
646
|
-
case "forEach": {
|
|
647
|
-
const mi = this.miCharacteristics(node);
|
|
648
|
-
const only = node.body.length === 1 ? node.body[0] : undefined;
|
|
649
|
-
// A single service-task body carries the MI characteristics directly; a
|
|
650
|
-
// multi-step body is wrapped in an embedded MI sub-process (its own token
|
|
651
|
-
// scope, with its own start/end).
|
|
652
|
-
if (only && (only.kind === "run" || only.kind === "task")) {
|
|
653
|
-
this.addServiceTask(only, mi);
|
|
654
|
-
this.connect(incoming, only.name);
|
|
655
|
-
return [this.newEdge(only.name)];
|
|
656
|
-
}
|
|
657
|
-
const subId = `Sub_${this.gw++}`;
|
|
658
|
-
this.addSubProcess(subId, mi);
|
|
659
|
-
this.connect(incoming, subId);
|
|
660
|
-
this.scopeStack.push(subId);
|
|
661
|
-
const startId = `${subId}_start`;
|
|
662
|
-
this.addPlainStart(startId);
|
|
663
|
-
const innerOut = this.emitList(node.body, [this.newEdge(startId)], null);
|
|
664
|
-
const endId = `${subId}_end`;
|
|
665
|
-
this.addPlainEnd(endId);
|
|
666
|
-
this.connect(innerOut, endId);
|
|
667
|
-
this.scopeStack.pop();
|
|
668
|
-
return [this.newEdge(subId)];
|
|
669
|
-
}
|
|
670
|
-
}
|
|
522
|
+
return requireNodeKind(node.kind).emit(node, incoming, loop, this.emitApi);
|
|
671
523
|
}
|
|
672
524
|
/** Render every node + sequence flow that belongs to `scope` (undefined = the
|
|
673
525
|
* root process, a sub-process id otherwise). Called for the process body and,
|
|
@@ -780,16 +632,22 @@ export function declarativeToBpmn(flow) {
|
|
|
780
632
|
}
|
|
781
633
|
// --- small XML / FEEL helpers ------------------------------------------------
|
|
782
634
|
const envelopeProp = (dir, value) => ` <zeebe:property name="io.nanobpm.dataEnvelope.${dir}" value="${escapeXml(value)}" />`;
|
|
783
|
-
/** Wrap a raw FEEL expression as a Zeebe condition body (leading `=`).
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
635
|
+
/** Wrap a raw FEEL expression as a Zeebe condition body (leading `=`). Exported
|
|
636
|
+
* for slice-added combinators that emit conditional sequence flows. */
|
|
637
|
+
export const feel = (expr) => `=${expr}`;
|
|
638
|
+
/** A FEEL equality test `subject = "value"`, with the value as a FEEL string.
|
|
639
|
+
* Exported for slice-added combinators that emit an XOR gateway. */
|
|
640
|
+
export const feelEquals = (subject, value) => `=${subject} = "${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
641
|
+
/** Render the `<bpmn:incoming>`/`<bpmn:outgoing>` refs of a flow node. Exported
|
|
642
|
+
* for slice `RenderNode.render` closures. */
|
|
643
|
+
export function incomingOutgoing(inc, outg) {
|
|
788
644
|
return (inc.map((f) => ` <bpmn:incoming>${f}</bpmn:incoming>\n`).join("") +
|
|
789
645
|
outg.map((f) => ` <bpmn:outgoing>${f}</bpmn:outgoing>\n`).join(""));
|
|
790
646
|
}
|
|
791
|
-
|
|
792
|
-
const
|
|
647
|
+
/** Render only the `<bpmn:incoming>` refs (for end-like events). Exported. */
|
|
648
|
+
export const incomingOnly = (inc) => inc.map((f) => `<bpmn:incoming>${f}</bpmn:incoming>`).join("");
|
|
649
|
+
/** Render only the `<bpmn:outgoing>` refs (for start-like events). Exported. */
|
|
650
|
+
export const outgoingOnly = (outg) => outg.map((f) => `<bpmn:outgoing>${f}</bpmn:outgoing>`).join("");
|
|
793
651
|
function sequenceFlow(e) {
|
|
794
652
|
const nm = e.name ? ` name="${escapeXml(e.name)}"` : "";
|
|
795
653
|
if (e.condition) {
|