Skip to content

Advanced Examples

Complex validation scenarios and advanced patterns using Validno for sophisticated applications.

Multi-Step Form Wizard

A complex wizard form with conditional validation:

javascript
import Schema from 'validno';

class FormWizard {
  constructor() {
    this.steps = {
      personal: new Schema({
        firstName: {
          type: String,
          required: true,
          rules: { lengthMin: 2, lengthMax: 50 }
        },
        lastName: {
          type: String,
          required: true,
          rules: { lengthMin: 2, lengthMax: 50 }
        },
        birthDate: {
          type: Date,
          required: true,
          rules: {
            custom: (value) => {
              const age = this.calculateAge(new Date(value));
              return {
                result: age >= 18 && age <= 100,
                details: age < 18 ? 'Must be 18 or older' : age > 100 ? 'Invalid birth date' : ''
              };
            }
          }
        },
        gender: {
          type: String,
          required: false,
          rules: { enum: ['male', 'female', 'other', 'prefer-not-to-say'] }
        }
      }),

      contact: new Schema({
        email: {
          type: String,
          required: true,
          rules: { isEmail: true }
        },
        phone: {
          type: String,
          required: true,
          rules: {
            regex: /^(\+\d{1,3}[- ]?)?\(?\d{3}\)?[- ]?\d{3}[- ]?\d{4}$/
          }
        },
        address: {
          street: {
            type: String,
            required: true,
            rules: { lengthMin: 5, lengthMax: 100 }
          },
          city: {
            type: String,
            required: true,
            rules: { lengthMin: 2, lengthMax: 50 }
          },
          state: {
            type: String,
            required: true,
            rules: { length: 2, regex: /^[A-Z]{2}$/ }
          },
          zipCode: {
            type: String,
            required: true,
            rules: { regex: /^\d{5}(-\d{4})?$/ }
          },
          country: {
            type: String,
            required: true,
            rules: { enum: ['US', 'CA', 'MX'] }
          }
        }
      }),

      professional: new Schema({
        employmentStatus: {
          type: String,
          required: true,
          rules: { enum: ['employed', 'unemployed', 'student', 'retired', 'self-employed'] }
        },
        company: {
          type: String,
          required: false, // Will be required conditionally
          rules: { lengthMax: 100 }
        },
        jobTitle: {
          type: String,
          required: false, // Will be required conditionally
          rules: { lengthMax: 100 }
        },
        annualIncome: {
          type: Number,
          required: false,
          rules: { min: 0, max: 10000000 }
        },
        education: {
          type: String,
          required: true,
          rules: {
            enum: [
              'high-school',
              'associates',
              'bachelors',
              'masters',
              'doctorate',
              'other'
            ]
          }
        }
      }),

      preferences: new Schema({
        newsletter: {
          type: Boolean,
          required: false
        },
        communicationMethods: {
          type: Array,
          eachType: String,
          required: true,
          rules: {
            enum: ['email', 'phone', 'sms', 'mail'],
            lengthMin: 1
          }
        },
        interests: {
          type: Array,
          eachType: String,
          required: false,
          rules: {
            enum: [
              'technology',
              'finance',
              'health',
              'travel',
              'education',
              'entertainment',
              'sports',
              'food'
            ],
            lengthMax: 5
          }
        },
        language: {
          type: String,
          required: true,
          rules: { enum: ['en', 'es', 'fr'] }
        }
      })
    };
  }

  calculateAge(birthDate) {
    const today = new Date();
    const birth = new Date(birthDate);
    let age = today.getFullYear() - birth.getFullYear();
    const monthDiff = today.getMonth() - birth.getMonth();
    
    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
      age--;
    }
    
