Skip to content
D
Documentation

Engine

reference
10 min readUpdated

Import these from @grafloria/engine.

Functions

isValidDiagramMode

Type guard to check if a string is a valid DiagramMode

ts
function isValidDiagramMode(mode: string): mode is DiagramMode

Classes

DiagramEngine

ts
class DiagramEngine

Properties

NameTypeDefaultDescription
eventBusEventBus
storeDiagramStore
commandManagerCommandManager
pluginManagerPluginManager
typeRegistryTypeRegistry
validationEngineValidationEngine
serializerDiagramSerializer
performanceMonitorPerformanceMonitor
modeManagerModeManager
clipboardManagerClipboardManager
selectionManagerSelectionManager
routingEngineRoutingEngine
templateRegistryTemplateRegistry

Methods

  • constructor(config: DiagramEngineConfig = {})
  • getDiagram(): DiagramModel | null — Get current diagram
  • getConfig(): DiagramEngineConfig — Get configuration
  • getInteractionConfig(): InteractionConfig — Get interaction configuration Returns the current interaction mode settings

A CACHED, FROZEN snapshot — not a fresh spread per call. This getter is on the hottest paths in the product: the renderer consults it per port and per link inside every frame, and the binder on every pointer event, so the old { ...config } allocated tens of thousands of full copies per second and showed up as the single largest self-time in a 2,000-node drag profile (~590ms of a 4.5s gesture — more than routing).

The spread existed to keep callers from mutating engine state; the freeze keeps that promise the honest way. A caller that used to scribble on its private copy now throws instead of silently diverging — which is the correct outcome, because two callers sharing one snapshot must not see each other's scribbles.

  • setInteractionConfig(config: Partial<InteractionConfig>): void — Set interaction configuration Updates interaction mode settings and emits event
  • getConnectionStateManager(): ConnectionStateManager — Get connection state manager Used for managing connection drag operations
  • getReconnectionPreview(): ReconnectionPreview | null — Current endpoint-reconnection preview, or null when no endpoint is being dragged. The renderer reads this to draw a ghost link.
  • setReconnectionPreview(preview: ReconnectionPreview | null): void — Set (or clear, with null) the endpoint-reconnection preview. Called by the interaction layer on start/move/end of an endpoint drag. Does not emit — the interaction layer already triggers re-render.
  • getProximityPreview(): ProximityPreview | null — The proximity-connect proposal the renderer draws as a live wire, or null.
  • setProximityPreview(preview: ProximityPreview | null): void — Set (or clear, with null) the proximity-connect proposal. Does not emit — the node drag that drives it already triggers re-renders.
  • getSnapGuides(): SnapGuideSegment[] | null — The live snap-guide segments a node drag is showing, or null.
  • setSnapGuides(guides: SnapGuideSegment[] | null): void — Set (or clear, with null) the live snap guides. Does not emit — the node drag that drives them already triggers re-renders.
  • getRoutingEngine(): RoutingEngine — Get routing engine Used for calculating link paths with various algorithms
  • enableLiveRerouting(): void — Enable live rerouting Automatically updates link paths when nodes move or resize
  • disableLiveRerouting(): void — Disable live rerouting
  • getLiveReroutingEngine(): LiveReroutingEngine | null — Get live rerouting engine
  • setDiagram(diagram: DiagramModel | null): void — Set diagram
  • createDiagram(name: string = 'Untitled'): DiagramModel — Create new diagram
  • clearDiagram(): void — Clear diagram
  • async addNode(config: { type: string; position: Point; size?: Size; data?: any; }): Promise<NodeModel> — Add node (from config) Add node (pre-created NodeModel) Add node implementation
  • async addNode(node: NodeModel): Promise<NodeModel> — Add node (from config) Add node (pre-created NodeModel) Add node implementation
  • async addNode(configOrNode: { type: string; position: Point; size?: Size; data?: any } | NodeModel): Promise<NodeModel> — Add node (from config) Add node (pre-created NodeModel) Add node implementation
  • async removeNode(nodeId: string): Promise<void> — Remove node

