@bpmnkit/core 0.0.8

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.
Files changed (104) hide show
  1. package/README.md +185 -0
  2. package/dist/bpmn/auto-layout.d.ts +10 -0
  3. package/dist/bpmn/auto-layout.js +203 -0
  4. package/dist/bpmn/bpmn-builder.d.ts +381 -0
  5. package/dist/bpmn/bpmn-builder.js +1223 -0
  6. package/dist/bpmn/bpmn-model.d.ts +403 -0
  7. package/dist/bpmn/bpmn-model.js +2 -0
  8. package/dist/bpmn/bpmn-parser.d.ts +4 -0
  9. package/dist/bpmn/bpmn-parser.js +642 -0
  10. package/dist/bpmn/bpmn-serializer.d.ts +4 -0
  11. package/dist/bpmn/bpmn-serializer.js +515 -0
  12. package/dist/bpmn/compact.d.ts +91 -0
  13. package/dist/bpmn/compact.js +349 -0
  14. package/dist/bpmn/di-color.d.ts +9 -0
  15. package/dist/bpmn/di-color.js +32 -0
  16. package/dist/bpmn/index.d.ts +105 -0
  17. package/dist/bpmn/index.js +176 -0
  18. package/dist/bpmn/optimize/feel.d.ts +4 -0
  19. package/dist/bpmn/optimize/feel.js +186 -0
  20. package/dist/bpmn/optimize/flow.d.ts +4 -0
  21. package/dist/bpmn/optimize/flow.js +323 -0
  22. package/dist/bpmn/optimize/index.d.ts +5 -0
  23. package/dist/bpmn/optimize/index.js +51 -0
  24. package/dist/bpmn/optimize/naming.d.ts +8 -0
  25. package/dist/bpmn/optimize/naming.js +145 -0
  26. package/dist/bpmn/optimize/tasks.d.ts +4 -0
  27. package/dist/bpmn/optimize/tasks.js +273 -0
  28. package/dist/bpmn/optimize/types.d.ts +44 -0
  29. package/dist/bpmn/optimize/types.js +2 -0
  30. package/dist/bpmn/optimize/utils.d.ts +23 -0
  31. package/dist/bpmn/optimize/utils.js +129 -0
  32. package/dist/bpmn/rest-connector.d.ts +48 -0
  33. package/dist/bpmn/rest-connector.js +69 -0
  34. package/dist/bpmn/svg.d.ts +35 -0
  35. package/dist/bpmn/svg.js +557 -0
  36. package/dist/bpmn/type-guards.d.ts +136 -0
  37. package/dist/bpmn/type-guards.js +211 -0
  38. package/dist/bpmn/utils.d.ts +88 -0
  39. package/dist/bpmn/utils.js +207 -0
  40. package/dist/bpmn/zeebe-extensions.d.ts +72 -0
  41. package/dist/bpmn/zeebe-extensions.js +96 -0
  42. package/dist/dmn/compact.d.ts +44 -0
  43. package/dist/dmn/compact.js +125 -0
  44. package/dist/dmn/dmn-builder.d.ts +59 -0
  45. package/dist/dmn/dmn-builder.js +144 -0
  46. package/dist/dmn/dmn-layout.d.ts +57 -0
  47. package/dist/dmn/dmn-layout.js +256 -0
  48. package/dist/dmn/dmn-model.d.ts +158 -0
  49. package/dist/dmn/dmn-model.js +2 -0
  50. package/dist/dmn/dmn-parser.d.ts +4 -0
  51. package/dist/dmn/dmn-parser.js +275 -0
  52. package/dist/dmn/dmn-serializer.d.ts +4 -0
  53. package/dist/dmn/dmn-serializer.js +296 -0
  54. package/dist/dmn/index.d.ts +28 -0
  55. package/dist/dmn/index.js +51 -0
  56. package/dist/errors.d.ts +77 -0
  57. package/dist/errors.js +66 -0
  58. package/dist/form/compact.d.ts +20 -0
  59. package/dist/form/compact.js +146 -0
  60. package/dist/form/form-builder.d.ts +111 -0
  61. package/dist/form/form-builder.js +347 -0
  62. package/dist/form/form-model.d.ts +221 -0
  63. package/dist/form/form-model.js +2 -0
  64. package/dist/form/form-parser.d.ts +4 -0
  65. package/dist/form/form-parser.js +514 -0
  66. package/dist/form/form-serializer.d.ts +4 -0
  67. package/dist/form/form-serializer.js +240 -0
  68. package/dist/form/index.d.ts +27 -0
  69. package/dist/form/index.js +43 -0
  70. package/dist/index.d.ts +37 -0
  71. package/dist/index.js +19 -0
  72. package/dist/layout/bench.d.ts +86 -0
  73. package/dist/layout/bench.js +225 -0
  74. package/dist/layout/coordinates.d.ts +62 -0
  75. package/dist/layout/coordinates.js +868 -0
  76. package/dist/layout/crossing.d.ts +8 -0
  77. package/dist/layout/crossing.js +60 -0
  78. package/dist/layout/graph.d.ts +28 -0
  79. package/dist/layout/graph.js +126 -0
  80. package/dist/layout/index.d.ts +7 -0
  81. package/dist/layout/index.js +5 -0
  82. package/dist/layout/layers.d.ts +13 -0
  83. package/dist/layout/layers.js +49 -0
  84. package/dist/layout/layout-engine.d.ts +21 -0
  85. package/dist/layout/layout-engine.js +191 -0
  86. package/dist/layout/overlap.d.ts +10 -0
  87. package/dist/layout/overlap.js +70 -0
  88. package/dist/layout/routing.d.ts +26 -0
  89. package/dist/layout/routing.js +439 -0
  90. package/dist/layout/subprocess.d.ts +14 -0
  91. package/dist/layout/subprocess.js +77 -0
  92. package/dist/layout/types.d.ts +72 -0
  93. package/dist/layout/types.js +37 -0
  94. package/dist/types/id-generator.d.ts +8 -0
  95. package/dist/types/id-generator.js +31 -0
  96. package/dist/types/index.d.ts +3 -0
  97. package/dist/types/index.js +2 -0
  98. package/dist/types/xml-element.d.ts +12 -0
  99. package/dist/types/xml-element.js +2 -0
  100. package/dist/xml/index.d.ts +2 -0
  101. package/dist/xml/index.js +2 -0
  102. package/dist/xml/xml-parser.d.ts +13 -0
  103. package/dist/xml/xml-parser.js +271 -0
  104. package/package.json +38 -0
