Utilities/Resolve-CachePolicy.ps1
class CachePolicy { [string] $Mode # 'MaxAge' | 'Absolute' | 'Sliding' [int] $TtlSeconds [datetime] $ExpireAtUtc # UTC [bool] $Sliding } <# .SYNOPSIS Resolves an effective cache policy from caller options or provider defaults. .DESCRIPTION Resolve-CachePolicy determines the cache expiration policy to use, applying a clear precedence and sane minimums. It returns a [CachePolicy] object with: - Mode : 'MaxAge' | 'Absolute' | 'Sliding' - TtlSeconds : integer TTL (minimum 1 second) - ExpireAtUtc : absolute expiration in UTC - Sliding : $true only for sliding policies Precedence (first present wins): 1) -MaxAge → TTL-based (“stale if older than…”) 2) -ExpireAtUtc → absolute expiry at a specific UTC moment 3) -SlidingAge → sliding TTL (renews on access; providers may refresh ExpireAtUtc) If none of the above are provided: 4) -DefaultPolicy (used as-is) 5) -DefaultMaxAge (converted to MaxAge) 6) Fallback = 5 minutes (MaxAge) Null semantics (when a parameter is *present* but $null): - -MaxAge $null → treat as TTL = 1 second (minimal nonzero) - -ExpireAtUtc $null → expire ~now + 1 second - -SlidingAge $null → TTL = 1 second, Sliding = $true Safety: - TTLs ≤ 0 are coerced to 1 second. - ExpireAtUtc is normalized to UTC; past times become TTL = 1 second. .PARAMETER MaxAge Optional TimeSpan for TTL-style freshness. If present, overrides all other options. When $null, a minimal TTL of 1 second is used. .PARAMETER ExpireAtUtc Optional DateTime (interpreted/converted to UTC) for absolute expiration. If present, used unless -MaxAge is also present. If passed as $null, expires ~now + 1 second. .PARAMETER SlidingAge Optional TimeSpan for sliding expiration (renews on access). If present, used unless -MaxAge or -ExpireAtUtc is present. When $null, TTL = 1 second, Sliding = $true. .PARAMETER DefaultPolicy Optional precomputed [CachePolicy] to use when caller did not specify MaxAge/ExpireAtUtc/SlidingAge. .PARAMETER DefaultMaxAge Optional TimeSpan used as a fallback TTL when no caller choice and no DefaultPolicy are supplied. .OUTPUTS CachePolicy .EXAMPLE # Caller chooses MaxAge (10 minutes). Overrides everything else. Resolve-CachePolicy -MaxAge (New-TimeSpan -Minutes 10) .EXAMPLE # Absolute expiration at midnight UTC tonight. $midnight = [DateTime]::UtcNow.Date.AddDays(1) Resolve-CachePolicy -ExpireAtUtc $midnight .EXAMPLE # Sliding expiration (2 minutes). Providers should refresh on access. Resolve-CachePolicy -SlidingAge (New-TimeSpan -Minutes 2) .EXAMPLE # Nothing specified by caller → use provider's default policy as-is. $default = [CachePolicy]@{ Mode='MaxAge'; TtlSeconds=300; ExpireAtUtc=[DateTime]::UtcNow.AddMinutes(5); Sliding=$false } Resolve-CachePolicy -DefaultPolicy $default .EXAMPLE # No caller choice, no DefaultPolicy → use DefaultMaxAge (1 hour). Resolve-CachePolicy -DefaultMaxAge (New-TimeSpan -Hours 1) .EXAMPLE # Edge case: present but null → minimal nonzero TTL (1 second). Resolve-CachePolicy -MaxAge $null .NOTES - Providers can use TtlSeconds directly (e.g., as Redis TTL) and may treat Sliding=$true to refresh expiry on access. ExpireAtUtc is computed for convenience; providers may prefer TtlSeconds. - All DateTimes are normalized to UTC. #> function Resolve-CachePolicy { [CmdletBinding()] [OutputType([CachePolicy])] param( # Caller-specified (optional). Keep them nullable so callers can omit them without prompts. [Nullable[TimeSpan]] $MaxAge, [Nullable[datetime]] $ExpireAtUtc, [Nullable[TimeSpan]] $SlidingAge, # Provider defaults (optional) [CachePolicy] $DefaultPolicy, [Nullable[TimeSpan]] $DefaultMaxAge ) $nowUtc = (Get-Date).ToUniversalTime() # 1) Precedence: MaxAge > ExpireAtUtc > SlidingAge if ($PSBoundParameters.ContainsKey('MaxAge')) { $ttl = 1 if ($null -ne $MaxAge) { $ttl = [int][Math]::Ceiling([double]$MaxAge.Value.TotalSeconds) if ($ttl -le 0) { $ttl = 1 } } return [CachePolicy]@{ Mode = 'MaxAge' TtlSeconds = $ttl ExpireAtUtc = $nowUtc.AddSeconds($ttl) Sliding = $false } } if ($PSBoundParameters.ContainsKey('ExpireAtUtc')) { $abs = if ($null -ne $ExpireAtUtc) { $ExpireAtUtc.Value.ToUniversalTime() } else { $nowUtc.AddSeconds(1) } $ttl = [int][Math]::Ceiling(([double]($abs - $nowUtc).TotalSeconds)) if ($ttl -le 0) { $ttl = 1 } return [CachePolicy]@{ Mode = 'Absolute' TtlSeconds = $ttl ExpireAtUtc = $abs Sliding = $false } } if ($PSBoundParameters.ContainsKey('SlidingAge')) { $ttl = 1 if ($null -ne $SlidingAge) { $ttl = [int][Math]::Ceiling([double]$SlidingAge.Value.TotalSeconds) if ($ttl -le 0) { $ttl = 1 } } return [CachePolicy]@{ Mode = 'Sliding' TtlSeconds = $ttl ExpireAtUtc = $nowUtc.AddSeconds($ttl) Sliding = $true } } # 2) No caller choice → provider defaults if ($null -ne $DefaultPolicy) { return $DefaultPolicy } if ($PSBoundParameters.ContainsKey('DefaultMaxAge') -and $null -ne $DefaultMaxAge) { $ttl = [int][Math]::Ceiling([double]$DefaultMaxAge.Value.TotalSeconds) if ($ttl -le 0) { $ttl = 1 } return [CachePolicy]@{ Mode = 'MaxAge' TtlSeconds = $ttl ExpireAtUtc = $nowUtc.AddSeconds($ttl) Sliding = $false } } # 3) Library-wide fallback (5 minutes) $fallback = [TimeSpan]::FromMinutes(5) $ttl2 = [int][Math]::Ceiling([double]$fallback.TotalSeconds) [CachePolicy]@{ Mode = 'MaxAge' TtlSeconds = $ttl2 ExpireAtUtc = $nowUtc.AddSeconds($ttl2) Sliding = $false } } |