Savvi Studio

Registry Pattern Refactoring

Overview

Extracted common registry and builder patterns from ToolRegistry and TypeMapperContext into reusable base classes in src/lib/common/registry/.

Motivation

Both ToolRegistry and TypeMapperContext implemented similar patterns:

  • Registry pattern: Map-based storage with type-safe lookups
  • Builder pattern: Fluent API for constructing immutable instances
  • Dispatcher pattern: Main handler + specialized handlers by pattern
  • Validation: Common validation interfaces and utilities

New Abstractions

Base Classes

Registry<K, V>

Location: src/lib/common/registry/base-registry.ts

Abstract base class for immutable registries:

  • Readonly Map storage
  • Type-safe get/getOrThrow methods
  • Common query methods (keys, values, entries, has)
abstract class Registry<K, V> {
  protected readonly items: ReadonlyMap<K, V>;
  get(key: K): V | undefined
  getOrThrow(key: K): V
  has(key: K): boolean
  keys(): K[]
  values(): V[]
  // ... more
}

BuildableRegistry<K, V>

Location: src/lib/common/registry/buildable-registry.ts

Base class for registries built with builder pattern:

  • Extends Registry<K, V>
  • Provides BaseBuilder class that can be extended
  • Mutable during construction, immutable after build()
abstract class BuildableRegistry<K, V> extends Registry<K, V> {
  protected static BaseBuilder = class<K, V> {
    protected items = new Map<K, V>();
    register(key: K, value: V): this
    has(key: K): boolean
    protected getItems(): Map<K, V>
  }
}

Dispatcher<T> & DispatcherBuilder<T>

Location: src/lib/common/registry/dispatcher.ts

Pattern for main handler + specialized handlers:

  • Immutable Dispatcher with main + specialized Map
  • Builder accumulates handlers then builds immutable instance
class Dispatcher<T> {
  readonly main: T;
  readonly specialized: ReadonlyMap<string, T>;
  getSpecialized(pattern: string): T | undefined
}

class DispatcherBuilder<T> {
  setMain(handler: T): this
  register(pattern: string, handler: T): this
  build(): Dispatcher<T>
}

Validation Utilities

Location: src/lib/common/registry/validation.ts

Common validation patterns:

  • Validatable interface
  • ValidationResult type
  • validateAll() helper function
  • validateSubset() helper
  • checkRequired() helper
interface Validatable {
  validate(): Promise<ValidationResult> | ValidationResult
}

interface ValidationResult {
  valid: boolean
  errors?: string[]
  warnings?: string[]
}

Refactored Components

ToolRegistry

File: src/lib/common/tool.ts

Before: Mutable registry with direct Map manipulation

class ToolRegistry {
  private tools = new Map<QualifiedName, Tool>();
  register(tool: Tool) { this.tools.set(...) }
}
export const toolRegistry = new ToolRegistry();

After: Immutable registry with builder pattern

class ToolRegistry extends BuildableRegistry<QualifiedName, Tool> {
  static Builder = class { ... }
  static builder(): Builder { ... }
  private constructor(tools: Map<...>) { super(tools); }
}

export const toolRegistry = ToolRegistry.builder().build();

Benefits:

  • ✅ Immutable after construction
  • ✅ Builder pattern for clean initialization
  • ✅ Inherits common query methods from base class
  • ✅ Implements Validatable interface

TypeMapperContext

File: src/lib/codegen/builders/ast/mappings/context.ts

Before: Custom dispatcher implementation

class TypeMapperContext {
  private readonly registries: {
    zod: { main: T, specialized: Map<string, T> }
    typeNode: { main: T, specialized: Map<string, T> }
  }
}

After: Uses Dispatcher pattern

type ConverterDispatchers = {
  zod: Dispatcher<ZodASTConverter>
  typeNode: Dispatcher<TypeNodeConverter>
}

class TypeMapperContext {
  private readonly dispatchers: ConverterDispatchers;
  
  static Builder = class {
    private builders = {
      zod: new DispatcherBuilder<ZodASTConverter>(),
      typeNode: new DispatcherBuilder<TypeNodeConverter>(),
    }
  }
}

Benefits:

  • ✅ Reuses Dispatcher abstraction
  • ✅ Cleaner builder implementation
  • ✅ Explicit dispatcher semantics
  • ✅ Reduced code duplication

File Structure

src/lib/common/
├── registry/
│   ├── index.ts              # Re-exports all abstractions
│   ├── base-registry.ts      # Registry<K, V> base class
│   ├── buildable-registry.ts # BuildableRegistry<K, V>
│   ├── dispatcher.ts         # Dispatcher<T> + DispatcherBuilder<T>
│   └── validation.ts         # Validatable, ValidationResult, helpers
└── tool.ts                   # ToolRegistry uses BuildableRegistry

Usage Examples

Creating a New Registry

import { BuildableRegistry } from '@/lib/common/registry';

class MyRegistry extends BuildableRegistry<string, MyItem> {
  static Builder = class {
    private items = new Map<string, MyItem>();
    
    register(item: MyItem): this {
      this.items.set(item.id, item);
      return this;
    }
    
    build(): MyRegistry {
      return new MyRegistry(this.items);
    }
  };
  
  static builder() {
    return new MyRegistry.Builder();
  }
  
  private constructor(items: Map<string, MyItem>) {
    super(items);
  }
}

// Usage
const registry = MyRegistry.builder()
  .register(item1)
  .register(item2)
  .build();

Using Dispatcher Pattern

import { DispatcherBuilder } from '@/lib/common/registry';

type Handler = (input: string) => string;

const dispatcher = new DispatcherBuilder<Handler>()
  .setMain((input) => `main: ${input}`)
  .register('special', (input) => `special: ${input}`)
  .build();

dispatcher.main('test');                    // "main: test"
dispatcher.getSpecialized('special')?.('x'); // "special: x"

Breaking Changes

ToolRegistry

Old API (mutable):

toolRegistry.register(tool);  // Direct mutation

New API (immutable):

// At initialization
const registry = ToolRegistry.builder()
  .register(tool1)
  .register(tool2)
  .build();

// Usage (unchanged)
const tool = registry.get(name);

TypeMapperContext

No breaking changes - internal refactoring only. Public API remains the same.

Benefits

  1. DRY - Eliminates duplicate registry/builder logic
  2. Type Safety - Generic base classes ensure consistency
  3. Immutability - Enforced through readonly Maps and builder pattern
  4. Testability - Base classes can be unit tested once
  5. Reusability - Other parts of codebase can adopt these patterns
  6. Documentation - Patterns are now explicitly documented

Future Enhancements

  • Add more registry types (e.g., CacheRegistry, ServiceRegistry)
  • Add registry composition utilities
  • Add serialization/deserialization support
  • Add registry merging capabilities
  • Extract more patterns as needed
  • src/lib/common/tool.ts - Tool and ToolRegistry
  • src/lib/codegen/builders/ast/mappings/context.ts - TypeMapperContext
  • src/lib/common/registry/ - All registry abstractions