feat: complete legacy UI and playout parity migration
This commit is contained in:
@@ -0,0 +1,664 @@
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
|
||||
/// <summary>
|
||||
/// C# authority for the original UC3 comparison workflow. Search results and fixed targets
|
||||
/// are selected by opaque ids, saved bytes are parsed by the strict Core CP949 parser, and
|
||||
/// an action id is the only caller-supplied value used to materialize a playlist draft.
|
||||
/// </summary>
|
||||
public sealed class LegacyComparisonWorkflowController
|
||||
{
|
||||
private readonly ILegacyComparisonDataService _dataService;
|
||||
private readonly ILegacyComparisonPairStore _pairStore;
|
||||
private List<SearchResultState> _domesticResults = [];
|
||||
private List<SearchResultState> _worldResults = [];
|
||||
private List<SavedPairState> _savedPairs = [];
|
||||
private LegacyComparisonTarget? _firstTarget;
|
||||
private LegacyComparisonTarget? _secondTarget;
|
||||
private string? _selectedDomesticResultId;
|
||||
private string? _selectedWorldResultId;
|
||||
private string? _selectedFixedTargetId;
|
||||
private string? _selectedPairId;
|
||||
private string _domesticQuery = string.Empty;
|
||||
private string _worldQuery = string.Empty;
|
||||
private string _domesticError = string.Empty;
|
||||
private string _worldError = string.Empty;
|
||||
private string _statusMessage = string.Empty;
|
||||
private LegacyComparisonSearchStatus _domesticStatus = LegacyComparisonSearchStatus.Idle;
|
||||
private LegacyComparisonSearchStatus _worldStatus = LegacyComparisonSearchStatus.Idle;
|
||||
private LegacyOperatorStatusKind _statusKind = LegacyOperatorStatusKind.Neutral;
|
||||
private long _revision;
|
||||
private long _pairSequence;
|
||||
private long _draftSequence;
|
||||
private int _domesticSearchVersion;
|
||||
private int _worldSearchVersion;
|
||||
private bool _isBusy;
|
||||
|
||||
public LegacyComparisonWorkflowController(
|
||||
ILegacyComparisonDataService dataService,
|
||||
ILegacyComparisonPairStore pairStore)
|
||||
{
|
||||
_dataService = dataService ?? throw new ArgumentNullException(nameof(dataService));
|
||||
_pairStore = pairStore ?? throw new ArgumentNullException(nameof(pairStore));
|
||||
}
|
||||
|
||||
public LegacyComparisonWorkflowSnapshot Current => CreateSnapshot();
|
||||
|
||||
public async Task<LegacyComparisonWorkflowSnapshot> SearchDomesticAsync(
|
||||
string query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedQuery = NormalizeSearchQuery(query);
|
||||
var version = ++_domesticSearchVersion;
|
||||
_domesticQuery = normalizedQuery;
|
||||
_domesticStatus = LegacyComparisonSearchStatus.Loading;
|
||||
_domesticError = string.Empty;
|
||||
_domesticResults = [];
|
||||
_selectedDomesticResultId = null;
|
||||
Touch();
|
||||
|
||||
try
|
||||
{
|
||||
var results = await _dataService.SearchDomesticAsync(
|
||||
normalizedQuery,
|
||||
LegacyComparisonWorkflowCatalog.DomesticMaximumResults,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (version != _domesticSearchVersion)
|
||||
{
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
_domesticResults = ValidateSearchResults(
|
||||
results,
|
||||
LegacyComparisonWorkflowCatalog.DomesticMaximumResults,
|
||||
domestic: true,
|
||||
version);
|
||||
_domesticStatus = LegacyComparisonSearchStatus.Ready;
|
||||
_domesticError = string.Empty;
|
||||
Touch();
|
||||
return CreateSnapshot();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (version == _domesticSearchVersion)
|
||||
{
|
||||
_domesticStatus = LegacyComparisonSearchStatus.Idle;
|
||||
_domesticError = string.Empty;
|
||||
Touch();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (version == _domesticSearchVersion)
|
||||
{
|
||||
_domesticResults = [];
|
||||
_domesticStatus = LegacyComparisonSearchStatus.Error;
|
||||
_domesticError = "국내·NXT 종목 검색 결과를 안전하게 확인하지 못했습니다.";
|
||||
Touch();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LegacyComparisonWorkflowSnapshot> SearchWorldAsync(
|
||||
string query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// UC3's blank world query loaded the complete US/TW universe, so blank remains valid.
|
||||
var normalizedQuery = NormalizeSearchQuery(query);
|
||||
var version = ++_worldSearchVersion;
|
||||
_worldQuery = normalizedQuery;
|
||||
_worldStatus = LegacyComparisonSearchStatus.Loading;
|
||||
_worldError = string.Empty;
|
||||
_worldResults = [];
|
||||
_selectedWorldResultId = null;
|
||||
Touch();
|
||||
|
||||
try
|
||||
{
|
||||
var results = await _dataService.SearchWorldAsync(
|
||||
normalizedQuery,
|
||||
LegacyComparisonWorkflowCatalog.WorldMaximumResults,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (version != _worldSearchVersion)
|
||||
{
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
_worldResults = ValidateSearchResults(
|
||||
results,
|
||||
LegacyComparisonWorkflowCatalog.WorldMaximumResults,
|
||||
domestic: false,
|
||||
version);
|
||||
_worldStatus = LegacyComparisonSearchStatus.Ready;
|
||||
_worldError = string.Empty;
|
||||
Touch();
|
||||
return CreateSnapshot();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (version == _worldSearchVersion)
|
||||
{
|
||||
_worldStatus = LegacyComparisonSearchStatus.Idle;
|
||||
_worldError = string.Empty;
|
||||
Touch();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (version == _worldSearchVersion)
|
||||
{
|
||||
_worldResults = [];
|
||||
_worldStatus = LegacyComparisonSearchStatus.Error;
|
||||
_worldError = "미국·대만 종목 검색 결과를 안전하게 확인하지 못했습니다.";
|
||||
Touch();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public LegacyComparisonWorkflowSnapshot SelectDomesticResult(string selectionId) =>
|
||||
SelectSearchResult(_domesticResults, selectionId, domestic: true);
|
||||
|
||||
public LegacyComparisonWorkflowSnapshot SelectWorldResult(string selectionId) =>
|
||||
SelectSearchResult(_worldResults, selectionId, domestic: false);
|
||||
|
||||
public LegacyComparisonWorkflowSnapshot SelectFixedTarget(string targetId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(targetId);
|
||||
if (LegacyComparisonWorkflowCatalog.GetFixedTarget(targetId) is not null)
|
||||
{
|
||||
_selectedFixedTargetId = targetId;
|
||||
Touch();
|
||||
}
|
||||
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
public LegacyComparisonWorkflowSnapshot ChooseDomesticResult(string selectionId) =>
|
||||
ChooseSearchResult(_domesticResults, selectionId, domestic: true);
|
||||
|
||||
public LegacyComparisonWorkflowSnapshot ChooseWorldResult(string selectionId) =>
|
||||
ChooseSearchResult(_worldResults, selectionId, domestic: false);
|
||||
|
||||
public LegacyComparisonWorkflowSnapshot ChooseFixedTarget(string targetId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(targetId);
|
||||
var target = LegacyComparisonWorkflowCatalog.GetFixedTarget(targetId);
|
||||
if (target is not null)
|
||||
{
|
||||
_selectedFixedTargetId = targetId;
|
||||
RotateDraft(target);
|
||||
Touch();
|
||||
}
|
||||
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
public LegacyComparisonWorkflowSnapshot SwapDraftTargets()
|
||||
{
|
||||
(_firstTarget, _secondTarget) = (_secondTarget, _firstTarget);
|
||||
Touch();
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
public LegacyComparisonWorkflowSnapshot ClearDraftTargets()
|
||||
{
|
||||
_firstTarget = null;
|
||||
_secondTarget = null;
|
||||
Touch();
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
public async Task<LegacyComparisonWorkflowSnapshot> LoadSavedPairsAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
BeginBusy();
|
||||
try
|
||||
{
|
||||
var source = await _pairStore.ReadCp949Async(cancellationToken).ConfigureAwait(false);
|
||||
var candidate = new List<SavedPairState>();
|
||||
if (source is { Length: > 0 })
|
||||
{
|
||||
var bytes = source.ToArray();
|
||||
var rows = LegacyComparisonFileParser.Parse(bytes);
|
||||
foreach (var row in rows)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var firstReference = new LegacyComparisonStoredTargetReference(
|
||||
row.First.Name,
|
||||
row.First.Market);
|
||||
var secondReference = new LegacyComparisonStoredTargetReference(
|
||||
row.Second.Name,
|
||||
row.Second.Market);
|
||||
var first = await ResolveStoredTargetAsync(
|
||||
firstReference,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var second = await ResolveStoredTargetAsync(
|
||||
secondReference,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
candidate.Add(new SavedPairState(
|
||||
$"comparison-pair-{row.RowNumber:D8}",
|
||||
first,
|
||||
second));
|
||||
}
|
||||
}
|
||||
|
||||
_savedPairs = candidate;
|
||||
_pairSequence = Math.Max(_pairSequence, candidate.Count);
|
||||
_selectedPairId = candidate.FirstOrDefault()?.RowId;
|
||||
_statusMessage = candidate.Count == 0
|
||||
? "저장된 종목 비교쌍이 없습니다."
|
||||
: $"저장된 종목 비교쌍 {candidate.Count}개를 불러왔습니다.";
|
||||
_statusKind = LegacyOperatorStatusKind.Information;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_statusMessage = "CP949 종목비교.dat를 안전하게 불러오지 못했습니다.";
|
||||
_statusKind = LegacyOperatorStatusKind.Error;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndBusy();
|
||||
}
|
||||
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
public async Task<LegacyComparisonWorkflowSnapshot> AddDraftPairAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_firstTarget is null || _secondTarget is null)
|
||||
{
|
||||
throw new InvalidOperationException("Select two comparison targets first.");
|
||||
}
|
||||
|
||||
if (_savedPairs.Count >= LegacyComparisonFileParser.MaximumRows)
|
||||
{
|
||||
throw new InvalidOperationException("The saved comparison pair limit was reached.");
|
||||
}
|
||||
|
||||
var signature = LegacyComparisonWorkflowCatalog.PairSignature(
|
||||
_firstTarget,
|
||||
_secondTarget);
|
||||
if (_savedPairs.Any(pair => string.Equals(
|
||||
LegacyComparisonWorkflowCatalog.PairSignature(pair.First, pair.Second),
|
||||
signature,
|
||||
StringComparison.Ordinal)))
|
||||
{
|
||||
throw new InvalidOperationException("The exact comparison pair is already saved.");
|
||||
}
|
||||
|
||||
var rowId = $"comparison-pair-{++_pairSequence:D8}";
|
||||
var candidate = new List<SavedPairState>(_savedPairs)
|
||||
{
|
||||
new(rowId, _firstTarget, _secondTarget)
|
||||
};
|
||||
return await CommitPairsAsync(
|
||||
candidate,
|
||||
rowId,
|
||||
"종목 비교쌍을 저장했습니다.",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public LegacyComparisonWorkflowSnapshot SelectSavedPair(string rowId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(rowId);
|
||||
if (_savedPairs.Any(pair => string.Equals(pair.RowId, rowId, StringComparison.Ordinal)))
|
||||
{
|
||||
_selectedPairId = rowId;
|
||||
Touch();
|
||||
}
|
||||
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
public async Task<LegacyComparisonWorkflowSnapshot> MoveSelectedPairAsync(
|
||||
LegacyComparisonPairMoveDirection direction,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!Enum.IsDefined(direction))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(direction));
|
||||
}
|
||||
|
||||
var index = SelectedPairIndex();
|
||||
var destination = index + (direction == LegacyComparisonPairMoveDirection.Up ? -1 : 1);
|
||||
if (index < 0 || destination < 0 || destination >= _savedPairs.Count)
|
||||
{
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
var candidate = new List<SavedPairState>(_savedPairs);
|
||||
(candidate[index], candidate[destination]) = (candidate[destination], candidate[index]);
|
||||
return await CommitPairsAsync(
|
||||
candidate,
|
||||
_selectedPairId,
|
||||
"종목 비교쌍 순서를 변경했습니다.",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<LegacyComparisonWorkflowSnapshot> DeleteSelectedPairAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var index = SelectedPairIndex();
|
||||
if (index < 0)
|
||||
{
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
var candidate = new List<SavedPairState>(_savedPairs);
|
||||
candidate.RemoveAt(index);
|
||||
var nextSelection = candidate.Count == 0
|
||||
? null
|
||||
: candidate[Math.Min(index, candidate.Count - 1)].RowId;
|
||||
return await CommitPairsAsync(
|
||||
candidate,
|
||||
nextSelection,
|
||||
"선택한 종목 비교쌍을 삭제했습니다.",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<LegacyComparisonWorkflowSnapshot> DeleteAllPairsAsync(
|
||||
CancellationToken cancellationToken = default) =>
|
||||
_savedPairs.Count == 0
|
||||
? Task.FromResult(CreateSnapshot())
|
||||
: CommitPairsAsync(
|
||||
[],
|
||||
null,
|
||||
"종목 비교쌍을 모두 삭제했습니다.",
|
||||
cancellationToken);
|
||||
|
||||
public LegacyGenericPlaylistDraft MaterializeSelectedAction(string actionId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(actionId);
|
||||
if (_isBusy)
|
||||
{
|
||||
throw new InvalidOperationException("The comparison workflow is busy.");
|
||||
}
|
||||
|
||||
var index = SelectedPairIndex();
|
||||
if (index < 0)
|
||||
{
|
||||
throw new InvalidOperationException("Select a saved comparison pair first.");
|
||||
}
|
||||
|
||||
var pair = _savedPairs[index];
|
||||
var draftId = $"comparison-draft-{++_draftSequence:D8}";
|
||||
return LegacyComparisonWorkflowCatalog.Materialize(
|
||||
actionId,
|
||||
pair.First,
|
||||
pair.Second,
|
||||
draftId);
|
||||
}
|
||||
|
||||
private LegacyComparisonWorkflowSnapshot ChooseSearchResult(
|
||||
IReadOnlyList<SearchResultState> results,
|
||||
string selectionId,
|
||||
bool domestic)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(selectionId);
|
||||
var result = results.FirstOrDefault(item =>
|
||||
string.Equals(item.SelectionId, selectionId, StringComparison.Ordinal));
|
||||
if (result is not null)
|
||||
{
|
||||
if (domestic)
|
||||
{
|
||||
_selectedDomesticResultId = selectionId;
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedWorldResultId = selectionId;
|
||||
}
|
||||
|
||||
RotateDraft(result.Target);
|
||||
Touch();
|
||||
}
|
||||
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
private LegacyComparisonWorkflowSnapshot SelectSearchResult(
|
||||
IReadOnlyList<SearchResultState> results,
|
||||
string selectionId,
|
||||
bool domestic)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(selectionId);
|
||||
if (results.Any(item => string.Equals(
|
||||
item.SelectionId,
|
||||
selectionId,
|
||||
StringComparison.Ordinal)))
|
||||
{
|
||||
if (domestic)
|
||||
{
|
||||
_selectedDomesticResultId = selectionId;
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedWorldResultId = selectionId;
|
||||
}
|
||||
|
||||
Touch();
|
||||
}
|
||||
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
private void RotateDraft(LegacyComparisonTarget selected)
|
||||
{
|
||||
_secondTarget = _firstTarget;
|
||||
_firstTarget = selected;
|
||||
}
|
||||
|
||||
private async Task<LegacyComparisonTarget> ResolveStoredTargetAsync(
|
||||
LegacyComparisonStoredTargetReference stored,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var fixedTarget = LegacyComparisonWorkflowCatalog.TryResolveFixedStoredTarget(stored);
|
||||
var resolved = fixedTarget ?? await _dataService.ResolveStoredTargetAsync(
|
||||
stored,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (resolved is null)
|
||||
{
|
||||
throw new InvalidOperationException("A stored comparison target no longer resolves.");
|
||||
}
|
||||
|
||||
resolved = LegacyComparisonWorkflowCatalog.RequireCanonicalTarget(resolved);
|
||||
if (!LegacyComparisonWorkflowCatalog.MatchesStoredReference(resolved, stored))
|
||||
{
|
||||
throw new InvalidOperationException("A stored comparison target resolved to a different identity.");
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private async Task<LegacyComparisonWorkflowSnapshot> CommitPairsAsync(
|
||||
IReadOnlyList<SavedPairState> candidate,
|
||||
string? nextSelection,
|
||||
string successMessage,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
BeginBusy();
|
||||
try
|
||||
{
|
||||
var writeRows = candidate.Select((pair, index) =>
|
||||
{
|
||||
var first = LegacyComparisonWorkflowCatalog.ToStoredReference(pair.First);
|
||||
var second = LegacyComparisonWorkflowCatalog.ToStoredReference(pair.Second);
|
||||
return new LegacyComparisonPairWriteRow(
|
||||
index + 1,
|
||||
first.Name,
|
||||
first.Market,
|
||||
second.Name,
|
||||
second.Market);
|
||||
}).ToArray();
|
||||
await _pairStore.WriteCp949Async(
|
||||
Array.AsReadOnly(writeRows),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_savedPairs = candidate.ToList();
|
||||
_selectedPairId = nextSelection;
|
||||
_statusMessage = successMessage;
|
||||
_statusKind = LegacyOperatorStatusKind.Information;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_statusMessage = "종목 비교쌍 파일 쓰기를 완료하지 못해 변경을 적용하지 않았습니다.";
|
||||
_statusKind = LegacyOperatorStatusKind.Error;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndBusy();
|
||||
}
|
||||
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
private static List<SearchResultState> ValidateSearchResults(
|
||||
IReadOnlyList<LegacyComparisonTarget>? results,
|
||||
int maximumResults,
|
||||
bool domestic,
|
||||
int version)
|
||||
{
|
||||
if (results is null || results.Count > maximumResults)
|
||||
{
|
||||
throw new InvalidOperationException("The comparison search result count is invalid.");
|
||||
}
|
||||
|
||||
var validated = new List<SearchResultState>(results.Count);
|
||||
var targetIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
for (var index = 0; index < results.Count; index++)
|
||||
{
|
||||
var target = LegacyComparisonWorkflowCatalog.RequireCanonicalTarget(results[index]);
|
||||
var allowedKind = domestic
|
||||
? target.Kind is LegacyComparisonTargetKind.KrxStock or LegacyComparisonTargetKind.NxtStock
|
||||
: target.Kind == LegacyComparisonTargetKind.WorldStock;
|
||||
if (!allowedKind || !targetIds.Add(target.TargetId))
|
||||
{
|
||||
throw new InvalidOperationException("The comparison search result identity is invalid.");
|
||||
}
|
||||
|
||||
validated.Add(new SearchResultState(
|
||||
$"comparison-{(domestic ? "domestic" : "world")}-{version:D4}-{index + 1:D4}",
|
||||
target));
|
||||
}
|
||||
|
||||
return validated;
|
||||
}
|
||||
|
||||
private LegacyComparisonWorkflowSnapshot CreateSnapshot()
|
||||
{
|
||||
var selectedPair = SelectedPairIndex() is var selectedIndex && selectedIndex >= 0
|
||||
? _savedPairs[selectedIndex]
|
||||
: null;
|
||||
var domesticResults = _domesticResults
|
||||
.Select(result => LegacyComparisonWorkflowCatalog.ToView(
|
||||
result.Target,
|
||||
result.SelectionId))
|
||||
.ToArray();
|
||||
var worldResults = _worldResults
|
||||
.Select(result => LegacyComparisonWorkflowCatalog.ToView(
|
||||
result.Target,
|
||||
result.SelectionId))
|
||||
.ToArray();
|
||||
var fixedTargets = LegacyComparisonWorkflowCatalog.FixedTargets
|
||||
.Select(target => LegacyComparisonWorkflowCatalog.ToView(target, target.TargetId))
|
||||
.ToArray();
|
||||
var savedPairs = _savedPairs.Select((pair, index) =>
|
||||
new LegacyComparisonSavedPairView(
|
||||
pair.RowId,
|
||||
index + 1,
|
||||
$"{pair.First.DisplayName} ↔ {pair.Second.DisplayName}",
|
||||
LegacyComparisonWorkflowCatalog.ToView(pair.First, $"{pair.RowId}-first"),
|
||||
LegacyComparisonWorkflowCatalog.ToView(pair.Second, $"{pair.RowId}-second"),
|
||||
string.Equals(pair.RowId, _selectedPairId, StringComparison.Ordinal)))
|
||||
.ToArray();
|
||||
return new LegacyComparisonWorkflowSnapshot(
|
||||
_revision,
|
||||
_isBusy,
|
||||
new LegacyComparisonSearchSnapshot(
|
||||
_domesticQuery,
|
||||
_domesticStatus,
|
||||
Array.AsReadOnly(domesticResults),
|
||||
_selectedDomesticResultId,
|
||||
domesticResults.Length,
|
||||
_domesticError),
|
||||
new LegacyComparisonSearchSnapshot(
|
||||
_worldQuery,
|
||||
_worldStatus,
|
||||
Array.AsReadOnly(worldResults),
|
||||
_selectedWorldResultId,
|
||||
worldResults.Length,
|
||||
_worldError),
|
||||
Array.AsReadOnly(fixedTargets),
|
||||
_selectedFixedTargetId,
|
||||
_firstTarget is null
|
||||
? null
|
||||
: LegacyComparisonWorkflowCatalog.ToView(_firstTarget, "comparison-draft-first"),
|
||||
_secondTarget is null
|
||||
? null
|
||||
: LegacyComparisonWorkflowCatalog.ToView(_secondTarget, "comparison-draft-second"),
|
||||
Array.AsReadOnly(savedPairs),
|
||||
_selectedPairId,
|
||||
LegacyComparisonWorkflowCatalog.CreateActionViews(
|
||||
selectedPair?.First,
|
||||
selectedPair?.Second),
|
||||
_statusMessage,
|
||||
_statusKind);
|
||||
}
|
||||
|
||||
private int SelectedPairIndex() => _selectedPairId is null
|
||||
? -1
|
||||
: _savedPairs.FindIndex(pair =>
|
||||
string.Equals(pair.RowId, _selectedPairId, StringComparison.Ordinal));
|
||||
|
||||
private static string NormalizeSearchQuery(string query)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
var normalized = query.Trim();
|
||||
if (normalized.Length > 64 ||
|
||||
normalized.Any(static character => char.IsControl(character) || char.IsSurrogate(character)))
|
||||
{
|
||||
throw new ArgumentException("A comparison query must be at most 64 safe characters.", nameof(query));
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private void BeginBusy()
|
||||
{
|
||||
if (_isBusy)
|
||||
{
|
||||
throw new InvalidOperationException("The comparison workflow is already busy.");
|
||||
}
|
||||
|
||||
_isBusy = true;
|
||||
Touch();
|
||||
}
|
||||
|
||||
private void EndBusy()
|
||||
{
|
||||
_isBusy = false;
|
||||
Touch();
|
||||
}
|
||||
|
||||
private void Touch() => _revision++;
|
||||
|
||||
private sealed record SearchResultState(
|
||||
string SelectionId,
|
||||
LegacyComparisonTarget Target);
|
||||
|
||||
private sealed record SavedPairState(
|
||||
string RowId,
|
||||
LegacyComparisonTarget First,
|
||||
LegacyComparisonTarget Second);
|
||||
}
|
||||
Reference in New Issue
Block a user