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:
11 files changed
+60
-6
No files matched your search
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user