    return age;
  }

  validateStep(stepName, data) {
    const schema = this.steps[stepName];
    if (!schema) {
      throw new Error(`Unknown step: ${stepName}`);
    }

    // Apply conditional validation
    if (stepName === 'professional') {
      return this.validateProfessionalStep(data);
    }

    return schema.validate(data);
  }

  validateProfessionalStep(data) {
    const { employmentStatus } = data;
    
    // Create dynamic schema based on employment status
    const dynamicRules = { ...this.steps.professional.definition };
    
    if (employmentStatus === 'employed' || employmentStatus === 'self-employed') {
      // Company and job title are required for employed people
      dynamicRules.company.required = true;
      dynamicRules.jobTitle.required = true;
    }

    const dynamicSchema = new Schema(dynamicRules);
    return dynamicSchema.validate(data);
  }

  validateAllSteps(allData) {
    const results = {};
    let allValid = true;

    for (const [stepName, stepData] of Object.entries(allData)) {
      const result = this.validateStep(stepName, stepData);
      results[stepName] = result;
      
      if (!result.ok) {
        allValid = false;
      }
    }

    return {
      valid: allValid,
      stepResults: results,
      summary: this.generateSummary(results)
    };
  }

  generateSummary(results) {
    const summary = {
      totalSteps: Object.keys(results).length,
      validSteps: 0,
      invalidSteps: 0,
      totalErrors: 0,
      errorsByStep: {}
    };

    for (const [stepName, result] of Object.entries(results)) {
      if (result.ok) {
        summary.validSteps++;
      } else {
        summary.invalidSteps++;
        summary.totalErrors += result.errors.length;
        summary.errorsByStep[stepName] = result.errors;
      }
    }

    return summary;
  }
}

// Usage
const wizard = new FormWizard();

const formData = {
  personal: {
    firstName: "John",
    lastName: "Doe",
    birthDate: new Date('1985-06-15'),
    gender: "male"
  },
  contact: {
    email: "john.doe@example.com",
    phone: "(555) 123-4567",
    address: {
      street: "123 Main St",
      city: "New York",
      state: "NY",
      zipCode: "10001",
      country: "US"
    }
  },
  professional: {
    employmentStatus: "employed",
    company: "Tech Corp",
    jobTitle: "Software Developer",
    annualIncome: 75000,
    education: "bachelors"
  },
  preferences: {
    newsletter: true,
    communicationMethods: ["email", "phone"],
    interests: ["technology", "education"],
    language: "en"
  }
};

// Validate individual step
const step1Result = wizard.validateStep('personal', formData.personal);

// Validate all steps
const fullResult = wizard.validateAllSteps(formData);

Dynamic Schema Generation

Create schemas dynamically based on configuration:

javascript
class DynamicSchemaBuilder {
  constructor() {
    this.fieldTemplates = {
      string: {
        type: String,
        required: true,
        rules: {}
      },
      email: {
        type: String,
        required: true,
        rules: { isEmail: true }
      },
      number: {
        type: Number,
        required: true,
        rules: {}
      },
      select: {
        type: String,
        required: true,
        rules: { enum: [] }
      },
      multiSelect: {
        type: Array,
        eachType: String,
        required: true,
        rules: { enum: [] }
      },
      boolean: {
        type: Boolean,
        required: true
      },
      date: {
        type: Date,
        required: true,
        rules: {}
      }
    };
  }

  buildFromConfig(config) {
    const definition = {};

    for (const field of config.fields) {
      definition[field.name] = this.buildField(field);
    }

    return new Schema(definition);
  }

  buildField(fieldConfig) {
    const template = this.fieldTemplates[fieldConfig.type];
    if (!template) {
      throw new Error(`Unknown field type: ${fieldConfig.type}`);
    }

    const field = JSON.parse(JSON.stringify(template)); // Deep clone

    // Apply field-specific configuration
    field.required = fieldConfig.required !== false;
    field.title = fieldConfig.label || fieldConfig.name;

    // Apply validation rules
    if (fieldConfig.validation) {
      this.applyValidationRules(field, fieldConfig.validation);
    }

    // Apply custom message
    if (fieldConfig.customMessage) {
      field.customMessage = this.createCustomMessage(fieldConfig.customMessage);
    }

    return field;
  }

