Public/Get-ExpressionCache.ps1
<#
.SYNOPSIS Gets a cached value if present; otherwise computes it via a ScriptBlock and caches the result. .DESCRIPTION Get-ExpressionCache provides a provider-agnostic "get or create" operation. When a cache entry is missing or stale, the supplied ScriptBlock is invoked with -Arguments, its result is returned, and the provider stores it. Keys are either supplied via -Key or generated deterministically from the ScriptBlock and Arguments. Provider selection: - Uses -ProviderName if supplied; otherwise ENV var EXPRESSIONCACHE_PROVIDERNAME; otherwise 'LocalFileSystemCache'. Freshness: - Use -MaxAge for “stale if older than…” logic (TTL-like). - Use -ExpireAtUtc for an absolute expiration time (some providers translate to TTL). .PARAMETER Key Optional cache key. If omitted, a stable key is generated from the ScriptBlock and Arguments. Avoid putting secrets in keys; prefer opaque, stable identifiers. .PARAMETER ScriptBlock The computation to run on a cache miss. Invoked with the supplied -Arguments. .PARAMETER Arguments Arguments passed positionally to the ScriptBlock. .PARAMETER ProviderName Provider to use (e.g., 'LocalFileSystemCache', 'Redis'). .PARAMETER MaxAge A TimeSpan; cached values older than this are treated as stale and recomputed. .PARAMETER ExpireAtUtc An absolute UTC DateTime at which the entry should expire. .OUTPUTS System.Object (whatever the ScriptBlock returns) .EXAMPLE # GitHub API (cache repo metadata for 10 minutes) # Requires a GitHub token in $env:GITHUB_TOKEN. GitHub requires a User-Agent header. $owner = 'PowerShell' $repo = 'PowerShell' $url = "https://api.github.com/repos/$owner/$repo" $header = @{ 'User-Agent' = 'ExpressionCache' 'Authorization' = "Bearer $($env:GITHUB_TOKEN)" } Get-ExpressionCache ` -Key "github:repos/$owner/$repo" ` -MaxAge (New-TimeSpan -Minutes 10) ` -ScriptBlock { param($u, $h) Invoke-RestMethod -Uri $u -Headers $h -Method GET } ` -Arguments $url, $header .EXAMPLE # Jira REST API (cache an issue for 15 minutes) # Assumes $env:JIRA_EMAIL and $env:JIRA_API_TOKEN are set (Atlassian Cloud). $base = 'https://yourcompany.atlassian.net' $issue = 'PROJ-123' $basic = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$($env:JIRA_EMAIL):$($env:JIRA_API_TOKEN)")) $header = @{ Authorization = "Basic $basic" } Get-ExpressionCache ` -Key "jira:issue:$issue" ` -MaxAge (New-TimeSpan -Minutes 15) ` -ScriptBlock { param($b, $i, $h) Invoke-RestMethod -Uri "$b/rest/api/3/issue/$i" -Headers $h -Method GET } ` -Arguments $base, $issue, $header .EXAMPLE # Daily refresh using an absolute expiration (expires at next midnight UTC) $tomorrowUtcMidnight = [DateTime]::UtcNow.Date.AddDays(1) Get-ExpressionCache -Key 'daily:report' -ExpireAtUtc $tomorrowUtcMidnight -ScriptBlock { # ...expensive computation... } .EXAMPLE # Use Redis provider explicitly for shared cache (password via env) Get-ExpressionCache ` -ProviderName 'Redis' ` -Key 'feature-flags' ` -MaxAge (New-TimeSpan -Minutes 5) ` -ScriptBlock { Invoke-RestMethod 'https://api.example.com/flags' -Headers @{ Authorization = "Bearer $env:API_TOKEN" } } .NOTES - Do not embed secrets in cache keys. Pass tokens/headers via -Arguments or environment variables. - Null results are not cached; if you need to cache “not found” results, wrap them in a sentinel object. - When designing keys, include all inputs that affect the result (e.g., URL path + query, tenant, locale). .LINK New-ExpressionCacheKey Initialize-ExpressionCache Get-ExpressionCacheProvider about_CommonParameters #> function Get-ExpressionCache { [CmdletBinding(DefaultParameterSetName = 'ByMaxAge')] param( [string]$ProviderName = 'LocalFileSystemCache', [Parameter(Mandatory)] [scriptblock]$ScriptBlock, [object[]]$Arguments, [string]$Key, # A) Keep for this long (default) [Parameter(ParameterSetName = 'ByMaxAge')] [TimeSpan]$MaxAge, # B) Expire at this absolute time (UTC or local; normalized inside) [Parameter(ParameterSetName = 'ByAbsolute')] [DateTime]$ExpireAtUtc, # C) Sliding expiration: renew TTL on reads [Parameter(ParameterSetName = 'BySliding')] [TimeSpan]$SlidingAge ) begin { if (-not $script:RegisteredStorageProviders) { throw "Module not initialized. Call Initialize-ExpressionCache first." } $strategy = $script:RegisteredStorageProviders | Where-Object { $_.Name -eq $ProviderName } | Select-Object -First 1 if (-not $strategy) { throw "Provider '$ProviderName' not registered." } # Lazy init once per invocation if (-not $strategy.State.Initialized -and $strategy.Initialize) { $initSplat = Build-CallableSplat -CommandName $strategy.Initialize ` -Config $strategy.Config ` -PreferArgs ` -Log -LogPrefix 'Init' Assert-MandatoryParamsPresent -CommandName $strategy.Initialize -Splat $initSplat & $strategy.Initialize @initSplat Write-Verbose "[Get-ExpressionCache] Provider '$ProviderName' initialized." } $providerFunc = $strategy.GetOrCreate if (-not (Get-Command $providerFunc -ErrorAction SilentlyContinue)) { throw "Provider function '$providerFunc' not found." } $cacheVersion = if ($strategy.Config.PSObject.Properties.Name -contains 'CacheVersion' -and $strategy.Config.CacheVersion) { $strategy.Config.CacheVersion } else { $script:Config.Version } } process { # Auto-generate a stable key if not provided $keyToUse = if ($Key) { $Key } else { New-ExpressionCacheKey -ScriptBlock $ScriptBlock -Arguments $Arguments } $defaultPolicy = $strategy.Config.DefaultPolicy $defaultMaxAge = $strategy.Config.DefaultMaxAge $policy = Resolve-CachePolicy -MaxAge $MaxAge -ExpireAtUtc $ExpireAtUtc -SlidingAge $SlidingAge -DefaultPolicy $defaultPolicy -DefaultMaxAge $defaultMaxAge # Base args common to all providers $runtimeArgs = @{ ProviderName = $ProviderName Key = $keyToUse ScriptBlock = $ScriptBlock Arguments = $Arguments CacheVersion = $cacheVersion Policy = $policy } # Provider-specific extras (e.g., LocalFS needs CacheFolder) if ($strategy.Config.PSObject.Properties.Name -contains 'CacheFolder' -and $strategy.Config.CacheFolder) { $runtimeArgs.CacheFolder = $strategy.Config.CacheFolder } $splat = Build-CallableSplat -CommandName $providerFunc ` -Config $strategy.Config ` -Arguments $runtimeArgs ` -PreferArgs ` -Log -LogPrefix 'Provider' Write-Verbose "[Get-ExpressionCache] Invoking provider '$providerFunc' with $($splat.Keys.Count) params. Key='$keyToUse' Mode=$($policy.Mode) TTL=$($policy.TtlSeconds)s" & $providerFunc @splat } end { } } |