Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/utils/isRealtimePayloadEqual.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { RealTimeTaskResponse } from '@/hoc/RealTime'
import { isTaskPayloadEqual } from '@/utils/isRealtimePayloadEqual'
import { RealtimePostgresChangesPayload } from '@supabase/supabase-js'

const buildTaskPayload = ({
newPayload,
oldPayload,
}: {
newPayload: Record<string, unknown>
oldPayload: Record<string, unknown>
}): RealtimePostgresChangesPayload<RealTimeTaskResponse> =>
({
commit_timestamp: '2026-07-08T12:00:00.000Z',
errors: [],
eventType: 'UPDATE',
new: newPayload,
old: oldPayload,
schema: 'public',
table: 'Tasks',
}) as unknown as RealtimePostgresChangesPayload<RealTimeTaskResponse>

describe('isTaskPayloadEqual', () => {
it('ignores no-op updates for task fields', () => {
const task = {
id: 'task-1',
title: 'Prepare docs',
workflowStateId: 'workflow-state-1',
associations: [{ companyId: 'company-1' }],
isShared: true,
}

expect(isTaskPayloadEqual(buildTaskPayload({ newPayload: { ...task }, oldPayload: { ...task } }))).toBe(true)
})

it('detects changed task fields', () => {
expect(
isTaskPayloadEqual(
buildTaskPayload({
newPayload: { id: 'task-1', title: 'Prepare docs' },
oldPayload: { id: 'task-1', title: 'Prepare draft' },
}),
),
).toBe(false)
})

it('does not recurse through cyclic payload values', () => {
const newPayload: Record<string, unknown> = { id: 'task-1', title: 'Prepare docs' }
const oldPayload: Record<string, unknown> = { id: 'task-1', title: 'Prepare docs' }
newPayload.self = newPayload
oldPayload.self = oldPayload

expect(() => isTaskPayloadEqual(buildTaskPayload({ newPayload, oldPayload }))).not.toThrow()
})
})
79 changes: 73 additions & 6 deletions src/utils/isRealtimePayloadEqual.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,82 @@
import { RealTimeTaskResponse } from '@/hoc/RealTime'
import { RealTimeTemplateResponse } from '@/hoc/RealtimeTemplates'
import type { RealTimeTaskResponse } from '@/hoc/RealTime'
import type { RealTimeTemplateResponse } from '@/hoc/RealtimeTemplates'
import { RealtimePostgresChangesPayload } from '@supabase/supabase-js'
import deepEqual from 'deep-equal'

const taskRealtimeFields = [
'id',
'label',
'workspaceId',
'assigneeId',
'internalUserId',
'clientId',
'companyId',
'assigneeType',
'title',
'body',
'createdById',
'workflowStateId',
'assignedAt',
'completedAt',
'dueDate',
'lastActivityLogUpdated',
'lastSubtaskUpdated',
'createdAt',
'updatedAt',
'deletedAt',
'parentId',
'subtaskCount',
'path',
'isArchived',
'lastArchivedDate',
'source',
'templateId',
'completedBy',
'completedByUserType',
'archivedBy',
'deletedBy',
'associations',
'isShared',
] as const

const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null && !Array.isArray(value)

const toRecord = (value: unknown): Record<string, unknown> | undefined => (isRecord(value) ? value : undefined)

const arePrimitiveRecordValuesEqual = (newValue: Record<string, unknown>, oldValue: Record<string, unknown>): boolean => {
const keys = [...new Set([...Object.keys(newValue), ...Object.keys(oldValue)])]
return keys.every((key) => Object.is(newValue[key], oldValue[key]))
}

const areFlatArraysEqual = (newValue: unknown[], oldValue: unknown[]): boolean => {
if (newValue.length !== oldValue.length) return false
return newValue.every((item, index) => {
const oldItem = oldValue[index]
if (isRecord(item) && isRecord(oldItem)) {
return arePrimitiveRecordValuesEqual(item, oldItem)
}
return Object.is(item, oldItem)
})
}

const areRealtimeValuesEqual = (newValue: unknown, oldValue: unknown): boolean => {
if (Object.is(newValue, oldValue)) return true
if (Array.isArray(newValue) && Array.isArray(oldValue)) {
return areFlatArraysEqual(newValue, oldValue)
}
if (isRecord(newValue) && isRecord(oldValue)) {
return arePrimitiveRecordValuesEqual(newValue, oldValue)
}
return false
}

export function isTaskPayloadEqual(
payload: RealtimePostgresChangesPayload<RealTimeTaskResponse | RealTimeTemplateResponse>,
): boolean {
const newPayload = payload.new
const oldPayload = payload.old
const newPayload = toRecord(payload.new)
const oldPayload = toRecord(payload.old)
if (!newPayload || !oldPayload) return true
return deepEqual(newPayload, oldPayload)
return taskRealtimeFields.every((field) => areRealtimeValuesEqual(newPayload[field], oldPayload[field]))
}

export function isTemplatePayloadEqual(payload: RealtimePostgresChangesPayload<RealTimeTemplateResponse>): boolean {
Expand Down
Loading