fix(ui): guard Bind/LoadForList against interleaved DbContext awaits
Added CancellationTokenSource per-load in both DetailsIslandViewModel and TasksIslandViewModel. Public entry points cancel any in-flight load before starting a new one. DB calls and collection mutations after awaits are guarded with ThrowIfCancellationRequested. DetailsIslandViewModel now sets _subscribedTaskId only after the DB confirms the entity exists, preventing the SignalR handler from routing messages to a stale load. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private ListNavItemViewModel? _currentList;
|
||||
private CancellationTokenSource? _loadCts;
|
||||
|
||||
public event EventHandler? SelectionChanged;
|
||||
public event EventHandler? FocusAddTaskRequested;
|
||||
@@ -30,8 +31,13 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
_dbFactory = dbFactory;
|
||||
}
|
||||
|
||||
public async void LoadForList(ListNavItemViewModel? list)
|
||||
public void LoadForList(ListNavItemViewModel? list)
|
||||
{
|
||||
_loadCts?.Cancel();
|
||||
_loadCts?.Dispose();
|
||||
_loadCts = new CancellationTokenSource();
|
||||
var ct = _loadCts.Token;
|
||||
|
||||
_currentList = list;
|
||||
Items.Clear();
|
||||
if (list is null) return;
|
||||
@@ -39,27 +45,38 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
HeaderTitle = list.Name;
|
||||
HeaderEyebrow = DateTime.Now.ToString("dddd · MMM dd").ToUpperInvariant();
|
||||
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var all = await db.Tasks
|
||||
.Include(t => t.List)
|
||||
.Include(t => t.Worktree)
|
||||
.ToListAsync();
|
||||
_ = LoadForListAsync(list, ct);
|
||||
}
|
||||
|
||||
IEnumerable<TaskEntity> filtered = list.Kind switch
|
||||
private async Task LoadForListAsync(ListNavItemViewModel list, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
ListKind.Smart when list.Id == "smart:my-day" => all.Where(t => t.IsMyDay),
|
||||
ListKind.Smart when list.Id == "smart:important" => all.Where(t => t.IsStarred),
|
||||
ListKind.Smart when list.Id == "smart:planned" => all.Where(t => t.ScheduledFor != null),
|
||||
ListKind.Virtual when list.Id == "virtual:running" => all.Where(t => t.Status == TaskStatus.Running),
|
||||
ListKind.Virtual when list.Id == "virtual:review" => all.Where(t => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active),
|
||||
ListKind.User => all.Where(t => $"user:{t.ListId}" == list.Id),
|
||||
_ => Enumerable.Empty<TaskEntity>(),
|
||||
};
|
||||
await using var db = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var all = await db.Tasks
|
||||
.Include(t => t.List)
|
||||
.Include(t => t.Worktree)
|
||||
.ToListAsync(ct);
|
||||
|
||||
foreach (var t in filtered)
|
||||
Items.Add(TaskRowViewModel.FromEntity(t));
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
UpdateSubtitle();
|
||||
IEnumerable<TaskEntity> filtered = list.Kind switch
|
||||
{
|
||||
ListKind.Smart when list.Id == "smart:my-day" => all.Where(t => t.IsMyDay),
|
||||
ListKind.Smart when list.Id == "smart:important" => all.Where(t => t.IsStarred),
|
||||
ListKind.Smart when list.Id == "smart:planned" => all.Where(t => t.ScheduledFor != null),
|
||||
ListKind.Virtual when list.Id == "virtual:running" => all.Where(t => t.Status == TaskStatus.Running),
|
||||
ListKind.Virtual when list.Id == "virtual:review" => all.Where(t => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active),
|
||||
ListKind.User => all.Where(t => $"user:{t.ListId}" == list.Id),
|
||||
_ => Enumerable.Empty<TaskEntity>(),
|
||||
};
|
||||
|
||||
foreach (var t in filtered)
|
||||
Items.Add(TaskRowViewModel.FromEntity(t));
|
||||
|
||||
UpdateSubtitle();
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
private void UpdateSubtitle()
|
||||
|
||||
Reference in New Issue
Block a user