@happyvertical/smrt-types

Shared TypeScript types and enums for the SMRT framework. Prevents circular dependencies by centralizing types that multiple packages need. Zero runtime code except enums.

v0.20.44Core FoundationESMZero Dependencies

Overview

@happyvertical/smrt-types provides shared TypeScript definitions that are used across the entire SMRT ecosystem. By centralizing these types, we avoid circular dependencies between packages and ensure consistent interfaces throughout the SDK.

This package exports three categories: Signal System types (method tracking and observability), Module UI types (module registration and admin panels), and User/Tenant Status enums (lifecycle status values with runtime code).

Installation

bash
pnpm add @happyvertical/smrt-types

Note: This package is typically installed as a dependency of other SMRT packages. You rarely need to install it directly.

Signal System

The Signal System provides automatic observability into SMRT method execution, enabling logging, metrics, pub/sub updates, and distributed tracing without manual instrumentation.

Signal Interface

typescript
import type { Signal } from '@happyvertical/smrt-types';

// Signal structure
interface Signal {
  // Execution tracking
  id: string;              // Unique execution ID
  objectId: string;        // SMRT object instance ID
  className: string;       // SMRT class name
  method: string;          // Method being executed

  // Lifecycle stage
  type: 'start' | 'step' | 'end' | 'error';

  // Optional progress tracking
  step?: string;           // Custom step label

  // Execution data
  args?: any[];           // Sanitized arguments
  result?: any;           // Return value (on 'end')
  error?: Error;          // Error (on 'error')
  duration?: number;      // Execution time in ms

  // Metadata
  timestamp: Date;
  metadata?: Record<string, any>;
}

SignalType

Four lifecycle stages for method execution:

  • start - Method execution started
  • step - Manual progress step (optional)
  • end - Method completed successfully
  • error - Method failed with error
typescript
type SignalType = 'start' | 'step' | 'end' | 'error';

SignalAdapter Interface

Adapters consume signals for specific purposes (logging, metrics, tracing, etc.):

typescript
import type { SignalAdapter, Signal } from '@happyvertical/smrt-types';

class MyAdapter implements SignalAdapter {
  async handle(signal: Signal): Promise<void> {
    // Process the signal
    // Errors are caught by SignalBus
  }
}

// Common adapter use cases:
// - Logging: Write to console, file, or logging service
// - Metrics: Track execution counts, durations, errors
// - Pub/Sub: Broadcast real-time updates to clients
// - Tracing: Send spans to distributed tracing systems

Module UI Types

Types for module registration and admin panel UI slots. Used by packages that register UI components for admin interfaces.

typescript
import type {
  SmrtModuleMeta,      // Module metadata (name, version, description)
  ModuleUISlot,        // UI slot definition for module admin panels
  ModuleComponentType, // Component type classification
  ModuleUIBaseProps,   // Base props interface for module UI components
  ModuleUIRegistryInterface  // Registry for module UI registration
} from '@happyvertical/smrt-types';

User/Tenant Status Enums

Runtime enum values for user and tenant lifecycle status. These are the only exports with runtime code -- use regular imports (not import type).

typescript
import {
  UserStatus,              // User lifecycle status
  TenantStatus,            // Tenant lifecycle status
  MembershipStatus,        // Membership lifecycle status
  SessionStatus,           // Session lifecycle status
  OverrideEffect,          // Permission override effect
  TenantPermissionEffect   // Tenant-level permission effect
} from '@happyvertical/smrt-types';

Usage in SMRT Objects

SMRT objects automatically emit signals during method execution. You can listen to these signals using the internal signal bus:

typescript
import { SmrtObject } from '@happyvertical/smrt-core';
import type { Signal } from '@happyvertical/smrt-types';

class Product extends SmrtObject {
  async analyze() {
    // Signals automatically emitted:
    // - 'start' when method begins
    // - 'end' when method completes
    // - 'error' if method throws
  }
}

// Listen to signals
const product = new Product();
const signalBus = product._signalBus;

signalBus?.on('method:before', (signal: Signal) => {
  console.log(`Starting: ${signal.method}`);
});

signalBus?.on('method:after', (signal: Signal) => {
  console.log(`Completed: ${signal.method} in ${signal.duration}ms`);
});

