Private/Invoke-WithColorScriptCacheEntryLock.ps1

function Invoke-WithColorScriptCacheEntryLock {
    <#
    .SYNOPSIS
        Serializes cache mutations for one colorscript across processes.
 
    .DESCRIPTION
        New-ColorScriptCache can be started concurrently from profiles, terminals, or explicit
        warm-up commands. A named mutex prevents competing writers from corrupting or deleting
        the same cache entry while still allowing different scripts to build in parallel.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$CacheRoot,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ScriptName,

        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [scriptblock]$Operation,

        [Parameter()]
        [ValidateRange(1, 3600)]
        [int]$TimeoutSeconds = 600,

        [Parameter()]
        [object[]]$ArgumentList = @()
    )

    $cachePath = Join-Path -Path $CacheRoot -ChildPath ("{0}.cache" -f $ScriptName)
    try {
        $cachePath = [System.IO.Path]::GetFullPath($cachePath)
    }
    catch {
        Write-Verbose ("Unable to normalize cache lock path '{0}': {1}" -f $cachePath, $_.Exception.Message)
    }

    if ([System.IO.Path]::DirectorySeparatorChar -eq '\') {
        $cachePath = $cachePath.ToUpperInvariant()
    }

    $hashAlgorithm = [System.Security.Cryptography.SHA256]::Create()
    try {
        $identityBytes = [System.Text.Encoding]::UTF8.GetBytes($cachePath)
        $identityHash = $hashAlgorithm.ComputeHash($identityBytes)
        $mutexSuffix = ([System.BitConverter]::ToString($identityHash)).Replace('-', '')
    }
    finally {
        $hashAlgorithm.Dispose()
    }

    $mutexName = 'ColorScriptsEnhanced.CacheEntry.{0}' -f $mutexSuffix
    $mutex = [System.Threading.Mutex]::new($false, $mutexName)
    $lockTaken = $false

    try {
        try {
            $lockTaken = $mutex.WaitOne([TimeSpan]::FromSeconds($TimeoutSeconds))
        }
        catch [System.Threading.AbandonedMutexException] {
            # The previous owner terminated without releasing the mutex. Ownership transfers to
            # this thread, so the cache operation can safely recover the entry.
            $lockTaken = $true
        }

        if (-not $lockTaken) {
            throw [System.TimeoutException]::new(
                ("Timed out after {0} seconds waiting to update cache entry '{1}'." -f $TimeoutSeconds, $ScriptName)
            )
        }

        & $Operation @ArgumentList
    }
    finally {
        if ($lockTaken) {
            $mutex.ReleaseMutex()
        }

        $mutex.Dispose()
    }
}