Models as the Heart of KOS
In traditional web development, you might start with components, routes, or database schemas. In KOS, you start with models—digital representations of the physical entities your application needs to manage. These aren’t just data containers or business logic classes; they’re living representations of real-world objects that automatically stay synchronized with their physical counterparts.
Building a KOS model means creating a piece of code that thinks and behaves like the physical thing it represents. When you model a sensor, your code should naturally reflect how sensors work—they take readings, they can be calibrated, they might need maintenance. When you model a pump, it should understand flow rates, operational states, and error conditions.
This approach creates applications that feel intuitive because the code structure mirrors the mental model that users already have about the physical systems they’re working with.
The KOS CLI: Your Model Generator
The fastest way to create well-structured KOS models is through the KOS CLI’s model generator. This tool creates all the boilerplate code, follows established conventions, and sets up the proper file structure for your models.
# Create a basic sensor model
kosui model --name sensor --project app-models
# Create a sensor model with container management (recommended for most models)
kosui model --name sensor --project app-models --container
# Create a singleton model (only one instance allowed)
kosui model --name system-config --project app-models --singletonThe CLI generator creates more than just a model file—it creates a complete, production-ready structure that includes type definitions, registration patterns, and integration points for services and UI components.
Understanding when to use the --container flag is crucial for building scalable applications. Use containers when you’re managing multiple instances of the same type of thing: multiple sensors, multiple devices, multiple users. Skip containers when you’re building coordinators or managers that handle a single unified system.
Anatomy of a KOS Model
Every KOS model follows a consistent structure that makes them predictable and maintainable. Let’s build a sensor model step by step to understand how the pieces fit together.
The Foundation: Model Declaration
import { kosModel, kosTopicHandler, IKosDataModel } from "@kosdev-code/kos-ui-sdk";
@kosModel({ modelTypeId: "sensor-model", singleton: false })
export class SensorModelImpl implements IKosDataModel {
currentReading?: SensorReading;
lastCalibration?: Date;
status: 'online' | 'offline' | 'maintenance' = 'offline';
// Real-time data updates from the physical sensor
@kosTopicHandler({
topic: TOPIC_READING,
websocket: true,
})
handleReading(payload: SensorReading): void {
this.currentReading = payload;
this.status = 'online';
}
}The @kosModel decorator tells the KOS framework that this class represents a model, and the string parameter identifies the model type throughout the system. The IKosDataModel interface ensures your model has the basic structure that KOS expects.
This foundation gives you a model that can receive real-time updates from physical sensors, automatically updating its state as conditions change in the real world.
Adding Lifecycle Management
KOS models have a well-defined lifecycle that ensures proper initialization and cleanup. Understanding this lifecycle helps you put initialization code in the right places and ensures your models start up reliably.
@kosModel({ modelTypeId: "sensor-model", singleton: false })
export class SensorModelImpl implements IKosDataModel {
private deviceAPI?: DeviceAPI;
// Phase 1: Basic setup - no async operations
async init(): Promise<void> {
this.logger.debug(`Initializing sensor ${this.id}`);
this.status = 'initializing';
}
// Phase 2: Load baseline data from backend
async load(): Promise<void> {
this.logger.debug(`Loading sensor ${this.id}`);
// Fetch current sensor configuration and state
const [error, sensorData] = await this.sensorService.getSensorState(this.id);
if (!error && sensorData) {
this.currentReading = sensorData.lastReading;
this.lastCalibration = new Date(sensorData.calibrationDate);
this.status = sensorData.status;
}
}
// Models become "online" and start processing events after load completes
}The lifecycle pattern ensures that your model has all the data it needs before it starts processing real-time updates. This prevents race conditions and ensures consistent behavior across your application.
Container Models: Managing Collections
When you’re working with multiple instances of the same type—like managing several sensors across a facility—container models provide powerful collection management capabilities.
@kosModel({ modelTypeId: "sensor-container-model", singleton: true })
export class SensorContainerModelImpl implements IKosDataModel {
private _sensors = new KosModelContainer<SensorModel>({
indexMap: {
byType: "sensorType",
byLocation: "location",
offline: (sensor) => sensor.status === "offline",
needsCalibration: (sensor) =>
sensor.lastCalibration &&
Date.now() - sensor.lastCalibration.getTime() > 30 * 24 * 60 * 60 * 1000 // 30 days
}
});
get sensors(): SensorModel[] {
return this._sensors.data;
}
// Efficient queries using built-in indexing
getOfflineSensors(): SensorModel[] {
return this._sensors.getIndexByKey("offline", true);
}
getSensorsByLocation(location: string): SensorModel[] {
return this._sensors.getIndexByKey("byLocation", location);
}
async load(): Promise<void> {
// Load all sensors from the backend
const [error, sensorsData] = await this.sensorService.getAllSensors();
if (!error && sensorsData) {
// Create individual sensor models and add to container
const sensors = sensorsData.map(data =>
Sensor.instance(data.id).options(data).build()
);
sensors.forEach(sensor => this._sensors.addModel(sensor));
}
}
// Real-time collection updates
@kosTopicHandler({ topic: TOPIC_SENSOR_ADDED, websocket: true })
handleSensorAdded(payload: SensorData): void {
const sensor = Sensor.instance(payload.id).options(payload).build();
this._sensors.addModel(sensor);
}
@kosTopicHandler({ topic: TOPIC_SENSOR_REMOVED, websocket: true })
handleSensorRemoved(payload: { sensorId: string }): void {
this._sensors.removeModel(payload.sensorId);
}
}Container models transform what could be complex collection management logic into simple, declarative configurations. The indexing system automatically maintains efficient lookups, and the reactive nature ensures UI components automatically update when the collection changes.
Connecting Models to Physical Systems
The real power of KOS models comes from their ability to stay synchronized with physical systems through the communication patterns we explored earlier. Let’s see how a complete model integrates multiple communication patterns to create a comprehensive representation of a physical device.
Configuration Properties: Operational Parameters
Physical devices have parameters that control their behavior. KOS configuration properties provide structured access to these parameters with automatic type conversion, validation, and persistence.
@kosModel({ modelTypeId: "pump-model", singleton: false })
export class PumpModelImpl implements IKosDataModel {
// Configurable operational parameter from the device
@kosConfigProperty({
path: "assembly:core:board:micro:pump:S4",
attribute: "nominalRate"
})
private nominalRateConfig!: KosConfigProperty<number>;
// Access the configured value with type safety
get flowRate(): number {
return this.nominalRateConfig.value ?? 1.0; // Default fallback
}
// Update configuration with automatic persistence
async updateFlowRate(newRate: number): Promise<void> {
this.nominalRateConfig.updateProperty(newRate);
this.logger.info(`Flow rate updated to ${newRate}`);
}
// Business logic based on configuration
get canDispense(): boolean {
return this.flowRate > 0 && this.status === 'ready';
}
}Configuration properties bridge the gap between your user interface and the physical parameters that control device behavior. They handle all the complexity of backend communication while providing a simple, type-safe interface for your application logic.
State Monitoring: Reflecting Physical Reality
Some aspects of physical systems can’t be controlled—they can only be observed. State properties provide read-only access to these physical conditions, automatically updating as the real world changes.
@kosModel({ modelTypeId: "door-model", singleton: false })
export class DoorModelImpl implements IKosDataModel {
// Read-only state that reflects actual door position
@kosStateProp({
path: "/hardware/doors/front",
attribute: "state"
})
private doorState!: KosStateProp<"open" | "closed">;
get isOpen(): boolean {
return this.doorState.value === "open";
}
// Automatic response to physical state changes
@kosModelEffect({
dependencies: (model) => [model.doorState.value],
})
handleDoorStateChange(): void {
if (this.isOpen) {
this.pauseOperations();
this.showMaintenanceAccess();
} else {
this.resumeOperations();
this.hideMaintenanceAccess();
}
}
}State properties make your application naturally responsive to physical conditions without requiring polling or manual refresh operations. The reactive system ensures that business logic automatically executes when physical conditions change.
Long-Running Operations: Futures in Action
Physical systems often require operations that take significant time to complete. KOS Futures provide structured support for these extended procedures with progress tracking and user feedback.
@kosModel({ modelTypeId: "calibration-model", singleton: false })
export class CalibrationModelImpl implements IKosDataModel {
@kosFuture()
calibrationFuture?: Future<CalibrationResult>;
async startCalibration(pumpId: string): Promise<void> {
const [error, future] = await this.calibrationService.startPumpCalibration(pumpId);
if (error) {
this.logger.error("Failed to start calibration", error);
return;
}
this.calibrationFuture = future;
}
// Automatic progress monitoring
@kosModelEffect({
dependencies: (model) => [model.calibrationFuture?.progress],
})
handleProgressUpdate(): void {
if (this.calibrationFuture?.progress) {
this.logger.info(`Calibration ${this.calibrationFuture.progress.percent}% complete`);
// UI components automatically update with progress
}
}
@kosModelEffect({
dependencies: (model) => [model.calibrationFuture?.isComplete],
})
handleCalibrationComplete(): void {
if (this.calibrationFuture?.isComplete) {
const result = this.calibrationFuture.result;
if (result.success) {
this.applyCalibrationResults(result);
this.logger.info("Calibration completed successfully");
} else {
this.handleCalibrationFailure(result.error);
}
}
}
}Futures transform potentially frustrating user experiences into informative, manageable processes. Users see clear progress indication and can make informed decisions about ongoing operations.
Model Registration and Factory Patterns
KOS models use a registration system that makes them available throughout your application while maintaining proper lifecycle management and dependency resolution. The @kosModel decorator automatically injects a static Registration property onto your model class:
// sensor-model.ts
export const MODEL_TYPE = "sensor-model";
export type SensorModel = PublicModelInterface<SensorModelImpl>;
@kosModel({ modelTypeId: MODEL_TYPE, singleton: false })
@kosLoggerAware()
export class SensorModelImpl implements IKosDataModel, IKosIdentifiable {
// Registration property injected by @kosModel decorator
static Registration: KosModelRegistrationType<SensorModel, SensorOptions>;
id: string;
// ... model implementation
}
// Convenient export for creating instances
export const Sensor = SensorModelImpl.Registration;The registration pattern provides a clean factory interface for creating model instances while hiding the complexity of the underlying instantiation process:
// Create model instances with proper configuration
const temperatureSensor = Sensor.instance("temp-001")
.options({
sensorType: "temperature",
location: "main-room",
calibrationInterval: 30
})
.build();
// Singleton models don't need IDs
const systemConfig = SystemConfig.instance()
.options({ environment: "production" })
.build();
// Force update existing instances
const updatedSensor = Sensor.instance("temp-001")
.forceUpdate
.options({ calibrationInterval: 15 })
.build();Legacy Registration Pattern (Pre-2.x)
If you encounter separate -registration.ts files in older KOS codebases, these represent a legacy registration pattern that has been phased out in favor of the decorator-based approach shown above. The modern pattern eliminates boilerplate while maintaining the same factory interface.
Legacy Pattern (for reference only):
// OLD: Separate registration file (no longer used)
// sensor-registration.ts
export const Sensor = new KosModelRegistrationFactory<
SensorModel,
SensorOptions
>({
class: SensorModelImpl,
type: MODEL_TYPE,
});
// OLD: Constructor with KosCreationContext
constructor(
modelId: string,
options: SensorOptions,
context: KosCreationContext
) {
this.logger = context.logger; // Manual logger injection
}Modern Pattern (current):
The @kosModel decorator automatically injects the Registration property, and the @kosLoggerAware decorator handles logger injection. This eliminates separate registration files and enables cleaner constructor signatures with decorator-based service injection.
Building Reactive Business Logic
KOS models become powerful when they combine multiple communication patterns with reactive business logic. Model effects let you create logic that automatically executes when specific conditions change.
@kosModel({ modelTypeId: "beverage-dispenser-model", singleton: false })
export class BeverageDispenserModelImpl implements IKosDataModel {
@kosDependency({ modelType: TroubleContainerModel.type })
troubleContainer!: TroubleContainerModel;
@kosConfigProperty({
path: "assembly:core:board:micro:pump:S4",
attribute: "nominalRate"
})
pumpRate!: KosConfigProperty<number>;
@kosStateProp({
path: "/hardware/doors/front",
attribute: "state"
})
doorState!: KosStateProp<"open" | "closed">;
// Observable getter - compute operational state from conditions
get operationalState(): string {
const hasCriticalTroubles = this.troubleContainer.troubles
.some(trouble => trouble.severity >= 8);
const doorClosed = this.doorState.value === "closed";
const pumpConfigured = this.pumpRate.value > 0;
if (hasCriticalTroubles) return 'error';
if (!doorClosed) return 'maintenance';
if (pumpConfigured) return 'ready';
return 'configuration-needed';
}
// Model effect - perform side effects when operational state changes
@kosModelEffect({
dependencies: (model) => [model.operationalState],
})
handleOperationalStateChange(): void {
switch (this.operationalState) {
case 'error':
this.disableDispensing();
break;
case 'maintenance':
this.pauseDispensing();
break;
case 'ready':
this.enableDispensing();
break;
case 'configuration-needed':
this.showConfigurationPrompt();
break;
}
}
get canDispense(): boolean {
return this.operationalState === 'ready';
}
}Model effects create applications that feel intelligent because they automatically adapt to changing conditions. The reactive system ensures this logic executes immediately when any relevant condition changes, without requiring manual triggers or polling.
Integration with React Components
KOS models integrate seamlessly with React through generated hooks that provide reactive data access and proper lifecycle management.
// Generated hook provides reactive model access
function SensorDashboard() {
const { model: sensorContainer } = useSensorContainer();
const offlineSensors = sensorContainer.getOfflineSensors();
const sensorsNeedingCalibration = sensorContainer.sensors.filter(
sensor => sensor.needsCalibration
);
return (
<div>
<h2>Sensor Status</h2>
<p>Total Sensors: {sensorContainer.sensors.length}</p>
<p>Offline: {offlineSensors.length}</p>
{sensorsNeedingCalibration.length > 0 && (
<div className="alert">
<h3>Calibration Required</h3>
{sensorsNeedingCalibration.map(sensor => (
<SensorCalibrationCard key={sensor.id} sensor={sensor} />
))}
</div>
)}
<div className="sensor-grid">
{sensorContainer.sensors.map(sensor => (
<SensorCard key={sensor.id} sensor={sensor} />
))}
</div>
</div>
);
}The reactive integration means your UI components automatically update when model data changes—when new sensors come online, when readings update, or when operational conditions change. This creates user interfaces that feel naturally responsive to the physical world.
Advanced Model Patterns
Model Relationships and Dependencies
Models can establish relationships with other models to create complex system representations:
@kosModel({ modelTypeId: "brewing-system-model", singleton: false })
export class BrewingSystemModelImpl implements IKosDataModel {
@kosChild brewingContainer?: IngredientContainerModel;
@kosChild temperatureControl?: TemperatureControlModel;
// Critical dependency - must exist or initialization fails
@kosDependency({
modelType: SystemConfigModel.type,
lifecycle: DependencyLifecycle.INIT,
resolutionPolicy: DependencyResolutionPolicy.FAIL
})
systemConfig!: SystemConfigModel;
// Optional analytics - graceful degradation if unavailable
@kosDependency({
modelType: AnalyticsModel.type,
lifecycle: DependencyLifecycle.READY,
resolutionPolicy: DependencyResolutionPolicy.CONTINUE
})
analytics?: AnalyticsModel;
async init(): Promise<void> {
// Child models are created automatically
this.brewingContainer = IngredientContainer.instance()
.options({ maxIngredients: 8 })
.build();
this.temperatureControl = TemperatureControl.instance()
.options({
targetTemp: this.systemConfig.defaultBrewingTemp,
tolerance: 2.0
})
.build();
}
// Coordinated system operations
async startBrewing(recipe: BrewingRecipe): Promise<void> {
// Ensure all subsystems are ready
if (!this.brewingContainer?.hasRequiredIngredients(recipe.ingredients)) {
throw new Error("Missing required ingredients");
}
if (!this.temperatureControl?.isAtTargetTemp()) {
await this.temperatureControl.heatToTarget();
}
// Optional analytics tracking - safely check before use
if (this.analytics) {
this.analytics.trackBrewingStart(recipe.id);
}
// Start coordinated brewing process
await this.executeBrewingSequence(recipe);
}
}Dependency Resolution Policies
The @kosDependency decorator supports three resolution policies to handle different dependency scenarios:
- CREATE (default): Attempts to create the dependency if it doesn’t exist. Throws an exception if the model requires options that aren’t provided (when the model has
optionsRequired: true). - FAIL: Explicitly fails with detailed error logging if the dependency cannot be resolved. Use for critical dependencies where absence should halt execution.
- CONTINUE: Logs an error but allows the model to initialize with an undefined dependency. Use for optional features that can degrade gracefully.
Options Validation
Models can declare that they require options using the optionsRequired flag in the @kosModel decorator:
@kosModel({
modelTypeId: "network-interface",
singleton: false,
optionsRequired: true // This model cannot be created without options
})
export class NetworkInterfaceImpl implements IKosDataModel {
constructor(modelId: string, options: NetworkOptions) {
// Options are guaranteed to be provided when created properly
this.interface = options.interface;
this.address = options.address;
}
}When a model with optionsRequired: true is used as a dependency without providing options, the framework will throw a clear error with the CREATE policy:
// This will fail because NetworkInterface requires options
@kosDependency({
modelType: "network-interface"
// No options provided!
})
network?: NetworkInterfaceData;
// Error: Model network-interface requires options but none were provided.
// Use resolutionPolicy: DependencyResolutionPolicy.CONTINUE if this dependency is optional.When dependency resolution fails, the framework provides comprehensive diagnostic logging including the dependency type, parent model information, provided options, and the specific error encountered. This makes troubleshooting dependency issues straightforward.
Model relationships create natural hierarchies that mirror the structure of the physical systems you’re controlling, making the application architecture intuitive and maintainable.
Testing KOS Models
KOS models are designed to be easily testable through dependency injection and clear separation of concerns:
describe("SensorModel", () => {
let sensor: SensorModel;
let mockSensorService: jest.Mocked<SensorService>;
beforeEach(() => {
mockSensorService = createMockSensorService();
sensor = Sensor.instance("test-sensor")
.options({
sensorType: "temperature",
location: "test-lab"
})
.build();
// Inject mock service
sensor.sensorService = mockSensorService;
});
test("should update status when receiving readings", () => {
// Act
sensor.handleReading({
value: 23.5,
unit: "°C",
timestamp: new Date().toISOString()
});
// Assert
expect(sensor.status).toBe("online");
expect(sensor.currentReading?.value).toBe(23.5);
});
test("should load baseline state during initialization", async () => {
// Arrange
mockSensorService.getSensorState.mockResolvedValue([
null,
{
lastReading: { value: 20.0, unit: "°C" },
calibrationDate: "2023-01-01T00:00:00Z",
status: "online"
}
]);
// Act
await sensor.load();
// Assert
expect(sensor.currentReading?.value).toBe(20.0);
expect(sensor.status).toBe("online");
});
});The clear separation between models, services, and external dependencies makes it straightforward to create comprehensive test suites that verify both individual model behavior and system integration.
Building Your Model Development Workflow
Creating effective KOS models becomes easier when you follow a consistent development workflow:
- Start with the CLI generator – Use the appropriate flags for your use case
- Define your data types – Create clear interfaces for your model’s data
- Implement lifecycle methods – Add init() and load() logic for proper startup
- Add communication patterns – Connect to backend systems through topics, config properties, and state monitoring
- Build business logic – Use model effects for reactive behavior
- Create container indexes – Define efficient query patterns for collections
- Write tests – Verify model behavior and integration points
- Generate React hooks – Create UI integration points
This systematic approach ensures your models are well-structured, properly integrated, and maintainable as your application grows in complexity.
Bringing Physical and Digital Worlds Together
When you build KOS models effectively, you create applications that feel naturally connected to the physical systems they control. Users don’t think about the complexity of backend communication, real-time updates, or state synchronization—they simply interact with digital representations that behave exactly like their physical counterparts.
This is the true power of the KOS model-first approach: it transforms complex technical challenges into intuitive application experiences by building software that thinks like the physical world it represents.
Ready to connect your models to React components? Continue with React Integration Patterns to learn how KOS models provide reactive data to your user interface, or explore Advanced Model Patterns for complex scenarios involving multiple systems and coordinated operations.
