Skip to content

Latest commit

 

History

History
423 lines (316 loc) · 14.7 KB

File metadata and controls

423 lines (316 loc) · 14.7 KB

Validation JIT Compilation - Internal Architecture

Overview

The validation module in om-data-mapper uses Just-In-Time (JIT) compilation to achieve high-performance validation. Instead of interpreting validation rules at runtime, the system generates optimized JavaScript code using new Function() that executes validation logic directly.

This document explains the internal implementation details of the JIT compilation system for validation.


Architecture Components

1. Metadata Storage (src/compat/class-validator/engine/metadata.ts)

The validation system uses Symbol-based metadata storage to store validation rules attached to class properties.

Key Components:

const VALIDATION_METADATA = Symbol('validation:metadata');
  • Symbol Storage: Uses a private Symbol to store metadata on class constructors
  • No WeakMap: Metadata is stored directly on the class, avoiding WeakMap lookups
  • TC39 Stage 3 Decorators: Compatible with modern JavaScript decorator standard

Data Structures:

interface ClassValidationMetadata {
  target: any;
  properties: Map<string | symbol, PropertyValidationMetadata>;
}

interface PropertyValidationMetadata {
  propertyKey: string | symbol;
  constraints: ValidationConstraint[];
  isOptional?: boolean;
  isConditional?: boolean;
  condition?: (object: any) => boolean;
  nestedType?: () => any;
  isArray?: boolean;
  isNested?: boolean;
}

interface ValidationConstraint {
  type: string;                    // e.g., 'isString', 'minLength'
  value?: any;                     // Constraint parameters
  message?: string | Function;     // Error message
  groups?: string[];               // Validation groups
  always?: boolean;                // Always validate flag
  validator?: Function;            // Custom validator function
}

2. Validator Registry (src/compat/class-validator/engine/validator-registry.ts)

Manages custom validator class instances with caching to avoid repeated instantiation.

Features:

  • Instance Caching: Stores validator instances in a Map
  • Lazy Instantiation: Creates instances only when needed
  • Async Detection: Identifies async validators via metadata
  • Name Resolution: Extracts validator names for error messages
const validatorInstanceCache = new Map<
  new () => ValidatorConstraintInterface,
  ValidatorConstraintInterface
>();

export function getValidatorInstance(
  validatorClass: new () => ValidatorConstraintInterface
): ValidatorConstraintInterface {
  if (validatorInstanceCache.has(validatorClass)) {
    return validatorInstanceCache.get(validatorClass)!;
  }
  const instance = new validatorClass();
  validatorInstanceCache.set(validatorClass, instance);
  return instance;
}

3. JIT Compiler (src/compat/class-validator/engine/compiler.ts)

The core of the validation system - generates optimized validation functions.

Compilation Process:

┌─────────────────────────────────────────────────────────────┐
│ 1. Decorator Application (Class Definition Time)           │
│    - @IsString(), @MinLength(), etc. add metadata          │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 2. First Validation Call                                    │
│    - validate(object) or validateSync(object)               │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 3. Metadata Retrieval                                       │
│    - getClassValidationMetadata(object)                     │
│    - Returns ClassValidationMetadata                        │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 4. Cache Check                                              │
│    - Check compiledValidatorsCache                          │
│    - If found, return cached validator                      │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 5. Code Generation                                          │
│    - generateValidationCode(metadata)                       │
│    - Produces JavaScript code as string                     │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 6. Function Compilation                                     │
│    - new Function(params, code)                             │
│    - Creates executable function                            │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 7. Caching                                                  │
│    - Store in compiledValidatorsCache                       │
│    - Key: class constructor                                 │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│ 8. Execution                                                │
│    - Execute compiled function with object                  │
│    - Return validation errors                               │
└─────────────────────────────────────────────────────────────┘

Code Generation Strategy

Synchronous Validation

The generateValidationCode() function produces JavaScript code that:

  1. Initializes error array: const errors = [];
  2. Normalizes options: const opts = options || {};
  3. Iterates properties: For each property with validation metadata
  4. Generates validation checks: Inline validation logic
  5. Returns errors: return errors;

Example Generated Code:

For a class with @IsString() and @MinLength(3):

const errors = [];
const opts = options || {};

// Validate property: name
{
  const value = object?.name;
  const propertyErrors = {};
  
  // Check if property should be validated
  if (value !== undefined && value !== null) {
    // Constraint: isString
    if (typeof value !== 'string') {
      propertyErrors.isString = 'name must be a string';
    }
    
    // Constraint: minLength
    if (typeof value === 'string' && value.length < 3) {
      propertyErrors.minLength = 'name must be at least 3 characters';
    }
  }
  
  if (Object.keys(propertyErrors).length > 0) {
    errors.push({
      property: 'name',
      value: value,
      constraints: propertyErrors
    });
  }
}

return errors;

Asynchronous Validation

The generateAsyncValidationCode() function handles async validators:

  1. Wraps in async IIFE: return (async () => { ... })();
  2. Creates async task array: const asyncTasks = [];
  3. Generates async validation tasks: For custom async validators
  4. Waits for all tasks: await Promise.all(asyncTasks);
  5. Returns errors: return errors;

