@bpmnkit/core 0.0.24 → 0.0.26
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/bpmn/bpmn-builder.d.ts +88 -13
- package/dist/bpmn/bpmn-builder.js +376 -74
- package/dist/bpmn/bpmn-model.d.ts +4 -0
- package/dist/bpmn/bpmn-parser.js +25 -4
- package/dist/bpmn/bpmn-serializer.js +12 -2
- package/dist/layout/layout-engine.js +114 -0
- package/package.json +1 -1
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import type { BpmnAssociation, BpmnDefinitions, BpmnFlowElement, BpmnSequenceFlow, BpmnTextAnnotation } from "./bpmn-model.js";
|
|
1
|
+
import type { BpmnAssociation, BpmnDefinitions, BpmnError, BpmnEscalation, BpmnFlowElement, BpmnMessage, BpmnSequenceFlow, BpmnSignal, BpmnTextAnnotation } from "./bpmn-model.js";
|
|
2
2
|
import type { RestConnectorConfig } from "./rest-connector.js";
|
|
3
3
|
/** Options shared by all element methods. */
|
|
4
4
|
export interface ElementOptions {
|
|
5
5
|
name?: string;
|
|
6
|
+
isForCompensation?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Options for send/receive task elements. */
|
|
9
|
+
export interface MessageTaskOptions extends ElementOptions {
|
|
10
|
+
/** Message name — generates or reuses a root <bpmn:message> and sets messageRef. */
|
|
11
|
+
messageName?: string;
|
|
6
12
|
}
|
|
7
13
|
/** Options for creating a start event. */
|
|
8
14
|
export interface StartEventOptions extends ElementOptions {
|
|
@@ -25,6 +31,11 @@ export interface StartEventOptions extends ElementOptions {
|
|
|
25
31
|
modelerTemplateVersion?: string;
|
|
26
32
|
/** Zeebe modeler template icon (data URI). */
|
|
27
33
|
modelerTemplateIcon?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Non-interrupting flag — only meaningful for start events inside event sub-processes.
|
|
36
|
+
* Pass `false` to emit `isInterrupting="false"`. Omit for the default interrupting behavior.
|
|
37
|
+
*/
|
|
38
|
+
isInterrupting?: boolean;
|
|
28
39
|
}
|
|
29
40
|
/** Options for creating a service task. */
|
|
30
41
|
export interface ServiceTaskOptions {
|
|
@@ -53,6 +64,8 @@ export interface ServiceTaskOptions {
|
|
|
53
64
|
modelerTemplateVersion?: string;
|
|
54
65
|
/** Zeebe modeler template icon (data URI). */
|
|
55
66
|
modelerTemplateIcon?: string;
|
|
67
|
+
/** Mark this task as a compensation handler. */
|
|
68
|
+
isForCompensation?: boolean;
|
|
56
69
|
}
|
|
57
70
|
/** Options for creating a script task. */
|
|
58
71
|
export interface ScriptTaskOptions {
|
|
@@ -62,6 +75,8 @@ export interface ScriptTaskOptions {
|
|
|
62
75
|
expression: string;
|
|
63
76
|
/** Variable name to store the result. */
|
|
64
77
|
resultVariable: string;
|
|
78
|
+
/** Mark this task as a compensation handler. */
|
|
79
|
+
isForCompensation?: boolean;
|
|
65
80
|
}
|
|
66
81
|
/** Options for creating a user task. */
|
|
67
82
|
export interface UserTaskOptions {
|
|
@@ -71,6 +86,8 @@ export interface UserTaskOptions {
|
|
|
71
86
|
formId?: string;
|
|
72
87
|
/** Emit <zeebe:userTask /> to mark as a Camunda 8 native user task. */
|
|
73
88
|
zeebeUserTask?: boolean;
|
|
89
|
+
/** Mark this task as a compensation handler. */
|
|
90
|
+
isForCompensation?: boolean;
|
|
74
91
|
}
|
|
75
92
|
/** Options for creating a call activity. */
|
|
76
93
|
export interface CallActivityOptions {
|
|
@@ -80,6 +97,8 @@ export interface CallActivityOptions {
|
|
|
80
97
|
processId: string;
|
|
81
98
|
/** Whether to propagate all child variables. */
|
|
82
99
|
propagateAllChildVariables?: boolean;
|
|
100
|
+
/** Mark this activity as a compensation handler. */
|
|
101
|
+
isForCompensation?: boolean;
|
|
83
102
|
}
|
|
84
103
|
/** Options for creating a business rule task. */
|
|
85
104
|
export interface BusinessRuleTaskOptions {
|
|
@@ -91,6 +110,8 @@ export interface BusinessRuleTaskOptions {
|
|
|
91
110
|
decisionId?: string;
|
|
92
111
|
/** Variable to store the result. */
|
|
93
112
|
resultVariable?: string;
|
|
113
|
+
/** Mark this task as a compensation handler. */
|
|
114
|
+
isForCompensation?: boolean;
|
|
94
115
|
}
|
|
95
116
|
/** Options for gateway elements. */
|
|
96
117
|
export interface GatewayOptions extends ElementOptions {
|
|
@@ -118,12 +139,16 @@ export interface IntermediateThrowEventOptions extends ElementOptions {
|
|
|
118
139
|
signalName?: string;
|
|
119
140
|
/** Escalation code — creates an escalation throw event (aspirational). */
|
|
120
141
|
escalationCode?: string;
|
|
142
|
+
/** Emit a compensateEventDefinition. */
|
|
143
|
+
compensation?: boolean;
|
|
144
|
+
/** Activity to compensate (activityRef attribute on compensateEventDefinition). */
|
|
145
|
+
activityRef?: string;
|
|
121
146
|
}
|
|
122
147
|
/** Options for an end event. */
|
|
123
148
|
export interface EndEventOptions extends ElementOptions {
|
|
124
149
|
/** Error code — creates an error end event. */
|
|
125
150
|
errorCode?: string;
|
|
126
|
-
/** Error
|
|
151
|
+
/** Error code — creates an error end event. Alias for errorCode. */
|
|
127
152
|
errorRef?: string;
|
|
128
153
|
/** Message name — creates a message end event. */
|
|
129
154
|
messageName?: string;
|
|
@@ -140,7 +165,7 @@ export interface BoundaryEventOptions extends ElementOptions {
|
|
|
140
165
|
cancelActivity?: boolean;
|
|
141
166
|
/** Error code — creates an error boundary event. */
|
|
142
167
|
errorCode?: string;
|
|
143
|
-
/** Error
|
|
168
|
+
/** Error code — creates an error boundary event. Alias for errorCode. */
|
|
144
169
|
errorRef?: string;
|
|
145
170
|
/** Timer duration — creates a timer boundary event (aspirational). */
|
|
146
171
|
timerDuration?: string;
|
|
@@ -152,6 +177,8 @@ export interface BoundaryEventOptions extends ElementOptions {
|
|
|
152
177
|
messageName?: string;
|
|
153
178
|
/** Signal name — creates a signal boundary event (aspirational). */
|
|
154
179
|
signalName?: string;
|
|
180
|
+
/** Creates a compensation boundary event. */
|
|
181
|
+
compensation?: boolean;
|
|
155
182
|
}
|
|
156
183
|
/** Multi-instance loop configuration. */
|
|
157
184
|
export interface MultiInstanceOptions {
|
|
@@ -230,9 +257,17 @@ export declare class BranchBuilder {
|
|
|
230
257
|
readonly _textAnnotations: BpmnTextAnnotation[];
|
|
231
258
|
/** @internal */
|
|
232
259
|
readonly _associations: BpmnAssociation[];
|
|
260
|
+
/** @internal – ID of the last gateway added in this branch (for nested branch() support). */
|
|
261
|
+
private currentGatewayId;
|
|
262
|
+
/** @internal – Open ends of nested branches waiting to auto-connect to the next element. */
|
|
263
|
+
private openBranchEnds;
|
|
233
264
|
private readonly _annCounters;
|
|
265
|
+
private readonly rootErrors;
|
|
266
|
+
private readonly rootMessages;
|
|
267
|
+
private readonly rootSignals;
|
|
268
|
+
private readonly rootEscalations;
|
|
234
269
|
/** @internal */
|
|
235
|
-
constructor(gatewayId: string, branchName: string);
|
|
270
|
+
constructor(gatewayId: string, branchName: string, rootErrors?: BpmnError[], rootMessages?: BpmnMessage[], rootSignals?: BpmnSignal[], rootEscalations?: BpmnEscalation[]);
|
|
236
271
|
/** Set a FEEL condition expression on this branch's outgoing sequence flow. */
|
|
237
272
|
condition(expression: string): this;
|
|
238
273
|
/** Mark this branch as the gateway's default (no-condition) flow. */
|
|
@@ -243,8 +278,10 @@ export declare class BranchBuilder {
|
|
|
243
278
|
* Supports forward references (element created later) and backward references (loops).
|
|
244
279
|
*/
|
|
245
280
|
connectTo(targetId: string): this;
|
|
246
|
-
/** @internal – ID of the last element added (or
|
|
247
|
-
get _lastNodeId(): string;
|
|
281
|
+
/** @internal – ID of the last element added (or undefined if branches are open). */
|
|
282
|
+
get _lastNodeId(): string | undefined;
|
|
283
|
+
/** @internal – Open ends of nested branches that have not yet been connected. */
|
|
284
|
+
get _openBranchEnds(): string[];
|
|
248
285
|
/** Attach a text annotation to the element at the current cursor position. */
|
|
249
286
|
textAnnotation(text: string): this;
|
|
250
287
|
/** Attach a text annotation to an element by explicit ID. */
|
|
@@ -252,10 +289,12 @@ export declare class BranchBuilder {
|
|
|
252
289
|
serviceTask(id: string, options: ServiceTaskOptions): this;
|
|
253
290
|
userTask(id: string, options?: UserTaskOptions): this;
|
|
254
291
|
scriptTask(id: string, options: ScriptTaskOptions): this;
|
|
255
|
-
sendTask(id: string, options?:
|
|
256
|
-
receiveTask(id: string, options?:
|
|
292
|
+
sendTask(id: string, options?: MessageTaskOptions): this;
|
|
293
|
+
receiveTask(id: string, options?: MessageTaskOptions): this;
|
|
257
294
|
businessRuleTask(id: string, options?: BusinessRuleTaskOptions): this;
|
|
258
295
|
callActivity(id: string, options: CallActivityOptions): this;
|
|
296
|
+
/** Add an abstract task with no Zeebe extensions. */
|
|
297
|
+
task(id: string, options?: ElementOptions): this;
|
|
259
298
|
startEvent(id?: string, options?: StartEventOptions): this;
|
|
260
299
|
endEvent(id?: string, options?: EndEventOptions): this;
|
|
261
300
|
intermediateThrowEvent(id?: string, options?: IntermediateThrowEventOptions): this;
|
|
@@ -264,6 +303,32 @@ export declare class BranchBuilder {
|
|
|
264
303
|
parallelGateway(id: string, options?: ElementOptions): this;
|
|
265
304
|
inclusiveGateway(id: string, options?: GatewayOptions): this;
|
|
266
305
|
eventBasedGateway(id: string, options?: ElementOptions): this;
|
|
306
|
+
/**
|
|
307
|
+
* Add a boundary event attached to an existing activity in this branch.
|
|
308
|
+
*
|
|
309
|
+
* The boundary event is NOT connected by a sequence flow — it attaches via
|
|
310
|
+
* `attachedToRef`. The builder cursor advances to the boundary event so
|
|
311
|
+
* subsequent elements chain from it. Use `withBoundary()` if you want the
|
|
312
|
+
* cursor to return to the task afterward.
|
|
313
|
+
*/
|
|
314
|
+
boundaryEvent(id: string, options: BoundaryEventOptions): this;
|
|
315
|
+
/**
|
|
316
|
+
* Attach a boundary event to the preceding task and build its outgoing path,
|
|
317
|
+
* then restore the branch cursor to the preceding task so the main branch flow continues.
|
|
318
|
+
*
|
|
319
|
+
* @param id - ID for the boundary event element.
|
|
320
|
+
* @param options - Boundary event options (without `attachedTo` — inferred from cursor).
|
|
321
|
+
* @param handler - Callback that chains elements from the boundary event.
|
|
322
|
+
*/
|
|
323
|
+
withBoundary(id: string, options: Omit<BoundaryEventOptions, "attachedTo">, handler: (b: BranchBuilder) => void): this;
|
|
324
|
+
/**
|
|
325
|
+
* Create a named branch from the last gateway added inside this branch.
|
|
326
|
+
*
|
|
327
|
+
* Works identically to the top-level `ProcessBuilder.branch()` — use
|
|
328
|
+
* `.condition(expr)` or `.defaultFlow()` inside the callback, finish with
|
|
329
|
+
* `.connectTo(id)` or an `.endEvent()` to terminate the nested branch.
|
|
330
|
+
*/
|
|
331
|
+
branch(name: string, callback: (b: BranchBuilder) => void): this;
|
|
267
332
|
}
|
|
268
333
|
/** Builder for the contents of a sub-process or ad-hoc sub-process. */
|
|
269
334
|
export declare class SubProcessContentBuilder {
|
|
@@ -279,6 +344,9 @@ export declare class SubProcessContentBuilder {
|
|
|
279
344
|
private lastNodeId;
|
|
280
345
|
private currentGatewayId;
|
|
281
346
|
private openBranchEnds;
|
|
347
|
+
private readonly rootMessages;
|
|
348
|
+
/** @internal */
|
|
349
|
+
constructor(rootMessages?: BpmnMessage[]);
|
|
282
350
|
private addElement;
|
|
283
351
|
startEvent(id?: string, options?: StartEventOptions): this;
|
|
284
352
|
endEvent(id?: string, options?: EndEventOptions): this;
|
|
@@ -289,8 +357,10 @@ export declare class SubProcessContentBuilder {
|
|
|
289
357
|
userTask(id: string, options?: UserTaskOptions): this;
|
|
290
358
|
businessRuleTask(id: string, options?: BusinessRuleTaskOptions): this;
|
|
291
359
|
callActivity(id: string, options: CallActivityOptions): this;
|
|
292
|
-
sendTask(id: string, options?:
|
|
293
|
-
receiveTask(id: string, options?:
|
|
360
|
+
sendTask(id: string, options?: MessageTaskOptions): this;
|
|
361
|
+
receiveTask(id: string, options?: MessageTaskOptions): this;
|
|
362
|
+
/** Add an abstract task with no Zeebe extensions. */
|
|
363
|
+
task(id: string, options?: ElementOptions): this;
|
|
294
364
|
exclusiveGateway(id: string, options?: GatewayOptions): this;
|
|
295
365
|
parallelGateway(id: string, options?: ElementOptions): this;
|
|
296
366
|
inclusiveGateway(id: string, options?: GatewayOptions): this;
|
|
@@ -313,6 +383,8 @@ export declare class ProcessBuilder {
|
|
|
313
383
|
private readonly sequenceFlows;
|
|
314
384
|
private readonly rootErrors;
|
|
315
385
|
private readonly rootMessages;
|
|
386
|
+
private readonly rootSignals;
|
|
387
|
+
private readonly rootEscalations;
|
|
316
388
|
private readonly _textAnnotations;
|
|
317
389
|
private readonly _associations;
|
|
318
390
|
private readonly _annCounters;
|
|
@@ -322,6 +394,7 @@ export declare class ProcessBuilder {
|
|
|
322
394
|
private _autoLayout;
|
|
323
395
|
private _executionPlatformVersion;
|
|
324
396
|
private _serviceTaskDefaults;
|
|
397
|
+
private _savedMainFlowId;
|
|
325
398
|
constructor(processId: string);
|
|
326
399
|
/** Enable auto-layout: `build()` will run the layout engine and populate diagram interchange data. */
|
|
327
400
|
withAutoLayout(): this;
|
|
@@ -386,13 +459,15 @@ export declare class ProcessBuilder {
|
|
|
386
459
|
/** Add a user task with optional form reference. */
|
|
387
460
|
userTask(id: string, options?: UserTaskOptions): this;
|
|
388
461
|
/** Add a send task (aspirational). */
|
|
389
|
-
sendTask(id: string, options?:
|
|
462
|
+
sendTask(id: string, options?: MessageTaskOptions): this;
|
|
390
463
|
/** Add a receive task (aspirational). */
|
|
391
|
-
receiveTask(id: string, options?:
|
|
464
|
+
receiveTask(id: string, options?: MessageTaskOptions): this;
|
|
392
465
|
/** Add a business rule task. */
|
|
393
466
|
businessRuleTask(id: string, options?: BusinessRuleTaskOptions): this;
|
|
394
467
|
/** Add a call activity referencing another process. */
|
|
395
468
|
callActivity(id: string, options: CallActivityOptions): this;
|
|
469
|
+
/** Add an abstract task with no Zeebe extensions. */
|
|
470
|
+
task(id: string, options?: ElementOptions): this;
|
|
396
471
|
/** Add an exclusive gateway (XOR split/join). */
|
|
397
472
|
exclusiveGateway(id: string, options?: GatewayOptions): this;
|
|
398
473
|
/** Add a parallel gateway (AND split/join). */
|
|
@@ -432,7 +507,7 @@ export declare class ProcessBuilder {
|
|
|
432
507
|
adHocSubProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: AdHocSubProcessOptions): this;
|
|
433
508
|
/** Add a sub-process (aspirational). */
|
|
434
509
|
subProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: SubProcessOptions): this;
|
|
435
|
-
/** Add an event sub-process
|
|
510
|
+
/** Add an event sub-process. Triggered by its start event — no incoming or outgoing sequence flows. */
|
|
436
511
|
eventSubProcess(id: string, content: (b: SubProcessContentBuilder) => void, options?: ElementOptions): this;
|
|
437
512
|
/** Attach a text annotation to the element at the current cursor position. */
|
|
438
513
|
textAnnotation(text: string): this;
|
|
@@ -7,7 +7,15 @@ const EXPORTER_VERSION = "0.0.23";
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// Internal helpers
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
-
function
|
|
10
|
+
function resolveMessage(messageName, rootMessages) {
|
|
11
|
+
let existing = rootMessages.find((m) => m.name === messageName);
|
|
12
|
+
if (!existing) {
|
|
13
|
+
existing = { id: generateId("Message"), name: messageName, unknownAttributes: {} };
|
|
14
|
+
rootMessages.push(existing);
|
|
15
|
+
}
|
|
16
|
+
return existing.id;
|
|
17
|
+
}
|
|
18
|
+
function buildEventDefinitions(opts, rootErrors, rootMessages, rootSignals, rootEscalations) {
|
|
11
19
|
const defs = [];
|
|
12
20
|
if (opts.timerDuration || opts.timerDate || opts.timerCycle) {
|
|
13
21
|
defs.push({
|
|
@@ -18,28 +26,57 @@ function buildEventDefinitions(opts, rootErrors, rootMessages) {
|
|
|
18
26
|
});
|
|
19
27
|
}
|
|
20
28
|
if (opts.errorCode !== undefined || opts.errorRef !== undefined) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
rootErrors.
|
|
25
|
-
|
|
29
|
+
const codeOrRef = opts.errorCode ?? opts.errorRef;
|
|
30
|
+
let errorRef;
|
|
31
|
+
if (codeOrRef !== undefined && rootErrors) {
|
|
32
|
+
let existing = rootErrors.find((e) => e.errorCode === codeOrRef || e.name === codeOrRef);
|
|
33
|
+
if (!existing) {
|
|
34
|
+
existing = { id: generateId("Error"), name: codeOrRef, errorCode: codeOrRef };
|
|
35
|
+
rootErrors.push(existing);
|
|
36
|
+
}
|
|
37
|
+
errorRef = existing.id;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
errorRef = codeOrRef;
|
|
26
41
|
}
|
|
27
42
|
defs.push({ type: "error", errorRef });
|
|
28
43
|
}
|
|
29
44
|
if (opts.messageName !== undefined) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
rootMessages.push({ id: messageId, name: opts.messageName, unknownAttributes: {} });
|
|
34
|
-
messageRef = messageId;
|
|
35
|
-
}
|
|
45
|
+
const messageRef = rootMessages
|
|
46
|
+
? resolveMessage(opts.messageName, rootMessages)
|
|
47
|
+
: opts.messageName;
|
|
36
48
|
defs.push({ type: "message", messageRef });
|
|
37
49
|
}
|
|
38
50
|
if (opts.signalName !== undefined) {
|
|
39
|
-
|
|
51
|
+
let signalRef = opts.signalName;
|
|
52
|
+
if (rootSignals) {
|
|
53
|
+
let existing = rootSignals.find((s) => s.name === opts.signalName);
|
|
54
|
+
if (!existing) {
|
|
55
|
+
existing = { id: generateId("Signal"), name: opts.signalName };
|
|
56
|
+
rootSignals.push(existing);
|
|
57
|
+
}
|
|
58
|
+
signalRef = existing.id;
|
|
59
|
+
}
|
|
60
|
+
defs.push({ type: "signal", signalRef });
|
|
40
61
|
}
|
|
41
62
|
if (opts.escalationCode !== undefined) {
|
|
42
|
-
|
|
63
|
+
let escalationRef = opts.escalationCode;
|
|
64
|
+
if (rootEscalations) {
|
|
65
|
+
let existing = rootEscalations.find((e) => e.escalationCode === opts.escalationCode);
|
|
66
|
+
if (!existing) {
|
|
67
|
+
existing = {
|
|
68
|
+
id: generateId("Escalation"),
|
|
69
|
+
name: opts.escalationCode,
|
|
70
|
+
escalationCode: opts.escalationCode,
|
|
71
|
+
};
|
|
72
|
+
rootEscalations.push(existing);
|
|
73
|
+
}
|
|
74
|
+
escalationRef = existing.id;
|
|
75
|
+
}
|
|
76
|
+
defs.push({ type: "escalation", escalationRef });
|
|
77
|
+
}
|
|
78
|
+
if (opts.compensation) {
|
|
79
|
+
defs.push({ type: "compensate", activityRef: opts.activityRef });
|
|
43
80
|
}
|
|
44
81
|
return defs;
|
|
45
82
|
}
|
|
@@ -213,10 +250,12 @@ function makeServiceTaskEl(id, options) {
|
|
|
213
250
|
extensionElements: buildServiceTaskExtensions(options),
|
|
214
251
|
});
|
|
215
252
|
el.unknownAttributes = unknownAttributes;
|
|
253
|
+
if (options.isForCompensation)
|
|
254
|
+
el.isForCompensation = true;
|
|
216
255
|
return el;
|
|
217
256
|
}
|
|
218
257
|
function makeScriptTaskEl(id, options) {
|
|
219
|
-
|
|
258
|
+
const el = makeFlowElement(id, "scriptTask", {
|
|
220
259
|
name: options.name,
|
|
221
260
|
extensionElements: zeebeExtensionsToXmlElements({
|
|
222
261
|
unknownElements: [
|
|
@@ -228,13 +267,19 @@ function makeScriptTaskEl(id, options) {
|
|
|
228
267
|
],
|
|
229
268
|
}),
|
|
230
269
|
});
|
|
270
|
+
if (options.isForCompensation)
|
|
271
|
+
el.isForCompensation = true;
|
|
272
|
+
return el;
|
|
231
273
|
}
|
|
232
274
|
function makeUserTaskEl(id, options) {
|
|
233
275
|
const ext = zeebeExtensionsToXmlElements({
|
|
234
276
|
...(options?.zeebeUserTask ? { userTask: true } : {}),
|
|
235
277
|
...(options?.formId ? { formDefinition: { formId: options.formId } } : {}),
|
|
236
278
|
});
|
|
237
|
-
|
|
279
|
+
const el = makeFlowElement(id, "userTask", { name: options?.name, extensionElements: ext });
|
|
280
|
+
if (options?.isForCompensation)
|
|
281
|
+
el.isForCompensation = true;
|
|
282
|
+
return el;
|
|
238
283
|
}
|
|
239
284
|
function makeBusinessRuleTaskEl(id, options) {
|
|
240
285
|
const ext = [];
|
|
@@ -249,19 +294,28 @@ function makeBusinessRuleTaskEl(id, options) {
|
|
|
249
294
|
},
|
|
250
295
|
}));
|
|
251
296
|
}
|
|
252
|
-
|
|
297
|
+
const el = makeFlowElement(id, "businessRuleTask", {
|
|
298
|
+
name: options?.name,
|
|
299
|
+
extensionElements: ext,
|
|
300
|
+
});
|
|
301
|
+
if (options?.isForCompensation)
|
|
302
|
+
el.isForCompensation = true;
|
|
303
|
+
return el;
|
|
253
304
|
}
|
|
254
305
|
function makeCallActivityEl(id, options) {
|
|
255
306
|
const attrs = { processId: options.processId };
|
|
256
307
|
if (options.propagateAllChildVariables !== undefined) {
|
|
257
308
|
attrs.propagateAllChildVariables = String(options.propagateAllChildVariables);
|
|
258
309
|
}
|
|
259
|
-
|
|
310
|
+
const el = makeFlowElement(id, "callActivity", {
|
|
260
311
|
name: options.name,
|
|
261
312
|
extensionElements: zeebeExtensionsToXmlElements({
|
|
262
313
|
unknownElements: [{ name: "zeebe:calledElement", attributes: attrs, children: [] }],
|
|
263
314
|
}),
|
|
264
315
|
});
|
|
316
|
+
if (options.isForCompensation)
|
|
317
|
+
el.isForCompensation = true;
|
|
318
|
+
return el;
|
|
265
319
|
}
|
|
266
320
|
function makeExclusiveGatewayEl(id, options) {
|
|
267
321
|
const el = makeFlowElement(id, "exclusiveGateway", options);
|
|
@@ -332,15 +386,17 @@ function insertJoinGateways(elements, flows) {
|
|
|
332
386
|
const gwType = elementTypes.get(splitId);
|
|
333
387
|
if (!gwType)
|
|
334
388
|
continue;
|
|
389
|
+
// eventBasedGateway is split-only; converge through an XOR join instead
|
|
390
|
+
const joinType = gwType === "eventBasedGateway" ? "exclusiveGateway" : gwType;
|
|
335
391
|
const targetType = elementTypes.get(targetId);
|
|
336
|
-
if (targetType ===
|
|
392
|
+
if (targetType === joinType)
|
|
337
393
|
continue;
|
|
338
394
|
const joinId = `${splitId}_join`;
|
|
339
395
|
if (elementTypes.has(joinId))
|
|
340
396
|
continue;
|
|
341
|
-
const joinElement = makeFlowElement(joinId,
|
|
397
|
+
const joinElement = makeFlowElement(joinId, joinType, {});
|
|
342
398
|
elements.push(joinElement);
|
|
343
|
-
elementTypes.set(joinId,
|
|
399
|
+
elementTypes.set(joinId, joinType);
|
|
344
400
|
for (const flow of convergingFlows)
|
|
345
401
|
flow.targetRef = joinId;
|
|
346
402
|
flows.push({
|
|
@@ -400,12 +456,24 @@ export class BranchBuilder {
|
|
|
400
456
|
_textAnnotations = [];
|
|
401
457
|
/** @internal */
|
|
402
458
|
_associations = [];
|
|
459
|
+
/** @internal – ID of the last gateway added in this branch (for nested branch() support). */
|
|
460
|
+
currentGatewayId;
|
|
461
|
+
/** @internal – Open ends of nested branches waiting to auto-connect to the next element. */
|
|
462
|
+
openBranchEnds = [];
|
|
403
463
|
_annCounters = new Map();
|
|
464
|
+
rootErrors;
|
|
465
|
+
rootMessages;
|
|
466
|
+
rootSignals;
|
|
467
|
+
rootEscalations;
|
|
404
468
|
/** @internal */
|
|
405
|
-
constructor(gatewayId, branchName) {
|
|
469
|
+
constructor(gatewayId, branchName, rootErrors = [], rootMessages = [], rootSignals = [], rootEscalations = []) {
|
|
406
470
|
this.gatewayId = gatewayId;
|
|
407
471
|
this.branchName = branchName;
|
|
408
472
|
this.lastNodeId = gatewayId;
|
|
473
|
+
this.rootErrors = rootErrors;
|
|
474
|
+
this.rootMessages = rootMessages;
|
|
475
|
+
this.rootSignals = rootSignals;
|
|
476
|
+
this.rootEscalations = rootEscalations;
|
|
409
477
|
}
|
|
410
478
|
/** Set a FEEL condition expression on this branch's outgoing sequence flow. */
|
|
411
479
|
condition(expression) {
|
|
@@ -419,22 +487,34 @@ export class BranchBuilder {
|
|
|
419
487
|
}
|
|
420
488
|
addElement(element) {
|
|
421
489
|
this._elements.push(element);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
this.
|
|
490
|
+
if (this.lastNodeId) {
|
|
491
|
+
const flowId = generateId("Flow");
|
|
492
|
+
const flow = {
|
|
493
|
+
id: flowId,
|
|
494
|
+
sourceRef: this.lastNodeId,
|
|
495
|
+
targetRef: element.id,
|
|
496
|
+
name: this.isFirstElement ? this.branchName : undefined,
|
|
497
|
+
conditionExpression: this.isFirstElement && this.pendingCondition
|
|
498
|
+
? makeConditionExpression(this.pendingCondition)
|
|
499
|
+
: undefined,
|
|
500
|
+
extensionElements: [],
|
|
501
|
+
unknownAttributes: {},
|
|
502
|
+
};
|
|
503
|
+
this._flows.push(flow);
|
|
504
|
+
if (this.isFirstElement && this.pendingDefault) {
|
|
505
|
+
this._defaultFlowId = flowId;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
for (const branchEnd of this.openBranchEnds) {
|
|
509
|
+
this._flows.push({
|
|
510
|
+
id: generateId("Flow"),
|
|
511
|
+
sourceRef: branchEnd,
|
|
512
|
+
targetRef: element.id,
|
|
513
|
+
extensionElements: [],
|
|
514
|
+
unknownAttributes: {},
|
|
515
|
+
});
|
|
437
516
|
}
|
|
517
|
+
this.openBranchEnds = [];
|
|
438
518
|
this.isFirstElement = false;
|
|
439
519
|
this.lastNodeId = element.id;
|
|
440
520
|
return this;
|
|
@@ -447,6 +527,7 @@ export class BranchBuilder {
|
|
|
447
527
|
const flowId = generateId("Flow");
|
|
448
528
|
const flow = {
|
|
449
529
|
id: flowId,
|
|
530
|
+
// biome-ignore lint/style/noNonNullAssertion: lastNodeId starts as gatewayId and is always defined in pre-branch context
|
|
450
531
|
sourceRef: this.lastNodeId,
|
|
451
532
|
targetRef: targetId,
|
|
452
533
|
name: this.isFirstElement ? this.branchName : undefined,
|
|
@@ -465,13 +546,18 @@ export class BranchBuilder {
|
|
|
465
546
|
this._connected = true;
|
|
466
547
|
return this;
|
|
467
548
|
}
|
|
468
|
-
/** @internal – ID of the last element added (or
|
|
549
|
+
/** @internal – ID of the last element added (or undefined if branches are open). */
|
|
469
550
|
get _lastNodeId() {
|
|
470
551
|
return this.lastNodeId;
|
|
471
552
|
}
|
|
553
|
+
/** @internal – Open ends of nested branches that have not yet been connected. */
|
|
554
|
+
get _openBranchEnds() {
|
|
555
|
+
return this.openBranchEnds;
|
|
556
|
+
}
|
|
472
557
|
// ---- Annotations ----
|
|
473
558
|
/** Attach a text annotation to the element at the current cursor position. */
|
|
474
559
|
textAnnotation(text) {
|
|
560
|
+
// biome-ignore lint/style/noNonNullAssertion: lastNodeId starts as gatewayId and is always defined in pre-branch context
|
|
475
561
|
return this.annotate(this.lastNodeId, text);
|
|
476
562
|
}
|
|
477
563
|
/** Attach a text annotation to an element by explicit ID. */
|
|
@@ -500,10 +586,20 @@ export class BranchBuilder {
|
|
|
500
586
|
return this.addElement(makeScriptTaskEl(id, options));
|
|
501
587
|
}
|
|
502
588
|
sendTask(id, options) {
|
|
503
|
-
|
|
589
|
+
const el = makeFlowElement(id, "sendTask", options);
|
|
590
|
+
if (options?.isForCompensation)
|
|
591
|
+
el.isForCompensation = true;
|
|
592
|
+
if (options?.messageName)
|
|
593
|
+
el.messageRef = resolveMessage(options.messageName, this.rootMessages);
|
|
594
|
+
return this.addElement(el);
|
|
504
595
|
}
|
|
505
596
|
receiveTask(id, options) {
|
|
506
|
-
|
|
597
|
+
const el = makeFlowElement(id, "receiveTask", options);
|
|
598
|
+
if (options?.isForCompensation)
|
|
599
|
+
el.isForCompensation = true;
|
|
600
|
+
if (options?.messageName)
|
|
601
|
+
el.messageRef = resolveMessage(options.messageName, this.rootMessages);
|
|
602
|
+
return this.addElement(el);
|
|
507
603
|
}
|
|
508
604
|
businessRuleTask(id, options) {
|
|
509
605
|
return this.addElement(makeBusinessRuleTaskEl(id, options));
|
|
@@ -511,47 +607,157 @@ export class BranchBuilder {
|
|
|
511
607
|
callActivity(id, options) {
|
|
512
608
|
return this.addElement(makeCallActivityEl(id, options));
|
|
513
609
|
}
|
|
610
|
+
/** Add an abstract task with no Zeebe extensions. */
|
|
611
|
+
task(id, options) {
|
|
612
|
+
const el = makeFlowElement(id, "task", options);
|
|
613
|
+
if (options?.isForCompensation)
|
|
614
|
+
el.isForCompensation = true;
|
|
615
|
+
return this.addElement(el);
|
|
616
|
+
}
|
|
514
617
|
startEvent(id, options) {
|
|
515
618
|
const el = makeFlowElement(id ?? generateId("StartEvent"), "startEvent", options);
|
|
516
|
-
if (el.type === "startEvent" &&
|
|
517
|
-
(options
|
|
518
|
-
el.eventDefinitions = buildEventDefinitions(options);
|
|
619
|
+
if (el.type === "startEvent" && options) {
|
|
620
|
+
el.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
|
|
519
621
|
}
|
|
520
622
|
return this.addElement(el);
|
|
521
623
|
}
|
|
522
624
|
endEvent(id, options) {
|
|
523
625
|
const el = makeFlowElement(id ?? generateId("EndEvent"), "endEvent", options);
|
|
524
626
|
if (el.type === "endEvent" && options) {
|
|
525
|
-
el.eventDefinitions = buildEventDefinitions(options);
|
|
627
|
+
el.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
|
|
526
628
|
}
|
|
527
629
|
return this.addElement(el);
|
|
528
630
|
}
|
|
529
631
|
intermediateThrowEvent(id, options) {
|
|
530
632
|
const el = makeFlowElement(id ?? generateId("IntermediateThrowEvent"), "intermediateThrowEvent", options);
|
|
531
633
|
if (el.type === "intermediateThrowEvent" && options) {
|
|
532
|
-
el.eventDefinitions = buildEventDefinitions(options);
|
|
634
|
+
el.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
|
|
533
635
|
}
|
|
534
636
|
return this.addElement(el);
|
|
535
637
|
}
|
|
536
638
|
intermediateCatchEvent(id, options) {
|
|
537
639
|
const el = makeFlowElement(id ?? generateId("IntermediateCatchEvent"), "intermediateCatchEvent", options);
|
|
538
640
|
if (el.type === "intermediateCatchEvent" && options) {
|
|
539
|
-
el.eventDefinitions = buildEventDefinitions(options);
|
|
641
|
+
el.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
|
|
540
642
|
}
|
|
541
643
|
return this.addElement(el);
|
|
542
644
|
}
|
|
543
645
|
exclusiveGateway(id, options) {
|
|
646
|
+
this.currentGatewayId = id;
|
|
544
647
|
return this.addElement(makeExclusiveGatewayEl(id, options));
|
|
545
648
|
}
|
|
546
649
|
parallelGateway(id, options) {
|
|
650
|
+
this.currentGatewayId = id;
|
|
547
651
|
return this.addElement(makeFlowElement(id, "parallelGateway", options));
|
|
548
652
|
}
|
|
549
653
|
inclusiveGateway(id, options) {
|
|
654
|
+
this.currentGatewayId = id;
|
|
550
655
|
return this.addElement(makeInclusiveGatewayEl(id, options));
|
|
551
656
|
}
|
|
552
657
|
eventBasedGateway(id, options) {
|
|
658
|
+
this.currentGatewayId = id;
|
|
553
659
|
return this.addElement(makeFlowElement(id, "eventBasedGateway", options));
|
|
554
660
|
}
|
|
661
|
+
/**
|
|
662
|
+
* Add a boundary event attached to an existing activity in this branch.
|
|
663
|
+
*
|
|
664
|
+
* The boundary event is NOT connected by a sequence flow — it attaches via
|
|
665
|
+
* `attachedToRef`. The builder cursor advances to the boundary event so
|
|
666
|
+
* subsequent elements chain from it. Use `withBoundary()` if you want the
|
|
667
|
+
* cursor to return to the task afterward.
|
|
668
|
+
*/
|
|
669
|
+
boundaryEvent(id, options) {
|
|
670
|
+
const element = makeFlowElement(id, "boundaryEvent", options);
|
|
671
|
+
if (element.type === "boundaryEvent") {
|
|
672
|
+
element.attachedToRef = options.attachedTo;
|
|
673
|
+
element.cancelActivity = options.cancelActivity;
|
|
674
|
+
element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
|
|
675
|
+
}
|
|
676
|
+
// Push directly — no sequence flow, boundary events attach via attachedToRef
|
|
677
|
+
this._elements.push(element);
|
|
678
|
+
this.lastNodeId = element.id;
|
|
679
|
+
this.isFirstElement = false;
|
|
680
|
+
return this;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Attach a boundary event to the preceding task and build its outgoing path,
|
|
684
|
+
* then restore the branch cursor to the preceding task so the main branch flow continues.
|
|
685
|
+
*
|
|
686
|
+
* @param id - ID for the boundary event element.
|
|
687
|
+
* @param options - Boundary event options (without `attachedTo` — inferred from cursor).
|
|
688
|
+
* @param handler - Callback that chains elements from the boundary event.
|
|
689
|
+
*/
|
|
690
|
+
withBoundary(id, options, handler) {
|
|
691
|
+
const attachedTo = this.lastNodeId;
|
|
692
|
+
if (!attachedTo || attachedTo === this.gatewayId) {
|
|
693
|
+
throw new Error("withBoundary() must follow a task element inside the branch. Current builder position has no active task.");
|
|
694
|
+
}
|
|
695
|
+
const attachedEl = this._elements.find((n) => n.id === attachedTo);
|
|
696
|
+
if (attachedEl?.type === "boundaryEvent") {
|
|
697
|
+
throw new Error("withBoundary() cannot attach to a boundary event. It must follow a task or activity element.");
|
|
698
|
+
}
|
|
699
|
+
const savedLast = this.lastNodeId;
|
|
700
|
+
const savedGateway = this.currentGatewayId;
|
|
701
|
+
const savedConnected = this._connected;
|
|
702
|
+
const savedOpenEnds = [...this.openBranchEnds];
|
|
703
|
+
this.openBranchEnds = [];
|
|
704
|
+
// Create and push the boundary event (no sequence flow)
|
|
705
|
+
this.boundaryEvent(id, { ...options, attachedTo });
|
|
706
|
+
// Build the boundary event's outgoing path
|
|
707
|
+
handler(this);
|
|
708
|
+
// Restore cursor to the task so the branch main flow continues
|
|
709
|
+
this.lastNodeId = savedLast;
|
|
710
|
+
this.currentGatewayId = savedGateway;
|
|
711
|
+
this._connected = savedConnected;
|
|
712
|
+
this.openBranchEnds = savedOpenEnds;
|
|
713
|
+
return this;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Create a named branch from the last gateway added inside this branch.
|
|
717
|
+
*
|
|
718
|
+
* Works identically to the top-level `ProcessBuilder.branch()` — use
|
|
719
|
+
* `.condition(expr)` or `.defaultFlow()` inside the callback, finish with
|
|
720
|
+
* `.connectTo(id)` or an `.endEvent()` to terminate the nested branch.
|
|
721
|
+
*/
|
|
722
|
+
branch(name, callback) {
|
|
723
|
+
if (!this.currentGatewayId) {
|
|
724
|
+
throw new Error("branch() must be called after a gateway element");
|
|
725
|
+
}
|
|
726
|
+
const b = new BranchBuilder(this.currentGatewayId, name, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
|
|
727
|
+
callback(b);
|
|
728
|
+
for (const el of b._elements) {
|
|
729
|
+
if (this._elements.some((n) => n.id === el.id)) {
|
|
730
|
+
throw new Error(`Duplicate element ID "${el.id}"`);
|
|
731
|
+
}
|
|
732
|
+
this._elements.push(el);
|
|
733
|
+
}
|
|
734
|
+
for (const fl of b._flows)
|
|
735
|
+
this._flows.push(fl);
|
|
736
|
+
for (const ann of b._textAnnotations)
|
|
737
|
+
this._textAnnotations.push(ann);
|
|
738
|
+
for (const assoc of b._associations)
|
|
739
|
+
this._associations.push(assoc);
|
|
740
|
+
if (b._defaultFlowId) {
|
|
741
|
+
const gw = this._elements.find((n) => n.id === this.currentGatewayId);
|
|
742
|
+
if (gw && (gw.type === "exclusiveGateway" || gw.type === "inclusiveGateway")) {
|
|
743
|
+
gw.default = b._defaultFlowId;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (!b._connected) {
|
|
747
|
+
const allEnds = [
|
|
748
|
+
...(b._lastNodeId !== undefined ? [b._lastNodeId] : []),
|
|
749
|
+
...b._openBranchEnds,
|
|
750
|
+
];
|
|
751
|
+
for (const endId of allEnds) {
|
|
752
|
+
const endEl = this._elements.find((n) => n.id === endId);
|
|
753
|
+
if (endEl && endEl.type !== "endEvent") {
|
|
754
|
+
this.openBranchEnds.push(endId);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
this.lastNodeId = undefined;
|
|
759
|
+
return this;
|
|
760
|
+
}
|
|
555
761
|
}
|
|
556
762
|
// ---------------------------------------------------------------------------
|
|
557
763
|
// Sub-process content builder
|
|
@@ -570,6 +776,11 @@ export class SubProcessContentBuilder {
|
|
|
570
776
|
lastNodeId;
|
|
571
777
|
currentGatewayId;
|
|
572
778
|
openBranchEnds = [];
|
|
779
|
+
rootMessages;
|
|
780
|
+
/** @internal */
|
|
781
|
+
constructor(rootMessages = []) {
|
|
782
|
+
this.rootMessages = rootMessages;
|
|
783
|
+
}
|
|
573
784
|
addElement(element) {
|
|
574
785
|
if (this._elements.some((n) => n.id === element.id)) {
|
|
575
786
|
throw new Error(`Duplicate element ID "${element.id}" in sub-process`);
|
|
@@ -600,8 +811,11 @@ export class SubProcessContentBuilder {
|
|
|
600
811
|
// ---- Events ----
|
|
601
812
|
startEvent(id, options) {
|
|
602
813
|
const el = makeFlowElement(id ?? generateId("StartEvent"), "startEvent", options);
|
|
603
|
-
if (el.type === "startEvent" && options)
|
|
814
|
+
if (el.type === "startEvent" && options) {
|
|
604
815
|
el.eventDefinitions = buildEventDefinitions(options);
|
|
816
|
+
if (options.isInterrupting === false)
|
|
817
|
+
el.isInterrupting = false;
|
|
818
|
+
}
|
|
605
819
|
return this.addElement(el);
|
|
606
820
|
}
|
|
607
821
|
endEvent(id, options) {
|
|
@@ -639,10 +853,27 @@ export class SubProcessContentBuilder {
|
|
|
639
853
|
return this.addElement(makeCallActivityEl(id, options));
|
|
640
854
|
}
|
|
641
855
|
sendTask(id, options) {
|
|
642
|
-
|
|
856
|
+
const el = makeFlowElement(id, "sendTask", options);
|
|
857
|
+
if (options?.isForCompensation)
|
|
858
|
+
el.isForCompensation = true;
|
|
859
|
+
if (options?.messageName)
|
|
860
|
+
el.messageRef = resolveMessage(options.messageName, this.rootMessages);
|
|
861
|
+
return this.addElement(el);
|
|
643
862
|
}
|
|
644
863
|
receiveTask(id, options) {
|
|
645
|
-
|
|
864
|
+
const el = makeFlowElement(id, "receiveTask", options);
|
|
865
|
+
if (options?.isForCompensation)
|
|
866
|
+
el.isForCompensation = true;
|
|
867
|
+
if (options?.messageName)
|
|
868
|
+
el.messageRef = resolveMessage(options.messageName, this.rootMessages);
|
|
869
|
+
return this.addElement(el);
|
|
870
|
+
}
|
|
871
|
+
/** Add an abstract task with no Zeebe extensions. */
|
|
872
|
+
task(id, options) {
|
|
873
|
+
const el = makeFlowElement(id, "task", options);
|
|
874
|
+
if (options?.isForCompensation)
|
|
875
|
+
el.isForCompensation = true;
|
|
876
|
+
return this.addElement(el);
|
|
646
877
|
}
|
|
647
878
|
// ---- Gateways ----
|
|
648
879
|
exclusiveGateway(id, options) {
|
|
@@ -709,10 +940,16 @@ export class SubProcessContentBuilder {
|
|
|
709
940
|
gw.default = b._defaultFlowId;
|
|
710
941
|
}
|
|
711
942
|
}
|
|
712
|
-
if (!b._connected
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
943
|
+
if (!b._connected) {
|
|
944
|
+
const allEnds = [
|
|
945
|
+
...(b._lastNodeId !== undefined ? [b._lastNodeId] : []),
|
|
946
|
+
...b._openBranchEnds,
|
|
947
|
+
];
|
|
948
|
+
for (const endId of allEnds) {
|
|
949
|
+
const endEl = this._elements.find((n) => n.id === endId);
|
|
950
|
+
if (endEl && endEl.type !== "endEvent") {
|
|
951
|
+
this.openBranchEnds.push(endId);
|
|
952
|
+
}
|
|
716
953
|
}
|
|
717
954
|
}
|
|
718
955
|
this.lastNodeId = undefined;
|
|
@@ -753,6 +990,8 @@ export class ProcessBuilder {
|
|
|
753
990
|
sequenceFlows = [];
|
|
754
991
|
rootErrors = [];
|
|
755
992
|
rootMessages = [];
|
|
993
|
+
rootSignals = [];
|
|
994
|
+
rootEscalations = [];
|
|
756
995
|
_textAnnotations = [];
|
|
757
996
|
_associations = [];
|
|
758
997
|
_annCounters = new Map();
|
|
@@ -762,6 +1001,7 @@ export class ProcessBuilder {
|
|
|
762
1001
|
_autoLayout = false;
|
|
763
1002
|
_executionPlatformVersion = "8.9.0";
|
|
764
1003
|
_serviceTaskDefaults = {};
|
|
1004
|
+
_savedMainFlowId = undefined;
|
|
765
1005
|
constructor(processId) {
|
|
766
1006
|
this.processId = processId;
|
|
767
1007
|
}
|
|
@@ -811,7 +1051,9 @@ export class ProcessBuilder {
|
|
|
811
1051
|
extensionElements: extElements,
|
|
812
1052
|
});
|
|
813
1053
|
if (element.type === "startEvent" && options) {
|
|
814
|
-
element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages);
|
|
1054
|
+
element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
|
|
1055
|
+
if (options.isInterrupting === false)
|
|
1056
|
+
element.isInterrupting = false;
|
|
815
1057
|
}
|
|
816
1058
|
if (options?.modelerTemplate) {
|
|
817
1059
|
element.unknownAttributes["zeebe:modelerTemplate"] = options.modelerTemplate;
|
|
@@ -850,7 +1092,7 @@ export class ProcessBuilder {
|
|
|
850
1092
|
const nodeId = id ?? generateId("EndEvent");
|
|
851
1093
|
const element = makeFlowElement(nodeId, "endEvent", options);
|
|
852
1094
|
if (element.type === "endEvent" && options) {
|
|
853
|
-
element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages);
|
|
1095
|
+
element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
|
|
854
1096
|
}
|
|
855
1097
|
this.addFlowElement(element);
|
|
856
1098
|
return this;
|
|
@@ -860,7 +1102,7 @@ export class ProcessBuilder {
|
|
|
860
1102
|
const nodeId = id ?? generateId("IntermediateThrowEvent");
|
|
861
1103
|
const element = makeFlowElement(nodeId, "intermediateThrowEvent", options);
|
|
862
1104
|
if (element.type === "intermediateThrowEvent" && options) {
|
|
863
|
-
element.eventDefinitions = buildEventDefinitions(options);
|
|
1105
|
+
element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
|
|
864
1106
|
}
|
|
865
1107
|
this.addFlowElement(element);
|
|
866
1108
|
return this;
|
|
@@ -870,7 +1112,7 @@ export class ProcessBuilder {
|
|
|
870
1112
|
const nodeId = id ?? generateId("IntermediateCatchEvent");
|
|
871
1113
|
const element = makeFlowElement(nodeId, "intermediateCatchEvent", options);
|
|
872
1114
|
if (element.type === "intermediateCatchEvent" && options) {
|
|
873
|
-
element.eventDefinitions = buildEventDefinitions(options);
|
|
1115
|
+
element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
|
|
874
1116
|
}
|
|
875
1117
|
this.addFlowElement(element);
|
|
876
1118
|
return this;
|
|
@@ -886,12 +1128,17 @@ export class ProcessBuilder {
|
|
|
886
1128
|
if (element.type === "boundaryEvent") {
|
|
887
1129
|
element.attachedToRef = options.attachedTo;
|
|
888
1130
|
element.cancelActivity = options.cancelActivity;
|
|
889
|
-
element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages);
|
|
1131
|
+
element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
|
|
890
1132
|
}
|
|
891
1133
|
// Boundary events never auto-connect — temporarily clear lastNodeId
|
|
892
1134
|
const prevLast = this.lastNodeId;
|
|
893
1135
|
this.lastNodeId = undefined;
|
|
894
1136
|
this.addFlowElement(element);
|
|
1137
|
+
// For compensation boundary events, save the main-flow cursor AFTER addFlowElement
|
|
1138
|
+
// so subsequent normal elements don't accidentally clear it before it's consumed.
|
|
1139
|
+
if (options.compensation) {
|
|
1140
|
+
this._savedMainFlowId = prevLast;
|
|
1141
|
+
}
|
|
895
1142
|
// Don't restore prevLast — the builder now chains from the boundary event
|
|
896
1143
|
void prevLast;
|
|
897
1144
|
return this;
|
|
@@ -921,6 +1168,7 @@ export class ProcessBuilder {
|
|
|
921
1168
|
this.lastNodeId = savedLast;
|
|
922
1169
|
this.currentGatewayId = savedGateway;
|
|
923
1170
|
this.openBranchEnds = savedOpenEnds;
|
|
1171
|
+
this._savedMainFlowId = undefined;
|
|
924
1172
|
return this;
|
|
925
1173
|
}
|
|
926
1174
|
// ---- Tasks ----
|
|
@@ -971,12 +1219,22 @@ export class ProcessBuilder {
|
|
|
971
1219
|
}
|
|
972
1220
|
/** Add a send task (aspirational). */
|
|
973
1221
|
sendTask(id, options) {
|
|
974
|
-
|
|
1222
|
+
const el = makeFlowElement(id, "sendTask", options);
|
|
1223
|
+
if (options?.isForCompensation)
|
|
1224
|
+
el.isForCompensation = true;
|
|
1225
|
+
if (options?.messageName)
|
|
1226
|
+
el.messageRef = resolveMessage(options.messageName, this.rootMessages);
|
|
1227
|
+
this.addFlowElement(el);
|
|
975
1228
|
return this;
|
|
976
1229
|
}
|
|
977
1230
|
/** Add a receive task (aspirational). */
|
|
978
1231
|
receiveTask(id, options) {
|
|
979
|
-
|
|
1232
|
+
const el = makeFlowElement(id, "receiveTask", options);
|
|
1233
|
+
if (options?.isForCompensation)
|
|
1234
|
+
el.isForCompensation = true;
|
|
1235
|
+
if (options?.messageName)
|
|
1236
|
+
el.messageRef = resolveMessage(options.messageName, this.rootMessages);
|
|
1237
|
+
this.addFlowElement(el);
|
|
980
1238
|
return this;
|
|
981
1239
|
}
|
|
982
1240
|
/** Add a business rule task. */
|
|
@@ -989,6 +1247,14 @@ export class ProcessBuilder {
|
|
|
989
1247
|
this.addFlowElement(makeCallActivityEl(id, options));
|
|
990
1248
|
return this;
|
|
991
1249
|
}
|
|
1250
|
+
/** Add an abstract task with no Zeebe extensions. */
|
|
1251
|
+
task(id, options) {
|
|
1252
|
+
const el = makeFlowElement(id, "task", options);
|
|
1253
|
+
if (options?.isForCompensation)
|
|
1254
|
+
el.isForCompensation = true;
|
|
1255
|
+
this.addFlowElement(el);
|
|
1256
|
+
return this;
|
|
1257
|
+
}
|
|
992
1258
|
// ---- Gateways ----
|
|
993
1259
|
/** Add an exclusive gateway (XOR split/join). */
|
|
994
1260
|
exclusiveGateway(id, options) {
|
|
@@ -1036,7 +1302,7 @@ export class ProcessBuilder {
|
|
|
1036
1302
|
if (!this.currentGatewayId) {
|
|
1037
1303
|
throw new Error("branch() must be called after a gateway element");
|
|
1038
1304
|
}
|
|
1039
|
-
const b = new BranchBuilder(this.currentGatewayId, name);
|
|
1305
|
+
const b = new BranchBuilder(this.currentGatewayId, name, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
|
|
1040
1306
|
callback(b);
|
|
1041
1307
|
for (const el of b._elements) {
|
|
1042
1308
|
if (this.flowElements.some((n) => n.id === el.id)) {
|
|
@@ -1060,10 +1326,16 @@ export class ProcessBuilder {
|
|
|
1060
1326
|
}
|
|
1061
1327
|
// Track the branch's open end so the next element auto-connects from it.
|
|
1062
1328
|
// Skip branches that terminated at an end event (those are intentional dead-ends).
|
|
1063
|
-
if (!b._connected
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1329
|
+
if (!b._connected) {
|
|
1330
|
+
const allEnds = [
|
|
1331
|
+
...(b._lastNodeId !== undefined ? [b._lastNodeId] : []),
|
|
1332
|
+
...b._openBranchEnds,
|
|
1333
|
+
];
|
|
1334
|
+
for (const endId of allEnds) {
|
|
1335
|
+
const endEl = this.flowElements.find((n) => n.id === endId);
|
|
1336
|
+
if (endEl && endEl.type !== "endEvent") {
|
|
1337
|
+
this.openBranchEnds.push(endId);
|
|
1338
|
+
}
|
|
1067
1339
|
}
|
|
1068
1340
|
}
|
|
1069
1341
|
this.lastNodeId = undefined;
|
|
@@ -1102,7 +1374,7 @@ export class ProcessBuilder {
|
|
|
1102
1374
|
// ---- Sub-processes ----
|
|
1103
1375
|
/** Add an ad-hoc sub-process with optional AI agent or multi-instance configuration. */
|
|
1104
1376
|
adHocSubProcess(id, content, options) {
|
|
1105
|
-
const sub = new SubProcessContentBuilder();
|
|
1377
|
+
const sub = new SubProcessContentBuilder(this.rootMessages);
|
|
1106
1378
|
content(sub);
|
|
1107
1379
|
insertJoinGateways(sub._elements, sub._flows);
|
|
1108
1380
|
recomputeIncomingOutgoing(sub._elements, sub._flows);
|
|
@@ -1170,7 +1442,7 @@ export class ProcessBuilder {
|
|
|
1170
1442
|
}
|
|
1171
1443
|
/** Add a sub-process (aspirational). */
|
|
1172
1444
|
subProcess(id, content, options) {
|
|
1173
|
-
const sub = new SubProcessContentBuilder();
|
|
1445
|
+
const sub = new SubProcessContentBuilder(this.rootMessages);
|
|
1174
1446
|
content(sub);
|
|
1175
1447
|
insertJoinGateways(sub._elements, sub._flows);
|
|
1176
1448
|
recomputeIncomingOutgoing(sub._elements, sub._flows);
|
|
@@ -1187,20 +1459,29 @@ export class ProcessBuilder {
|
|
|
1187
1459
|
this.addFlowElement(element);
|
|
1188
1460
|
return this;
|
|
1189
1461
|
}
|
|
1190
|
-
/** Add an event sub-process
|
|
1462
|
+
/** Add an event sub-process. Triggered by its start event — no incoming or outgoing sequence flows. */
|
|
1191
1463
|
eventSubProcess(id, content, options) {
|
|
1192
|
-
const sub = new SubProcessContentBuilder();
|
|
1464
|
+
const sub = new SubProcessContentBuilder(this.rootMessages);
|
|
1193
1465
|
content(sub);
|
|
1194
1466
|
insertJoinGateways(sub._elements, sub._flows);
|
|
1195
1467
|
recomputeIncomingOutgoing(sub._elements, sub._flows);
|
|
1196
|
-
const element = makeFlowElement(id, "
|
|
1197
|
-
if (element.type === "
|
|
1468
|
+
const element = makeFlowElement(id, "subProcess", options);
|
|
1469
|
+
if (element.type === "subProcess") {
|
|
1470
|
+
element.triggeredByEvent = true;
|
|
1198
1471
|
element.flowElements = sub._elements;
|
|
1199
1472
|
element.sequenceFlows = sub._flows;
|
|
1200
1473
|
element.textAnnotations = sub._textAnnotations;
|
|
1201
1474
|
element.associations = sub._associations;
|
|
1202
1475
|
}
|
|
1203
|
-
|
|
1476
|
+
// Event sub-processes have no incoming/outgoing sequence flows and must not
|
|
1477
|
+
// advance the flow cursor — the surrounding process wires around them.
|
|
1478
|
+
// openBranchEnds is intentionally NOT drained here; the next normal
|
|
1479
|
+
// addFlowElement call will drain it and connect branch ends to that element.
|
|
1480
|
+
if (this.flowElements.some((n) => n.id === element.id)) {
|
|
1481
|
+
throw new Error(`Duplicate element ID "${element.id}" in process "${this.processId}"`);
|
|
1482
|
+
}
|
|
1483
|
+
this._savedMainFlowId = undefined;
|
|
1484
|
+
this.flowElements.push(element);
|
|
1204
1485
|
return this;
|
|
1205
1486
|
}
|
|
1206
1487
|
// ---- Annotations ----
|
|
@@ -1283,9 +1564,9 @@ export class ProcessBuilder {
|
|
|
1283
1564
|
"modeler:executionPlatformVersion": this._executionPlatformVersion,
|
|
1284
1565
|
},
|
|
1285
1566
|
errors: this.rootErrors,
|
|
1286
|
-
escalations:
|
|
1567
|
+
escalations: this.rootEscalations,
|
|
1287
1568
|
messages: this.rootMessages,
|
|
1288
|
-
signals:
|
|
1569
|
+
signals: this.rootSignals,
|
|
1289
1570
|
collaborations: [],
|
|
1290
1571
|
processes: [process],
|
|
1291
1572
|
// Seed diagram stub so applyAutoLayout preserves process-specific IDs.
|
|
@@ -1323,6 +1604,24 @@ export class ProcessBuilder {
|
|
|
1323
1604
|
throw new Error(`Duplicate element ID "${element.id}" in process "${this.processId}"`);
|
|
1324
1605
|
}
|
|
1325
1606
|
this.flowElements.push(element);
|
|
1607
|
+
// Compensation handlers are outside the normal token flow: link via association
|
|
1608
|
+
// from the preceding compensation boundary event, then restore the main-flow cursor.
|
|
1609
|
+
if (element.isForCompensation) {
|
|
1610
|
+
if (this.lastNodeId) {
|
|
1611
|
+
this._associations.push({
|
|
1612
|
+
id: generateId("Association"),
|
|
1613
|
+
sourceRef: this.lastNodeId,
|
|
1614
|
+
targetRef: element.id,
|
|
1615
|
+
associationDirection: "One",
|
|
1616
|
+
unknownAttributes: {},
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
// Restore main-flow cursor (saved by boundaryEvent() when compensation: true)
|
|
1620
|
+
this.lastNodeId = this._savedMainFlowId;
|
|
1621
|
+
this._savedMainFlowId = undefined;
|
|
1622
|
+
// Do NOT connect open branch ends — handler is outside normal flow
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1326
1625
|
if (this.lastNodeId) {
|
|
1327
1626
|
const flowId = generateId("Flow");
|
|
1328
1627
|
this.sequenceFlows.push({
|
|
@@ -1345,6 +1644,9 @@ export class ProcessBuilder {
|
|
|
1345
1644
|
});
|
|
1346
1645
|
}
|
|
1347
1646
|
this.openBranchEnds = [];
|
|
1647
|
+
// Clear any saved compensation cursor — a normal element advancing the cursor
|
|
1648
|
+
// means the compensation boundary/handler pattern has been interrupted.
|
|
1649
|
+
this._savedMainFlowId = undefined;
|
|
1348
1650
|
this.lastNodeId = element.id;
|
|
1349
1651
|
}
|
|
1350
1652
|
}
|
|
@@ -105,10 +105,12 @@ interface BpmnFlowNodeBase {
|
|
|
105
105
|
documentation?: string;
|
|
106
106
|
extensionElements: XmlElement[];
|
|
107
107
|
unknownAttributes: Record<string, string>;
|
|
108
|
+
isForCompensation?: boolean;
|
|
108
109
|
}
|
|
109
110
|
export interface BpmnStartEvent extends BpmnFlowNodeBase {
|
|
110
111
|
type: "startEvent";
|
|
111
112
|
eventDefinitions: BpmnEventDefinition[];
|
|
113
|
+
isInterrupting?: boolean;
|
|
112
114
|
}
|
|
113
115
|
export interface BpmnEndEvent extends BpmnFlowNodeBase {
|
|
114
116
|
type: "endEvent";
|
|
@@ -150,10 +152,12 @@ export interface BpmnCallActivity extends BpmnFlowNodeBase {
|
|
|
150
152
|
}
|
|
151
153
|
export interface BpmnSendTask extends BpmnFlowNodeBase {
|
|
152
154
|
type: "sendTask";
|
|
155
|
+
messageRef?: string;
|
|
153
156
|
loopCharacteristics?: BpmnMultiInstanceLoopCharacteristics;
|
|
154
157
|
}
|
|
155
158
|
export interface BpmnReceiveTask extends BpmnFlowNodeBase {
|
|
156
159
|
type: "receiveTask";
|
|
160
|
+
messageRef?: string;
|
|
157
161
|
loopCharacteristics?: BpmnMultiInstanceLoopCharacteristics;
|
|
158
162
|
}
|
|
159
163
|
export interface BpmnAdHocSubProcess extends BpmnFlowNodeBase {
|
package/dist/bpmn/bpmn-parser.js
CHANGED
|
@@ -42,6 +42,7 @@ const KNOWN_ATTRS = new Set([
|
|
|
42
42
|
"default",
|
|
43
43
|
"attachedToRef",
|
|
44
44
|
"cancelActivity",
|
|
45
|
+
"isForCompensation",
|
|
45
46
|
"sourceRef",
|
|
46
47
|
"targetRef",
|
|
47
48
|
"associationDirection",
|
|
@@ -275,7 +276,15 @@ function parseFlowElement(element) {
|
|
|
275
276
|
unknownAttributes: unknownAttrs(element),
|
|
276
277
|
};
|
|
277
278
|
switch (ln) {
|
|
278
|
-
case "startEvent":
|
|
279
|
+
case "startEvent": {
|
|
280
|
+
const isInterruptingAttr = attr(element, "isInterrupting");
|
|
281
|
+
return {
|
|
282
|
+
...base,
|
|
283
|
+
type: "startEvent",
|
|
284
|
+
eventDefinitions: parseEventDefinitions(element),
|
|
285
|
+
...(isInterruptingAttr === "false" ? { isInterrupting: false } : {}),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
279
288
|
case "endEvent":
|
|
280
289
|
case "intermediateCatchEvent":
|
|
281
290
|
case "intermediateThrowEvent":
|
|
@@ -294,12 +303,24 @@ function parseFlowElement(element) {
|
|
|
294
303
|
case "serviceTask":
|
|
295
304
|
case "scriptTask":
|
|
296
305
|
case "userTask":
|
|
297
|
-
case "sendTask":
|
|
298
|
-
case "receiveTask":
|
|
299
306
|
case "businessRuleTask":
|
|
300
307
|
case "manualTask":
|
|
301
308
|
case "callActivity":
|
|
302
|
-
return {
|
|
309
|
+
return {
|
|
310
|
+
...base,
|
|
311
|
+
type: ln,
|
|
312
|
+
loopCharacteristics: parseLoopCharacteristics(element),
|
|
313
|
+
isForCompensation: attr(element, "isForCompensation") === "true" ? true : undefined,
|
|
314
|
+
};
|
|
315
|
+
case "sendTask":
|
|
316
|
+
case "receiveTask":
|
|
317
|
+
return {
|
|
318
|
+
...base,
|
|
319
|
+
type: ln,
|
|
320
|
+
messageRef: attr(element, "messageRef"),
|
|
321
|
+
loopCharacteristics: parseLoopCharacteristics(element),
|
|
322
|
+
isForCompensation: attr(element, "isForCompensation") === "true" ? true : undefined,
|
|
323
|
+
};
|
|
303
324
|
case "adHocSubProcess":
|
|
304
325
|
return {
|
|
305
326
|
...base,
|
|
@@ -157,6 +157,8 @@ function serializeFlowElement(fe, ns) {
|
|
|
157
157
|
const attrs = { id: fe.id, ...fe.unknownAttributes };
|
|
158
158
|
if (fe.name !== undefined)
|
|
159
159
|
attrs.name = fe.name;
|
|
160
|
+
if (fe.isForCompensation)
|
|
161
|
+
attrs.isForCompensation = "true";
|
|
160
162
|
const children = [];
|
|
161
163
|
// Documentation
|
|
162
164
|
if (fe.documentation !== undefined) {
|
|
@@ -169,6 +171,10 @@ function serializeFlowElement(fe, ns) {
|
|
|
169
171
|
children.push(...flowRefs(fe.outgoing, "outgoing", bp));
|
|
170
172
|
switch (fe.type) {
|
|
171
173
|
case "startEvent":
|
|
174
|
+
if (fe.isInterrupting === false)
|
|
175
|
+
attrs.isInterrupting = "false";
|
|
176
|
+
children.push(...serializeEventDefinitions(fe.eventDefinitions, bp));
|
|
177
|
+
break;
|
|
172
178
|
case "endEvent":
|
|
173
179
|
case "intermediateCatchEvent":
|
|
174
180
|
case "intermediateThrowEvent":
|
|
@@ -184,13 +190,17 @@ function serializeFlowElement(fe, ns) {
|
|
|
184
190
|
case "serviceTask":
|
|
185
191
|
case "scriptTask":
|
|
186
192
|
case "userTask":
|
|
187
|
-
case "sendTask":
|
|
188
|
-
case "receiveTask":
|
|
189
193
|
case "businessRuleTask":
|
|
190
194
|
case "manualTask":
|
|
191
195
|
case "callActivity":
|
|
192
196
|
children.push(...serializeLoopCharacteristics(fe.loopCharacteristics, bp));
|
|
193
197
|
break;
|
|
198
|
+
case "sendTask":
|
|
199
|
+
case "receiveTask":
|
|
200
|
+
if (fe.messageRef)
|
|
201
|
+
attrs.messageRef = fe.messageRef;
|
|
202
|
+
children.push(...serializeLoopCharacteristics(fe.loopCharacteristics, bp));
|
|
203
|
+
break;
|
|
194
204
|
case "adHocSubProcess":
|
|
195
205
|
children.push(...serializeLoopCharacteristics(fe.loopCharacteristics, bp));
|
|
196
206
|
children.push(...serializeProcessContents(fe, ns));
|
|
@@ -36,6 +36,7 @@ function repositionBoundaryEvents(flowElements, result) {
|
|
|
36
36
|
ps.add(edge.sourceRef);
|
|
37
37
|
predIds.set(edge.targetRef, ps);
|
|
38
38
|
}
|
|
39
|
+
const allChainNodes = new Set();
|
|
39
40
|
for (const [hostId, beIds] of boundaryMap) {
|
|
40
41
|
const hostNode = nodeById.get(hostId);
|
|
41
42
|
if (!hostNode)
|
|
@@ -72,6 +73,9 @@ function repositionBoundaryEvents(flowElements, result) {
|
|
|
72
73
|
queue.push(...(succIds.get(id) ?? []));
|
|
73
74
|
}
|
|
74
75
|
}
|
|
76
|
+
// Record all chain members so the forward pass can identify them.
|
|
77
|
+
for (const cid of chainSet)
|
|
78
|
+
allChainNodes.add(cid);
|
|
75
79
|
// Each boundary event's chain gets its own vertical lane
|
|
76
80
|
let maxChainH = 0;
|
|
77
81
|
for (const id of chainOrder) {
|
|
@@ -133,6 +137,116 @@ function repositionBoundaryEvents(flowElements, result) {
|
|
|
133
137
|
}
|
|
134
138
|
}
|
|
135
139
|
}
|
|
140
|
+
// Forward-placement pass: any node not in any chain but whose predecessor
|
|
141
|
+
// has been relocated further right must be pushed rightward.
|
|
142
|
+
// Process in topological order (Kahn's algorithm over the sequenceFlow graph).
|
|
143
|
+
const inDegree = new Map();
|
|
144
|
+
for (const id of nodeById.keys()) {
|
|
145
|
+
inDegree.set(id, (predIds.get(id) ?? new Set()).size);
|
|
146
|
+
}
|
|
147
|
+
const topoQueue = [];
|
|
148
|
+
for (const [id, deg] of inDegree) {
|
|
149
|
+
if (deg === 0)
|
|
150
|
+
topoQueue.push(id);
|
|
151
|
+
}
|
|
152
|
+
const topoOrder = [];
|
|
153
|
+
while (topoQueue.length > 0) {
|
|
154
|
+
const id = topoQueue.shift();
|
|
155
|
+
if (!id)
|
|
156
|
+
break;
|
|
157
|
+
topoOrder.push(id);
|
|
158
|
+
for (const succId of succIds.get(id) ?? []) {
|
|
159
|
+
const newDeg = (inDegree.get(succId) ?? 1) - 1;
|
|
160
|
+
inDegree.set(succId, newDeg);
|
|
161
|
+
if (newDeg === 0)
|
|
162
|
+
topoQueue.push(succId);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const movedInPass = new Set();
|
|
166
|
+
for (const id of topoOrder) {
|
|
167
|
+
if (allChainNodes.has(id))
|
|
168
|
+
continue;
|
|
169
|
+
const node = nodeById.get(id);
|
|
170
|
+
if (!node)
|
|
171
|
+
continue;
|
|
172
|
+
const preds = predIds.get(id) ?? new Set();
|
|
173
|
+
if (preds.size === 0)
|
|
174
|
+
continue;
|
|
175
|
+
let maxPredRight = 0;
|
|
176
|
+
for (const predId of preds) {
|
|
177
|
+
const pred = nodeById.get(predId);
|
|
178
|
+
if (pred)
|
|
179
|
+
maxPredRight = Math.max(maxPredRight, pred.bounds.x + pred.bounds.width);
|
|
180
|
+
}
|
|
181
|
+
const minX = maxPredRight + CHAIN_GAP;
|
|
182
|
+
if (minX > node.bounds.x) {
|
|
183
|
+
const delta = minX - node.bounds.x;
|
|
184
|
+
node.bounds.x = minX;
|
|
185
|
+
if (node.labelBounds)
|
|
186
|
+
node.labelBounds.x += delta;
|
|
187
|
+
movedInPass.add(id);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Spatial bump: if a moved node now overlaps a non-chain node on a parallel
|
|
191
|
+
// path (no predecessor/successor relationship), push it clear and cascade.
|
|
192
|
+
let bumped = true;
|
|
193
|
+
while (bumped) {
|
|
194
|
+
bumped = false;
|
|
195
|
+
for (const movedId of movedInPass) {
|
|
196
|
+
const moved = nodeById.get(movedId);
|
|
197
|
+
if (!moved)
|
|
198
|
+
continue;
|
|
199
|
+
const movedRight = moved.bounds.x + moved.bounds.width;
|
|
200
|
+
for (const [otherId, other] of nodeById) {
|
|
201
|
+
if (otherId === movedId)
|
|
202
|
+
continue;
|
|
203
|
+
if (allChainNodes.has(otherId))
|
|
204
|
+
continue;
|
|
205
|
+
if (movedInPass.has(otherId))
|
|
206
|
+
continue;
|
|
207
|
+
// Check y overlap
|
|
208
|
+
if (other.bounds.y + other.bounds.height <= moved.bounds.y)
|
|
209
|
+
continue;
|
|
210
|
+
if (other.bounds.y >= moved.bounds.y + moved.bounds.height)
|
|
211
|
+
continue;
|
|
212
|
+
// Check x overlap (moved node intrudes into other's space)
|
|
213
|
+
if (other.bounds.x >= movedRight)
|
|
214
|
+
continue;
|
|
215
|
+
if (other.bounds.x + other.bounds.width <= moved.bounds.x)
|
|
216
|
+
continue;
|
|
217
|
+
// Push other right of moved
|
|
218
|
+
const newX = movedRight + CHAIN_GAP;
|
|
219
|
+
if (newX > other.bounds.x) {
|
|
220
|
+
const delta = newX - other.bounds.x;
|
|
221
|
+
other.bounds.x = newX;
|
|
222
|
+
if (other.labelBounds)
|
|
223
|
+
other.labelBounds.x += delta;
|
|
224
|
+
movedInPass.add(otherId);
|
|
225
|
+
bumped = true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Re-route edges where a chain source now points at a moved target,
|
|
231
|
+
// or where the source itself was moved by the forward pass.
|
|
232
|
+
for (const edge of result.edges) {
|
|
233
|
+
const srcMoved = movedInPass.has(edge.sourceRef);
|
|
234
|
+
const tgtMoved = movedInPass.has(edge.targetRef);
|
|
235
|
+
if (!srcMoved && !tgtMoved)
|
|
236
|
+
continue;
|
|
237
|
+
const src = nodeById.get(edge.sourceRef);
|
|
238
|
+
const tgt = nodeById.get(edge.targetRef);
|
|
239
|
+
if (!src || !tgt)
|
|
240
|
+
continue;
|
|
241
|
+
const srcX = Math.round(src.bounds.x + src.bounds.width);
|
|
242
|
+
const srcY = Math.round(src.bounds.y + src.bounds.height / 2);
|
|
243
|
+
const tgtX = Math.round(tgt.bounds.x);
|
|
244
|
+
const tgtY = Math.round(tgt.bounds.y + tgt.bounds.height / 2);
|
|
245
|
+
edge.waypoints = [
|
|
246
|
+
{ x: srcX, y: srcY },
|
|
247
|
+
{ x: tgtX, y: tgtY },
|
|
248
|
+
];
|
|
249
|
+
}
|
|
136
250
|
}
|
|
137
251
|
/**
|
|
138
252
|
* Auto-layout a BPMN process using the Sugiyama/layered algorithm.
|