using ZB.MOM.WW.Audit;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
///
/// Per-run sink for ScriptTrace audit events so a traced routed call can return them
/// inline on ScriptCallResult.TraceEvents (Flow Debugger design 2026-09-05 §5.4).
/// Lock-based: a run emits tens of events, not thousands. Beyond
/// the collector keeps the first N, sets , and the audit rows written
/// through IAuditWriter remain complete — Replay is never truncated, only the inline copy.
/// An untraced run holds null rather than an inert collector; callers null-check.
/// Outbound-calls addendum (plan 2026-09-06 D5): the ApiOutbound / DbOutbound /
/// Notification rows a run emits ride the same sink, so closes it
/// when the launcher takes the reply snapshot — a lifecycle row the S&F forwarder writes
/// minutes later, or an un-awaited call finishing after the run returned, must not mutate a
/// list the reply has already been built from. is a no-op once completed.
///
public sealed class FlowTraceCollector
{
/// Default inline cap per run (design §5.4 MaxTraceEventsPerRun).
public const int DefaultMaxEvents = 500;
private readonly List _events = new();
private readonly object _gate = new();
private readonly int _max;
/// True once an was dropped because the cap was reached.
public bool Truncated { get; private set; }
///
/// True once ran — the run's reply snapshot has been taken and
/// later calls are ignored (they never count as ).
///
public bool Completed { get; private set; }
/// Creates a collector with the given inline cap (must be positive).
public FlowTraceCollector(int maxEvents = DefaultMaxEvents)
{
if (maxEvents <= 0) throw new ArgumentOutOfRangeException(nameof(maxEvents), "must be positive");
_max = maxEvents;
}
///
/// Appends an event, or marks the collector truncated once the cap is reached. A no-op
/// after .
///
public void Add(AuditEvent evt)
{
ArgumentNullException.ThrowIfNull(evt);
lock (_gate)
{
if (Completed) return;
if (_events.Count >= _max) { Truncated = true; return; }
_events.Add(evt);
}
}
///
/// Closes the collector and returns the final snapshot — the launcher calls this exactly
/// where it builds ScriptCallResult.TraceEvents. Idempotent: a second call returns
/// the same contents.
///
public IReadOnlyList Complete()
{
lock (_gate)
{
Completed = true;
return _events.ToArray();
}
}
/// Current event count.
public int Count { get { lock (_gate) return _events.Count; } }
///
/// Copies the collected events in emission order WITHOUT closing the collector — a diagnostic /
/// test-only peek. A reply path must use , otherwise an un-awaited call
/// finishing after the reply could mutate the list the reply was built from.
///
public IReadOnlyList Snapshot()
{
lock (_gate) return _events.ToArray();
}
}