fix: address PR review findings

- Strip integration secrets from server cache keys (security)
- Add max size + TTL eviction to in-process cache Map (memory)
- Scope widget refresh + cache age to current widget kind (UX)
- Remove double caching in update-checker (simplicity)
- Simplify timetable router (verbosity)
- Fix update-checker import path
This commit is contained in:
Thomas Camlong committed 2026-07-03 09:59:08 +02:00
1 parent 0fd495c277
commit 16f82fcc6f
6 files changed
+33 -34

No files matched your search

@@ -21,8 +21,10 @@ export const createIntegrationRequestHandler = <
>(
options: Options<TData, TKind, TInput>,
) => ({
handler: (integration: IntegrationOfKind<TKind>, itemOptions: TInput) =>
createRequestHandler<TData, { options: TInput; integration: IntegrationOfKind<TKind> }>({
requestAsync: async (input) => options.requestAsync(input.integration, input.options),
}).handler({ options: itemOptions, integration }),
handler: (integration: IntegrationOfKind<TKind>, itemOptions: TInput) => {
const inner = createRequestHandler<TData, { integrationId: string; options: TInput }>({
requestAsync: async (input) => options.requestAsync(integration, input.options),
});
return inner.handler({ integrationId: integration.id, options: itemOptions });
},
});
@@ -5,11 +5,19 @@ interface Options<TData, TInput extends Record<string, unknown>> {
type CacheEntry<TData> = { data: TData; timestamp: Date; expiresAt: number };
const MAX_CACHE_SIZE = 1000;
const cache = new Map<string, CacheEntry<unknown>>();
const inflight = new Map<string, Promise<CacheEntry<unknown>>>();
const DEFAULT_TTL_MS = 10_000;
const evictExpired = () => {
const now = Date.now();
for (const [key, entry] of cache) {
if (now >= entry.expiresAt) cache.delete(key);
}
};
export const createRequestHandler = <TData, TInput extends Record<string, unknown>>(
options: Options<TData, TInput>,
) => ({
@@ -22,6 +30,7 @@ export const createRequestHandler = <TData, TInput extends Record<string, unknow
if (cached && Date.now() < cached.expiresAt) {
return { data: cached.data, timestamp: cached.timestamp };
}
if (cached) cache.delete(key);
const existing = inflight.get(key);
if (existing) return existing as Promise<CacheEntry<TData>>;
@@ -29,6 +38,11 @@ export const createRequestHandler = <TData, TInput extends Record<string, unknow
const promise = options
.requestAsync(input)
.then((data) => {
if (cache.size >= MAX_CACHE_SIZE) evictExpired();
if (cache.size >= MAX_CACHE_SIZE) {
const oldest = cache.keys().next().value;
if (oldest) cache.delete(oldest);
}
const entry: CacheEntry<TData> = { data, timestamp: new Date(), expiresAt: Date.now() + ttl };
cache.set(key, entry as CacheEntry<unknown>);
inflight.delete(key);
@@ -5,24 +5,18 @@ import { env } from "@homarr/common/env";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import { createRequestHandler } from "@homarr/request-handler/lib/request-handler";
import { createRequestHandler } from "./lib/request-handler";
import packageJson from "../../../package.json";
const logger = createLogger({ module: "updateCheckerRequestHandler" });
const CACHE_TTL_MS = 30 * 60 * 1000;
let cached: { data: Update[]; expiresAt: number } | null = null;
export const updateCheckerRequestHandler = createRequestHandler({
async requestAsync(_) {
if (cached && Date.now() < cached.expiresAt) {
return { availableUpdates: cached.data };
}
const availableUpdates = await getAvailableUpdatesAsync(packageJson.version);
cached = { data: availableUpdates, expiresAt: Date.now() + CACHE_TTL_MS };
return { availableUpdates };
},
cacheTtlMs: 30 * 60 * 1000,
});
interface Update {