Public/State.ps1

<#
.SYNOPSIS
Sets an object within the shared state.
 
.DESCRIPTION
Sets an object within the shared state.
 
.PARAMETER Name
The name of the state object.
 
.PARAMETER Value
The value to set in the state.
 
.PARAMETER Scope
An optional Scope for the state object, used when saving the state.
 
.PARAMETER NoPassThru
If supplied, the value that was set will not be returned.
 
.EXAMPLE
$value = Set-PodeState -Name 'Data' -Value @{ 'Name' = 'Rick Sanchez' }
 
.EXAMPLE
$value = Set-PodeState -Name 'Users' -Value @('user1', 'user2') -Scope General, Users
 
.EXAMPLE
Set-PodeState -Name 'Data' -Value @{ 'Name' = 'Rick Sanchez' } -NoPassThru
#>

function Set-PodeState {
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(ValueFromPipeline = $true, Position = 0)]
        [object]
        $Value,

        [Parameter()]
        [string[]]
        $Scope,

        [switch]
        $NoPassThru
    )

    begin {
        if ($null -eq $PodeContext.Server.State) {
            # Pode has not been initialized
            throw ($PodeLocale.podeNotInitializedExceptionMessage)
        }

        if ($null -eq $Scope) {
            $Scope = @()
        }

        # Initialize an array to hold piped-in values
        $pipelineValue = @()
    }

    process {
        # Add the current piped-in value to the array
        $pipelineValue += $_
    }

    end {
        # Set Value to the array of values
        if ($pipelineValue.Count -gt 1) {
            $Value = $pipelineValue
        }

        # create state object
        $PodeContext.Server.State[$Name] = New-PodeStateDictionary -Data @{
            Value = $Value
            Scope = $Scope
        }

        # return the value that was set
        if (!$NoPassThru) {
            return $Value
        }
    }
}

<#
.SYNOPSIS
Retrieves some state object from the shared state.
 
.DESCRIPTION
Retrieves some state object from the shared state.
 
.PARAMETER Name
The name of the state object.
 
.PARAMETER WithScope
If supplied, the state's value and scope will be returned as a hashtable.
 
.EXAMPLE
$value = Get-PodeState -Name 'Data'
#>

function Get-PodeState {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [switch]
        $WithScope
    )

    if ($null -eq $PodeContext.Server.State) {
        # Pode has not been initialized
        throw ($PodeLocale.podeNotInitializedExceptionMessage)
    }

    if ($WithScope) {
        return $PodeContext.Server.State[$Name]
    }

    return $PodeContext.Server.State[$Name].Value
}

<#
.SYNOPSIS
Returns the current names of state variables.
 
.DESCRIPTION
Returns the current names of state variables that have been set. You can filter the result using Scope or a Pattern.
 
.PARAMETER Pattern
An optional regex Pattern to filter the state names.
 
.PARAMETER Scope
An optional Scope to filter the state names.
 
.PARAMETER Exclude
An optional array of state names to exclude from the result.
 
.PARAMETER Include
An optional array of state names to include in the result.
 
.EXAMPLE
$names = Get-PodeStateNames -Scope '<scope>'
 
.EXAMPLE
$names = Get-PodeStateNames -Pattern '^\w+[0-9]{0,2}$'
 
.EXAMPLE
$names = Get-PodeStateNames -Include 'Name1', 'Name2' -Exclude 'Name3'
#>

function Get-PodeStateNames {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Pattern,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter()]
        [string[]]
        $Exclude,

        [Parameter()]
        [string[]]
        $Include
    )

    if ($null -eq $PodeContext.Server.State) {
        # Pode has not been initialized
        throw ($PodeLocale.podeNotInitializedExceptionMessage)
    }

    if ($null -eq $Scope) {
        $Scope = @()
    }

    # get the keys from the state
    $keys = $PodeContext.Server.State.Keys

    # filter by scope if supplied
    if ($Scope.Length -gt 0) {
        $keys = @(foreach ($key in $keys) {
                $value = $null
                if ($PodeContext.Server.State.TryGetValue($key, [ref]$value)) {
                    foreach ($_scope in $value.Scope) {
                        if ($Scope -icontains $_scope) {
                            $key
                            break
                        }
                    }
                }
            })
    }

    # filter by include if supplied
    if ($Include.Length -gt 0) {
        $keys = @(foreach ($key in $keys) {
                if ($Include -inotcontains $key) {
                    continue
                }

                $key
            })
    }

    # filter by exclude if supplied
    if ($Exclude.Length -gt 0) {
        $keys = @(foreach ($key in $keys) {
                if ($Exclude -icontains $key) {
                    continue
                }

                $key
            })
    }

    # filter by pattern if supplied
    if (![string]::IsNullOrWhiteSpace($Pattern)) {
        $keys = @(foreach ($key in $keys) {
                if ($key -imatch $Pattern) {
                    $key
                }
            })
    }

    return $keys
}

