fix(KB-613): resolve status update race condition and fix stuck timeout units

- Add timestamp comparison logic in handleUpdated to prevent stale websocket data from overwriting newer task state
- Rename stuck timeout setting from minutes to milliseconds for finer control
- Add comprehensive tests for status update race condition scenarios
- Add changeset for status update race condition fix
This commit is contained in:
gsxdsm
2026-03-31 17:37:31 -07:00
parent f159a5edd8
commit 9030eeebd7
3 changed files with 142 additions and 15 deletions

View File

@@ -64,22 +64,35 @@ export function useTasks() {
prev.map((t) => {
if (t.id !== incoming.id) return t;
// Race condition prevention: If the incoming update has stale column data,
// preserve the current column. A task:moved event always carries the
// authoritative column in the 'to' field and updates columnMovedAt.
// If current state has a newer columnMovedAt, reject the column change.
// First check overall freshness using updatedAt
const updatedAtCompare = compareTimestamps(incoming.updatedAt, t.updatedAt);
// If incoming is older overall, skip the update
if (updatedAtCompare < 0) {
return t;
}
// If columns are the same, no conflict - accept the incoming update
if (t.column === incoming.column) {
return incoming;
}
// Columns differ - need to check columnMovedAt to resolve conflict
const columnTimestampCompare = compareTimestamps(t.columnMovedAt, incoming.columnMovedAt);
if (columnTimestampCompare > 0 && t.column !== incoming.column) {
// Edge case: current has columnMovedAt but incoming doesn't (legacy data)
// Preserve the column information we have
if (t.columnMovedAt && !incoming.columnMovedAt) {
return { ...incoming, column: t.column, columnMovedAt: t.columnMovedAt };
}
// If current state has a newer columnMovedAt, reject the column change
if (columnTimestampCompare > 0) {
// Current state is newer - preserve column, merge other fields
return { ...incoming, column: t.column, columnMovedAt: t.columnMovedAt };
}
// Edge case: current has columnMovedAt but incoming doesn't (legacy data)
// Preserve the column information we have
if (t.columnMovedAt && !incoming.columnMovedAt && t.column !== incoming.column) {
return { ...incoming, column: t.column, columnMovedAt: t.columnMovedAt };
}
// Incoming has newer or equal columnMovedAt, accept the update
return incoming;
})
);