modules/Core/ErrorHandling.psm1
|
# Error Handling Framework Module # Provides centralized error handling and custom exception types for Spotify CLI live features using namespace System.Management.Automation # Custom exception types for Spotify CLI live features class SpotifyLiveFeatureException : System.Exception { [string] $ErrorCode [hashtable] $Context SpotifyLiveFeatureException([string]$message) : base($message) { $this.ErrorCode = "GENERAL_ERROR" $this.Context = @{} } SpotifyLiveFeatureException([string]$message, [string]$errorCode) : base($message) { $this.ErrorCode = $errorCode $this.Context = @{} } SpotifyLiveFeatureException([string]$message, [string]$errorCode, [hashtable]$context) : base($message) { $this.ErrorCode = $errorCode $this.Context = $context } SpotifyLiveFeatureException([string]$message, [System.Exception]$innerException) : base($message, $innerException) { $this.ErrorCode = "GENERAL_ERROR" $this.Context = @{} } } # Live Display specific exceptions class LiveDisplayException : SpotifyLiveFeatureException { LiveDisplayException([string]$message) : base($message, "LIVE_DISPLAY_ERROR") {} LiveDisplayException([string]$message, [hashtable]$context) : base($message, "LIVE_DISPLAY_ERROR", $context) {} } class DisplayEngineNotSupportedException : LiveDisplayException { DisplayEngineNotSupportedException([string]$engineType) : base("Display engine not supported: $engineType") { $this.ErrorCode = "DISPLAY_ENGINE_NOT_SUPPORTED" } } class TerminalCapabilityException : LiveDisplayException { TerminalCapabilityException([string]$capability) : base("Terminal capability not available: $capability") { $this.ErrorCode = "TERMINAL_CAPABILITY_MISSING" } } # Lyrics specific exceptions class LyricsException : SpotifyLiveFeatureException { LyricsException([string]$message) : base($message, "LYRICS_ERROR") {} LyricsException([string]$message, [hashtable]$context) : base($message, "LYRICS_ERROR", $context) {} } class LyricsProviderException : LyricsException { [string] $ProviderName LyricsProviderException([string]$providerName, [string]$message) : base("Lyrics provider '$providerName' error: $message") { $this.ProviderName = $providerName $this.ErrorCode = "LYRICS_PROVIDER_ERROR" } } class LyricsCacheException : LyricsException { LyricsCacheException([string]$message) : base("Lyrics cache error: $message") { $this.ErrorCode = "LYRICS_CACHE_ERROR" } } # Statistics specific exceptions class StatisticsException : SpotifyLiveFeatureException { StatisticsException([string]$message) : base($message, "STATISTICS_ERROR") {} StatisticsException([string]$message, [hashtable]$context) : base($message, "STATISTICS_ERROR", $context) {} } class DataCollectionException : StatisticsException { DataCollectionException([string]$message) : base("Data collection error: $message") { $this.ErrorCode = "DATA_COLLECTION_ERROR" } } class AnalyticsProcessingException : StatisticsException { AnalyticsProcessingException([string]$message) : base("Analytics processing error: $message") { $this.ErrorCode = "ANALYTICS_PROCESSING_ERROR" } } # API Client specific exceptions class ApiClientException : SpotifyLiveFeatureException { [int] $StatusCode [string] $ResponseBody ApiClientException([string]$message) : base($message, "API_CLIENT_ERROR") { $this.StatusCode = 0 $this.ResponseBody = "" } ApiClientException([string]$message, [int]$statusCode) : base($message, "API_CLIENT_ERROR") { $this.StatusCode = $statusCode $this.ResponseBody = "" } ApiClientException([string]$message, [int]$statusCode, [string]$responseBody) : base($message, "API_CLIENT_ERROR") { $this.StatusCode = $statusCode $this.ResponseBody = $responseBody } } class RateLimitException : ApiClientException { [int] $RetryAfterSeconds RateLimitException([int]$retryAfter) : base("Rate limit exceeded", 429) { $this.RetryAfterSeconds = $retryAfter $this.ErrorCode = "RATE_LIMIT_EXCEEDED" } } class AuthenticationException : ApiClientException { AuthenticationException([string]$message) : base($message, 401) { $this.ErrorCode = "AUTHENTICATION_ERROR" } } # Configuration specific exceptions class ConfigurationException : SpotifyLiveFeatureException { ConfigurationException([string]$message) : base($message, "CONFIGURATION_ERROR") {} } class ConfigurationValidationException : ConfigurationException { [string[]] $ValidationErrors ConfigurationValidationException([string[]]$errors) : base("Configuration validation failed") { $this.ValidationErrors = $errors $this.ErrorCode = "CONFIGURATION_VALIDATION_ERROR" } } # Graceful degradation manager (must be defined before ErrorHandler) class GracefulDegradationManager { [hashtable] $CachedDataSources = @{} [hashtable] $FeatureStates = @{} [bool] $OfflineMode = $false [DateTime] $LastSuccessfulApiCall = [DateTime]::MinValue [int] $ConsecutiveFailures = 0 [int] $MaxConsecutiveFailures = 5 GracefulDegradationManager() { $this.InitializeFeatureStates() } [void] InitializeFeatureStates() { $this.FeatureStates = @{ "LiveDisplay" = @{ Enabled = $true; LastWorking = [DateTime]::UtcNow } "LyricsDisplay" = @{ Enabled = $true; LastWorking = [DateTime]::UtcNow } "Statistics" = @{ Enabled = $true; LastWorking = [DateTime]::UtcNow } "RealTimeUpdates" = @{ Enabled = $true; LastWorking = [DateTime]::UtcNow } } } [void] RegisterCachedDataSource([string]$sourceType, [object]$dataSource) { $this.CachedDataSources[$sourceType] = $dataSource } [object] GetCachedData([string]$sourceType, [string]$key) { if ($this.CachedDataSources.ContainsKey($sourceType)) { $source = $this.CachedDataSources[$sourceType] # Try to get cached data based on source type switch ($sourceType) { "ApiClient" { # Check if source has GetCachedData method (avoid circular dependency) if ($source -and ($source | Get-Member -Name "GetCachedData" -MemberType Method)) { return $source.GetCachedData($key) } } "LyricsCache" { return $source.GetCachedLyrics($key) } "StatisticsCache" { return $source.GetCachedStatistics($key) } } } return $null } [void] RecordApiSuccess() { $this.LastSuccessfulApiCall = [DateTime]::UtcNow $this.ConsecutiveFailures = 0 if ($this.OfflineMode) { $this.OfflineMode = $false Write-Host "🌐 Connection restored. Returning to online mode." -ForegroundColor Green } } [void] RecordApiFailure() { $this.ConsecutiveFailures++ if ($this.ConsecutiveFailures -ge $this.MaxConsecutiveFailures -and -not $this.OfflineMode) { $this.OfflineMode = $true Write-Host "📴 Multiple API failures detected. Switching to offline mode with cached data." -ForegroundColor Yellow } } [bool] ShouldUseCachedData() { return $this.OfflineMode -or $this.ConsecutiveFailures -gt 2 -or ([DateTime]::UtcNow - $this.LastSuccessfulApiCall).TotalMinutes -gt 10 } [void] DisableFeature([string]$featureName, [string]$reason) { if ($this.FeatureStates.ContainsKey($featureName)) { $this.FeatureStates[$featureName].Enabled = $false $this.FeatureStates[$featureName].DisabledReason = $reason $this.FeatureStates[$featureName].DisabledAt = [DateTime]::UtcNow Write-Warning "Feature '$featureName' disabled: $reason" } } [void] EnableFeature([string]$featureName) { if ($this.FeatureStates.ContainsKey($featureName)) { $this.FeatureStates[$featureName].Enabled = $true $this.FeatureStates[$featureName].LastWorking = [DateTime]::UtcNow if ($this.FeatureStates[$featureName].ContainsKey('DisabledReason')) { $this.FeatureStates[$featureName].Remove('DisabledReason') $this.FeatureStates[$featureName].Remove('DisabledAt') } Write-Host "✅ Feature '$featureName' re-enabled." -ForegroundColor Green } } [bool] IsFeatureEnabled([string]$featureName) { if ($this.FeatureStates.ContainsKey($featureName)) { return $this.FeatureStates[$featureName].Enabled } return $true } [hashtable] GetDegradationStatus() { return @{ OfflineMode = $this.OfflineMode ConsecutiveFailures = $this.ConsecutiveFailures LastSuccessfulApiCall = $this.LastSuccessfulApiCall FeatureStates = $this.FeatureStates.Clone() ShouldUseCachedData = $this.ShouldUseCachedData() } } [object] HandleApiFailure([System.Exception]$exception, [string]$operation, [hashtable]$fallbackOptions = @{}) { $this.RecordApiFailure() # Try to get cached data if available if ($fallbackOptions.ContainsKey('CacheKey') -and $fallbackOptions.ContainsKey('CacheSource')) { $cachedData = $this.GetCachedData($fallbackOptions.CacheSource, $fallbackOptions.CacheKey) if ($cachedData -ne $null) { Write-Host "📋 Using cached data for $operation" -ForegroundColor Cyan return $cachedData } } # Disable real-time features if in offline mode if ($this.OfflineMode) { $this.DisableFeature("RealTimeUpdates", "API unavailable") } # Return fallback data if provided if ($fallbackOptions.ContainsKey('FallbackData')) { return $fallbackOptions.FallbackData } # Re-throw the exception if no fallback is available throw $exception } } # Error handler class for centralized error management class ErrorHandler { [hashtable] $ErrorStrategies [bool] $LoggingEnabled = $true [string] $LogLevel = "Error" [GracefulDegradationManager] $DegradationManager ErrorHandler() { $this.InitializeErrorStrategies() $this.DegradationManager = [GracefulDegradationManager]::new() } [void] InitializeErrorStrategies() { $this.ErrorStrategies = @{ "RATE_LIMIT_EXCEEDED" = @{ Action = "Retry" RetryDelay = 60000 MaxRetries = 3 UserMessage = "Rate limit exceeded. Waiting before retry..." } "AUTHENTICATION_ERROR" = @{ Action = "Reauth" UserMessage = "Authentication required. Please re-authenticate with Spotify." SuggestedAction = "Run authentication command" } "API_CLIENT_ERROR" = @{ Action = "Retry" RetryDelay = 2000 MaxRetries = 2 UserMessage = "API request failed. Retrying..." FallbackToCachedData = $true } "DISPLAY_ENGINE_NOT_SUPPORTED" = @{ Action = "Fallback" UserMessage = "Display engine not supported. Using fallback display mode." FallbackMode = "console" } "TERMINAL_CAPABILITY_MISSING" = @{ Action = "Fallback" UserMessage = "Terminal capability missing. Using simplified display." FallbackMode = "simple" } "LYRICS_PROVIDER_ERROR" = @{ Action = "Fallback" UserMessage = "Lyrics provider failed. Trying alternative provider..." FallbackAction = "NextProvider" } "LYRICS_CACHE_ERROR" = @{ Action = "Continue" UserMessage = "Lyrics cache error. Fetching lyrics directly..." SkipCache = $true } "DATA_COLLECTION_ERROR" = @{ Action = "Continue" UserMessage = "Data collection failed. Statistics may be incomplete." DisableFeature = "DataCollection" } "CONFIGURATION_ERROR" = @{ Action = "UseDefaults" UserMessage = "Configuration error. Using default settings." ResetToDefaults = $true } "GENERAL_ERROR" = @{ Action = "Log" UserMessage = "An unexpected error occurred." ShowDetails = $false FallbackToCachedData = $true } "NETWORK_ERROR" = @{ Action = "Retry" RetryDelay = 5000 MaxRetries = 3 UserMessage = "Network connection failed. Retrying..." FallbackToCachedData = $true ExponentialBackoff = $true } "SERVICE_UNAVAILABLE" = @{ Action = "Fallback" UserMessage = "Spotify service is temporarily unavailable. Using cached data..." FallbackToCachedData = $true DisableRealTimeFeatures = $true } } } [hashtable] HandleError([System.Exception]$exception, [hashtable]$operationContext = @{}) { $errorCode = "GENERAL_ERROR" $context = @{} # Extract error code and context from custom exceptions if ($exception -is [SpotifyLiveFeatureException]) { $errorCode = $exception.ErrorCode $context = $exception.Context } # Determine error code from exception type and message $errorCode = $this.DetermineErrorCode($exception, $errorCode) # Get error strategy $strategy = $this.GetErrorStrategy($errorCode) # Handle graceful degradation if applicable $fallbackData = $null if ($strategy.ContainsKey('FallbackToCachedData') -and $strategy.FallbackToCachedData) { try { $fallbackData = $this.DegradationManager.HandleApiFailure($exception, $operationContext.Operation, $operationContext) } catch { # Fallback failed, continue with normal error handling } } # Log the error if ($this.LoggingEnabled) { $this.LogError($exception, $errorCode, $context) } # Create response $response = @{ ErrorCode = $errorCode Exception = $exception Strategy = $strategy Context = $context OperationContext = $operationContext FallbackData = $fallbackData Timestamp = [DateTime]::UtcNow DegradationStatus = $this.DegradationManager.GetDegradationStatus() } return $response } [string] DetermineErrorCode([System.Exception]$exception, [string]$defaultCode) { $message = $exception.Message.ToLower() # Network-related errors if ($message -like "*network*" -or $message -like "*connection*" -or $message -like "*timeout*") { return "NETWORK_ERROR" } # Service unavailable if ($message -like "*service unavailable*" -or $message -like "*503*" -or $message -like "*502*" -or $message -like "*504*") { return "SERVICE_UNAVAILABLE" } # Rate limiting if ($message -like "*rate limit*" -or $message -like "*429*" -or $message -like "*too many requests*") { return "RATE_LIMIT_EXCEEDED" } # Authentication if ($message -like "*unauthorized*" -or $message -like "*401*" -or $message -like "*authentication*") { return "AUTHENTICATION_ERROR" } return $defaultCode } [hashtable] GetErrorStrategy([string]$errorCode) { if ($this.ErrorStrategies.ContainsKey($errorCode)) { return $this.ErrorStrategies[$errorCode].Clone() } return $this.ErrorStrategies["GENERAL_ERROR"].Clone() } [void] LogError([System.Exception]$exception, [string]$errorCode, [hashtable]$context) { $logEntry = @{ Timestamp = [DateTime]::UtcNow ErrorCode = $errorCode Message = $exception.Message StackTrace = $exception.StackTrace Context = $context } # In a real implementation, this would write to a log file # For now, we'll use Write-Warning for visibility if ($this.LogLevel -eq "Error" -or $this.LogLevel -eq "Debug") { Write-Warning "[$errorCode] $($exception.Message)" if ($this.LogLevel -eq "Debug" -and $context.Count -gt 0) { Write-Warning "Context: $($context | ConvertTo-Json -Compress)" } } } [void] ShowUserFriendlyError([hashtable]$errorResponse) { $strategy = $errorResponse.Strategy $exception = $errorResponse.Exception if ($strategy.ContainsKey('UserMessage')) { Write-Host $strategy.UserMessage -ForegroundColor Yellow } if ($strategy.ContainsKey('SuggestedAction')) { Write-Host "💡 Suggestion: $($strategy.SuggestedAction)" -ForegroundColor Cyan } # Show technical details in debug mode if ($this.LogLevel -eq "Debug") { Write-Host "Technical details: $($exception.Message)" -ForegroundColor Gray } } [bool] ShouldRetry([hashtable]$errorResponse, [int]$currentAttempt) { $strategy = $errorResponse.Strategy if ($strategy.Action -ne "Retry") { return $false } $maxRetries = $strategy.ContainsKey('MaxRetries') ? $strategy.MaxRetries : 1 return $currentAttempt -lt $maxRetries } [int] GetRetryDelay([hashtable]$errorResponse, [int]$attempt) { $strategy = $errorResponse.Strategy $baseDelay = $strategy.ContainsKey('RetryDelay') ? $strategy.RetryDelay : 1000 # Exponential backoff return $baseDelay * [Math]::Pow(2, $attempt - 1) } [void] SetLoggingLevel([string]$level) { if ($level -in @("Error", "Warning", "Info", "Debug")) { $this.LogLevel = $level } } [void] EnableLogging([bool]$enabled) { $this.LoggingEnabled = $enabled } [void] AddCustomErrorStrategy([string]$errorCode, [hashtable]$strategy) { $this.ErrorStrategies[$errorCode] = $strategy } [void] RegisterCachedDataSource([string]$sourceType, [object]$dataSource) { $this.DegradationManager.RegisterCachedDataSource($sourceType, $dataSource) } [void] RecordApiSuccess() { $this.DegradationManager.RecordApiSuccess() } [bool] IsOfflineMode() { return $this.DegradationManager.OfflineMode } [bool] ShouldUseCachedData() { return $this.DegradationManager.ShouldUseCachedData() } [bool] IsFeatureEnabled([string]$featureName) { return $this.DegradationManager.IsFeatureEnabled($featureName) } [hashtable] GetDegradationStatus() { return $this.DegradationManager.GetDegradationStatus() } [hashtable] GetErrorStatistics() { # In a real implementation, this would return statistics from logged errors return @{ TotalErrors = 0 ErrorsByCode = @{} MostCommonError = "None" LastError = $null DegradationStatus = $this.GetDegradationStatus() } } } # Retry helper class class RetryHelper { static [object] ExecuteWithRetry([scriptblock]$operation, [ErrorHandler]$errorHandler, [int]$maxAttempts = 3, [hashtable]$operationContext = @{}) { $attempt = 1 $lastErrorResponse = $null while ($attempt -le $maxAttempts) { try { $result = $operation.Invoke() # Record success if we had previous failures if ($attempt -gt 1) { $errorHandler.RecordApiSuccess() } return $result } catch { $lastErrorResponse = $errorHandler.HandleError($_, $operationContext) # Check if we have fallback data if ($lastErrorResponse.FallbackData -ne $null) { Write-Verbose "Using fallback data for operation" return $lastErrorResponse.FallbackData } if ($errorHandler.ShouldRetry($lastErrorResponse, $attempt) -and $attempt -lt $maxAttempts) { $delay = $errorHandler.GetRetryDelay($lastErrorResponse, $attempt) # Apply exponential backoff if specified in strategy if ($lastErrorResponse.Strategy.ContainsKey('ExponentialBackoff') -and $lastErrorResponse.Strategy.ExponentialBackoff) { $delay = $delay * [Math]::Pow(2, $attempt - 1) } Write-Verbose "Retrying operation in $delay ms (attempt $($attempt + 1)/$maxAttempts)" Start-Sleep -Milliseconds $delay $attempt++ } else { $errorHandler.ShowUserFriendlyError($lastErrorResponse) # Check if we should return fallback data instead of throwing if ($lastErrorResponse.Strategy.ContainsKey('FallbackToCachedData') -and $lastErrorResponse.Strategy.FallbackToCachedData) { # Try one more time to get cached data try { $fallbackData = $errorHandler.DegradationManager.GetCachedData($operationContext.CacheSource, $operationContext.CacheKey) if ($fallbackData -ne $null) { Write-Host "📋 Returning cached data as final fallback" -ForegroundColor Yellow return $fallbackData } } catch { # Ignore fallback errors } } throw } } } # Final attempt to use cached data before giving up if ($lastErrorResponse -and $lastErrorResponse.Strategy.ContainsKey('FallbackToCachedData') -and $lastErrorResponse.Strategy.FallbackToCachedData) { try { $fallbackData = $errorHandler.DegradationManager.GetCachedData($operationContext.CacheSource, $operationContext.CacheKey) if ($fallbackData -ne $null) { Write-Host "📋 Maximum retries exceeded. Using cached data." -ForegroundColor Yellow return $fallbackData } } catch { # Ignore fallback errors } } throw [System.InvalidOperationException]::new("Maximum retry attempts exceeded and no fallback data available") } static [object] ExecuteWithGracefulDegradation([scriptblock]$operation, [ErrorHandler]$errorHandler, [hashtable]$fallbackOptions) { try { $result = $operation.Invoke() $errorHandler.RecordApiSuccess() return $result } catch { return $errorHandler.DegradationManager.HandleApiFailure($_, $fallbackOptions.Operation, $fallbackOptions) } } } # Error context builder class ErrorContextBuilder { [hashtable] $Context = @{} [ErrorContextBuilder] AddTrackInfo([hashtable]$trackData) { if ($trackData) { $this.Context["TrackId"] = $trackData.id $this.Context["TrackName"] = $trackData.name $this.Context["ArtistName"] = ($trackData.artists | ForEach-Object { $_.name }) -join ", " } return $this } [ErrorContextBuilder] AddApiInfo([string]$endpoint, [string]$method, [int]$statusCode) { $this.Context["ApiEndpoint"] = $endpoint $this.Context["HttpMethod"] = $method $this.Context["StatusCode"] = $statusCode return $this } [ErrorContextBuilder] AddUserAction([string]$action, [hashtable]$parameters) { $this.Context["UserAction"] = $action $this.Context["ActionParameters"] = $parameters return $this } [ErrorContextBuilder] AddSystemInfo([string]$component, [string]$version) { $this.Context["Component"] = $component $this.Context["Version"] = $version $this.Context["PowerShellVersion"] = (Get-Variable PSVersionTable -ValueOnly).PSVersion.ToString() $this.Context["OperatingSystem"] = [System.Environment]::OSVersion.ToString() return $this } [ErrorContextBuilder] AddCustomData([string]$key, $value) { $this.Context[$key] = $value return $this } [hashtable] Build() { return $this.Context.Clone() } [void] Clear() { $this.Context.Clear() } } # Global error handler instance $script:GlobalErrorHandler = [ErrorHandler]::new() # Helper functions for easy error handling function New-SpotifyLiveFeatureException { param( [Parameter(Mandatory)] [string]$Message, [string]$ErrorCode = "GENERAL_ERROR", [hashtable]$Context = @{}, [System.Exception]$InnerException ) if ($InnerException) { return [SpotifyLiveFeatureException]::new($Message, $InnerException) } else { return [SpotifyLiveFeatureException]::new($Message, $ErrorCode, $Context) } } function Invoke-WithErrorHandling { param( [Parameter(Mandatory)] [scriptblock]$ScriptBlock, [ErrorHandler]$ErrorHandler = $script:GlobalErrorHandler, [int]$MaxRetries = 1, [hashtable]$OperationContext = @{}, [switch]$ShowUserFriendlyErrors, [switch]$UseGracefulDegradation ) try { if ($UseGracefulDegradation -and $OperationContext.Count -gt 0) { return [RetryHelper]::ExecuteWithGracefulDegradation($ScriptBlock, $ErrorHandler, $OperationContext) } elseif ($MaxRetries -gt 1) { return [RetryHelper]::ExecuteWithRetry($ScriptBlock, $ErrorHandler, $MaxRetries, $OperationContext) } else { $result = $ScriptBlock.Invoke() $ErrorHandler.RecordApiSuccess() return $result } } catch { $errorResponse = $ErrorHandler.HandleError($_, $OperationContext) if ($ShowUserFriendlyErrors) { $ErrorHandler.ShowUserFriendlyError($errorResponse) } # Return fallback data if available if ($errorResponse.FallbackData -ne $null) { return $errorResponse.FallbackData } throw } } function Get-ErrorContextBuilder { return [ErrorContextBuilder]::new() } function Get-GlobalErrorHandler { return $script:GlobalErrorHandler } function Set-ErrorHandlerLogging { param( [bool]$Enabled = $true, [ValidateSet("Error", "Warning", "Info", "Debug")] [string]$Level = "Error" ) $script:GlobalErrorHandler.EnableLogging($Enabled) $script:GlobalErrorHandler.SetLoggingLevel($Level) } function Register-CachedDataSource { param( [Parameter(Mandatory)] [string]$SourceType, [Parameter(Mandatory)] [object]$DataSource, [ErrorHandler]$ErrorHandler = $script:GlobalErrorHandler ) $ErrorHandler.RegisterCachedDataSource($SourceType, $DataSource) } function Test-OfflineMode { param( [ErrorHandler]$ErrorHandler = $script:GlobalErrorHandler ) return $ErrorHandler.IsOfflineMode() } function Test-FeatureEnabled { param( [Parameter(Mandatory)] [string]$FeatureName, [ErrorHandler]$ErrorHandler = $script:GlobalErrorHandler ) return $ErrorHandler.IsFeatureEnabled($FeatureName) } function Get-DegradationStatus { param( [ErrorHandler]$ErrorHandler = $script:GlobalErrorHandler ) return $ErrorHandler.GetDegradationStatus() } function Invoke-WithGracefulDegradation { param( [Parameter(Mandatory)] [scriptblock]$ScriptBlock, [Parameter(Mandatory)] [hashtable]$FallbackOptions, [ErrorHandler]$ErrorHandler = $script:GlobalErrorHandler ) return [RetryHelper]::ExecuteWithGracefulDegradation($ScriptBlock, $ErrorHandler, $FallbackOptions) } # Export functions and classes Export-ModuleMember -Function @( 'New-SpotifyLiveFeatureException', 'Invoke-WithErrorHandling', 'Get-ErrorContextBuilder', 'Get-GlobalErrorHandler', 'Set-ErrorHandlerLogging', 'Register-CachedDataSource', 'Test-OfflineMode', 'Test-FeatureEnabled', 'Get-DegradationStatus', 'Invoke-WithGracefulDegradation' ) -Variable @() |