Event-Driven Communication – Real-Time Device Synchronization

KOS applications use event-driven communication to maintain real-time synchronization between physical devices and the model layer. This approach creates responsive, accurate interfaces that automatically reflect the true state of hardware.

The Communication Philosophy

Traditional Web Communication: Pull Model

Traditional web applications use a “pull” model where the client requests information:

  • User clicks “Refresh” → Client requests data → Server responds
  • Form submission → Client sends data → Server processes and responds
  • Periodic polling → Client regularly asks “anything new?”
  • Manual cache invalidation → Client decides when data is stale

KOS Communication: WebSocket-Based Push Model

KOS applications use WebSocket connections for all device communication, but expose patterns that feel familiar to web developers:

Transport Layer: All communication happens over WebSocket connections
Initial Data Loading: Models fetch baseline state using service functions
Real-time Updates: Automatic push notifications for state changes via topic handlers
Developer Experience: Patterns that feel like traditional web development

Baseline + Delta Pattern:

  1. Baseline: Models fetch initial state using service functions in load()
  2. Delta: Models receive incremental updates via @kosTopicHandler decorators
  3. Synchronization: Models merge baseline data with delta to maintain current state

What KOS Models Actually Represent

Models as Architectural Bridge

KOS models form a distinct architectural layer between hardware systems and UI components – similar to a backend-for-frontend pattern but running in the browser:

Hardware/Device Layer: Physical pumps, sensors, motors, dispensers
Backend System: Creates abstract models of hardware concepts
KOS Model Layer: Exposes backend abstractions in a performant, accessible way
UI Components: Consume model state for display and interaction

Models exist because hardware systems can’t necessarily maintain easy-to-access state in a performant manner for UI development. They bridge this architectural gap.

Example: An IngredientModel doesn’t replicate pump mechanics or fluid dynamics. It exposes:

  • Ingredient availability status
  • Expiration dates
  • Assignments to physical pumps
  • Properties needed for beverage creation

This allows UI developers to work with ingredient concepts without understanding the underlying hardware complexity.

Synchronization vs. Replication

KOS models synchronize with backend state, they don’t replicate device functionality:

Physical Device: Pump calibrates, sensors read temperature, motors dispense
Backend System: Creates abstract models of pumps, ingredients, beverages
KOS Model Layer: Exposes these abstractions to enable UI development
UI Components: Display and interact with the exposed state

Event Streams as State Synchronization

Physical Device Changes Drive Everything

In the physical world, devices don’t wait to be asked about their state – they actively signal when things change:

  • Temperature sensor detects change → Backend updates abstract model → Event published
  • User presses physical button → Backend processes → State change event published
  • Ingredient level drops → Backend updates model → Notification sent
  • Motor completes operation → Backend records completion → Event signals completion

Events Mirror Backend State Changes

KOS topic handlers keep the model layer synchronized with backend state:

Physical Event: Temperature sensor reading changes
Backend Processing: Temperature model updated
Digital Event: /sensors/temperature/reading topic message
Model Response: Update temperature property, trigger dependent logic

Physical Event: User presses dispense button on device
Backend Processing: Dispense operation initiated
Digital Event: /dispenser/button/press topic message
Model Response: Update state, notify UI

Topic-Based Event Architecture

Topics as Event Channels

Topics in KOS are event channels that carry specific types of device information:

  • /sensors/temperature/readings – Temperature readings (sensor ID in payload)
  • /dispenser/nozzle/status – Nozzle status updates (nozzle ID in payload)
  • /system/maintenance/alert – System-wide maintenance alerts
  • /door/state/changed – Door state events (door ID in payload)

Models Subscribe to Relevant Events

Models use @kosTopicHandler decorators to subscribe to events they care about:

Sensor Model subscribes to sensor readings:

  • Receives temperature data
  • Updates internal state
  • Triggers dependent model updates

Dispenser Model subscribes to dispense events:

  • Receives pour completion notifications
  • Updates ingredient levels
  • Notifies dependent models

Maintenance Model subscribes to system alerts:

  • Receives error notifications
  • Tracks maintenance schedules
  • Escalates critical issues

Baseline + Delta Communication Pattern

Initial Data Loading: Service Functions

Models fetch baseline data using service functions during the load() lifecycle method:

