using Acornima.Ast;
using Jint;
namespace XFESystem.Browser;
/// <summary>
/// Bounded LRU for immutable packaged script syntax. Jint's Prepared instance is
/// documented as reusable across engines; no Engine, realm, JS object, global or
/// evaluated result is retained here. Ordinary network/user scripts never enter.
/// Exact source equality guards changed resources, independent of key collisions.
/// </summary>
internal sealed class PreparedScriptCache(int maximumEntries = 24, int maximumCharacters = 8_388_608, int maximumNodes = 2_000_000)
{
internal const int MinimumSourceCharacters = 1_024;
internal static PreparedScriptCache Shared { get; } = new();
private sealed record Entry(string Key, string Source, Prepared<Script> Script, int Nodes);
private readonly object gate = new();
private readonly List<Entry> entries = [];
private int characters, nodes;
internal (int Entries, int Characters, int Nodes) Snapshot
{
get
{
Monitor.Enter(gate);
var snapshot = (entries.Count, characters, nodes);
Monitor.Exit(gate);
return snapshot;
}
}
internal Prepared<Script> GetOrPrepare(string source, string key, out bool reused)
{
reused = false;
Monitor.Enter(gate);
var found = Find(key, source);
if (found is not null)
{
reused = true;
Monitor.Exit(gate);
return found.Script;
}
Monitor.Exit(gate);
// Parse outside the global cache gate. There is no engine/realm capture.
// Disabling constant folding avoids retaining prepared JS literal values;
// only syntax/static analysis is reused. Execution constraints are applied
// by Engine.Evaluate(prepared) exactly as for Engine.Evaluate(source).
var prepared = Engine.PrepareScript(source, key, strict: false,
options: new ScriptPreparationOptions { FoldConstants = false });
if (source.Length > maximumCharacters || key.Length > 2_048 || maximumEntries <= 0) return prepared;
int count = CountNodes(prepared.Program!, maximumNodes);
if (count > maximumNodes) return prepared;
Monitor.Enter(gate);
found = Find(key, source);
if (found is not null)
{
reused = true;
Monitor.Exit(gate);
return found.Script;
}
while (entries.Count >= maximumEntries || characters > maximumCharacters - source.Length || nodes > maximumNodes - count)
RemoveAt(0);
entries.Add(new(key, source, prepared, count));
characters += source.Length; nodes += count;
Monitor.Exit(gate);
return prepared;
}
private Entry? Find(string key, string source)
{
for (int index = 0; index < entries.Count; index++)
{
var entry = entries[index];
if (!string.Equals(entry.Key, key, StringComparison.Ordinal)) continue;
if (!string.Equals(entry.Source, source, StringComparison.Ordinal)) { RemoveAt(index); return null; }
entries.RemoveAt(index); entries.Add(entry);
return entry;
}
return null;
}
private void RemoveAt(int index)
{
var entry = entries[index]; entries.RemoveAt(index);
characters -= entry.Source.Length; nodes -= entry.Nodes;
}
private static int CountNodes(Node root, int maximum)
{
var pending = new Stack<Node>(); pending.Push(root); int count = 0;
while (pending.TryPop(out var node))
{
if (++count > maximum) return count;
foreach (var child in node.ChildNodes) pending.Push(child);
}
return count;
}
}
using Acornima.Ast;
using Jint;
namespace XFESystem.Browser;
/// <summary>
/// Bounded LRU for immutable packaged script syntax. Jint's Prepared instance is
/// documented as reusable across engines; no Engine, realm, JS object, global or
/// evaluated result is retained here. Ordinary network/user scripts never enter.
/// Exact source equality guards changed resources, independent of key collisions.
/// </summary>
internal sealed class PreparedScriptCache(int maximumEntries = 24, int maximumCharacters = 8_388_608, int maximumNodes = 2_000_000)
{
internal const int MinimumSourceCharacters = 1_024;
internal static PreparedScriptCache Shared { get; } = new();
private sealed record Entry(string Key, string Source, Prepared<Script> Script, int Nodes);
private readonly object gate = new();
private readonly List<Entry> entries = [];
private int characters, nodes;
internal (int Entries, int Characters, int Nodes) Snapshot
{
get
{
Monitor.Enter(gate);
var snapshot = (entries.Count, characters, nodes);
Monitor.Exit(gate);
return snapshot;
}
}
internal Prepared<Script> GetOrPrepare(string source, string key, out bool reused)
{
reused = false;
Monitor.Enter(gate);
var found = Find(key, source);
if (found is not null)
{
reused = true;
Monitor.Exit(gate);
return found.Script;
}
Monitor.Exit(gate);
// Parse outside the global cache gate. There is no engine/realm capture.
// Disabling constant folding avoids retaining prepared JS literal values;
// only syntax/static analysis is reused. Execution constraints are applied
// by Engine.Evaluate(prepared) exactly as for Engine.Evaluate(source).
var prepared = Engine.PrepareScript(source, key, strict: false,
options: new ScriptPreparationOptions { FoldConstants = false });
if (source.Length > maximumCharacters || key.Length > 2_048 || maximumEntries <= 0) return prepared;
int count = CountNodes(prepared.Program!, maximumNodes);
if (count > maximumNodes) return prepared;
Monitor.Enter(gate);
found = Find(key, source);
if (found is not null)
{
reused = true;
Monitor.Exit(gate);
return found.Script;
}
while (entries.Count >= maximumEntries || characters > maximumCharacters - source.Length || nodes > maximumNodes - count)
RemoveAt(0);
entries.Add(new(key, source, prepared, count));
characters += source.Length; nodes += count;
Monitor.Exit(gate);
return prepared;
}
private Entry? Find(string key, string source)
{
for (int index = 0; index < entries.Count; index++)
{
var entry = entries[index];
if (!string.Equals(entry.Key, key, StringComparison.Ordinal)) continue;
if (!string.Equals(entry.Source, source, StringComparison.Ordinal)) { RemoveAt(index); return null; }
entries.RemoveAt(index); entries.Add(entry);
return entry;
}
return null;
}
private void RemoveAt(int index)
{
var entry = entries[index]; entries.RemoveAt(index);
characters -= entry.Source.Length; nodes -= entry.Nodes;
}
private static int CountNodes(Node root, int maximum)
{
var pending = new Stack<Node>(); pending.Push(root); int count = 0;
while (pending.TryPop(out var node))
{
if (++count > maximum) return count;
foreach (var child in node.ChildNodes) pending.Push(child);
}
return count;
}
}