Bust UserCard cache when banner changed

This commit is contained in:
Kalle
2026-08-08 14:01:18 +03:00
parent 5d2010057d
commit 830c31d13c
2 changed files with 31 additions and 13 deletions

View File

@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it } from "vitest";
import * as ImageFactory from "~/db/seed/factories/ImageFactory";
import * as UserFactory from "~/db/seed/factories/UserFactory";
import * as XRankPlacementFactory from "~/db/seed/factories/XRankPlacementFactory";
import { db } from "~/db/sql";
import * as PrivateUserNoteRepository from "~/features/sendouq/PrivateUserNoteRepository.server";
import { withNoUser, withUserId } from "~/utils/Test";
import * as UserCardRepository from "./UserCardRepository.server";
@@ -396,15 +397,8 @@ describe("UserCardRepository.findAllByUserIdsCached", () => {
const first = await cachedCard(target.id);
expect(first.userCards.get(target.id)?.shortBio).toBeNull();
await withUserId(target.id, () =>
UserCardRepository.updateOwnCard({
shortBio: "edited",
bannerPresetImg: null,
bannerImgId: null,
unverifiedPeakXP: null,
hiddenCardStats: [],
}),
);
// written past the repository so the cached card is left in place
await updateShortBioDirectly(target.id, "edited");
const { userCards } = await withNoUser(() =>
UserCardRepository.findAllByUserIdsCached({
@@ -419,16 +413,26 @@ describe("UserCardRepository.findAllByUserIdsCached", () => {
it("returns a fresh card once the cached one has expired", async () => {
await cachedCard(target.id);
await updateShortBioDirectly(target.id, "edited");
UserCardRepository.clearUserCardCache();
const { userCards } = await cachedCard(target.id);
expect(userCards.get(target.id)?.shortBio).toBe("edited");
});
it("returns a fresh card right after its owner edited it", async () => {
await cachedCard(target.id);
await withUserId(target.id, () =>
UserCardRepository.updateOwnCard({
shortBio: "edited",
bannerPresetImg: null,
bannerImgId: null,
unverifiedPeakXP: null,
xpDivision: null,
hiddenCardStats: [],
}),
);
UserCardRepository.clearUserCardCache();
const { userCards } = await cachedCard(target.id);
expect(userCards.get(target.id)?.shortBio).toBe("edited");
@@ -446,3 +450,12 @@ describe("UserCardRepository.findAllByUserIdsCached", () => {
);
});
});
function updateShortBioDirectly(userId: number, shortBio: string) {
// biome-ignore lint/plugin: updateOwnCard invalidates the cached card, which these tests need to leave in place
return db
.updateTable("User")
.set({ shortBio })
.where("id", "=", userId)
.execute();
}

View File

@@ -273,8 +273,11 @@ export async function findVerifiedXpByUserId(
return verifiedXp(row?.xpPeaks ?? null, xpDivision);
}
/** Updates the editable user card fields of the acting user (their own card). */
export function updateOwnCard(args: {
/**
* Updates the editable user card fields of the acting user (their own card), dropping their
* cached card so they are not shown their pre-edit one by {@link findAllByUserIdsCached}.
*/
export async function updateOwnCard(args: {
shortBio: string | null;
bannerPresetImg: string | null;
bannerImgId: number | null;
@@ -284,7 +287,7 @@ export function updateOwnCard(args: {
hiddenCardStats: Array<HideableUserCardStat>;
}) {
const userId = actorId();
return db.transaction().execute(async (trx) => {
await db.transaction().execute(async (trx) => {
// a removed or replaced uploaded banner is no longer referenced by anything,
// so its submitted image row is cleaned up (mirrors custom avatar handling)
const current = await trx
@@ -318,6 +321,8 @@ export function updateOwnCard(args: {
.where("id", "=", userId)
.execute();
});
cardCache.delete(userId);
}
/** SQLite `case` expression mapping `User.id % PRESET_COLORS.length` to a preset banner color. */