Appearance
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/bpmnrenderer 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
useBpmnRendererInputcomposable - Transforms raw
responseintoBpmnRendererInputformat - Computed reactively - updates when response changes
- Returns
undefinedif 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:
- Validates panel configuration and request settings
- Resolves Object GUIDs (from configuration or Object-selection events)
- Builds
MethodRunRequestbody - Executes POST to
/MetaMethods/Run - Stores the response and the exact resolved Object GUIDs in state
- Initializes
lastModelfrom the computed model
Object Resolution:
- Uses
panel.request.objectsif provided - Falls back to
useSelectedObjectForEvent()ifgetSelectedObjectForEventIdconfigured - Filters falsy values from the
objectsarray - Records the resolved values in
graphContextObjectGuidsonly 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:
- Compares
previousvscurrentmodel to identify changes - Captures the current graph-request Object and guards it before/after every contextual Link lookup and again immediately before
POST /Link processBpmnChanges()categorizes changes into typed eventspersistBpmnGraphChange()dispatches each event to its action handler- After a Link creation request or any persistence error, the coordinator runs the configured graph method and uses its returned model
- For a trusted Link receipt, the store verifies that every created UUID is present among the reloaded edge IDs
- The store reports the aggregate outcome; the component accepts only an ordinary
acceptedmodel and resets after every reconciled/error outcome
Supported Change Events:
update:nodesPositions- Node drag/drop position updatescreate:nodes- New workflow steps addeddelete:nodes- Workflow steps removedcreate:edges- Edge creationdelete: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:
- Deletions (nodes, edges)
- Position updates
- 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 /Linkreturned 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 completerequestblock from the panel configuration. That block showsrequest.linkNameand whether the optionalrequest.sourceandrequest.targetfields 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 modelChange 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 outcomeIntegration Points
Composables
useBpmnRendererInput(panelId)
Location: ../composables/use-bpmn-renderer-input.ts
- Transforms raw
GraphStateResponseintoBpmnRendererInput - 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
onObjectCreateactions 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, orfailed
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 datagraphContextObjectGuids- Confirmed context owned by that responseloading,error- Request status flagsnewNodeData,lastCreatedNodeGuid- Temporary creation statelastModel- 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:
- After fetch, capture
lastModelsnapshot - User interacts with UI
- Component watches for model changes
- When changed, compare current vs
lastModel - Persist only the delta
- Update
lastModelto 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).
Related Documentation
- BPMN Allowed Items Processing - Toolbar configuration
- API Reference - Type definitions
- Panel Types - Configuration schema