import { kosModel, IKosDataModel, KosCreationContext, kosTopicHandler } from '@kosdev-code/kos-ui-sdk';
import { fetchCurrentReadings, type SensorReadingsResponse } from './services/sensor-service';
const TOPIC_SENSOR_UPDATES = '/sensors/reading-updates';
@kosModel({ modelTypeId: "sensor-monitoring", singleton: false })
export class SensorMonitoringModel implements IKosDataModel {
  id: string;
  // Model properties are automatically reactive
  currentReadings: SensorReadingsResponse['readings'] = [];
  loading = false;
  error: Error | null = null;
  constructor(modelId: string, options: SensorMonitoringOptions, context: KosCreationContext) {
    this.id = modelId;
  }
  // Load baseline data during model initialization
  async load(): Promise<void> {
    this.loading = true;
    // Call service function (returns error-first tuple)
    const [error, data] = await fetchCurrentReadings();
    if (error) {
      this.error = error;
      this.loading = false;
      return;
    }
    // Store baseline readings
    this.currentReadings = data.readings;
    this.loading = false;
  }
  // Receive delta updates via topic handler
  @kosTopicHandler({ topic: TOPIC_SENSOR_UPDATES, websocket: true })
  handleReadingUpdate(update: SensorReading): void {
    // Find and update the specific reading
    const index = this.currentReadings.findIndex(r => r.sensorId === update.sensorId);
    if (index >= 0) {
      this.currentReadings[index] = update;
    }
  }
}

The service function (fetchCurrentReadings) follows the KOS Service Request Pattern:

// services/sensor-service.ts
import { type ServiceResponse, KosLog } from '@kosdev-code/kos-ui-sdk';
import API, { type ApiPath, type ApiResponse } from '../../../utils/service';
const log = KosLog.createLogger({
  name: 'sensor-service',
  group: 'Services'
});
// PATH constants for endpoints
export const PATH_SENSOR_READINGS: ApiPath = '/api/sensors/current-readings';
// Type-safe response type
export type SensorReadingsResponse = ApiResponse<typeof PATH_SENSOR_READINGS, 'get'>;
// Service function returns error-first tuple
export async function fetchCurrentReadings(): Promise<ServiceResponse<SensorReadingsResponse>> {
  log.debug('Fetching current sensor readings');
  return await API.get(PATH_SENSOR_READINGS);
}

Continuous Updates: WebSocket Topics

After baseline is established, models receive incremental updates:

Traditional Pattern Problems:

Every 5 seconds:
  1. Request all sensor data
  2. Server queries all sensors
  3. Return complete dataset
  4. UI updates entire display
Issues:
- Massive data transfer for small changes
- 5-second delays for critical updates
- Unnecessary server load
- Bandwidth waste

KOS Baseline + Delta Pattern:

Initial Load (load() lifecycle method):
  1. Fetch baseline: current state of all sensors via service function
  2. Subscribe to delta topics: /sensors/updates (via @kosTopicHandler)
Ongoing Updates:
  When sensor X changes:
    1. Only sensor X data sent via topic
    2. Model merges delta with baseline
    3. UI updates only affected sensor display
Benefits:
- Minimal data transfer (only changes)
- Instant updates when changes occur
- Efficient use of network resources
- Always current, complete data

Event Processing Patterns

Simple Event Handling

Direct State Updates:
Device reports current temperature → Model updates temperature property → UI shows new value

Status Changes:
Device completes operation → Model updates status → UI removes loading indicator

Complex Event Processing

Event Aggregation:
Multiple sensor readings arrive rapidly → Model batches them → UI updates with average/summary

Event Correlation:
Door opens + motion detected + temperature change → Model infers “user interaction” → UI adjusts behavior

Event Filtering:
Noisy sensor data → Model filters outliers → UI shows stable readings

Communication Patterns in Practice

Device Commands Using Service Functions

Device control uses service functions over WebSocket transport:

import { kosModel, kosTopicHandler, IKosDataModel, KosCreationContext } from '@kosdev-code/kos-ui-sdk';
import { startPour, type PourResult } from './services/dispenser-service';
const TOPIC_POUR_PROGRESS = '/dispenser/pour-progress';
@kosModel({ modelTypeId: "dispenser-controller", singleton: false })
export class DispenserControllerModel implements IKosDataModel {
  id: string;
  currentProgress: PourProgress | null = null;
  constructor(modelId: string, options: DispenserOptions, context: KosCreationContext) {
    this.id = modelId;
  }
  // Command device using service function
  async requestPour(recipe: Recipe): Promise<void> {
    // Service function returns error-first tuple
    const [error, result] = await startPour(recipe);
    if (error) {
      console.error('Pour failed to start:', error);
      return;
    }
    console.log('Pour started:', result.operationId);
  }
  // Receive real-time progress updates via topics
  @kosTopicHandler({ topic: TOPIC_POUR_PROGRESS, websocket: true })
  handlePourProgress(progress: PourProgress): void {
    // Real-time updates during operation
    this.currentProgress = progress;
  }
}

