Event Modeling

The Event Model swim-lane renders a service's command-event-aggregate flow as a seven-lane diagram. It is a projection of the unified lifecycle model — the same code ∪ runtime ∪ declared graph the Workflow screen assembles — mapped onto Event Modeling lanes by lifecycleToLanes. There is one model and one renderer (SwimlaneView.vue); every surface below feeds it a slice of that model.
History (#765). The swim-lane used to have a second, manifest-only acquisition path (
event-model-store.ts→flattenManifest+ drift-palette + a live causation overlay). That path was retired in favour of the assembler model, which is a strict capability superset (cross-service stitching, provenance, observed overlay). The drift pills and the trace-replay overlay described in older revisions of this page no longer exist on the swim-lane.
Where to find it
There is no standalone "Event Model" tab. The swim-lane appears in two places, both reading the assembler model:
- Event Store Explorer → Configuration tab (
/explorer) — the per-service view. The lifecycle endpoint auto-detects a service seed (#767) from the selected service andlifecycleToLanesfilters the boundary graph to that service. - Workflow screen → Event Model view mode (Workflow) — the fleet-wide view. Seeds on one message/event/saga/aggregate type and stitches its flow across every service boundary. Use it to follow one type end-to-end.
For live message flow over time use the Timeline; for the projection / read-model angle, Projections.
What you see
Seven lanes, left to right, in the order each node typically participates in a request. Each lane is populated by mapping a lifecycle participant lane onto its Event Modeling equivalent (LANE_MAP in lifecycleToLanes.ts):
| Lane | Lifecycle source | What lands here |
|---|---|---|
| Trigger | endpoint | The transport/route a command arrives on — HTTP route, queue, gRPC method, scheduled origin. |
| Command | trigger | The initiating message — the intent that starts the workflow. |
| Handler | handler | Handler classes and saga types (sagas are handlers). |
| Event | eventStore | The appended event(s). |
| Projection | projection | Projection types the workflow feeds. |
| Read Model | readModel | Read models / boundary models the workflow produces. |
| Query | (none yet) | Read queries aren't modelled by the assembler, so this lane renders empty. |
Structurally empty lanes are hidden (#764, visibleLanes) so the remaining lanes stay wide and legible — a non-event-sourced service simply shows command/handler and nothing to their right. When nothing is populated the full lane scaffold renders as the empty state instead of collapsing to zero width.
Nodes carry their short type name on the body. Hover any node for a tooltip with the fully-qualified name and its subtitle (owning service, saga/aggregate kind). Edges carry the message type and, when CritterWatch has metrics for that hop, a health annotation (#768).
Richness scales with the source generator (F6). A service that references
CritterWatch.SourceGenerationcontributes a statically-discovered manifest to the lifecycle model, so its lanes come through fully populated even before any traffic. A service without it is assembled from runtime observation — the same lanes, filled in as messages actually flow. Both render; the source-generated service is just richer up front.
Reading the swim-lane
Handler drill-down
Clicking a Handler lane node opens the source-viewer drawer with the handler chain's generated source code. The handler chain is keyed by the message it accepts, not the handler type — the assembler carries that message on the node's navMessage (the same side map WorkflowLanes uses for click-through). The fetch goes through the RequestHandlerSourceCode wire pair and reuses the source-code cache in services-store.
Projection drill-down
Clicking a Projection lane node opens the same drawer with the projection class's source code, via RequestProjectionSourceCode. It resolves the whole projection class — the Projection Stepper tab carries the method-level (Apply / Create) drill-down.
Both drill-downs degrade gracefully: an empty drawer shows when the source generator wasn't run (or didn't capture this type), and the drawer surfaces backend errors in a tag at the top. The other lanes (command / event / read-model / trigger / query) note "drill-down coming soon" on the Configuration tab; on the Workflow page they re-seed the workflow on the clicked type (#764).
Exports
Two buttons on the Configuration-tab header export the current SVG:
- Export SVG — serialises the live
<svg>to disk. The diagram is self-contained (D3 inlinesfill/strokeon every element), so it opens directly in browsers, Illustrator, or Figma without external CSS. - Export PNG — rasterises the same SVG through a 2D canvas onto a white background.
Filenames are stamped {service}-event-model-yyyymmdd-hhmm.{svg|png} using local time.
Architecture (for developers)
Data source
Both surfaces fetch the assembled graph from the lifecycle assembler over HTTP and project it into lanes:
useWorkflowModel(seed).trace() → GET /api/critterwatch/lifecycle/{seed}?depth=N → Lifecycle
→ lifecycleToLanes(lifecycle, serviceFilter?) → SwimLaneSlices → SwimlaneView.vueuseWorkflowModel(src/composables/useWorkflowModel.ts) owns the fetch/parse and reactivelifecycle/loading/errorstate. The HTTP response is wrapped{ "lifecycle": { nodes, edges, gaps, ... } }.- The seed is a message/event/saga/aggregate type on the Workflow page, or a service name on the Configuration tab — the endpoint returns that service's boundary graph (#767).
lifecycleToLanes(lifecycle, serviceFilter?)(src/composables/lifecycleToLanes.ts) is a pure, DOM-free projection. With aserviceFilterit keeps only that service's nodes plus shared infra. It returns{ slices, navByNodeId };navByNodeIdis the click-through side map (service scope + the handler'snavMessage).- The shared lane vocabulary (
SwimLane,SwimLaneSlices,SwimLaneNode, overlay/trace primitives) lives in the neutral, store-freesrc/composables/swimlaneModel.ts(#765 step 1).
Instance overlay (Workflow page)
WorkflowLanes.vue accepts optional per-instance observed spans (#656) and dims the projected (still-to-come) remainder against the observed path via buildTraceOverlay from swimlaneModel.ts. The Configuration tab renders the static model without an overlay.
Wire pairs used
| Wire pair | Direction | Purpose |
|---|---|---|
RequestHandlerSourceCode → HandlerSourceCodeResponse | Page → service | Handler-lane drill-down. |
RequestProjectionSourceCode → ProjectionSourceCodeResponse | Page → service | Projection-lane drill-down. |
The lifecycle graph itself is fetched over the read-only HTTP endpoint (/api/critterwatch/lifecycle/...), not a SignalR wire pair.
Troubleshooting
The swim-lane is empty. The lifecycle assembler found no workflow artifacts for this service's boundary graph. A service with no discovered handlers, and no observed traffic, has nothing to draw yet. If you expect nodes, confirm the service is registered and (for static discovery) references CritterWatch.SourceGeneration.
A lane I expect is missing. Structurally empty lanes are hidden by design (visibleLanes). A service that isn't event-sourced legitimately has no event/projection/read-model lanes — that's the correct picture, not a bug.
A downstream node shows a "no handler discovered" gap. A terminal cascaded message (emitted with no downstream handler/hop) renders as a gap rather than an edge — the assembler only draws a Cascades edge when a converging handler exists. This is faithful to the model: the gap says "nothing consumes this yet."
Handler drill-down says "no handler chain found." The node's navMessage was empty — some publisher nodes (HTTP/gRPC/scheduled origins) don't carry an inbound message, so there's no handler chain to resolve. The projection/source-code path only applies to message-driven handlers.
Export PNG produces a blank image. The canvas roundtrip needs the SVG to load through an Image element before drawing. If the diagram referenced external resources (it shouldn't — D3 inlines everything), the image load fails silently. Open the export's source SVG to inspect.