Async + awaited, mirroring removeGroup(). The execute() promise used to float — a command failure became an unhandled rejection (fatal under Node), and callers could not sequence on the removal completing.

  • async addLink(config: { sourcePortId: string; targetPortId: string; type?: string; data?: any; }): Promise<LinkModel> — Add link
  • async removeLink(linkId: string): Promise<void> — Remove link

Async + awaited, mirroring removeGroup() — see removeNode().

  • async addGroup(config: { name: string }): Promise<GroupModel> — Add group
  • async removeGroup(groupId: string): Promise<void> — Remove group
  • async addToGroup(groupId: string, entityId: string): Promise<void> — Add entity to group
  • async removeFromGroup(groupId: string, entityId: string): Promise<void> — Remove entity from group
  • async expandGroup(groupId: string): Promise<void> — Expand group
  • async collapseGroup(groupId: string, options?: CollapseOptions): Promise<void> — Collapse group
  • getGroup(groupId: string): GroupModel | undefined — Get group by ID
  • getGroups(): GroupModel[] — Get all groups
  • async setLayout( groupId: string, layoutType: 'flexbox' | 'grid', layoutConfig: LayoutConfig ): Promise<void> — Set layout configuration on a group
  • async clearLayout(groupId: string): Promise<void> — Clear layout configuration from a group
  • getLayout(groupId: string): { type: LayoutType; config?: LayoutConfig } | undefined — Get layout configuration from a group
  • async setFlexItem(nodeId: string, flexConfig: FlexItemConfig): Promise<void> — Set flex item configuration on a node
  • async setGridItem(nodeId: string, gridConfig: GridItemConfig): Promise<void> — Set grid item configuration on a node
  • async copy(options?: { includeGroups?: boolean; includeLinks?: boolean }): Promise<void> — Copy selected entities to clipboard
  • async paste(options?: { offset?: Point; selectPasted?: boolean }): Promise<void> — Paste entities from clipboard
  • async duplicate(options?: { offset?: Point; selectDuplicated?: boolean }): Promise<void> — Duplicate selected entities
  • async deleteSelection(options?: { deleteChildren?: boolean; deleteLinks?: boolean }): Promise<void> — Delete selected entities
  • getClipboardData() — Get clipboard data
  • hasClipboardData(): boolean — Check if clipboard has data
  • clearClipboard(): void — Clear clipboard
  • getClipboardStats() — Get clipboard statistics
  • validateDiagram(options?: { validateTypes?: boolean; validateConnections?: boolean; validatePorts?: boolean; strict?: boolean }): ValidationResult — Validate the entire diagram
  • validateNode(nodeId: string, options?: { validateTypes?: boolean; validateConnections?: boolean; validatePorts?: boolean; strict?: boolean }): ValidationResult — Validate a specific node
  • validateLink(linkId: string, options?: { validateTypes?: boolean; validateConnections?: boolean; validatePorts?: boolean; strict?: boolean }): ValidationResult — Validate a specific link
  • validatePort(portId: string, nodeId: string, options?: { validateTypes?: boolean; validateConnections?: boolean; validatePorts?: boolean; strict?: boolean }): ValidationResult — Validate a specific port
  • validateLayout(groupId: string, options?: { strict?: boolean }): ValidationResult — Validate layout configuration for a group
  • registerNodeType(definition: NodeTypeDefinition): void — Register a node type definition
  • registerPortType(definition: PortTypeDefinition): void — Register a port type definition
  • registerLinkType(definition: LinkTypeDefinition): void — Register a link type definition
  • registerGroupType(definition: GroupTypeDefinition): void — Register a group type definition
  • enableRealTimeValidation(): void — Enable real-time validation
  • disableRealTimeValidation(): void — Disable real-time validation
  • isRealTimeValidationEnabled(): boolean — Check if real-time validation is enabled
  • selectNodes(nodeIds: string[]): void — Select nodes
  • selectLinks(linkIds: string[]): void — Select links
  • clearSelection(): void — Clear selection
  • async undo(): Promise<void> — Undo
  • async redo(): Promise<void> — Redo
  • canUndo(): boolean — Can undo
  • canRedo(): boolean — Can redo
  • validate(): ValidationResult — Validate diagram
  • serialize(): SerializedDiagram | null — Serialize diagram (with mode)
  • deserialize(data: SerializedDiagram, options?: import('../models/DiagramModel').DiagramLoadOptions): DiagramModel — Deserialize diagram (with mode)
  • loadFromJSON( json: string | SerializedDiagram, options?: import('../models/DiagramModel').DiagramLoadOptions ): DiagramModel — Load diagram from JSON (with mode)
  • saveToJSON(): string | null — Save diagram to JSON
  • async registerPlugin(plugin: Plugin): Promise<void> — Register a plugin AND bring it to life.

