Private/HeadersAndRateLimit.ps1

function ConvertFrom-DattoApiHeaders {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Net.Http.HttpResponseMessage]$Response
    )

    $headers = [ordered]@{}
    foreach ($header in $Response.Headers) {
        $headers[[string]$header.Key] = (@($header.Value) -join ', ')
    }
    if ($null -ne $Response.Content) {
        foreach ($header in $Response.Content.Headers) {
            $headers[[string]$header.Key] = (@($header.Value) -join ', ')
        }
    }
    return $headers
}

function Get-DattoApiHeaderValue {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Collections.IDictionary]$Headers,

        [Parameter(Mandatory)]
        [string]$Name
    )

    foreach ($key in $Headers.Keys) {
        if ([string]$key -ieq $Name) {
            return [string]$Headers[$key]
        }
    }
    return $null
}

function ConvertFrom-DattoApiEpochSecondsInternal {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [long]$Seconds
    )
    return ([datetimeoffset]'1970-01-01T00:00:00+00:00').AddSeconds($Seconds)
}

function Update-DattoApiRateLimit {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Collections.IDictionary]$ConnectionState,

        [Parameter(Mandatory)]
        [System.Collections.IDictionary]$Headers
    )

    $remainingText = Get-DattoApiHeaderValue -Headers $Headers -Name 'X-API-Limit-Remaining'
    $resetText = Get-DattoApiHeaderValue -Headers $Headers -Name 'X-API-Limit-Resets'
    $costText = Get-DattoApiHeaderValue -Headers $Headers -Name 'X-API-Limit-Cost'

    $remaining = 0L
    $resetEpoch = 0L
    $cost = 0L
    $hasAny = $false

    if (-not [string]::IsNullOrWhiteSpace($remainingText) -and [long]::TryParse($remainingText, [ref]$remaining)) {
        $ConnectionState.RateLimit.Remaining = $remaining
        $hasAny = $true
    }
    if (-not [string]::IsNullOrWhiteSpace($resetText) -and [long]::TryParse($resetText, [ref]$resetEpoch)) {
        $ConnectionState.RateLimit.ResetEpoch = $resetEpoch
        try {
            $ConnectionState.RateLimit.ResetAtUtc = ConvertFrom-DattoApiEpochSecondsInternal -Seconds $resetEpoch
        }
        catch {
            $ConnectionState.RateLimit.ResetAtUtc = $null
        }
        $hasAny = $true
    }
    if (-not [string]::IsNullOrWhiteSpace($costText) -and [long]::TryParse($costText, [ref]$cost)) {
        $ConnectionState.RateLimit.Cost = $cost
        $hasAny = $true
    }
    if ($hasAny) {
        $ConnectionState.RateLimit.ObservedAtUtc = [datetimeoffset]::UtcNow
    }
}

function Get-DattoApiRetryDelayMilliseconds {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [int]$Attempt,

        [Parameter(Mandatory)]
        [System.Collections.IDictionary]$Headers,

        [Parameter(Mandatory)]
        [int]$MaximumDelaySeconds,

        [Parameter()]
        [switch]$UseRateLimitReset
    )

    $delaySeconds = $null
    $retryAfter = Get-DattoApiHeaderValue -Headers $Headers -Name 'Retry-After'
    $seconds = 0
    if (-not [string]::IsNullOrWhiteSpace($retryAfter)) {
        if ([int]::TryParse($retryAfter, [ref]$seconds)) {
            $delaySeconds = [double]$seconds
        }
        else {
            $retryDate = [datetimeoffset]::MinValue
            $dateStyles = [Globalization.DateTimeStyles]::AssumeUniversal -bor [Globalization.DateTimeStyles]::AdjustToUniversal
            if ([datetimeoffset]::TryParse($retryAfter, [Globalization.CultureInfo]::InvariantCulture, $dateStyles, [ref]$retryDate)) {
                $delaySeconds = [Math]::Max(0, ($retryDate - [datetimeoffset]::UtcNow).TotalSeconds)
            }
        }
    }

    if ($null -eq $delaySeconds -and $UseRateLimitReset) {
        $resetText = Get-DattoApiHeaderValue -Headers $Headers -Name 'X-API-Limit-Resets'
        $resetEpoch = 0L
        if (-not [string]::IsNullOrWhiteSpace($resetText) -and [long]::TryParse($resetText, [ref]$resetEpoch)) {
            try {
                $resetDate = ConvertFrom-DattoApiEpochSecondsInternal -Seconds $resetEpoch
                $delaySeconds = [Math]::Max(0, ($resetDate - [datetimeoffset]::UtcNow).TotalSeconds)
            }
            catch {
                $delaySeconds = $null
            }
        }
    }

    if ($null -eq $delaySeconds) {
        $jitterMilliseconds = Get-Random -Minimum 0 -Maximum 500
        $delaySeconds = [Math]::Pow(2, [Math]::Max(0, $Attempt - 1)) + ($jitterMilliseconds / 1000.0)
    }

    $delaySeconds = [Math]::Min([double]$MaximumDelaySeconds, [Math]::Max(0, [double]$delaySeconds))
    return [int][Math]::Round($delaySeconds * 1000)
}