Systems
While Operators represent modular visual or physical instances scattered across 3D meshes, Systems represent headless, singleton controllers running continuously in the background of the Aircada experience.
They have the same capabilities as Operators, but they have no Visual Studio presence (so their @Property fields are not visually exposed) and are instantiated exactly once to coordinate global application logic, analytical triggers, or complex multi-component rules.
2. Configuration Orchestrator Example#
Implementation of a hub orchestrator managing global state, applying rules, and communicating with spoke Operators.
// src/systems/ConfigOrchestrator.ts
import {
System, Context, Input, Output, Setup, Dispose,
AircadaContext, AirEvent
} from "@aircada/spec";
import { Events, EventPayloads, PedestalColor, SpinState } from "../registry/registry.generated";
@System({
name: "Configuration Orchestrator",
type: "config_orchestrator"
})
export class ConfigOrchestrator {
// 1. Context Fencing
@Context()
air!: AircadaContext;
// 2. The Master State (The single source of truth)
private currentColor: PedestalColor = "BLUE";
private currentSpin: SpinState = "IDLE";
// 3. Execution Commands (Outputs sent to the Spokes)
@Output({ name: "Apply Color", emits: Events.APPLY_COLOR })
onApplyColor = new AirEvent<EventPayloads[typeof Events.APPLY_COLOR]>();
@Output({ name: "Apply Spin State", emits: Events.APPLY_SPIN_STATE })
onApplySpinState = new AirEvent<EventPayloads[typeof Events.APPLY_SPIN_STATE]>();
// 4. Facts (Outcomes sent back to the UI)
@Output({ name: "Validation Failed", emits: Events.VALIDATION_FAILED })
onValidationFailed = new AirEvent<EventPayloads[typeof Events.VALIDATION_FAILED]>();
// Memory management closures
private unsubValidation?: () => void;
@Setup()
setup() {
console.log("ConfigOrchestrator initialized. Master state set to defaults.");
// Example of capturing local events for logging and memory safety
this.unsubValidation = this.onValidationFailed.addListener((payload) => {
console.warn(`[Hub Validation Error]: ${payload.reason}`);
});
// Initialize the scene to the default state
// We defer slightly to ensure all Spoke Operators have mounted
setTimeout(() => {
this.onApplyColor.invoke({ color: this.currentColor });
this.onApplySpinState.invoke({ state: this.currentSpin });
}, 100);
}
// 5. Hub Logic: Intercept UI Requests
@Input({ name: "Handle Color Request", onEvent: Events.REQUEST_COLOR_CHANGE })
handleColorRequest(payload: EventPayloads[typeof Events.REQUEST_COLOR_CHANGE]) {
this.currentColor = payload.color;
// Enforce implicit rule: If we turn red, we must stop spinning
if (this.currentColor === "RED" && this.currentSpin === "SPINNING") {
this.currentSpin = "IDLE";
this.onApplySpinState.invoke({ state: "IDLE" });
}
// Fire the validated execution command
this.onApplyColor.invoke({ color: this.currentColor });
}
@Input({ name: "Handle Spin Request", onEvent: Events.REQUEST_SPIN_TOGGLE })
handleSpinRequest(payload: EventPayloads[typeof Events.REQUEST_SPIN_TOGGLE]) {
// Enforce explicit business rule: Red cannot spin.
if (this.currentColor === "RED" && payload.state === "SPINNING") {
// Reject the command and inform the UI
this.onValidationFailed.invoke({ reason: "The pedestal cannot spin while it is RED." });
return;
}
// State is valid. Update master state and execute.
this.currentSpin = payload.state;
this.onApplySpinState.invoke({ state: this.currentSpin });
}
@Dispose()
dispose() {
if (this.unsubValidation) {
this.unsubValidation();
}
}
}1. Systems vs Operators#
A system class can implement all the same logic and use the same decorators that an operator can, but its instantiation and lifecycle are completely managed by the backend, in contrast to operators that can be instantiated numerous times.
And unlike operators, @Property declarations cannot be viewed or controlled in the docs.studio environment.
- Singleton Lifecycle: Automatically managed by the backend.
- No Studio UI:
@Propertydeclarations are not visible in the Visual Studio.
System Setup Lifecycle and Async Coordination#
Headless @System controllers support the exact same lifecycle stages, async scheduling, and topological dependency sorting mechanisms as @Operator components.
Stage Scheduling and Coordination
Just like Operators, Systems can declare @Setup({ stage: 'bootstrap' | 'initialize' | 'finalize' | 'runtime' }) methods. Systems are typically used to coordinate and instantiate multiple Operator child instances during the early bootstrap stage, ensuring that child operators are fully mounted and ready before later stages run.
Topological Constraints Across Systems
Systems can declare before and after execution dependencies relative to other Systems or Operators. This makes it straightforward to establish a clear bootstrapping order for complex configurator backends.
import { System, Context, Setup, AircadaContext } from '@aircada/spec';
@System({
name: 'OrchestratorSystem',
type: 'orchestrator_system'
})
export class OrchestratorSystem {
@Context()
air!: AircadaContext;
@Setup({ stage: 'bootstrap' })
async setup() {
console.log('[OrchestratorSystem] Instantiating child operators for dependency scheduler verification...');
// Programmatically create the dependent operators so they participate in later stages
await this.air.operators.create('normal_stage_operator');
await this.air.operators.create('topo_a');
await this.air.operators.create('topo_b');
await this.air.operators.create('topo_c');
}
}3. Programmatic Operator Instantiation#
In advanced orchestration scenarios, a headless @System can dynamically spawn, configure, and manage Operator instances at runtime rather than relying purely on static, designer-placed operators in the Studio scene.
This is accomplished using the this.air.operators.create("OPERATOR_TYPE", { properties }) API, exposing dynamic event listening and lifecycle delegation.
Lifecycle Stage Scheduling
Operators instantiated during a System's @Setup({ stage: 'bootstrap' }) hook are registered with the engine's loading manager in time to participate in all subsequent lifecycle phases. Once created, their @Setup hooks for initialize, finalize, and runtime stages will be automatically scheduled and executed in order by the engine alongside static operators.
Programmatically created operators function as headless, runtime-managed processors that can process inputs and emit signals, completely governed by the orchestrating System.
- Dynamic Creation: Instantiate helper operators on-demand during system initialization or in response to events.
- Lifecycle Delegation: The system must explicitly manage and clean up any programmatically spawned operators.
- Property Configuration: Properties are passed dynamically as configuration options matching the operator's
@Propertykeys.
import {
System, Context, Setup, Dispose,
AircadaContext
} from "@aircada/spec";
@System({
name: "Underline System",
type: "underline_system"
})
export class UnderlineSystem {
@Context()
air!: AircadaContext;
private boundsOperator!: any;
@Setup()
async setup() {
console.log("Initializing UnderlineSystem...");
// Programmatically spawn a helper operator at runtime
this.boundsOperator = await this.air.operators.create("BOUNDS", {
targetUnitId: "some-unit-id"
});
// Event subscription mapping for programmatically created operator signals
if (this.boundsOperator && this.boundsOperator.onBoundsChanged) {
this.boundsOperator.onBoundsChanged.addListener((bounds: any) => {
console.log("Dynamic bounds update received in System:", bounds);
});
}
}
@Dispose()
async dispose() {
console.log("Disposing UnderlineSystem and programmatically created operators...");
// CRITICAL: Clean up dynamically created operators to prevent runtime leaks
if (this.boundsOperator) {
await this.boundsOperator.dispose();
}
}
}





