From 29a9cb38739bb9130e0ce2db9bcff4da3bcac21e Mon Sep 17 00:00:00 2001 From: Allamagoosa <36551512+dmatlock171@users.noreply.github.com> Date: Fri, 21 Aug 2026 06:33:31 -0700 Subject: [PATCH] Fix the sidebar entry, a test race, and comments left pointing at deleted members The docs page was linked from docs/index.md but missing from the VitePress sidebar, where every sibling feature page is listed. In the dispatch test fake, Started.Enqueue ran before the counter incremented, so WaitForStarted could return in the gap and the HighWaterMark assertion could observe one book fewer than had started. Comments still named TrackedQueue.Current and .Active after both were deleted, the Configuration doc comment still called itself the bound on a spinner when nothing binds it any more, and the ClaimAbort comment said the winner was the book that answered the dialog when it is really whichever book gets there first. --- .vitepress/config.js | 4 ++++ .../Views/ProcessQueueControl.axaml.cs | 4 ++-- .../Configuration.PersistentSettings.cs | 9 +++++---- .../ProcessQueue/ProcessQueueViewModel.cs | 18 ++++++++++-------- .../ProcessQueueDispatchTests.cs | 6 +++++- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/.vitepress/config.js b/.vitepress/config.js index bbb59d24..2a06e74a 100644 --- a/.vitepress/config.js +++ b/.vitepress/config.js @@ -99,6 +99,10 @@ export default defineConfig({ link: "/docs/features/daily-download-limit", }, { text: "Naming Templates", link: "/docs/features/naming-templates" }, + { + text: "Parallel Downloads", + link: "/docs/features/parallel-downloads", + }, { text: "Retrying Refused Downloads", link: "/docs/features/retrying-refused-downloads", diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs index 7969640d..1cc6752b 100644 --- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs +++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs @@ -155,8 +155,8 @@ public partial class ProcessQueueControl : UserControl public async void CancelAllBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { - // Cancels every active book, not just Current - with parallel downloads there - // can be several running at once. + // Cancels every active book, not just the first one - with parallel downloads + // there can be several running at once. if (_viewModel is ProcessQueueViewModel vm) await vm.CancelAllAsync(); } diff --git a/Source/LibationFileManager/Configuration.PersistentSettings.cs b/Source/LibationFileManager/Configuration.PersistentSettings.cs index 462cf3ed..e4f87bac 100644 --- a/Source/LibationFileManager/Configuration.PersistentSettings.cs +++ b/Source/LibationFileManager/Configuration.PersistentSettings.cs @@ -431,10 +431,11 @@ public partial class Configuration public const int DefaultConcurrentDownloads = 3; /// - /// The highest is worth running on this machine, for use as - /// the bound on a spinner. Deliberately not applied to the stored value: a setting saved - /// on an eight-core desktop must survive being opened on a two-core laptop or in a container and - /// come back intact, rather than being silently rewritten to what that machine could manage. + /// The highest is worth running on this machine. Applied only + /// where it bites - when the queue decides how many books to start - and deliberately not + /// to the stored value or to any spinner's bound: a setting saved on an eight-core desktop must + /// survive being opened on a two-core laptop or in a container and come back intact, rather than + /// being silently rewritten to what that machine could manage. /// /// /// Processor count is also not the default. Downloading is bound by Audible's license throttling diff --git a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs index 7e624253..e6104ab5 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs @@ -152,8 +152,8 @@ public class ProcessQueueViewModel : ReactiveObject : 0; config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024); - // Apply to all currently active books. Over a copy: the speed limit is changed from the UI - // thread while book tasks start and finish, and Active is the live list. + // Apply to all currently active books. Over a copy, because the speed limit is changed from + // the UI thread while book tasks start and finish, which mutates the queue's active list. foreach (var activeBook in Queue.GetActive().OfType()) activeBook.Configuration.DownloadSpeedLimit = config.DownloadSpeedLimit; @@ -717,10 +717,12 @@ public class ProcessQueueViewModel : ReactiveObject var result = await ProcessBookHandler(book); - // Claimed before the queue is touched, and before the logging. Only the book that - // actually answered Abort tears the queue down: Abort is a session-wide override now, so - // every book in flight arrives here - directly or by inheriting that answer - and each one - // re-entering CancelAllAsync would have every book asking every other book to cancel. + // Claimed before the queue is touched, and before the logging. Exactly one book tears the + // queue down: Abort is a session-wide override now, so every book in flight arrives here - + // directly or by inheriting that answer - and each one re-entering CancelAllAsync would + // have every book asking every other book to cancel. The winner is whichever book reaches + // this line first, not necessarily the one whose dialog was answered; they are + // interchangeable here, and racing to identify the answering book would buy nothing. // Claiming this early also keeps the window small in which the loop can start another // book, which would then outlive the abort by starting after CancelAllAsync snapshots // what to cancel. @@ -744,8 +746,8 @@ public class ProcessQueueViewModel : ReactiveObject await CancelAllAsync(book); else { - // Inherited the abort rather than answering it. It was cancelled by the book - // that did, and that is what it should report. + // Inherited the abort rather than triggering the teardown. It was cancelled by + // the book that did, and that is what it should report. book.Result = ProcessBookResult.Cancelled; book.Status = ProcessBookStatus.Cancelled; } diff --git a/Source/_Tests/LibationUiBase.Tests/ProcessQueueDispatchTests.cs b/Source/_Tests/LibationUiBase.Tests/ProcessQueueDispatchTests.cs index 324fa3ee..09a72a0e 100644 --- a/Source/_Tests/LibationUiBase.Tests/ProcessQueueDispatchTests.cs +++ b/Source/_Tests/LibationUiBase.Tests/ProcessQueueDispatchTests.cs @@ -70,8 +70,10 @@ public class ProcessQueueDispatchTests public Task Handle(ProcessBookViewModel book) { var asin = book.LibraryBook.Book.AudibleProductId.ToString()!; - Started.Enqueue(asin); + // Count first, then publish. WaitForStarted polls Started, so enqueuing before the + // counter moves lets a test wake up in the gap and assert a HighWaterMark that is one + // short of the books it just waited for. lock (countLock) { running++; @@ -79,6 +81,8 @@ public class ProcessQueueDispatchTests HighWaterMark = running; } + Started.Enqueue(asin); + return GateFor(asin).Task.ContinueWith(t => { lock (countLock) running--;