fix: address Bugbot review #2 findings

- Invalidate integration data cache from widget interact mutations
  (downloads, dns-hole, media-requests, indexer-manager, smart-home, anchor-notes)
- Redis handler getExpiration: fetch missing tags from Redis (multi-instance coherence)
- saveItemOptions: invalidate board cache after saving
- Remove unused cacheTags.app
- Fix integration data cache comment accuracy
This commit is contained in:
Thomas Camlong committed 2026-08-07 19:08:57 +02:00
1 parent 6b45aca5d5
commit 0f8362c3da
11 files changed
+60 -6

No files matched your search

-1
View File
@@ -10,7 +10,6 @@ describe("cacheTags", () => {
expect(cacheTags.boardList()).toBe("board-list");
expect(cacheTags.user("u1")).toBe("user:u1");
expect(cacheTags.integration("i1")).toBe("integration:i1");
expect(cacheTags.app("a1")).toBe("app:a1");
});
it("boardByName uppercases consistently", () => {
-1
View File
@@ -5,5 +5,4 @@ export const cacheTags = {
boardList: () => "board-list" as const,
user: (userId: string) => `user:${userId}` as const,
integration: (integrationId: string) => `integration:${integrationId}` as const,
app: (appId: string) => `app:${appId}` as const,
} as const;
+2 -2
View File
@@ -1,6 +1,6 @@
// ponytail: module-level cache is single-instance only.
// Multi-instance deployments (REDIS_IS_EXTERNAL=true) bypass this and rely on
// the Redis-backed Next cache handler for cross-instance coordination.
// Multi-instance deployments (REDIS_IS_EXTERNAL=true) bypass this entirely —
// each request calls the upstream fetcher directly.
const isMultiInstance = process.env.REDIS_IS_EXTERNAL === "true";
interface CachedEntry<T> {
@@ -7,6 +7,7 @@ import { anchorNotesListInputSchema, anchorNoteUpdateInputSchema, createIntegrat
import { anchorNoteRequestHandler, anchorNotesListRequestHandler } from "@homarr/request-handler/anchor-notes";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { invalidateIntegrationDataCache } from "../../integration-data-cache";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
const noteIdInput = z.object({
@@ -75,6 +76,7 @@ export const anchorNotesRouter = createTRPCRouter({
...(normalizedContent !== undefined ? { content: normalizedContent } : {}),
});
invalidateIntegrationDataCache(ctx.integration.id);
return updatedNote;
} catch (error) {
if (error instanceof ResponseError && error.statusCode === 400) {
@@ -5,6 +5,7 @@ import { createIntegrationAsync } from "@homarr/integrations";
import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole";
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
import { invalidateIntegrationDataCache } from "../../integration-data-cache";
import { integrationQueryKey, settleIntegrationQueries } from "../../settle-integrations";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
@@ -44,6 +45,7 @@ export const dnsHoleRouter = createTRPCRouter({
.mutation(async ({ ctx: { integration } }) => {
const client = await createIntegrationAsync(integration);
await client.enableAsync();
invalidateIntegrationDataCache(integration.id);
}),
disable: protectedProcedure
@@ -63,5 +65,6 @@ export const dnsHoleRouter = createTRPCRouter({
.mutation(async ({ ctx: { integration }, input }) => {
const client = await createIntegrationAsync(integration);
await client.disableAsync(input.duration);
invalidateIntegrationDataCache(integration.id);
}),
});
@@ -6,6 +6,7 @@ import { downloadClientRequestHandler } from "@homarr/request-handler/downloads"
import type { IntegrationAction } from "../../middlewares/integration";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { invalidateIntegrationDataCache } from "../../integration-data-cache";
import { integrationQueryKey, settleIntegrationQueries } from "../../settle-integrations";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
@@ -53,6 +54,9 @@ export const downloadsRouter = createTRPCRouter({
await integrationInstance.pauseQueueAsync();
}),
);
for (const integration of ctx.integrations) {
invalidateIntegrationDataCache(integration.id);
}
downloadClientRequestHandler.invalidateCache();
}),
pauseItem: protectedProcedure
@@ -65,6 +69,9 @@ export const downloadsRouter = createTRPCRouter({
await integrationInstance.pauseItemAsync(input.item);
}),
);
for (const integration of ctx.integrations) {
invalidateIntegrationDataCache(integration.id);
}
downloadClientRequestHandler.invalidateCache();
}),
resume: protectedProcedure
@@ -83,6 +90,9 @@ export const downloadsRouter = createTRPCRouter({
await integrationInstance.resumeQueueAsync();
}),
);
for (const integration of ctx.integrations) {
invalidateIntegrationDataCache(integration.id);
}
downloadClientRequestHandler.invalidateCache();
}),
resumeItem: protectedProcedure
@@ -95,6 +105,9 @@ export const downloadsRouter = createTRPCRouter({
await integrationInstance.resumeItemAsync(input.item);
}),
);
for (const integration of ctx.integrations) {
invalidateIntegrationDataCache(integration.id);
}
downloadClientRequestHandler.invalidateCache();
}),
deleteItem: protectedProcedure
@@ -107,6 +120,9 @@ export const downloadsRouter = createTRPCRouter({
await integrationInstance.deleteItemAsync(input.item, input.fromDisk);
}),
);
for (const integration of ctx.integrations) {
invalidateIntegrationDataCache(integration.id);
}
downloadClientRequestHandler.invalidateCache();
}),
});
@@ -6,6 +6,7 @@ import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-ma
import type { IntegrationAction } from "../../middlewares/integration";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { invalidateIntegrationDataCache } from "../../integration-data-cache";
import { integrationQueryKey, settleIntegrationQueries } from "../../settle-integrations";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
@@ -45,5 +46,8 @@ export const indexerManagerRouter = createTRPCRouter({
});
}),
);
for (const integration of ctx.integrations) {
invalidateIntegrationDataCache(integration.id);
}
}),
});
@@ -10,6 +10,7 @@ import {
import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats";
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
import { invalidateIntegrationDataCache } from "../../integration-data-cache";
import { integrationQueryKey, settleIntegrationQueries } from "../../settle-integrations";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
@@ -107,5 +108,6 @@ export const mediaRequestsRouter = createTRPCRouter({
} as const;
await answerActions[input.answer](input.requestId);
invalidateIntegrationDataCache(integration.id);
}),
});
@@ -7,6 +7,7 @@ import { getServerSettingsAsync } from "@homarr/db/queries";
import { boards, items } from "@homarr/db/schema";
import type { WidgetOptionsSettings } from "../../../../widgets/src/definition";
import { invalidateBoardCache } from "../../cache-invalidation";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
import { throwIfActionForbiddenAsync } from "../board/board-access";
@@ -51,5 +52,7 @@ export const optionsRouter = createTRPCRouter({
.update(items)
.set({ options: SuperJSON.stringify(options) })
.where(eq(items.id, input.itemId));
invalidateBoardCache(input.boardId);
}),
});
@@ -6,6 +6,7 @@ import { smartHomeEntityStateRequestHandler } from "@homarr/request-handler/smar
import type { IntegrationAction } from "../../middlewares/integration";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { invalidateIntegrationDataCache } from "../../integration-data-cache";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
const createSmartHomeIntegrationMiddleware = (action: IntegrationAction) =>
@@ -41,6 +42,7 @@ export const smartHomeRouter = createTRPCRouter({
const client = await createIntegrationAsync(integration);
const success = await client.triggerToggleAsync(input.entityId);
invalidateIntegrationDataCache(integration.id);
return success;
}),
executeAutomation: protectedProcedure
@@ -56,5 +58,6 @@ export const smartHomeRouter = createTRPCRouter({
.mutation(async ({ ctx: { integration }, input }) => {
const client = await createIntegrationAsync(integration);
await client.triggerAutomationAsync(input.automationId);
invalidateIntegrationDataCache(integration.id);
}),
});
@@ -152,13 +152,36 @@ export class RedisCacheHandler implements CacheHandler {
async getExpiration(tags: string[]): Promise<number> {
if (tags.length === 0) return 0;
const missingTags: string[] = [];
let maxTimestamp = 0;
for (const tag of tags) {
const cached = this.tagCache.get(tag);
if (cached && cached > maxTimestamp) {
maxTimestamp = cached;
if (cached !== undefined) {
if (cached > maxTimestamp) maxTimestamp = cached;
} else {
missingTags.push(tag);
}
}
if (missingTags.length > 0) {
try {
await this.redis.connect().catch(() => {});
const keys = missingTags.map((t) => this.tagKey(t));
const values = await this.redis.mget(...keys);
for (let i = 0; i < missingTags.length; i++) {
const val = values[i];
if (val) {
const ts = Number(val);
this.tagCache.set(missingTags[i]!, ts);
if (ts > maxTimestamp) maxTimestamp = ts;
}
}
} catch {
// Fail open
}
}
return maxTimestamp;
}