Add cache for markdown card and markdown element (#24217)
* Add cache for markdown card and markdown element * Rename to expiration * Only use cache logic for markdown card * Add tests * Improve testspull/24278/head
parent
00d0cb7afa
commit
3ee3cfa6cb
|
@ -126,6 +126,7 @@
|
|||
"marked": "15.0.7",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
"object-hash": "3.0.0",
|
||||
"punycode": "2.3.1",
|
||||
"qr-scanner": "1.4.2",
|
||||
"qrcode": "1.5.4",
|
||||
|
@ -215,7 +216,6 @@
|
|||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
"map-stream": "0.0.7",
|
||||
"object-hash": "3.0.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.5.1",
|
||||
"rspack-manifest-plugin": "5.0.3",
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import hash from "object-hash";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { renderMarkdown } from "../resources/render-markdown";
|
||||
import { CacheManager } from "../util/cache-manager";
|
||||
|
||||
const markdownCache = new CacheManager<string>(1000);
|
||||
|
||||
const _gitHubMarkdownAlerts = {
|
||||
reType:
|
||||
|
@ -26,6 +31,16 @@ class HaMarkdownElement extends ReactiveElement {
|
|||
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean }) public cache = false;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.cache) {
|
||||
const key = this._computeCacheKey();
|
||||
markdownCache.set(key, this.innerHTML);
|
||||
}
|
||||
}
|
||||
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
@ -37,6 +52,24 @@ class HaMarkdownElement extends ReactiveElement {
|
|||
}
|
||||
}
|
||||
|
||||
protected willUpdate(_changedProperties: PropertyValues): void {
|
||||
if (!this.innerHTML && this.cache) {
|
||||
const key = this._computeCacheKey();
|
||||
if (markdownCache.has(key)) {
|
||||
this.innerHTML = markdownCache.get(key)!;
|
||||
this._resize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _computeCacheKey() {
|
||||
return hash({
|
||||
content: this.content,
|
||||
allowSvg: this.allowSvg,
|
||||
breaks: this.breaks,
|
||||
});
|
||||
}
|
||||
|
||||
private async _render() {
|
||||
this.innerHTML = await renderMarkdown(
|
||||
String(this.content),
|
||||
|
|
|
@ -13,6 +13,8 @@ export class HaMarkdown extends LitElement {
|
|||
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean }) public cache = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.content) {
|
||||
return nothing;
|
||||
|
@ -23,6 +25,7 @@ export class HaMarkdown extends LitElement {
|
|||
.allowSvg=${this.allowSvg}
|
||||
.breaks=${this.breaks}
|
||||
.lazyImages=${this.lazyImages}
|
||||
.cache=${this.cache}
|
||||
></ha-markdown-element>`;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,17 +3,21 @@ import type { PropertyValues } from "lit";
|
|||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import hash from "object-hash";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-alert";
|
||||
import type { RenderTemplateResult } from "../../../data/ws-templates";
|
||||
import { subscribeRenderTemplate } from "../../../data/ws-templates";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { CacheManager } from "../../../util/cache-manager";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import type { MarkdownCardConfig } from "./types";
|
||||
|
||||
const templateCache = new CacheManager<RenderTemplateResult>(1000);
|
||||
|
||||
@customElement("hui-markdown-card")
|
||||
export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
|
@ -68,9 +72,32 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
|||
this._tryConnect();
|
||||
}
|
||||
|
||||
private _computeCacheKey() {
|
||||
return hash(this._config);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._tryDisconnect();
|
||||
|
||||
if (this._config && this._templateResult) {
|
||||
const key = this._computeCacheKey();
|
||||
templateCache.set(key, this._templateResult);
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(_changedProperties: PropertyValues): void {
|
||||
super.willUpdate(_changedProperties);
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._templateResult) {
|
||||
const key = this._computeCacheKey();
|
||||
if (templateCache.has(key)) {
|
||||
this._templateResult = templateCache.get(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
@ -87,6 +114,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
|||
: nothing}
|
||||
<ha-card .header=${this._config.title}>
|
||||
<ha-markdown
|
||||
cache
|
||||
breaks
|
||||
class=${classMap({
|
||||
"no-header": !this._config.title,
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
export class CacheManager<T> {
|
||||
constructor(expiration?: number) {
|
||||
this._expiration = expiration;
|
||||
}
|
||||
|
||||
private _expiration?: number;
|
||||
|
||||
private _cache = new Map<string, T>();
|
||||
|
||||
public get(key: string): T | undefined {
|
||||
return this._cache.get(key);
|
||||
}
|
||||
|
||||
public set(key: string, value: T): void {
|
||||
this._cache.set(key, value);
|
||||
if (this._expiration) {
|
||||
window.setTimeout(() => this._cache.delete(key), this._expiration);
|
||||
}
|
||||
}
|
||||
|
||||
public has(key: string): boolean {
|
||||
return this._cache.has(key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { CacheManager } from "../../src/util/cache-manager";
|
||||
|
||||
const savedSetTimeout = setTimeout;
|
||||
|
||||
describe("cache-manager", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
window.setTimeout = setTimeout;
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
window.setTimeout = savedSetTimeout;
|
||||
});
|
||||
it("should return value before expiration", async () => {
|
||||
const cacheManager = new CacheManager<string>(1000);
|
||||
cacheManager.set("key", "value");
|
||||
|
||||
expect(cacheManager.has("key")).toBe(true);
|
||||
expect(cacheManager.get("key")).toBe("value");
|
||||
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(cacheManager.has("key")).toBe(true);
|
||||
expect(cacheManager.get("key")).toBe("value");
|
||||
});
|
||||
|
||||
it("should not return value after expiration", async () => {
|
||||
const cacheManager = new CacheManager<string>(1000);
|
||||
cacheManager.set("key", "value");
|
||||
|
||||
expect(cacheManager.has("key")).toBe(true);
|
||||
expect(cacheManager.get("key")).toBe("value");
|
||||
|
||||
vi.advanceTimersByTime(2000);
|
||||
expect(cacheManager.has("key")).toBe(false);
|
||||
expect(cacheManager.get("key")).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should always return value if no expiration", async () => {
|
||||
const cacheManager = new CacheManager<string>();
|
||||
cacheManager.set("key", "value");
|
||||
|
||||
expect(cacheManager.has("key")).toBe(true);
|
||||
expect(cacheManager.get("key")).toBe("value");
|
||||
|
||||
vi.advanceTimersByTime(10000000000000000000000);
|
||||
expect(cacheManager.has("key")).toBe(true);
|
||||
expect(cacheManager.get("key")).toBe("value");
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue