Skip to content

feat(workflows): project BPMN diagnostics onto an element-keyed instance overlay - #1024

Merged
sfmskywalker merged 6 commits into
mainfrom
claude/bpmn-w13-element-overlay
Sep 12, 2026
Merged

sfmskywalker merged 6 commits into
mainfrom
claude/bpmn-w13-element-overlay

Conversation

@sfmskywalker

Copy link
Copy Markdown
Member

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.

  • Contract mirror (Elsa.Studio.Workflows.Core/Domain/Models/Bpmn/): BpmnDiagnosticEventNames and BpmnDiagnosticLogPayload mirror elsa-core's constants and payload one-to-one, with a pointer to the core files as the source of truth.
  • One mapping: BpmnElementStatsProjector is 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.
  • Refresh: DiagramDesignerWrapper.RefreshElementStatsAsync runs on the same observer tick as activity stats in WorkflowInstanceDesigner. It filters the journal by every BpmnProcess activity 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.
  • Plumbing: an optional IBpmnElementStatsSink, implemented only by BpmnDiagramDesigner, forwards through the wrapper and component to the X6 adapter's existing updateBpmnElementStats. IDiagramDesigner is not widened for every designer.
  • Rendering: node badges already existed. Edges now render "taken" in the existing badge tone.
  • Write-only: nothing is read back into the engine, and no element gains behaviour.

Verification

cd src/modules/Elsa.Studio.Workflows.Designer/ClientLib && npm run check:generated && npm test && npm run build   # 243/243
dotnet build Elsa.Studio.sln --configuration Release                                                                   # 0 errors
dotnet test src/modules/Elsa.Studio.Workflows.Tests --configuration Release --no-build --framework net10.0            # 363/363
dotnet test src/modules/Elsa.Studio.Workflows.Designer.Tests --configuration Release --no-build --framework net10.0   # 83/83

Tests cover:

  • The issue's verification scenario, with journal records built exactly as elsa-core writes them: in a parallel split where one branch blocks, the join shows a waiting token, the blocked task shows blocked, and the completed branch's flows show taken.
  • Nested-scope folding.
  • An incremental refresh fetching only new records.
  • A reset when the instance changes.
  • A backlog larger than one tick's cap, fully folded across ticks.
  • A later record for the same element overriding an earlier one across a refresh boundary, identical to a single pass.
  • Wiring into the instance designer's observer.

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 details stays nullable, deliberately defensive when reading a payload off the wire.

🤖 Generated with Claude Code

sfmskywalker and others added 4 commits September 12, 2026 11:26
…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>
@greptile-apps

greptile-apps Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

Merge safe.

Summary

  • This change projects BPMN journal diagnostics into an element-keyed instance overlay so gateways, events, and sequence flows display runtime state alongside activity statistics.
  • It retains overlays across the Blazor and browser-canvas mount lifecycle, including the initial refresh used by finished instances, and adds focused coverage for projection, pagination, refresh, rendering, and mount timing.

Reviews (2) · Last reviewed commit: "refactor(workflows): stop discarding the..."

sfmskywalker and others added 2 commits September 12, 2026 12:30
…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>
@sfmskywalker
sfmskywalker merged commit 7c42ff9 into main Sep 12, 2026
10 checks passed
@sfmskywalker
sfmskywalker deleted the claude/bpmn-w13-element-overlay branch September 12, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BPMN W13: element-keyed instance overlay from BPMN journal entries

1 participant