PluginManager.register() only RECORDS a plugin; install() and activate() are separate steps. Calling register alone — which both of this engine's entry points used to do — left every plugin permanently inert: its hooks never fired, though getPlugin() happily returned it. "Register a plugin" can only sensibly mean "make it run", so this drives the full lifecycle. A plugin that throws is reported and skipped rather than taking the host down with it.

  • getPlugin(name: string): Plugin | undefined — Get plugin

  • setViewport(viewport: Viewport): void — Set viewport

  • setZoom(zoom: number): void — Set zoom

  • getPerformanceReport(): PerformanceReport — Get performance report

  • getMode(): DiagramMode — Get current diagram mode

  • setMode(mode: DiagramMode): void — Set diagram mode

  • isDesignerMode(): boolean — Check if in designer mode

  • isRunningMode(): boolean — Check if in running mode

  • isViewMode(): boolean — Check if in view mode

  • isDebugMode(): boolean — Check if in debug mode

  • isPresentationMode(): boolean — Check if in presentation mode

  • isReadOnlyMode(): boolean — Check if in read-only mode (any mode except designer)

  • addModeGuard(name: string, guard: ModeGuardFunction): void — Add mode transition guard

  • removeModeGuard(name: string): void — Remove mode transition guard

  • clearModeGuards(): void — Clear all mode transition guards

  • configureModeViewport(mode: DiagramMode, settings: ModeViewportSettings): void — Configure viewport settings for specific mode

  • getModeViewportSettings(mode: DiagramMode): ModeViewportSettings — Get viewport settings for specific mode

  • getModeHistory(): ModeHistoryEntry[] — Get mode history

  • clearModeHistory(): void — Clear mode history

  • previousMode(): void — Navigate to previous mode

  • nextMode(): void — Navigate to next mode

  • pushMode(mode: DiagramMode): void — Push mode onto stack (save current, switch to new)

  • popMode(): void — Pop mode from stack (return to previous)

  • getModeAnalytics(): ModeAnalytics — Get mode analytics

  • beforeModeChange(hook: ModeChangeHook): () => void — Register before mode change hook

  • afterModeChange(hook: ModeChangeHook): () => void — Register after mode change hook

  • getLinkBehaviorForMode(baseBehavior: Partial<{ deletable: boolean; selectable: boolean }>): { deletable: boolean; selectable: boolean; } — Get link behavior adjusted for current mode

  • initialize(): void — Initialize the engine

  • getStore(): DiagramStore — Get the store

  • on(event: string, listener: (...args: any[]) => void): void — Subscribe to events

  • off(event: string, listener: (...args: any[]) => void): void — Unsubscribe from events

  • destroy(): void — Destroy engine

  • refreshGroupObstacles(): void — Reconcile the shared ObstacleMap with the diagram's GROUP state, idempotently:

  • a COLLAPSED group (with geometry) is ONE solid obstacle;

    • members hidden under a collapsed group (at any depth) are NOT obstacles — they are not visible, and routing around invisible things produces inexplicable detours;
    • expanding restores the members and removes the group block.