<#
.SYNOPSIS
Removes some state object from the shared state.
 
.DESCRIPTION
Removes some state object from the shared state. After removal, the original object being stored is returned.
 
.PARAMETER Name
The name of the state object.
 
.PARAMETER NoPassThru
If supplied, the removed state object will not be returned.
 
.EXAMPLE
$value = Remove-PodeState -Name 'Data'
 
.EXAMPLE
Remove-PodeState -Name 'Data' -NoPassThru
#>

function Remove-PodeState {
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [switch]
        $NoPassThru
    )

    if ($null -eq $PodeContext.Server.State) {
        # Pode has not been initialized
        throw ($PodeLocale.podeNotInitializedExceptionMessage)
    }

    $item = $null
    if (!$PodeContext.Server.State.TryRemove($Name, [ref]$item)) {
        return $null
    }

    if (!$NoPassThru) {
        return $item.Value
    }
}

<#
.SYNOPSIS
Saves the current shared state to a supplied JSON file.
 
.DESCRIPTION
Saves the current shared state to a supplied JSON file. When using this function, it's recommended to wrap it in a Lock-PodeObject block.
 
.PARAMETER Path
The path to a JSON file which the current state will be saved to.
 
.PARAMETER Scope
An optional array of scopes for state objects that should be saved. (This has a lower precedence than Exclude/Include)
 
.PARAMETER Exclude
An optional array of state object names to exclude from being saved. (This has a higher precedence than Include)
 
.PARAMETER Include
An optional array of state object names to only include when being saved.
 
.PARAMETER Depth
Saved JSON maximum depth. Will be passed to ConvertTo-JSON's -Depth parameter. Default is 20.
 
.PARAMETER Compress
If supplied, the saved JSON will be compressed.
 
.EXAMPLE
Save-PodeState -Path './state.json'
 
.EXAMPLE
Save-PodeState -Path './state.json' -Exclude Name1, Name2
 
.EXAMPLE
Save-PodeState -Path './state.json' -Scope Users
#>

function Save-PodeState {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string[]]
        $Scope,

        [Parameter()]
        [string[]]
        $Exclude,

        [Parameter()]
        [string[]]
        $Include,

        [Parameter()]
        [int16]
        $Depth = 20,

        [switch]
        $Compress
    )

    # error if attempting to use outside of the pode server
    if ($null -eq $PodeContext.Server.State) {
        # Pode has not been initialized
        throw ($PodeLocale.podeNotInitializedExceptionMessage)
    }

    # get the full path to save the state
    $Path = Get-PodeRelativePath -Path $Path -JoinRoot

    # which keys in the state do we need to save?
    $keys = Get-PodeStateNames -Scope $Scope -Exclude $Exclude -Include $Include

    # create a clone of the state, using only the keys we want to save
    $state = New-PodeStateDictionary
    foreach ($key in $keys) {
        $value = $null
        if ($PodeContext.Server.State.TryGetValue($key, [ref]$value)) {
            $state[$key] = $value
        }
    }

    # save the state
    $result = @{
        '__pode_state_metadata__' = @{
            Version   = 2
            Timestamp = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
        }
        '__pode_state_data__'     = ConvertTo-PodeStateData -InputObject $state
    }

    $null = $result |
        ConvertTo-Json -Depth $Depth -Compress:$Compress |
        Out-File -FilePath $Path -Force

    $state = $null
}

<#
.SYNOPSIS
Restores the shared state from some JSON file.
 
.DESCRIPTION
Restores the shared state from some JSON file.
 
.PARAMETER Path
The path to a JSON file that contains the state information.
 
.PARAMETER Merge
If supplied, the state loaded from the JSON file will be merged with the current state, instead of overwriting it.
 
