@aranzatech/diagrams-bpmn 0.2.10 → 0.2.11

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/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.0
6
+
7
+ ### Batch process simulation
8
+
9
+ New public API for running synthetic batch simulations directly on a BPMN diagram
10
+ without an external engine. Enables bottleneck detection, heatmap overlays, and
11
+ operational capacity projections from inside the modeler.
12
+
13
+ **New exports (root `@aranzatech/diagrams-bpmn`)**
14
+
15
+ - `runBatchSimulation(diagram, config)` — runs `N` process instances synchronously
16
+ and aggregates per-node statistics. Designed to be called from a Web Worker for
17
+ large `N` to keep the main thread unblocked.
18
+
19
+ **New types**
20
+
21
+ - `BatchSimConfig` — `instances`, `gatewayWeights`, `nodeDurations`, `maxSteps`
22
+ - `BatchSimResult` — `total`, `completed`, `deadlocked`, `avgSteps`, `nodeStats`
23
+ - `NodeSimStats` — `visits`, `totalDurationMs`, `peakConcurrency` per node
24
+ - `GatewayWeights` — edge-id → relative weight map for exclusive / inclusive gateways
25
+
26
+ Gateway weights are normalised internally; edges absent from the map default to
27
+ weight `1`. `nodeDurations` drive `totalDurationMs` (visits × duration), enabling
28
+ duration-based heatmaps and daily workload projections.
29
+
5
30
  ## 0.2.7
6
31
 
7
32
  ### XML round-trip improvements
