Skip to content

BPMN Store Architecture ​

This document explains the state management architecture for BPMN panels, including data flow, change detection, and backend persistence.

Overview ​

The BPMN store (useBpmnPanelsStore) is a Pinia store that manages state for all BPMN workflow diagram panels. It coordinates between:

  • UI Layer: @mes/bpmn renderer component
  • Backend API: Method run endpoints for graph data and persistence
  • Action Modules: Specialized handlers for create/update/delete operations

Store Structure ​

State Schema ​

Each panel maintains its own state object:

typescript
type PanelState = {
  loading: boolean; // API request in progress
  response: GraphStateResponse; // Raw backend response (3-level nested array)
  graphContextObjectGuids?: string[]; // Object resources that produced the confirmed graph
  error: unknown | null; // Last error if any
  lastModel?: BpmnRendererInput; // Snapshot for change detection
  newNodeData?: {
    // Temporary data during node creation
    tempId: string;
    position: { x: number; y: number };
  };
  lastCreatedNodeGuid?: string; // GUID of most recently created node
  viewbox?: {
    // Saved canvas position
    x: number;
    y: number;
    zoom: number;
  };
};

Getters ​

bpmnJsonModel(panelId) ​

Returns the computed BPMN model ready for @mes/bpmn rendering.

  • Delegates to useBpmnRendererInput composable
  • Transforms raw response into BpmnRendererInput format
  • Computed reactively - updates when response changes
  • Returns undefined if no response available

lastModel(panelId) ​

Returns the most recent snapshot of the BPMN model.

  • Used for change detection by comparing with current model
  • Updated after fetchData() and after applying changes
  • Identifies what the user modified (positions, edges, nodes)

Actions ​

ensurePanelState(panelId) ​

Initializes panel state if not already present.

  • Creates default state structure
  • Safe to call multiple times (no-op if exists)
  • Prevents undefined access errors

cleanupPanelTransientState(panelId) ​