.PARAMETER Depth
Saved JSON maximum depth. Will be passed to ConvertFrom-JSON's -Depth parameter (Powershell >=6). Default is 20.
 
.EXAMPLE
Restore-PodeState -Path './state.json'
#>

function Restore-PodeState {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [switch]
        $Merge,

        [int16]
        $Depth = 20
    )

    # error if attempting to use outside of the pode server
    if ($null -eq $PodeContext.Server.State) {
        # Pode has not been initialized
        throw ($PodeLocale.podeNotInitializedExceptionMessage)
    }

    # get the full path to the state
    $Path = Get-PodeRelativePath -Path $Path -JoinRoot
    if (!(Test-Path $Path)) {
        return
    }

    # load the json state from file
    $params = @{}
    if (Test-PodeIsPSCore) {
        $params['AsHashtable'] = $true
        $params['Depth'] = $Depth
    }

    $content = Get-Content -Path $Path -Force | ConvertFrom-Json @params
    if ([string]::IsNullOrEmpty($content)) {
        return
    }

    # convert into a concurrent dictionary based on the version of the state file
    $state = ConvertFrom-PodeStateJson -InputObject $content

    # check for no scopes, and add for backwards compat
    foreach ($_key in $state.Keys) {
        if ($null -eq $state[$_key].Scope) {
            $state[$_key] = @{
                Value = $state[$_key]
                Scope = @()
            }
        }
    }

    # set the scope to the main context
    if ($Merge) {
        foreach ($_key in $state.Keys) {
            $PodeContext.Server.State[$_key] = $state[$_key]
        }
    }
    else {
        $PodeContext.Server.State = $state
    }
}

<#
.SYNOPSIS
Tests if the shared state contains some state object.
 
.DESCRIPTION
Tests if the shared state contains some state object.
 
.PARAMETER Name
The name of the state object.
 
.EXAMPLE
Test-PodeState -Name 'Data'
#>

function Test-PodeState {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )

    if ($null -eq $PodeContext.Server.State) {
        # Pode has not been initialized
        throw ($PodeLocale.podeNotInitializedExceptionMessage)
    }

    return $PodeContext.Server.State.ContainsKey($Name)
}

<#
.SYNOPSIS
Creates a new concurrent dictionary for use in the shared state.
 
.DESCRIPTION
Creates a new concurrent dictionary for use in the shared state. This is useful for storing key-value pairs in a thread-safe manner.
 
.PARAMETER Data
An optional hashtable of initial key-value pairs to populate the dictionary.
 
.EXAMPLE
Set-PodeState -Name 'MyDictionary' -Value (New-PodeStateDictionary)
 
.EXAMPLE
Set-PodeState -Name 'MyDictionary' -Value (New-PodeStateDictionary -Data @{ 'Key1' = 'Value1'; 'Key2' = 'Value2' })
#>

function New-PodeStateDictionary {
    [CmdletBinding()]
    [OutputType([System.Collections.Concurrent.ConcurrentDictionary[string, object]])]
    param(
        [Parameter()]
        [hashtable]
        $Data
    )

    $dict = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([StringComparer]::InvariantCultureIgnoreCase)
    if (($null -eq $Data) -or ($Data.Count -eq 0)) {
        return $dict
    }

    foreach ($key in $Data.Keys) {
        $dict[$key] = $Data[$key]
    }

    return $dict
}

<#
.SYNOPSIS
Creates a new concurrent ordered dictionary for use in the shared state.
 
.DESCRIPTION
Creates a new concurrent ordered dictionary for use in the shared state. This is useful for storing key-value pairs in a thread-safe manner while maintaining the order of insertion.
 
.PARAMETER Data
An optional hashtable of initial key-value pairs to populate the ordered dictionary.
 
.EXAMPLE
Set-PodeState -Name 'MyOrderedDictionary' -Value (New-PodeStateOrderedDictionary)
 
.EXAMPLE
Set-PodeState -Name 'MyOrderedDictionary' -Value (New-PodeStateOrderedDictionary -Data @{ 'Key1' = 'Value1'; 'Key2' = 'Value2' })
 
.NOTES
This uses a custom concurrent ordered dictionary implementation to ensure thread safety and maintain order of insertion.
The PodeConcurrentOrderedDictionary class is defined in the Pode.Utilities.Structures namespace.
#>

