s-m-r-t

@happyvertical/smrt-types

Shared TypeScript type definitions and interfaces used across multiple SMRT packages to prevent circular dependencies and ensure type safety.

v0.17.100Core 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.

Currently, this package exports the Signal System types, which power automatic method tracking and event distribution across SMRT objects.

Installation

bash
npm install @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

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
// Type exports (no runtime code)
export type { Signal } from './signals.js';
export type { SignalAdapter } from './signals.js';
export type { SignalType } from './signals.js';

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. Don't install directly - Let other SMRT packages bring it in as a dependency
  2. Use type-only imports - import type { Signal } from '@happyvertical/smrt-types'
  3. Handle adapter errors - SignalAdapter.handle() should catch its own errors
  4. Sanitize sensitive data - Don't include passwords or keys in signal.args
  5. Keep adapters lightweight - Signals are emitted frequently; avoid heavy operations

Next Steps