Consumer UI Foundation Models

This guide explains the consumer-facing model hierarchy provided by the dispense SDK for building beverage selection interfaces. These models represent the dispenser from a consumer’s perspective, focusing on product selection and pouring rather than hardware details.

The consumer UI hierarchy (DeviceAssembly → Nozzle → Availability) is distinct from the hardware hierarchy (Assembly → Holder/Pump) and optimized for consumer-facing applications where users select brands, beverages, and flavors.

Why a Separate Hierarchy?

Consumer UIs need selection-focused models, not hardware-focused models:

  • Hardware hierarchy: Targets crew/maintenance apps needing holder, pump, and board access for ingredient management, calibration, and troubleshooting
  • Consumer hierarchy: Targets consumer apps needing brand, beverage, and flavor selection for intuitive product discovery and pouring

The consumer hierarchy provides:

  • Product-centric navigation (brands → beverages → flavors)
  • Availability and visibility management
  • Selection state tracking
  • Pour operations without hardware complexity
  • Real-time availability updates via WebSocket
  • Companion model support for specialized behavior

Model Overview

The SDK provides three core consumer UI models:

DeviceAssemblyModel

Singleton entry point for consumer applications. Loads nozzles from device topology and provides access to dispense points.

Key Properties

interface DeviceAssemblyModel {
  // Identity
  id: string;
  // Nozzle Access
  nozzlePaths: string[];  // All nozzle paths on device
}

Usage

DeviceAssemblyModel is a singleton—one instance per device:

import { DeviceAssembly } from "@kosdev-code/kos-dispense-sdk";
import { getKosModel } from "@kosdev-code/kos-ui-sdk";
// Get singleton instance
const { model: deviceAssembly } = await getKosModel(DeviceAssembly.type);
// List all nozzles
console.log("Nozzle paths:", deviceAssembly.nozzlePaths);
// Get specific nozzle
const nozzle = deviceAssembly.getNozzle("assembly:core:nozzle:nozzle");
if (nozzle) {
  console.log("Nozzle available:", nozzle.name);
}

Lifecycle

During the load phase, DeviceAssemblyModel:

  1. Calls getAssembly() service to fetch device topology
  2. Extracts nozzle data from assemblies
  3. Creates NozzleModel instances for each nozzle
  4. Stores nozzles in internal container

This is the same topology data used by AssemblyModel, but DeviceAssemblyModel only extracts nozzle information needed for consumer UIs.

AvailabilityModel

Represents a selectable item in the availability graph. Can represent brands, beverages, flavors, add-ons, or any selectable product.

Key Properties

interface AvailabilityModel<T = any, P = any> {
  // Identity
  id: string;  // Unique model identifier
  rawId: string;  // Original ID from backend
  altId: string;  // Alternative ID for correlation
  type: string;  // Type classification
  // Organization
  groupId: string;  // Group membership (brands, beverages, flavors)
  parentId?: string;  // Parent relationship for drill-down
  taggedIds: string[];  // Array of related IDs
  // State
  available: boolean;  // Whether item can be poured
  visible: boolean;  // Whether item should display in UI
  note?: string;  // Optional note
  // Extension Data
  data: T;  // Custom typed data
  properties: P;  // Additional typed properties
}

Usage

Availability models are typically accessed through NozzleModel:

// Access via group
const brands = nozzle.brands;
brands.forEach(brand => {
  console.log("Brand:", brand.data.name);
  console.log("Available:", brand.available);
  console.log("Visible:", brand.visible);
});
// Check state
if (brand.available && brand.visible) {
  // Show in UI and allow selection
}
// Access custom data
interface BrandData {
  name: string;
  imageUrl: string;
  brandColor: string;
}
const brand: AvailabilityModel<BrandData> = ...;
console.log(brand.data.name);
console.log(brand.data.imageUrl);
console.log(brand.data.brandColor);
// Access properties
interface BeverageProperties {
  calories: number;
  sugar: number;
}
const beverage: AvailabilityModel<any, BeverageProperties> = ...;
console.log(beverage.properties.calories);
console.log(beverage.properties.sugar);

Parent-Child Relationships

Availability models support hierarchical navigation via ParentAware:

// Find child items
const beverages = nozzle.getAvailabilityByParent(brand.id);
beverages.forEach(beverage => {
  console.log("Beverage under brand:", beverage.data.name);
});
// Tagged relationships
brand.taggedIds.forEach(taggedId => {
  const beverage = nozzle.beverages.find(b => b.rawId === taggedId);
  if (beverage) {
    console.log("Related beverage:", beverage.data.name);
  }
});

Relationship types:

  • parentId: Direct parent reference (set by taggedIds processing)
  • taggedIds: Array of related item IDs (from backend)

State Management

Availability models support state updates:

// Update availability
availability.updateAvailability(false);  // Mark unavailable
availability.updateAvailability(true);   // Mark available
// Check state changes (reactive via kosAutoEffect)
kosAutoEffect(() => {
  if (!availability.available) {
    console.log("Item became unavailable");
  }
});

