Engine Lifecycle
The engine is the center of the runtime. It owns or accepts a GraphStore, adapts graph snapshots into renderer snapshots, coordinates layout execution, tracks viewport state, and handles mount/render/resize/teardown lifecycle.
import { createGraphEngine } from '@graphora/engine'
import { createCircularLayout } from '@graphora/layouts'
import { CanvasGraphRenderer, createViewport } from '@graphora/renderer'
const engine = createGraphEngine({
graph,
renderer: new CanvasGraphRenderer(),
layout: createCircularLayout(),
viewport: createViewport({ width: 640, height: 360 })
})
engine.mount(container)
await engine.runLayout({ fitToView: true })Applications can observe engine work without mixing it into the core graph mutation stream:
const unsubscribe = engine.onAnyLifecycle((event) => {
status.textContent = event.type
})
engine.onLifecycle('layout:completed', (event) => {
console.log(
`Applied ${event.appliedNodeCount} nodes at v${event.appliedVersion}`
)
})Engine-First Flow
- Create a graph store directly or pass raw graph data to
createGraphEngine. - Provide a renderer, layout, viewport, theme, and interaction styles as needed.
- Mount the engine into an
HTMLElement. - Run a layout or apply positions.
- Mutate graph data through the engine/store.
- Update viewport or interaction state through engine methods.
- Unmount or destroy the engine when the view is no longer needed.
Rendering
Mounted engines schedule renderer updates through render invalidation. Graph changes, viewport updates, layout application, interaction changes, and resize changes request a render. Pending renders are coalesced so repeated updates do not force immediate draw calls for every small change.
render() is still available as an immediate escape hatch for tests and tightly controlled integrations.
One requested frame emits render:scheduled. Further invalidations join that frame, and render:before/render:after expose the complete set of reasons. An immediate render() consumes pending reasons and cancels the scheduled handle. Rendering never starts layout work. Errors while creating the immutable render snapshot or drawing emit render:error and still propagate to the caller or scheduler.
Layout Execution
runLayout() calls the configured layout with the current core graph snapshot, applies returned positions atomically through the store's bulk position update, and can fit the viewport to the resulting bounds.
Use applyLayoutResult(result) when the app runs a layout itself. The engine validates public IDs and internal keys before mutating positions, so stale or unknown layout results fail clearly instead of partially applying.
Each layout:started has one terminal event: layout:completed, layout:stale, layout:cancelled, or layout:error. Newer runs cancel older runs, graph mutations make pending results stale, and destroy cancels them. These transitions settle the public runLayout() promise promptly. Graphora cannot stop arbitrary work hidden behind an adapter promise, so that computation may continue, but a late result cannot apply and a late rejection is consumed.
Layout positions publish atomically. Core position and graph events occur before layout:completed; its appliedVersion identifies that publication. Viewport fitting is computed and installed before position events, so a subscriber that changes the viewport during publication keeps its newer value.
Observer And Reentrancy Policy
Lifecycle listeners are notifications. Their exceptions go to the optional lifecycleObserverErrorHandler; without one, the platform reporter or an asynchronous throw makes the failure visible while engine state continues.
A listener may mutate graph or viewport state, start another layout, render, unmount, or destroy. Reentrant layout starts wait until the current event has reached its subscribers. Reentrant render() does not recurse, and mutations during a draw request one later coalesced frame. Unmount or destroy during render:before prevents the renderer call. engine:destroyed is delivered once as the final lifecycle event, after required layout terminal delivery.
Resize And Teardown
mount(container) reads the container size when possible, resizes the renderer, and schedules the first render. When ResizeObserver exists, the engine watches the container and updates renderer size and viewport dimensions.
unmount() and destroy() cancel pending renders and disconnect resize observation. destroy() is the final cleanup path.
Renderer cleanup failures propagate after all cleanup attempts and the final destroy notification. Calling destroy() again is a no-op.
Current Boundary
The engine does not implement drawing algorithms, layout algorithms, DOM interaction listeners, React behavior, custom command systems, or a public continuous-simulation controller. Those belong to renderer, layouts, interactions, React, app code, or the separate GAP-L07 simulation work.