Add performance optimizations and component refactors
- Cache read-heavy public API routes via redisCache (leaderboard, values, shop, articles, photos, guilds, teams, staff, users, home, radio, badges) - Add single-flight and bounded-memory cache layer with unit tests - Parallelize independent DB queries on search, rares, shop, staff, polls and profile pages - Push radio points leaderboard aggregation to SQL with a LIMIT - Split studio-client and import-furni-client into focused modules - Clean up next.config.ts
This commit is contained in:
1 parent
54f2bc5e0c
commit
f285a7cd98
42 files changed
+1616
-1205
No files matched your search
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/redis", () => ({
|
||||
redis: null,
|
||||
}));
|
||||
|
||||
import { cached, invalidateMemory } from "./cache";
|
||||
|
||||
describe("cached (memory-only, no Redis)", () => {
|
||||
it("computes once and serves the cached value", async () => {
|
||||
const fn = vi.fn(async () => 42);
|
||||
const key = `once-${Math.random()}`;
|
||||
expect(await cached(key, 10_000, fn)).toBe(42);
|
||||
expect(await cached(key, 10_000, fn)).toBe(42);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("dedupes concurrent misses into a single computation", async () => {
|
||||
let resolveFn: (value: number) => void = () => {};
|
||||
const fn = vi.fn(
|
||||
() =>
|
||||
new Promise<number>((resolve) => {
|
||||
resolveFn = resolve;
|
||||
}),
|
||||
);
|
||||
const key = `dedupe-${Math.random()}`;
|
||||
const p1 = cached(key, 10_000, fn);
|
||||
const p2 = cached(key, 10_000, fn);
|
||||
const p3 = cached(key, 10_000, fn);
|
||||
resolveFn(7);
|
||||
expect(await Promise.all([p1, p2, p3])).toEqual([7, 7, 7]);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("recomputes after the TTL expires", async () => {
|
||||
let count = 0;
|
||||
const fn = vi.fn(async () => ++count);
|
||||
const key = `ttl-${Math.random()}`;
|
||||
expect(await cached(key, 1, fn)).toBe(1);
|
||||
expect(await cached(key, 1, fn)).toBe(1);
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
expect(await cached(key, 1, fn)).toBe(2);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("invalidates a key so the next read recomputes", async () => {
|
||||
let count = 0;
|
||||
const fn = vi.fn(async () => ++count);
|
||||
const key = `inv-${Math.random()}`;
|
||||
expect(await cached(key, 10_000, fn)).toBe(1);
|
||||
invalidateMemory(key);
|
||||
expect(await cached(key, 10_000, fn)).toBe(2);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("evicts the oldest entry when the in-memory cache is full", async () => {
|
||||
const fn = vi.fn(async () => 1);
|
||||
for (let i = 0; i < 600; i++) {
|
||||
await cached(`bulk-${i}`, 10_000, fn);
|
||||
}
|
||||
// Key "bulk-0" was evicted (insertion order), so it must recompute.
|
||||
await cached("bulk-0", 10_000, fn);
|
||||
expect(fn).toHaveBeenCalledTimes(601);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user