package/dist/index.cjs CHANGED
@@ -4054,6 +4054,130 @@ function isCompleted(state) {
4054
4054
  function setVariable(state, key, value) {
4055
4055
  return { ...state, variables: { ...state.variables, [key]: value } };
4056
4056
  }
4057
+
4058
+ // src/simulation/batch.ts
4059
+ function weightedChoice(edgeIds, weights) {
4060
+ if (edgeIds.length === 0) return "";
4061
+ const w = edgeIds.map((id) => Math.max(0, weights[id] ?? 1));
4062
+ const total = w.reduce((a, b) => a + b, 0);
4063
+ if (total === 0) return edgeIds[0];
4064
+ let r = Math.random() * total;
4065
+ for (let i = 0; i < edgeIds.length; i++) {
4066
+ r -= w[i];
4067
+ if (r <= 0) return edgeIds[i];
4068
+ }
4069
+ return edgeIds[edgeIds.length - 1];
4070
+ }
4071
+ function buildBatchDiagram(diagram, gatewayWeights, gwVarMap) {
4072
+ const patchedEdges = diagram.edges.map((edge) => {
4073
+ const gwId = edge.source;
4074
+ if (!(gwId in gatewayWeights) || edge.type !== "sequenceFlow") return edge;
4075
+ const varName = gwVarMap[gwId];
4076
+ const outEdges = diagram.edges.filter(
4077
+ (e) => e.source === gwId && e.type === "sequenceFlow"
4078
+ );
4079
+ const idx = outEdges.findIndex((e) => e.id === edge.id);
4080
+ if (idx === -1) return edge;
4081
+ return {
4082
+ ...edge,
4083
+ conditionExpression: `\${${varName} == ${idx}}`,
4084
+ isDefault: false
4085
+ };
4086
+ });
4087
+ return { nodes: diagram.nodes, edges: patchedEdges };
4088
+ }
4089
+ function runOneInstance(diagram, initialVars, maxSteps) {
4090
+ let state = createSimulation(diagram, initialVars);
4091
+ state = tick(diagram, state);
4092
+ const visitedNodes = /* @__PURE__ */ new Set();
4093
+ const peakPerNode = {};
4094
+ function snapshot() {
4095
+ const counts = {};
4096
+ for (const token of state.tokens) {
4097
+ visitedNodes.add(token.elementId);
4098
+ counts[token.elementId] = (counts[token.elementId] ?? 0) + 1;
4099
+ }
4100
+ for (const [nodeId, c] of Object.entries(counts)) {
4101
+ if (c > (peakPerNode[nodeId] ?? 0)) peakPerNode[nodeId] = c;
4102
+ }
4103
+ }
4104
+ snapshot();
4105
+ let steps = 0;
4106
+ while (state.status === "running" && steps < maxSteps) {
4107
+ const fireable = getFireable(diagram, state);
4108
+ if (fireable.length === 0) break;
4109
+ for (const id of fireable) {
4110
+ if (state.status !== "running") break;
4111
+ state = fire(diagram, state, id);
4112
+ snapshot();
4113
+ }
4114
+ steps++;
4115
+ }
4116
+ for (const entry of state.log) {
4117
+ visitedNodes.add(entry.elementId);
4118
+ }
4119
+ return {
4120
+ visitedNodes,
4121
+ peakPerNode,
4122
+ steps: state.step,
4123
+ completed: state.status === "completed"
4124
+ };
4125
+ }
4126
+ function runBatchSimulation(diagram, config = {}) {
4127
+ const n = Math.max(1, config.instances ?? 100);
4128
+ const maxSteps = Math.max(1, config.maxSteps ?? 500);
4129
+ const gwWeights = config.gatewayWeights ?? {};
4130
+ const nodeDurations = config.nodeDurations ?? {};
4131
+ const gwVarMap = {};
4132
+ Object.keys(gwWeights).forEach((gwId, i) => {
4133
+ gwVarMap[gwId] = `__g${i}`;
4134
+ });
4135
+ const batchDiagram = Object.keys(gwWeights).length > 0 ? buildBatchDiagram(diagram, gwWeights, gwVarMap) : diagram;
4136
+ const nodeStats = {};
4137
+ function node(id) {
4138
+ if (!nodeStats[id]) {
4139
+ nodeStats[id] = { visits: 0, totalDurationMs: 0, peakConcurrency: 0 };
4140
+ }
4141
+ return nodeStats[id];
4142
+ }
4143
+ let completed = 0;
4144
+ let deadlocked = 0;
4145
+ let totalSteps = 0;
4146
+ for (let i = 0; i < n; i++) {
4147
+ const instanceVars = {};
4148
+ for (const [gwId, varName] of Object.entries(gwVarMap)) {
4149
+ const outEdges = diagram.edges.filter(
4150
+ (e) => e.source === gwId && e.type === "sequenceFlow"
4151
+ );
4152
+ const edgeIds = outEdges.map((e) => e.id);
4153
+ const chosen = weightedChoice(edgeIds, gwWeights[gwId]);
4154
+ instanceVars[varName] = outEdges.findIndex((e) => e.id === chosen);
4155
+ }
4156
+ const result = runOneInstance(batchDiagram, instanceVars, maxSteps);
4157
+ if (result.completed) {
4158
+ completed++;
4159
+ totalSteps += result.steps;
4160
+ } else {
4161
+ deadlocked++;
4162
+ }
4163
+ for (const nodeId of result.visitedNodes) {
4164
+ const s = node(nodeId);
4165
+ s.visits++;
4166
+ s.totalDurationMs += nodeDurations[nodeId] ?? 0;
4167
+ }
4168
+ for (const [nodeId, peak] of Object.entries(result.peakPerNode)) {
4169
+ const s = node(nodeId);
4170
+ if (peak > s.peakConcurrency) s.peakConcurrency = peak;
4171
+ }
4172
+ }
4173
+ return {
4174
+ nodeStats,
4175
+ completed,
4176
+ deadlocked,
4177
+ total: n,
4178
+ avgSteps: completed > 0 ? Math.round(totalSteps / completed) : 0
4179
+ };
4180
+ }
4057
4181
  function createBpmnNode(options) {
4058
4182
  const size = getBpmnElementSize(options.elementType);
4059
4183
  const meta = BPMN_ELEMENT_CATALOG[options.elementType];
@@ -4997,6 +5121,7 @@ exports.resizeBpmnNodeByHandleCommand = resizeBpmnNodeByHandleCommand;
4997
5121
  exports.resizeBpmnNodeCommand = resizeBpmnNodeCommand;
4998
5122
  exports.restoreBpmnHistory = restoreBpmnHistory;
4999
5123
  exports.routeBpmnEdgeCommand = routeBpmnEdgeCommand;
5124
+ exports.runBatchSimulation = runBatchSimulation;
5000
5125
  exports.runBpmnCommand = runBpmnCommand;
5001
5126
  exports.runBpmnCommands = runBpmnCommands;
5002
5127
  exports.selectBpmnElementsCommand = selectBpmnElementsCommand;