Cleans up transient state while preserving persistent data.

  • Called on: Component unmount
  • Preserves: viewbox (user's zoom/pan position)
  • Clears: response, graphContextObjectGuids, loading/error flags, creation data, lastModel
  • Purpose: Prevent memory leaks and state pollution between panels

setLastModel({ panelId, model }) ​

Captures current model state as baseline for change detection.

  • Called after fetchData() completes
  • Called after applying changes
  • Maintains sync between backend state and local model

fetchData({ panelId }) ​

Fetches BPMN graph data from backend via method run API.

Process:

  1. Validates panel configuration and request settings
  2. Resolves Object GUIDs (from configuration or Object-selection events)
  3. Builds MethodRunRequest body
  4. Executes POST to /MetaMethods/Run
  5. Stores the response and the exact resolved Object GUIDs in state
  6. Initializes lastModel from the computed model

Object Resolution:

  • Uses panel.request.objects if provided
  • Falls back to useSelectedObjectForEvent() if getSelectedObjectForEventId configured
  • Filters falsy values from the objects array
  • Records the resolved values in graphContextObjectGuids only with the successfully acquired response; graph-context Link lookup requires exactly one unchanged confirmed Object

applyBpmnChanges({ panelId, previous, current }) ​

Detects and persists user modifications to the BPMN graph.

Process:

  1. Compares previous vs current model to identify changes
  2. Captures the current graph-request Object and guards it before/after every contextual Link lookup and again immediately before POST /Link
  3. processBpmnChanges() categorizes changes into typed events
  4. persistBpmnGraphChange() dispatches each event to its action handler
  5. After a Link creation request or any persistence error, the coordinator runs the configured graph method and uses its returned model
  6. For a trusted Link receipt, the store verifies that every created UUID is present among the reloaded edge IDs
  7. The store reports the aggregate outcome; the component accepts only an ordinary accepted model and resets after every reconciled/error outcome

Supported Change Events:

  • update:nodesPositions - Node drag/drop position updates
  • create:nodes - New workflow steps added
  • delete:nodes - Workflow steps removed
  • create:edges - Edge creation
  • delete:edges - Edge deletion

When the user drags the source or target of an edge that was already present in the previous model, processBpmnChanges() rejects the change before PUT /Link. The coordinator runs the graph acquisition method again, and the returned model replaces the attempted move. This applies to every persisted edge. Delete the connector and create it again to change an endpoint. The restriction exists because a reloaded edge does not retain enough source data to distinguish an Object endpoint from a Link endpoint.

Event Processing Order:

  1. Deletions (nodes, edges)
  2. Position updates
  3. Creations (edges, nodes)

This order ensures referential integrity (delete before create).

Persistence Outcomes:

  • accepted: the write completed without requiring reconciliation; the component accepts the renderer model as its next baseline.
  • reconciled: after a persistence error the graph method returned a model, or after creation it returned every trusted Link UUID; the component resets to that returned model.
  • persisted-unreconciled: POST /Link returned one valid UUID per request, but graph reload failed or omitted one of them; the store reports those GUIDs and tells the user not to draw the connector again because the Link already exists. The user checks the Link by GUID in the relationship-editing interface or attaches the GUID and notification text to an issue report.
  • commit-unknown: the Link receipt was missing, malformed, or duplicated, or a Link request lost its response and the graph method did not resolve which write completed. The store does not invent a GUID and tells the user not to draw the connector again until they check both the connector between the source and target nodes and the expected Link in the relationship-editing interface. If that check is unavailable, the user reports the notification text and the complete request block from the panel configuration. That block shows request.linkName and whether the optional request.source and request.target fields are present or absent.
  • failed: persistence was not confirmed and the graph method also failed.

requestDeleteNodes({ panelId, bpmnNodes }) ​

Delegates node deletion to action module.

  • Exposed as store action for external callers (e.g., confirmation dialogs)
  • Directly calls requestDeleteNodes() action module
  • Used when deletion needs to be triggered outside of applyBpmnChanges() flow

Data Flow ​

Initial Load ​

1. Component mount
   ↓
2. fetchData({ panelId })
   ↓
3. POST /MetaMethods/Run with parent/method/objects/params
   ↓
4. Backend returns GraphStateResponse (nested array structure)
   ↓
5. Store saves response
   ↓
6. Store saves the resolved Object resources as graphContextObjectGuids
   ↓
7. useBpmnRendererInput transforms response → BpmnRendererInput
   ↓
8. setLastModel() captures snapshot
   ↓
9. Component renders @mes/bpmn with model

Change Detection & Persistence ​

1. User interaction (drag, connect, create, delete)
   ↓
2. @mes/bpmn emits bpmn:changed event
   ↓
3. Component calls applyBpmnChanges(previous, current)
   ↓
4. processBpmnChanges() diffs models → typed events
   ↓
5. Contextual endpoints assert the captured graph Object before/after lookup
   and immediately before Link creation
   ↓
6. Switch statement routes events to action modules:
   - update:nodesPositions → updateNodesPositions()
   - create:edges → requestCreateEdges()
   - delete:edges → requestDeleteEdges()
   - create:nodes → requestCreateNodes()
   - delete:nodes → requestDeleteNodes()
   ↓
7. Action modules make backend API calls
   ↓
8. persistBpmnGraphChange() runs the configured panel graph method when required
   ↓
9. Trusted created Link UUIDs must occur in the reloaded edge IDs
   ↓
10. Store maps the persistence outcome to localized feedback; the component
   accepts or resets its model from that outcome

Integration Points ​

Composables ​

useBpmnRendererInput(panelId) ​

Location: ../composables/use-bpmn-renderer-input.ts

  • Transforms raw GraphStateResponse into BpmnRendererInput
  • Extracts nodes and edges from nested array structure
  • Matches backend Object resources to BPMN node types
  • Builds node cards with field data
  • Returns computed model that updates reactively

Action Modules ​

Location: ./actions/

Action modules own their individual transport boundary:

  • Accept typed payload
  • Build backend API request
  • Execute request via $fetchApi

The BPMN store and persistBpmnGraphChange() own aggregate reconciliation and feedback. In particular, Link-creation success is not shown inside the action.

updateNodesPositions(payload) ​

Updates x_pos, y_pos attributes for moved nodes.

requestCreateEdges(payload) ​

Creates new Link resources with the resolved sourceGuid and targetGuid endpoints, which may identify an Object or a Link. It validates that the response contains one distinct UUID per requested Link and returns an identified or unidentified receipt. The store shows success only after the configured graph method returns an edge model containing every trusted UUID.

requestDeleteEdges(edgeGuids) ​

Deletes Link resources by GUID.

requestCreateNodes({ panelId, bpmnNodes }) ​

Creates new Object resources for nodes with position attributes.

  • Matches toolbar item by node.data._bpmnTaskId
  • Executes onObjectCreate actions from toolbar item events
  • Supports direct request creation without modal (editor.mode = "direct-request")
  • Updates node GUID after object creation

requestDeleteNodes({ panelId, bpmnNodes }) ​

Deletes Object resources for nodes by GUID.

  • Confirmation dialog shown before calling
  • Removes Object resources from the backend
  • Graph refetch triggered after deletion

requestUpdateEdges(payload) ​

The transport action still exists, but the BPMN user path does not call it for persisted reconnects because safe endpoint provenance is unavailable.

Utilities ​

processBpmnChanges(previous, current) ​

Location: ./utils/process-bpmn-changes.ts

  • Diffs two BPMN models using just-diff
  • Categorizes changes into typed events
  • Returns array of ProcessBpmnChangesResult
  • Rejects mixed endpoint strategies before lookup
  • Rejects every persisted reconnect and requires delete-and-recreate

persistBpmnGraphChange({ getResults, handlers, reconcile }) ​

Location: ./utils/persist-bpmn-graph-change.ts

  • Executes detected writes in order
  • Tracks whether Link creation was attempted, whether it completed, and whether returned GUIDs are trusted
  • Reloads the graph after Link creation or any persistence error
  • Passes trusted created UUIDs to the store for verification against the reloaded edge IDs
  • Returns accepted, reconciled, persisted-unreconciled, commit-unknown, or failed

Detection Strategy:

  • Compares node/edge ID sets to detect creates/deletes
  • Inspects diff paths to detect position changes
  • Validates changes before including in results

State Management Patterns ​

Transient vs Persistent State ​

Transient (cleared on unmount):

  • response - Large graph data
  • graphContextObjectGuids - Confirmed context owned by that response
  • loading, error - Request status flags
  • newNodeData, lastCreatedNodeGuid - Temporary creation state
  • lastModel - Can be regenerated from response

Persistent (preserved on unmount):

  • viewbox - User's zoom/pan position

This prevents memory leaks while maintaining good UX (canvas position preserved).

Change Detection ​

Change detection uses snapshot comparison:

  1. After fetch, capture lastModel snapshot
  2. User interacts with UI
  3. Component watches for model changes
  4. When changed, compare current vs lastModel
  5. Persist only the delta
  6. Update lastModel to current

This approach sends only detected changes. Whenever the transient renderer state can no longer be trusted, the coordinator runs the configured graph method and replaces the widget model with its result.

Error Boundaries ​

Errors are handled at multiple levels:

  • Action modules: validate payloads and execute one API boundary.
  • Persistence coordinator: catches write errors, runs the configured graph method again, and preserves known, unknown, or response-lost Link-commit identity.
  • Store: logs and maps the aggregate outcome to localized feedback.
  • Component: accepts only accepted; every other outcome resets the widget from the graph returned by the method or the last confirmed model.

Best Practices ​

When to Use Store vs Composable ​

Use Store for:

  • Persistent state across component lifecycles
  • Shared state between multiple panels
  • Backend API interactions
  • Change tracking and history

Use Composable for:

  • Computed transformations (like useBpmnRendererInput)
  • Reactive derivations from store state
  • Component-specific logic
  • One-time calculations

Panel State Cleanup ​

Always call cleanupPanelTransientState() on unmount:

typescript
onBeforeUnmount(() => {
  useBpmnPanelsStore().cleanupPanelTransientState(panelId);
});

This prevents memory leaks from accumulated response data.

Object GUID Resolution ​

When panel needs object context:

typescript
// Preferred: Config-based
panel.request.objects = ['object-guid-1', 'object-guid-2'];

// Fallback: Event-based
panel.request.getSelectedObjectForEventId = 'main:object-selected';

Event-based is useful for dynamic object selection (e.g., from tree navigation).