Graphora API
    Preparing search index...

    Module @graphora/engine

    Engine Package

    Package name: @graphora/engine

    Responsibility: framework-agnostic runtime orchestration for Graphora.

    The engine creates or accepts a GraphStore, exposes core graph selectors and event subscriptions, coordinates renderer and layout lifecycle hooks, and owns teardown cleanup. It does not implement rendering, layout algorithms, interactions, viewport math, or React behavior.

    import { createGraphEngine } from '@graphora/engine'

    const engine = createGraphEngine({
    graph: {
    nodes: [{ id: 'a' }, { id: 'b' }],
    edges: [{ source: 'a', target: 'b' }]
    }
    })

    engine.on('graph:changed', (event) => {
    console.log(event.version)
    })

    engine.mount(container)
    engine.loadGraph({ nodes: [{ id: 'next' }] })
    engine.destroy()

    Renderer integrations use the public GraphRenderer contract from @graphora/renderer. GraphEngineRenderer remains as a compatibility type alias, not a separate renderer API. The engine adapts core graph snapshots into renderer-ready RenderSnapshot values before calling renderer.render.

    Theme and interaction style inputs are also passed through createGraphEngine and resolved while the engine creates render snapshots:

    const engine = createGraphEngine({
    graph,
    renderer,
    theme: {
    backgroundColor: '#f8fafc',
    node: { fill: '#94a3b8', radius: 18 },
    edge: { stroke: '#475569' },
    label: { visible: true }
    },
    interactionStyles: {
    hover: { node: { fill: '#38bdf8' } },
    selection: { node: { stroke: '#7c3aed', strokeWidth: 3 } }
    }
    })

    The engine keeps style resolution in @graphora/renderer; it only supplies the graph snapshot items, theme input, explicit item attributes, and current interaction state. setInteractionState(input) updates hover, selection, focus, or drag render state and schedules a redraw when the engine is mounted.

    Layout integrations use the public GraphLayout contract from @graphora/layouts. GraphEngineLayout remains as a compatibility type alias, not a separate engine-owned layout API. Layout execution is explicit:

    import { createGraphEngine } from '@graphora/engine'
    import { createCircularLayout } from '@graphora/layouts'

    const engine = createGraphEngine({
    graph: {
    nodes: [{ id: 'a' }, { id: 'b' }],
    edges: [{ source: 'a', target: 'b' }]
    },
    layout: createCircularLayout()
    })

    await engine.runLayout({ fitToView: true })

    Size-aware layouts use explicit renderer-neutral geometry. The default producer derives each built-in Canvas node's fill-body box from the engine theme and node attributes; interaction emphasis is excluded and label measurement remains a separate follow-up. It does not infer persistent stroke or decoration extents, so callers that need that clearance must include it in padding. Install the frozen snapshot before running the layout:

    const geometry = engine.createLayoutGeometry({
    padding: 8,
    styleRevision: 0,
    metricsRevision: 0
    })

    engine.setLayoutGeometry(geometry)
    await engine.runLayout({ fitToView: true })

    getLayoutGeometry() returns the active isolated snapshot. Graph changes clear it, while a successful layout position commit conditionally rebases it to the applied graph version. Reentrant observers that mutate the graph or install new measurements prevent the old snapshot from being restored.

    runLayout() calls the configured layout with the current graph snapshot, applies the returned positions through one atomic position update, and returns the applied result metadata. applyLayoutResult(result) is available when callers run a layout themselves. It validates that every result node still exists by public id and internal key before mutating the store, so stale or unknown layout results fail clearly instead of partially applying.

    The returned appliedVersion identifies the graph version assigned to that atomic position publication. layout:completed follows the core position and graph events for that version.

    Async layouts use the same contract. The engine rejects a pending layout result before applying it when a newer runLayout() call has superseded it or when the graph version changed while it was running. Future worker-backed layouts should present themselves as normal async layouts and rely on this engine-level guard in addition to any transport-level cancellation.

    Layout results can explicitly replace or clear edge routes and measured label placements. Install matching geometry first. Manual applyLayoutResult validates all fields atomically; runLayout({ output }) additionally enforces requested outputs and declared capabilities. Defaults are ignore; required unsupported outputs fail before calculation. Inspect frozen stage state with getLayoutArtifacts(): null means absent, an empty array means current empty output.

    Position patches invalidate previous routes/labels; route updates invalidate old labels; label-only stages preserve routes. Same-result replacements are rebased after the exact successful position commit. Reentrant source/view/geometry changes cannot revive stale artifacts. New geometry, drag, filtering and data mutations clear stage state; camera/selection changes preserve it. Canvas uses accepted world-space polylines and label centers for drawing, picking and bitmap export. This is the application boundary; obstacle-routing and label-placement algorithms remain separate features. See docs/concepts/layout-artifacts.md.

    Engine work has its own typed event stream, separate from core graph mutation events. Subscribe to one event type or to the complete lifecycle union:

    const stopWatchingLayouts = engine.onLifecycle(
    'layout:completed',
    ({ runId, appliedVersion, appliedNodeCount }) => {
    console.log({ runId, appliedVersion, appliedNodeCount })
    }
    )

    const stopWatchingEverything = engine.onAnyLifecycle((event) => {
    console.log(event.type)
    })

    Render work emits render:scheduled, render:before, render:after, and render:error. Scheduled frames carry their first reason; execution events carry every coalesced reason and the graph snapshot version used for the draw. Layout runs emit layout:started and exactly one of layout:completed, layout:stale, layout:cancelled, or layout:error. engine:destroyed is the last lifecycle event and clears the subscriptions.

    Starting a newer layout cancels the engine-facing promise for older work, graph changes mark pending work stale, and destroy cancels it. A layout adapter owns its underlying computation: an opaque promise can continue after cancellation, but its late result or rejection is consumed and cannot update the engine. The built-in browser Worker layout can also terminate its private worker transport.

    The engine can own an ordered list of synchronous node and edge filters. The canonical source remains available from snapshot(), while viewSnapshot() is the current graph passed to rendering, layout, and guarded engine.pick():

    const engine = createGraphEngine({
    graph,
    renderer,
    viewFilters: [
    {
    id: 'active',
    kind: 'node-filter',
    predicate: (node) => node.data?.active === true
    }
    ]
    })

    engine.setViewFilterEnabled('active', false)
    engine.setViewFilters(nextFilters)

    Filtered snapshots expose sourceVersion, stable { id, version } view identity, coherent clone-safe viewAdjacency, and frozen source provenance. Source mutations reevaluate installed filters automatically. If that refresh fails, getViewState() retains the last committed snapshot for diagnostics while current layout and picking stay blocked until refreshView() or a successful configuration replacement recovers the view.

    Filter predicates must be pure, synchronous, and return booleans. Grouping, collapse/expand, synthetic topology, and many-source provenance are outside the filter runtime and remain follow-up work.

    Observer exceptions do not interrupt engine bookkeeping. Provide lifecycleObserverErrorHandler to report them through application telemetry; otherwise the engine uses the platform error reporter or an asynchronous throw. The optional handler's own errors use the same default reporting path.

    Viewport state can be read or updated without reaching into the renderer:

    const viewport = engine.getViewport()

    engine.setViewport({
    center: { x: viewport.center.x, y: viewport.center.y },
    zoom: 2
    })

    Mounted engines schedule renderer updates through render invalidation. Graph changes, viewport updates, layout application, and resize changes request a render and coalesce while a frame is already pending. render() remains available as an immediate escape hatch for tests or controlled integrations.

    engine.invalidateRender()
    engine.resize({ width: 640, height: 360 })
    engine.render()

    resize(size) updates the engine viewport dimensions, calls renderer.resize(size), and invalidates the mounted renderer. During mount, the engine reads the container dimensions when possible, resizes the renderer before the first scheduled render, and attaches a ResizeObserver when the environment provides one. unmount() and destroy() cancel pending renders and disconnect resize observation so renderer work does not run after teardown.

    Tests and non-browser adapters can inject a narrow renderScheduler or resizeObserverFactory through createGraphEngine options. These hooks are for lifecycle integration only; application features should still use the public engine, renderer, layout, and interaction APIs.

    Dedicated renderer, layout, interaction, viewport, and React work should extend their own packages rather than expanding this package into an application SDK.

    createGraphPositionMemory() preserves finite coordinates across filtering and expand/collapse visibility changes without mutating source graph input. Capture the current runtime snapshot before loading a newly derived graph, then prepare that graph before passing it to the engine:

    const positions = createGraphPositionMemory()

    positions.capture(engine.snapshot())
    engine.loadGraph(positions.prepare(nextVisibleGraph))

    Incoming finite node.position values are authoritative. Otherwise the helper restores the last position for the normalized node ID. Never-seen nodes are placed deterministically near visible neighbors, near the last collapsed group that represented them, or in a stable fallback spiral. Hidden positions remain in this helper instance until clear(); they are not persisted globally.

    createClusteredGraph(snapshot, options) derives a clustered visible graph from an existing source snapshot without mutating the source GraphStore.

    import { createGraphStore } from '@graphora/core'
    import { createClusteredGraph } from '@graphora/engine'

    const store = createGraphStore({
    nodes: [
    { id: 'api', data: { domain: 'ingest' } },
    { id: 'queue', data: { domain: 'ingest' } }
    ],
    edges: [{ id: 'api-queue', source: 'api', target: 'queue' }]
    })

    const clusteredGraph = createClusteredGraph(store.snapshot(), {
    getClusterId: (node) => node.data?.domain
    })

    The returned value is normal RawGraph input: synthetic cluster nodes replace eligible source nodes, edges between the same visible endpoints are grouped, and internal cluster edges are hidden. Synthetic nodes and grouped edges carry GRAPHORA_CLUSTER_ATTRIBUTE metadata with the source node or edge IDs they represent. Expand/collapse state, community-detection algorithms, nested folding, and a full transform pipeline remain separate future work.

    createCollapseState() and createCollapsedGraph(snapshot, options) provide the first framework-agnostic group visibility helper.

    import {
    createCollapsedGraph,
    createCollapseState,
    toggleCollapsedGroup
    } from '@graphora/engine'

    let collapseState = createCollapseState({
    collapsedGroupIds: ['backend']
    })

    const visibleGraph = createCollapsedGraph(store.snapshot(), {
    state: collapseState,
    getGroupId: (node) => node.data?.area
    })

    collapseState = toggleCollapsedGroup(collapseState, 'backend')

    Collapsed groups become synthetic visible nodes, internal source edges are hidden, and crossing edges are grouped when they resolve to the same visible endpoints. Synthetic collapsed nodes and grouped edges carry GRAPHORA_COLLAPSE_ATTRIBUTE metadata with represented source IDs. The helper does not mutate the source store and does not implement nested folding, drilldown graph views, parent-relative editor geometry, or selection expansion policies.

    installGraphExtension installs explicit behavior/plugin objects with scoped activation, disable and disposal. See scoped extensions for ownership, cleanup, failure handling and the runnable counter example.

    styleRules in engine options and setStyleRules accept ordered typed node/edge rules with stable IDs, optional pure predicates and constant or synchronous appearance callbacks. getStyleRules returns frozen configuration; refreshStyles handles captured application state. Rules do not mutate core attributes; item attributes and interaction overlays retain priority.

    Evaluation is cached per visible snapshot/configuration and reused for camera, picking and interaction changes. Replacement preflights atomically; later data errors surface as GraphStyleEvaluationError through the render lifecycle and prevent stale picking/measurements. Layout base sizes use the same rules; paint-only updates retain geometry, while geometry-sensitive updates invalidate measurements/artifacts. See the styling guide.

    GraphEngine
    GraphStyleEvaluationError
    GraphViewUnavailableError
    ClusterEdgeGroup
    ClusteredGraphOptions
    ClusterGroup
    CollapsedGraphOptions
    CollapseEdgeGroup
    CollapseGroup
    CollapseState
    CollapseStateInput
    CreateLayoutGeometryOptions
    EdgeAppearance
    GraphEngineDestroyedEvent
    GraphEngineLayoutApplication
    GraphEngineLayoutCancelledEvent
    GraphEngineLayoutCompletedEvent
    GraphEngineLayoutErrorEvent
    GraphEngineLayoutOptions
    GraphEngineLayoutRun
    GraphEngineLayoutStaleEvent
    GraphEngineLayoutStartedEvent
    GraphEngineOptions
    GraphEngineRenderAfterEvent
    GraphEngineRenderBeforeEvent
    GraphEngineRenderErrorEvent
    GraphEngineRenderScheduledEvent
    GraphEngineRenderScheduler
    GraphEngineResizeObserver
    GraphEngineViewChangedEvent
    GraphEngineViewErrorEvent
    GraphExtension
    GraphExtensionContext
    GraphExtensionHandle
    GraphLayoutArtifacts
    GraphPositionMemory
    GraphPositionMemoryOptions
    GraphStyleRule
    GraphStyleRules
    GraphViewAdjacency
    GraphViewAdjacencyEntry
    GraphViewFilterContext
    GraphViewIdentity
    GraphViewProvenance
    GraphViewSnapshot
    NodeAppearance
    SourceViewEdgeProvenance
    SourceViewNodeProvenance
    EdgeStyleRule
    GraphEngineLayout
    GraphEngineLifecycleEvent
    GraphEngineLifecycleEventType
    GraphEngineLifecycleListener
    GraphEngineLifecycleObserverErrorHandler
    GraphEngineRenderer
    GraphEngineRenderHandle
    GraphEngineRenderMode
    GraphEngineRenderReason
    GraphEngineResizeObserverFactory
    GraphViewFilter
    GraphViewPickResult
    GraphViewRefreshReason
    GraphViewRefreshResult
    GraphViewState
    NodeStyleRule
    ResolvedGraphStyleRules
    enginePackageDependencies
    enginePackageInfo
    enginePackageName
    GRAPHORA_CLUSTER_ATTRIBUTE
    GRAPHORA_COLLAPSE_ATTRIBUTE
    collapseGroup
    createClusteredGraph
    createCollapsedGraph
    createCollapseState
    createGraphEngine
    createGraphPositionMemory
    expandGroup
    installGraphExtension
    isGroupCollapsed
    toggleCollapsedGroup