@bpmnkit/core 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +30 -1
  2. package/dist/bpmn/bpmn-builder.d.ts +209 -3
  3. package/dist/bpmn/bpmn-builder.js +456 -16
  4. package/dist/bpmn/bpmn-model.d.ts +110 -0
  5. package/dist/bpmn/bpmn-parser.js +1413 -528
  6. package/dist/bpmn/bpmn-serializer.js +101 -19
  7. package/dist/bpmn/compact.d.ts +17 -2
  8. package/dist/bpmn/compact.js +3 -3
  9. package/dist/bpmn/full-operations.d.ts +89 -0
  10. package/dist/bpmn/full-operations.js +478 -0
  11. package/dist/bpmn/index.d.ts +19 -0
  12. package/dist/bpmn/index.js +21 -0
  13. package/dist/bpmn/optimize/feel.js +2 -2
  14. package/dist/bpmn/optimize/patterns.js +23 -16
  15. package/dist/bpmn/optimize/tasks.js +30 -7
  16. package/dist/bpmn/optimize/utils.js +2 -4
  17. package/dist/bpmn/optimize/variable-flow.js +58 -67
  18. package/dist/bpmn/semantic-hash.d.ts +93 -0
  19. package/dist/bpmn/semantic-hash.js +155 -0
  20. package/dist/bpmn/sha256.d.ts +17 -0
  21. package/dist/bpmn/sha256.js +95 -0
  22. package/dist/bpmn/zeebe-extensions.d.ts +56 -0
  23. package/dist/bpmn/zeebe-extensions.js +79 -0
  24. package/dist/bpmn/zeebe-placement.d.ts +12 -0
  25. package/dist/bpmn/zeebe-placement.js +140 -0
  26. package/dist/errors.d.ts +40 -1
  27. package/dist/errors.js +41 -0
  28. package/dist/index.d.ts +10 -4
  29. package/dist/index.js +7 -3
  30. package/dist/layout/semantic/graph.d.ts +9 -1
  31. package/dist/layout/semantic/graph.js +42 -17
  32. package/dist/layout/semantic/route.js +102 -42
  33. package/dist/node/index.d.ts +10 -0
  34. package/dist/node/index.js +9 -0
  35. package/dist/node/write.d.ts +81 -0
  36. package/dist/node/write.js +167 -0
  37. package/dist/types/id-generator.js +11 -3
  38. package/dist/xml/index.d.ts +3 -1
  39. package/dist/xml/index.js +2 -1
  40. package/dist/xml/xml-parser.d.ts +32 -0
  41. package/dist/xml/xml-parser.js +394 -143
  42. package/package.json +8 -1
@@ -1,7 +1,7 @@
1
1
  import { generateId } from "../types/id-generator.js";
2
2
  import { applyAutoLayout } from "./auto-layout.js";
3
3
  import { restConnectorRetries, restConnectorTaskType, restConnectorToIoMappingInputs, restConnectorToTaskHeaders, } from "./rest-connector.js";
4
- import { zeebeExtensionsToXmlElements } from "./zeebe-extensions.js";
4
+ import { ensureZeebeExtension, zeebeExtensionsToXmlElements, } from "./zeebe-extensions.js";
5
5
  // Keep in sync with packages/core/package.json version
6
6
  const EXPORTER_VERSION = "0.0.23";
7
7
  // ---------------------------------------------------------------------------
@@ -207,6 +207,17 @@ function buildAdHocLoopCharacteristics(lc) {
207
207
  ],
208
208
  };
209
209
  }
