Skip to content

fix(coordinator): give each tab its own row count task so one cannot orphan another - #2062

Merged
datlechin merged 2 commits into
mainfrom
fix/2059-row-count-task-lifecycle
Aug 10, 2026
Merged

fix(coordinator): give each tab its own row count task so one cannot orphan another#2062
datlechin merged 2 commits into
mainfrom
fix/2059-row-count-task-lifecycle

Conversation

@datlechin

Copy link
Copy Markdown
Member

Closes #2059.

currentRowCountTask was one slot on the coordinator shared by every tab in the window, while the work it holds is per tab. Two writers assigned to it without cancelling what was there, and teardown() never cancelled it at all.

What went wrong

Tab A starts a row count. The user switches to tab B and runs a query there. Tab B's count overwrites the slot, dropping the only reference to tab A's task. Nothing can cancel it after that: not Stop, not a refresh, not closing the window. It runs to the end of its driver round trip, and because both task bodies capture parent strongly, it holds the whole coordinator alive while it does.

teardown() cancels seven other handles and misses this one, so closing a window leaves every in-flight count running.

What this changes

The handle becomes rowCountTasks: [UUID: Task<Void, Never>], keyed by tab, which is how tableLoadTasks already models the same shape. Four helpers own the slot so no call site can get it wrong:

  • setRowCountTask(_:for:) cancels the tab's previous count before storing the new one
  • clearRowCountTask(for:) drops a finished task's handle without cancelling
  • cancelRowCountTask(for:) for a single tab
  • cancelAllRowCountTasks() for Stop and teardown

Cancel-before-assign alone would have been wrong here. With one shared slot, cancelling the predecessor means starting a count on tab B kills tab A's legitimate count. Keying by tab fixes the orphaning and the cross-tab interference together.

Also wired in:

  • teardown() now cancels every row count.
  • supersedeExecution(for:) cancels that tab's count. Pointing a tab at another table makes its old count meaningless, and this stops the server working on it.
  • Removed changeManagerUpdateTask. It was declared and cancelled in teardown() but never assigned anywhere in the repo.

Tests

RowCountTaskLifecycleTests covers the six cases: a second count on the same tab cancels the first, a count on another tab does not, retargeting cancels only its own, teardown and Stop cancel all, and clearing a finished handle cancels nothing. The two existing tests in MainContentCoordinatorRefreshTests that poked the old slot are updated in the same commit.

28 tests pass across RowCountTaskLifecycleTests, MainContentCoordinatorRefreshTests, TabRetargetInvalidationTests and DriverCancellationPolicyTests. swiftlint lint --strict reports 0 violations in 1300 files.

Scope note

The write-back guards were already correct, so no late count could corrupt the grid. This is about work that outlives its owner, not wrong data.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
@datlechin
datlechin merged commit a4f8f16 into main Aug 10, 2026
4 checks passed
@datlechin
datlechin deleted the fix/2059-row-count-task-lifecycle branch August 10, 2026 05:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

currentRowCountTask is never cancelled on teardown and is overwritten without cancelling

1 participant