State properties:

  • available: Whether item can be poured (determined by pour engine)
  • visible: Whether item should display in UI (determined by pour engine)
  • Both properties update automatically via WebSocket

Extension Data

Availability models support typed custom data:

// Define custom data type
interface BeverageData {
  name: string;
  description: string;
  imageUrl: string;
  nutritionalInfo: {
    calories: number;
    sugar: number;
    caffeine: number;
  };
}
// Access typed data
const beverage: AvailabilityModel<BeverageData> = ...;
console.log(beverage.data.name);
console.log(beverage.data.description);
console.log(beverage.data.nutritionalInfo.calories);
// Properties for additional metadata
interface BeverageProperties {
  popular: boolean;
  seasonal: boolean;
  allergens: string[];
}
const beverage: AvailabilityModel<BeverageData, BeverageProperties> = ...;
console.log(beverage.properties.popular);
console.log(beverage.properties.seasonal);

Extension data populated via ExtensionType.AvailabilityMapper during nozzle load. See Extension Points for details.

Companion Models for Specific Behavior

While ExtensionType.AvailabilityMapper adds custom data, companion models add specific behavior and methods. This allows generic AvailabilityModel to be specialized (Brand, Beverage, Flavor) with type-specific operations.

When to use companions vs extension data:

  • Extension data (AvailabilityMapper): Additional properties (icons, names, colors, metadata)
  • Companion models: Type-specific behavior (child beverage lists, custom methods, app-specific operations)

Example: BrandCompanion for brand-specific behavior

// Companion provides brand-specific behavior
interface BrandCompanion extends KosCompanionDecorator<AvailabilityModel> {}
@kosModel("brand-companion")
@kosCompanion()  // Decorator mode - transparent proxy to parent
class BrandCompanion implements IKosDataModel {
  id: string;
  // Brand-specific method
  getChildBeverages(nozzle: NozzleModel): AvailabilityModel[] {
    return nozzle.getAvailabilityByParent(this.id);  // 'id' from parent via proxy
  }
  // Brand-specific computed property
  get brandColor(): string {
    return this.data.brandColor;  // 'data' from parent via proxy
  }
  // Brand-specific operation
  async loadBrandAssets(): Promise<void> {
    // Load brand-specific resources using this.rawId, this.data, etc
  }
}
// Access companion
const brand: AvailabilityModel = ...;
const brandCompanion = getKosCompanionModel<BrandCompanion>(
  brand,
  BrandCompanion.type
);
const childBeverages = brandCompanion.getChildBeverages(nozzle);
// Companion acts as transparent proxy - all parent properties available
console.log(brandCompanion.name);  // From AvailabilityModel.data.name
console.log(brandCompanion.available);  // From AvailabilityModel

Benefits of companion pattern:

  • Extends SDK models without modifying source code
  • Type-safe access to specialized behavior
  • Clean separation of generic framework vs app-specific logic
  • Automatic lifecycle management (created/destroyed with parent)
  • Transparent proxy mode makes companion drop-in replacement for parent

See Extension Points for complete companion model documentation.

Use Cases

Availability models represent any selectable item:

  • Brands: Top-level product categories
  • Beverages: Individual drink products
  • Flavors: Flavor additions/shots
  • Add-ons: Customizations or enhancements
  • Sizes: Volume options (if modeled as items)

All use the same AvailabilityModel interface with different groupId values.

Complete Example

Basic consumer selection workflow:

import { DeviceAssembly } from "@kosdev-code/kos-dispense-sdk";
import { getKosModel } from "@kosdev-code/kos-ui-sdk";
// 1. Get device and nozzle
const { model: deviceAssembly } = await getKosModel(DeviceAssembly.type);
const nozzle = deviceAssembly.getNozzle("assembly:core:nozzle:nozzle");
if (!nozzle) {
  throw new Error("Nozzle not found");
}
// 2. Display brands
console.log("Available brands:");
nozzle.visibleBrands.forEach(brand => {
  console.log(`- ${brand.data.name}`);
});
// 3. User selects brand
const selectedBrand = nozzle.visibleBrands[0];
nozzle.setSelectedBrand(selectedBrand);
// 4. Display beverages for brand
const beverages = nozzle.getAvailabilityByParent(selectedBrand.id);
console.log(`Beverages for ${selectedBrand.data.name}:`);
beverages.forEach(beverage => {
  console.log(`- ${beverage.data.name} (${beverage.available ? "available" : "unavailable"})`);
});
// 5. User selects beverage
const selectedBeverage = beverages.find(b => b.available);
if (!selectedBeverage) {
  throw new Error("No available beverages");
}
await nozzle.setSelectedPourable({
  beverage: selectedBeverage
});
// 6. Execute pour
if (nozzle.canPour) {
  console.log("Starting pour...");
  await nozzle.pour();
  console.log("Pour complete!");
} else {
  console.log("Cannot pour - beverage unavailable or already pouring");
}

For complete consumer UI patterns including flavor selection and pour tracking, see Consumer UI Kit (CUI Kit).


Next Steps