fix(ui): retry the task delta refresh instead of swallowing the error
This commit is contained in:
@@ -161,6 +161,11 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
}
|
||||
|
||||
private async void OnWorkerTaskUpdated(string taskId)
|
||||
=> await RefreshTaskFromWorkerAsync(taskId);
|
||||
|
||||
// Awaitable so tests can drive it deterministically. One retry, then a full reload:
|
||||
// a swallowed exception here used to leave the row on a stale status permanently.
|
||||
internal async Task RefreshTaskFromWorkerAsync(string taskId)
|
||||
{
|
||||
var list = _currentList;
|
||||
if (list is null) return;
|
||||
@@ -176,52 +181,71 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var entity = await db.Tasks
|
||||
.Include(t => t.List)
|
||||
.Include(t => t.Worktree)
|
||||
.FirstOrDefaultAsync(t => t.Id == taskId);
|
||||
|
||||
// A parent transition (finalize/discard) broadcasts only the parent's id, but it
|
||||
// changes its children's derived state — finalize flips them Draft→Planned, discard
|
||||
// deletes them. The delta path below only touches the parent row and never recomputes
|
||||
// the child-derived flags (ParentFinalized, HasPlanningChildren) nor drops deleted
|
||||
// children, so reconcile the whole list when the updated task is (or owns) a subtree.
|
||||
if (entity is not null &&
|
||||
(entity.PlanningPhase != PlanningPhase.None || Items.Any(r => r.ParentTaskId == entity.Id)))
|
||||
{
|
||||
LoadForList(list);
|
||||
return;
|
||||
}
|
||||
|
||||
var existing = Items.FirstOrDefault(r => r.Id == taskId);
|
||||
|
||||
if (entity is null)
|
||||
{
|
||||
if (existing is not null) Items.Remove(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
var matches = TaskMatchesList(entity, list);
|
||||
if (existing is not null && matches) existing.UpdateFromEntity(entity);
|
||||
else if (existing is not null) Items.Remove(existing);
|
||||
else if (matches) { LoadForList(list); return; }
|
||||
else return;
|
||||
}
|
||||
|
||||
// Keep the parent's HasQueuedSubtasks flag in sync when a child's status flips.
|
||||
if (entity is not null && !string.IsNullOrEmpty(entity.ParentTaskId))
|
||||
{
|
||||
var parent = Items.FirstOrDefault(r => r.Id == entity.ParentTaskId);
|
||||
if (parent is not null)
|
||||
parent.HasQueuedSubtasks = Items.Any(r =>
|
||||
r.ParentTaskId == parent.Id && (r.IsQueued || r.IsWaiting));
|
||||
}
|
||||
|
||||
Regroup();
|
||||
UpdateSubtitle();
|
||||
await ApplyDeltaAsync(taskId, list);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception first)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"TasksIsland: delta refresh for {taskId} failed ({first.Message}); retrying");
|
||||
try
|
||||
{
|
||||
await ApplyDeltaAsync(taskId, list);
|
||||
}
|
||||
catch (Exception second)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"TasksIsland: delta retry for {taskId} failed ({second.Message}); full reload");
|
||||
LoadForList(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyDeltaAsync(string taskId, ListNavItemViewModel list)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var entity = await db.Tasks
|
||||
.Include(t => t.List)
|
||||
.Include(t => t.Worktree)
|
||||
.FirstOrDefaultAsync(t => t.Id == taskId);
|
||||
|
||||
// A parent transition (finalize/discard) broadcasts only the parent's id, but it
|
||||
// changes its children's derived state — finalize flips them Draft→Planned, discard
|
||||
// deletes them. The delta path below only touches the parent row and never recomputes
|
||||
// the child-derived flags (ParentFinalized, HasPlanningChildren) nor drops deleted
|
||||
// children, so reconcile the whole list when the updated task is (or owns) a subtree.
|
||||
if (entity is not null &&
|
||||
(entity.PlanningPhase != PlanningPhase.None || Items.Any(r => r.ParentTaskId == entity.Id)))
|
||||
{
|
||||
LoadForList(list);
|
||||
return;
|
||||
}
|
||||
|
||||
var existing = Items.FirstOrDefault(r => r.Id == taskId);
|
||||
|
||||
if (entity is null)
|
||||
{
|
||||
if (existing is not null) Items.Remove(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
var matches = TaskMatchesList(entity, list);
|
||||
if (existing is not null && matches) existing.UpdateFromEntity(entity);
|
||||
else if (existing is not null) Items.Remove(existing);
|
||||
else if (matches) { LoadForList(list); return; }
|
||||
else return;
|
||||
}
|
||||
|
||||
// Keep the parent's HasQueuedSubtasks flag in sync when a child's status flips.
|
||||
if (entity is not null && !string.IsNullOrEmpty(entity.ParentTaskId))
|
||||
{
|
||||
var parent = Items.FirstOrDefault(r => r.Id == entity.ParentTaskId);
|
||||
if (parent is not null)
|
||||
parent.HasQueuedSubtasks = Items.Any(r =>
|
||||
r.ParentTaskId == parent.Id && (r.IsQueued || r.IsWaiting));
|
||||
}
|
||||
|
||||
Regroup();
|
||||
UpdateSubtitle();
|
||||
}
|
||||
|
||||
// NOTE: virtual:queued/virtual:running cannot be decided by a single entity — a Planning
|
||||
|
||||
Reference in New Issue
Block a user