Skip to content

Commit 30bc926

Browse files
committed
fix(OUT-2055): realtime and cache issues for subtask count and subtask list.
- subtasks count cache issue : accessibleTask was use to calculate subtask count component for task card and task list. only implemented accessibleTask to be update from server on the first laod, rest of the time, its updated from realtime. - subtasks list update issue : subtasks list were only refetched when no of subtasks in parent task was change, but while updating it doesnt change : - created a migration adding lastSubtaskUpdated timestamp in task schema. - formatted the timestamp for lastSubtaskUpdated in realtime response. - refetched subtasks according to the change in lastSubtaskUpdated. - updated lastSubtaskUpdated while creating, updating and deleted a sub task. - made sure we dont compromise on previously applied optimization by adding refetching check on mount and optimistic updates.
1 parent f9259c6 commit 30bc926

9 files changed

Lines changed: 52 additions & 14 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "Tasks" ADD COLUMN "lastSubtaskUpdated" TIMESTAMP(3);

prisma/schema/task.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ model Task {
3232
completedAt DateTime?
3333
dueDate String? @db.VarChar(10)
3434
lastActivityLogUpdated DateTime?
35+
lastSubtaskUpdated DateTime?
3536
createdAt DateTime @default(now())
3637
deletedAt DateTime?
3738
ClientNotification ClientNotification[]

src/app/api/tasks/tasks.service.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { maxSubTaskDepth } from '@/constants/tasks'
22
import { MAX_FETCH_ASSIGNEE_COUNT } from '@/constants/users'
33
import { deleteTaskNotifications, sendTaskCreateNotifications, sendTaskUpdateNotifications } from '@/jobs/notifications'
44
import { sendClientUpdateTaskNotifications } from '@/jobs/notifications/send-client-task-update-notifications'
5-
import { ClientResponse, CompanyResponse, InternalUsers } from '@/types/common'
5+
import { ClientResponse, CompanyResponse, InternalUsers, Uuid } from '@/types/common'
66
import { TaskWithWorkflowState } from '@/types/db'
77
import { AncestorTaskResponse, CreateTaskRequest, UpdateTaskRequest } from '@/types/dto/tasks.dto'
88
import { DISPATCHABLE_EVENT } from '@/types/webhook'
@@ -247,7 +247,10 @@ export class TasksService extends BaseService {
247247
// Increment parent task's subtask count, if exists
248248
if (newTask.parentId) {
249249
const subtaskService = new SubtaskService(this.user)
250-
await subtaskService.addSubtaskCount(newTask.parentId)
250+
await Promise.all([
251+
subtaskService.addSubtaskCount(newTask.parentId),
252+
this.setNewLastSubtaskUpdated(newTask.parentId),
253+
])
251254
}
252255
} catch (e: unknown) {
253256
// Manually rollback task creation
@@ -415,6 +418,7 @@ export class TasksService extends BaseService {
415418
const isBodyChanged = prevTask.body !== updatedTask.body
416419
await Promise.all([
417420
activityLogger.logTaskUpdated(prevTask),
421+
this.setNewLastSubtaskUpdated(updatedTask.parentId),
418422
sendTaskUpdateNotifications.trigger({ prevTask, updatedTask, user: this.user }),
419423
dispatchUpdatedWebhookEvent(this.user, prevTask, updatedTask, opts?.isPublicApi || false),
420424
isBodyChanged && opts?.isPublicApi ? queueBodyUpdatedWebhook(this.user, updatedTask) : undefined,
@@ -461,6 +465,7 @@ export class TasksService extends BaseService {
461465
subtaskService.setTransaction(tx as PrismaClient)
462466
if (task.parentId) {
463467
await subtaskService.decreaseSubtaskCount(task.parentId)
468+
await this.setNewLastSubtaskUpdated(task.parentId)
464469
}
465470
await subtaskService.softDeleteAllSubtasks(task.id)
466471
return deletedTask
@@ -991,4 +996,20 @@ export class TasksService extends BaseService {
991996
assigneeType: null,
992997
}
993998
}
999+
1000+
private async setNewLastSubtaskUpdated(parentId?: z.infer<typeof Uuid> | null) {
1001+
if (!parentId) {
1002+
return
1003+
}
1004+
try {
1005+
await this.db.task.update({
1006+
where: { id: parentId, workspaceId: this.user.workspaceId },
1007+
data: {
1008+
lastSubtaskUpdated: new Date(),
1009+
},
1010+
})
1011+
} catch (e) {
1012+
console.error('TaskService#setNewLastSubtaskUpdated::', e)
1013+
}
1014+
}
9941015
}

src/app/detail/ui/ActivityWrapper.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export const ActivityWrapper = ({
116116
{
117117
optimisticData: { data: optimisticData },
118118
rollbackOnError: true,
119-
revalidate: false, // Make sure to revalidate after mutation
119+
revalidate: false,
120120
},
121121
)
122122
} catch (error) {

src/app/detail/ui/Subtasks.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { NewTaskCard } from '@/app/detail/ui/NewTaskCard'
66
import { TaskCardList } from '@/app/detail/ui/TaskCardList'
77
import { AddBtn } from '@/components/buttons/AddBtn'
88
import { GhostBtn } from '@/components/buttons/GhostBtn'
9+
import { useDebounce } from '@/hooks/useDebounce'
910
import { GrayAddMediumIcon } from '@/icons'
1011
import { selectAuthDetails } from '@/redux/features/authDetailsSlice'
1112
import { selectTaskBoard } from '@/redux/features/taskBoardSlice'
@@ -40,7 +41,7 @@ export const Subtasks = ({
4041
const { workflowStates, assignee, activeTask } = useSelector(selectTaskBoard)
4142
const { tokenPayload } = useSelector(selectAuthDetails)
4243
const [optimisticUpdates, setOptimisticUpdates] = useState<OptimisticUpdate[]>([]) //might need this server-temp id maps in the future.
43-
44+
const [lastUpdated, setLastUpdated] = useState<string | null>()
4445
const handleFormCancel = () => setOpenTaskForm(false)
4546
const handleFormOpen = () => setOpenTaskForm(!openTaskForm)
4647

@@ -53,18 +54,26 @@ export const Subtasks = ({
5354
revalidateOnFocus: false,
5455
})
5556
const didMount = useRef(false)
57+
const shouldRefetchRef = useRef(true) //preventing double fetching from subtask update apis. Due to optimistic update revalidation, we are already fetching logs there. So no need to refetch in case for subtask update.
5658

5759
const { mutate } = useSWRConfig()
5860

59-
useEffect(() => {
60-
const taskListLength = subTasks?.tasks?.length
61-
62-
if (!activeTask || typeof taskListLength !== 'number') return
61+
const _debounceMutate = async (cacheKey: string) => await mutate(cacheKey)
62+
const debounceMutate = useDebounce(_debounceMutate, 200)
6363

64-
if (activeTask.subtaskCount !== taskListLength) {
65-
mutate?.(cacheKey)
64+
useEffect(() => {
65+
if (!activeTask) return
66+
if (!didMount.current || !shouldRefetchRef.current) {
67+
didMount.current = true
68+
shouldRefetchRef.current = true
69+
setLastUpdated(activeTask?.lastSubtaskUpdated)
70+
return //skip the refetch on first mount and shouldRefetch is false.
71+
}
72+
if (activeTask?.lastSubtaskUpdated && activeTask?.lastSubtaskUpdated !== lastUpdated) {
73+
debounceMutate(cacheKey)
6674
}
67-
}, [activeTask?.subtaskCount, activeTask?.isArchived, subTasks?.tasks?.length, activeTask, cacheKey, mutate])
75+
setLastUpdated(activeTask?.lastSubtaskUpdated)
76+
}, [activeTask?.lastSubtaskUpdated])
6877

6978
const handleSubTaskCreation = (payload: CreateTaskRequest) => {
7079
const tempId = generateRandomString('temp-task')
@@ -115,13 +124,14 @@ export const Subtasks = ({
115124
await mutate(
116125
cacheKey,
117126
async () => {
127+
shouldRefetchRef.current = false
118128
await updater()
119129
return await fetcher(cacheKey)
120130
},
121131
{
122132
optimisticData: { tasks: updatedTasks },
123133
rollbackOnError: true,
124-
revalidate: true,
134+
revalidate: false,
125135
},
126136
)
127137
} catch (error) {

src/cmd/load-testing/load-testing.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ class LoadTester {
107107
| 'internalUserId'
108108
| 'clientId'
109109
| 'companyId'
110+
| 'lastSubtaskUpdated'
110111
>[] = []
111112
const currentUser = await authenticateWithToken(this.token, this.apiKey)
112113
const labelsService = new LabelMappingService(currentUser, this.apiKey)

src/hoc/ClientSideStateUpdate.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export const ClientSideStateUpdate = ({
6161
accesibleTaskIds?: string[]
6262
accessibleTasks?: TaskResponse[]
6363
}) => {
64-
const { tasks: tasksInStore, viewSettingsTemp } = useSelector(selectTaskBoard)
64+
const { tasks: tasksInStore, viewSettingsTemp, accessibleTasks: accessibleTaskInStore } = useSelector(selectTaskBoard)
6565
useEffect(() => {
6666
if (workflowStates) {
6767
store.dispatch(setWorkflowStates(workflowStates))
@@ -121,7 +121,8 @@ export const ClientSideStateUpdate = ({
121121
}
122122

123123
if (accessibleTasks) {
124-
store.dispatch(setAccessibleTasks(accessibleTasks))
124+
const accessibleTaskData = accessibleTaskInStore.length ? accessibleTaskInStore : accessibleTasks
125+
store.dispatch(setAccessibleTasks(accessibleTaskData))
125126
}
126127

127128
return () => {

src/lib/realtime.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export class RealtimeHandler {
3636
createdAt: newTask.createdAt && new Date(newTask.createdAt + 'Z').toISOString(),
3737
updatedAt: newTask.updatedAt && new Date(newTask.updatedAt + 'Z').toISOString(),
3838
lastActivityLogUpdated: newTask.lastActivityLogUpdated && new Date(newTask.lastActivityLogUpdated + 'Z').toISOString(),
39+
lastSubtaskUpdated: newTask.lastSubtaskUpdated && new Date(newTask.lastSubtaskUpdated + 'Z').toISOString(),
3940
}
4041
}
4142

src/types/dto/tasks.dto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export const TaskResponseSchema = z.object({
7878
updatedAt: z.string().datetime().nullish(),
7979
assignee: z.union([ClientResponseSchema, InternalUsersSchema, CompanyResponseSchema]).optional(),
8080
lastActivityLogUpdated: z.string().datetime().nullish(),
81+
lastSubtaskUpdated: z.string().datetime().nullish(),
8182
isArchived: z.boolean().optional(),
8283
lastArchivedDate: z.string().datetime(),
8384
parentId: z.string().nullish(),

0 commit comments

Comments
 (0)