  applyValidationRules(field, validation) {
    const rules = field.rules;

    // String validations
    if (validation.minLength) rules.lengthMin = validation.minLength;
    if (validation.maxLength) rules.lengthMax = validation.maxLength;
    if (validation.pattern) rules.regex = new RegExp(validation.pattern);

    // Number validations
    if (validation.min !== undefined) rules.min = validation.min;
    if (validation.max !== undefined) rules.max = validation.max;

    // Array/Select validations
    if (validation.options) rules.enum = validation.options;
    if (validation.minItems) rules.lengthMin = validation.minItems;
    if (validation.maxItems) rules.lengthMax = validation.maxItems;

    // Custom validation
    if (validation.custom) {
      rules.custom = this.createCustomRule(validation.custom);
    }
  }

  createCustomRule(customConfig) {
    return (value, context) => {
      // This could be expanded to support various custom rule types
      if (customConfig.type === 'unique') {
        // Example: check uniqueness against existing values
        const existingValues = customConfig.existingValues || [];
        return {
          result: !existingValues.includes(value),
          details: !existingValues.includes(value) ? '' : customConfig.message || 'Value must be unique'
        };
      }

      if (customConfig.type === 'conditional') {
        // Example: conditional validation based on other fields
        const condition = customConfig.condition;
        const dependentField = condition.field;
        const dependentValue = context.input[dependentField];

        if (dependentValue === condition.value) {
          // Apply conditional validation
          if (condition.required && !value) {
            return {
              result: false,
              details: customConfig.message || 'This field is required'
            };
          }
        }

        return true;
      }

      return true;
    };
  }

  createCustomMessage(messageConfig) {
    return ({ keyword, value, title, rules }) => {
      const messages = messageConfig.messages || {};
      
      if (messages[keyword]) {
        return messages[keyword]
          .replace('{title}', title)
          .replace('{value}', value)
          .replace('{min}', rules.lengthMin || rules.min)
          .replace('{max}', rules.lengthMax || rules.max);
      }

      return `Invalid ${title || 'value'}`;
    };
  }
}

// Configuration-driven form
const formConfig = {
  name: "User Registration Form",
  fields: [
    {
      name: "username",
      type: "string",
      label: "Username",
      required: true,
      validation: {
        minLength: 3,
        maxLength: 20,
        pattern: "^[a-zA-Z0-9_]+$",
        custom: {
          type: "unique",
          existingValues: ["admin", "root", "test"],
          message: "Username is already taken"
        }
      },
      customMessage: {
        messages: {
          lengthMin: "{title} must be at least {min} characters",
          lengthMax: "{title} cannot exceed {max} characters",
          regex: "{title} can only contain letters, numbers, and underscores"
        }
      }
    },
    {
      name: "email",
      type: "email",
      label: "Email Address",
      required: true
    },
    {
      name: "age",
      type: "number",
      label: "Age",
      required: true,
      validation: {
        min: 13,
        max: 120
      }
    },
    {
      name: "country",
      type: "select",
      label: "Country",
      required: true,
      validation: {
        options: ["US", "CA", "UK", "AU", "DE", "FR"]
      }
    },
    {
      name: "interests",
      type: "multiSelect",
      label: "Interests",
      required: false,
      validation: {
        options: ["technology", "sports", "music", "travel", "food"],
        maxItems: 3
      }
    },
    {
      name: "newsletter",
      type: "boolean",
      label: "Subscribe to Newsletter",
      required: false
    },
    {
      name: "emergencyContact",
      type: "string",
      label: "Emergency Contact",
      required: false,
      validation: {
        custom: {
          type: "conditional",
          condition: {
            field: "age",
            value: 18,
            operator: "<",
            required: true
          },
          message: "Emergency contact is required for minors"
        }
      }
    }
  ]
};

// Build and use the schema
const builder = new DynamicSchemaBuilder();
const dynamicSchema = builder.buildFromConfig(formConfig);

const userData = {
  username: "john_doe",
  email: "john@example.com",
  age: 25,
  country: "US",
  interests: ["technology", "travel"],
  newsletter: true,
  emergencyContact: ""
};

const result = dynamicSchema.validate(userData);

Database Entity Validation

Complex validation for database entities with relationships:

