Files
twenty/packages/twenty-shared/src/utils/compute-diff-between-objects.ts
T
Charles BochetandGitHub 4103efcb84 fix: replace slow deep-equal with fastDeepEqual to resolve CPU bottleneck (#19771)
## Summary

- Replaced the `deep-equal` npm package with the existing
`fastDeepEqual` from `twenty-shared/utils` across 5 files in the server
and shared packages
- `deep-equal` was causing severe CPU overhead in the record update hot
path (`executeMany` → `formatTwentyOrmEventToDatabaseBatchEvent` →
`objectRecordChangedValues` → `deepEqual`, called **per field per
record**)
- `fastDeepEqual` is ~100x faster for plain JSON database records since
it skips unnecessary prototype chain inspection and edge-case handling
- Removed the now-unnecessary `LARGE_JSON_FIELDS` branching in
`objectRecordChangedValues` since all fields now use the fast
implementation
2026-04-16 17:23:50 +02:00

94 lines
2.3 KiB
TypeScript

import { isDefined } from '@/utils/validation';
import { fastDeepEqual } from './json/fast-deep-equal';
type Diff<T extends { id: string }> = {
toCreate: T[];
toUpdate: T[];
toRestoreAndUpdate: T[];
idsToRemove: string[];
};
const extractProperties = <T extends { id: string }>(
object: T,
properties: (keyof T)[],
) => {
return properties.reduce((acc, property) => {
return {
...acc,
[property]: object[property],
};
}, {});
};
type ComputeDiffBetweenObjectsParams<
T extends { id: string },
K extends { id: string },
> = {
existingObjects: T[];
receivedObjects: K[];
propertiesToCompare: (keyof K & keyof T)[];
isEntityIncluded: (entity: NoInfer<T>) => boolean;
};
export const computeDiffBetweenObjects = <
T extends { id: string },
K extends { id: string },
>({
existingObjects,
receivedObjects,
propertiesToCompare,
isEntityIncluded,
}: ComputeDiffBetweenObjectsParams<T, K>): Diff<K> => {
const toCreate: K[] = [];
const toUpdate: K[] = [];
const toRestoreAndUpdate: K[] = [];
const existingEntitiesMap = new Map(
existingObjects.map((entity) => [entity.id, entity]),
);
const receivedEntitiesMap = new Map(
receivedObjects.map((entity) => [entity.id, entity]),
);
for (const receivedObject of receivedObjects) {
const existingEntity = existingEntitiesMap.get(receivedObject.id);
if (isDefined(existingEntity)) {
if (!isEntityIncluded(existingEntity)) {
toRestoreAndUpdate.push(receivedObject);
} else {
const comparableExistingEntity = extractProperties(
existingEntity,
propertiesToCompare,
);
const comparableReceivedEntity = extractProperties(
receivedObject,
propertiesToCompare,
);
if (
!fastDeepEqual(comparableExistingEntity, comparableReceivedEntity)
) {
toUpdate.push(receivedObject);
}
}
} else {
toCreate.push(receivedObject);
}
}
const idsToRemove = existingObjects
.filter((existingEntity) => isEntityIncluded(existingEntity))
.filter((existingEntity) => !receivedEntitiesMap.has(existingEntity.id))
.map((entity) => entity.id);
return {
toCreate,
toUpdate,
toRestoreAndUpdate,
idsToRemove,
};
};