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)
  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, 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 config or selected object events)
  3. Builds MethodRunRequest body
  4. Executes POST to /api/methods/run
  5. Stores response in state
  6. Initializes lastModel from computed model

Object Resolution:

  • Uses panel.request.objects if provided
  • Falls back to useSelectedObjectForEvent() if getSelectedObjectForEventId configured
  • Filters falsy values from objects array

applyBpmnChanges({ panelId, previous, current })

Detects and persists user modifications to the BPMN graph.

Process:

  1. Compares previous vs current model to identify changes
  2. processBpmnChanges() categorizes changes into typed events
  3. Dispatches each event to appropriate action handler
  4. Action handlers make backend API calls to persist changes

Supported Change Events:

  • update:nodesPositions - Node drag/drop position updates
  • create:nodes - New workflow steps added
  • delete:nodes - Workflow steps removed
  • create:edges - New connections between nodes
  • delete:edges - Connections removed
  • update:edges - Edge source/target reconnection

Event Processing Order:

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

This order ensures referential integrity (delete before create).

Error Handling:

  • Logs warning to console
  • Shows error toast with localized message
  • Does not throw (allows other operations to continue)

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 /api/methods/run with parent/method/objects/params

4. Backend returns GraphStateResponse (nested array structure)

5. Store saves response

6. useBpmnRendererInput transforms response → BpmnRendererInput

7. setLastModel() captures snapshot

8. 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. Switch statement routes events to action modules:
   - update:nodesPositions → updateNodesPositions()
   - create:edges → requestCreateEdges()
   - delete:edges → requestDeleteEdges()
   - create:nodes → requestCreateNodes()
   - delete:nodes → requestDeleteNodes()
   - update:edges → requestUpdateEdges()

6. Action modules make backend API calls

7. Success: changes persisted

8. Failure: error toast shown, state remains unchanged

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 objects to BPMN node types
  • Builds node cards with field data
  • Returns computed model that updates reactively

Action Modules

Location: ./actions/

All action modules follow similar patterns:

  • Accept typed payload
  • Build backend API request
  • Execute request via $fetchApi
  • Handle success/error states
  • Show toast notifications

updateNodesPositions(payload)

Updates x_pos, y_pos attributes for moved nodes.

requestCreateEdges(payload)

Creates new link objects between nodes.

requestDeleteEdges(edgeGuids)

Deletes link objects by GUID.

requestCreateNodes({ panelId, bpmnNodes })

Creates new node objects 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 node objects by GUID.

  • Confirmation dialog shown before calling
  • Removes objects from backend
  • Graph refetch triggered after deletion

requestUpdateEdges(payload)

Updates edge source/target when reconnected.

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

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
  • 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:

  • Minimizes backend calls (only changed data sent)
  • Preserves undo/redo potential
  • Enables optimistic updates

Error Boundaries

Errors are handled at multiple levels:

  • Store actions: Catch, log, toast, don't throw
  • Action modules: Validate payloads, catch API errors
  • Components: Can handle specific error states

This prevents one failed operation from breaking the entire panel.

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).