perf(api): dedup public-resume view increments within a short window

This commit is contained in:
Amruth Pillai
2026-07-08 15:55:16 +02:00
parent c9b3fa5c10
commit 97ab3c9732
3 changed files with 103 additions and 1 deletions
+5 -1
View File
@@ -17,6 +17,7 @@ import { getStorageService } from "../storage/service";
import { grantResumeAccess, hasResumeAccess } from "./access";
import { assertCanView, isOwner, redactResumeForViewer, shouldCountForStatistics } from "./access-policy";
import { publishResumeUpdated } from "./events";
import { clientKeyFromHeaders, shouldCountView } from "./view-dedup";
type DbOrTx = typeof db | Parameters<Parameters<typeof db.transaction>[0]>[0];
@@ -503,7 +504,10 @@ export const resumeService = {
}
if (shouldCountForStatistics(resume, viewer)) {
await resumeService.statistics.increment({ id: resume.id, views: true });
const key = `${resume.id}:${clientKeyFromHeaders(input.requestHeaders)}`;
if (shouldCountView(key, Date.now())) {
await resumeService.statistics.increment({ id: resume.id, views: true });
}
}
return toSharedResumeResponse(redactResumeForViewer(resume, isOwner(resume, viewer)), resume.hasPassword);
@@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import { clientKeyFromHeaders, shouldCountView } from "./view-dedup";
// `seen` is module-level state shared across tests, so each test uses a unique key.
const WINDOW_MS = 60 * 60 * 1000;
describe("shouldCountView", () => {
it("counts the first view, skips repeats within the window, counts again after it", () => {
const key = "resume-1:viewer-a";
const t0 = 1_000_000;
expect(shouldCountView(key, t0)).toBe(true);
expect(shouldCountView(key, t0 + 1)).toBe(false);
expect(shouldCountView(key, t0 + WINDOW_MS - 1)).toBe(false);
// Once now is past the window, the same key counts again.
expect(shouldCountView(key, t0 + WINDOW_MS + 1)).toBe(true);
});
it("treats different keys independently", () => {
const t0 = 2_000_000;
expect(shouldCountView("resume-2:viewer-a", t0)).toBe(true);
expect(shouldCountView("resume-2:viewer-b", t0)).toBe(true);
expect(shouldCountView("resume-2:viewer-a", t0 + 1)).toBe(false);
});
});
describe("clientKeyFromHeaders", () => {
it("derives distinct keys from distinct trusted-IP headers", () => {
const a = clientKeyFromHeaders(new Headers({ "X-Forwarded-For": "1.1.1.1" }));
const b = clientKeyFromHeaders(new Headers({ "X-Forwarded-For": "2.2.2.2" }));
expect(a).toBe("ip:1.1.1.1");
expect(a).not.toBe(b);
});
it("uses the first IP from a comma-delimited proxy chain", () => {
const key = clientKeyFromHeaders(new Headers({ "X-Forwarded-For": "3.3.3.3, 10.0.0.1" }));
expect(key).toBe("ip:3.3.3.3");
});
it("falls back to a stable user-agent fingerprint when no trusted IP header is present", () => {
const headers = new Headers({ "user-agent": "Mozilla/5.0", "accept-language": "en-US,en" });
const first = clientKeyFromHeaders(headers);
const second = clientKeyFromHeaders(headers);
expect(first).toBe(second);
expect(first.startsWith("fp:")).toBe(true);
// A different UA yields a different fallback key.
expect(clientKeyFromHeaders(new Headers({ "user-agent": "curl/8" }))).not.toBe(first);
});
});
@@ -0,0 +1,45 @@
import { TRUSTED_IP_HEADERS } from "@reactive-resume/utils/rate-limit";
// ponytail: in-memory per-process dedup window. Single-instance is the default deploy; for
// multi-instance, swap the Map for a Redis SETNX+EXPIRE keyed the same way (REDIS_URL already
// exists in env). Upgrade only if you scale out — each instance dedups independently otherwise.
const WINDOW_MS = 60 * 60 * 1000; // 1 hour
const MAX_ENTRIES = 50_000; // bound the Map; prune expired entries before growing past this.
const seen = new Map<string, number>(); // key -> expiry timestamp
/**
* Returns `true` at most once per `key` per window. `now` is a parameter (not `Date.now()`)
* so callers stay testable with a driven clock.
*/
export function shouldCountView(key: string, now: number): boolean {
const expiry = seen.get(key);
if (expiry !== undefined && expiry > now) return false;
if (seen.size >= MAX_ENTRIES) {
for (const [k, exp] of seen) {
if (exp <= now) seen.delete(k);
}
}
seen.set(key, now + WINDOW_MS);
return true;
}
// Mirrors the rate-limit middleware's client-key derivation so dedup and rate limiting agree on
// "who is this viewer": trusted proxy IP first, then a user-agent + language fingerprint fallback.
export function clientKeyFromHeaders(headers: Headers): string {
for (const headerName of TRUSTED_IP_HEADERS) {
const raw = headers.get(headerName)?.trim();
if (!raw) continue;
// Some proxies provide a comma-delimited chain; the first item is the original client.
const ip = raw.split(",")[0]?.trim();
if (ip) return `ip:${ip}`;
}
const userAgent = headers.get("user-agent")?.trim() ?? "unknown";
const language = headers.get("accept-language")?.split(",")[0]?.trim() ?? "none";
return `fp:${userAgent.slice(0, 64)}:${language.slice(0, 16)}`;
}