Files
ISA-Frontend/libs/common/decorators/README.md
2025-07-10 16:00:16 +00:00

7.1 KiB

Common Decorators Library

A collection of TypeScript decorators for common cross-cutting concerns in Angular applications.

Installation

This library is already configured in the project's tsconfig.base.json. Import decorators using:

import { InFlight, InFlightWithKey, InFlightWithCache } from '@isa/common/decorators';

Available Decorators

🚀 InFlight Decorators

Prevent multiple simultaneous calls to the same async method. All concurrent calls receive the same Promise result.

Basic Usage

import { InFlight } from '@isa/common/decorators';

@Injectable()
class DataService {
  @InFlight()
  async fetchData(): Promise<Data> {
    // Even if called multiple times simultaneously, 
    // only one API call will be made
    return await this.http.get<Data>('/api/data').toPromise();
  }
}

Benefits:

  • Prevents duplicate API calls
  • Reduces server load
  • Improves application performance
  • All callers receive the same result

🔑 InFlightWithKey

Prevents duplicate calls while considering method arguments. Each unique set of arguments gets its own in-flight tracking.

import { InFlightWithKey } from '@isa/common/decorators';

@Injectable()
class UserService {
  @InFlightWithKey({
    keyGenerator: (userId: string) => userId
  })
  async fetchUser(userId: string): Promise<User> {
    // Multiple calls with same userId share the same request
    // Different userIds can execute simultaneously
    return await this.http.get<User>(`/api/users/${userId}`).toPromise();
  }

  @InFlightWithKey() // Uses JSON.stringify by default
  async searchUsers(query: string, page: number): Promise<User[]> {
    return await this.http.get<User[]>(`/api/users/search`, {
      params: { query, page: page.toString() }
    }).toPromise();
  }
}

Configuration Options:

  • keyGenerator?: (...args) => string - Custom key generation function
  • If not provided, uses JSON.stringify(args) as the key

🗄️ InFlightWithCache

Combines in-flight request deduplication with result caching.

import { InFlightWithCache } from '@isa/common/decorators';

@Injectable()
class ProductService {
  @InFlightWithCache({
    cacheTime: 5 * 60 * 1000, // Cache for 5 minutes
    keyGenerator: (productId: string) => productId
  })
  async getProduct(productId: string): Promise<Product> {
    // Results are cached for 5 minutes
    // Multiple calls within cache time return cached result
    return await this.http.get<Product>(`/api/products/${productId}`).toPromise();
  }
}

Configuration Options:

  • cacheTime?: number - Cache duration in milliseconds
  • keyGenerator?: (...args) => string - Custom key generation function

How It Works

Memory Management

All decorators use WeakMap for memory efficiency:

  • Automatic garbage collection when instances are destroyed
  • No memory leaks
  • Per-instance state isolation

Error Handling

  • Failed requests are not cached
  • In-flight tracking is cleaned up on both success and error
  • All concurrent callers receive the same error

Thread Safety

  • Decorators are instance-aware
  • Each service instance has its own in-flight tracking
  • No shared state between instances

Real-World Examples

Solving Your Original Problem

// Before: Multiple simultaneous calls
@Injectable({ providedIn: 'root' })
export class RemissionProductGroupService {
  async fetchProductGroups(): Promise<KeyValueStringAndString[]> {
    // Multiple calls = multiple API requests
    return await this.apiCall();
  }
}

// After: Using InFlight decorator
@Injectable({ providedIn: 'root' })
export class RemissionProductGroupService {
  @InFlight()
  async fetchProductGroups(): Promise<KeyValueStringAndString[]> {
    // Multiple simultaneous calls = single API request
    return await this.apiCall();
  }
}

Advanced Scenarios

@Injectable()
class OrderService {
  // Different cache times for different data types
  @InFlightWithCache({ cacheTime: 30 * 1000 }) // 30 seconds
  async getOrderStatus(orderId: string): Promise<OrderStatus> {
    return await this.http.get<OrderStatus>(`/api/orders/${orderId}/status`).toPromise();
  }

  @InFlightWithCache({ cacheTime: 10 * 60 * 1000 }) // 10 minutes
  async getOrderHistory(customerId: string): Promise<Order[]> {
    return await this.http.get<Order[]>(`/api/customers/${customerId}/orders`).toPromise();
  }

  // Custom key generation for complex parameters
  @InFlightWithKey({
    keyGenerator: (filter: OrderFilter) => 
      `${filter.status}-${filter.dateFrom}-${filter.dateTo}`
  })
  async searchOrders(filter: OrderFilter): Promise<Order[]> {
    return await this.http.post<Order[]>('/api/orders/search', filter).toPromise();
  }
}

Best Practices

Do

  • Use @InFlight() for simple methods without parameters
  • Use @InFlightWithKey() for methods with parameters
  • Use @InFlightWithCache() for expensive operations with stable results
  • Provide custom keyGenerator for complex parameter objects
  • Set appropriate cache times based on data volatility

Don't

  • Use on methods that return different results for the same input
  • Use excessively long cache times for dynamic data
  • Use on methods that have side effects (POST, PUT, DELETE)
  • Rely on argument order for default key generation

Performance Considerations

Memory Usage

  • InFlight: Minimal memory overhead (one Promise per instance)
  • InFlightWithKey: Memory usage scales with unique parameter combinations
  • InFlightWithCache: Additional memory for cached results

Cleanup

  • In-flight requests are automatically cleaned up on completion
  • Cache entries are cleaned up on expiry
  • WeakMap ensures instances can be garbage collected

Testing

The decorators are fully tested with comprehensive unit tests. Key test scenarios include:

  • Multiple simultaneous calls deduplication
  • Error handling and cleanup
  • Cache expiration
  • Instance isolation
  • Key generation

Run tests with:

npx nx test common-decorators

Migration Guide

From Manual Implementation

// Before: Manual in-flight tracking
class MyService {
  private inFlight: Promise<Data> | null = null;

  async fetchData(): Promise<Data> {
    if (this.inFlight) {
      return this.inFlight;
    }
    
    this.inFlight = this.doFetch();
    try {
      return await this.inFlight;
    } finally {
      this.inFlight = null;
    }
  }
}

// After: Using decorator
class MyService {
  @InFlight()
  async fetchData(): Promise<Data> {
    return await this.doFetch();
  }
}

From RxJS shareReplay

// Before: RxJS approach
class MyService {
  private data$ = this.http.get<Data>('/api/data').pipe(
    shareReplay({ bufferSize: 1, refCount: true })
  );

  getData(): Observable<Data> {
    return this.data$;
  }
}

// After: Promise-based with decorator
class MyService {
  @InFlightWithCache({ cacheTime: 5 * 60 * 1000 })
  async getData(): Promise<Data> {
    return await this.http.get<Data>('/api/data').toPromise();
  }
}

Contributing

When adding new decorators:

  1. Add implementation in src/lib/
  2. Include comprehensive unit tests
  3. Update this documentation
  4. Export from src/index.ts