Skip to content

Architecture

Miravo's internal architecture — the engine, asset graph, event bus, tick scheduler, generator pipeline, model compiler, and protocol adapter interface.

Miravo follows a one-way data flow: Configuration goes in, protocol output comes out.

Config (.twin.yaml + .miravo.yaml)
-> Twin Runtime (compile models, spawn instances)
-> Tick Scheduler (fixed-interval loop)
-> Generators + Lifecycle + Faults (evaluate members)
-> Asset Graph (source of truth)
-> Event Bus (typed events)
-> Protocol Adapters (MQTT, OPC UA, and more)

The createEngine() factory wires everything together and returns the public API: start, stop, execute commands, register adapters, get metrics/state. The engine owns the lifecycle of all subsystems.

Registers compiled models and spawns asset instances. Each instance gets:

  • A forked RNG (deterministic per-instance randomness)
  • Varied parameters (based on variation setting)
  • Per-instance generator instances (stateful generators like random-walk are independent)
  • Writable member overrides (for methods like SetSpeed)

Runs the tick loop at a fixed interval (default 1000ms). Each tick:

  1. Snapshot each instance’s parameters (tick-consistent view)
  2. Evaluate all members in declaration order
  3. Apply lifecycle effects (value * multiplier + offset)
  4. Apply fault effects (spike, then multiplier, then offset)
  5. Write values to the asset graph (skip unchanged values)
  6. Emit tick:complete with the graph snapshot and delta

The scheduler uses writeMemberValue() to skip writes when both value and quality are unchanged, preserving timestamps and avoiding unnecessary delta entries.

The AssetGraph is the runtime source of truth. It holds all active AssetNode instances with their current member values, parameters, lifecycle state, and active faults. Snapshots are immutable by contract — protocol adapters receive a consistent point-in-time view.

A typed mitt bus carries all internal communication. Components never reference each other directly. Key events:

EventPayload
tick:complete{ graph: AssetGraphSnapshot, delta: AssetGraphDelta }
instance:created / instance:removed{ id }
lifecycle:changed{ instanceId, stage }
fault:triggered / fault:cleared{ instanceId, fault }
engine:state-changed{ from, to } (idle, running, paused, stopped)
adapter:enabled / adapter:disabled{ name, endpoint? }

compileModel() validates a raw model definition:

  • Topological ordering of member dependencies
  • $param.X reference validation (parameter must exist and be numeric)
  • input.member references (must be declared earlier)
  • Method argument types and writable targets
  • Lifecycle/fault effect targets

Invalid models fail at compile time, before the simulation starts.

All protocol adapters implement the ProtocolAdapter interface:

interface ProtocolAdapter {
name: string;
start(config: unknown): Promise<void>;
stop(): Promise<void>;
onTick(graph: AssetGraphSnapshot): void;
getMetrics(): AdapterMetrics;
}

Adapters receive the graph snapshot each tick. They never compute member values — they only project what the engine provides. This interface is how MQTT and OPC UA are implemented today, and how upcoming protocols (Modbus TCP, Sparkplug B, and others) will be added.

All current adapters use delta-driven updates. onTick() receives the full graph snapshot each tick. Structural changes (instance creation and removal) are processed as they occur so the address space stays consistent between ticks.

Miravo uses a seeded xoshiro128** PRNG. Each instance gets a forked RNG from the parent seed. Same seed + same config = bit-identical output. Math.random() and Date.now() are never used.

The ContentCatalog resolves models and templates through three layers:

  1. Current working directory
  2. Local registry (~/.miravo/registry/local/)
  3. Built-in content (@miravo/content package)

Higher-priority layers shadow lower ones by name.

State persistence saves structural snapshots to $MIRAVO_HOME/state/<name>.json. On startup, the engine restores from the snapshot if one exists. Saves are debounced and serialized to prevent write races.