using ZB.MOM.WW.GalaxyRepository; using ZB.MOM.WW.GalaxyRepository.Grpc; namespace ZB.MOM.WW.MxGateway.Server.Dashboard; /// /// Flat, pre-lowercased attribute index behind the Browse page's filter box, shared by /// every circuit and rebuilt only when the Galaxy cache sequence changes. /// /// /// The filter used to walk every object and attribute of the cached Galaxy, doing two /// case-insensitive Contains per attribute, synchronously in the property setter /// bound to oninput — so a large Galaxy was rescanned on the render dispatcher /// once per keystroke, per circuit. The page now debounces and runs the search off the /// dispatcher; this index makes each search a single pass of ordinal substring checks /// over strings lowercased once per Galaxy deploy rather than once per comparison. /// Lowercasing is invariant-culture, which matches the previous /// behaviour for the ASCII tag names /// Galaxy uses. /// public sealed class DashboardBrowseSearchIndex { // How often a scan checks its cancellation token. A superseded keystroke stops // within this many attributes rather than finishing the whole Galaxy. private const int CancellationCheckInterval = 1024; private IndexSnapshot? _snapshot; private int _buildCount; /// Gets how many times the index has been (re)built (diagnostic, for tests). internal int BuildCount => Volatile.Read(ref _buildCount); /// /// Returns up to attributes whose full tag reference or /// attribute name contains , case-insensitively, in /// Galaxy object order. /// /// The current Galaxy cache entry; its sequence keys the index. /// The filter text as typed; it is trimmed. /// Maximum number of matches. /// Cancelled when a newer query supersedes this one. /// The matches; empty for a blank query. /// was cancelled. public IReadOnlyList Search( GalaxyHierarchyCacheEntry entry, string rawQuery, int limit, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(entry); string query = (rawQuery ?? string.Empty).Trim().ToLowerInvariant(); if (query.Length == 0 || limit <= 0) { return []; } IndexedAttribute[] attributes = GetOrBuild(entry, cancellationToken); List matches = []; for (int i = 0; i < attributes.Length; i++) { if (i % CancellationCheckInterval == 0) { cancellationToken.ThrowIfCancellationRequested(); } IndexedAttribute candidate = attributes[i]; if (candidate.FullTagReference.Contains(query, StringComparison.Ordinal) || candidate.AttributeName.Contains(query, StringComparison.Ordinal)) { matches.Add(candidate.Attribute); if (matches.Count >= limit) { break; } } } return matches; } private IndexedAttribute[] GetOrBuild(GalaxyHierarchyCacheEntry entry, CancellationToken cancellationToken) { IndexSnapshot? current = Volatile.Read(ref _snapshot); if (current is not null && current.Sequence == entry.Sequence) { return current.Attributes; } // A racing build for the same sequence is harmless: the projection is pure, so // either winner stores equivalent content. List attributes = []; foreach (GalaxyObject galaxyObject in entry.Objects) { cancellationToken.ThrowIfCancellationRequested(); foreach (GalaxyAttribute attribute in galaxyObject.Attributes) { attributes.Add(new IndexedAttribute( attribute.FullTagReference.ToLowerInvariant(), attribute.AttributeName.ToLowerInvariant(), attribute)); } } IndexSnapshot built = new(entry.Sequence, [.. attributes]); Volatile.Write(ref _snapshot, built); Interlocked.Increment(ref _buildCount); return built.Attributes; } private sealed record IndexSnapshot(long Sequence, IndexedAttribute[] Attributes); private readonly record struct IndexedAttribute( string FullTagReference, string AttributeName, GalaxyAttribute Attribute); }