@happyvertical/smrt-types
Shared TypeScript type definitions and interfaces used across multiple SMRT packages to prevent circular dependencies and ensure type safety.
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
npm install @happyvertical/smrt-typesNote: 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
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 startedstep- Manual progress step (optional)end- Method completed successfullyerror- Method failed with error
type SignalType = 'start' | 'step' | 'end' | 'error';SignalAdapter Interface
Adapters consume signals for specific purposes (logging, metrics, tracing, etc.):
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 systemsUsage in SMRT Objects
SMRT objects automatically emit signals during method execution. You can listen to these signals using the internal signal bus:
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:
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
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
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
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?
- Prevent Circular Dependencies - Shared types can be imported without creating dependency cycles
- Single Source of Truth - Type definitions maintained in one place
- Version Synchronization - All packages use the same type version
- Type Safety - TypeScript ensures consistency across the SDK
- Tree Shaking - Zero runtime overhead (types only)
API Reference
Exports
// 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
| Property | Type | Description |
|---|---|---|
id | string | Unique execution identifier |
objectId | string | SMRT object instance ID |
className | string | SMRT class name |
method | string | Method being executed |
type | SignalType | Lifecycle stage |
step | string? | Custom progress label |
args | any[]? | Sanitized arguments |
result | any? | Return value (on 'end') |
error | Error? | Error (on 'error') |
duration | number? | Execution time (ms) |
timestamp | Date | When signal was emitted |
metadata | Record? | Additional context |
Best Practices
- Don't install directly - Let other SMRT packages bring it in as a dependency
- Use type-only imports -
import type { Signal } from '@happyvertical/smrt-types' - Handle adapter errors - SignalAdapter.handle() should catch its own errors
- Sanitize sensitive data - Don't include passwords or keys in signal.args
- Keep adapters lightweight - Signals are emitted frequently; avoid heavy operations