javascript
class EntityValidator {
  constructor(database) {
    this.db = database;
    this.setupSchemas();
  }

  setupSchemas() {
    // User entity schema
    this.userSchema = new Schema({
      id: {
        type: Number,
        required: false, // Not required for creation
        rules: {
          custom: async (value) => {
            if (!value) return true; // Skip for new entities
            
            const exists = await this.db.users.exists(value);
            return {
              result: exists,
              details: exists ? '' : 'User ID does not exist'
            };
          }
        }
      },

      username: {
        type: String,
        required: true,
        rules: {
          lengthMin: 3,
          lengthMax: 50,
          regex: /^[a-zA-Z0-9_]+$/,
          custom: async (value, { input }) => {
            const existing = await this.db.users.findByUsername(value);
            if (existing && existing.id !== input.id) {
              return {
                result: false,
                details: 'Username is already taken'
              };
            }
            return true;
          }
        }
      },

      email: {
        type: String,
        required: true,
        rules: {
          isEmail: true,
          custom: async (value, { input }) => {
            const existing = await this.db.users.findByEmail(value);
            if (existing && existing.id !== input.id) {
              return {
                result: false,
                details: 'Email is already registered'
              };
            }
            return true;
          }
        }
      },

      roleId: {
        type: Number,
        required: true,
        rules: {
          custom: async (value) => {
            const role = await this.db.roles.findById(value);
            return {
              result: !!role,
              details: role ? '' : 'Invalid role ID'
            };
          }
        }
      },

      departmentId: {
        type: Number,
        required: false,
        rules: {
          custom: async (value) => {
            if (!value) return true;
            
            const department = await this.db.departments.findById(value);
            return {
              result: !!department,
              details: department ? '' : 'Invalid department ID'
            };
          }
        }
      }
    });

    // Order entity schema with complex business rules
    this.orderSchema = new Schema({
      customerId: {
        type: Number,
        required: true,
        rules: {
          custom: async (value) => {
            const customer = await this.db.customers.findById(value);
            if (!customer) {
              return {
                result: false,
                details: 'Customer not found'
              };
            }

            if (customer.status === 'suspended') {
              return {
                result: false,
                details: 'Cannot create order for suspended customer'
              };
            }

            return true;
          }
        }
      },

      items: {
        type: Array,
        eachType: Object,
        required: true,
        rules: {
          lengthMin: 1,
          custom: async (items) => {
            // Validate each item
            for (const item of items) {
              const product = await this.db.products.findById(item.productId);
              
              if (!product) {
                return {
                  result: false,
                  details: `Product ${item.productId} not found`
                };
              }

              if (product.stock < item.quantity) {
                return {
                  result: false,
                  details: `Insufficient stock for product ${product.name}`
                };
              }

              if (item.quantity <= 0) {
                return {
                  result: false,
                  details: 'Item quantity must be positive'
                };
              }
            }

            return true;
          }
        }
      },

      shippingAddress: {
        street: { type: String, required: true },
        city: { type: String, required: true },
        state: { type: String, required: true },
        zipCode: { type: String, required: true },
        country: { type: String, required: true }
      },

      paymentMethod: {
        type: String,
        required: true,
        rules: {
          enum: ['credit_card', 'debit_card', 'paypal', 'bank_transfer']
        }
      },

      discountCode: {
        type: String,
        required: false,
        rules: {
          custom: async (value, { input }) => {
            if (!value) return true;

            const discount = await this.db.discounts.findByCode(value);
            
            if (!discount) {
              return {
                result: false,
                details: 'Invalid discount code'
              };
            }

            if (discount.expiresAt < new Date()) {
              return {
                result: false,
                details: 'Discount code has expired'
              };
            }

            if (discount.usageLimit && discount.usageCount >= discount.usageLimit) {
              return {
                result: false,
                details: 'Discount code usage limit exceeded'
              };
            }

            // Check minimum order amount
            const orderTotal = this.calculateOrderTotal(input.items);
            if (discount.minimumAmount && orderTotal < discount.minimumAmount) {
              return {
                result: false,
                details: `Minimum order amount of $${discount.minimumAmount} required for this discount`
              };
            }

            return true;
          }
        }
      }
    });
  }

