feat(workflows): project BPMN diagnostics onto an element-keyed instance overlay - #1024
Merged
Merged
Conversation
…nce overlay Adds the element-keyed BPMN instance overlay (W13): diagnostics elsa-core projects onto the journal (BpmnDiagnosticEventNames/BpmnDiagnosticLogPayload, mirrored from elsa-core) are folded by BpmnElementStatsProjector into a BpmnElementStats map keyed by BPMN element or flow id, refreshed on the same cadence as the existing activity-keyed stats and pushed through BpmnDiagramDesigner's new IBpmnElementStatsSink into the X6 adapter, which now also renders "taken" styling on sequence flow edges. Gateways and events -- which have no bound activity under Option A -- can now show a waiting token or a fired join, and a completed branch's flows show taken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RefreshElementStatsAsync re-fetched and re-folded the whole journal from skip 0 on every tick, capped at 10,000 entries. Track a per-instance high-water mark and fold only newly-arrived records into the existing map via a new BpmnElementStatsProjector.Fold overload, resetting both when the instance or the BPMN scope set changes, and drop the silent truncation now that a tick no longer re-reads the whole history. Also demote RefreshElementStatsAsync from public virtual to internal virtual to match the file's existing test-double precedent (NotifyStateChangedAsync). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…age cap Cap RefreshElementStatsAsync's journal paging to a fixed number of pages per tick (50 pages x 200 records) instead of looping unbounded, so a first load or a burst after the tab was backgrounded can no longer page an instance's entire history in one refresh. Nothing is lost: the high-water mark still advances by whatever was fetched, so a backlog bigger than the cap is folded across successive refreshes. Also adds a regression test proving that a later record (Joined) folded in a second refresh correctly overwrites an earlier record's state (Waiting) for the same element, matching a single pass over both records. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…instead of reflection Expose ElementStatsMaxPagesPerRefresh as an internal property (backed by the existing InternalsVisibleTo grant to the test assembly) instead of reaching into a private field via reflection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
|
…as mount race A refresh could name BpmnDiagramDesigner before its BpmnDesignerWrapper -- and the canvas underneath it -- had ever rendered, most notably the one unconditional refresh a freshly opened instance gets on load. The update was silently dropped, so a finished instance (which never gets a later observer-driven refresh to fall back on) never showed its gateway, event and taken-flow overlay at all. Both BpmnDiagramDesigner and BpmnDesignerWrapper now retain the latest stats and apply them once their next component down mounts, mirroring BpmnDesigner's own first-render drain of pending graph actions. Also replaces an implicit-filter foreach in BpmnElementStatsProjector with an explicit .Where(), per CodeQL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hand the retained overlay to BpmnDesignerWrapper synchronously on capture instead of firing-and-forgetting UpdateElementStatsAsync, so an exception on that path is no longer swallowed. BpmnDesignerWrapper is now the sole owner of retain-and-flush; BpmnDiagramDesigner keeps only a transient field until the wrapper is captured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1000. Part of elsa-workflows/elsa-core#7909 (W13, Studio half). The core half, which projects interpreter diagnostics into the journal, merged in elsa-workflows/elsa-core#8058.
What changed
The BPMN instance viewer now lights up gateways, events and flows, not only bound tasks. Under the design's Option A, only bound work has an activity id, so the existing activity-keyed stats channel could never show where a token waits at a gateway.
Elsa.Studio.Workflows.Core/Domain/Models/Bpmn/):BpmnDiagnosticEventNamesandBpmnDiagnosticLogPayloadmirror elsa-core's constants and payload one-to-one, with a pointer to the core files as the source of truth.BpmnElementStatsProjectoris the only place diagnostic kinds turn into element stats, keyed by BPMN element id or flow id. Covered states: token present, flow taken, gateway joined, boundary triggered, work completed, cancelled or torn down, and waiting shown as blocked. Entries from nested scopes land in the same map, since BPMN ids are document-unique.DiagramDesignerWrapper.RefreshElementStatsAsyncruns on the same observer tick as activity stats inWorkflowInstanceDesigner. It filters the journal by everyBpmnProcessactivity id, not by event names, because several kind names collide with generic lifecycle events. It is incremental: a high-water mark per instance and scope set means each tick fetches only new records, folded into the existing map. That is safe because the server orders the filtered journal by a per-instance sequence that survives resumes. Each tick pages at most 10,000 records, and a larger backlog continues on the next tick, so nothing is truncated. The refresh inherits the existing observer disposal (fix(workflows): stop instance designer refresh timers when the circuit disconnects #992); no new timer.IBpmnElementStatsSink, implemented only byBpmnDiagramDesigner, forwards through the wrapper and component to the X6 adapter's existingupdateBpmnElementStats.IDiagramDesigneris not widened for every designer.Verification
Tests cover:
Review: three iterations on the standards and spec axes, with four must-fix findings resolved: incremental reads, a bounded per-tick catch-up, the cross-boundary test, and internal test seams instead of reflection. Won't-fix: the mirror's
detailsstays nullable, deliberately defensive when reading a payload off the wire.🤖 Generated with Claude Code