@@ -0,0 +1,1223 @@
1
+ import { layoutProcess } from "../layout/layout-engine.js";
2
+ import { generateId } from "../types/id-generator.js";
3
+ import { restConnectorRetries, restConnectorTaskType, restConnectorToIoMappingInputs, restConnectorToTaskHeaders, } from "./rest-connector.js";
4
+ import { zeebeExtensionsToXmlElements } from "./zeebe-extensions.js";
5
+ // ---------------------------------------------------------------------------
6
+ // Internal helpers
7
+ // ---------------------------------------------------------------------------
8
+ function buildEventDefinitions(opts, rootErrors, rootMessages) {
9
+ const defs = [];
10
+ if (opts.timerDuration || opts.timerDate || opts.timerCycle) {
11
+ defs.push({
12
+ type: "timer",
13
+ timeDuration: opts.timerDuration,
14
+ timeDate: opts.timerDate,
15
+ timeCycle: opts.timerCycle,
16
+ });
17
+ }
18
+ if (opts.errorCode !== undefined || opts.errorRef !== undefined) {
19
+ let errorRef = opts.errorRef;
20
+ if (!errorRef && opts.errorCode !== undefined && rootErrors) {
21
+ const errorId = generateId("Error");
22
+ rootErrors.push({ id: errorId, name: opts.errorCode, errorCode: opts.errorCode });
23
+ errorRef = errorId;
24
+ }
25
+ defs.push({ type: "error", errorRef });
26
+ }
27
+ if (opts.messageName !== undefined) {
28
+ let messageRef = opts.messageName;
29
+ if (rootMessages) {
30
+ const messageId = generateId("Message");
31
+ rootMessages.push({ id: messageId, name: opts.messageName, unknownAttributes: {} });
32
+ messageRef = messageId;
33
+ }
34
+ defs.push({ type: "message", messageRef });
35
+ }
36
+ if (opts.signalName !== undefined) {
37
+ defs.push({ type: "signal", signalRef: opts.signalName });
38
+ }
39
+ if (opts.escalationCode !== undefined) {
40
+ defs.push({ type: "escalation", escalationRef: opts.escalationCode });
41
+ }
42
+ return defs;
43
+ }
44
+ function makeFlowElement(id, type, options) {
45
+ const base = {
46
+ id,
47
+ name: options?.name,
48
+ incoming: [],
49
+ outgoing: [],
50
+ extensionElements: options?.extensionElements ?? [],
51
+ unknownAttributes: {},
52
+ };
53
+ switch (type) {
54
+ case "startEvent":
55
+ case "endEvent":
56
+ case "intermediateThrowEvent":
57
+ case "intermediateCatchEvent":
58
+ return { ...base, type, eventDefinitions: [] };
59
+ case "boundaryEvent":
60
+ return {
61
+ ...base,
62
+ type: "boundaryEvent",
63
+ attachedToRef: "",
64
+ eventDefinitions: [],
65
+ };
66
+ case "task":
67
+ case "serviceTask":
68
+ case "scriptTask":
69
+ case "userTask":
70
+ case "sendTask":
71
+ case "receiveTask":
72
+ case "businessRuleTask":
73
+ case "manualTask":
74
+ case "callActivity":
75
+ return { ...base, type };
76
+ case "exclusiveGateway":
77
+ case "inclusiveGateway":
78
+ case "complexGateway":
79
+ return { ...base, type };
80
+ case "parallelGateway":
81
+ case "eventBasedGateway":
82
+ return { ...base, type };
83
+ case "subProcess":
84
+ return {
85
+ ...base,
86
+ type: "subProcess",
87
+ flowElements: [],
88
+ sequenceFlows: [],
89
+ textAnnotations: [],
90
+ associations: [],
91
+ };
92
+ case "adHocSubProcess":
93
+ return {
94
+ ...base,
95
+ type: "adHocSubProcess",
96
+ flowElements: [],
97
+ sequenceFlows: [],
98
+ textAnnotations: [],
99
+ associations: [],
100
+ };
101
+ case "eventSubProcess":
102
+ return {
103
+ ...base,
104
+ type: "eventSubProcess",
105
+ flowElements: [],
106
+ sequenceFlows: [],
107
+ textAnnotations: [],
108
+ associations: [],
109
+ };
110
+ case "transaction":
111
+ return {
112
+ ...base,
113
+ type: "transaction",
114
+ flowElements: [],
115
+ sequenceFlows: [],
116
+ textAnnotations: [],
117
+ associations: [],
118
+ };
119
+ }
120
+ }
121
+ function buildMultiInstance(options) {
122
+ const extChildren = [];
123
+ if (options.collection) {
124
+ const attrs = {
125
+ inputCollection: options.collection,
126
+ };
127
+ if (options.elementVariable) {
128
+ attrs.inputElement = options.elementVariable;
129
+ }
130
+ extChildren.push({
131
+ name: "zeebe:loopCharacteristics",
132
+ attributes: attrs,
133
+ children: [],
134
+ });
135
+ }
136
+ return { extensionElements: extChildren };
137
+ }
138
+ function buildAdHocLoopCharacteristics(lc) {
139
+ const attrs = {
140
+ inputCollection: lc.inputCollection,
141
+ };
142
+ if (lc.inputElement)
143
+ attrs.inputElement = lc.inputElement;
144
+ if (lc.outputCollection)
145
+ attrs.outputCollection = lc.outputCollection;
146
+ if (lc.outputElement)
147
+ attrs.outputElement = lc.outputElement;
148
+ return {
149
+ extensionElements: [
150
+ {
151
+ name: "zeebe:loopCharacteristics",
152
+ attributes: attrs,
153
+ children: [],
154
+ },
155
+ ],
156
+ };
157
+ }
158
+ /** Convert a layout engine result into a BPMN diagram interchange structure. */
159
+ function layoutResultToDiagram(processId, layout) {
160
+ const shapes = layout.nodes.map((node) => {
161
+ const shape = {
162
+ id: `${node.id}_di`,
163
+ bpmnElement: node.id,
164
+ bounds: { ...node.bounds },
165
+ unknownAttributes: {},
166
+ };
167
+ if (node.isExpanded !== undefined) {
168
+ shape.isExpanded = node.isExpanded;
169
+ }
170
+ if (node.labelBounds) {
171
+ shape.label = { bounds: { ...node.labelBounds } };
172
+ }
173
+ return shape;
174
+ });
175
+ const edges = layout.edges.map((edge) => {
176
+ const diEdge = {
177
+ id: `${edge.id}_di`,
178
+ bpmnElement: edge.id,
179
+ waypoints: edge.waypoints.map((wp) => ({ ...wp })),
180
+ unknownAttributes: {},
181
+ };
182
+ if (edge.labelBounds) {
183
+ diEdge.label = { bounds: { ...edge.labelBounds } };
184
+ }
185
+ return diEdge;
186
+ });
187
+ const plane = {
188
+ id: `${processId}_di_plane`,
189
+ bpmnElement: processId,
190
+ shapes,
191
+ edges,
192
+ };
193
+ return {
194
+ id: `${processId}_di`,
195
+ plane,
196
+ };
197
+ }
198
+ function recomputeIncomingOutgoing(elements, flows) {
199
+ for (const el of elements) {
200
+ el.incoming = [];
201
+ el.outgoing = [];
202
+ }
203
+ const elementMap = new Map(elements.map((el) => [el.id, el]));
204
+ for (const flow of flows) {
205
+ elementMap.get(flow.sourceRef)?.outgoing.push(flow.id);
206
+ elementMap.get(flow.targetRef)?.incoming.push(flow.id);
207
+ }
208
+ }
209
+ function buildServiceTaskExtensions(options) {
210
+ const extensions = {
211
+ taskDefinition: {
212
+ type: options.taskType,
213
+ retries: options.retries,
214
+ },
215
+ };
216
+ if (options.ioMapping) {
217
+ extensions.ioMapping = {
218
+ inputs: options.ioMapping.inputs ?? [],
219
+ outputs: options.ioMapping.outputs ?? [],
220
+ };
221
+ }
222
+ if (options.taskHeaders) {
223
+ extensions.taskHeaders = {
224
+ headers: Object.entries(options.taskHeaders).map(([key, value]) => ({
225
+ key,
226
+ value,
227
+ })),
228
+ };
229
+ }
230
+ return zeebeExtensionsToXmlElements(extensions);
231
+ }
232
+ function makeConditionExpression(expression) {
233
+ return {
234
+ text: expression,
235
+ attributes: { "xsi:type": "bpmn:tFormalExpression" },
236
+ };
237
+ }
238
+ // ---------------------------------------------------------------------------
239
+ // Branch builder (used inside gateway branch callbacks)
240
+ // ---------------------------------------------------------------------------
241
+ /**
242
+ * Builder for a single named branch path from a gateway.
243
+ *
244
+ * Use `.condition(expr)` to set a FEEL condition on the outgoing sequence
245
+ * flow from the gateway, or `.defaultFlow()` to mark it as the default path.
246
+ */
247
+ export class BranchBuilder {
248
+ /** @internal */
249
+ _elements = [];
250
+ /** @internal */
251
+ _flows = [];
252
+ /** @internal */
253
+ _defaultFlowId;
254
+ lastNodeId;
255
+ gatewayId;
256
+ branchName;
257
+ isFirstElement = true;
258
+ pendingCondition;
259
+ pendingDefault = false;
260
+ /** @internal – true once connectTo() has been called, meaning the branch end is already wired */
261
+ _connected = false;
262
+ /** @internal */
263
+ constructor(gatewayId, branchName) {
264
+ this.gatewayId = gatewayId;
265
+ this.branchName = branchName;
266
+ this.lastNodeId = gatewayId;
267
+ }
268
+ /** Set a FEEL condition expression on this branch's outgoing sequence flow. */
269
+ condition(expression) {
270
+ this.pendingCondition = expression;
271
+ return this;
272
+ }
273
+ /** Mark this branch as the gateway's default (no-condition) flow. */
274
+ defaultFlow() {
275
+ this.pendingDefault = true;
276
+ return this;
277
+ }
278
+ addElement(element) {
279
+ this._elements.push(element);
280
+ const flowId = generateId("Flow");
281
+ const flow = {
282
+ id: flowId,
283
+ sourceRef: this.lastNodeId,
284
+ targetRef: element.id,
285
+ name: this.isFirstElement ? this.branchName : undefined,
286
+ conditionExpression: this.isFirstElement && this.pendingCondition
287
+ ? makeConditionExpression(this.pendingCondition)
288
+ : undefined,
289
+ extensionElements: [],
290
+ unknownAttributes: {},
291
+ };
292
+ this._flows.push(flow);
293
+ if (this.isFirstElement && this.pendingDefault) {
294
+ this._defaultFlowId = flowId;
295
+ }
296
+ this.isFirstElement = false;
297
+ this.lastNodeId = element.id;
298
+ return this;
299
+ }
300
+ /**
301
+ * Connect the current position to an existing or future element by ID.
302
+ * Supports forward references (element created later) and backward references (loops).
303
+ */
304
+ connectTo(targetId) {
305
+ const flowId = generateId("Flow");
306
+ const flow = {
307
+ id: flowId,
308
+ sourceRef: this.lastNodeId,
309
+ targetRef: targetId,
310
+ name: this.isFirstElement ? this.branchName : undefined,
311
+ conditionExpression: this.isFirstElement && this.pendingCondition
312
+ ? makeConditionExpression(this.pendingCondition)
313
+ : undefined,
314
+ extensionElements: [],
315
+ unknownAttributes: {},
316
+ };
317
+ this._flows.push(flow);
318
+ if (this.isFirstElement && this.pendingDefault) {
319
+ this._defaultFlowId = flowId;
320
+ }
321
+ this.isFirstElement = false;
322
+ this.lastNodeId = targetId;
323
+ this._connected = true;
324
+ return this;
325
+ }
326
+ /** @internal – ID of the last element added (or the gateway if branch is empty) */
327
+ get _lastNodeId() {
328
+ return this.lastNodeId;
329
+ }
330
+ // ---- Flow-node methods (mirror ProcessBuilder) ----
331
+ serviceTask(id, options) {
332
+ const unknownAttributes = {};
333
+ if (options.modelerTemplate)
334
+ unknownAttributes["zeebe:modelerTemplate"] = options.modelerTemplate;
335
+ if (options.modelerTemplateVersion)
336
+ unknownAttributes["zeebe:modelerTemplateVersion"] = options.modelerTemplateVersion;
337
+ if (options.modelerTemplateIcon)
338
+ unknownAttributes["zeebe:modelerTemplateIcon"] = options.modelerTemplateIcon;
339
+ const el = makeFlowElement(id, "serviceTask", {
340
+ name: options.name,
341
+ extensionElements: buildServiceTaskExtensions(options),
342
+ });
343
+ el.unknownAttributes = unknownAttributes;
344
+ return this.addElement(el);
345
+ }
346
+ userTask(id, options) {
347
+ const ext = options?.formId
348
+ ? zeebeExtensionsToXmlElements({ formDefinition: { formId: options.formId } })
349
+ : [];
350
+ return this.addElement(makeFlowElement(id, "userTask", {
351
+ name: options?.name,
352
+ extensionElements: ext,
353
+ }));
354
+ }
355
+ scriptTask(id, options) {
356
+ return this.addElement(makeFlowElement(id, "scriptTask", {
357
+ name: options.name,
358
+ extensionElements: zeebeExtensionsToXmlElements({
359
+ unknownElements: [
360
+ {
361
+ name: "zeebe:script",
362
+ attributes: {
363
+ expression: options.expression,
364
+ resultVariable: options.resultVariable,
365
+ },
366
+ children: [],
367
+ },
368
+ ],
369
+ }),
370
+ }));
371
+ }
372
+ sendTask(id, options) {
373
+ return this.addElement(makeFlowElement(id, "sendTask", options));
374
+ }
375
+ receiveTask(id, options) {
376
+ return this.addElement(makeFlowElement(id, "receiveTask", options));
377
+ }
378
+ businessRuleTask(id, options) {
379
+ const ext = options?.decisionId
380
+ ? zeebeExtensionsToXmlElements({
381
+ calledDecision: {
382
+ decisionId: options.decisionId,
383
+ resultVariable: options.resultVariable ?? "result",
384
+ },
385
+ })
386
+ : [];
387
+ return this.addElement(makeFlowElement(id, "businessRuleTask", {
388
+ name: options?.name,
389
+ extensionElements: ext,
390
+ }));
391
+ }
392
+ callActivity(id, options) {
393
+ const attrs = { processId: options.processId };
394
+ if (options.propagateAllChildVariables !== undefined) {
395
+ attrs.propagateAllChildVariables = String(options.propagateAllChildVariables);
396
+ }
397
+ return this.addElement(makeFlowElement(id, "callActivity", {
398
+ name: options.name,
399
+ extensionElements: zeebeExtensionsToXmlElements({
400
+ unknownElements: [
401
+ {
402
+ name: "zeebe:calledElement",
403
+ attributes: attrs,
404
+ children: [],
405
+ },
406
+ ],
407
+ }),
408
+ }));
409
+ }
410
+ startEvent(id, options) {
411
+ const el = makeFlowElement(id ?? generateId("StartEvent"), "startEvent", options);
412
+ if (el.type === "startEvent" &&
413
+ (options?.timerDuration || options?.timerCycle || options?.timerDate)) {
414
+ el.eventDefinitions = buildEventDefinitions(options);
415
+ }
416
+ return this.addElement(el);
417
+ }
418
+ endEvent(id, options) {
419
+ return this.addElement(makeFlowElement(id ?? generateId("EndEvent"), "endEvent", options));
420
+ }
421
+ intermediateThrowEvent(id, options) {
422
+ const el = makeFlowElement(id ?? generateId("IntermediateThrowEvent"), "intermediateThrowEvent", options);
423
+ if (el.type === "intermediateThrowEvent" && options) {
424
+ el.eventDefinitions = buildEventDefinitions(options);
425
+ }
426
+ return this.addElement(el);
427
+ }
428
+ intermediateCatchEvent(id, options) {
429
+ const el = makeFlowElement(id ?? generateId("IntermediateCatchEvent"), "intermediateCatchEvent", options);
430
+ if (el.type === "intermediateCatchEvent" && options) {
431
+ el.eventDefinitions = buildEventDefinitions(options);
432
+ }
433
+ return this.addElement(el);
434
+ }
435
+ exclusiveGateway(id, options) {
436
+ const el = makeFlowElement(id, "exclusiveGateway", options);
437
+ if (options?.defaultFlow && el.type === "exclusiveGateway") {
438
+ ;
439
+ el.default = options.defaultFlow;
440
+ }
441
+ return this.addElement(el);
442
+ }
443
+ parallelGateway(id, options) {
444
+ return this.addElement(makeFlowElement(id, "parallelGateway", options));
445
+ }
446
+ inclusiveGateway(id, options) {
447
+ const el = makeFlowElement(id, "inclusiveGateway", options);
448
+ if (options?.defaultFlow && el.type === "inclusiveGateway") {
449
+ ;
450
+ el.default = options.defaultFlow;
451
+ }
452
+ return this.addElement(el);
453
+ }
454
+ eventBasedGateway(id, options) {
455
+ return this.addElement(makeFlowElement(id, "eventBasedGateway", options));
456
+ }
457
+ }
458
+ // ---------------------------------------------------------------------------
459
+ // Sub-process content builder
460
+ // ---------------------------------------------------------------------------
461
+ /** Builder for the contents of a sub-process or ad-hoc sub-process. */
462
+ export class SubProcessContentBuilder {
463
+ /** @internal */
464
+ _elements = [];
465
+ /** @internal */
466
+ _flows = [];
467
+ lastNodeId;
468
+ addElement(element) {
469
+ this._elements.push(element);
470
+ if (this.lastNodeId) {
471
+ const flowId = generateId("Flow");
472
+ this._flows.push({
473
+ id: flowId,
474
+ sourceRef: this.lastNodeId,
475
+ targetRef: element.id,
476
+ extensionElements: [],
477
+ unknownAttributes: {},
478
+ });
479
+ }
480
+ this.lastNodeId = element.id;
481
+ return this;
482
+ }
483
+ startEvent(id, options) {
484
+ const el = makeFlowElement(id ?? generateId("StartEvent"), "startEvent", options);
485
+ if (el.type === "startEvent" && options) {
486
+ el.eventDefinitions = buildEventDefinitions(options);
487
+ }
488
+ return this.addElement(el);
489
+ }
490
+ endEvent(id, options) {
491
+ return this.addElement(makeFlowElement(id ?? generateId("EndEvent"), "endEvent", options));
492
+ }
493
+ serviceTask(id, options) {
494
+ const unknownAttributes = {};
495
+ if (options.modelerTemplate)
496
+ unknownAttributes["zeebe:modelerTemplate"] = options.modelerTemplate;
497
+ if (options.modelerTemplateVersion)
498
+ unknownAttributes["zeebe:modelerTemplateVersion"] = options.modelerTemplateVersion;
499
+ if (options.modelerTemplateIcon)
500
+ unknownAttributes["zeebe:modelerTemplateIcon"] = options.modelerTemplateIcon;
501
+ const el = makeFlowElement(id, "serviceTask", {
502
+ name: options.name,
503
+ extensionElements: buildServiceTaskExtensions(options),
504
+ });
505
+ el.unknownAttributes = unknownAttributes;
506
+ return this.addElement(el);
507
+ }
508
+ userTask(id, options) {
509
+ return this.addElement(makeFlowElement(id, "userTask", { name: options?.name }));
510
+ }
511
+ scriptTask(id, options) {
512
+ return this.addElement(makeFlowElement(id, "scriptTask", {
513
+ name: options.name,
514
+ extensionElements: zeebeExtensionsToXmlElements({
515
+ unknownElements: [
516
+ {
517
+ name: "zeebe:script",
518
+ attributes: {
519
+ expression: options.expression,
520
+ resultVariable: options.resultVariable,
521
+ },
522
+ children: [],
523
+ },
524
+ ],
525
+ }),
526
+ }));
527
+ }
528
+ callActivity(id, options) {
529
+ const attrs = { processId: options.processId };
530
+ if (options.propagateAllChildVariables !== undefined) {
531
+ attrs.propagateAllChildVariables = String(options.propagateAllChildVariables);
532
+ }
533
+ return this.addElement(makeFlowElement(id, "callActivity", {
534
+ name: options.name,
535
+ extensionElements: zeebeExtensionsToXmlElements({
536
+ unknownElements: [
537
+ {
538
+ name: "zeebe:calledElement",
539
+ attributes: attrs,
540
+ children: [],
541
+ },
542
+ ],
543
+ }),
544
+ }));
545
+ }
546
+ intermediateThrowEvent(id, options) {
547
+ const el = makeFlowElement(id ?? generateId("IntermediateThrowEvent"), "intermediateThrowEvent", options);
548
+ if (el.type === "intermediateThrowEvent" && options) {
549
+ el.eventDefinitions = buildEventDefinitions(options);
550
+ }
551
+ return this.addElement(el);
552
+ }
553
+ intermediateCatchEvent(id, options) {
554
+ const el = makeFlowElement(id ?? generateId("IntermediateCatchEvent"), "intermediateCatchEvent", options);
555
+ if (el.type === "intermediateCatchEvent" && options) {
556
+ el.eventDefinitions = buildEventDefinitions(options);
557
+ }
558
+ return this.addElement(el);
559
+ }
560
+ }
561
+ // ---------------------------------------------------------------------------
562
+ // Process builder (top-level entry point)
563
+ // ---------------------------------------------------------------------------
564
+ /** Fluent builder for constructing BPMN processes. */
565
+ export class ProcessBuilder {
566
+ processId;
567
+ processName;
568
+ _isExecutable = true;
569
+ _versionTag;
570
+ flowElements = [];
571
+ sequenceFlows = [];
572
+ rootErrors = [];
573
+ rootMessages = [];
574
+ lastNodeId;
575
+ currentGatewayId;
576
+ openBranchEnds = [];
577
+ _autoLayout = false;
578
+ constructor(processId) {
579
+ this.processId = processId;
580
+ }
581
+ /** Enable auto-layout: `build()` will run the layout engine and populate diagram interchange data. */
582
+ withAutoLayout() {
583
+ this._autoLayout = true;
584
+ return this;
585
+ }
586
+ /** Set the display name for this process. */
587
+ name(name) {
588
+ this.processName = name;
589
+ return this;
590
+ }
591
+ /** Set whether this process is executable. */
592
+ executable(value) {
593
+ this._isExecutable = value;
594
+ return this;
595
+ }
596
+ /** Set the process version tag. */
597
+ versionTag(tag) {
598
+ this._versionTag = tag;
599
+ return this;
600
+ }
601
+ // ---- Events ----
602
+ /** Add a start event. Start events never auto-connect from the previous element. */
603
+ startEvent(id, options) {
604
+ const nodeId = id ?? generateId("StartEvent");
605
+ const extElements = [];
606
+ if (options?.zeebeProperties) {
607
+ extElements.push(...zeebeExtensionsToXmlElements({
608
+ properties: { properties: options.zeebeProperties },
609
+ }));
610
+ }
611
+ const element = makeFlowElement(nodeId, "startEvent", {
612
+ name: options?.name,
613
+ extensionElements: extElements,
614
+ });
615
+ if (element.type === "startEvent" && options) {
616
+ element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages);
617
+ }
618
+ if (options?.modelerTemplate) {
619
+ element.unknownAttributes["zeebe:modelerTemplate"] = options.modelerTemplate;
620
+ }
621
+ if (options?.modelerTemplateVersion) {
622
+ element.unknownAttributes["zeebe:modelerTemplateVersion"] = options.modelerTemplateVersion;
623
+ }
624
+ if (options?.modelerTemplateIcon) {
625
+ element.unknownAttributes["zeebe:modelerTemplateIcon"] = options.modelerTemplateIcon;
626
+ }
627
+ this.addFlowElement(element);
628
+ return this;
629
+ }
630
+ /**
631
+ * Add a disconnected start event — begins a new parallel path in the process.
632
+ *
633
+ * Unlike `startEvent()` which continues from the current position,
634
+ * `addStartEvent()` clears the current position first so the start event
635
+ * is completely disconnected.
636
+ */
637
+ addStartEvent(id, options) {
638
+ this.lastNodeId = undefined;
639
+ this.currentGatewayId = undefined;
640
+ return this.startEvent(id, options);
641
+ }
642
+ /** Add an end event. */
643
+ endEvent(id, options) {
644
+ const nodeId = id ?? generateId("EndEvent");
645
+ this.addFlowElement(makeFlowElement(nodeId, "endEvent", options));
646
+ return this;
647
+ }
648
+ /** Add an intermediate throw event (none, message, signal, escalation). */
649
+ intermediateThrowEvent(id, options) {
650
+ const nodeId = id ?? generateId("IntermediateThrowEvent");
651
+ const element = makeFlowElement(nodeId, "intermediateThrowEvent", options);
652
+ if (element.type === "intermediateThrowEvent" && options) {
653
+ element.eventDefinitions = buildEventDefinitions(options);
654
+ }
655
+ this.addFlowElement(element);
656
+ return this;
657
+ }
658
+ /** Add an intermediate catch event (timer, message, signal). */
659
+ intermediateCatchEvent(id, options) {
660
+ const nodeId = id ?? generateId("IntermediateCatchEvent");
661
+ const element = makeFlowElement(nodeId, "intermediateCatchEvent", options);
662
+ if (element.type === "intermediateCatchEvent" && options) {
663
+ element.eventDefinitions = buildEventDefinitions(options);
664
+ }
665
+ this.addFlowElement(element);
666
+ return this;
667
+ }
668
+ /**
669
+ * Add a boundary event attached to an existing activity.
670
+ *
671
+ * Boundary events do not auto-connect from the previous element.
672
+ * They start a new outgoing chain from the boundary event itself.
673
+ */
674
+ boundaryEvent(id, options) {
675
+ const element = makeFlowElement(id, "boundaryEvent", options);
676
+ if (element.type === "boundaryEvent") {
677
+ element.attachedToRef = options.attachedTo;
678
+ element.cancelActivity = options.cancelActivity;
679
+ element.eventDefinitions = buildEventDefinitions(options, this.rootErrors, this.rootMessages);
680
+ }
681
+ // Boundary events never auto-connect — temporarily clear lastNodeId
682
+ const prevLast = this.lastNodeId;
683
+ this.lastNodeId = undefined;
684
+ this.addFlowElement(element);
685
+ // Don't restore prevLast — the builder now chains from the boundary event
686
+ void prevLast;
687
+ return this;
688
+ }
689
+ // ---- Tasks ----
690
+ /** Add a service task with Zeebe task definition and optional IO mappings. */
691
+ serviceTask(id, options) {
692
+ const unknownAttributes = {};
693
+ if (options.modelerTemplate)
694
+ unknownAttributes["zeebe:modelerTemplate"] = options.modelerTemplate;
695
+ if (options.modelerTemplateVersion)
696
+ unknownAttributes["zeebe:modelerTemplateVersion"] = options.modelerTemplateVersion;
697
+ if (options.modelerTemplateIcon)
698
+ unknownAttributes["zeebe:modelerTemplateIcon"] = options.modelerTemplateIcon;
699
+ const el = makeFlowElement(id, "serviceTask", {
700
+ name: options.name,
701
+ extensionElements: buildServiceTaskExtensions(options),
702
+ });
703
+ el.unknownAttributes = unknownAttributes;
704
+ this.addFlowElement(el);
705
+ return this;
706
+ }
707
+ /** Add a REST connector task — syntactic sugar over `serviceTask()`. */
708
+ restConnector(id, config) {
709
+ const inputs = restConnectorToIoMappingInputs(config);
710
+ const taskHeaderEntries = restConnectorToTaskHeaders(config);
711
+ const extensions = {
712
+ taskDefinition: {
713
+ type: restConnectorTaskType(),
714
+ retries: restConnectorRetries(config),
715
+ },
716
+ ioMapping: { inputs, outputs: [] },
717
+ };
718
+ if (taskHeaderEntries.length > 0) {
719
+ extensions.taskHeaders = { headers: taskHeaderEntries };
720
+ }
721
+ const el = makeFlowElement(id, "serviceTask", {
722
+ name: config.name,
723
+ extensionElements: zeebeExtensionsToXmlElements(extensions),
724
+ });
725
+ // Stamp the template identifier so editors recognise this as a REST connector
726
+ el.unknownAttributes = {
727
+ "zeebe:modelerTemplate": "io.camunda.connectors.HttpJson.v2",
728
+ "zeebe:modelerTemplateVersion": "12",
729
+ };
730
+ this.addFlowElement(el);
731
+ return this;
732
+ }
733
+ /** Add a script task with a FEEL expression. */
734
+ scriptTask(id, options) {
735
+ this.addFlowElement(makeFlowElement(id, "scriptTask", {
736
+ name: options.name,
737
+ extensionElements: zeebeExtensionsToXmlElements({
738
+ unknownElements: [
739
+ {
740
+ name: "zeebe:script",
741
+ attributes: {
742
+ expression: options.expression,
743
+ resultVariable: options.resultVariable,
744
+ },
745
+ children: [],
746
+ },
747
+ ],
748
+ }),
749
+ }));
750
+ return this;
751
+ }
752
+ /** Add a user task with optional form reference. */
753
+ userTask(id, options) {
754
+ const extensionElements = options?.formId
755
+ ? zeebeExtensionsToXmlElements({ formDefinition: { formId: options.formId } })
756
+ : [];
757
+ this.addFlowElement(makeFlowElement(id, "userTask", {
758
+ name: options?.name,
759
+ extensionElements,
760
+ }));
761
+ return this;
762
+ }
763
+ /** Add a send task (aspirational). */
764
+ sendTask(id, options) {
765
+ this.addFlowElement(makeFlowElement(id, "sendTask", options));
766
+ return this;
767
+ }
768
+ /** Add a receive task (aspirational). */
769
+ receiveTask(id, options) {
770
+ this.addFlowElement(makeFlowElement(id, "receiveTask", options));
771
+ return this;
772
+ }
773
+ /** Add a business rule task. */
774
+ businessRuleTask(id, options) {
775
+ const ext = [];
776
+ if (options?.taskType) {
777
+ ext.push(...zeebeExtensionsToXmlElements({ taskDefinition: { type: options.taskType } }));
778
+ }
779
+ if (options?.decisionId) {
780
+ ext.push(...zeebeExtensionsToXmlElements({
781
+ calledDecision: {
782
+ decisionId: options.decisionId,
783
+ resultVariable: options.resultVariable ?? "result",
784
+ },
785
+ }));
786
+ }
787
+ this.addFlowElement(makeFlowElement(id, "businessRuleTask", {
788
+ name: options?.name,
789
+ extensionElements: ext,
790
+ }));
791
+ return this;
792
+ }
793
+ /** Add a call activity referencing another process. */
794
+ callActivity(id, options) {
795
+ const attrs = {
796
+ processId: options.processId,
797
+ };
798
+ if (options.propagateAllChildVariables !== undefined) {
799
+ attrs.propagateAllChildVariables = String(options.propagateAllChildVariables);
800
+ }
801
+ this.addFlowElement(makeFlowElement(id, "callActivity", {
802
+ name: options.name,
803
+ extensionElements: zeebeExtensionsToXmlElements({
804
+ unknownElements: [
805
+ {
806
+ name: "zeebe:calledElement",
807
+ attributes: attrs,
808
+ children: [],
809
+ },
810
+ ],
811
+ }),
812
+ }));
813
+ return this;
814
+ }
815
+ // ---- Gateways ----
816
+ /** Add an exclusive gateway (XOR split/join). */
817
+ exclusiveGateway(id, options) {
818
+ const element = makeFlowElement(id, "exclusiveGateway", options);
819
+ if (options?.defaultFlow && element.type === "exclusiveGateway") {
820
+ ;
821
+ element.default = options.defaultFlow;
822
+ }
823
+ this.addFlowElement(element);
824
+ this.currentGatewayId = id;
825
+ return this;
826
+ }
827
+ /** Add a parallel gateway (AND split/join). */
828
+ parallelGateway(id, options) {
829
+ this.addFlowElement(makeFlowElement(id, "parallelGateway", options));
830
+ this.currentGatewayId = id;
831
+ return this;
832
+ }
833
+ /** Add an inclusive gateway (OR split/join). Aspirational. */
834
+ inclusiveGateway(id, options) {
835
+ const element = makeFlowElement(id, "inclusiveGateway", options);
836
+ if (options?.defaultFlow && element.type === "inclusiveGateway") {
837
+ ;
838
+ element.default = options.defaultFlow;
839
+ }
840
+ this.addFlowElement(element);
841
+ this.currentGatewayId = id;
842
+ return this;
843
+ }
844
+ /** Add an event-based gateway. Aspirational. */
845
+ eventBasedGateway(id, options) {
846
+ this.addFlowElement(makeFlowElement(id, "eventBasedGateway", options));
847
+ this.currentGatewayId = id;
848
+ return this;
849
+ }
850
+ // ---- Branching & flow control ----
851
+ /**
852
+ * Create a named branch from the current gateway.
853
+ *
854
+ * Each branch receives a {@link BranchBuilder} whose chain starts from
855
+ * the gateway. Use `.condition(expr)` or `.defaultFlow()` to configure
856
+ * the outgoing sequence flow, then chain flow-node methods, and finish
857
+ * with `.connectTo(targetId)` to merge into another element.
858
+ *
859
+ * @example
860
+ * ```ts
861
+ * .exclusiveGateway("check")
862
+ * .branch("yes", b => b.condition("= ok").serviceTask("do", { taskType: "x" }).connectTo("merge"))
863
+ * .branch("no", b => b.defaultFlow().serviceTask("fail", { taskType: "y" }).connectTo("merge"))
864
+ * .exclusiveGateway("merge")
865
+ * .endEvent("end")
866
+ * ```
867
+ */
868
+ branch(name, callback) {
869
+ if (!this.currentGatewayId) {
870
+ throw new Error("branch() must be called after a gateway element");
871
+ }
872
+ const b = new BranchBuilder(this.currentGatewayId, name);
873
+ callback(b);
874
+ for (const el of b._elements) {
875
+ if (this.flowElements.some((n) => n.id === el.id)) {
876
+ throw new Error(`Duplicate element ID "${el.id}"`);
877
+ }
878
+ this.flowElements.push(el);
879
+ }
880
+ for (const fl of b._flows) {
881
+ this.sequenceFlows.push(fl);
882
+ }
883
+ // If the branch is the default flow, set the gateway's default
884
+ if (b._defaultFlowId) {
885
+ const gateway = this.flowElements.find((n) => n.id === this.currentGatewayId);
886
+ if (gateway && (gateway.type === "exclusiveGateway" || gateway.type === "inclusiveGateway")) {
887
+ gateway.default = b._defaultFlowId;
888
+ }
889
+ }
890
+ // Track the branch's open end so the next element auto-connects from it.
891
+ // Skip branches that terminated at an end event (those are intentional dead-ends).
892
+ if (!b._connected && b._elements.length > 0) {
893
+ const lastEl = b._elements[b._elements.length - 1];
894
+ if (lastEl && lastEl.type !== "endEvent") {
895
+ this.openBranchEnds.push(b._lastNodeId);
896
+ }
897
+ }
898
+ this.lastNodeId = undefined;
899
+ return this;
900
+ }
901
+ /** Connect the current position to an existing or future element by ID. */
902
+ connectTo(targetId) {
903
+ if (this.lastNodeId) {
904
+ const flowId = generateId("Flow");
905
+ this.sequenceFlows.push({
906
+ id: flowId,
907
+ sourceRef: this.lastNodeId,
908
+ targetRef: targetId,
909
+ extensionElements: [],
910
+ unknownAttributes: {},
911
+ });
912
+ }
913
+ this.lastNodeId = undefined;
914
+ return this;
915
+ }
916
+ /**
917
+ * Position the builder at an existing element, allowing additional
918
+ * outgoing flows from any point in the graph.
919
+ *
920
+ * @throws If no element with the given ID exists.
921
+ */
922
+ element(elementId) {
923
+ const found = this.flowElements.some((n) => n.id === elementId);
924
+ if (!found) {
925
+ throw new Error(`Element "${elementId}" not found in process "${this.processId}"`);
926
+ }
927
+ this.lastNodeId = elementId;
928
+ this.currentGatewayId = undefined;
929
+ return this;
930
+ }
931
+ // ---- Sub-processes ----
932
+ /** Add an ad-hoc sub-process with optional AI agent or multi-instance configuration. */
933
+ adHocSubProcess(id, content, options) {
934
+ const sub = new SubProcessContentBuilder();
935
+ content(sub);
936
+ recomputeIncomingOutgoing(sub._elements, sub._flows);
937
+ const zeebeExt = {};
938
+ if (options?.taskDefinition) {
939
+ zeebeExt.taskDefinition = options.taskDefinition;
940
+ }
941
+ if (options?.ioMapping) {
942
+ zeebeExt.ioMapping = {
943
+ inputs: options.ioMapping.inputs ?? [],
944
+ outputs: options.ioMapping.outputs ?? [],
945
+ };
946
+ }
947
+ if (options?.taskHeaders) {
948
+ zeebeExt.taskHeaders = {
949
+ headers: Object.entries(options.taskHeaders).map(([key, value]) => ({ key, value })),
950
+ };
951
+ }
952
+ const extensionElements = zeebeExtensionsToXmlElements(zeebeExt);
953
+ // zeebe:adHoc element
954
+ const adHocAttrs = {};
955
+ if (options?.activeElementsCollection) {
956
+ adHocAttrs.activeElementsCollection = options.activeElementsCollection;
957
+ }
958
+ if (options?.outputCollection) {
959
+ adHocAttrs.outputCollection = options.outputCollection;
960
+ }
961
+ if (options?.outputElement) {
962
+ adHocAttrs.outputElement = options.outputElement;
963
+ }
964
+ if (Object.keys(adHocAttrs).length > 0) {
965
+ extensionElements.push({
966
+ name: "zeebe:adHoc",
967
+ attributes: adHocAttrs,
968
+ children: [],
969
+ });
970
+ }
971
+ const element = makeFlowElement(id, "adHocSubProcess", {
972
+ name: options?.name,
973
+ extensionElements,
974
+ });
975
+ if (options?.modelerTemplate) {
976
+ element.unknownAttributes["zeebe:modelerTemplate"] = options.modelerTemplate;
977
+ }
978
+ if (options?.modelerTemplateVersion) {
979
+ element.unknownAttributes["zeebe:modelerTemplateVersion"] = options.modelerTemplateVersion;
980
+ }
981
+ if (options?.modelerTemplateIcon) {
982
+ element.unknownAttributes["zeebe:modelerTemplateIcon"] = options.modelerTemplateIcon;
983
+ }
984
+ if (element.type === "adHocSubProcess") {
985
+ element.flowElements = sub._elements;
986
+ element.sequenceFlows = sub._flows;
987
+ if (options?.loopCharacteristics) {
988
+ element.loopCharacteristics = buildAdHocLoopCharacteristics(options.loopCharacteristics);
989
+ }
990
+ else if (options?.multiInstance) {
991
+ element.loopCharacteristics = buildMultiInstance(options.multiInstance);
992
+ }
993
+ }
994
+ this.addFlowElement(element);
995
+ return this;
996
+ }
997
+ /** Add a sub-process (aspirational). */
998
+ subProcess(id, content, options) {
999
+ const sub = new SubProcessContentBuilder();
1000
+ content(sub);
1001
+ recomputeIncomingOutgoing(sub._elements, sub._flows);
1002
+ const element = makeFlowElement(id, "subProcess", options);
1003
+ if (element.type === "subProcess") {
1004
+ element.flowElements = sub._elements;
1005
+ element.sequenceFlows = sub._flows;
1006
+ if (options?.multiInstance) {
1007
+ element.loopCharacteristics = buildMultiInstance(options.multiInstance);
1008
+ }
1009
+ }
1010
+ this.addFlowElement(element);
1011
+ return this;
1012
+ }
1013
+ /** Add an event sub-process (aspirational). */
1014
+ eventSubProcess(id, content, options) {
1015
+ const sub = new SubProcessContentBuilder();
1016
+ content(sub);
1017
+ recomputeIncomingOutgoing(sub._elements, sub._flows);
1018
+ const element = makeFlowElement(id, "eventSubProcess", options);
1019
+ if (element.type === "eventSubProcess") {
1020
+ element.flowElements = sub._elements;
1021
+ element.sequenceFlows = sub._flows;
1022
+ }
1023
+ this.addFlowElement(element);
1024
+ return this;
1025
+ }
1026
+ // ---- Build ----
1027
+ /**
1028
+ * Build the complete BPMN definitions model.
1029
+ *
1030
+ * Resolves all forward-referenced `incoming` / `outgoing` arrays and wraps
1031
+ * the process in a {@link BpmnDefinitions} ready for XML serialization.
1032
+ */
1033
+ build() {
1034
+ this.insertJoinGateways();
1035
+ recomputeIncomingOutgoing(this.flowElements, this.sequenceFlows);
1036
+ const extensionElements = [];
1037
+ if (this._versionTag) {
1038
+ extensionElements.push({
1039
+ name: "zeebe:versionTag",
1040
+ attributes: { value: this._versionTag },
1041
+ children: [],
1042
+ });
1043
+ }
1044
+ const process = {
1045
+ id: this.processId,
1046
+ name: this.processName,
1047
+ isExecutable: this._isExecutable,
1048
+ extensionElements,
1049
+ flowElements: this.flowElements,
1050
+ sequenceFlows: this.sequenceFlows,
1051
+ textAnnotations: [],
1052
+ associations: [],
1053
+ unknownAttributes: {},
1054
+ };
1055
+ return {
1056
+ id: "Definitions_1",
1057
+ targetNamespace: "http://bpmn.io/schema/bpmn",
1058
+ exporter: "@bpmnkit/core",
1059
+ exporterVersion: "0.0.1",
1060
+ namespaces: {
1061
+ bpmn: "http://www.omg.org/spec/BPMN/20100524/MODEL",
1062
+ bpmndi: "http://www.omg.org/spec/BPMN/20100524/DI",
1063
+ dc: "http://www.omg.org/spec/DD/20100524/DC",
1064
+ di: "http://www.omg.org/spec/DD/20100524/DI",
1065
+ zeebe: "http://camunda.org/schema/zeebe/1.0",
1066
+ modeler: "http://camunda.org/schema/modeler/1.0",
1067
+ xsi: "http://www.w3.org/2001/XMLSchema-instance",
1068
+ },
1069
+ unknownAttributes: {
1070
+ "modeler:executionPlatform": "Camunda Cloud",
1071
+ "modeler:executionPlatformVersion": "8.6.0",
1072
+ },
1073
+ errors: this.rootErrors,
1074
+ escalations: [],
1075
+ messages: this.rootMessages,
1076
+ collaborations: [],
1077
+ processes: [process],
1078
+ diagrams: this._autoLayout ? [this.buildDiagram(process)] : [],
1079
+ };
1080
+ }
1081
+ buildDiagram(process) {
1082
+ const layoutResult = layoutProcess(process);
1083
+ return layoutResultToDiagram(this.processId, layoutResult);
1084
+ }
1085
+ // ---- Internal ----
1086
+ /**
1087
+ * Insert matching join gateways where split-gateway branches converge
1088
+ * on a non-gateway target. BPMN best practice: every split has a join.
1089
+ */
1090
+ insertJoinGateways() {
1091
+ const GATEWAY_TYPES = new Set([
1092
+ "exclusiveGateway",
1093
+ "parallelGateway",
1094
+ "inclusiveGateway",
1095
+ "eventBasedGateway",
1096
+ ]);
1097
+ const elementTypes = new Map();
1098
+ for (const el of this.flowElements) {
1099
+ elementTypes.set(el.id, el.type);
1100
+ }
1101
+ // Find split gateways (2+ outgoing flows)
1102
+ const outCount = new Map();
1103
+ for (const flow of this.sequenceFlows) {
1104
+ outCount.set(flow.sourceRef, (outCount.get(flow.sourceRef) ?? 0) + 1);
1105
+ }
1106
+ const splitGateways = new Set();
1107
+ for (const [id, count] of outCount) {
1108
+ const type = elementTypes.get(id);
1109
+ if (type && GATEWAY_TYPES.has(type) && count >= 2) {
1110
+ splitGateways.add(id);
1111
+ }
1112
+ }
1113
+ if (splitGateways.size === 0)
1114
+ return;
1115
+ // Build incoming flow map
1116
+ const incoming = new Map();
1117
+ for (const flow of this.sequenceFlows) {
1118
+ const arr = incoming.get(flow.targetRef);
1119
+ if (arr)
1120
+ arr.push(flow);
1121
+ else
1122
+ incoming.set(flow.targetRef, [flow]);
1123
+ }
1124
+ // For each target with 2+ incoming flows, check if they trace back
1125
+ // to the same split gateway → insert a join gateway if needed
1126
+ for (const [targetId, inFlows] of incoming) {
1127
+ if (inFlows.length < 2)
1128
+ continue;
1129
+ // Group incoming flows by originating split gateway
1130
+ const splitToFlows = new Map();
1131
+ for (const flow of inFlows) {
1132
+ const split = this.traceBackToSplit(flow.sourceRef, splitGateways);
1133
+ if (split) {
1134
+ const arr = splitToFlows.get(split);
1135
+ if (arr)
1136
+ arr.push(flow);
1137
+ else
1138
+ splitToFlows.set(split, [flow]);
1139
+ }
1140
+ }
1141
+ for (const [splitId, convergingFlows] of splitToFlows) {
1142
+ if (convergingFlows.length < 2)
1143
+ continue;
1144
+ const gwType = elementTypes.get(splitId);
1145
+ if (!gwType)
1146
+ continue;
1147
+ // Don't insert if target is already a matching gateway type
1148
+ const targetType = elementTypes.get(targetId);
1149
+ if (targetType === gwType)
1150
+ continue;
1151
+ const joinId = `${splitId}_join`;
1152
+ if (elementTypes.has(joinId))
1153
+ continue;
1154
+ const joinElement = makeFlowElement(joinId, gwType, {});
1155
+ this.flowElements.push(joinElement);
1156
+ elementTypes.set(joinId, gwType);
1157
+ // Re-route converging flows to the join gateway
1158
+ for (const flow of convergingFlows) {
1159
+ flow.targetRef = joinId;
1160
+ }
1161
+ // Add flow from join to original target
1162
+ this.sequenceFlows.push({
1163
+ id: generateId("Flow"),
1164
+ sourceRef: joinId,
1165
+ targetRef: targetId,
1166
+ extensionElements: [],
1167
+ unknownAttributes: {},
1168
+ });
1169
+ }
1170
+ }
1171
+ }
1172
+ /** Trace backward from a node to find which split gateway it belongs to. */
1173
+ traceBackToSplit(nodeId, splitGateways) {
1174
+ const visited = new Set();
1175
+ let current = nodeId;
1176
+ while (current) {
1177
+ if (visited.has(current))
1178
+ return undefined;
1179
+ visited.add(current);
1180
+ if (splitGateways.has(current))
1181
+ return current;
1182
+ // Follow single incoming flow backward
1183
+ const inFlows = this.sequenceFlows.filter((f) => f.targetRef === current);
1184
+ if (inFlows.length !== 1)
1185
+ return undefined;
1186
+ const prev = inFlows[0];
1187
+ if (!prev)
1188
+ return undefined;
1189
+ current = prev.sourceRef;
1190
+ }
1191
+ return undefined;
1192
+ }
1193
+ addFlowElement(element) {
1194
+ if (this.flowElements.some((n) => n.id === element.id)) {
1195
+ throw new Error(`Duplicate element ID "${element.id}" in process "${this.processId}"`);
1196
+ }
1197
+ this.flowElements.push(element);
1198
+ if (this.lastNodeId) {
1199
+ const flowId = generateId("Flow");
1200
+ this.sequenceFlows.push({
1201
+ id: flowId,
1202
+ sourceRef: this.lastNodeId,
1203
+ targetRef: element.id,
1204
+ extensionElements: [],
1205
+ unknownAttributes: {},
1206
+ });
1207
+ }
1208
+ // Auto-connect any open branch ends (from branch() calls without .connectTo())
1209
+ for (const branchEnd of this.openBranchEnds) {
1210
+ const flowId = generateId("Flow");
1211
+ this.sequenceFlows.push({
1212
+ id: flowId,
1213
+ sourceRef: branchEnd,
1214
+ targetRef: element.id,
1215
+ extensionElements: [],
1216
+ unknownAttributes: {},
1217
+ });
1218
+ }
1219
+ this.openBranchEnds = [];
1220
+ this.lastNodeId = element.id;
1221
+ }
1222
+ }
1223
+ //# sourceMappingURL=bpmn-builder.js.map