signalBus?.on('method:error', (signal: Signal) => {
  console.error(`Failed: ${signal.method}`, signal.error);
});

await product.analyze();

Custom Step Signals

Emit manual progress steps within methods:

typescript
class Document extends SmrtObject {
  async process() {
    this._signalBus?.emit({
      id: 'exec-123',
      objectId: this.id,
      className: 'Document',
      method: 'process',
      type: 'step',
      step: 'downloading',
      timestamp: new Date()
    });

    await this.download();

    this._signalBus?.emit({
      ...
      step: 'parsing',
      ...
    });

    await this.parse();
  }
}

Signal Adapters

Example: Logging Adapter

typescript
import type { SignalAdapter, Signal } from '@happyvertical/smrt-types';

class LoggingAdapter implements SignalAdapter {
  async handle(signal: Signal): Promise<void> {
    const log = {
      time: signal.timestamp,
      class: signal.className,
      method: signal.method,
      type: signal.type
    };

    if (signal.type === 'end') {
      log.duration = signal.duration;
      console.log('✓', log);
    } else if (signal.type === 'error') {
      log.error = signal.error?.message;
      console.error('✗', log);
    } else if (signal.type === 'start') {
      console.log('→', log);
    }
  }
}

Example: Metrics Adapter

typescript
class MetricsAdapter implements SignalAdapter {
  private metrics = new Map<string, number[]>();

  async handle(signal: Signal): Promise<void> {
    if (signal.type === 'end' && signal.duration) {
      const key = `${signal.className}.${signal.method}`;
      const durations = this.metrics.get(key) || [];
      durations.push(signal.duration);
      this.metrics.set(key, durations);
    }
  }

  getAverageDuration(className: string, method: string): number {
    const key = `${className}.${method}`;
    const durations = this.metrics.get(key) || [];
    return durations.reduce((a, b) => a + b, 0) / durations.length;
  }
}

Example: Pub/Sub Adapter

typescript
class WebSocketAdapter implements SignalAdapter {
  constructor(private ws: WebSocket) {}

  async handle(signal: Signal): Promise<void> {
    // Broadcast progress to connected clients
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({
        event: 'method:progress',
        data: {
          class: signal.className,
          method: signal.method,
          type: signal.type,
          progress: signal.step,
          timestamp: signal.timestamp
        }
      }));
    }
  }
}

Why Centralize Types?

  1. Prevent Circular Dependencies - Shared types can be imported without creating dependency cycles
  2. Single Source of Truth - Type definitions maintained in one place
  3. Version Synchronization - All packages use the same type version
  4. Type Safety - TypeScript ensures consistency across the SDK
  5. Tree Shaking - Zero runtime overhead (types only)

API Reference

Exports

typescript
// Signal System (types — no runtime code)
import type { Signal, SignalType, SignalAdapter } from '@happyvertical/smrt-types';

// Module UI (types — no runtime code)
import type {
  SmrtModuleMeta, ModuleUISlot, ModuleComponentType,
  ModuleUIBaseProps, ModuleUIRegistryInterface
} from '@happyvertical/smrt-types';

// User/Tenant Status (enums — have runtime values, use regular import)
import {
  UserStatus, TenantStatus, MembershipStatus,
  SessionStatus, OverrideEffect, TenantPermissionEffect
} from '@happyvertical/smrt-types';

Signal Properties

PropertyTypeDescription
idstringUnique execution identifier
objectIdstringSMRT object instance ID
classNamestringSMRT class name
methodstringMethod being executed
typeSignalTypeLifecycle stage
stepstring?Custom progress label
argsany[]?Sanitized arguments
resultany?Return value (on 'end')
errorError?Error (on 'error')
durationnumber?Execution time (ms)
timestampDateWhen signal was emitted
metadataRecord?Additional context

Best Practices

  1. Use import type for non-enum imports to avoid unnecessary runtime dependencies
  2. Use regular import for enums (UserStatus, TenantStatus, etc.) which need runtime values
  3. Add shared types here if two or more packages need the same type definition
  4. Handle adapter errors - SignalAdapter.handle() should catch its own errors
  5. Sanitize sensitive data - Don't include passwords or keys in signal.args
  6. Keep adapters lightweight - Signals are emitted frequently; avoid heavy operations

Next Steps