CacheMeIfYouCan.psm1
using namespace System.Collections.Generic using namespace System.Collections using namespace System.Management.Automation.Language using namespace System.Management.Automation using namespace System.Management # ModuleConfig # ExportCoercionFunctions $script:DefaultTemplateTypeMapping = @{} $script:ModuleConfig = @{ ExportAggressiveNames = $false VerboseLifetimeMessages = $true ExportDebugFunctions = $false VerboseJson_ArgCompletions = $false PrintExtraSummaryOnTabCompletion = $false WarnWhen_GetValueCommand_KeyDoesNotExist = $true } $script:Cache = @{} if($Script:ModuleConfig.VerboseJson_ArgCompletions) { (Join-Path (gi 'temp:\') 'CacheMeIfYouCan.ArgCompletions.log') | Join-String -op 'CacheMeIfYouCan: VerboseLogging for ArgCompletions is enabled at: ' | write-warning } $script:Color = @{ DimYellow = '#dcdcaa' DimOrange = '#ce8d70' DimPurple = '#c586c0' } function Cache._Read { <# .SYNOPSIS Internal helper, that encapsulates reading keys Why the name? I wrote it during an 8 hour car ride. #> # [Alias('Cache._Read')] [OutputType( [CachedRecord] )] [CmdletBinding()] param( [Parameter(Position=0, Mandatory)] [Alias('Name')] [ArgumentCompleter( [CachedKeyNameArgumentCompleter] )] [string]$KeyName, [switch]$ErrorWhenMissing ) [bool]$exists? = $script:Cache.ContainsKey( $KeyName ) if( $ErrorWhenMissing -and -not $exists? ) { if($script:ModuleConfig.WarnWhen_GetValueCommand_KeyDoesNotExist) { $PSCmdlet.MyInvocation.ScriptName | Join-String -op 'Cmdlet.MyInvo.Script: ' | Write-Debug throw ( $KeyName | Join-String -f 'CacheMe::_Read: Tried reading from a key that does not exist! Key = "{0}"') } else { $KeyName | Join-String -f 'CacheMe::_Read: Tried reading from a key that does not exist! Key = "{0}"' | Write-Debug } } return $script:Cache[ $KeyName ] } # class CachedKeyNameArgumentCompleter : IArgum class CachedKeyNameArgumentCompleter : System.Management.Automation.IArgumentCompleter { [IEnumerable[CompletionResult]] CompleteArgument( [string] $CommandName, [string] $ParameterName, [string] $WordToComplete, [CommandAst] $CommandAst, [IDictionary] $FakeBoundParameters ) { [List[CompletionResult]]$Completions = @() if($Script:ModuleConfig.VerboseJson_ArgCompletions) { $Script:Cache | ConvertTo-Json -wa 'ignore' -depth 3 | set-content -path (Join-Path (gi 'temp:\') 'CacheMeIfYouCan.ArgCompletions.log') } $script:Cache.GetEnumerator() | %{ $Item = $_.Value $toCompleteAs = $Item.Name $Completions.Add( [CompletionResult]::new( $toCompleteAs, $toCompleteAs, 'ParameterValue', $Item.Tooltip() ) ) } if( $script:ModuleConfig.PrintExtraSummaryOnTabCompletion) { "`n" | write-host $Completions | format-table | out-string | write-host #-bg $Script:Color.DimPurple "`n" | write-host } return $Completions } } 'CachedKeyNameArgumentCompleter : not quite working, wip' | Write-host -bg 'darkorange' function Cache._Write { <# .SYNOPSIS Internal helper, that encapsulates writing keys #> # [Alias('Cache._Write')] [CmdletBinding()] param( # [ValidateNotNullOrWhiteSpace()] [ArgumentCompleter( { [CachedKeyNameArgumentCompleter] } )] # [ValidateNotNullOrEmpty()] [Parameter(Mandatory, Position=0)] [string]$KeyName, # [ValidateNotNullOrWhiteSpace()] # [ValidateNotNullOrEmpty()] # [Parameter(Mandatory, ValueFromPipeline )] # maybe this should allow caching in circumstances [Parameter(Mandatory, Position = 1 )] [CachedRecord]$CachedRecord ) if(-not $PSBoundParameters.ContainsKey('CachedRecord')){ throw 'MissingArgException' } if( $CachedRecord -isnot [CachedRecord] ) { throw 'ShouldNeverReachInvalidParameterTypeException' } $script:Cache[ $KeyName ] = $Record 'wrote: {0}, calcDuration: {1}' -f @( $KeyName $CachedRecord.CalculationDuration ) | New-Text -fg $script:Colors.DimOrange | write-information -infa 'continue' } class CachedRecord { # Todo: make me [string]$Name # created by [scriptblock]$ScriptBlock [object]$Value [datetime]$EvaluatedAt [timespan]$CalculationDuration [hashtable]$Metadata [timespan]Age () { return [datetime]::Now - $This.EvaluatedAt } [string] ToString() { return '[CacheRecord := Name: {0}, Age: {1} ]' -f @( $This.Name $This.Age().TotalSeconds | Join-String -f '{0:n2}' ) } [string] Tooltip() { # used for custom rendering for tooltips return $This.ToString() } } function CacheMe.ColorTest { $script:Color.GetEnumerator() | %{ $_.Key | New-Text -fg $_.Value | Join-String }| Join-String -sep "`n" } function CacheMe.Clear { [Alias('Clear-CacheMe')] param() 'clearing all cached values...' | write-host -fg 'orange' $script:Cache.Clear() } function Get-CacheMeRecords { <# .SYNOPSIS enumerates the raw [CachedRecord[]] records #> [OutputType( [CachedRecord] )] [Alias()] param() # ( $script:Cache.Keys ).count $script:Cache.GetEnumerator() | %{ $_.Value } } function CacheMe.ListKeys { <# .SYNOPSIS Returns key names as untouched strings. Writes optional colors to to Write-Information .DESCRIPTION You'll .EXAMPLE Pwsh7 🐒 > $names = CacheMe.ListKeys > $Names -join ', ' All_Modules, gcm_less .EXAMPLE Pwsh7 🐒 > $names = CacheMe.ListKeys -infa Continue | Join.UL keys => All_Modules, gcm_less - All_Modules - gcm_less .EXAMPLE Pwsh7 🐒 > CacheMe.ListKeys | Join-String -sep ', ' -SingleQuote 'All_Modules', 'gcm_less' .EXAMPLE Pwsh7 🐒> foreach($x in (CacheMe.ListKeys)) { "DoStuff: $x" } #> [Alias( 'Get-CacheMe.Keys', 'Get-CacheMeKeys' )] [CmdletBinding()] [OutputType('String[]')] param() $PSStyle.OutputRendering = 'ansi' Cache._Read -KeyName foo|ft -AutoSize | Out-String | Write-Information [string[]]$Keys = @($script:Cache).Keys?.Clone() | Sort-Object -Unique $Keys | join-String -sep ', ' -p { $_ | New-Text -fg 'gray70' -bg 'gray40' } | Join-String -op 'keys => ' | Write-Information return $Keys } function CacheMe.Add-CachedValue { <# .SYNOPSIS # currently 'Value' in the alias is redundant # whether it's Get or Set is based on whether -value Argument is included #> [Alias( # currently 'Value' in the alias is redundant # whether it's Get or Set is based on whether -value Argument is included # 'Add-CacheMeValue', 'CacheMe.Value', # 'Add-CacheMeValue', 'Add-CacheMe', # 'Get-CacheMeValue', 'Get-CacheMe', # 'Set-CacheMeValue', 'Set-CacheMe' # 'Set-CacheMeValue', # 'Set-CacheMeValue', # 'Cache.Value', # 'Cached' )] <# .SYNOPSIS minimal, fast, caching of random results - todo refresh: can I re-calc using scriptblock that's s aved? .EXAMPLE Cache.Value -Name 'gmo-noargs' -ScriptBlock { get-module } -Verbose -Force | % Value| Out-Null .EXAMPLE Cache.Value -Name <tab>'gmo-noargs' # returns value .EXAMPLE $x = Namo.CachedValue -Name 'gmo-list' { get-module -ListAvailable } -Verbose # totalTime: 0.02 ms #> [CmdletBinding()] param( [Alias('Label')] # [ArgumentCompleter( { [CachedKeyNameArgumentCompleter()] })] [ArgumentCompleter( [CachedKeyNameArgumentCompleter] )] [string]$Name, [ValidateScript({throw 'nyi'})] # -PassThru returns the raw [CacheRecord], instead of the value [switch]$PassThru, [Alias('Expression', 'SB', 'E')] [scriptblock]$ScriptBlock, # force calculation [Alias('Rebuild')] [switch]$Force, # move to another cmdlet when exporting module [switch]$FullReset ) if($FullReset) { $script:Cache.Clear() } if( [string]::IsNullOrWhiteSpace($ScriptBlock)) { return (Cache._Read -KeyName $Name -Verbose ) } [datetime]$Start = [datetime]::Now [datetime]$End = $Start if($Force -or ($script:Cache.count -eq 0) -or (-not $script:Cache.ContainsKey($Name))){ try { # if a parameter binding exception is thrown, this isn't caught here $newValue = & $ScriptBlock $end = [datetime]::Now $record = [CachedRecord]@{ CalculationDuration = $End - $Start EvaluatedAt = $Start Name = $Name ScriptBlock = $ScriptBlock Value = $newValue Metadata = @{ SourceCommand = $PSCommandPath SourcePSScriptRoot = $PSScriptRoot MyCmdName = $MyInvocation.MyCommand.Name MyModule = $MyInvocation.MyCommand.ModuleName MyScriptLine = $MyInvocation.ScriptLineNumber MyScriptName = $MyInvocation.ScriptName PathString = $MyInvocation | Join-String -p { $_.ScriptName, $_.ScriptLineNumber -join ':' } } } Cache._Write -KeyName $Record.Name -CachedRecord $record -verb # $script:Cache[ $Name ] = $Record # $newValue } catch { $PSCommandPath 'Namo.CachedValue( {0} ) invoked by {1}' -f @( $Name $PSCommandPath ) | write-verbose $_ | write-warning return $Null } } $delta = $end - $start $delta | join-String -p TotalMilliseconds -f 'total {0:n} ms' $delta | Join-String -p TotalMilliseconds -f 'total {0:n2} ms' | New-Text -fg $script:Color.DimPurple | Write-Information -infa 'Continue' # return @( $script:Cache[ $Name ] )?.Value return (Cache._Read -KeyName $Name -Verbose ) } Export-ModuleMember -Function @( '*-CacheMe*' 'CacheMe.*' if( $ModuleConfig.ExportDebugFunctions ) { # 'WarnIf.NotType' 'Cache*_*' } if ($ModuleConfig.ExportAggressiveNames) { 'Cache.*' 'Cached*' } ) -Alias @( '*-CacheMe*' 'CacheMe.*' if ($ModuleConfig.ExportAggressiveNames) { 'Cached*' 'Cache.*' } if( $ModuleConfig.ExportDebugFunctions ) { # 'WarnIf.NotType' 'Cache*_*' } ) -Variable @( # '*CacheMe*' # 'CacheMe.*' if ($ModuleConfig.ExportAggressiveNames) { # 'Cache.*' # 'Cached*' } ) if($ModuleConfig.ExportDebugFunctions) { 'CacheMeIfYouCan::ExportDebugFunctions is enabled' | Write-warning } if($ModuleConfig.ExportAggressiveNames) { $ModuleConfig.ExportAggressiveNames | Join-String -op 'CacheMeIfYouCan::ModuleConfig.ExportAggressiveNames: ' | write-warning } |