mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-19 14:52:28 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f97c02500b | |||
| b70666e5f8 |
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Memoizes a single-argument function keyed by its argument's object identity,
|
||||
* caching results in a `WeakMap`.
|
||||
*
|
||||
* Unlike `memoizeOne`, this keeps every distinct argument cached, not just the
|
||||
* most recent one. That suits pure derivations from stable config objects that
|
||||
* are evaluated for many different configs and on a hot path, e.g. card
|
||||
* `shouldUpdate` helpers running on every state change. Entries are garbage
|
||||
* collected once the key object is no longer referenced.
|
||||
*/
|
||||
export const weakMemoize = <K extends object, V>(
|
||||
func: (arg: K) => V
|
||||
): ((arg: K) => V) => {
|
||||
const cache = new WeakMap<K, V>();
|
||||
return (arg) => {
|
||||
if (!cache.has(arg)) {
|
||||
cache.set(arg, func(arg));
|
||||
}
|
||||
return cache.get(arg)!;
|
||||
};
|
||||
};
|
||||
@@ -2,8 +2,17 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import type { EntityRegistryDisplayEntry } from "../../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { weakMemoize } from "../../../common/util/weak-memoize";
|
||||
import { processConfigEntities } from "./process-config-entities";
|
||||
|
||||
// `hasConfigOrEntitiesChanged` runs in `shouldUpdate` on every state change, but
|
||||
// the entities config only changes on (re)configure. Memoize the parsed result
|
||||
// per config array so it is parsed once and reused instead of re-allocated on
|
||||
// every state change.
|
||||
const getConfigEntities = weakMemoize((entities: any[]) =>
|
||||
processConfigEntities(entities, false)
|
||||
);
|
||||
|
||||
export function hasConfigChanged(
|
||||
element: any,
|
||||
changedProps: PropertyValues
|
||||
@@ -102,7 +111,7 @@ export function hasConfigOrEntitiesChanged(
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant;
|
||||
const newHass = element.hass as HomeAssistant;
|
||||
|
||||
const entities = processConfigEntities(element._config!.entities, false);
|
||||
const entities = getConfigEntities(element._config!.entities);
|
||||
|
||||
return entities.some((entity) => {
|
||||
if (!("entity" in entity)) {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { weakMemoize } from "../../../src/common/util/weak-memoize";
|
||||
|
||||
describe("weakMemoize", () => {
|
||||
it("computes once per key and returns the cached result", () => {
|
||||
const spy = vi.fn((arg: { id: number }) => ({ doubled: arg.id * 2 }));
|
||||
const memoized = weakMemoize(spy);
|
||||
|
||||
const key = { id: 21 };
|
||||
const first = memoized(key);
|
||||
const second = memoized(key);
|
||||
|
||||
expect(first).toEqual({ doubled: 42 });
|
||||
expect(second).toBe(first);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("computes separately for different keys", () => {
|
||||
const spy = vi.fn((arg: { id: number }) => arg.id);
|
||||
const memoized = weakMemoize(spy);
|
||||
|
||||
const a = { id: 1 };
|
||||
const b = { id: 1 };
|
||||
expect(memoized(a)).toBe(1);
|
||||
expect(memoized(b)).toBe(1);
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("caches falsy results without recomputing", () => {
|
||||
const spy = vi.fn(() => undefined);
|
||||
const memoized = weakMemoize(spy);
|
||||
|
||||
const key = {};
|
||||
memoized(key);
|
||||
memoized(key);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("gives each memoized function its own cache", () => {
|
||||
const memoizedA = weakMemoize((arg: object) => arg);
|
||||
const memoizedB = weakMemoize(() => "b");
|
||||
const key = {};
|
||||
expect(memoizedA(key)).toBe(key);
|
||||
expect(memoizedB(key)).toBe("b");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { hasConfigOrEntitiesChanged } from "../../../../src/panels/lovelace/common/has-changed";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
|
||||
const state = (entity_id: string, value: string) => ({
|
||||
entity_id,
|
||||
state: value,
|
||||
attributes: {},
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
context: { id: "" },
|
||||
});
|
||||
|
||||
// Shared references for everything hasConfigChanged compares, so that only
|
||||
// the `states` differences drive the result in these tests.
|
||||
const SHARED = {
|
||||
connected: true,
|
||||
entities: {},
|
||||
config: { state: "RUNNING" },
|
||||
themes: {},
|
||||
locale: {},
|
||||
localize: () => "",
|
||||
formatEntityState: () => "",
|
||||
formatEntityAttributeName: () => "",
|
||||
formatEntityAttributeValue: () => "",
|
||||
};
|
||||
|
||||
const createHass = (states: Record<string, any>): HomeAssistant =>
|
||||
({ ...SHARED, states }) as unknown as HomeAssistant;
|
||||
|
||||
const changedProps = (oldHass: HomeAssistant) =>
|
||||
new Map([["hass", oldHass]]) as any;
|
||||
|
||||
describe("hasConfigOrEntitiesChanged", () => {
|
||||
it("returns true when a configured entity's state changes", () => {
|
||||
const oldHass = createHass({
|
||||
"light.a": state("light.a", "on"),
|
||||
"light.b": state("light.b", "on"),
|
||||
});
|
||||
const newHass = createHass({
|
||||
"light.a": state("light.a", "off"),
|
||||
"light.b": oldHass.states["light.b"],
|
||||
});
|
||||
const element = {
|
||||
hass: newHass,
|
||||
_config: { entities: ["light.a", "light.b"] },
|
||||
};
|
||||
expect(hasConfigOrEntitiesChanged(element, changedProps(oldHass))).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false when only unconfigured entities change", () => {
|
||||
const sharedA = state("light.a", "on");
|
||||
const oldHass = createHass({
|
||||
"light.a": sharedA,
|
||||
"light.other": state("light.other", "on"),
|
||||
});
|
||||
const newHass = createHass({
|
||||
"light.a": sharedA,
|
||||
"light.other": state("light.other", "off"),
|
||||
});
|
||||
const element = {
|
||||
hass: newHass,
|
||||
_config: { entities: ["light.a"] },
|
||||
};
|
||||
expect(hasConfigOrEntitiesChanged(element, changedProps(oldHass))).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps detecting changes across repeated calls (cached config parsing)", () => {
|
||||
const config = { entities: ["light.a"] };
|
||||
const base = state("light.a", "on");
|
||||
const hass1 = createHass({ "light.a": base });
|
||||
|
||||
// First call: config parsed and cached.
|
||||
expect(
|
||||
hasConfigOrEntitiesChanged(
|
||||
{ hass: hass1, _config: config },
|
||||
changedProps(hass1)
|
||||
)
|
||||
).toBe(false);
|
||||
|
||||
// Second call with a real change still detected (cache must not mask it).
|
||||
const hass2 = createHass({ "light.a": state("light.a", "off") });
|
||||
expect(
|
||||
hasConfigOrEntitiesChanged(
|
||||
{ hass: hass2, _config: config },
|
||||
changedProps(hass1)
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user