  calculateOrderTotal(items) {
    // This would calculate the total from items
    return items.reduce((total, item) => total + (item.price * item.quantity), 0);
  }

  async validateUser(userData, isUpdate = false) {
    // For updates, make ID optional but validate existing user
    if (isUpdate) {
      const modifiedSchema = { ...this.userSchema.definition };
      modifiedSchema.id.required = true;
      const updateSchema = new Schema(modifiedSchema);
      return await updateSchema.validate(userData);
    }

    return await this.userSchema.validate(userData);
  }

  async validateOrder(orderData) {
    return await this.orderSchema.validate(orderData);
  }

  async validateBulkUsers(userDataArray) {
    const results = [];
    const errors = [];

    for (let i = 0; i < userDataArray.length; i++) {
      const userData = userDataArray[i];
      
      try {
        const result = await this.validateUser(userData);
        results.push({
          index: i,
          data: userData,
          valid: result.ok,
          errors: result.errors
        });

        if (!result.ok) {
          errors.push(`Row ${i + 1}: ${result.joinErrors(', ')}`);
        }
      } catch (error) {
        results.push({
          index: i,
          data: userData,
          valid: false,
          errors: [error.message]
        });
        errors.push(`Row ${i + 1}: ${error.message}`);
      }
    }

    return {
      valid: errors.length === 0,
      results,
      summary: {
        total: userDataArray.length,
        valid: results.filter(r => r.valid).length,
        invalid: results.filter(r => !r.valid).length
      },
      errors
    };
  }
}

// Usage with a database abstraction
const mockDb = {
  users: {
    exists: async (id) => id < 1000,
    findByUsername: async (username) => username === 'admin' ? { id: 1 } : null,
    findByEmail: async (email) => email === 'admin@example.com' ? { id: 1 } : null
  },
  roles: {
    findById: async (id) => [1, 2, 3].includes(id) ? { id, name: 'Role' } : null
  },
  departments: {
    findById: async (id) => [1, 2, 3, 4].includes(id) ? { id, name: 'Dept' } : null
  },
  customers: {
    findById: async (id) => ({ id, status: 'active' })
  },
  products: {
    findById: async (id) => ({ id, name: 'Product', stock: 100, price: 10.00 })
  },
  discounts: {
    findByCode: async (code) => code === 'SAVE10' ? {
      code,
      percentage: 10,
      expiresAt: new Date(Date.now() + 86400000),
      usageLimit: 100,
      usageCount: 50,
      minimumAmount: 50
    } : null
  }
};

const validator = new EntityValidator(mockDb);

// Validate single user
const userData = {
  username: "new_user",
  email: "user@example.com",
  roleId: 2,
  departmentId: 1
};

const userResult = await validator.validateUser(userData);

// Validate order
const orderData = {
  customerId: 123,
  items: [
    { productId: 1, quantity: 2, price: 10.00 },
    { productId: 2, quantity: 1, price: 25.00 }
  ],
  shippingAddress: {
    street: "123 Main St",
    city: "New York",
    state: "NY",
    zipCode: "10001",
    country: "US"
  },
  paymentMethod: "credit_card",
  discountCode: "SAVE10"
};

const orderResult = await validator.validateOrder(orderData);

Real-time Validation Service

A WebSocket-based real-time validation service:

javascript
class ValidationService {
  constructor() {
    this.schemas = new Map();
    this.validationSessions = new Map();
    this.setupSchemas();
  }

