using System.Globalization; using System.Text.Json; using MBN_STOCK_WEBVIEW.Infrastructure; using MMoneyCoderSharp.Data; namespace MBN_STOCK_WEBVIEW; public sealed partial class MainWindow { private const string OperatorCatalogQuarantineMessage = "A theme/expert database write result could not be confirmed. Restart the app and reconcile the database before another catalog write."; private const decimal OperatorCatalogMaximumWebSafeInteger = 9_007_199_254_740_991m; private readonly SemaphoreSlim _operatorCatalogWriteGate = new(1, 1); private readonly object _operatorCatalogReadLanesGate = new(); private readonly Dictionary _operatorCatalogReadLanes = new(StringComparer.Ordinal); private IThemeCatalogPersistenceService? _themeCatalogPersistenceService; private IExpertCatalogPersistenceService? _expertCatalogPersistenceService; private IOperatorCatalogSchemaValidationService? _operatorCatalogSchemaValidationService; private CancellationTokenSource? _operatorCatalogWriteCancellation; private string? _operatorCatalogInitializationError; private string? _operatorCatalogWriteQuarantineMessage; private int _operatorCatalogRuntimeState; private int _operatorCatalogWriteInFlight; private int _operatorCatalogWriteQuarantined; private int _operatorCatalogBrowserGeneration; /// /// Root integration hook. It is safe to call after InitializeDatabaseRuntime; /// request handlers also call it lazily so a missing optional eager call fails /// closed rather than dereferencing an uninitialized service. /// private void InitializeOperatorCatalogRuntime() { if (Volatile.Read(ref _operatorCatalogRuntimeState) != 0) { return; } var runtime = _databaseRuntime; if (runtime is null || Interlocked.CompareExchange(ref _operatorCatalogRuntimeState, 1, 0) != 0) { return; } try { var mutationExecutor = new OperatorCatalogMutationExecutor( runtime.ConnectionFactory, runtime.Options.Resilience); _themeCatalogPersistenceService = new LegacyThemeCatalogPersistenceService( runtime.Executor, mutationExecutor); _expertCatalogPersistenceService = new LegacyExpertCatalogPersistenceService( runtime.Executor, mutationExecutor); _operatorCatalogSchemaValidationService = new LegacyOperatorCatalogSchemaValidationService(runtime.Executor); } catch { _themeCatalogPersistenceService = null; _expertCatalogPersistenceService = null; _operatorCatalogSchemaValidationService = null; _operatorCatalogInitializationError = "The theme/expert database boundary could not be initialized."; Volatile.Write(ref _operatorCatalogRuntimeState, -1); } } /// /// Root WebMessage switch hook. Return true means the request type belongs to /// this boundary, including malformed payloads which receive a correlated /// INVALID_REQUEST response here. /// private bool TryHandleOperatorCatalogRequest(string? requestType, JsonElement payload) { switch (requestType) { case "request-theme-catalog-list": _ = HandleThemeCatalogListRequestAsync(payload); return true; case "request-expert-catalog-list": _ = HandleExpertCatalogListRequestAsync(payload); return true; case "request-operator-catalog-schema": _ = HandleOperatorCatalogSchemaRequestAsync(payload); return true; case "request-theme-catalog-next-code": _ = HandleThemeCatalogNextCodeRequestAsync(payload); return true; case "request-expert-catalog-next-code": _ = HandleExpertCatalogNextCodeRequestAsync(payload); return true; case "create-theme-catalog": _ = HandleThemeCatalogCreateRequestAsync(payload); return true; case "replace-theme-catalog": _ = HandleThemeCatalogReplaceRequestAsync(payload); return true; case "delete-theme-catalog": _ = HandleThemeCatalogDeleteRequestAsync(payload); return true; case "create-expert-catalog": _ = HandleExpertCatalogCreateRequestAsync(payload); return true; case "replace-expert-catalog": _ = HandleExpertCatalogReplaceRequestAsync(payload); return true; case "delete-expert-catalog": _ = HandleExpertCatalogDeleteRequestAsync(payload); return true; default: return false; } } private async Task HandleThemeCatalogListRequestAsync(JsonElement payload) { if (!TryParseThemeCatalogListRequest( payload, out var requestId, out var query, out var nxtSession, out var maximumResults)) { PostOperatorCatalogInvalidRequest( SafeOperatorCatalogRequestId(payload), "theme", "list"); return; } InitializeOperatorCatalogRuntime(); var service = _themeSelectionService; if (service is null) { PostOperatorCatalogUnavailable(requestId, "theme", "list"); return; } await ExecuteOperatorCatalogReadAsync( requestId, "theme", "list", async cancellationToken => { var result = await service.SearchAsync( query, nxtSession, maximumResults, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); return new OperatorCatalogBridgeReply( "theme-catalog-list-results", new { requestId, query = result.Query, nxtSession = ToWireValue(result.NxtSession), result.RetrievedAt, totalRowCount = result.Items.Count, truncated = result.IsTruncated, canMutate = CanMutateThemeCatalog(), writeQuarantined = IsOperatorCatalogWriteQuarantined(), results = result.Items.Select(item => new { market = ToWireValue(item.Identity.Market), session = ToWireValue(item.Identity.Session), themeCode = item.Identity.ThemeCode, themeTitle = item.Identity.ThemeTitle, source = ToWireValue(item.Source) }).ToArray() }); }); } private async Task HandleExpertCatalogListRequestAsync(JsonElement payload) { if (!TryParseExpertCatalogListRequest( payload, out var requestId, out var query, out var maximumResults)) { PostOperatorCatalogInvalidRequest( SafeOperatorCatalogRequestId(payload), "expert", "list"); return; } InitializeOperatorCatalogRuntime(); var service = _expertSelectionService; if (service is null) { PostOperatorCatalogUnavailable(requestId, "expert", "list"); return; } await ExecuteOperatorCatalogReadAsync( requestId, "expert", "list", async cancellationToken => { var result = await service.SearchAsync( query, maximumResults, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); return new OperatorCatalogBridgeReply( "expert-catalog-list-results", new { requestId, query = result.Query, result.RetrievedAt, totalRowCount = result.Items.Count, truncated = result.IsTruncated, canMutate = CanMutateExpertCatalog(), writeQuarantined = IsOperatorCatalogWriteQuarantined(), results = result.Items.Select(item => new { expertCode = item.Identity.ExpertCode, expertName = item.Identity.ExpertName, source = ToWireValue(item.Source) }).ToArray() }); }); } private async Task HandleOperatorCatalogSchemaRequestAsync(JsonElement payload) { if (!TryParseOperatorCatalogRequestIdOnly(payload, out var requestId)) { PostOperatorCatalogInvalidRequest( SafeOperatorCatalogRequestId(payload), "catalog", "schemaPreflight"); return; } InitializeOperatorCatalogRuntime(); var service = _operatorCatalogSchemaValidationService; if (service is null) { PostOperatorCatalogUnavailable( requestId, "catalog", "schemaPreflight"); return; } await ExecuteOperatorCatalogReadAsync( requestId, "catalog", "schemaPreflight", async cancellationToken => { var result = await service.ValidateAsync(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); return new OperatorCatalogBridgeReply( "operator-catalog-schema-results", new { requestId, result.ValidatedAt, validatedSources = result.ValidatedSources .Select(ToWireValue) .ToArray(), themeCanMutate = CanMutateThemeCatalog(), expertCanMutate = CanMutateExpertCatalog(), writeQuarantined = IsOperatorCatalogWriteQuarantined() }); }); } private async Task HandleThemeCatalogNextCodeRequestAsync(JsonElement payload) { if (!TryParseThemeCatalogNextCodeRequest( payload, out var requestId, out var market)) { PostOperatorCatalogInvalidRequest( SafeOperatorCatalogRequestId(payload), "theme", "nextCode"); return; } InitializeOperatorCatalogRuntime(); var service = _themeCatalogPersistenceService; if (service is null) { PostOperatorCatalogUnavailable(requestId, "theme", "nextCode"); return; } await ExecuteOperatorCatalogReadAsync( requestId, "theme", "nextCode", async cancellationToken => { var code = await service.SuggestNextCodeAsync(market, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); return new OperatorCatalogBridgeReply( "theme-catalog-next-code-results", new { requestId, market = ToWireValue(market), themeCode = code, canMutate = CanMutateThemeCatalog(), writeQuarantined = IsOperatorCatalogWriteQuarantined() }); }); } private async Task HandleExpertCatalogNextCodeRequestAsync(JsonElement payload) { if (!TryParseOperatorCatalogRequestIdOnly(payload, out var requestId)) { PostOperatorCatalogInvalidRequest( SafeOperatorCatalogRequestId(payload), "expert", "nextCode"); return; } InitializeOperatorCatalogRuntime(); var service = _expertCatalogPersistenceService; if (service is null) { PostOperatorCatalogUnavailable(requestId, "expert", "nextCode"); return; } await ExecuteOperatorCatalogReadAsync( requestId, "expert", "nextCode", async cancellationToken => { var code = await service.SuggestNextCodeAsync(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); return new OperatorCatalogBridgeReply( "expert-catalog-next-code-results", new { requestId, expertCode = code, canMutate = CanMutateExpertCatalog(), writeQuarantined = IsOperatorCatalogWriteQuarantined() }); }); } private async Task HandleThemeCatalogCreateRequestAsync(JsonElement payload) { if (!TryParseThemeCatalogCreateRequest( payload, out var requestId, out var definition)) { PostOperatorCatalogInvalidRequest( SafeOperatorCatalogRequestId(payload), "theme", "create"); return; } InitializeOperatorCatalogRuntime(); var service = _themeCatalogPersistenceService; await ExecuteOperatorCatalogWriteAsync( requestId, "theme", "create", definition.ThemeCode, service?.CanMutate == true, cancellationToken => service!.CreateAsync(definition, cancellationToken)); } private async Task HandleThemeCatalogReplaceRequestAsync(JsonElement payload) { if (!TryParseThemeCatalogReplaceRequest( payload, out var requestId, out var expectedIdentity, out var newTitle, out var items)) { PostOperatorCatalogInvalidRequest( SafeOperatorCatalogRequestId(payload), "theme", "replace"); return; } InitializeOperatorCatalogRuntime(); var service = _themeCatalogPersistenceService; await ExecuteOperatorCatalogWriteAsync( requestId, "theme", "replace", expectedIdentity.ThemeCode, service?.CanMutate == true, cancellationToken => service!.ReplaceAsync( expectedIdentity, newTitle, items, cancellationToken)); } private async Task HandleThemeCatalogDeleteRequestAsync(JsonElement payload) { if (!TryParseThemeCatalogDeleteRequest( payload, out var requestId, out var identity)) { PostOperatorCatalogInvalidRequest( SafeOperatorCatalogRequestId(payload), "theme", "delete"); return; } InitializeOperatorCatalogRuntime(); var service = _themeCatalogPersistenceService; await ExecuteOperatorCatalogWriteAsync( requestId, "theme", "delete", identity.ThemeCode, service?.CanMutate == true, cancellationToken => service!.DeleteAsync(identity, cancellationToken)); } private async Task HandleExpertCatalogCreateRequestAsync(JsonElement payload) { if (!TryParseExpertCatalogCreateRequest( payload, out var requestId, out var definition)) { PostOperatorCatalogInvalidRequest( SafeOperatorCatalogRequestId(payload), "expert", "create"); return; } InitializeOperatorCatalogRuntime(); var service = _expertCatalogPersistenceService; await ExecuteOperatorCatalogWriteAsync( requestId, "expert", "create", definition.Identity.ExpertCode, service?.CanMutate == true, cancellationToken => service!.CreateAsync(definition, cancellationToken)); } private async Task HandleExpertCatalogReplaceRequestAsync(JsonElement payload) { if (!TryParseExpertCatalogReplaceRequest( payload, out var requestId, out var expectedIdentity, out var newName, out var recommendations)) { PostOperatorCatalogInvalidRequest( SafeOperatorCatalogRequestId(payload), "expert", "replace"); return; } InitializeOperatorCatalogRuntime(); var service = _expertCatalogPersistenceService; await ExecuteOperatorCatalogWriteAsync( requestId, "expert", "replace", expectedIdentity.ExpertCode, service?.CanMutate == true, cancellationToken => service!.ReplaceAsync( expectedIdentity, newName, recommendations, cancellationToken)); } private async Task HandleExpertCatalogDeleteRequestAsync(JsonElement payload) { if (!TryParseExpertCatalogDeleteRequest( payload, out var requestId, out var identity)) { PostOperatorCatalogInvalidRequest( SafeOperatorCatalogRequestId(payload), "expert", "delete"); return; } InitializeOperatorCatalogRuntime(); var service = _expertCatalogPersistenceService; await ExecuteOperatorCatalogWriteAsync( requestId, "expert", "delete", identity.ExpertCode, service?.CanMutate == true, cancellationToken => service!.DeleteAsync(identity, cancellationToken)); } private async Task ExecuteOperatorCatalogReadAsync( string requestId, string entity, string operation, Func> operationBody) { var requestCancellation = CancellationTokenSource.CreateLinkedTokenSource( _lifetimeCancellation.Token); var requestToken = requestCancellation.Token; var browserGeneration = Volatile.Read(ref _operatorCatalogBrowserGeneration); var request = new OperatorCatalogReadRequest( $"{entity}:{operation}", requestId, entity, operation, browserGeneration, requestCancellation); RegisterOperatorCatalogRead(request); var databaseActivityEntered = false; try { if (Volatile.Read(ref _playoutCommandInFlight) != 0) { requestCancellation.Cancel(); PostOperatorCatalogReadCancellationIfCurrent( request, "PLAYOUT_PRIORITY", "The catalog read was canceled because playout has database priority."); return; } await _databaseActivityGate.WaitAsync(requestToken); databaseActivityEntered = true; requestToken.ThrowIfCancellationRequested(); if (Volatile.Read(ref _playoutCommandInFlight) != 0) { requestCancellation.Cancel(); PostOperatorCatalogReadCancellationIfCurrent( request, "PLAYOUT_PRIORITY", "The catalog read was canceled because playout has database priority."); return; } if (browserGeneration != Volatile.Read(ref _operatorCatalogBrowserGeneration)) { requestCancellation.Cancel(); return; } var reply = await operationBody(requestToken); requestToken.ThrowIfCancellationRequested(); if (browserGeneration != Volatile.Read(ref _operatorCatalogBrowserGeneration) || !TryCompleteOperatorCatalogReadIfCurrent(request)) { return; } PostMessage(reply.MessageType, reply.Payload); } catch (OperationCanceledException) when (requestToken.IsCancellationRequested) { PostOperatorCatalogReadCancellationIfCurrent( request, "PLAYOUT_PRIORITY", "The catalog read was canceled because playout has database priority."); } catch (DatabaseOperationException) when (requestToken.IsCancellationRequested) { PostOperatorCatalogReadCancellationIfCurrent( request, "PLAYOUT_PRIORITY", "The catalog read was canceled because playout has database priority."); } catch (Exception) when (requestToken.IsCancellationRequested) { PostOperatorCatalogReadCancellationIfCurrent( request, "PLAYOUT_PRIORITY", "The catalog read was canceled because playout has database priority."); } catch (ArgumentException) { PostOperatorCatalogReadErrorIfCurrent( request, "INVALID_REQUEST", "The theme/expert catalog request is invalid.", retryable: false); } catch (ThemeSelectionDataException) { PostOperatorCatalogReadErrorIfCurrent( request, "INVALID_DATA", "Theme catalog data is ambiguous or invalid.", retryable: false); } catch (ExpertSelectionDataException) { PostOperatorCatalogReadErrorIfCurrent( request, "INVALID_DATA", "Expert catalog data is ambiguous or invalid.", retryable: false); } catch (OperatorCatalogSchemaException) { PostOperatorCatalogReadErrorIfCurrent( request, "INVALID_SCHEMA", "The theme/expert database schema does not match the migrated contract.", retryable: false); } catch (DatabaseOperationException exception) { PostOperatorCatalogReadErrorIfCurrent( request, "DATABASE_ERROR", exception.Message, retryable: true); _ = PostDatabaseStatusAsync(_lifetimeCancellation.Token); } catch { PostOperatorCatalogReadErrorIfCurrent( request, "DATABASE_ERROR", "The theme/expert database read failed.", retryable: true); _ = PostDatabaseStatusAsync(_lifetimeCancellation.Token); } finally { if (databaseActivityEntered) { _databaseActivityGate.Release(); } ReleaseOperatorCatalogRead(request); requestCancellation.Dispose(); } } private async Task ExecuteOperatorCatalogWriteAsync( string requestId, string entity, string operation, string identityCode, bool canMutate, Func> operationBody) { if (!canMutate) { PostOperatorCatalogUnavailable(requestId, entity, operation, identityCode); return; } if (IsOperatorCatalogWriteQuarantined()) { PostOperatorCatalogQuarantined(requestId, entity, operation, identityCode); return; } if (!await _operatorCatalogWriteGate.WaitAsync(0)) { PostOperatorCatalogError( requestId, entity, operation, identityCode, "WRITE_BUSY", "Another theme/expert write is already running.", retryable: false, outcomeUnknown: false); return; } var playoutGateEntered = false; var databaseActivityEntered = false; var mutationDispatched = false; var requestCancellation = CancellationTokenSource.CreateLinkedTokenSource( _lifetimeCancellation.Token); var requestToken = requestCancellation.Token; var browserGeneration = Volatile.Read(ref _operatorCatalogBrowserGeneration); Volatile.Write(ref _operatorCatalogWriteCancellation, requestCancellation); try { playoutGateEntered = await _playoutCommandGate.WaitAsync(0, requestToken); if (!playoutGateEntered) { PostOperatorCatalogPlayoutActive( requestId, entity, operation, identityCode); return; } if (!CanStartOperatorCatalogWrite()) { PostOperatorCatalogPlayoutActive( requestId, entity, operation, identityCode); return; } databaseActivityEntered = await _databaseActivityGate.WaitAsync(0, requestToken); if (!databaseActivityEntered) { PostOperatorCatalogError( requestId, entity, operation, identityCode, "DATABASE_BUSY", "The database is busy. Refresh before making a new explicit write request.", retryable: false, outcomeUnknown: false); return; } requestToken.ThrowIfCancellationRequested(); if (!CanStartOperatorCatalogWrite()) { PostOperatorCatalogPlayoutActive( requestId, entity, operation, identityCode); return; } if (browserGeneration != Volatile.Read(ref _operatorCatalogBrowserGeneration)) { requestCancellation.Cancel(); return; } mutationDispatched = true; Interlocked.Exchange(ref _operatorCatalogWriteInFlight, 1); var receipt = await operationBody(requestToken); if (browserGeneration != Volatile.Read(ref _operatorCatalogBrowserGeneration) || !ReferenceEquals( Volatile.Read(ref _operatorCatalogWriteCancellation), requestCancellation)) { LatchOperatorCatalogWriteQuarantine(OperatorCatalogQuarantineMessage); return; } PostMessage("operator-catalog-mutation-results", new { requestId, entity, operation, identityCode, operationId = receipt.OperationId, receipt.CommittedAt, writeQuarantined = IsOperatorCatalogWriteQuarantined() }); } catch (OperationCanceledException) when (_lifetimeCancellation.IsCancellationRequested) { if (mutationDispatched) { LatchOperatorCatalogWriteQuarantine(OperatorCatalogQuarantineMessage); } } catch (OperatorCatalogConflictException) { PostOperatorCatalogWriteErrorIfCurrent( requestId, entity, operation, identityCode, "CONFLICT", "The catalog changed or a code/name is already in use. Refresh before a new edit.", outcomeUnknown: false, requestCancellation); } catch (OperatorCatalogMutationException exception) { if (exception.OutcomeUnknown) { LatchOperatorCatalogWriteQuarantine(OperatorCatalogQuarantineMessage); PostOperatorCatalogWriteErrorIfCurrent( requestId, entity, operation, identityCode, "OUTCOME_UNKNOWN", OperatorCatalogQuarantineMessage, outcomeUnknown: true, requestCancellation); } else { PostOperatorCatalogWriteErrorIfCurrent( requestId, entity, operation, identityCode, "WRITE_FAILED", "The database transaction rolled back and was not retried.", outcomeUnknown: false, requestCancellation); } } catch (ArgumentException) { PostOperatorCatalogWriteErrorIfCurrent( requestId, entity, operation, identityCode, "INVALID_REQUEST", "The theme/expert write request is invalid.", outcomeUnknown: false, requestCancellation); } catch (OperationCanceledException) { if (mutationDispatched) { LatchOperatorCatalogWriteQuarantine(OperatorCatalogQuarantineMessage); PostOperatorCatalogWriteErrorIfCurrent( requestId, entity, operation, identityCode, "OUTCOME_UNKNOWN", OperatorCatalogQuarantineMessage, outcomeUnknown: true, requestCancellation); } else { PostOperatorCatalogWriteErrorIfCurrent( requestId, entity, operation, identityCode, "CANCELLED", "The write stopped before a database transaction began.", outcomeUnknown: false, requestCancellation); } } catch { if (mutationDispatched) { LatchOperatorCatalogWriteQuarantine(OperatorCatalogQuarantineMessage); } PostOperatorCatalogWriteErrorIfCurrent( requestId, entity, operation, identityCode, mutationDispatched ? "OUTCOME_UNKNOWN" : "WRITE_FAILED", mutationDispatched ? OperatorCatalogQuarantineMessage : "The write failed before a database transaction began.", outcomeUnknown: mutationDispatched, requestCancellation); } finally { Interlocked.Exchange(ref _operatorCatalogWriteInFlight, 0); if (databaseActivityEntered) { _databaseActivityGate.Release(); } if (playoutGateEntered) { _playoutCommandGate.Release(); } Interlocked.CompareExchange( ref _operatorCatalogWriteCancellation, null, requestCancellation); requestCancellation.Dispose(); _operatorCatalogWriteGate.Release(); } } private bool CanStartOperatorCatalogWrite() { if (Volatile.Read(ref _playoutCommandInFlight) != 0 || Volatile.Read(ref _playoutShutdownStarted) != 0 || IsBrowserCorrelationQuarantined() || IsOperatorCatalogWriteQuarantined()) { return false; } return CanStartOperatorMutation(IsOperatorCatalogWriteQuarantined()); } private void RegisterOperatorCatalogRead(OperatorCatalogReadRequest request) { OperatorCatalogReadRequest? superseded; var shouldPostSuperseded = false; lock (_operatorCatalogReadLanesGate) { _operatorCatalogReadLanes.TryGetValue(request.Lane, out superseded); _operatorCatalogReadLanes[request.Lane] = request; shouldPostSuperseded = superseded?.TryComplete() == true; } if (superseded is null) { return; } CancelRequest(superseded.Cancellation); if (shouldPostSuperseded) { PostOperatorCatalogError( superseded.RequestId, superseded.Entity, superseded.Operation, string.Empty, "CANCELLED", "The catalog read was canceled because a newer request replaced it.", retryable: true, outcomeUnknown: false); } } private bool TryCompleteOperatorCatalogReadIfCurrent(OperatorCatalogReadRequest request) { lock (_operatorCatalogReadLanesGate) { return request.BrowserGeneration == Volatile.Read(ref _operatorCatalogBrowserGeneration) && _operatorCatalogReadLanes.TryGetValue(request.Lane, out var current) && ReferenceEquals(current, request) && request.TryComplete(); } } private void ReleaseOperatorCatalogRead(OperatorCatalogReadRequest request) { lock (_operatorCatalogReadLanesGate) { if (_operatorCatalogReadLanes.TryGetValue(request.Lane, out var current) && ReferenceEquals(current, request)) { _operatorCatalogReadLanes.Remove(request.Lane); } } } private OperatorCatalogReadRequest[] SnapshotOperatorCatalogReads(bool clear) { lock (_operatorCatalogReadLanesGate) { var requests = _operatorCatalogReadLanes.Values.ToArray(); if (clear) { _operatorCatalogReadLanes.Clear(); foreach (var request in requests) { _ = request.TryComplete(); } } return requests; } } private void PostOperatorCatalogReadCancellationIfCurrent( OperatorCatalogReadRequest request, string code, string message) => PostOperatorCatalogReadErrorIfCurrent( request, code, message, retryable: true); private void PostOperatorCatalogReadErrorIfCurrent( OperatorCatalogReadRequest request, string code, string message, bool retryable) { if (_lifetimeCancellation.IsCancellationRequested || !TryCompleteOperatorCatalogReadIfCurrent(request)) { return; } PostOperatorCatalogError( request.RequestId, request.Entity, request.Operation, string.Empty, code, message, retryable, outcomeUnknown: false); } private void PostOperatorCatalogWriteErrorIfCurrent( string requestId, string entity, string operation, string identityCode, string code, string message, bool outcomeUnknown, CancellationTokenSource requestCancellation) { if (_lifetimeCancellation.IsCancellationRequested || !ReferenceEquals( Volatile.Read(ref _operatorCatalogWriteCancellation), requestCancellation)) { return; } PostOperatorCatalogError( requestId, entity, operation, identityCode, code, message, retryable: false, outcomeUnknown); } private void PostOperatorCatalogInvalidRequest( string requestId, string entity, string operation) => PostOperatorCatalogError( requestId, entity, operation, string.Empty, "INVALID_REQUEST", "The theme/expert catalog request is invalid.", retryable: false, outcomeUnknown: false); private void PostOperatorCatalogUnavailable( string requestId, string entity, string operation, string identityCode = "") => PostOperatorCatalogError( requestId, entity, operation, identityCode, "DATABASE_UNAVAILABLE", _operatorCatalogInitializationError ?? _databaseInitializationError ?? "The theme/expert database boundary is unavailable.", retryable: false, outcomeUnknown: false); private void PostOperatorCatalogPlayoutActive( string requestId, string entity, string operation, string identityCode) => PostOperatorCatalogError( requestId, entity, operation, identityCode, "PLAYOUT_ACTIVE", "Theme/expert database writes are allowed only while playout is fully IDLE.", retryable: false, outcomeUnknown: false); private void PostOperatorCatalogQuarantined( string requestId, string entity, string operation, string identityCode) => PostOperatorCatalogError( requestId, entity, operation, identityCode, "OUTCOME_UNKNOWN", GetOperatorMutationQuarantineMessage( Volatile.Read(ref _operatorCatalogWriteQuarantineMessage) ?? OperatorCatalogQuarantineMessage), retryable: false, outcomeUnknown: true); private void PostOperatorCatalogError( string requestId, string entity, string operation, string identityCode, string code, string message, bool retryable, bool outcomeUnknown) { PostMessage("operator-catalog-error", new { requestId, entity, operation, identityCode, code, message, retryable = retryable && !outcomeUnknown, outcomeUnknown, writeQuarantined = IsOperatorCatalogWriteQuarantined() }); } private bool CanMutateThemeCatalog() => _themeCatalogPersistenceService?.CanMutate == true && !IsOperatorCatalogWriteQuarantined(); private bool CanMutateExpertCatalog() => _expertCatalogPersistenceService?.CanMutate == true && !IsOperatorCatalogWriteQuarantined(); private bool IsOperatorCatalogWriteQuarantined() => Volatile.Read(ref _operatorCatalogWriteQuarantined) != 0 || IsOperatorMutationQuarantined(); private void LatchOperatorCatalogWriteQuarantine(string message) { LatchOperatorMutationQuarantine(message); if (Interlocked.CompareExchange(ref _operatorCatalogWriteQuarantined, 1, 0) == 0) { Volatile.Write( ref _operatorCatalogWriteQuarantineMessage, string.IsNullOrWhiteSpace(message) ? OperatorCatalogQuarantineMessage : message); } } /// /// Root browser-navigation/process-failure hook. Losing JavaScript request /// correlation while a write is active permanently quarantines writes for /// this process and never retries the command. /// private void InvalidateOperatorCatalogBrowserRequests() { Interlocked.Increment(ref _operatorCatalogBrowserGeneration); if (Volatile.Read(ref _operatorCatalogWriteInFlight) != 0) { LatchOperatorCatalogWriteQuarantine(OperatorCatalogQuarantineMessage); } foreach (var request in SnapshotOperatorCatalogReads(clear: true)) { CancelRequest(request.Cancellation); } CancelRequest(Interlocked.Exchange(ref _operatorCatalogWriteCancellation, null)); } /// /// Root playout-priority hook. A catalog write owns the playout gate and /// therefore cannot reach this point concurrently; only read work is canceled. /// private void CancelOperatorCatalogReadsForPlayout() { foreach (var request in SnapshotOperatorCatalogReads(clear: false)) { CancelRequest(request.Cancellation); } } /// Root window-close hook. private void ShutdownOperatorCatalogRuntime() { if (Volatile.Read(ref _operatorCatalogWriteInFlight) != 0) { LatchOperatorCatalogWriteQuarantine(OperatorCatalogQuarantineMessage); } foreach (var request in SnapshotOperatorCatalogReads(clear: true)) { CancelRequest(request.Cancellation); } CancelRequest(Interlocked.Exchange(ref _operatorCatalogWriteCancellation, null)); _themeCatalogPersistenceService = null; _expertCatalogPersistenceService = null; _operatorCatalogSchemaValidationService = null; } private static bool TryParseThemeCatalogListRequest( JsonElement payload, out string requestId, out string query, out ThemeSession nxtSession, out int maximumResults) { requestId = string.Empty; query = string.Empty; nxtSession = default; maximumResults = LegacyThemeSelectionService.DefaultMaximumResults; return payload.ValueKind == JsonValueKind.Object && HasOnlyProperties(payload, "requestId", "query", "nxtSession", "maximumResults") && TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) && TryGetOperatorCatalogQuery( payload, "query", LegacyThemeSelectionService.MaximumQueryLength, out query) && TryParseThemeNxtSession(GetString(payload, "nxtSession"), out nxtSession) && TryGetOptionalOperatorCatalogLimit( payload, LegacyThemeSelectionService.MaximumResults, ref maximumResults); } private static bool TryParseExpertCatalogListRequest( JsonElement payload, out string requestId, out string query, out int maximumResults) { requestId = string.Empty; query = string.Empty; maximumResults = LegacyExpertSelectionService.DefaultMaximumResults; return payload.ValueKind == JsonValueKind.Object && HasOnlyProperties(payload, "requestId", "query", "maximumResults") && TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) && TryGetOperatorCatalogQuery( payload, "query", LegacyExpertSelectionService.MaximumQueryLength, out query) && TryGetOptionalOperatorCatalogLimit( payload, LegacyExpertSelectionService.MaximumResults, ref maximumResults); } private static bool TryParseOperatorCatalogRequestIdOnly( JsonElement payload, out string requestId) { requestId = string.Empty; return payload.ValueKind == JsonValueKind.Object && HasOnlyProperties(payload, "requestId") && TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId); } private static bool TryParseThemeCatalogNextCodeRequest( JsonElement payload, out string requestId, out ThemeMarket market) { requestId = string.Empty; market = default; return payload.ValueKind == JsonValueKind.Object && HasOnlyProperties(payload, "requestId", "market") && TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) && TryParseThemeMarket(GetString(payload, "market"), out market); } private static bool TryParseThemeCatalogCreateRequest( JsonElement payload, out string requestId, out ThemeCatalogDefinition definition) { requestId = string.Empty; definition = null!; if (payload.ValueKind != JsonValueKind.Object || !HasOnlyProperties(payload, "requestId", "draft") || !TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) || !payload.TryGetProperty("draft", out var draft) || draft.ValueKind != JsonValueKind.Object || !HasOnlyProperties(draft, "market", "themeCode", "themeTitle", "items") || !TryParseThemeMarket(GetString(draft, "market"), out var market) || !TryGetOperatorCatalogCode(draft, "themeCode", 8, out var code) || !TryGetOperatorCatalogText(draft, "themeTitle", 128, out var title) || market == ThemeMarket.Nxt && title.Contains("(NXT)", StringComparison.OrdinalIgnoreCase) || !TryParseThemeCatalogItems(draft, market, out var items)) { return false; } definition = new ThemeCatalogDefinition(market, code, title, items); return true; } private static bool TryParseThemeCatalogReplaceRequest( JsonElement payload, out string requestId, out ThemeSelectionIdentity expectedIdentity, out string newTitle, out IReadOnlyList items) { requestId = string.Empty; expectedIdentity = null!; newTitle = string.Empty; items = Array.Empty(); if (payload.ValueKind != JsonValueKind.Object || !HasOnlyProperties(payload, "requestId", "expectedIdentity", "newTitle", "items") || !TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) || !payload.TryGetProperty("expectedIdentity", out var identityElement) || !TryParseThemeCatalogIdentity(identityElement, out expectedIdentity) || !TryGetOperatorCatalogText(payload, "newTitle", 128, out newTitle) || expectedIdentity.Market == ThemeMarket.Nxt && newTitle.Contains("(NXT)", StringComparison.OrdinalIgnoreCase) || !TryParseThemeCatalogItems(payload, expectedIdentity.Market, out items)) { return false; } return true; } private static bool TryParseThemeCatalogDeleteRequest( JsonElement payload, out string requestId, out ThemeSelectionIdentity identity) { requestId = string.Empty; identity = null!; return payload.ValueKind == JsonValueKind.Object && HasOnlyProperties(payload, "requestId", "identity") && TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) && payload.TryGetProperty("identity", out var identityElement) && TryParseThemeCatalogIdentity(identityElement, out identity); } private static bool TryParseThemeCatalogIdentity( JsonElement element, out ThemeSelectionIdentity identity) { identity = null!; if (element.ValueKind != JsonValueKind.Object || !HasOnlyProperties(element, "market", "session", "themeCode", "themeTitle") || !TryParseThemeMarket(GetString(element, "market"), out var market) || !TryParseThemeSession(GetString(element, "session"), out var session) || market == ThemeMarket.Krx && session != ThemeSession.NotApplicable || market == ThemeMarket.Nxt && session is not (ThemeSession.PreMarket or ThemeSession.AfterMarket) || !TryGetOperatorCatalogCode(element, "themeCode", 8, out var code) || !TryGetOperatorCatalogText(element, "themeTitle", 128, out var title) || market == ThemeMarket.Nxt && title.Contains("(NXT)", StringComparison.OrdinalIgnoreCase)) { return false; } identity = new ThemeSelectionIdentity(market, session, code, title); return true; } private static bool TryParseThemeCatalogItems( JsonElement parent, ThemeMarket market, out IReadOnlyList items) { items = Array.Empty(); if (!parent.TryGetProperty("items", out var array) || array.ValueKind != JsonValueKind.Array || array.GetArrayLength() > LegacyThemeCatalogPersistenceService.MaximumItems) { return false; } var parsed = new List(array.GetArrayLength()); var codes = new HashSet(StringComparer.Ordinal); var names = new HashSet(StringComparer.Ordinal); var expectedIndex = 0; foreach (var element in array.EnumerateArray()) { if (element.ValueKind != JsonValueKind.Object || !HasOnlyProperties(element, "inputIndex", "itemCode", "itemName") || !element.TryGetProperty("inputIndex", out var indexElement) || indexElement.ValueKind != JsonValueKind.Number || !indexElement.TryGetInt32(out var inputIndex) || inputIndex != expectedIndex || !TryGetOperatorCatalogText(element, "itemCode", 13, out var itemCode) || !IsThemeItemCodeForMarket(itemCode, market) || !TryGetOperatorCatalogText(element, "itemName", 200, out var itemName) || !codes.Add(itemCode) || !names.Add(itemName)) { return false; } parsed.Add(new ThemeCatalogItem(inputIndex, itemCode, itemName)); expectedIndex++; } items = parsed.AsReadOnly(); return true; } private static bool TryParseExpertCatalogCreateRequest( JsonElement payload, out string requestId, out ExpertCatalogDefinition definition) { requestId = string.Empty; definition = null!; if (payload.ValueKind != JsonValueKind.Object || !HasOnlyProperties(payload, "requestId", "draft") || !TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) || !payload.TryGetProperty("draft", out var draft) || draft.ValueKind != JsonValueKind.Object || !HasOnlyProperties(draft, "expertCode", "expertName", "recommendations") || !TryGetOperatorCatalogCode(draft, "expertCode", 4, out var code) || !TryGetOperatorCatalogText(draft, "expertName", 128, out var name) || !TryParseExpertCatalogRecommendations(draft, out var recommendations)) { return false; } definition = new ExpertCatalogDefinition( new ExpertSelectionIdentity(code, name), recommendations); return true; } private static bool TryParseExpertCatalogReplaceRequest( JsonElement payload, out string requestId, out ExpertSelectionIdentity expectedIdentity, out string newName, out IReadOnlyList recommendations) { requestId = string.Empty; expectedIdentity = null!; newName = string.Empty; recommendations = Array.Empty(); if (payload.ValueKind != JsonValueKind.Object || !HasOnlyProperties( payload, "requestId", "expectedIdentity", "newName", "recommendations") || !TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) || !payload.TryGetProperty("expectedIdentity", out var identityElement) || !TryParseExpertCatalogIdentity(identityElement, out expectedIdentity) || !TryGetOperatorCatalogText(payload, "newName", 128, out newName) || !TryParseExpertCatalogRecommendations(payload, out recommendations)) { return false; } return true; } private static bool TryParseExpertCatalogDeleteRequest( JsonElement payload, out string requestId, out ExpertSelectionIdentity identity) { requestId = string.Empty; identity = null!; return payload.ValueKind == JsonValueKind.Object && HasOnlyProperties(payload, "requestId", "identity") && TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) && payload.TryGetProperty("identity", out var identityElement) && TryParseExpertCatalogIdentity(identityElement, out identity); } private static bool TryParseExpertCatalogIdentity( JsonElement element, out ExpertSelectionIdentity identity) { identity = null!; if (element.ValueKind != JsonValueKind.Object || !HasOnlyProperties(element, "expertCode", "expertName") || !TryGetOperatorCatalogCode(element, "expertCode", 4, out var code) || !TryGetOperatorCatalogText(element, "expertName", 128, out var name)) { return false; } identity = new ExpertSelectionIdentity(code, name); return true; } private static bool TryParseExpertCatalogRecommendations( JsonElement parent, out IReadOnlyList recommendations) { recommendations = Array.Empty(); if (!parent.TryGetProperty("recommendations", out var array) || array.ValueKind != JsonValueKind.Array || array.GetArrayLength() > LegacyExpertCatalogPersistenceService.MaximumRecommendations) { return false; } var parsed = new List(array.GetArrayLength()); var codes = new HashSet(StringComparer.Ordinal); var names = new HashSet(StringComparer.Ordinal); var expectedIndex = 0; foreach (var element in array.EnumerateArray()) { if (element.ValueKind != JsonValueKind.Object || !HasOnlyProperties( element, "playIndex", "stockCode", "stockName", "buyAmount") || !element.TryGetProperty("playIndex", out var indexElement) || indexElement.ValueKind != JsonValueKind.Number || !indexElement.TryGetInt32(out var playIndex) || playIndex != expectedIndex || !TryGetOperatorCatalogText(element, "stockCode", 32, out var stockCode) || !stockCode.All(char.IsAsciiLetterOrDigit) || !TryGetOperatorCatalogText(element, "stockName", 200, out var stockName) || !element.TryGetProperty("buyAmount", out var amountElement) || amountElement.ValueKind != JsonValueKind.Number || !amountElement.TryGetInt64(out var amount) || amount <= 0 || amount > OperatorCatalogMaximumWebSafeInteger || !codes.Add(stockCode) || !names.Add(stockName)) { return false; } parsed.Add(new ExpertCatalogRecommendation( playIndex, stockCode, stockName, amount)); expectedIndex++; } recommendations = parsed.AsReadOnly(); return true; } private static bool TryGetOptionalOperatorCatalogLimit( JsonElement payload, int maximum, ref int value) { if (!payload.TryGetProperty("maximumResults", out var element)) { return true; } return element.ValueKind == JsonValueKind.Number && element.TryGetInt32(out value) && value is >= 1 && value <= maximum; } private static bool TryGetOperatorCatalogQuery( JsonElement payload, string propertyName, int maximumLength, out string value) { value = GetString(payload, propertyName) ?? string.Empty; return payload.TryGetProperty(propertyName, out var element) && element.ValueKind == JsonValueKind.String && value.Length <= maximumLength && string.Equals(value, value.Trim(), StringComparison.Ordinal) && IsSafeOperatorCatalogText(value); } private static bool TryGetOperatorCatalogCode( JsonElement payload, string propertyName, int length, out string value) { value = GetString(payload, propertyName) ?? string.Empty; return value.Length == length && value.All(char.IsAsciiDigit); } private static bool TryGetOperatorCatalogText( JsonElement payload, string propertyName, int maximumLength, out string value) { value = GetString(payload, propertyName) ?? string.Empty; return payload.TryGetProperty(propertyName, out var element) && element.ValueKind == JsonValueKind.String && value.Length is > 0 && value.Length <= maximumLength && string.Equals(value, value.Trim(), StringComparison.Ordinal) && IsSafeOperatorCatalogText(value); } private static bool IsSafeOperatorCatalogText(string value) { foreach (var character in value) { var category = CharUnicodeInfo.GetUnicodeCategory(character); if (char.IsControl(character) || char.IsSurrogate(character) || character == '\uFFFD' || category is UnicodeCategory.Format or UnicodeCategory.LineSeparator or UnicodeCategory.ParagraphSeparator) { return false; } } return true; } private static bool IsThemeItemCodeForMarket(string itemCode, ThemeMarket market) { if (itemCode.Length is < 2 or > 13 || !itemCode.AsSpan(1).ToArray().All(char.IsAsciiLetterOrDigit)) { return false; } return market switch { ThemeMarket.Krx => itemCode[0] is 'P' or 'D', ThemeMarket.Nxt => itemCode[0] is 'X' or 'T', _ => false }; } private static string SafeOperatorCatalogRequestId(JsonElement payload) { return payload.ValueKind == JsonValueKind.Object && TryGetSafeToken( payload, "requestId", MaximumPlayoutRequestIdLength, out var requestId) ? requestId : string.Empty; } private readonly record struct OperatorCatalogBridgeReply(string MessageType, object Payload); private sealed class OperatorCatalogReadRequest { private int _terminalState; public OperatorCatalogReadRequest( string lane, string requestId, string entity, string operation, int browserGeneration, CancellationTokenSource cancellation) { Lane = lane; RequestId = requestId; Entity = entity; Operation = operation; BrowserGeneration = browserGeneration; Cancellation = cancellation; } public string Lane { get; } public string RequestId { get; } public string Entity { get; } public string Operation { get; } public int BrowserGeneration { get; } public CancellationTokenSource Cancellation { get; } public bool TryComplete() => Interlocked.CompareExchange(ref _terminalState, 1, 0) == 0; } }