@contiva/sap-integration-suite-client
Version:
SAP Cloud Platform Integration API Client
682 lines (681 loc) • 26 kB
TypeScript
/**
* Redis-based cache manager for SAP API responses
*
* @module cache-manager
*/
import { CachedData, CacheOptions, CacheAdminStats, CacheKeyInfo } from '../types/cache';
/**
* Manages caching operations with Redis
*/
export declare class CacheManager {
private client;
private isConnected;
private isEnabled;
private connectionString;
private connectPromise;
private encryptionKey;
private encryptionEnabled;
private connectedAt;
private _memoryFallbackCache;
private _memoryFallbackMaxSize;
private _memoryFallbackEnabled;
private maxReconnectAttempts;
private reconnectDelay;
private maxReconnectDelay;
private currentReconnectAttempt;
private isReconnecting;
private _revalidationQueues;
private _revalidationExecuting;
private _revalidationProcessing;
private _revalidationConcurrency;
private _revalidationDelayMs;
private _maxQueueLengthPerHost;
private _queueDropStrategy;
private _revalidationSessionCount;
private _revalidationSessionStart;
/**
* Tracks ongoing revalidation operations to prevent duplicate SAP API calls
*
* Maps cache keys to their revalidation promises. When multiple requests
* arrive for the same stale cache key, subsequent requests will be skipped
* while the first revalidation is in progress.
*
* Memory Management:
* - Keys are automatically removed after revalidation completes (success or failure)
* - All entries are cleared when the cache manager is closed
*
* @private
* @since 1.x.x (Queue Deduplication Feature)
*/
private _revalidationInProgress;
/**
* Creates a new CacheManager instance
*
* @param connectionString - Redis connection string
* @param enabled - Whether caching is enabled
* @param encryptionSecret - Optional secret for encrypting cache values (recommended: use OAuth client secret)
* @param maxQueueLength - Maximum revalidation queue length (default: 500, increased from 100 for P2-2 fix)
* @param queueDropStrategy - Strategy for handling queue overflow: 'oldest' (drop oldest tasks), 'newest' (drop newest/incoming tasks), 'warn' (only warn, no limit)
*/
constructor(connectionString: string, enabled?: boolean, encryptionSecret?: string, maxQueueLength?: number, queueDropStrategy?: 'oldest' | 'newest' | 'warn');
/**
* Initializes encryption key from the provided secret
* Uses PBKDF2 to derive a secure encryption key
*
* FIX P3-1: Salt is now tenant-specific by including a hash of the secret
* This ensures different tenants (with different secrets) get different encryption keys
* even if the static salt prefix is the same.
*
* @param secret - The secret to derive the encryption key from
*/
private initializeEncryption;
/**
* Encrypts data using AES-256-GCM
*
* @param data - The data to encrypt
* @returns Encrypted data with IV prepended
*/
private encrypt;
/**
* Decrypts data using AES-256-GCM
*
* @param encryptedData - The encrypted data with IV prepended
* @returns Decrypted data
*/
private decrypt;
/**
* Initializes and connects to Redis
* Must be called before using the cache manager
*/
connect(): Promise<void>;
/**
* Initializes the Redis client connection
*/
private initialize;
/**
* Gets cached data by key
* Falls back to in-memory cache if Redis is unavailable (P2-3 fix)
*
* @param key - The cache key
* @returns Cached data or null if not found
*/
get(key: string): Promise<CachedData | null>;
/**
* Stores data in the in-memory fallback cache
* Implements simple LRU eviction when max size is reached
*
* @param key - The cache key
* @param data - The cached data
* @private
*/
private _setMemoryFallback;
/**
* Sets cached data with options
* Also stores in memory fallback for resilience (P2-3 fix)
*
* @param key - The cache key
* @param data - The data to cache
* @param options - Cache options (TTL and revalidation time)
*/
set(key: string, data: any, options: CacheOptions): Promise<void>;
/**
* Determines if cached data should be revalidated
*
* @param cachedData - The cached data to check
* @param forceRevalidate - Force revalidation regardless of time
* @returns true if revalidation is needed
*/
shouldRevalidate(cachedData: CachedData, forceRevalidate?: boolean): boolean;
/**
* Checks if cached data is expired
*
* @param cachedData - The cached data to check
* @returns true if the data is expired
*/
isExpired(cachedData: CachedData): boolean;
private _getHostnameQueue;
private _getHostnameExecuting;
private _processHostnameQueue;
private _getTotalQueueLength;
/**
* Cleans up revalidation tracking after completion
*
* This method is called in the `finally` block of each revalidation task
* to ensure the cache key is removed from `_revalidationInProgress`,
* regardless of whether the revalidation succeeded or failed.
*
* Memory Safety:
* - Guaranteed cleanup through `finally` block
* - Safe to call multiple times (idempotent)
* - Handles non-existent keys gracefully
*
* Debug Logging:
* When DEBUG=true, logs the cleanup operation and the current number
* of revalidations still in progress.
*
* @param key - The cache key that completed revalidation
* @private
* @since 1.x.x (Queue Deduplication Feature)
*/
private _cleanupRevalidation;
/**
* Revalidates cache in the background with timeout and rate limiting
* Supports differential updates for collection endpoints
*
* Queue Deduplication:
* Prevents multiple concurrent revalidations of the same cache key.
* If a revalidation for a given key is already in progress, subsequent
* requests will be skipped to avoid duplicate SAP API calls.
*
* Example scenario:
* - 50 users request the same stale artifact data simultaneously
* - Without deduplication: 50 SAP API calls
* - With deduplication: 1 SAP API call
*
* @param key - The cache key to revalidate
* @param fetchFn - Function that fetches fresh data
* @param options - Cache options for the new data
* @param enableDifferential - Enable differential updates (only for collections)
* @param isCollectionEndpoint - Whether this is a collection endpoint
*/
revalidateInBackground(key: string, fetchFn: () => Promise<any>, options: CacheOptions, enableDifferential?: boolean, isCollectionEndpoint?: boolean): Promise<void>;
clearRevalidationQueue(): number;
getQueueStatus(): {
length: number;
executing: number;
processing: boolean;
maxLength: number;
perHost: Record<string, number>;
};
/**
* Extracts artifacts array from various cache data formats
* Supports: Direct array, OData v2/v4, IntegrationPackages, IntegrationRuntimeArtifacts
*
* @param data - The cache data to extract from
* @returns Array of artifacts or null if format not recognized
*/
private extractArtifactsArray;
/**
* Saves CachedData directly to Redis (internal helper for differential updates)
*
* @param key - The cache key
* @param cachedData - The complete CachedData object to save
*/
private saveCachedData;
/**
* Closes the Redis connection and removes all event listeners
*/
close(): Promise<void>;
/**
* Checks if the cache manager is ready to use
*/
isReady(): boolean;
/**
* Disables the cache manager
* Useful when connection fails and cache should be disabled
*/
disable(): void;
/**
* Finds cache keys matching a pattern using Redis SCAN
* Uses SCAN instead of KEYS for better performance in production
*
* @param pattern - The pattern to match (supports wildcards like *)
* @returns Array of matching cache keys
*
* @example
* const keys = await cacheManager.findKeysByPattern('sap:hostname:GET:/IntegrationRuntimeArtifacts*');
*/
findKeysByPattern(pattern: string): Promise<string[]>;
/**
* Updates a partial cache entry by applying an update function
* Reads the cache entry, applies the update function, and writes it back
*
* @param key - The cache key to update
* @param updateFn - Function that receives the cached data and returns updated data
* @returns true if update was successful, false otherwise
*
* @example
* await cacheManager.updatePartial('my-key', (cachedData) => {
* cachedData.data.status = 'updated';
* return cachedData;
* });
*/
updatePartial(key: string, updateFn: (cachedData: CachedData) => CachedData): Promise<boolean>;
/**
* Deletes a single cache entry by key
*
* @param key - The cache key to delete
* @returns true if the key was deleted, false if the key doesn't exist or an error occurred
*
* @example
* const deleted = await cacheManager.delete('sap:hostname:GET:/IntegrationRuntimeArtifacts(\'artifactId\')');
*/
delete(key: string): Promise<boolean>;
/**
* Deletes all cache entries matching a pattern
* Uses findKeysByPattern internally to find matching keys, then deletes them
*
* @param pattern - The pattern to match (supports wildcards like *)
* @returns The number of keys deleted
*
* @example
* const deletedCount = await cacheManager.deleteByPattern('sap:hostname:GET:/IntegrationRuntimeArtifacts*');
*/
deleteByPattern(pattern: string): Promise<number>;
/**
* Updates a single field in a cache entry using a dot-notation path
*
* @param key - The cache key to update
* @param fieldPath - The dot-notation path to the field (e.g. 'data.Status' or 'data.d.results[0].Status')
* @param value - The value to set
* @returns true if the update was successful, false otherwise
*
* @example
* await cacheManager.updateField('my-key', 'data.Status', 'STARTED');
*/
updateField(key: string, fieldPath: string, value: any): Promise<boolean>;
/**
* Updates multiple fields in a cache entry using dot-notation paths
*
* @param key - The cache key to update
* @param updates - Object mapping field paths to values
* @returns true if the update was successful, false otherwise
*
* @example
* await cacheManager.updateFields('my-key', {
* 'data.Status': 'STARTED',
* 'data.DeployedBy': 'user@example.com',
* 'data.DeployedOn': '2024-01-01T00:00:00Z'
* });
*/
updateFields(key: string, updates: Record<string, any>): Promise<boolean>;
/**
* Updates an item in an array within a cache entry
* Finds the item by ID and updates its fields
*
* @param key - The cache key to update
* @param arrayPath - The dot-notation path to the array (e.g. 'data.d.results' or 'data.IntegrationPackages')
* @param itemId - The ID of the item to update (matches 'Id' or 'id' property)
* @param updates - Object mapping field names to values to update
* @returns true if the update was successful, false otherwise
*
* @example
* await cacheManager.updateInArray('my-key', 'data.d.results', 'artifact1', {
* Status: 'STARTED',
* DeployedBy: 'user@example.com'
* });
*/
updateInArray(key: string, arrayPath: string, itemId: string, updates: Record<string, any>): Promise<boolean>;
/**
* Performs multiple cache updates in a single batch using Redis Pipeline
*
* **CRITICAL**: Uses Redis Pipeline for performance (50-100 updates = 1 roundtrip instead of 50-100)
*
* @param updates - Array of update operations, each containing a key and updates object
* @returns Object with success and failed counts
*
* @example
* const result = await cacheManager.batchUpdate([
* { key: 'key1', updates: { 'data.Status': 'STARTED' } },
* { key: 'key2', updates: { 'data.Status': 'STOPPED' } }
* ]);
* // result: { success: 2, failed: 0 }
*/
batchUpdate(updates: Array<{
key: string;
updates: Record<string, any>;
}>): Promise<{
success: number;
failed: number;
}>;
/**
* Deletes multiple cache keys in a single batch using Redis Pipeline
*
* **CRITICAL**: Uses Redis Pipeline for performance (50-100 deletes = 1 roundtrip instead of 50-100)
*
* @param keys - Array of cache keys to delete
* @returns The number of keys successfully deleted
*
* @example
* const deleted = await cacheManager.batchDelete(['key1', 'key2', 'key3']);
*/
batchDelete(keys: string[]): Promise<number>;
/**
* Gets cache statistics for admin/debug purposes
*
* **WARNING**: This API is expensive (uses MEMORY USAGE + SCAN) and should NOT be used in hot-path code.
* Primarily intended for admin/debug/monitoring purposes.
*
* @returns Cache statistics including total keys, size, TTL information, etc.
*
* @example
* const stats = await cacheManager.getStats();
* console.log(`Total keys: ${stats.totalKeys}, Total size: ${stats.totalSize} bytes`);
*/
getStats(): Promise<CacheAdminStats>;
/**
* Gets cache statistics for keys matching a specific pattern
*
* **WARNING**: This API is expensive (uses MEMORY USAGE + SCAN) and should not be used in hot-path code.
* Primarily intended for admin/debug/monitoring purposes.
*
* @param pattern - The pattern to match cache keys (e.g., 'sap:*:GET:/IntegrationPackages*')
* @returns Cache statistics for matching keys
*
* @example
* const stats = await cacheManager.getStatsByPattern('sap:*:GET:/IntegrationRuntimeArtifacts*');
* console.log(`Matching keys: ${stats.totalKeys}, Total size: ${stats.totalSize} bytes`);
*/
getStatsByPattern(pattern: string): Promise<CacheAdminStats>;
/**
* Gets the health status of the cache
*
* @returns Health status including connection status, total keys, and uptime
*
* @example
* const health = await cacheManager.getCacheHealth();
* console.log(`Cache is ${health.isHealthy ? 'healthy' : 'unhealthy'}, Uptime: ${health.uptime}s`);
*/
getCacheHealth(): Promise<{
isHealthy: boolean;
connectionStatus: 'connected' | 'disconnected' | 'error';
totalKeys: number;
errorRate?: number;
lastError?: string;
uptime: number;
}>;
/**
* Gets cache statistics grouped by endpoint
*
* **WARNING**: This API is expensive (uses MEMORY USAGE + SCAN) and should not be used in hot-path code.
* Primarily intended for admin/debug/monitoring purposes.
*
* @param endpoint - The endpoint path (e.g., '/IntegrationPackages' or '/IntegrationRuntimeArtifacts')
* @returns Aggregated statistics for the endpoint
*
* @example
* const stats = await cacheManager.getStatsByEndpoint('/IntegrationPackages');
* console.log(`Endpoint: ${stats.endpoint}, Keys: ${stats.keys}, Size: ${stats.totalSize} bytes`);
*/
getStatsByEndpoint(endpoint: string): Promise<{
endpoint: string;
keys: number;
totalSize: number;
averageTtl?: number;
}>;
/**
* Gets information about a specific cache key
*
* **WARNING**: This API uses MEMORY USAGE which is expensive and should NOT be used in hot-path code.
* Primarily intended for admin/debug purposes.
*
* @param key - The cache key to get information about
* @returns Information about the cache key, or null if the key doesn't exist
*
* @example
* const info = await cacheManager.getKeyInfo('sap:hostname:GET:/IntegrationRuntimeArtifacts');
* if (info) {
* console.log(`Key exists: ${info.exists}, TTL: ${info.ttl}s, Size: ${info.size} bytes`);
* }
*/
getKeyInfo(key: string): Promise<CacheKeyInfo | null>;
/**
* Adds an artifact to a cache entry
*
* @param key - The cache key to update
* @param artifact - The artifact to add
* @param options - Optional configuration
* @param options.arrayPath - Custom dot-notation path to the array (e.g. 'data.custom.path')
* @param options.preventDuplicates - If true, prevents adding duplicate artifacts
* @returns true if the artifact was added successfully, false otherwise
*
* @example
* const success = await cacheManager.addToCache('my-key', newArtifact, {
* preventDuplicates: true
* });
*/
addToCache(key: string, artifact: any, options?: {
arrayPath?: string;
preventDuplicates?: boolean;
}): Promise<boolean>;
/**
* Removes an artifact from a cache entry
*
* @param key - The cache key to update
* @param artifactId - The ID of the artifact to remove
* @param options - Optional configuration
* @param options.arrayPath - Custom dot-notation path to the array (e.g. 'data.custom.path')
* @returns true if the artifact was removed successfully, false otherwise
*
* @example
* const success = await cacheManager.removeFromCache('my-key', 'MyArtifactId');
*/
removeFromCache(key: string, artifactId: string, options?: {
arrayPath?: string;
}): Promise<boolean>;
/**
* Adds multiple artifacts to cache entries in a single batch using Redis Pipeline
*
* **CRITICAL**: Uses Redis Pipeline for performance (50-100 updates = 1 roundtrip instead of 50-100)
*
* @param updates - Array of artifact addition operations, each containing a key, artifact, and optional options
* @returns Object with success and failed counts
*
* @example
* const result = await cacheManager.batchAddToCache([
* { key: 'key1', artifact: { Id: 'Artifact1' }, options: { preventDuplicates: true } },
* { key: 'key2', artifact: { Id: 'Artifact2' } }
* ]);
* // result: { success: 2, failed: 0 }
*/
batchAddToCache(updates: Array<{
key: string;
artifact: any;
options?: {
arrayPath?: string;
preventDuplicates?: boolean;
};
}>): Promise<{
success: number;
failed: number;
}>;
/**
* Updates the TTL (Time-To-Live) of a cache entry
*
* @param key - The cache key to update
* @param newTTL - The new TTL in seconds
* @returns true if the TTL was updated successfully, false otherwise
*
* @example
* const success = await cacheManager.updateTTL('my-key', 3600); // Set TTL to 1 hour
*/
updateTTL(key: string, newTTL: number): Promise<boolean>;
/**
* Extends the TTL (Time-To-Live) of a cache entry by adding additional seconds
*
* @param key - The cache key to update
* @param additionalSeconds - The number of seconds to add to the current TTL
* @returns true if the TTL was extended successfully, false otherwise
*
* @example
* const success = await cacheManager.extendTTL('my-key', 1800); // Add 30 minutes
*/
extendTTL(key: string, additionalSeconds: number): Promise<boolean>;
/**
* Updates TTL for multiple cache keys in a single batch using Redis Pipeline
*
* **CRITICAL**: Uses Redis Pipeline for performance (50-100 updates = 1 roundtrip instead of 50-100)
*
* @param updates - Array of TTL update operations, each containing a key and new TTL
* @returns Object with success and failed counts
*
* @example
* const result = await cacheManager.batchUpdateTTL([
* { key: 'key1', ttl: 3600 },
* { key: 'key2', ttl: 7200 }
* ]);
* // result: { success: 2, failed: 0 }
*/
batchUpdateTTL(updates: Array<{
key: string;
ttl: number;
}>): Promise<{
success: number;
failed: number;
}>;
/**
* Warms the cache by pre-loading multiple endpoints
*
* @param endpoints - Array of endpoints to warm, each containing cache key, fetch function, and cache options
* @param config - Optional configuration for warming behavior
* @param config.timeout - Timeout per endpoint in milliseconds (default: 30000)
* @param config.parallel - Whether to execute endpoints in parallel (default: true)
* @returns Statistics about the warming operation (success count, failed count, duration, errors)
*
* @example
* const result = await cacheManager.warmCache([
* {
* key: 'sap:hostname:GET:/IntegrationPackages',
* fetchFn: () => api.getIntegrationPackages(),
* options: { ttl: 3600, revalidateAfter: 1800 }
* }
* ]);
*/
warmCache(endpoints: Array<{
key: string;
fetchFn: () => Promise<any>;
options?: CacheOptions;
}>, config?: {
timeout?: number;
parallel?: boolean;
}): Promise<{
success: number;
failed: number;
duration: number;
errors?: Array<{
key: string;
error: string;
}>;
}>;
/**
* Validates a single cache entry for consistency and validity
*
* @param key - The cache key to validate
* @param options - Optional validation options
* @param options.compareWithSap - Whether to compare cache data with SAP API (requires fetchFn)
* @param options.fetchFn - Function to fetch fresh data from SAP API for comparison
* @param options.autoRepair - Whether to automatically repair invalid entries
* @returns Validation result with validity status, issues, and optional repair status
*
* @example
* const result = await cacheManager.validateCacheEntry('my-key', {
* compareWithSap: true,
* fetchFn: () => api.getData(),
* autoRepair: true
* });
*/
validateCacheEntry(key: string, options?: {
compareWithSap?: boolean;
fetchFn?: () => Promise<any>;
autoRepair?: boolean;
}): Promise<{
isValid: boolean;
issues: string[];
repaired?: boolean;
comparisonResult?: {
matches: boolean;
differences?: string[];
};
}>;
/**
* Validates all cache entries matching a pattern
*
* @param pattern - The pattern to match cache keys (supports wildcards like *)
* @param options - Optional validation options
* @param options.autoRepair - Whether to automatically repair invalid entries
* @param options.compareWithSap - Whether to compare cache data with SAP API (requires fetchFn)
* @param options.fetchFn - Function to fetch fresh data from SAP API for comparison (receives key as parameter)
* @returns Validation statistics and issues per key
*
* @example
* const result = await cacheManager.validateCacheByPattern('sap:hostname:GET:/IntegrationPackages*', {
* autoRepair: true
* });
*/
validateCacheByPattern(pattern: string, options?: {
autoRepair?: boolean;
compareWithSap?: boolean;
fetchFn?: (key: string) => Promise<any>;
}): Promise<{
total: number;
valid: number;
invalid: number;
repaired: number;
issues: Array<{
key: string;
issues: string[];
}>;
}>;
/**
* Gets data from cache or fetches it using the provided function
* Implements Stale-While-Revalidate pattern:
* - If data is in cache and fresh: return immediately
* - If data is in cache but stale: return immediately, revalidate in background
* - If data is expired or not in cache: fetch fresh data
*
* @param key - The cache key
* @param fetchFn - Function to fetch fresh data
* @param options - Cache options (TTL and revalidation time)
* @param forceRefresh - Force fetching fresh data regardless of cache state
* @returns Object containing data and cache info
*
* @example
* const result = await cacheManager.getOrFetch(
* 'sap:hostname:GET:/IntegrationPackages',
* async () => await api.getPackages(),
* { ttl: 2592000, revalidateAfter: 300 },
* false
* );
* console.log(result.data); // The actual data
* console.log(result.cacheInfo); // { hit: true, age: 120, status: 'HIT' }
*/
getOrFetch<T>(key: string, fetchFn: () => Promise<T>, options: CacheOptions, forceRefresh?: boolean): Promise<{
data: T;
cacheInfo: {
hit: boolean;
age: number | null;
status: 'HIT' | 'HIT-STALE' | 'MISS' | 'DISABLED';
};
}>;
/**
* Returns health status of the cache manager and Redis connection
* Useful for monitoring and failover detection
*
* @returns Health status object
*/
getHealthStatus(): {
enabled: boolean;
connected: boolean;
reconnecting: boolean;
reconnectAttempts: number;
maxReconnectAttempts: number;
uptime: number;
connectedAt: string | null;
encryptionEnabled: boolean;
queueStatus: {
length: number;
executing: number;
processing: boolean;
maxLength: number;
perHost: Record<string, number>;
};
status: "reconnecting" | "healthy" | "degraded" | "offline";
};
/**
* Gets a human-readable connection status
*
* @private
* @returns Connection status string
*/
private _getConnectionStatus;
}