using System.Text.Json; using MBN_STOCK_WEBVIEW.Core.Playout; using MBN_STOCK_WEBVIEW.Core.Playout.Scenes; using MBN_STOCK_WEBVIEW.Infrastructure; namespace MBN_STOCK_WEBVIEW; public sealed partial class MainWindow { private async Task HandlePlaylistPagePlansRequestAsync(JsonElement payload) { var parsed = PlayoutBridgeProtocol.ParsePagePlanRequest(payload); var request = parsed.Request; if (!parsed.IsValid) { PostPlaylistPagePlansError( request.RequestId, "INVALID_REQUEST", parsed.Error, retryable: false); return; } var workflow = _playoutWorkflow; var engine = _playoutEngine; if (workflow is null || engine is null) { PostPlaylistPagePlansError( request.RequestId, "PLAYOUT_UNAVAILABLE", _playoutInitializationError ?? "The playout workflow is not available for page preflight.", retryable: false); return; } var requestCancellation = CancellationTokenSource.CreateLinkedTokenSource( _lifetimeCancellation.Token); var requestToken = requestCancellation.Token; var browserGeneration = Volatile.Read(ref _playlistPagePlanBrowserGeneration); var previousCancellation = Interlocked.Exchange( ref _playlistPagePlanCancellation, requestCancellation); CancelRequest(previousCancellation); var databaseActivityEntered = false; try { if (Volatile.Read(ref _playoutCommandInFlight) != 0) { PostPlaylistPagePlansErrorIfCurrent( request.RequestId, "PLAYOUT_PRIORITY", "A playout command has priority over page preflight.", retryable: true, requestCancellation, browserGeneration); return; } if (!IsPlaylistPagePlanIdle(workflow, engine)) { PostPlaylistPagePlansErrorIfCurrent( request.RequestId, "PLAYOUT_NOT_IDLE", "Page preflight is available only while no scene is prepared or on air.", retryable: true, requestCancellation, browserGeneration); return; } await _databaseActivityGate.WaitAsync(requestToken); databaseActivityEntered = true; requestToken.ThrowIfCancellationRequested(); if (Volatile.Read(ref _playoutCommandInFlight) != 0) { PostPlaylistPagePlansErrorIfCurrent( request.RequestId, "PLAYOUT_PRIORITY", "A playout command cancelled page preflight before the database read.", retryable: true, requestCancellation, browserGeneration); return; } if (!IsPlaylistPagePlanIdle(workflow, engine)) { PostPlaylistPagePlansErrorIfCurrent( request.RequestId, "PLAYOUT_NOT_IDLE", "Playout became active before page preflight could start.", retryable: true, requestCancellation, browserGeneration); return; } var plans = await workflow.PreflightPagePlansAsync( request.Entries!, requestToken); requestToken.ThrowIfCancellationRequested(); if (!IsCurrentPlaylistPagePlanRequest(requestCancellation, browserGeneration)) { return; } PostMessage("playlist-page-plans-results", new { requestId = request.RequestId, calculatedAt = DateTimeOffset.UtcNow, plans = plans.Select(plan => new { entryId = plan.EntryId, itemCount = plan.ItemCount, pageSize = plan.PageSize, pageCount = plan.PageCount, accessibleItemCount = plan.AccessibleItemCount, isTruncated = plan.IsTruncated, builderKey = plan.BuilderKey }) }); } catch (OperationCanceledException) when (requestToken.IsCancellationRequested) { if (Volatile.Read(ref _playoutCommandInFlight) != 0) { PostPlaylistPagePlansErrorIfCurrent( request.RequestId, "PLAYOUT_PRIORITY", "A playout command cancelled the read-only page preflight.", retryable: true, requestCancellation, browserGeneration); } } catch (DatabaseOperationException exception) { PostPlaylistPagePlansErrorIfCurrent( request.RequestId, "DATABASE_UNAVAILABLE", exception.Message, retryable: true, requestCancellation, browserGeneration); } catch (LegacySceneDataException exception) { PostPlaylistPagePlansErrorIfCurrent( request.RequestId, "SCENE_DATA_INVALID", exception.Message, retryable: false, requestCancellation, browserGeneration); } catch (InvalidOperationException exception) { PostPlaylistPagePlansErrorIfCurrent( request.RequestId, "PLAYOUT_NOT_IDLE", exception.Message, retryable: true, requestCancellation, browserGeneration); } catch (Exception exception) { PostPlaylistPagePlansErrorIfCurrent( request.RequestId, "PAGE_PREFLIGHT_FAILED", exception.Message, retryable: false, requestCancellation, browserGeneration); } finally { if (databaseActivityEntered) { _databaseActivityGate.Release(); } Interlocked.CompareExchange( ref _playlistPagePlanCancellation, null, requestCancellation); requestCancellation.Dispose(); } } private static bool IsPlaylistPagePlanIdle( LegacyPlayoutWorkflow workflow, IPlayoutEngine engine) { var workflowState = workflow.State; var engineStatus = engine.Status; return string.IsNullOrWhiteSpace(workflowState.PreparedCutCode) && string.IsNullOrWhiteSpace(workflowState.OnAirCutCode) && string.IsNullOrWhiteSpace(engineStatus.PreparedSceneName) && string.IsNullOrWhiteSpace(engineStatus.OnAirSceneName); } private bool IsCurrentPlaylistPagePlanRequest( CancellationTokenSource requestCancellation, int browserGeneration) => ReferenceEquals( Volatile.Read(ref _playlistPagePlanCancellation), requestCancellation) && browserGeneration == Volatile.Read(ref _playlistPagePlanBrowserGeneration) && !_lifetimeCancellation.IsCancellationRequested; private void PostPlaylistPagePlansErrorIfCurrent( string requestId, string code, string message, bool retryable, CancellationTokenSource requestCancellation, int browserGeneration) { if (!IsCurrentPlaylistPagePlanRequest(requestCancellation, browserGeneration)) { return; } PostPlaylistPagePlansError(requestId, code, message, retryable); } private void PostPlaylistPagePlansError( string requestId, string code, string message, bool retryable) { PostMessage("playlist-page-plans-error", new { requestId, code, message, retryable }); } private void InvalidatePlaylistPagePlanBrowserRequest() { Interlocked.Increment(ref _playlistPagePlanBrowserGeneration); CancelRequest(Interlocked.Exchange(ref _playlistPagePlanCancellation, null)); } }