From f02f370ed9d1f78e2a85b89caef925579edb2927 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Mon, 15 Jun 2026 12:47:57 -0400 Subject: [PATCH] fix(detail): reload track on route-param change so Play uses the right track Detail pages loaded only in OnInitialized, which doesn't re-run when an InteractiveAuto component instance is reused across same-template navigations, leaving a stale track that Play streamed. Move load to OnParametersSetAsync keyed on the route id, and guard the prerender bridge restore against an id mismatch. --- .../Pages/ReleaseDetailBase.cs | 27 ++++++++++++++++--- .../Pages/TrackDetail.razor.cs | 27 ++++++++++++++++--- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/DeepDrftPublic.Client/Pages/ReleaseDetailBase.cs b/DeepDrftPublic.Client/Pages/ReleaseDetailBase.cs index ec4b824..e3e4615 100644 --- a/DeepDrftPublic.Client/Pages/ReleaseDetailBase.cs +++ b/DeepDrftPublic.Client/Pages/ReleaseDetailBase.cs @@ -19,16 +19,32 @@ public abstract class ReleaseDetailBase : ComponentBase, IDisposable private PersistingComponentStateSubscription _persistingSubscription; + // The release id the ViewModel currently holds. Tracks param-only navigations (e.g. + // /mixes/5 -> /mixes/8) which reuse this component instance and fire OnParametersSet + // without re-running OnInitialized — without this, the page would keep the prior + // release's track and Play would stream the wrong audio. + private long _loadedId; + private bool _loaded; + // Distinct keys per medium so a Session restore never lands on a Mix page. protected abstract string PersistKey { get; } - protected override async Task OnInitializedAsync() + protected override void OnInitialized() + => _persistingSubscription = PersistentState.RegisterOnPersisting(Persist); + + protected override async Task OnParametersSetAsync() { - _persistingSubscription = PersistentState.RegisterOnPersisting(Persist); + // Re-run whenever the route id changes. Component instances are reused across + // same-template navigations, so the load decision must live here, not in + // OnInitialized (which fires once per instance). + if (_loaded && _loadedId == Id) return; // The bridged payload carries both the release and its resolved track so the interactive - // pass renders identically without a second round-trip. - if (PersistentState.TryTakeFromJson(PersistKey, out var restored) && restored?.Release is not null) + // pass renders identically without a second round-trip. Guard on the id: a payload for a + // different release must not seed this page (stale-bridge bleed across navigation). + if (PersistentState.TryTakeFromJson(PersistKey, out var restored) + && restored?.Release is not null + && restored.Release.Id == Id) { ViewModel.Restore(restored.Release, restored.Track); } @@ -36,6 +52,9 @@ public abstract class ReleaseDetailBase : ComponentBase, IDisposable { await ViewModel.Load(Id); } + + _loadedId = Id; + _loaded = true; } private Task Persist() diff --git a/DeepDrftPublic.Client/Pages/TrackDetail.razor.cs b/DeepDrftPublic.Client/Pages/TrackDetail.razor.cs index 23280e2..3d39cb0 100644 --- a/DeepDrftPublic.Client/Pages/TrackDetail.razor.cs +++ b/DeepDrftPublic.Client/Pages/TrackDetail.razor.cs @@ -14,23 +14,42 @@ public partial class TrackDetail : ComponentBase, IDisposable private PersistingComponentStateSubscription _persistingSubscription; - protected override async Task OnInitializedAsync() - { + // The entry key the ViewModel currently holds. Tracks param-only navigations + // (e.g. /track/A -> /track/B) which reuse this component instance and fire + // OnParametersSet without re-running OnInitialized — without this, the page keeps + // the prior track and Play streams the wrong audio. + private string? _loadedEntryKey; + + protected override void OnInitialized() // Carry the prerendered track across the prerender -> interactive (WASM) seam. // Without this, the WASM pass gets a fresh scoped ViewModel, re-renders the // skeleton, and re-fetches. Mirror the TracksView bridge: persist on the way // out of prerender, restore on the interactive pass, and only fetch on a miss. - _persistingSubscription = PersistentState.RegisterOnPersisting(PersistTrack); + => _persistingSubscription = PersistentState.RegisterOnPersisting(PersistTrack); - if (PersistentState.TryTakeFromJson(PersistKey, out var restored) && restored is not null) + protected override async Task OnParametersSetAsync() + { + // Re-run whenever the route key changes. Component instances are reused across + // same-template navigations, so the load decision must live here, not in + // OnInitialized (which fires once per instance). + if (_loadedEntryKey == EntryKey) return; + + // Guard the bridge on the key: a payload for a different track must not seed this + // page (stale-bridge bleed across navigation). + if (PersistentState.TryTakeFromJson(PersistKey, out var restored) + && restored is not null + && restored.EntryKey == EntryKey) { ViewModel.Track = restored; + ViewModel.NotFound = false; ViewModel.IsLoading = false; } else { await ViewModel.Load(EntryKey); } + + _loadedEntryKey = EntryKey; } private Task PersistTrack()