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(); } }