← back to Wine Finder Next

lib/performance.ts

255 lines

// Performance monitoring utilities
export class PerformanceMonitor {
  private static instance: PerformanceMonitor;
  private metrics: Map<string, number[]> = new Map();

  static getInstance(): PerformanceMonitor {
    if (!PerformanceMonitor.instance) {
      PerformanceMonitor.instance = new PerformanceMonitor();
    }
    return PerformanceMonitor.instance;
  }

  // Measure function execution time
  async measureAsync<T>(
    name: string,
    fn: () => Promise<T>
  ): Promise<T> {
    const start = performance.now();
    try {
      const result = await fn();
      const duration = performance.now() - start;
      this.recordMetric(name, duration);
      return result;
    } catch (error) {
      const duration = performance.now() - start;
      this.recordMetric(name, duration);
      throw error;
    }
  }

  // Record a metric
  recordMetric(name: string, value: number): void {
    if (!this.metrics.has(name)) {
      this.metrics.set(name, []);
    }
    const values = this.metrics.get(name)!;
    values.push(value);

    // Keep only last 100 values
    if (values.length > 100) {
      values.shift();
    }
  }

  // Get statistics for a metric
  getStats(name: string) {
    const values = this.metrics.get(name) || [];
    if (values.length === 0) {
      return null;
    }

    const sorted = [...values].sort((a, b) => a - b);
    const sum = values.reduce((a, b) => a + b, 0);

    return {
      count: values.length,
      mean: sum / values.length,
      median: sorted[Math.floor(sorted.length / 2)],
      p95: sorted[Math.floor(sorted.length * 0.95)],
      p99: sorted[Math.floor(sorted.length * 0.99)],
      min: sorted[0],
      max: sorted[sorted.length - 1],
    };
  }

  // Get all metrics
  getAllStats() {
    const stats: Record<string, any> = {};
    for (const [name] of this.metrics) {
      stats[name] = this.getStats(name);
    }
    return stats;
  }

  // Clear metrics
  clear(): void {
    this.metrics.clear();
  }
}

// Export singleton
export const performanceMonitor = PerformanceMonitor.getInstance();

// Database query optimizer
export class QueryOptimizer {
  // Add indexes to optimize queries
  static getOptimizedIndexes() {
    return {
      bottles: [
        { name: 1 },
        { vintage: 1 },
        { status: 1 },
        { availableMembershipUnits: 1 },
        { createdAt: -1 },
      ],
      membershipUnits: [
        { bottleId: 1, userId: 1 },
        { userId: 1 },
        { status: 1 },
      ],
      votes: [
        { bottleId: 1 },
        { status: 1, endDate: -1 },
        { userId: 1 },
      ],
      marketplace: [
        { status: 1, listedAt: -1 },
        { bottleId: 1 },
        { sellerId: 1 },
        { price: 1 },
      ],
    };
  }

  // Optimize query with projection
  static optimizeProjection(fields: string[]): Record<string, 0 | 1> {
    const projection: Record<string, 0 | 1> = {};
    fields.forEach(field => {
      projection[field] = 1;
    });
    return projection;
  }

  // Add pagination
  static paginate(query: any, page: number, limit: number) {
    const skip = (page - 1) * limit;
    return query.skip(skip).limit(limit);
  }
}

// Image optimization utilities
export class ImageOptimizer {
  static getOptimizedSrc(src: string, width: number, quality = 75): string {
    // Next.js Image optimization API
    const params = new URLSearchParams({
      url: src,
      w: width.toString(),
      q: quality.toString(),
    });
    return `/_next/image?${params.toString()}`;
  }

  static getSrcSet(src: string, widths: number[], quality = 75): string {
    return widths
      .map(w => `${this.getOptimizedSrc(src, w, quality)} ${w}w`)
      .join(', ');
  }

  static getResponsiveSizes(): string {
    return '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw';
  }
}

// Lazy loading utilities
export class LazyLoader {
  private static observer: IntersectionObserver | null = null;

  static initObserver(callback: IntersectionObserverCallback): void {
    if (typeof window === 'undefined') return;

    this.observer = new IntersectionObserver(callback, {
      rootMargin: '50px',
      threshold: 0.01,
    });
  }

  static observe(element: Element): void {
    if (this.observer) {
      this.observer.observe(element);
    }
  }

  static unobserve(element: Element): void {
    if (this.observer) {
      this.observer.unobserve(element);
    }
  }

  static disconnect(): void {
    if (this.observer) {
      this.observer.disconnect();
      this.observer = null;
    }
  }
}

// Request debouncing
export function debounce<T extends (...args: any[]) => any>(
  func: T,
  wait: number
): (...args: Parameters<T>) => void {
  let timeout: NodeJS.Timeout;

  return function executedFunction(...args: Parameters<T>) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };

    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// Request throttling
export function throttle<T extends (...args: any[]) => any>(
  func: T,
  limit: number
): (...args: Parameters<T>) => void {
  let inThrottle: boolean;

  return function executedFunction(...args: Parameters<T>) {
    if (!inThrottle) {
      func.apply(null, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// Memory cache for client-side
export class MemoryCache<T> {
  private cache: Map<string, { value: T; expiry: number }> = new Map();

  set(key: string, value: T, ttlMs: number): void {
    const expiry = Date.now() + ttlMs;
    this.cache.set(key, { value, expiry });
  }

  get(key: string): T | null {
    const item = this.cache.get(key);
    if (!item) return null;

    if (Date.now() > item.expiry) {
      this.cache.delete(key);
      return null;
    }

    return item.value;
  }

  clear(): void {
    this.cache.clear();
  }

  // Clean expired entries
  cleanup(): void {
    const now = Date.now();
    for (const [key, item] of this.cache) {
      if (now > item.expiry) {
        this.cache.delete(key);
      }
    }
  }
}