Runs on every group add/remove/collapse/expand. Public so the grouping feature (which owns collapse SEMANTICS but not the ObstacleMap) can force a reconcile after batch operations.

  • setLayoutService(service: { applyLayout(diagram: DiagramModel, config: any): Promise<any>; }): void — Set layout service for diagram layouts
  • getLayoutRegistry(): LayoutRegistry — The named-algorithm registry, with the built-ins already registered.

THE BUG THIS CLOSES: applyLayout() below requires setLayoutService() — and NOTHING in the codebase ever called it (the only mention is a doc comment in layout/index.ts). So dagre, ELK, force, spectral and community — thousands of lines, several of them untested — were UNREACHABLE from the engine. That is the whole "auto-layout is fragmented" finding. Layout now works out of the box, with no setup call.

  • async layout( name: string = DEFAULT_LAYOUT_NAME, options: UnifiedLayoutOptions = {} ): Promise<UnifiedLayoutResult> — Lay out the whole diagram.

await engine.layout('dagre', { direction: 'LR' });

DETERMINISTIC and IDEMPOTENT: the same graph and seed produce byte-identical coordinates, and running it twice changes nothing the second time. (The seed defaults to a fixed constant, so an author who never thinks about seeds still gets the same picture on every reload; randomness is opt-in.)

NOT to be confused with DiagramModel.getLayoutManager(), which answers a DIFFERENT question — "where should this ONE newly-added node go?" — and is a placement strategy, not a graph layout. The audit called them "two parallel stacks" and asked for them to be merged; they are not parallel, and merging them would force a single-node placer to pretend it can lay out a graph.

  • async layoutIncremental( options: IncrementalOptions & { name?: string } & UnifiedLayoutOptions = {} ): Promise<UnifiedLayoutResult & { movement: MovementReport; tween: TweenPlan }> — Mental-map-preserving incremental layout.

await engine.layoutIncremental({ changed: [newNode.id], budget: { maxPerNode: 60 } });

Mermaid re-renders the whole diagram from scratch on every edit and destroys the user's spatial memory of their own diagram. This does the opposite:

Returns a tween PLAN rather than animating: the engine says where things go at time t, the host drives t — which is what keeps this runnable in a worker, in SSR and in a test.

ONE SEMANTIC, STATED PLAINLY. This runs the layered engine, because it is the only one that honours anchors DURING coordinate assignment. If the diagram's current positions came from a DIFFERENT engine, the first incremental pass necessarily re-draws it — and that is not a bug to paper over: "move as little as possible" is ill-posed across engines, because there is no meaningful small move between two engines' idea of the same graph.

  • setLayoutPort(port?: LayoutPort): void — Run layout off the main thread.

The engine does NOT construct the Worker — that would bake one bundler's URL scheme into the engine, which is exactly what the old (never-instantiated) LayoutWorkerPool did with its hardcoded /assets/workers/layout.worker.js. The caller builds the worker however its toolchain likes and hands it in:

const worker = new Worker(new URL('./layout.worker', import.meta.url), { type: 'module' }); engine.setLayoutPort(worker as unknown as LayoutPort);

Pass undefined to go back to running inline.

  • async applyLayout(config: { adapter: string | any; options?: any; animate?: boolean; animationDuration?: number; fit?: boolean; canvasDimensions?: { width: number; height: number }; }): Promise<{ nodePositions: Map<string, { x: number; y: number }>; bounds: { x: number; y: number; width: number; height: number }; metadata?: any; }> — Apply layout to current diagram
  • async applyDagreLayout( options?: { rankdir?: 'TB' | 'BT' | 'LR' | 'RL'; align?: 'UL' | 'UR' | 'DL' | 'DR'; nodesep?: number; edgesep?: number; ranksep?: number; marginx?: number; marginy?: number; ranker?: 'network-simplex' | 'tight-tree' | 'longest-path'; }, canvasDimensions?: { width: number; height: number } ): Promise<any> — Quick helper: Apply Dagre layout
  • async applyELKLayout( options?: { algorithm?: 'layered' | 'force' | 'stress' | 'mrtree' | 'radial' | 'disco'; 'elk.direction'?: 'RIGHT' | 'LEFT' | 'DOWN' | 'UP'; 'elk.spacing.nodeNode'?: number; [key: string]: any; }, canvasDimensions?: { width: number; height: number } ): Promise<any> — Quick helper: Apply ELK layout
  • dispose(): void — Cleanup and dispose of all resources Should be called when the engine is no longer needed