210
+ const GATEWAY_ELEMENT_TYPES = new Set([
211
+ "exclusiveGateway",
212
+ "parallelGateway",
213
+ "inclusiveGateway",
214
+ "eventBasedGateway",
215
+ "complexGateway",
216
+ ]);
217
+ /** Gateways are the elements for which several outgoing flows are the point. */
218
+ function isGatewayType(type) {
219
+ return GATEWAY_ELEMENT_TYPES.has(type);
220
+ }
210
221
  function recomputeIncomingOutgoing(elements, flows) {
211
222
  for (const el of elements) {
212
223
  el.incoming = [];
@@ -570,6 +581,8 @@ function traceBackToSplit(nodeId, splitGateways, flows) {
570
581
  export class BranchBuilder {
571
582
  /** @internal */
572
583
  _elements = [];
584
+ /** Ids in `_elements`, for O(1) duplicate checks. */
585
+ _ids = new Set();
573
586
  /** @internal */
574
587
  _flows = [];
575
588
  /** @internal */
@@ -617,6 +630,7 @@ export class BranchBuilder {
617
630
  }
618
631
  addElement(element) {
619
632
  this._elements.push(element);
633
+ this._ids.add(element.id);
620
634
  if (this.lastNodeId) {
621
635
  const flowId = generateId("Flow");
622
636
  const flow = {
@@ -811,6 +825,7 @@ export class BranchBuilder {
811
825
  }
812
826
  // Push directly — no sequence flow, boundary events attach via attachedToRef
813
827
  this._elements.push(element);
828
+ this._ids.add(element.id);
814
829
  this.lastNodeId = element.id;
815
830
  this.isFirstElement = false;
816
831
  return this;
@@ -901,10 +916,11 @@ export class BranchBuilder {
901
916
  }
902
917
  // Event sub-processes have no incoming/outgoing sequence flows and must not
903
918
  // advance the branch cursor — mirrors ProcessBuilder.eventSubProcess.
904
- if (this._elements.some((n) => n.id === element.id)) {
919
+ if (this._ids.has(element.id)) {
905
920
  throw new Error(`Duplicate element ID "${element.id}"`);
906
921
  }
907
922
  this._elements.push(element);
923
+ this._ids.add(element.id);
908
924
  return this;
909
925
  }
910
926
  /**
@@ -921,10 +937,11 @@ export class BranchBuilder {
921
937
  const b = new BranchBuilder(this.currentGatewayId, name, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
922
938
  callback(b);
923
939
  for (const el of b._elements) {
924
- if (this._elements.some((n) => n.id === el.id)) {
940
+ if (this._ids.has(el.id)) {
925
941
  throw new Error(`Duplicate element ID "${el.id}"`);
926
942
  }
927
943
  this._elements.push(el);
944
+ this._ids.add(el.id);
928
945
  }
929
946
  for (const fl of b._flows)
930
947
  this._flows.push(fl);
@@ -961,6 +978,8 @@ export class BranchBuilder {
961
978
  export class SubProcessContentBuilder {
962
979
  /** @internal */
963
980
  _elements = [];
981
+ /** Ids in `_elements`, for O(1) duplicate checks. */
982
+ _ids = new Set();
964
983
  /** @internal */
965
984
  _flows = [];
966
985
  /** @internal */
@@ -977,10 +996,11 @@ export class SubProcessContentBuilder {
977
996
  this.rootMessages = rootMessages;
978
997
  }
979
998
  addElement(element) {
980
- if (this._elements.some((n) => n.id === element.id)) {
999
+ if (this._ids.has(element.id)) {
981
1000
  throw new Error(`Duplicate element ID "${element.id}" in sub-process`);
982
1001
  }
983
1002
  this._elements.push(element);
1003
+ this._ids.add(element.id);
984
1004
  if (this.lastNodeId) {
985
1005
  this._flows.push({
986
1006
  id: generateId("Flow"),
@@ -1012,10 +1032,11 @@ export class SubProcessContentBuilder {
1012
1032
  * the current cursor and does not move it.
1013
1033
  */
1014
1034
  addDisconnected(element) {
1015
- if (this._elements.some((n) => n.id === element.id)) {
1035
+ if (this._ids.has(element.id)) {
1016
1036
  throw new Error(`Duplicate element ID "${element.id}" in sub-process`);
1017
1037
  }
1018
1038
  this._elements.push(element);
1039
+ this._ids.add(element.id);
1019
1040
  return this;
1020
1041
  }
1021
1042
  // ---- Events ----
@@ -1136,10 +1157,11 @@ export class SubProcessContentBuilder {
1136
1157
  const b = new BranchBuilder(this.currentGatewayId, name);
1137
1158
  callback(b);
1138
1159
  for (const el of b._elements) {
1139
- if (this._elements.some((n) => n.id === el.id)) {
1160
+ if (this._ids.has(el.id)) {
1140
1161
  throw new Error(`Duplicate element ID "${el.id}"`);
1141
1162
  }
1142
1163
  this._elements.push(el);
1164
+ this._ids.add(el.id);
1143
1165
  }
1144
1166
  for (const fl of b._flows)
1145
1167
  this._flows.push(fl);
@@ -1182,7 +1204,7 @@ export class SubProcessContentBuilder {
1182
1204
  return this;
1183
1205
  }
1184
1206
  element(elementId) {
1185
- if (!this._elements.some((n) => n.id === elementId)) {
1207
+ if (!this._ids.has(elementId)) {
1186
1208
  throw new Error(`Element "${elementId}" not found in sub-process`);
1187
1209
  }
1188
1210
  this.lastNodeId = elementId;
@@ -1210,6 +1232,7 @@ export class SubProcessContentBuilder {
1210
1232
  }
1211
1233
  // Push directly — no sequence flow, boundary events attach via attachedToRef
1212
1234
  this._elements.push(element);
1235
+ this._ids.add(element.id);
1213
1236
  this.lastNodeId = element.id;
1214
1237
  return this;
1215
1238
  }
@@ -1297,10 +1320,11 @@ export class SubProcessContentBuilder {
1297
1320
  }
1298
1321
  // Event sub-processes have no incoming/outgoing sequence flows and must not
1299
1322
  // advance the cursor — mirrors ProcessBuilder.eventSubProcess.
1300
- if (this._elements.some((n) => n.id === element.id)) {
1323
+ if (this._ids.has(element.id)) {
1301
1324
  throw new Error(`Duplicate element ID "${element.id}" in sub-process`);
1302
1325
  }
1303
1326
  this._elements.push(element);
1327
+ this._ids.add(element.id);
1304
1328
  return this;
1305
1329
  }
1306
1330
  }
@@ -1312,8 +1336,12 @@ export class ProcessBuilder {
1312
1336
  processId;
1313
1337
  processName;
1314
1338
  _isExecutable = true;
1339
+ /** Whether `executable()` was called, so continue-mode leaves it alone if not. */
1340
+ _executableSet = false;
1315
1341
  _versionTag;
1316
1342
  flowElements = [];
1343
+ /** Ids in `flowElements`, for O(1) duplicate and existence checks. */
1344
+ elementIds = new Set();
1317
1345
  sequenceFlows = [];
1318
1346
  rootErrors = [];
1319
1347
  rootMessages = [];
@@ -1329,9 +1357,140 @@ export class ProcessBuilder {
1329
1357
  _executionPlatformVersion = "8.9.0";
1330
1358
  _serviceTaskDefaults = {};
1331
1359
  _savedMainFlowId = undefined;
1360
+ /** Set by {@link ProcessBuilder.from}; makes `build()` update rather than generate. */
1361
+ _source;
1362
+ /** Pre-existing flow endpoints, so `build()` can prove it did not rewire them. */
1363
+ _sourceFlowTargets = new Map();
1364
+ /** An `insertAfter` flow waiting to be reattached to what gets built next. */
1365
+ _pendingSpliceFlowId;
1366
+ /** Flows the caller deliberately spliced, exempt from the rewiring guard. */
1367
+ _splicedFlowIds = new Set();
1332
1368
  constructor(processId) {
1333
1369
  this.processId = processId;
1334
1370
  }
1371
+ /**
1372
+ * Continues an existing model rather than generating a new one.
1373
+ *
1374
+ * `build()` then returns *that document* with this process's contents
1375
+ * replaced, so everything the builder has no opinion about — other processes,
1376
+ * the collaboration, lanes, diagram interchange, root elements, imports,
1377
+ * unmodelled content — is still there afterwards. Generating a replacement
1378
+ * from scratch is what loses those.
1379
+ *
1380
+ * The input is not mutated; the builder works on a copy.
1381
+ *
1382
+ * @param definitions - The parsed model to continue.
1383
+ * @param processId - Which process to continue. Named explicitly, because
1384
+ * "the first process" is a guess that goes wrong on a collaboration.
1385
+ * @example
1386
+ * ```typescript
1387
+ * const updated = ProcessBuilder.from(Bpmn.parse(xml), "order")
1388
+ * .at("validate")
1389
+ * .serviceTask("notify", { name: "Notify", taskType: "notify" })
1390
+ * .build()
1391
+ * ```
1392
+ */
1393
+ static from(definitions, processId) {
1394
+ const copy = structuredClone(definitions);
1395
+ const process = copy.processes.find((candidate) => candidate.id === processId);
1396
+ if (process === undefined) {
1397
+ const available = copy.processes.map((candidate) => candidate.id);
1398
+ throw new Error(`Process "${processId}" is not in this document. It contains: ${available.length > 0 ? available.map((id) => `"${id}"`).join(", ") : "no processes"}`);
1399
+ }
1400
+ const builder = new ProcessBuilder(processId);
1401
+ builder._source = { definitions: copy, process };
1402
+ builder.processName = process.name;
1403
+ builder.flowElements.push(...process.flowElements);
1404
+ for (const element of process.flowElements)
1405
+ builder.elementIds.add(element.id);
1406
+ builder.sequenceFlows.push(...process.sequenceFlows);
1407
+ builder._textAnnotations.push(...process.textAnnotations);
1408
+ builder._associations.push(...process.associations);
1409
+ // Seeded so a message event reuses the document's existing message rather
1410
+ // than declaring a second one with the same name.
1411
+ builder.rootErrors.push(...copy.errors);
1412
+ builder.rootMessages.push(...copy.messages);
1413
+ builder.rootSignals.push(...copy.signals);
1414
+ builder.rootEscalations.push(...copy.escalations);
1415
+ builder._sourceFlowTargets = new Map(process.sequenceFlows.map((flow) => [flow.id, `${flow.sourceRef}→${flow.targetRef}`]));
1416
+ return builder;
1417
+ }
1418
+ /**
1419
+ * Moves the cursor to an existing flow node, so the next call chains from it.
1420
+ *
1421
+ * @param nodeId - A flow node directly contained by this process. Nodes inside
1422
+ * a sub-process are not reachable: continuing into one means building that
1423
+ * sub-process, not this one.
1424
+ */
1425
+ at(nodeId) {
1426
+ const node = this.flowElements.find((element) => element.id === nodeId);
1427
+ if (node === undefined) {
1428
+ throw new Error(`"${nodeId}" is not a flow node in process "${this.processId}". Nodes inside a sub-process cannot be continued from here.`);
1429
+ }
1430
+ if (node.type === "endEvent") {
1431
+ throw new Error(`Cannot continue from end event "${nodeId}": an end event has no outgoing sequence flow.`);
1432
+ }
1433
+ const hasOutgoing = this.sequenceFlows.some((flow) => flow.sourceRef === nodeId);
1434
+ if (hasOutgoing && !isGatewayType(node.type)) {
1435
+ throw new Error(`"${nodeId}" already has an outgoing sequence flow. Continuing from it would give a ${node.type} two outgoing flows, which is an uncontrolled split. Use insertAfter("${nodeId}") to splice into that path instead.`);
1436
+ }
1437
+ this.moveCursor(node);
1438
+ return this;
1439
+ }
1440
+ /**
1441
+ * Splices what you build next into the path leaving an existing node.
1442
+ *
1443
+ * `insertAfter("validate")` followed by `.serviceTask("notify", …)` turns
1444
+ * `validate → end` into `validate → notify → end`. The existing flow keeps its
1445
+ * id and its target and only changes where it starts, so an edge nobody asked
1446
+ * to move keeps its identity in the diagram and in a diff.
1447
+ *
1448
+ * This is the counterpart to {@link at}, which continues from a node whose
1449
+ * path is open. Which one you mean is not guessable, so it is not guessed.
1450
+ *
1451
+ * @param nodeId - A flow node with exactly one outgoing sequence flow.
1452
+ */
1453
+ insertAfter(nodeId) {
1454
+ const node = this.flowElements.find((element) => element.id === nodeId);
1455
+ if (node === undefined) {
1456
+ throw new Error(`"${nodeId}" is not a flow node in process "${this.processId}".`);
1457
+ }
1458
+ const outgoing = this.sequenceFlows.filter((flow) => flow.sourceRef === nodeId);
1459
+ if (outgoing.length === 0) {
1460
+ throw new Error(`"${nodeId}" has no outgoing sequence flow, so there is nothing to insert into. Use at("${nodeId}") to continue from it.`);
1461
+ }
1462
+ if (outgoing.length > 1) {
1463
+ throw new Error(`"${nodeId}" has ${outgoing.length} outgoing sequence flows, so "after" is ambiguous. Name the flow's target and insert before that instead.`);
1464
+ }
1465
+ this.resolvePendingSplice();
1466
+ const flow = outgoing[0];
1467
+ this._pendingSpliceFlowId = flow.id;
1468
+ this._splicedFlowIds.add(flow.id);
1469
+ this.moveCursor(node);
1470
+ return this;
1471
+ }
1472
+ /** Points the cursor at an existing node and forgets any branch state. */
1473
+ moveCursor(node) {
1474
+ this.lastNodeId = node.id;
1475
+ this.currentGatewayId = isGatewayType(node.type) ? node.id : undefined;
1476
+ this.openBranchEnds = [];
1477
+ this._savedMainFlowId = undefined;
1478
+ }
1479
+ /**
1480
+ * Reattaches the flow a pending `insertAfter` detached, to whatever the cursor
1481
+ * has reached. With nothing built in between the cursor has not moved and this
1482
+ * is a no-op, which is the right answer for `insertAfter(x)` followed by
1483
+ * nothing.
1484
+ */
1485
+ resolvePendingSplice() {
1486
+ const flowId = this._pendingSpliceFlowId;
1487
+ if (flowId === undefined)
1488
+ return;
1489
+ this._pendingSpliceFlowId = undefined;
1490
+ const flow = this.sequenceFlows.find((candidate) => candidate.id === flowId);
1491
+ if (flow !== undefined && this.lastNodeId !== undefined)
1492
+ flow.sourceRef = this.lastNodeId;
1493
+ }
1335
1494
  /** Enable auto-layout: `build()` will run the layout engine and populate diagram interchange data. */
1336
1495
  withAutoLayout() {
1337
1496
  this._autoLayout = true;
@@ -1356,6 +1515,7 @@ export class ProcessBuilder {
1356
1515
  /** Set whether this process is executable. */
1357
1516
  executable(value) {
1358
1517
  this._isExecutable = value;
1518
+ this._executableSet = true;
1359
1519
  return this;
1360
1520
  }
1361
1521
  /** Set the process version tag. */
@@ -1638,10 +1798,11 @@ export class ProcessBuilder {
1638
1798
  const b = new BranchBuilder(this.currentGatewayId, name, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
1639
1799
  callback(b);
1640
1800
  for (const el of b._elements) {
1641
- if (this.flowElements.some((n) => n.id === el.id)) {
1801
+ if (this.elementIds.has(el.id)) {
1642
1802
  throw new Error(`Duplicate element ID "${el.id}"`);
1643
1803
  }
1644
1804
  this.flowElements.push(el);
1805
+ this.elementIds.add(el.id);
1645
1806
  }
1646
1807
  for (const fl of b._flows) {
1647
1808
  this.sequenceFlows.push(fl);
@@ -1696,7 +1857,7 @@ export class ProcessBuilder {
1696
1857
  * @throws If no element with the given ID exists.
1697
1858
  */
1698
1859
  element(elementId) {
1699
- const found = this.flowElements.some((n) => n.id === elementId);
1860
+ const found = this.elementIds.has(elementId);
1700
1861
  if (!found) {
1701
1862
  throw new Error(`Element "${elementId}" not found in process "${this.processId}"`);
1702
1863
  }
@@ -1761,11 +1922,12 @@ export class ProcessBuilder {
1761
1922
  // advance the flow cursor — the surrounding process wires around them.
1762
1923
  // openBranchEnds is intentionally NOT drained here; the next normal
1763
1924
  // addFlowElement call will drain it and connect branch ends to that element.
1764
- if (this.flowElements.some((n) => n.id === element.id)) {
1925
+ if (this.elementIds.has(element.id)) {
1765
1926
  throw new Error(`Duplicate element ID "${element.id}" in process "${this.processId}"`);
1766
1927
  }
1767
1928
  this._savedMainFlowId = undefined;
1768
1929
  this.flowElements.push(element);
1930
+ this.elementIds.add(element.id);
1769
1931
  return this;
1770
1932
  }
1771
1933
  // ---- Annotations ----
@@ -1797,19 +1959,50 @@ export class ProcessBuilder {
1797
1959
  *
1798
1960
  * Resolves all forward-referenced `incoming` / `outgoing` arrays and wraps
1799
1961
  * the process in a {@link BpmnDefinitions} ready for XML serialization.
1962
+ *
1963
+ * **The join contract.** Branches built with `.branch()` converge implicitly:
1964
+ * where several paths from one gateway reach the same element, a matching join
1965
+ * gateway is inserted for you. That is convenient by hand and a trap for
1966
+ * generated code, which cannot see the element it did not write. Pass
1967
+ * `{ explicitJoins: true }` to be told instead of helped — the build throws,
1968
+ * naming the gateways it would have inserted, and you declare them yourself
1969
+ * with `.connectTo(joinId)`.
1970
+ *
1971
+ * A join you declare only counts if it *matches the split*: an exclusive split
1972
+ * converging on a parallel gateway is not the gateway inference would have
1973
+ * added, so it is still inferred — and with `explicitJoins` that refusal is the
1974
+ * only thing that tells you.
1975
+ *
1976
+ * {@link ProcessBuilder.from} never infers joins at all, whatever this option
1977
+ * says: inference reads the whole topology, and on a document you were handed
1978
+ * that means rewriting edges you never touched.
1979
+ *
1980
+ * @param options - `explicitJoins` refuses inferred join gateways. `strict` is
1981
+ * the former name for it and still works.
1800
1982
  */
1801
1983
  build(options) {
1984
+ this.resolvePendingSplice();
1802
1985
  const beforeCount = this.flowElements.length;
1803
- insertJoinGateways(this.flowElements, this.sequenceFlows);
1804
- if (options?.strict && this.flowElements.length > beforeCount) {
1986
+ // Continuing a document never infers joins. `insertJoinGateways` reads the
1987
+ // whole topology, so on a parsed model it retargets edges the caller never
1988
+ // touched — the corpus has a document where a no-op continue would have
1989
+ // invented a join gateway and rerouted two flows into it. Building a branch
1990
+ // that needs a join here means saying so with `.connectTo(joinId)`.
1991
+ if (this._source === undefined)
1992
+ insertJoinGateways(this.flowElements, this.sequenceFlows);
1993
+ const explicitJoins = options?.explicitJoins ?? options?.strict ?? false;
1994
+ if (explicitJoins && this.flowElements.length > beforeCount) {
1805
1995
  const inserted = this.flowElements
1806
1996
  .slice(beforeCount)
1807
1997
  .map((e) => e.id)
1808
1998
  .join(", ");
1809
- throw new Error(`auto-join gateways were inserted: ${inserted}. Use explicit .connectTo(joinId) to make gateway topology explicit, or remove { strict: true }.`);
1999
+ throw new Error(`Inferred join gateways: ${inserted}. Declare them with .connectTo(joinId), or drop { explicitJoins: true } to keep the inference.`);
1810
2000
  }
1811
2001
  this.validate();
2002
+ this.assertSourceTopologyIntact();
1812
2003
  recomputeIncomingOutgoing(this.flowElements, this.sequenceFlows);
2004
+ if (this._source !== undefined)
2005
+ return this.buildOntoSource(this._source);
1813
2006
  const extensionElements = [];
1814
2007
  if (this._versionTag) {
1815
2008
  extensionElements.push({
@@ -1871,6 +2064,78 @@ export class ProcessBuilder {
1871
2064
  };
1872
2065
  return this._autoLayout ? applyAutoLayout(defs) : defs;
1873
2066
  }
2067
+ /**
2068
+ * Writes this process's contents back into the document it came from.
2069
+ *
2070
+ * Everything not listed here is kept by identity — other processes, the
2071
+ * collaboration, diagram interchange, root elements, the process's own lanes,
2072
+ * documentation, extensions and unmodelled content. That is the whole point of
2073
+ * continuing rather than regenerating.
2074
+ *
2075
+ * Diagram interchange is *not* regenerated: existing shapes keep their
2076
+ * positions, and elements added here have none until `withAutoLayout()` or a
2077
+ * later `applyAutoLayout()` gives them one.
2078
+ */
2079
+ buildOntoSource(source) {
2080
+ const { definitions, process } = source;
2081
+ process.flowElements = this.flowElements;
2082
+ process.sequenceFlows = this.sequenceFlows;
2083
+ process.textAnnotations = this._textAnnotations;
2084
+ process.associations = this._associations;
2085
+ if (this.processName !== undefined)
2086
+ process.name = this.processName;
2087
+ // Only when asked. BPMN reads an absent `isExecutable` as false, so writing
2088
+ // the builder's `true` default onto a process that never carried it makes a
2089
+ // non-executable process executable — a change nobody requested.
2090
+ if (this._executableSet)
2091
+ process.isExecutable = this._isExecutable;
2092
+ if (this._versionTag !== undefined) {
2093
+ // Set the attribute on the existing element rather than replacing the
2094
+ // bag: a real process carries other extensions next to it.
2095
+ ensureZeebeExtension({ type: "process", extensionElements: process.extensionElements }, "zeebe:versionTag").attributes.value = this._versionTag;
2096
+ }
2097
+ definitions.errors = this.rootErrors;
2098
+ definitions.messages = this.rootMessages;
2099
+ definitions.signals = this.rootSignals;
2100
+ definitions.escalations = this.rootEscalations;
2101
+ return this._autoLayout ? applyAutoLayout(definitions) : definitions;
2102
+ }
2103
+ /**
2104
+ * Refuses to rewire a flow that was already in the document.
2105
+ *
2106
+ * `insertJoinGateways` retargets converging flows, which is right for a
2107
+ * topology this builder just created and wrong for one it was handed: a
2108
+ * document would come back with edges the caller never touched pointing
2109
+ * somewhere else. Continuing a model has to leave the model alone.
2110
+ */
2111
+ assertSourceTopologyIntact() {
2112
+ if (this._source === undefined)
2113
+ return;
2114
+ const rewired = [];
2115
+ const byId = new Map(this.sequenceFlows.map((flow) => [flow.id, flow]));
2116
+ for (const [id, endpoints] of this._sourceFlowTargets) {
2117
+ const flow = byId.get(id);
2118
+ if (flow === undefined) {
2119
+ rewired.push(`${id} (removed)`);
2120
+ continue;
2121
+ }
2122
+ // A spliced flow was moved on purpose, but only its source may move:
2123
+ // `insertAfter` inserts into a path, it does not redirect one.
2124
+ const [, target] = endpoints.split("→");
2125
+ if (this._splicedFlowIds.has(id)) {
2126
+ if (flow.targetRef !== target) {
2127
+ rewired.push(`${id} (spliced flow now targets ${flow.targetRef}, not ${target})`);
2128
+ }
2129
+ continue;
2130
+ }
2131
+ const now = `${flow.sourceRef}→${flow.targetRef}`;
2132
+ if (now !== endpoints)
2133
+ rewired.push(`${id} (${endpoints} became ${now})`);
2134
+ }
2135
+ if (rewired.length > 0) {
2136
+ throw new Error(`Building would rewire sequence flows this document already had: ${rewired.join(", ")}. Continuing a model must not change the part you did not touch.`);
2137
+ }
2138
+ }
1874
2139
  validate() {
1875
2140
  const elementIds = new Set(this.flowElements.map((el) => el.id));
1876
2141
  for (const flow of this.sequenceFlows) {
@@ -1885,10 +2150,11 @@ export class ProcessBuilder {
1885
2150
  }
1886
2151
  }
1887
2152
  addFlowElement(element) {
1888
- if (this.flowElements.some((n) => n.id === element.id)) {
2153
+ if (this.elementIds.has(element.id)) {
1889
2154
  throw new Error(`Duplicate element ID "${element.id}" in process "${this.processId}"`);
1890
2155
  }
1891
2156
  this.flowElements.push(element);
2157
+ this.elementIds.add(element.id);
1892
2158
  // Compensation handlers are outside the normal token flow: link via association
1893
2159
  // from the preceding compensation boundary event, then restore the main-flow cursor.
1894
2160
  if (element.isForCompensation) {
@@ -1947,6 +2213,9 @@ export class DiagramBuilder {
1947
2213
  _processes = [];
1948
2214
  _errors = [];
1949
2215
  _messages = [];
2216
+ _participants = [];
2217
+ _messageFlows = [];
2218
+ _collaborationId = "Collaboration_1";
1950
2219
  _executionPlatformVersion = "8.9.0";
1951
2220
  constructor(id) {
1952
2221
  this._id = id;
@@ -1965,6 +2234,152 @@ export class DiagramBuilder {
1965
2234
  this._messages.push(...defs.messages);
1966
2235
  return this;
1967
2236
  }
2237
+ /** Renames the collaboration element. Defaults to `"Collaboration_1"`. */
2238
+ collaborationId(id) {
2239
+ this._collaborationId = id;
2240
+ return this;
2241
+ }
2242
+ /**
2243
+ * Adds a pool.
2244
+ *
2245
+ * Omit `processId` for a black box — a participant whose internals are not
2246
+ * modelled. That is a real BPMN construct, not an incomplete one: it is how
2247
+ * you draw the counterparty you exchange messages with but do not execute.
2248
+ *
2249
+ * @param id - The participant's element id, used verbatim.
2250
+ */
2251
+ participant(id, options = {}) {
2252
+ const participant = { id, unknownAttributes: {} };
2253
+ if (options.name !== undefined)
2254
+ participant.name = options.name;
2255
+ if (options.processId !== undefined)
2256
+ participant.processRef = options.processId;
2257
+ this._participants.push(participant);
2258
+ return this;
2259
+ }
2260
+ /**
2261
+ * Declares a root-level message, which a message flow may name and a Camunda 8
2262
+ * message subscription correlates on.
2263
+ *
2264
+ * `ProcessBuilder` already creates messages by name for message events, so
2265
+ * only call this for a message no event declared — typically one carried by a
2266
+ * message flow between pools.
2267
+ *
2268
+ * @param id - The message's element id, used verbatim.
2269
+ */
2270
+ message(id, options = {}) {
2271
+ const message = { id, unknownAttributes: {} };
2272
+ if (options.name !== undefined)
2273
+ message.name = options.name;
2274
+ const extensions = zeebeExtensionsToXmlElements(options.correlationKey === undefined
2275
+ ? {}
2276
+ : { subscription: { correlationKey: options.correlationKey } });
2277
+ if (extensions.length > 0)
2278
+ message.extensionElements = extensions;
2279
+ this._messages.push(message);
2280
+ return this;
2281
+ }
2282
+ /**
2283
+ * Connects two pools.
2284
+ *
2285
+ * `source` and `target` name either participants or flow nodes inside them.
2286
+ * Both forms are valid BPMN and the layout engine reads either, but they must
2287
+ * be in *different* pools — a message flow is what crosses a pool boundary,
2288
+ * and one that does not is the error this catches.
2289
+ *
2290
+ * @param id - The message flow's element id, used verbatim.
2291
+ */
2292
+ messageFlow(id, options) {
2293
+ const flow = {
2294
+ id,
2295
+ sourceRef: options.source,
2296
+ targetRef: options.target,
2297
+ unknownAttributes: {},
2298
+ };
2299
+ if (options.name !== undefined)
2300
+ flow.name = options.name;
2301
+ if (options.messageRef !== undefined)
2302
+ flow.messageRef = options.messageRef;
2303
+ this._messageFlows.push(flow);
2304
+ return this;
2305
+ }
2306
+ /**
2307
+ * Reports every way the declared collaboration would not survive contact with
2308
+ * a modeler, so `build()` can refuse rather than emit a file that opens broken.
2309
+ */
2310
+ collaborationProblems() {
2311
+ const problems = [];
2312
+ const seen = new Set();
2313
+ for (const id of [
2314
+ ...this._participants.map((p) => p.id),
2315
+ ...this._messageFlows.map((f) => f.id),
2316
+ ]) {
2317
+ if (seen.has(id))
2318
+ problems.push(`Duplicate element ID "${id}"`);
2319
+ seen.add(id);
2320
+ }
2321
+ const processIds = new Set(this._processes.map((process) => process.id));
2322
+ const claimed = new Map();
2323
+ for (const participant of this._participants) {
2324
+ const processId = participant.processRef;
2325
+ if (processId === undefined)
2326
+ continue;
2327
+ if (!processIds.has(processId)) {
2328
+ problems.push(`Participant "${participant.id}" references process "${processId}", which this diagram does not contain`);
2329
+ continue;
2330
+ }
2331
+ const owner = claimed.get(processId);
2332
+ if (owner !== undefined) {
2333
+ problems.push(`Participants "${owner}" and "${participant.id}" both reference process "${processId}"; a process belongs to one pool`);
2334
+ }
2335
+ claimed.set(processId, participant.id);
2336
+ }
2337
+ const owningParticipant = this.participantIndex();
2338
+ const messageIds = new Set(this._messages.map((message) => message.id));
2339
+ for (const flow of this._messageFlows) {
2340
+ for (const [role, ref] of [
2341
+ ["source", flow.sourceRef],
2342
+ ["target", flow.targetRef],
2343
+ ]) {
2344
+ if (!owningParticipant.has(ref)) {
2345
+ problems.push(`Message flow "${flow.id}" names ${role} "${ref}", which is not a participant or a flow node in one`);
2346
+ }
2347
+ }
2348
+ const from = owningParticipant.get(flow.sourceRef);
2349
+ const to = owningParticipant.get(flow.targetRef);
2350
+ if (from !== undefined && from === to) {
2351
+ problems.push(`Message flow "${flow.id}" starts and ends in participant "${from}"; a message flow crosses pools, a sequence flow stays inside one`);
2352
+ }
2353
+ if (flow.messageRef !== undefined && !messageIds.has(flow.messageRef)) {
2354
+ problems.push(`Message flow "${flow.id}" references message "${flow.messageRef}", which this diagram does not declare`);
2355
+ }
2356
+ }
2357
+ return problems;
2358
+ }
2359
+ /** Maps every participant id and every flow node id to its owning participant. */
2360
+ participantIndex() {
2361
+ const index = new Map();
2362
+ const byProcess = new Map();
2363
+ for (const participant of this._participants) {
2364
+ index.set(participant.id, participant.id);
2365
+ if (participant.processRef !== undefined)
2366
+ byProcess.set(participant.processRef, participant.id);
2367
+ }
2368
+ const walk = (elements, owner) => {
2369
+ for (const element of elements) {
2370
+ index.set(element.id, owner);
2371
+ if ("flowElements" in element && Array.isArray(element.flowElements)) {
2372
+ walk(element.flowElements, owner);
2373
+ }
2374
+ }
2375
+ };
2376
+ for (const process of this._processes) {
2377
+ const owner = byProcess.get(process.id);
2378
+ if (owner !== undefined)
2379
+ walk(process.flowElements, owner);
2380
+ }
2381
+ return index;
2382
+ }
1968
2383
  build() {
1969
2384
  return {
1970
2385
  id: this._id,
@@ -1988,10 +2403,35 @@ export class DiagramBuilder {
1988
2403
  escalations: [],
1989
2404
  messages: this._messages,
1990
2405
  signals: [],
1991
- collaborations: [],
2406
+ collaborations: this.buildCollaborations(),
1992
2407
  processes: this._processes,
1993
2408
  diagrams: [],
1994
2409
  };
1995
2410
  }
2411
+ /**
2412
+ * A document with no participants has no collaboration — an empty
2413
+ * `<bpmn:collaboration/>` is not a neutral addition, it makes every process a
2414
+ * pool-less participant in a modeler.
2415
+ */
2416
+ buildCollaborations() {
2417
+ if (this._participants.length === 0 && this._messageFlows.length === 0)
2418
+ return [];
2419
+ const problems = this.collaborationProblems();
2420
+ if (problems.length > 0) {
2421
+ throw new Error(`Invalid collaboration:\n ${problems.join("\n ")}`);
2422
+ }
2423
+ return [
2424
+ {
2425
+ id: this._collaborationId,
2426
+ participants: this._participants,
2427
+ messageFlows: this._messageFlows,
2428
+ textAnnotations: [],
2429
+ associations: [],
2430
+ groups: [],
2431
+ extensionElements: [],
2432
+ unknownAttributes: {},
2433
+ },
2434
+ ];
2435
+ }
1996
2436
  }
1997
2437
  //# sourceMappingURL=bpmn-builder.js.map