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",
|
"marked": "15.0.7",
|
||||||
"memoize-one": "6.0.0",
|
"memoize-one": "6.0.0",
|
||||||
"node-vibrant": "4.0.3",
|
"node-vibrant": "4.0.3",
|
||||||
|
"object-hash": "3.0.0",
|
||||||
"punycode": "2.3.1",
|
"punycode": "2.3.1",
|
||||||
"qr-scanner": "1.4.2",
|
"qr-scanner": "1.4.2",
|
||||||
"qrcode": "1.5.4",
|
"qrcode": "1.5.4",
|
||||||
|
@ -215,7 +216,6 @@
|
||||||
"lodash.merge": "4.6.2",
|
"lodash.merge": "4.6.2",
|
||||||
"lodash.template": "4.5.0",
|
"lodash.template": "4.5.0",
|
||||||
"map-stream": "0.0.7",
|
"map-stream": "0.0.7",
|
||||||
"object-hash": "3.0.0",
|
|
||||||
"pinst": "3.0.0",
|
"pinst": "3.0.0",
|
||||||
"prettier": "3.5.1",
|
"prettier": "3.5.1",
|
||||||
"rspack-manifest-plugin": "5.0.3",
|
"rspack-manifest-plugin": "5.0.3",
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
|
import type { PropertyValues } from "lit";
|
||||||
import { ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import hash from "object-hash";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { renderMarkdown } from "../resources/render-markdown";
|
import { renderMarkdown } from "../resources/render-markdown";
|
||||||
|
import { CacheManager } from "../util/cache-manager";
|
||||||
|
|
||||||
|
const markdownCache = new CacheManager<string>(1000);
|
||||||
|
|
||||||
const _gitHubMarkdownAlerts = {
|
const _gitHubMarkdownAlerts = {
|
||||||
reType:
|
reType:
|
||||||
|
@ -26,6 +31,16 @@ class HaMarkdownElement extends ReactiveElement {
|
||||||
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
|
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
|
||||||
false;
|
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() {
|
protected createRenderRoot() {
|
||||||
return this;
|
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() {
|
private async _render() {
|
||||||
this.innerHTML = await renderMarkdown(
|
this.innerHTML = await renderMarkdown(
|
||||||
String(this.content),
|
String(this.content),
|
||||||
|
|
|
@ -13,6 +13,8 @@ export class HaMarkdown extends LitElement {
|
||||||
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
|
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
|
||||||
false;
|
false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public cache = false;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.content) {
|
if (!this.content) {
|
||||||
return nothing;
|
return nothing;
|
||||||
|
@ -23,6 +25,7 @@ export class HaMarkdown extends LitElement {
|
||||||
.allowSvg=${this.allowSvg}
|
.allowSvg=${this.allowSvg}
|
||||||
.breaks=${this.breaks}
|
.breaks=${this.breaks}
|
||||||
.lazyImages=${this.lazyImages}
|
.lazyImages=${this.lazyImages}
|
||||||
|
.cache=${this.cache}
|
||||||
></ha-markdown-element>`;
|
></ha-markdown-element>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,17 +3,21 @@ import type { PropertyValues } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
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 { 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-card";
|
||||||
import "../../../components/ha-markdown";
|
import "../../../components/ha-markdown";
|
||||||
import "../../../components/ha-alert";
|
|
||||||
import type { RenderTemplateResult } from "../../../data/ws-templates";
|
import type { RenderTemplateResult } from "../../../data/ws-templates";
|
||||||
import { subscribeRenderTemplate } from "../../../data/ws-templates";
|
import { subscribeRenderTemplate } from "../../../data/ws-templates";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import { CacheManager } from "../../../util/cache-manager";
|
||||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||||
import type { MarkdownCardConfig } from "./types";
|
import type { MarkdownCardConfig } from "./types";
|
||||||
|
|
||||||
|
const templateCache = new CacheManager<RenderTemplateResult>(1000);
|
||||||
|
|
||||||
@customElement("hui-markdown-card")
|
@customElement("hui-markdown-card")
|
||||||
export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||||
|
@ -68,9 +72,32 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||||
this._tryConnect();
|
this._tryConnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _computeCacheKey() {
|
||||||
|
return hash(this._config);
|
||||||
|
}
|
||||||
|
|
||||||
public disconnectedCallback() {
|
public disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this._tryDisconnect();
|
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() {
|
protected render() {
|
||||||
|
@ -87,6 +114,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||||
: nothing}
|
: nothing}
|
||||||
<ha-card .header=${this._config.title}>
|
<ha-card .header=${this._config.title}>
|
||||||
<ha-markdown
|
<ha-markdown
|
||||||
|
cache
|
||||||
breaks
|
breaks
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
"no-header": !this._config.title,
|
"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