The service function:

// services/dispenser-service.ts
import { type ServiceResponse } from '@kosdev-code/kos-ui-sdk';
import API, { type ApiPath, type ApiResponse } from '../../../utils/service';
export const PATH_START_POUR: ApiPath = '/api/dispenser/start-pour';
export type PourResult = ApiResponse<typeof PATH_START_POUR, 'post'>;
export async function startPour(recipe: Recipe): Promise<ServiceResponse<PourResult>> {
  return await API.post(PATH_START_POUR, {}, recipe);
}

Complete Communication Flow

User Intent → Service Function → Device Action → Topic Updates → UI Reflection

  1. User Intent: User wants coffee
  2. Service Function: startPour(recipe) called – returns error-first tuple
  3. Device Action: Backend instructs dispenser hardware to start pouring
  4. Topic Updates: Dispenser publishes progress via /dispenser/pour-progress topic
  5. UI Reflection: UI shows real-time progress, completion status

Monitoring Flow with Baseline + Delta

Baseline Load → Topic Subscription → Delta Updates → UI Reflection

  1. Baseline Load: Model calls service function in load() to fetch current readings
  2. Topic Subscription: Model has @kosTopicHandler for /sensors/temperature/updates
  3. Delta Updates: Only changed temperature values sent via topics
  4. UI Reflection: Temperature display updates with new values, maintains complete state

Benefits of Baseline + Delta Communication

1. Familiar Developer Experience

  • Service functions feel like traditional API calls
  • Standard error-first tuple pattern for error handling
  • WebSocket transport is abstracted away from developer

2. Optimal Network Efficiency

  • Baseline load: Complete initial state in one request
  • Delta updates: Only changed data sent via topics
  • No polling waste, no massive data transfers

3. Real-Time Accuracy

  • Models always reflect actual device state
  • Instant updates when devices change
  • Complete data integrity (baseline + deltas = current state)

4. Reliable State Management

  • Models maintain complete, consistent state
  • Automatic merge of baseline and delta data
  • Handles network interruptions gracefully

5. Scalable Architecture

  • Single WebSocket connection handles all communication
  • Topic-based routing scales to many devices
  • Efficient broadcast to multiple clients

Design Questions for Baseline + Delta Systems

For Initial Data Loading:

  • What baseline state does the model need at startup?
  • Which service functions provide baseline data?
  • How should we handle baseline loading errors?
  • What’s the retry strategy for baseline requests?

For Delta Updates:

  • Which device changes should generate topic events?
  • How frequently will delta updates occur?
  • What’s the payload size for typical updates?
  • How do we handle missed delta messages?

For Model State Management:

  • How do we merge delta updates with baseline data?
  • What happens when deltas arrive before baseline?
  • How do we detect and recover from state inconsistencies?
  • When should we refresh the entire baseline?

The Result: Responsive Device Interfaces

The baseline + delta communication pattern creates interfaces that feel connected to the physical world while providing familiar developer experience:

For Users:

  • Immediate feedback – Real-time updates when devices change
  • Accurate status – Always current data, never stale
  • Natural responsiveness – UI reflects actual device behavior

For Developers:

  • Familiar patterns – Service functions for data loading
  • Efficient communication – WebSocket transport without complexity
  • Reliable state – Automatic baseline + delta synchronization
  • Type safety – Full TypeScript support via OpenAPI integration

This approach creates applications that maintain real-time synchronization with physical device state through a performant, accessible model layer, while keeping the developer experience familiar and predictable.

Key Takeaways

Models are an architectural layer – They bridge the gap between hardware architecture and UI needs, providing performant, accessible state similar to a backend-for-frontend pattern.

Communication is WebSocket-based – But patterns feel familiar to web developers through service functions and topic handlers.

Baseline + Delta is the core pattern:

  • load() lifecycle method fetches initial state via service functions
  • @kosTopicHandler decorators subscribe to real-time updates
  • Models merge baseline and delta to maintain current state

Service functions return error-first tuples – Making error handling explicit and predictable.

Use actual KOS patterns – Service functions, @kosTopicHandler, and the load() lifecycle method are the real APIs provided by the framework.