using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
///
/// Process-wide cache of site-side script s, keyed on
/// the globals type plus a SHA-256 hash of the script code. Unlike the TemplateEngine's
/// ScriptCompileVerdictCache (which caches central-validation verdicts), this
/// caches the full compiled result — the immutable Roslyn Script<object?> itself —
/// so the synchronous deploy gate and the InstanceActor.PreStart recompile share one
/// Roslyn compile per script body per process lifetime (N4).
///
/// Soundness rests on three invariants:
///
/// - A compile result is a pure function of (code, globals type, compile-time-static
/// trust policy) — none of which vary at runtime for a given key.
/// - The stored error text is name-free (Roslyn Diagnostic.GetMessage() /
/// "Compilation exception: …"); callers prefix their own script-name context, so a
/// shared cached failure renders correctly for every caller. Failures are cached too —
/// a bad script body fails identically for every caller.
/// - Roslyn Script<T> is immutable and thread-safe (RunAsync creates
/// per-run state), so one compiled instance may be shared across actors and threads.
///
///
///
/// Bounded at entries — deliberately smaller than the verdict cache's
/// 4096 because entries pin compiled assemblies, not just verdict strings. On overflow the cache
/// is cleared wholesale (results are recomputable, so a coarse reset avoids eviction bookkeeping).
/// // are exposed for tests and diagnostics.
///
///
internal static class SiteScriptCompileCache
{
/// Upper bound on cached entries; the cache is cleared wholesale on overflow.
internal const int MaxEntries = 1024;
private static readonly ConcurrentDictionary Cache = new();
private static long _hits;
/// Number of cache hits observed since the last .
public static long Hits => Interlocked.Read(ref _hits);
/// Current number of cached compile results.
public static int Count => Cache.Count;
///
/// Returns the cached compile result for against
/// , or computes it via and caches
/// it on a miss. A hit increments . Both success and failure results are
/// cached — the error text is name-free by construction.
///
/// The script source code to look up (hashed to form the cache key).
/// The Roslyn globals surface the script compiles against; part of the key so identical source under different globals stays distinct.
/// Computes the compile result on a cache miss.
/// The cached or newly computed compile result.
public static ScriptCompilationResult GetOrAdd(string code, Type globalsType, Func factory)
{
var key = globalsType.FullName + ":" + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(code)));
if (Cache.TryGetValue(key, out var result))
{
Interlocked.Increment(ref _hits);
return result;
}
result = factory();
// Coarse bound: on overflow drop everything rather than track evictions.
if (Cache.Count >= MaxEntries)
Cache.Clear();
Cache[key] = result;
return result;
}
/// Clears all cached compile results and resets the hit counter.
public static void Clear()
{
Cache.Clear();
Interlocked.Exchange(ref _hits, 0);
}
}