function New-PodeStateOrderedDictionary {
    [CmdletBinding()]
    [OutputType([Pode.Utilities.Structures.PodeConcurrentOrderedDictionary[string, object]])]
    [OutputType([System.Object[]])]
    param(
        [Parameter()]
        [hashtable]
        $Data
    )

    $dict = [Pode.Utilities.Structures.PodeConcurrentOrderedDictionary[string, object]]::new()
    if (($null -eq $Data) -or ($Data.Count -eq 0)) {
        return , $dict
    }

    foreach ($key in $Data.Keys) {
        $dict[$key] = $Data[$key]
    }

    return , $dict
}

<#
.SYNOPSIS
Creates a new concurrent bag for use in the shared state.
 
.DESCRIPTION
Creates a new concurrent bag for use in the shared state. This is useful for storing a collection of objects in a thread-safe manner.
 
.PARAMETER Data
An optional array of initial objects to populate the bag.
 
.EXAMPLE
Set-PodeState -Name 'MyBag' -Value (New-PodeStateBag)
 
.EXAMPLE
Set-PodeState -Name 'MyBag' -Value (New-PodeStateBag -Data @(1, 2, 3))
#>

function New-PodeStateBag {
    [CmdletBinding()]
    [OutputType([System.Collections.Concurrent.ConcurrentBag[object]])]
    [OutputType([System.Object[]])]
    param(
        [Parameter()]
        [object[]]
        $Data
    )

    $bag = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
    if (($null -eq $Data) -or ($Data.Count -eq 0)) {
        return , $bag
    }

    foreach ($item in $Data) {
        $null = $bag.Add($item)
    }

    return , $bag
}

<#
.SYNOPSIS
Creates a new concurrent hash set for use in the shared state.
 
.DESCRIPTION
Creates a new concurrent hash set for use in the shared state. This is useful for storing a collection of unique objects in a thread-safe manner.
 
.PARAMETER Data
An optional array of initial objects to populate the hash set.
 
.EXAMPLE
Set-PodeState -Name 'MySet' -Value (New-PodeStateSet)
 
.EXAMPLE
Set-PodeState -Name 'MySet' -Value (New-PodeStateSet -Data @('Item1', 'Item2', 'Item3'))
 
.NOTES
This uses a custom concurrent set implementation to ensure thread safety and uniqueness of items.
The PodeConcurrentSet class is defined in the Pode.Utilities.Structures namespace.
#>

function New-PodeStateSet {
    [CmdletBinding()]
    [OutputType([Pode.Utilities.Structures.PodeConcurrentSet[object]])]
    [OutputType([System.Object[]])]
    param(
        [Parameter()]
        [object[]]
        $Data
    )

    $set = [Pode.Utilities.Structures.PodeConcurrentSet[object]]::new()
    if (($null -eq $Data) -or ($Data.Count -eq 0)) {
        return , $set
    }

    foreach ($item in $Data) {
        $null = $set.TryAdd($item)
    }

    return , $set
}

<#
.SYNOPSIS
Creates a new concurrent list for use in the shared state.
 
.DESCRIPTION
Creates a new concurrent list for use in the shared state. This is useful for storing a collection of objects in a thread-safe manner while maintaining the order of insertion.
 
.PARAMETER Data
An optional array of initial objects to populate the list.
 
.EXAMPLE
Set-PodeState -Name 'MyList' -Value (New-PodeStateList)
 
.EXAMPLE
Set-PodeState -Name 'MyList' -Value (New-PodeStateList -Data @('Item1', 'Item2'))
 
.NOTES
This uses a custom concurrent list implementation to ensure thread safety and maintain order of insertion.
The PodeConcurrentList class is defined in the Pode.Utilities.Structures namespace.
#>

function New-PodeStateList {
    [CmdletBinding()]
    [OutputType([Pode.Utilities.Structures.PodeConcurrentList[object]])]
    [OutputType([System.Object[]])]
    param(
        [Parameter()]
        [object[]]
        $Data
    )

    $list = [Pode.Utilities.Structures.PodeConcurrentList[object]]::new()
    if (($null -eq $Data) -or ($Data.Count -eq 0)) {
        return , $list
    }

    foreach ($item in $Data) {
        $null = $list.Add($item)
    }

    return , $list
}