  setupSchemas() {
    // Real-time chat message validation
    this.schemas.set('chatMessage', new Schema({
      content: {
        type: String,
        required: true,
        rules: {
          lengthMin: 1,
          lengthMax: 500,
          custom: (value) => {
            // Check for spam patterns
            const spamPatterns = [
              /(.)\1{10,}/, // Repeated characters
              /http[s]?:\/\/[^\s]+/i, // URLs (if not allowed)
              /\b(buy|sell|discount|offer)\b.*\b(now|today|urgent)\b/i // Spam words
            ];

            for (const pattern of spamPatterns) {
              if (pattern.test(value)) {
                return {
                  result: false,
                  details: 'Message appears to be spam'
                };
              }
            }

            return true;
          }
        }
      },
      
      userId: {
        type: Number,
        required: true,
        rules: { min: 1 }
      },
      
      roomId: {
        type: String,
        required: true,
        rules: {
          regex: /^[a-zA-Z0-9_-]+$/,
          lengthMin: 1,
          lengthMax: 50
        }
      },
      
      timestamp: {
        type: Date,
        required: true,
        rules: {
          custom: (value) => {
            const now = new Date();
            const messageTime = new Date(value);
            const diffMs = Math.abs(now - messageTime);
            const diffMinutes = diffMs / (1000 * 60);

            return {
              result: diffMinutes <= 5,
              details: diffMinutes <= 5 ? '' : 'Message timestamp too old'
            };
          }
        }
      }
    }));

    // Live form validation
    this.schemas.set('liveForm', new Schema({
      formId: {
        type: String,
        required: true,
        rules: {
          regex: /^[a-zA-Z0-9_-]+$/
        }
      },
      
      fieldName: {
        type: String,
        required: true,
        rules: {
          lengthMin: 1,
          lengthMax: 100
        }
      },
      
      value: {
        type: 'any',
        required: false // Can be empty during typing
      },
      
      sessionId: {
        type: String,
        required: true,
        rules: {
          regex: /^[a-f0-9-]{36}$/ // UUID format
        }
      }
    }));
  }

  validateRealtime(schemaName, data, sessionId) {
    const schema = this.schemas.get(schemaName);
    if (!schema) {
      return {
        valid: false,
        error: 'Unknown schema',
        timestamp: new Date()
      };
    }

    const result = schema.validate(data);
    
    // Store validation state for the session
    if (!this.validationSessions.has(sessionId)) {
      this.validationSessions.set(sessionId, {
        validations: [],
        startTime: new Date()
      });
    }

    const session = this.validationSessions.get(sessionId);
    session.validations.push({
      schema: schemaName,
      data,
      result,
      timestamp: new Date()
    });

    // Clean up old validations (keep only last 100)
    if (session.validations.length > 100) {
      session.validations = session.validations.slice(-100);
    }

    return {
      valid: result.ok,
      errors: result.errors,
      fieldErrors: result.errorsByKeys,
      timestamp: new Date(),
      sessionStats: {
        totalValidations: session.validations.length,
        validCount: session.validations.filter(v => v.result.ok).length,
        sessionDuration: new Date() - session.startTime
      }
    };
  }

  getSessionStats(sessionId) {
    const session = this.validationSessions.get(sessionId);
    if (!session) {
      return null;
    }

    const validations = session.validations;
    const now = new Date();
    const recentValidations = validations.filter(
      v => (now - v.timestamp) < 60000 // Last minute
    );

    return {
      totalValidations: validations.length,
      recentValidations: recentValidations.length,
      successRate: validations.length > 0 
        ? validations.filter(v => v.result.ok).length / validations.length 
        : 0,
      averageResponseTime: this.calculateAverageResponseTime(validations),
      sessionDuration: now - session.startTime,
      mostCommonErrors: this.getMostCommonErrors(validations)
    };
  }

  calculateAverageResponseTime(validations) {
    if (validations.length < 2) return 0;
    
    let totalTime = 0;
    for (let i = 1; i < validations.length; i++) {
      totalTime += validations[i].timestamp - validations[i-1].timestamp;
    }
    
    return totalTime / (validations.length - 1);
  }

  getMostCommonErrors(validations) {
    const errorCount = {};
    
    validations
      .filter(v => !v.result.ok)
      .forEach(v => {
        v.result.errors.forEach(error => {
          errorCount[error] = (errorCount[error] || 0) + 1;
        });
      });

    return Object.entries(errorCount)
      .sort((a, b) => b[1] - a[1])
      .slice(0, 5)
      .map(([error, count]) => ({ error, count }));
  }