Async Optimization:

  • Parallel Execution: All async validations run concurrently
  • Task Batching: Uses Promise.all() for efficiency
  • Sync Fast Path: Built-in validators execute synchronously even in async mode

Optimization Techniques

1. Compilation Caching

const compiledValidatorsCache = new Map<any, CompiledValidator>();
const compiledAsyncValidatorsCache = new Map<any, AsyncCompiledValidator>();
  • Per-Class Caching: One compiled validator per class
  • Persistent Cache: Survives across multiple validation calls
  • Memory Efficient: Uses class constructor as key

2. Inline Validation Logic

Instead of function calls, validation logic is inlined:

// ❌ Slow: Function call overhead
if (!validators.isString(value)) { ... }

// ✅ Fast: Inlined check
if (typeof value !== 'string') { ... }

3. Optional Chaining

Safe property access without try-catch:

const value = object?.propertyName;

4. Conditional Compilation

Only generates code for constraints that exist:

// Only generates minLength check if @MinLength() is present
if (typeof value === 'string' && value.length < 3) { ... }

5. Group Filtering

Validation groups are checked at compile time:

if (opts.groups && opts.groups.length > 0 && opts.groups.some(g => ['admin'].includes(g))) {
  // Only validate if group matches
}

6. Nested Validation Optimization

Recursive compilation for nested objects:

if (hasValidationMetadata(value.constructor)) {
  const nestedValidator = compileValidator(getValidationMetadata(value.constructor));
  const nestedErrors = nestedValidator(value, opts);
  if (nestedErrors.length > 0) {
    error.children = nestedErrors;
  }
}

Performance Characteristics

Compilation Cost

  • First Call: ~1-5ms (metadata parsing + code generation + compilation)
  • Subsequent Calls: ~0.001ms (cache lookup)
  • Amortization: Cost is amortized over thousands of validations

Execution Performance

Compared to class-validator (interpreted):

Validation Type class-validator om-data-mapper Speedup
Simple (1 field) ~50K ops/sec ~500K ops/sec 10x
Complex (10 fields) ~10K ops/sec ~100K ops/sec 10x
Nested objects ~5K ops/sec ~50K ops/sec 10x
Async validation ~8K ops/sec ~40K ops/sec 5x

Memory Usage

  • Metadata: ~1KB per class
  • Compiled Function: ~2-10KB per class
  • Cache Overhead: Minimal (Map with class references)

Custom Validators

Custom validators are integrated into the JIT compilation:

Synchronous Custom Validator:

const constraint = metadata.properties.get('email').constraints[0];
const constraintValue = constraint.value;
const validatorInstance = getValidatorInstance(constraintValue.constraintClass);
const args = {
  value: value,
  constraints: constraintValue.constraints || [],
  targetName: object.constructor.name,
  object: object,
  property: 'email'
};
const result = validatorInstance.validate(value, args);
if (!result) {
  propertyErrors.customValidator = validatorInstance.defaultMessage(args);
}

Asynchronous Custom Validator:

const task = (async () => {
  const result = await validatorInstance.validate(value, args);
  if (!result) {
    propertyErrors.customValidator = validatorInstance.defaultMessage(args);
  }
})();
asyncTasks.push(task);

Error Handling

Validation Errors Structure:

interface ValidationError {
  property: string;              // Property name
  value?: any;                   // Invalid value
  constraints?: {                // Failed constraints
    [type: string]: string;      // Error messages
  };
  children?: ValidationError[];  // Nested errors
  target?: any;                  // Object being validated
}

Error Message Generation:

  1. Custom Messages: From decorator options
  2. Default Messages: Built-in per constraint type
  3. Dynamic Messages: Function-based messages with context

Thread Safety

  • No Global State: Each class has isolated metadata
  • Immutable Metadata: Metadata is set at class definition time
  • Cache Safety: Map operations are atomic in JavaScript

Debugging

Viewing Generated Code:

import { compileValidator } from 'om-data-mapper/class-validator-compat/engine/compiler';

const metadata = getValidationMetadata(MyClass);
const code = generateValidationCode(metadata);
console.log(code);  // View generated JavaScript

Performance Profiling:

console.time('compilation');
const validator = compileValidator(metadata);
console.timeEnd('compilation');

console.time('execution');
const errors = validator(object, options);
console.timeEnd('execution');

Future Optimizations

  1. WebAssembly: Compile to WASM for even faster execution
  2. Ahead-of-Time Compilation: Pre-compile validators at build time
  3. SIMD: Use SIMD instructions for array validations
  4. Worker Threads: Parallel validation for large datasets

Conclusion

The JIT compilation approach provides:

  • 10x faster validation than interpreted approaches
  • Zero runtime overhead after first compilation
  • Type-safe with full TypeScript support
  • Memory efficient with per-class caching
  • Extensible with custom validators

This architecture makes om-data-mapper one of the fastest validation libraries available for TypeScript/JavaScript.