layoutShallowEquals -> shallowEquals

This commit is contained in:
Johannes Marbach
2026-10-08 10:57:03 +02:00
parent 6600c891f3
commit 53e038b09b
5 changed files with 82 additions and 107 deletions
+4 -4
View File
@@ -82,6 +82,7 @@ import {
type ReactionOption, type ReactionOption,
} from "../../reactions"; } from "../../reactions";
import { shallowEquals as shallowArrayEquals } from "../../utils/array"; import { shallowEquals as shallowArrayEquals } from "../../utils/array";
import { shallowEquals as shallowObjectEquals } from "../../utils/object";
import { type MediaDevices } from "../MediaDevices"; import { type MediaDevices } from "../MediaDevices";
import { constant, type Behavior } from "../Behavior"; import { constant, type Behavior } from "../Behavior";
import { E2eeType } from "../../e2ee/e2eeType"; import { E2eeType } from "../../e2ee/e2eeType";
@@ -91,7 +92,6 @@ import { HeaderStyle, type UrlParams } from "../../UrlParams";
import { type ProcessorState } from "../../livekit/TrackProcessorContext"; import { type ProcessorState } from "../../livekit/TrackProcessorContext";
import { type HostBridge, nullHostBridge } from "../../HostBridge"; import { type HostBridge, nullHostBridge } from "../../HostBridge";
import { import {
layoutShallowEquals,
type Alignment, type Alignment,
type Layout, type Layout,
type LayoutMedia, type LayoutMedia,
@@ -1028,7 +1028,7 @@ export function createCallViewModel$(
[ringingMedia$, screenShares$, spotlightSpeaker$, localUserMediaForPip$], [ringingMedia$, screenShares$, spotlightSpeaker$, localUserMediaForPip$],
(ringing, screenShares, speaker, localPip) => (ringing, screenShares, speaker, localPip) =>
computeSpotlight({ ringing, screenShares, speaker, localPip }), computeSpotlight({ ringing, screenShares, speaker, localPip }),
).pipe(distinctUntilChanged(layoutShallowEquals)), ).pipe(distinctUntilChanged(shallowObjectEquals)),
); );
const spotlight$ = scope.behavior<MediaViewModel[]>( const spotlight$ = scope.behavior<MediaViewModel[]>(
@@ -1185,7 +1185,7 @@ export function createCallViewModel$(
desktop: platform === "desktop", desktop: platform === "desktop",
}), }),
), ),
distinctUntilChanged(layoutShallowEquals), distinctUntilChanged(shallowObjectEquals<LayoutMedia>),
), ),
); );
@@ -1486,7 +1486,7 @@ export function createCallViewModel$(
layoutInternals$.pipe( layoutInternals$.pipe(
map(({ layout }) => layout), map(({ layout }) => layout),
// Drop redundant layout updates before they would hit React. // Drop redundant layout updates before they would hit React.
distinctUntilChanged<Layout>(layoutShallowEquals), distinctUntilChanged(shallowObjectEquals<Layout>),
), ),
); );
-74
View File
@@ -1,74 +0,0 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { test, expect } from "vitest";
import {
type Alignment,
layoutShallowEquals,
type Layout,
} from "./layout-types";
import {
type SpotlightTileViewModel,
type GridTileViewModel,
} from "./TileViewModel";
import { BehaviorSubject } from "rxjs";
const spotlightTile = {} as unknown as SpotlightTileViewModel;
const gridTile = {} as unknown as GridTileViewModel;
const pipAlignment$ = new BehaviorSubject<Alignment>({
inline: "end",
block: "end",
});
const spotlightExpanded: Layout = {
type: "spotlight-expanded",
spotlight: spotlightTile,
pipAlignment$,
};
const spotlightPortrait: Layout = {
type: "spotlight-portrait",
spotlight: spotlightTile,
grid: [gridTile],
setVisibleTiles: () => {},
};
test("layoutShallowEquals considers a layout to be equal to its shallow clone", () =>
expect(layoutShallowEquals(spotlightExpanded, { ...spotlightExpanded })).toBe(
true,
));
test("layoutShallowEquals detects a missing key", () => {
expect(
layoutShallowEquals(spotlightExpanded, {
...spotlightExpanded,
pip: gridTile,
}),
).toBe(false);
expect(
layoutShallowEquals(
{ ...spotlightExpanded, pip: gridTile },
spotlightExpanded,
),
).toBe(false);
});
test("layoutShallowEquals considers grid arrays with equal contents to be equal", () =>
expect(
layoutShallowEquals(spotlightPortrait, {
...spotlightPortrait,
grid: [...spotlightPortrait.grid],
}),
).toBe(true));
test("layoutShallowEquals detects grid arrays with different contents", () =>
expect(
layoutShallowEquals(spotlightPortrait, {
...spotlightPortrait,
grid: [...spotlightPortrait.grid, gridTile],
}),
).toBe(false));
-29
View File
@@ -16,7 +16,6 @@ import {
type SpotlightTileViewModel, type SpotlightTileViewModel,
} from "./TileViewModel.ts"; } from "./TileViewModel.ts";
import { type Behavior } from "./Behavior.ts"; import { type Behavior } from "./Behavior.ts";
import { shallowEquals as arrayShallowEquals } from "../utils/array.ts";
/** /**
* The general shape of the space the call is drawn in. Called a window because * The general shape of the space the call is drawn in. Called a window because
@@ -148,31 +147,3 @@ export type Layout =
| OneOnOneDesktopLayout | OneOnOneDesktopLayout
| OneOnOneMobileLayout | OneOnOneMobileLayout
| PipLayout; | PipLayout;
/**
* Tests whether the top-level properties and array elements of `a` are equal
* to those of `b`. Useful for deduping redundant layout and layout media
* updates.
*/
export function layoutShallowEquals<T extends object>(a: T, b: T): boolean {
// If a and b have the same number of keys and every key in a is also in b,
// then they have the same keys.
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) return false;
for (const key of aKeys) {
if (!(key in b)) return false;
// Now check that they have the same values.
const aValue = (a as any)[key];
const bValue = (b as any)[key];
if (Array.isArray(aValue) && Array.isArray(bValue)) {
// Special case for arrays so we can detect when the grid tiles arrays are
// essentially the same.
if (!arrayShallowEquals(aValue, bValue)) return false;
} else if (aValue !== bValue) return false;
}
return true;
}
+40
View File
@@ -0,0 +1,40 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { test, expect } from "vitest";
import { shallowEquals } from "./object";
interface Thing {
type: string;
items: object[];
value: number;
extra?: object;
}
const item = {};
const withArray: Thing = { type: "a", items: [item], value: 1 };
test("shallowEquals considers an object to be equal to its shallow clone", () =>
expect(shallowEquals(withArray, { ...withArray })).toBe(true));
test("shallowEquals detects a missing key", () => {
expect(shallowEquals(withArray, { ...withArray, extra: item })).toBe(false);
expect(shallowEquals({ ...withArray, extra: item }, withArray)).toBe(false);
});
test("shallowEquals detects a differing value", () =>
expect(shallowEquals(withArray, { ...withArray, value: 2 })).toBe(false));
test("shallowEquals considers arrays with equal contents to be equal", () =>
expect(
shallowEquals(withArray, { ...withArray, items: [...withArray.items] }),
).toBe(true));
test("shallowEquals detects arrays with different contents", () =>
expect(
shallowEquals(withArray, { ...withArray, items: [...withArray.items, {}] }),
).toBe(false));
+38
View File
@@ -0,0 +1,38 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { shallowEquals as arrayShallowEquals } from "./array.ts";
/**
* Determine whether two objects are equal by shallow comparison of their
* top-level properties. Array-valued properties are themselves compared
* shallowly, so two objects holding distinct arrays with the same elements are
* considered equal.
*/
export function shallowEquals<T extends object>(
a: Readonly<T>,
b: Readonly<T>,
): boolean {
// If a and b have the same number of keys and every key in a is also in b,
// then they have the same keys.
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) return false;
for (const key of aKeys) {
if (!(key in b)) return false;
// Now check that they have the same values.
const aValue = (a as any)[key];
const bValue = (b as any)[key];
if (Array.isArray(aValue) && Array.isArray(bValue)) {
if (!arrayShallowEquals(aValue, bValue)) return false;
} else if (aValue !== bValue) return false;
}
return true;
}