mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
7.1 KiB
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 millisecondskeyGenerator?: (...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
keyGeneratorfor 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 combinationsInFlightWithCache: 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:
- Add implementation in
src/lib/ - Include comprehensive unit tests
- Update this documentation
- Export from
src/index.ts