@bpmnkit/engine 0.1.7
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 +130 -0
- package/dist/dmn.d.ts +7 -0
- package/dist/dmn.js +115 -0
- package/dist/engine.d.ts +36 -0
- package/dist/engine.js +66 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +7 -0
- package/dist/instance.d.ts +72 -0
- package/dist/instance.js +654 -0
- package/dist/timers.d.ts +14 -0
- package/dist/timers.js +85 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.js +3 -0
- package/dist/variables.d.ts +25 -0
- package/dist/variables.js +76 -0
- package/dist/zeebe.d.ts +35 -0
- package/dist/zeebe.js +75 -0
- package/package.json +31 -0
package/dist/instance.js
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { generateId } from "@bpmnkit/core";
|
|
2
|
+
import { evaluate, parseExpression } from "@bpmnkit/feel";
|
|
3
|
+
import { evaluateDecision } from "./dmn.js";
|
|
4
|
+
import { scheduleTimer } from "./timers.js";
|
|
5
|
+
import { VariableStore } from "./variables.js";
|
|
6
|
+
import { parseZeebeExt } from "./zeebe.js";
|
|
7
|
+
// ── ProcessInstance ────────────────────────────────────────────────────────────
|
|
8
|
+
export class ProcessInstance {
|
|
9
|
+
id;
|
|
10
|
+
processId;
|
|
11
|
+
_state = "active";
|
|
12
|
+
_error;
|
|
13
|
+
/** tokenId → Token */
|
|
14
|
+
allTokens = new Map();
|
|
15
|
+
/** Scope stack: rootScopeId + any active sub-process scopes */
|
|
16
|
+
scopes = new Map();
|
|
17
|
+
/** Message correlation: messageName → resolve callback */
|
|
18
|
+
messageSubscriptions = new Map();
|
|
19
|
+
/** Timer cancel functions keyed by tokenId */
|
|
20
|
+
timerCancels = new Map();
|
|
21
|
+
/** Activation count per elementId — used to detect infinite loops. */
|
|
22
|
+
activationCount = new Map();
|
|
23
|
+
static MAX_ACTIVATIONS = 100;
|
|
24
|
+
/** Active boundary timer cancels, keyed by elementId */
|
|
25
|
+
boundaryTimerCancels = new Map();
|
|
26
|
+
variables;
|
|
27
|
+
rootScopeId;
|
|
28
|
+
listeners = [];
|
|
29
|
+
decisions;
|
|
30
|
+
forms;
|
|
31
|
+
jobWorkers;
|
|
32
|
+
/**
|
|
33
|
+
* Optional hook called just before an element completes (token moves on).
|
|
34
|
+
* Returning a Promise lets the caller pause execution — useful for
|
|
35
|
+
* step-by-step simulation. Set via {@link Engine.start} options.
|
|
36
|
+
*/
|
|
37
|
+
beforeComplete;
|
|
38
|
+
constructor(process, decisions, forms, jobWorkers, initialVars) {
|
|
39
|
+
this.id = generateId("pi");
|
|
40
|
+
this.processId = process.id;
|
|
41
|
+
this.decisions = decisions;
|
|
42
|
+
this.forms = forms;
|
|
43
|
+
this.jobWorkers = jobWorkers;
|
|
44
|
+
this.variables = new VariableStore();
|
|
45
|
+
this.rootScopeId = `scope_${this.id}`;
|
|
46
|
+
this.variables.createScope(this.rootScopeId);
|
|
47
|
+
for (const [k, v] of Object.entries(initialVars)) {
|
|
48
|
+
this.variables.setLocal(this.rootScopeId, k, v);
|
|
49
|
+
}
|
|
50
|
+
const rootCtx = this.buildScopeCtx(this.rootScopeId, undefined, undefined, process.flowElements, process.sequenceFlows);
|
|
51
|
+
this.scopes.set(this.rootScopeId, rootCtx);
|
|
52
|
+
}
|
|
53
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
54
|
+
get state() {
|
|
55
|
+
return this._state;
|
|
56
|
+
}
|
|
57
|
+
get error() {
|
|
58
|
+
return this._error;
|
|
59
|
+
}
|
|
60
|
+
get activeElements() {
|
|
61
|
+
return [...this.allTokens.values()].map((t) => t.elementId);
|
|
62
|
+
}
|
|
63
|
+
get variables_snapshot() {
|
|
64
|
+
return this.variables.getAll(this.rootScopeId);
|
|
65
|
+
}
|
|
66
|
+
onChange(callback) {
|
|
67
|
+
this.listeners.push(callback);
|
|
68
|
+
return () => {
|
|
69
|
+
const idx = this.listeners.indexOf(callback);
|
|
70
|
+
if (idx !== -1)
|
|
71
|
+
this.listeners.splice(idx, 1);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
cancel() {
|
|
75
|
+
this._state = "terminated";
|
|
76
|
+
this.cancelAllTimers();
|
|
77
|
+
this.allTokens.clear();
|
|
78
|
+
for (const ctx of this.scopes.values())
|
|
79
|
+
ctx.tokens.clear();
|
|
80
|
+
}
|
|
81
|
+
/** Kick off execution. Called by Engine after construction. */
|
|
82
|
+
start() {
|
|
83
|
+
const ctx = this.scopes.get(this.rootScopeId);
|
|
84
|
+
if (ctx === undefined)
|
|
85
|
+
return;
|
|
86
|
+
const starts = [...ctx.elements.values()].filter((el) => el.type === "startEvent" && el.incoming.length === 0);
|
|
87
|
+
// Defer activation so callers can attach onChange listeners before events fire.
|
|
88
|
+
void Promise.resolve().then(() => {
|
|
89
|
+
// Emit variable:set for initial variables so listeners (e.g. play panel) see them.
|
|
90
|
+
for (const [name, value] of Object.entries(this.variables.getAll(this.rootScopeId))) {
|
|
91
|
+
this.emit({ type: "variable:set", name, value, scopeId: this.rootScopeId });
|
|
92
|
+
}
|
|
93
|
+
for (const s of starts) {
|
|
94
|
+
void this.activate(s.id, this.rootScopeId, undefined);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** Deliver a message to a waiting element. */
|
|
99
|
+
deliverMessage(messageName) {
|
|
100
|
+
const resolve = this.messageSubscriptions.get(messageName);
|
|
101
|
+
if (resolve !== undefined) {
|
|
102
|
+
this.messageSubscriptions.delete(messageName);
|
|
103
|
+
resolve();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// ── Scope building ─────────────────────────────────────────────────────────
|
|
107
|
+
buildScopeCtx(scopeId, parentScopeId, onComplete, elements, flows) {
|
|
108
|
+
const elemMap = new Map();
|
|
109
|
+
const outgoing = new Map();
|
|
110
|
+
const flowMap = new Map();
|
|
111
|
+
const boundaries = new Map();
|
|
112
|
+
for (const el of elements) {
|
|
113
|
+
elemMap.set(el.id, el);
|
|
114
|
+
if (el.type === "boundaryEvent") {
|
|
115
|
+
const list = boundaries.get(el.attachedToRef) ?? [];
|
|
116
|
+
list.push(el);
|
|
117
|
+
boundaries.set(el.attachedToRef, list);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const flow of flows) {
|
|
121
|
+
flowMap.set(flow.id, flow);
|
|
122
|
+
const list = outgoing.get(flow.sourceRef) ?? [];
|
|
123
|
+
list.push(flow);
|
|
124
|
+
outgoing.set(flow.sourceRef, list);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
scopeId,
|
|
128
|
+
parentScopeId,
|
|
129
|
+
onComplete,
|
|
130
|
+
elements: elemMap,
|
|
131
|
+
outgoing,
|
|
132
|
+
flows: flowMap,
|
|
133
|
+
boundaries,
|
|
134
|
+
tokens: new Set(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// ── Token lifecycle ────────────────────────────────────────────────────────
|
|
138
|
+
createToken(elementId, scopeId) {
|
|
139
|
+
const token = { id: generateId("tok"), elementId, scopeId };
|
|
140
|
+
this.allTokens.set(token.id, token);
|
|
141
|
+
this.scopes.get(scopeId)?.tokens.add(token.id);
|
|
142
|
+
return token;
|
|
143
|
+
}
|
|
144
|
+
removeToken(token) {
|
|
145
|
+
this.allTokens.delete(token.id);
|
|
146
|
+
this.scopes.get(token.scopeId)?.tokens.delete(token.id);
|
|
147
|
+
this.timerCancels.get(token.id)?.();
|
|
148
|
+
this.timerCancels.delete(token.id);
|
|
149
|
+
}
|
|
150
|
+
// ── Parallel join tracking ─────────────────────────────────────────────────
|
|
151
|
+
/** elementId → set of incomingFlowIds received */
|
|
152
|
+
joins = new Map();
|
|
153
|
+
// ── Activation ─────────────────────────────────────────────────────────────
|
|
154
|
+
async activate(elementId, scopeId, incomingFlowId) {
|
|
155
|
+
if (this._state !== "active")
|
|
156
|
+
return;
|
|
157
|
+
const ctx = this.scopes.get(scopeId);
|
|
158
|
+
if (ctx === undefined)
|
|
159
|
+
return;
|
|
160
|
+
const el = ctx.elements.get(elementId);
|
|
161
|
+
if (el === undefined)
|
|
162
|
+
return;
|
|
163
|
+
// ── Infinite-loop guard ────────────────────────────────────────────────
|
|
164
|
+
const activations = (this.activationCount.get(elementId) ?? 0) + 1;
|
|
165
|
+
this.activationCount.set(elementId, activations);
|
|
166
|
+
if (activations > ProcessInstance.MAX_ACTIVATIONS) {
|
|
167
|
+
const error = `Infinite loop detected at element "${elementId}" (activated ${activations} times)`;
|
|
168
|
+
this.emit({ type: "element:failed", elementId, error });
|
|
169
|
+
this._state = "failed";
|
|
170
|
+
this._error = error;
|
|
171
|
+
this.emit({ type: "process:failed", error });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// ── Parallel gateway join ──────────────────────────────────────────────
|
|
175
|
+
if (el.type === "parallelGateway" && el.incoming.length > 1) {
|
|
176
|
+
const seen = this.joins.get(elementId) ?? new Set();
|
|
177
|
+
if (incomingFlowId !== undefined)
|
|
178
|
+
seen.add(incomingFlowId);
|
|
179
|
+
this.joins.set(elementId, seen);
|
|
180
|
+
if (seen.size < el.incoming.length)
|
|
181
|
+
return;
|
|
182
|
+
this.joins.delete(elementId);
|
|
183
|
+
}
|
|
184
|
+
this.emit({ type: "element:entering", elementId, elementName: el.name, elementType: el.type });
|
|
185
|
+
// Apply ioMapping inputs
|
|
186
|
+
const ext = parseZeebeExt(el.extensionElements);
|
|
187
|
+
if (ext.ioMapping) {
|
|
188
|
+
for (const inp of ext.ioMapping.inputs) {
|
|
189
|
+
const val = this.evalFeel(inp.source, scopeId, {
|
|
190
|
+
elementId: el.id,
|
|
191
|
+
property: `input:${inp.target}`,
|
|
192
|
+
});
|
|
193
|
+
this.variables.setLocal(scopeId, inp.target, val);
|
|
194
|
+
this.emit({ type: "variable:set", name: inp.target, value: val, scopeId });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const token = this.createToken(elementId, scopeId);
|
|
198
|
+
this.emit({ type: "element:entered", elementId, elementName: el.name, elementType: el.type });
|
|
199
|
+
// Schedule boundary events for activities
|
|
200
|
+
this.scheduleBoundaryTimers(el.id, token, scopeId, ctx);
|
|
201
|
+
await this.dispatch(token, el, ext, ctx);
|
|
202
|
+
}
|
|
203
|
+
// ── Dispatch ───────────────────────────────────────────────────────────────
|
|
204
|
+
async dispatch(token, el, ext, ctx) {
|
|
205
|
+
switch (el.type) {
|
|
206
|
+
case "startEvent":
|
|
207
|
+
case "task":
|
|
208
|
+
case "manualTask":
|
|
209
|
+
case "sendTask":
|
|
210
|
+
case "receiveTask":
|
|
211
|
+
case "intermediateThrowEvent":
|
|
212
|
+
await this.complete(token, ctx);
|
|
213
|
+
break;
|
|
214
|
+
case "endEvent":
|
|
215
|
+
await this.handleEndEvent(token, el, ctx);
|
|
216
|
+
break;
|
|
217
|
+
case "serviceTask":
|
|
218
|
+
case "userTask":
|
|
219
|
+
await this.handleJobTask(token, el, ext, ctx);
|
|
220
|
+
break;
|
|
221
|
+
case "scriptTask":
|
|
222
|
+
this.handleScriptTask(el, ext, ctx.scopeId);
|
|
223
|
+
await this.complete(token, ctx);
|
|
224
|
+
break;
|
|
225
|
+
case "businessRuleTask":
|
|
226
|
+
this.handleBusinessRuleTask(el, ext, ctx.scopeId);
|
|
227
|
+
await this.complete(token, ctx);
|
|
228
|
+
break;
|
|
229
|
+
case "exclusiveGateway":
|
|
230
|
+
await this.handleExclusiveGateway(token, el, ctx);
|
|
231
|
+
break;
|
|
232
|
+
case "parallelGateway":
|
|
233
|
+
await this.complete(token, ctx);
|
|
234
|
+
break;
|
|
235
|
+
case "inclusiveGateway":
|
|
236
|
+
await this.handleInclusiveGateway(token, el, ctx);
|
|
237
|
+
break;
|
|
238
|
+
case "intermediateCatchEvent":
|
|
239
|
+
await this.handleIntermediateCatchEvent(token, el, ctx);
|
|
240
|
+
break;
|
|
241
|
+
case "subProcess":
|
|
242
|
+
case "transaction":
|
|
243
|
+
await this.handleSubProcess(token, el, ctx);
|
|
244
|
+
break;
|
|
245
|
+
default:
|
|
246
|
+
// eventSubProcess, adHocSubProcess, callActivity, etc. — auto-complete
|
|
247
|
+
await this.complete(token, ctx);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// ── Element handlers ───────────────────────────────────────────────────────
|
|
252
|
+
async handleEndEvent(token, el, ctx) {
|
|
253
|
+
const eventDef = el.eventDefinitions[0];
|
|
254
|
+
if (eventDef?.type === "terminate") {
|
|
255
|
+
this.cancelAllTimers();
|
|
256
|
+
for (const [id] of this.allTokens) {
|
|
257
|
+
const tok = this.allTokens.get(id);
|
|
258
|
+
if (tok !== undefined)
|
|
259
|
+
this.scopes.get(tok.scopeId)?.tokens.delete(id);
|
|
260
|
+
}
|
|
261
|
+
this.allTokens.clear();
|
|
262
|
+
this.emit({
|
|
263
|
+
type: "element:leaving",
|
|
264
|
+
elementId: el.id,
|
|
265
|
+
elementName: el.name,
|
|
266
|
+
elementType: el.type,
|
|
267
|
+
});
|
|
268
|
+
this.emit({
|
|
269
|
+
type: "element:left",
|
|
270
|
+
elementId: el.id,
|
|
271
|
+
elementName: el.name,
|
|
272
|
+
elementType: el.type,
|
|
273
|
+
});
|
|
274
|
+
this.finishProcess();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (eventDef?.type === "error") {
|
|
278
|
+
this.removeToken(token);
|
|
279
|
+
this.emit({
|
|
280
|
+
type: "element:leaving",
|
|
281
|
+
elementId: el.id,
|
|
282
|
+
elementName: el.name,
|
|
283
|
+
elementType: el.type,
|
|
284
|
+
});
|
|
285
|
+
this.emit({
|
|
286
|
+
type: "element:left",
|
|
287
|
+
elementId: el.id,
|
|
288
|
+
elementName: el.name,
|
|
289
|
+
elementType: el.type,
|
|
290
|
+
});
|
|
291
|
+
this.propagateError(eventDef.errorRef ?? "unknown", ctx);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
await this.complete(token, ctx);
|
|
295
|
+
}
|
|
296
|
+
async handleJobTask(token, el, ext, ctx) {
|
|
297
|
+
const jobType = ext.taskDefinition?.type ?? el.type;
|
|
298
|
+
const handler = this.jobWorkers.get(jobType);
|
|
299
|
+
if (handler === undefined) {
|
|
300
|
+
// No real worker — apply example output JSON if configured (play mode simulation)
|
|
301
|
+
if (ext.exampleOutputJson) {
|
|
302
|
+
try {
|
|
303
|
+
const example = JSON.parse(ext.exampleOutputJson);
|
|
304
|
+
for (const [k, v] of Object.entries(example)) {
|
|
305
|
+
this.variables.set(ctx.scopeId, k, v);
|
|
306
|
+
this.emit({ type: "variable:set", name: k, value: v, scopeId: ctx.scopeId });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// Invalid JSON — skip silently
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
await this.complete(token, ctx);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const jobId = generateId("job");
|
|
317
|
+
const headers = ext.taskHeaders ?? {};
|
|
318
|
+
const vars = this.variables.getAll(ctx.scopeId);
|
|
319
|
+
let jobError;
|
|
320
|
+
await new Promise((resolve) => {
|
|
321
|
+
const job = {
|
|
322
|
+
id: jobId,
|
|
323
|
+
type: jobType,
|
|
324
|
+
headers,
|
|
325
|
+
variables: vars,
|
|
326
|
+
complete: (outVars) => {
|
|
327
|
+
if (outVars) {
|
|
328
|
+
for (const [k, v] of Object.entries(outVars)) {
|
|
329
|
+
this.variables.set(ctx.scopeId, k, v);
|
|
330
|
+
this.emit({ type: "variable:set", name: k, value: v, scopeId: ctx.scopeId });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
resolve();
|
|
334
|
+
},
|
|
335
|
+
fail: (error) => {
|
|
336
|
+
jobError = error;
|
|
337
|
+
resolve();
|
|
338
|
+
},
|
|
339
|
+
throwError: (_code, message) => {
|
|
340
|
+
jobError = message;
|
|
341
|
+
resolve();
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
this.emit({ type: "job:created", job });
|
|
345
|
+
void Promise.resolve(handler(job)).catch((err) => {
|
|
346
|
+
jobError = err instanceof Error ? err.message : String(err);
|
|
347
|
+
resolve();
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
if (jobError !== undefined) {
|
|
351
|
+
this._state = "failed";
|
|
352
|
+
this._error = jobError;
|
|
353
|
+
this.emit({ type: "process:failed", error: jobError });
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (this._state === "active") {
|
|
357
|
+
this.cancelBoundaryTimers(el.id);
|
|
358
|
+
await this.complete(token, ctx);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
handleScriptTask(el, ext, scopeId) {
|
|
362
|
+
const script = ext.scriptTask;
|
|
363
|
+
if (script === undefined || script.expression.trim() === "")
|
|
364
|
+
return;
|
|
365
|
+
const result = this.evalFeel(script.expression, scopeId, {
|
|
366
|
+
elementId: el.id,
|
|
367
|
+
property: "script",
|
|
368
|
+
});
|
|
369
|
+
if (script.resultVariable !== "") {
|
|
370
|
+
this.variables.set(scopeId, script.resultVariable, result);
|
|
371
|
+
this.emit({ type: "variable:set", name: script.resultVariable, value: result, scopeId });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
handleBusinessRuleTask(_el, ext, scopeId) {
|
|
375
|
+
const cd = ext.calledDecision;
|
|
376
|
+
if (cd === undefined)
|
|
377
|
+
return;
|
|
378
|
+
const decision = this.decisions.get(cd.decisionId);
|
|
379
|
+
if (decision === undefined)
|
|
380
|
+
return;
|
|
381
|
+
const result = evaluateDecision(decision, this.variables.getAll(scopeId));
|
|
382
|
+
this.variables.set(scopeId, cd.resultVariable, result);
|
|
383
|
+
this.emit({ type: "variable:set", name: cd.resultVariable, value: result, scopeId });
|
|
384
|
+
}
|
|
385
|
+
async handleExclusiveGateway(token, el, ctx) {
|
|
386
|
+
const outflows = ctx.outgoing.get(el.id) ?? [];
|
|
387
|
+
const defaultFlowId = el.default;
|
|
388
|
+
// First pass: conditioned flows (the default flow is always skipped here).
|
|
389
|
+
let taken;
|
|
390
|
+
for (const flow of outflows) {
|
|
391
|
+
if (flow.id === defaultFlowId)
|
|
392
|
+
continue;
|
|
393
|
+
if (flow.conditionExpression === undefined)
|
|
394
|
+
continue;
|
|
395
|
+
if (this.evalCondition(flow.conditionExpression.text, ctx.scopeId, {
|
|
396
|
+
elementId: el.id,
|
|
397
|
+
property: `flow:${flow.id}`,
|
|
398
|
+
})) {
|
|
399
|
+
taken = flow;
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Second pass: unconditioned non-default flows act as fallback (lower priority than
|
|
404
|
+
// conditioned flows so they behave like an implicit "else" branch).
|
|
405
|
+
if (taken === undefined) {
|
|
406
|
+
for (const flow of outflows) {
|
|
407
|
+
if (flow.id === defaultFlowId)
|
|
408
|
+
continue;
|
|
409
|
+
if (flow.conditionExpression === undefined) {
|
|
410
|
+
taken = flow;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Last resort: explicit default flow. Its conditionExpression (if any) is intentionally
|
|
416
|
+
// ignored — the default flow is unconditional by definition.
|
|
417
|
+
if (taken === undefined && defaultFlowId !== undefined) {
|
|
418
|
+
taken = ctx.flows.get(defaultFlowId);
|
|
419
|
+
}
|
|
420
|
+
if (taken === undefined) {
|
|
421
|
+
const error = outflows.length === 0
|
|
422
|
+
? `Gateway "${el.id}" has no outgoing flows`
|
|
423
|
+
: `No condition matched at gateway "${el.id}" — add conditions to flows or mark one as the default`;
|
|
424
|
+
this.emit({ type: "element:failed", elementId: el.id, error });
|
|
425
|
+
this._state = "failed";
|
|
426
|
+
this._error = error;
|
|
427
|
+
this.emit({ type: "process:failed", error });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
await this.complete(token, ctx, [taken]);
|
|
431
|
+
}
|
|
432
|
+
async handleInclusiveGateway(token, el, ctx) {
|
|
433
|
+
const outflows = ctx.outgoing.get(el.id) ?? [];
|
|
434
|
+
const defaultFlowId = el.default;
|
|
435
|
+
const matching = outflows.filter((f) => {
|
|
436
|
+
if (f.id === defaultFlowId)
|
|
437
|
+
return false;
|
|
438
|
+
if (f.conditionExpression === undefined)
|
|
439
|
+
return true;
|
|
440
|
+
return this.evalCondition(f.conditionExpression.text, ctx.scopeId, {
|
|
441
|
+
elementId: el.id,
|
|
442
|
+
property: `flow:${f.id}`,
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
const flows = matching.length > 0
|
|
446
|
+
? matching
|
|
447
|
+
: defaultFlowId !== undefined
|
|
448
|
+
? [ctx.flows.get(defaultFlowId)].filter((f) => f !== undefined)
|
|
449
|
+
: [];
|
|
450
|
+
await this.complete(token, ctx, flows);
|
|
451
|
+
}
|
|
452
|
+
async handleIntermediateCatchEvent(token, el, ctx) {
|
|
453
|
+
const eventDef = el.eventDefinitions[0];
|
|
454
|
+
if (eventDef?.type === "timer") {
|
|
455
|
+
if (this.beforeComplete === undefined) {
|
|
456
|
+
// Normal mode: honour the real timer duration.
|
|
457
|
+
await new Promise((resolve) => {
|
|
458
|
+
const cancel = scheduleTimer(eventDef, resolve);
|
|
459
|
+
this.timerCancels.set(token.id, cancel);
|
|
460
|
+
});
|
|
461
|
+
this.timerCancels.delete(token.id);
|
|
462
|
+
}
|
|
463
|
+
// Controlled mode (step / auto-play): skip the real wait — the
|
|
464
|
+
// beforeComplete hook in complete() is the user-visible pause point.
|
|
465
|
+
}
|
|
466
|
+
else if (eventDef?.type === "message") {
|
|
467
|
+
const msgRef = eventDef.messageRef ?? el.id;
|
|
468
|
+
await new Promise((resolve) => {
|
|
469
|
+
this.messageSubscriptions.set(msgRef, resolve);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
await this.complete(token, ctx);
|
|
473
|
+
}
|
|
474
|
+
async handleSubProcess(token, el, parentCtx) {
|
|
475
|
+
const childScopeId = `scope_sub_${token.id}`;
|
|
476
|
+
this.variables.createScope(childScopeId, parentCtx.scopeId);
|
|
477
|
+
await new Promise((resolve) => {
|
|
478
|
+
const childCtx = this.buildScopeCtx(childScopeId, parentCtx.scopeId, resolve, el.flowElements, el.sequenceFlows);
|
|
479
|
+
this.scopes.set(childScopeId, childCtx);
|
|
480
|
+
const starts = [...childCtx.elements.values()].filter((e) => e.type === "startEvent" && e.incoming.length === 0);
|
|
481
|
+
if (starts.length === 0) {
|
|
482
|
+
resolve();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
for (const s of starts) {
|
|
486
|
+
void this.activate(s.id, childScopeId, undefined);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
this.scopes.delete(childScopeId);
|
|
490
|
+
this.variables.removeScope(childScopeId);
|
|
491
|
+
await this.complete(token, parentCtx);
|
|
492
|
+
}
|
|
493
|
+
// ── Boundary events ────────────────────────────────────────────────────────
|
|
494
|
+
scheduleBoundaryTimers(elementId, _parentToken, scopeId, ctx) {
|
|
495
|
+
const boundaries = ctx.boundaries.get(elementId) ?? [];
|
|
496
|
+
for (const be of boundaries) {
|
|
497
|
+
const timerDef = be.eventDefinitions.find((d) => d.type === "timer");
|
|
498
|
+
if (timerDef === undefined)
|
|
499
|
+
continue;
|
|
500
|
+
const cancel = scheduleTimer(timerDef, () => {
|
|
501
|
+
this.boundaryTimerCancels.delete(elementId);
|
|
502
|
+
// Remove the parent token
|
|
503
|
+
for (const [, tok] of this.allTokens) {
|
|
504
|
+
if (tok.elementId === elementId) {
|
|
505
|
+
this.removeToken(tok);
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (be.cancelActivity !== false) {
|
|
510
|
+
void this.activate(be.id, scopeId, undefined);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
this.boundaryTimerCancels.set(elementId, cancel);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
cancelBoundaryTimers(elementId) {
|
|
517
|
+
this.boundaryTimerCancels.get(elementId)?.();
|
|
518
|
+
this.boundaryTimerCancels.delete(elementId);
|
|
519
|
+
}
|
|
520
|
+
propagateError(errorCode, ctx) {
|
|
521
|
+
// Search boundary events in the current scope
|
|
522
|
+
for (const [attachedTo, boundaries] of ctx.boundaries) {
|
|
523
|
+
for (const be of boundaries) {
|
|
524
|
+
const errDef = be.eventDefinitions.find((d) => d.type === "error");
|
|
525
|
+
if (errDef === undefined)
|
|
526
|
+
continue;
|
|
527
|
+
if (errDef.errorRef !== undefined && errDef.errorRef !== errorCode)
|
|
528
|
+
continue;
|
|
529
|
+
for (const [, tok] of this.allTokens) {
|
|
530
|
+
if (tok.elementId === attachedTo) {
|
|
531
|
+
this.removeToken(tok);
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
void this.activate(be.id, ctx.scopeId, undefined);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// Propagate to parent scope
|
|
540
|
+
if (ctx.parentScopeId !== undefined) {
|
|
541
|
+
const parentCtx = this.scopes.get(ctx.parentScopeId);
|
|
542
|
+
if (parentCtx !== undefined) {
|
|
543
|
+
this.propagateError(errorCode, parentCtx);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
this._state = "failed";
|
|
548
|
+
this._error = errorCode;
|
|
549
|
+
this.emit({ type: "process:failed", error: errorCode });
|
|
550
|
+
}
|
|
551
|
+
// ── Complete ───────────────────────────────────────────────────────────────
|
|
552
|
+
async complete(token, ctx, forcedFlows) {
|
|
553
|
+
if (this._state !== "active")
|
|
554
|
+
return;
|
|
555
|
+
const el = ctx.elements.get(token.elementId);
|
|
556
|
+
if (el === undefined)
|
|
557
|
+
return;
|
|
558
|
+
const ext = parseZeebeExt(el.extensionElements);
|
|
559
|
+
// Apply ioMapping outputs
|
|
560
|
+
if (ext.ioMapping) {
|
|
561
|
+
for (const out of ext.ioMapping.outputs) {
|
|
562
|
+
const val = this.evalFeel(out.source, ctx.scopeId, {
|
|
563
|
+
elementId: el.id,
|
|
564
|
+
property: `output:${out.target}`,
|
|
565
|
+
});
|
|
566
|
+
this.variables.set(ctx.scopeId, out.target, val);
|
|
567
|
+
this.emit({ type: "variable:set", name: out.target, value: val, scopeId: ctx.scopeId });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (this.beforeComplete !== undefined) {
|
|
571
|
+
await this.beforeComplete(token.elementId);
|
|
572
|
+
if (this._state !== "active")
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
this.emit({
|
|
576
|
+
type: "element:leaving",
|
|
577
|
+
elementId: el.id,
|
|
578
|
+
elementName: el.name,
|
|
579
|
+
elementType: el.type,
|
|
580
|
+
});
|
|
581
|
+
this.cancelBoundaryTimers(el.id);
|
|
582
|
+
this.removeToken(token);
|
|
583
|
+
this.emit({
|
|
584
|
+
type: "element:left",
|
|
585
|
+
elementId: el.id,
|
|
586
|
+
elementName: el.name,
|
|
587
|
+
elementType: el.type,
|
|
588
|
+
});
|
|
589
|
+
const flows = forcedFlows ?? this.getOutgoingFlows(el.id, ctx);
|
|
590
|
+
if (flows.length === 0) {
|
|
591
|
+
// Sub-process scope: notify parent when scope tokens are exhausted
|
|
592
|
+
if (ctx.tokens.size === 0 && ctx.onComplete !== undefined) {
|
|
593
|
+
ctx.onComplete();
|
|
594
|
+
}
|
|
595
|
+
else if (ctx.tokens.size === 0 && ctx.parentScopeId === undefined) {
|
|
596
|
+
this.finishProcess();
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
await Promise.all(flows.map((f) => this.activate(f.targetRef, ctx.scopeId, f.id)));
|
|
601
|
+
}
|
|
602
|
+
getOutgoingFlows(elementId, ctx) {
|
|
603
|
+
// Condition expressions are only meaningful on exclusive/inclusive gateway outgoing flows
|
|
604
|
+
// (handled by their dedicated handlers). For all other elements, take every outgoing flow.
|
|
605
|
+
return ctx.outgoing.get(elementId) ?? [];
|
|
606
|
+
}
|
|
607
|
+
finishProcess() {
|
|
608
|
+
if (this._state !== "active")
|
|
609
|
+
return;
|
|
610
|
+
this._state = "completed";
|
|
611
|
+
this.emit({
|
|
612
|
+
type: "process:completed",
|
|
613
|
+
variables: this.variables.getAll(this.rootScopeId),
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
cancelAllTimers() {
|
|
617
|
+
for (const cancel of this.timerCancels.values())
|
|
618
|
+
cancel();
|
|
619
|
+
this.timerCancels.clear();
|
|
620
|
+
for (const cancel of this.boundaryTimerCancels.values())
|
|
621
|
+
cancel();
|
|
622
|
+
this.boundaryTimerCancels.clear();
|
|
623
|
+
}
|
|
624
|
+
// ── FEEL helpers ───────────────────────────────────────────────────────────
|
|
625
|
+
evalFeel(expr, scopeId, emitCtx) {
|
|
626
|
+
const vars = this.variables.getAll(scopeId);
|
|
627
|
+
// Strip Camunda FEEL prefix ("= expr") — the leading "=" is a type indicator, not part of the expression.
|
|
628
|
+
const normalized = expr.trim().replace(/^=\s*/, "");
|
|
629
|
+
const parsed = parseExpression(normalized);
|
|
630
|
+
if (parsed.ast === null)
|
|
631
|
+
return undefined;
|
|
632
|
+
const result = evaluate(parsed.ast, { vars: vars });
|
|
633
|
+
if (emitCtx !== undefined) {
|
|
634
|
+
this.emit({
|
|
635
|
+
type: "feel:evaluated",
|
|
636
|
+
elementId: emitCtx.elementId,
|
|
637
|
+
property: emitCtx.property,
|
|
638
|
+
expression: expr.trim(),
|
|
639
|
+
result,
|
|
640
|
+
variables: { ...vars },
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
return result;
|
|
644
|
+
}
|
|
645
|
+
evalCondition(expr, scopeId, emitCtx) {
|
|
646
|
+
return this.evalFeel(expr, scopeId, emitCtx) === true;
|
|
647
|
+
}
|
|
648
|
+
// ── Emit ───────────────────────────────────────────────────────────────────
|
|
649
|
+
emit(event) {
|
|
650
|
+
for (const listener of this.listeners)
|
|
651
|
+
listener(event);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
//# sourceMappingURL=instance.js.map
|
package/dist/timers.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BpmnTimerEventDefinition } from "@bpmnkit/core";
|
|
2
|
+
/**
|
|
3
|
+
* Schedule a timer from a BPMN timer event definition.
|
|
4
|
+
* Supports ISO 8601 durations (PT2M), dates (2025-06-01T00:00:00Z),
|
|
5
|
+
* and cycles (R3/PT5S — fires N times or indefinitely when R/...).
|
|
6
|
+
* Returns a cancel function.
|
|
7
|
+
*/
|
|
8
|
+
export declare function scheduleTimer(def: BpmnTimerEventDefinition, callback: () => void): () => void;
|
|
9
|
+
/**
|
|
10
|
+
* Parse an ISO 8601 duration string into milliseconds.
|
|
11
|
+
* Handles: PT#S, PT#M, PT#H, P#D, P#W, and combinations.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseDurationMs(duration: string): number;
|
|
14
|
+
//# sourceMappingURL=timers.d.ts.map
|