From d9d47eca6f4d0205c42e2329e1e125620f14797e Mon Sep 17 00:00:00 2001 From: abstrakt Date: Tue, 23 Dec 2025 16:44:38 +0000 Subject: [PATCH] fix: Do not applyProps if value has not changed --- packages/lib/src/utils/compare.tsx | 74 ++++++++++++++++++++-------- packages/lib/src/utils/validation.ts | 9 ++++ 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/packages/lib/src/utils/compare.tsx b/packages/lib/src/utils/compare.tsx index 1391d5e0..a475a0f4 100644 --- a/packages/lib/src/utils/compare.tsx +++ b/packages/lib/src/utils/compare.tsx @@ -1,4 +1,3 @@ - /** * Simple deep equality check for two objects * @param {Record} a - The first object @@ -16,23 +15,61 @@ export function deepEqual(a: Record, b: Record return aKeys.every(key => deepEqual(a[key] as Record, b[key] as Record)); } -type Equalable = { - equals: (other: unknown) => boolean; - }; - - function hasEqualsMethod(obj: unknown): obj is Equalable { - return typeof obj === 'object' && - obj !== null && - 'equals' in obj && - typeof (obj as Equalable).equals === 'function'; - } - - export const shallowEquals = (objA: Record, objB: Record): boolean => { +interface Approximate { + equalsApprox(other: unknown): boolean; +} + +interface Equatable { + equals(other: unknown): boolean; +} + +/** + * Check if two values are equal. Handles primitives, null/undefined, + * and objects with `equalsApprox` or `equals` methods (Vec3, Color, etc.) + * + * Priority order: + * 1. Strict equality (===) + * 2. Floating point approximation (equalsApprox) - handles precision drift + * 3. Structural equality (equals) + * + * @param a - First value to compare + * @param b - Second value to compare + * @returns True if values are equal, false otherwise + */ +export function valuesEqual(a: unknown, b: unknown): boolean { + if (a === b) return true; + + // Early exit if either is null/undefined (using type coercion) + if (a == null || b == null) return false; + + if (typeof a === 'object') { + // Priority 1: Floating point approximation (handles precision drift) + if ('equalsApprox' in a && typeof (a as Approximate).equalsApprox === 'function') { + return (a as Approximate).equalsApprox(b); + } + + // Priority 2: Strict structural equality + if ('equals' in a && typeof (a as Equatable).equals === 'function') { + return (a as Equatable).equals(b); + } + } + + // For other objects, return false to trigger re-apply (conservative) + return false; +} + +/** + * Shallow equality check for two objects. Compares each property using valuesEqual. + * + * @param objA - First object to compare + * @param objB - Second object to compare + * @returns True if objects are shallowly equal, false otherwise + */ +export const shallowEquals = (objA: Record, objB: Record): boolean => { // If the two objects are the same object, return true if (objA === objB) { return true; } - // If either is not an object (null or primitives), return false if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { @@ -48,15 +85,10 @@ type Equalable = { return false; } - // Check if all keys and their values are equal + // Check if all keys and their values are equal using valuesEqual for (let i = 0; i < keysA.length; i++) { const key = keysA[i]; - const propA = objA[key]; - const propB = objB[key]; - // If the object has an equality operator, use this - if(hasEqualsMethod(propA)) { - return propA.equals(propB); - } else if (propA !== propB) { + if (!valuesEqual(objA[key], objB[key])) { return false; } } diff --git a/packages/lib/src/utils/validation.ts b/packages/lib/src/utils/validation.ts index eef1c68e..a86a7b89 100644 --- a/packages/lib/src/utils/validation.ts +++ b/packages/lib/src/utils/validation.ts @@ -2,6 +2,7 @@ import { Color, Quat, Vec2, Vec3, Vec4, Mat4, Application, NullGraphicsDevice, M import { getColorFromName } from "./color.ts"; import { Serializable } from "./types-utils.ts"; import { env } from "./env.ts"; +import { valuesEqual } from "./compare.tsx"; // Limit the size of the warned set to prevent memory leaks const MAX_WARNED_SIZE = 1000; @@ -218,6 +219,7 @@ export function validatePropsWithDefaults( * @param schema The schema of the container * @param props The props to apply */ + export function applyProps, InstanceType>( instance: InstanceType, schema: Schema, @@ -227,6 +229,13 @@ export function applyProps, InstanceType>( if (key in schema) { const propDef = schema[key as keyof T] as PropValidator; if (propDef) { + const currentValue = (instance as Record)[key]; + + // Skip if value hasn't changed (avoids side effects from setters) + if (valuesEqual(currentValue, value)) { + return; + } + if (propDef.apply) { // Use type assertion to satisfy the type checker propDef.apply(instance, props, key as string);