Beyond Individual Models: The Collection Challenge
Most KOS models represent individual entities—a single sensor, a specific temperature controller, or one dispenser unit. But real-world systems often involve multiple instances of the same type of entity. Consider a beverage facility with dozens of ingredient containers, multiple pump systems, or a network of sensors distributed throughout a building. Managing these collections manually creates complexity that scales poorly and leads to brittle, hard-to-maintain code.
Container models solve this challenge by providing sophisticated collection management built specifically for KOS’s reactive architecture. They handle the lifecycle of multiple model instances, provide efficient indexing and querying capabilities, and maintain automatic synchronization with changing backend data—all while preserving the reactive patterns that make KOS applications feel responsive and reliable.
The Container Model Architecture
Container models are built around the KosModelContainer class, which serves as an intelligent collection manager for groups of related models. Unlike simple arrays or maps, KOS containers provide automatic indexing, reactive updates, and efficient query operations designed specifically for device control scenarios.
Modern Container Pattern with @kosContainerAware
The @kosContainerAware decorator eliminates the boilerplate code traditionally required for container models, automatically injecting all necessary container management capabilities:
import { kosModel, kosContainerAware, KosContainerAware, IKosDataModel } from "@kosdev-code/kos-ui-sdk";
// TypeScript interface merging for full type safety
interface SensorContainerModelImpl extends KosContainerAware<SensorModel> {}
@kosModel({ modelTypeId: "sensor-container-model", singleton: false })
@kosContainerAware<SensorModel>()
export class SensorContainerModelImpl implements IKosDataModel {
id: string;
constructor(modelId: string, options: SensorContainerOptions, context: KosCreationContext) {
this.id = modelId;
// Container is automatically injected with parentId: modelId
// All methods (getModel, addModel, removeModel) are automatically available
// All reactive getters (models, data) are automatically available
}
// Only business logic methods needed - container management is automated
addSensor(sensor: SensorModel): void {
this.addModel(sensor); // Method injected by decorator
}
removeSensor(sensorId: string): void {
this.removeModel(sensorId); // Method injected by decorator
}
getSensor(sensorId: string): SensorModel | undefined {
return this.getModel(sensorId); // Method injected by decorator
}
clearAllSensors(): void {
this.container.clear(); // Container property injected by decorator
}
get sensors(): SensorModel[] {
return this.data; // Reactive array getter injected by decorator
}
get sensorCount(): number {
return this.data.length; // Reactive array getter injected by decorator
}
}Decorator Benefits:
- Eliminates 15+ lines of boilerplate per container model
- Automatic parentId inference from constructor
modelIdparameter - Full type safety through interface merging
- Consistent API across all container models
- Zero maintenance overhead for container functionality
Custom Container Property Names
When domain terminology requires a specific property name instead of the default container, use the KosContainerAwareWithProp interface:
import { kosModel, kosContainerAware, KosContainerAwareWithProp, IKosDataModel } from "@kosdev-code/kos-ui-sdk";
// Use KosContainerAwareWithProp with custom property name "devices"
interface DeviceManagerModelImpl extends KosContainerAwareWithProp<DeviceModel, "devices"> {}
@kosModel({ modelTypeId: "device-manager-model", singleton: false })
@kosContainerAware<DeviceModel>({ containerProperty: "devices" })
export class DeviceManagerModelImpl implements IKosDataModel {
id: string;
constructor(modelId: string, options: DeviceManagerOptions, context: KosCreationContext) {
this.id = modelId;
// Container is now available as this.devices instead of this.container
}
// Access container via custom property name
addNewDevice(deviceData: DeviceData): void {
const device = Device.instance(deviceData.id).options(deviceData).build();
this.devices.addModel(device); // Using custom property name
}
getDevicesByLocation(location: string): DeviceModel[] {
return this.devices.getIndexByKey("byLocation", location);
}
get deviceCount(): number {
return this.data.length; // Standard getters still available
}
}Benefits of custom property names:
- Improved domain clarity – Use terminology that matches your business domain
- Seamless migration – Maintain existing API contracts when refactoring
- Better readability – Code reads more naturally with domain-specific naming
Legacy Container Pattern (Manual Setup)
For reference or when you need complete control over container instantiation, the traditional manual approach remains available:
import { KosModelContainer, kosModel, kosTopicHandler, IKosDataModel } from "@kosdev-code/kos-ui-sdk";
@kosModel({ modelTypeId: "sensor-container-model", singleton: false })
export class SensorContainerModelImpl implements IKosDataModel {
private _sensors = new KosModelContainer<SensorModel>();
get sensors(): SensorModel[] {
return this._sensors.data;
}
addSensor(sensor: SensorModel): void {
this._sensors.addModel(sensor);
}
removeSensor(sensorId: string): void {
this._sensors.removeModel(sensorId);
}
getSensor(sensorId: string): SensorModel | undefined {
return this._sensors.getModel(sensorId);
}
clearAllSensors(): void {
this._sensors.clear();
}
get sensorCount(): number {
return this._sensors.data.length;
}
}Both patterns provide centralized management for sensor collections with automatic lifecycle handling. When sensors are added or removed, the container manages their cleanup, and any UI components connected to the container automatically update to reflect the current state.
CLI-Generated Container Models
The KOS CLI provides the --container flag specifically for scenarios where you need to manage multiple instances of the same model type. All newly generated container models automatically use the @kosContainerAware decorator pattern for minimal boilerplate.
# Creates both sensor-model.ts and sensor-container-model.ts with decorator pattern
kosui model --name sensor --project app-models --container
# For ingredient management with both individual and collection models
kosui model --name ingredient --project dispense-models --container
# For device monitoring across multiple units
kosui model --name device --project monitoring-models --containerGenerated container models include:
- @kosContainerAware decorator with automatic parentId inference
- TypeScript interface merging for full type safety
- Minimal boilerplate – just constructor and business logic methods
- All container functionality injected automatically
The CLI generates container models when you’re managing multiple instances of the same type—sensor arrays, device collections, ingredient inventories, or user lists. It avoids containers for single coordinators like “LED Manager” or “System Controller” that orchestrate functionality rather than manage collections.
Intelligent Indexing: Beyond Manual Maps and Sets
One of the most powerful features of KOS containers is their automatic indexing system. Instead of manually maintaining separate Map and Set data structures to organize your collections, containers provide declarative indexing that updates automatically as collection contents change.
interface DeviceContainerModelImpl extends KosContainerAware<DeviceModel> {}
@kosModel({ modelTypeId: "device-container-model", singleton: false })
@kosContainerAware<DeviceModel>({
containerOptions: {
indexMap: {
byType: "deviceType",
byLocation: "location",
byStatus: "connectionStatus",
needsMaintenance: (device) => device.lastMaintenance < device.maintenanceDue,
critical: (device) => device.errorLevel >= 8
},
sortKey: "name"
// parentId automatically inferred from modelId
}
})
export class DeviceContainerModelImpl implements IKosDataModel {
id: string;
constructor(modelId: string, options: DeviceContainerOptions, context: KosCreationContext) {
this.id = modelId;
// Container with indexing automatically injected
}
// Efficient queries using automatic indexes via injected container
getDevicesByType(type: string): DeviceModel[] {
return this.container.getIndexByKey("byType", type);
}
getOfflineDevices(): DeviceModel[] {
return this.container.getIndexByKey("byStatus", "offline");
}
getDevicesNeedingMaintenance(): DeviceModel[] {
return this.container.getIndexByKey("needsMaintenance", true);
}
getCriticalDevices(): DeviceModel[] {
return this.container.getIndexByKey("critical", true);
}
getAvailableTypes(): string[] {
return this.container.getIndexKeys("byType");
}
get devices(): DeviceModel[] {
return this.data; // Uses injected data array getter
}
get deviceCount(): number {
return this.data.length; // Uses injected data array getter
}
}This indexing system eliminates the need for manual Map and Set management while providing superior performance and automatic reactivity. When device properties change, the indexes update automatically, and any UI components using the indexed queries immediately reflect the new organization.
Container Lifecycle and Data Loading
Container models follow a specific lifecycle pattern where they’re responsible for loading collection data and creating individual model instances. This typically happens during the container’s load() method, where it fetches data from services and populates itself with properly configured models.
@kosModel({ modelTypeId: "ingredient-container-model", singleton: false })
export class IngredientContainerModelImpl implements IKosDataModel {
private _ingredients = new KosModelContainer<IngredientModel>({
indexMap: {
byCategory: "category",
bySupplier: "supplier",
lowStock: (ingredient) => ingredient.quantity <= ingredient.reorderLevel,
expired: (ingredient) => ingredient.expirationDate <= new Date()
}
});
get ingredients(): IngredientModel[] {
return this._ingredients.data;
}
async load(): Promise<void> {
await this.loadIngredientCollection();
}
private async loadIngredientCollection(): Promise<void> {
this.logger.debug("Loading ingredient collection");
const backendData = await this.fetchIngredientsFromService();
const ingredients = this.createIngredientModels(backendData);
this.populateContainer(ingredients);
this.logger.debug(`Loaded ${ingredients.length} ingredients`);
}
private async fetchIngredientsFromService(): Promise<IngredientData[]> {
return await ingredientService.getIngredients();
}
private createIngredientModels(ingredientData: IngredientData[]): IngredientModel[] {
return ingredientData.map(data =>
Ingredient.instance(data.id).options(data).build()
);
}
private populateContainer(ingredients: IngredientModel[]): void {
// Use addAll for efficient bulk population
this._ingredients.addAll(ingredients);
}
// Business logic methods using indexes
getLowStockIngredients(): IngredientModel[] {
return this._ingredients.getIndexByKey("lowStock", true);
}
getExpiredIngredients(): IngredientModel[] {
return this._ingredients.getIndexByKey("expired", true);
}
getIngredientsByCategory(category: string): IngredientModel[] {
return this._ingredients.getIndexByKey("byCategory", category);
}
addIngredient(ingredientData: IngredientData): void {
const ingredient = Ingredient.instance(ingredientData.id)
.options(ingredientData)
.build();
this._ingredients.addModel(ingredient);
}
removeIngredient(ingredientId: string): void {
this._ingredients.removeModel(ingredientId);
}
}The container handles the complexity of fetching backend data, transforming it into model instances, and managing the collection lifecycle. Individual ingredient models focus on their specific concerns while the container coordinates the overall collection behavior.
Real-Time Collection Updates
Container models excel at managing dynamic collections that change based on real-time events. They use topic handlers to respond to add, remove, and update events, automatically maintaining collection synchronization with backend systems.
@kosModel({ modelTypeId: "pump-container-model", singleton: false })
export class PumpContainerModelImpl implements IKosDataModel {
private _pumps = new KosModelContainer<PumpModel>({
indexMap: {
byType: "pumpType",
byFlowRate: (pump) => pump.flowRate > 100 ? "high" : "standard",
active: (pump) => pump.status === "running",
requiresCalibration: (pump) => pump.lastCalibration < pump.calibrationDue
}
});
get pumps(): PumpModel[] {
return this._pumps.data;
}
async load(): Promise<void> {
const pumpData = await pumpService.getAllPumps();
const pumps = pumpData.map(data =>
Pump.instance(data.id).options(data).build()
);
this._pumps.addAll(pumps);
}
@kosTopicHandler({
topic: "/hardware/pumps/added",
websocket: true
})
handlePumpAdded(payload: PumpData): void {
this.logger.debug(`Adding pump: ${payload.id}`);
const pump = Pump.instance(payload.id).options(payload).build();
this._pumps.addModel(pump);
}
@kosTopicHandler({
topic: "/hardware/pumps/removed",
websocket: true
})
handlePumpRemoved(payload: { pumpId: string }): void {
this.logger.debug(`Removing pump: ${payload.pumpId}`);
this._pumps.removeModel(payload.pumpId);
}
@kosTopicHandler({
topic: "/hardware/pumps/batch-updated",
websocket: true
})
handlePumpBatchUpdate(payload: PumpUpdateData[]): void {
// Transform updates into updated model instances
const updatedPumps = payload
.map(updateData => {
const existingPump = this._pumps.getModel(updateData.pumpId);
if (existingPump) {
// Update the existing pump model with new data
this.updatePumpFromData(existingPump, updateData);
return existingPump;
}
return null;
})
.filter(pump => pump !== null) as PumpModel[];
// addAll detects existing models and updates them efficiently
this._pumps.addAll(updatedPumps);
}
private updatePumpFromData(pump: PumpModel, data: PumpUpdateData): void {
if (data.status !== undefined) {
pump.status = data.status;
}
if (data.flowRate !== undefined) {
pump.flowRate = data.flowRate;
}
if (data.lastMaintenance !== undefined) {
pump.lastMaintenance = new Date(data.lastMaintenance);
}
}
// Business logic using indexes
getActivePumps(): PumpModel[] {
return this._pumps.getIndexByKey("active", true);
}
getPumpsRequiringCalibration(): PumpModel[] {
return this._pumps.getIndexByKey("requiresCalibration", true);
}
getHighFlowPumps(): PumpModel[] {
return this._pumps.getIndexByKey("byFlowRate", "high");
}
}This pattern creates containers that stay synchronized with system state in real-time. When pumps are added, removed, or updated on the backend, the container immediately reflects those changes. The addAll method intelligently detects existing models and updates them rather than creating duplicates, making batch updates efficient and reliable.
Service Integration and Data Transformation
Container models often integrate with backend services through generated service layers, but backend API response types rarely align perfectly with model option types. This requires a transformation layer that converts backend data formats into the structured data that models expect.
// Backend API types (from generated services)
interface BackendIngredientType {
id: string;
name: string;
type: string;
created_timestamp: number;
expiry_date?: number;
nutrition_data: any;
category: string;
status: string;
}
// Model option types (what models expect)
interface IngredientOptions {
id: string;
name: string;
type: string;
createdDate: Date;
expirationDate?: Date;
nutritionalInfo: NutritionInfo;
category: IngredientCategory;
isActive: boolean;
}
@kosModel({ modelTypeId: "ingredient-container-model", singleton: false })
export class IngredientContainerModelImpl implements IKosDataModel {
private _ingredients = new KosModelContainer<IngredientModel>();
async load(): Promise<void> {
await this.loadIngredientCollection();
}
private async loadIngredientCollection(): Promise<void> {
const [error, backendData] = await this.fetchIngredientsFromService();
if (error) {
this.handleServiceError(error);
return;
}
const ingredients = transformBackendDataToIngredients(backendData);
this._ingredients.addAll(ingredients);
this.logger.info(`Loaded ${ingredients.length} ingredients`);
}
private async fetchIngredientsFromService(): Promise<[Error | null, BackendIngredientType[] | null]> {
const [primaryError, primaryData] = await ingredientService.getIngredients();
if (!primaryError) {
return [null, primaryData];
}
this.logger.warn("Primary service failed, trying fallback", primaryError);
const [fallbackError, fallbackData] = await ingredientService.getCachedIngredients();
return [fallbackError, fallbackData];
}
private handleServiceError(error: Error): void {
this.logger.error("Failed to load ingredients", error);
// Container should still function with empty collection
}
}
// Independent transformation functions (separate module)
export function transformBackendDataToIngredients(
backendData: BackendIngredientType[]
): IngredientModel[] {
return backendData.map(data => {
const options = transformToModelOptions(data);
return Ingredient.instance(data.id).options(options).build();
});
}
export function transformToModelOptions(
backendData: BackendIngredientType
): IngredientOptions {
return {
id: backendData.id,
name: backendData.name?.trim() || 'Unknown',
type: backendData.type?.toLowerCase() || 'unknown',
createdDate: new Date(backendData.created_timestamp * 1000),
expirationDate: backendData.expiry_date
? new Date(backendData.expiry_date * 1000)
: undefined,
nutritionalInfo: normalizeNutritionData(backendData.nutrition_data),
category: mapCategoryToEnum(backendData.category),
isActive: backendData.status === 'active',
};
}
function normalizeNutritionData(rawData: any): NutritionInfo {
return {
calories: Number(rawData?.calories || 0),
sugar: Number(rawData?.sugar_grams || 0),
sodium: Number(rawData?.sodium_mg || 0),
allergens: Array.isArray(rawData?.allergens) ? rawData.allergens : []
};
}
function mapCategoryToEnum(category: string): IngredientCategory {
switch (category?.toUpperCase()) {
case 'SYRUP': return IngredientCategory.SYRUP;
case 'FLAVOR': return IngredientCategory.FLAVOR;
case 'BASE': return IngredientCategory.BASE;
default: return IngredientCategory.UNKNOWN;
}
}This transformation approach keeps backend API types separate from model types while providing reusable functions that work consistently across both initial loading and real-time updates.
Advanced Container Patterns
Container models support sophisticated patterns for complex operational scenarios. Consider a multi-zone cooling system that needs to coordinate temperature control across multiple zones while maintaining individual zone autonomy.
@kosModel({ modelTypeId: "cooling-zone-container-model", singleton: false })
export class CoolingZoneContainerModelImpl implements IKosDataModel {
private _zones = new KosModelContainer<CoolingZoneModel>({
indexMap: {
byBuilding: "buildingId",
byFloor: "floor",
overTemp: (zone) => zone.currentTemp > zone.targetTemp + 2,
underTemp: (zone) => zone.currentTemp < zone.targetTemp - 2,
optimal: (zone) => Math.abs(zone.currentTemp - zone.targetTemp) <= 1,
requiresMaintenance: (zone) => zone.lastMaintenance < zone.maintenanceDue
},
sortKey: "name"
});
get zones(): CoolingZoneModel[] {
return this._zones.data;
}
async load(): Promise<void> {
const zoneData = await coolingService.getAllZones();
const zones = zoneData.map(data =>
CoolingZone.instance(data.id).options(data).build()
);
this._zones.addAll(zones);
}
@kosTopicHandler({
topic: "/cooling/zones/temperature-update",
websocket: true
})
handleTemperatureUpdate(payload: ZoneTemperatureUpdate): void {
const zone = this._zones.getModel(payload.zoneId);
if (zone) {
zone.currentTemp = payload.temperature;
zone.lastReading = new Date(payload.timestamp);
// addModel with existing model updates it automatically
this._zones.addModel(zone);
}
}
// Complex operational methods using multiple indexes
getZonesNeedingCooling(): CoolingZoneModel[] {
return this._zones.getIndexByKey("overTemp", true);
}
getZonesNeedingHeating(): CoolingZoneModel[] {
return this._zones.getIndexByKey("underTemp", true);
}
getOptimalZones(): CoolingZoneModel[] {
return this._zones.getIndexByKey("optimal", true);
}
getZonesByFloor(floor: number): CoolingZoneModel[] {
return this._zones.getIndexByKey("byFloor", floor.toString());
}
// Aggregate operations across the collection
getSystemStatus(): SystemStatus {
const zones = this._zones.data;
const overTemp = this.getZonesNeedingCooling().length;
const underTemp = this.getZonesNeedingHeating().length;
const optimal = this.getOptimalZones().length;
return {
totalZones: zones.length,
optimalZones: optimal,
overTempZones: overTemp,
underTempZones: underTemp,
systemEfficiency: zones.length > 0 ? optimal / zones.length : 0,
maintenanceRequired: this.getMaintenanceRequiredZones().length
};
}
private getMaintenanceRequiredZones(): CoolingZoneModel[] {
return this._zones.getIndexByKey("requiresMaintenance", true);
}
// Batch operations for system-wide changes
async adjustAllTargetTemperatures(adjustment: number): Promise<void> {
const zones = this._zones.data;
const updates = zones.map(zone => ({
zoneId: zone.id,
targetTemp: zone.targetTemp + adjustment
}));
await coolingService.batchUpdateTargetTemperatures(updates);
}
async triggerEmergencyCooling(): Promise<void> {
const overTempZones = this.getZonesNeedingCooling();
if (overTempZones.length > 0) {
await Promise.all(
overTempZones.map(zone =>
coolingService.activateEmergencyCooling(zone.id)
)
);
}
}
}This advanced pattern demonstrates how containers can coordinate complex multi-zone operations while maintaining individual zone control and providing system-wide intelligence about operational status.
Performance and Scalability
Container models are designed to handle substantial collections efficiently. The automatic indexing system provides O(1) lookups for indexed properties, while the addAll method minimizes reactive update cycles by intelligently handling both new additions and existing model updates.
@kosModel({ modelTypeId: "large-sensor-network-container-model", singleton: false })
export class LargeSensorNetworkContainerModelImpl implements IKosDataModel {
private _sensors = new KosModelContainer<SensorModel>({
indexMap: {
byType: "sensorType",
byZone: "zone",
byStatus: "status",
batteryLow: (sensor) => sensor.batteryLevel < 20,
offline: (sensor) => sensor.status === "offline"
}
});
// Efficient bulk operations for large collections
@kosTopicHandler({
topic: "/sensors/batch-readings",
websocket: true,
debounce: 1000 // Collect readings for 1 second
})
handleSensorReadingsBatch(readings: SensorReading[]): void {
// Process batch efficiently by updating existing models
const updatedSensors: SensorModel[] = [];
readings.forEach(reading => {
const sensor = this._sensors.getModel(reading.sensorId);
if (sensor) {
sensor.currentReading = reading.value;
sensor.lastUpdate = new Date(reading.timestamp);
updatedSensors.push(sensor);
}
});
// addAll detects existing models and updates them in one reactive cycle
this._sensors.addAll(updatedSensors);
this.logger.debug(`Processed ${readings.length} sensor readings in batch`);
}
// Efficient aggregate computations using indexes
getNetworkHealthSummary(): NetworkHealth {
return {
totalSensors: this._sensors.data.length,
onlineSensors: this._sensors.data.length - this.getOfflineSensors().length,
lowBatterySensors: this.getLowBatterySensors().length,
sensorsByZone: this.getSensorDistribution(),
lastUpdateTime: this.getLastNetworkUpdate()
};
}
private getOfflineSensors(): SensorModel[] {
return this._sensors.getIndexByKey("offline", true);
}
private getLowBatterySensors(): SensorModel[] {
return this._sensors.getIndexByKey("batteryLow", true);
}
private getSensorDistribution(): Record<string, number> {
const zones = this._sensors.getIndexKeys("byZone");
const distribution: Record<string, number> = {};
zones.forEach(zone => {
distribution[zone] = this._sensors.getIndexByKey("byZone", zone).length;
});
return distribution;
}
private getLastNetworkUpdate(): Date | null {
const sensors = this._sensors.data;
if (sensors.length === 0) return null;
return sensors.reduce((latest, sensor) => {
return sensor.lastUpdate > latest ? sensor.lastUpdate : latest;
}, sensors[0].lastUpdate);
}
}This approach handles thousands of sensors efficiently by using batch operations, intelligent debouncing, and the addAll method’s built-in capability to detect and update existing models without creating duplicates.
Container Capacity Management: The Safety Net Pattern
Container models provide automatic capacity management as a safety net for event-driven scenarios where collections might grow unbounded. This feature is designed to gracefully handle edge cases where explicit cleanup handlers fail or data inconsistencies occur, providing a last line of defense against memory exhaustion.
Philosophy: Graceful Degradation Over System Crash
The capacity management system operates on a critical principle: inconsistent data is better than a full system crash. When explicit cleanup handlers are working correctly, capacity limits are never reached. When they’re not, the system logs comprehensive debugging information while automatically preventing runaway memory consumption.
Configuring Capacity Limits
Capacity management is configured through the @kosContainerAware decorator or directly on container instances:
interface TroubleContainerModelImpl extends KosContainerAware<TroubleModel> {}
@kosModel({ modelTypeId: "trouble-container-model", singleton: true })
@kosContainerAware<TroubleModel>({
containerOptions: {
// SAFETY NET: Maximum number of trouble models to retain
maxCapacity: 100,
// Eviction strategy: 'fifo', 'lru', or 'custom'
evictionStrategy: 'fifo',
// Number of models to evict when capacity is exceeded
evictionBatchSize: 10,
// Optional: custom eviction logic
customEvictionFilter: (models: TroubleModel[]) => {
// Return models to evict (e.g., resolved troubles first)
return models
.filter(t => t.isResolved)
.sort((a, b) => a.timestamp - b.timestamp);
}
}
})
export class TroubleContainerModelImpl implements IKosDataModel {
id: string;
constructor(modelId: string, options: ContainerOptions, context: KosCreationContext) {
this.id = modelId;
}
@kosTopicHandler({
topic: "/device/troubles",
websocket: true
})
handleTroubleEvent(event: TroubleEvent): void {
if (event.action === "add") {
const trouble = TroubleModel.instance(event.troubleId)
.options(event)
.build();
this.addModel(trouble);
// Primary cleanup: explicit removal on resolve
} else if (event.action === "remove") {
this.removeModel(event.troubleId);
}
}
}Eviction Strategies
Container capacity management provides three eviction strategies, each suited for different use cases:
FIFO (First-In-First-Out)
Best for event streams and logs where older events become less relevant over time:
@kosContainerAware<LogEntryModel>({
containerOptions: {
maxCapacity: 1000,
evictionStrategy: 'fifo', // Remove oldest entries first
evictionBatchSize: 50 // Evict 50 entries when capacity exceeded
}
})
export class LogContainerModelImpl implements IKosDataModel {
// Oldest log entries are automatically evicted when capacity is exceeded
// Ensures recent logs are always available for debugging
}When to use FIFO:
- Event logs and audit trails
- Time-series data where recent events matter most
- Notification queues
- Any scenario where chronological order determines relevance
LRU (Least Recently Used)
Best for caches and frequently accessed collections where access patterns indicate importance:
@kosContainerAware<CachedDataModel>({
containerOptions: {
maxCapacity: 500,
evictionStrategy: 'lru', // Remove least recently accessed
evictionBatchSize: 25
}
})
export class CacheContainerModelImpl implements IKosDataModel {
getCachedItem(id: string): CachedDataModel | undefined {
// getModel() automatically updates LRU access time
return this.getModel(id);
}
// Frequently accessed items stay in cache
// Rarely accessed items are evicted first
}When to use LRU:
- Data caches
- Recently viewed items
- Active session tracking
- Any scenario where access frequency indicates value
Custom Strategy
Best for domain-specific eviction logic where business rules determine what to remove:
@kosContainerAware<TroubleModel>({
containerOptions: {
maxCapacity: 200,
evictionStrategy: 'custom',
customEvictionFilter: (models: TroubleModel[]) => {
// Evict resolved troubles first, then oldest unresolved
const resolved = models
.filter(t => t.isResolved)
.sort((a, b) => a.resolvedAt - b.resolvedAt);
const unresolved = models
.filter(t => !t.isResolved)
.sort((a, b) => a.createdAt - b.createdAt);
return [...resolved, ...unresolved];
}
}
})
export class TroubleContainerModelImpl implements IKosDataModel {
// Custom logic prioritizes keeping active troubles
// while cleaning up resolved troubles first
}When to use Custom:
- Complex business rules for retention
- Priority-based eviction
- Multi-factor eviction decisions
- Domain-specific importance calculations
Eviction Logging and Debugging
All evictions are logged at WARNING level with comprehensive debugging information:
// When capacity is exceeded:
log.warn(
`Container capacity exceeded (105/100). ` +
`Evicting 10 models using fifo strategy. ` +
`This may indicate missing cleanup handlers or data inconsistency.`,
{ parentId: 'trouble-container' }
);
// For each evicted model:
log.warn(
`Evicting model: trouble-123 (type: TroubleModel)`,
{
modelId: 'trouble-123',
parentId: 'trouble-container',
strategy: 'fifo'
}
);
// After eviction completes:
log.info(
`Evicted 10 models. Current size: 95/100`,
{ parentId: 'trouble-container' }
);This logging enables rapid triage when capacity limits are reached:
- Identify the problem: Which container is overflowing?
- Understand the cause: Are cleanup handlers failing?
- Track evicted data: Which specific models were removed?
- Monitor system health: Is the issue recurring?
Real-World Example: Trouble Stream Safety Net
Consider a trouble event stream where the backend occasionally fails to send removal events:
interface TroubleStreamContainerImpl extends KosContainerAware<TroubleModel> {}
@kosModel({ modelTypeId: "trouble-stream-container", singleton: true })
@kosContainerAware<TroubleModel>({
containerOptions: {
indexMap: {
byType: "troubleType",
byDevice: "deviceId",
resolved: (t: TroubleModel) => t.isResolved
},
// SAFETY NET: Prevent unbounded growth from missing remove events
maxCapacity: 500,
evictionStrategy: 'custom',
evictionBatchSize: 50,
customEvictionFilter: (models: TroubleModel[]) => {
// Remove resolved troubles first (likely missing backend cleanup)
const resolved = models.filter(t => t.isResolved);
const unresolved = models.filter(t => !t.isResolved);
// Sort by age within each group
resolved.sort((a, b) => a.createdAt - b.createdAt);
unresolved.sort((a, b) => a.createdAt - b.createdAt);
return [...resolved, ...unresolved];
}
}
})
export class TroubleStreamContainerImpl implements IKosDataModel {
id: string;
constructor(modelId: string, options: ContainerOptions, context: KosCreationContext) {
this.id = modelId;
}
@kosTopicHandler({
topic: "/device/trouble-stream",
websocket: true
})
handleTroubleUpdate(event: TroubleStreamEvent): void {
// Primary path: explicit add/remove handling
if (event.action === "added") {
const trouble = TroubleModel.instance(event.id)
.options({ ...event, deviceId: event.sourceDevice })
.build();
this.addModel(trouble);
}
else if (event.action === "removed") {
// Primary cleanup mechanism
this.removeModel(event.id);
}
else if (event.action === "resolved") {
const trouble = this.getModel(event.id);
if (trouble) {
trouble.isResolved = true;
trouble.resolvedAt = Date.now();
}
}
}
// Capacity safety net activates ONLY when:
// 1. Backend fails to send removal events
// 2. Cleanup handlers fail
// 3. Data inconsistency prevents normal removal
get activeTroubles(): TroubleModel[] {
return this.getIndexByKey("resolved", false);
}
get resolvedTroubles(): TroubleModel[] {
return this.getIndexByKey("resolved", true);
}
}How this pattern works in practice:
- Normal operation: Troubles are added via events, resolved via updates, removed via explicit removal events. Capacity limit is never reached.
- Backend failure: If removal events stop arriving (network issue, backend bug), resolved troubles accumulate.
- Safety net activation: When capacity (500) is exceeded:
- System logs WARNING with context
- Custom filter identifies oldest resolved troubles
- Batch eviction removes 50 oldest resolved troubles
- System remains stable with consistent data
- Debugging enabled: Logs show exactly which troubles were evicted, enabling investigation of why removal events weren’t received.
Capacity Management vs. Primary Cleanup
Critical distinction: Capacity management is NOT a replacement for proper cleanup handlers:
| Aspect | Primary Cleanup | Capacity Management |
|---|---|---|
| Purpose | Correct, intended data lifecycle | Emergency overflow prevention |
| Trigger | Business logic (resolve event, user action) | Capacity threshold exceeded |
| Logging Level | DEBUG/INFO | WARNING (indicates problem) |
| Data Consistency | Guaranteed correct | Best-effort graceful degradation |
| When Active | Always, as part of normal flow | Only when primary cleanup fails |
Think of capacity management as:
- Circuit breaker for memory exhaustion
- Debugging aid for data lifecycle issues
- Graceful degradation mechanism
- Emergency pressure relief valve
NOT as:
- Primary data cleanup mechanism
- Replacement for proper event handling
- Normal operational behavior
Monitoring and Alerting
Capacity eviction warnings should trigger operational alerts:
@kosModel({ modelTypeId: "monitoring-container", singleton: true })
export class MonitoringContainerModelImpl implements IKosDataModel {
private evictionCount = 0;
private lastEvictionTime: number | null = null;
// Override to track evictions (if needed for metrics)
addModel(model: MonitoredModel): void {
super.addModel(model);
// Check if eviction occurred (compare before/after size)
if (this.container.data.length === this.container['_maxCapacity']) {
this.evictionCount++;
this.lastEvictionTime = Date.now();
// Alert: capacity management is activating frequently
if (this.evictionCount > 10) {
this.sendAlert({
severity: 'high',
message: 'Container capacity management activating frequently',
context: {
evictionCount: this.evictionCount,
maxCapacity: this.container['_maxCapacity'],
currentSize: this.container.data.length
}
});
}
}
}
}Building Scalable Collection Management
Container models transform the challenge of managing multiple device instances from manual collection handling into automatic, reactive, and efficient systems. By providing intelligent indexing, real-time synchronization, and sophisticated query capabilities, they create the foundation for applications that can scale from small installations to large distributed systems.
The combination of automatic lifecycle management, declarative indexing, and seamless integration with KOS’s reactive patterns makes container models particularly well-suited for device control scenarios where collections of similar entities need coordinated management while maintaining individual autonomy.
Whether managing a handful of sensors in a single facility or coordinating hundreds of devices across multiple locations, container models provide the organizational intelligence and performance characteristics necessary for building robust, maintainable device management systems.
Ready to enhance container functionality further? Continue with Companion Models to learn about extending model capabilities through the decorator pattern, or explore Advanced Performance Patterns for optimizing large-scale collection operations.