  cleanupSession(sessionId) {
    this.validationSessions.delete(sessionId);
  }

  cleanupOldSessions(maxAgeMs = 3600000) { // 1 hour
    const now = new Date();
    
    for (const [sessionId, session] of this.validationSessions.entries()) {
      if ((now - session.startTime) > maxAgeMs) {
        this.validationSessions.delete(sessionId);
      }
    }
  }
}

// WebSocket server integration
class ValidationWebSocketServer {
  constructor() {
    this.validationService = new ValidationService();
    this.connections = new Map();
    
    // Cleanup old sessions every 30 minutes
    setInterval(() => {
      this.validationService.cleanupOldSessions();
    }, 1800000);
  }

  handleConnection(ws, sessionId) {
    this.connections.set(sessionId, ws);

    ws.on('message', (message) => {
      try {
        const data = JSON.parse(message);
        this.handleValidationRequest(ws, sessionId, data);
      } catch (error) {
        this.sendError(ws, 'Invalid JSON message');
      }
    });

    ws.on('close', () => {
      this.connections.delete(sessionId);
      this.validationService.cleanupSession(sessionId);
    });
  }

  handleValidationRequest(ws, sessionId, request) {
    const { type, schema, data } = request;

    switch (type) {
      case 'validate':
        const result = this.validationService.validateRealtime(
          schema, 
          data, 
          sessionId
        );
        
        this.sendResponse(ws, {
          type: 'validation_result',
          result,
          requestId: request.id
        });
        break;

      case 'get_stats':
        const stats = this.validationService.getSessionStats(sessionId);
        
        this.sendResponse(ws, {
          type: 'session_stats',
          stats,
          requestId: request.id
        });
        break;

      default:
        this.sendError(ws, 'Unknown request type');
    }
  }

  sendResponse(ws, response) {
    if (ws.readyState === ws.OPEN) {
      ws.send(JSON.stringify(response));
    }
  }

  sendError(ws, error) {
    this.sendResponse(ws, {
      type: 'error',
      error,
      timestamp: new Date()
    });
  }
}

// Client-side usage example
class RealtimeValidator {
  constructor(wsUrl) {
    this.ws = new WebSocket(wsUrl);
    this.pendingRequests = new Map();
    this.requestId = 0;

    this.ws.onmessage = (event) => {
      const response = JSON.parse(event.data);
      this.handleResponse(response);
    };
  }

  validateField(schema, data) {
    return new Promise((resolve, reject) => {
      const requestId = ++this.requestId;
      
      this.pendingRequests.set(requestId, { resolve, reject });
      
      this.ws.send(JSON.stringify({
        type: 'validate',
        schema,
        data,
        id: requestId
      }));

      // Timeout after 5 seconds
      setTimeout(() => {
        if (this.pendingRequests.has(requestId)) {
          this.pendingRequests.delete(requestId);
          reject(new Error('Validation timeout'));
        }
      }, 5000);
    });
  }

  getStats() {
    return new Promise((resolve, reject) => {
      const requestId = ++this.requestId;
      
      this.pendingRequests.set(requestId, { resolve, reject });
      
      this.ws.send(JSON.stringify({
        type: 'get_stats',
        id: requestId
      }));
    });
  }

  handleResponse(response) {
    const { requestId, type } = response;
    
    if (requestId && this.pendingRequests.has(requestId)) {
      const { resolve } = this.pendingRequests.get(requestId);
      this.pendingRequests.delete(requestId);
      resolve(response);
    }
  }
}

// Usage
const validator = new RealtimeValidator('ws://localhost:8080');

// Validate chat message in real-time
const validateChatMessage = async (message) => {
  const response = await validator.validateField('chatMessage', {
    content: message.content,
    userId: message.userId,
    roomId: message.roomId,
    timestamp: new Date()
  });

  return response.result;
};

// Live form validation
const validateFormField = async (formId, fieldName, value) => {
  const response = await validator.validateField('liveForm', {
    formId,
    fieldName,
    value,
    sessionId: getCurrentSessionId()
  });

  return response.result;
};

Next Steps

Released under the ISC License.