Interfaces

DiagramEngineConfig

ts
interface DiagramEngineConfig

Properties

NameTypeDefaultDescription
plugins?Plugin[]
mode?DiagramMode
performance?{ enableMonitoring?: boolean; enableProfiling?: boolean; warnThreshold?: number; }
validation?{ realTime?: boolean; strict?: boolean; }
history?{ maxCommands?: number; maxSnapshots?: number; }
interaction?Partial<InteractionConfig>

ModeChangeEvent

Mode change event payload

ts
interface ModeChangeEvent

Properties

NameTypeDefaultDescription
previousModeDiagramMode
currentModeDiagramMode

ProximityPreview

The port pair a proximity-connect DROP would link, while a node drag is inside the radius. The renderer reads this to draw the proposed wire itself — highlighting only the two ports left the proposal nearly invisible (live report: "the wire isn't showing"). Same seam shape as {@link ReconnectionPreview}: interaction layer writes, renderer reads, cleared on drop/cancel.

ts
interface ProximityPreview

Properties

NameTypeDefaultDescription
sourceNodeIdstring
sourcePortIdstring
targetNodeIdstring
targetPortIdstring

ReconnectionPreview

Transient state for the endpoint-reconnection live preview. Set by the interaction layer while an endpoint handle is being dragged; read by the renderer to draw a ghost link from the stationary endpoint to the cursor. Deliberately separate from {@link ConnectionStateManager} (which owns NEW-link creation) so the two previews never double-render.

ts
interface ReconnectionPreview

Properties

NameTypeDefaultDescription
linkIdstringId of the link whose endpoint is being reconnected.
endpoint'source' | 'target'Which endpoint the cursor is dragging (the OTHER end stays fixed).
mousePointPointCurrent cursor position in world coordinates.
isValidbooleanWhether the port/node currently under the cursor is a valid drop target.

SnapGuideSegment

One drawable snap-guide segment, in world coordinates. The interaction layer computes alignment / equal-spacing guides during a node drag and publishes them here; the renderer draws them as dashed overlay lines (spacing segments may carry a gap label). Cleared (null) when the drag ends or nothing is within snapping distance.

ts
interface SnapGuideSegment

Properties

NameTypeDefaultDescription
x1number
y1number
x2number
y2number
kind'alignment' | 'spacing'
label?string

Enums

DiagramMode

Diagram mode enum - defines the operational mode of the diagram engine

ts
enum DiagramMode

Members

  • DESIGNER = 'designer' — Designer mode - Full editing capabilities (default)
  • All node/link operations enabled
  • Create, edit, delete, move, resize nodes
  • Create and delete links
  • RUNNING = 'running' — Running mode - Execution/simulation mode
  • Editing disabled
  • Nodes selectable for execution flow visualization
  • No structural changes allowed
  • VIEW = 'view' — View mode - Read-only viewing
  • All editing disabled
  • Nodes selectable for inspection
  • Pure viewing experience
  • DEBUG = 'debug' — Debug mode - Debugging mode
  • Similar to running but with debug capabilities
  • Breakpoints, step-through, inspection
  • No structural changes allowed
  • PRESENTATION = 'presentation' — Presentation mode - Clean presentation view
  • All editing disabled
  • Nodes selectable for navigation
  • Clean UI without clutter

Was this page helpful?